@riducms/admin 0.2.2 → 0.3.0

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 (352) hide show
  1. package/THIRD_PARTY_NOTICES.md +60 -37
  2. package/package.json +27 -15
  3. package/src/app/admin-core-view-route.svelte +38 -53
  4. package/src/app/admin-extension-route.svelte +19 -0
  5. package/src/app/admin-layout.scss +175 -0
  6. package/src/app/admin-layout.svelte +144 -410
  7. package/src/app/admin-navigation-progress.svelte +11 -77
  8. package/src/app/admin-page-title.svelte +17 -85
  9. package/src/app/admin-page-title.ts +136 -0
  10. package/src/app/admin-route-error.svelte +24 -0
  11. package/src/app/admin-router-provider.svelte +34 -0
  12. package/src/app/admin-router-root.svelte +41 -0
  13. package/src/app/admin-routes.svelte +56 -19
  14. package/src/app/loading-boundary.svelte +3 -1
  15. package/src/app-meta.ts +13 -0
  16. package/src/app.css +138 -496
  17. package/src/app.svelte +60 -26
  18. package/src/components/brand/ridu-logo.svelte +19 -5
  19. package/src/components/data/list-table.scss +63 -0
  20. package/src/components/icons/LICENSE.md +24 -0
  21. package/src/components/icons/chevron.svelte +8 -0
  22. package/src/components/icons/plus.svelte +12 -0
  23. package/src/components/icons/search.svelte +12 -0
  24. package/src/components/icons/x.svelte +8 -0
  25. package/src/components/json-tree/json-tree-node.svelte +61 -67
  26. package/src/components/json-tree/json-viewer.scss +153 -0
  27. package/src/components/json-tree/json-viewer.svelte +57 -96
  28. package/src/components/ui/banner/banner.scss +28 -0
  29. package/src/components/ui/banner/banner.svelte +9 -8
  30. package/src/components/ui/banner/index.ts +1 -5
  31. package/src/components/ui/code-editor/code-editor.scss +20 -0
  32. package/src/components/ui/code-editor/code-editor.svelte +71 -0
  33. package/src/components/ui/code-editor/code-editor.ts +151 -0
  34. package/src/components/ui/code-editor/code-languages.ts +29 -0
  35. package/src/components/ui/collapsible-panel/collapsible-panel.scss +38 -0
  36. package/src/components/ui/collapsible-panel/collapsible-panel.svelte +13 -0
  37. package/src/components/ui/collapsible-panel/index.ts +1 -0
  38. package/src/components/ui/combobox/combobox.scss +163 -0
  39. package/src/components/ui/combobox/index.ts +22 -0
  40. package/src/components/ui/date-value-control/date-value-calendar.svelte +128 -16
  41. package/src/components/ui/date-value-control/date-value-control-loader.scss +26 -0
  42. package/src/components/ui/date-value-control/date-value-control-loader.svelte +35 -13
  43. package/src/components/ui/date-value-control/date-value-control.scss +223 -0
  44. package/src/components/ui/date-value-control/date-value-control.svelte +62 -44
  45. package/src/components/ui/drawer/drawer-depth.ts +8 -0
  46. package/src/components/ui/dropdown-menu/dropdown-menu-content.svelte +2 -5
  47. package/src/components/ui/dropdown-menu/dropdown-menu-item.svelte +2 -5
  48. package/src/components/ui/dropdown-menu/dropdown-menu-separator.svelte +2 -2
  49. package/src/components/ui/dropdown-menu/dropdown-menu.scss +62 -0
  50. package/src/components/ui/kbd/kbd.scss +17 -0
  51. package/src/components/ui/kbd/kbd.svelte +3 -10
  52. package/src/components/ui/navigation-drawer/navigation-drawer.scss +40 -0
  53. package/src/components/ui/navigation-drawer/navigation-drawer.svelte +38 -0
  54. package/src/components/ui/popover/popover-content.svelte +3 -5
  55. package/src/components/ui/popover/popover.scss +18 -0
  56. package/src/components/ui/radio-group/index.ts +0 -1
  57. package/src/components/ui/radio-group/radio-card-item.svelte +7 -12
  58. package/src/components/ui/radio-group/radio-group.svelte +2 -2
  59. package/src/components/ui/radio-group/radio.scss +45 -0
  60. package/src/components/ui/select/select-content.svelte +5 -6
  61. package/src/components/ui/select/select-item.svelte +8 -8
  62. package/src/components/ui/select/select-scroll-down-button.svelte +3 -5
  63. package/src/components/ui/select/select-scroll-up-button.svelte +3 -5
  64. package/src/components/ui/select/select-trigger.svelte +6 -7
  65. package/src/components/ui/select/select.scss +126 -0
  66. package/src/components/ui/sheet/sheet-content.svelte +6 -34
  67. package/src/components/ui/sheet/sheet-description.svelte +2 -2
  68. package/src/components/ui/sheet/sheet-overlay.svelte +2 -21
  69. package/src/components/ui/sheet/sheet-title.svelte +2 -2
  70. package/src/components/ui/sheet/sheet.scss +221 -0
  71. package/src/components/ui/skeleton/skeleton.scss +20 -0
  72. package/src/components/ui/skeleton/skeleton.svelte +4 -16
  73. package/src/components/ui/sonner/sonner.scss +121 -0
  74. package/src/components/ui/sonner/sonner.svelte +11 -10
  75. package/src/components/ui/status-indicator/status-indicator.scss +48 -0
  76. package/src/components/ui/status-indicator/status-indicator.svelte +14 -14
  77. package/src/components/ui/tabs/tabs-content.svelte +2 -2
  78. package/src/components/ui/tabs/tabs-list.svelte +7 -7
  79. package/src/components/ui/tabs/tabs-trigger.svelte +2 -8
  80. package/src/components/ui/tabs/tabs.scss +86 -0
  81. package/src/components/ui/tabs/tabs.svelte +2 -2
  82. package/src/components/ui/timezone-picker/timezone-picker.svelte +152 -0
  83. package/src/core/api/admin-client.ts +2 -2
  84. package/src/core/bootstrap/admin-bootstrap.ts +396 -0
  85. package/src/core/bootstrap/admin-prepared-state.ts +213 -0
  86. package/src/core/bootstrap/admin-route-modules.ts +66 -0
  87. package/src/core/clipboard/copy-feedback.svelte.ts +43 -0
  88. package/src/core/forms/embedded-fields.ts +4 -8
  89. package/src/core/forms/field-editor-authoring.ts +8 -0
  90. package/src/core/forms/field-issue-focus.ts +4 -4
  91. package/src/core/forms/field-occurrence.ts +4 -7
  92. package/src/core/forms/form-controller.svelte.ts +14 -21
  93. package/src/core/forms/form-draft-recovery.ts +17 -4
  94. package/src/core/forms/form-issue-correlation.ts +13 -13
  95. package/src/core/forms/form-path.ts +17 -0
  96. package/src/core/forms/form-schema.ts +12 -43
  97. package/src/core/forms/form-validation.ts +7 -46
  98. package/src/core/forms/plugin-field-authoring.ts +6 -2
  99. package/src/core/forms/plugin-field-write.ts +2 -6
  100. package/src/core/i18n/admin-i18n.svelte.ts +18 -0
  101. package/src/core/i18n/format-admin-date-time.ts +19 -0
  102. package/src/core/i18n/humanize-identifier.ts +5 -0
  103. package/src/core/i18n/time-zone-label.ts +20 -0
  104. package/src/core/loaders/admin-view-host.svelte +72 -0
  105. package/src/core/loaders/admin-view-loader.svelte.ts +90 -0
  106. package/src/core/notifications/validation-toast.svelte +2 -2
  107. package/src/core/plugins/field-registry.ts +7 -8
  108. package/src/core/routing/admin-paths.ts +53 -2
  109. package/src/core/routing/admin-scroll.svelte.ts +74 -0
  110. package/src/core/routing/admin-view-selection.ts +78 -0
  111. package/src/core/runtime/admin-runtime.svelte.ts +121 -124
  112. package/src/core/schema/field-default.ts +33 -0
  113. package/src/core/schema/field-localization.ts +15 -0
  114. package/src/core/schema/read-document-path.ts +11 -0
  115. package/src/features/account/account-menu.scss +37 -0
  116. package/src/features/account/account-menu.svelte +115 -0
  117. package/src/features/{auth → account}/account-navigation.svelte +16 -19
  118. package/src/features/{auth → account}/account-route.svelte +206 -49
  119. package/src/features/account/account-security-controller.svelte.ts +157 -0
  120. package/src/features/{auth → account}/account-security-route.svelte +140 -42
  121. package/src/features/api-reference/api-reference-code.svelte +64 -0
  122. package/src/features/api-reference/api-reference-content.svelte +258 -0
  123. package/src/features/api-reference/api-reference-drawer.scss +74 -0
  124. package/src/features/api-reference/api-reference-examples.ts +381 -0
  125. package/src/features/api-reference/api-reference-schema.ts +332 -0
  126. package/src/features/api-reference/api-reference.scss +190 -0
  127. package/src/features/api-reference/api-reference.svelte +74 -0
  128. package/src/features/auth/auth-brand.svelte +5 -3
  129. package/src/features/auth/auth-field.svelte +92 -0
  130. package/src/features/auth/auth-frame.svelte +5 -8
  131. package/src/features/auth/auth.scss +122 -0
  132. package/src/features/auth/create-first-user-controller.svelte.ts +20 -51
  133. package/src/features/auth/create-first-user-route.svelte +83 -122
  134. package/src/features/auth/forgot-password-route.svelte +31 -23
  135. package/src/features/auth/login-route.svelte +68 -116
  136. package/src/features/auth/logout-button.scss +34 -0
  137. package/src/features/auth/logout-button.svelte +52 -0
  138. package/src/features/auth/request-verification-route.svelte +27 -22
  139. package/src/features/auth/reset-password-route.svelte +108 -41
  140. package/src/features/auth/verify-email-route.svelte +49 -15
  141. package/src/features/bulk-edit/bulk-edit-fields.ts +23 -0
  142. package/src/features/bulk-edit/bulk-edit.scss +152 -0
  143. package/src/features/bulk-edit/field-picker.svelte +176 -0
  144. package/src/features/collections/bulk/bulk-actions.svelte +148 -0
  145. package/src/features/collections/bulk/bulk-confirmation.svelte +153 -0
  146. package/src/features/collections/bulk/bulk-editor-loader.svelte +95 -0
  147. package/src/features/collections/bulk/bulk-editor.svelte +186 -0
  148. package/src/features/collections/collection-list-cell-values.ts +144 -0
  149. package/src/features/collections/collection-list-controller.svelte.ts +268 -379
  150. package/src/features/collections/collection-list-preferences-controller.svelte.ts +54 -40
  151. package/src/features/collections/collection-list-query.ts +223 -0
  152. package/src/features/collections/collection-list-results.svelte +205 -329
  153. package/src/features/collections/collection-list-route.svelte +143 -884
  154. package/src/features/collections/collection-list.scss +261 -0
  155. package/src/features/collections/collection-list.svelte.ts +362 -0
  156. package/src/features/collections/controls/column-picker.svelte +110 -0
  157. package/src/features/collections/controls/column-pill.svelte +62 -0
  158. package/src/features/collections/controls/filter-builder.svelte +193 -0
  159. package/src/features/collections/controls/filter-row.svelte +142 -0
  160. package/src/features/collections/controls/list-controls.scss +196 -0
  161. package/src/features/collections/controls/list-options.svelte +203 -0
  162. package/src/features/collections/controls/list-pagination.scss +78 -0
  163. package/src/features/collections/controls/list-pagination.svelte +115 -0
  164. package/src/features/collections/controls/list-toolbar.svelte +132 -0
  165. package/src/features/collections/controls/sort-heading.scss +52 -0
  166. package/src/features/collections/controls/sort-heading.svelte +78 -0
  167. package/src/features/collections/list-columns.ts +43 -0
  168. package/src/features/collections/list-workspace.ts +155 -54
  169. package/src/features/dashboard/dashboard-route.svelte +82 -86
  170. package/src/features/dashboard/dashboard.scss +131 -0
  171. package/src/features/development/development-tools.svelte +20 -36
  172. package/src/features/documents/document-action-bar.svelte +438 -0
  173. package/src/features/documents/document-api-view-loader.svelte +17 -6
  174. package/src/features/documents/document-api-view.scss +120 -0
  175. package/src/features/documents/document-api-view.svelte +169 -454
  176. package/src/features/documents/document-controller.svelte.ts +526 -613
  177. package/src/features/documents/document-credentials.scss +37 -0
  178. package/src/features/documents/document-credentials.svelte +72 -0
  179. package/src/features/documents/{document-route-dirty.ts → document-credentials.ts} +25 -0
  180. package/src/features/documents/document-field-sections.scss +39 -0
  181. package/src/features/documents/document-field-sections.svelte +4 -13
  182. package/src/features/documents/document-loading.scss +24 -0
  183. package/src/features/documents/document-lock-controller.svelte.ts +187 -0
  184. package/src/features/documents/document-operation-dialogs.svelte +110 -0
  185. package/src/features/documents/document-route-loader.svelte +17 -5
  186. package/src/features/documents/document-route.svelte +305 -811
  187. package/src/features/documents/document-schedule.scss +127 -0
  188. package/src/features/documents/document-schedule.svelte +323 -0
  189. package/src/features/documents/document-title.ts +25 -0
  190. package/src/features/documents/document.scss +274 -0
  191. package/src/features/documents/live-preview-panel.scss +117 -0
  192. package/src/features/documents/live-preview-panel.svelte +167 -168
  193. package/src/features/documents/live-preview.ts +2 -10
  194. package/src/features/documents/upload-document-preview-loader.svelte +26 -23
  195. package/src/features/localization/content-locale-switcher.scss +43 -0
  196. package/src/features/localization/content-locale-switcher.svelte +24 -17
  197. package/src/features/navigation/admin-breadcrumbs.scss +56 -0
  198. package/src/features/navigation/admin-breadcrumbs.svelte +102 -37
  199. package/src/features/navigation/admin-command-menu.svelte +38 -15
  200. package/src/features/navigation/admin-navigation.scss +133 -0
  201. package/src/features/navigation/admin-navigation.svelte +169 -0
  202. package/src/features/navigation/breadcrumb-context.svelte.ts +15 -0
  203. package/src/features/navigation/navigation-preference.svelte.ts +70 -0
  204. package/src/features/reference-browser/reference-browser-editor.svelte +180 -0
  205. package/src/features/reference-browser/reference-browser-list.scss +113 -0
  206. package/src/features/reference-browser/reference-browser-list.svelte +370 -0
  207. package/src/features/reference-browser/reference-browser-loader.svelte +26 -8
  208. package/src/features/reference-browser/reference-browser-workflow.svelte.ts +335 -236
  209. package/src/features/reference-browser/reference-browser.scss +217 -0
  210. package/src/features/reference-browser/reference-browser.svelte +154 -797
  211. package/src/features/reference-browser/reference-collection-picker.svelte +116 -0
  212. package/src/features/reference-browser/reference-list-query.ts +35 -0
  213. package/src/features/uploads/bulk-upload-controller.svelte.ts +604 -0
  214. package/src/features/uploads/bulk-upload-edit-all.svelte +92 -0
  215. package/src/features/uploads/bulk-upload-editor.svelte +191 -0
  216. package/src/features/uploads/bulk-upload-file-picker.svelte +70 -0
  217. package/src/features/uploads/bulk-upload-remote.svelte +100 -0
  218. package/src/features/uploads/bulk-upload-route-loader.svelte +54 -0
  219. package/src/features/uploads/bulk-upload-route.svelte +172 -592
  220. package/src/features/uploads/bulk-upload-sidebar.svelte +125 -0
  221. package/src/features/uploads/bulk-upload.scss +526 -0
  222. package/src/features/uploads/bulk-upload.ts +105 -14
  223. package/src/features/uploads/crop-surface.ts +150 -0
  224. package/src/features/uploads/image-editor.svelte +231 -0
  225. package/src/features/uploads/rendition-preview.svelte +64 -0
  226. package/src/features/uploads/upload-control.svelte +340 -0
  227. package/src/features/uploads/upload-document-contracts.ts +30 -0
  228. package/src/features/uploads/upload-draft.svelte.ts +294 -0
  229. package/src/features/uploads/upload-metadata-fields.scss +32 -0
  230. package/src/features/uploads/upload-metadata-fields.svelte +8 -7
  231. package/src/features/uploads/upload-thumbnail.svelte +47 -0
  232. package/src/features/uploads/upload.scss +361 -0
  233. package/src/features/versions/version-comparison.svelte +361 -0
  234. package/src/features/versions/version-diff-fields.svelte +136 -0
  235. package/src/features/versions/version-diff.ts +296 -41
  236. package/src/features/versions/version-history-controller.svelte.ts +311 -0
  237. package/src/features/versions/version-history-route-loader.svelte +32 -0
  238. package/src/features/versions/version-history-route.svelte +157 -522
  239. package/src/features/versions/version-history.scss +444 -0
  240. package/src/features/versions/version-history.ts +45 -0
  241. package/src/features/versions/version-table.svelte +91 -0
  242. package/src/fields/embedded-schema-draft-editor.scss +48 -0
  243. package/src/fields/embedded-schema-draft-editor.svelte +24 -16
  244. package/src/fields/field-collapsible.svelte +14 -8
  245. package/src/fields/field-layout.scss +171 -0
  246. package/src/fields/field-layout.svelte +3 -5
  247. package/src/fields/field-renderer.svelte +17 -29
  248. package/src/fields/field-shell.svelte +1 -1
  249. package/src/fields/field-tab-layout.svelte +25 -19
  250. package/src/fields/join/join-field-controller.svelte.ts +513 -0
  251. package/src/fields/join/join-field-values.ts +111 -0
  252. package/src/fields/join/join-field.scss +189 -0
  253. package/src/fields/join/join-field.svelte +164 -332
  254. package/src/fields/live-validation-feedback.svelte +4 -3
  255. package/src/fields/local-field-editor.svelte +2 -2
  256. package/src/fields/nested/block-name-input.svelte +11 -12
  257. package/src/fields/nested/block-picker.scss +128 -0
  258. package/src/fields/nested/block-picker.svelte +81 -0
  259. package/src/fields/nested/block-placeholder.svg +2 -0
  260. package/src/fields/nested/nested-field.scss +331 -0
  261. package/src/fields/nested/nested-field.svelte +265 -245
  262. package/src/fields/nested/sortable-row.svelte +9 -7
  263. package/src/fields/plugin-field.svelte +4 -6
  264. package/src/fields/point/point-field.svelte +51 -38
  265. package/src/fields/primitive-list/primitive-list-field.svelte +63 -19
  266. package/src/fields/primitive-list/primitive-list.scss +109 -0
  267. package/src/fields/radio/radio-field.svelte +2 -1
  268. package/src/fields/relationship/relationship-field-controller.svelte.ts +81 -41
  269. package/src/fields/relationship/relationship-field.svelte +144 -175
  270. package/src/fields/relationship/relationship-lookup-controller.svelte.ts +58 -30
  271. package/src/fields/relationship/relationship-query.ts +2 -10
  272. package/src/fields/relationship/relationship-quick-picker-controller.svelte.ts +36 -62
  273. package/src/fields/relationship/relationship-quick-picker.svelte +178 -227
  274. package/src/fields/relationship/relationship-selection.svelte +120 -0
  275. package/src/fields/relationship/relationship-value.ts +46 -7
  276. package/src/fields/relationship/relationship.scss +182 -0
  277. package/src/fields/scalar/code-field.svelte +101 -0
  278. package/src/fields/scalar/date-control-value.ts +13 -0
  279. package/src/fields/scalar/scalar-field.svelte +25 -67
  280. package/src/fields/select/select-field.svelte +132 -94
  281. package/src/fields/text/slug-field.scss +28 -0
  282. package/src/fields/text/slug-field.svelte +19 -15
  283. package/src/fields/text/text-field.svelte +1 -2
  284. package/src/fields/ui/ui-field.svelte +7 -4
  285. package/src/fields/virtual/virtual-field.svelte +2 -5
  286. package/src/index.ts +1 -2
  287. package/src/routing.ts +2 -0
  288. package/src/app/deferred-redirect.svelte +0 -23
  289. package/src/components/code-block/code-block.svelte +0 -107
  290. package/src/components/ui/bulk-action-bar/bulk-action-bar.svelte +0 -50
  291. package/src/components/ui/bulk-action-bar/index.ts +0 -1
  292. package/src/components/ui/button/index.ts +0 -8
  293. package/src/components/ui/calendar/calendar-caption.svelte +0 -72
  294. package/src/components/ui/calendar/calendar-cell.svelte +0 -20
  295. package/src/components/ui/calendar/calendar-day.svelte +0 -27
  296. package/src/components/ui/calendar/calendar-grid-row.svelte +0 -13
  297. package/src/components/ui/calendar/calendar-grid.svelte +0 -17
  298. package/src/components/ui/calendar/calendar-head-cell.svelte +0 -20
  299. package/src/components/ui/calendar/calendar-header.svelte +0 -20
  300. package/src/components/ui/calendar/calendar-heading.svelte +0 -17
  301. package/src/components/ui/calendar/calendar-month-select.svelte +0 -45
  302. package/src/components/ui/calendar/calendar-month.svelte +0 -16
  303. package/src/components/ui/calendar/calendar-months.svelte +0 -20
  304. package/src/components/ui/calendar/calendar-nav.svelte +0 -20
  305. package/src/components/ui/calendar/calendar-next-button.svelte +0 -30
  306. package/src/components/ui/calendar/calendar-prev-button.svelte +0 -30
  307. package/src/components/ui/calendar/calendar-year-select.svelte +0 -44
  308. package/src/components/ui/calendar/calendar.svelte +0 -118
  309. package/src/components/ui/calendar/index.ts +0 -1
  310. package/src/components/ui/checkbox/checkbox.svelte +0 -35
  311. package/src/components/ui/checkbox/index.ts +0 -1
  312. package/src/components/ui/dialog/dialog-close.svelte +0 -11
  313. package/src/components/ui/dialog/dialog-content.svelte +0 -52
  314. package/src/components/ui/dialog/dialog-description.svelte +0 -20
  315. package/src/components/ui/dialog/dialog-footer.svelte +0 -20
  316. package/src/components/ui/dialog/dialog-header.svelte +0 -20
  317. package/src/components/ui/dialog/dialog-overlay.svelte +0 -20
  318. package/src/components/ui/dialog/dialog-title.svelte +0 -17
  319. package/src/components/ui/dialog/dialog-trigger.svelte +0 -11
  320. package/src/components/ui/dialog/dialog.svelte +0 -7
  321. package/src/components/ui/dialog/index.ts +0 -8
  322. package/src/components/ui/empty-state/empty-state.svelte +0 -34
  323. package/src/components/ui/empty-state/index.ts +0 -1
  324. package/src/components/ui/input/index.ts +0 -1
  325. package/src/components/ui/pagination/index.ts +0 -1
  326. package/src/components/ui/pagination/pagination.svelte +0 -88
  327. package/src/components/ui/radio-group/radio-group-item.svelte +0 -17
  328. package/src/components/ui/slider/index.ts +0 -3
  329. package/src/components/ui/slider/slider-content.svelte +0 -59
  330. package/src/components/ui/slider/slider.svelte +0 -35
  331. package/src/components/ui/table/index.ts +0 -6
  332. package/src/components/ui/table/table-body.svelte +0 -20
  333. package/src/components/ui/table/table-cell.svelte +0 -23
  334. package/src/components/ui/table/table-head.svelte +0 -23
  335. package/src/components/ui/table/table-header.svelte +0 -20
  336. package/src/components/ui/table/table-row.svelte +0 -23
  337. package/src/components/ui/table/table.svelte +0 -22
  338. package/src/components/ui/textarea/index.ts +0 -1
  339. package/src/components/ui/tooltip/index.ts +0 -4
  340. package/src/components/ui/tooltip/tooltip-content.svelte +0 -50
  341. package/src/components/ui/tooltip/tooltip-provider.svelte +0 -7
  342. package/src/components/ui/tooltip/tooltip-trigger.svelte +0 -7
  343. package/src/components/ui/tooltip/tooltip.svelte +0 -7
  344. package/src/features/auth/account-menu.svelte +0 -159
  345. package/src/features/auth/account-security-controller.svelte.ts +0 -119
  346. package/src/features/collections/collection-list-bulk-actions.svelte +0 -143
  347. package/src/features/collections/collection-list-bulk-editor.svelte +0 -157
  348. package/src/features/collections/collection-list-filter-popover.svelte +0 -238
  349. package/src/features/collections/collection-list-workspace-toolbar.svelte +0 -409
  350. package/src/features/collections/collection-table-skeleton.svelte +0 -44
  351. package/src/features/documents/document-heading.ts +0 -20
  352. package/src/features/documents/upload-document-preview.svelte +0 -525
