docs(webui): add user-management UI redesign spec

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
m4
2026-08-12 16:12:27 +08:00
parent 3efaa9f34e
commit 1ae2fe1b2a
@@ -0,0 +1,138 @@
# User Management UI Redesign — Design
Date: 2026-08-12
Status: Approved (brainstorming 2026-08-12)
## Goal
The admin user-management UI (`UserManagementSection.tsx`, embedded in
`AccountDialog`) is a plain list with native `<input>`/`<select>` elements and
`window.prompt`/`window.confirm` interactions. Redesign it in place to match
the rest of the app's visual quality and interaction patterns.
Decisions locked in during brainstorming:
- **Scope:** in-place beautification + interaction upgrade. The section stays
inside `AccountDialog`; no standalone admin page.
- **Features:** unchanged — list, create, promote/demote, reset password,
delete. No new backend capabilities.
- **Layout:** list rows with avatar + role badge + a `⋯` actions dropdown;
"Add user" becomes a header button that opens a dialog.
- **i18n:** all copy goes through next-intl (zh + en), replacing the current
hardcoded English strings.
## Current state
`src/app/components/UserManagementSection.tsx` renders:
- a `<ul>` of users with three small outline buttons per row (Promote/Demote,
Password, Delete),
- a persistent bottom form (two native inputs + native `<select>` + Create
button) for adding users,
- `window.prompt` for password reset and `window.confirm` for deletion,
- hardcoded English copy despite the app shipping zh/en i18n.
Available shadcn/ui primitives in `src/components/ui/`: `button`, `dialog`,
`dropdown-menu`, `input`, `label`, `select`, `skeleton`, `scroll-area` —
everything needed; no new dependencies.
## Components
### 1. Reworked `UserManagementSection`
Same file, same props (`{ self: string }`), same fetch logic and API calls.
**Header row:** section title + user count (`3 users`), and a primary-size-sm
"Add user" button on the right that opens the create dialog.
**User rows** replace the `<ul>` styling:
- Left: circular avatar (`size-8 rounded-full bg-accent`) showing the
username's first letter, uppercased.
- Middle: username (`font-medium truncate`), a role badge, and a `(you)`
marker for the self row. Badges: admin = `bg-primary/10 text-primary`,
user = `bg-accent text-muted-foreground`; shared badge markup extracted to a
small `RoleBadge` inline component.
- Right: `DropdownMenu` triggered by a ghost icon button (`⋯`,
`aria-label` = "Actions for {username}") with items:
- "Make admin" / "Make user" (label depends on current role),
- "Reset password",
- "Delete" — `text-destructive`, separated by `DropdownMenuSeparator`,
hidden on the self row.
- Rows get `hover:bg-accent/50` and consistent `px-3 py-2` rhythm.
**States:**
- Loading: three `Skeleton` rows (`size-8` circle + two bars) instead of the
"Loading users…" text.
- Empty: centered muted placeholder line.
- Error: existing `role="alert"` destructive paragraph, restyled into the
section header area.
**Scrolling:** the list is wrapped in the existing `scroll-area` with a max
height so the dialog doesn't grow unbounded with many users.
### 2. Three dialogs (all using `dialog.tsx`)
Managed by local state in `UserManagementSection`:
- **`CreateUserDialog`** — fields: username (`input.tsx` + `label.tsx`),
password (`type="password"`, hint "8–256 characters"), role (`select.tsx`).
Submit disabled until username matches the backend pattern
(`/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/`) and password ≥ 8 chars. API errors
render inline above the footer (destructive text), not just as a toast.
On success: toast + close + refresh.
- **`ResetPasswordDialog`** — single password field + hint, targeted at one
user; title includes the username. Replaces `window.prompt`.
- **`DeleteUserDialog`** — confirmation with the username repeated in the
body; confirm button is `variant="destructive"`. Replaces
`window.confirm`.
Only one dialog is open at a time; each receives `open`, `onOpenChange`, and
the target user (create dialog: none).
### 3. i18n
New namespace `userManagement` in
`src/i18n/messages/{zh,en}/userManagement.ts`, registered in both `index.ts`
files (spread like the existing modules). Keys cover: section title, user
count (ICU plural), badges, `(you)`, menu items, all three dialogs (titles,
descriptions, labels, hints, submit/cancel), empty/loading states, and success
toasts. Component calls `useTranslations("userManagement")`.
## Data flow
Unchanged: `GET /api/auth/users`, `POST /api/auth/users`,
`PATCH/DELETE /api/auth/users/[username]`. The existing `run()` helper
(busy-flag + toast + refresh) is kept; dialogs layer on top of it and surface
API errors inline before dismissing.
## Error handling
- Client-side validation mirrors server rules (username pattern, password
8–256) so most errors never hit the network.
- Server errors (e.g. last-admin guard on demote/delete) show both in the
open dialog (inline) and via the existing `errorToast("users.manage", …)`.
- The self row hides Delete; demoting/deleting the last admin remains a
server-enforced rule — the UI does not try to predict it.
## Testing
Vitest + Testing Library, following existing `*.test.ts(x)` conventions:
- List rendering: rows, badges, self marker, Delete hidden for self.
- Create dialog: validation gating (bad username, short password), successful
submit calls the right endpoint and closes.
- Menu actions: promote/demote label flips with role; reset password and
delete open their dialogs with the right username.
- Delete flow: cancel does nothing; confirm sends DELETE and refreshes.
- Loading skeleton and empty states render.
Fetch is mocked at the `fetch` boundary, as in existing component tests.
## Out of scope
- Backend changes of any kind.
- A standalone `/users` page, search/filter/pagination, avatar uploads,
disable/enable accounts.
- Restyling `AccountDialog` itself beyond what the section needs.