Track datasetRefs against the live draft so manual edits and extract link immediately

This commit is contained in:
2026-06-07 23:54:01 +03:00
parent cdd5cf149f
commit 7d247e8d2c
8 changed files with 183 additions and 57 deletions
+37 -10
View File
@@ -24,7 +24,7 @@ import {
type CreateSnippetOptions,
type Snippet,
} from '@core/snippet';
import { recomputeDatasetRefs, renameDatasetInSpec } from '@core/spec-refs';
import { extractDatasetRefs, recomputeDatasetRefs, renameDatasetInSpec } from '@core/spec-refs';
import {
DEFAULT_SORT_BY,
DEFAULT_SORT_ORDER,
@@ -204,10 +204,13 @@ export const useSnippetStore = create<SnippetState>((set, get) => ({
createSnippet: (options) => {
get().commitDraft(); // flush the outgoing snippet's valid edits before switching away
const created = createSnippet(options);
// Mirror datasetRefs from the spec at creation, like publish does, so a snippet
// built with a named-data reference (Chart Builder, §06) is linked to its dataset
// immediately. Inline-data specs (the sample template) resolve to no refs.
const snippet = { ...created, datasetRefs: recomputeDatasetRefs(created.spec) };
// Mirror datasetRefs from the draft at creation, so a snippet built with a
// named-data reference (Chart Builder, §06) is linked to its dataset immediately.
// datasetRefs tracks the draft — the version being edited — at every mutation
// (create/auto-save/extract/revert/publish), so the link reflects what the user
// sees, not only the last publish (docs/architecture/07 §3). Inline-data specs
// (the sample template) resolve to no refs. spec === draftSpec at creation.
const snippet = { ...created, datasetRefs: recomputeDatasetRefs(created.draftSpec) };
set((s) => ({
snippets: [snippet, ...s.snippets],
activeSnippetId: snippet.id,
@@ -314,9 +317,13 @@ export const useSnippetStore = create<SnippetState>((set, get) => ({
const { activeSnippetId } = get();
if (!activeSnippetId) return;
const modified = (now ?? new Date()).toISOString();
const datasetRefs = recomputeDatasetRefs(text);
set((s) => ({
snippets: s.snippets.map((x) =>
x.id === activeSnippetId ? { ...x, draftSpec: text, modified } : x,
// Recompute datasetRefs from the new draft so a programmatic rewrite that
// introduces a by-name reference — Extract-to-Dataset (spec §03F) — links
// the dataset to the snippet immediately, not only on the next publish.
x.id === activeSnippetId ? { ...x, draftSpec: text, datasetRefs, modified } : x,
),
draftText: text,
editorView: 'draft',
@@ -341,9 +348,15 @@ export const useSnippetStore = create<SnippetState>((set, get) => ({
}
const modified = (now ?? new Date()).toISOString();
// Recompute datasetRefs from the just-committed draft so a hand-typed by-name
// reference links to its dataset as soon as auto-save fires. This runs on the
// debounced auto-save and only on valid JSON (the parse guard above), so it is
// not "every keystroke" and never sees a transiently-invalid draft — it keeps
// datasetRefs mirroring the draft the user is editing (docs/architecture/07 §3).
const datasetRefs = recomputeDatasetRefs(draftText);
set({
snippets: snippets.map((s) =>
s.id === activeSnippetId ? { ...s, draftSpec: draftText, modified } : s,
s.id === activeSnippetId ? { ...s, draftSpec: draftText, datasetRefs, modified } : s,
),
});
return true;
@@ -358,8 +371,18 @@ export const useSnippetStore = create<SnippetState>((set, get) => ({
let updated = 0;
let activeDraftAfter: string | null = null;
// Whether a spec text references the old name (case-insensitive). Reads the
// content rather than reserializing, so a non-referencing snippet's text is
// left byte-for-byte intact (no spurious reformat/modified bump).
const referencesOld = (text: string): boolean =>
extractDatasetRefs(text).some((r) => r.toLowerCase() === lower);
const next = snippets.map((s) => {
if (!s.datasetRefs.some((r) => r.toLowerCase() === lower)) return s;
// Consult both spec and draft, not only datasetRefs: datasetRefs now mirrors
// the DRAFT, so a name still referenced only by the published spec (the user
// removed it from the draft but hasn't published) would otherwise be missed
// and the published spec would rot to a now-renamed dataset (arch 07 §6).
if (!referencesOld(s.spec) && !referencesOld(s.draftSpec)) return s;
updated++;
const spec = renameDatasetInSpec(s.spec, oldName, newName);
const draftSpec = renameDatasetInSpec(s.draftSpec, oldName, newName);
@@ -368,7 +391,7 @@ export const useSnippetStore = create<SnippetState>((set, get) => ({
...s,
spec,
draftSpec,
datasetRefs: recomputeDatasetRefs(spec),
datasetRefs: recomputeDatasetRefs(draftSpec),
modified: (now ?? new Date()).toISOString(),
};
});
@@ -422,9 +445,13 @@ export const useSnippetStore = create<SnippetState>((set, get) => ({
if (!active) return false;
const modified = (now ?? new Date()).toISOString();
// The draft is restored to the published spec, so datasetRefs must mirror it
// again — drop any link that existed only in the discarded draft (e.g. an
// Extract-to-Dataset rewrite the user reverted before publishing).
const datasetRefs = recomputeDatasetRefs(active.spec);
set((s) => ({
snippets: s.snippets.map((x) =>
x.id === activeSnippetId ? { ...x, draftSpec: x.spec, modified } : x,
x.id === activeSnippetId ? { ...x, draftSpec: x.spec, datasetRefs, modified } : x,
),
// Reload the editor with the restored draft, on the editable view (spec §03D).
draftText: active.spec,