From b8ecfad280bda11c5d90f7f029651926c74b63ad Mon Sep 17 00:00:00 2001 From: Coleman Watts Date: Tue, 13 Sep 2022 17:08:03 -0400 Subject: [PATCH 1/3] SearchKit - Improve metadata for ContactType & add standalone path to edit form Giving the listing page and the edit form seperate url paths allow each to be replaced incrimentally by the civicrm_admin_ui extension. --- CRM/Admin/Form/ContactType.php | 7 +++++++ CRM/Admin/Page/ContactType.php | 5 ++--- CRM/Contact/BAO/ContactType.php | 20 +++++++++++++++++++ CRM/Contact/DAO/ContactType.php | 13 +++++++++++- CRM/Core/xml/Menu/Admin.xml | 5 +++++ CRM/Utils/Check/Component/ContactTypes.php | 2 +- Civi/Api4/ContactType.php | 2 +- Civi/Api4/Generic/DAODeleteAction.php | 4 +++- .../SearchDisplay/AbstractRunAction.php | 11 ++++++---- xml/schema/Contact/ContactType.xml | 5 +++++ 10 files changed, 63 insertions(+), 11 deletions(-) diff --git a/CRM/Admin/Form/ContactType.php b/CRM/Admin/Form/ContactType.php index 386bd4785c49..676a7e4e9758 100644 --- a/CRM/Admin/Form/ContactType.php +++ b/CRM/Admin/Form/ContactType.php @@ -20,6 +20,13 @@ */ class CRM_Admin_Form_ContactType extends CRM_Admin_Form { + public function preProcess(): void { + CRM_Utils_Request::retrieve('action', 'String', $this); + CRM_Utils_Request::retrieve('id', 'Positive', $this, FALSE, 0); + $this->set('BAOName', 'CRM_Contact_BAO_ContactType'); + parent::preProcess(); + } + /** * Build the form object. */ diff --git a/CRM/Admin/Page/ContactType.php b/CRM/Admin/Page/ContactType.php index be396a9709fe..593c583f3c49 100644 --- a/CRM/Admin/Page/ContactType.php +++ b/CRM/Admin/Page/ContactType.php @@ -50,7 +50,7 @@ public function &links() { self::$_links = [ CRM_Core_Action::UPDATE => [ 'name' => ts('Edit'), - 'url' => 'civicrm/admin/options/subtype', + 'url' => 'civicrm/admin/options/subtype/edit', 'qs' => 'action=update&id=%%id%%&reset=1', 'title' => ts('Edit Contact Type'), ], @@ -66,7 +66,7 @@ public function &links() { ], CRM_Core_Action::DELETE => [ 'name' => ts('Delete'), - 'url' => 'civicrm/admin/options/subtype', + 'url' => 'civicrm/admin/options/subtype/edit', 'qs' => 'action=delete&id=%%id%%', 'title' => ts('Delete Contact Type'), ], @@ -81,7 +81,6 @@ public function &links() { public function run() { $action = CRM_Utils_Request::retrieve('action', 'String', $this, FALSE, 0); $this->assign('action', $action); - $id = CRM_Utils_Request::retrieve('id', 'Positive', $this, FALSE, 0); if (!$action) { $this->browse(); } diff --git a/CRM/Contact/BAO/ContactType.php b/CRM/Contact/BAO/ContactType.php index dfafc2c0c751..dcf4150abde9 100644 --- a/CRM/Contact/BAO/ContactType.php +++ b/CRM/Contact/BAO/ContactType.php @@ -886,4 +886,24 @@ public static function getAllContactTypes() { return $contactTypes; } + /** + * @param string $entityName + * @param string $action + * @param array $record + * @param $userID + * @return bool + * @see CRM_Core_DAO::checkAccess + */ + public static function _checkAccess(string $entityName, string $action, array $record, $userID): bool { + // Only records with a parent may be deleted + if ($action === 'delete') { + if (!array_key_exists('parent_id', $record)) { + $record['parent_id'] = CRM_Core_DAO::getFieldValue(parent::class, $record['id'], 'parent_id'); + } + return (bool) $record['parent_id']; + } + // Gatekeeper permissions suffice for everything else + return TRUE; + } + } diff --git a/CRM/Contact/DAO/ContactType.php b/CRM/Contact/DAO/ContactType.php index 0ae0d5274892..6d795d0493f0 100644 --- a/CRM/Contact/DAO/ContactType.php +++ b/CRM/Contact/DAO/ContactType.php @@ -6,7 +6,7 @@ * * Generated from xml/schema/CRM/Contact/ContactType.xml * DO NOT EDIT. Generated by CRM_Core_CodeGen - * (GenCodeChecksum:13c81c203681009e8f71fd8387ebdbc2) + * (GenCodeChecksum:9858d69cd4bfdc5a3ce45c77eecd1145) */ /** @@ -30,6 +30,17 @@ class CRM_Contact_DAO_ContactType extends CRM_Core_DAO { */ public static $_log = FALSE; + /** + * Paths for accessing this entity in the UI. + * + * @var string[] + */ + protected static $_paths = [ + 'add' => 'civicrm/admin/options/subtype/edit?action=add&reset=1', + 'update' => 'civicrm/admin/options/subtype/edit?action=update&id=[id]&reset=1', + 'delete' => 'civicrm/admin/options/subtype/edit?action=delete&id=[id]&reset=1', + ]; + /** * Contact Type ID * diff --git a/CRM/Core/xml/Menu/Admin.xml b/CRM/Core/xml/Menu/Admin.xml index 596018228add..030b2b03d974 100644 --- a/CRM/Core/xml/Menu/Admin.xml +++ b/CRM/Core/xml/Menu/Admin.xml @@ -125,6 +125,11 @@ Customize Data and Screens 40 + + civicrm/admin/options/subtype/edit + Edit Contact Type + CRM_Admin_Form_ContactType + civicrm/admin/options/gender Gender Options diff --git a/CRM/Utils/Check/Component/ContactTypes.php b/CRM/Utils/Check/Component/ContactTypes.php index bfd2983d1f5b..ebeee75f9d68 100644 --- a/CRM/Utils/Check/Component/ContactTypes.php +++ b/CRM/Utils/Check/Component/ContactTypes.php @@ -41,7 +41,7 @@ public function checkContactTypeIcons() { 'fa-picture-o' ); foreach ($contactTypesWithImages as $contactType) { - $message->addAction($contactType['label'], FALSE, 'href', ['path' => 'civicrm/admin/options/subtype', 'query' => ['action' => 'update', 'id' => $contactType['id'], 'reset' => 1]], 'fa-pencil'); + $message->addAction($contactType['label'], FALSE, 'href', ['path' => 'civicrm/admin/options/subtype/edit', 'query' => ['action' => 'update', 'id' => $contactType['id'], 'reset' => 1]], 'fa-pencil'); } $messages[] = $message; } diff --git a/Civi/Api4/ContactType.php b/Civi/Api4/ContactType.php index e949845613a1..cf478def15b1 100644 --- a/Civi/Api4/ContactType.php +++ b/Civi/Api4/ContactType.php @@ -21,7 +21,7 @@ * * @see https://docs.civicrm.org/user/en/latest/organising-your-data/contacts/#contact-subtypes * @see \Civi\Api4\Contact - * @searchable none + * @searchable secondary * @since 5.19 * @package Civi\Api4 */ diff --git a/Civi/Api4/Generic/DAODeleteAction.php b/Civi/Api4/Generic/DAODeleteAction.php index 3dbab2475711..cd7a32fc1d55 100644 --- a/Civi/Api4/Generic/DAODeleteAction.php +++ b/Civi/Api4/Generic/DAODeleteAction.php @@ -36,8 +36,10 @@ public function _run(Result $result) { $items = $this->getBatchRecords(); if ($this->getCheckPermissions()) { + $idField = CoreUtil::getIdFieldName($this->getEntityName()); foreach ($items as $key => $item) { - if (!CoreUtil::checkAccessRecord($this, $item, \CRM_Core_Session::getLoggedInContactID() ?: 0)) { + // Don't pass the entire item because only the id is a trusted value + if (!CoreUtil::checkAccessRecord($this, [$idField => $item[$idField]], \CRM_Core_Session::getLoggedInContactID() ?: 0)) { throw new UnauthorizedException("ACL check failed"); } $items[$key]['check_permissions'] = TRUE; diff --git a/ext/search_kit/Civi/Api4/Action/SearchDisplay/AbstractRunAction.php b/ext/search_kit/Civi/Api4/Action/SearchDisplay/AbstractRunAction.php index 6d417c4baf7f..b06f67c8192e 100644 --- a/ext/search_kit/Civi/Api4/Action/SearchDisplay/AbstractRunAction.php +++ b/ext/search_kit/Civi/Api4/Action/SearchDisplay/AbstractRunAction.php @@ -509,18 +509,21 @@ private function getLinkPath($link, $data = NULL, $index = 0) { if ($prefix) { $path = str_replace('[', '[' . $prefix, $path); } - // Check access for edit/update links + // Check access for edit/update/delete links // (presumably if a record is shown in SearchKit the user already has view access, and the check is expensive) if ($path && isset($data) && !in_array($link['action'], ['view', 'preview'], TRUE)) { $id = $data[$prefix . $idKey] ?? NULL; $id = is_array($id) ? $id[$index] ?? NULL : $id; if ($id) { + $values = [$idField => $id]; + // If not aggregated, add other values to help checkAccess be efficient + if (!is_array($data[$prefix . $idKey])) { + $values += \CRM_Utils_Array::filterByPrefix($data, $prefix); + } $access = civicrm_api4($link['entity'], 'checkAccess', [ // Fudge links with funny action names to check 'update' 'action' => $link['action'] === 'delete' ? 'delete' : 'update', - 'values' => [ - $idField => $id, - ], + 'values' => $values, ], 0)['access']; if (!$access) { return NULL; diff --git a/xml/schema/Contact/ContactType.xml b/xml/schema/Contact/ContactType.xml index b9fe0b75b572..aec9c423fa90 100644 --- a/xml/schema/Contact/ContactType.xml +++ b/xml/schema/Contact/ContactType.xml @@ -6,6 +6,11 @@ civicrm_contact_type Provide type information for contacts 3.1 + + civicrm/admin/options/subtype/edit?action=add&reset=1 + civicrm/admin/options/subtype/edit?action=update&id=[id]&reset=1 + civicrm/admin/options/subtype/edit?action=delete&id=[id]&reset=1 + id Contact Type ID From 8d0bafa435d7b2eed771cd2dd2c180e566063592 Mon Sep 17 00:00:00 2001 From: Coleman Watts Date: Tue, 13 Sep 2022 22:38:25 -0400 Subject: [PATCH 2/3] SearchKit - Support an "Add New" button at the top of displays --- ext/search_kit/Civi/Search/Admin.php | 5 +++++ ext/search_kit/ang/crmSearchAdmin.module.js | 3 +++ .../crmSearchAdminDisplay.component.js | 19 +++++++++++++++++++ .../displays/common/searchButtonConfig.html | 9 +++++++++ .../ang/crmSearchDisplay/AddButton.html | 4 ++++ .../traits/searchDisplayBaseTrait.service.js | 4 ++++ .../crmSearchDisplayGrid.html | 5 ++++- .../crmSearchDisplayList.html | 5 ++++- .../crmSearchDisplayTable.html | 1 + 9 files changed, 53 insertions(+), 2 deletions(-) create mode 100644 ext/search_kit/ang/crmSearchDisplay/AddButton.html diff --git a/ext/search_kit/Civi/Search/Admin.php b/ext/search_kit/Civi/Search/Admin.php index f8b2bef44243..a0f8b7c280d0 100644 --- a/ext/search_kit/Civi/Search/Admin.php +++ b/ext/search_kit/Civi/Search/Admin.php @@ -17,6 +17,7 @@ use Civi\Api4\Query\SqlFunction; use Civi\Api4\SearchDisplay; use Civi\Api4\Tag; +use Civi\Api4\Utils\CoreUtil; use CRM_Search_ExtensionUtil as E; /** @@ -136,6 +137,10 @@ public static function getSchema(): array { if ($links) { $entity['links'] = array_values($links); } + $paths = CoreUtil::getInfoItem($entity['name'], 'paths'); + if (!empty($paths['add'])) { + $entity['addPath'] = $paths['add']; + } $getFields = civicrm_api4($entity['name'], 'getFields', [ 'select' => ['name', 'title', 'label', 'description', 'type', 'options', 'input_type', 'input_attrs', 'data_type', 'serialize', 'entity', 'fk_entity', 'readonly', 'operators', 'suffixes', 'nullable'], 'where' => [['name', 'NOT IN', ['api_key', 'hash']]], diff --git a/ext/search_kit/ang/crmSearchAdmin.module.js b/ext/search_kit/ang/crmSearchAdmin.module.js index bfcfa54092ba..1669081dcbd7 100644 --- a/ext/search_kit/ang/crmSearchAdmin.module.js +++ b/ext/search_kit/ang/crmSearchAdmin.module.js @@ -343,6 +343,9 @@ } return { getEntity: getEntity, + getBaseEntity: function() { + return getEntity(searchEntity); + }, getField: function(fieldName, entityName) { return getFieldAndJoin(fieldName, entityName || searchEntity).field; }, diff --git a/ext/search_kit/ang/crmSearchAdmin/crmSearchAdminDisplay.component.js b/ext/search_kit/ang/crmSearchAdmin/crmSearchAdminDisplay.component.js index 88ce9ce15535..9299cd79ab69 100644 --- a/ext/search_kit/ang/crmSearchAdmin/crmSearchAdminDisplay.component.js +++ b/ext/search_kit/ang/crmSearchAdmin/crmSearchAdminDisplay.component.js @@ -271,6 +271,25 @@ }); }; + this.toggleAddButton = function() { + if (ctrl.display.settings.addButton && ctrl.display.settings.addButton.path) { + delete ctrl.display.settings.addButton; + } else { + var entity = searchMeta.getBaseEntity(); + ctrl.display.settings.addButton = { + path: entity.addPath || 'civicrm/', + text: ts('Add %1', {1: entity.title}), + icon: 'fa-plus' + }; + } + }; + + this.onChangeAddButtonPath = function() { + if (!ctrl.display.settings.addButton.path) { + delete ctrl.display.settings.addButton; + } + }; + // Helper function to sort active from hidden columns and initialize each column with defaults this.initColumns = function(defaults) { if (!ctrl.display.settings.columns) { diff --git a/ext/search_kit/ang/crmSearchAdmin/displays/common/searchButtonConfig.html b/ext/search_kit/ang/crmSearchAdmin/displays/common/searchButtonConfig.html index 790c3cdaafbc..1424ee30dd64 100644 --- a/ext/search_kit/ang/crmSearchAdmin/displays/common/searchButtonConfig.html +++ b/ext/search_kit/ang/crmSearchAdmin/displays/common/searchButtonConfig.html @@ -11,3 +11,12 @@ +
+
+ +
+
+ diff --git a/ext/search_kit/ang/crmSearchDisplay/AddButton.html b/ext/search_kit/ang/crmSearchDisplay/AddButton.html new file mode 100644 index 000000000000..29c6b7253666 --- /dev/null +++ b/ext/search_kit/ang/crmSearchDisplay/AddButton.html @@ -0,0 +1,4 @@ + + + {{:: $ctrl.settings.addButton.text }} + diff --git a/ext/search_kit/ang/crmSearchDisplay/traits/searchDisplayBaseTrait.service.js b/ext/search_kit/ang/crmSearchDisplay/traits/searchDisplayBaseTrait.service.js index cdb572b99854..5bfdafcecb48 100644 --- a/ext/search_kit/ang/crmSearchDisplay/traits/searchDisplayBaseTrait.service.js +++ b/ext/search_kit/ang/crmSearchDisplay/traits/searchDisplayBaseTrait.service.js @@ -27,6 +27,10 @@ this.placeholders.push({}); } + if (this.settings.addButton && this.settings.addButton.path) { + this.settings.addButton.url = CRM.url(this.settings.addButton.path); + } + this.getResults = _.debounce(function() { $scope.$apply(function() { ctrl.runSearch(); diff --git a/ext/search_kit/ang/crmSearchDisplayGrid/crmSearchDisplayGrid.html b/ext/search_kit/ang/crmSearchDisplayGrid/crmSearchDisplayGrid.html index 46c09304ed63..710f0dfa693a 100644 --- a/ext/search_kit/ang/crmSearchDisplayGrid/crmSearchDisplayGrid.html +++ b/ext/search_kit/ang/crmSearchDisplayGrid/crmSearchDisplayGrid.html @@ -1,5 +1,8 @@
-
+
+
+
+
-
+
+
+
+
      diff --git a/ext/search_kit/ang/crmSearchDisplayTable/crmSearchDisplayTable.html b/ext/search_kit/ang/crmSearchDisplayTable/crmSearchDisplayTable.html index 82e9e03cc4c0..3d57aedf18d5 100644 --- a/ext/search_kit/ang/crmSearchDisplayTable/crmSearchDisplayTable.html +++ b/ext/search_kit/ang/crmSearchDisplayTable/crmSearchDisplayTable.html @@ -2,6 +2,7 @@
      +
      From bb231168ba054c48c26b114ad007d7d8a39c36e0 Mon Sep 17 00:00:00 2001 From: Coleman Watts Date: Wed, 14 Sep 2022 09:23:45 -0400 Subject: [PATCH 3/3] AdminUI - Add SearchKit-based screen for Administer Contact Types --- .../ang/afsearchAdminContactTypes.aff.html | 9 ++ .../ang/afsearchAdminContactTypes.aff.json | 8 + .../SavedSearch_Administer_Contact_Types.php | 151 ++++++++++++++++++ 3 files changed, 168 insertions(+) create mode 100644 ext/civicrm_admin_ui/ang/afsearchAdminContactTypes.aff.html create mode 100644 ext/civicrm_admin_ui/ang/afsearchAdminContactTypes.aff.json create mode 100644 ext/civicrm_admin_ui/managed/SavedSearch_Administer_Contact_Types.php diff --git a/ext/civicrm_admin_ui/ang/afsearchAdminContactTypes.aff.html b/ext/civicrm_admin_ui/ang/afsearchAdminContactTypes.aff.html new file mode 100644 index 000000000000..fc89171324d1 --- /dev/null +++ b/ext/civicrm_admin_ui/ang/afsearchAdminContactTypes.aff.html @@ -0,0 +1,9 @@ +
      +
      +
      + {{:: ts('CiviCRM comes with 3 basic (built-in) contact types: Individual, Household, and Organization. You can create additional contact types based on these basic types to further differentiate contacts (for example you might create Student, Parent, Staff, and /or Volunteer "subtypes" from the basic Individual type...). You can also re-name the built-in types. Contact subtypes are especially useful when you need to collect and display different sets of custom data for different types of contacts.') }} + {{:: ts('Learn more...') }} +
      +
      + +
      diff --git a/ext/civicrm_admin_ui/ang/afsearchAdminContactTypes.aff.json b/ext/civicrm_admin_ui/ang/afsearchAdminContactTypes.aff.json new file mode 100644 index 000000000000..902c469d7f2a --- /dev/null +++ b/ext/civicrm_admin_ui/ang/afsearchAdminContactTypes.aff.json @@ -0,0 +1,8 @@ +{ + "type": "search", + "title": "Contact Types", + "description": "Administer contact types and sub-types", + "icon": "fa-list-alt", + "server_route": "civicrm/admin/options/subtype", + "permission": "access CiviCRM" +} diff --git a/ext/civicrm_admin_ui/managed/SavedSearch_Administer_Contact_Types.php b/ext/civicrm_admin_ui/managed/SavedSearch_Administer_Contact_Types.php new file mode 100644 index 000000000000..bce63cb882ed --- /dev/null +++ b/ext/civicrm_admin_ui/managed/SavedSearch_Administer_Contact_Types.php @@ -0,0 +1,151 @@ + 'SavedSearch_Administer_Contact_Types', + 'entity' => 'SavedSearch', + 'cleanup' => 'always', + 'update' => 'unmodified', + 'params' => [ + 'version' => 4, + 'values' => [ + 'name' => 'Administer_Contact_Types', + 'label' => 'Administer Contact Types', + 'form_values' => NULL, + 'mapping_id' => NULL, + 'search_custom_id' => NULL, + 'api_entity' => 'ContactType', + 'api_params' => [ + 'version' => 4, + 'select' => [ + 'label', + 'parent_id:label', + 'description', + ], + 'orderBy' => [], + 'where' => [], + 'groupBy' => [], + 'join' => [], + 'having' => [], + ], + 'expires_date' => NULL, + 'description' => NULL, + ], + ], + ], + [ + 'name' => 'SavedSearch_Administer_Contact_Types_SearchDisplay_Contact_Types_Table', + 'entity' => 'SearchDisplay', + 'cleanup' => 'always', + 'update' => 'unmodified', + 'params' => [ + 'version' => 4, + 'values' => [ + 'name' => 'Contact_Types_Table', + 'label' => 'Contact Types Table', + 'saved_search_id.name' => 'Administer_Contact_Types', + 'type' => 'table', + 'settings' => [ + 'actions' => FALSE, + 'limit' => 50, + 'classes' => [ + 'table', + 'table-striped', + ], + 'pager' => [ + 'show_count' => TRUE, + ], + 'placeholder' => 5, + 'sort' => [ + [ + 'parent_id:label', + 'ASC', + ], + [ + 'label', + 'ASC', + ], + ], + 'columns' => [ + [ + 'type' => 'field', + 'key' => 'label', + 'dataType' => 'String', + 'label' => 'Label', + 'sortable' => TRUE, + 'icons' => [ + [ + 'field' => 'icon', + 'side' => 'left', + ], + ], + 'editable' => TRUE, + ], + [ + 'type' => 'field', + 'key' => 'parent_id:label', + 'dataType' => 'Integer', + 'label' => 'Parent', + 'sortable' => TRUE, + 'icons' => [ + [ + 'icon' => 'fa-lock', + 'side' => 'left', + 'if' => [ + 'parent_id:label', + 'IS EMPTY', + ], + ], + ], + ], + [ + 'type' => 'field', + 'key' => 'description', + 'dataType' => 'Text', + 'label' => 'Description', + 'sortable' => TRUE, + 'editable' => TRUE, + ], + [ + 'size' => 'btn-sm', + 'links' => [ + [ + 'entity' => 'ContactType', + 'action' => 'update', + 'join' => '', + 'target' => 'crm-popup', + 'icon' => 'fa-pencil', + 'text' => 'Edit', + 'style' => 'default', + 'path' => '', + 'condition' => [], + ], + [ + 'entity' => 'ContactType', + 'action' => 'delete', + 'join' => '', + 'target' => 'crm-popup', + 'icon' => 'fa-trash', + 'text' => 'Delete', + 'style' => 'danger', + 'path' => '', + 'condition' => [ + 'parent_id:label', + 'IS NOT EMPTY', + ], + ], + ], + 'type' => 'buttons', + 'alignment' => 'text-right', + ], + ], + 'addButton' => [ + 'path' => 'civicrm/admin/options/subtype/edit?action=add&reset=1', + 'text' => 'Add Contact Type', + 'icon' => 'fa-plus', + ], + ], + 'acl_bypass' => FALSE, + ], + ], + ], +];