From eefbab734d8ba70e6227b30637354b4df66fdf43 Mon Sep 17 00:00:00 2001 From: rafavalls Date: Fri, 8 May 2026 12:43:59 -0300 Subject: [PATCH 1/3] overhaul variant UI: editable card, picker dialog, name editing Reworks the variants experience end-to-end so the page-level and section-level variant flows share the same components, sizing, and behaviour. - Replace the custom matcher-picker modal with a CommandDialog list - Convert the variant rule area into a collapsible bordered card (state persisted in localStorage) with the Rule picker and schema fields aligned at the same px-5 inset - Make the variant title in the breadcrumb inline-editable via a pencil affordance; persist as a top-level "name" on the raw multivariate variant (exposed through CmsVariant / PageVariantEntry) - Rebuild DateField as a styled overlay over a real datetime-local input so users can both type and pick; restore the input border beneath the overlay - Unify both variant lists on a single SortableVariantItem (flag icon only, no colored chip) with drag-reorder for page variants too - Allow adding the first page variant from a flat-sections page - Add a delete button inside the page-variant detail breadcrumb - Drop all-caps title styling; bring section labels to text-sm font-semibold to match the FieldLabel scale Co-Authored-By: Claude Opus 4.7 (1M context) --- api/tools/files.ts | 16 +- web/tools/file-explorer/cms-form.tsx | 127 ++- web/tools/file-explorer/index.tsx | 1083 +++++++++++++++++--------- 3 files changed, 842 insertions(+), 384 deletions(-) diff --git a/api/tools/files.ts b/api/tools/files.ts index dd99730..9149e22 100644 --- a/api/tools/files.ts +++ b/api/tools/files.ts @@ -757,6 +757,7 @@ export type CmsVariant = { value: Record; rule: Record; label: string; + name?: string; }; export type CmsSection = { @@ -797,6 +798,7 @@ const cmsSectionItemSchema = z value: z.record(z.string(), z.unknown()), rule: z.record(z.string(), z.unknown()), label: z.string(), + name: z.string().optional(), }), ) .optional(), @@ -816,6 +818,7 @@ export const getPageSectionsOutputSchema = z.object({ label: z.string(), rule: z.record(z.string(), z.unknown()), sections: z.array(cmsSectionItemSchema), + name: z.string().optional(), }), ) .optional(), @@ -1099,6 +1102,7 @@ export const getPageSectionsTool = createTool({ variants?: Array<{ value?: Record; rule?: Record; + name?: string; }>; }; const rawVariants = Array.isArray(mvObj.variants) @@ -1143,6 +1147,9 @@ export const getPageSectionsTool = createTool({ value, rule, label: formatMatcher(rule), + ...(typeof v.name === "string" && v.name + ? { name: v.name } + : {}), }; }); const firstValueRt = ( @@ -1182,6 +1189,7 @@ export const getPageSectionsTool = createTool({ label: string; rule: Record; sections: CmsSection[]; + name?: string; }; let pageVariants: PageVariantEntry[] | undefined; @@ -1200,6 +1208,7 @@ export const getPageSectionsTool = createTool({ variants?: Array<{ value?: unknown[]; rule?: Record; + name?: string; }>; }; const mvVariants = Array.isArray(mvField.variants) @@ -1210,7 +1219,12 @@ export const getPageSectionsTool = createTool({ const rule = (v.rule ?? {}) as Record; const label = formatMatcher(rule); const { sections } = parseSectionsFromArray(varSections); - return { label, rule, sections }; + return { + label, + rule, + sections, + ...(typeof v.name === "string" && v.name ? { name: v.name } : {}), + }; }); // Default display: first variant's sections rawSections = Array.isArray(mvVariants[0]?.value) diff --git a/web/tools/file-explorer/cms-form.tsx b/web/tools/file-explorer/cms-form.tsx index be26dcd..11177bd 100644 --- a/web/tools/file-explorer/cms-form.tsx +++ b/web/tools/file-explorer/cms-form.tsx @@ -29,6 +29,7 @@ import { Trash2, Upload, VideoIcon, + X, } from "lucide-react"; import { createContext, @@ -306,6 +307,20 @@ function formatForNativeInput( return `${y}-${m}-${d}T${hh}:${mm}`; } +function formatDateDisplay( + dateStr: string | undefined, + mode: "date" | "date-time", +): string { + if (!dateStr) return ""; + const date = new Date(dateStr); + if (Number.isNaN(date.getTime())) return ""; + return new Intl.DateTimeFormat("en-US", + mode === "date" + ? { month: "short", day: "numeric", year: "numeric" } + : { month: "short", day: "numeric", year: "numeric", hour: "numeric", minute: "2-digit" }, + ).format(date); +} + function DateField({ label, description, @@ -322,10 +337,9 @@ function DateField({ const inputType = mode === "date" ? "date" : "datetime-local"; const inputRef = useRef(null); const [local, setLocal] = useState(() => formatForNativeInput(value, mode)); + const [focused, setFocused] = useState(false); - // Sync external value changes, but only if the formatted value actually - // differs from what we already have — avoids re-renders that kill the - // native date picker popup. + // Sync external value changes without clobbering an open picker. const prevFormattedRef = useRef(local); useEffect(() => { const formatted = formatForNativeInput(value, mode); @@ -353,47 +367,104 @@ function DateField({ } }; + const handleBlur = () => { + setFocused(false); + // If user typed an unparseable value, snap back to the source-of-truth value. + const formatted = formatForNativeInput(value, mode); + if (formatted !== local) { + setLocal(formatted); + prevFormattedRef.current = formatted; + } + }; + + const handleClear = (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + setLocal(""); + prevFormattedRef.current = ""; + onChange(""); + }; + + const openPicker = (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + const el = inputRef.current; + if (!el) return; + if (typeof el.showPicker === "function") { + try { + el.showPicker(); + return; + } catch { + // fall through to focus + } + } + el.focus(); + }; + + const display = formatDateDisplay(value, mode); + const placeholder = mode === "date" ? "Set a date…" : "Set date & time…"; + const showOverlay = !focused; + return (
-
+
setFocused(true)} + onBlur={handleBlur} + className="flex h-9 w-full rounded-md border border-input bg-background px-3 pr-9 text-sm shadow-xs outline-none transition-colors focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 [&::-webkit-calendar-picker-indicator]:hidden [&::-webkit-calendar-picker-indicator]:appearance-none" /> + {showOverlay && ( +
+ + + {display || placeholder} + +
+ )} + {value && !focused && ( + + )}
@@ -1221,7 +1292,7 @@ function ImageField({

{stem}

{ext && ( -

+

{ext}

)} @@ -1356,7 +1427,7 @@ function MediaField({ {stem}

{ext && ( -

+

{ext}

)} @@ -1370,7 +1441,7 @@ function MediaField({ {stem}

{ext && ( -

+

{ext}

)} diff --git a/web/tools/file-explorer/index.tsx b/web/tools/file-explorer/index.tsx index 4248ce3..ff69085 100644 --- a/web/tools/file-explorer/index.tsx +++ b/web/tools/file-explorer/index.tsx @@ -50,6 +50,7 @@ import { MousePointer2, Package, PanelLeft, + Pencil, Plus, RefreshCw, Save, @@ -103,6 +104,14 @@ import { ContextMenuItem, ContextMenuTrigger, } from "@/components/ui/context-menu.tsx"; +import { + CommandDialog, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@/components/ui/command.tsx"; import { Dialog, DialogContent, @@ -169,7 +178,7 @@ import type { } from "../../../api/tools/files.ts"; import type { GitRawOutput, GitStatus } from "../../../api/tools/git.ts"; import type { SchemaProperties } from "./cms-form.tsx"; -import { SectionForm } from "./cms-form.tsx"; +import { FieldLabel, SectionForm } from "./cms-form.tsx"; import { PublishDialog } from "./publish-dialog.tsx"; import type { CmsInspectPayload, @@ -872,21 +881,16 @@ function MatcherPicker({ onSelect: (resolveType: string) => void; }) { const [open, setOpen] = useState(false); - const [search, setSearch] = useState(""); const handleOpen = () => { setOpen(true); - setSearch(""); onFetchMatchers(); }; - const filtered = matchers?.filter( - (m) => - !search || - m.title.toLowerCase().includes(search.toLowerCase()) || - m.resolveType.toLowerCase().includes(search.toLowerCase()) || - m.description?.toLowerCase().includes(search.toLowerCase()), - ); + const handleSelect = (rt: string) => { + onSelect(rt); + setOpen(false); + }; const CurrentIcon = matcherIcon( matchers?.find((m) => m.resolveType === currentRt)?.icon, @@ -894,122 +898,76 @@ function MatcherPicker({ return ( <> - {/* Trigger */} - {/* Modal */} - {open && ( -
{ - if (e.target === e.currentTarget) setOpen(false); - }} - onKeyDown={(e) => { - if (e.key === "Escape") setOpen(false); - }} - > -
- {/* Header + search */} -
- - Segment Rule - -
- - setSearch(e.target.value)} - placeholder="Search…" - className="w-full bg-transparent text-xs outline-none placeholder:text-muted-foreground/50" - /> -
- + + + + {!matchers ? ( +
+ + Loading rules…
- - {/* Grid */} -
- {!matchers ? ( -
- - Loading matchers\u2026 -
- ) : ( -
- {/* Always card */} - - - {(filtered ?? []).map((m) => { - const Icon = matcherIcon(m.icon); - return ( - - ); - })} -
- )} - - {matchers && filtered?.length === 0 && ( -
- No matchers found -
- )} -
-
-
- )} +
+ + ); + })} + + + )} + + ); } @@ -1193,7 +1151,7 @@ function SortableVariantItem({ }} className="group flex cursor-pointer select-none items-center gap-2 rounded-md px-2 py-2 text-[oklch(0.45_0.15_160)] hover:bg-[oklch(0.65_0.15_160/0.12)] dark:text-[oklch(0.78_0.15_160)] dark:hover:bg-[oklch(0.65_0.15_160/0.15)]" > - +
{valueLabel} @@ -1310,7 +1268,7 @@ function AppsPanel({ .sort(([a], [b]) => a.localeCompare(b)) .map(([category, apps]) => (
- + {category}
@@ -1411,6 +1369,82 @@ interface BreadcrumbCrumb { label: string; onClick?: () => void; color?: string; + editable?: boolean; + editValue?: string; + placeholder?: string; + onLabelChange?: (v: string) => void; +} + +function EditableBreadcrumbLabel({ + value, + placeholder, + color, + onChange, +}: { + value: string; + placeholder: string; + color?: string; + onChange: (v: string) => void; +}) { + const [editing, setEditing] = useState(false); + const [draft, setDraft] = useState(value); + const inputRef = useRef(null); + + useEffect(() => { + if (!editing) setDraft(value); + }, [value, editing]); + + useEffect(() => { + if (editing) { + inputRef.current?.focus(); + inputRef.current?.select(); + } + }, [editing]); + + const commit = () => { + setEditing(false); + if (draft !== value) onChange(draft); + }; + + if (editing) { + return ( + setDraft(e.target.value)} + onBlur={commit} + onKeyDown={(e) => { + if (e.key === "Enter") { + e.preventDefault(); + commit(); + } else if (e.key === "Escape") { + setDraft(value); + setEditing(false); + } + }} + className="-mx-1 min-w-0 flex-1 truncate rounded bg-transparent px-1 py-0.5 font-semibold outline-none ring-1 ring-ring/40 focus:ring-2 focus:ring-ring/40" + style={color ? { color } : undefined} + /> + ); + } + + return ( + + ); } function PanelBreadcrumb({ @@ -1435,7 +1469,14 @@ function PanelBreadcrumb({ {i > 0 && ( )} - {isLast || !c.onClick ? ( + {isLast && c.editable && c.onLabelChange ? ( + + ) : isLast || !c.onClick ? ( , ) => void; + onChangeVariantName: (name: string) => void; onReorderVariants: (srcIdx: number, destIdx: number) => void; availableMatchers: ListMatchersOutput["matchers"] | null; onFetchMatchers: () => void; @@ -1516,9 +1558,11 @@ interface CmsPanelProps { pageVariantRuleSchema?: SchemaProperties | null; onChangePageVariantRule?: (data: Record) => void; onChangePageVariantMatcherType?: (resolveType: string) => void; + onChangePageVariantName?: (name: string) => void; onAddPageVariant?: () => void; onDuplicatePageVariant?: (idx: number) => void; onRemovePageVariant?: (idx: number) => void; + onReorderPageVariants?: (srcIdx: number, destIdx: number) => void; } function CmsPanel({ @@ -1551,6 +1595,7 @@ function CmsPanel({ onSelectVariant, onDeselectVariant, onChangeVariantData, + onChangeVariantName, onReorderVariants, availableMatchers, onFetchMatchers, @@ -1567,9 +1612,11 @@ function CmsPanel({ pageVariantRuleSchema, onChangePageVariantRule, onChangePageVariantMatcherType, + onChangePageVariantName, onAddPageVariant, onDuplicatePageVariant, onRemovePageVariant, + onReorderPageVariants, }: CmsPanelProps) { const sensors = useSensors( useSensor(MouseSensor, { activationConstraint: { distance: 3 } }), @@ -1585,6 +1632,25 @@ function CmsPanel({ (data?.pageData.path as string | undefined) ?? "", ); + const [variantCardOpen, setVariantCardOpen] = useState(() => { + try { + return localStorage.getItem("variant-card-open") !== "false"; + } catch { + return true; + } + }); + const toggleVariantCard = () => { + setVariantCardOpen((prev) => { + const next = !prev; + try { + localStorage.setItem("variant-card-open", String(next)); + } catch { + // ignore + } + return next; + }); + }; + useEffect(() => { setEditName((data?.pageData.name as string | undefined) ?? ""); setEditPath((data?.pageData.path as string | undefined) ?? ""); @@ -1621,9 +1687,16 @@ function CmsPanel({ onReorderVariants(srcIdx, destIdx); }; - const [variantRuleOpen, setVariantRuleOpen] = useState(true); - const [variantSectionOpen, setVariantSectionOpen] = useState(true); - const [pageVariantRuleOpen, setPageVariantRuleOpen] = useState(true); + const pageVariantSortableIds = (pageVariants ?? []).map((_, i) => String(i)); + + const handlePageVariantDragEnd = ({ active, over }: DragEndEvent) => { + if (!over || active.id === over.id) return; + const srcIdx = Number(active.id); + const destIdx = Number(over.id); + if (!Number.isNaN(srcIdx) && !Number.isNaN(destIdx)) + onReorderPageVariants?.(srcIdx, destIdx); + }; + const editingGlobally = savedBlock === "editing"; @@ -1669,8 +1742,20 @@ function CmsPanel({ }, { label: - activeSection?.variants?.[selectedVariant ?? 0]?.label ?? + (activeSection?.variants?.[selectedVariant ?? 0] as { + name?: string; + } | undefined)?.name || + activeSection?.variants?.[selectedVariant ?? 0]?.label || `Variant ${(selectedVariant ?? 0) + 1}`, + editable: true, + editValue: + (activeSection?.variants?.[selectedVariant ?? 0] as { + name?: string; + } | undefined)?.name ?? "", + placeholder: + activeSection?.variants?.[selectedVariant ?? 0]?.label || + `Variant ${(selectedVariant ?? 0) + 1}`, + onLabelChange: onChangeVariantName, }, ]} actions={ @@ -1743,15 +1828,36 @@ function CmsPanel({ { label: editName || "Page", onClick: onDeselectPageVariant }, { label: - activePageVariant?.label ?? + (activePageVariant as { name?: string } | undefined)?.name || `Variant ${selectedPageVariant! + 1}`, color: "oklch(0.45 0.15 160)", + editable: true, + editValue: + (activePageVariant as { name?: string } | undefined)?.name ?? + "", + placeholder: `Variant ${selectedPageVariant! + 1}`, + onLabelChange: (v: string) => onChangePageVariantName?.(v), }, ]} actions={ - autoSaving && ( - - ) + <> + {(pageVariants?.length ?? 0) > 1 && ( + + )} + {autoSaving && ( + + )} + } /> ) : ( @@ -1802,25 +1908,48 @@ function CmsPanel({ No page data
) : isEditingVariant && sectionData ? ( - /* ── Variant editing: rule + section collapsibles ── */ + /* ── Variant editing: rule + section ── */
- {/* Segment Rule collapsible */} -
- - {variantRuleOpen && ( -
+ + {variantCardOpen && ( +
{(() => { const variant = activeSection?.variants?.[selectedVariant ?? 0]; @@ -1833,14 +1962,17 @@ function CmsPanel({ ?.replace(/\.(tsx|ts)$/, "") : "Always"; return ( -
- + <> +
+ + +
{variantRuleSchema ? ( onChangeVariantData("rule", d)} /> ) : ruleRt ? ( -
+
Loading schema…
) : null} -
+ ); })()}
- )} + )} +
- {/* Section props collapsible */} -
- - {variantSectionOpen && ( -
- onChangeVariantData("value", d)} - /> -
- )} + {/* Section content */} +
+ + Content + +
+
+ onChangeVariantData("value", d)} + />
) : isMultivariate && activeSection?.variants ? ( @@ -1913,7 +2034,9 @@ function CmsPanel({ onSelectVariant(vIdx)} onDuplicate={() => onDuplicateVariant(vIdx)} @@ -1956,58 +2079,34 @@ function CmsPanel({ /* ── Page-level variant list ─────────────────────────────── */
- {pageVariants?.map((pv, i) => ( -
+ - -
onSelectPageVariant?.(i)} - > - - {pv.label} - - - {pv.sections.length === 1 - ? "1 section" - : `${pv.sections.length} sections`} - -
- - - - - - onDuplicatePageVariant?.(i)} - className="cursor-pointer" - > - - Duplicate - - onRemovePageVariant?.(i)} - className="cursor-pointer text-destructive focus:text-destructive" - > - - Remove - - - -
- ))} + {pageVariants?.map((pv, i) => ( + onSelectPageVariant?.(i)} + onDuplicate={() => onDuplicatePageVariant?.(i)} + onRemove={() => onRemovePageVariant?.(i)} + /> + ))} + +
- {pageVariantRuleOpen && ( -
- {(() => { - const pvRule = activePageVariant?.rule ?? {}; - const pvRuleRt = (pvRule.__resolveType as string) ?? ""; - const pvRuleLabel = pvRuleRt - ? pvRuleRt - .split("/") - .pop() - ?.replace(/\.(tsx|ts)$/, "") - : "Always"; - return ( -
- onChangePageVariantMatcherType?.(rt)} - /> - {pageVariantRuleSchema ? ( - onChangePageVariantRule?.(d)} - /> - ) : pvRuleRt ? ( -
- Loading schema… + style={{ borderColor: "oklch(0.55 0.15 160 / 0.35)" }} + > + + {variantCardOpen && ( +
+ {(() => { + const pvRule = activePageVariant?.rule ?? {}; + const pvRuleRt = (pvRule.__resolveType as string) ?? ""; + const pvRuleLabel = pvRuleRt + ? pvRuleRt + .split("/") + .pop() + ?.replace(/\.(tsx|ts)$/, "") + : "Always"; + return ( + <> +
+ + + onChangePageVariantMatcherType?.(rt) + } + />
- ) : null} -
- ); - })()} -
- )} -
- {/* Sections list */} -
- {data.sections.length === 0 ? ( -
- No sections on this page. + {pageVariantRuleSchema ? ( + onChangePageVariantRule?.(d)} + /> + ) : pvRuleRt ? ( +
+ Loading schema… +
+ ) : null} + + ); + })()} +
+ )}
- ) : ( - - + {/* Sections title */} +
+ + Sections + +
+
+ {data.sections.length === 0 ? ( +
+ No sections on this page. +
+ ) : ( + -
- {data.sections.map((section) => ( - onSelectSection(section.index)} - onDuplicate={() => onDuplicateSection(section.index)} - onRemove={() => onRemoveSection(section.index)} - onToggleLazy={() => onToggleLazySection(section.index)} - onToggleHidden={() => - onToggleHiddenSection(section.index) - } - /> - ))} -
- -
- )} + +
+ {data.sections.map((section) => ( + onSelectSection(section.index)} + onDuplicate={() => onDuplicateSection(section.index)} + onRemove={() => onRemoveSection(section.index)} + onToggleLazy={() => onToggleLazySection(section.index)} + onToggleHidden={() => + onToggleHiddenSection(section.index) + } + /> + ))} +
+
+ + )} +
-
+
+
)} @@ -3699,6 +3844,68 @@ function FileExplorerWorkspace({ } }; + const handleChangePageVariantName = (newName: string) => { + const pvIdx = cmsSelectedPageVariantRef.current; + if (pvIdx === null || !cmsData) return; + + const mv = { + ...(cmsData.pageData.sections as Record), + }; + const variants = [...((mv.variants as unknown[]) ?? [])]; + const current = (variants[pvIdx] as Record) ?? {}; + const updated = { ...current }; + if (newName.trim()) { + updated.name = newName; + } else { + delete updated.name; + } + variants[pvIdx] = updated; + const updatedPageData = { + ...cmsData.pageData, + sections: { ...mv, variants }, + }; + + const newPageVariants = cmsData.pageVariants + ? [...cmsData.pageVariants] + : undefined; + if (newPageVariants?.[pvIdx]) { + const trimmed = newName.trim(); + newPageVariants[pvIdx] = { + ...newPageVariants[pvIdx], + ...(trimmed ? { name: trimmed } : { name: undefined }), + }; + } + + const next: GetPageSectionsOutput = { + ...cmsData, + pageData: updatedPageData, + ...(newPageVariants ? { pageVariants: newPageVariants } : {}), + }; + setCmsData(next); + cmsDataRef.current = next; + + setCmsAutoSaving(true); + if (cmsAutoSaveTimerRef.current) clearTimeout(cmsAutoSaveTimerRef.current); + cmsAutoSaveTimerRef.current = setTimeout(async () => { + try { + const result = await app?.callServerTool({ + name: "write_file", + arguments: { + env: userEnv, + filepath: next.filePath, + content: JSON.stringify(updatedPageData, null, 2), + }, + }); + if (result?.isError) throw new Error("write_file failed"); + setPreviewRefreshKey((k) => k + 1); + } catch { + toast.error("Auto-save failed"); + } finally { + setCmsAutoSaving(false); + } + }, 800); + }; + const handleChangePageVariantRule = (newRule: Record) => { const pvIdx = cmsSelectedPageVariantRef.current; if (pvIdx === null || !cmsData) return; @@ -3762,44 +3969,73 @@ function FileExplorerWorkspace({ const handleAddPageVariant = () => { const snap = cmsDataRef.current; - if (!snap?.pageVariants || !app || !userEnv) return; - - const mv = { - ...(snap.pageData.sections as Record), - }; - const rawVariants = [...((mv.variants as unknown[]) ?? [])]; - - // Clone the last variant as template - const lastIdx = rawVariants.length - 1; - const lastRaw = rawVariants[lastIdx] as - | { - value?: unknown[]; - rule?: Record; - } - | undefined; - const clonedRaw = lastRaw - ? JSON.parse(JSON.stringify(lastRaw)) - : { value: [], rule: {} }; - rawVariants.push(clonedRaw); - - const updatedPageData = { - ...snap.pageData, - sections: { ...mv, variants: rawVariants }, - }; + if (!snap || !app || !userEnv) return; - // Clone the last display entry - const lastDisplay = snap.pageVariants[snap.pageVariants.length - 1]; - const clonedDisplay = lastDisplay - ? (JSON.parse( - JSON.stringify(lastDisplay), - ) as (typeof snap.pageVariants)[number]) - : { + let updatedPageData: Record; + let newPageVariants: NonNullable; + + if (!snap.pageVariants) { + // First-time: convert flat sections array into page-level multivariate + const rawSectionsArray = snap.pageData.sections as unknown[]; + const clonedSections = JSON.parse( + JSON.stringify(rawSectionsArray), + ) as unknown[]; + const mv = { + __resolveType: "website/flags/multivariate.ts", + variants: [ + { value: rawSectionsArray, rule: {} }, + { value: clonedSections, rule: {} }, + ], + }; + updatedPageData = { ...snap.pageData, sections: mv }; + newPageVariants = [ + { label: formatMatcherRule({}), - rule: {} as Record, - sections: [] as GetPageSectionsOutput["sections"], - }; - const newPageVariants = [...snap.pageVariants]; - newPageVariants.push(clonedDisplay); + rule: {}, + sections: snap.sections, + }, + { + label: formatMatcherRule({}), + rule: {}, + sections: [...snap.sections], + }, + ]; + } else { + const mv = { + ...(snap.pageData.sections as Record), + }; + const rawVariants = [...((mv.variants as unknown[]) ?? [])]; + + // Clone the last variant as template + const lastIdx = rawVariants.length - 1; + const lastRaw = rawVariants[lastIdx] as + | { + value?: unknown[]; + rule?: Record; + } + | undefined; + const clonedRaw = lastRaw + ? JSON.parse(JSON.stringify(lastRaw)) + : { value: [], rule: {} }; + rawVariants.push(clonedRaw); + updatedPageData = { + ...snap.pageData, + sections: { ...mv, variants: rawVariants }, + }; + + // Clone the last display entry + const lastDisplay = snap.pageVariants[snap.pageVariants.length - 1]; + const clonedDisplay = lastDisplay + ? (JSON.parse( + JSON.stringify(lastDisplay), + ) as (typeof snap.pageVariants)[number]) + : { + label: formatMatcherRule({}), + rule: {} as Record, + sections: [] as GetPageSectionsOutput["sections"], + }; + newPageVariants = [...snap.pageVariants, clonedDisplay]; + } const next: GetPageSectionsOutput = { ...snap, @@ -4195,6 +4431,92 @@ function FileExplorerWorkspace({ } }; + const handleChangeVariantName = (newName: string) => { + if (!cmsData || cmsSelectedVariant === null) return; + const sectionIdx = cmsSelectedSectionRef.current; + if (sectionIdx === null) return; + + const rawSections = [ + ...(getActiveSectionsArray( + cmsData.pageData, + cmsSelectedPageVariantRef.current, + ) as Record[]), + ]; + const rawSection = { ...rawSections[sectionIdx] } as Record; + const displaySection = cmsData.sections[sectionIdx]!; + const mvContainer = { + ...getMultivariateContainer(rawSection, displaySection), + } as { + __resolveType: string; + variants: Array<{ + value: Record; + rule: Record; + name?: string; + }>; + }; + const variants = [...(mvContainer.variants ?? [])]; + const trimmed = newName.trim(); + const updated = { ...variants[cmsSelectedVariant] } as Record; + if (trimmed) { + updated.name = trimmed; + } else { + delete updated.name; + } + variants[cmsSelectedVariant] = updated as (typeof variants)[number]; + mvContainer.variants = variants; + rawSections[sectionIdx] = rebuildRawSection( + rawSection, + mvContainer, + displaySection, + ); + + const newDisplaySections = [...cmsData.sections]; + const updatedDisplaySection = { ...newDisplaySections[sectionIdx] }; + if (updatedDisplaySection.variants) { + const newVariants = [...updatedDisplaySection.variants]; + const target = { ...newVariants[cmsSelectedVariant] } as (typeof newVariants)[number] & { name?: string }; + if (trimmed) target.name = trimmed; + else target.name = undefined; + newVariants[cmsSelectedVariant] = target; + updatedDisplaySection.variants = newVariants; + } + newDisplaySections[sectionIdx] = updatedDisplaySection; + + const updatedPageData = withSectionsArray( + cmsData.pageData, + cmsSelectedPageVariantRef.current, + rawSections, + ); + const next: GetPageSectionsOutput = { + ...cmsData, + pageData: updatedPageData, + sections: newDisplaySections, + }; + setCmsData(next); + cmsDataRef.current = next; + + setCmsAutoSaving(true); + if (cmsAutoSaveTimerRef.current) clearTimeout(cmsAutoSaveTimerRef.current); + cmsAutoSaveTimerRef.current = setTimeout(async () => { + try { + const result = await app?.callServerTool({ + name: "write_file", + arguments: { + env: userEnv, + filepath: next.filePath, + content: JSON.stringify(updatedPageData, null, 2), + }, + }); + if (result?.isError) throw new Error("write_file failed"); + setPreviewRefreshKey((k) => k + 1); + } catch { + toast.error("Auto-save failed"); + } finally { + setCmsAutoSaving(false); + } + }, 800); + }; + const handleChangeVariantData = ( field: "value" | "rule", newData: Record, @@ -4322,6 +4644,54 @@ function FileExplorerWorkspace({ return mutatedContainer; }; + const handleReorderPageVariants = (srcIdx: number, destIdx: number) => { + if (!cmsData?.pageVariants) return; + + const mv = { + ...(cmsData.pageData.sections as Record), + }; + const rawVariants = [...((mv.variants as unknown[]) ?? [])]; + const [movedRaw] = rawVariants.splice(srcIdx, 1); + rawVariants.splice(destIdx, 0, movedRaw); + const updatedPageData = { + ...cmsData.pageData, + sections: { ...mv, variants: rawVariants }, + }; + + const newPageVariants = [...cmsData.pageVariants]; + const [movedDisplay] = newPageVariants.splice(srcIdx, 1); + newPageVariants.splice(destIdx, 0, movedDisplay); + + const next: GetPageSectionsOutput = { + ...cmsData, + pageData: updatedPageData, + pageVariants: newPageVariants, + }; + setCmsData(next); + cmsDataRef.current = next; + + setCmsAutoSaving(true); + if (cmsAutoSaveTimerRef.current) clearTimeout(cmsAutoSaveTimerRef.current); + cmsAutoSaveTimerRef.current = setTimeout(async () => { + try { + const result = await app?.callServerTool({ + name: "write_file", + arguments: { + env: userEnv, + filepath: next.filePath, + content: JSON.stringify(updatedPageData, null, 2), + }, + }); + if (result?.isError) throw new Error("write_file failed"); + setPreviewRefreshKey((k) => k + 1); + } catch { + toast.error("Auto-save failed"); + } finally { + setCmsAutoSaving(false); + } + }, 800); + }; + const handleReorderVariants = (srcIdx: number, destIdx: number) => { if (!cmsData) return; const sectionIdx = cmsSelectedSectionRef.current; @@ -6672,6 +7042,7 @@ function FileExplorerWorkspace({ onSelectVariant={handleCmsSelectVariant} onDeselectVariant={handleCmsDeselectVariant} onChangeVariantData={handleChangeVariantData} + onChangeVariantName={handleChangeVariantName} onReorderVariants={handleReorderVariants} availableMatchers={cmsAvailableMatchers} onFetchMatchers={() => void fetchMatchersList()} @@ -6692,9 +7063,11 @@ function FileExplorerWorkspace({ onChangePageVariantMatcherType={ handleChangePageVariantMatcherType } + onChangePageVariantName={handleChangePageVariantName} onAddPageVariant={handleAddPageVariant} onDuplicatePageVariant={handleDuplicatePageVariant} onRemovePageVariant={handleRemovePageVariant} + onReorderPageVariants={handleReorderPageVariants} /> )} @@ -6896,7 +7269,7 @@ function FileExplorerWorkspace({
{globals.length > 0 && (
-

+

Global sections

@@ -6907,7 +7280,7 @@ function FileExplorerWorkspace({ {types.length > 0 && (
{globals.length > 0 && ( -

+

Sections

)} From e367c59015a4e867c45a3eb1c92d16992ffaeafd Mon Sep 17 00:00:00 2001 From: rafavalls Date: Fri, 8 May 2026 12:46:10 -0300 Subject: [PATCH 2/3] chore: format files and sort imports Apply biome auto-fixes to satisfy ci:check: - Format api/tools/files.ts and web/tools/file-explorer/cms-form.tsx - Sort imports in web/tools/file-explorer/index.tsx Co-Authored-By: Claude Opus 4.7 (1M context) --- api/tools/files.ts | 4 +- web/tools/file-explorer/cms-form.tsx | 65 ++++++-- web/tools/file-explorer/index.tsx | 229 ++++++++++++++------------- 3 files changed, 173 insertions(+), 125 deletions(-) diff --git a/api/tools/files.ts b/api/tools/files.ts index 9149e22..4edb345 100644 --- a/api/tools/files.ts +++ b/api/tools/files.ts @@ -1147,9 +1147,7 @@ export const getPageSectionsTool = createTool({ value, rule, label: formatMatcher(rule), - ...(typeof v.name === "string" && v.name - ? { name: v.name } - : {}), + ...(typeof v.name === "string" && v.name ? { name: v.name } : {}), }; }); const firstValueRt = ( diff --git a/web/tools/file-explorer/cms-form.tsx b/web/tools/file-explorer/cms-form.tsx index 11177bd..51248a9 100644 --- a/web/tools/file-explorer/cms-form.tsx +++ b/web/tools/file-explorer/cms-form.tsx @@ -314,10 +314,17 @@ function formatDateDisplay( if (!dateStr) return ""; const date = new Date(dateStr); if (Number.isNaN(date.getTime())) return ""; - return new Intl.DateTimeFormat("en-US", + return new Intl.DateTimeFormat( + "en-US", mode === "date" ? { month: "short", day: "numeric", year: "numeric" } - : { month: "short", day: "numeric", year: "numeric", hour: "numeric", minute: "2-digit" }, + : { + month: "short", + day: "numeric", + year: "numeric", + hour: "numeric", + minute: "2-digit", + }, ).format(date); } @@ -428,11 +435,30 @@ function DateField({ aria-hidden="true" className="shrink-0 text-muted-foreground" > - + - + - + {display || placeholder}
@@ -462,9 +488,22 @@ function DateField({ fill="none" aria-hidden="true" > - + - +
@@ -1292,9 +1331,7 @@ function ImageField({

{stem}

{ext && ( -

- {ext} -

+

{ext}

)}
)} @@ -1427,9 +1464,7 @@ function MediaField({ {stem}

{ext && ( -

- {ext} -

+

{ext}

)}
@@ -1441,9 +1476,7 @@ function MediaField({ {stem}

{ext && ( -

- {ext} -

+

{ext}

)}
diff --git a/web/tools/file-explorer/index.tsx b/web/tools/file-explorer/index.tsx index ff69085..6992b14 100644 --- a/web/tools/file-explorer/index.tsx +++ b/web/tools/file-explorer/index.tsx @@ -98,12 +98,6 @@ import { CardHeader, CardTitle, } from "@/components/ui/card.tsx"; -import { - ContextMenu, - ContextMenuContent, - ContextMenuItem, - ContextMenuTrigger, -} from "@/components/ui/context-menu.tsx"; import { CommandDialog, CommandEmpty, @@ -112,6 +106,12 @@ import { CommandItem, CommandList, } from "@/components/ui/command.tsx"; +import { + ContextMenu, + ContextMenuContent, + ContextMenuItem, + ContextMenuTrigger, +} from "@/components/ui/context-menu.tsx"; import { Dialog, DialogContent, @@ -133,16 +133,16 @@ import { EmptyTitle, } from "@/components/ui/empty.tsx"; import { Input } from "@/components/ui/input.tsx"; -import { - ResizableHandle, - ResizablePanel, - ResizablePanelGroup, -} from "@/components/ui/resizable.tsx"; import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover.tsx"; +import { + ResizableHandle, + ResizablePanel, + ResizablePanelGroup, +} from "@/components/ui/resizable.tsx"; import { ScrollArea } from "@/components/ui/scroll-area.tsx"; import { @@ -1697,7 +1697,6 @@ function CmsPanel({ onReorderPageVariants?.(srcIdx, destIdx); }; - const editingGlobally = savedBlock === "editing"; // Page-level multivariate derived state @@ -1742,16 +1741,24 @@ function CmsPanel({ }, { label: - (activeSection?.variants?.[selectedVariant ?? 0] as { - name?: string; - } | undefined)?.name || + ( + activeSection?.variants?.[selectedVariant ?? 0] as + | { + name?: string; + } + | undefined + )?.name || activeSection?.variants?.[selectedVariant ?? 0]?.label || `Variant ${(selectedVariant ?? 0) + 1}`, editable: true, editValue: - (activeSection?.variants?.[selectedVariant ?? 0] as { - name?: string; - } | undefined)?.name ?? "", + ( + activeSection?.variants?.[selectedVariant ?? 0] as + | { + name?: string; + } + | undefined + )?.name ?? "", placeholder: activeSection?.variants?.[selectedVariant ?? 0]?.label || `Variant ${(selectedVariant ?? 0) + 1}`, @@ -1949,47 +1956,47 @@ function CmsPanel({ /> {variantCardOpen && ( -
- {(() => { - const variant = - activeSection?.variants?.[selectedVariant ?? 0]; - const ruleData = variant?.rule ?? {}; - const ruleRt = (ruleData.__resolveType as string) ?? ""; - const ruleLabel = ruleRt - ? ruleRt - .split("/") - .pop() - ?.replace(/\.(tsx|ts)$/, "") - : "Always"; - return ( - <> -
- - -
- {variantRuleSchema ? ( - onChangeVariantData("rule", d)} - /> - ) : ruleRt ? ( -
- Loading schema… +
+ {(() => { + const variant = + activeSection?.variants?.[selectedVariant ?? 0]; + const ruleData = variant?.rule ?? {}; + const ruleRt = (ruleData.__resolveType as string) ?? ""; + const ruleLabel = ruleRt + ? ruleRt + .split("/") + .pop() + ?.replace(/\.(tsx|ts)$/, "") + : "Always"; + return ( + <> +
+ +
- ) : null} - - ); - })()} -
+ {variantRuleSchema ? ( + onChangeVariantData("rule", d)} + /> + ) : ruleRt ? ( +
+ Loading schema… +
+ ) : null} + + ); + })()} +
)}
@@ -2097,9 +2104,7 @@ function CmsPanel({ ? "1 section" : `${pv.sections.length} sections` } - valueLabel={ - (pv as { name?: string }).name || pv.label - } + valueLabel={(pv as { name?: string }).name || pv.label} onSelect={() => onSelectPageVariant?.(i)} onDuplicate={() => onDuplicatePageVariant?.(i)} onRemove={() => onRemovePageVariant?.(i)} @@ -2164,47 +2169,47 @@ function CmsPanel({ /> {variantCardOpen && ( -
- {(() => { - const pvRule = activePageVariant?.rule ?? {}; - const pvRuleRt = (pvRule.__resolveType as string) ?? ""; - const pvRuleLabel = pvRuleRt - ? pvRuleRt - .split("/") - .pop() - ?.replace(/\.(tsx|ts)$/, "") - : "Always"; - return ( - <> -
- - - onChangePageVariantMatcherType?.(rt) - } - /> -
- {pageVariantRuleSchema ? ( - onChangePageVariantRule?.(d)} - /> - ) : pvRuleRt ? ( -
- Loading schema… +
+ {(() => { + const pvRule = activePageVariant?.rule ?? {}; + const pvRuleRt = (pvRule.__resolveType as string) ?? ""; + const pvRuleLabel = pvRuleRt + ? pvRuleRt + .split("/") + .pop() + ?.replace(/\.(tsx|ts)$/, "") + : "Always"; + return ( + <> +
+ + + onChangePageVariantMatcherType?.(rt) + } + />
- ) : null} - - ); - })()} -
+ {pageVariantRuleSchema ? ( + onChangePageVariantRule?.(d)} + /> + ) : pvRuleRt ? ( +
+ Loading schema… +
+ ) : null} + + ); + })()} +
)}
@@ -2237,7 +2242,9 @@ function CmsPanel({ onSelect={() => onSelectSection(section.index)} onDuplicate={() => onDuplicateSection(section.index)} onRemove={() => onRemoveSection(section.index)} - onToggleLazy={() => onToggleLazySection(section.index)} + onToggleLazy={() => + onToggleLazySection(section.index) + } onToggleHidden={() => onToggleHiddenSection(section.index) } @@ -4442,7 +4449,10 @@ function FileExplorerWorkspace({ cmsSelectedPageVariantRef.current, ) as Record[]), ]; - const rawSection = { ...rawSections[sectionIdx] } as Record; + const rawSection = { ...rawSections[sectionIdx] } as Record< + string, + unknown + >; const displaySection = cmsData.sections[sectionIdx]!; const mvContainer = { ...getMultivariateContainer(rawSection, displaySection), @@ -4456,7 +4466,10 @@ function FileExplorerWorkspace({ }; const variants = [...(mvContainer.variants ?? [])]; const trimmed = newName.trim(); - const updated = { ...variants[cmsSelectedVariant] } as Record; + const updated = { ...variants[cmsSelectedVariant] } as Record< + string, + unknown + >; if (trimmed) { updated.name = trimmed; } else { @@ -4474,7 +4487,9 @@ function FileExplorerWorkspace({ const updatedDisplaySection = { ...newDisplaySections[sectionIdx] }; if (updatedDisplaySection.variants) { const newVariants = [...updatedDisplaySection.variants]; - const target = { ...newVariants[cmsSelectedVariant] } as (typeof newVariants)[number] & { name?: string }; + const target = { + ...newVariants[cmsSelectedVariant], + } as (typeof newVariants)[number] & { name?: string }; if (trimmed) target.name = trimmed; else target.name = undefined; newVariants[cmsSelectedVariant] = target; @@ -7063,7 +7078,9 @@ function FileExplorerWorkspace({ onChangePageVariantMatcherType={ handleChangePageVariantMatcherType } - onChangePageVariantName={handleChangePageVariantName} + onChangePageVariantName={ + handleChangePageVariantName + } onAddPageVariant={handleAddPageVariant} onDuplicatePageVariant={handleDuplicatePageVariant} onRemovePageVariant={handleRemovePageVariant} From 21b1f559c126e64ede7ca519f7858a26f793e5e5 Mon Sep 17 00:00:00 2001 From: rafavalls Date: Fri, 8 May 2026 12:53:16 -0300 Subject: [PATCH 3/3] deslop: drop defensive try/catch and structural-type casts - Remove try/catch around localStorage and showPicker; let the rare failure modes throw naturally - Drop ad-hoc { name?: string } casts on variant types now that name lives on CmsVariant and PageVariantEntry - Collapse repeated newName.trim() and intermediate Record shuffling in the rename handlers - Use cn() instead of an inline ternary on the date overlay span - Strip a few what-comments left over from refactors Co-Authored-By: Claude Opus 4.7 (1M context) --- web/tools/file-explorer/cms-form.tsx | 30 ++------ web/tools/file-explorer/index.tsx | 102 +++++++++------------------ 2 files changed, 39 insertions(+), 93 deletions(-) diff --git a/web/tools/file-explorer/cms-form.tsx b/web/tools/file-explorer/cms-form.tsx index 51248a9..0308b2b 100644 --- a/web/tools/file-explorer/cms-form.tsx +++ b/web/tools/file-explorer/cms-form.tsx @@ -376,7 +376,6 @@ function DateField({ const handleBlur = () => { setFocused(false); - // If user typed an unparseable value, snap back to the source-of-truth value. const formatted = formatForNativeInput(value, mode); if (formatted !== local) { setLocal(formatted); @@ -384,28 +383,14 @@ function DateField({ } }; - const handleClear = (e: React.MouseEvent) => { - e.preventDefault(); - e.stopPropagation(); + const handleClear = () => { setLocal(""); prevFormattedRef.current = ""; onChange(""); }; - const openPicker = (e: React.MouseEvent) => { - e.preventDefault(); - e.stopPropagation(); - const el = inputRef.current; - if (!el) return; - if (typeof el.showPicker === "function") { - try { - el.showPicker(); - return; - } catch { - // fall through to focus - } - } - el.focus(); + const openPicker = () => { + inputRef.current?.showPicker(); }; const display = formatDateDisplay(value, mode); @@ -453,11 +438,10 @@ function DateField({ /> {display || placeholder} diff --git a/web/tools/file-explorer/index.tsx b/web/tools/file-explorer/index.tsx index 6992b14..12540ed 100644 --- a/web/tools/file-explorer/index.tsx +++ b/web/tools/file-explorer/index.tsx @@ -1632,21 +1632,13 @@ function CmsPanel({ (data?.pageData.path as string | undefined) ?? "", ); - const [variantCardOpen, setVariantCardOpen] = useState(() => { - try { - return localStorage.getItem("variant-card-open") !== "false"; - } catch { - return true; - } - }); + const [variantCardOpen, setVariantCardOpen] = useState( + () => localStorage.getItem("variant-card-open") !== "false", + ); const toggleVariantCard = () => { setVariantCardOpen((prev) => { const next = !prev; - try { - localStorage.setItem("variant-card-open", String(next)); - } catch { - // ignore - } + localStorage.setItem("variant-card-open", String(next)); return next; }); }; @@ -1739,31 +1731,17 @@ function CmsPanel({ onClick: onDeselectVariant, color: "oklch(0.45 0.15 160)", }, - { - label: - ( - activeSection?.variants?.[selectedVariant ?? 0] as - | { - name?: string; - } - | undefined - )?.name || - activeSection?.variants?.[selectedVariant ?? 0]?.label || - `Variant ${(selectedVariant ?? 0) + 1}`, - editable: true, - editValue: - ( - activeSection?.variants?.[selectedVariant ?? 0] as - | { - name?: string; - } - | undefined - )?.name ?? "", - placeholder: - activeSection?.variants?.[selectedVariant ?? 0]?.label || - `Variant ${(selectedVariant ?? 0) + 1}`, - onLabelChange: onChangeVariantName, - }, + (() => { + const v = activeSection?.variants?.[selectedVariant ?? 0]; + const fallback = `Variant ${(selectedVariant ?? 0) + 1}`; + return { + label: v?.name || v?.label || fallback, + editable: true, + editValue: v?.name ?? "", + placeholder: v?.label || fallback, + onLabelChange: onChangeVariantName, + }; + })(), ]} actions={ autoSaving && ( @@ -1835,13 +1813,11 @@ function CmsPanel({ { label: editName || "Page", onClick: onDeselectPageVariant }, { label: - (activePageVariant as { name?: string } | undefined)?.name || + activePageVariant?.name || `Variant ${selectedPageVariant! + 1}`, color: "oklch(0.45 0.15 160)", editable: true, - editValue: - (activePageVariant as { name?: string } | undefined)?.name ?? - "", + editValue: activePageVariant?.name ?? "", placeholder: `Variant ${selectedPageVariant! + 1}`, onLabelChange: (v: string) => onChangePageVariantName?.(v), }, @@ -2041,9 +2017,7 @@ function CmsPanel({ onSelectVariant(vIdx)} onDuplicate={() => onDuplicateVariant(vIdx)} @@ -2104,7 +2078,7 @@ function CmsPanel({ ? "1 section" : `${pv.sections.length} sections` } - valueLabel={(pv as { name?: string }).name || pv.label} + valueLabel={pv.name || pv.label} onSelect={() => onSelectPageVariant?.(i)} onDuplicate={() => onDuplicatePageVariant?.(i)} onRemove={() => onRemovePageVariant?.(i)} @@ -3854,19 +3828,16 @@ function FileExplorerWorkspace({ const handleChangePageVariantName = (newName: string) => { const pvIdx = cmsSelectedPageVariantRef.current; if (pvIdx === null || !cmsData) return; + const trimmed = newName.trim(); const mv = { ...(cmsData.pageData.sections as Record), }; const variants = [...((mv.variants as unknown[]) ?? [])]; - const current = (variants[pvIdx] as Record) ?? {}; - const updated = { ...current }; - if (newName.trim()) { - updated.name = newName; - } else { - delete updated.name; - } - variants[pvIdx] = updated; + variants[pvIdx] = { + ...(variants[pvIdx] as Record), + name: trimmed || undefined, + }; const updatedPageData = { ...cmsData.pageData, sections: { ...mv, variants }, @@ -3876,10 +3847,9 @@ function FileExplorerWorkspace({ ? [...cmsData.pageVariants] : undefined; if (newPageVariants?.[pvIdx]) { - const trimmed = newName.trim(); newPageVariants[pvIdx] = { ...newPageVariants[pvIdx], - ...(trimmed ? { name: trimmed } : { name: undefined }), + name: trimmed || undefined, }; } @@ -4466,16 +4436,10 @@ function FileExplorerWorkspace({ }; const variants = [...(mvContainer.variants ?? [])]; const trimmed = newName.trim(); - const updated = { ...variants[cmsSelectedVariant] } as Record< - string, - unknown - >; - if (trimmed) { - updated.name = trimmed; - } else { - delete updated.name; - } - variants[cmsSelectedVariant] = updated as (typeof variants)[number]; + variants[cmsSelectedVariant] = { + ...variants[cmsSelectedVariant], + name: trimmed || undefined, + }; mvContainer.variants = variants; rawSections[sectionIdx] = rebuildRawSection( rawSection, @@ -4487,12 +4451,10 @@ function FileExplorerWorkspace({ const updatedDisplaySection = { ...newDisplaySections[sectionIdx] }; if (updatedDisplaySection.variants) { const newVariants = [...updatedDisplaySection.variants]; - const target = { + newVariants[cmsSelectedVariant] = { ...newVariants[cmsSelectedVariant], - } as (typeof newVariants)[number] & { name?: string }; - if (trimmed) target.name = trimmed; - else target.name = undefined; - newVariants[cmsSelectedVariant] = target; + name: trimmed || undefined, + }; updatedDisplaySection.variants = newVariants; } newDisplaySections[sectionIdx] = updatedDisplaySection;