Add dataset library, extract-to-dataset, and render-time reference resolution

This commit is contained in:
2026-06-05 15:49:40 +03:00
parent 25849461e0
commit a4e4d96d3b
41 changed files with 3909 additions and 19 deletions
+73
View File
@@ -0,0 +1,73 @@
/**
* Modal registry (docs/architecture/03 → Layer 1).
*
* One metadata entry per feature modal — the single source of truth the
* coordinator and shell read, so adding a modal is one entry plus its component
* rather than edits scattered across the shell, URL sync, and close logic.
*
* Divergence from the arch sketch's optional generic footer: each modal renders
* its OWN action row inside its body (the Datasets manager is multi-view, so a
* single shell-level Save/Cancel doesn't fit). The registry therefore omits
* `hasError`/`getError` (validity is the modal's own concern) and keeps only
* `getState` for unsaved-change detection on close. The registry is a partial
* map: modals land milestone by milestone (settings/about/donate → M5/M6,
* chartBuilder → M4), so only the implemented ones are registered here.
*/
import type { ComponentType } from 'react';
import type { ActiveModal, ModalName } from './types';
import { DatasetsModal } from '../components/DatasetsModal';
import { ExtractModal } from '../components/ExtractModal';
import { useDatasetStore } from '../stores/DatasetStore';
import { useExtractStore } from '../stores/ExtractStore';
export interface ModalConfig {
name: ModalName;
/** Header title (literal for now; an i18n key once strings are centralized). */
title: string;
/** The body rendered inside the shell. */
component: ComponentType;
/** Initialize transient state on open. `arg` carries an optional sub-target. */
init?: (arg?: string) => void;
/**
* Serializable snapshot of in-progress edits for unsaved-change detection.
* Return null when there is nothing to lose (browsing, or no open form) so
* closing doesn't prompt. Omit entirely for modals that apply immediately.
*/
getState?: () => Record<string, unknown> | null;
/** Whether the modal is reflected in the URL hash (navigable). */
isUrlNavigable?: boolean;
}
export const MODAL_REGISTRY: Partial<Record<ModalName, ModalConfig>> = {
// Navigable, multi-view manager. The snapshot captures only the open create/edit
// form, so browsing list↔detail never trips a false discard prompt.
datasets: {
name: 'datasets',
title: 'Datasets',
component: DatasetsModal,
isUrlNavigable: true,
init: (datasetId) => useDatasetStore.getState().select(datasetId ? Number(datasetId) : null),
getState: () => {
const s = useDatasetStore.getState();
return s.view === 'new' || s.view === 'edit' ? { form: s.form } : null;
},
},
// Opened from the snippet editor with the active draft's inline data to lift out.
extract: {
name: 'extract',
title: 'Extract to Dataset',
component: ExtractModal,
init: () => useExtractStore.getState().init(),
getState: () => ({ name: useExtractStore.getState().name }),
},
};
export const getModalConfig = (name: ActiveModal): ModalConfig | undefined =>
name ? MODAL_REGISTRY[name] : undefined;
export const getModalTitle = (name: ActiveModal): string => getModalConfig(name)?.title ?? '';
export const isUrlNavigable = (name: ActiveModal): boolean =>
getModalConfig(name)?.isUrlNavigable ?? false;