@elytracms/studio 0.0.100 → 0.0.102
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/analytics-cost.test.ts +1 -1
- package/convex/asset-meta-patch.test.ts +7 -27
- package/convex/assets.ts +6 -41
- package/convex/cms.ts +2 -29
- package/convex/delivery.ts +2 -2
- package/convex/record-functions.ts +15 -82
- package/convex/redirects.ts +11 -116
- package/convex/schema.ts +4 -23
- package/convex/shared-capture.convex.test.ts +165 -0
- package/convex/triggers.ts +7 -14
- package/dist/components/ai/ai-chat-panel-lazy.js +7 -2
- package/dist/components/ai/ai-chat-panel-lazy.js.map +1 -1
- package/dist/components/ai/ai-chat-panel.js +8 -5
- package/dist/components/ai/ai-chat-panel.js.map +1 -1
- package/dist/components/ai/proposal-card.js +2 -1
- package/dist/components/ai/proposal-card.js.map +1 -1
- package/dist/components/ai/proposal-unfilled.d.ts +14 -0
- package/dist/components/ai/proposal-unfilled.js +33 -0
- package/dist/components/ai/proposal-unfilled.js.map +1 -0
- package/dist/components/ai/translate-document-action.js +8 -6
- package/dist/components/ai/translate-document-action.js.map +1 -1
- package/dist/components/ai/translate-skipped.js +1 -0
- package/dist/components/ai/translate-skipped.js.map +1 -1
- package/dist/components/app-views/studio-config.d.ts +12 -12
- package/dist/components/app-views/studio-config.js.map +1 -1
- package/dist/components/assets/asset-detail.js +1 -2
- package/dist/components/assets/asset-detail.js.map +1 -1
- package/dist/components/assets/asset-metadata.d.ts +2 -9
- package/dist/components/assets/asset-metadata.js +3 -10
- package/dist/components/assets/asset-metadata.js.map +1 -1
- package/dist/components/assets/asset-search.js +1 -1
- package/dist/components/assets/asset-search.js.map +1 -1
- package/dist/components/assets/asset-selection.js +2 -2
- package/dist/components/assets/asset-selection.js.map +1 -1
- package/dist/components/assets/assets-collection-view-source.d.ts +2 -3
- package/dist/components/assets/assets-collection-view-source.js +2 -3
- package/dist/components/assets/assets-collection-view-source.js.map +1 -1
- package/dist/components/builder/__fixtures__/reuse-race.d.ts +30 -0
- package/dist/components/builder/__fixtures__/reuse-race.js +77 -0
- package/dist/components/builder/__fixtures__/reuse-race.js.map +1 -0
- package/dist/components/builder/builder-store.d.ts +37 -1
- package/dist/components/builder/builder-store.js +21 -6
- package/dist/components/builder/builder-store.js.map +1 -1
- package/dist/components/builder/canvas-chrome-css.d.ts +1 -1
- package/dist/components/builder/canvas-chrome-css.js +1 -1
- package/dist/components/builder/canvas-chrome-css.js.map +1 -1
- package/dist/components/builder/canvas-chrome.d.ts +1 -1
- package/dist/components/builder/canvas-chrome.js +1 -1
- package/dist/components/builder/canvas-chrome.js.map +1 -1
- package/dist/components/builder/canvas-rebase.d.ts +4 -4
- package/dist/components/builder/canvas-rebase.js +3 -2
- package/dist/components/builder/canvas-rebase.js.map +1 -1
- package/dist/components/builder/canvas-render-context.d.ts +1 -1
- package/dist/components/builder/canvas-render-context.js +1 -1
- package/dist/components/builder/canvas-render-context.js.map +1 -1
- package/dist/components/builder/composition-field-editor-lazy.js +2 -1
- package/dist/components/builder/composition-field-editor-lazy.js.map +1 -1
- package/dist/components/builder/composition-field-editor.js +3 -3
- package/dist/components/builder/composition-field-editor.js.map +1 -1
- package/dist/components/builder/reuse-context.d.ts +9 -2
- package/dist/components/builder/reuse-context.js +46 -6
- package/dist/components/builder/reuse-context.js.map +1 -1
- package/dist/components/builder/reuse.d.ts +113 -70
- package/dist/components/builder/reuse.js +123 -108
- package/dist/components/builder/reuse.js.map +1 -1
- package/dist/components/cms/asset-field.js +1 -3
- package/dist/components/cms/asset-field.js.map +1 -1
- package/dist/components/cms/cell.d.ts +2 -2
- package/dist/components/cms/cell.js +8 -18
- package/dist/components/cms/cell.js.map +1 -1
- package/dist/components/cms/cms-store.d.ts +9 -1
- package/dist/components/cms/cms-store.js +1 -0
- package/dist/components/cms/cms-store.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/field-control.js +13 -1
- package/dist/components/cms/field-control.js.map +1 -1
- package/dist/components/cms/prop-fields.js +1 -1
- package/dist/components/cms/prop-fields.js.map +1 -1
- package/dist/components/cms/rich-text/lazy.js +2 -1
- package/dist/components/cms/rich-text/lazy.js.map +1 -1
- package/dist/components/cms/rich-text/toolbar-state.d.ts +1 -1
- package/dist/components/cms/rich-text/toolbar-state.js +1 -1
- package/dist/components/cms/rich-text/toolbar-state.js.map +1 -1
- package/dist/components/cms/use-field-focus.js +3 -3
- package/dist/components/cms/use-field-focus.js.map +1 -1
- package/dist/components/cms/version-compare-lazy.js +2 -1
- package/dist/components/cms/version-compare-lazy.js.map +1 -1
- package/dist/components/components/components-index.js +1 -1
- package/dist/components/components/components-index.js.map +1 -1
- package/dist/components/deployment-setup-gate.d.ts +1 -1
- package/dist/components/deployment-setup-gate.js +1 -1
- package/dist/components/deployment-setup-gate.js.map +1 -1
- package/dist/components/load-failure.d.ts +62 -0
- package/dist/components/load-failure.js +110 -0
- package/dist/components/load-failure.js.map +1 -0
- package/dist/components/notifications/shell-findings.js +3 -3
- package/dist/components/notifications/shell-findings.js.map +1 -1
- package/dist/customization.d.ts +3 -3
- package/dist/customization.js.map +1 -1
- package/dist/lib/ai/apply-content.js +99 -28
- package/dist/lib/ai/apply-content.js.map +1 -1
- package/dist/lib/ai/chat-context.d.ts +77 -7
- package/dist/lib/ai/chat-context.js +139 -14
- package/dist/lib/ai/chat-context.js.map +1 -1
- package/dist/lib/ai/chat-state.d.ts +7 -1
- package/dist/lib/ai/chat-state.js.map +1 -1
- package/dist/lib/ai/constraints.d.ts +5 -0
- package/dist/lib/ai/constraints.js +21 -6
- package/dist/lib/ai/constraints.js.map +1 -1
- package/dist/lib/ai/fake-provider.d.ts +76 -26
- package/dist/lib/ai/fake-provider.js +210 -66
- package/dist/lib/ai/fake-provider.js.map +1 -1
- package/dist/lib/ai/index.d.ts +1 -0
- package/dist/lib/ai/index.js +1 -0
- package/dist/lib/ai/index.js.map +1 -1
- package/dist/lib/ai/keyless-assistant.d.ts +37 -0
- package/dist/lib/ai/keyless-assistant.js +193 -0
- package/dist/lib/ai/keyless-assistant.js.map +1 -0
- package/dist/lib/ai/knowledge.d.ts +9 -9
- package/dist/lib/ai/knowledge.js +22 -0
- package/dist/lib/ai/knowledge.js.map +1 -1
- package/dist/lib/ai/shape.d.ts +25 -2
- package/dist/lib/ai/shape.js +58 -7
- package/dist/lib/ai/shape.js.map +1 -1
- package/dist/lib/ai/tools.d.ts +18 -1
- package/dist/lib/ai/tools.js +74 -11
- package/dist/lib/ai/tools.js.map +1 -1
- package/dist/lib/ai/translate-document.d.ts +8 -1
- package/dist/lib/ai/translate-document.js +25 -9
- package/dist/lib/ai/translate-document.js.map +1 -1
- package/dist/lib/ai/types.d.ts +22 -7
- package/dist/lib/ai/types.js.map +1 -1
- package/dist/lib/ai/update-fields.js +13 -17
- package/dist/lib/ai/update-fields.js.map +1 -1
- package/dist/lib/ai/writer-form.d.ts +72 -3
- package/dist/lib/ai/writer-form.js +320 -42
- package/dist/lib/ai/writer-form.js.map +1 -1
- package/dist/lib/assets/upload.d.ts +1 -2
- package/dist/lib/assets/upload.js +1 -2
- package/dist/lib/assets/upload.js.map +1 -1
- package/dist/lib/auth-errors.d.ts +1 -1
- package/dist/lib/auth-errors.js +1 -1
- package/dist/lib/auth-errors.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.d.ts +1 -1
- package/dist/lib/i18n/messages.de.js +32 -8
- package/dist/lib/i18n/messages.de.js.map +1 -1
- package/dist/lib/i18n/messages.en.d.ts +25 -3
- package/dist/lib/i18n/messages.en.js +40 -10
- package/dist/lib/i18n/messages.en.js.map +1 -1
- package/dist/lib/operations/cms-persistence.d.ts +28 -0
- package/dist/lib/operations/cms-persistence.js +55 -0
- package/dist/lib/operations/cms-persistence.js.map +1 -1
- package/dist/lib/persistence/convex-adapter.js +7 -2
- package/dist/lib/persistence/convex-adapter.js.map +1 -1
- package/dist/lib/workspace/index.d.ts +3 -3
- package/dist/lib/workspace/index.js +3 -3
- package/dist/lib/workspace/index.js.map +1 -1
- package/dist/router/navigate-once.d.ts +1 -1
- package/dist/router/navigate-once.js.map +1 -1
- package/dist/router/root.d.ts +3 -2
- package/dist/router/root.js +25 -4
- package/dist/router/root.js.map +1 -1
- package/package.json +3 -3
- package/convex/asset-alt-migration.test.ts +0 -127
- package/convex/redirect-migration.test.ts +0 -205
- package/convex/version-kind-stamping.test.ts +0 -118
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CmsDocument, CollectionDef, FieldDef, PropFieldLookup, ValuePath, ValuePosition } from '@elytracms/core/cms-core';
|
|
1
|
+
import type { CmsDocument, CollectionDef, FieldDef, PropFieldLookup, ValuePath, ValuePosition, WriterLock } from '@elytracms/core/cms-core';
|
|
2
2
|
import type { WriterLeash } from './types.js';
|
|
3
3
|
/** The ids a document already references — reusable anywhere in it without a search. */
|
|
4
4
|
export interface HeldReferences {
|
|
@@ -34,7 +34,14 @@ export interface WriterFormProblem {
|
|
|
34
34
|
readonly message: string;
|
|
35
35
|
/** An id outside the leash: it was REMOVED from the converted value. */
|
|
36
36
|
readonly leash?: true;
|
|
37
|
+
/** A write to a writer-locked position (EC-716): the position KEPT its current value. */
|
|
38
|
+
readonly locked?: true;
|
|
37
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* What a writer is told about a write to a locked position (EC-716) — one wording for the
|
|
42
|
+
* create's dropped-value warning, the edit's refusal and a component prop's refusal.
|
|
43
|
+
*/
|
|
44
|
+
export declare function lockedWriteMessage(subjectText: string, lock: WriterLock): string;
|
|
38
45
|
export interface FromWriterFormResult {
|
|
39
46
|
/** The stored form (`undefined` — clear the position). */
|
|
40
47
|
readonly value: unknown;
|
|
@@ -49,6 +56,40 @@ export interface FromWriterFormResult {
|
|
|
49
56
|
export declare function fromWriterForm(position: ValuePosition, current: unknown, ctx: WriterFormContext): FromWriterFormResult;
|
|
50
57
|
/** {@link fromWriterForm} for a top-level field's value. */
|
|
51
58
|
export declare function fromWriterFieldValue(field: FieldDef, raw: unknown, current: unknown, ctx: WriterFormContext, locale?: string): FromWriterFormResult;
|
|
59
|
+
/**
|
|
60
|
+
* One writer-locked position under a value — the TOPMOST locked one on its branch (it stands
|
|
61
|
+
* for everything below it, which is locked too) — with its value (possibly absent: the
|
|
62
|
+
* position exists because its holder does), its lock and its location below the field.
|
|
63
|
+
*/
|
|
64
|
+
export interface LockedPosition {
|
|
65
|
+
readonly value: unknown;
|
|
66
|
+
readonly lock: WriterLock;
|
|
67
|
+
readonly at: readonly (string | number)[];
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* What a writer is told about a write that would remove locked content (EC-738), located at
|
|
71
|
+
* the HOLDER the model wrote. Read-only content is named (the model reads it); excluded
|
|
72
|
+
* content never is (EC-741: the model never sees it, so naming its path would disclose it).
|
|
73
|
+
*/
|
|
74
|
+
export declare function lockedRemovalMessage(holder: string, root: string, locked: readonly LockedPosition[], shortened: boolean): string;
|
|
75
|
+
/**
|
|
76
|
+
* The value at `position` with every writer-locked position taken from `current` by key
|
|
77
|
+
* (EC-738, translate): where `current` holds that position — its value, an absent one
|
|
78
|
+
* included — the current one wins; a position `current` does not hold at all (a new item, a
|
|
79
|
+
* node it lacks) keeps `position`'s own. {@link lockedChanges} then says whether that kept
|
|
80
|
+
* the locked content as it is.
|
|
81
|
+
*/
|
|
82
|
+
export declare function withLockedFrom(position: ValuePosition, current: unknown, ctx: Pick<WriterFormContext, 'lookupManifest'>): unknown;
|
|
83
|
+
/**
|
|
84
|
+
* The locked values a write from `position.value` (the CURRENT value) to `next` would lose or
|
|
85
|
+
* create, judged both ways (EC-738, translate): `lost` — a defined locked value `next` does
|
|
86
|
+
* not hold unchanged; `created` — a defined locked value `next` holds where the current value
|
|
87
|
+
* holds none.
|
|
88
|
+
*/
|
|
89
|
+
export declare function lockedChanges(position: ValuePosition, next: unknown, ctx: Pick<WriterFormContext, 'lookupManifest'>): {
|
|
90
|
+
readonly lost: readonly LockedPosition[];
|
|
91
|
+
readonly created: readonly LockedPosition[];
|
|
92
|
+
};
|
|
52
93
|
export type FromWriterFormAtNodeResult = {
|
|
53
94
|
readonly ok: true;
|
|
54
95
|
/** The field's next value (the displayed value with the one prop converted). */
|
|
@@ -84,8 +125,9 @@ export declare function isTextPosition(position: ValuePosition): boolean;
|
|
|
84
125
|
* same traversal"): a `text` value, and a `richText` / `blocks` sequence (its prose —
|
|
85
126
|
* the sequence's own text — is the caller's to split) together with every text position
|
|
86
127
|
* below it: component text props in a composition and in rich-text embeds, object
|
|
87
|
-
* sub-fields, repeater items, asset placement sub-fields.
|
|
88
|
-
* everything below it
|
|
128
|
+
* sub-fields, repeater items, asset placement sub-fields. A writer-locked position
|
|
129
|
+
* (EC-716: `ai.exclude` or `form.readOnly`) and everything below it is not text a writer
|
|
130
|
+
* may touch — not a unit, and so never a skip either. Depth-first, document order.
|
|
89
131
|
*/
|
|
90
132
|
export declare function textPositionsOf(position: ValuePosition, ctx: Pick<WriterFormContext, 'lookupManifest'>): ValuePosition[];
|
|
91
133
|
/**
|
|
@@ -104,6 +146,16 @@ export declare function rebuildTextPositions(position: ValuePosition, ctx: Pick<
|
|
|
104
146
|
* stays, so nothing echoed back can silently lose structure), at EVERY position: a
|
|
105
147
|
* top-level field, an object sub-field, a repeater item, a component's rich-text prop.
|
|
106
148
|
* Everything else reads as stored (envelopes are writer forms too).
|
|
149
|
+
*
|
|
150
|
+
* **THE model-read projection** (EC-741): a position core's `writerLockOf` marks
|
|
151
|
+
* `excluded` (`ai: { exclude: true }` — "the model never sees or writes the field") reads
|
|
152
|
+
* as ABSENT, and so does everything below it, at every depth: a sub-field, a repeater
|
|
153
|
+
* item's sub-field, a placement sub-field, a component prop in a composition, a slot or
|
|
154
|
+
* typography content. A `read-only` position stays readable. Every model read crosses this
|
|
155
|
+
* one fold — `getDocument`'s output, the titles the model is shown
|
|
156
|
+
* ({@link modelFacingTitle}) and the echo checks (a value the model read and sent back is
|
|
157
|
+
* no change) — so what the model can see and what it can echo are one declaration. The raw
|
|
158
|
+
* stored document stays local: proposals are always built and validated against it.
|
|
107
159
|
*/
|
|
108
160
|
export declare function toWriterForm(position: ValuePosition, ctx: Pick<WriterFormContext, 'lookupManifest'>): unknown;
|
|
109
161
|
/**
|
|
@@ -112,5 +164,22 @@ export declare function toWriterForm(position: ValuePosition, ctx: Pick<WriterFo
|
|
|
112
164
|
* — in first-seen order. What the `getDocument` output's prop catalog lists.
|
|
113
165
|
*/
|
|
114
166
|
export declare function componentIdsOf(collection: CollectionDef, document: CmsDocument, lookupManifest: PropFieldLookup): string[];
|
|
167
|
+
/**
|
|
168
|
+
* {@link toWriterForm} over one values bag: each declared field in its read form, an
|
|
169
|
+
* excluded one (and an excluded position inside any field) left out. A key the collection
|
|
170
|
+
* does not declare reads as stored (AD-14: the schema is a lens).
|
|
171
|
+
*/
|
|
172
|
+
export declare function toWriterValues(collection: CollectionDef, bag: Readonly<Record<string, unknown>>, ctx: Pick<WriterFormContext, 'lookupManifest'>, locale?: string): Record<string, unknown>;
|
|
115
173
|
/** {@link toWriterForm} over every field of every bucket of a document. */
|
|
116
174
|
export declare function toWriterDocument(collection: CollectionDef, document: CmsDocument, ctx: Pick<WriterFormContext, 'lookupManifest'>): CmsDocument;
|
|
175
|
+
/**
|
|
176
|
+
* The title a MODEL is shown for a document (EC-741) — core's `resolveDocumentTitle` over
|
|
177
|
+
* the projected values ({@link toWriterValues}), so an excluded field never reaches the
|
|
178
|
+
* model as a title: neither as the named or first-field fallback, nor through a function
|
|
179
|
+
* `titleField` that reads it (the function sees the field absent). Every title the model
|
|
180
|
+
* reads goes through here: the `updateFields` / `createDocument` outputs, the
|
|
181
|
+
* `searchDocuments` results (their query is matched against this title too, so an
|
|
182
|
+
* excluded value cannot be probed by search) and the system prompt's "currently open"
|
|
183
|
+
* line. `values` is the bag the caller resolves titles from (a locale's merged values).
|
|
184
|
+
*/
|
|
185
|
+
export declare function modelFacingTitle(collection: CollectionDef, values: Readonly<Record<string, unknown>>, fallbackId: string, ctx?: Pick<WriterFormContext, 'lookupManifest'>): string;
|
|
@@ -36,11 +36,25 @@
|
|
|
36
36
|
* (`FIELD_TYPES.blocks.writer === 'composition'`), and a rich-text value must keep the
|
|
37
37
|
* component nodes it holds (same ids, same components, same places). A component's
|
|
38
38
|
* PROP is content — {@link fromWriterFormAtNode} changes one, by its node, at any depth
|
|
39
|
-
* the traversal reaches
|
|
39
|
+
* the traversal reaches;
|
|
40
|
+
* - **the writer lock** (EC-716, owner ruling "Yes, honour readOnly") — a position core's
|
|
41
|
+
* `writerLockOf` locks (`ai.exclude` or `form.readOnly`, and everything below it) keeps
|
|
42
|
+
* its CURRENT value. Leaving it out of the written value, or echoing it back unchanged,
|
|
43
|
+
* is no write; any other value is reported as a `locked` problem (create drops it, edit
|
|
44
|
+
* refuses — the same two policies as the id leash);
|
|
45
|
+
* - **the locked-content judge** (EC-738) — a write to a WRITABLE position must not erase
|
|
46
|
+
* the locked content below it, which its own walk never visits: a holder omitted below
|
|
47
|
+
* the write's root keeps its current value whole (omission is no write — and the model
|
|
48
|
+
* may not know the holder holds anything, EC-741); an explicit removal (a root `null` /
|
|
49
|
+
* omitted value, a nested `null`, a shorter list, a removed asset entry) of a holder whose
|
|
50
|
+
* current value holds a defined locked value is a `locked` problem, located at the holder,
|
|
51
|
+
* and the holder keeps its current value. A repeater whose items hold one cannot be
|
|
52
|
+
* shortened at all (items are matched by index). Translate reuses the judge
|
|
53
|
+
* ({@link withLockedFrom}, {@link lockedChanges}).
|
|
40
54
|
*
|
|
41
55
|
* Pure and deterministic; no provider, no I/O.
|
|
42
56
|
*/
|
|
43
|
-
import { carryAssetPlacements, childPositions, fieldPath, fieldTypeOf, fieldValuePosition, isSequenceValue, rebuildFromChildren, referenceNode, sequenceComponentNodes, walkReferences, walkValuePositions, } from '@elytracms/core/cms-core';
|
|
57
|
+
import { carryAssetPlacements, childPositions, fieldPath, fieldTypeOf, fieldValuePosition, isSequenceValue, readAssetId, rebuildFromChildren, referenceNode, resolveDocumentTitle, seedObjectItem, sequenceComponentNodes, walkReferences, walkValuePositions, writerLockOf, } from '@elytracms/core/cms-core';
|
|
44
58
|
import { parseRichTextValue, richTextValueToMarkdownProse } from '@elytracms/core/rich-text';
|
|
45
59
|
import { richTextValueFromModelInput } from './rich-text-input.js';
|
|
46
60
|
export const NO_HELD_REFERENCES = { documents: new Map(), assets: new Set() };
|
|
@@ -65,6 +79,15 @@ export function heldReferencesOf(document) {
|
|
|
65
79
|
});
|
|
66
80
|
return { documents, assets };
|
|
67
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* What a writer is told about a write to a locked position (EC-716) — one wording for the
|
|
84
|
+
* create's dropped-value warning, the edit's refusal and a component prop's refusal.
|
|
85
|
+
*/
|
|
86
|
+
export function lockedWriteMessage(subjectText, lock) {
|
|
87
|
+
return lock === 'excluded'
|
|
88
|
+
? `${subjectText} must not be edited by the assistant.`
|
|
89
|
+
: `${subjectText} is read-only — no one changes it in the studio, the assistant included.`;
|
|
90
|
+
}
|
|
68
91
|
// --- Writer form → stored form -------------------------------------------------------
|
|
69
92
|
/**
|
|
70
93
|
* Convert a value a model authored at `position` (its `value` is the WRITER form) into
|
|
@@ -73,8 +96,12 @@ export function heldReferencesOf(document) {
|
|
|
73
96
|
* which the placement carry and the composition leash read. `null` / `undefined` clears.
|
|
74
97
|
*/
|
|
75
98
|
export function fromWriterForm(position, current, ctx) {
|
|
99
|
+
return convertRoot(position, current, ctx, position.field.name);
|
|
100
|
+
}
|
|
101
|
+
/** One write, rooted at `position`; `root` names the top-level field in messages. */
|
|
102
|
+
function convertRoot(position, current, ctx, root) {
|
|
76
103
|
const problems = [];
|
|
77
|
-
const value = convert(position, current, ctx, problems,
|
|
104
|
+
const value = convert(position, current, ctx, problems, root, true);
|
|
78
105
|
return { value, problems };
|
|
79
106
|
}
|
|
80
107
|
/** {@link fromWriterForm} for a top-level field's value. */
|
|
@@ -132,9 +159,196 @@ function convertChildren(pos, current, ctx, problems, root) {
|
|
|
132
159
|
currentChildren.set(childKey(pos, child), child.value);
|
|
133
160
|
}
|
|
134
161
|
}
|
|
135
|
-
return rebuildFromChildren(pos, positionCtx, (child) => convert(child, currentChildren.get(childKey(pos, child)), ctx, problems, root));
|
|
162
|
+
return rebuildFromChildren(pos, positionCtx, (child) => convert(child, currentChildren.get(childKey(pos, child)), ctx, problems, root, false));
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* `isRoot` — whether `pos` is the write's root (the field, or the prop a node change
|
|
166
|
+
* names): there an omitted value CLEARS (`update-fields`: "`null` / omitted clears");
|
|
167
|
+
* below it an omitted holder of locked content is kept whole (EC-738).
|
|
168
|
+
*/
|
|
169
|
+
function convert(pos, current, ctx, problems, root, isRoot) {
|
|
170
|
+
// EC-716: a locked position keeps its current value — and so, by not being visited,
|
|
171
|
+
// does everything below it (the lock cascades as the form's `readOnly` does).
|
|
172
|
+
const lock = writerLockOf(pos.field);
|
|
173
|
+
if (lock === undefined)
|
|
174
|
+
return convertWritable(pos, current, ctx, problems, root, isRoot);
|
|
175
|
+
if (pos.value === undefined || isEcho(pos, current, ctx, root))
|
|
176
|
+
return current;
|
|
177
|
+
problems.push({
|
|
178
|
+
at: pos.at,
|
|
179
|
+
code: 'invalid-field-value',
|
|
180
|
+
message: lockedWriteMessage(`Field ${subject(pos, root)}`, lock),
|
|
181
|
+
locked: true,
|
|
182
|
+
});
|
|
183
|
+
return current;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* {@link convert} for a WRITABLE position, judged for the locked content below it (EC-738).
|
|
187
|
+
* The conversion never visits a locked position its written value no longer holds (a
|
|
188
|
+
* cleared object, a shortened list, a removed asset entry), so the judge compares the locked
|
|
189
|
+
* values under the CURRENT value with those under the converted one.
|
|
190
|
+
*/
|
|
191
|
+
function convertWritable(pos, current, ctx, problems, root, isRoot) {
|
|
192
|
+
if (!isPresent(current))
|
|
193
|
+
return convertValue(pos, current, ctx, problems, root);
|
|
194
|
+
const positionCtx = ctx.lookupManifest ? { lookupManifest: ctx.lookupManifest } : {};
|
|
195
|
+
const before = lockedPositionsUnder({ ...pos, value: current }, positionCtx);
|
|
196
|
+
const held = [...before.values()].filter((entry) => isPresent(entry.value));
|
|
197
|
+
if (held.length === 0)
|
|
198
|
+
return convertValue(pos, current, ctx, problems, root);
|
|
199
|
+
// An omission below the write's root is no write (EC-716's rule for a locked position,
|
|
200
|
+
// applied to the holder of one): the holder keeps its current value, whole.
|
|
201
|
+
if (!isRoot && pos.value === undefined)
|
|
202
|
+
return current;
|
|
203
|
+
const next = convertValue(pos, current, ctx, problems, root);
|
|
204
|
+
const lost = lockedLosses(before, lockedPositionsUnder({ ...pos, value: next }, positionCtx));
|
|
205
|
+
// A repeater's items are matched by INDEX: removing one shifts every later item's
|
|
206
|
+
// writable content onto the locked values of the item before it — invisible to a per-key
|
|
207
|
+
// comparison, so any shortening of a repeater that holds locked content is refused.
|
|
208
|
+
const shortened = pos.field.type === 'object' &&
|
|
209
|
+
pos.field.multiple === true &&
|
|
210
|
+
Array.isArray(current) &&
|
|
211
|
+
(Array.isArray(next) ? next.length : 0) < current.length;
|
|
212
|
+
if (lost.length === 0 && !shortened)
|
|
213
|
+
return next;
|
|
214
|
+
problems.push({
|
|
215
|
+
at: pos.at,
|
|
216
|
+
code: 'invalid-field-value',
|
|
217
|
+
message: lockedRemovalMessage(subject(pos, root), root, lost.length > 0 ? lost : held, shortened),
|
|
218
|
+
locked: true,
|
|
219
|
+
});
|
|
220
|
+
return current;
|
|
221
|
+
}
|
|
222
|
+
// --- The locked-content judge (EC-738) -----------------------------------------------
|
|
223
|
+
/** A value that is there (`undefined` / `null` — nothing to lose; the converter's own rule). */
|
|
224
|
+
function isPresent(value) {
|
|
225
|
+
return value !== undefined && value !== null;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* The key matching a child position across two values of one position: a component prop by
|
|
229
|
+
* its NODE (ids are unique in a document and survive a prose edit that shifts indices), an
|
|
230
|
+
* entry of an asset LIST by its asset id and occurrence (the carry's own matching,
|
|
231
|
+
* `carryAssetPlacements` — a reordered gallery keeps each placement), anything else by its
|
|
232
|
+
* location below the parent (a repeater item by index, a sub-field by name).
|
|
233
|
+
*/
|
|
234
|
+
function lockedChildKey(parent, parentKey, child) {
|
|
235
|
+
if (child.kind === 'prop' && child.component)
|
|
236
|
+
return `node:${child.component.nodeId}:${child.field.name}`;
|
|
237
|
+
const below = child.at.slice(parent.at.length);
|
|
238
|
+
if (parent.field.type === 'asset' && parent.field.multiple !== false && Array.isArray(parent.value)) {
|
|
239
|
+
const index = below[0];
|
|
240
|
+
const entries = parent.value;
|
|
241
|
+
const id = readAssetId(entries[index]);
|
|
242
|
+
const occurrence = entries.slice(0, index).filter((entry) => readAssetId(entry) === id).length;
|
|
243
|
+
return `${parentKey}/asset:${JSON.stringify(id)}#${occurrence}/${JSON.stringify(below.slice(1))}`;
|
|
244
|
+
}
|
|
245
|
+
return `${parentKey}/${JSON.stringify(below)}`;
|
|
246
|
+
}
|
|
247
|
+
/** Every topmost writer-locked position at or below `position`, by {@link lockedChildKey}. */
|
|
248
|
+
function lockedPositionsUnder(position, positionCtx) {
|
|
249
|
+
const out = new Map();
|
|
250
|
+
const visit = (pos, key) => {
|
|
251
|
+
const lock = writerLockOf(pos.field);
|
|
252
|
+
if (lock !== undefined) {
|
|
253
|
+
out.set(key, { value: pos.value, lock, at: pos.at });
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
for (const child of childPositions(pos, positionCtx))
|
|
257
|
+
visit(child, lockedChildKey(pos, key, child));
|
|
258
|
+
};
|
|
259
|
+
visit(position, '');
|
|
260
|
+
return out;
|
|
261
|
+
}
|
|
262
|
+
/** The defined locked values of `before` that `after` does not hold, unchanged, at their key. */
|
|
263
|
+
function lockedLosses(before, after) {
|
|
264
|
+
return [...before].flatMap(([key, entry]) => {
|
|
265
|
+
if (!isPresent(entry.value))
|
|
266
|
+
return [];
|
|
267
|
+
const now = after.get(key);
|
|
268
|
+
return now && isPresent(now.value) && canonicalJson(now.value) === canonicalJson(entry.value) ? [] : [entry];
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* What a writer is told about a write that would remove locked content (EC-738), located at
|
|
273
|
+
* the HOLDER the model wrote. Read-only content is named (the model reads it); excluded
|
|
274
|
+
* content never is (EC-741: the model never sees it, so naming its path would disclose it).
|
|
275
|
+
*/
|
|
276
|
+
export function lockedRemovalMessage(holder, root, locked, shortened) {
|
|
277
|
+
const excluded = locked.some((entry) => entry.lock === 'excluded');
|
|
278
|
+
const what = excluded
|
|
279
|
+
? 'content the assistant must not change'
|
|
280
|
+
: `read-only content (${locked.map((entry) => label(root, entry.at)).join(', ')}) — no one changes it in the studio, the assistant included`;
|
|
281
|
+
return shortened
|
|
282
|
+
? `Field ${holder} holds ${what}; the assistant cannot remove items from it (items are matched by position, so removing one would delete that content or move it onto another item). Keep every item and change only their other values.`
|
|
283
|
+
: `Field ${holder} holds ${what}; the assistant cannot clear or remove it. Change only its other values.`;
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* The value at `position` with every writer-locked position taken from `current` by key
|
|
287
|
+
* (EC-738, translate): where `current` holds that position — its value, an absent one
|
|
288
|
+
* included — the current one wins; a position `current` does not hold at all (a new item, a
|
|
289
|
+
* node it lacks) keeps `position`'s own. {@link lockedChanges} then says whether that kept
|
|
290
|
+
* the locked content as it is.
|
|
291
|
+
*/
|
|
292
|
+
export function withLockedFrom(position, current, ctx) {
|
|
293
|
+
const positionCtx = ctx.lookupManifest ? { lookupManifest: ctx.lookupManifest } : {};
|
|
294
|
+
const held = lockedPositionsUnder({ ...position, value: current }, positionCtx);
|
|
295
|
+
const rebuild = (pos, key) => {
|
|
296
|
+
if (writerLockOf(pos.field) !== undefined)
|
|
297
|
+
return held.has(key) ? held.get(key).value : pos.value;
|
|
298
|
+
return rebuildFromChildren(pos, positionCtx, (child) => rebuild(child, lockedChildKey(pos, key, child)));
|
|
299
|
+
};
|
|
300
|
+
return rebuild(position, '');
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* The locked values a write from `position.value` (the CURRENT value) to `next` would lose or
|
|
304
|
+
* create, judged both ways (EC-738, translate): `lost` — a defined locked value `next` does
|
|
305
|
+
* not hold unchanged; `created` — a defined locked value `next` holds where the current value
|
|
306
|
+
* holds none.
|
|
307
|
+
*/
|
|
308
|
+
export function lockedChanges(position, next, ctx) {
|
|
309
|
+
const positionCtx = ctx.lookupManifest ? { lookupManifest: ctx.lookupManifest } : {};
|
|
310
|
+
const before = lockedPositionsUnder(position, positionCtx);
|
|
311
|
+
const after = lockedPositionsUnder({ ...position, value: next }, positionCtx);
|
|
312
|
+
return {
|
|
313
|
+
lost: lockedLosses(before, after),
|
|
314
|
+
created: [...after].flatMap(([key, entry]) => isPresent(entry.value) && !isPresent(before.get(key)?.value) ? [entry] : []),
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Whether a value written to a locked position leaves it as it is: a clear of an empty
|
|
319
|
+
* position, or the current value echoed back — as stored, in the form the model READ it
|
|
320
|
+
* in ({@link toWriterForm}: rich text as markdown), or in any writer form that converts to
|
|
321
|
+
* exactly the stored value.
|
|
322
|
+
*/
|
|
323
|
+
function isEcho(pos, current, ctx, root) {
|
|
324
|
+
const written = pos.value;
|
|
325
|
+
if (written === null)
|
|
326
|
+
return current === undefined || current === null;
|
|
327
|
+
if (current === undefined || current === null)
|
|
328
|
+
return false;
|
|
329
|
+
const json = canonicalJson(written);
|
|
330
|
+
if (json === canonicalJson(current))
|
|
331
|
+
return true;
|
|
332
|
+
if (json === canonicalJson(toWriterForm({ ...pos, value: current }, ctx)))
|
|
333
|
+
return true;
|
|
334
|
+
const scratch = [];
|
|
335
|
+
const stored = convertValue(pos, current, ctx, scratch, root);
|
|
336
|
+
return scratch.length === 0 && canonicalJson(stored) === canonicalJson(current);
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* A repeater's current value with every item the written list adds seeded the way a
|
|
340
|
+
* manual "add item" seeds it (core's `seedObjectItem`), so a locked sub-field of a NEW item
|
|
341
|
+
* keeps its default rather than staying empty (EC-716, sol-C 22 at item level). Items are
|
|
342
|
+
* matched by index — the matching the converter uses for every repeater.
|
|
343
|
+
*/
|
|
344
|
+
function withSeededItems(current, length, fields) {
|
|
345
|
+
const items = Array.isArray(current) ? current : [];
|
|
346
|
+
if (items.length >= length)
|
|
347
|
+
return current;
|
|
348
|
+
return [...items, ...Array.from({ length: length - items.length }, () => seedObjectItem(fields))];
|
|
136
349
|
}
|
|
137
|
-
|
|
350
|
+
/** {@link convert} for a position no lock holds: the writer form → the stored form. */
|
|
351
|
+
function convertValue(pos, current, ctx, problems, root) {
|
|
138
352
|
const { field } = pos;
|
|
139
353
|
const raw = pos.value === null ? undefined : pos.value;
|
|
140
354
|
const report = (code, message, leash) => {
|
|
@@ -144,8 +358,13 @@ function convert(pos, current, ctx, problems, root) {
|
|
|
144
358
|
switch (fieldTypeOf(field).writer) {
|
|
145
359
|
case 'composition': {
|
|
146
360
|
// 4c: a composition is never written whole — the value stays exactly what it is
|
|
147
|
-
// (re-sending it unchanged is no change); its props change one at a time.
|
|
148
|
-
|
|
361
|
+
// (re-sending it unchanged is no change); its props change one at a time. EC-741:
|
|
362
|
+
// "unchanged" includes the value as the model READ it ({@link toWriterForm}: excluded
|
|
363
|
+
// props absent, rich-text props as markdown) — the rule `isEcho` reads.
|
|
364
|
+
const json = canonicalJson(raw);
|
|
365
|
+
if (json === canonicalJson(current))
|
|
366
|
+
return current;
|
|
367
|
+
if (current !== undefined && json === canonicalJson(toWriterForm({ ...pos, value: current }, ctx)))
|
|
149
368
|
return current;
|
|
150
369
|
return report('invalid-field-value', `Field ${subject(pos, root)} is a page-builder composition: adding, reordering or removing its components is done on the canvas, not by the assistant. Change one component's prop with blockId + prop (the component's id from getDocument).`);
|
|
151
370
|
}
|
|
@@ -246,9 +465,15 @@ function convert(pos, current, ctx, problems, root) {
|
|
|
246
465
|
return undefined;
|
|
247
466
|
return relation.multiple === true ? refs : refs[0];
|
|
248
467
|
}
|
|
249
|
-
case 'object':
|
|
468
|
+
case 'object': {
|
|
250
469
|
// A value of the wrong shape has no children; the validator names it.
|
|
251
|
-
|
|
470
|
+
if (raw === undefined)
|
|
471
|
+
return undefined;
|
|
472
|
+
const base = field.type === 'object' && field.multiple && Array.isArray(raw)
|
|
473
|
+
? withSeededItems(current, raw.length, field.fields)
|
|
474
|
+
: current;
|
|
475
|
+
return convertChildren({ ...pos, value: raw }, base, ctx, problems, root);
|
|
476
|
+
}
|
|
252
477
|
case 'value':
|
|
253
478
|
return raw;
|
|
254
479
|
}
|
|
@@ -273,16 +498,31 @@ export function fromWriterFormAtNode(field, displayed, nodeId, prop, raw, ctx, l
|
|
|
273
498
|
const root = fieldValuePosition(field, displayed, locale !== undefined ? { locale } : {});
|
|
274
499
|
const isTarget = (position) => position.kind === 'prop' && position.component?.nodeId === nodeId && position.field.name === prop;
|
|
275
500
|
let target;
|
|
501
|
+
// EC-716: the lock the target sits under — its own, or the nearest locked ancestor's
|
|
502
|
+
// (a component embedded in a read-only rich-text prop, a prop of a locked object…).
|
|
503
|
+
let targetLock;
|
|
276
504
|
const declared = [];
|
|
277
505
|
let componentId;
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
506
|
+
const visit = (position, inherited) => {
|
|
507
|
+
const lock = writerLockOf(position.field) ?? inherited;
|
|
508
|
+
if (position.kind === 'prop' && position.component?.nodeId === nodeId) {
|
|
509
|
+
componentId = position.component.componentId;
|
|
510
|
+
declared.push(position.field.name);
|
|
511
|
+
if (target === undefined && position.field.name === prop) {
|
|
512
|
+
target = position;
|
|
513
|
+
targetLock = lock;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
for (const child of childPositions(position, positionCtx))
|
|
517
|
+
visit(child, lock);
|
|
518
|
+
};
|
|
519
|
+
visit(root, undefined);
|
|
520
|
+
if (target && targetLock !== undefined) {
|
|
521
|
+
return {
|
|
522
|
+
ok: false,
|
|
523
|
+
message: lockedWriteMessage(`Prop "${prop}" of component "${nodeId}" (${componentId})`, targetLock),
|
|
524
|
+
};
|
|
525
|
+
}
|
|
286
526
|
if (!target) {
|
|
287
527
|
if (componentId === undefined) {
|
|
288
528
|
return {
|
|
@@ -296,7 +536,9 @@ export function fromWriterFormAtNode(field, displayed, nodeId, prop, raw, ctx, l
|
|
|
296
536
|
};
|
|
297
537
|
}
|
|
298
538
|
const found = target;
|
|
299
|
-
|
|
539
|
+
// The prop is the write's ROOT (an omitted value clears it); messages locate positions
|
|
540
|
+
// from the top-level field (`body.content[0].props.group.locked`).
|
|
541
|
+
const { value: converted, problems } = convertRoot({ ...found, value: raw }, found.value, ctx, field.name);
|
|
300
542
|
const rebuild = (position) => rebuildFromChildren(position, positionCtx, (child) => (isTarget(child) ? converted : rebuild(child)));
|
|
301
543
|
const value = rebuild(root);
|
|
302
544
|
return found.exact
|
|
@@ -309,22 +551,24 @@ export function isTextPosition(position) {
|
|
|
309
551
|
const { type } = position.field;
|
|
310
552
|
return type === 'text' || type === 'richText' || type === 'blocks';
|
|
311
553
|
}
|
|
312
|
-
|
|
313
|
-
|
|
554
|
+
/** EC-716: a writer-locked position — no unit, no write, nothing below it either. */
|
|
555
|
+
function isLocked(position) {
|
|
556
|
+
return writerLockOf(position.field) !== undefined;
|
|
314
557
|
}
|
|
315
558
|
/**
|
|
316
559
|
* Every TEXT position at or below `position` (EC-593, ratified 4e — "translate uses the
|
|
317
560
|
* same traversal"): a `text` value, and a `richText` / `blocks` sequence (its prose —
|
|
318
561
|
* the sequence's own text — is the caller's to split) together with every text position
|
|
319
562
|
* below it: component text props in a composition and in rich-text embeds, object
|
|
320
|
-
* sub-fields, repeater items, asset placement sub-fields.
|
|
321
|
-
* everything below it
|
|
563
|
+
* sub-fields, repeater items, asset placement sub-fields. A writer-locked position
|
|
564
|
+
* (EC-716: `ai.exclude` or `form.readOnly`) and everything below it is not text a writer
|
|
565
|
+
* may touch — not a unit, and so never a skip either. Depth-first, document order.
|
|
322
566
|
*/
|
|
323
567
|
export function textPositionsOf(position, ctx) {
|
|
324
568
|
const positionCtx = ctx.lookupManifest ? { lookupManifest: ctx.lookupManifest } : {};
|
|
325
569
|
const out = [];
|
|
326
570
|
const visit = (current) => {
|
|
327
|
-
if (
|
|
571
|
+
if (isLocked(current))
|
|
328
572
|
return;
|
|
329
573
|
if (isTextPosition(current))
|
|
330
574
|
out.push(current);
|
|
@@ -346,7 +590,7 @@ export function textPositionsOf(position, ctx) {
|
|
|
346
590
|
export function rebuildTextPositions(position, ctx, replace) {
|
|
347
591
|
const positionCtx = ctx.lookupManifest ? { lookupManifest: ctx.lookupManifest } : {};
|
|
348
592
|
const rebuild = (current) => {
|
|
349
|
-
if (
|
|
593
|
+
if (isLocked(current))
|
|
350
594
|
return current.value;
|
|
351
595
|
if (current.field.type === 'text')
|
|
352
596
|
return replace(current);
|
|
@@ -364,8 +608,20 @@ export function rebuildTextPositions(position, ctx, replace) {
|
|
|
364
608
|
* stays, so nothing echoed back can silently lose structure), at EVERY position: a
|
|
365
609
|
* top-level field, an object sub-field, a repeater item, a component's rich-text prop.
|
|
366
610
|
* Everything else reads as stored (envelopes are writer forms too).
|
|
611
|
+
*
|
|
612
|
+
* **THE model-read projection** (EC-741): a position core's `writerLockOf` marks
|
|
613
|
+
* `excluded` (`ai: { exclude: true }` — "the model never sees or writes the field") reads
|
|
614
|
+
* as ABSENT, and so does everything below it, at every depth: a sub-field, a repeater
|
|
615
|
+
* item's sub-field, a placement sub-field, a component prop in a composition, a slot or
|
|
616
|
+
* typography content. A `read-only` position stays readable. Every model read crosses this
|
|
617
|
+
* one fold — `getDocument`'s output, the titles the model is shown
|
|
618
|
+
* ({@link modelFacingTitle}) and the echo checks (a value the model read and sent back is
|
|
619
|
+
* no change) — so what the model can see and what it can echo are one declaration. The raw
|
|
620
|
+
* stored document stays local: proposals are always built and validated against it.
|
|
367
621
|
*/
|
|
368
622
|
export function toWriterForm(position, ctx) {
|
|
623
|
+
if (writerLockOf(position.field) === 'excluded')
|
|
624
|
+
return undefined;
|
|
369
625
|
const { value } = position;
|
|
370
626
|
if (value === undefined || value === null)
|
|
371
627
|
return value;
|
|
@@ -402,28 +658,50 @@ export function componentIdsOf(collection, document, lookupManifest) {
|
|
|
402
658
|
visitBucket(bucket);
|
|
403
659
|
return [...ids];
|
|
404
660
|
}
|
|
661
|
+
/**
|
|
662
|
+
* {@link toWriterForm} over one values bag: each declared field in its read form, an
|
|
663
|
+
* excluded one (and an excluded position inside any field) left out. A key the collection
|
|
664
|
+
* does not declare reads as stored (AD-14: the schema is a lens).
|
|
665
|
+
*/
|
|
666
|
+
export function toWriterValues(collection, bag, ctx, locale) {
|
|
667
|
+
let next;
|
|
668
|
+
for (const field of collection.fields) {
|
|
669
|
+
if (!Object.hasOwn(bag, field.name))
|
|
670
|
+
continue;
|
|
671
|
+
const value = bag[field.name];
|
|
672
|
+
const read = toWriterForm(fieldValuePosition(field, value, locale !== undefined ? { locale } : {}), ctx);
|
|
673
|
+
if (read === value)
|
|
674
|
+
continue;
|
|
675
|
+
next ??= { ...bag };
|
|
676
|
+
if (read === undefined)
|
|
677
|
+
delete next[field.name];
|
|
678
|
+
else
|
|
679
|
+
next[field.name] = read;
|
|
680
|
+
}
|
|
681
|
+
return next ?? bag;
|
|
682
|
+
}
|
|
405
683
|
/** {@link toWriterForm} over every field of every bucket of a document. */
|
|
406
684
|
export function toWriterDocument(collection, document, ctx) {
|
|
407
|
-
const
|
|
408
|
-
const mapBucket = (bucket, locale) => {
|
|
409
|
-
let next = bucket;
|
|
410
|
-
for (const [name, value] of Object.entries(bucket)) {
|
|
411
|
-
const field = byName.get(name);
|
|
412
|
-
if (!field)
|
|
413
|
-
continue;
|
|
414
|
-
const read = toWriterForm(fieldValuePosition(field, value, locale !== undefined ? { locale } : {}), ctx);
|
|
415
|
-
if (read === value)
|
|
416
|
-
continue;
|
|
417
|
-
if (next === bucket)
|
|
418
|
-
next = { ...bucket };
|
|
419
|
-
next[name] = read;
|
|
420
|
-
}
|
|
421
|
-
return next;
|
|
422
|
-
};
|
|
423
|
-
const values = mapBucket(document.values);
|
|
685
|
+
const values = toWriterValues(collection, document.values, ctx);
|
|
424
686
|
const localized = document.localized
|
|
425
|
-
? Object.fromEntries(Object.entries(document.localized).map(([locale, bucket]) => [
|
|
687
|
+
? Object.fromEntries(Object.entries(document.localized).map(([locale, bucket]) => [
|
|
688
|
+
locale,
|
|
689
|
+
toWriterValues(collection, bucket, ctx, locale),
|
|
690
|
+
]))
|
|
426
691
|
: undefined;
|
|
427
692
|
return { ...document, values, ...(localized ? { localized } : {}) };
|
|
428
693
|
}
|
|
694
|
+
/**
|
|
695
|
+
* The title a MODEL is shown for a document (EC-741) — core's `resolveDocumentTitle` over
|
|
696
|
+
* the projected values ({@link toWriterValues}), so an excluded field never reaches the
|
|
697
|
+
* model as a title: neither as the named or first-field fallback, nor through a function
|
|
698
|
+
* `titleField` that reads it (the function sees the field absent). Every title the model
|
|
699
|
+
* reads goes through here: the `updateFields` / `createDocument` outputs, the
|
|
700
|
+
* `searchDocuments` results (their query is matched against this title too, so an
|
|
701
|
+
* excluded value cannot be probed by search) and the system prompt's "currently open"
|
|
702
|
+
* line. `values` is the bag the caller resolves titles from (a locale's merged values).
|
|
703
|
+
*/
|
|
704
|
+
export function modelFacingTitle(collection, values, fallbackId, ctx = {}) {
|
|
705
|
+
return resolveDocumentTitle(collection, toWriterValues(collection, values, ctx), fallbackId);
|
|
706
|
+
}
|
|
429
707
|
//# sourceMappingURL=writer-form.js.map
|