refactor(addressbook): move from aside icon-bar to explorer nav link

Introduce generic explorer.nav_item slot type in app-main-aside.phtml
so modules can contribute links to the explorer nav panel.

Switch addressbook module from aside.tab_panel to explorer.nav_item.
Remove aside department-filter panel, AddressBookLayoutProvider, and
AddressBookSessionProvider (only served the now-removed aside panel).

Fix grid Name column overflow into Email column: CSS selector
.grid-name-cell > span:last-child never matched the <a> name link;
changed to :last-child and scoped flex-shrink:0 to :first-child only.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-26 15:06:15 +01:00
parent 376cf67c31
commit 5b2ea6bf27
9 changed files with 35 additions and 408 deletions

View File

@@ -1,80 +0,0 @@
<?php
namespace MintyPHP\Module\AddressBook\Providers;
use MintyPHP\App\AppContainer;
use MintyPHP\App\Module\Contracts\LayoutContextProvider;
/**
* Provides address book data for the layout navigation context.
*
* Contributes the 'addressbook.nav' key to $layoutNav with URL, active filters,
* and people groups from the session.
*/
final class AddressBookLayoutProvider implements LayoutContextProvider
{
public function provide(array $session, AppContainer $container): array
{
// Read query params for active filter state
$query = [];
try {
$query = requestInput()->queryAll();
} catch (\Throwable) {
// fail-open: may not be available in CLI context
}
return [
'addressbook.nav' => [
'url' => lurl('address-book'),
'activeSearch' => trim((string) ($query['search'] ?? '')),
'activeTenants' => appNormalizeStringList($query['tenants'] ?? ''),
'activeDepartments' => appNormalizePositiveIntList($query['departments'] ?? ''),
'activeRoles' => appNormalizePositiveIntList($query['roles'] ?? ''),
'activeCustomFilters' => self::normalizeCustomFilterQuery($query),
'peopleGroups' => is_array($session['module.addressbook.departments_by_tenant'] ?? null)
? $session['module.addressbook.departments_by_tenant']
: [],
],
];
}
/**
* Normalize address-book custom field query keys/values.
*
* @param array<string, mixed> $rawQuery
* @return array<string, string|list<int>>
*/
public static function normalizeCustomFilterQuery(array $rawQuery): array
{
$normalized = [];
foreach ($rawQuery as $rawKey => $rawValue) {
$key = strtolower(trim((string) $rawKey));
if ($key === '') {
continue;
}
if (preg_match('/^cf_[a-f0-9-]{36}$/', $key)) {
$value = trim((string) $rawValue);
if ($value !== '') {
$normalized[$key] = $value;
}
continue;
}
if (preg_match('/^cfm_[a-f0-9-]{36}$/', $key)) {
$ids = appNormalizePositiveIntList($rawValue);
if ($ids) {
$normalized[$key] = $ids;
}
continue;
}
if (preg_match('/^cfd_[a-f0-9-]{36}_(from|to)$/', $key)) {
$value = trim((string) $rawValue);
$dt = \DateTimeImmutable::createFromFormat('Y-m-d', $value);
if ($dt && $dt->format('Y-m-d') === $value) {
$normalized[$key] = $value;
}
}
}
ksort($normalized, SORT_STRING);
return $normalized;
}
}

View File

@@ -1,51 +0,0 @@
<?php
namespace MintyPHP\Module\AddressBook\Providers;
use MintyPHP\App\AppContainer;
use MintyPHP\App\Module\Contracts\SessionProvider;
use MintyPHP\Http\SessionStoreInterface;
use MintyPHP\Service\User\UserTenantContextService;
/**
* Populates session with department-by-tenant hierarchy data used by the
* address book aside panel.
*/
final class AddressBookSessionProvider implements SessionProvider
{
public function populate(array $user, AppContainer $container): void
{
$sessionStore = $container->get(SessionStoreInterface::class);
if (!$sessionStore instanceof SessionStoreInterface) {
return;
}
$userId = (int) ($user['id'] ?? 0);
if ($userId <= 0) {
$sessionStore->remove('module.addressbook.departments_by_tenant');
return;
}
try {
$tenantContext = $container->get(UserTenantContextService::class);
} catch (\Throwable) {
return;
}
if (!$tenantContext instanceof UserTenantContextService) {
return;
}
$sessionStore->set(
'module.addressbook.departments_by_tenant',
$tenantContext->getAvailableDepartmentsByTenant($userId)
);
}
public function clear(AppContainer $container): void
{
$sessionStore = $container->get(SessionStoreInterface::class);
if ($sessionStore instanceof SessionStoreInterface) {
$sessionStore->remove('module.addressbook.departments_by_tenant');
}
}
}

View File

