From dff53e55bb531898f5b1d241d6a67fb99357ce47 Mon Sep 17 00:00:00 2001 From: chihlasm Date: Thu, 19 Feb 2026 03:15:00 -0500 Subject: [PATCH] feat: restructure procedural editor with collapsible sections and fixed-height layout Convert scrolling document layout to fixed-height editor with accordion-mode collapsible sections for Details and Intake Form. Step list now gets all remaining height with independent scrolling. Add CollapsibleEditorSection component with ARIA attributes (aria-expanded, aria-controls). Co-Authored-By: Claude Opus 4.6 --- ...02-19-procedural-editor-redesign-design.md | 31 +++++--- ...6-02-19-procedural-editor-redesign-impl.md | 34 ++++++--- .../CollapsibleEditorSection.tsx | 68 ++++++++++++++++++ .../procedural-editor/IntakeFormBuilder.tsx | 11 +-- frontend/src/pages/ProceduralEditorPage.tsx | 72 ++++++++++++++----- 5 files changed, 174 insertions(+), 42 deletions(-) create mode 100644 frontend/src/components/procedural-editor/CollapsibleEditorSection.tsx diff --git a/docs/plans/2026-02-19-procedural-editor-redesign-design.md b/docs/plans/2026-02-19-procedural-editor-redesign-design.md index 30555b4b..2aa83ae1 100644 --- a/docs/plans/2026-02-19-procedural-editor-redesign-design.md +++ b/docs/plans/2026-02-19-procedural-editor-redesign-design.md @@ -81,6 +81,14 @@ A reusable component used by Details, Intake Form, and Maintenance Schedule. **Expanded state:** Full content slides down with a subtle animation. Collapse chevron rotates. +**Accordion mode:** Single-open by default — expanding one section collapses others. Controlled by parent page component. + +**Accessibility:** +- Toggle button has `aria-expanded` and `aria-controls` pointing to section content `id` +- Content region has matching `id` +- Keyboard operable (Enter/Space to toggle) +- Focus remains stable after toggle + ### Details Section — Collapsed Summary Format: `"Flow Name" · N tags · Public/Private` @@ -107,7 +115,9 @@ Only renders when `treeType === 'maintenance'`. **New flow (no schedule):** Section starts expanded with: - Cron expression builder (frequency picker: daily/weekly/monthly + time + timezone) - Target list selector (dropdown of saved target lists, or create new inline) -- These fields write to the store and get saved with the flow +- These fields write to local UI draft state (NOT tree_structure) +- On save: tree saved first, then schedule created via `maintenanceSchedulesApi.create` with resulting `tree_id` +- If schedule create fails: tree save remains successful, show actionable error, preserve draft **Existing flow (has schedule):** Collapsed summary: - Format: `"Every Monday at 2:00 AM UTC · 5 targets"` @@ -142,7 +152,7 @@ The Steps section header shows aggregate info: - `Steps (4 steps · ~25 min estimated)` — when steps have time estimates - `Steps (4 steps)` — when no time estimates set -- `Steps (0 steps)` — empty, triggers the empty state below +- `Steps (0 steps)` — empty state (note: store currently enforces minimum one `procedure_step`, so 0-step state only appears if invariant is intentionally changed) ### Auto-Expand New Steps @@ -159,8 +169,8 @@ Same for `addSectionHeader()` — auto-expand for immediate title editing. - Dragged card lifts with `shadow-lg` and slight scale - Drop target: blue insertion line between steps - Section headers are draggable — moving a section header moves it independently (steps below stay in place) -- On drop: update the store's step array order and recalculate `display_order` values -- Keyboard accessible: focus grip handle, Enter to pick up, arrow keys to move, Enter to drop +- On drop: update the store's step array order (array-index based only, no `display_order` recalculation) +- Keyboard accessible: focus grip handle, Enter/Space to pick up, arrow keys to move, Enter to drop, Escape to cancel **Implementation:** - Wrap step list in `` + `` @@ -208,11 +218,16 @@ Maintenance flows show `Wrench` icon + "Edit Maintenance Flow" title. | `components/procedural-editor/CollapsibleEditorSection.tsx` | Shared collapsible wrapper with summary display | | `components/procedural-editor/MaintenanceScheduleSection.tsx` | Schedule builder + collapsed summary for maintenance flows | -### New Dependencies +### Existing Dependencies Used -- `@dnd-kit/core` — drag-and-drop framework -- `@dnd-kit/sortable` — sortable preset for ordered lists -- `@dnd-kit/utilities` — CSS utilities for transforms +- `@dnd-kit/core` — drag-and-drop framework (already installed) +- `@dnd-kit/sortable` — sortable preset for ordered lists (already installed) +- `@dnd-kit/utilities` — CSS utilities for transforms (already installed) + +### Existing APIs Used + +- `frontend/src/api/maintenanceSchedules.ts` — schedule CRUD via separate endpoints (NOT tree_structure) +- `frontend/src/api/targetLists.ts` — target list selection for schedules ### Unchanged diff --git a/docs/plans/2026-02-19-procedural-editor-redesign-impl.md b/docs/plans/2026-02-19-procedural-editor-redesign-impl.md index a4f794cf..1c6002bf 100644 --- a/docs/plans/2026-02-19-procedural-editor-redesign-impl.md +++ b/docs/plans/2026-02-19-procedural-editor-redesign-impl.md @@ -31,6 +31,8 @@ interface CollapsibleEditorSectionProps { title: string icon: ReactNode summary: string + expanded?: boolean + onToggle?: () => void defaultExpanded?: boolean children: ReactNode } @@ -39,28 +41,42 @@ export function CollapsibleEditorSection({ title, icon, summary, + expanded: controlledExpanded, + onToggle, defaultExpanded = false, children, }: CollapsibleEditorSectionProps) { - const [expanded, setExpanded] = useState(defaultExpanded) + const [internalExpanded, setInternalExpanded] = useState(defaultExpanded) + const isExpanded = controlledExpanded ?? internalExpanded + const sectionId = `section-${title.toLowerCase().replace(/\s+/g, '-')}` + + const handleToggle = () => { + if (onToggle) { + onToggle() + } else { + setInternalExpanded(!internalExpanded) + } + } return (
{/* Collapsed header — always visible */} {/* Expanded content */} - {expanded && ( -
+ {isExpanded && ( +
{children}
)} @@ -165,7 +181,7 @@ git commit -m "feat: restructure procedural editor to fixed-height layout with c **Files:** - Modify: `frontend/src/components/procedural-editor/StepList.tsx` -**Context:** The step list needs: (1) a step count + total estimated time in the header, (2) an empty state when no steps exist. The step list's outer card wrapper should be removed since ProceduralEditorPage now provides the scrolling container — StepList should just render its header and step items directly. +**Context:** The step list needs: (1) a step count + total estimated time in the header, (2) auto-scroll to new steps. Note: the store enforces minimum one `procedure_step`, so remove any "0 steps" UI paths. The step list's outer card wrapper should be removed since ProceduralEditorPage now provides the scrolling container — StepList should just render its header and step items directly. **Step 1: Read the current file** @@ -259,7 +275,7 @@ git commit -m "feat: add empty state, step count header, and auto-scroll to step **Files:** - Modify: `frontend/src/components/procedural-editor/StepList.tsx` -**Context:** @dnd-kit is already installed (`@dnd-kit/core@^6.3.1`, `@dnd-kit/sortable@^10.0.0`). There's an existing pattern in `frontend/src/components/step-library/CategoryRow.tsx` using `useSortable` and in `frontend/src/pages/admin/AdminCategoriesPage.tsx` using `DndContext` + `SortableContext`. The store already has `moveStep(fromIndex, toIndex)`. +**Context:** @dnd-kit is already installed (`@dnd-kit/core@^6.3.1`, `@dnd-kit/sortable@^10.0.0`). There's an existing pattern in `frontend/src/components/step-library/CategoryRow.tsx` using `useSortable` and in `frontend/src/pages/admin/AdminCategoriesPage.tsx` using `DndContext` + `SortableContext`. The store already has `moveStep(fromIndex, toIndex)`. Reorder is array-index based only — do NOT recalculate `display_order`. `procedure_end` must remain non-draggable and always last. Drag handles must have accessible labels and keyboard support (Enter/Space to pick up, arrow keys to move, Escape to cancel). **Step 1: Read the existing @dnd-kit pattern** @@ -390,7 +406,7 @@ git commit -m "feat: add drag-to-reorder steps with @dnd-kit" **Files:** - Create: `frontend/src/components/procedural-editor/MaintenanceScheduleSection.tsx` -**Context:** This component renders only for maintenance flows. It shows a schedule builder for new flows (or flows without a schedule) and a collapsed summary for existing scheduled flows. It uses the existing `maintenanceSchedulesApi` from `frontend/src/api/maintenanceSchedules.ts` and `targetListsApi` from `frontend/src/api/targetLists.ts`. Types are in `frontend/src/types/maintenance.ts`. +**Context:** This component renders only for maintenance flows. Schedule is NOT part of `tree_structure` — it's persisted through separate maintenance schedule API endpoints. Two-stage save: tree first, then schedule. If schedule save fails, tree save remains successful — show actionable error and preserve schedule draft state. It uses the existing `maintenanceSchedulesApi` from `frontend/src/api/maintenanceSchedules.ts` and `targetListsApi` from `frontend/src/api/targetLists.ts`. Types are in `frontend/src/types/maintenance.ts`. Schedule draft state should be local UI state, not stored in proceduralEditorStore. Read these files first: - `frontend/src/api/maintenanceSchedules.ts` diff --git a/frontend/src/components/procedural-editor/CollapsibleEditorSection.tsx b/frontend/src/components/procedural-editor/CollapsibleEditorSection.tsx new file mode 100644 index 00000000..2325e4ff --- /dev/null +++ b/frontend/src/components/procedural-editor/CollapsibleEditorSection.tsx @@ -0,0 +1,68 @@ +import { useState, useId, type ReactNode } from 'react' +import { ChevronRight } from 'lucide-react' +import { cn } from '@/lib/utils' + +interface CollapsibleEditorSectionProps { + title: string + icon: ReactNode + summary: string + expanded?: boolean + onToggle?: () => void + defaultExpanded?: boolean + children: ReactNode +} + +export function CollapsibleEditorSection({ + title, + icon, + summary, + expanded: controlledExpanded, + onToggle, + defaultExpanded = false, + children, +}: CollapsibleEditorSectionProps) { + const [internalExpanded, setInternalExpanded] = useState(defaultExpanded) + const isExpanded = controlledExpanded ?? internalExpanded + const generatedId = useId() + const sectionId = `section-${generatedId}` + + const handleToggle = () => { + if (onToggle) { + onToggle() + } else { + setInternalExpanded(!internalExpanded) + } + } + + return ( +
+ + + {isExpanded && ( +
+ {children} +
+ )} +
+ ) +} diff --git a/frontend/src/components/procedural-editor/IntakeFormBuilder.tsx b/frontend/src/components/procedural-editor/IntakeFormBuilder.tsx index eb3969fa..bc28e34b 100644 --- a/frontend/src/components/procedural-editor/IntakeFormBuilder.tsx +++ b/frontend/src/components/procedural-editor/IntakeFormBuilder.tsx @@ -6,15 +6,8 @@ export function IntakeFormBuilder() { const { intakeForm, addField, removeField, updateField } = useProceduralEditorStore() return ( -
-
-
- -

Intake Form

- - ({intakeForm.length} field{intakeForm.length !== 1 ? 's' : ''}) - -
+
+
@@ -144,7 +171,7 @@ export function ProceduralEditorPage() {
- {/* Content */} -
- {/* Metadata */} -
-

Details

+ {/* Collapsible sections */} +
+ } + summary={detailsSummary} + expanded={expandedSection === 'details'} + onToggle={() => toggleSection('details')} + >
@@ -199,12 +230,21 @@ export function ProceduralEditorPage() {
-
+ - {/* Intake Form Builder */} - + } + summary={intakeSummary} + expanded={expandedSection === 'intake'} + onToggle={() => toggleSection('intake')} + > + + +
- {/* Step List */} + {/* Step List — flex-1, scrolls independently */} +