@elytracms/studio 0.0.87 → 0.0.90
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 +35 -42
- package/convex/delivery.ts +8 -11
- package/convex/document-revalidate.test.ts +175 -0
- package/convex/redirect-revalidate.test.ts +36 -36
- package/convex/revalidate.ts +18 -30
- package/convex/triggers.ts +47 -38
- package/convex/webhooks.ts +77 -62
- package/dist/components/account/self-account-view-source.js +2 -1
- package/dist/components/account/self-account-view-source.js.map +1 -1
- package/dist/components/ai/ai-chat-panel.js +9 -2
- package/dist/components/ai/ai-chat-panel.js.map +1 -1
- package/dist/components/app-views/default-modules.js +3 -3
- package/dist/components/app-views/default-modules.js.map +1 -1
- package/dist/components/assets/asset-grid.js +1 -1
- package/dist/components/assets/asset-grid.js.map +1 -1
- package/dist/components/assets/assets-collection-view-source.js +3 -2
- 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/canvas-render-context.d.ts +12 -1
- package/dist/components/builder/canvas-render-context.js +20 -16
- package/dist/components/builder/canvas-render-context.js.map +1 -1
- package/dist/components/builder/reuse-context.js +2 -2
- package/dist/components/builder/reuse-context.js.map +1 -1
- package/dist/components/builder/reuse.d.ts +6 -2
- package/dist/components/builder/reuse.js +12 -7
- 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 +27 -59
- 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 -7
- package/dist/components/cms/cell-edit.js +7 -25
- package/dist/components/cms/cell-edit.js.map +1 -1
- package/dist/components/cms/cms-store.d.ts +3 -2
- package/dist/components/cms/cms-store.js +38 -47
- package/dist/components/cms/cms-store.js.map +1 -1
- package/dist/components/cms/collection-table.js +14 -4
- package/dist/components/cms/collection-table.js.map +1 -1
- package/dist/components/cms/detail-editor.js +20 -21
- package/dist/components/cms/detail-editor.js.map +1 -1
- package/dist/components/cms/duplicate-document.d.ts +3 -2
- package/dist/components/cms/duplicate-document.js +5 -5
- package/dist/components/cms/duplicate-document.js.map +1 -1
- package/dist/components/cms/editable-cell.js +3 -6
- package/dist/components/cms/editable-cell.js.map +1 -1
- package/dist/components/cms/field-control.js +7 -6
- package/dist/components/cms/field-control.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/publish-clean.d.ts +13 -6
- package/dist/components/cms/publish-clean.js +14 -6
- package/dist/components/cms/publish-clean.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-publish-controller.js +3 -2
- package/dist/components/cms/use-publish-controller.js.map +1 -1
- package/dist/components/components/transfer-dialog.js +1 -0
- package/dist/components/components/transfer-dialog.js.map +1 -1
- package/dist/components/components/transfer.d.ts +10 -4
- package/dist/components/components/transfer.js +11 -9
- package/dist/components/components/transfer.js.map +1 -1
- package/dist/components/delivery-health-banner.d.ts +3 -2
- package/dist/components/delivery-health-banner.js +3 -2
- package/dist/components/delivery-health-banner.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/redirects/redirects-collection-view-source.js +3 -2
- 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/users/users-collection-view-source.js +2 -1
- package/dist/components/users/users-collection-view-source.js.map +1 -1
- package/dist/lib/ai/apply-content.js +32 -28
- package/dist/lib/ai/apply-content.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/translate-document.js +11 -7
- package/dist/lib/ai/translate-document.js.map +1 -1
- package/dist/lib/ai/types.d.ts +9 -5
- package/dist/lib/ai/types.js.map +1 -1
- package/dist/lib/ai/update-fields.js +10 -29
- package/dist/lib/ai/update-fields.js.map +1 -1
- package/dist/lib/ai/vite.d.ts +22 -2
- package/dist/lib/ai/vite.js +12 -3
- package/dist/lib/ai/vite.js.map +1 -1
- package/dist/lib/cms/published-documents.d.ts +14 -3
- package/dist/lib/cms/published-documents.js +12 -3
- package/dist/lib/cms/published-documents.js.map +1 -1
- package/dist/lib/cms-views/view-model.d.ts +4 -4
- package/dist/lib/cms-views/view-model.js +16 -13
- package/dist/lib/cms-views/view-model.js.map +1 -1
- package/dist/lib/data/collection-content.d.ts +22 -1
- package/dist/lib/data/collection-content.js +51 -14
- package/dist/lib/data/collection-content.js.map +1 -1
- package/dist/lib/delivery/instant-publish.d.ts +0 -3
- package/dist/lib/delivery/instant-publish.js +0 -1
- package/dist/lib/delivery/instant-publish.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 +5 -0
- package/dist/lib/i18n/messages.de.js.map +1 -1
- package/dist/lib/i18n/messages.en.d.ts +4 -0
- package/dist/lib/i18n/messages.en.js +6 -0
- package/dist/lib/i18n/messages.en.js.map +1 -1
- package/dist/lib/locale/fallback.d.ts +5 -19
- package/dist/lib/locale/fallback.js +18 -39
- package/dist/lib/locale/fallback.js.map +1 -1
- package/dist/lib/locale/locale-store.js +2 -1
- package/dist/lib/locale/locale-store.js.map +1 -1
- package/dist/lib/operations/session-hydration.d.ts +24 -26
- package/dist/lib/operations/session-hydration.js +15 -53
- package/dist/lib/operations/session-hydration.js.map +1 -1
- package/dist/lib/operations/studio-operations.js +4 -6
- package/dist/lib/operations/studio-operations.js.map +1 -1
- package/dist/lib/persistence/backend-mode.d.ts +5 -25
- package/dist/lib/persistence/backend-mode.js +13 -30
- package/dist/lib/persistence/backend-mode.js.map +1 -1
- package/dist/lib/references/reference-model.d.ts +17 -3
- package/dist/lib/references/reference-model.js +17 -23
- package/dist/lib/references/reference-model.js.map +1 -1
- package/dist/lib/references/references-store.js +7 -0
- package/dist/lib/references/references-store.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 +1 -8
- package/dist/lib/workspace/index.js +1 -10
- package/dist/lib/workspace/index.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/package.json +3 -7
- package/dist/lib/webhooks/config.d.ts +0 -15
- package/dist/lib/webhooks/config.js +0 -65
- package/dist/lib/webhooks/config.js.map +0 -1
- package/dist/lib/webhooks/dispatcher.d.ts +0 -70
- package/dist/lib/webhooks/dispatcher.js +0 -61
- package/dist/lib/webhooks/dispatcher.js.map +0 -1
- package/dist/lib/webhooks/index.d.ts +0 -11
- package/dist/lib/webhooks/index.js +0 -10
- package/dist/lib/webhooks/index.js.map +0 -1
- package/dist/lib/webhooks/signing.d.ts +0 -15
- package/dist/lib/webhooks/signing.js +0 -24
- package/dist/lib/webhooks/signing.js.map +0 -1
- package/dist/lib/webhooks/tags.d.ts +0 -25
- package/dist/lib/webhooks/tags.js +0 -289
- package/dist/lib/webhooks/tags.js.map +0 -1
|
@@ -1,31 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
* Studio backend mode resolution (EC-153). Convex is the **default and primary**
|
|
3
|
-
* persistence backend; the deterministic in-memory adapter is demoted to an
|
|
4
|
-
* explicit, labeled **playground** mode and is never an automatic fallback:
|
|
5
|
-
*
|
|
6
|
-
* - `VITE_CONVEX_URL` set → Convex mode (the production/dev default).
|
|
7
|
-
* - `VITE_ELYTRA_MODE=playground` → playground mode (seeded in-memory adapter,
|
|
8
|
-
* local + ephemeral). The flag wins over a configured URL — explicit beats
|
|
9
|
-
* implicit — so a checked-out `.env.local` can be flipped without deleting it.
|
|
10
|
-
* - Neither → **misconfigured**: the studio renders a deterministic boot-error
|
|
11
|
-
* screen (see `DeploymentSetupGate`) telling the developer exactly what to set.
|
|
12
|
-
* Likewise, a Convex backend that is unreachable at runtime is a visible
|
|
13
|
-
* error state — the studio never silently swaps in in-memory storage.
|
|
14
|
-
*/
|
|
1
|
+
import type { BackendMode } from '@elytracms/core/export-sync/deployment-ready';
|
|
15
2
|
export declare const MODE_ENV_VAR = "VITE_ELYTRA_MODE";
|
|
16
3
|
export declare const PLAYGROUND_MODE = "playground";
|
|
17
4
|
export declare const CONVEX_URL_ENV_VAR = "VITE_CONVEX_URL";
|
|
18
5
|
export type StudioBackendMode = 'convex' | 'playground';
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
url: string;
|
|
22
|
-
} | {
|
|
23
|
-
kind: 'playground';
|
|
24
|
-
} | {
|
|
25
|
-
kind: 'misconfigured';
|
|
26
|
-
reason: 'missing-convex-url' | 'unknown-mode';
|
|
27
|
-
detail: string;
|
|
28
|
-
};
|
|
6
|
+
/** The backend verdict — core's {@link BackendMode}, under the studio's historical name. */
|
|
7
|
+
export type StudioBackendConfig = BackendMode;
|
|
29
8
|
/** The studio's env surface: Vite client env first, Node/SSR env second. */
|
|
30
9
|
export type EnvRecord = Record<string, string | undefined>;
|
|
31
10
|
/**
|
|
@@ -91,7 +70,8 @@ export type StudioEnvAnomaly =
|
|
|
91
70
|
export declare function detectStudioEnvAnomaly(provided: EnvRecord | null, engineVisible: EnvRecord | null, inBrowser: boolean): StudioEnvAnomaly | null;
|
|
92
71
|
/**
|
|
93
72
|
* Pure mode decision — testable with a plain env record. The app calls it with
|
|
94
|
-
* {@link studioEnv}; `CONVEX_URL` (Node-side alias) is honored for SSR/tools.
|
|
73
|
+
* {@link studioEnv}; `CONVEX_URL` (Node-side alias) is honored for SSR/tools. The rule
|
|
74
|
+
* itself is core's `parseBackendMode` (EC-603) — the same one the CLI applies.
|
|
95
75
|
*/
|
|
96
76
|
export declare function resolveBackendConfig(env?: EnvRecord): StudioBackendConfig;
|
|
97
77
|
/** True when the studio runs in the explicit, labeled playground mode. */
|
|
@@ -11,10 +11,15 @@
|
|
|
11
11
|
* screen (see `DeploymentSetupGate`) telling the developer exactly what to set.
|
|
12
12
|
* Likewise, a Convex backend that is unreachable at runtime is a visible
|
|
13
13
|
* error state — the studio never silently swaps in in-memory storage.
|
|
14
|
+
*
|
|
15
|
+
* EC-603: the RULE above lives in ONE place — core's `parseBackendMode`, which the
|
|
16
|
+
* `elytra` CLI runs over the same env bytes. This module keeps the studio's env SOURCE
|
|
17
|
+
* (the mount-provided env, {@link studioEnv}) and hands it to that parser.
|
|
14
18
|
*/
|
|
15
|
-
|
|
16
|
-
export const
|
|
17
|
-
export const
|
|
19
|
+
import { BACKEND_MODE_ENV_VAR, BACKEND_URL_ENV_VAR, PLAYGROUND_MODE as CORE_PLAYGROUND_MODE, parseBackendMode, readEnvValue, } from '@elytracms/core/export-sync/deployment-ready';
|
|
20
|
+
export const MODE_ENV_VAR = BACKEND_MODE_ENV_VAR;
|
|
21
|
+
export const PLAYGROUND_MODE = CORE_PLAYGROUND_MODE;
|
|
22
|
+
export const CONVEX_URL_ENV_VAR = BACKEND_URL_ENV_VAR;
|
|
18
23
|
/**
|
|
19
24
|
* The ENGINE-visible `import.meta.env`, or `null` when the module has none at all.
|
|
20
25
|
* `null` is a meaningful signal (EC-357): every module Vite transforms live gets an
|
|
@@ -35,10 +40,8 @@ function viteEnv() {
|
|
|
35
40
|
function nodeEnv() {
|
|
36
41
|
return typeof process !== 'undefined' ? (process.env ?? {}) : {};
|
|
37
42
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
return value && value.trim() !== '' ? value.trim() : undefined;
|
|
41
|
-
}
|
|
43
|
+
/** Trimmed, blank-is-absent — the parser's own read, so every studio env key reads alike. */
|
|
44
|
+
const readEnv = readEnvValue;
|
|
42
45
|
let envOverride = null;
|
|
43
46
|
/**
|
|
44
47
|
* Test seam: pin the env the selectors see. `vi.stubEnv` cannot reach the
|
|
@@ -165,31 +168,11 @@ function maybeWarnEngineEnvStale() {
|
|
|
165
168
|
}
|
|
166
169
|
/**
|
|
167
170
|
* Pure mode decision — testable with a plain env record. The app calls it with
|
|
168
|
-
* {@link studioEnv}; `CONVEX_URL` (Node-side alias) is honored for SSR/tools.
|
|
171
|
+
* {@link studioEnv}; `CONVEX_URL` (Node-side alias) is honored for SSR/tools. The rule
|
|
172
|
+
* itself is core's `parseBackendMode` (EC-603) — the same one the CLI applies.
|
|
169
173
|
*/
|
|
170
174
|
export function resolveBackendConfig(env = studioEnv()) {
|
|
171
|
-
|
|
172
|
-
if (mode === PLAYGROUND_MODE)
|
|
173
|
-
return { kind: 'playground' };
|
|
174
|
-
if (mode !== undefined) {
|
|
175
|
-
return {
|
|
176
|
-
kind: 'misconfigured',
|
|
177
|
-
reason: 'unknown-mode',
|
|
178
|
-
detail: `${MODE_ENV_VAR}="${mode}" is not a known mode. The only supported value is "${PLAYGROUND_MODE}"; unset it to run against Convex.`,
|
|
179
|
-
};
|
|
180
|
-
}
|
|
181
|
-
const url = readEnv(env, CONVEX_URL_ENV_VAR) ?? readEnv(env, 'CONVEX_URL');
|
|
182
|
-
if (url)
|
|
183
|
-
return { kind: 'convex', url };
|
|
184
|
-
return {
|
|
185
|
-
kind: 'misconfigured',
|
|
186
|
-
reason: 'missing-convex-url',
|
|
187
|
-
// EC-357: name BOTH causes — the var may be genuinely unset, or set but never
|
|
188
|
-
// reaching the client at runtime (Vite dep-optimized engine code; see
|
|
189
|
-
// `provideStudioEnv`). The paired fix text (deployment-ready `FIX.backendUrl`)
|
|
190
|
-
// carries the full remedy for the second cause.
|
|
191
|
-
detail: `No persistence backend is configured — ${CONVEX_URL_ENV_VAR} is unset or is not reaching the client at runtime. Set it to your Convex deployment URL (see docs/backend-setup.md), or opt into the ephemeral local playground with ${MODE_ENV_VAR}=${PLAYGROUND_MODE}.`,
|
|
192
|
-
};
|
|
175
|
+
return parseBackendMode(env);
|
|
193
176
|
}
|
|
194
177
|
/** True when the studio runs in the explicit, labeled playground mode. */
|
|
195
178
|
export function isPlaygroundMode() {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"backend-mode.js","sourceRoot":"","sources":["../../../src/lib/persistence/backend-mode.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Studio backend mode resolution (EC-153). Convex is the **default and primary**\n * persistence backend; the deterministic in-memory adapter is demoted to an\n * explicit, labeled **playground** mode and is never an automatic fallback:\n *\n * - `VITE_CONVEX_URL` set → Convex mode (the production/dev default).\n * - `VITE_ELYTRA_MODE=playground` → playground mode (seeded in-memory adapter,\n * local + ephemeral). The flag wins over a configured URL — explicit beats\n * implicit — so a checked-out `.env.local` can be flipped without deleting it.\n * - Neither → **misconfigured**: the studio renders a deterministic boot-error\n * screen (see `DeploymentSetupGate`) telling the developer exactly what to set.\n * Likewise, a Convex backend that is unreachable at runtime is a visible\n * error state — the studio never silently swaps in in-memory storage.\n */\n\nexport const MODE_ENV_VAR = 'VITE_ELYTRA_MODE'\nexport const PLAYGROUND_MODE = 'playground'\nexport const CONVEX_URL_ENV_VAR = 'VITE_CONVEX_URL'\n\nexport type StudioBackendMode = 'convex' | 'playground'\n\nexport type StudioBackendConfig =\n | { kind: 'convex'; url: string }\n | { kind: 'playground' }\n | { kind: 'misconfigured'; reason: 'missing-convex-url' | 'unknown-mode'; detail: string }\n\n/** The studio's env surface: Vite client env first, Node/SSR env second. */\nexport type EnvRecord = Record<string, string | undefined>\n\n/**\n * The ENGINE-visible `import.meta.env`, or `null` when the module has none at all.\n * `null` is a meaningful signal (EC-357): every module Vite transforms live gets an\n * `import.meta.env`, so its complete absence in a browser means this engine module\n * was served from Vite's dep-optimizer cache — where Vite ≥8 no longer inlines\n * custom `VITE_*` values (and Vite 7 froze them at optimize time). Engine code must\n * therefore never trust this view when a mount-provided env exists — see\n * {@link provideStudioEnv}.\n */\nfunction engineVisibleEnv(): EnvRecord | null {\n if (typeof import.meta === 'undefined') return null\n return (import.meta as { env?: EnvRecord }).env ?? null\n}\n\nfunction viteEnv(): EnvRecord {\n return engineVisibleEnv() ?? {}\n}\n\nfunction nodeEnv(): EnvRecord {\n return typeof process !== 'undefined' ? (process.env ?? {}) : {}\n}\n\nfunction readEnv(env: EnvRecord, key: string): string | undefined {\n const value = env[key]\n return value && value.trim() !== '' ? value.trim() : undefined\n}\n\nlet envOverride: EnvRecord | null = null\n\n/**\n * Test seam: pin the env the selectors see. `vi.stubEnv` cannot reach the\n * `import.meta.env` snapshot Vitest hands this module, so suites that exercise\n * `selectPersistenceAdapter`/`selectAuthAdapter` against specific configurations\n * use this instead — keeping results independent of any developer `.env.local`.\n */\nexport function setStudioEnvOverrideForTests(env: EnvRecord | null): void {\n envOverride = env\n}\n\n// ---------------------------------------------------------------------------\n// Consumer-provided env (EC-357 — the Vite dep-optimizer env fix)\n// ---------------------------------------------------------------------------\n\n/** Normalized mount-provided env: the string entries + the captured `DEV` flag. */\ninterface ProvidedStudioEnv {\n record: EnvRecord\n dev: boolean | undefined\n}\n\nlet providedEnv: ProvidedStudioEnv | null = null\n\n/**\n * Install the CONSUMER-captured env as the studio's env source (EC-357). The root\n * cause this fixes: the engine used to read `import.meta.env.VITE_*` inside\n * `@elytracms` package code, which Vite dep-optimizes in a real consumer — on\n * Vite ≥8 those values are simply absent at runtime in the optimized chunk (the\n * backend URL / mode / dev-login silently vanish and every sign-in fails), and on\n * Vite 7 they froze at optimize time (stale until `node_modules/.vite` is wiped).\n *\n * The consumer's own entry module is code Vite ALWAYS transforms live, so the env\n * is captured there and handed down: `createStudio(config, studioConfig, { env:\n * import.meta.env })`. When a provided env exists it REPLACES the engine-visible\n * Vite layer entirely (never merged with it — a stale optimizer snapshot must not\n * shadow a removed key); `process.env` stays underneath as the SSR/tooling base.\n *\n * `DEV` is captured alongside (it gates dev auto-login): in a production consumer\n * build Vite statically inlines `import.meta.env` with `DEV: false`, so the\n * dev-login gate keeps failing closed exactly as before. Non-string values other\n * than `DEV` are dropped — the studio env surface is a string record.\n *\n * Passing `null`/`undefined` is a no-op (a consumer entry that predates the option\n * keeps today's engine-visible fallback behavior, plus a loud runtime warning when\n * that fallback is provably broken — see {@link maybeWarnEngineEnvUnreachable}).\n */\nexport function provideStudioEnv(env: Record<string, unknown> | null | undefined): void {\n if (env === null || env === undefined) return\n const record: EnvRecord = {}\n for (const [key, value] of Object.entries(env)) {\n if (typeof value === 'string') record[key] = value\n }\n providedEnv = {\n record,\n dev: env.DEV === true ? true : env.DEV === false ? false : undefined,\n }\n maybeWarnEngineEnvStale()\n}\n\n/** Test seam: clear the mount-provided env. Never used by app code. */\nexport function resetProvidedStudioEnvForTests(): void {\n providedEnv = null\n warnedStale = false\n warnedUnreachable = false\n}\n\n/**\n * Resolve the configured studio env. Precedence: the test override, else the\n * mount-provided env (over `process.env`), else the engine-visible Vite env\n * (over `process.env`).\n */\nexport function studioEnv(): EnvRecord {\n if (envOverride) return envOverride\n if (providedEnv) return { ...nodeEnv(), ...providedEnv.record }\n maybeWarnEngineEnvUnreachable()\n return { ...nodeEnv(), ...viteEnv() }\n}\n\n// --- Boot-time env self-check (EC-357, Oli: \"easier to catch\") ---------------\n\n/** The env keys whose loss/staleness breaks boot or sign-in. */\nexport const STUDIO_ENV_KEYS: readonly string[] = [\n 'VITE_ELYTRA_MODE',\n 'VITE_CONVEX_URL',\n 'VITE_ELYTRA_DEV_LOGIN',\n]\n\nexport type StudioEnvAnomaly =\n /** No mount-provided env AND the engine module has no `import.meta.env` at all. */\n | { kind: 'engine-env-unreachable' }\n /** The engine-visible env disagrees with the mount-provided env (stale optimizer cache). */\n | { kind: 'engine-env-stale'; keys: string[] }\n\n/**\n * Pure anomaly detection over the two env views (unit-testable core of the boot\n * warnings). `engineVisible === null` means the engine module has no\n * `import.meta.env` — expected under dep optimization WITH a provided env (silent),\n * fatal without one (the values can never arrive). A stale mismatch fires only for\n * keys the ENGINE sees with a different value than the mount provided (the Vite 7\n * freeze); keys the engine simply lacks (Vite 8) are not anomalies — the provided\n * env is authoritative and in effect.\n */\nexport function detectStudioEnvAnomaly(\n provided: EnvRecord | null,\n engineVisible: EnvRecord | null,\n inBrowser: boolean,\n): StudioEnvAnomaly | null {\n if (!inBrowser) return null\n if (!provided) return engineVisible === null ? { kind: 'engine-env-unreachable' } : null\n if (engineVisible === null) return null\n const stale = STUDIO_ENV_KEYS.filter((key) => {\n const engineValue = readEnv(engineVisible, key)\n return engineValue !== undefined && engineValue !== readEnv(provided, key)\n })\n return stale.length > 0 ? { kind: 'engine-env-stale', keys: stale } : null\n}\n\nfunction inBrowser(): boolean {\n return typeof window !== 'undefined' && typeof document !== 'undefined'\n}\n\nlet warnedUnreachable = false\nfunction maybeWarnEngineEnvUnreachable(): void {\n if (warnedUnreachable) return\n const anomaly = detectStudioEnvAnomaly(null, engineVisibleEnv(), inBrowser())\n if (anomaly?.kind !== 'engine-env-unreachable') return\n warnedUnreachable = true\n console.warn(\n '[elytra] The engine cannot see import.meta.env — this @elytracms module was dep-optimized by Vite, ' +\n 'so VITE_CONVEX_URL / VITE_ELYTRA_MODE / VITE_ELYTRA_DEV_LOGIN cannot reach it at runtime. ' +\n 'Fix: pass your studio entry’s env to the mount — createStudio(config, studioConfig, { env: import.meta.env }) — ' +\n \"or add optimizeDeps.exclude: ['@elytracms/studio', '@elytracms/core'] to the studio vite.config.ts.\",\n )\n}\n\nlet warnedStale = false\nfunction maybeWarnEngineEnvStale(): void {\n if (warnedStale || !providedEnv) return\n const anomaly = detectStudioEnvAnomaly(providedEnv.record, engineVisibleEnv(), inBrowser())\n if (anomaly?.kind !== 'engine-env-stale') return\n warnedStale = true\n console.warn(\n `[elytra] import.meta.env inside the engine disagrees with the studio mount's env for: ${anomaly.keys.join(', ')}. ` +\n \"The engine copy is a stale Vite dep-optimizer snapshot; the mount-provided values are in effect. \" +\n 'Delete node_modules/.vite to drop the stale copy.',\n )\n}\n\n/**\n * Pure mode decision — testable with a plain env record. The app calls it with\n * {@link studioEnv}; `CONVEX_URL` (Node-side alias) is honored for SSR/tools.\n */\nexport function resolveBackendConfig(env: EnvRecord = studioEnv()): StudioBackendConfig {\n const mode = readEnv(env, MODE_ENV_VAR)\n if (mode === PLAYGROUND_MODE) return { kind: 'playground' }\n if (mode !== undefined) {\n return {\n kind: 'misconfigured',\n reason: 'unknown-mode',\n detail: `${MODE_ENV_VAR}=\"${mode}\" is not a known mode. The only supported value is \"${PLAYGROUND_MODE}\"; unset it to run against Convex.`,\n }\n }\n const url = readEnv(env, CONVEX_URL_ENV_VAR) ?? readEnv(env, 'CONVEX_URL')\n if (url) return { kind: 'convex', url }\n return {\n kind: 'misconfigured',\n reason: 'missing-convex-url',\n // EC-357: name BOTH causes — the var may be genuinely unset, or set but never\n // reaching the client at runtime (Vite dep-optimized engine code; see\n // `provideStudioEnv`). The paired fix text (deployment-ready `FIX.backendUrl`)\n // carries the full remedy for the second cause.\n detail: `No persistence backend is configured — ${CONVEX_URL_ENV_VAR} is unset or is not reaching the client at runtime. Set it to your Convex deployment URL (see docs/backend-setup.md), or opt into the ephemeral local playground with ${MODE_ENV_VAR}=${PLAYGROUND_MODE}.`,\n }\n}\n\n/** True when the studio runs in the explicit, labeled playground mode. */\nexport function isPlaygroundMode(): boolean {\n return resolveBackendConfig().kind === 'playground'\n}\n\n// ---------------------------------------------------------------------------\n// Dev auto-login (unattended testing — NOT an auth bypass)\n// ---------------------------------------------------------------------------\n\n/** `email:password` for unattended dev sign-in (gitignored `.env.local`). */\nexport const DEV_LOGIN_ENV_VAR = 'VITE_ELYTRA_DEV_LOGIN'\n\n/** Parsed dev auto-login credentials. */\nexport interface DevAutoLoginCredentials {\n email: string\n password: string\n}\n\n/**\n * Is this a Vite *dev* build? The mount-provided env's captured `DEV` flag wins\n * (EC-357 — inside dep-optimized engine code the engine-visible flag is absent on\n * Vite ≥8 and frozen on Vite 7); the consumer's `import.meta.env.DEV` is statically\n * `true` under `vite dev` and statically `false` in any production build, so the\n * dev auto-login gate still fails closed in production. Without a provided env the\n * engine-visible static flag applies as before (where it also dead-code-eliminates\n * the feature out of a directly-bundled production build).\n */\nfunction isViteDevBuild(): boolean {\n if (providedEnv?.dev !== undefined) return providedEnv.dev\n if (typeof import.meta === 'undefined') return false\n return (import.meta as { env?: { DEV?: boolean } }).env?.DEV === true\n}\n\n/**\n * The studio's house dev-build check, public: dev-only surfaces (e.g. the\n * EC-414 canvas-theme migration notice) gate on THIS — it honors the\n * mount-provided env's captured `DEV` flag (the EC-357 dep-optimization\n * nuance) before falling back to the engine-visible static flag.\n */\nexport function isDevRuntime(): boolean {\n return isViteDevBuild()\n}\n\n/**\n * Dev-only unattended auth — **not a bypass**. When `VITE_ELYTRA_DEV_LOGIN=\n * email:password` is set in a dev build, the studio auto-signs-in with those\n * credentials on load (see {@link DevAutoLoginAdapter}) so local/automated\n * testing skips the sign-in screen while still establishing a REAL backend\n * session: Convex's server-side `requireRole` guards still apply, so the dev\n * user must exist and be a member. Returns null in production builds (gated by\n * {@link isViteDevBuild}) and when unset/malformed, so it can never weaken a\n * deployed app. `isDevBuild` is a parameter purely so the decision is unit\n * testable (`import.meta.env.DEV` is not reachable via `vi.stubEnv`).\n *\n * `password` may itself contain `:` — only the first colon separates the fields.\n */\nexport function resolveDevAutoLogin(\n env: EnvRecord = studioEnv(),\n isDevBuild: boolean = isViteDevBuild(),\n): DevAutoLoginCredentials | null {\n if (!isDevBuild) return null\n const raw = readEnv(env, DEV_LOGIN_ENV_VAR)\n if (!raw) return null\n const separator = raw.indexOf(':')\n if (separator <= 0) return null\n const email = raw.slice(0, separator).trim()\n const password = raw.slice(separator + 1)\n if (!email || !password) return null\n return { email, password }\n}\n\n/** True when dev auto-login is configured (dev build only) — for the UI marker. */\nexport function isDevAutoLoginActive(): boolean {\n return resolveDevAutoLogin() !== null\n}\n"]}
|
|
1
|
+
{"version":3,"file":"backend-mode.js","sourceRoot":"","sources":["../../../src/lib/persistence/backend-mode.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Studio backend mode resolution (EC-153). Convex is the **default and primary**\n * persistence backend; the deterministic in-memory adapter is demoted to an\n * explicit, labeled **playground** mode and is never an automatic fallback:\n *\n * - `VITE_CONVEX_URL` set → Convex mode (the production/dev default).\n * - `VITE_ELYTRA_MODE=playground` → playground mode (seeded in-memory adapter,\n * local + ephemeral). The flag wins over a configured URL — explicit beats\n * implicit — so a checked-out `.env.local` can be flipped without deleting it.\n * - Neither → **misconfigured**: the studio renders a deterministic boot-error\n * screen (see `DeploymentSetupGate`) telling the developer exactly what to set.\n * Likewise, a Convex backend that is unreachable at runtime is a visible\n * error state — the studio never silently swaps in in-memory storage.\n *\n * EC-603: the RULE above lives in ONE place — core's `parseBackendMode`, which the\n * `elytra` CLI runs over the same env bytes. This module keeps the studio's env SOURCE\n * (the mount-provided env, {@link studioEnv}) and hands it to that parser.\n */\nimport {\n BACKEND_MODE_ENV_VAR,\n BACKEND_URL_ENV_VAR,\n PLAYGROUND_MODE as CORE_PLAYGROUND_MODE,\n parseBackendMode,\n readEnvValue,\n} from '@elytracms/core/export-sync/deployment-ready'\nimport type { BackendMode } from '@elytracms/core/export-sync/deployment-ready'\n\nexport const MODE_ENV_VAR = BACKEND_MODE_ENV_VAR\nexport const PLAYGROUND_MODE = CORE_PLAYGROUND_MODE\nexport const CONVEX_URL_ENV_VAR = BACKEND_URL_ENV_VAR\n\nexport type StudioBackendMode = 'convex' | 'playground'\n\n/** The backend verdict — core's {@link BackendMode}, under the studio's historical name. */\nexport type StudioBackendConfig = BackendMode\n\n/** The studio's env surface: Vite client env first, Node/SSR env second. */\nexport type EnvRecord = Record<string, string | undefined>\n\n/**\n * The ENGINE-visible `import.meta.env`, or `null` when the module has none at all.\n * `null` is a meaningful signal (EC-357): every module Vite transforms live gets an\n * `import.meta.env`, so its complete absence in a browser means this engine module\n * was served from Vite's dep-optimizer cache — where Vite ≥8 no longer inlines\n * custom `VITE_*` values (and Vite 7 froze them at optimize time). Engine code must\n * therefore never trust this view when a mount-provided env exists — see\n * {@link provideStudioEnv}.\n */\nfunction engineVisibleEnv(): EnvRecord | null {\n if (typeof import.meta === 'undefined') return null\n return (import.meta as { env?: EnvRecord }).env ?? null\n}\n\nfunction viteEnv(): EnvRecord {\n return engineVisibleEnv() ?? {}\n}\n\nfunction nodeEnv(): EnvRecord {\n return typeof process !== 'undefined' ? (process.env ?? {}) : {}\n}\n\n/** Trimmed, blank-is-absent — the parser's own read, so every studio env key reads alike. */\nconst readEnv: (env: EnvRecord, key: string) => string | undefined = readEnvValue\n\nlet envOverride: EnvRecord | null = null\n\n/**\n * Test seam: pin the env the selectors see. `vi.stubEnv` cannot reach the\n * `import.meta.env` snapshot Vitest hands this module, so suites that exercise\n * `selectPersistenceAdapter`/`selectAuthAdapter` against specific configurations\n * use this instead — keeping results independent of any developer `.env.local`.\n */\nexport function setStudioEnvOverrideForTests(env: EnvRecord | null): void {\n envOverride = env\n}\n\n// ---------------------------------------------------------------------------\n// Consumer-provided env (EC-357 — the Vite dep-optimizer env fix)\n// ---------------------------------------------------------------------------\n\n/** Normalized mount-provided env: the string entries + the captured `DEV` flag. */\ninterface ProvidedStudioEnv {\n record: EnvRecord\n dev: boolean | undefined\n}\n\nlet providedEnv: ProvidedStudioEnv | null = null\n\n/**\n * Install the CONSUMER-captured env as the studio's env source (EC-357). The root\n * cause this fixes: the engine used to read `import.meta.env.VITE_*` inside\n * `@elytracms` package code, which Vite dep-optimizes in a real consumer — on\n * Vite ≥8 those values are simply absent at runtime in the optimized chunk (the\n * backend URL / mode / dev-login silently vanish and every sign-in fails), and on\n * Vite 7 they froze at optimize time (stale until `node_modules/.vite` is wiped).\n *\n * The consumer's own entry module is code Vite ALWAYS transforms live, so the env\n * is captured there and handed down: `createStudio(config, studioConfig, { env:\n * import.meta.env })`. When a provided env exists it REPLACES the engine-visible\n * Vite layer entirely (never merged with it — a stale optimizer snapshot must not\n * shadow a removed key); `process.env` stays underneath as the SSR/tooling base.\n *\n * `DEV` is captured alongside (it gates dev auto-login): in a production consumer\n * build Vite statically inlines `import.meta.env` with `DEV: false`, so the\n * dev-login gate keeps failing closed exactly as before. Non-string values other\n * than `DEV` are dropped — the studio env surface is a string record.\n *\n * Passing `null`/`undefined` is a no-op (a consumer entry that predates the option\n * keeps today's engine-visible fallback behavior, plus a loud runtime warning when\n * that fallback is provably broken — see {@link maybeWarnEngineEnvUnreachable}).\n */\nexport function provideStudioEnv(env: Record<string, unknown> | null | undefined): void {\n if (env === null || env === undefined) return\n const record: EnvRecord = {}\n for (const [key, value] of Object.entries(env)) {\n if (typeof value === 'string') record[key] = value\n }\n providedEnv = {\n record,\n dev: env.DEV === true ? true : env.DEV === false ? false : undefined,\n }\n maybeWarnEngineEnvStale()\n}\n\n/** Test seam: clear the mount-provided env. Never used by app code. */\nexport function resetProvidedStudioEnvForTests(): void {\n providedEnv = null\n warnedStale = false\n warnedUnreachable = false\n}\n\n/**\n * Resolve the configured studio env. Precedence: the test override, else the\n * mount-provided env (over `process.env`), else the engine-visible Vite env\n * (over `process.env`).\n */\nexport function studioEnv(): EnvRecord {\n if (envOverride) return envOverride\n if (providedEnv) return { ...nodeEnv(), ...providedEnv.record }\n maybeWarnEngineEnvUnreachable()\n return { ...nodeEnv(), ...viteEnv() }\n}\n\n// --- Boot-time env self-check (EC-357, Oli: \"easier to catch\") ---------------\n\n/** The env keys whose loss/staleness breaks boot or sign-in. */\nexport const STUDIO_ENV_KEYS: readonly string[] = [\n 'VITE_ELYTRA_MODE',\n 'VITE_CONVEX_URL',\n 'VITE_ELYTRA_DEV_LOGIN',\n]\n\nexport type StudioEnvAnomaly =\n /** No mount-provided env AND the engine module has no `import.meta.env` at all. */\n | { kind: 'engine-env-unreachable' }\n /** The engine-visible env disagrees with the mount-provided env (stale optimizer cache). */\n | { kind: 'engine-env-stale'; keys: string[] }\n\n/**\n * Pure anomaly detection over the two env views (unit-testable core of the boot\n * warnings). `engineVisible === null` means the engine module has no\n * `import.meta.env` — expected under dep optimization WITH a provided env (silent),\n * fatal without one (the values can never arrive). A stale mismatch fires only for\n * keys the ENGINE sees with a different value than the mount provided (the Vite 7\n * freeze); keys the engine simply lacks (Vite 8) are not anomalies — the provided\n * env is authoritative and in effect.\n */\nexport function detectStudioEnvAnomaly(\n provided: EnvRecord | null,\n engineVisible: EnvRecord | null,\n inBrowser: boolean,\n): StudioEnvAnomaly | null {\n if (!inBrowser) return null\n if (!provided) return engineVisible === null ? { kind: 'engine-env-unreachable' } : null\n if (engineVisible === null) return null\n const stale = STUDIO_ENV_KEYS.filter((key) => {\n const engineValue = readEnv(engineVisible, key)\n return engineValue !== undefined && engineValue !== readEnv(provided, key)\n })\n return stale.length > 0 ? { kind: 'engine-env-stale', keys: stale } : null\n}\n\nfunction inBrowser(): boolean {\n return typeof window !== 'undefined' && typeof document !== 'undefined'\n}\n\nlet warnedUnreachable = false\nfunction maybeWarnEngineEnvUnreachable(): void {\n if (warnedUnreachable) return\n const anomaly = detectStudioEnvAnomaly(null, engineVisibleEnv(), inBrowser())\n if (anomaly?.kind !== 'engine-env-unreachable') return\n warnedUnreachable = true\n console.warn(\n '[elytra] The engine cannot see import.meta.env — this @elytracms module was dep-optimized by Vite, ' +\n 'so VITE_CONVEX_URL / VITE_ELYTRA_MODE / VITE_ELYTRA_DEV_LOGIN cannot reach it at runtime. ' +\n 'Fix: pass your studio entry’s env to the mount — createStudio(config, studioConfig, { env: import.meta.env }) — ' +\n \"or add optimizeDeps.exclude: ['@elytracms/studio', '@elytracms/core'] to the studio vite.config.ts.\",\n )\n}\n\nlet warnedStale = false\nfunction maybeWarnEngineEnvStale(): void {\n if (warnedStale || !providedEnv) return\n const anomaly = detectStudioEnvAnomaly(providedEnv.record, engineVisibleEnv(), inBrowser())\n if (anomaly?.kind !== 'engine-env-stale') return\n warnedStale = true\n console.warn(\n `[elytra] import.meta.env inside the engine disagrees with the studio mount's env for: ${anomaly.keys.join(', ')}. ` +\n \"The engine copy is a stale Vite dep-optimizer snapshot; the mount-provided values are in effect. \" +\n 'Delete node_modules/.vite to drop the stale copy.',\n )\n}\n\n/**\n * Pure mode decision — testable with a plain env record. The app calls it with\n * {@link studioEnv}; `CONVEX_URL` (Node-side alias) is honored for SSR/tools. The rule\n * itself is core's `parseBackendMode` (EC-603) — the same one the CLI applies.\n */\nexport function resolveBackendConfig(env: EnvRecord = studioEnv()): StudioBackendConfig {\n return parseBackendMode(env)\n}\n\n/** True when the studio runs in the explicit, labeled playground mode. */\nexport function isPlaygroundMode(): boolean {\n return resolveBackendConfig().kind === 'playground'\n}\n\n// ---------------------------------------------------------------------------\n// Dev auto-login (unattended testing — NOT an auth bypass)\n// ---------------------------------------------------------------------------\n\n/** `email:password` for unattended dev sign-in (gitignored `.env.local`). */\nexport const DEV_LOGIN_ENV_VAR = 'VITE_ELYTRA_DEV_LOGIN'\n\n/** Parsed dev auto-login credentials. */\nexport interface DevAutoLoginCredentials {\n email: string\n password: string\n}\n\n/**\n * Is this a Vite *dev* build? The mount-provided env's captured `DEV` flag wins\n * (EC-357 — inside dep-optimized engine code the engine-visible flag is absent on\n * Vite ≥8 and frozen on Vite 7); the consumer's `import.meta.env.DEV` is statically\n * `true` under `vite dev` and statically `false` in any production build, so the\n * dev auto-login gate still fails closed in production. Without a provided env the\n * engine-visible static flag applies as before (where it also dead-code-eliminates\n * the feature out of a directly-bundled production build).\n */\nfunction isViteDevBuild(): boolean {\n if (providedEnv?.dev !== undefined) return providedEnv.dev\n if (typeof import.meta === 'undefined') return false\n return (import.meta as { env?: { DEV?: boolean } }).env?.DEV === true\n}\n\n/**\n * The studio's house dev-build check, public: dev-only surfaces (e.g. the\n * EC-414 canvas-theme migration notice) gate on THIS — it honors the\n * mount-provided env's captured `DEV` flag (the EC-357 dep-optimization\n * nuance) before falling back to the engine-visible static flag.\n */\nexport function isDevRuntime(): boolean {\n return isViteDevBuild()\n}\n\n/**\n * Dev-only unattended auth — **not a bypass**. When `VITE_ELYTRA_DEV_LOGIN=\n * email:password` is set in a dev build, the studio auto-signs-in with those\n * credentials on load (see {@link DevAutoLoginAdapter}) so local/automated\n * testing skips the sign-in screen while still establishing a REAL backend\n * session: Convex's server-side `requireRole` guards still apply, so the dev\n * user must exist and be a member. Returns null in production builds (gated by\n * {@link isViteDevBuild}) and when unset/malformed, so it can never weaken a\n * deployed app. `isDevBuild` is a parameter purely so the decision is unit\n * testable (`import.meta.env.DEV` is not reachable via `vi.stubEnv`).\n *\n * `password` may itself contain `:` — only the first colon separates the fields.\n */\nexport function resolveDevAutoLogin(\n env: EnvRecord = studioEnv(),\n isDevBuild: boolean = isViteDevBuild(),\n): DevAutoLoginCredentials | null {\n if (!isDevBuild) return null\n const raw = readEnv(env, DEV_LOGIN_ENV_VAR)\n if (!raw) return null\n const separator = raw.indexOf(':')\n if (separator <= 0) return null\n const email = raw.slice(0, separator).trim()\n const password = raw.slice(separator + 1)\n if (!email || !password) return null\n return { email, password }\n}\n\n/** True when dev auto-login is configured (dev build only) — for the UI marker. */\nexport function isDevAutoLoginActive(): boolean {\n return resolveDevAutoLogin() !== null\n}\n"]}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CmsDocument, CollectionDef } from '@elytracms/core/cms-core';
|
|
1
|
+
import type { CmsDocument, CollectionDef, LocaleConfig } from '@elytracms/core/cms-core';
|
|
2
2
|
import type { ReferenceEntryRecord, ReferenceKind, ReferenceSource, ReferenceTarget } from '@elytracms/core/persistence';
|
|
3
3
|
/**
|
|
4
4
|
* Pure presentation model for the EC-172 references panels.
|
|
@@ -84,6 +84,11 @@ export interface ReferenceLabelContext {
|
|
|
84
84
|
}>;
|
|
85
85
|
/** Resolves a component id to its manifest title (live registry lookup). */
|
|
86
86
|
componentTitle: (componentId: string) => string | undefined;
|
|
87
|
+
/**
|
|
88
|
+
* The project's locale config (EC-580) — document titles read the default-locale
|
|
89
|
+
* view through it. `null` when no CMS session has installed one.
|
|
90
|
+
*/
|
|
91
|
+
locales: LocaleConfig | null;
|
|
87
92
|
}
|
|
88
93
|
/** One fully-labelled panel row. */
|
|
89
94
|
export interface ReferenceEntryView extends MergedReferenceEntry {
|
|
@@ -99,8 +104,17 @@ export interface ReferenceEntryView extends MergedReferenceEntry {
|
|
|
99
104
|
/** Link to the referenced entity's surface. */
|
|
100
105
|
targetLink: ReferenceDeepLink | null;
|
|
101
106
|
}
|
|
102
|
-
/**
|
|
103
|
-
|
|
107
|
+
/**
|
|
108
|
+
* Resolve a document's display title — the shared identity resolver
|
|
109
|
+
* (`resolveDocumentTitle`, EC-242: a string `titleField` or a function pattern) over
|
|
110
|
+
* the document's DEFAULT-locale view (EC-580: `valuesInLocale`, the one locale-value
|
|
111
|
+
* reader; the project's config is the only default locale). A document authored
|
|
112
|
+
* only in another locale has an empty default bucket, so its label is the fallback
|
|
113
|
+
* id — the same thing every other default-locale surface shows (vision 3.5).
|
|
114
|
+
* Without a project locale config (no CMS session installed one) only the shared
|
|
115
|
+
* `values` can be read.
|
|
116
|
+
*/
|
|
117
|
+
export declare function documentTitleOf(collection: CollectionDef | undefined, document: CmsDocument | undefined, fallback: string, locales: LocaleConfig | null): string;
|
|
104
118
|
/** Strip the namespace off a component id for compact labels. */
|
|
105
119
|
export declare function shortComponentId(componentId: string): string;
|
|
106
120
|
/** Label + link one merged entry (see {@link buildReferenceEntryViews}). */
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { resolveDocumentTitle } from '@elytracms/core/cms-core';
|
|
1
|
+
import { resolveDocumentTitle, valuesInLocale } from '@elytracms/core/cms-core';
|
|
2
2
|
import { localizedLabel } from '@elytracms/studio/lib/i18n';
|
|
3
3
|
/**
|
|
4
4
|
* Pure presentation model for the EC-172 references panels.
|
|
@@ -81,29 +81,23 @@ function fieldLabelOf(collection, fieldName) {
|
|
|
81
81
|
function collectionLabelOf(collection, collectionId) {
|
|
82
82
|
return (localizedLabel(collection?.form?.labelSingular) || localizedLabel(collection?.form?.label, collectionId));
|
|
83
83
|
}
|
|
84
|
-
/**
|
|
85
|
-
|
|
84
|
+
/**
|
|
85
|
+
* Resolve a document's display title — the shared identity resolver
|
|
86
|
+
* (`resolveDocumentTitle`, EC-242: a string `titleField` or a function pattern) over
|
|
87
|
+
* the document's DEFAULT-locale view (EC-580: `valuesInLocale`, the one locale-value
|
|
88
|
+
* reader; the project's config is the only default locale). A document authored
|
|
89
|
+
* only in another locale has an empty default bucket, so its label is the fallback
|
|
90
|
+
* id — the same thing every other default-locale surface shows (vision 3.5).
|
|
91
|
+
* Without a project locale config (no CMS session installed one) only the shared
|
|
92
|
+
* `values` can be read.
|
|
93
|
+
*/
|
|
94
|
+
export function documentTitleOf(collection, document, fallback, locales) {
|
|
86
95
|
if (!collection || !document)
|
|
87
96
|
return fallback;
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
}
|
|
93
|
-
const field = typeof collection.titleField === 'string' ? collection.titleField : 'title';
|
|
94
|
-
const direct = document.values[field];
|
|
95
|
-
if (typeof direct === 'string' && direct.length > 0)
|
|
96
|
-
return direct;
|
|
97
|
-
const locales = Object.keys(document.localized ?? {}).sort();
|
|
98
|
-
const preferred = document.defaultLocale;
|
|
99
|
-
for (const locale of [preferred, ...locales]) {
|
|
100
|
-
if (!locale)
|
|
101
|
-
continue;
|
|
102
|
-
const value = document.localized?.[locale]?.[field];
|
|
103
|
-
if (typeof value === 'string' && value.length > 0)
|
|
104
|
-
return value;
|
|
105
|
-
}
|
|
106
|
-
return fallback;
|
|
97
|
+
const values = locales
|
|
98
|
+
? valuesInLocale(collection, document, locales.default, locales)
|
|
99
|
+
: document.values;
|
|
100
|
+
return resolveDocumentTitle(collection, values, fallback);
|
|
107
101
|
}
|
|
108
102
|
/** Strip the namespace off a component id for compact labels. */
|
|
109
103
|
export function shortComponentId(componentId) {
|
|
@@ -118,7 +112,7 @@ function documentParts(context, key) {
|
|
|
118
112
|
collection,
|
|
119
113
|
collectionId: ref.collection,
|
|
120
114
|
documentId: ref.id,
|
|
121
|
-
title: documentTitleOf(collection, context.documents.get(key), ref.id),
|
|
115
|
+
title: documentTitleOf(collection, context.documents.get(key), ref.id, context.locales),
|
|
122
116
|
};
|
|
123
117
|
}
|
|
124
118
|
function describeDocumentSource(context, entry) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reference-model.js","sourceRoot":"","sources":["../../../src/lib/references/reference-model.ts"],"names":[],"mappings":"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","sourcesContent":["import { resolveDocumentTitle } from '@elytracms/core/cms-core'\nimport { localizedLabel } from '@elytracms/studio/lib/i18n'\nimport type { CmsDocument, CollectionDef } from '@elytracms/core/cms-core'\nimport type {\n ReferenceEntryRecord,\n ReferenceKind,\n ReferenceSource,\n ReferenceTarget,\n} from '@elytracms/core/persistence'\n\n/**\n * Pure presentation model for the EC-172 references panels.\n *\n * The EC-155 index stores entries as `(source, path, target, kind)` per\n * variant; this module turns those records into renderable rows: variants\n * merged onto one row (draft/published badges), human-readable source/\n * location/target labels, and deep-link descriptors that pinpoint the exact\n * field (CMS) or graph node (builder) a path identifies. No data fetching, no\n * React — everything here is a deterministic function of its inputs, so it is\n * fully testable in the node environment.\n */\n\n// --- Path parsing -------------------------------------------------------------\n\n/** Split a `collection:id` document key (the index's document source/target id). */\nexport function parseDocumentKey(key: string): { collection: string; id: string } | null {\n const separator = key.indexOf(':')\n if (separator <= 0 || separator === key.length - 1) return null\n return { collection: key.slice(0, separator), id: key.slice(separator + 1) }\n}\n\nexport interface DocumentFieldPath {\n /** Present for `localized/<locale>/<field>` paths. */\n locale?: string\n field: string\n /** Tokens below the field value (array indices, rich-text node paths). */\n rest: readonly string[]\n}\n\n/**\n * Parse a document index path (`values/<field>/…` or\n * `localized/<locale>/<field>/…`) onto the owning field.\n */\nexport function parseDocumentFieldPath(path: string): DocumentFieldPath | null {\n const tokens = path.split('/')\n if (tokens[0] === 'values' && tokens[1]) {\n return { field: tokens[1], rest: tokens.slice(2) }\n }\n if (tokens[0] === 'localized' && tokens[1] && tokens[2]) {\n return { locale: tokens[1], field: tokens[2], rest: tokens.slice(3) }\n }\n return null\n}\n\n// --- Variant merging ----------------------------------------------------------\n\n/** One panel row: the same logical entry across both variants, with flags. */\nexport interface MergedReferenceEntry {\n /** Stable row key (source + path + target + kind). */\n key: string\n source: ReferenceSource\n path: string\n target: ReferenceTarget\n kind: ReferenceKind\n /** Present in the draft variant of the index. */\n draft: boolean\n /** Present in the published variant (a live reference). */\n published: boolean\n}\n\n/**\n * Merge per-variant index records onto one row per logical entry. A\n * draft-only row (`draft && !published`) is the Sanity hidden-draft-reference\n * case the panel must make visible. Order is deterministic:\n * source type → source id → path → target type → target id → kind.\n */\nexport function mergeReferenceVariants(\n records: readonly ReferenceEntryRecord[],\n): MergedReferenceEntry[] {\n const byKey = new Map<string, MergedReferenceEntry>()\n for (const record of records) {\n const key = `${record.source.type} ${record.source.id} ${record.path} ${record.target.type} ${record.target.id} ${record.kind}`\n const existing = byKey.get(key)\n if (existing) {\n if (record.variant === 'draft') existing.draft = true\n else existing.published = true\n continue\n }\n byKey.set(key, {\n key,\n source: record.source,\n path: record.path,\n target: record.target,\n kind: record.kind,\n draft: record.variant === 'draft',\n published: record.variant === 'published',\n })\n }\n return [...byKey.values()].sort((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0))\n}\n\n// --- Labels & deep links --------------------------------------------------------\n\n/** Human label per reference kind (entry-row badge). */\nexport const REFERENCE_KIND_LABELS: Record<ReferenceKind, string> = {\n 'relation-field': 'relation',\n 'asset-field': 'asset field',\n 'rich-text-link': 'rich-text link',\n 'rich-text-node': 'rich-text node',\n 'component-usage': 'component usage',\n}\n\n/**\n * A deep-link descriptor the panel renders as a router link. `cms-field`\n * carries the full index path so the document editor can highlight the exact\n * field control; `builder-node` carries the resolved node id so the builder\n * can select + scroll to the node.\n */\nexport type ReferenceDeepLink =\n | { kind: 'cms-field'; collectionId: string; documentId: string; fieldPath?: string }\n | { kind: 'builder-node'; pageId?: string; nodeId?: string }\n | { kind: 'component'; componentId: string }\n | { kind: 'asset'; assetId: string }\n\n/** Everything the model needs to label entries (loaded once per panel). */\nexport interface ReferenceLabelContext {\n collections: readonly CollectionDef[]\n /** Live documents keyed by `collection:id`. */\n documents: ReadonlyMap<string, CmsDocument>\n /** Asset metadata keyed by asset id. */\n assets: ReadonlyMap<string, { filename: string; title?: string | undefined }>\n /** Resolves a component id to its manifest title (live registry lookup). */\n componentTitle: (componentId: string) => string | undefined\n}\n\n/** One fully-labelled panel row. */\nexport interface ReferenceEntryView extends MergedReferenceEntry {\n kindLabel: string\n /** The holding entity, e.g. `Post · Hello World` or `Page · Home`. */\n sourceLabel: string\n /** The exact location inside the source, e.g. `Author` or `Text (n-post-title)`. */\n locationLabel: string\n /** The referenced entity, e.g. `Ada Lovelace` or `cover.jpg`. */\n targetLabel: string\n /** Deep link to the exact source location (field control / graph node). */\n sourceLink: ReferenceDeepLink | null\n /** Link to the referenced entity's surface. */\n targetLink: ReferenceDeepLink | null\n}\n\nfunction fieldLabelOf(collection: CollectionDef | undefined, fieldName: string): string {\n const field = collection?.fields.find((f) => f.name === fieldName)\n // Non-hook `localizedLabel` reads the active UI locale (safe anywhere), so a\n // `{ en, de }` label resolves to the editor's language here too.\n return localizedLabel(field?.form?.label, fieldName)\n}\n\nfunction collectionLabelOf(collection: CollectionDef | undefined, collectionId: string): string {\n return (\n localizedLabel(collection?.form?.labelSingular) || localizedLabel(collection?.form?.label, collectionId)\n )\n}\n\n/** Resolve a document's display title (titleField value, default locale first). */\nexport function documentTitleOf(\n collection: CollectionDef | undefined,\n document: CmsDocument | undefined,\n fallback: string,\n): string {\n if (!collection || !document) return fallback\n // EC-242: a function `titleField` (title pattern) has no single source field —\n // resolve it from the base values via the shared identity resolver.\n if (typeof collection.titleField === 'function') {\n return resolveDocumentTitle(collection, document.values, fallback)\n }\n const field = typeof collection.titleField === 'string' ? collection.titleField : 'title'\n const direct = document.values[field]\n if (typeof direct === 'string' && direct.length > 0) return direct\n const locales = Object.keys(document.localized ?? {}).sort()\n const preferred = document.defaultLocale\n for (const locale of [preferred, ...locales]) {\n if (!locale) continue\n const value = document.localized?.[locale]?.[field]\n if (typeof value === 'string' && value.length > 0) return value\n }\n return fallback\n}\n\n/** Strip the namespace off a component id for compact labels. */\nexport function shortComponentId(componentId: string): string {\n return componentId.replace('base.', '').replace('project.', '')\n}\n\ninterface DocumentParts {\n collection: CollectionDef | undefined\n collectionId: string\n documentId: string\n title: string\n}\n\nfunction documentParts(context: ReferenceLabelContext, key: string): DocumentParts | null {\n const ref = parseDocumentKey(key)\n if (!ref) return null\n const collection = context.collections.find((c) => c.id === ref.collection)\n return {\n collection,\n collectionId: ref.collection,\n documentId: ref.id,\n title: documentTitleOf(collection, context.documents.get(key), ref.id),\n }\n}\n\nfunction describeDocumentSource(\n context: ReferenceLabelContext,\n entry: MergedReferenceEntry,\n): { sourceLabel: string; locationLabel: string; sourceLink: ReferenceDeepLink | null } {\n const parts = documentParts(context, entry.source.id)\n if (!parts) {\n return { sourceLabel: entry.source.id, locationLabel: entry.path, sourceLink: null }\n }\n const parsed = parseDocumentFieldPath(entry.path)\n const fieldLabel = parsed ? fieldLabelOf(parts.collection, parsed.field) : entry.path\n const qualifiers: string[] = []\n if (parsed?.locale) qualifiers.push(parsed.locale)\n if (entry.kind === 'rich-text-link') qualifiers.push('link')\n if (entry.kind === 'rich-text-node') qualifiers.push('embedded node')\n if (\n entry.kind === 'relation-field' &&\n parsed &&\n parsed.rest.length === 1 &&\n /^\\d+$/.test(parsed.rest[0]!)\n ) {\n qualifiers.push(`item ${parsed.rest[0]}`)\n }\n return {\n sourceLabel: `${collectionLabelOf(parts.collection, parts.collectionId)} · ${parts.title}`,\n locationLabel: qualifiers.length > 0 ? `${fieldLabel} (${qualifiers.join(', ')})` : fieldLabel,\n sourceLink: {\n kind: 'cms-field',\n collectionId: parts.collectionId,\n documentId: parts.documentId,\n fieldPath: entry.path,\n },\n }\n}\n\n\nfunction describeTarget(\n context: ReferenceLabelContext,\n target: ReferenceTarget,\n): { targetLabel: string; targetLink: ReferenceDeepLink | null } {\n switch (target.type) {\n case 'document': {\n const parts = documentParts(context, target.id)\n if (!parts) return { targetLabel: target.id, targetLink: null }\n return {\n targetLabel: `${collectionLabelOf(parts.collection, parts.collectionId)} · ${parts.title}`,\n targetLink: {\n kind: 'cms-field',\n collectionId: parts.collectionId,\n documentId: parts.documentId,\n },\n }\n }\n case 'component':\n return {\n targetLabel: context.componentTitle(target.id) ?? shortComponentId(target.id),\n targetLink: { kind: 'component', componentId: target.id },\n }\n case 'asset': {\n const asset = context.assets.get(target.id)\n return {\n targetLabel: asset ? (asset.title ?? asset.filename) : target.id,\n targetLink: { kind: 'asset', assetId: target.id },\n }\n }\n }\n}\n\n/** Label + link one merged entry (see {@link buildReferenceEntryViews}). */\nexport function buildReferenceEntryView(\n entry: MergedReferenceEntry,\n context: ReferenceLabelContext,\n): ReferenceEntryView {\n // EC-287: a reference source is only ever a CMS document (structure is code).\n const sourceParts = describeDocumentSource(context, entry)\n return {\n ...entry,\n kindLabel: REFERENCE_KIND_LABELS[entry.kind],\n ...sourceParts,\n ...describeTarget(context, entry.target),\n }\n}\n\n/** Merge variants and label every record — the panel's full render model. */\nexport function buildReferenceEntryViews(\n records: readonly ReferenceEntryRecord[],\n context: ReferenceLabelContext,\n): ReferenceEntryView[] {\n return mergeReferenceVariants(records).map((entry) => buildReferenceEntryView(entry, context))\n}\n"]}
|
|
1
|
+
{"version":3,"file":"reference-model.js","sourceRoot":"","sources":["../../../src/lib/references/reference-model.ts"],"names":[],"mappings":"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","sourcesContent":["import { resolveDocumentTitle, valuesInLocale } from '@elytracms/core/cms-core'\nimport { localizedLabel } from '@elytracms/studio/lib/i18n'\nimport type { CmsDocument, CollectionDef, LocaleConfig } from '@elytracms/core/cms-core'\nimport type {\n ReferenceEntryRecord,\n ReferenceKind,\n ReferenceSource,\n ReferenceTarget,\n} from '@elytracms/core/persistence'\n\n/**\n * Pure presentation model for the EC-172 references panels.\n *\n * The EC-155 index stores entries as `(source, path, target, kind)` per\n * variant; this module turns those records into renderable rows: variants\n * merged onto one row (draft/published badges), human-readable source/\n * location/target labels, and deep-link descriptors that pinpoint the exact\n * field (CMS) or graph node (builder) a path identifies. No data fetching, no\n * React — everything here is a deterministic function of its inputs, so it is\n * fully testable in the node environment.\n */\n\n// --- Path parsing -------------------------------------------------------------\n\n/** Split a `collection:id` document key (the index's document source/target id). */\nexport function parseDocumentKey(key: string): { collection: string; id: string } | null {\n const separator = key.indexOf(':')\n if (separator <= 0 || separator === key.length - 1) return null\n return { collection: key.slice(0, separator), id: key.slice(separator + 1) }\n}\n\nexport interface DocumentFieldPath {\n /** Present for `localized/<locale>/<field>` paths. */\n locale?: string\n field: string\n /** Tokens below the field value (array indices, rich-text node paths). */\n rest: readonly string[]\n}\n\n/**\n * Parse a document index path (`values/<field>/…` or\n * `localized/<locale>/<field>/…`) onto the owning field.\n */\nexport function parseDocumentFieldPath(path: string): DocumentFieldPath | null {\n const tokens = path.split('/')\n if (tokens[0] === 'values' && tokens[1]) {\n return { field: tokens[1], rest: tokens.slice(2) }\n }\n if (tokens[0] === 'localized' && tokens[1] && tokens[2]) {\n return { locale: tokens[1], field: tokens[2], rest: tokens.slice(3) }\n }\n return null\n}\n\n// --- Variant merging ----------------------------------------------------------\n\n/** One panel row: the same logical entry across both variants, with flags. */\nexport interface MergedReferenceEntry {\n /** Stable row key (source + path + target + kind). */\n key: string\n source: ReferenceSource\n path: string\n target: ReferenceTarget\n kind: ReferenceKind\n /** Present in the draft variant of the index. */\n draft: boolean\n /** Present in the published variant (a live reference). */\n published: boolean\n}\n\n/**\n * Merge per-variant index records onto one row per logical entry. A\n * draft-only row (`draft && !published`) is the Sanity hidden-draft-reference\n * case the panel must make visible. Order is deterministic:\n * source type → source id → path → target type → target id → kind.\n */\nexport function mergeReferenceVariants(\n records: readonly ReferenceEntryRecord[],\n): MergedReferenceEntry[] {\n const byKey = new Map<string, MergedReferenceEntry>()\n for (const record of records) {\n const key = `${record.source.type} ${record.source.id} ${record.path} ${record.target.type} ${record.target.id} ${record.kind}`\n const existing = byKey.get(key)\n if (existing) {\n if (record.variant === 'draft') existing.draft = true\n else existing.published = true\n continue\n }\n byKey.set(key, {\n key,\n source: record.source,\n path: record.path,\n target: record.target,\n kind: record.kind,\n draft: record.variant === 'draft',\n published: record.variant === 'published',\n })\n }\n return [...byKey.values()].sort((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0))\n}\n\n// --- Labels & deep links --------------------------------------------------------\n\n/** Human label per reference kind (entry-row badge). */\nexport const REFERENCE_KIND_LABELS: Record<ReferenceKind, string> = {\n 'relation-field': 'relation',\n 'asset-field': 'asset field',\n 'rich-text-link': 'rich-text link',\n 'rich-text-node': 'rich-text node',\n 'component-usage': 'component usage',\n}\n\n/**\n * A deep-link descriptor the panel renders as a router link. `cms-field`\n * carries the full index path so the document editor can highlight the exact\n * field control; `builder-node` carries the resolved node id so the builder\n * can select + scroll to the node.\n */\nexport type ReferenceDeepLink =\n | { kind: 'cms-field'; collectionId: string; documentId: string; fieldPath?: string }\n | { kind: 'builder-node'; pageId?: string; nodeId?: string }\n | { kind: 'component'; componentId: string }\n | { kind: 'asset'; assetId: string }\n\n/** Everything the model needs to label entries (loaded once per panel). */\nexport interface ReferenceLabelContext {\n collections: readonly CollectionDef[]\n /** Live documents keyed by `collection:id`. */\n documents: ReadonlyMap<string, CmsDocument>\n /** Asset metadata keyed by asset id. */\n assets: ReadonlyMap<string, { filename: string; title?: string | undefined }>\n /** Resolves a component id to its manifest title (live registry lookup). */\n componentTitle: (componentId: string) => string | undefined\n /**\n * The project's locale config (EC-580) — document titles read the default-locale\n * view through it. `null` when no CMS session has installed one.\n */\n locales: LocaleConfig | null\n}\n\n/** One fully-labelled panel row. */\nexport interface ReferenceEntryView extends MergedReferenceEntry {\n kindLabel: string\n /** The holding entity, e.g. `Post · Hello World` or `Page · Home`. */\n sourceLabel: string\n /** The exact location inside the source, e.g. `Author` or `Text (n-post-title)`. */\n locationLabel: string\n /** The referenced entity, e.g. `Ada Lovelace` or `cover.jpg`. */\n targetLabel: string\n /** Deep link to the exact source location (field control / graph node). */\n sourceLink: ReferenceDeepLink | null\n /** Link to the referenced entity's surface. */\n targetLink: ReferenceDeepLink | null\n}\n\nfunction fieldLabelOf(collection: CollectionDef | undefined, fieldName: string): string {\n const field = collection?.fields.find((f) => f.name === fieldName)\n // Non-hook `localizedLabel` reads the active UI locale (safe anywhere), so a\n // `{ en, de }` label resolves to the editor's language here too.\n return localizedLabel(field?.form?.label, fieldName)\n}\n\nfunction collectionLabelOf(collection: CollectionDef | undefined, collectionId: string): string {\n return (\n localizedLabel(collection?.form?.labelSingular) || localizedLabel(collection?.form?.label, collectionId)\n )\n}\n\n/**\n * Resolve a document's display title — the shared identity resolver\n * (`resolveDocumentTitle`, EC-242: a string `titleField` or a function pattern) over\n * the document's DEFAULT-locale view (EC-580: `valuesInLocale`, the one locale-value\n * reader; the project's config is the only default locale). A document authored\n * only in another locale has an empty default bucket, so its label is the fallback\n * id — the same thing every other default-locale surface shows (vision 3.5).\n * Without a project locale config (no CMS session installed one) only the shared\n * `values` can be read.\n */\nexport function documentTitleOf(\n collection: CollectionDef | undefined,\n document: CmsDocument | undefined,\n fallback: string,\n locales: LocaleConfig | null,\n): string {\n if (!collection || !document) return fallback\n const values = locales\n ? valuesInLocale(collection, document, locales.default, locales)\n : document.values\n return resolveDocumentTitle(collection, values, fallback)\n}\n\n/** Strip the namespace off a component id for compact labels. */\nexport function shortComponentId(componentId: string): string {\n return componentId.replace('base.', '').replace('project.', '')\n}\n\ninterface DocumentParts {\n collection: CollectionDef | undefined\n collectionId: string\n documentId: string\n title: string\n}\n\nfunction documentParts(context: ReferenceLabelContext, key: string): DocumentParts | null {\n const ref = parseDocumentKey(key)\n if (!ref) return null\n const collection = context.collections.find((c) => c.id === ref.collection)\n return {\n collection,\n collectionId: ref.collection,\n documentId: ref.id,\n title: documentTitleOf(collection, context.documents.get(key), ref.id, context.locales),\n }\n}\n\nfunction describeDocumentSource(\n context: ReferenceLabelContext,\n entry: MergedReferenceEntry,\n): { sourceLabel: string; locationLabel: string; sourceLink: ReferenceDeepLink | null } {\n const parts = documentParts(context, entry.source.id)\n if (!parts) {\n return { sourceLabel: entry.source.id, locationLabel: entry.path, sourceLink: null }\n }\n const parsed = parseDocumentFieldPath(entry.path)\n const fieldLabel = parsed ? fieldLabelOf(parts.collection, parsed.field) : entry.path\n const qualifiers: string[] = []\n if (parsed?.locale) qualifiers.push(parsed.locale)\n if (entry.kind === 'rich-text-link') qualifiers.push('link')\n if (entry.kind === 'rich-text-node') qualifiers.push('embedded node')\n if (\n entry.kind === 'relation-field' &&\n parsed &&\n parsed.rest.length === 1 &&\n /^\\d+$/.test(parsed.rest[0]!)\n ) {\n qualifiers.push(`item ${parsed.rest[0]}`)\n }\n return {\n sourceLabel: `${collectionLabelOf(parts.collection, parts.collectionId)} · ${parts.title}`,\n locationLabel: qualifiers.length > 0 ? `${fieldLabel} (${qualifiers.join(', ')})` : fieldLabel,\n sourceLink: {\n kind: 'cms-field',\n collectionId: parts.collectionId,\n documentId: parts.documentId,\n fieldPath: entry.path,\n },\n }\n}\n\n\nfunction describeTarget(\n context: ReferenceLabelContext,\n target: ReferenceTarget,\n): { targetLabel: string; targetLink: ReferenceDeepLink | null } {\n switch (target.type) {\n case 'document': {\n const parts = documentParts(context, target.id)\n if (!parts) return { targetLabel: target.id, targetLink: null }\n return {\n targetLabel: `${collectionLabelOf(parts.collection, parts.collectionId)} · ${parts.title}`,\n targetLink: {\n kind: 'cms-field',\n collectionId: parts.collectionId,\n documentId: parts.documentId,\n },\n }\n }\n case 'component':\n return {\n targetLabel: context.componentTitle(target.id) ?? shortComponentId(target.id),\n targetLink: { kind: 'component', componentId: target.id },\n }\n case 'asset': {\n const asset = context.assets.get(target.id)\n return {\n targetLabel: asset ? (asset.title ?? asset.filename) : target.id,\n targetLink: { kind: 'asset', assetId: target.id },\n }\n }\n }\n}\n\n/** Label + link one merged entry (see {@link buildReferenceEntryViews}). */\nexport function buildReferenceEntryView(\n entry: MergedReferenceEntry,\n context: ReferenceLabelContext,\n): ReferenceEntryView {\n // EC-287: a reference source is only ever a CMS document (structure is code).\n const sourceParts = describeDocumentSource(context, entry)\n return {\n ...entry,\n kindLabel: REFERENCE_KIND_LABELS[entry.kind],\n ...sourceParts,\n ...describeTarget(context, entry.target),\n }\n}\n\n/** Merge variants and label every record — the panel's full render model. */\nexport function buildReferenceEntryViews(\n records: readonly ReferenceEntryRecord[],\n context: ReferenceLabelContext,\n): ReferenceEntryView[] {\n return mergeReferenceVariants(records).map((entry) => buildReferenceEntryView(entry, context))\n}\n"]}
|
|
@@ -2,6 +2,8 @@ import { useMemo } from 'react';
|
|
|
2
2
|
import { documentKey } from '@elytracms/core/cms-core';
|
|
3
3
|
import { useOperationQuery } from '@elytracms/studio/lib/query';
|
|
4
4
|
import { localizedLabel } from '../i18n/index.js';
|
|
5
|
+
import { getStudioLocaleConfig } from '../locale/locale-store.js';
|
|
6
|
+
import { resolveDeploymentLocaleConfig } from '../workspace/index.js';
|
|
5
7
|
import { getStudioOperations } from '../operations/index.js';
|
|
6
8
|
import { getStudioRegistry } from '../registry/index.js';
|
|
7
9
|
import { buildReferenceEntryViews } from './reference-model.js';
|
|
@@ -75,6 +77,11 @@ async function loadLabelContext(operations, projectId) {
|
|
|
75
77
|
collections,
|
|
76
78
|
documents,
|
|
77
79
|
assets,
|
|
80
|
+
// EC-580: titles read the default-locale view of the project's ONE locale
|
|
81
|
+
// config — the one the CMS session installed (`configureStudioLocales`), else
|
|
82
|
+
// (a rail opened before any CMS session, e.g. Assets) the active config
|
|
83
|
+
// project's, which is where the session's value comes from in the first place.
|
|
84
|
+
locales: getStudioLocaleConfig() ?? resolveDeploymentLocaleConfig() ?? null,
|
|
78
85
|
// EC-407: `title` is a LocalizedString — resolve it to the active UI locale
|
|
79
86
|
// (a plain string is unchanged), never leak a `{ en, de }` object to the label.
|
|
80
87
|
componentTitle: (componentId) => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"references-store.js","sourceRoot":"","sources":["../../../src/lib/references/references-store.ts"],"names":[],"mappings":"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","sourcesContent":["import { useMemo } from 'react'\nimport { documentKey } from '@elytracms/core/cms-core'\nimport { useOperationQuery } from '@elytracms/studio/lib/query'\nimport type { CmsDocument, DocumentRef } from '@elytracms/core/cms-core'\nimport type {\n ReferenceEntryRecord,\n ReferenceSource,\n ReferenceTarget,\n} from '@elytracms/core/persistence'\nimport { localizedLabel } from '../i18n'\nimport { getStudioOperations } from '../operations'\nimport type { OperationEvent, OperationsClient, StudioOperations } from '../operations'\nimport { getStudioRegistry } from '../registry'\nimport { buildReferenceEntryViews } from './reference-model'\nimport type { ReferenceEntryView, ReferenceLabelContext } from './reference-model'\n\n/**\n * Data layer for the EC-172 references panels. All reads go through\n * `@elytracms/core/operations` queries (`references.outbound` / `references.inbound`\n * plus the labeling queries) — UI code never touches persistence repositories\n * or index tables. The hook keeps panels live: any succeeded command in a\n * content-bearing domain re-queries after the EC-155 maintained-on-write\n * attachment has flushed.\n */\n\nexport interface ReferencePanelQuery {\n projectId: string\n /** Enables the outbound (\"references\") section. */\n source?: ReferenceSource | undefined\n /** Enables the inbound (\"used by\") section. */\n target?: ReferenceTarget | undefined\n}\n\nexport interface ReferencePanelData {\n outbound: ReferenceEntryView[]\n inbound: ReferenceEntryView[]\n /**\n * Per-direction read error (EC-205): a direction whose query returns a\n * structured non-ok degrades to an empty list plus this message, surfaced as a\n * visible per-section error — instead of collapsing the whole panel into its\n * hard-error state. `null` when the direction loaded (or was not requested).\n */\n outboundError: string | null\n inboundError: string | null\n}\n\n// --- Index bootstrap -----------------------------------------------------------\n\n/**\n * The EC-155 index is maintained on write, but seeded/imported projects start\n * with an empty index. {@link ensureReferenceIndex} seeds it once per project per\n * operations client; failures (e.g. a viewer's command denial) degrade to\n * whatever the maintained-on-write index already holds.\n */\nconst backfillsByClient = new WeakMap<StudioOperations, Map<string, Promise<void>>>()\n\n/**\n * Seed the index once if it has never been backfilled, then never again — the\n * EC-269 mutation-free read path. The index is maintained on write, so the\n * (clear + rebuild) `backfill` is only needed to seed a never-built index. A\n * persisted marker (`references.buildMarker`, a read) records completion; when\n * present this returns after a single query with ZERO mutations — so opening the\n * References panel no longer rewrites the whole index every session (the normal\n * Convex case: the EC-233 import already backfilled). Only a genuinely empty/\n * never-built project still pays the one-time backfill (and a viewer who cannot\n * run it degrades to the maintained-on-write index, as before).\n */\nasync function seedReferenceIndex(studio: StudioOperations, projectId: string): Promise<void> {\n try {\n const marker = await studio.operations.references.buildMarker({ projectId })\n if (marker.ok && marker.value !== null) return\n } catch {\n // A marker-read failure falls through to the backfill (best-effort seed).\n }\n await studio.operations.references.backfill({ projectId }).then(\n () => undefined,\n () => undefined,\n )\n}\n\nexport function ensureReferenceIndex(studio: StudioOperations, projectId: string): Promise<void> {\n let byProject = backfillsByClient.get(studio)\n if (!byProject) {\n byProject = new Map()\n backfillsByClient.set(studio, byProject)\n }\n let pending = byProject.get(projectId)\n if (!pending) {\n pending = seedReferenceIndex(studio, projectId)\n byProject.set(projectId, pending)\n }\n return pending\n}\n\n// --- Label context loading -------------------------------------------------------\n\nasync function loadLabelContext(\n operations: OperationsClient,\n projectId: string,\n): Promise<ReferenceLabelContext> {\n const [schemaResult, documentsResult, assetsResult] = await Promise.all([\n operations.schema.get({ projectId }),\n operations.documents.list({ projectId }),\n operations.assets.list({ projectId }),\n ])\n\n const collections = schemaResult.ok ? schemaResult.value.collections : []\n const documents = new Map<string, CmsDocument>()\n if (documentsResult.ok) {\n for (const record of documentsResult.value) {\n documents.set(\n documentKey({ collection: record.document.collection, id: record.document.id }),\n record.document,\n )\n }\n }\n // EC-287: the project graph is retired; reference source labels resolve from\n // CMS documents only. `page`/`layout` graph sources no longer exist.\n const assets = new Map<string, { filename: string; title?: string | undefined }>()\n if (assetsResult.ok) {\n for (const asset of assetsResult.value) {\n assets.set(asset.id, { filename: asset.filename, title: asset.title })\n }\n }\n const registry = getStudioRegistry().registry\n return {\n collections,\n documents,\n assets,\n // EC-407: `title` is a LocalizedString — resolve it to the active UI locale\n // (a plain string is unchanged), never leak a `{ en, de }` object to the label.\n componentTitle: (componentId) => {\n const title = registry.getManifest(componentId)?.title\n return title === undefined ? undefined : localizedLabel(title) || undefined\n },\n }\n}\n\n// --- Panel data loading -----------------------------------------------------------\n\n/** Load both directions + labels for one panel (operations queries only). */\nexport async function loadReferencePanelData(\n query: ReferencePanelQuery,\n): Promise<ReferencePanelData> {\n const studio = await getStudioOperations()\n await ensureReferenceIndex(studio, query.projectId)\n // Observe a settled index: maintained-on-write updates run async after events.\n await studio.references.flush()\n const operations = studio.operations\n\n // EC-205: load each direction INDEPENDENTLY. A structured non-ok from one\n // direction is a degradable operation result (it carries an OperationError —\n // the documented contract), not an exception: degrade that direction to empty\n // and surface its error per-section (references-panel) while the other still\n // renders, instead of throwing the whole panel into its hard-error state. A\n // genuinely broken backend still throws from getStudioOperations/flush above.\n const [outbound, inbound] = await Promise.all([\n query.source\n ? operations.references.outbound({ projectId: query.projectId, source: query.source })\n : null,\n query.target\n ? operations.references.inbound({ projectId: query.projectId, target: query.target })\n : null,\n ])\n\n type DirectionResult = Awaited<ReturnType<typeof operations.references.outbound>>\n const settle = (\n result: DirectionResult | null,\n ): { records: ReferenceEntryRecord[]; error: string | null } =>\n result === null\n ? { records: [], error: null }\n : result.ok\n ? { records: result.value, error: null }\n : { records: [], error: result.error.message }\n const out = settle(outbound)\n const inb = settle(inbound)\n\n // EC-302: nothing referenced in either direction → no sources/targets to label,\n // so skip the label-context load entirely (schema + ALL documents + ALL assets).\n // Common for leaf content; mirrors the asset short-circuit in\n // `loadInboundAssetReferenceViews`.\n if (out.records.length === 0 && inb.records.length === 0) {\n return { outbound: [], inbound: [], outboundError: out.error, inboundError: inb.error }\n }\n\n const context = await loadLabelContext(operations, query.projectId)\n return {\n outbound: buildReferenceEntryViews(out.records, context),\n inbound: buildReferenceEntryViews(inb.records, context),\n outboundError: out.error,\n inboundError: inb.error,\n }\n}\n\n// --- Impact loading (EC-173 safe unpublish/delete) -------------------------------\n\n/**\n * Load the labelled inbound entry views for a set of target documents in ONE\n * pass (one index flush, one label-context load) — the pre-flight data of the\n * EC-173 confirmation dialogs, including the bulk flows. Returns entries in\n * `targets` order; pair with `buildReferenceImpact` for break/block semantics.\n *\n * `toTarget` maps each acted-on ref to its reference-index target shape\n * (EC-433): the default is the document shape every `cmsDocuments` collection\n * indexes under; a rail whose records live under a different target type (the\n * assets rail, `{ type: 'asset' }`) supplies its own via\n * `CollectionViewSource.referenceTarget` — otherwise its pre-flight would\n * query the wrong shape and report referenced records as \"not referenced\".\n */\nexport async function loadInboundReferenceViews(\n projectId: string,\n targets: readonly DocumentRef[],\n toTarget: (ref: DocumentRef) => ReferenceTarget = (ref) => ({\n type: 'document',\n id: documentKey(ref),\n }),\n): Promise<{ ref: DocumentRef; inbound: ReferenceEntryView[] }[]> {\n const studio = await getStudioOperations()\n await ensureReferenceIndex(studio, projectId)\n // Observe a settled index: maintained-on-write updates run async after events.\n await studio.references.flush()\n const operations = studio.operations\n\n // EC-308: one batched query for ALL targets (the operation loops the by_target\n // index server-side) instead of a per-target `inbound` fan-out. Returns one\n // record list per target, in `targets` order.\n const recordsPerTarget = await operations.references\n .inboundMany({\n projectId,\n targets: targets.map((ref) => toTarget(ref)),\n })\n .then((result) => {\n if (!result.ok) throw new Error(result.error.message)\n return result.value\n })\n // EC-302: no inbound refs for ANY target → no sources to label, so skip the\n // label-context load (schema + ALL documents + ALL assets). The common case when\n // deleting/unpublishing unreferenced documents (incl. bulk); mirrors the EC-301\n // short-circuit already in `loadInboundAssetReferenceViews`.\n if (recordsPerTarget.every((records) => records.length === 0)) {\n return targets.map((ref) => ({ ref, inbound: [] }))\n }\n const context = await loadLabelContext(operations, projectId)\n return targets.map((ref, index) => ({\n ref,\n inbound: buildReferenceEntryViews(recordsPerTarget[index] ?? [], context),\n }))\n}\n\n/**\n * Inbound reference views for ONE asset (EC-202): the \"what uses this asset\"\n * pre-flight of the asset-delete dialog, index-accurate (the EC-155 reference\n * index, not the retired `find-asset-usages` string scan). Mirrors\n * {@link loadInboundReferenceViews} for a single `{ type: 'asset' }` target —\n * the source labels resolve the referencing documents/graph-nodes identically.\n */\nexport async function loadInboundAssetReferenceViews(\n projectId: string,\n assetId: string,\n): Promise<ReferenceEntryView[]> {\n const studio = await getStudioOperations()\n await ensureReferenceIndex(studio, projectId)\n // Observe a settled index: maintained-on-write updates run async after events.\n await studio.references.flush()\n const operations = studio.operations\n const result = await operations.references.inbound({\n projectId,\n target: { type: 'asset', id: assetId },\n })\n if (!result.ok) throw new Error(result.error.message)\n // No inbound references → no sources to label, so skip the label-context load\n // entirely (schema + ALL documents + ALL assets). This is the common case for a\n // deletable asset, making the delete dialog's pre-flight a single indexed query\n // instead of three full-table lists (EC-301 perf).\n if (result.value.length === 0) return []\n const context = await loadLabelContext(operations, projectId)\n return buildReferenceEntryViews(result.value, context)\n}\n\n// --- React hook ----------------------------------------------------------------\n\n/**\n * Domains whose successful commands can change the reference index or labels and\n * should refresh an open panel. `references` is deliberately ABSENT (EC-269):\n * reacting to reference-index command events (the `backfill`) would make the\n * panel re-trigger on its OWN index maintenance — a self-cascade that, compounded\n * with the per-keystroke `documents` events, produced hundreds of redundant\n * reloads per document. The index is maintained on write; the panel reacts to the\n * CONTENT domains that feed it, never to its own index writes.\n */\nconst REFERENCE_REFRESH_TRIGGERS = [\n 'documents',\n 'graph',\n 'publishing',\n 'assets',\n 'schema',\n] as const\n\nconst REFRESH_DOMAINS: ReadonlySet<string> = new Set(REFERENCE_REFRESH_TRIGGERS)\n\n/**\n * Whether one operation event should refresh an open reference panel. Exported\n * for tests — pins the EC-269 contract that reference-index commands (and any\n * non-succeeded / non-command event) never trigger a reload.\n */\nexport function isReferenceRefreshEvent(event: OperationEvent): boolean {\n return (\n event.kind === 'command' && event.status === 'succeeded' && REFRESH_DOMAINS.has(event.domain)\n )\n}\n\nexport type ReferencePanelStatus = 'loading' | 'ready' | 'error'\n\nexport interface ReferencePanelState {\n status: ReferencePanelStatus\n data: ReferencePanelData | null\n errorMessage: string | null\n refresh: () => void\n}\n\n/**\n * Live panel state for one entity (EC-308/AD-17): loads + caches through the one\n * `useOperationQuery` seam, and re-queries whenever a content-bearing command\n * succeeds anywhere in the studio (the index is maintained on write). Per-direction\n * read errors degrade INSIDE `loadReferencePanelData` (carried on the data); a hard\n * failure (broken backend) surfaces as the query's error → `status: 'error'`.\n */\nexport function useReferencePanel(query: ReferencePanelQuery): ReferencePanelState {\n const { projectId } = query\n const sourceType = query.source?.type\n const sourceId = query.source?.id\n const targetType = query.target?.type\n const targetId = query.target?.id\n\n const stableQuery = useMemo<ReferencePanelQuery>(\n () => ({\n projectId,\n ...(sourceType && sourceId ? { source: { type: sourceType, id: sourceId } } : {}),\n ...(targetType && targetId ? { target: { type: targetType, id: targetId } } : {}),\n }),\n [projectId, sourceType, sourceId, targetType, targetId],\n )\n\n const result = useOperationQuery<ReferencePanelData>({\n queryKey: [\n 'referencePanel',\n projectId,\n sourceType ?? '',\n sourceId ?? '',\n targetType ?? '',\n targetId ?? '',\n ],\n loader: () => loadReferencePanelData(stableQuery),\n invalidateOn: REFERENCE_REFRESH_TRIGGERS,\n })\n\n const status: ReferencePanelStatus = result.isPending\n ? 'loading'\n : result.isError\n ? 'error'\n : 'ready'\n return {\n status,\n data: result.data ?? null,\n errorMessage: result.error instanceof Error ? result.error.message : null,\n refresh: () => void result.refetch(),\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"references-store.js","sourceRoot":"","sources":["../../../src/lib/references/references-store.ts"],"names":[],"mappings":"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","sourcesContent":["import { useMemo } from 'react'\nimport { documentKey } from '@elytracms/core/cms-core'\nimport { useOperationQuery } from '@elytracms/studio/lib/query'\nimport type { CmsDocument, DocumentRef } from '@elytracms/core/cms-core'\nimport type {\n ReferenceEntryRecord,\n ReferenceSource,\n ReferenceTarget,\n} from '@elytracms/core/persistence'\nimport { localizedLabel } from '../i18n'\nimport { getStudioLocaleConfig } from '../locale/locale-store'\nimport { resolveDeploymentLocaleConfig } from '../workspace'\nimport { getStudioOperations } from '../operations'\nimport type { OperationEvent, OperationsClient, StudioOperations } from '../operations'\nimport { getStudioRegistry } from '../registry'\nimport { buildReferenceEntryViews } from './reference-model'\nimport type { ReferenceEntryView, ReferenceLabelContext } from './reference-model'\n\n/**\n * Data layer for the EC-172 references panels. All reads go through\n * `@elytracms/core/operations` queries (`references.outbound` / `references.inbound`\n * plus the labeling queries) — UI code never touches persistence repositories\n * or index tables. The hook keeps panels live: any succeeded command in a\n * content-bearing domain re-queries after the EC-155 maintained-on-write\n * attachment has flushed.\n */\n\nexport interface ReferencePanelQuery {\n projectId: string\n /** Enables the outbound (\"references\") section. */\n source?: ReferenceSource | undefined\n /** Enables the inbound (\"used by\") section. */\n target?: ReferenceTarget | undefined\n}\n\nexport interface ReferencePanelData {\n outbound: ReferenceEntryView[]\n inbound: ReferenceEntryView[]\n /**\n * Per-direction read error (EC-205): a direction whose query returns a\n * structured non-ok degrades to an empty list plus this message, surfaced as a\n * visible per-section error — instead of collapsing the whole panel into its\n * hard-error state. `null` when the direction loaded (or was not requested).\n */\n outboundError: string | null\n inboundError: string | null\n}\n\n// --- Index bootstrap -----------------------------------------------------------\n\n/**\n * The EC-155 index is maintained on write, but seeded/imported projects start\n * with an empty index. {@link ensureReferenceIndex} seeds it once per project per\n * operations client; failures (e.g. a viewer's command denial) degrade to\n * whatever the maintained-on-write index already holds.\n */\nconst backfillsByClient = new WeakMap<StudioOperations, Map<string, Promise<void>>>()\n\n/**\n * Seed the index once if it has never been backfilled, then never again — the\n * EC-269 mutation-free read path. The index is maintained on write, so the\n * (clear + rebuild) `backfill` is only needed to seed a never-built index. A\n * persisted marker (`references.buildMarker`, a read) records completion; when\n * present this returns after a single query with ZERO mutations — so opening the\n * References panel no longer rewrites the whole index every session (the normal\n * Convex case: the EC-233 import already backfilled). Only a genuinely empty/\n * never-built project still pays the one-time backfill (and a viewer who cannot\n * run it degrades to the maintained-on-write index, as before).\n */\nasync function seedReferenceIndex(studio: StudioOperations, projectId: string): Promise<void> {\n try {\n const marker = await studio.operations.references.buildMarker({ projectId })\n if (marker.ok && marker.value !== null) return\n } catch {\n // A marker-read failure falls through to the backfill (best-effort seed).\n }\n await studio.operations.references.backfill({ projectId }).then(\n () => undefined,\n () => undefined,\n )\n}\n\nexport function ensureReferenceIndex(studio: StudioOperations, projectId: string): Promise<void> {\n let byProject = backfillsByClient.get(studio)\n if (!byProject) {\n byProject = new Map()\n backfillsByClient.set(studio, byProject)\n }\n let pending = byProject.get(projectId)\n if (!pending) {\n pending = seedReferenceIndex(studio, projectId)\n byProject.set(projectId, pending)\n }\n return pending\n}\n\n// --- Label context loading -------------------------------------------------------\n\nasync function loadLabelContext(\n operations: OperationsClient,\n projectId: string,\n): Promise<ReferenceLabelContext> {\n const [schemaResult, documentsResult, assetsResult] = await Promise.all([\n operations.schema.get({ projectId }),\n operations.documents.list({ projectId }),\n operations.assets.list({ projectId }),\n ])\n\n const collections = schemaResult.ok ? schemaResult.value.collections : []\n const documents = new Map<string, CmsDocument>()\n if (documentsResult.ok) {\n for (const record of documentsResult.value) {\n documents.set(\n documentKey({ collection: record.document.collection, id: record.document.id }),\n record.document,\n )\n }\n }\n // EC-287: the project graph is retired; reference source labels resolve from\n // CMS documents only. `page`/`layout` graph sources no longer exist.\n const assets = new Map<string, { filename: string; title?: string | undefined }>()\n if (assetsResult.ok) {\n for (const asset of assetsResult.value) {\n assets.set(asset.id, { filename: asset.filename, title: asset.title })\n }\n }\n const registry = getStudioRegistry().registry\n return {\n collections,\n documents,\n assets,\n // EC-580: titles read the default-locale view of the project's ONE locale\n // config — the one the CMS session installed (`configureStudioLocales`), else\n // (a rail opened before any CMS session, e.g. Assets) the active config\n // project's, which is where the session's value comes from in the first place.\n locales: getStudioLocaleConfig() ?? resolveDeploymentLocaleConfig() ?? null,\n // EC-407: `title` is a LocalizedString — resolve it to the active UI locale\n // (a plain string is unchanged), never leak a `{ en, de }` object to the label.\n componentTitle: (componentId) => {\n const title = registry.getManifest(componentId)?.title\n return title === undefined ? undefined : localizedLabel(title) || undefined\n },\n }\n}\n\n// --- Panel data loading -----------------------------------------------------------\n\n/** Load both directions + labels for one panel (operations queries only). */\nexport async function loadReferencePanelData(\n query: ReferencePanelQuery,\n): Promise<ReferencePanelData> {\n const studio = await getStudioOperations()\n await ensureReferenceIndex(studio, query.projectId)\n // Observe a settled index: maintained-on-write updates run async after events.\n await studio.references.flush()\n const operations = studio.operations\n\n // EC-205: load each direction INDEPENDENTLY. A structured non-ok from one\n // direction is a degradable operation result (it carries an OperationError —\n // the documented contract), not an exception: degrade that direction to empty\n // and surface its error per-section (references-panel) while the other still\n // renders, instead of throwing the whole panel into its hard-error state. A\n // genuinely broken backend still throws from getStudioOperations/flush above.\n const [outbound, inbound] = await Promise.all([\n query.source\n ? operations.references.outbound({ projectId: query.projectId, source: query.source })\n : null,\n query.target\n ? operations.references.inbound({ projectId: query.projectId, target: query.target })\n : null,\n ])\n\n type DirectionResult = Awaited<ReturnType<typeof operations.references.outbound>>\n const settle = (\n result: DirectionResult | null,\n ): { records: ReferenceEntryRecord[]; error: string | null } =>\n result === null\n ? { records: [], error: null }\n : result.ok\n ? { records: result.value, error: null }\n : { records: [], error: result.error.message }\n const out = settle(outbound)\n const inb = settle(inbound)\n\n // EC-302: nothing referenced in either direction → no sources/targets to label,\n // so skip the label-context load entirely (schema + ALL documents + ALL assets).\n // Common for leaf content; mirrors the asset short-circuit in\n // `loadInboundAssetReferenceViews`.\n if (out.records.length === 0 && inb.records.length === 0) {\n return { outbound: [], inbound: [], outboundError: out.error, inboundError: inb.error }\n }\n\n const context = await loadLabelContext(operations, query.projectId)\n return {\n outbound: buildReferenceEntryViews(out.records, context),\n inbound: buildReferenceEntryViews(inb.records, context),\n outboundError: out.error,\n inboundError: inb.error,\n }\n}\n\n// --- Impact loading (EC-173 safe unpublish/delete) -------------------------------\n\n/**\n * Load the labelled inbound entry views for a set of target documents in ONE\n * pass (one index flush, one label-context load) — the pre-flight data of the\n * EC-173 confirmation dialogs, including the bulk flows. Returns entries in\n * `targets` order; pair with `buildReferenceImpact` for break/block semantics.\n *\n * `toTarget` maps each acted-on ref to its reference-index target shape\n * (EC-433): the default is the document shape every `cmsDocuments` collection\n * indexes under; a rail whose records live under a different target type (the\n * assets rail, `{ type: 'asset' }`) supplies its own via\n * `CollectionViewSource.referenceTarget` — otherwise its pre-flight would\n * query the wrong shape and report referenced records as \"not referenced\".\n */\nexport async function loadInboundReferenceViews(\n projectId: string,\n targets: readonly DocumentRef[],\n toTarget: (ref: DocumentRef) => ReferenceTarget = (ref) => ({\n type: 'document',\n id: documentKey(ref),\n }),\n): Promise<{ ref: DocumentRef; inbound: ReferenceEntryView[] }[]> {\n const studio = await getStudioOperations()\n await ensureReferenceIndex(studio, projectId)\n // Observe a settled index: maintained-on-write updates run async after events.\n await studio.references.flush()\n const operations = studio.operations\n\n // EC-308: one batched query for ALL targets (the operation loops the by_target\n // index server-side) instead of a per-target `inbound` fan-out. Returns one\n // record list per target, in `targets` order.\n const recordsPerTarget = await operations.references\n .inboundMany({\n projectId,\n targets: targets.map((ref) => toTarget(ref)),\n })\n .then((result) => {\n if (!result.ok) throw new Error(result.error.message)\n return result.value\n })\n // EC-302: no inbound refs for ANY target → no sources to label, so skip the\n // label-context load (schema + ALL documents + ALL assets). The common case when\n // deleting/unpublishing unreferenced documents (incl. bulk); mirrors the EC-301\n // short-circuit already in `loadInboundAssetReferenceViews`.\n if (recordsPerTarget.every((records) => records.length === 0)) {\n return targets.map((ref) => ({ ref, inbound: [] }))\n }\n const context = await loadLabelContext(operations, projectId)\n return targets.map((ref, index) => ({\n ref,\n inbound: buildReferenceEntryViews(recordsPerTarget[index] ?? [], context),\n }))\n}\n\n/**\n * Inbound reference views for ONE asset (EC-202): the \"what uses this asset\"\n * pre-flight of the asset-delete dialog, index-accurate (the EC-155 reference\n * index, not the retired `find-asset-usages` string scan). Mirrors\n * {@link loadInboundReferenceViews} for a single `{ type: 'asset' }` target —\n * the source labels resolve the referencing documents/graph-nodes identically.\n */\nexport async function loadInboundAssetReferenceViews(\n projectId: string,\n assetId: string,\n): Promise<ReferenceEntryView[]> {\n const studio = await getStudioOperations()\n await ensureReferenceIndex(studio, projectId)\n // Observe a settled index: maintained-on-write updates run async after events.\n await studio.references.flush()\n const operations = studio.operations\n const result = await operations.references.inbound({\n projectId,\n target: { type: 'asset', id: assetId },\n })\n if (!result.ok) throw new Error(result.error.message)\n // No inbound references → no sources to label, so skip the label-context load\n // entirely (schema + ALL documents + ALL assets). This is the common case for a\n // deletable asset, making the delete dialog's pre-flight a single indexed query\n // instead of three full-table lists (EC-301 perf).\n if (result.value.length === 0) return []\n const context = await loadLabelContext(operations, projectId)\n return buildReferenceEntryViews(result.value, context)\n}\n\n// --- React hook ----------------------------------------------------------------\n\n/**\n * Domains whose successful commands can change the reference index or labels and\n * should refresh an open panel. `references` is deliberately ABSENT (EC-269):\n * reacting to reference-index command events (the `backfill`) would make the\n * panel re-trigger on its OWN index maintenance — a self-cascade that, compounded\n * with the per-keystroke `documents` events, produced hundreds of redundant\n * reloads per document. The index is maintained on write; the panel reacts to the\n * CONTENT domains that feed it, never to its own index writes.\n */\nconst REFERENCE_REFRESH_TRIGGERS = [\n 'documents',\n 'graph',\n 'publishing',\n 'assets',\n 'schema',\n] as const\n\nconst REFRESH_DOMAINS: ReadonlySet<string> = new Set(REFERENCE_REFRESH_TRIGGERS)\n\n/**\n * Whether one operation event should refresh an open reference panel. Exported\n * for tests — pins the EC-269 contract that reference-index commands (and any\n * non-succeeded / non-command event) never trigger a reload.\n */\nexport function isReferenceRefreshEvent(event: OperationEvent): boolean {\n return (\n event.kind === 'command' && event.status === 'succeeded' && REFRESH_DOMAINS.has(event.domain)\n )\n}\n\nexport type ReferencePanelStatus = 'loading' | 'ready' | 'error'\n\nexport interface ReferencePanelState {\n status: ReferencePanelStatus\n data: ReferencePanelData | null\n errorMessage: string | null\n refresh: () => void\n}\n\n/**\n * Live panel state for one entity (EC-308/AD-17): loads + caches through the one\n * `useOperationQuery` seam, and re-queries whenever a content-bearing command\n * succeeds anywhere in the studio (the index is maintained on write). Per-direction\n * read errors degrade INSIDE `loadReferencePanelData` (carried on the data); a hard\n * failure (broken backend) surfaces as the query's error → `status: 'error'`.\n */\nexport function useReferencePanel(query: ReferencePanelQuery): ReferencePanelState {\n const { projectId } = query\n const sourceType = query.source?.type\n const sourceId = query.source?.id\n const targetType = query.target?.type\n const targetId = query.target?.id\n\n const stableQuery = useMemo<ReferencePanelQuery>(\n () => ({\n projectId,\n ...(sourceType && sourceId ? { source: { type: sourceType, id: sourceId } } : {}),\n ...(targetType && targetId ? { target: { type: targetType, id: targetId } } : {}),\n }),\n [projectId, sourceType, sourceId, targetType, targetId],\n )\n\n const result = useOperationQuery<ReferencePanelData>({\n queryKey: [\n 'referencePanel',\n projectId,\n sourceType ?? '',\n sourceId ?? '',\n targetType ?? '',\n targetId ?? '',\n ],\n loader: () => loadReferencePanelData(stableQuery),\n invalidateOn: REFERENCE_REFRESH_TRIGGERS,\n })\n\n const status: ReferencePanelStatus = result.isPending\n ? 'loading'\n : result.isError\n ? 'error'\n : 'ready'\n return {\n status,\n data: result.data ?? null,\n errorMessage: result.error instanceof Error ? result.error.message : null,\n refresh: () => void result.refetch(),\n }\n}\n"]}
|
|
@@ -27,6 +27,12 @@ import type { CustomizationIssue } from '@elytracms/core/studio-core';
|
|
|
27
27
|
* declarations, so it locates the config, not an override list.
|
|
28
28
|
*/
|
|
29
29
|
export declare const PROJECT_CONFIG_POINT = "project";
|
|
30
|
+
/**
|
|
31
|
+
* The file a developer fixes these findings in — interpolated, never localized.
|
|
32
|
+
* Exported for the rail foot's other config-remedy message (EC-606's orphaned
|
|
33
|
+
* collections), so the file name is declared once.
|
|
34
|
+
*/
|
|
35
|
+
export declare const PROJECT_CONFIG_FILE = "elytra.config.ts";
|
|
30
36
|
/**
|
|
31
37
|
* Catalog keys for the issues below (EC-478). Named here, next to the producer, so
|
|
32
38
|
* the studio's i18n parity test asserts coverage against the producer rather than a
|
|
@@ -25,8 +25,12 @@
|
|
|
25
25
|
* declarations, so it locates the config, not an override list.
|
|
26
26
|
*/
|
|
27
27
|
export const PROJECT_CONFIG_POINT = 'project';
|
|
28
|
-
/**
|
|
29
|
-
|
|
28
|
+
/**
|
|
29
|
+
* The file a developer fixes these findings in — interpolated, never localized.
|
|
30
|
+
* Exported for the rail foot's other config-remedy message (EC-606's orphaned
|
|
31
|
+
* collections), so the file name is declared once.
|
|
32
|
+
*/
|
|
33
|
+
export const PROJECT_CONFIG_FILE = 'elytra.config.ts';
|
|
30
34
|
/**
|
|
31
35
|
* Catalog keys for the issues below (EC-478). Named here, next to the producer, so
|
|
32
36
|
* the studio's i18n parity test asserts coverage against the producer rather than a
|
|
@@ -60,10 +64,10 @@ export function projectConfigIssues(collections) {
|
|
|
60
64
|
{
|
|
61
65
|
code: 'missing-configuration',
|
|
62
66
|
severity: 'error',
|
|
63
|
-
message: `This project declares no content collections. Add them to "${
|
|
67
|
+
message: `This project declares no content collections. Add them to "${PROJECT_CONFIG_FILE}" — the studio shows your schema or none, never sample content.`,
|
|
64
68
|
messageRef: {
|
|
65
69
|
key: PROJECT_CONFIG_ISSUE_KEYS.noCollections,
|
|
66
|
-
vars: { file:
|
|
70
|
+
vars: { file: PROJECT_CONFIG_FILE },
|
|
67
71
|
},
|
|
68
72
|
point: PROJECT_CONFIG_POINT,
|
|
69
73
|
},
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config-issues.js","sourceRoot":"","sources":["../../../src/lib/workspace/config-issues.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH;;;;GAIG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,SAAS,CAAA;AAE7C
|
|
1
|
+
{"version":3,"file":"config-issues.js","sourceRoot":"","sources":["../../../src/lib/workspace/config-issues.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH;;;;GAIG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,SAAS,CAAA;AAE7C;;;;GAIG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,kBAAkB,CAAA;AAErD;;;;GAIG;AACH,MAAM,CAAC,MAAM,yBAAyB,GAAG;IACvC,aAAa,EAAE,mCAAmC;CAC1C,CAAA;AAEV;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,mBAAmB,CACjC,WAAqC;IAErC,IAAI,WAAW,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,EAAE,CAAA;IACrC,OAAO;QACL;YACE,IAAI,EAAE,uBAAuB;YAC7B,QAAQ,EAAE,OAAO;YACjB,OAAO,EAAE,8DAA8D,mBAAmB,iEAAiE;YAC3J,UAAU,EAAE;gBACV,GAAG,EAAE,yBAAyB,CAAC,aAAa;gBAC5C,IAAI,EAAE,EAAE,IAAI,EAAE,mBAAmB,EAAE;aACpC;YACD,KAAK,EAAE,oBAAoB;SAC5B;KACF,CAAA;AACH,CAAC","sourcesContent":["import type { CollectionDef } from '@elytracms/core/cms-core'\nimport type { CustomizationIssue } from '@elytracms/core/studio-core'\n\n/**\n * EC-479 — project-level configuration findings for the section-rail foot.\n *\n * The engine used to fill a missing declaration with its own `cms-core` fixtures:\n * `?? sampleFilterableCollections` in the CMS store, `?? sampleDocuments` beside\n * it, `?? sampleLocaleConfig` in the locale store. A studio whose config never\n * arrived therefore looked *configured* — it showed six documents about Ada\n * Lovelace under a four-collection schema, in `en/de`, none of which the project\n * had declared. The owner's ruling (2026-07-30): \"fixtures should only show in the\n * playground, but not be hard-wired in the engine\".\n *\n * With the fallbacks retired the same state renders as an empty CMS, which on its\n * own is the OTHER silent failure — the house rule bans both (\"if you are writing\n * a fallback that hides a broken state, that *is* the bug\"). This module is the\n * third option: say so, at the rail foot, where EC-467's reuse issues and EC-469's\n * URL-map issues already report project-level problems, on every studio surface.\n *\n * Pure and deterministic — no config-holder reads, no React. The caller supplies\n * the resolved config (`resolveDeploymentCollections()`), which is also what makes\n * this testable against a synthetic project instead of the live one.\n */\n\n/**\n * The extension-point name stamped on these issues. Not a customization registry\n * like `'sidebar'`/`'fieldRenderers'` — the finding is about the project's own\n * declarations, so it locates the config, not an override list.\n */\nexport const PROJECT_CONFIG_POINT = 'project'\n\n/**\n * The file a developer fixes these findings in — interpolated, never localized.\n * Exported for the rail foot's other config-remedy message (EC-606's orphaned\n * collections), so the file name is declared once.\n */\nexport const PROJECT_CONFIG_FILE = 'elytra.config.ts'\n\n/**\n * Catalog keys for the issues below (EC-478). Named here, next to the producer, so\n * the studio's i18n parity test asserts coverage against the producer rather than a\n * hand-copied list — the same shape as `default-modules.tsx`'s `KEYSPACE_KEYS`.\n */\nexport const PROJECT_CONFIG_ISSUE_KEYS = {\n noCollections: 'customization.issue.noCollections',\n} as const\n\n/**\n * Report what the project has NOT declared but the studio cannot work without.\n *\n * Today that is exactly one thing: collections. A studio with none has no content\n * model at all — the CMS rail expands to nothing, every list is empty, and before\n * EC-479 the store quietly substituted the engine's sample schema so none of that\n * was visible. The remedy is one line in `elytra.config.ts`, which is why the\n * message names the file.\n *\n * Deliberately NOT reported here: an absent locale config. A `Project` only exists\n * for a config-declared project and `resolveWorkspaceConfig` gives every one of\n * them a `locales` array, so \"no locales\" is unreachable from the rail's vantage\n * point; the CMS session guards it directly instead (`CmsProvider`), where it is\n * both reachable and actionable. Reporting an unreachable state would be copy\n * nobody can trigger. Since EC-490 (owner-ratified 2026-08-01) that unreachability\n * is STRUCTURAL rather than incidental: a config declaring no `locales` is rejected\n * outright, so there is no resolved project behind which the question could hide.\n */\nexport function projectConfigIssues(\n collections: readonly CollectionDef[],\n): CustomizationIssue[] {\n if (collections.length > 0) return []\n return [\n {\n code: 'missing-configuration',\n severity: 'error',\n message: `This project declares no content collections. Add them to \"${PROJECT_CONFIG_FILE}\" — the studio shows your schema or none, never sample content.`,\n messageRef: {\n key: PROJECT_CONFIG_ISSUE_KEYS.noCollections,\n vars: { file: PROJECT_CONFIG_FILE },\n },\n point: PROJECT_CONFIG_POINT,\n },\n ]\n}\n"]}
|