@elytracms/studio 0.0.91 → 0.0.93
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/convex/asset-alt-migration.test.ts +127 -0
- package/convex/asset-replace.test.ts +168 -0
- package/convex/asset-revalidate.test.ts +6 -6
- package/convex/assets.ts +41 -5
- package/convex/cms.ts +537 -177
- package/convex/contract-parity.test.ts +173 -19
- package/convex/delivery.ts +26 -13
- package/convex/document-revalidate.test.ts +17 -16
- package/convex/orphan-hydration.test.ts +116 -0
- package/convex/presence-cost.test.ts +89 -0
- package/convex/rail-write-parity.test.ts +17 -14
- package/convex/referenceMaintenance.ts +15 -15
- package/convex/schema.ts +70 -5
- package/convex/triggers.ts +21 -8
- package/convex/write-limits.ts +1 -1
- package/dist/components/ai/ai-chat-panel.js +33 -22
- package/dist/components/ai/ai-chat-panel.js.map +1 -1
- package/dist/components/ai/proposal-card.js +2 -1
- package/dist/components/ai/proposal-card.js.map +1 -1
- package/dist/components/ai/translate-document-action.d.ts +8 -2
- package/dist/components/ai/translate-document-action.js.map +1 -1
- package/dist/components/ai/translate-document-dialog.d.ts +1 -1
- package/dist/components/ai/translate-document-dialog.js +14 -8
- package/dist/components/ai/translate-document-dialog.js.map +1 -1
- package/dist/components/ai/translate-skipped.d.ts +19 -0
- package/dist/components/ai/translate-skipped.js +46 -0
- package/dist/components/ai/translate-skipped.js.map +1 -0
- package/dist/components/app-views/default-modules.d.ts +23 -6
- package/dist/components/app-views/default-modules.js +33 -14
- package/dist/components/app-views/default-modules.js.map +1 -1
- package/dist/components/assets/asset-detail.js +18 -8
- package/dist/components/assets/asset-detail.js.map +1 -1
- package/dist/components/assets/asset-metadata.d.ts +15 -4
- package/dist/components/assets/asset-metadata.js +15 -2
- package/dist/components/assets/asset-metadata.js.map +1 -1
- package/dist/components/assets/asset-search.js +4 -1
- package/dist/components/assets/asset-search.js.map +1 -1
- package/dist/components/assets/asset-selection.d.ts +0 -8
- package/dist/components/assets/asset-selection.js +4 -23
- package/dist/components/assets/asset-selection.js.map +1 -1
- package/dist/components/assets/asset-thumb.js +3 -2
- package/dist/components/assets/asset-thumb.js.map +1 -1
- package/dist/components/assets/assets-collection-view-source.d.ts +3 -3
- package/dist/components/assets/assets-collection-view-source.js +9 -5
- package/dist/components/assets/assets-collection-view-source.js.map +1 -1
- package/dist/components/assets/assets-store.d.ts +14 -0
- package/dist/components/assets/assets-store.js +23 -1
- package/dist/components/assets/assets-store.js.map +1 -1
- package/dist/components/assets/replace-file.d.ts +22 -0
- package/dist/components/assets/replace-file.js +59 -0
- package/dist/components/assets/replace-file.js.map +1 -0
- package/dist/components/builder/blocks-model.d.ts +14 -26
- package/dist/components/builder/blocks-model.js +35 -41
- package/dist/components/builder/blocks-model.js.map +1 -1
- package/dist/components/builder/blocks-panel.d.ts +7 -3
- package/dist/components/builder/blocks-panel.js +6 -5
- package/dist/components/builder/blocks-panel.js.map +1 -1
- package/dist/components/builder/builder-store.d.ts +126 -30
- package/dist/components/builder/builder-store.js +169 -67
- package/dist/components/builder/builder-store.js.map +1 -1
- package/dist/components/builder/canvas-body.d.ts +30 -0
- package/dist/components/builder/canvas-body.js +38 -0
- package/dist/components/builder/canvas-body.js.map +1 -0
- package/dist/components/builder/canvas-chrome-css.js +7 -0
- package/dist/components/builder/canvas-chrome-css.js.map +1 -1
- package/dist/components/builder/canvas-dnd.d.ts +7 -7
- package/dist/components/builder/canvas-dnd.js +5 -5
- package/dist/components/builder/canvas-dnd.js.map +1 -1
- package/dist/components/builder/canvas-render-context.d.ts +30 -22
- package/dist/components/builder/canvas-render-context.js +44 -44
- package/dist/components/builder/canvas-render-context.js.map +1 -1
- package/dist/components/builder/canvas-runs.d.ts +45 -0
- package/dist/components/builder/canvas-runs.js +51 -0
- package/dist/components/builder/canvas-runs.js.map +1 -0
- package/dist/components/builder/canvas.d.ts +5 -4
- package/dist/components/builder/canvas.js +34 -6
- package/dist/components/builder/canvas.js.map +1 -1
- package/dist/components/builder/clipboard-actions.d.ts +15 -0
- package/dist/components/builder/clipboard-actions.js +50 -13
- package/dist/components/builder/clipboard-actions.js.map +1 -1
- package/dist/components/builder/composition-field-editor.d.ts +10 -16
- package/dist/components/builder/composition-field-editor.js +30 -40
- package/dist/components/builder/composition-field-editor.js.map +1 -1
- package/dist/components/builder/composition-patches.d.ts +7 -0
- package/dist/components/builder/composition-patches.js +256 -0
- package/dist/components/builder/composition-patches.js.map +1 -0
- package/dist/components/builder/delete-key.d.ts +5 -0
- package/dist/components/builder/delete-key.js +4 -0
- package/dist/components/builder/delete-key.js.map +1 -1
- package/dist/components/builder/insert-context.d.ts +17 -25
- package/dist/components/builder/insert-context.js +45 -92
- package/dist/components/builder/insert-context.js.map +1 -1
- package/dist/components/builder/page-tree.d.ts +3 -3
- package/dist/components/builder/page-tree.js +3 -3
- package/dist/components/builder/page-tree.js.map +1 -1
- package/dist/components/builder/placement-refusal.d.ts +18 -0
- package/dist/components/builder/placement-refusal.js +46 -0
- package/dist/components/builder/placement-refusal.js.map +1 -0
- package/dist/components/builder/property-inspector.js +4 -0
- package/dist/components/builder/property-inspector.js.map +1 -1
- package/dist/components/builder/reuse-context.js +65 -26
- package/dist/components/builder/reuse-context.js.map +1 -1
- package/dist/components/builder/reuse.d.ts +79 -8
- package/dist/components/builder/reuse.js +71 -23
- package/dist/components/builder/reuse.js.map +1 -1
- package/dist/components/builder/run-inspector.d.ts +11 -0
- package/dist/components/builder/run-inspector.js +33 -0
- package/dist/components/builder/run-inspector.js.map +1 -0
- package/dist/components/cms/asset-field.d.ts +38 -1
- package/dist/components/cms/asset-field.js +182 -58
- package/dist/components/cms/asset-field.js.map +1 -1
- package/dist/components/cms/asset-picker-core.d.ts +16 -0
- package/dist/components/cms/asset-picker-core.js +20 -0
- package/dist/components/cms/asset-picker-core.js.map +1 -1
- package/dist/components/cms/asset-picker.js +12 -18
- package/dist/components/cms/asset-picker.js.map +1 -1
- package/dist/components/cms/bulk-actions.d.ts +4 -20
- package/dist/components/cms/bulk-actions.js +3 -24
- package/dist/components/cms/bulk-actions.js.map +1 -1
- package/dist/components/cms/cell-edit.d.ts +5 -5
- package/dist/components/cms/cell-edit.js.map +1 -1
- package/dist/components/cms/cell.js.map +1 -1
- package/dist/components/cms/cms-store.d.ts +87 -45
- package/dist/components/cms/cms-store.js +347 -203
- package/dist/components/cms/cms-store.js.map +1 -1
- package/dist/components/cms/collection-table.js +7 -2
- package/dist/components/cms/collection-table.js.map +1 -1
- package/dist/components/cms/commit-policy.d.ts +24 -0
- package/dist/components/cms/commit-policy.js +37 -0
- package/dist/components/cms/commit-policy.js.map +1 -0
- package/dist/components/cms/detail-editor.js +83 -137
- package/dist/components/cms/detail-editor.js.map +1 -1
- package/dist/components/cms/document-diff-view.d.ts +3 -1
- package/dist/components/cms/document-diff-view.js +12 -7
- package/dist/components/cms/document-diff-view.js.map +1 -1
- package/dist/components/cms/draft-commit.d.ts +70 -0
- package/dist/components/cms/draft-commit.js +82 -0
- package/dist/components/cms/draft-commit.js.map +1 -0
- package/dist/components/cms/duplicate-document.d.ts +11 -13
- package/dist/components/cms/duplicate-document.js +26 -34
- package/dist/components/cms/duplicate-document.js.map +1 -1
- package/dist/components/cms/field-anchors.d.ts +7 -4
- package/dist/components/cms/field-anchors.js +9 -7
- package/dist/components/cms/field-anchors.js.map +1 -1
- package/dist/components/cms/field-collaboration.d.ts +47 -0
- package/dist/components/cms/field-collaboration.js +87 -0
- package/dist/components/cms/field-collaboration.js.map +1 -0
- package/dist/components/cms/field-control-lib.d.ts +22 -1
- package/dist/components/cms/field-control-lib.js +33 -4
- package/dist/components/cms/field-control-lib.js.map +1 -1
- package/dist/components/cms/field-control.js +17 -14
- package/dist/components/cms/field-control.js.map +1 -1
- package/dist/components/cms/gate-validation.d.ts +21 -30
- package/dist/components/cms/gate-validation.js +11 -22
- package/dist/components/cms/gate-validation.js.map +1 -1
- package/dist/components/cms/object-array-field.js +4 -4
- package/dist/components/cms/object-array-field.js.map +1 -1
- package/dist/components/cms/prop-fields.d.ts +7 -2
- package/dist/components/cms/prop-fields.js +13 -6
- package/dist/components/cms/prop-fields.js.map +1 -1
- package/dist/components/cms/reference-confirm.js +10 -4
- package/dist/components/cms/reference-confirm.js.map +1 -1
- package/dist/components/cms/rich-text/embed-picker.d.ts +1 -1
- package/dist/components/cms/rich-text/embed-picker.js.map +1 -1
- package/dist/components/cms/rich-text/insert.d.ts +20 -9
- package/dist/components/cms/rich-text/insert.js +25 -17
- package/dist/components/cms/rich-text/insert.js.map +1 -1
- package/dist/components/cms/use-document-draft.d.ts +96 -0
- package/dist/components/cms/use-document-draft.js +283 -0
- package/dist/components/cms/use-document-draft.js.map +1 -0
- package/dist/components/cms/use-document-versions.js +2 -2
- package/dist/components/cms/use-document-versions.js.map +1 -1
- package/dist/components/cms/use-live-url.d.ts +6 -12
- package/dist/components/cms/use-live-url.js +16 -15
- package/dist/components/cms/use-live-url.js.map +1 -1
- package/dist/components/cms/use-publish-controller.d.ts +13 -18
- package/dist/components/cms/use-publish-controller.js +24 -41
- package/dist/components/cms/use-publish-controller.js.map +1 -1
- package/dist/components/cms/use-published-perspective.d.ts +2 -3
- package/dist/components/cms/use-published-perspective.js +4 -3
- package/dist/components/cms/use-published-perspective.js.map +1 -1
- package/dist/components/cms/versions-pane.js +8 -1
- package/dist/components/cms/versions-pane.js.map +1 -1
- package/dist/components/collection-view/cms-collection-view-source.js +24 -7
- package/dist/components/collection-view/cms-collection-view-source.js.map +1 -1
- package/dist/components/components/transfer-dialog.js +28 -23
- package/dist/components/components/transfer-dialog.js.map +1 -1
- package/dist/components/components/transfer.d.ts +31 -21
- package/dist/components/components/transfer.js +49 -35
- package/dist/components/components/transfer.js.map +1 -1
- package/dist/components/project-list.js.map +1 -1
- package/dist/components/project-session.d.ts +6 -3
- package/dist/components/project-session.js +14 -8
- package/dist/components/project-session.js.map +1 -1
- package/dist/components/project-switcher.js.map +1 -1
- package/dist/components/rail-view-source.js +2 -1
- package/dist/components/rail-view-source.js.map +1 -1
- package/dist/components/section-nav.js +1 -1
- package/dist/components/section-nav.js.map +1 -1
- package/dist/lib/ai/apply-content.js +56 -116
- package/dist/lib/ai/apply-content.js.map +1 -1
- package/dist/lib/ai/chat-context.d.ts +8 -0
- package/dist/lib/ai/chat-context.js +37 -6
- package/dist/lib/ai/chat-context.js.map +1 -1
- package/dist/lib/ai/chat-state.d.ts +14 -1
- package/dist/lib/ai/chat-state.js.map +1 -1
- package/dist/lib/ai/constraints.d.ts +20 -5
- package/dist/lib/ai/constraints.js +125 -33
- package/dist/lib/ai/constraints.js.map +1 -1
- package/dist/lib/ai/fake-provider.d.ts +16 -3
- package/dist/lib/ai/fake-provider.js +22 -6
- package/dist/lib/ai/fake-provider.js.map +1 -1
- package/dist/lib/ai/index.d.ts +2 -0
- package/dist/lib/ai/index.js +2 -0
- package/dist/lib/ai/index.js.map +1 -1
- package/dist/lib/ai/leash.d.ts +34 -0
- package/dist/lib/ai/leash.js +28 -0
- package/dist/lib/ai/leash.js.map +1 -0
- package/dist/lib/ai/provenance.d.ts +1 -1
- package/dist/lib/ai/provenance.js.map +1 -1
- package/dist/lib/ai/rich-text-input.d.ts +8 -8
- package/dist/lib/ai/rich-text-input.js +2 -1
- package/dist/lib/ai/rich-text-input.js.map +1 -1
- package/dist/lib/ai/rich-text-translate.d.ts +27 -4
- package/dist/lib/ai/rich-text-translate.js +28 -10
- package/dist/lib/ai/rich-text-translate.js.map +1 -1
- package/dist/lib/ai/shape.d.ts +13 -13
- package/dist/lib/ai/shape.js +69 -93
- package/dist/lib/ai/shape.js.map +1 -1
- package/dist/lib/ai/tools.d.ts +36 -41
- package/dist/lib/ai/tools.js +85 -87
- package/dist/lib/ai/tools.js.map +1 -1
- package/dist/lib/ai/translate-document.d.ts +39 -5
- package/dist/lib/ai/translate-document.js +160 -51
- package/dist/lib/ai/translate-document.js.map +1 -1
- package/dist/lib/ai/types.d.ts +34 -23
- package/dist/lib/ai/types.js.map +1 -1
- package/dist/lib/ai/update-fields.d.ts +21 -12
- package/dist/lib/ai/update-fields.js +118 -177
- package/dist/lib/ai/update-fields.js.map +1 -1
- package/dist/lib/ai/writer-form.d.ts +116 -0
- package/dist/lib/ai/writer-form.js +429 -0
- package/dist/lib/ai/writer-form.js.map +1 -0
- package/dist/lib/ai-current-document.d.ts +11 -11
- package/dist/lib/ai-current-document.js +4 -4
- package/dist/lib/ai-current-document.js.map +1 -1
- package/dist/lib/assets/upload.d.ts +31 -2
- package/dist/lib/assets/upload.js +61 -6
- package/dist/lib/assets/upload.js.map +1 -1
- package/dist/lib/clipboard/os-clipboard.d.ts +10 -2
- package/dist/lib/clipboard/os-clipboard.js +8 -4
- package/dist/lib/clipboard/os-clipboard.js.map +1 -1
- package/dist/lib/clipboard/paste-target.d.ts +18 -9
- package/dist/lib/clipboard/paste-target.js +8 -7
- package/dist/lib/clipboard/paste-target.js.map +1 -1
- package/dist/lib/cms/field-patch.d.ts +15 -0
- package/dist/lib/cms/field-patch.js +25 -0
- package/dist/lib/cms/field-patch.js.map +1 -0
- package/dist/lib/cms/published-documents.d.ts +1 -1
- package/dist/lib/cms/published-documents.js +1 -1
- package/dist/lib/cms/published-documents.js.map +1 -1
- package/dist/lib/cms-views/view-model.d.ts +3 -3
- package/dist/lib/collection-view/__fixtures__/document-writes.d.ts +6 -0
- package/dist/lib/collection-view/__fixtures__/document-writes.js +23 -0
- package/dist/lib/collection-view/__fixtures__/document-writes.js.map +1 -0
- package/dist/lib/collection-view/collection-view-source.d.ts +69 -6
- package/dist/lib/collection-view/collection-view-source.js.map +1 -1
- package/dist/lib/data/collection-content.d.ts +4 -13
- package/dist/lib/data/collection-content.js +11 -5
- package/dist/lib/data/collection-content.js.map +1 -1
- package/dist/lib/dnd/drop-rules.d.ts +25 -31
- package/dist/lib/dnd/drop-rules.js +53 -59
- package/dist/lib/dnd/drop-rules.js.map +1 -1
- package/dist/lib/engine-version.d.ts +1 -1
- package/dist/lib/engine-version.js +1 -1
- package/dist/lib/engine-version.js.map +1 -1
- package/dist/lib/i18n/messages.de.js +72 -15
- package/dist/lib/i18n/messages.de.js.map +1 -1
- package/dist/lib/i18n/messages.en.d.ts +61 -12
- package/dist/lib/i18n/messages.en.js +76 -16
- package/dist/lib/i18n/messages.en.js.map +1 -1
- package/dist/lib/operations/__fixtures__/write-document.d.ts +30 -0
- package/dist/lib/operations/__fixtures__/write-document.js +16 -0
- package/dist/lib/operations/__fixtures__/write-document.js.map +1 -0
- package/dist/lib/operations/cms-persistence.d.ts +124 -31
- package/dist/lib/operations/cms-persistence.js +447 -68
- package/dist/lib/operations/cms-persistence.js.map +1 -1
- package/dist/lib/operations/config-manifest-lookup.d.ts +35 -0
- package/dist/lib/operations/config-manifest-lookup.js +56 -0
- package/dist/lib/operations/config-manifest-lookup.js.map +1 -0
- package/dist/lib/operations/project-runtime-config.d.ts +39 -0
- package/dist/lib/operations/project-runtime-config.js +54 -0
- package/dist/lib/operations/project-runtime-config.js.map +1 -0
- package/dist/lib/operations/publish-with-revision.d.ts +64 -0
- package/dist/lib/operations/publish-with-revision.js +66 -0
- package/dist/lib/operations/publish-with-revision.js.map +1 -0
- package/dist/lib/operations/runtime-validator.d.ts +32 -0
- package/dist/lib/operations/runtime-validator.js +18 -0
- package/dist/lib/operations/runtime-validator.js.map +1 -0
- package/dist/lib/operations/session-hydration.d.ts +31 -14
- package/dist/lib/operations/session-hydration.js +12 -16
- package/dist/lib/operations/session-hydration.js.map +1 -1
- package/dist/lib/operations/studio-operations.d.ts +59 -2
- package/dist/lib/operations/studio-operations.js +76 -14
- package/dist/lib/operations/studio-operations.js.map +1 -1
- package/dist/lib/persistence/convex-adapter.d.ts +13 -5
- package/dist/lib/persistence/convex-adapter.js +189 -74
- package/dist/lib/persistence/convex-adapter.js.map +1 -1
- package/dist/lib/persistence/convex-client.d.ts +65 -5
- package/dist/lib/persistence/convex-client.js +106 -3
- package/dist/lib/persistence/convex-client.js.map +1 -1
- package/dist/lib/persistence/index.js +17 -18
- package/dist/lib/persistence/index.js.map +1 -1
- package/dist/lib/persona/labels.js +4 -1
- package/dist/lib/persona/labels.js.map +1 -1
- package/dist/lib/query/operations-query.d.ts +1 -1
- package/dist/lib/query/operations-query.js +1 -1
- package/dist/lib/query/operations-query.js.map +1 -1
- package/dist/lib/references/impact.d.ts +21 -15
- package/dist/lib/references/impact.js +13 -26
- package/dist/lib/references/impact.js.map +1 -1
- package/dist/lib/references/references-store.d.ts +9 -0
- package/dist/lib/references/references-store.js +24 -4
- package/dist/lib/references/references-store.js.map +1 -1
- package/dist/lib/routes/route-issues.d.ts +6 -6
- package/dist/lib/routes/route-issues.js +26 -80
- package/dist/lib/routes/route-issues.js.map +1 -1
- package/dist/lib/workspace/config-issues.js +2 -1
- package/dist/lib/workspace/config-issues.js.map +1 -1
- package/dist/lib/workspace/index.d.ts +31 -1
- package/dist/lib/workspace/index.js +64 -17
- package/dist/lib/workspace/index.js.map +1 -1
- package/dist/lib/workspace/switcher-source.d.ts +3 -3
- package/dist/lib/workspace/switcher-source.js.map +1 -1
- package/dist/lib/workspace/workspace-source.d.ts +2 -2
- package/dist/lib/workspace/workspace-source.js +2 -1
- package/dist/lib/workspace/workspace-source.js.map +1 -1
- package/package.json +3 -3
- package/dist/components/cms/cms-composition-validators.d.ts +0 -16
- package/dist/components/cms/cms-composition-validators.js +0 -133
- package/dist/components/cms/cms-composition-validators.js.map +0 -1
- package/dist/components/cms/publish-clean.d.ts +0 -28
- package/dist/components/cms/publish-clean.js +0 -38
- package/dist/components/cms/publish-clean.js.map +0 -1
- package/dist/components/cms/use-save-mode.d.ts +0 -35
- package/dist/components/cms/use-save-mode.js +0 -54
- package/dist/components/cms/use-save-mode.js.map +0 -1
- package/dist/lib/assets/asset-placements.d.ts +0 -28
- package/dist/lib/assets/asset-placements.js +0 -83
- package/dist/lib/assets/asset-placements.js.map +0 -1
- package/dist/lib/cms/document-gate.d.ts +0 -47
- package/dist/lib/cms/document-gate.js +0 -61
- package/dist/lib/cms/document-gate.js.map +0 -1
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { liftLegacyAlt } from '@elytracms/core/persistence';
|
|
1
2
|
/**
|
|
2
3
|
* Asset metadata editing (EC-152). A patch is applied through the
|
|
3
4
|
* {@link AssetRepository} interface — in the studio that is the EC-141
|
|
@@ -42,6 +43,14 @@ export function formatBytes(byteSize) {
|
|
|
42
43
|
return `${(byteSize / 1024).toFixed(1)} KB`;
|
|
43
44
|
return `${(byteSize / (1024 * 1024)).toFixed(1)} MB`;
|
|
44
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* The meta bag an editor starts from (EC-587): the stored `meta` with a legacy `alt`
|
|
48
|
+
* column lifted in (core `liftLegacyAlt`), so a not-yet-migrated asset shows the alt
|
|
49
|
+
* every reader shows — and the save writes it to `meta`, its home.
|
|
50
|
+
*/
|
|
51
|
+
export function assetMetaSeed(record) {
|
|
52
|
+
return liftLegacyAlt(record)?.meta ?? { ...(record.meta ?? {}) };
|
|
53
|
+
}
|
|
45
54
|
/** A text column's write: a trimmed value sets, `null`/empty clears. */
|
|
46
55
|
function textWrite(value) {
|
|
47
56
|
const trimmed = value?.trim() ?? '';
|
|
@@ -53,14 +62,18 @@ function textWrite(value) {
|
|
|
53
62
|
* upload-owned columns, and never `thumb`/`lqip`: a stale copy of those would
|
|
54
63
|
* overwrite a preview backfill that finished while the dialog was open, and a
|
|
55
64
|
* clear expressed as an omitted key is exactly what never persisted on Convex.
|
|
65
|
+
*
|
|
66
|
+
* EC-587: a `meta` write also clears the legacy `alt` column. The bag was seeded with
|
|
67
|
+
* that column lifted in ({@link assetMetaSeed}), so the alt the editor saw is in the bag
|
|
68
|
+
* (or was deliberately cleared): the column has nothing left to say, and leaving it
|
|
69
|
+
* would re-surface a cleared alt through the read fallback (ASSETS D21).
|
|
56
70
|
*/
|
|
57
71
|
export function assetMetadataWrite(assetId, patch) {
|
|
58
72
|
return {
|
|
59
73
|
id: assetId,
|
|
60
|
-
...(patch.alt !== undefined ? { alt: textWrite(patch.alt) } : {}),
|
|
61
74
|
...(patch.title !== undefined ? { title: textWrite(patch.title) } : {}),
|
|
62
75
|
...(patch.focalPoint !== undefined ? { focalPoint: patch.focalPoint } : {}),
|
|
63
|
-
...(patch.meta !== undefined ? { meta: patch.meta } : {}),
|
|
76
|
+
...(patch.meta !== undefined ? { meta: patch.meta, alt: null } : {}),
|
|
64
77
|
};
|
|
65
78
|
}
|
|
66
79
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"asset-metadata.js","sourceRoot":"","sources":["../../../src/components/assets/asset-metadata.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"asset-metadata.js","sourceRoot":"","sources":["../../../src/components/assets/asset-metadata.ts"],"names":[],"mappings":"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","sourcesContent":["import type { CollectionDef, FieldDef } from '@elytracms/core/cms-core'\nimport { liftLegacyAlt } from '@elytracms/core/persistence'\nimport type { AssetRecord, AssetRepository, AssetWriteInput } from '@elytracms/core/persistence'\n\n/**\n * Asset metadata editing (EC-152). A patch is applied through the\n * {@link AssetRepository} interface — in the studio that is the EC-141\n * operations facade, so every edit runs the `assets.upsert` command\n * (validate → authorize → mutate → event). Pure orchestration; injectable\n * for tests.\n */\n\n/**\n * System columns the upload pipeline owns — never editable metadata, so they're\n * excluded from the asset collection's schema-driven forms: the image field's\n * options dialog (EC-301, `field-control.tsx`) and the assets rail's detail\n * surface (EC-319, `asset-detail.tsx`) share this ONE reserved set so neither\n * drifts from the other. Everything else a project declares on the `kind:'asset'`\n * collection round-trips through the record's open `meta` bag.\n */\nexport const SYSTEM_ASSET_FIELDS: ReadonlySet<string> = new Set([\n 'filename',\n 'url',\n 'contentType',\n 'byteSize',\n 'width',\n 'height',\n 'storageKey',\n 'title',\n])\n\n/**\n * The asset collection's EDITABLE metadata fields — declared fields minus the\n * system columns above and anything explicitly `readOnly` (EC-301/EC-319).\n */\nexport function assetMetaFieldsOf(collection: CollectionDef): FieldDef[] {\n return collection.fields.filter((f) => !SYSTEM_ASSET_FIELDS.has(f.name) && !f.form?.readOnly)\n}\n\n/** Drop `undefined` values so a persisted meta bag stays JSON/Convex-clean. */\nexport function pruneMeta(meta: Record<string, unknown>): Record<string, unknown> {\n return Object.fromEntries(Object.entries(meta).filter(([, v]) => v !== undefined))\n}\n\n/** Human-readable byte size (`1536` → `1.5 KB`). Shared by the asset grid + detail. */\nexport function formatBytes(byteSize: number): string {\n if (byteSize < 1024) return `${byteSize} B`\n if (byteSize < 1024 * 1024) return `${(byteSize / 1024).toFixed(1)} KB`\n return `${(byteSize / (1024 * 1024)).toFixed(1)} MB`\n}\n\n/**\n * An editor's metadata change, in the EC-577 write rule: an ABSENT key leaves the\n * stored value untouched, `null` clears it, a value sets it.\n */\nexport interface AssetMetadataPatch {\n /** New display title: a value sets (trimmed), `null` or a trimmed-empty string clears. */\n title?: string | null\n /**\n * New focal point (EC-248), the normalized 0–1 hotspot threaded to delivery's\n * `object-position` (EC-230). `{x,y}` sets it, `null` clears it (\"Reset to\n * centre\").\n */\n focalPoint?: { x: number; y: number } | null\n /**\n * Schema-driven asset metadata bag (EC-301): alt, copyright, credit, … keyed by\n * the asset collection's `fields`. An object **replaces** the whole bag (the\n * image options dialog holds the complete edited meta, seeded through\n * {@link assetMetaSeed}); `null` clears it. `meta.alt` is the asset's ONE alt\n * (EC-587) — there is no separate alt key.\n */\n meta?: Record<string, unknown> | null\n}\n\n/**\n * The meta bag an editor starts from (EC-587): the stored `meta` with a legacy `alt`\n * column lifted in (core `liftLegacyAlt`), so a not-yet-migrated asset shows the alt\n * every reader shows — and the save writes it to `meta`, its home.\n */\nexport function assetMetaSeed(record: Pick<AssetRecord, 'alt' | 'meta'>): Record<string, unknown> {\n return liftLegacyAlt(record)?.meta ?? { ...(record.meta ?? {}) }\n}\n\n/** A text column's write: a trimmed value sets, `null`/empty clears. */\nfunction textWrite(value: string | null): string | null {\n const trimmed = value?.trim() ?? ''\n return trimmed.length > 0 ? trimmed : null\n}\n\n/**\n * The asset write for a metadata edit (EC-577): ONLY the keys the editor changed,\n * with `null` for a clear. It deliberately restates nothing else — not the\n * upload-owned columns, and never `thumb`/`lqip`: a stale copy of those would\n * overwrite a preview backfill that finished while the dialog was open, and a\n * clear expressed as an omitted key is exactly what never persisted on Convex.\n *\n * EC-587: a `meta` write also clears the legacy `alt` column. The bag was seeded with\n * that column lifted in ({@link assetMetaSeed}), so the alt the editor saw is in the bag\n * (or was deliberately cleared): the column has nothing left to say, and leaving it\n * would re-surface a cleared alt through the read fallback (ASSETS D21).\n */\nexport function assetMetadataWrite(assetId: string, patch: AssetMetadataPatch): AssetWriteInput {\n return {\n id: assetId,\n ...(patch.title !== undefined ? { title: textWrite(patch.title) } : {}),\n ...(patch.focalPoint !== undefined ? { focalPoint: patch.focalPoint } : {}),\n ...(patch.meta !== undefined ? { meta: patch.meta, alt: null } : {}),\n }\n}\n\nexport type UpdateAssetMetadataResult =\n | { ok: true; asset: AssetRecord }\n | { ok: false; message: string }\n\n/**\n * Persist a metadata edit via the repository (the operations facade in the\n * studio). No read first: the write carries only the change, and the adapter\n * resolves it against the stored record inside its own transaction (EC-577). An\n * asset that no longer exists is refused there (a metadata write cannot create\n * one). Failures come back as a structured result — never a throw the UI has to\n * guess about.\n */\nexport async function updateAssetMetadata(\n deps: { assets: AssetRepository },\n projectId: string,\n assetId: string,\n patch: AssetMetadataPatch,\n): Promise<UpdateAssetMetadataResult> {\n try {\n const asset = await deps.assets.upsertAsset(projectId, assetMetadataWrite(assetId, patch))\n return { ok: true, asset }\n } catch (cause) {\n return { ok: false, message: cause instanceof Error ? cause.message : String(cause) }\n }\n}\n"]}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { assetAlt } from '@elytracms/core/persistence';
|
|
1
2
|
/**
|
|
2
3
|
* Client-side asset search (EC-152): case-insensitive substring match over
|
|
3
4
|
* filename, alt text, and title. Pure and deterministic — input order is
|
|
@@ -7,6 +8,8 @@ export function filterAssets(assets, query) {
|
|
|
7
8
|
const q = query.trim().toLowerCase();
|
|
8
9
|
if (q.length === 0)
|
|
9
10
|
return [...assets];
|
|
10
|
-
return assets.filter((asset) =>
|
|
11
|
+
return assets.filter((asset) =>
|
|
12
|
+
// EC-587 (ASSETS D20): the asset's alt through THE reader (`meta.alt`, its home).
|
|
13
|
+
[asset.filename, assetAlt(asset), asset.title].some((value) => value != null && value.toLowerCase().includes(q)));
|
|
11
14
|
}
|
|
12
15
|
//# sourceMappingURL=asset-search.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"asset-search.js","sourceRoot":"","sources":["../../../src/components/assets/asset-search.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"asset-search.js","sourceRoot":"","sources":["../../../src/components/assets/asset-search.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,6BAA6B,CAAA;AAGtD;;;;GAIG;AACH,MAAM,UAAU,YAAY,CAAC,MAA8B,EAAE,KAAa;IACxE,MAAM,CAAC,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAA;IACpC,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,CAAC,GAAG,MAAM,CAAC,CAAA;IACtC,OAAO,MAAM,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE;IAC7B,kFAAkF;IAClF,CAAC,KAAK,CAAC,QAAQ,EAAE,QAAQ,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC,IAAI,CACjD,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,IAAI,IAAI,IAAI,KAAK,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC,CAC5D,CACF,CAAA;AACH,CAAC","sourcesContent":["import { assetAlt } from '@elytracms/core/persistence'\nimport type { AssetRecord } from '@elytracms/core/persistence'\n\n/**\n * Client-side asset search (EC-152): case-insensitive substring match over\n * filename, alt text, and title. Pure and deterministic — input order is\n * preserved; an empty/whitespace query matches everything.\n */\nexport function filterAssets(assets: readonly AssetRecord[], query: string): AssetRecord[] {\n const q = query.trim().toLowerCase()\n if (q.length === 0) return [...assets]\n return assets.filter((asset) =>\n // EC-587 (ASSETS D20): the asset's alt through THE reader (`meta.alt`, its home).\n [asset.filename, assetAlt(asset), asset.title].some(\n (value) => value != null && value.toLowerCase().includes(q),\n ),\n )\n}\n"]}
|
|
@@ -2,14 +2,6 @@
|
|
|
2
2
|
export declare function assetIdsOf(value: unknown): string[];
|
|
3
3
|
/** The single id a `one` asset field currently holds, if any. */
|
|
4
4
|
export declare function singleAssetIdOf(value: unknown): string | null;
|
|
5
|
-
/**
|
|
6
|
-
* Remove the `index`-th placement of a `many` field — `index` counts the readable
|
|
7
|
-
* entries, the list {@link assetIdsOf} shows — keeping every other placement VERBATIM,
|
|
8
|
-
* per-use sub-fields included (EC-586). It used to rebuild the list from the bare ids
|
|
9
|
-
* (`ids.filter(…)`), which wrote the picker's bare form AND dropped every remaining
|
|
10
|
-
* placement's caption. Removing the last placement leaves the field absent, never `[]`.
|
|
11
|
-
*/
|
|
12
|
-
export declare function removeAssetPlacementAt(current: unknown, index: number): unknown;
|
|
13
5
|
/**
|
|
14
6
|
* Apply a picker selection to the stored field value, keeping every placement's
|
|
15
7
|
* per-use sub-fields (EC-558).
|
|
@@ -7,11 +7,11 @@
|
|
|
7
7
|
*
|
|
8
8
|
* EC-558: the pick operations below are the ones the table cell and the detail
|
|
9
9
|
* editor BOTH commit. They work out which ids the field holds next, and hand the
|
|
10
|
-
* per-use sub-field carry to the one rule, `carryAssetPlacements` (
|
|
11
|
-
*
|
|
10
|
+
* per-use sub-field carry to the one rule, `carryAssetPlacements` (core, EC-587 —
|
|
11
|
+
* AI `updateFields` uses it too). Neither writer rebuilds the value itself; a gallery
|
|
12
|
+
* remove is core's `removeAssetPlacementAt`.
|
|
12
13
|
*/
|
|
13
|
-
import { readAssetId } from '@elytracms/core/cms-core';
|
|
14
|
-
import { carryAssetPlacements } from '@elytracms/studio/lib/assets/asset-placements';
|
|
14
|
+
import { carryAssetPlacements, readAssetId } from '@elytracms/core/cms-core';
|
|
15
15
|
/** The id list a `many` asset field currently holds (unreadable entries dropped). */
|
|
16
16
|
export function assetIdsOf(value) {
|
|
17
17
|
if (!Array.isArray(value))
|
|
@@ -27,25 +27,6 @@ export function singleAssetIdOf(value) {
|
|
|
27
27
|
return readAssetId(value[0]) ?? null;
|
|
28
28
|
return null;
|
|
29
29
|
}
|
|
30
|
-
/**
|
|
31
|
-
* Remove the `index`-th placement of a `many` field — `index` counts the readable
|
|
32
|
-
* entries, the list {@link assetIdsOf} shows — keeping every other placement VERBATIM,
|
|
33
|
-
* per-use sub-fields included (EC-586). It used to rebuild the list from the bare ids
|
|
34
|
-
* (`ids.filter(…)`), which wrote the picker's bare form AND dropped every remaining
|
|
35
|
-
* placement's caption. Removing the last placement leaves the field absent, never `[]`.
|
|
36
|
-
*/
|
|
37
|
-
export function removeAssetPlacementAt(current, index) {
|
|
38
|
-
if (!Array.isArray(current))
|
|
39
|
-
return current;
|
|
40
|
-
let seen = -1;
|
|
41
|
-
const next = current.filter((entry) => {
|
|
42
|
-
if (readAssetId(entry) === undefined)
|
|
43
|
-
return true;
|
|
44
|
-
seen += 1;
|
|
45
|
-
return seen !== index;
|
|
46
|
-
});
|
|
47
|
-
return next.length === 0 ? undefined : next;
|
|
48
|
-
}
|
|
49
30
|
/**
|
|
50
31
|
* Apply a picker selection to the stored field value, keeping every placement's
|
|
51
32
|
* per-use sub-fields (EC-558).
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"asset-selection.js","sourceRoot":"","sources":["../../../src/components/assets/asset-selection.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"asset-selection.js","sourceRoot":"","sources":["../../../src/components/assets/asset-selection.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,EAAE,oBAAoB,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAA;AAE5E,qFAAqF;AACrF,MAAM,UAAU,UAAU,CAAC,KAAc;IACvC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,EAAE,CAAA;IACpC,OAAO,KAAK,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC,MAAM,CAAC,CAAC,EAAE,EAAgB,EAAE,CAAC,EAAE,KAAK,SAAS,CAAC,CAAA;AAC9E,CAAC;AAED,iEAAiE;AACjE,MAAM,UAAU,eAAe,CAAC,KAAc;IAC5C,MAAM,MAAM,GAAG,WAAW,CAAC,KAAK,CAAC,CAAA;IACjC,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO,MAAM,CAAA;IACvC,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,WAAW,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,IAAI,CAAA;IAC9D,OAAO,IAAI,CAAA;AACb,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,mBAAmB,CACjC,OAAgB,EAChB,MAAqB,EACrB,QAAiB;IAEjB,IAAI,CAAC,QAAQ;QAAE,OAAO,oBAAoB,CAAC,OAAO,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC,CAAA;IACpF,IAAI,MAAM,KAAK,IAAI;QAAE,OAAO,OAAO,CAAA;IACnC,OAAO,qBAAqB,CAAC,OAAO,EAAE,CAAC,MAAM,CAAC,CAAC,CAAA;AACjD,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,qBAAqB,CAAC,OAAgB,EAAE,MAAyB;IAC/E,MAAM,GAAG,GAAG,UAAU,CAAC,OAAO,CAAC,CAAA;IAC/B,MAAM,KAAK,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,CAAA;IACpE,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,OAAO,CAAA;IACtC,OAAO,oBAAoB,CAAC,OAAO,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC,GAAG,GAAG,EAAE,GAAG,KAAK,CAAC,EAAE,CAAC,CAAA;AACnF,CAAC","sourcesContent":["/**\n * Pure value logic for CMS `asset` fields (EC-075 → EC-152): a field stores one\n * placement (cardinality `one`) or a list of them (`many`). These helpers read stored\n * values and apply a picker selection without the UI owning the rules. EC-586: every\n * placement a pick writes is the canonical `{ type:'asset', id, … }` envelope; reads\n * still tolerate a legacy bare id (the tolerant reader goes after the reseed window).\n *\n * EC-558: the pick operations below are the ones the table cell and the detail\n * editor BOTH commit. They work out which ids the field holds next, and hand the\n * per-use sub-field carry to the one rule, `carryAssetPlacements` (core, EC-587 —\n * AI `updateFields` uses it too). Neither writer rebuilds the value itself; a gallery\n * remove is core's `removeAssetPlacementAt`.\n */\nimport { carryAssetPlacements, readAssetId } from '@elytracms/core/cms-core'\n\n/** The id list a `many` asset field currently holds (unreadable entries dropped). */\nexport function assetIdsOf(value: unknown): string[] {\n if (!Array.isArray(value)) return []\n return value.map(readAssetId).filter((id): id is string => id !== undefined)\n}\n\n/** The single id a `one` asset field currently holds, if any. */\nexport function singleAssetIdOf(value: unknown): string | null {\n const direct = readAssetId(value)\n if (direct !== undefined) return direct\n if (Array.isArray(value)) return readAssetId(value[0]) ?? null\n return null\n}\n\n/**\n * Apply a picker selection to the stored field value, keeping every placement's\n * per-use sub-fields (EC-558).\n * - `one`: the picked id replaces the image and keeps the placement's sub-fields.\n * Picking the id it already shows is a no-op, and `null` clears it (`undefined`).\n * - `many`: the picked id is appended once, and held entries stay as they are.\n * An already-present id and `null` are no-ops.\n *\n * A no-op returns `current` itself, so a caller can skip the write.\n */\nexport function applyAssetSelection(\n current: unknown,\n picked: string | null,\n multiple: boolean,\n): unknown {\n if (!multiple) return carryAssetPlacements(current, { multiple: false, id: picked })\n if (picked === null) return current\n return appendAssetSelections(current, [picked])\n}\n\n/**\n * Attach several picked ids to a `many` field at once: an in-picker or drop upload\n * batch (EC-460). Each id not already held is appended once, in arrival order, and\n * held entries stay as they are. Returns `current` itself when nothing is new.\n */\nexport function appendAssetSelections(current: unknown, picked: readonly string[]): unknown {\n const ids = assetIdsOf(current)\n const fresh = [...new Set(picked)].filter((id) => !ids.includes(id))\n if (fresh.length === 0) return current\n return carryAssetPlacements(current, { multiple: true, ids: [...ids, ...fresh] })\n}\n"]}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useMemo, useRef, useState } from 'react';
|
|
3
3
|
import { Image } from 'lucide-react';
|
|
4
|
+
import { assetAlt } from '@elytracms/core/persistence';
|
|
4
5
|
import { Text } from '@elytracms/studio/ui';
|
|
5
6
|
import { translate } from '@elytracms/studio/lib/i18n';
|
|
6
7
|
import { cn } from '@elytracms/studio/lib/utils';
|
|
@@ -53,7 +54,7 @@ function thumbnailSrc(record) {
|
|
|
53
54
|
export function AssetThumb({ record, className, alt, }) {
|
|
54
55
|
const src = thumbnailSrc(record);
|
|
55
56
|
if (record && src) {
|
|
56
|
-
return (_jsx("img", { src: src, alt: alt ?? record
|
|
57
|
+
return (_jsx("img", { src: src, alt: alt ?? assetAlt(record) ?? record.title ?? record.filename, loading: "lazy", style: { objectPosition: focalPointObjectPosition(record.focalPoint) }, className: cn('rounded border border-border bg-muted object-cover', className) }));
|
|
57
58
|
}
|
|
58
59
|
return (_jsx("div", { className: cn('flex items-center justify-center rounded border border-border bg-muted text-muted-foreground', className), children: _jsx(Image, { className: "size-4", "aria-hidden": true }) }));
|
|
59
60
|
}
|
|
@@ -92,7 +93,7 @@ export function FocalPointPicker({ record, point, onChange, onPreview, disabled,
|
|
|
92
93
|
e.currentTarget.releasePointerCapture(e.pointerId);
|
|
93
94
|
onChange(pointFrom(e)); // commit on release only
|
|
94
95
|
}
|
|
95
|
-
return (_jsxs("button", { type: "button", "aria-label": translate('assets.focalPoint.setLabel'), disabled: disabled, onPointerDown: onPointerDown, onPointerMove: onPointerMove, onPointerUp: onPointerUp, className: cn('relative block w-full touch-none select-none overflow-hidden rounded border border-border bg-muted', 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/40', disabled ? 'cursor-default opacity-60' : 'cursor-crosshair'), children: [_jsx("img", { src: record.url, alt: record
|
|
96
|
+
return (_jsxs("button", { type: "button", "aria-label": translate('assets.focalPoint.setLabel'), disabled: disabled, onPointerDown: onPointerDown, onPointerMove: onPointerMove, onPointerUp: onPointerUp, className: cn('relative block w-full touch-none select-none overflow-hidden rounded border border-border bg-muted', 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/40', disabled ? 'cursor-default opacity-60' : 'cursor-crosshair'), children: [_jsx("img", { src: record.url, alt: assetAlt(record) ?? record.filename, draggable: false, onLoad: () => setLoaded(true), className: cn('block h-auto w-full select-none transition-opacity', loaded ? 'opacity-100' : 'opacity-0') }), !loaded ? (_jsx("span", { className: "absolute inset-0 grid aspect-video place-items-center bg-muted", children: _jsx(Image, { className: "size-6 animate-pulse text-muted-foreground", "aria-hidden": true }) })) : (_jsx("span", { "aria-hidden": true, className: "pointer-events-none absolute size-4 -translate-x-1/2 -translate-y-1/2 rounded-full border-2 border-white shadow-[0_0_0_1.5px_rgba(0,0,0,0.55)] ring-1 ring-black/40", style: { left: `${marker.x * 100}%`, top: `${marker.y * 100}%` } }))] }));
|
|
96
97
|
}
|
|
97
98
|
/** Common crop aspect ratios shown as focal-aware previews (EC-301). */
|
|
98
99
|
const FOCAL_CROP_RATIOS = [
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"asset-thumb.js","sourceRoot":"","sources":["../../../src/components/assets/asset-thumb.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useMemo, useRef, useState } from 'react'\nimport { Image } from 'lucide-react'\nimport type { AssetRecord } from '@elytracms/core/persistence'\nimport { Text } from '@elytracms/studio/ui'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { cn } from '@elytracms/studio/lib/utils'\nimport { useOptionalAssets } from './assets-store'\nimport { isDisplayableAssetUrl } from '@elytracms/studio/lib/assets'\nimport {\n CENTER_FOCAL_POINT,\n focalPointFromPoint,\n focalPointObjectPosition,\n} from './focal-point'\nimport type { FocalPoint } from './focal-point'\n\n/** Resolve an asset record by id from the ambient library, if one is mounted. */\nexport function useAssetRecord(assetId: string | null | undefined): AssetRecord | undefined {\n const store = useOptionalAssets()\n return useMemo(() => {\n if (!assetId || !store || store.status !== 'ready') return undefined\n return store.assets.find((a) => a.id === assetId)\n }, [assetId, store])\n}\n\n/** True when the record is an image with a browser-displayable URL (EC-212, EC-459). */\nfunction isResolvableImage(record: AssetRecord | undefined): record is AssetRecord & { url: string } {\n return (\n record !== undefined &&\n record.contentType.startsWith('image/') &&\n isDisplayableAssetUrl(record.url)\n )\n}\n\n/**\n * The best studio thumbnail src for an image record (EC-400): prefer the stored\n * downscaled `thumb` variant when it is browser-displayable, else fall back to\n * the full original `url` (EC-459: `http(s)`, `blob:`, and `data:` all qualify —\n * an opaque backend locator like `memory://` does not). A missing/broken thumb\n * therefore degrades to the original — never a broken tile (scope §4). Non-image\n * / unresolvable records yield `undefined` (the icon placeholder). Focal-point\n * editing + the canvas keep the original elsewhere (delivery fidelity) — this\n * preference is thumbnail-only.\n */\nfunction thumbnailSrc(record: AssetRecord | undefined): string | undefined {\n if (!record || !record.contentType.startsWith('image/')) return undefined\n const thumbUrl = record.thumb?.url\n if (isDisplayableAssetUrl(thumbUrl)) return thumbUrl\n if (isDisplayableAssetUrl(record.url)) return record.url\n return undefined\n}\n\n/**\n * A read-only image thumbnail honouring the asset's focal point (EC-248): the\n * preview crops with the same `object-position` delivery uses (EC-230), so the\n * editor sees exactly what a focal-point change does. Prefers the EC-400\n * downscaled `thumb` variant (light egress) and falls back to the original.\n * Non-image / unresolvable assets fall back to the labelled icon, matching the\n * table cell's preview.\n */\nexport function AssetThumb({\n record,\n className,\n alt,\n}: {\n record: AssetRecord | undefined\n className?: string\n alt?: string\n}) {\n const src = thumbnailSrc(record)\n if (record && src) {\n return (\n <img\n src={src}\n alt={alt ?? record.alt ?? record.title ?? record.filename}\n loading=\"lazy\"\n style={{ objectPosition: focalPointObjectPosition(record.focalPoint) }}\n className={cn('rounded border border-border bg-muted object-cover', className)}\n />\n )\n }\n return (\n <div\n className={cn(\n 'flex items-center justify-center rounded border border-border bg-muted text-muted-foreground',\n className,\n )}\n >\n <Image className=\"size-4\" aria-hidden />\n </div>\n )\n}\n\n/**\n * The focal-point picker (EC-248/EC-301): click or **drag** anywhere on the image\n * to place the normalized 0–1 hotspot delivery threads to `object-position`\n * (EC-230). The marker follows the pointer live while dragging and only commits on\n * release (`onPreview` updates the marker, `onChange` commits). `onChange(null)`\n * resets to centre. Shows a placeholder until the image loads; renders a muted note\n * when the asset has no resolvable image.\n */\nexport function FocalPointPicker({\n record,\n point,\n onChange,\n onPreview,\n disabled,\n}: {\n record: AssetRecord | undefined\n point: FocalPoint | undefined\n onChange: (point: FocalPoint | null) => void\n /** Called continuously while dragging to move the marker without committing. */\n onPreview?: (point: FocalPoint) => void\n disabled?: boolean\n}) {\n const marker = point ?? CENTER_FOCAL_POINT\n const dragging = useRef(false)\n const [loaded, setLoaded] = useState(false)\n\n if (!isResolvableImage(record)) {\n return (\n <Text size=\"xs\" tone=\"subtle\">\n {translate('assets.focalPoint.noPreview')}\n </Text>\n )\n }\n\n const pointFrom = (e: React.PointerEvent<HTMLElement>): FocalPoint =>\n focalPointFromPoint(e.currentTarget.getBoundingClientRect(), e.clientX, e.clientY)\n\n function onPointerDown(e: React.PointerEvent<HTMLButtonElement>) {\n if (disabled) return\n dragging.current = true\n e.currentTarget.setPointerCapture(e.pointerId)\n ;(onPreview ?? onChange)(pointFrom(e))\n }\n function onPointerMove(e: React.PointerEvent<HTMLButtonElement>) {\n if (disabled || !dragging.current) return\n ;(onPreview ?? onChange)(pointFrom(e))\n }\n function onPointerUp(e: React.PointerEvent<HTMLButtonElement>) {\n if (disabled || !dragging.current) return\n dragging.current = false\n e.currentTarget.releasePointerCapture(e.pointerId)\n onChange(pointFrom(e)) // commit on release only\n }\n\n return (\n <button\n type=\"button\"\n aria-label={translate('assets.focalPoint.setLabel')}\n disabled={disabled}\n onPointerDown={onPointerDown}\n onPointerMove={onPointerMove}\n onPointerUp={onPointerUp}\n className={cn(\n 'relative block w-full touch-none select-none overflow-hidden rounded border border-border bg-muted',\n 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/40',\n disabled ? 'cursor-default opacity-60' : 'cursor-crosshair',\n )}\n >\n {/* Width-constrained, natural aspect (no object-fit), so the button wraps\n the image tightly and the bounding rect equals the image box — the\n normalized pointer coordinates are then exact, with no letterbox offset. */}\n <img\n src={record.url}\n alt={record.alt ?? record.filename}\n draggable={false}\n onLoad={() => setLoaded(true)}\n className={cn('block h-auto w-full select-none transition-opacity', loaded ? 'opacity-100' : 'opacity-0')}\n />\n {!loaded ? (\n <span className=\"absolute inset-0 grid aspect-video place-items-center bg-muted\">\n <Image className=\"size-6 animate-pulse text-muted-foreground\" aria-hidden />\n </span>\n ) : (\n <span\n aria-hidden\n className=\"pointer-events-none absolute size-4 -translate-x-1/2 -translate-y-1/2 rounded-full border-2 border-white shadow-[0_0_0_1.5px_rgba(0,0,0,0.55)] ring-1 ring-black/40\"\n style={{ left: `${marker.x * 100}%`, top: `${marker.y * 100}%` }}\n />\n )}\n </button>\n )\n}\n\n/** Common crop aspect ratios shown as focal-aware previews (EC-301). */\nconst FOCAL_CROP_RATIOS = [\n { label: '9:16', className: 'aspect-[9/16]' },\n { label: '3:4', className: 'aspect-[3/4]' },\n { label: '1:1', className: 'aspect-square' },\n { label: '4:3', className: 'aspect-[4/3]' },\n { label: '16:9', className: 'aspect-video' },\n] as const\n\n/**\n * A row of common-ratio crop previews (EC-301), each cropping the image with the\n * same `object-position` delivery uses — so the editor sees how the focal point\n * holds the subject across portrait/square/landscape crops. Updates live as the\n * point is dragged.\n */\nexport function FocalCropPreviews({\n record,\n point,\n}: {\n record: AssetRecord | undefined\n point: FocalPoint | undefined\n}) {\n if (!isResolvableImage(record)) return null\n const objectPosition = focalPointObjectPosition(point)\n return (\n <div className=\"flex items-end gap-2\">\n {FOCAL_CROP_RATIOS.map((ratio) => (\n <div key={ratio.label} className=\"flex min-w-0 flex-col items-center gap-1\">\n <div className={cn('h-14 overflow-hidden rounded border border-border bg-muted', ratio.className)}>\n <img\n src={record.url}\n alt=\"\"\n draggable={false}\n style={{ objectPosition }}\n className=\"h-full w-full object-cover\"\n />\n </div>\n <Text size=\"xs\" tone=\"subtle\">\n {ratio.label}\n </Text>\n </div>\n ))}\n </div>\n )\n}\n"]}
|
|
1
|
+
{"version":3,"file":"asset-thumb.js","sourceRoot":"","sources":["../../../src/components/assets/asset-thumb.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useMemo, useRef, useState } from 'react'\nimport { Image } from 'lucide-react'\nimport { assetAlt } from '@elytracms/core/persistence'\nimport type { AssetRecord } from '@elytracms/core/persistence'\nimport { Text } from '@elytracms/studio/ui'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { cn } from '@elytracms/studio/lib/utils'\nimport { useOptionalAssets } from './assets-store'\nimport { isDisplayableAssetUrl } from '@elytracms/studio/lib/assets'\nimport {\n CENTER_FOCAL_POINT,\n focalPointFromPoint,\n focalPointObjectPosition,\n} from './focal-point'\nimport type { FocalPoint } from './focal-point'\n\n/** Resolve an asset record by id from the ambient library, if one is mounted. */\nexport function useAssetRecord(assetId: string | null | undefined): AssetRecord | undefined {\n const store = useOptionalAssets()\n return useMemo(() => {\n if (!assetId || !store || store.status !== 'ready') return undefined\n return store.assets.find((a) => a.id === assetId)\n }, [assetId, store])\n}\n\n/** True when the record is an image with a browser-displayable URL (EC-212, EC-459). */\nfunction isResolvableImage(record: AssetRecord | undefined): record is AssetRecord & { url: string } {\n return (\n record !== undefined &&\n record.contentType.startsWith('image/') &&\n isDisplayableAssetUrl(record.url)\n )\n}\n\n/**\n * The best studio thumbnail src for an image record (EC-400): prefer the stored\n * downscaled `thumb` variant when it is browser-displayable, else fall back to\n * the full original `url` (EC-459: `http(s)`, `blob:`, and `data:` all qualify —\n * an opaque backend locator like `memory://` does not). A missing/broken thumb\n * therefore degrades to the original — never a broken tile (scope §4). Non-image\n * / unresolvable records yield `undefined` (the icon placeholder). Focal-point\n * editing + the canvas keep the original elsewhere (delivery fidelity) — this\n * preference is thumbnail-only.\n */\nfunction thumbnailSrc(record: AssetRecord | undefined): string | undefined {\n if (!record || !record.contentType.startsWith('image/')) return undefined\n const thumbUrl = record.thumb?.url\n if (isDisplayableAssetUrl(thumbUrl)) return thumbUrl\n if (isDisplayableAssetUrl(record.url)) return record.url\n return undefined\n}\n\n/**\n * A read-only image thumbnail honouring the asset's focal point (EC-248): the\n * preview crops with the same `object-position` delivery uses (EC-230), so the\n * editor sees exactly what a focal-point change does. Prefers the EC-400\n * downscaled `thumb` variant (light egress) and falls back to the original.\n * Non-image / unresolvable assets fall back to the labelled icon, matching the\n * table cell's preview.\n */\nexport function AssetThumb({\n record,\n className,\n alt,\n}: {\n record: AssetRecord | undefined\n className?: string\n alt?: string\n}) {\n const src = thumbnailSrc(record)\n if (record && src) {\n return (\n <img\n src={src}\n alt={alt ?? assetAlt(record) ?? record.title ?? record.filename}\n loading=\"lazy\"\n style={{ objectPosition: focalPointObjectPosition(record.focalPoint) }}\n className={cn('rounded border border-border bg-muted object-cover', className)}\n />\n )\n }\n return (\n <div\n className={cn(\n 'flex items-center justify-center rounded border border-border bg-muted text-muted-foreground',\n className,\n )}\n >\n <Image className=\"size-4\" aria-hidden />\n </div>\n )\n}\n\n/**\n * The focal-point picker (EC-248/EC-301): click or **drag** anywhere on the image\n * to place the normalized 0–1 hotspot delivery threads to `object-position`\n * (EC-230). The marker follows the pointer live while dragging and only commits on\n * release (`onPreview` updates the marker, `onChange` commits). `onChange(null)`\n * resets to centre. Shows a placeholder until the image loads; renders a muted note\n * when the asset has no resolvable image.\n */\nexport function FocalPointPicker({\n record,\n point,\n onChange,\n onPreview,\n disabled,\n}: {\n record: AssetRecord | undefined\n point: FocalPoint | undefined\n onChange: (point: FocalPoint | null) => void\n /** Called continuously while dragging to move the marker without committing. */\n onPreview?: (point: FocalPoint) => void\n disabled?: boolean\n}) {\n const marker = point ?? CENTER_FOCAL_POINT\n const dragging = useRef(false)\n const [loaded, setLoaded] = useState(false)\n\n if (!isResolvableImage(record)) {\n return (\n <Text size=\"xs\" tone=\"subtle\">\n {translate('assets.focalPoint.noPreview')}\n </Text>\n )\n }\n\n const pointFrom = (e: React.PointerEvent<HTMLElement>): FocalPoint =>\n focalPointFromPoint(e.currentTarget.getBoundingClientRect(), e.clientX, e.clientY)\n\n function onPointerDown(e: React.PointerEvent<HTMLButtonElement>) {\n if (disabled) return\n dragging.current = true\n e.currentTarget.setPointerCapture(e.pointerId)\n ;(onPreview ?? onChange)(pointFrom(e))\n }\n function onPointerMove(e: React.PointerEvent<HTMLButtonElement>) {\n if (disabled || !dragging.current) return\n ;(onPreview ?? onChange)(pointFrom(e))\n }\n function onPointerUp(e: React.PointerEvent<HTMLButtonElement>) {\n if (disabled || !dragging.current) return\n dragging.current = false\n e.currentTarget.releasePointerCapture(e.pointerId)\n onChange(pointFrom(e)) // commit on release only\n }\n\n return (\n <button\n type=\"button\"\n aria-label={translate('assets.focalPoint.setLabel')}\n disabled={disabled}\n onPointerDown={onPointerDown}\n onPointerMove={onPointerMove}\n onPointerUp={onPointerUp}\n className={cn(\n 'relative block w-full touch-none select-none overflow-hidden rounded border border-border bg-muted',\n 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/40',\n disabled ? 'cursor-default opacity-60' : 'cursor-crosshair',\n )}\n >\n {/* Width-constrained, natural aspect (no object-fit), so the button wraps\n the image tightly and the bounding rect equals the image box — the\n normalized pointer coordinates are then exact, with no letterbox offset. */}\n <img\n src={record.url}\n alt={assetAlt(record) ?? record.filename}\n draggable={false}\n onLoad={() => setLoaded(true)}\n className={cn('block h-auto w-full select-none transition-opacity', loaded ? 'opacity-100' : 'opacity-0')}\n />\n {!loaded ? (\n <span className=\"absolute inset-0 grid aspect-video place-items-center bg-muted\">\n <Image className=\"size-6 animate-pulse text-muted-foreground\" aria-hidden />\n </span>\n ) : (\n <span\n aria-hidden\n className=\"pointer-events-none absolute size-4 -translate-x-1/2 -translate-y-1/2 rounded-full border-2 border-white shadow-[0_0_0_1.5px_rgba(0,0,0,0.55)] ring-1 ring-black/40\"\n style={{ left: `${marker.x * 100}%`, top: `${marker.y * 100}%` }}\n />\n )}\n </button>\n )\n}\n\n/** Common crop aspect ratios shown as focal-aware previews (EC-301). */\nconst FOCAL_CROP_RATIOS = [\n { label: '9:16', className: 'aspect-[9/16]' },\n { label: '3:4', className: 'aspect-[3/4]' },\n { label: '1:1', className: 'aspect-square' },\n { label: '4:3', className: 'aspect-[4/3]' },\n { label: '16:9', className: 'aspect-video' },\n] as const\n\n/**\n * A row of common-ratio crop previews (EC-301), each cropping the image with the\n * same `object-position` delivery uses — so the editor sees how the focal point\n * holds the subject across portrait/square/landscape crops. Updates live as the\n * point is dragged.\n */\nexport function FocalCropPreviews({\n record,\n point,\n}: {\n record: AssetRecord | undefined\n point: FocalPoint | undefined\n}) {\n if (!isResolvableImage(record)) return null\n const objectPosition = focalPointObjectPosition(point)\n return (\n <div className=\"flex items-end gap-2\">\n {FOCAL_CROP_RATIOS.map((ratio) => (\n <div key={ratio.label} className=\"flex min-w-0 flex-col items-center gap-1\">\n <div className={cn('h-14 overflow-hidden rounded border border-border bg-muted', ratio.className)}>\n <img\n src={record.url}\n alt=\"\"\n draggable={false}\n style={{ objectPosition }}\n className=\"h-full w-full object-cover\"\n />\n </div>\n <Text size=\"xs\" tone=\"subtle\">\n {ratio.label}\n </Text>\n </div>\n ))}\n </div>\n )\n}\n"]}
|
|
@@ -29,9 +29,9 @@ export declare const ASSET_COLLECTION_CAPABILITIES: CollectionViewCapabilities;
|
|
|
29
29
|
* schema or not, so a save hands the bag back whole (D22). Declared fields then
|
|
30
30
|
* take their canonical reading on top — a column never loses to a same-named meta
|
|
31
31
|
* key: a system column (`filename`, `url`, dimensions, …) reads straight off the
|
|
32
|
-
* record, and `alt
|
|
33
|
-
*
|
|
34
|
-
*
|
|
32
|
+
* record, and `alt` reads through THE reader (`assetAlt`, EC-587 — `meta.alt`, with a
|
|
33
|
+
* not-yet-migrated legacy column lifted in), exactly what delivery and the image
|
|
34
|
+
* field's options dialog show.
|
|
35
35
|
*/
|
|
36
36
|
export declare function assetToDocument(collection: CollectionDef, record: AssetRecord): CmsDocument;
|
|
37
37
|
/**
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useCallback, useMemo } from 'react';
|
|
2
2
|
import { createCollectionRegistry, validateDocument } from '@elytracms/core/cms-core';
|
|
3
|
+
import { assetAlt } from '@elytracms/core/persistence';
|
|
3
4
|
import { studioPath } from '@elytracms/studio/router';
|
|
4
5
|
import { useAssets } from './assets-store.js';
|
|
5
6
|
import { SYSTEM_ASSET_FIELDS, pruneMeta } from './asset-metadata.js';
|
|
@@ -73,16 +74,19 @@ function systemFieldValue(record, name) {
|
|
|
73
74
|
* schema or not, so a save hands the bag back whole (D22). Declared fields then
|
|
74
75
|
* take their canonical reading on top — a column never loses to a same-named meta
|
|
75
76
|
* key: a system column (`filename`, `url`, dimensions, …) reads straight off the
|
|
76
|
-
* record, and `alt
|
|
77
|
-
*
|
|
78
|
-
*
|
|
77
|
+
* record, and `alt` reads through THE reader (`assetAlt`, EC-587 — `meta.alt`, with a
|
|
78
|
+
* not-yet-migrated legacy column lifted in), exactly what delivery and the image
|
|
79
|
+
* field's options dialog show.
|
|
79
80
|
*/
|
|
80
81
|
export function assetToDocument(collection, record) {
|
|
81
82
|
const values = { ...(record.meta ?? {}) };
|
|
82
83
|
for (const field of collection.fields) {
|
|
83
84
|
if (field.name === 'alt') {
|
|
84
|
-
const
|
|
85
|
-
|
|
85
|
+
const alt = assetAlt(record);
|
|
86
|
+
if (alt !== null)
|
|
87
|
+
values.alt = alt;
|
|
88
|
+
else
|
|
89
|
+
delete values.alt;
|
|
86
90
|
}
|
|
87
91
|
else if (SYSTEM_ASSET_FIELDS.has(field.name)) {
|
|
88
92
|
values[field.name] = systemFieldValue(record, field.name);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"assets-collection-view-source.js","sourceRoot":"","sources":["../../../src/components/assets/assets-collection-view-source.ts"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useMemo } from 'react'\nimport type { CmsDocument, CollectionDef, DocumentRef, Locale, LocaleConfig } from '@elytracms/core/cms-core'\nimport { createCollectionRegistry, validateDocument } from '@elytracms/core/cms-core'\nimport type { AssetRecord, ReferenceTarget } 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, pruneMeta } from './asset-metadata'\nimport { metaBagProjection, normalizeOkResult, useRailViewSource } from '../rail-view-source'\nimport { parseLocaleConfig } from '@elytracms/core/cms-core'\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 = parseLocaleConfig({\n default: ASSETS_LOCALE,\n locales: [ASSETS_LOCALE],\n})\n\n/**\n * The capability profile of the assets rail (EC-319, EC-433). Hoisted to a\n * module-level constant (no prop/state dependency) so it's both the value the\n * hook returns AND independently unit-testable.\n *\n * `create` is off — a new asset comes from an upload, never a blank draft.\n * `remove` + `bulk` are ON since EC-433: the list's row/bulk delete used to be\n * off deliberately because the generic `ReferenceConfirmDialog` enumerated\n * inbound references as `{ type: 'document' }` targets — the wrong index shape\n * for an asset — which would have shown \"not referenced\" for an asset that IS\n * referenced. That pre-flight now queries the shape THIS source declares via\n * {@link assetReferenceTarget} (`{ type: 'asset' }`, the same lookup the\n * detail surface runs), and the confirmed delete dispatches the SAME checked\n * store delete the detail surface uses (`deleteAsset`: original + EC-400 thumb\n * blobs, then the record — EC-420 lqips ride on the record). Nothing else\n * applies to assets: no publish/versions/locales/inline-edit/live-URL/\n * hierarchy/import-export.\n */\nexport const ASSET_COLLECTION_CAPABILITIES: CollectionViewCapabilities = {\n publish: false,\n versions: false,\n locales: false,\n inlineEdit: false,\n create: false,\n duplicate: false, // EC-442: blob duplication is a different feature, out of scope\n remove: true,\n bulk: true,\n liveUrl: false,\n hierarchy: false,\n importExport: false,\n adminOnly: false,\n}\n\n/** Read one of the record's fixed, upload-managed columns by field name. */\nfunction systemFieldValue(record: AssetRecord, name: string): unknown {\n switch (name) {\n case 'filename':\n return record.filename\n case 'url':\n return record.url\n case 'contentType':\n return record.contentType\n case 'byteSize':\n return record.byteSize\n case 'width':\n return record.width\n case 'height':\n return record.height\n case 'storageKey':\n return record.storageKey\n case 'title':\n return record.title\n default:\n return undefined\n }\n}\n\n/**\n * Project a stored asset record into the lens's `values` shape (EC-319) — the\n * value-keyed projection the redirects/users rails use (EC-577, RESIDUE-3d): EVERY\n * key of the record's open `meta` bag (EC-301) rides in `values`, declared by the\n * schema or not, so a save hands the bag back whole (D22). Declared fields then\n * take their canonical reading on top — a column never loses to a same-named meta\n * key: a system column (`filename`, `url`, dimensions, …) reads straight off the\n * record, and `alt`, schema-driven but ALSO a legacy record column, reads the meta\n * value falling back to the column (matches delivery's `meta.alt ?? record.alt`\n * read and the image field's options dialog).\n */\nexport function assetToDocument(collection: CollectionDef, record: AssetRecord): CmsDocument {\n const values: Record<string, unknown> = { ...(record.meta ?? {}) }\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 } else if (SYSTEM_ASSET_FIELDS.has(field.name)) {\n values[field.name] = systemFieldValue(record, field.name)\n }\n }\n return { collection: collection.id, id: record.id, values }\n}\n\n/**\n * The inverse of {@link assetToDocument}: the meta bag a save writes back — every\n * value outside the system set (EC-301/EC-311), declared or not, through the ONE\n * value-keyed projection the users/redirects rails share (EC-577, RESIDUE-3d).\n * `meta` is written whole (it replaces the stored bag), so a key the lens never\n * declared must be in it: the schema-driven projection this replaced dropped them\n * on every rail save (D22), while the options dialog and asset detail kept them.\n * System columns are upload-managed and never written here.\n */\nexport function documentToAssetMetaPatch(doc: CmsDocument): Record<string, unknown> {\n return pruneMeta(metaBagProjection(doc.values ?? {}, SYSTEM_ASSET_FIELDS))\n}\n\n/** `create` when `capabilities.create === false` — never reached from the UI\n * (list/detail never call it); an explicit throw rather than a silent no-op. */\nfunction createUnsupported(): DocumentRef {\n throw new Error('assets are created by upload, not the assets rail')\n}\n\n/** Assets' `remove()` returns an `outcome`-discriminated {@link DeleteAssetResult}\n * (not the `{ ok }` shape users/redirects use, EC-327). `'error'` surfaces as-is;\n * `'blocked'` (the unconfirmed usage gate fired — nothing was removed) surfaces\n * as an explicit failure too (EC-433): a delete that left the asset in place\n * must NEVER look like success. It is unreachable from this rail's UI — every\n * delete path confirms through the reference dialog and dispatches with\n * `confirmInUse` — but stays honest for any direct caller. `'not-found'` maps\n * to success (idempotent delete: the desired end state — gone — holds). */\nexport function normalizeAssetRemoveResult(result: DeleteAssetResult): OperationError | null {\n if (result.outcome === 'error') return { code: 'unknown', message: result.message }\n if (result.outcome === 'blocked') {\n return {\n code: 'unknown',\n message: `Asset is referenced in ${result.usageCount} ${result.usageCount === 1 ? 'place' : 'places'} and was not deleted.`,\n }\n }\n return null\n}\n\n/**\n * How an asset row is addressed in the EC-155 reference index (EC-433):\n * `{ type: 'asset' }`, never the document shape — so the delete confirmation's\n * inbound pre-flight counts the SAME references `loadInboundAssetReferenceViews`\n * (the detail surface's check) does.\n */\nexport function assetReferenceTarget(ref: DocumentRef): ReferenceTarget {\n return { type: 'asset', id: ref.id }\n}\n\n/**\n * Assemble a {@link CollectionViewSource} for the **assets rail** (EC-319) — the\n * portable list surface over the existing asset library store instead of a\n * bespoke listing. `schema` is the DEPLOYMENT-resolved `kind:'asset'` collection\n * (EC-311: a project's own declared fields, or a mount's `nav.assets({ fields })`\n * override merged over it — see `resolveAssetsSchema` in `app-views/default-modules.tsx`),\n * never a hardcoded engine default — so a project's `copyright`/`credit`/… fields\n * (and any EC-311 override) drive the list's columns/filters exactly as they\n * drive the image field's options dialog.\n *\n * Must be mounted under an `<AssetsProvider>` (for {@link useAssets}) — already\n * ambient at the project level (EC-195), so callers rarely mount their own.\n *\n * The shared scaffold (`useRailViewSource`, EC-327) owns `viewState`/`lastError`\n * plumbing and the assembled source; this hook supplies only the asset-store\n * reads/writes, the capability profile, and the projections above.\n */\nexport function useAssetsCollectionViewSource(\n projectId: string,\n schema: CollectionDef,\n): CollectionViewSource {\n const store = useAssets()\n const registry = useMemo(() => createCollectionRegistry([schema]), [schema])\n\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(doc) }),\n [store],\n )\n // The unconfirmed delete (the store's usage gate still applies — a referenced\n // asset comes back `blocked`, surfaced via `lastError`). The UI's delete paths\n // never land here: row/bulk deletes confirm through the reference dialog and\n // dispatch `bulkRemove` below.\n const remove = useCallback((ref: DocumentRef) => store.remove(ref.id), [store])\n // The CONFIRMED delete the bulk/row path dispatches (EC-433): the reference\n // dialog has already enumerated inbound usages (asset-shaped, see\n // `assetReferenceTarget`), so this passes `confirmInUse` exactly like the\n // detail surface's confirmed delete — same blob + thumb + cache handling.\n const bulkRemove = useCallback(\n (ref: DocumentRef) => store.remove(ref.id, { confirmInUse: true }),\n [store],\n )\n\n return useRailViewSource({\n projectId,\n schema,\n capabilities: ASSET_COLLECTION_CAPABILITIES,\n localeConfig: 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 // EC-433: the delete confirmation must enumerate ASSET-shaped inbound\n // references — the document-shaped default would report \"not referenced\"\n // for a referenced asset.\n referenceTarget: assetReferenceTarget,\n validate,\n create: createUnsupported,\n save,\n remove,\n bulkRemove,\n normalizeSaveResult: normalizeOkResult,\n normalizeRemoveResult: normalizeAssetRemoveResult,\n })\n}\n"]}
|
|
1
|
+
{"version":3,"file":"assets-collection-view-source.js","sourceRoot":"","sources":["../../../src/components/assets/assets-collection-view-source.ts"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useMemo } from 'react'\nimport type { CmsDocument, CollectionDef, DocumentRef, Locale, LocaleConfig } from '@elytracms/core/cms-core'\nimport { createCollectionRegistry, validateDocument } from '@elytracms/core/cms-core'\nimport { assetAlt } from '@elytracms/core/persistence'\nimport type { AssetRecord, ReferenceTarget } 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, pruneMeta } from './asset-metadata'\nimport { metaBagProjection, normalizeOkResult, useRailViewSource } from '../rail-view-source'\nimport { parseLocaleConfig } from '@elytracms/core/cms-core'\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 = parseLocaleConfig({\n default: ASSETS_LOCALE,\n locales: [ASSETS_LOCALE],\n})\n\n/**\n * The capability profile of the assets rail (EC-319, EC-433). Hoisted to a\n * module-level constant (no prop/state dependency) so it's both the value the\n * hook returns AND independently unit-testable.\n *\n * `create` is off — a new asset comes from an upload, never a blank draft.\n * `remove` + `bulk` are ON since EC-433: the list's row/bulk delete used to be\n * off deliberately because the generic `ReferenceConfirmDialog` enumerated\n * inbound references as `{ type: 'document' }` targets — the wrong index shape\n * for an asset — which would have shown \"not referenced\" for an asset that IS\n * referenced. That pre-flight now queries the shape THIS source declares via\n * {@link assetReferenceTarget} (`{ type: 'asset' }`, the same lookup the\n * detail surface runs), and the confirmed delete dispatches the SAME checked\n * store delete the detail surface uses (`deleteAsset`: original + EC-400 thumb\n * blobs, then the record — EC-420 lqips ride on the record). Nothing else\n * applies to assets: no publish/versions/locales/inline-edit/live-URL/\n * hierarchy/import-export.\n */\nexport const ASSET_COLLECTION_CAPABILITIES: CollectionViewCapabilities = {\n publish: false,\n versions: false,\n locales: false,\n inlineEdit: false,\n create: false,\n duplicate: false, // EC-442: blob duplication is a different feature, out of scope\n remove: true,\n bulk: true,\n liveUrl: false,\n hierarchy: false,\n importExport: false,\n adminOnly: false,\n}\n\n/** Read one of the record's fixed, upload-managed columns by field name. */\nfunction systemFieldValue(record: AssetRecord, name: string): unknown {\n switch (name) {\n case 'filename':\n return record.filename\n case 'url':\n return record.url\n case 'contentType':\n return record.contentType\n case 'byteSize':\n return record.byteSize\n case 'width':\n return record.width\n case 'height':\n return record.height\n case 'storageKey':\n return record.storageKey\n case 'title':\n return record.title\n default:\n return undefined\n }\n}\n\n/**\n * Project a stored asset record into the lens's `values` shape (EC-319) — the\n * value-keyed projection the redirects/users rails use (EC-577, RESIDUE-3d): EVERY\n * key of the record's open `meta` bag (EC-301) rides in `values`, declared by the\n * schema or not, so a save hands the bag back whole (D22). Declared fields then\n * take their canonical reading on top — a column never loses to a same-named meta\n * key: a system column (`filename`, `url`, dimensions, …) reads straight off the\n * record, and `alt` reads through THE reader (`assetAlt`, EC-587 — `meta.alt`, with a\n * not-yet-migrated legacy column lifted in), exactly what delivery and the image\n * field's options dialog show.\n */\nexport function assetToDocument(collection: CollectionDef, record: AssetRecord): CmsDocument {\n const values: Record<string, unknown> = { ...(record.meta ?? {}) }\n for (const field of collection.fields) {\n if (field.name === 'alt') {\n const alt = assetAlt(record)\n if (alt !== null) values.alt = alt\n else delete values.alt\n } else if (SYSTEM_ASSET_FIELDS.has(field.name)) {\n values[field.name] = systemFieldValue(record, field.name)\n }\n }\n return { collection: collection.id, id: record.id, values }\n}\n\n/**\n * The inverse of {@link assetToDocument}: the meta bag a save writes back — every\n * value outside the system set (EC-301/EC-311), declared or not, through the ONE\n * value-keyed projection the users/redirects rails share (EC-577, RESIDUE-3d).\n * `meta` is written whole (it replaces the stored bag), so a key the lens never\n * declared must be in it: the schema-driven projection this replaced dropped them\n * on every rail save (D22), while the options dialog and asset detail kept them.\n * System columns are upload-managed and never written here.\n */\nexport function documentToAssetMetaPatch(doc: CmsDocument): Record<string, unknown> {\n return pruneMeta(metaBagProjection(doc.values ?? {}, SYSTEM_ASSET_FIELDS))\n}\n\n/** `create` when `capabilities.create === false` — never reached from the UI\n * (list/detail never call it); an explicit throw rather than a silent no-op. */\nfunction createUnsupported(): DocumentRef {\n throw new Error('assets are created by upload, not the assets rail')\n}\n\n/** Assets' `remove()` returns an `outcome`-discriminated {@link DeleteAssetResult}\n * (not the `{ ok }` shape users/redirects use, EC-327). `'error'` surfaces as-is;\n * `'blocked'` (the unconfirmed usage gate fired — nothing was removed) surfaces\n * as an explicit failure too (EC-433): a delete that left the asset in place\n * must NEVER look like success. It is unreachable from this rail's UI — every\n * delete path confirms through the reference dialog and dispatches with\n * `confirmInUse` — but stays honest for any direct caller. `'not-found'` maps\n * to success (idempotent delete: the desired end state — gone — holds). */\nexport function normalizeAssetRemoveResult(result: DeleteAssetResult): OperationError | null {\n if (result.outcome === 'error') return { code: 'unknown', message: result.message }\n if (result.outcome === 'blocked') {\n return {\n code: 'unknown',\n message: `Asset is referenced in ${result.usageCount} ${result.usageCount === 1 ? 'place' : 'places'} and was not deleted.`,\n }\n }\n return null\n}\n\n/**\n * How an asset row is addressed in the EC-155 reference index (EC-433):\n * `{ type: 'asset' }`, never the document shape — so the delete confirmation's\n * inbound pre-flight counts the SAME references `loadInboundAssetReferenceViews`\n * (the detail surface's check) does.\n */\nexport function assetReferenceTarget(ref: DocumentRef): ReferenceTarget {\n return { type: 'asset', id: ref.id }\n}\n\n/**\n * Assemble a {@link CollectionViewSource} for the **assets rail** (EC-319) — the\n * portable list surface over the existing asset library store instead of a\n * bespoke listing. `schema` is the DEPLOYMENT-resolved `kind:'asset'` collection\n * (EC-311: a project's own declared fields, or a mount's `nav.assets({ fields })`\n * override merged over it — see `resolveAssetsSchema` in `app-views/default-modules.tsx`),\n * never a hardcoded engine default — so a project's `copyright`/`credit`/… fields\n * (and any EC-311 override) drive the list's columns/filters exactly as they\n * drive the image field's options dialog.\n *\n * Must be mounted under an `<AssetsProvider>` (for {@link useAssets}) — already\n * ambient at the project level (EC-195), so callers rarely mount their own.\n *\n * The shared scaffold (`useRailViewSource`, EC-327) owns `viewState`/`lastError`\n * plumbing and the assembled source; this hook supplies only the asset-store\n * reads/writes, the capability profile, and the projections above.\n */\nexport function useAssetsCollectionViewSource(\n projectId: string,\n schema: CollectionDef,\n): CollectionViewSource {\n const store = useAssets()\n const registry = useMemo(() => createCollectionRegistry([schema]), [schema])\n\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(doc) }),\n [store],\n )\n // The unconfirmed delete (the store's usage gate still applies — a referenced\n // asset comes back `blocked`, surfaced via `lastError`). The UI's delete paths\n // never land here: row/bulk deletes confirm through the reference dialog and\n // dispatch `bulkRemove` below.\n const remove = useCallback((ref: DocumentRef) => store.remove(ref.id), [store])\n // The CONFIRMED delete the bulk/row path dispatches (EC-433): the reference\n // dialog has already enumerated inbound usages (asset-shaped, see\n // `assetReferenceTarget`), so this passes `confirmInUse` exactly like the\n // detail surface's confirmed delete — same blob + thumb + cache handling.\n const bulkRemove = useCallback(\n (ref: DocumentRef) => store.remove(ref.id, { confirmInUse: true }),\n [store],\n )\n\n return useRailViewSource({\n projectId,\n schema,\n capabilities: ASSET_COLLECTION_CAPABILITIES,\n localeConfig: 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 // EC-433: the delete confirmation must enumerate ASSET-shaped inbound\n // references — the document-shaped default would report \"not referenced\"\n // for a referenced asset.\n referenceTarget: assetReferenceTarget,\n validate,\n create: createUnsupported,\n save,\n remove,\n bulkRemove,\n normalizeSaveResult: normalizeOkResult,\n normalizeRemoveResult: normalizeAssetRemoveResult,\n })\n}\n"]}
|
|
@@ -46,6 +46,12 @@ export interface AssetsStore {
|
|
|
46
46
|
}): Promise<DeleteAssetResult>;
|
|
47
47
|
/** Edit alt/title (EC-152); persists through the operations asset facade. */
|
|
48
48
|
updateMetadata(assetId: string, patch: AssetMetadataPatch): Promise<UpdateAssetMetadataResult>;
|
|
49
|
+
/**
|
|
50
|
+
* EC-568 — replace the file inside an asset: the same id, the new bytes, everything
|
|
51
|
+
* derived regenerated through THE upload path, the file's embedded metadata over
|
|
52
|
+
* `meta` per key (else kept), the focal point kept.
|
|
53
|
+
*/
|
|
54
|
+
replaceFile(assetId: string, file: UploadableFile): Promise<ReplaceFileResult>;
|
|
49
55
|
/**
|
|
50
56
|
* Backfill downscaled previews onto existing images (EC-400 AD-1). Sequential,
|
|
51
57
|
* idempotent, cancellable; each patch flows through the operations facade and
|
|
@@ -58,6 +64,14 @@ export interface AssetsStore {
|
|
|
58
64
|
}): Promise<BackfillProgress>;
|
|
59
65
|
refresh(): Promise<void>;
|
|
60
66
|
}
|
|
67
|
+
/** The outcome of {@link AssetsStore.replaceFile}. */
|
|
68
|
+
export type ReplaceFileResult = {
|
|
69
|
+
ok: true;
|
|
70
|
+
asset: AssetRecord;
|
|
71
|
+
} | {
|
|
72
|
+
ok: false;
|
|
73
|
+
message: string;
|
|
74
|
+
};
|
|
61
75
|
declare const useAssets: () => AssetsStore;
|
|
62
76
|
export declare function AssetsProvider({ projectId, children, }: {
|
|
63
77
|
projectId: string;
|
|
@@ -3,6 +3,7 @@ import { useCallback, useContext, useMemo, useRef, useState } from 'react';
|
|
|
3
3
|
import { deleteAsset, runVariantBackfill, runUpload, xhrUploadTransport } from '@elytracms/studio/lib/assets';
|
|
4
4
|
import { loadInboundAssetReferenceViews } from '@elytracms/studio/lib/references';
|
|
5
5
|
import { getStudioOperations, operationsAssetRepository } from '@elytracms/studio/lib/operations';
|
|
6
|
+
import { projectCollections } from '@elytracms/studio/lib/operations/project-runtime-config';
|
|
6
7
|
import { listStoreState, useOperationCache, useOperationQuery } from '@elytracms/studio/lib/query';
|
|
7
8
|
import { createRequiredContext } from '@elytracms/studio/lib/required-context';
|
|
8
9
|
import { updateAssetMetadata } from './asset-metadata.js';
|
|
@@ -27,6 +28,9 @@ export function AssetsProvider({ projectId, children, }) {
|
|
|
27
28
|
const landedRef = useRef(new Map());
|
|
28
29
|
const uploadSeq = useRef(0);
|
|
29
30
|
const [uploads, setUploads] = useState([]);
|
|
31
|
+
// EC-568: the asset collection's fields — their `source` declarations map a file's
|
|
32
|
+
// embedded IPTC facts onto `meta` on every upload and replace. Config, read once.
|
|
33
|
+
const assetFields = useMemo(() => projectCollections(projectId).find((collection) => collection.kind === 'asset')?.fields, [projectId]);
|
|
30
34
|
/** The shared studio operations bundle + the metadata-write facade over it. */
|
|
31
35
|
const studio = useCallback(async () => {
|
|
32
36
|
const bundle = await getStudioOperations();
|
|
@@ -58,6 +62,7 @@ export function AssetsProvider({ projectId, children, }) {
|
|
|
58
62
|
// confirmed through the `assets.upsert` operation command.
|
|
59
63
|
blobs: adapter.blobs,
|
|
60
64
|
assets: assetRepo,
|
|
65
|
+
...(assetFields ? { assetFields } : {}),
|
|
61
66
|
...(typeof XMLHttpRequest !== 'undefined' ? { transport: xhrUploadTransport } : {}),
|
|
62
67
|
}, {
|
|
63
68
|
projectId,
|
|
@@ -71,7 +76,7 @@ export function AssetsProvider({ projectId, children, }) {
|
|
|
71
76
|
setUploads((prev) => prev.filter((u) => u.id !== uploadId));
|
|
72
77
|
await refresh();
|
|
73
78
|
}
|
|
74
|
-
}, [studio, projectId, refresh, setUploadState]);
|
|
79
|
+
}, [studio, projectId, refresh, setUploadState, assetFields]);
|
|
75
80
|
const upload = useCallback((files) => {
|
|
76
81
|
const ids = [];
|
|
77
82
|
for (const file of files) {
|
|
@@ -132,6 +137,21 @@ export function AssetsProvider({ projectId, children, }) {
|
|
|
132
137
|
}
|
|
133
138
|
return result;
|
|
134
139
|
}, [studio, projectId, cache]);
|
|
140
|
+
const replaceFile = useCallback(async (assetId, file) => {
|
|
141
|
+
const { adapter, assetRepo } = await studio();
|
|
142
|
+
// EC-568: THE upload path, in replace mode — not a second uploader.
|
|
143
|
+
const result = await runUpload({
|
|
144
|
+
blobs: adapter.blobs,
|
|
145
|
+
assets: assetRepo,
|
|
146
|
+
...(assetFields ? { assetFields } : {}),
|
|
147
|
+
...(typeof XMLHttpRequest !== 'undefined' ? { transport: xhrUploadTransport } : {}),
|
|
148
|
+
}, { projectId, file, replace: { assetId } });
|
|
149
|
+
if (!result.ok)
|
|
150
|
+
return { ok: false, message: result.failure.message };
|
|
151
|
+
const updated = result.asset;
|
|
152
|
+
cache.patch(assetsQueryKey(projectId), (prev) => (prev ?? []).map((a) => (a.id === assetId ? updated : a)));
|
|
153
|
+
return { ok: true, asset: updated };
|
|
154
|
+
}, [studio, projectId, cache, assetFields]);
|
|
135
155
|
const generatePreviews = useCallback(async (handlers) => {
|
|
136
156
|
const { adapter, assetRepo } = await studio();
|
|
137
157
|
// Reads (list) + the variant patch (upsert) go through the operations facade;
|
|
@@ -160,6 +180,7 @@ export function AssetsProvider({ projectId, children, }) {
|
|
|
160
180
|
usagesOf,
|
|
161
181
|
remove,
|
|
162
182
|
updateMetadata,
|
|
183
|
+
replaceFile,
|
|
163
184
|
generatePreviews,
|
|
164
185
|
refresh,
|
|
165
186
|
}), [
|
|
@@ -175,6 +196,7 @@ export function AssetsProvider({ projectId, children, }) {
|
|
|
175
196
|
usagesOf,
|
|
176
197
|
remove,
|
|
177
198
|
updateMetadata,
|
|
199
|
+
replaceFile,
|
|
178
200
|
generatePreviews,
|
|
179
201
|
refresh,
|
|
180
202
|
]);
|