From c981ee7dde810a3d6619f34d0e1f20b2e15fe8fa Mon Sep 17 00:00:00 2001 From: Coleman Watts 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 --- .../SearchDisplay/AbstractRunAction.php | 62 ++++++++++++++----- .../crmSearchDisplayEditable.component.js | 2 +- 2 files changed, 49 insertions(+), 15 deletions(-) diff --git a/ext/search_kit/Civi/Api4/Action/SearchDisplay/AbstractRunAction.php b/ext/search_kit/Civi/Api4/Action/SearchDisplay/AbstractRunAction.php index 0ed8a5642c8c..71d7c0bad3da 100644 --- a/ext/search_kit/Civi/Api4/Action/SearchDisplay/AbstractRunAction.php +++ b/ext/search_kit/Civi/Api4/Action/SearchDisplay/AbstractRunAction.php @@ -441,35 +441,68 @@ 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, 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; + // Get values for creation from the join clause + $join = array_filter($this->getQuery()->getJoin(), function($join) use ($editable) { + return (explode(' AS ', $join[0])[1] ?? '') === $editable['explicit_join']; + })[0] ?? []; + foreach ($join 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, 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, 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 @@ -495,6 +528,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 052b9dcfbc5f..3ba5b34ca48c 100644 --- a/ext/search_kit/ang/crmSearchDisplay/crmSearchDisplayEditable.component.js +++ b/ext/search_kit/ang/crmSearchDisplay/crmSearchDisplayEditable.component.js @@ -58,7 +58,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() {