From 48cbec8fae3c8c6756bc08e4b1b5ce200b70a77d Mon Sep 17 00:00:00 2001 From: Edouard Vanbelle Date: Thu, 30 Jul 2026 00:52:12 +0200 Subject: [PATCH] feat(admin ui): too many tabs, change display --- frontend/src/lib/components/AppShell.svelte | 155 +++++++++++++-- .../routes/admin/{ => [[tab]]}/+page.svelte | 177 +++++++----------- .../routes/admin/{ => [[tab]]}/page.test.ts | 0 frontend/static/locales/en.json | 2 + 4 files changed, 211 insertions(+), 123 deletions(-) rename frontend/src/routes/admin/{ => [[tab]]}/+page.svelte (98%) rename frontend/src/routes/admin/{ => [[tab]]}/page.test.ts (100%) diff --git a/frontend/src/lib/components/AppShell.svelte b/frontend/src/lib/components/AppShell.svelte index 861f8ab2..934610e8 100644 --- a/frontend/src/lib/components/AppShell.svelte +++ b/frontend/src/lib/components/AppShell.svelte @@ -32,15 +32,12 @@ const palette = lazyComponent(() => import('$lib/components/CommandPalette.svelte')); interface NavLink { - href: - | '/files' - | '/shared' - | '/shared-with-me' - | '/recent' - | '/favorites' - | '/photos' - | '/music' - | '/trash'; + /** + * String rather than a literal union so admin links (which + * include a dynamic path segment) can share the same shape. + * `resolve()` accepts any string, so no type-level cost. + */ + href: string; label: string; icon: string; /** Stable key driving the per-section icon colour (see sidebar.css). */ @@ -69,12 +66,103 @@ { href: '/trash', label: t('nav.trash', 'Trash'), icon: 'trash', section: 'trash' } ]; + // Admin sidebar — populated when the URL is under /admin. The + // admin +page.svelte used to render its own horizontal tab + // strip; that was displaced here so the section navigation + // scales past ~7 items and matches deep-link URLs from the + // address bar. + const ADMIN_LINKS: NavLink[] = [ + { + href: '/admin', + label: t('admin.dashboard', 'Dashboard'), + icon: 'chart-pie', + section: 'admin-dashboard' + }, + { + href: '/admin/users', + label: t('admin.users', 'Users'), + icon: 'users', + section: 'admin-users' + }, + { + href: '/admin/drives', + label: t('admin.drives', 'Drives'), + icon: 'folder', + section: 'admin-drives' + }, + { + href: '/admin/mounts', + label: t('admin.mounts', 'External Mounts'), + icon: 'folder', + section: 'admin-mounts' + }, + { + href: '/admin/oidc', + label: t('admin.oidc', 'OIDC / SSO'), + icon: 'key', + section: 'admin-oidc' + }, + { + href: '/admin/storage', + label: t('admin.storage_tab', 'Storage'), + icon: 'database', + section: 'admin-storage' + }, + { + href: '/admin/smtp', + label: t('admin.smtp', 'Email (SMTP)'), + icon: 'envelope', + section: 'admin-smtp' + }, + { + href: '/admin/plugins', + label: t('admin.plugins', 'Plugins'), + icon: 'layer-group', + section: 'admin-plugins' + }, + { + href: '/admin/jobs', + label: t('admin.jobs.tab', 'Jobs'), + icon: 'cogs', + section: 'admin-jobs' + } + ]; + const isAdmin = $derived(session.user?.role === 'admin'); + // Any URL under /admin swaps the sidebar to admin mode. Uses + // startsWith so a trailing slash / query params / hash don't + // desync. Root `/admin` counts too (dashboard). + const isAdminSection = $derived(page.url.pathname.startsWith('/admin')); + const currentLinks = $derived(isAdminSection ? ADMIN_LINKS : LINKS); + function active(href: string): boolean { return page.url.pathname === href || page.url.pathname.startsWith(`${href}/`); } + // Sidebar-item active check. Non-admin links use `active()` + // (matches href + any subpath). Admin links need a stricter + // rule for `/admin` itself — a plain `startsWith('/admin/')` + // would light up the Dashboard item on `/admin/drives` too. + // So `/admin` matches ONLY the exact path; every other admin + // item uses the same startsWith rule as before. + function activeLink(href: string): boolean { + if (href === '/admin') return page.url.pathname === '/admin'; + return active(href); + } + + /** + * Data-driven sidebar links (`LINKS`, `ADMIN_LINKS`) hold + * runtime strings, not compile-time route keys. SvelteKit's + * typed `resolve()` refuses them; we know they're valid + * routes at runtime. Cast at the callsite, one place, so + * the template stays clean. + */ + function navHref(href: string): string { + // @ts-expect-error runtime-known route string, not a literal typed key + return resolve(href); + } + // ── Sidebar drop targets ───────────────────────────────────────────────── // The row-drag on `/files` (and other resource surfaces) sets a // `application/x-oxi-item` MIME with a JSON array of `{ id, name, kind }`. @@ -467,16 +555,50 @@
OxiCloud
-