diff --git a/CRM/Core/BAO/Navigation.php b/CRM/Core/BAO/Navigation.php index 688d399d6f5a..f3e72a6b3fdd 100644 --- a/CRM/Core/BAO/Navigation.php +++ b/CRM/Core/BAO/Navigation.php @@ -19,6 +19,42 @@ class CRM_Core_BAO_Navigation extends CRM_Core_DAO_Navigation { // Number of characters in the menu js cache key const CACHE_KEY_STRLEN = 8; + /** + * Override parent method to flush caches after a write op. + * + * Note: this only applies to APIv4 because v3 uses the singular writeRecord. + * + * @param array[] $records + * @return CRM_Core_DAO_Navigation[] + * @throws CRM_Core_Exception + */ + public static function writeRecords($records): array { + $results = []; + foreach ($records as $record) { + $results[] = self::writeRecord($record); + } + self::resetNavigation(); + return $results; + } + + /** + * Override parent method to flush caches after delete. + * + * Note: this only applies to APIv4 because v3 uses the singular writeRecord. + * + * @param array[] $records + * @return CRM_Core_DAO_Navigation[] + * @throws CRM_Core_Exception + */ + public static function deleteRecords(array $records) { + $results = []; + foreach ($records as $record) { + $results[] = self::deleteRecord($record); + } + self::resetNavigation(); + return $results; + } + /** * Update the is_active flag in the db. * diff --git a/ext/afform/admin/ang/afGuiEditor.css b/ext/afform/admin/ang/afGuiEditor.css index 585a39784595..445fab469b9d 100644 --- a/ext/afform/admin/ang/afGuiEditor.css +++ b/ext/afform/admin/ang/afGuiEditor.css @@ -112,10 +112,6 @@ margin-bottom: 10px; } -#afGuiEditor-palette-config .form-inline label { - min-width: 110px; -} - #afGuiEditor-palette-config .af-gui-entity-palette [type=search] { width: 120px; padding: 3px 3px 3px 5px; diff --git a/ext/afform/admin/ang/afGuiEditor/afGuiEditor.component.js b/ext/afform/admin/ang/afGuiEditor/afGuiEditor.component.js index e99e02c58ce8..a1bdb7a7aae4 100644 --- a/ext/afform/admin/ang/afGuiEditor/afGuiEditor.component.js +++ b/ext/afform/admin/ang/afGuiEditor/afGuiEditor.component.js @@ -30,6 +30,7 @@ undoHistory = [], undoPosition = 0, undoAction = null, + lastSaved, sortableOptions = {}; // ngModelOptions to debounce input @@ -100,6 +101,11 @@ $scope.layoutHtml = ''; $scope.entities = {}; setEditorLayout(); + setLastSaved(); + + if (editor.afform.navigation) { + loadNavigationMenu(); + } if (editor.getFormType() === 'form') { editor.allowEntityConfig = true; @@ -334,6 +340,57 @@ } }; + this.toggleNavigation = function() { + if (editor.afform.navigation) { + editor.afform.navigation = null; + } else { + loadNavigationMenu(); + editor.afform.navigation = { + parent: null, + label: editor.afform.title, + weight: 0 + }; + } + }; + + function loadNavigationMenu() { + if ('navigationMenu' in editor) { + return; + } + editor.navigationMenu = null; + var conditions = [ + ['domain_id', '=', 'current_domain'], + ['name', '!=', 'Home'] + ]; + if (editor.afform.name) { + conditions.push(['name', '!=', editor.afform.name]); + } + crmApi4('Navigation', 'get', { + select: ['name', 'label', 'parent_id', 'icon'], + where: conditions, + orderBy: {weight: 'ASC'} + }).then(function(items) { + editor.navigationMenu = buildTree(items, null); + }); + } + + function buildTree(items, parentId) { + return _.transform(items, function(navigationMenu, item) { + if (parentId === item.parent_id) { + var children = buildTree(items, item.id), + menuItem = { + id: item.name, + text: item.label, + icon: item.icon + }; + if (children.length) { + menuItem.children = children; + } + navigationMenu.push(menuItem); + } + }, []); + } + // Collects all search displays currently on the form function getSearchDisplaysOnForm() { var searchFieldsets = afGui.findRecursive(editor.afform.layout, {'af-fieldset': ''}); @@ -522,6 +579,13 @@ snapshot.saved = index === undoPosition; snapshot.afform.name = data[0].name; }); + if (!angular.equals(afform.navigation, lastSaved.navigation) || + (afform.server_route !== lastSaved.server_route && afform.navigation) + (afform.icon !== lastSaved.icon && afform.navigation) + ) { + refreshMenubar(); + } + setLastSaved(); }); }; @@ -535,6 +599,19 @@ } }); + // Sets last-saved form metadata (used to determine if the menubar needs refresh) + function setLastSaved() { + lastSaved = JSON.parse(angular.toJson(editor.afform)); + delete lastSaved.layout; + } + + // Force-refresh the menubar to instantly display the afform menu item + function refreshMenubar() { + CRM.menubar.destroy(); + CRM.menubar.cacheCode = Math.random(); + CRM.menubar.initialize(); + } + // Force editor panels to a fixed height, to avoid palette scrolling offscreen function fixEditorHeight() { var height = $(window).height() - $('#afGuiEditor').offset().top; diff --git a/ext/afform/admin/ang/afGuiEditor/config-form.html b/ext/afform/admin/ang/afGuiEditor/config-form.html index ca85a02cb134..c316e9ec4e47 100644 --- a/ext/afform/admin/ang/afGuiEditor/config-form.html +++ b/ext/afform/admin/ang/afGuiEditor/config-form.html @@ -32,7 +32,7 @@

{{:: ts('Expose the form as a standalone webpage. (Example: "civicrm/my-form")') }}

@@ -54,11 +54,32 @@
- -

{{:: ts('Allow CiviCRM users to add the form to their home dashboard.') }}

+
+ +
+ + + + + + + + + +
+
+

{{:: ts('Requires a page route') }}

+
+ +
+
+ + +
@@ -71,12 +92,20 @@ -
- -
-

{{:: ts('Placement can be configured using the Contact Layout Editor.') }}

+

+ {{:: ts('Placement can be configured using the Contact Layout Editor.') }} +

+ +
+ +

{{:: ts('Allow CiviCRM users to add the form to their home dashboard.') }}

+
+ diff --git a/ext/afform/core/Civi/Api4/Afform.php b/ext/afform/core/Civi/Api4/Afform.php index 15d2aea8d8f2..2a21cf756cb0 100644 --- a/ext/afform/core/Civi/Api4/Afform.php +++ b/ext/afform/core/Civi/Api4/Afform.php @@ -182,6 +182,11 @@ public static function getFields($checkPermissions = TRUE) { 'name' => 'create_submission', 'data_type' => 'Boolean', ], + [ + 'name' => 'navigation', + 'data_type' => 'Array', + 'description' => 'Insert into navigation menu {parent: string, label: string, weight: int}', + ], [ 'name' => 'layout', 'data_type' => 'Array', diff --git a/ext/afform/core/Civi/Api4/Utils/AfformSaveTrait.php b/ext/afform/core/Civi/Api4/Utils/AfformSaveTrait.php index 0d109351af57..466705a00d34 100644 --- a/ext/afform/core/Civi/Api4/Utils/AfformSaveTrait.php +++ b/ext/afform/core/Civi/Api4/Utils/AfformSaveTrait.php @@ -67,10 +67,14 @@ protected function writeRecord($item) { return ($item[$field] ?? NULL) !== ($orig[$field] ?? NULL); }; - // If the dashlet setting changed, managed entities must be reconciled + // If the dashlet or navigation setting changed, managed entities must be reconciled + // TODO: If this list of conditions gets any longer, then + // maybe we should unconditionally reconcile and accept the small performance drag. if ( $isChanged('is_dashlet') || - (!empty($meta['is_dashlet']) && $isChanged('title')) + $isChanged('navigation') || + (!empty($meta['is_dashlet']) && $isChanged('title')) || + (!empty($meta['navigation']) && ($isChanged('title') || $isChanged('permission') || $isChanged('icon') || $isChanged('server_route'))) ) { \CRM_Core_ManagedEntities::singleton()->reconcile(E::LONG_NAME); } diff --git a/ext/afform/core/afform.php b/ext/afform/core/afform.php index be15c8f32277..b1d294b44f09 100644 --- a/ext/afform/core/afform.php +++ b/ext/afform/core/afform.php @@ -13,7 +13,7 @@ function _afform_fields_filter($params) { $result = []; $fields = \Civi\Api4\Afform::getfields(FALSE)->setAction('create')->execute()->indexBy('name'); foreach ($fields as $fieldName => $field) { - if (isset($params[$fieldName])) { + if (array_key_exists($fieldName, $params)) { $result[$fieldName] = $params[$fieldName]; if ($field['data_type'] === 'Boolean' && !is_bool($params[$fieldName])) { @@ -140,32 +140,66 @@ function afform_civicrm_managed(&$entities, $modules) { // This AfformScanner instance only lives during this method call, and it feeds off the regular cache. $scanner = new CRM_Afform_AfformScanner(); } + $domains = NULL; foreach ($scanner->getMetas() as $afform) { - if (empty($afform['is_dashlet']) || empty($afform['name'])) { + if (empty($afform['name'])) { continue; } - $entities[] = [ - 'module' => E::LONG_NAME, - 'name' => 'afform_dashlet_' . $afform['name'], - 'entity' => 'Dashboard', - 'update' => 'always', - // ideal cleanup policy might be to (a) deactivate if used and (b) remove if unused - 'cleanup' => 'always', - 'params' => [ - 'version' => 4, - 'values' => [ - // Q: Should we loop through all domains? - 'domain_id' => 'current_domain', - 'is_active' => TRUE, - 'name' => $afform['name'], - 'label' => $afform['title'] ?? E::ts('(Untitled)'), - 'directive' => _afform_angular_module_name($afform['name'], 'dash'), - 'permission' => "@afform:" . $afform['name'], - 'url' => NULL, + if (!empty($afform['is_dashlet'])) { + $entities[] = [ + 'module' => E::LONG_NAME, + 'name' => 'afform_dashlet_' . $afform['name'], + 'entity' => 'Dashboard', + 'update' => 'always', + // ideal cleanup policy might be to (a) deactivate if used and (b) remove if unused + 'cleanup' => 'always', + 'params' => [ + 'version' => 4, + 'values' => [ + // Q: Should we loop through all domains? + 'domain_id' => 'current_domain', + 'is_active' => TRUE, + 'name' => $afform['name'], + 'label' => $afform['title'] ?? E::ts('(Untitled)'), + 'directive' => _afform_angular_module_name($afform['name'], 'dash'), + 'permission' => "@afform:" . $afform['name'], + 'url' => NULL, + ], ], - ], - ]; + ]; + } + if (!empty($afform['navigation']) && !empty($afform['server_route'])) { + $domains = $domains ?: \Civi\Api4\Domain::get(FALSE)->addSelect('id')->execute(); + foreach ($domains as $domain) { + $params = [ + 'version' => 4, + 'values' => [ + 'name' => $afform['name'], + 'label' => $afform['navigation']['label'] ?: $afform['title'], + 'permission' => (array) $afform['permission'], + 'permission_operator' => 'OR', + 'weight' => $afform['navigation']['weight'] ?? 0, + 'url' => $afform['server_route'], + 'is_active' => 1, + 'icon' => 'crm-i ' . $afform['icon'], + 'domain_id' => $domain['id'], + ], + 'match' => ['domain_id', 'name'], + ]; + if (!empty($afform['navigation']['parent'])) { + $params['values']['parent_id.name'] = $afform['navigation']['parent']; + } + $entities[] = [ + 'module' => E::LONG_NAME, + 'name' => 'navigation_' . $afform['name'] . '_' . $domain['id'], + 'cleanup' => 'always', + 'update' => 'unmodified', + 'entity' => 'Navigation', + 'params' => $params, + ]; + } + } } }