@elytracms/studio 0.0.42 → 0.0.44

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.
Files changed (229) hide show
  1. package/dist/components/app-views/studio-config.d.ts +12 -0
  2. package/dist/components/app-views/studio-config.js.map +1 -1
  3. package/dist/components/assets/asset-detail.js +1 -1
  4. package/dist/components/assets/asset-detail.js.map +1 -1
  5. package/dist/components/assets/assets-collection-view-source.d.ts +13 -1
  6. package/dist/components/assets/assets-collection-view-source.js +42 -51
  7. package/dist/components/assets/assets-collection-view-source.js.map +1 -1
  8. package/dist/components/assets/assets-module.js +19 -6
  9. package/dist/components/assets/assets-module.js.map +1 -1
  10. package/dist/components/assets/assets-store.d.ts +2 -1
  11. package/dist/components/assets/assets-store.js +4 -8
  12. package/dist/components/assets/assets-store.js.map +1 -1
  13. package/dist/components/assets/assets-view-preference.d.ts +0 -8
  14. package/dist/components/assets/assets-view-preference.js +12 -13
  15. package/dist/components/assets/assets-view-preference.js.map +1 -1
  16. package/dist/components/auth-context.d.ts +2 -1
  17. package/dist/components/auth-context.js +6 -8
  18. package/dist/components/auth-context.js.map +1 -1
  19. package/dist/components/builder/builder-store.d.ts +2 -2
  20. package/dist/components/builder/builder-store.js +4 -8
  21. package/dist/components/builder/builder-store.js.map +1 -1
  22. package/dist/components/cms/bulk-actions.js +2 -1
  23. package/dist/components/cms/bulk-actions.js.map +1 -1
  24. package/dist/components/cms/cell-edit.js +2 -1
  25. package/dist/components/cms/cell-edit.js.map +1 -1
  26. package/dist/components/cms/cms-composition-validators.d.ts +25 -0
  27. package/dist/components/cms/cms-composition-validators.js +75 -0
  28. package/dist/components/cms/cms-composition-validators.js.map +1 -0
  29. package/dist/components/cms/cms-labels.d.ts +15 -0
  30. package/dist/components/cms/cms-labels.js +27 -0
  31. package/dist/components/cms/cms-labels.js.map +1 -0
  32. package/dist/components/cms/cms-store.d.ts +23 -10
  33. package/dist/components/cms/cms-store.js +87 -165
  34. package/dist/components/cms/cms-store.js.map +1 -1
  35. package/dist/components/cms/collection-table.js +1 -1
  36. package/dist/components/cms/collection-table.js.map +1 -1
  37. package/dist/components/cms/detail-editor.js +111 -348
  38. package/dist/components/cms/detail-editor.js.map +1 -1
  39. package/dist/components/cms/discard-draft.d.ts +18 -0
  40. package/dist/components/cms/discard-draft.js +18 -0
  41. package/dist/components/cms/discard-draft.js.map +1 -1
  42. package/dist/components/cms/document-editor-context.d.ts +2 -1
  43. package/dist/components/cms/document-editor-context.js +4 -9
  44. package/dist/components/cms/document-editor-context.js.map +1 -1
  45. package/dist/components/cms/document-ref-utils.d.ts +33 -0
  46. package/dist/components/cms/document-ref-utils.js +47 -0
  47. package/dist/components/cms/document-ref-utils.js.map +1 -0
  48. package/dist/components/cms/document-staging.d.ts +8 -3
  49. package/dist/components/cms/document-staging.js +81 -21
  50. package/dist/components/cms/document-staging.js.map +1 -1
  51. package/dist/components/cms/field-control.js +1 -1
  52. package/dist/components/cms/field-control.js.map +1 -1
  53. package/dist/components/cms/issues-pane.js +1 -1
  54. package/dist/components/cms/issues-pane.js.map +1 -1
  55. package/dist/components/cms/rich-text/block-commands.d.ts +36 -0
  56. package/dist/components/cms/rich-text/block-commands.js +47 -0
  57. package/dist/components/cms/rich-text/block-commands.js.map +1 -0
  58. package/dist/components/cms/rich-text/slash-command.js +25 -8
  59. package/dist/components/cms/rich-text/slash-command.js.map +1 -1
  60. package/dist/components/cms/rich-text/toolbar.js +3 -2
  61. package/dist/components/cms/rich-text/toolbar.js.map +1 -1
  62. package/dist/components/cms/use-compare-selection.d.ts +27 -0
  63. package/dist/components/cms/use-compare-selection.js +29 -0
  64. package/dist/components/cms/use-compare-selection.js.map +1 -0
  65. package/dist/components/cms/use-discard-controller.d.ts +44 -0
  66. package/dist/components/cms/use-discard-controller.js +51 -0
  67. package/dist/components/cms/use-discard-controller.js.map +1 -0
  68. package/dist/components/cms/use-embed-inspector-host.d.ts +12 -0
  69. package/dist/components/cms/use-embed-inspector-host.js +17 -0
  70. package/dist/components/cms/use-embed-inspector-host.js.map +1 -0
  71. package/dist/components/cms/use-field-focus.d.ts +38 -0
  72. package/dist/components/cms/use-field-focus.js +70 -0
  73. package/dist/components/cms/use-field-focus.js.map +1 -0
  74. package/dist/components/cms/use-live-url.d.ts +41 -0
  75. package/dist/components/cms/use-live-url.js +55 -0
  76. package/dist/components/cms/use-live-url.js.map +1 -0
  77. package/dist/components/cms/use-publish-controller.d.ts +49 -0
  78. package/dist/components/cms/use-publish-controller.js +102 -0
  79. package/dist/components/cms/use-publish-controller.js.map +1 -0
  80. package/dist/components/cms/use-published-perspective.d.ts +67 -0
  81. package/dist/components/cms/use-published-perspective.js +67 -0
  82. package/dist/components/cms/use-published-perspective.js.map +1 -0
  83. package/dist/components/cms/use-restore-version.d.ts +57 -0
  84. package/dist/components/cms/use-restore-version.js +66 -0
  85. package/dist/components/cms/use-restore-version.js.map +1 -0
  86. package/dist/components/cms/use-save-mode.d.ts +35 -0
  87. package/dist/components/cms/use-save-mode.js +54 -0
  88. package/dist/components/cms/use-save-mode.js.map +1 -0
  89. package/dist/components/cms/version-compare.js +14 -17
  90. package/dist/components/cms/version-compare.js.map +1 -1
  91. package/dist/components/cms/versions-model.d.ts +31 -0
  92. package/dist/components/cms/versions-model.js +30 -0
  93. package/dist/components/cms/versions-model.js.map +1 -1
  94. package/dist/components/cms/versions-pane.js +21 -19
  95. package/dist/components/cms/versions-pane.js.map +1 -1
  96. package/dist/components/cms/view-bar.js +9 -6
  97. package/dist/components/cms/view-bar.js.map +1 -1
  98. package/dist/components/collab/collab-context.d.ts +2 -1
  99. package/dist/components/collab/collab-context.js +4 -8
  100. package/dist/components/collab/collab-context.js.map +1 -1
  101. package/dist/components/collection-view/collection-view-context.d.ts +9 -1
  102. package/dist/components/collection-view/collection-view-context.js +4 -9
  103. package/dist/components/collection-view/collection-view-context.js.map +1 -1
  104. package/dist/components/components/component-detail.js +2 -5
  105. package/dist/components/components/component-detail.js.map +1 -1
  106. package/dist/components/components/component-preview.js +8 -4
  107. package/dist/components/components/component-preview.js.map +1 -1
  108. package/dist/components/components/registry-browser.js +24 -5
  109. package/dist/components/components/registry-browser.js.map +1 -1
  110. package/dist/components/components/transfer-dialog.d.ts +21 -6
  111. package/dist/components/components/transfer-dialog.js +120 -31
  112. package/dist/components/components/transfer-dialog.js.map +1 -1
  113. package/dist/components/components/transfer.d.ts +74 -8
  114. package/dist/components/components/transfer.js +87 -2
  115. package/dist/components/components/transfer.js.map +1 -1
  116. package/dist/components/delivery-health-banner.d.ts +12 -6
  117. package/dist/components/delivery-health-banner.js +27 -29
  118. package/dist/components/delivery-health-banner.js.map +1 -1
  119. package/dist/components/dev-login-badge.d.ts +0 -2
  120. package/dist/components/dev-login-badge.js +1 -3
  121. package/dist/components/dev-login-badge.js.map +1 -1
  122. package/dist/components/playground-badge.d.ts +0 -2
  123. package/dist/components/playground-badge.js +0 -2
  124. package/dist/components/playground-badge.js.map +1 -1
  125. package/dist/components/project-list.js +2 -6
  126. package/dist/components/project-list.js.map +1 -1
  127. package/dist/components/project-session.d.ts +6 -2
  128. package/dist/components/project-session.js +32 -83
  129. package/dist/components/project-session.js.map +1 -1
  130. package/dist/components/project-switcher.js +2 -6
  131. package/dist/components/project-switcher.js.map +1 -1
  132. package/dist/components/rail-store-status.d.ts +7 -5
  133. package/dist/components/rail-store-status.js +12 -9
  134. package/dist/components/rail-store-status.js.map +1 -1
  135. package/dist/components/rail-view-source.d.ts +111 -0
  136. package/dist/components/rail-view-source.js +139 -0
  137. package/dist/components/rail-view-source.js.map +1 -0
  138. package/dist/components/redirects/redirects-collection-view-source.d.ts +13 -1
  139. package/dist/components/redirects/redirects-collection-view-source.js +42 -70
  140. package/dist/components/redirects/redirects-collection-view-source.js.map +1 -1
  141. package/dist/components/redirects/redirects-store.d.ts +2 -1
  142. package/dist/components/redirects/redirects-store.js +4 -8
  143. package/dist/components/redirects/redirects-store.js.map +1 -1
  144. package/dist/components/section-state.d.ts +12 -3
  145. package/dist/components/section-state.js +8 -4
  146. package/dist/components/section-state.js.map +1 -1
  147. package/dist/components/studio-context.d.ts +2 -1
  148. package/dist/components/studio-context.js +4 -8
  149. package/dist/components/studio-context.js.map +1 -1
  150. package/dist/components/studio-shell.js +1 -1
  151. package/dist/components/studio-shell.js.map +1 -1
  152. package/dist/components/theme-context.d.ts +2 -1
  153. package/dist/components/theme-context.js +4 -8
  154. package/dist/components/theme-context.js.map +1 -1
  155. package/dist/components/use-hydration-phase.d.ts +65 -0
  156. package/dist/components/use-hydration-phase.js +64 -0
  157. package/dist/components/use-hydration-phase.js.map +1 -0
  158. package/dist/components/users/users-collection-view-source.d.ts +13 -1
  159. package/dist/components/users/users-collection-view-source.js +60 -79
  160. package/dist/components/users/users-collection-view-source.js.map +1 -1
  161. package/dist/components/users/users-store.d.ts +2 -1
  162. package/dist/components/users/users-store.js +4 -8
  163. package/dist/components/users/users-store.js.map +1 -1
  164. package/dist/components/workspace-projects.d.ts +14 -0
  165. package/dist/components/workspace-projects.js +21 -0
  166. package/dist/components/workspace-projects.js.map +1 -1
  167. package/dist/lib/ai/anthropic-provider.d.ts +29 -0
  168. package/dist/lib/ai/anthropic-provider.js +59 -0
  169. package/dist/lib/ai/anthropic-provider.js.map +1 -0
  170. package/dist/lib/ai/apply-content.d.ts +9 -0
  171. package/dist/lib/ai/apply-content.js +170 -0
  172. package/dist/lib/ai/apply-content.js.map +1 -0
  173. package/dist/lib/ai/config.d.ts +58 -0
  174. package/dist/lib/ai/config.js +134 -0
  175. package/dist/lib/ai/config.js.map +1 -0
  176. package/dist/lib/ai/fake-provider.d.ts +34 -0
  177. package/dist/lib/ai/fake-provider.js +31 -0
  178. package/dist/lib/ai/fake-provider.js.map +1 -0
  179. package/dist/lib/ai/index.d.ts +22 -0
  180. package/dist/lib/ai/index.js +23 -0
  181. package/dist/lib/ai/index.js.map +1 -0
  182. package/dist/lib/ai/knowledge.d.ts +39 -0
  183. package/dist/lib/ai/knowledge.js +60 -0
  184. package/dist/lib/ai/knowledge.js.map +1 -0
  185. package/dist/lib/ai/provenance.d.ts +20 -0
  186. package/dist/lib/ai/provenance.js +15 -0
  187. package/dist/lib/ai/provenance.js.map +1 -0
  188. package/dist/lib/ai/route.d.ts +24 -0
  189. package/dist/lib/ai/route.js +90 -0
  190. package/dist/lib/ai/route.js.map +1 -0
  191. package/dist/lib/ai/shape.d.ts +35 -0
  192. package/dist/lib/ai/shape.js +113 -0
  193. package/dist/lib/ai/shape.js.map +1 -0
  194. package/dist/lib/ai/types.d.ts +227 -0
  195. package/dist/lib/ai/types.js +2 -0
  196. package/dist/lib/ai/types.js.map +1 -0
  197. package/dist/lib/data/data-store.d.ts +2 -1
  198. package/dist/lib/data/data-store.js +4 -8
  199. package/dist/lib/data/data-store.js.map +1 -1
  200. package/dist/lib/delivery/instant-publish.d.ts +9 -13
  201. package/dist/lib/delivery/instant-publish.js +0 -7
  202. package/dist/lib/delivery/instant-publish.js.map +1 -1
  203. package/dist/lib/diff-view-preference.js +4 -4
  204. package/dist/lib/diff-view-preference.js.map +1 -1
  205. package/dist/lib/i18n/messages.en.js +5 -2
  206. package/dist/lib/i18n/messages.en.js.map +1 -1
  207. package/dist/lib/local-preference.d.ts +37 -0
  208. package/dist/lib/local-preference.js +36 -0
  209. package/dist/lib/local-preference.js.map +1 -1
  210. package/dist/lib/persistence/convex-adapter.d.ts +3 -1
  211. package/dist/lib/persistence/convex-adapter.js +20 -0
  212. package/dist/lib/persistence/convex-adapter.js.map +1 -1
  213. package/dist/lib/rail.js +7 -6
  214. package/dist/lib/rail.js.map +1 -1
  215. package/dist/lib/required-context.d.ts +24 -0
  216. package/dist/lib/required-context.js +28 -0
  217. package/dist/lib/required-context.js.map +1 -0
  218. package/dist/lib/theme.js +11 -4
  219. package/dist/lib/theme.js.map +1 -1
  220. package/dist/lib/workspace/index.d.ts +9 -0
  221. package/dist/lib/workspace/index.js +11 -0
  222. package/dist/lib/workspace/index.js.map +1 -1
  223. package/dist/ui/index.d.ts +1 -0
  224. package/dist/ui/index.js +1 -0
  225. package/dist/ui/index.js.map +1 -1
  226. package/dist/ui/segmented-control.d.ts +46 -0
  227. package/dist/ui/segmented-control.js +13 -0
  228. package/dist/ui/segmented-control.js.map +1 -0
  229. package/package.json +8 -2
