@elytracms/studio 0.0.27 → 0.0.28
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/assets.ts +19 -10
- package/convex/authority.ts +60 -0
- package/convex/cliTokens.ts +4 -4
- package/convex/cms.ts +12 -12
- package/convex/content.ts +10 -3
- package/convex/delivery.ts +22 -4
- package/convex/guard.ts +28 -11
- package/convex/members.ts +40 -7
- package/convex/redirects.ts +67 -0
- package/convex/references.ts +11 -11
- package/convex/schema.ts +30 -1
- package/convex/sync.ts +17 -10
- package/convex/users.ts +63 -0
- package/dist/components/app-views/default-modules.d.ts +33 -14
- package/dist/components/app-views/default-modules.js +59 -20
- package/dist/components/app-views/default-modules.js.map +1 -1
- package/dist/components/app-views/module.d.ts +32 -0
- package/dist/components/app-views/module.js +16 -0
- package/dist/components/app-views/module.js.map +1 -1
- package/dist/components/app-views/sidebar.d.ts +15 -1
- package/dist/components/app-views/sidebar.js +10 -0
- package/dist/components/app-views/sidebar.js.map +1 -1
- package/dist/components/cms/asset-picker.js +4 -4
- package/dist/components/cms/asset-picker.js.map +1 -1
- package/dist/components/cms/cell.js +16 -13
- package/dist/components/cms/cell.js.map +1 -1
- package/dist/components/cms/cms-store.d.ts +11 -26
- package/dist/components/cms/cms-store.js +25 -160
- package/dist/components/cms/cms-store.js.map +1 -1
- package/dist/components/cms/collection-table.d.ts +10 -0
- package/dist/components/cms/collection-table.js +72 -72
- package/dist/components/cms/collection-table.js.map +1 -1
- package/dist/components/cms/detail-editor.d.ts +14 -0
- package/dist/components/cms/detail-editor.js +224 -72
- package/dist/components/cms/detail-editor.js.map +1 -1
- package/dist/components/cms/document-editor-context.d.ts +26 -0
- package/dist/components/cms/document-editor-context.js.map +1 -1
- package/dist/components/cms/editable-cell.js +12 -7
- package/dist/components/cms/editable-cell.js.map +1 -1
- package/dist/components/cms/field-control.d.ts +2 -0
- package/dist/components/cms/field-control.js +40 -30
- package/dist/components/cms/field-control.js.map +1 -1
- package/dist/components/cms/filter-builder.d.ts +18 -0
- package/dist/components/cms/filter-builder.js +47 -6
- package/dist/components/cms/filter-builder.js.map +1 -1
- package/dist/components/cms/import-export.js +5 -3
- package/dist/components/cms/import-export.js.map +1 -1
- package/dist/components/cms/reference-confirm.js +8 -9
- package/dist/components/cms/reference-confirm.js.map +1 -1
- package/dist/components/cms/relation-picker.js +13 -10
- package/dist/components/cms/relation-picker.js.map +1 -1
- package/dist/components/cms/use-cell-edit.js +13 -7
- package/dist/components/cms/use-cell-edit.js.map +1 -1
- package/dist/components/cms/use-document-versions.d.ts +16 -0
- package/dist/components/cms/use-document-versions.js +31 -0
- package/dist/components/cms/use-document-versions.js.map +1 -0
- package/dist/components/cms/version-compare-text.d.ts +30 -0
- package/dist/components/cms/version-compare-text.js +113 -0
- package/dist/components/cms/version-compare-text.js.map +1 -0
- package/dist/components/cms/version-compare.d.ts +12 -0
- package/dist/components/cms/version-compare.js +271 -0
- package/dist/components/cms/version-compare.js.map +1 -0
- package/dist/components/cms/versions-pane.d.ts +3 -0
- package/dist/components/cms/versions-pane.js +47 -38
- package/dist/components/cms/versions-pane.js.map +1 -1
- package/dist/components/cms/view-bar.d.ts +21 -6
- package/dist/components/cms/view-bar.js +42 -57
- package/dist/components/cms/view-bar.js.map +1 -1
- package/dist/components/collection-view/cms-collection-view-source.d.ts +15 -0
- package/dist/components/collection-view/cms-collection-view-source.js +158 -0
- package/dist/components/collection-view/cms-collection-view-source.js.map +1 -0
- package/dist/components/collection-view/collection-view-context.d.ts +10 -0
- package/dist/components/collection-view/collection-view-context.js +26 -0
- package/dist/components/collection-view/collection-view-context.js.map +1 -0
- package/dist/components/current-user-menu.js +8 -1
- package/dist/components/current-user-menu.js.map +1 -1
- package/dist/components/redirects/redirects-collection-view-source.d.ts +18 -0
- package/dist/components/redirects/redirects-collection-view-source.js +114 -0
- package/dist/components/redirects/redirects-collection-view-source.js.map +1 -0
- package/dist/components/redirects/redirects-module.d.ts +18 -0
- package/dist/components/redirects/redirects-module.js +43 -0
- package/dist/components/redirects/redirects-module.js.map +1 -0
- package/dist/components/redirects/redirects-schema.d.ts +10 -0
- package/dist/components/redirects/redirects-schema.js +42 -0
- package/dist/components/redirects/redirects-schema.js.map +1 -0
- package/dist/components/redirects/redirects-store.d.ts +46 -0
- package/dist/components/redirects/redirects-store.js +124 -0
- package/dist/components/redirects/redirects-store.js.map +1 -0
- package/dist/components/references/references-panel.js +2 -2
- package/dist/components/references/references-panel.js.map +1 -1
- package/dist/components/section-nav.js +14 -5
- package/dist/components/section-nav.js.map +1 -1
- package/dist/components/users/user-search.d.ts +8 -0
- package/dist/components/users/user-search.js +13 -0
- package/dist/components/users/user-search.js.map +1 -0
- package/dist/components/users/users-collection-view-source.d.ts +28 -0
- package/dist/components/users/users-collection-view-source.js +157 -0
- package/dist/components/users/users-collection-view-source.js.map +1 -0
- package/dist/components/users/users-module.d.ts +23 -0
- package/dist/components/users/users-module.js +61 -0
- package/dist/components/users/users-module.js.map +1 -0
- package/dist/components/users/users-schema.d.ts +19 -0
- package/dist/components/users/users-schema.js +38 -0
- package/dist/components/users/users-schema.js.map +1 -0
- package/dist/components/users/users-store.d.ts +40 -0
- package/dist/components/users/users-store.js +87 -0
- package/dist/components/users/users-store.js.map +1 -0
- package/dist/lib/cms-views/index.d.ts +4 -5
- package/dist/lib/cms-views/index.js +4 -5
- package/dist/lib/cms-views/index.js.map +1 -1
- package/dist/lib/cms-views/view-model.d.ts +48 -1
- package/dist/lib/cms-views/view-model.js +77 -1
- package/dist/lib/cms-views/view-model.js.map +1 -1
- package/dist/lib/collection-view/collection-view-source.d.ts +251 -0
- package/dist/lib/collection-view/collection-view-source.js +10 -0
- package/dist/lib/collection-view/collection-view-source.js.map +1 -0
- package/dist/lib/operations/session-hydration.d.ts +7 -0
- package/dist/lib/operations/session-hydration.js +7 -1
- package/dist/lib/operations/session-hydration.js.map +1 -1
- package/dist/lib/persistence/convex-adapter.d.ts +5 -1
- package/dist/lib/persistence/convex-adapter.js +148 -1
- package/dist/lib/persistence/convex-adapter.js.map +1 -1
- package/dist/lib/workspace/index.d.ts +1 -8
- package/dist/lib/workspace/index.js +3 -9
- package/dist/lib/workspace/index.js.map +1 -1
- package/dist/router/match.d.ts +4 -0
- package/dist/router/match.js +8 -0
- package/dist/router/match.js.map +1 -1
- package/dist/ui/index.d.ts +2 -0
- package/dist/ui/index.js +2 -0
- package/dist/ui/index.js.map +1 -1
- package/dist/ui/popover.d.ts +13 -0
- package/dist/ui/popover.js +17 -0
- package/dist/ui/popover.js.map +1 -0
- package/dist/ui/row-item.d.ts +16 -0
- package/dist/ui/row-item.js +18 -0
- package/dist/ui/row-item.js.map +1 -0
- package/package.json +3 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-cell-edit.js","sourceRoot":"","sources":["../../../src/components/cms/use-cell-edit.ts"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useRef, useState } from 'react'\nimport type { RefObject } from 'react'\nimport { resolveField } from '@elytracms/core/cms-core'\nimport type { CmsDocument, CollectionDef } from '@elytracms/core/cms-core'\nimport { cellErrorMessage, cellKey, nextEditableCell } from './cell-edit'\nimport type { AdvanceDirection, CellCoord } from './cell-edit'\nimport { useCms } from './cms-store'\n\n/**\n * Table-level cell-edit controller (EC-168). Owns the state machine the cells\n * render from:\n *\n * idle → editing (click/Enter) → committing (Enter/Tab/blur) → idle\n * ↘ cancelled (Esc, no operation) ↘ rejected (rollback + cell error)\n *\n * Exactly one cell edits at a time; commits go through\n * `cms.commitFieldEdit` (one operations command each); rejections park a\n * per-cell error that the cell renders inline until the cell is edited again.\n */\nexport interface CellEditController {\n /** Ref for the table container (focus restoration after commits/cancels). */\n containerRef: RefObject<HTMLDivElement | null>\n /** Key of the cell currently editing, or null. */\n activeKey: string | null\n /** Key of the cell with a commit in flight, or null. */\n busyKey: string | null\n errorOf(key: string): string | null\n start(coord: CellCoord): void\n cancel(coord: CellCoord): void\n commit(coord: CellCoord, value: unknown, advance: AdvanceDirection | null): void\n}\n\nexport function useCellEdit({\n collection,\n docs,\n editableFields,\n}: {\n collection: CollectionDef\n /** Documents in rendered (sorted) row order. */\n docs: readonly CmsDocument[]\n /** Editable column field names in rendered order. */\n editableFields: readonly string[]\n}): CellEditController {\n const cms = useCms()\n const [activeKey, setActiveKey] = useState<string | null>(null)\n const [busyKey, setBusyKey] = useState<string | null>(null)\n const [errors, setErrors] = useState<Record<string, string>>({})\n const containerRef = useRef<HTMLDivElement | null>(null)\n\n // Snapshots for async commit completion (row order may re-sort mid-flight).\n const activeRef = useRef(activeKey)\n activeRef.current = activeKey\n const docsRef = useRef(docs)\n docsRef.current = docs\n const fieldsRef = useRef(editableFields)\n fieldsRef.current = editableFields\n\n const clearError = useCallback((key: string) => {\n setErrors((prev) => {\n if (!(key in prev)) return prev\n const next = { ...prev }\n delete next[key]\n return next\n })\n }, [])\n\n const focusCell = useCallback((key: string) => {\n // After state settles: return focus to the cell so Enter re-edits and the\n // keyboard flow continues where the user left off.\n requestAnimationFrame(() => {\n containerRef.current\n ?.querySelector<HTMLElement>(`[data-cell-key=\"${CSS.escape(key)}\"]`)\n ?.focus()\n })\n }, [])\n\n const start = useCallback(\n (coord: CellCoord) => {\n const key = cellKey(coord)\n clearError(key)\n setActiveKey(key)\n },\n [clearError],\n )\n\n const cancel = useCallback(\n (coord: CellCoord) => {\n const key = cellKey(coord)\n if (activeRef.current !== key) return\n setActiveKey(null)\n focusCell(key)\n },\n [focusCell],\n )\n\n /** Close the editor and move on (commit settled or value unchanged). */\n const settle = useCallback(\n (coord: CellCoord, next: CellCoord | null) => {\n const key = cellKey(coord)\n // The user may have clicked into another cell while the commit ran —\n // never clobber their new edit session.\n if (activeRef.current !== key) return\n if (next) {\n clearError(cellKey(next))\n setActiveKey(cellKey(next))\n } else {\n setActiveKey(null)\n focusCell(key)\n }\n },\n [clearError, focusCell],\n )\n\n const commit = useCallback(\n (coord: CellCoord, value: unknown, advance: AdvanceDirection | null) => {\n const key = cellKey(coord)\n const doc = docsRef.current.find((d) => d.id === coord.docId)\n const field = collection.fields.find((f) => f.name === coord.field)\n if (!doc || !field) {\n settle(coord, null)\n return\n }\n // The advance target is computed against the row order at commit time —\n // a re-sort caused by the edit must not teleport the cursor.\n const next = advance\n ? nextEditableCell(\n docsRef.current.map((d) => d.id),\n fieldsRef.current,\n coord,\n advance,\n )\n : null\n\n // Same value as displayed → close (and advance) without any operation.\n // Notably: re-committing an unchanged fallback-shown value dispatches\n // nothing — a localized variant is only created by an actual edit.\n const locale = cms.activeLocale\n const original = resolveField(collection, doc, field.name, locale, cms.localeConfig).value\n if (sameCellValue(value, original)) {\n settle(coord, next)\n return\n }\n\n setBusyKey(key)\n void cms\n .commitFieldEdit({ collection, doc, field, locale, value })\n .then((outcome) => {\n if (outcome.ok) {\n clearError(key)\n settle(coord, next)\n return\n }\n // Rejected: the session already rolled back — park the error on the\n // cell (never a toast) and return focus there.\n setErrors((prev) => ({ ...prev, [key]: cellErrorMessage(outcome.error) }))\n settle(coord, null)\n })\n .finally(() => {\n setBusyKey((prev) => (prev === key ? null : prev))\n })\n },\n [cms, collection, clearError, settle],\n )\n\n return {\n containerRef,\n activeKey,\n busyKey,\n errorOf: (key) => errors[key] ?? null,\n start,\n cancel,\n commit,\n }\n}\n\n/** Structural equality good enough for cell values (scalars, refs, id arrays). */\nfunction sameCellValue(a: unknown, b: unknown): boolean {\n if (a === undefined && b === undefined) return true\n try {\n return JSON.stringify(a) === JSON.stringify(b)\n } catch {\n return false\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"use-cell-edit.js","sourceRoot":"","sources":["../../../src/components/cms/use-cell-edit.ts"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useRef, useState } from 'react'\nimport type { RefObject } from 'react'\nimport { resolveField } from '@elytracms/core/cms-core'\nimport type { CmsDocument, CollectionDef } from '@elytracms/core/cms-core'\nimport { cellErrorMessage, cellKey, nextEditableCell } from './cell-edit'\nimport type { AdvanceDirection, CellCoord } from './cell-edit'\nimport { useCollectionView } from '../collection-view/collection-view-context'\n\n/**\n * Table-level cell-edit controller (EC-168). Owns the state machine the cells\n * render from:\n *\n * idle → editing (click/Enter) → committing (Enter/Tab/blur) → idle\n * ↘ cancelled (Esc, no operation) ↘ rejected (rollback + cell error)\n *\n * Exactly one cell edits at a time; commits go through\n * `cms.commitFieldEdit` (one operations command each); rejections park a\n * per-cell error that the cell renders inline until the cell is edited again.\n */\nexport interface CellEditController {\n /** Ref for the table container (focus restoration after commits/cancels). */\n containerRef: RefObject<HTMLDivElement | null>\n /** Key of the cell currently editing, or null. */\n activeKey: string | null\n /** Key of the cell with a commit in flight, or null. */\n busyKey: string | null\n errorOf(key: string): string | null\n start(coord: CellCoord): void\n cancel(coord: CellCoord): void\n commit(coord: CellCoord, value: unknown, advance: AdvanceDirection | null): void\n}\n\nexport function useCellEdit({\n collection,\n docs,\n editableFields,\n}: {\n collection: CollectionDef\n /** Documents in rendered (sorted) row order. */\n docs: readonly CmsDocument[]\n /** Editable column field names in rendered order. */\n editableFields: readonly string[]\n}): CellEditController {\n const source = useCollectionView()\n const [activeKey, setActiveKey] = useState<string | null>(null)\n const [busyKey, setBusyKey] = useState<string | null>(null)\n const [errors, setErrors] = useState<Record<string, string>>({})\n const containerRef = useRef<HTMLDivElement | null>(null)\n\n // Snapshots for async commit completion (row order may re-sort mid-flight).\n const activeRef = useRef(activeKey)\n activeRef.current = activeKey\n const docsRef = useRef(docs)\n docsRef.current = docs\n const fieldsRef = useRef(editableFields)\n fieldsRef.current = editableFields\n\n const clearError = useCallback((key: string) => {\n setErrors((prev) => {\n if (!(key in prev)) return prev\n const next = { ...prev }\n delete next[key]\n return next\n })\n }, [])\n\n const focusCell = useCallback((key: string) => {\n // After state settles: return focus to the cell so Enter re-edits and the\n // keyboard flow continues where the user left off.\n requestAnimationFrame(() => {\n containerRef.current\n ?.querySelector<HTMLElement>(`[data-cell-key=\"${CSS.escape(key)}\"]`)\n ?.focus()\n })\n }, [])\n\n const start = useCallback(\n (coord: CellCoord) => {\n const key = cellKey(coord)\n clearError(key)\n setActiveKey(key)\n },\n [clearError],\n )\n\n const cancel = useCallback(\n (coord: CellCoord) => {\n const key = cellKey(coord)\n if (activeRef.current !== key) return\n setActiveKey(null)\n focusCell(key)\n },\n [focusCell],\n )\n\n /** Close the editor and move on (commit settled or value unchanged). */\n const settle = useCallback(\n (coord: CellCoord, next: CellCoord | null) => {\n const key = cellKey(coord)\n // The user may have clicked into another cell while the commit ran —\n // never clobber their new edit session.\n if (activeRef.current !== key) return\n if (next) {\n clearError(cellKey(next))\n setActiveKey(cellKey(next))\n } else {\n setActiveKey(null)\n focusCell(key)\n }\n },\n [clearError, focusCell],\n )\n\n const commit = useCallback(\n (coord: CellCoord, value: unknown, advance: AdvanceDirection | null) => {\n const key = cellKey(coord)\n const doc = docsRef.current.find((d) => d.id === coord.docId)\n const field = collection.fields.find((f) => f.name === coord.field)\n if (!doc || !field) {\n settle(coord, null)\n return\n }\n // The advance target is computed against the row order at commit time —\n // a re-sort caused by the edit must not teleport the cursor.\n const next = advance\n ? nextEditableCell(\n docsRef.current.map((d) => d.id),\n fieldsRef.current,\n coord,\n advance,\n )\n : null\n\n // Same value as displayed → close (and advance) without any operation.\n // Notably: re-committing an unchanged fallback-shown value dispatches\n // nothing — a localized variant is only created by an actual edit.\n const locale = source.activeLocale\n const original = resolveField(collection, doc, field.name, locale, source.localeConfig).value\n if (sameCellValue(value, original)) {\n settle(coord, next)\n return\n }\n\n // Inline editing is a capability — absent it (a read-only rail) there is\n // nothing to commit; close the editor without dispatching.\n const commitField = source.inlineEditing?.commitField\n if (!commitField) {\n settle(coord, null)\n return\n }\n\n setBusyKey(key)\n void commitField({ collection, doc, field, locale, value })\n .then((outcome) => {\n if (outcome.ok) {\n clearError(key)\n settle(coord, next)\n return\n }\n // Rejected: the session already rolled back — park the error on the\n // cell (never a toast) and return focus there.\n setErrors((prev) => ({ ...prev, [key]: cellErrorMessage(outcome.error) }))\n settle(coord, null)\n })\n .finally(() => {\n setBusyKey((prev) => (prev === key ? null : prev))\n })\n },\n [source, collection, clearError, settle],\n )\n\n return {\n containerRef,\n activeKey,\n busyKey,\n errorOf: (key) => errors[key] ?? null,\n start,\n cancel,\n commit,\n }\n}\n\n/** Structural equality good enough for cell values (scalars, refs, id arrays). */\nfunction sameCellValue(a: unknown, b: unknown): boolean {\n if (a === undefined && b === undefined) return true\n try {\n return JSON.stringify(a) === JSON.stringify(b)\n } catch {\n return false\n }\n}\n"]}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { DocumentRef } from '@elytracms/core/cms-core';
|
|
2
|
+
import type { DocumentVersionsView } from './cms-store';
|
|
3
|
+
/** The one cache key for a document's version history — shared so the Versions
|
|
4
|
+
* pane and the EC-298 compare view read through a single deduped query. */
|
|
5
|
+
export declare function documentVersionsKey(projectId: string | undefined, ref: DocumentRef): readonly ["documentVersions", string, string, string];
|
|
6
|
+
/**
|
|
7
|
+
* A document's recorded version history, read through the one operations-backed
|
|
8
|
+
* query cache (EC-308/AD-17). The flush-before-read ordering lives IN the loader,
|
|
9
|
+
* so every fetch — initial, invalidated, or refetch — lists an authoritative
|
|
10
|
+
* history regardless of which surface triggered the change. Two mounts with the
|
|
11
|
+
* same document share one fetch (TanStack dedup on the key).
|
|
12
|
+
*/
|
|
13
|
+
export declare function useDocumentVersionsQuery(ref: DocumentRef): {
|
|
14
|
+
versionsQuery: import("@tanstack/react-query").UseQueryResult<DocumentVersionsView>;
|
|
15
|
+
versionKey: readonly ["documentVersions", string, string, string];
|
|
16
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import { useOperationQuery } from '@elytracms/studio/lib/query';
|
|
3
|
+
import { useCollectionView } from '../collection-view/collection-view-context';
|
|
4
|
+
/** Recorded-version freshness: any document command (publish/restore/upsert) may
|
|
5
|
+
* change the history, so re-read on the whole `documents` domain. */
|
|
6
|
+
const VERSION_RELOAD_OPS = ['documents'];
|
|
7
|
+
/** The one cache key for a document's version history — shared so the Versions
|
|
8
|
+
* pane and the EC-298 compare view read through a single deduped query. */
|
|
9
|
+
export function documentVersionsKey(projectId, ref) {
|
|
10
|
+
return ['documentVersions', projectId ?? '∅', ref.collection, ref.id];
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* A document's recorded version history, read through the one operations-backed
|
|
14
|
+
* query cache (EC-308/AD-17). The flush-before-read ordering lives IN the loader,
|
|
15
|
+
* so every fetch — initial, invalidated, or refetch — lists an authoritative
|
|
16
|
+
* history regardless of which surface triggered the change. Two mounts with the
|
|
17
|
+
* same document share one fetch (TanStack dedup on the key).
|
|
18
|
+
*/
|
|
19
|
+
export function useDocumentVersionsQuery(ref) {
|
|
20
|
+
const source = useCollectionView();
|
|
21
|
+
const versionKey = useMemo(() => documentVersionsKey(source.projectId, ref), [source.projectId, ref.collection, ref.id]);
|
|
22
|
+
const versionsQuery = useOperationQuery({
|
|
23
|
+
queryKey: versionKey,
|
|
24
|
+
// The versioning capability owns the flush-before-read ordering; a source
|
|
25
|
+
// without versions (never mounts this) degrades to an empty history.
|
|
26
|
+
loader: async () => (await source.versioning?.load(ref)) ?? { versions: [] },
|
|
27
|
+
invalidateOn: VERSION_RELOAD_OPS,
|
|
28
|
+
});
|
|
29
|
+
return { versionsQuery, versionKey };
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=use-document-versions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-document-versions.js","sourceRoot":"","sources":["../../../src/components/cms/use-document-versions.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,OAAO,CAAA;AAE/B,OAAO,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAA;AAC/D,OAAO,EAAE,iBAAiB,EAAE,MAAM,4CAA4C,CAAA;AAG9E;sEACsE;AACtE,MAAM,kBAAkB,GAAG,CAAC,WAAW,CAAU,CAAA;AAEjD;4EAC4E;AAC5E,MAAM,UAAU,mBAAmB,CAAC,SAA6B,EAAE,GAAgB;IACjF,OAAO,CAAC,kBAAkB,EAAE,SAAS,IAAI,GAAG,EAAE,GAAG,CAAC,UAAU,EAAE,GAAG,CAAC,EAAE,CAAU,CAAA;AAChF,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,wBAAwB,CAAC,GAAgB;IACvD,MAAM,MAAM,GAAG,iBAAiB,EAAE,CAAA;IAClC,MAAM,UAAU,GAAG,OAAO,CACxB,GAAG,EAAE,CAAC,mBAAmB,CAAC,MAAM,CAAC,SAAS,EAAE,GAAG,CAAC,EAChD,CAAC,MAAM,CAAC,SAAS,EAAE,GAAG,CAAC,UAAU,EAAE,GAAG,CAAC,EAAE,CAAC,CAC3C,CAAA;IACD,MAAM,aAAa,GAAG,iBAAiB,CAAuB;QAC5D,QAAQ,EAAE,UAAU;QACpB,0EAA0E;QAC1E,qEAAqE;QACrE,MAAM,EAAE,KAAK,IAAI,EAAE,CAAC,CAAC,MAAM,MAAM,CAAC,UAAU,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,QAAQ,EAAE,EAAE,EAAE;QAC5E,YAAY,EAAE,kBAAkB;KACjC,CAAC,CAAA;IACF,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,CAAA;AACtC,CAAC","sourcesContent":["import { useMemo } from 'react'\nimport type { DocumentRef } from '@elytracms/core/cms-core'\nimport { useOperationQuery } from '@elytracms/studio/lib/query'\nimport { useCollectionView } from '../collection-view/collection-view-context'\nimport type { DocumentVersionsView } from './cms-store'\n\n/** Recorded-version freshness: any document command (publish/restore/upsert) may\n * change the history, so re-read on the whole `documents` domain. */\nconst VERSION_RELOAD_OPS = ['documents'] as const\n\n/** The one cache key for a document's version history — shared so the Versions\n * pane and the EC-298 compare view read through a single deduped query. */\nexport function documentVersionsKey(projectId: string | undefined, ref: DocumentRef) {\n return ['documentVersions', projectId ?? '∅', ref.collection, ref.id] as const\n}\n\n/**\n * A document's recorded version history, read through the one operations-backed\n * query cache (EC-308/AD-17). The flush-before-read ordering lives IN the loader,\n * so every fetch — initial, invalidated, or refetch — lists an authoritative\n * history regardless of which surface triggered the change. Two mounts with the\n * same document share one fetch (TanStack dedup on the key).\n */\nexport function useDocumentVersionsQuery(ref: DocumentRef) {\n const source = useCollectionView()\n const versionKey = useMemo(\n () => documentVersionsKey(source.projectId, ref),\n [source.projectId, ref.collection, ref.id],\n )\n const versionsQuery = useOperationQuery<DocumentVersionsView>({\n queryKey: versionKey,\n // The versioning capability owns the flush-before-read ordering; a source\n // without versions (never mounts this) degrades to an empty history.\n loader: async () => (await source.versioning?.load(ref)) ?? { versions: [] },\n invalidateOn: VERSION_RELOAD_OPS,\n })\n return { versionsQuery, versionKey }\n}\n"]}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure text helpers for the EC-298 compare view — extracted so the fiddly bits
|
|
3
|
+
* (word-level LCS, format-agnostic "rich text" extraction) are unit-tested apart
|
|
4
|
+
* from the React surface.
|
|
5
|
+
*/
|
|
6
|
+
export interface WordToken {
|
|
7
|
+
text: string;
|
|
8
|
+
changed: boolean;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Word-level LCS diff (prose, not code): tokens NOT on the longest common
|
|
12
|
+
* subsequence are marked — removed on the before side, added on the after side.
|
|
13
|
+
* Whitespace tokens participate, so spacing is preserved on render.
|
|
14
|
+
*/
|
|
15
|
+
export declare function wordDiff(before: string, after: string): {
|
|
16
|
+
before: WordToken[];
|
|
17
|
+
after: WordToken[];
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* A "rich text" value as readable text for the prose diff — FORMAT-AGNOSTIC, since
|
|
21
|
+
* the stored value can be anything the field's editor writes: a plain/HTML STRING
|
|
22
|
+
* (a simple text input), a TipTap `{version, doc}` tree, or a bare doc. We show
|
|
23
|
+
* whatever is entered rather than assuming a structure (EC-298).
|
|
24
|
+
*/
|
|
25
|
+
export declare function richTextToText(value: unknown): string;
|
|
26
|
+
export declare function looksLikeHtml(s: string): boolean;
|
|
27
|
+
/** Strip HTML to readable text, keeping block breaks so paragraphs still separate. */
|
|
28
|
+
export declare function htmlToText(html: string): string;
|
|
29
|
+
/** Plain text from ProseMirror/TipTap nodes: text runs joined, blocks separated. */
|
|
30
|
+
export declare function plainTextFromNodes(nodes: unknown[]): string;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure text helpers for the EC-298 compare view — extracted so the fiddly bits
|
|
3
|
+
* (word-level LCS, format-agnostic "rich text" extraction) are unit-tested apart
|
|
4
|
+
* from the React surface.
|
|
5
|
+
*/
|
|
6
|
+
function tokenize(s) {
|
|
7
|
+
return s.split(/(\s+)/).filter((t) => t.length > 0);
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Word-level LCS diff (prose, not code): tokens NOT on the longest common
|
|
11
|
+
* subsequence are marked — removed on the before side, added on the after side.
|
|
12
|
+
* Whitespace tokens participate, so spacing is preserved on render.
|
|
13
|
+
*/
|
|
14
|
+
export function wordDiff(before, after) {
|
|
15
|
+
const a = tokenize(before);
|
|
16
|
+
const b = tokenize(after);
|
|
17
|
+
// Bound the DP so a pathological pair can't stall the editor.
|
|
18
|
+
if (a.length + b.length > 4000) {
|
|
19
|
+
return {
|
|
20
|
+
before: a.map((text) => ({ text, changed: true })),
|
|
21
|
+
after: b.map((text) => ({ text, changed: true })),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
const m = a.length;
|
|
25
|
+
const n = b.length;
|
|
26
|
+
const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
|
|
27
|
+
for (let i = m - 1; i >= 0; i--) {
|
|
28
|
+
for (let j = n - 1; j >= 0; j--) {
|
|
29
|
+
dp[i][j] = a[i] === b[j] ? dp[i + 1][j + 1] + 1 : Math.max(dp[i + 1][j], dp[i][j + 1]);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const beforeTokens = [];
|
|
33
|
+
const afterTokens = [];
|
|
34
|
+
let i = 0;
|
|
35
|
+
let j = 0;
|
|
36
|
+
while (i < m && j < n) {
|
|
37
|
+
if (a[i] === b[j]) {
|
|
38
|
+
beforeTokens.push({ text: a[i], changed: false });
|
|
39
|
+
afterTokens.push({ text: b[j], changed: false });
|
|
40
|
+
i++;
|
|
41
|
+
j++;
|
|
42
|
+
}
|
|
43
|
+
else if (dp[i + 1][j] >= dp[i][j + 1]) {
|
|
44
|
+
beforeTokens.push({ text: a[i], changed: true });
|
|
45
|
+
i++;
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
afterTokens.push({ text: b[j], changed: true });
|
|
49
|
+
j++;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
while (i < m)
|
|
53
|
+
beforeTokens.push({ text: a[i++], changed: true });
|
|
54
|
+
while (j < n)
|
|
55
|
+
afterTokens.push({ text: b[j++], changed: true });
|
|
56
|
+
return { before: beforeTokens, after: afterTokens };
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* A "rich text" value as readable text for the prose diff — FORMAT-AGNOSTIC, since
|
|
60
|
+
* the stored value can be anything the field's editor writes: a plain/HTML STRING
|
|
61
|
+
* (a simple text input), a TipTap `{version, doc}` tree, or a bare doc. We show
|
|
62
|
+
* whatever is entered rather than assuming a structure (EC-298).
|
|
63
|
+
*/
|
|
64
|
+
export function richTextToText(value) {
|
|
65
|
+
if (typeof value === 'string')
|
|
66
|
+
return looksLikeHtml(value) ? htmlToText(value) : value;
|
|
67
|
+
if (value && typeof value === 'object') {
|
|
68
|
+
const wrapper = value;
|
|
69
|
+
const doc = (wrapper.doc ?? wrapper);
|
|
70
|
+
if (Array.isArray(doc.content))
|
|
71
|
+
return plainTextFromNodes(doc.content);
|
|
72
|
+
}
|
|
73
|
+
return '';
|
|
74
|
+
}
|
|
75
|
+
export function looksLikeHtml(s) {
|
|
76
|
+
return /<[a-z!/][\s\S]*>/i.test(s);
|
|
77
|
+
}
|
|
78
|
+
/** Strip HTML to readable text, keeping block breaks so paragraphs still separate. */
|
|
79
|
+
export function htmlToText(html) {
|
|
80
|
+
return html
|
|
81
|
+
.replace(/<\s*br\s*\/?>/gi, '\n')
|
|
82
|
+
.replace(/<\/(p|div|h[1-6]|blockquote)\s*>/gi, '\n\n')
|
|
83
|
+
.replace(/<\/(li|tr|ul|ol)\s*>/gi, '\n')
|
|
84
|
+
.replace(/<[^>]+>/g, '')
|
|
85
|
+
.replace(/ /gi, ' ')
|
|
86
|
+
.replace(/&/gi, '&')
|
|
87
|
+
.replace(/</gi, '<')
|
|
88
|
+
.replace(/>/gi, '>')
|
|
89
|
+
.replace(/"/gi, '"')
|
|
90
|
+
.replace(/'/gi, "'")
|
|
91
|
+
.replace(/[ \t]+/g, ' ')
|
|
92
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
93
|
+
.trim();
|
|
94
|
+
}
|
|
95
|
+
/** Plain text from ProseMirror/TipTap nodes: text runs joined, blocks separated. */
|
|
96
|
+
export function plainTextFromNodes(nodes) {
|
|
97
|
+
const blockText = (node) => {
|
|
98
|
+
const parts = [];
|
|
99
|
+
const walk = (n) => {
|
|
100
|
+
if (!n || typeof n !== 'object')
|
|
101
|
+
return;
|
|
102
|
+
const rec = n;
|
|
103
|
+
if (typeof rec.text === 'string')
|
|
104
|
+
parts.push(rec.text);
|
|
105
|
+
if (Array.isArray(rec.content))
|
|
106
|
+
rec.content.forEach(walk);
|
|
107
|
+
};
|
|
108
|
+
walk(node);
|
|
109
|
+
return parts.join('');
|
|
110
|
+
};
|
|
111
|
+
return nodes.map(blockText).filter(Boolean).join('\n\n');
|
|
112
|
+
}
|
|
113
|
+
//# sourceMappingURL=version-compare-text.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"version-compare-text.js","sourceRoot":"","sources":["../../../src/components/cms/version-compare-text.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Pure text helpers for the EC-298 compare view — extracted so the fiddly bits\n * (word-level LCS, format-agnostic \"rich text\" extraction) are unit-tested apart\n * from the React surface.\n */\n\nexport interface WordToken {\n text: string\n changed: boolean\n}\n\nfunction tokenize(s: string): string[] {\n return s.split(/(\\s+)/).filter((t) => t.length > 0)\n}\n\n/**\n * Word-level LCS diff (prose, not code): tokens NOT on the longest common\n * subsequence are marked — removed on the before side, added on the after side.\n * Whitespace tokens participate, so spacing is preserved on render.\n */\nexport function wordDiff(before: string, after: string): { before: WordToken[]; after: WordToken[] } {\n const a = tokenize(before)\n const b = tokenize(after)\n // Bound the DP so a pathological pair can't stall the editor.\n if (a.length + b.length > 4000) {\n return {\n before: a.map((text) => ({ text, changed: true })),\n after: b.map((text) => ({ text, changed: true })),\n }\n }\n const m = a.length\n const n = b.length\n const dp: number[][] = Array.from({ length: m + 1 }, () => new Array<number>(n + 1).fill(0))\n for (let i = m - 1; i >= 0; i--) {\n for (let j = n - 1; j >= 0; j--) {\n dp[i]![j] = a[i] === b[j] ? dp[i + 1]![j + 1]! + 1 : Math.max(dp[i + 1]![j]!, dp[i]![j + 1]!)\n }\n }\n const beforeTokens: WordToken[] = []\n const afterTokens: WordToken[] = []\n let i = 0\n let j = 0\n while (i < m && j < n) {\n if (a[i] === b[j]) {\n beforeTokens.push({ text: a[i]!, changed: false })\n afterTokens.push({ text: b[j]!, changed: false })\n i++\n j++\n } else if (dp[i + 1]![j]! >= dp[i]![j + 1]!) {\n beforeTokens.push({ text: a[i]!, changed: true })\n i++\n } else {\n afterTokens.push({ text: b[j]!, changed: true })\n j++\n }\n }\n while (i < m) beforeTokens.push({ text: a[i++]!, changed: true })\n while (j < n) afterTokens.push({ text: b[j++]!, changed: true })\n return { before: beforeTokens, after: afterTokens }\n}\n\n/**\n * A \"rich text\" value as readable text for the prose diff — FORMAT-AGNOSTIC, since\n * the stored value can be anything the field's editor writes: a plain/HTML STRING\n * (a simple text input), a TipTap `{version, doc}` tree, or a bare doc. We show\n * whatever is entered rather than assuming a structure (EC-298).\n */\nexport function richTextToText(value: unknown): string {\n if (typeof value === 'string') return looksLikeHtml(value) ? htmlToText(value) : value\n if (value && typeof value === 'object') {\n const wrapper = value as { doc?: unknown; content?: unknown }\n const doc = (wrapper.doc ?? wrapper) as { content?: unknown }\n if (Array.isArray(doc.content)) return plainTextFromNodes(doc.content)\n }\n return ''\n}\n\nexport function looksLikeHtml(s: string): boolean {\n return /<[a-z!/][\\s\\S]*>/i.test(s)\n}\n\n/** Strip HTML to readable text, keeping block breaks so paragraphs still separate. */\nexport function htmlToText(html: string): string {\n return html\n .replace(/<\\s*br\\s*\\/?>/gi, '\\n')\n .replace(/<\\/(p|div|h[1-6]|blockquote)\\s*>/gi, '\\n\\n')\n .replace(/<\\/(li|tr|ul|ol)\\s*>/gi, '\\n')\n .replace(/<[^>]+>/g, '')\n .replace(/ /gi, ' ')\n .replace(/&/gi, '&')\n .replace(/</gi, '<')\n .replace(/>/gi, '>')\n .replace(/"/gi, '\"')\n .replace(/'/gi, \"'\")\n .replace(/[ \\t]+/g, ' ')\n .replace(/\\n{3,}/g, '\\n\\n')\n .trim()\n}\n\n/** Plain text from ProseMirror/TipTap nodes: text runs joined, blocks separated. */\nexport function plainTextFromNodes(nodes: unknown[]): string {\n const blockText = (node: unknown): string => {\n const parts: string[] = []\n const walk = (n: unknown) => {\n if (!n || typeof n !== 'object') return\n const rec = n as { text?: unknown; content?: unknown }\n if (typeof rec.text === 'string') parts.push(rec.text)\n if (Array.isArray(rec.content)) rec.content.forEach(walk)\n }\n walk(node)\n return parts.join('')\n }\n return nodes.map(blockText).filter(Boolean).join('\\n\\n')\n}\n"]}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EC-298 — the version compare view. Renders as the sticky **Compare** content tab
|
|
3
|
+
* (the Versions sidebar pane stays on as the navigator).
|
|
4
|
+
*
|
|
5
|
+
* A calm change-list grouped BY FIELD (a localized field's per-locale variants sit
|
|
6
|
+
* together): before | after across ONE continuous central divider. Text reads as a
|
|
7
|
+
* prose diff (changed words lit in place — rich text via its markdown projection so
|
|
8
|
+
* the content, not just "changed", is visible); assets as thumbnails; relations as
|
|
9
|
+
* titles. Each field/locale (and each block, at property level) restores back to the
|
|
10
|
+
* base version with a single ↩.
|
|
11
|
+
*/
|
|
12
|
+
export declare function VersionCompare(): import("react").JSX.Element | null;
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useMemo, useState } from 'react';
|
|
3
|
+
import { ArrowRight, ChevronDown, ChevronLeft, ChevronRight, Image as ImageIcon, RotateCcw } from 'lucide-react';
|
|
4
|
+
import { diffDocuments } from '@elytracms/core/content';
|
|
5
|
+
import { isAssetNode, readAssetId, readDocumentRef } from '@elytracms/core/cms-core';
|
|
6
|
+
import { Badge, Button, Code, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, Row, Spacer, Stack, Text, } from '@elytracms/studio/ui';
|
|
7
|
+
import { useOperationCache } from '@elytracms/studio/lib/query';
|
|
8
|
+
import { localeChipLabel } from '@elytracms/studio/lib/locale';
|
|
9
|
+
import { cn } from '@elytracms/studio/lib/utils';
|
|
10
|
+
import { useCollectionView } from '../collection-view/collection-view-context';
|
|
11
|
+
import { toRefArray } from './cell';
|
|
12
|
+
import { richTextToText, wordDiff } from './version-compare-text';
|
|
13
|
+
import { useDocumentEditor } from './document-editor-context';
|
|
14
|
+
import { useDocumentVersionsQuery } from './use-document-versions';
|
|
15
|
+
import { formatVersionTimestamp } from './versions-model';
|
|
16
|
+
/**
|
|
17
|
+
* EC-298 — the version compare view. Renders as the sticky **Compare** content tab
|
|
18
|
+
* (the Versions sidebar pane stays on as the navigator).
|
|
19
|
+
*
|
|
20
|
+
* A calm change-list grouped BY FIELD (a localized field's per-locale variants sit
|
|
21
|
+
* together): before | after across ONE continuous central divider. Text reads as a
|
|
22
|
+
* prose diff (changed words lit in place — rich text via its markdown projection so
|
|
23
|
+
* the content, not just "changed", is visible); assets as thumbnails; relations as
|
|
24
|
+
* titles. Each field/locale (and each block, at property level) restores back to the
|
|
25
|
+
* base version with a single ↩.
|
|
26
|
+
*/
|
|
27
|
+
export function VersionCompare() {
|
|
28
|
+
const source = useCollectionView();
|
|
29
|
+
const { collection, docRef, draft, dirty, viewReadOnly, save, replaceDraft, compare, setCompareBase, setCompareTarget, } = useDocumentEditor();
|
|
30
|
+
const { versionsQuery, versionKey } = useDocumentVersionsQuery(docRef);
|
|
31
|
+
const cache = useOperationCache();
|
|
32
|
+
const [busy, setBusy] = useState(false);
|
|
33
|
+
const view = versionsQuery.data ?? null;
|
|
34
|
+
const versions = useMemo(() => view?.versions ?? [], [view]);
|
|
35
|
+
const baseRecord = compare ? versions.find((v) => v.version === compare.base) : undefined;
|
|
36
|
+
const targetSnapshot = compare?.target === 'draft'
|
|
37
|
+
? draft
|
|
38
|
+
: versions.find((v) => v.version === compare?.target)?.snapshot;
|
|
39
|
+
const diff = useMemo(() => baseRecord && targetSnapshot
|
|
40
|
+
? diffDocuments(collection, baseRecord.snapshot, targetSnapshot)
|
|
41
|
+
: { changes: [], unchangedCount: 0 }, [collection, baseRecord, targetSnapshot]);
|
|
42
|
+
const fieldOrder = useMemo(() => new Map(collection.fields.map((f, i) => [f.name, i])), [collection]);
|
|
43
|
+
const localeRank = useMemo(() => new Map(source.localeConfig.locales.map((l, i) => [l, i])), [source.localeConfig]);
|
|
44
|
+
const groups = useMemo(() => groupByField(diff.changes, fieldOrder, localeRank), [diff.changes, fieldOrder, localeRank]);
|
|
45
|
+
const restore = useCallback(async (scope) => {
|
|
46
|
+
if (!compare)
|
|
47
|
+
return;
|
|
48
|
+
setBusy(true);
|
|
49
|
+
try {
|
|
50
|
+
if (dirty)
|
|
51
|
+
save();
|
|
52
|
+
const restored = await source.versioning?.restore(docRef, compare.base, scope);
|
|
53
|
+
if (restored)
|
|
54
|
+
replaceDraft(restored);
|
|
55
|
+
await cache.invalidate(versionKey);
|
|
56
|
+
}
|
|
57
|
+
finally {
|
|
58
|
+
setBusy(false);
|
|
59
|
+
}
|
|
60
|
+
}, [compare, dirty, save, source, docRef, replaceDraft, cache, versionKey]);
|
|
61
|
+
if (!compare)
|
|
62
|
+
return null;
|
|
63
|
+
if (view === null)
|
|
64
|
+
return _jsx(CompareNotice, { children: "Loading version history\u2026" });
|
|
65
|
+
if (!baseRecord || !targetSnapshot)
|
|
66
|
+
return _jsx(CompareNotice, { children: "That version is no longer available. Pick another to compare." });
|
|
67
|
+
const canRestore = compare.target === 'draft' && !viewReadOnly;
|
|
68
|
+
const targetLabel = compare.target === 'draft' ? 'Current draft' : `v${compare.target}`;
|
|
69
|
+
// Step the base through history, skipping the target so the sides never collapse.
|
|
70
|
+
const baseIndex = versions.findIndex((v) => v.version === compare.base);
|
|
71
|
+
const nextBaseFor = (dir) => {
|
|
72
|
+
let i = baseIndex + dir;
|
|
73
|
+
while (i >= 0 && i < versions.length && versions[i].version === compare.target)
|
|
74
|
+
i += dir;
|
|
75
|
+
return i >= 0 && i < versions.length ? versions[i].version : undefined;
|
|
76
|
+
};
|
|
77
|
+
const newerBase = nextBaseFor(-1); // versions are newest-first
|
|
78
|
+
const olderBase = nextBaseFor(1);
|
|
79
|
+
const baseOptions = versions
|
|
80
|
+
.filter((v) => v.version !== compare.target)
|
|
81
|
+
.map((v) => ({ value: v.version, label: `v${v.version}`, meta: formatVersionTimestamp(v.createdAt) }));
|
|
82
|
+
const targetOptions = [
|
|
83
|
+
{ value: 'draft', label: 'Current draft', meta: dirty ? 'unsaved edits' : undefined },
|
|
84
|
+
...versions
|
|
85
|
+
.filter((v) => v.version !== compare.base)
|
|
86
|
+
.map((v) => ({ value: v.version, label: `v${v.version}`, meta: formatVersionTimestamp(v.createdAt) })),
|
|
87
|
+
];
|
|
88
|
+
return (_jsxs(Stack, { gap: "3", className: "mx-auto w-full max-w-5xl p-4 sm:p-6", children: [_jsxs(Row, { gap: "2", align: "center", wrap: true, className: "rounded-md border border-border/70 bg-muted/30 px-3 py-2", children: [_jsx(VersionMenu, { caption: "Base", current: `v${compare.base}`, meta: formatVersionTimestamp(baseRecord.createdAt), options: baseOptions, onSelect: (v) => setCompareBase(v) }), _jsx(ArrowRight, { className: "size-4 shrink-0 text-muted-foreground", "aria-hidden": true }), _jsx(VersionMenu, { caption: "Compare with", current: targetLabel, options: targetOptions, onSelect: (v) => setCompareTarget(v) }), _jsx(Spacer, {}), _jsxs(Row, { gap: "1", children: [_jsx(Button, { variant: "outline", size: "icon-sm", onClick: () => newerBase !== undefined && setCompareBase(newerBase), disabled: newerBase === undefined, title: "Compare a newer version", "aria-label": "Compare a newer version", children: _jsx(ChevronLeft, {}) }), _jsx(Button, { variant: "outline", size: "icon-sm", onClick: () => olderBase !== undefined && setCompareBase(olderBase), disabled: olderBase === undefined, title: "Compare an older version", "aria-label": "Compare an older version", children: _jsx(ChevronRight, {}) })] }), canRestore ? (_jsxs(Button, { size: "sm", variant: "outline", onClick: () => void restore({ kind: 'all' }), disabled: busy || diff.changes.length === 0, title: `Restore the whole document to v${compare.base}`, children: [_jsx(RotateCcw, {}), " Restore version"] })) : null] }), _jsxs(Text, { size: "xs", tone: "muted", className: "px-0.5", children: [diff.changes.length === 0
|
|
89
|
+
? 'No differences'
|
|
90
|
+
: `${countLabel(diff.changes.length, 'change', 'changes')} · ${diff.unchangedCount} unchanged`, !canRestore && diff.changes.length > 0
|
|
91
|
+
? ' · comparing two past versions — set “Compare with” to Current draft to restore'
|
|
92
|
+
: ''] }), diff.changes.length === 0 ? (_jsx(CompareNotice, { children: "These two versions are identical." })) : (_jsxs("div", { className: "relative", children: [_jsx("div", { className: "pointer-events-none absolute inset-y-0 left-1/2 w-0.5 -translate-x-1/2 bg-border", "aria-hidden": true }), _jsx(TwoCol, { className: "px-0.5 pb-1", before: _jsxs(Text, { size: "xs", tone: "subtle", className: "uppercase tracking-wide", children: ["Before \u00B7 v", compare.base] }), after: _jsxs(Text, { size: "xs", tone: "subtle", className: "uppercase tracking-wide", children: ["After \u00B7 ", targetLabel] }) }), groups.map((group) => (_jsx(FieldGroup, { group: group, canRestore: canRestore, busy: busy, onRestore: restore }, group.field)))] }))] }));
|
|
93
|
+
}
|
|
94
|
+
/** A before | after grid; the continuous centre divider lives behind it (in the
|
|
95
|
+
* parent), so the gap is where the two columns meet. */
|
|
96
|
+
function TwoCol({ before, after, className }) {
|
|
97
|
+
return (_jsxs("div", { className: cn('grid grid-cols-2 gap-x-6', className), children: [_jsx("div", { className: "min-w-0", children: before }), _jsx("div", { className: "min-w-0", children: after })] }));
|
|
98
|
+
}
|
|
99
|
+
function VersionMenu({ caption, current, meta, options, onSelect, }) {
|
|
100
|
+
return (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(Button, { variant: "outline", size: "sm", className: "gap-1.5", children: [_jsx(Text, { size: "xs", tone: "subtle", className: "uppercase tracking-wide", children: caption }), _jsx(Text, { size: "sm", weight: "medium", mono: true, children: current }), meta ? (_jsx(Text, { size: "xs", tone: "muted", className: "hidden sm:inline", children: meta })) : null, _jsx(ChevronDown, { className: "size-3.5 text-muted-foreground" })] }) }), _jsx(DropdownMenuContent, { align: "start", className: "max-h-72 overflow-auto", children: options.map((o) => (_jsxs(DropdownMenuItem, { onClick: () => onSelect(o.value), className: "gap-2", children: [_jsx(Text, { size: "sm", weight: "medium", mono: o.value !== 'draft', children: o.label }), o.meta ? (_jsx(Text, { size: "xs", tone: "muted", children: o.meta })) : null] }, String(o.value)))) })] }));
|
|
101
|
+
}
|
|
102
|
+
function FieldGroup({ group, canRestore, busy, onRestore, }) {
|
|
103
|
+
// A plain (non-localized) field is a single entry — label + value inline. A
|
|
104
|
+
// localized field groups its locale variants under one label.
|
|
105
|
+
const single = group.entries.length === 1 && group.entries[0].locale === undefined;
|
|
106
|
+
if (single) {
|
|
107
|
+
const entry = group.entries[0];
|
|
108
|
+
return (_jsxs("div", { className: "border-t border-border/30 py-3", children: [_jsxs(Row, { gap: "2", align: "center", className: "mb-1.5", children: [_jsx(Text, { size: "sm", weight: "medium", className: "min-w-0 truncate", children: group.label }), _jsx(KindTag, { kind: entry.kind }), _jsx(Spacer, {}), canRestore ? (_jsx(RestoreIcon, { busy: busy, title: `Restore ${group.label}`, onClick: () => onRestore(fieldScope(entry)) })) : null] }), _jsx(EntryBody, { change: entry, canRestore: canRestore, busy: busy, onRestore: onRestore })] }));
|
|
109
|
+
}
|
|
110
|
+
return (_jsxs("div", { className: "border-t border-border/30 py-3", children: [_jsx(Text, { size: "sm", weight: "medium", className: "mb-2 block", children: group.label }), _jsx(Stack, { gap: "3", children: group.entries.map((entry) => (_jsxs("div", { children: [_jsxs(Row, { gap: "2", align: "center", className: "mb-1", children: [entry.locale ? (_jsx(Badge, { variant: "outline", className: "px-1 py-0 text-[0.6rem] uppercase tracking-wide text-muted-foreground", children: localeChipLabel(entry.locale) })) : null, _jsx(KindTag, { kind: entry.kind }), _jsx(Spacer, {}), canRestore ? (_jsx(RestoreIcon, { busy: busy, title: `Restore ${group.label}${entry.locale ? ` (${localeChipLabel(entry.locale)})` : ''}`, onClick: () => onRestore(fieldScope(entry)) })) : null] }), _jsx(EntryBody, { change: entry, canRestore: canRestore, busy: busy, onRestore: onRestore })] }, entry.locale ?? '__base__'))) })] }));
|
|
111
|
+
}
|
|
112
|
+
function EntryBody({ change, canRestore, busy, onRestore, }) {
|
|
113
|
+
const localeScope = change.locale !== undefined ? { locale: change.locale } : {};
|
|
114
|
+
if (change.blocks) {
|
|
115
|
+
return (_jsx(Stack, { gap: "3", className: "mt-1", children: change.blocks.map((block) => (_jsx(BlockSection, { block: block, canRestore: canRestore, busy: busy, onRestore: () => onRestore({ kind: 'block', field: change.field, blockId: block.blockId, ...localeScope }) }, block.blockId))) }));
|
|
116
|
+
}
|
|
117
|
+
if (change.type === 'richText') {
|
|
118
|
+
const before = richTextToText(change.before);
|
|
119
|
+
const after = richTextToText(change.after);
|
|
120
|
+
if (before === after)
|
|
121
|
+
return _jsx(MutedNote, { children: before ? 'Text unchanged — formatting or whitespace differs.' : 'Content changed.' });
|
|
122
|
+
return _jsx(WordDiffCols, { before: before, after: after });
|
|
123
|
+
}
|
|
124
|
+
if (change.kind === 'changed' && typeof change.before === 'string' && typeof change.after === 'string') {
|
|
125
|
+
return _jsx(WordDiffCols, { before: change.before, after: change.after });
|
|
126
|
+
}
|
|
127
|
+
return (_jsx(TwoCol, { before: _jsx(SideValue, { value: change.before, other: change.after, typeHint: change.type }), after: _jsx(SideValue, { value: change.after, other: change.before, typeHint: change.type }) }));
|
|
128
|
+
}
|
|
129
|
+
function BlockSection({ block, canRestore, busy, onRestore, }) {
|
|
130
|
+
const name = block.componentId.split('.').pop() ?? block.componentId;
|
|
131
|
+
return (_jsxs("div", { children: [_jsxs(Row, { gap: "2", align: "center", className: "mb-1", children: [_jsx(Text, { size: "xs", weight: "medium", className: "min-w-0 truncate", children: name }), _jsx(Code, { tone: "muted", children: block.componentId }), _jsx(KindTag, { kind: block.kind }), _jsx(Spacer, {}), canRestore ? _jsx(RestoreIcon, { busy: busy, title: "Restore this block", onClick: onRestore }) : null] }), block.props && block.props.length > 0 ? (_jsx(Stack, { gap: "2", children: block.props.map((prop) => (_jsx(PropRow, { prop: prop }, prop.name))) })) : null, block.slotsChanged ? _jsx(MutedNote, { children: "Nested content changed." }) : null, block.kind !== 'changed' && !block.props?.length ? (_jsx(MutedNote, { children: block.kind === 'added' ? 'Block added.' : 'Block removed.' })) : null] }));
|
|
132
|
+
}
|
|
133
|
+
function PropRow({ prop }) {
|
|
134
|
+
const useWordDiff = prop.kind === 'changed' && typeof prop.before === 'string' && typeof prop.after === 'string';
|
|
135
|
+
return (_jsxs("div", { children: [_jsx(Text, { size: "xs", tone: "subtle", className: "mb-0.5 block uppercase tracking-wide", children: prettyName(prop.name) }), useWordDiff ? (_jsx(WordDiffCols, { before: String(prop.before), after: String(prop.after) })) : (_jsx(TwoCol, { before: _jsx(SideValue, { value: prop.before, other: prop.after }), after: _jsx(SideValue, { value: prop.after, other: prop.before }) }))] }));
|
|
136
|
+
}
|
|
137
|
+
/** One side of a non-text change — the value, or an "empty" placeholder. */
|
|
138
|
+
function SideValue({ value, other, typeHint }) {
|
|
139
|
+
if (value === undefined || value === null || value === '') {
|
|
140
|
+
return (_jsx(Text, { size: "sm", tone: "subtle", className: "italic", children: "empty" }));
|
|
141
|
+
}
|
|
142
|
+
return _jsx(ValueView, { value: value, other: other, typeHint: typeHint });
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Read-only value display, dispatching on the field type where known and on the
|
|
146
|
+
* self-describing value shape otherwise (block props carry no schema) — assets as
|
|
147
|
+
* thumbnails, relations as titles, everything else as calm text.
|
|
148
|
+
*/
|
|
149
|
+
function ValueView({ value, other, typeHint }) {
|
|
150
|
+
if (typeHint === 'asset' || isAssetNode(value))
|
|
151
|
+
return _jsx(AssetValue, { value: value });
|
|
152
|
+
if (typeHint === 'relation' || readDocumentRef(value))
|
|
153
|
+
return _jsx(RelationValue, { value: value, other: other });
|
|
154
|
+
if (typeHint === 'richText')
|
|
155
|
+
return (_jsx(Text, { size: "sm", className: "whitespace-pre-wrap break-words", children: richTextToText(value) || 'rich text' }));
|
|
156
|
+
if (typeHint === 'boolean' || typeof value === 'boolean')
|
|
157
|
+
return (_jsx(Text, { size: "sm", mono: true, children: value === true ? 'Yes' : 'No' }));
|
|
158
|
+
if (typeHint === 'date')
|
|
159
|
+
return (_jsx(Text, { size: "sm", mono: true, children: String(value).slice(0, 10) }));
|
|
160
|
+
if (typeof value === 'object')
|
|
161
|
+
return (_jsx(Text, { size: "sm", tone: "muted", children: Array.isArray(value) ? countLabel(value.length, 'item', 'items') : 'object' }));
|
|
162
|
+
return (_jsx(Text, { size: "sm", className: "whitespace-pre-wrap break-words", children: String(value) }));
|
|
163
|
+
}
|
|
164
|
+
function RelationValue({ value, other }) {
|
|
165
|
+
const source = useCollectionView();
|
|
166
|
+
const refs = toRefArray(value);
|
|
167
|
+
if (refs.length === 0)
|
|
168
|
+
return (_jsx(Text, { size: "sm", className: "whitespace-pre-wrap break-words", children: String(value) }));
|
|
169
|
+
const otherRefs = other !== undefined ? toRefArray(other) : [];
|
|
170
|
+
// Disambiguate a same-titled but different target (the "Ada → Ada" trap): show
|
|
171
|
+
// the id only when the titles would otherwise read identically.
|
|
172
|
+
const label = refs
|
|
173
|
+
.map((ref) => {
|
|
174
|
+
const title = source.titleOf(ref);
|
|
175
|
+
const collision = otherRefs.some((o) => o.id !== ref.id && source.titleOf(o) === title);
|
|
176
|
+
return collision ? `${title} · ${ref.id}` : title;
|
|
177
|
+
})
|
|
178
|
+
.join(', ');
|
|
179
|
+
return _jsx(Text, { size: "sm", children: label });
|
|
180
|
+
}
|
|
181
|
+
function AssetValue({ value }) {
|
|
182
|
+
const ids = (Array.isArray(value) ? value : [value])
|
|
183
|
+
.map(readAssetId)
|
|
184
|
+
.filter((id) => id !== undefined);
|
|
185
|
+
if (ids.length === 0)
|
|
186
|
+
return (_jsx(Text, { size: "sm", className: "whitespace-pre-wrap break-words", children: String(value) }));
|
|
187
|
+
return (_jsx("div", { className: "flex flex-wrap gap-2", children: ids.map((id) => (_jsx(AssetThumb, { id: id }, id))) }));
|
|
188
|
+
}
|
|
189
|
+
/** An asset as a thumbnail (image with a resolvable URL) or a labelled badge —
|
|
190
|
+
* the visual diff for a cover-image change, not two identical filenames. */
|
|
191
|
+
function AssetThumb({ id }) {
|
|
192
|
+
const source = useCollectionView();
|
|
193
|
+
const assetCollectionId = source.assetCollectionId ?? 'asset';
|
|
194
|
+
const record = source.resolveAsset?.(id);
|
|
195
|
+
const label = record?.title ?? record?.filename ?? source.titleOf({ collection: assetCollectionId, id });
|
|
196
|
+
const isImage = record?.contentType?.startsWith('image/') ?? false;
|
|
197
|
+
const url = record?.url;
|
|
198
|
+
if (isImage && url && url.startsWith('http')) {
|
|
199
|
+
return (_jsxs("span", { className: "inline-flex min-w-0 items-center gap-2", children: [_jsx("img", { src: url, alt: record?.alt ?? label, loading: "lazy", className: "size-12 shrink-0 rounded border border-border bg-muted object-cover" }), _jsx(Text, { size: "xs", tone: "muted", className: "min-w-0 truncate", children: label })] }));
|
|
200
|
+
}
|
|
201
|
+
return (_jsxs(Badge, { variant: "outline", children: [_jsx(ImageIcon, { className: "size-3" }), " ", label] }));
|
|
202
|
+
}
|
|
203
|
+
// --- Prose word diff --------------------------------------------------------------
|
|
204
|
+
/** Prose-diff across the central divider, both sides from one LCS. */
|
|
205
|
+
function WordDiffCols({ before, after }) {
|
|
206
|
+
const wd = useMemo(() => wordDiff(before, after), [before, after]);
|
|
207
|
+
return (_jsx(TwoCol, { before: _jsx(WordTokens, { tokens: wd.before, side: "before" }), after: _jsx(WordTokens, { tokens: wd.after, side: "after" }) }));
|
|
208
|
+
}
|
|
209
|
+
function WordTokens({ tokens, side }) {
|
|
210
|
+
return (_jsx(Text, { size: "sm", className: "whitespace-pre-wrap break-words leading-relaxed", children: tokens.map((token, i) => token.changed ? (_jsx("mark", { className: cn('rounded-sm px-0.5', side === 'before'
|
|
211
|
+
? 'bg-destructive/15 text-destructive line-through decoration-destructive/40'
|
|
212
|
+
: 'bg-success/20 text-foreground'), children: token.text }, i)) : (_jsx("span", { className: "text-muted-foreground", children: token.text }, i))) }));
|
|
213
|
+
}
|
|
214
|
+
// --- Small shared bits ------------------------------------------------------------
|
|
215
|
+
/** A quiet "· new" / "· removed" tag; nothing for a plain change (implicit). */
|
|
216
|
+
function KindTag({ kind }) {
|
|
217
|
+
if (kind === 'changed')
|
|
218
|
+
return null;
|
|
219
|
+
return (_jsxs(Text, { size: "xs", tone: "subtle", children: ["\u00B7 ", kind === 'added' ? 'new' : 'removed'] }));
|
|
220
|
+
}
|
|
221
|
+
function RestoreIcon({ busy, title, onClick }) {
|
|
222
|
+
return (_jsx(Button, { variant: "ghost", size: "icon-sm", disabled: busy, title: title, "aria-label": title, onClick: onClick, children: _jsx(RotateCcw, {}) }));
|
|
223
|
+
}
|
|
224
|
+
function MutedNote({ children }) {
|
|
225
|
+
return (_jsx(Text, { size: "sm", tone: "muted", className: "italic", children: children }));
|
|
226
|
+
}
|
|
227
|
+
function CompareNotice({ children }) {
|
|
228
|
+
return (_jsx("div", { className: "mx-auto w-full max-w-5xl p-4 sm:p-6", children: _jsx("div", { className: "rounded-md border border-dashed border-border/70 px-4 py-8 text-center", children: _jsx(Text, { size: "sm", tone: "muted", children: children }) }) }));
|
|
229
|
+
}
|
|
230
|
+
function fieldScope(change) {
|
|
231
|
+
return { kind: 'field', field: change.field, ...(change.locale !== undefined ? { locale: change.locale } : {}) };
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Group changes BY FIELD (a localized field's per-locale variants together),
|
|
235
|
+
* fields in schema order, locales in project order (base bucket first).
|
|
236
|
+
*/
|
|
237
|
+
function groupByField(changes, fieldOrder, localeRank) {
|
|
238
|
+
const map = new Map();
|
|
239
|
+
for (const change of changes) {
|
|
240
|
+
let group = map.get(change.field);
|
|
241
|
+
if (!group) {
|
|
242
|
+
group = { field: change.field, label: change.label, type: change.type, entries: [] };
|
|
243
|
+
map.set(change.field, group);
|
|
244
|
+
}
|
|
245
|
+
group.entries.push(change);
|
|
246
|
+
}
|
|
247
|
+
const groups = [...map.values()];
|
|
248
|
+
groups.sort((a, b) => (fieldOrder.get(a.field) ?? Number.MAX_SAFE_INTEGER) - (fieldOrder.get(b.field) ?? Number.MAX_SAFE_INTEGER));
|
|
249
|
+
for (const group of groups) {
|
|
250
|
+
group.entries.sort((a, b) => localeCompare(a.locale, b.locale, localeRank));
|
|
251
|
+
}
|
|
252
|
+
return groups;
|
|
253
|
+
}
|
|
254
|
+
function localeCompare(a, b, rank) {
|
|
255
|
+
if (a === b)
|
|
256
|
+
return 0;
|
|
257
|
+
if (a === undefined)
|
|
258
|
+
return -1; // base bucket first
|
|
259
|
+
if (b === undefined)
|
|
260
|
+
return 1;
|
|
261
|
+
return (rank.get(a) ?? 99) - (rank.get(b) ?? 99);
|
|
262
|
+
}
|
|
263
|
+
function countLabel(n, one, many) {
|
|
264
|
+
return `${n} ${n === 1 ? one : many}`;
|
|
265
|
+
}
|
|
266
|
+
/** camelCase / snake prop name → a readable label. */
|
|
267
|
+
function prettyName(name) {
|
|
268
|
+
const spaced = name.replace(/[_-]+/g, ' ').replace(/([a-z0-9])([A-Z])/g, '$1 $2');
|
|
269
|
+
return spaced.charAt(0).toUpperCase() + spaced.slice(1);
|
|
270
|
+
}
|
|
271
|
+
//# sourceMappingURL=version-compare.js.map
|