@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,633 +1,115 @@
1
1
  <script lang="ts">
2
- import { Link, useLocation, useNavigate, useParams } from "@hvniel/svelte-router";
3
- import type { SchemaCollection, SchemaField } from "@riducms/protocol";
4
- import FolderIcon from "~icons/lucide/folder";
5
- import RotateCcwIcon from "~icons/lucide/rotate-ccw";
6
- import Trash2Icon from "~icons/lucide/trash-2";
7
- import UploadCloudIcon from "~icons/lucide/upload-cloud";
8
-
2
+ import APIReference from "@admin/features/api-reference/api-reference.svelte";
3
+ import "@admin/features/collections/collection-list.scss";
4
+ import "@admin/components/data/list-table.scss";
5
+ import {
6
+ Link,
7
+ useHref,
8
+ useLocation,
9
+ useNavigation,
10
+ useParams,
11
+ useSearchParams,
12
+ } from "@hvniel/svelte-router";
13
+ import { Button } from "@riducms/ui";
9
14
  import { Banner } from "@admin/components/ui/banner";
10
- import { Button, buttonVariants } from "@admin/components/ui/button";
11
- import { ConfirmationDialog } from "@admin/components/ui/confirmation-dialog";
12
15
  import {
13
16
  collectionPath,
14
17
  collectionTrashPath,
15
- collectionUploadPath,
16
- createDocumentPath,
17
- documentPath,
18
+ withContentLocale,
18
19
  } from "@admin/core/routing/admin-paths";
20
+ import { registerAdminScrollPage } from "@admin/core/routing/admin-scroll.svelte";
19
21
  import { getAdminRuntime } from "@admin/core/runtime/admin-runtime.svelte";
22
+ import { getAdminBootstrapCoordinator } from "@admin/core/bootstrap/admin-bootstrap";
20
23
  import { getNotificationCenter } from "@admin/core/notifications/notification-center.svelte";
21
- import type { AdminDocument } from "@admin/core/api/admin-client";
22
- import { preferenceOwnerID } from "@admin/core/preferences/preference-write-queue";
23
-
24
- import { CollectionListController } from "@admin/features/collections/collection-list-controller.svelte";
25
- import CollectionListBulkActions from "@admin/features/collections/collection-list-bulk-actions.svelte";
26
- import CollectionListBulkEditor from "@admin/features/collections/collection-list-bulk-editor.svelte";
27
24
  import {
28
- CollectionListPreferencesController,
29
- type CollectionListPreset,
30
- } from "@admin/features/collections/collection-list-preferences-controller.svelte";
25
+ CollectionList,
26
+ setCollectionList,
27
+ } from "@admin/features/collections/collection-list.svelte";
28
+ import { CollectionListQuery } from "@admin/features/collections/collection-list-query";
31
29
  import CollectionListResults from "@admin/features/collections/collection-list-results.svelte";
32
- import CollectionListWorkspaceToolbar from "@admin/features/collections/collection-list-workspace-toolbar.svelte";
33
- import {
34
- buildListFilterWhere,
35
- defaultListColumns,
36
- filterableFields,
37
- listColumnFields,
38
- listPageSizes,
39
- parseListFilters,
40
- parseListPageSize,
41
- visibleColumnNames,
42
- type ListFilter,
43
- type ListPageSize,
44
- } from "@admin/features/collections/list-workspace";
45
- import { formatDateDisplay } from "@admin/fields/scalar/date-value";
30
+ import ListToolbar from "@admin/features/collections/controls/list-toolbar.svelte";
31
+ import CollectionListBulkActions from "@admin/features/collections/bulk/bulk-actions.svelte";
32
+ import CollectionListBulkEditorLoader from "@admin/features/collections/bulk/bulk-editor-loader.svelte";
33
+ import BulkConfirmation, {
34
+ type BulkConfirmationAction,
35
+ } from "@admin/features/collections/bulk/bulk-confirmation.svelte";
46
36
 
47
37
  const runtime = getAdminRuntime();
