@antelopejs/dms 0.9.8 → 0.10.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 (420) hide show
  1. package/CHANGELOG.md +102 -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 -0
  21. package/dist/config.js +5 -3
  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 +22 -89
  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 +1 -2
  41. package/dist/implementations/dms/manifest.js +2 -0
  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 +28 -48
  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 +24 -13
  129. package/dist/store/listing.js.map +1 -1
  130. package/dist/store/operations.js +4 -0
  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/validation/auth.schema.d.ts +2 -2
  139. package/dist/validation/member-invite.schema.d.ts +2 -10
  140. package/dist/validation/member-invite.schema.js +2 -15
  141. package/dist/validation/member-invite.schema.js.map +1 -1
  142. package/frontend-vue/dms.frontend.ts +24 -5
  143. package/frontend-vue/dms.server.ts +17 -0
  144. package/frontend-vue/layers/dms-auth/server/accounts.ts +3 -4
  145. package/frontend-vue/layers/dms-core/app/build/composables/renderNow.ts +34 -0
  146. package/frontend-vue/layers/dms-core/app/build/composables/user/tablePreferences.ts +5 -1
  147. package/frontend-vue/layers/dms-core/app/build/utils/crossTab.ts +199 -0
  148. package/frontend-vue/layers/dms-core/app/build/utils/sharedRealtimeConnection.ts +410 -0
  149. package/frontend-vue/layers/dms-core/app/composables/auth/useApiData.ts +77 -12
  150. package/frontend-vue/layers/dms-core/app/composables/data-types/useDataType.ts +26 -0
  151. package/frontend-vue/layers/dms-core/app/composables/hydration/isHydratingComponent.ts +29 -0
  152. package/frontend-vue/layers/dms-core/app/composables/live/livePauseSource.ts +29 -0
  153. package/frontend-vue/layers/dms-core/app/composables/period/compareResolvers.ts +150 -9
  154. package/frontend-vue/layers/dms-core/app/composables/period/usePeriod.ts +6 -4
  155. package/frontend-vue/layers/dms-core/app/composables/period/usePeriodScope.ts +27 -1
  156. package/frontend-vue/layers/dms-core/app/composables/realtime/useUserRealtime.ts +256 -119
  157. package/frontend-vue/layers/dms-core/app/composables/translation/useComposedText.ts +21 -8
  158. package/frontend-vue/layers/dms-core/app/composables/translation/useTranslation.ts +22 -5
  159. package/frontend-vue/layers/dms-core/app/plugins/dev-error-log.ts +16 -0
  160. package/frontend-vue/layers/dms-core/app/types/user.ts +6 -0
  161. package/frontend-vue/layers/dms-core/app/utils/composedText.ts +7 -2
  162. package/frontend-vue/layers/dms-core/app/utils/formatter.ts +166 -35
  163. package/frontend-vue/layers/dms-core/app/utils/url-interpolation.ts +18 -1
  164. package/frontend-vue/layers/dms-core/i18n/locales/core-en-GB.json +134 -2
  165. package/frontend-vue/layers/dms-core/i18n/locales/core-fr-FR.json +134 -2
  166. package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardBanners.vue +3 -2
  167. package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardHeader.vue +41 -21
  168. package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardPageBody.vue +9 -0
  169. package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardSidebar.vue +4 -19
  170. package/frontend-vue/layers/dms-layout/app/build/components/layout/Footer.vue +11 -3
  171. package/frontend-vue/layers/dms-layout/app/build/components/layout/GithubStarPrompt.vue +18 -7
  172. package/frontend-vue/layers/dms-layout/app/build/components/layout/PageHeaderActionBar.vue +8 -0
  173. package/frontend-vue/layers/dms-layout/app/build/components/layout/PageHeaderActionsOutlet.vue +14 -0
  174. package/frontend-vue/layers/dms-layout/app/build/components/layout/RowSidePanel.vue +43 -82
  175. package/frontend-vue/layers/dms-layout/app/build/components/layout/SidebarUserMenu.vue +44 -3
  176. package/frontend-vue/layers/dms-layout/app/build/components/layout/UserIdentity.vue +11 -3
  177. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleCatalogTile.vue +132 -35
  178. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleManageMenu.vue +59 -0
  179. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleSourceLabel.vue +25 -0
  180. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleStoreBanners.vue +73 -0
  181. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleStoreOperation.vue +116 -0
  182. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleStoreRequirements.vue +8 -3
  183. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleStoreTile.vue +85 -207
  184. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleTile.vue +10 -3
  185. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleTileSkeleton.vue +68 -0
  186. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/notification/NotificationInboxDisplay.vue +24 -8
  187. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/notification/NotificationInboxItem.vue +8 -5
  188. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/region/RegionTimeZoneInput.vue +1 -0
  189. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/roles/role-permissions.ts +19 -0
  190. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/roles/useRoleEditor.ts +2 -1
  191. package/frontend-vue/layers/dms-layout/app/build/composables/layout/useSidePanelWidth.ts +31 -70
  192. package/frontend-vue/layers/dms-layout/app/build/composables/notification/useNotificationGroupsOpen.ts +11 -1
  193. package/frontend-vue/layers/dms-layout/app/build/utils/dev-reload-stream.ts +81 -0
  194. package/frontend-vue/layers/dms-layout/app/build/utils/module-store-catalog.ts +232 -36
  195. package/frontend-vue/layers/dms-layout/app/build/utils/modules-catalog.ts +122 -14
  196. package/frontend-vue/layers/dms-layout/app/composables/layout/useLayoutBanners.ts +32 -3
  197. package/frontend-vue/layers/dms-layout/app/composables/layout/usePageRecord.ts +50 -0
  198. package/frontend-vue/layers/dms-layout/app/composables/layout/useServedHeaderActions.ts +25 -7
  199. package/frontend-vue/layers/dms-layout/app/composables/page/useModuleHistory.ts +3 -2
  200. package/frontend-vue/layers/dms-layout/app/composables/page/useModuleStore.ts +51 -4
  201. package/frontend-vue/layers/dms-layout/app/composables/page/useModuleStoreActions.ts +194 -0
  202. package/frontend-vue/layers/dms-layout/app/composables/page/usePageComponentPreload.ts +23 -3
  203. package/frontend-vue/layers/dms-layout/app/composables/page/usePageLayout.ts +4 -2
  204. package/frontend-vue/layers/dms-layout/app/composables/page/useSiteLayout.ts +6 -1
  205. package/frontend-vue/layers/dms-layout/app/custom-layouts/DefaultLayout.vue +9 -0
  206. package/frontend-vue/layers/dms-layout/app/custom-layouts/SettingsLayout.vue +9 -0
  207. package/frontend-vue/layers/dms-layout/app/custom-pages/modules-store.vue +291 -0
  208. package/frontend-vue/layers/dms-layout/app/custom-pages/modules.vue +87 -474
  209. package/frontend-vue/layers/dms-layout/app/middleware/module-routing.global.ts +3 -0
  210. package/frontend-vue/layers/dms-layout/app/pages/[...slug].vue +6 -22
  211. package/frontend-vue/layers/dms-layout/app/plugins/command-palette-navigation.client.ts +2 -1
  212. package/frontend-vue/layers/dms-layout/app/plugins/command-palette-personal-pages.client.ts +14 -44
  213. package/frontend-vue/layers/dms-layout/app/plugins/dms-dev-reload.client.ts +14 -9
  214. package/frontend-vue/layers/dms-layout/app/theme/form-controls.ts +9 -4
  215. package/frontend-vue/layers/dms-layout/app/theme/surfaces.ts +22 -6
  216. package/frontend-vue/layers/dms-layout/app/types/module-store.ts +9 -0
  217. package/frontend-vue/layers/dms-layout/app/types/page.ts +15 -1
  218. package/frontend-vue/layers/dms-layout/app/utils/account-menu.ts +11 -0
  219. package/frontend-vue/layers/dms-layout/app/utils/breadcrumb.ts +57 -0
  220. package/frontend-vue/layers/dms-layout/app/utils/pageQuery.ts +26 -0
  221. package/frontend-vue/layers/dms-layout/app/utils/pageRecord.ts +71 -0
  222. package/frontend-vue/layers/dms-layout/i18n/locales/layout-en-GB.json +39 -14
  223. package/frontend-vue/layers/dms-layout/i18n/locales/layout-fr-FR.json +38 -13
  224. package/frontend-vue/layers/dms-ui/app/build/components/containers/TargetComponentTree.vue +31 -0
  225. package/frontend-vue/layers/dms-ui/app/build/components/containers/constants.ts +25 -0
  226. package/frontend-vue/layers/dms-ui/app/build/components/containers/drawer/DynamicDrawer.vue +244 -15
  227. package/frontend-vue/layers/dms-ui/app/build/components/containers/modal/DynamicModal.vue +42 -25
  228. package/frontend-vue/layers/dms-ui/app/build/components/containers/row/RowHeader.vue +153 -0
  229. package/frontend-vue/layers/dms-ui/app/build/components/containers/row/RowNotFound.vue +44 -0
  230. package/frontend-vue/layers/dms-ui/app/build/components/containers/row/drawerRowHeader.ts +35 -0
  231. package/frontend-vue/layers/dms-ui/app/build/components/form/FormEntries.vue +44 -9
  232. package/frontend-vue/layers/dms-ui/app/build/components/form/FormErrorSummary.vue +11 -4
  233. package/frontend-vue/layers/dms-ui/app/build/components/form/FormFieldControl.vue +38 -5
  234. package/frontend-vue/layers/dms-ui/app/build/components/form/FormFieldNotes.vue +29 -5
  235. package/frontend-vue/layers/dms-ui/app/build/components/form/FormSectionNav.vue +3 -1
  236. package/frontend-vue/layers/dms-ui/app/build/components/form/FormSections.vue +106 -27
  237. package/frontend-vue/layers/dms-ui/app/build/components/table/Actions.vue +12 -4
  238. package/frontend-vue/layers/dms-ui/app/build/components/table/Empty.vue +4 -2
  239. package/frontend-vue/layers/dms-ui/app/build/components/table/FiltersRow.vue +38 -1
  240. package/frontend-vue/layers/dms-ui/app/build/components/table/Pagination.vue +55 -54
  241. package/frontend-vue/layers/dms-ui/app/build/components/table/RowSelection.vue +2 -1
  242. package/frontend-vue/layers/dms-ui/app/build/components/table/Table.vue +396 -288
  243. package/frontend-vue/layers/dms-ui/app/build/components/table/Tabs.vue +2 -1
  244. package/frontend-vue/layers/dms-ui/app/build/components/table/Views.vue +2 -1
  245. package/frontend-vue/layers/dms-ui/app/build/components/table-view/CardAvatar.vue +59 -0
  246. package/frontend-vue/layers/dms-ui/app/build/components/table-view/CardGridSkeleton.vue +19 -2
  247. package/frontend-vue/layers/dms-ui/app/build/components/table-view/CardRowActions.vue +38 -0
  248. package/frontend-vue/layers/dms-ui/app/build/components/table-view/CardsDisplay.vue +148 -104
  249. package/frontend-vue/layers/dms-ui/app/build/components/table-view/LiveHoldBanner.vue +17 -2
  250. package/frontend-vue/layers/dms-ui/app/build/composables/actions/bulkSelection.ts +43 -9
  251. package/frontend-vue/layers/dms-ui/app/build/composables/actions/rowComponentOptions.ts +76 -0
  252. package/frontend-vue/layers/dms-ui/app/build/composables/actions/rowNavigation.ts +172 -27
  253. package/frontend-vue/layers/dms-ui/app/build/composables/actions/useActionTargets.ts +270 -46
  254. package/frontend-vue/layers/dms-ui/app/build/composables/confirm/confirmDialogTexts.ts +2 -0
  255. package/frontend-vue/layers/dms-ui/app/build/composables/containers/closingLayers.ts +186 -0
  256. package/frontend-vue/layers/dms-ui/app/build/composables/containers/context.ts +14 -0
  257. package/frontend-vue/layers/dms-ui/app/build/composables/containers/drawerSize.ts +229 -0
  258. package/frontend-vue/layers/dms-ui/app/build/composables/containers/useResizableSize.ts +175 -0
  259. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/metricCells.ts +18 -35
  260. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/numberSuffix.ts +47 -0
  261. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/registerDefaults.ts +99 -24
  262. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/registerFieldTypes.ts +2 -1
  263. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/relationFilter.ts +75 -0
  264. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/relationRows.ts +71 -0
  265. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/relationValue.ts +39 -0
  266. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/twoLineCell.ts +3 -3
  267. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/useColumnValueRenderer.ts +6 -2
  268. package/frontend-vue/layers/dms-ui/app/build/composables/form/createFormFields.ts +15 -0
  269. package/frontend-vue/layers/dms-ui/app/build/composables/form/fieldSaved.ts +40 -0
  270. package/frontend-vue/layers/dms-ui/app/build/composables/form/fieldValueRule.ts +62 -0
  271. package/frontend-vue/layers/dms-ui/app/build/composables/form/formEntryContext.ts +5 -0
  272. package/frontend-vue/layers/dms-ui/app/build/composables/form/formFooter.ts +42 -7
  273. package/frontend-vue/layers/dms-ui/app/build/composables/form/formLayout.ts +106 -4
  274. package/frontend-vue/layers/dms-ui/app/build/composables/form/formSections.ts +32 -0
  275. package/frontend-vue/layers/dms-ui/app/build/composables/form/readonlyValue.ts +19 -0
  276. package/frontend-vue/layers/dms-ui/app/build/composables/form/registerFormFunctions.ts +27 -0
  277. package/frontend-vue/layers/dms-ui/app/build/composables/form/repeaterTable.ts +65 -0
  278. package/frontend-vue/layers/dms-ui/app/build/composables/form/serverFieldErrors.ts +49 -0
  279. package/frontend-vue/layers/dms-ui/app/build/composables/form/toasterClearance.ts +118 -0
  280. package/frontend-vue/layers/dms-ui/app/build/composables/form/useInstantForm.ts +12 -1
  281. package/frontend-vue/layers/dms-ui/app/build/composables/live/currentPageLive.ts +41 -3
  282. package/frontend-vue/layers/dms-ui/app/build/composables/live/livePoller.ts +10 -2
  283. package/frontend-vue/layers/dms-ui/app/build/composables/live/pageReadClock.ts +28 -0
  284. package/frontend-vue/layers/dms-ui/app/build/composables/live/shortcuts.ts +1 -1
  285. package/frontend-vue/layers/dms-ui/app/build/composables/live/usePageLiveHost.ts +17 -3
  286. package/frontend-vue/layers/dms-ui/app/build/composables/live/useUpdatedLabel.ts +38 -0
  287. package/frontend-vue/layers/dms-ui/app/build/composables/page-toolbar/pageToolbarContext.ts +75 -0
  288. package/frontend-vue/layers/dms-ui/app/build/composables/page-toolbar/toolbarControlHeight.ts +29 -0
  289. package/frontend-vue/layers/dms-ui/app/build/composables/period/periodComparison.ts +52 -0
  290. package/frontend-vue/layers/dms-ui/app/build/composables/period/periodDisplay.ts +21 -0
  291. package/frontend-vue/layers/dms-ui/app/build/composables/period/periodShortLabels.ts +28 -0
  292. package/frontend-vue/layers/dms-ui/app/build/composables/period/usePeriodDraft.ts +1 -0
  293. package/frontend-vue/layers/dms-ui/app/build/composables/table/useRelationFilterLabels.ts +73 -0
  294. package/frontend-vue/layers/dms-ui/app/build/composables/table/useRowScrollAnchor.ts +106 -0
  295. package/frontend-vue/layers/dms-ui/app/build/composables/table/useTable.ts +9 -0
  296. package/frontend-vue/layers/dms-ui/app/build/composables/table/useTableColumns.ts +1 -2
  297. package/frontend-vue/layers/dms-ui/app/build/composables/table/utils/archivedCount.ts +31 -0
  298. package/frontend-vue/layers/dms-ui/app/build/composables/table/utils/filterEditorValue.ts +38 -0
  299. package/frontend-vue/layers/dms-ui/app/build/composables/table/utils/formatFilterValue.ts +33 -2
  300. package/frontend-vue/layers/dms-ui/app/build/composables/table/utils/scrollEdges.ts +31 -0
  301. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useCardFields.ts +33 -0
  302. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useMobileDisplay.ts +48 -0
  303. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useScrolledPastTop.ts +41 -18
  304. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableFooter.ts +3 -2
  305. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableLinkedVisit.ts +78 -0
  306. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableLiveHold.ts +135 -22
  307. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableRows.ts +56 -25
  308. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViewRowActions.ts +77 -32
  309. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViews.ts +86 -10
  310. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/bulkActions.ts +14 -4
  311. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/card.ts +127 -2
  312. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/compareValues.ts +13 -0
  313. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/expandedRows.ts +28 -0
  314. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/groupedRows.ts +114 -1
  315. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/linkedArrival.ts +308 -0
  316. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/locateRow.ts +106 -0
  317. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/pageArrival.ts +34 -0
  318. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/pinnedRows.ts +17 -0
  319. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/quickFilters.ts +41 -33
  320. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/recordLink.ts +24 -8
  321. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/tableCounts.ts +113 -0
  322. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/tableQuery.ts +27 -4
  323. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/views.ts +22 -6
  324. package/frontend-vue/layers/dms-ui/app/build/utils/addressSuggestions.ts +49 -0
  325. package/frontend-vue/layers/dms-ui/app/build/utils/eyebrow.ts +12 -0
  326. package/frontend-vue/layers/dms-ui/app/build/utils/fieldOptionsI18n.ts +51 -11
  327. package/frontend-vue/layers/dms-ui/app/build/utils/inputWidth.ts +53 -0
  328. package/frontend-vue/layers/dms-ui/app/build/utils/numberAdjustment.ts +6 -3
  329. package/frontend-vue/layers/dms-ui/app/build/utils/relativeDate.ts +19 -5
  330. package/frontend-vue/layers/dms-ui/app/build/utils/scrollWithin.ts +99 -0
  331. package/frontend-vue/layers/dms-ui/app/build/utils/urlVariables.ts +36 -14
  332. package/frontend-vue/layers/dms-ui/app/components/PasswordStrength.vue +1 -1
  333. package/frontend-vue/layers/dms-ui/app/components/RecursiveComponent.vue +9 -1
  334. package/frontend-vue/layers/dms-ui/app/components/blocks/LiveToggleBlock.vue +80 -32
  335. package/frontend-vue/layers/dms-ui/app/components/blocks/PageToolbarBlock.vue +137 -0
  336. package/frontend-vue/layers/dms-ui/app/components/blocks/StatGroupBlock.vue +5 -0
  337. package/frontend-vue/layers/dms-ui/app/components/chart/ApexChartHost.vue +16 -2
  338. package/frontend-vue/layers/dms-ui/app/components/chart/Chart.vue +24 -5
  339. package/frontend-vue/layers/dms-ui/app/components/chart/ChartCard.vue +41 -23
  340. package/frontend-vue/layers/dms-ui/app/components/confirm/ConfirmModal.vue +1 -1
  341. package/frontend-vue/layers/dms-ui/app/components/container-footer/ContainerFooter.vue +34 -0
  342. package/frontend-vue/layers/dms-ui/app/components/field-row/FieldRow.vue +25 -2
  343. package/frontend-vue/layers/dms-ui/app/components/flow-canvas/FlowCanvas.vue +2 -2
  344. package/frontend-vue/layers/dms-ui/app/components/form/Form.vue +118 -36
  345. package/frontend-vue/layers/dms-ui/app/components/form/components/DatePicker.vue +20 -1
  346. package/frontend-vue/layers/dms-ui/app/components/form/components/Display.vue +13 -1
  347. package/frontend-vue/layers/dms-ui/app/components/form/components/Image.vue +8 -3
  348. package/frontend-vue/layers/dms-ui/app/components/form/components/InputAddress.vue +53 -5
  349. package/frontend-vue/layers/dms-ui/app/components/form/components/InputCode.vue +29 -5
  350. package/frontend-vue/layers/dms-ui/app/components/form/components/InputNumber.vue +45 -8
  351. package/frontend-vue/layers/dms-ui/app/components/form/components/InputPercentage.vue +28 -5
  352. package/frontend-vue/layers/dms-ui/app/components/form/components/InputSecret.vue +5 -0
  353. package/frontend-vue/layers/dms-ui/app/components/form/components/InputText.vue +19 -2
  354. package/frontend-vue/layers/dms-ui/app/components/form/components/InputTime.vue +2 -1
  355. package/frontend-vue/layers/dms-ui/app/components/form/components/Relation.vue +205 -65
  356. package/frontend-vue/layers/dms-ui/app/components/form/components/Repeater.vue +80 -30
  357. package/frontend-vue/layers/dms-ui/app/components/form/components/RepeaterTable.vue +203 -0
  358. package/frontend-vue/layers/dms-ui/app/components/form/components/Select.vue +4 -2
  359. package/frontend-vue/layers/dms-ui/app/components/grid/GridRow.vue +11 -2
  360. package/frontend-vue/layers/dms-ui/app/components/key-value-list/KeyValueList.vue +79 -70
  361. package/frontend-vue/layers/dms-ui/app/components/kpi/KpiCard.vue +117 -21
  362. package/frontend-vue/layers/dms-ui/app/components/meter/Meter.vue +2 -1
  363. package/frontend-vue/layers/dms-ui/app/components/period/PeriodSelector.vue +113 -34
  364. package/frontend-vue/layers/dms-ui/app/components/save-bar/SaveBar.vue +14 -2
  365. package/frontend-vue/layers/dms-ui/app/components/section-header/Eyebrow.vue +6 -1
  366. package/frontend-vue/layers/dms-ui/app/components/segmented/Segmented.vue +13 -0
  367. package/frontend-vue/layers/dms-ui/app/components/stat-group/StatGroup.vue +52 -3
  368. package/frontend-vue/layers/dms-ui/app/components/tab/Tab.vue +2 -1
  369. package/frontend-vue/layers/dms-ui/app/components/table-view/KanbanBoard.vue +45 -35
  370. package/frontend-vue/layers/dms-ui/app/components/table-view/KanbanDisplay.vue +4 -1
  371. package/frontend-vue/layers/dms-ui/app/components/table-view/TableView.vue +410 -96
  372. package/frontend-vue/layers/dms-ui/app/components/top-list/TopListCard.vue +33 -5
  373. package/frontend-vue/layers/dms-ui/app/composables/chart/apexCategoryLabels.ts +68 -0
  374. package/frontend-vue/layers/dms-ui/app/composables/chart/apexDatetimeBuckets.ts +306 -0
  375. package/frontend-vue/layers/dms-ui/app/composables/chart/formatValue.ts +4 -3
  376. package/frontend-vue/layers/dms-ui/app/composables/chart/heatmapColors.ts +93 -0
  377. package/frontend-vue/layers/dms-ui/app/composables/chart/localizeSeries.ts +62 -0
  378. package/frontend-vue/layers/dms-ui/app/composables/chart/useApexChart.ts +116 -8
  379. package/frontend-vue/layers/dms-ui/app/composables/chart/useApexChart.types.ts +7 -0
  380. package/frontend-vue/layers/dms-ui/app/composables/containers/types.ts +84 -1
  381. package/frontend-vue/layers/dms-ui/app/composables/containers/useDrawer.ts +69 -8
  382. package/frontend-vue/layers/dms-ui/app/composables/form/types/field-saved.ts +11 -0
  383. package/frontend-vue/layers/dms-ui/app/composables/form/types/field.ts +4 -0
  384. package/frontend-vue/layers/dms-ui/app/composables/form/types/functions.ts +1 -0
  385. package/frontend-vue/layers/dms-ui/app/composables/form/useForm.ts +142 -21
  386. package/frontend-vue/layers/dms-ui/app/composables/live/usePageLive.ts +11 -4
  387. package/frontend-vue/layers/dms-ui/app/composables/page-toolbar/usePageToolbarSize.ts +30 -0
  388. package/frontend-vue/layers/dms-ui/app/composables/table-view/kanban.ts +4 -2
  389. package/frontend-vue/layers/dms-ui/app/composables/table-view/types/action-target.ts +22 -3
  390. package/frontend-vue/layers/dms-ui/app/composables/table-view/types/config.ts +4 -0
  391. package/frontend-vue/layers/dms-ui/app/composables/table-view/types/display.ts +24 -1
  392. package/frontend-vue/layers/dms-ui/app/plugins/closing-layers.client.ts +10 -0
  393. package/frontend-vue/layers/dms-ui/app/plugins/live-pause-source.ts +15 -0
  394. package/frontend-vue/layers/dms-ui/app/plugins/register.ts +2 -0
  395. package/frontend-vue/layers/dms-ui/app/types/input-number.ts +3 -2
  396. package/frontend-vue/layers/dms-ui/app/types/page-toolbar.ts +7 -0
  397. package/frontend-vue/layers/dms-ui/app/types/row-action.ts +0 -2
  398. package/frontend-vue/layers/dms-ui/app/types/row-navigation.ts +17 -0
  399. package/frontend-vue/layers/dms-ui/app/types/table/query.ts +2 -1
  400. package/frontend-vue/layers/dms-ui/app/utils/blockRefresh.ts +3 -1
  401. package/frontend-vue/layers/dms-ui/app/utils/componentTree.ts +24 -0
  402. package/frontend-vue/layers/dms-ui/i18n/locales/ui-en-GB.json +16 -4
  403. package/frontend-vue/layers/dms-ui/i18n/locales/ui-fr-FR.json +16 -4
  404. package/frontend-vue/package.json +2 -2
  405. package/package.json +7 -6
  406. package/skills/dms/SKILL.md +1 -1
  407. package/skills/dms-auth/SKILL.md +5 -3
  408. package/skills/dms-best-practices/REFERENCE.md +2 -0
  409. package/skills/dms-best-practices/SKILL.md +4 -0
  410. package/skills/dms-module/SKILL.md +3 -2
  411. package/skills/dms-pages/REFERENCE.md +125 -16
  412. package/skills/dms-pages/SKILL.md +1 -1
  413. package/skills/dms-project/DEV-SERVERS.md +23 -1
  414. package/skills/dms-project/REFERENCE.md +1 -1
  415. package/skills/dms-project/SKILL.md +4 -1
  416. package/skills/dms-project/scripts/dms-dev.sh +205 -17
  417. package/dist/routes/startup-gate.d.ts +0 -7
  418. package/dist/routes/startup-gate.js +0 -39
  419. package/dist/routes/startup-gate.js.map +0 -1
  420. package/frontend-vue/layers/dms-core/app/composables/period/periodRenderNow.ts +0 -42
