@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
@@ -0,0 +1,513 @@
1
+ import { untrack } from "svelte";
2
+ import type { FieldAuthoringHost } from "@riducms/plugin";
3
+ import type { SchemaField } from "@riducms/protocol";
4
+ import type { AdminDocument } from "@admin/core/api/admin-client";
5
+ import type { FormController } from "@admin/core/forms/form-controller.svelte";
6
+ import type { AdminRuntime } from "@admin/core/runtime/admin-runtime.svelte";
7
+ import type { NotificationCenter } from "@admin/core/notifications/notification-center.svelte";
8
+ import { readDocumentPath } from "@admin/core/schema/read-document-path";
9
+ import { createCollectionListCellFormatter } from "@admin/features/collections/collection-list-cell-values";
10
+ import { resolveListColumns } from "@admin/features/collections/list-columns";
11
+ import type { ListColumn, ListColumnSelection } from "@admin/features/collections/list-workspace";
12
+ import { documentLabel } from "@admin/features/documents/document-title";
13
+ import {
14
+ defaultJoinColumns,
15
+ joinDefaultValues,
16
+ joinDocuments,
17
+ joinReference,
18
+ sortJoinDocuments,
19
+ } from "@admin/fields/join/join-field-values";
20
+
21
+ interface JoinFieldOptions {
22
+ runtime: AdminRuntime;
23
+ notifications: NotificationCenter;
24
+ readonly field: SchemaField;
25
+ readonly form: FormController;
26
+ readonly authoring: FieldAuthoringHost | undefined;
27
+ }
28
+
29
+ type BrowserMode = "manage" | "create" | "edit";
30
+
31
+ /** One mounted join owns its saved rows, authoring requests and reference-label cache. */
32
+ export class JoinFieldController {
33
+ #browser = $state<{ open: boolean; owner: string; mode: BrowserMode; documentID?: string }>();
34
+ #pending = $state(false);
35
+ #mutationError = $state<string>();
36
+ #mutationRequest?: AbortController;
37
+ #refreshRequest?: AbortController;
38
+ #labelRequest = new AbortController();
39
+ #labelPending = new Set<string>();
40
+ #referenceDocuments = $state.raw<Record<string, AdminDocument | null>>({});
41
+ #disposed = false;
42
+ #columnOverride = $state<{ owner: string; columns: ListColumnSelection[] }>();
43
+ #sortOverride = $state<{ owner: string; value: string }>();
44
+
45
+ #owner = $derived.by(() =>
46
+ JSON.stringify([
47
+ this.options.form.resource?.collection,
48
+ this.options.form.resource?.id,
49
+ this.options.form.resource?.global,
50
+ this.options.form.contentLocale,
51
+ this.options.field.path,
52
+ this.options.field.join?.collectionId,
53
+ ])
54
+ );
55
+ #target = $derived.by(() =>
56
+ this.options.authoring?.collections.find(
57
+ (collection) => collection.id === this.options.field.join?.collectionId
58
+ )
59
+ );
60
+ #operations = $derived.by(() =>
61
+ this.#target ? this.options.runtime.collectionOperations[this.#target.slug] : undefined
62
+ );
63
+ #documents = $derived.by(() => joinDocuments(this.options.form.get(this.options.field.path)));
64
+
65
+ #columns = $derived.by(() => resolveListColumns(this.#target, this.options.runtime.i18n));
66
+ #defaultColumns = $derived.by(() =>
67
+ defaultJoinColumns(this.options.field, this.#target, this.#columns)
68
+ );
69
+ #columnSelection = $derived.by(() =>
70
+ this.#columnOverride?.owner === this.#owner
71
+ ? this.#columnOverride.columns
72
+ : this.#defaultColumns
73
+ );
74
+ #visibleColumns = $derived.by(() =>
75
+ this.#columnSelection.flatMap((selection) => {
76
+ const column =
77
+ selection.active && this.#columns.find((column) => column.path === selection.path);
78
+ return column ? [column] : [];
79
+ })
80
+ );
81
+
82
+ #sort = $derived.by(() =>
83
+ this.#sortOverride?.owner === this.#owner
84
+ ? this.#sortOverride.value
85
+ : (this.options.field.join?.defaultSort ?? "")
86
+ );
87
+ #sortedDocuments = $derived.by(() =>
88
+ sortJoinDocuments(
89
+ this.#documents,
90
+ this.#sort,
91
+ this.#columns,
92
+ this.options.runtime.i18n.language,
93
+ this.referenceText
94
+ )
95
+ );
96
+ #formatCell: ReturnType<typeof createCollectionListCellFormatter>;
97
+
98
+ constructor(private readonly options: JoinFieldOptions) {
99
+ this.#formatCell = createCollectionListCellFormatter({
100
+ i18n: options.runtime.i18n,
101
+ get collections() {
102
+ return options.authoring?.collections ?? [];
103
+ },
104
+ // Computed join rows have already passed server-side field redaction.
105
+ canReadField: () => true,
106
+ });
107
+
108
+ // Revoke callbacks and pending writes before a retained field switches document or locale.
109
+ $effect.pre(() => {
110
+ this.#owner;
111
+ return () => {
112
+ this.#browser = undefined;
113
+ this.#mutationRequest?.abort();
114
+ this.#refreshRequest?.abort();
115
+ this.#mutationRequest = undefined;
116
+ this.#refreshRequest = undefined;
117
+ this.#pending = false;
118
+ this.#mutationError = undefined;
119
+ };
120
+ });
121
+
122
+ // Label results belong to one source, locale and saved-document revision.
123
+ $effect.pre(() => {
124
+ this.#owner;
125
+ this.options.runtime.documentRevision;
126
+ return () => {
127
+ this.#labelRequest.abort();
128
+ this.#labelRequest = new AbortController();
129
+ this.#labelPending.clear();
130
+ this.#referenceDocuments = {};
131
+ };
132
+ });
133
+
134
+ $effect(() => {
135
+ this.#owner;
136
+ this.options.runtime.documentRevision;
137
+ const references = this.#visibleColumns.flatMap((column) => {
138
+ const field = column.field;
139
+ if (!field?.relationship && !field?.upload) return [];
140
+
141
+ return this.#documents.flatMap((document) => {
142
+ const raw = readDocumentPath(document, column.path);
143
+ return (Array.isArray(raw) ? raw : [raw]).flatMap((value) => {
144
+ const reference = joinReference(
145
+ value,
146
+ field,
147
+ this.options.authoring?.collections ?? []
148
+ );
149
+ return reference &&
150
+ (reference.populated === undefined ||
151
+ reference.populated === null ||
152
+ reference.populated === "")
153
+ ? [reference]
154
+ : [];
155
+ });
156
+ });
157
+ });
158
+ // Cache reads are bookkeeping; completed label requests must not launch another load.
159
+ untrack(() => this.#loadReferenceLabels(references));
160
+ });
161
+
162
+ $effect(() => () => {
163
+ this.#disposed = true;
164
+ this.#mutationRequest?.abort();
165
+ this.#refreshRequest?.abort();
166
+ this.#labelRequest.abort();
167
+ });
168
+ }
169
+
170
+ get targetCollection() {
171
+ return this.#target;
172
+ }
173
+ get sourceID() {
174
+ return this.options.form.resource?.global ? undefined : this.options.form.resource?.id;
175
+ }
176
+
177
+ get canCreate() {
178
+ return (
179
+ this.sourceID !== undefined &&
180
+ !this.options.field.admin.readOnly &&
181
+ this.options.field.join?.allowCreate !== false &&
182
+ this.#operations?.create === true
183
+ );
184
+ }
185
+ get canManage() {
186
+ return (
187
+ !this.options.field.admin.readOnly &&
188
+ this.sourceID !== undefined &&
189
+ this.#operations?.update === true
190
+ );
191
+ }
192
+ get canOpenDocument() {
193
+ return this.sourceID !== undefined && this.#operations?.read === true;
194
+ }
195
+ get editingBlocked() {
196
+ return this.options.form.editingBlocked;
197
+ }
198
+
199
+ get pending() {
200
+ return this.#pending;
201
+ }
202
+ get mutationError() {
203
+ return this.#mutationError;
204
+ }
205
+
206
+ get documents() {
207
+ return this.#documents;
208
+ }
209
+ get sortedDocuments() {
210
+ return this.#sortedDocuments;
211
+ }
212
+
213
+ get availableColumns() {
214
+ return this.#columns;
215
+ }
216
+ get columnSelection() {
217
+ return this.#columnSelection;
218
+ }
219
+ get visibleColumns() {
220
+ return this.#visibleColumns;
221
+ }
222
+ get sort() {
223
+ return this.#sort;
224
+ }
225
+
226
+ get browserOpen() {
227
+ return this.#browser?.owner === this.#owner && this.#browser.open;
228
+ }
229
+ get browserMode() {
230
+ return this.#browser?.mode;
231
+ }
232
+ get browserDocumentID() {
233
+ return this.#browser?.documentID;
234
+ }
235
+ get browserReadOnly() {
236
+ return (
237
+ this.options.field.admin.readOnly ||
238
+ this.editingBlocked ||
239
+ (this.browserMode === "create"
240
+ ? !this.canCreate
241
+ : this.browserMode === "edit"
242
+ ? this.#operations?.update !== true
243
+ : !this.canManage)
244
+ );
245
+ }
246
+ get defaultValues() {
247
+ return joinDefaultValues(this.options.field.join?.on ?? "", this.sourceID ?? "");
248
+ }
249
+
250
+ setColumns = (columns: ListColumnSelection[]) => {
251
+ this.#columnOverride = { owner: this.#owner, columns };
252
+ };
253
+
254
+ setSort = (path: string, descending: boolean) => {
255
+ this.#sortOverride = { owner: this.#owner, value: `${descending ? "-" : ""}${path}` };
256
+ };
257
+
258
+ setBrowserOpen = (open: boolean) => {
259
+ if (this.#browser) this.#browser.open = open;
260
+ };
261
+
262
+ openBrowser = (mode: BrowserMode, documentID?: string) => {
263
+ if (
264
+ this.#disposed ||
265
+ this.sourceID === undefined ||
266
+ !this.#target ||
267
+ !this.options.authoring?.referenceBrowser
268
+ )
269
+ return;
270
+ this.#browser = { open: true, owner: this.#owner, mode, documentID };
271
+ };
272
+
273
+ referenceText = (value: unknown, field: SchemaField): string => {
274
+ const i18n = this.options.runtime.i18n;
275
+ if (Array.isArray(value))
276
+ return i18n.formatList(value.map((value) => this.referenceText(value, field)));
277
+
278
+ const reference = joinReference(value, field, this.options.authoring?.collections ?? []);
279
+ if (!reference) return "—";
280
+ if (
281
+ reference.populated !== undefined &&
282
+ reference.populated !== null &&
283
+ reference.populated !== ""
284
+ )
285
+ return String(reference.populated);
286
+
287
+ const document = this.#referenceDocuments[reference.key];
288
+ const label = document ? documentLabel(reference.collection, document) : undefined;
289
+ return label && label !== reference.id ? label : i18n.t("fields:relatedDocument");
290
+ };
291
+
292
+ cellText = (document: AdminDocument, column: ListColumn) => {
293
+ if (column.field?.relationship || column.field?.upload)
294
+ return this.referenceText(readDocumentPath(document, column.path), column.field);
295
+ return column.field
296
+ ? (this.#formatCell(document, column.field) ?? "—")
297
+ : String(readDocumentPath(document, column.path) ?? "—");
298
+ };
299
+
300
+ async #loadReferenceLabels(references: NonNullable<ReturnType<typeof joinReference>>[]) {
301
+ const groups = new Map<string, Map<string, string>>();
302
+ for (const reference of references) {
303
+ if (reference.key in this.#referenceDocuments || this.#labelPending.has(reference.key))
304
+ continue;
305
+
306
+ this.#labelPending.add(reference.key);
307
+ const group = groups.get(reference.collection.slug) ?? new Map<string, string>();
308
+ group.set(reference.id, reference.key);
309
+ groups.set(reference.collection.slug, group);
310
+ }
311
+
312
+ const signal = this.#labelRequest.signal;
313
+ const locale = this.options.form.contentLocale;
314
+
315
+ // Bound request size, not the number of labels the author can resolve.
316
+ for (const [slug, group] of groups) {
317
+ const ids = [...group.keys()];
318
+
319
+ for (let offset = 0; offset < ids.length; offset += 100) {
320
+ if (signal.aborted || this.#disposed) return;
321
+
322
+ const batch = ids.slice(offset, offset + 100);
323
+ const resolved: Record<string, AdminDocument | null> = Object.fromEntries(
324
+ batch.map((id) => [group.get(id)!, null])
325
+ );
326
+
327
+ try {
328
+ let page = 1;
329
+ while (true) {
330
+ const result = await this.options.runtime.client.list(slug, {
331
+ where: { id: { in: batch } },
332
+ limit: 100,
333
+ page,
334
+ depth: 0,
335
+ locale,
336
+ signal,
337
+ });
338
+ if (signal.aborted || this.#disposed) return;
339
+
340
+ for (const document of result.docs) {
341
+ const key = group.get(document.id);
342
+ if (key) resolved[key] = document;
343
+ }
344
+
345
+ if (page >= result.pagination.totalPages) break;
346
+ page++;
347
+ }
348
+ } catch {
349
+ // Denied, deleted or unavailable references retain the neutral label until refresh.
350
+ }
351
+ if (signal.aborted || this.#disposed) return;
352
+
353
+ this.#referenceDocuments = { ...this.#referenceDocuments, ...resolved };
354
+ for (const id of batch) this.#labelPending.delete(group.get(id)!);
355
+ }
356
+ }
357
+ }
358
+
359
+ commit = async (ids: string[]) => {
360
+ if (
361
+ this.#disposed ||
362
+ this.editingBlocked ||
363
+ this.options.field.admin.readOnly ||
364
+ (this.browserMode !== "create" && !this.canManage)
365
+ )
366
+ return false;
367
+
368
+ this.#refreshRequest?.abort();
369
+ const source = this.options.form.resource;
370
+ if (source?.id === undefined || source.global || !this.#target || !this.options.field.join)
371
+ return false;
372
+
373
+ const owner = this.#owner;
374
+ const request = new AbortController();
375
+ this.#mutationRequest?.abort();
376
+ this.#mutationRequest = request;
377
+ const current = () =>
378
+ !this.#disposed &&
379
+ !request.signal.aborted &&
380
+ this.#mutationRequest === request &&
381
+ owner === this.#owner;
382
+
383
+ const { collection, id } = source;
384
+ const {
385
+ path,
386
+ admin: { label },
387
+ } = this.options.field;
388
+ const locale = this.options.form.contentLocale;
389
+ const creating = this.browserMode === "create";
390
+
391
+ this.#pending = true;
392
+ this.#mutationError = undefined;
393
+
394
+ try {
395
+ let confirmed = this.#documents;
396
+ if (creating) {
397
+ // Creation already saved the backlink; attaching it again could require update access.
398
+ const refreshed = await this.#refresh(collection, id, path, locale, request.signal);
399
+ if (!current()) return false;
400
+ this.#documents = refreshed;
401
+ confirmed = refreshed;
402
+ if (ids.every((id) => refreshed.some((document) => document.id === id))) return true;
403
+ if (!this.canManage) return false;
404
+ }
405
+
406
+ const currentIDs = new Set(confirmed.map((document) => document.id));
407
+ const requested = new Set(ids);
408
+ const additions = ids.filter((id) => !currentIDs.has(id));
409
+ const removals = creating
410
+ ? []
411
+ : confirmed
412
+ .filter((document) => !requested.has(document.id))
413
+ .map((document) => document.id);
414
+ if (additions.length === 0 && removals.length === 0) return true;
415
+
416
+ const result = await this.options.runtime.client.mutateJoin(
417
+ collection,
418
+ id,
419
+ path,
420
+ { additions, removals },
421
+ { signal: request.signal, locale }
422
+ );
423
+ if (!current()) return false;
424
+
425
+ this.#documents = joinDocuments(readDocumentPath(result.doc, path));
426
+ this.options.runtime.documentsChanged();
427
+ this.options.notifications.success({
428
+ title: this.options.runtime.i18n.t("fields:updated", { label }),
429
+ message: this.options.runtime.i18n.t("fields:joinChanges", {
430
+ added: result.added,
431
+ removed: result.removed,
432
+ }),
433
+ });
434
+ return true;
435
+ } catch (cause) {
436
+ if (!current()) return false;
437
+
438
+ this.#mutationError =
439
+ cause instanceof Error
440
+ ? cause.message
441
+ : this.options.runtime.i18n.t("errors:relatedDocumentsUpdate");
442
+ const refreshed = await this.#refresh(collection, id, path, locale, request.signal).catch(
443
+ () => undefined
444
+ );
445
+ if (!current()) return false;
446
+ if (refreshed !== undefined) this.#documents = refreshed;
447
+
448
+ this.options.notifications.error({
449
+ title: this.options.runtime.i18n.t("errors:updateUnconfirmed", { label }),
450
+ message: this.#mutationError,
451
+ });
452
+ return false;
453
+ } finally {
454
+ if (current()) {
455
+ this.#pending = false;
456
+ this.#mutationRequest = undefined;
457
+ }
458
+ }
459
+ };
460
+
461
+ async #refresh(
462
+ collection: string,
463
+ id: string,
464
+ path: string,
465
+ locale: string | undefined,
466
+ signal: AbortSignal
467
+ ) {
468
+ const document = await this.options.runtime.client.find(collection, id, { locale, signal });
469
+ return joinDocuments(readDocumentPath(document, path));
470
+ }
471
+
472
+ browserClosed = async () => {
473
+ const browser = this.#browser;
474
+ this.setBrowserOpen(false);
475
+ const source = this.options.form.resource;
476
+ if (
477
+ browser?.owner !== this.#owner ||
478
+ this.#disposed ||
479
+ source?.id === undefined ||
480
+ source.global
481
+ )
482
+ return;
483
+
484
+ const owner = this.#owner;
485
+ const path = this.options.field.path;
486
+ const locale = this.options.form.contentLocale;
487
+ const request = new AbortController();
488
+ this.#refreshRequest?.abort();
489
+ this.#refreshRequest = request;
490
+
491
+ try {
492
+ const refreshed = await this.#refresh(
493
+ source.collection,
494
+ source.id,
495
+ path,
496
+ locale,
497
+ request.signal
498
+ );
499
+ if (
500
+ this.#disposed ||
501
+ request.signal.aborted ||
502
+ this.#refreshRequest !== request ||
503
+ this.#owner !== owner
504
+ )
505
+ return;
506
+ this.#documents = refreshed;
507
+ } catch {
508
+ // A failed refresh leaves the last confirmed rows visible.
509
+ } finally {
510
+ if (this.#refreshRequest === request) this.#refreshRequest = undefined;
511
+ }
512
+ };
513
+ }
@@ -0,0 +1,111 @@
1
+ import { isRecord } from "@riducms/protocol";
2
+ import type { SchemaCollection, SchemaField } from "@riducms/protocol";
3
+ import type { AdminDocument } from "@admin/core/api/admin-client";
4
+ import { readDocumentPath } from "@admin/core/schema/read-document-path";
5
+ import { documentTitleField } from "@admin/features/documents/document-title";
6
+ import type { ListColumn, ListColumnSelection } from "@admin/features/collections/list-workspace";
7
+
8
+ export function joinDocuments(value: unknown) {
9
+ return Array.isArray(value)
10
+ ? value.filter(
11
+ (document): document is AdminDocument =>
12
+ isRecord(document) && typeof document.id === "string"
13
+ )
14
+ : [];
15
+ }
16
+
17
+ export function defaultJoinColumns(
18
+ field: SchemaField,
19
+ collection: SchemaCollection | undefined,
20
+ available: readonly ListColumn[]
21
+ ): ListColumnSelection[] {
22
+ const configured = field.join?.defaultColumns ?? collection?.admin.defaultColumns ?? [];
23
+ const defaults =
24
+ configured.length > 0
25
+ ? configured
26
+ : [documentTitleField(collection)?.path ?? "id", "updatedAt"];
27
+ const selected = new Set(defaults);
28
+ const byPath = new Set(available.map((column) => column.path));
29
+ return [
30
+ ...defaults.filter((path) => byPath.has(path)).map((path) => ({ path, active: true })),
31
+ ...available
32
+ .filter((column) => !selected.has(column.path))
33
+ .map((column) => ({ path: column.path, active: false })),
34
+ ];
35
+ }
36
+
37
+ export function joinDefaultValues(path: string, id: string) {
38
+ const values: Record<string, unknown> = {};
39
+ const segments = path.split(".").filter(Boolean);
40
+ let current = values;
41
+ for (const segment of segments.slice(0, -1)) {
42
+ const nested: Record<string, unknown> = {};
43
+ current[segment] = nested;
44
+ current = nested;
45
+ }
46
+ const last = segments.at(-1);
47
+ if (last !== undefined) current[last] = id;
48
+ return values;
49
+ }
50
+
51
+ /** Resolve either a monomorphic reference or the populated { relationTo, id: document } shape. */
52
+ export function joinReference(
53
+ value: unknown,
54
+ field: SchemaField,
55
+ collections: readonly SchemaCollection[]
56
+ ) {
57
+ const configured = field.relationship?.collectionSlug ?? field.upload?.collectionSlug;
58
+ const slug =
59
+ isRecord(value) && typeof value.relationTo === "string" ? value.relationTo : configured;
60
+ const collection = collections.find((collection) => collection.slug === slug);
61
+ const document = isRecord(value) ? (isRecord(value.id) ? value.id : value) : undefined;
62
+ const id = typeof value === "string" ? value : document?.id;
63
+ if (!collection || typeof id !== "string") return undefined;
64
+
65
+ const title = documentTitleField(collection);
66
+ const populated = title ? readDocumentPath(document, title.path) : undefined;
67
+ return { collection, id, key: `${collection.slug}\u001f${id}`, populated };
68
+ }
69
+
70
+ /** Sort stored scalar values; localized cell text is never a date or numeric sort key. */
71
+ export function sortJoinDocuments(
72
+ documents: readonly AdminDocument[],
73
+ sort: string,
74
+ columns: readonly ListColumn[],
75
+ language: string,
76
+ referenceLabel: (value: unknown, field: SchemaField) => string
77
+ ) {
78
+ if (sort === "") return documents;
79
+
80
+ const descending = sort.startsWith("-");
81
+ const path = descending ? sort.slice(1) : sort;
82
+ const field = columns.find((column) => column.path === path)?.field;
83
+ const collator = new Intl.Collator(language, { numeric: true });
84
+ const key = (document: AdminDocument): string | number | boolean | undefined => {
85
+ const value = readDocumentPath(document, path);
86
+ if (value === undefined || value === null || value === "") return undefined;
87
+ if (field?.relationship || field?.upload) return referenceLabel(value, field);
88
+ if (field?.type === "date" && field.date?.format !== "time") {
89
+ const time = Date.parse(String(value));
90
+ return Number.isNaN(time) ? undefined : time;
91
+ }
92
+ return typeof value === "number" || typeof value === "boolean" || typeof value === "string"
93
+ ? value
94
+ : undefined;
95
+ };
96
+ return documents
97
+ .map((document) => ({ document, value: key(document) }))
98
+ .sort((left, right) => {
99
+ // Keep absent/redacted values last in either direction.
100
+ if (left.value === undefined) return right.value === undefined ? 0 : 1;
101
+ if (right.value === undefined) return -1;
102
+ const comparison =
103
+ typeof left.value === "number" && typeof right.value === "number"
104
+ ? left.value - right.value
105
+ : typeof left.value === "boolean" && typeof right.value === "boolean"
106
+ ? Number(left.value) - Number(right.value)
107
+ : collator.compare(String(left.value), String(right.value));
108
+ return descending ? -comparison : comparison;
109
+ })
110
+ .map(({ document }) => document);
111
+ }