Skip to content

feat(preview): add sections editor panel and pages dropdown to previe…#3371

Open
guitavano wants to merge 3 commits into
mainfrom
cms-form
Open

feat(preview): add sections editor panel and pages dropdown to previe…#3371
guitavano wants to merge 3 commits into
mainfrom
cms-form

Conversation

@guitavano
Copy link
Copy Markdown
Contributor

@guitavano guitavano commented May 15, 2026

…w toolbar

Adds a schema-driven form editor for deco.cx page sections, integrated as a side panel in the Preview tab. The URL bar now shows a pages dropdown (from the decofile) for quick navigation. Saves go through a new vm-file API proxy that writes block JSON files to the sandbox.

What is this contribution about?

Describe your changes and why they're needed.

Screenshots/Demonstration

Add screenshots or a Loom video if your changes affect the UI.

How to Test

Provide step-by-step instructions for reviewers to test your changes:

  1. Step one
  2. Step two
  3. Expected outcome

Migration Notes

If this PR requires database migrations, configuration changes, or other setup steps, document them here. Remove this section if not applicable.

Review Checklist

  • PR title is clear and descriptive
  • Changes are tested and working
  • Documentation is updated (if needed)
  • No breaking changes

Summary by cubic

Adds a schema-driven Sections Editor side panel in Preview and a pages dropdown in the URL bar for quick navigation. You can edit deco.cx page sections and named blocks with debounced auto-save to the sandbox via a new vm-file API proxy.

  • New Features
    • Side panel: browse pages/sections from /.decofile, drill-down list → form, drag-and-drop reorder (entire row is draggable), and edit page name/path with autosave.
    • Schema/form: resolver mirrors admin-mcp (follows $ref, merges allOf/anyOf/oneOf, const-enum extraction); supports block-ref variants and text/number/boolean/array/object/image/color; unwraps lazy/hidden/multivariate and saved (incl. lazy-wrapped) blocks for editing and rewraps on save.
    • Toolbar: pages dropdown sourced from /.decofile navigates the preview iframe; “Sections Editor” toggle added; preview reloads after saves and tries to preserve focus.
    • API/Queries: new proxy /api/:org/vm-file/{write,read} forwards authenticated requests and writes .deco/blocks/<key>.json; added query keys decofile and live-meta.

Written for commit b1b2c0d. Summary will update on new commits. Review in cubic

…w toolbar

Adds a schema-driven form editor for deco.cx page sections, integrated as a
side panel in the Preview tab. The URL bar now shows a pages dropdown (from the
decofile) for quick navigation. Saves go through a new vm-file API proxy that
writes block JSON files to the sandbox.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

🧪 Benchmark

Should we run the Virtual MCP strategy benchmark for this PR?

React with 👍 to run the benchmark.

Reaction Action
👍 Run quick benchmark (10 & 128 tools)

Benchmark will run on the next push after you react.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 15, 2026

Release Options

Suggested: Minor (2.329.0) — based on feat: prefix

React with an emoji to override the release type:

Reaction Type Next Version
👍 Prerelease 2.328.1-alpha.1
🎉 Patch 2.328.1
❤️ Minor 2.329.0
🚀 Major 3.0.0

Current version: 2.328.0

Note: If multiple reactions exist, the smallest bump wins. If no reactions, the suggested bump is used (default: patch).

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

7 issues found across 22 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/mesh/src/web/components/sections-editor/fields/enum-field.tsx">

<violation number="1" location="apps/mesh/src/web/components/sections-editor/fields/enum-field.tsx:28">
P2: Map the selected string back to the original enum option before calling `onChange` so numbers, booleans, and other non-string enum values preserve their types.</violation>
</file>

<file name="apps/mesh/src/web/components/sections-editor/fields/any-of-field.tsx">

<violation number="1" location="apps/mesh/src/web/components/sections-editor/fields/any-of-field.tsx:44">
P2: `selectedIdx` is derived from `value` only on first render, so external `value` updates can desynchronize the selected option from the actual field value.</violation>
</file>

