Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Afform - provide easy way to add navigation menu from the form #24013

Merged
merged 3 commits into from
Jul 19, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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