Skip to content

Commit

Permalink
SearchKit - In-place edit without refreshing results
Browse files Browse the repository at this point in the history
  • Loading branch information
colemanw committed Aug 20, 2023
1 parent 9e70be1 commit aa1287f
Show file tree
Hide file tree
Showing 7 changed files with 71 additions and 33 deletions.
Expand Up @@ -706,7 +706,7 @@ private function getUrl(string $path, $query = NULL) {
/**
* @param array $column
* @param array $data
* @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
* @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_path: string}|null
*/
private function formatEditableColumn($column, $data) {
$editable = $this->getEditableInfo($column['key']);
Expand All @@ -715,7 +715,6 @@ private function formatEditableColumn($column, $data) {
if (!empty($data[$editable['id_path']])) {
$editable['action'] = 'update';
$editable['record'][$editable['id_key']] = $data[$editable['id_path']];
$editable['value'] = $data[$editable['value_path']];
// Ensure field is appropriate to this entity sub-type
$field = $this->getField($column['key']);
$entityValues = FormattingUtil::filterByPath($data, $editable['id_path'], $editable['id_key']);
Expand All @@ -726,7 +725,6 @@ private function formatEditableColumn($column, $data) {
// Generate params to create new record, if applicable
elseif ($editable['explicit_join'] && !$this->getJoin($editable['explicit_join'])['bridge']) {
$editable['action'] = 'create';
$editable['value'] = NULL;
$editable['nullable'] = FALSE;
// Get values for creation from the join clause
$join = $this->getQuery()->getExplicitJoin($editable['explicit_join']);
Expand Down Expand Up @@ -776,8 +774,14 @@ private function formatEditableColumn($column, $data) {
'values' => $entityValues,
], 0)['access'];
if ($access) {
// Add currency formatting info
if ($editable['data_type'] === 'Money') {
$currencyField = $this->getCurrencyField($column['key']);
$currency = is_string($data[$currencyField] ?? NULL) ? $data[$currencyField] : NULL;
$editable['currency_format'] = \Civi::format()->money(1234.56, $currency);
}
// Remove info that's for internal use only
\CRM_Utils_Array::remove($editable, 'id_key', 'id_path', 'value_path', 'explicit_join', 'grouping_fields');
\CRM_Utils_Array::remove($editable, 'id_key', 'id_path', 'explicit_join', 'grouping_fields');
return $editable;
}
}
Expand Down
6 changes: 3 additions & 3 deletions ext/search_kit/ang/crmSearchDisplay/colType/field.html
@@ -1,10 +1,10 @@
<crm-search-display-editable row="row" col="colData" do-save="$ctrl.runSearch({inPlaceEdit: apiCall}, {}, row)" cancel="$ctrl.editing = null;" ng-if="colData.edit && $ctrl.editing && $ctrl.editing[0] === rowIndex && $ctrl.editing[1] === colIndex"></crm-search-display-editable>
<span ng-if="::!colData.links" ng-class="{'crm-editable-enabled': colData.edit && !$ctrl.editing, 'crm-editable-disabled': colData.edit && $ctrl.editing}" ng-click="colData.edit && !$ctrl.editing && ($ctrl.editing = [rowIndex, colIndex])">
<crm-search-display-editable row="row" col="colData" cancel="$ctrl.editing = null;" ng-if="colData.edit && $ctrl.isEditing(rowIndex, colIndex)"></crm-search-display-editable>
<span ng-if="!colData.links && !$ctrl.isEditing(rowIndex, colIndex)" ng-class="{'crm-editable-enabled': colData.edit && !$ctrl.editing, 'crm-editable-disabled': colData.edit && $ctrl.editing}" ng-click="colData.edit && !$ctrl.editing && ($ctrl.editing = [rowIndex, colIndex])">
<i ng-repeat="icon in colData.icons" ng-if="icon.side === 'left'" class="crm-i {{:: icon['class'] }}"></i>
{{:: $ctrl.formatFieldValue(colData) }}
<i ng-repeat="icon in colData.icons" ng-if="icon.side === 'right'" class="crm-i {{:: icon['class'] }}"></i>
</span>
<span ng-if="::colData.links">
<span ng-if="colData.links && !$ctrl.isEditing(rowIndex, colIndex)">
<span ng-repeat="link in colData.links">
<a target="{{:: link.target }}" ng-href="{{:: link.url }}" title="{{:: link.title }}" ng-click="$ctrl.onClickLink(link, row.key, $event)">
<i ng-repeat="icon in colData.icons" ng-if="icon.side === 'left'" class="crm-i {{:: icon['class'] }}"></i>
Expand Down
Expand Up @@ -8,8 +8,7 @@
bindings: {
row: '<',
col: '<',
cancel: '&',
doSave: '&'
cancel: '&'
},
templateUrl: '~/crmSearchDisplay/crmSearchDisplayEditable.html',
controller: function($scope, $element, crmApi4) {
Expand All @@ -19,8 +18,8 @@

this.$onInit = function() {
col = this.col;
this.value = _.cloneDeep(col.edit.value);
initialValue = _.cloneDeep(col.edit.value);
this.value = _.cloneDeep(this.row.data[col.edit.value_path]);
initialValue = _.cloneDeep(this.row.data[col.edit.value_path]);

this.field = {
data_type: col.edit.data_type,
Expand Down Expand Up @@ -50,16 +49,52 @@
};

this.save = function() {
if (ctrl.value === initialValue) {
ctrl.cancel();
return;
const value = formatDataType(ctrl.value);
if (value !== initialValue) {
col.edit.record[col.edit.value_key] = value;
CRM.status({}, crmApi4(col.edit.entity, col.edit.action, {values: col.edit.record}));
ctrl.row.data[col.edit.value_path] = value;
col.val = formatDisplayValue(value);
}
var record = _.cloneDeep(col.edit.record);
record[col.edit.value_key] = ctrl.value;
$('input', $element).attr('disabled', true);
ctrl.doSave({apiCall: [col.edit.entity, col.edit.action, {values: record}]});
ctrl.cancel();
};

function formatDataType(val) {
if (_.isArray(val)) {
const formatted = angular.copy(val);
formatted.forEach((v, i) => formatted[i] = formatDataType(v));
return formatted;
}
if (ctrl.field.data_type === 'Integer') {
return +val;
}
return val;
}

function formatDisplayValue(val) {
let displayValue = angular.copy(val);
if (_.isArray(displayValue)) {
displayValue.forEach((v, i) => displayValue[i] = formatDisplayValue(v));
return displayValue;
}
if (ctrl.field.options) {
ctrl.field.options.forEach((option) => {
if (('' + option.id) === ('' + val)) {
displayValue = option.label;
}
});
} else if (ctrl.field.data_type === 'Boolean' && val === true) {
displayValue = ts('Yes');
} else if (ctrl.field.data_type === 'Boolean' && val === false) {
displayValue = ts('No');
} else if (ctrl.field.data_type === 'Date' || ctrl.field.data_type === 'Timestamp') {
displayValue = CRM.utils.formatDate(val, null, ctrl.field.data_type === 'Timestamp');
} else if (ctrl.field.data_type === 'Money') {
displayValue = CRM.formatMoney(displayValue, false, col.edit.currency_format);
}
return displayValue;
}

function loadOptions() {
var cacheKey = col.edit.entity + ' ' + ctrl.field.name;
if (optionsCache[cacheKey]) {
Expand Down
Expand Up @@ -201,6 +201,9 @@
},
formatFieldValue: function(colData) {
return angular.isArray(colData.val) ? colData.val.join(', ') : colData.val;
},
isEditing: function(rowIndex, colIndex) {
return this.editing && this.editing[0] === rowIndex && this.editing[1] === colIndex;
}
};
});
Expand Down
4 changes: 0 additions & 4 deletions ext/search_kit/css/crmSearchTasks.css
Expand Up @@ -14,10 +14,6 @@
position: relative;
}

.crm-search-display crm-search-display-editable + span.crm-editable-disabled {
display: none !important;
}

.crm-search-display .crm-search-display-editable-buttons {
position: absolute;
bottom: -24px;
Expand Down
Expand Up @@ -551,7 +551,7 @@ public function testInPlaceEditAndCreate() {
$this->assertEquals('String', $result[0]['columns'][0]['edit']['data_type']);
$this->assertEquals('first_name', $result[0]['columns'][0]['edit']['value_key']);
$this->assertEquals('update', $result[0]['columns'][0]['edit']['action']);
$this->assertEquals('One', $result[0]['columns'][0]['edit']['value']);
$this->assertEquals('One', $result[0]['data'][$result[0]['columns'][0]['edit']['value_path']]);

// Contact 1 email can be updated
$this->assertEquals('testmail@unit.test', $result[0]['columns'][1]['val']);
Expand All @@ -561,7 +561,7 @@ public function testInPlaceEditAndCreate() {
$this->assertEquals('String', $result[0]['columns'][1]['edit']['data_type']);
$this->assertEquals('email', $result[0]['columns'][1]['edit']['value_key']);
$this->assertEquals('update', $result[0]['columns'][1]['edit']['action']);
$this->assertEquals('testmail@unit.test', $result[0]['columns'][1]['edit']['value']);
$this->assertEquals('testmail@unit.test', $result[0]['data'][$result[0]['columns'][1]['edit']['value_path']]);

// Contact 1 - new phone can be created
$this->assertNull($result[0]['columns'][2]['val']);
Expand All @@ -571,7 +571,7 @@ public function testInPlaceEditAndCreate() {
$this->assertEquals('String', $result[0]['columns'][2]['edit']['data_type']);
$this->assertEquals('phone', $result[0]['columns'][2]['edit']['value_key']);
$this->assertEquals('create', $result[0]['columns'][2]['edit']['action']);
$this->assertNull($result[0]['columns'][2]['edit']['value']);
$this->assertEquals('Contact_Phone_contact_id_01.phone', $result[0]['columns'][2]['edit']['value_path']);

// Contact 2 first name can be added
$this->assertNull($result[1]['columns'][0]['val']);
Expand All @@ -581,7 +581,7 @@ public function testInPlaceEditAndCreate() {
$this->assertEquals('String', $result[1]['columns'][0]['edit']['data_type']);
$this->assertEquals('first_name', $result[1]['columns'][0]['edit']['value_key']);
$this->assertEquals('update', $result[1]['columns'][0]['edit']['action']);
$this->assertNull($result[1]['columns'][0]['edit']['value']);
$this->assertEquals('first_name', $result[1]['columns'][0]['edit']['value_path']);

// Contact 2 - new email can be created
$this->assertNull($result[1]['columns'][1]['val']);
Expand All @@ -591,7 +591,7 @@ public function testInPlaceEditAndCreate() {
$this->assertEquals('String', $result[1]['columns'][1]['edit']['data_type']);
$this->assertEquals('email', $result[1]['columns'][1]['edit']['value_key']);
$this->assertEquals('create', $result[1]['columns'][1]['edit']['action']);
$this->assertNull($result[1]['columns'][1]['edit']['value']);
$this->assertEquals('Contact_Email_contact_id_01.email', $result[1]['columns'][1]['edit']['value_path']);

// Contact 2 phone can be updated
$this->assertEquals('123456', $result[1]['columns'][2]['val']);
Expand All @@ -601,7 +601,7 @@ public function testInPlaceEditAndCreate() {
$this->assertEquals('String', $result[1]['columns'][2]['edit']['data_type']);
$this->assertEquals('phone', $result[1]['columns'][2]['edit']['value_key']);
$this->assertEquals('update', $result[1]['columns'][2]['edit']['action']);
$this->assertEquals('123456', $result[1]['columns'][2]['edit']['value']);
$this->assertEquals('123456', $result[1]['data'][$result[0]['columns'][2]['edit']['value_path']]);
}

/**
Expand Down Expand Up @@ -1497,7 +1497,7 @@ public function testEditableContactFields() {
'value_key' => 'first_name',
'record' => ['id' => $contact[0]['id']],
'action' => 'update',
'value' => 'One',
'value_path' => 'first_name',
];
// Ensure first_name is editable but not organization_name or household_name
$this->assertEquals($expectedFirstNameEdit, $result[0]['columns'][0]['edit']);
Expand All @@ -1506,21 +1506,22 @@ public function testEditableContactFields() {

// Second Individual
$expectedFirstNameEdit['record']['id'] = $contact[1]['id'];
$expectedFirstNameEdit['value'] = NULL;
$this->assertEquals($expectedFirstNameEdit, $result[1]['columns'][0]['edit']);
$this->assertTrue(!isset($result[1]['columns'][1]['edit']));
$this->assertTrue(!isset($result[1]['columns'][2]['edit']));

// Third contact: Organization
$expectedFirstNameEdit['record']['id'] = $contact[2]['id'];
$expectedFirstNameEdit['value_key'] = 'organization_name';
$expectedFirstNameEdit['value_path'] = 'organization_name';
$this->assertTrue(!isset($result[2]['columns'][0]['edit']));
$this->assertEquals($expectedFirstNameEdit, $result[2]['columns'][1]['edit']);
$this->assertTrue(!isset($result[2]['columns'][2]['edit']));

// Third contact: Household
$expectedFirstNameEdit['record']['id'] = $contact[3]['id'];
$expectedFirstNameEdit['value_key'] = 'household_name';
$expectedFirstNameEdit['value_path'] = 'household_name';
$this->assertTrue(!isset($result[3]['columns'][0]['edit']));
$this->assertTrue(!isset($result[3]['columns'][1]['edit']));
$this->assertEquals($expectedFirstNameEdit, $result[3]['columns'][2]['edit']);
Expand Down
Expand Up @@ -360,9 +360,9 @@ public function testEditableCustomFields() {
'value_key' => 'meeting_phone.sub_field',
'record' => ['id' => $activity[0]['id']],
'action' => 'update',
'value' => 'Abc',
'value_path' => 'meeting_phone.sub_field',
];
$expectedSubjectEdit = ['value_key' => 'subject', 'value' => $subject] + $expectedCustomFieldEdit;
$expectedSubjectEdit = ['value_key' => 'subject', 'value_path' => 'subject'] + $expectedCustomFieldEdit;

// First Activity
$this->assertEquals($expectedSubjectEdit, $result[0]['columns'][0]['edit']);
Expand All @@ -372,7 +372,6 @@ public function testEditableCustomFields() {
// Second Activity
$expectedSubjectEdit['record']['id'] = $activity[1]['id'];
$expectedCustomFieldEdit['record']['id'] = $activity[1]['id'];
$expectedCustomFieldEdit['value'] = NULL;
$this->assertEquals($expectedSubjectEdit, $result[1]['columns'][0]['edit']);
$this->assertEquals($expectedCustomFieldEdit, $result[1]['columns'][1]['edit']);
$this->assertEquals($activityTypes['Phone Call'], $result[1]['data']['activity_type_id']);
Expand Down

0 comments on commit aa1287f

Please sign in to comment.