From 5e6031a7cda5cd2750659a422026e40663a466ec Mon Sep 17 00:00:00 2001 From: Guido X Jansen Date: Wed, 4 Mar 2026 15:51:12 +0100 Subject: [PATCH] fix(admin): replace error states with P3 placeholders on users and plugins pages (#161) The admin users and plugins pages showed "API may be unreachable" errors because their backend endpoints are planned for P3.2 and not yet implemented. Replace with clear "Coming in P3" placeholder states. --- src/app/admin/plugins/page.test.tsx | 108 +++------------------------- src/app/admin/plugins/page.tsx | 83 +++------------------ src/app/admin/users/page.test.tsx | 51 ++----------- src/app/admin/users/page.tsx | 103 +++----------------------- 4 files changed, 33 insertions(+), 312 deletions(-) diff --git a/src/app/admin/plugins/page.test.tsx b/src/app/admin/plugins/page.test.tsx index 4a06aed..ad6c405 100644 --- a/src/app/admin/plugins/page.test.tsx +++ b/src/app/admin/plugins/page.test.tsx @@ -1,15 +1,13 @@ /** - * Tests for admin plugins page. + * Tests for admin plugins page (P3 placeholder). * @see specs/prd-web.md Section M13 */ -import { describe, it, expect } from 'vitest' -import { render, screen, waitFor, within } from '@testing-library/react' -import userEvent from '@testing-library/user-event' +import { describe, it, expect, vi } from 'vitest' +import { render, screen } from '@testing-library/react' import { axe } from 'vitest-axe' import AdminPluginsPage from './page' -// Mock next/navigation vi.mock('next/navigation', () => ({ usePathname: () => '/admin/plugins', })) @@ -33,110 +31,20 @@ vi.mock('@/hooks/use-auth', () => { return { useAuth: () => mockAuth } }) -vi.mock('@/hooks/use-toast', () => ({ - useToast: () => ({ toast: vi.fn(), dismiss: vi.fn() }), -})) - describe('AdminPluginsPage', () => { - it('renders page heading', async () => { - render() - await waitFor(() => { - expect(screen.getByRole('heading', { name: /plugins/i, level: 1 })).toBeInTheDocument() - }) - }) - - it('renders plugin list from API', async () => { - render() - await waitFor(() => { - expect(screen.getByText('Full-Text Search')).toBeInTheDocument() - }) - expect(screen.getByText('Markdown Editor')).toBeInTheDocument() - expect(screen.getByText('Code Highlighting')).toBeInTheDocument() - expect(screen.getByText('Community Analytics')).toBeInTheDocument() - expect(screen.getByText('Webhook Notifications')).toBeInTheDocument() - }) - - it('shows source badges (Core, Official, Community, Experimental)', async () => { - render() - await waitFor(() => { - expect(screen.getAllByText('Core').length).toBeGreaterThanOrEqual(2) - }) - expect(screen.getByText('Official')).toBeInTheDocument() - expect(screen.getByText('Community')).toBeInTheDocument() - expect(screen.getByText('Experimental')).toBeInTheDocument() - }) - - it('shows version numbers', async () => { - render() - await waitFor(() => { - expect(screen.getByText('v1.0.0')).toBeInTheDocument() - }) - expect(screen.getByText('v1.2.0')).toBeInTheDocument() - expect(screen.getByText('v0.2.0-beta')).toBeInTheDocument() - }) - - it('shows enabled/disabled state for plugins', async () => { - render() - await waitFor(() => { - expect(screen.getByText('Full-Text Search')).toBeInTheDocument() - }) - // Find toggle buttons - enabled plugins should have checked toggles - const toggles = screen.getAllByRole('switch') - expect(toggles.length).toBeGreaterThanOrEqual(5) - }) - - it('shows dependency warning when attempting to disable a plugin with dependents', async () => { - const user = userEvent.setup() - render() - await waitFor(() => { - expect(screen.getByText('Markdown Editor')).toBeInTheDocument() - }) - - // Markdown Editor has dependents: ['barazo-plugin-code-highlight'] - // Find the Markdown Editor card and its toggle - const markdownCard = screen.getByText('Markdown Editor').closest('article') - expect(markdownCard).toBeTruthy() - const toggle = within(markdownCard!).getByRole('switch') - await user.click(toggle) - - // Should show dependency warning dialog - await waitFor(() => { - expect(screen.getByText('Dependency warning')).toBeInTheDocument() - }) - expect(screen.getByText('Disable anyway')).toBeInTheDocument() - }) - - it('opens settings modal when settings button is clicked', async () => { - const user = userEvent.setup() + it('renders page heading', () => { render() - await waitFor(() => { - expect(screen.getByText('Full-Text Search')).toBeInTheDocument() - }) - - // Click settings button on Full-Text Search plugin - const searchCard = screen.getByText('Full-Text Search').closest('article') - expect(searchCard).toBeTruthy() - const settingsBtn = within(searchCard!).getByRole('button', { name: /settings/i }) - await user.click(settingsBtn) - - // Should show settings modal with schema fields - await waitFor(() => { - expect(screen.getByText('Enable semantic search')).toBeInTheDocument() - }) + expect(screen.getByRole('heading', { name: /plugins/i, level: 1 })).toBeInTheDocument() }) - it('shows plugin descriptions', async () => { + it('shows coming in P3 message', () => { render() - await waitFor(() => { - expect(screen.getByText(/full-text search for topics and replies/i)).toBeInTheDocument() - }) + expect(screen.getByRole('heading', { name: /coming in p3/i })).toBeInTheDocument() + expect(screen.getByText(/planned for the p3\.2 milestone/i)).toBeInTheDocument() }) it('passes axe accessibility check', async () => { const { container } = render() - await waitFor(() => { - expect(screen.getByText('Full-Text Search')).toBeInTheDocument() - }) const results = await axe(container) expect(results).toHaveNoViolations() }) diff --git a/src/app/admin/plugins/page.tsx b/src/app/admin/plugins/page.tsx index 83283f0..63a7e21 100644 --- a/src/app/admin/plugins/page.tsx +++ b/src/app/admin/plugins/page.tsx @@ -2,90 +2,27 @@ * Admin plugin management page. * URL: /admin/plugins * Lists installed plugins with enable/disable, settings, and uninstall controls. - * Phase 1: manage installed plugins. Phase 2: marketplace search/install. + * Backend endpoints not yet implemented (planned for P3.2). * @see specs/prd-web.md Section M13 */ -'use client' - +import { PuzzlePiece } from '@phosphor-icons/react/dist/ssr' import { AdminLayout } from '@/components/admin/admin-layout' -import { ErrorAlert } from '@/components/error-alert' -import { PluginCard } from '@/components/admin/plugins/plugin-card' -import { PluginSettingsModal } from '@/components/admin/plugins/plugin-settings-modal' -import { DependencyWarningDialog } from '@/components/admin/plugins/dependency-warning-dialog' -import { usePluginManagement } from '@/hooks/admin/use-plugin-management' export default function AdminPluginsPage() { - const { - plugins, - loading, - settingsPlugin, - setSettingsPlugin, - dependencyWarning, - setDependencyWarning, - loadError, - actionError, - setActionError, - fetchPlugins, - handleToggle, - confirmDisable, - handleSaveSettings, - handleUninstall, - } = usePluginManagement() - return (
-
-

Plugins

-

- {plugins.filter((p) => p.enabled).length} of {plugins.length} enabled +

Plugins

+ +
+ +

Coming in P3

+

+ Plugin management (install, enable/disable, configure) is planned for the P3.2 + milestone. The plugin API endpoints are not yet available.

- - {loadError && ( - void fetchPlugins()} /> - )} - - {actionError && setActionError(null)} />} - - {loading &&

Loading plugins...

} - - {!loading && plugins.length === 0 && ( -

No plugins installed.

- )} - - {!loading && plugins.length > 0 && ( -
- {plugins.map((plugin) => ( - void handleToggle(p)} - onUninstall={(p) => void handleUninstall(p)} - /> - ))} -
- )} - - {dependencyWarning && ( - void confirmDisable()} - onCancel={() => setDependencyWarning(null)} - /> - )} - - {settingsPlugin && ( - setSettingsPlugin(null)} - onSave={(settings) => void handleSaveSettings(settings)} - /> - )}
) diff --git a/src/app/admin/users/page.test.tsx b/src/app/admin/users/page.test.tsx index 0bb4060..6904682 100644 --- a/src/app/admin/users/page.test.tsx +++ b/src/app/admin/users/page.test.tsx @@ -1,9 +1,9 @@ /** - * Tests for admin user management page. + * Tests for admin user management page (P3 placeholder). */ import { describe, it, expect, vi } from 'vitest' -import { render, screen, waitFor } from '@testing-library/react' +import { render, screen } from '@testing-library/react' import { axe } from 'vitest-axe' import AdminUsersPage from './page' @@ -50,61 +50,20 @@ vi.mock('@/hooks/use-auth', () => { return { useAuth: () => mockAuth } }) -vi.mock('@/hooks/use-toast', () => ({ - useToast: () => ({ toast: vi.fn(), dismiss: vi.fn() }), -})) - describe('AdminUsersPage', () => { it('renders user management heading', () => { render() expect(screen.getByRole('heading', { name: /user management/i })).toBeInTheDocument() }) - it('renders user list from API', async () => { - render() - await waitFor(() => { - expect(screen.getByText('Jay Admin')).toBeInTheDocument() - }) - expect(screen.getByText('Alex Moderator')).toBeInTheDocument() - expect(screen.getByText('Sam Member')).toBeInTheDocument() - }) - - it('shows user roles', async () => { - render() - await waitFor(() => { - expect(screen.getByText('admin')).toBeInTheDocument() - }) - expect(screen.getByText('moderator')).toBeInTheDocument() - }) - - it('shows banned status', async () => { - render() - await waitFor(() => { - expect(screen.getByText('Morgan Banned')).toBeInTheDocument() - }) - // Morgan should show as banned - expect(screen.getByText('Banned')).toBeInTheDocument() - }) - - it('shows cross-community ban warning', async () => { - render() - await waitFor(() => { - expect(screen.getByText(/banned from 2 other communities/i)).toBeInTheDocument() - }) - }) - - it('shows ban/unban buttons', async () => { + it('shows coming in P3 message', () => { render() - await waitFor(() => { - expect(screen.getAllByRole('button', { name: /ban/i }).length).toBeGreaterThan(0) - }) + expect(screen.getByRole('heading', { name: /coming in p3/i })).toBeInTheDocument() + expect(screen.getByText(/planned for the p3\.2 milestone/i)).toBeInTheDocument() }) it('passes axe accessibility check', async () => { const { container } = render() - await waitFor(() => { - expect(screen.getByText('Jay Admin')).toBeInTheDocument() - }) const results = await axe(container) expect(results).toHaveNoViolations() }) diff --git a/src/app/admin/users/page.tsx b/src/app/admin/users/page.tsx index d38288c..326c526 100644 --- a/src/app/admin/users/page.tsx +++ b/src/app/admin/users/page.tsx @@ -2,110 +2,27 @@ * Admin user management page. * URL: /admin/users * User list with ban controls and cross-community ban warnings. + * Backend endpoint not yet implemented (planned for P3.2). * @see specs/prd-web.md Section M11 */ -'use client' - -import { useState, useEffect, useCallback } from 'react' +import { Users } from '@phosphor-icons/react/dist/ssr' import { AdminLayout } from '@/components/admin/admin-layout' -import { ErrorAlert } from '@/components/error-alert' -import { UserCard } from '@/components/admin/users/user-card' -import { getAdminUsers, banUser, unbanUser } from '@/lib/api/client' -import type { AdminUser } from '@/lib/api/types' -import { useAuth } from '@/hooks/use-auth' -import { useToast } from '@/hooks/use-toast' export default function AdminUsersPage() { - const { getAccessToken } = useAuth() - const { toast } = useToast() - const [users, setUsers] = useState([]) - const [loading, setLoading] = useState(true) - const [loadError, setLoadError] = useState(null) - const [actionError, setActionError] = useState(null) - - const fetchUsers = useCallback(async () => { - setLoadError(null) - try { - const response = await getAdminUsers(getAccessToken() ?? '') - setUsers(response.users) - } catch { - setLoadError('Failed to load users. The API may be unreachable.') - } finally { - setLoading(false) - } - }, [getAccessToken]) - - useEffect(() => { - void fetchUsers() - }, [fetchUsers]) - - const handleBan = async (did: string) => { - setActionError(null) - try { - await banUser(did, 'Banned by admin', getAccessToken() ?? '') - setUsers((prev) => - prev.map((u) => - u.did === did - ? { - ...u, - isBanned: true, - bannedAt: new Date().toISOString(), - banReason: 'Banned by admin', - } - : u - ) - ) - toast({ title: 'User banned' }) - } catch { - setActionError('Failed to ban user. Please try again.') - } - } - - const handleUnban = async (did: string) => { - setActionError(null) - try { - await unbanUser(did, getAccessToken() ?? '') - setUsers((prev) => - prev.map((u) => - u.did === did ? { ...u, isBanned: false, bannedAt: null, banReason: null } : u - ) - ) - toast({ title: 'User unbanned' }) - } catch { - setActionError('Failed to unban user. Please try again.') - } - } - return (

User management

- {loadError && ( - void fetchUsers()} /> - )} - - {actionError && setActionError(null)} />} - - {loading &&

Loading users...

} - - {!loading && users.length === 0 && ( -

No users found.

- )} - - {!loading && users.length > 0 && ( -
- {users.map((user) => ( - void handleBan(did)} - onUnban={(did) => void handleUnban(did)} - /> - ))} -
- )} +
+ +

Coming in P3

+

+ User management (search, role assignment, ban controls) is planned for the P3.2 + milestone. The admin API endpoint is not yet available. +

+
) -- 2.51.2