Move breadcrumb rendering from individual page templates into the core topbar. Each page now sets $breadcrumbs in its action .php file; the topbar renders it automatically via the shared partial. - Remove global back/forward buttons and app-nav-history.js component - Remove Alt+Arrow keyboard shortcuts for history navigation - Render breadcrumb in topbar-left section (replaces button area) - Clean up breadcrumb CSS: context-neutral base (flex, no margin) - Recalculate sticky titlebar offset in details container - Migrate all 41 pages (core + helpdesk, audit, addressbook, api-docs) - Add missing breadcrumbs to addressbook detail view - Update architecture contract tests (nav-history references removed) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
236 lines
9.7 KiB
PHTML
236 lines
9.7 KiB
PHTML
<?php
|
|
use MintyPHP\Buffer;
|
|
use MintyPHP\Session;
|
|
|
|
$csrfKey = $csrfKey ?? Session::$csrfSessionKey;
|
|
$csrfToken = $csrfToken ?? ($_SESSION[$csrfKey] ?? '');
|
|
$customFieldFilterDefinitions = is_array($customFieldFilterDefinitions ?? null) ? $customFieldFilterDefinitions : [];
|
|
$activeCustomFieldQuery = is_array($customFieldFilterQuery ?? null) ? $customFieldFilterQuery : [];
|
|
$toolbarFilterSchema = is_array($toolbarFilterSchema ?? null) ? $toolbarFilterSchema : [];
|
|
$searchToolbarFilterSchema = is_array($searchToolbarFilterSchema ?? null) ? $searchToolbarFilterSchema : [];
|
|
$drawerToolbarFilterSchema = is_array($drawerToolbarFilterSchema ?? null) ? $drawerToolbarFilterSchema : [];
|
|
$toolbarFilterState = is_array($toolbarFilterState ?? null) ? $toolbarFilterState : [];
|
|
$toolbarOptionSets = is_array($toolbarOptionSets ?? null) ? $toolbarOptionSets : [];
|
|
$clientFilterSchema = is_array($clientFilterSchema ?? null) ? $clientFilterSchema : [];
|
|
$filterChipMeta = is_array($filterChipMeta ?? null) ? $filterChipMeta : [];
|
|
$searchConfig = is_array($searchConfig ?? null) ? $searchConfig : null;
|
|
?>
|
|
<?php
|
|
$listTitle = t('Address book');
|
|
$listTitleActionsHtml = '';
|
|
require templatePath('partials/app-list-titlebar.phtml');
|
|
?>
|
|
<div
|
|
class="app-list-toolbar"
|
|
id="address-book-toolbar"
|
|
>
|
|
<?php renderGridFilterToolbar($searchToolbarFilterSchema, $toolbarFilterState, $toolbarOptionSets); ?>
|
|
<button
|
|
type="button"
|
|
class="outline secondary icon-button"
|
|
data-filter-drawer-open
|
|
data-tooltip="<?php e(t('Filter')); ?>"
|
|
data-tooltip-pos="top"
|
|
aria-label="<?php e(t('Filter')); ?>"
|
|
aria-expanded="false"
|
|
>
|
|
<i class="bi bi-funnel"></i> <?php e(t('Filter')); ?>
|
|
</button>
|
|
</div>
|
|
<div
|
|
class="app-active-filters"
|
|
data-active-filter-chips
|
|
data-filter-chips-clear-all-label="<?php e(t('Clear all filters')); ?>"
|
|
data-filter-chips-remove-label="<?php e(t('Remove filter')); ?>"
|
|
></div>
|
|
<p
|
|
class="app-visually-hidden"
|
|
data-filter-live-region
|
|
data-filter-live-applied-message="<?php e(t('Filters applied')); ?>"
|
|
data-filter-live-removed-message="<?php e(t('Filter removed')); ?>"
|
|
data-filter-live-cleared-message="<?php e(t('All filters cleared')); ?>"
|
|
aria-live="polite"
|
|
aria-atomic="true"
|
|
></p>
|
|
<div
|
|
class="app-filter-drawer"
|
|
data-filter-drawer
|
|
role="dialog"
|
|
aria-modal="true"
|
|
aria-hidden="true"
|
|
aria-labelledby="address-book-filter-drawer-title"
|
|
hidden
|
|
>
|
|
<aside class="app-filter-drawer-panel" data-filter-drawer-panel>
|
|
<header class="app-filter-drawer-header">
|
|
<h2 id="address-book-filter-drawer-title"><?php e(t('Filter')); ?></h2>
|
|
<button
|
|
type="button"
|
|
class="transparent icon-button"
|
|
data-filter-drawer-close
|
|
aria-label="<?php e(t('Close filters')); ?>"
|
|
>
|
|
<i class="bi bi-x-lg"></i>
|
|
</button>
|
|
</header>
|
|
<div class="app-filter-drawer-body">
|
|
<div
|
|
class="app-list-toolbar"
|
|
id="address-book-drawer-toolbar"
|
|
data-tenant-dept-map="<?php e(json_encode($tenantDepartmentMap ?? [], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); ?>"
|
|
>
|
|
<?php renderGridFilterToolbar($drawerToolbarFilterSchema, $toolbarFilterState, $toolbarOptionSets); ?>
|
|
</div>
|
|
<?php if ($customFieldFilterDefinitions): ?>
|
|
<div class="app-list-toolbar" id="address-book-custom-filters">
|
|
<?php foreach ($customFieldFilterDefinitions as $definition): ?>
|
|
<?php
|
|
$definitionUuid = strtolower(trim((string) ($definition['uuid'] ?? '')));
|
|
$definitionType = strtolower(trim((string) ($definition['type'] ?? '')));
|
|
$definitionLabel = trim((string) ($definition['label'] ?? ''));
|
|
$definitionOptions = is_array($definition['options'] ?? null) ? $definition['options'] : [];
|
|
if ($definitionUuid === '' || $definitionLabel === '' || $definitionType === '') {
|
|
continue;
|
|
}
|
|
$label = $definitionLabel;
|
|
$idSuffix = preg_replace('/[^a-z0-9]+/', '-', $definitionUuid);
|
|
$paramScalar = 'cf_' . $definitionUuid;
|
|
$paramMulti = 'cfm_' . $definitionUuid;
|
|
$paramDateFrom = 'cfd_' . $definitionUuid . '_from';
|
|
$paramDateTo = 'cfd_' . $definitionUuid . '_to';
|
|
?>
|
|
<?php if ($definitionType === 'select'): ?>
|
|
<?php $currentValue = (string) ($activeCustomFieldQuery[$paramScalar] ?? ''); ?>
|
|
<?php
|
|
$selectItems = [['id' => '', 'description' => 'Please select']];
|
|
foreach ($definitionOptions as $option) {
|
|
$optionId = (string) ($option['id'] ?? '');
|
|
if ($optionId === '') {
|
|
continue;
|
|
}
|
|
$selectItems[] = [
|
|
'id' => $optionId,
|
|
'description' => (string) ($option['description'] ?? ''),
|
|
'translate' => false,
|
|
];
|
|
}
|
|
selectFilter(
|
|
'address-book-custom-filter-' . $idSuffix,
|
|
$label,
|
|
$selectItems,
|
|
$currentValue,
|
|
['for' => 'address-book-custom-filter-' . $idSuffix],
|
|
[
|
|
'data-address-book-custom-filter' => true,
|
|
'data-address-book-custom-filter-param' => $paramScalar,
|
|
]
|
|
);
|
|
?>
|
|
<?php elseif ($definitionType === 'boolean'): ?>
|
|
<?php $currentValue = strtolower(trim((string) ($activeCustomFieldQuery[$paramScalar] ?? ''))); ?>
|
|
<?php
|
|
$selectedBoolean = '';
|
|
if (in_array($currentValue, ['1', 'true'], true)) {
|
|
$selectedBoolean = '1';
|
|
} elseif (in_array($currentValue, ['0', 'false'], true)) {
|
|
$selectedBoolean = '0';
|
|
}
|
|
selectFilter(
|
|
'address-book-custom-filter-' . $idSuffix,
|
|
$label,
|
|
[
|
|
['id' => '', 'description' => 'Please select'],
|
|
['id' => '1', 'description' => 'Yes'],
|
|
['id' => '0', 'description' => 'No'],
|
|
],
|
|
$selectedBoolean,
|
|
['for' => 'address-book-custom-filter-' . $idSuffix],
|
|
[
|
|
'data-address-book-custom-filter' => true,
|
|
'data-address-book-custom-filter-param' => $paramScalar,
|
|
]
|
|
);
|
|
?>
|
|
<?php elseif ($definitionType === 'multiselect'): ?>
|
|
<?php
|
|
$currentValues = array_values(array_filter(array_map(
|
|
'trim',
|
|
explode(',', (string) ($activeCustomFieldQuery[$paramMulti] ?? ''))
|
|
), static fn ($item): bool => $item !== ''));
|
|
multiSelectFilter(
|
|
'address-book-custom-filter-' . $idSuffix,
|
|
$label,
|
|
'Select options',
|
|
$definitionOptions,
|
|
$currentValues
|
|
);
|
|
?>
|
|
<input type="hidden"
|
|
data-address-book-custom-filter
|
|
data-address-book-custom-filter-type="multiselect"
|
|
data-address-book-custom-filter-param="<?php e($paramMulti); ?>"
|
|
data-address-book-custom-filter-input="#address-book-custom-filter-<?php e($idSuffix); ?>"
|
|
>
|
|
<?php elseif ($definitionType === 'date'): ?>
|
|
<?php
|
|
$currentFrom = (string) ($activeCustomFieldQuery[$paramDateFrom] ?? '');
|
|
$currentTo = (string) ($activeCustomFieldQuery[$paramDateTo] ?? '');
|
|
?>
|
|
<label class="app-field" for="address-book-custom-filter-<?php e($idSuffix); ?>-from">
|
|
<span><?php e($label . ' ' . t('From')); ?></span>
|
|
<input type="date"
|
|
id="address-book-custom-filter-<?php e($idSuffix); ?>-from"
|
|
value="<?php e($currentFrom); ?>"
|
|
data-address-book-custom-filter
|
|
data-address-book-custom-filter-param="<?php e($paramDateFrom); ?>">
|
|
</label>
|
|
<label class="app-field" for="address-book-custom-filter-<?php e($idSuffix); ?>-to">
|
|
<span><?php e($label . ' ' . t('To')); ?></span>
|
|
<input type="date"
|
|
id="address-book-custom-filter-<?php e($idSuffix); ?>-to"
|
|
value="<?php e($currentTo); ?>"
|
|
data-address-book-custom-filter
|
|
data-address-book-custom-filter-param="<?php e($paramDateTo); ?>">
|
|
</label>
|
|
<?php endif; ?>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
<footer class="app-filter-drawer-footer">
|
|
<button type="button" class="primary" data-filter-drawer-apply>
|
|
<?php e(t('Apply filters')); ?>
|
|
</button>
|
|
</footer>
|
|
</aside>
|
|
</div>
|
|
<div class="app-list-table">
|
|
<div id="address-book-grid"></div>
|
|
</div>
|
|
|
|
<?php
|
|
$gridLang = json_decode(appBufferValue('grid_lang'), true);
|
|
if (!is_array($gridLang)) {
|
|
$gridLang = [];
|
|
}
|
|
$pageConfig = [
|
|
'gridSearch' => $searchConfig,
|
|
'filterSchema' => $clientFilterSchema,
|
|
'filterChipMeta' => $filterChipMeta,
|
|
'gridLang' => $gridLang,
|
|
'labels' => [
|
|
'name' => t('Name'),
|
|
'email' => t('Email'),
|
|
'phone' => t('Phone'),
|
|
'mobile' => t('Mobile'),
|
|
'shortDial' => t('Short dial'),
|
|
'tenants' => t('Tenants'),
|
|
'departments' => t('Departments'),
|
|
'roles' => t('Roles'),
|
|
],
|
|
];
|
|
?>
|
|
<script src="<?php e(assetVersion('vendor/gridjs/gridjs.umd.js')); ?>"></script>
|
|
<script type="application/json" id="page-config-address-book-index"><?php gridJsonForJs($pageConfig); ?></script>
|
|
<script type="module" src="<?php e(assetVersion('modules/addressbook/js/pages/address-book-index.js')); ?>"></script>
|