@elytracms/studio 0.0.43 → 0.0.44

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 (100) hide show
  1. package/dist/components/app-views/studio-config.d.ts +12 -0
  2. package/dist/components/app-views/studio-config.js.map +1 -1
  3. package/dist/components/assets/assets-collection-view-source.d.ts +13 -1
  4. package/dist/components/assets/assets-collection-view-source.js +42 -51
  5. package/dist/components/assets/assets-collection-view-source.js.map +1 -1
  6. package/dist/components/cms/cms-store.js +9 -11
  7. package/dist/components/cms/cms-store.js.map +1 -1
  8. package/dist/components/cms/detail-editor.js +110 -338
  9. package/dist/components/cms/detail-editor.js.map +1 -1
  10. package/dist/components/cms/document-staging.d.ts +8 -3
  11. package/dist/components/cms/document-staging.js +81 -21
  12. package/dist/components/cms/document-staging.js.map +1 -1
  13. package/dist/components/cms/use-compare-selection.d.ts +27 -0
  14. package/dist/components/cms/use-compare-selection.js +29 -0
  15. package/dist/components/cms/use-compare-selection.js.map +1 -0
  16. package/dist/components/cms/use-discard-controller.d.ts +44 -0
  17. package/dist/components/cms/use-discard-controller.js +51 -0
  18. package/dist/components/cms/use-discard-controller.js.map +1 -0
  19. package/dist/components/cms/use-embed-inspector-host.d.ts +12 -0
  20. package/dist/components/cms/use-embed-inspector-host.js +17 -0
  21. package/dist/components/cms/use-embed-inspector-host.js.map +1 -0
  22. package/dist/components/cms/use-field-focus.d.ts +38 -0
  23. package/dist/components/cms/use-field-focus.js +70 -0
  24. package/dist/components/cms/use-field-focus.js.map +1 -0
  25. package/dist/components/cms/use-live-url.d.ts +41 -0
  26. package/dist/components/cms/use-live-url.js +55 -0
  27. package/dist/components/cms/use-live-url.js.map +1 -0
  28. package/dist/components/cms/use-publish-controller.d.ts +49 -0
  29. package/dist/components/cms/use-publish-controller.js +102 -0
  30. package/dist/components/cms/use-publish-controller.js.map +1 -0
  31. package/dist/components/cms/use-published-perspective.d.ts +67 -0
  32. package/dist/components/cms/use-published-perspective.js +67 -0
  33. package/dist/components/cms/use-published-perspective.js.map +1 -0
  34. package/dist/components/cms/use-restore-version.d.ts +57 -0
  35. package/dist/components/cms/use-restore-version.js +66 -0
  36. package/dist/components/cms/use-restore-version.js.map +1 -0
  37. package/dist/components/cms/use-save-mode.d.ts +35 -0
  38. package/dist/components/cms/use-save-mode.js +54 -0
  39. package/dist/components/cms/use-save-mode.js.map +1 -0
  40. package/dist/components/cms/version-compare.js +13 -16
  41. package/dist/components/cms/version-compare.js.map +1 -1
  42. package/dist/components/cms/versions-pane.js +21 -19
  43. package/dist/components/cms/versions-pane.js.map +1 -1
  44. package/dist/components/components/transfer-dialog.d.ts +11 -6
  45. package/dist/components/components/transfer-dialog.js +98 -25
  46. package/dist/components/components/transfer-dialog.js.map +1 -1
  47. package/dist/components/components/transfer.d.ts +74 -8
  48. package/dist/components/components/transfer.js +87 -2
  49. package/dist/components/components/transfer.js.map +1 -1
  50. package/dist/components/rail-store-status.d.ts +7 -5
  51. package/dist/components/rail-store-status.js +12 -9
  52. package/dist/components/rail-store-status.js.map +1 -1
  53. package/dist/components/rail-view-source.d.ts +111 -0
  54. package/dist/components/rail-view-source.js +139 -0
  55. package/dist/components/rail-view-source.js.map +1 -0
  56. package/dist/components/redirects/redirects-collection-view-source.d.ts +13 -1
  57. package/dist/components/redirects/redirects-collection-view-source.js +42 -70
  58. package/dist/components/redirects/redirects-collection-view-source.js.map +1 -1
  59. package/dist/components/section-state.d.ts +12 -3
  60. package/dist/components/section-state.js +8 -4
  61. package/dist/components/section-state.js.map +1 -1
  62. package/dist/components/users/users-collection-view-source.d.ts +13 -1
  63. package/dist/components/users/users-collection-view-source.js +60 -79
  64. package/dist/components/users/users-collection-view-source.js.map +1 -1
  65. package/dist/lib/ai/anthropic-provider.d.ts +29 -0
  66. package/dist/lib/ai/anthropic-provider.js +59 -0
  67. package/dist/lib/ai/anthropic-provider.js.map +1 -0
  68. package/dist/lib/ai/apply-content.d.ts +9 -0
  69. package/dist/lib/ai/apply-content.js +170 -0
  70. package/dist/lib/ai/apply-content.js.map +1 -0
  71. package/dist/lib/ai/config.d.ts +58 -0
  72. package/dist/lib/ai/config.js +134 -0
  73. package/dist/lib/ai/config.js.map +1 -0
  74. package/dist/lib/ai/fake-provider.d.ts +34 -0
  75. package/dist/lib/ai/fake-provider.js +31 -0
  76. package/dist/lib/ai/fake-provider.js.map +1 -0
  77. package/dist/lib/ai/index.d.ts +22 -0
  78. package/dist/lib/ai/index.js +23 -0
  79. package/dist/lib/ai/index.js.map +1 -0
  80. package/dist/lib/ai/knowledge.d.ts +39 -0
  81. package/dist/lib/ai/knowledge.js +60 -0
  82. package/dist/lib/ai/knowledge.js.map +1 -0
  83. package/dist/lib/ai/provenance.d.ts +20 -0
  84. package/dist/lib/ai/provenance.js +15 -0
  85. package/dist/lib/ai/provenance.js.map +1 -0
  86. package/dist/lib/ai/route.d.ts +24 -0
  87. package/dist/lib/ai/route.js +90 -0
  88. package/dist/lib/ai/route.js.map +1 -0
  89. package/dist/lib/ai/shape.d.ts +35 -0
  90. package/dist/lib/ai/shape.js +113 -0
  91. package/dist/lib/ai/shape.js.map +1 -0
  92. package/dist/lib/ai/types.d.ts +227 -0
  93. package/dist/lib/ai/types.js +2 -0
  94. package/dist/lib/ai/types.js.map +1 -0
  95. package/dist/lib/i18n/messages.en.js +4 -2
  96. package/dist/lib/i18n/messages.en.js.map +1 -1
  97. package/dist/lib/workspace/index.d.ts +9 -0
  98. package/dist/lib/workspace/index.js +11 -0
  99. package/dist/lib/workspace/index.js.map +1 -1
  100. package/package.json +8 -2
