mirror of
https://github.com/olehomelchenko/astrolabe.git
synced 2026-08-08 02:02:33 +00:00
161 lines
6.0 KiB
TypeScript
161 lines
6.0 KiB
TypeScript
/**
|
|
* Extract-embedded-data → Dataset state (spec §03F).
|
|
*
|
|
* Backs the Extract modal: the reverse of a named reference. It captures the
|
|
* embedded data of the **view the cursor sits in**, takes a dataset name, and on
|
|
* confirm saves the data as a new library dataset and rewrites the spec so the
|
|
* embedded data is replaced by a by-name reference.
|
|
*
|
|
* Two shapes of embedded data, captured as a `target` (spec §03F; multi-view scope
|
|
* doc M3):
|
|
* - **inline** — a view's `data.values`. Confirm rewrites that view's `data`
|
|
* block at its anchor path to `{ name }`.
|
|
* - **self-defined** — a `{ name: X }` reference to the spec's own top-level
|
|
* `datasets.X`. Confirm removes the `datasets` entry (and the map when it
|
|
* empties); the reference resolves to the new library dataset, renamed when the
|
|
* name changes (`core/spec-refs` → `promoteSelfDefinedDataset`).
|
|
*
|
|
* The store is the editor-free side of the flow: it never reads the cursor or the
|
|
* Monaco model — `services/extract-action` resolves the focused binding and seeds
|
|
* it via `begin`, so this stays a plain, testable store.
|
|
*/
|
|
|
|
import { create } from 'zustand';
|
|
import { createDataset } from '@core/dataset';
|
|
import { formatSpec } from '@core/json-format';
|
|
import { isNameTaken } from '@core/naming';
|
|
import { setDataBindingAtPath } from '@core/spec-data';
|
|
import { type InlinePayload, specHasExtractableData } from '@core/spec-inline-data';
|
|
import { promoteSelfDefinedDataset } from '@core/spec-refs';
|
|
import { useDatasetStore } from './DatasetStore';
|
|
import { notify } from './NotificationStore';
|
|
import { useSnippetStore } from './SnippetStore';
|
|
|
|
/** The path of the view whose `data` block Extract rewrites (`[]` = root). */
|
|
type AnchorPath = ReadonlyArray<string | number>;
|
|
|
|
/** What confirm rewrites — an inline `data` block, or a self-defined `datasets` entry. */
|
|
export type ExtractTarget =
|
|
| { kind: 'inline'; anchorPath: AnchorPath }
|
|
| { kind: 'self-defined'; datasetName: string };
|
|
|
|
/** True when the active snippet's draft has data Extract can lift, in any view. */
|
|
export function hasExtractableData(draftText: string): boolean {
|
|
try {
|
|
return specHasExtractableData(JSON.parse(draftText));
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
export interface ExtractState {
|
|
/** Proposed dataset name (required, unique). */
|
|
name: string;
|
|
/** The focused view's embedded data captured at open, for the read-only preview. */
|
|
source: InlinePayload | null;
|
|
/** Where `confirm` writes the by-name reference. */
|
|
target: ExtractTarget | null;
|
|
/** Inline validation message, or null. */
|
|
error: string | null;
|
|
|
|
/** Seed the form with the focused view's captured data (extract-action). */
|
|
begin: (captured: { source: InlinePayload; target: ExtractTarget; name?: string }) => void;
|
|
setName: (name: string) => void;
|
|
/**
|
|
* Validate, create the dataset, and rewrite the spec to reference it by name.
|
|
* Returns whether it committed; on failure `error` is set. `now` injectable for
|
|
* tests.
|
|
*/
|
|
confirm: (now?: Date) => boolean;
|
|
reset: () => void;
|
|
}
|
|
|
|
const INITIAL = {
|
|
name: '',
|
|
source: null as InlinePayload | null,
|
|
target: null as ExtractTarget | null,
|
|
error: null as string | null,
|
|
};
|
|
|
|
export const useExtractStore = create<ExtractState>((set, get) => ({
|
|
...INITIAL,
|
|
|
|
begin: ({ source, target, name = '' }) => set({ name, source, target, error: null }),
|
|
|
|
setName: (name) => set({ name, error: null }),
|
|
|
|
confirm: (now) => {
|
|
const name = get().name.trim();
|
|
if (name === '') {
|
|
set({ error: 'Enter a dataset name.' });
|
|
return false;
|
|
}
|
|
if (isNameTaken(name, useDatasetStore.getState().datasets)) {
|
|
set({ error: `A dataset named "${name}" already exists. Choose a different name.` });
|
|
return false;
|
|
}
|
|
const { source, target } = get();
|
|
if (!source || !target) {
|
|
set({ error: 'No data to extract.' });
|
|
return false;
|
|
}
|
|
|
|
// Resolve the rewrite before any side effect: parse the live draft and apply the
|
|
// target's rewrite to a fresh copy. Both can fail (the draft is no longer valid
|
|
// JSON, or its shape changed under the modal); bail with a message and create
|
|
// nothing rather than leave a half-done extraction.
|
|
const draftText = useSnippetStore.getState().draftText;
|
|
let spec: unknown;
|
|
try {
|
|
spec = JSON.parse(draftText);
|
|
} catch {
|
|
set({ error: 'The spec is no longer valid JSON. Close and reopen Extract.' });
|
|
return false;
|
|
}
|
|
|
|
let rewritten: unknown;
|
|
if (target.kind === 'inline') {
|
|
if (!setDataBindingAtPath(spec, target.anchorPath, { name })) {
|
|
set({ error: 'Could not locate the data to replace. Close and reopen Extract.' });
|
|
return false;
|
|
}
|
|
rewritten = spec;
|
|
} else {
|
|
const next = promoteSelfDefinedDataset(spec, target.datasetName, name);
|
|
if (!next) {
|
|
set({ error: 'Could not locate the data to replace. Close and reopen Extract.' });
|
|
return false;
|
|
}
|
|
rewritten = next;
|
|
}
|
|
|
|
// JSON/TopoJSON store the parsed value; CSV/TSV keep raw text — but the captured
|
|
// `values` is already the right runtime shape for each, so store it directly.
|
|
const dataset = createDataset({
|
|
name,
|
|
data: source.values,
|
|
format: source.format,
|
|
source: 'inline',
|
|
now,
|
|
});
|
|
useDatasetStore.getState().add(dataset);
|
|
|
|
// Re-serialize in the app's house style (json-format), preserving the spec and
|
|
// only swapping the captured data for the reference.
|
|
useSnippetStore.getState().replaceActiveDraft(formatSpec(rewritten), now);
|
|
|
|
// Success confirmation (spec §03F). Title states the action; the message adds
|
|
// the consequence — the spec was rewritten to reference the new dataset by name
|
|
// (council toast-copy rule, docs/architecture/10 → Toast copy).
|
|
notify({
|
|
kind: 'success',
|
|
title: 'Dataset created',
|
|
message: `The spec now references "${name}" instead of its embedded data.`,
|
|
});
|
|
set(INITIAL);
|
|
return true;
|
|
},
|
|
|
|
reset: () => set(INITIAL),
|
|
}));
|