}
.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 {
$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();
->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']) {
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) {
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);
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);
if (link) {
ctrl.column.link = link.path;
ctrl.column.title = link.title;
+ delete ctrl.column.editable;
} else {
if (val === 'civicrm/') {
ctrl.column.link = val;
</div>
<div class="form-inline crm-search-admin-flex-row">
<label title="{{ ts('Change the contents of this field, or combine multiple field values.') }}">
- <input type="checkbox" ng-checked="col.rewrite" ng-click="col.rewrite = col.rewrite ? null : '['+col.key+']'" >
+ <input type="checkbox" ng-checked="col.rewrite" ng-click="$ctrl.parent.toggleRewrite(col)" >
{{ col.rewrite ? ts('Rewrite:') : ts('Rewrite') }}
</label>
<input type="text" class="form-control" ng-if="col.rewrite" ng-model="col.rewrite" ng-model-options="{updateOn: 'blur'}">
<crm-search-admin-token-select ng-if="col.rewrite" api-entity="$ctrl.apiEntity" api-params="$ctrl.apiParams" model="col" field="rewrite"></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.') }}">
+ <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.') }}">
+ <input type="checkbox" disabled>
+ {{:: ts('In-place edit') }}
+ </label>
+</div>
}
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;
-<span ng-bind-html="$ctrl.formatFieldValue(row, col)"></span>
+<crm-search-display-editable row="row" col="col" on-success="$ctrl.refresh(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-bind-html=":: $ctrl.formatFieldValue(row, col)" ng-class="{'crm-editable-enabled': col.editable && !$ctrl.editing && row[col.editable.id]}" ng-click="col.editable && !$ctrl.editing && ($ctrl.editing = [rowIndex, col.key])"></span>
--- /dev/null
+// 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._);
--- /dev/null
+<crm-search-input class="form-inline" field="$ctrl.field" ng-model="$ctrl.value"></crm-search-input>
+<div class="form-inline crm-search-display-editable-buttons">
+ <button type="button" ng-click="$ctrl.save()" class="btn btn-xs btn-success">
+ <i class="crm-i fa-check"></i>
+ </button>
+ <button type="button" ng-click="$ctrl.cancel()" class="btn btn-xs btn-danger">
+ <i class="crm-i fa-times"></i>
+ </button>
+</div>
$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();
-<ol ng-if=":: $ctrl.settings.style === 'ol'" ng-include="'~/crmSearchDisplayList/crmSearchDisplayListItems.html'" ng-style="{'list-style': $ctrl.settings.symbol}"></ol>
-<ul ng-if=":: $ctrl.settings.style !== 'ol'" ng-include="'~/crmSearchDisplayList/crmSearchDisplayListItems.html'" ng-style="{'list-style': $ctrl.settings.symbol}"></ul>
-<div ng-include="'~/crmSearchDisplay/Pager.html'"></div>
+<div class="crm-search-display crm-search-display-list">
+ <ol ng-if=":: $ctrl.settings.style === 'ol'" ng-include="'~/crmSearchDisplayList/crmSearchDisplayListItems.html'" ng-style="{'list-style': $ctrl.settings.symbol}"></ol>
+ <ul ng-if=":: $ctrl.settings.style !== 'ol'" ng-include="'~/crmSearchDisplayList/crmSearchDisplayListItems.html'" ng-style="{'list-style': $ctrl.settings.symbol}"></ul>
+ <div ng-include="'~/crmSearchDisplay/Pager.html'"></div>
+</div>
-<li ng-repeat="row in $ctrl.results">
+<li ng-repeat="(rowIndex, row) in $ctrl.results">
<div ng-repeat="col in $ctrl.settings.columns" title="{{:: displayUtils.replaceTokens(col.title, row, $ctrl.settings.columns) }}" class="{{:: col.break ? '' : 'crm-inline-block' }}">
<label ng-if=":: col.label && (col.type !== 'field' || col.forceLabel || row[col.key])">
{{:: displayUtils.replaceTokens(col.label, row, $ctrl.settings.columns) }}
$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 page after inline-editing a row
+ this.refresh = function(row) {
+ var rowId = row.id;
+ searchDisplayUtils.getResults(ctrl)
+ .then(function() {
+ // If edited row disappears (because edits cause it to not meet search criteria), deselect it
+ var index = ctrl.selectedRows.indexOf(rowId);
+ if (index > -1 && !_.findWhere(ctrl.results, {id: rowId})) {
+ ctrl.selectedRows.splice(index, 1);
+ }
+ });
+ };
+
+ function onChangeFilters() {
ctrl.page = 1;
ctrl.rowCount = null;
+ ctrl.selectedRows.legth = 0;
+ ctrl.allRowsSelected = false;
ctrl.getResults();
}
-<div class="form-inline" ng-if="$ctrl.settings.actions">
- <crm-search-actions entity="$ctrl.apiEntity" ids="$ctrl.selectedRows" refresh="$ctrl.getResults()"></crm-search-actions>
+<div class="crm-search-display crm-search-display-table">
+ <div class="form-inline" ng-if="$ctrl.settings.actions">
+ <crm-search-actions entity="$ctrl.apiEntity" ids="$ctrl.selectedRows" refresh="$ctrl.getResults()"></crm-search-actions>
+ </div>
+ <table>
+ <thead>
+ <tr>
+ <th class="crm-search-result-select" ng-if=":: $ctrl.settings.actions">
+ <input type="checkbox" ng-checked="$ctrl.allRowsSelected" ng-click="selectAllRows()" >
+ </th>
+ <th ng-repeat="col in $ctrl.settings.columns" ng-click="setSort(col, $event)" title="{{:: ts('Click to sort results (shift-click to sort by multiple).') }}">
+ <i ng-if="col.type === 'field'" class="crm-i {{ getSort(col) }}"></i>
+ <span>{{ col.label }}</span>
+ </th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr ng-repeat="(rowIndex, row) in $ctrl.results">
+ <td ng-if=":: $ctrl.settings.actions">
+ <input type="checkbox" ng-checked="isRowSelected(row)" ng-click="selectRow(row)" ng-disabled="!(!loadingAllRows && row.id)">
+ </td>
+ <td ng-repeat="col in $ctrl.settings.columns" ng-include="'~/crmSearchDisplay/colType/' + col.type + '.html'" title="{{:: displayUtils.replaceTokens(col.title, row, $ctrl.settings.columns) }}" class="{{:: col.alignment }}">
+ </td>
+ <td></td>
+ </tr>
+ </tbody>
+ </table>
+ <div ng-include="'~/crmSearchDisplay/Pager.html'"></div>
</div>
-<table>
- <thead>
- <tr>
- <th class="crm-search-result-select" ng-if="$ctrl.settings.actions">
- <input type="checkbox" ng-checked="$ctrl.allRowsSelected" ng-click="selectAllRows()" >
- </th>
- <th ng-repeat="col in $ctrl.settings.columns" ng-click="setSort(col, $event)" title="{{:: ts('Click to sort results (shift-click to sort by multiple).') }}">
- <i ng-if="col.type === 'field'" class="crm-i {{ getSort(col) }}"></i>
- <span>{{ col.label }}</span>
- </th>
- </tr>
- </thead>
- <tbody>
- <tr ng-repeat="row in $ctrl.results">
- <td ng-if="$ctrl.settings.actions">
- <input type="checkbox" ng-checked="isRowSelected(row)" ng-click="selectRow(row)" ng-disabled="!(!loadingAllRows && row.id)">
- </td>
- <td ng-repeat="col in $ctrl.settings.columns" ng-include="'~/crmSearchDisplay/colType/' + col.type + '.html'" title="{{:: displayUtils.replaceTokens(col.title, row, $ctrl.settings.columns) }}" class="{{:: col.alignment }}">
- </td>
- <td></td>
- </tr>
- </tbody>
-</table>
-<div ng-include="'~/crmSearchDisplay/Pager.html'"></div>
margin-top: 10px;
border: 1px solid lightgrey;
}
+
+.crm-search-display.crm-search-display-table td > crm-search-display-editable,
+.crm-search-display.crm-search-display-table td > .crm-editable-enabled {
+ display: block !important;
+}
+
+.crm-search-display crm-search-display-editable {
+ position: relative;
+}
+
+.crm-search-display crm-search-display-editable + span {
+ display: none !important;
+}
+
+.crm-search-display .crm-search-display-editable-buttons {
+ position: absolute;
+ bottom: -22px;
+ left: 0;
+}