@antelopejs/dms 0.9.8 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (451) hide show
  1. package/CHANGELOG.md +171 -0
  2. package/README.md +1 -1
  3. package/data/module-store/catalog.json +2 -2
  4. package/dist/attachments/table-view.js +2 -2
  5. package/dist/attachments/table-view.js.map +1 -1
  6. package/dist/automation/actions.d.ts +2 -0
  7. package/dist/automation/actions.js +116 -71
  8. package/dist/automation/actions.js.map +1 -1
  9. package/dist/automation/i18n.d.ts +4 -0
  10. package/dist/automation/i18n.js +19 -0
  11. package/dist/automation/i18n.js.map +1 -0
  12. package/dist/automation/send-notification.d.ts +21 -0
  13. package/dist/automation/send-notification.js +163 -0
  14. package/dist/automation/send-notification.js.map +1 -0
  15. package/dist/automation/triggers.js +19 -25
  16. package/dist/automation/triggers.js.map +1 -1
  17. package/dist/client-origin-cors.d.ts +1 -0
  18. package/dist/client-origin-cors.js +19 -0
  19. package/dist/client-origin-cors.js.map +1 -0
  20. package/dist/config.d.ts +3 -2
  21. package/dist/config.js +5 -7
  22. package/dist/config.js.map +1 -1
  23. package/dist/frontend-module.d.ts +4 -0
  24. package/dist/frontend-module.js +37 -0
  25. package/dist/frontend-module.js.map +1 -0
  26. package/dist/implementations/dms/client-ip.js +1 -1
  27. package/dist/implementations/dms/client-ip.js.map +1 -1
  28. package/dist/implementations/dms/dev-reload.d.ts +5 -0
  29. package/dist/implementations/dms/dev-reload.js +23 -3
  30. package/dist/implementations/dms/dev-reload.js.map +1 -1
  31. package/dist/implementations/dms/frontend-modules.d.ts +2 -4
  32. package/dist/implementations/dms/frontend-modules.js +38 -94
  33. package/dist/implementations/dms/frontend-modules.js.map +1 -1
  34. package/dist/implementations/dms/frontend-ready.d.ts +12 -2
  35. package/dist/implementations/dms/frontend-ready.js +34 -4
  36. package/dist/implementations/dms/frontend-ready.js.map +1 -1
  37. package/dist/implementations/dms/layout-banners.d.ts +2 -1
  38. package/dist/implementations/dms/layout-banners.js +9 -0
  39. package/dist/implementations/dms/layout-banners.js.map +1 -1
  40. package/dist/implementations/dms/manifest.d.ts +7 -2
  41. package/dist/implementations/dms/manifest.js +12 -7
  42. package/dist/implementations/dms/manifest.js.map +1 -1
  43. package/dist/implementations/dms/page.d.ts +4 -3
  44. package/dist/implementations/dms/page.js +49 -18
  45. package/dist/implementations/dms/page.js.map +1 -1
  46. package/dist/implementations/dms/permissions-resolver.d.ts +2 -2
  47. package/dist/implementations/dms/permissions-resolver.js +2 -2
  48. package/dist/implementations/dms/permissions-resolver.js.map +1 -1
  49. package/dist/implementations/dms/realtime.js +1 -0
  50. package/dist/implementations/dms/realtime.js.map +1 -1
  51. package/dist/implementations/dms-base/bulk-selection.d.ts +1 -1
  52. package/dist/implementations/dms-base/bulk-selection.js +23 -1
  53. package/dist/implementations/dms-base/bulk-selection.js.map +1 -1
  54. package/dist/implementations/dms-base/filter-tokens.js +5 -3
  55. package/dist/implementations/dms-base/filter-tokens.js.map +1 -1
  56. package/dist/implementations/dms-base/search-route.d.ts +1 -0
  57. package/dist/implementations/dms-base/search-route.js +35 -13
  58. package/dist/implementations/dms-base/search-route.js.map +1 -1
  59. package/dist/implementations/dms-base/table-view.js +11 -6
  60. package/dist/implementations/dms-base/table-view.js.map +1 -1
  61. package/dist/implementations/dms-notifications/index.d.ts +2 -2
  62. package/dist/implementations/dms-notifications/index.js +5 -5
  63. package/dist/implementations/dms-notifications/index.js.map +1 -1
  64. package/dist/index.js +32 -55
  65. package/dist/index.js.map +1 -1
  66. package/dist/pages/modules/index.d.ts +1 -0
  67. package/dist/pages/modules/index.js +1 -0
  68. package/dist/pages/modules/index.js.map +1 -1
  69. package/dist/pages/modules/modules-store.d.ts +9 -0
  70. package/dist/pages/modules/modules-store.js +30 -0
  71. package/dist/pages/modules/modules-store.js.map +1 -0
  72. package/dist/pages/settings/users/account-credentials.js +2 -2
  73. package/dist/pages/settings/users/account-credentials.js.map +1 -1
  74. package/dist/pages/settings/users/account-data-store.js +25 -8
  75. package/dist/pages/settings/users/account-data-store.js.map +1 -1
  76. package/dist/pages/settings/users/invite-extension-routes.js +9 -3
  77. package/dist/pages/settings/users/invite-extension-routes.js.map +1 -1
  78. package/dist/pages/settings/users/member-management.js +13 -14
  79. package/dist/pages/settings/users/member-management.js.map +1 -1
  80. package/dist/pages/settings/users/profile.d.ts +1 -4
  81. package/dist/pages/settings/users/profile.js +0 -35
  82. package/dist/pages/settings/users/profile.js.map +1 -1
  83. package/dist/pages/settings/users/role-editor-store.js +26 -18
  84. package/dist/pages/settings/users/role-editor-store.js.map +1 -1
  85. package/dist/pages/settings/users/role-editor.d.ts +0 -1
  86. package/dist/pages/settings/users/role-editor.js +1 -2
  87. package/dist/pages/settings/users/role-editor.js.map +1 -1
  88. package/dist/pages/settings/users/role-preview.js +1 -2
  89. package/dist/pages/settings/users/role-preview.js.map +1 -1
  90. package/dist/pages/settings/users/roles.js +2 -2
  91. package/dist/pages/settings/users/roles.js.map +1 -1
  92. package/dist/pages/settings/users/two-factor-operations.js.map +1 -1
  93. package/dist/realtime/broker.d.ts +6 -0
  94. package/dist/realtime/broker.js +8 -1
  95. package/dist/realtime/broker.js.map +1 -1
  96. package/dist/realtime/presence.d.ts +8 -3
  97. package/dist/realtime/presence.js +8 -4
  98. package/dist/realtime/presence.js.map +1 -1
  99. package/dist/realtime/sessions.d.ts +11 -1
  100. package/dist/realtime/sessions.js +21 -3
  101. package/dist/realtime/sessions.js.map +1 -1
  102. package/dist/realtime/table-view.d.ts +2 -2
  103. package/dist/realtime/table-view.js +4 -3
  104. package/dist/realtime/table-view.js.map +1 -1
  105. package/dist/routes/auth/client-ip.d.ts +1 -0
  106. package/dist/routes/auth/client-ip.js +18 -0
  107. package/dist/routes/auth/client-ip.js.map +1 -1
  108. package/dist/routes/auth/oauth/config.d.ts +2 -1
  109. package/dist/routes/auth/oauth/config.js +5 -1
  110. package/dist/routes/auth/oauth/config.js.map +1 -1
  111. package/dist/routes/auth/session-handoff.d.ts +0 -1
  112. package/dist/routes/auth/session-handoff.js +0 -2
  113. package/dist/routes/auth/session-handoff.js.map +1 -1
  114. package/dist/routes/auth.js +3 -2
  115. package/dist/routes/auth.js.map +1 -1
  116. package/dist/routes/index.d.ts +0 -1
  117. package/dist/routes/index.js +0 -1
  118. package/dist/routes/index.js.map +1 -1
  119. package/dist/routes/realtime.d.ts +13 -1
  120. package/dist/routes/realtime.js +114 -29
  121. package/dist/routes/realtime.js.map +1 -1
  122. package/dist/startup-config.d.ts +2 -0
  123. package/dist/startup-config.js +62 -0
  124. package/dist/startup-config.js.map +1 -0
  125. package/dist/store/interfaces.d.ts +3 -2
  126. package/dist/store/interfaces.js +16 -6
  127. package/dist/store/interfaces.js.map +1 -1
  128. package/dist/store/listing.js +25 -15
  129. package/dist/store/listing.js.map +1 -1
  130. package/dist/store/operations.js +5 -2
  131. package/dist/store/operations.js.map +1 -1
  132. package/dist/store/requirements.js +3 -2
  133. package/dist/store/requirements.js.map +1 -1
  134. package/dist/store/runtime.d.ts +10 -1
  135. package/dist/store/runtime.js +33 -6
  136. package/dist/store/runtime.js.map +1 -1
  137. package/dist/store/types.d.ts +10 -0
  138. package/dist/utils/ensure-default-tenant.d.ts +2 -1
  139. package/dist/utils/ensure-default-tenant.js +13 -1
  140. package/dist/utils/ensure-default-tenant.js.map +1 -1
  141. package/dist/utils/export-jobs.js +2 -1
  142. package/dist/utils/export-jobs.js.map +1 -1
  143. package/dist/validation/auth.schema.d.ts +2 -2
  144. package/dist/validation/member-invite.schema.d.ts +2 -10
  145. package/dist/validation/member-invite.schema.js +2 -15
  146. package/dist/validation/member-invite.schema.js.map +1 -1
  147. package/frontend-vue/dms.frontend.ts +24 -5
  148. package/frontend-vue/dms.server.ts +17 -0
  149. package/frontend-vue/layers/dms-auth/server/accounts.ts +3 -4
  150. package/frontend-vue/layers/dms-core/app/build/composables/renderNow.ts +33 -0
  151. package/frontend-vue/layers/dms-core/app/build/composables/user/tablePreferences.ts +5 -1
  152. package/frontend-vue/layers/dms-core/app/build/utils/crossTab.ts +199 -0
  153. package/frontend-vue/layers/dms-core/app/build/utils/sharedRealtimeConnection.ts +410 -0
  154. package/frontend-vue/layers/dms-core/app/composables/auth/useApiData.ts +77 -12
  155. package/frontend-vue/layers/dms-core/app/composables/data-types/useDataType.ts +26 -0
  156. package/frontend-vue/layers/dms-core/app/composables/live/livePauseSource.ts +29 -0
  157. package/frontend-vue/layers/dms-core/app/composables/period/compareResolvers.ts +150 -9
  158. package/frontend-vue/layers/dms-core/app/composables/period/usePeriod.ts +6 -4
  159. package/frontend-vue/layers/dms-core/app/composables/period/usePeriodScope.ts +27 -1
  160. package/frontend-vue/layers/dms-core/app/composables/realtime/useUserRealtime.ts +256 -119
  161. package/frontend-vue/layers/dms-core/app/composables/translation/useComposedText.ts +21 -8
  162. package/frontend-vue/layers/dms-core/app/composables/translation/useTranslation.ts +25 -5
  163. package/frontend-vue/layers/dms-core/app/composables/useApiError.ts +3 -5
  164. package/frontend-vue/layers/dms-core/app/composables/useFieldErrors.ts +61 -26
  165. package/frontend-vue/layers/dms-core/app/plugins/dev-error-log.ts +16 -0
  166. package/frontend-vue/layers/dms-core/app/types/user.ts +6 -0
  167. package/frontend-vue/layers/dms-core/app/utils/composedText.ts +7 -2
  168. package/frontend-vue/layers/dms-core/app/utils/formatter.ts +134 -35
  169. package/frontend-vue/layers/dms-core/app/utils/url-interpolation.ts +18 -1
  170. package/frontend-vue/layers/dms-core/i18n/locales/core-en-GB.json +136 -2
  171. package/frontend-vue/layers/dms-core/i18n/locales/core-fr-FR.json +136 -2
  172. package/frontend-vue/layers/dms-core/shared/types/app-config.ts +0 -2
  173. package/frontend-vue/layers/dms-layout/app/app.config.ts +0 -1
  174. package/frontend-vue/layers/dms-layout/app/assets/css/main.css +11 -1
  175. package/frontend-vue/layers/dms-layout/app/assets/fonts/Geist-Italic-Variable.woff2 +0 -0
  176. package/frontend-vue/layers/dms-layout/app/assets/fonts/Geist-Variable.woff2 +0 -0
  177. package/frontend-vue/layers/dms-layout/app/assets/fonts/GeistMono-Italic-Variable.woff2 +0 -0
  178. package/frontend-vue/layers/dms-layout/app/assets/fonts/GeistMono-Variable.woff2 +0 -0
  179. package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardBanners.vue +3 -2
  180. package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardHeader.vue +41 -21
  181. package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardPageBody.vue +6 -2
  182. package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardSidebar.vue +4 -19
  183. package/frontend-vue/layers/dms-layout/app/build/components/layout/Footer.vue +11 -107
  184. package/frontend-vue/layers/dms-layout/app/build/components/layout/GithubStarPrompt.vue +18 -7
  185. package/frontend-vue/layers/dms-layout/app/build/components/layout/PageHeaderActionsOutlet.vue +4 -0
  186. package/frontend-vue/layers/dms-layout/app/build/components/layout/RowSidePanel.vue +43 -82
  187. package/frontend-vue/layers/dms-layout/app/build/components/layout/SidebarUserMenu.vue +44 -3
  188. package/frontend-vue/layers/dms-layout/app/build/components/layout/UserIdentity.vue +11 -3
  189. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleCatalogTile.vue +132 -35
  190. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleManageMenu.vue +59 -0
  191. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleSourceLabel.vue +25 -0
  192. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleStoreBanners.vue +73 -0
  193. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleStoreOperation.vue +116 -0
  194. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleStoreRequirements.vue +8 -3
  195. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleStoreTile.vue +85 -207
  196. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleTile.vue +10 -3
  197. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleTileSkeleton.vue +68 -0
  198. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/notification/NotificationInboxDisplay.vue +24 -8
  199. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/notification/NotificationInboxItem.vue +8 -5
  200. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/region/RegionTimeZoneInput.vue +1 -0
  201. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/roles/role-permissions.ts +19 -0
  202. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/roles/useRoleEditor.ts +2 -1
  203. package/frontend-vue/layers/dms-layout/app/build/composables/layout/useSidePanelWidth.ts +31 -70
  204. package/frontend-vue/layers/dms-layout/app/build/composables/notification/useNotificationGroupsOpen.ts +11 -1
  205. package/frontend-vue/layers/dms-layout/app/build/utils/dev-reload-stream.ts +81 -0
  206. package/frontend-vue/layers/dms-layout/app/build/utils/github-repository.ts +2 -2
  207. package/frontend-vue/layers/dms-layout/app/build/utils/module-store-catalog.ts +232 -36
  208. package/frontend-vue/layers/dms-layout/app/build/utils/modules-catalog.ts +122 -14
  209. package/frontend-vue/layers/dms-layout/app/composables/layout/useLayoutBanners.ts +32 -3
  210. package/frontend-vue/layers/dms-layout/app/composables/layout/useServedHeaderActions.ts +49 -11
  211. package/frontend-vue/layers/dms-layout/app/composables/page/useModuleHistory.ts +3 -2
  212. package/frontend-vue/layers/dms-layout/app/composables/page/useModuleStore.ts +59 -4
  213. package/frontend-vue/layers/dms-layout/app/composables/page/useModuleStoreActions.ts +194 -0
  214. package/frontend-vue/layers/dms-layout/app/composables/page/usePageComponentPreload.ts +23 -3
  215. package/frontend-vue/layers/dms-layout/app/composables/page/usePageLayout.ts +4 -2
  216. package/frontend-vue/layers/dms-layout/app/composables/page/usePageRecordDeletion.ts +162 -0
  217. package/frontend-vue/layers/dms-layout/app/composables/page/useSiteLayout.ts +6 -1
  218. package/frontend-vue/layers/dms-layout/app/custom-layouts/DefaultLayout.vue +6 -2
  219. package/frontend-vue/layers/dms-layout/app/custom-layouts/SettingsLayout.vue +6 -2
  220. package/frontend-vue/layers/dms-layout/app/custom-pages/modules-store.vue +291 -0
  221. package/frontend-vue/layers/dms-layout/app/custom-pages/modules.vue +87 -474
  222. package/frontend-vue/layers/dms-layout/app/middleware/module-routing.global.ts +3 -0
  223. package/frontend-vue/layers/dms-layout/app/pages/[...slug].vue +13 -22
  224. package/frontend-vue/layers/dms-layout/app/plugins/command-palette-navigation.client.ts +11 -10
  225. package/frontend-vue/layers/dms-layout/app/plugins/command-palette-personal-pages.client.ts +22 -52
  226. package/frontend-vue/layers/dms-layout/app/plugins/command-palette-quick-actions.client.ts +7 -7
  227. package/frontend-vue/layers/dms-layout/app/plugins/command-palette-session.client.ts +11 -10
  228. package/frontend-vue/layers/dms-layout/app/plugins/dms-dev-reload.client.ts +14 -9
  229. package/frontend-vue/layers/dms-layout/app/theme/actions.ts +5 -4
  230. package/frontend-vue/layers/dms-layout/app/theme/form-controls.ts +9 -4
  231. package/frontend-vue/layers/dms-layout/app/theme/surfaces.ts +22 -6
  232. package/frontend-vue/layers/dms-layout/app/types/module-store.ts +9 -0
  233. package/frontend-vue/layers/dms-layout/app/types/page.ts +15 -1
  234. package/frontend-vue/layers/dms-layout/app/utils/account-menu.ts +11 -0
  235. package/frontend-vue/layers/dms-layout/app/utils/breadcrumb.ts +57 -0
  236. package/frontend-vue/layers/dms-layout/app/utils/pageQuery.ts +26 -0
  237. package/frontend-vue/layers/dms-layout/i18n/locales/layout-en-GB.json +45 -18
  238. package/frontend-vue/layers/dms-layout/i18n/locales/layout-fr-FR.json +44 -17
  239. package/frontend-vue/layers/dms-ui/app/build/components/containers/TargetComponentTree.vue +31 -0
  240. package/frontend-vue/layers/dms-ui/app/build/components/containers/constants.ts +25 -0
  241. package/frontend-vue/layers/dms-ui/app/build/components/containers/drawer/DynamicDrawer.vue +244 -15
  242. package/frontend-vue/layers/dms-ui/app/build/components/containers/modal/DynamicModal.vue +42 -25
  243. package/frontend-vue/layers/dms-ui/app/build/components/containers/row/RowHeader.vue +153 -0
  244. package/frontend-vue/layers/dms-ui/app/build/components/containers/row/RowNotFound.vue +44 -0
  245. package/frontend-vue/layers/dms-ui/app/build/components/containers/row/drawerRowHeader.ts +35 -0
  246. package/frontend-vue/layers/dms-ui/app/build/components/form/FormEntries.vue +44 -9
  247. package/frontend-vue/layers/dms-ui/app/build/components/form/FormErrorSummary.vue +11 -4
  248. package/frontend-vue/layers/dms-ui/app/build/components/form/FormFieldControl.vue +38 -5
  249. package/frontend-vue/layers/dms-ui/app/build/components/form/FormFieldNotes.vue +29 -5
  250. package/frontend-vue/layers/dms-ui/app/build/components/form/FormLoadError.vue +5 -1
  251. package/frontend-vue/layers/dms-ui/app/build/components/form/FormSectionNav.vue +3 -1
  252. package/frontend-vue/layers/dms-ui/app/build/components/form/FormSections.vue +106 -27
  253. package/frontend-vue/layers/dms-ui/app/build/components/table/Actions.vue +12 -4
  254. package/frontend-vue/layers/dms-ui/app/build/components/table/Empty.vue +4 -2
  255. package/frontend-vue/layers/dms-ui/app/build/components/table/FiltersRow.vue +48 -1
  256. package/frontend-vue/layers/dms-ui/app/build/components/table/Pagination.vue +55 -54
  257. package/frontend-vue/layers/dms-ui/app/build/components/table/RowSelection.vue +2 -1
  258. package/frontend-vue/layers/dms-ui/app/build/components/table/Table.vue +396 -288
  259. package/frontend-vue/layers/dms-ui/app/build/components/table/Tabs.vue +2 -1
  260. package/frontend-vue/layers/dms-ui/app/build/components/table/Views.vue +2 -1
  261. package/frontend-vue/layers/dms-ui/app/build/components/table-view/CardAvatar.vue +59 -0
  262. package/frontend-vue/layers/dms-ui/app/build/components/table-view/CardGridSkeleton.vue +19 -2
  263. package/frontend-vue/layers/dms-ui/app/build/components/table-view/CardRowActions.vue +38 -0
  264. package/frontend-vue/layers/dms-ui/app/build/components/table-view/CardsDisplay.vue +148 -104
  265. package/frontend-vue/layers/dms-ui/app/build/components/table-view/LiveHoldBanner.vue +17 -2
  266. package/frontend-vue/layers/dms-ui/app/build/composables/actions/bulkSelection.ts +43 -9
  267. package/frontend-vue/layers/dms-ui/app/build/composables/actions/rowComponentOptions.ts +76 -0
  268. package/frontend-vue/layers/dms-ui/app/build/composables/actions/rowNavigation.ts +172 -27
  269. package/frontend-vue/layers/dms-ui/app/build/composables/actions/useActionTargets.ts +270 -46
  270. package/frontend-vue/layers/dms-ui/app/build/composables/confirm/actionError.ts +6 -1
  271. package/frontend-vue/layers/dms-ui/app/build/composables/confirm/confirmDialogTexts.ts +2 -0
  272. package/frontend-vue/layers/dms-ui/app/build/composables/containers/closingLayers.ts +186 -0
  273. package/frontend-vue/layers/dms-ui/app/build/composables/containers/context.ts +14 -0
  274. package/frontend-vue/layers/dms-ui/app/build/composables/containers/drawerSize.ts +229 -0
  275. package/frontend-vue/layers/dms-ui/app/build/composables/containers/useResizableSize.ts +175 -0
  276. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/metricCells.ts +18 -35
  277. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/numberSuffix.ts +44 -0
  278. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/registerDefaults.ts +123 -27
  279. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/registerFieldTypes.ts +2 -1
  280. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/relationFilter.ts +75 -0
  281. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/relationRows.ts +92 -0
  282. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/relationValue.ts +39 -0
  283. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/twoLineCell.ts +3 -3
  284. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/useColumnValueRenderer.ts +6 -2
  285. package/frontend-vue/layers/dms-ui/app/build/composables/form/createFormFields.ts +15 -0
  286. package/frontend-vue/layers/dms-ui/app/build/composables/form/fieldSaved.ts +40 -0
  287. package/frontend-vue/layers/dms-ui/app/build/composables/form/fieldValueRule.ts +62 -0
  288. package/frontend-vue/layers/dms-ui/app/build/composables/form/formEntryContext.ts +5 -0
  289. package/frontend-vue/layers/dms-ui/app/build/composables/form/formFooter.ts +42 -7
  290. package/frontend-vue/layers/dms-ui/app/build/composables/form/formLayout.ts +106 -4
  291. package/frontend-vue/layers/dms-ui/app/build/composables/form/formRecordLoad.ts +11 -5
  292. package/frontend-vue/layers/dms-ui/app/build/composables/form/formSections.ts +32 -0
  293. package/frontend-vue/layers/dms-ui/app/build/composables/form/formValidation.ts +36 -4
  294. package/frontend-vue/layers/dms-ui/app/build/composables/form/readonlyValue.ts +19 -0
  295. package/frontend-vue/layers/dms-ui/app/build/composables/form/registerFormFunctions.ts +27 -0
  296. package/frontend-vue/layers/dms-ui/app/build/composables/form/repeaterTable.ts +78 -0
  297. package/frontend-vue/layers/dms-ui/app/build/composables/form/serverFieldErrors.ts +49 -0
  298. package/frontend-vue/layers/dms-ui/app/build/composables/form/toasterClearance.ts +118 -0
  299. package/frontend-vue/layers/dms-ui/app/build/composables/form/useInstantForm.ts +12 -1
  300. package/frontend-vue/layers/dms-ui/app/build/composables/live/currentPageLive.ts +41 -3
  301. package/frontend-vue/layers/dms-ui/app/build/composables/live/livePoller.ts +10 -2
  302. package/frontend-vue/layers/dms-ui/app/build/composables/live/pageReadClock.ts +28 -0
  303. package/frontend-vue/layers/dms-ui/app/build/composables/live/shortcuts.ts +1 -1
  304. package/frontend-vue/layers/dms-ui/app/build/composables/live/usePageLiveHost.ts +17 -3
  305. package/frontend-vue/layers/dms-ui/app/build/composables/live/useUpdatedLabel.ts +38 -0
  306. package/frontend-vue/layers/dms-ui/app/build/composables/page-toolbar/pageToolbarContext.ts +75 -0
  307. package/frontend-vue/layers/dms-ui/app/build/composables/page-toolbar/toolbarControlHeight.ts +29 -0
  308. package/frontend-vue/layers/dms-ui/app/build/composables/period/periodComparison.ts +52 -0
  309. package/frontend-vue/layers/dms-ui/app/build/composables/period/periodDisplay.ts +21 -0
  310. package/frontend-vue/layers/dms-ui/app/build/composables/period/periodShortLabels.ts +28 -0
  311. package/frontend-vue/layers/dms-ui/app/build/composables/period/usePeriodDraft.ts +1 -0
  312. package/frontend-vue/layers/dms-ui/app/build/composables/table/useRelationFilterLabels.ts +97 -0
  313. package/frontend-vue/layers/dms-ui/app/build/composables/table/useRowScrollAnchor.ts +106 -0
  314. package/frontend-vue/layers/dms-ui/app/build/composables/table/useTable.ts +9 -0
  315. package/frontend-vue/layers/dms-ui/app/build/composables/table/useTableColumns.ts +1 -2
  316. package/frontend-vue/layers/dms-ui/app/build/composables/table/utils/archivedCount.ts +31 -0
  317. package/frontend-vue/layers/dms-ui/app/build/composables/table/utils/filterEditorValue.ts +38 -0
  318. package/frontend-vue/layers/dms-ui/app/build/composables/table/utils/formatFilterValue.ts +33 -2
  319. package/frontend-vue/layers/dms-ui/app/build/composables/table/utils/scrollEdges.ts +31 -0
  320. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useCardFields.ts +33 -0
  321. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useMobileDisplay.ts +48 -0
  322. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useScrolledPastTop.ts +41 -18
  323. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableFooter.ts +3 -2
  324. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableLinkedVisit.ts +78 -0
  325. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableLiveHold.ts +135 -22
  326. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableRows.ts +56 -25
  327. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViewRowActions.ts +90 -35
  328. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViews.ts +86 -10
  329. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/bulkActions.ts +14 -4
  330. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/card.ts +127 -2
  331. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/compareValues.ts +13 -0
  332. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/expandedRows.ts +28 -0
  333. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/groupedRows.ts +114 -1
  334. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/linkedArrival.ts +308 -0
  335. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/locateRow.ts +106 -0
  336. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/pageArrival.ts +34 -0
  337. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/pinnedRows.ts +17 -0
  338. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/quickFilters.ts +41 -33
  339. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/recordLink.ts +24 -8
  340. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/tableCounts.ts +113 -0
  341. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/tableQuery.ts +27 -4
  342. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/views.ts +22 -6
  343. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/writeError.ts +21 -0
  344. package/frontend-vue/layers/dms-ui/app/build/utils/addressSuggestions.ts +49 -0
  345. package/frontend-vue/layers/dms-ui/app/build/utils/codeHighlight.ts +67 -29
  346. package/frontend-vue/layers/dms-ui/app/build/utils/eyebrow.ts +12 -0
  347. package/frontend-vue/layers/dms-ui/app/build/utils/fieldOptionsI18n.ts +51 -11
  348. package/frontend-vue/layers/dms-ui/app/build/utils/htmlGrammar.ts +79 -0
  349. package/frontend-vue/layers/dms-ui/app/build/utils/inputWidth.ts +53 -0
  350. package/frontend-vue/layers/dms-ui/app/build/utils/numberAdjustment.ts +6 -3
  351. package/frontend-vue/layers/dms-ui/app/build/utils/relativeDate.ts +19 -5
  352. package/frontend-vue/layers/dms-ui/app/build/utils/scrollWithin.ts +99 -0
  353. package/frontend-vue/layers/dms-ui/app/build/utils/shellParser.ts +1 -1
  354. package/frontend-vue/layers/dms-ui/app/build/utils/streamHighlight.ts +90 -0
  355. package/frontend-vue/layers/dms-ui/app/build/utils/urlVariables.ts +36 -14
  356. package/frontend-vue/layers/dms-ui/app/components/PasswordStrength.vue +1 -1
  357. package/frontend-vue/layers/dms-ui/app/components/RecursiveComponent.vue +9 -1
  358. package/frontend-vue/layers/dms-ui/app/components/blocks/LiveToggleBlock.vue +80 -32
  359. package/frontend-vue/layers/dms-ui/app/components/blocks/PageToolbarBlock.vue +137 -0
  360. package/frontend-vue/layers/dms-ui/app/components/blocks/StatGroupBlock.vue +5 -0
  361. package/frontend-vue/layers/dms-ui/app/components/chart/ApexChartHost.vue +16 -2
  362. package/frontend-vue/layers/dms-ui/app/components/chart/Chart.vue +24 -5
  363. package/frontend-vue/layers/dms-ui/app/components/chart/ChartCard.vue +41 -23
  364. package/frontend-vue/layers/dms-ui/app/components/code-snippet/CodeSnippet.vue +23 -12
  365. package/frontend-vue/layers/dms-ui/app/components/confirm/ConfirmModal.vue +1 -1
  366. package/frontend-vue/layers/dms-ui/app/components/container-footer/ContainerFooter.vue +34 -0
  367. package/frontend-vue/layers/dms-ui/app/components/field-row/FieldRow.vue +25 -2
  368. package/frontend-vue/layers/dms-ui/app/components/flow-canvas/FlowCanvas.vue +2 -2
  369. package/frontend-vue/layers/dms-ui/app/components/form/Form.vue +119 -36
  370. package/frontend-vue/layers/dms-ui/app/components/form/components/Cascader.vue +53 -22
  371. package/frontend-vue/layers/dms-ui/app/components/form/components/DatePicker.vue +20 -1
  372. package/frontend-vue/layers/dms-ui/app/components/form/components/Display.vue +13 -1
  373. package/frontend-vue/layers/dms-ui/app/components/form/components/Image.vue +8 -3
  374. package/frontend-vue/layers/dms-ui/app/components/form/components/InputAddress.vue +53 -5
  375. package/frontend-vue/layers/dms-ui/app/components/form/components/InputCode.vue +29 -5
  376. package/frontend-vue/layers/dms-ui/app/components/form/components/InputNumber.vue +45 -8
  377. package/frontend-vue/layers/dms-ui/app/components/form/components/InputPercentage.vue +28 -5
  378. package/frontend-vue/layers/dms-ui/app/components/form/components/InputSecret.vue +5 -0
  379. package/frontend-vue/layers/dms-ui/app/components/form/components/InputText.vue +19 -2
  380. package/frontend-vue/layers/dms-ui/app/components/form/components/InputTime.vue +2 -1
  381. package/frontend-vue/layers/dms-ui/app/components/form/components/Relation.vue +226 -65
  382. package/frontend-vue/layers/dms-ui/app/components/form/components/Repeater.vue +80 -30
  383. package/frontend-vue/layers/dms-ui/app/components/form/components/RepeaterTable.vue +210 -0
  384. package/frontend-vue/layers/dms-ui/app/components/form/components/Select.vue +4 -2
  385. package/frontend-vue/layers/dms-ui/app/components/grid/GridRow.vue +11 -2
  386. package/frontend-vue/layers/dms-ui/app/components/key-value-list/KeyValueList.vue +79 -70
  387. package/frontend-vue/layers/dms-ui/app/components/kpi/KpiCard.vue +117 -21
  388. package/frontend-vue/layers/dms-ui/app/components/meter/Meter.vue +2 -1
  389. package/frontend-vue/layers/dms-ui/app/components/period/PeriodSelector.vue +113 -34
  390. package/frontend-vue/layers/dms-ui/app/components/save-bar/SaveBar.vue +14 -2
  391. package/frontend-vue/layers/dms-ui/app/components/section-header/Eyebrow.vue +6 -1
  392. package/frontend-vue/layers/dms-ui/app/components/segmented/Segmented.vue +13 -0
  393. package/frontend-vue/layers/dms-ui/app/components/stat-group/StatGroup.vue +52 -3
  394. package/frontend-vue/layers/dms-ui/app/components/tab/Tab.vue +2 -1
  395. package/frontend-vue/layers/dms-ui/app/components/table-view/CascaderPath.vue +74 -25
  396. package/frontend-vue/layers/dms-ui/app/components/table-view/KanbanBoard.vue +48 -44
  397. package/frontend-vue/layers/dms-ui/app/components/table-view/KanbanDisplay.vue +4 -1
  398. package/frontend-vue/layers/dms-ui/app/components/table-view/TableView.vue +426 -97
  399. package/frontend-vue/layers/dms-ui/app/components/top-list/TopListCard.vue +33 -5
  400. package/frontend-vue/layers/dms-ui/app/composables/chart/apexCategoryLabels.ts +68 -0
  401. package/frontend-vue/layers/dms-ui/app/composables/chart/apexDatetimeBuckets.ts +306 -0
  402. package/frontend-vue/layers/dms-ui/app/composables/chart/formatValue.ts +4 -3
  403. package/frontend-vue/layers/dms-ui/app/composables/chart/heatmapColors.ts +93 -0
  404. package/frontend-vue/layers/dms-ui/app/composables/chart/localizeSeries.ts +62 -0
  405. package/frontend-vue/layers/dms-ui/app/composables/chart/useApexChart.ts +116 -8
  406. package/frontend-vue/layers/dms-ui/app/composables/chart/useApexChart.types.ts +7 -0
  407. package/frontend-vue/layers/dms-ui/app/composables/containers/types.ts +84 -1
  408. package/frontend-vue/layers/dms-ui/app/composables/containers/useDrawer.ts +69 -8
  409. package/frontend-vue/layers/dms-ui/app/composables/form/types/field-saved.ts +11 -0
  410. package/frontend-vue/layers/dms-ui/app/composables/form/types/field.ts +16 -0
  411. package/frontend-vue/layers/dms-ui/app/composables/form/types/functions.ts +1 -0
  412. package/frontend-vue/layers/dms-ui/app/composables/form/useForm.ts +189 -28
  413. package/frontend-vue/layers/dms-ui/app/composables/live/usePageLive.ts +29 -12
  414. package/frontend-vue/layers/dms-ui/app/composables/page-toolbar/usePageToolbarSize.ts +30 -0
  415. package/frontend-vue/layers/dms-ui/app/composables/table-view/kanban.ts +4 -2
  416. package/frontend-vue/layers/dms-ui/app/composables/table-view/types/action-target.ts +22 -3
  417. package/frontend-vue/layers/dms-ui/app/composables/table-view/types/config.ts +4 -0
  418. package/frontend-vue/layers/dms-ui/app/composables/table-view/types/display.ts +24 -1
  419. package/frontend-vue/layers/dms-ui/app/plugins/closing-layers.client.ts +10 -0
  420. package/frontend-vue/layers/dms-ui/app/plugins/live-pause-source.ts +15 -0
  421. package/frontend-vue/layers/dms-ui/app/plugins/register.ts +2 -0
  422. package/frontend-vue/layers/dms-ui/app/types/input-number.ts +3 -2
  423. package/frontend-vue/layers/dms-ui/app/types/page-toolbar.ts +7 -0
  424. package/frontend-vue/layers/dms-ui/app/types/row-action.ts +0 -2
  425. package/frontend-vue/layers/dms-ui/app/types/row-navigation.ts +17 -0
  426. package/frontend-vue/layers/dms-ui/app/types/table/query.ts +2 -1
  427. package/frontend-vue/layers/dms-ui/app/utils/blockRefresh.ts +3 -1
  428. package/frontend-vue/layers/dms-ui/app/utils/cascader.ts +94 -3
  429. package/frontend-vue/layers/dms-ui/app/utils/componentTree.ts +24 -0
  430. package/frontend-vue/layers/dms-ui/i18n/locales/ui-en-GB.json +20 -5
  431. package/frontend-vue/layers/dms-ui/i18n/locales/ui-fr-FR.json +20 -5
  432. package/frontend-vue/package.json +6 -2
  433. package/package.json +11 -10
  434. package/skills/dms/SKILL.md +1 -1
  435. package/skills/dms-auth/SKILL.md +5 -3
  436. package/skills/dms-best-practices/REFERENCE.md +2 -0
  437. package/skills/dms-best-practices/SKILL.md +4 -0
  438. package/skills/dms-module/SKILL.md +3 -2
  439. package/skills/dms-pages/REFERENCE.md +129 -16
  440. package/skills/dms-pages/SKILL.md +1 -1
  441. package/skills/dms-project/DEV-SERVERS.md +23 -1
  442. package/skills/dms-project/REFERENCE.md +1 -1
  443. package/skills/dms-project/SKILL.md +4 -1
  444. package/skills/dms-project/scripts/dms-dev.sh +205 -17
  445. package/dist/hooks/tenant-instances.d.ts +0 -1
  446. package/dist/hooks/tenant-instances.js +0 -13
  447. package/dist/hooks/tenant-instances.js.map +0 -1
  448. package/dist/routes/startup-gate.d.ts +0 -7
  449. package/dist/routes/startup-gate.js +0 -39
  450. package/dist/routes/startup-gate.js.map +0 -1
  451. package/frontend-vue/layers/dms-core/app/composables/period/periodRenderNow.ts +0 -42
