@elytracms/studio 0.0.41 → 0.0.43
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/dist/components/assets/asset-detail.js +1 -1
- package/dist/components/assets/asset-detail.js.map +1 -1
- package/dist/components/assets/assets-module.js +19 -6
- package/dist/components/assets/assets-module.js.map +1 -1
- package/dist/components/assets/assets-store.d.ts +2 -1
- package/dist/components/assets/assets-store.js +4 -8
- package/dist/components/assets/assets-store.js.map +1 -1
- package/dist/components/assets/assets-view-preference.d.ts +0 -8
- package/dist/components/assets/assets-view-preference.js +12 -13
- package/dist/components/assets/assets-view-preference.js.map +1 -1
- package/dist/components/auth-context.d.ts +2 -1
- package/dist/components/auth-context.js +6 -8
- package/dist/components/auth-context.js.map +1 -1
- package/dist/components/builder/builder-store.d.ts +2 -2
- package/dist/components/builder/builder-store.js +4 -8
- package/dist/components/builder/builder-store.js.map +1 -1
- package/dist/components/builder/canvas.js +16 -2
- package/dist/components/builder/canvas.js.map +1 -1
- package/dist/components/cms/bulk-actions.js +2 -1
- package/dist/components/cms/bulk-actions.js.map +1 -1
- package/dist/components/cms/cell-edit.js +2 -1
- package/dist/components/cms/cell-edit.js.map +1 -1
- package/dist/components/cms/cms-composition-validators.d.ts +25 -0
- package/dist/components/cms/cms-composition-validators.js +75 -0
- package/dist/components/cms/cms-composition-validators.js.map +1 -0
- package/dist/components/cms/cms-labels.d.ts +15 -0
- package/dist/components/cms/cms-labels.js +27 -0
- package/dist/components/cms/cms-labels.js.map +1 -0
- package/dist/components/cms/cms-store.d.ts +23 -10
- package/dist/components/cms/cms-store.js +78 -154
- package/dist/components/cms/cms-store.js.map +1 -1
- package/dist/components/cms/collection-table.js +1 -1
- package/dist/components/cms/collection-table.js.map +1 -1
- package/dist/components/cms/detail-editor.js +67 -67
- package/dist/components/cms/detail-editor.js.map +1 -1
- package/dist/components/cms/details-pane.d.ts +11 -21
- package/dist/components/cms/details-pane.js +14 -31
- package/dist/components/cms/details-pane.js.map +1 -1
- package/dist/components/cms/discard-draft.d.ts +18 -0
- package/dist/components/cms/discard-draft.js +18 -0
- package/dist/components/cms/discard-draft.js.map +1 -1
- package/dist/components/cms/document-editor-context.d.ts +2 -1
- package/dist/components/cms/document-editor-context.js +4 -9
- package/dist/components/cms/document-editor-context.js.map +1 -1
- package/dist/components/cms/document-ref-utils.d.ts +33 -0
- package/dist/components/cms/document-ref-utils.js +47 -0
- package/dist/components/cms/document-ref-utils.js.map +1 -0
- package/dist/components/cms/document-url-path.d.ts +39 -0
- package/dist/components/cms/document-url-path.js +60 -0
- package/dist/components/cms/document-url-path.js.map +1 -0
- package/dist/components/cms/field-control.js +19 -13
- package/dist/components/cms/field-control.js.map +1 -1
- package/dist/components/cms/issues-pane.js +1 -1
- package/dist/components/cms/issues-pane.js.map +1 -1
- package/dist/components/cms/rich-text/block-commands.d.ts +36 -0
- package/dist/components/cms/rich-text/block-commands.js +47 -0
- package/dist/components/cms/rich-text/block-commands.js.map +1 -0
- package/dist/components/cms/rich-text/slash-command.js +25 -8
- package/dist/components/cms/rich-text/slash-command.js.map +1 -1
- package/dist/components/cms/rich-text/toolbar.js +3 -2
- package/dist/components/cms/rich-text/toolbar.js.map +1 -1
- package/dist/components/cms/version-compare.js +1 -1
- package/dist/components/cms/version-compare.js.map +1 -1
- package/dist/components/cms/versions-model.d.ts +31 -0
- package/dist/components/cms/versions-model.js +30 -0
- package/dist/components/cms/versions-model.js.map +1 -1
- package/dist/components/cms/view-bar.js +9 -6
- package/dist/components/cms/view-bar.js.map +1 -1
- package/dist/components/collab/collab-context.d.ts +2 -1
- package/dist/components/collab/collab-context.js +4 -8
- package/dist/components/collab/collab-context.js.map +1 -1
- package/dist/components/collection-view/collection-view-context.d.ts +9 -1
- package/dist/components/collection-view/collection-view-context.js +4 -9
- package/dist/components/collection-view/collection-view-context.js.map +1 -1
- package/dist/components/components/component-detail.js +2 -5
- package/dist/components/components/component-detail.js.map +1 -1
- package/dist/components/components/component-preview.js +8 -4
- package/dist/components/components/component-preview.js.map +1 -1
- package/dist/components/components/registry-browser.js +24 -5
- package/dist/components/components/registry-browser.js.map +1 -1
- package/dist/components/components/transfer-dialog.d.ts +10 -0
- package/dist/components/components/transfer-dialog.js +24 -8
- package/dist/components/components/transfer-dialog.js.map +1 -1
- package/dist/components/delivery-health-banner.d.ts +12 -6
- package/dist/components/delivery-health-banner.js +27 -29
- package/dist/components/delivery-health-banner.js.map +1 -1
- package/dist/components/dev-login-badge.d.ts +0 -2
- package/dist/components/dev-login-badge.js +1 -3
- package/dist/components/dev-login-badge.js.map +1 -1
- package/dist/components/inspector/inspector-sidebar.js +20 -3
- package/dist/components/inspector/inspector-sidebar.js.map +1 -1
- package/dist/components/playground-badge.d.ts +0 -2
- package/dist/components/playground-badge.js +0 -2
- package/dist/components/playground-badge.js.map +1 -1
- package/dist/components/project-list.js +2 -6
- package/dist/components/project-list.js.map +1 -1
- package/dist/components/project-session.d.ts +6 -2
- package/dist/components/project-session.js +32 -83
- package/dist/components/project-session.js.map +1 -1
- package/dist/components/project-switcher.js +2 -6
- package/dist/components/project-switcher.js.map +1 -1
- package/dist/components/redirects/redirects-store.d.ts +2 -1
- package/dist/components/redirects/redirects-store.js +4 -8
- package/dist/components/redirects/redirects-store.js.map +1 -1
- package/dist/components/studio-context.d.ts +2 -1
- package/dist/components/studio-context.js +4 -8
- package/dist/components/studio-context.js.map +1 -1
- package/dist/components/studio-shell.js +1 -1
- package/dist/components/studio-shell.js.map +1 -1
- package/dist/components/theme-context.d.ts +2 -1
- package/dist/components/theme-context.js +4 -8
- package/dist/components/theme-context.js.map +1 -1
- package/dist/components/use-hydration-phase.d.ts +65 -0
- package/dist/components/use-hydration-phase.js +64 -0
- package/dist/components/use-hydration-phase.js.map +1 -0
- package/dist/components/users/users-store.d.ts +2 -1
- package/dist/components/users/users-store.js +4 -8
- package/dist/components/users/users-store.js.map +1 -1
- package/dist/components/workspace-projects.d.ts +14 -0
- package/dist/components/workspace-projects.js +21 -0
- package/dist/components/workspace-projects.js.map +1 -1
- package/dist/lib/data/data-store.d.ts +2 -1
- package/dist/lib/data/data-store.js +4 -8
- package/dist/lib/data/data-store.js.map +1 -1
- package/dist/lib/delivery/instant-publish.d.ts +9 -13
- package/dist/lib/delivery/instant-publish.js +0 -7
- package/dist/lib/delivery/instant-publish.js.map +1 -1
- package/dist/lib/diff-view-preference.js +4 -4
- package/dist/lib/diff-view-preference.js.map +1 -1
- package/dist/lib/i18n/messages.en.js +7 -5
- package/dist/lib/i18n/messages.en.js.map +1 -1
- package/dist/lib/local-preference.d.ts +37 -0
- package/dist/lib/local-preference.js +36 -0
- package/dist/lib/local-preference.js.map +1 -1
- package/dist/lib/persistence/convex-adapter.d.ts +3 -1
- package/dist/lib/persistence/convex-adapter.js +20 -0
- package/dist/lib/persistence/convex-adapter.js.map +1 -1
- package/dist/lib/rail.js +7 -6
- package/dist/lib/rail.js.map +1 -1
- package/dist/lib/required-context.d.ts +24 -0
- package/dist/lib/required-context.js +28 -0
- package/dist/lib/required-context.js.map +1 -0
- package/dist/lib/theme.js +11 -4
- package/dist/lib/theme.js.map +1 -1
- package/dist/ui/index.d.ts +1 -0
- package/dist/ui/index.js +1 -0
- package/dist/ui/index.js.map +1 -1
- package/dist/ui/segmented-control.d.ts +46 -0
- package/dist/ui/segmented-control.js +13 -0
- package/dist/ui/segmented-control.js.map +1 -0
- package/package.json +2 -2
|
@@ -1,32 +1,24 @@
|
|
|
1
|
-
import type { CmsDocument, CollectionDef, FieldDef
|
|
1
|
+
import type { CmsDocument, CollectionDef, FieldDef } from '@elytracms/core/cms-core';
|
|
2
2
|
import type { ReactNode } from 'react';
|
|
3
|
-
/** 0.0.39 — routing context the Details pane needs to compose the document URL row. */
|
|
4
|
-
export interface DetailsUrlContext {
|
|
5
|
-
projectId: string;
|
|
6
|
-
routes: readonly RouteRecord[];
|
|
7
|
-
localeConfig: LocaleConfig;
|
|
8
|
-
locale: string;
|
|
9
|
-
}
|
|
10
3
|
/**
|
|
11
|
-
* EC-284 + EC-285
|
|
12
|
-
*
|
|
4
|
+
* EC-284 + EC-285 — document-level metadata pane (the right inspector). Up to two
|
|
5
|
+
* optional regions:
|
|
13
6
|
*
|
|
14
|
-
* 1. **
|
|
15
|
-
* split" moves the composed URL out of the cramped slug field and up to the
|
|
16
|
-
* document, where a URL is a first-class property. Ancestor-DOCUMENT segments
|
|
17
|
-
* link to their editors; static route segments are plain text.
|
|
18
|
-
* 2. **Slot** (EC-284): a stable, named handle the site uses to find this document
|
|
7
|
+
* 1. **Slot** (EC-284): a stable, named handle the site uses to find this document
|
|
19
8
|
* (the main menu, the footer menu…) instead of which exact entry it is — shown
|
|
20
9
|
* only when the collection declares alias slots.
|
|
21
|
-
*
|
|
10
|
+
* 2. **Sidebar fields** (EC-285): the document's `sidebar`-group fields (slug,
|
|
22
11
|
* author, publish date…) — document-level properties an editor sets *about* the
|
|
23
12
|
* document rather than as body content. Rendered via the detail editor's own
|
|
24
13
|
* field-row renderer, so they behave identically to a content field.
|
|
25
14
|
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
15
|
+
* Both are identity/metadata, not content, so they live here — off the content tabs.
|
|
16
|
+
* The composed URL is NOT here: 0.0.42 (round 3) moved it up to the detail-editor
|
|
17
|
+
* HEADER, under the record title, where a URL reads as a first-class property (a lone
|
|
18
|
+
* URL row on an otherwise-empty pane looked odd). The empty state shows only when
|
|
19
|
+
* there is nothing at all to display.
|
|
28
20
|
*/
|
|
29
|
-
export declare function DetailsPane({ collection, doc, siblings, readOnly, fields, renderField, onAliasChange,
|
|
21
|
+
export declare function DetailsPane({ collection, doc, siblings, readOnly, fields, renderField, onAliasChange, }: {
|
|
30
22
|
collection: CollectionDef;
|
|
31
23
|
doc: CmsDocument;
|
|
32
24
|
/** The other documents in this collection (to show which slots are taken). */
|
|
@@ -37,6 +29,4 @@ export declare function DetailsPane({ collection, doc, siblings, readOnly, field
|
|
|
37
29
|
/** Render one field row — shared with the content groups so behaviour matches. */
|
|
38
30
|
renderField: (field: FieldDef) => ReactNode;
|
|
39
31
|
onAliasChange: (alias: string | undefined) => void;
|
|
40
|
-
/** Routing context for the composed URL row (0.0.39); omitted for non-routable rails. */
|
|
41
|
-
urlContext?: DetailsUrlContext;
|
|
42
32
|
}): import("react").JSX.Element;
|
|
@@ -1,50 +1,33 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { declaredAliases
|
|
3
|
-
import {
|
|
4
|
-
import { Row, Select, Separator, Stack, Text } from '@elytracms/studio/ui';
|
|
5
|
-
import { cn } from '@elytracms/studio/lib/utils';
|
|
2
|
+
import { declaredAliases } from '@elytracms/core/cms-core';
|
|
3
|
+
import { Select, Separator, Stack, Text } from '@elytracms/studio/ui';
|
|
6
4
|
import { translate } from '@elytracms/studio/lib/i18n';
|
|
7
5
|
/**
|
|
8
|
-
* EC-284 + EC-285
|
|
9
|
-
*
|
|
6
|
+
* EC-284 + EC-285 — document-level metadata pane (the right inspector). Up to two
|
|
7
|
+
* optional regions:
|
|
10
8
|
*
|
|
11
|
-
* 1. **
|
|
12
|
-
* split" moves the composed URL out of the cramped slug field and up to the
|
|
13
|
-
* document, where a URL is a first-class property. Ancestor-DOCUMENT segments
|
|
14
|
-
* link to their editors; static route segments are plain text.
|
|
15
|
-
* 2. **Slot** (EC-284): a stable, named handle the site uses to find this document
|
|
9
|
+
* 1. **Slot** (EC-284): a stable, named handle the site uses to find this document
|
|
16
10
|
* (the main menu, the footer menu…) instead of which exact entry it is — shown
|
|
17
11
|
* only when the collection declares alias slots.
|
|
18
|
-
*
|
|
12
|
+
* 2. **Sidebar fields** (EC-285): the document's `sidebar`-group fields (slug,
|
|
19
13
|
* author, publish date…) — document-level properties an editor sets *about* the
|
|
20
14
|
* document rather than as body content. Rendered via the detail editor's own
|
|
21
15
|
* field-row renderer, so they behave identically to a content field.
|
|
22
16
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
17
|
+
* Both are identity/metadata, not content, so they live here — off the content tabs.
|
|
18
|
+
* The composed URL is NOT here: 0.0.42 (round 3) moved it up to the detail-editor
|
|
19
|
+
* HEADER, under the record title, where a URL reads as a first-class property (a lone
|
|
20
|
+
* URL row on an otherwise-empty pane looked odd). The empty state shows only when
|
|
21
|
+
* there is nothing at all to display.
|
|
25
22
|
*/
|
|
26
|
-
export function DetailsPane({ collection, doc, siblings, readOnly, fields, renderField, onAliasChange,
|
|
23
|
+
export function DetailsPane({ collection, doc, siblings, readOnly, fields, renderField, onAliasChange, }) {
|
|
27
24
|
const slots = declaredAliases(collection);
|
|
28
25
|
const hasSlots = slots.length > 0;
|
|
29
26
|
const hasFields = fields.length > 0;
|
|
30
|
-
|
|
31
|
-
return (_jsxs(Stack, { gap: "4", className: "p-3", children: [routable && urlContext ? (_jsx(DocumentUrlRow, { collection: collection, doc: doc, siblings: siblings, context: urlContext })) : null, routable && (hasSlots || hasFields) ? _jsx(Separator, {}) : null, hasSlots ? (_jsxs(Stack, { gap: "1", children: [_jsx(Text, { size: "xs", weight: "semibold", children: translate('cms.details.slot') }), _jsxs(Select, { value: doc.alias ?? '', disabled: readOnly, "aria-label": translate('cms.details.slot'), "data-testid": "alias-select", onChange: (e) => onAliasChange(e.target.value === '' ? undefined : e.target.value), children: [_jsx("option", { value: "", children: translate('cms.details.noSlot') }), slots.map((slot) => {
|
|
27
|
+
return (_jsxs(Stack, { gap: "4", className: "p-3", children: [hasSlots ? (_jsxs(Stack, { gap: "1", children: [_jsx(Text, { size: "xs", weight: "semibold", children: translate('cms.details.slot') }), _jsxs(Select, { value: doc.alias ?? '', disabled: readOnly, "aria-label": translate('cms.details.slot'), "data-testid": "alias-select", onChange: (e) => onAliasChange(e.target.value === '' ? undefined : e.target.value), children: [_jsx("option", { value: "", children: translate('cms.details.noSlot') }), slots.map((slot) => {
|
|
32
28
|
const takenByOther = siblings.some((s) => s.id !== doc.id && s.alias === slot);
|
|
33
29
|
const blocked = takenByOther && doc.alias !== slot;
|
|
34
30
|
return (_jsxs("option", { value: slot, disabled: blocked, children: [slot, blocked ? translate('cms.details.slotInUse') : ''] }, slot));
|
|
35
|
-
})] }), _jsx(Text, { size: "xs", tone: "muted", children: translate('cms.details.slotHint') })] })) : null, hasSlots && hasFields ? _jsx(Separator, {}) : null, hasFields ? _jsx(Stack, { gap: "4", children: fields.map(renderField) }) : null, !
|
|
36
|
-
}
|
|
37
|
-
/**
|
|
38
|
-
* The document-level URL row (0.0.39 — EC-315 "hybrid split"). The full composed path,
|
|
39
|
-
* classified segment by segment: an ancestor-DOCUMENT segment is a link to that
|
|
40
|
-
* document's editor (autosave keeps the current edit safe on navigate); a static
|
|
41
|
-
* route segment (mount prefix, or `blog` in `/blog/:slug`) is plain text; this
|
|
42
|
-
* document's own slug segment is highlighted as the current page. Draft-derived (it
|
|
43
|
-
* updates as you type) — the header's published-gated "View live" stays the sole
|
|
44
|
-
* open-live action, so the draft/live distinction is preserved here too.
|
|
45
|
-
*/
|
|
46
|
-
function DocumentUrlRow({ collection, doc, siblings, context, }) {
|
|
47
|
-
const segments = resolveUrlSegments({ routes: [...context.routes], localeConfig: context.localeConfig }, collection, doc, { siblings, locale: context.locale });
|
|
48
|
-
return (_jsxs(Stack, { gap: "1", "data-testid": "details-url", children: [_jsx(Text, { size: "xs", weight: "semibold", children: translate('cms.details.url') }), _jsx(Row, { gap: "none", align: "center", wrap: true, className: "font-mono text-xs text-muted-foreground", children: segments.length === 0 ? (_jsx("span", { "aria-hidden": true, children: "/" })) : (segments.map((seg, i) => (_jsxs("span", { className: "inline-flex items-center", children: [_jsx("span", { "aria-hidden": true, className: "text-muted-foreground/50", children: "/" }), seg.kind === 'ancestor' && seg.documentId ? (_jsx(Link, { to: STUDIO_PROJECT_ROUTE, params: { projectId: context.projectId, _splat: studioPath.cmsDocument(collection.id, seg.documentId) }, title: translate('cms.relation.openTitle', { title: seg.text }), "data-testid": "details-url-ancestor", className: "rounded px-0.5 text-primary hover:bg-primary/10 hover:underline", children: seg.text })) : (_jsx("span", { className: cn('px-0.5', seg.kind === 'current' && 'font-medium text-foreground'), title: seg.kind === 'current' ? translate('cms.details.urlCurrent') : undefined, children: seg.text }))] }, `${seg.text}-${i}`)))) })] }));
|
|
31
|
+
})] }), _jsx(Text, { size: "xs", tone: "muted", children: translate('cms.details.slotHint') })] })) : null, hasSlots && hasFields ? _jsx(Separator, {}) : null, hasFields ? _jsx(Stack, { gap: "4", children: fields.map(renderField) }) : null, !hasSlots && !hasFields ? (_jsx(Text, { size: "xs", tone: "muted", children: translate('cms.details.empty') })) : null] }));
|
|
49
32
|
}
|
|
50
33
|
//# sourceMappingURL=details-pane.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"details-pane.js","sourceRoot":"","sources":["../../../src/components/cms/details-pane.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAA;AAS9E,OAAO,EAAE,IAAI,EAAE,oBAAoB,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAA;AACjF,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAA;AAC1E,OAAO,EAAE,EAAE,EAAE,MAAM,6BAA6B,CAAA;AAChD,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAA;AAUtD;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,WAAW,CAAC,EAC1B,UAAU,EACV,GAAG,EACH,QAAQ,EACR,QAAQ,EACR,MAAM,EACN,WAAW,EACX,aAAa,EACb,UAAU,GAcX;IACC,MAAM,KAAK,GAAG,eAAe,CAAC,UAAU,CAAC,CAAA;IACzC,MAAM,QAAQ,GAAG,KAAK,CAAC,MAAM,GAAG,CAAC,CAAA;IACjC,MAAM,SAAS,GAAG,MAAM,CAAC,MAAM,GAAG,CAAC,CAAA;IACnC,MAAM,QAAQ,GACZ,UAAU,KAAK,SAAS,IAAI,UAAU,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,EAAE,UAAU,KAAK,UAAU,CAAC,EAAE,CAAC,CAAA;IAErG,OAAO,CACL,MAAC,KAAK,IAAC,GAAG,EAAC,GAAG,EAAC,SAAS,EAAC,KAAK,aAC3B,QAAQ,IAAI,UAAU,CAAC,CAAC,CAAC,CACxB,KAAC,cAAc,IAAC,UAAU,EAAE,UAAU,EAAE,GAAG,EAAE,GAAG,EAAE,QAAQ,EAAE,QAAQ,EAAE,OAAO,EAAE,UAAU,GAAI,CAC9F,CAAC,CAAC,CAAC,IAAI,EAEP,QAAQ,IAAI,CAAC,QAAQ,IAAI,SAAS,CAAC,CAAC,CAAC,CAAC,KAAC,SAAS,KAAG,CAAC,CAAC,CAAC,IAAI,EAE1D,QAAQ,CAAC,CAAC,CAAC,CACV,MAAC,KAAK,IAAC,GAAG,EAAC,GAAG,aACZ,KAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,MAAM,EAAC,UAAU,YAC9B,SAAS,CAAC,kBAAkB,CAAC,GACzB,EACP,MAAC,MAAM,IACL,KAAK,EAAE,GAAG,CAAC,KAAK,IAAI,EAAE,EACtB,QAAQ,EAAE,QAAQ,gBACN,SAAS,CAAC,kBAAkB,CAAC,iBAC7B,cAAc,EAC1B,QAAQ,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,aAAa,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,KAAK,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,aAElF,iBAAQ,KAAK,EAAC,EAAE,YAAE,SAAS,CAAC,oBAAoB,CAAC,GAAU,EAC1D,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE;gCAClB,MAAM,YAAY,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,GAAG,CAAC,EAAE,IAAI,CAAC,CAAC,KAAK,KAAK,IAAI,CAAC,CAAA;gCAC9E,MAAM,OAAO,GAAG,YAAY,IAAI,GAAG,CAAC,KAAK,KAAK,IAAI,CAAA;gCAClD,OAAO,CACL,kBAAmB,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,aAC9C,IAAI,EACJ,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,uBAAuB,CAAC,CAAC,CAAC,CAAC,EAAE,KAFvC,IAAI,CAGR,CACV,CAAA;4BACH,CAAC,CAAC,IACK,EACT,KAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,IAAI,EAAC,OAAO,YACzB,SAAS,CAAC,sBAAsB,CAAC,GAC7B,IACD,CACT,CAAC,CAAC,CAAC,IAAI,EAEP,QAAQ,IAAI,SAAS,CAAC,CAAC,CAAC,KAAC,SAAS,KAAG,CAAC,CAAC,CAAC,IAAI,EAE5C,SAAS,CAAC,CAAC,CAAC,KAAC,KAAK,IAAC,GAAG,EAAC,GAAG,YAAE,MAAM,CAAC,GAAG,CAAC,WAAW,CAAC,GAAS,CAAC,CAAC,CAAC,IAAI,EAEnE,CAAC,QAAQ,IAAI,CAAC,QAAQ,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CACtC,KAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,IAAI,EAAC,OAAO,YACzB,SAAS,CAAC,mBAAmB,CAAC,GAC1B,CACR,CAAC,CAAC,CAAC,IAAI,IACF,CACT,CAAA;AACH,CAAC;AAED;;;;;;;;GAQG;AACH,SAAS,cAAc,CAAC,EACtB,UAAU,EACV,GAAG,EACH,QAAQ,EACR,OAAO,GAMR;IACC,MAAM,QAAQ,GAAG,kBAAkB,CACjC,EAAE,MAAM,EAAE,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,EAAE,YAAY,EAAE,OAAO,CAAC,YAAY,EAAE,EACnE,UAAU,EACV,GAAG,EACH,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CACrC,CAAA;IACD,OAAO,CACL,MAAC,KAAK,IAAC,GAAG,EAAC,GAAG,iBAAa,aAAa,aACtC,KAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,MAAM,EAAC,UAAU,YAC9B,SAAS,CAAC,iBAAiB,CAAC,GACxB,EACP,KAAC,GAAG,IAAC,GAAG,EAAC,MAAM,EAAC,KAAK,EAAC,QAAQ,EAAC,IAAI,QAAC,SAAS,EAAC,yCAAyC,YACpF,QAAQ,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,CACvB,oDAA0B,CAC3B,CAAC,CAAC,CAAC,CACF,QAAQ,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC,CACvB,gBAA+B,SAAS,EAAC,0BAA0B,aACjE,oCAAkB,SAAS,EAAC,0BAA0B,kBAE/C,EACN,GAAG,CAAC,IAAI,KAAK,UAAU,IAAI,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,CAC3C,KAAC,IAAI,IACH,EAAE,EAAE,oBAAoB,EACxB,MAAM,EAAE,EAAE,SAAS,EAAE,OAAO,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,CAAC,WAAW,CAAC,UAAU,CAAC,EAAE,EAAE,GAAG,CAAC,UAAU,CAAC,EAAE,EACvG,KAAK,EAAE,SAAS,CAAC,wBAAwB,EAAE,EAAE,KAAK,EAAE,GAAG,CAAC,IAAI,EAAE,CAAC,iBACnD,sBAAsB,EAClC,SAAS,EAAC,iEAAiE,YAE1E,GAAG,CAAC,IAAI,GACJ,CACR,CAAC,CAAC,CAAC,CACF,eACE,SAAS,EAAE,EAAE,CAAC,QAAQ,EAAE,GAAG,CAAC,IAAI,KAAK,SAAS,IAAI,6BAA6B,CAAC,EAChF,KAAK,EAAE,GAAG,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,wBAAwB,CAAC,CAAC,CAAC,CAAC,SAAS,YAE9E,GAAG,CAAC,IAAI,GACJ,CACR,KArBQ,GAAG,GAAG,CAAC,IAAI,IAAI,CAAC,EAAE,CAsBtB,CACR,CAAC,CACH,GACG,IACA,CACT,CAAA;AACH,CAAC","sourcesContent":["import { declaredAliases, resolveUrlSegments } from '@elytracms/core/cms-core'\nimport type {\n CmsDocument,\n CollectionDef,\n FieldDef,\n LocaleConfig,\n RouteRecord,\n} from '@elytracms/core/cms-core'\nimport type { ReactNode } from 'react'\nimport { Link, STUDIO_PROJECT_ROUTE, studioPath } from '@elytracms/studio/router'\nimport { Row, Select, Separator, Stack, Text } from '@elytracms/studio/ui'\nimport { cn } from '@elytracms/studio/lib/utils'\nimport { translate } from '@elytracms/studio/lib/i18n'\n\n/** 0.0.39 — routing context the Details pane needs to compose the document URL row. */\nexport interface DetailsUrlContext {\n projectId: string\n routes: readonly RouteRecord[]\n localeConfig: LocaleConfig\n locale: string\n}\n\n/**\n * EC-284 + EC-285 (+ 0.0.39 URL row) — document-level metadata pane (the right\n * inspector). Up to three optional regions:\n *\n * 1. **URL** (0.0.39): the full composed path as its own row — the EC-315 \"hybrid\n * split\" moves the composed URL out of the cramped slug field and up to the\n * document, where a URL is a first-class property. Ancestor-DOCUMENT segments\n * link to their editors; static route segments are plain text.\n * 2. **Slot** (EC-284): a stable, named handle the site uses to find this document\n * (the main menu, the footer menu…) instead of which exact entry it is — shown\n * only when the collection declares alias slots.\n * 3. **Sidebar fields** (EC-285): the document's `sidebar`-group fields (slug,\n * author, publish date…) — document-level properties an editor sets *about* the\n * document rather than as body content. Rendered via the detail editor's own\n * field-row renderer, so they behave identically to a content field.\n *\n * All three are identity/metadata, not content, so they live here — off the content\n * tabs. The empty state shows only when there is nothing at all to display.\n */\nexport function DetailsPane({\n collection,\n doc,\n siblings,\n readOnly,\n fields,\n renderField,\n onAliasChange,\n urlContext,\n}: {\n collection: CollectionDef\n doc: CmsDocument\n /** The other documents in this collection (to show which slots are taken). */\n siblings: readonly CmsDocument[]\n readOnly?: boolean\n /** The collection's `sidebar`-group fields, in order (EC-285). */\n fields: readonly FieldDef[]\n /** Render one field row — shared with the content groups so behaviour matches. */\n renderField: (field: FieldDef) => ReactNode\n onAliasChange: (alias: string | undefined) => void\n /** Routing context for the composed URL row (0.0.39); omitted for non-routable rails. */\n urlContext?: DetailsUrlContext\n}) {\n const slots = declaredAliases(collection)\n const hasSlots = slots.length > 0\n const hasFields = fields.length > 0\n const routable =\n urlContext !== undefined && urlContext.routes.some((r) => r.document?.collection === collection.id)\n\n return (\n <Stack gap=\"4\" className=\"p-3\">\n {routable && urlContext ? (\n <DocumentUrlRow collection={collection} doc={doc} siblings={siblings} context={urlContext} />\n ) : null}\n\n {routable && (hasSlots || hasFields) ? <Separator /> : null}\n\n {hasSlots ? (\n <Stack gap=\"1\">\n <Text size=\"xs\" weight=\"semibold\">\n {translate('cms.details.slot')}\n </Text>\n <Select\n value={doc.alias ?? ''}\n disabled={readOnly}\n aria-label={translate('cms.details.slot')}\n data-testid=\"alias-select\"\n onChange={(e) => onAliasChange(e.target.value === '' ? undefined : e.target.value)}\n >\n <option value=\"\">{translate('cms.details.noSlot')}</option>\n {slots.map((slot) => {\n const takenByOther = siblings.some((s) => s.id !== doc.id && s.alias === slot)\n const blocked = takenByOther && doc.alias !== slot\n return (\n <option key={slot} value={slot} disabled={blocked}>\n {slot}\n {blocked ? translate('cms.details.slotInUse') : ''}\n </option>\n )\n })}\n </Select>\n <Text size=\"xs\" tone=\"muted\">\n {translate('cms.details.slotHint')}\n </Text>\n </Stack>\n ) : null}\n\n {hasSlots && hasFields ? <Separator /> : null}\n\n {hasFields ? <Stack gap=\"4\">{fields.map(renderField)}</Stack> : null}\n\n {!routable && !hasSlots && !hasFields ? (\n <Text size=\"xs\" tone=\"muted\">\n {translate('cms.details.empty')}\n </Text>\n ) : null}\n </Stack>\n )\n}\n\n/**\n * The document-level URL row (0.0.39 — EC-315 \"hybrid split\"). The full composed path,\n * classified segment by segment: an ancestor-DOCUMENT segment is a link to that\n * document's editor (autosave keeps the current edit safe on navigate); a static\n * route segment (mount prefix, or `blog` in `/blog/:slug`) is plain text; this\n * document's own slug segment is highlighted as the current page. Draft-derived (it\n * updates as you type) — the header's published-gated \"View live\" stays the sole\n * open-live action, so the draft/live distinction is preserved here too.\n */\nfunction DocumentUrlRow({\n collection,\n doc,\n siblings,\n context,\n}: {\n collection: CollectionDef\n doc: CmsDocument\n siblings: readonly CmsDocument[]\n context: DetailsUrlContext\n}) {\n const segments = resolveUrlSegments(\n { routes: [...context.routes], localeConfig: context.localeConfig },\n collection,\n doc,\n { siblings, locale: context.locale },\n )\n return (\n <Stack gap=\"1\" data-testid=\"details-url\">\n <Text size=\"xs\" weight=\"semibold\">\n {translate('cms.details.url')}\n </Text>\n <Row gap=\"none\" align=\"center\" wrap className=\"font-mono text-xs text-muted-foreground\">\n {segments.length === 0 ? (\n <span aria-hidden>/</span>\n ) : (\n segments.map((seg, i) => (\n <span key={`${seg.text}-${i}`} className=\"inline-flex items-center\">\n <span aria-hidden className=\"text-muted-foreground/50\">\n /\n </span>\n {seg.kind === 'ancestor' && seg.documentId ? (\n <Link\n to={STUDIO_PROJECT_ROUTE}\n params={{ projectId: context.projectId, _splat: studioPath.cmsDocument(collection.id, seg.documentId) }}\n title={translate('cms.relation.openTitle', { title: seg.text })}\n data-testid=\"details-url-ancestor\"\n className=\"rounded px-0.5 text-primary hover:bg-primary/10 hover:underline\"\n >\n {seg.text}\n </Link>\n ) : (\n <span\n className={cn('px-0.5', seg.kind === 'current' && 'font-medium text-foreground')}\n title={seg.kind === 'current' ? translate('cms.details.urlCurrent') : undefined}\n >\n {seg.text}\n </span>\n )}\n </span>\n ))\n )}\n </Row>\n </Stack>\n )\n}\n"]}
|
|
1
|
+
{"version":3,"file":"details-pane.js","sourceRoot":"","sources":["../../../src/components/cms/details-pane.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAA;AAG1D,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAA;AACrE,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAA;AAEtD;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,WAAW,CAAC,EAC1B,UAAU,EACV,GAAG,EACH,QAAQ,EACR,QAAQ,EACR,MAAM,EACN,WAAW,EACX,aAAa,GAYd;IACC,MAAM,KAAK,GAAG,eAAe,CAAC,UAAU,CAAC,CAAA;IACzC,MAAM,QAAQ,GAAG,KAAK,CAAC,MAAM,GAAG,CAAC,CAAA;IACjC,MAAM,SAAS,GAAG,MAAM,CAAC,MAAM,GAAG,CAAC,CAAA;IAEnC,OAAO,CACL,MAAC,KAAK,IAAC,GAAG,EAAC,GAAG,EAAC,SAAS,EAAC,KAAK,aAC3B,QAAQ,CAAC,CAAC,CAAC,CACV,MAAC,KAAK,IAAC,GAAG,EAAC,GAAG,aACZ,KAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,MAAM,EAAC,UAAU,YAC9B,SAAS,CAAC,kBAAkB,CAAC,GACzB,EACP,MAAC,MAAM,IACL,KAAK,EAAE,GAAG,CAAC,KAAK,IAAI,EAAE,EACtB,QAAQ,EAAE,QAAQ,gBACN,SAAS,CAAC,kBAAkB,CAAC,iBAC7B,cAAc,EAC1B,QAAQ,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,aAAa,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,KAAK,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,aAElF,iBAAQ,KAAK,EAAC,EAAE,YAAE,SAAS,CAAC,oBAAoB,CAAC,GAAU,EAC1D,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE;gCAClB,MAAM,YAAY,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,GAAG,CAAC,EAAE,IAAI,CAAC,CAAC,KAAK,KAAK,IAAI,CAAC,CAAA;gCAC9E,MAAM,OAAO,GAAG,YAAY,IAAI,GAAG,CAAC,KAAK,KAAK,IAAI,CAAA;gCAClD,OAAO,CACL,kBAAmB,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,aAC9C,IAAI,EACJ,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,uBAAuB,CAAC,CAAC,CAAC,CAAC,EAAE,KAFvC,IAAI,CAGR,CACV,CAAA;4BACH,CAAC,CAAC,IACK,EACT,KAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,IAAI,EAAC,OAAO,YACzB,SAAS,CAAC,sBAAsB,CAAC,GAC7B,IACD,CACT,CAAC,CAAC,CAAC,IAAI,EAEP,QAAQ,IAAI,SAAS,CAAC,CAAC,CAAC,KAAC,SAAS,KAAG,CAAC,CAAC,CAAC,IAAI,EAE5C,SAAS,CAAC,CAAC,CAAC,KAAC,KAAK,IAAC,GAAG,EAAC,GAAG,YAAE,MAAM,CAAC,GAAG,CAAC,WAAW,CAAC,GAAS,CAAC,CAAC,CAAC,IAAI,EAEnE,CAAC,QAAQ,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CACzB,KAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,IAAI,EAAC,OAAO,YACzB,SAAS,CAAC,mBAAmB,CAAC,GAC1B,CACR,CAAC,CAAC,CAAC,IAAI,IACF,CACT,CAAA;AACH,CAAC","sourcesContent":["import { declaredAliases } from '@elytracms/core/cms-core'\nimport type { CmsDocument, CollectionDef, FieldDef } from '@elytracms/core/cms-core'\nimport type { ReactNode } from 'react'\nimport { Select, Separator, Stack, Text } from '@elytracms/studio/ui'\nimport { translate } from '@elytracms/studio/lib/i18n'\n\n/**\n * EC-284 + EC-285 — document-level metadata pane (the right inspector). Up to two\n * optional regions:\n *\n * 1. **Slot** (EC-284): a stable, named handle the site uses to find this document\n * (the main menu, the footer menu…) instead of which exact entry it is — shown\n * only when the collection declares alias slots.\n * 2. **Sidebar fields** (EC-285): the document's `sidebar`-group fields (slug,\n * author, publish date…) — document-level properties an editor sets *about* the\n * document rather than as body content. Rendered via the detail editor's own\n * field-row renderer, so they behave identically to a content field.\n *\n * Both are identity/metadata, not content, so they live here — off the content tabs.\n * The composed URL is NOT here: 0.0.42 (round 3) moved it up to the detail-editor\n * HEADER, under the record title, where a URL reads as a first-class property (a lone\n * URL row on an otherwise-empty pane looked odd). The empty state shows only when\n * there is nothing at all to display.\n */\nexport function DetailsPane({\n collection,\n doc,\n siblings,\n readOnly,\n fields,\n renderField,\n onAliasChange,\n}: {\n collection: CollectionDef\n doc: CmsDocument\n /** The other documents in this collection (to show which slots are taken). */\n siblings: readonly CmsDocument[]\n readOnly?: boolean\n /** The collection's `sidebar`-group fields, in order (EC-285). */\n fields: readonly FieldDef[]\n /** Render one field row — shared with the content groups so behaviour matches. */\n renderField: (field: FieldDef) => ReactNode\n onAliasChange: (alias: string | undefined) => void\n}) {\n const slots = declaredAliases(collection)\n const hasSlots = slots.length > 0\n const hasFields = fields.length > 0\n\n return (\n <Stack gap=\"4\" className=\"p-3\">\n {hasSlots ? (\n <Stack gap=\"1\">\n <Text size=\"xs\" weight=\"semibold\">\n {translate('cms.details.slot')}\n </Text>\n <Select\n value={doc.alias ?? ''}\n disabled={readOnly}\n aria-label={translate('cms.details.slot')}\n data-testid=\"alias-select\"\n onChange={(e) => onAliasChange(e.target.value === '' ? undefined : e.target.value)}\n >\n <option value=\"\">{translate('cms.details.noSlot')}</option>\n {slots.map((slot) => {\n const takenByOther = siblings.some((s) => s.id !== doc.id && s.alias === slot)\n const blocked = takenByOther && doc.alias !== slot\n return (\n <option key={slot} value={slot} disabled={blocked}>\n {slot}\n {blocked ? translate('cms.details.slotInUse') : ''}\n </option>\n )\n })}\n </Select>\n <Text size=\"xs\" tone=\"muted\">\n {translate('cms.details.slotHint')}\n </Text>\n </Stack>\n ) : null}\n\n {hasSlots && hasFields ? <Separator /> : null}\n\n {hasFields ? <Stack gap=\"4\">{fields.map(renderField)}</Stack> : null}\n\n {!hasSlots && !hasFields ? (\n <Text size=\"xs\" tone=\"muted\">\n {translate('cms.details.empty')}\n </Text>\n ) : null}\n </Stack>\n )\n}\n"]}
|
|
@@ -30,3 +30,21 @@ export declare function discardDraftStatus(input: {
|
|
|
30
30
|
collection: CollectionDef;
|
|
31
31
|
draft: CmsDocument;
|
|
32
32
|
}): DiscardDraftStatus;
|
|
33
|
+
/**
|
|
34
|
+
* EC-325 — the flush-before-restore ordering "Discard changes" must follow,
|
|
35
|
+
* mirroring the IDENTICAL `if (dirty) save()` step the Versions pane's
|
|
36
|
+
* (`versions-pane.tsx`'s `handleRestore`) and the Compare view's
|
|
37
|
+
* (`version-compare.tsx`'s `restore`) own restores already take before theirs.
|
|
38
|
+
* `documents.restore` (`packages/core/src/operations/documents.ts`) snapshots
|
|
39
|
+
* whatever is currently PERSISTED as its own "pre-restore" version — never the
|
|
40
|
+
* client's in-memory draft — so an edit still sitting in the autosave debounce
|
|
41
|
+
* window when Discard fires would otherwise never reach the server and be
|
|
42
|
+
* silently unrecoverable once the draft is reverted. The underlying
|
|
43
|
+
* save/restore commands are serialized by the shared operations queue
|
|
44
|
+
* (EC-225), so the un-awaited `save()` here still lands before `restore` reads
|
|
45
|
+
* the persisted document.
|
|
46
|
+
*/
|
|
47
|
+
export declare function flushDirtyDraft(input: {
|
|
48
|
+
dirty: boolean;
|
|
49
|
+
save: () => void;
|
|
50
|
+
}): void;
|
|
@@ -10,4 +10,22 @@ export function discardDraftStatus(input) {
|
|
|
10
10
|
? 'up-to-date'
|
|
11
11
|
: 'ready';
|
|
12
12
|
}
|
|
13
|
+
/**
|
|
14
|
+
* EC-325 — the flush-before-restore ordering "Discard changes" must follow,
|
|
15
|
+
* mirroring the IDENTICAL `if (dirty) save()` step the Versions pane's
|
|
16
|
+
* (`versions-pane.tsx`'s `handleRestore`) and the Compare view's
|
|
17
|
+
* (`version-compare.tsx`'s `restore`) own restores already take before theirs.
|
|
18
|
+
* `documents.restore` (`packages/core/src/operations/documents.ts`) snapshots
|
|
19
|
+
* whatever is currently PERSISTED as its own "pre-restore" version — never the
|
|
20
|
+
* client's in-memory draft — so an edit still sitting in the autosave debounce
|
|
21
|
+
* window when Discard fires would otherwise never reach the server and be
|
|
22
|
+
* silently unrecoverable once the draft is reverted. The underlying
|
|
23
|
+
* save/restore commands are serialized by the shared operations queue
|
|
24
|
+
* (EC-225), so the un-awaited `save()` here still lands before `restore` reads
|
|
25
|
+
* the persisted document.
|
|
26
|
+
*/
|
|
27
|
+
export function flushDirtyDraft(input) {
|
|
28
|
+
if (input.dirty)
|
|
29
|
+
input.save();
|
|
30
|
+
}
|
|
13
31
|
//# sourceMappingURL=discard-draft.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"discard-draft.js","sourceRoot":"","sources":["../../../src/components/cms/discard-draft.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,yBAAyB,CAAA;AAuBhE,MAAM,UAAU,kBAAkB,CAAC,KAWlC;IACC,IAAI,CAAC,KAAK,CAAC,UAAU,IAAI,CAAC,KAAK,CAAC,SAAS;QAAE,OAAO,QAAQ,CAAA;IAC1D,IAAI,KAAK,CAAC,YAAY;QAAE,OAAO,gBAAgB,CAAA;IAC/C,IAAI,KAAK,CAAC,iBAAiB,KAAK,SAAS;QAAE,OAAO,SAAS,CAAA;IAC3D,OAAO,sBAAsB,CAAC,KAAK,CAAC,UAAU,EAAE,KAAK,CAAC,iBAAiB,EAAE,KAAK,CAAC,KAAK,CAAC;QACnF,CAAC,CAAC,YAAY;QACd,CAAC,CAAC,OAAO,CAAA;AACb,CAAC","sourcesContent":["import { documentsSemanticEqual } from '@elytracms/core/content'\nimport type { CmsDocument, CollectionDef } from '@elytracms/core/cms-core'\n\n/**\n * EC-316 — the \"Discard changes\" header action's enablement, as one pure\n * decision. Discarding reverts the draft to exactly the published content (via\n * the EC-298 restore machinery), so it is offered iff there is a published\n * version to revert TO and the draft actually differs from it:\n *\n * - `hidden` — the rail can't discard at all (no publish/versions\n * capability) or the document isn't published: there is no\n * published content to revert to, so no menu item.\n * - `view-read-only`— viewing the Published snapshot (EC-226): the draft isn't\n * the active view, so the item is disabled with a hint.\n * - `loading` — the version history hasn't resolved yet; disabled.\n * - `up-to-date` — the draft SEMANTICALLY equals the published snapshot\n * (`documentsSemanticEqual`, the SAME comparator the\n * Versions pane's publish gate uses, EC-298 — so the two\n * surfaces never contradict); nothing to discard, disabled.\n * - `ready` — a published version exists and the draft differs: enabled.\n */\nexport type DiscardDraftStatus = 'hidden' | 'view-read-only' | 'loading' | 'up-to-date' | 'ready'\n\nexport function discardDraftStatus(input: {\n /** The rail can publish AND has version history (both required to discard). */\n canDiscard: boolean\n /** The document IS published (version pointer set). */\n published: boolean\n /** The editor is viewing the read-only Published snapshot (EC-226). */\n viewReadOnly: boolean\n /** The pinned published version's snapshot; `undefined` while loading / none pinned. */\n publishedSnapshot: CmsDocument | undefined\n collection: CollectionDef\n draft: CmsDocument\n}): DiscardDraftStatus {\n if (!input.canDiscard || !input.published) return 'hidden'\n if (input.viewReadOnly) return 'view-read-only'\n if (input.publishedSnapshot === undefined) return 'loading'\n return documentsSemanticEqual(input.collection, input.publishedSnapshot, input.draft)\n ? 'up-to-date'\n : 'ready'\n}\n"]}
|
|
1
|
+
{"version":3,"file":"discard-draft.js","sourceRoot":"","sources":["../../../src/components/cms/discard-draft.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,yBAAyB,CAAA;AAuBhE,MAAM,UAAU,kBAAkB,CAAC,KAWlC;IACC,IAAI,CAAC,KAAK,CAAC,UAAU,IAAI,CAAC,KAAK,CAAC,SAAS;QAAE,OAAO,QAAQ,CAAA;IAC1D,IAAI,KAAK,CAAC,YAAY;QAAE,OAAO,gBAAgB,CAAA;IAC/C,IAAI,KAAK,CAAC,iBAAiB,KAAK,SAAS;QAAE,OAAO,SAAS,CAAA;IAC3D,OAAO,sBAAsB,CAAC,KAAK,CAAC,UAAU,EAAE,KAAK,CAAC,iBAAiB,EAAE,KAAK,CAAC,KAAK,CAAC;QACnF,CAAC,CAAC,YAAY;QACd,CAAC,CAAC,OAAO,CAAA;AACb,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,eAAe,CAAC,KAA2C;IACzE,IAAI,KAAK,CAAC,KAAK;QAAE,KAAK,CAAC,IAAI,EAAE,CAAA;AAC/B,CAAC","sourcesContent":["import { documentsSemanticEqual } from '@elytracms/core/content'\nimport type { CmsDocument, CollectionDef } from '@elytracms/core/cms-core'\n\n/**\n * EC-316 — the \"Discard changes\" header action's enablement, as one pure\n * decision. Discarding reverts the draft to exactly the published content (via\n * the EC-298 restore machinery), so it is offered iff there is a published\n * version to revert TO and the draft actually differs from it:\n *\n * - `hidden` — the rail can't discard at all (no publish/versions\n * capability) or the document isn't published: there is no\n * published content to revert to, so no menu item.\n * - `view-read-only`— viewing the Published snapshot (EC-226): the draft isn't\n * the active view, so the item is disabled with a hint.\n * - `loading` — the version history hasn't resolved yet; disabled.\n * - `up-to-date` — the draft SEMANTICALLY equals the published snapshot\n * (`documentsSemanticEqual`, the SAME comparator the\n * Versions pane's publish gate uses, EC-298 — so the two\n * surfaces never contradict); nothing to discard, disabled.\n * - `ready` — a published version exists and the draft differs: enabled.\n */\nexport type DiscardDraftStatus = 'hidden' | 'view-read-only' | 'loading' | 'up-to-date' | 'ready'\n\nexport function discardDraftStatus(input: {\n /** The rail can publish AND has version history (both required to discard). */\n canDiscard: boolean\n /** The document IS published (version pointer set). */\n published: boolean\n /** The editor is viewing the read-only Published snapshot (EC-226). */\n viewReadOnly: boolean\n /** The pinned published version's snapshot; `undefined` while loading / none pinned. */\n publishedSnapshot: CmsDocument | undefined\n collection: CollectionDef\n draft: CmsDocument\n}): DiscardDraftStatus {\n if (!input.canDiscard || !input.published) return 'hidden'\n if (input.viewReadOnly) return 'view-read-only'\n if (input.publishedSnapshot === undefined) return 'loading'\n return documentsSemanticEqual(input.collection, input.publishedSnapshot, input.draft)\n ? 'up-to-date'\n : 'ready'\n}\n\n/**\n * EC-325 — the flush-before-restore ordering \"Discard changes\" must follow,\n * mirroring the IDENTICAL `if (dirty) save()` step the Versions pane's\n * (`versions-pane.tsx`'s `handleRestore`) and the Compare view's\n * (`version-compare.tsx`'s `restore`) own restores already take before theirs.\n * `documents.restore` (`packages/core/src/operations/documents.ts`) snapshots\n * whatever is currently PERSISTED as its own \"pre-restore\" version — never the\n * client's in-memory draft — so an edit still sitting in the autosave debounce\n * window when Discard fires would otherwise never reach the server and be\n * silently unrecoverable once the draft is reverted. The underlying\n * save/restore commands are serialized by the shared operations queue\n * (EC-225), so the un-awaited `save()` here still lands before `restore` reads\n * the persisted document.\n */\nexport function flushDirtyDraft(input: { dirty: boolean; save: () => void }): void {\n if (input.dirty) input.save()\n}\n"]}
|
|
@@ -60,11 +60,12 @@ export interface DocumentEditorContextValue {
|
|
|
60
60
|
/** Leave compare mode, back to editing. */
|
|
61
61
|
exitCompare: () => void;
|
|
62
62
|
}
|
|
63
|
+
declare const useDocumentEditor: () => DocumentEditorContextValue;
|
|
63
64
|
export declare function DocumentEditorProvider({ value, children, }: {
|
|
64
65
|
value: DocumentEditorContextValue;
|
|
65
66
|
children: ReactNode;
|
|
66
67
|
}): import("react").JSX.Element;
|
|
67
|
-
export
|
|
68
|
+
export { useDocumentEditor };
|
|
68
69
|
/**
|
|
69
70
|
* The open document's editing context, or null when there is none (e.g. a
|
|
70
71
|
* relation picker opened from a table cell rather than the detail editor). Lets a
|
|
@@ -1,16 +1,11 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
3
|
-
|
|
2
|
+
import { useContext } from 'react';
|
|
3
|
+
import { createRequiredContext } from '@elytracms/studio/lib/required-context';
|
|
4
|
+
const { Context: DocumentEditorContext, useRequired: useDocumentEditor } = createRequiredContext('DocumentEditor');
|
|
4
5
|
export function DocumentEditorProvider({ value, children, }) {
|
|
5
6
|
return _jsx(DocumentEditorContext.Provider, { value: value, children: children });
|
|
6
7
|
}
|
|
7
|
-
export
|
|
8
|
-
const value = useContext(DocumentEditorContext);
|
|
9
|
-
if (!value) {
|
|
10
|
-
throw new Error('useDocumentEditor must be used within a <DocumentEditorProvider>.');
|
|
11
|
-
}
|
|
12
|
-
return value;
|
|
13
|
-
}
|
|
8
|
+
export { useDocumentEditor };
|
|
14
9
|
/**
|
|
15
10
|
* The open document's editing context, or null when there is none (e.g. a
|
|
16
11
|
* relation picker opened from a table cell rather than the detail editor). Lets a
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"document-editor-context.js","sourceRoot":"","sources":["../../../src/components/cms/document-editor-context.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"document-editor-context.js","sourceRoot":"","sources":["../../../src/components/cms/document-editor-context.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,OAAO,CAAA;AAGlC,OAAO,EAAE,qBAAqB,EAAE,MAAM,wCAAwC,CAAA;AAgE9E,MAAM,EAAE,OAAO,EAAE,qBAAqB,EAAE,WAAW,EAAE,iBAAiB,EAAE,GACtE,qBAAqB,CAA6B,gBAAgB,CAAC,CAAA;AAErE,MAAM,UAAU,sBAAsB,CAAC,EACrC,KAAK,EACL,QAAQ,GAIT;IACC,OAAO,KAAC,qBAAqB,CAAC,QAAQ,IAAC,KAAK,EAAE,KAAK,YAAG,QAAQ,GAAkC,CAAA;AAClG,CAAC;AAED,OAAO,EAAE,iBAAiB,EAAE,CAAA;AAE5B;;;;;GAKG;AACH,MAAM,UAAU,yBAAyB;IACvC,OAAO,UAAU,CAAC,qBAAqB,CAAC,CAAA;AAC1C,CAAC","sourcesContent":["import { useContext } from 'react'\nimport type { ReactNode } from 'react'\nimport type { CmsDocument, CollectionDef, DocumentRef } from '@elytracms/core/cms-core'\nimport { createRequiredContext } from '@elytracms/studio/lib/required-context'\n\n/**\n * EC-224 — the open document's editing context, provided by the detail editor\n * and consumed by document-level inspector panes (the `Versions` pane).\n *\n * It exists because the Versions pane is rendered in two places — directly in\n * the form view's sidebar, and inside the canvas view's own sidebar (deep in the\n * scoped `BuilderProvider` subtree). A context lets the pane reach the open\n * document's ref + live draft + actions from either mount point, without\n * threading props through the canvas field editor.\n */\n/**\n * EC-298 version-compare selection: which two snapshots the compare view diffs.\n * `base` is a recorded version number (the older side); `target` is either\n * another recorded version or the live working draft.\n */\nexport interface CompareState {\n base: number\n target: number | 'draft'\n}\n\nexport interface DocumentEditorContextValue {\n /** The project being edited (undefined in fixture/playground sessions). */\n projectId: string | undefined\n collection: CollectionDef\n docRef: DocumentRef\n /** The editor's live (possibly unsaved) draft buffer. */\n draft: CmsDocument\n /** Does the draft buffer diverge from the saved document? */\n dirty: boolean\n /** Does the draft have error-severity validation issues (blocks publish)? */\n hasErrors: boolean\n /**\n * Is the editor in the read-only Published view (EC-226)? Document-level panes\n * must disable their mutating actions (publish/restore) while it is true.\n */\n viewReadOnly: boolean\n /** Persist the current draft buffer (the editor's Save). */\n save: () => void\n /**\n * Replace the editor's draft buffer — used after a version restore swaps the\n * working draft for an older snapshot, so the open form/canvas reflect it.\n */\n replaceDraft: (doc: CmsDocument) => void\n /**\n * EC-298 — the open version comparison, or null when none. The Compare tab is a\n * sticky content tab: this selection persists while the user tabs between the\n * diff and the real fields, and is cleared only by closing the tab (its ✕).\n */\n compare: CompareState | null\n /** Is the Compare tab the currently VISIBLE view? Drives the Versions pane's\n * base-picker mode — the pane navigates only while the diff is on screen. */\n compareActive: boolean\n /** Open (or refocus) the Compare tab with `base` vs the current draft. */\n startCompare: (base: number) => void\n /** Change which recorded version is the (older) base side. */\n setCompareBase: (base: number) => void\n /** Change what the base is compared against (a version, or the live draft). */\n setCompareTarget: (target: number | 'draft') => void\n /** Leave compare mode, back to editing. */\n exitCompare: () => void\n}\n\nconst { Context: DocumentEditorContext, useRequired: useDocumentEditor } =\n createRequiredContext<DocumentEditorContextValue>('DocumentEditor')\n\nexport function DocumentEditorProvider({\n value,\n children,\n}: {\n value: DocumentEditorContextValue\n children: ReactNode\n}) {\n return <DocumentEditorContext.Provider value={value}>{children}</DocumentEditorContext.Provider>\n}\n\nexport { useDocumentEditor }\n\n/**\n * The open document's editing context, or null when there is none (e.g. a\n * relation picker opened from a table cell rather than the detail editor). Lets a\n * control opt into doc-aware behavior — like the EC-218 parent cycle guard —\n * without requiring the provider.\n */\nexport function useOptionalDocumentEditor(): DocumentEditorContextValue | null {\n return useContext(DocumentEditorContext)\n}\n"]}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { CmsDocument, DocumentRef } from '@elytracms/core/cms-core';
|
|
2
|
+
/**
|
|
3
|
+
* By-ref document array helpers (EC-326) — a shared idiom over
|
|
4
|
+
* `@elytracms/core/cms-core`'s `documentRefEquals`, replacing ~12 hand-inlined
|
|
5
|
+
* `d.collection === x.collection && d.id === x.id` predicates across
|
|
6
|
+
* `cms-store.tsx`, `bulk-actions.ts`, and `cell-edit.ts`.
|
|
7
|
+
*
|
|
8
|
+
* Two real shapes exist over a document array and must stay distinct:
|
|
9
|
+
* - {@link replaceDoc}/{@link removeDoc} are pure, no-op-if-absent (an edit or
|
|
10
|
+
* delete of a document that's already gone from the session is a silent
|
|
11
|
+
* no-op, never an append).
|
|
12
|
+
* - {@link upsertDoc} is replace-or-append — `saveDocument` and
|
|
13
|
+
* `importDocuments` rely on the append branch to create new documents; a
|
|
14
|
+
* naive `replaceDoc` there would silently stop those flows from ever adding
|
|
15
|
+
* a document to the session.
|
|
16
|
+
*/
|
|
17
|
+
/** Does `doc` identify the same document as `ref` (collection + id, EC-017)? */
|
|
18
|
+
export declare function matchesRef(doc: DocumentRef, ref: DocumentRef): boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Replace the document matching `next`'s ref with `next`. A no-op (returns
|
|
21
|
+
* `docs` unchanged) if no document with that ref exists — pure map-replace,
|
|
22
|
+
* never appends.
|
|
23
|
+
*/
|
|
24
|
+
export declare function replaceDoc(docs: readonly CmsDocument[], next: CmsDocument): CmsDocument[];
|
|
25
|
+
/** Remove the document identified by `ref`, if present. A no-op otherwise. */
|
|
26
|
+
export declare function removeDoc(docs: readonly CmsDocument[], ref: DocumentRef): CmsDocument[];
|
|
27
|
+
/**
|
|
28
|
+
* Replace the document matching `next`'s ref with `next`, or append it when
|
|
29
|
+
* no match exists (upsert) — the shape `saveDocument` and the accepted-batch
|
|
30
|
+
* fold of `importDocuments` need, so a create/import flow always lands the
|
|
31
|
+
* document even on a session with no prior entry for that ref.
|
|
32
|
+
*/
|
|
33
|
+
export declare function upsertDoc(docs: readonly CmsDocument[], next: CmsDocument): CmsDocument[];
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { documentRefEquals } from '@elytracms/core/cms-core';
|
|
2
|
+
/**
|
|
3
|
+
* By-ref document array helpers (EC-326) — a shared idiom over
|
|
4
|
+
* `@elytracms/core/cms-core`'s `documentRefEquals`, replacing ~12 hand-inlined
|
|
5
|
+
* `d.collection === x.collection && d.id === x.id` predicates across
|
|
6
|
+
* `cms-store.tsx`, `bulk-actions.ts`, and `cell-edit.ts`.
|
|
7
|
+
*
|
|
8
|
+
* Two real shapes exist over a document array and must stay distinct:
|
|
9
|
+
* - {@link replaceDoc}/{@link removeDoc} are pure, no-op-if-absent (an edit or
|
|
10
|
+
* delete of a document that's already gone from the session is a silent
|
|
11
|
+
* no-op, never an append).
|
|
12
|
+
* - {@link upsertDoc} is replace-or-append — `saveDocument` and
|
|
13
|
+
* `importDocuments` rely on the append branch to create new documents; a
|
|
14
|
+
* naive `replaceDoc` there would silently stop those flows from ever adding
|
|
15
|
+
* a document to the session.
|
|
16
|
+
*/
|
|
17
|
+
/** Does `doc` identify the same document as `ref` (collection + id, EC-017)? */
|
|
18
|
+
export function matchesRef(doc, ref) {
|
|
19
|
+
return documentRefEquals(doc, ref);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Replace the document matching `next`'s ref with `next`. A no-op (returns
|
|
23
|
+
* `docs` unchanged) if no document with that ref exists — pure map-replace,
|
|
24
|
+
* never appends.
|
|
25
|
+
*/
|
|
26
|
+
export function replaceDoc(docs, next) {
|
|
27
|
+
return docs.map((d) => (matchesRef(d, next) ? next : d));
|
|
28
|
+
}
|
|
29
|
+
/** Remove the document identified by `ref`, if present. A no-op otherwise. */
|
|
30
|
+
export function removeDoc(docs, ref) {
|
|
31
|
+
return docs.filter((d) => !matchesRef(d, ref));
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Replace the document matching `next`'s ref with `next`, or append it when
|
|
35
|
+
* no match exists (upsert) — the shape `saveDocument` and the accepted-batch
|
|
36
|
+
* fold of `importDocuments` need, so a create/import flow always lands the
|
|
37
|
+
* document even on a session with no prior entry for that ref.
|
|
38
|
+
*/
|
|
39
|
+
export function upsertDoc(docs, next) {
|
|
40
|
+
const i = docs.findIndex((d) => matchesRef(d, next));
|
|
41
|
+
if (i < 0)
|
|
42
|
+
return [...docs, next];
|
|
43
|
+
const out = [...docs];
|
|
44
|
+
out[i] = next;
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=document-ref-utils.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"document-ref-utils.js","sourceRoot":"","sources":["../../../src/components/cms/document-ref-utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAA;AAG5D;;;;;;;;;;;;;;GAcG;AAEH,gFAAgF;AAChF,MAAM,UAAU,UAAU,CAAC,GAAgB,EAAE,GAAgB;IAC3D,OAAO,iBAAiB,CAAC,GAAG,EAAE,GAAG,CAAC,CAAA;AACpC,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,UAAU,CAAC,IAA4B,EAAE,IAAiB;IACxE,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,UAAU,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;AAC1D,CAAC;AAED,8EAA8E;AAC9E,MAAM,UAAU,SAAS,CAAC,IAA4B,EAAE,GAAgB;IACtE,OAAO,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,UAAU,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAA;AAChD,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,SAAS,CAAC,IAA4B,EAAE,IAAiB;IACvE,MAAM,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,CAAA;IACpD,IAAI,CAAC,GAAG,CAAC;QAAE,OAAO,CAAC,GAAG,IAAI,EAAE,IAAI,CAAC,CAAA;IACjC,MAAM,GAAG,GAAG,CAAC,GAAG,IAAI,CAAC,CAAA;IACrB,GAAG,CAAC,CAAC,CAAC,GAAG,IAAI,CAAA;IACb,OAAO,GAAG,CAAA;AACZ,CAAC","sourcesContent":["import { documentRefEquals } from '@elytracms/core/cms-core'\nimport type { CmsDocument, DocumentRef } from '@elytracms/core/cms-core'\n\n/**\n * By-ref document array helpers (EC-326) — a shared idiom over\n * `@elytracms/core/cms-core`'s `documentRefEquals`, replacing ~12 hand-inlined\n * `d.collection === x.collection && d.id === x.id` predicates across\n * `cms-store.tsx`, `bulk-actions.ts`, and `cell-edit.ts`.\n *\n * Two real shapes exist over a document array and must stay distinct:\n * - {@link replaceDoc}/{@link removeDoc} are pure, no-op-if-absent (an edit or\n * delete of a document that's already gone from the session is a silent\n * no-op, never an append).\n * - {@link upsertDoc} is replace-or-append — `saveDocument` and\n * `importDocuments` rely on the append branch to create new documents; a\n * naive `replaceDoc` there would silently stop those flows from ever adding\n * a document to the session.\n */\n\n/** Does `doc` identify the same document as `ref` (collection + id, EC-017)? */\nexport function matchesRef(doc: DocumentRef, ref: DocumentRef): boolean {\n return documentRefEquals(doc, ref)\n}\n\n/**\n * Replace the document matching `next`'s ref with `next`. A no-op (returns\n * `docs` unchanged) if no document with that ref exists — pure map-replace,\n * never appends.\n */\nexport function replaceDoc(docs: readonly CmsDocument[], next: CmsDocument): CmsDocument[] {\n return docs.map((d) => (matchesRef(d, next) ? next : d))\n}\n\n/** Remove the document identified by `ref`, if present. A no-op otherwise. */\nexport function removeDoc(docs: readonly CmsDocument[], ref: DocumentRef): CmsDocument[] {\n return docs.filter((d) => !matchesRef(d, ref))\n}\n\n/**\n * Replace the document matching `next`'s ref with `next`, or append it when\n * no match exists (upsert) — the shape `saveDocument` and the accepted-batch\n * fold of `importDocuments` need, so a create/import flow always lands the\n * document even on a session with no prior entry for that ref.\n */\nexport function upsertDoc(docs: readonly CmsDocument[], next: CmsDocument): CmsDocument[] {\n const i = docs.findIndex((d) => matchesRef(d, next))\n if (i < 0) return [...docs, next]\n const out = [...docs]\n out[i] = next\n return out\n}\n"]}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { CmsDocument, CollectionDef, LocaleConfig, RouteRecord, UrlSegment } from '@elytracms/core/cms-core';
|
|
2
|
+
/**
|
|
3
|
+
* One classified URL segment. An `ancestor` segment (with a `projectId` to build the
|
|
4
|
+
* link) navigates to that document's editor; a `static`/`current` segment — or an
|
|
5
|
+
* ancestor with no `projectId` (fixtures/no persistence) — is plain text, the current
|
|
6
|
+
* one highlighted. The single source of segment semantics for every surface.
|
|
7
|
+
*/
|
|
8
|
+
export declare function DocumentUrlSegment({ segment, projectId, collectionId, }: {
|
|
9
|
+
segment: UrlSegment;
|
|
10
|
+
projectId: string | undefined;
|
|
11
|
+
collectionId: string;
|
|
12
|
+
}): import("react").JSX.Element;
|
|
13
|
+
/**
|
|
14
|
+
* The detail-editor header's composed-URL row (0.0.42): a compact single line under
|
|
15
|
+
* the record title. The ancestor/static prefix ellipsizes gracefully on overflow (a
|
|
16
|
+
* real CSS ellipsis; the full path is the row tooltip) so the row NEVER wraps or
|
|
17
|
+
* pushes the header actions around; the leaf segment + open-live icon never shrink.
|
|
18
|
+
*
|
|
19
|
+
* `live` carries the published live-page affordance (header only), mirroring the
|
|
20
|
+
* retired "View live" button's gating exactly: `href` present → the leaf + external
|
|
21
|
+
* icon open the live page in a new tab; absent → the leaf is plain highlighted text
|
|
22
|
+
* and the icon is a muted, non-interactive hint carrying `title` (publish / set
|
|
23
|
+
* frontendUrl / loading). Omit `live` entirely on non-live surfaces.
|
|
24
|
+
*/
|
|
25
|
+
export declare function DocumentUrlPath({ projectId, collection, doc, siblings, routes, localeConfig, locale, live, className, }: {
|
|
26
|
+
projectId: string;
|
|
27
|
+
collection: CollectionDef;
|
|
28
|
+
doc: CmsDocument;
|
|
29
|
+
siblings: readonly CmsDocument[];
|
|
30
|
+
routes: readonly RouteRecord[];
|
|
31
|
+
localeConfig: LocaleConfig;
|
|
32
|
+
locale: string;
|
|
33
|
+
/** Published live-page affordance (header). Omitted → the leaf is plain text. */
|
|
34
|
+
live?: {
|
|
35
|
+
href?: string;
|
|
36
|
+
title: string;
|
|
37
|
+
};
|
|
38
|
+
className?: string;
|
|
39
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { ExternalLink } from 'lucide-react';
|
|
3
|
+
import { resolveUrlSegments } from '@elytracms/core/cms-core';
|
|
4
|
+
import { Link, STUDIO_PROJECT_ROUTE, studioPath } from '@elytracms/studio/router';
|
|
5
|
+
import { Row } from '@elytracms/studio/ui';
|
|
6
|
+
import { cn } from '@elytracms/studio/lib/utils';
|
|
7
|
+
import { translate } from '@elytracms/studio/lib/i18n';
|
|
8
|
+
/**
|
|
9
|
+
* 0.0.42 — the shared composed-URL segment surface (EC-315 "hybrid split", round 3).
|
|
10
|
+
*
|
|
11
|
+
* A document's URL is a first-class property, classified segment by segment:
|
|
12
|
+
*
|
|
13
|
+
* - `ancestor` — a hierarchy-parent DOCUMENT's slug: a link to that document's
|
|
14
|
+
* editor (autosave keeps the current edit safe on navigate);
|
|
15
|
+
* - `static` — a mount prefix or route-pattern literal (`blog` in `/blog/:slug`);
|
|
16
|
+
* - `current` — this document's own slug segment (highlighted).
|
|
17
|
+
*
|
|
18
|
+
* ONE renderer feeds every surface — the detail-editor header path row
|
|
19
|
+
* ({@link DocumentUrlPath}) and the slug field's compact parent prefix (which reuses
|
|
20
|
+
* {@link DocumentUrlSegment}) — so a segment reads and behaves identically wherever
|
|
21
|
+
* it shows. Draft-derived (updates as you type); the published-gated live link on the
|
|
22
|
+
* header row is the sole open-live action, preserving the draft/live distinction.
|
|
23
|
+
*/
|
|
24
|
+
/** The `/` separator between segments (dimmed so the segments read as the content). */
|
|
25
|
+
function Separator() {
|
|
26
|
+
return (_jsx("span", { "aria-hidden": true, className: "text-muted-foreground/50", children: "/" }));
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* One classified URL segment. An `ancestor` segment (with a `projectId` to build the
|
|
30
|
+
* link) navigates to that document's editor; a `static`/`current` segment — or an
|
|
31
|
+
* ancestor with no `projectId` (fixtures/no persistence) — is plain text, the current
|
|
32
|
+
* one highlighted. The single source of segment semantics for every surface.
|
|
33
|
+
*/
|
|
34
|
+
export function DocumentUrlSegment({ segment, projectId, collectionId, }) {
|
|
35
|
+
if (segment.kind === 'ancestor' && segment.documentId !== undefined && projectId !== undefined) {
|
|
36
|
+
return (_jsx(Link, { to: STUDIO_PROJECT_ROUTE, params: { projectId, _splat: studioPath.cmsDocument(collectionId, segment.documentId) }, title: translate('cms.relation.openTitle', { title: segment.text }), "data-testid": "doc-url-ancestor", className: "rounded px-0.5 text-primary hover:bg-primary/10 hover:underline", children: segment.text }));
|
|
37
|
+
}
|
|
38
|
+
return (_jsx("span", { className: cn('px-0.5', segment.kind === 'current' && 'font-medium text-foreground'), title: segment.kind === 'current' ? translate('cms.details.urlCurrent') : undefined, children: segment.text }));
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The detail-editor header's composed-URL row (0.0.42): a compact single line under
|
|
42
|
+
* the record title. The ancestor/static prefix ellipsizes gracefully on overflow (a
|
|
43
|
+
* real CSS ellipsis; the full path is the row tooltip) so the row NEVER wraps or
|
|
44
|
+
* pushes the header actions around; the leaf segment + open-live icon never shrink.
|
|
45
|
+
*
|
|
46
|
+
* `live` carries the published live-page affordance (header only), mirroring the
|
|
47
|
+
* retired "View live" button's gating exactly: `href` present → the leaf + external
|
|
48
|
+
* icon open the live page in a new tab; absent → the leaf is plain highlighted text
|
|
49
|
+
* and the icon is a muted, non-interactive hint carrying `title` (publish / set
|
|
50
|
+
* frontendUrl / loading). Omit `live` entirely on non-live surfaces.
|
|
51
|
+
*/
|
|
52
|
+
export function DocumentUrlPath({ projectId, collection, doc, siblings, routes, localeConfig, locale, live, className, }) {
|
|
53
|
+
const segments = resolveUrlSegments({ routes: [...routes], localeConfig }, collection, doc, { siblings, locale });
|
|
54
|
+
const fullPath = segments.length === 0 ? '/' : `/${segments.map((s) => s.text).join('/')}`;
|
|
55
|
+
// The leaf is this page; everything before it is the (truncatable) context prefix.
|
|
56
|
+
const prefix = segments.slice(0, -1);
|
|
57
|
+
const leaf = segments[segments.length - 1];
|
|
58
|
+
return (_jsxs(Row, { gap: "none", align: "center", className: cn('min-w-0 flex-nowrap font-mono text-xs text-muted-foreground', className), title: fullPath, "data-testid": "header-url", children: [_jsx("span", { className: "block min-w-0 shrink overflow-hidden text-ellipsis whitespace-nowrap", children: prefix.map((seg, i) => (_jsxs("span", { children: [_jsx(Separator, {}), _jsx(DocumentUrlSegment, { segment: seg, projectId: projectId, collectionId: collection.id })] }, `${seg.text}-${i}`))) }), _jsxs("span", { className: "inline-flex shrink-0 items-center", children: [_jsx(Separator, {}), live?.href ? (_jsxs("a", { href: live.href, target: "_blank", rel: "noreferrer", title: live.title, "data-testid": "header-live-link", className: "inline-flex items-center gap-0.5 rounded px-0.5 font-medium text-foreground hover:bg-primary/10 hover:text-primary hover:underline", children: [leaf?.text ?? '', _jsx(ExternalLink, { className: "size-3", "aria-hidden": true })] })) : (_jsxs("span", { className: "inline-flex items-center gap-0.5 px-0.5", children: [leaf !== undefined ? (_jsx("span", { className: "font-medium text-foreground", title: translate('cms.details.urlCurrent'), children: leaf.text })) : null, live ? (_jsx("span", { title: live.title, "aria-label": live.title, "data-testid": "header-live-link-disabled", className: "inline-flex text-muted-foreground/40", children: _jsx(ExternalLink, { className: "size-3", "aria-hidden": true }) })) : null] }))] })] }));
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=document-url-path.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"document-url-path.js","sourceRoot":"","sources":["../../../src/components/cms/document-url-path.tsx"],"names":[],"mappings":"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","sourcesContent":["import { ExternalLink } from 'lucide-react'\nimport { resolveUrlSegments } from '@elytracms/core/cms-core'\nimport type {\n CmsDocument,\n CollectionDef,\n LocaleConfig,\n RouteRecord,\n UrlSegment,\n} from '@elytracms/core/cms-core'\nimport { Link, STUDIO_PROJECT_ROUTE, studioPath } from '@elytracms/studio/router'\nimport { Row } from '@elytracms/studio/ui'\nimport { cn } from '@elytracms/studio/lib/utils'\nimport { translate } from '@elytracms/studio/lib/i18n'\n\n/**\n * 0.0.42 — the shared composed-URL segment surface (EC-315 \"hybrid split\", round 3).\n *\n * A document's URL is a first-class property, classified segment by segment:\n *\n * - `ancestor` — a hierarchy-parent DOCUMENT's slug: a link to that document's\n * editor (autosave keeps the current edit safe on navigate);\n * - `static` — a mount prefix or route-pattern literal (`blog` in `/blog/:slug`);\n * - `current` — this document's own slug segment (highlighted).\n *\n * ONE renderer feeds every surface — the detail-editor header path row\n * ({@link DocumentUrlPath}) and the slug field's compact parent prefix (which reuses\n * {@link DocumentUrlSegment}) — so a segment reads and behaves identically wherever\n * it shows. Draft-derived (updates as you type); the published-gated live link on the\n * header row is the sole open-live action, preserving the draft/live distinction.\n */\n\n/** The `/` separator between segments (dimmed so the segments read as the content). */\nfunction Separator() {\n return (\n <span aria-hidden className=\"text-muted-foreground/50\">\n /\n </span>\n )\n}\n\n/**\n * One classified URL segment. An `ancestor` segment (with a `projectId` to build the\n * link) navigates to that document's editor; a `static`/`current` segment — or an\n * ancestor with no `projectId` (fixtures/no persistence) — is plain text, the current\n * one highlighted. The single source of segment semantics for every surface.\n */\nexport function DocumentUrlSegment({\n segment,\n projectId,\n collectionId,\n}: {\n segment: UrlSegment\n projectId: string | undefined\n collectionId: string\n}) {\n if (segment.kind === 'ancestor' && segment.documentId !== undefined && projectId !== undefined) {\n return (\n <Link\n to={STUDIO_PROJECT_ROUTE}\n params={{ projectId, _splat: studioPath.cmsDocument(collectionId, segment.documentId) }}\n title={translate('cms.relation.openTitle', { title: segment.text })}\n data-testid=\"doc-url-ancestor\"\n className=\"rounded px-0.5 text-primary hover:bg-primary/10 hover:underline\"\n >\n {segment.text}\n </Link>\n )\n }\n return (\n <span\n className={cn('px-0.5', segment.kind === 'current' && 'font-medium text-foreground')}\n title={segment.kind === 'current' ? translate('cms.details.urlCurrent') : undefined}\n >\n {segment.text}\n </span>\n )\n}\n\n/**\n * The detail-editor header's composed-URL row (0.0.42): a compact single line under\n * the record title. The ancestor/static prefix ellipsizes gracefully on overflow (a\n * real CSS ellipsis; the full path is the row tooltip) so the row NEVER wraps or\n * pushes the header actions around; the leaf segment + open-live icon never shrink.\n *\n * `live` carries the published live-page affordance (header only), mirroring the\n * retired \"View live\" button's gating exactly: `href` present → the leaf + external\n * icon open the live page in a new tab; absent → the leaf is plain highlighted text\n * and the icon is a muted, non-interactive hint carrying `title` (publish / set\n * frontendUrl / loading). Omit `live` entirely on non-live surfaces.\n */\nexport function DocumentUrlPath({\n projectId,\n collection,\n doc,\n siblings,\n routes,\n localeConfig,\n locale,\n live,\n className,\n}: {\n projectId: string\n collection: CollectionDef\n doc: CmsDocument\n siblings: readonly CmsDocument[]\n routes: readonly RouteRecord[]\n localeConfig: LocaleConfig\n locale: string\n /** Published live-page affordance (header). Omitted → the leaf is plain text. */\n live?: { href?: string; title: string }\n className?: string\n}) {\n const segments = resolveUrlSegments(\n { routes: [...routes], localeConfig },\n collection,\n doc,\n { siblings, locale },\n )\n const fullPath = segments.length === 0 ? '/' : `/${segments.map((s) => s.text).join('/')}`\n // The leaf is this page; everything before it is the (truncatable) context prefix.\n const prefix = segments.slice(0, -1)\n const leaf = segments[segments.length - 1]\n\n return (\n <Row\n gap=\"none\"\n align=\"center\"\n className={cn('min-w-0 flex-nowrap font-mono text-xs text-muted-foreground', className)}\n title={fullPath}\n data-testid=\"header-url\"\n >\n {/* Prefix — locale/static/ancestor segments before the leaf. A block with inline\n children so `text-overflow: ellipsis` clips the ANCESTORS (not the leaf) when\n the path is too long; `min-w-0` lets it shrink inside the flex row. */}\n <span className=\"block min-w-0 shrink overflow-hidden text-ellipsis whitespace-nowrap\">\n {prefix.map((seg, i) => (\n <span key={`${seg.text}-${i}`}>\n <Separator />\n <DocumentUrlSegment segment={seg} projectId={projectId} collectionId={collection.id} />\n </span>\n ))}\n </span>\n {/* Leaf + live affordance — never shrinks, so the current page and the open-live\n icon stay visible even when the ancestors ellipsize. */}\n <span className=\"inline-flex shrink-0 items-center\">\n <Separator />\n {live?.href ? (\n <a\n href={live.href}\n target=\"_blank\"\n rel=\"noreferrer\"\n title={live.title}\n data-testid=\"header-live-link\"\n className=\"inline-flex items-center gap-0.5 rounded px-0.5 font-medium text-foreground hover:bg-primary/10 hover:text-primary hover:underline\"\n >\n {leaf?.text ?? ''}\n <ExternalLink className=\"size-3\" aria-hidden />\n </a>\n ) : (\n <span className=\"inline-flex items-center gap-0.5 px-0.5\">\n {leaf !== undefined ? (\n <span className=\"font-medium text-foreground\" title={translate('cms.details.urlCurrent')}>\n {leaf.text}\n </span>\n ) : null}\n {live ? (\n <span\n title={live.title}\n aria-label={live.title}\n data-testid=\"header-live-link-disabled\"\n className=\"inline-flex text-muted-foreground/40\"\n >\n <ExternalLink className=\"size-3\" aria-hidden />\n </span>\n ) : null}\n </span>\n )}\n </span>\n </Row>\n )\n}\n"]}
|
|
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
2
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, applySlugify, assetAcceptAttr, assetMatchesAccept, assetNode, assetSubfieldsOf, describeAccept, descendantIds, findSlugCollision, isFieldHidden, parentRelationField, readAssetId, resolveSlugPath, slugSourceValue, } from '@elytracms/core/cms-core';
|
|
5
|
+
import { BUILTIN_FIELD_CONTROLS, applySlugify, assetAcceptAttr, assetMatchesAccept, assetNode, assetSubfieldsOf, describeAccept, descendantIds, findSlugCollision, isFieldHidden, parentRelationField, readAssetId, resolveSlugPath, resolveUrlSegments, slugSourceValue, } from '@elytracms/core/cms-core';
|
|
6
6
|
import { resolveDeploymentRoutes } from '@elytracms/studio/lib/workspace';
|
|
7
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';
|
|
8
8
|
import { cn } from '@elytracms/studio/lib/utils';
|
|
@@ -13,7 +13,7 @@ import { useOptionalAssets } from '../assets/assets-store';
|
|
|
13
13
|
import { AssetThumb, FocalCropPreviews, FocalPointPicker, useAssetRecord } from '../assets/asset-thumb';
|
|
14
14
|
import { newlyAddedAssetIds } from '../assets/focal-point';
|
|
15
15
|
import { assetMetaFieldsOf, pruneMeta } from '../assets/asset-metadata';
|
|
16
|
-
import { fieldLabel } from './cms-
|
|
16
|
+
import { fieldLabel } from './cms-labels';
|
|
17
17
|
import { useCollectionView } from '../collection-view/collection-view-context';
|
|
18
18
|
import { useFieldDensity } from './field-density';
|
|
19
19
|
import { useOptionalDocumentEditor } from './document-editor-context';
|
|
@@ -21,6 +21,7 @@ import { toRefArray } from './cell';
|
|
|
21
21
|
import { RelationPicker } from './relation-picker';
|
|
22
22
|
import { AssetPicker } from './asset-picker';
|
|
23
23
|
import { RichTextEditor } from './rich-text';
|
|
24
|
+
import { DocumentUrlSegment } from './document-url-path';
|
|
24
25
|
import { FieldRendererBoundary, FieldRendererIssue, useFieldRenderers, } from './field-renderer';
|
|
25
26
|
/**
|
|
26
27
|
* The document-editor field control. Resolves a project's custom renderer by the
|
|
@@ -182,6 +183,14 @@ function SlugField({ field, value, onChange, readOnly, id, }) {
|
|
|
182
183
|
const parts = draft && routable
|
|
183
184
|
? resolveSlugPath({ routes, localeConfig }, source.schema, draft, { siblings, locale })
|
|
184
185
|
: undefined;
|
|
186
|
+
// The classified context segments (0.0.42, round 3) — the same `resolveUrlSegments`
|
|
187
|
+
// the header URL row uses, so the slug prefix and the header path attribute each
|
|
188
|
+
// segment identically. Everything before this document's own `current` slug is the
|
|
189
|
+
// context prefix; its LAST segment is the immediate parent shown in the compact pill.
|
|
190
|
+
const contextSegments = draft && routable
|
|
191
|
+
? resolveUrlSegments({ routes, localeConfig }, source.schema, draft, { siblings, locale }).filter((s) => s.kind !== 'current')
|
|
192
|
+
: [];
|
|
193
|
+
const lastContext = contextSegments[contextSegments.length - 1];
|
|
185
194
|
// Default (sync) uniqueness — the first colliding sibling's title, or null.
|
|
186
195
|
const defaultCollisionTitle = useMemo(() => {
|
|
187
196
|
if (slugConfig?.unique || !draft)
|
|
@@ -247,17 +256,14 @@ function SlugField({ field, value, onChange, readOnly, id, }) {
|
|
|
247
256
|
}
|
|
248
257
|
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
258
|
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"):
|
|
251
|
-
// context segment (`…/produkte/`)
|
|
252
|
-
// sidebar
|
|
253
|
-
//
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
:
|
|
257
|
-
|
|
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] }));
|
|
259
|
+
// The compact, ellipsized read-only prefix (0.0.39 "hybrid split", round-3 clickable):
|
|
260
|
+
// only the LAST context segment (`…/produkte/`) so a deep path can't overflow the
|
|
261
|
+
// narrow Details sidebar — but that shown segment is now CLICKABLE with the same
|
|
262
|
+
// navigation semantics as the header URL row (an ancestor DOCUMENT links to its
|
|
263
|
+
// editor; a static route literal stays plain). The full composed path is the tooltip.
|
|
264
|
+
const prefixTitle = parts?.path ??
|
|
265
|
+
(contextSegments.length > 0 ? `/${contextSegments.map((s) => s.text).join('/')}/` : '/');
|
|
266
|
+
return (_jsxs(Stack, { gap: "2", children: [parts ? (_jsxs(Row, { gap: "1", align: "center", "data-testid": "slug-composed-path", children: [_jsx("span", { className: "inline-flex min-w-0 shrink-0 items-center 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: contextSegments.length === 0 ? (_jsx("span", { "aria-hidden": true, className: "text-muted-foreground/50", children: "/" })) : (_jsxs(_Fragment, { children: [_jsx("span", { "aria-hidden": true, className: "text-muted-foreground/50", children: contextSegments.length > 1 ? '…/' : '/' }), lastContext ? (_jsx(DocumentUrlSegment, { segment: lastContext, projectId: editor?.projectId, collectionId: source.collectionId })) : null, _jsx("span", { "aria-hidden": true, className: "text-muted-foreground/50", children: "/" })] })) }), 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
267
|
}
|
|
262
268
|
function asRecord(value) {
|
|
263
269
|
return value !== null && typeof value === 'object' && !Array.isArray(value)
|