@elytracms/studio 0.0.80 → 0.0.82
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/convex/contract-parity.test.ts +1 -1
- package/convex/functions.ts +13 -3
- package/convex/http-routes.test.ts +143 -0
- package/convex/http.ts +19 -9
- package/convex/redirect-revalidate.test.ts +130 -0
- package/convex/revalidate.ts +9 -7
- package/convex/schema.ts +0 -19
- package/convex/triggers.ts +33 -3
- package/convex/users.ts +1 -1
- package/dist/components/ai/ai-chat-panel.js +26 -4
- package/dist/components/ai/ai-chat-panel.js.map +1 -1
- package/dist/components/ai/proposal-card.d.ts +27 -0
- package/dist/components/ai/proposal-card.js +31 -3
- package/dist/components/ai/proposal-card.js.map +1 -1
- package/dist/components/ai/proposal-issues.d.ts +55 -0
- package/dist/components/ai/proposal-issues.js +69 -0
- package/dist/components/ai/proposal-issues.js.map +1 -0
- package/dist/components/ai/translate-document-dialog.js +38 -3
- package/dist/components/ai/translate-document-dialog.js.map +1 -1
- package/dist/components/app-views/default-modules.d.ts +141 -44
- package/dist/components/app-views/default-modules.js +229 -65
- package/dist/components/app-views/default-modules.js.map +1 -1
- package/dist/components/app-views/module.d.ts +38 -0
- package/dist/components/app-views/module.js.map +1 -1
- package/dist/components/app-views/sidebar.d.ts +5 -2
- package/dist/components/app-views/sidebar.js +16 -5
- package/dist/components/app-views/sidebar.js.map +1 -1
- package/dist/components/app-views/studio-config.d.ts +28 -1
- package/dist/components/app-views/studio-config.js +1 -1
- package/dist/components/app-views/studio-config.js.map +1 -1
- package/dist/components/builder/canvas-render-context.js +10 -2
- package/dist/components/builder/canvas-render-context.js.map +1 -1
- package/dist/components/builder/clipboard-actions.js +5 -3
- package/dist/components/builder/clipboard-actions.js.map +1 -1
- package/dist/components/builder/reuse-context.js +7 -1
- package/dist/components/builder/reuse-context.js.map +1 -1
- package/dist/components/builder/reuse.d.ts +94 -16
- package/dist/components/builder/reuse.js +201 -19
- package/dist/components/builder/reuse.js.map +1 -1
- package/dist/components/cms/bulk-actions.js +10 -5
- package/dist/components/cms/bulk-actions.js.map +1 -1
- package/dist/components/cms/cell-edit.d.ts +18 -3
- package/dist/components/cms/cell-edit.js +29 -7
- package/dist/components/cms/cell-edit.js.map +1 -1
- package/dist/components/cms/cms-store.d.ts +54 -14
- package/dist/components/cms/cms-store.js +142 -45
- package/dist/components/cms/cms-store.js.map +1 -1
- package/dist/components/cms/collection-table.js +2 -2
- package/dist/components/cms/collection-table.js.map +1 -1
- package/dist/components/cms/detail-editor.js +15 -14
- package/dist/components/cms/detail-editor.js.map +1 -1
- package/dist/components/cms/document-editor-context.d.ts +12 -1
- package/dist/components/cms/document-editor-context.js.map +1 -1
- package/dist/components/cms/field-control.js +22 -8
- package/dist/components/cms/field-control.js.map +1 -1
- package/dist/components/cms/field-renderer.js +8 -1
- package/dist/components/cms/field-renderer.js.map +1 -1
- package/dist/components/cms/gate-validation.d.ts +36 -0
- package/dist/components/cms/gate-validation.js +17 -0
- package/dist/components/cms/gate-validation.js.map +1 -0
- package/dist/components/cms/import-export.js +2 -1
- package/dist/components/cms/import-export.js.map +1 -1
- package/dist/components/cms/inline-field-errors.d.ts +8 -0
- package/dist/components/cms/inline-field-errors.js +52 -0
- package/dist/components/cms/inline-field-errors.js.map +1 -0
- package/dist/components/cms/issues-pane.d.ts +4 -2
- package/dist/components/cms/issues-pane.js +13 -7
- package/dist/components/cms/issues-pane.js.map +1 -1
- package/dist/components/cms/slug-collision-report.d.ts +41 -0
- package/dist/components/cms/slug-collision-report.js +52 -0
- package/dist/components/cms/slug-collision-report.js.map +1 -0
- package/dist/components/components/transfer-dialog.js +10 -12
- package/dist/components/components/transfer-dialog.js.map +1 -1
- package/dist/components/deployment-setup-gate.d.ts +24 -0
- package/dist/components/deployment-setup-gate.js +29 -4
- package/dist/components/deployment-setup-gate.js.map +1 -1
- package/dist/components/issue-severity.d.ts +73 -0
- package/dist/components/issue-severity.js +56 -0
- package/dist/components/issue-severity.js.map +1 -0
- package/dist/components/nav-issues.d.ts +68 -0
- package/dist/components/nav-issues.js +78 -0
- package/dist/components/nav-issues.js.map +1 -0
- package/dist/components/operation-error.js +3 -3
- package/dist/components/operation-error.js.map +1 -1
- package/dist/components/project-list.d.ts +9 -3
- package/dist/components/project-list.js +11 -5
- package/dist/components/project-list.js.map +1 -1
- package/dist/components/project-switcher.d.ts +3 -1
- package/dist/components/project-switcher.js +4 -2
- package/dist/components/project-switcher.js.map +1 -1
- package/dist/components/redirects/redirects-store.d.ts +21 -0
- package/dist/components/redirects/redirects-store.js +17 -4
- package/dist/components/redirects/redirects-store.js.map +1 -1
- package/dist/components/section-nav.js +75 -10
- package/dist/components/section-nav.js.map +1 -1
- package/dist/customization.d.ts +7 -0
- package/dist/customization.js.map +1 -1
- package/dist/lib/ai/apply-content.js +6 -7
- package/dist/lib/ai/apply-content.js.map +1 -1
- package/dist/lib/ai/chat-context.d.ts +32 -26
- package/dist/lib/ai/chat-context.js +81 -9
- package/dist/lib/ai/chat-context.js.map +1 -1
- package/dist/lib/ai/constraints.d.ts +42 -0
- package/dist/lib/ai/constraints.js +259 -0
- package/dist/lib/ai/constraints.js.map +1 -0
- package/dist/lib/ai/index.d.ts +2 -0
- package/dist/lib/ai/index.js +2 -0
- package/dist/lib/ai/index.js.map +1 -1
- package/dist/lib/ai/proposal-validation.d.ts +26 -0
- package/dist/lib/ai/proposal-validation.js +17 -0
- package/dist/lib/ai/proposal-validation.js.map +1 -0
- package/dist/lib/ai/shape.js +60 -13
- package/dist/lib/ai/shape.js.map +1 -1
- package/dist/lib/ai/tools.d.ts +8 -4
- package/dist/lib/ai/tools.js +43 -14
- package/dist/lib/ai/tools.js.map +1 -1
- package/dist/lib/ai/translate-document.d.ts +35 -1
- package/dist/lib/ai/translate-document.js +16 -1
- package/dist/lib/ai/translate-document.js.map +1 -1
- package/dist/lib/ai/types.d.ts +11 -6
- package/dist/lib/ai/types.js.map +1 -1
- package/dist/lib/ai/update-fields.d.ts +8 -6
- package/dist/lib/ai/update-fields.js +9 -22
- package/dist/lib/ai/update-fields.js.map +1 -1
- package/dist/lib/clipboard/paste-issues.d.ts +5 -0
- package/dist/lib/clipboard/paste-issues.js +16 -6
- package/dist/lib/clipboard/paste-issues.js.map +1 -1
- package/dist/lib/cms/document-gate.d.ts +37 -0
- package/dist/lib/cms/document-gate.js +40 -0
- package/dist/lib/cms/document-gate.js.map +1 -0
- package/dist/lib/cms-views/view-model.d.ts +5 -0
- package/dist/lib/cms-views/view-model.js +10 -4
- package/dist/lib/cms-views/view-model.js.map +1 -1
- package/dist/lib/data/collection-content.d.ts +6 -1
- package/dist/lib/data/collection-content.js +51 -15
- package/dist/lib/data/collection-content.js.map +1 -1
- package/dist/lib/i18n/i18n.d.ts +51 -16
- package/dist/lib/i18n/i18n.js +119 -43
- package/dist/lib/i18n/i18n.js.map +1 -1
- package/dist/lib/i18n/index.d.ts +8 -1
- package/dist/lib/i18n/index.js +1 -1
- package/dist/lib/i18n/index.js.map +1 -1
- package/dist/lib/i18n/messages.de.js +200 -0
- package/dist/lib/i18n/messages.de.js.map +1 -1
- package/dist/lib/i18n/messages.en.d.ts +162 -0
- package/dist/lib/i18n/messages.en.js +226 -0
- package/dist/lib/i18n/messages.en.js.map +1 -1
- package/dist/lib/i18n/types.d.ts +15 -9
- package/dist/lib/i18n/types.js.map +1 -1
- package/dist/lib/locale/binding-fallback.d.ts +8 -1
- package/dist/lib/locale/binding-fallback.js +8 -1
- package/dist/lib/locale/binding-fallback.js.map +1 -1
- package/dist/lib/locale/locale-store.d.ts +41 -9
- package/dist/lib/locale/locale-store.js +34 -15
- package/dist/lib/locale/locale-store.js.map +1 -1
- package/dist/lib/locale/locale-switcher.d.ts +3 -1
- package/dist/lib/locale/locale-switcher.js +4 -2
- package/dist/lib/locale/locale-switcher.js.map +1 -1
- package/dist/lib/locale/use-studio-locale.d.ts +9 -4
- package/dist/lib/locale/use-studio-locale.js.map +1 -1
- package/dist/lib/operations/studio-operations.js +19 -2
- package/dist/lib/operations/studio-operations.js.map +1 -1
- package/dist/lib/persistence/convex-adapter.d.ts +1 -3
- package/dist/lib/persistence/convex-adapter.js +0 -55
- package/dist/lib/persistence/convex-adapter.js.map +1 -1
- package/dist/lib/persistence/deployment-readiness.js +10 -0
- package/dist/lib/persistence/deployment-readiness.js.map +1 -1
- package/dist/lib/persona/labels.d.ts +85 -9
- package/dist/lib/persona/labels.js +198 -58
- package/dist/lib/persona/labels.js.map +1 -1
- package/dist/lib/routes/route-issues.d.ts +9 -0
- package/dist/lib/routes/route-issues.js +117 -0
- package/dist/lib/routes/route-issues.js.map +1 -0
- package/dist/lib/webhooks/dispatcher.d.ts +5 -2
- package/dist/lib/webhooks/dispatcher.js.map +1 -1
- package/dist/lib/webhooks/tags.js +150 -17
- package/dist/lib/webhooks/tags.js.map +1 -1
- package/dist/lib/workspace/config-issues.d.ts +56 -0
- package/dist/lib/workspace/config-issues.js +72 -0
- package/dist/lib/workspace/config-issues.js.map +1 -0
- package/dist/lib/workspace/index.d.ts +24 -8
- package/dist/lib/workspace/index.js +28 -18
- package/dist/lib/workspace/index.js.map +1 -1
- package/dist/lib/workspace/switcher-source.d.ts +20 -3
- package/dist/lib/workspace/switcher-source.js.map +1 -1
- package/dist/lib/workspace/workspace-source.js +27 -0
- package/dist/lib/workspace/workspace-source.js.map +1 -1
- package/dist/previews/manifest-format.d.ts +13 -6
- package/dist/previews/manifest-format.js +9 -2
- package/dist/previews/manifest-format.js.map +1 -1
- package/dist/router/root.d.ts +36 -1
- package/dist/router/root.js +54 -7
- package/dist/router/root.js.map +1 -1
- package/package.json +2 -2
- package/convex/cliTokens.ts +0 -63
- package/convex/sync.ts +0 -297
package/dist/lib/i18n/types.d.ts
CHANGED
|
@@ -32,16 +32,22 @@ export interface StudioI18nConfig {
|
|
|
32
32
|
*/
|
|
33
33
|
defaultLocale?: string;
|
|
34
34
|
/**
|
|
35
|
-
* **Pin** the studio UI to a fixed locale, overriding browser detection.
|
|
36
|
-
*
|
|
35
|
+
* **Pin** the studio UI to a fixed locale, overriding browser detection. **This is the
|
|
36
|
+
* supported way to choose the studio's language** (EC-513) — set it for any deployment
|
|
37
|
+
* whose editors work in one language, which is what both production consumers do.
|
|
37
38
|
*
|
|
38
|
-
* Why it
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
39
|
+
* Why prefer it: browser detection reads `navigator.languages`, which is **client-only**.
|
|
40
|
+
* The server renders the default locale, and the studio therefore boots in that locale
|
|
41
|
+
* and hands over to the browser's preference only after hydration — correct, but it
|
|
42
|
+
* means a moment of default-locale chrome on a cold load. A pin is read from *config*,
|
|
43
|
+
* imported identically on server and client, so the right language is there from the
|
|
44
|
+
* first byte.
|
|
45
|
+
*
|
|
46
|
+
* There is deliberately **no server-side locale seam** (no `Accept-Language` resolver):
|
|
47
|
+
* locale resolution is client-side, and a pin is how a host states the answer up front.
|
|
48
|
+
*
|
|
49
|
+
* A locale that is not available (neither `defaultLocale` nor a key of `locales`) is
|
|
50
|
+
* ignored with a console signal, and detection resumes. Omit it to follow the browser.
|
|
45
51
|
*/
|
|
46
52
|
locale?: string;
|
|
47
53
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/lib/i18n/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/lib/i18n/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAsDH;;;;GAIG;AACH,MAAM,UAAU,GAAG,CAAC,OAAe,EAAE,WAAoB;IACvD,OAAO,WAAW,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAA;AAC3E,CAAC","sourcesContent":["/**\n * Studio-UI localization types (EC-241) — the leaf module both the message catalog\n * and the resolver import, so nothing here may import studio runtime code (keeps the\n * `defineStudio` config surface free of import cycles).\n *\n * This is the **studio-chrome** axis (engine-owned strings). Schema labels are a\n * separate mechanism — a {@link import('@elytracms/core/cms-core').LocalizedString}\n * on the field/collection def, resolved via `resolveLabel`. Content-*value*\n * localization is a third, unrelated axis (`cms-core/localization.ts`).\n */\n\n/** A source-catalog entry: the English string + an optional translator hint. */\nexport interface SourceMessage {\n /** The English (source-of-truth) string. */\n message: string\n /**\n * Translator hint/context — the same word can translate differently by context\n * (the standard `msgctxt`/Lingui-comment idea). **Authoring/translation-time only:**\n * it lives on the in-engine source catalog and is NOT shipped in the flat\n * consumer-provided translation catalogs.\n */\n description?: string\n}\n\n/** The in-engine English source catalog: message key → {@link SourceMessage}. */\nexport type SourceCatalog = Record<string, SourceMessage>\n\n/** A consumer-provided translation catalog: flat message key → localized string. */\nexport type MessageCatalog = Record<string, string>\n\n/** The studio-UI localization config passed to `defineStudio({ i18n })` (EC-241). */\nexport interface StudioI18nConfig {\n /**\n * The locale used when the browser's preferred locale matches nothing available.\n * Defaults to `'en'` (the in-engine source language).\n */\n defaultLocale?: string\n /**\n * **Pin** the studio UI to a fixed locale, overriding browser detection. **This is the\n * supported way to choose the studio's language** (EC-513) — set it for any deployment\n * whose editors work in one language, which is what both production consumers do.\n *\n * Why prefer it: browser detection reads `navigator.languages`, which is **client-only**.\n * The server renders the default locale, and the studio therefore boots in that locale\n * and hands over to the browser's preference only after hydration — correct, but it\n * means a moment of default-locale chrome on a cold load. A pin is read from *config*,\n * imported identically on server and client, so the right language is there from the\n * first byte.\n *\n * There is deliberately **no server-side locale seam** (no `Accept-Language` resolver):\n * locale resolution is client-side, and a pin is how a host states the answer up front.\n *\n * A locale that is not available (neither `defaultLocale` nor a key of `locales`) is\n * ignored with a console signal, and detection resumes. Omit it to follow the browser.\n */\n locale?: string\n /**\n * Imported translation catalogs keyed by locale code, e.g. `{ de: deMessages }`.\n * English is always available as the built-in source, so it need not be listed.\n */\n locales?: Record<string, MessageCatalog>\n}\n\n/**\n * Author a source-catalog entry with an optional translator hint (EC-241). Kept in\n * this leaf module so `messages.en.ts` can build the catalog without importing the\n * resolver (which would import studio runtime code).\n */\nexport function msg(message: string, description?: string): SourceMessage {\n return description !== undefined ? { message, description } : { message }\n}\n"]}
|
|
@@ -25,4 +25,11 @@ export interface BoundValueFallback extends DisplayedFallback {
|
|
|
25
25
|
* `null` when the binding is not CMS-shaped, points at no field, or the value
|
|
26
26
|
* is a real translation.
|
|
27
27
|
*/
|
|
28
|
-
export declare function cmsBindingFallback(binding: BindingReference, source: DataSource | undefined,
|
|
28
|
+
export declare function cmsBindingFallback(binding: BindingReference, source: DataSource | undefined,
|
|
29
|
+
/**
|
|
30
|
+
* The locale to judge fallback in. Optional (EC-479): the ambient studio locale
|
|
31
|
+
* is `null` until a project's locale config is installed, and the content engine
|
|
32
|
+
* then resolves in the session's own default — so a caller that has no locale
|
|
33
|
+
* omits it rather than substituting one.
|
|
34
|
+
*/
|
|
35
|
+
locale?: Locale): BoundValueFallback | null;
|
|
@@ -16,7 +16,14 @@ function pathOfToken(token) {
|
|
|
16
16
|
* `null` when the binding is not CMS-shaped, points at no field, or the value
|
|
17
17
|
* is a real translation.
|
|
18
18
|
*/
|
|
19
|
-
export function cmsBindingFallback(binding, source,
|
|
19
|
+
export function cmsBindingFallback(binding, source,
|
|
20
|
+
/**
|
|
21
|
+
* The locale to judge fallback in. Optional (EC-479): the ambient studio locale
|
|
22
|
+
* is `null` until a project's locale config is installed, and the content engine
|
|
23
|
+
* then resolves in the session's own default — so a caller that has no locale
|
|
24
|
+
* omits it rather than substituting one.
|
|
25
|
+
*/
|
|
26
|
+
locale) {
|
|
20
27
|
if (source === undefined || !isCmsSource(source))
|
|
21
28
|
return null;
|
|
22
29
|
if (binding.mode !== 'value' && binding.mode !== 'repeaterItem')
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"binding-fallback.js","sourceRoot":"","sources":["../../../src/lib/locale/binding-fallback.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,iBAAiB,EAAE,MAAM,8BAA8B,CAAA;AAEhE,OAAO,EAAE,qBAAqB,EAAE,sBAAsB,EAAE,MAAM,4BAA4B,CAAA;AAsB1F,SAAS,WAAW,CAAC,MAAkB;IACrC,OAAO,MAAM,CAAC,IAAI,KAAK,KAAK,CAAA;AAC9B,CAAC;AAED,4EAA4E;AAC5E,SAAS,WAAW,CAAC,KAAa;IAChC,OAAO,KAAK;SACT,KAAK,CAAC,GAAG,CAAC;SACV,MAAM,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC;SACvC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,UAAU,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,UAAU,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,CAAA;AAC1E,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CAChC,OAAyB,EACzB,MAA8B,
|
|
1
|
+
{"version":3,"file":"binding-fallback.js","sourceRoot":"","sources":["../../../src/lib/locale/binding-fallback.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Locale } from '@elytracms/core/cms-core'\nimport type { BindingReference } from '@elytracms/core/project-graph'\nimport type { CmsSource, DataSource } from '@elytracms/core/data-binding'\nimport { listQueryOfConfig } from '../bindings/section-bindings'\nimport type { QueryCmsConfig } from '../bindings/section-bindings'\nimport { getCollectionDocument, runCollectionListQuery } from '../data/collection-content'\nimport type { DisplayedFallback } from './fallback'\n\n/**\n * Fallback visibility for BOUND content (EC-176): given a stored content\n * binding and its CMS source, decide whether the value the canvas currently\n * previews for the active locale is served via EC-018 locale fallback. Used\n * by the inspector's Content tab — the surface that explains what a bound\n * field shows (the canvas overlay itself is out of this wave's reach; see\n * EC-176 notes).\n *\n * Deterministic over the playground delivery engine: the same\n * `@elytracms/core/content` resolution that produced the previewed value reports\n * which fields fell back (`ResolutionInfo.localeFallbacks`), so the label can\n * never disagree with the displayed value.\n */\n\n/** A previewed bound value that fell back, plus which field it came from. */\nexport interface BoundValueFallback extends DisplayedFallback {\n field: string\n}\n\nfunction isCmsSource(source: DataSource): source is CmsSource {\n return source.kind === 'cms'\n}\n\n/** JSON-pointer-ish token → path segments ('/0/title' → ['0', 'title']). */\nfunction pathOfToken(token: string): string[] {\n return token\n .split('/')\n .filter((segment) => segment.length > 0)\n .map((segment) => segment.replaceAll('~1', '/').replaceAll('~0', '~'))\n}\n\n/**\n * Whether the bound field's previewed value (first entry for list/repeater\n * bindings, the bound document otherwise) is a locale fallback in `locale`.\n * `null` when the binding is not CMS-shaped, points at no field, or the value\n * is a real translation.\n */\nexport function cmsBindingFallback(\n binding: BindingReference,\n source: DataSource | undefined,\n /**\n * The locale to judge fallback in. Optional (EC-479): the ambient studio locale\n * is `null` until a project's locale config is installed, and the content engine\n * then resolves in the session's own default — so a caller that has no locale\n * omits it rather than substituting one.\n */\n locale?: Locale,\n): BoundValueFallback | null {\n if (source === undefined || !isCmsSource(source)) return null\n if (binding.mode !== 'value' && binding.mode !== 'repeaterItem') return null\n\n const config = source.config as QueryCmsConfig\n let path = pathOfToken(binding.token)\n if (path.length === 0) return null\n\n // List-shaped value bindings carry a leading entry index; repeater-item\n // bindings preview the first entry (the same convention as ContentPreview).\n let entryIndex = 0\n const listShaped = config.documentId === undefined\n if (binding.mode === 'value' && listShaped) {\n const head = path[0]!\n if (!/^\\d+$/.test(head)) return null\n entryIndex = Number(head)\n path = path.slice(1)\n }\n const field = path[0]\n if (field === undefined) return null\n\n // Resolve the previewed document's id, then ask the delivery engine which\n // fields fell back for it in the active locale.\n let documentId = config.documentId\n if (documentId === undefined) {\n const list = runCollectionListQuery(config.collectionId, listQueryOfConfig(config), locale)\n if (!list.ok) return null\n const entry = list.documents[entryIndex]\n if (entry === undefined) return null\n documentId = entry.id\n }\n\n const { info } = getCollectionDocument(config.collectionId, documentId, locale)\n if (info === null) return null\n const fellBack = info.localeFallbacks.find((entry) => entry.path[0] === field)\n if (fellBack === undefined) return null\n return {\n field,\n sourceLocale: fellBack.sourceLocale,\n requestedLocale: fellBack.requestedLocale,\n }\n}\n"]}
|
|
@@ -11,20 +11,49 @@ import type { Locale, LocaleConfig } from '@elytracms/core/cms-core';
|
|
|
11
11
|
*
|
|
12
12
|
* Deliberately NOT part of `lib/data/data-store` — the locale is studio
|
|
13
13
|
* state, not a data source.
|
|
14
|
+
*
|
|
15
|
+
* EC-479/EC-483 — **before a project's locale config is installed, this store
|
|
16
|
+
* holds `null`, not a locale.** It used to open on the engine's `en/de`
|
|
17
|
+
* `sampleLocaleConfig` fixture, which is the worst fallback in the family: the
|
|
18
|
+
* default locale is the literal STORAGE KEY for every localized field value
|
|
19
|
+
* (`documents.ts:88`), so a fabricated one writes a German project's content
|
|
20
|
+
* under `en` and correcting it later is a data migration, not a config edit. The
|
|
21
|
+
* locale is a deliberate choice at project creation (EC-483) and reaches the
|
|
22
|
+
* studio through {@link configureStudioLocales} from the project's own config —
|
|
23
|
+
* so "not loaded" is an honest state that surfaces, never an invented one that
|
|
24
|
+
* silently becomes a key.
|
|
25
|
+
*/
|
|
26
|
+
/**
|
|
27
|
+
* `{ config, active }` once a project's locales are installed; `{ null, null }`
|
|
28
|
+
* before that. The two fields move together by construction — there is no state
|
|
29
|
+
* with a config but no active locale, or an active locale nobody declared.
|
|
14
30
|
*/
|
|
15
|
-
|
|
31
|
+
export type StudioLocaleState = {
|
|
16
32
|
readonly config: LocaleConfig;
|
|
17
33
|
readonly active: Locale;
|
|
18
|
-
}
|
|
34
|
+
} | {
|
|
35
|
+
readonly config: null;
|
|
36
|
+
readonly active: null;
|
|
37
|
+
};
|
|
19
38
|
/** Snapshot of `{ config, active }` — stable identity until the next change. */
|
|
20
39
|
export declare function getStudioLocaleState(): StudioLocaleState;
|
|
21
|
-
/**
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
40
|
+
/**
|
|
41
|
+
* The active studio locale every surface renders in — `null` until a project's
|
|
42
|
+
* locale config is installed. A caller that genuinely needs a locale reads the
|
|
43
|
+
* PROJECT's own default (`CmsStore.localeConfig.default`, the content session's
|
|
44
|
+
* config); it must never substitute one of its own.
|
|
45
|
+
*/
|
|
46
|
+
export declare function getStudioLocale(): Locale | null;
|
|
47
|
+
/**
|
|
48
|
+
* The project locale config driving the switcher options — `null` until one is
|
|
49
|
+
* installed (the studio has no locales of its own to offer).
|
|
50
|
+
*/
|
|
51
|
+
export declare function getStudioLocaleConfig(): LocaleConfig | null;
|
|
25
52
|
/**
|
|
26
53
|
* Switch the ambient locale. Unknown locales are ignored (the switcher only
|
|
27
|
-
* offers configured ones; programmatic callers get a no-op, never a crash)
|
|
54
|
+
* offers configured ones; programmatic callers get a no-op, never a crash), and
|
|
55
|
+
* so is any switch attempted before a config is installed — there is nothing to
|
|
56
|
+
* validate the locale against.
|
|
28
57
|
*/
|
|
29
58
|
export declare function setStudioLocale(locale: Locale): void;
|
|
30
59
|
/**
|
|
@@ -35,6 +64,9 @@ export declare function setStudioLocale(locale: Locale): void;
|
|
|
35
64
|
export declare function configureStudioLocales(config: LocaleConfig): void;
|
|
36
65
|
/** Subscribe to locale/config changes. Returns the unsubscribe function. */
|
|
37
66
|
export declare function subscribeStudioLocale(listener: () => void): () => void;
|
|
38
|
-
/**
|
|
67
|
+
/**
|
|
68
|
+
* Reset to the pre-config "not loaded" state (tests only — module state is
|
|
69
|
+
* shared). Never re-seeds a fixture config: a test that queries localized content
|
|
70
|
+
* must say which locale it means, exactly like a project does.
|
|
71
|
+
*/
|
|
39
72
|
export declare function resetStudioLocale(): void;
|
|
40
|
-
export {};
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { createLocaleConfig, isKnownLocale
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
};
|
|
1
|
+
import { createLocaleConfig, isKnownLocale } from '@elytracms/core/cms-core';
|
|
2
|
+
/** The pre-config state: no project locales have been installed yet. */
|
|
3
|
+
const NOT_LOADED = { config: null, active: null };
|
|
4
|
+
let state = NOT_LOADED;
|
|
6
5
|
const listeners = new Set();
|
|
7
6
|
function emit() {
|
|
8
7
|
for (const listener of listeners)
|
|
@@ -12,22 +11,34 @@ function emit() {
|
|
|
12
11
|
export function getStudioLocaleState() {
|
|
13
12
|
return state;
|
|
14
13
|
}
|
|
15
|
-
/**
|
|
14
|
+
/**
|
|
15
|
+
* The active studio locale every surface renders in — `null` until a project's
|
|
16
|
+
* locale config is installed. A caller that genuinely needs a locale reads the
|
|
17
|
+
* PROJECT's own default (`CmsStore.localeConfig.default`, the content session's
|
|
18
|
+
* config); it must never substitute one of its own.
|
|
19
|
+
*/
|
|
16
20
|
export function getStudioLocale() {
|
|
17
21
|
return state.active;
|
|
18
22
|
}
|
|
19
|
-
/**
|
|
23
|
+
/**
|
|
24
|
+
* The project locale config driving the switcher options — `null` until one is
|
|
25
|
+
* installed (the studio has no locales of its own to offer).
|
|
26
|
+
*/
|
|
20
27
|
export function getStudioLocaleConfig() {
|
|
21
28
|
return state.config;
|
|
22
29
|
}
|
|
23
30
|
/**
|
|
24
31
|
* Switch the ambient locale. Unknown locales are ignored (the switcher only
|
|
25
|
-
* offers configured ones; programmatic callers get a no-op, never a crash)
|
|
32
|
+
* offers configured ones; programmatic callers get a no-op, never a crash), and
|
|
33
|
+
* so is any switch attempted before a config is installed — there is nothing to
|
|
34
|
+
* validate the locale against.
|
|
26
35
|
*/
|
|
27
36
|
export function setStudioLocale(locale) {
|
|
37
|
+
if (state.config === null)
|
|
38
|
+
return;
|
|
28
39
|
if (locale === state.active || !isKnownLocale(state.config, locale))
|
|
29
40
|
return;
|
|
30
|
-
state = {
|
|
41
|
+
state = { config: state.config, active: locale };
|
|
31
42
|
emit();
|
|
32
43
|
}
|
|
33
44
|
/**
|
|
@@ -37,10 +48,14 @@ export function setStudioLocale(locale) {
|
|
|
37
48
|
*/
|
|
38
49
|
export function configureStudioLocales(config) {
|
|
39
50
|
const normalized = createLocaleConfig({ default: config.default, locales: config.locales });
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
normalized.
|
|
43
|
-
|
|
51
|
+
const current = state.config;
|
|
52
|
+
const sameConfig = current !== null &&
|
|
53
|
+
normalized.default === current.default &&
|
|
54
|
+
normalized.locales.length === current.locales.length &&
|
|
55
|
+
normalized.locales.every((locale, i) => locale === current.locales[i]);
|
|
56
|
+
const active = state.active !== null && isKnownLocale(normalized, state.active)
|
|
57
|
+
? state.active
|
|
58
|
+
: normalized.default;
|
|
44
59
|
if (sameConfig && active === state.active)
|
|
45
60
|
return;
|
|
46
61
|
state = { config: normalized, active };
|
|
@@ -53,9 +68,13 @@ export function subscribeStudioLocale(listener) {
|
|
|
53
68
|
listeners.delete(listener);
|
|
54
69
|
};
|
|
55
70
|
}
|
|
56
|
-
/**
|
|
71
|
+
/**
|
|
72
|
+
* Reset to the pre-config "not loaded" state (tests only — module state is
|
|
73
|
+
* shared). Never re-seeds a fixture config: a test that queries localized content
|
|
74
|
+
* must say which locale it means, exactly like a project does.
|
|
75
|
+
*/
|
|
57
76
|
export function resetStudioLocale() {
|
|
58
|
-
state =
|
|
77
|
+
state = NOT_LOADED;
|
|
59
78
|
emit();
|
|
60
79
|
}
|
|
61
80
|
//# sourceMappingURL=locale-store.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"locale-store.js","sourceRoot":"","sources":["../../../src/lib/locale/locale-store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,aAAa,EAAE,
|
|
1
|
+
{"version":3,"file":"locale-store.js","sourceRoot":"","sources":["../../../src/lib/locale/locale-store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAA;AAqC5E,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;;;;GAIG;AACH,MAAM,UAAU,sBAAsB,CAAC,MAAoB;IACzD,MAAM,UAAU,GAAG,kBAAkB,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,CAAC,CAAA;IAC3F,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 { createLocaleConfig, 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 (e.g. when a CMS session or builder\n * shell mounts with a starter kit's locales). The active locale is kept when\n * it is still configured, otherwise it snaps back to the new default.\n */\nexport function configureStudioLocales(config: LocaleConfig): void {\n const normalized = createLocaleConfig({ 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"]}
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
* builder toolbar and the CMS surface header, both driving the same
|
|
4
4
|
* studio-level active locale. Options come from the project locale config;
|
|
5
5
|
* the default locale is marked. Renders nothing for single-locale projects —
|
|
6
|
-
* no per-control locale ceremony anywhere else
|
|
6
|
+
* no per-control locale ceremony anywhere else — nor before a project's locale
|
|
7
|
+
* config has been installed (EC-479: there is nothing to switch between, and the
|
|
8
|
+
* engine no longer supplies its own `en/de` fixture to pretend otherwise).
|
|
7
9
|
*/
|
|
8
10
|
export declare function LocaleSwitcher({ className }: {
|
|
9
11
|
className?: string;
|
|
@@ -8,11 +8,13 @@ import { useStudioLocale } from './use-studio-locale.js';
|
|
|
8
8
|
* builder toolbar and the CMS surface header, both driving the same
|
|
9
9
|
* studio-level active locale. Options come from the project locale config;
|
|
10
10
|
* the default locale is marked. Renders nothing for single-locale projects —
|
|
11
|
-
* no per-control locale ceremony anywhere else
|
|
11
|
+
* no per-control locale ceremony anywhere else — nor before a project's locale
|
|
12
|
+
* config has been installed (EC-479: there is nothing to switch between, and the
|
|
13
|
+
* engine no longer supplies its own `en/de` fixture to pretend otherwise).
|
|
12
14
|
*/
|
|
13
15
|
export function LocaleSwitcher({ className }) {
|
|
14
16
|
const { locale, config, setLocale } = useStudioLocale();
|
|
15
|
-
if (config.locales.length <= 1)
|
|
17
|
+
if (config === null || config.locales.length <= 1)
|
|
16
18
|
return null;
|
|
17
19
|
return (_jsxs("div", { role: "group", "aria-label": "Active locale", "data-testid": "locale-switcher", className: cn('flex items-center gap-1 rounded-md bg-muted/50 p-0.5', className), children: [_jsx(Globe, { "aria-hidden": true, className: "ml-1 size-3.5 self-center text-muted-foreground" }), config.locales.map((option) => {
|
|
18
20
|
const isDefault = option === config.default;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"locale-switcher.js","sourceRoot":"","sources":["../../../src/lib/locale/locale-switcher.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,cAAc,CAAA;AACpC,OAAO,EAAE,EAAE,EAAE,MAAM,UAAU,CAAA;AAC7B,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAC5C,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAA;AAErD
|
|
1
|
+
{"version":3,"file":"locale-switcher.js","sourceRoot":"","sources":["../../../src/lib/locale/locale-switcher.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,cAAc,CAAA;AACpC,OAAO,EAAE,EAAE,EAAE,MAAM,UAAU,CAAA;AAC7B,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAC5C,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAA;AAErD;;;;;;;;GAQG;AACH,MAAM,UAAU,cAAc,CAAC,EAAE,SAAS,EAA0B;IAClE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,GAAG,eAAe,EAAE,CAAA;IACvD,IAAI,MAAM,KAAK,IAAI,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,IAAI,CAAC;QAAE,OAAO,IAAI,CAAA;IAE9D,OAAO,CACL,eACE,IAAI,EAAC,OAAO,gBACD,eAAe,iBACd,iBAAiB,EAC7B,SAAS,EAAE,EAAE,CAAC,sDAAsD,EAAE,SAAS,CAAC,aAEhF,KAAC,KAAK,yBAAa,SAAS,EAAC,iDAAiD,GAAG,EAChF,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE;gBAC7B,MAAM,SAAS,GAAG,MAAM,KAAK,MAAM,CAAC,OAAO,CAAA;gBAC3C,OAAO,CACL,kBAEE,IAAI,EAAC,QAAQ,kBACC,MAAM,KAAK,MAAM,EAC/B,KAAK,EAAE,SAAS,CAAC,CAAC,CAAC,GAAG,eAAe,CAAC,MAAM,CAAC,mBAAmB,CAAC,CAAC,CAAC,eAAe,CAAC,MAAM,CAAC,iBAC7E,iBAAiB,MAAM,EAAE,EACtC,OAAO,EAAE,GAAG,EAAE,CAAC,SAAS,CAAC,MAAM,CAAC,EAChC,SAAS,EAAE,EAAE,CACX,2EAA2E,EAC3E,MAAM,KAAK,MAAM;wBACf,CAAC,CAAC,yCAAyC;wBAC3C,CAAC,CAAC,6CAA6C,CAClD,aAEA,MAAM,EACN,SAAS,CAAC,CAAC,CAAC,eAAM,SAAS,EAAC,iCAAiC,uBAAS,CAAC,CAAC,CAAC,IAAI,KAdzE,MAAM,CAeJ,CACV,CAAA;YACH,CAAC,CAAC,IACE,CACP,CAAA;AACH,CAAC","sourcesContent":["import { Globe } from 'lucide-react'\nimport { cn } from '../utils'\nimport { localeChipLabel } from './fallback'\nimport { useStudioLocale } from './use-studio-locale'\n\n/**\n * The ambient locale switcher (EC-176): one segmented control, mounted in the\n * builder toolbar and the CMS surface header, both driving the same\n * studio-level active locale. Options come from the project locale config;\n * the default locale is marked. Renders nothing for single-locale projects —\n * no per-control locale ceremony anywhere else — nor before a project's locale\n * config has been installed (EC-479: there is nothing to switch between, and the\n * engine no longer supplies its own `en/de` fixture to pretend otherwise).\n */\nexport function LocaleSwitcher({ className }: { className?: string }) {\n const { locale, config, setLocale } = useStudioLocale()\n if (config === null || config.locales.length <= 1) return null\n\n return (\n <div\n role=\"group\"\n aria-label=\"Active locale\"\n data-testid=\"locale-switcher\"\n className={cn('flex items-center gap-1 rounded-md bg-muted/50 p-0.5', className)}\n >\n <Globe aria-hidden className=\"ml-1 size-3.5 self-center text-muted-foreground\" />\n {config.locales.map((option) => {\n const isDefault = option === config.default\n return (\n <button\n key={option}\n type=\"button\"\n aria-pressed={option === locale}\n title={isDefault ? `${localeChipLabel(option)} (default locale)` : localeChipLabel(option)}\n data-testid={`locale-option-${option}`}\n onClick={() => setLocale(option)}\n className={cn(\n 'rounded-[5px] px-2 py-0.5 text-xs font-medium uppercase transition-colors',\n option === locale\n ? 'bg-background text-foreground shadow-sm'\n : 'text-muted-foreground hover:text-foreground',\n )}\n >\n {option}\n {isDefault ? <span className=\"ml-0.5 text-muted-foreground/70\">·</span> : null}\n </button>\n )\n })}\n </div>\n )\n}\n"]}
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import type { Locale, LocaleConfig } from '@elytracms/core/cms-core';
|
|
2
2
|
export interface StudioLocale {
|
|
3
|
-
/**
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
/**
|
|
4
|
+
* The ambient active locale every studio surface renders in — `null` before a
|
|
5
|
+
* project's locale config is installed (EC-479: the engine no longer opens on
|
|
6
|
+
* its own `en/de` fixture). A surface that must render in *some* locale reads
|
|
7
|
+
* the project's own default; it never invents one.
|
|
8
|
+
*/
|
|
9
|
+
locale: Locale | null;
|
|
10
|
+
/** The project locale config driving the switcher options — `null` until installed. */
|
|
11
|
+
config: LocaleConfig | null;
|
|
7
12
|
setLocale: (locale: Locale) => void;
|
|
8
13
|
}
|
|
9
14
|
/** Subscribe a React surface to the studio's ambient locale (EC-176). */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-studio-locale.js","sourceRoot":"","sources":["../../../src/lib/locale/use-studio-locale.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,OAAO,CAAA;AAE5C,OAAO,EACL,oBAAoB,EACpB,eAAe,EACf,qBAAqB,GACtB,MAAM,gBAAgB,CAAA;
|
|
1
|
+
{"version":3,"file":"use-studio-locale.js","sourceRoot":"","sources":["../../../src/lib/locale/use-studio-locale.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,OAAO,CAAA;AAE5C,OAAO,EACL,oBAAoB,EACpB,eAAe,EACf,qBAAqB,GACtB,MAAM,gBAAgB,CAAA;AAevB,yEAAyE;AACzE,MAAM,UAAU,eAAe;IAC7B,MAAM,KAAK,GAAG,oBAAoB,CAChC,qBAAqB,EACrB,oBAAoB,EACpB,oBAAoB,CACrB,CAAA;IACD,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,SAAS,EAAE,eAAe,EAAE,CAAA;AACnF,CAAC","sourcesContent":["import { useSyncExternalStore } from 'react'\nimport type { Locale, LocaleConfig } from '@elytracms/core/cms-core'\nimport {\n getStudioLocaleState,\n setStudioLocale,\n subscribeStudioLocale,\n} from './locale-store'\n\nexport interface StudioLocale {\n /**\n * The ambient active locale every studio surface renders in — `null` before a\n * project's locale config is installed (EC-479: the engine no longer opens on\n * its own `en/de` fixture). A surface that must render in *some* locale reads\n * the project's own default; it never invents one.\n */\n locale: Locale | null\n /** The project locale config driving the switcher options — `null` until installed. */\n config: LocaleConfig | null\n setLocale: (locale: Locale) => void\n}\n\n/** Subscribe a React surface to the studio's ambient locale (EC-176). */\nexport function useStudioLocale(): StudioLocale {\n const state = useSyncExternalStore(\n subscribeStudioLocale,\n getStudioLocaleState,\n getStudioLocaleState,\n )\n return { locale: state.active, config: state.config, setLocale: setStudioLocale }\n}\n"]}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { activeDeploymentKey, selectPersistenceAdapter } from '../persistence/index.js';
|
|
2
2
|
import { attachSessionExpiryReporting } from '../session-expiry.js';
|
|
3
3
|
import { attachConfiguredRevalidationWebhooks } from '../webhooks/index.js';
|
|
4
|
+
import { sessionValidateDocument } from '../cms/document-gate.js';
|
|
4
5
|
import { attachReferenceIndex, createChangesets, createOperations, createRoleAuthorization, } from './operations-package.js';
|
|
5
6
|
/**
|
|
6
7
|
* A no-op {@link ReferenceIndexAttachment} for self-maintaining adapters (EC-277):
|
|
@@ -15,7 +16,17 @@ export function createStudioOperations(adapter, options = {}) {
|
|
|
15
16
|
const authorized = options.actor
|
|
16
17
|
? { actor: options.actor, authorize: createRoleAuthorization(adapter.members) }
|
|
17
18
|
: {};
|
|
18
|
-
|
|
19
|
+
// EC-497: the publish gate validates with the CMS session's canonical option
|
|
20
|
+
// bag — the same one the detail editor, bulk publish, cell edit, import and
|
|
21
|
+
// every AI path use — instead of deciding its own (weaker, mode-dependent)
|
|
22
|
+
// strength off the adapter-served schema record. The seam reads a validator the
|
|
23
|
+
// mounted session published; with no session it returns `undefined` and the gate
|
|
24
|
+
// takes its documented fallback.
|
|
25
|
+
const operations = createOperations({
|
|
26
|
+
adapter,
|
|
27
|
+
validateDocument: sessionValidateDocument,
|
|
28
|
+
...authorized,
|
|
29
|
+
});
|
|
19
30
|
// EC-277: when the adapter maintains the index in its own write path (AD-6 "at the
|
|
20
31
|
// source" — the Convex provider does this server-side, so a raw `convex run` is
|
|
21
32
|
// caught too), the client-side subscriber would only double the work. Attach it
|
|
@@ -25,7 +36,13 @@ export function createStudioOperations(adapter, options = {}) {
|
|
|
25
36
|
const references = adapter.maintainsReferenceIndex
|
|
26
37
|
? noopReferenceIndexAttachment()
|
|
27
38
|
: attachReferenceIndex(operations.events, adapter);
|
|
28
|
-
const changesets = createChangesets({
|
|
39
|
+
const changesets = createChangesets({
|
|
40
|
+
adapter,
|
|
41
|
+
events: operations.events,
|
|
42
|
+
// EC-497: a changeset-driven publish is gated identically — same seam, same bag.
|
|
43
|
+
validateDocument: sessionValidateDocument,
|
|
44
|
+
...authorized,
|
|
45
|
+
});
|
|
29
46
|
// EC-146 instant publish: when a host revalidate endpoint is configured via
|
|
30
47
|
// env, every successful publish-shaped command (document upsert/delete/
|
|
31
48
|
// setState, graph publish/unpublish) fires a signed revalidation webhook.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"studio-operations.js","sourceRoot":"","sources":["../../../src/lib/operations/studio-operations.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,mBAAmB,EAAE,wBAAwB,EAAE,MAAM,gBAAgB,CAAA;AAC9E,OAAO,EAAE,4BAA4B,EAAE,MAAM,mBAAmB,CAAA;AAChE,OAAO,EAAE,oCAAoC,EAAE,MAAM,aAAa,CAAA;AAClE,OAAO,EACL,oBAAoB,EACpB,gBAAgB,EAChB,gBAAgB,EAChB,uBAAuB,GACxB,MAAM,sBAAsB,CAAA;AAgD7B;;;;GAIG;AACH,SAAS,4BAA4B;IACnC,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,GAAE,CAAC,EAAE,KAAK,EAAE,KAAK,IAAI,EAAE,GAAE,CAAC,EAAE,CAAA;AACpD,CAAC;AAED,6EAA6E;AAC7E,MAAM,UAAU,sBAAsB,CACpC,OAA2B,EAC3B,UAAyC,EAAE;IAE3C,MAAM,UAAU,GAAG,OAAO,CAAC,KAAK;QAC9B,CAAC,CAAC,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,SAAS,EAAE,uBAAuB,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE;QAC/E,CAAC,CAAC,EAAE,CAAA;IACN,MAAM,UAAU,GAAG,gBAAgB,CAAC,EAAE,OAAO,EAAE,GAAG,UAAU,EAAE,CAAC,CAAA;IAC/D,mFAAmF;IACnF,gFAAgF;IAChF,gFAAgF;IAChF,kFAAkF;IAClF,+EAA+E;IAC/E,0EAA0E;IAC1E,MAAM,UAAU,GAAG,OAAO,CAAC,uBAAuB;QAChD,CAAC,CAAC,4BAA4B,EAAE;QAChC,CAAC,CAAC,oBAAoB,CAAC,UAAU,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACpD,MAAM,UAAU,GAAG,gBAAgB,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,MAAM,EAAE,GAAG,UAAU,EAAE,CAAC,CAAA;IAC1F,4EAA4E;IAC5E,wEAAwE;IACxE,0EAA0E;IAC1E,8BAA8B;IAC9B,oCAAoC,CAAC,UAAU,CAAC,MAAM,CAAC,CAAA;IACvD,+EAA+E;IAC/E,2EAA2E;IAC3E,+EAA+E;IAC/E,6EAA6E;IAC7E,mCAAmC;IACnC,4BAA4B,CAAC,UAAU,CAAC,MAAM,CAAC,CAAA;IAC/C,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,OAAO,EAAE,UAAU,EAAE,CAAA;AACxD,CAAC;AAED,gFAAgF;AAChF,gFAAgF;AAChF,2EAA2E;AAC3E,MAAM,aAAa,GAAG,IAAI,GAAG,EAAuC,CAAA;AACpE,MAAM,cAAc,GAAG,IAAI,GAAG,EAAqC,CAAA;AACnE,IAAI,YAAY,GAAqB,IAAI,CAAA;AAEzC;;;;;GAKG;AACH,SAAS,aAAa;IACpB,IAAI,CAAC;QACH,OAAO,mBAAmB,EAAE,CAAA;IAC9B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,kBAAkB,CAAA;IAC3B,CAAC;AACH,CAAC;AAED,SAAS,UAAU,CAAC,GAAW;IAC7B,IAAI,OAAO,GAAG,aAAa,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;IACpC,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,OAAO,GAAG,wBAAwB,EAAE,CAAA;QACpC,aAAa,CAAC,GAAG,CAAC,GAAG,EAAE,OAAO,CAAC,CAAA;IACjC,CAAC;IACD,OAAO,OAAO,CAAA;AAChB,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,mBAAmB;IACjC,MAAM,GAAG,GAAG,aAAa,EAAE,CAAA;IAC3B,IAAI,QAAQ,GAAG,cAAc,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;IACtC,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,MAAM,KAAK,GAAG,YAAY,CAAA;QAC1B,QAAQ,GAAG,UAAU,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CAC1C,sBAAsB,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CACxD,CAAA;QACD,cAAc,CAAC,GAAG,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAA;IACnC,CAAC;IACD,OAAO,QAAQ,CAAA;AACjB,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,gBAAgB,CAAC,KAAuB;IACtD,IAAI,YAAY,EAAE,EAAE,KAAK,KAAK,EAAE,EAAE;QAAE,OAAM,CAAC,8CAA8C;IACzF,YAAY,GAAG,KAAK,CAAA;IACpB,cAAc,CAAC,KAAK,EAAE,CAAA;AACxB,CAAC;AAED,0EAA0E;AAC1E,MAAM,UAAU,2BAA2B,CAAC,IAA6B;IACvE,IAAI,IAAI,EAAE,CAAC;QACT,cAAc,CAAC,GAAG,CAAC,aAAa,EAAE,EAAE,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAA;QAC1D,OAAM;IACR,CAAC;IACD,cAAc,CAAC,KAAK,EAAE,CAAA;IACtB,aAAa,CAAC,KAAK,EAAE,CAAA;IACrB,YAAY,GAAG,IAAI,CAAA;AACrB,CAAC","sourcesContent":["import type { PersistenceAdapter } from '@elytracms/core/persistence'\nimport { activeDeploymentKey, selectPersistenceAdapter } from '../persistence'\nimport { attachSessionExpiryReporting } from '../session-expiry'\nimport { attachConfiguredRevalidationWebhooks } from '../webhooks'\nimport {\n attachReferenceIndex,\n createChangesets,\n createOperations,\n createRoleAuthorization,\n} from './operations-package'\nimport type {\n ChangesetOperations,\n OperationsClient,\n Principal,\n ReferenceIndexAttachment,\n} from './operations-package'\n\n/**\n * The studio's single operations bootstrap (EC-141, vision AD-7).\n *\n * One `OperationsClient` + one `ChangesetOperations` instance, built over the\n * selected persistence adapter, shared by every store and surface in the app.\n * The UI never talks to a repository for writes — every mutation is a typed\n * operation command (validate → authorize → mutate → event), so the studio\n * dogfoods the same seam plugins, AI changesets, and the CLI use.\n *\n * The EC-155 reference index is attached to the shared event bus here, so any\n * document/graph/publishing command issued anywhere in the studio keeps the\n * index maintained.\n */\nexport interface StudioOperations {\n readonly operations: OperationsClient\n readonly changesets: ChangesetOperations\n /**\n * The raw adapter, exposed for *transport and read paths only*: blob byte\n * upload/download (`adapter.blobs`) and existing read stores. Studio code\n * must not call repository write methods on it — writes go through\n * `operations`.\n */\n readonly adapter: PersistenceAdapter\n /** EC-155 maintained-on-write index attachment (flushable in tests). */\n readonly references: ReferenceIndexAttachment\n}\n\nexport interface CreateStudioOperationsOptions {\n /**\n * The signed-in principal (EC-150). When present, the EC-150 role hook\n * (`createRoleAuthorization` over `adapter.members`) becomes the\n * authorization chokepoint for every command and query — viewers get\n * structured denials on mutations, editors on administration. Without an\n * actor the client runs pass-through (seeds, fixtures, tests that exercise\n * domain logic rather than permissions); the route guard keeps\n * unauthenticated users away from every operation surface.\n */\n actor?: Principal\n}\n\n/**\n * A no-op {@link ReferenceIndexAttachment} for self-maintaining adapters (EC-277):\n * nothing is subscribed, so `detach` and `flush` are trivially satisfied (the index\n * is kept current in the adapter's write path, not by a queued subscriber).\n */\nfunction noopReferenceIndexAttachment(): ReferenceIndexAttachment {\n return { detach: () => {}, flush: async () => {} }\n}\n\n/** Build a studio operations bundle over an adapter (exported for tests). */\nexport function createStudioOperations(\n adapter: PersistenceAdapter,\n options: CreateStudioOperationsOptions = {},\n): StudioOperations {\n const authorized = options.actor\n ? { actor: options.actor, authorize: createRoleAuthorization(adapter.members) }\n : {}\n const operations = createOperations({ adapter, ...authorized })\n // EC-277: when the adapter maintains the index in its own write path (AD-6 \"at the\n // source\" — the Convex provider does this server-side, so a raw `convex run` is\n // caught too), the client-side subscriber would only double the work. Attach it\n // ONLY for providers that don't self-maintain. Where it does run, extraction is a\n // schema-free, manifest-free walk over self-describing content, so it needs no\n // registry (compositions self-typed their relation/asset props on write).\n const references = adapter.maintainsReferenceIndex\n ? noopReferenceIndexAttachment()\n : attachReferenceIndex(operations.events, adapter)\n const changesets = createChangesets({ adapter, events: operations.events, ...authorized })\n // EC-146 instant publish: when a host revalidate endpoint is configured via\n // env, every successful publish-shaped command (document upsert/delete/\n // setState, graph publish/unpublish) fires a signed revalidation webhook.\n // No env (dev/tests) → no-op.\n attachConfiguredRevalidationWebhooks(operations.events)\n // EC-403: session-expiry detection rides the SAME lifecycle bus — every failed\n // command/query/changeset operation is checked against the session-expired\n // predicate here, so expiry raises the ONE app-level blocking re-auth state no\n // matter which surface issued the operation (per-surface banners can't cover\n // surfaces that render no banner).\n attachSessionExpiryReporting(operations.events)\n return { operations, changesets, adapter, references }\n}\n\n// One adapter + one operations bundle per backend (EC-179): keyed by the active\n// deployment so switching to a project on another Convex deployment selects (or\n// lazily builds) that backend's client, rather than reusing the first one.\nconst adaptersByKey = new Map<string, Promise<PersistenceAdapter>>()\nconst instancesByKey = new Map<string, Promise<StudioOperations>>()\nlet currentActor: Principal | null = null\n\n/**\n * The active backend key, tolerant of an unconfigured env: the real app never\n * reaches `getStudioOperations` while misconfigured (the `DeploymentSetupGate` renders a\n * boot error first), but tests inject a bundle directly via the seam below — so\n * a missing backend resolves to one stable key rather than throwing.\n */\nfunction deploymentKey(): string {\n try {\n return activeDeploymentKey()\n } catch {\n return '__unconfigured__'\n }\n}\n\nfunction getAdapter(key: string): Promise<PersistenceAdapter> {\n let adapter = adaptersByKey.get(key)\n if (!adapter) {\n adapter = selectPersistenceAdapter()\n adaptersByKey.set(key, adapter)\n }\n return adapter\n}\n\n/**\n * The shared studio operations client for the active backend (lazily\n * bootstrapped, cached per deployment). In-memory mode this owns the one seeded\n * adapter the whole session mutates; against Convex it owns one client per\n * deployment — the active project (EC-179) selects which.\n */\nexport function getStudioOperations(): Promise<StudioOperations> {\n const key = deploymentKey()\n let instance = instancesByKey.get(key)\n if (!instance) {\n const actor = currentActor\n instance = getAdapter(key).then((adapter) =>\n createStudioOperations(adapter, actor ? { actor } : {}),\n )\n instancesByKey.set(key, instance)\n }\n return instance\n}\n\n/**\n * Bind (or clear) the session principal (EC-150). Called by the auth context\n * on sign-in/sign-out. Rebuilds the operations clients over the *same* adapters\n * — the persisted state (and, in-memory, the whole session's edits) survives;\n * only the authorization identity changes. All cached backends are rebuilt so a\n * later switch picks up the new actor too.\n */\nexport function setStudioSession(actor: Principal | null): void {\n if (currentActor?.id === actor?.id) return // same user (or both null) — keep the clients\n currentActor = actor\n instancesByKey.clear()\n}\n\n/** Test seam: replace (or clear) the singleton for the active backend. */\nexport function setStudioOperationsForTests(next: StudioOperations | null): void {\n if (next) {\n instancesByKey.set(deploymentKey(), Promise.resolve(next))\n return\n }\n instancesByKey.clear()\n adaptersByKey.clear()\n currentActor = null\n}\n"]}
|
|
1
|
+
{"version":3,"file":"studio-operations.js","sourceRoot":"","sources":["../../../src/lib/operations/studio-operations.ts"],"names":[],"mappings":"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","sourcesContent":["import type { PersistenceAdapter } from '@elytracms/core/persistence'\nimport { activeDeploymentKey, selectPersistenceAdapter } from '../persistence'\nimport { attachSessionExpiryReporting } from '../session-expiry'\nimport { attachConfiguredRevalidationWebhooks } from '../webhooks'\nimport { sessionValidateDocument } from '../cms/document-gate'\nimport {\n attachReferenceIndex,\n createChangesets,\n createOperations,\n createRoleAuthorization,\n} from './operations-package'\nimport type {\n ChangesetOperations,\n OperationsClient,\n Principal,\n ReferenceIndexAttachment,\n} from './operations-package'\n\n/**\n * The studio's single operations bootstrap (EC-141, vision AD-7).\n *\n * One `OperationsClient` + one `ChangesetOperations` instance, built over the\n * selected persistence adapter, shared by every store and surface in the app.\n * The UI never talks to a repository for writes — every mutation is a typed\n * operation command (validate → authorize → mutate → event), so the studio\n * dogfoods the same seam plugins, AI changesets, and the CLI use.\n *\n * The EC-155 reference index is attached to the shared event bus here, so any\n * document/graph/publishing command issued anywhere in the studio keeps the\n * index maintained.\n */\nexport interface StudioOperations {\n readonly operations: OperationsClient\n readonly changesets: ChangesetOperations\n /**\n * The raw adapter, exposed for *transport and read paths only*: blob byte\n * upload/download (`adapter.blobs`) and existing read stores. Studio code\n * must not call repository write methods on it — writes go through\n * `operations`.\n */\n readonly adapter: PersistenceAdapter\n /** EC-155 maintained-on-write index attachment (flushable in tests). */\n readonly references: ReferenceIndexAttachment\n}\n\nexport interface CreateStudioOperationsOptions {\n /**\n * The signed-in principal (EC-150). When present, the EC-150 role hook\n * (`createRoleAuthorization` over `adapter.members`) becomes the\n * authorization chokepoint for every command and query — viewers get\n * structured denials on mutations, editors on administration. Without an\n * actor the client runs pass-through (seeds, fixtures, tests that exercise\n * domain logic rather than permissions); the route guard keeps\n * unauthenticated users away from every operation surface.\n */\n actor?: Principal\n}\n\n/**\n * A no-op {@link ReferenceIndexAttachment} for self-maintaining adapters (EC-277):\n * nothing is subscribed, so `detach` and `flush` are trivially satisfied (the index\n * is kept current in the adapter's write path, not by a queued subscriber).\n */\nfunction noopReferenceIndexAttachment(): ReferenceIndexAttachment {\n return { detach: () => {}, flush: async () => {} }\n}\n\n/** Build a studio operations bundle over an adapter (exported for tests). */\nexport function createStudioOperations(\n adapter: PersistenceAdapter,\n options: CreateStudioOperationsOptions = {},\n): StudioOperations {\n const authorized = options.actor\n ? { actor: options.actor, authorize: createRoleAuthorization(adapter.members) }\n : {}\n // EC-497: the publish gate validates with the CMS session's canonical option\n // bag — the same one the detail editor, bulk publish, cell edit, import and\n // every AI path use — instead of deciding its own (weaker, mode-dependent)\n // strength off the adapter-served schema record. The seam reads a validator the\n // mounted session published; with no session it returns `undefined` and the gate\n // takes its documented fallback.\n const operations = createOperations({\n adapter,\n validateDocument: sessionValidateDocument,\n ...authorized,\n })\n // EC-277: when the adapter maintains the index in its own write path (AD-6 \"at the\n // source\" — the Convex provider does this server-side, so a raw `convex run` is\n // caught too), the client-side subscriber would only double the work. Attach it\n // ONLY for providers that don't self-maintain. Where it does run, extraction is a\n // schema-free, manifest-free walk over self-describing content, so it needs no\n // registry (compositions self-typed their relation/asset props on write).\n const references = adapter.maintainsReferenceIndex\n ? noopReferenceIndexAttachment()\n : attachReferenceIndex(operations.events, adapter)\n const changesets = createChangesets({\n adapter,\n events: operations.events,\n // EC-497: a changeset-driven publish is gated identically — same seam, same bag.\n validateDocument: sessionValidateDocument,\n ...authorized,\n })\n // EC-146 instant publish: when a host revalidate endpoint is configured via\n // env, every successful publish-shaped command (document upsert/delete/\n // setState, graph publish/unpublish) fires a signed revalidation webhook.\n // No env (dev/tests) → no-op.\n attachConfiguredRevalidationWebhooks(operations.events)\n // EC-403: session-expiry detection rides the SAME lifecycle bus — every failed\n // command/query/changeset operation is checked against the session-expired\n // predicate here, so expiry raises the ONE app-level blocking re-auth state no\n // matter which surface issued the operation (per-surface banners can't cover\n // surfaces that render no banner).\n attachSessionExpiryReporting(operations.events)\n return { operations, changesets, adapter, references }\n}\n\n// One adapter + one operations bundle per backend (EC-179): keyed by the active\n// deployment so switching to a project on another Convex deployment selects (or\n// lazily builds) that backend's client, rather than reusing the first one.\nconst adaptersByKey = new Map<string, Promise<PersistenceAdapter>>()\nconst instancesByKey = new Map<string, Promise<StudioOperations>>()\nlet currentActor: Principal | null = null\n\n/**\n * The active backend key, tolerant of an unconfigured env: the real app never\n * reaches `getStudioOperations` while misconfigured (the `DeploymentSetupGate` renders a\n * boot error first), but tests inject a bundle directly via the seam below — so\n * a missing backend resolves to one stable key rather than throwing.\n */\nfunction deploymentKey(): string {\n try {\n return activeDeploymentKey()\n } catch {\n return '__unconfigured__'\n }\n}\n\nfunction getAdapter(key: string): Promise<PersistenceAdapter> {\n let adapter = adaptersByKey.get(key)\n if (!adapter) {\n adapter = selectPersistenceAdapter()\n adaptersByKey.set(key, adapter)\n }\n return adapter\n}\n\n/**\n * The shared studio operations client for the active backend (lazily\n * bootstrapped, cached per deployment). In-memory mode this owns the one seeded\n * adapter the whole session mutates; against Convex it owns one client per\n * deployment — the active project (EC-179) selects which.\n */\nexport function getStudioOperations(): Promise<StudioOperations> {\n const key = deploymentKey()\n let instance = instancesByKey.get(key)\n if (!instance) {\n const actor = currentActor\n instance = getAdapter(key).then((adapter) =>\n createStudioOperations(adapter, actor ? { actor } : {}),\n )\n instancesByKey.set(key, instance)\n }\n return instance\n}\n\n/**\n * Bind (or clear) the session principal (EC-150). Called by the auth context\n * on sign-in/sign-out. Rebuilds the operations clients over the *same* adapters\n * — the persisted state (and, in-memory, the whole session's edits) survives;\n * only the authorization identity changes. All cached backends are rebuilt so a\n * later switch picks up the new actor too.\n */\nexport function setStudioSession(actor: Principal | null): void {\n if (currentActor?.id === actor?.id) return // same user (or both null) — keep the clients\n currentActor = actor\n instancesByKey.clear()\n}\n\n/** Test seam: replace (or clear) the singleton for the active backend. */\nexport function setStudioOperationsForTests(next: StudioOperations | null): void {\n if (next) {\n instancesByKey.set(deploymentKey(), Promise.resolve(next))\n return\n }\n instancesByKey.clear()\n adaptersByKey.clear()\n currentActor = null\n}\n"]}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ConvexHttpClient } from 'convex/browser';
|
|
2
2
|
import type { CollectionDef } from '@elytracms/core/cms-core';
|
|
3
|
-
import type { AssetRepository, BackendStatus, BackendValidationInput, BackendValidationResult, BlobStorageAdapter,
|
|
3
|
+
import type { AssetRepository, BackendStatus, BackendValidationInput, BackendValidationResult, BlobStorageAdapter, CmsRepository, DeliveryRepository, MembersRepository, PersistenceAdapter, ReferenceIndexRepository, RedirectRepository, UserRepository, InviteRepository } from '@elytracms/core/persistence';
|
|
4
4
|
/**
|
|
5
5
|
* Convex-backed {@link PersistenceAdapter} (brief §10, EC-039). Every repository
|
|
6
6
|
* is implemented against the Convex functions in `apps/builder/convex/*`, mapping
|
|
@@ -19,8 +19,6 @@ export declare class ConvexPersistenceAdapter implements PersistenceAdapter {
|
|
|
19
19
|
readonly users: UserRepository;
|
|
20
20
|
/** Editorial redirects rail — data-backed 301/302 forwards (EC-303). */
|
|
21
21
|
readonly redirects: RedirectRepository;
|
|
22
|
-
/** Revocable CLI sync tokens, stored hashed (EC-150). */
|
|
23
|
-
readonly cliTokens: CliTokenRepository;
|
|
24
22
|
/** Convex file storage behind the EC-133 blob boundary (EC-151). */
|
|
25
23
|
readonly blobs: BlobStorageAdapter;
|
|
26
24
|
/** Delivery-pipeline (instant-publish) health, read-only (EC-268/EC-329). */
|
|
@@ -612,58 +612,6 @@ class ConvexInviteRepository {
|
|
|
612
612
|
}
|
|
613
613
|
}
|
|
614
614
|
}
|
|
615
|
-
function cliTokenRecordFromDoc(doc) {
|
|
616
|
-
return {
|
|
617
|
-
id: doc.tokenId,
|
|
618
|
-
label: doc.label,
|
|
619
|
-
tokenHash: doc.tokenHash,
|
|
620
|
-
createdAt: doc.createdAt,
|
|
621
|
-
...(doc.revokedAt ? { revokedAt: doc.revokedAt } : {}),
|
|
622
|
-
};
|
|
623
|
-
}
|
|
624
|
-
class ConvexCliTokenRepository {
|
|
625
|
-
client;
|
|
626
|
-
clock;
|
|
627
|
-
constructor(client, clock = realClock) {
|
|
628
|
-
this.client = client;
|
|
629
|
-
this.clock = clock;
|
|
630
|
-
}
|
|
631
|
-
async createToken(input) {
|
|
632
|
-
const doc = (await this.client.mutation(mutationRef('cliTokens:create'), {
|
|
633
|
-
tokenId: newId('tok'),
|
|
634
|
-
label: input.label,
|
|
635
|
-
tokenHash: input.tokenHash,
|
|
636
|
-
createdAt: this.clock(),
|
|
637
|
-
}));
|
|
638
|
-
return cliTokenRecordFromDoc(doc);
|
|
639
|
-
}
|
|
640
|
-
async listTokens() {
|
|
641
|
-
const docs = (await this.client.query(queryRef('cliTokens:list'), {}));
|
|
642
|
-
return docs.map(cliTokenRecordFromDoc);
|
|
643
|
-
}
|
|
644
|
-
async revokeToken(tokenId) {
|
|
645
|
-
try {
|
|
646
|
-
const doc = (await this.client.mutation(mutationRef('cliTokens:revoke'), {
|
|
647
|
-
tokenId,
|
|
648
|
-
revokedAt: this.clock(),
|
|
649
|
-
}));
|
|
650
|
-
return cliTokenRecordFromDoc(doc);
|
|
651
|
-
}
|
|
652
|
-
catch (error) {
|
|
653
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
654
|
-
if (/not-found/i.test(message)) {
|
|
655
|
-
throw new RecordNotFoundError('cli-token', tokenId);
|
|
656
|
-
}
|
|
657
|
-
throw error;
|
|
658
|
-
}
|
|
659
|
-
}
|
|
660
|
-
async findByHash(tokenHash) {
|
|
661
|
-
const doc = (await this.client.query(queryRef('cliTokens:byHash'), {
|
|
662
|
-
tokenHash,
|
|
663
|
-
}));
|
|
664
|
-
return doc ? cliTokenRecordFromDoc(doc) : undefined;
|
|
665
|
-
}
|
|
666
|
-
}
|
|
667
615
|
function redirectEntryFromDoc(doc, projectId) {
|
|
668
616
|
return {
|
|
669
617
|
id: doc.redirectId,
|
|
@@ -764,8 +712,6 @@ export class ConvexPersistenceAdapter {
|
|
|
764
712
|
users;
|
|
765
713
|
/** Editorial redirects rail — data-backed 301/302 forwards (EC-303). */
|
|
766
714
|
redirects;
|
|
767
|
-
/** Revocable CLI sync tokens, stored hashed (EC-150). */
|
|
768
|
-
cliTokens;
|
|
769
715
|
/** Convex file storage behind the EC-133 blob boundary (EC-151). */
|
|
770
716
|
blobs;
|
|
771
717
|
/** Delivery-pipeline (instant-publish) health, read-only (EC-268/EC-329). */
|
|
@@ -794,7 +740,6 @@ export class ConvexPersistenceAdapter {
|
|
|
794
740
|
this.users = new ConvexUserRepository(client);
|
|
795
741
|
this.invites = new ConvexInviteRepository(client);
|
|
796
742
|
this.redirects = new ConvexRedirectRepository(client);
|
|
797
|
-
this.cliTokens = new ConvexCliTokenRepository(client);
|
|
798
743
|
this.blobs = new ConvexBlobStorage(client);
|
|
799
744
|
this.delivery = new ConvexDeliveryRepository(client);
|
|
800
745
|
}
|