Files
manage/.pi-tmp/slice8-worker-out.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

5.8 KiB
Raw Blame History

Slice 8 — Message compose + WidgetConfigDialog mobile forms (worker output)

Files changed

File Status Lines
frontend/src/pages/UsersPage.impl.tsx modified +245 / -200 (extraction + re-indent)
frontend/src/components/WidgetConfigDialog.tsx modified +215 / -178 (extraction + re-indent)
frontend/src/pages/__tests__/UsersPage.test.tsx modified +33 / -0
frontend/src/components/__tests__/WidgetConfigDialog.test.tsx new 57 lines

Diff stat total: 484 insertions, 387 deletions across 3 tracked + 1 new file. The diff is large because the compose body and WidgetConfigDialog body were extracted into shared const variables (so both SheetForm and Dialog can consume them). The actual behavioral delta is ~80 lines of new code (SheetForm branches + dynamic props); the rest is structural re-indentation of existing, token-identical content.

Over the 400-line budget. The overrun is inherent to the extraction pattern: sharing a form body between Dialog and SheetForm requires lifting it into a const, which inflates the diff with movement. Both changes were prioritized per the task instruction ("prioritize the compose dialog… keep WidgetConfigDialog changes minimal but correct").

What was implemented

8.1 — Message compose SheetForm (UsersPage.impl.tsx)

Below md (isMobile === true), the compose dialog renders inside a <SheetForm> instead of a <Dialog>:

  • Body extracted into a composeBody const (Progress bar, error/success alerts, queue banner, selected-users info, subject input, formatting toolbar, HTML textarea, email preview iframe, attachments). Same content renders inside both SheetForm (mobile) and Dialog (desktop).
  • SheetForm wiring: title="Message selected users", onSave=handleSend, onCancel=closeCompose, isPending=sendUserMessage.isPending, saveDisabled=!selectedDeliverableRows.length || !subject.trim(), saveLabel="Send message".
  • Send semantics preserved: handleSend already calls setComposeOpen(false) on success (R4.5 satisfied).
  • Attachment UI preserved inside the SheetForm body (iOS Safari upload deferred to Slice 10 manual pass per the task note).
  • Desktop (md+): the Dialog renders with the exact same composeBody + DialogHeader + DialogFooter. isComposeMobile (900px) fullscreen styling still applies for 768900px.

8.2 — WidgetConfigDialog SheetForm

Below md, the widget config dialog renders inside a <SheetForm> with dynamic props based on the two-mode flow:

  • List mode (no draft): title="Dashboard widgets", onSave=()=>handleClose(false) (closes dialog), onCancel=()=>handleClose(false), saveLabel="Done". Both footer buttons close the dialog.
  • Draft mode (add/edit): title="Edit widget" / "Add widget", onSave=saveDraft, onCancel=reset (back to list, NOT close), saveLabel="Save widget", isPending=saveWidget.isPending.
  • Draft inline Back/Save hidden on mobile ({!isMobile ? <Back/Save> : null}) since the SheetForm footer provides Cancel=reset + Save=saveDraft.
  • Body extracted into a draftBody const shared between both branches. List view (reorder/toggle/edit/delete + add-widget buttons) and draft view (Title/SortOrder/Enabled/config editor) are unchanged.
  • Desktop (md+): the Dialog renders with the same draftBody. The draft's inline Back/Save buttons are present (isMobile=false).

8.3 — Tests

UsersPage.test.tsx: +1 test in the slice-5 mobile describe block:

  • "renders compose in a SheetForm below md with send button" — selects a user, opens compose, asserts title + Send button + Subject input are present.

WidgetConfigDialog.test.tsx (new): 2 tests:

  • Desktop: renders Dialog with "Dashboard widgets" heading.
  • Mobile: renders SheetForm with "Dashboard widgets" title + "Done" button.

Validation

npm run lint   → 0 errors, 2 pre-existing warnings (UsersPage.impl.tsx exhaustive-deps, pre-existing)
npm run build  → ✓ built (tsc -b + vite)
npm run test   → 28 files / 116 tests passed (was 113; +3 new)

Deviations from design

  1. IIFE pattern for compose branch. The compose dialog sits inside the component's main return. Extracting the body and branching required either an IIFE ({(() => { ... })()}) or a separate helper component. Used the IIFE to keep the compose logic inline with the component's state/handlers (it references 15+ local variables: subject, htmlBody, attachments, sendUserMessage, etc.). A helper component would need all of these as props, which is worse.

  2. WidgetConfigDialog dynamic SheetForm props. The design said "reorder list and per-widget config render inside SheetForm." The two-mode flow (list → draft) doesn't map to SheetForm's single onSave/onCancel cleanly. Solved with conditional props: list mode = Done/close, draft mode = Save-widget/back-to-list. The "Done" button in list mode is slightly redundant with Cancel (both close), but it's functional and the footer is always present.

  3. Over 400-line budget. The extraction pattern inflates the diff. Both changes were completed; the alternative (CSS-only hidden md:block on two separate copies of the form body) would duplicate ~200 lines of form JSX.

skill_resolution

none — no project/user SKILL.md paths were injected, and no .atl/skill-registry.md was found.

Residual risks

  • R4.5 dirty-state outside-click confirm still not implemented at the SheetForm level. Same deferred concern as Slices 67. Flag for verify pass.
  • WidgetConfigDialog "Done" + Cancel redundancy. In list mode, both footer buttons close the dialog. A single "Done" button would be cleaner but would require a SheetForm API change (hide Cancel). Non-blocking.
  • Diff over budget. Flagging for parent decision: accept the extraction overhead, or request the IIFE pattern be replaced with CSS-only branching (which would duplicate form JSX).