Skip to content

Commit

Permalink
SearchKit - Use server-side preprocessing for editable fields
Browse files Browse the repository at this point in the history
  • Loading branch information
colemanw committed Oct 23, 2021
1 parent ddefe5e commit ba6ef69
Show file tree
Hide file tree
Showing 8 changed files with 92 additions and 55 deletions.
3 changes: 0 additions & 3 deletions Civi/Api4/Generic/AbstractAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -430,9 +430,6 @@ public function getPermissions() {
public function entityFields() {
if (!$this->_entityFields) {
$allowedTypes = ['Field', 'Filter', 'Extra'];
if (method_exists($this, 'getCustomGroup')) {
$allowedTypes[] = 'Custom';
}
$getFields = \Civi\API\Request::create($this->getEntityName(), 'getFields', [
'version' => 4,
'checkPermissions' => FALSE,
Expand Down
10 changes: 9 additions & 1 deletion Civi/Api4/Query/Api4SelectQuery.php
Original file line number Diff line number Diff line change
Expand Up @@ -722,12 +722,14 @@ private function addExplicitJoins() {
$joinEntityFields = $joinEntityGet->entityFields();
foreach ($joinEntityFields as $field) {
$field['sql_name'] = '`' . $alias . '`.`' . $field['column_name'] . '`';
$field['explicit_join'] = $alias;
$this->addSpecField($alias . '.' . $field['name'], $field);
}
$tableName = CoreUtil::getTableName($entity);
// Save join info to be retrieved by $this->getExplicitJoin()
$this->explicitJoins[$alias] = [
'entity' => $entity,
'alias' => $alias,
'table' => $tableName,
'bridge' => NULL,
];
Expand Down Expand Up @@ -938,6 +940,7 @@ private function registerBridgeJoinFields($bridgeEntity, $joinRef, $baseRef, str
// For INNER joins, these fields get a sql alias pointing to the bridge entity,
// but an api alias pretending they belong to the join entity.
$field['sql_name'] = '`' . ($side === 'LEFT' ? $alias : $bridgeAlias) . '`.`' . $field['column_name'] . '`';
$field['explicit_join'] = $alias;
$this->addSpecField($alias . '.' . $name, $field);
if ($field['type'] === 'Field') {
$fakeFields[$field['column_name']] = '`' . $bridgeAlias . '`.`' . $field['column_name'] . '`';
Expand Down Expand Up @@ -1094,6 +1097,8 @@ protected function autoJoinFK($key) {
else {
$fieldArray['sql_name'] = '`' . $baseTableAlias . '`.`' . $link->getBaseColumn() . '`';
}
$fieldArray['implicit_join'] = $link->getBaseColumn();
$fieldArray['explicit_join'] = $explicitJoin ? $explicitJoin['alias'] : NULL;
// Custom fields will already have the group name prefixed
$fieldName = $isCustom ? explode('.', $fieldArray['name'])[1] : $fieldArray['name'];
$this->addSpecField($joinTreeNode[$joinName]['#path'] . $fieldName, $fieldArray);
Expand Down Expand Up @@ -1252,7 +1257,10 @@ private function addSpecField($path, $field) {
$this->apiFieldSpec[$path] = FALSE;
return;
}
$this->apiFieldSpec[$path] = $field;
$this->apiFieldSpec[$path] = $field + [
'implicit_join' => NULL,
'explicit_join' => NULL,
];
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -275,10 +275,64 @@ private function getUrl(string $path) {
return \CRM_Utils_System::url($path, NULL, $absolute, NULL, FALSE);
}

/**
* @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
*/
private function formatEditableColumn($column, $data) {
$editable = $this->getEditableInfo($column['key']);
if (!empty($data[$editable['id_path']])) {
$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
*/
private function getEditableInfo($key) {
[$key] = explode(':', $key);
$field = $this->getField($key);
// If field is an implicit join, use the original fk field
if (!empty($field['implicit_join'])) {
return $this->getEditableInfo(substr($key, 0, -1 - strlen($field['name'])));
}
if ($field) {
$idKey = CoreUtil::getIdFieldName($field['entity']);
$idPath = ($field['explicit_join'] ? $field['explicit_join'] . '.' : '') . $idKey;
// Hack to support editing relationships
if ($field['entity'] === 'RelationshipCache') {
$field['entity'] = 'Relationship';
$idPath = ($field['explicit_join'] ? $field['explicit_join'] . '.' : '') . 'relationship_id';
}
return [
'entity' => $field['entity'],
'input_type' => $field['input_type'],
'data_type' => $field['data_type'],
'options' => !empty($field['options']),
'serialize' => !empty($field['serialize']),
'fk_entity' => $field['fk_entity'],
'value_key' => $field['name'],
'value_path' => $key,
'id_key' => $idKey,
'id_path' => $idPath,
];
}
return NULL;
}

/**
* @param $column
* @param $data
* @return array{url: string, width: int, height: int}
*/
private function formatImage($column, $data) {
$tokenExpr = $column['rewrite'] ?: '[' . $column['key'] . ']';
return [
Expand Down Expand Up @@ -575,10 +629,13 @@ protected function augmentSelectClause(&$apiParams): void {
$possibleTokens .= implode('', array_column($column['links'], 'text'));
}

// Select value fields for in-place editing
if (isset($column['editable']['value'])) {
$additions[] = $column['editable']['value'];
$additions[] = $column['editable']['id'];
// Select id & value for in-place editing
if (!empty($column['editable'])) {
$editable = $this->getEditableInfo($column['key']);
if ($editable) {
$additions[] = $editable['value_path'];
$additions[] = $editable['id_path'];
}
}
}
// Add fields referenced via token
Expand Down
1 change: 0 additions & 1 deletion ext/search_kit/Civi/Search/Admin.php
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,6 @@ public static function getSchema() {
$field['fieldName'] = $field['name'];
// Hack for RelationshipCache to make Relationship fields editable
if ($entity['name'] === 'RelationshipCache') {
$entity['primary_key'] = ['relationship_id'];
if (in_array($field['name'], ['is_active', 'start_date', 'end_date'])) {
$field['readonly'] = FALSE;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,37 +156,12 @@
this.toggleEditable = function(col) {
if (col.editable) {
delete col.editable;
return;
}

var info = searchMeta.parseExpr(col.key),
arg = _.findWhere(info.args, {type: 'field'}) || {},
value = col.key.split(':')[0];
if (!arg.field || info.fn) {
delete col.editable;
return;
}
// If field is an implicit join, use the original fk field
if (arg.field.name !== arg.field.fieldName) {
value = value.substr(0, value.lastIndexOf('.'));
info = searchMeta.parseExpr(value);
arg = info.args[0];
} else {
col.editable = true;
}
col.editable = {
// Hack to support editing relationships
entity: arg.field.entity.replace('RelationshipCache', 'Relationship'),
input_type: arg.field.input_type,
data_type: arg.field.data_type,
options: !!arg.field.options,
serialize: !!arg.field.serialize,
fk_entity: arg.field.fk_entity,
id: arg.prefix + searchMeta.getEntity(arg.field.entity).primary_key[0],
name: arg.field.name,
value: value
};
};

this.isEditable = function(col) {
this.canBeEditable = function(col) {
var expr = ctrl.getExprFromSelect(col.key),
info = searchMeta.parseExpr(expr);
return !col.image && !col.rewrite && !col.link && !info.fn && info.args[0] && info.args[0].field && !info.args[0].field.readonly;
Expand All @@ -213,6 +188,7 @@
if (column.link) {
ctrl.onChangeLink(column, column.link.path, '');
} else {
delete col.editable;
var defaultLink = ctrl.getLinks(column.key)[0];
column.link = {path: defaultLink ? defaultLink.path : 'civicrm/'};
ctrl.onChangeLink(column, null, column.link.path);
Expand Down
4 changes: 2 additions & 2 deletions ext/search_kit/ang/crmSearchAdmin/displays/colType/field.html
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,11 @@
<crm-search-admin-token-select ng-if="col.rewrite" model="col" field="rewrite" suffix=":label"></crm-search-admin-token-select>
</div>
<div class="form-inline">
<label ng-if="$ctrl.parent.isEditable(col)" title="{{:: ts('Users will be able to click to edit this field.') }}">
<label ng-if="$ctrl.parent.canBeEditable(col)" title="{{:: ts('Users will be able to click to edit this field.') }}">
<input type="checkbox" ng-checked="col.editable" ng-click="$ctrl.parent.toggleEditable(col)">
{{:: ts('In-Place Edit') }}
</label>
<label ng-if="!$ctrl.parent.isEditable(col)" class="disabled" title="{{:: ts('Read-only or rewritten fields cannot be editable.') }}">
<label ng-if="!$ctrl.parent.canBeEditable(col)" class="disabled" title="{{:: ts('Read-only or rewritten fields cannot be editable.') }}">
<input type="checkbox" disabled>
{{:: ts('In-Place Edit') }}
</label>
Expand Down
4 changes: 2 additions & 2 deletions ext/search_kit/ang/crmSearchDisplay/colType/field.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<crm-search-display-editable row="row" col="$ctrl.settings.columns[colIndex]" on-success="$ctrl.runSearch(row)" cancel="$ctrl.editing = null;" ng-if="col.editable && $ctrl.editing && $ctrl.editing[0] === rowIndex && $ctrl.editing[1] === col.key"></crm-search-display-editable>
<span ng-if="::!colData.links && !colData.img" ng-class="{'crm-editable-enabled': colData.edit && !$ctrl.editing}" ng-click="col.edit && !$ctrl.editing && ($ctrl.editing = [rowIndex, col.key])">
<crm-search-display-editable row="row" col="colData" on-success="$ctrl.runSearch(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 && !colData.img" ng-class="{'crm-editable-enabled': colData.edit && !$ctrl.editing}" ng-click="colData.edit && !$ctrl.editing && ($ctrl.editing = [rowIndex, colIndex])">
{{:: colData.val }}
</span>
<span ng-if="::colData.links">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,16 @@

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

this.field = {
data_type: col.editable.data_type,
input_type: col.editable.input_type,
name: col.editable.name,
options: col.editable.options,
fk_entity: col.editable.fk_entity,
serialize: col.editable.serialize,
data_type: col.edit.data_type,
input_type: col.edit.input_type,
name: col.edit.value_key,
options: col.edit.options,
fk_entity: col.edit.fk_entity,
serialize: col.edit.serialize,
};

$(document).on('keydown.crmSearchDisplayEditable', function(e) {
Expand All @@ -55,21 +55,21 @@
ctrl.cancel();
return;
}
var values = {id: ctrl.row[col.editable.id].raw};
values[col.editable.name] = ctrl.value;
var record = _.cloneDeep(col.edit.record);
record[col.edit.value_key] = ctrl.value;
$('input', $element).attr('disabled', true);
crmStatus({}, crmApi4(col.editable.entity, 'update', {
values: values
crmStatus({}, crmApi4(col.edit.entity, 'update', {
values: record
})).then(ctrl.onSuccess);
};

function loadOptions() {
var cacheKey = col.editable.entity + ' ' + ctrl.field.name;
var cacheKey = col.edit.entity + ' ' + ctrl.field.name;
if (optionsCache[cacheKey]) {
ctrl.field.options = optionsCache[cacheKey];
return;
}
crmApi4(col.editable.entity, 'getFields', {
crmApi4(col.edit.entity, 'getFields', {
action: 'update',
select: ['options'],
loadOptions: ['id', 'name', 'label', 'description', 'color', 'icon'],
Expand Down

0 comments on commit ba6ef69

Please sign in to comment.