@@ -16,6 +16,10 @@ import {
16
16
  numberAdjustment,
17
17
  parseTypedNumber,
18
18
  } from "../../../build/utils/numberAdjustment";
19
+ import {
20
+ type InputWidth,
21
+ isCompactNumberInput,
22
+ } from "../../../build/utils/inputWidth";
19
23
  import type { InputNumberAdjustment } from "../../../types/input-number";
20
24
 
21
25
  defineOptions({ inheritAttrs: false });
@@ -28,12 +32,23 @@ interface InputNumberFieldProps extends InputNumberProps {
28
32
  * it as the field's error, instead of clamping it into range on blur.
29
33
  */
30
34
  clamp?: boolean;
35
+ /**
36
+ * `"compact"` sizes the field for a short number from `sm` up, `"full"`
37
+ * fills the row; left out, a `max` of a few digits makes it compact.
38
+ */
39
+ width?: InputWidth;
40
+ /**
41
+ * `true` puts a typed value on the nearest `step` when the field is left.
42
+ * Off by default: `step` only drives the steppers and the arrow keys, and
43
+ * a value is never rewritten (9,692 kept as 9,692 with steps of 100).
44
+ */
45
+ stepSnapping?: boolean;
31
46
  }
32
47
 
33
48
  interface InputNumberAdjustEmits {
34
49
  /**
35
50
  * The field kept another value than the one typed, on leaving it or on
36
- * Enter: clamped to `min` / `max`, or put on its `step`.
51
+ * Enter: clamped to `min` / `max`, or put on its `step` (`stepSnapping`).
37
52
  */
38
53
  adjust: [adjustment: InputNumberAdjustment];
39
54
  }