@@ -1,6 +1,7 @@
1
1
  import type { ReactNode } from 'react';
2
2
  import type { FieldDef } from '@elytracms/core/cms-core';
3
3
  import type { StudioI18nConfig } from '@elytracms/studio/lib/i18n';
4
+ import type { StudioAiConfig } from '@elytracms/studio/lib/ai';
4
5
  import type { SidebarNode } from './module';
5
6
  /**
6
7
  * EC-236 — the studio customization config: the single, studio-only home for
@@ -47,6 +48,17 @@ export interface StudioConfig {
47
48
  * `LocalizedString` on the field/collection def.
48
49
  */
49
50
  i18n?: StudioI18nConfig;
51
+ /**
52
+ * AI platform (EC-331): provider, project knowledge, and soft limits. The block's
53
+ * PRESENCE enables the AI feature; its ABSENCE means the feature is fully absent —
54
+ * no AI UI and no AI dependencies on the critical path (the runtime lives behind a
55
+ * lazy boundary in `@elytracms/studio/lib/ai`, only reached when configured).
56
+ * `enabled: false` is a temporary kill-switch. Studio-only live code (holds the
57
+ * provider adapter + knowledge functions) — never reaches the delivery bundle.
58
+ * Validated at load by `resolveAiConfig` (capability gate: tool calling + structured
59
+ * output required, fail-loud).
60
+ */
61
+ ai?: StudioAiConfig;
50
62
  }
51
63
  /** The props a custom field renderer receives — identical to the built-in control. */