@@ -1,7 +1,13 @@
1
- import type { SchemaCollection, SchemaField } from "@riducms/protocol";
1
+ import {
2
+ isRecord,
3
+ resolveBlockTypes,
4
+ type SchemaCollection,
5
+ type SchemaField,
6
+ } from "@riducms/protocol";
2
7
  import type { AdminI18n } from "@riducms/translations";
3
-
4
- import type { AdminDocument, AdminVersion } from "@admin/core/api/admin-client";
8
+ import type { AdminVersion } from "@admin/core/api/admin-client";
9
+ import { versionDate } from "@admin/features/versions/version-history";
10
+ import { formatDateDisplay } from "@admin/fields/scalar/date-value";
5
11
 
6
12
  export interface VersionDiffRow {
7
13
  path: string;
@@ -9,64 +15,313 @@ export interface VersionDiffRow {
9
15
  before: unknown;
10
16
  after: unknown;
11
17
  changed: boolean;
18
+ field?: SchemaField;
19
+ locale?: string;
20
+ children?: VersionDiffRow[];
12
21
  }
13
22
 
14
23
  export function versionDiffRows(
15
24
  collection: SchemaCollection,
16
25
  current: AdminVersion,
17
26
  comparison: AdminVersion | undefined,
18
- i18n: AdminI18n
27
+ i18n: AdminI18n,
28
+ locales?: readonly string[]
29
+ ): VersionDiffRow[] {
30
+ const rows = compareFields(
31
+ collection.fields,
32
+ comparison?.Snapshot,
33
+ current.Snapshot,
34
+ "",
35
+ i18n,
36
+ locales
37
+ );
38
+ for (const name of ["updatedAt", "createdAt", "deletedAt"] as const) {
39
+ if (name in current.Snapshot || (comparison && name in comparison.Snapshot))
40
+ rows.push({
41
+ path: name,
42
+ label: i18n.t(`versions:${name}`),
43
+ before: comparison?.Snapshot[name],
44
+ after: current.Snapshot[name],
45
+ changed: !sameValue(comparison?.Snapshot[name], current.Snapshot[name]),
46
+ });
47
+ }
48
+ rows.push({
49
+ path: "_status",
50
+ label: i18n.t("versions:status"),
51
+ before: comparison?.Status,
52
+ after: current.Status,
53
+ changed: comparison?.Status !== current.Status,
54
+ });
55
+ return rows;
56
+ }
57
+
58
+ function compareFields(
59
+ fields: readonly SchemaField[],
60
+ before: unknown,
61
+ after: unknown,
62
+ prefix: string,
63
+ i18n: AdminI18n,
64
+ locales?: readonly string[]
19
65
  ): VersionDiffRow[] {
20
- const fields = comparisonFields(collection.fields);
21
- return [
22
- {
23
- path: "_status",
24
- label: i18n.t("versions:status"),
25
- before: comparison?.Status,
26
- after: current.Status,
27
- changed: !sameValue(comparison?.Status, current.Status),
28
- },
29
- ...fields.map(({ field, path }) => {
30
- const before = valueAtPath(comparison?.Snapshot, path);
31
- const after = valueAtPath(current.Snapshot, path);
66
+ const rows: VersionDiffRow[] = [];
67
+ for (const field of fields) {
68
+ if (field.type === "ui" || field.virtual || field.join || field.admin.hidden) continue;
69
+ const label = i18n.text(field.admin.label || field.name, field.admin.labelTranslations);
70
+ const path = prefix ? `${prefix}.${field.name}` : field.name;
71
+ const left = isRecord(before) ? before[field.name] : undefined;
72
+ const right = isRecord(after) ? after[field.name] : undefined;
73
+ const row = compareField(field, left, right, path, label, i18n, locales);
74
+ const section = field.admin.tab || field.admin.collapsible?.label;
75
+ if (section) {
76
+ const sectionPath = `${prefix}:section:${field.admin.tabGroup?.id ?? field.admin.collapsible?.id ?? ""}:${section}`;
77
+ let group = rows.find((item) => item.path === sectionPath);
78
+ if (!group) {
79
+ group = {
80
+ path: sectionPath,
81
+ label: i18n.text(
82
+ section,
83
+ field.admin.tab
84
+ ? field.admin.tabTranslations
85
+ : field.admin.collapsible?.labelTranslations
86
+ ),
87
+ before: undefined,
88
+ after: undefined,
89
+ changed: false,
90
+ children: [],
91
+ };
92
+ rows.push(group);
93
+ }
94
+ group.children!.push(row);
95
+ group.changed ||= row.changed;
96
+ } else rows.push(row);
97
+ }
98
+ return rows.map((row) => {
99
+ const child = row.children?.[0];
100
+ return !row.field && row.children?.length === 1 && child?.children && child.label === row.label
101
+ ? child
102
+ : row;
103
+ });
104
+ }
105
+
106
+ function compareField(
107
+ field: SchemaField,
108
+ before: unknown,
109
+ after: unknown,
110
+ path: string,
111
+ label: string,
112
+ i18n: AdminI18n,
113
+ locales?: readonly string[]
114
+ ): VersionDiffRow {
115
+ const row: VersionDiffRow = {
116
+ path,
117
+ label,
118
+ before,
119
+ after,
120
+ field,
121
+ changed: !sameValue(before, after),
122
+ };
123
+ if (field.localized && locales) {
124
+ row.children = locales.map((locale) => ({
125
+ ...compareField(
126
+ { ...field, localized: false },
127
+ isRecord(before) ? before[locale] : undefined,
128
+ isRecord(after) ? after[locale] : undefined,
129
+ `${path}:${locale}`,
130
+ label,
131
+ i18n,
132
+ locales
133
+ ),
134
+ locale,
135
+ }));
136
+ row.changed = row.children.some((child) => child.changed);
137
+ } else if (field.type === "group" && field.nested) {
138
+ row.children = compareFields(field.nested.fields, before, after, path, i18n, locales);
139
+ row.changed = row.children.some((child) => child.changed);
140
+ } else if ((field.type === "array" && field.nested) || field.blocks) {
141
+ const left = Array.isArray(before) ? before : [];
142
+ const right = Array.isArray(after) ? after : [];
143
+ const rowKey = (value: unknown, index: number) =>
144
+ isRecord(value) && typeof value._key === "string" ? value._key : String(index);
145
+ const leftKeys = left.map(rowKey);
146
+ const rightKeys = right.map(rowKey);
147
+ const keys = [...new Set([...rightKeys, ...leftKeys])];
148
+ const blocks = resolveBlockTypes(field.blocks);
149
+ row.children = keys.map((key, index) => {
150
+ const oldIndex = leftKeys.indexOf(key);
151
+ const newIndex = rightKeys.indexOf(key);
152
+ const oldValue = left[oldIndex];
153
+ const newValue = right[newIndex];
154
+ const value = newValue ?? oldValue;
155
+ const oldBlock = isRecord(oldValue)
156
+ ? blocks.find((block) => block.slug === oldValue.blockType)
157
+ : undefined;
158
+ const block = isRecord(value)
159
+ ? blocks.find((block) => block.slug === value.blockType)
160
+ : undefined;
161
+ const childPath = `${path}.${key}`;
162
+ // A type replacement may retain row identity. Keep removed fields from the old block visible.
163
+ const childFields = [
164
+ ...new Map(
165
+ [...(oldBlock?.fields ?? []), ...(block?.fields ?? field.nested?.fields ?? [])].map(
166
+ (child) => [child.name, child]
167
+ )
168
+ ).values(),
169
+ ];
170
+ const labels = block?.labels ?? field.nested?.rowLabels;
171
+ const childLabel = `${labels ? i18n.text(labels.singular, labels.singularTranslations) : i18n.t("versions:row")} ${i18n.formatNumber(index + 1, { minimumIntegerDigits: 2 })}`;
172
+ const children = compareFields(childFields, oldValue, newValue, childPath, i18n, locales);
173
+ if (field.blocks) {
174
+ const oldType = isRecord(oldValue) ? oldValue.blockType : undefined;
175
+ const newType = isRecord(newValue) ? newValue.blockType : undefined;
176
+ children.unshift({
177
+ path: `${childPath}.blockType`,
178
+ label: i18n.t("versions:blockType"),
179
+ before: oldType,
180
+ after: newType,
181
+ changed: !sameValue(oldType, newType),
182
+ });
183
+ }
184
+ if (
185
+ field.blocks &&
186
+ ((oldValue !== undefined && !oldBlock) || (newValue !== undefined && !block))
187
+ ) {
188
+ children.push({
189
+ path: `${childPath}:content`,
190
+ label: i18n.t("versions:content"),
191
+ before: oldValue,
192
+ after: newValue,
193
+ changed: !sameValue(oldValue, newValue),
194
+ });
195
+ }
196
+ if (oldIndex !== newIndex) {
197
+ children.push({
198
+ path: `${childPath}:position`,
199
+ label: i18n.t("versions:position"),
200
+ before: oldIndex < 0 ? undefined : oldIndex + 1,
201
+ after: newIndex < 0 ? undefined : newIndex + 1,
202
+ changed: true,
203
+ });
204
+ }
205
+
32
206
  return {
33
- path,
34
- label: i18n.text(field.admin.label || field.name, field.admin.labelTranslations),
35
- before,
36
- after,
37
- changed: !sameValue(before, after),
207
+ path: childPath,
208
+ label: childLabel,
209
+ before: oldValue,
210
+ after: newValue,
211
+ changed: !sameValue(oldValue, newValue) || oldIndex !== newIndex,
212
+ children,
38
213
  };
39
- }),
40
- ];
214
+ });
215
+ }
216
+ return row;
41
217
  }
42
218
 
43
- export function formatVersionValue(value: unknown, i18n: AdminI18n): string {
44
- if (value === undefined || value === null || value === "") return "—";
219
+ export function formatVersionValue(value: unknown, i18n: AdminI18n, row?: VersionDiffRow): string {
220
+ if (value === undefined || value === null || value === "") return "";
221
+ if (row?.path === "_status")
222
+ return i18n.t(value === "published" ? "documents:published" : "documents:draft");
223
+ if (row?.field?.type === "date") return formatDateDisplay(value, row.field.date?.format, i18n);
224
+ if (["updatedAt", "createdAt", "deletedAt"].includes(row?.path ?? ""))
225
+ return versionDate(String(value), i18n);
45
226
  if (typeof value === "string") return value;
46
227
  if (typeof value === "number") return i18n.formatNumber(value);
47
228
  if (typeof value === "boolean") return i18n.t(value ? "versions:true" : "versions:false");
229
+ if (row?.field?.plugin?.key === "richtext" || (isRecord(value) && isRecord(value.root))) {
230
+ const text = richText(value);
231
+ if (text) return text;
232
+ }
48
233
  return JSON.stringify(value, null, 2);
49
234
  }
50
235
 
51
- function comparisonFields(fields: readonly SchemaField[]) {
52
- return fields.flatMap((field): Array<{ field: SchemaField; path: string }> => {
53
- if (field.type === "ui" || field.virtual !== undefined || field.join !== undefined) return [];
54
- if (field.type === "group" && field.nested !== undefined) {
55
- return comparisonFields(field.nested.fields);
56
- }
57
- return [{ field, path: field.path || field.name }];
58
- });
236
+ function richText(value: unknown): string {
237
+ if (!isRecord(value)) return "";
238
+ if (typeof value.text === "string") return value.text;
239
+ if (isRecord(value.root)) return richText(value.root);
240
+ if (!Array.isArray(value.children)) return "";
241
+ return value.children.map(richText).join(value.type === "root" ? "\n\n" : "");
59
242
  }
60
243
 
61
- function valueAtPath(document: AdminDocument | undefined, path: string): unknown {
62
- let value: unknown = document;
63
- for (const segment of path.split(".")) {
64
- if (typeof value !== "object" || value === null || Array.isArray(value)) return undefined;
65
- value = Reflect.get(value, segment);
244
+ export function sameValue(left: unknown, right: unknown): boolean {
245
+ if (left === right) return true;
246
+ if (Array.isArray(left) && Array.isArray(right))
247
+ return (
248
+ left.length === right.length && left.every((value, index) => sameValue(value, right[index]))
249
+ );
250
+ if (isRecord(left) && isRecord(right)) {
251
+ const keys = Object.keys(left);
252
+ return (
253
+ keys.length === Object.keys(right).length &&
254
+ keys.every((key) => Object.hasOwn(right, key) && sameValue(left[key], right[key]))
255
+ );
66
256
  }
67
- return value;
257
+ return false;
68
258
  }
69
259
 
70
- function sameValue(left: unknown, right: unknown) {
71
- return JSON.stringify(left) === JSON.stringify(right);
260
+ /** Keep unchanged words readable; bound the LCS matrix for large code and rich-text values. */
261
+ export function versionTextDiff(before: string, after: string) {
262
+ const left = before.split(/(\s+|[.,;:!?()[\]{}])/).filter(Boolean);
263
+ const right = after.split(/(\s+|[.,;:!?()[\]{}])/).filter(Boolean);
264
+ let start = 0;
265
+ while (start < left.length && start < right.length && left[start] === right[start]) start++;
266
+ let end = 0;
267
+ while (
268
+ end < left.length - start &&
269
+ end < right.length - start &&
270
+ left[left.length - end - 1] === right[right.length - end - 1]
271
+ )
272
+ end++;
273
+ const a = left.slice(start, left.length - end);
274
+ const b = right.slice(start, right.length - end);
275
+ const oldParts: { text: string; changed: boolean }[] = [];
276
+ const newParts: { text: string; changed: boolean }[] = [];
277
+ const add = (parts: typeof oldParts, text: string, changed: boolean) => {
278
+ if (!text) return;
279
+ const last = parts.at(-1);
280
+ if (last?.changed === changed) last.text += text;
281
+ else parts.push({ text, changed });
282
+ };
283
+ add(oldParts, left.slice(0, start).join(""), false);
284
+ add(newParts, right.slice(0, start).join(""), false);
285
+ if (a.length * b.length > 40_000) {
286
+ add(oldParts, a.join(""), true);
287
+ add(newParts, b.join(""), true);
288
+ } else {
289
+ const lengths = Array.from({ length: a.length + 1 }, () => new Uint32Array(b.length + 1));
290
+ for (let i = a.length - 1; i >= 0; i--)
291
+ for (let j = b.length - 1; j >= 0; j--)
292
+ lengths[i]![j] =
293
+ a[i] === b[j]
294
+ ? 1 + lengths[i + 1]![j + 1]!
295
+ : Math.max(lengths[i + 1]![j]!, lengths[i]![j + 1]!);
296
+ let i = 0,
297
+ j = 0;
298
+ while (i < a.length || j < b.length) {
299
+ if (i < a.length && j < b.length && a[i] === b[j]) {
300
+ add(oldParts, a[i++]!, false);
301
+ add(newParts, b[j++]!, false);
302
+ } else if (i < a.length && (j === b.length || lengths[i + 1]![j]! >= lengths[i]![j + 1]!))
303
+ add(oldParts, a[i++]!, true);
304
+ else add(newParts, b[j++]!, true);
305
+ }
306
+ }
307
+ add(oldParts, end ? left.slice(-end).join("") : "", false);
308
+ add(newParts, end ? right.slice(-end).join("") : "", false);
309
+ return { before: oldParts, after: newParts };
310
+ }
311
+
312
+ export function versionReferences(row: VersionDiffRow, value: unknown) {
313
+ const reference = row.field?.relationship ?? row.field?.upload;
314
+ if (!reference) return [];
315
+ return (
316
+ Array.isArray(value) ? value : value === undefined || value === null ? [] : [value]
317
+ ).flatMap((item) => {
318
+ const collection =
319
+ isRecord(item) && typeof item.relationTo === "string"
320
+ ? item.relationTo
321
+ : reference.collectionSlug;
322
+ const id = isRecord(item) ? item.id : item;
323
+ return collection && (typeof id === "string" || typeof id === "number")
324
+ ? [{ collection, id: String(id), key: `${collection}:${id}` }]
325
+ : [];
326
+ });
72
327
  }
@@ -0,0 +1,311 @@
1
+ import { RiduError } from "@riducms/sdk";
2
+ import type { AdminVersionsDataV1, OperationCapabilities } from "@riducms/protocol";
3
+ import type { AdminDocument, AdminVersion } from "@admin/core/api/admin-client";
4
+ import type { AdminRuntime } from "@admin/core/runtime/admin-runtime.svelte";
5
+ import type { NotificationCenter } from "@admin/core/notifications/notification-center.svelte";
6
+
7
+ export interface VersionHistoryTarget {
8
+ slug: string;
9
+ id: string;
10
+ global: boolean;
11
+ revision?: number;
12
+ locale?: string;
13
+ }
14
+
15
+ export type VersionComparisonState =
16
+ | { revision: number; status: "loading" }
17
+ | { revision: number; status: "ready"; version: AdminVersion }
18
+ | { revision: number; status: "error"; error: string };
19
+
20
+ /** One visit owns its reads and restore result, including when the route component is retained. */
21
+ export class VersionHistoryController {
22
+ #runtime: AdminRuntime;
23
+ #notifications: NotificationCenter;
24
+ #target?: VersionHistoryTarget;
25
+ #loadRequest?: AbortController;
26
+ #comparisonRequest?: AbortController;
27
+ #comparisonSummary?: AdminVersion;
28
+ #visit = 0;
29
+ #key = "";
30
+ #history = $state.raw<AdminVersion[]>([]);
31
+ #detail = $state.raw<AdminVersion>();
32
+ #document = $state.raw<AdminDocument>();
33
+ #operations = $state.raw<OperationCapabilities>();
34
+ #loading = $state(true);
35
+ #error = $state<string>();
36
+ #failureStatus = $state<number>();
37
+ #restoring = $state(false);
38
+ #comparisonState = $state.raw<VersionComparisonState>();
39
+
40
+ constructor(runtime: AdminRuntime, notifications: NotificationCenter) {
41
+ this.#runtime = runtime;
42
+ this.#notifications = notifications;
43
+ }
44
+
45
+ get versions() {
46
+ return this.#history;
47
+ }
48
+ get selectedVersion() {
49
+ return this.#detail;
50
+ }
51
+ get document() {
52
+ return this.#document;
53
+ }
54
+ get loading() {
55
+ return this.#loading;
56
+ }
57
+ get error() {
58
+ return this.#error;
59
+ }
60
+ get canRetry() {
61
+ return ![401, 403, 404].includes(this.#failureStatus ?? 0);
62
+ }
63
+ get restoring() {
64
+ return this.#restoring;
65
+ }
66
+ get comparisonState() {
67
+ return this.#comparisonState;
68
+ }
69
+
70
+ sync = (
71
+ target: VersionHistoryTarget,
72
+ manifestRevision: number,
73
+ prepared?: AdminVersionsDataV1
74
+ ) => {
75
+ const key = JSON.stringify([target, manifestRevision]);
76
+ if (key === this.#key) return;
77
+ this.#key = key;
78
+ this.#target = target;
79
+ this.#visit++;
80
+ this.#restoring = false;
81
+ this.#loadRequest?.abort();
82
+ this.#clear();
83
+
84
+ if (prepared) {
85
+ const { history, detail, document } = prepared;
86
+ const detailMissing = detail?.error?.code === "not_found";
87
+ const failure =
88
+ history.error ??
89
+ (detailMissing ? undefined : detail?.error) ??
90
+ document.document.error ??
91
+ document.access.error;
92
+ if (failure) {
93
+ this.#error = failure.message;
94
+ this.#failureStatus = failure.status;
95
+ this.#loading = false;
96
+ return;
97
+ }
98
+ if (
99
+ history.value &&
100
+ document.document.value &&
101
+ document.access.value &&
102
+ (target.revision === undefined || detail?.value || detailMissing)
103
+ ) {
104
+ this.#apply(
105
+ history.value,
106
+ detail?.value,
107
+ document.document.value,
108
+ document.access.value.operations
109
+ );
110
+ return;
111
+ }
112
+ }
113
+ this.#load(target);
114
+ };
115
+
116
+ retry = () => {
117
+ if (this.#target && !this.#restoring) this.#load(this.#target);
118
+ };
119
+
120
+ syncComparison = async (summary: AdminVersion | undefined) => {
121
+ const target = this.#target;
122
+ if (!summary || !target || summary.Revision === this.#detail?.Revision) {
123
+ this.#clearComparison();
124
+ return;
125
+ }
126
+ if (this.#comparisonState?.revision === summary.Revision) return;
127
+
128
+ this.#comparisonRequest?.abort();
129
+ this.#comparisonSummary = summary;
130
+ if (!this.#runtime.contentLocales.length) {
131
+ this.#comparisonState = { revision: summary.Revision, status: "ready", version: summary };
132
+ this.#comparisonRequest = undefined;
133
+ return;
134
+ }
135
+
136
+ this.#comparisonState = { revision: summary.Revision, status: "loading" };
137
+ const request = new AbortController();
138
+ this.#comparisonRequest = request;
139
+ const options = { signal: request.signal, locale: "all" };
140
+ try {
141
+ const version = target.global
142
+ ? await this.#runtime.client.globalVersion(target.slug, summary.Revision, options)
143
+ : await this.#runtime.client.version(target.slug, target.id, summary.Revision, options);
144
+ if (!request.signal.aborted)
145
+ this.#comparisonState = { revision: summary.Revision, status: "ready", version };
146
+ } catch (cause) {
147
+ if (!request.signal.aborted)
148
+ this.#comparisonState = {
149
+ revision: summary.Revision,
150
+ status: "error",
151
+ error:
152
+ cause instanceof Error
153
+ ? cause.message
154
+ : this.#runtime.i18n.t("versions:historyLoadFailed"),
155
+ };
156
+ } finally {
157
+ if (this.#comparisonRequest === request) this.#comparisonRequest = undefined;
158
+ }
159
+ };
160
+
161
+ retryComparison = async () => {
162
+ const state = this.#comparisonState;
163
+ const summary = this.#comparisonSummary;
164
+ if (state?.status !== "error" || !summary) return;
165
+ this.#comparisonState = undefined;
166
+ await this.syncComparison(summary);
167
+ };
168
+
169
+ async #load(target: VersionHistoryTarget) {
170
+ this.#loadRequest?.abort();
171
+ const request = new AbortController();
172
+ this.#loadRequest = request;
173
+ this.#loading = true;
174
+ this.#error = undefined;
175
+ const { client, i18n } = this.#runtime;
176
+ const { slug, id, global, revision, locale } = target;
177
+ const historyOptions = {
178
+ signal: request.signal,
179
+ locale,
180
+ };
181
+ const detailOptions = {
182
+ signal: request.signal,
183
+ locale: this.#runtime.contentLocales.length ? "all" : undefined,
184
+ };
185
+ const detailRequest =
186
+ revision === undefined
187
+ ? Promise.resolve(undefined)
188
+ : (global
189
+ ? client.globalVersion(slug, revision, detailOptions)
190
+ : client.version(slug, id, revision, detailOptions)
191
+ ).catch((cause) => {
192
+ if (cause instanceof RiduError && cause.code === "not_found") return undefined;
193
+ throw cause;
194
+ });
195
+ try {
196
+ const [history, detail, document, access] = await Promise.all([
197
+ global
198
+ ? client.globalVersions(slug, historyOptions)
199
+ : client.versions(slug, id, historyOptions),
200
+ detailRequest,
201
+ global
202
+ ? client.global(slug, { signal: request.signal, locale })
203
+ : client.find(slug, id, { signal: request.signal, locale }),
204
+ global
205
+ ? client.globalAccess(slug, { signal: request.signal, locale })
206
+ : client.collectionAccess(slug, { id, signal: request.signal, locale }),
207
+ ]);
208
+ if (request.signal.aborted) return;
209
+ this.#apply(history, detail, document, access.operations);
210
+ } catch (cause) {
211
+ if (request.signal.aborted) return;
212
+ this.#clear();
213
+ this.#failureStatus = cause instanceof RiduError ? cause.status : undefined;
214
+ this.#error = cause instanceof Error ? cause.message : i18n.t("versions:historyLoadFailed");
215
+ } finally {
216
+ if (!request.signal.aborted) this.#loading = false;
217
+ }
218
+ }
219
+
220
+ #clear() {
221
+ this.#clearComparison();
222
+ this.#history = [];
223
+ this.#detail = undefined;
224
+ this.#document = undefined;
225
+ this.#operations = undefined;
226
+ this.#error = undefined;
227
+ this.#failureStatus = undefined;
228
+ this.#loading = true;
229
+ }
230
+
231
+ #clearComparison() {
232
+ this.#comparisonRequest?.abort();
233
+ this.#comparisonRequest = undefined;
234
+ this.#comparisonSummary = undefined;
235
+ this.#comparisonState = undefined;
236
+ }
237
+
238
+ #apply(
239
+ history: AdminVersion[],
240
+ detail: AdminVersion | undefined,
241
+ document: AdminDocument,
242
+ operations: OperationCapabilities
243
+ ) {
244
+ // Detail is an exact authorized read, not a fallback to a potentially stale list snapshot.
245
+ this.#detail = detail;
246
+ this.#history = [...history].sort((a, b) => b.Revision - a.Revision);
247
+ this.#document = document;
248
+ this.#operations = operations;
249
+ this.#loading = false;
250
+ }
251
+
252
+ canRestore(draft: boolean) {
253
+ const revision = this.#document?._revision;
254
+ const operation = draft || this.#detail?.Status === "draft" ? "unpublish" : "publish";
255
+ return (
256
+ Number.isInteger(revision) && Number(revision) > 0 && this.#operations?.[operation] === true
257
+ );
258
+ }
259
+
260
+ restore = async (draft: boolean, onRestored: () => void) => {
261
+ const target = this.#target;
262
+ const selected = this.#detail;
263
+ const document = this.#document;
264
+ if (
265
+ !target ||
266
+ !selected ||
267
+ !document ||
268
+ this.#loading ||
269
+ this.#restoring ||
270
+ !this.canRestore(draft)
271
+ )
272
+ return;
273
+ const { slug, id, global, locale } = target;
274
+ const visit = this.#visit;
275
+ const { client, i18n } = this.#runtime;
276
+ this.#restoring = true;
277
+ this.#loadRequest?.abort();
278
+ try {
279
+ const options = {
280
+ revision: document._revision as number,
281
+ draft,
282
+ locale,
283
+ };
284
+ if (global) await client.restoreGlobal(slug, selected.Revision, options);
285
+ else await client.restore(slug, id, selected.Revision, options);
286
+ if (visit !== this.#visit) return;
287
+ this.#runtime.documentsChanged();
288
+ this.#notifications.success({
289
+ title: i18n.t(draft ? "versions:revisionRestoredAsDraft" : "versions:revisionRestored", {
290
+ revision: i18n.formatNumber(selected.Revision),
291
+ }),
292
+ message: i18n.t("versions:previousCurrentPreserved"),
293
+ });
294
+ onRestored();
295
+ } catch (cause) {
296
+ if (visit !== this.#visit) return;
297
+ this.#notifications.error({
298
+ title: i18n.t("versions:revisionNotRestored"),
299
+ message: cause instanceof Error ? cause.message : i18n.t("versions:revisionRestoreFailed"),
300
+ });
301
+ } finally {
302
+ if (visit === this.#visit) this.#restoring = false;
303
+ }
304
+ };
305
+
306
+ dispose = () => {
307
+ this.#visit++;
308
+ this.#loadRequest?.abort();
309
+ this.#comparisonRequest?.abort();
310
+ };
311
+ }