@elytracms/studio 0.0.101 → 0.0.103
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-meta-patch.test.ts +7 -27
- package/convex/assets.ts +6 -41
- package/convex/cms.ts +2 -29
- package/convex/delivery.ts +2 -2
- package/convex/ingest.test.ts +54 -21
- package/convex/ingest.ts +52 -15
- package/convex/record-functions.ts +15 -82
- package/convex/redirects.ts +11 -116
- package/convex/schema.ts +4 -23
- package/convex/triggers.ts +7 -14
- package/dist/components/ai/ai-chat-panel.js +8 -5
- package/dist/components/ai/ai-chat-panel.js.map +1 -1
- package/dist/components/ai/translate-document-dialog.d.ts +43 -0
- package/dist/components/ai/translate-document-dialog.js +104 -36
- package/dist/components/ai/translate-document-dialog.js.map +1 -1
- package/dist/components/ai/translate-skipped.d.ts +19 -3
- package/dist/components/ai/translate-skipped.js +34 -7
- package/dist/components/ai/translate-skipped.js.map +1 -1
- package/dist/components/assets/asset-detail.js +1 -2
- package/dist/components/assets/asset-detail.js.map +1 -1
- package/dist/components/assets/asset-metadata.d.ts +2 -9
- package/dist/components/assets/asset-metadata.js +3 -10
- package/dist/components/assets/asset-metadata.js.map +1 -1
- package/dist/components/assets/asset-search.js +1 -1
- package/dist/components/assets/asset-search.js.map +1 -1
- package/dist/components/assets/asset-selection.js +2 -2
- package/dist/components/assets/asset-selection.js.map +1 -1
- package/dist/components/assets/assets-collection-view-source.d.ts +2 -3
- package/dist/components/assets/assets-collection-view-source.js +2 -3
- package/dist/components/assets/assets-collection-view-source.js.map +1 -1
- package/dist/components/builder/__fixtures__/reuse-race.d.ts +30 -0
- package/dist/components/builder/__fixtures__/reuse-race.js +77 -0
- package/dist/components/builder/__fixtures__/reuse-race.js.map +1 -0
- package/dist/components/builder/composition-field-editor.js +3 -3
- package/dist/components/builder/composition-field-editor.js.map +1 -1
- package/dist/components/builder/reuse-context.d.ts +2 -1
- package/dist/components/builder/reuse-context.js +5 -20
- package/dist/components/builder/reuse-context.js.map +1 -1
- package/dist/components/builder/reuse.d.ts +18 -11
- package/dist/components/builder/reuse.js +12 -14
- package/dist/components/builder/reuse.js.map +1 -1
- package/dist/components/cms/asset-field.js +1 -3
- package/dist/components/cms/asset-field.js.map +1 -1
- package/dist/components/cms/cell.d.ts +2 -2
- package/dist/components/cms/cell.js +8 -18
- package/dist/components/cms/cell.js.map +1 -1
- package/dist/components/cms/detail-editor.js +2 -1
- package/dist/components/cms/detail-editor.js.map +1 -1
- package/dist/components/cms/field-control.js +13 -1
- package/dist/components/cms/field-control.js.map +1 -1
- package/dist/lib/ai/chat-context.d.ts +6 -2
- package/dist/lib/ai/chat-context.js +11 -5
- package/dist/lib/ai/chat-context.js.map +1 -1
- package/dist/lib/ai/knowledge.d.ts +9 -9
- package/dist/lib/ai/knowledge.js +22 -0
- package/dist/lib/ai/knowledge.js.map +1 -1
- package/dist/lib/ai/tools.d.ts +8 -1
- package/dist/lib/ai/tools.js +33 -15
- package/dist/lib/ai/tools.js.map +1 -1
- package/dist/lib/ai/translate-apply.d.ts +69 -0
- package/dist/lib/ai/translate-apply.js +36 -0
- package/dist/lib/ai/translate-apply.js.map +1 -0
- package/dist/lib/ai/translate-document.d.ts +101 -7
- package/dist/lib/ai/translate-document.js +189 -54
- package/dist/lib/ai/translate-document.js.map +1 -1
- package/dist/lib/ai/update-fields.js +5 -12
- package/dist/lib/ai/update-fields.js.map +1 -1
- package/dist/lib/ai/writer-form.d.ts +93 -1
- package/dist/lib/ai/writer-form.js +389 -37
- package/dist/lib/ai/writer-form.js.map +1 -1
- package/dist/lib/assets/upload.d.ts +1 -2
- package/dist/lib/assets/upload.js +1 -2
- package/dist/lib/assets/upload.js.map +1 -1
- package/dist/lib/engine-version.d.ts +1 -1
- package/dist/lib/engine-version.js +1 -1
- package/dist/lib/engine-version.js.map +1 -1
- package/dist/lib/i18n/messages.de.js +54 -4
- package/dist/lib/i18n/messages.de.js.map +1 -1
- package/dist/lib/i18n/messages.en.d.ts +44 -1
- package/dist/lib/i18n/messages.en.js +58 -4
- package/dist/lib/i18n/messages.en.js.map +1 -1
- package/dist/lib/operations/cms-persistence.d.ts +20 -10
- package/dist/lib/operations/cms-persistence.js +11 -3
- package/dist/lib/operations/cms-persistence.js.map +1 -1
- package/dist/lib/persistence/convex-adapter.js +7 -2
- package/dist/lib/persistence/convex-adapter.js.map +1 -1
- package/package.json +3 -3
- package/convex/asset-alt-migration.test.ts +0 -127
- package/convex/redirect-migration.test.ts +0 -205
- package/convex/version-kind-stamping.test.ts +0 -118
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"asset-metadata.js","sourceRoot":"","sources":["../../../src/components/assets/asset-metadata.ts"],"names":[],"mappings":"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","sourcesContent":["import type { CollectionDef, FieldDef } from '@elytracms/core/cms-core'\nimport { assetMetaIsLocalized, assetMetaValueAt, liftLegacyAlt, metaPatchBetween } from '@elytracms/core/persistence'\nimport type {\n AssetMetaDrift,\n AssetReadLocale,\n AssetRecord,\n AssetRepository,\n AssetWriteInput,\n} from '@elytracms/core/persistence'\n\n/**\n * Asset metadata editing (EC-152). A patch is applied through the\n * {@link AssetRepository} interface — in the studio that is the EC-141\n * operations facade, so every edit runs the `assets.upsert` command\n * (validate → authorize → mutate → event). Pure orchestration; injectable\n * for tests.\n */\n\n/**\n * System columns the upload pipeline owns — never editable metadata, so they're\n * excluded from the asset collection's schema-driven forms: the image field's\n * options dialog (EC-301, `field-control.tsx`) and the assets rail's detail\n * surface (EC-319, `asset-detail.tsx`) share this ONE reserved set so neither\n * drifts from the other. Everything else a project declares on the `kind:'asset'`\n * collection round-trips through the record's open `meta` bag.\n */\nexport const SYSTEM_ASSET_FIELDS: ReadonlySet<string> = new Set([\n 'filename',\n 'url',\n 'contentType',\n 'byteSize',\n 'width',\n 'height',\n 'storageKey',\n 'title',\n])\n\n/**\n * The asset collection's EDITABLE metadata fields — declared fields minus the\n * system columns above and anything explicitly `readOnly` (EC-301/EC-319).\n */\nexport function assetMetaFieldsOf(collection: CollectionDef): FieldDef[] {\n return collection.fields.filter((f) => !SYSTEM_ASSET_FIELDS.has(f.name) && !f.form?.readOnly)\n}\n\n/** Drop `undefined` values so a persisted meta bag stays JSON/Convex-clean. */\nexport function pruneMeta(meta: Record<string, unknown>): Record<string, unknown> {\n return Object.fromEntries(Object.entries(meta).filter(([, v]) => v !== undefined))\n}\n\n/** Human-readable byte size (`1536` → `1.5 KB`). Shared by the asset grid + detail. */\nexport function formatBytes(byteSize: number): string {\n if (byteSize < 1024) return `${byteSize} B`\n if (byteSize < 1024 * 1024) return `${(byteSize / 1024).toFixed(1)} KB`\n return `${(byteSize / (1024 * 1024)).toFixed(1)} MB`\n}\n\n/**\n * An editor's metadata change, in the EC-577 write rule: an ABSENT key leaves the\n * stored value untouched, `null` clears it, a value sets it.\n */\nexport interface AssetMetadataPatch {\n /** New display title: a value sets (trimmed), `null` or a trimmed-empty string clears. */\n title?: string | null\n /**\n * New focal point (EC-248), the normalized 0–1 hotspot threaded to delivery's\n * `object-position` (EC-230). `{x,y}` sets it, `null` clears it (\"Reset to\n * centre\").\n */\n focalPoint?: { x: number; y: number } | null\n /**\n * A per-key change to the schema-driven metadata bag (EC-301 → EC-634): alt,\n * copyright, credit, … keyed by the asset collection's `fields`. A key with a value\n * sets it, `null` removes it, a key the patch does not name keeps its STORED value —\n * applied to the bag as stored, inside the write (core `resolveAssetWrite`), so an\n * editor never writes back a stale copy of a key another session changed. Build it\n * with {@link assetMetaDraftPatch} from the draft the editor opened and the one it holds.\n * `meta.alt` is the asset's ONE shared alt (EC-587) — there is no separate alt key. There\n * is no whole-bag form here: every studio editor of an asset's metadata patches per key.\n */\n metaPatch?: Record<string, unknown>\n /**\n * EC-632 — the same per-key change to the per-locale buckets of the asset collection's\n * `localized: true` fields: `{ [locale]: { [field]: value | null } }`, applied per locale\n * and key to the stored buckets inside the write.\n */\n localizedMetaPatch?: Record<string, Record<string, unknown>>\n}\n\n/**\n * An asset's metadata as an editor holds it (EC-632) — the DOCUMENT shape, so editing it\n * is the mechanism documents use: the shared bag (a document's `values`) and the\n * per-locale buckets of the `localized: true` fields (its `localized`). The form reads and\n * writes one field in one locale ({@link assetMetaFieldValue} / {@link setAssetMetaField});\n * the save is a structural diff of the whole draft ({@link assetMetaDraftPatch}), so it\n * needs no config and never writes a key or a locale the editor did not change.\n */\nexport interface AssetMetaDraft {\n readonly meta: Readonly<Record<string, unknown>>\n readonly localizedMeta: Readonly<Record<string, Readonly<Record<string, unknown>>>>\n}\n\n/**\n * The fields of the asset collection an editor edits by — only the name matters here.\n * EC-659: whether a field is edited per locale is the read locale's LENS (`at.lens`, the\n * project's asset collection as declared now — the one delivery reads by), never the\n * form's own copy of the field: a mount's rail override cannot make the editor write a\n * translation delivery would not read.\n */\ntype MetaField = Pick<FieldDef, 'name'>\n\n/**\n * The draft an editor opens (EC-587 + EC-632): the stored bags, with a legacy `alt` column\n * lifted into the shared one (core `liftLegacyAlt`), so a not-yet-migrated asset shows the\n * alt every reader shows — and its first save moves it home.\n */\nexport function assetMetaDraft(record: Pick<AssetRecord, 'alt' | 'meta' | 'localizedMeta'>): AssetMetaDraft {\n const localizedMeta: Record<string, Record<string, unknown>> = {}\n for (const [locale, bucket] of Object.entries(record.localizedMeta ?? {})) localizedMeta[locale] = { ...bucket }\n return { meta: liftLegacyAlt(record)?.meta ?? { ...(record.meta ?? {}) }, localizedMeta }\n}\n\nconst hasOwn = (bag: Readonly<Record<string, unknown>> | undefined, key: string): boolean =>\n bag !== undefined && Object.prototype.hasOwnProperty.call(bag, key)\n\n/**\n * What the form shows for one field (EC-632) — read by THE positional rule delivery's view\n * reads by (core `assetMetaValueAt`, EC-659), with the editor's fallback (`'own'`): a field\n * the lens localizes shows the editor's locale's own value (an untranslated locale shows\n * empty — it falls back to the default locale on delivery); the default locale also shows a\n * value written before the field was localized (the shared bag — delivery's last-resort\n * fallback, so it IS that locale's value). Any other field the shared value — a translation\n * left from a former localization is drift, shown by no reader. Without a locale config,\n * the shared value.\n */\nexport function assetMetaFieldValue(draft: AssetMetaDraft, field: MetaField, at: AssetReadLocale | null): unknown {\n const read = assetMetaValueAt(draft, field.name, at, 'own')\n return read.found ? read.value : undefined\n}\n\n/**\n * Write one field of the draft (EC-632) — `undefined` removes the value. Where it goes is\n * the lens's answer (EC-659, `assetMetaIsLocalized` — the rule every reader reads by):\n * - a field the lens localizes goes into the editor's locale's bucket; in the DEFAULT\n * locale a value written before the field was localized leaves the shared bag — the\n * default bucket is its home now, so a clear there really clears it;\n * - any other field goes into the shared bag, and a value a former localization left in a\n * bucket is removed (one home per key — the stale value is drift, read by nobody; this\n * edit is how an editor clears it);\n * - without a locale config (`at` null) there is no lens: the shared bag, and every\n * bucket left alone — a translation is never removed on a guess.\n */\nexport function setAssetMetaField(\n draft: AssetMetaDraft,\n field: MetaField,\n at: AssetReadLocale | null,\n value: unknown,\n): AssetMetaDraft {\n const name = field.name\n const withValue = (bag: Readonly<Record<string, unknown>> | undefined): Record<string, unknown> => {\n const next = { ...(bag ?? {}) }\n if (value === undefined) delete next[name]\n else next[name] = value\n return next\n }\n const without = (bag: Readonly<Record<string, unknown>>): Record<string, unknown> => {\n const next = { ...bag }\n delete next[name]\n return next\n }\n // No locale config installed yet (no lens): the shared bag, the buckets left untouched.\n if (at === null) return { meta: withValue(draft.meta), localizedMeta: draft.localizedMeta }\n if (assetMetaIsLocalized(at, name)) {\n const bucket = withValue(draft.localizedMeta[at.locale])\n const localizedMeta = { ...draft.localizedMeta, [at.locale]: bucket }\n const meta = at.locale === at.default && hasOwn(draft.meta, name) ? without(draft.meta) : draft.meta\n return { meta, localizedMeta }\n }\n const localizedMeta: Record<string, Readonly<Record<string, unknown>>> = {}\n for (const [locale, bucket] of Object.entries(draft.localizedMeta)) {\n localizedMeta[locale] = hasOwn(bucket, name) ? without(bucket) : bucket\n }\n return { meta: withValue(draft.meta), localizedMeta }\n}\n\n/**\n * The per-key, per-locale patch of a metadata edit (EC-634 × EC-632): what changed between\n * the draft the editor OPENED ({@link assetMetaDraft} at open, or the draft of its last\n * successful save) and the draft it holds — `undefined` values pruned, a removed key\n * `null`, a locale it did not touch absent. Every asset metadata editor (the image options\n * dialog, the asset library's detail, the assets rail) sends this instead of its buffer.\n */\nexport function assetMetaDraftPatch(\n opened: AssetMetaDraft,\n edited: AssetMetaDraft,\n): Pick<AssetMetadataPatch, 'metaPatch' | 'localizedMetaPatch'> {\n const metaPatch = metaPatchBetween(pruneMeta({ ...opened.meta }), pruneMeta({ ...edited.meta }))\n const localizedMetaPatch: Record<string, Record<string, unknown>> = {}\n for (const locale of new Set([...Object.keys(opened.localizedMeta), ...Object.keys(edited.localizedMeta)])) {\n const patch = metaPatchBetween(\n pruneMeta({ ...(opened.localizedMeta[locale] ?? {}) }),\n pruneMeta({ ...(edited.localizedMeta[locale] ?? {}) }),\n )\n if (Object.keys(patch).length > 0) localizedMetaPatch[locale] = patch\n }\n return {\n ...(Object.keys(metaPatch).length > 0 ? { metaPatch } : {}),\n ...(Object.keys(localizedMetaPatch).length > 0 ? { localizedMetaPatch } : {}),\n }\n}\n\n/**\n * The asset detail's drift notice, as data (EC-659): the stored translations the lens does\n * not read (core `assetMetaDrift`), grouped by reason, each field listed with the locales\n * holding one — `alt (de, fr)`. Empty when there is none. Pure.\n */\nexport function assetMetaDriftSummary(\n drift: readonly AssetMetaDrift[],\n): Array<{ reason: AssetMetaDrift['reason']; fields: string }> {\n const lines: Array<{ reason: AssetMetaDrift['reason']; fields: string }> = []\n for (const reason of ['not-localized', 'undeclared'] as const) {\n const byField = new Map<string, string[]>()\n for (const entry of drift) {\n if (entry.reason !== reason) continue\n byField.set(entry.field, [...(byField.get(entry.field) ?? []), entry.locale])\n }\n if (byField.size === 0) continue\n const fields = [...byField.entries()]\n .sort(([a], [b]) => a.localeCompare(b))\n .map(([field, locales]) => `${field} (${locales.join(', ')})`)\n .join(', ')\n lines.push({ reason, fields })\n }\n return lines\n}\n\n/** The form's flat view of a draft in one locale — `{ [field]: value }` for the given fields. */\nexport function assetMetaFormValues(\n draft: AssetMetaDraft,\n fields: readonly MetaField[],\n at: AssetReadLocale | null,\n): Record<string, unknown> {\n return Object.fromEntries(fields.map((field) => [field.name, assetMetaFieldValue(draft, field, at)]))\n}\n\n/** A text column's write: a trimmed value sets, `null`/empty clears. */\nfunction textWrite(value: string | null): string | null {\n const trimmed = value?.trim() ?? ''\n return trimmed.length > 0 ? trimmed : null\n}\n\n/**\n * The asset write for a metadata edit (EC-577): ONLY the keys the editor changed,\n * with `null` for a clear. It deliberately restates nothing else — not the\n * upload-owned columns, and never `thumb`/`lqip`: a stale copy of those would\n * overwrite a preview backfill that finished while the dialog was open, and a\n * clear expressed as an omitted key is exactly what never persisted on Convex.\n *\n * EC-634: `meta` travels as the per-key `metaPatch` — never the whole bag, which would\n * carry a stale copy of every key the editor did not change. An empty patch is not sent.\n * The legacy `alt` column (EC-587) is lifted into the stored bag and cleared by the write\n * rule itself, in the same transaction, whenever a patch is applied — so an alt the editor\n * did not touch survives, and one the editor cleared stays cleared (ASSETS D21).\n */\nexport function assetMetadataWrite(assetId: string, patch: AssetMetadataPatch): AssetWriteInput {\n const metaPatch = patch.metaPatch\n const localizedMetaPatch = patch.localizedMetaPatch\n return {\n id: assetId,\n ...(patch.title !== undefined ? { title: textWrite(patch.title) } : {}),\n ...(patch.focalPoint !== undefined ? { focalPoint: patch.focalPoint } : {}),\n ...(metaPatch !== undefined && Object.keys(metaPatch).length > 0 ? { metaPatch } : {}),\n ...(localizedMetaPatch !== undefined && Object.keys(localizedMetaPatch).length > 0 ? { localizedMetaPatch } : {}),\n }\n}\n\nexport type UpdateAssetMetadataResult =\n | { ok: true; asset: AssetRecord }\n | { ok: false; message: string }\n\n/**\n * Persist a metadata edit via the repository (the operations facade in the\n * studio). No read first: the write carries only the change, and the adapter\n * resolves it against the stored record inside its own transaction (EC-577). An\n * asset that no longer exists is refused there (a metadata write cannot create\n * one). Failures come back as a structured result — never a throw the UI has to\n * guess about.\n */\nexport async function updateAssetMetadata(\n deps: { assets: AssetRepository },\n projectId: string,\n assetId: string,\n patch: AssetMetadataPatch,\n): Promise<UpdateAssetMetadataResult> {\n try {\n const asset = await deps.assets.upsertAsset(projectId, assetMetadataWrite(assetId, patch))\n return { ok: true, asset }\n } catch (cause) {\n return { ok: false, message: cause instanceof Error ? cause.message : String(cause) }\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"asset-metadata.js","sourceRoot":"","sources":["../../../src/components/assets/asset-metadata.ts"],"names":[],"mappings":"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","sourcesContent":["import type { CollectionDef, FieldDef } from '@elytracms/core/cms-core'\nimport { assetMetaIsLocalized, assetMetaValueAt, metaPatchBetween } from '@elytracms/core/persistence'\nimport type {\n AssetMetaDrift,\n AssetReadLocale,\n AssetRecord,\n AssetRepository,\n AssetWriteInput,\n} from '@elytracms/core/persistence'\n\n/**\n * Asset metadata editing (EC-152). A patch is applied through the\n * {@link AssetRepository} interface — in the studio that is the EC-141\n * operations facade, so every edit runs the `assets.upsert` command\n * (validate → authorize → mutate → event). Pure orchestration; injectable\n * for tests.\n */\n\n/**\n * System columns the upload pipeline owns — never editable metadata, so they're\n * excluded from the asset collection's schema-driven forms: the image field's\n * options dialog (EC-301, `field-control.tsx`) and the assets rail's detail\n * surface (EC-319, `asset-detail.tsx`) share this ONE reserved set so neither\n * drifts from the other. Everything else a project declares on the `kind:'asset'`\n * collection round-trips through the record's open `meta` bag.\n */\nexport const SYSTEM_ASSET_FIELDS: ReadonlySet<string> = new Set([\n 'filename',\n 'url',\n 'contentType',\n 'byteSize',\n 'width',\n 'height',\n 'storageKey',\n 'title',\n])\n\n/**\n * The asset collection's EDITABLE metadata fields — declared fields minus the\n * system columns above and anything explicitly `readOnly` (EC-301/EC-319).\n */\nexport function assetMetaFieldsOf(collection: CollectionDef): FieldDef[] {\n return collection.fields.filter((f) => !SYSTEM_ASSET_FIELDS.has(f.name) && !f.form?.readOnly)\n}\n\n/** Drop `undefined` values so a persisted meta bag stays JSON/Convex-clean. */\nexport function pruneMeta(meta: Record<string, unknown>): Record<string, unknown> {\n return Object.fromEntries(Object.entries(meta).filter(([, v]) => v !== undefined))\n}\n\n/** Human-readable byte size (`1536` → `1.5 KB`). Shared by the asset grid + detail. */\nexport function formatBytes(byteSize: number): string {\n if (byteSize < 1024) return `${byteSize} B`\n if (byteSize < 1024 * 1024) return `${(byteSize / 1024).toFixed(1)} KB`\n return `${(byteSize / (1024 * 1024)).toFixed(1)} MB`\n}\n\n/**\n * An editor's metadata change, in the EC-577 write rule: an ABSENT key leaves the\n * stored value untouched, `null` clears it, a value sets it.\n */\nexport interface AssetMetadataPatch {\n /** New display title: a value sets (trimmed), `null` or a trimmed-empty string clears. */\n title?: string | null\n /**\n * New focal point (EC-248), the normalized 0–1 hotspot threaded to delivery's\n * `object-position` (EC-230). `{x,y}` sets it, `null` clears it (\"Reset to\n * centre\").\n */\n focalPoint?: { x: number; y: number } | null\n /**\n * A per-key change to the schema-driven metadata bag (EC-301 → EC-634): alt,\n * copyright, credit, … keyed by the asset collection's `fields`. A key with a value\n * sets it, `null` removes it, a key the patch does not name keeps its STORED value —\n * applied to the bag as stored, inside the write (core `resolveAssetWrite`), so an\n * editor never writes back a stale copy of a key another session changed. Build it\n * with {@link assetMetaDraftPatch} from the draft the editor opened and the one it holds.\n * `meta.alt` is the asset's ONE shared alt (EC-587) — there is no separate alt key. There\n * is no whole-bag form here: every studio editor of an asset's metadata patches per key.\n */\n metaPatch?: Record<string, unknown>\n /**\n * EC-632 — the same per-key change to the per-locale buckets of the asset collection's\n * `localized: true` fields: `{ [locale]: { [field]: value | null } }`, applied per locale\n * and key to the stored buckets inside the write.\n */\n localizedMetaPatch?: Record<string, Record<string, unknown>>\n}\n\n/**\n * An asset's metadata as an editor holds it (EC-632) — the DOCUMENT shape, so editing it\n * is the mechanism documents use: the shared bag (a document's `values`) and the\n * per-locale buckets of the `localized: true` fields (its `localized`). The form reads and\n * writes one field in one locale ({@link assetMetaFieldValue} / {@link setAssetMetaField});\n * the save is a structural diff of the whole draft ({@link assetMetaDraftPatch}), so it\n * needs no config and never writes a key or a locale the editor did not change.\n */\nexport interface AssetMetaDraft {\n readonly meta: Readonly<Record<string, unknown>>\n readonly localizedMeta: Readonly<Record<string, Readonly<Record<string, unknown>>>>\n}\n\n/**\n * The fields of the asset collection an editor edits by — only the name matters here.\n * EC-659: whether a field is edited per locale is the read locale's LENS (`at.lens`, the\n * project's asset collection as declared now — the one delivery reads by), never the\n * form's own copy of the field: a mount's rail override cannot make the editor write a\n * translation delivery would not read.\n */\ntype MetaField = Pick<FieldDef, 'name'>\n\n/** The draft an editor opens (EC-587 + EC-632): the stored bags, copied. */\nexport function assetMetaDraft(record: Pick<AssetRecord, 'meta' | 'localizedMeta'>): AssetMetaDraft {\n const localizedMeta: Record<string, Record<string, unknown>> = {}\n for (const [locale, bucket] of Object.entries(record.localizedMeta ?? {})) localizedMeta[locale] = { ...bucket }\n return { meta: { ...(record.meta ?? {}) }, localizedMeta }\n}\n\nconst hasOwn = (bag: Readonly<Record<string, unknown>> | undefined, key: string): boolean =>\n bag !== undefined && Object.prototype.hasOwnProperty.call(bag, key)\n\n/**\n * What the form shows for one field (EC-632) — read by THE positional rule delivery's view\n * reads by (core `assetMetaValueAt`, EC-659), with the editor's fallback (`'own'`): a field\n * the lens localizes shows the editor's locale's own value (an untranslated locale shows\n * empty — it falls back to the default locale on delivery); the default locale also shows a\n * value written before the field was localized (the shared bag — delivery's last-resort\n * fallback, so it IS that locale's value). Any other field the shared value — a translation\n * left from a former localization is drift, shown by no reader. Without a locale config,\n * the shared value.\n */\nexport function assetMetaFieldValue(draft: AssetMetaDraft, field: MetaField, at: AssetReadLocale | null): unknown {\n const read = assetMetaValueAt(draft, field.name, at, 'own')\n return read.found ? read.value : undefined\n}\n\n/**\n * Write one field of the draft (EC-632) — `undefined` removes the value. Where it goes is\n * the lens's answer (EC-659, `assetMetaIsLocalized` — the rule every reader reads by):\n * - a field the lens localizes goes into the editor's locale's bucket; in the DEFAULT\n * locale a value written before the field was localized leaves the shared bag — the\n * default bucket is its home now, so a clear there really clears it;\n * - any other field goes into the shared bag, and a value a former localization left in a\n * bucket is removed (one home per key — the stale value is drift, read by nobody; this\n * edit is how an editor clears it);\n * - without a locale config (`at` null) there is no lens: the shared bag, and every\n * bucket left alone — a translation is never removed on a guess.\n */\nexport function setAssetMetaField(\n draft: AssetMetaDraft,\n field: MetaField,\n at: AssetReadLocale | null,\n value: unknown,\n): AssetMetaDraft {\n const name = field.name\n const withValue = (bag: Readonly<Record<string, unknown>> | undefined): Record<string, unknown> => {\n const next = { ...(bag ?? {}) }\n if (value === undefined) delete next[name]\n else next[name] = value\n return next\n }\n const without = (bag: Readonly<Record<string, unknown>>): Record<string, unknown> => {\n const next = { ...bag }\n delete next[name]\n return next\n }\n // No locale config installed yet (no lens): the shared bag, the buckets left untouched.\n if (at === null) return { meta: withValue(draft.meta), localizedMeta: draft.localizedMeta }\n if (assetMetaIsLocalized(at, name)) {\n const bucket = withValue(draft.localizedMeta[at.locale])\n const localizedMeta = { ...draft.localizedMeta, [at.locale]: bucket }\n const meta = at.locale === at.default && hasOwn(draft.meta, name) ? without(draft.meta) : draft.meta\n return { meta, localizedMeta }\n }\n const localizedMeta: Record<string, Readonly<Record<string, unknown>>> = {}\n for (const [locale, bucket] of Object.entries(draft.localizedMeta)) {\n localizedMeta[locale] = hasOwn(bucket, name) ? without(bucket) : bucket\n }\n return { meta: withValue(draft.meta), localizedMeta }\n}\n\n/**\n * The per-key, per-locale patch of a metadata edit (EC-634 × EC-632): what changed between\n * the draft the editor OPENED ({@link assetMetaDraft} at open, or the draft of its last\n * successful save) and the draft it holds — `undefined` values pruned, a removed key\n * `null`, a locale it did not touch absent. Every asset metadata editor (the image options\n * dialog, the asset library's detail, the assets rail) sends this instead of its buffer.\n */\nexport function assetMetaDraftPatch(\n opened: AssetMetaDraft,\n edited: AssetMetaDraft,\n): Pick<AssetMetadataPatch, 'metaPatch' | 'localizedMetaPatch'> {\n const metaPatch = metaPatchBetween(pruneMeta({ ...opened.meta }), pruneMeta({ ...edited.meta }))\n const localizedMetaPatch: Record<string, Record<string, unknown>> = {}\n for (const locale of new Set([...Object.keys(opened.localizedMeta), ...Object.keys(edited.localizedMeta)])) {\n const patch = metaPatchBetween(\n pruneMeta({ ...(opened.localizedMeta[locale] ?? {}) }),\n pruneMeta({ ...(edited.localizedMeta[locale] ?? {}) }),\n )\n if (Object.keys(patch).length > 0) localizedMetaPatch[locale] = patch\n }\n return {\n ...(Object.keys(metaPatch).length > 0 ? { metaPatch } : {}),\n ...(Object.keys(localizedMetaPatch).length > 0 ? { localizedMetaPatch } : {}),\n }\n}\n\n/**\n * The asset detail's drift notice, as data (EC-659): the stored translations the lens does\n * not read (core `assetMetaDrift`), grouped by reason, each field listed with the locales\n * holding one — `alt (de, fr)`. Empty when there is none. Pure.\n */\nexport function assetMetaDriftSummary(\n drift: readonly AssetMetaDrift[],\n): Array<{ reason: AssetMetaDrift['reason']; fields: string }> {\n const lines: Array<{ reason: AssetMetaDrift['reason']; fields: string }> = []\n for (const reason of ['not-localized', 'undeclared'] as const) {\n const byField = new Map<string, string[]>()\n for (const entry of drift) {\n if (entry.reason !== reason) continue\n byField.set(entry.field, [...(byField.get(entry.field) ?? []), entry.locale])\n }\n if (byField.size === 0) continue\n const fields = [...byField.entries()]\n .sort(([a], [b]) => a.localeCompare(b))\n .map(([field, locales]) => `${field} (${locales.join(', ')})`)\n .join(', ')\n lines.push({ reason, fields })\n }\n return lines\n}\n\n/** The form's flat view of a draft in one locale — `{ [field]: value }` for the given fields. */\nexport function assetMetaFormValues(\n draft: AssetMetaDraft,\n fields: readonly MetaField[],\n at: AssetReadLocale | null,\n): Record<string, unknown> {\n return Object.fromEntries(fields.map((field) => [field.name, assetMetaFieldValue(draft, field, at)]))\n}\n\n/** A text column's write: a trimmed value sets, `null`/empty clears. */\nfunction textWrite(value: string | null): string | null {\n const trimmed = value?.trim() ?? ''\n return trimmed.length > 0 ? trimmed : null\n}\n\n/**\n * The asset write for a metadata edit (EC-577): ONLY the keys the editor changed,\n * with `null` for a clear. It deliberately restates nothing else — not the\n * upload-owned columns, and never `thumb`/`lqip`: a stale copy of those would\n * overwrite a preview backfill that finished while the dialog was open, and a\n * clear expressed as an omitted key is exactly what never persisted on Convex.\n *\n * EC-634: `meta` travels as the per-key `metaPatch` — never the whole bag, which would\n * carry a stale copy of every key the editor did not change. An empty patch is not sent.\n */\nexport function assetMetadataWrite(assetId: string, patch: AssetMetadataPatch): AssetWriteInput {\n const metaPatch = patch.metaPatch\n const localizedMetaPatch = patch.localizedMetaPatch\n return {\n id: assetId,\n ...(patch.title !== undefined ? { title: textWrite(patch.title) } : {}),\n ...(patch.focalPoint !== undefined ? { focalPoint: patch.focalPoint } : {}),\n ...(metaPatch !== undefined && Object.keys(metaPatch).length > 0 ? { metaPatch } : {}),\n ...(localizedMetaPatch !== undefined && Object.keys(localizedMetaPatch).length > 0 ? { localizedMetaPatch } : {}),\n }\n}\n\nexport type UpdateAssetMetadataResult =\n | { ok: true; asset: AssetRecord }\n | { ok: false; message: string }\n\n/**\n * Persist a metadata edit via the repository (the operations facade in the\n * studio). No read first: the write carries only the change, and the adapter\n * resolves it against the stored record inside its own transaction (EC-577). An\n * asset that no longer exists is refused there (a metadata write cannot create\n * one). Failures come back as a structured result — never a throw the UI has to\n * guess about.\n */\nexport async function updateAssetMetadata(\n deps: { assets: AssetRepository },\n projectId: string,\n assetId: string,\n patch: AssetMetadataPatch,\n): Promise<UpdateAssetMetadataResult> {\n try {\n const asset = await deps.assets.upsertAsset(projectId, assetMetadataWrite(assetId, patch))\n return { ok: true, asset }\n } catch (cause) {\n return { ok: false, message: cause instanceof Error ? cause.message : String(cause) }\n }\n}\n"]}
|
|
@@ -13,7 +13,7 @@ export function filterAssets(assets, query, at) {
|
|
|
13
13
|
if (q.length === 0)
|
|
14
14
|
return [...assets];
|
|
15
15
|
return assets.filter((asset) =>
|
|
16
|
-
// EC-587 (ASSETS D20): the asset's alt through THE reader (`meta.alt`, its home) —
|
|
16
|
+
// EC-587 (ASSETS D20): the asset's alt through THE reader (`meta.alt`, its one home) —
|
|
17
17
|
// EC-632: in every locale it is translated into, so a search finds it from any tab.
|
|
18
18
|
[asset.filename, ...searchableAlts(asset, at), asset.title].some((value) => value != null && value.toLowerCase().includes(q)));
|
|
19
19
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"asset-search.js","sourceRoot":"","sources":["../../../src/components/assets/asset-search.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,6BAA6B,CAAA;AAGtD;;;;;;;;GAQG;AACH,MAAM,UAAU,YAAY,CAAC,MAA8B,EAAE,KAAa,EAAE,EAA0B;IACpG,MAAM,CAAC,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAA;IACpC,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,CAAC,GAAG,MAAM,CAAC,CAAA;IACtC,OAAO,MAAM,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE;IAC7B,
|
|
1
|
+
{"version":3,"file":"asset-search.js","sourceRoot":"","sources":["../../../src/components/assets/asset-search.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,6BAA6B,CAAA;AAGtD;;;;;;;;GAQG;AACH,MAAM,UAAU,YAAY,CAAC,MAA8B,EAAE,KAAa,EAAE,EAA0B;IACpG,MAAM,CAAC,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAA;IACpC,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,CAAC,GAAG,MAAM,CAAC,CAAA;IACtC,OAAO,MAAM,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE;IAC7B,uFAAuF;IACvF,oFAAoF;IACpF,CAAC,KAAK,CAAC,QAAQ,EAAE,GAAG,cAAc,CAAC,KAAK,EAAE,EAAE,CAAC,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC,IAAI,CAC9D,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,IAAI,IAAI,IAAI,KAAK,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC,CAC5D,CACF,CAAA;AACH,CAAC;AAED;;;;GAIG;AACH,SAAS,cAAc,CAAC,KAAkB,EAAE,EAA0B;IACpE,IAAI,EAAE,KAAK,IAAI;QAAE,OAAO,CAAC,QAAQ,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC,CAAA;IAC/C,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,aAAa,IAAI,EAAE,CAAC,CAAA;IACtD,OAAO,CAAC,QAAQ,CAAC,KAAK,EAAE,IAAI,CAAC,EAAE,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,QAAQ,CAAC,KAAK,EAAE,EAAE,GAAG,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,CAAA;AACjH,CAAC","sourcesContent":["import { assetAlt } from '@elytracms/core/persistence'\nimport type { AssetReadLocale, AssetRecord } from '@elytracms/core/persistence'\n\n/**\n * Client-side asset search (EC-152): case-insensitive substring match over\n * filename, alt text, and title. Pure and deterministic — input order is\n * preserved; an empty/whitespace query matches everything.\n *\n * EC-659: the alts searched are the ones the readers show — through `at`'s lens (the\n * project's asset collection as declared now), so a translation the schema no longer\n * reads (drift) does not make an asset match.\n */\nexport function filterAssets(assets: readonly AssetRecord[], query: string, at: AssetReadLocale | null): AssetRecord[] {\n const q = query.trim().toLowerCase()\n if (q.length === 0) return [...assets]\n return assets.filter((asset) =>\n // EC-587 (ASSETS D20): the asset's alt through THE reader (`meta.alt`, its one home) —\n // EC-632: in every locale it is translated into, so a search finds it from any tab.\n [asset.filename, ...searchableAlts(asset, at), asset.title].some(\n (value) => value != null && value.toLowerCase().includes(q),\n ),\n )\n}\n\n/**\n * An asset's alt in the shared bag and in each locale it holds a bucket for (EC-632),\n * through THE reader — each locale read on its own (its bucket, else the shared value),\n * with `at`'s lens deciding whether a bucket is read at all (EC-659).\n */\nfunction searchableAlts(asset: AssetRecord, at: AssetReadLocale | null): (string | null)[] {\n if (at === null) return [assetAlt(asset, null)]\n const locales = Object.keys(asset.localizedMeta ?? {})\n return [assetAlt(asset, null), ...locales.map((locale) => assetAlt(asset, { ...at, locale, default: locale }))]\n}\n"]}
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* Pure value logic for CMS `asset` fields (EC-075 → EC-152): a field stores one
|
|
3
3
|
* placement (cardinality `one`) or a list of them (`many`). These helpers read stored
|
|
4
4
|
* values and apply a picker selection without the UI owning the rules. EC-586: every
|
|
5
|
-
* placement a pick writes is the canonical `{ type:'asset', id, … }` envelope
|
|
6
|
-
*
|
|
5
|
+
* placement a pick writes is the canonical `{ type:'asset', id, … }` envelope, and only
|
|
6
|
+
* an envelope reads as a placement.
|
|
7
7
|
*
|
|
8
8
|
* EC-558: the pick operations below are the ones the table cell and the detail
|
|
9
9
|
* editor BOTH commit. They work out which ids the field holds next, and hand the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"asset-selection.js","sourceRoot":"","sources":["../../../src/components/assets/asset-selection.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,EAAE,oBAAoB,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAA;AAE5E,qFAAqF;AACrF,MAAM,UAAU,UAAU,CAAC,KAAc;IACvC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,EAAE,CAAA;IACpC,OAAO,KAAK,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC,MAAM,CAAC,CAAC,EAAE,EAAgB,EAAE,CAAC,EAAE,KAAK,SAAS,CAAC,CAAA;AAC9E,CAAC;AAED,iEAAiE;AACjE,MAAM,UAAU,eAAe,CAAC,KAAc;IAC5C,MAAM,MAAM,GAAG,WAAW,CAAC,KAAK,CAAC,CAAA;IACjC,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO,MAAM,CAAA;IACvC,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,WAAW,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,IAAI,CAAA;IAC9D,OAAO,IAAI,CAAA;AACb,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,mBAAmB,CACjC,OAAgB,EAChB,MAAqB,EACrB,QAAiB;IAEjB,IAAI,CAAC,QAAQ;QAAE,OAAO,oBAAoB,CAAC,OAAO,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC,CAAA;IACpF,IAAI,MAAM,KAAK,IAAI;QAAE,OAAO,OAAO,CAAA;IACnC,OAAO,qBAAqB,CAAC,OAAO,EAAE,CAAC,MAAM,CAAC,CAAC,CAAA;AACjD,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,qBAAqB,CAAC,OAAgB,EAAE,MAAyB;IAC/E,MAAM,GAAG,GAAG,UAAU,CAAC,OAAO,CAAC,CAAA;IAC/B,MAAM,KAAK,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,CAAA;IACpE,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,OAAO,CAAA;IACtC,OAAO,oBAAoB,CAAC,OAAO,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC,GAAG,GAAG,EAAE,GAAG,KAAK,CAAC,EAAE,CAAC,CAAA;AACnF,CAAC","sourcesContent":["/**\n * Pure value logic for CMS `asset` fields (EC-075 → EC-152): a field stores one\n * placement (cardinality `one`) or a list of them (`many`). These helpers read stored\n * values and apply a picker selection without the UI owning the rules. EC-586: every\n * placement a pick writes is the canonical `{ type:'asset', id, … }` envelope
|
|
1
|
+
{"version":3,"file":"asset-selection.js","sourceRoot":"","sources":["../../../src/components/assets/asset-selection.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,EAAE,oBAAoB,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAA;AAE5E,qFAAqF;AACrF,MAAM,UAAU,UAAU,CAAC,KAAc;IACvC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,EAAE,CAAA;IACpC,OAAO,KAAK,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC,MAAM,CAAC,CAAC,EAAE,EAAgB,EAAE,CAAC,EAAE,KAAK,SAAS,CAAC,CAAA;AAC9E,CAAC;AAED,iEAAiE;AACjE,MAAM,UAAU,eAAe,CAAC,KAAc;IAC5C,MAAM,MAAM,GAAG,WAAW,CAAC,KAAK,CAAC,CAAA;IACjC,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO,MAAM,CAAA;IACvC,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,WAAW,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,IAAI,CAAA;IAC9D,OAAO,IAAI,CAAA;AACb,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,mBAAmB,CACjC,OAAgB,EAChB,MAAqB,EACrB,QAAiB;IAEjB,IAAI,CAAC,QAAQ;QAAE,OAAO,oBAAoB,CAAC,OAAO,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC,CAAA;IACpF,IAAI,MAAM,KAAK,IAAI;QAAE,OAAO,OAAO,CAAA;IACnC,OAAO,qBAAqB,CAAC,OAAO,EAAE,CAAC,MAAM,CAAC,CAAC,CAAA;AACjD,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,qBAAqB,CAAC,OAAgB,EAAE,MAAyB;IAC/E,MAAM,GAAG,GAAG,UAAU,CAAC,OAAO,CAAC,CAAA;IAC/B,MAAM,KAAK,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,CAAA;IACpE,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,OAAO,CAAA;IACtC,OAAO,oBAAoB,CAAC,OAAO,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC,GAAG,GAAG,EAAE,GAAG,KAAK,CAAC,EAAE,CAAC,CAAA;AACnF,CAAC","sourcesContent":["/**\n * Pure value logic for CMS `asset` fields (EC-075 → EC-152): a field stores one\n * placement (cardinality `one`) or a list of them (`many`). These helpers read stored\n * values and apply a picker selection without the UI owning the rules. EC-586: every\n * placement a pick writes is the canonical `{ type:'asset', id, … }` envelope, and only\n * an envelope reads as a placement.\n *\n * EC-558: the pick operations below are the ones the table cell and the detail\n * editor BOTH commit. They work out which ids the field holds next, and hand the\n * per-use sub-field carry to the one rule, `carryAssetPlacements` (core, EC-587 —\n * AI `updateFields` uses it too). Neither writer rebuilds the value itself; a gallery\n * remove is core's `removeAssetPlacementAt`.\n */\nimport { carryAssetPlacements, readAssetId } from '@elytracms/core/cms-core'\n\n/** The id list a `many` asset field currently holds (unreadable entries dropped). */\nexport function assetIdsOf(value: unknown): string[] {\n if (!Array.isArray(value)) return []\n return value.map(readAssetId).filter((id): id is string => id !== undefined)\n}\n\n/** The single id a `one` asset field currently holds, if any. */\nexport function singleAssetIdOf(value: unknown): string | null {\n const direct = readAssetId(value)\n if (direct !== undefined) return direct\n if (Array.isArray(value)) return readAssetId(value[0]) ?? null\n return null\n}\n\n/**\n * Apply a picker selection to the stored field value, keeping every placement's\n * per-use sub-fields (EC-558).\n * - `one`: the picked id replaces the image and keeps the placement's sub-fields.\n * Picking the id it already shows is a no-op, and `null` clears it (`undefined`).\n * - `many`: the picked id is appended once, and held entries stay as they are.\n * An already-present id and `null` are no-ops.\n *\n * A no-op returns `current` itself, so a caller can skip the write.\n */\nexport function applyAssetSelection(\n current: unknown,\n picked: string | null,\n multiple: boolean,\n): unknown {\n if (!multiple) return carryAssetPlacements(current, { multiple: false, id: picked })\n if (picked === null) return current\n return appendAssetSelections(current, [picked])\n}\n\n/**\n * Attach several picked ids to a `many` field at once: an in-picker or drop upload\n * batch (EC-460). Each id not already held is appended once, in arrival order, and\n * held entries stay as they are. Returns `current` itself when nothing is new.\n */\nexport function appendAssetSelections(current: unknown, picked: readonly string[]): unknown {\n const ids = assetIdsOf(current)\n const fresh = [...new Set(picked)].filter((id) => !ids.includes(id))\n if (fresh.length === 0) return current\n return carryAssetPlacements(current, { multiple: true, ids: [...ids, ...fresh] })\n}\n"]}
|
|
@@ -30,9 +30,8 @@ export declare const ASSET_COLLECTION_CAPABILITIES: CollectionViewCapabilities;
|
|
|
30
30
|
* schema or not, so a save hands the bag back whole (D22). Declared fields then
|
|
31
31
|
* take their canonical reading on top — a column never loses to a same-named meta
|
|
32
32
|
* key: a system column (`filename`, `url`, dimensions, …) reads straight off the
|
|
33
|
-
* record, and `alt` reads through THE reader (`assetAlt`, EC-587 — `meta.alt`,
|
|
34
|
-
*
|
|
35
|
-
* field's options dialog show.
|
|
33
|
+
* record, and `alt` reads through THE reader (`assetAlt`, EC-587 — `meta.alt`, its one
|
|
34
|
+
* home), exactly what delivery and the image field's options dialog show.
|
|
36
35
|
*
|
|
37
36
|
* EC-632: the values are the metadata's view in `at` (the studio's locale) — a
|
|
38
37
|
* `localized: true` field shows that locale's value, with delivery's fallback, held in
|
|
@@ -80,9 +80,8 @@ function systemFieldValue(record, name) {
|
|
|
80
80
|
* schema or not, so a save hands the bag back whole (D22). Declared fields then
|
|
81
81
|
* take their canonical reading on top — a column never loses to a same-named meta
|
|
82
82
|
* key: a system column (`filename`, `url`, dimensions, …) reads straight off the
|
|
83
|
-
* record, and `alt` reads through THE reader (`assetAlt`, EC-587 — `meta.alt`,
|
|
84
|
-
*
|
|
85
|
-
* field's options dialog show.
|
|
83
|
+
* record, and `alt` reads through THE reader (`assetAlt`, EC-587 — `meta.alt`, its one
|
|
84
|
+
* home), exactly what delivery and the image field's options dialog show.
|
|
86
85
|
*
|
|
87
86
|
* EC-632: the values are the metadata's view in `at` (the studio's locale) — a
|
|
88
87
|
* `localized: true` field shows that locale's value, with delivery's fallback, held in
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"assets-collection-view-source.js","sourceRoot":"","sources":["../../../src/components/assets/assets-collection-view-source.ts"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useMemo } from 'react'\nimport type { CmsDocument, CollectionDef, DocumentRef, Locale, LocaleConfig } from '@elytracms/core/cms-core'\nimport { createCollectionRegistry, semanticValueEqual, validateDocument } from '@elytracms/core/cms-core'\nimport { assetAlt, assetMetaIsLocalized, assetMetaView } from '@elytracms/core/persistence'\nimport type { AssetReadLocale, AssetRecord, ReferenceTarget } from '@elytracms/core/persistence'\nimport type { OperationError } from '@elytracms/core/operations'\nimport { studioPath } from '@elytracms/studio/router'\nimport type { DeleteAssetResult } from '../../lib/assets'\nimport type { CollectionViewSource, CollectionViewCapabilities } from '../../lib/collection-view/collection-view-source'\nimport { useAssets } from './assets-store'\nimport { SYSTEM_ASSET_FIELDS, assetMetaDraft, assetMetaDraftPatch, pruneMeta, setAssetMetaField } from './asset-metadata'\nimport type { AssetMetadataPatch } from './asset-metadata'\nimport { useAssetReadLocale } from './asset-locale'\nimport { metaBagProjection, normalizeOkResult, useRailViewSource } from '../rail-view-source'\nimport { parseLocaleConfig } from '@elytracms/core/cms-core'\n\n/**\n * The assets rail has no locale switcher of its own (capability `locales: false`): it\n * reads in ONE locale — the studio's (EC-632), a localized field's value in that locale —\n * so its lens is a one-locale document in that locale. Before a project's locales are\n * installed, the engine's neutral `en` (shared metadata only: nothing is localized then).\n */\nconst ASSETS_LOCALE: Locale = 'en'\nfunction railLocaleConfig(at: AssetReadLocale | null): LocaleConfig {\n const locale = at?.locale ?? ASSETS_LOCALE\n return parseLocaleConfig({ default: locale, locales: [locale] })\n}\n\n/**\n * The capability profile of the assets rail (EC-319, EC-433). Hoisted to a\n * module-level constant (no prop/state dependency) so it's both the value the\n * hook returns AND independently unit-testable.\n *\n * `create` is off — a new asset comes from an upload, never a blank draft.\n * `remove` + `bulk` are ON since EC-433: the list's row/bulk delete used to be\n * off deliberately because the generic `ReferenceConfirmDialog` enumerated\n * inbound references as `{ type: 'document' }` targets — the wrong index shape\n * for an asset — which would have shown \"not referenced\" for an asset that IS\n * referenced. That pre-flight now queries the shape THIS source declares via\n * {@link assetReferenceTarget} (`{ type: 'asset' }`, the same lookup the\n * detail surface runs), and the confirmed delete dispatches the SAME checked\n * store delete the detail surface uses (`deleteAsset`: original + EC-400 thumb\n * blobs, then the record — EC-420 lqips ride on the record). Nothing else\n * applies to assets: no publish/versions/locales/inline-edit/live-URL/\n * hierarchy/import-export.\n */\nexport const ASSET_COLLECTION_CAPABILITIES: CollectionViewCapabilities = {\n publish: false,\n versions: false,\n locales: false,\n inlineEdit: false,\n create: false,\n duplicate: false, // EC-442: blob duplication is a different feature, out of scope\n remove: true,\n bulk: true,\n liveUrl: false,\n hierarchy: false,\n importExport: false,\n adminOnly: false,\n}\n\n/** Read one of the record's fixed, upload-managed columns by field name. */\nfunction systemFieldValue(record: AssetRecord, name: string): unknown {\n switch (name) {\n case 'filename':\n return record.filename\n case 'url':\n return record.url\n case 'contentType':\n return record.contentType\n case 'byteSize':\n return record.byteSize\n case 'width':\n return record.width\n case 'height':\n return record.height\n case 'storageKey':\n return record.storageKey\n case 'title':\n return record.title\n default:\n return undefined\n }\n}\n\n/**\n * Project a stored asset record into the lens's `values` shape (EC-319) — the\n * value-keyed projection the redirects/users rails use (EC-577, RESIDUE-3d): EVERY\n * key of the record's open `meta` bag (EC-301) rides in `values`, declared by the\n * schema or not, so a save hands the bag back whole (D22). Declared fields then\n * take their canonical reading on top — a column never loses to a same-named meta\n * key: a system column (`filename`, `url`, dimensions, …) reads straight off the\n * record, and `alt` reads through THE reader (`assetAlt`, EC-587 — `meta.alt`, with a\n * not-yet-migrated legacy column lifted in), exactly what delivery and the image\n * field's options dialog show.\n *\n * EC-632: the values are the metadata's view in `at` (the studio's locale) — a\n * `localized: true` field shows that locale's value, with delivery's fallback, held in\n * the lens's one locale bucket (`localized[at.locale]`, the rail's locale), so the table\n * reads it as that locale's value rather than as a fallback. EC-659: \"localized\" is\n * `at`'s lens (the project's asset collection as declared now — what delivery reads by),\n * so the rail never shows a translation delivery does not serve.\n */\nexport function assetToDocument(collection: CollectionDef, record: AssetRecord, at: AssetReadLocale | null): CmsDocument {\n const values: Record<string, unknown> = assetMetaView(record, at)\n for (const field of collection.fields) {\n if (field.name === 'alt') {\n const alt = assetAlt(record, at)\n if (alt !== null) values.alt = alt\n else delete values.alt\n } else if (SYSTEM_ASSET_FIELDS.has(field.name)) {\n values[field.name] = systemFieldValue(record, field.name)\n }\n }\n if (at === null) return { collection: collection.id, id: record.id, values }\n const bucket: Record<string, unknown> = {}\n for (const field of collection.fields) {\n if (!assetMetaIsLocalized(at, field.name) || !Object.prototype.hasOwnProperty.call(values, field.name)) continue\n bucket[field.name] = values[field.name]\n delete values[field.name]\n }\n return Object.keys(bucket).length > 0\n ? { collection: collection.id, id: record.id, values, localized: { [at.locale]: bucket } }\n : { collection: collection.id, id: record.id, values }\n}\n\n/**\n * The inverse of {@link assetToDocument}: the meta bag a lens document holds — every\n * value outside the system set (EC-301/EC-311), declared or not, through the ONE\n * value-keyed projection the users/redirects rails share (EC-577, RESIDUE-3d). A key\n * the lens never declared is in it (the schema-driven projection this replaced dropped\n * them, D22). System columns are upload-managed and never written here.\n */\nexport function documentToAssetMeta(doc: CmsDocument): Record<string, unknown> {\n // EC-632: the lens's one locale bucket (a localized field's value) reads beside `values`.\n const localized = Object.values(doc.localized ?? {}).reduce<Record<string, unknown>>((all, bag) => ({ ...all, ...bag }), {})\n return pruneMeta(metaBagProjection({ ...(doc.values ?? {}), ...localized }, SYSTEM_ASSET_FIELDS))\n}\n\n/**\n * The rail save's metadata write (EC-634 × EC-632): only the keys whose value differs\n * between the document as the editor OPENED it (`base`, forwarded by the edit shell —\n * EC-616) and the one it saves, each written where its field lives — a field `at`'s lens\n * localizes into `at`'s bucket, any other into the shared bag (`setAssetMetaField`, the\n * editors' one rule) — as the per-key, per-locale patch against the record as stored. A\n * key this editor did not change is never written. Without a base every key of the saved\n * bag is set and none is removed.\n */\nexport function assetRailMetaPatch(\n collection: CollectionDef,\n record: AssetRecord,\n at: AssetReadLocale | null,\n doc: CmsDocument,\n base?: CmsDocument,\n): Pick<AssetMetadataPatch, 'metaPatch' | 'localizedMetaPatch'> {\n const before = base ? documentToAssetMeta(base) : {}\n const after = documentToAssetMeta(doc)\n const opened = assetMetaDraft(record)\n let edited = opened\n for (const name of new Set([...Object.keys(before), ...Object.keys(after)])) {\n if (semanticValueEqual(before[name], after[name])) continue\n const field = collection.fields.find((f) => f.name === name) ?? { name }\n edited = setAssetMetaField(edited, field, at, after[name])\n }\n return assetMetaDraftPatch(opened, edited)\n}\n\n/** `create` when `capabilities.create === false` — never reached from the UI\n * (list/detail never call it); an explicit throw rather than a silent no-op. */\nfunction createUnsupported(): DocumentRef {\n throw new Error('assets are created by upload, not the assets rail')\n}\n\n/** Assets' `remove()` returns an `outcome`-discriminated {@link DeleteAssetResult}\n * (not the `{ ok }` shape users/redirects use, EC-327). `'error'` surfaces as-is;\n * `'blocked'` (the unconfirmed usage gate fired — nothing was removed) surfaces\n * as an explicit failure too (EC-433): a delete that left the asset in place\n * must NEVER look like success. It is unreachable from this rail's UI — every\n * delete path confirms through the reference dialog and dispatches with\n * `confirmInUse` — but stays honest for any direct caller. `'not-found'` maps\n * to success (idempotent delete: the desired end state — gone — holds). */\nexport function normalizeAssetRemoveResult(result: DeleteAssetResult): OperationError | null {\n if (result.outcome === 'error') return { code: 'unknown', message: result.message }\n if (result.outcome === 'blocked') {\n return {\n code: 'unknown',\n message: `Asset is referenced in ${result.usageCount} ${result.usageCount === 1 ? 'place' : 'places'} and was not deleted.`,\n }\n }\n return null\n}\n\n/**\n * How an asset row is addressed in the EC-155 reference index (EC-433):\n * `{ type: 'asset' }`, never the document shape — so the delete confirmation's\n * inbound pre-flight counts the SAME references `loadInboundAssetReferenceViews`\n * (the detail surface's check) does.\n */\nexport function assetReferenceTarget(ref: DocumentRef): ReferenceTarget {\n return { type: 'asset', id: ref.id }\n}\n\n/**\n * Assemble a {@link CollectionViewSource} for the **assets rail** (EC-319) — the\n * portable list surface over the existing asset library store instead of a\n * bespoke listing. `schema` is the DEPLOYMENT-resolved `kind:'asset'` collection\n * (EC-311: a project's own declared fields, or a mount's `nav.assets({ fields })`\n * override merged over it — see `resolveAssetsSchema` in `app-views/default-modules.tsx`),\n * never a hardcoded engine default — so a project's `copyright`/`credit`/… fields\n * (and any EC-311 override) drive the list's columns/filters exactly as they\n * drive the image field's options dialog.\n *\n * Must be mounted under an `<AssetsProvider>` (for {@link useAssets}) — already\n * ambient at the project level (EC-195), so callers rarely mount their own.\n *\n * The shared scaffold (`useRailViewSource`, EC-327) owns `viewState`/`lastError`\n * plumbing and the assembled source; this hook supplies only the asset-store\n * reads/writes, the capability profile, and the projections above.\n */\nexport function useAssetsCollectionViewSource(\n projectId: string,\n schema: CollectionDef,\n): CollectionViewSource {\n const store = useAssets()\n const registry = useMemo(() => createCollectionRegistry([schema]), [schema])\n // EC-632: the rail reads (and a save writes) a localized field in the studio's locale.\n const at = useAssetReadLocale()\n\n const documents = useCallback(\n () => store.assets.map((record) => assetToDocument(schema, record, at)),\n [store.assets, schema, at],\n )\n const get = useCallback(\n (ref: DocumentRef): CmsDocument | undefined => {\n const record = store.assets.find((a) => a.id === ref.id)\n return record ? assetToDocument(schema, record, at) : undefined\n },\n [store.assets, schema, at],\n )\n const titleOf = useCallback(\n (ref: DocumentRef): string => {\n const record = store.assets.find((a) => a.id === ref.id)\n return record?.title ?? record?.filename ?? ref.id\n },\n [store.assets],\n )\n const localeConfig = useMemo(() => railLocaleConfig(at), [at])\n const validate = useCallback(\n (doc: CmsDocument) => validateDocument(registry, doc, { localeConfig }),\n [registry, localeConfig],\n )\n const save = useCallback(\n async (doc: CmsDocument, base?: CmsDocument) => {\n const record = store.assets.find((a) => a.id === doc.id)\n if (!record) return { ok: false as const, message: `No asset “${doc.id}” is loaded.` }\n return store.updateMetadata(doc.id, assetRailMetaPatch(schema, record, at, doc, base))\n },\n [store, schema, at],\n )\n // The unconfirmed delete (the store's usage gate still applies — a referenced\n // asset comes back `blocked`, surfaced via `lastError`). The UI's delete paths\n // never land here: row/bulk deletes confirm through the reference dialog and\n // dispatch `bulkRemove` below.\n const remove = useCallback((ref: DocumentRef) => store.remove(ref.id), [store])\n // The CONFIRMED delete the bulk/row path dispatches (EC-433): the reference\n // dialog has already enumerated inbound usages (asset-shaped, see\n // `assetReferenceTarget`), so this passes `confirmInUse` exactly like the\n // detail surface's confirmed delete — same blob + thumb + cache handling.\n const bulkRemove = useCallback(\n (ref: DocumentRef) => store.remove(ref.id, { confirmInUse: true }),\n [store],\n )\n\n return useRailViewSource({\n projectId,\n schema,\n capabilities: ASSET_COLLECTION_CAPABILITIES,\n localeConfig,\n documents,\n get,\n titleOf,\n // No `referenceCandidates`/`collections`/`isFieldReadOnly` override: assets carry\n // no relation/asset fields of their own (v1) — nothing cross-collection to\n // resolve — and read-only follows the factory's default `form.readOnly` check.\n detailPath: (id: string) => studioPath.asset(id),\n listPath: studioPath.assets(),\n // EC-433: the delete confirmation must enumerate ASSET-shaped inbound\n // references — the document-shaped default would report \"not referenced\"\n // for a referenced asset.\n referenceTarget: assetReferenceTarget,\n validate,\n create: createUnsupported,\n save,\n remove,\n bulkRemove,\n normalizeSaveResult: normalizeOkResult,\n normalizeRemoveResult: normalizeAssetRemoveResult,\n })\n}\n"]}
|
|
1
|
+
{"version":3,"file":"assets-collection-view-source.js","sourceRoot":"","sources":["../../../src/components/assets/assets-collection-view-source.ts"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useMemo } from 'react'\nimport type { CmsDocument, CollectionDef, DocumentRef, Locale, LocaleConfig } from '@elytracms/core/cms-core'\nimport { createCollectionRegistry, semanticValueEqual, validateDocument } from '@elytracms/core/cms-core'\nimport { assetAlt, assetMetaIsLocalized, assetMetaView } from '@elytracms/core/persistence'\nimport type { AssetReadLocale, AssetRecord, ReferenceTarget } from '@elytracms/core/persistence'\nimport type { OperationError } from '@elytracms/core/operations'\nimport { studioPath } from '@elytracms/studio/router'\nimport type { DeleteAssetResult } from '../../lib/assets'\nimport type { CollectionViewSource, CollectionViewCapabilities } from '../../lib/collection-view/collection-view-source'\nimport { useAssets } from './assets-store'\nimport { SYSTEM_ASSET_FIELDS, assetMetaDraft, assetMetaDraftPatch, pruneMeta, setAssetMetaField } from './asset-metadata'\nimport type { AssetMetadataPatch } from './asset-metadata'\nimport { useAssetReadLocale } from './asset-locale'\nimport { metaBagProjection, normalizeOkResult, useRailViewSource } from '../rail-view-source'\nimport { parseLocaleConfig } from '@elytracms/core/cms-core'\n\n/**\n * The assets rail has no locale switcher of its own (capability `locales: false`): it\n * reads in ONE locale — the studio's (EC-632), a localized field's value in that locale —\n * so its lens is a one-locale document in that locale. Before a project's locales are\n * installed, the engine's neutral `en` (shared metadata only: nothing is localized then).\n */\nconst ASSETS_LOCALE: Locale = 'en'\nfunction railLocaleConfig(at: AssetReadLocale | null): LocaleConfig {\n const locale = at?.locale ?? ASSETS_LOCALE\n return parseLocaleConfig({ default: locale, locales: [locale] })\n}\n\n/**\n * The capability profile of the assets rail (EC-319, EC-433). Hoisted to a\n * module-level constant (no prop/state dependency) so it's both the value the\n * hook returns AND independently unit-testable.\n *\n * `create` is off — a new asset comes from an upload, never a blank draft.\n * `remove` + `bulk` are ON since EC-433: the list's row/bulk delete used to be\n * off deliberately because the generic `ReferenceConfirmDialog` enumerated\n * inbound references as `{ type: 'document' }` targets — the wrong index shape\n * for an asset — which would have shown \"not referenced\" for an asset that IS\n * referenced. That pre-flight now queries the shape THIS source declares via\n * {@link assetReferenceTarget} (`{ type: 'asset' }`, the same lookup the\n * detail surface runs), and the confirmed delete dispatches the SAME checked\n * store delete the detail surface uses (`deleteAsset`: original + EC-400 thumb\n * blobs, then the record — EC-420 lqips ride on the record). Nothing else\n * applies to assets: no publish/versions/locales/inline-edit/live-URL/\n * hierarchy/import-export.\n */\nexport const ASSET_COLLECTION_CAPABILITIES: CollectionViewCapabilities = {\n publish: false,\n versions: false,\n locales: false,\n inlineEdit: false,\n create: false,\n duplicate: false, // EC-442: blob duplication is a different feature, out of scope\n remove: true,\n bulk: true,\n liveUrl: false,\n hierarchy: false,\n importExport: false,\n adminOnly: false,\n}\n\n/** Read one of the record's fixed, upload-managed columns by field name. */\nfunction systemFieldValue(record: AssetRecord, name: string): unknown {\n switch (name) {\n case 'filename':\n return record.filename\n case 'url':\n return record.url\n case 'contentType':\n return record.contentType\n case 'byteSize':\n return record.byteSize\n case 'width':\n return record.width\n case 'height':\n return record.height\n case 'storageKey':\n return record.storageKey\n case 'title':\n return record.title\n default:\n return undefined\n }\n}\n\n/**\n * Project a stored asset record into the lens's `values` shape (EC-319) — the\n * value-keyed projection the redirects/users rails use (EC-577, RESIDUE-3d): EVERY\n * key of the record's open `meta` bag (EC-301) rides in `values`, declared by the\n * schema or not, so a save hands the bag back whole (D22). Declared fields then\n * take their canonical reading on top — a column never loses to a same-named meta\n * key: a system column (`filename`, `url`, dimensions, …) reads straight off the\n * record, and `alt` reads through THE reader (`assetAlt`, EC-587 — `meta.alt`, its one\n * home), exactly what delivery and the image field's options dialog show.\n *\n * EC-632: the values are the metadata's view in `at` (the studio's locale) — a\n * `localized: true` field shows that locale's value, with delivery's fallback, held in\n * the lens's one locale bucket (`localized[at.locale]`, the rail's locale), so the table\n * reads it as that locale's value rather than as a fallback. EC-659: \"localized\" is\n * `at`'s lens (the project's asset collection as declared now — what delivery reads by),\n * so the rail never shows a translation delivery does not serve.\n */\nexport function assetToDocument(collection: CollectionDef, record: AssetRecord, at: AssetReadLocale | null): CmsDocument {\n const values: Record<string, unknown> = assetMetaView(record, at)\n for (const field of collection.fields) {\n if (field.name === 'alt') {\n const alt = assetAlt(record, at)\n if (alt !== null) values.alt = alt\n else delete values.alt\n } else if (SYSTEM_ASSET_FIELDS.has(field.name)) {\n values[field.name] = systemFieldValue(record, field.name)\n }\n }\n if (at === null) return { collection: collection.id, id: record.id, values }\n const bucket: Record<string, unknown> = {}\n for (const field of collection.fields) {\n if (!assetMetaIsLocalized(at, field.name) || !Object.prototype.hasOwnProperty.call(values, field.name)) continue\n bucket[field.name] = values[field.name]\n delete values[field.name]\n }\n return Object.keys(bucket).length > 0\n ? { collection: collection.id, id: record.id, values, localized: { [at.locale]: bucket } }\n : { collection: collection.id, id: record.id, values }\n}\n\n/**\n * The inverse of {@link assetToDocument}: the meta bag a lens document holds — every\n * value outside the system set (EC-301/EC-311), declared or not, through the ONE\n * value-keyed projection the users/redirects rails share (EC-577, RESIDUE-3d). A key\n * the lens never declared is in it (the schema-driven projection this replaced dropped\n * them, D22). System columns are upload-managed and never written here.\n */\nexport function documentToAssetMeta(doc: CmsDocument): Record<string, unknown> {\n // EC-632: the lens's one locale bucket (a localized field's value) reads beside `values`.\n const localized = Object.values(doc.localized ?? {}).reduce<Record<string, unknown>>((all, bag) => ({ ...all, ...bag }), {})\n return pruneMeta(metaBagProjection({ ...(doc.values ?? {}), ...localized }, SYSTEM_ASSET_FIELDS))\n}\n\n/**\n * The rail save's metadata write (EC-634 × EC-632): only the keys whose value differs\n * between the document as the editor OPENED it (`base`, forwarded by the edit shell —\n * EC-616) and the one it saves, each written where its field lives — a field `at`'s lens\n * localizes into `at`'s bucket, any other into the shared bag (`setAssetMetaField`, the\n * editors' one rule) — as the per-key, per-locale patch against the record as stored. A\n * key this editor did not change is never written. Without a base every key of the saved\n * bag is set and none is removed.\n */\nexport function assetRailMetaPatch(\n collection: CollectionDef,\n record: AssetRecord,\n at: AssetReadLocale | null,\n doc: CmsDocument,\n base?: CmsDocument,\n): Pick<AssetMetadataPatch, 'metaPatch' | 'localizedMetaPatch'> {\n const before = base ? documentToAssetMeta(base) : {}\n const after = documentToAssetMeta(doc)\n const opened = assetMetaDraft(record)\n let edited = opened\n for (const name of new Set([...Object.keys(before), ...Object.keys(after)])) {\n if (semanticValueEqual(before[name], after[name])) continue\n const field = collection.fields.find((f) => f.name === name) ?? { name }\n edited = setAssetMetaField(edited, field, at, after[name])\n }\n return assetMetaDraftPatch(opened, edited)\n}\n\n/** `create` when `capabilities.create === false` — never reached from the UI\n * (list/detail never call it); an explicit throw rather than a silent no-op. */\nfunction createUnsupported(): DocumentRef {\n throw new Error('assets are created by upload, not the assets rail')\n}\n\n/** Assets' `remove()` returns an `outcome`-discriminated {@link DeleteAssetResult}\n * (not the `{ ok }` shape users/redirects use, EC-327). `'error'` surfaces as-is;\n * `'blocked'` (the unconfirmed usage gate fired — nothing was removed) surfaces\n * as an explicit failure too (EC-433): a delete that left the asset in place\n * must NEVER look like success. It is unreachable from this rail's UI — every\n * delete path confirms through the reference dialog and dispatches with\n * `confirmInUse` — but stays honest for any direct caller. `'not-found'` maps\n * to success (idempotent delete: the desired end state — gone — holds). */\nexport function normalizeAssetRemoveResult(result: DeleteAssetResult): OperationError | null {\n if (result.outcome === 'error') return { code: 'unknown', message: result.message }\n if (result.outcome === 'blocked') {\n return {\n code: 'unknown',\n message: `Asset is referenced in ${result.usageCount} ${result.usageCount === 1 ? 'place' : 'places'} and was not deleted.`,\n }\n }\n return null\n}\n\n/**\n * How an asset row is addressed in the EC-155 reference index (EC-433):\n * `{ type: 'asset' }`, never the document shape — so the delete confirmation's\n * inbound pre-flight counts the SAME references `loadInboundAssetReferenceViews`\n * (the detail surface's check) does.\n */\nexport function assetReferenceTarget(ref: DocumentRef): ReferenceTarget {\n return { type: 'asset', id: ref.id }\n}\n\n/**\n * Assemble a {@link CollectionViewSource} for the **assets rail** (EC-319) — the\n * portable list surface over the existing asset library store instead of a\n * bespoke listing. `schema` is the DEPLOYMENT-resolved `kind:'asset'` collection\n * (EC-311: a project's own declared fields, or a mount's `nav.assets({ fields })`\n * override merged over it — see `resolveAssetsSchema` in `app-views/default-modules.tsx`),\n * never a hardcoded engine default — so a project's `copyright`/`credit`/… fields\n * (and any EC-311 override) drive the list's columns/filters exactly as they\n * drive the image field's options dialog.\n *\n * Must be mounted under an `<AssetsProvider>` (for {@link useAssets}) — already\n * ambient at the project level (EC-195), so callers rarely mount their own.\n *\n * The shared scaffold (`useRailViewSource`, EC-327) owns `viewState`/`lastError`\n * plumbing and the assembled source; this hook supplies only the asset-store\n * reads/writes, the capability profile, and the projections above.\n */\nexport function useAssetsCollectionViewSource(\n projectId: string,\n schema: CollectionDef,\n): CollectionViewSource {\n const store = useAssets()\n const registry = useMemo(() => createCollectionRegistry([schema]), [schema])\n // EC-632: the rail reads (and a save writes) a localized field in the studio's locale.\n const at = useAssetReadLocale()\n\n const documents = useCallback(\n () => store.assets.map((record) => assetToDocument(schema, record, at)),\n [store.assets, schema, at],\n )\n const get = useCallback(\n (ref: DocumentRef): CmsDocument | undefined => {\n const record = store.assets.find((a) => a.id === ref.id)\n return record ? assetToDocument(schema, record, at) : undefined\n },\n [store.assets, schema, at],\n )\n const titleOf = useCallback(\n (ref: DocumentRef): string => {\n const record = store.assets.find((a) => a.id === ref.id)\n return record?.title ?? record?.filename ?? ref.id\n },\n [store.assets],\n )\n const localeConfig = useMemo(() => railLocaleConfig(at), [at])\n const validate = useCallback(\n (doc: CmsDocument) => validateDocument(registry, doc, { localeConfig }),\n [registry, localeConfig],\n )\n const save = useCallback(\n async (doc: CmsDocument, base?: CmsDocument) => {\n const record = store.assets.find((a) => a.id === doc.id)\n if (!record) return { ok: false as const, message: `No asset “${doc.id}” is loaded.` }\n return store.updateMetadata(doc.id, assetRailMetaPatch(schema, record, at, doc, base))\n },\n [store, schema, at],\n )\n // The unconfirmed delete (the store's usage gate still applies — a referenced\n // asset comes back `blocked`, surfaced via `lastError`). The UI's delete paths\n // never land here: row/bulk deletes confirm through the reference dialog and\n // dispatch `bulkRemove` below.\n const remove = useCallback((ref: DocumentRef) => store.remove(ref.id), [store])\n // The CONFIRMED delete the bulk/row path dispatches (EC-433): the reference\n // dialog has already enumerated inbound usages (asset-shaped, see\n // `assetReferenceTarget`), so this passes `confirmInUse` exactly like the\n // detail surface's confirmed delete — same blob + thumb + cache handling.\n const bulkRemove = useCallback(\n (ref: DocumentRef) => store.remove(ref.id, { confirmInUse: true }),\n [store],\n )\n\n return useRailViewSource({\n projectId,\n schema,\n capabilities: ASSET_COLLECTION_CAPABILITIES,\n localeConfig,\n documents,\n get,\n titleOf,\n // No `referenceCandidates`/`collections`/`isFieldReadOnly` override: assets carry\n // no relation/asset fields of their own (v1) — nothing cross-collection to\n // resolve — and read-only follows the factory's default `form.readOnly` check.\n detailPath: (id: string) => studioPath.asset(id),\n listPath: studioPath.assets(),\n // EC-433: the delete confirmation must enumerate ASSET-shaped inbound\n // references — the document-shaped default would report \"not referenced\"\n // for a referenced asset.\n referenceTarget: assetReferenceTarget,\n validate,\n create: createUnsupported,\n save,\n remove,\n bulkRemove,\n normalizeSaveResult: normalizeOkResult,\n normalizeRemoveResult: normalizeAssetRemoveResult,\n })\n}\n"]}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { PersistenceAdapter } from '@elytracms/core/persistence';
|
|
2
|
+
import type { PromoteUndoOutcome } from '../reuse.js';
|
|
3
|
+
/**
|
|
4
|
+
* EC-739 (Codex round 8 F41) — a TEST fixture: *Make reusable*'s Undo racing another editor's
|
|
5
|
+
* adoption of the reusable it created, over ONE store, through the real persister, the real
|
|
6
|
+
* operations and the real `undoPromote` (vitest-free, so the adapter wrappers share it).
|
|
7
|
+
*
|
|
8
|
+
* A creates reusable `r`; Undo flushes, then runs the conditional delete; B saves a page with a
|
|
9
|
+
* legal relation (weak or strict) to `r` at `arrival`:
|
|
10
|
+
* - `none` — nobody adopts it (the control: Undo deletes);
|
|
11
|
+
* - `before-undo` — B's relation is stored before Undo starts (Codex's before-count arrival);
|
|
12
|
+
* - `before-delete` — window a: B saves after Undo began and before `documents.delete` runs
|
|
13
|
+
* (Codex's after-count arrival — the count it raced is gone, so the hook wraps `deleteCreated`);
|
|
14
|
+
* - `in-store-delete` — window b: B saves inside the store's `deleteDocument`, after the
|
|
15
|
+
* operation's guard read and before the store's step (the hook wraps the adapter's call).
|
|
16
|
+
*/
|
|
17
|
+
export type ReuseRaceArrival = 'none' | 'before-undo' | 'before-delete' | 'in-store-delete';
|
|
18
|
+
export interface ReuseRaceObservation {
|
|
19
|
+
/** The issues `resolveProject` reports for the fixture's config (the config control: `[]`). */
|
|
20
|
+
readonly configIssues: readonly unknown[];
|
|
21
|
+
readonly outcome: PromoteUndoOutcome;
|
|
22
|
+
/** `r` still exists after Undo. */
|
|
23
|
+
readonly kept: boolean;
|
|
24
|
+
/** B's stored relation (`undefined` when B never saved). */
|
|
25
|
+
readonly adoption: unknown;
|
|
26
|
+
}
|
|
27
|
+
export declare function runReuseRace(adapter: PersistenceAdapter, options: {
|
|
28
|
+
readonly strict: boolean;
|
|
29
|
+
readonly arrival: ReuseRaceArrival;
|
|
30
|
+
}): Promise<ReuseRaceObservation>;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { defineComponent } from '@elytracms/core/component-registry';
|
|
2
|
+
import { resolveProject } from '@elytracms/core/studio-core';
|
|
3
|
+
import { createOperations } from '../../../lib/operations/operations-package.js';
|
|
4
|
+
import { createCmsPersister } from '../../../lib/operations/cms-persistence.js';
|
|
5
|
+
import { undoPromote } from '../reuse.js';
|
|
6
|
+
const project = 'site';
|
|
7
|
+
export async function runReuseRace(adapter, options) {
|
|
8
|
+
const collections = [
|
|
9
|
+
{ id: 'reusable', kind: 'document', fields: [{ name: 'title', type: 'text' }, { name: 'content', type: 'blocks', form: { tab: 'canvas' } }] },
|
|
10
|
+
{ id: 'page', kind: 'document', fields: [{ name: 'pick', type: 'relation', target: 'reusable', multiple: false, strict: options.strict }] },
|
|
11
|
+
];
|
|
12
|
+
const configIssues = resolveProject({
|
|
13
|
+
name: 'Site',
|
|
14
|
+
slug: 'site',
|
|
15
|
+
locales: { default: 'en', locales: ['en'] },
|
|
16
|
+
collections,
|
|
17
|
+
components: [{ manifest: defineComponent({ id: 'Text', props: {}, slots: [] }), implementation: () => null }],
|
|
18
|
+
}).issues;
|
|
19
|
+
// Window b's hook sits between the operation (its guard has read) and the store's step: the
|
|
20
|
+
// operations see a view of the adapter whose `deleteDocument` saves B's page first, then
|
|
21
|
+
// forwards to the real one. (A view, not a patch: a write-path-indexed store's `cms` is a
|
|
22
|
+
// forwarding proxy, where an assigned method lands on the inner repository it forwards to.)
|
|
23
|
+
let adopt = async () => { };
|
|
24
|
+
const cms = Object.create(adapter.cms, {
|
|
25
|
+
deleteDocument: {
|
|
26
|
+
value: async (...args) => {
|
|
27
|
+
if (options.arrival === 'in-store-delete')
|
|
28
|
+
await adopt();
|
|
29
|
+
return adapter.cms.deleteDocument(...args);
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
const view = Object.create(adapter, { cms: { value: cms } });
|
|
34
|
+
const ops = createOperations({ adapter: view, collections: () => collections, validateDocument: () => [] });
|
|
35
|
+
const persister = createCmsPersister({
|
|
36
|
+
projectId: project,
|
|
37
|
+
getOperations: async () => ops,
|
|
38
|
+
getCollections: () => collections,
|
|
39
|
+
onError: (error) => {
|
|
40
|
+
throw new Error(error.message);
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
const document = {
|
|
44
|
+
collection: 'reusable',
|
|
45
|
+
id: 'r',
|
|
46
|
+
values: { title: 'Promoted', content: { version: 2, content: [{ type: 'elytra/component', id: 'promoted', componentId: 'Text', props: {} }] } },
|
|
47
|
+
};
|
|
48
|
+
persister.create(document);
|
|
49
|
+
await persister.flush();
|
|
50
|
+
const referrer = { collection: 'page', id: 'other', values: { pick: { type: 'reference', collection: 'reusable', id: 'r' } } };
|
|
51
|
+
// B: another editor, its own operations over the same store.
|
|
52
|
+
const other = createOperations({ adapter, collections: () => collections, validateDocument: () => [] });
|
|
53
|
+
adopt = async () => {
|
|
54
|
+
const saved = await other.documents.replace({ projectId: project, document: referrer, precondition: { ifAbsent: true } });
|
|
55
|
+
if (!saved.ok)
|
|
56
|
+
throw new Error(`B's save failed: ${saved.error.message}`);
|
|
57
|
+
};
|
|
58
|
+
if (options.arrival === 'before-undo')
|
|
59
|
+
await adopt();
|
|
60
|
+
const outcome = await undoPromote({ kind: 'reusable-created', ref: document, name: 'Promoted', document }, {
|
|
61
|
+
flush: () => persister.flush(),
|
|
62
|
+
// Window a: B's save completes after Undo began and before the conditional delete runs.
|
|
63
|
+
deleteCreated: async (ref) => {
|
|
64
|
+
if (options.arrival === 'before-delete')
|
|
65
|
+
await adopt();
|
|
66
|
+
return persister.deleteCreated(ref);
|
|
67
|
+
},
|
|
68
|
+
read: async (ref) => {
|
|
69
|
+
const record = await adapter.cms.findDocument(project, ref);
|
|
70
|
+
return record ? { published: record.publishedVersion !== undefined } : undefined;
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
const kept = (await adapter.cms.findDocument(project, document)) !== undefined;
|
|
74
|
+
const stored = options.arrival === 'none' ? undefined : await adapter.cms.findDocument(project, referrer);
|
|
75
|
+
return { configIssues, outcome, kept, adoption: stored?.document.values.pick };
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=reuse-race.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reuse-race.js","sourceRoot":"","sources":["../../../../src/components/builder/__fixtures__/reuse-race.ts"],"names":[],"mappings":"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","sourcesContent":["import { defineComponent } from '@elytracms/core/component-registry'\nimport type { CmsDocument, CollectionDef } from '@elytracms/core/cms-core'\nimport type { CmsRepository, PersistenceAdapter } from '@elytracms/core/persistence'\nimport { resolveProject } from '@elytracms/core/studio-core'\nimport { createOperations } from '../../../lib/operations/operations-package'\nimport { createCmsPersister } from '../../../lib/operations/cms-persistence'\nimport { undoPromote } from '../reuse'\nimport type { PromoteUndoOutcome } from '../reuse'\n\n/**\n * EC-739 (Codex round 8 F41) — a TEST fixture: *Make reusable*'s Undo racing another editor's\n * adoption of the reusable it created, over ONE store, through the real persister, the real\n * operations and the real `undoPromote` (vitest-free, so the adapter wrappers share it).\n *\n * A creates reusable `r`; Undo flushes, then runs the conditional delete; B saves a page with a\n * legal relation (weak or strict) to `r` at `arrival`:\n * - `none` — nobody adopts it (the control: Undo deletes);\n * - `before-undo` — B's relation is stored before Undo starts (Codex's before-count arrival);\n * - `before-delete` — window a: B saves after Undo began and before `documents.delete` runs\n * (Codex's after-count arrival — the count it raced is gone, so the hook wraps `deleteCreated`);\n * - `in-store-delete` — window b: B saves inside the store's `deleteDocument`, after the\n * operation's guard read and before the store's step (the hook wraps the adapter's call).\n */\n\nexport type ReuseRaceArrival = 'none' | 'before-undo' | 'before-delete' | 'in-store-delete'\n\nexport interface ReuseRaceObservation {\n /** The issues `resolveProject` reports for the fixture's config (the config control: `[]`). */\n readonly configIssues: readonly unknown[]\n readonly outcome: PromoteUndoOutcome\n /** `r` still exists after Undo. */\n readonly kept: boolean\n /** B's stored relation (`undefined` when B never saved). */\n readonly adoption: unknown\n}\n\nconst project = 'site'\n\nexport async function runReuseRace(\n adapter: PersistenceAdapter,\n options: { readonly strict: boolean; readonly arrival: ReuseRaceArrival },\n): Promise<ReuseRaceObservation> {\n const collections: CollectionDef[] = [\n { id: 'reusable', kind: 'document', fields: [{ name: 'title', type: 'text' }, { name: 'content', type: 'blocks', form: { tab: 'canvas' } }] },\n { id: 'page', kind: 'document', fields: [{ name: 'pick', type: 'relation', target: 'reusable', multiple: false, strict: options.strict }] },\n ]\n const configIssues = resolveProject({\n name: 'Site',\n slug: 'site',\n locales: { default: 'en', locales: ['en'] },\n collections,\n components: [{ manifest: defineComponent({ id: 'Text', props: {}, slots: [] }), implementation: () => null }],\n }).issues\n // Window b's hook sits between the operation (its guard has read) and the store's step: the\n // operations see a view of the adapter whose `deleteDocument` saves B's page first, then\n // forwards to the real one. (A view, not a patch: a write-path-indexed store's `cms` is a\n // forwarding proxy, where an assigned method lands on the inner repository it forwards to.)\n let adopt = async (): Promise<void> => {}\n const cms: CmsRepository = Object.create(adapter.cms, {\n deleteDocument: {\n value: async (...args: Parameters<CmsRepository['deleteDocument']>) => {\n if (options.arrival === 'in-store-delete') await adopt()\n return adapter.cms.deleteDocument(...args)\n },\n },\n })\n const view: PersistenceAdapter = Object.create(adapter, { cms: { value: cms } })\n const ops = createOperations({ adapter: view, collections: () => collections, validateDocument: () => [] })\n const persister = createCmsPersister({\n projectId: project,\n getOperations: async () => ops,\n getCollections: () => collections,\n onError: (error) => {\n throw new Error(error.message)\n },\n })\n const document: CmsDocument = {\n collection: 'reusable',\n id: 'r',\n values: { title: 'Promoted', content: { version: 2, content: [{ type: 'elytra/component', id: 'promoted', componentId: 'Text', props: {} }] } },\n }\n persister.create(document)\n await persister.flush()\n\n const referrer: CmsDocument = { collection: 'page', id: 'other', values: { pick: { type: 'reference', collection: 'reusable', id: 'r' } } }\n // B: another editor, its own operations over the same store.\n const other = createOperations({ adapter, collections: () => collections, validateDocument: () => [] })\n adopt = async () => {\n const saved = await other.documents.replace({ projectId: project, document: referrer, precondition: { ifAbsent: true } })\n if (!saved.ok) throw new Error(`B's save failed: ${saved.error.message}`)\n }\n if (options.arrival === 'before-undo') await adopt()\n\n const outcome = await undoPromote(\n { kind: 'reusable-created', ref: document, name: 'Promoted', document },\n {\n flush: () => persister.flush(),\n // Window a: B's save completes after Undo began and before the conditional delete runs.\n deleteCreated: async (ref) => {\n if (options.arrival === 'before-delete') await adopt()\n return persister.deleteCreated(ref)\n },\n read: async (ref) => {\n const record = await adapter.cms.findDocument(project, ref)\n return record ? { published: record.publishedVersion !== undefined } : undefined\n },\n },\n )\n const kept = (await adapter.cms.findDocument(project, document)) !== undefined\n const stored = options.arrival === 'none' ? undefined : await adapter.cms.findDocument(project, referrer)\n return { configIssues, outcome, kept, adoption: stored?.document.values.pick }\n}\n"]}
|
|
@@ -36,9 +36,9 @@ import { fieldLabel } from '../cms/cms-labels.js';
|
|
|
36
36
|
* `{ version: 2, content: BlockNode[] }`. The canvas edits the sequence's
|
|
37
37
|
* `elytra/component` nodes; typography nodes survive the session byte-for-byte
|
|
38
38
|
* and keep their order relative to components on write-back (see
|
|
39
|
-
* {@link splitCompositionValue} / {@link mergeCompositionValue}).
|
|
40
|
-
* node/array
|
|
41
|
-
*
|
|
39
|
+
* {@link splitCompositionValue} / {@link mergeCompositionValue}). Every write
|
|
40
|
+
* emits v2; a legacy bare node/array has no roots (EC-708 deleted the staging
|
|
41
|
+
* read — validation names the value). This reuses the real canvas + inspector +
|
|
42
42
|
* blocks panel unchanged — the same EC-158–164 mechanics — without touching the
|
|
43
43
|
* project-wide builder store. Surfaced on a dedicated "Canvas" fieldset tab in
|
|
44
44
|
* the CMS detail editor so the page builder gets full screen real estate rather
|