@@ -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,44 @@
1
+ import type { DataTypeFormatter } from "#dms-core/app/composables/data-types/useDataType";
2
+ import { getAppTranslation } from "../../../../../dms-core/app/composables/translation/useTranslation";
3
+
4
+ /** A formatter writing a number for a locale, with Intl's options. */
5
+ type NumberFormatter = (
6
+ value: unknown,
7
+ locale: string,
8
+ options?: Intl.NumberFormatOptions,
9
+ ) => unknown;
10
+
11
+ /** The options of a numeric field a cell reads besides Intl's. */
12
+ interface NumberSuffixOptions {
13
+ /** Unit after the value ("days"): an i18n key with `$`, or a literal. */
14
+ suffix?: string;
15
+ }
16
+
17
+ // Keeps the unit on the line of its number when a cell wraps.
18
+ const UNIT_SEPARATOR = " ";
19
+
20
+ /**
21
+ * A numeric data type's formatter writing the field's unit after the value,
22
+ * as its form input does ("12.5 kWp"), wherever the type is drawn: table
23
+ * cells, expanded rows, cards, read-only fields. A value that is not a
24
+ * number is left as the formatter returns it.
25
+ *
26
+ * A formatter also runs where no component setup is active (a footer sum, a
27
+ * group header, an export), so the unit is translated through the
28
+ * application's composer, never through a composable.
29
+ */
30
+ export function withNumberSuffix(format: NumberFormatter): DataTypeFormatter {
31
+ return (value, locale, options) => {
32
+ const formatted = format(
33
+ value,
34
+ locale,
35
+ options as Intl.NumberFormatOptions | undefined,
36
+ );
37
+ const suffix = (options as NumberSuffixOptions | undefined)?.suffix;
38
+ if (!suffix || typeof value !== "number" || typeof formatted !== "string") {
39
+ return formatted;
40
+ }
41
+ const unit = getAppTranslation().processI18n(suffix);
42
+ return `${formatted}${UNIT_SEPARATOR}${unit}`;
43
+ };
44
+ }
@@ -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 { NotFoundRelation, 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}`)
@@ -282,12 +338,31 @@ function renderRelationBadge(label: string, title?: string) {
282
338
  });
283
339
  }
284
340
 
341
+ // An id its `select` route did not answer: "Not found", the id on hover.
342
+ function renderNotFoundRelation(relation: NotFoundRelation) {
343
+ const { t } = getAppTranslation();
344
+ return h(
345
+ "span",
346
+ { class: "text-muted", title: relation.id },
347
+ t("dms.form.relation.not_found"),
348
+ );
349
+ }
350
+
285
351
  function renderRelationBadges(values: unknown[], labelKey: string) {
286
- const { processI18n } = useTranslation();
287
- const { visible, hidden } = buildRelationBadges(values, labelKey);
288
- const badges = visible.map((label) =>
289
- renderRelationBadge(processI18n(label)),
352
+ const { processI18n, t } = getAppTranslation();
353
+ const notFound = values.filter(
354
+ (item): item is NotFoundRelation => item instanceof NotFoundRelation,
290
355
  );
356
+ const { visible, hidden } = buildRelationBadges(
357
+ values.filter((item) => !(item instanceof NotFoundRelation)),
358
+ labelKey,
359
+ );
360
+ const badges = [
361
+ ...visible.map((label) => renderRelationBadge(processI18n(label))),
362
+ ...notFound.map((relation) =>
363
+ renderRelationBadge(t("dms.form.relation.not_found"), relation.id),
364
+ ),
365
+ ];
291
366
  if (hidden.length > 0) {
292
367
  const hiddenLabels = hidden.map((label) => processI18n(label)).join(", ");
293
368
  badges.push(renderRelationBadge(`+${hidden.length}`, hiddenLabels));
@@ -303,6 +378,7 @@ function renderRelation(value: unknown, options: unknown) {
303
378
  return renderRelationBadges(value, labelKey);
304
379
  }
305
380
 
381
+ if (value instanceof NotFoundRelation) return renderNotFoundRelation(value);
306
382
  if (!value || !isObject(value)) return value;
307
383
 
308
384
  const valueObject = value as Record<string, string>;
@@ -354,7 +430,7 @@ function renderImage(value: unknown, options: unknown) {
354
430
  }
355
431
 
356
432
  function renderFile(value: unknown, options: unknown) {
357
- const { t } = useI18n();
433
+ const { t } = getAppTranslation();
358
434
  const opts = options as FileOptions | undefined;
359
435
 
360
436
  if (opts?.multiple && Array.isArray(value)) {
@@ -408,8 +484,8 @@ const matchesBadge = (badge: IdentityBadgeOption, value: unknown): boolean =>
408
484
  * `emptyIcon` stand for a row without a value.
409
485
  */
410
486
  function renderIdentity(value: unknown, options: unknown, row: Row) {
411
- const { processI18n } = useTranslation();
412
- const { processText } = useComposedText();
487
+ const { processI18n } = getAppTranslation();
488
+ const { processText } = getAppComposedText();
413
489
  const opts = (options ?? {}) as IdentityOptions;
414
490
  const badges = (opts.badges ?? [])
415
491
  .filter((badge) => matchesBadge(badge, readRowField(row, badge.field)))
@@ -526,7 +602,7 @@ function renderPill(pill: PillItem) {
526
602
  * a relation could not resolve (a deleted row) is skipped.
527
603
  */
528
604
  function renderPills(value: unknown, options: unknown, row: Row) {
529
- const { processI18n } = useTranslation();
605
+ const { processI18n } = getAppTranslation();
530
606
  const opts = (options ?? {}) as PillsOptions;
531
607
  if (opts.exclusive && isSet(readRowField(row, opts.exclusive.field))) {
532
608
  return h("span", { class: [PILL_CLASS, PILL_FILLED_CLASS] }, [
@@ -605,11 +681,14 @@ function renderRelativeDate(
605
681
  options: unknown,
606
682
  row: Row,
607
683
  ) {
608
- const { processI18n } = useTranslation();
684
+ const { processI18n } = getAppTranslation();
609
685
  const opts = (options ?? {}) as RelativeDateOptions;
610
686
  const date = new Date(value as string);
611
687
  if (Number.isNaN(date.getTime())) return String(value);
612
- const deltaMs = date.getTime() - Date.now();
688
+ // Measured from the server render's instant while hydrating it: "3 min ago"
689
+ // written at 12:00:59 stays so in a browser hydrating at 12:01:01.
690
+ const now = serverRenderNow() ?? Date.now();
691
+ const deltaMs = date.getTime() - now;
613
692
  const isPast = deltaMs < 0;
614
693
  const title = formatDate(date, locale, RELATIVE_TITLE_FORMAT) ?? undefined;
615
694
 
@@ -624,8 +703,8 @@ function renderRelativeDate(
624
703
  const style = (isPast ? opts.pastStyle : undefined) ?? opts.style;
625
704
  let text =
626
705
  style === "day"
627
- ? formatDayMonth(date, locale)
628
- : formatRelativeDate(date, locale);
706
+ ? formatDayMonth(date, locale, now)
707
+ : formatRelativeDate(date, locale, now);
629
708
  const isSoon = !isPast && !!opts.soonWithinMs && deltaMs < opts.soonWithinMs;
630
709
  const tone = isPast
631
710
  ? (opts.pastTone ?? opts.tone)
@@ -644,7 +723,7 @@ function renderRelativeDate(
644
723
  }
645
724
 
646
725
  function renderMissingDate(options: unknown) {
647
- const { processI18n } = useTranslation();
726
+ const { processI18n } = getAppTranslation();
648
727
  const opts = (options ?? {}) as RelativeDateOptions;
649
728
  if (!opts.emptyLabel) return "-";
650
729
  return h(
@@ -670,7 +749,7 @@ interface IndicatorOptions {
670
749
  * on for a set flag or a non-empty list.
671
750
  */
672
751
  function renderIndicator(value: unknown, options: unknown) {
673
- const { processI18n } = useTranslation();
752
+ const { processI18n } = getAppTranslation();
674
753
  const opts = (options ?? {}) as IndicatorOptions;
675
754
  const isOn = isSet(value);
676
755
  const icon = isOn ? opts.onIcon : opts.offIcon;
@@ -768,7 +847,8 @@ const registerPrimitiveTypes = (
768
847
  ) => {
769
848
  registerDataType({
770
849
  id: "number",
771
- formatter: { default: formatNumber as DataTypeFormatter },
850
+ formatter: { default: withNumberSuffix(formatNumber) },
851
+ filterEditorValue: numberFilterEditorValue,
772
852
  });
773
853
  registerDataType({
774
854
  id: "string",
@@ -776,15 +856,24 @@ const registerPrimitiveTypes = (
776
856
  });
777
857
  registerDataType({
778
858
  id: "price",
779
- formatter: { default: formatPrice as DataTypeFormatter },
859
+ formatter: { default: withNumberSuffix(formatPrice) },
860
+ filterEditorValue: numberFilterEditorValue,
780
861
  });
781
862
  registerDataType({
782
863
  id: "percentage",
783
- formatter: { default: formatPercentage as DataTypeFormatter },
864
+ formatter: { default: withNumberSuffix(formatPercentage) },
865
+ filterEditorValue: numberFilterEditorValue,
784
866
  });
785
867
  registerDataType({
786
868
  id: "string_time",
787
- formatter: { default: formatTimeSpan },
869
+ // Written as the field writes it ("7 h 30 min"), in the reader's locale,
870
+ // rounded as every duration cell is: the field keeps the exact value.
871
+ formatter: {
872
+ default: (value: unknown, locale: string) =>
873
+ isNumber(value)
874
+ ? formatDuration(Number(value), locale, { precision: "rounded" })
875
+ : value,
876
+ },
788
877
  });
789
878
  };
790
879
 
@@ -801,17 +890,18 @@ const registerDateType = (registerDataType: (dataType: DataType) => void) => {
801
890
  id: "date",
802
891
  formatter: {
803
892
  default: (value: unknown, locale: string) => {
804
- const { t } = useI18n();
893
+ const { t } = getAppTranslation();
805
894
  return formatDate(value, locale) ?? t("dms.date.undefined");
806
895
  },
807
896
  cell: (value: unknown, locale: string) => {
808
- const { t } = useI18n();
897
+ const { t } = getAppTranslation();
809
898
  return (
810
899
  formatDate(value, locale, CELL_DATE_FORMAT) ?? t("dms.date.undefined")
811
900
  );
812
901
  },
813
902
  is_between: formatDateBetween,
814
903
  },
904
+ filterEditorValue: dateFilterEditorValue,
815
905
  });
816
906
  };
817
907
 
@@ -875,9 +965,13 @@ const registerLinkTypes = (registerDataType: (dataType: DataType) => void) => {
875
965
  id: "email",
876
966
  formatter: { default: createLinkRenderer({ hrefPrefix: "mailto:" }) },
877
967
  });
968
+ const renderLink = createLinkRenderer({ target: "_blank" });
878
969
  registerDataType({
879
970
  id: "url",
880
- formatter: { default: createLinkRenderer({ target: "_blank" }) },
971
+ formatter: {
972
+ default: (value: unknown, locale: string, options: unknown, row?: Row) =>
973
+ renderUrl(value, locale, options, row) ?? renderLink(value, locale),
974
+ },
881
975
  });
882
976
  };
883
977
 
@@ -892,6 +986,7 @@ const registerRelationType = (
892
986
  },
893
987
  beforeStateMapper: mapRelationBeforeState,
894
988
  isSameValue: sameRelationValue,
989
+ selectSource: relationSelectSource,
895
990
  });
896
991
  };
897
992
 
@@ -906,6 +1001,7 @@ const registerCascaderRelationType = (
906
1001
  },
907
1002
  beforeStateMapper: mapRelationBeforeState,
908
1003
  isSameValue: sameRelationValue,
1004
+ selectSource: relationSelectSource,
909
1005
  });
910
1006
  };
911
1007
 
@@ -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,92 @@
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
+ * An id a relation's `select` route did not answer: a deleted row, or one out
50
+ * of the reader's reach — it cannot tell which, so it is shown as "Not
51
+ * found", never as "Deleted", with the id itself in its tooltip.
52
+ */
53
+ export class NotFoundRelation {
54
+ constructor(readonly id: string) {}
55
+ }
56
+
57
+ /** The ids of a read that its rows do not answer. */
58
+ export function notFoundRelationIds(
59
+ source: DataTypeSelectSource,
60
+ ids: string[],
61
+ rows: RelationRow[],
62
+ ): string[] {
63
+ const answered = new Set(
64
+ rows.map((row) => String(relationRowValue(source, row))),
65
+ );
66
+ return ids.filter((id) => !answered.has(id));
67
+ }
68
+
69
+ /**
70
+ * Reads the rows of a relation's ids from its `select` route, narrowed with
71
+ * `?ids=` to those rows alone (matched on the mapped value key), whatever the
72
+ * route's page size. The one way the picker, the filter chips and the
73
+ * read-only lists name the ids they hold. A failed read answers no rows: the
74
+ * ids then show as not found, as a row out of the reader's reach does.
75
+ */
76
+ export function useRelationRows() {
77
+ const { $authFetch } = useAuthFetch();
78
+
79
+ async function readRelationRows(
80
+ source: DataTypeSelectSource,
81
+ ids: string[],
82
+ ): Promise<RelationRow[]> {
83
+ const url = pageRequestUrl(source.searchUrl);
84
+ if (!url || ids.length === 0) return [];
85
+ const response = await $authFetch<RelationSelectResponse>(url, {
86
+ params: relationIdsQuery(ids, source.keyMapping),
87
+ }).catch(() => undefined);
88
+ return response?.results ?? [];
89
+ }
90
+
91
+ return { readRelationRows };
92
+ }