@elytracms/studio 0.0.94 → 0.0.95

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.
Files changed (126) hide show
  1. package/convex/asset-alt-migration.test.ts +2 -2
  2. package/convex/asset-meta-patch.test.ts +218 -0
  3. package/convex/asset-replace-delete-race.test.ts +127 -0
  4. package/convex/assets.ts +27 -2
  5. package/convex/delivery-redirects.test.ts +167 -0
  6. package/convex/delivery.ts +15 -5
  7. package/convex/rail-write-parity.test.ts +6 -6
  8. package/convex/redirects.ts +1 -1
  9. package/convex/schema.ts +6 -0
  10. package/dist/addon.d.ts +1 -1
  11. package/dist/addon.js.map +1 -1
  12. package/dist/components/assets/asset-detail.js +26 -11
  13. package/dist/components/assets/asset-detail.js.map +1 -1
  14. package/dist/components/assets/asset-locale.d.ts +9 -0
  15. package/dist/components/assets/asset-locale.js +16 -0
  16. package/dist/components/assets/asset-locale.js.map +1 -0
  17. package/dist/components/assets/asset-meta-label.d.ts +12 -0
  18. package/dist/components/assets/asset-meta-label.js +15 -0
  19. package/dist/components/assets/asset-meta-label.js.map +1 -0
  20. package/dist/components/assets/asset-metadata.d.ts +69 -15
  21. package/dist/components/assets/asset-metadata.js +99 -11
  22. package/dist/components/assets/asset-metadata.js.map +1 -1
  23. package/dist/components/assets/asset-search.js +8 -2
  24. package/dist/components/assets/asset-search.js.map +1 -1
  25. package/dist/components/assets/asset-thumb.js +6 -2
  26. package/dist/components/assets/asset-thumb.js.map +1 -1
  27. package/dist/components/assets/assets-collection-view-source.d.ts +23 -9
  28. package/dist/components/assets/assets-collection-view-source.js +78 -26
  29. package/dist/components/assets/assets-collection-view-source.js.map +1 -1
  30. package/dist/components/assets/assets-store.js +17 -4
  31. package/dist/components/assets/assets-store.js.map +1 -1
  32. package/dist/components/builder/builder-store.d.ts +21 -0
  33. package/dist/components/builder/builder-store.js +22 -0
  34. package/dist/components/builder/builder-store.js.map +1 -1
  35. package/dist/components/builder/canvas-commit-boundary.d.ts +22 -0
  36. package/dist/components/builder/canvas-commit-boundary.js +27 -0
  37. package/dist/components/builder/canvas-commit-boundary.js.map +1 -0
  38. package/dist/components/builder/canvas-rebase.d.ts +77 -0
  39. package/dist/components/builder/canvas-rebase.js +169 -0
  40. package/dist/components/builder/canvas-rebase.js.map +1 -0
  41. package/dist/components/builder/canvas-render-context.d.ts +6 -3
  42. package/dist/components/builder/canvas-render-context.js +17 -9
  43. package/dist/components/builder/canvas-render-context.js.map +1 -1
  44. package/dist/components/builder/composition-field-editor.d.ts +20 -2
  45. package/dist/components/builder/composition-field-editor.js +45 -27
  46. package/dist/components/builder/composition-field-editor.js.map +1 -1
  47. package/dist/components/builder/node-factory.js +15 -3
  48. package/dist/components/builder/node-factory.js.map +1 -1
  49. package/dist/components/builder/property-inspector.js +3 -1
  50. package/dist/components/builder/property-inspector.js.map +1 -1
  51. package/dist/components/builder/run-inspector.js +10 -6
  52. package/dist/components/builder/run-inspector.js.map +1 -1
  53. package/dist/components/cms/asset-field.d.ts +5 -2
  54. package/dist/components/cms/asset-field.js +34 -17
  55. package/dist/components/cms/asset-field.js.map +1 -1
  56. package/dist/components/cms/asset-picker.js +3 -1
  57. package/dist/components/cms/asset-picker.js.map +1 -1
  58. package/dist/components/cms/canvas-commit.d.ts +56 -0
  59. package/dist/components/cms/canvas-commit.js +35 -0
  60. package/dist/components/cms/canvas-commit.js.map +1 -0
  61. package/dist/components/cms/collection-table.js +23 -7
  62. package/dist/components/cms/collection-table.js.map +1 -1
  63. package/dist/components/cms/commit-policy.d.ts +6 -2
  64. package/dist/components/cms/commit-policy.js +1 -1
  65. package/dist/components/cms/commit-policy.js.map +1 -1
  66. package/dist/components/cms/detail-editor.js +11 -1
  67. package/dist/components/cms/detail-editor.js.map +1 -1
  68. package/dist/components/cms/use-document-draft.d.ts +62 -4
  69. package/dist/components/cms/use-document-draft.js +102 -32
  70. package/dist/components/cms/use-document-draft.js.map +1 -1
  71. package/dist/components/cms/use-live-url.d.ts +6 -1
  72. package/dist/components/cms/use-live-url.js +12 -9
  73. package/dist/components/cms/use-live-url.js.map +1 -1
  74. package/dist/components/collection-view/cms-collection-view-source.js +9 -5
  75. package/dist/components/collection-view/cms-collection-view-source.js.map +1 -1
  76. package/dist/components/deployment-setup-gate.js +10 -1
  77. package/dist/components/deployment-setup-gate.js.map +1 -1
  78. package/dist/components/records/record-lens.js +3 -8
  79. package/dist/components/records/record-lens.js.map +1 -1
  80. package/dist/lib/assets/upload.d.ts +25 -7
  81. package/dist/lib/assets/upload.js +30 -21
  82. package/dist/lib/assets/upload.js.map +1 -1
  83. package/dist/lib/cms/published-documents.d.ts +24 -6
  84. package/dist/lib/cms/published-documents.js +39 -6
  85. package/dist/lib/cms/published-documents.js.map +1 -1
  86. package/dist/lib/cms-views/view-model.d.ts +14 -101
  87. package/dist/lib/cms-views/view-model.js +20 -101
  88. package/dist/lib/cms-views/view-model.js.map +1 -1
  89. package/dist/lib/collection-view/collection-view-source.d.ts +6 -1
  90. package/dist/lib/collection-view/collection-view-source.js.map +1 -1
  91. package/dist/lib/engine-version.d.ts +1 -1
  92. package/dist/lib/engine-version.js +1 -1
  93. package/dist/lib/engine-version.js.map +1 -1
  94. package/dist/lib/i18n/messages.de.js +7 -3
  95. package/dist/lib/i18n/messages.de.js.map +1 -1
  96. package/dist/lib/i18n/messages.en.d.ts +5 -1
  97. package/dist/lib/i18n/messages.en.js +8 -4
  98. package/dist/lib/i18n/messages.en.js.map +1 -1
  99. package/dist/lib/operations/config-manifest-lookup.d.ts +1 -1
  100. package/dist/lib/operations/config-manifest-lookup.js +3 -3
  101. package/dist/lib/operations/config-manifest-lookup.js.map +1 -1
  102. package/dist/lib/persistence/deployment-readiness.js +14 -11
  103. package/dist/lib/persistence/deployment-readiness.js.map +1 -1
  104. package/dist/lib/persona/labels.js +1 -0
  105. package/dist/lib/persona/labels.js.map +1 -1
  106. package/dist/lib/query/index.d.ts +2 -2
  107. package/dist/lib/query/index.js +1 -1
  108. package/dist/lib/query/index.js.map +1 -1
  109. package/dist/lib/query/operations-query.d.ts +26 -0
  110. package/dist/lib/query/operations-query.js +12 -0
  111. package/dist/lib/query/operations-query.js.map +1 -1
  112. package/dist/lib/registry/index.js +2 -2
  113. package/dist/lib/registry/index.js.map +1 -1
  114. package/dist/lib/registry/preview.d.ts +10 -0
  115. package/dist/lib/registry/preview.js +13 -4
  116. package/dist/lib/registry/preview.js.map +1 -1
  117. package/dist/lib/routes/route-issues.js +6 -3
  118. package/dist/lib/routes/route-issues.js.map +1 -1
  119. package/dist/lib/workspace/workspace-source.js +6 -7
  120. package/dist/lib/workspace/workspace-source.js.map +1 -1
  121. package/dist/previews/entry-node.d.ts +14 -0
  122. package/dist/previews/entry-node.js +22 -0
  123. package/dist/previews/entry-node.js.map +1 -0
  124. package/dist/previews/harness.js +2 -16
  125. package/dist/previews/harness.js.map +1 -1
  126. package/package.json +3 -3
@@ -1,6 +1,6 @@
1
1
  import { useCallback, useMemo } from 'react';
2
2
  import { isHierarchical } from '@elytracms/core/cms-core';
3
- import { assetAlt } from '@elytracms/core/persistence';
3
+ import { assetAlt, assetReadLocale } from '@elytracms/core/persistence';
4
4
  import { studioPath } from '@elytracms/studio/router';
5
5
  import { resolveDeploymentFrontendUrl, resolveDeploymentRoutes } from '../../lib/workspace/index.js';
6
6
  import { useCms } from '../cms/cms-store.js';
