diff --git a/api/tools/files.ts b/api/tools/files.ts index dd99730..4edb345 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,7 @@ export const getPageSectionsTool = createTool({ value, rule, label: formatMatcher(rule), + ...(typeof v.name === "string" && v.name ? { name: v.name } : {}), }; }); const firstValueRt = ( @@ -1182,6 +1187,7 @@ export const getPageSectionsTool = createTool({ label: string; rule: Record; sections: CmsSection[]; + name?: string; }; let pageVariants: PageVariantEntry[] | undefined; @@ -1200,6 +1206,7 @@ export const getPageSectionsTool = createTool({ variants?: Array<{ value?: unknown[]; rule?: Record; + name?: string; }>; }; const mvVariants = Array.isArray(mvField.variants) @@ -1210,7 +1217,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..0308b2b 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,27 @@ 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 +344,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,31 +374,104 @@ function DateField({ } }; + const handleBlur = () => { + setFocused(false); + const formatted = formatForNativeInput(value, mode); + if (formatted !== local) { + setLocal(formatted); + prevFormattedRef.current = formatted; + } + }; + + const handleClear = () => { + setLocal(""); + prevFormattedRef.current = ""; + onChange(""); + }; + + const openPicker = () => { + inputRef.current?.showPicker(); + }; + + 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 && ( + + )}
)} @@ -1356,9 +1448,7 @@ function MediaField({ {stem}

{ext && ( -

- {ext} -

+

{ext}

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

{ext && ( -

- {ext} -

+

{ext}

)}
diff --git a/web/tools/file-explorer/index.tsx b/web/tools/file-explorer/index.tsx index 4248ce3..12540ed 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, @@ -97,6 +98,14 @@ import { CardHeader, CardTitle, } from "@/components/ui/card.tsx"; +import { + CommandDialog, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@/components/ui/command.tsx"; import { ContextMenu, ContextMenuContent, @@ -124,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 { @@ -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,17 @@ function CmsPanel({ (data?.pageData.path as string | undefined) ?? "", ); + const [variantCardOpen, setVariantCardOpen] = useState( + () => localStorage.getItem("variant-card-open") !== "false", + ); + const toggleVariantCard = () => { + setVariantCardOpen((prev) => { + const next = !prev; + localStorage.setItem("variant-card-open", String(next)); + return next; + }); + }; + useEffect(() => { setEditName((data?.pageData.name as string | undefined) ?? ""); setEditPath((data?.pageData.path as string | undefined) ?? ""); @@ -1621,9 +1679,15 @@ 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"; @@ -1667,11 +1731,17 @@ function CmsPanel({ onClick: onDeselectVariant, color: "oklch(0.45 0.15 160)", }, - { - label: - activeSection?.variants?.[selectedVariant ?? 0]?.label ?? - `Variant ${(selectedVariant ?? 0) + 1}`, - }, + (() => { + 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 && ( @@ -1743,15 +1813,34 @@ function CmsPanel({ { label: editName || "Page", onClick: onDeselectPageVariant }, { label: - activePageVariant?.label ?? + activePageVariant?.name || `Variant ${selectedPageVariant! + 1}`, color: "oklch(0.45 0.15 160)", + editable: true, + editValue: activePageVariant?.name ?? "", + placeholder: `Variant ${selectedPageVariant! + 1}`, + onLabelChange: (v: string) => onChangePageVariantName?.(v), }, ]} actions={ - autoSaving && ( - - ) + <> + {(pageVariants?.length ?? 0) > 1 && ( + + )} + {autoSaving && ( + + )} + } /> ) : ( @@ -1802,90 +1891,105 @@ function CmsPanel({ No page data
) : isEditingVariant && sectionData ? ( - /* ── Variant editing: rule + section collapsibles ── */ + /* ── Variant editing: rule + section ── */
- {/* Segment Rule collapsible */} -
- - {variantRuleOpen && ( -
- {(() => { - 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… + + {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 ( + <> +
+ +
- ) : null} -
- ); - })()} -
- )} + {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 +2017,7 @@ function CmsPanel({ onSelectVariant(vIdx)} onDuplicate={() => onDuplicateVariant(vIdx)} @@ -1956,58 +2060,32 @@ 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… -
- ) : null} -
- ); - })()} -
- )} -
- {/* Sections list */} -
- {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) - } - /> - ))} + + + Variant + + + + {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… +
+ ) : 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) + } + /> + ))} +
+
+
+ )} +
-
+
+
)} @@ -3699,6 +3825,64 @@ 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[]) ?? [])]; + variants[pvIdx] = { + ...(variants[pvIdx] as Record), + name: trimmed || undefined, + }; + const updatedPageData = { + ...cmsData.pageData, + sections: { ...mv, variants }, + }; + + const newPageVariants = cmsData.pageVariants + ? [...cmsData.pageVariants] + : undefined; + if (newPageVariants?.[pvIdx]) { + newPageVariants[pvIdx] = { + ...newPageVariants[pvIdx], + name: trimmed || 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 +3946,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 +4408,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< + string, + unknown + >; + 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(); + variants[cmsSelectedVariant] = { + ...variants[cmsSelectedVariant], + name: trimmed || undefined, + }; + mvContainer.variants = variants; + rawSections[sectionIdx] = rebuildRawSection( + rawSection, + mvContainer, + displaySection, + ); + + const newDisplaySections = [...cmsData.sections]; + const updatedDisplaySection = { ...newDisplaySections[sectionIdx] }; + if (updatedDisplaySection.variants) { + const newVariants = [...updatedDisplaySection.variants]; + newVariants[cmsSelectedVariant] = { + ...newVariants[cmsSelectedVariant], + name: trimmed || undefined, + }; + 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 +4621,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 +7019,7 @@ function FileExplorerWorkspace({ onSelectVariant={handleCmsSelectVariant} onDeselectVariant={handleCmsDeselectVariant} onChangeVariantData={handleChangeVariantData} + onChangeVariantName={handleChangeVariantName} onReorderVariants={handleReorderVariants} availableMatchers={cmsAvailableMatchers} onFetchMatchers={() => void fetchMatchersList()} @@ -6692,9 +7040,13 @@ function FileExplorerWorkspace({ onChangePageVariantMatcherType={ handleChangePageVariantMatcherType } + onChangePageVariantName={ + handleChangePageVariantName + } onAddPageVariant={handleAddPageVariant} onDuplicatePageVariant={handleDuplicatePageVariant} onRemovePageVariant={handleRemovePageVariant} + onReorderPageVariants={handleReorderPageVariants} /> )} @@ -6896,7 +7248,7 @@ function FileExplorerWorkspace({
{globals.length > 0 && (
-

+

Global sections

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

+

Sections

)}