From 0da785f4b72926a7161a56796bd18557cb1444a0 Mon Sep 17 00:00:00 2001 From: fs Date: Wed, 22 Apr 2026 19:10:48 +0200 Subject: [PATCH] refactor(admin-tenants): simplify list to 2 columns (avatar + name, users) Reduce tenant grid from 8 columns to 2. Remove status, theme, sso, active/inactive users, and created columns from the list view. All details remain accessible on the edit page. - filter-schema: allow order by description and users only - data endpoint: return minimal row shape (id, uuid, description, status metadata for badge only, is_default, has_avatar, total_users) - JS grid: 2 visible columns + hidden UUID for navigation - CSS: add .grid-tenant-profile utility for flex avatar+name layout Refs: TENANT-LIST-MINIMAL-001 --- pages/admin/tenants/data().php | 78 ++------------ pages/admin/tenants/filter-schema.php | 2 +- pages/admin/tenants/index(default).phtml | 13 +-- web/css/vendor-overrides/gridjs.css | 15 +++ web/js/pages/admin-tenants-index.js | 128 ++++++----------------- 5 files changed, 59 insertions(+), 177 deletions(-) diff --git a/pages/admin/tenants/data().php b/pages/admin/tenants/data().php index 4965bf7..bf2f8cf 100644 --- a/pages/admin/tenants/data().php +++ b/pages/admin/tenants/data().php @@ -1,7 +1,6 @@ computeCounts( $tenantRows, $userTenantRepository->countUsersByTenantIds(...), $userTenantRepository->countActiveUsersByTenantIds(...) ); - $tenantIds = array_keys($userCounts); - $tenantSsoMap = $tenantMicrosoftAuthRepository->listByTenantIds($tenantIds); - $defaultTenantId = $settingsDefaultsGateway->getDefaultTenantId(); $rows = []; foreach ($tenantRows as $row) { $tenantId = (int) ($row['id'] ?? 0); - $tenantTheme = strtolower(trim((string) ($row['default_theme'] ?? ''))); - $themeIsTenantOverride = $tenantTheme !== '' && isset($themes[$tenantTheme]); - $resolvedThemeKey = $themeIsTenantOverride ? $tenantTheme : $globalDefaultTheme; - $resolvedThemeLabel = (string) ($themes[$resolvedThemeKey] ?? ucfirst($resolvedThemeKey)); - - $themePolicyRaw = $row['allow_user_theme'] ?? null; - $themePolicyMode = 'inherit'; - if ($themePolicyRaw !== null && $themePolicyRaw !== '') { - $normalized = strtolower(trim((string) $themePolicyRaw)); - if (in_array($normalized, ['1', 'true', 'yes', 'on'], true)) { - $themePolicyMode = 'allow'; - } elseif (in_array($normalized, ['0', 'false', 'no', 'off'], true)) { - $themePolicyMode = 'disallow'; - } - } - - $tenantSso = $tenantSsoMap[$tenantId] ?? null; - $ssoEnabled = !empty($tenantSso['enabled']); - $ssoMicrosoftOnly = $ssoEnabled && !empty($tenantSso['enforce_microsoft_login']); $counts = $userCounts[$tenantId] ?? ['active_users' => 0, 'inactive_users' => 0]; $tenantStatus = TenantStatus::normalizeOr((string) ($row['status'] ?? ''), TenantStatus::Active); $tenantUuid = (string) ($row['uuid'] ?? ''); @@ -75,20 +46,7 @@ $fetchRows = static function ( 'status' => $tenantStatus->value, 'status_badge' => $tenantStatus->badgeVariant(), 'status_label' => t($tenantStatus->labelToken()), - 'theme' => [ - 'key' => $resolvedThemeKey, - 'label' => t($resolvedThemeLabel), - 'is_override' => $themeIsTenantOverride ? 1 : 0, - 'policy_mode' => $themePolicyMode, - ], - 'sso' => [ - 'enabled' => $ssoEnabled ? 1 : 0, - 'microsoft_only' => $ssoMicrosoftOnly ? 1 : 0, - ], - 'active_users' => $counts['active_users'], - 'inactive_users' => $counts['inactive_users'], - 'created' => dt($row['created'] ?? ''), - 'modified' => dt($row['modified'] ?? ''), + 'total_users' => $counts['active_users'] + $counts['inactive_users'], 'has_avatar' => $tenantUuid !== '' && $tenantAvatarService->hasAvatar($tenantUuid), ]; } @@ -96,13 +54,6 @@ $fetchRows = static function ( return $rows; }; -$themes = appThemes(); -$globalDefaultTheme = $settingsAppGateway->getAppTheme(); -if ($globalDefaultTheme === null || !isset($themes[$globalDefaultTheme])) { - $envTheme = strtolower(trim((string) (getenv('APP_THEME') ?: 'light'))); - $globalDefaultTheme = isset($themes[$envTheme]) ? $envTheme : 'light'; -} - $listOptions = [ 'limit' => $filters['limit'], 'offset' => $filters['offset'], @@ -131,25 +82,10 @@ if (in_array($order, $computedOrderKeys, true)) { 'order' => 'description', 'dir' => 'asc', ]); - $rows = $fetchRows($fullResult['rows'] ?? [], $themes, $globalDefaultTheme); + $rows = $fetchRows($fullResult['rows'] ?? []); - usort($rows, static function (array $a, array $b) use ($order, $dir): int { - $valueA = ''; - $valueB = ''; - if ($order === 'theme') { - $valueA = strtolower((string) ($a['theme']['label'] ?? '')); - $valueB = strtolower((string) ($b['theme']['label'] ?? '')); - } elseif ($order === 'sso') { - $valueA = ((int) $a['sso']['enabled'] * 10) + (int) $a['sso']['microsoft_only']; - $valueB = ((int) $b['sso']['enabled'] * 10) + (int) $b['sso']['microsoft_only']; - } elseif ($order === 'inactive_users') { - $valueA = (int) $a['inactive_users']; - $valueB = (int) $b['inactive_users']; - } else { - $valueA = (int) $a['active_users']; - $valueB = (int) $b['active_users']; - } - $cmp = $valueA <=> $valueB; + usort($rows, static function (array $a, array $b) use ($dir): int { + $cmp = ((int) $a['total_users']) <=> ((int) $b['total_users']); if ($cmp === 0) { $cmp = strcasecmp((string) $a['description'], (string) $b['description']); } @@ -157,7 +93,7 @@ if (in_array($order, $computedOrderKeys, true)) { }); $rows = array_slice($rows, $filters['offset'], $filters['limit']); } else { - $rows = $fetchRows($result['rows'] ?? [], $themes, $globalDefaultTheme); + $rows = $fetchRows($result['rows'] ?? []); } gridJsonDataResult($rows, $total); diff --git a/pages/admin/tenants/filter-schema.php b/pages/admin/tenants/filter-schema.php index 977ec32..2879938 100644 --- a/pages/admin/tenants/filter-schema.php +++ b/pages/admin/tenants/filter-schema.php @@ -18,7 +18,7 @@ return gridFilterSchema([ 'status' => ['type' => 'enum', 'allowed' => TenantStatus::values(), 'default' => '', 'lowercase' => true], 'order' => [ 'type' => 'order', - 'allowed' => ['description', 'status', 'theme', 'sso', 'active_users', 'inactive_users', 'created', 'modified'], + 'allowed' => ['description', 'users'], 'default' => 'description', ], 'dir' => ['type' => 'dir', 'default' => 'asc'], diff --git a/pages/admin/tenants/index(default).phtml b/pages/admin/tenants/index(default).phtml index 31ce279..2a75936 100644 --- a/pages/admin/tenants/index(default).phtml +++ b/pages/admin/tenants/index(default).phtml @@ -44,17 +44,8 @@ $pageConfig = [ 'gridLang' => $gridLang, 'labels' => [ 'avatar' => t('Avatar'), - 'description' => t('Description'), - 'status' => t('Status'), - 'theme' => t('Theme'), - 'sso' => t('SSO'), - 'activeUsers' => t('Active users'), - 'inactiveUsers' => t('Inactive users'), - 'created' => t('Created'), - 'active' => t('Active'), - 'default' => t('Default'), - 'microsoftOnly' => t('Microsoft only'), - 'inactive' => t('Inactive'), + 'tenant' => t('Tenant'), + 'users' => t('Users'), ], ]; ?> diff --git a/web/css/vendor-overrides/gridjs.css b/web/css/vendor-overrides/gridjs.css index b42a1df..ccdbe32 100644 --- a/web/css/vendor-overrides/gridjs.css +++ b/web/css/vendor-overrides/gridjs.css @@ -203,6 +203,21 @@ text-overflow: ellipsis; } + .grid-tenant-profile { + display: inline-flex; + align-items: center; + gap: 0.75rem; + min-width: 0; + } + + .grid-tenant-profile-name-text { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-weight: 500; + } + .gridjs-loading-bar:after { background-image: linear-gradient( 90deg, diff --git a/web/js/pages/admin-tenants-index.js b/web/js/pages/admin-tenants-index.js index de010d6..2578051 100644 --- a/web/js/pages/admin-tenants-index.js +++ b/web/js/pages/admin-tenants-index.js @@ -1,5 +1,5 @@ import { createListPageModule } from '../core/app-list-page-module.js'; -import { badgeHtml, escapeHtml } from './app-list-utils.js'; +import { escapeHtml } from './app-list-utils.js'; createListPageModule({ configId: 'admin-tenants-index', @@ -7,54 +7,19 @@ createListPageModule({ missingGridMessage: 'Tenants grid init failed: Grid.js missing', initErrorMessage: 'Tenants grid init failed', setup: ({ config, gridjs, appBase, initListPage }) => { - const gridSearch = config.gridSearch ?? null; const filterSchema = config.filterSchema ?? []; const filterChipMeta = config.filterChipMeta ?? []; const labels = config.labels ?? {}; - const tenantUuidIndex = 8; - const tenantDescriptionIndex = 1; - const formatBadge = (value) => badgeHtml(gridjs, value); + const tenantUuidIndex = 2; const initialsForRow = (row) => { - const descriptionCell = row?.cells?.[tenantDescriptionIndex]?.data; + const descriptionCell = row?.cells?.[0]?.data; const label = typeof descriptionCell === 'object' && descriptionCell !== null ? String(descriptionCell.label ?? '') : String(descriptionCell ?? ''); const initial = label.trim().charAt(0).toUpperCase(); return initial || '?'; }; - const formatStatus = (value) => { - if (!value || typeof value !== 'object') { - return gridjs.html('-'); - } - const variant = String(value.variant || 'neutral'); - const label = String(value.label || '-'); - return gridjs.html(`${label}`); - }; - const formatTheme = (cell) => { - if (!cell || typeof cell !== 'object') { - return gridjs.html(''); - } - const themeLabel = escapeHtml(cell.label ?? ''); - const label = cell.is_override ? themeLabel : `${themeLabel} (${escapeHtml(labels.default || 'Default')})`; - return gridjs.html(label); - }; - const formatSso = (cell) => { - if (!cell || typeof cell !== 'object') { - return gridjs.html(''); - } - if (!cell.enabled) { - return gridjs.html(escapeHtml(labels.inactive || 'Inactive')); - } - if (cell.microsoft_only) { - return gridjs.html(escapeHtml(labels.microsoftOnly || 'Microsoft only')); - } - return gridjs.html(escapeHtml(labels.active || 'Active')); - }; - const formatCount = (value) => { - const number = Number(value ?? 0); - return Number.isFinite(number) ? number : 0; - }; const { gridConfig } = initListPage({ grid: { @@ -64,84 +29,59 @@ createListPageModule({ appBase, columns: [ { - name: labels.avatar || 'Avatar', - sort: false, + name: labels.tenant || 'Tenant', + sort: true, + width: '75%', formatter: (cell, row) => { - if (!cell) { + const hasAvatar = cell?.has_avatar ? 1 : 0; + const uuid = encodeURIComponent(String(cell?.uuid ?? '')); + let avatarHtml = ''; + if (hasAvatar && uuid) { + const src = new URL(`admin/tenants/avatar-file?uuid=${uuid}&size=64`, appBase).toString(); + avatarHtml = ``; + } else { const initials = escapeHtml(initialsForRow(row)); - return gridjs.html(`${initials}`); + avatarHtml = `${initials}`; } - const uuid = encodeURIComponent(String(cell)); - const src = new URL(`admin/tenants/avatar-file?uuid=${uuid}&size=64`, appBase).toString(); - const full = new URL(`admin/tenants/avatar-file?uuid=${uuid}&size=256`, appBase).toString(); - return gridjs.html(``); + + const nameText = escapeHtml(String(cell?.label ?? '')); + + return gridjs.html( + `
` + + `${avatarHtml}` + + `${nameText}` + + `
` + ); }, }, { - name: labels.description || 'Description', + name: labels.users || 'Users', sort: true, + width: '25%', formatter: (cell) => { - if (!cell || typeof cell !== 'object') { - return gridjs.html(escapeHtml(String(cell ?? ''))); - } - const label = escapeHtml(cell.label ?? ''); - return gridjs.html(label); + const number = Number(cell ?? 0); + return gridjs.html(String(Number.isFinite(number) ? number : 0)); }, }, - { - name: labels.status || 'Status', - sort: false, - formatter: (cell) => formatStatus(cell), - }, - { - name: labels.theme || 'Theme', - sort: true, - formatter: (cell) => formatTheme(cell), - }, - { - name: labels.sso || 'SSO', - sort: true, - formatter: (cell) => formatSso(cell), - }, - { - name: labels.activeUsers || 'Active users', - sort: true, - formatter: (cell) => formatCount(cell), - }, - { - name: labels.inactiveUsers || 'Inactive users', - sort: true, - formatter: (cell) => formatCount(cell), - }, - { - name: labels.created || 'Created', - sort: true, - formatter: (cell) => formatBadge(cell), - }, { name: 'UUID', hidden: true, }, ], - sortColumns: [null, 'description', 'status', 'theme', 'sso', 'active_users', 'inactive_users', 'created', null], + sortColumns: ['description', 'users', null], paginationLimit: 10, language: config.gridLang ?? {}, mapData: (data) => data.data.map((row) => [ - row.has_avatar ? row.uuid : '', { - label: row.description, - is_default: row.is_default ? 1 : 0, + uuid: row.uuid ?? '', + label: row.description ?? '', + has_avatar: row.has_avatar ? 1 : 0, }, - { variant: row.status_badge || 'neutral', label: row.status_label || row.status || '-' }, - row.theme ?? null, - row.sso ?? null, - row.active_users ?? 0, - row.inactive_users ?? 0, - row.created, + row.total_users ?? 0, row.uuid, ]), - rowInteraction: { linkColumn: 1 }, - search: gridSearch, + rowInteraction: { linkColumn: 0 }, + search: config.gridSearch ?? null, filterSchema, urlSync: true, rowDataset: (row) => ({