a8dfbd5dc6
Delete the old top-level page files whose content was migrated into service-page tabs in slices 5-9: - pages/Media.tsx, Applications.tsx (-> MediaTab) - pages/FileBrowser.tsx, FileBrowser.impl.tsx (-> FilesTab) - pages/Actions.tsx (-> ActionsTab) - pages/Users.tsx, UsersPage.impl.tsx (replaced by Authentik tabs) - components/BackupsPage.tsx (-> JobsTab) - components/ObservabilityPage.tsx (split into Alerts/Links/Metrics tabs) - hooks/useUsers.ts (orphaned after Users page deletion) - the corresponding page test files (Media, FileBrowser, Applications, Actions, UsersPage) that tested the deleted pages directly. The service-tab components are the live implementations; ServicePage renders them. No live code references the deleted files. Docs: append an Information Architecture section to REQUIREMENTS.md documenting the services-as-hub model (nav shape, service-page tabs, service type registry, Users->Authentik, Observability split, legacy route 404s, empty state). Add a CHANGELOG entry under [Unreleased]. 92 frontend tests pass (was 112; -20 deleted page tests); 271 backend tests pass; lint/build green. Refs openspec/changes/services-as-hub-ia/ (tasks slice 11).
180 lines
8.5 KiB
Markdown
180 lines
8.5 KiB
Markdown
# Slice 8 Review — Message compose + WidgetConfigDialog mobile forms
|
||
|
||
**Change:** `mobile-responsive-parity` · **Slice:** 8 (R4.1, R4.2, R4.4)
|
||
**Reviewer mode:** fresh adversarial · **Date:** 2026-06-26
|
||
**Verdict: commit** (no blockers; two non-blocking suggestions)
|
||
|
||
---
|
||
|
||
## Verification commands
|
||
|
||
```
|
||
cd frontend && npm run lint → 0 errors, 2 warnings (PRE-EXISTING, confirmed via git stash)
|
||
cd frontend && npm run build → ✓ built (tsc -b + vite), 1977 modules
|
||
cd frontend && npm run test → 28 files, 116 tests passed
|
||
```
|
||
|
||
The two lint warnings (`react-hooks/exhaustive-deps` on `baseRows`/`rows` useMemo,
|
||
UsersPage.impl.tsx:150/189) exist on the committed Slice 7 tree and are unrelated
|
||
to this diff.
|
||
|
||
---
|
||
|
||
## 1. Desktop non-regression — CONFIRMED for BOTH components
|
||
|
||
### UsersPage compose (`UsersPage.impl.tsx`)
|
||
|
||
- The shared `composeBody` const (IIFE, lines ~820–985) bundles exactly the same
|
||
children the desktop `DialogContent` rendered before: `Progress` (when pending)
|
||
followed by `<div className="flex flex-1 flex-col gap-4 overflow-y-auto px-4 py-4">`
|
||
containing the error/success alerts, queue banner, recipient badges, subject input,
|
||
formatting toolbar, body textarea, preview iframe, and attachment UI.
|
||
- Desktop branch (lines ~1029–1072) renders `<DialogHeader>` → `{composeBody}` →
|
||
`<DialogFooter>` with the same Cancel / `Send message` buttons, same `disabled`
|
||
condition (`sendUserMessage.isPending || !selectedDeliverableRows.length || !subject.trim()`),
|
||
and same `onClick={handleSend}`. Token-identical to the pre-slice output.
|
||
- **768–900px range preserved:** the desktop branch still applies
|
||
`isComposeMobile` (`useComposeViewport("(max-width: 900px)")`, line 139) as the
|
||
fullscreen className on `DialogContent`. In that band `isMobile` (768px) is false
|
||
and `isComposeMobile` (900px) is true → Dialog renders fullscreen. Unchanged.
|
||
|
||
### WidgetConfigDialog (`WidgetConfigDialog.tsx`)
|
||
|
||
- `draftBody` (lines ~284–470) is the shared const covering both the draft branch
|
||
and the list branch. Desktop path renders `<DialogHeader><DialogTitle>{dialogTitle}</DialogTitle></DialogHeader>`
|
||
then `{draftBody}`.
|
||
- `dialogTitle` (line ~459) reproduces the exact original ternary:
|
||
`draft ? (draft.id ? "Edit widget" : "Add widget") : "Dashboard widgets"`.
|
||
- The inline Back/Save buttons in the draft branch are gated by `{!isMobile ? (...) : null}`
|
||
(line ~332). On desktop `isMobile=false` → they render identically to before.
|
||
List branch content (sorted instance rows, reorder/enable/edit/delete actions,
|
||
Add-widget buttons, help text) is byte-for-byte the same JSX, only re-indented.
|
||
- Confirmed token-identical desktop output.
|
||
|
||
---
|
||
|
||
## 2. Compose SheetForm wiring — CONFIRMED
|
||
|
||
`UsersPage.impl.tsx` mobile branch (lines ~1006–1027):
|
||
|
||
- `title="Message selected users"` ✓
|
||
- `onSave={handleSend}` ✓ — `handleSend` (line 365) does `await mutateAsync` then
|
||
`setComposeOpen(false)` + clears state → **R4.5 close-on-success satisfied**
|
||
- `onCancel={closeCompose}` ✓ — `closeCompose` (line 317) closes + `sendUserMessage.reset()`
|
||
- `isPending={sendUserMessage.isPending}` ✓
|
||
- `saveDisabled={!selectedDeliverableRows.length || !subject.trim()}` ✓ (mirrors desktop)
|
||
- `saveLabel="Send message"` ✓
|
||
- Attachment UI (Paperclip + remove badges) is inside `composeBody`, preserved ✓
|
||
|
||
---
|
||
|
||
## 3. WidgetConfigDialog two-mode SheetForm — CONFIRMED
|
||
|
||
Mobile branch (lines ~471–488):
|
||
|
||
- `title={dialogTitle}` → "Dashboard widgets" (list) / "Add widget" | "Edit widget" (draft) ✓
|
||
- `onSave={draft ? saveDraft : () => handleClose(false)}` — list "Done" closes, draft saves ✓
|
||
- `onCancel={draft ? reset : () => handleClose(false)}` — **draft Cancel = reset (back to list, NOT close)**, list Cancel closes ✓
|
||
- `saveLabel={draft ? "Save widget" : "Done"}` ✓
|
||
- `isPending={draft ? saveWidget.isPending : false}` ✓
|
||
- `onOpenChange={(next) => { if (!next) handleClose(next); }}` ✓
|
||
- List↔draft↔save flow intact: `startAddBuiltIn`/`startAddService`/`startEdit` set
|
||
`draft` → footer/title reactive-swap to draft mode; `saveDraft` mutates then
|
||
`reset()` returns to list (sheet stays open); `reset` returns to list without closing ✓
|
||
- The "both close in list mode" redundancy (Done + Cancel both call `handleClose(false)`)
|
||
is functional and matches the documented intent ✓
|
||
|
||
---
|
||
|
||
## 4. Rules of Hooks — CONFIRMED clean
|
||
|
||
**WidgetConfigDialog:** all hooks (`useWidgetInstances`, `useServiceInstances`,
|
||
`useTasks`, `useSaveWidgetInstance`, `useDeleteWidgetInstance`, `useState`,
|
||
`useMemo`, `useIsMobile`) are called unconditionally at the top of the component
|
||
before the `if (isMobile) return <SheetForm>…` early return. `useIsMobile()` is
|
||
placed after `draftBinding` (a plain derived value, not a hook) — no ordering
|
||
violation. ESLint `react-hooks/rules-of-hooks` produced **0 errors**.
|
||
|
||
**UsersPage:** the compose branch uses an IIFE `{(() => { … })()}` that declares
|
||
`composeBody` as a JSX const (no hooks, no state) and returns either `<SheetForm>`
|
||
or `<Dialog>`. No hooks are called inside the IIFE; no state is introduced or leaked.
|
||
Clean.
|
||
|
||
---
|
||
|
||
## 5. IIFE pattern — CONFIRMED correct
|
||
|
||
The IIFE only constructs a local `composeBody` JSX expression and branches on the
|
||
already-computed `isMobile` boolean. It introduces no closures over hooks, performs
|
||
no side effects, and returns a single root element. It does not leak state. The only
|
||
cost is readability (a moderately large nested expression), which is acceptable.
|
||
|
||
---
|
||
|
||
## 6. Test quality — ADEQUATE (one suggestion)
|
||
|
||
- **UsersPage compose mobile test** (UsersPage.test.tsx:345–376): real behavioral
|
||
assertion — selects a deliverable user via the mobile card checkbox, opens compose,
|
||
and asserts the SheetForm title ("Message selected users"), the "Send message"
|
||
footer button, and the Subject input render. Not a pure smoke test.
|
||
- **WidgetConfigDialog tests** (new file, 2 cases): desktop asserts the Dialog
|
||
heading "Dashboard widgets"; mobile asserts the SheetForm title + "Done" footer
|
||
button. These are **smoke-level only** — they do not exercise the draft-mode
|
||
footer ("Save widget"), the `reset`-back-to-list Cancel behavior, or the
|
||
list→draft→save round trip. See Suggestion S1.
|
||
|
||
All 3 new tests pass; AC8 (Vitest case per touched component at <768px and ≥768px)
|
||
is satisfied.
|
||
|
||
---
|
||
|
||
## 7. Diff size — CONFIRMED mostly re-indentation
|
||
|
||
`git diff --stat`: 472 insertions / 391 deletions across 3 files (~863 changed lines).
|
||
The actual behavioral delta is small and bounded:
|
||
|
||
- compose mobile `<SheetForm>` branch + `composeBody` extraction guard: ~25 lines
|
||
- WidgetConfigDialog mobile `<SheetForm>` branch + `draftBody` extraction + `!isMobile`
|
||
button guard + `dialogTitle`/`useIsMobile` lines: ~30 lines
|
||
- New + updated tests: ~65 lines
|
||
|
||
The remaining ~740 lines are extraction/re-indentation of unchanged JSX into the
|
||
shared consts, consistent with the task brief. No scope creep: no backend, no other
|
||
pages, no new dependencies.
|
||
|
||
---
|
||
|
||
## Suggestions (non-blocking)
|
||
|
||
**S1 — WidgetConfigDialog mobile draft-mode test.** Add one mobile test that opens
|
||
the dialog, taps an "Add widget" button, and asserts the footer swaps to "Save widget"
|
||
and that Cancel returns to the list view (title reverts to "Dashboard widgets")
|
||
without closing the sheet. This would cover the most error-prone part of the two-mode
|
||
wiring and is currently untested.
|
||
|
||
**S2 — R4.5 dirty-state outside-click confirm (cross-slice, not slice-8).**
|
||
`SheetForm` does not implement the spec'd "do not close on outside-click while the
|
||
form is dirty" guard; Radix `Sheet` dismisses the overlay by default, calling
|
||
`onOpenChange(false)`. This is a property of the shared primitive landed in Slice 1
|
||
and inherited by Slices 6, 7, and 8 — not a regression introduced here. Flagging as
|
||
a residual risk to be addressed when the SheetForm primitive is revisited (or accept
|
||
the deviation explicitly in the verify report).
|
||
|
||
---
|
||
|
||
## Residual risks / repo hygiene
|
||
|
||
- `swap-pane` and `.pi-tmp/` are untracked and unrelated to this slice; ensure only
|
||
the three intended files (`WidgetConfigDialog.tsx`, `UsersPage.impl.tsx`,
|
||
`UsersPage.test.tsx`) plus the new `components/__tests__/WidgetConfigDialog.test.tsx`
|
||
are staged for the Slice 8 commit.
|
||
- No staged files currently (`git diff --cached` empty). Good.
|
||
|
||
---
|
||
|
||
## Conclusion
|
||
|
||
Desktop output is token-identical for both components; mobile SheetForm wiring is
|
||
correct for compose (single mode) and WidgetConfigDialog (two-mode list/draft);
|
||
Rules of Hooks and the IIFE are clean; lint/build/test are green. **Verdict: commit.**
|