From b2ee26f099a6c8c555189dd2fe8972a0c51cebd2 Mon Sep 17 00:00:00 2001 From: Coleman Watts Date: Tue, 9 Mar 2021 10:42:31 -0500 Subject: [PATCH] SearchKit - Add "in-place edit" feature --- css/civicrm.css | 3 + .../Civi/Api4/Action/SearchDisplay/Run.php | 6 ++ ext/search/Civi/Search/Admin.php | 2 +- .../crmSearchActionUpdate.ctrl.js | 1 + .../crmSearchAdminDisplay.component.js | 41 ++++++++- .../crmSearchAdminLinkSelect.component.js | 1 + .../displays/colType/field.html | 12 ++- ext/search/ang/crmSearchDisplay.module.js | 4 +- .../ang/crmSearchDisplay/colType/field.html | 3 +- .../crmSearchDisplayEditable.component.js | 83 +++++++++++++++++++ .../crmSearchDisplayEditable.html | 9 ++ .../crmSearchDisplayList.component.js | 11 ++- .../crmSearchDisplayList.html | 8 +- .../crmSearchDisplayListItems.html | 2 +- .../crmSearchDisplayTable.component.js | 21 ++++- .../crmSearchDisplayTable.html | 54 ++++++------ ext/search/css/crmSearchActions.css | 19 +++++ 17 files changed, 238 insertions(+), 42 deletions(-) create mode 100644 ext/search/ang/crmSearchDisplay/crmSearchDisplayEditable.component.js create mode 100644 ext/search/ang/crmSearchDisplay/crmSearchDisplayEditable.html diff --git a/css/civicrm.css b/css/civicrm.css index 9a4ae97a4e..c928b5fb5b 100644 --- a/css/civicrm.css +++ b/css/civicrm.css @@ -3358,6 +3358,9 @@ span.crm-select-item-color { } .crm-container span.crm-editable-enabled { display: inline-block !important; + padding-right: 2px; + min-height: 1em; + min-width: 3em; } .crm-container .crm-editable-enabled .crm-i { diff --git a/ext/search/Civi/Api4/Action/SearchDisplay/Run.php b/ext/search/Civi/Api4/Action/SearchDisplay/Run.php index 9a890b6aa8..7ad2c72c9c 100644 --- a/ext/search/Civi/Api4/Action/SearchDisplay/Run.php +++ b/ext/search/Civi/Api4/Action/SearchDisplay/Run.php @@ -134,6 +134,12 @@ class Run extends \Civi\Api4\Generic\AbstractAction { $apiParams['select'][] = $idField; } } + // Select value fields for in-place editing + foreach ($settings['columns'] ?? [] as $column) { + if (isset($column['editable']['value']) && !in_array($column['editable']['value'], $apiParams['select'])) { + $apiParams['select'][] = $column['editable']['value']; + } + } } $this->applyFilters(); diff --git a/ext/search/Civi/Search/Admin.php b/ext/search/Civi/Search/Admin.php index bcb73ca433..5b77712b89 100644 --- a/ext/search/Civi/Search/Admin.php +++ b/ext/search/Civi/Search/Admin.php @@ -87,7 +87,7 @@ class Admin { ->setChain([ 'get' => ['$name', 'getActions', ['where' => [['name', '=', 'get']]], ['params']], ])->execute(); - $getFields = ['name', 'title', 'label', 'description', 'options', 'input_type', 'input_attrs', 'data_type', 'serialize', 'entity', 'fk_entity']; + $getFields = ['name', 'title', 'label', 'description', 'options', 'input_type', 'input_attrs', 'data_type', 'serialize', 'entity', 'fk_entity', 'readonly']; foreach ($entities as $entity) { // Skip if entity doesn't have a 'get' action or the user doesn't have permission to use get if ($entity['get']) { diff --git a/ext/search/ang/crmSearchActions/crmSearchActionUpdate.ctrl.js b/ext/search/ang/crmSearchActions/crmSearchActionUpdate.ctrl.js index 2297be2950..55f704e0b3 100644 --- a/ext/search/ang/crmSearchActions/crmSearchActionUpdate.ctrl.js +++ b/ext/search/ang/crmSearchActions/crmSearchActionUpdate.ctrl.js @@ -13,6 +13,7 @@ crmApi4(model.entity, 'getFields', { action: 'update', + select: ['name', 'label', 'description', 'data_type', 'serialize', 'options'], loadOptions: ['id', 'name', 'label', 'description', 'color', 'icon'], where: [["readonly", "=", false]], }).then(function(fields) { diff --git a/ext/search/ang/crmSearchAdmin/crmSearchAdminDisplay.component.js b/ext/search/ang/crmSearchAdmin/crmSearchAdminDisplay.component.js index 2e1e07654c..560445ff53 100644 --- a/ext/search/ang/crmSearchAdmin/crmSearchAdminDisplay.component.js +++ b/ext/search/ang/crmSearchAdmin/crmSearchAdminDisplay.component.js @@ -119,6 +119,45 @@ return ctrl.colTypes[col.type].label; }; + this.toggleRewrite = function(col) { + if (col.rewrite) { + col.rewrite = ''; + } else { + col.rewrite = '[' + col.key + ']'; + delete col.editable; + } + }; + + this.toggleEditable = function(col) { + if (col.editable) { + delete col.editable; + return; + } + + var info = searchMeta.parseExpr(col.key), + value = col.key.split(':')[0]; + // If field is an implicit join, use the original fk field + if (info.field.entity !== info.field.baseEntity) { + value = value.substr(0, value.indexOf('.')) + '_id'; + info = searchMeta.parseExpr(value); + } + col.editable = { + entity: info.field.baseEntity, + options: !!info.field.options, + serialize: !!info.field.serialize, + fk_entity: info.field.fk_entity, + id: info.prefix + 'id', + name: info.field.name, + value: value + }; + }; + + this.isEditable = function(col) { + var expr = ctrl.getExprFromSelect(col.key), + info = searchMeta.parseExpr(expr); + return !col.rewrite && !col.link && !info.fn && info.field && !info.field.readonly; + }; + function fieldToColumn(fieldExpr, defaults) { var info = searchMeta.parseExpr(fieldExpr), values = _.cloneDeep(defaults); @@ -169,7 +208,7 @@ var idField = info.field.fk_entity ? fieldName : fieldName.substr(0, fieldName.lastIndexOf('.')) + '_id'; if (!ctrl.crmSearchAdmin.canAggregate(idField)) { var joinEntity = searchMeta.getEntity(info.field.fk_entity || info.field.entity); - _.each(joinEntity.paths, function(path) { + _.each((joinEntity || {}).paths, function(path) { var link = _.cloneDeep(path); link.path = link.path.replace(/\[id/g, '[' + idField); links.push(link); diff --git a/ext/search/ang/crmSearchAdmin/crmSearchAdminLinkSelect.component.js b/ext/search/ang/crmSearchAdmin/crmSearchAdminLinkSelect.component.js index 394f8158e4..eb8604e9e9 100644 --- a/ext/search/ang/crmSearchAdmin/crmSearchAdminLinkSelect.component.js +++ b/ext/search/ang/crmSearchAdmin/crmSearchAdminLinkSelect.component.js @@ -19,6 +19,7 @@ if (link) { ctrl.column.link = link.path; ctrl.column.title = link.title; + delete ctrl.column.editable; } else { if (val === 'civicrm/') { ctrl.column.link = val; diff --git a/ext/search/ang/crmSearchAdmin/displays/colType/field.html b/ext/search/ang/crmSearchAdmin/displays/colType/field.html index 20e1976e79..5960cc5ef2 100644 --- a/ext/search/ang/crmSearchAdmin/displays/colType/field.html +++ b/ext/search/ang/crmSearchAdmin/displays/colType/field.html @@ -9,9 +9,19 @@
+
+ + +
diff --git a/ext/search/ang/crmSearchDisplay.module.js b/ext/search/ang/crmSearchDisplay.module.js index 7c134b9f47..a8f7cb8501 100644 --- a/ext/search/ang/crmSearchDisplay.module.js +++ b/ext/search/ang/crmSearchDisplay.module.js @@ -74,9 +74,9 @@ } function getResults(ctrl) { - var params = getApiParams(ctrl); - crmApi4('SearchDisplay', 'run', params).then(function(results) { + return crmApi4('SearchDisplay', 'run', getApiParams(ctrl)).then(function(results) { ctrl.results = results; + ctrl.editing = false; if (ctrl.settings.pager && !ctrl.rowCount) { if (results.length < ctrl.settings.limit) { ctrl.rowCount = results.length; diff --git a/ext/search/ang/crmSearchDisplay/colType/field.html b/ext/search/ang/crmSearchDisplay/colType/field.html index 1c61060100..4f21543abf 100644 --- a/ext/search/ang/crmSearchDisplay/colType/field.html +++ b/ext/search/ang/crmSearchDisplay/colType/field.html @@ -1 +1,2 @@ - + + diff --git a/ext/search/ang/crmSearchDisplay/crmSearchDisplayEditable.component.js b/ext/search/ang/crmSearchDisplay/crmSearchDisplayEditable.component.js new file mode 100644 index 0000000000..181c0d26d2 --- /dev/null +++ b/ext/search/ang/crmSearchDisplay/crmSearchDisplayEditable.component.js @@ -0,0 +1,83 @@ +// https://civicrm.org/licensing +(function(angular, $, _) { + "use strict"; + + var optionsCache = {}; + + angular.module('crmSearchDisplay').component('crmSearchDisplayEditable', { + bindings: { + row: '<', + col: '<', + cancel: '&', + onSuccess: '&' + }, + templateUrl: '~/crmSearchDisplay/crmSearchDisplayEditable.html', + controller: function($scope, $element, crmApi4, crmStatus) { + var ctrl = this, + initialValue, + col; + + this.$onInit = function() { + col = this.col; + this.value = _.cloneDeep(this.row[col.editable.value]); + initialValue = _.cloneDeep(this.row[col.editable.value]); + + this.field = { + data_type: col.dataType, + name: col.editable.name, + options: col.editable.options, + fk_entity: col.editable.fk_entity, + serialize: col.editable.serialize, + }; + + $(document).on('keydown.crmSearchDisplayEditable', function(e) { + if (e.key === 'Escape') { + $scope.$apply(function() { + ctrl.cancel(); + }); + } else if (e.key === 'Enter') { + $scope.$apply(ctrl.save); + } + }); + + if (this.field.options === true) { + loadOptions(); + } + }; + + this.$onDestroy = function() { + $(document).off('.crmSearchDisplayEditable'); + }; + + this.save = function() { + if (ctrl.value === initialValue) { + ctrl.cancel(); + return; + } + var values = {id: ctrl.row[col.editable.id]}; + values[col.editable.name] = ctrl.value; + $('input', $element).attr('disabled', true); + crmStatus({}, crmApi4(col.editable.entity, 'update', { + values: values + })).then(ctrl.onSuccess); + }; + + function loadOptions() { + var cacheKey = col.editable.entity + ' ' + ctrl.field.name; + if (optionsCache[cacheKey]) { + ctrl.field.options = optionsCache[cacheKey]; + return; + } + crmApi4(col.editable.entity, 'getFields', { + action: 'update', + select: ['options'], + loadOptions: ['id', 'name', 'label', 'description', 'color', 'icon'], + where: [['name', '=', ctrl.field.name]], + }, 0).then(function(field) { + ctrl.field.options = optionsCache[cacheKey] = field.options; + }); + } + } + }); + +})(angular, CRM.$, CRM._); diff --git a/ext/search/ang/crmSearchDisplay/crmSearchDisplayEditable.html b/ext/search/ang/crmSearchDisplay/crmSearchDisplayEditable.html new file mode 100644 index 0000000000..d0d8eca08f --- /dev/null +++ b/ext/search/ang/crmSearchDisplay/crmSearchDisplayEditable.html @@ -0,0 +1,9 @@ + +
+ + +
diff --git a/ext/search/ang/crmSearchDisplayList/crmSearchDisplayList.component.js b/ext/search/ang/crmSearchDisplayList/crmSearchDisplayList.component.js index 3245db91c2..2153e6dc1e 100644 --- a/ext/search/ang/crmSearchDisplayList/crmSearchDisplayList.component.js +++ b/ext/search/ang/crmSearchDisplayList/crmSearchDisplayList.component.js @@ -26,16 +26,21 @@ $scope.displayUtils = searchDisplayUtils; if (this.afFieldset) { - $scope.$watch(this.afFieldset.getFieldData, refresh, true); + $scope.$watch(this.afFieldset.getFieldData, onChangeFilters, true); } - $scope.$watch('$ctrl.filters', refresh, true); + $scope.$watch('$ctrl.filters', onChangeFilters, true); }; this.getResults = _.debounce(function() { searchDisplayUtils.getResults(ctrl); }, 100); - function refresh() { + // Refresh current page + this.refresh = function(row) { + searchDisplayUtils.getResults(ctrl); + }; + + function onChangeFilters() { ctrl.page = 1; ctrl.rowCount = null; ctrl.getResults(); diff --git a/ext/search/ang/crmSearchDisplayList/crmSearchDisplayList.html b/ext/search/ang/crmSearchDisplayList/crmSearchDisplayList.html index 00c0f55e47..9d039ba19c 100644 --- a/ext/search/ang/crmSearchDisplayList/crmSearchDisplayList.html +++ b/ext/search/ang/crmSearchDisplayList/crmSearchDisplayList.html @@ -1,3 +1,5 @@ -
    - -
    +
    +
      + +
      +
      diff --git a/ext/search/ang/crmSearchDisplayList/crmSearchDisplayListItems.html b/ext/search/ang/crmSearchDisplayList/crmSearchDisplayListItems.html index e99f04b04f..59bae1aab8 100644 --- a/ext/search/ang/crmSearchDisplayList/crmSearchDisplayListItems.html +++ b/ext/search/ang/crmSearchDisplayList/crmSearchDisplayListItems.html @@ -1,4 +1,4 @@ -
    1. +