@antelopejs/dms 0.9.8 → 0.11.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 (451) hide show
  1. package/CHANGELOG.md +171 -0
  2. package/README.md +1 -1
  3. package/data/module-store/catalog.json +2 -2
  4. package/dist/attachments/table-view.js +2 -2
  5. package/dist/attachments/table-view.js.map +1 -1
  6. package/dist/automation/actions.d.ts +2 -0
  7. package/dist/automation/actions.js +116 -71
  8. package/dist/automation/actions.js.map +1 -1
  9. package/dist/automation/i18n.d.ts +4 -0
  10. package/dist/automation/i18n.js +19 -0
  11. package/dist/automation/i18n.js.map +1 -0
  12. package/dist/automation/send-notification.d.ts +21 -0
  13. package/dist/automation/send-notification.js +163 -0
  14. package/dist/automation/send-notification.js.map +1 -0
  15. package/dist/automation/triggers.js +19 -25
  16. package/dist/automation/triggers.js.map +1 -1
  17. package/dist/client-origin-cors.d.ts +1 -0
  18. package/dist/client-origin-cors.js +19 -0
  19. package/dist/client-origin-cors.js.map +1 -0
  20. package/dist/config.d.ts +3 -2
  21. package/dist/config.js +5 -7
  22. package/dist/config.js.map +1 -1
  23. package/dist/frontend-module.d.ts +4 -0
  24. package/dist/frontend-module.js +37 -0
  25. package/dist/frontend-module.js.map +1 -0
  26. package/dist/implementations/dms/client-ip.js +1 -1
  27. package/dist/implementations/dms/client-ip.js.map +1 -1
  28. package/dist/implementations/dms/dev-reload.d.ts +5 -0
  29. package/dist/implementations/dms/dev-reload.js +23 -3
  30. package/dist/implementations/dms/dev-reload.js.map +1 -1
  31. package/dist/implementations/dms/frontend-modules.d.ts +2 -4
  32. package/dist/implementations/dms/frontend-modules.js +38 -94
  33. package/dist/implementations/dms/frontend-modules.js.map +1 -1
  34. package/dist/implementations/dms/frontend-ready.d.ts +12 -2
  35. package/dist/implementations/dms/frontend-ready.js +34 -4
  36. package/dist/implementations/dms/frontend-ready.js.map +1 -1
  37. package/dist/implementations/dms/layout-banners.d.ts +2 -1
  38. package/dist/implementations/dms/layout-banners.js +9 -0
  39. package/dist/implementations/dms/layout-banners.js.map +1 -1
  40. package/dist/implementations/dms/manifest.d.ts +7 -2
  41. package/dist/implementations/dms/manifest.js +12 -7
  42. package/dist/implementations/dms/manifest.js.map +1 -1
  43. package/dist/implementations/dms/page.d.ts +4 -3
  44. package/dist/implementations/dms/page.js +49 -18
  45. package/dist/implementations/dms/page.js.map +1 -1
  46. package/dist/implementations/dms/permissions-resolver.d.ts +2 -2
  47. package/dist/implementations/dms/permissions-resolver.js +2 -2
  48. package/dist/implementations/dms/permissions-resolver.js.map +1 -1
  49. package/dist/implementations/dms/realtime.js +1 -0
  50. package/dist/implementations/dms/realtime.js.map +1 -1
  51. package/dist/implementations/dms-base/bulk-selection.d.ts +1 -1
  52. package/dist/implementations/dms-base/bulk-selection.js +23 -1
  53. package/dist/implementations/dms-base/bulk-selection.js.map +1 -1
  54. package/dist/implementations/dms-base/filter-tokens.js +5 -3
  55. package/dist/implementations/dms-base/filter-tokens.js.map +1 -1
  56. package/dist/implementations/dms-base/search-route.d.ts +1 -0
  57. package/dist/implementations/dms-base/search-route.js +35 -13
  58. package/dist/implementations/dms-base/search-route.js.map +1 -1
  59. package/dist/implementations/dms-base/table-view.js +11 -6
  60. package/dist/implementations/dms-base/table-view.js.map +1 -1
  61. package/dist/implementations/dms-notifications/index.d.ts +2 -2
  62. package/dist/implementations/dms-notifications/index.js +5 -5
  63. package/dist/implementations/dms-notifications/index.js.map +1 -1
  64. package/dist/index.js +32 -55
  65. package/dist/index.js.map +1 -1
  66. package/dist/pages/modules/index.d.ts +1 -0
  67. package/dist/pages/modules/index.js +1 -0
  68. package/dist/pages/modules/index.js.map +1 -1
  69. package/dist/pages/modules/modules-store.d.ts +9 -0
  70. package/dist/pages/modules/modules-store.js +30 -0
  71. package/dist/pages/modules/modules-store.js.map +1 -0
  72. package/dist/pages/settings/users/account-credentials.js +2 -2
  73. package/dist/pages/settings/users/account-credentials.js.map +1 -1
  74. package/dist/pages/settings/users/account-data-store.js +25 -8
  75. package/dist/pages/settings/users/account-data-store.js.map +1 -1
  76. package/dist/pages/settings/users/invite-extension-routes.js +9 -3
  77. package/dist/pages/settings/users/invite-extension-routes.js.map +1 -1
  78. package/dist/pages/settings/users/member-management.js +13 -14
  79. package/dist/pages/settings/users/member-management.js.map +1 -1
  80. package/dist/pages/settings/users/profile.d.ts +1 -4
  81. package/dist/pages/settings/users/profile.js +0 -35
  82. package/dist/pages/settings/users/profile.js.map +1 -1
  83. package/dist/pages/settings/users/role-editor-store.js +26 -18
  84. package/dist/pages/settings/users/role-editor-store.js.map +1 -1
  85. package/dist/pages/settings/users/role-editor.d.ts +0 -1
  86. package/dist/pages/settings/users/role-editor.js +1 -2
  87. package/dist/pages/settings/users/role-editor.js.map +1 -1
  88. package/dist/pages/settings/users/role-preview.js +1 -2
  89. package/dist/pages/settings/users/role-preview.js.map +1 -1
  90. package/dist/pages/settings/users/roles.js +2 -2
  91. package/dist/pages/settings/users/roles.js.map +1 -1
  92. package/dist/pages/settings/users/two-factor-operations.js.map +1 -1
  93. package/dist/realtime/broker.d.ts +6 -0
  94. package/dist/realtime/broker.js +8 -1
  95. package/dist/realtime/broker.js.map +1 -1
  96. package/dist/realtime/presence.d.ts +8 -3
  97. package/dist/realtime/presence.js +8 -4
  98. package/dist/realtime/presence.js.map +1 -1
  99. package/dist/realtime/sessions.d.ts +11 -1
  100. package/dist/realtime/sessions.js +21 -3
  101. package/dist/realtime/sessions.js.map +1 -1
  102. package/dist/realtime/table-view.d.ts +2 -2
  103. package/dist/realtime/table-view.js +4 -3
  104. package/dist/realtime/table-view.js.map +1 -1
  105. package/dist/routes/auth/client-ip.d.ts +1 -0
  106. package/dist/routes/auth/client-ip.js +18 -0
  107. package/dist/routes/auth/client-ip.js.map +1 -1
  108. package/dist/routes/auth/oauth/config.d.ts +2 -1
  109. package/dist/routes/auth/oauth/config.js +5 -1
  110. package/dist/routes/auth/oauth/config.js.map +1 -1
  111. package/dist/routes/auth/session-handoff.d.ts +0 -1
  112. package/dist/routes/auth/session-handoff.js +0 -2
  113. package/dist/routes/auth/session-handoff.js.map +1 -1
  114. package/dist/routes/auth.js +3 -2
  115. package/dist/routes/auth.js.map +1 -1
  116. package/dist/routes/index.d.ts +0 -1
  117. package/dist/routes/index.js +0 -1
  118. package/dist/routes/index.js.map +1 -1
  119. package/dist/routes/realtime.d.ts +13 -1
  120. package/dist/routes/realtime.js +114 -29
  121. package/dist/routes/realtime.js.map +1 -1
  122. package/dist/startup-config.d.ts +2 -0
  123. package/dist/startup-config.js +62 -0
  124. package/dist/startup-config.js.map +1 -0
  125. package/dist/store/interfaces.d.ts +3 -2
  126. package/dist/store/interfaces.js +16 -6
  127. package/dist/store/interfaces.js.map +1 -1
  128. package/dist/store/listing.js +25 -15
  129. package/dist/store/listing.js.map +1 -1
  130. package/dist/store/operations.js +5 -2
  131. package/dist/store/operations.js.map +1 -1
  132. package/dist/store/requirements.js +3 -2
  133. package/dist/store/requirements.js.map +1 -1
  134. package/dist/store/runtime.d.ts +10 -1
  135. package/dist/store/runtime.js +33 -6
  136. package/dist/store/runtime.js.map +1 -1
  137. package/dist/store/types.d.ts +10 -0
  138. package/dist/utils/ensure-default-tenant.d.ts +2 -1
  139. package/dist/utils/ensure-default-tenant.js +13 -1
  140. package/dist/utils/ensure-default-tenant.js.map +1 -1
  141. package/dist/utils/export-jobs.js +2 -1
  142. package/dist/utils/export-jobs.js.map +1 -1
  143. package/dist/validation/auth.schema.d.ts +2 -2
  144. package/dist/validation/member-invite.schema.d.ts +2 -10
  145. package/dist/validation/member-invite.schema.js +2 -15
  146. package/dist/validation/member-invite.schema.js.map +1 -1
  147. package/frontend-vue/dms.frontend.ts +24 -5
  148. package/frontend-vue/dms.server.ts +17 -0
  149. package/frontend-vue/layers/dms-auth/server/accounts.ts +3 -4
  150. package/frontend-vue/layers/dms-core/app/build/composables/renderNow.ts +33 -0
  151. package/frontend-vue/layers/dms-core/app/build/composables/user/tablePreferences.ts +5 -1
  152. package/frontend-vue/layers/dms-core/app/build/utils/crossTab.ts +199 -0
  153. package/frontend-vue/layers/dms-core/app/build/utils/sharedRealtimeConnection.ts +410 -0
  154. package/frontend-vue/layers/dms-core/app/composables/auth/useApiData.ts +77 -12
  155. package/frontend-vue/layers/dms-core/app/composables/data-types/useDataType.ts +26 -0
  156. package/frontend-vue/layers/dms-core/app/composables/live/livePauseSource.ts +29 -0
  157. package/frontend-vue/layers/dms-core/app/composables/period/compareResolvers.ts +150 -9
  158. package/frontend-vue/layers/dms-core/app/composables/period/usePeriod.ts +6 -4
  159. package/frontend-vue/layers/dms-core/app/composables/period/usePeriodScope.ts +27 -1
  160. package/frontend-vue/layers/dms-core/app/composables/realtime/useUserRealtime.ts +256 -119
  161. package/frontend-vue/layers/dms-core/app/composables/translation/useComposedText.ts +21 -8
  162. package/frontend-vue/layers/dms-core/app/composables/translation/useTranslation.ts +25 -5
  163. package/frontend-vue/layers/dms-core/app/composables/useApiError.ts +3 -5
  164. package/frontend-vue/layers/dms-core/app/composables/useFieldErrors.ts +61 -26
  165. package/frontend-vue/layers/dms-core/app/plugins/dev-error-log.ts +16 -0
  166. package/frontend-vue/layers/dms-core/app/types/user.ts +6 -0
  167. package/frontend-vue/layers/dms-core/app/utils/composedText.ts +7 -2
  168. package/frontend-vue/layers/dms-core/app/utils/formatter.ts +134 -35
  169. package/frontend-vue/layers/dms-core/app/utils/url-interpolation.ts +18 -1
  170. package/frontend-vue/layers/dms-core/i18n/locales/core-en-GB.json +136 -2
  171. package/frontend-vue/layers/dms-core/i18n/locales/core-fr-FR.json +136 -2
  172. package/frontend-vue/layers/dms-core/shared/types/app-config.ts +0 -2
  173. package/frontend-vue/layers/dms-layout/app/app.config.ts +0 -1
  174. package/frontend-vue/layers/dms-layout/app/assets/css/main.css +11 -1
  175. package/frontend-vue/layers/dms-layout/app/assets/fonts/Geist-Italic-Variable.woff2 +0 -0
  176. package/frontend-vue/layers/dms-layout/app/assets/fonts/Geist-Variable.woff2 +0 -0
  177. package/frontend-vue/layers/dms-layout/app/assets/fonts/GeistMono-Italic-Variable.woff2 +0 -0
  178. package/frontend-vue/layers/dms-layout/app/assets/fonts/GeistMono-Variable.woff2 +0 -0
  179. package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardBanners.vue +3 -2
  180. package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardHeader.vue +41 -21
  181. package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardPageBody.vue +6 -2
  182. package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardSidebar.vue +4 -19
  183. package/frontend-vue/layers/dms-layout/app/build/components/layout/Footer.vue +11 -107
  184. package/frontend-vue/layers/dms-layout/app/build/components/layout/GithubStarPrompt.vue +18 -7
  185. package/frontend-vue/layers/dms-layout/app/build/components/layout/PageHeaderActionsOutlet.vue +4 -0
  186. package/frontend-vue/layers/dms-layout/app/build/components/layout/RowSidePanel.vue +43 -82
  187. package/frontend-vue/layers/dms-layout/app/build/components/layout/SidebarUserMenu.vue +44 -3
  188. package/frontend-vue/layers/dms-layout/app/build/components/layout/UserIdentity.vue +11 -3
  189. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleCatalogTile.vue +132 -35
  190. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleManageMenu.vue +59 -0
  191. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleSourceLabel.vue +25 -0
  192. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleStoreBanners.vue +73 -0
  193. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleStoreOperation.vue +116 -0
  194. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleStoreRequirements.vue +8 -3
  195. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleStoreTile.vue +85 -207
  196. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleTile.vue +10 -3
  197. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleTileSkeleton.vue +68 -0
  198. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/notification/NotificationInboxDisplay.vue +24 -8
  199. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/notification/NotificationInboxItem.vue +8 -5
  200. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/region/RegionTimeZoneInput.vue +1 -0
  201. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/roles/role-permissions.ts +19 -0
  202. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/roles/useRoleEditor.ts +2 -1
  203. package/frontend-vue/layers/dms-layout/app/build/composables/layout/useSidePanelWidth.ts +31 -70
  204. package/frontend-vue/layers/dms-layout/app/build/composables/notification/useNotificationGroupsOpen.ts +11 -1
  205. package/frontend-vue/layers/dms-layout/app/build/utils/dev-reload-stream.ts +81 -0
  206. package/frontend-vue/layers/dms-layout/app/build/utils/github-repository.ts +2 -2
  207. package/frontend-vue/layers/dms-layout/app/build/utils/module-store-catalog.ts +232 -36
  208. package/frontend-vue/layers/dms-layout/app/build/utils/modules-catalog.ts +122 -14
  209. package/frontend-vue/layers/dms-layout/app/composables/layout/useLayoutBanners.ts +32 -3
  210. package/frontend-vue/layers/dms-layout/app/composables/layout/useServedHeaderActions.ts +49 -11
  211. package/frontend-vue/layers/dms-layout/app/composables/page/useModuleHistory.ts +3 -2
  212. package/frontend-vue/layers/dms-layout/app/composables/page/useModuleStore.ts +59 -4
  213. package/frontend-vue/layers/dms-layout/app/composables/page/useModuleStoreActions.ts +194 -0
  214. package/frontend-vue/layers/dms-layout/app/composables/page/usePageComponentPreload.ts +23 -3
  215. package/frontend-vue/layers/dms-layout/app/composables/page/usePageLayout.ts +4 -2
  216. package/frontend-vue/layers/dms-layout/app/composables/page/usePageRecordDeletion.ts +162 -0
  217. package/frontend-vue/layers/dms-layout/app/composables/page/useSiteLayout.ts +6 -1
  218. package/frontend-vue/layers/dms-layout/app/custom-layouts/DefaultLayout.vue +6 -2
  219. package/frontend-vue/layers/dms-layout/app/custom-layouts/SettingsLayout.vue +6 -2
  220. package/frontend-vue/layers/dms-layout/app/custom-pages/modules-store.vue +291 -0
  221. package/frontend-vue/layers/dms-layout/app/custom-pages/modules.vue +87 -474
  222. package/frontend-vue/layers/dms-layout/app/middleware/module-routing.global.ts +3 -0
  223. package/frontend-vue/layers/dms-layout/app/pages/[...slug].vue +13 -22
  224. package/frontend-vue/layers/dms-layout/app/plugins/command-palette-navigation.client.ts +11 -10
  225. package/frontend-vue/layers/dms-layout/app/plugins/command-palette-personal-pages.client.ts +22 -52
  226. package/frontend-vue/layers/dms-layout/app/plugins/command-palette-quick-actions.client.ts +7 -7
  227. package/frontend-vue/layers/dms-layout/app/plugins/command-palette-session.client.ts +11 -10
  228. package/frontend-vue/layers/dms-layout/app/plugins/dms-dev-reload.client.ts +14 -9
  229. package/frontend-vue/layers/dms-layout/app/theme/actions.ts +5 -4
  230. package/frontend-vue/layers/dms-layout/app/theme/form-controls.ts +9 -4
  231. package/frontend-vue/layers/dms-layout/app/theme/surfaces.ts +22 -6
  232. package/frontend-vue/layers/dms-layout/app/types/module-store.ts +9 -0
  233. package/frontend-vue/layers/dms-layout/app/types/page.ts +15 -1
  234. package/frontend-vue/layers/dms-layout/app/utils/account-menu.ts +11 -0
  235. package/frontend-vue/layers/dms-layout/app/utils/breadcrumb.ts +57 -0
  236. package/frontend-vue/layers/dms-layout/app/utils/pageQuery.ts +26 -0
  237. package/frontend-vue/layers/dms-layout/i18n/locales/layout-en-GB.json +45 -18
  238. package/frontend-vue/layers/dms-layout/i18n/locales/layout-fr-FR.json +44 -17
  239. package/frontend-vue/layers/dms-ui/app/build/components/containers/TargetComponentTree.vue +31 -0
  240. package/frontend-vue/layers/dms-ui/app/build/components/containers/constants.ts +25 -0
  241. package/frontend-vue/layers/dms-ui/app/build/components/containers/drawer/DynamicDrawer.vue +244 -15
  242. package/frontend-vue/layers/dms-ui/app/build/components/containers/modal/DynamicModal.vue +42 -25
  243. package/frontend-vue/layers/dms-ui/app/build/components/containers/row/RowHeader.vue +153 -0
  244. package/frontend-vue/layers/dms-ui/app/build/components/containers/row/RowNotFound.vue +44 -0
  245. package/frontend-vue/layers/dms-ui/app/build/components/containers/row/drawerRowHeader.ts +35 -0
  246. package/frontend-vue/layers/dms-ui/app/build/components/form/FormEntries.vue +44 -9
  247. package/frontend-vue/layers/dms-ui/app/build/components/form/FormErrorSummary.vue +11 -4
  248. package/frontend-vue/layers/dms-ui/app/build/components/form/FormFieldControl.vue +38 -5
  249. package/frontend-vue/layers/dms-ui/app/build/components/form/FormFieldNotes.vue +29 -5
  250. package/frontend-vue/layers/dms-ui/app/build/components/form/FormLoadError.vue +5 -1
  251. package/frontend-vue/layers/dms-ui/app/build/components/form/FormSectionNav.vue +3 -1
  252. package/frontend-vue/layers/dms-ui/app/build/components/form/FormSections.vue +106 -27
  253. package/frontend-vue/layers/dms-ui/app/build/components/table/Actions.vue +12 -4
  254. package/frontend-vue/layers/dms-ui/app/build/components/table/Empty.vue +4 -2
  255. package/frontend-vue/layers/dms-ui/app/build/components/table/FiltersRow.vue +48 -1
  256. package/frontend-vue/layers/dms-ui/app/build/components/table/Pagination.vue +55 -54
  257. package/frontend-vue/layers/dms-ui/app/build/components/table/RowSelection.vue +2 -1
  258. package/frontend-vue/layers/dms-ui/app/build/components/table/Table.vue +396 -288
  259. package/frontend-vue/layers/dms-ui/app/build/components/table/Tabs.vue +2 -1
  260. package/frontend-vue/layers/dms-ui/app/build/components/table/Views.vue +2 -1
  261. package/frontend-vue/layers/dms-ui/app/build/components/table-view/CardAvatar.vue +59 -0
  262. package/frontend-vue/layers/dms-ui/app/build/components/table-view/CardGridSkeleton.vue +19 -2
  263. package/frontend-vue/layers/dms-ui/app/build/components/table-view/CardRowActions.vue +38 -0
  264. package/frontend-vue/layers/dms-ui/app/build/components/table-view/CardsDisplay.vue +148 -104
  265. package/frontend-vue/layers/dms-ui/app/build/components/table-view/LiveHoldBanner.vue +17 -2
  266. package/frontend-vue/layers/dms-ui/app/build/composables/actions/bulkSelection.ts +43 -9
  267. package/frontend-vue/layers/dms-ui/app/build/composables/actions/rowComponentOptions.ts +76 -0
  268. package/frontend-vue/layers/dms-ui/app/build/composables/actions/rowNavigation.ts +172 -27
  269. package/frontend-vue/layers/dms-ui/app/build/composables/actions/useActionTargets.ts +270 -46
  270. package/frontend-vue/layers/dms-ui/app/build/composables/confirm/actionError.ts +6 -1
  271. package/frontend-vue/layers/dms-ui/app/build/composables/confirm/confirmDialogTexts.ts +2 -0
  272. package/frontend-vue/layers/dms-ui/app/build/composables/containers/closingLayers.ts +186 -0
  273. package/frontend-vue/layers/dms-ui/app/build/composables/containers/context.ts +14 -0
  274. package/frontend-vue/layers/dms-ui/app/build/composables/containers/drawerSize.ts +229 -0
  275. package/frontend-vue/layers/dms-ui/app/build/composables/containers/useResizableSize.ts +175 -0
  276. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/metricCells.ts +18 -35
  277. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/numberSuffix.ts +44 -0
  278. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/registerDefaults.ts +123 -27
  279. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/registerFieldTypes.ts +2 -1
  280. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/relationFilter.ts +75 -0
  281. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/relationRows.ts +92 -0
  282. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/relationValue.ts +39 -0
  283. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/twoLineCell.ts +3 -3
  284. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/useColumnValueRenderer.ts +6 -2
  285. package/frontend-vue/layers/dms-ui/app/build/composables/form/createFormFields.ts +15 -0
  286. package/frontend-vue/layers/dms-ui/app/build/composables/form/fieldSaved.ts +40 -0
  287. package/frontend-vue/layers/dms-ui/app/build/composables/form/fieldValueRule.ts +62 -0
  288. package/frontend-vue/layers/dms-ui/app/build/composables/form/formEntryContext.ts +5 -0
  289. package/frontend-vue/layers/dms-ui/app/build/composables/form/formFooter.ts +42 -7
  290. package/frontend-vue/layers/dms-ui/app/build/composables/form/formLayout.ts +106 -4
  291. package/frontend-vue/layers/dms-ui/app/build/composables/form/formRecordLoad.ts +11 -5
  292. package/frontend-vue/layers/dms-ui/app/build/composables/form/formSections.ts +32 -0
  293. package/frontend-vue/layers/dms-ui/app/build/composables/form/formValidation.ts +36 -4
  294. package/frontend-vue/layers/dms-ui/app/build/composables/form/readonlyValue.ts +19 -0
  295. package/frontend-vue/layers/dms-ui/app/build/composables/form/registerFormFunctions.ts +27 -0
  296. package/frontend-vue/layers/dms-ui/app/build/composables/form/repeaterTable.ts +78 -0
  297. package/frontend-vue/layers/dms-ui/app/build/composables/form/serverFieldErrors.ts +49 -0
  298. package/frontend-vue/layers/dms-ui/app/build/composables/form/toasterClearance.ts +118 -0
  299. package/frontend-vue/layers/dms-ui/app/build/composables/form/useInstantForm.ts +12 -1
  300. package/frontend-vue/layers/dms-ui/app/build/composables/live/currentPageLive.ts +41 -3
  301. package/frontend-vue/layers/dms-ui/app/build/composables/live/livePoller.ts +10 -2
  302. package/frontend-vue/layers/dms-ui/app/build/composables/live/pageReadClock.ts +28 -0
  303. package/frontend-vue/layers/dms-ui/app/build/composables/live/shortcuts.ts +1 -1
  304. package/frontend-vue/layers/dms-ui/app/build/composables/live/usePageLiveHost.ts +17 -3
  305. package/frontend-vue/layers/dms-ui/app/build/composables/live/useUpdatedLabel.ts +38 -0
  306. package/frontend-vue/layers/dms-ui/app/build/composables/page-toolbar/pageToolbarContext.ts +75 -0
  307. package/frontend-vue/layers/dms-ui/app/build/composables/page-toolbar/toolbarControlHeight.ts +29 -0
  308. package/frontend-vue/layers/dms-ui/app/build/composables/period/periodComparison.ts +52 -0
  309. package/frontend-vue/layers/dms-ui/app/build/composables/period/periodDisplay.ts +21 -0
  310. package/frontend-vue/layers/dms-ui/app/build/composables/period/periodShortLabels.ts +28 -0
  311. package/frontend-vue/layers/dms-ui/app/build/composables/period/usePeriodDraft.ts +1 -0
  312. package/frontend-vue/layers/dms-ui/app/build/composables/table/useRelationFilterLabels.ts +97 -0
  313. package/frontend-vue/layers/dms-ui/app/build/composables/table/useRowScrollAnchor.ts +106 -0
  314. package/frontend-vue/layers/dms-ui/app/build/composables/table/useTable.ts +9 -0
  315. package/frontend-vue/layers/dms-ui/app/build/composables/table/useTableColumns.ts +1 -2
  316. package/frontend-vue/layers/dms-ui/app/build/composables/table/utils/archivedCount.ts +31 -0
  317. package/frontend-vue/layers/dms-ui/app/build/composables/table/utils/filterEditorValue.ts +38 -0
  318. package/frontend-vue/layers/dms-ui/app/build/composables/table/utils/formatFilterValue.ts +33 -2
  319. package/frontend-vue/layers/dms-ui/app/build/composables/table/utils/scrollEdges.ts +31 -0
  320. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useCardFields.ts +33 -0
  321. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useMobileDisplay.ts +48 -0
  322. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useScrolledPastTop.ts +41 -18
  323. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableFooter.ts +3 -2
  324. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableLinkedVisit.ts +78 -0
  325. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableLiveHold.ts +135 -22
  326. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableRows.ts +56 -25
  327. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViewRowActions.ts +90 -35
  328. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViews.ts +86 -10
  329. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/bulkActions.ts +14 -4
  330. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/card.ts +127 -2
  331. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/compareValues.ts +13 -0
  332. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/expandedRows.ts +28 -0
  333. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/groupedRows.ts +114 -1
  334. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/linkedArrival.ts +308 -0
  335. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/locateRow.ts +106 -0
  336. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/pageArrival.ts +34 -0
  337. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/pinnedRows.ts +17 -0
  338. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/quickFilters.ts +41 -33
  339. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/recordLink.ts +24 -8
  340. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/tableCounts.ts +113 -0
  341. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/tableQuery.ts +27 -4
  342. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/views.ts +22 -6
  343. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/writeError.ts +21 -0
  344. package/frontend-vue/layers/dms-ui/app/build/utils/addressSuggestions.ts +49 -0
  345. package/frontend-vue/layers/dms-ui/app/build/utils/codeHighlight.ts +67 -29
  346. package/frontend-vue/layers/dms-ui/app/build/utils/eyebrow.ts +12 -0
  347. package/frontend-vue/layers/dms-ui/app/build/utils/fieldOptionsI18n.ts +51 -11
  348. package/frontend-vue/layers/dms-ui/app/build/utils/htmlGrammar.ts +79 -0
  349. package/frontend-vue/layers/dms-ui/app/build/utils/inputWidth.ts +53 -0
  350. package/frontend-vue/layers/dms-ui/app/build/utils/numberAdjustment.ts +6 -3
  351. package/frontend-vue/layers/dms-ui/app/build/utils/relativeDate.ts +19 -5
  352. package/frontend-vue/layers/dms-ui/app/build/utils/scrollWithin.ts +99 -0
  353. package/frontend-vue/layers/dms-ui/app/build/utils/shellParser.ts +1 -1
  354. package/frontend-vue/layers/dms-ui/app/build/utils/streamHighlight.ts +90 -0
  355. package/frontend-vue/layers/dms-ui/app/build/utils/urlVariables.ts +36 -14
  356. package/frontend-vue/layers/dms-ui/app/components/PasswordStrength.vue +1 -1
  357. package/frontend-vue/layers/dms-ui/app/components/RecursiveComponent.vue +9 -1
  358. package/frontend-vue/layers/dms-ui/app/components/blocks/LiveToggleBlock.vue +80 -32
  359. package/frontend-vue/layers/dms-ui/app/components/blocks/PageToolbarBlock.vue +137 -0
  360. package/frontend-vue/layers/dms-ui/app/components/blocks/StatGroupBlock.vue +5 -0
  361. package/frontend-vue/layers/dms-ui/app/components/chart/ApexChartHost.vue +16 -2
  362. package/frontend-vue/layers/dms-ui/app/components/chart/Chart.vue +24 -5
  363. package/frontend-vue/layers/dms-ui/app/components/chart/ChartCard.vue +41 -23
  364. package/frontend-vue/layers/dms-ui/app/components/code-snippet/CodeSnippet.vue +23 -12
  365. package/frontend-vue/layers/dms-ui/app/components/confirm/ConfirmModal.vue +1 -1
  366. package/frontend-vue/layers/dms-ui/app/components/container-footer/ContainerFooter.vue +34 -0
  367. package/frontend-vue/layers/dms-ui/app/components/field-row/FieldRow.vue +25 -2
  368. package/frontend-vue/layers/dms-ui/app/components/flow-canvas/FlowCanvas.vue +2 -2
  369. package/frontend-vue/layers/dms-ui/app/components/form/Form.vue +119 -36
  370. package/frontend-vue/layers/dms-ui/app/components/form/components/Cascader.vue +53 -22
  371. package/frontend-vue/layers/dms-ui/app/components/form/components/DatePicker.vue +20 -1
  372. package/frontend-vue/layers/dms-ui/app/components/form/components/Display.vue +13 -1
  373. package/frontend-vue/layers/dms-ui/app/components/form/components/Image.vue +8 -3
  374. package/frontend-vue/layers/dms-ui/app/components/form/components/InputAddress.vue +53 -5
  375. package/frontend-vue/layers/dms-ui/app/components/form/components/InputCode.vue +29 -5
  376. package/frontend-vue/layers/dms-ui/app/components/form/components/InputNumber.vue +45 -8
  377. package/frontend-vue/layers/dms-ui/app/components/form/components/InputPercentage.vue +28 -5
  378. package/frontend-vue/layers/dms-ui/app/components/form/components/InputSecret.vue +5 -0
  379. package/frontend-vue/layers/dms-ui/app/components/form/components/InputText.vue +19 -2
  380. package/frontend-vue/layers/dms-ui/app/components/form/components/InputTime.vue +2 -1
  381. package/frontend-vue/layers/dms-ui/app/components/form/components/Relation.vue +226 -65
  382. package/frontend-vue/layers/dms-ui/app/components/form/components/Repeater.vue +80 -30
  383. package/frontend-vue/layers/dms-ui/app/components/form/components/RepeaterTable.vue +210 -0
  384. package/frontend-vue/layers/dms-ui/app/components/form/components/Select.vue +4 -2
  385. package/frontend-vue/layers/dms-ui/app/components/grid/GridRow.vue +11 -2
  386. package/frontend-vue/layers/dms-ui/app/components/key-value-list/KeyValueList.vue +79 -70
  387. package/frontend-vue/layers/dms-ui/app/components/kpi/KpiCard.vue +117 -21
  388. package/frontend-vue/layers/dms-ui/app/components/meter/Meter.vue +2 -1
  389. package/frontend-vue/layers/dms-ui/app/components/period/PeriodSelector.vue +113 -34
  390. package/frontend-vue/layers/dms-ui/app/components/save-bar/SaveBar.vue +14 -2
  391. package/frontend-vue/layers/dms-ui/app/components/section-header/Eyebrow.vue +6 -1
  392. package/frontend-vue/layers/dms-ui/app/components/segmented/Segmented.vue +13 -0
  393. package/frontend-vue/layers/dms-ui/app/components/stat-group/StatGroup.vue +52 -3
  394. package/frontend-vue/layers/dms-ui/app/components/tab/Tab.vue +2 -1
  395. package/frontend-vue/layers/dms-ui/app/components/table-view/CascaderPath.vue +74 -25
  396. package/frontend-vue/layers/dms-ui/app/components/table-view/KanbanBoard.vue +48 -44
  397. package/frontend-vue/layers/dms-ui/app/components/table-view/KanbanDisplay.vue +4 -1
  398. package/frontend-vue/layers/dms-ui/app/components/table-view/TableView.vue +426 -97
  399. package/frontend-vue/layers/dms-ui/app/components/top-list/TopListCard.vue +33 -5
  400. package/frontend-vue/layers/dms-ui/app/composables/chart/apexCategoryLabels.ts +68 -0
  401. package/frontend-vue/layers/dms-ui/app/composables/chart/apexDatetimeBuckets.ts +306 -0
  402. package/frontend-vue/layers/dms-ui/app/composables/chart/formatValue.ts +4 -3
  403. package/frontend-vue/layers/dms-ui/app/composables/chart/heatmapColors.ts +93 -0
  404. package/frontend-vue/layers/dms-ui/app/composables/chart/localizeSeries.ts +62 -0
  405. package/frontend-vue/layers/dms-ui/app/composables/chart/useApexChart.ts +116 -8
  406. package/frontend-vue/layers/dms-ui/app/composables/chart/useApexChart.types.ts +7 -0
  407. package/frontend-vue/layers/dms-ui/app/composables/containers/types.ts +84 -1
  408. package/frontend-vue/layers/dms-ui/app/composables/containers/useDrawer.ts +69 -8
  409. package/frontend-vue/layers/dms-ui/app/composables/form/types/field-saved.ts +11 -0
  410. package/frontend-vue/layers/dms-ui/app/composables/form/types/field.ts +16 -0
  411. package/frontend-vue/layers/dms-ui/app/composables/form/types/functions.ts +1 -0
  412. package/frontend-vue/layers/dms-ui/app/composables/form/useForm.ts +189 -28
  413. package/frontend-vue/layers/dms-ui/app/composables/live/usePageLive.ts +29 -12
  414. package/frontend-vue/layers/dms-ui/app/composables/page-toolbar/usePageToolbarSize.ts +30 -0
  415. package/frontend-vue/layers/dms-ui/app/composables/table-view/kanban.ts +4 -2
  416. package/frontend-vue/layers/dms-ui/app/composables/table-view/types/action-target.ts +22 -3
  417. package/frontend-vue/layers/dms-ui/app/composables/table-view/types/config.ts +4 -0
  418. package/frontend-vue/layers/dms-ui/app/composables/table-view/types/display.ts +24 -1
  419. package/frontend-vue/layers/dms-ui/app/plugins/closing-layers.client.ts +10 -0
  420. package/frontend-vue/layers/dms-ui/app/plugins/live-pause-source.ts +15 -0
  421. package/frontend-vue/layers/dms-ui/app/plugins/register.ts +2 -0
  422. package/frontend-vue/layers/dms-ui/app/types/input-number.ts +3 -2
  423. package/frontend-vue/layers/dms-ui/app/types/page-toolbar.ts +7 -0
  424. package/frontend-vue/layers/dms-ui/app/types/row-action.ts +0 -2
  425. package/frontend-vue/layers/dms-ui/app/types/row-navigation.ts +17 -0
  426. package/frontend-vue/layers/dms-ui/app/types/table/query.ts +2 -1
  427. package/frontend-vue/layers/dms-ui/app/utils/blockRefresh.ts +3 -1
  428. package/frontend-vue/layers/dms-ui/app/utils/cascader.ts +94 -3
  429. package/frontend-vue/layers/dms-ui/app/utils/componentTree.ts +24 -0
  430. package/frontend-vue/layers/dms-ui/i18n/locales/ui-en-GB.json +20 -5
  431. package/frontend-vue/layers/dms-ui/i18n/locales/ui-fr-FR.json +20 -5
  432. package/frontend-vue/package.json +6 -2
  433. package/package.json +11 -10
  434. package/skills/dms/SKILL.md +1 -1
  435. package/skills/dms-auth/SKILL.md +5 -3
  436. package/skills/dms-best-practices/REFERENCE.md +2 -0
  437. package/skills/dms-best-practices/SKILL.md +4 -0
  438. package/skills/dms-module/SKILL.md +3 -2
  439. package/skills/dms-pages/REFERENCE.md +129 -16
  440. package/skills/dms-pages/SKILL.md +1 -1
  441. package/skills/dms-project/DEV-SERVERS.md +23 -1
  442. package/skills/dms-project/REFERENCE.md +1 -1
  443. package/skills/dms-project/SKILL.md +4 -1
  444. package/skills/dms-project/scripts/dms-dev.sh +205 -17
  445. package/dist/hooks/tenant-instances.d.ts +0 -1
  446. package/dist/hooks/tenant-instances.js +0 -13
  447. package/dist/hooks/tenant-instances.js.map +0 -1
  448. package/dist/routes/startup-gate.d.ts +0 -7
  449. package/dist/routes/startup-gate.js +0 -39
  450. package/dist/routes/startup-gate.js.map +0 -1
  451. package/frontend-vue/layers/dms-core/app/composables/period/periodRenderNow.ts +0 -42