@@ -0,0 +1,57 @@
1
+ import type { QueryKey } from '@tanstack/react-query';
2
+ import type { CmsDocument, DocumentRef, RestoreScope } from '@elytracms/core/cms-core';
3
+ import type { CollectionViewSource } from '@elytracms/studio/lib/collection-view/collection-view-source';
4
+ /**
5
+ * EC-325 — the version-restore sequence, single-sourced. The Versions pane's
6
+ * per-version restore, the Compare view's scoped/whole restore, and the header's
7
+ * "Discard changes" (a whole-document restore of the pinned published version)
8
+ * all ran their OWN copy of the identical four steps:
9
+ *
10
+ * 1. flush a dirty in-memory buffer FIRST (`flushDirtyDraft`) — `documents.restore`
11
+ * snapshots whatever is currently PERSISTED as its pre-restore version, so an
12
+ * edit still in the autosave debounce window must reach the server before the
13
+ * restore reads it, or it is silently unrecoverable (the undo-guarantee gap
14
+ * the Discard path used to have);
15
+ * 2. `versioning.restore(ref, version, scope?)` — scope absent ⇒ whole document
16
+ * (`documents.restore` reads `scope ?? { kind: 'all' }`, so an omitted scope
17
+ * and `{ kind: 'all' }` are the same restore);
18
+ * 3. adopt the restored draft into the open editor (`replaceDraft`);
19
+ * 4. invalidate the shared version-history cache key so the history re-reads.
20
+ *
21
+ * The pure {@link performRestore} pins that ordering (unit-tested with spies); the
22
+ * {@link useRestoreVersion} hook wraps it with the per-call-site busy flag and the
23
+ * operations cache. The differing signatures reconcile to `(version, scope?)`:
24
+ * whole-document callers pass no scope, the Compare view passes its `RestoreScope`,
25
+ * and Discard passes the pinned published version with `{ kind: 'all' }`.
26
+ */
27
+ export declare function performRestore(input: {
28
+ dirty: boolean;
29
+ save: () => void;
30
+ restore: (version: number, scope?: RestoreScope) => Promise<CmsDocument | null>;
31
+ replaceDraft: (doc: CmsDocument) => void;
32
+ invalidate: () => Promise<void>;
33
+ version: number;
34
+ scope?: RestoreScope;
35
+ }): Promise<void>;
36
+ /** Dependencies the restore sequence reads from its call site's editor state. */
37
+ export interface RestoreVersionDeps {
38
+ source: CollectionViewSource;
39
+ docRef: DocumentRef;
40
+ /** The shared version-history cache key ({@link documentVersionsKey}). */
41
+ versionKey: QueryKey;
42
+ /** Is the in-memory draft dirty (an unsaved buffer to flush first)? */
43
+ dirty: boolean;
44
+ /** Persist the current draft buffer. */
45
+ save: () => void;
46
+ /** Adopt the restored snapshot as the open editor's draft. */
47
+ replaceDraft: (doc: CmsDocument) => void;
48
+ }
49
+ /**
50
+ * The shared restore action + its in-flight flag. `restore(version, scope?)`
51
+ * runs the {@link performRestore} sequence; `busy` gates the calling surface's
52
+ * restore/discard controls while it round-trips.
53
+ */
54
+ export declare function useRestoreVersion(deps: RestoreVersionDeps): {
55
+ restore: (version: number, scope?: RestoreScope) => Promise<void>;
56
+ busy: boolean;
57
+ };
@@ -0,0 +1,66 @@
1
+ import { useCallback, useState } from 'react';
2
+ import { useOperationCache } from '@elytracms/studio/lib/query';
3
+ import { flushDirtyDraft } from './discard-draft';
4
+ /**
5
+ * EC-325 — the version-restore sequence, single-sourced. The Versions pane's
6
+ * per-version restore, the Compare view's scoped/whole restore, and the header's
7
+ * "Discard changes" (a whole-document restore of the pinned published version)
8
+ * all ran their OWN copy of the identical four steps:
9
+ *
10
+ * 1. flush a dirty in-memory buffer FIRST (`flushDirtyDraft`) — `documents.restore`
11
+ * snapshots whatever is currently PERSISTED as its pre-restore version, so an
12
+ * edit still in the autosave debounce window must reach the server before the
13
+ * restore reads it, or it is silently unrecoverable (the undo-guarantee gap
14
+ * the Discard path used to have);
15
+ * 2. `versioning.restore(ref, version, scope?)` — scope absent ⇒ whole document
16
+ * (`documents.restore` reads `scope ?? { kind: 'all' }`, so an omitted scope
17
+ * and `{ kind: 'all' }` are the same restore);
18
+ * 3. adopt the restored draft into the open editor (`replaceDraft`);
19
+ * 4. invalidate the shared version-history cache key so the history re-reads.
20
+ *
21
+ * The pure {@link performRestore} pins that ordering (unit-tested with spies); the
22
+ * {@link useRestoreVersion} hook wraps it with the per-call-site busy flag and the
23
+ * operations cache. The differing signatures reconcile to `(version, scope?)`:
24
+ * whole-document callers pass no scope, the Compare view passes its `RestoreScope`,
25
+ * and Discard passes the pinned published version with `{ kind: 'all' }`.
26
+ */
27
+ export async function performRestore(input) {
28
+ // Step 1 — flush before the restore reads the persisted document.
29
+ flushDirtyDraft({ dirty: input.dirty, save: input.save });
30
+ // Step 2 — the restore itself (scope absent ⇒ whole document).
31
+ const restored = await input.restore(input.version, input.scope);
32
+ // Step 3 — the open editor adopts the restored snapshot as its new draft.
33
+ if (restored)
34
+ input.replaceDraft(restored);
35
+ // Step 4 — re-read the recorded history (the new pre-restore version appears).
36
+ await input.invalidate();
37
+ }
38
+ /**
39
+ * The shared restore action + its in-flight flag. `restore(version, scope?)`
40
+ * runs the {@link performRestore} sequence; `busy` gates the calling surface's
41
+ * restore/discard controls while it round-trips.
42
+ */
43
+ export function useRestoreVersion(deps) {
44
+ const { source, docRef, versionKey, dirty, save, replaceDraft } = deps;
45
+ const cache = useOperationCache();
46
+ const [busy, setBusy] = useState(false);
47
+ const restore = useCallback(async (version, scope) => {
48
+ setBusy(true);
49
+ try {
50
+ await performRestore({
51
+ dirty,
52
+ save,
53
+ restore: async (v, s) => (await source.versioning?.restore(docRef, v, s)) ?? null,
54
+ replaceDraft,
55
+ invalidate: () => cache.invalidate(versionKey),
56
+ version,
57
+ scope,
58
+ });
59
+ }
60
+ finally {
61
+ setBusy(false);
62
+ }
63
+ }, [source, docRef, versionKey, dirty, save, replaceDraft, cache]);
64
+ return { restore, busy };
65
+ }
66
+ //# sourceMappingURL=use-restore-version.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-restore-version.js","sourceRoot":"","sources":["../../../src/components/cms/use-restore-version.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAG7C,OAAO,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAA;AAE/D,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AAEjD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,KAQpC;IACC,kEAAkE;IAClE,eAAe,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,CAAC,CAAA;IACzD,+DAA+D;IAC/D,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,KAAK,CAAC,CAAA;IAChE,0EAA0E;IAC1E,IAAI,QAAQ;QAAE,KAAK,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAA;IAC1C,+EAA+E;IAC/E,MAAM,KAAK,CAAC,UAAU,EAAE,CAAA;AAC1B,CAAC;AAgBD;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAAC,IAAwB;IAIxD,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,IAAI,EAAE,YAAY,EAAE,GAAG,IAAI,CAAA;IACtE,MAAM,KAAK,GAAG,iBAAiB,EAAE,CAAA;IACjC,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAA;IACvC,MAAM,OAAO,GAAG,WAAW,CACzB,KAAK,EAAE,OAAe,EAAE,KAAoB,EAAE,EAAE;QAC9C,OAAO,CAAC,IAAI,CAAC,CAAA;QACb,IAAI,CAAC;YACH,MAAM,cAAc,CAAC;gBACnB,KAAK;gBACL,IAAI;gBACJ,OAAO,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,MAAM,MAAM,CAAC,UAAU,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,IAAI;gBACjF,YAAY;gBACZ,UAAU,EAAE,GAAG,EAAE,CAAC,KAAK,CAAC,UAAU,CAAC,UAAU,CAAC;gBAC9C,OAAO;gBACP,KAAK;aACN,CAAC,CAAA;QACJ,CAAC;gBAAS,CAAC;YACT,OAAO,CAAC,KAAK,CAAC,CAAA;QAChB,CAAC;IACH,CAAC,EACD,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,IAAI,EAAE,YAAY,EAAE,KAAK,CAAC,CAC/D,CAAA;IACD,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAA;AAC1B,CAAC","sourcesContent":["import { useCallback, useState } from 'react'\nimport type { QueryKey } from '@tanstack/react-query'\nimport type { CmsDocument, DocumentRef, RestoreScope } from '@elytracms/core/cms-core'\nimport { useOperationCache } from '@elytracms/studio/lib/query'\nimport type { CollectionViewSource } from '@elytracms/studio/lib/collection-view/collection-view-source'\nimport { flushDirtyDraft } from './discard-draft'\n\n/**\n * EC-325 — the version-restore sequence, single-sourced. The Versions pane's\n * per-version restore, the Compare view's scoped/whole restore, and the header's\n * \"Discard changes\" (a whole-document restore of the pinned published version)\n * all ran their OWN copy of the identical four steps:\n *\n * 1. flush a dirty in-memory buffer FIRST (`flushDirtyDraft`) — `documents.restore`\n * snapshots whatever is currently PERSISTED as its pre-restore version, so an\n * edit still in the autosave debounce window must reach the server before the\n * restore reads it, or it is silently unrecoverable (the undo-guarantee gap\n * the Discard path used to have);\n * 2. `versioning.restore(ref, version, scope?)` — scope absent ⇒ whole document\n * (`documents.restore` reads `scope ?? { kind: 'all' }`, so an omitted scope\n * and `{ kind: 'all' }` are the same restore);\n * 3. adopt the restored draft into the open editor (`replaceDraft`);\n * 4. invalidate the shared version-history cache key so the history re-reads.\n *\n * The pure {@link performRestore} pins that ordering (unit-tested with spies); the\n * {@link useRestoreVersion} hook wraps it with the per-call-site busy flag and the\n * operations cache. The differing signatures reconcile to `(version, scope?)`:\n * whole-document callers pass no scope, the Compare view passes its `RestoreScope`,\n * and Discard passes the pinned published version with `{ kind: 'all' }`.\n */\nexport async function performRestore(input: {\n dirty: boolean\n save: () => void\n restore: (version: number, scope?: RestoreScope) => Promise<CmsDocument | null>\n replaceDraft: (doc: CmsDocument) => void\n invalidate: () => Promise<void>\n version: number\n scope?: RestoreScope\n}): Promise<void> {\n // Step 1 — flush before the restore reads the persisted document.\n flushDirtyDraft({ dirty: input.dirty, save: input.save })\n // Step 2 — the restore itself (scope absent ⇒ whole document).\n const restored = await input.restore(input.version, input.scope)\n // Step 3 — the open editor adopts the restored snapshot as its new draft.\n if (restored) input.replaceDraft(restored)\n // Step 4 — re-read the recorded history (the new pre-restore version appears).\n await input.invalidate()\n}\n\n/** Dependencies the restore sequence reads from its call site's editor state. */\nexport interface RestoreVersionDeps {\n source: CollectionViewSource\n docRef: DocumentRef\n /** The shared version-history cache key ({@link documentVersionsKey}). */\n versionKey: QueryKey\n /** Is the in-memory draft dirty (an unsaved buffer to flush first)? */\n dirty: boolean\n /** Persist the current draft buffer. */\n save: () => void\n /** Adopt the restored snapshot as the open editor's draft. */\n replaceDraft: (doc: CmsDocument) => void\n}\n\n/**\n * The shared restore action + its in-flight flag. `restore(version, scope?)`\n * runs the {@link performRestore} sequence; `busy` gates the calling surface's\n * restore/discard controls while it round-trips.\n */\nexport function useRestoreVersion(deps: RestoreVersionDeps): {\n restore: (version: number, scope?: RestoreScope) => Promise<void>\n busy: boolean\n} {\n const { source, docRef, versionKey, dirty, save, replaceDraft } = deps\n const cache = useOperationCache()\n const [busy, setBusy] = useState(false)\n const restore = useCallback(\n async (version: number, scope?: RestoreScope) => {\n setBusy(true)\n try {\n await performRestore({\n dirty,\n save,\n restore: async (v, s) => (await source.versioning?.restore(docRef, v, s)) ?? null,\n replaceDraft,\n invalidate: () => cache.invalidate(versionKey),\n version,\n scope,\n })\n } finally {\n setBusy(false)\n }\n },\n [source, docRef, versionKey, dirty, save, replaceDraft, cache],\n )\n return { restore, busy }\n}\n"]}
@@ -0,0 +1,35 @@
1
+ import type { CmsDocument } from '@elytracms/core/cms-core';
2
+ import type { CollectionViewSource } from '@elytracms/studio/lib/collection-view/collection-view-source';
3
+ /**
4
+ * EC-225 / EC-240 — the two persistence disciplines the detail editor supports,
5
+ * as one pure decision so the autosave scheduler and the unmount flush can never
6
+ * diverge on WHEN a dirty draft is persisted silently:
7
+ *
8
+ * - A **publishing** rail AUTOSAVES: the draft persists 1s after the last edit and
9
+ * publish is the deliberate commit — so a dirty draft is flushed both on the
10
+ * debounce timer and on unmount (navigating away inside the debounce window).
11
+ * - A **non-publishing** rail (users, EC-240) SAVES EXPLICITLY: edits stay in the
12
+ * draft until the Save button, and an `UnsavedChangesGuard` protects navigation —
13
+ * so it NEVER persists silently (returns false here).
14
+ */
15
+ export declare function shouldPersistDraft(input: {
16
+ explicitSave: boolean;
17
+ dirty: boolean;
18
+ }): boolean;
19
+ /**
20
+ * EC-225 autosave · EC-240 explicit save. Owns the debounced autosave timer and
21
+ * the unmount flush for a publishing rail; a non-publishing rail persists only via
22
+ * its Save button (the caller's `UnsavedChangesGuard` covers navigation). Refs let
23
+ * the unmount flush read the latest values without re-subscribing.
24
+ *
25
+ * Returns `explicitSave` — the caller renders the Save button + guard (explicit)
26
+ * or the autosave status text (autosave) from it.
27
+ */
28
+ export declare function useSaveMode(input: {
29
+ canPublish: boolean;
30
+ dirty: boolean;
31
+ draft: CmsDocument;
32
+ source: CollectionViewSource;
33
+ }): {
34
+ explicitSave: boolean;
35
+ };
@@ -0,0 +1,54 @@
1
+ import { useEffect, useRef } from 'react';
2
+ /**
3
+ * EC-225 / EC-240 — the two persistence disciplines the detail editor supports,
4
+ * as one pure decision so the autosave scheduler and the unmount flush can never
5
+ * diverge on WHEN a dirty draft is persisted silently:
6
+ *
7
+ * - A **publishing** rail AUTOSAVES: the draft persists 1s after the last edit and
8
+ * publish is the deliberate commit — so a dirty draft is flushed both on the
9
+ * debounce timer and on unmount (navigating away inside the debounce window).
10
+ * - A **non-publishing** rail (users, EC-240) SAVES EXPLICITLY: edits stay in the
11
+ * draft until the Save button, and an `UnsavedChangesGuard` protects navigation —
12
+ * so it NEVER persists silently (returns false here).
13
+ */
14
+ export function shouldPersistDraft(input) {
15
+ return !input.explicitSave && input.dirty;
16
+ }
17
+ /**
18
+ * EC-225 autosave · EC-240 explicit save. Owns the debounced autosave timer and
19
+ * the unmount flush for a publishing rail; a non-publishing rail persists only via
20
+ * its Save button (the caller's `UnsavedChangesGuard` covers navigation). Refs let
21
+ * the unmount flush read the latest values without re-subscribing.
22
+ *
23
+ * Returns `explicitSave` — the caller renders the Save button + guard (explicit)
24
+ * or the autosave status text (autosave) from it.
25
+ */
26
+ export function useSaveMode(input) {
27
+ const { canPublish, dirty, draft, source } = input;
28
+ const explicitSave = !canPublish;
29
+ const draftRef = useRef(draft);
30
+ draftRef.current = draft;
31
+ const dirtyRef = useRef(dirty);
32
+ dirtyRef.current = dirty;
33
+ const sourceRef = useRef(source);
34
+ sourceRef.current = source;
35
+ const explicitSaveRef = useRef(explicitSave);
36
+ explicitSaveRef.current = explicitSave;
37
+ useEffect(() => {
38
+ if (explicitSave)
39
+ return; // explicit rails persist only via the Save button
40
+ if (!dirty)
41
+ return;
42
+ const timer = window.setTimeout(() => sourceRef.current.save(draftRef.current), 1000);
43
+ return () => window.clearTimeout(timer);
44
+ }, [dirty, draft, explicitSave]);
45
+ // Flush a still-pending AUTOSAVE edit on unmount (navigating away inside the
46
+ // debounce window) so nothing is lost. Explicit rails never flush silently —
47
+ // their `UnsavedChangesGuard` catches the navigation instead. Unmount-only.
48
+ useEffect(() => () => {
49
+ if (shouldPersistDraft({ explicitSave: explicitSaveRef.current, dirty: dirtyRef.current }))
50
+ sourceRef.current.save(draftRef.current);
51
+ }, []);
52
+ return { explicitSave };
53
+ }
54
+ //# sourceMappingURL=use-save-mode.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-save-mode.js","sourceRoot":"","sources":["../../../src/components/cms/use-save-mode.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,OAAO,CAAA;AAIzC;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,kBAAkB,CAAC,KAAgD;IACjF,OAAO,CAAC,KAAK,CAAC,YAAY,IAAI,KAAK,CAAC,KAAK,CAAA;AAC3C,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,WAAW,CAAC,KAK3B;IACC,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,KAAK,CAAA;IAClD,MAAM,YAAY,GAAG,CAAC,UAAU,CAAA;IAChC,MAAM,QAAQ,GAAG,MAAM,CAAC,KAAK,CAAC,CAAA;IAC9B,QAAQ,CAAC,OAAO,GAAG,KAAK,CAAA;IACxB,MAAM,QAAQ,GAAG,MAAM,CAAC,KAAK,CAAC,CAAA;IAC9B,QAAQ,CAAC,OAAO,GAAG,KAAK,CAAA;IACxB,MAAM,SAAS,GAAG,MAAM,CAAC,MAAM,CAAC,CAAA;IAChC,SAAS,CAAC,OAAO,GAAG,MAAM,CAAA;IAC1B,MAAM,eAAe,GAAG,MAAM,CAAC,YAAY,CAAC,CAAA;IAC5C,eAAe,CAAC,OAAO,GAAG,YAAY,CAAA;IAEtC,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,YAAY;YAAE,OAAM,CAAC,kDAAkD;QAC3E,IAAI,CAAC,KAAK;YAAE,OAAM;QAClB,MAAM,KAAK,GAAG,MAAM,CAAC,UAAU,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,IAAI,CAAC,CAAA;QACrF,OAAO,GAAG,EAAE,CAAC,MAAM,CAAC,YAAY,CAAC,KAAK,CAAC,CAAA;IACzC,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,YAAY,CAAC,CAAC,CAAA;IAEhC,6EAA6E;IAC7E,6EAA6E;IAC7E,4EAA4E;IAC5E,SAAS,CACP,GAAG,EAAE,CAAC,GAAG,EAAE;QACT,IAAI,kBAAkB,CAAC,EAAE,YAAY,EAAE,eAAe,CAAC,OAAO,EAAE,KAAK,EAAE,QAAQ,CAAC,OAAO,EAAE,CAAC;YACxF,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAA;IAC5C,CAAC,EACD,EAAE,CACH,CAAA;IAED,OAAO,EAAE,YAAY,EAAE,CAAA;AACzB,CAAC","sourcesContent":["import { useEffect, useRef } from 'react'\nimport type { CmsDocument } from '@elytracms/core/cms-core'\nimport type { CollectionViewSource } from '@elytracms/studio/lib/collection-view/collection-view-source'\n\n/**\n * EC-225 / EC-240 — the two persistence disciplines the detail editor supports,\n * as one pure decision so the autosave scheduler and the unmount flush can never\n * diverge on WHEN a dirty draft is persisted silently:\n *\n * - A **publishing** rail AUTOSAVES: the draft persists 1s after the last edit and\n * publish is the deliberate commit — so a dirty draft is flushed both on the\n * debounce timer and on unmount (navigating away inside the debounce window).\n * - A **non-publishing** rail (users, EC-240) SAVES EXPLICITLY: edits stay in the\n * draft until the Save button, and an `UnsavedChangesGuard` protects navigation —\n * so it NEVER persists silently (returns false here).\n */\nexport function shouldPersistDraft(input: { explicitSave: boolean; dirty: boolean }): boolean {\n return !input.explicitSave && input.dirty\n}\n\n/**\n * EC-225 autosave · EC-240 explicit save. Owns the debounced autosave timer and\n * the unmount flush for a publishing rail; a non-publishing rail persists only via\n * its Save button (the caller's `UnsavedChangesGuard` covers navigation). Refs let\n * the unmount flush read the latest values without re-subscribing.\n *\n * Returns `explicitSave` — the caller renders the Save button + guard (explicit)\n * or the autosave status text (autosave) from it.\n */\nexport function useSaveMode(input: {\n canPublish: boolean\n dirty: boolean\n draft: CmsDocument\n source: CollectionViewSource\n}): { explicitSave: boolean } {\n const { canPublish, dirty, draft, source } = input\n const explicitSave = !canPublish\n const draftRef = useRef(draft)\n draftRef.current = draft\n const dirtyRef = useRef(dirty)\n dirtyRef.current = dirty\n const sourceRef = useRef(source)\n sourceRef.current = source\n const explicitSaveRef = useRef(explicitSave)\n explicitSaveRef.current = explicitSave\n\n useEffect(() => {\n if (explicitSave) return // explicit rails persist only via the Save button\n if (!dirty) return\n const timer = window.setTimeout(() => sourceRef.current.save(draftRef.current), 1000)\n return () => window.clearTimeout(timer)\n }, [dirty, draft, explicitSave])\n\n // Flush a still-pending AUTOSAVE edit on unmount (navigating away inside the\n // debounce window) so nothing is lost. Explicit rails never flush silently —\n // their `UnsavedChangesGuard` catches the navigation instead. Unmount-only.\n useEffect(\n () => () => {\n if (shouldPersistDraft({ explicitSave: explicitSaveRef.current, dirty: dirtyRef.current }))\n sourceRef.current.save(draftRef.current)\n },\n [],\n )\n\n return { explicitSave }\n}\n"]}
@@ -4,17 +4,17 @@ import { ArrowRight, ChevronDown, ChevronLeft, ChevronRight, Image as ImageIcon,
4
4
  import { diffDocuments } from '@elytracms/core/content';
5
5
  import { assetSubfieldsOf, isAssetNode, readAssetId, readDocumentRef } from '@elytracms/core/cms-core';
6
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
7
  import { translate } from '@elytracms/studio/lib/i18n';
9
8
  import { localeChipLabel } from '@elytracms/studio/lib/locale';
10
9
  import { cn } from '@elytracms/studio/lib/utils';
11
10
  import { readDiffViewMode, storeDiffViewMode } from '@elytracms/studio/lib/diff-view-preference';
12
11
  import { useCollectionView } from '../collection-view/collection-view-context';
13
- import { fieldLabel } from './cms-store';
12
+ import { fieldLabel } from './cms-labels';
14
13
  import { toRefArray } from './cell';
15
14
  import { DIFF_TRUNCATE_THRESHOLD, mergeWordDiff, richTextToText, truncateText, truncateTokens, wordDiff, } from './version-compare-text';
16
15
  import { useDocumentEditor } from './document-editor-context';
17
16
  import { useDocumentVersionsQuery } from './use-document-versions';
17
+ import { useRestoreVersion } from './use-restore-version';
18
18
  import { formatVersionTimestamp } from './versions-model';
19
19
  /**
20
20
  * EC-298 — the version compare view. Renders as the sticky **Compare** content tab
@@ -35,8 +35,16 @@ export function VersionCompare() {
35
35
  const source = useCollectionView();
36
36
  const { collection, docRef, draft, dirty, viewReadOnly, save, replaceDraft, compare, setCompareBase, setCompareTarget, } = useDocumentEditor();
37
37
  const { versionsQuery, versionKey } = useDocumentVersionsQuery(docRef);
38
- const cache = useOperationCache();
39
- const [busy, setBusy] = useState(false);
38
+ // EC-325 — the shared restore sequence (flush → restore → replaceDraft →
39
+ // invalidate). `busy` gates the restore controls below while it round-trips.
40
+ const { restore: restoreVersion, busy } = useRestoreVersion({
41
+ source,
42
+ docRef,
43
+ versionKey,
44
+ dirty,
45
+ save,
46
+ replaceDraft,
47
+ });
40
48
  // EC-317 — the unified/side-by-side toggle is a transient, personal-local view
41
49
  // preference (same class as the theme/rail prefs): read once at mount, written
42
50
  // back on every change, never round-tripped through the project graph.
@@ -97,19 +105,8 @@ export function VersionCompare() {
97
105
  const restore = useCallback(async (scope) => {
98
106
  if (!compare)
99
107
  return;
100
- setBusy(true);
101
- try {
102
- if (dirty)
103
- save();
104
- const restored = await source.versioning?.restore(docRef, compare.base, scope);
105
- if (restored)
106
- replaceDraft(restored);
107
- await cache.invalidate(versionKey);
108
- }
109
- finally {
110
- setBusy(false);
111
- }
112
- }, [compare, dirty, save, source, docRef, replaceDraft, cache, versionKey]);
108
+ await restoreVersion(compare.base, scope);
109
+ }, [compare, restoreVersion]);
113
110
  if (!compare)
114
111
  return null;
115
112
  if (view === null)