Files
manage/.pi-tmp/slice8-review.md
T
Developer a8dfbd5dc6 Cleanup: delete dead top-level pages + update docs (Slice 11)
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).
2026-06-26 20:11:02 +00:00

8.5 KiB
Raw Blame History

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 ~820985) 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 ~10291072) 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.
  • 768900px 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 ~284470) 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 ~10061027):

  • 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 ~471488):

  • 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:345376): 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.