@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
@@ -1,8 +1,12 @@
1
+ import {
2
+ formatDuration,
3
+ readableNumberFormat,
4
+ } from "#dms-core/app/utils/formatter";
1
5
  import { defineAsyncComponent, h } from "vue";
2
6
  import type { DataType } from "#dms-core/app/composables/data-types/useDataType";
3
7
  import StatusPill from "../../../components/status-pill/StatusPill.vue";
4
8
  import MonoCell from "../../components/table/MonoCell.vue";
5
- import { useComposedText } from "../../../../../dms-core/app/composables/translation/useComposedText";
9
+ import { getAppComposedText } from "../../../../../dms-core/app/composables/translation/useComposedText";
6
10
  import {
7
11
  readRowField,
8
12
  readRowTone,
@@ -11,6 +15,7 @@ import {
11
15
  toneClass,
12
16
  type Row,
13
17
  } from "./cellHelpers";
18
+ import { getAppTranslation } from "../../../../../dms-core/app/composables/translation/useTranslation";
14
19
 
15
20
  const Sparkline = defineAsyncComponent(
16
21
  () => import("../../../components/chart/internal/Sparkline.vue"),
@@ -50,8 +55,8 @@ function statusPillTone(opts: StatusPillOptions, key: string, row: Row) {
50
55
  * `subTone`, else the pill's.
51
56
  */
52
57
  function renderStatusPill(value: unknown, options: unknown, row: Row) {
53
- const { processI18n } = useTranslation();
54
- const { processText } = useComposedText();
58
+ const { processI18n } = getAppTranslation();
59
+ const { processText } = getAppComposedText();
55
60
  const opts = (options ?? {}) as StatusPillOptions;
56
61
  const key = String(value);
57
62
  const item = opts.typeOptions?.items?.find(
@@ -175,10 +180,6 @@ function renderSparkline(options: unknown, row: Row) {
175
180
  }
176
181
 
177
182
  const MS_PER_SECOND = 1000;
178
- const SECONDS_PER_MINUTE = 60;
179
- const MINUTES_PER_HOUR = 60;
180
- const MS_PER_MINUTE = MS_PER_SECOND * SECONDS_PER_MINUTE;
181
- const MS_PER_HOUR = MS_PER_MINUTE * MINUTES_PER_HOUR;
182
183
  const DURATION_UNIT_MS: Record<string, number> = { ms: 1, s: MS_PER_SECOND };
183
184
 
184
185
  // SI symbols read the same in every language; the figure follows the locale.
@@ -188,39 +189,16 @@ const withSymbol = (
188
189
  symbol: string,
189
190
  maximumFractionDigits = 0,
190
191
  ): string =>
191
- `${new Intl.NumberFormat(locale, { maximumFractionDigits }).format(value)} ${symbol}`;
192
+ `${readableNumberFormat(locale, { maximumFractionDigits }).format(value)} ${symbol}`;
192
193
 
193
194
  /** A duration always in milliseconds, its digits grouped: "2,149 ms". */
194
195
  export function formatMilliseconds(ms: number, locale: string): string {
195
- const figure = new Intl.NumberFormat(locale, {
196
+ const figure = readableNumberFormat(locale, {
196
197
  maximumFractionDigits: 0,
197
- // `true` groups a four-digit figure too, where a locale's default (es,
198
- // pl) would leave "2149" against "12.149" further down the column.
199
- useGrouping: true,
200
198
  }).format(ms);
201
199
  return `${figure} ms`;
202
200
  }
203
201
 
204
- /**
205
- * A duration in its most readable unit: "910 ms", "1.8 s", "4 min 12 s",
206
- * "2 h 5 min".
207
- */
208
- export function formatDuration(ms: number, locale: string): string {
209
- const absolute = Math.abs(ms);
210
- if (absolute < MS_PER_SECOND) return withSymbol(locale, Math.round(ms), "ms");
211
- if (absolute < MS_PER_MINUTE) {
212
- return withSymbol(locale, ms / MS_PER_SECOND, "s", 1);
213
- }
214
- const [major, majorSymbol, minor, minorSymbol] =
215
- absolute < MS_PER_HOUR
216
- ? ([MS_PER_MINUTE, "min", MS_PER_SECOND, "s"] as const)
217
- : ([MS_PER_HOUR, "h", MS_PER_MINUTE, "min"] as const);
218
- const whole = Math.trunc(ms / major);
219
- const rest = Math.round((ms - whole * major) / minor);
220
- const head = withSymbol(locale, whole, majorSymbol);
221
- return rest === 0 ? head : `${head} ${withSymbol(locale, rest, minorSymbol)}`;
222
- }
223
-
224
202
  const BYTE_SYMBOLS = ["B", "kB", "MB", "GB", "TB"];
225
203
  const BYTE_STEP = 1000;
226
204
 
@@ -253,8 +231,12 @@ interface DurationOptions {
253
231
  toneField?: string;
254
232
  }
255
233
 
256
- const DURATION_FORMATS: Record<string, typeof formatDuration> = {
257
- readable: formatDuration,
234
+ type DurationWriter = (ms: number, locale: string) => string;
235
+
236
+ // A cell is read at a glance: the readable duration is rounded there.
237
+ const DURATION_FORMATS: Record<string, DurationWriter> = {
238
+ readable: (ms, locale) =>
239
+ formatDuration(ms, locale, { precision: "rounded" }),
258
240
  ms: formatMilliseconds,
259
241
  };
260
242
 
@@ -270,7 +252,8 @@ function renderDuration(
270
252
  ) {
271
253
  const opts = (options ?? {}) as DurationOptions;
272
254
  const ms = numberOf(value) * (DURATION_UNIT_MS[opts.unit ?? "ms"] ?? 1);
273
- const format = DURATION_FORMATS[opts.format ?? "readable"] ?? formatDuration;
255
+ const format =
256
+ DURATION_FORMATS[opts.format ?? "readable"] ?? DURATION_FORMATS.readable!;
274
257
  return figure(
275
258
  format(ms, locale),
276
259
  rowToneTextClass(row, opts.toneField, FIGURE_TONE_CLASS),
@@ -0,0 +1,47 @@
1
+ import type { DataTypeFormatter } from "#dms-core/app/composables/data-types/useDataType";
2
+ import {
3
+ getPluginI18n,
4
+ resolveI18nKey,
5
+ } from "../../../../../dms-core/app/composables/translation/useTranslation";
6
+
7
+ /** A formatter writing a number for a locale, with Intl's options. */
8
+ type NumberFormatter = (
9
+ value: unknown,
10
+ locale: string,
11
+ options?: Intl.NumberFormatOptions,
12
+ ) => unknown;
13
+
14
+ /** The options of a numeric field a cell reads besides Intl's. */
15
+ interface NumberSuffixOptions {
16
+ /** Unit after the value ("days"): an i18n key with `$`, or a literal. */
17
+ suffix?: string;
18
+ }
19
+
20
+ // Keeps the unit on the line of its number when a cell wraps.
21
+ const UNIT_SEPARATOR = " ";
22
+
23
+ /**
24
+ * A numeric data type's formatter writing the field's unit after the value,
25
+ * as its form input does ("12.5 kWp"), wherever the type is drawn: table
26
+ * cells, expanded rows, cards, read-only fields. A value that is not a
27
+ * number is left as the formatter returns it.
28
+ *
29
+ * A formatter also runs where no component setup is active (a footer sum, a
30
+ * group header, an export), so the unit is translated through the
31
+ * application's composer, never through a composable.
32
+ */
33
+ export function withNumberSuffix(format: NumberFormatter): DataTypeFormatter {
34
+ return (value, locale, options) => {
35
+ const formatted = format(
36
+ value,
37
+ locale,
38
+ options as Intl.NumberFormatOptions | undefined,
39
+ );
40
+ const suffix = (options as NumberSuffixOptions | undefined)?.suffix;
41
+ if (!suffix || typeof value !== "number" || typeof formatted !== "string") {
42
+ return formatted;
43
+ }
44
+ const unit = resolveI18nKey(getPluginI18n().translate, suffix);
45
+ return `${formatted}${UNIT_SEPARATOR}${unit}`;
46
+ };
47
+ }
@@ -7,6 +7,7 @@ import UIcon from "@nuxt/ui/runtime/vue/components/Icon.vue";
7
7
  import ULink from "@nuxt/ui/components/Link.vue";
8
8
  import { buildRelationBadges } from "./relationBadges";
9
9
  import { mapRelationBeforeState, sameRelationValue } from "./relationValue";
10
+ import { relationSelectSource } from "./relationRows";
10
11
  import {
11
12
  isSet,
12
13
  readRowField,
@@ -17,8 +18,15 @@ import {
17
18
  toneClass,
18
19
  } from "./cellHelpers";
19
20
  import { registerMetricCellTypes } from "./metricCells";
21
+ import {
22
+ dateFilterEditorValue,
23
+ numberFilterEditorValue,
24
+ } from "../table/utils/filterEditorValue";
25
+ import { formatDuration } from "#dms-core/app/utils/formatter";
26
+ import { withNumberSuffix } from "./numberSuffix";
20
27
  import { registerTwoLineCellType } from "./twoLineCell";
21
- import { useComposedText } from "../../../../../dms-core/app/composables/translation/useComposedText";
28
+ import { formatRecordLabel } from "../table-view/utils/formTexts";
29
+ import { getAppComposedText } from "../../../../../dms-core/app/composables/translation/useComposedText";
22
30
  import StatusPill, {
23
31
  type StatusPillVariant,
24
32
  } from "../../../components/status-pill/StatusPill.vue";
@@ -29,6 +37,8 @@ import {
29
37
  formatDayMonth,
30
38
  formatRelativeDate,
31
39
  } from "../../utils/relativeDate";
40
+ import { serverRenderNow } from "#dms-core/app/build/composables/renderNow";
41
+ import { getAppTranslation } from "../../../../../dms-core/app/composables/translation/useTranslation";
32
42
 
33
43
  const FilePreview = defineAsyncComponent(
34
44
  () => import("../../../components/table-view/FilePreview.vue"),
@@ -141,11 +151,57 @@ function createLinkRenderer(options: LinkRendererOptions) {
141
151
  };
142
152
  }
143
153
 
154
+ /** A `UrlType`'s options its read-only value reads. */
155
+ interface UrlOptions {
156
+ preview?: "avatar" | "image";
157
+ /**
158
+ * The label of the field shown, handed by the form's read-only display:
159
+ * the image's name when its row has none.
160
+ */
161
+ fieldLabel?: string;
162
+ }
163
+
164
+ const URL_IMAGE_CLASS =
165
+ "border-default h-16 w-auto max-w-full rounded-md border object-cover";
166
+
167
+ // The fields that name a row, read in order for its image's text.
168
+ const ROW_NAME_FIELDS = ["name", "label", "title"];
169
+
170
+ /**
171
+ * What an image stands for to a reader who cannot see it: the name of the
172
+ * row it belongs to (a team member's photo is that member), or else the
173
+ * field's label.
174
+ */
175
+ function imageAlt(row: Row, locale: string, options: UrlOptions): string {
176
+ for (const field of ROW_NAME_FIELDS) {
177
+ const name = formatRecordLabel(row?.[field], locale);
178
+ if (name) return name;
179
+ }
180
+ return options.fieldLabel ?? "";
181
+ }
182
+
183
+ /**
184
+ * A URL drawn as the image it points to, when its type asks for it
185
+ * (`preview`): a remote photo reads as a picture, not as its address.
186
+ */
187
+ function renderUrl(value: unknown, locale: string, options: unknown, row: Row) {
188
+ const opts = (options ?? {}) as UrlOptions;
189
+ if (!opts.preview || !isString(value) || !value) return undefined;
190
+ const alt = imageAlt(row, locale, opts);
191
+ if (opts.preview === "avatar") return h(UAvatar, { src: value, alt });
192
+ return h("img", {
193
+ src: value,
194
+ alt,
195
+ loading: "lazy",
196
+ class: URL_IMAGE_CLASS,
197
+ });
198
+ }
199
+
144
200
  const convertToBoolean = (value: unknown): boolean =>
145
201
  parse(String(value)) === true;
146
202
 
147
203
  const getBooleanLabel = (value: unknown): string => {
148
- const { processI18n } = useTranslation();
204
+ const { processI18n } = getAppTranslation();
149
205
  return convertToBoolean(value)
150
206
  ? processI18n("$dms.table.filter.boolean.checked")
151
207
  : processI18n("$dms.table.filter.boolean.unchecked");
@@ -160,7 +216,7 @@ function renderBoolean(value: unknown) {
160
216
  }
161
217
 
162
218
  function formatDateBetween(value: unknown, locale: string): string {
163
- const { t } = useI18n();
219
+ const { t } = getAppTranslation();
164
220
  const [rawStart, rawEnd] = Array.isArray(value)
165
221
  ? value
166
222
  : [
@@ -174,7 +230,7 @@ function formatDateBetween(value: unknown, locale: string): string {
174
230
 
175
231
  // v2 .status pill, tinted with the configured online/offline color.
176
232
  function renderStatus(value: unknown, options: unknown) {
177
- const { processI18n } = useTranslation();
233
+ const { processI18n } = getAppTranslation();
178
234
  const opts = options as StatusOptions | undefined;
179
235
  const isOnline = Boolean(value);
180
236
  const onlineLabel = opts?.onlineLabel || "$common.status.online";
@@ -194,7 +250,7 @@ function renderStatusPill(label: string, color: string) {
194
250
  }
195
251
 
196
252
  function renderSelectItem(item: SelectItem) {
197
- const { processI18n } = useTranslation();
253
+ const { processI18n } = getAppTranslation();
198
254
  const label = processI18n(item.label);
199
255
  if (!item.icon && !item.iconColor && !item.textColor) return label;
200
256
 
@@ -253,7 +309,7 @@ function renderSelect(value: unknown, options?: unknown) {
253
309
  function formatAddress(value: unknown): string {
254
310
  if (!value || typeof value !== "object") return " ";
255
311
 
256
- const { processI18n } = useTranslation();
312
+ const { processI18n } = getAppTranslation();
257
313
  const addr = value as Record<string, string>;
258
314
  const countryName = addr.countryCode
259
315
  ? processI18n(`$country.${addr.countryCode}`)
@@ -283,7 +339,7 @@ function renderRelationBadge(label: string, title?: string) {
283
339
  }
284
340
 
285
341
  function renderRelationBadges(values: unknown[], labelKey: string) {
286
- const { processI18n } = useTranslation();
342
+ const { processI18n } = getAppTranslation();
287
343
  const { visible, hidden } = buildRelationBadges(values, labelKey);
288
344
  const badges = visible.map((label) =>
289
345
  renderRelationBadge(processI18n(label)),
@@ -354,7 +410,7 @@ function renderImage(value: unknown, options: unknown) {
354
410
  }
355
411
 
356
412
  function renderFile(value: unknown, options: unknown) {
357
- const { t } = useI18n();
413
+ const { t } = getAppTranslation();
358
414
  const opts = options as FileOptions | undefined;
359
415
 
360
416
  if (opts?.multiple && Array.isArray(value)) {
@@ -408,8 +464,8 @@ const matchesBadge = (badge: IdentityBadgeOption, value: unknown): boolean =>
408
464
  * `emptyIcon` stand for a row without a value.
409
465
  */
410
466
  function renderIdentity(value: unknown, options: unknown, row: Row) {
411
- const { processI18n } = useTranslation();
412
- const { processText } = useComposedText();
467
+ const { processI18n } = getAppTranslation();
468
+ const { processText } = getAppComposedText();
413
469
  const opts = (options ?? {}) as IdentityOptions;
414
470
  const badges = (opts.badges ?? [])
415
471
  .filter((badge) => matchesBadge(badge, readRowField(row, badge.field)))
@@ -526,7 +582,7 @@ function renderPill(pill: PillItem) {
526
582
  * a relation could not resolve (a deleted row) is skipped.
527
583
  */
528
584
  function renderPills(value: unknown, options: unknown, row: Row) {
529
- const { processI18n } = useTranslation();
585
+ const { processI18n } = getAppTranslation();
530
586
  const opts = (options ?? {}) as PillsOptions;
531
587
  if (opts.exclusive && isSet(readRowField(row, opts.exclusive.field))) {
532
588
  return h("span", { class: [PILL_CLASS, PILL_FILLED_CLASS] }, [
@@ -605,11 +661,14 @@ function renderRelativeDate(
605
661
  options: unknown,
606
662
  row: Row,
607
663
  ) {
608
- const { processI18n } = useTranslation();
664
+ const { processI18n } = getAppTranslation();
609
665
  const opts = (options ?? {}) as RelativeDateOptions;
610
666
  const date = new Date(value as string);
611
667
  if (Number.isNaN(date.getTime())) return String(value);
612
- const deltaMs = date.getTime() - Date.now();
668
+ // Measured from the server render's instant while hydrating it: "3 min ago"
669
+ // written at 12:00:59 stays so in a browser hydrating at 12:01:01.
670
+ const now = serverRenderNow() ?? Date.now();
671
+ const deltaMs = date.getTime() - now;
613
672
  const isPast = deltaMs < 0;
614
673
  const title = formatDate(date, locale, RELATIVE_TITLE_FORMAT) ?? undefined;
615
674
 
@@ -624,8 +683,8 @@ function renderRelativeDate(
624
683
  const style = (isPast ? opts.pastStyle : undefined) ?? opts.style;
625
684
  let text =
626
685
  style === "day"
627
- ? formatDayMonth(date, locale)
628
- : formatRelativeDate(date, locale);
686
+ ? formatDayMonth(date, locale, now)
687
+ : formatRelativeDate(date, locale, now);
629
688
  const isSoon = !isPast && !!opts.soonWithinMs && deltaMs < opts.soonWithinMs;
630
689
  const tone = isPast
631
690
  ? (opts.pastTone ?? opts.tone)
@@ -644,7 +703,7 @@ function renderRelativeDate(
644
703
  }
645
704
 
646
705
  function renderMissingDate(options: unknown) {
647
- const { processI18n } = useTranslation();
706
+ const { processI18n } = getAppTranslation();
648
707
  const opts = (options ?? {}) as RelativeDateOptions;
649
708
  if (!opts.emptyLabel) return "-";
650
709
  return h(
@@ -670,7 +729,7 @@ interface IndicatorOptions {
670
729
  * on for a set flag or a non-empty list.
671
730
  */
672
731
  function renderIndicator(value: unknown, options: unknown) {
673
- const { processI18n } = useTranslation();
732
+ const { processI18n } = getAppTranslation();
674
733
  const opts = (options ?? {}) as IndicatorOptions;
675
734
  const isOn = isSet(value);
676
735
  const icon = isOn ? opts.onIcon : opts.offIcon;
@@ -768,7 +827,8 @@ const registerPrimitiveTypes = (
768
827
  ) => {
769
828
  registerDataType({
770
829
  id: "number",
771
- formatter: { default: formatNumber as DataTypeFormatter },
830
+ formatter: { default: withNumberSuffix(formatNumber) },
831
+ filterEditorValue: numberFilterEditorValue,
772
832
  });
773
833
  registerDataType({
774
834
  id: "string",
@@ -776,15 +836,24 @@ const registerPrimitiveTypes = (
776
836
  });
777
837
  registerDataType({
778
838
  id: "price",
779
- formatter: { default: formatPrice as DataTypeFormatter },
839
+ formatter: { default: withNumberSuffix(formatPrice) },
840
+ filterEditorValue: numberFilterEditorValue,
780
841
  });
781
842
  registerDataType({
782
843
  id: "percentage",
783
- formatter: { default: formatPercentage as DataTypeFormatter },
844
+ formatter: { default: withNumberSuffix(formatPercentage) },
845
+ filterEditorValue: numberFilterEditorValue,
784
846
  });
785
847
  registerDataType({
786
848
  id: "string_time",
787
- formatter: { default: formatTimeSpan },
849
+ // Written as the field writes it ("7 h 30 min"), in the reader's locale,
850
+ // rounded as every duration cell is: the field keeps the exact value.
851
+ formatter: {
852
+ default: (value: unknown, locale: string) =>
853
+ isNumber(value)
854
+ ? formatDuration(Number(value), locale, { precision: "rounded" })
855
+ : value,
856
+ },
788
857
  });
789
858
  };
790
859
 
@@ -801,17 +870,18 @@ const registerDateType = (registerDataType: (dataType: DataType) => void) => {
801
870
  id: "date",
802
871
  formatter: {
803
872
  default: (value: unknown, locale: string) => {
804
- const { t } = useI18n();
873
+ const { t } = getAppTranslation();
805
874
  return formatDate(value, locale) ?? t("dms.date.undefined");
806
875
  },
807
876
  cell: (value: unknown, locale: string) => {
808
- const { t } = useI18n();
877
+ const { t } = getAppTranslation();
809
878
  return (
810
879
  formatDate(value, locale, CELL_DATE_FORMAT) ?? t("dms.date.undefined")
811
880
  );
812
881
  },
813
882
  is_between: formatDateBetween,
814
883
  },
884
+ filterEditorValue: dateFilterEditorValue,
815
885
  });
816
886
  };
817
887
 
@@ -875,9 +945,13 @@ const registerLinkTypes = (registerDataType: (dataType: DataType) => void) => {
875
945
  id: "email",
876
946
  formatter: { default: createLinkRenderer({ hrefPrefix: "mailto:" }) },
877
947
  });
948
+ const renderLink = createLinkRenderer({ target: "_blank" });
878
949
  registerDataType({
879
950
  id: "url",
880
- formatter: { default: createLinkRenderer({ target: "_blank" }) },
951
+ formatter: {
952
+ default: (value: unknown, locale: string, options: unknown, row?: Row) =>
953
+ renderUrl(value, locale, options, row) ?? renderLink(value, locale),
954
+ },
881
955
  });
882
956
  };
883
957
 
@@ -892,6 +966,7 @@ const registerRelationType = (
892
966
  },
893
967
  beforeStateMapper: mapRelationBeforeState,
894
968
  isSameValue: sameRelationValue,
969
+ selectSource: relationSelectSource,
895
970
  });
896
971
  };
897
972
 
@@ -1,5 +1,6 @@
1
1
  import { h } from "vue";
2
2
  import type { DataType } from "#dms-core/app/composables/data-types/useDataType";
3
+ import { getAppTranslation } from "../../../../../dms-core/app/composables/translation/useTranslation";
3
4
 
4
5
  // How the field types made of several values read in a table cell or a
5
6
  // read-only form: a count, a masked secret, the first line of the code,
@@ -19,7 +20,7 @@ const countOf = (value: unknown): number => {
19
20
 
20
21
  function renderCount(key: string) {
21
22
  return (value: unknown) => {
22
- const { t } = useI18n();
23
+ const { t } = getAppTranslation();
23
24
  const count = countOf(value);
24
25
  return t(key, { count }, count);
25
26
  };
@@ -0,0 +1,75 @@
1
+ import { isEmptyFormValue, sameFormValue } from "../unsaved-changes/formValue";
2
+
3
+ /**
4
+ * A relation picker narrowed by another field of its form: the related
5
+ * table's `field` must equal the form's value of `from`.
6
+ */
7
+ export interface RelationFilterBy {
8
+ field: string;
9
+ from: string;
10
+ }
11
+
12
+ /** The compare mode the search route matches the related `field` with. */
13
+ const FILTER_MODE = "is";
14
+
15
+ type FilterValue = string | number | boolean;
16
+
17
+ function isFilterValue(value: unknown): value is FilterValue {
18
+ if (typeof value === "string") return value !== "";
19
+ return typeof value === "number" || typeof value === "boolean";
20
+ }
21
+
22
+ /**
23
+ * The query narrowing the picker's search: the related table's filter
24
+ * syntax (`filter_<field>=is:<value>`), the one its funnel sends. Nothing
25
+ * while the source field holds no single value: every row is offered.
26
+ */
27
+ export function relationFilterParams(
28
+ filterBy: RelationFilterBy | undefined,
29
+ sourceValue: unknown,
30
+ ): Record<string, string> {
31
+ if (!filterBy || !isFilterValue(sourceValue)) return {};
32
+ return { [`filter_${filterBy.field}`]: `${FILTER_MODE}:${sourceValue}` };
33
+ }
34
+
35
+ /** The source field and the pick, before and after a change of either. */
36
+ export interface RelationPickChange {
37
+ source: unknown;
38
+ pick: unknown;
39
+ previousSource: unknown;
40
+ previousPick: unknown;
41
+ /** The source value the current pick was made (or loaded) with. */
42
+ pickSource: unknown;
43
+ }
44
+
45
+ /** Whether to clear the pick, and the source value the pick now goes with. */
46
+ export interface RelationPickOutcome {
47
+ shouldClear: boolean;
48
+ pickSource: unknown;
49
+ }
50
+
51
+ /** Whether a picker holds a value. */
52
+ function hasRelationPick(value: unknown): boolean {
53
+ return !isEmptyFormValue(value);
54
+ }
55
+
56
+ /**
57
+ * Whether a change of the source field voids the pick: made for another of
58
+ * its values. A pick changing at the same time (a record loading both
59
+ * values) or after goes with the current source; going back to the value the
60
+ * pick was made with keeps it.
61
+ */
62
+ export function resolveRelationPick(
63
+ change: RelationPickChange,
64
+ ): RelationPickOutcome {
65
+ const isPickChange = !sameFormValue(change.pick, change.previousPick);
66
+ if (isPickChange || !hasRelationPick(change.pick)) {
67
+ return { shouldClear: false, pickSource: change.source };
68
+ }
69
+ const isSourceChange = !sameFormValue(change.source, change.previousSource);
70
+ const isPickSource = sameFormValue(change.source, change.pickSource);
71
+ return {
72
+ shouldClear: isSourceChange && !isPickSource,
73
+ pickSource: change.pickSource,
74
+ };
75
+ }
@@ -0,0 +1,71 @@
1
+ import type { DataTypeSelectSource } from "#dms-core/app/composables/data-types/useDataType";
2
+ import { pageRequestUrl } from "../page/pageUrlValues";
3
+ import { relationIdsQuery } from "./relationValue";
4
+
5
+ /** The keys a `select` route's row carries its label and value under by default. */
6
+ export const RELATION_LABEL_KEY = "label";
7
+ export const RELATION_VALUE_KEY = "value";
8
+
9
+ type RelationRow = Record<string, unknown>;
10
+
11
+ interface RelationSelectResponse {
12
+ results?: RelationRow[];
13
+ }
14
+
15
+ interface RelationSourceOptions {
16
+ searchUrl?: unknown;
17
+ keyMapping?: DataTypeSelectSource["keyMapping"];
18
+ }
19
+
20
+ /**
21
+ * The `select` route a relation's control reads its rows from, and their key
22
+ * mapping: the relation data type's {@link DataTypeSelectSource}.
23
+ */
24
+ export function relationSelectSource(
25
+ options: unknown,
26
+ ): DataTypeSelectSource | undefined {
27
+ const opts = options as RelationSourceOptions | undefined;
28
+ if (typeof opts?.searchUrl !== "string") return undefined;
29
+ return { searchUrl: opts.searchUrl, keyMapping: opts.keyMapping };
30
+ }
31
+
32
+ /** The value a row of the source is picked by. */
33
+ export function relationRowValue(
34
+ source: DataTypeSelectSource,
35
+ row: RelationRow,
36
+ ): unknown {
37
+ return row[source.keyMapping?.value || RELATION_VALUE_KEY];
38
+ }
39
+
40
+ /** The label a row of the source is named by. */
41
+ export function relationRowLabel(
42
+ source: DataTypeSelectSource,
43
+ row: RelationRow,
44
+ ): unknown {
45
+ return row[source.keyMapping?.label || RELATION_LABEL_KEY];
46
+ }
47
+
48
+ /**
49
+ * Reads the rows of a relation's ids from its `select` route, narrowed with
50
+ * `?ids=` to those rows alone (matched on the mapped value key), whatever the
51
+ * route's page size. The one way the picker, the filter chips and the
52
+ * read-only lists name the ids they hold. A failed read answers no rows: the
53
+ * ids then show as they are.
54
+ */
55
+ export function useRelationRows() {
56
+ const { $authFetch } = useAuthFetch();
57
+
58
+ async function readRelationRows(
59
+ source: DataTypeSelectSource,
60
+ ids: string[],
61
+ ): Promise<RelationRow[]> {
62
+ const url = pageRequestUrl(source.searchUrl);
63
+ if (!url || ids.length === 0) return [];
64
+ const response = await $authFetch<RelationSelectResponse>(url, {
65
+ params: relationIdsQuery(ids, source.keyMapping),
66
+ }).catch(() => undefined);
67
+ return response?.results ?? [];
68
+ }
69
+
70
+ return { readRelationRows };
71
+ }
@@ -64,3 +64,42 @@ export function sameRelationValue(
64
64
  };
65
65
  return sameFormValue(comparable(left), comparable(right));
66
66
  }
67
+
68
+ /**
69
+ * What a relation field asks its target's `select` route for to label the
70
+ * ids it holds: those rows alone, matched on the field's value key (the
71
+ * route falls back to the row id when none is mapped).
72
+ */
73
+ export interface RelationIdsQuery {
74
+ ids: string[];
75
+ idKey?: string;
76
+ limit: number;
77
+ }
78
+
79
+ const isIdValue = (value: unknown): value is string | number =>
80
+ (typeof value === "string" && value !== "") || typeof value === "number";
81
+
82
+ /**
83
+ * The ids a relation value holds that no known row labels yet — a form whose
84
+ * read answers the id (`"u-5"`) rather than the joined row. A joined row
85
+ * labels itself, so only plain ids are returned, each once.
86
+ */
87
+ export function unlabeledRelationIds(
88
+ value: unknown,
89
+ labeledIds: ReadonlySet<string>,
90
+ ): string[] {
91
+ const ids = [value].flat().filter(isIdValue).map(String);
92
+ return [...new Set(ids)].filter((id) => !labeledIds.has(id));
93
+ }
94
+
95
+ /** The `select` query answering the rows of `ids`. */
96
+ export function relationIdsQuery(
97
+ ids: string[],
98
+ keyMapping?: RelationKeyMapping,
99
+ ): RelationIdsQuery {
100
+ return {
101
+ ids,
102
+ ...(keyMapping?.value ? { idKey: keyMapping.value } : {}),
103
+ limit: ids.length,
104
+ };
105
+ }
@@ -1,6 +1,6 @@
1
1
  import { h, type VNodeChild } from "vue";
2
2
  import type { DataType } from "#dms-core/app/composables/data-types/useDataType";
3
- import { useComposedText } from "../../../../../dms-core/app/composables/translation/useComposedText";
3
+ import { getAppComposedText } from "../../../../../dms-core/app/composables/translation/useComposedText";
4
4
  import {
5
5
  type ComposableText,
6
6
  isComposedText,
@@ -53,7 +53,7 @@ function readPrimary(
53
53
  opts: TwoLineOptions,
54
54
  row: Row,
55
55
  ): VNodeChild | undefined {
56
- const { processText } = useComposedText();
56
+ const { processText } = getAppComposedText();
57
57
  if (opts.primaryField) {
58
58
  const text = processText(
59
59
  readRowField(row, opts.primaryField) as ComposableText,
@@ -76,7 +76,7 @@ function renderTwoLine(
76
76
  options: unknown,
77
77
  row: Row,
78
78
  ) {
79
- const { processText } = useComposedText();
79
+ const { processText } = getAppComposedText();
80
80
  const opts = (options ?? {}) as TwoLineOptions;
81
81
  const primary = readPrimary(value, locale, opts, row);
82
82
  const sub = readSubline(readRowField(row, opts.subField), {