From 35ceb7e68991139f05cee8cd283afdb2a8ed12d7 Mon Sep 17 00:00:00 2001 From: Coleman Watts <coleman@civicrm.org> Date: Tue, 4 Jan 2022 21:25:34 -0500 Subject: [PATCH] SearchKit - Allow creation of new records via in-place edit Allows e.g. an email record to be created if one does not already exist. Fixes dev/core#2853 --- Civi/Api4/Query/Api4SelectQuery.php | 3 +- .../SearchDisplay/AbstractRunAction.php | 61 ++++++++++++++----- .../crmSearchDisplayEditable.component.js | 2 +- 3 files changed, 50 insertions(+), 16 deletions(-) diff --git a/Civi/Api4/Query/Api4SelectQuery.php b/Civi/Api4/Query/Api4SelectQuery.php index a6e0eaed088b..76a3842ee7da 100644 --- a/Civi/Api4/Query/Api4SelectQuery.php +++ b/Civi/Api4/Query/Api4SelectQuery.php @@ -751,6 +751,7 @@ private function addExplicitJoins() { 'alias' => $alias, 'table' => $tableName, 'bridge' => NULL, + 'on' => array_filter(array_filter($join, 'is_array')), ]; // If the first condition is a string, it's the name of a bridge entity if (!empty($join[0]) && is_string($join[0]) && \CRM_Utils_Rule::alphanumeric($join[0])) { @@ -758,7 +759,7 @@ private function addExplicitJoins() { } else { $conditions = $this->getJoinConditions($join, $entity, $alias, $joinEntityFields); - foreach (array_filter($join) as $clause) { + foreach ($join['on'] as $clause) { $conditions[] = $this->treeWalkClauses($clause, 'ON'); } $this->join($side, $tableName, $alias, $conditions); diff --git a/ext/search_kit/Civi/Api4/Action/SearchDisplay/AbstractRunAction.php b/ext/search_kit/Civi/Api4/Action/SearchDisplay/AbstractRunAction.php index 4d95db8520d3..74dbc046b646 100644 --- a/ext/search_kit/Civi/Api4/Action/SearchDisplay/AbstractRunAction.php +++ b/ext/search_kit/Civi/Api4/Action/SearchDisplay/AbstractRunAction.php @@ -488,35 +488,67 @@ private function getUrl(string $path, $query = NULL) { /** * @param $column * @param $data - * @return array{entity: string, input_type: string, data_type: string, options: bool, serialize: bool, fk_entity: string, value_key: string, record: array, value: mixed}|null + * @return array{entity: string, action: string, input_type: string, data_type: string, options: bool, serialize: bool, nullable: bool, fk_entity: string, value_key: string, record: array, value: mixed}|null */ private function formatEditableColumn($column, $data) { $editable = $this->getEditableInfo($column['key']); + $editable['record'] = []; + // Generate params to edit existing record if (!empty($data[$editable['id_path']])) { + $editable['action'] = 'update'; + $editable['record'][$editable['id_key']] = $data[$editable['id_path']]; + $editable['value'] = $data[$editable['value_path']]; + } + // Generate params to create new record, if applicable + elseif ($editable['explicit_join']) { + $editable['action'] = 'create'; + $editable['value'] = NULL; + $editable['nullable'] = FALSE; + // Get values for creation from the join clause + $join = $this->getQuery()->getExplicitJoin($editable['explicit_join']); + foreach ($join['on'] ?? [] as $clause) { + if (is_array($clause) && count($clause) === 3 && $clause[1] === '=') { + // Because clauses are reversible, check both directions to see which side has a fieldName belonging to this join + foreach ([0 => 2, 2 => 0] as $field => $value) { + if (strpos($clause[$field], $editable['explicit_join'] . '.') === 0) { + $fieldName = substr($clause[$field], strlen($editable['explicit_join']) + 1); + // If the value is a field, get it from the data + if (isset($data[$clause[$value]])) { + $editable['record'][$fieldName] = $data[$clause[$value]]; + } + // If it's a literal bool or number + elseif (is_bool($clause[$value]) || is_numeric($clause[$value])) { + $editable['record'][$fieldName] = $clause[$value]; + } + // If it's a literal string it will be quoted + elseif (is_string($clause[$value]) && in_array($clause[$value][0], ['"', "'"], TRUE) && substr($clause[$value], -1) === $clause[$value][0]) { + $editable['record'][$fieldName] = substr($clause[$value], 1, -1); + } + } + } + } + } + } + // Ensure current user has access + if ($editable['record']) { $access = civicrm_api4($editable['entity'], 'checkAccess', [ - 'action' => 'update', - 'values' => [ - $editable['id_key'] => $data[$editable['id_path']], - ], + 'action' => $editable['action'], + 'values' => $editable['record'], ], 0)['access']; - if (!$access) { - return NULL; + if ($access) { + \CRM_Utils_Array::remove($editable, 'id_key', 'id_path', 'value_path', 'explicit_join'); + return $editable; } - $editable['record'] = [ - $editable['id_key'] => $data[$editable['id_path']], - ]; - $editable['value'] = $data[$editable['value_path']]; - \CRM_Utils_Array::remove($editable, 'id_key', 'id_path', 'value_path'); - return $editable; } return NULL; } /** * @param $key - * @return array{entity: string, input_type: string, data_type: string, options: bool, serialize: bool, nullable: bool, fk_entity: string, value_key: string, value_path: string, id_key: string, id_path: string}|null + * @return array{entity: string, input_type: string, data_type: string, options: bool, serialize: bool, nullable: bool, fk_entity: string, value_key: string, value_path: string, id_key: string, id_path: string, explicit_join: string}|null */ private function getEditableInfo($key) { + // Strip pseudoconstant suffix [$key] = explode(':', $key); $field = $this->getField($key); // If field is an implicit join to another entity (not a custom group), use the original fk field @@ -543,6 +575,7 @@ private function getEditableInfo($key) { 'value_path' => $key, 'id_key' => $idKey, 'id_path' => $idPath, + 'explicit_join' => $field['explicit_join'], ]; } return NULL; diff --git a/ext/search_kit/ang/crmSearchDisplay/crmSearchDisplayEditable.component.js b/ext/search_kit/ang/crmSearchDisplay/crmSearchDisplayEditable.component.js index f205e4a3aa05..cedb4db87d53 100644 --- a/ext/search_kit/ang/crmSearchDisplay/crmSearchDisplayEditable.component.js +++ b/ext/search_kit/ang/crmSearchDisplay/crmSearchDisplayEditable.component.js @@ -57,7 +57,7 @@ var record = _.cloneDeep(col.edit.record); record[col.edit.value_key] = ctrl.value; $('input', $element).attr('disabled', true); - ctrl.doSave({apiCall: [col.edit.entity, 'update', {values: record}]}); + ctrl.doSave({apiCall: [col.edit.entity, col.edit.action, {values: record}]}); }; function loadOptions() {