Skip to content

Commit

Permalink
Merge pull request #24013 from colemanw/afformNav
Browse files Browse the repository at this point in the history
Afform - provide easy way to add navigation menu from the form
  • Loading branch information
eileenmcnaughton authored Jul 19, 2022
2 parents 2c3d19f + 1d67800 commit a9882d7
Show file tree
Hide file tree
Showing 7 changed files with 219 additions and 38 deletions.
36 changes: 36 additions & 0 deletions CRM/Core/BAO/Navigation.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
4 changes: 0 additions & 4 deletions ext/afform/admin/ang/afGuiEditor.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
77 changes: 77 additions & 0 deletions ext/afform/admin/ang/afGuiEditor/afGuiEditor.component.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
undoHistory = [],
undoPosition = 0,
undoAction = null,
lastSaved,
sortableOptions = {};

// ngModelOptions to debounce input
Expand Down Expand Up @@ -100,6 +101,11 @@
$scope.layoutHtml = '';
$scope.entities = {};
setEditorLayout();
setLastSaved();

if (editor.afform.navigation) {
loadNavigationMenu();
}

if (editor.getFormType() === 'form') {
editor.allowEntityConfig = true;
Expand Down Expand Up @@ -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': ''});
Expand Down Expand Up @@ -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();
});
};

Expand All @@ -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;
Expand Down
49 changes: 39 additions & 10 deletions ext/afform/admin/ang/afGuiEditor/config-form.html
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@

<div class="form-group" ng-class="{'has-error': !!config_form.server_route.$error.pattern}">
<label for="af_config_form_server_route">
{{:: ts('Page') }}
{{:: ts('Page Route') }}
</label>
<input ng-model="editor.afform.server_route" name="server_route" class="form-control" id="af_config_form_server_route" pattern="^civicrm\/[-0-9a-zA-Z\/_]+$" onfocus="this.value = this.value || 'civicrm/'" onblur="if (this.value === 'civicrm/') this.value = ''" title="{{:: ts('Path must begin with &quot;civicrm/&quot;') }}" ng-model-options="editor.debounceMode">
<p class="help-block">{{:: ts('Expose the form as a standalone webpage. (Example: "civicrm/my-form")') }}</p>
Expand All @@ -54,11 +54,32 @@
</div>

<div class="form-group">
<label>
<input type="checkbox" ng-model="editor.afform.is_dashlet">
{{:: ts('Add to Dashboard') }}
</label>
<p class="help-block">{{:: ts('Allow CiviCRM users to add the form to their home dashboard.') }}</p>
<div class="form-inline">
<label ng-class="{disabled: !editor.afform.server_route}">
<input type="checkbox" ng-checked="editor.afform.server_route && editor.afform.navigation" ng-disabled="!editor.afform.server_route" ng-click="editor.toggleNavigation()">
{{:: ts('Add to Navigation Menu') }}
</label>
<div class="form-group" ng-if="editor.afform.navigation">
<input class="form-control" ng-model="editor.afform.navigation.label" ng-model-options="editor.debounceMode" placeholder="{{:: ts('Title') }}" required>
<span ng-if="!editor.navigationMenu">
<input class="form-control loading" disabled crm-ui-select="{placeholder: ts('Loading menu items'), data: []}">
</span>
<span ng-if="editor.navigationMenu">
<input class="form-control" ng-model="editor.afform.navigation.parent"
crm-ui-select="{allowClear: true, placeholder: ts('Top Level'), data: editor.navigationMenu || []}">
</span>
<label for="afform-admin-navigation-weight">{{:: ts('Order') }}</label>
<input class="form-control" id="afform-admin-navigation-weight" type="number" placeholder="{{:: ts('Order') }}" min="0" step="1" ng-model="editor.afform.navigation.weight" required>
</div>
</div>
<p class="help-block disabled" ng-if="!editor.afform.server_route">{{:: ts('Requires a page route') }}</p>
</div>

<div class="form-group" ng-show="!!editor.afform.navigation || editor.afform.contact_summary === 'tab'">
<div class="form-inline">
<label for="afform_icon">{{:: ts('Icon') }}</label>
<input required id="afform_icon" ng-model="editor.afform.icon" crm-ui-icon-picker class="form-control">
</div>
</div>

<div class="form-group">
Expand All @@ -71,12 +92,20 @@
<option value="block">{{:: ts('As Block') }}</option>
<option value="tab">{{:: ts('As Tab') }}</option>
</select>
<div class="form-group" ng-show="editor.afform.contact_summary === 'tab'">
<input required ng-model="editor.afform.icon" crm-ui-icon-picker class="form-control">
</div>
</div>
<p class="help-block">{{:: ts('Placement can be configured using the Contact Layout Editor.') }}</p>
<p class="help-block" ng-show="editor.afform.contact_summary">
{{:: ts('Placement can be configured using the Contact Layout Editor.') }}
</p>
</div>

<div class="form-group">
<label>
<input type="checkbox" ng-model="editor.afform.is_dashlet">
{{:: ts('Add to Dashboard') }}
</label>
<p class="help-block">{{:: ts('Allow CiviCRM users to add the form to their home dashboard.') }}</p>
</div>

</fieldset>

<!-- Submit actions are only applicable to form types with a submit button (exclude blocks and search forms) -->
Expand Down
5 changes: 5 additions & 0 deletions ext/afform/core/Civi/Api4/Afform.php
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
8 changes: 6 additions & 2 deletions ext/afform/core/Civi/Api4/Utils/AfformSaveTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
78 changes: 56 additions & 22 deletions ext/afform/core/afform.php
Original file line number Diff line number Diff line change
Expand Up @@ -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])) {
Expand Down Expand Up @@ -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,
];
}
}
}
}

Expand Down

0 comments on commit a9882d7

Please sign in to comment.