@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,4 +1,13 @@
1
- import type { AccessCapabilitiesEnvelope, Pagination, SchemaSelectOption } from "@riducms/protocol";
1
+ import { documentLabel } from "@admin/features/documents/document-title";
2
+ import { untrack } from "svelte";
3
+ import { RiduError } from "@riducms/sdk";
4
+ import type {
5
+ AccessCapabilitiesEnvelope,
6
+ AdminCollectionListDataV1,
7
+ CollectionPageEnvelope,
8
+ Pagination,
9
+ SchemaCollection,
10
+ } from "@riducms/protocol";
2
11
  import type { AdminI18n } from "@riducms/translations";
3
12
 
4
13
  import type { AdminClient, AdminDocument } from "@admin/core/api/admin-client";
@@ -10,46 +19,38 @@ interface CollectionListControllerOptions {
10
19
  i18n: AdminI18n;
11
20
  get slug(): string;
12
21
  get locale(): string | undefined;
13
- get page(): number;
14
- get search(): string;
15
- get titleField(): string | undefined;
16
- get status(): string;
17
- get statusField(): string | undefined;
18
- get statusOptions(): readonly SchemaSelectOption[];
19
- get versioned(): boolean;
22
+ get query(): string;
23
+ get ready(): boolean;
24
+ get prepared(): AdminCollectionListDataV1 | undefined;
25
+ get collection(): SchemaCollection | undefined;
20
26
  get trashOnly(): boolean;
21
- get folderField(): string | undefined;
22
- get folderID(): string;
23
- get hierarchy(): boolean;
24
- get limit(): number;
25
- get sort(): readonly string[];
26
- get filterWhere(): readonly Where[];
27
27
  }
28
28
 
29
29
  interface CollectionListRequest {
30
30
  slug: string;
31
31
  locale: string | undefined;
32
- page: number;
33
- limit: number;
34
- search: string;
35
- titleField: string | undefined;
36
- status: string;
37
- statusField: string | undefined;
38
- statusValues: readonly string[];
32
+ query: string;
39
33
  trashOnly: boolean;
40
- folderField: string | undefined;
41
- folderID: string;
42
- hierarchy: boolean;
43
- sort: readonly string[];
44
- filterWhere: readonly Where[];
34
+ }
35
+
36
+ interface CollectionListMutation {
37
+ request: CollectionListRequest;
38
+ collection: SchemaCollection | undefined;
39
+ }
40
+
41
+ export interface CollectionBulkUpdateTarget {
42
+ readonly ids: readonly string[];
43
+ readonly slug: string;
44
+ readonly locale: string | undefined;
45
+ readonly collection: SchemaCollection | undefined;
46
+ readonly accessIdentity: Readonly<Record<string, AccessCapabilitiesEnvelope>>;
45
47
  }
46
48
 
47
49
  type CollectionListStatus = "initial" | "loading" | "ready" | "failed";
48
- type Where = Record<string, unknown>;
49
50
 