52
64
  export interface CustomFieldRendererProps {
@@ -1 +1 @@
1
- {"version":3,"file":"studio-config.js","sourceRoot":"","sources":["../../../src/components/app-views/studio-config.ts"],"names":[],"mappings":"AA8DA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,MAAM,UAAU,YAAY,CAAC,MAAoB;IAC/C,OAAO,MAAM,CAAA;AACf,CAAC","sourcesContent":["import type { ReactNode } from 'react'\nimport type { FieldDef } from '@elytracms/core/cms-core'\nimport type { StudioI18nConfig } from '@elytracms/studio/lib/i18n'\nimport type { SidebarNode } from './module'\n\n/**\n * EC-236 — the studio customization config: the single, studio-only home for\n * everything a consuming repo customizes about the builder UI.\n *\n * This is the \"separate studio-only config solution\": it lives in the consumer's\n * `studio.config.tsx`, is `defineStudio({...})`-typed, and is passed to\n * `createStudio(config, studioConfig)` at the mount — NEVER mixed into the shared\n * `elytra.config.ts`, so the live React it carries (views, icons, field renderers,\n * future branding) never reaches the `frontend/` delivery bundle.\n *\n * The customization PRINCIPLE it embodies: customization is **code passed\n * directly**, not data referenced by a string key. Every field here holds live\n * values/components by reference — `sidebar` is built from `nav.*` view objects,\n * `fieldRenderers` maps a control name to a component. The only string indirection\n * in the whole surface is `fieldRenderers`' key, and ONLY because its other end\n * (`form.control` on a field) lives in the delivery-shared serializable schema and\n * cannot import studio code. This config grows by adding direct-value keys\n * (branding/logo/panels/theme), never by adding registries.\n */\nexport interface StudioConfig {\n /**\n * The section rail (EC-237) as an ordered list of `nav.*` nodes — views, groups,\n * and separators. Omitted → the studio derives the default rail from its built-in\n * views. A rail node carries its view object directly; there are no string ids to\n * keep in sync.\n */\n sidebar?: SidebarNode[]\n /**\n * Custom field renderers (EC-238) keyed by CONTROL name (`form.control`), each a\n * React component replacing the built-in document-editor control. A field opts in\n * per-field via `form: { control: '<name>' }` — so a renderer targets exactly the\n * fields that ask for it, not every field of a datatype. This is the one\n * boundary-crossing seam: the field names its control by string in the shared\n * schema, this map binds that name to a studio-only component.\n */\n fieldRenderers?: Record<string, CustomFieldRenderer>\n /**\n * Studio-UI localization (EC-241): the default locale + imported translation\n * catalogs. English is the built-in source; a consumer adds e.g.\n * `i18n: { locales: { de: deMessages } }` and the studio chrome renders in the\n * browser's preferred locale (no persisted selector in v1). Studio-only — never\n * reaches the delivery bundle. Schema labels localize separately, via a\n * `LocalizedString` on the field/collection def.\n */\n i18n?: StudioI18nConfig\n}\n\n/** The props a custom field renderer receives — identical to the built-in control. */\nexport interface CustomFieldRendererProps {\n field: FieldDef\n value: unknown\n onChange: (value: unknown) => void\n readOnly: boolean\n id: string\n}\nexport type CustomFieldRenderer = (props: CustomFieldRendererProps) => ReactNode\n\n/**\n * Typed identity helper for `studio.config.tsx` — keeps the config declarative\n * (like {@link import('@elytracms/core/studio-core').defineElytraConfig}). No\n * validation here; the studio resolves + validates the sidebar and field renderers\n * through the EC-236 seam at mount, surfacing every issue visibly (never throws).\n *\n * @example\n * ```tsx\n * // studio.config.tsx\n * import { defineStudio, nav } from '@elytracms/studio/customization'\n * import { ColorField } from './fields/color'\n *\n * export default defineStudio({\n * sidebar: [\n * nav.cms({ label: 'Content' }),\n * nav.group('Catalog', [\n * nav.cms({ label: 'Catalog', collections: ['product', 'productCategory'] }),\n * nav.cms({ label: 'Categories', collection: 'productCategory' }),\n * ]),\n * nav.separator(),\n * nav.assets(),\n * nav.settings(),\n * ],\n * fieldRenderers: { color: ColorField },\n * })\n * ```\n */\nexport function defineStudio(config: StudioConfig): StudioConfig {\n return config\n}\n"]}
1
+ {"version":3,"file":"studio-config.js","sourceRoot":"","sources":["../../../src/components/app-views/studio-config.ts"],"names":[],"mappings":"AA0EA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,MAAM,UAAU,YAAY,CAAC,MAAoB;IAC/C,OAAO,MAAM,CAAA;AACf,CAAC","sourcesContent":["import type { ReactNode } from 'react'\nimport type { FieldDef } from '@elytracms/core/cms-core'\nimport type { StudioI18nConfig } from '@elytracms/studio/lib/i18n'\nimport type { StudioAiConfig } from '@elytracms/studio/lib/ai'\nimport type { SidebarNode } from './module'\n\n/**\n * EC-236 — the studio customization config: the single, studio-only home for\n * everything a consuming repo customizes about the builder UI.\n *\n * This is the \"separate studio-only config solution\": it lives in the consumer's\n * `studio.config.tsx`, is `defineStudio({...})`-typed, and is passed to\n * `createStudio(config, studioConfig)` at the mount — NEVER mixed into the shared\n * `elytra.config.ts`, so the live React it carries (views, icons, field renderers,\n * future branding) never reaches the `frontend/` delivery bundle.\n *\n * The customization PRINCIPLE it embodies: customization is **code passed\n * directly**, not data referenced by a string key. Every field here holds live\n * values/components by reference — `sidebar` is built from `nav.*` view objects,\n * `fieldRenderers` maps a control name to a component. The only string indirection\n * in the whole surface is `fieldRenderers`' key, and ONLY because its other end\n * (`form.control` on a field) lives in the delivery-shared serializable schema and\n * cannot import studio code. This config grows by adding direct-value keys\n * (branding/logo/panels/theme), never by adding registries.\n */\nexport interface StudioConfig {\n /**\n * The section rail (EC-237) as an ordered list of `nav.*` nodes — views, groups,\n * and separators. Omitted → the studio derives the default rail from its built-in\n * views. A rail node carries its view object directly; there are no string ids to\n * keep in sync.\n */\n sidebar?: SidebarNode[]\n /**\n * Custom field renderers (EC-238) keyed by CONTROL name (`form.control`), each a\n * React component replacing the built-in document-editor control. A field opts in\n * per-field via `form: { control: '<name>' }` — so a renderer targets exactly the\n * fields that ask for it, not every field of a datatype. This is the one\n * boundary-crossing seam: the field names its control by string in the shared\n * schema, this map binds that name to a studio-only component.\n */\n fieldRenderers?: Record<string, CustomFieldRenderer>\n /**\n * Studio-UI localization (EC-241): the default locale + imported translation\n * catalogs. English is the built-in source; a consumer adds e.g.\n * `i18n: { locales: { de: deMessages } }` and the studio chrome renders in the\n * browser's preferred locale (no persisted selector in v1). Studio-only — never\n * reaches the delivery bundle. Schema labels localize separately, via a\n * `LocalizedString` on the field/collection def.\n */\n i18n?: StudioI18nConfig\n /**\n * AI platform (EC-331): provider, project knowledge, and soft limits. The block's\n * PRESENCE enables the AI feature; its ABSENCE means the feature is fully absent —\n * no AI UI and no AI dependencies on the critical path (the runtime lives behind a\n * lazy boundary in `@elytracms/studio/lib/ai`, only reached when configured).\n * `enabled: false` is a temporary kill-switch. Studio-only live code (holds the\n * provider adapter + knowledge functions) — never reaches the delivery bundle.\n * Validated at load by `resolveAiConfig` (capability gate: tool calling + structured\n * output required, fail-loud).\n */\n ai?: StudioAiConfig\n}\n\n/** The props a custom field renderer receives — identical to the built-in control. */\nexport interface CustomFieldRendererProps {\n field: FieldDef\n value: unknown\n onChange: (value: unknown) => void\n readOnly: boolean\n id: string\n}\nexport type CustomFieldRenderer = (props: CustomFieldRendererProps) => ReactNode\n\n/**\n * Typed identity helper for `studio.config.tsx` — keeps the config declarative\n * (like {@link import('@elytracms/core/studio-core').defineElytraConfig}). No\n * validation here; the studio resolves + validates the sidebar and field renderers\n * through the EC-236 seam at mount, surfacing every issue visibly (never throws).\n *\n * @example\n * ```tsx\n * // studio.config.tsx\n * import { defineStudio, nav } from '@elytracms/studio/customization'\n * import { ColorField } from './fields/color'\n *\n * export default defineStudio({\n * sidebar: [\n * nav.cms({ label: 'Content' }),\n * nav.group('Catalog', [\n * nav.cms({ label: 'Catalog', collections: ['product', 'productCategory'] }),\n * nav.cms({ label: 'Categories', collection: 'productCategory' }),\n * ]),\n * nav.separator(),\n * nav.assets(),\n * nav.settings(),\n * ],\n * fieldRenderers: { color: ColorField },\n * })\n * ```\n */\nexport function defineStudio(config: StudioConfig): StudioConfig {\n return config\n}\n"]}
@@ -1,6 +1,8 @@
1
1
  import type { CmsDocument, CollectionDef } from '@elytracms/core/cms-core';
2
2
  import type { AssetRecord } from '@elytracms/core/persistence';
3
- import type { CollectionViewCapabilities, CollectionViewSource } from '@elytracms/studio/lib/collection-view/collection-view-source';
3
+ import type { OperationError } from '@elytracms/core/operations';
4
+ import type { DeleteAssetResult } from '@elytracms/studio/lib/assets';
5
+ import type { CollectionViewSource, CollectionViewCapabilities } from '@elytracms/studio/lib/collection-view/collection-view-source';
4
6
  /**
5
7
  * The capability profile of the assets rail (EC-319). Hoisted to a module-level
6
8
  * constant (no prop/state dependency) so it's both the value the hook returns
@@ -38,6 +40,12 @@ export declare function assetToDocument(collection: CollectionDef, record: Asset
38
40
  * never written here.
39
41
  */
40
42
  export declare function documentToAssetMetaPatch(collection: CollectionDef, doc: CmsDocument): Record<string, unknown>;
43
+ /** Assets' `remove()` returns an `outcome`-discriminated {@link DeleteAssetResult}
44
+ * (not the `{ ok }` shape users/redirects use, EC-327): only `'error'` is a
45
+ * failure the shell surfaces via `lastError`. `'blocked'`/`'not-found'` are
46
+ * unreached from this rail's UI (`capabilities.remove === false`) but handled
47
+ * soundly regardless, matching the pre-EC-327 behaviour exactly. */
48
+ export declare function normalizeAssetRemoveResult(result: DeleteAssetResult): OperationError | null;
41
49
  /**
42
50
  * Assemble a {@link CollectionViewSource} for the **assets rail** (EC-319) — the
43
51
  * portable list surface over the existing asset library store instead of a
@@ -50,5 +58,9 @@ export declare function documentToAssetMetaPatch(collection: CollectionDef, doc:
50
58
  *
51
59
  * Must be mounted under an `<AssetsProvider>` (for {@link useAssets}) — already
52
60
  * ambient at the project level (EC-195), so callers rarely mount their own.
61
+ *
62
+ * The shared scaffold (`useRailViewSource`, EC-327) owns `viewState`/`lastError`
63
+ * plumbing and the assembled source; this hook supplies only the asset-store
64
+ * reads/writes, the capability profile, and the projections above.
53
65
  */
54
66
  export declare function useAssetsCollectionViewSource(projectId: string, schema: CollectionDef): CollectionViewSource;
@@ -1,9 +1,9 @@
1
- import { useCallback, useMemo, useState } from 'react';
1
+ import { useCallback, useMemo } from 'react';
2
2
  import { createCollectionRegistry, validateDocument } from '@elytracms/core/cms-core';
3
3
  import { studioPath } from '@elytracms/studio/router';
4
- import { defaultViewState } from '@elytracms/studio/lib/cms-views';
5
4
  import { useAssets } from './assets-store';
6
5
  import { SYSTEM_ASSET_FIELDS } from './asset-metadata';
6
+ import { normalizeOkResult, useRailViewSource } from '../rail-view-source';
7
7
  /** The assets rail is not localized in v1 (EC-301: asset metadata is non-localized). */
8
8
  const ASSETS_LOCALE = 'en';
9
9
  const ASSETS_LOCALE_CONFIG = {
@@ -105,6 +105,19 @@ export function documentToAssetMetaPatch(collection, doc) {
105
105
  }
106
106
  return meta;
107
107
  }
108
+ /** `create` when `capabilities.create === false` — never reached from the UI
109
+ * (list/detail never call it); an explicit throw rather than a silent no-op. */
110
+ function createUnsupported() {
111
+ throw new Error('assets are created by upload, not the assets rail');
112
+ }
113
+ /** Assets' `remove()` returns an `outcome`-discriminated {@link DeleteAssetResult}
114
+ * (not the `{ ok }` shape users/redirects use, EC-327): only `'error'` is a
115
+ * failure the shell surfaces via `lastError`. `'blocked'`/`'not-found'` are
116
+ * unreached from this rail's UI (`capabilities.remove === false`) but handled
117
+ * soundly regardless, matching the pre-EC-327 behaviour exactly. */
118
+ export function normalizeAssetRemoveResult(result) {
119
+ return result.outcome === 'error' ? { code: 'unknown', message: result.message } : null;
120
+ }
108
121
  /**
109
122
  * Assemble a {@link CollectionViewSource} for the **assets rail** (EC-319) — the
110
123
  * portable list surface over the existing asset library store instead of a
@@ -117,11 +130,13 @@ export function documentToAssetMetaPatch(collection, doc) {
117
130
  *
118
131
  * Must be mounted under an `<AssetsProvider>` (for {@link useAssets}) — already
119
132
  * ambient at the project level (EC-195), so callers rarely mount their own.
133
+ *
134
+ * The shared scaffold (`useRailViewSource`, EC-327) owns `viewState`/`lastError`
135
+ * plumbing and the assembled source; this hook supplies only the asset-store
136
+ * reads/writes, the capability profile, and the projections above.
120
137
  */
121
138
  export function useAssetsCollectionViewSource(projectId, schema) {
122
139
  const store = useAssets();
123
- const [viewState, setViewStateRaw] = useState(() => defaultViewState(schema));
124
- const [lastError, setLastError] = useState(null);
125
140
  const registry = useMemo(() => createCollectionRegistry([schema]), [schema]);
126
141
  const documents = useCallback(() => store.assets.map((record) => assetToDocument(schema, record)), [store.assets, schema]);
127
142
  const get = useCallback((ref) => {
@@ -132,55 +147,31 @@ export function useAssetsCollectionViewSource(projectId, schema) {
132
147
  const record = store.assets.find((a) => a.id === ref.id);
133
148
  return record?.title ?? record?.filename ?? ref.id;
134
149
  }, [store.assets]);
135
- const setViewState = useCallback((update) => {
136
- setViewStateRaw((prev) => (typeof update === 'function' ? update(prev) : update));
137
- }, []);
138
- const save = useCallback((doc) => {
139
- const meta = documentToAssetMetaPatch(schema, doc);
140
- void store.updateMetadata(doc.id, { meta }).then((result) => {
141
- if (!result.ok)
142
- setLastError({ code: 'unknown', message: result.message });
143
- });
144
- }, [schema, store]);
150
+ const validate = useCallback((doc) => validateDocument(registry, doc, { localeConfig: ASSETS_LOCALE_CONFIG }), [registry]);
151
+ const save = useCallback((doc) => store.updateMetadata(doc.id, { meta: documentToAssetMetaPatch(schema, doc) }), [schema, store]);
145
152
  // Unreached from the UI (`capabilities.remove === false`); implemented soundly
146
153
  // (the same checked delete the store always runs) in case anything ever calls
147
154
  // it directly, rather than throwing.
148
- const remove = useCallback((ref) => {
149
- void store.remove(ref.id).then((result) => {
150
- if (result.outcome === 'error')
151
- setLastError({ code: 'unknown', message: result.message });
152
- });
153
- }, [store]);
154
- return useMemo(() => {
155
- const source = {
156
- collectionId: schema.id,
157
- schema,
158
- capabilities: ASSET_COLLECTION_CAPABILITIES,
159
- projectId,
160
- localeConfig: ASSETS_LOCALE_CONFIG,
161
- activeLocale: ASSETS_LOCALE,
162
- documents,
163
- get,
164
- titleOf,
165
- // Assets carry no relation/asset fields of their own (v1) — nothing cross-collection to resolve.
166
- referenceCandidates: () => [],
167
- collections: [schema],
168
- detailPath: (id) => studioPath.asset(id),
169
- listPath: studioPath.assets(),
170
- isFieldReadOnly: (field) => field.form?.readOnly ?? false,
171
- validate: (doc) => validateDocument(registry, doc, { localeConfig: ASSETS_LOCALE_CONFIG }),
172
- lastError,
173
- clearLastError: () => setLastError(null),
174
- viewState: () => viewState,
175
- setViewState,
176
- // create is gated off (capabilities.create === false); never reached.
177
- create: () => {
178
- throw new Error('assets are created by upload, not the assets rail');
179
- },
180
- save,
181
- remove,
182
- };
183
- return source;
184
- }, [projectId, schema, registry, documents, get, titleOf, setViewState, save, remove, viewState, lastError]);
155
+ const remove = useCallback((ref) => store.remove(ref.id), [store]);
156
+ return useRailViewSource({
157
+ projectId,
158
+ schema,
159
+ capabilities: ASSET_COLLECTION_CAPABILITIES,
160
+ localeConfig: ASSETS_LOCALE_CONFIG,
161
+ documents,
162
+ get,
163
+ titleOf,
164
+ // No `referenceCandidates`/`collections`/`isFieldReadOnly` override: assets carry
165
+ // no relation/asset fields of their own (v1) — nothing cross-collection to
166
+ // resolve — and read-only follows the factory's default `form.readOnly` check.
167
+ detailPath: (id) => studioPath.asset(id),
168
+ listPath: studioPath.assets(),
169
+ validate,
170
+ create: createUnsupported,
171
+ save,
172
+ remove,
173
+ normalizeSaveResult: normalizeOkResult,
174
+ normalizeRemoveResult: normalizeAssetRemoveResult,
175
+ });
185
176
  }
186
177
  //# sourceMappingURL=assets-collection-view-source.js.map
@@ -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, useState } from 'react'\nimport type {\n CmsDocument,\n CollectionDef,\n DocumentRef,\n FieldDef,\n Locale,\n LocaleConfig,\n} from '@elytracms/core/cms-core'\nimport { createCollectionRegistry, validateDocument } from '@elytracms/core/cms-core'\nimport type { AssetRecord } from '@elytracms/core/persistence'\nimport type { OperationError } from '@elytracms/core/operations'\nimport { studioPath } from '@elytracms/studio/router'\nimport { defaultViewState } from '@elytracms/studio/lib/cms-views'\nimport type { TableViewState } from '@elytracms/studio/lib/cms-views'\nimport type {\n CollectionViewCapabilities,\n CollectionViewSource,\n} from '@elytracms/studio/lib/collection-view/collection-view-source'\nimport { useAssets } from './assets-store'\nimport { SYSTEM_ASSET_FIELDS } from './asset-metadata'\n\n/** The assets rail is not localized in v1 (EC-301: asset metadata is non-localized). */\nconst ASSETS_LOCALE: Locale = 'en'\nconst ASSETS_LOCALE_CONFIG: LocaleConfig = {\n default: ASSETS_LOCALE,\n locales: [ASSETS_LOCALE],\n}\n\n/**\n * The capability profile of the assets rail (EC-319). Hoisted to a module-level\n * constant (no prop/state dependency) so it's both the value the hook returns\n * AND independently unit-testable.\n *\n * `create` is off — a new asset comes from an upload, never a blank draft.\n * `remove` is off DELIBERATELY, even though the store can safely delete one\n * (EC-202): the universal table's generic row/bulk delete runs through\n * `ReferenceConfirmDialog`, which enumerates inbound references as\n * `{ type: 'document' }` targets — the wrong index shape for an asset (indexed\n * as `{ type: 'asset' }`, see `loadInboundAssetReferenceViews`). Wiring\n * `remove: true` here would silently show \"not referenced\" for an asset that\n * IS referenced. Deletion instead stays on the asset detail surface\n * (`asset-detail.tsx`), which keeps calling the asset-specific reference check.\n * Nothing else applies to assets: no publish/versions/locales/inline-edit/bulk/\n * live-URL/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 remove: false,\n bulk: false,\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), reading\n * only the schema's DECLARED fields — the same \"schema is a lens\" projection the\n * redirects/users rails use. A system column (`filename`, `url`, dimensions, …)\n * reads straight off the record; everything else — the built-in `alt`, a\n * project's own `copyright`/`credit`, or an EC-311 additive field — round-trips\n * through the record's open `meta` bag (EC-301). `alt` is schema-driven but ALSO\n * has a legacy record column: the meta value wins, falling back to the column\n * (matches delivery's `meta.alt ?? record.alt` read and the image field's\n * options dialog).\n */\nexport function assetToDocument(collection: CollectionDef, record: AssetRecord): CmsDocument {\n const values: Record<string, unknown> = {}\n for (const field of collection.fields) {\n if (field.name === 'alt') {\n const metaAlt = typeof record.meta?.alt === 'string' ? record.meta.alt : undefined\n values.alt = metaAlt ?? record.alt\n continue\n }\n values[field.name] = SYSTEM_ASSET_FIELDS.has(field.name)\n ? systemFieldValue(record, field.name)\n : record.meta?.[field.name]\n }\n return { collection: collection.id, id: record.id, values }\n}\n\n/**\n * The inverse of {@link assetToDocument}: the schema-driven meta bag a save\n * writes back — every declared field OUTSIDE the system set (EC-301/EC-311),\n * keyed exactly as the record stores it. System columns are upload-managed and\n * never written here.\n */\nexport function documentToAssetMetaPatch(\n collection: CollectionDef,\n doc: CmsDocument,\n): Record<string, unknown> {\n const values = doc.values ?? {}\n const meta: Record<string, unknown> = {}\n for (const field of collection.fields) {\n if (SYSTEM_ASSET_FIELDS.has(field.name)) continue\n if (field.name in values) meta[field.name] = values[field.name]\n }\n return meta\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 */\nexport function useAssetsCollectionViewSource(\n projectId: string,\n schema: CollectionDef,\n): CollectionViewSource {\n const store = useAssets()\n const [viewState, setViewStateRaw] = useState<TableViewState>(() => defaultViewState(schema))\n const [lastError, setLastError] = useState<OperationError | null>(null)\n const registry = useMemo(() => createCollectionRegistry([schema]), [schema])\n\n const documents = useCallback(\n () => store.assets.map((record) => assetToDocument(schema, record)),\n [store.assets, schema],\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) : undefined\n },\n [store.assets, schema],\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 setViewState = useCallback<CollectionViewSource['setViewState']>((update) => {\n setViewStateRaw((prev) => (typeof update === 'function' ? update(prev) : update))\n }, [])\n\n const save = useCallback(\n (doc: CmsDocument) => {\n const meta = documentToAssetMetaPatch(schema, doc)\n void store.updateMetadata(doc.id, { meta }).then((result) => {\n if (!result.ok) setLastError({ code: 'unknown', message: result.message })\n })\n },\n [schema, store],\n )\n // Unreached from the UI (`capabilities.remove === false`); implemented soundly\n // (the same checked delete the store always runs) in case anything ever calls\n // it directly, rather than throwing.\n const remove = useCallback(\n (ref: DocumentRef) => {\n void store.remove(ref.id).then((result) => {\n if (result.outcome === 'error') setLastError({ code: 'unknown', message: result.message })\n })\n },\n [store],\n )\n\n return useMemo(() => {\n const source: CollectionViewSource = {\n collectionId: schema.id,\n schema,\n capabilities: ASSET_COLLECTION_CAPABILITIES,\n projectId,\n localeConfig: ASSETS_LOCALE_CONFIG,\n activeLocale: ASSETS_LOCALE,\n documents,\n get,\n titleOf,\n // Assets carry no relation/asset fields of their own (v1) — nothing cross-collection to resolve.\n referenceCandidates: () => [],\n collections: [schema],\n detailPath: (id: string) => studioPath.asset(id),\n listPath: studioPath.assets(),\n isFieldReadOnly: (field: FieldDef) => field.form?.readOnly ?? false,\n validate: (doc: CmsDocument) =>\n validateDocument(registry, doc, { localeConfig: ASSETS_LOCALE_CONFIG }),\n lastError,\n clearLastError: () => setLastError(null),\n viewState: () => viewState,\n setViewState,\n // create is gated off (capabilities.create === false); never reached.\n create: () => {\n throw new Error('assets are created by upload, not the assets rail')\n },\n save,\n remove,\n }\n return source\n }, [projectId, schema, registry, documents, get, titleOf, setViewState, save, remove, viewState, lastError])\n}\n"]}
1
+ {"version":3,"file":"assets-collection-view-source.js","sourceRoot":"","sources":["../../../src/components/assets/assets-collection-view-source.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,OAAO,CAAA;AAE5C,OAAO,EAAE,wBAAwB,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAA;AAGrF,OAAO,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAA;AAGrD,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAC1C,OAAO,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAA;AACtD,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAA;AAE1E,wFAAwF;AACxF,MAAM,aAAa,GAAW,IAAI,CAAA;AAClC,MAAM,oBAAoB,GAAiB;IACzC,OAAO,EAAE,aAAa;IACtB,OAAO,EAAE,CAAC,aAAa,CAAC;CACzB,CAAA;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,CAAC,MAAM,6BAA6B,GAA+B;IACvE,OAAO,EAAE,KAAK;IACd,QAAQ,EAAE,KAAK;IACf,OAAO,EAAE,KAAK;IACd,UAAU,EAAE,KAAK;IACjB,MAAM,EAAE,KAAK;IACb,MAAM,EAAE,KAAK;IACb,IAAI,EAAE,KAAK;IACX,OAAO,EAAE,KAAK;IACd,SAAS,EAAE,KAAK;IAChB,YAAY,EAAE,KAAK;IACnB,SAAS,EAAE,KAAK;CACjB,CAAA;AAED,4EAA4E;AAC5E,SAAS,gBAAgB,CAAC,MAAmB,EAAE,IAAY;IACzD,QAAQ,IAAI,EAAE,CAAC;QACb,KAAK,UAAU;YACb,OAAO,MAAM,CAAC,QAAQ,CAAA;QACxB,KAAK,KAAK;YACR,OAAO,MAAM,CAAC,GAAG,CAAA;QACnB,KAAK,aAAa;YAChB,OAAO,MAAM,CAAC,WAAW,CAAA;QAC3B,KAAK,UAAU;YACb,OAAO,MAAM,CAAC,QAAQ,CAAA;QACxB,KAAK,OAAO;YACV,OAAO,MAAM,CAAC,KAAK,CAAA;QACrB,KAAK,QAAQ;YACX,OAAO,MAAM,CAAC,MAAM,CAAA;QACtB,KAAK,YAAY;YACf,OAAO,MAAM,CAAC,UAAU,CAAA;QAC1B,KAAK,OAAO;YACV,OAAO,MAAM,CAAC,KAAK,CAAA;QACrB;YACE,OAAO,SAAS,CAAA;IACpB,CAAC;AACH,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,eAAe,CAAC,UAAyB,EAAE,MAAmB;IAC5E,MAAM,MAAM,GAA4B,EAAE,CAAA;IAC1C,KAAK,MAAM,KAAK,IAAI,UAAU,CAAC,MAAM,EAAE,CAAC;QACtC,IAAI,KAAK,CAAC,IAAI,KAAK,KAAK,EAAE,CAAC;YACzB,MAAM,OAAO,GAAG,OAAO,MAAM,CAAC,IAAI,EAAE,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,CAAA;YAClF,MAAM,CAAC,GAAG,GAAG,OAAO,IAAI,MAAM,CAAC,GAAG,CAAA;YAClC,SAAQ;QACV,CAAC;QACD,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,mBAAmB,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC;YACtD,CAAC,CAAC,gBAAgB,CAAC,MAAM,EAAE,KAAK,CAAC,IAAI,CAAC;YACtC,CAAC,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;IAC/B,CAAC;IACD,OAAO,EAAE,UAAU,EAAE,UAAU,CAAC,EAAE,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,CAAA;AAC7D,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,wBAAwB,CACtC,UAAyB,EACzB,GAAgB;IAEhB,MAAM,MAAM,GAAG,GAAG,CAAC,MAAM,IAAI,EAAE,CAAA;IAC/B,MAAM,IAAI,GAA4B,EAAE,CAAA;IACxC,KAAK,MAAM,KAAK,IAAI,UAAU,CAAC,MAAM,EAAE,CAAC;QACtC,IAAI,mBAAmB,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC;YAAE,SAAQ;QACjD,IAAI,KAAK,CAAC,IAAI,IAAI,MAAM;YAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;IACjE,CAAC;IACD,OAAO,IAAI,CAAA;AACb,CAAC;AAED;iFACiF;AACjF,SAAS,iBAAiB;IACxB,MAAM,IAAI,KAAK,CAAC,mDAAmD,CAAC,CAAA;AACtE,CAAC;AAED;;;;qEAIqE;AACrE,MAAM,UAAU,0BAA0B,CAAC,MAAyB;IAClE,OAAO,MAAM,CAAC,OAAO,KAAK,OAAO,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,IAAI,CAAA;AACzF,CAAC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,6BAA6B,CAC3C,SAAiB,EACjB,MAAqB;IAErB,MAAM,KAAK,GAAG,SAAS,EAAE,CAAA;IACzB,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC,wBAAwB,CAAC,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC,CAAA;IAE5E,MAAM,SAAS,GAAG,WAAW,CAC3B,GAAG,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,eAAe,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,EACnE,CAAC,KAAK,CAAC,MAAM,EAAE,MAAM,CAAC,CACvB,CAAA;IACD,MAAM,GAAG,GAAG,WAAW,CACrB,CAAC,GAAgB,EAA2B,EAAE;QAC5C,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,GAAG,CAAC,EAAE,CAAC,CAAA;QACxD,OAAO,MAAM,CAAC,CAAC,CAAC,eAAe,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;IAC7D,CAAC,EACD,CAAC,KAAK,CAAC,MAAM,EAAE,MAAM,CAAC,CACvB,CAAA;IACD,MAAM,OAAO,GAAG,WAAW,CACzB,CAAC,GAAgB,EAAU,EAAE;QAC3B,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,GAAG,CAAC,EAAE,CAAC,CAAA;QACxD,OAAO,MAAM,EAAE,KAAK,IAAI,MAAM,EAAE,QAAQ,IAAI,GAAG,CAAC,EAAE,CAAA;IACpD,CAAC,EACD,CAAC,KAAK,CAAC,MAAM,CAAC,CACf,CAAA;IACD,MAAM,QAAQ,GAAG,WAAW,CAC1B,CAAC,GAAgB,EAAE,EAAE,CAAC,gBAAgB,CAAC,QAAQ,EAAE,GAAG,EAAE,EAAE,YAAY,EAAE,oBAAoB,EAAE,CAAC,EAC7F,CAAC,QAAQ,CAAC,CACX,CAAA;IACD,MAAM,IAAI,GAAG,WAAW,CACtB,CAAC,GAAgB,EAAE,EAAE,CAAC,KAAK,CAAC,cAAc,CAAC,GAAG,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,wBAAwB,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,CAAC,EACnG,CAAC,MAAM,EAAE,KAAK,CAAC,CAChB,CAAA;IACD,+EAA+E;IAC/E,8EAA8E;IAC9E,qCAAqC;IACrC,MAAM,MAAM,GAAG,WAAW,CAAC,CAAC,GAAgB,EAAE,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,CAAA;IAE/E,OAAO,iBAAiB,CAAC;QACvB,SAAS;QACT,MAAM;QACN,YAAY,EAAE,6BAA6B;QAC3C,YAAY,EAAE,oBAAoB;QAClC,SAAS;QACT,GAAG;QACH,OAAO;QACP,kFAAkF;QAClF,2EAA2E;QAC3E,+EAA+E;QAC/E,UAAU,EAAE,CAAC,EAAU,EAAE,EAAE,CAAC,UAAU,CAAC,KAAK,CAAC,EAAE,CAAC;QAChD,QAAQ,EAAE,UAAU,CAAC,MAAM,EAAE;QAC7B,QAAQ;QACR,MAAM,EAAE,iBAAiB;QACzB,IAAI;QACJ,MAAM;QACN,mBAAmB,EAAE,iBAAiB;QACtC,qBAAqB,EAAE,0BAA0B;KAClD,CAAC,CAAA;AACJ,CAAC","sourcesContent":["import { useCallback, useMemo } from 'react'\nimport type { CmsDocument, CollectionDef, DocumentRef, Locale, LocaleConfig } from '@elytracms/core/cms-core'\nimport { createCollectionRegistry, validateDocument } from '@elytracms/core/cms-core'\nimport type { AssetRecord } from '@elytracms/core/persistence'\nimport type { OperationError } from '@elytracms/core/operations'\nimport { studioPath } from '@elytracms/studio/router'\nimport type { DeleteAssetResult } from '@elytracms/studio/lib/assets'\nimport type { CollectionViewSource, CollectionViewCapabilities } from '@elytracms/studio/lib/collection-view/collection-view-source'\nimport { useAssets } from './assets-store'\nimport { SYSTEM_ASSET_FIELDS } from './asset-metadata'\nimport { normalizeOkResult, useRailViewSource } from '../rail-view-source'\n\n/** The assets rail is not localized in v1 (EC-301: asset metadata is non-localized). */\nconst ASSETS_LOCALE: Locale = 'en'\nconst ASSETS_LOCALE_CONFIG: LocaleConfig = {\n default: ASSETS_LOCALE,\n locales: [ASSETS_LOCALE],\n}\n\n/**\n * The capability profile of the assets rail (EC-319). Hoisted to a module-level\n * constant (no prop/state dependency) so it's both the value the hook returns\n * AND independently unit-testable.\n *\n * `create` is off — a new asset comes from an upload, never a blank draft.\n * `remove` is off DELIBERATELY, even though the store can safely delete one\n * (EC-202): the universal table's generic row/bulk delete runs through\n * `ReferenceConfirmDialog`, which enumerates inbound references as\n * `{ type: 'document' }` targets — the wrong index shape for an asset (indexed\n * as `{ type: 'asset' }`, see `loadInboundAssetReferenceViews`). Wiring\n * `remove: true` here would silently show \"not referenced\" for an asset that\n * IS referenced. Deletion instead stays on the asset detail surface\n * (`asset-detail.tsx`), which keeps calling the asset-specific reference check.\n * Nothing else applies to assets: no publish/versions/locales/inline-edit/bulk/\n * live-URL/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 remove: false,\n bulk: false,\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), reading\n * only the schema's DECLARED fields — the same \"schema is a lens\" projection the\n * redirects/users rails use. A system column (`filename`, `url`, dimensions, …)\n * reads straight off the record; everything else — the built-in `alt`, a\n * project's own `copyright`/`credit`, or an EC-311 additive field — round-trips\n * through the record's open `meta` bag (EC-301). `alt` is schema-driven but ALSO\n * has a legacy record column: the meta value wins, falling back to the column\n * (matches delivery's `meta.alt ?? record.alt` read and the image field's\n * options dialog).\n */\nexport function assetToDocument(collection: CollectionDef, record: AssetRecord): CmsDocument {\n const values: Record<string, unknown> = {}\n for (const field of collection.fields) {\n if (field.name === 'alt') {\n const metaAlt = typeof record.meta?.alt === 'string' ? record.meta.alt : undefined\n values.alt = metaAlt ?? record.alt\n continue\n }\n values[field.name] = SYSTEM_ASSET_FIELDS.has(field.name)\n ? systemFieldValue(record, field.name)\n : record.meta?.[field.name]\n }\n return { collection: collection.id, id: record.id, values }\n}\n\n/**\n * The inverse of {@link assetToDocument}: the schema-driven meta bag a save\n * writes back — every declared field OUTSIDE the system set (EC-301/EC-311),\n * keyed exactly as the record stores it. System columns are upload-managed and\n * never written here.\n */\nexport function documentToAssetMetaPatch(\n collection: CollectionDef,\n doc: CmsDocument,\n): Record<string, unknown> {\n const values = doc.values ?? {}\n const meta: Record<string, unknown> = {}\n for (const field of collection.fields) {\n if (SYSTEM_ASSET_FIELDS.has(field.name)) continue\n if (field.name in values) meta[field.name] = values[field.name]\n }\n return meta\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): only `'error'` is a\n * failure the shell surfaces via `lastError`. `'blocked'`/`'not-found'` are\n * unreached from this rail's UI (`capabilities.remove === false`) but handled\n * soundly regardless, matching the pre-EC-327 behaviour exactly. */\nexport function normalizeAssetRemoveResult(result: DeleteAssetResult): OperationError | null {\n return result.outcome === 'error' ? { code: 'unknown', message: result.message } : null\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\n const documents = useCallback(\n () => store.assets.map((record) => assetToDocument(schema, record)),\n [store.assets, schema],\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) : undefined\n },\n [store.assets, schema],\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 validate = useCallback(\n (doc: CmsDocument) => validateDocument(registry, doc, { localeConfig: ASSETS_LOCALE_CONFIG }),\n [registry],\n )\n const save = useCallback(\n (doc: CmsDocument) => store.updateMetadata(doc.id, { meta: documentToAssetMetaPatch(schema, doc) }),\n [schema, store],\n )\n // Unreached from the UI (`capabilities.remove === false`); implemented soundly\n // (the same checked delete the store always runs) in case anything ever calls\n // it directly, rather than throwing.\n const remove = useCallback((ref: DocumentRef) => store.remove(ref.id), [store])\n\n return useRailViewSource({\n projectId,\n schema,\n capabilities: ASSET_COLLECTION_CAPABILITIES,\n localeConfig: ASSETS_LOCALE_CONFIG,\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 validate,\n create: createUnsupported,\n save,\n remove,\n normalizeSaveResult: normalizeOkResult,\n normalizeRemoveResult: normalizeAssetRemoveResult,\n })\n}\n"]}
@@ -11,7 +11,7 @@ import { createRequiredContext } from '@elytracms/studio/lib/required-context';
11
11
  import { createCmsPersister, getStudioOperations } from '@elytracms/studio/lib/operations';
12
12
  import { ALL_VIEW_ID, canonicalViewState, configViewsFor, defaultViewState, viewStatesEqual, } from '@elytracms/studio/lib/cms-views';
13
13
  import { useOptionalAssets } from '../assets/assets-store';
14
- import { stagedDocuments } from './document-staging';
14
+ import { consumeStagedDocuments } from './document-staging';
15
15
  import { applyCellValue, commitCellEdit } from './cell-edit';
16
16
  import { runBulkAction } from './bulk-actions';
17
17
  const { Context: CmsContext, useRequired: useCms } = createRequiredContext('Cms');
@@ -83,16 +83,14 @@ function defaultView(collection) {
83
83
  export function CmsProvider({ children, projectId, initialCollections, initialDocuments, initialPublishedKeys, initialLocaleConfig, }) {
84
84
  const [documents, setDocuments] = useState(() => {
85
85
  const seeded = (initialDocuments ?? sampleDocuments).map((d) => structuredClone(d));
86
- // EC-320: merge any documents STAGED by the Components-view "Use in document…"
87
- // transfer (a playground cross-mount handoff — see document-staging). Staged
88
- // wins by id; empty in Convex mode, where hydration is the source of truth.
89
- const staged = stagedDocuments(projectId);
90
- if (staged.length === 0)
91
- return seeded;
92
- const byKey = new Map(seeded.map((d) => [`${d.collection}/${d.id}`, d]));
93
- for (const d of staged)
94
- byKey.set(`${d.collection}/${d.id}`, d);
95
- return [...byKey.values()];
86
+ // EC-320 (0.0.44 follow-up): merge any documents STAGED by the
87
+ // Components-view "Use in document…" transfer — a cross-mount handoff in
88
+ // EVERY mode now (see document-staging.ts), not just the playground. The
89
+ // merge rule itself stays mode-aware there: playground's seeded set is a
90
+ // static baseline that never reflects a write (staged always wins);
91
+ // Convex's is a fresh per-mount hydration query (staged fills gaps only,
92
+ // dropped once the seeded set actually carries the key).
93
+ return consumeStagedDocuments(projectId, seeded);
96
94
  });
97
95
  // Published-ness as the version POINTER (EC-224), decoupled from the flat
98
96
  // `state` enum: seeded from hydration's pointer set, or — when none is supplied