@elytracms/studio 0.0.86 → 0.0.88
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/asset-revalidate.test.ts +186 -0
- package/convex/assets.ts +53 -19
- package/convex/cms.ts +22 -0
- package/convex/delivery.ts +33 -116
- package/convex/functions.ts +3 -3
- package/convex/rail-write-parity.test.ts +180 -0
- package/convex/redirects.ts +33 -11
- package/convex/revalidate.ts +7 -5
- package/convex/triggers.ts +53 -3
- package/dist/components/ai/ai-chat-panel.js +8 -1
- package/dist/components/ai/ai-chat-panel.js.map +1 -1
- package/dist/components/app-views/default-modules.d.ts +20 -20
- package/dist/components/app-views/default-modules.js +27 -48
- package/dist/components/app-views/default-modules.js.map +1 -1
- package/dist/components/app-views/studio-config.d.ts +5 -6
- package/dist/components/app-views/studio-config.js.map +1 -1
- package/dist/components/assets/asset-metadata.d.ts +28 -19
- package/dist/components/assets/asset-metadata.js +24 -31
- package/dist/components/assets/asset-metadata.js.map +1 -1
- package/dist/components/assets/asset-selection.d.ts +24 -5
- package/dist/components/assets/asset-selection.js +39 -9
- package/dist/components/assets/asset-selection.js.map +1 -1
- package/dist/components/assets/assets-collection-view-source.d.ts +17 -14
- package/dist/components/assets/assets-collection-view-source.js +25 -31
- package/dist/components/assets/assets-collection-view-source.js.map +1 -1
- package/dist/components/assets/assets-store.d.ts +11 -1
- package/dist/components/assets/assets-store.js +10 -0
- package/dist/components/assets/assets-store.js.map +1 -1
- package/dist/components/assets/focal-point.d.ts +0 -7
- package/dist/components/assets/focal-point.js +0 -10
- package/dist/components/assets/focal-point.js.map +1 -1
- package/dist/components/builder/blocks-panel.js +3 -3
- package/dist/components/builder/blocks-panel.js.map +1 -1
- package/dist/components/builder/builder-store.d.ts +25 -1
- package/dist/components/builder/builder-store.js +3 -2
- package/dist/components/builder/builder-store.js.map +1 -1
- package/dist/components/builder/canvas-chrome-css.js +0 -17
- package/dist/components/builder/canvas-chrome-css.js.map +1 -1
- package/dist/components/builder/canvas-chrome.d.ts +10 -12
- package/dist/components/builder/canvas-chrome.js +12 -13
- package/dist/components/builder/canvas-chrome.js.map +1 -1
- package/dist/components/builder/canvas-dnd.d.ts +8 -0
- package/dist/components/builder/canvas-dnd.js +5 -3
- package/dist/components/builder/canvas-dnd.js.map +1 -1
- package/dist/components/builder/canvas-render-context.d.ts +23 -24
- package/dist/components/builder/canvas-render-context.js +35 -60
- package/dist/components/builder/canvas-render-context.js.map +1 -1
- package/dist/components/builder/canvas.js +5 -11
- package/dist/components/builder/canvas.js.map +1 -1
- package/dist/components/builder/clipboard-actions.d.ts +14 -6
- package/dist/components/builder/clipboard-actions.js +29 -20
- package/dist/components/builder/clipboard-actions.js.map +1 -1
- package/dist/components/builder/composition-field-editor.d.ts +19 -26
- package/dist/components/builder/composition-field-editor.js +30 -52
- package/dist/components/builder/composition-field-editor.js.map +1 -1
- package/dist/components/builder/insert-context.d.ts +3 -1
- package/dist/components/builder/insert-context.js +15 -10
- package/dist/components/builder/insert-context.js.map +1 -1
- package/dist/components/builder/page-tree.d.ts +29 -0
- package/dist/components/builder/page-tree.js +56 -58
- package/dist/components/builder/page-tree.js.map +1 -1
- package/dist/components/builder/reuse.js +5 -2
- package/dist/components/builder/reuse.js.map +1 -1
- package/dist/components/cms/asset-field.d.ts +2 -1
- package/dist/components/cms/asset-field.js +39 -83
- package/dist/components/cms/asset-field.js.map +1 -1
- package/dist/components/cms/asset-picker-core.d.ts +77 -40
- package/dist/components/cms/asset-picker-core.js +73 -46
- package/dist/components/cms/asset-picker-core.js.map +1 -1
- package/dist/components/cms/asset-picker.d.ts +48 -19
- package/dist/components/cms/asset-picker.js +69 -30
- package/dist/components/cms/asset-picker.js.map +1 -1
- package/dist/components/cms/cell-edit.d.ts +3 -1
- package/dist/components/cms/cell-edit.js +2 -2
- package/dist/components/cms/cell-edit.js.map +1 -1
- package/dist/components/cms/cms-composition-validators.js +11 -2
- package/dist/components/cms/cms-composition-validators.js.map +1 -1
- package/dist/components/cms/cms-store.js +15 -23
- package/dist/components/cms/cms-store.js.map +1 -1
- package/dist/components/cms/editable-cell.js +3 -3
- package/dist/components/cms/editable-cell.js.map +1 -1
- package/dist/components/cms/gate-validation.d.ts +27 -2
- package/dist/components/cms/gate-validation.js +19 -1
- package/dist/components/cms/gate-validation.js.map +1 -1
- package/dist/components/cms/rich-text/editor.js +2 -2
- package/dist/components/cms/rich-text/editor.js.map +1 -1
- package/dist/components/cms/rich-text/insert.d.ts +7 -0
- package/dist/components/cms/rich-text/insert.js +7 -0
- package/dist/components/cms/rich-text/insert.js.map +1 -1
- package/dist/components/cms/use-document-versions.js +1 -0
- package/dist/components/cms/use-document-versions.js.map +1 -1
- package/dist/components/cms/versions-model.d.ts +0 -12
- package/dist/components/cms/versions-model.js +3 -15
- package/dist/components/cms/versions-model.js.map +1 -1
- package/dist/components/components/component-detail.js +1 -1
- package/dist/components/components/component-detail.js.map +1 -1
- package/dist/components/components/transfer-dialog.js +2 -4
- package/dist/components/components/transfer-dialog.js.map +1 -1
- package/dist/components/components/transfer.d.ts +13 -10
- package/dist/components/components/transfer.js +21 -19
- package/dist/components/components/transfer.js.map +1 -1
- package/dist/components/nav-issues.d.ts +23 -3
- package/dist/components/nav-issues.js +38 -2
- package/dist/components/nav-issues.js.map +1 -1
- package/dist/components/project-session.d.ts +9 -0
- package/dist/components/project-session.js +1 -1
- package/dist/components/project-session.js.map +1 -1
- package/dist/components/redirects/redirects-collection-view-source.d.ts +17 -2
- package/dist/components/redirects/redirects-collection-view-source.js +20 -6
- package/dist/components/redirects/redirects-collection-view-source.js.map +1 -1
- package/dist/components/section-nav.js +26 -2
- package/dist/components/section-nav.js.map +1 -1
- package/dist/components/section-surface.js +3 -2
- package/dist/components/section-surface.js.map +1 -1
- package/dist/components/settings-form.js +1 -2
- package/dist/components/settings-form.js.map +1 -1
- package/dist/components/studio-context.js +1 -18
- package/dist/components/studio-context.js.map +1 -1
- package/dist/components/studio-project-screen.js +3 -5
- package/dist/components/studio-project-screen.js.map +1 -1
- package/dist/lib/ai/apply-content.js +73 -29
- package/dist/lib/ai/apply-content.js.map +1 -1
- package/dist/lib/ai/constraints.d.ts +35 -0
- package/dist/lib/ai/constraints.js +104 -4
- package/dist/lib/ai/constraints.js.map +1 -1
- package/dist/lib/ai/route.js +4 -1
- package/dist/lib/ai/route.js.map +1 -1
- package/dist/lib/ai/shape.d.ts +3 -2
- package/dist/lib/ai/shape.js +16 -24
- package/dist/lib/ai/shape.js.map +1 -1
- package/dist/lib/ai/tools.d.ts +10 -4
- package/dist/lib/ai/tools.js +12 -4
- package/dist/lib/ai/tools.js.map +1 -1
- package/dist/lib/ai/types.d.ts +8 -0
- package/dist/lib/ai/types.js.map +1 -1
- package/dist/lib/ai/update-fields.js +33 -12
- package/dist/lib/ai/update-fields.js.map +1 -1
- package/dist/lib/assets/asset-placements.d.ts +35 -0
- package/dist/lib/assets/asset-placements.js +74 -0
- package/dist/lib/assets/asset-placements.js.map +1 -0
- package/dist/lib/assets/backfill-thumbs.d.ts +9 -8
- package/dist/lib/assets/backfill-thumbs.js +13 -25
- package/dist/lib/assets/backfill-thumbs.js.map +1 -1
- package/dist/lib/clipboard/paste-issues.d.ts +17 -15
- package/dist/lib/clipboard/paste-issues.js +8 -45
- package/dist/lib/clipboard/paste-issues.js.map +1 -1
- package/dist/lib/clipboard/paste-target.d.ts +25 -1
- package/dist/lib/clipboard/paste-target.js +13 -6
- package/dist/lib/clipboard/paste-target.js.map +1 -1
- package/dist/lib/cms/published-documents.js +1 -0
- package/dist/lib/cms/published-documents.js.map +1 -1
- package/dist/lib/cms-views/view-model.d.ts +3 -3
- package/dist/lib/data/collection-content.d.ts +22 -1
- package/dist/lib/data/collection-content.js +15 -0
- package/dist/lib/data/collection-content.js.map +1 -1
- package/dist/lib/data/index.d.ts +0 -3
- package/dist/lib/data/index.js +4 -6
- package/dist/lib/data/index.js.map +1 -1
- package/dist/lib/dnd/drop-rules.d.ts +45 -2
- package/dist/lib/dnd/drop-rules.js +64 -9
- package/dist/lib/dnd/drop-rules.js.map +1 -1
- package/dist/lib/engine-version.d.ts +3 -3
- package/dist/lib/engine-version.js +3 -3
- package/dist/lib/engine-version.js.map +1 -1
- package/dist/lib/i18n/messages.de.js +9 -56
- package/dist/lib/i18n/messages.de.js.map +1 -1
- package/dist/lib/i18n/messages.en.d.ts +8 -53
- package/dist/lib/i18n/messages.en.js +14 -57
- package/dist/lib/i18n/messages.en.js.map +1 -1
- package/dist/lib/locale/fallback.js +8 -2
- package/dist/lib/locale/fallback.js.map +1 -1
- package/dist/lib/locale/index.d.ts +0 -1
- package/dist/lib/locale/index.js +0 -1
- package/dist/lib/locale/index.js.map +1 -1
- package/dist/lib/locale/locale-store.d.ts +10 -3
- package/dist/lib/locale/locale-store.js +12 -5
- package/dist/lib/locale/locale-store.js.map +1 -1
- package/dist/lib/operations/session-hydration.d.ts +24 -22
- package/dist/lib/operations/session-hydration.js +15 -53
- package/dist/lib/operations/session-hydration.js.map +1 -1
- package/dist/lib/persistence/convex-adapter.js +63 -75
- package/dist/lib/persistence/convex-adapter.js.map +1 -1
- package/dist/lib/persona/labels.d.ts +0 -2
- package/dist/lib/persona/labels.js +1 -7
- package/dist/lib/persona/labels.js.map +1 -1
- package/dist/lib/registry/index.d.ts +4 -2
- package/dist/lib/registry/index.js +26 -21
- package/dist/lib/registry/index.js.map +1 -1
- package/dist/lib/registry/project-components.js +0 -3
- package/dist/lib/registry/project-components.js.map +1 -1
- package/dist/lib/routes/route-issues.js +4 -5
- package/dist/lib/routes/route-issues.js.map +1 -1
- package/dist/lib/theme.js +3 -2
- package/dist/lib/theme.js.map +1 -1
- package/dist/lib/webhooks/tags.js +11 -14
- package/dist/lib/webhooks/tags.js.map +1 -1
- package/dist/lib/workspace/config-issues.d.ts +6 -0
- package/dist/lib/workspace/config-issues.js +8 -4
- package/dist/lib/workspace/config-issues.js.map +1 -1
- package/dist/lib/workspace/index.d.ts +2 -2
- package/dist/lib/workspace/index.js +2 -2
- package/dist/lib/workspace/index.js.map +1 -1
- package/dist/previews/enumerate.d.ts +27 -9
- package/dist/previews/enumerate.js +27 -6
- package/dist/previews/enumerate.js.map +1 -1
- package/dist/previews/manifest-format.d.ts +9 -0
- package/dist/previews/manifest-format.js.map +1 -1
- package/dist/router/match.d.ts +0 -2
- package/dist/router/match.js +0 -2
- package/dist/router/match.js.map +1 -1
- package/dist/router/navigate-once.d.ts +19 -4
- package/dist/router/navigate-once.js +22 -5
- package/dist/router/navigate-once.js.map +1 -1
- package/dist/ui/badge.d.ts +1 -1
- package/package.json +3 -3
- package/src/styles/studio.css +5 -4
- package/src/styles/tokens.css +72 -55
- package/convex/content.ts +0 -291
- package/dist/components/collab/collab-context.d.ts +0 -27
- package/dist/components/collab/collab-context.js +0 -39
- package/dist/components/collab/collab-context.js.map +0 -1
- package/dist/components/collab/collab-seed.d.ts +0 -15
- package/dist/components/collab/collab-seed.js +0 -20
- package/dist/components/collab/collab-seed.js.map +0 -1
- package/dist/components/extensions/extensions-view.d.ts +0 -7
- package/dist/components/extensions/extensions-view.js +0 -127
- package/dist/components/extensions/extensions-view.js.map +0 -1
- package/dist/lib/bindings/section-bindings.d.ts +0 -24
- package/dist/lib/bindings/section-bindings.js +0 -9
- package/dist/lib/bindings/section-bindings.js.map +0 -1
- package/dist/lib/data/data-store.d.ts +0 -98
- package/dist/lib/data/data-store.js +0 -211
- package/dist/lib/data/data-store.js.map +0 -1
- package/dist/lib/data/providers.d.ts +0 -143
- package/dist/lib/data/providers.js +0 -255
- package/dist/lib/data/providers.js.map +0 -1
- package/dist/lib/data/sample-capture.d.ts +0 -30
- package/dist/lib/data/sample-capture.js +0 -70
- package/dist/lib/data/sample-capture.js.map +0 -1
- package/dist/lib/data/sources-store.d.ts +0 -42
- package/dist/lib/data/sources-store.js +0 -120
- package/dist/lib/data/sources-store.js.map +0 -1
- package/dist/lib/locale/binding-fallback.d.ts +0 -35
- package/dist/lib/locale/binding-fallback.js +0 -73
- package/dist/lib/locale/binding-fallback.js.map +0 -1
- package/dist/lib/starters/kit-session.d.ts +0 -5
- package/dist/lib/starters/kit-session.js +0 -17
- package/dist/lib/starters/kit-session.js.map +0 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { documentDefaultLocale, isFieldLocalized, resolveField } from '@elytracms/core/cms-core';
|
|
2
|
+
import { isEmptyBlockSequence } from '@elytracms/core/project-graph';
|
|
2
3
|
/** True when the document stores an explicit variant of `field` for `locale`. */
|
|
3
4
|
export function hasLocaleVariant(doc, fieldName, locale) {
|
|
4
5
|
const bag = doc.localized?.[locale];
|
|
@@ -73,9 +74,14 @@ export function activeLocaleValues(doc, locale) {
|
|
|
73
74
|
// store a cleared value as `undefined`, which DELETES the key — so clearing a
|
|
74
75
|
// localized field reconnects it to base rather than diverging it to empty.
|
|
75
76
|
// ───────────────────────────────────────────────────────────────────────────
|
|
76
|
-
/**
|
|
77
|
+
/**
|
|
78
|
+
* The "no value" sentinels the built-in controls store (nothing to fill from) —
|
|
79
|
+
* including the empty v2 sequence a cleared canvas writes (EC-575, the same
|
|
80
|
+
* project-graph rule `required` reads). Filling from it would only diverge the
|
|
81
|
+
* locale to an empty value, the state this model deliberately cannot represent.
|
|
82
|
+
*/
|
|
77
83
|
function isBlankValue(value) {
|
|
78
|
-
return value === undefined || value === null || value === '';
|
|
84
|
+
return value === undefined || value === null || value === '' || isEmptyBlockSequence(value);
|
|
79
85
|
}
|
|
80
86
|
/** Derive the connected/diverged state of `field` in `locale` (EC-318). */
|
|
81
87
|
export function fieldConnection(collection, doc, fieldName, locale, config) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fallback.js","sourceRoot":"","sources":["../../../src/lib/locale/fallback.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAA;AAmBhG,iFAAiF;AACjF,MAAM,UAAU,gBAAgB,CAAC,GAAgB,EAAE,SAAiB,EAAE,MAAc;IAClF,MAAM,GAAG,GAAG,GAAG,CAAC,SAAS,EAAE,CAAC,MAAM,CAAC,CAAA;IACnC,OAAO,GAAG,KAAK,SAAS,IAAI,SAAS,IAAI,GAAG,CAAA;AAC9C,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,sBAAsB,CACpC,UAAyB,EACzB,GAAgB,EAChB,SAAiB,EACjB,MAAc,EACd,MAAoB;IAEpB,IAAI,CAAC,gBAAgB,CAAC,UAAU,EAAE,SAAS,CAAC;QAAE,OAAO,IAAI,CAAA;IACzD,IAAI,gBAAgB,CAAC,GAAG,EAAE,SAAS,EAAE,MAAM,CAAC;QAAE,OAAO,IAAI,CAAA;IACzD,MAAM,QAAQ,GAAG,YAAY,CAAC,UAAU,EAAE,GAAG,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,CAAC,CAAA;IACzE,IAAI,CAAC,QAAQ,CAAC,QAAQ,IAAI,QAAQ,CAAC,KAAK,KAAK,SAAS;QAAE,OAAO,IAAI,CAAA;IACnE,OAAO,EAAE,YAAY,EAAE,QAAQ,CAAC,YAAY,EAAE,eAAe,EAAE,MAAM,EAAE,CAAA;AACzE,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,qBAAqB,CACnC,UAAyB,EACzB,GAAgB,EAChB,SAAiB,EACjB,MAAc,EACd,MAAoB;IAEpB,OAAO;QACL,KAAK,EAAE,YAAY,CAAC,UAAU,EAAE,GAAG,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC,KAAK;QACrE,QAAQ,EAAE,sBAAsB,CAAC,UAAU,EAAE,GAAG,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,CAAC;KAC7E,CAAA;AACH,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CAChC,UAAyB,EACzB,GAAgB,EAChB,SAAiB,EACjB,MAAc,EACd,MAAoB;IAEpB,IAAI,CAAC,gBAAgB,CAAC,UAAU,EAAE,SAAS,CAAC;QAAE,OAAO,KAAK,CAAA;IAC1D,IAAI,MAAM,KAAK,qBAAqB,CAAC,GAAG,EAAE,MAAM,CAAC;QAAE,OAAO,KAAK,CAAA;IAC/D,OAAO,CAAC,gBAAgB,CAAC,GAAG,EAAE,SAAS,EAAE,MAAM,CAAC,CAAA;AAClD,CAAC;AAED,4EAA4E;AAC5E,MAAM,UAAU,eAAe,CAAC,MAAc;IAC5C,OAAO,MAAM,CAAC,WAAW,EAAE,CAAA;AAC7B,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CAAC,GAAgB,EAAE,MAAc;IACjE,OAAO,EAAE,GAAG,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,CAAC,SAAS,EAAE,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,EAAE,CAAA;AAC9D,CAAC;AAED,8EAA8E;AAC9E,4BAA4B;AAC5B,EAAE;AACF,kFAAkF;AAClF,0EAA0E;AAC1E,gFAAgF;AAChF,4EAA4E;AAC5E,mFAAmF;AACnF,gEAAgE;AAChE,EAAE;AACF,4EAA4E;AAC5E,kFAAkF;AAClF,8EAA8E;AAC9E,2EAA2E;AAC3E,8EAA8E;AAE9E,mFAAmF;AACnF,SAAS,YAAY,CAAC,KAAc;IAClC,OAAO,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,EAAE,CAAA;AAC9D,CAAC;AAmBD,2EAA2E;AAC3E,MAAM,UAAU,eAAe,CAC7B,UAAyB,EACzB,GAAgB,EAChB,SAAiB,EACjB,MAAc,EACd,MAAoB;IAEpB,IAAI,CAAC,gBAAgB,CAAC,UAAU,EAAE,SAAS,CAAC;QAAE,OAAO,IAAI,CAAA;IACzD,MAAM,UAAU,GAAG,qBAAqB,CAAC,GAAG,EAAE,MAAM,CAAC,CAAA;IACrD,IAAI,MAAM,KAAK,UAAU;QAAE,OAAO,IAAI,CAAA;IACtC,MAAM,SAAS,GAAG,YAAY,CAAC,UAAU,EAAE,GAAG,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,CAAC,CAAC,KAAK,CAAA;IACpF,OAAO;QACL,UAAU;QACV,SAAS,EAAE,CAAC,gBAAgB,CAAC,GAAG,EAAE,SAAS,EAAE,MAAM,CAAC;QACpD,SAAS;QACT,eAAe,EAAE,CAAC,YAAY,CAAC,SAAS,CAAC;KAC1C,CAAA;AACH,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,aAAa,CAC3B,UAAyB,EACzB,GAAgB,EAChB,SAAiB,EACjB,MAAc,EACd,KAAc;IAEd,IAAI,gBAAgB,CAAC,UAAU,EAAE,SAAS,CAAC,EAAE,CAAC;QAC5C,MAAM,SAAS,GAAG,EAAE,GAAG,CAAC,GAAG,CAAC,SAAS,EAAE,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,EAAE,CAAA;QACxD,IAAI,KAAK,KAAK,SAAS;YAAE,OAAO,SAAS,CAAC,SAAS,CAAC,CAAA;;YAC/C,SAAS,CAAC,SAAS,CAAC,GAAG,KAAK,CAAA;QACjC,OAAO,EAAE,GAAG,GAAG,EAAE,SAAS,EAAE,EAAE,GAAG,GAAG,CAAC,SAAS,EAAE,CAAC,MAAM,CAAC,EAAE,SAAS,EAAE,EAAE,CAAA;IACzE,CAAC;IACD,MAAM,MAAM,GAAG,EAAE,GAAG,GAAG,CAAC,MAAM,EAAE,CAAA;IAChC,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,MAAM,CAAC,SAAS,CAAC,CAAA;;QAC5C,MAAM,CAAC,SAAS,CAAC,GAAG,KAAK,CAAA;IAC9B,OAAO,EAAE,GAAG,GAAG,EAAE,MAAM,EAAE,CAAA;AAC3B,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAC/B,UAAyB,EACzB,GAAgB,EAChB,SAAiB,EACjB,MAAc,EACd,MAAoB;IAEpB,MAAM,UAAU,GAAG,eAAe,CAAC,UAAU,EAAE,GAAG,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,CAAC,CAAA;IAC9E,IAAI,CAAC,UAAU,IAAI,CAAC,UAAU,CAAC,eAAe;QAAE,OAAO,GAAG,CAAA;IAC1D,OAAO,aAAa,CAAC,UAAU,EAAE,GAAG,EAAE,SAAS,EAAE,MAAM,EAAE,UAAU,CAAC,SAAS,CAAC,CAAA;AAChF,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,gBAAgB,CAC9B,UAAyB,EACzB,GAAgB,EAChB,SAAiB,EACjB,MAAc;IAEd,OAAO,aAAa,CAAC,UAAU,EAAE,GAAG,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,CAAC,CAAA;AACrE,CAAC","sourcesContent":["import { documentDefaultLocale, isFieldLocalized, resolveField } from '@elytracms/core/cms-core'\nimport type { CmsDocument, CollectionDef, Locale, LocaleConfig } from '@elytracms/core/cms-core'\n\n/**\n * Fallback visibility (EC-176) — the pure \"is this displayed value a\n * fallback?\" decision the CMS table cells, detail-editor fields, and the\n * builder's Content tab all share. One function answers it, so a value is\n * either labeled everywhere or a real translation everywhere — never\n * silently mixed.\n */\n\n/** A displayed value that came from the fallback locale, not the active one. */\nexport interface DisplayedFallback {\n /** The locale the rendered value was actually read from. */\n sourceLocale: Locale\n /** The locale the surface asked for (the ambient active locale). */\n requestedLocale: Locale\n}\n\n/** True when the document stores an explicit variant of `field` for `locale`. */\nexport function hasLocaleVariant(doc: CmsDocument, fieldName: string, locale: Locale): boolean {\n const bag = doc.localized?.[locale]\n return bag !== undefined && fieldName in bag\n}\n\n/**\n * Whether the value a surface displays for `field` in `locale` is served via\n * EC-018 default-locale fallback. `null` means \"not a fallback\": the field is\n * not localized (locale-independent by design), the active locale has its own\n * variant, or there is nothing to display at all (an empty field is empty,\n * not a fallback).\n */\nexport function displayedFieldFallback(\n collection: CollectionDef,\n doc: CmsDocument,\n fieldName: string,\n locale: Locale,\n config: LocaleConfig,\n): DisplayedFallback | null {\n if (!isFieldLocalized(collection, fieldName)) return null\n if (hasLocaleVariant(doc, fieldName, locale)) return null\n const resolved = resolveField(collection, doc, fieldName, locale, config)\n if (!resolved.fellBack || resolved.value === undefined) return null\n return { sourceLocale: resolved.sourceLocale, requestedLocale: locale }\n}\n\n/**\n * The value a surface should display for `field` in `locale` (explicit\n * variant, else fallback) together with its fallback marker. Display and\n * label come from the same resolution, so they can never disagree.\n */\nexport function resolveDisplayedValue(\n collection: CollectionDef,\n doc: CmsDocument,\n fieldName: string,\n locale: Locale,\n config: LocaleConfig,\n): { value: unknown; fallback: DisplayedFallback | null } {\n return {\n value: resolveField(collection, doc, fieldName, locale, config).value,\n fallback: displayedFieldFallback(collection, doc, fieldName, locale, config),\n }\n}\n\n/**\n * True when editing `field` in `locale` would CREATE a new localized variant\n * (rather than update an existing one): the field is localized and the active\n * locale isn't the document's fallback source and has no variant yet. Drives\n * the \"translating from EN\" affordance on editors.\n */\nexport function editCreatesVariant(\n collection: CollectionDef,\n doc: CmsDocument,\n fieldName: string,\n locale: Locale,\n config: LocaleConfig,\n): boolean {\n if (!isFieldLocalized(collection, fieldName)) return false\n if (locale === documentDefaultLocale(doc, config)) return false\n return !hasLocaleVariant(doc, fieldName, locale)\n}\n\n/** Compact display form of a locale code for chips/badges (\"en\" → \"EN\"). */\nexport function localeChipLabel(locale: Locale): string {\n return locale.toUpperCase()\n}\n\n/**\n * The active-locale value bag: the document's shared `values` overlaid with the\n * active locale's variants (`localized[locale]`). The merged view a surface reads\n * to evaluate conditional-visibility predicates (EC-273) and derive the auto-slug\n * source (EC-315) — deduped between the detail editor and the slug control. Pure.\n */\nexport function activeLocaleValues(doc: CmsDocument, locale: Locale): Record<string, unknown> {\n return { ...doc.values, ...(doc.localized?.[locale] ?? {}) }\n}\n\n// ───────────────────────────────────────────────────────────────────────────\n// EC-318 — connected fields\n//\n// The Sanity/Contentful \"connected field\" model, over the SAME present/absent-key\n// storage EC-176 already uses. Connection state is DERIVED, never stored:\n// • connected = NO per-locale key → the field FOLLOWS the base-locale value.\n// • diverged = a per-locale key EXISTS → the field OWNS a locale value.\n// Editing a connected field diverges it (writes the key); \"fill from base\" seeds a\n// divergent copy; \"reset to base\" deletes the key to reconnect.\n//\n// Accepted limitation (a deliberate product decision, not an oversight): an\n// intentionally-EMPTY divergent value is not representable. The built-in controls\n// store a cleared value as `undefined`, which DELETES the key — so clearing a\n// localized field reconnects it to base rather than diverging it to empty.\n// ───────────────────────────────────────────────────────────────────────────\n\n/** The \"no value\" sentinels the built-in controls store (nothing to fill from). */\nfunction isBlankValue(value: unknown): boolean {\n return value === undefined || value === null || value === ''\n}\n\n/**\n * The per-locale CONNECTION state of a localized field (EC-318), derived from the\n * document — never stored. `null` when the connected-field affordance does not\n * apply: the field isn't localized, or the requested locale IS the document's base\n * locale (there is nothing to follow — the base owns the value).\n */\nexport interface FieldConnection {\n /** The document's base/default locale — the value a connected field follows. */\n baseLocale: Locale\n /** True when NO per-locale key exists: the field follows the base value. */\n connected: boolean\n /** The current base-locale value (\"fill from base\"/\"reset\" act against it). */\n baseValue: unknown\n /** True when the base holds a non-empty value there is something to fill from. */\n canFillFromBase: boolean\n}\n\n/** Derive the connected/diverged state of `field` in `locale` (EC-318). */\nexport function fieldConnection(\n collection: CollectionDef,\n doc: CmsDocument,\n fieldName: string,\n locale: Locale,\n config: LocaleConfig,\n): FieldConnection | null {\n if (!isFieldLocalized(collection, fieldName)) return null\n const baseLocale = documentDefaultLocale(doc, config)\n if (locale === baseLocale) return null\n const baseValue = resolveField(collection, doc, fieldName, baseLocale, config).value\n return {\n baseLocale,\n connected: !hasLocaleVariant(doc, fieldName, locale),\n baseValue,\n canFillFromBase: !isBlankValue(baseValue),\n }\n}\n\n/**\n * The present/absent-key write for one field — the FOUNDATION of the connected-\n * field model (EC-176/EC-318). A localized field's value lives under\n * `localized[locale][field]`; writing `undefined` DELETES that key (reconnecting\n * the field to base — the accepted empty-value limitation above). Non-localized\n * fields write the shared `values` bag. Pure — returns a new document.\n */\nexport function setFieldValue(\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/**\n * \"Fill from base\" (EC-318) — copy the current base-locale value into the active\n * locale, instantly DISCONNECTING the field with a pre-filled starting point. A\n * no-op when there is nothing to copy (blank base value). Pure.\n */\nexport function fillFieldFromBase(\n collection: CollectionDef,\n doc: CmsDocument,\n fieldName: string,\n locale: Locale,\n config: LocaleConfig,\n): CmsDocument {\n const connection = fieldConnection(collection, doc, fieldName, locale, config)\n if (!connection || !connection.canFillFromBase) return doc\n return setFieldValue(collection, doc, fieldName, locale, connection.baseValue)\n}\n\n/**\n * \"Reset to base\" / reconnect (EC-318) — delete the active-locale variant so the\n * field follows the base value again. Discards the translation, so gate it behind\n * a confirm. Pure.\n */\nexport function resetFieldToBase(\n collection: CollectionDef,\n doc: CmsDocument,\n fieldName: string,\n locale: Locale,\n): CmsDocument {\n return setFieldValue(collection, doc, fieldName, locale, undefined)\n}\n"]}
|
|
1
|
+
{"version":3,"file":"fallback.js","sourceRoot":"","sources":["../../../src/lib/locale/fallback.ts"],"names":[],"mappings":"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","sourcesContent":["import { documentDefaultLocale, isFieldLocalized, resolveField } from '@elytracms/core/cms-core'\nimport type { CmsDocument, CollectionDef, Locale, LocaleConfig } from '@elytracms/core/cms-core'\nimport { isEmptyBlockSequence } from '@elytracms/core/project-graph'\n\n/**\n * Fallback visibility (EC-176) — the pure \"is this displayed value a\n * fallback?\" decision the CMS table cells, detail-editor fields, and the\n * builder's Content tab all share. One function answers it, so a value is\n * either labeled everywhere or a real translation everywhere — never\n * silently mixed.\n */\n\n/** A displayed value that came from the fallback locale, not the active one. */\nexport interface DisplayedFallback {\n /** The locale the rendered value was actually read from. */\n sourceLocale: Locale\n /** The locale the surface asked for (the ambient active locale). */\n requestedLocale: Locale\n}\n\n/** True when the document stores an explicit variant of `field` for `locale`. */\nexport function hasLocaleVariant(doc: CmsDocument, fieldName: string, locale: Locale): boolean {\n const bag = doc.localized?.[locale]\n return bag !== undefined && fieldName in bag\n}\n\n/**\n * Whether the value a surface displays for `field` in `locale` is served via\n * EC-018 default-locale fallback. `null` means \"not a fallback\": the field is\n * not localized (locale-independent by design), the active locale has its own\n * variant, or there is nothing to display at all (an empty field is empty,\n * not a fallback).\n */\nexport function displayedFieldFallback(\n collection: CollectionDef,\n doc: CmsDocument,\n fieldName: string,\n locale: Locale,\n config: LocaleConfig,\n): DisplayedFallback | null {\n if (!isFieldLocalized(collection, fieldName)) return null\n if (hasLocaleVariant(doc, fieldName, locale)) return null\n const resolved = resolveField(collection, doc, fieldName, locale, config)\n if (!resolved.fellBack || resolved.value === undefined) return null\n return { sourceLocale: resolved.sourceLocale, requestedLocale: locale }\n}\n\n/**\n * The value a surface should display for `field` in `locale` (explicit\n * variant, else fallback) together with its fallback marker. Display and\n * label come from the same resolution, so they can never disagree.\n */\nexport function resolveDisplayedValue(\n collection: CollectionDef,\n doc: CmsDocument,\n fieldName: string,\n locale: Locale,\n config: LocaleConfig,\n): { value: unknown; fallback: DisplayedFallback | null } {\n return {\n value: resolveField(collection, doc, fieldName, locale, config).value,\n fallback: displayedFieldFallback(collection, doc, fieldName, locale, config),\n }\n}\n\n/**\n * True when editing `field` in `locale` would CREATE a new localized variant\n * (rather than update an existing one): the field is localized and the active\n * locale isn't the document's fallback source and has no variant yet. Drives\n * the \"translating from EN\" affordance on editors.\n */\nexport function editCreatesVariant(\n collection: CollectionDef,\n doc: CmsDocument,\n fieldName: string,\n locale: Locale,\n config: LocaleConfig,\n): boolean {\n if (!isFieldLocalized(collection, fieldName)) return false\n if (locale === documentDefaultLocale(doc, config)) return false\n return !hasLocaleVariant(doc, fieldName, locale)\n}\n\n/** Compact display form of a locale code for chips/badges (\"en\" → \"EN\"). */\nexport function localeChipLabel(locale: Locale): string {\n return locale.toUpperCase()\n}\n\n/**\n * The active-locale value bag: the document's shared `values` overlaid with the\n * active locale's variants (`localized[locale]`). The merged view a surface reads\n * to evaluate conditional-visibility predicates (EC-273) and derive the auto-slug\n * source (EC-315) — deduped between the detail editor and the slug control. Pure.\n */\nexport function activeLocaleValues(doc: CmsDocument, locale: Locale): Record<string, unknown> {\n return { ...doc.values, ...(doc.localized?.[locale] ?? {}) }\n}\n\n// ───────────────────────────────────────────────────────────────────────────\n// EC-318 — connected fields\n//\n// The Sanity/Contentful \"connected field\" model, over the SAME present/absent-key\n// storage EC-176 already uses. Connection state is DERIVED, never stored:\n// • connected = NO per-locale key → the field FOLLOWS the base-locale value.\n// • diverged = a per-locale key EXISTS → the field OWNS a locale value.\n// Editing a connected field diverges it (writes the key); \"fill from base\" seeds a\n// divergent copy; \"reset to base\" deletes the key to reconnect.\n//\n// Accepted limitation (a deliberate product decision, not an oversight): an\n// intentionally-EMPTY divergent value is not representable. The built-in controls\n// store a cleared value as `undefined`, which DELETES the key — so clearing a\n// localized field reconnects it to base rather than diverging it to empty.\n// ───────────────────────────────────────────────────────────────────────────\n\n/**\n * The \"no value\" sentinels the built-in controls store (nothing to fill from) —\n * including the empty v2 sequence a cleared canvas writes (EC-575, the same\n * project-graph rule `required` reads). Filling from it would only diverge the\n * locale to an empty value, the state this model deliberately cannot represent.\n */\nfunction isBlankValue(value: unknown): boolean {\n return value === undefined || value === null || value === '' || isEmptyBlockSequence(value)\n}\n\n/**\n * The per-locale CONNECTION state of a localized field (EC-318), derived from the\n * document — never stored. `null` when the connected-field affordance does not\n * apply: the field isn't localized, or the requested locale IS the document's base\n * locale (there is nothing to follow — the base owns the value).\n */\nexport interface FieldConnection {\n /** The document's base/default locale — the value a connected field follows. */\n baseLocale: Locale\n /** True when NO per-locale key exists: the field follows the base value. */\n connected: boolean\n /** The current base-locale value (\"fill from base\"/\"reset\" act against it). */\n baseValue: unknown\n /** True when the base holds a non-empty value there is something to fill from. */\n canFillFromBase: boolean\n}\n\n/** Derive the connected/diverged state of `field` in `locale` (EC-318). */\nexport function fieldConnection(\n collection: CollectionDef,\n doc: CmsDocument,\n fieldName: string,\n locale: Locale,\n config: LocaleConfig,\n): FieldConnection | null {\n if (!isFieldLocalized(collection, fieldName)) return null\n const baseLocale = documentDefaultLocale(doc, config)\n if (locale === baseLocale) return null\n const baseValue = resolveField(collection, doc, fieldName, baseLocale, config).value\n return {\n baseLocale,\n connected: !hasLocaleVariant(doc, fieldName, locale),\n baseValue,\n canFillFromBase: !isBlankValue(baseValue),\n }\n}\n\n/**\n * The present/absent-key write for one field — the FOUNDATION of the connected-\n * field model (EC-176/EC-318). A localized field's value lives under\n * `localized[locale][field]`; writing `undefined` DELETES that key (reconnecting\n * the field to base — the accepted empty-value limitation above). Non-localized\n * fields write the shared `values` bag. Pure — returns a new document.\n */\nexport function setFieldValue(\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/**\n * \"Fill from base\" (EC-318) — copy the current base-locale value into the active\n * locale, instantly DISCONNECTING the field with a pre-filled starting point. A\n * no-op when there is nothing to copy (blank base value). Pure.\n */\nexport function fillFieldFromBase(\n collection: CollectionDef,\n doc: CmsDocument,\n fieldName: string,\n locale: Locale,\n config: LocaleConfig,\n): CmsDocument {\n const connection = fieldConnection(collection, doc, fieldName, locale, config)\n if (!connection || !connection.canFillFromBase) return doc\n return setFieldValue(collection, doc, fieldName, locale, connection.baseValue)\n}\n\n/**\n * \"Reset to base\" / reconnect (EC-318) — delete the active-locale variant so the\n * field follows the base value again. Discards the translation, so gate it behind\n * a confirm. Pure.\n */\nexport function resetFieldToBase(\n collection: CollectionDef,\n doc: CmsDocument,\n fieldName: string,\n locale: Locale,\n): CmsDocument {\n return setFieldValue(collection, doc, fieldName, locale, undefined)\n}\n"]}
|
package/dist/lib/locale/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/lib/locale/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAA;AAC9B,cAAc,YAAY,CAAA;AAC1B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/lib/locale/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAA;AAC9B,cAAc,YAAY,CAAA;AAC1B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,qBAAqB,CAAA;AACnC,cAAc,mBAAmB,CAAA","sourcesContent":["export * from './locale-store'\nexport * from './fallback'\nexport * from './route-variant'\nexport * from './use-studio-locale'\nexport * from './locale-switcher'\n"]}
|
|
@@ -57,9 +57,16 @@ export declare function getStudioLocaleConfig(): LocaleConfig | null;
|
|
|
57
57
|
*/
|
|
58
58
|
export declare function setStudioLocale(locale: Locale): void;
|
|
59
59
|
/**
|
|
60
|
-
* Install the project's locale config (
|
|
61
|
-
*
|
|
62
|
-
*
|
|
60
|
+
* Install the project's locale config (when a CMS session mounts). The active
|
|
61
|
+
* locale is kept when it is still configured, otherwise it snaps back to the new
|
|
62
|
+
* default.
|
|
63
|
+
*
|
|
64
|
+
* The config arrives valid by construction: every caller hands over a
|
|
65
|
+
* config-derived `LocaleConfig` that the workspace gate has already checked
|
|
66
|
+
* (`default-locale-not-in-locales` is an error there), so nothing is repaired
|
|
67
|
+
* here. The old `createLocaleConfig` repair existed for starter-kit locales, a
|
|
68
|
+
* rung EC-574 removed — a second, silent declaration of "the default is in the
|
|
69
|
+
* set" is exactly the drift the one gate exists to prevent.
|
|
63
70
|
*/
|
|
64
71
|
export declare function configureStudioLocales(config: LocaleConfig): void;
|
|
65
72
|
/** Subscribe to locale/config changes. Returns the unsubscribe function. */
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { isKnownLocale } from '@elytracms/core/cms-core';
|
|
2
2
|
/** The pre-config state: no project locales have been installed yet. */
|
|
3
3
|
const NOT_LOADED = { config: null, active: null };
|
|
4
4
|
let state = NOT_LOADED;
|
|
@@ -42,12 +42,19 @@ export function setStudioLocale(locale) {
|
|
|
42
42
|
emit();
|
|
43
43
|
}
|
|
44
44
|
/**
|
|
45
|
-
* Install the project's locale config (
|
|
46
|
-
*
|
|
47
|
-
*
|
|
45
|
+
* Install the project's locale config (when a CMS session mounts). The active
|
|
46
|
+
* locale is kept when it is still configured, otherwise it snaps back to the new
|
|
47
|
+
* default.
|
|
48
|
+
*
|
|
49
|
+
* The config arrives valid by construction: every caller hands over a
|
|
50
|
+
* config-derived `LocaleConfig` that the workspace gate has already checked
|
|
51
|
+
* (`default-locale-not-in-locales` is an error there), so nothing is repaired
|
|
52
|
+
* here. The old `createLocaleConfig` repair existed for starter-kit locales, a
|
|
53
|
+
* rung EC-574 removed — a second, silent declaration of "the default is in the
|
|
54
|
+
* set" is exactly the drift the one gate exists to prevent.
|
|
48
55
|
*/
|
|
49
56
|
export function configureStudioLocales(config) {
|
|
50
|
-
const normalized =
|
|
57
|
+
const normalized = { default: config.default, locales: [...config.locales] };
|
|
51
58
|
const current = state.config;
|
|
52
59
|
const sameConfig = current !== null &&
|
|
53
60
|
normalized.default === current.default &&
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"locale-store.js","sourceRoot":"","sources":["../../../src/lib/locale/locale-store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"locale-store.js","sourceRoot":"","sources":["../../../src/lib/locale/locale-store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAA;AAqCxD,wEAAwE;AACxE,MAAM,UAAU,GAAsB,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAA;AAEpE,IAAI,KAAK,GAAsB,UAAU,CAAA;AAEzC,MAAM,SAAS,GAAG,IAAI,GAAG,EAAc,CAAA;AAEvC,SAAS,IAAI;IACX,KAAK,MAAM,QAAQ,IAAI,SAAS;QAAE,QAAQ,EAAE,CAAA;AAC9C,CAAC;AAED,gFAAgF;AAChF,MAAM,UAAU,oBAAoB;IAClC,OAAO,KAAK,CAAA;AACd,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,eAAe;IAC7B,OAAO,KAAK,CAAC,MAAM,CAAA;AACrB,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,qBAAqB;IACnC,OAAO,KAAK,CAAC,MAAM,CAAA;AACrB,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,eAAe,CAAC,MAAc;IAC5C,IAAI,KAAK,CAAC,MAAM,KAAK,IAAI;QAAE,OAAM;IACjC,IAAI,MAAM,KAAK,KAAK,CAAC,MAAM,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,MAAM,EAAE,MAAM,CAAC;QAAE,OAAM;IAC3E,KAAK,GAAG,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,CAAA;IAChD,IAAI,EAAE,CAAA;AACR,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,sBAAsB,CAAC,MAAoB;IACzD,MAAM,UAAU,GAAiB,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,OAAO,EAAE,CAAC,GAAG,MAAM,CAAC,OAAO,CAAC,EAAE,CAAA;IAC1F,MAAM,OAAO,GAAG,KAAK,CAAC,MAAM,CAAA;IAC5B,MAAM,UAAU,GACd,OAAO,KAAK,IAAI;QAChB,UAAU,CAAC,OAAO,KAAK,OAAO,CAAC,OAAO;QACtC,UAAU,CAAC,OAAO,CAAC,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,MAAM;QACpD,UAAU,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAA;IACxE,MAAM,MAAM,GACV,KAAK,CAAC,MAAM,KAAK,IAAI,IAAI,aAAa,CAAC,UAAU,EAAE,KAAK,CAAC,MAAM,CAAC;QAC9D,CAAC,CAAC,KAAK,CAAC,MAAM;QACd,CAAC,CAAC,UAAU,CAAC,OAAO,CAAA;IACxB,IAAI,UAAU,IAAI,MAAM,KAAK,KAAK,CAAC,MAAM;QAAE,OAAM;IACjD,KAAK,GAAG,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,CAAA;IACtC,IAAI,EAAE,CAAA;AACR,CAAC;AAED,4EAA4E;AAC5E,MAAM,UAAU,qBAAqB,CAAC,QAAoB;IACxD,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;IACvB,OAAO,GAAG,EAAE;QACV,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAA;IAC5B,CAAC,CAAA;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,iBAAiB;IAC/B,KAAK,GAAG,UAAU,CAAA;IAClB,IAAI,EAAE,CAAA;AACR,CAAC","sourcesContent":["import { isKnownLocale } from '@elytracms/core/cms-core'\nimport type { Locale, LocaleConfig } from '@elytracms/core/cms-core'\n\n/**\n * The studio's ambient locale (EC-176) — framework-free core.\n *\n * One studio-level active locale, set once via the locale switcher, that the\n * builder canvas preview, the CMS table, and the detail editor all follow.\n * Module-level (not React context) on purpose: the playground's content\n * client (`lib/data/collection-content`) reads it at query time without any\n * provider plumbing, and React surfaces subscribe through\n * `useStudioLocale` (`useSyncExternalStore`).\n *\n * Deliberately NOT part of `lib/data/data-store` — the locale is studio\n * state, not a data source.\n *\n * EC-479/EC-483 — **before a project's locale config is installed, this store\n * holds `null`, not a locale.** It used to open on the engine's `en/de`\n * `sampleLocaleConfig` fixture, which is the worst fallback in the family: the\n * default locale is the literal STORAGE KEY for every localized field value\n * (`documents.ts:88`), so a fabricated one writes a German project's content\n * under `en` and correcting it later is a data migration, not a config edit. The\n * locale is a deliberate choice at project creation (EC-483) and reaches the\n * studio through {@link configureStudioLocales} from the project's own config —\n * so \"not loaded\" is an honest state that surfaces, never an invented one that\n * silently becomes a key.\n */\n\n/**\n * `{ config, active }` once a project's locales are installed; `{ null, null }`\n * before that. The two fields move together by construction — there is no state\n * with a config but no active locale, or an active locale nobody declared.\n */\nexport type StudioLocaleState =\n | { readonly config: LocaleConfig; readonly active: Locale }\n | { readonly config: null; readonly active: null }\n\n/** The pre-config state: no project locales have been installed yet. */\nconst NOT_LOADED: StudioLocaleState = { config: null, active: null }\n\nlet state: StudioLocaleState = NOT_LOADED\n\nconst listeners = new Set<() => void>()\n\nfunction emit(): void {\n for (const listener of listeners) listener()\n}\n\n/** Snapshot of `{ config, active }` — stable identity until the next change. */\nexport function getStudioLocaleState(): StudioLocaleState {\n return state\n}\n\n/**\n * The active studio locale every surface renders in — `null` until a project's\n * locale config is installed. A caller that genuinely needs a locale reads the\n * PROJECT's own default (`CmsStore.localeConfig.default`, the content session's\n * config); it must never substitute one of its own.\n */\nexport function getStudioLocale(): Locale | null {\n return state.active\n}\n\n/**\n * The project locale config driving the switcher options — `null` until one is\n * installed (the studio has no locales of its own to offer).\n */\nexport function getStudioLocaleConfig(): LocaleConfig | null {\n return state.config\n}\n\n/**\n * Switch the ambient locale. Unknown locales are ignored (the switcher only\n * offers configured ones; programmatic callers get a no-op, never a crash), and\n * so is any switch attempted before a config is installed — there is nothing to\n * validate the locale against.\n */\nexport function setStudioLocale(locale: Locale): void {\n if (state.config === null) return\n if (locale === state.active || !isKnownLocale(state.config, locale)) return\n state = { config: state.config, active: locale }\n emit()\n}\n\n/**\n * Install the project's locale config (when a CMS session mounts). The active\n * locale is kept when it is still configured, otherwise it snaps back to the new\n * default.\n *\n * The config arrives valid by construction: every caller hands over a\n * config-derived `LocaleConfig` that the workspace gate has already checked\n * (`default-locale-not-in-locales` is an error there), so nothing is repaired\n * here. The old `createLocaleConfig` repair existed for starter-kit locales, a\n * rung EC-574 removed — a second, silent declaration of \"the default is in the\n * set\" is exactly the drift the one gate exists to prevent.\n */\nexport function configureStudioLocales(config: LocaleConfig): void {\n const normalized: LocaleConfig = { default: config.default, locales: [...config.locales] }\n const current = state.config\n const sameConfig =\n current !== null &&\n normalized.default === current.default &&\n normalized.locales.length === current.locales.length &&\n normalized.locales.every((locale, i) => locale === current.locales[i])\n const active =\n state.active !== null && isKnownLocale(normalized, state.active)\n ? state.active\n : normalized.default\n if (sameConfig && active === state.active) return\n state = { config: normalized, active }\n emit()\n}\n\n/** Subscribe to locale/config changes. Returns the unsubscribe function. */\nexport function subscribeStudioLocale(listener: () => void): () => void {\n listeners.add(listener)\n return () => {\n listeners.delete(listener)\n }\n}\n\n/**\n * Reset to the pre-config \"not loaded\" state (tests only — module state is\n * shared). Never re-seeds a fixture config: a test that queries localized content\n * must say which locale it means, exactly like a project does.\n */\nexport function resetStudioLocale(): void {\n state = NOT_LOADED\n emit()\n}\n"]}
|
|
@@ -1,13 +1,6 @@
|
|
|
1
|
-
import type { CmsDocument, CollectionDef } from '@elytracms/core/cms-core';
|
|
1
|
+
import type { CmsDocument, CollectionDef, OrphanedCollection } from '@elytracms/core/cms-core';
|
|
2
2
|
import type { AssetRecord } from '@elytracms/core/persistence';
|
|
3
3
|
import type { OperationResult, OperationsClient } from './operations-package.js';
|
|
4
|
-
/**
|
|
5
|
-
* Legacy marker (EC-246b): editor-owned saved views are retired — list
|
|
6
|
-
* presentation is config-as-code now (named views live in `cms/`). The id is kept
|
|
7
|
-
* only so hydration keeps excluding any `cms-saved-views` documents a pre-EC-246b
|
|
8
|
-
* deployment persisted, so they never surface as content.
|
|
9
|
-
*/
|
|
10
|
-
export declare const VIEWS_COLLECTION = "cms-saved-views";
|
|
11
4
|
/**
|
|
12
5
|
* Persisted-session hydration (Milestone B sign-off fix, EC-177 gap 1).
|
|
13
6
|
*
|
|
@@ -33,18 +26,29 @@ export declare const VIEWS_COLLECTION = "cms-saved-views";
|
|
|
33
26
|
* to hydrate the canvas/preview content session; only the render-gating is
|
|
34
27
|
* Convex-specific.
|
|
35
28
|
*/
|
|
36
|
-
/**
|
|
37
|
-
* Collections owned by studio subsystems (data sources, saved views). They live
|
|
38
|
-
* in the same persisted schema but are NOT content: the CMS session, the
|
|
39
|
-
* sidebar, and the content preview engine must never offer them as authorable
|
|
40
|
-
* collections. Routes/redirects are NO LONGER system collections (EC-207): they
|
|
41
|
-
* are config-sourced and never persisted to the backend.
|
|
42
|
-
*/
|
|
43
|
-
export declare const STUDIO_SYSTEM_COLLECTION_IDS: ReadonlySet<string>;
|
|
44
|
-
/** The persisted CMS content of a project: schema + documents, system collections excluded. */
|
|
29
|
+
/** The persisted CMS content of a project: the declared schema + the documents of its collections. */
|
|
45
30
|
export interface HydratedCms {
|
|
46
31
|
collections: CollectionDef[];
|
|
32
|
+
/** Documents of the declared collections only — see {@link orphans} for the rest. */
|
|
47
33
|
documents: CmsDocument[];
|
|
34
|
+
/**
|
|
35
|
+
* EC-606 — the stored rows the session does NOT carry: every collection id that
|
|
36
|
+
* stored documents use but the project does not declare, with its row count and
|
|
37
|
+
* how many of those rows are published. Sorted by collection id; empty when every
|
|
38
|
+
* row belongs to a declared collection.
|
|
39
|
+
*
|
|
40
|
+
* Hydration keeps only declared collections' documents (there is no schema to edit
|
|
41
|
+
* the rest under). Before EC-606 the others were dropped right here with no count,
|
|
42
|
+
* so renaming a collection id in config made its documents vanish without a trace.
|
|
43
|
+
* Counted by the one core counter (`orphanedCollections`), which `elytra doctor`
|
|
44
|
+
* also runs over the published snapshot. The rail foot reports this list through
|
|
45
|
+
* the content session (`configureContentSession(contentSessionOf(cms))`).
|
|
46
|
+
*
|
|
47
|
+
* EC-606 B1: there is no "system collection" exemption any more. The only one left
|
|
48
|
+
* was `cms-saved-views`, the editor saved views EC-246b retired; nothing reads
|
|
49
|
+
* those rows, so they are counted like any other undeclared collection.
|
|
50
|
+
*/
|
|
51
|
+
orphans: OrphanedCollection[];
|
|
48
52
|
/**
|
|
49
53
|
* Published document keys (EC-224), via the version pointer: a record is
|
|
50
54
|
* published iff `publishedVersion` is set. The session boots its
|
|
@@ -98,11 +102,9 @@ export interface HydratedProjectSession {
|
|
|
98
102
|
}
|
|
99
103
|
/**
|
|
100
104
|
* Load the persisted CMS content state. `null` when the project has no
|
|
101
|
-
* persisted content schema
|
|
102
|
-
* persister's baseline will persist them on
|
|
103
|
-
*
|
|
104
|
-
* system stores (saved views, routes) may create the schema record before any
|
|
105
|
-
* content baseline ran.
|
|
105
|
+
* persisted content schema, or one that declares no collections (first run —
|
|
106
|
+
* the session seeds fixtures and the persister's baseline will persist them on
|
|
107
|
+
* the first edit).
|
|
106
108
|
*/
|
|
107
109
|
export declare function loadHydratedCms(operations: OperationsClient, projectId: string): Promise<OperationResult<HydratedCms | null>>;
|
|
108
110
|
/**
|
|
@@ -1,62 +1,16 @@
|
|
|
1
|
-
import { documentKey } from '@elytracms/core/cms-core';
|
|
2
|
-
import { DATA_SOURCES_COLLECTION } from '../data/sources-store.js';
|
|
3
|
-
/**
|
|
4
|
-
* Legacy marker (EC-246b): editor-owned saved views are retired — list
|
|
5
|
-
* presentation is config-as-code now (named views live in `cms/`). The id is kept
|
|
6
|
-
* only so hydration keeps excluding any `cms-saved-views` documents a pre-EC-246b
|
|
7
|
-
* deployment persisted, so they never surface as content.
|
|
8
|
-
*/
|
|
9
|
-
export const VIEWS_COLLECTION = 'cms-saved-views';
|
|
10
|
-
/**
|
|
11
|
-
* Persisted-session hydration (Milestone B sign-off fix, EC-177 gap 1).
|
|
12
|
-
*
|
|
13
|
-
* Against a persistent backend (Convex mode) a studio session must BOOT from
|
|
14
|
-
* what is persisted — the saved CMS schema + documents (+ asset records) — and
|
|
15
|
-
* fall back to the deterministic fixtures only when the project has nothing
|
|
16
|
-
* persisted yet (first run). Before this, every session seeded from fixtures,
|
|
17
|
-
* so reloads hid the editor's work.
|
|
18
|
-
*
|
|
19
|
-
* EC-287: the DB-resident project graph and its publish path are retired —
|
|
20
|
-
* layouts/config/schema/components are CODE, the DB holds content only. A
|
|
21
|
-
* session no longer hydrates a graph revision or graph-publishing state; CMS
|
|
22
|
-
* content (with per-document `publishedVersion`, EC-224) is the only persisted
|
|
23
|
-
* surface a session boots from.
|
|
24
|
-
*
|
|
25
|
-
* This module is the pure load half: read-only `@elytracms/core/operations` queries
|
|
26
|
-
* composed into one hydration result. `not-found` is a *normal* answer (the
|
|
27
|
-
* first-run fallback); every other failure propagates as the structured
|
|
28
|
-
* operation error so the gate renders it — never a silent fixture fallback.
|
|
29
|
-
* The React gate that applies it lives in
|
|
30
|
-
* `components/studio/project-session.tsx`. EC-361: the playground calls the
|
|
31
|
-
* SAME loader (over the seeded in-memory adapter, through operations — AD-17)
|
|
32
|
-
* to hydrate the canvas/preview content session; only the render-gating is
|
|
33
|
-
* Convex-specific.
|
|
34
|
-
*/
|
|
35
|
-
/**
|
|
36
|
-
* Collections owned by studio subsystems (data sources, saved views). They live
|
|
37
|
-
* in the same persisted schema but are NOT content: the CMS session, the
|
|
38
|
-
* sidebar, and the content preview engine must never offer them as authorable
|
|
39
|
-
* collections. Routes/redirects are NO LONGER system collections (EC-207): they
|
|
40
|
-
* are config-sourced and never persisted to the backend.
|
|
41
|
-
*/
|
|
42
|
-
export const STUDIO_SYSTEM_COLLECTION_IDS = new Set([
|
|
43
|
-
DATA_SOURCES_COLLECTION,
|
|
44
|
-
VIEWS_COLLECTION,
|
|
45
|
-
]);
|
|
1
|
+
import { documentKey, orphanedCollections } from '@elytracms/core/cms-core';
|
|
46
2
|
/**
|
|
47
3
|
* Load the persisted CMS content state. `null` when the project has no
|
|
48
|
-
* persisted content schema
|
|
49
|
-
* persister's baseline will persist them on
|
|
50
|
-
*
|
|
51
|
-
* system stores (saved views, routes) may create the schema record before any
|
|
52
|
-
* content baseline ran.
|
|
4
|
+
* persisted content schema, or one that declares no collections (first run —
|
|
5
|
+
* the session seeds fixtures and the persister's baseline will persist them on
|
|
6
|
+
* the first edit).
|
|
53
7
|
*/
|
|
54
8
|
export async function loadHydratedCms(operations, projectId) {
|
|
55
9
|
const schema = await operations.schema.get({ projectId });
|
|
56
10
|
if (!schema.ok) {
|
|
57
11
|
return schema.error.code === 'not-found' ? { ok: true, value: null } : schema;
|
|
58
12
|
}
|
|
59
|
-
const collections = schema.value.collections
|
|
13
|
+
const collections = schema.value.collections;
|
|
60
14
|
if (collections.length === 0)
|
|
61
15
|
return { ok: true, value: null };
|
|
62
16
|
const documents = await operations.documents.list({ projectId });
|
|
@@ -66,15 +20,23 @@ export async function loadHydratedCms(operations, projectId) {
|
|
|
66
20
|
// listing/relation results resolve their nested `asset` fields (EC-262). Non-fatal:
|
|
67
21
|
// a failed asset list degrades previews to no images, never blocks the session.
|
|
68
22
|
const assets = await operations.assets.list({ projectId });
|
|
69
|
-
const contentIds = new Set(collections.map((collection) => collection.id));
|
|
70
|
-
const contentRecords = documents.value.filter((record) => contentIds.has(record.document.collection));
|
|
71
23
|
// EC-224: a record is published when its version pointer is set — the same
|
|
72
24
|
// gate delivery reads. There is no flat-state fallback (the field is gone).
|
|
73
25
|
const isRecordPublished = (record) => record.publishedVersion !== undefined;
|
|
26
|
+
// The declared set, read once: the session carries the documents of these
|
|
27
|
+
// collections, and every other stored row is COUNTED (EC-606), never dropped
|
|
28
|
+
// silently.
|
|
29
|
+
const declaredIds = new Set(collections.map((collection) => collection.id));
|
|
30
|
+
const contentRecords = documents.value.filter((record) => declaredIds.has(record.document.collection));
|
|
31
|
+
const orphans = orphanedCollections(documents.value.map((record) => ({
|
|
32
|
+
collection: record.document.collection,
|
|
33
|
+
published: isRecordPublished(record),
|
|
34
|
+
})), declaredIds);
|
|
74
35
|
return {
|
|
75
36
|
ok: true,
|
|
76
37
|
value: {
|
|
77
38
|
collections,
|
|
39
|
+
orphans,
|
|
78
40
|
// The live row hydrates AS PERSISTED — a continuous draft (EC-224). We no
|
|
79
41
|
// longer stamp a `state:'published'` mirror: published-ness is the version
|
|
80
42
|
// pointer (`publishedKeys`), which every studio reader now consults (the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-hydration.js","sourceRoot":"","sources":["../../../src/lib/operations/session-hydration.ts"],"names":[],"mappings":"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","sourcesContent":["import { documentKey } from '@elytracms/core/cms-core'\nimport type { CmsDocument, CollectionDef } from '@elytracms/core/cms-core'\nimport type { AssetRecord } from '@elytracms/core/persistence'\nimport { DATA_SOURCES_COLLECTION } from '../data/sources-store'\nimport type { OperationResult, OperationsClient } from './operations-package'\n\n/**\n * Legacy marker (EC-246b): editor-owned saved views are retired — list\n * presentation is config-as-code now (named views live in `cms/`). The id is kept\n * only so hydration keeps excluding any `cms-saved-views` documents a pre-EC-246b\n * deployment persisted, so they never surface as content.\n */\nexport const VIEWS_COLLECTION = 'cms-saved-views'\n\n/**\n * Persisted-session hydration (Milestone B sign-off fix, EC-177 gap 1).\n *\n * Against a persistent backend (Convex mode) a studio session must BOOT from\n * what is persisted — the saved CMS schema + documents (+ asset records) — and\n * fall back to the deterministic fixtures only when the project has nothing\n * persisted yet (first run). Before this, every session seeded from fixtures,\n * so reloads hid the editor's work.\n *\n * EC-287: the DB-resident project graph and its publish path are retired —\n * layouts/config/schema/components are CODE, the DB holds content only. A\n * session no longer hydrates a graph revision or graph-publishing state; CMS\n * content (with per-document `publishedVersion`, EC-224) is the only persisted\n * surface a session boots from.\n *\n * This module is the pure load half: read-only `@elytracms/core/operations` queries\n * composed into one hydration result. `not-found` is a *normal* answer (the\n * first-run fallback); every other failure propagates as the structured\n * operation error so the gate renders it — never a silent fixture fallback.\n * The React gate that applies it lives in\n * `components/studio/project-session.tsx`. EC-361: the playground calls the\n * SAME loader (over the seeded in-memory adapter, through operations — AD-17)\n * to hydrate the canvas/preview content session; only the render-gating is\n * Convex-specific.\n */\n\n/**\n * Collections owned by studio subsystems (data sources, saved views). They live\n * in the same persisted schema but are NOT content: the CMS session, the\n * sidebar, and the content preview engine must never offer them as authorable\n * collections. Routes/redirects are NO LONGER system collections (EC-207): they\n * are config-sourced and never persisted to the backend.\n */\nexport const STUDIO_SYSTEM_COLLECTION_IDS: ReadonlySet<string> = new Set([\n DATA_SOURCES_COLLECTION,\n VIEWS_COLLECTION,\n])\n\n/** The persisted CMS content of a project: schema + documents, system collections excluded. */\nexport interface HydratedCms {\n collections: CollectionDef[]\n documents: CmsDocument[]\n /**\n * Published document keys (EC-224), via the version pointer: a record is\n * published iff `publishedVersion` is set. The session boots its\n * `publishedKeys` from this — published-ness is the pointer, nothing else.\n */\n publishedKeys: string[]\n /**\n * Per-document last-write timestamps (EC-445), keyed by `documentKey`\n * (`collection:id`) → the persisted `cmsDocuments.updatedAt` ISO string. Backs the\n * `$updatedAt` list column; the session seeds its `documentUpdatedAt` map from this.\n * Optional so pre-EC-445 fixtures/callers stay valid (the session falls back to a\n * session-start stamp for the seeded documents); `loadHydratedCms` always sets it.\n */\n timestamps?: Record<string, string>\n /**\n * Per-document FIRST-publish instants (EC-447), keyed by `documentKey`\n * (`collection:id`) → the persisted `cmsDocuments.publishedAt` ISO string. Present\n * only for documents that carry one (currently published, with a recorded publish\n * instant); the session seeds its `documentPublishedAt` map from this. Backs the\n * `$publishedAt` list column. UNLIKE {@link timestamps} there is no session-start\n * fallback — an absent entry is a real \"no publish date\" (EmDash), and the store\n * re-stamps/clears it authoritatively on publish/unpublish. Optional so pre-EC-447\n * fixtures/callers stay valid; `loadHydratedCms` always sets it.\n */\n publishTimestamps?: Record<string, string>\n /**\n * Document keys whose working draft carries UNPUBLISHED CHANGES (EC-449) — the\n * persisted `cmsDocuments.hasUnpublishedChanges === true` set, keyed like\n * {@link publishedKeys}. Backs the amber \"changed\" state of the `$state` list\n * dot; the session seeds its changed-set from this and re-stamps it\n * authoritatively on save/publish/unpublish from the returned records. Only\n * `true` flags contribute — an absent entry means \"no unpublished changes\"\n * (`false` or not yet materialized both render the same dot). Optional so\n * pre-EC-449 fixtures/callers stay valid; `loadHydratedCms` always sets it.\n */\n changedKeys?: string[]\n /**\n * The project's asset records (the `assets` table — NOT cms `asset`-collection\n * documents). The preview content engine wires these into its lookup so listing/\n * relation results resolve their `asset`-typed FIELDS (e.g. a ProductGrid product's\n * `gallery`) to delivery shape — matching what the renderer's `resolveAsset` already\n * does for static `asset` props (EC-262). Without them, the studio canvas renders\n * such nested images blank even though the documents reference real assets.\n */\n assets: AssetRecord[]\n}\n\n/** Everything a Convex-mode session boots from. `null` fields mean \"nothing persisted\" (first run). */\nexport interface HydratedProjectSession {\n /** Persisted CMS schema + documents, or `null` when no content schema was ever saved. */\n cms: HydratedCms | null\n}\n\n/**\n * Load the persisted CMS content state. `null` when the project has no\n * persisted content schema (first run — the session seeds fixtures and the\n * persister's baseline will persist them on the first edit). A schema that\n * contains only studio system collections counts as \"no content persisted\":\n * system stores (saved views, routes) may create the schema record before any\n * content baseline ran.\n */\nexport async function loadHydratedCms(\n operations: OperationsClient,\n projectId: string,\n): Promise<OperationResult<HydratedCms | null>> {\n const schema = await operations.schema.get({ projectId })\n if (!schema.ok) {\n return schema.error.code === 'not-found' ? { ok: true, value: null } : schema\n }\n const collections = schema.value.collections.filter(\n (collection) => !STUDIO_SYSTEM_COLLECTION_IDS.has(collection.id),\n )\n if (collections.length === 0) return { ok: true, value: null }\n const documents = await operations.documents.list({ projectId })\n if (!documents.ok) return documents\n // The project's asset records (assets table), wired into the preview lookup so\n // listing/relation results resolve their nested `asset` fields (EC-262). Non-fatal:\n // a failed asset list degrades previews to no images, never blocks the session.\n const assets = await operations.assets.list({ projectId })\n const contentIds = new Set(collections.map((collection) => collection.id))\n const contentRecords = documents.value.filter((record) =>\n contentIds.has(record.document.collection),\n )\n // EC-224: a record is published when its version pointer is set — the same\n // gate delivery reads. There is no flat-state fallback (the field is gone).\n const isRecordPublished = (record: (typeof contentRecords)[number]) =>\n record.publishedVersion !== undefined\n return {\n ok: true,\n value: {\n collections,\n // The live row hydrates AS PERSISTED — a continuous draft (EC-224). We no\n // longer stamp a `state:'published'` mirror: published-ness is the version\n // pointer (`publishedKeys`), which every studio reader now consults (the\n // table badge/$state via `isPublished`, the preview engine via the pin).\n documents: contentRecords.map((record) => record.document),\n publishedKeys: contentRecords\n .filter(isRecordPublished)\n .map((record) => documentKey(record.document)),\n // EC-445: carry each record's `updatedAt` onto the session for the $updatedAt\n // column — a per-document metadata accessor, exactly like `publishedKeys`.\n timestamps: Object.fromEntries(\n contentRecords.map((record) => [documentKey(record.document), record.updatedAt]),\n ),\n // EC-447: carry each PUBLISHED record's materialized `publishedAt` for the\n // $publishedAt column. Only records that actually have one contribute (no\n // session-start fallback) — an absent entry is a real \"no publish date\".\n publishTimestamps: Object.fromEntries(\n contentRecords\n .filter((record) => record.publishedAt !== undefined)\n .map((record) => [documentKey(record.document), record.publishedAt as string]),\n ),\n // EC-449: the documents whose draft carries unpublished changes (the amber\n // list dot) — only materialized `true` flags contribute (absent/false both\n // mean \"not changed\"; the flag only exists on published docs).\n changedKeys: contentRecords\n .filter((record) => record.hasUnpublishedChanges === true)\n .map((record) => documentKey(record.document)),\n assets: assets.ok ? assets.value : [],\n },\n }\n}\n\n/**\n * Load everything a project session hydrates from. Post-EC-287 that is just the\n * persisted CMS content (schema + documents + assets): the project graph and its\n * publish path are gone, so there is no graph revision or graph-publishing state\n * to load. Per-document `publishedVersion` (EC-224) is the only published surface.\n */\nexport async function loadProjectSession(\n operations: OperationsClient,\n projectId: string,\n): Promise<OperationResult<HydratedProjectSession>> {\n const cms = await loadHydratedCms(operations, projectId)\n if (!cms.ok) return cms\n return { ok: true, value: { cms: cms.value } }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"session-hydration.js","sourceRoot":"","sources":["../../../src/lib/operations/session-hydration.ts"],"names":[],"mappings":"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","sourcesContent":["import { documentKey, orphanedCollections } from '@elytracms/core/cms-core'\nimport type { CmsDocument, CollectionDef, OrphanedCollection } from '@elytracms/core/cms-core'\nimport type { AssetRecord } from '@elytracms/core/persistence'\nimport type { OperationResult, OperationsClient } from './operations-package'\n\n/**\n * Persisted-session hydration (Milestone B sign-off fix, EC-177 gap 1).\n *\n * Against a persistent backend (Convex mode) a studio session must BOOT from\n * what is persisted — the saved CMS schema + documents (+ asset records) — and\n * fall back to the deterministic fixtures only when the project has nothing\n * persisted yet (first run). Before this, every session seeded from fixtures,\n * so reloads hid the editor's work.\n *\n * EC-287: the DB-resident project graph and its publish path are retired —\n * layouts/config/schema/components are CODE, the DB holds content only. A\n * session no longer hydrates a graph revision or graph-publishing state; CMS\n * content (with per-document `publishedVersion`, EC-224) is the only persisted\n * surface a session boots from.\n *\n * This module is the pure load half: read-only `@elytracms/core/operations` queries\n * composed into one hydration result. `not-found` is a *normal* answer (the\n * first-run fallback); every other failure propagates as the structured\n * operation error so the gate renders it — never a silent fixture fallback.\n * The React gate that applies it lives in\n * `components/studio/project-session.tsx`. EC-361: the playground calls the\n * SAME loader (over the seeded in-memory adapter, through operations — AD-17)\n * to hydrate the canvas/preview content session; only the render-gating is\n * Convex-specific.\n */\n\n/** The persisted CMS content of a project: the declared schema + the documents of its collections. */\nexport interface HydratedCms {\n collections: CollectionDef[]\n /** Documents of the declared collections only — see {@link orphans} for the rest. */\n documents: CmsDocument[]\n /**\n * EC-606 — the stored rows the session does NOT carry: every collection id that\n * stored documents use but the project does not declare, with its row count and\n * how many of those rows are published. Sorted by collection id; empty when every\n * row belongs to a declared collection.\n *\n * Hydration keeps only declared collections' documents (there is no schema to edit\n * the rest under). Before EC-606 the others were dropped right here with no count,\n * so renaming a collection id in config made its documents vanish without a trace.\n * Counted by the one core counter (`orphanedCollections`), which `elytra doctor`\n * also runs over the published snapshot. The rail foot reports this list through\n * the content session (`configureContentSession(contentSessionOf(cms))`).\n *\n * EC-606 B1: there is no \"system collection\" exemption any more. The only one left\n * was `cms-saved-views`, the editor saved views EC-246b retired; nothing reads\n * those rows, so they are counted like any other undeclared collection.\n */\n orphans: OrphanedCollection[]\n /**\n * Published document keys (EC-224), via the version pointer: a record is\n * published iff `publishedVersion` is set. The session boots its\n * `publishedKeys` from this — published-ness is the pointer, nothing else.\n */\n publishedKeys: string[]\n /**\n * Per-document last-write timestamps (EC-445), keyed by `documentKey`\n * (`collection:id`) → the persisted `cmsDocuments.updatedAt` ISO string. Backs the\n * `$updatedAt` list column; the session seeds its `documentUpdatedAt` map from this.\n * Optional so pre-EC-445 fixtures/callers stay valid (the session falls back to a\n * session-start stamp for the seeded documents); `loadHydratedCms` always sets it.\n */\n timestamps?: Record<string, string>\n /**\n * Per-document FIRST-publish instants (EC-447), keyed by `documentKey`\n * (`collection:id`) → the persisted `cmsDocuments.publishedAt` ISO string. Present\n * only for documents that carry one (currently published, with a recorded publish\n * instant); the session seeds its `documentPublishedAt` map from this. Backs the\n * `$publishedAt` list column. UNLIKE {@link timestamps} there is no session-start\n * fallback — an absent entry is a real \"no publish date\" (EmDash), and the store\n * re-stamps/clears it authoritatively on publish/unpublish. Optional so pre-EC-447\n * fixtures/callers stay valid; `loadHydratedCms` always sets it.\n */\n publishTimestamps?: Record<string, string>\n /**\n * Document keys whose working draft carries UNPUBLISHED CHANGES (EC-449) — the\n * persisted `cmsDocuments.hasUnpublishedChanges === true` set, keyed like\n * {@link publishedKeys}. Backs the amber \"changed\" state of the `$state` list\n * dot; the session seeds its changed-set from this and re-stamps it\n * authoritatively on save/publish/unpublish from the returned records. Only\n * `true` flags contribute — an absent entry means \"no unpublished changes\"\n * (`false` or not yet materialized both render the same dot). Optional so\n * pre-EC-449 fixtures/callers stay valid; `loadHydratedCms` always sets it.\n */\n changedKeys?: string[]\n /**\n * The project's asset records (the `assets` table — NOT cms `asset`-collection\n * documents). The preview content engine wires these into its lookup so listing/\n * relation results resolve their `asset`-typed FIELDS (e.g. a ProductGrid product's\n * `gallery`) to delivery shape — matching what the renderer's `resolveAsset` already\n * does for static `asset` props (EC-262). Without them, the studio canvas renders\n * such nested images blank even though the documents reference real assets.\n */\n assets: AssetRecord[]\n}\n\n/** Everything a Convex-mode session boots from. `null` fields mean \"nothing persisted\" (first run). */\nexport interface HydratedProjectSession {\n /** Persisted CMS schema + documents, or `null` when no content schema was ever saved. */\n cms: HydratedCms | null\n}\n\n/**\n * Load the persisted CMS content state. `null` when the project has no\n * persisted content schema, or one that declares no collections (first run —\n * the session seeds fixtures and the persister's baseline will persist them on\n * the first edit).\n */\nexport async function loadHydratedCms(\n operations: OperationsClient,\n projectId: string,\n): Promise<OperationResult<HydratedCms | null>> {\n const schema = await operations.schema.get({ projectId })\n if (!schema.ok) {\n return schema.error.code === 'not-found' ? { ok: true, value: null } : schema\n }\n const collections = schema.value.collections\n if (collections.length === 0) return { ok: true, value: null }\n const documents = await operations.documents.list({ projectId })\n if (!documents.ok) return documents\n // The project's asset records (assets table), wired into the preview lookup so\n // listing/relation results resolve their nested `asset` fields (EC-262). Non-fatal:\n // a failed asset list degrades previews to no images, never blocks the session.\n const assets = await operations.assets.list({ projectId })\n // EC-224: a record is published when its version pointer is set — the same\n // gate delivery reads. There is no flat-state fallback (the field is gone).\n const isRecordPublished = (record: (typeof documents.value)[number]) =>\n record.publishedVersion !== undefined\n // The declared set, read once: the session carries the documents of these\n // collections, and every other stored row is COUNTED (EC-606), never dropped\n // silently.\n const declaredIds = new Set(collections.map((collection) => collection.id))\n const contentRecords = documents.value.filter((record) =>\n declaredIds.has(record.document.collection),\n )\n const orphans = orphanedCollections(\n documents.value.map((record) => ({\n collection: record.document.collection,\n published: isRecordPublished(record),\n })),\n declaredIds,\n )\n return {\n ok: true,\n value: {\n collections,\n orphans,\n // The live row hydrates AS PERSISTED — a continuous draft (EC-224). We no\n // longer stamp a `state:'published'` mirror: published-ness is the version\n // pointer (`publishedKeys`), which every studio reader now consults (the\n // table badge/$state via `isPublished`, the preview engine via the pin).\n documents: contentRecords.map((record) => record.document),\n publishedKeys: contentRecords\n .filter(isRecordPublished)\n .map((record) => documentKey(record.document)),\n // EC-445: carry each record's `updatedAt` onto the session for the $updatedAt\n // column — a per-document metadata accessor, exactly like `publishedKeys`.\n timestamps: Object.fromEntries(\n contentRecords.map((record) => [documentKey(record.document), record.updatedAt]),\n ),\n // EC-447: carry each PUBLISHED record's materialized `publishedAt` for the\n // $publishedAt column. Only records that actually have one contribute (no\n // session-start fallback) — an absent entry is a real \"no publish date\".\n publishTimestamps: Object.fromEntries(\n contentRecords\n .filter((record) => record.publishedAt !== undefined)\n .map((record) => [documentKey(record.document), record.publishedAt as string]),\n ),\n // EC-449: the documents whose draft carries unpublished changes (the amber\n // list dot) — only materialized `true` flags contribute (absent/false both\n // mean \"not changed\"; the flag only exists on published docs).\n changedKeys: contentRecords\n .filter((record) => record.hasUnpublishedChanges === true)\n .map((record) => documentKey(record.document)),\n assets: assets.ok ? assets.value : [],\n },\n }\n}\n\n/**\n * Load everything a project session hydrates from. Post-EC-287 that is just the\n * persisted CMS content (schema + documents + assets): the project graph and its\n * publish path are gone, so there is no graph revision or graph-publishing state\n * to load. Per-document `publishedVersion` (EC-224) is the only published surface.\n */\nexport async function loadProjectSession(\n operations: OperationsClient,\n projectId: string,\n): Promise<OperationResult<HydratedProjectSession>> {\n const cms = await loadHydratedCms(operations, projectId)\n if (!cms.ok) return cms\n return { ok: true, value: { cms: cms.value } }\n}\n"]}
|