@elytracms/studio 0.0.80 → 0.0.82
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/contract-parity.test.ts +1 -1
- package/convex/functions.ts +13 -3
- package/convex/http-routes.test.ts +143 -0
- package/convex/http.ts +19 -9
- package/convex/redirect-revalidate.test.ts +130 -0
- package/convex/revalidate.ts +9 -7
- package/convex/schema.ts +0 -19
- package/convex/triggers.ts +33 -3
- package/convex/users.ts +1 -1
- package/dist/components/ai/ai-chat-panel.js +26 -4
- package/dist/components/ai/ai-chat-panel.js.map +1 -1
- package/dist/components/ai/proposal-card.d.ts +27 -0
- package/dist/components/ai/proposal-card.js +31 -3
- package/dist/components/ai/proposal-card.js.map +1 -1
- package/dist/components/ai/proposal-issues.d.ts +55 -0
- package/dist/components/ai/proposal-issues.js +69 -0
- package/dist/components/ai/proposal-issues.js.map +1 -0
- package/dist/components/ai/translate-document-dialog.js +38 -3
- package/dist/components/ai/translate-document-dialog.js.map +1 -1
- package/dist/components/app-views/default-modules.d.ts +141 -44
- package/dist/components/app-views/default-modules.js +229 -65
- package/dist/components/app-views/default-modules.js.map +1 -1
- package/dist/components/app-views/module.d.ts +38 -0
- package/dist/components/app-views/module.js.map +1 -1
- package/dist/components/app-views/sidebar.d.ts +5 -2
- package/dist/components/app-views/sidebar.js +16 -5
- package/dist/components/app-views/sidebar.js.map +1 -1
- package/dist/components/app-views/studio-config.d.ts +28 -1
- package/dist/components/app-views/studio-config.js +1 -1
- package/dist/components/app-views/studio-config.js.map +1 -1
- package/dist/components/builder/canvas-render-context.js +10 -2
- package/dist/components/builder/canvas-render-context.js.map +1 -1
- package/dist/components/builder/clipboard-actions.js +5 -3
- package/dist/components/builder/clipboard-actions.js.map +1 -1
- package/dist/components/builder/reuse-context.js +7 -1
- package/dist/components/builder/reuse-context.js.map +1 -1
- package/dist/components/builder/reuse.d.ts +94 -16
- package/dist/components/builder/reuse.js +201 -19
- package/dist/components/builder/reuse.js.map +1 -1
- package/dist/components/cms/bulk-actions.js +10 -5
- package/dist/components/cms/bulk-actions.js.map +1 -1
- package/dist/components/cms/cell-edit.d.ts +18 -3
- package/dist/components/cms/cell-edit.js +29 -7
- package/dist/components/cms/cell-edit.js.map +1 -1
- package/dist/components/cms/cms-store.d.ts +54 -14
- package/dist/components/cms/cms-store.js +142 -45
- package/dist/components/cms/cms-store.js.map +1 -1
- package/dist/components/cms/collection-table.js +2 -2
- package/dist/components/cms/collection-table.js.map +1 -1
- package/dist/components/cms/detail-editor.js +15 -14
- package/dist/components/cms/detail-editor.js.map +1 -1
- package/dist/components/cms/document-editor-context.d.ts +12 -1
- package/dist/components/cms/document-editor-context.js.map +1 -1
- package/dist/components/cms/field-control.js +22 -8
- package/dist/components/cms/field-control.js.map +1 -1
- package/dist/components/cms/field-renderer.js +8 -1
- package/dist/components/cms/field-renderer.js.map +1 -1
- package/dist/components/cms/gate-validation.d.ts +36 -0
- package/dist/components/cms/gate-validation.js +17 -0
- package/dist/components/cms/gate-validation.js.map +1 -0
- package/dist/components/cms/import-export.js +2 -1
- package/dist/components/cms/import-export.js.map +1 -1
- package/dist/components/cms/inline-field-errors.d.ts +8 -0
- package/dist/components/cms/inline-field-errors.js +52 -0
- package/dist/components/cms/inline-field-errors.js.map +1 -0
- package/dist/components/cms/issues-pane.d.ts +4 -2
- package/dist/components/cms/issues-pane.js +13 -7
- package/dist/components/cms/issues-pane.js.map +1 -1
- package/dist/components/cms/slug-collision-report.d.ts +41 -0
- package/dist/components/cms/slug-collision-report.js +52 -0
- package/dist/components/cms/slug-collision-report.js.map +1 -0
- package/dist/components/components/transfer-dialog.js +10 -12
- package/dist/components/components/transfer-dialog.js.map +1 -1
- package/dist/components/deployment-setup-gate.d.ts +24 -0
- package/dist/components/deployment-setup-gate.js +29 -4
- package/dist/components/deployment-setup-gate.js.map +1 -1
- package/dist/components/issue-severity.d.ts +73 -0
- package/dist/components/issue-severity.js +56 -0
- package/dist/components/issue-severity.js.map +1 -0
- package/dist/components/nav-issues.d.ts +68 -0
- package/dist/components/nav-issues.js +78 -0
- package/dist/components/nav-issues.js.map +1 -0
- package/dist/components/operation-error.js +3 -3
- package/dist/components/operation-error.js.map +1 -1
- package/dist/components/project-list.d.ts +9 -3
- package/dist/components/project-list.js +11 -5
- package/dist/components/project-list.js.map +1 -1
- package/dist/components/project-switcher.d.ts +3 -1
- package/dist/components/project-switcher.js +4 -2
- package/dist/components/project-switcher.js.map +1 -1
- package/dist/components/redirects/redirects-store.d.ts +21 -0
- package/dist/components/redirects/redirects-store.js +17 -4
- package/dist/components/redirects/redirects-store.js.map +1 -1
- package/dist/components/section-nav.js +75 -10
- package/dist/components/section-nav.js.map +1 -1
- package/dist/customization.d.ts +7 -0
- package/dist/customization.js.map +1 -1
- package/dist/lib/ai/apply-content.js +6 -7
- package/dist/lib/ai/apply-content.js.map +1 -1
- package/dist/lib/ai/chat-context.d.ts +32 -26
- package/dist/lib/ai/chat-context.js +81 -9
- package/dist/lib/ai/chat-context.js.map +1 -1
- package/dist/lib/ai/constraints.d.ts +42 -0
- package/dist/lib/ai/constraints.js +259 -0
- package/dist/lib/ai/constraints.js.map +1 -0
- package/dist/lib/ai/index.d.ts +2 -0
- package/dist/lib/ai/index.js +2 -0
- package/dist/lib/ai/index.js.map +1 -1
- package/dist/lib/ai/proposal-validation.d.ts +26 -0
- package/dist/lib/ai/proposal-validation.js +17 -0
- package/dist/lib/ai/proposal-validation.js.map +1 -0
- package/dist/lib/ai/shape.js +60 -13
- package/dist/lib/ai/shape.js.map +1 -1
- package/dist/lib/ai/tools.d.ts +8 -4
- package/dist/lib/ai/tools.js +43 -14
- package/dist/lib/ai/tools.js.map +1 -1
- package/dist/lib/ai/translate-document.d.ts +35 -1
- package/dist/lib/ai/translate-document.js +16 -1
- package/dist/lib/ai/translate-document.js.map +1 -1
- package/dist/lib/ai/types.d.ts +11 -6
- package/dist/lib/ai/types.js.map +1 -1
- package/dist/lib/ai/update-fields.d.ts +8 -6
- package/dist/lib/ai/update-fields.js +9 -22
- package/dist/lib/ai/update-fields.js.map +1 -1
- package/dist/lib/clipboard/paste-issues.d.ts +5 -0
- package/dist/lib/clipboard/paste-issues.js +16 -6
- package/dist/lib/clipboard/paste-issues.js.map +1 -1
- package/dist/lib/cms/document-gate.d.ts +37 -0
- package/dist/lib/cms/document-gate.js +40 -0
- package/dist/lib/cms/document-gate.js.map +1 -0
- package/dist/lib/cms-views/view-model.d.ts +5 -0
- package/dist/lib/cms-views/view-model.js +10 -4
- package/dist/lib/cms-views/view-model.js.map +1 -1
- package/dist/lib/data/collection-content.d.ts +6 -1
- package/dist/lib/data/collection-content.js +51 -15
- package/dist/lib/data/collection-content.js.map +1 -1
- package/dist/lib/i18n/i18n.d.ts +51 -16
- package/dist/lib/i18n/i18n.js +119 -43
- package/dist/lib/i18n/i18n.js.map +1 -1
- package/dist/lib/i18n/index.d.ts +8 -1
- package/dist/lib/i18n/index.js +1 -1
- package/dist/lib/i18n/index.js.map +1 -1
- package/dist/lib/i18n/messages.de.js +200 -0
- package/dist/lib/i18n/messages.de.js.map +1 -1
- package/dist/lib/i18n/messages.en.d.ts +162 -0
- package/dist/lib/i18n/messages.en.js +226 -0
- package/dist/lib/i18n/messages.en.js.map +1 -1
- package/dist/lib/i18n/types.d.ts +15 -9
- package/dist/lib/i18n/types.js.map +1 -1
- package/dist/lib/locale/binding-fallback.d.ts +8 -1
- package/dist/lib/locale/binding-fallback.js +8 -1
- package/dist/lib/locale/binding-fallback.js.map +1 -1
- package/dist/lib/locale/locale-store.d.ts +41 -9
- package/dist/lib/locale/locale-store.js +34 -15
- package/dist/lib/locale/locale-store.js.map +1 -1
- package/dist/lib/locale/locale-switcher.d.ts +3 -1
- package/dist/lib/locale/locale-switcher.js +4 -2
- package/dist/lib/locale/locale-switcher.js.map +1 -1
- package/dist/lib/locale/use-studio-locale.d.ts +9 -4
- package/dist/lib/locale/use-studio-locale.js.map +1 -1
- package/dist/lib/operations/studio-operations.js +19 -2
- package/dist/lib/operations/studio-operations.js.map +1 -1
- package/dist/lib/persistence/convex-adapter.d.ts +1 -3
- package/dist/lib/persistence/convex-adapter.js +0 -55
- package/dist/lib/persistence/convex-adapter.js.map +1 -1
- package/dist/lib/persistence/deployment-readiness.js +10 -0
- package/dist/lib/persistence/deployment-readiness.js.map +1 -1
- package/dist/lib/persona/labels.d.ts +85 -9
- package/dist/lib/persona/labels.js +198 -58
- package/dist/lib/persona/labels.js.map +1 -1
- package/dist/lib/routes/route-issues.d.ts +9 -0
- package/dist/lib/routes/route-issues.js +117 -0
- package/dist/lib/routes/route-issues.js.map +1 -0
- package/dist/lib/webhooks/dispatcher.d.ts +5 -2
- package/dist/lib/webhooks/dispatcher.js.map +1 -1
- package/dist/lib/webhooks/tags.js +150 -17
- package/dist/lib/webhooks/tags.js.map +1 -1
- package/dist/lib/workspace/config-issues.d.ts +56 -0
- package/dist/lib/workspace/config-issues.js +72 -0
- package/dist/lib/workspace/config-issues.js.map +1 -0
- package/dist/lib/workspace/index.d.ts +24 -8
- package/dist/lib/workspace/index.js +28 -18
- package/dist/lib/workspace/index.js.map +1 -1
- package/dist/lib/workspace/switcher-source.d.ts +20 -3
- package/dist/lib/workspace/switcher-source.js.map +1 -1
- package/dist/lib/workspace/workspace-source.js +27 -0
- package/dist/lib/workspace/workspace-source.js.map +1 -1
- package/dist/previews/manifest-format.d.ts +13 -6
- package/dist/previews/manifest-format.js +9 -2
- package/dist/previews/manifest-format.js.map +1 -1
- package/dist/router/root.d.ts +36 -1
- package/dist/router/root.js +54 -7
- package/dist/router/root.js.map +1 -1
- package/package.json +2 -2
- package/convex/cliTokens.ts +0 -63
- package/convex/sync.ts +0 -297
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CmsDocument, CmsValidationIssue, CollectionDef, CollectionRegistry, FieldDef, Locale, LocaleConfig } from '@elytracms/core/cms-core';
|
|
1
|
+
import type { CmsDocument, CmsValidationIssue, CollectionDef, CollectionRegistry, DocumentRef, FieldDef, Locale, LocaleConfig } from '@elytracms/core/cms-core';
|
|
2
2
|
import type { OperationError, OperationResult } from '@elytracms/studio/lib/operations';
|
|
3
3
|
/**
|
|
4
4
|
* Inline cell editing for the collection table (EC-168) — the pure half.
|
|
@@ -42,8 +42,14 @@ export declare function applyCellValue(collection: CollectionDef, doc: CmsDocume
|
|
|
42
42
|
* Error-severity issues that the edited cell itself caused. Localized fields
|
|
43
43
|
* only reject on issues for the edited locale — a missing translation in some
|
|
44
44
|
* *other* locale must not block this commit.
|
|
45
|
+
*
|
|
46
|
+
* EC-497: validates with THE bag (`gateValidateOptions`), not the `store` +
|
|
47
|
+
* `localeConfig` subset it used to assemble here. A cell edit is not a weaker
|
|
48
|
+
* KIND of validation — it is the same validation, FILTERED to the cell being
|
|
49
|
+
* committed, which is the whole option-set contract in one function: surfaces
|
|
50
|
+
* differ in what they do with the issues, never in what they validated with.
|
|
45
51
|
*/
|
|
46
|
-
export declare function cellEditIssues(registry: CollectionRegistry, documents: readonly CmsDocument[], localeConfig: LocaleConfig, collection: CollectionDef, next: CmsDocument, fieldName: string, locale: Locale): CmsValidationIssue[];
|
|
52
|
+
export declare function cellEditIssues(registry: CollectionRegistry, documents: readonly CmsDocument[], localeConfig: LocaleConfig, collection: CollectionDef, next: CmsDocument, fieldName: string, locale: Locale, oracles?: CellEditOracles): CmsValidationIssue[];
|
|
47
53
|
/** Outcome of one cell commit: the new document, or a structured rejection. */
|
|
48
54
|
export type CellCommitOutcome = {
|
|
49
55
|
ok: true;
|
|
@@ -62,7 +68,16 @@ export interface CellCommitInput {
|
|
|
62
68
|
/** The committed draft value (`undefined` clears the field). */
|
|
63
69
|
value: unknown;
|
|
64
70
|
}
|
|
65
|
-
|
|
71
|
+
/**
|
|
72
|
+
* The session oracles the canonical option bag needs beyond the store + locales
|
|
73
|
+
* (EC-497). Optional so a fixture-only caller still gets the rest of the bag;
|
|
74
|
+
* absent `isPublished` means "nothing is published", the engine's own default.
|
|
75
|
+
*/
|
|
76
|
+
export interface CellEditOracles {
|
|
77
|
+
isPublished?: (ref: DocumentRef) => boolean;
|
|
78
|
+
assetExists?: (id: string) => boolean;
|
|
79
|
+
}
|
|
80
|
+
export interface CellCommitContext extends CellEditOracles {
|
|
66
81
|
registry: CollectionRegistry;
|
|
67
82
|
/** Current session documents (relation/asset target resolution). */
|
|
68
83
|
documents: readonly CmsDocument[];
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { createDocumentStore, isFieldLocalized, validateDocument } from '@elytracms/core/cms-core';
|
|
2
|
+
import { gateValidateOptions } from './gate-validation.js';
|
|
2
3
|
import { replaceDoc } from './document-ref-utils.js';
|
|
4
|
+
import { cmsValidationIssueMessage, operationErrorMessage, operationIssueMessage, } from '@elytracms/studio/lib/persona/labels';
|
|
3
5
|
/** Stable key for a cell (state maps, DOM lookups). */
|
|
4
6
|
export function cellKey(coord) {
|
|
5
7
|
return `${coord.docId}::${coord.field}`;
|
|
@@ -70,12 +72,23 @@ export function applyCellValue(collection, doc, fieldName, locale, value) {
|
|
|
70
72
|
* Error-severity issues that the edited cell itself caused. Localized fields
|
|
71
73
|
* only reject on issues for the edited locale — a missing translation in some
|
|
72
74
|
* *other* locale must not block this commit.
|
|
75
|
+
*
|
|
76
|
+
* EC-497: validates with THE bag (`gateValidateOptions`), not the `store` +
|
|
77
|
+
* `localeConfig` subset it used to assemble here. A cell edit is not a weaker
|
|
78
|
+
* KIND of validation — it is the same validation, FILTERED to the cell being
|
|
79
|
+
* committed, which is the whole option-set contract in one function: surfaces
|
|
80
|
+
* differ in what they do with the issues, never in what they validated with.
|
|
73
81
|
*/
|
|
74
|
-
export function cellEditIssues(registry, documents, localeConfig, collection, next, fieldName, locale) {
|
|
82
|
+
export function cellEditIssues(registry, documents, localeConfig, collection, next, fieldName, locale, oracles = {}) {
|
|
75
83
|
// Validate against the session with the edited document swapped in, so
|
|
76
84
|
// self-relations and same-document targets resolve.
|
|
77
85
|
const store = createDocumentStore(replaceDoc(documents, next));
|
|
78
|
-
const issues = validateDocument(registry, next, {
|
|
86
|
+
const issues = validateDocument(registry, next, gateValidateOptions({
|
|
87
|
+
store,
|
|
88
|
+
localeConfig,
|
|
89
|
+
isPublished: oracles.isPublished ?? (() => false),
|
|
90
|
+
...(oracles.assetExists ? { assetExists: oracles.assetExists } : {}),
|
|
91
|
+
}));
|
|
79
92
|
const localized = isFieldLocalized(collection, fieldName);
|
|
80
93
|
return issues.filter((issue) => {
|
|
81
94
|
if (issue.severity !== 'error')
|
|
@@ -99,16 +112,22 @@ export function cellEditIssues(registry, documents, localeConfig, collection, ne
|
|
|
99
112
|
*/
|
|
100
113
|
export async function commitCellEdit(context, input) {
|
|
101
114
|
const next = applyCellValue(input.collection, input.doc, input.field.name, input.locale, input.value);
|
|
102
|
-
const issues = cellEditIssues(context.registry, context.documents, context.localeConfig, input.collection, next, input.field.name, input.locale
|
|
115
|
+
const issues = cellEditIssues(context.registry, context.documents, context.localeConfig, input.collection, next, input.field.name, input.locale, {
|
|
116
|
+
...(context.isPublished ? { isPublished: context.isPublished } : {}),
|
|
117
|
+
...(context.assetExists ? { assetExists: context.assetExists } : {}),
|
|
118
|
+
});
|
|
103
119
|
if (issues.length > 0) {
|
|
104
120
|
return {
|
|
105
121
|
ok: false,
|
|
106
122
|
error: {
|
|
107
123
|
code: 'validation',
|
|
108
|
-
|
|
124
|
+
// EC-487/EC-494: this producer is CLIENT-side and already holds the structured
|
|
125
|
+
// issue, so it resolves the message here. (Since EC-494 the wire also carries
|
|
126
|
+
// the ref, which is what makes the SERVER-side publish gate localize.)
|
|
127
|
+
message: cmsValidationIssueMessage(issues[0]),
|
|
109
128
|
issues: issues.map((issue) => ({
|
|
110
129
|
code: issue.code,
|
|
111
|
-
message: issue
|
|
130
|
+
message: cmsValidationIssueMessage(issue),
|
|
112
131
|
path: [...issue.path],
|
|
113
132
|
domain: 'cms',
|
|
114
133
|
})),
|
|
@@ -138,8 +157,11 @@ export async function commitCellEdit(context, input) {
|
|
|
138
157
|
}
|
|
139
158
|
/** The message the cell renders for a rejection: the first issue beats the envelope. */
|
|
140
159
|
export function cellErrorMessage(error) {
|
|
160
|
+
// EC-494: an OperationIssue now carries its catalog ref across the wire, so a
|
|
161
|
+
// rejection built SERVER-SIDE (the publish gate) localizes here too — not only the
|
|
162
|
+
// studio-built ones above.
|
|
141
163
|
if (error.code === 'validation' && error.issues.length > 0)
|
|
142
|
-
return error.issues[0]
|
|
143
|
-
return error
|
|
164
|
+
return operationIssueMessage(error.issues[0]);
|
|
165
|
+
return operationErrorMessage(error);
|
|
144
166
|
}
|
|
145
167
|
//# sourceMappingURL=cell-edit.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cell-edit.js","sourceRoot":"","sources":["../../../src/components/cms/cell-edit.ts"],"names":[],"mappings":"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","sourcesContent":["import { createDocumentStore, isFieldLocalized, validateDocument } from '@elytracms/core/cms-core'\nimport type {\n CmsDocument,\n CmsValidationIssue,\n CollectionDef,\n CollectionRegistry,\n FieldDef,\n Locale,\n LocaleConfig,\n} from '@elytracms/core/cms-core'\nimport type { OperationError, OperationResult } from '@elytracms/studio/lib/operations'\nimport { replaceDoc } from './document-ref-utils'\n\n/**\n * Inline cell editing for the collection table (EC-168) — the pure half.\n *\n * Everything here is plain data + functions so the cell-edit state machine is\n * testable without a DOM: which cells are editable, how Tab/Enter advance,\n * how a draft value is folded into a document (active locale only), and how a\n * commit flows through exactly one `documents.upsert` operations command with\n * optimistic apply + rollback on rejection.\n */\n\n// --- Cell identity -----------------------------------------------------------\n\n/** A cell in the collection table: one document × one field column. */\nexport interface CellCoord {\n docId: string\n field: string\n}\n\n/** Stable key for a cell (state maps, DOM lookups). */\nexport function cellKey(coord: CellCoord): string {\n return `${coord.docId}::${coord.field}`\n}\n\n// --- Editability -------------------------------------------------------------\n\n/** How a cell edits: inline control, anchored picker dialog, or not at all. */\nexport type CellEditKind = 'inline' | 'picker' | 'none'\n\nconst INLINE_TYPES: ReadonlySet<FieldDef['type']> = new Set([\n 'text',\n 'number',\n 'boolean',\n 'select',\n 'date',\n])\n\n/**\n * Classify a field's in-table editing surface. Rich text is deliberately not\n * inline-editable (the detail editor owns it); read-only fields never edit.\n */\nexport function cellEditKind(field: FieldDef, readOnly: boolean): CellEditKind {\n if (readOnly) return 'none'\n if (field.type === 'relation' || field.type === 'asset') return 'picker'\n return INLINE_TYPES.has(field.type) ? 'inline' : 'none'\n}\n\n// --- Keyboard advance --------------------------------------------------------\n\n/** Where a committed edit moves next: Tab → next, Shift-Tab → prev, Enter → down. */\nexport type AdvanceDirection = 'next' | 'prev' | 'down'\n\n/**\n * The next editable cell in the table grid, or `null` at a boundary.\n * `docIds` is the rendered row order; `fields` the editable column order.\n * 'next'/'prev' walk row-major (wrapping across rows); 'down' stays in the\n * same column.\n */\nexport function nextEditableCell(\n docIds: readonly string[],\n fields: readonly string[],\n current: CellCoord,\n direction: AdvanceDirection,\n): CellCoord | null {\n const row = docIds.indexOf(current.docId)\n const col = fields.indexOf(current.field)\n if (row < 0 || col < 0 || fields.length === 0) return null\n if (direction === 'down') {\n const nextDoc = docIds[row + 1]\n return nextDoc === undefined ? null : { docId: nextDoc, field: current.field }\n }\n const flat = row * fields.length + col + (direction === 'next' ? 1 : -1)\n if (flat < 0 || flat >= docIds.length * fields.length) return null\n const docId = docIds[Math.floor(flat / fields.length)]!\n return { docId, field: fields[flat % fields.length]! }\n}\n\n// --- Applying a draft value ----------------------------------------------------\n\n/**\n * Fold one field value into a document. Localized fields write ONLY the given\n * locale's bag (other locale variants untouched, EC-018); non-localized fields\n * write the shared `values` bag. `undefined` clears the entry.\n */\nexport function applyCellValue(\n collection: CollectionDef,\n doc: CmsDocument,\n fieldName: string,\n locale: Locale,\n value: unknown,\n): CmsDocument {\n if (isFieldLocalized(collection, fieldName)) {\n const perLocale = { ...(doc.localized?.[locale] ?? {}) }\n if (value === undefined) delete perLocale[fieldName]\n else perLocale[fieldName] = value\n return { ...doc, localized: { ...doc.localized, [locale]: perLocale } }\n }\n const values = { ...doc.values }\n if (value === undefined) delete values[fieldName]\n else values[fieldName] = value\n return { ...doc, values }\n}\n\n// --- Validation gate -----------------------------------------------------------\n\n/**\n * Error-severity issues that the edited cell itself caused. Localized fields\n * only reject on issues for the edited locale — a missing translation in some\n * *other* locale must not block this commit.\n */\nexport function cellEditIssues(\n registry: CollectionRegistry,\n documents: readonly CmsDocument[],\n localeConfig: LocaleConfig,\n collection: CollectionDef,\n next: CmsDocument,\n fieldName: string,\n locale: Locale,\n): CmsValidationIssue[] {\n // Validate against the session with the edited document swapped in, so\n // self-relations and same-document targets resolve.\n const store = createDocumentStore(replaceDoc(documents, next))\n const issues = validateDocument(registry, next, { store, localeConfig })\n const localized = isFieldLocalized(collection, fieldName)\n return issues.filter((issue) => {\n if (issue.severity !== 'error') return false\n if (issue.meta?.field !== fieldName) return false\n if (!localized) return true\n // Localized field paths are ['documents', key, 'localized', <locale>, <field>, …].\n const at = issue.path.indexOf('localized')\n return at < 0 || issue.path[at + 1] === locale\n })\n}\n\n// --- Commit pipeline -------------------------------------------------------------\n\n/** Outcome of one cell commit: the new document, or a structured rejection. */\nexport type CellCommitOutcome =\n | { ok: true; doc: CmsDocument }\n | { ok: false; error: OperationError }\n\nexport interface CellCommitInput {\n collection: CollectionDef\n /** The document as currently rendered (the rollback target). */\n doc: CmsDocument\n field: FieldDef\n /** The active locale at the point of edit (table locale resolution). */\n locale: Locale\n /** The committed draft value (`undefined` clears the field). */\n value: unknown\n}\n\nexport interface CellCommitContext {\n registry: CollectionRegistry\n /** Current session documents (relation/asset target resolution). */\n documents: readonly CmsDocument[]\n localeConfig: LocaleConfig\n /** Optimistically apply the edited document to the session. */\n applySession: (next: CmsDocument) => void\n /** Restore the pre-edit document after a rejected dispatch. */\n rollbackSession: (previous: CmsDocument) => void\n /**\n * Dispatch the ONE operations command for this commit\n * (`documents.upsert`, EC-141). Absent in fixture-only sessions\n * (no project scope) — the commit is session-only then.\n */\n dispatch?: (next: CmsDocument) => Promise<OperationResult<unknown>>\n}\n\n/**\n * Commit one cell edit:\n * 1. fold the value into the document (active locale only),\n * 2. gate on field validation — rejected edits dispatch NOTHING and report a\n * structured validation error for the cell,\n * 3. apply optimistically, dispatch exactly one operations command,\n * 4. roll the session back when the operation result is a rejection.\n */\nexport async function commitCellEdit(\n context: CellCommitContext,\n input: CellCommitInput,\n): Promise<CellCommitOutcome> {\n const next = applyCellValue(input.collection, input.doc, input.field.name, input.locale, input.value)\n\n const issues = cellEditIssues(\n context.registry,\n context.documents,\n context.localeConfig,\n input.collection,\n next,\n input.field.name,\n input.locale,\n )\n if (issues.length > 0) {\n return {\n ok: false,\n error: {\n code: 'validation',\n message: issues[0]!.message,\n issues: issues.map((issue) => ({\n code: issue.code,\n message: issue.message,\n path: [...issue.path],\n domain: 'cms',\n })),\n },\n }\n }\n\n context.applySession(next)\n if (context.dispatch) {\n let result: OperationResult<unknown>\n try {\n result = await context.dispatch(next)\n } catch (cause) {\n // Operations never throw by contract; treat a broken transport the same\n // as a failed command so the cell still rolls back visibly.\n result = {\n ok: false,\n error: { code: 'unknown', message: cause instanceof Error ? cause.message : String(cause) },\n }\n }\n if (!result.ok) {\n context.rollbackSession(input.doc)\n return { ok: false, error: result.error }\n }\n }\n return { ok: true, doc: next }\n}\n\n/** The message the cell renders for a rejection: the first issue beats the envelope. */\nexport function cellErrorMessage(error: OperationError): string {\n if (error.code === 'validation' && error.issues.length > 0) return error.issues[0]!.message\n return error.message\n}\n"]}
|
|
1
|
+
{"version":3,"file":"cell-edit.js","sourceRoot":"","sources":["../../../src/components/cms/cell-edit.ts"],"names":[],"mappings":"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","sourcesContent":["import { createDocumentStore, isFieldLocalized, validateDocument } from '@elytracms/core/cms-core'\nimport type {\n CmsDocument,\n CmsValidationIssue,\n CollectionDef,\n CollectionRegistry,\n DocumentRef,\n FieldDef,\n Locale,\n LocaleConfig,\n} from '@elytracms/core/cms-core'\nimport { gateValidateOptions } from './gate-validation'\nimport type { OperationError, OperationResult } from '@elytracms/studio/lib/operations'\nimport { replaceDoc } from './document-ref-utils'\nimport {\n cmsValidationIssueMessage,\n operationErrorMessage,\n operationIssueMessage,\n} from '@elytracms/studio/lib/persona/labels'\n\n/**\n * Inline cell editing for the collection table (EC-168) — the pure half.\n *\n * Everything here is plain data + functions so the cell-edit state machine is\n * testable without a DOM: which cells are editable, how Tab/Enter advance,\n * how a draft value is folded into a document (active locale only), and how a\n * commit flows through exactly one `documents.upsert` operations command with\n * optimistic apply + rollback on rejection.\n */\n\n// --- Cell identity -----------------------------------------------------------\n\n/** A cell in the collection table: one document × one field column. */\nexport interface CellCoord {\n docId: string\n field: string\n}\n\n/** Stable key for a cell (state maps, DOM lookups). */\nexport function cellKey(coord: CellCoord): string {\n return `${coord.docId}::${coord.field}`\n}\n\n// --- Editability -------------------------------------------------------------\n\n/** How a cell edits: inline control, anchored picker dialog, or not at all. */\nexport type CellEditKind = 'inline' | 'picker' | 'none'\n\nconst INLINE_TYPES: ReadonlySet<FieldDef['type']> = new Set([\n 'text',\n 'number',\n 'boolean',\n 'select',\n 'date',\n])\n\n/**\n * Classify a field's in-table editing surface. Rich text is deliberately not\n * inline-editable (the detail editor owns it); read-only fields never edit.\n */\nexport function cellEditKind(field: FieldDef, readOnly: boolean): CellEditKind {\n if (readOnly) return 'none'\n if (field.type === 'relation' || field.type === 'asset') return 'picker'\n return INLINE_TYPES.has(field.type) ? 'inline' : 'none'\n}\n\n// --- Keyboard advance --------------------------------------------------------\n\n/** Where a committed edit moves next: Tab → next, Shift-Tab → prev, Enter → down. */\nexport type AdvanceDirection = 'next' | 'prev' | 'down'\n\n/**\n * The next editable cell in the table grid, or `null` at a boundary.\n * `docIds` is the rendered row order; `fields` the editable column order.\n * 'next'/'prev' walk row-major (wrapping across rows); 'down' stays in the\n * same column.\n */\nexport function nextEditableCell(\n docIds: readonly string[],\n fields: readonly string[],\n current: CellCoord,\n direction: AdvanceDirection,\n): CellCoord | null {\n const row = docIds.indexOf(current.docId)\n const col = fields.indexOf(current.field)\n if (row < 0 || col < 0 || fields.length === 0) return null\n if (direction === 'down') {\n const nextDoc = docIds[row + 1]\n return nextDoc === undefined ? null : { docId: nextDoc, field: current.field }\n }\n const flat = row * fields.length + col + (direction === 'next' ? 1 : -1)\n if (flat < 0 || flat >= docIds.length * fields.length) return null\n const docId = docIds[Math.floor(flat / fields.length)]!\n return { docId, field: fields[flat % fields.length]! }\n}\n\n// --- Applying a draft value ----------------------------------------------------\n\n/**\n * Fold one field value into a document. Localized fields write ONLY the given\n * locale's bag (other locale variants untouched, EC-018); non-localized fields\n * write the shared `values` bag. `undefined` clears the entry.\n */\nexport function applyCellValue(\n collection: CollectionDef,\n doc: CmsDocument,\n fieldName: string,\n locale: Locale,\n value: unknown,\n): CmsDocument {\n if (isFieldLocalized(collection, fieldName)) {\n const perLocale = { ...(doc.localized?.[locale] ?? {}) }\n if (value === undefined) delete perLocale[fieldName]\n else perLocale[fieldName] = value\n return { ...doc, localized: { ...doc.localized, [locale]: perLocale } }\n }\n const values = { ...doc.values }\n if (value === undefined) delete values[fieldName]\n else values[fieldName] = value\n return { ...doc, values }\n}\n\n// --- Validation gate -----------------------------------------------------------\n\n/**\n * Error-severity issues that the edited cell itself caused. Localized fields\n * only reject on issues for the edited locale — a missing translation in some\n * *other* locale must not block this commit.\n *\n * EC-497: validates with THE bag (`gateValidateOptions`), not the `store` +\n * `localeConfig` subset it used to assemble here. A cell edit is not a weaker\n * KIND of validation — it is the same validation, FILTERED to the cell being\n * committed, which is the whole option-set contract in one function: surfaces\n * differ in what they do with the issues, never in what they validated with.\n */\nexport function cellEditIssues(\n registry: CollectionRegistry,\n documents: readonly CmsDocument[],\n localeConfig: LocaleConfig,\n collection: CollectionDef,\n next: CmsDocument,\n fieldName: string,\n locale: Locale,\n oracles: CellEditOracles = {},\n): CmsValidationIssue[] {\n // Validate against the session with the edited document swapped in, so\n // self-relations and same-document targets resolve.\n const store = createDocumentStore(replaceDoc(documents, next))\n const issues = validateDocument(\n registry,\n next,\n gateValidateOptions({\n store,\n localeConfig,\n isPublished: oracles.isPublished ?? (() => false),\n ...(oracles.assetExists ? { assetExists: oracles.assetExists } : {}),\n }),\n )\n const localized = isFieldLocalized(collection, fieldName)\n return issues.filter((issue) => {\n if (issue.severity !== 'error') return false\n if (issue.meta?.field !== fieldName) return false\n if (!localized) return true\n // Localized field paths are ['documents', key, 'localized', <locale>, <field>, …].\n const at = issue.path.indexOf('localized')\n return at < 0 || issue.path[at + 1] === locale\n })\n}\n\n// --- Commit pipeline -------------------------------------------------------------\n\n/** Outcome of one cell commit: the new document, or a structured rejection. */\nexport type CellCommitOutcome =\n | { ok: true; doc: CmsDocument }\n | { ok: false; error: OperationError }\n\nexport interface CellCommitInput {\n collection: CollectionDef\n /** The document as currently rendered (the rollback target). */\n doc: CmsDocument\n field: FieldDef\n /** The active locale at the point of edit (table locale resolution). */\n locale: Locale\n /** The committed draft value (`undefined` clears the field). */\n value: unknown\n}\n\n/**\n * The session oracles the canonical option bag needs beyond the store + locales\n * (EC-497). Optional so a fixture-only caller still gets the rest of the bag;\n * absent `isPublished` means \"nothing is published\", the engine's own default.\n */\nexport interface CellEditOracles {\n isPublished?: (ref: DocumentRef) => boolean\n assetExists?: (id: string) => boolean\n}\n\nexport interface CellCommitContext extends CellEditOracles {\n registry: CollectionRegistry\n /** Current session documents (relation/asset target resolution). */\n documents: readonly CmsDocument[]\n localeConfig: LocaleConfig\n /** Optimistically apply the edited document to the session. */\n applySession: (next: CmsDocument) => void\n /** Restore the pre-edit document after a rejected dispatch. */\n rollbackSession: (previous: CmsDocument) => void\n /**\n * Dispatch the ONE operations command for this commit\n * (`documents.upsert`, EC-141). Absent in fixture-only sessions\n * (no project scope) — the commit is session-only then.\n */\n dispatch?: (next: CmsDocument) => Promise<OperationResult<unknown>>\n}\n\n/**\n * Commit one cell edit:\n * 1. fold the value into the document (active locale only),\n * 2. gate on field validation — rejected edits dispatch NOTHING and report a\n * structured validation error for the cell,\n * 3. apply optimistically, dispatch exactly one operations command,\n * 4. roll the session back when the operation result is a rejection.\n */\nexport async function commitCellEdit(\n context: CellCommitContext,\n input: CellCommitInput,\n): Promise<CellCommitOutcome> {\n const next = applyCellValue(input.collection, input.doc, input.field.name, input.locale, input.value)\n\n const issues = cellEditIssues(\n context.registry,\n context.documents,\n context.localeConfig,\n input.collection,\n next,\n input.field.name,\n input.locale,\n {\n ...(context.isPublished ? { isPublished: context.isPublished } : {}),\n ...(context.assetExists ? { assetExists: context.assetExists } : {}),\n },\n )\n if (issues.length > 0) {\n return {\n ok: false,\n error: {\n code: 'validation',\n // EC-487/EC-494: this producer is CLIENT-side and already holds the structured\n // issue, so it resolves the message here. (Since EC-494 the wire also carries\n // the ref, which is what makes the SERVER-side publish gate localize.)\n message: cmsValidationIssueMessage(issues[0]!),\n issues: issues.map((issue) => ({\n code: issue.code,\n message: cmsValidationIssueMessage(issue),\n path: [...issue.path],\n domain: 'cms',\n })),\n },\n }\n }\n\n context.applySession(next)\n if (context.dispatch) {\n let result: OperationResult<unknown>\n try {\n result = await context.dispatch(next)\n } catch (cause) {\n // Operations never throw by contract; treat a broken transport the same\n // as a failed command so the cell still rolls back visibly.\n result = {\n ok: false,\n error: { code: 'unknown', message: cause instanceof Error ? cause.message : String(cause) },\n }\n }\n if (!result.ok) {\n context.rollbackSession(input.doc)\n return { ok: false, error: result.error }\n }\n }\n return { ok: true, doc: next }\n}\n\n/** The message the cell renders for a rejection: the first issue beats the envelope. */\nexport function cellErrorMessage(error: OperationError): string {\n // EC-494: an OperationIssue now carries its catalog ref across the wire, so a\n // rejection built SERVER-SIDE (the publish gate) localizes here too — not only the\n // studio-built ones above.\n if (error.code === 'validation' && error.issues.length > 0)\n return operationIssueMessage(error.issues[0]!)\n return operationErrorMessage(error)\n}\n"]}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ReactNode } from 'react';
|
|
2
|
-
import type { CmsDocument, CmsValidationIssue, CollectionDef, CollectionRegistry, DocumentRef, FieldDef, Locale, LocaleConfig, LocalizedString, RestoreScope } from '@elytracms/core/cms-core';
|
|
2
|
+
import type { CmsDocument, CmsValidationIssue, CollectionDef, CollectionRegistry, DocumentRef, FieldDef, Locale, LocaleConfig, LocalizedString, RestoreScope, ValidateDocumentOptions } from '@elytracms/core/cms-core';
|
|
3
3
|
import type { DocumentVersionRecord } from '@elytracms/core/persistence';
|
|
4
4
|
import type { OperationError } from '@elytracms/studio/lib/operations';
|
|
5
5
|
import type { TableViewState } from '@elytracms/studio/lib/cms-views';
|
|
@@ -134,6 +134,19 @@ export interface CmsStore {
|
|
|
134
134
|
restoreDocumentVersion(ref: DocumentRef, version: number, scope?: RestoreScope): Promise<CmsDocument | null>;
|
|
135
135
|
importDocuments(collectionId: string, docs: unknown[]): ImportReport;
|
|
136
136
|
validate(doc: CmsDocument): CmsValidationIssue[];
|
|
137
|
+
/**
|
|
138
|
+
* THE validation option bag {@link validate} uses (EC-497) — the session document
|
|
139
|
+
* store, the project locales, the composition + rich-text-embed leashes, the
|
|
140
|
+
* published-ness pointer and the real asset-library oracle, built once by
|
|
141
|
+
* `gateValidateOptions`.
|
|
142
|
+
*
|
|
143
|
+
* Exposed so a surface validating a document that is NOT yet in the session (the
|
|
144
|
+
* AI engines judge a proposal before it exists) passes the editor's rules WHOLE.
|
|
145
|
+
* EC-473 exposed the two store-dependent halves for that; handing over the bag
|
|
146
|
+
* instead removes the remaining way to get it wrong — assembling a subset at the
|
|
147
|
+
* call site, which is the defect EC-497 exists to end.
|
|
148
|
+
*/
|
|
149
|
+
validateOptions: ValidateDocumentOptions;
|
|
137
150
|
isFieldReadOnly(field: FieldDef): boolean;
|
|
138
151
|
viewsOf(collectionId: string): SavedView[];
|
|
139
152
|
activeViewId(collectionId: string): string;
|
|
@@ -211,14 +224,7 @@ export declare function buildNewDocument(collectionId: string, registry: Collect
|
|
|
211
224
|
* can never materialize a second singleton document.
|
|
212
225
|
*/
|
|
213
226
|
export declare function hasDocumentFor(docs: readonly CmsDocument[], collectionId: string): boolean;
|
|
214
|
-
|
|
215
|
-
* The CMS session store (EC-141): documents and collections are optimistic,
|
|
216
|
-
* synchronous session state (live validation while typing); every mutation —
|
|
217
|
-
* create/save/delete/state transitions — is mirrored onto the operations
|
|
218
|
-
* layer (`documents.*` / `schema.*` commands) through {@link createCmsPersister}.
|
|
219
|
-
* Failed/denied commands surface as {@link CmsStore.persistError}.
|
|
220
|
-
*/
|
|
221
|
-
export declare function CmsProvider({ children, projectId, initialCollections, initialDocuments, initialPublishedKeys, initialTimestamps, initialPublishTimestamps, initialChangedKeys, initialLocaleConfig, seedReflectsWrites, }: {
|
|
227
|
+
export interface CmsProviderProps {
|
|
222
228
|
children: ReactNode;
|
|
223
229
|
/** Enables persistence through the shared operations client. */
|
|
224
230
|
projectId?: string;
|
|
@@ -229,13 +235,23 @@ export declare function CmsProvider({ children, projectId, initialCollections, i
|
|
|
229
235
|
* Omitted → the historical mode default (connected hydrates, playground may not).
|
|
230
236
|
*/
|
|
231
237
|
seedReflectsWrites?: boolean;
|
|
232
|
-
/**
|
|
238
|
+
/**
|
|
239
|
+
* The session's collections — the project's own schema (hydration, a starter
|
|
240
|
+
* kit, or `elytra.config.ts`). Absent → **no collections** (EC-479): the store
|
|
241
|
+
* used to fall back to `cms-core`'s `sampleFilterableCollections`, so a studio
|
|
242
|
+
* whose config never arrived showed the engine's demo schema as if it were the
|
|
243
|
+
* project's. The absence is now reported at the rail (`projectConfigIssues`).
|
|
244
|
+
*/
|
|
233
245
|
initialCollections?: CollectionDef[];
|
|
246
|
+
/**
|
|
247
|
+
* The session's documents. Absent → **none** (EC-479; was `sampleDocuments`).
|
|
248
|
+
*/
|
|
234
249
|
initialDocuments?: CmsDocument[];
|
|
235
250
|
/**
|
|
236
251
|
* Published document keys from hydration (EC-224): the version pointer
|
|
237
|
-
* (`publishedVersion` set). Absent (kit / fixture sessions) →
|
|
238
|
-
*
|
|
252
|
+
* (`publishedVersion` set). Absent (kit / fixture sessions) → none published
|
|
253
|
+
* (EC-479 retired the `samplePublishedKeys` fixture default, which only ever
|
|
254
|
+
* described the engine's own sample documents).
|
|
239
255
|
*/
|
|
240
256
|
initialPublishedKeys?: readonly string[];
|
|
241
257
|
/**
|
|
@@ -261,9 +277,33 @@ export declare function CmsProvider({ children, projectId, initialCollections, i
|
|
|
261
277
|
* returned records. Absent (playground / kit) → empty set.
|
|
262
278
|
*/
|
|
263
279
|
initialChangedKeys?: readonly string[];
|
|
264
|
-
/**
|
|
280
|
+
/**
|
|
281
|
+
* Locale config for this session — the PROJECT's own (config-declared locales,
|
|
282
|
+
* or its persisted settings; see `resolveCmsInitialLocaleConfig`). Absent → the
|
|
283
|
+
* active config project's locales are read directly, and failing that the
|
|
284
|
+
* session runs with no locale at all rather than the engine's `en/de`
|
|
285
|
+
* `sampleLocaleConfig` fixture (EC-479/EC-483: the default locale is the literal
|
|
286
|
+
* storage key for localized values, so inventing one is a future data migration).
|
|
287
|
+
*/
|
|
265
288
|
initialLocaleConfig?: LocaleConfig;
|
|
266
|
-
}
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* The CMS session store (EC-141): documents and collections are optimistic,
|
|
292
|
+
* synchronous session state (live validation while typing); every mutation —
|
|
293
|
+
* create/save/delete/state transitions — is mirrored onto the operations
|
|
294
|
+
* layer (`documents.*` / `schema.*` commands) through {@link createCmsPersister}.
|
|
295
|
+
* Failed/denied commands surface as {@link CmsStore.persistError}.
|
|
296
|
+
*
|
|
297
|
+
* EC-479 — this outer component exists to make ONE property structural: a CMS
|
|
298
|
+
* session always runs in the project's own locale, or it does not run. The locale
|
|
299
|
+
* is resolved here (the caller's, else the active `elytra.config.ts` project's)
|
|
300
|
+
* and handed to {@link CmsSession} as a REQUIRED value, so no branch inside the
|
|
301
|
+
* session can reach for the engine's `sampleLocaleConfig` — the default locale is
|
|
302
|
+
* the storage key every localized value is written under (EC-483), and a session
|
|
303
|
+
* that guessed it would corrupt content rather than fail. With no locale at all
|
|
304
|
+
* the surface says so instead of editing in an invented one.
|
|
305
|
+
*/
|
|
306
|
+
export declare function CmsProvider(props: CmsProviderProps): import("react").JSX.Element;
|
|
267
307
|
export { useCms };
|
|
268
308
|
export { primaryField };
|
|
269
309
|
export type { Locale };
|
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
3
|
-
import { createCollectionRegistry, createDocumentStore, documentKey, documentSchema, resolveDocumentTitle, resolveField,
|
|
3
|
+
import { createCollectionRegistry, createDocumentStore, documentKey, documentReachabilityIssues, documentSchema, resolveDocumentTitle, resolveField, seedDefaults, resolveSingletonDocument, titleFieldOf, validateDocument, } from '@elytracms/core/cms-core';
|
|
4
4
|
import { translate } from '@elytracms/studio/lib/i18n';
|
|
5
5
|
import { configureStudioLocales, useStudioLocale } from '@elytracms/studio/lib/locale';
|
|
6
6
|
import { studioManifestLookup } from '@elytracms/studio/lib/registry';
|
|
7
|
-
import { resolveDeploymentCollections, resolveDeploymentRoutes } from '@elytracms/studio/lib/workspace';
|
|
8
|
-
import {
|
|
7
|
+
import { resolveDeploymentCollections, resolveDeploymentLocaleConfig, resolveDeploymentRoutes, } from '@elytracms/studio/lib/workspace';
|
|
8
|
+
import { Heading, Panel, Stack, Text } from '@elytracms/studio/ui';
|
|
9
|
+
import { gateValidateOptions } from './gate-validation.js';
|
|
10
|
+
import { clearSessionDocumentValidator, setSessionDocumentValidator, } from '@elytracms/studio/lib/cms/document-gate';
|
|
9
11
|
import { matchesRef, removeDoc, replaceDoc, upsertDoc } from './document-ref-utils.js';
|
|
10
12
|
import { createRequiredContext } from '@elytracms/studio/lib/required-context';
|
|
11
13
|
import { createCmsPersister, getStudioOperations } from '@elytracms/studio/lib/operations';
|
|
@@ -116,10 +118,34 @@ function defaultView(collection, pseudo = []) {
|
|
|
116
118
|
* create/save/delete/state transitions — is mirrored onto the operations
|
|
117
119
|
* layer (`documents.*` / `schema.*` commands) through {@link createCmsPersister}.
|
|
118
120
|
* Failed/denied commands surface as {@link CmsStore.persistError}.
|
|
121
|
+
*
|
|
122
|
+
* EC-479 — this outer component exists to make ONE property structural: a CMS
|
|
123
|
+
* session always runs in the project's own locale, or it does not run. The locale
|
|
124
|
+
* is resolved here (the caller's, else the active `elytra.config.ts` project's)
|
|
125
|
+
* and handed to {@link CmsSession} as a REQUIRED value, so no branch inside the
|
|
126
|
+
* session can reach for the engine's `sampleLocaleConfig` — the default locale is
|
|
127
|
+
* the storage key every localized value is written under (EC-483), and a session
|
|
128
|
+
* that guessed it would corrupt content rather than fail. With no locale at all
|
|
129
|
+
* the surface says so instead of editing in an invented one.
|
|
130
|
+
*/
|
|
131
|
+
export function CmsProvider(props) {
|
|
132
|
+
const localeConfig = props.initialLocaleConfig ?? resolveDeploymentLocaleConfig();
|
|
133
|
+
if (localeConfig === undefined)
|
|
134
|
+
return _jsx(MissingProjectLocales, {});
|
|
135
|
+
return _jsx(CmsSession, { ...props, localeConfig: localeConfig });
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* The project declares no locales, so there is no key to write localized values
|
|
139
|
+
* under. Visible and specific rather than a silent fixture locale (EC-479) — and
|
|
140
|
+
* the rail foot carries the same finding as a project-level validation issue
|
|
141
|
+
* (`projectConfigIssues`), so it is reported wherever the developer is looking.
|
|
119
142
|
*/
|
|
120
|
-
|
|
143
|
+
function MissingProjectLocales() {
|
|
144
|
+
return (_jsx(Panel, { className: "m-6 p-6", children: _jsxs(Stack, { gap: "2", children: [_jsx(Heading, { level: 2, size: "h4", children: translate('cms.session.noLocales.title') }), _jsx(Text, { size: "sm", tone: "subtle", children: translate('cms.session.noLocales.body') })] }) }));
|
|
145
|
+
}
|
|
146
|
+
function CmsSession({ children, projectId, initialCollections, initialDocuments, initialPublishedKeys, initialTimestamps, initialPublishTimestamps, initialChangedKeys, seedReflectsWrites, localeConfig, }) {
|
|
121
147
|
const [documents, setDocuments] = useState(() => {
|
|
122
|
-
const seeded = (initialDocuments ??
|
|
148
|
+
const seeded = (initialDocuments ?? []).map((d) => structuredClone(d));
|
|
123
149
|
// EC-320 (0.0.44 follow-up): merge any documents STAGED by the
|
|
124
150
|
// Components-view "Use in document…" transfer — a cross-mount handoff in
|
|
125
151
|
// EVERY mode (see document-staging.ts). EC-461: the merge rule keys on the
|
|
@@ -131,15 +157,13 @@ export function CmsProvider({ children, projectId, initialCollections, initialDo
|
|
|
131
157
|
: consumeStagedDocuments(projectId, seeded, seedReflectsWrites);
|
|
132
158
|
});
|
|
133
159
|
// Published-ness as the version POINTER (EC-224), decoupled from the flat
|
|
134
|
-
// `state` enum: seeded from hydration's pointer set
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
-
//
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
return new Set(initialDocuments ? [] : samplePublishedKeys);
|
|
142
|
-
});
|
|
160
|
+
// `state` enum: seeded from hydration's pointer set. A kit's imported content
|
|
161
|
+
// (`initialDocuments` without a pointer set) starts unpublished (drafts) until
|
|
162
|
+
// an editor publishes it, and so does a session with no pointer set at all —
|
|
163
|
+
// EC-479 retired the `samplePublishedKeys` branch, which pinned the ENGINE's
|
|
164
|
+
// sample documents live in a session that no longer has any. The flat `state`
|
|
165
|
+
// is never consulted.
|
|
166
|
+
const [publishedKeys, setPublishedKeys] = useState(() => new Set(initialPublishedKeys ?? []));
|
|
143
167
|
// EC-445: per-document last-write timestamps behind the `$updatedAt` column.
|
|
144
168
|
// Seeded from hydration (connected: the persisted `cmsDocuments.updatedAt`), then
|
|
145
169
|
// re-stamped on the common edit paths. A seeded document with no hydrated timestamp
|
|
@@ -152,7 +176,7 @@ export function CmsProvider({ children, projectId, initialCollections, initialDo
|
|
|
152
176
|
for (const [key, value] of Object.entries(initialTimestamps))
|
|
153
177
|
map.set(key, value);
|
|
154
178
|
}
|
|
155
|
-
for (const doc of initialDocuments ??
|
|
179
|
+
for (const doc of initialDocuments ?? []) {
|
|
156
180
|
const key = documentKey(doc);
|
|
157
181
|
if (!map.has(key))
|
|
158
182
|
map.set(key, now);
|
|
@@ -239,12 +263,13 @@ export function CmsProvider({ children, projectId, initialCollections, initialDo
|
|
|
239
263
|
// Collections are stateful so AI-generated schemas (EC-097) can be accepted
|
|
240
264
|
// into the session. The session schema IS the source the sort/filter
|
|
241
265
|
// offering follows (EC-177 gap 3): hydrated sessions receive the persisted
|
|
242
|
-
// schema via `initialCollections
|
|
243
|
-
//
|
|
244
|
-
//
|
|
245
|
-
//
|
|
266
|
+
// schema via `initialCollections`, which is the project's own throughout —
|
|
267
|
+
// hydration, a kit, or `elytra.config.ts` (`resolveCmsInitialCollections`).
|
|
268
|
+
// EC-479: with none, the session has NO collections and the rail says so; it
|
|
269
|
+
// no longer borrows the engine's sample schema, which offered `filterable`
|
|
270
|
+
// fields the project's real backend would reject.
|
|
246
271
|
const [collections, setCollections] = useState(() => [
|
|
247
|
-
...(initialCollections ??
|
|
272
|
+
...(initialCollections ?? []),
|
|
248
273
|
]);
|
|
249
274
|
const [persistError, setPersistError] = useState(null);
|
|
250
275
|
const seq = useRef(0);
|
|
@@ -275,9 +300,17 @@ export function CmsProvider({ children, projectId, initialCollections, initialDo
|
|
|
275
300
|
onDocumentSaved: adoptSavedRecord,
|
|
276
301
|
});
|
|
277
302
|
}
|
|
303
|
+
// EC-224: published-ness IS the version pointer (`publishedKeys`) — the SOLE
|
|
304
|
+
// studio source of truth. Declared up here (EC-497) because it is part of THE
|
|
305
|
+
// validation option bag, which the cell-edit commit — defined above the publish
|
|
306
|
+
// helpers — now also takes.
|
|
307
|
+
const isPublished = useCallback((ref) => publishedKeys.has(documentKey(ref)), [publishedKeys]);
|
|
278
308
|
const registry = useMemo(() => createCollectionRegistry(collections), [collections]);
|
|
309
|
+
// EC-497: the gate seam registers ONCE and reads through refs, so it always sees
|
|
310
|
+
// the latest session without re-registering on every keystroke.
|
|
311
|
+
const registryRef = useRef(registry);
|
|
312
|
+
registryRef.current = registry;
|
|
279
313
|
const store = useMemo(() => createDocumentStore(documents), [documents]);
|
|
280
|
-
const localeConfig = initialLocaleConfig ?? sampleLocaleConfig;
|
|
281
314
|
// Asset existence is validated against the REAL asset library (assets live in
|
|
282
315
|
// their own repository, not as CMS documents). With the ambient AssetsProvider
|
|
283
316
|
// present, the oracle is the set of uploaded asset ids — but only once loaded;
|
|
@@ -295,7 +328,12 @@ export function CmsProvider({ children, projectId, initialCollections, initialDo
|
|
|
295
328
|
}, [assetStore]);
|
|
296
329
|
// The ambient studio locale (EC-176): this session's locale config drives
|
|
297
330
|
// the switcher options; every CMS surface follows the one active locale.
|
|
298
|
-
|
|
331
|
+
// EC-479: the ambient store is `null` until this effect installs the project's
|
|
332
|
+
// config (it no longer opens on the engine's `en/de` fixture), so the FIRST
|
|
333
|
+
// render — and any surface mounted outside a CMS session — reads the project's
|
|
334
|
+
// own default here rather than a locale nobody declared.
|
|
335
|
+
const { locale } = useStudioLocale();
|
|
336
|
+
const activeLocale = locale ?? localeConfig.default;
|
|
299
337
|
useEffect(() => {
|
|
300
338
|
configureStudioLocales(localeConfig);
|
|
301
339
|
}, [localeConfig]);
|
|
@@ -338,6 +376,11 @@ export function CmsProvider({ children, projectId, initialCollections, initialDo
|
|
|
338
376
|
registry,
|
|
339
377
|
documents: documentsRef.current,
|
|
340
378
|
localeConfig,
|
|
379
|
+
// EC-497: the remaining halves of THE option bag. A cell edit now
|
|
380
|
+
// validates with exactly what the editor and the publish gate use and
|
|
381
|
+
// filters the result to the committed cell — it is not a weaker check.
|
|
382
|
+
isPublished,
|
|
383
|
+
...(assetExists ? { assetExists } : {}),
|
|
341
384
|
// EC-445: the optimistic apply bumps the $updatedAt column (a rollback
|
|
342
385
|
// leaves it — it self-corrects on the next hydration).
|
|
343
386
|
applySession: (doc) => {
|
|
@@ -362,7 +405,15 @@ export function CmsProvider({ children, projectId, initialCollections, initialDo
|
|
|
362
405
|
}
|
|
363
406
|
: {}),
|
|
364
407
|
}, input);
|
|
365
|
-
}, [
|
|
408
|
+
}, [
|
|
409
|
+
registry,
|
|
410
|
+
localeConfig,
|
|
411
|
+
projectId,
|
|
412
|
+
isPublished,
|
|
413
|
+
assetExists,
|
|
414
|
+
stampUpdatedAt,
|
|
415
|
+
adoptSavedRecord,
|
|
416
|
+
]);
|
|
366
417
|
// Shared insert sequence (EC-326): existingIds/mint/seedDefaults (via
|
|
367
418
|
// `buildNewDocument`) + optimistic append + persist, consumed by both
|
|
368
419
|
// `createDocument` (always inserts) and `ensureSingletonDocument`'s empty
|
|
@@ -438,7 +489,6 @@ export function CmsProvider({ children, projectId, initialCollections, initialDo
|
|
|
438
489
|
return next;
|
|
439
490
|
});
|
|
440
491
|
}, []);
|
|
441
|
-
const isPublished = useCallback((ref) => publishedKeys.has(documentKey(ref)), [publishedKeys]);
|
|
442
492
|
// Reconcile published-ness for a ref against the AUTHORITATIVE backend record
|
|
443
493
|
// (EC-225): the optimistic flip is a guess; once the queued op settles we read
|
|
444
494
|
// the actual pin so the badge can never claim a state the backend refused
|
|
@@ -527,18 +577,53 @@ export function CmsProvider({ children, projectId, initialCollections, initialDo
|
|
|
527
577
|
adoptSavedRecord(result.value);
|
|
528
578
|
return restored;
|
|
529
579
|
}, [projectId, adoptSavedRecord]);
|
|
530
|
-
|
|
580
|
+
/**
|
|
581
|
+
* EC-497 — THE option bag (AD-497a), built once and used by every surface that
|
|
582
|
+
* validates a CMS document. `gateValidateOptions` is the only place options are
|
|
583
|
+
* assembled; before it there were seven hand-assembled subsets and the weakest
|
|
584
|
+
* one was the publish gate. Note it carries `isPublished` too: the EC-173
|
|
585
|
+
* weak-reference warning sources published-ness from the version POINTER, so
|
|
586
|
+
* editing a published doc that references an unpublished target surfaces the
|
|
587
|
+
* fallback warning (it had gone inert when the flat `state` mirror was dropped).
|
|
588
|
+
*/
|
|
589
|
+
const validateOptions = useMemo(() => gateValidateOptions({
|
|
531
590
|
store,
|
|
532
591
|
localeConfig,
|
|
533
|
-
validateComposition,
|
|
534
|
-
validateRichTextEmbeds,
|
|
535
|
-
// EC-224: the EC-173 weak-reference warning sources published-ness from
|
|
536
|
-
// the version pointer, so editing a published doc that references an
|
|
537
|
-
// unpublished target surfaces the fallback warning again (it had gone
|
|
538
|
-
// inert once the flat `state` mirror was dropped in B-3b).
|
|
539
592
|
isPublished,
|
|
540
|
-
assetExists,
|
|
541
|
-
}), [
|
|
593
|
+
...(assetExists ? { assetExists } : {}),
|
|
594
|
+
}), [store, localeConfig, isPublished, assetExists]);
|
|
595
|
+
const validate = useCallback((doc) => {
|
|
596
|
+
const issues = validateDocument(registry, doc, validateOptions);
|
|
597
|
+
// EC-492: a document whose composed URL no request can reach — a top-level
|
|
598
|
+
// slug that collides with a declared locale code, which the host reads as a
|
|
599
|
+
// locale prefix. Not part of `validateDocument` because it needs the project
|
|
600
|
+
// ROUTES (the schema validator is route-free by design); emitted here, where
|
|
601
|
+
// the deployment's routes and the collection's sibling set are both in hand.
|
|
602
|
+
// Silent in a single-locale project — the fault is structurally impossible.
|
|
603
|
+
const collection = registry.get(doc.collection);
|
|
604
|
+
if (!collection)
|
|
605
|
+
return issues;
|
|
606
|
+
return [
|
|
607
|
+
...issues,
|
|
608
|
+
...documentReachabilityIssues({ routes: resolveDeploymentRoutes(), localeConfig }, collection, doc, { siblings: documents.filter((sibling) => sibling.collection === doc.collection) }),
|
|
609
|
+
];
|
|
610
|
+
}, [registry, validateOptions, documents, localeConfig]);
|
|
611
|
+
/**
|
|
612
|
+
* EC-497 — publish the session's canonical validator to the gate seam, so the
|
|
613
|
+
* operations-layer `documents.publish` gate judges a document with exactly these
|
|
614
|
+
* rules instead of the weaker, mode-dependent ones it read off the adapter's
|
|
615
|
+
* schema record. `[]` for a collection the config does not declare preserves the
|
|
616
|
+
* EC-183/AD-11 data-store rule (studio system collections carry no config field
|
|
617
|
+
* rules); a validator kept fresh through a ref means the seam sees the latest
|
|
618
|
+
* session without re-registering per keystroke.
|
|
619
|
+
*/
|
|
620
|
+
const gateRef = useRef(validate);
|
|
621
|
+
gateRef.current = validate;
|
|
622
|
+
useEffect(() => {
|
|
623
|
+
const owned = (doc) => registryRef.current.has(doc.collection) ? gateRef.current(doc) : [];
|
|
624
|
+
setSessionDocumentValidator(owned);
|
|
625
|
+
return () => clearSessionDocumentValidator(owned);
|
|
626
|
+
}, []);
|
|
542
627
|
const importDocuments = useCallback((collectionId, raw) => {
|
|
543
628
|
const report = { added: 0, rejected: [] };
|
|
544
629
|
// Pre-parse so intra-batch relation/asset targets resolve during validation.
|
|
@@ -565,8 +650,17 @@ export function CmsProvider({ children, projectId, initialCollections, initialDo
|
|
|
565
650
|
// Validate each against existing docs PLUS the rest of the batch.
|
|
566
651
|
const batchStore = createDocumentStore([...documents, ...parsedBatch]);
|
|
567
652
|
const accepted = [];
|
|
653
|
+
// EC-497: the same bag as everywhere else, over the batch-aware store. It
|
|
654
|
+
// used to omit the composition leashes and the published-ness pointer, so an
|
|
655
|
+
// import could land a document the editor would immediately flag.
|
|
656
|
+
const importOptions = gateValidateOptions({
|
|
657
|
+
store: batchStore,
|
|
658
|
+
localeConfig,
|
|
659
|
+
isPublished,
|
|
660
|
+
...(assetExists ? { assetExists } : {}),
|
|
661
|
+
});
|
|
568
662
|
for (const doc of parsedBatch) {
|
|
569
|
-
const issues = validateDocument(registry, doc,
|
|
663
|
+
const issues = validateDocument(registry, doc, importOptions);
|
|
570
664
|
const errors = issues.filter((iss) => iss.severity === 'error');
|
|
571
665
|
if (errors.length > 0)
|
|
572
666
|
report.rejected.push({ id: doc.id, issues: errors });
|
|
@@ -587,7 +681,7 @@ export function CmsProvider({ children, projectId, initialCollections, initialDo
|
|
|
587
681
|
report.added = accepted.length;
|
|
588
682
|
}
|
|
589
683
|
return report;
|
|
590
|
-
}, [registry, documents, localeConfig, assetExists]);
|
|
684
|
+
}, [registry, documents, localeConfig, isPublished, assetExists]);
|
|
591
685
|
const isFieldReadOnly = useCallback((field) => field.form?.readOnly === true, []);
|
|
592
686
|
// --- Views (EC-246b: config-as-code) --------------------------------------
|
|
593
687
|
// EC-445: the reserved pseudo-columns a CMS collection's list surfaces. Every
|
|
@@ -720,17 +814,17 @@ export function CmsProvider({ children, projectId, initialCollections, initialDo
|
|
|
720
814
|
return runBulkAction({
|
|
721
815
|
documents: documentsRef.current,
|
|
722
816
|
titleOf,
|
|
723
|
-
|
|
817
|
+
// EC-497: THE bag, over the freshest session snapshot (`runBulk` is
|
|
818
|
+
// async, so `documentsRef.current` — not the memoized store — is the
|
|
819
|
+
// world as it is now). It used to be assembled here and silently missed
|
|
820
|
+
// `isPublished`, so a bulk publish and a single publish of the same
|
|
821
|
+
// document could disagree about the EC-173 weak-reference warning.
|
|
822
|
+
validate: (doc) => validateDocument(registry, doc, gateValidateOptions({
|
|
724
823
|
store: createDocumentStore(documentsRef.current),
|
|
725
824
|
localeConfig,
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
// registry, so composition validity is a studio-validation invariant.
|
|
730
|
-
validateComposition,
|
|
731
|
-
validateRichTextEmbeds,
|
|
732
|
-
assetExists,
|
|
733
|
-
}),
|
|
825
|
+
isPublished,
|
|
826
|
+
...(assetExists ? { assetExists } : {}),
|
|
827
|
+
})),
|
|
734
828
|
// EC-425: bulk publish joins the single-document clean point — the
|
|
735
829
|
// SAME shared hidden-strip `doPublish` runs (EC-273 field values +
|
|
736
830
|
// EC-394 component props, session registry), per item.
|
|
@@ -775,6 +869,7 @@ export function CmsProvider({ children, projectId, initialCollections, initialDo
|
|
|
775
869
|
localeConfig,
|
|
776
870
|
activeLocale,
|
|
777
871
|
setPublished,
|
|
872
|
+
isPublished,
|
|
778
873
|
assetExists,
|
|
779
874
|
adoptSavedRecord,
|
|
780
875
|
stampHasUnpublishedChanges,
|
|
@@ -803,6 +898,7 @@ export function CmsProvider({ children, projectId, initialCollections, initialDo
|
|
|
803
898
|
restoreDocumentVersion,
|
|
804
899
|
importDocuments,
|
|
805
900
|
validate,
|
|
901
|
+
validateOptions,
|
|
806
902
|
isFieldReadOnly,
|
|
807
903
|
viewsOf,
|
|
808
904
|
activeViewId: (collectionId) => activeViews[collectionId] ?? initialViewId(collectionId),
|
|
@@ -858,6 +954,7 @@ export function CmsProvider({ children, projectId, initialCollections, initialDo
|
|
|
858
954
|
restoreDocumentVersion,
|
|
859
955
|
importDocuments,
|
|
860
956
|
validate,
|
|
957
|
+
validateOptions,
|
|
861
958
|
isFieldReadOnly,
|
|
862
959
|
viewsOf,
|
|
863
960
|
activeViews,
|