SheetForm dirty-state confirm + wire isDirty into all form consumers (R4.5)

SheetForm gains an isDirty prop. When true, any close attempt (Cancel
button, header X, Radix overlay click, Escape) opens a 'Discard changes?'
ConfirmDialog instead of discarding unsaved edits. Radix dismiss callbacks
(onEscapeKeyDown, onPointerDownOutside) are intercepted when dirty so the
guard applies uniformly.

All four form consumers now compute and pass isDirty:
- ServicePage: name/enabled/config differ from the persisted instance.
- Settings machine editor: field-by-field draft vs editingMachine
  (create mode is always dirty; secret write-only fields excluded).
- Message compose: subject non-empty, body differs from default, or
  attachments present.
- WidgetConfigDialog: draft !== null (only draft mode is guarded; list
  mode has nothing to discard).

Tests: 3 new SheetForm dirty-guard cases (prompt on cancel, abort discard,
clean close when not dirty) + one focused dirty-guard test per consumer.
122 tests pass; lint/build green.

Refs openspec/changes/mobile-responsive-parity/verify-report.md residual
risk #1.
This commit is contained in:
Developer
2026-06-26 15:31:30 +00:00
parent 32516f6e3b
commit 09b9c45665
9 changed files with 248 additions and 13 deletions
@@ -315,7 +315,7 @@ export function WidgetConfigDialog({ open, onClose }: Props) {
<div className="flex items-center gap-2">
<Switch
id="widget-enabled"
className="mobile-touch-target"
className="mobile-touch-target"
checked={draft.enabled}
onCheckedChange={(checked) =>
setDraft({ ...draft, enabled: checked })
@@ -394,8 +394,8 @@ export function WidgetConfigDialog({ open, onClose }: Props) {
<ChevronDown className="h-4 w-4" />
</Button>
<Switch
className="mobile-touch-target"
checked={instance.enabled}
className="mobile-touch-target"
checked={instance.enabled}
onCheckedChange={() => toggleEnabled(instance)}
aria-label={`Toggle ${instance.title}`}
/>
@@ -479,6 +479,7 @@ export function WidgetConfigDialog({ open, onClose }: Props) {
onCancel={draft ? reset : () => handleClose(false)}
saveLabel={draft ? "Save widget" : "Done"}
isPending={draft ? saveWidget.isPending : false}
isDirty={draft !== null}
>
<div className="flex flex-col gap-4">{draftBody}</div>
</SheetForm>