From 957358aa4b1cde0a34d9f5fea6734fc1e59c145a Mon Sep 17 00:00:00 2001 From: Coleman Watts Date: Sat, 7 Aug 2021 11:42:12 -0400 Subject: [PATCH] SearchKit - Switch results table to use a search display This greatly simplifies the SearchKit admin code by creating a specialized searchDisplay (copied from crmSearchDisplayTable) which eliminates all the code for the admin screen to fetch, format and display results. --- .../Civi/Api4/Action/SearchDisplay/Run.php | 2 + ext/search_kit/ang/crmSearchAdmin.module.js | 9 + .../{compose/criteria.html => compose.html} | 0 .../ang/crmSearchAdmin/compose/controls.html | 14 - .../ang/crmSearchAdmin/compose/debug.html | 10 - .../ang/crmSearchAdmin/compose/pager.html | 35 -- .../ang/crmSearchAdmin/compose/results.html | 35 -- .../crmSearchAdmin.component.js | 399 +++--------------- .../ang/crmSearchAdmin/crmSearchAdmin.html | 7 +- .../crmSearchAdminDisplay.component.js | 81 +--- .../crmSearchAdminResultsTable.component.js | 108 +++++ .../crmSearchAdminResultsTable.html | 31 ++ .../crmSearchAdmin/resultsTable/debug.html | 17 + .../traits/searchDisplayBaseTrait.service.js | 19 +- .../traits/searchDisplayTasksTrait.service.js | 7 +- ext/search_kit/css/crmSearchAdmin.css | 7 - 16 files changed, 261 insertions(+), 520 deletions(-) rename ext/search_kit/ang/crmSearchAdmin/{compose/criteria.html => compose.html} (100%) delete mode 100644 ext/search_kit/ang/crmSearchAdmin/compose/controls.html delete mode 100644 ext/search_kit/ang/crmSearchAdmin/compose/debug.html delete mode 100644 ext/search_kit/ang/crmSearchAdmin/compose/pager.html delete mode 100644 ext/search_kit/ang/crmSearchAdmin/compose/results.html create mode 100644 ext/search_kit/ang/crmSearchAdmin/resultsTable/crmSearchAdminResultsTable.component.js create mode 100644 ext/search_kit/ang/crmSearchAdmin/resultsTable/crmSearchAdminResultsTable.html create mode 100644 ext/search_kit/ang/crmSearchAdmin/resultsTable/debug.html diff --git a/ext/search_kit/Civi/Api4/Action/SearchDisplay/Run.php b/ext/search_kit/Civi/Api4/Action/SearchDisplay/Run.php index d5a93d5aac..74bf8efae2 100644 --- a/ext/search_kit/Civi/Api4/Action/SearchDisplay/Run.php +++ b/ext/search_kit/Civi/Api4/Action/SearchDisplay/Run.php @@ -130,6 +130,7 @@ class Run extends \Civi\Api4\Generic\AbstractAction { $page = explode(':', $this->return)[1]; } $limit = !empty($settings['pager']['expose_limit']) && $this->limit ? $this->limit : NULL; + $apiParams['debug'] = $this->debug; $apiParams['limit'] = $limit ?? $settings['limit'] ?? NULL; $apiParams['offset'] = $page ? $apiParams['limit'] * ($page - 1) : 0; $apiParams['orderBy'] = $this->getOrderByFromSort(); @@ -142,6 +143,7 @@ class Run extends \Civi\Api4\Generic\AbstractAction { $result->rowCount = $apiResult->rowCount; $result->exchangeArray($apiResult->getArrayCopy()); + $result->debug = $apiResult->debug; } /** diff --git a/ext/search_kit/ang/crmSearchAdmin.module.js b/ext/search_kit/ang/crmSearchAdmin.module.js index 312466d47f..52bad76c33 100644 --- a/ext/search_kit/ang/crmSearchAdmin.module.js +++ b/ext/search_kit/ang/crmSearchAdmin.module.js @@ -245,6 +245,15 @@ deferred.resolve($(this).val()); }); return deferred.promise; + }, + // Returns name of explicit or implicit join, for links + getJoinEntity: function(info) { + if (info.field.fk_entity || info.field.name !== info.field.fieldName) { + return info.prefix + (info.field.fk_entity ? info.field.name : info.field.name.substr(0, info.field.name.lastIndexOf('.'))); + } else if (info.prefix) { + return info.prefix.replace('.', ''); + } + return ''; } }; }) diff --git a/ext/search_kit/ang/crmSearchAdmin/compose/criteria.html b/ext/search_kit/ang/crmSearchAdmin/compose.html similarity index 100% rename from ext/search_kit/ang/crmSearchAdmin/compose/criteria.html rename to ext/search_kit/ang/crmSearchAdmin/compose.html diff --git a/ext/search_kit/ang/crmSearchAdmin/compose/controls.html b/ext/search_kit/ang/crmSearchAdmin/compose/controls.html deleted file mode 100644 index bd2c52c2d4..0000000000 --- a/ext/search_kit/ang/crmSearchAdmin/compose/controls.html +++ /dev/null @@ -1,14 +0,0 @@ -
-
-
- - -
- -
diff --git a/ext/search_kit/ang/crmSearchAdmin/compose/debug.html b/ext/search_kit/ang/crmSearchAdmin/compose/debug.html deleted file mode 100644 index 4bb483d1af..0000000000 --- a/ext/search_kit/ang/crmSearchAdmin/compose/debug.html +++ /dev/null @@ -1,10 +0,0 @@ - diff --git a/ext/search_kit/ang/crmSearchAdmin/compose/pager.html b/ext/search_kit/ang/crmSearchAdmin/compose/pager.html deleted file mode 100644 index 954911b1aa..0000000000 --- a/ext/search_kit/ang/crmSearchAdmin/compose/pager.html +++ /dev/null @@ -1,35 +0,0 @@ -
-
-
- - - -
-
-
-
    -
    -
    - - -
    -
    diff --git a/ext/search_kit/ang/crmSearchAdmin/compose/results.html b/ext/search_kit/ang/crmSearchAdmin/compose/results.html deleted file mode 100644 index 1f7f053176..0000000000 --- a/ext/search_kit/ang/crmSearchAdmin/compose/results.html +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - - - - - - - - - -
    - - - - {{ $ctrl.getFieldLabel(col) }} - - - - - - -
    - -
    -
    -

    {{:: ts('An error occurred') }}

    -

    {{ error }}

    -
    diff --git a/ext/search_kit/ang/crmSearchAdmin/crmSearchAdmin.component.js b/ext/search_kit/ang/crmSearchAdmin/crmSearchAdmin.component.js index 532726c2a2..86e9fa816a 100644 --- a/ext/search_kit/ang/crmSearchAdmin/crmSearchAdmin.component.js +++ b/ext/search_kit/ang/crmSearchAdmin/crmSearchAdmin.component.js @@ -13,17 +13,7 @@ this.DEFAULT_AGGREGATE_FN = 'GROUP_CONCAT'; - this.selectedRows = []; - this.limit = CRM.crmSearchAdmin.defaultPagerSize; - this.page = 1; this.displayTypes = _.indexBy(CRM.crmSearchAdmin.displayTypes, 'id'); - // After a search this.results is an object of result arrays keyed by page, - // Initially this.results is an empty string because 1: it's falsey (unlike an empty object) and 2: it doesn't throw an error if you try to access undefined properties (unlike null) - this.results = ''; - this.rowCount = false; - this.allRowsSelected = false; - // Have the filters (WHERE, HAVING, GROUP BY, JOIN) changed? - this.stale = true; $scope.controls = {tab: 'compose', joinType: 'LEFT'}; $scope.joinTypes = [ @@ -69,21 +59,16 @@ $scope.$watchCollection('$ctrl.savedSearch.api_params.select', onChangeSelect); - $scope.$watch('$ctrl.savedSearch.api_params.where', onChangeFilters, true); - if (this.paramExists('groupBy')) { this.savedSearch.api_params.groupBy = this.savedSearch.api_params.groupBy || []; - $scope.$watchCollection('$ctrl.savedSearch.api_params.groupBy', onChangeFilters); } if (this.paramExists('join')) { this.savedSearch.api_params.join = this.savedSearch.api_params.join || []; - $scope.$watch('$ctrl.savedSearch.api_params.join', onChangeFilters, true); } if (this.paramExists('having')) { this.savedSearch.api_params.having = this.savedSearch.api_params.having || []; - $scope.$watch('$ctrl.savedSearch.api_params.having', onChangeFilters, true); } $scope.$watch('$ctrl.savedSearch', onChangeAnything, true); @@ -382,37 +367,6 @@ return !errors.length; } - /** - * Called when clicking on a column header - * @param col - * @param $event - */ - $scope.setOrderBy = function(col, $event) { - col = _.last(col.split(' AS ')); - var dir = $scope.getOrderBy(col) === 'fa-sort-asc' ? 'DESC' : 'ASC'; - if (!$event.shiftKey || !ctrl.savedSearch.api_params.orderBy) { - ctrl.savedSearch.api_params.orderBy = {}; - } - ctrl.savedSearch.api_params.orderBy[col] = dir; - if (ctrl.results) { - ctrl.refreshPage(); - } - }; - - /** - * Returns crm-i icon class for a sortable column - * @param col - * @returns {string} - */ - $scope.getOrderBy = function(col) { - col = _.last(col.split(' AS ')); - var dir = ctrl.savedSearch.api_params.orderBy && ctrl.savedSearch.api_params.orderBy[col]; - if (dir) { - return 'fa-sort-' + dir.toLowerCase(); - } - return 'fa-sort disabled'; - }; - this.addParam = function(name, value) { if (value && !_.contains(ctrl.savedSearch.api_params[name], value)) { ctrl.savedSearch.api_params[name].push(value); @@ -426,148 +380,6 @@ ctrl.savedSearch.api_params[name].splice(idx, 1); }; - // Prevent visual jumps in results table height during loading - function lockTableHeight() { - var $table = $('.crm-search-results', $element); - $table.css('height', $table.height()); - } - - function unlockTableHeight() { - $('.crm-search-results', $element).css('height', ''); - } - - // Debounced callback for loadResults - function _loadResultsCallback() { - // Multiply limit to read 2 pages at once & save ajax requests - var params = _.merge(_.cloneDeep(ctrl.savedSearch.api_params), {debug: true, limit: ctrl.limit * 2}); - // Select the join field of implicitly joined entities (helps with displaying links) - _.each(params.select, function(fieldName) { - if (_.includes(fieldName, '.') && !_.includes(fieldName, ' AS ')) { - var info = searchMeta.parseExpr(fieldName); - if (info.field && !info.suffix && !info.fn && (info.field.name !== info.field.fieldName)) { - var idField = fieldName.substr(0, fieldName.lastIndexOf('.')); - if (!_.includes(params.select, idField) && !ctrl.canAggregate(idField)) { - params.select.push(idField); - } - } - } - }); - // Select primary key of explicitly joined entities (helps with displaying links) - _.each(params.join, function(join) { - var entity = join[0].split(' AS ')[0], - alias = join[0].split(' AS ')[1], - primaryKeys = searchMeta.getEntity(entity).primary_key, - idField = alias + '.' + primaryKeys[0]; - if (primaryKeys.length && !_.includes(params.select, idField) && !ctrl.canAggregate(idField)) { - params.select.push(idField); - } - }); - lockTableHeight(); - $scope.error = false; - if (ctrl.stale) { - ctrl.page = 1; - ctrl.rowCount = false; - } - params.offset = ctrl.limit * (ctrl.page - 1); - crmApi4(ctrl.savedSearch.api_entity, 'get', params).then(function(success) { - if (ctrl.stale) { - ctrl.results = {}; - // Get row count for pager - if (success.length < params.limit) { - ctrl.rowCount = success.count; - } else { - var countParams = _.cloneDeep(params); - // Select is only needed needed by HAVING - countParams.select = countParams.having && countParams.having.length ? countParams.select : []; - countParams.select.push('row_count'); - delete countParams.debug; - crmApi4(ctrl.savedSearch.api_entity, 'get', countParams).then(function(result) { - ctrl.rowCount = result.count; - }); - } - } - ctrl.debug = success.debug; - // populate this page & the next - ctrl.results[ctrl.page] = success.slice(0, ctrl.limit); - if (success.length > ctrl.limit) { - ctrl.results[ctrl.page + 1] = success.slice(ctrl.limit); - } - $scope.loading = false; - ctrl.stale = false; - unlockTableHeight(); - }, function(error) { - $scope.loading = false; - ctrl.results = {}; - ctrl.stale = true; - ctrl.debug = error.debug; - $scope.error = errorMsg(error); - }) - .finally(function() { - if (ctrl.debug) { - ctrl.debug.params = JSON.stringify(params, null, 2); - if (ctrl.debug.timeIndex) { - ctrl.debug.timeIndex = Number.parseFloat(ctrl.debug.timeIndex).toPrecision(2); - } - } - }); - } - - var _loadResults = _.debounce(_loadResultsCallback, 250); - - function loadResults() { - $scope.loading = true; - _loadResults(); - } - - // What to tell the user when search returns an error from the server - // Todo: parse error codes and give helpful feedback. - function errorMsg(error) { - return ts('Ensure all search critera are set correctly and try again.'); - } - - this.changePage = function() { - if (ctrl.stale || !ctrl.results[ctrl.page]) { - lockTableHeight(); - loadResults(); - } - }; - - this.refreshAll = function() { - ctrl.stale = true; - clearSelection(); - loadResults(); - }; - - // Refresh results while staying on current page. - this.refreshPage = function() { - lockTableHeight(); - ctrl.results = {}; - loadResults(); - }; - - $scope.onClickSearch = function() { - if (ctrl.autoSearch) { - ctrl.autoSearch = false; - } else { - ctrl.refreshAll(); - } - }; - - $scope.onClickAuto = function() { - ctrl.autoSearch = !ctrl.autoSearch; - if (ctrl.autoSearch && ctrl.stale) { - ctrl.refreshAll(); - } - $('.crm-search-auto-toggle').blur(); - }; - - $scope.onChangeLimit = function() { - // Refresh only if search has already been run - if (ctrl.autoSearch || ctrl.results) { - ctrl.refreshAll(); - } - }; - function onChangeSelect(newSelect, oldSelect) { // When removing a column from SELECT, also remove from ORDER BY & HAVING _.each(_.difference(oldSelect, newSelect), function(col) { @@ -577,68 +389,8 @@ return clauseUsesFields(clause, [col]); }); }); - // Re-arranging or removing columns doesn't merit a refresh, only adding columns does - if (!oldSelect || _.difference(newSelect, oldSelect).length) { - if (ctrl.autoSearch) { - ctrl.refreshPage(); - } else { - ctrl.stale = true; - } - } - } - - function onChangeFilters() { - ctrl.stale = true; - clearSelection(); - if (ctrl.autoSearch) { - ctrl.refreshAll(); - } - } - - function clearSelection() { - ctrl.allRowsSelected = false; - ctrl.selectedRows.length = 0; } - $scope.selectAllRows = function() { - // Deselect all - if (ctrl.allRowsSelected) { - clearSelection(); - return; - } - // Select all - ctrl.allRowsSelected = true; - if (ctrl.page === 1 && ctrl.results[1].length < ctrl.limit) { - ctrl.selectedRows = _.pluck(ctrl.results[1], 'id'); - return; - } - // If more than one page of results, use ajax to fetch all ids - $scope.loadingAllRows = true; - var params = _.cloneDeep(ctrl.savedSearch.api_params); - // Select is only needed needed by HAVING - params.select = params.having && params.having.length ? params.select : []; - params.select.push('id'); - crmApi4(ctrl.savedSearch.api_entity, 'get', params, ['id']).then(function(ids) { - $scope.loadingAllRows = false; - ctrl.selectedRows = _.toArray(ids); - }); - }; - - $scope.selectRow = function(row) { - var index = ctrl.selectedRows.indexOf(row.id); - if (index < 0) { - ctrl.selectedRows.push(row.id); - ctrl.allRowsSelected = (ctrl.rowCount === ctrl.selectedRows.length); - } else { - ctrl.allRowsSelected = false; - ctrl.selectedRows.splice(index, 1); - } - }; - - $scope.isRowSelected = function(row) { - return ctrl.allRowsSelected || _.includes(ctrl.selectedRows, row.id); - }; - this.getFieldLabel = searchMeta.getDefaultLabel; // Is a column eligible to use an aggregate function? @@ -657,70 +409,6 @@ return ctrl.savedSearch.api_params.groupBy.indexOf(info.prefix + idField) < 0; }; - $scope.formatResult = function(row, col) { - var info = searchMeta.parseExpr(col), - value = row[info.alias]; - return formatFieldValue(row, info, value); - }; - - // Attempts to construct a view url for a given entity - function getEntityUrl(row, info, index) { - var entity = searchMeta.getEntity(info.field.entity), - path = _.result(_.findWhere(entity.paths, {action: 'view'}), 'path'); - // Only proceed if the path metadata exists for this entity - if (path) { - // Replace tokens in the path (e.g. [id]) - var tokens = path.match(/\[\w*]/g) || [], - prefix = info.prefix; - var replacements = _.transform(tokens, function(replacements, token) { - var fieldName = token.slice(1, token.length - 1); - // For implicit join fields - if (fieldName === 'id' && info.field.name !== info.field.fieldName) { - fieldName = info.field.name.substr(0, info.field.name.lastIndexOf('.')); - } - var replacement = row[prefix + fieldName]; - if (replacement) { - replacements.push(_.isArray(replacement) ? replacement[index] : replacement); - } - }); - // Only proceed if the row contains all the necessary data to resolve tokens - if (tokens.length === replacements.length) { - _.each(tokens, function(token, key) { - path = path.replace(token, replacements[key]); - }); - return {url: CRM.url(path), title: path.title}; - } - } - } - - function formatFieldValue(row, info, value, index) { - var type = (info.fn && info.fn.dataType) || info.field.data_type, - result = value, - link; - if (_.isArray(value)) { - return _.map(value, function(val, idx) { - return formatFieldValue(row, info, val, idx); - }).join(', '); - } - if (value && (type === 'Date' || type === 'Timestamp') && /^\d{4}-\d{2}-\d{2}/.test(value)) { - result = CRM.utils.formatDate(value, null, type === 'Timestamp'); - } - else if (type === 'Boolean' && typeof value === 'boolean') { - result = value ? ts('Yes') : ts('No'); - } - else if (type === 'Money' && typeof value === 'number') { - result = CRM.formatMoney(value); - } - // Output user-facing name/label fields as a link, if possible - if (info.field.fieldName === searchMeta.getEntity(info.field.entity).label_field && !info.fn) { - link = getEntityUrl(row, info, index || 0); - } - if (link) { - return '' + _.escape(result) + ''; - } - return _.escape(result); - } - $scope.fieldsForGroupBy = function() { return {results: ctrl.getAllFields('', ['Field', 'Custom'], function(key) { return _.contains(ctrl.savedSearch.api_params.groupBy, key); @@ -728,13 +416,6 @@ }; }; - $scope.fieldsForSelect = function() { - return {results: ctrl.getAllFields(':label', ['Field', 'Custom', 'Extra'], function(key) { - return _.contains(ctrl.savedSearch.api_params.select, key); - }) - }; - }; - function getFieldsForJoin(joinEntity) { return {results: ctrl.getAllFields(':name', ['Field', 'Custom'], null, joinEntity)}; } @@ -754,17 +435,6 @@ return {results: ctrl.getSelectFields()}; }; - $scope.sortableColumnOptions = { - axis: 'x', - handle: '.crm-draggable', - update: function(e, ui) { - // Don't allow items to be moved to position 0 if locked - if (!ui.item.sortable.dropindex && ctrl.groupExists) { - ui.item.sortable.cancel(); - } - } - }; - // Sets the default select clause based on commonly-named fields function getDefaultSelect() { var entity = searchMeta.getEntity(ctrl.savedSearch.api_entity); @@ -907,6 +577,75 @@ } } + // Build a list of all possible links to main entity & join entities + this.buildLinks = function() { + function addTitle(link, entityName) { + switch (link.action) { + case 'view': + link.title = ts('View %1', {1: entityName}); + break; + + case 'update': + link.title = ts('Edit %1', {1: entityName}); + break; + + case 'delete': + link.title = ts('Delete %1', {1: entityName}); + break; + } + } + + // Links to main entity + var mainEntity = searchMeta.getEntity(ctrl.savedSearch.api_entity), + links = _.cloneDeep(mainEntity.paths || []); + _.each(links, function(link) { + link.join = ''; + addTitle(link, mainEntity.title); + }); + // Links to explicitly joined entities + _.each(ctrl.savedSearch.api_params.join, function(joinClause) { + var join = searchMeta.getJoin(joinClause[0]), + joinEntity = searchMeta.getEntity(join.entity), + bridgeEntity = _.isString(joinClause[2]) ? searchMeta.getEntity(joinClause[2]) : null; + _.each(joinEntity.paths, function(path) { + var link = _.cloneDeep(path); + link.path = link.path.replace(/\[/g, '[' + join.alias + '.'); + link.join = join.alias; + addTitle(link, join.label); + links.push(link); + }); + _.each(bridgeEntity && bridgeEntity.paths, function(path) { + var link = _.cloneDeep(path); + link.path = link.path.replace(/\[/g, '[' + join.alias + '.'); + link.join = join.alias; + addTitle(link, join.label + (bridgeEntity.bridge_title ? ' ' + bridgeEntity.bridge_title : '')); + links.push(link); + }); + }); + // Links to implicit joins + _.each(ctrl.savedSearch.api_params.select, function(fieldName) { + if (!_.includes(fieldName, ' AS ')) { + var info = searchMeta.parseExpr(fieldName); + if (info.field && !info.suffix && !info.fn && (info.field.fk_entity || info.field.name !== info.field.fieldName)) { + var idFieldName = info.field.fk_entity ? fieldName : fieldName.substr(0, fieldName.lastIndexOf('.')), + idField = searchMeta.parseExpr(idFieldName).field; + if (!ctrl.canAggregate(idFieldName)) { + var joinEntity = searchMeta.getEntity(idField.fk_entity), + label = (idField.join ? idField.join.label + ': ' : '') + (idField.input_attrs && idField.input_attrs.label || idField.label); + _.each((joinEntity || {}).paths, function(path) { + var link = _.cloneDeep(path); + link.path = link.path.replace(/\[id/g, '[' + idFieldName); + link.join = idFieldName; + addTitle(link, label); + links.push(link); + }); + } + } + } + }); + return _.uniq(links, 'path'); + }; + } }); diff --git a/ext/search_kit/ang/crmSearchAdmin/crmSearchAdmin.html b/ext/search_kit/ang/crmSearchAdmin/crmSearchAdmin.html index db7c24aa50..a486708809 100644 --- a/ext/search_kit/ang/crmSearchAdmin/crmSearchAdmin.html +++ b/ext/search_kit/ang/crmSearchAdmin/crmSearchAdmin.html @@ -33,11 +33,8 @@
    -
    -
    -
    -
    -
    +
    +
    diff --git a/ext/search_kit/ang/crmSearchAdmin/crmSearchAdminDisplay.component.js b/ext/search_kit/ang/crmSearchAdmin/crmSearchAdminDisplay.component.js index 21b6469b10..7ea85b3888 100644 --- a/ext/search_kit/ang/crmSearchAdmin/crmSearchAdminDisplay.component.js +++ b/ext/search_kit/ang/crmSearchAdmin/crmSearchAdminDisplay.component.js @@ -190,95 +190,20 @@ this.getLinks = function(columnKey) { if (!ctrl.links) { - ctrl.links = {'*': buildLinks()}; + ctrl.links = {'*': ctrl.crmSearchAdmin.buildLinks()}; } if (!columnKey) { return ctrl.links['*']; } var expr = ctrl.getExprFromSelect(columnKey), info = searchMeta.parseExpr(expr), - joinEntity = ''; - if (info.field.fk_entity || info.field.name !== info.field.fieldName) { - joinEntity = info.prefix + (info.field.fk_entity ? info.field.name : info.field.name.substr(0, info.field.name.lastIndexOf('.'))); - } else if (info.prefix) { - joinEntity = info.prefix.replace('.', ''); - } + joinEntity = searchMeta.getJoinEntity(info); if (!ctrl.links[joinEntity]) { - ctrl.links[joinEntity] = _.filter(ctrl.links['*'], function(link) { - return joinEntity === (link.join || ''); - }); + ctrl.links[joinEntity] = _.filter(ctrl.links['*'], {join: joinEntity}); } return ctrl.links[joinEntity]; }; - // Build a list of all possible links to main entity or join entities - function buildLinks() { - function addTitle(link, entityName) { - switch (link.action) { - case 'view': - link.title = ts('View %1', {1: entityName}); - break; - - case 'update': - link.title = ts('Edit %1', {1: entityName}); - break; - - case 'delete': - link.title = ts('Delete %1', {1: entityName}); - break; - } - } - - // Links to main entity - var mainEntity = searchMeta.getEntity(ctrl.savedSearch.api_entity), - links = _.cloneDeep(mainEntity.paths || []); - _.each(links, function(link) { - addTitle(link, mainEntity.title); - }); - // Links to explicitly joined entities - _.each(ctrl.savedSearch.api_params.join, function(joinClause) { - var join = searchMeta.getJoin(joinClause[0]), - joinEntity = searchMeta.getEntity(join.entity), - bridgeEntity = _.isString(joinClause[2]) ? searchMeta.getEntity(joinClause[2]) : null; - _.each(joinEntity.paths, function(path) { - var link = _.cloneDeep(path); - link.path = link.path.replace(/\[/g, '[' + join.alias + '.'); - link.join = join.alias; - addTitle(link, join.label); - links.push(link); - }); - _.each(bridgeEntity && bridgeEntity.paths, function(path) { - var link = _.cloneDeep(path); - link.path = link.path.replace(/\[/g, '[' + join.alias + '.'); - link.join = join.alias; - addTitle(link, join.label + (bridgeEntity.bridge_title ? ' ' + bridgeEntity.bridge_title : '')); - links.push(link); - }); - }); - // Links to implicit joins - _.each(ctrl.savedSearch.api_params.select, function(fieldName) { - if (!_.includes(fieldName, ' AS ')) { - var info = searchMeta.parseExpr(fieldName); - if (info.field && !info.suffix && !info.fn && (info.field.fk_entity || info.field.name !== info.field.fieldName)) { - var idFieldName = info.field.fk_entity ? fieldName : fieldName.substr(0, fieldName.lastIndexOf('.')), - idField = searchMeta.parseExpr(idFieldName).field; - if (!ctrl.crmSearchAdmin.canAggregate(idFieldName)) { - var joinEntity = searchMeta.getEntity(idField.fk_entity), - label = (idField.join ? idField.join.label + ': ' : '') + (idField.input_attrs && idField.input_attrs.label || idField.label); - _.each((joinEntity || {}).paths, function(path) { - var link = _.cloneDeep(path); - link.path = link.path.replace(/\[id/g, '[' + idFieldName); - link.join = idFieldName; - addTitle(link, label); - links.push(link); - }); - } - } - } - }); - return _.uniq(links, 'path'); - } - this.pickIcon = function(model, key) { searchMeta.pickIcon().then(function(icon) { model[key] = icon; diff --git a/ext/search_kit/ang/crmSearchAdmin/resultsTable/crmSearchAdminResultsTable.component.js b/ext/search_kit/ang/crmSearchAdmin/resultsTable/crmSearchAdminResultsTable.component.js new file mode 100644 index 0000000000..2bc06594bb --- /dev/null +++ b/ext/search_kit/ang/crmSearchAdmin/resultsTable/crmSearchAdminResultsTable.component.js @@ -0,0 +1,108 @@ +(function(angular, $, _) { + "use strict"; + + // Specialized searchDisplay, only used by Admins + angular.module('crmSearchAdmin').component('crmSearchAdminResultsTable', { + bindings: { + search: '<' + }, + require: { + crmSearchAdmin: '^crmSearchAdmin' + }, + templateUrl: '~/crmSearchAdmin/resultsTable/crmSearchAdminResultsTable.html', + controller: function($scope, searchMeta, searchDisplayBaseTrait, searchDisplayTasksTrait, searchDisplaySortableTrait) { + var ts = $scope.ts = CRM.ts('org.civicrm.search_kit'), + // Mix in traits to this controller + ctrl = angular.extend(this, searchDisplayBaseTrait, searchDisplayTasksTrait, searchDisplaySortableTrait); + + // Output user-facing name/label fields as a link, if possible + function getViewLink(fieldExpr, links) { + var info = searchMeta.parseExpr(fieldExpr), + entity = searchMeta.getEntity(info.field.entity); + if (!info.fn && entity && info.field.fieldName === entity.label_field) { + var joinEntity = searchMeta.getJoinEntity(info); + return _.find(links, {join: joinEntity, action: 'view'}); + } + } + + function buildSettings() { + var links = ctrl.crmSearchAdmin.buildLinks(); + ctrl.apiEntity = ctrl.search.api_entity; + ctrl.display = { + type: 'table', + settings: { + limit: CRM.crmSearchAdmin.defaultPagerSize, + pager: {show_count: true, expose_limit: true}, + actions: true, + button: ts('Search'), + columns: _.transform(ctrl.search.api_params.select, function(columns, fieldExpr) { + var column = {label: true}, + link = getViewLink(fieldExpr, links); + if (link) { + column.title = link.title; + column.link = { + path: link.path, + target: '_blank' + }; + } + columns.push(searchMeta.fieldToColumn(fieldExpr, column)); + }) + } + }; + ctrl.debug = { + apiParams: JSON.stringify(ctrl.search.api_params, null, 2) + }; + ctrl.settings = ctrl.display.settings; + } + + this.$onInit = function() { + buildSettings(); + this.initializeDisplay($scope, $()); + $scope.$watch('$ctrl.search.api_entity', buildSettings); + $scope.$watch('$ctrl.search.api_params', buildSettings, true); + }; + + // Refresh current page + this.refresh = function(row) { + ctrl.runSearch(); + }; + + // Add callbacks for pre & post run + this.onPreRun.push(function(apiParams) { + apiParams.debug = true; + }); + + this.onPostRun.push(function(result) { + ctrl.debug = _.extend(_.pick(ctrl.debug, 'apiParams'), result.debug); + }); + + $scope.sortableColumnOptions = { + axis: 'x', + handle: '.crm-draggable', + update: function(e, ui) { + // Don't allow items to be moved to position 0 if locked + if (!ui.item.sortable.dropindex && ctrl.crmSearchAdmin.groupExists) { + ui.item.sortable.cancel(); + } + } + }; + + $scope.fieldsForSelect = function() { + return {results: ctrl.crmSearchAdmin.getAllFields(':label', ['Field', 'Custom', 'Extra'], function(key) { + return _.contains(ctrl.search.api_params.select, key); + }) + }; + }; + + $scope.addColumn = function(col) { + ctrl.crmSearchAdmin.addParam('select', col); + }; + + $scope.removeColumn = function(index) { + ctrl.crmSearchAdmin.clearParam('select', index); + }; + + } + }); + +})(angular, CRM.$, CRM._); diff --git a/ext/search_kit/ang/crmSearchAdmin/resultsTable/crmSearchAdminResultsTable.html b/ext/search_kit/ang/crmSearchAdmin/resultsTable/crmSearchAdminResultsTable.html new file mode 100644 index 0000000000..67ac27fef9 --- /dev/null +++ b/ext/search_kit/ang/crmSearchAdmin/resultsTable/crmSearchAdminResultsTable.html @@ -0,0 +1,31 @@ +
    +
    +
    +
    + +
    + + + + + + + + + +
    + + + + {{ $ctrl.settings.columns[$index].label }} + + + + + + +
    +
    +
    diff --git a/ext/search_kit/ang/crmSearchAdmin/resultsTable/debug.html b/ext/search_kit/ang/crmSearchAdmin/resultsTable/debug.html new file mode 100644 index 0000000000..d435faed5d --- /dev/null +++ b/ext/search_kit/ang/crmSearchAdmin/resultsTable/debug.html @@ -0,0 +1,17 @@ +
    + + + {{:: ts('Query Info') }} + +
    +
    {{ ts('Request took %1 seconds.', {1: $ctrl.debug.timeIndex}) }}
    +
    + API: +
    +
    {{ $ctrl.debug.apiParams }}
    +
    + SQL: +
    +
    {{ query }}
    +
    +
    diff --git a/ext/search_kit/ang/crmSearchDisplay/traits/searchDisplayBaseTrait.service.js b/ext/search_kit/ang/crmSearchDisplay/traits/searchDisplayBaseTrait.service.js index 62f8cd0d90..86900be0db 100644 --- a/ext/search_kit/ang/crmSearchDisplay/traits/searchDisplayBaseTrait.service.js +++ b/ext/search_kit/ang/crmSearchDisplay/traits/searchDisplayBaseTrait.service.js @@ -79,6 +79,10 @@ page: 1, rowCount: null, getUrl: getUrl, + // Arrays may contain callback functions for various events + onChangeFilters: [], + onPreRun: [], + onPostRun: [], // Called by the controller's $onInit function initializeDisplay: function($scope, $element) { @@ -106,9 +110,9 @@ function onChangeFilters() { ctrl.page = 1; ctrl.rowCount = null; - if (ctrl.onChangeFilters) { - ctrl.onChangeFilters(); - } + _.each(ctrl.onChangeFilters, function(callback) { + callback.call(ctrl); + }); if (!ctrl.settings.button) { ctrl.getResults(); } @@ -149,6 +153,9 @@ var ctrl = this, apiParams = this.getApiParams(); this.loading = true; + _.each(ctrl.onPreRun, function(callback) { + callback.call(ctrl, apiParams); + }); return crmApi4('SearchDisplay', 'run', apiParams).then(function(results) { ctrl.results = results; ctrl.editing = ctrl.loading = false; @@ -162,9 +169,15 @@ }); } } + _.each(ctrl.onPostRun, function(callback) { + callback.call(ctrl, results, 'success'); + }); }, function(error) { ctrl.results = []; ctrl.editing = ctrl.loading = false; + _.each(ctrl.onPostRun, function(callback) { + callback.call(ctrl, error, 'error'); + }); }); }, replaceTokens: function(value, row) { diff --git a/ext/search_kit/ang/crmSearchTasks/traits/searchDisplayTasksTrait.service.js b/ext/search_kit/ang/crmSearchTasks/traits/searchDisplayTasksTrait.service.js index 1644052869..27475eac63 100644 --- a/ext/search_kit/ang/crmSearchTasks/traits/searchDisplayTasksTrait.service.js +++ b/ext/search_kit/ang/crmSearchTasks/traits/searchDisplayTasksTrait.service.js @@ -52,11 +52,12 @@ return this.allRowsSelected || _.includes(this.selectedRows, row.id); }, - // Reset selection when filters are changed - onChangeFilters: function() { + // Overwrite empty onChangeFilters array from searchDisplayBaseTrait + onChangeFilters: [function() { + // Reset selection when filters are changed this.selectedRows.length = 0; this.allRowsSelected = false; - } + }] }; }); diff --git a/ext/search_kit/css/crmSearchAdmin.css b/ext/search_kit/css/crmSearchAdmin.css index eceab7fcf6..cf3b0743c9 100644 --- a/ext/search_kit/css/crmSearchAdmin.css +++ b/ext/search_kit/css/crmSearchAdmin.css @@ -13,13 +13,6 @@ min-width: 500px; } -#bootstrap-theme #crm-search-results-page-size { - width: 5em; -} -#bootstrap-theme .crm-search-results { - min-height: 200px; -} - #bootstrap-theme.crm-search .nav-stacked { margin-left: 0; margin-right: 20px; -- 2.25.1