@@ -16,11 +16,27 @@ import type { ArrayOrNested } from "@nuxt/ui/runtime/types/utils.js";
16
16
 
17
17
  import { refDebounced } from "@vueuse/core";
18
18
  import DmsForm from "../Form.vue";
19
+ import { dmsOwnDrawer } from "../../../build/composables/containers/context";
19
20
  import { FIELD_TRIGGER_ICON } from "../../../build/utils/fieldTrigger";
21
+ import { unlabeledRelationIds } from "../../../build/composables/data-types/relationValue";
22
+ import {
23
+ notFoundRelationIds,
24
+ useRelationRows,
25
+ } from "../../../build/composables/data-types/relationRows";
20
26
  import type { FormProps } from "../../../composables/form/types";
27
+ import { sameFormValue } from "../../../build/composables/unsaved-changes/formValue";
28
+ import { FORM_ENTRY_CONTEXT_KEY } from "../../../build/composables/form/formEntryContext";
29
+ import {
30
+ type RelationFilterBy,
31
+ relationFilterParams,
32
+ resolveRelationPick,
33
+ } from "../../../build/composables/data-types/relationFilter";
21
34
 
22
35
  const SEARCH_DEBOUNCE_MS = 200;
23
- const ADD_NEW_VALUE = "__dms_relation_add_new__";
36
+ // How long the menu's content gets to leave the page before the add form opens
37
+ // anyway, and the beat its focus return takes once it has.
38
+ const MENU_CLOSE_MAX_MS = 1000;
39
+ const MENU_FOCUS_SETTLE_MS = 60;
24
40
 
