Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 33 additions & 2 deletions apps/web/src/components/sandbox/blocks/blocks-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,12 @@ import {
BlocksEmptyState,
BlocksErrorState,
} from "@/layouts/main-panel-tabs/blocks-tab-states";
import { useContentBackend } from "@/components/sections-editor/use-content-backend";
import { isProtocolProject } from "@/components/sections-editor/content-backend";
import { PanelLoading } from "@/layouts/main-panel-boundary";
import { isNoSchemaMeta } from "@/components/sections-editor/schemaless";
import { SchemalessEditor } from "@/components/sandbox/content/schemaless-editor";
import { useNewBlocksEditorState } from "@/hooks/use-new-blocks-editor";

const SectionsEditor = lazy(() =>
import("@/components/sections-editor/sections-editor").then((m) => ({
Expand Down Expand Up @@ -67,23 +72,49 @@ export function BlocksPanel({
previewUrl,
}
: null;
const contentBackend = useContentBackend(virtualMcpId, currentBranch);
const decofile = useDecofile(fetchParams, { fetchEnabled: devServerReady });
const meta = useLiveMeta(fetchParams, { fetchEnabled: devServerReady });
const state = resolveBlocksTabState({
lifecyclePhase: sandboxEvents.lifecycle.phase,
decofile: toBlocksQueryState(decofile),
meta: toBlocksQueryState(meta),
hasEditableContent: hasEditableDecoContent(decofile.data, meta.data),
fastPreviewActive: useSessionRuntime(virtualMcpId).runtime === "cms",
fastPreviewActive:
useSessionRuntime(virtualMcpId).runtime === "cms" ||
isProtocolProject(contentBackend),
});

// Which editor this site gets (v8: the new one) waits on the same
// detection, so neither editor shows and then swaps to the other.
const editorUndecided = useNewBlocksEditorState() === undefined;

const panel = (children: ReactNode) => (
<div data-testid="blocks-panel" className="h-full min-h-0 overflow-hidden">
{children}
</div>
);

if (state.kind === "loading") return panel(<PanelLoading />);
if (state.kind === "loading" || editorUndecided) {
return panel(<PanelLoading />);
}
// No schema yet: the page (or any block) as plain fields, never an error.
if (decofile.data && isNoSchemaMeta(meta.data)) {
const target = workspace.state.target;
const initialKey =
target?.kind === "page" || target?.kind === "section" ? target.key : null;
return panel(
<SchemalessEditor
key={initialKey ?? ""}
orgSlug={org.slug}
virtualMcpId={virtualMcpId}
branch={currentBranch ?? ""}
decofile={decofile.data}
initialKey={initialKey}
compact
/>,
);
}
if (state.kind === "empty") return panel(<BlocksEmptyState />);
if (state.kind === "error") {
const retry = () => {
Expand Down
20 changes: 14 additions & 6 deletions apps/web/src/components/sandbox/content/app-editor.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useOptionalChatTask } from "@/components/chat/chat-context";
import { Spinner } from "@decocms/ui/components/spinner.tsx";
import { ChevronRight } from "@untitledui/icons";
import { useRef, useState } from "react";
import { type ReactNode, useRef, useState } from "react";
import { toast } from "sonner";
import { cn } from "@decocms/ui/lib/utils.ts";
import { ScrollArea } from "@decocms/ui/components/scroll-area.tsx";
Expand All @@ -12,6 +12,7 @@ import type { LiveMeta } from "@/components/sections-editor/resolve-schema";
import type { SectionCatalogEntry } from "@/components/sections-editor/section-catalog";
import { createReferencedBlockSaver } from "@/components/sections-editor/save-referenced-block";
import {
type SaveGuard,
useDebouncedSaveBlock,
useSaveBlock,
} from "@/components/sections-editor/use-save-block";
Expand All @@ -38,6 +39,8 @@ export function AppEditor({
excludeFields,
schemaPending = false,
previewBaseUrl = null,
notice,
saveGuard,
}: {
orgSlug: string;
virtualMcpId: string;
Expand All @@ -51,13 +54,18 @@ export function AppEditor({
excludeFields?: readonly string[];
schemaPending?: boolean;
previewBaseUrl?: string | null;
/** Shown above the form (e.g. that the block is not saved yet). */
notice?: ReactNode;
/** A version guard for this block's autosaves (e.g. create-only on the first save). */
saveGuard?: SaveGuard;
}) {
const threadId = useOptionalChatTask()?.taskId ?? null;
const t = useT();
// Section-gallery previews render against the sandbox dev server, falling
// back to the Fast Preview production deployment while the sandbox boots.
const sectionPreviewBase = useSectionPreviewBase({
virtualMcpId,
branch,
sandboxUrl: previewBaseUrl,
});
const resolveType =
Expand All @@ -68,11 +76,10 @@ export function AppEditor({
const title =
titleOverride ?? (block ? appLabel(blockKey, block, meta) : blockKey);

const { save, flush, isPending } = useDebouncedSaveBlock({
orgSlug,
virtualMcpId,
branch,
});
const { save, flush, isPending } = useDebouncedSaveBlock(
{ orgSlug, virtualMcpId, branch },
{ guard: saveGuard },
);
const saveBlock = useSaveBlock({ orgSlug, virtualMcpId, branch });
const saveReferencedBlock = createReferencedBlockSaver((refKey, data) =>
save(refKey, data),
Expand Down Expand Up @@ -212,6 +219,7 @@ export function AppEditor({
<ScrollArea className="min-h-0 min-w-0 flex-1 [&_[data-slot=scroll-area-viewport]>div]:!block">
<div className="px-6 py-6">
<div className="mx-auto max-w-xl">
{notice}
{hasEditableFields ? (
<SchemaForm
key={`${blockKey}:${formResetKey}`}
Expand Down
45 changes: 45 additions & 0 deletions apps/web/src/components/sandbox/content/schemaless-editor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { useEffect, useEffectEvent } from "react";
import { useDebouncedSaveBlock } from "@/components/sections-editor/use-save-block";
import { SchemalessContent } from "./schemaless-content";

/**
* `SchemalessContent` wired to the editor's autosave (debounced, one
* `blocks.apply` per block). Pending edits are saved on leaving a block and
* when this unmounts, which is what happens the moment the schema appears
* and the regular editor takes over.
*/
export function SchemalessEditor({
orgSlug,
virtualMcpId,
branch,
decofile,
initialKey,
compact,
}: {
orgSlug: string;
virtualMcpId: string;
branch: string;
decofile: Record<string, unknown>;
initialKey?: string | null;
compact?: boolean;
}) {
const { save, flush } = useDebouncedSaveBlock({
orgSlug,
virtualMcpId,
branch,
});
// Saves what's pending when the editor goes away (the debounce alone
// would drop it): runs after the save hook's own timer cleanup.
const flushPending = useEffectEvent(() => flush());
// oxlint-disable-next-line ban-use-effect/ban-use-effect -- flush pending autosaves on unmount
useEffect(() => () => flushPending(), []);
return (
<SchemalessContent
decofile={decofile}
onChange={(blockKey, next) => save(blockKey, next)}
onLeaveBlock={flush}
initialKey={initialKey}
compact={compact}
/>
);
}
Loading
Loading