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 <noreply@anthropic.com>
This commit is contained in:
chihlasm
2026-02-19 03:15:00 -05:00
parent 96b8818b36
commit dff53e55bb
5 changed files with 174 additions and 42 deletions

View File

@@ -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 `<DndContext>` + `<SortableContext>`
@@ -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

View File

@@ -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 (
<div className="border-b border-border">
{/* Collapsed header — always visible */}
<button
type="button"
onClick={() => setExpanded(!expanded)}
onClick={handleToggle}
aria-expanded={isExpanded}
aria-controls={sectionId}
className="flex w-full items-center gap-3 px-4 py-2.5 text-left transition-colors hover:bg-accent/50"
>
<ChevronRight
className={cn(
'h-4 w-4 shrink-0 text-muted-foreground transition-transform duration-200',
expanded && 'rotate-90'
isExpanded && 'rotate-90'
)}
/>
<span className="shrink-0 text-muted-foreground">{icon}</span>
<span className="text-sm font-medium text-foreground">{title}</span>
{!expanded && (
{!isExpanded && (
<span className="min-w-0 truncate text-sm text-muted-foreground">
{summary}
</span>
@@ -68,8 +84,8 @@ export function CollapsibleEditorSection({
</button>
{/* Expanded content */}
{expanded && (
<div className="px-4 pb-4 pt-1">
{isExpanded && (
<div id={sectionId} className="px-4 pb-4 pt-1">
{children}
</div>
)}
@@ -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`