diff --git a/modules/addressbook/lib/Module/AddressBook/Providers/AddressBookLayoutProvider.php b/modules/addressbook/lib/Module/AddressBook/Providers/AddressBookLayoutProvider.php deleted file mode 100644 index 7381a25..0000000 --- a/modules/addressbook/lib/Module/AddressBook/Providers/AddressBookLayoutProvider.php +++ /dev/null @@ -1,80 +0,0 @@ -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 $rawQuery - * @return array> - */ - 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; - } -} diff --git a/modules/addressbook/lib/Module/AddressBook/Providers/AddressBookSessionProvider.php b/modules/addressbook/lib/Module/AddressBook/Providers/AddressBookSessionProvider.php deleted file mode 100644 index f09f983..0000000 --- a/modules/addressbook/lib/Module/AddressBook/Providers/AddressBookSessionProvider.php +++ /dev/null @@ -1,51 +0,0 @@ -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'); - } - } -} diff --git a/modules/addressbook/module.php b/modules/addressbook/module.php index 5db8783..b4974e1 100644 --- a/modules/addressbook/module.php +++ b/modules/addressbook/module.php @@ -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' => [ [ diff --git a/modules/addressbook/templates/aside-people-panel.phtml b/modules/addressbook/templates/aside-people-panel.phtml deleted file mode 100644 index 9cee5bd..0000000 --- a/modules/addressbook/templates/aside-people-panel.phtml +++ /dev/null @@ -1,89 +0,0 @@ - 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); -?> - diff --git a/templates/partials/app-main-aside.phtml b/templates/partials/app-main-aside.phtml index f9553d8..84ab36a 100644 --- a/templates/partials/app-main-aside.phtml +++ b/templates/partials/app-main-aside.phtml @@ -6,6 +6,9 @@ $layoutAuth = is_array($viewAuth['layout'] ?? null) ? $viewAuth['layout'] : []; $moduleNavItemSlots = is_array(($layoutNav ?? [])['moduleSlots']['sidebar.admin_nav_item'] ?? null) ? $layoutNav['moduleSlots']['sidebar.admin_nav_item'] : []; +$moduleExplorerNavSlots = is_array(($layoutNav ?? [])['moduleSlots']['explorer.nav_item'] ?? null) + ? $layoutNav['moduleSlots']['explorer.nav_item'] + : []; $canViewTenants = (bool) ($layoutAuth['can_view_tenants'] ?? false); $canViewDepartments = (bool) ($layoutAuth['can_view_departments'] ?? false); $canViewUsers = (bool) ($layoutAuth['can_view_users'] ?? false); @@ -308,6 +311,26 @@ $moduleSearchSlots = is_array($moduleSlots['search.resource_item'] ?? null) ? $m + +
  • + > + + +
  • + diff --git a/tests/Architecture/ModuleRegistryBootstrapContractTest.php b/tests/Architecture/ModuleRegistryBootstrapContractTest.php index 1ca5c47..7397684 100644 --- a/tests/Architecture/ModuleRegistryBootstrapContractTest.php +++ b/tests/Architecture/ModuleRegistryBootstrapContractTest.php @@ -22,9 +22,9 @@ final class ModuleRegistryBootstrapContractTest extends TestCase self::assertTrue($registry->hasModule('addressbook')); self::assertNotEmpty($registry->getPermissions()); self::assertNotEmpty($registry->getSearchResources()); - self::assertNotEmpty($registry->getLayoutContextProviders()); - self::assertNotEmpty($registry->getSessionProviders()); - self::assertNotEmpty($registry->getSlotContributions('aside.tab_panel')); + self::assertEmpty($registry->getLayoutContextProviders()); + self::assertEmpty($registry->getSessionProviders()); + self::assertNotEmpty($registry->getSlotContributions('explorer.nav_item')); } public function testEmptyEnvDisablesAllConfiguredModules(): void diff --git a/tests/Unit/Module/LayoutContextProviderTest.php b/tests/Unit/Module/LayoutContextProviderTest.php deleted file mode 100644 index fc689e0..0000000 --- a/tests/Unit/Module/LayoutContextProviderTest.php +++ /dev/null @@ -1,86 +0,0 @@ -has(\MintyPHP\App\Module\ModuleRegistry::class) - || !($GLOBALS['minty_app_container'])->has(\MintyPHP\Service\Access\UiAccessService::class)) { - $container = require dirname(__DIR__, 3) . '/lib/App/registerContainer.php'; - $this->pushAppContainer($container); - setAppContainer($container); - } - } - - protected function tearDown(): void - { - $this->restoreAppContainer(); - } - - public function testProviderReturnsAddressBookKey(): void - { - $provider = new AddressBookLayoutProvider(); - $container = new AppContainer(); - - $session = [ - 'module.addressbook.departments_by_tenant' => [ - [ - 'tenant' => ['uuid' => 'abc', 'description' => 'Tenant A'], - 'departments' => [['id' => 1, 'description' => 'Dept 1']], - ], - ], - ]; - - $result = $provider->provide($session, $container); - - self::assertArrayHasKey('addressbook.nav', $result); - $ab = $result['addressbook.nav']; - self::assertArrayHasKey('url', $ab); - self::assertArrayHasKey('activeSearch', $ab); - self::assertArrayHasKey('activeTenants', $ab); - self::assertArrayHasKey('activeDepartments', $ab); - self::assertArrayHasKey('activeRoles', $ab); - self::assertArrayHasKey('activeCustomFilters', $ab); - self::assertArrayHasKey('peopleGroups', $ab); - self::assertCount(1, $ab['peopleGroups']); - } - - public function testProviderReturnsEmptyPeopleGroupsWhenSessionEmpty(): void - { - $provider = new AddressBookLayoutProvider(); - $container = new AppContainer(); - - $result = $provider->provide([], $container); - - self::assertArrayHasKey('addressbook.nav', $result); - self::assertSame([], $result['addressbook.nav']['peopleGroups']); - } - - public function testProviderIsIncludedInLayoutNavWhenModuleActive(): void - { - // When the module is active, appBuildLayoutNavContext() should include - // addressbook.nav via the provider loop - $layoutNav = appBuildLayoutNavContext([], [], []); - - self::assertArrayHasKey( - 'addressbook.nav', - $layoutNav, - 'addressbook.nav key must be present in layoutNav when module is active' - ); - } -} diff --git a/tests/Unit/Module/SessionProviderTest.php b/tests/Unit/Module/SessionProviderTest.php deleted file mode 100644 index 1566d0f..0000000 --- a/tests/Unit/Module/SessionProviderTest.php +++ /dev/null @@ -1,83 +0,0 @@ -set(SessionStoreInterface::class, static fn (): SessionStore => new SessionStore()); - return $container; - } - - public function testPopulateWithInvalidUserClearsSessionKey(): void - { - $_SESSION['module.addressbook.departments_by_tenant'] = [['tenant' => 'old']]; - - $provider = new AddressBookSessionProvider(); - $provider->populate(['id' => 0], $this->containerWithSessionStore()); - - self::assertArrayNotHasKey('module.addressbook.departments_by_tenant', $_SESSION); - } - - public function testPopulateWithMissingUserIdClearsSessionKey(): void - { - $_SESSION['module.addressbook.departments_by_tenant'] = [['tenant' => 'old']]; - - $provider = new AddressBookSessionProvider(); - $provider->populate([], $this->containerWithSessionStore()); - - self::assertArrayNotHasKey('module.addressbook.departments_by_tenant', $_SESSION); - } - - public function testPopulateReturnsEarlyWhenContainerMissesDependencies(): void - { - $container = $this->containerWithSessionStore(); - $provider = new AddressBookSessionProvider(); - // Container has SessionStore but no UserTenantContextService → provider returns early - $provider->populate(['id' => 42], $container); - - self::assertArrayNotHasKey('module.addressbook.departments_by_tenant', $_SESSION); - } - - public function testClearRemovesSessionKey(): void - { - $_SESSION['module.addressbook.departments_by_tenant'] = [ - ['tenant' => ['uuid' => 'abc'], 'departments' => []], - ]; - - $provider = new AddressBookSessionProvider(); - $provider->clear($this->containerWithSessionStore()); - - self::assertArrayNotHasKey('module.addressbook.departments_by_tenant', $_SESSION); - } - - public function testClearIsIdempotent(): void - { - // Key doesn't exist — clear() should not throw - $provider = new AddressBookSessionProvider(); - $provider->clear($this->containerWithSessionStore()); - - self::assertArrayNotHasKey('module.addressbook.departments_by_tenant', $_SESSION); - } -} diff --git a/web/css/vendor-overrides/gridjs.css b/web/css/vendor-overrides/gridjs.css index ed3b2bd..e72e2cf 100644 --- a/web/css/vendor-overrides/gridjs.css +++ b/web/css/vendor-overrides/gridjs.css @@ -191,12 +191,13 @@ min-width: 0; } - .grid-name-cell > a { + .grid-name-cell > a:first-child, + .grid-name-cell > span:first-child { display: inline-flex; flex-shrink: 0; } - .grid-name-cell > span:last-child { + .grid-name-cell > :last-child { min-width: 0; overflow: hidden; text-overflow: ellipsis;