@elytracms/studio 0.0.88 → 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 +1 -1
- 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/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/cell-edit.d.ts +0 -6
- package/dist/components/cms/cell-edit.js +5 -23
- 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 +23 -24
- 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/field-control.js +7 -6
- package/dist/components/cms/field-control.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/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/redirects/redirects-collection-view-source.js +3 -2
- package/dist/components/redirects/redirects-collection-view-source.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/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.js +36 -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 +1 -1
- package/dist/lib/engine-version.js +1 -1
- package/dist/lib/engine-version.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/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/index.d.ts +0 -7
- package/dist/lib/workspace/index.js +0 -9
- package/dist/lib/workspace/index.js.map +1 -1
- package/package.json +2 -6
- 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
package/dist/lib/ai/vite.js
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
* `@elytracms/studio/lib/ai` directly.
|
|
20
20
|
*/
|
|
21
21
|
import { loadEnv } from 'vite';
|
|
22
|
+
import { parseBackendMode } from '@elytracms/core/export-sync/deployment-ready';
|
|
22
23
|
import { INSECURE_ANONYMOUS_SESSION_ENV_VALUE, INSECURE_ANONYMOUS_SESSION_ENV_VAR, } from './convex-session.js';
|
|
23
24
|
import { toNodeHandler } from './node-handler.js';
|
|
24
25
|
import { DEFAULT_AI_ENDPOINT } from './route-provider.js';
|
|
@@ -28,14 +29,21 @@ export const DEFAULT_AI_DEV_ENV_KEYS = [
|
|
|
28
29
|
'VITE_CONVEX_SITE_URL',
|
|
29
30
|
'VITE_CONVEX_URL',
|
|
30
31
|
];
|
|
31
|
-
/**
|
|
32
|
+
/**
|
|
33
|
+
* Resolve the anonymous-bypass opt-in against the loaded env (pure; unit-pinned).
|
|
34
|
+
* `'playground-mode'` asks the SAME question the studio asks of the same env — core's
|
|
35
|
+
* `parseBackendMode` (EC-603) — so a padded flag the studio boots as playground opens
|
|
36
|
+
* the bypass too, and a misspelled one (the studio's boot error) keeps it shut.
|
|
37
|
+
*/
|
|
32
38
|
export function resolveInsecureAnonymousOptIn(optIn, env) {
|
|
33
39
|
if (optIn === true)
|
|
34
40
|
return true;
|
|
35
41
|
if (optIn === 'playground-mode')
|
|
36
|
-
return env.
|
|
42
|
+
return parseBackendMode(env).kind === 'playground';
|
|
37
43
|
return false;
|
|
38
44
|
}
|
|
45
|
+
/** The plugin's `name` — how `elytra doctor` finds the AI route in a studio's Vite config. */
|
|
46
|
+
export const AI_ROUTE_PLUGIN_NAME = 'elytra-ai-route';
|
|
39
47
|
function jsonError(code, status) {
|
|
40
48
|
return new Response(JSON.stringify({ error: code }), {
|
|
41
49
|
status,
|
|
@@ -53,8 +61,9 @@ export function createAiDevPlugin(options = {}) {
|
|
|
53
61
|
const exportName = options.exportName ?? 'aiRouteHandler';
|
|
54
62
|
let env = {};
|
|
55
63
|
return {
|
|
56
|
-
name:
|
|
64
|
+
name: AI_ROUTE_PLUGIN_NAME,
|
|
57
65
|
apply: 'serve',
|
|
66
|
+
api: { elytraAiRoute: { route, handlerModulePath, exportName } },
|
|
58
67
|
configResolved(config) {
|
|
59
68
|
env = loadEnv(config.mode, config.root, '');
|
|
60
69
|
for (const key of [...DEFAULT_AI_DEV_ENV_KEYS, ...(options.envKeys ?? [])]) {
|
package/dist/lib/ai/vite.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vite.js","sourceRoot":"","sources":["../../../src/lib/ai/vite.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AAE9B,OAAO,EACL,oCAAoC,EACpC,kCAAkC,GACnC,MAAM,kBAAkB,CAAA;AACzB,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAC9C,OAAO,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAA;AAGtD,0FAA0F;AAC1F,MAAM,CAAC,MAAM,uBAAuB,GAAG;IACrC,mBAAmB;IACnB,sBAAsB;IACtB,iBAAiB;CACT,CAAA;AAwBV
|
|
1
|
+
{"version":3,"file":"vite.js","sourceRoot":"","sources":["../../../src/lib/ai/vite.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `@elytracms/studio/lib/ai/vite` — `createAiDevPlugin`, the DEV half of the AI\n * route mounting kit (EC-371; closes EC-369 finding #2). A studio app is a client\n * SPA with no server build, so in dev the host route lives as a Vite dev-server\n * middleware: this plugin loads the framework-agnostic `Request → Response` handler\n * module through Vite's SSR pipeline (so its `@elytracms/*` imports resolve like the\n * rest of the app) and bridges Node req/res via the shared {@link toNodeHandler}.\n *\n * It also owns the env bridge the resolver needs (the piece the template had\n * drifted on): `loadEnv` surfaces `ANTHROPIC_API_KEY` / `VITE_CONVEX_SITE_URL` /\n * `VITE_CONVEX_URL` from `.env`/`.env.local` into `process.env` for the SSR-loaded\n * handler — Vite only exposes `VITE_`-prefixed vars via `import.meta.env`, and a\n * `VITE_` prefix on the provider key would leak it into the client bundle.\n *\n * SEPARATE ENTRY on purpose: this module statically imports `vite`, which is a\n * dev-only tool. It is NOT re-exported from the `lib/ai` barrel (pinned in\n * `bundle.test.ts`), and the plugin is `apply: 'serve'` — no production build or\n * serverless mount ever includes it. Production mounts use `toNodeHandler` from\n * `@elytracms/studio/lib/ai` directly.\n */\nimport { loadEnv } from 'vite'\nimport type { Plugin, ViteDevServer } from 'vite'\nimport { parseBackendMode } from '@elytracms/core/export-sync/deployment-ready'\nimport {\n INSECURE_ANONYMOUS_SESSION_ENV_VALUE,\n INSECURE_ANONYMOUS_SESSION_ENV_VAR,\n} from './convex-session'\nimport { toNodeHandler } from './node-handler'\nimport { DEFAULT_AI_ENDPOINT } from './route-provider'\nimport type { AiRouteHandler } from './types'\n\n/** Env keys the plugin surfaces from `.env*` files into `process.env` for the handler. */\nexport const DEFAULT_AI_DEV_ENV_KEYS = [\n 'ANTHROPIC_API_KEY',\n 'VITE_CONVEX_SITE_URL',\n 'VITE_CONVEX_URL',\n] as const\n\nexport interface AiDevPluginOptions {\n /** The handler module (Vite root-relative), default `/src/server/ai-handler.ts`. */\n handlerModulePath?: string\n /** The mounted route path (default {@link DEFAULT_AI_ENDPOINT}, `/api/ai`). */\n route?: string\n /** The handler module's export name (default `aiRouteHandler`). */\n exportName?: string\n /** Extra env keys to surface into `process.env` (merged over the defaults). */\n envKeys?: readonly string[]\n /**\n * INSECURE, dev-only: let requests presenting NO credential pass the session gate.\n * A playground studio has no Convex session, so without this the assistant panel\n * answers 401 there. `'playground-mode'` scopes the bypass to dev servers booted\n * with `VITE_ELYTRA_MODE=playground` (the recommended setting); `true` applies it\n * to every dev boot. Defaults OFF. It cannot reach production: the plugin is\n * `apply: 'serve'`, the handshake env var is set only in `configureServer`, and\n * the resolver additionally refuses it under `NODE_ENV=production`. A PRESENTED\n * token must still verify for real — the bypass never masks a bad credential.\n */\n insecureAllowAnonymousSessions?: boolean | 'playground-mode'\n}\n\n/**\n * Resolve the anonymous-bypass opt-in against the loaded env (pure; unit-pinned).\n * `'playground-mode'` asks the SAME question the studio asks of the same env — core's\n * `parseBackendMode` (EC-603) — so a padded flag the studio boots as playground opens\n * the bypass too, and a misspelled one (the studio's boot error) keeps it shut.\n */\nexport function resolveInsecureAnonymousOptIn(\n optIn: boolean | 'playground-mode' | undefined,\n env: Record<string, string | undefined>,\n): boolean {\n if (optIn === true) return true\n if (optIn === 'playground-mode') return parseBackendMode(env).kind === 'playground'\n return false\n}\n\n/** The plugin's `name` — how `elytra doctor` finds the AI route in a studio's Vite config. */\nexport const AI_ROUTE_PLUGIN_NAME = 'elytra-ai-route'\n\n/**\n * What the plugin mounts, exposed on its `api` (Vite's inter-plugin channel) so a tool\n * that loads the studio's Vite config can read the AI route from that config instead of\n * guessing a file path (EC-603: `elytra doctor` reads it to decide whether an AI\n * provider key is expected, and checks the handler module exists).\n */\nexport interface AiDevPluginApi {\n elytraAiRoute: { route: string; handlerModulePath: string; exportName: string }\n}\n\nfunction jsonError(code: string, status: number): Response {\n return new Response(JSON.stringify({ error: code }), {\n status,\n headers: { 'content-type': 'application/json' },\n })\n}\n\n/**\n * Mount the AI route in the Vite dev server. Dev-only by construction\n * (`apply: 'serve'`); a production deploy mounts the same handler module via\n * `toNodeHandler` in its serverless entry instead.\n */\nexport function createAiDevPlugin(options: AiDevPluginOptions = {}): Plugin<AiDevPluginApi> {\n const handlerModulePath = options.handlerModulePath ?? '/src/server/ai-handler.ts'\n const route = options.route ?? DEFAULT_AI_ENDPOINT\n const exportName = options.exportName ?? 'aiRouteHandler'\n let env: Record<string, string | undefined> = {}\n\n return {\n name: AI_ROUTE_PLUGIN_NAME,\n apply: 'serve',\n api: { elytraAiRoute: { route, handlerModulePath, exportName } },\n configResolved(config) {\n env = loadEnv(config.mode, config.root, '')\n for (const key of [...DEFAULT_AI_DEV_ENV_KEYS, ...(options.envKeys ?? [])]) {\n if (env[key] && !process.env[key]) process.env[key] = env[key]\n }\n },\n configureServer(server: ViteDevServer) {\n if (resolveInsecureAnonymousOptIn(options.insecureAllowAnonymousSessions, env)) {\n process.env[INSECURE_ANONYMOUS_SESSION_ENV_VAR] = INSECURE_ANONYMOUS_SESSION_ENV_VALUE\n server.config.logger.warn(\n `[elytra-ai] insecureAllowAnonymousSessions is ON for this dev server: ${route} ` +\n 'accepts requests without a session token (requests presenting a token are still verified).',\n )\n }\n\n // Per request: SSR-load the handler (picks up its @elytracms/* imports through\n // Vite) and run it; a load/handler failure is a structured 500, never a crash.\n const bridge = toNodeHandler(async (request: Request): Promise<Response> => {\n try {\n const mod = (await server.ssrLoadModule(handlerModulePath)) as Record<string, unknown>\n const handler = mod[exportName]\n if (typeof handler !== 'function') return jsonError('route-handler-missing', 500)\n return await (handler as AiRouteHandler)(request)\n } catch {\n return jsonError('route-error', 500)\n }\n })\n\n server.middlewares.use((req, res, next) => {\n if (!req.url || !req.url.startsWith(route)) return next()\n void bridge(req, res).catch(() => {\n if (!res.headersSent) res.statusCode = 500\n res.end()\n })\n })\n },\n }\n}\n"]}
|
|
@@ -10,15 +10,26 @@ import type { OperationsClient } from '../operations/index.js';
|
|
|
10
10
|
* URL from these ancestors yields the URL the live site actually resolves.
|
|
11
11
|
*
|
|
12
12
|
* EC-308/AD-17: loaded + kept fresh through the one `useOperationQuery` cache
|
|
13
|
-
* seam — invalidated only when a
|
|
14
|
-
*
|
|
15
|
-
*
|
|
13
|
+
* seam — invalidated only when a command that moves a live pin succeeds (NOT on
|
|
14
|
+
* every autosave upsert — a draft edit doesn't change what's published). Cost: ONE
|
|
15
|
+
* bulk `documents.listPublished` read (EC-307).
|
|
16
16
|
*/
|
|
17
17
|
export interface PublishedDocuments {
|
|
18
18
|
/** Pinned published snapshot per published document id. Empty while loading. */
|
|
19
19
|
byId: ReadonlyMap<string, CmsDocument>;
|
|
20
20
|
status: 'loading' | 'ready';
|
|
21
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* Commands that change which version is pinned live (and thus the pool).
|
|
24
|
+
*
|
|
25
|
+
* EC-579 (RESIDUE-2c): `documents.pinVersion` moves the pin too — it is how a
|
|
26
|
+
* changeset revert restores a prior live version — and was missing, so a revert left
|
|
27
|
+
* the live URLs on the old pool until a reload. `documents.restore` was here and
|
|
28
|
+
* moves nothing: it writes the DRAFT from a recorded version (`cms.upsertDocument`),
|
|
29
|
+
* never the pin. The same answer the Convex trigger gives server-side: only a pin
|
|
30
|
+
* move or the delete of a pinned row changes published output.
|
|
31
|
+
*/
|
|
32
|
+
export declare const PUBLISHED_RELOAD_OPS: readonly ["documents.publish", "documents.unpublish", "documents.pinVersion", "documents.delete", "documents.clear"];
|
|
22
33
|
/**
|
|
23
34
|
* Build the published pool of a collection (pure over the operations client): the
|
|
24
35
|
* pinned published snapshot of every currently-published document, keyed by id.
|
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
import { useOperationQuery } from '@elytracms/studio/lib/query';
|
|
2
|
-
/**
|
|
3
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Commands that change which version is pinned live (and thus the pool).
|
|
4
|
+
*
|
|
5
|
+
* EC-579 (RESIDUE-2c): `documents.pinVersion` moves the pin too — it is how a
|
|
6
|
+
* changeset revert restores a prior live version — and was missing, so a revert left
|
|
7
|
+
* the live URLs on the old pool until a reload. `documents.restore` was here and
|
|
8
|
+
* moves nothing: it writes the DRAFT from a recorded version (`cms.upsertDocument`),
|
|
9
|
+
* never the pin. The same answer the Convex trigger gives server-side: only a pin
|
|
10
|
+
* move or the delete of a pinned row changes published output.
|
|
11
|
+
*/
|
|
12
|
+
export const PUBLISHED_RELOAD_OPS = [
|
|
4
13
|
'documents.publish',
|
|
5
14
|
'documents.unpublish',
|
|
6
|
-
'documents.
|
|
15
|
+
'documents.pinVersion',
|
|
7
16
|
'documents.delete',
|
|
8
17
|
'documents.clear',
|
|
9
18
|
];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"published-documents.js","sourceRoot":"","sources":["../../../src/lib/cms/published-documents.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAA;AAsB/D
|
|
1
|
+
{"version":3,"file":"published-documents.js","sourceRoot":"","sources":["../../../src/lib/cms/published-documents.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAA;AAsB/D;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG;IAClC,mBAAmB;IACnB,qBAAqB;IACrB,sBAAsB;IACtB,kBAAkB;IAClB,iBAAiB;CACT,CAAA;AAEV,MAAM,UAAU,GAAqC,IAAI,GAAG,EAAE,CAAA;AAE9D;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAC1C,UAA4B,EAC5B,SAAiB,EACjB,YAAoB;IAEpB,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,SAAS,CAAC,aAAa,CAAC,EAAE,SAAS,EAAE,UAAU,EAAE,YAAY,EAAE,CAAC,CAAA;IAChG,IAAI,CAAC,MAAM,CAAC,EAAE;QAAE,OAAO,IAAI,GAAG,EAAE,CAAA;IAChC,MAAM,IAAI,GAAG,IAAI,GAAG,EAAuB,CAAA;IAC3C,KAAK,MAAM,QAAQ,IAAI,MAAM,CAAC,KAAK;QAAE,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,EAAE,QAAQ,CAAC,CAAA;IACpE,OAAO,IAAI,CAAA;AACb,CAAC;AAED,MAAM,UAAU,qBAAqB,CACnC,SAA6B,EAC7B,YAAoB;IAEpB,MAAM,KAAK,GAAG,iBAAiB,CAA2B;QACxD,4EAA4E;QAC5E,QAAQ,EAAE,CAAC,oBAAoB,EAAE,SAAS,IAAI,GAAG,EAAE,YAAY,CAAC;QAChE,MAAM,EAAE,CAAC,UAAU,EAAE,EAAE,CAAC,sBAAsB,CAAC,UAAU,EAAE,SAAmB,EAAE,YAAY,CAAC;QAC7F,YAAY,EAAE,oBAAoB;QAClC,OAAO,EAAE,SAAS,KAAK,SAAS;KACjC,CAAC,CAAA;IACF,IAAI,SAAS,KAAK,SAAS;QAAE,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,CAAA;IACzE,OAAO,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,IAAI,UAAU,EAAE,MAAM,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,EAAE,CAAA;AAC1F,CAAC","sourcesContent":["import type { CmsDocument } from '@elytracms/core/cms-core'\nimport type { OperationsClient } from '../operations'\nimport { useOperationQuery } from '@elytracms/studio/lib/query'\n\n/**\n * The PUBLISHED-perspective documents of a collection (EC-205, \"live = published\")\n * — the pinned published snapshot of each currently-published document, keyed by\n * id. Live surfaces (a document's live URL + \"View live\", EC-216) MUST derive\n * from this, never from the draft rows the studio edits: an unpublished document,\n * or an unpublished `slug`/`parent` edit, must never leak into a \"live\" URL. The\n * pool is exactly the set the live site would serve, so composing a nested page's\n * URL from these ancestors yields the URL the live site actually resolves.\n *\n * EC-308/AD-17: loaded + kept fresh through the one `useOperationQuery` cache\n * seam — invalidated only when a command that moves a live pin succeeds (NOT on\n * every autosave upsert — a draft edit doesn't change what's published). Cost: ONE\n * bulk `documents.listPublished` read (EC-307).\n */\nexport interface PublishedDocuments {\n /** Pinned published snapshot per published document id. Empty while loading. */\n byId: ReadonlyMap<string, CmsDocument>\n status: 'loading' | 'ready'\n}\n\n/**\n * Commands that change which version is pinned live (and thus the pool).\n *\n * EC-579 (RESIDUE-2c): `documents.pinVersion` moves the pin too — it is how a\n * changeset revert restores a prior live version — and was missing, so a revert left\n * the live URLs on the old pool until a reload. `documents.restore` was here and\n * moves nothing: it writes the DRAFT from a recorded version (`cms.upsertDocument`),\n * never the pin. The same answer the Convex trigger gives server-side: only a pin\n * move or the delete of a pinned row changes published output.\n */\nexport const PUBLISHED_RELOAD_OPS = [\n 'documents.publish',\n 'documents.unpublish',\n 'documents.pinVersion',\n 'documents.delete',\n 'documents.clear',\n] as const\n\nconst EMPTY_POOL: ReadonlyMap<string, CmsDocument> = new Map()\n\n/**\n * Build the published pool of a collection (pure over the operations client): the\n * pinned published snapshot of every currently-published document, keyed by id.\n * EC-307: one bulk `documents.listPublished` read — the backend resolves each\n * `publishedVersion` pointer to its recorded snapshot — instead of a `list` plus a\n * per-document `listVersions` fan-out. Read failures degrade to an empty pool.\n */\nexport async function loadPublishedDocuments(\n operations: OperationsClient,\n projectId: string,\n collectionId: string,\n): Promise<Map<string, CmsDocument>> {\n const result = await operations.documents.listPublished({ projectId, collection: collectionId })\n if (!result.ok) return new Map()\n const byId = new Map<string, CmsDocument>()\n for (const snapshot of result.value) byId.set(snapshot.id, snapshot)\n return byId\n}\n\nexport function usePublishedDocuments(\n projectId: string | undefined,\n collectionId: string,\n): PublishedDocuments {\n const query = useOperationQuery<Map<string, CmsDocument>>({\n // Playground/fixture mode (no backend) is `enabled: false` → never queried.\n queryKey: ['publishedDocuments', projectId ?? '∅', collectionId],\n loader: (operations) => loadPublishedDocuments(operations, projectId as string, collectionId),\n invalidateOn: PUBLISHED_RELOAD_OPS,\n enabled: projectId !== undefined,\n })\n if (projectId === undefined) return { byId: EMPTY_POOL, status: 'ready' }\n return { byId: query.data ?? EMPTY_POOL, status: query.isPending ? 'loading' : 'ready' }\n}\n"]}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import type { CmsDocument, CollectionDef, FieldDef, LocaleConfig, LocalizedString } from '@elytracms/core/cms-core';
|
|
2
|
+
import type { CmsDocument, CollectionDef, FieldDef, Locale, LocaleConfig, LocalizedString } from '@elytracms/core/cms-core';
|
|
3
3
|
import type { FilterOperator, FilterScalar } from '@elytracms/core/content';
|
|
4
4
|
/**
|
|
5
5
|
* The pure table-view model for the CMS collection table (EC-169).
|
|
@@ -239,14 +239,14 @@ export type IsPublished = (doc: CmsDocument) => boolean;
|
|
|
239
239
|
* `matchesFieldValue` over the RAW field value (so `contains`/`empty`/multiple-
|
|
240
240
|
* relation membership all behave the same on the table as on the live site).
|
|
241
241
|
*/
|
|
242
|
-
export declare function clauseMatches(collection: CollectionDef, doc: CmsDocument, localeConfig: LocaleConfig, clause: FilterClause, isPublished?: IsPublished): boolean;
|
|
242
|
+
export declare function clauseMatches(collection: CollectionDef, doc: CmsDocument, localeConfig: LocaleConfig, locale: Locale, clause: FilterClause, isPublished?: IsPublished): boolean;
|
|
243
243
|
/**
|
|
244
244
|
* Sort documents by one column in the canonical deterministic order
|
|
245
245
|
* (`compareListEntries`: typed value order, id-ascending tiebreak). Unlike the
|
|
246
246
|
* content accessors, the *table* may sort any rendered column — sorting is a
|
|
247
247
|
* presentation concern here; the `filterable` flag governs filters.
|
|
248
248
|
*/
|
|
249
|
-
export declare function sortViewDocuments(collection: CollectionDef, docs: readonly CmsDocument[], localeConfig: LocaleConfig, sort: ViewSort | null, isPublished?: IsPublished, meta?: PseudoColumnMeta): CmsDocument[];
|
|
249
|
+
export declare function sortViewDocuments(collection: CollectionDef, docs: readonly CmsDocument[], localeConfig: LocaleConfig, locale: Locale, sort: ViewSort | null, isPublished?: IsPublished, meta?: PseudoColumnMeta): CmsDocument[];
|
|
250
250
|
/** The filtered + sorted documents of a view, plus any clause issues. */
|
|
251
251
|
export interface AppliedViewState {
|
|
252
252
|
docs: CmsDocument[];
|
|
@@ -258,7 +258,7 @@ export interface AppliedViewState {
|
|
|
258
258
|
* ones, sort. Invalid clauses never filter silently — they come back as
|
|
259
259
|
* issues for the UI to surface.
|
|
260
260
|
*/
|
|
261
|
-
export declare function applyViewState(collection: CollectionDef, docs: readonly CmsDocument[], localeConfig: LocaleConfig, state: TableViewState, isPublished?: IsPublished, meta?: PseudoColumnMeta): AppliedViewState;
|
|
261
|
+
export declare function applyViewState(collection: CollectionDef, docs: readonly CmsDocument[], localeConfig: LocaleConfig, locale: Locale, state: TableViewState, isPublished?: IsPublished, meta?: PseudoColumnMeta): AppliedViewState;
|
|
262
262
|
/** The id of the synthesized built-in "All entries" baseline view (EC-444). */
|
|
263
263
|
export declare const ALL_VIEW_ID = "all";
|
|
264
264
|
export declare function defaultViewState(collection: CollectionDef, pseudo?: readonly string[]): TableViewState;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import {
|
|
2
|
+
import { localizedStringSchema, resolveField, resolveLabel, titleFieldOf, } from '@elytracms/core/cms-core';
|
|
3
3
|
import { localizedLabel, translate } from '@elytracms/studio/lib/i18n';
|
|
4
4
|
import { contentQueryErrorMessage } from '@elytracms/studio/lib/persona/labels';
|
|
5
5
|
import { FILTER_OPERATORS, compareListEntries, filterScalarOf, filterableFields, matchesFieldValue, validateWhere, } from '@elytracms/core/content';
|
|
@@ -340,17 +340,21 @@ const defaultIsPublished = () => false;
|
|
|
340
340
|
* otherwise. Matching runs on this (via `matchesFieldValue`) so it agrees with the
|
|
341
341
|
* delivery/listing path exactly: array-contains for `multiple` relations (EC-255)
|
|
342
342
|
* and whole-value `empty` semantics (EC-345). Sorting keeps its own scalar reduction.
|
|
343
|
+
*
|
|
344
|
+
* EC-580: read in the DISPLAYED view of the table's active locale (`resolveField`,
|
|
345
|
+
* the per-field form of `valuesInLocale`) — what the row shows is what it sorts and
|
|
346
|
+
* filters by, as delivery does in the request locale. It used to read a
|
|
347
|
+
* per-document default locale, so a `de` table sorted by English values.
|
|
343
348
|
*/
|
|
344
|
-
function rawValueOf(collection, doc, field, localeConfig, isPublished) {
|
|
349
|
+
function rawValueOf(collection, doc, field, localeConfig, locale, isPublished) {
|
|
345
350
|
if (field === STATE_FIELD)
|
|
346
351
|
return isPublished(doc) ? 'published' : 'draft';
|
|
347
352
|
if (!collection.fields.some((f) => f.name === field))
|
|
348
353
|
return undefined;
|
|
349
|
-
const locale = documentDefaultLocale(doc, localeConfig);
|
|
350
354
|
return resolveField(collection, doc, field, locale, localeConfig).value;
|
|
351
355
|
}
|
|
352
|
-
/** The scalar a document exposes for a SORT field, locale
|
|
353
|
-
function scalarOf(collection, doc, field, localeConfig, isPublished, meta = {}) {
|
|
356
|
+
/** The scalar a document exposes for a SORT field, in the active locale's view. */
|
|
357
|
+
function scalarOf(collection, doc, field, localeConfig, locale, isPublished, meta = {}) {
|
|
354
358
|
if (field === STATE_FIELD)
|
|
355
359
|
return isPublished(doc) ? 'published' : 'draft';
|
|
356
360
|
// EC-445: the pseudo-DATE columns sort by the source's materialized ISO timestamp
|
|
@@ -363,7 +367,6 @@ function scalarOf(collection, doc, field, localeConfig, isPublished, meta = {})
|
|
|
363
367
|
return null;
|
|
364
368
|
if (!collection.fields.some((f) => f.name === field))
|
|
365
369
|
return null;
|
|
366
|
-
const locale = documentDefaultLocale(doc, localeConfig);
|
|
367
370
|
return filterScalarOf(resolveField(collection, doc, field, locale, localeConfig).value);
|
|
368
371
|
}
|
|
369
372
|
/**
|
|
@@ -372,8 +375,8 @@ function scalarOf(collection, doc, field, localeConfig, isPublished, meta = {})
|
|
|
372
375
|
* `matchesFieldValue` over the RAW field value (so `contains`/`empty`/multiple-
|
|
373
376
|
* relation membership all behave the same on the table as on the live site).
|
|
374
377
|
*/
|
|
375
|
-
export function clauseMatches(collection, doc, localeConfig, clause, isPublished = defaultIsPublished) {
|
|
376
|
-
const value = rawValueOf(collection, doc, clause.field, localeConfig, isPublished);
|
|
378
|
+
export function clauseMatches(collection, doc, localeConfig, locale, clause, isPublished = defaultIsPublished) {
|
|
379
|
+
const value = rawValueOf(collection, doc, clause.field, localeConfig, locale, isPublished);
|
|
377
380
|
const condition = { [clause.op]: clause.value };
|
|
378
381
|
return matchesFieldValue(value, condition);
|
|
379
382
|
}
|
|
@@ -383,12 +386,12 @@ export function clauseMatches(collection, doc, localeConfig, clause, isPublished
|
|
|
383
386
|
* content accessors, the *table* may sort any rendered column — sorting is a
|
|
384
387
|
* presentation concern here; the `filterable` flag governs filters.
|
|
385
388
|
*/
|
|
386
|
-
export function sortViewDocuments(collection, docs, localeConfig, sort, isPublished = defaultIsPublished, meta = {}) {
|
|
389
|
+
export function sortViewDocuments(collection, docs, localeConfig, locale, sort, isPublished = defaultIsPublished, meta = {}) {
|
|
387
390
|
if (sort === null)
|
|
388
391
|
return [...docs];
|
|
389
392
|
const positions = new Map(docs.map((doc) => [
|
|
390
393
|
doc.id,
|
|
391
|
-
scalarOf(collection, doc, sort.field, localeConfig, isPublished, meta),
|
|
394
|
+
scalarOf(collection, doc, sort.field, localeConfig, locale, isPublished, meta),
|
|
392
395
|
]));
|
|
393
396
|
return [...docs].sort((a, b) => compareListEntries({ id: a.id, sortValue: positions.get(a.id) ?? null }, { id: b.id, sortValue: positions.get(b.id) ?? null }, sort.dir));
|
|
394
397
|
}
|
|
@@ -397,13 +400,13 @@ export function sortViewDocuments(collection, docs, localeConfig, sort, isPublis
|
|
|
397
400
|
* ones, sort. Invalid clauses never filter silently — they come back as
|
|
398
401
|
* issues for the UI to surface.
|
|
399
402
|
*/
|
|
400
|
-
export function applyViewState(collection, docs, localeConfig, state, isPublished = defaultIsPublished, meta = {}) {
|
|
403
|
+
export function applyViewState(collection, docs, localeConfig, locale, state, isPublished = defaultIsPublished, meta = {}) {
|
|
401
404
|
const issues = validateClauses(collection, state.filters);
|
|
402
405
|
const invalid = new Set(issues.map((issue) => issue.index));
|
|
403
406
|
const active = state.filters.filter((_, index) => !invalid.has(index));
|
|
404
|
-
const matched = docs.filter((doc) => active.every((clause) => clauseMatches(collection, doc, localeConfig, clause, isPublished)));
|
|
407
|
+
const matched = docs.filter((doc) => active.every((clause) => clauseMatches(collection, doc, localeConfig, locale, clause, isPublished)));
|
|
405
408
|
return {
|
|
406
|
-
docs: sortViewDocuments(collection, matched, localeConfig, state.sort, isPublished, meta),
|
|
409
|
+
docs: sortViewDocuments(collection, matched, localeConfig, locale, state.sort, isPublished, meta),
|
|
407
410
|
issues,
|
|
408
411
|
};
|
|
409
412
|
}
|