@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
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import { AlertTriangle, ChevronDown, ChevronUp, GripVertical, ImagePlus, Link2, MoreHorizontal, Pencil, Plus, RotateCcw, SlidersHorizontal, Trash2, Upload, X } from 'lucide-react';
|
|
2
|
+
import { AlertTriangle, ChevronDown, ChevronUp, GripVertical, ImagePlus, Link2, MoreHorizontal, Pencil, Plus, RotateCcw, RotateCw, SlidersHorizontal, Trash2, Upload, X } from 'lucide-react';
|
|
3
3
|
import { createContext, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
|
4
4
|
import { Link, STUDIO_PROJECT_ROUTE, studioPath } from '@elytracms/studio/router';
|
|
5
|
-
import { BUILTIN_FIELD_CONTROLS, assetAcceptAttr, assetMatchesAccept, assetNode, assetSubfieldsOf, describeAccept, descendantIds, isFieldHidden, parentRelationField, readAssetId, } from '@elytracms/core/cms-core';
|
|
5
|
+
import { BUILTIN_FIELD_CONTROLS, applySlugify, assetAcceptAttr, assetMatchesAccept, assetNode, assetSubfieldsOf, describeAccept, descendantIds, findSlugCollision, isFieldHidden, parentRelationField, readAssetId, resolveSlugPath, slugSourceValue, } from '@elytracms/core/cms-core';
|
|
6
|
+
import { resolveDeploymentRoutes } from '@elytracms/studio/lib/workspace';
|
|
6
7
|
import { Button, Card, Code, Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, Field, Input, Row, RowItem, Select, Spacer, Stack, Text, } from '@elytracms/studio/ui';
|
|
7
8
|
import { cn } from '@elytracms/studio/lib/utils';
|
|
8
9
|
import { localizedLabel, translate } from '@elytracms/studio/lib/i18n';
|
|
10
|
+
import { activeLocaleValues } from '@elytracms/studio/lib/locale';
|
|
9
11
|
import { applyAssetSelection, assetIdsOf, singleAssetIdOf } from '../assets/asset-selection';
|
|
10
12
|
import { useOptionalAssets } from '../assets/assets-store';
|
|
11
13
|
import { AssetThumb, FocalCropPreviews, FocalPointPicker, useAssetRecord } from '../assets/asset-thumb';
|
|
12
14
|
import { newlyAddedAssetIds } from '../assets/focal-point';
|
|
15
|
+
import { assetMetaFieldsOf, pruneMeta } from '../assets/asset-metadata';
|
|
13
16
|
import { fieldLabel } from './cms-store';
|
|
14
17
|
import { useCollectionView } from '../collection-view/collection-view-context';
|
|
15
18
|
import { useFieldDensity } from './field-density';
|
|
@@ -50,7 +53,26 @@ export function FieldControl(props) {
|
|
|
50
53
|
}
|
|
51
54
|
return builtIn;
|
|
52
55
|
}
|
|
56
|
+
/** The conventional slug field name (mirrors `url-for-document`'s `SLUG_FIELD`). */
|
|
57
|
+
const SLUG_FIELD_NAME = 'slug';
|
|
58
|
+
/** The built-in control name that opts any `text` field into the slug control (EC-315). */
|
|
59
|
+
const SLUG_CONTROL = 'slug';
|
|
60
|
+
/**
|
|
61
|
+
* EC-315: a `text` field is rendered by the smart slug control when it opts in via
|
|
62
|
+
* `form.control: 'slug'`, OR (the default auto-upgrade) when it is simply named
|
|
63
|
+
* `slug` — the long-standing convention `url-for-document`/hierarchy already key off.
|
|
64
|
+
*/
|
|
65
|
+
function isSlugControl(field) {
|
|
66
|
+
return field.type === 'text' && (field.form?.control === SLUG_CONTROL || field.name === SLUG_FIELD_NAME);
|
|
67
|
+
}
|
|
53
68
|
function BuiltInFieldControl({ field, value, onChange, readOnly, id }) {
|
|
69
|
+
// EC-315: the smart slug control (composed path, auto-slug, per-locale segments)
|
|
70
|
+
// subsumes the old inline `slug` input + PathPreview. It degrades to a plain input
|
|
71
|
+
// when the collection isn't routable (name-based auto-upgrade only) or no document
|
|
72
|
+
// editor context is present (e.g. a table-cell picker).
|
|
73
|
+
if (isSlugControl(field)) {
|
|
74
|
+
return _jsx(SlugField, { field: field, value: value, onChange: onChange, readOnly: readOnly, id: id });
|
|
75
|
+
}
|
|
54
76
|
switch (field.type) {
|
|
55
77
|
case 'boolean':
|
|
56
78
|
return (_jsxs("label", { className: "flex h-8 items-center gap-2 text-sm", children: [_jsx("input", { id: id, type: "checkbox", checked: value === true, disabled: readOnly, onChange: (e) => onChange(e.target.checked), className: "size-4 accent-primary disabled:opacity-50" }), _jsx(Text, { size: "sm", tone: "muted", children: translate(value === true ? 'common.true' : 'common.false') })] }));
|
|
@@ -99,6 +121,144 @@ export function SegmentedOptions({ field, value, onChange, readOnly, id, }) {
|
|
|
99
121
|
return (_jsx(Button, { type: "button", variant: active ? 'default' : 'outline', size: "sm", role: "radio", "aria-checked": active, title: localizedLabel(opt.description) || undefined, disabled: readOnly, onClick: () => onChange(opt.value), children: localizedLabel(opt.label, opt.value) }, opt.value));
|
|
100
122
|
}) }));
|
|
101
123
|
}
|
|
124
|
+
/**
|
|
125
|
+
* EC-315 — the smart slug / URL-segment control. A `text` field named `slug` (or any
|
|
126
|
+
* `text` field with `form.control: 'slug'`) renders here instead of a bare input:
|
|
127
|
+
*
|
|
128
|
+
* - **Composed path**: the route-derived URL, decomposed by {@link resolveSlugPath}
|
|
129
|
+
* into read-only parent/context segments + the editable slug segment inline + the
|
|
130
|
+
* locale prefix (non-default locales). Reuses the same `urlForDocument` the live
|
|
131
|
+
* site uses, so the preview matches what visitors get. Draft-derived (updates as
|
|
132
|
+
* you type); the live "View live" button in the header stays the published open.
|
|
133
|
+
* - **Auto-slug**: while the slug is empty or was last produced by auto-derivation,
|
|
134
|
+
* it live-derives from the source field (default: the collection's `titleField`).
|
|
135
|
+
* A manual edit stops the auto-fill; the ↻ button always re-derives.
|
|
136
|
+
* - **Per-locale**: reads/writes through the shared `value`/`onChange` seam, so a
|
|
137
|
+
* `localized` slug edits the active locale's segment with no bespoke locale code.
|
|
138
|
+
* - **Uniqueness**: an inline warning when the candidate collides with a sibling in
|
|
139
|
+
* the same scope (same parent when hierarchical, same locale). A custom
|
|
140
|
+
* `slug.unique` hook replaces the default sibling scan.
|
|
141
|
+
*
|
|
142
|
+
* Degrades to a plain input when there is no document editor context (a table-cell
|
|
143
|
+
* picker) or when a name-based auto-upgrade lands on a non-routable collection.
|
|
144
|
+
*/
|
|
145
|
+
function SlugField({ field, value, onChange, readOnly, id, }) {
|
|
146
|
+
const source = useCollectionView();
|
|
147
|
+
const editor = useOptionalDocumentEditor();
|
|
148
|
+
const slugConfig = field.slug;
|
|
149
|
+
const explicit = field.form?.control === SLUG_CONTROL;
|
|
150
|
+
const current = typeof value === 'string' ? value : '';
|
|
151
|
+
const locale = source.activeLocale;
|
|
152
|
+
const localeConfig = source.localeConfig;
|
|
153
|
+
// 0.0.39 regression fix: resolve the deployment routes on EVERY render (a cheap
|
|
154
|
+
// synchronous accessor), never freeze them at first paint. The earlier
|
|
155
|
+
// `useMemo(() => resolveDeploymentRoutes(), [])` captured the EMPTY route set from
|
|
156
|
+
// before the workspace/config finished loading — so the composed path never
|
|
157
|
+
// appeared once routes arrived, and a stale set could paint a misleading prefix.
|
|
158
|
+
const routes = resolveDeploymentRoutes();
|
|
159
|
+
const routable = routes.some((r) => r.document?.collection === source.collectionId);
|
|
160
|
+
const draft = editor?.draft;
|
|
161
|
+
// The active-locale value bag the auto-slug source derives from (title, etc.).
|
|
162
|
+
const values = useMemo(() => (draft ? activeLocaleValues(draft, locale) : {}), [draft, locale]);
|
|
163
|
+
const derived = applySlugify(slugConfig, slugSourceValue(slugConfig, source.schema, values));
|
|
164
|
+
// Track whether the slug was manually set — auto-derive stays active while the
|
|
165
|
+
// slug is empty or was last produced by auto-derivation (never overwrites a manual
|
|
166
|
+
// slug). Seeded true when the loaded document already carries a slug.
|
|
167
|
+
const [manuallyEdited, setManuallyEdited] = useState(() => current.trim() !== '');
|
|
168
|
+
const onChangeRef = useRef(onChange);
|
|
169
|
+
onChangeRef.current = onChange;
|
|
170
|
+
useEffect(() => {
|
|
171
|
+
if (readOnly || manuallyEdited)
|
|
172
|
+
return;
|
|
173
|
+
if (derived && derived !== current)
|
|
174
|
+
onChangeRef.current(derived);
|
|
175
|
+
}, [derived, manuallyEdited, readOnly, current]);
|
|
176
|
+
// The composed path parts (routable collections with a document editor only).
|
|
177
|
+
const siblings = draft ? source.documents() : [];
|
|
178
|
+
// Draft-derived (routes + locale only, no `frontendUrl`) — the composed path is a
|
|
179
|
+
// read-only preview that updates as you type, mirroring the detail editor's draft
|
|
180
|
+
// URL. The header's published-gated "View live" stays the sole open-live action,
|
|
181
|
+
// so the draft/live distinction is preserved (an unpublished slug never links out).
|
|
182
|
+
const parts = draft && routable
|
|
183
|
+
? resolveSlugPath({ routes, localeConfig }, source.schema, draft, { siblings, locale })
|
|
184
|
+
: undefined;
|
|
185
|
+
// Default (sync) uniqueness — the first colliding sibling's title, or null.
|
|
186
|
+
const defaultCollisionTitle = useMemo(() => {
|
|
187
|
+
if (slugConfig?.unique || !draft)
|
|
188
|
+
return null;
|
|
189
|
+
const trimmed = current.trim();
|
|
190
|
+
if (trimmed === '')
|
|
191
|
+
return null;
|
|
192
|
+
const hit = findSlugCollision(trimmed, draft, source.documents(), {
|
|
193
|
+
collection: source.schema,
|
|
194
|
+
locale,
|
|
195
|
+
localeConfig,
|
|
196
|
+
});
|
|
197
|
+
return hit ? source.titleOf({ collection: source.collectionId, id: hit.id }) : null;
|
|
198
|
+
}, [current, draft, slugConfig, locale, localeConfig, source]);
|
|
199
|
+
// Custom (possibly async) uniqueness hook — race-guarded.
|
|
200
|
+
const [asyncTaken, setAsyncTaken] = useState(false);
|
|
201
|
+
useEffect(() => {
|
|
202
|
+
const unique = slugConfig?.unique;
|
|
203
|
+
if (!unique || !draft) {
|
|
204
|
+
setAsyncTaken(false);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
const trimmed = current.trim();
|
|
208
|
+
if (trimmed === '') {
|
|
209
|
+
setAsyncTaken(false);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
let cancelled = false;
|
|
213
|
+
Promise.resolve(unique(trimmed, { document: draft, locale, siblings: source.documents() }))
|
|
214
|
+
.then((ok) => {
|
|
215
|
+
if (!cancelled)
|
|
216
|
+
setAsyncTaken(ok === false);
|
|
217
|
+
})
|
|
218
|
+
.catch(() => {
|
|
219
|
+
if (!cancelled)
|
|
220
|
+
setAsyncTaken(false);
|
|
221
|
+
});
|
|
222
|
+
return () => {
|
|
223
|
+
cancelled = true;
|
|
224
|
+
};
|
|
225
|
+
}, [current, slugConfig, draft, locale, source]);
|
|
226
|
+
const collisionMessage = slugConfig?.unique
|
|
227
|
+
? asyncTaken
|
|
228
|
+
? translate('cms.field.slug.takenGeneric')
|
|
229
|
+
: undefined
|
|
230
|
+
: defaultCollisionTitle
|
|
231
|
+
? translate('cms.field.slug.taken', { title: defaultCollisionTitle })
|
|
232
|
+
: undefined;
|
|
233
|
+
function handleInput(next) {
|
|
234
|
+
onChange(next || undefined);
|
|
235
|
+
// Clearing re-enables auto-derive; any non-empty entry is a manual override.
|
|
236
|
+
setManuallyEdited(next.trim() !== '');
|
|
237
|
+
}
|
|
238
|
+
function regenerate() {
|
|
239
|
+
setManuallyEdited(false);
|
|
240
|
+
const next = applySlugify(slugConfig, slugSourceValue(slugConfig, source.schema, values));
|
|
241
|
+
onChange(next || undefined);
|
|
242
|
+
}
|
|
243
|
+
// No document editor (a picker), or a name-based auto-upgrade on a non-routable
|
|
244
|
+
// collection: keep the historical plain input, no slug chrome (no regression).
|
|
245
|
+
if (!editor || (!explicit && !routable)) {
|
|
246
|
+
return (_jsx(Input, { id: id, disabled: readOnly, value: current, onChange: (e) => onChange(e.target.value || undefined) }));
|
|
247
|
+
}
|
|
248
|
+
const input = (_jsx(Input, { id: id, disabled: readOnly, value: current, placeholder: derived || undefined, onChange: (e) => handleInput(e.target.value), className: cn(parts ? 'h-7 min-w-[10ch] flex-1 bg-background font-mono text-xs' : 'flex-1') }));
|
|
249
|
+
const regenerateButton = !readOnly ? (_jsx(Button, { type: "button", variant: "ghost", size: "icon-sm", onClick: regenerate, "aria-label": translate('cms.field.slug.regenerate'), title: translate('cms.field.slug.regenerate'), children: _jsx(RotateCw, {}) })) : null;
|
|
250
|
+
// The compact, ellipsized read-only prefix (0.0.39 "hybrid split"): only the LAST
|
|
251
|
+
// context segment (`…/produkte/`), so a deep path can't overflow the narrow Details
|
|
252
|
+
// sidebar. The full composed path is the tooltip AND the document-level URL row in
|
|
253
|
+
// the Details pane (which owns the composed URL — a first-class document property).
|
|
254
|
+
const prefixSegments = parts
|
|
255
|
+
? [...(parts.localePrefix ? [parts.localePrefix] : []), ...parts.parentSegments]
|
|
256
|
+
: [];
|
|
257
|
+
const lastSegment = prefixSegments[prefixSegments.length - 1];
|
|
258
|
+
const compactPrefix = lastSegment ? `${prefixSegments.length > 1 ? '…/' : '/'}${lastSegment}/` : '/';
|
|
259
|
+
const prefixTitle = parts?.path ?? (prefixSegments.length > 0 ? `/${prefixSegments.join('/')}/` : '/');
|
|
260
|
+
return (_jsxs(Stack, { gap: "2", children: [parts ? (_jsxs(Row, { gap: "1", align: "center", "data-testid": "slug-composed-path", children: [_jsx("span", { className: "shrink-0 rounded bg-muted/40 px-1.5 py-1 font-mono text-xs text-muted-foreground ring-1 ring-inset ring-border", title: prefixTitle, "data-testid": "slug-prefix", children: compactPrefix }), input, regenerateButton] })) : (_jsxs(Row, { gap: "1", align: "center", children: [input, regenerateButton] })), collisionMessage ? (_jsxs(Row, { gap: "1", align: "center", "data-testid": "slug-collision", children: [_jsx(AlertTriangle, { className: "size-3.5 shrink-0 text-warning", "aria-hidden": true }), _jsx(Text, { size: "xs", tone: "muted", children: collisionMessage })] })) : null] }));
|
|
261
|
+
}
|
|
102
262
|
function asRecord(value) {
|
|
103
263
|
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
104
264
|
? value
|
|
@@ -522,10 +682,7 @@ function AssetField({ field, value, onChange, readOnly, }) {
|
|
|
522
682
|
// is the belt to the picker's stopPropagation braces: the field cannot react at all.
|
|
523
683
|
const dragProps = canUpload && !open ? { onDragEnter, onDragOver, onDragLeave, onDrop } : {};
|
|
524
684
|
const selectButton = !readOnly ? (_jsxs(Button, { variant: "outline", size: "sm", onClick: () => setOpen(true), children: [_jsx(ImagePlus, {}), " ", translate('common.select')] })) : null;
|
|
525
|
-
return (_jsxs(Stack, { gap: "2", className: cn('relative rounded-lg transition-shadow', dragging && 'ring-2 ring-primary ring-offset-2 ring-offset-background'), ...dragProps, children: [many && hasSub ? (
|
|
526
|
-
// Gallery with per-use sub-fields (EC-301): a card per placement — thumbnail +
|
|
527
|
-
// its own sub-field form — reusing the object/repeater item rendering.
|
|
528
|
-
_jsxs(Stack, { gap: "2", children: [entries.length > 0 ? (_jsx(Stack, { gap: "2", children: entries.map((entry, i) => {
|
|
685
|
+
return (_jsxs(Stack, { gap: "2", className: cn('relative rounded-lg transition-shadow', dragging && 'ring-2 ring-primary ring-offset-2 ring-offset-background'), ...dragProps, children: [many && hasSub ? (_jsxs(Stack, { gap: "2", children: [entries.length > 0 ? (_jsx(Stack, { gap: "2", children: entries.map((entry, i) => {
|
|
529
686
|
const aid = readAssetId(entry);
|
|
530
687
|
if (!aid)
|
|
531
688
|
return null;
|
|
@@ -534,12 +691,7 @@ function AssetField({ field, value, onChange, readOnly, }) {
|
|
|
534
691
|
nextEntries[i] = assetNode(aid, next);
|
|
535
692
|
onChange(nextEntries);
|
|
536
693
|
}, onRemove: readOnly ? undefined : () => onChange(entries.filter((_, j) => j !== i)) }, `${aid}-${i}`));
|
|
537
|
-
}) })) : (_jsx(EmptyAssetState, { compact: compact, canUpload: canUpload, onSelect: () => setOpen(true) })), _jsx(Row, { gap: "2", wrap: true, align: "center", children: selectButton })] })) : many ? (
|
|
538
|
-
// Gallery: a grid of focal-aware previews + the single Select entry.
|
|
539
|
-
_jsxs(Stack, { gap: "2", children: [ids.length > 0 ? (_jsx("div", { className: cn('grid gap-2', compact ? 'grid-cols-2' : 'grid-cols-3 sm:grid-cols-4'), children: ids.map((aid, i) => (_jsx(AssetChip, { assetId: aid, collectionId: assetCollectionId, onRemove: readOnly ? undefined : () => onChange(ids.filter((x) => x !== aid)) }, `${aid}-${i}`))) })) : (_jsx(EmptyAssetState, { compact: compact, canUpload: canUpload, onSelect: () => setOpen(true) })), _jsx(Row, { gap: "2", wrap: true, align: "center", children: selectButton })] })) : single && hasSub ? (
|
|
540
|
-
// With per-use sub-fields the asset + its fields read as ONE grouped unit
|
|
541
|
-
// (like an object), so it's clear they belong together (EC-301 feedback).
|
|
542
|
-
_jsx(Card, { className: "border-l-2 border-l-border p-3", children: _jsxs(Stack, { gap: "3", children: [_jsx(AssetSinglePreview, { assetId: single, compact: compact, readOnly: readOnly, hasLibrary: assets !== null, fallbackTitle: source.titleOf({ collection: assetCollectionId, id: single }), selectButton: selectButton, onEdit: () => setEditing(true), onRemove: () => onChange(applyAssetSelection(value, null, field.multiple)) }), _jsx("div", { className: "border-t border-border/60 pt-3", children: _jsx(ObjectItemForm, { fields: subFields, value: assetSubfieldsOf(value), readOnly: readOnly, idPrefix: "asset-sub", onChange: (next) => onChange(assetNode(single, next)) }) })] }) })) : single ? (_jsx(AssetSinglePreview, { assetId: single, compact: compact, readOnly: readOnly, hasLibrary: assets !== null, fallbackTitle: source.titleOf({ collection: assetCollectionId, id: single }), selectButton: selectButton, onEdit: () => setEditing(true), onRemove: () => onChange(applyAssetSelection(value, null, field.multiple)) })) : (_jsxs(Stack, { gap: "2", children: [_jsx(EmptyAssetState, { compact: compact, canUpload: canUpload, onSelect: () => setOpen(true) }), _jsx(Row, { gap: "2", wrap: true, align: "center", children: selectButton })] })), !many && single && assets ? (_jsx(AssetOptionsDialog, { assetId: single, readOnly: readOnly, open: editing, onOpenChange: setEditing })) : null, _jsx(AssetPicker, { value: single, open: open, onOpenChange: setOpen, accept: assetAcceptAttr(field.accept), onChange: applyPicked }), rejected.length > 0 ? (_jsxs(Row, { gap: "2", align: "center", children: [_jsx(AlertTriangle, { className: "size-4 shrink-0 text-warning" }), _jsx(Text, { size: "xs", tone: "muted", children: rejected.length === 1
|
|
694
|
+
}) })) : (_jsx(EmptyAssetState, { compact: compact, canUpload: canUpload, onSelect: () => setOpen(true) })), _jsx(Row, { gap: "2", wrap: true, align: "center", children: selectButton })] })) : many ? (_jsxs(Stack, { gap: "2", children: [ids.length > 0 ? (_jsx("div", { className: cn('grid gap-2', compact ? 'grid-cols-2' : 'grid-cols-3 sm:grid-cols-4'), children: ids.map((aid, i) => (_jsx(AssetChip, { assetId: aid, collectionId: assetCollectionId, onRemove: readOnly ? undefined : () => onChange(ids.filter((x) => x !== aid)) }, `${aid}-${i}`))) })) : (_jsx(EmptyAssetState, { compact: compact, canUpload: canUpload, onSelect: () => setOpen(true) })), _jsx(Row, { gap: "2", wrap: true, align: "center", children: selectButton })] })) : single && hasSub ? (_jsx(Card, { className: "border-l-2 border-l-border p-3", children: _jsxs(Stack, { gap: "3", children: [_jsx(AssetSinglePreview, { assetId: single, compact: compact, readOnly: readOnly, hasLibrary: assets !== null, fallbackTitle: source.titleOf({ collection: assetCollectionId, id: single }), selectButton: selectButton, onEdit: () => setEditing(true), onRemove: () => onChange(applyAssetSelection(value, null, field.multiple)) }), _jsx("div", { className: "border-t border-border/60 pt-3", children: _jsx(ObjectItemForm, { fields: subFields, value: assetSubfieldsOf(value), readOnly: readOnly, idPrefix: "asset-sub", onChange: (next) => onChange(assetNode(single, next)) }) })] }) })) : single ? (_jsx(AssetSinglePreview, { assetId: single, compact: compact, readOnly: readOnly, hasLibrary: assets !== null, fallbackTitle: source.titleOf({ collection: assetCollectionId, id: single }), selectButton: selectButton, onEdit: () => setEditing(true), onRemove: () => onChange(applyAssetSelection(value, null, field.multiple)) })) : (_jsxs(Stack, { gap: "2", children: [_jsx(EmptyAssetState, { compact: compact, canUpload: canUpload, onSelect: () => setOpen(true) }), _jsx(Row, { gap: "2", wrap: true, align: "center", children: selectButton })] })), !many && single && assets ? (_jsx(AssetOptionsDialog, { assetId: single, readOnly: readOnly, open: editing, onOpenChange: setEditing })) : null, _jsx(AssetPicker, { value: single, open: open, onOpenChange: setOpen, accept: assetAcceptAttr(field.accept), onChange: applyPicked }), rejected.length > 0 ? (_jsxs(Row, { gap: "2", align: "center", children: [_jsx(AlertTriangle, { className: "size-4 shrink-0 text-warning" }), _jsx(Text, { size: "xs", tone: "muted", children: rejected.length === 1
|
|
543
695
|
? translate('cms.asset.rejected.one', {
|
|
544
696
|
name: rejected[0],
|
|
545
697
|
types: describeAccept(field.accept),
|
|
@@ -605,25 +757,6 @@ function AssetGalleryItem({ assetId, collectionId, fields, value, onChangeSubs,
|
|
|
605
757
|
const title = source.titleOf({ collection: collectionId, id: assetId });
|
|
606
758
|
return (_jsxs(Card, { className: "border-l-2 border-l-border p-0", children: [_jsxs(Row, { gap: "2", align: "center", className: "border-b border-border/60 bg-muted/30 px-2 py-1.5", children: [_jsx(AssetThumb, { record: record, alt: title, className: "size-10 shrink-0" }), _jsx(Text, { size: "sm", className: "min-w-0 flex-1 truncate font-medium", title: title, children: title }), onRemove ? (_jsx(Button, { variant: "ghost", size: "sm", "aria-label": translate('common.remove'), onClick: onRemove, children: _jsx(Trash2, { className: "size-3.5" }) })) : null] }), _jsx("div", { className: "px-3 pb-3", children: _jsx(ObjectItemForm, { fields: fields, value: value, onChange: onChangeSubs, readOnly: readOnly, idPrefix: idPrefix }) })] }));
|
|
607
759
|
}
|
|
608
|
-
/**
|
|
609
|
-
* System columns the upload pipeline owns — never editable metadata, so they're
|
|
610
|
-
* excluded from the asset collection's schema-driven options form (EC-301). The
|
|
611
|
-
* filename + dimensions already show in the field's info column.
|
|
612
|
-
*/
|
|
613
|
-
const SYSTEM_ASSET_FIELDS = new Set([
|
|
614
|
-
'filename',
|
|
615
|
-
'url',
|
|
616
|
-
'contentType',
|
|
617
|
-
'byteSize',
|
|
618
|
-
'width',
|
|
619
|
-
'height',
|
|
620
|
-
'storageKey',
|
|
621
|
-
'title',
|
|
622
|
-
]);
|
|
623
|
-
/** Drop `undefined` values so the persisted meta bag stays JSON/Convex-clean. */
|
|
624
|
-
function pruneMeta(meta) {
|
|
625
|
-
return Object.fromEntries(Object.entries(meta).filter(([, v]) => v !== undefined));
|
|
626
|
-
}
|
|
627
760
|
/**
|
|
628
761
|
* The image options dialog (EC-301): the focal point (engine-special visual
|
|
629
762
|
* control) + the asset's **schema-driven** metadata, rendered from the
|
|
@@ -638,9 +771,9 @@ function AssetOptionsDialog({ assetId, readOnly, open, onOpenChange, }) {
|
|
|
638
771
|
const assets = useOptionalAssets();
|
|
639
772
|
const record = useAssetRecord(assetId);
|
|
640
773
|
const metaFields = useMemo(() => {
|
|
641
|
-
const
|
|
642
|
-
const collection = source.collections.find((c) => c.id ===
|
|
643
|
-
return
|
|
774
|
+
const assetCollectionId = source.assetCollectionId ?? 'asset';
|
|
775
|
+
const collection = source.collections.find((c) => c.id === assetCollectionId);
|
|
776
|
+
return collection ? assetMetaFieldsOf(collection) : [];
|
|
644
777
|
}, [source]);
|
|
645
778
|
// Local edit buffers, seeded once per open from the record (so an external edit
|
|
646
779
|
// still wins between opens). Metadata is non-localized in v1 (the asset
|