@@ -34,10 +34,13 @@ export function useCmsCollectionViewSource(collectionId) {
34
34
  // fallback-home rule (same as the document editor's header affordance,
35
35
  // `use-live-url.ts`) — otherwise a fallback-addressed home page showed no URL
36
36
  // in this column even though the live site serves it.
37
+ // EC-628: the pool's read state carries through — a failed read is the column's error.
37
38
  const liveUrlMap = useMemo(() => {
38
39
  if (!collection || !isRoutable)
39
40
  return null;
40
- const siblings = [...publishedDocs.byId.values()];
41
+ if (publishedDocs.status !== 'ready')
42
+ return publishedDocs;
43
+ const siblings = [...publishedDocs.value.values()];
41
44
  const map = new Map();
42
45
  for (const published of siblings) {
43
46
  const url = resolveDocumentLiveUrl({
@@ -52,7 +55,7 @@ export function useCmsCollectionViewSource(collectionId) {
52
55
  if (url)
53
56
  map.set(published.id, url);
54
57
  }
55
- return map;
58
+ return { status: 'ready', value: map };
56
59
  }, [collection, isRoutable, publishedDocs, cms.localeConfig, cms.activeLocale]);
57
60
  const documents = useCallback(() => cms.documentsOf(collectionId), [cms, collectionId]);
58
61
  const viewState = useCallback(() => cms.tableStateOf(collectionId), [cms, collectionId]);
@@ -69,7 +72,8 @@ export function useCmsCollectionViewSource(collectionId) {
69
72
  const record = assetStore?.assets.find((asset) => asset.id === id);
70
73
  if (!record)
71
74
  return undefined;
72
- const alt = assetAlt(record); // EC-587: the one reader
75
+ // EC-587: the one reader — EC-632: in the CMS surface's active locale.
76
+ const alt = assetAlt(record, assetReadLocale(cms.localeConfig, cms.activeLocale));
73
77
  return {
74
78
  ...(record.url !== undefined ? { url: record.url } : {}),
75
79
  contentType: record.contentType,
@@ -77,7 +81,7 @@ export function useCmsCollectionViewSource(collectionId) {
77
81
  filename: record.filename,
78
82
  ...(alt !== null ? { alt } : {}),
79
83
  };
80
- }, [assetStore]);
84
+ }, [assetStore, cms.localeConfig, cms.activeLocale]);
81
85
  const liveUrls = useCallback(() => liveUrlMap, [liveUrlMap]);
82
86
  // EC-595 A2: the CMS writes revisioned documents — patches at the source.
83
87
  const writes = useMemo(() => ({
@@ -1 +1 @@
1
- {"version":3,"file":"cms-collection-view-source.js","sourceRoot":"","sources":["../../../src/components/collection-view/cms-collection-view-source.ts"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useMemo } from 'react'\nimport { isHierarchical } from '@elytracms/core/cms-core'\nimport type { DocumentRef, DocumentUrl } from '@elytracms/core/cms-core'\nimport { assetAlt } from '@elytracms/core/persistence'\nimport { studioPath } from '@elytracms/studio/router'\nimport { resolveDeploymentFrontendUrl, resolveDeploymentRoutes } from '../../lib/workspace'\nimport type {\n CollectionViewCapabilities,\n CollectionViewSource,\n DocumentWrites,\n AssetDisplay,\n} from '../../lib/collection-view/collection-view-source'\nimport { useCms } from '../cms/cms-store'\nimport { useOptionalAssets } from '../assets/assets-store'\nimport { usePublishedDocuments } from '../../lib/cms/published-documents'\nimport { resolveDocumentLiveUrl } from '../cms/use-live-url'\n\n/**\n * Assemble a {@link CollectionViewSource} for a `cmsDocuments` collection from the\n * ambient `CmsStore` + its side contexts (assets, published pool) + the\n * deployment config reads (EC-240 / AD-19). This is the CMS adapter: a\n * behaviour-preserving pass-through so the portable list + edit surfaces render\n * over the source instead of `useCms()` directly, byte-for-byte as before.\n *\n * Returns `null` for an unknown collection id (the caller renders the fallback).\n *\n * Referential stability: collection-parameterised methods are `useCallback`-bound\n * and the source object is `useMemo`-ed, so the surfaces' `useMemo` dependency\n * arrays stay stable across renders (the inline-edit order snapshot depends on it).\n */\nexport function useCmsCollectionViewSource(collectionId: string): CollectionViewSource | null {\n const cms = useCms()\n const assetStore = useOptionalAssets()\n const collection = cms.getCollection(collectionId)\n const publishedDocs = usePublishedDocuments(cms.projectId, collectionId)\n\n // The asset collection id is resolved once from the schema (as the cell does).\n const assetCollectionId = useMemo(\n () => cms.collections.find((c) => c.kind === 'asset')?.id ?? 'asset',\n [cms.collections],\n )\n\n // Live-URL column: the published pool × deployment routes, memoised exactly as\n // the table did — derived from PUBLISHED snapshots, never draft rows (EC-205).\n const isRoutable = useMemo(\n () => resolveDeploymentRoutes().some((route) => route.document?.collection === collectionId),\n [collectionId],\n )\n // EC-384: `resolveDocumentLiveUrl` merges the stored routes with the kit's\n // fallback-home rule (same as the document editor's header affordance,\n // `use-live-url.ts`) — otherwise a fallback-addressed home page showed no URL\n // in this column even though the live site serves it.\n const liveUrlMap = useMemo(() => {\n if (!collection || !isRoutable) return null\n const siblings = [...publishedDocs.byId.values()]\n const map = new Map<string, DocumentUrl>()\n for (const published of siblings) {\n const url = resolveDocumentLiveUrl({\n routes: resolveDeploymentRoutes(),\n frontendUrl: resolveDeploymentFrontendUrl(),\n localeConfig: cms.localeConfig,\n collection,\n doc: published,\n siblings,\n locale: cms.activeLocale,\n })\n if (url) map.set(published.id, url)\n }\n return map\n }, [collection, isRoutable, publishedDocs, cms.localeConfig, cms.activeLocale])\n\n const documents = useCallback(() => cms.documentsOf(collectionId), [cms, collectionId])\n const viewState = useCallback(() => cms.tableStateOf(collectionId), [cms, collectionId])\n const setViewState = useCallback(\n (update: Parameters<CollectionViewSource['setViewState']>[0]) =>\n cms.setTableState(collectionId, update),\n [cms, collectionId],\n )\n // Named views (EC-246b): config-as-code lenses the header selector switches between.\n const views = useMemo(\n () => ({\n list: () => cms.viewsOf(collectionId),\n activeId: () => cms.activeViewId(collectionId),\n setActive: (id: string) => cms.setActiveView(collectionId, id),\n }),\n [cms, collectionId],\n )\n const create = useCallback(() => cms.createDocument(collectionId), [cms, collectionId])\n const imports = useMemo(\n () => ({ importDocuments: (items: unknown[]) => cms.importDocuments(collectionId, items) }),\n [cms, collectionId],\n )\n const resolveAsset = useCallback(\n (id: string): AssetDisplay | undefined => {\n const record = assetStore?.assets.find((asset) => asset.id === id)\n if (!record) return undefined\n const alt = assetAlt(record) // EC-587: the one reader\n return {\n ...(record.url !== undefined ? { url: record.url } : {}),\n contentType: record.contentType,\n ...(record.title !== undefined ? { title: record.title } : {}),\n filename: record.filename,\n ...(alt !== null ? { alt } : {}),\n }\n },\n [assetStore],\n )\n const liveUrls = useCallback(() => liveUrlMap, [liveUrlMap])\n // EC-595 A2: the CMS writes revisioned documents — patches at the source.\n const writes = useMemo<DocumentWrites>(\n () => ({\n kind: 'document',\n commit: cms.commitPatches,\n revisionOf: cms.revisionOf,\n insert: cms.insertDocument,\n newId: cms.newDocumentId,\n conflicts: cms.documentConflicts,\n resolveConflict: cms.resolveConflict,\n adoptSnapshot: cms.adoptDocumentSnapshot,\n flush: (ref) => cms.flushPersistence(ref),\n replace: cms.replaceDocument,\n subscribe: cms.subscribeDocument,\n reportPresence: cms.reportPresence,\n clearPresence: cms.clearPresence,\n presenceSessionId: cms.presenceSessionId,\n }),\n [cms],\n )\n\n return useMemo(() => {\n if (!collection) return null\n const capabilities: CollectionViewCapabilities = {\n publish: true,\n versions: true,\n locales: true,\n inlineEdit: true,\n create: true,\n duplicate: true, // EC-442: CMS collections only (owner call — rails skip)\n remove: true,\n bulk: true,\n liveUrl: isRoutable,\n hierarchy: isHierarchical(collection),\n importExport: true,\n }\n const source: CollectionViewSource = {\n collectionId,\n schema: collection,\n capabilities,\n ...(cms.projectId !== undefined ? { projectId: cms.projectId } : {}),\n assetCollectionId,\n localeConfig: cms.localeConfig,\n activeLocale: cms.activeLocale,\n documents,\n get: cms.getDocument,\n titleOf: cms.titleOf,\n // References are collection-based (EC-072): resolve candidates of ANY target\n // collection the schema declares — the workspace doc store already serves them.\n referenceCandidates: cms.documentsOf,\n collections: cms.collections,\n isFieldReadOnly: cms.isFieldReadOnly,\n validate: cms.validate,\n resolveAsset,\n lastError: cms.persistError,\n clearLastError: cms.clearPersistError,\n // Queue flush, so a pre-flight reference-index read is current.\n flush: () => cms.flushPersistence(),\n detailPath: (id: string) => studioPath.cmsDocument(collectionId, id),\n listPath: studioPath.cmsCollection(collectionId),\n viewState,\n setViewState,\n views,\n imports,\n create,\n writes,\n remove: cms.deleteDocument,\n bulk: (ids, action, setField) => cms.runBulk(collectionId, ids, action, setField),\n publishing: {\n isPublished: cms.isPublished,\n // EC-449: the materialized changed flag behind the amber `$state` dot\n // (chokepoint-computed; the store re-stamps it on save/publish/unpublish).\n hasUnpublishedChanges: cms.documentHasUnpublishedChanges,\n publish: cms.publishDocument,\n unpublish: cms.unpublishDocument,\n liveUrls,\n },\n // EC-445/EC-447: per-document timestamps behind the $updatedAt + $publishedAt\n // columns, sourced from the store (hydration seeds them from the persisted\n // `cmsDocuments.updatedAt` / `.publishedAt`; the store re-stamps on the edit +\n // publish/unpublish paths). Both are referentially-stable store accessors.\n timestamps: {\n updatedAt: cms.documentUpdatedAt,\n publishedAt: cms.documentPublishedAt,\n },\n versioning: {\n restore: (ref: DocumentRef, version: number, scope) =>\n cms.restoreDocumentVersion(ref, version, scope),\n // Flush-before-read ordering folds into the loader (EC-224) so any surface\n // that mutated the history still lists an authoritative version set.\n load: async (ref: DocumentRef) => {\n await cms.flushPersistence()\n return cms.loadDocumentVersions(ref)\n },\n },\n inlineEditing: {\n commitField: (input) => cms.commitFieldEdit(input),\n },\n }\n return source\n }, [\n collection,\n collectionId,\n cms,\n isRoutable,\n documents,\n viewState,\n setViewState,\n views,\n imports,\n create,\n resolveAsset,\n liveUrls,\n assetCollectionId,\n writes,\n ])\n}\n"]}
1
+ {"version":3,"file":"cms-collection-view-source.js","sourceRoot":"","sources":["../../../src/components/collection-view/cms-collection-view-source.ts"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useMemo } from 'react'\nimport { isHierarchical } from '@elytracms/core/cms-core'\nimport type { DocumentRef, DocumentUrl } from '@elytracms/core/cms-core'\nimport { assetAlt, assetReadLocale } from '@elytracms/core/persistence'\nimport { studioPath } from '@elytracms/studio/router'\nimport { resolveDeploymentFrontendUrl, resolveDeploymentRoutes } from '../../lib/workspace'\nimport type {\n CollectionViewCapabilities,\n CollectionViewSource,\n DocumentWrites,\n AssetDisplay,\n} from '../../lib/collection-view/collection-view-source'\nimport { useCms } from '../cms/cms-store'\nimport { useOptionalAssets } from '../assets/assets-store'\nimport { usePublishedDocuments } from '../../lib/cms/published-documents'\nimport type { ReadState } from '../../lib/query'\nimport { resolveDocumentLiveUrl } from '../cms/use-live-url'\n\n/**\n * Assemble a {@link CollectionViewSource} for a `cmsDocuments` collection from the\n * ambient `CmsStore` + its side contexts (assets, published pool) + the\n * deployment config reads (EC-240 / AD-19). This is the CMS adapter: a\n * behaviour-preserving pass-through so the portable list + edit surfaces render\n * over the source instead of `useCms()` directly, byte-for-byte as before.\n *\n * Returns `null` for an unknown collection id (the caller renders the fallback).\n *\n * Referential stability: collection-parameterised methods are `useCallback`-bound\n * and the source object is `useMemo`-ed, so the surfaces' `useMemo` dependency\n * arrays stay stable across renders (the inline-edit order snapshot depends on it).\n */\nexport function useCmsCollectionViewSource(collectionId: string): CollectionViewSource | null {\n const cms = useCms()\n const assetStore = useOptionalAssets()\n const collection = cms.getCollection(collectionId)\n const publishedDocs = usePublishedDocuments(cms.projectId, collectionId)\n\n // The asset collection id is resolved once from the schema (as the cell does).\n const assetCollectionId = useMemo(\n () => cms.collections.find((c) => c.kind === 'asset')?.id ?? 'asset',\n [cms.collections],\n )\n\n // Live-URL column: the published pool × deployment routes, memoised exactly as\n // the table did — derived from PUBLISHED snapshots, never draft rows (EC-205).\n const isRoutable = useMemo(\n () => resolveDeploymentRoutes().some((route) => route.document?.collection === collectionId),\n [collectionId],\n )\n // EC-384: `resolveDocumentLiveUrl` merges the stored routes with the kit's\n // fallback-home rule (same as the document editor's header affordance,\n // `use-live-url.ts`) — otherwise a fallback-addressed home page showed no URL\n // in this column even though the live site serves it.\n // EC-628: the pool's read state carries through — a failed read is the column's error.\n const liveUrlMap = useMemo((): ReadState<ReadonlyMap<string, DocumentUrl>> | null => {\n if (!collection || !isRoutable) return null\n if (publishedDocs.status !== 'ready') return publishedDocs\n const siblings = [...publishedDocs.value.values()]\n const map = new Map<string, DocumentUrl>()\n for (const published of siblings) {\n const url = resolveDocumentLiveUrl({\n routes: resolveDeploymentRoutes(),\n frontendUrl: resolveDeploymentFrontendUrl(),\n localeConfig: cms.localeConfig,\n collection,\n doc: published,\n siblings,\n locale: cms.activeLocale,\n })\n if (url) map.set(published.id, url)\n }\n return { status: 'ready', value: map }\n }, [collection, isRoutable, publishedDocs, cms.localeConfig, cms.activeLocale])\n\n const documents = useCallback(() => cms.documentsOf(collectionId), [cms, collectionId])\n const viewState = useCallback(() => cms.tableStateOf(collectionId), [cms, collectionId])\n const setViewState = useCallback(\n (update: Parameters<CollectionViewSource['setViewState']>[0]) =>\n cms.setTableState(collectionId, update),\n [cms, collectionId],\n )\n // Named views (EC-246b): config-as-code lenses the header selector switches between.\n const views = useMemo(\n () => ({\n list: () => cms.viewsOf(collectionId),\n activeId: () => cms.activeViewId(collectionId),\n setActive: (id: string) => cms.setActiveView(collectionId, id),\n }),\n [cms, collectionId],\n )\n const create = useCallback(() => cms.createDocument(collectionId), [cms, collectionId])\n const imports = useMemo(\n () => ({ importDocuments: (items: unknown[]) => cms.importDocuments(collectionId, items) }),\n [cms, collectionId],\n )\n const resolveAsset = useCallback(\n (id: string): AssetDisplay | undefined => {\n const record = assetStore?.assets.find((asset) => asset.id === id)\n if (!record) return undefined\n // EC-587: the one reader — EC-632: in the CMS surface's active locale.\n const alt = assetAlt(record, assetReadLocale(cms.localeConfig, cms.activeLocale))\n return {\n ...(record.url !== undefined ? { url: record.url } : {}),\n contentType: record.contentType,\n ...(record.title !== undefined ? { title: record.title } : {}),\n filename: record.filename,\n ...(alt !== null ? { alt } : {}),\n }\n },\n [assetStore, cms.localeConfig, cms.activeLocale],\n )\n const liveUrls = useCallback(() => liveUrlMap, [liveUrlMap])\n // EC-595 A2: the CMS writes revisioned documents — patches at the source.\n const writes = useMemo<DocumentWrites>(\n () => ({\n kind: 'document',\n commit: cms.commitPatches,\n revisionOf: cms.revisionOf,\n insert: cms.insertDocument,\n newId: cms.newDocumentId,\n conflicts: cms.documentConflicts,\n resolveConflict: cms.resolveConflict,\n adoptSnapshot: cms.adoptDocumentSnapshot,\n flush: (ref) => cms.flushPersistence(ref),\n replace: cms.replaceDocument,\n subscribe: cms.subscribeDocument,\n reportPresence: cms.reportPresence,\n clearPresence: cms.clearPresence,\n presenceSessionId: cms.presenceSessionId,\n }),\n [cms],\n )\n\n return useMemo(() => {\n if (!collection) return null\n const capabilities: CollectionViewCapabilities = {\n publish: true,\n versions: true,\n locales: true,\n inlineEdit: true,\n create: true,\n duplicate: true, // EC-442: CMS collections only (owner call — rails skip)\n remove: true,\n bulk: true,\n liveUrl: isRoutable,\n hierarchy: isHierarchical(collection),\n importExport: true,\n }\n const source: CollectionViewSource = {\n collectionId,\n schema: collection,\n capabilities,\n ...(cms.projectId !== undefined ? { projectId: cms.projectId } : {}),\n assetCollectionId,\n localeConfig: cms.localeConfig,\n activeLocale: cms.activeLocale,\n documents,\n get: cms.getDocument,\n titleOf: cms.titleOf,\n // References are collection-based (EC-072): resolve candidates of ANY target\n // collection the schema declares — the workspace doc store already serves them.\n referenceCandidates: cms.documentsOf,\n collections: cms.collections,\n isFieldReadOnly: cms.isFieldReadOnly,\n validate: cms.validate,\n resolveAsset,\n lastError: cms.persistError,\n clearLastError: cms.clearPersistError,\n // Queue flush, so a pre-flight reference-index read is current.\n flush: () => cms.flushPersistence(),\n detailPath: (id: string) => studioPath.cmsDocument(collectionId, id),\n listPath: studioPath.cmsCollection(collectionId),\n viewState,\n setViewState,\n views,\n imports,\n create,\n writes,\n remove: cms.deleteDocument,\n bulk: (ids, action, setField) => cms.runBulk(collectionId, ids, action, setField),\n publishing: {\n isPublished: cms.isPublished,\n // EC-449: the materialized changed flag behind the amber `$state` dot\n // (chokepoint-computed; the store re-stamps it on save/publish/unpublish).\n hasUnpublishedChanges: cms.documentHasUnpublishedChanges,\n publish: cms.publishDocument,\n unpublish: cms.unpublishDocument,\n liveUrls,\n },\n // EC-445/EC-447: per-document timestamps behind the $updatedAt + $publishedAt\n // columns, sourced from the store (hydration seeds them from the persisted\n // `cmsDocuments.updatedAt` / `.publishedAt`; the store re-stamps on the edit +\n // publish/unpublish paths). Both are referentially-stable store accessors.\n timestamps: {\n updatedAt: cms.documentUpdatedAt,\n publishedAt: cms.documentPublishedAt,\n },\n versioning: {\n restore: (ref: DocumentRef, version: number, scope) =>\n cms.restoreDocumentVersion(ref, version, scope),\n // Flush-before-read ordering folds into the loader (EC-224) so any surface\n // that mutated the history still lists an authoritative version set.\n load: async (ref: DocumentRef) => {\n await cms.flushPersistence()\n return cms.loadDocumentVersions(ref)\n },\n },\n inlineEditing: {\n commitField: (input) => cms.commitFieldEdit(input),\n },\n }\n return source\n }, [\n collection,\n collectionId,\n cms,\n isRoutable,\n documents,\n viewState,\n setViewState,\n views,\n imports,\n create,\n resolveAsset,\n liveUrls,\n assetCollectionId,\n writes,\n ])\n}\n"]}
@@ -147,7 +147,16 @@ function EnvCheckRow({ check }) {
147
147
  return (_jsxs("div", { role: "listitem", className: `rounded-md border p-2 ${failing ? 'border-destructive/30 bg-destructive/5' : 'border-border bg-muted/30'}`, children: [_jsxs(Text, { size: "xs", mono: true, className: failing ? 'text-destructive' : undefined, children: [ENV_STATUS_GLYPH[check.status], " [", check.id, "]"] }), _jsx(Text, { size: "xs", tone: "muted", children: check.detail }), check.fix && (check.status === 'fail' || check.status === 'warn') ? (_jsxs(Text, { size: "xs", tone: "muted", className: "mt-1", children: [translate('shell.deploymentSetup.fixLabel'), ": ", _jsx(Code, { className: "break-all", children: check.fix })] })) : null] }));
148
148
  }
149
149
  function CheckRow({ check }) {
150
- return (_jsxs("div", { role: "listitem", className: "rounded-md border border-destructive/30 bg-destructive/5 p-2", children: [_jsxs(Text, { size: "xs", mono: true, className: "text-destructive", children: ["[", check.id, "]"] }), _jsx(Text, { size: "sm", children: check.detail }), check.fix ? (_jsxs(Text, { size: "xs", tone: "muted", className: "mt-1", children: [translate('shell.deploymentSetup.fixLabel'), ": ", _jsx(Code, { className: "break-all", children: check.fix })] })) : null] }));
150
+ return (_jsxs("div", { role: "listitem", className: "rounded-md border border-destructive/30 bg-destructive/5 p-2", children: [_jsxs(Text, { size: "xs", mono: true, className: "text-destructive", children: ["[", check.id, "]"] }), _jsx(Text, { size: "sm", children: check.detail }), check.issues && check.issues.length > 0 ? _jsx(CheckIssueList, { issues: check.issues }) : null, check.fix ? (_jsxs(Text, { size: "xs", tone: "muted", className: "mt-1", children: [translate('shell.deploymentSetup.fixLabel'), ": ", _jsx(Code, { className: "break-all", children: check.fix })] })) : null] }));
151
+ }
152
+ /**
153
+ * EC-626 — the located issues behind a failing check, one line each, exactly as
154
+ * `elytra doctor` prints them (core's `formatConfigIssue`, applied in the shared
155
+ * contract): `config-valid` lists every config issue instead of a count. English, like
156
+ * the check's `detail` — they name keys and paths in the developer's own config.
157
+ */
158
+ function CheckIssueList({ issues }) {
159
+ return (_jsx(Stack, { gap: "1", role: "list", "aria-label": translate('shell.workspaceGate.issuesLabel'), className: "mt-1", children: issues.map((issue, index) => (_jsx(Text, { role: "listitem", size: "xs", mono: true, className: "break-words", children: issue }, index))) }));
151
160
  }
152
161
  function BootScreenFrame({ children }) {
153
162
  return _jsx("div", { className: "grid min-h-dvh place-items-center bg-background p-6", children: children });
@@ -1 +1 @@
1
- {"version":3,"file":"deployment-setup-gate.js","sourceRoot":"","sources":["../../src/components/deployment-setup-gate.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useEffect, useMemo, useState } from 'react'\nimport type { ReactNode } from 'react'\nimport { Loader2 } from 'lucide-react'\nimport { Button, Card, CardContent, CardHeader, CardTitle, Code, Stack, Text } from '@elytracms/studio/ui'\nimport {\n resolveBackendConfig,\n selectPersistenceAdapter,\n studioDeploymentReadiness,\n} from '../lib/persistence'\nimport {\n getWorkspaceState,\n resolveDeploymentAi,\n resolveDeploymentCollections,\n resolveDeploymentUrl,\n} from '../lib/workspace'\nimport { translate, useHydrated, useT } from '../lib/i18n'\nimport { probeDeployedFunctionsVersion } from '@elytracms/core/export-sync/deployment-ready'\nimport type {\n DeploymentReadinessReport,\n FunctionsVersionProbe,\n ReadinessCheck,\n} from '@elytracms/core/export-sync/deployment-ready'\n\n/**\n * Deployment-setup boot gate (EC-258) — the single readiness surface that\n * supersedes the separate `BackendGate` (missing `VITE_CONVEX_URL` / unreachable)\n * and `WorkspaceGate` (invalid config) screens by composing the shared\n * `checkDeploymentReady` contract. It renders ONE deterministic \"Deployment setup\"\n * screen listing each failing precondition with its copy-pasteable fix, instead of\n * letting the app boot into a misleading downstream error (most painfully the\n * Konditorei JWT-key footgun). All checks passing → children render untouched.\n *\n * Runs before auth and any adapter is constructed (it is the thing that keeps\n * `selectAuthAdapter`/`selectPersistenceAdapter` from throwing on a misconfigured\n * backend). Convex mode probes reachability once on the client (`adapter.status()`)\n * with a retry; playground and unconfigured resolve synchronously. SSR renders the\n * deterministic \"checking\" state. Auth-signing-keys is reported as `unknown` at boot\n * (the JWKS endpoint is CORS-blocked from the browser — EC-257's honest sign-in error\n * is the browser's actionable surface; `elytra doctor` probes it from node), and the\n * per-user bucket stays `unknown` (this gate runs pre-auth), so neither ever blocks boot.\n */\nexport function DeploymentSetupGate({ children }: { children: ReactNode }) {\n const config = useMemo(() => resolveBackendConfig(), [])\n // EC-399: a configured (and not disabled) `defineStudio({ ai })` block means the\n // AI route exists — its provider-key expectation joins the deployment-env checks.\n const aiRouteConfigured = useMemo(() => {\n const ai = resolveDeploymentAi()\n return Boolean(ai && ai.enabled !== false)\n }, [])\n\n const staticReport = useCallback(\n (): DeploymentReadinessReport =>\n studioDeploymentReadiness({\n backend: config,\n workspace: getWorkspaceState(),\n collections: resolveDeploymentCollections(),\n aiRouteConfigured,\n }),\n [config, aiRouteConfigured],\n )\n\n const [state, setState] = useState<GateState>(() =>\n config.kind === 'convex' ? { phase: 'checking' } : { phase: 'resolved', report: staticReport() },\n )\n\n const check = useCallback(() => {\n if (config.kind !== 'convex') {\n setState({ phase: 'resolved', report: staticReport() })\n return\n }\n const effectiveUrl = resolveDeploymentUrl(config.url)\n const resolve = (\n reachable: { available: boolean; detail?: string },\n functionsVersion: FunctionsVersionProbe,\n ) =>\n setState({\n phase: 'resolved',\n report: studioDeploymentReadiness({\n backend: config,\n effectiveUrl,\n workspace: getWorkspaceState(),\n collections: resolveDeploymentCollections(),\n reachable,\n functionsVersion,\n aiRouteConfigured,\n }),\n })\n setState({ phase: 'checking' })\n // EC-552: the version handshake probes in parallel with the reachability check —\n // same deployment URL, one extra public query. The probe never rejects (network\n // failures resolve to an honest `unknown`), so awaiting it in both paths is safe.\n const versionProbe = probeDeployedFunctionsVersion(effectiveUrl)\n void selectPersistenceAdapter()\n .then((adapter) => adapter.status())\n .then(async (status) =>\n resolve(\n { available: status.available, ...(status.detail ? { detail: status.detail } : {}) },\n await versionProbe,\n ),\n )\n .catch(async (error: unknown) =>\n resolve(\n { available: false, detail: error instanceof Error ? error.message : String(error) },\n await versionProbe,\n ),\n )\n }, [config, staticReport, aiRouteConfigured])\n\n useEffect(() => {\n if (config.kind === 'convex') check()\n }, [config.kind, check])\n\n if (state.phase === 'checking') return <DeploymentChecking />\n if (blockingFailures(state.report).length > 0) {\n return (\n <DeploymentSetupScreen\n report={state.report}\n onRetry={config.kind === 'convex' ? check : undefined}\n />\n )\n }\n return <>{children}</>\n}\n\ntype GateState = { phase: 'checking' } | { phase: 'resolved'; report: DeploymentReadinessReport }\n\n/**\n * The checks that BLOCK boot: a failed deployment-, deployment-env-, or app-level\n * precondition. The per-user bucket (\"not a member\") is not a boot blocker — it is\n * surfaced post-auth (\"ask an admin\"), and at boot it is `unknown` (pre-auth) anyway.\n * Deployment-env checks are `unknown` in the browser today (nothing gathered — EC-399),\n * so they never block in practice; `warn` findings never block anywhere.\n */\nfunction blockingFailures(report: DeploymentReadinessReport): ReadinessCheck[] {\n return report.checks.filter(\n (c) =>\n c.status === 'fail' &&\n (c.bucket === 'deployment' || c.bucket === 'deployment-env' || c.bucket === 'app'),\n )\n}\n\n/**\n * Deterministic boot state while the backend probe is in flight — and, in a\n * CONNECTED build, the body of the prerendered shell itself.\n *\n * **EC-525.** EC-523 removed the last localized string from the shell and proved it\n * against the built artefact — but against the *playground* build, which is the demo's\n * default and the one nobody deploys. Every real studio is connected, so\n * `config.kind === 'convex'` seeds `{ phase: 'checking' }` and *this* is what a\n * production cold load actually bakes: `Checking deployment…`, in the build machine's\n * locale, shown to every visitor whatever theirs is. The class was not closed until\n * both builds satisfied it.\n *\n * Same shape as `SessionLoading` (`router/root.tsx`), for the same reasons, which are\n * written out there: a build-time prerender has no request and therefore no locale, so\n * the shell shows a spinner and an EMPTY `role=\"status\"` region, and `useHydrated`\n * fills it in the visitor's real language once hydration lands. No `aria-label` — that\n * bakes the wrong-language string somewhere a screen-reader user cannot see it flip —\n * and no `aria-busy`, which would suppress the announcement this shape exists to make.\n *\n * Exported so the connected shell's pre-hydration markup is pinnable with\n * `renderToStaticMarkup` — same reason `SessionLoading` is. The built artefact is the\n * real proof; the pin is what fails when someone puts a string back.\n */\nexport function DeploymentChecking() {\n const hydrated = useHydrated()\n const t = useT()\n return (\n <BootScreenFrame>\n <div role=\"status\">\n <Loader2 className=\"size-6 animate-spin text-muted-foreground\" aria-hidden=\"true\" />\n {hydrated ? <span className=\"sr-only\">{t('shell.deploymentSetup.checking')}</span> : null}\n </div>\n </BootScreenFrame>\n )\n}\n\n/**\n * The unified \"Deployment setup\" screen (EC-258): every failing check + its exact\n * fix. Pure/presentational so it is fixture-reproducible and static-render testable.\n */\nexport function DeploymentSetupScreen({\n report,\n onRetry,\n}: {\n report: DeploymentReadinessReport\n onRetry?: () => void\n}) {\n const failures = report.checks.filter((c) => c.status === 'fail')\n const envChecks = report.checks.filter((c) => c.bucket === 'deployment-env')\n return (\n <BootScreenFrame>\n <Card className=\"max-w-2xl border-destructive/40\">\n <CardHeader>\n <CardTitle>{translate('shell.deploymentSetup.title')}</CardTitle>\n </CardHeader>\n <CardContent>\n <Stack gap=\"3\">\n <Text size=\"sm\" tone=\"muted\">\n {translate('shell.deploymentSetup.description')}\n </Text>\n <Stack gap=\"2\" role=\"list\" aria-label={translate('shell.deploymentSetup.issuesLabel')}>\n {failures.map((check) => (\n <CheckRow key={check.id} check={check} />\n ))}\n </Stack>\n {envChecks.length > 0 ? <ExpectedEnvSection checks={envChecks} /> : null}\n {onRetry ? (\n <Button variant=\"outline\" size=\"sm\" onClick={onRetry} className=\"self-start\">\n {translate('shell.deploymentSetup.retry')}\n </Button>\n ) : null}\n </Stack>\n </CardContent>\n </Card>\n </BootScreenFrame>\n )\n}\n\n/**\n * The expected-env facts (EC-399) — the SAME contract `elytra doctor` checks, so the\n * studio and CLI tell one story. Presence only: each row is a var name + set/missing/\n * unknown status text; values never exist in the report. In the browser the presence\n * sources aren't readable pre-auth, so rows typically read `unknown` and point at\n * `elytra doctor` — which is exactly the honest answer.\n */\nfunction ExpectedEnvSection({ checks }: { checks: ReadinessCheck[] }) {\n return (\n <Stack gap=\"2\">\n <Text size=\"sm\" className=\"font-medium\">\n {translate('shell.deploymentSetup.envSection')}\n </Text>\n <Text size=\"xs\" tone=\"muted\">\n {translate('shell.deploymentSetup.envSectionHint')}\n </Text>\n <Stack gap=\"1\" role=\"list\" aria-label={translate('shell.deploymentSetup.envSection')}>\n {checks.map((check) => (\n <EnvCheckRow key={check.id} check={check} />\n ))}\n </Stack>\n </Stack>\n )\n}\n\nconst ENV_STATUS_GLYPH: Record<ReadinessCheck['status'], string> = {\n ok: '✓',\n warn: '!',\n fail: '✗',\n unknown: '?',\n}\n\nfunction EnvCheckRow({ check }: { check: ReadinessCheck }) {\n const failing = check.status === 'fail'\n return (\n <div\n role=\"listitem\"\n className={`rounded-md border p-2 ${failing ? 'border-destructive/30 bg-destructive/5' : 'border-border bg-muted/30'}`}\n >\n <Text size=\"xs\" mono className={failing ? 'text-destructive' : undefined}>\n {ENV_STATUS_GLYPH[check.status]} [{check.id}]\n </Text>\n <Text size=\"xs\" tone=\"muted\">\n {check.detail}\n </Text>\n {check.fix && (check.status === 'fail' || check.status === 'warn') ? (\n <Text size=\"xs\" tone=\"muted\" className=\"mt-1\">\n {translate('shell.deploymentSetup.fixLabel')}: <Code className=\"break-all\">{check.fix}</Code>\n </Text>\n ) : null}\n </div>\n )\n}\n\nfunction CheckRow({ check }: { check: ReadinessCheck }) {\n return (\n <div role=\"listitem\" className=\"rounded-md border border-destructive/30 bg-destructive/5 p-2\">\n <Text size=\"xs\" mono className=\"text-destructive\">\n [{check.id}]\n </Text>\n <Text size=\"sm\">{check.detail}</Text>\n {check.fix ? (\n <Text size=\"xs\" tone=\"muted\" className=\"mt-1\">\n {translate('shell.deploymentSetup.fixLabel')}: <Code className=\"break-all\">{check.fix}</Code>\n </Text>\n ) : null}\n </div>\n )\n}\n\nfunction BootScreenFrame({ children }: { children: ReactNode }) {\n return <div className=\"grid min-h-dvh place-items-center bg-background p-6\">{children}</div>\n}\n"]}
1
+ {"version":3,"file":"deployment-setup-gate.js","sourceRoot":"","sources":["../../src/components/deployment-setup-gate.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useEffect, useMemo, useState } from 'react'\nimport type { ReactNode } from 'react'\nimport { Loader2 } from 'lucide-react'\nimport { Button, Card, CardContent, CardHeader, CardTitle, Code, Stack, Text } from '@elytracms/studio/ui'\nimport {\n resolveBackendConfig,\n selectPersistenceAdapter,\n studioDeploymentReadiness,\n} from '../lib/persistence'\nimport {\n getWorkspaceState,\n resolveDeploymentAi,\n resolveDeploymentCollections,\n resolveDeploymentUrl,\n} from '../lib/workspace'\nimport { translate, useHydrated, useT } from '../lib/i18n'\nimport { probeDeployedFunctionsVersion } from '@elytracms/core/export-sync/deployment-ready'\nimport type {\n DeploymentReadinessReport,\n FunctionsVersionProbe,\n ReadinessCheck,\n} from '@elytracms/core/export-sync/deployment-ready'\n\n/**\n * Deployment-setup boot gate (EC-258) — the single readiness surface that\n * supersedes the separate `BackendGate` (missing `VITE_CONVEX_URL` / unreachable)\n * and `WorkspaceGate` (invalid config) screens by composing the shared\n * `checkDeploymentReady` contract. It renders ONE deterministic \"Deployment setup\"\n * screen listing each failing precondition with its copy-pasteable fix, instead of\n * letting the app boot into a misleading downstream error (most painfully the\n * Konditorei JWT-key footgun). All checks passing → children render untouched.\n *\n * Runs before auth and any adapter is constructed (it is the thing that keeps\n * `selectAuthAdapter`/`selectPersistenceAdapter` from throwing on a misconfigured\n * backend). Convex mode probes reachability once on the client (`adapter.status()`)\n * with a retry; playground and unconfigured resolve synchronously. SSR renders the\n * deterministic \"checking\" state. Auth-signing-keys is reported as `unknown` at boot\n * (the JWKS endpoint is CORS-blocked from the browser — EC-257's honest sign-in error\n * is the browser's actionable surface; `elytra doctor` probes it from node), and the\n * per-user bucket stays `unknown` (this gate runs pre-auth), so neither ever blocks boot.\n */\nexport function DeploymentSetupGate({ children }: { children: ReactNode }) {\n const config = useMemo(() => resolveBackendConfig(), [])\n // EC-399: a configured (and not disabled) `defineStudio({ ai })` block means the\n // AI route exists — its provider-key expectation joins the deployment-env checks.\n const aiRouteConfigured = useMemo(() => {\n const ai = resolveDeploymentAi()\n return Boolean(ai && ai.enabled !== false)\n }, [])\n\n const staticReport = useCallback(\n (): DeploymentReadinessReport =>\n studioDeploymentReadiness({\n backend: config,\n workspace: getWorkspaceState(),\n collections: resolveDeploymentCollections(),\n aiRouteConfigured,\n }),\n [config, aiRouteConfigured],\n )\n\n const [state, setState] = useState<GateState>(() =>\n config.kind === 'convex' ? { phase: 'checking' } : { phase: 'resolved', report: staticReport() },\n )\n\n const check = useCallback(() => {\n if (config.kind !== 'convex') {\n setState({ phase: 'resolved', report: staticReport() })\n return\n }\n const effectiveUrl = resolveDeploymentUrl(config.url)\n const resolve = (\n reachable: { available: boolean; detail?: string },\n functionsVersion: FunctionsVersionProbe,\n ) =>\n setState({\n phase: 'resolved',\n report: studioDeploymentReadiness({\n backend: config,\n effectiveUrl,\n workspace: getWorkspaceState(),\n collections: resolveDeploymentCollections(),\n reachable,\n functionsVersion,\n aiRouteConfigured,\n }),\n })\n setState({ phase: 'checking' })\n // EC-552: the version handshake probes in parallel with the reachability check —\n // same deployment URL, one extra public query. The probe never rejects (network\n // failures resolve to an honest `unknown`), so awaiting it in both paths is safe.\n const versionProbe = probeDeployedFunctionsVersion(effectiveUrl)\n void selectPersistenceAdapter()\n .then((adapter) => adapter.status())\n .then(async (status) =>\n resolve(\n { available: status.available, ...(status.detail ? { detail: status.detail } : {}) },\n await versionProbe,\n ),\n )\n .catch(async (error: unknown) =>\n resolve(\n { available: false, detail: error instanceof Error ? error.message : String(error) },\n await versionProbe,\n ),\n )\n }, [config, staticReport, aiRouteConfigured])\n\n useEffect(() => {\n if (config.kind === 'convex') check()\n }, [config.kind, check])\n\n if (state.phase === 'checking') return <DeploymentChecking />\n if (blockingFailures(state.report).length > 0) {\n return (\n <DeploymentSetupScreen\n report={state.report}\n onRetry={config.kind === 'convex' ? check : undefined}\n />\n )\n }\n return <>{children}</>\n}\n\ntype GateState = { phase: 'checking' } | { phase: 'resolved'; report: DeploymentReadinessReport }\n\n/**\n * The checks that BLOCK boot: a failed deployment-, deployment-env-, or app-level\n * precondition. The per-user bucket (\"not a member\") is not a boot blocker — it is\n * surfaced post-auth (\"ask an admin\"), and at boot it is `unknown` (pre-auth) anyway.\n * Deployment-env checks are `unknown` in the browser today (nothing gathered — EC-399),\n * so they never block in practice; `warn` findings never block anywhere.\n */\nfunction blockingFailures(report: DeploymentReadinessReport): ReadinessCheck[] {\n return report.checks.filter(\n (c) =>\n c.status === 'fail' &&\n (c.bucket === 'deployment' || c.bucket === 'deployment-env' || c.bucket === 'app'),\n )\n}\n\n/**\n * Deterministic boot state while the backend probe is in flight — and, in a\n * CONNECTED build, the body of the prerendered shell itself.\n *\n * **EC-525.** EC-523 removed the last localized string from the shell and proved it\n * against the built artefact — but against the *playground* build, which is the demo's\n * default and the one nobody deploys. Every real studio is connected, so\n * `config.kind === 'convex'` seeds `{ phase: 'checking' }` and *this* is what a\n * production cold load actually bakes: `Checking deployment…`, in the build machine's\n * locale, shown to every visitor whatever theirs is. The class was not closed until\n * both builds satisfied it.\n *\n * Same shape as `SessionLoading` (`router/root.tsx`), for the same reasons, which are\n * written out there: a build-time prerender has no request and therefore no locale, so\n * the shell shows a spinner and an EMPTY `role=\"status\"` region, and `useHydrated`\n * fills it in the visitor's real language once hydration lands. No `aria-label` — that\n * bakes the wrong-language string somewhere a screen-reader user cannot see it flip —\n * and no `aria-busy`, which would suppress the announcement this shape exists to make.\n *\n * Exported so the connected shell's pre-hydration markup is pinnable with\n * `renderToStaticMarkup` — same reason `SessionLoading` is. The built artefact is the\n * real proof; the pin is what fails when someone puts a string back.\n */\nexport function DeploymentChecking() {\n const hydrated = useHydrated()\n const t = useT()\n return (\n <BootScreenFrame>\n <div role=\"status\">\n <Loader2 className=\"size-6 animate-spin text-muted-foreground\" aria-hidden=\"true\" />\n {hydrated ? <span className=\"sr-only\">{t('shell.deploymentSetup.checking')}</span> : null}\n </div>\n </BootScreenFrame>\n )\n}\n\n/**\n * The unified \"Deployment setup\" screen (EC-258): every failing check + its exact\n * fix. Pure/presentational so it is fixture-reproducible and static-render testable.\n */\nexport function DeploymentSetupScreen({\n report,\n onRetry,\n}: {\n report: DeploymentReadinessReport\n onRetry?: () => void\n}) {\n const failures = report.checks.filter((c) => c.status === 'fail')\n const envChecks = report.checks.filter((c) => c.bucket === 'deployment-env')\n return (\n <BootScreenFrame>\n <Card className=\"max-w-2xl border-destructive/40\">\n <CardHeader>\n <CardTitle>{translate('shell.deploymentSetup.title')}</CardTitle>\n </CardHeader>\n <CardContent>\n <Stack gap=\"3\">\n <Text size=\"sm\" tone=\"muted\">\n {translate('shell.deploymentSetup.description')}\n </Text>\n <Stack gap=\"2\" role=\"list\" aria-label={translate('shell.deploymentSetup.issuesLabel')}>\n {failures.map((check) => (\n <CheckRow key={check.id} check={check} />\n ))}\n </Stack>\n {envChecks.length > 0 ? <ExpectedEnvSection checks={envChecks} /> : null}\n {onRetry ? (\n <Button variant=\"outline\" size=\"sm\" onClick={onRetry} className=\"self-start\">\n {translate('shell.deploymentSetup.retry')}\n </Button>\n ) : null}\n </Stack>\n </CardContent>\n </Card>\n </BootScreenFrame>\n )\n}\n\n/**\n * The expected-env facts (EC-399) — the SAME contract `elytra doctor` checks, so the\n * studio and CLI tell one story. Presence only: each row is a var name + set/missing/\n * unknown status text; values never exist in the report. In the browser the presence\n * sources aren't readable pre-auth, so rows typically read `unknown` and point at\n * `elytra doctor` — which is exactly the honest answer.\n */\nfunction ExpectedEnvSection({ checks }: { checks: ReadinessCheck[] }) {\n return (\n <Stack gap=\"2\">\n <Text size=\"sm\" className=\"font-medium\">\n {translate('shell.deploymentSetup.envSection')}\n </Text>\n <Text size=\"xs\" tone=\"muted\">\n {translate('shell.deploymentSetup.envSectionHint')}\n </Text>\n <Stack gap=\"1\" role=\"list\" aria-label={translate('shell.deploymentSetup.envSection')}>\n {checks.map((check) => (\n <EnvCheckRow key={check.id} check={check} />\n ))}\n </Stack>\n </Stack>\n )\n}\n\nconst ENV_STATUS_GLYPH: Record<ReadinessCheck['status'], string> = {\n ok: '✓',\n warn: '!',\n fail: '✗',\n unknown: '?',\n}\n\nfunction EnvCheckRow({ check }: { check: ReadinessCheck }) {\n const failing = check.status === 'fail'\n return (\n <div\n role=\"listitem\"\n className={`rounded-md border p-2 ${failing ? 'border-destructive/30 bg-destructive/5' : 'border-border bg-muted/30'}`}\n >\n <Text size=\"xs\" mono className={failing ? 'text-destructive' : undefined}>\n {ENV_STATUS_GLYPH[check.status]} [{check.id}]\n </Text>\n <Text size=\"xs\" tone=\"muted\">\n {check.detail}\n </Text>\n {check.fix && (check.status === 'fail' || check.status === 'warn') ? (\n <Text size=\"xs\" tone=\"muted\" className=\"mt-1\">\n {translate('shell.deploymentSetup.fixLabel')}: <Code className=\"break-all\">{check.fix}</Code>\n </Text>\n ) : null}\n </div>\n )\n}\n\nfunction CheckRow({ check }: { check: ReadinessCheck }) {\n return (\n <div role=\"listitem\" className=\"rounded-md border border-destructive/30 bg-destructive/5 p-2\">\n <Text size=\"xs\" mono className=\"text-destructive\">\n [{check.id}]\n </Text>\n <Text size=\"sm\">{check.detail}</Text>\n {check.issues && check.issues.length > 0 ? <CheckIssueList issues={check.issues} /> : null}\n {check.fix ? (\n <Text size=\"xs\" tone=\"muted\" className=\"mt-1\">\n {translate('shell.deploymentSetup.fixLabel')}: <Code className=\"break-all\">{check.fix}</Code>\n </Text>\n ) : null}\n </div>\n )\n}\n\n/**\n * EC-626 — the located issues behind a failing check, one line each, exactly as\n * `elytra doctor` prints them (core's `formatConfigIssue`, applied in the shared\n * contract): `config-valid` lists every config issue instead of a count. English, like\n * the check's `detail` — they name keys and paths in the developer's own config.\n */\nfunction CheckIssueList({ issues }: { issues: readonly string[] }) {\n return (\n <Stack gap=\"1\" role=\"list\" aria-label={translate('shell.workspaceGate.issuesLabel')} className=\"mt-1\">\n {issues.map((issue, index) => (\n <Text key={index} role=\"listitem\" size=\"xs\" mono className=\"break-words\">\n {issue}\n </Text>\n ))}\n </Stack>\n )\n}\n\nfunction BootScreenFrame({ children }: { children: ReactNode }) {\n return <div className=\"grid min-h-dvh place-items-center bg-background p-6\">{children}</div>\n}\n"]}
@@ -1,4 +1,5 @@
1
1
  import { collectionDefSchema, semanticValueEqual } from '@elytracms/core/cms-core';
2
+ import { metaPatchBetween } from '@elytracms/core/persistence';
2
3
  import { metaBagProjection } from '../rail-view-source.js';
3
4
  /**
4
5
  * EC-616 — the projections between a record kind and the portable list/edit surfaces
@@ -92,14 +93,8 @@ export function lensDocumentToRecordWrite(kind, doc, base) {
92
93
  // A cleared optional column is `null` on the wire (EC-577: clear), never a silent absence.
93
94
  write[column] = value === undefined ? (isRequiredText(field) ? '' : null) : columnWriteValue(field, value);
94
95
  }
95
- const meta = metaBagProjection(values, columns);
96
- const baseMeta = metaBagProjection(before, columns);
97
- const metaPatch = {};
98
- for (const key of new Set([...Object.keys(meta), ...Object.keys(baseMeta)])) {
99
- if (semanticValueEqual(meta[key], baseMeta[key]))
100
- continue;
101
- metaPatch[key] = meta[key] === undefined ? null : meta[key];
102
- }
96
+ // The one per-key diff (core `metaPatchBetween`), shared with the asset editors (EC-634).
97
+ const metaPatch = metaPatchBetween(metaBagProjection(before, columns), metaBagProjection(values, columns));
103
98
  if (Object.keys(metaPatch).length > 0)
104
99
  write.metaPatch = metaPatch;
105
100
  return write;
@@ -1 +1 @@
1
- {"version":3,"file":"record-lens.js","sourceRoot":"","sources":["../../../src/components/records/record-lens.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAA;AAGlF,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAA;AAEvD;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,iFAAiF;AACjF,MAAM,UAAU,iBAAiB,CAAC,IAAmB;IACnD,OAAO,IAAI,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAA;AAC1C,CAAC;AAED,uEAAuE;AACvE,MAAM,UAAU,oBAAoB,CAA0B,IAAO,EAAE,KAAuB;IAC5F,MAAM,MAAM,GAAG,KAAgC,CAAA;IAC/C,MAAM,OAAO,GAA4B,EAAE,CAAA;IAC3C,KAAK,MAAM,MAAM,IAAI,iBAAiB,CAAC,IAAI,CAAC,EAAE,CAAC;QAC7C,IAAI,MAAM,CAAC,MAAM,CAAC,KAAK,SAAS;YAAE,OAAO,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC,MAAM,CAAC,CAAA;IACpE,CAAC;IACD,OAAO;QACL,UAAU,EAAE,IAAI,CAAC,IAAI;QACrB,EAAE,EAAE,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC;QACrB,MAAM,EAAE,EAAE,GAAG,CAAE,MAAM,CAAC,IAA4C,IAAI,EAAE,CAAC,EAAE,GAAG,OAAO,EAAE;KACxF,CAAA;AACH,CAAC;AAYD,SAAS,KAAK,CAAC,KAAc;IAC3B,OAAQ,KAAuC,CAAC,IAAI,CAAC,GAAG,CAAA;AAC1D,CAAC;AAED,wEAAwE;AACxE,SAAS,cAAc,CAAC,KAAc;IACpC,OAAO,KAAK,CAAC,KAAK,CAAC,CAAC,IAAI,KAAK,QAAQ,CAAA;AACvC,CAAC;AAED,gFAAgF;AAChF,SAAS,SAAS,CAAC,KAAc;IAC/B,IAAI,GAAG,GAAG,KAAK,CAAC,KAAK,CAAC,CAAA;IACtB,OAAO,CAAC,GAAG,CAAC,IAAI,KAAK,UAAU,IAAI,GAAG,CAAC,IAAI,KAAK,SAAS,IAAI,GAAG,CAAC,IAAI,KAAK,UAAU,CAAC,IAAI,GAAG,CAAC,SAAS,EAAE,CAAC;QACvG,GAAG,GAAG,GAAG,CAAC,SAAS,CAAC,IAAI,CAAC,GAAG,CAAA;IAC9B,CAAC;IACD,OAAO,GAAG,CAAA;AACZ,CAAC;AAED,qGAAqG;AACrG,SAAS,gBAAgB,CAAC,KAAc,EAAE,KAAc;IACtD,OAAO,cAAc,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK,CAAA;AACjF,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,yBAAyB,CACvC,IAAO,EACP,GAAgB,EAChB,IAAkB;IAElB,MAAM,MAAM,GAAG,GAAG,CAAC,MAAM,IAAI,EAAE,CAAA;IAC/B,MAAM,KAAK,GAA4B,EAAE,EAAE,EAAE,GAAG,CAAC,EAAE,EAAE,CAAA;IACrD,MAAM,OAAO,GAAG,iBAAiB,CAAC,IAAI,CAAC,CAAA;IACvC,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,KAAK,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;YAC1D,MAAM,KAAK,GAAG,gBAAgB,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,CAAA;YACrD,IAAI,KAAK,KAAK,SAAS;gBAAE,KAAK,CAAC,MAAM,CAAC,GAAG,KAAK,CAAA;QAChD,CAAC;QACD,MAAM,IAAI,GAAG,iBAAiB,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;QAC/C,IAAI,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,MAAM,GAAG,CAAC;YAAE,KAAK,CAAC,IAAI,GAAG,IAAI,CAAA;QACnD,OAAO,KAAyB,CAAA;IAClC,CAAC;IACD,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,IAAI,EAAE,CAAA;IAChC,KAAK,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;QAC1D,MAAM,KAAK,GAAG,MAAM,CAAC,MAAM,CAAC,CAAA;QAC5B,IAAI,kBAAkB,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC;YAAE,SAAQ;QACvD,2FAA2F;QAC3F,KAAK,CAAC,MAAM,CAAC,GAAG,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,gBAAgB,CAAC,KAAK,EAAE,KAAK,CAAC,CAAA;IAC5G,CAAC;IACD,MAAM,IAAI,GAAG,iBAAiB,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC/C,MAAM,QAAQ,GAAG,iBAAiB,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACnD,MAAM,SAAS,GAA4B,EAAE,CAAA;IAC7C,KAAK,MAAM,GAAG,IAAI,IAAI,GAAG,CAAC,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,GAAG,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC;QAC5E,IAAI,kBAAkB,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,QAAQ,CAAC,GAAG,CAAC,CAAC;YAAE,SAAQ;QAC1D,SAAS,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;IAC7D,CAAC;IACD,IAAI,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,MAAM,GAAG,CAAC;QAAE,KAAK,CAAC,SAAS,GAAG,SAAS,CAAA;IAClE,OAAO,KAAyB,CAAA;AAClC,CAAC;AAED,4GAA4G;AAC5G,SAAS,aAAa,CAAC,KAAc;IACnC,QAAQ,SAAS,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE,CAAC;QAC9B,KAAK,QAAQ;YACX,OAAO,MAAM,CAAA;QACf,KAAK,SAAS;YACZ,OAAO,SAAS,CAAA;QAClB,KAAK,QAAQ;YACX,OAAO,QAAQ,CAAA;QACjB,KAAK,MAAM;YACT,OAAO,QAAQ,CAAA;QACjB;YACE,OAAO,SAAS,CAAA;IACpB,CAAC;AACH,CAAC;AAED,SAAS,QAAQ,CAAC,IAAY;IAC5B,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,oBAAoB,EAAE,OAAO,CAAC,CAAC,OAAO,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAA;IACjF,OAAO,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;AACzD,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,aAAa,CAAC,IAAmB,EAAE,UAAmD,EAAE;IACtG,MAAM,MAAM,GAA8B,EAAE,CAAA;IAC5C,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;QACxD,MAAM,IAAI,GAAG,aAAa,CAAC,KAAK,CAAC,CAAA;QACjC,IAAI,CAAC,IAAI;YAAE,SAAQ;QACnB,MAAM,KAAK,GAA4B,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,QAAQ,CAAC,IAAI,CAAC,EAAE,EAAE,CAAA;QACtF,IAAI,IAAI,KAAK,QAAQ,EAAE,CAAC;YACtB,KAAK,CAAC,OAAO,GAAG,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAA;QAChI,CAAC;QACD,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;IACpB,CAAC;IACD,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,IAAI,MAAM,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,KAAK,MAAM,CAAC,EAAE,IAAI,CAAA;IAC5F,OAAO,mBAAmB,CAAC,KAAK,CAAC;QAC/B,EAAE,EAAE,IAAI,CAAC,IAAI;QACb,gFAAgF;QAChF,IAAI,EAAE,UAAU;QAChB,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACrC,IAAI,EAAE,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,IAAI,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE;QACvD,MAAM;KACP,CAAC,CAAA;AACJ,CAAC","sourcesContent":["import { collectionDefSchema, semanticValueEqual } from '@elytracms/core/cms-core'\nimport type { CmsDocument, CollectionDef, FieldDef } from '@elytracms/core/cms-core'\nimport type { AnyRecordKind, RecordEntryOf, RecordWriteOf } from '@elytracms/core/persistence'\nimport { metaBagProjection } from '../rail-view-source'\n\n/**\n * EC-616 — the projections between a record kind and the portable list/edit surfaces\n * (AD-19): a record rail edits a record as a LENS document (`{ collection, id, values }`)\n * over a `CollectionDef`. Pure — no React — so the mapping that decides what a save\n * writes is unit-testable on its own (it is the one place the write is derived from the\n * edited lens; a slip here silently corrupts a record).\n *\n * - **Read** ({@link recordToLensDocument}): the stored `meta` bag spread FIRST, then\n * the kind's columns — a canonical column always wins over a same-named meta key\n * (RESIDUE-3c: spread last, a stored `meta.to` displayed as the redirect's target and a\n * re-save wrote it into the canonical `to`).\n * - **Write** ({@link lensDocumentToRecordWrite}): every lens value keyed by a column is\n * that column; every other key round-trips through the open `meta` bag (EC-311 Tier 2\n * — a mount's additive field). A column the document does not carry is left out, so the\n * stored value is kept (EC-577: a create default applies once, in the adapter's\n * transaction — never restated here). A REQUIRED text column is always sent (`''` when\n * blank): the editor owns it, and an empty one is refused visibly by the write's\n * validation rather than kept silently. `meta` is omitted when no additive value exists,\n * so a rail with no additive fields never touches the bag.\n */\n\n/** The kind's columns (its declared fields; `meta` is the bag, not a column). */\nexport function recordColumnNames(kind: AnyRecordKind): ReadonlySet<string> {\n return new Set(Object.keys(kind.fields))\n}\n\n/** A stored record as the lens document the portable surfaces edit. */\nexport function recordToLensDocument<K extends AnyRecordKind>(kind: K, entry: RecordEntryOf<K>): CmsDocument {\n const record = entry as Record<string, unknown>\n const columns: Record<string, unknown> = {}\n for (const column of recordColumnNames(kind)) {\n if (record[column] !== undefined) columns[column] = record[column]\n }\n return {\n collection: kind.kind,\n id: String(record.id),\n values: { ...((record.meta as Record<string, unknown> | undefined) ?? {}), ...columns },\n }\n}\n\n/**\n * A column's zod definition, read through `_zod.def` rather than `instanceof`: a kind is\n * declared in whichever package owns it (an addon), whose `zod` may be a different copy\n * than this one — `instanceof` across copies is silently false.\n */\ninterface ZodDefView {\n type: string\n innerType?: { _zod: { def: ZodDefView } }\n entries?: Record<string, unknown>\n}\nfunction defOf(field: unknown): ZodDefView {\n return (field as { _zod: { def: ZodDefView } })._zod.def\n}\n\n/** Is this column a required text column (no default, not optional)? */\nfunction isRequiredText(field: unknown): boolean {\n return defOf(field).type === 'string'\n}\n\n/** The column's own definition, below any optional/default/nullable wrapper. */\nfunction unwrapped(field: unknown): ZodDefView {\n let def = defOf(field)\n while ((def.type === 'optional' || def.type === 'default' || def.type === 'nullable') && def.innerType) {\n def = def.innerType._zod.def\n }\n return def\n}\n\n/** A lens value as the write sends it for one column (a required text column is always a string). */\nfunction columnWriteValue(field: unknown, value: unknown): unknown {\n return isRequiredText(field) ? (typeof value === 'string' ? value : '') : value\n}\n\n/**\n * An edited lens document as the record write (see the module header).\n *\n * **With `base`** — the lens document of the record as the editor OPENED it — the write\n * carries only what the editor changed (EC-616, the multi-session baseline): a column whose\n * value differs from the base, and the changed `meta` keys as a per-key `metaPatch` (a key the\n * editor removed is `null`). Every other column is ABSENT, so the EC-577 rule keeps the stored\n * value — a column another writer changed after the editor opened the record survives the\n * editor's save. With nothing changed the write is just the id (a no-op edit).\n *\n * **Without `base`** (a record the editor is creating) the write is whole, as it always was.\n */\nexport function lensDocumentToRecordWrite<K extends AnyRecordKind>(\n kind: K,\n doc: CmsDocument,\n base?: CmsDocument,\n): RecordWriteOf<K> {\n const values = doc.values ?? {}\n const write: Record<string, unknown> = { id: doc.id }\n const columns = recordColumnNames(kind)\n if (!base) {\n for (const [column, field] of Object.entries(kind.fields)) {\n const value = columnWriteValue(field, values[column])\n if (value !== undefined) write[column] = value\n }\n const meta = metaBagProjection(values, columns)\n if (Object.keys(meta).length > 0) write.meta = meta\n return write as RecordWriteOf<K>\n }\n const before = base.values ?? {}\n for (const [column, field] of Object.entries(kind.fields)) {\n const value = values[column]\n if (semanticValueEqual(value, before[column])) continue\n // A cleared optional column is `null` on the wire (EC-577: clear), never a silent absence.\n write[column] = value === undefined ? (isRequiredText(field) ? '' : null) : columnWriteValue(field, value)\n }\n const meta = metaBagProjection(values, columns)\n const baseMeta = metaBagProjection(before, columns)\n const metaPatch: Record<string, unknown> = {}\n for (const key of new Set([...Object.keys(meta), ...Object.keys(baseMeta)])) {\n if (semanticValueEqual(meta[key], baseMeta[key])) continue\n metaPatch[key] = meta[key] === undefined ? null : meta[key]\n }\n if (Object.keys(metaPatch).length > 0) write.metaPatch = metaPatch\n return write as RecordWriteOf<K>\n}\n\n/** The lens field type a column edits as (unwrapping optional/default); `undefined` = not lens-editable. */\nfunction lensFieldType(field: unknown): FieldDef['type'] | undefined {\n switch (unwrapped(field).type) {\n case 'string':\n return 'text'\n case 'boolean':\n return 'boolean'\n case 'number':\n return 'number'\n case 'enum':\n return 'select'\n default:\n return undefined\n }\n}\n\nfunction humanize(name: string): string {\n const spaced = name.replace(/([a-z0-9])([A-Z])/g, '$1 $2').replace(/[_-]+/g, ' ')\n return spaced.charAt(0).toUpperCase() + spaced.slice(1)\n}\n\n/**\n * The DEFAULT lens of a kind: one field per column whose type the portable form edits\n * (text, boolean, number, select — derived from the column's zod type), labelled from its\n * name. A column of another shape (an object, a list) is not in the lens: the rail never\n * writes it, so the stored value is kept on every save (EC-577). A mount refines the lens\n * by name through `nav.rail(kind, { fields })` (EC-311 — the same override mechanism the\n * built-in rails use), or passes its own `schema`.\n */\nexport function recordLensDef(kind: AnyRecordKind, options: { label?: string; titleField?: string } = {}): CollectionDef {\n const fields: Record<string, unknown>[] = []\n for (const [name, field] of Object.entries(kind.fields)) {\n const type = lensFieldType(field)\n if (!type) continue\n const entry: Record<string, unknown> = { name, type, form: { label: humanize(name) } }\n if (type === 'select') {\n entry.options = Object.values(unwrapped(field).entries ?? {}).map((value) => ({ value: String(value), label: String(value) }))\n }\n fields.push(entry)\n }\n const titleField = options.titleField ?? fields.find((field) => field.type === 'text')?.name\n return collectionDefSchema.parse({\n id: kind.kind,\n // A record kind is not a CMS `kind`; 'document' just satisfies the lens schema.\n kind: 'document',\n ...(titleField ? { titleField } : {}),\n form: { label: options.label ?? humanize(kind.module) },\n fields,\n })\n}\n"]}
1
+ {"version":3,"file":"record-lens.js","sourceRoot":"","sources":["../../../src/components/records/record-lens.ts"],"names":[],"mappings":"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","sourcesContent":["import { collectionDefSchema, semanticValueEqual } from '@elytracms/core/cms-core'\nimport type { CmsDocument, CollectionDef, FieldDef } from '@elytracms/core/cms-core'\nimport { metaPatchBetween } from '@elytracms/core/persistence'\nimport type { AnyRecordKind, RecordEntryOf, RecordWriteOf } from '@elytracms/core/persistence'\nimport { metaBagProjection } from '../rail-view-source'\n\n/**\n * EC-616 — the projections between a record kind and the portable list/edit surfaces\n * (AD-19): a record rail edits a record as a LENS document (`{ collection, id, values }`)\n * over a `CollectionDef`. Pure — no React — so the mapping that decides what a save\n * writes is unit-testable on its own (it is the one place the write is derived from the\n * edited lens; a slip here silently corrupts a record).\n *\n * - **Read** ({@link recordToLensDocument}): the stored `meta` bag spread FIRST, then\n * the kind's columns — a canonical column always wins over a same-named meta key\n * (RESIDUE-3c: spread last, a stored `meta.to` displayed as the redirect's target and a\n * re-save wrote it into the canonical `to`).\n * - **Write** ({@link lensDocumentToRecordWrite}): every lens value keyed by a column is\n * that column; every other key round-trips through the open `meta` bag (EC-311 Tier 2\n * — a mount's additive field). A column the document does not carry is left out, so the\n * stored value is kept (EC-577: a create default applies once, in the adapter's\n * transaction — never restated here). A REQUIRED text column is always sent (`''` when\n * blank): the editor owns it, and an empty one is refused visibly by the write's\n * validation rather than kept silently. `meta` is omitted when no additive value exists,\n * so a rail with no additive fields never touches the bag.\n */\n\n/** The kind's columns (its declared fields; `meta` is the bag, not a column). */\nexport function recordColumnNames(kind: AnyRecordKind): ReadonlySet<string> {\n return new Set(Object.keys(kind.fields))\n}\n\n/** A stored record as the lens document the portable surfaces edit. */\nexport function recordToLensDocument<K extends AnyRecordKind>(kind: K, entry: RecordEntryOf<K>): CmsDocument {\n const record = entry as Record<string, unknown>\n const columns: Record<string, unknown> = {}\n for (const column of recordColumnNames(kind)) {\n if (record[column] !== undefined) columns[column] = record[column]\n }\n return {\n collection: kind.kind,\n id: String(record.id),\n values: { ...((record.meta as Record<string, unknown> | undefined) ?? {}), ...columns },\n }\n}\n\n/**\n * A column's zod definition, read through `_zod.def` rather than `instanceof`: a kind is\n * declared in whichever package owns it (an addon), whose `zod` may be a different copy\n * than this one — `instanceof` across copies is silently false.\n */\ninterface ZodDefView {\n type: string\n innerType?: { _zod: { def: ZodDefView } }\n entries?: Record<string, unknown>\n}\nfunction defOf(field: unknown): ZodDefView {\n return (field as { _zod: { def: ZodDefView } })._zod.def\n}\n\n/** Is this column a required text column (no default, not optional)? */\nfunction isRequiredText(field: unknown): boolean {\n return defOf(field).type === 'string'\n}\n\n/** The column's own definition, below any optional/default/nullable wrapper. */\nfunction unwrapped(field: unknown): ZodDefView {\n let def = defOf(field)\n while ((def.type === 'optional' || def.type === 'default' || def.type === 'nullable') && def.innerType) {\n def = def.innerType._zod.def\n }\n return def\n}\n\n/** A lens value as the write sends it for one column (a required text column is always a string). */\nfunction columnWriteValue(field: unknown, value: unknown): unknown {\n return isRequiredText(field) ? (typeof value === 'string' ? value : '') : value\n}\n\n/**\n * An edited lens document as the record write (see the module header).\n *\n * **With `base`** — the lens document of the record as the editor OPENED it — the write\n * carries only what the editor changed (EC-616, the multi-session baseline): a column whose\n * value differs from the base, and the changed `meta` keys as a per-key `metaPatch` (a key the\n * editor removed is `null`). Every other column is ABSENT, so the EC-577 rule keeps the stored\n * value — a column another writer changed after the editor opened the record survives the\n * editor's save. With nothing changed the write is just the id (a no-op edit).\n *\n * **Without `base`** (a record the editor is creating) the write is whole, as it always was.\n */\nexport function lensDocumentToRecordWrite<K extends AnyRecordKind>(\n kind: K,\n doc: CmsDocument,\n base?: CmsDocument,\n): RecordWriteOf<K> {\n const values = doc.values ?? {}\n const write: Record<string, unknown> = { id: doc.id }\n const columns = recordColumnNames(kind)\n if (!base) {\n for (const [column, field] of Object.entries(kind.fields)) {\n const value = columnWriteValue(field, values[column])\n if (value !== undefined) write[column] = value\n }\n const meta = metaBagProjection(values, columns)\n if (Object.keys(meta).length > 0) write.meta = meta\n return write as RecordWriteOf<K>\n }\n const before = base.values ?? {}\n for (const [column, field] of Object.entries(kind.fields)) {\n const value = values[column]\n if (semanticValueEqual(value, before[column])) continue\n // A cleared optional column is `null` on the wire (EC-577: clear), never a silent absence.\n write[column] = value === undefined ? (isRequiredText(field) ? '' : null) : columnWriteValue(field, value)\n }\n // The one per-key diff (core `metaPatchBetween`), shared with the asset editors (EC-634).\n const metaPatch = metaPatchBetween(metaBagProjection(before, columns), metaBagProjection(values, columns))\n if (Object.keys(metaPatch).length > 0) write.metaPatch = metaPatch\n return write as RecordWriteOf<K>\n}\n\n/** The lens field type a column edits as (unwrapping optional/default); `undefined` = not lens-editable. */\nfunction lensFieldType(field: unknown): FieldDef['type'] | undefined {\n switch (unwrapped(field).type) {\n case 'string':\n return 'text'\n case 'boolean':\n return 'boolean'\n case 'number':\n return 'number'\n case 'enum':\n return 'select'\n default:\n return undefined\n }\n}\n\nfunction humanize(name: string): string {\n const spaced = name.replace(/([a-z0-9])([A-Z])/g, '$1 $2').replace(/[_-]+/g, ' ')\n return spaced.charAt(0).toUpperCase() + spaced.slice(1)\n}\n\n/**\n * The DEFAULT lens of a kind: one field per column whose type the portable form edits\n * (text, boolean, number, select — derived from the column's zod type), labelled from its\n * name. A column of another shape (an object, a list) is not in the lens: the rail never\n * writes it, so the stored value is kept on every save (EC-577). A mount refines the lens\n * by name through `nav.rail(kind, { fields })` (EC-311 — the same override mechanism the\n * built-in rails use), or passes its own `schema`.\n */\nexport function recordLensDef(kind: AnyRecordKind, options: { label?: string; titleField?: string } = {}): CollectionDef {\n const fields: Record<string, unknown>[] = []\n for (const [name, field] of Object.entries(kind.fields)) {\n const type = lensFieldType(field)\n if (!type) continue\n const entry: Record<string, unknown> = { name, type, form: { label: humanize(name) } }\n if (type === 'select') {\n entry.options = Object.values(unwrapped(field).entries ?? {}).map((value) => ({ value: String(value), label: String(value) }))\n }\n fields.push(entry)\n }\n const titleField = options.titleField ?? fields.find((field) => field.type === 'text')?.name\n return collectionDefSchema.parse({\n id: kind.kind,\n // A record kind is not a CMS `kind`; 'document' just satisfies the lens schema.\n kind: 'document',\n ...(titleField ? { titleField } : {}),\n form: { label: options.label ?? humanize(kind.module) },\n fields,\n })\n}\n"]}
@@ -1,4 +1,4 @@
1
- import type { FieldDef } from '@elytracms/core/cms-core';
1
+ import type { FieldDef, Locale } from '@elytracms/core/cms-core';
2
2
  import type { AssetRecord, AssetRepository, AssetWriteInput, BlobStorageAdapter } from '@elytracms/core/persistence';
3
3
  import type { ImageDimensions } from './image-dimensions.js';
4
4
  import type { GeneratedVariants } from './thumbnail.js';
@@ -35,6 +35,15 @@ export interface UploadDeps {
35
35
  * written (a field without `source` is never filled).
36
36
  */
37
37
  assetFields?: readonly FieldDef[];
38
+ /**
39
+ * The project's locales (EC-632): a `localized: true` asset field's embedded value goes
40
+ * to the locale buckets — each locale its `xml:lang` alternative, the default the
41
+ * `x-default` reading. Absent → every field is written to the shared `meta`.
42
+ */
43
+ locales?: {
44
+ readonly default: Locale;
45
+ readonly locales: readonly Locale[];
46
+ };
38
47
  /** Client-direct transport; only used when `blobs.createUploadUrl` exists. */
39
48
  transport?: UploadTransport;
40
49
  /** Injectable dimension detection; defaults to `createImageBitmap`-based. */
@@ -79,16 +88,25 @@ export type UploadResult = {
79
88
  * previous `state` to retry (the attempt counter carries over).
80
89
  */
81
90
  export declare function runUpload(deps: UploadDeps, request: UploadRequest, previous?: UploadState): Promise<UploadResult>;
91
+ /** What a file's embedded facts write (EC-568 → EC-632): shared `meta` keys, and per-locale buckets of the localized fields. */
92
+ export interface EmbeddedAssetWrite {
93
+ meta: Record<string, unknown>;
94
+ localizedMeta: Record<string, Record<string, unknown>>;
95
+ }
82
96
  /**
83
97
  * The write of a file REPLACE (EC-568, the owner's ruling: "override the values with
84
98
  * available metadata, or keep the existing if no metadata is available"):
85
99
  * - the bytes' columns are SET — and a derived column the new file does not have is
86
100
  * CLEARED (`null`), so an old thumb, lqip or dimension never outlives its file;
87
- * - `meta` is the stored bag (a legacy `alt` column lifted in, EC-587) with each key the
88
- * new file brings overridden; written only when the file brings something, and then
89
- * the legacy column is cleared, as every `meta` write does;
90
- * - the id, the focal point, the title and every other column are absent → kept.
101
+ * - each `meta` key the new file brings is a per-key `metaPatch` (EC-634), applied to the
102
+ * bag as STORED inside the write — never a copy read before the upload, so an editor's
103
+ * metadata save that lands while the bytes travel is kept. Sent only when the file
104
+ * brings something; the write rule then lifts a legacy `alt` column into the bag and
105
+ * clears it (EC-587), as for every patch;
106
+ * - the id, the focal point, the title and every other column are absent → kept;
107
+ * - `mustExist`: the write only updates the stored asset — gone (deleted meanwhile), it is
108
+ * refused inside the write and nothing is created.
91
109
  */
92
- export declare function replaceWrite(stored: AssetRecord, derived: Pick<AssetRecord, 'filename' | 'contentType' | 'byteSize' | 'storageKey'> & {
110
+ export declare function replaceWrite(assetId: string, derived: Pick<AssetRecord, 'filename' | 'contentType' | 'byteSize' | 'storageKey'> & {
93
111
  url: string;
94
- }, dimensions: ImageDimensions | undefined, variants: GeneratedVariants, embedded: Record<string, unknown>): AssetWriteInput;
112
+ }, dimensions: ImageDimensions | undefined, variants: GeneratedVariants, embedded: EmbeddedAssetWrite): AssetWriteInput;
@@ -1,5 +1,4 @@
1
- import { embeddedMetaValues, readEmbeddedMetadata } from '@elytracms/core/cms-core';
2
- import { liftLegacyAlt } from '@elytracms/core/persistence';
1
+ import { embeddedAssetMeta, readEmbeddedMetadata } from '@elytracms/core/cms-core';
3
2
  import { detectImageDimensions } from './image-dimensions.js';
4
3
  import { generateVariants } from './thumbnail.js';
5
4
  import { initialUploadState, uploadReducer } from './upload-state.js';
@@ -123,7 +122,7 @@ export async function runUpload(deps, request, previous) {
123
122
  // --- confirm: metadata record linked to the stored blob --------------------
124
123
  try {
125
124
  // EC-568: the ONE embedded-metadata reader, for a first upload and a replace alike.
126
- const embedded = await embeddedAssetMeta(file, deps.assetFields);
125
+ const embedded = await embeddedMetaOf(file, deps.assetFields, deps.locales);
127
126
  const detect = deps.detectDimensions ?? defaultDetectDimensions;
128
127
  const dimensions = await detect(file);
129
128
  // EC-400 + EC-420: produce the thumb + lqip previews from the bytes already in
@@ -147,10 +146,10 @@ export async function runUpload(deps, request, previous) {
147
146
  };
148
147
  let write;
149
148
  if (request.replace) {
150
- const stored = await assets.findAsset(projectId, request.replace.assetId);
151
- if (!stored)
152
- throw new Error(`No asset “${request.replace.assetId}” is stored to replace the file of.`);
153
- write = replaceWrite(stored, derived, dimensions, variants, embedded);
149
+ // A replace edits an asset, never creates one — decided by the write itself
150
+ // (`mustExist`), against the row as it is when the write lands: a delete by another
151
+ // session in the meantime makes the write fail here, visibly, and creates nothing.
152
+ write = replaceWrite(request.replace.assetId, derived, dimensions, variants, embedded);
154
153
  }
155
154
  else {
156
155
  write = {
@@ -158,7 +157,8 @@ export async function runUpload(deps, request, previous) {
158
157
  ...(dimensions ? { width: dimensions.width, height: dimensions.height } : {}),
159
158
  ...(variants.thumb ? { thumb: variants.thumb } : {}),
160
159
  ...(variants.lqip ? { lqip: variants.lqip } : {}),
161
- ...(Object.keys(embedded).length > 0 ? { meta: embedded } : {}),
160
+ ...(Object.keys(embedded.meta).length > 0 ? { meta: embedded.meta } : {}),
161
+ ...(Object.keys(embedded.localizedMeta).length > 0 ? { localizedMeta: embedded.localizedMeta } : {}),
162
162
  };
163
163
  }
164
164
  const asset = await assets.upsertAsset(projectId, write);
@@ -175,14 +175,15 @@ export async function runUpload(deps, request, previous) {
175
175
  * bytes, mapped through the asset collection's `source` declarations. A file that cannot
176
176
  * be read yields `{}` — metadata never fails an upload.
177
177
  */
178
- async function embeddedAssetMeta(file, fields) {
178
+ async function embeddedMetaOf(file, fields, locales) {
179
+ const none = { meta: {}, localizedMeta: {} };
179
180
  if (!fields || !fields.some((field) => field.source !== undefined))
180
- return {};
181
+ return none;
181
182
  try {
182
- return embeddedMetaValues(fields, readEmbeddedMetadata(new Uint8Array(await file.arrayBuffer())));
183
+ return embeddedAssetMeta(fields, readEmbeddedMetadata(new Uint8Array(await file.arrayBuffer())), locales);
183
184
  }
184
185
  catch {
185
- return {};
186
+ return none;
186
187
  }
187
188
  }
188
189
  /**
@@ -190,22 +191,30 @@ async function embeddedAssetMeta(file, fields) {
190
191
  * available metadata, or keep the existing if no metadata is available"):
191
192
  * - the bytes' columns are SET — and a derived column the new file does not have is
192
193
  * CLEARED (`null`), so an old thumb, lqip or dimension never outlives its file;
193
- * - `meta` is the stored bag (a legacy `alt` column lifted in, EC-587) with each key the
194
- * new file brings overridden; written only when the file brings something, and then
195
- * the legacy column is cleared, as every `meta` write does;
196
- * - the id, the focal point, the title and every other column are absent → kept.
194
+ * - each `meta` key the new file brings is a per-key `metaPatch` (EC-634), applied to the
195
+ * bag as STORED inside the write — never a copy read before the upload, so an editor's
196
+ * metadata save that lands while the bytes travel is kept. Sent only when the file
197
+ * brings something; the write rule then lifts a legacy `alt` column into the bag and
198
+ * clears it (EC-587), as for every patch;
199
+ * - the id, the focal point, the title and every other column are absent → kept;
200
+ * - `mustExist`: the write only updates the stored asset — gone (deleted meanwhile), it is
201
+ * refused inside the write and nothing is created.
197
202
  */
198
- export function replaceWrite(stored, derived, dimensions, variants, embedded) {
199
- const brings = Object.keys(embedded).length > 0;
200
- const base = liftLegacyAlt(stored)?.meta ?? stored.meta ?? {};
203
+ export function replaceWrite(assetId, derived, dimensions, variants, embedded) {
204
+ const brings = Object.keys(embedded.meta).length > 0;
205
+ const bringsLocales = Object.keys(embedded.localizedMeta).length > 0;
201
206
  return {
202
- id: stored.id,
207
+ id: assetId,
208
+ // EC-634 follow-on: an update of a stored asset, never a create (a concurrent delete wins).
209
+ mustExist: true,
203
210
  ...derived,
204
211
  width: dimensions?.width ?? null,
205
212
  height: dimensions?.height ?? null,
206
213
  thumb: variants.thumb ?? null,
207
214
  lqip: variants.lqip ?? null,
208
- ...(brings ? { meta: { ...base, ...embedded }, alt: null } : {}),
215
+ ...(brings ? { metaPatch: embedded.meta } : {}),
216
+ // EC-632: a localized field's embedded values patch their locale buckets per key.
217
+ ...(bringsLocales ? { localizedMetaPatch: embedded.localizedMeta } : {}),
209
218
  };
210
219
  }
211
220
  //# sourceMappingURL=upload.js.map