@elytracms/studio 0.0.38 → 0.0.40
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/cms.ts +8 -2
- package/dist/components/app-views/default-modules.d.ts +60 -5
- package/dist/components/app-views/default-modules.js +53 -15
- package/dist/components/app-views/default-modules.js.map +1 -1
- package/dist/components/app-views/module.d.ts +2 -0
- package/dist/components/app-views/module.js +8 -0
- package/dist/components/app-views/module.js.map +1 -1
- package/dist/components/assets/asset-detail.d.ts +24 -0
- package/dist/components/assets/asset-detail.js +163 -0
- package/dist/components/assets/asset-detail.js.map +1 -0
- package/dist/components/assets/asset-grid.d.ts +28 -0
- package/dist/components/assets/asset-grid.js +68 -0
- package/dist/components/assets/asset-grid.js.map +1 -0
- package/dist/components/assets/asset-metadata.d.ts +19 -0
- package/dist/components/assets/asset-metadata.js +44 -0
- package/dist/components/assets/asset-metadata.js.map +1 -1
- package/dist/components/assets/assets-collection-view-source.d.ts +54 -0
- package/dist/components/assets/assets-collection-view-source.js +186 -0
- package/dist/components/assets/assets-collection-view-source.js.map +1 -0
- package/dist/components/assets/assets-module.d.ts +18 -0
- package/dist/components/assets/assets-module.js +94 -0
- package/dist/components/assets/assets-module.js.map +1 -0
- package/dist/components/assets/assets-store.js +2 -7
- package/dist/components/assets/assets-store.js.map +1 -1
- package/dist/components/assets/assets-view-preference.d.ts +15 -0
- package/dist/components/assets/assets-view-preference.js +22 -0
- package/dist/components/assets/assets-view-preference.js.map +1 -0
- package/dist/components/builder/builder-store.d.ts +12 -0
- package/dist/components/builder/builder-store.js +18 -16
- package/dist/components/builder/builder-store.js.map +1 -1
- package/dist/components/builder/clipboard-actions.js +5 -14
- package/dist/components/builder/clipboard-actions.js.map +1 -1
- package/dist/components/builder/composition-field-editor-lazy.d.ts +19 -0
- package/dist/components/builder/composition-field-editor-lazy.js +29 -0
- package/dist/components/builder/composition-field-editor-lazy.js.map +1 -0
- package/dist/components/builder/composition-field-editor.js +1 -4
- package/dist/components/builder/composition-field-editor.js.map +1 -1
- package/dist/components/builder/insert-context.d.ts +8 -0
- package/dist/components/builder/insert-context.js +1 -3
- package/dist/components/builder/insert-context.js.map +1 -1
- package/dist/components/builder/page-tree.js +1 -3
- package/dist/components/builder/page-tree.js.map +1 -1
- package/dist/components/builder/property-inspector.js +7 -134
- package/dist/components/builder/property-inspector.js.map +1 -1
- package/dist/components/cms/cms-store.d.ts +15 -4
- package/dist/components/cms/cms-store.js +42 -24
- package/dist/components/cms/cms-store.js.map +1 -1
- package/dist/components/cms/collection-table.d.ts +8 -1
- package/dist/components/cms/collection-table.js +75 -11
- package/dist/components/cms/collection-table.js.map +1 -1
- package/dist/components/cms/column-width.d.ts +25 -0
- package/dist/components/cms/column-width.js +47 -0
- package/dist/components/cms/column-width.js.map +1 -0
- package/dist/components/cms/detail-editor.js +143 -79
- package/dist/components/cms/detail-editor.js.map +1 -1
- package/dist/components/cms/details-pane.d.ts +21 -8
- package/dist/components/cms/details-pane.js +31 -11
- package/dist/components/cms/details-pane.js.map +1 -1
- package/dist/components/cms/discard-draft.d.ts +32 -0
- package/dist/components/cms/discard-draft.js +13 -0
- package/dist/components/cms/discard-draft.js.map +1 -0
- package/dist/components/cms/document-staging.d.ts +7 -0
- package/dist/components/cms/document-staging.js +44 -0
- package/dist/components/cms/document-staging.js.map +1 -0
- package/dist/components/cms/field-connection.d.ts +22 -0
- package/dist/components/cms/field-connection.js +37 -0
- package/dist/components/cms/field-connection.js.map +1 -0
- package/dist/components/cms/field-control.js +167 -34
- package/dist/components/cms/field-control.js.map +1 -1
- package/dist/components/cms/prop-fields.d.ts +43 -0
- package/dist/components/cms/prop-fields.js +154 -0
- package/dist/components/cms/prop-fields.js.map +1 -0
- package/dist/components/cms/rich-text/component-embed.d.ts +17 -3
- package/dist/components/cms/rich-text/component-embed.js +29 -7
- package/dist/components/cms/rich-text/component-embed.js.map +1 -1
- package/dist/components/cms/rich-text/embed-props.d.ts +49 -0
- package/dist/components/cms/rich-text/embed-props.js +106 -0
- package/dist/components/cms/rich-text/embed-props.js.map +1 -0
- package/dist/components/cms/rich-text/index.d.ts +6 -1
- package/dist/components/cms/rich-text/index.js +6 -1
- package/dist/components/cms/rich-text/index.js.map +1 -1
- package/dist/components/cms/rich-text/lazy.d.ts +19 -0
- package/dist/components/cms/rich-text/lazy.js +26 -0
- package/dist/components/cms/rich-text/lazy.js.map +1 -0
- package/dist/components/cms/rich-text/link-control.js +3 -1
- package/dist/components/cms/rich-text/link-control.js.map +1 -1
- package/dist/components/cms/rich-text/toolbar-state.d.ts +45 -0
- package/dist/components/cms/rich-text/toolbar-state.js +66 -0
- package/dist/components/cms/rich-text/toolbar-state.js.map +1 -0
- package/dist/components/cms/rich-text/toolbar.js +7 -15
- package/dist/components/cms/rich-text/toolbar.js.map +1 -1
- package/dist/components/cms/row-actions.d.ts +28 -0
- package/dist/components/cms/row-actions.js +20 -0
- package/dist/components/cms/row-actions.js.map +1 -0
- package/dist/components/cms/use-document-versions.d.ts +8 -1
- package/dist/components/cms/use-document-versions.js +31 -4
- package/dist/components/cms/use-document-versions.js.map +1 -1
- package/dist/components/cms/version-compare-lazy.d.ts +1 -0
- package/dist/components/cms/version-compare-lazy.js +23 -0
- package/dist/components/cms/version-compare-lazy.js.map +1 -0
- package/dist/components/cms/version-compare-text.d.ts +46 -0
- package/dist/components/cms/version-compare-text.js +78 -0
- package/dist/components/cms/version-compare-text.js.map +1 -1
- package/dist/components/cms/version-compare.d.ts +9 -5
- package/dist/components/cms/version-compare.js +217 -64
- package/dist/components/cms/version-compare.js.map +1 -1
- package/dist/components/cms/versions-pane.js +10 -4
- package/dist/components/cms/versions-pane.js.map +1 -1
- package/dist/components/components/component-detail.d.ts +5 -3
- package/dist/components/components/component-detail.js +41 -8
- package/dist/components/components/component-detail.js.map +1 -1
- package/dist/components/components/component-preview.d.ts +97 -0
- package/dist/components/components/component-preview.js +202 -0
- package/dist/components/components/component-preview.js.map +1 -0
- package/dist/components/components/components-index.d.ts +5 -3
- package/dist/components/components/components-index.js +19 -24
- package/dist/components/components/components-index.js.map +1 -1
- package/dist/components/components/registry-browser.d.ts +7 -0
- package/dist/components/components/registry-browser.js +20 -36
- package/dist/components/components/registry-browser.js.map +1 -1
- package/dist/components/components/taxonomy.d.ts +51 -0
- package/dist/components/components/taxonomy.js +73 -0
- package/dist/components/components/taxonomy.js.map +1 -0
- package/dist/components/components/transfer-dialog.d.ts +20 -0
- package/dist/components/components/transfer-dialog.js +64 -0
- package/dist/components/components/transfer-dialog.js.map +1 -0
- package/dist/components/components/transfer.d.ts +43 -0
- package/dist/components/components/transfer.js +31 -0
- package/dist/components/components/transfer.js.map +1 -0
- package/dist/components/project-list.js +3 -4
- package/dist/components/project-list.js.map +1 -1
- package/dist/components/project-switcher.js +3 -2
- package/dist/components/project-switcher.js.map +1 -1
- package/dist/components/rail-store-status.d.ts +21 -0
- package/dist/components/rail-store-status.js +21 -0
- package/dist/components/rail-store-status.js.map +1 -0
- package/dist/components/redirects/redirects-module.js +4 -8
- package/dist/components/redirects/redirects-module.js.map +1 -1
- package/dist/components/redirects/redirects-store.js +2 -7
- package/dist/components/redirects/redirects-store.js.map +1 -1
- package/dist/components/sign-in-needed-badge.d.ts +6 -0
- package/dist/components/sign-in-needed-badge.js +13 -0
- package/dist/components/sign-in-needed-badge.js.map +1 -0
- package/dist/components/studio-project-screen.js +1 -4
- package/dist/components/studio-project-screen.js.map +1 -1
- package/dist/components/studio-shell.js +1 -4
- package/dist/components/studio-shell.js.map +1 -1
- package/dist/components/users/users-module.js +5 -7
- package/dist/components/users/users-module.js.map +1 -1
- package/dist/components/users/users-store.js +2 -7
- package/dist/components/users/users-store.js.map +1 -1
- package/dist/lib/auth/index.js +1 -1
- package/dist/lib/auth/index.js.map +1 -1
- package/dist/lib/clipboard/paste-target.js +1 -3
- package/dist/lib/clipboard/paste-target.js.map +1 -1
- package/dist/lib/cms-views/view-model.d.ts +0 -2
- package/dist/lib/cms-views/view-model.js +0 -7
- package/dist/lib/cms-views/view-model.js.map +1 -1
- package/dist/lib/diff-view-preference.d.ts +7 -0
- package/dist/lib/diff-view-preference.js +21 -0
- package/dist/lib/diff-view-preference.js.map +1 -0
- package/dist/lib/i18n/messages.en.js +94 -33
- package/dist/lib/i18n/messages.en.js.map +1 -1
- package/dist/lib/local-preference.d.ts +11 -0
- package/dist/lib/local-preference.js +26 -0
- package/dist/lib/local-preference.js.map +1 -0
- package/dist/lib/locale/fallback.d.ts +45 -0
- package/dist/lib/locale/fallback.js +85 -0
- package/dist/lib/locale/fallback.js.map +1 -1
- package/dist/lib/persona/labels.d.ts +6 -0
- package/dist/lib/persona/labels.js +8 -0
- package/dist/lib/persona/labels.js.map +1 -1
- package/dist/lib/query/index.d.ts +1 -1
- package/dist/lib/query/index.js +1 -1
- package/dist/lib/query/index.js.map +1 -1
- package/dist/lib/query/operations-query.d.ts +14 -2
- package/dist/lib/query/operations-query.js +11 -6
- package/dist/lib/query/operations-query.js.map +1 -1
- package/dist/lib/rail.d.ts +0 -6
- package/dist/lib/rail.js +6 -25
- package/dist/lib/rail.js.map +1 -1
- package/dist/lib/theme.d.ts +0 -6
- package/dist/lib/theme.js +10 -13
- package/dist/lib/theme.js.map +1 -1
- package/dist/router/match.d.ts +1 -0
- package/dist/router/match.js +4 -0
- package/dist/router/match.js.map +1 -1
- package/dist/ui/button.d.ts +1 -1
- package/package.json +22 -25
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useMemo, useState } from 'react';
|
|
3
|
+
import { AlertTriangle, Link2, RotateCcw, Trash2 } from 'lucide-react';
|
|
4
|
+
import { Link, STUDIO_PROJECT_ROUTE, useNavigate } from '@elytracms/studio/router';
|
|
5
|
+
import { localizedLabel, translate } from '@elytracms/studio/lib/i18n';
|
|
6
|
+
import { buildAssetReferenceImpact, loadInboundAssetReferenceViews } from '@elytracms/studio/lib/references';
|
|
7
|
+
import { Button, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, Field, Heading, Panel, PanelBody, PanelHeader, Row, Spacer, Stack, Text, } from '@elytracms/studio/ui';
|
|
8
|
+
import { FieldControl } from '../cms/field-control';
|
|
9
|
+
import { fieldLabel } from '../cms/cms-store';
|
|
10
|
+
import { ReferenceImpactRows } from '../cms/reference-confirm';
|
|
11
|
+
import { ReferencesPanel } from '../references/references-panel';
|
|
12
|
+
import { useSetBreadcrumb } from '../breadcrumb-context';
|
|
13
|
+
import { useCollectionView } from '../collection-view/collection-view-context';
|
|
14
|
+
import { FocalCropPreviews, FocalPointPicker, useAssetRecord } from './asset-thumb';
|
|
15
|
+
import { assetMetaFieldsOf, formatBytes, pruneMeta } from './asset-metadata';
|
|
16
|
+
import { useAssets } from './assets-store';
|
|
17
|
+
/**
|
|
18
|
+
* The assets rail's DETAIL surface (EC-319). Pragmatic call: assets don't fit
|
|
19
|
+
* the portable `PortableDetailEditor` shell cleanly — there's no dirty/Save/
|
|
20
|
+
* Publish document model here (an asset record isn't a `cmsDocument`), and the
|
|
21
|
+
* focal-point picker is a bespoke drag interaction over the image, not a
|
|
22
|
+
* scalar field value a generic form row can host without contortions. So this
|
|
23
|
+
* is a bespoke surface (mirrors the CMS document editor's shape: a header +
|
|
24
|
+
* panels), reached via the SAME rail-owned `detailPath`/`listPath` seam every
|
|
25
|
+
* other rail uses (AD-19) — only the body differs.
|
|
26
|
+
*
|
|
27
|
+
* Unifies what were two divergent asset-edit UIs before this task: the plain
|
|
28
|
+
* alt/title dialog on the old asset library card, and the richer schema-driven
|
|
29
|
+
* + focal-point "image options" dialog opened from an `asset` field elsewhere
|
|
30
|
+
* (`field-control.tsx`). This page reuses the LATTER (richer) content, so
|
|
31
|
+
* editing an asset here is never a downgrade from editing it via a field.
|
|
32
|
+
*
|
|
33
|
+
* Deletion keeps the EC-202 reference-index safe-delete path (`assets-store.tsx`
|
|
34
|
+
* → `deleteAsset`, keyed on `{ type: 'asset' }` — never the generic document
|
|
35
|
+
* reference-confirm the universal table's row/bulk delete would wrongly apply).
|
|
36
|
+
*/
|
|
37
|
+
export function AssetDetail({ projectId, assetId }) {
|
|
38
|
+
const source = useCollectionView();
|
|
39
|
+
const store = useAssets();
|
|
40
|
+
const record = useAssetRecord(assetId);
|
|
41
|
+
const collection = source.schema;
|
|
42
|
+
const collectionLabel = localizedLabel(collection.form?.label, collection.id);
|
|
43
|
+
useSetBreadcrumb(useMemo(() => {
|
|
44
|
+
if (source.listPath === undefined)
|
|
45
|
+
return [{ label: collectionLabel }];
|
|
46
|
+
const title = record?.title ?? record?.filename ?? assetId;
|
|
47
|
+
return [
|
|
48
|
+
{ label: collectionLabel, to: STUDIO_PROJECT_ROUTE, params: { projectId, _splat: source.listPath } },
|
|
49
|
+
{ label: title },
|
|
50
|
+
];
|
|
51
|
+
}, [collectionLabel, source.listPath, record, projectId, assetId]));
|
|
52
|
+
if (record === undefined) {
|
|
53
|
+
if (store.status === 'loading') {
|
|
54
|
+
return (_jsx(Stack, { className: "h-full place-items-center p-8", align: "center", justify: "center", children: _jsx(Text, { size: "sm", tone: "muted", children: translate('assets.loading') }) }));
|
|
55
|
+
}
|
|
56
|
+
return (_jsx("div", { className: "grid h-full place-items-center p-8", children: _jsxs(Stack, { gap: "3", align: "center", className: "text-center", children: [_jsx(Heading, { level: 2, size: "h2", children: translate('cms.editor.notFoundTitle') }), source.listPath !== undefined ? (_jsx(Link, { to: STUDIO_PROJECT_ROUTE, params: { projectId, _splat: source.listPath }, className: "text-sm text-primary hover:underline", children: translate('cms.editor.backToList', { label: collectionLabel }) })) : null] }) }));
|
|
57
|
+
}
|
|
58
|
+
// Remount per asset (the surface's own key, set by the caller) resets every
|
|
59
|
+
// local buffer below cleanly when navigating from one asset's detail to
|
|
60
|
+
// another — no stale-state carryover to guard against by hand.
|
|
61
|
+
return _jsx(AssetDetailBody, { projectId: projectId, record: record, collection: source.schema });
|
|
62
|
+
}
|
|
63
|
+
function AssetDetailBody({ projectId, record, collection, }) {
|
|
64
|
+
const store = useAssets();
|
|
65
|
+
const metaFields = useMemo(() => assetMetaFieldsOf(collection), [collection]);
|
|
66
|
+
// Metadata buffers locally and commits explicitly (Save) — an asset record has
|
|
67
|
+
// no dirty/Save document model of its own to piggyback on. The focal point
|
|
68
|
+
// commits immediately (it drives a live visual), matching the field's options
|
|
69
|
+
// dialog (`field-control.tsx`).
|
|
70
|
+
const [meta, setMeta] = useState(() => ({
|
|
71
|
+
...(record.alt !== undefined ? { alt: record.alt } : {}),
|
|
72
|
+
...(record.meta ?? {}),
|
|
73
|
+
}));
|
|
74
|
+
const [focal, setFocal] = useState(record.focalPoint);
|
|
75
|
+
const [dirty, setDirty] = useState(false);
|
|
76
|
+
const [saving, setSaving] = useState(false);
|
|
77
|
+
const [saveError, setSaveError] = useState(null);
|
|
78
|
+
function setField(name, value) {
|
|
79
|
+
setDirty(true);
|
|
80
|
+
setMeta((m) => ({ ...m, [name]: value }));
|
|
81
|
+
}
|
|
82
|
+
async function save() {
|
|
83
|
+
setSaving(true);
|
|
84
|
+
setSaveError(null);
|
|
85
|
+
const result = await store.updateMetadata(record.id, { meta: pruneMeta(meta) });
|
|
86
|
+
setSaving(false);
|
|
87
|
+
if (result.ok)
|
|
88
|
+
setDirty(false);
|
|
89
|
+
else
|
|
90
|
+
setSaveError(result.message);
|
|
91
|
+
}
|
|
92
|
+
function commitFocal(point) {
|
|
93
|
+
setFocal(point ?? undefined);
|
|
94
|
+
void store.updateMetadata(record.id, { focalPoint: point });
|
|
95
|
+
}
|
|
96
|
+
return (_jsxs(Stack, { gap: "4", className: "mx-auto max-w-3xl p-4 pb-10 sm:p-6", children: [_jsx(AssetDetailHeader, { projectId: projectId, record: record }), _jsxs(Panel, { children: [_jsx(PanelHeader, { children: _jsxs(Row, { align: "center", className: "w-full", children: [_jsx(Text, { size: "xs", weight: "semibold", tone: "muted", children: translate('cms.asset.focalPoint') }), _jsx(Spacer, {}), focal ? (_jsxs(Button, { variant: "ghost", size: "sm", onClick: () => commitFocal(null), children: [_jsx(RotateCcw, {}), " ", translate('cms.asset.resetToCentre')] })) : null] }) }), _jsx(PanelBody, { children: _jsxs(Stack, { gap: "2", children: [_jsx(FocalPointPicker, { record: record, point: focal, onChange: commitFocal, onPreview: setFocal }), _jsx(Text, { size: "xs", tone: "subtle", children: translate('cms.asset.focalPointHint') }), _jsx(FocalCropPreviews, { record: record, point: focal })] }) })] }), _jsxs(Panel, { "data-testid": "asset-detail-meta", children: [_jsx(PanelHeader, { children: _jsx(Text, { size: "xs", weight: "semibold", tone: "muted", children: translate('assets.detail.metadataHeading') }) }), _jsx(PanelBody, { children: _jsxs(Stack, { gap: "3", children: [metaFields.map((f) => (_jsx(Field, { label: fieldLabel(f), hint: localizedLabel(f.form?.description) || undefined, children: _jsx(FieldControl, { id: `asset-detail-${f.name}`, field: f, value: meta[f.name], readOnly: false, onChange: (v) => setField(f.name, v) }) }, f.name))), metaFields.length === 0 ? (_jsx(Text, { size: "sm", tone: "subtle", children: translate('cms.asset.noMetaFields', { code1: 'alt', code2: 'copyright' }) })) : null, saveError ? (_jsx(Text, { size: "sm", className: "text-destructive", "data-testid": "asset-detail-save-error", children: saveError })) : null, metaFields.length > 0 ? (_jsxs(Row, { gap: "2", children: [_jsx(Spacer, {}), _jsx(Button, { size: "sm", onClick: () => void save(), disabled: !dirty || saving, "data-testid": "asset-detail-save", children: saving ? translate('common.saving') : translate('common.save') })] })) : null] }) })] })] }));
|
|
97
|
+
}
|
|
98
|
+
/** Filename/title heading + system info (content type, dimensions, size) + actions. */
|
|
99
|
+
function AssetDetailHeader({ projectId, record }) {
|
|
100
|
+
const dims = record.width && record.height ? `${record.width}×${record.height}` : null;
|
|
101
|
+
const info = [record.contentType, dims, formatBytes(record.byteSize)].filter(Boolean).join(' · ');
|
|
102
|
+
return (_jsxs(Row, { gap: "3", align: "start", wrap: true, children: [_jsxs(Stack, { gap: "1", className: "min-w-0 flex-1", children: [_jsx(Heading, { level: 2, size: "h2", className: "truncate", title: record.title ?? record.filename, children: record.title ?? record.filename }), record.title ? (_jsx(Text, { size: "sm", tone: "muted", className: "truncate", children: record.filename })) : null, _jsx(Text, { size: "xs", tone: "subtle", children: info })] }), _jsxs(Row, { gap: "2", children: [_jsx(AssetReferencesButton, { projectId: projectId, record: record }), _jsx(AssetDeleteButton, { projectId: projectId, record: record })] })] }));
|
|
103
|
+
}
|
|
104
|
+
/** "Where is this asset used?" (EC-172): the shared references panel, EC-155-backed. */
|
|
105
|
+
function AssetReferencesButton({ projectId, record }) {
|
|
106
|
+
const [open, setOpen] = useState(false);
|
|
107
|
+
return (_jsxs(_Fragment, { children: [_jsxs(Button, { variant: "outline", size: "sm", onClick: () => setOpen(true), "data-testid": "asset-detail-references", children: [_jsx(Link2, {}), " ", translate('assets.references.buttonLabel')] }), _jsx(Dialog, { open: open, onOpenChange: setOpen, children: _jsxs(DialogContent, { className: "max-w-lg", children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: translate('assets.references.title', { name: record.title ?? record.filename }) }), _jsx(DialogDescription, { children: translate('assets.references.description') })] }), open ? (_jsx(ReferencesPanel, { projectId: projectId, target: { type: 'asset', id: record.id } })) : null] }) })] }));
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Reference-respecting delete (EC-151, EC-202): enumerates the exact inbound
|
|
111
|
+
* references — index-accurate, with deep links to the field/node — before
|
|
112
|
+
* removing. Deleting a referenced asset is weak-by-default (allowed with an
|
|
113
|
+
* explicit "delete anyway"); every reference dangles into a visible validation
|
|
114
|
+
* issue rather than silently breaking. On success, returns to the list.
|
|
115
|
+
*/
|
|
116
|
+
function AssetDeleteButton({ projectId, record }) {
|
|
117
|
+
const source = useCollectionView();
|
|
118
|
+
const store = useAssets();
|
|
119
|
+
const navigate = useNavigate();
|
|
120
|
+
const [open, setOpen] = useState(false);
|
|
121
|
+
const [usages, setUsages] = useState(null);
|
|
122
|
+
const [error, setError] = useState(null);
|
|
123
|
+
const [busy, setBusy] = useState(false);
|
|
124
|
+
const openDialog = async () => {
|
|
125
|
+
setOpen(true);
|
|
126
|
+
setError(null);
|
|
127
|
+
setUsages(null);
|
|
128
|
+
try {
|
|
129
|
+
setUsages(await loadInboundAssetReferenceViews(projectId, record.id));
|
|
130
|
+
}
|
|
131
|
+
catch (cause) {
|
|
132
|
+
setError(cause instanceof Error ? cause.message : String(cause));
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
const confirm = async () => {
|
|
136
|
+
setBusy(true);
|
|
137
|
+
setError(null);
|
|
138
|
+
// The dialog has already enumerated usages (or shown there are none) and the
|
|
139
|
+
// user is confirming either way — confirm unconditionally, skipping the
|
|
140
|
+
// redundant inbound re-check (EC-301 perf). Weak-by-default: a still-
|
|
141
|
+
// referenced asset deletes anyway, dangling into a visible issue.
|
|
142
|
+
const result = await store.remove(record.id, { confirmInUse: true });
|
|
143
|
+
setBusy(false);
|
|
144
|
+
if (result.outcome === 'deleted' || result.outcome === 'not-found') {
|
|
145
|
+
setOpen(false);
|
|
146
|
+
if (source.listPath !== undefined) {
|
|
147
|
+
void navigate({ to: STUDIO_PROJECT_ROUTE, params: { projectId, _splat: source.listPath } });
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
else if (result.outcome === 'blocked') {
|
|
151
|
+
setError(translate(result.usageCount === 1 ? 'assets.delete.raceError.one' : 'assets.delete.raceError.other', { count: result.usageCount }));
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
setError(result.message);
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
const impact = usages ? buildAssetReferenceImpact(usages) : null;
|
|
158
|
+
const usageCount = impact?.broken.length ?? 0;
|
|
159
|
+
return (_jsxs(_Fragment, { children: [_jsxs(Button, { variant: "outline", size: "sm", onClick: () => void openDialog(), "data-testid": "asset-detail-delete", children: [_jsx(Trash2, {}), " ", translate('common.delete')] }), _jsx(Dialog, { open: open, onOpenChange: setOpen, children: _jsxs(DialogContent, { className: "max-h-[80vh] max-w-md overflow-y-auto", children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: translate('assets.delete.title', { filename: record.filename }) }), _jsx(DialogDescription, { children: translate('assets.delete.description') })] }), usages === null && error === null ? (_jsx(Text, { size: "sm", tone: "muted", children: translate('assets.delete.checking') })) : usageCount > 0 ? (_jsxs(Stack, { gap: "2", "data-testid": "asset-usage-warning", children: [_jsxs(Row, { gap: "2", children: [_jsx(AlertTriangle, { className: "size-4 text-warning" }), _jsx(Text, { size: "sm", weight: "semibold", children: translate(usageCount === 1 ? 'assets.delete.inUse.one' : 'assets.delete.inUse.other', {
|
|
160
|
+
count: usageCount,
|
|
161
|
+
}) })] }), _jsx(ReferenceImpactRows, { projectId: projectId, broken: impact.broken, onNavigate: () => setOpen(false) }), _jsx(Text, { size: "xs", tone: "muted", children: translate('assets.delete.brokenWarning') })] })) : (_jsx(Text, { size: "sm", tone: "muted", children: translate('assets.delete.noReferences') })), error ? (_jsx(Text, { size: "sm", className: "text-destructive", "data-testid": "asset-delete-error", children: error })) : null, _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "ghost", size: "sm", onClick: () => setOpen(false), disabled: busy, children: translate('common.cancel') }), _jsx(Button, { variant: "destructive", size: "sm", onClick: () => void confirm(), disabled: busy || usages === null, "data-testid": "asset-delete-confirm", children: usageCount > 0 ? translate('assets.delete.confirmAnyway') : translate('common.delete') })] })] }) })] }));
|
|
162
|
+
}
|
|
163
|
+
//# sourceMappingURL=asset-detail.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"asset-detail.js","sourceRoot":"","sources":["../../../src/components/assets/asset-detail.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useMemo, useState } from 'react'\nimport { AlertTriangle, Link2, RotateCcw, Trash2 } from 'lucide-react'\nimport type { CollectionDef, FieldDef } from '@elytracms/core/cms-core'\nimport type { AssetRecord } from '@elytracms/core/persistence'\nimport { Link, STUDIO_PROJECT_ROUTE, useNavigate } from '@elytracms/studio/router'\nimport { localizedLabel, translate } from '@elytracms/studio/lib/i18n'\nimport { buildAssetReferenceImpact, loadInboundAssetReferenceViews } from '@elytracms/studio/lib/references'\nimport type { ReferenceEntryView } from '@elytracms/studio/lib/references'\nimport {\n Button,\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n Field,\n Heading,\n Panel,\n PanelBody,\n PanelHeader,\n Row,\n Spacer,\n Stack,\n Text,\n} from '@elytracms/studio/ui'\nimport { FieldControl } from '../cms/field-control'\nimport { fieldLabel } from '../cms/cms-store'\nimport { ReferenceImpactRows } from '../cms/reference-confirm'\nimport { ReferencesPanel } from '../references/references-panel'\nimport { useSetBreadcrumb } from '../breadcrumb-context'\nimport { useCollectionView } from '../collection-view/collection-view-context'\nimport { FocalCropPreviews, FocalPointPicker, useAssetRecord } from './asset-thumb'\nimport type { FocalPoint } from './focal-point'\nimport { assetMetaFieldsOf, formatBytes, pruneMeta } from './asset-metadata'\nimport { useAssets } from './assets-store'\n\n/**\n * The assets rail's DETAIL surface (EC-319). Pragmatic call: assets don't fit\n * the portable `PortableDetailEditor` shell cleanly — there's no dirty/Save/\n * Publish document model here (an asset record isn't a `cmsDocument`), and the\n * focal-point picker is a bespoke drag interaction over the image, not a\n * scalar field value a generic form row can host without contortions. So this\n * is a bespoke surface (mirrors the CMS document editor's shape: a header +\n * panels), reached via the SAME rail-owned `detailPath`/`listPath` seam every\n * other rail uses (AD-19) — only the body differs.\n *\n * Unifies what were two divergent asset-edit UIs before this task: the plain\n * alt/title dialog on the old asset library card, and the richer schema-driven\n * + focal-point \"image options\" dialog opened from an `asset` field elsewhere\n * (`field-control.tsx`). This page reuses the LATTER (richer) content, so\n * editing an asset here is never a downgrade from editing it via a field.\n *\n * Deletion keeps the EC-202 reference-index safe-delete path (`assets-store.tsx`\n * → `deleteAsset`, keyed on `{ type: 'asset' }` — never the generic document\n * reference-confirm the universal table's row/bulk delete would wrongly apply).\n */\nexport function AssetDetail({ projectId, assetId }: { projectId: string; assetId: string }) {\n const source = useCollectionView()\n const store = useAssets()\n const record = useAssetRecord(assetId)\n const collection = source.schema\n\n const collectionLabel = localizedLabel(collection.form?.label, collection.id)\n useSetBreadcrumb(\n useMemo(() => {\n if (source.listPath === undefined) return [{ label: collectionLabel }]\n const title = record?.title ?? record?.filename ?? assetId\n return [\n { label: collectionLabel, to: STUDIO_PROJECT_ROUTE, params: { projectId, _splat: source.listPath } },\n { label: title },\n ]\n }, [collectionLabel, source.listPath, record, projectId, assetId]),\n )\n\n if (record === undefined) {\n if (store.status === 'loading') {\n return (\n <Stack className=\"h-full place-items-center p-8\" align=\"center\" justify=\"center\">\n <Text size=\"sm\" tone=\"muted\">\n {translate('assets.loading')}\n </Text>\n </Stack>\n )\n }\n return (\n <div className=\"grid h-full place-items-center p-8\">\n <Stack gap=\"3\" align=\"center\" className=\"text-center\">\n <Heading level={2} size=\"h2\">\n {translate('cms.editor.notFoundTitle')}\n </Heading>\n {source.listPath !== undefined ? (\n <Link\n to={STUDIO_PROJECT_ROUTE}\n params={{ projectId, _splat: source.listPath }}\n className=\"text-sm text-primary hover:underline\"\n >\n {translate('cms.editor.backToList', { label: collectionLabel })}\n </Link>\n ) : null}\n </Stack>\n </div>\n )\n }\n\n // Remount per asset (the surface's own key, set by the caller) resets every\n // local buffer below cleanly when navigating from one asset's detail to\n // another — no stale-state carryover to guard against by hand.\n return <AssetDetailBody projectId={projectId} record={record} collection={source.schema} />\n}\n\nfunction AssetDetailBody({\n projectId,\n record,\n collection,\n}: {\n projectId: string\n record: AssetRecord\n collection: CollectionDef\n}) {\n const store = useAssets()\n const metaFields = useMemo(() => assetMetaFieldsOf(collection), [collection])\n\n // Metadata buffers locally and commits explicitly (Save) — an asset record has\n // no dirty/Save document model of its own to piggyback on. The focal point\n // commits immediately (it drives a live visual), matching the field's options\n // dialog (`field-control.tsx`).\n const [meta, setMeta] = useState<Record<string, unknown>>(() => ({\n ...(record.alt !== undefined ? { alt: record.alt } : {}),\n ...(record.meta ?? {}),\n }))\n const [focal, setFocal] = useState<FocalPoint | undefined>(record.focalPoint)\n const [dirty, setDirty] = useState(false)\n const [saving, setSaving] = useState(false)\n const [saveError, setSaveError] = useState<string | null>(null)\n\n function setField(name: string, value: unknown) {\n setDirty(true)\n setMeta((m) => ({ ...m, [name]: value }))\n }\n\n async function save() {\n setSaving(true)\n setSaveError(null)\n const result = await store.updateMetadata(record.id, { meta: pruneMeta(meta) })\n setSaving(false)\n if (result.ok) setDirty(false)\n else setSaveError(result.message)\n }\n\n function commitFocal(point: FocalPoint | null) {\n setFocal(point ?? undefined)\n void store.updateMetadata(record.id, { focalPoint: point })\n }\n\n return (\n <Stack gap=\"4\" className=\"mx-auto max-w-3xl p-4 pb-10 sm:p-6\">\n <AssetDetailHeader projectId={projectId} record={record} />\n\n <Panel>\n <PanelHeader>\n <Row align=\"center\" className=\"w-full\">\n <Text size=\"xs\" weight=\"semibold\" tone=\"muted\">\n {translate('cms.asset.focalPoint')}\n </Text>\n <Spacer />\n {focal ? (\n <Button variant=\"ghost\" size=\"sm\" onClick={() => commitFocal(null)}>\n <RotateCcw /> {translate('cms.asset.resetToCentre')}\n </Button>\n ) : null}\n </Row>\n </PanelHeader>\n <PanelBody>\n <Stack gap=\"2\">\n <FocalPointPicker record={record} point={focal} onChange={commitFocal} onPreview={setFocal} />\n <Text size=\"xs\" tone=\"subtle\">\n {translate('cms.asset.focalPointHint')}\n </Text>\n <FocalCropPreviews record={record} point={focal} />\n </Stack>\n </PanelBody>\n </Panel>\n\n <Panel data-testid=\"asset-detail-meta\">\n <PanelHeader>\n <Text size=\"xs\" weight=\"semibold\" tone=\"muted\">\n {translate('assets.detail.metadataHeading')}\n </Text>\n </PanelHeader>\n <PanelBody>\n <Stack gap=\"3\">\n {metaFields.map((f: FieldDef) => (\n <Field\n key={f.name}\n label={fieldLabel(f)}\n hint={localizedLabel(f.form?.description) || undefined}\n >\n <FieldControl\n id={`asset-detail-${f.name}`}\n field={f}\n value={meta[f.name]}\n readOnly={false}\n onChange={(v) => setField(f.name, v)}\n />\n </Field>\n ))}\n {metaFields.length === 0 ? (\n <Text size=\"sm\" tone=\"subtle\">\n {translate('cms.asset.noMetaFields', { code1: 'alt', code2: 'copyright' })}\n </Text>\n ) : null}\n {saveError ? (\n <Text size=\"sm\" className=\"text-destructive\" data-testid=\"asset-detail-save-error\">\n {saveError}\n </Text>\n ) : null}\n {metaFields.length > 0 ? (\n <Row gap=\"2\">\n <Spacer />\n <Button\n size=\"sm\"\n onClick={() => void save()}\n disabled={!dirty || saving}\n data-testid=\"asset-detail-save\"\n >\n {saving ? translate('common.saving') : translate('common.save')}\n </Button>\n </Row>\n ) : null}\n </Stack>\n </PanelBody>\n </Panel>\n </Stack>\n )\n}\n\n/** Filename/title heading + system info (content type, dimensions, size) + actions. */\nfunction AssetDetailHeader({ projectId, record }: { projectId: string; record: AssetRecord }) {\n const dims = record.width && record.height ? `${record.width}×${record.height}` : null\n const info = [record.contentType, dims, formatBytes(record.byteSize)].filter(Boolean).join(' · ')\n return (\n <Row gap=\"3\" align=\"start\" wrap>\n <Stack gap=\"1\" className=\"min-w-0 flex-1\">\n <Heading level={2} size=\"h2\" className=\"truncate\" title={record.title ?? record.filename}>\n {record.title ?? record.filename}\n </Heading>\n {record.title ? (\n <Text size=\"sm\" tone=\"muted\" className=\"truncate\">\n {record.filename}\n </Text>\n ) : null}\n <Text size=\"xs\" tone=\"subtle\">\n {info}\n </Text>\n </Stack>\n <Row gap=\"2\">\n <AssetReferencesButton projectId={projectId} record={record} />\n <AssetDeleteButton projectId={projectId} record={record} />\n </Row>\n </Row>\n )\n}\n\n/** \"Where is this asset used?\" (EC-172): the shared references panel, EC-155-backed. */\nfunction AssetReferencesButton({ projectId, record }: { projectId: string; record: AssetRecord }) {\n const [open, setOpen] = useState(false)\n return (\n <>\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={() => setOpen(true)}\n data-testid=\"asset-detail-references\"\n >\n <Link2 /> {translate('assets.references.buttonLabel')}\n </Button>\n <Dialog open={open} onOpenChange={setOpen}>\n <DialogContent className=\"max-w-lg\">\n <DialogHeader>\n <DialogTitle>\n {translate('assets.references.title', { name: record.title ?? record.filename })}\n </DialogTitle>\n <DialogDescription>{translate('assets.references.description')}</DialogDescription>\n </DialogHeader>\n {open ? (\n <ReferencesPanel projectId={projectId} target={{ type: 'asset', id: record.id }} />\n ) : null}\n </DialogContent>\n </Dialog>\n </>\n )\n}\n\n/**\n * Reference-respecting delete (EC-151, EC-202): enumerates the exact inbound\n * references — index-accurate, with deep links to the field/node — before\n * removing. Deleting a referenced asset is weak-by-default (allowed with an\n * explicit \"delete anyway\"); every reference dangles into a visible validation\n * issue rather than silently breaking. On success, returns to the list.\n */\nfunction AssetDeleteButton({ projectId, record }: { projectId: string; record: AssetRecord }) {\n const source = useCollectionView()\n const store = useAssets()\n const navigate = useNavigate()\n const [open, setOpen] = useState(false)\n const [usages, setUsages] = useState<ReferenceEntryView[] | null>(null)\n const [error, setError] = useState<string | null>(null)\n const [busy, setBusy] = useState(false)\n\n const openDialog = async () => {\n setOpen(true)\n setError(null)\n setUsages(null)\n try {\n setUsages(await loadInboundAssetReferenceViews(projectId, record.id))\n } catch (cause) {\n setError(cause instanceof Error ? cause.message : String(cause))\n }\n }\n\n const confirm = async () => {\n setBusy(true)\n setError(null)\n // The dialog has already enumerated usages (or shown there are none) and the\n // user is confirming either way — confirm unconditionally, skipping the\n // redundant inbound re-check (EC-301 perf). Weak-by-default: a still-\n // referenced asset deletes anyway, dangling into a visible issue.\n const result = await store.remove(record.id, { confirmInUse: true })\n setBusy(false)\n if (result.outcome === 'deleted' || result.outcome === 'not-found') {\n setOpen(false)\n if (source.listPath !== undefined) {\n void navigate({ to: STUDIO_PROJECT_ROUTE, params: { projectId, _splat: source.listPath } })\n }\n } else if (result.outcome === 'blocked') {\n setError(\n translate(\n result.usageCount === 1 ? 'assets.delete.raceError.one' : 'assets.delete.raceError.other',\n { count: result.usageCount },\n ),\n )\n } else {\n setError(result.message)\n }\n }\n\n const impact = usages ? buildAssetReferenceImpact(usages) : null\n const usageCount = impact?.broken.length ?? 0\n return (\n <>\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={() => void openDialog()}\n data-testid=\"asset-detail-delete\"\n >\n <Trash2 /> {translate('common.delete')}\n </Button>\n <Dialog open={open} onOpenChange={setOpen}>\n <DialogContent className=\"max-h-[80vh] max-w-md overflow-y-auto\">\n <DialogHeader>\n <DialogTitle>{translate('assets.delete.title', { filename: record.filename })}</DialogTitle>\n <DialogDescription>{translate('assets.delete.description')}</DialogDescription>\n </DialogHeader>\n {usages === null && error === null ? (\n <Text size=\"sm\" tone=\"muted\">\n {translate('assets.delete.checking')}\n </Text>\n ) : usageCount > 0 ? (\n <Stack gap=\"2\" data-testid=\"asset-usage-warning\">\n <Row gap=\"2\">\n <AlertTriangle className=\"size-4 text-warning\" />\n <Text size=\"sm\" weight=\"semibold\">\n {translate(usageCount === 1 ? 'assets.delete.inUse.one' : 'assets.delete.inUse.other', {\n count: usageCount,\n })}\n </Text>\n </Row>\n <ReferenceImpactRows\n projectId={projectId}\n broken={impact!.broken}\n onNavigate={() => setOpen(false)}\n />\n <Text size=\"xs\" tone=\"muted\">\n {translate('assets.delete.brokenWarning')}\n </Text>\n </Stack>\n ) : (\n <Text size=\"sm\" tone=\"muted\">\n {translate('assets.delete.noReferences')}\n </Text>\n )}\n {error ? (\n <Text size=\"sm\" className=\"text-destructive\" data-testid=\"asset-delete-error\">\n {error}\n </Text>\n ) : null}\n <DialogFooter>\n <Button variant=\"ghost\" size=\"sm\" onClick={() => setOpen(false)} disabled={busy}>\n {translate('common.cancel')}\n </Button>\n <Button\n variant=\"destructive\"\n size=\"sm\"\n onClick={() => void confirm()}\n disabled={busy || usages === null}\n data-testid=\"asset-delete-confirm\"\n >\n {usageCount > 0 ? translate('assets.delete.confirmAnyway') : translate('common.delete')}\n </Button>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n </>\n )\n}\n"]}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { CmsDocument } from '@elytracms/core/cms-core';
|
|
3
|
+
import type { AssetRecord } from '@elytracms/core/persistence';
|
|
4
|
+
/**
|
|
5
|
+
* The assets rail's GRID surface (EC-319) — the thumbnail-first counterpart to
|
|
6
|
+
* `LoadedCollectionTable`, over the SAME portable `CollectionViewSource` (the
|
|
7
|
+
* same search/filter state, `applyViewState`, and count — genuine parity with
|
|
8
|
+
* list mode, not a separate ad-hoc query). A media library reads naturally as
|
|
9
|
+
* thumbnails; the universal TABLE (list mode) is still one toggle away for
|
|
10
|
+
* columns/sort.
|
|
11
|
+
*/
|
|
12
|
+
export declare function AssetGridView({ projectId, toolbarExtra, }: {
|
|
13
|
+
projectId: string;
|
|
14
|
+
toolbarExtra?: ReactNode;
|
|
15
|
+
}): import("react").JSX.Element;
|
|
16
|
+
/**
|
|
17
|
+
* One thumbnail card: image preview (focal-point aware, EC-248) + filename/title
|
|
18
|
+
* + key record metadata (dimensions, content type, size). Links to the asset's
|
|
19
|
+
* detail surface (`asset-detail.tsx`) — the same place list mode's row/"…" menu
|
|
20
|
+
* opens — so edit/delete/references live in exactly one surface, not duplicated
|
|
21
|
+
* per view mode. Pure presentational: the record is resolved once by the parent
|
|
22
|
+
* grid, not looked up per card.
|
|
23
|
+
*/
|
|
24
|
+
export declare function AssetGridCard({ doc, record, projectId, }: {
|
|
25
|
+
doc: CmsDocument;
|
|
26
|
+
record: AssetRecord | undefined;
|
|
27
|
+
projectId: string;
|
|
28
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useMemo } from 'react';
|
|
3
|
+
import { Image as ImageIcon } from 'lucide-react';
|
|
4
|
+
import { Link, STUDIO_PROJECT_ROUTE } from '@elytracms/studio/router';
|
|
5
|
+
import { applyViewState } from '@elytracms/studio/lib/cms-views';
|
|
6
|
+
import { localizedLabel, translate } from '@elytracms/studio/lib/i18n';
|
|
7
|
+
import { Code, Grid, Panel, Row, Separator, Spacer, Stack, Text } from '@elytracms/studio/ui';
|
|
8
|
+
import { FilterControl } from '../cms/filter-builder';
|
|
9
|
+
import { ViewTabs } from '../cms/view-bar';
|
|
10
|
+
import { useSetBreadcrumb } from '../breadcrumb-context';
|
|
11
|
+
import { useCollectionView } from '../collection-view/collection-view-context';
|
|
12
|
+
import { AssetThumb } from './asset-thumb';
|
|
13
|
+
import { formatBytes } from './asset-metadata';
|
|
14
|
+
import { useAssets } from './assets-store';
|
|
15
|
+
/**
|
|
16
|
+
* The assets rail's GRID surface (EC-319) — the thumbnail-first counterpart to
|
|
17
|
+
* `LoadedCollectionTable`, over the SAME portable `CollectionViewSource` (the
|
|
18
|
+
* same search/filter state, `applyViewState`, and count — genuine parity with
|
|
19
|
+
* list mode, not a separate ad-hoc query). A media library reads naturally as
|
|
20
|
+
* thumbnails; the universal TABLE (list mode) is still one toggle away for
|
|
21
|
+
* columns/sort.
|
|
22
|
+
*/
|
|
23
|
+
export function AssetGridView({ projectId, toolbarExtra, }) {
|
|
24
|
+
const source = useCollectionView();
|
|
25
|
+
const collection = source.schema;
|
|
26
|
+
const collectionId = source.collectionId;
|
|
27
|
+
useSetBreadcrumb([{ label: localizedLabel(collection.form?.label, collectionId) }]);
|
|
28
|
+
const viewState = source.viewState();
|
|
29
|
+
const docs = source.documents();
|
|
30
|
+
const { docs: visibleDocs } = useMemo(() => applyViewState(collection, docs, source.localeConfig, viewState), [collection, docs, source.localeConfig, viewState]);
|
|
31
|
+
// Resolved once per render (O(n), not per-card `.find()`) — the card stays a
|
|
32
|
+
// pure presentational component (record passed in, no store lookup of its own).
|
|
33
|
+
const store = useAssets();
|
|
34
|
+
const recordsById = useMemo(() => new Map(store.assets.map((record) => [record.id, record])), [store.assets]);
|
|
35
|
+
const pluralLabel = localizedLabel(collection.form?.label, `${collectionId}s`);
|
|
36
|
+
const singularLabel = localizedLabel(collection.form?.labelSingular, collectionId);
|
|
37
|
+
const countLabel = visibleDocs.length === docs.length
|
|
38
|
+
? translate('cms.list.count', {
|
|
39
|
+
count: docs.length,
|
|
40
|
+
noun: docs.length === 1 ? singularLabel : pluralLabel,
|
|
41
|
+
})
|
|
42
|
+
: translate('cms.list.countFiltered', {
|
|
43
|
+
visible: visibleDocs.length,
|
|
44
|
+
total: docs.length,
|
|
45
|
+
noun: pluralLabel,
|
|
46
|
+
});
|
|
47
|
+
return (_jsxs("div", { className: "flex h-full min-h-0 flex-col", children: [_jsx("div", { className: "shrink-0 border-b border-border px-4 py-3 sm:px-6", children: _jsxs(Row, { gap: "2", wrap: true, children: [_jsx(ViewTabs, {}), _jsx(FilterControl, { collection: collection }), _jsx(Separator, { orientation: "vertical", className: "h-5 self-center" }), _jsx(Text, { as: "span", size: "xs", tone: "muted", className: "whitespace-nowrap tabular-nums", "data-testid": "asset-grid-count", children: countLabel }), _jsx(Spacer, {}), toolbarExtra] }) }), _jsx("div", { className: "min-h-0 flex-1 overflow-auto p-4 sm:p-6", children: docs.length === 0 ? (_jsx("div", { className: "grid h-full place-items-center p-8", children: _jsx(Text, { tone: "muted", children: translate('cms.list.emptyNone', { noun: pluralLabel.toLowerCase() }) }) })) : visibleDocs.length === 0 ? (_jsx("div", { className: "grid h-full place-items-center p-8", children: _jsx(Text, { tone: "muted", "data-testid": "empty-filtered", children: translate('cms.list.emptyFiltered') }) })) : (_jsx(Grid, { minItemWidth: "220px", gap: "3", "data-testid": "asset-grid", children: visibleDocs.map((doc) => (_jsx(AssetGridCard, { doc: doc, record: recordsById.get(doc.id), projectId: projectId }, doc.id))) })) })] }));
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* One thumbnail card: image preview (focal-point aware, EC-248) + filename/title
|
|
51
|
+
* + key record metadata (dimensions, content type, size). Links to the asset's
|
|
52
|
+
* detail surface (`asset-detail.tsx`) — the same place list mode's row/"…" menu
|
|
53
|
+
* opens — so edit/delete/references live in exactly one surface, not duplicated
|
|
54
|
+
* per view mode. Pure presentational: the record is resolved once by the parent
|
|
55
|
+
* grid, not looked up per card.
|
|
56
|
+
*/
|
|
57
|
+
export function AssetGridCard({ doc, record, projectId, }) {
|
|
58
|
+
const source = useCollectionView();
|
|
59
|
+
const title = record?.title ?? record?.filename ?? doc.id;
|
|
60
|
+
const dims = record?.width && record?.height ? `${record.width}×${record.height}` : null;
|
|
61
|
+
const kind = record?.contentType?.split('/')[1]?.toUpperCase();
|
|
62
|
+
const splat = source.detailPath?.(doc.id);
|
|
63
|
+
const card = (_jsxs(Panel, { className: "overflow-hidden", "data-testid": "asset-grid-card", children: [_jsx(AssetThumb, { record: record, alt: title, className: "aspect-video w-full rounded-none border-0" }), _jsxs(Stack, { gap: "1", className: "p-3", children: [_jsx(Text, { size: "sm", weight: "medium", className: "truncate", title: record?.filename ?? title, children: title }), record === undefined ? (_jsxs(Row, { gap: "1", align: "center", children: [_jsx(ImageIcon, { className: "size-3.5 text-muted-foreground" }), _jsx(Text, { size: "xs", tone: "subtle", children: translate('assets.loading') })] })) : (_jsxs(Row, { gap: "1", wrap: true, children: [kind ? _jsx(Code, { tone: "muted", children: kind }) : null, dims ? _jsx(Code, { tone: "muted", children: dims }) : null, _jsx(Code, { tone: "muted", children: formatBytes(record.byteSize) })] }))] })] }));
|
|
64
|
+
if (splat === undefined)
|
|
65
|
+
return card;
|
|
66
|
+
return (_jsx(Link, { to: STUDIO_PROJECT_ROUTE, params: { projectId, _splat: splat }, className: "block rounded-lg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/40", children: card }));
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=asset-grid.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"asset-grid.js","sourceRoot":"","sources":["../../../src/components/assets/asset-grid.tsx"],"names":[],"mappings":"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","sourcesContent":["import type { ReactNode } from 'react'\nimport { useMemo } from 'react'\nimport { Image as ImageIcon } from 'lucide-react'\nimport type { CmsDocument } from '@elytracms/core/cms-core'\nimport type { AssetRecord } from '@elytracms/core/persistence'\nimport { Link, STUDIO_PROJECT_ROUTE } from '@elytracms/studio/router'\nimport { applyViewState } from '@elytracms/studio/lib/cms-views'\nimport { localizedLabel, translate } from '@elytracms/studio/lib/i18n'\nimport { Code, Grid, Panel, Row, Separator, Spacer, Stack, Text } from '@elytracms/studio/ui'\nimport { FilterControl } from '../cms/filter-builder'\nimport { ViewTabs } from '../cms/view-bar'\nimport { useSetBreadcrumb } from '../breadcrumb-context'\nimport { useCollectionView } from '../collection-view/collection-view-context'\nimport { AssetThumb } from './asset-thumb'\nimport { formatBytes } from './asset-metadata'\nimport { useAssets } from './assets-store'\n\n/**\n * The assets rail's GRID surface (EC-319) — the thumbnail-first counterpart to\n * `LoadedCollectionTable`, over the SAME portable `CollectionViewSource` (the\n * same search/filter state, `applyViewState`, and count — genuine parity with\n * list mode, not a separate ad-hoc query). A media library reads naturally as\n * thumbnails; the universal TABLE (list mode) is still one toggle away for\n * columns/sort.\n */\nexport function AssetGridView({\n projectId,\n toolbarExtra,\n}: {\n projectId: string\n toolbarExtra?: ReactNode\n}) {\n const source = useCollectionView()\n const collection = source.schema\n const collectionId = source.collectionId\n useSetBreadcrumb([{ label: localizedLabel(collection.form?.label, collectionId) }])\n\n const viewState = source.viewState()\n const docs = source.documents()\n const { docs: visibleDocs } = useMemo(\n () => applyViewState(collection, docs, source.localeConfig, viewState),\n [collection, docs, source.localeConfig, viewState],\n )\n\n // Resolved once per render (O(n), not per-card `.find()`) — the card stays a\n // pure presentational component (record passed in, no store lookup of its own).\n const store = useAssets()\n const recordsById = useMemo(\n () => new Map(store.assets.map((record) => [record.id, record])),\n [store.assets],\n )\n\n const pluralLabel = localizedLabel(collection.form?.label, `${collectionId}s`)\n const singularLabel = localizedLabel(collection.form?.labelSingular, collectionId)\n const countLabel =\n visibleDocs.length === docs.length\n ? translate('cms.list.count', {\n count: docs.length,\n noun: docs.length === 1 ? singularLabel : pluralLabel,\n })\n : translate('cms.list.countFiltered', {\n visible: visibleDocs.length,\n total: docs.length,\n noun: pluralLabel,\n })\n\n return (\n <div className=\"flex h-full min-h-0 flex-col\">\n <div className=\"shrink-0 border-b border-border px-4 py-3 sm:px-6\">\n <Row gap=\"2\" wrap>\n <ViewTabs />\n <FilterControl collection={collection} />\n <Separator orientation=\"vertical\" className=\"h-5 self-center\" />\n <Text\n as=\"span\"\n size=\"xs\"\n tone=\"muted\"\n className=\"whitespace-nowrap tabular-nums\"\n data-testid=\"asset-grid-count\"\n >\n {countLabel}\n </Text>\n <Spacer />\n {toolbarExtra}\n </Row>\n </div>\n\n <div className=\"min-h-0 flex-1 overflow-auto p-4 sm:p-6\">\n {docs.length === 0 ? (\n <div className=\"grid h-full place-items-center p-8\">\n <Text tone=\"muted\">{translate('cms.list.emptyNone', { noun: pluralLabel.toLowerCase() })}</Text>\n </div>\n ) : visibleDocs.length === 0 ? (\n <div className=\"grid h-full place-items-center p-8\">\n <Text tone=\"muted\" data-testid=\"empty-filtered\">\n {translate('cms.list.emptyFiltered')}\n </Text>\n </div>\n ) : (\n <Grid minItemWidth=\"220px\" gap=\"3\" data-testid=\"asset-grid\">\n {visibleDocs.map((doc) => (\n <AssetGridCard\n key={doc.id}\n doc={doc}\n record={recordsById.get(doc.id)}\n projectId={projectId}\n />\n ))}\n </Grid>\n )}\n </div>\n </div>\n )\n}\n\n/**\n * One thumbnail card: image preview (focal-point aware, EC-248) + filename/title\n * + key record metadata (dimensions, content type, size). Links to the asset's\n * detail surface (`asset-detail.tsx`) — the same place list mode's row/\"…\" menu\n * opens — so edit/delete/references live in exactly one surface, not duplicated\n * per view mode. Pure presentational: the record is resolved once by the parent\n * grid, not looked up per card.\n */\nexport function AssetGridCard({\n doc,\n record,\n projectId,\n}: {\n doc: CmsDocument\n record: AssetRecord | undefined\n projectId: string\n}) {\n const source = useCollectionView()\n const title = record?.title ?? record?.filename ?? doc.id\n const dims = record?.width && record?.height ? `${record.width}×${record.height}` : null\n const kind = record?.contentType?.split('/')[1]?.toUpperCase()\n const splat = source.detailPath?.(doc.id)\n\n const card = (\n <Panel className=\"overflow-hidden\" data-testid=\"asset-grid-card\">\n <AssetThumb record={record} alt={title} className=\"aspect-video w-full rounded-none border-0\" />\n <Stack gap=\"1\" className=\"p-3\">\n <Text size=\"sm\" weight=\"medium\" className=\"truncate\" title={record?.filename ?? title}>\n {title}\n </Text>\n {record === undefined ? (\n <Row gap=\"1\" align=\"center\">\n <ImageIcon className=\"size-3.5 text-muted-foreground\" />\n <Text size=\"xs\" tone=\"subtle\">\n {translate('assets.loading')}\n </Text>\n </Row>\n ) : (\n <Row gap=\"1\" wrap>\n {kind ? <Code tone=\"muted\">{kind}</Code> : null}\n {dims ? <Code tone=\"muted\">{dims}</Code> : null}\n <Code tone=\"muted\">{formatBytes(record.byteSize)}</Code>\n </Row>\n )}\n </Stack>\n </Panel>\n )\n\n if (splat === undefined) return card\n return (\n <Link\n to={STUDIO_PROJECT_ROUTE}\n params={{ projectId, _splat: splat }}\n className=\"block rounded-lg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/40\"\n >\n {card}\n </Link>\n )\n}\n"]}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { CollectionDef, FieldDef } from '@elytracms/core/cms-core';
|
|
1
2
|
import type { AssetRecord, AssetRepository, NewAssetInput } from '@elytracms/core/persistence';
|
|
2
3
|
/**
|
|
3
4
|
* Asset metadata editing (EC-152). A patch is applied through the
|
|
@@ -6,6 +7,24 @@ import type { AssetRecord, AssetRepository, NewAssetInput } from '@elytracms/cor
|
|
|
6
7
|
* (validate → authorize → mutate → event). Pure orchestration; injectable
|
|
7
8
|
* for tests.
|
|
8
9
|
*/
|
|
10
|
+
/**
|
|
11
|
+
* System columns the upload pipeline owns — never editable metadata, so they're
|
|
12
|
+
* excluded from the asset collection's schema-driven forms: the image field's
|
|
13
|
+
* options dialog (EC-301, `field-control.tsx`) and the assets rail's detail
|
|
14
|
+
* surface (EC-319, `asset-detail.tsx`) share this ONE reserved set so neither
|
|
15
|
+
* drifts from the other. Everything else a project declares on the `kind:'asset'`
|
|
16
|
+
* collection round-trips through the record's open `meta` bag.
|
|
17
|
+
*/
|
|
18
|
+
export declare const SYSTEM_ASSET_FIELDS: ReadonlySet<string>;
|
|
19
|
+
/**
|
|
20
|
+
* The asset collection's EDITABLE metadata fields — declared fields minus the
|
|
21
|
+
* system columns above and anything explicitly `readOnly` (EC-301/EC-319).
|
|
22
|
+
*/
|
|
23
|
+
export declare function assetMetaFieldsOf(collection: CollectionDef): FieldDef[];
|
|
24
|
+
/** Drop `undefined` values so a persisted meta bag stays JSON/Convex-clean. */
|
|
25
|
+
export declare function pruneMeta(meta: Record<string, unknown>): Record<string, unknown>;
|
|
26
|
+
/** Human-readable byte size (`1536` → `1.5 KB`). Shared by the asset grid + detail. */
|
|
27
|
+
export declare function formatBytes(byteSize: number): string;
|
|
9
28
|
export interface AssetMetadataPatch {
|
|
10
29
|
/**
|
|
11
30
|
* New alt text. A present key overwrites; a trimmed-empty string clears
|
|
@@ -1,3 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Asset metadata editing (EC-152). A patch is applied through the
|
|
3
|
+
* {@link AssetRepository} interface — in the studio that is the EC-141
|
|
4
|
+
* operations facade, so every edit runs the `assets.upsert` command
|
|
5
|
+
* (validate → authorize → mutate → event). Pure orchestration; injectable
|
|
6
|
+
* for tests.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* System columns the upload pipeline owns — never editable metadata, so they're
|
|
10
|
+
* excluded from the asset collection's schema-driven forms: the image field's
|
|
11
|
+
* options dialog (EC-301, `field-control.tsx`) and the assets rail's detail
|
|
12
|
+
* surface (EC-319, `asset-detail.tsx`) share this ONE reserved set so neither
|
|
13
|
+
* drifts from the other. Everything else a project declares on the `kind:'asset'`
|
|
14
|
+
* collection round-trips through the record's open `meta` bag.
|
|
15
|
+
*/
|
|
16
|
+
export const SYSTEM_ASSET_FIELDS = new Set([
|
|
17
|
+
'filename',
|
|
18
|
+
'url',
|
|
19
|
+
'contentType',
|
|
20
|
+
'byteSize',
|
|
21
|
+
'width',
|
|
22
|
+
'height',
|
|
23
|
+
'storageKey',
|
|
24
|
+
'title',
|
|
25
|
+
]);
|
|
26
|
+
/**
|
|
27
|
+
* The asset collection's EDITABLE metadata fields — declared fields minus the
|
|
28
|
+
* system columns above and anything explicitly `readOnly` (EC-301/EC-319).
|
|
29
|
+
*/
|
|
30
|
+
export function assetMetaFieldsOf(collection) {
|
|
31
|
+
return collection.fields.filter((f) => !SYSTEM_ASSET_FIELDS.has(f.name) && !f.form?.readOnly);
|
|
32
|
+
}
|
|
33
|
+
/** Drop `undefined` values so a persisted meta bag stays JSON/Convex-clean. */
|
|
34
|
+
export function pruneMeta(meta) {
|
|
35
|
+
return Object.fromEntries(Object.entries(meta).filter(([, v]) => v !== undefined));
|
|
36
|
+
}
|
|
37
|
+
/** Human-readable byte size (`1536` → `1.5 KB`). Shared by the asset grid + detail. */
|
|
38
|
+
export function formatBytes(byteSize) {
|
|
39
|
+
if (byteSize < 1024)
|
|
40
|
+
return `${byteSize} B`;
|
|
41
|
+
if (byteSize < 1024 * 1024)
|
|
42
|
+
return `${(byteSize / 1024).toFixed(1)} KB`;
|
|
43
|
+
return `${(byteSize / (1024 * 1024)).toFixed(1)} MB`;
|
|
44
|
+
}
|
|
1
45
|
/** Merge one patch field: present+empty clears, present+value sets, absent keeps. */
|
|
2
46
|
function mergeField(patchValue, patchHasKey, currentValue) {
|
|
3
47
|
if (!patchHasKey)
|
|
@@ -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 type { AssetRecord, AssetRepository, NewAssetInput } 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\nexport interface AssetMetadataPatch {\n /**\n * New alt text. A present key overwrites; a trimmed-empty string clears\n * the field. An absent key leaves the current value untouched.\n */\n alt?: string\n /** New display title; same present/empty/absent semantics as `alt`. */\n title?: string\n /**\n * New focal point (EC-248), the normalized 0–1 hotspot threaded to delivery's\n * `object-position` (EC-230). A present `{x,y}` sets it, `null` clears it, an\n * absent key leaves the current value untouched.\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`. A present object **replaces** the whole bag\n * (the image options dialog holds the complete edited meta); an absent key\n * leaves the current bag untouched.\n */\n meta?: Record<string, unknown>\n}\n\n/** Merge one patch field: present+empty clears, present+value sets, absent keeps. */\nfunction mergeField(\n patchValue: string | undefined,\n patchHasKey: boolean,\n currentValue: string | undefined,\n): string | undefined {\n if (!patchHasKey) return currentValue\n const trimmed = patchValue?.trim() ?? ''\n return trimmed.length > 0 ? trimmed : undefined\n}\n\n/** The full upsert input that re-states `current` with the patch applied. */\nexport function assetMetadataUpsertInput(\n current: AssetRecord,\n patch: AssetMetadataPatch,\n): NewAssetInput {\n const alt = mergeField(patch.alt, 'alt' in patch, current.alt)\n const title = mergeField(patch.title, 'title' in patch, current.title)\n // present `{x,y}` sets, present `null` clears, absent keeps the current point.\n const focalPoint = 'focalPoint' in patch ? (patch.focalPoint ?? undefined) : current.focalPoint\n // present object replaces the whole bag; absent keeps the current one (EC-301).\n const meta = 'meta' in patch ? patch.meta : current.meta\n return {\n id: current.id,\n filename: current.filename,\n contentType: current.contentType,\n byteSize: current.byteSize,\n storageKey: current.storageKey,\n ...(current.width !== undefined ? { width: current.width } : {}),\n ...(current.height !== undefined ? { height: current.height } : {}),\n ...(current.url !== undefined ? { url: current.url } : {}),\n ...(alt !== undefined ? { alt } : {}),\n ...(title !== undefined ? { title } : {}),\n ...(focalPoint !== undefined ? { focalPoint } : {}),\n ...(meta !== undefined ? { meta } : {}),\n }\n}\n\nexport type UpdateAssetMetadataResult =\n | { ok: true; asset: AssetRecord }\n | { ok: false; message: string }\n\n/**\n * Load the current record, apply the patch, and persist it via the\n * repository (the operations facade in the studio). Failures come back as a\n * structured result — never a throw the UI has to 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 current = await deps.assets.getAsset(projectId, assetId)\n const asset = await deps.assets.upsertAsset(projectId, assetMetadataUpsertInput(current, patch))\n return { ok: true, asset }\n } catch (cause) {\n return { ok: false, message: cause instanceof Error ? cause.message : String(cause) }\n }\n}\n"]}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { CmsDocument, CollectionDef } from '@elytracms/core/cms-core';
|
|
2
|
+
import type { AssetRecord } from '@elytracms/core/persistence';
|
|
3
|
+
import type { CollectionViewCapabilities, CollectionViewSource } from '@elytracms/studio/lib/collection-view/collection-view-source';
|
|
4
|
+
/**
|
|
5
|
+
* The capability profile of the assets rail (EC-319). Hoisted to a module-level
|
|
6
|
+
* constant (no prop/state dependency) so it's both the value the hook returns
|
|
7
|
+
* AND independently unit-testable.
|
|
8
|
+
*
|
|
9
|
+
* `create` is off — a new asset comes from an upload, never a blank draft.
|
|
10
|
+
* `remove` is off DELIBERATELY, even though the store can safely delete one
|
|
11
|
+
* (EC-202): the universal table's generic row/bulk delete runs through
|
|
12
|
+
* `ReferenceConfirmDialog`, which enumerates inbound references as
|
|
13
|
+
* `{ type: 'document' }` targets — the wrong index shape for an asset (indexed
|
|
14
|
+
* as `{ type: 'asset' }`, see `loadInboundAssetReferenceViews`). Wiring
|
|
15
|
+
* `remove: true` here would silently show "not referenced" for an asset that
|
|
16
|
+
* IS referenced. Deletion instead stays on the asset detail surface
|
|
17
|
+
* (`asset-detail.tsx`), which keeps calling the asset-specific reference check.
|
|
18
|
+
* Nothing else applies to assets: no publish/versions/locales/inline-edit/bulk/
|
|
19
|
+
* live-URL/hierarchy/import-export.
|
|
20
|
+
*/
|
|
21
|
+
export declare const ASSET_COLLECTION_CAPABILITIES: CollectionViewCapabilities;
|
|
22
|
+
/**
|
|
23
|
+
* Project a stored asset record into the lens's `values` shape (EC-319), reading
|
|
24
|
+
* only the schema's DECLARED fields — the same "schema is a lens" projection the
|
|
25
|
+
* redirects/users rails use. A system column (`filename`, `url`, dimensions, …)
|
|
26
|
+
* reads straight off the record; everything else — the built-in `alt`, a
|
|
27
|
+
* project's own `copyright`/`credit`, or an EC-311 additive field — round-trips
|
|
28
|
+
* through the record's open `meta` bag (EC-301). `alt` is schema-driven but ALSO
|
|
29
|
+
* has a legacy record column: the meta value wins, falling back to the column
|
|
30
|
+
* (matches delivery's `meta.alt ?? record.alt` read and the image field's
|
|
31
|
+
* options dialog).
|
|
32
|
+
*/
|
|
33
|
+
export declare function assetToDocument(collection: CollectionDef, record: AssetRecord): CmsDocument;
|
|
34
|
+
/**
|
|
35
|
+
* The inverse of {@link assetToDocument}: the schema-driven meta bag a save
|
|
36
|
+
* writes back — every declared field OUTSIDE the system set (EC-301/EC-311),
|
|
37
|
+
* keyed exactly as the record stores it. System columns are upload-managed and
|
|
38
|
+
* never written here.
|
|
39
|
+
*/
|
|
40
|
+
export declare function documentToAssetMetaPatch(collection: CollectionDef, doc: CmsDocument): Record<string, unknown>;
|
|
41
|
+
/**
|
|
42
|
+
* Assemble a {@link CollectionViewSource} for the **assets rail** (EC-319) — the
|
|
43
|
+
* portable list surface over the existing asset library store instead of a
|
|
44
|
+
* bespoke listing. `schema` is the DEPLOYMENT-resolved `kind:'asset'` collection
|
|
45
|
+
* (EC-311: a project's own declared fields, or a mount's `nav.assets({ fields })`
|
|
46
|
+
* override merged over it — see `resolveAssetsSchema` in `app-views/default-modules.tsx`),
|
|
47
|
+
* never a hardcoded engine default — so a project's `copyright`/`credit`/… fields
|
|
48
|
+
* (and any EC-311 override) drive the list's columns/filters exactly as they
|
|
49
|
+
* drive the image field's options dialog.
|
|
50
|
+
*
|
|
51
|
+
* Must be mounted under an `<AssetsProvider>` (for {@link useAssets}) — already
|
|
52
|
+
* ambient at the project level (EC-195), so callers rarely mount their own.
|
|
53
|
+
*/
|
|
54
|
+
export declare function useAssetsCollectionViewSource(projectId: string, schema: CollectionDef): CollectionViewSource;
|