@@ -42,6 +57,7 @@ type InputNumberFieldEmits = InputNumberEmits & InputNumberAdjustEmits;
42
57
 
43
58
  const props = withDefaults(defineProps<InputNumberFieldProps>(), {
44
59
  clamp: true,
60
+ stepSnapping: false,
45
61
  });
46
62
  const emits = defineEmits<InputNumberFieldEmits>();
47
63
  defineSlots<InputNumberSlots>();
@@ -54,6 +70,7 @@ const inputProps = computed(() => {
54
70
  const {
55
71
  suffix: _suffix,
56
72
  clamp: _clamp,
73
+ width: _width,
57
74
  onAdjust: _onAdjust,
58
75
  ...rest
59
76
  } = forwarded.value;
@@ -64,12 +81,32 @@ const inputProps = computed(() => {
64
81
  ...numberStepperState(props.modelValue, bounds.value),
65
82
  };
66
83
  });
84
+ const isCompact = computed(() => isCompactNumberInput(props));
67
85
  const { processI18n } = useTranslation();
68
86
  const { locale } = useI18n();
69
87
 
70
88
  // v2 number fields keep the value left-aligned with a compact caret stepper.
71
89
  const DEFAULT_ORIENTATION = "vertical";
72
90
 
91
+ // The unit sits inside the box, before the stepper, so a field with a unit
92
+ // is as wide as the fields around it: the value's end padding makes room for
93
+ // the stepper (2rem, `end-8` below), a gap and the unit, sized in
94
+ // characters (`ch` slightly overestimates a short unit, never clips it).
95
+ const SUFFIX_BASE_CLASS = "pe-[calc(var(--dms-field-suffix-width)+2.375rem)]";
96
+ const suffixText = computed(() =>
97
+ props.suffix ? processI18n(props.suffix) : "",
98
+ );
99
+ const suffixStyle = computed(() =>
100
+ suffixText.value
101
+ ? { "--dms-field-suffix-width": `${suffixText.value.length}ch` }
102
+ : undefined,
103
+ );
104
+ const fieldUi = computed(() =>
105
+ suffixText.value
106
+ ? { ...props.ui, base: [props.ui?.base, SUFFIX_BASE_CLASS] }
107
+ : props.ui,
108
+ );
109
+
73
110
  // A minus typed or pasted where no negative number is allowed is refused
74
111
  // by the field before it shows: it says why, rather than leaving an emptied
75
112
  // field to read as never filled.
@@ -134,10 +171,9 @@ async function settle(event: Event): Promise<void> {
134
171
  </script>
135
172
 
136
173
  <template>
137
- <!-- The unit sits after the field rather than inside it, where the
138
- stepper already takes the end of the box. -->
139
174
  <div
140
- :class="props.suffix ? 'flex items-center gap-2' : 'contents'"
175
+ :class="suffixText ? 'relative flex items-center' : 'contents'"
176
+ :style="suffixStyle"
141
177
  @beforeinput.capture="onIncomingText"
142
178
  @input="onTyped"
143
179
  @keydown.enter="settle"
@@ -146,17 +182,18 @@ async function settle(event: Event): Promise<void> {
146
182
  v-bind="{ ...inputProps, ...$attrs }"
147
183
  @blur="settle"
148
184
  @update:model-value="onSettledValue"
149
- :class="props.suffix && 'min-w-0 flex-1'"
185
+ :class="[suffixText && 'min-w-0 flex-1', isCompact && 'sm:max-w-44']"
186
+ :ui="fieldUi"
150
187
  :orientation="props.orientation ?? DEFAULT_ORIENTATION"
151
188
  :increment="{ color: 'neutral' }"
152
189
  :decrement="{ color: 'neutral' }"
153
190
  />
154
191
  <span
155
- v-if="props.suffix"
156
- class="text-muted shrink-0 text-[13px]"
192
+ v-if="suffixText"
193
+ class="text-muted pointer-events-none absolute inset-y-0 end-8 flex items-center text-[13px] whitespace-nowrap"
157
194
  data-field-suffix
158
195
  >
159
- {{ processI18n(props.suffix) }}
196
+ {{ suffixText }}
160
197
  </span>
161
198
  </div>
162
199
  </template>
@@ -4,10 +4,20 @@ import type {
4
4
  InputNumberEmits,
5
5
  InputNumberSlots,
6
6
  } from "@nuxt/ui";
7
+ import { computed } from "vue";
8
+ import {
9
+ type InputWidth,
10
+ isCompactNumberInput,
11
+ } from "../../../build/utils/inputWidth";
12
+
13
+ interface InputPercentageProps extends InputNumberProps {
14
+ /** As on `InputNumber`; left out, the 0-100 bounds make it compact. */
15
+ width?: InputWidth;
16
+ }
7
17
 
8
18
  const PERCENTAGE_MULTIPLIER = 100;
9
19
 
10
- const props = defineProps<InputNumberProps>();
20
+ const props = defineProps<InputPercentageProps>();
11
21
  const emits = defineEmits<InputNumberEmits>();
12
22
  defineSlots<InputNumberSlots>();
13
23
 
@@ -25,13 +35,23 @@ const displayValue = computed({
25
35
  },
26
36
  });
27
37
 
38
+ const bounds = computed(() => ({
39
+ min: props.min ?? 0,
40
+ max: props.max ?? PERCENTAGE_MULTIPLIER,
41
+ }));
42
+ const isCompact = computed(() =>
43
+ isCompactNumberInput({ width: props.width, ...bounds.value }),
44
+ );
45
+
28
46
  const forwardedProps = computed(() => {
47
+ const { width: _width, ...rest } = props;
29
48
  return {
30
- ...props,
49
+ ...rest,
50
+ ...bounds.value,
31
51
  modelValue: displayValue.value,
32
- min: props.min ?? 0,
33
- max: props.max ?? PERCENTAGE_MULTIPLIER,
34
52
  step: props.step ?? 0.01,
53
+ // The step drives the steppers only: a stored 12.345 % stays as it is.
54
+ stepSnapping: props.stepSnapping ?? false,
35
55
  // The wrapper takes the caller's class, so the field fills it.
36
56
  class: "w-full",
37
57
  ui: { ...props.ui, base: ["pe-7", props.ui?.base] },
@@ -40,7 +60,10 @@ const forwardedProps = computed(() => {
40
60
  </script>
41
61
 
42
62
  <template>
43
- <div class="relative inline-flex" :class="props.class">
63
+ <div
64
+ class="relative inline-flex"
65
+ :class="[props.class, isCompact && 'sm:max-w-44']"
66
+ >
44
67
  <UInputNumber
45
68
  v-bind="forwardedProps"
46
69
  @update:model-value="displayValue = $event"
@@ -1,6 +1,7 @@
1
1
  <script setup lang="ts">
2
2
  import { pageRequestUrl } from "../../../build/composables/page/pageUrlValues";
3
3
  import { useConfirm } from "../../../composables/confirm/useConfirm";
4
+ import { FORM_FIELD_SAVED_KEY } from "../../../composables/form/types/field-saved";
4
5
  import DmsCopyButton from "../../copy/CopyButton.vue";
5
6
 
6
7
  interface InputSecretProps {
@@ -23,6 +24,7 @@ const { t } = useI18n();
23
24
  const toast = useToast();
24
25
  const { confirm } = useConfirm();
25
26
  const { $authFetch } = useAuthFetch();
27
+ const markFieldSaved = inject(FORM_FIELD_SAVED_KEY, null);
26
28
 
27
29
  // v2 .secret: the end of the value stays visible, so a reader can tell two
28
30
  // secrets apart without revealing either.
@@ -56,6 +58,9 @@ async function rotate(): Promise<void> {
56
58
  method: "POST",
57
59
  });
58
60
  model.value = response.value;
61
+ // The server stored the new secret already: it is not a change to
62
+ // save, and "Discard" must not bring the old one back.
63
+ if (props.id) markFieldSaved?.(props.id, response.value);
59
64
  },
60
65
  });
61
66
  if (confirmed) {
@@ -7,12 +7,23 @@ import type {
7
7
  import { computed } from "vue";
8
8
  import { useForwardPropsEmits } from "reka-ui";
9
9
  import { characterCountText } from "../../../build/utils/characterCount";
10
+ import {
11
+ type InputWidth,
12
+ isCompactTextInput,
13
+ } from "../../../build/utils/inputWidth";
10
14
 
11
15
  interface InputTextProps extends InputProps {
12
16
  /** Longest value accepted; the counter reads against it. */
13
17
  maxLength?: number;
14
18
  /** Shows the characters typed at the end of the field ("4 / 60"). */
15
19
  showCount?: boolean;
20
+ /**
21
+ * `"compact"` sizes the field for a short text from `sm` up, `"full"`
22
+ * fills the row; left out, a `maxLength` of 16 or less makes it compact.
23
+ */
24
+ width?: InputWidth;
25
+ /** How the value reads shown, not edited (a URL's image): not the input's. */
26
+ preview?: "avatar" | "image";
16
27
  }
17
28
 
18
29
  const props = defineProps<InputTextProps>();
@@ -21,16 +32,22 @@ defineSlots<InputSlots>();
21
32
 
22
33
  const forwarded = useForwardPropsEmits(props, emits);
23
34
  const inputProps = computed(() => {
24
- const { showCount: _showCount, ...rest } = forwarded.value;
35
+ const {
36
+ showCount: _showCount,
37
+ width: _width,
38
+ preview: _preview,
39
+ ...rest
40
+ } = forwarded.value;
25
41
  return rest;
26
42
  });
43
+ const isCompact = computed(() => isCompactTextInput(props));
27
44
  const countText = computed(() =>
28
45
  characterCountText(props.modelValue, props.maxLength),
29
46
  );
30
47
  </script>
31
48
 
32
49
  <template>
33
- <UInput v-bind="inputProps">
50
+ <UInput v-bind="inputProps" :class="isCompact && 'sm:max-w-44'">
34
51
  <template v-if="props.showCount" #trailing>
35
52
  <span class="text-dimmed text-xs tabular-nums" data-character-count>
36
53
  {{ countText }}
@@ -10,6 +10,7 @@ interface InputTimeProps extends Omit<InputProps, "modelValue" | "type"> {
10
10
 
11
11
  const props = defineProps<InputTimeProps>();
12
12
  const { report } = useControlError();
13
+ const { locale } = useI18n();
13
14
  const UNREADABLE_DURATION = "$dms.field_errors.invalid_duration";
14
15
  const emits = defineEmits<{
15
16
  "update:modelValue": [value: number | null];
@@ -21,7 +22,7 @@ const isUserTyping = ref(false);
21
22
  const updateDisplayValue = () => {
22
23
  if (isUserTyping.value) return;
23
24
  if (props.modelValue !== undefined && props.modelValue !== null) {
24
- displayValue.value = formatTimeSpan(props.modelValue);
25
+ displayValue.value = formatDuration(props.modelValue, locale.value);
25
26
  } else {
26
27
  displayValue.value = "";
27
28
  }
@@ -16,11 +16,24 @@ 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 { useRelationRows } from "../../../build/composables/data-types/relationRows";
20
23
  import type { FormProps } from "../../../composables/form/types";
24
+ import { sameFormValue } from "../../../build/composables/unsaved-changes/formValue";
25
+ import { FORM_ENTRY_CONTEXT_KEY } from "../../../build/composables/form/formEntryContext";
26
+ import {
27
+ type RelationFilterBy,
28
+ relationFilterParams,
29
+ resolveRelationPick,
30
+ } from "../../../build/composables/data-types/relationFilter";
21
31
 
22
32
  const SEARCH_DEBOUNCE_MS = 200;
23
- const ADD_NEW_VALUE = "__dms_relation_add_new__";
33
+ // How long the menu's content gets to leave the page before the add form opens
34
+ // anyway, and the beat its focus return takes once it has.
35
+ const MENU_CLOSE_MAX_MS = 1000;
36
+ const MENU_FOCUS_SETTLE_MS = 60;
24
37
 
25
38
  /** Title and description of the drawer a relation picker's "add" opens. */
26
39
  interface RelationAddFormTexts {
@@ -38,6 +51,11 @@ interface RelationProps extends SelectMenuProps<T, "value", M> {
38
51
  avatar?: string;
39
52
  disabled?: string;
40
53
  };
54
+ /**
55
+ * Lists only the related rows whose `field` equals the value of the form
56
+ * field `from`; ignored outside a form (a filter's picker).
57
+ */
58
+ filterBy?: RelationFilterBy;
41
59
  addForm?: { componentName: string; options?: FormProps };
42
60
  addPermissionIds?: string[];
43
61
  /**
@@ -63,33 +81,24 @@ const forwardedProps = computed(() => {
63
81
  addForm: ______,
64
82
  addPermissionIds: _______,
65
83
  addFormTexts: ________,
84
+ filterBy: _________,
66
85
  ...rest
67
86
  } = props;
68
87
  return rest as SelectMenuProps<T, "value", M>;
69
88
  });
70
- const forwarded = useForwardPropsEmits(forwardedProps, emits);
89
+ const forwardedPropsEmits = useForwardPropsEmits(forwardedProps, emits);
90
+ // The picks go through `handleUpdateModelValue` alone, which deselects a value
91
+ // picked again: forwarded as well, the form got the pick it had undone.
92
+ const forwarded = computed(() => {
93
+ const { "onUpdate:modelValue": _, ...rest } = forwardedPropsEmits.value as {
94
+ "onUpdate:modelValue"?: unknown;
95
+ };
96
+ return rest as typeof forwardedPropsEmits.value;
97
+ });
71
98
 
72
99
  type ModelValue = SelectMenuEmits<T, "value", M>["update:modelValue"][0];
73
100
 
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
101
  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
102
  const shouldDeselect =
94
103
  props.deselectable && !props.multiple && value === props.modelValue;
95
104
  emits(
@@ -103,7 +112,23 @@ const instanceId = useId();
103
112
  const searchTerm = ref("");
104
113
  const searchTermDebounced = refDebounced(searchTerm, SEARCH_DEBOUNCE_MS);
105
114
 
115
+ // The form this picker fills, whose `filterBy.from` field narrows it.
116
+ const formContext = inject(FORM_ENTRY_CONTEXT_KEY, null);
117
+ const filterSourceValue = computed(() =>
118
+ props.filterBy && formContext
119
+ ? formContext.state.value[props.filterBy.from]
120
+ : undefined,
121
+ );
122
+ const filterParams = computed(() =>
123
+ relationFilterParams(props.filterBy, filterSourceValue.value),
124
+ );
125
+ // A string, so a recomputed but equal filter does not search again.
126
+ const filterKey = computed(() => JSON.stringify(filterParams.value));
127
+ // The source value the record loaded with, which its loaded pick matches.
128
+ const loadedFilterSource = filterSourceValue.value;
129
+
106
130
  const { $authFetch } = useAuthFetch();
131
+ const { readRelationRows } = useRelationRows();
107
132
  const toast = useToast();
108
133
  const { t } = useI18n();
109
134
  const { processI18n } = useTranslation();
@@ -118,6 +143,9 @@ const addContainerId = `relation-add-${useId()}`;
118
143
  // unwrapped item is an unbounded type for the checker. The list is always
119
144
  // replaced, never mutated in place.
120
145
  const locallyAddedItems: Ref<SelectMenuItem[]> = shallowRef([]);
146
+ // The rows of the ids the value holds, read from the `select` route when the
147
+ // form's data answers the ids rather than the joined rows.
148
+ const valueItems: Ref<SelectMenuItem[]> = shallowRef([]);
121
149
 
122
150
  const canAdd = computed(
123
151
  () =>
@@ -197,6 +225,7 @@ async function fetchRelationItems(): Promise<RelationItems> {
197
225
  const data = await $authFetch<RelationResponse>(url, {
198
226
  params: {
199
227
  search: searchTermDebounced.value || undefined,
228
+ ...filterParams.value,
200
229
  },
201
230
  onRequestError: ({ error }: { error: Error }) => {
202
231
  toast.add({
@@ -234,7 +263,7 @@ const { data, status, execute } = await useDmsAsyncData(
234
263
  {
235
264
  lazy: true,
236
265
  immediate: false,
237
- watch: [searchTermDebounced],
266
+ watch: [searchTermDebounced, filterKey],
238
267
  // Options to pick from, not the page's data.
239
268
  refreshOnPageRefresh: false,
240
269
  },
@@ -246,6 +275,8 @@ function isMappableObject(value: unknown): value is Record<string, string> {
246
275
 
247
276
  const initialItems = computed(() => {
248
277
  if (!props.initialValue) return [];
278
+ // The loaded row matched the loaded source value, not another one.
279
+ if (!sameFormValue(filterSourceValue.value, loadedFilterSource)) return [];
249
280
 
250
281
  const values = Array.isArray(props.initialValue)
251
282
  ? props.initialValue
@@ -254,42 +285,28 @@ const initialItems = computed(() => {
254
285
  return values.filter(isMappableObject).map(mapData);
255
286
  });
256
287
 
288
+ function itemValue(item: SelectMenuItem): unknown {
289
+ return typeof item === "object" && item !== null && "value" in item
290
+ ? item.value
291
+ : item;
292
+ }
293
+
257
294
  const mergedItems = computed<SelectMenuItem[]>(() => {
258
295
  const searchResults = data.value?.results || [];
259
296
  const initial = initialItems.value || [];
260
297
  const added = locallyAddedItems.value;
261
298
 
262
299
  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;
300
+ const existingValues = new Set(merged.map(itemValue));
276
301
 
277
- if (!existingValues.has(itemValue)) {
302
+ [...valueItems.value, ...searchResults].forEach((item) => {
303
+ const value = itemValue(item);
304
+ if (!existingValues.has(value)) {
305
+ existingValues.add(value);
278
306
  merged.push(item);
279
307
  }
280
308
  });
281
309
 
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
310
  return merged;
294
311
  });
295
312
 
@@ -326,34 +343,139 @@ watch(
326
343
  { immediate: true },
327
344
  );
328
345
 
346
+ // The pick as the form holds it, read alongside the source field: a record
347
+ // loading both values changes them in the same tick.
348
+ const pickValue = computed(() =>
349
+ formContext && props.id
350
+ ? formContext.state.value[props.id]
351
+ : props.modelValue,
352
+ );
353
+ let pickSource = loadedFilterSource;
354
+
355
+ // A pick made for another value of the source field is void once it changes.
356
+ watch(
357
+ [filterSourceValue, pickValue],
358
+ ([source, pick], [prevSource, prevPick]) => {
359
+ if (!props.filterBy || !formContext) return;
360
+ const outcome = resolveRelationPick({
361
+ source,
362
+ pick,
363
+ previousSource: prevSource,
364
+ previousPick: prevPick,
365
+ pickSource,
366
+ });
367
+ pickSource = outcome.pickSource;
368
+ if (!outcome.shouldClear) return;
369
+ emits("update:modelValue", (props.multiple ? [] : undefined) as ModelValue);
370
+ },
371
+ );
372
+
373
+ const isMenuOpen = ref(false);
374
+
375
+ const slots = useSlots();
376
+ // `content-bottom` is filled below, after the add entry.
377
+ const forwardedSlotNames = computed(() =>
378
+ Object.keys(slots).filter((name) => name !== "content-bottom"),
379
+ );
380
+
381
+ const labeledIds = computed(
382
+ () => new Set(mergedItems.value.map((item) => String(itemValue(item)))),
383
+ );
384
+ // Asked once each: an id the route does not answer (deleted, out of reach)
385
+ // keeps showing as is rather than being asked again.
386
+ const requestedIds = new Set<string>();
387
+
388
+ async function loadValueItems(ids: string[]) {
389
+ for (const id of ids) requestedIds.add(id);
390
+ const rows = await readRelationRows(
391
+ { searchUrl: props.searchUrl, keyMapping: props.keyMapping },
392
+ ids,
393
+ );
394
+ if (!rows.length) return;
395
+ valueItems.value = [
396
+ ...valueItems.value,
397
+ ...rows.map((item) => mapData(item as Record<string, string>)),
398
+ ];
399
+ }
400
+
401
+ // The picker shows a value through its item: an id no item carries yet is
402
+ // labelled (and given its avatar) from its row before the picker is opened.
403
+ onMounted(() => {
404
+ watch(
405
+ () =>
406
+ unlabeledRelationIds(props.modelValue, labeledIds.value).filter(
407
+ (id) => !requestedIds.has(id),
408
+ ),
409
+ (ids) => {
410
+ if (ids.length > 0) void loadValueItems(ids);
411
+ },
412
+ { immediate: true },
413
+ );
414
+ });
415
+
329
416
  function onOpen(isOpen: boolean) {
417
+ isMenuOpen.value = isOpen;
330
418
  if (isOpen && !data.value?.results?.length) {
331
419
  execute();
332
420
  }
333
421
  }
334
422
 
423
+ const addEntryRef = useTemplateRef<HTMLElement>("addEntry");
424
+
425
+ function waitForDetached(element: HTMLElement | null): Promise<void> {
426
+ const startedAt = performance.now();
427
+ return new Promise((resolve) => {
428
+ const check = () => {
429
+ if (
430
+ !element?.isConnected ||
431
+ performance.now() - startedAt > MENU_CLOSE_MAX_MS
432
+ ) {
433
+ resolve();
434
+ return;
435
+ }
436
+ requestAnimationFrame(check);
437
+ };
438
+ check();
439
+ });
440
+ }
441
+
442
+ /**
443
+ * Opens the add form once the menu has left the page: its content animates
444
+ * out, then hands the focus back to the field, and a drawer opened before
445
+ * that took the focus leaving it for a click outside and closed at once.
446
+ */
447
+ async function openAddDrawerOnceClosed(): Promise<void> {
448
+ const menuContent = addEntryRef.value;
449
+ isMenuOpen.value = false;
450
+ await waitForDetached(menuContent);
451
+ await new Promise((resolve) => setTimeout(resolve, MENU_FOCUS_SETTLE_MS));
452
+ openAddDrawer();
453
+ }
454
+
335
455
  function openAddDrawer() {
336
456
  const formOptions = addFormOptions.value;
337
457
  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);
458
+ const drawer = openDrawer(
459
+ dmsOwnDrawer({
460
+ title: props.addFormTexts?.displayName
461
+ ? processI18n(props.addFormTexts.displayName)
462
+ : t("dms.table.new_item"),
463
+ description: props.addFormTexts?.description
464
+ ? processI18n(props.addFormTexts.description)
465
+ : t("dms.table.new_item_description"),
466
+ direction: "bottom",
467
+ containerId: addContainerId,
468
+ component: DmsForm,
469
+ componentOptions: {
470
+ ...formOptions,
471
+ onSuccessCallback: (response: unknown, formData: unknown) => {
472
+ clearGuards(addContainerId);
473
+ drawer.close();
474
+ handleAddSuccess(response, formData);
475
+ },
354
476
  },
355
- },
356
- });
477
+ }),
478
+ );
357
479
  }
358
480
 
359
481
  function extractInsertedId(response: unknown): string | undefined {
@@ -428,20 +550,38 @@ const addFormOptions = computed(() => {
428
550
  v-model:search-term="searchTerm"
429
551
  :trailing-icon="props.trailingIcon ?? FIELD_TRIGGER_ICON"
430
552
  :model-value="props.modelValue"
553
+ :open="isMenuOpen"
431
554
  :items="displayItems as T"
432
555
  :loading="status === 'pending'"
433
556
  value-key="value"
434
557
  :portal="false"
435
558
  :ignore-filter="true"
436
559
  :ui="{ content: 'z-50' }"
560
+ :aria-label="t('dms.a11y.show_options')"
437
561
  @update:open="onOpen"
438
562
  @update:model-value="handleUpdateModelValue"
439
563
  >
440
- <template v-for="(_, name) in $slots" :key="name" #[name]="slotData">
564
+ <template v-for="name in forwardedSlotNames" :key="name" #[name]="slotData">
441
565
  <slot
442
566
  :name="name as keyof SelectMenuSlots<T, 'value', M>"
443
567
  v-bind="slotData ?? {}"
444
568
  />
445
569
  </template>
570
+ <!-- The add entry is no item: picked, it became the field's value. -->
571
+ <template v-if="canAdd || $slots['content-bottom']" #content-bottom>
572
+ <div v-if="canAdd" ref="addEntry" class="border-default border-t p-1">
573
+ <UButton
574
+ :label="t('dms.form.relation.add_new')"
575
+ icon="i-ph-plus"
576
+ color="neutral"
577
+ variant="ghost"
578
+ size="sm"
579
+ block
580
+ class="justify-start"
581
+ @click="openAddDrawerOnceClosed"
582
+ />
583
+ </div>
584
+ <slot name="content-bottom" />
585
+ </template>
446
586
  </USelectMenu>
447
587
  </template>