diff --git a/web/src/lib/components/AdminSidebar.svelte b/web/src/lib/components/AdminSidebar.svelte new file mode 100644 index 00000000..8097f1e9 --- /dev/null +++ b/web/src/lib/components/AdminSidebar.svelte @@ -0,0 +1,68 @@ + + + + + diff --git a/web/src/lib/components/AdminSidebar.test.ts b/web/src/lib/components/AdminSidebar.test.ts new file mode 100644 index 00000000..792913e7 --- /dev/null +++ b/web/src/lib/components/AdminSidebar.test.ts @@ -0,0 +1,51 @@ +import { describe, expect, test, vi } from 'vitest'; +import { render, screen } from '@testing-library/svelte'; + +const state = vi.hoisted(() => ({ + pageUrl: new URL('http://localhost/admin') +})); + +vi.mock('$app/state', () => ({ + page: { get url() { return state.pageUrl; } } +})); + +import AdminSidebar from './AdminSidebar.svelte'; + +describe('AdminSidebar', () => { + test('Overview link is active when on /admin', () => { + state.pageUrl = new URL('http://localhost/admin'); + render(AdminSidebar); + const overview = screen.getByRole('link', { name: /overview/i }); + expect(overview).toHaveAttribute('aria-current', 'page'); + }); + + test('Integrations link is active when on /admin/integrations', () => { + state.pageUrl = new URL('http://localhost/admin/integrations'); + render(AdminSidebar); + expect(screen.getByRole('link', { name: /integrations/i })).toHaveAttribute( + 'aria-current', + 'page' + ); + expect(screen.getByRole('link', { name: /overview/i })).not.toHaveAttribute('aria-current'); + }); + + test('Requests link is active when on /admin/requests', () => { + state.pageUrl = new URL('http://localhost/admin/requests'); + render(AdminSidebar); + expect(screen.getByRole('link', { name: /requests/i })).toHaveAttribute( + 'aria-current', + 'page' + ); + }); + + test('placeholder items render as non-links with aria-disabled', () => { + state.pageUrl = new URL('http://localhost/admin'); + render(AdminSidebar); + expect(screen.queryByRole('link', { name: /quarantine/i })).not.toBeInTheDocument(); + const quar = screen.getByText(/quarantine/i); + expect(quar.closest('[aria-disabled="true"]')).toBeInTheDocument(); + // Users + Library are also placeholders today. + expect(screen.queryByRole('link', { name: /^users$/i })).not.toBeInTheDocument(); + expect(screen.queryByRole('link', { name: /^library$/i })).not.toBeInTheDocument(); + }); +}); diff --git a/web/src/lib/components/Shell.svelte b/web/src/lib/components/Shell.svelte index 9c83f655..9fc18724 100644 --- a/web/src/lib/components/Shell.svelte +++ b/web/src/lib/components/Shell.svelte @@ -30,6 +30,13 @@ { href: '/playlists', label: 'Playlists' }, { href: '/settings', label: 'Settings' } ]; + + // Admin link sits between Playlists and Settings, only visible to admins. + const visibleNavItems = $derived( + user.value?.is_admin + ? [...navItems.slice(0, -1), { href: '/admin', label: 'Admin' }, navItems[navItems.length - 1]] + : navItems + ); (menuOpen = false)} onkeydown={(e) => e.key === 'Escape' && (menuOpen = false)} /> @@ -70,7 +77,7 @@