@@ -3,8 +3,8 @@
/**
* Address Book module manifest.
*
* Contributes routes, sidebar slots, search entry, user-edit aside action,
* layout context data, and session lifecycle.
* Contributes routes, explorer nav link, search entry, user-edit aside action,
* and authorization policy.
*/
return [
'id' => 'addressbook',
@@ -27,16 +27,12 @@ return [
],
'ui_slots' => [
'aside.tab_panel' => [
'explorer.nav_item' => [
[
'key' => 'people',
'key' => 'address-book',
'label' => 'Address book',
'icon' => 'bi-people',
'href' => 'address-book',
'permission' => 'addressbook.view',
'panel_template' => 'templates/aside-people-panel.phtml',
'details_storage' => 'aside-people-tenant',
'details_open_active' => true,
'order' => 110,
],
],
@@ -77,13 +73,9 @@ return [
],
'scheduler_jobs' => [],
'layout_context_providers' => [
\MintyPHP\Module\AddressBook\Providers\AddressBookLayoutProvider::class,
],
'layout_context_providers' => [],
'session_providers' => [
\MintyPHP\Module\AddressBook\Providers\AddressBookSessionProvider::class,
],
'session_providers' => [],
'permissions' => [
[

View File

@@ -1,89 +0,0 @@
<?php
/**
* Address book aside panel — people browser with tenant/department hierarchy.
*
* Rendered by the module slot system. The outer <nav> with id, role, aria
* attributes is created by the slot renderer in app-main-aside.phtml.
* This template provides only the inner content.
*
* Available in scope via include: $layoutNav, $layoutAuth, $viewAuth
*/
$addressBook = is_array($layoutNav['addressbook.nav'] ?? null) ? $layoutNav['addressbook.nav'] : [];
$addressBookUrl = trim((string) ($addressBook['url'] ?? lurl('address-book')));
$activeAddressSearch = trim((string) ($addressBook['activeSearch'] ?? ''));
$activeAddressTenants = is_array($addressBook['activeTenants'] ?? null) ? $addressBook['activeTenants'] : [];
$activeAddressDepartments = is_array($addressBook['activeDepartments'] ?? null) ? $addressBook['activeDepartments'] : [];
$activeAddressRoles = is_array($addressBook['activeRoles'] ?? null) ? $addressBook['activeRoles'] : [];
$activeAddressCustomFilters = is_array($addressBook['activeCustomFilters'] ?? null) ? $addressBook['activeCustomFilters'] : [];
$peopleGroups = is_array($addressBook['peopleGroups'] ?? null) ? $addressBook['peopleGroups'] : [];
$addressBookActive = navActive('address-book', true);
?>
<ul>
<?php if (!$peopleGroups): ?>
<li>
<a href="<?php e($addressBookUrl); ?>" class="<?php e($addressBookActive['class'] ?? ''); ?>"
<?php echo $addressBookActive['aria'] ?? ''; ?>>
<?php e(t('Address book')); ?>
</a>
</li>
<?php else: ?>
<?php foreach ($peopleGroups as $index => $group): ?>
<?php
$tenant = $group['tenant'] ?? [];
$departments = $group['departments'] ?? [];
$tenantUuid = (string) ($tenant['uuid'] ?? '');
$tenantName = (string) ($tenant['description'] ?? '');
if ($tenantUuid === '') {
continue;
}
$baseHref = $addressBookUrl . '?tenants=' . urlencode($tenantUuid);
$isActiveTenant = $addressBookActive['isActive']
&& !$activeAddressDepartments
&& count($activeAddressTenants) === 1
&& $activeAddressTenants[0] === $tenantUuid;
?>
<li class="app-sidebar-group">
<small>
<a href="<?php e($baseHref); ?>" class="<?php e($isActiveTenant ? 'active' : 'muted'); ?>"
<?php echo $isActiveTenant ? 'aria-current="page"' : ''; ?>>
<?php e($tenantName); ?>
</a>
</small>
<ul>
<?php if (!$departments): ?>
<li>
<?php
$emptyState = [
'message' => t('No departments'),
'size' => 'small',
'align' => 'center',
];
require templatePath('partials/app-empty-state.phtml');
?>
</li>
<?php else: ?>
<?php foreach ($departments as $department): ?>
<?php
$departmentId = (int) ($department['id'] ?? 0);
if ($departmentId <= 0) {
continue;
}
$departmentName = (string) ($department['description'] ?? '');
$href = $baseHref . '&departments=' . $departmentId;
$isActive = $addressBookActive['isActive']
&& in_array($tenantUuid, $activeAddressTenants, true)
&& in_array($departmentId, $activeAddressDepartments, true);
?>
<li>
<a href="<?php e($href); ?>" class="<?php e($isActive ? 'active' : ''); ?>"
<?php echo $isActive ? 'aria-current="page"' : ''; ?>>
<?php e($departmentName); ?>
</a>
</li>
<?php endforeach; ?>
<?php endif; ?>
</ul>
</li>
<?php endforeach; ?>
<?php endif; ?>
</ul>