50
51
  const emptyPagination: Pagination = {
51
52
  page: 1,
52
- limit: 25,
53
+ limit: 10,
53
54
  totalDocs: 0,
54
55
  totalPages: 0,
55
56
  hasNextPage: false,
@@ -59,68 +60,106 @@ const emptyPagination: Pagination = {
59
60
  const maxAtomicSelection = 100;
60
61
 
61
62
  export class CollectionListController {
62
- showStatus = $state(true);
63
- showUpdated = $state(true);
64
63
  selectedIDs = $state.raw<Set<string>>(new Set());
65
64
  #status = $state<CollectionListStatus>("initial");
66
65
  #docs = $state.raw<AdminDocument[]>([]);
67
66
  #pagination = $state.raw<Pagination>(emptyPagination);
68
- #statusCounts = $state.raw<Record<string, number>>({});
69
67
  #collectionAccess = $state.raw<AccessCapabilitiesEnvelope>();
70
68
  #documentAccess = $state.raw<Record<string, AccessCapabilitiesEnvelope>>({});
71
- #error = $state<string>();
69
+ #pageError = $state<string>();
72
70
  #request?: AbortController;
73
71
  #selectionRequest?: AbortController;
74
72
  #requestGeneration = 0;
73
+ #mutation?: CollectionListMutation;
74
+ #disposed = false;
75
75
  #current?: CollectionListRequest;
76
+ #selectionWhere?: Record<string, unknown>;
77
+ #collection?: SchemaCollection;
78
+ #prepared?: AdminCollectionListDataV1;
76
79
  #desiredRequestKey = $state("");
77
- #desiredSelectionQueryKey = $state("");
78
80
  #loadedRequestKey = $state<string>();
79
- #loadedSelectionQueryKey = $state<string>();
80
- mutationPending = $state.raw<Set<string>>(new Set());
81
+ #loadedSelectionQueryKey?: string;
81
82
  bulkPending = $state(false);
82
83
  selectionPending = $state(false);
83
84
 
84
85
  constructor(readonly options: CollectionListControllerOptions) {
85
- $effect(() => {
86
- const request: CollectionListRequest = {
87
- slug: this.options.slug,
88
- locale: this.options.locale,
89
- page: this.options.hierarchy ? 1 : this.options.page,
90
- limit: this.options.hierarchy ? 100 : this.options.limit,
91
- search: this.options.search,
92
- titleField: this.options.titleField,
93
- status: this.options.status,
94
- statusField: this.options.statusField,
95
- statusValues: this.options.statusOptions.map((option) => option.value),
96
- trashOnly: this.options.trashOnly,
97
- folderField: this.options.folderField,
98
- folderID: this.options.folderID,
99
- hierarchy: this.options.hierarchy,
100
- sort: this.options.sort,
101
- filterWhere: this.options.filterWhere,
102
- };
103
- if (request.slug === "") return;
86
+ $effect.pre(() => {
87
+ if (this.#disposed) return;
88
+ const request = this.#requestFromOptions();
89
+ const collection = this.options.collection;
90
+ // Revoke an outgoing mutation even while the next route's preferences are loading.
91
+ if (
92
+ this.#mutation !== undefined &&
93
+ (requestKey(this.#mutation.request) !== requestKey(request) ||
94
+ this.#mutation.collection !== collection)
95
+ )
96
+ this.#invalidateMutation();
104
97
  const previous = this.#current;
105
- if (previous !== undefined && selectionQueryKey(previous) !== selectionQueryKey(request)) {
106
- this.clearSelection();
107
- }
108
- if (previous !== undefined && requestKey(previous) !== requestKey(request)) {
98
+ const schemaChanged = collection !== this.#collection;
99
+ if (
100
+ previous !== undefined &&
101
+ (schemaChanged || requestKey(previous) !== requestKey(request))
102
+ ) {
109
103
  this.#requestGeneration += 1;
110
104
  this.#request?.abort();
111
105
  this.#request = undefined;
106
+ this.#cancelSelectionRequest();
107
+ }
108
+ if (!this.options.ready || request.slug === "") return;
109
+ this.#collection = collection;
110
+ if (schemaChanged) {
111
+ this.clearSelection();
112
+ this.#loadedRequestKey = undefined;
112
113
  }
113
114
  this.#current = request;
114
115
  this.#desiredRequestKey = requestKey(request);
115
- this.#desiredSelectionQueryKey = selectionQueryKey(request);
116
- const delay = request.search.trim().length > 0 ? 180 : 0;
117
- const timer = window.setTimeout(() => this.#open(request), delay);
116
+ const prepared = this.options.prepared;
117
+ if (prepared !== undefined && prepared !== this.#prepared) {
118
+ this.#prepared = prepared;
119
+ // Merging selected-row access must not turn selection into a route dependency.
120
+ untrack(() => this.#adoptPrepared(request, prepared));
121
+ return;
122
+ }
123
+ const search = new URLSearchParams(request.query).get("q") ?? "";
124
+ const searchChanged =
125
+ previous !== undefined && new URLSearchParams(previous.query).get("q") !== search;
126
+ const delay = searchChanged && search.trim().length > 0 ? 180 : 0;
127
+ const timer = window.setTimeout(async () => {
128
+ if (this.#loadedRequestKey !== requestKey(request)) await this.#open(request);
129
+ }, delay);
118
130
  return () => window.clearTimeout(timer);
119
131
  });
120
-
121
132
  $effect(() => () => this.dispose());
122
133
  }
123
134
 
135
+ #requestFromOptions(): CollectionListRequest {
136
+ return {
137
+ slug: this.options.slug,
138
+ locale: this.options.locale,
139
+ query: this.options.query,
140
+ trashOnly: this.options.trashOnly,
141
+ };
142
+ }
143
+
144
+ #adoptPrepared(request: CollectionListRequest, data: AdminCollectionListDataV1) {
145
+ this.#pageError = undefined;
146
+ this.#applyListPage(request, data);
147
+ }
148
+
149
+ #applyListPage(request: CollectionListRequest, data: AdminCollectionListDataV1) {
150
+ const key = selectionQueryKey(request, data.query);
151
+ if (this.#loadedSelectionQueryKey !== key) this.clearSelection();
152
+ const page = data.page;
153
+ if (page === undefined) throw new Error("Collection list response omitted its page");
154
+ if (page.error !== undefined) {
155
+ this.#failPage(new RiduError(page.error));
156
+ } else {
157
+ this.#selectionWhere = data.query.where;
158
+ this.#applyPage(request, page.value);
159
+ this.#loadedSelectionQueryKey = key;
160
+ }
161
+ }
162
+
124
163
  get status() {
125
164
  return this.#status;
126
165
  }
@@ -133,10 +172,6 @@ export class CollectionListController {
133
172
  return this.#pagination;
134
173
  }
135
174
 
136
- get statusCounts() {
137
- return this.#statusCounts;
138
- }
139
-
140
175
  get canCreate() {
141
176
  return this.#collectionAccess?.operations.create === true;
142
177
  }
@@ -145,6 +180,10 @@ export class CollectionListController {
145
180
  return this.#selectedAllow("update");
146
181
  }
147
182
 
183
+ canBulkUpdateField = (path: string) =>
184
+ this.canBulkUpdate &&
185
+ [...this.selectedIDs].every((id) => this.#documentAccess[id]?.fields[path]?.update !== false);
186
+
148
187
  get canBulkPublish() {
149
188
  return this.#selectedAllow("publish");
150
189
  }
@@ -165,11 +204,6 @@ export class CollectionListController {
165
204
  return this.#selectedAllow("deletePermanent");
166
205
  }
167
206
 
168
- canRestore = (id: string) => this.#documentAccess[id]?.operations.restoreDeleted === true;
169
-
170
- canDeletePermanent = (id: string) =>
171
- this.#documentAccess[id]?.operations.deletePermanent === true;
172
-
173
207
  canReadField = (path: string, id?: string) => {
174
208
  const access = id === undefined ? this.#collectionAccess : this.#documentAccess[id];
175
209
  if (access?.operations.read !== true) return false;
@@ -177,31 +211,7 @@ export class CollectionListController {
177
211
  };
178
212
 
179
213
  get error() {
180
- return this.#error;
181
- }
182
-
183
- get statusColumnAvailable() {
184
- return this.options.statusField !== undefined || this.options.versioned;
185
- }
186
-
187
- get statusVisible() {
188
- return this.showStatus && this.statusColumnAvailable;
189
- }
190
-
191
- get allDocuments() {
192
- return this.options.statusField === undefined
193
- ? this.pagination.totalDocs
194
- : (this.statusCounts[""] ?? this.pagination.totalDocs);
195
- }
196
-
197
- get rangeStart() {
198
- return this.pagination.totalDocs === 0
199
- ? 0
200
- : (this.pagination.page - 1) * this.pagination.limit + 1;
201
- }
202
-
203
- get rangeEnd() {
204
- return Math.min(this.pagination.page * this.pagination.limit, this.pagination.totalDocs);
214
+ return this.#pageError;
205
215
  }
206
216
 
207
217
  get selectedOnPage() {
@@ -223,7 +233,7 @@ export class CollectionListController {
223
233
  return (
224
234
  this.selectionControlsReady &&
225
235
  this.#collectionAccess?.operations.selectAll === true &&
226
- this.allOnPageSelected &&
236
+ this.selectedIDs.size > 0 &&
227
237
  this.selectedIDs.size < this.pagination.totalDocs
228
238
  );
229
239
  }
@@ -232,8 +242,9 @@ export class CollectionListController {
232
242
  return (
233
243
  this.selectionControlsReady &&
234
244
  this.#collectionAccess?.operations.selectAll === true &&
235
- this.allOnPageSelected &&
236
- this.pagination.totalDocs > this.docs.length
245
+ this.selectedIDs.size > 0 &&
246
+ (this.pagination.totalDocs > this.selectedIDs.size ||
247
+ this.selectedIDs.size > this.docs.length)
237
248
  );
238
249
  }
239
250
 
@@ -241,62 +252,17 @@ export class CollectionListController {
241
252
  return this.#desiredRequestKey !== "" && this.#loadedRequestKey === this.#desiredRequestKey;
242
253
  }
243
254
 
244
- get selectionQueryReady() {
245
- return (
246
- this.#desiredSelectionQueryKey !== "" &&
247
- this.#loadedSelectionQueryKey === this.#desiredSelectionQueryKey
248
- );
249
- }
250
-
251
255
  get selectionLimitExceeded() {
252
256
  return this.pagination.totalDocs > maxAtomicSelection;
253
257
  }
254
258
 
255
- retry = async () => {
256
- if (this.#current !== undefined) await this.#open(this.#current);
257
- };
258
-
259
- setShowStatus = (show: boolean) => {
260
- this.showStatus = show;
261
- };
262
-
263
- setShowUpdated = (show: boolean) => {
264
- this.showUpdated = show;
265
- };
266
-
267
- title = (document: AdminDocument) =>
268
- String(
269
- (this.options.titleField === undefined ? undefined : document[this.options.titleField]) ??
270
- document.id
271
- );
272
-
273
- documentStatus = (document: AdminDocument) => {
274
- if (typeof document._status === "string") return document._status;
275
- if (this.options.statusField === undefined) return undefined;
276
- const value = document[this.options.statusField];
277
- return typeof value === "string" ? value : undefined;
278
- };
279
-
280
- statusLabel = (value: string) => {
281
- const configured = this.options.statusOptions.find((option) => option.value === value)?.label;
282
- if (configured !== undefined) return configured;
283
- const normalized = value.toLocaleLowerCase(this.options.i18n.language);
284
- if (normalized === "published") return this.options.i18n.t("documents:published");
285
- if (normalized === "draft") return this.options.i18n.t("documents:draft");
286
- return humanize(value, this.options.i18n.language);
287
- };
288
-
289
- statusTone = (value: string) => {
290
- const normalized = value.toLocaleLowerCase(this.options.i18n.language);
291
- if (normalized === "published" || normalized === "live") return "live";
292
- if (normalized === "draft") return "muted";
293
- return "warning";
294
- };
259
+ get selectionLimit() {
260
+ return maxAtomicSelection;
261
+ }
295
262
 
296
- updated = (document: AdminDocument) => formatUpdated(document.updatedAt, this.options.i18n);
297
- created = (document: AdminDocument) => formatUpdated(document.createdAt, this.options.i18n);
263
+ retry = () => (this.#current === undefined ? Promise.resolve() : this.#open(this.#current));
298
264
 
299
- optionCount = (option: SchemaSelectOption) => this.statusCounts[option.value];
265
+ title = (document: AdminDocument) => documentLabel(this.options.collection, document);
300
266
 
301
267
  toggleDocument = (id: string, checked: boolean) => {
302
268
  if (!this.selectionControlsReady || !this.docs.some((document) => document.id === id)) return;
@@ -332,14 +298,14 @@ export class CollectionListController {
332
298
  this.selectionPending
333
299
  )
334
300
  return;
335
- const selectionKey = selectionQueryKey(request);
301
+ const selectionKey = this.#loadedSelectionQueryKey;
336
302
  this.#selectionRequest?.abort();
337
303
  const activeRequest = new AbortController();
338
304
  this.#selectionRequest = activeRequest;
339
305
  this.selectionPending = true;
340
306
  try {
341
307
  const selection = await this.options.client.resolveFilteredSelection(request.slug, {
342
- where: buildWhere(request),
308
+ where: this.#selectionWhere,
343
309
  trash: request.trashOnly,
344
310
  locale: request.locale,
345
311
  signal: activeRequest.signal,
@@ -347,7 +313,7 @@ export class CollectionListController {
347
313
  if (
348
314
  activeRequest.signal.aborted ||
349
315
  this.#current === undefined ||
350
- selectionKey !== selectionQueryKey(this.#current)
316
+ selectionKey !== this.#loadedSelectionQueryKey
351
317
  )
352
318
  return;
353
319
  this.selectedIDs = new Set(selection.items.map((item) => item.id));
@@ -371,43 +337,41 @@ export class CollectionListController {
371
337
  }
372
338
  };
373
339
 
374
- restore = async (id: string) => {
375
- await this.#mutate(id, "restore");
376
- };
377
-
378
- deletePermanent = async (id: string) => {
379
- await this.#mutate(id, "permanent");
380
- };
381
-
382
- bulkUpdate = async (data: Record<string, unknown>) => {
383
- return this.#bulk("update", data);
384
- };
385
-
386
- bulkPublish = async () => {
387
- await this.#bulk("publish");
388
- };
389
-
390
- bulkUnpublish = async () => {
391
- await this.#bulk("unpublish");
392
- };
393
-
394
- bulkDelete = async () => {
395
- await this.#bulk("delete");
396
- };
397
-
398
- bulkRestore = async () => {
399
- await this.#bulk("restoreDeleted");
400
- };
340
+ captureBulkUpdateTarget = (
341
+ ids: readonly string[] = [...this.selectedIDs]
342
+ ): CollectionBulkUpdateTarget => ({
343
+ ids: [...ids],
344
+ slug: this.options.slug,
345
+ locale: this.options.locale,
346
+ collection: this.options.collection,
347
+ accessIdentity: this.#documentAccess,
348
+ });
401
349
 
402
- bulkDeletePermanent = async () => {
403
- await this.#bulk("deletePermanent");
350
+ bulkUpdate = async (target: CollectionBulkUpdateTarget, data: Record<string, unknown>) => {
351
+ if (
352
+ target.slug !== this.options.slug ||
353
+ target.locale !== this.options.locale ||
354
+ target.collection !== this.options.collection ||
355
+ target.accessIdentity !== this.#documentAccess
356
+ )
357
+ return false;
358
+ return this.#bulk("update", data, { ids: target.ids, surfaceIssues: true });
404
359
  };
360
+ bulkPublish = () => this.#bulk("publish");
361
+ bulkUnpublish = () => this.#bulk("unpublish");
362
+ bulkDelete = () => this.#bulk("delete");
363
+ bulkRestore = () => this.#bulk("restoreDeleted");
364
+ bulkDeletePermanent = () => this.#bulk("deletePermanent");
405
365
 
406
366
  emptyTrash = async () => {
407
- if (this.bulkPending) return;
408
- this.bulkPending = true;
367
+ const mutation = this.#beginMutation();
368
+ if (mutation === undefined) return;
369
+ const { request } = mutation;
409
370
  try {
410
- const documents = await this.options.client.emptyTrash(this.options.slug);
371
+ const documents = await this.options.client.emptyTrash(request.slug, {
372
+ locale: request.locale,
373
+ });
374
+ if (!this.#ownsMutation(mutation)) return;
411
375
  this.clearSelection();
412
376
  this.options.notifications.success({
413
377
  title: this.options.i18n.t("collections:documentsPermanentlyDeleted", {
@@ -415,8 +379,9 @@ export class CollectionListController {
415
379
  formattedCount: this.options.i18n.formatNumber(documents.length),
416
380
  }),
417
381
  });
418
- if (this.#current !== undefined) await this.#open(this.#current);
382
+ await this.#open(request);
419
383
  } catch (cause) {
384
+ if (!this.#ownsMutation(mutation)) return;
420
385
  this.options.notifications.error({
421
386
  title: this.options.i18n.t("collections:trashNotEmptied"),
422
387
  message:
@@ -425,11 +390,13 @@ export class CollectionListController {
425
390
  : this.options.i18n.t("collections:noDocumentsChanged"),
426
391
  });
427
392
  } finally {
428
- this.bulkPending = false;
393
+ if (this.#mutation === mutation) this.#invalidateMutation();
429
394
  }
430
395
  };
431
396
 
432
397
  dispose() {
398
+ this.#disposed = true;
399
+ this.#invalidateMutation();
433
400
  this.#requestGeneration += 1;
434
401
  this.#request?.abort();
435
402
  this.#request = undefined;
@@ -443,180 +410,152 @@ export class CollectionListController {
443
410
  }
444
411
 
445
412
  #open = async (request: CollectionListRequest) => {
446
- if (
447
- this.#current !== undefined &&
448
- selectionQueryKey(this.#current) !== selectionQueryKey(request)
449
- ) {
450
- this.clearSelection();
451
- }
413
+ if (this.#disposed) return;
452
414
  this.#current = request;
453
415
  this.#request?.abort();
454
416
  const activeRequest = new AbortController();
455
417
  const generation = ++this.#requestGeneration;
456
418
  this.#request = activeRequest;
457
419
  this.#status = "loading";
458
- this.#error = undefined;
420
+ this.#pageError = undefined;
459
421
 
460
422
  try {
461
- const [page, statusCounts, collectionAccess] = await Promise.all([
462
- this.options.client.list(request.slug, {
463
- page: request.page,
464
- limit: request.limit,
465
- where: buildWhere(request),
466
- sort: request.sort,
467
- trash: request.trashOnly,
468
- locale: request.locale,
469
- signal: activeRequest.signal,
470
- }),
471
- this.#loadStatusCounts(request, activeRequest.signal),
472
- this.options.client.collectionAccess(request.slug, {
473
- trash: request.trashOnly,
474
- locale: request.locale,
475
- signal: activeRequest.signal,
476
- }),
477
- ]);
478
- const documentAccessEntries = await Promise.all(
479
- page.docs.map(async (document) => {
480
- try {
481
- return [
482
- document.id,
483
- await this.options.client.collectionAccess(request.slug, {
484
- id: document.id,
485
- trash: request.trashOnly,
486
- locale: request.locale,
487
- signal: activeRequest.signal,
488
- }),
489
- ] as const;
490
- } catch {
491
- return undefined;
492
- }
493
- })
423
+ const data = await this.options.client.adminCollectionList(
424
+ request.slug,
425
+ request.query,
426
+ "page",
427
+ { signal: activeRequest.signal }
494
428
  );
495
429
  if (activeRequest.signal.aborted || generation !== this.#requestGeneration) return;
496
- this.#docs = page.docs;
497
- this.#pagination = page.pagination;
498
- this.#statusCounts = statusCounts;
499
- this.#collectionAccess = collectionAccess;
500
- const visibleAccess = Object.fromEntries(
501
- documentAccessEntries.filter((entry) => entry !== undefined)
502
- );
503
- this.#documentAccess = {
504
- ...Object.fromEntries(
505
- Object.entries(this.#documentAccess).filter(([id]) => this.selectedIDs.has(id))
506
- ),
507
- ...visibleAccess,
508
- };
509
- this.#loadedRequestKey = requestKey(request);
510
- this.#loadedSelectionQueryKey = selectionQueryKey(request);
511
- this.#status = "ready";
430
+ this.#applyListPage(request, data);
512
431
  } catch (cause) {
513
432
  if (activeRequest.signal.aborted || generation !== this.#requestGeneration) return;
514
- this.#error =
515
- cause instanceof Error
516
- ? cause.message
517
- : this.options.i18n.t("collections:documentsLoadFailed");
518
- this.#status = "failed";
433
+ this.#failPage(cause);
519
434
  } finally {
520
435
  if (this.#request === activeRequest) this.#request = undefined;
521
436
  }
522
437
  };
523
438
 
524
- async #loadStatusCounts(request: CollectionListRequest, signal: AbortSignal) {
525
- if (request.statusField === undefined || request.statusValues.length === 0) return {};
526
- const entries = await Promise.all([
527
- this.options.client
528
- .count(request.slug, {
529
- where: buildWhere(request, ""),
530
- trash: request.trashOnly,
531
- locale: request.locale,
532
- signal,
533
- })
534
- .then((result) => ["", result.totalDocs] as const),
535
- ...request.statusValues.map(async (status) => {
536
- const result = await this.options.client.count(request.slug, {
537
- where: buildWhere(request, status),
538
- trash: request.trashOnly,
539
- locale: request.locale,
540
- signal,
541
- });
542
- return [status, result.totalDocs] as const;
543
- }),
544
- ]);
545
- return Object.fromEntries(entries);
439
+ #applyPage(request: CollectionListRequest, page: CollectionPageEnvelope<AdminDocument>) {
440
+ this.#docs = page.docs;
441
+ this.#pagination = page.pagination;
442
+ this.#collectionAccess = page.access.collection;
443
+ const visibleIDs = new Set(page.docs.map((document) => document.id));
444
+ this.#documentAccess = {
445
+ ...Object.fromEntries(
446
+ Object.entries(this.#documentAccess).filter(
447
+ ([id]) => this.selectedIDs.has(id) && !visibleIDs.has(id)
448
+ )
449
+ ),
450
+ ...page.access.documents,
451
+ };
452
+ this.#loadedRequestKey = requestKey(request);
453
+ this.#status = "ready";
454
+ }
455
+
456
+ #failPage(cause: unknown) {
457
+ const visibleIDs = new Set(this.#docs.map((document) => document.id));
458
+ this.#collectionAccess = undefined;
459
+ this.#documentAccess = Object.fromEntries(
460
+ Object.entries(this.#documentAccess).filter(
461
+ ([id]) => this.selectedIDs.has(id) && !visibleIDs.has(id)
462
+ )
463
+ );
464
+ this.#pageError =
465
+ cause instanceof Error
466
+ ? cause.message
467
+ : this.options.i18n.t("collections:documentsLoadFailed");
468
+ this.#status = "failed";
469
+ }
470
+
471
+ #beginMutation() {
472
+ if (this.#disposed || this.bulkPending || !this.options.ready) return;
473
+ const request = this.#requestFromOptions();
474
+ const collection = this.options.collection;
475
+ if (
476
+ this.#current === undefined ||
477
+ requestKey(this.#current) !== requestKey(request) ||
478
+ this.#collection !== collection
479
+ )
480
+ return;
481
+ const mutation = { request, collection };
482
+ this.#mutation = mutation;
483
+ this.bulkPending = true;
484
+ return mutation;
485
+ }
486
+
487
+ #ownsMutation(mutation: CollectionListMutation) {
488
+ return (
489
+ !this.#disposed &&
490
+ this.#mutation === mutation &&
491
+ requestKey(mutation.request) === requestKey(this.#requestFromOptions()) &&
492
+ mutation.collection === this.options.collection
493
+ );
494
+ }
495
+
496
+ #invalidateMutation() {
497
+ this.#mutation = undefined;
498
+ this.bulkPending = false;
546
499
  }
547
500
 
548
501
  #selectedAllow(
549
502
  operation: "update" | "delete" | "publish" | "unpublish" | "restoreDeleted" | "deletePermanent"
550
503
  ) {
551
- return (
552
- this.selectedIDs.size > 0 &&
553
- [...this.selectedIDs].every((id) => this.#documentAccess[id]?.operations[operation] === true)
554
- );
504
+ return this.#idsAllow([...this.selectedIDs], operation);
555
505
  }
556
506
 
557
- async #mutate(id: string, action: "restore" | "permanent") {
558
- if (this.mutationPending.has(id)) return;
559
- this.mutationPending = new Set(this.mutationPending).add(id);
560
- try {
561
- if (action === "restore") await this.options.client.restoreDeleted(this.options.slug, id);
562
- else await this.options.client.deletePermanent(this.options.slug, id);
563
- this.options.notifications.success({
564
- title:
565
- action === "restore"
566
- ? this.options.i18n.t("collections:documentRestored")
567
- : this.options.i18n.t("collections:documentPermanentlyDeleted"),
568
- });
569
- if (this.#current !== undefined) await this.#open(this.#current);
570
- } catch (cause) {
571
- this.options.notifications.error({
572
- title:
573
- action === "restore"
574
- ? this.options.i18n.t("collections:documentNotRestored")
575
- : this.options.i18n.t("collections:documentNotDeleted"),
576
- message:
577
- cause instanceof Error
578
- ? cause.message
579
- : this.options.i18n.t("collections:operationFailed"),
580
- });
581
- } finally {
582
- const pending = new Set(this.mutationPending);
583
- pending.delete(id);
584
- this.mutationPending = pending;
585
- }
507
+ #idsAllow(
508
+ ids: readonly string[],
509
+ operation: "update" | "delete" | "publish" | "unpublish" | "restoreDeleted" | "deletePermanent"
510
+ ) {
511
+ return (
512
+ ids.length > 0 && ids.every((id) => this.#documentAccess[id]?.operations[operation] === true)
513
+ );
586
514
  }
587
515
 
588
516
  async #bulk(
589
517
  action: "update" | "publish" | "unpublish" | "delete" | "restoreDeleted" | "deletePermanent",
590
- data?: Record<string, unknown>
518
+ data?: Record<string, unknown>,
519
+ options: { ids?: readonly string[]; surfaceIssues?: boolean } = {}
591
520
  ) {
592
- const ids = [...this.selectedIDs];
593
- if (ids.length === 0 || this.bulkPending || !this.selectionQueryReady) return false;
594
- this.bulkPending = true;
521
+ const ids = [...(options.ids ?? this.selectedIDs)];
522
+ if (!this.selectionControlsReady || !this.#idsAllow(ids, action)) return false;
523
+ if (
524
+ action === "update" &&
525
+ Object.keys(data ?? {}).some((path) =>
526
+ ids.some((id) => this.#documentAccess[id]?.fields[path]?.update === false)
527
+ )
528
+ )
529
+ return false;
530
+ const mutation = this.#beginMutation();
531
+ if (mutation === undefined) return false;
532
+ const { request } = mutation;
595
533
  try {
596
534
  if (action === "update")
597
- await this.options.client.bulkUpdate(this.options.slug, ids, data ?? {}, {
598
- locale: this.options.locale,
535
+ await this.options.client.bulkUpdate(request.slug, ids, data ?? {}, {
536
+ locale: request.locale,
599
537
  });
600
538
  else if (action === "publish")
601
- await this.options.client.bulkPublish(this.options.slug, ids, {
602
- locale: this.options.locale,
539
+ await this.options.client.bulkPublish(request.slug, ids, {
540
+ locale: request.locale,
603
541
  });
604
542
  else if (action === "unpublish")
605
- await this.options.client.bulkUnpublish(this.options.slug, ids, {
606
- locale: this.options.locale,
543
+ await this.options.client.bulkUnpublish(request.slug, ids, {
544
+ locale: request.locale,
607
545
  });
608
546
  else if (action === "delete")
609
- await this.options.client.bulkDelete(this.options.slug, ids, {
610
- locale: this.options.locale,
547
+ await this.options.client.bulkDelete(request.slug, ids, {
548
+ locale: request.locale,
611
549
  });
612
550
  else if (action === "restoreDeleted")
613
- await this.options.client.bulkRestoreDeleted(this.options.slug, ids, {
614
- locale: this.options.locale,
551
+ await this.options.client.bulkRestoreDeleted(request.slug, ids, {
552
+ locale: request.locale,
615
553
  });
616
554
  else
617
- await this.options.client.bulkDeletePermanent(this.options.slug, ids, {
618
- locale: this.options.locale,
555
+ await this.options.client.bulkDeletePermanent(request.slug, ids, {
556
+ locale: request.locale,
619
557
  });
558
+ if (!this.#ownsMutation(mutation)) return false;
620
559
  this.clearSelection();
621
560
  this.options.notifications.success({
622
561
  title: this.options.i18n.t(bulkTranslationKey(action), {
@@ -624,9 +563,10 @@ export class CollectionListController {
624
563
  formattedCount: this.options.i18n.formatNumber(ids.length),
625
564
  }),
626
565
  });
627
- if (this.#current !== undefined) await this.#open(this.#current);
628
- return true;
566
+ await this.#open(request);
567
+ return this.#ownsMutation(mutation);
629
568
  } catch (cause) {
569
+ if (!this.#ownsMutation(mutation)) return false;
630
570
  this.options.notifications.error({
631
571
  title: this.options.i18n.t("collections:bulkOperationFailed"),
632
572
  message:
@@ -634,9 +574,11 @@ export class CollectionListController {
634
574
  ? cause.message
635
575
  : this.options.i18n.t("collections:noDocumentsChanged"),
636
576
  });
577
+ if (options.surfaceIssues && cause instanceof RiduError && cause.issues.length > 0)
578
+ throw cause;
637
579
  return false;
638
580
  } finally {
639
- this.bulkPending = false;
581
+ if (this.#mutation === mutation) this.#invalidateMutation();
640
582
  }
641
583
  }
642
584
  }
@@ -652,66 +594,13 @@ function bulkTranslationKey(
652
594
  return "collections:documentsDeleted" as const;
653
595
  }
654
596
 
655
- function buildWhere(request: CollectionListRequest, status = request.status) {
656
- const filters: Where[] = [];
657
- const search = request.search.trim();
658
- if (search.length > 0 && request.titleField !== undefined) {
659
- filters.push({ [request.titleField]: { like: search } });
660
- }
661
- if (status !== "" && request.statusField !== undefined) {
662
- filters.push({ [request.statusField]: { equals: status } });
663
- }
664
- if (request.folderID !== "" && request.folderField !== undefined) {
665
- filters.push({ [request.folderField]: { equals: request.folderID } });
666
- }
667
- filters.push(...request.filterWhere);
668
- if (filters.length === 0) return undefined;
669
- if (filters.length === 1) return filters[0];
670
- return { and: filters };
671
- }
672
-
673
- function selectionQueryKey(request: CollectionListRequest) {
674
- return JSON.stringify({
675
- slug: request.slug,
676
- locale: request.locale,
677
- search: request.search,
678
- titleField: request.titleField,
679
- status: request.status,
680
- statusField: request.statusField,
681
- trashOnly: request.trashOnly,
682
- folderField: request.folderField,
683
- folderID: request.folderID,
684
- hierarchy: request.hierarchy,
685
- sort: request.sort,
686
- filterWhere: request.filterWhere,
687
- });
597
+ function selectionQueryKey(
598
+ request: CollectionListRequest,
599
+ query: AdminCollectionListDataV1["query"]
600
+ ) {
601
+ return JSON.stringify([request.slug, query.locale, query.trash, query.where]);
688
602
  }
689
603
 
690
604
  function requestKey(request: CollectionListRequest) {
691
605
  return JSON.stringify(request);
692
606
  }
693
-
694
- function humanize(value: string, language: string) {
695
- return value
696
- .replaceAll(/[_-]+/g, " ")
697
- .replace(/\b\w/g, (letter) => letter.toLocaleUpperCase(language));
698
- }
699
-
700
- function formatUpdated(value: string | undefined, i18n: AdminI18n) {
701
- if (value === undefined) return "—";
702
- const date = new Date(value);
703
- if (Number.isNaN(date.valueOf())) return value;
704
- const seconds = Math.round((date.valueOf() - Date.now()) / 1_000);
705
- if (Math.abs(seconds) < 60) return i18n.formatRelativeTime(seconds, "second");
706
- const minutes = Math.round(seconds / 60);
707
- if (Math.abs(minutes) < 60) return i18n.formatRelativeTime(minutes, "minute");
708
- const hours = Math.round(minutes / 60);
709
- if (Math.abs(hours) < 24) return i18n.formatRelativeTime(hours, "hour");
710
- const days = Math.round(hours / 24);
711
- if (Math.abs(days) < 7) return i18n.formatRelativeTime(days, "day");
712
- return i18n.formatDate(date, {
713
- day: "2-digit",
714
- month: "short",
715
- year: date.getFullYear() === new Date().getFullYear() ? undefined : "numeric",
716
- });
717
- }