<file name="apps/mesh/src/web/components/sections-editor/fields/array-field.tsx">

<violation number="1" location="apps/mesh/src/web/components/sections-editor/fields/array-field.tsx:19">
P2: New array items default to an empty string for all non-object types, producing schema-invalid values for number/boolean/integer arrays.</violation>

<violation number="2" location="apps/mesh/src/web/components/sections-editor/fields/array-field.tsx:46">
P2: Using the array index as the row key can cause item state to shift to the wrong row after removals.</violation>
</file>

<file name="apps/mesh/src/web/components/sections-editor/resolve-schema.ts">

<violation number="1" location="apps/mesh/src/web/components/sections-editor/resolve-schema.ts:79">
P2: `allOf` flattening drops `required` constraints from member schemas, so required fields can be incorrectly treated as optional in the resolved schema.</violation>
</file>

<file name="apps/mesh/src/web/components/sections-editor/page-list.tsx">

<violation number="1" location="apps/mesh/src/web/components/sections-editor/page-list.tsx:23">
P2: Guard `decodeURIComponent` to avoid runtime crashes on malformed page keys.</violation>
</file>

<file name="apps/mesh/src/web/components/vm/preview/preview.tsx">

<violation number="1" location="apps/mesh/src/web/components/vm/preview/preview.tsx:571">
P2: Dropdown page items use `onMouseDown` instead of `onClick`, so keyboard activation (Enter/Space) cannot navigate to a page.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

{schema.description && (
<p className="text-xs text-muted-foreground">{schema.description}</p>
)}
<Select value={strValue} onValueChange={(v) => onChange(v)}>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Map the selected string back to the original enum option before calling onChange so numbers, booleans, and other non-string enum values preserve their types.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mesh/src/web/components/sections-editor/fields/enum-field.tsx, line 28:

<comment>Map the selected string back to the original enum option before calling `onChange` so numbers, booleans, and other non-string enum values preserve their types.</comment>

<file context>
@@ -0,0 +1,42 @@
+      {schema.description && (
+        <p className="text-xs text-muted-foreground">{schema.description}</p>
+      )}
+      <Select value={strValue} onValueChange={(v) => onChange(v)}>
+        <SelectTrigger>
+          <SelectValue placeholder="Select..." />
</file context>

Comment thread apps/mesh/src/web/components/sections-editor/fields/any-of-field.tsx Outdated
<p className="text-xs text-muted-foreground">{schema.description}</p>
)}
{items.map((item, i) => (
<div key={`${path}.${i}`} className="border rounded-md p-3 relative">
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Using the array index as the row key can cause item state to shift to the wrong row after removals.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mesh/src/web/components/sections-editor/fields/array-field.tsx, line 46:

<comment>Using the array index as the row key can cause item state to shift to the wrong row after removals.</comment>

<file context>
@@ -0,0 +1,79 @@
+        <p className="text-xs text-muted-foreground">{schema.description}</p>
+      )}
+      {items.map((item, i) => (
+        <div key={`${path}.${i}`} className="border rounded-md p-3 relative">
+          <Button
+            type="button"
</file context>


const addItem = () => {
const defaultVal =
itemSchema?.type === "object" ? {} : (itemSchema?.default ?? "");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: New array items default to an empty string for all non-object types, producing schema-invalid values for number/boolean/integer arrays.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mesh/src/web/components/sections-editor/fields/array-field.tsx, line 19:

<comment>New array items default to an empty string for all non-object types, producing schema-invalid values for number/boolean/integer arrays.</comment>

<file context>
@@ -0,0 +1,79 @@
+
+  const addItem = () => {
+    const defaultVal =
+      itemSchema?.type === "object" ? {} : (itemSchema?.default ?? "");
+    onChange([...items, defaultVal]);
+  };
</file context>

}

// Merge allOf
if (Array.isArray(schema.allOf)) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: allOf flattening drops required constraints from member schemas, so required fields can be incorrectly treated as optional in the resolved schema.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mesh/src/web/components/sections-editor/resolve-schema.ts, line 79:

<comment>`allOf` flattening drops `required` constraints from member schemas, so required fields can be incorrectly treated as optional in the resolved schema.</comment>

<file context>
@@ -0,0 +1,198 @@
+  }
+
+  // Merge allOf
+  if (Array.isArray(schema.allOf)) {
+    let merged: RawSchema = {};
+    for (const entry of schema.allOf) {
</file context>

name = name.slice(0, lastDash);
}
}
return decodeURIComponent(name);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Guard decodeURIComponent to avoid runtime crashes on malformed page keys.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mesh/src/web/components/sections-editor/page-list.tsx, line 23:

<comment>Guard `decodeURIComponent` to avoid runtime crashes on malformed page keys.</comment>

<file context>
@@ -0,0 +1,80 @@
+      name = name.slice(0, lastDash);
+    }
+  }
+  return decodeURIComponent(name);
+}
+
</file context>

