@elytracms/studio 0.0.98 → 0.0.99
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/convex/ingest.test.ts +84 -7
- package/convex/ingest.ts +110 -31
- package/convex/path-segment-refusal.test.ts +201 -0
- package/convex/records-column-transform.convex.test.ts +147 -0
- package/convex/schema.ts +1 -1
- package/dist/addon.d.ts +5 -3
- package/dist/addon.js +4 -3
- package/dist/addon.js.map +1 -1
- package/dist/components/app-views/default-modules.d.ts +10 -3
- package/dist/components/app-views/default-modules.js +5 -1
- package/dist/components/app-views/default-modules.js.map +1 -1
- package/dist/components/app-views/module.d.ts +15 -8
- package/dist/components/app-views/module.js.map +1 -1
- package/dist/components/builder/reuse-context.d.ts +29 -1
- package/dist/components/builder/reuse-context.js +58 -12
- package/dist/components/builder/reuse-context.js.map +1 -1
- package/dist/components/cms/cms-store.d.ts +3 -2
- package/dist/components/cms/cms-store.js +8 -2
- package/dist/components/cms/cms-store.js.map +1 -1
- package/dist/components/cms/field-control.js +15 -5
- package/dist/components/cms/field-control.js.map +1 -1
- package/dist/components/cms/use-document-draft.d.ts +4 -1
- package/dist/components/cms/use-document-draft.js +4 -1
- package/dist/components/cms/use-document-draft.js.map +1 -1
- package/dist/components/cms/use-publish-controller.d.ts +7 -0
- package/dist/components/cms/use-publish-controller.js +17 -3
- package/dist/components/cms/use-publish-controller.js.map +1 -1
- package/dist/components/cms/versions-pane.js +8 -5
- package/dist/components/cms/versions-pane.js.map +1 -1
- package/dist/components/delivery-health-banner.d.ts +34 -16
- package/dist/components/delivery-health-banner.js +59 -27
- package/dist/components/delivery-health-banner.js.map +1 -1
- package/dist/components/issue-severity.d.ts +3 -2
- package/dist/components/issue-severity.js.map +1 -1
- package/dist/components/nav-issues-registry.d.ts +39 -24
- package/dist/components/nav-issues-registry.js +50 -21
- package/dist/components/nav-issues-registry.js.map +1 -1
- package/dist/components/nav-issues.d.ts +23 -50
- package/dist/components/nav-issues.js +31 -59
- package/dist/components/nav-issues.js.map +1 -1
- package/dist/components/notifications/notice-center.d.ts +61 -0
- package/dist/components/notifications/notice-center.js +119 -0
- package/dist/components/notifications/notice-center.js.map +1 -0
- package/dist/components/notifications/notice-model.d.ts +137 -0
- package/dist/components/notifications/notice-model.js +107 -0
- package/dist/components/notifications/notice-model.js.map +1 -0
- package/dist/components/notifications/outcome-notice.d.ts +16 -0
- package/dist/components/notifications/outcome-notice.js +34 -0
- package/dist/components/notifications/outcome-notice.js.map +1 -0
- package/dist/components/notifications/shell-findings.d.ts +36 -0
- package/dist/components/notifications/shell-findings.js +140 -0
- package/dist/components/notifications/shell-findings.js.map +1 -0
- package/dist/components/rail-view-source.js +5 -0
- package/dist/components/rail-view-source.js.map +1 -1
- package/dist/components/records/record-rail.d.ts +10 -2
- package/dist/components/records/record-rail.js +74 -4
- package/dist/components/records/record-rail.js.map +1 -1
- package/dist/components/section-nav.d.ts +15 -3
- package/dist/components/section-nav.js +39 -147
- package/dist/components/section-nav.js.map +1 -1
- package/dist/components/studio-project-screen.js.map +1 -1
- package/dist/components/studio-shell.js +8 -3
- package/dist/components/studio-shell.js.map +1 -1
- package/dist/components/use-narrow-viewport.d.ts +11 -0
- package/dist/components/use-narrow-viewport.js +25 -0
- package/dist/components/use-narrow-viewport.js.map +1 -0
- package/dist/components/users/role-policy-drift-issues.d.ts +14 -7
- package/dist/components/users/role-policy-drift-issues.js +17 -8
- package/dist/components/users/role-policy-drift-issues.js.map +1 -1
- package/dist/lib/collection-view/collection-view-source.d.ts +4 -2
- package/dist/lib/collection-view/collection-view-source.js.map +1 -1
- package/dist/lib/engine-version.d.ts +1 -1
- package/dist/lib/engine-version.js +1 -1
- package/dist/lib/engine-version.js.map +1 -1
- package/dist/lib/i18n/messages.de.js +21 -3
- package/dist/lib/i18n/messages.de.js.map +1 -1
- package/dist/lib/i18n/messages.en.d.ts +19 -2
- package/dist/lib/i18n/messages.en.js +24 -4
- package/dist/lib/i18n/messages.en.js.map +1 -1
- package/dist/lib/notices/toast-store.d.ts +61 -0
- package/dist/lib/notices/toast-store.js +116 -0
- package/dist/lib/notices/toast-store.js.map +1 -0
- package/dist/lib/operations/cms-persistence.d.ts +24 -4
- package/dist/lib/operations/cms-persistence.js +417 -80
- package/dist/lib/operations/cms-persistence.js.map +1 -1
- package/dist/lib/persistence/convex-adapter.js +61 -30
- package/dist/lib/persistence/convex-adapter.js.map +1 -1
- package/dist/ui/toast.d.ts +24 -16
- package/dist/ui/toast.js +23 -32
- package/dist/ui/toast.js.map +1 -1
- package/package.json +3 -3
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useMemo, useSyncExternalStore } from 'react';
|
|
3
|
+
import { redirectRecordKind, roleGrantsCapability } from '@elytracms/core/persistence';
|
|
4
|
+
import { getStudioRegistry } from '../../lib/registry/index.js';
|
|
5
|
+
import { projectConfigIssues, resolveDeploymentCollections, resolveDeploymentReuse, resolveDeploymentRoutes, } from '../../lib/workspace/index.js';
|
|
6
|
+
import { routeValidationIssues } from '../../lib/routes/route-issues.js';
|
|
7
|
+
import { contentSessionOrphans, serverContentSessionOrphans, subscribeContentSession } from '../../lib/data/index.js';
|
|
8
|
+
import { projectRolePolicyFor } from '../../lib/operations/project-runtime-config.js';
|
|
9
|
+
import { customizationItems, orphanedCollectionItems, routeIssueItems } from '../nav-issues.js';
|
|
10
|
+
import { useContributedNavIssues, useNavIssues } from '../nav-issues-registry.js';
|
|
11
|
+
import { resolveReuse } from '../builder/reuse.js';
|
|
12
|
+
import { useRecordList } from '../records/record-rail.js';
|
|
13
|
+
import { useFieldRenderers } from '../cms/field-renderer.js';
|
|
14
|
+
import { useProjectRole } from '../auth-context.js';
|
|
15
|
+
import { isNavViewVisible, useStudioNav } from '../app-views/sidebar.js';
|
|
16
|
+
import { useDeliveryHealthFindings } from '../delivery-health-banner.js';
|
|
17
|
+
import { canSeeFinding, orderFindings } from './notice-model.js';
|
|
18
|
+
/**
|
|
19
|
+
* EC-690 — every persistent finding of the studio shell, for the ONE issues tray.
|
|
20
|
+
*
|
|
21
|
+
* The ENGINE's own sources are computed here, in render — config (EC-479), customization
|
|
22
|
+
* (EC-236/467/478), the URL map (EC-469/590/628), orphaned collections (EC-606) and
|
|
23
|
+
* instant-publish health (EC-268) — so they are there on the first paint (no effect has to
|
|
24
|
+
* run before the badge can count them) and no contributor can overwrite them: they are not
|
|
25
|
+
* in the registry's namespace at all. Contributions (`useNavIssues`, a view's `useIssues`)
|
|
26
|
+
* come from the registry. Then the viewer's audience filter (owner ruling (b)) and the ONE
|
|
27
|
+
* order ({@link orderFindings}).
|
|
28
|
+
*
|
|
29
|
+
* The findings hooks moved here from `SectionNav`, which renders none of them any more (the
|
|
30
|
+
* rail foot is only its collapse toggle — owner ruling (a)).
|
|
31
|
+
*/
|
|
32
|
+
export function useShellFindings(project) {
|
|
33
|
+
const nav = useStudioNav(project);
|
|
34
|
+
const { issues: rendererIssues } = useFieldRenderers();
|
|
35
|
+
const configIssues = useProjectConfigIssues();
|
|
36
|
+
const reuseIssues = useReuseIssues();
|
|
37
|
+
const routeIssues = useRouteIssues(project.id);
|
|
38
|
+
const orphanIssues = useOrphanedCollectionIssues();
|
|
39
|
+
const deliveryIssues = useDeliveryHealthFindings(project.id);
|
|
40
|
+
const contributed = useContributedNavIssues();
|
|
41
|
+
const audience = useFindingAudience(project);
|
|
42
|
+
return useMemo(() => {
|
|
43
|
+
const engine = [
|
|
44
|
+
// Config issues lead: "this project declares no collections" explains every other
|
|
45
|
+
// emptiness on the screen, so it is the first thing to read.
|
|
46
|
+
...asFindings('config', customizationItems(configIssues)),
|
|
47
|
+
...asFindings('customization', customizationItems([...nav.issues, ...rendererIssues, ...reuseIssues])),
|
|
48
|
+
...asFindings('routes', routeIssues),
|
|
49
|
+
...asFindings('orphans', orphanIssues),
|
|
50
|
+
...asFindings('delivery', deliveryIssues),
|
|
51
|
+
];
|
|
52
|
+
return orderFindings([...engine, ...contributed]).filter((finding) => canSeeFinding(finding, audience));
|
|
53
|
+
}, [configIssues, nav.issues, rendererIssues, reuseIssues, routeIssues, orphanIssues, deliveryIssues, contributed, audience]);
|
|
54
|
+
}
|
|
55
|
+
function asFindings(source, items) {
|
|
56
|
+
return items.map((item) => ({ id: `${source}:${item.key}`, source, ...item }));
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Who is reading (owner ruling (b)): an admin (`project.administer`) sees config and
|
|
60
|
+
* deployment findings; a located finding shows to whoever can open its location. While the
|
|
61
|
+
* role is still loading nobody is an admin and no view is visible, so a gated finding
|
|
62
|
+
* appears once the role is known, never to the wrong reader first.
|
|
63
|
+
*/
|
|
64
|
+
export function useFindingAudience(project) {
|
|
65
|
+
const nav = useStudioNav(project);
|
|
66
|
+
const role = useProjectRole(project.id);
|
|
67
|
+
return useMemo(() => {
|
|
68
|
+
const policy = projectRolePolicyFor(project.id);
|
|
69
|
+
const ctx = { project, role };
|
|
70
|
+
return {
|
|
71
|
+
isAdmin: role !== null && roleGrantsCapability(role, 'project.administer', policy),
|
|
72
|
+
canReadContent: role !== null && roleGrantsCapability(role, 'content.read', policy),
|
|
73
|
+
views: nav.registry.views.map((view) => ({
|
|
74
|
+
id: view.id,
|
|
75
|
+
...(view.recordKind !== undefined ? { recordKind: view.recordKind } : {}),
|
|
76
|
+
visible: role !== null && isNavViewVisible(view, ctx),
|
|
77
|
+
})),
|
|
78
|
+
};
|
|
79
|
+
}, [nav, role, project]);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* EC-616 → EC-690: runs every view's always-on findings hook (`useIssues`) — whether or not
|
|
83
|
+
* the view is open — and publishes what it returns. One component per view (each hook keeps
|
|
84
|
+
* its own hook order), each its OWN publisher with the source `view:<id>`: before EC-690 every
|
|
85
|
+
* probe published under the one source `view:`, so two such views overwrote each other and
|
|
86
|
+
* one unmount retracted both (critic point 1).
|
|
87
|
+
*/
|
|
88
|
+
export function ViewIssuesProbes({ project }) {
|
|
89
|
+
const nav = useStudioNav(project);
|
|
90
|
+
const issueViews = useMemo(() => nav.registry.views.filter((view) => view.useIssues), [nav]);
|
|
91
|
+
return (_jsx(_Fragment, { children: issueViews.map((view) => (_jsx(ViewIssuesProbe, { view: view, project: project }, view.id))) }));
|
|
92
|
+
}
|
|
93
|
+
function ViewIssuesProbe({ view, project }) {
|
|
94
|
+
const issues = view.useIssues({ project, projectId: project.id });
|
|
95
|
+
useNavIssues(viewIssuesSource(view.id), issues);
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
/** The source a view's always-on findings publish under. */
|
|
99
|
+
export function viewIssuesSource(viewId) {
|
|
100
|
+
return `view:${viewId}`;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* EC-479 — what the project has not declared but the studio cannot work without. Read from
|
|
104
|
+
* the config holder alone (static per studio boot), so it needs no CMS provider and is
|
|
105
|
+
* memoised once. The pure decision core is `projectConfigIssues`.
|
|
106
|
+
*/
|
|
107
|
+
function useProjectConfigIssues() {
|
|
108
|
+
return useMemo(() => projectConfigIssues(resolveDeploymentCollections()), []);
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* EC-467 — the project's reusable-elements config issues: a declaration that cannot
|
|
112
|
+
* resolve, or an ambiguous inference the studio refuses to guess through. Static per studio
|
|
113
|
+
* boot (registry + config collections + studio config). The pure core is `resolveReuse`.
|
|
114
|
+
*/
|
|
115
|
+
function useReuseIssues() {
|
|
116
|
+
return useMemo(() => resolveReuse(getStudioRegistry().registry.list(), resolveDeploymentCollections(), resolveDeploymentReuse())
|
|
117
|
+
.issues, []);
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* EC-469 — the project's URL-map issues: two routes that match the same URLs, a redirect
|
|
121
|
+
* chain that loops, a duplicate or unreadable redirect. Routes are CODE (config, static per
|
|
122
|
+
* boot); redirects are editorial DATA (the rail, live) — the read shares the rail's own query
|
|
123
|
+
* key, so a finding disappears the moment it is fixed; while that query is loading or failed
|
|
124
|
+
* only the code-side conflicts are reported (the rail surfaces its own load error).
|
|
125
|
+
*/
|
|
126
|
+
function useRouteIssues(projectId) {
|
|
127
|
+
const redirects = useRecordList(redirectRecordKind, projectId).data;
|
|
128
|
+
return useMemo(() => routeIssueItems(routeValidationIssues({ routes: resolveDeploymentRoutes(), collections: resolveDeploymentCollections() }, redirects ?? [])), [redirects]);
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* EC-606 — stored documents whose collection the project does not declare. Read from the
|
|
132
|
+
* content session (subscribable, configured from the same hydration in both modes); the
|
|
133
|
+
* server snapshot is empty — there is no session during SSR. The pure mapping is
|
|
134
|
+
* {@link orphanedCollectionItems}.
|
|
135
|
+
*/
|
|
136
|
+
function useOrphanedCollectionIssues() {
|
|
137
|
+
const orphans = useSyncExternalStore(subscribeContentSession, contentSessionOrphans, serverContentSessionOrphans);
|
|
138
|
+
return useMemo(() => orphanedCollectionItems(orphans), [orphans]);
|
|
139
|
+
}
|
|
140
|
+
//# sourceMappingURL=shell-findings.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shell-findings.js","sourceRoot":"","sources":["../../../src/components/notifications/shell-findings.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useMemo, useSyncExternalStore } from 'react'\nimport type { Project } from '@elytracms/core/studio-core'\nimport type { CustomizationIssue } from '@elytracms/core/studio-core'\nimport { redirectRecordKind, roleGrantsCapability } from '@elytracms/core/persistence'\nimport { getStudioRegistry } from '../../lib/registry'\nimport {\n projectConfigIssues,\n resolveDeploymentCollections,\n resolveDeploymentReuse,\n resolveDeploymentRoutes,\n} from '../../lib/workspace'\nimport { routeValidationIssues } from '../../lib/routes/route-issues'\nimport { contentSessionOrphans, serverContentSessionOrphans, subscribeContentSession } from '../../lib/data'\nimport { projectRolePolicyFor } from '../../lib/operations/project-runtime-config'\nimport { customizationItems, orphanedCollectionItems, routeIssueItems } from '../nav-issues'\nimport type { NavIssueItem } from '../nav-issues'\nimport { useContributedNavIssues, useNavIssues } from '../nav-issues-registry'\nimport { resolveReuse } from '../builder/reuse'\nimport { useRecordList } from '../records/record-rail'\nimport { useFieldRenderers } from '../cms/field-renderer'\nimport { useProjectRole } from '../auth-context'\nimport { isNavViewVisible, useStudioNav } from '../app-views/sidebar'\nimport type { AppViewModule } from '../app-views/module'\nimport { useDeliveryHealthFindings } from '../delivery-health-banner'\nimport type { DeliveryHealthFinding } from '../delivery-health-banner'\nimport { canSeeFinding, orderFindings } from './notice-model'\nimport type { EngineFindingSource, Finding, FindingAudience } from './notice-model'\n\n/**\n * EC-690 — every persistent finding of the studio shell, for the ONE issues tray.\n *\n * The ENGINE's own sources are computed here, in render — config (EC-479), customization\n * (EC-236/467/478), the URL map (EC-469/590/628), orphaned collections (EC-606) and\n * instant-publish health (EC-268) — so they are there on the first paint (no effect has to\n * run before the badge can count them) and no contributor can overwrite them: they are not\n * in the registry's namespace at all. Contributions (`useNavIssues`, a view's `useIssues`)\n * come from the registry. Then the viewer's audience filter (owner ruling (b)) and the ONE\n * order ({@link orderFindings}).\n *\n * The findings hooks moved here from `SectionNav`, which renders none of them any more (the\n * rail foot is only its collapse toggle — owner ruling (a)).\n */\nexport function useShellFindings(project: Project): Finding[] {\n const nav = useStudioNav(project)\n const { issues: rendererIssues } = useFieldRenderers()\n const configIssues = useProjectConfigIssues()\n const reuseIssues = useReuseIssues()\n const routeIssues = useRouteIssues(project.id)\n const orphanIssues = useOrphanedCollectionIssues()\n const deliveryIssues = useDeliveryHealthFindings(project.id)\n const contributed = useContributedNavIssues()\n const audience = useFindingAudience(project)\n\n return useMemo(() => {\n const engine: Finding[] = [\n // Config issues lead: \"this project declares no collections\" explains every other\n // emptiness on the screen, so it is the first thing to read.\n ...asFindings('config', customizationItems(configIssues)),\n ...asFindings('customization', customizationItems([...nav.issues, ...rendererIssues, ...reuseIssues])),\n ...asFindings('routes', routeIssues),\n ...asFindings('orphans', orphanIssues),\n ...asFindings('delivery', deliveryIssues),\n ]\n return orderFindings([...engine, ...contributed]).filter((finding) => canSeeFinding(finding, audience))\n }, [configIssues, nav.issues, rendererIssues, reuseIssues, routeIssues, orphanIssues, deliveryIssues, contributed, audience])\n}\n\nfunction asFindings(source: EngineFindingSource, items: readonly (NavIssueItem | DeliveryHealthFinding)[]): Finding[] {\n return items.map((item) => ({ id: `${source}:${item.key}`, source, ...item }))\n}\n\n/**\n * Who is reading (owner ruling (b)): an admin (`project.administer`) sees config and\n * deployment findings; a located finding shows to whoever can open its location. While the\n * role is still loading nobody is an admin and no view is visible, so a gated finding\n * appears once the role is known, never to the wrong reader first.\n */\nexport function useFindingAudience(project: Project): FindingAudience {\n const nav = useStudioNav(project)\n const role = useProjectRole(project.id)\n return useMemo(() => {\n const policy = projectRolePolicyFor(project.id)\n const ctx = { project, role }\n return {\n isAdmin: role !== null && roleGrantsCapability(role, 'project.administer', policy),\n canReadContent: role !== null && roleGrantsCapability(role, 'content.read', policy),\n views: nav.registry.views.map((view) => ({\n id: view.id,\n ...(view.recordKind !== undefined ? { recordKind: view.recordKind } : {}),\n visible: role !== null && isNavViewVisible(view, ctx),\n })),\n }\n }, [nav, role, project])\n}\n\n/**\n * EC-616 → EC-690: runs every view's always-on findings hook (`useIssues`) — whether or not\n * the view is open — and publishes what it returns. One component per view (each hook keeps\n * its own hook order), each its OWN publisher with the source `view:<id>`: before EC-690 every\n * probe published under the one source `view:`, so two such views overwrote each other and\n * one unmount retracted both (critic point 1).\n */\nexport function ViewIssuesProbes({ project }: { project: Project }) {\n const nav = useStudioNav(project)\n const issueViews = useMemo(() => nav.registry.views.filter((view) => view.useIssues), [nav])\n return (\n <>\n {issueViews.map((view) => (\n <ViewIssuesProbe key={view.id} view={view} project={project} />\n ))}\n </>\n )\n}\n\nfunction ViewIssuesProbe({ view, project }: { view: AppViewModule; project: Project }) {\n const issues = view.useIssues!({ project, projectId: project.id })\n useNavIssues(viewIssuesSource(view.id), issues)\n return null\n}\n\n/** The source a view's always-on findings publish under. */\nexport function viewIssuesSource(viewId: string): string {\n return `view:${viewId}`\n}\n\n/**\n * EC-479 — what the project has not declared but the studio cannot work without. Read from\n * the config holder alone (static per studio boot), so it needs no CMS provider and is\n * memoised once. The pure decision core is `projectConfigIssues`.\n */\nfunction useProjectConfigIssues(): CustomizationIssue[] {\n return useMemo(() => projectConfigIssues(resolveDeploymentCollections()), [])\n}\n\n/**\n * EC-467 — the project's reusable-elements config issues: a declaration that cannot\n * resolve, or an ambiguous inference the studio refuses to guess through. Static per studio\n * boot (registry + config collections + studio config). The pure core is `resolveReuse`.\n */\nfunction useReuseIssues(): CustomizationIssue[] {\n return useMemo(\n () =>\n resolveReuse(getStudioRegistry().registry.list(), resolveDeploymentCollections(), resolveDeploymentReuse())\n .issues,\n [],\n )\n}\n\n/**\n * EC-469 — the project's URL-map issues: two routes that match the same URLs, a redirect\n * chain that loops, a duplicate or unreadable redirect. Routes are CODE (config, static per\n * boot); redirects are editorial DATA (the rail, live) — the read shares the rail's own query\n * key, so a finding disappears the moment it is fixed; while that query is loading or failed\n * only the code-side conflicts are reported (the rail surfaces its own load error).\n */\nfunction useRouteIssues(projectId: string): NavIssueItem[] {\n const redirects = useRecordList(redirectRecordKind, projectId).data\n return useMemo(\n () =>\n routeIssueItems(\n routeValidationIssues(\n { routes: resolveDeploymentRoutes(), collections: resolveDeploymentCollections() },\n redirects ?? [],\n ),\n ),\n [redirects],\n )\n}\n\n/**\n * EC-606 — stored documents whose collection the project does not declare. Read from the\n * content session (subscribable, configured from the same hydration in both modes); the\n * server snapshot is empty — there is no session during SSR. The pure mapping is\n * {@link orphanedCollectionItems}.\n */\nfunction useOrphanedCollectionIssues(): NavIssueItem[] {\n const orphans = useSyncExternalStore(subscribeContentSession, contentSessionOrphans, serverContentSessionOrphans)\n return useMemo(() => orphanedCollectionItems(orphans), [orphans])\n}\n"]}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useCallback, useMemo, useState } from 'react';
|
|
2
2
|
import { defaultViewState } from '../lib/cms-views/index.js';
|
|
3
3
|
import { runBulkAction } from './cms/bulk-actions.js';
|
|
4
|
+
import { useOutcomeNotice } from './notifications/outcome-notice.js';
|
|
4
5
|
/** The default normalizer for an {@link OkResult}: a failure maps to a
|
|
5
6
|
* surfaced {@link OperationError}; a success maps to no error at all. */
|
|
6
7
|
export function normalizeOkResult(result) {
|
|
@@ -122,6 +123,10 @@ export function useRailViewSource(adapter) {
|
|
|
122
123
|
setViewStateRaw((prev) => (typeof update === 'function' ? update(prev) : update));
|
|
123
124
|
}, []);
|
|
124
125
|
const clearLastError = useCallback(() => setLastError(null), []);
|
|
126
|
+
// EC-690: a failed save/remove is ALSO an error notice of the shell's issues tray, for as long
|
|
127
|
+
// as this rail holds it — registered here, at its owner, once (the edit shell only renders it);
|
|
128
|
+
// dismissing it in the tray clears it here too.
|
|
129
|
+
useOutcomeNotice(lastError, clearLastError);
|
|
125
130
|
const save = useCallback(createErrorMappedWriter(rawSave, normalizeSaveResult, setLastError), [
|
|
126
131
|
rawSave,
|
|
127
132
|
normalizeSaveResult,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rail-view-source.js","sourceRoot":"","sources":["../../src/components/rail-view-source.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAWtD,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAA;AAOnD,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA;AA4BlD;0EAC0E;AAC1E,MAAM,UAAU,iBAAiB,CAAC,MAAgB;IAChD,OAAO,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,CAAA;AACxE,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,iBAAiB,CAC/B,MAA+B,EAC/B,cAAmC;IAEnC,MAAM,IAAI,GAA4B,EAAE,CAAA;IACxC,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QAClD,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,GAAG,CAAC;YAAE,IAAI,CAAC,GAAG,CAAC,GAAG,KAAK,CAAA;IACjD,CAAC;IACD,OAAO,IAAI,CAAA;AACb,CAAC;AAED;;gFAEgF;AAChF,MAAM,UAAU,uBAAuB,CACrC,KAAsD,EACtD,SAAqD,EACrD,QAAyC;IAEzC,+DAA+D;IAC/D,OAAO,CAAC,GAAS,EAAE,KAAc,EAAE,EAAE;QACnC,KAAK,KAAK,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE;YACrC,MAAM,KAAK,GAAG,SAAS,CAAC,MAAM,CAAC,CAAA;YAC/B,IAAI,KAAK;gBAAE,QAAQ,CAAC,KAAK,CAAC,CAAA;QAC5B,CAAC,CAAC,CAAA;IACJ,CAAC,CAAA;AACH,CAAC;AAmBD;;;;;;;;GAQG;AACH,MAAM,UAAU,iBAAiB,CAC/B,KAAoB,EACpB,GAAsB,EACtB,MAAkB,EAClB,QAAuB;IAEvB,OAAO,aAAa,CAClB;QACE,SAAS,EAAE,KAAK,CAAC,SAAS;QAC1B,OAAO,EAAE,KAAK,CAAC,OAAO;QACtB,QAAQ,EAAE,KAAK,CAAC,QAAQ;QACxB,qEAAqE;QACrE,yEAAyE;QACzE,wEAAwE;QACxE,yBAAyB;QACzB,UAAU,EAAE,GAAG,EAAE,CAAC,SAAS;QAC3B,WAAW,EAAE,GAAG,EAAE,CAAC,SAAS;QAC5B,QAAQ,EAAE,CAAC,UAAU,EAAE,GAAG,EAAE,EAAE,CAC5B,UAAU,KAAK,QAAQ;YACrB,CAAC,CAAC,KAAK,CAAC,eAAe,CAAC,GAAG,CAAC;YAC5B,CAAC,CAAC,OAAO,CAAC,OAAO,CAA2B;gBACxC,EAAE,EAAE,KAAK;gBACT,KAAK,EAAE;oBACL,IAAI,EAAE,SAAS;oBACf,OAAO,EAAE,QAAQ,UAAU,iCAAiC;iBAC7D;aACF,CAAC;KACT,EACD,KAAK,CAAC,YAAY,EAClB,GAAG,EACH,MAAM,EACN,QAAQ,CACT,CAAA;AACH,CAAC;AAuED;;;;;;GAMG;AACH,MAAM,UAAU,sBAAsB,CAAC,KAAkC;IACvE,MAAM,EACJ,SAAS,EACT,MAAM,EACN,YAAY,EACZ,YAAY,EACZ,SAAS,EACT,GAAG,EACH,OAAO,EACP,mBAAmB,EACnB,WAAW,EACX,UAAU,EACV,QAAQ,EACR,eAAe,EACf,WAAW,EACX,eAAe,EACf,QAAQ,EACR,MAAM,EACN,SAAS,EACT,cAAc,EACd,SAAS,EACT,YAAY,EACZ,IAAI,EACJ,MAAM,EACN,IAAI,GACL,GAAG,KAAK,CAAA;IACT,OAAO;QACL,YAAY,EAAE,MAAM,CAAC,EAAE;QACvB,MAAM;QACN,YAAY;QACZ,SAAS;QACT,YAAY;QACZ,YAAY,EAAE,YAAY,CAAC,OAAO;QAClC,SAAS;QACT,GAAG;QACH,OAAO;QACP,mBAAmB,EAAE,mBAAmB,IAAI,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC;QACtD,WAAW,EAAE,WAAW,IAAI,CAAC,MAAM,CAAC;QACpC,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACrC,GAAG,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC/C,eAAe,EAAE,eAAe,IAAI,CAAC,CAAC,KAAe,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,EAAE,QAAQ,IAAI,KAAK,CAAC;QACxF,GAAG,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACvC,GAAG,CAAC,eAAe,CAAC,CAAC,CAAC,EAAE,eAAe,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC/C,QAAQ;QACR,SAAS;QACT,cAAc;QACd,SAAS,EAAE,GAAG,EAAE,CAAC,SAAS;QAC1B,YAAY;QACZ,MAAM;QACN,4EAA4E;QAC5E,MAAM,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE;QAChC,MAAM;QACN,8EAA8E;QAC9E,GAAG,CAAC,YAAY,CAAC,IAAI,IAAI,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAC/C,CAAA;AACH,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,iBAAiB,CAC/B,OAAgD;IAEhD,MAAM,EACJ,SAAS,EACT,MAAM,EACN,YAAY,EACZ,YAAY,EACZ,SAAS,EACT,GAAG,EACH,OAAO,EACP,mBAAmB,EACnB,WAAW,EACX,UAAU,EACV,QAAQ,EACR,eAAe,EACf,WAAW,EACX,eAAe,EACf,QAAQ,EACR,MAAM,EACN,IAAI,EAAE,OAAO,EACb,MAAM,EAAE,SAAS,EACjB,UAAU,EACV,mBAAmB,EACnB,qBAAqB,GACtB,GAAG,OAAO,CAAA;IAEX,MAAM,CAAC,SAAS,EAAE,eAAe,CAAC,GAAG,QAAQ,CAAiB,GAAG,EAAE,CAAC,gBAAgB,CAAC,MAAM,CAAC,CAAC,CAAA;IAC7F,MAAM,CAAC,SAAS,EAAE,YAAY,CAAC,GAAG,QAAQ,CAAwB,IAAI,CAAC,CAAA;IAEvE,MAAM,YAAY,GAAG,WAAW,CAAuC,CAAC,MAAM,EAAE,EAAE;QAChF,eAAe,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,OAAO,MAAM,KAAK,UAAU,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAA;IACnF,CAAC,EAAE,EAAE,CAAC,CAAA;IACN,MAAM,cAAc,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC,CAAA;IAEhE,MAAM,IAAI,GAAG,WAAW,CAAC,uBAAuB,CAAC,OAAO,EAAE,mBAAmB,EAAE,YAAY,CAAC,EAAE;QAC5F,OAAO;QACP,mBAAmB;KACpB,CAAC,CAAA;IACF,MAAM,MAAM,GAAG,WAAW,CACxB,uBAAuB,CAAC,SAAS,EAAE,qBAAqB,EAAE,YAAY,CAAC,EACvE,CAAC,SAAS,EAAE,qBAAqB,CAAC,CACnC,CAAA;IAED,2EAA2E;IAC3E,uEAAuE;IACvE,6EAA6E;IAC7E,0EAA0E;IAC1E,MAAM,IAAI,GAAG,WAAW,CACtB,CAAC,GAAsB,EAAE,MAAkB,EAAE,QAAuB,EAAuB,EAAE,CAC3F,iBAAiB,CACf;QACE,YAAY,EAAE,MAAM,CAAC,EAAE;QACvB,SAAS,EAAE,SAAS,EAAE;QACtB,OAAO;QACP,QAAQ;QACR,eAAe,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE;YAC7B,MAAM,MAAM,GAAG,MAAM,CAAC,UAAU,IAAI,SAAS,CAAC,CAAC,GAAG,CAAC,CAAA;YACnD,MAAM,KAAK,GAAG,qBAAqB,CAAC,MAAM,CAAC,CAAA;YAC3C,OAAO,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,SAAS,EAAE,CAAA;QACtE,CAAC;KACF,EACD,GAAG,EACH,MAAM,EACN,QAAQ,CACT,EACH,CAAC,MAAM,CAAC,EAAE,EAAE,SAAS,EAAE,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,SAAS,EAAE,qBAAqB,CAAC,CACxF,CAAA;IAED,OAAO,OAAO,CACZ,GAAG,EAAE,CACH,sBAAsB,CAAC;QACrB,SAAS;QACT,MAAM;QACN,YAAY;QACZ,YAAY;QACZ,SAAS;QACT,GAAG;QACH,OAAO;QACP,mBAAmB;QACnB,WAAW;QACX,UAAU;QACV,QAAQ;QACR,eAAe;QACf,WAAW;QACX,eAAe;QACf,QAAQ;QACR,MAAM;QACN,SAAS;QACT,cAAc;QACd,SAAS;QACT,YAAY;QACZ,IAAI;QACJ,MAAM;QACN,IAAI;KACL,CAAC,EACJ;QACE,SAAS;QACT,MAAM;QACN,YAAY;QACZ,YAAY;QACZ,SAAS;QACT,GAAG;QACH,OAAO;QACP,mBAAmB;QACnB,WAAW;QACX,UAAU;QACV,QAAQ;QACR,eAAe;QACf,WAAW;QACX,eAAe;QACf,QAAQ;QACR,MAAM;QACN,SAAS;QACT,cAAc;QACd,SAAS;QACT,YAAY;QACZ,IAAI;QACJ,MAAM;QACN,IAAI;KACL,CACF,CAAA;AACH,CAAC","sourcesContent":["import { useCallback, useMemo, useState } from 'react'\nimport type {\n CmsDocument,\n CmsValidationIssue,\n CollectionDef,\n DocumentRef,\n FieldDef,\n LocaleConfig,\n} from '@elytracms/core/cms-core'\nimport type { OperationError, OperationResult } from '@elytracms/core/operations'\nimport type { ReferenceTarget } from '@elytracms/core/persistence'\nimport { defaultViewState } from '../lib/cms-views'\nimport type { TableViewState } from '../lib/cms-views'\nimport type {\n CollectionViewCapabilities,\n CollectionViewSource,\n FieldAccess,\n} from '../lib/collection-view/collection-view-source'\nimport { runBulkAction } from './cms/bulk-actions'\nimport type { BulkAction, BulkReport, BulkSetField } from './cms/bulk-actions'\n\n/**\n * The shared scaffold behind the primitive-rail `CollectionViewSource` hooks\n * (EC-327) — assets, users, redirects. All three hand-rolled identical\n * `viewState`/`lastError` state, an identical `setViewState` updater-fn\n * wrapper, an identical `if (!result.ok) setLastError(...)` error-mapping\n * shape (assets' `remove` diverges — see {@link OkResult} below), and a large\n * final `useMemo` assembling the same ~20-field {@link CollectionViewSource}.\n * The 4th rail (`useCmsCollectionViewSource`) does NOT reduplicate this — it\n * delegates to the ambient `CmsStore` — so this scaffold is scoped to the\n * three self-contained rails, not a fully generic replacement for the CMS\n * adapter.\n *\n * Genuinely divergent per-rail behaviour is a PARAMETER, never hidden here:\n * users' principal-based `fieldAccess`, users' no-op vs. redirects/assets'\n * registry-driven `validate`, redirects-only `create`, and — the one save/\n * remove result-shape mismatch — assets' `remove()` returns an\n * `outcome`-discriminated result where users/redirects use `{ ok }`; each rail\n * supplies its own `normalizeSaveResult`/`normalizeRemoveResult` to reconcile\n * whichever shape its store returns into `lastError`.\n */\n\n/** The common `{ ok: true; … } | { ok: false; message }` store-result shape —\n * users' and redirects' save/remove, and assets' `updateMetadata` (save). */\nexport type OkResult = { ok: true } | { ok: false; message: string }\n\n/** The default normalizer for an {@link OkResult}: a failure maps to a\n * surfaced {@link OperationError}; a success maps to no error at all. */\nexport function normalizeOkResult(result: OkResult): OperationError | null {\n return result.ok ? null : { code: 'unknown', message: result.message }\n}\n\n/**\n * Project a lens document's values into a meta bag (EC-311/EC-240/EC-303):\n * every entry whose key is NOT one of the rail's canonical/identity field\n * names round-trips through the record's open `meta` bag. Shared by the users\n * (`IDENTITY_FIELDS`) and redirects (`CANONICAL_REDIRECT_FIELDS`) rails — the\n * assets rail's projection is schema-FIELDS-driven inclusion (iterating the\n * schema's declared fields, not the document's value keys), a genuinely\n * different shape that stays bespoke in `assets-collection-view-source.ts`.\n */\nexport function metaBagProjection(\n values: Record<string, unknown>,\n canonicalNames: ReadonlySet<string>,\n): Record<string, unknown> {\n const meta: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(values)) {\n if (!canonicalNames.has(key)) meta[key] = value\n }\n return meta\n}\n\n/** Wrap a raw store write (save/remove) so a normalized failure surfaces via\n * `setError` — the factory's error-mapping plumbing, pulled out as a plain\n * function (no hook) so it is unit-testable without a `renderHook` harness. */\nexport function createErrorMappedWriter<TArg, TResult, TExtra = never>(\n write: (arg: TArg, extra?: TExtra) => Promise<TResult>,\n normalize: (result: TResult) => OperationError | null,\n setError: (error: OperationError) => void,\n): (arg: TArg, extra?: TExtra) => void {\n // `extra` (EC-616): a record save's base, forwarded untouched.\n return (arg: TArg, extra?: TExtra) => {\n void write(arg, extra).then((result) => {\n const error = normalize(result)\n if (error) setError(error)\n })\n }\n}\n\n/** The store-facing input of {@link runRailBulkAction} — plain data/functions,\n * so the rail bulk path is unit-testable without a hook harness. */\nexport interface RailBulkInput {\n collectionId: string\n /** Current rail rows (lens documents) — the runner's lookup + validation input. */\n documents: readonly CmsDocument[]\n titleOf(ref: DocumentRef): string\n validate(doc: CmsDocument): CmsValidationIssue[]\n /**\n * Dispatch ONE already-confirmed delete (the EC-173 reference dialog ran\n * before anything reaches this) and reconcile the rail store's result shape\n * into an operation-shaped outcome — a failure here is a visible per-item\n * failure in the report, never a swallowed one.\n */\n removeConfirmed(ref: DocumentRef): Promise<OperationResult<unknown>>\n}\n\n/**\n * The primitive rails' bulk runner (EC-433): the SAME `runBulkAction` machinery\n * the CMS rail uses ([[client-data-flow-one-cache]] — no parallel data flow),\n * wired to the rail's own store delete. Only `delete` is dispatchable — the\n * rails have no publish lifecycle and no bulk set-field — and an unsupported\n * action fails each item HONESTLY in the report rather than silently no-oping\n * (the exact bug this replaces: the assets rail's bulk delete confirmed into a\n * missing `source.bulk` and dropped the whole action on the floor).\n */\nexport function runRailBulkAction(\n input: RailBulkInput,\n ids: readonly string[],\n action: BulkAction,\n setField?: BulkSetField,\n): Promise<BulkReport> {\n return runBulkAction(\n {\n documents: input.documents,\n titleOf: input.titleOf,\n validate: input.validate,\n // A rail store patches its own cache/state inside a succeeded remove\n // (assets/users/redirects all do), and publish/unpublish/set-field never\n // succeed here (dispatch rejects them), so the session-apply hooks have\n // nothing left to apply.\n applyState: () => undefined,\n applyDelete: () => undefined,\n dispatch: (bulkAction, ref) =>\n bulkAction === 'delete'\n ? input.removeConfirmed(ref)\n : Promise.resolve<OperationResult<unknown>>({\n ok: false,\n error: {\n code: 'unknown',\n message: `Bulk ${bulkAction} is not supported in this view.`,\n },\n }),\n },\n input.collectionId,\n ids,\n action,\n setField,\n )\n}\n\n/** The fields shared by {@link RailAdapter} and the pure assembly input —\n * everything the factory copies straight onto the assembled source, or\n * defaults when a rail omits it. */\ninterface RailViewSourceCore {\n readonly projectId: string\n readonly schema: CollectionDef\n readonly capabilities: CollectionViewCapabilities\n readonly localeConfig: LocaleConfig\n documents(): CmsDocument[]\n get(ref: DocumentRef): CmsDocument | undefined\n titleOf(ref: DocumentRef): string\n /** Default `() => []` — none of the three rails carry cross-collection relation fields (v1). */\n referenceCandidates?(collectionId: string): CmsDocument[]\n /** Default `[schema]` — the rail's own schema is the only collection in scope. */\n collections?: readonly CollectionDef[]\n detailPath?(id: string): string\n listPath?: string\n /** Default `field => field.form?.readOnly ?? false`. */\n isFieldReadOnly?(field: FieldDef): boolean\n /** Principal-based tri-state — only the users rail supplies one. */\n fieldAccess?(field: FieldDef, record?: CmsDocument): FieldAccess\n /**\n * How this rail's records are addressed in the reference index (EC-433) —\n * only the assets rail supplies one (`{ type: 'asset' }`); absent ⇒ the\n * confirm dialog's default document-shaped lookup.\n */\n referenceTarget?(ref: DocumentRef): ReferenceTarget\n validate(doc: CmsDocument): CmsValidationIssue[]\n create(): DocumentRef\n}\n\n/** A rail's input to {@link useRailViewSource}: its store-facing reads/writes,\n * capability profile, and lens projections. `TSaveResult`/`TRemoveResult` are\n * each rail's own store-result shape — reconciled to `lastError` by the\n * rail's own `normalizeSaveResult`/`normalizeRemoveResult`. */\nexport interface RailAdapter<TSaveResult, TRemoveResult> extends RailViewSourceCore {\n /** `base`: the record as the editor opened it (EC-616 — see `RecordWrites.save`). */\n save(doc: CmsDocument, base?: CmsDocument): Promise<TSaveResult>\n remove(ref: DocumentRef): Promise<TRemoveResult>\n /**\n * The delete the BULK path dispatches (EC-433) — always post-confirmation\n * (the EC-173 reference dialog ran first). Defaults to {@link remove}; the\n * assets rail overrides it to pass `confirmInUse: true` so a confirmed bulk\n * delete of a referenced asset actually deletes (mirroring the detail\n * surface's confirmed delete) instead of re-blocking on the usage gate.\n */\n bulkRemove?(ref: DocumentRef): Promise<TRemoveResult>\n normalizeSaveResult(result: TSaveResult): OperationError | null\n normalizeRemoveResult(result: TRemoveResult): OperationError | null\n}\n\n/** The already-resolved state + error-wrapped writes {@link useRailViewSource}\n * hands to the pure assembly function below. */\ninterface RailViewSourceAssemblyInput extends RailViewSourceCore {\n lastError: OperationError | null\n clearLastError(): void\n viewState: TableViewState\n setViewState: CollectionViewSource['setViewState']\n save(doc: CmsDocument, base?: CmsDocument): void\n remove(ref: DocumentRef): void\n /**\n * The rail bulk runner (EC-433). Assembled onto the source ONLY when\n * `capabilities.bulk` is set — the `CollectionViewSource` contract is\n * \"method present iff the flag is\" — so a bulk-less rail's list never\n * half-offers the affordance.\n */\n bulk?: CollectionViewSource['bulk']\n}\n\n/**\n * Pure assembly of a {@link CollectionViewSource} from a rail's resolved\n * fields + state. Exported (and taking plain data/functions, no hook) so it is\n * directly unit-testable — this workspace runs vitest in `environment: 'node'`\n * (no `renderHook`); {@link useRailViewSource} is a thin `useState`/`useMemo`\n * wrapper around it.\n */\nexport function assembleRailViewSource(input: RailViewSourceAssemblyInput): CollectionViewSource {\n const {\n projectId,\n schema,\n capabilities,\n localeConfig,\n documents,\n get,\n titleOf,\n referenceCandidates,\n collections,\n detailPath,\n listPath,\n isFieldReadOnly,\n fieldAccess,\n referenceTarget,\n validate,\n create,\n lastError,\n clearLastError,\n viewState,\n setViewState,\n save,\n remove,\n bulk,\n } = input\n return {\n collectionId: schema.id,\n schema,\n capabilities,\n projectId,\n localeConfig,\n activeLocale: localeConfig.default,\n documents,\n get,\n titleOf,\n referenceCandidates: referenceCandidates ?? (() => []),\n collections: collections ?? [schema],\n ...(detailPath ? { detailPath } : {}),\n ...(listPath !== undefined ? { listPath } : {}),\n isFieldReadOnly: isFieldReadOnly ?? ((field: FieldDef) => field.form?.readOnly ?? false),\n ...(fieldAccess ? { fieldAccess } : {}),\n ...(referenceTarget ? { referenceTarget } : {}),\n validate,\n lastError,\n clearLastError,\n viewState: () => viewState,\n setViewState,\n create,\n // EC-595 A2: a rail record is written whole, on the editor's explicit Save.\n writes: { kind: 'record', save },\n remove,\n // Present iff the capability flag is set (the CollectionViewSource contract).\n ...(capabilities.bulk && bulk ? { bulk } : {}),\n }\n}\n\n/**\n * The shared scaffold factory (EC-327) behind the assets/users/redirects rail\n * `CollectionViewSource` hooks: `viewState`/`lastError` state, the\n * `setViewState` updater-fn wrapper, the error-mapped save/remove wiring (via\n * the rail's own result normalizer), and the final assembled source. See the\n * module doc for what stays a per-rail parameter rather than being absorbed\n * here.\n */\nexport function useRailViewSource<TSaveResult, TRemoveResult>(\n adapter: RailAdapter<TSaveResult, TRemoveResult>,\n): CollectionViewSource {\n const {\n projectId,\n schema,\n capabilities,\n localeConfig,\n documents,\n get,\n titleOf,\n referenceCandidates,\n collections,\n detailPath,\n listPath,\n isFieldReadOnly,\n fieldAccess,\n referenceTarget,\n validate,\n create,\n save: rawSave,\n remove: rawRemove,\n bulkRemove,\n normalizeSaveResult,\n normalizeRemoveResult,\n } = adapter\n\n const [viewState, setViewStateRaw] = useState<TableViewState>(() => defaultViewState(schema))\n const [lastError, setLastError] = useState<OperationError | null>(null)\n\n const setViewState = useCallback<CollectionViewSource['setViewState']>((update) => {\n setViewStateRaw((prev) => (typeof update === 'function' ? update(prev) : update))\n }, [])\n const clearLastError = useCallback(() => setLastError(null), [])\n\n const save = useCallback(createErrorMappedWriter(rawSave, normalizeSaveResult, setLastError), [\n rawSave,\n normalizeSaveResult,\n ])\n const remove = useCallback(\n createErrorMappedWriter(rawRemove, normalizeRemoveResult, setLastError),\n [rawRemove, normalizeRemoveResult],\n )\n\n // The rail bulk runner (EC-433): the shared `runBulkAction` machinery over\n // the rail's own confirmed delete, its store result reconciled into an\n // operation-shaped outcome (a failing remove is a visible per-item failure).\n // Assembled onto the source only when `capabilities.bulk` is set (below).\n const bulk = useCallback(\n (ids: readonly string[], action: BulkAction, setField?: BulkSetField): Promise<BulkReport> =>\n runRailBulkAction(\n {\n collectionId: schema.id,\n documents: documents(),\n titleOf,\n validate,\n removeConfirmed: async (ref) => {\n const result = await (bulkRemove ?? rawRemove)(ref)\n const error = normalizeRemoveResult(result)\n return error ? { ok: false, error } : { ok: true, value: undefined }\n },\n },\n ids,\n action,\n setField,\n ),\n [schema.id, documents, titleOf, validate, bulkRemove, rawRemove, normalizeRemoveResult],\n )\n\n return useMemo(\n () =>\n assembleRailViewSource({\n projectId,\n schema,\n capabilities,\n localeConfig,\n documents,\n get,\n titleOf,\n referenceCandidates,\n collections,\n detailPath,\n listPath,\n isFieldReadOnly,\n fieldAccess,\n referenceTarget,\n validate,\n create,\n lastError,\n clearLastError,\n viewState,\n setViewState,\n save,\n remove,\n bulk,\n }),\n [\n projectId,\n schema,\n capabilities,\n localeConfig,\n documents,\n get,\n titleOf,\n referenceCandidates,\n collections,\n detailPath,\n listPath,\n isFieldReadOnly,\n fieldAccess,\n referenceTarget,\n validate,\n create,\n lastError,\n clearLastError,\n viewState,\n setViewState,\n save,\n remove,\n bulk,\n ],\n )\n}\n"]}
|
|
1
|
+
{"version":3,"file":"rail-view-source.js","sourceRoot":"","sources":["../../src/components/rail-view-source.ts"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useMemo, useState } from 'react'\nimport type {\n CmsDocument,\n CmsValidationIssue,\n CollectionDef,\n DocumentRef,\n FieldDef,\n LocaleConfig,\n} from '@elytracms/core/cms-core'\nimport type { OperationError, OperationResult } from '@elytracms/core/operations'\nimport type { ReferenceTarget } from '@elytracms/core/persistence'\nimport { defaultViewState } from '../lib/cms-views'\nimport type { TableViewState } from '../lib/cms-views'\nimport type {\n CollectionViewCapabilities,\n CollectionViewSource,\n FieldAccess,\n} from '../lib/collection-view/collection-view-source'\nimport { runBulkAction } from './cms/bulk-actions'\nimport { useOutcomeNotice } from './notifications/outcome-notice'\nimport type { BulkAction, BulkReport, BulkSetField } from './cms/bulk-actions'\n\n/**\n * The shared scaffold behind the primitive-rail `CollectionViewSource` hooks\n * (EC-327) — assets, users, redirects. All three hand-rolled identical\n * `viewState`/`lastError` state, an identical `setViewState` updater-fn\n * wrapper, an identical `if (!result.ok) setLastError(...)` error-mapping\n * shape (assets' `remove` diverges — see {@link OkResult} below), and a large\n * final `useMemo` assembling the same ~20-field {@link CollectionViewSource}.\n * The 4th rail (`useCmsCollectionViewSource`) does NOT reduplicate this — it\n * delegates to the ambient `CmsStore` — so this scaffold is scoped to the\n * three self-contained rails, not a fully generic replacement for the CMS\n * adapter.\n *\n * Genuinely divergent per-rail behaviour is a PARAMETER, never hidden here:\n * users' principal-based `fieldAccess`, users' no-op vs. redirects/assets'\n * registry-driven `validate`, redirects-only `create`, and — the one save/\n * remove result-shape mismatch — assets' `remove()` returns an\n * `outcome`-discriminated result where users/redirects use `{ ok }`; each rail\n * supplies its own `normalizeSaveResult`/`normalizeRemoveResult` to reconcile\n * whichever shape its store returns into `lastError`.\n */\n\n/** The common `{ ok: true; … } | { ok: false; message }` store-result shape —\n * users' and redirects' save/remove, and assets' `updateMetadata` (save). */\nexport type OkResult = { ok: true } | { ok: false; message: string }\n\n/** The default normalizer for an {@link OkResult}: a failure maps to a\n * surfaced {@link OperationError}; a success maps to no error at all. */\nexport function normalizeOkResult(result: OkResult): OperationError | null {\n return result.ok ? null : { code: 'unknown', message: result.message }\n}\n\n/**\n * Project a lens document's values into a meta bag (EC-311/EC-240/EC-303):\n * every entry whose key is NOT one of the rail's canonical/identity field\n * names round-trips through the record's open `meta` bag. Shared by the users\n * (`IDENTITY_FIELDS`) and redirects (`CANONICAL_REDIRECT_FIELDS`) rails — the\n * assets rail's projection is schema-FIELDS-driven inclusion (iterating the\n * schema's declared fields, not the document's value keys), a genuinely\n * different shape that stays bespoke in `assets-collection-view-source.ts`.\n */\nexport function metaBagProjection(\n values: Record<string, unknown>,\n canonicalNames: ReadonlySet<string>,\n): Record<string, unknown> {\n const meta: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(values)) {\n if (!canonicalNames.has(key)) meta[key] = value\n }\n return meta\n}\n\n/** Wrap a raw store write (save/remove) so a normalized failure surfaces via\n * `setError` — the factory's error-mapping plumbing, pulled out as a plain\n * function (no hook) so it is unit-testable without a `renderHook` harness. */\nexport function createErrorMappedWriter<TArg, TResult, TExtra = never>(\n write: (arg: TArg, extra?: TExtra) => Promise<TResult>,\n normalize: (result: TResult) => OperationError | null,\n setError: (error: OperationError) => void,\n): (arg: TArg, extra?: TExtra) => void {\n // `extra` (EC-616): a record save's base, forwarded untouched.\n return (arg: TArg, extra?: TExtra) => {\n void write(arg, extra).then((result) => {\n const error = normalize(result)\n if (error) setError(error)\n })\n }\n}\n\n/** The store-facing input of {@link runRailBulkAction} — plain data/functions,\n * so the rail bulk path is unit-testable without a hook harness. */\nexport interface RailBulkInput {\n collectionId: string\n /** Current rail rows (lens documents) — the runner's lookup + validation input. */\n documents: readonly CmsDocument[]\n titleOf(ref: DocumentRef): string\n validate(doc: CmsDocument): CmsValidationIssue[]\n /**\n * Dispatch ONE already-confirmed delete (the EC-173 reference dialog ran\n * before anything reaches this) and reconcile the rail store's result shape\n * into an operation-shaped outcome — a failure here is a visible per-item\n * failure in the report, never a swallowed one.\n */\n removeConfirmed(ref: DocumentRef): Promise<OperationResult<unknown>>\n}\n\n/**\n * The primitive rails' bulk runner (EC-433): the SAME `runBulkAction` machinery\n * the CMS rail uses ([[client-data-flow-one-cache]] — no parallel data flow),\n * wired to the rail's own store delete. Only `delete` is dispatchable — the\n * rails have no publish lifecycle and no bulk set-field — and an unsupported\n * action fails each item HONESTLY in the report rather than silently no-oping\n * (the exact bug this replaces: the assets rail's bulk delete confirmed into a\n * missing `source.bulk` and dropped the whole action on the floor).\n */\nexport function runRailBulkAction(\n input: RailBulkInput,\n ids: readonly string[],\n action: BulkAction,\n setField?: BulkSetField,\n): Promise<BulkReport> {\n return runBulkAction(\n {\n documents: input.documents,\n titleOf: input.titleOf,\n validate: input.validate,\n // A rail store patches its own cache/state inside a succeeded remove\n // (assets/users/redirects all do), and publish/unpublish/set-field never\n // succeed here (dispatch rejects them), so the session-apply hooks have\n // nothing left to apply.\n applyState: () => undefined,\n applyDelete: () => undefined,\n dispatch: (bulkAction, ref) =>\n bulkAction === 'delete'\n ? input.removeConfirmed(ref)\n : Promise.resolve<OperationResult<unknown>>({\n ok: false,\n error: {\n code: 'unknown',\n message: `Bulk ${bulkAction} is not supported in this view.`,\n },\n }),\n },\n input.collectionId,\n ids,\n action,\n setField,\n )\n}\n\n/** The fields shared by {@link RailAdapter} and the pure assembly input —\n * everything the factory copies straight onto the assembled source, or\n * defaults when a rail omits it. */\ninterface RailViewSourceCore {\n readonly projectId: string\n readonly schema: CollectionDef\n readonly capabilities: CollectionViewCapabilities\n readonly localeConfig: LocaleConfig\n documents(): CmsDocument[]\n get(ref: DocumentRef): CmsDocument | undefined\n titleOf(ref: DocumentRef): string\n /** Default `() => []` — none of the three rails carry cross-collection relation fields (v1). */\n referenceCandidates?(collectionId: string): CmsDocument[]\n /** Default `[schema]` — the rail's own schema is the only collection in scope. */\n collections?: readonly CollectionDef[]\n detailPath?(id: string): string\n listPath?: string\n /** Default `field => field.form?.readOnly ?? false`. */\n isFieldReadOnly?(field: FieldDef): boolean\n /** Principal-based tri-state — only the users rail supplies one. */\n fieldAccess?(field: FieldDef, record?: CmsDocument): FieldAccess\n /**\n * How this rail's records are addressed in the reference index (EC-433) —\n * only the assets rail supplies one (`{ type: 'asset' }`); absent ⇒ the\n * confirm dialog's default document-shaped lookup.\n */\n referenceTarget?(ref: DocumentRef): ReferenceTarget\n validate(doc: CmsDocument): CmsValidationIssue[]\n create(): DocumentRef\n}\n\n/** A rail's input to {@link useRailViewSource}: its store-facing reads/writes,\n * capability profile, and lens projections. `TSaveResult`/`TRemoveResult` are\n * each rail's own store-result shape — reconciled to `lastError` by the\n * rail's own `normalizeSaveResult`/`normalizeRemoveResult`. */\nexport interface RailAdapter<TSaveResult, TRemoveResult> extends RailViewSourceCore {\n /** `base`: the record as the editor opened it (EC-616 — see `RecordWrites.save`). */\n save(doc: CmsDocument, base?: CmsDocument): Promise<TSaveResult>\n remove(ref: DocumentRef): Promise<TRemoveResult>\n /**\n * The delete the BULK path dispatches (EC-433) — always post-confirmation\n * (the EC-173 reference dialog ran first). Defaults to {@link remove}; the\n * assets rail overrides it to pass `confirmInUse: true` so a confirmed bulk\n * delete of a referenced asset actually deletes (mirroring the detail\n * surface's confirmed delete) instead of re-blocking on the usage gate.\n */\n bulkRemove?(ref: DocumentRef): Promise<TRemoveResult>\n normalizeSaveResult(result: TSaveResult): OperationError | null\n normalizeRemoveResult(result: TRemoveResult): OperationError | null\n}\n\n/** The already-resolved state + error-wrapped writes {@link useRailViewSource}\n * hands to the pure assembly function below. */\ninterface RailViewSourceAssemblyInput extends RailViewSourceCore {\n lastError: OperationError | null\n clearLastError(): void\n viewState: TableViewState\n setViewState: CollectionViewSource['setViewState']\n save(doc: CmsDocument, base?: CmsDocument): void\n remove(ref: DocumentRef): void\n /**\n * The rail bulk runner (EC-433). Assembled onto the source ONLY when\n * `capabilities.bulk` is set — the `CollectionViewSource` contract is\n * \"method present iff the flag is\" — so a bulk-less rail's list never\n * half-offers the affordance.\n */\n bulk?: CollectionViewSource['bulk']\n}\n\n/**\n * Pure assembly of a {@link CollectionViewSource} from a rail's resolved\n * fields + state. Exported (and taking plain data/functions, no hook) so it is\n * directly unit-testable — this workspace runs vitest in `environment: 'node'`\n * (no `renderHook`); {@link useRailViewSource} is a thin `useState`/`useMemo`\n * wrapper around it.\n */\nexport function assembleRailViewSource(input: RailViewSourceAssemblyInput): CollectionViewSource {\n const {\n projectId,\n schema,\n capabilities,\n localeConfig,\n documents,\n get,\n titleOf,\n referenceCandidates,\n collections,\n detailPath,\n listPath,\n isFieldReadOnly,\n fieldAccess,\n referenceTarget,\n validate,\n create,\n lastError,\n clearLastError,\n viewState,\n setViewState,\n save,\n remove,\n bulk,\n } = input\n return {\n collectionId: schema.id,\n schema,\n capabilities,\n projectId,\n localeConfig,\n activeLocale: localeConfig.default,\n documents,\n get,\n titleOf,\n referenceCandidates: referenceCandidates ?? (() => []),\n collections: collections ?? [schema],\n ...(detailPath ? { detailPath } : {}),\n ...(listPath !== undefined ? { listPath } : {}),\n isFieldReadOnly: isFieldReadOnly ?? ((field: FieldDef) => field.form?.readOnly ?? false),\n ...(fieldAccess ? { fieldAccess } : {}),\n ...(referenceTarget ? { referenceTarget } : {}),\n validate,\n lastError,\n clearLastError,\n viewState: () => viewState,\n setViewState,\n create,\n // EC-595 A2: a rail record is written whole, on the editor's explicit Save.\n writes: { kind: 'record', save },\n remove,\n // Present iff the capability flag is set (the CollectionViewSource contract).\n ...(capabilities.bulk && bulk ? { bulk } : {}),\n }\n}\n\n/**\n * The shared scaffold factory (EC-327) behind the assets/users/redirects rail\n * `CollectionViewSource` hooks: `viewState`/`lastError` state, the\n * `setViewState` updater-fn wrapper, the error-mapped save/remove wiring (via\n * the rail's own result normalizer), and the final assembled source. See the\n * module doc for what stays a per-rail parameter rather than being absorbed\n * here.\n */\nexport function useRailViewSource<TSaveResult, TRemoveResult>(\n adapter: RailAdapter<TSaveResult, TRemoveResult>,\n): CollectionViewSource {\n const {\n projectId,\n schema,\n capabilities,\n localeConfig,\n documents,\n get,\n titleOf,\n referenceCandidates,\n collections,\n detailPath,\n listPath,\n isFieldReadOnly,\n fieldAccess,\n referenceTarget,\n validate,\n create,\n save: rawSave,\n remove: rawRemove,\n bulkRemove,\n normalizeSaveResult,\n normalizeRemoveResult,\n } = adapter\n\n const [viewState, setViewStateRaw] = useState<TableViewState>(() => defaultViewState(schema))\n const [lastError, setLastError] = useState<OperationError | null>(null)\n\n const setViewState = useCallback<CollectionViewSource['setViewState']>((update) => {\n setViewStateRaw((prev) => (typeof update === 'function' ? update(prev) : update))\n }, [])\n const clearLastError = useCallback(() => setLastError(null), [])\n // EC-690: a failed save/remove is ALSO an error notice of the shell's issues tray, for as long\n // as this rail holds it — registered here, at its owner, once (the edit shell only renders it);\n // dismissing it in the tray clears it here too.\n useOutcomeNotice(lastError, clearLastError)\n\n const save = useCallback(createErrorMappedWriter(rawSave, normalizeSaveResult, setLastError), [\n rawSave,\n normalizeSaveResult,\n ])\n const remove = useCallback(\n createErrorMappedWriter(rawRemove, normalizeRemoveResult, setLastError),\n [rawRemove, normalizeRemoveResult],\n )\n\n // The rail bulk runner (EC-433): the shared `runBulkAction` machinery over\n // the rail's own confirmed delete, its store result reconciled into an\n // operation-shaped outcome (a failing remove is a visible per-item failure).\n // Assembled onto the source only when `capabilities.bulk` is set (below).\n const bulk = useCallback(\n (ids: readonly string[], action: BulkAction, setField?: BulkSetField): Promise<BulkReport> =>\n runRailBulkAction(\n {\n collectionId: schema.id,\n documents: documents(),\n titleOf,\n validate,\n removeConfirmed: async (ref) => {\n const result = await (bulkRemove ?? rawRemove)(ref)\n const error = normalizeRemoveResult(result)\n return error ? { ok: false, error } : { ok: true, value: undefined }\n },\n },\n ids,\n action,\n setField,\n ),\n [schema.id, documents, titleOf, validate, bulkRemove, rawRemove, normalizeRemoveResult],\n )\n\n return useMemo(\n () =>\n assembleRailViewSource({\n projectId,\n schema,\n capabilities,\n localeConfig,\n documents,\n get,\n titleOf,\n referenceCandidates,\n collections,\n detailPath,\n listPath,\n isFieldReadOnly,\n fieldAccess,\n referenceTarget,\n validate,\n create,\n lastError,\n clearLastError,\n viewState,\n setViewState,\n save,\n remove,\n bulk,\n }),\n [\n projectId,\n schema,\n capabilities,\n localeConfig,\n documents,\n get,\n titleOf,\n referenceCandidates,\n collections,\n detailPath,\n listPath,\n isFieldReadOnly,\n fieldAccess,\n referenceTarget,\n validate,\n create,\n lastError,\n clearLastError,\n viewState,\n setViewState,\n save,\n remove,\n bulk,\n ],\n )\n}\n"]}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ReactNode } from 'react';
|
|
2
|
-
import type { CollectionDef } from '@elytracms/core/cms-core';
|
|
2
|
+
import type { CmsDocument, CmsValidationIssue, CollectionDef } from '@elytracms/core/cms-core';
|
|
3
3
|
import type { AnyRecordKind, RecordEntryOf, RecordWriteOf } from '@elytracms/core/persistence';
|
|
4
4
|
import type { CollectionViewCapabilities, CollectionViewSource } from '../../lib/collection-view/collection-view-source.js';
|
|
5
5
|
/** The default capability profile of a record rail: create, edit, remove, bulk delete — content, not administration. */
|
|
@@ -8,7 +8,7 @@ export declare const RECORD_RAIL_CAPABILITIES: CollectionViewCapabilities;
|
|
|
8
8
|
export declare function recordListKey(kind: AnyRecordKind, projectId: string): readonly [string, string, string];
|
|
9
9
|
/**
|
|
10
10
|
* A kind's records in a project, read through the shared operations cache — the ONE query
|
|
11
|
-
* a rail and any reader outside it (the
|
|
11
|
+
* a rail and any reader outside it (the issues tray's URL-map validation reads redirects)
|
|
12
12
|
* share. One key, one cache entry: a save's `cache.patch` updates both at once.
|
|
13
13
|
*/
|
|
14
14
|
export declare function useRecordList<K extends AnyRecordKind>(kind: K, projectId: string): import("@tanstack/react-query").UseQueryResult<RecordEntryOf<K>[]>;
|
|
@@ -55,6 +55,14 @@ export declare function RecordRailProvider<K extends AnyRecordKind>({ kind, proj
|
|
|
55
55
|
* applies the defaults itself for a write that leaves them out (EC-577).
|
|
56
56
|
*/
|
|
57
57
|
export declare function recordDraftColumns(kind: AnyRecordKind): Record<string, unknown>;
|
|
58
|
+
/**
|
|
59
|
+
* The set findings of `doc` as the editor sees it NOW: the stored set with this record's
|
|
60
|
+
* columns replaced by the draft's, so fixing a duplicate `from` clears the finding as you
|
|
61
|
+
* type. An unsaved "New" draft joins the set as an extra row; it is only ever told about a
|
|
62
|
+
* DUPLICATE (a half-typed draft is not an "unreadable stored redirect" — its required fields
|
|
63
|
+
* report that, field by field). Pure; exported for the tests.
|
|
64
|
+
*/
|
|
65
|
+
export declare function recordSetIssues(kind: AnyRecordKind, records: readonly Record<string, unknown>[], doc: CmsDocument): CmsValidationIssue[];
|
|
58
66
|
/** How a rail addresses its list and one record's detail (the view's own route segment). */
|
|
59
67
|
export interface RecordRailPaths {
|
|
60
68
|
listPath: string;
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { useCallback, useMemo, useRef, useState } from 'react';
|
|
3
3
|
import { createCollectionRegistry, parseLocaleConfig, validateDocument } from '@elytracms/core/cms-core';
|
|
4
|
+
import { redirectRecordKind } from '@elytracms/core/persistence';
|
|
5
|
+
import { routeValidationIssues } from '../../lib/routes/route-issues.js';
|
|
6
|
+
import { resolveDeploymentCollections, resolveDeploymentRoutes } from '../../lib/workspace/index.js';
|
|
4
7
|
import { createUuidIdFactory } from '@elytracms/core/determinism';
|
|
5
8
|
import { getStudioOperations } from '../../lib/operations/index.js';
|
|
6
9
|
import { listStoreState, useOperationCache, useOperationQuery } from '../../lib/query/index.js';
|
|
@@ -9,7 +12,7 @@ import { localizedLabel, translate } from '../../lib/i18n/index.js';
|
|
|
9
12
|
import { normalizeOkResult, useRailViewSource } from '../rail-view-source.js';
|
|
10
13
|
import { RailStoreStatus } from '../rail-store-status.js';
|
|
11
14
|
import { CollectionViewSurface } from '../collection-view/collection-view-surface.js';
|
|
12
|
-
import { lensDocumentToRecordWrite, recordToLensDocument } from './record-lens.js';
|
|
15
|
+
import { lensDocumentToRecordWrite, recordColumnNames, recordToLensDocument } from './record-lens.js';
|
|
13
16
|
/**
|
|
14
17
|
* EC-616 — the generic RECORD RAIL: a record kind (`defineRecordKind`) on the portable
|
|
15
18
|
* list/edit surfaces (AD-19), read and written through the operations chokepoint
|
|
@@ -17,7 +20,7 @@ import { lensDocumentToRecordWrite, recordToLensDocument } from './record-lens.j
|
|
|
17
20
|
* the engine's redirects rail is exactly this over `redirectRecordKind`.
|
|
18
21
|
*
|
|
19
22
|
* - **Store** ({@link RecordRailProvider}): the kind's records through ONE query key per
|
|
20
|
-
* kind and project ({@link useRecordList} — the
|
|
23
|
+
* kind and project ({@link useRecordList} — the issues tray and the rail share it); the
|
|
21
24
|
* "New" flow keeps an unsaved record as a LOCAL draft until the first explicit save, so
|
|
22
25
|
* an abandoned "New" never writes a stub and never shows in the list.
|
|
23
26
|
* - **Source** ({@link useRecordCollectionViewSource}): the lens projections
|
|
@@ -48,7 +51,7 @@ export function recordListKey(kind, projectId) {
|
|
|
48
51
|
}
|
|
49
52
|
/**
|
|
50
53
|
* A kind's records in a project, read through the shared operations cache — the ONE query
|
|
51
|
-
* a rail and any reader outside it (the
|
|
54
|
+
* a rail and any reader outside it (the issues tray's URL-map validation reads redirects)
|
|
52
55
|
* share. One key, one cache entry: a save's `cache.patch` updates both at once.
|
|
53
56
|
*/
|
|
54
57
|
export function useRecordList(kind, projectId) {
|
|
@@ -172,6 +175,59 @@ function sortForRail(kind, records) {
|
|
|
172
175
|
return left < right ? -1 : left > right ? 1 : 0;
|
|
173
176
|
});
|
|
174
177
|
}
|
|
178
|
+
/**
|
|
179
|
+
* EC-690 (critic point 5) — the findings a record has only as a member of its kind's SET,
|
|
180
|
+
* judged by the ONE producer the studio's issues tray reads, so the record's own Issues pane
|
|
181
|
+
* and the tray can never disagree. Before this, the losing row of a duplicate redirect pair
|
|
182
|
+
* said "No issues" while the rail foot named it (measured live, wave-0100): the rail
|
|
183
|
+
* validated one record without its siblings.
|
|
184
|
+
*
|
|
185
|
+
* Keyed by record kind; a kind with no set rule has no set findings. Redirects: core's one
|
|
186
|
+
* normalised redirect set and router (`routeValidationIssues` — duplicates after
|
|
187
|
+
* trailing-slash normalisation, loops, unreadable rows), over the project's routes.
|
|
188
|
+
*/
|
|
189
|
+
const RECORD_SET_RULES = {
|
|
190
|
+
[redirectRecordKind.kind]: (records) => routeValidationIssues({ routes: resolveDeploymentRoutes(), collections: resolveDeploymentCollections() }, records),
|
|
191
|
+
};
|
|
192
|
+
/** The set rule's issues located AT one record (`[<domain>, <id>]`), keyed by record id. */
|
|
193
|
+
function issuesById(issues) {
|
|
194
|
+
const byId = new Map();
|
|
195
|
+
for (const issue of issues) {
|
|
196
|
+
const id = issue.path[1];
|
|
197
|
+
if (typeof id !== 'string')
|
|
198
|
+
continue;
|
|
199
|
+
byId.set(id, [...(byId.get(id) ?? []), issue]);
|
|
200
|
+
}
|
|
201
|
+
return byId;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* The set findings of `doc` as the editor sees it NOW: the stored set with this record's
|
|
205
|
+
* columns replaced by the draft's, so fixing a duplicate `from` clears the finding as you
|
|
206
|
+
* type. An unsaved "New" draft joins the set as an extra row; it is only ever told about a
|
|
207
|
+
* DUPLICATE (a half-typed draft is not an "unreadable stored redirect" — its required fields
|
|
208
|
+
* report that, field by field). Pure; exported for the tests.
|
|
209
|
+
*/
|
|
210
|
+
export function recordSetIssues(kind, records, doc) {
|
|
211
|
+
const rule = RECORD_SET_RULES[kind.kind];
|
|
212
|
+
if (!rule)
|
|
213
|
+
return [];
|
|
214
|
+
const columns = recordColumnNames(kind);
|
|
215
|
+
const index = records.findIndex((record) => record.id === doc.id);
|
|
216
|
+
const draftColumns = Object.fromEntries(Object.entries(doc.values ?? {}).filter(([name]) => columns.has(name)));
|
|
217
|
+
const own = { ...(index === -1 ? {} : records[index]), ...draftColumns, id: doc.id };
|
|
218
|
+
const set = index === -1 ? [...records, own] : records.map((record, i) => (i === index ? own : record));
|
|
219
|
+
const located = issuesById(rule(set)).get(doc.id) ?? [];
|
|
220
|
+
return index === -1 ? located.filter((issue) => issue.meta?.duplicateOf !== undefined) : located;
|
|
221
|
+
}
|
|
222
|
+
/** True when the draft's columns are the stored record's — the memoised stored verdict applies. */
|
|
223
|
+
function draftMatchesStored(kind, stored, doc) {
|
|
224
|
+
const values = doc.values ?? {};
|
|
225
|
+
for (const column of recordColumnNames(kind)) {
|
|
226
|
+
if (!Object.is(values[column], stored[column]))
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
return true;
|
|
230
|
+
}
|
|
175
231
|
/**
|
|
176
232
|
* The {@link CollectionViewSource} of a record rail (must be mounted under its
|
|
177
233
|
* {@link RecordRailProvider}). `schema` is the rail's lens — the kind's default lens or a
|
|
@@ -196,7 +252,21 @@ export function useRecordCollectionViewSource(kind, projectId, schema, paths, ca
|
|
|
196
252
|
return title;
|
|
197
253
|
return translate('records.untitled', { label: localizedLabel(singular) ?? kind.kind });
|
|
198
254
|
}, [store, titleField, singular, kind]);
|
|
199
|
-
|
|
255
|
+
// EC-690: the record's own fields AND its set-level findings (a duplicate redirect, a loop) —
|
|
256
|
+
// the stored set's verdict is computed once per list; a draft that changed a column is
|
|
257
|
+
// re-judged against its siblings.
|
|
258
|
+
const records = store.records;
|
|
259
|
+
const storedSetIssues = useMemo(() => (RECORD_SET_RULES[kind.kind] ? issuesById(RECORD_SET_RULES[kind.kind](records)) : undefined), [kind, records]);
|
|
260
|
+
const validate = useCallback((doc) => {
|
|
261
|
+
const own = validateDocument(registry, doc, { localeConfig: RAIL_LOCALE_CONFIG });
|
|
262
|
+
if (!storedSetIssues)
|
|
263
|
+
return own;
|
|
264
|
+
const stored = records.find((record) => record.id === doc.id);
|
|
265
|
+
const set = stored && draftMatchesStored(kind, stored, doc)
|
|
266
|
+
? (storedSetIssues.get(doc.id) ?? [])
|
|
267
|
+
: recordSetIssues(kind, records, doc);
|
|
268
|
+
return set.length > 0 ? [...own, ...set] : own;
|
|
269
|
+
}, [registry, storedSetIssues, records, kind]);
|
|
200
270
|
const create = useCallback(() => ({ collection: kind.kind, id: store.createDraft() }), [kind, store]);
|
|
201
271
|
// EC-616 (multi-session): an edit of a STORED record writes only what the editor changed
|
|
202
272
|
// against the record they opened (`base`, from the edit shell) — the EC-577 rule keeps every
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"record-rail.js","sourceRoot":"","sources":["../../../src/components/records/record-rail.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useMemo, useRef, useState } from 'react'\nimport type { ReactNode } from 'react'\nimport { createCollectionRegistry, parseLocaleConfig, validateDocument } from '@elytracms/core/cms-core'\nimport type { CmsDocument, CollectionDef, DocumentRef, Locale, LocaleConfig } from '@elytracms/core/cms-core'\nimport type { AnyRecordKind, RecordEntryOf, RecordWriteOf } from '@elytracms/core/persistence'\nimport { createUuidIdFactory } from '@elytracms/core/determinism'\nimport { getStudioOperations } from '../../lib/operations'\nimport { listStoreState, useOperationCache, useOperationQuery } from '../../lib/query'\nimport { createRequiredContext } from '../../lib/required-context'\nimport { localizedLabel, translate } from '../../lib/i18n'\nimport type {\n CollectionViewCapabilities,\n CollectionViewSource,\n} from '../../lib/collection-view/collection-view-source'\nimport { normalizeOkResult, useRailViewSource } from '../rail-view-source'\nimport { RailStoreStatus } from '../rail-store-status'\nimport { CollectionViewSurface } from '../collection-view/collection-view-surface'\nimport { lensDocumentToRecordWrite, recordToLensDocument } from './record-lens'\n\n/**\n * EC-616 — the generic RECORD RAIL: a record kind (`defineRecordKind`) on the portable\n * list/edit surfaces (AD-19), read and written through the operations chokepoint\n * (`operations.records(kind)`) and the one query cache (AD-17). A rail is `nav.rail(kind)`;\n * the engine's redirects rail is exactly this over `redirectRecordKind`.\n *\n * - **Store** ({@link RecordRailProvider}): the kind's records through ONE query key per\n * kind and project ({@link useRecordList} — the rail foot and the rail share it); the\n * \"New\" flow keeps an unsaved record as a LOCAL draft until the first explicit save, so\n * an abandoned \"New\" never writes a stub and never shows in the list.\n * - **Source** ({@link useRecordCollectionViewSource}): the lens projections\n * (`record-lens.ts`), the lens's own validation, the rail's capabilities.\n * - **Surface** ({@link RecordRailModule}): the list, or one record's detail.\n */\n\n/** A record rail is not localized — one implicit locale keeps the lens honest. */\nconst RAIL_LOCALE: Locale = 'en'\nconst RAIL_LOCALE_CONFIG: LocaleConfig = parseLocaleConfig({ default: RAIL_LOCALE, locales: [RAIL_LOCALE] })\n\n/** The default capability profile of a record rail: create, edit, remove, bulk delete — content, not administration. */\nexport const RECORD_RAIL_CAPABILITIES: CollectionViewCapabilities = {\n publish: false,\n versions: false,\n locales: false,\n inlineEdit: false,\n create: true,\n duplicate: false,\n remove: true,\n bulk: true,\n liveUrl: false,\n hierarchy: false,\n importExport: false,\n adminOnly: false,\n}\n\n/** The one query key of a kind's records in a project. */\nexport function recordListKey(kind: AnyRecordKind, projectId: string): readonly [string, string, string] {\n return ['records', kind.kind, projectId]\n}\n\n/**\n * A kind's records in a project, read through the shared operations cache — the ONE query\n * a rail and any reader outside it (the rail foot's URL-map validation reads redirects)\n * share. One key, one cache entry: a save's `cache.patch` updates both at once.\n */\nexport function useRecordList<K extends AnyRecordKind>(kind: K, projectId: string) {\n return useOperationQuery<RecordEntryOf<K>[]>({\n queryKey: recordListKey(kind, projectId),\n loader: async (operations) => {\n const result = await operations.records(kind).list({ projectId })\n if (!result.ok) throw new Error(result.error.message)\n return [...result.value]\n },\n })\n}\n\nexport interface RecordRailStore<K extends AnyRecordKind = AnyRecordKind> {\n kind: K\n projectId: string\n status: 'loading' | 'ready' | 'error'\n errorMessage?: string\n /** The persisted records, in the kind's order. Unsaved drafts are excluded. */\n records: readonly RecordEntryOf<K>[]\n /** One record: a local unsaved draft if present, else a persisted record. */\n get(id: string): RecordEntryOf<K> | undefined\n /** Mint a blank in-memory draft and return its id (the \"New\" navigation target). */\n createDraft(): string\n /** Persist a create/modify; drops the matching draft on success. */\n save(input: RecordWriteOf<K>): Promise<{ ok: true; record: RecordEntryOf<K> } | { ok: false; message: string }>\n /** Delete a persisted record, or silently discard an unsaved draft. */\n remove(id: string): Promise<{ ok: true } | { ok: false; message: string }>\n /** Re-list from the backend. */\n refresh(): Promise<void>\n}\n\nconst { Context: RecordRailContext, useRequired: useRecordRailContext } =\n createRequiredContext<RecordRailStore>('RecordRail', { article: 'a' })\n\n/** The record rail store of the nearest {@link RecordRailProvider}. */\nexport function useRecordRail<K extends AnyRecordKind>(): RecordRailStore<K> {\n return useRecordRailContext() as unknown as RecordRailStore<K>\n}\n\nlet mintDraftUuid: (() => string) | undefined\n\nexport function RecordRailProvider<K extends AnyRecordKind>({\n kind,\n projectId,\n children,\n}: {\n kind: K\n projectId: string\n children: ReactNode\n}) {\n const query = useRecordList(kind, projectId)\n const cache = useOperationCache()\n const persisted = query.data ?? EMPTY\n const { status, errorMessage } = listStoreState(query)\n // In-memory unsaved \"New\" drafts, keyed by id. Not listed; only get()-able.\n const [drafts, setDrafts] = useState<Record<string, RecordEntryOf<K>>>({})\n const draftsRef = useRef(drafts)\n draftsRef.current = drafts\n\n const refetch = query.refetch\n const refresh = useCallback(async () => {\n await refetch()\n }, [refetch])\n\n const get = useCallback(\n (id: string) =>\n drafts[id] ?? (persisted as RecordEntryOf<K>[]).find((record) => (record as { id: string }).id === id),\n [drafts, persisted],\n )\n\n const createDraft = useCallback(() => {\n // The draft id becomes the record's real id on its first save: the production id\n // factory (EC-578 — never a clock or a per-session counter, which collide).\n mintDraftUuid ??= createUuidIdFactory()\n const id = `${kind.idPrefix}_${mintDraftUuid()}`\n const now = new Date().toISOString()\n const draft = { ...recordDraftColumns(kind), id, projectId, createdAt: now, updatedAt: now } as unknown as RecordEntryOf<K>\n setDrafts((prev) => ({ ...prev, [id]: draft }))\n return id\n }, [kind, projectId])\n\n const save = useCallback<RecordRailStore<K>['save']>(\n async (input) => {\n const { operations } = await getStudioOperations()\n const result = await operations.records(kind).save({ projectId, ...input } as never)\n if (!result.ok) return { ok: false, message: result.error.message }\n const saved = result.value as RecordEntryOf<K>\n const savedId = (saved as { id: string }).id\n // The server returns the kind's order on the next read; place the save now and\n // let the patched list keep its order key (re-read on the next fetch).\n cache.patch<RecordEntryOf<K>[]>(recordListKey(kind, projectId), (prev) => {\n const rest = (prev ?? []).filter((record) => (record as { id: string }).id !== savedId)\n return sortForRail(kind, [...rest, saved])\n })\n setDrafts((prev) => {\n if (!(savedId in prev)) return prev\n const next = { ...prev }\n delete next[savedId]\n return next\n })\n return { ok: true, record: saved }\n },\n [kind, projectId, cache],\n )\n\n const remove = useCallback<RecordRailStore<K>['remove']>(\n async (id) => {\n // An unsaved draft was never persisted — discard it locally, no backend call.\n if (draftsRef.current[id]) {\n setDrafts((prev) => {\n const next = { ...prev }\n delete next[id]\n return next\n })\n return { ok: true }\n }\n const { operations } = await getStudioOperations()\n const result = await operations.records(kind).remove({ projectId, id })\n if (!result.ok) return { ok: false, message: result.error.message }\n cache.patch<RecordEntryOf<K>[]>(recordListKey(kind, projectId), (prev) =>\n (prev ?? []).filter((record) => record.id !== id),\n )\n return { ok: true }\n },\n [kind, projectId, cache],\n )\n\n const store = useMemo<RecordRailStore<K>>(\n () => ({\n kind,\n projectId,\n status,\n ...(errorMessage !== undefined ? { errorMessage } : {}),\n records: persisted as RecordEntryOf<K>[],\n get,\n createDraft,\n save,\n remove,\n refresh,\n }),\n [kind, projectId, status, errorMessage, persisted, get, createDraft, save, remove, refresh],\n )\n\n return <RecordRailContext.Provider value={store as unknown as RecordRailStore}>{children}</RecordRailContext.Provider>\n}\n\nconst EMPTY: readonly never[] = []\n\n/**\n * The columns a blank \"New\" draft shows: each column's CREATE default (a new redirect\n * reads enabled + permanent — the reason you create one is for it to work), and `''` for\n * a required text column. The save then sends what the editor saw; the adapter still\n * applies the defaults itself for a write that leaves them out (EC-577).\n */\nexport function recordDraftColumns(kind: AnyRecordKind): Record<string, unknown> {\n const columns: Record<string, unknown> = {}\n for (const [name, field] of Object.entries(kind.fields)) {\n const parsed = (field as { safeParse(value: unknown): { success: boolean; data?: unknown } }).safeParse(undefined)\n if (parsed.success && parsed.data !== undefined) columns[name] = parsed.data\n else if ((field as { _zod: { def: { type: string } } })._zod.def.type === 'string') columns[name] = ''\n }\n return columns\n}\n\n/** The kind's own list order for a locally patched list (`orderBy`, else creation time). */\nfunction sortForRail<R>(kind: AnyRecordKind, records: R[]): R[] {\n const column = kind.orderBy ?? 'createdAt'\n return [...records].sort((a, b) => {\n const left = String((a as Record<string, unknown>)[column] ?? '')\n const right = String((b as Record<string, unknown>)[column] ?? '')\n return left < right ? -1 : left > right ? 1 : 0\n })\n}\n\n/** How a rail addresses its list and one record's detail (the view's own route segment). */\nexport interface RecordRailPaths {\n listPath: string\n detailPath(id: string): string\n}\n\n/**\n * The {@link CollectionViewSource} of a record rail (must be mounted under its\n * {@link RecordRailProvider}). `schema` is the rail's lens — the kind's default lens or a\n * mount-customized one (EC-311 overrides by name; additive fields persist in `meta`).\n */\nexport function useRecordCollectionViewSource<K extends AnyRecordKind>(\n kind: K,\n projectId: string,\n schema: CollectionDef,\n paths: RecordRailPaths,\n capabilities: CollectionViewCapabilities = RECORD_RAIL_CAPABILITIES,\n): CollectionViewSource {\n const store = useRecordRail<K>()\n // Run the (possibly customized) lens's validation — required/pattern and custom\n // `validate` fns on an overridden field must actually fire in the editor (EC-311).\n const registry = useMemo(() => createCollectionRegistry([schema]), [schema])\n const singular = schema.form?.labelSingular\n const titleField = typeof schema.titleField === 'string' ? schema.titleField : undefined\n\n const documents = useCallback(\n () => store.records.map((record) => recordToLensDocument(kind, record)),\n [kind, store.records],\n )\n const get = useCallback(\n (ref: DocumentRef): CmsDocument | undefined => {\n const record = store.get(ref.id)\n return record ? recordToLensDocument(kind, record) : undefined\n },\n [kind, store],\n )\n const titleOf = useCallback(\n (ref: DocumentRef): string => {\n const record = store.get(ref.id) as Record<string, unknown> | undefined\n const title = titleField ? record?.[titleField] : undefined\n if (typeof title === 'string' && title.length > 0) return title\n return translate('records.untitled', { label: localizedLabel(singular) ?? kind.kind })\n },\n [store, titleField, singular, kind],\n )\n const validate = useCallback(\n (doc: CmsDocument) => validateDocument(registry, doc, { localeConfig: RAIL_LOCALE_CONFIG }),\n [registry],\n )\n const create = useCallback((): DocumentRef => ({ collection: kind.kind, id: store.createDraft() }), [kind, store])\n // EC-616 (multi-session): an edit of a STORED record writes only what the editor changed\n // against the record they opened (`base`, from the edit shell) — the EC-577 rule keeps every\n // other column, so another writer's change made meanwhile survives this save. A record the\n // editor is creating (an unsaved draft) is written whole.\n const save = useCallback(\n (doc: CmsDocument, base?: CmsDocument) => {\n const stored = store.records.some((record) => (record as { id: string }).id === doc.id)\n return store.save(lensDocumentToRecordWrite(kind, doc, stored ? base : undefined))\n },\n [kind, store],\n )\n const remove = useCallback((ref: DocumentRef) => store.remove(ref.id), [store])\n\n return useRailViewSource({\n projectId,\n schema,\n capabilities,\n localeConfig: RAIL_LOCALE_CONFIG,\n documents,\n get,\n titleOf,\n detailPath: paths.detailPath,\n listPath: paths.listPath,\n validate,\n create,\n save,\n remove,\n normalizeSaveResult: normalizeOkResult,\n normalizeRemoveResult: normalizeOkResult,\n })\n}\n\nexport interface RecordRailModuleProps<K extends AnyRecordKind> {\n kind: K\n projectId: string\n /** `null` → the list; an id → that record's detail. */\n recordId: string | null\n schema: CollectionDef\n paths: RecordRailPaths\n capabilities?: CollectionViewCapabilities\n /** The rail's label, for its loading / failure states. */\n label: string\n /** Optional copy for the loading / failure states (default: the generic catalog keys). */\n messages?: { loading?: () => string; loadFailed?: () => string }\n}\n\n/** The surface of a record rail: the portable list, or one record's detail. */\nexport function RecordRailModule<K extends AnyRecordKind>(props: RecordRailModuleProps<K>) {\n return (\n <RecordRailProvider kind={props.kind} projectId={props.projectId}>\n <RecordRailSurface {...props} />\n </RecordRailProvider>\n )\n}\n\nfunction RecordRailSurface<K extends AnyRecordKind>({\n kind,\n projectId,\n recordId,\n schema,\n paths,\n capabilities,\n label,\n messages,\n}: RecordRailModuleProps<K>) {\n const store = useRecordRail<K>()\n const source = useRecordCollectionViewSource(kind, projectId, schema, paths, capabilities)\n\n if (store.status !== 'ready') {\n return (\n <RailStoreStatus\n store={store}\n errorLabel={messages?.loadFailed?.() ?? translate('records.loadFailed', { label })}\n loadingLabel={messages?.loading?.() ?? translate('records.loading', { label })}\n errorTestId={`${kind.kind}-rail-error`}\n />\n )\n }\n // One record's detail opens in the portable edit shell (AD-19); the list otherwise.\n return <CollectionViewSurface source={source} projectId={projectId} documentId={recordId} />\n}\n"]}
|
|
1
|
+
{"version":3,"file":"record-rail.js","sourceRoot":"","sources":["../../../src/components/records/record-rail.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAE9D,OAAO,EAAE,wBAAwB,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAA;AAExG,OAAO,EAAE,kBAAkB,EAAE,MAAM,6BAA6B,CAAA;AAEhE,OAAO,EAAE,qBAAqB,EAAE,MAAM,+BAA+B,CAAA;AACrE,OAAO,EAAE,4BAA4B,EAAE,uBAAuB,EAAE,MAAM,qBAAqB,CAAA;AAC3F,OAAO,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAA;AACjE,OAAO,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAA;AAC1D,OAAO,EAAE,cAAc,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAA;AACtF,OAAO,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAA;AAClE,OAAO,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAK1D,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAA;AAC1E,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AACtD,OAAO,EAAE,qBAAqB,EAAE,MAAM,4CAA4C,CAAA;AAClF,OAAO,EAAE,yBAAyB,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAA;AAElG;;;;;;;;;;;;;GAaG;AAEH,kFAAkF;AAClF,MAAM,WAAW,GAAW,IAAI,CAAA;AAChC,MAAM,kBAAkB,GAAiB,iBAAiB,CAAC,EAAE,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,CAAC,WAAW,CAAC,EAAE,CAAC,CAAA;AAE5G,wHAAwH;AACxH,MAAM,CAAC,MAAM,wBAAwB,GAA+B;IAClE,OAAO,EAAE,KAAK;IACd,QAAQ,EAAE,KAAK;IACf,OAAO,EAAE,KAAK;IACd,UAAU,EAAE,KAAK;IACjB,MAAM,EAAE,IAAI;IACZ,SAAS,EAAE,KAAK;IAChB,MAAM,EAAE,IAAI;IACZ,IAAI,EAAE,IAAI;IACV,OAAO,EAAE,KAAK;IACd,SAAS,EAAE,KAAK;IAChB,YAAY,EAAE,KAAK;IACnB,SAAS,EAAE,KAAK;CACjB,CAAA;AAED,0DAA0D;AAC1D,MAAM,UAAU,aAAa,CAAC,IAAmB,EAAE,SAAiB;IAClE,OAAO,CAAC,SAAS,EAAE,IAAI,CAAC,IAAI,EAAE,SAAS,CAAC,CAAA;AAC1C,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAA0B,IAAO,EAAE,SAAiB;IAC/E,OAAO,iBAAiB,CAAqB;QAC3C,QAAQ,EAAE,aAAa,CAAC,IAAI,EAAE,SAAS,CAAC;QACxC,MAAM,EAAE,KAAK,EAAE,UAAU,EAAE,EAAE;YAC3B,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,EAAE,SAAS,EAAE,CAAC,CAAA;YACjE,IAAI,CAAC,MAAM,CAAC,EAAE;gBAAE,MAAM,IAAI,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAA;YACrD,OAAO,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,CAAA;QAC1B,CAAC;KACF,CAAC,CAAA;AACJ,CAAC;AAqBD,MAAM,EAAE,OAAO,EAAE,iBAAiB,EAAE,WAAW,EAAE,oBAAoB,EAAE,GACrE,qBAAqB,CAAkB,YAAY,EAAE,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,CAAA;AAExE,uEAAuE;AACvE,MAAM,UAAU,aAAa;IAC3B,OAAO,oBAAoB,EAAmC,CAAA;AAChE,CAAC;AAED,IAAI,aAAyC,CAAA;AAE7C,MAAM,UAAU,kBAAkB,CAA0B,EAC1D,IAAI,EACJ,SAAS,EACT,QAAQ,GAKT;IACC,MAAM,KAAK,GAAG,aAAa,CAAC,IAAI,EAAE,SAAS,CAAC,CAAA;IAC5C,MAAM,KAAK,GAAG,iBAAiB,EAAE,CAAA;IACjC,MAAM,SAAS,GAAG,KAAK,CAAC,IAAI,IAAI,KAAK,CAAA;IACrC,MAAM,EAAE,MAAM,EAAE,YAAY,EAAE,GAAG,cAAc,CAAC,KAAK,CAAC,CAAA;IACtD,4EAA4E;IAC5E,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,GAAG,QAAQ,CAAmC,EAAE,CAAC,CAAA;IAC1E,MAAM,SAAS,GAAG,MAAM,CAAC,MAAM,CAAC,CAAA;IAChC,SAAS,CAAC,OAAO,GAAG,MAAM,CAAA;IAE1B,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAA;IAC7B,MAAM,OAAO,GAAG,WAAW,CAAC,KAAK,IAAI,EAAE;QACrC,MAAM,OAAO,EAAE,CAAA;IACjB,CAAC,EAAE,CAAC,OAAO,CAAC,CAAC,CAAA;IAEb,MAAM,GAAG,GAAG,WAAW,CACrB,CAAC,EAAU,EAAE,EAAE,CACb,MAAM,CAAC,EAAE,CAAC,IAAK,SAAgC,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAE,MAAyB,CAAC,EAAE,KAAK,EAAE,CAAC,EACxG,CAAC,MAAM,EAAE,SAAS,CAAC,CACpB,CAAA;IAED,MAAM,WAAW,GAAG,WAAW,CAAC,GAAG,EAAE;QACnC,iFAAiF;QACjF,4EAA4E;QAC5E,aAAa,KAAK,mBAAmB,EAAE,CAAA;QACvC,MAAM,EAAE,GAAG,GAAG,IAAI,CAAC,QAAQ,IAAI,aAAa,EAAE,EAAE,CAAA;QAChD,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAA;QACpC,MAAM,KAAK,GAAG,EAAE,GAAG,kBAAkB,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,SAAS,EAAE,SAAS,EAAE,GAAG,EAAE,SAAS,EAAE,GAAG,EAAiC,CAAA;QAC3H,SAAS,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,IAAI,EAAE,CAAC,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,CAAA;QAC/C,OAAO,EAAE,CAAA;IACX,CAAC,EAAE,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC,CAAA;IAErB,MAAM,IAAI,GAAG,WAAW,CACtB,KAAK,EAAE,KAAK,EAAE,EAAE;QACd,MAAM,EAAE,UAAU,EAAE,GAAG,MAAM,mBAAmB,EAAE,CAAA;QAClD,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAAW,CAAC,CAAA;QACpF,IAAI,CAAC,MAAM,CAAC,EAAE;YAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,CAAC,KAAK,CAAC,OAAO,EAAE,CAAA;QACnE,MAAM,KAAK,GAAG,MAAM,CAAC,KAAyB,CAAA;QAC9C,MAAM,OAAO,GAAI,KAAwB,CAAC,EAAE,CAAA;QAC5C,+EAA+E;QAC/E,uEAAuE;QACvE,KAAK,CAAC,KAAK,CAAqB,aAAa,CAAC,IAAI,EAAE,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,EAAE;YACvE,MAAM,IAAI,GAAG,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,MAAM,EAAE,EAAE,CAAE,MAAyB,CAAC,EAAE,KAAK,OAAO,CAAC,CAAA;YACvF,OAAO,WAAW,CAAC,IAAI,EAAE,CAAC,GAAG,IAAI,EAAE,KAAK,CAAC,CAAC,CAAA;QAC5C,CAAC,CAAC,CAAA;QACF,SAAS,CAAC,CAAC,IAAI,EAAE,EAAE;YACjB,IAAI,CAAC,CAAC,OAAO,IAAI,IAAI,CAAC;gBAAE,OAAO,IAAI,CAAA;YACnC,MAAM,IAAI,GAAG,EAAE,GAAG,IAAI,EAAE,CAAA;YACxB,OAAO,IAAI,CAAC,OAAO,CAAC,CAAA;YACpB,OAAO,IAAI,CAAA;QACb,CAAC,CAAC,CAAA;QACF,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,CAAA;IACpC,CAAC,EACD,CAAC,IAAI,EAAE,SAAS,EAAE,KAAK,CAAC,CACzB,CAAA;IAED,MAAM,MAAM,GAAG,WAAW,CACxB,KAAK,EAAE,EAAE,EAAE,EAAE;QACX,8EAA8E;QAC9E,IAAI,SAAS,CAAC,OAAO,CAAC,EAAE,CAAC,EAAE,CAAC;YAC1B,SAAS,CAAC,CAAC,IAAI,EAAE,EAAE;gBACjB,MAAM,IAAI,GAAG,EAAE,GAAG,IAAI,EAAE,CAAA;gBACxB,OAAO,IAAI,CAAC,EAAE,CAAC,CAAA;gBACf,OAAO,IAAI,CAAA;YACb,CAAC,CAAC,CAAA;YACF,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,CAAA;QACrB,CAAC;QACD,MAAM,EAAE,UAAU,EAAE,GAAG,MAAM,mBAAmB,EAAE,CAAA;QAClD,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,EAAE,SAAS,EAAE,EAAE,EAAE,CAAC,CAAA;QACvE,IAAI,CAAC,MAAM,CAAC,EAAE;YAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,CAAC,KAAK,CAAC,OAAO,EAAE,CAAA;QACnE,KAAK,CAAC,KAAK,CAAqB,aAAa,CAAC,IAAI,EAAE,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,EAAE,CACvE,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,EAAE,KAAK,EAAE,CAAC,CAClD,CAAA;QACD,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,CAAA;IACrB,CAAC,EACD,CAAC,IAAI,EAAE,SAAS,EAAE,KAAK,CAAC,CACzB,CAAA;IAED,MAAM,KAAK,GAAG,OAAO,CACnB,GAAG,EAAE,CAAC,CAAC;QACL,IAAI;QACJ,SAAS;QACT,MAAM;QACN,GAAG,CAAC,YAAY,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,YAAY,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACvD,OAAO,EAAE,SAA+B;QACxC,GAAG;QACH,WAAW;QACX,IAAI;QACJ,MAAM;QACN,OAAO;KACR,CAAC,EACF,CAAC,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,YAAY,EAAE,SAAS,EAAE,GAAG,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,CAC5F,CAAA;IAED,OAAO,KAAC,iBAAiB,CAAC,QAAQ,IAAC,KAAK,EAAE,KAAmC,YAAG,QAAQ,GAA8B,CAAA;AACxH,CAAC;AAED,MAAM,KAAK,GAAqB,EAAE,CAAA;AAElC;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CAAC,IAAmB;IACpD,MAAM,OAAO,GAA4B,EAAE,CAAA;IAC3C,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;QACxD,MAAM,MAAM,GAAI,KAA6E,CAAC,SAAS,CAAC,SAAS,CAAC,CAAA;QAClH,IAAI,MAAM,CAAC,OAAO,IAAI,MAAM,CAAC,IAAI,KAAK,SAAS;YAAE,OAAO,CAAC,IAAI,CAAC,GAAG,MAAM,CAAC,IAAI,CAAA;aACvE,IAAK,KAA6C,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,KAAK,QAAQ;YAAE,OAAO,CAAC,IAAI,CAAC,GAAG,EAAE,CAAA;IACxG,CAAC;IACD,OAAO,OAAO,CAAA;AAChB,CAAC;AAED,4FAA4F;AAC5F,SAAS,WAAW,CAAI,IAAmB,EAAE,OAAY;IACvD,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,IAAI,WAAW,CAAA;IAC1C,OAAO,CAAC,GAAG,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE;QAChC,MAAM,IAAI,GAAG,MAAM,CAAE,CAA6B,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,CAAA;QACjE,MAAM,KAAK,GAAG,MAAM,CAAE,CAA6B,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,CAAA;QAClE,OAAO,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;IACjD,CAAC,CAAC,CAAA;AACJ,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,gBAAgB,GAA6G;IACjI,CAAC,kBAAkB,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,CACrC,qBAAqB,CACnB,EAAE,MAAM,EAAE,uBAAuB,EAAE,EAAE,WAAW,EAAE,4BAA4B,EAAE,EAAE,EAClF,OAA8C,CAC/C;CACJ,CAAA;AAED,4FAA4F;AAC5F,SAAS,UAAU,CAAC,MAAqC;IACvD,MAAM,IAAI,GAAG,IAAI,GAAG,EAAgC,CAAA;IACpD,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC3B,MAAM,EAAE,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;QACxB,IAAI,OAAO,EAAE,KAAK,QAAQ;YAAE,SAAQ;QACpC,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,EAAE,KAAK,CAAC,CAAC,CAAA;IAChD,CAAC;IACD,OAAO,IAAI,CAAA;AACb,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,eAAe,CAC7B,IAAmB,EACnB,OAA2C,EAC3C,GAAgB;IAEhB,MAAM,IAAI,GAAG,gBAAgB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IACxC,IAAI,CAAC,IAAI;QAAE,OAAO,EAAE,CAAA;IACpB,MAAM,OAAO,GAAG,iBAAiB,CAAC,IAAI,CAAC,CAAA;IACvC,MAAM,KAAK,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,EAAE,KAAK,GAAG,CAAC,EAAE,CAAC,CAAA;IACjE,MAAM,YAAY,GAAG,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IAC/G,MAAM,GAAG,GAAG,EAAE,GAAG,CAAC,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,YAAY,EAAE,EAAE,EAAE,GAAG,CAAC,EAAE,EAAE,CAAA;IACpF,MAAM,GAAG,GAAG,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAA;IACvG,MAAM,OAAO,GAAG,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,EAAE,CAAA;IACvD,OAAO,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,EAAE,WAAW,KAAK,SAAS,CAAC,CAAC,CAAC,CAAC,OAAO,CAAA;AAClG,CAAC;AAED,mGAAmG;AACnG,SAAS,kBAAkB,CAAC,IAAmB,EAAE,MAA+B,EAAE,GAAgB;IAChG,MAAM,MAAM,GAAG,GAAG,CAAC,MAAM,IAAI,EAAE,CAAA;IAC/B,KAAK,MAAM,MAAM,IAAI,iBAAiB,CAAC,IAAI,CAAC,EAAE,CAAC;QAC7C,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC;YAAE,OAAO,KAAK,CAAA;IAC9D,CAAC;IACD,OAAO,IAAI,CAAA;AACb,CAAC;AAQD;;;;GAIG;AACH,MAAM,UAAU,6BAA6B,CAC3C,IAAO,EACP,SAAiB,EACjB,MAAqB,EACrB,KAAsB,EACtB,eAA2C,wBAAwB;IAEnE,MAAM,KAAK,GAAG,aAAa,EAAK,CAAA;IAChC,gFAAgF;IAChF,mFAAmF;IACnF,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC,wBAAwB,CAAC,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC,CAAA;IAC5E,MAAM,QAAQ,GAAG,MAAM,CAAC,IAAI,EAAE,aAAa,CAAA;IAC3C,MAAM,UAAU,GAAG,OAAO,MAAM,CAAC,UAAU,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,SAAS,CAAA;IAExF,MAAM,SAAS,GAAG,WAAW,CAC3B,GAAG,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,oBAAoB,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,EACvE,CAAC,IAAI,EAAE,KAAK,CAAC,OAAO,CAAC,CACtB,CAAA;IACD,MAAM,GAAG,GAAG,WAAW,CACrB,CAAC,GAAgB,EAA2B,EAAE;QAC5C,MAAM,MAAM,GAAG,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;QAChC,OAAO,MAAM,CAAC,CAAC,CAAC,oBAAoB,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;IAChE,CAAC,EACD,CAAC,IAAI,EAAE,KAAK,CAAC,CACd,CAAA;IACD,MAAM,OAAO,GAAG,WAAW,CACzB,CAAC,GAAgB,EAAU,EAAE;QAC3B,MAAM,MAAM,GAAG,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAwC,CAAA;QACvE,MAAM,KAAK,GAAG,UAAU,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;QAC3D,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC;YAAE,OAAO,KAAK,CAAA;QAC/D,OAAO,SAAS,CAAC,kBAAkB,EAAE,EAAE,KAAK,EAAE,cAAc,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC,CAAA;IACxF,CAAC,EACD,CAAC,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,IAAI,CAAC,CACpC,CAAA;IACD,8FAA8F;IAC9F,uFAAuF;IACvF,kCAAkC;IAClC,MAAM,OAAO,GAAG,KAAK,CAAC,OAAwD,CAAA;IAC9E,MAAM,eAAe,GAAG,OAAO,CAC7B,GAAG,EAAE,CAAC,CAAC,gBAAgB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,gBAAgB,CAAC,IAAI,CAAC,IAAI,CAAE,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,EACnG,CAAC,IAAI,EAAE,OAAO,CAAC,CAChB,CAAA;IACD,MAAM,QAAQ,GAAG,WAAW,CAC1B,CAAC,GAAgB,EAAE,EAAE;QACnB,MAAM,GAAG,GAAG,gBAAgB,CAAC,QAAQ,EAAE,GAAG,EAAE,EAAE,YAAY,EAAE,kBAAkB,EAAE,CAAC,CAAA;QACjF,IAAI,CAAC,eAAe;YAAE,OAAO,GAAG,CAAA;QAChC,MAAM,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,EAAE,KAAK,GAAG,CAAC,EAAE,CAAC,CAAA;QAC7D,MAAM,GAAG,GACP,MAAM,IAAI,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,CAAC;YAC7C,CAAC,CAAC,CAAC,eAAe,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC;YACrC,CAAC,CAAC,eAAe,CAAC,IAAI,EAAE,OAAO,EAAE,GAAG,CAAC,CAAA;QACzC,OAAO,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,GAAG,EAAE,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAA;IAChD,CAAC,EACD,CAAC,QAAQ,EAAE,eAAe,EAAE,OAAO,EAAE,IAAI,CAAC,CAC3C,CAAA;IACD,MAAM,MAAM,GAAG,WAAW,CAAC,GAAgB,EAAE,CAAC,CAAC,EAAE,UAAU,EAAE,IAAI,CAAC,IAAI,EAAE,EAAE,EAAE,KAAK,CAAC,WAAW,EAAE,EAAE,CAAC,EAAE,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC,CAAA;IAClH,yFAAyF;IACzF,6FAA6F;IAC7F,2FAA2F;IAC3F,0DAA0D;IAC1D,MAAM,IAAI,GAAG,WAAW,CACtB,CAAC,GAAgB,EAAE,IAAkB,EAAE,EAAE;QACvC,MAAM,MAAM,GAAG,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAE,MAAyB,CAAC,EAAE,KAAK,GAAG,CAAC,EAAE,CAAC,CAAA;QACvF,OAAO,KAAK,CAAC,IAAI,CAAC,yBAAyB,CAAC,IAAI,EAAE,GAAG,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAA;IACpF,CAAC,EACD,CAAC,IAAI,EAAE,KAAK,CAAC,CACd,CAAA;IACD,MAAM,MAAM,GAAG,WAAW,CAAC,CAAC,GAAgB,EAAE,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,CAAA;IAE/E,OAAO,iBAAiB,CAAC;QACvB,SAAS;QACT,MAAM;QACN,YAAY;QACZ,YAAY,EAAE,kBAAkB;QAChC,SAAS;QACT,GAAG;QACH,OAAO;QACP,UAAU,EAAE,KAAK,CAAC,UAAU;QAC5B,QAAQ,EAAE,KAAK,CAAC,QAAQ;QACxB,QAAQ;QACR,MAAM;QACN,IAAI;QACJ,MAAM;QACN,mBAAmB,EAAE,iBAAiB;QACtC,qBAAqB,EAAE,iBAAiB;KACzC,CAAC,CAAA;AACJ,CAAC;AAgBD,+EAA+E;AAC/E,MAAM,UAAU,gBAAgB,CAA0B,KAA+B;IACvF,OAAO,CACL,KAAC,kBAAkB,IAAC,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,SAAS,EAAE,KAAK,CAAC,SAAS,YAC9D,KAAC,iBAAiB,OAAK,KAAK,GAAI,GACb,CACtB,CAAA;AACH,CAAC;AAED,SAAS,iBAAiB,CAA0B,EAClD,IAAI,EACJ,SAAS,EACT,QAAQ,EACR,MAAM,EACN,KAAK,EACL,YAAY,EACZ,KAAK,EACL,QAAQ,GACiB;IACzB,MAAM,KAAK,GAAG,aAAa,EAAK,CAAA;IAChC,MAAM,MAAM,GAAG,6BAA6B,CAAC,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,YAAY,CAAC,CAAA;IAE1F,IAAI,KAAK,CAAC,MAAM,KAAK,OAAO,EAAE,CAAC;QAC7B,OAAO,CACL,KAAC,eAAe,IACd,KAAK,EAAE,KAAK,EACZ,UAAU,EAAE,QAAQ,EAAE,UAAU,EAAE,EAAE,IAAI,SAAS,CAAC,oBAAoB,EAAE,EAAE,KAAK,EAAE,CAAC,EAClF,YAAY,EAAE,QAAQ,EAAE,OAAO,EAAE,EAAE,IAAI,SAAS,CAAC,iBAAiB,EAAE,EAAE,KAAK,EAAE,CAAC,EAC9E,WAAW,EAAE,GAAG,IAAI,CAAC,IAAI,aAAa,GACtC,CACH,CAAA;IACH,CAAC;IACD,oFAAoF;IACpF,OAAO,KAAC,qBAAqB,IAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,UAAU,EAAE,QAAQ,GAAI,CAAA;AAC9F,CAAC","sourcesContent":["import { useCallback, useMemo, useRef, useState } from 'react'\nimport type { ReactNode } from 'react'\nimport { createCollectionRegistry, parseLocaleConfig, validateDocument } from '@elytracms/core/cms-core'\nimport type { CmsDocument, CmsValidationIssue, CollectionDef, DocumentRef, Locale, LocaleConfig } from '@elytracms/core/cms-core'\nimport { redirectRecordKind } from '@elytracms/core/persistence'\nimport type { AnyRecordKind, RecordEntryOf, RecordWriteOf, RedirectEntry } from '@elytracms/core/persistence'\nimport { routeValidationIssues } from '../../lib/routes/route-issues'\nimport { resolveDeploymentCollections, resolveDeploymentRoutes } from '../../lib/workspace'\nimport { createUuidIdFactory } from '@elytracms/core/determinism'\nimport { getStudioOperations } from '../../lib/operations'\nimport { listStoreState, useOperationCache, useOperationQuery } from '../../lib/query'\nimport { createRequiredContext } from '../../lib/required-context'\nimport { localizedLabel, translate } from '../../lib/i18n'\nimport type {\n CollectionViewCapabilities,\n CollectionViewSource,\n} from '../../lib/collection-view/collection-view-source'\nimport { normalizeOkResult, useRailViewSource } from '../rail-view-source'\nimport { RailStoreStatus } from '../rail-store-status'\nimport { CollectionViewSurface } from '../collection-view/collection-view-surface'\nimport { lensDocumentToRecordWrite, recordColumnNames, recordToLensDocument } from './record-lens'\n\n/**\n * EC-616 — the generic RECORD RAIL: a record kind (`defineRecordKind`) on the portable\n * list/edit surfaces (AD-19), read and written through the operations chokepoint\n * (`operations.records(kind)`) and the one query cache (AD-17). A rail is `nav.rail(kind)`;\n * the engine's redirects rail is exactly this over `redirectRecordKind`.\n *\n * - **Store** ({@link RecordRailProvider}): the kind's records through ONE query key per\n * kind and project ({@link useRecordList} — the issues tray and the rail share it); the\n * \"New\" flow keeps an unsaved record as a LOCAL draft until the first explicit save, so\n * an abandoned \"New\" never writes a stub and never shows in the list.\n * - **Source** ({@link useRecordCollectionViewSource}): the lens projections\n * (`record-lens.ts`), the lens's own validation, the rail's capabilities.\n * - **Surface** ({@link RecordRailModule}): the list, or one record's detail.\n */\n\n/** A record rail is not localized — one implicit locale keeps the lens honest. */\nconst RAIL_LOCALE: Locale = 'en'\nconst RAIL_LOCALE_CONFIG: LocaleConfig = parseLocaleConfig({ default: RAIL_LOCALE, locales: [RAIL_LOCALE] })\n\n/** The default capability profile of a record rail: create, edit, remove, bulk delete — content, not administration. */\nexport const RECORD_RAIL_CAPABILITIES: CollectionViewCapabilities = {\n publish: false,\n versions: false,\n locales: false,\n inlineEdit: false,\n create: true,\n duplicate: false,\n remove: true,\n bulk: true,\n liveUrl: false,\n hierarchy: false,\n importExport: false,\n adminOnly: false,\n}\n\n/** The one query key of a kind's records in a project. */\nexport function recordListKey(kind: AnyRecordKind, projectId: string): readonly [string, string, string] {\n return ['records', kind.kind, projectId]\n}\n\n/**\n * A kind's records in a project, read through the shared operations cache — the ONE query\n * a rail and any reader outside it (the issues tray's URL-map validation reads redirects)\n * share. One key, one cache entry: a save's `cache.patch` updates both at once.\n */\nexport function useRecordList<K extends AnyRecordKind>(kind: K, projectId: string) {\n return useOperationQuery<RecordEntryOf<K>[]>({\n queryKey: recordListKey(kind, projectId),\n loader: async (operations) => {\n const result = await operations.records(kind).list({ projectId })\n if (!result.ok) throw new Error(result.error.message)\n return [...result.value]\n },\n })\n}\n\nexport interface RecordRailStore<K extends AnyRecordKind = AnyRecordKind> {\n kind: K\n projectId: string\n status: 'loading' | 'ready' | 'error'\n errorMessage?: string\n /** The persisted records, in the kind's order. Unsaved drafts are excluded. */\n records: readonly RecordEntryOf<K>[]\n /** One record: a local unsaved draft if present, else a persisted record. */\n get(id: string): RecordEntryOf<K> | undefined\n /** Mint a blank in-memory draft and return its id (the \"New\" navigation target). */\n createDraft(): string\n /** Persist a create/modify; drops the matching draft on success. */\n save(input: RecordWriteOf<K>): Promise<{ ok: true; record: RecordEntryOf<K> } | { ok: false; message: string }>\n /** Delete a persisted record, or silently discard an unsaved draft. */\n remove(id: string): Promise<{ ok: true } | { ok: false; message: string }>\n /** Re-list from the backend. */\n refresh(): Promise<void>\n}\n\nconst { Context: RecordRailContext, useRequired: useRecordRailContext } =\n createRequiredContext<RecordRailStore>('RecordRail', { article: 'a' })\n\n/** The record rail store of the nearest {@link RecordRailProvider}. */\nexport function useRecordRail<K extends AnyRecordKind>(): RecordRailStore<K> {\n return useRecordRailContext() as unknown as RecordRailStore<K>\n}\n\nlet mintDraftUuid: (() => string) | undefined\n\nexport function RecordRailProvider<K extends AnyRecordKind>({\n kind,\n projectId,\n children,\n}: {\n kind: K\n projectId: string\n children: ReactNode\n}) {\n const query = useRecordList(kind, projectId)\n const cache = useOperationCache()\n const persisted = query.data ?? EMPTY\n const { status, errorMessage } = listStoreState(query)\n // In-memory unsaved \"New\" drafts, keyed by id. Not listed; only get()-able.\n const [drafts, setDrafts] = useState<Record<string, RecordEntryOf<K>>>({})\n const draftsRef = useRef(drafts)\n draftsRef.current = drafts\n\n const refetch = query.refetch\n const refresh = useCallback(async () => {\n await refetch()\n }, [refetch])\n\n const get = useCallback(\n (id: string) =>\n drafts[id] ?? (persisted as RecordEntryOf<K>[]).find((record) => (record as { id: string }).id === id),\n [drafts, persisted],\n )\n\n const createDraft = useCallback(() => {\n // The draft id becomes the record's real id on its first save: the production id\n // factory (EC-578 — never a clock or a per-session counter, which collide).\n mintDraftUuid ??= createUuidIdFactory()\n const id = `${kind.idPrefix}_${mintDraftUuid()}`\n const now = new Date().toISOString()\n const draft = { ...recordDraftColumns(kind), id, projectId, createdAt: now, updatedAt: now } as unknown as RecordEntryOf<K>\n setDrafts((prev) => ({ ...prev, [id]: draft }))\n return id\n }, [kind, projectId])\n\n const save = useCallback<RecordRailStore<K>['save']>(\n async (input) => {\n const { operations } = await getStudioOperations()\n const result = await operations.records(kind).save({ projectId, ...input } as never)\n if (!result.ok) return { ok: false, message: result.error.message }\n const saved = result.value as RecordEntryOf<K>\n const savedId = (saved as { id: string }).id\n // The server returns the kind's order on the next read; place the save now and\n // let the patched list keep its order key (re-read on the next fetch).\n cache.patch<RecordEntryOf<K>[]>(recordListKey(kind, projectId), (prev) => {\n const rest = (prev ?? []).filter((record) => (record as { id: string }).id !== savedId)\n return sortForRail(kind, [...rest, saved])\n })\n setDrafts((prev) => {\n if (!(savedId in prev)) return prev\n const next = { ...prev }\n delete next[savedId]\n return next\n })\n return { ok: true, record: saved }\n },\n [kind, projectId, cache],\n )\n\n const remove = useCallback<RecordRailStore<K>['remove']>(\n async (id) => {\n // An unsaved draft was never persisted — discard it locally, no backend call.\n if (draftsRef.current[id]) {\n setDrafts((prev) => {\n const next = { ...prev }\n delete next[id]\n return next\n })\n return { ok: true }\n }\n const { operations } = await getStudioOperations()\n const result = await operations.records(kind).remove({ projectId, id })\n if (!result.ok) return { ok: false, message: result.error.message }\n cache.patch<RecordEntryOf<K>[]>(recordListKey(kind, projectId), (prev) =>\n (prev ?? []).filter((record) => record.id !== id),\n )\n return { ok: true }\n },\n [kind, projectId, cache],\n )\n\n const store = useMemo<RecordRailStore<K>>(\n () => ({\n kind,\n projectId,\n status,\n ...(errorMessage !== undefined ? { errorMessage } : {}),\n records: persisted as RecordEntryOf<K>[],\n get,\n createDraft,\n save,\n remove,\n refresh,\n }),\n [kind, projectId, status, errorMessage, persisted, get, createDraft, save, remove, refresh],\n )\n\n return <RecordRailContext.Provider value={store as unknown as RecordRailStore}>{children}</RecordRailContext.Provider>\n}\n\nconst EMPTY: readonly never[] = []\n\n/**\n * The columns a blank \"New\" draft shows: each column's CREATE default (a new redirect\n * reads enabled + permanent — the reason you create one is for it to work), and `''` for\n * a required text column. The save then sends what the editor saw; the adapter still\n * applies the defaults itself for a write that leaves them out (EC-577).\n */\nexport function recordDraftColumns(kind: AnyRecordKind): Record<string, unknown> {\n const columns: Record<string, unknown> = {}\n for (const [name, field] of Object.entries(kind.fields)) {\n const parsed = (field as { safeParse(value: unknown): { success: boolean; data?: unknown } }).safeParse(undefined)\n if (parsed.success && parsed.data !== undefined) columns[name] = parsed.data\n else if ((field as { _zod: { def: { type: string } } })._zod.def.type === 'string') columns[name] = ''\n }\n return columns\n}\n\n/** The kind's own list order for a locally patched list (`orderBy`, else creation time). */\nfunction sortForRail<R>(kind: AnyRecordKind, records: R[]): R[] {\n const column = kind.orderBy ?? 'createdAt'\n return [...records].sort((a, b) => {\n const left = String((a as Record<string, unknown>)[column] ?? '')\n const right = String((b as Record<string, unknown>)[column] ?? '')\n return left < right ? -1 : left > right ? 1 : 0\n })\n}\n\n/**\n * EC-690 (critic point 5) — the findings a record has only as a member of its kind's SET,\n * judged by the ONE producer the studio's issues tray reads, so the record's own Issues pane\n * and the tray can never disagree. Before this, the losing row of a duplicate redirect pair\n * said \"No issues\" while the rail foot named it (measured live, wave-0100): the rail\n * validated one record without its siblings.\n *\n * Keyed by record kind; a kind with no set rule has no set findings. Redirects: core's one\n * normalised redirect set and router (`routeValidationIssues` — duplicates after\n * trailing-slash normalisation, loops, unreadable rows), over the project's routes.\n */\nconst RECORD_SET_RULES: Readonly<Record<string, (records: readonly Record<string, unknown>[]) => readonly CmsValidationIssue[]>> = {\n [redirectRecordKind.kind]: (records) =>\n routeValidationIssues(\n { routes: resolveDeploymentRoutes(), collections: resolveDeploymentCollections() },\n records as unknown as readonly RedirectEntry[],\n ),\n}\n\n/** The set rule's issues located AT one record (`[<domain>, <id>]`), keyed by record id. */\nfunction issuesById(issues: readonly CmsValidationIssue[]): Map<string, CmsValidationIssue[]> {\n const byId = new Map<string, CmsValidationIssue[]>()\n for (const issue of issues) {\n const id = issue.path[1]\n if (typeof id !== 'string') continue\n byId.set(id, [...(byId.get(id) ?? []), issue])\n }\n return byId\n}\n\n/**\n * The set findings of `doc` as the editor sees it NOW: the stored set with this record's\n * columns replaced by the draft's, so fixing a duplicate `from` clears the finding as you\n * type. An unsaved \"New\" draft joins the set as an extra row; it is only ever told about a\n * DUPLICATE (a half-typed draft is not an \"unreadable stored redirect\" — its required fields\n * report that, field by field). Pure; exported for the tests.\n */\nexport function recordSetIssues(\n kind: AnyRecordKind,\n records: readonly Record<string, unknown>[],\n doc: CmsDocument,\n): CmsValidationIssue[] {\n const rule = RECORD_SET_RULES[kind.kind]\n if (!rule) return []\n const columns = recordColumnNames(kind)\n const index = records.findIndex((record) => record.id === doc.id)\n const draftColumns = Object.fromEntries(Object.entries(doc.values ?? {}).filter(([name]) => columns.has(name)))\n const own = { ...(index === -1 ? {} : records[index]), ...draftColumns, id: doc.id }\n const set = index === -1 ? [...records, own] : records.map((record, i) => (i === index ? own : record))\n const located = issuesById(rule(set)).get(doc.id) ?? []\n return index === -1 ? located.filter((issue) => issue.meta?.duplicateOf !== undefined) : located\n}\n\n/** True when the draft's columns are the stored record's — the memoised stored verdict applies. */\nfunction draftMatchesStored(kind: AnyRecordKind, stored: Record<string, unknown>, doc: CmsDocument): boolean {\n const values = doc.values ?? {}\n for (const column of recordColumnNames(kind)) {\n if (!Object.is(values[column], stored[column])) return false\n }\n return true\n}\n\n/** How a rail addresses its list and one record's detail (the view's own route segment). */\nexport interface RecordRailPaths {\n listPath: string\n detailPath(id: string): string\n}\n\n/**\n * The {@link CollectionViewSource} of a record rail (must be mounted under its\n * {@link RecordRailProvider}). `schema` is the rail's lens — the kind's default lens or a\n * mount-customized one (EC-311 overrides by name; additive fields persist in `meta`).\n */\nexport function useRecordCollectionViewSource<K extends AnyRecordKind>(\n kind: K,\n projectId: string,\n schema: CollectionDef,\n paths: RecordRailPaths,\n capabilities: CollectionViewCapabilities = RECORD_RAIL_CAPABILITIES,\n): CollectionViewSource {\n const store = useRecordRail<K>()\n // Run the (possibly customized) lens's validation — required/pattern and custom\n // `validate` fns on an overridden field must actually fire in the editor (EC-311).\n const registry = useMemo(() => createCollectionRegistry([schema]), [schema])\n const singular = schema.form?.labelSingular\n const titleField = typeof schema.titleField === 'string' ? schema.titleField : undefined\n\n const documents = useCallback(\n () => store.records.map((record) => recordToLensDocument(kind, record)),\n [kind, store.records],\n )\n const get = useCallback(\n (ref: DocumentRef): CmsDocument | undefined => {\n const record = store.get(ref.id)\n return record ? recordToLensDocument(kind, record) : undefined\n },\n [kind, store],\n )\n const titleOf = useCallback(\n (ref: DocumentRef): string => {\n const record = store.get(ref.id) as Record<string, unknown> | undefined\n const title = titleField ? record?.[titleField] : undefined\n if (typeof title === 'string' && title.length > 0) return title\n return translate('records.untitled', { label: localizedLabel(singular) ?? kind.kind })\n },\n [store, titleField, singular, kind],\n )\n // EC-690: the record's own fields AND its set-level findings (a duplicate redirect, a loop) —\n // the stored set's verdict is computed once per list; a draft that changed a column is\n // re-judged against its siblings.\n const records = store.records as unknown as readonly Record<string, unknown>[]\n const storedSetIssues = useMemo(\n () => (RECORD_SET_RULES[kind.kind] ? issuesById(RECORD_SET_RULES[kind.kind]!(records)) : undefined),\n [kind, records],\n )\n const validate = useCallback(\n (doc: CmsDocument) => {\n const own = validateDocument(registry, doc, { localeConfig: RAIL_LOCALE_CONFIG })\n if (!storedSetIssues) return own\n const stored = records.find((record) => record.id === doc.id)\n const set =\n stored && draftMatchesStored(kind, stored, doc)\n ? (storedSetIssues.get(doc.id) ?? [])\n : recordSetIssues(kind, records, doc)\n return set.length > 0 ? [...own, ...set] : own\n },\n [registry, storedSetIssues, records, kind],\n )\n const create = useCallback((): DocumentRef => ({ collection: kind.kind, id: store.createDraft() }), [kind, store])\n // EC-616 (multi-session): an edit of a STORED record writes only what the editor changed\n // against the record they opened (`base`, from the edit shell) — the EC-577 rule keeps every\n // other column, so another writer's change made meanwhile survives this save. A record the\n // editor is creating (an unsaved draft) is written whole.\n const save = useCallback(\n (doc: CmsDocument, base?: CmsDocument) => {\n const stored = store.records.some((record) => (record as { id: string }).id === doc.id)\n return store.save(lensDocumentToRecordWrite(kind, doc, stored ? base : undefined))\n },\n [kind, store],\n )\n const remove = useCallback((ref: DocumentRef) => store.remove(ref.id), [store])\n\n return useRailViewSource({\n projectId,\n schema,\n capabilities,\n localeConfig: RAIL_LOCALE_CONFIG,\n documents,\n get,\n titleOf,\n detailPath: paths.detailPath,\n listPath: paths.listPath,\n validate,\n create,\n save,\n remove,\n normalizeSaveResult: normalizeOkResult,\n normalizeRemoveResult: normalizeOkResult,\n })\n}\n\nexport interface RecordRailModuleProps<K extends AnyRecordKind> {\n kind: K\n projectId: string\n /** `null` → the list; an id → that record's detail. */\n recordId: string | null\n schema: CollectionDef\n paths: RecordRailPaths\n capabilities?: CollectionViewCapabilities\n /** The rail's label, for its loading / failure states. */\n label: string\n /** Optional copy for the loading / failure states (default: the generic catalog keys). */\n messages?: { loading?: () => string; loadFailed?: () => string }\n}\n\n/** The surface of a record rail: the portable list, or one record's detail. */\nexport function RecordRailModule<K extends AnyRecordKind>(props: RecordRailModuleProps<K>) {\n return (\n <RecordRailProvider kind={props.kind} projectId={props.projectId}>\n <RecordRailSurface {...props} />\n </RecordRailProvider>\n )\n}\n\nfunction RecordRailSurface<K extends AnyRecordKind>({\n kind,\n projectId,\n recordId,\n schema,\n paths,\n capabilities,\n label,\n messages,\n}: RecordRailModuleProps<K>) {\n const store = useRecordRail<K>()\n const source = useRecordCollectionViewSource(kind, projectId, schema, paths, capabilities)\n\n if (store.status !== 'ready') {\n return (\n <RailStoreStatus\n store={store}\n errorLabel={messages?.loadFailed?.() ?? translate('records.loadFailed', { label })}\n loadingLabel={messages?.loading?.() ?? translate('records.loading', { label })}\n errorTestId={`${kind.kind}-rail-error`}\n />\n )\n }\n // One record's detail opens in the portable edit shell (AD-19); the list otherwise.\n return <CollectionViewSurface source={source} projectId={projectId} documentId={recordId} />\n}\n"]}
|