48
- const notifications = getNotificationCenter();
49
- const navigate = useNavigate();
50
- const params = $derived(useParams<"collection">());
51
- const location = $derived(useLocation());
52
- const slug = $derived(params.collection ?? "");
53
- const trashOnly = $derived(location.pathname.endsWith("/trash"));
54
- const collection = $derived(runtime.manifest?.collections.find((item) => item.slug === slug));
55
- const titleField = $derived(
56
- collection?.fields.find((field) => field.name === collection.admin.useAsTitle) ??
57
- collection?.fields.find((field) => field.type === "text") ??
58
- collection?.fields.find((field) => field.type === "select")
59
- );
60
- const searchLabel = $derived(
61
- titleField === undefined
62
- ? runtime.i18n.t("collections:searchDocuments")
63
- : runtime.i18n.t("collections:searchBy", { label: titleField.admin.label })
64
- );
65
- const statusField = $derived(
66
- collection?.fields.find((field) => field.type === "select" && field.name === "status")
67
- );
68
- const statusOptions = $derived(statusField?.select?.options ?? []);
69
- const searchParams = $derived(new URLSearchParams(location.search));
70
- const localization = $derived(runtime.manifest?.application.localization);
71
- const requestedLocale = $derived(searchParams.get("locale"));
72
- const contentLocale = $derived(
73
- localization?.locales.some((locale) => locale.code === requestedLocale)
74
- ? (requestedLocale ?? localization?.defaultLocale)
75
- : (runtime.contentLocale ?? localization?.defaultLocale)
76
- );
77
- const query = $derived(searchParams.get("q") ?? "");
78
- const requestedPage = $derived(parsePage(searchParams.get("page")));
79
- const requestedStatus = $derived(searchParams.get("status") ?? "");
80
- const requestedFolder = $derived(searchParams.get("folder") ?? "");
81
- const hierarchy = $derived(searchParams.get("view") === "hierarchy");
82
- const requestedSort = $derived(searchParams.get("sort") ?? "");
83
- const folderField = $derived(
84
- collection?.fields.find((field) => field.name === collection.admin.folderField)
85
- );
86
- const parentField = $derived(
87
- collection?.fields.find((field) => field.name === collection.admin.parentField)
88
- );
89
- const folderCollection = $derived(
90
- runtime.manifest?.collections.find(
91
- (candidate) => candidate.slug === folderField?.relationship?.collectionSlug
92
- )
93
- );
94
- let folders = $state.raw<AdminDocument[]>([]);
95
- let referenceLabels = $state.raw<Record<string, string>>({});
96
- let searchInput = $state<HTMLInputElement | null>(null);
97
- const activeStatus = $derived(
98
- statusOptions.some((option) => option.value === requestedStatus) ? requestedStatus : ""
99
- );
100
- const bulkEditableFields = $derived(
101
- collection?.fields.filter(
102
- (field) =>
103
- field.category === "scalar" &&
104
- field.localized !== true &&
105
- field.admin.readOnly !== true &&
106
- ["text", "textarea", "email", "date", "number", "checkbox", "select", "radio"].includes(
107
- field.type
108
- )
109
- ) ?? []
110
- );
111
- const customListCells = $derived(
112
- runtime.listCells.flatMap((cell) => {
113
- if (cell.collection !== slug) return [];
114
- const field = collection?.fields.find(
115
- (candidate) => candidate.path === cell.field || candidate.name === cell.field
116
- );
117
- return field === undefined ? [] : [{ ...cell, field }];
118
- })
119
- );
120
- const availableColumnFields = $derived(
121
- listColumnFields(collection, titleField?.name).filter(
122
- (field) =>
123
- field.path !== statusField?.path &&
124
- !customListCells.some((cell) => cell.field.path === field.path)
125
- )
126
- );
127
- const availableFilterFields = $derived(filterableFields(collection?.fields ?? []));
128
- const defaultColumns = $derived(defaultListColumns(collection, availableColumnFields));
129
- const preferences = new CollectionListPreferencesController(
130
- runtime.client,
131
- notifications,
132
- () => runtime.session?.id ?? "",
133
- runtime.i18n
134
- );
135
- const { workspace, presets, presetsPending } = $derived(preferences);
136
- const visibleColumns = $derived(
137
- visibleColumnNames(searchParams.get("columns"), workspace.columns).flatMap((name) => {
138
- const field = availableColumnFields.find((candidate) => candidate.path === name);
139
- return field === undefined ? [] : [field];
140
- })
141
- );
142
- const pageSize = $derived(parseListPageSize(searchParams.get("limit"), workspace.limit));
143
- const showID = $derived(workspace.showID);
144
- const showCreated = $derived(workspace.showCreated);
145
- const listFilters = $derived(
146
- parseListFilters(searchParams.get("filters"), availableFilterFields)
147
- );
148
- const filterWhere = $derived(buildListFilterWhere(listFilters, availableFilterFields));
149
- const sortField = $derived(requestedSort.replace(/^-/, ""));
150
- const sortDescending = $derived(requestedSort.startsWith("-"));
151
- let bulkEditOpen = $state(false);
152
- type ConfirmationAction =
153
- | { kind: "delete-document"; id: string }
154
- | { kind: "delete-selected" }
155
- | { kind: "restore-selected" }
156
- | { kind: "delete-selected-permanently" }
157
- | { kind: "empty-trash" };
158
- let confirmationOpen = $state(false);
159
- let confirmationAction = $state.raw<ConfirmationAction>();
160
- const confirmationCopy = $derived(confirmationText(confirmationAction));
161
- const controller = new CollectionListController({
162
- client: runtime.client,
163
- notifications,
164
- i18n: runtime.i18n,
38
+ const bootstrap = getAdminBootstrapCoordinator();
39
+ const navigation = useNavigation();
40
+ const params = useParams<"collection">();
41
+ const location = useLocation();
42
+ const currentPath = useHref(() => location.current.pathname);
43
+ const [searchParams, setSearchParams] = useSearchParams();
44
+
45
+ const list = new CollectionList({
46
+ runtime,
47
+ notifications: getNotificationCenter(),
48
+ query: new CollectionListQuery(searchParams, setSearchParams, () => {
49
+ const destination = navigation.current.location;
50
+ if (
51
+ destination?.pathname === currentPath.current &&
52
+ new URLSearchParams(destination.search).get("locale") === searchParams.current.get("locale")
53
+ ) {
54
+ return destination.search;
55
+ }
56
+ }),
165
57
  get slug() {
166
- return slug;
167
- },
168
- get locale() {
169
- return contentLocale;
170
- },
171
- get page() {
172
- return requestedPage;
173
- },
174
- get search() {
175
- return query;
176
- },
177
- get titleField() {
178
- return titleField?.name;
179
- },
180
- get status() {
181
- return activeStatus;
182
- },
183
- get statusField() {
184
- return statusField?.name;
185
- },
186
- get statusOptions() {
187
- return statusOptions;
188
- },
189
- get versioned() {
190
- return collection?.capabilities.versions === true;
58
+ return params.current.collection ?? "";
191
59
  },
192
60
  get trashOnly() {
193
- return trashOnly;
61
+ return location.current.pathname.endsWith("/trash");
194
62
  },
195
- get folderField() {
196
- return folderField?.name;
63
+ get navigationIdle() {
64
+ return navigation.current.state === "idle";
197
65
  },
198
- get folderID() {
199
- return requestedFolder;
200
- },
201
- get hierarchy() {
202
- return hierarchy;
203
- },
204
- get limit() {
205
- return pageSize;
206
- },
207
- get sort() {
208
- return requestedSort === "" ? [] : [requestedSort];
209
- },
210
- get filterWhere() {
211
- return filterWhere;
212
- },
213
- });
214
- const {
215
- allDocuments,
216
- canCreate,
217
- docs,
218
- error,
219
- pagination,
220
- selectedIDs,
221
- bulkPending,
222
- showStatus,
223
- showUpdated,
224
- statusColumnAvailable,
225
- } = $derived(controller);
226
- const {
227
- optionCount,
228
- canReadField,
229
- deletePermanent,
230
- bulkDelete,
231
- bulkDeletePermanent,
232
- bulkRestore,
233
- emptyTrash,
234
- bulkUpdate,
235
- retry,
236
- setShowStatus,
237
- setShowUpdated,
238
- title,
239
- } = controller;
66
+ prepared() {
67
+ // A retained route may only adopt a seed for its exact pathname and query.
68
+ const route = bootstrap.routeData(location.current.pathname, location.current.search);
240
69
 
241
- $effect(() => {
242
- if (contentLocale === undefined || requestedLocale === contentLocale) return;
243
- const next = new URLSearchParams(location.search);
244
- next.set("locale", contentLocale);
245
- updateSearch(next, true);
70
+ return route?.kind === "collection-list" || route?.kind === "collection-trash"
71
+ ? route.data
72
+ : undefined;
73
+ },
246
74
  });
247
75
 
248
- $effect(() =>
249
- runtime.registerContentLocaleBlocker(
250
- () =>
251
- controller.bulkPending || controller.selectionPending || controller.mutationPending.size > 0
252
- )
253
- );
76
+ setCollectionList(list);
77
+ registerAdminScrollPage({ ready: () => list.ready });
254
78
 
255
- $effect(() => {
256
- preferences.enter({
257
- slug,
258
- sessionID: runtime.session?.id ?? "",
259
- preferenceOwnerID: preferenceOwnerID(runtime.session),
260
- columnFields: availableColumnFields,
261
- filterFields: availableFilterFields,
262
- defaultColumns,
263
- });
264
- });
79
+ const { controller } = list;
80
+ const { slug, collection, trashOnly, contentLocale, filtered } = $derived(list);
81
+ const { canCreate, error, pagination, bulkPending } = $derived(controller);
82
+ const { retry } = controller;
265
83
 
266
- $effect(() => {
267
- setShowStatus(workspace.showStatus);
268
- setShowUpdated(workspace.showUpdated);
269
- });
84
+ const collectionViews = $derived([
85
+ { trash: false, label: runtime.i18n.t("collections:allDocuments", { label: list.label }) },
86
+ { trash: true, label: runtime.i18n.t("collections:trash") },
87
+ ]);
270
88
 
271
- $effect(() => () => preferences.dispose());
89
+ let bulkEditOpen = $state(false);
90
+ let confirmationOpen = $state(false);
91
+ let confirmationAction = $state<BulkConfirmationAction>("delete-selected");
272
92
 
273
- $effect(() => {
274
- const target = folderCollection;
275
- const revision = runtime.documentRevision;
276
- if (target === undefined || revision < 0) {
277
- folders = [];
278
- return;
279
- }
280
- const request = new AbortController();
281
- runtime.client
282
- .list(target.slug, { limit: 100, locale: contentLocale, signal: request.signal })
283
- .then((page) => {
284
- if (!request.signal.aborted) folders = page.docs;
285
- })
286
- .catch(() => {
287
- if (!request.signal.aborted) folders = [];
288
- });
289
- return () => request.abort();
290
- });
93
+ let searchInput = $state<HTMLInputElement | null>(null);
94
+ let focusSearchAfterNavigation = $state(false);
291
95
 
292
96
  $effect(() => {
293
- const documents = docs;
294
- const fields = visibleColumns.filter(referenceField);
295
- const collections = runtime.manifest?.collections ?? [];
296
- if (documents.length === 0 || fields.length === 0) {
297
- referenceLabels = {};
298
- return;
299
- }
300
- const lookups = new Map<string, { collection: SchemaCollection; id: string }>();
301
- for (const document of documents) {
302
- for (const field of fields) {
303
- for (const reference of fieldReferences(field, documentPathValue(document, field.path))) {
304
- const target = collections.find((candidate) => candidate.slug === reference.collection);
305
- if (target !== undefined) {
306
- lookups.set(referenceKey(reference.collection, reference.id), {
307
- collection: target,
308
- id: reference.id,
309
- });
310
- }
311
- }
312
- }
97
+ if (list.navigationIdle && focusSearchAfterNavigation) {
98
+ focusSearchAfterNavigation = false;
99
+ searchInput?.focus();
313
100
  }
314
- const request = new AbortController();
315
- Promise.all(
316
- [...lookups].map(async ([key, lookup]) => {
317
- try {
318
- const document = await runtime.client.find(lookup.collection.slug, lookup.id, {
319
- signal: request.signal,
320
- locale: contentLocale,
321
- });
322
- return [key, referenceTitle(lookup.collection, document)] as const;
323
- } catch {
324
- return [key, lookup.id] as const;
325
- }
326
- })
327
- ).then((entries) => {
328
- if (!request.signal.aborted) referenceLabels = Object.fromEntries(entries);
329
- });
330
- return () => request.abort();
331
101
  });
332
102
 
333
- function parsePage(value: string | null) {
334
- const page = Number(value);
335
- return Number.isInteger(page) && page > 0 ? page : 1;
336
- }
337
-
338
- function contentPath(path: string) {
339
- if (contentLocale === undefined) return path;
340
- return `${path}?${new URLSearchParams({ locale: contentLocale }).toString()}`;
341
- }
342
-
343
- function updateSearch(next: URLSearchParams, replace: boolean) {
344
- navigate(
345
- { pathname: location.pathname, search: next.size === 0 ? "" : `?${next}` },
346
- { replace }
347
- );
348
- }
349
-
350
- function handleSearch(value: string) {
351
- const next = new URLSearchParams(searchParams);
352
- if (value.trim().length > 0) next.set("q", value);
353
- else next.delete("q");
354
- next.delete("page");
355
- updateSearch(next, true);
356
- }
357
-
358
- function clearSearch() {
359
- const next = new URLSearchParams(searchParams);
360
- next.delete("q");
361
- next.delete("page");
362
- updateSearch(next, true);
363
- }
364
-
365
- function clearFilters() {
366
- const next = new URLSearchParams(searchParams);
367
- next.delete("q");
368
- next.delete("status");
369
- next.delete("page");
370
- next.delete("folder");
371
- next.delete("filters");
372
- updateSearch(next, true);
373
- }
374
-
375
103
  function clearFiltersAndFocusSearch() {
376
- clearFilters();
377
- searchInput?.focus();
378
- }
379
-
380
- function updateListFilters(filters: readonly ListFilter[]) {
381
- const next = new URLSearchParams(searchParams);
382
- if (filters.length === 0) next.delete("filters");
383
- else next.set("filters", JSON.stringify(filters));
384
- next.delete("page");
385
- updateSearch(next, false);
386
- }
387
-
388
- function toggleSort(name: string) {
389
- const next = new URLSearchParams(searchParams);
390
- if (sortField !== name) next.set("sort", name);
391
- else if (!sortDescending) next.set("sort", `-${name}`);
392
- else next.delete("sort");
393
- next.delete("page");
394
- updateSearch(next, false);
395
- }
396
-
397
- function selectPageSize(limit: ListPageSize) {
398
- const next = new URLSearchParams(searchParams);
399
- next.set("limit", String(limit));
400
- next.delete("page");
401
- updateSearch(next, false);
402
- void preferences.persistWorkspace({ ...workspace, limit });
403
- }
404
-
405
- function toggleColumn(name: string, show: boolean) {
406
- // Workspace state is updated synchronously by persistWorkspace. Using it as
407
- // the source prevents two quick checkbox changes from racing a router update.
408
- const nextNames = new Set(workspace.columns);
409
- if (show) nextNames.add(name);
410
- else nextNames.delete(name);
411
- const columns = availableColumnFields
412
- .map((field) => field.path)
413
- .filter((candidate) => nextNames.has(candidate));
414
- const next = new URLSearchParams(searchParams);
415
- if (columns.length === 0) next.set("columns", "");
416
- else next.set("columns", columns.join(","));
417
- updateSearch(next, true);
418
- void preferences.persistWorkspace({ ...workspace, columns });
419
- }
420
-
421
- function toggleStatusColumn(show: boolean) {
422
- void preferences.persistWorkspace({ ...workspace, showStatus: show });
423
- }
424
-
425
- function toggleUpdatedColumn(show: boolean) {
426
- void preferences.persistWorkspace({ ...workspace, showUpdated: show });
427
- }
428
-
429
- function toggleIDColumn(show: boolean) {
430
- void preferences.persistWorkspace({ ...workspace, showID: show });
431
- }
432
-
433
- function toggleCreatedColumn(show: boolean) {
434
- void preferences.persistWorkspace({ ...workspace, showCreated: show });
435
- }
436
-
437
- function selectFolder(folder: string) {
438
- const next = new URLSearchParams(searchParams);
439
- if (folder === "") next.delete("folder");
440
- else next.set("folder", folder);
441
- next.delete("page");
442
- updateSearch(next, false);
443
- }
444
-
445
- function selectView(view: "list" | "hierarchy") {
446
- const next = new URLSearchParams(searchParams);
447
- if (view === "list") next.delete("view");
448
- else next.set("view", view);
449
- next.delete("page");
450
- updateSearch(next, false);
451
- }
452
-
453
- function savePreset(name: string) {
454
- return preferences.savePreset(name, {
455
- q: query,
456
- status: activeStatus,
457
- folder: requestedFolder,
458
- view: hierarchy ? "hierarchy" : "list",
459
- filters: listFilters,
460
- sort: requestedSort,
461
- columns: visibleColumns.map((field) => field.path),
462
- limit: pageSize,
463
- showStatus,
464
- showID,
465
- showCreated,
466
- showUpdated,
467
- });
468
- }
469
-
470
- function applyPreset(preset: CollectionListPreset) {
471
- const next = new URLSearchParams();
472
- if (contentLocale !== undefined) next.set("locale", contentLocale);
473
- if (preset.q !== "") next.set("q", preset.q);
474
- if (preset.status !== "") next.set("status", preset.status);
475
- if (preset.folder !== "") next.set("folder", preset.folder);
476
- if (preset.view === "hierarchy") next.set("view", "hierarchy");
477
- if (preset.filters.length > 0) next.set("filters", JSON.stringify(preset.filters));
478
- if (preset.sort !== "") next.set("sort", preset.sort);
479
- if (preset.columns.length > 0) next.set("columns", preset.columns.join(","));
480
- next.set("limit", String(preset.limit));
481
- void preferences.persistWorkspace({
482
- columns: preset.columns,
483
- limit: preset.limit,
484
- showStatus: preset.showStatus,
485
- showID: preset.showID,
486
- showCreated: preset.showCreated,
487
- showUpdated: preset.showUpdated,
488
- });
489
- updateSearch(next, false);
490
- }
491
-
492
- function selectStatus(status: string) {
493
- const next = new URLSearchParams(searchParams);
494
- if (status === "") next.delete("status");
495
- else next.set("status", status);
496
- next.delete("page");
497
- updateSearch(next, false);
498
- }
499
-
500
- function selectPage(page: number) {
501
- const next = new URLSearchParams(searchParams);
502
- if (page <= 1) next.delete("page");
503
- else next.set("page", String(page));
504
- updateSearch(next, false);
505
- }
506
-
507
- function columnValue(document: AdminDocument, field: SchemaField) {
508
- if (!canReadField(field.path, document.id)) return "—";
509
- const value = documentPathValue(document, field.path);
510
- if (value === undefined || value === null || value === "") return "—";
511
- if (field.type === "checkbox") {
512
- return value === true ? runtime.i18n.t("general:yes") : runtime.i18n.t("general:no");
513
- }
514
- if (field.type === "select" || field.type === "radio") {
515
- return field.select?.options.find((option) => option.value === value)?.label ?? String(value);
516
- }
517
- if (field.type === "array") return summarizeRows(value, "row");
518
- if (field.type === "blocks") return summarizeRows(value, "block");
519
- if (field.type === "json" || field.plugin !== undefined) return summarizeStructured(value);
520
- if (field.type === "date") return formatDateDisplay(value, field.date?.format, runtime.i18n);
521
- if (referenceField(field)) {
522
- return runtime.i18n.formatList(
523
- fieldReferences(field, value).map(
524
- (reference) =>
525
- referenceLabels[referenceKey(reference.collection, reference.id)] ?? reference.id
526
- )
527
- );
528
- }
529
- if (Array.isArray(value)) return runtime.i18n.formatList(value.map(displayReference));
530
- if (typeof value === "object") return displayReference(value);
531
- return String(value);
532
- }
533
-
534
- function referenceField(field: SchemaField) {
535
- return (
536
- field.relationship !== undefined || field.upload !== undefined || field.join !== undefined
537
- );
538
- }
539
-
540
- function fieldReferences(field: SchemaField, value: unknown) {
541
- const values = Array.isArray(value) ? value : [value];
542
- return values.flatMap((candidate): { collection: string; id: string }[] => {
543
- if (typeof candidate === "string") {
544
- const target =
545
- field.relationship?.collectionSlug ??
546
- field.upload?.collectionSlug ??
547
- field.join?.collectionSlug;
548
- return target === undefined ? [] : [{ collection: target, id: candidate }];
549
- }
550
- if (typeof candidate !== "object" || candidate === null) return [];
551
- const record = candidate as Record<string, unknown>;
552
- const id = typeof record.id === "string" ? record.id : undefined;
553
- const target =
554
- typeof record.relationTo === "string"
555
- ? record.relationTo
556
- : (field.relationship?.collectionSlug ??
557
- field.upload?.collectionSlug ??
558
- field.join?.collectionSlug);
559
- return id === undefined || target === undefined ? [] : [{ collection: target, id }];
560
- });
561
- }
562
-
563
- function referenceKey(collection: string, id: string) {
564
- return `${collection}:${id}`;
565
- }
566
-
567
- function referenceTitle(collection: SchemaCollection, document: AdminDocument) {
568
- const titleField =
569
- collection.fields.find((field) => field.name === collection.admin.useAsTitle) ??
570
- collection.fields.find((field) => ["text", "email", "select", "radio"].includes(field.type));
571
- const value =
572
- titleField === undefined ? undefined : documentPathValue(document, titleField.path);
573
- return value === undefined || value === null || value === "" ? document.id : String(value);
574
- }
575
-
576
- function displayReference(value: unknown) {
577
- if (typeof value === "string") return value;
578
- if (typeof value !== "object" || value === null) return String(value ?? "—");
579
- const record = value as Record<string, unknown>;
580
- return String(
581
- record.title ??
582
- record.name ??
583
- record.email ??
584
- record.id ??
585
- runtime.i18n.t("collections:relatedDocument")
586
- );
587
- }
588
-
589
- function documentPathValue(document: AdminDocument, path: string) {
590
- let value: unknown = document;
591
- for (const segment of path.split(".")) {
592
- if (typeof value !== "object" || value === null || Array.isArray(value)) return undefined;
593
- value = Reflect.get(value, segment);
594
- }
595
- return value;
104
+ focusSearchAfterNavigation = true;
105
+ return list.query.clearFilters();
596
106
  }
597
107
 
598
- function summarizeRows(value: unknown, singular: "row" | "block") {
599
- if (!Array.isArray(value) || value.length === 0) return "—";
600
- const previews = value.slice(0, 2).map((row) => {
601
- if (typeof row !== "object" || row === null) return String(row);
602
- const record = row as Record<string, unknown>;
603
- return String(
604
- record.label ??
605
- record.title ??
606
- record.heading ??
607
- record.name ??
608
- record.blockType ??
609
- runtime.i18n.t(singular === "row" ? "collections:rowNumber" : "collections:blockNumber", {
610
- number: runtime.i18n.formatNumber(value.indexOf(row) + 1),
611
- })
612
- );
613
- });
614
- const summary = runtime.i18n.formatList(previews);
615
- return value.length > previews.length
616
- ? runtime.i18n.t("collections:moreItems", {
617
- summary,
618
- count: runtime.i18n.formatNumber(value.length - previews.length),
619
- })
620
- : summary;
621
- }
622
-
623
- function summarizeStructured(value: unknown) {
624
- if (value === undefined || value === null) return "—";
625
- if (typeof value !== "object") return String(value);
626
- const text = JSON.stringify(value);
627
- return text.length > 72 ? `${text.slice(0, 69)}…` : text;
108
+ function contentPath(path: string) {
109
+ return withContentLocale(path, contentLocale);
628
110
  }
629
111
 
630
- function requestConfirmation(action: ConfirmationAction) {
112
+ function requestConfirmation(action: BulkConfirmationAction) {
631
113
  confirmationAction = action;
632
114
  confirmationOpen = true;
633
115
  }
@@ -635,316 +117,93 @@
635
117
  function openBulkEdit() {
636
118
  bulkEditOpen = true;
637
119
  }
638
-
639
- function applyBulkEdit(field: SchemaField, value: unknown) {
640
- return bulkUpdate({ [field.name]: value });
641
- }
642
-
643
- async function performConfirmation() {
644
- const action = confirmationAction;
645
- if (action === undefined) return;
646
- if (action.kind === "delete-document") await deletePermanent(action.id);
647
- else if (action.kind === "delete-selected") await bulkDelete();
648
- else if (action.kind === "restore-selected") await bulkRestore();
649
- else if (action.kind === "delete-selected-permanently") await bulkDeletePermanent();
650
- else await emptyTrash();
651
- }
652
-
653
- function confirmationText(action: ConfirmationAction | undefined) {
654
- const count = selectedIDs.size;
655
- if (action?.kind === "delete-document") {
656
- return {
657
- title: runtime.i18n.t("collections:confirmDeleteDocumentPermanently"),
658
- description: runtime.i18n.t("collections:cannotUndo"),
659
- confirmLabel: runtime.i18n.t("collections:deletePermanently"),
660
- destructive: true,
661
- };
662
- }
663
- if (action?.kind === "restore-selected") {
664
- return {
665
- title: runtime.i18n.t("collections:confirmRestoreSelected", {
666
- count,
667
- formattedCount: runtime.i18n.formatNumber(count),
668
- }),
669
- description: runtime.i18n.t("collections:restoreSelectedDescription"),
670
- confirmLabel: runtime.i18n.t("documents:restore"),
671
- destructive: false,
672
- };
673
- }
674
- if (action?.kind === "delete-selected-permanently") {
675
- return {
676
- title: runtime.i18n.t("collections:confirmDeleteSelectedPermanently", {
677
- count,
678
- formattedCount: runtime.i18n.formatNumber(count),
679
- }),
680
- description: runtime.i18n.t("collections:cannotUndo"),
681
- confirmLabel: runtime.i18n.t("collections:deletePermanently"),
682
- destructive: true,
683
- };
684
- }
685
- if (action?.kind === "empty-trash") {
686
- const trashCount = pagination.totalDocs;
687
- return {
688
- title: runtime.i18n.t("collections:confirmEmptyTrash", {
689
- label:
690
- collection?.labels.plural.toLocaleLowerCase(runtime.i18n.language) ??
691
- runtime.i18n.t("collections:documents"),
692
- }),
693
- description: runtime.i18n.t("collections:emptyTrashDescription", {
694
- count: trashCount,
695
- formattedCount: runtime.i18n.formatNumber(trashCount),
696
- }),
697
- confirmLabel: runtime.i18n.t("collections:emptyTrash"),
698
- destructive: true,
699
- };
700
- }
701
- return {
702
- title: runtime.i18n.t("collections:confirmDeleteSelected", {
703
- count,
704
- formattedCount: runtime.i18n.formatNumber(count),
705
- }),
706
- description: runtime.i18n.t("collections:deleteSelectedDescription"),
707
- confirmLabel: runtime.i18n.t("general:delete"),
708
- destructive: true,
709
- };
710
- }
711
120
  </script>
712
121
 
713
- <section class="w-full px-5 py-7 sm:px-8 sm:py-8 lg:px-10 lg:pb-20">
714
- <header class="flex flex-wrap items-start justify-between gap-4">
715
- <div>
716
- <div class="flex flex-wrap items-center gap-3">
717
- <h1
718
- class="text-[28px] font-semibold leading-tight tracking-[-0.02em] text-foreground-strong"
719
- >
720
- {collection?.labels.plural ?? slug}
721
- </h1>
722
- {#if !trashOnly && canCreate}
723
- <Link class={buttonVariants()} to={contentPath(createDocumentPath(slug))}>
724
- {runtime.i18n.t("general:create")}
725
- </Link>
726
- {/if}
727
- </div>
728
- <p class="mt-1.5 text-[12.5px] text-foreground-muted">
729
- {runtime.i18n.t("collections:documentCount", {
730
- count: allDocuments,
731
- formattedCount: runtime.i18n.formatNumber(allDocuments),
732
- })}
733
- {#if collection?.capabilities.versions === true}
734
- <span aria-hidden="true">·</span>
735
- {runtime.i18n.t("collections:draftsAndVersions")}
736
- {/if}
737
- </p>
738
- </div>
739
- <div class="flex items-center gap-2">
740
- {#if !trashOnly && canCreate && collection?.capabilities.upload === true}
122
+ <section class="ridu-list">
123
+ <header class="ridu-list__header" inert={!list.navigationIdle}>
124
+ <div class="ridu-list__title">
125
+ <h1>{collection?.labels.plural ?? slug}</h1>
126
+ {#if !trashOnly && canCreate}
741
127
  <Link
742
- class={buttonVariants({ variant: "outline" })}
743
- to={contentPath(collectionUploadPath(slug))}
128
+ class="ridu-list__pill"
129
+ aria-label={runtime.i18n.t("collections:createNew", {
130
+ label: collection?.labels.singular ?? slug,
131
+ })}
132
+ to={list.createHref}
744
133
  >
745
- <UploadCloudIcon class="size-3.5" />
746
- {runtime.i18n.t("collections:bulkUpload")}
134
+ {runtime.i18n.t("collections:createNewButton")}
747
135
  </Link>
748
136
  {/if}
749
- {#if collection?.capabilities.trash === true}
750
- {#if trashOnly}
751
- <Button
752
- variant="destructive"
753
- disabled={pagination.totalDocs === 0 || bulkPending}
754
- onclick={() => requestConfirmation({ kind: "empty-trash" })}
755
- >
756
- {runtime.i18n.t("collections:emptyTrash")}
757
- </Button>
758
- {/if}
759
- <Link
760
- class={buttonVariants({ variant: "outline" })}
761
- to={contentPath(trashOnly ? collectionPath(slug) : collectionTrashPath(slug))}
762
- >
763
- {#if trashOnly}<RotateCcwIcon class="size-3.5" />
764
- {runtime.i18n.t("collections:backToCollection")}{:else}<Trash2Icon class="size-3.5" />
765
- {runtime.i18n.t("collections:trash")}{/if}
137
+ {#if list.uploadHref !== undefined}
138
+ <Link class="ridu-list__pill" to={list.uploadHref}>
139
+ {runtime.i18n.t("collections:bulkUpload")}
766
140
  </Link>
767
141
  {/if}
768
- </div>
769
- </header>
770
-
771
- {#if statusOptions.length > 0 && !trashOnly}
772
- <div
773
- class="mt-6 flex h-9 w-full items-center justify-start gap-5 border-b border-control-border"
774
- role="group"
775
- aria-label={runtime.i18n.t("collections:statusFilters")}
776
- >
777
- <button
778
- type="button"
779
- class="relative h-9 flex-none cursor-pointer px-px pb-2.5 text-[13px] text-foreground-muted outline-none after:absolute after:inset-x-0 after:bottom-[-1px] after:h-0.5 after:bg-primary after:opacity-0 after:transition-opacity hover:text-foreground-strong focus-visible:outline-2 focus-visible:outline-primary/60 focus-visible:outline-offset-2 aria-pressed:text-foreground-strong aria-pressed:after:opacity-100"
780
- aria-pressed={activeStatus === ""}
781
- onclick={() => selectStatus("")}
782
- >
783
- {runtime.i18n.t("collections:all")}
784
- <span class="font-mono ms-1 text-[9.5px] text-foreground-faint">
785
- {runtime.i18n.formatNumber(allDocuments)}
786
- </span>
787
- </button>
788
- {#each statusOptions as option (option.value)}
789
- {const statusCount = $derived(optionCount(option))}
142
+ {#if trashOnly}
790
143
  <button
791
144
  type="button"
792
- class="relative h-9 flex-none cursor-pointer px-px pb-2.5 text-[13px] text-foreground-muted outline-none after:absolute after:inset-x-0 after:bottom-[-1px] after:h-0.5 after:bg-primary after:opacity-0 after:transition-opacity hover:text-foreground-strong focus-visible:outline-2 focus-visible:outline-primary/60 focus-visible:outline-offset-2 aria-pressed:text-foreground-strong aria-pressed:after:opacity-100"
793
- aria-pressed={activeStatus === option.value}
794
- onclick={() => selectStatus(option.value)}
145
+ class="ridu-list__pill"
146
+ disabled={bulkPending || (pagination.totalDocs === 0 && !filtered)}
147
+ onclick={() => requestConfirmation("empty-trash")}
795
148
  >
796
- {option.label}
797
- {#if statusCount !== undefined}
798
- <span class="font-mono ms-1 text-[9.5px] text-foreground-faint">
799
- {runtime.i18n.formatNumber(statusCount)}
800
- </span>
801
- {/if}
149
+ {runtime.i18n.t("collections:emptyTrash")}
802
150
  </button>
803
- {/each}
151
+ {/if}
804
152
  </div>
805
- {/if}
153
+
154
+ <div class="ridu-list__header-actions">
155
+ {#if collection}
156
+ {#key collection.slug}
157
+ <APIReference {collection} />
158
+ {/key}
159
+ {/if}
160
+ <CollectionListBulkActions
161
+ onEdit={openBulkEdit}
162
+ onRequestPublish={() => requestConfirmation("publish-selected")}
163
+ onRequestUnpublish={() => requestConfirmation("unpublish-selected")}
164
+ onRequestDelete={() => requestConfirmation("delete-selected")}
165
+ onRequestRestore={() => requestConfirmation("restore-selected")}
166
+ onRequestDeletePermanent={() => requestConfirmation("delete-selected-permanently")}
167
+ />
168
+
169
+ {#if collection?.capabilities.trash}
170
+ <nav class="ridu-list__views" aria-label={runtime.i18n.t("collections:collectionViews")}>
171
+ {#each collectionViews as view}
172
+ {#if trashOnly === view.trash}
173
+ <button type="button" class="ridu-list__view" aria-current="page" disabled>
174
+ {view.label}
175
+ </button>
176
+ {:else}
177
+ <Link
178
+ class="ridu-list__view"
179
+ to={contentPath(view.trash ? collectionTrashPath(slug) : collectionPath(slug))}
180
+ >
181
+ {view.label}
182
+ </Link>
183
+ {/if}
184
+ {/each}
185
+ </nav>
186
+ {/if}
187
+ </div>
188
+ </header>
806
189
 
807
190
  {#if error !== undefined}
808
- <Banner class="mt-6" tone="destructive">
809
- <span class="size-2 shrink-0 rounded-full bg-destructive" aria-hidden="true"></span>
810
- <span class="min-w-0 flex-1">{error}</span>
191
+ <Banner class="ridu-list__error" tone="destructive">
192
+ <span class="ridu-list__error-marker" aria-hidden="true"></span>
193
+ <span class="ridu-list__error-message">{error}</span>
811
194
  <Button variant="outline" size="sm" onclick={retry}>
812
195
  {runtime.i18n.t("general:retry")}
813
196
  </Button>
814
197
  </Banner>
815
198
  {/if}
816
199
 
817
- {#snippet manageFoldersLink()}
818
- {#if folderCollection !== undefined}
819
- <Link
820
- class={buttonVariants({ variant: "outline", class: "h-[31px] gap-1.75 px-3" })}
821
- to={contentPath(collectionPath(folderCollection.slug))}
822
- >
823
- <FolderIcon class="size-3" />
824
- {runtime.i18n.t("collections:manageFolders")}
825
- </Link>
826
- {/if}
827
- {/snippet}
828
-
829
- <CollectionListWorkspaceToolbar
830
- {slug}
831
- {query}
832
- {searchLabel}
833
- bind:searchInput
834
- filterFields={availableFilterFields}
835
- filters={listFilters}
836
- {canReadField}
837
- {folders}
838
- {requestedFolder}
839
- manageFoldersLink={folderCollection === undefined ? undefined : manageFoldersLink}
840
- showFolder={folderField !== undefined && !trashOnly}
841
- showHierarchy={parentField !== undefined && !trashOnly}
842
- {hierarchy}
843
- sessionActive={runtime.session !== undefined && !trashOnly}
844
- {presets}
845
- {presetsPending}
846
- {statusColumnAvailable}
847
- {availableColumnFields}
848
- {visibleColumns}
849
- {showStatus}
850
- {showID}
851
- {showCreated}
852
- {showUpdated}
853
- {pageSize}
854
- onSearch={handleSearch}
855
- onClearSearch={clearSearch}
856
- onFiltersChange={updateListFilters}
857
- onSelectFolder={selectFolder}
858
- onSelectView={selectView}
859
- onSavePreset={savePreset}
860
- onApplyPreset={applyPreset}
861
- onDeletePreset={preferences.deletePreset}
862
- onToggleColumn={toggleColumn}
863
- onToggleStatus={toggleStatusColumn}
864
- onToggleID={toggleIDColumn}
865
- onToggleCreated={toggleCreatedColumn}
866
- onToggleUpdated={toggleUpdatedColumn}
867
- onSelectPageSize={selectPageSize}
868
- />
200
+ {#key slug}
201
+ <ListToolbar bind:searchInput />
202
+ {/key}
869
203
 
870
- <CollectionListResults
871
- i18n={runtime.i18n}
872
- {controller}
873
- resourceKey={slug}
874
- {collection}
875
- {titleField}
876
- {customListCells}
877
- {visibleColumns}
878
- {showID}
879
- {showCreated}
880
- {showUpdated}
881
- parentFieldName={parentField?.name}
882
- {hierarchy}
883
- {trashOnly}
884
- filtered={query !== "" ||
885
- activeStatus !== "" ||
886
- requestedFolder !== "" ||
887
- listFilters.length > 0}
888
- {sortField}
889
- {sortDescending}
890
- {columnValue}
891
- onClearFilters={clearFiltersAndFocusSearch}
892
- onToggleSort={toggleSort}
893
- onSelectPage={selectPage}
894
- onRequestDeletePermanent={(id) => requestConfirmation({ kind: "delete-document", id })}
895
- >
896
- {#snippet documentLink(document)}
897
- <Link class="block max-w-[600px]" to={contentPath(documentPath(slug, document.id))}>
898
- <span
899
- class="block truncate text-[14px] font-medium text-foreground-strong transition-colors group-hover:text-primary"
900
- >
901
- {title(document)}
902
- </span>
903
- </Link>
904
- {/snippet}
905
- {#snippet createAction()}
906
- <Link
907
- class={buttonVariants({ variant: "outline" })}
908
- to={contentPath(createDocumentPath(slug))}
909
- >
910
- {runtime.i18n.t("collections:createNew", {
911
- label:
912
- collection?.labels.singular.toLocaleLowerCase(runtime.i18n.language) ??
913
- runtime.i18n.t("collections:document"),
914
- })}
915
- </Link>
916
- {/snippet}
917
- </CollectionListResults>
204
+ <CollectionListResults onClearFilters={clearFiltersAndFocusSearch} />
918
205
  </section>
919
206
 
920
- <CollectionListBulkActions
921
- i18n={runtime.i18n}
922
- {controller}
923
- {trashOnly}
924
- versioned={collection?.capabilities.versions === true}
925
- hasBulkEditableFields={bulkEditableFields.length > 0}
926
- onEdit={openBulkEdit}
927
- onRequestDelete={() => requestConfirmation({ kind: "delete-selected" })}
928
- onRequestRestore={() => requestConfirmation({ kind: "restore-selected" })}
929
- onRequestDeletePermanent={() => requestConfirmation({ kind: "delete-selected-permanently" })}
930
- onOpenDocument={(id) => navigate(contentPath(documentPath(slug, id)))}
931
- />
932
-
933
- <ConfirmationDialog
934
- bind:open={confirmationOpen}
935
- title={confirmationCopy.title}
936
- description={confirmationCopy.description}
937
- confirmLabel={confirmationCopy.confirmLabel}
938
- cancelLabel={runtime.i18n.t("general:cancel")}
939
- destructive={confirmationCopy.destructive}
940
- disabled={bulkPending}
941
- onconfirm={performConfirmation}
942
- />
207
+ <BulkConfirmation bind:open={confirmationOpen} action={confirmationAction} />
943
208
 
944
- <CollectionListBulkEditor
945
- bind:open={bulkEditOpen}
946
- selectedCount={selectedIDs.size}
947
- fields={bulkEditableFields}
948
- pending={bulkPending}
949
- onApply={applyBulkEdit}
950
- />
209
+ <CollectionListBulkEditorLoader bind:open={bulkEditOpen} />