Chart Builder: open a snippet in the builder to edit it in place

This commit is contained in:
2026-06-20 12:36:37 +03:00
parent efb5a9bbe0
commit be8cfc402d
16 changed files with 1046 additions and 14 deletions
+36 -2
View File
@@ -13,7 +13,8 @@
* inline near the editor (spec §03E), mirroring the preview via PreviewStore.
*/
import { useEffect, useRef, type RefObject } from 'react';
import { useEffect, useMemo, useRef, type RefObject } from 'react';
import { useShallow } from 'zustand/react/shallow';
// `edcore.main` is the full standalone editor — every feature contribution
// (folding, suggest widget, word operations like Cmd+Backspace, find, bracket
// colorization, multi-cursor, …) — but WITHOUT the `monaco-editor` barrel's
@@ -24,7 +25,8 @@ import 'monaco-editor/esm/vs/language/json/monaco.contribution';
import '../infrastructure/monaco-env'; // side-effect: wire workers before create
import { configureVegaLiteJson } from '../infrastructure/monaco-schema';
import { configureJsonFormatter, installFormatOnPaste } from '../infrastructure/monaco-format';
import { openModal } from '../modals/ModalCoordinator';
import { parseChartSpecText } from '@core/chart-builder';
import { openChartBuilderForEdit, openModal } from '../modals/ModalCoordinator';
import {
installSpecConfigActions,
runExtractConfig,
@@ -33,6 +35,7 @@ import {
} from '../services/spec-config-actions';
import { useAppStore } from '../stores/AppStore';
import { confirm } from '../stores/ConfirmStore';
import { useDatasetStore } from '../stores/DatasetStore';
import { hasInlineData } from '../stores/ExtractStore';
import { publishActiveSnippet } from '../services/snippet-actions';
import { notify } from '../stores/NotificationStore';
@@ -193,6 +196,26 @@ function EditorToolbar({
(s) => s.activeSnippetId !== null && hasInlineData(s.draftText),
);
// Offer "Open in builder" only when the active snippet's published spec is
// losslessly representable in the builder dialect AND its referenced dataset
// exists — the gate the builder's openForEdit enforces (spec §06 → Open in
// builder). Content-gated like Extract, so it is **hidden** when inapplicable
// (the toolbar's convention for content-gated actions — vs Revert/Config, which
// disable because they are merely state-gated; arch 10 §5). `datasetNames` is a
// shallow-stable string[] so the memo doesn't churn (MEMORY → stable selectors).
const activeSpec = useSnippetStore((s) => selectActiveSnippet(s)?.spec ?? null);
const datasetNames = useDatasetStore(useShallow((s) => s.datasets.map((d) => d.name)));
const canOpenInBuilder = useMemo(() => {
if (activeSpec === null) return false;
const config = parseChartSpecText(activeSpec);
return config !== null && datasetNames.includes(config.datasetName);
}, [activeSpec, datasetNames]);
const handleOpenInBuilder = () => {
const snippet = selectActiveSnippet(useSnippetStore.getState());
if (snippet) openChartBuilderForEdit(snippet);
};
// Publish + its success toast live in one place (services/snippet-actions) so
// the button and the Cmd/Ctrl+S shortcut (EventRouter) behave identically.
const handlePublish = publishActiveSnippet;
@@ -239,6 +262,17 @@ function EditorToolbar({
to two lines and Revert/Publish stay on one row. The label rides in
`title` (and the accessible name) when only the icon shows. Publish — the
one primary action — keeps its label at every width. */}
{canOpenInBuilder && (
<Button
className={styles.collapsible}
onClick={handleOpenInBuilder}
title="Open this chart in the visual builder to edit it"
aria-label="Open in builder"
>
<Icon name="chart" className={styles.actionIcon} />
<span className={styles.actionLabel}>Open in builder</span>
</Button>
)}
{canExtract && (
<Button
className={styles.collapsible}