Comment on lines +571 to +578
onMouseDown={(e) => {
e.preventDefault();
setPagesOpen(false);
// Navigate the iframe
const iframe = previewIframeRef.current;
if (iframe && previewUrl) {
iframe.src = new URL(page.path, previewUrl).href;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Dropdown page items use onMouseDown instead of onClick, so keyboard activation (Enter/Space) cannot navigate to a page.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mesh/src/web/components/vm/preview/preview.tsx, line 571:

<comment>Dropdown page items use `onMouseDown` instead of `onClick`, so keyboard activation (Enter/Space) cannot navigate to a page.</comment>

<file context>
@@ -493,10 +542,54 @@ export function PreviewContent() {
+                          key={page.key}
+                          type="button"
+                          className="flex w-full items-center gap-3 rounded-md px-3 py-2.5 text-left text-sm hover:bg-accent hover:text-accent-foreground"
+                          onMouseDown={(e) => {
+                            e.preventDefault();
+                            setPagesOpen(false);
</file context>
Suggested change
onMouseDown={(e) => {
e.preventDefault();
setPagesOpen(false);
// Navigate the iframe
const iframe = previewIframeRef.current;
if (iframe && previewUrl) {
iframe.src = new URL(page.path, previewUrl).href;
}
onClick={() => {
setPagesOpen(false);
// Navigate the iframe
const iframe = previewIframeRef.current;
if (iframe && previewUrl) {
iframe.src = new URL(page.path, previewUrl).href;
}
}}

…editable page fields

- Rewrite resolve-schema.ts to mirror admin-mcp buildProperty/collectProps
- Rewrite section-list.tsx with parseSections() for lazy/hidden/saved-block/multivariate detection
- Add drag-and-drop reordering with @dnd-kit/sortable
- Add editable page name/path inputs with auto-save
- Drill-down layout: section list → section form with back navigation
- Unwrap lazy/hidden/multivariate wrappers for form editing and re-wrap on save
- Detect lazy-wrapped saved blocks (e.g. Lazy → Header)
- Use TextInput icon for CMS toggle button
- Attempt to preserve focus during iframe reload

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

5 issues found across 7 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/mesh/src/web/components/sections-editor/fields/any-of-field.tsx">

<violation number="1" location="apps/mesh/src/web/components/sections-editor/fields/any-of-field.tsx:56">
P1: This change breaks the legacy `anyOf` rendering path: `AnyOfField` now returns `null` unless `anyOfRefs` is present, so non-`anyOfRefs` `anyOf` fields disappear from the editor.</violation>
</file>

<file name="apps/mesh/src/web/components/sections-editor/section-list.tsx">

<violation number="1" location="apps/mesh/src/web/components/sections-editor/section-list.tsx:221">
P2: The section item was changed from a `<button>` to a non-focusable `<div>`, which breaks keyboard selection. Make the row focusable and semantic (or use a real button) so Enter/Space selection works.</violation>
</file>

<file name="apps/mesh/src/web/components/sections-editor/sections-editor.tsx">

<violation number="1" location="apps/mesh/src/web/components/sections-editor/sections-editor.tsx:210">
P1: When `currentPath` switches pages, section/form state is not cleared, so stale data can be edited and saved against sections from the newly active page.</violation>

<violation number="2" location="apps/mesh/src/web/components/sections-editor/sections-editor.tsx:331">
P2: Debounced page header autosave can lose edits when `name` and `path` are changed close together; only the last changed field is persisted.</violation>
</file>

<file name="apps/mesh/src/web/components/sections-editor/resolve-schema.ts">

<violation number="1" location="apps/mesh/src/web/components/sections-editor/resolve-schema.ts:156">
P2: Selecting `resolved.type[0]` for union `type` arrays can misclassify nullable fields as `null`, causing those fields to disappear from the form when unset.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
Re-trigger cubic

}

// No variants available
return null;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: This change breaks the legacy anyOf rendering path: AnyOfField now returns null unless anyOfRefs is present, so non-anyOfRefs anyOf fields disappear from the editor.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mesh/src/web/components/sections-editor/fields/any-of-field.tsx, line 56:

<comment>This change breaks the legacy `anyOf` rendering path: `AnyOfField` now returns `null` unless `anyOfRefs` is present, so non-`anyOfRefs` `anyOf` fields disappear from the editor.</comment>

<file context>
@@ -17,104 +15,43 @@ export function AnyOfField({
-    </div>
-  );
+  // No variants available
+  return null;
 }
</file context>

const pages = extractPages(decofile);
const norm = (s: string) => s.replace(/\/+$/, "") || "/";
const activePage = pages.find((p) => norm(p.path) === norm(currentPath));
const activePageKey = activePage?.key ?? null;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: When currentPath switches pages, section/form state is not cleared, so stale data can be edited and saved against sections from the newly active page.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mesh/src/web/components/sections-editor/sections-editor.tsx, line 210:

<comment>When `currentPath` switches pages, section/form state is not cleared, so stale data can be edited and saved against sections from the newly active page.</comment>

<file context>
@@ -67,187 +205,226 @@ export function SectionsEditor({
   const pages = extractPages(decofile);
+  const norm = (s: string) => s.replace(/\/+$/, "") || "/";
+  const activePage = pages.find((p) => norm(p.path) === norm(currentPath));
+  const activePageKey = activePage?.key ?? null;
 
-  const selectedPage =
</file context>

<div
ref={setNodeRef}
style={style}
onClick={onSelect}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The section item was changed from a <button> to a non-focusable <div>, which breaks keyboard selection. Make the row focusable and semantic (or use a real button) so Enter/Space selection works.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mesh/src/web/components/sections-editor/section-list.tsx, line 221:

<comment>The section item was changed from a `<button>` to a non-focusable `<div>`, which breaks keyboard selection. Make the row focusable and semantic (or use a real button) so Enter/Space selection works.</comment>

<file context>
@@ -1,28 +1,339 @@
+    <div
+      ref={setNodeRef}
+      style={style}
+      onClick={onSelect}
+      onKeyDown={(e) => {
+        if (e.key === "Enter" || e.key === " ") onSelect();
</file context>
Suggested change
onClick={onSelect}
role="button"
tabIndex={0}
onClick={onSelect}

pageDebounceRef.current = setTimeout(() => {
const fullPageData = {
...(decofile[activePageKey] as Record<string, unknown>),
[field]: value,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Debounced page header autosave can lose edits when name and path are changed close together; only the last changed field is persisted.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mesh/src/web/components/sections-editor/sections-editor.tsx, line 331:

<comment>Debounced page header autosave can lose edits when `name` and `path` are changed close together; only the last changed field is persisted.</comment>

<file context>
@@ -67,187 +205,226 @@ export function SectionsEditor({
+    pageDebounceRef.current = setTimeout(() => {
+      const fullPageData = {
+        ...(decofile[activePageKey] as Record<string, unknown>),
+        [field]: value,
       };
-      const updatedSections = [...sections];
</file context>

Comment on lines +156 to +158
type = Array.isArray(resolved.type)
? String(resolved.type[0])
: String(resolved.type);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Selecting resolved.type[0] for union type arrays can misclassify nullable fields as null, causing those fields to disappear from the form when unset.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mesh/src/web/components/sections-editor/resolve-schema.ts, line 156:

<comment>Selecting `resolved.type[0]` for union `type` arrays can misclassify nullable fields as `null`, causing those fields to disappear from the form when unset.</comment>

<file context>
@@ -1,198 +1,363 @@
+    // Determine type
+    let type: string | undefined;
+    if (resolved.type) {
+      type = Array.isArray(resolved.type)
+        ? String(resolved.type[0])
+        : String(resolved.type);
</file context>
Suggested change
type = Array.isArray(resolved.type)
? String(resolved.type[0])
: String(resolved.type);
type = Array.isArray(resolved.type)
? String(
resolved.type.find((t) => t !== "null") ?? resolved.type[0],
)
: String(resolved.type);

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 1 file (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/mesh/src/web/components/sections-editor/section-list.tsx">

<violation number="1" location="apps/mesh/src/web/components/sections-editor/section-list.tsx:226">
P2: `touch-none` is now applied to the full row, which can block native touch scrolling on section items.</violation>

<violation number="2" location="apps/mesh/src/web/components/sections-editor/section-list.tsx:236">
P2: The added `listeners` spread overrides the existing `onKeyDown`, so keyboard selection (Enter/Space) no longer runs.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic

if (e.key === "Enter" || e.key === " ") onSelect();
}}
className={cn(
"group flex cursor-pointer select-none items-center gap-2 rounded-md px-2 py-2.5 touch-none transition-colors active:cursor-grabbing",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: touch-none is now applied to the full row, which can block native touch scrolling on section items.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mesh/src/web/components/sections-editor/section-list.tsx, line 226:

<comment>`touch-none` is now applied to the full row, which can block native touch scrolling on section items.</comment>

<file context>
@@ -223,7 +223,7 @@ function SortableSectionItem({
       }}
       className={cn(
-        "group flex cursor-pointer select-none items-center gap-2 rounded-md px-2 py-2.5 transition-colors",
+        "group flex cursor-pointer select-none items-center gap-2 rounded-md px-2 py-2.5 touch-none transition-colors active:cursor-grabbing",
         selected
           ? "bg-accent text-accent-foreground"
</file context>
Suggested change
"group flex cursor-pointer select-none items-center gap-2 rounded-md px-2 py-2.5 touch-none transition-colors active:cursor-grabbing",
"group flex cursor-pointer select-none items-center gap-2 rounded-md px-2 py-2.5 transition-colors active:cursor-grabbing",

Tip: Review your code locally with the cubic CLI to iterate faster.

: "text-foreground/80 hover:bg-accent hover:text-accent-foreground",
)}
{...attributes}
{...listeners}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The added listeners spread overrides the existing onKeyDown, so keyboard selection (Enter/Space) no longer runs.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mesh/src/web/components/sections-editor/section-list.tsx, line 236:

<comment>The added `listeners` spread overrides the existing `onKeyDown`, so keyboard selection (Enter/Space) no longer runs.</comment>

<file context>
@@ -232,17 +232,10 @@ function SortableSectionItem({
               : "text-foreground/80 hover:bg-accent hover:text-accent-foreground",
       )}
+      {...attributes}
+      {...listeners}
     >
-      {/* Drag handle */}
</file context>
Suggested change
{...listeners}
{...listeners}
onKeyDown={(e) => {
listeners?.onKeyDown?.(e);
if (e.key === "Enter" || e.key === " ") onSelect();
}}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant