@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,624 +1,204 @@
1
1
  <script lang="ts">
2
- import { Link, useLocation, useParams } from "@hvniel/svelte-router";
3
- import type { SchemaField, ValidationIssue } from "@riducms/protocol";
4
- import { RiduError } from "@riducms/sdk";
5
- import { tick } from "svelte";
6
- import ArrowLeftIcon from "~icons/lucide/arrow-left";
7
- import CheckIcon from "~icons/lucide/check";
8
- import FileUpIcon from "~icons/lucide/file-up";
9
- import RotateCcwIcon from "~icons/lucide/rotate-ccw";
10
- import Trash2Icon from "~icons/lucide/trash-2";
11
- import LinkIcon from "~icons/lucide/link";
2
+ import { Dialog as DialogPrimitive } from "bits-ui";
3
+ import { Link, useBlocker, useLocation, useNavigate, useParams } from "@hvniel/svelte-router";
4
+ import {
5
+ Button,
6
+ buttonVariants,
7
+ Dialog as AdminDialog,
8
+ DialogContent,
9
+ DialogDescription,
10
+ DialogFooter,
11
+ DialogHeader,
12
+ DialogTitle,
13
+ } from "@riducms/ui";
14
+ import XIcon from "~icons/lucide/x";
12
15
 
13
16
  import { Banner } from "@admin/components/ui/banner";
14
- import { Button, buttonVariants } from "@admin/components/ui/button";
15
- import { Input } from "@admin/components/ui/input";
17
+ import { getAdminBootstrapCoordinator } from "@admin/core/bootstrap/admin-bootstrap";
18
+ import { normalizedURLIdentity } from "@admin/core/bootstrap/admin-prepared-state";
16
19
  import { getNotificationCenter } from "@admin/core/notifications/notification-center.svelte";
17
- import { collectionPath, documentPath } from "@admin/core/routing/admin-paths";
20
+ import { collectionPath, withContentLocale } from "@admin/core/routing/admin-paths";
21
+ import { registerAdminScrollPage } from "@admin/core/routing/admin-scroll.svelte";
18
22
  import { getAdminRuntime } from "@admin/core/runtime/admin-runtime.svelte";
19
- import {
20
- bulkUploadMetadataFields,
21
- initialBulkUploadValues,
22
- uploadData,
23
- } from "@admin/features/uploads/bulk-upload";
24
- import UploadMetadataFields from "@admin/features/uploads/upload-metadata-fields.svelte";
23
+ import { BulkUploadController } from "@admin/features/uploads/bulk-upload-controller.svelte";
24
+ import BulkUploadEditAll from "@admin/features/uploads/bulk-upload-edit-all.svelte";
25
+ import BulkUploadEditor from "@admin/features/uploads/bulk-upload-editor.svelte";
26
+ import BulkUploadFilePicker from "@admin/features/uploads/bulk-upload-file-picker.svelte";
27
+ import BulkUploadRemote from "@admin/features/uploads/bulk-upload-remote.svelte";
28
+ import BulkUploadSidebar from "@admin/features/uploads/bulk-upload-sidebar.svelte";
25
29
 
26
- type QueueStatus = "queued" | "uploading" | "complete" | "failed" | "uncertain";
27
- type QueueItem = {
28
- id: string;
29
- file: File;
30
- values: Record<string, string>;
31
- status: QueueStatus;
32
- error?: string;
33
- issues: readonly ValidationIssue[];
34
- documentID?: string;
35
- };
30
+ import "@admin/features/uploads/bulk-upload.scss";
36
31
 
37
32
  const runtime = getAdminRuntime();
33
+ const bootstrap = getAdminBootstrapCoordinator();
38
34
  const notifications = getNotificationCenter();
39
- const params = $derived(useParams<"collection">());
40
- const location = $derived(useLocation());
41
- const slug = $derived(params.collection ?? "");
35
+ const navigate = useNavigate();
36
+ const params = useParams<"collection">();
37
+ const location = useLocation();
38
+ const slug = $derived(params.current.collection ?? "");
42
39
  const collection = $derived(runtime.manifest?.collections.find((item) => item.slug === slug));
43
- const requestedLocale = $derived(new URLSearchParams(location.search).get("locale"));
44
- const contentLocale = $derived(
45
- runtime.manifest?.application.localization?.locales.some(
46
- (locale) => locale.code === requestedLocale
40
+ const requestedLocale = $derived(new URLSearchParams(location.current.search).get("locale"));
41
+ const contentLocale = $derived(runtime.resolveContentLocale(requestedLocale));
42
+ const activeLocaleConfig = $derived(
43
+ runtime.manifest?.application.localization?.locales.find(
44
+ (locale) => locale.code === contentLocale
47
45
  )
48
- ? (requestedLocale ?? undefined)
49
- : undefined
50
46
  );
51
- const metadataFields = $derived(bulkUploadMetadataFields(collection?.fields ?? []));
52
- const acceptedTypes = $derived(collection?.uploadSettings?.mimeTypes?.join(",") ?? "");
53
- const acceptedTypeLabels = $derived(
54
- runtime.i18n.formatList(collection?.uploadSettings?.mimeTypes ?? [], {
55
- style: "short",
56
- type: "disjunction",
57
- })
47
+ const contentDirection = $derived<"ltr" | "rtl">(
48
+ activeLocaleConfig?.rtl === true ? "rtl" : "ltr"
58
49
  );
59
- const uploadEnabled = $derived(collection?.capabilities.upload === true);
60
- let queue = $state<QueueItem[]>([]);
61
- let running = $state(false);
62
- let accessLoading = $state(true);
63
- let canCreate = $state(false);
64
- let accessError = $state<string>();
65
- let remoteURL = $state("");
66
- let remoteValues = $state<Record<string, string>>({});
67
- let remotePending = $state(false);
68
- let remoteError = $state<string>();
69
- let remoteOutcomeUncertain = $state(false);
70
- let remoteAttemptURL = "";
71
- let remoteIssues = $state<readonly ValidationIssue[]>([]);
72
- let remoteDocumentID = $state<string>();
73
- let nextID = 0;
74
- let pageElement: HTMLElement;
75
- let queueRequest: AbortController | undefined;
76
- let remoteRequest: AbortController | undefined;
77
- const completed = $derived(queue.filter((item) => item.status === "complete").length);
78
- const failed = $derived(queue.filter((item) => item.status === "failed").length);
79
- const uncertain = $derived(queue.filter((item) => item.status === "uncertain").length);
80
- const pending = $derived(queue.filter((item) => item.status === "queued").length);
81
- const currentUpload = $derived(queue.find((item) => item.status === "uploading"));
50
+ const preparedRoute = $derived(
51
+ bootstrap.routeData(location.current.pathname, location.current.search)
52
+ );
53
+ const closePath = $derived(withContentLocale(collectionPath(slug), contentLocale));
54
+ const controller = new BulkUploadController({
55
+ runtime,
56
+ notifications,
57
+ get slug() {
58
+ return slug;
59
+ },
60
+ get locale() {
61
+ return contentLocale;
62
+ },
63
+ get collection() {
64
+ return collection;
65
+ },
66
+ get routeIdentity() {
67
+ return normalizedURLIdentity(location.current);
68
+ },
69
+ get preparedAccess() {
70
+ return preparedRoute?.kind === "upload" ? preparedRoute.access : undefined;
71
+ },
72
+ });
73
+ registerAdminScrollPage({ ready: () => !controller.accessLoading });
74
+ const navigationBlocker = useBlocker(({ currentLocation, nextLocation }) => {
75
+ if (!controller.dirty) return false;
76
+ const nextLocale = runtime.resolveContentLocale(
77
+ new URLSearchParams(nextLocation.search).get("locale")
78
+ );
79
+ return currentLocation.pathname !== nextLocation.pathname || nextLocale !== contentLocale;
80
+ });
81
+
82
+ $effect(() => runtime.registerContentLocaleBlocker(() => controller.dirty));
82
83
 
83
84
  $effect(() => {
84
- const currentSlug = slug;
85
- queueRequest?.abort();
86
- remoteRequest?.abort();
87
- queueRequest = undefined;
88
- remoteRequest = undefined;
89
- queue = [];
90
- running = false;
91
- canCreate = false;
92
- remoteURL = "";
93
- remoteValues = {};
94
- remotePending = false;
95
- remoteError = undefined;
96
- remoteOutcomeUncertain = false;
97
- remoteAttemptURL = "";
98
- remoteIssues = [];
99
- remoteDocumentID = undefined;
100
- const request = new AbortController();
101
- accessLoading = true;
102
- accessError = undefined;
103
- runtime.client
104
- .collectionAccess(currentSlug, { signal: request.signal, locale: contentLocale })
105
- .then((access) => {
106
- if (!request.signal.aborted) canCreate = access.operations.create;
107
- })
108
- .catch((cause: unknown) => {
109
- if (!request.signal.aborted) {
110
- accessError =
111
- cause instanceof Error ? cause.message : runtime.i18n.t("uploads:accessCheckFailed");
112
- }
113
- })
114
- .finally(() => {
115
- if (!request.signal.aborted) accessLoading = false;
116
- });
117
- return () => {
118
- request.abort();
119
- queueRequest?.abort();
120
- remoteRequest?.abort();
85
+ if (!controller.dirty) return;
86
+ const preventUnload = (event: BeforeUnloadEvent) => {
87
+ event.preventDefault();
88
+ event.returnValue = "";
121
89
  };
90
+ window.addEventListener("beforeunload", preventUnload);
91
+ return () => window.removeEventListener("beforeunload", preventUnload);
122
92
  });
123
93
 
124
- function addFiles(event: Event) {
125
- if (!uploadEnabled || !canCreate || running) return;
126
- const input = event.currentTarget as HTMLInputElement;
127
- const files = Array.from(input.files ?? []);
128
- const known = new Set(queue.map((item) => fileIdentity(item.file)));
129
- for (const file of files) {
130
- if (known.has(fileIdentity(file))) continue;
131
- known.add(fileIdentity(file));
132
- queue.push({
133
- id: `upload-${++nextID}`,
134
- file,
135
- values: initialBulkUploadValues(file.name, metadataFields, collection?.admin.useAsTitle),
136
- status: "queued",
137
- issues: [],
138
- });
139
- }
140
- input.value = "";
141
- }
142
-
143
- function removeItem(id: string) {
144
- queue = queue.filter((item) => item.id !== id);
145
- }
146
-
147
- function refreshQueue() {
148
- queue = [...queue];
149
- }
150
-
151
- async function retry(item: QueueItem, trigger: HTMLElement) {
152
- if (running || !uploadEnabled || !canCreate) return;
153
- const row = trigger.closest<HTMLElement>("[data-upload-row]");
154
- item.status = "queued";
155
- item.error = undefined;
156
- item.issues = [];
157
- refreshQueue();
158
- await tick();
159
- row?.focus();
160
- await runUploads([item]);
161
- await tick();
162
- const finalStatus = item.status as QueueStatus;
163
- if (finalStatus === "complete") {
164
- row?.querySelector<HTMLElement>("[data-upload-result-action]")?.focus();
165
- } else if (finalStatus === "failed" && item.issues.length === 0) {
166
- row?.querySelector<HTMLElement>("[data-upload-result-action]")?.focus();
167
- } else if (finalStatus === "uncertain") {
168
- row?.focus();
169
- }
170
- }
171
-
172
- async function uploadAll() {
173
- await runUploads(queue);
174
- }
175
-
176
- async function runUploads(items: readonly QueueItem[]) {
177
- if (running || !uploadEnabled || !canCreate) return;
178
- const currentSlug = slug;
179
- const request = new AbortController();
180
- queueRequest?.abort();
181
- queueRequest = request;
182
- running = true;
183
- for (const item of items) {
184
- if (request.signal.aborted) return;
185
- if (item.status !== "queued") continue;
186
- item.status = "uploading";
187
- item.error = undefined;
188
- item.issues = [];
189
- refreshQueue();
190
- try {
191
- const data = uploadData(item.values);
192
- const document = await runtime.client.upload(currentSlug, item.file, {
193
- data,
194
- signal: request.signal,
195
- locale: contentLocale,
196
- });
197
- if (request.signal.aborted || slug !== currentSlug) return;
198
- item.status = "complete";
199
- item.documentID = document.id;
200
- refreshQueue();
201
- runtime.documentsChanged();
202
- } catch (cause) {
203
- if (request.signal.aborted) return;
204
- const confirmed = cause instanceof RiduError && cause.status < 500;
205
- item.status = confirmed ? "failed" : "uncertain";
206
- item.error = confirmed ? cause.message : runtime.i18n.t("uploads:outcomeNotConfirmed");
207
- item.issues = confirmed ? cause.issues : [];
208
- refreshQueue();
209
- }
210
- }
211
- if (request.signal.aborted || slug !== currentSlug) return;
212
- running = false;
213
- queueRequest = undefined;
214
- const failures = queue.filter((item) => item.status === "failed").length;
215
- const successes = queue.filter((item) => item.status === "complete").length;
216
- const uncertainOutcomes = queue.filter((item) => item.status === "uncertain").length;
217
- if (failures === 0 && uncertainOutcomes === 0 && successes > 0) {
218
- notifications.success({
219
- title: runtime.i18n.t("uploads:assetsUploaded", { count: successes }),
220
- });
221
- } else if (failures > 0 || uncertainOutcomes > 0) {
222
- notifications.error({
223
- title: runtime.i18n.t("uploads:needAttention", {
224
- count: failures + uncertainOutcomes,
225
- }),
226
- message:
227
- uncertainOutcomes > 0
228
- ? runtime.i18n.t("uploads:unknownOutcomesDescription")
229
- : runtime.i18n.t("uploads:correctMetadata"),
230
- });
231
- if (failures > 0) await focusFirstInvalid();
232
- }
233
- }
234
-
235
- async function uploadRemote(event: SubmitEvent) {
236
- event.preventDefault();
237
- if (remotePending || !uploadEnabled || !canCreate || remoteURL.trim() === "") return;
238
- const currentSlug = slug;
239
- const request = new AbortController();
240
- remoteRequest?.abort();
241
- remoteRequest = request;
242
- remotePending = true;
243
- remoteError = undefined;
244
- remoteOutcomeUncertain = false;
245
- remoteAttemptURL = remoteURL.trim();
246
- remoteIssues = [];
247
- remoteDocumentID = undefined;
248
- try {
249
- const defaults = initialBulkUploadValues(
250
- remoteFilename(remoteURL),
251
- metadataFields,
252
- collection?.admin.useAsTitle
253
- );
254
- const document = await runtime.client.uploadFromURL(currentSlug, remoteURL.trim(), {
255
- data: uploadData({ ...defaults, ...remoteValues }),
256
- signal: request.signal,
257
- locale: contentLocale,
258
- });
259
- if (request.signal.aborted || slug !== currentSlug) return;
260
- remoteDocumentID = document.id;
261
- runtime.documentsChanged();
262
- notifications.success({ title: runtime.i18n.t("uploads:remoteAssetUploaded") });
263
- } catch (cause) {
264
- if (request.signal.aborted) return;
265
- const confirmed = cause instanceof RiduError && cause.status < 500;
266
- remoteOutcomeUncertain = !confirmed;
267
- remoteError = confirmed ? cause.message : runtime.i18n.t("uploads:remoteOutcomeNotConfirmed");
268
- remoteIssues = confirmed ? cause.issues : [];
269
- if (remoteIssues.length > 0) await focusFirstInvalid("[data-remote-upload]");
270
- } finally {
271
- if (!request.signal.aborted && slug === currentSlug) {
272
- remotePending = false;
273
- remoteRequest = undefined;
274
- }
275
- }
276
- }
277
-
278
- function clearItemIssues(item: QueueItem, path: string, name: string) {
279
- item.issues = item.issues.filter((issue) => issue.path !== path && issue.path !== name);
280
- if (item.issues.length === 0 && item.status === "failed") item.error = undefined;
281
- refreshQueue();
282
- }
283
-
284
- function clearRemoteIssues(path: string, name: string) {
285
- remoteIssues = remoteIssues.filter((issue) => issue.path !== path && issue.path !== name);
286
- if (remoteIssues.length === 0) remoteError = undefined;
287
- }
288
-
289
- function updateRemoteMetadata(field: SchemaField, value: string) {
290
- remoteValues[field.name] = value;
291
- clearRemoteIssues(field.path, field.name);
292
- }
293
-
294
- function updateItemMetadata(item: QueueItem, field: SchemaField, value: string) {
295
- item.values = { ...item.values, [field.name]: value };
296
- clearItemIssues(item, field.path, field.name);
297
- }
298
-
299
- function updateRemoteURL(value: string) {
300
- remoteURL = value;
301
- if (remoteOutcomeUncertain && value.trim() !== remoteAttemptURL) {
302
- remoteOutcomeUncertain = false;
303
- remoteError = undefined;
304
- }
305
- }
306
-
307
- async function focusFirstInvalid(scope = "") {
308
- await tick();
309
- const selector = scope === "" ? '[aria-invalid="true"]' : `${scope} [aria-invalid="true"]`;
310
- const invalid = pageElement?.querySelector<HTMLElement>(selector);
311
- invalid?.scrollIntoView({ behavior: "smooth", block: "center" });
312
- invalid?.focus({ preventScroll: true });
313
- }
314
-
315
- function fileIdentity(file: File) {
316
- return `${file.name}\u0000${file.size}\u0000${file.lastModified}`;
317
- }
318
-
319
- function remoteFilename(value: string) {
320
- try {
321
- const filename = new URL(value).pathname.split("/").filter(Boolean).at(-1);
322
- return filename === undefined ? "remote-upload" : decodeURIComponent(filename);
323
- } catch {
324
- return "remote-upload";
325
- }
326
- }
94
+ $effect(() => {
95
+ if (!controller.dirty && navigationBlocker.state === "blocked") navigationBlocker.reset();
96
+ });
327
97
 
328
- function formatBytes(value: number) {
329
- if (value < 1_024) return runtime.i18n.formatNumber(value, { style: "unit", unit: "byte" });
330
- if (value < 1_048_576)
331
- return runtime.i18n.formatNumber(value / 1_024, {
332
- maximumFractionDigits: 0,
333
- style: "unit",
334
- unit: "kilobyte",
335
- });
336
- return runtime.i18n.formatNumber(value / 1_048_576, {
337
- maximumFractionDigits: 1,
338
- style: "unit",
339
- unit: "megabyte",
340
- });
98
+ function cancelNavigation() {
99
+ if (navigationBlocker.state === "blocked") navigationBlocker.reset();
341
100
  }
342
101
 
343
- function statusLabel(status: QueueStatus) {
344
- return runtime.i18n.t(
345
- `uploads:status${status[0]?.toLocaleUpperCase("en-US")}${status.slice(1)}` as
346
- | "uploads:statusQueued"
347
- | "uploads:statusUploading"
348
- | "uploads:statusComplete"
349
- | "uploads:statusFailed"
350
- | "uploads:statusUncertain"
351
- );
352
- }
353
-
354
- function localizedPath(path: string) {
355
- return contentLocale === undefined
356
- ? path
357
- : `${path}?${new URLSearchParams({ locale: contentLocale })}`;
102
+ function discardAndNavigate() {
103
+ if (controller.running || controller.remotePending || navigationBlocker.state !== "blocked")
104
+ return;
105
+ controller.discard();
106
+ navigationBlocker.proceed();
358
107
  }
359
108
  </script>
360
109
 
361
- <section
362
- bind:this={pageElement}
363
- class="mx-auto w-full max-w-[1020px] px-5 py-9 sm:px-8 sm:py-11 lg:px-11 lg:pb-22.5"
364
- aria-busy={running || remotePending}
110
+ <DialogPrimitive.Root
111
+ open
112
+ onOpenChange={(open) => {
113
+ if (!open) navigate(closePath);
114
+ }}
365
115
  >
366
- <header class="flex flex-wrap items-end justify-between gap-5">
367
- <div>
368
- <Link
369
- class="mb-5 inline-flex items-center gap-1.5 text-[12px] text-foreground-faint hover:text-foreground"
370
- to={localizedPath(collectionPath(slug))}
371
- >
372
- <ArrowLeftIcon class="size-3.5 rtl:rotate-180" />
373
- {collection === undefined
374
- ? slug
375
- : runtime.i18n.text(collection.labels.plural, collection.labels.pluralTranslations)}
376
- </Link>
377
- <h1 class="font-serif text-[34px] leading-none tracking-[0.004em] text-foreground">
378
- {runtime.i18n.t("uploads:bulkUpload")}
379
- </h1>
380
- <p class="mt-2 max-w-xl text-[13px] leading-5 text-foreground-sub">
116
+ <DialogPrimitive.Portal>
117
+ <DialogPrimitive.Overlay class="ridu-bulk-upload-overlay" />
118
+ <DialogPrimitive.Content
119
+ class="ridu-bulk-upload"
120
+ dir={runtime.i18n.direction}
121
+ aria-busy={controller.running || controller.remotePending}
122
+ >
123
+ <DialogPrimitive.Description class="ridu-bulk-upload-description">
381
124
  {runtime.i18n.t("uploads:bulkUploadDescription")}
382
- </p>
383
- </div>
384
- <div class="flex items-center gap-2">
385
- {#if queue.length > 0}
386
- <span
387
- class="font-mono me-2 text-[10px] text-foreground-faint"
388
- role="status"
389
- aria-live="polite"
390
- >
391
- {runtime.i18n.t("uploads:queueSummary", {
392
- current:
393
- currentUpload === undefined
394
- ? ""
395
- : runtime.i18n.t("uploads:uploadingFilename", {
396
- filename: currentUpload.file.name,
397
- }),
398
- complete: runtime.i18n.formatNumber(completed),
399
- queued: runtime.i18n.formatNumber(pending),
400
- failed: runtime.i18n.formatNumber(failed),
401
- uncertain: runtime.i18n.formatNumber(uncertain),
402
- })}
403
- </span>
125
+ </DialogPrimitive.Description>
126
+ {#if controller.queue.length === 0}
127
+ <header class="ridu-bulk-upload-header">
128
+ <DialogPrimitive.Title
129
+ level={1}
130
+ id="bulk-upload-title"
131
+ class="ridu-bulk-upload-header__title"
132
+ >
133
+ {runtime.i18n.t("uploads:addFiles")}
134
+ </DialogPrimitive.Title>
135
+ <Link
136
+ class={buttonVariants({ variant: "ghost", size: "icon-sm" })}
137
+ to={closePath}
138
+ aria-label={runtime.i18n.t("general:close")}
139
+ >
140
+ <XIcon />
141
+ </Link>
142
+ </header>
143
+
144
+ <div class="ridu-bulk-upload-empty">
145
+ {const accessNotice = $derived(
146
+ collection !== undefined && collection.capabilities.upload !== true
147
+ ? {
148
+ message: runtime.i18n.t("uploads:notConfigured"),
149
+ tone: "destructive" as const,
150
+ }
151
+ : controller.accessError !== undefined
152
+ ? { message: controller.accessError, tone: "destructive" as const }
153
+ : !controller.accessLoading && !controller.canCreate
154
+ ? {
155
+ message: runtime.i18n.t("uploads:createPermissionDenied"),
156
+ tone: "warning" as const,
157
+ }
158
+ : undefined
159
+ )}
160
+ {#if accessNotice}
161
+ <Banner tone={accessNotice.tone}>{accessNotice.message}</Banner>
162
+ {/if}
163
+ <BulkUploadFilePicker {controller} />
164
+ <BulkUploadRemote {controller} {contentDirection} />
165
+ </div>
166
+ {:else}
167
+ <div class="ridu-bulk-upload-workspace">
168
+ <div class="ridu-bulk-upload-workspace__sidebar">
169
+ <BulkUploadSidebar {controller} />
170
+ <BulkUploadRemote {controller} {contentDirection} />
171
+ </div>
172
+ <BulkUploadEditor {controller} {closePath} {contentDirection} />
173
+ </div>
404
174
  {/if}
405
- <Button
406
- disabled={running || pending === 0 || !uploadEnabled || !canCreate}
407
- onclick={uploadAll}
408
- >
409
- <FileUpIcon class="size-3.5" />
410
- {running
411
- ? runtime.i18n.t("uploads:uploading")
412
- : runtime.i18n.t("uploads:uploadCount", {
413
- count: pending === 0 ? runtime.i18n.t("uploads:files") : pending,
414
- })}
415
- </Button>
416
- </div>
417
- </header>
175
+ </DialogPrimitive.Content>
176
+ </DialogPrimitive.Portal>
177
+ </DialogPrimitive.Root>
418
178
 
419
- {const accessNotice = $derived(
420
- collection !== undefined && collection.capabilities.upload !== true
421
- ? {
422
- message: runtime.i18n.t("uploads:notConfigured"),
423
- tone: "destructive" as const,
424
- }
425
- : accessError !== undefined
426
- ? { message: accessError, tone: "destructive" as const }
427
- : !accessLoading && !canCreate
428
- ? {
429
- message: runtime.i18n.t("uploads:createPermissionDenied"),
430
- tone: "warning" as const,
431
- }
432
- : undefined
433
- )}
179
+ <BulkUploadEditAll {controller} {contentDirection} />
434
180
 
435
- {#if accessNotice !== undefined}
436
- <Banner class="mt-7" tone={accessNotice.tone}>{accessNotice.message}</Banner>
437
- {/if}
438
-
439
- <label
440
- class="mt-7 grid min-h-36 cursor-pointer place-items-center rounded-[4px] border border-dashed border-control-border-hover bg-control px-5 py-7 text-center transition-colors hover:border-primary/55 hover:bg-primary/[0.035]"
441
- >
442
- <input
443
- class="sr-only"
444
- type="file"
445
- multiple
446
- accept={acceptedTypes}
447
- onchange={addFiles}
448
- disabled={!uploadEnabled || !canCreate || running}
449
- />
450
- <span>
451
- <FileUpIcon class="mx-auto size-5 text-foreground-faint" />
452
- <span class="mt-3 block text-[13.5px] font-medium text-foreground-muted">
453
- {runtime.i18n.t("uploads:chooseFilesForQueue")}
454
- </span>
455
- <span class="mt-1 block text-[11.5px] text-foreground-faint">
456
- {acceptedTypeLabels || runtime.i18n.t("uploads:configuredTypes")}
457
- </span>
458
- </span>
459
- </label>
460
-
461
- <form
462
- class="mt-4 grid gap-4 rounded-[4px] border border-control-border bg-control p-5"
463
- data-remote-upload
464
- onsubmit={uploadRemote}
465
- >
466
- <div class="flex items-start gap-3">
467
- <div
468
- class="grid size-9 shrink-0 place-items-center rounded-[3px] border border-control-border bg-background"
469
- >
470
- <LinkIcon class="size-4 text-foreground-faint" aria-hidden="true" />
471
- </div>
472
- <div>
473
- <h2 class="text-[13.5px] font-medium text-foreground">
474
- {runtime.i18n.t("uploads:pasteURL")}
475
- </h2>
476
- <p class="mt-0.5 text-[11.5px] leading-4 text-foreground-faint">
477
- {runtime.i18n.t("uploads:remoteImportDescription")}
478
- </p>
479
- </div>
480
- </div>
481
- <label class="grid gap-1.5 text-[11px] text-foreground-sub" for="remote-upload-url">
482
- <span>{runtime.i18n.t("uploads:assetURL")}</span>
483
- <Input
484
- id="remote-upload-url"
485
- type="url"
486
- placeholder="https://cdn.example.com/image.jpg"
487
- value={remoteURL}
488
- oninput={(event) => updateRemoteURL(event.currentTarget.value)}
489
- disabled={!uploadEnabled || !canCreate || remotePending}
490
- required
491
- />
492
- </label>
493
- {#if metadataFields.length > 0}
494
- <UploadMetadataFields
495
- fields={metadataFields}
496
- values={remoteValues}
497
- issues={[...remoteIssues]}
498
- idPrefix="remote-upload"
499
- disabled={!uploadEnabled || !canCreate || remotePending}
500
- labelPrefix={runtime.i18n.t("uploads:urlImportMetadata")}
501
- onValueChange={updateRemoteMetadata}
502
- />
503
- {/if}
504
- {#if remoteError !== undefined}<Banner tone="destructive">{remoteError}</Banner>{/if}
505
- <div class="flex flex-wrap items-center justify-between gap-3">
506
- {#if remoteDocumentID !== undefined}
507
- <Link
508
- class={buttonVariants({ variant: "ghost", size: "sm" })}
509
- to={localizedPath(documentPath(slug, remoteDocumentID))}
510
- >
511
- {runtime.i18n.t("uploads:openImportedAsset")}
512
- </Link>
513
- {:else}<span></span>{/if}
181
+ <AdminDialog
182
+ open={navigationBlocker.state === "blocked"}
183
+ onOpenChange={(open) => {
184
+ if (!open) cancelNavigation();
185
+ }}
186
+ >
187
+ <DialogContent variant="confirmation">
188
+ <DialogHeader>
189
+ <DialogTitle>{runtime.i18n.t("uploads:discardQueueTitle")}</DialogTitle>
190
+ <DialogDescription>{runtime.i18n.t("uploads:discardQueueDescription")}</DialogDescription>
191
+ </DialogHeader>
192
+ <DialogFooter>
193
+ <Button variant="outline" onclick={cancelNavigation}>
194
+ {runtime.i18n.t("uploads:keepUploading")}
195
+ </Button>
514
196
  <Button
515
- type="submit"
516
- disabled={!uploadEnabled ||
517
- !canCreate ||
518
- remotePending ||
519
- remoteOutcomeUncertain ||
520
- remoteURL.trim() === ""}
197
+ disabled={controller.running || controller.remotePending}
198
+ onclick={discardAndNavigate}
521
199
  >
522
- <LinkIcon class="size-3.5" aria-hidden="true" />
523
- {remotePending
524
- ? runtime.i18n.t("uploads:importing")
525
- : runtime.i18n.t("uploads:importFromURL")}
200
+ {runtime.i18n.t("uploads:discardQueue")}
526
201
  </Button>
527
- </div>
528
- </form>
529
-
530
- {#if queue.length === 0}
531
- <div
532
- class="mt-7 rounded-[4px] border border-control-border px-5 py-8 text-center text-[12.5px] text-foreground-faint"
533
- >
534
- {runtime.i18n.t("uploads:noFilesQueued")}
535
- </div>
536
- {:else}
537
- <ul
538
- class="mt-7 grid gap-3"
539
- aria-label={runtime.i18n.t("uploads:uploadQueue")}
540
- aria-busy={running}
541
- >
542
- {#each queue as item (item.id)}
543
- <li
544
- class="rounded-[4px] border border-control-border bg-control p-4"
545
- data-upload-row
546
- tabindex="-1"
547
- >
548
- <div class="flex items-start gap-4">
549
- <div
550
- class="grid size-10 shrink-0 place-items-center rounded-[3px] border border-control-border bg-background"
551
- >
552
- {#if item.status === "complete"}<CheckIcon
553
- class="size-4 text-success"
554
- />{:else}<FileUpIcon class="size-4 text-foreground-faint" />{/if}
555
- </div>
556
- <div class="min-w-0 flex-1">
557
- <div class="flex flex-wrap items-start justify-between gap-2">
558
- <div class="min-w-0">
559
- <p class="truncate text-[13px] font-medium text-foreground">{item.file.name}</p>
560
- <p class="font-mono mt-1 text-[9.5px] text-foreground-faint">
561
- {formatBytes(item.file.size)} · {statusLabel(item.status)}
562
- </p>
563
- </div>
564
- <div class="flex items-center gap-1">
565
- {#if item.status === "complete" && item.documentID !== undefined}
566
- <Link
567
- class={buttonVariants({ variant: "ghost", size: "xs" })}
568
- to={localizedPath(documentPath(slug, item.documentID))}
569
- data-upload-result-action
570
- >
571
- {runtime.i18n.t("uploads:openAsset")}
572
- </Link>
573
- {:else if item.status === "failed"}
574
- <Button
575
- variant="ghost"
576
- size="icon-xs"
577
- onclick={(event) => retry(item, event.currentTarget)}
578
- aria-label={runtime.i18n.t("uploads:retryFilename", {
579
- filename: item.file.name,
580
- })}
581
- data-upload-result-action
582
- disabled={running}
583
- >
584
- <RotateCcwIcon />
585
- </Button>
586
- {/if}
587
- {#if item.status === "queued" || item.status === "failed"}
588
- <Button
589
- variant="ghost"
590
- size="icon-xs"
591
- onclick={() => removeItem(item.id)}
592
- aria-label={runtime.i18n.t("uploads:removeFilename", {
593
- filename: item.file.name,
594
- })}
595
- disabled={running}
596
- >
597
- <Trash2Icon />
598
- </Button>
599
- {/if}
600
- </div>
601
- </div>
602
-
603
- {#if metadataFields.length > 0}
604
- <div class="mt-4">
605
- <UploadMetadataFields
606
- fields={metadataFields}
607
- values={item.values}
608
- issues={[...item.issues]}
609
- idPrefix={item.id}
610
- disabled={item.status === "uploading" || item.status === "complete"}
611
- onValueChange={(field, value) => updateItemMetadata(item, field, value)}
612
- />
613
- </div>
614
- {/if}
615
- {#if item.error !== undefined}<p class="mt-3 text-[11.5px] text-destructive">
616
- {item.error}
617
- </p>{/if}
618
- </div>
619
- </div>
620
- </li>
621
- {/each}
622
- </ul>
623
- {/if}
624
- </section>
202
+ </DialogFooter>
203
+ </DialogContent>
204
+ </AdminDialog>