mirror of
https://github.com/olehomelchenko/astrolabe.git
synced 2026-08-08 02:02:33 +00:00
Chart Builder: open a snippet in the builder to edit it in place
This commit is contained in:
@@ -58,6 +58,19 @@
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
/* Edit-in-place banner (spec §06 → Open in builder): names the snippet the builder
|
||||
is editing, so the primary "Save changes" action reads unambiguously. */
|
||||
.editingBanner {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.editingBanner strong {
|
||||
color: var(--text-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* ── Intent front door (spec §06 → Intent) ───────────────────────────────
|
||||
A persistent "what do you want to show?" strip under the dataset picker. A
|
||||
quiet accent-soft wash marks it as the guided on-ramp without competing with
|
||||
|
||||
@@ -1466,6 +1466,12 @@ export function ChartBuilderModal() {
|
||||
const setStack = useChartBuilderStore((s) => s.setStack);
|
||||
const applyWarningFix = useChartBuilderStore((s) => s.applyWarningFix);
|
||||
const runCreate = useChartBuilderStore((s) => s.createSnippet);
|
||||
const runSave = useChartBuilderStore((s) => s.saveEdits);
|
||||
// Edit-in-place (spec §06 → Open in builder): when a snippet was opened in the
|
||||
// builder, the primary action *saves back* to it instead of creating a new snippet.
|
||||
const editingSnippetId = useChartBuilderStore((s) => s.editingSnippetId);
|
||||
const editingSnippetName = useChartBuilderStore((s) => s.editingSnippetName);
|
||||
const editing = editingSnippetId !== null;
|
||||
|
||||
// Validity + guidance + which chart-level controls apply are derived from the
|
||||
// stable `config` reference via useMemo, NOT a store selector that would build a
|
||||
@@ -1526,6 +1532,11 @@ export function ChartBuilderModal() {
|
||||
<div className="visually-hidden" role="status" aria-live="polite">
|
||||
{fixAnnouncement}
|
||||
</div>
|
||||
{editing && (
|
||||
<p className={styles.editingBanner}>
|
||||
Editing <strong>{editingSnippetName}</strong>
|
||||
</p>
|
||||
)}
|
||||
<DatasetPicker datasetId={datasetId} />
|
||||
|
||||
<IntentStrip />
|
||||
@@ -1610,7 +1621,7 @@ export function ChartBuilderModal() {
|
||||
|
||||
{!valid && (
|
||||
<p id="cb-create-hint" className={styles.createHint}>
|
||||
Map at least one channel to a column to create a snippet.
|
||||
Map at least one channel to a column to {editing ? 'save changes' : 'create a snippet'}.
|
||||
</p>
|
||||
)}
|
||||
<div className={styles.actions}>
|
||||
@@ -1622,12 +1633,12 @@ export function ChartBuilderModal() {
|
||||
size="lg"
|
||||
disabled={!valid}
|
||||
aria-describedby={!valid ? 'cb-create-hint' : undefined}
|
||||
// The create is the user's confirmation — close with no discard prompt.
|
||||
// The commit is the user's confirmation — close with no discard prompt.
|
||||
onClick={() => {
|
||||
if (runCreate()) void closeModal(true);
|
||||
if (editing ? runSave() : runCreate()) void closeModal(true);
|
||||
}}
|
||||
>
|
||||
Create Snippet
|
||||
{editing ? 'Save changes' : 'Create Snippet'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user