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).
74 lines
5.8 KiB
Markdown
74 lines
5.8 KiB
Markdown
# 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 768–900px.
|
||
|
||
### 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 6–7. 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).
|