25
41
  /** Title and description of the drawer a relation picker's "add" opens. */
26
42
  interface RelationAddFormTexts {
@@ -38,6 +54,11 @@ interface RelationProps extends SelectMenuProps<T, "value", M> {
38
54
  avatar?: string;
39
55
  disabled?: string;
40
56
  };
57
+ /**
58
+ * Lists only the related rows whose `field` equals the value of the form
59
+ * field `from`; ignored outside a form (a filter's picker).
60
+ */
61
+ filterBy?: RelationFilterBy;
41
62
  addForm?: { componentName: string; options?: FormProps };
42
63
  addPermissionIds?: string[];
43
64
  /**
@@ -63,33 +84,24 @@ const forwardedProps = computed(() => {
63
84
  addForm: ______,
64
85
  addPermissionIds: _______,
65
86
  addFormTexts: ________,
87
+ filterBy: _________,
66
88
  ...rest
67
89
  } = props;
68
90
  return rest as SelectMenuProps<T, "value", M>;
69
91
  });
70
- const forwarded = useForwardPropsEmits(forwardedProps, emits);
92
+ const forwardedPropsEmits = useForwardPropsEmits(forwardedProps, emits);
93
+ // The picks go through `handleUpdateModelValue` alone, which deselects a value
94
+ // picked again: forwarded as well, the form got the pick it had undone.
95
+ const forwarded = computed(() => {
96
+ const { "onUpdate:modelValue": _, ...rest } = forwardedPropsEmits.value as {
97
+ "onUpdate:modelValue"?: unknown;
98
+ };
99
+ return rest as typeof forwardedPropsEmits.value;
100
+ });
71
101
 
72
102
  type ModelValue = SelectMenuEmits<T, "value", M>["update:modelValue"][0];
73
103
 
74
- function isAddNewValue(value: unknown): boolean {
75
- if (value === ADD_NEW_VALUE) return true;
76
- if (Array.isArray(value)) return value.includes(ADD_NEW_VALUE);
77
- return false;
78
- }
79
-
80
104
  function handleUpdateModelValue(value: ModelValue) {
81
- if (isAddNewValue(value)) {
82
- openAddDrawer();
83
- if (Array.isArray(value)) {
84
- emits(
85
- "update:modelValue",
86
- (value as unknown[]).filter((v) => v !== ADD_NEW_VALUE) as ModelValue,
87
- );
88
- } else {
89
- emits("update:modelValue", props.modelValue as ModelValue);
90
- }
91
- return;
92
- }
93
105
  const shouldDeselect =
94
106
  props.deselectable && !props.multiple && value === props.modelValue;
95
107
  emits(
@@ -103,7 +115,23 @@ const instanceId = useId();
103
115
  const searchTerm = ref("");
104
116
  const searchTermDebounced = refDebounced(searchTerm, SEARCH_DEBOUNCE_MS);
105
117
 
118
+ // The form this picker fills, whose `filterBy.from` field narrows it.
119
+ const formContext = inject(FORM_ENTRY_CONTEXT_KEY, null);
120
+ const filterSourceValue = computed(() =>
121
+ props.filterBy && formContext
122
+ ? formContext.state.value[props.filterBy.from]
123
+ : undefined,
124
+ );
125
+ const filterParams = computed(() =>
126
+ relationFilterParams(props.filterBy, filterSourceValue.value),
127
+ );
128
+ // A string, so a recomputed but equal filter does not search again.
129
+ const filterKey = computed(() => JSON.stringify(filterParams.value));
130
+ // The source value the record loaded with, which its loaded pick matches.
131
+ const loadedFilterSource = filterSourceValue.value;
132
+
106
133
  const { $authFetch } = useAuthFetch();
134
+ const { readRelationRows } = useRelationRows();
107
135
  const toast = useToast();
108
136
  const { t } = useI18n();
109
137
  const { processI18n } = useTranslation();
@@ -118,6 +146,9 @@ const addContainerId = `relation-add-${useId()}`;
118
146
  // unwrapped item is an unbounded type for the checker. The list is always
119
147
  // replaced, never mutated in place.
120
148
  const locallyAddedItems: Ref<SelectMenuItem[]> = shallowRef([]);
149
+ // The rows of the ids the value holds, read from the `select` route when the
150
+ // form's data answers the ids rather than the joined rows.
151
+ const valueItems: Ref<SelectMenuItem[]> = shallowRef([]);
121
152
 
122
153
  const canAdd = computed(
123
154
  () =>
@@ -197,6 +228,7 @@ async function fetchRelationItems(): Promise<RelationItems> {
197
228
  const data = await $authFetch<RelationResponse>(url, {
198
229
  params: {
199
230
  search: searchTermDebounced.value || undefined,
231
+ ...filterParams.value,
200
232
  },
201
233
  onRequestError: ({ error }: { error: Error }) => {
202
234
  toast.add({
@@ -234,7 +266,7 @@ const { data, status, execute } = await useDmsAsyncData(
234
266
  {
235
267
  lazy: true,
236
268
  immediate: false,
237
- watch: [searchTermDebounced],
269
+ watch: [searchTermDebounced, filterKey],
238
270
  // Options to pick from, not the page's data.
239
271
  refreshOnPageRefresh: false,
240
272
  },
@@ -246,6 +278,8 @@ function isMappableObject(value: unknown): value is Record<string, string> {
246
278
 
247
279
  const initialItems = computed(() => {
248
280
  if (!props.initialValue) return [];
281
+ // The loaded row matched the loaded source value, not another one.
282
+ if (!sameFormValue(filterSourceValue.value, loadedFilterSource)) return [];
249
283
 
250
284
  const values = Array.isArray(props.initialValue)
251
285
  ? props.initialValue
@@ -254,42 +288,28 @@ const initialItems = computed(() => {
254
288
  return values.filter(isMappableObject).map(mapData);
255
289
  });
256
290
 
291
+ function itemValue(item: SelectMenuItem): unknown {
292
+ return typeof item === "object" && item !== null && "value" in item
293
+ ? item.value
294
+ : item;
295
+ }
296
+
257
297
  const mergedItems = computed<SelectMenuItem[]>(() => {
258
298
  const searchResults = data.value?.results || [];
259
299
  const initial = initialItems.value || [];
260
300
  const added = locallyAddedItems.value;
261
301
 
262
302
  const merged: SelectMenuItem[] = [...initial, ...added];
263
- const existingValues = new Set(
264
- merged.map((item) =>
265
- typeof item === "object" && item !== null && "value" in item
266
- ? item.value
267
- : item,
268
- ),
269
- );
270
-
271
- searchResults.forEach((item) => {
272
- const itemValue =
273
- typeof item === "object" && item !== null && "value" in item
274
- ? item.value
275
- : item;
303
+ const existingValues = new Set(merged.map(itemValue));
276
304
 
277
- if (!existingValues.has(itemValue)) {
305
+ [...valueItems.value, ...searchResults].forEach((item) => {
306
+ const value = itemValue(item);
307
+ if (!existingValues.has(value)) {
308
+ existingValues.add(value);
278
309
  merged.push(item);
279
310
  }
280
311
  });
281
312
 
282
- if (canAdd.value) {
283
- if (merged.length > 0) {
284
- merged.push({ type: "separator" });
285
- }
286
- merged.push({
287
- label: t("dms.form.relation.add_new"),
288
- value: ADD_NEW_VALUE,
289
- icon: "i-ph-plus",
290
- });
291
- }
292
-
293
313
  return merged;
294
314
  });
295
315
 
@@ -326,34 +346,156 @@ watch(
326
346
  { immediate: true },
327
347
  );
328
348
 
349
+ // The pick as the form holds it, read alongside the source field: a record
350
+ // loading both values changes them in the same tick.
351
+ const pickValue = computed(() =>
352
+ formContext && props.id
353
+ ? formContext.state.value[props.id]
354
+ : props.modelValue,
355
+ );
356
+ let pickSource = loadedFilterSource;
357
+
358
+ // A pick made for another value of the source field is void once it changes.
359
+ watch(
360
+ [filterSourceValue, pickValue],
361
+ ([source, pick], [prevSource, prevPick]) => {
362
+ if (!props.filterBy || !formContext) return;
363
+ const outcome = resolveRelationPick({
364
+ source,
365
+ pick,
366
+ previousSource: prevSource,
367
+ previousPick: prevPick,
368
+ pickSource,
369
+ });
370
+ pickSource = outcome.pickSource;
371
+ if (!outcome.shouldClear) return;
372
+ emits("update:modelValue", (props.multiple ? [] : undefined) as ModelValue);
373
+ },
374
+ );
375
+
376
+ const isMenuOpen = ref(false);
377
+
378
+ const slots = useSlots();
379
+ // `content-bottom` is filled below, after the add entry.
380
+ const forwardedSlotNames = computed(() =>
381
+ Object.keys(slots).filter((name) => name !== "content-bottom"),
382
+ );
383
+
384
+ const labeledIds = computed(
385
+ () => new Set(mergedItems.value.map((item) => String(itemValue(item)))),
386
+ );
387
+ // Asked once each: an id the route does not answer (deleted, out of reach)
388
+ // keeps showing as is rather than being asked again.
389
+ const requestedIds = new Set<string>();
390
+
391
+ // The ids the route did not answer (deleted, out of reach), shown as "Not
392
+ // found" with the id on hover.
393
+ const notFoundIds: Ref<ReadonlySet<string>> = shallowRef(new Set());
394
+
395
+ async function loadValueItems(ids: string[]) {
396
+ for (const id of ids) requestedIds.add(id);
397
+ const source = { searchUrl: props.searchUrl, keyMapping: props.keyMapping };
398
+ const rows = await readRelationRows(source, ids);
399
+ const missing = notFoundRelationIds(source, ids, rows);
400
+ if (!rows.length && !missing.length) return;
401
+ notFoundIds.value = new Set([...notFoundIds.value, ...missing]);
402
+ valueItems.value = [
403
+ ...valueItems.value,
404
+ ...rows.map((item) => mapData(item as Record<string, string>)),
405
+ // Kept pickable, so the value can still be deselected.
406
+ ...missing.map((id) => ({
407
+ label: t("dms.form.relation.not_found"),
408
+ value: id,
409
+ })),
410
+ ];
411
+ }
412
+
413
+ const notFoundTitle = computed(() => {
414
+ const ids = [props.modelValue]
415
+ .flat()
416
+ .map(String)
417
+ .filter((id) => notFoundIds.value.has(id));
418
+ return ids.length ? ids.join(", ") : undefined;
419
+ });
420
+
421
+ // The picker shows a value through its item: an id no item carries yet is
422
+ // labelled (and given its avatar) from its row before the picker is opened.
423
+ onMounted(() => {
424
+ watch(
425
+ () =>
426
+ unlabeledRelationIds(props.modelValue, labeledIds.value).filter(
427
+ (id) => !requestedIds.has(id),
428
+ ),
429
+ (ids) => {
430
+ if (ids.length > 0) void loadValueItems(ids);
431
+ },
432
+ { immediate: true },
433
+ );
434
+ });
435
+
329
436
  function onOpen(isOpen: boolean) {
437
+ isMenuOpen.value = isOpen;
330
438
  if (isOpen && !data.value?.results?.length) {
331
439
  execute();
332
440
  }
333
441
  }
334
442
 
443
+ const addEntryRef = useTemplateRef<HTMLElement>("addEntry");
444
+
445
+ function waitForDetached(element: HTMLElement | null): Promise<void> {
446
+ const startedAt = performance.now();
447
+ return new Promise((resolve) => {
448
+ const check = () => {
449
+ if (
450
+ !element?.isConnected ||
451
+ performance.now() - startedAt > MENU_CLOSE_MAX_MS
452
+ ) {
453
+ resolve();
454
+ return;
455
+ }
456
+ requestAnimationFrame(check);
457
+ };
458
+ check();
459
+ });
460
+ }
461
+
462
+ /**
463
+ * Opens the add form once the menu has left the page: its content animates
464
+ * out, then hands the focus back to the field, and a drawer opened before
465
+ * that took the focus leaving it for a click outside and closed at once.
466
+ */
467
+ async function openAddDrawerOnceClosed(): Promise<void> {
468
+ const menuContent = addEntryRef.value;
469
+ isMenuOpen.value = false;
470
+ await waitForDetached(menuContent);
471
+ await new Promise((resolve) => setTimeout(resolve, MENU_FOCUS_SETTLE_MS));
472
+ openAddDrawer();
473
+ }
474
+
335
475
  function openAddDrawer() {
336
476
  const formOptions = addFormOptions.value;
337
477
  if (!formOptions) return;
338
- const drawer = openDrawer({
339
- title: props.addFormTexts?.displayName
340
- ? processI18n(props.addFormTexts.displayName)
341
- : t("dms.table.new_item"),
342
- description: props.addFormTexts?.description
343
- ? processI18n(props.addFormTexts.description)
344
- : t("dms.table.new_item_description"),
345
- direction: "bottom",
346
- containerId: addContainerId,
347
- component: DmsForm,
348
- componentOptions: {
349
- ...formOptions,
350
- onSuccessCallback: (response: unknown, formData: unknown) => {
351
- clearGuards(addContainerId);
352
- drawer.close();
353
- handleAddSuccess(response, formData);
478
+ const drawer = openDrawer(
479
+ dmsOwnDrawer({
480
+ title: props.addFormTexts?.displayName
481
+ ? processI18n(props.addFormTexts.displayName)
482
+ : t("dms.table.new_item"),
483
+ description: props.addFormTexts?.description
484
+ ? processI18n(props.addFormTexts.description)
485
+ : t("dms.table.new_item_description"),
486
+ direction: "bottom",
487
+ containerId: addContainerId,
488
+ component: DmsForm,
489
+ componentOptions: {
490
+ ...formOptions,
491
+ onSuccessCallback: (response: unknown, formData: unknown) => {
492
+ clearGuards(addContainerId);
493
+ drawer.close();
494
+ handleAddSuccess(response, formData);
495
+ },
354
496
  },
355
- },
356
- });
497
+ }),
498
+ );
357
499
  }
358
500
 
359
501
  function extractInsertedId(response: unknown): string | undefined {
@@ -428,20 +570,39 @@ const addFormOptions = computed(() => {
428
570
  v-model:search-term="searchTerm"
429
571
  :trailing-icon="props.trailingIcon ?? FIELD_TRIGGER_ICON"
430
572
  :model-value="props.modelValue"
573
+ :open="isMenuOpen"
431
574
  :items="displayItems as T"
432
575
  :loading="status === 'pending'"
433
576
  value-key="value"
434
577
  :portal="false"
435
578
  :ignore-filter="true"
436
579
  :ui="{ content: 'z-50' }"
580
+ :aria-label="t('dms.a11y.show_options')"
581
+ :title="notFoundTitle"
437
582
  @update:open="onOpen"
438
583
  @update:model-value="handleUpdateModelValue"
439
584
  >
440
- <template v-for="(_, name) in $slots" :key="name" #[name]="slotData">
585
+ <template v-for="name in forwardedSlotNames" :key="name" #[name]="slotData">
441
586
  <slot
442
587
  :name="name as keyof SelectMenuSlots<T, 'value', M>"
443
588
  v-bind="slotData ?? {}"
444
589
  />
445
590
  </template>
591
+ <!-- The add entry is no item: picked, it became the field's value. -->
592
+ <template v-if="canAdd || $slots['content-bottom']" #content-bottom>
593
+ <div v-if="canAdd" ref="addEntry" class="border-default border-t p-1">
594
+ <UButton
595
+ :label="t('dms.form.relation.add_new')"
596
+ icon="i-ph-plus"
597
+ color="neutral"
598
+ variant="ghost"
599
+ size="sm"
600
+ block
601
+ class="justify-start"
602
+ @click="openAddDrawerOnceClosed"
603
+ />
604
+ </div>
605
+ <slot name="content-bottom" />
606
+ </template>
446
607
  </USelectMenu>
447
608
  </template>
@@ -6,6 +6,7 @@ import {
6
6
  useFormField,
7
7
  } from "@nuxt/ui/composables/useFormField";
8
8
  import { resolveDmsComponent } from "../../../composables/resolveDmsComponent";
9
+ import DmsRepeaterTable from "./RepeaterTable.vue";
9
10
 
10
11
  /** A field of the rows: one column. */
11
12
  interface RepeaterColumn {
@@ -72,21 +73,43 @@ const FLUID_COLUMN = "minmax(0,1fr)";
72
73
  const COLUMN_WIDTHS: Record<string, string> = { boolean: "20px" };
73
74
  const HANDLE_COLUMN = "20px";
74
75
  const REMOVE_COLUMN = "28px";
76
+
77
+ /** The columns of a row: the handle, the given cell columns, the remove button. */
78
+ function rowTemplate(cellColumns: string[]): string {
79
+ return [props.sortable ? HANDLE_COLUMN : "", ...cellColumns, REMOVE_COLUMN]
80
+ .filter(Boolean)
81
+ .join(" ");
82
+ }
83
+
84
+ // Below 480px of the field's own width (a drawer, a phone), columns side by
85
+ // side would each be a few characters wide: each row stacks its cells in one
86
+ // column instead, a labelled field under another, as a card, and the column
87
+ // heads hide.
88
+ const STACKED_HIDDEN_CLASS = "@max-[30rem]:hidden";
89
+ const STACKED_ROW_CLASSES = {
90
+ row: "@max-[30rem]:grid-cols-(--dms-repeater-stacked-columns) @max-[30rem]:items-start @max-[30rem]:rounded-md @max-[30rem]:border @max-[30rem]:border-default @max-[30rem]:p-2.5",
91
+ // Side by side, each cell is a column of the row itself.
92
+ cells: "contents @max-[30rem]:grid @max-[30rem]:min-w-0 @max-[30rem]:gap-2.5",
93
+ cell: "grid min-w-0 gap-1",
94
+ label: `${EYEBROW_CLASS} text-dimmed hidden truncate @max-[30rem]:block`,
95
+ };
75
96
  const gridTemplate = computed(() => ({
76
- gridTemplateColumns: [
77
- props.sortable ? HANDLE_COLUMN : "",
78
- ...props.columns.map(
97
+ "--dms-repeater-columns": rowTemplate(
98
+ props.columns.map(
79
99
  (column) => COLUMN_WIDTHS[column.type ?? ""] ?? FLUID_COLUMN,
80
100
  ),
81
- REMOVE_COLUMN,
82
- ]
83
- .filter(Boolean)
84
- .join(" "),
101
+ ),
102
+ "--dms-repeater-stacked-columns": rowTemplate([FLUID_COLUMN]),
85
103
  }));
86
104
 
87
- function emitRows(next: RepeaterRow[]): void {
105
+ /**
106
+ * Sets the rows; `shouldValidate: false` leaves the form's validation alone: a row
107
+ * just added is empty, and is only checked once filled or on submit, never
108
+ * flagged "required" the moment it appears.
109
+ */
110
+ function emitRows(next: RepeaterRow[], shouldValidate = true): void {
88
111
  model.value = next;
89
- emitFormChange();
112
+ if (shouldValidate) emitFormChange();
90
113
  }
91
114
 
92
115
  function setCell(index: number, column: string, value: unknown): void {
@@ -99,7 +122,7 @@ function setCell(index: number, column: string, value: unknown): void {
99
122
 
100
123
  function addRow(): void {
101
124
  rowKeys.value.push(nextKey++);
102
- emitRows([...rows.value, {}]);
125
+ emitRows([...rows.value, {}], false);
103
126
  }
104
127
 
105
128
  function removeRow(index: number): void {
@@ -176,10 +199,21 @@ const hasHeaders = computed(() => props.columns.some((column) => column.label));
176
199
  </script>
177
200
 
178
201
  <template>
179
- <div :id="props.id" class="grid gap-1.5" role="group">
202
+ <!-- Read-only, the rows are values to read, not controls: a table. -->
203
+ <DmsRepeaterTable
204
+ v-if="props.disabled"
205
+ :id="props.id"
206
+ :columns="props.columns"
207
+ :rows="rows"
208
+ />
209
+ <div v-else :id="props.id" class="@container grid gap-1.5" role="group">
180
210
  <div
181
211
  v-if="hasHeaders && rows.length"
182
- :class="[EYEBROW_CLASS, 'text-dimmed grid items-end gap-2']"
212
+ :class="[
213
+ EYEBROW_CLASS,
214
+ 'text-dimmed grid grid-cols-(--dms-repeater-columns) items-end gap-2',
215
+ STACKED_HIDDEN_CLASS,
216
+ ]"
183
217
  :style="gridTemplate"
184
218
  aria-hidden="true"
185
219
  >
@@ -193,8 +227,11 @@ const hasHeaders = computed(() => props.columns.some((column) => column.label));
193
227
  <div
194
228
  v-for="(row, index) in rows"
195
229
  :key="rowKeys[index]"
196
- class="grid items-center gap-2"
197
- :class="draggedIndex === index && 'opacity-50'"
230
+ :class="[
231
+ 'grid grid-cols-(--dms-repeater-columns) items-center gap-2',
232
+ STACKED_ROW_CLASSES.row,
233
+ draggedIndex === index && 'opacity-50',
234
+ ]"
198
235
  :style="gridTemplate"
199
236
  @dragover.prevent
200
237
  @drop.prevent="onDrop(index)"
@@ -212,22 +249,35 @@ const hasHeaders = computed(() => props.columns.some((column) => column.label));
212
249
  >
213
250
  <UIcon name="i-ph-dots-six-vertical" class="size-4" />
214
251
  </button>
215
- <Component
216
- :is="cellComponent(column)"
217
- v-for="column in props.columns"
218
- :id="cellName(index, column.id)"
219
- :key="column.id"
220
- :model-value="row[column.id]"
221
- size="sm"
222
- class="w-full min-w-0"
223
- :disabled="props.disabled"
224
- :aria-label="column.label"
225
- :aria-invalid="isCellInvalid(index, column.id) || undefined"
226
- :color="isCellInvalid(index, column.id) ? 'error' : undefined"
227
- :highlight="isCellInvalid(index, column.id) || undefined"
228
- v-bind="column.component.options || {}"
229
- @update:model-value="setCell(index, column.id, $event)"
230
- />
252
+ <div :class="STACKED_ROW_CLASSES.cells">
253
+ <div
254
+ v-for="column in props.columns"
255
+ :key="column.id"
256
+ :class="STACKED_ROW_CLASSES.cell"
257
+ >
258
+ <span
259
+ v-if="column.label"
260
+ :class="STACKED_ROW_CLASSES.label"
261
+ aria-hidden="true"
262
+ >
263
+ {{ column.label }}
264
+ </span>
265
+ <Component
266
+ :is="cellComponent(column)"
267
+ :id="cellName(index, column.id)"
268
+ :model-value="row[column.id]"
269
+ size="sm"
270
+ class="w-full min-w-0"
271
+ :disabled="props.disabled"
272
+ :aria-label="column.label"
273
+ :aria-invalid="isCellInvalid(index, column.id) || undefined"
274
+ :color="isCellInvalid(index, column.id) ? 'error' : undefined"
275
+ :highlight="isCellInvalid(index, column.id) || undefined"
276
+ v-bind="column.component.options || {}"
277
+ @update:model-value="setCell(index, column.id, $event)"
278
+ />
279
+ </div>
280
+ </div>
231
281
  <UButton
232
282
  icon="i-ph-x"
233
283
  color="neutral"