@antelopejs/dms 0.9.7 → 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 (414) hide show
  1. package/CHANGELOG.md +114 -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 -45
  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/realtime.d.ts +13 -1
  117. package/dist/routes/realtime.js +114 -29
  118. package/dist/routes/realtime.js.map +1 -1
  119. package/dist/startup-config.d.ts +2 -0
  120. package/dist/startup-config.js +62 -0
  121. package/dist/startup-config.js.map +1 -0
  122. package/dist/store/interfaces.d.ts +3 -2
  123. package/dist/store/interfaces.js +16 -6
  124. package/dist/store/interfaces.js.map +1 -1
  125. package/dist/store/listing.js +24 -13
  126. package/dist/store/listing.js.map +1 -1
  127. package/dist/store/operations.js +4 -0
  128. package/dist/store/operations.js.map +1 -1
  129. package/dist/store/requirements.js +3 -2
  130. package/dist/store/requirements.js.map +1 -1
  131. package/dist/store/runtime.d.ts +10 -1
  132. package/dist/store/runtime.js +33 -6
  133. package/dist/store/runtime.js.map +1 -1
  134. package/dist/store/types.d.ts +10 -0
  135. package/dist/validation/auth.schema.d.ts +2 -2
  136. package/dist/validation/member-invite.schema.d.ts +2 -10
  137. package/dist/validation/member-invite.schema.js +2 -15
  138. package/dist/validation/member-invite.schema.js.map +1 -1
  139. package/frontend-vue/dms.frontend.ts +24 -5
  140. package/frontend-vue/dms.server.ts +17 -0
  141. package/frontend-vue/layers/dms-auth/server/accounts.ts +3 -4
  142. package/frontend-vue/layers/dms-core/app/build/composables/renderNow.ts +34 -0
  143. package/frontend-vue/layers/dms-core/app/build/composables/user/tablePreferences.ts +5 -1
  144. package/frontend-vue/layers/dms-core/app/build/utils/crossTab.ts +199 -0
  145. package/frontend-vue/layers/dms-core/app/build/utils/sharedRealtimeConnection.ts +410 -0
  146. package/frontend-vue/layers/dms-core/app/composables/auth/useApiData.ts +77 -12
  147. package/frontend-vue/layers/dms-core/app/composables/data-types/useDataType.ts +26 -0
  148. package/frontend-vue/layers/dms-core/app/composables/hydration/isHydratingComponent.ts +29 -0
  149. package/frontend-vue/layers/dms-core/app/composables/live/livePauseSource.ts +29 -0
  150. package/frontend-vue/layers/dms-core/app/composables/period/compareResolvers.ts +150 -9
  151. package/frontend-vue/layers/dms-core/app/composables/period/usePeriod.ts +6 -4
  152. package/frontend-vue/layers/dms-core/app/composables/period/usePeriodScope.ts +27 -1
  153. package/frontend-vue/layers/dms-core/app/composables/realtime/useUserRealtime.ts +256 -119
  154. package/frontend-vue/layers/dms-core/app/composables/translation/useComposedText.ts +21 -8
  155. package/frontend-vue/layers/dms-core/app/composables/translation/useTranslation.ts +22 -5
  156. package/frontend-vue/layers/dms-core/app/plugins/dev-error-log.ts +16 -0
  157. package/frontend-vue/layers/dms-core/app/types/user.ts +6 -0
  158. package/frontend-vue/layers/dms-core/app/utils/composedText.ts +7 -2
  159. package/frontend-vue/layers/dms-core/app/utils/formatter.ts +166 -35
  160. package/frontend-vue/layers/dms-core/app/utils/url-interpolation.ts +18 -1
  161. package/frontend-vue/layers/dms-core/i18n/locales/core-en-GB.json +134 -2
  162. package/frontend-vue/layers/dms-core/i18n/locales/core-fr-FR.json +134 -2
  163. package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardBanners.vue +3 -2
  164. package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardHeader.vue +41 -21
  165. package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardPageBody.vue +9 -0
  166. package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardSidebar.vue +4 -19
  167. package/frontend-vue/layers/dms-layout/app/build/components/layout/Footer.vue +11 -3
  168. package/frontend-vue/layers/dms-layout/app/build/components/layout/GithubStarPrompt.vue +18 -7
  169. package/frontend-vue/layers/dms-layout/app/build/components/layout/PageHeaderActionBar.vue +8 -0
  170. package/frontend-vue/layers/dms-layout/app/build/components/layout/PageHeaderActionsOutlet.vue +14 -0
  171. package/frontend-vue/layers/dms-layout/app/build/components/layout/RowSidePanel.vue +43 -82
  172. package/frontend-vue/layers/dms-layout/app/build/components/layout/SidebarUserMenu.vue +44 -3
  173. package/frontend-vue/layers/dms-layout/app/build/components/layout/UserIdentity.vue +11 -3
  174. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleCatalogTile.vue +132 -35
  175. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleManageMenu.vue +59 -0
  176. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleSourceLabel.vue +25 -0
  177. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleStoreBanners.vue +73 -0
  178. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleStoreOperation.vue +116 -0
  179. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleStoreRequirements.vue +8 -3
  180. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleStoreTile.vue +85 -207
  181. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleTile.vue +10 -3
  182. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleTileSkeleton.vue +68 -0
  183. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/notification/NotificationInboxDisplay.vue +24 -8
  184. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/notification/NotificationInboxItem.vue +8 -5
  185. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/region/RegionTimeZoneInput.vue +1 -0
  186. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/roles/role-permissions.ts +19 -0
  187. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/roles/useRoleEditor.ts +2 -1
  188. package/frontend-vue/layers/dms-layout/app/build/composables/layout/useSidePanelWidth.ts +31 -70
  189. package/frontend-vue/layers/dms-layout/app/build/composables/notification/useNotificationGroupsOpen.ts +11 -1
  190. package/frontend-vue/layers/dms-layout/app/build/utils/dev-reload-stream.ts +81 -0
  191. package/frontend-vue/layers/dms-layout/app/build/utils/module-store-catalog.ts +232 -36
  192. package/frontend-vue/layers/dms-layout/app/build/utils/modules-catalog.ts +122 -14
  193. package/frontend-vue/layers/dms-layout/app/composables/layout/useLayoutBanners.ts +32 -3
  194. package/frontend-vue/layers/dms-layout/app/composables/layout/usePageRecord.ts +50 -0
  195. package/frontend-vue/layers/dms-layout/app/composables/layout/useServedHeaderActions.ts +25 -7
  196. package/frontend-vue/layers/dms-layout/app/composables/page/useModuleHistory.ts +3 -2
  197. package/frontend-vue/layers/dms-layout/app/composables/page/useModuleStore.ts +51 -4
  198. package/frontend-vue/layers/dms-layout/app/composables/page/useModuleStoreActions.ts +194 -0
  199. package/frontend-vue/layers/dms-layout/app/composables/page/usePageComponentPreload.ts +23 -3
  200. package/frontend-vue/layers/dms-layout/app/composables/page/usePageLayout.ts +4 -2
  201. package/frontend-vue/layers/dms-layout/app/composables/page/useSiteLayout.ts +6 -1
  202. package/frontend-vue/layers/dms-layout/app/custom-layouts/DefaultLayout.vue +9 -0
  203. package/frontend-vue/layers/dms-layout/app/custom-layouts/SettingsLayout.vue +9 -0
  204. package/frontend-vue/layers/dms-layout/app/custom-pages/modules-store.vue +291 -0
  205. package/frontend-vue/layers/dms-layout/app/custom-pages/modules.vue +87 -474
  206. package/frontend-vue/layers/dms-layout/app/middleware/module-routing.global.ts +3 -0
  207. package/frontend-vue/layers/dms-layout/app/pages/[...slug].vue +6 -22
  208. package/frontend-vue/layers/dms-layout/app/plugins/command-palette-navigation.client.ts +2 -1
  209. package/frontend-vue/layers/dms-layout/app/plugins/command-palette-personal-pages.client.ts +14 -44
  210. package/frontend-vue/layers/dms-layout/app/plugins/dms-dev-reload.client.ts +14 -9
  211. package/frontend-vue/layers/dms-layout/app/theme/form-controls.ts +9 -4
  212. package/frontend-vue/layers/dms-layout/app/theme/surfaces.ts +22 -6
  213. package/frontend-vue/layers/dms-layout/app/types/module-store.ts +9 -0
  214. package/frontend-vue/layers/dms-layout/app/types/page.ts +15 -1
  215. package/frontend-vue/layers/dms-layout/app/utils/account-menu.ts +11 -0
  216. package/frontend-vue/layers/dms-layout/app/utils/breadcrumb.ts +57 -0
  217. package/frontend-vue/layers/dms-layout/app/utils/pageQuery.ts +26 -0
  218. package/frontend-vue/layers/dms-layout/app/utils/pageRecord.ts +71 -0
  219. package/frontend-vue/layers/dms-layout/i18n/locales/layout-en-GB.json +39 -14
  220. package/frontend-vue/layers/dms-layout/i18n/locales/layout-fr-FR.json +38 -13
  221. package/frontend-vue/layers/dms-ui/app/build/components/containers/TargetComponentTree.vue +31 -0
  222. package/frontend-vue/layers/dms-ui/app/build/components/containers/constants.ts +25 -0
  223. package/frontend-vue/layers/dms-ui/app/build/components/containers/drawer/DynamicDrawer.vue +244 -15
  224. package/frontend-vue/layers/dms-ui/app/build/components/containers/modal/DynamicModal.vue +42 -25
  225. package/frontend-vue/layers/dms-ui/app/build/components/containers/row/RowHeader.vue +153 -0
  226. package/frontend-vue/layers/dms-ui/app/build/components/containers/row/RowNotFound.vue +44 -0
  227. package/frontend-vue/layers/dms-ui/app/build/components/containers/row/drawerRowHeader.ts +35 -0
  228. package/frontend-vue/layers/dms-ui/app/build/components/form/FormEntries.vue +44 -9
  229. package/frontend-vue/layers/dms-ui/app/build/components/form/FormErrorSummary.vue +11 -4
  230. package/frontend-vue/layers/dms-ui/app/build/components/form/FormFieldControl.vue +38 -5
  231. package/frontend-vue/layers/dms-ui/app/build/components/form/FormFieldNotes.vue +29 -5
  232. package/frontend-vue/layers/dms-ui/app/build/components/form/FormSectionNav.vue +3 -1
  233. package/frontend-vue/layers/dms-ui/app/build/components/form/FormSections.vue +106 -27
  234. package/frontend-vue/layers/dms-ui/app/build/components/table/Actions.vue +12 -4
  235. package/frontend-vue/layers/dms-ui/app/build/components/table/Empty.vue +4 -2
  236. package/frontend-vue/layers/dms-ui/app/build/components/table/FiltersRow.vue +38 -1
  237. package/frontend-vue/layers/dms-ui/app/build/components/table/Pagination.vue +55 -54
  238. package/frontend-vue/layers/dms-ui/app/build/components/table/RowSelection.vue +2 -1
  239. package/frontend-vue/layers/dms-ui/app/build/components/table/Table.vue +396 -288
  240. package/frontend-vue/layers/dms-ui/app/build/components/table/Tabs.vue +2 -1
  241. package/frontend-vue/layers/dms-ui/app/build/components/table/Views.vue +2 -1
  242. package/frontend-vue/layers/dms-ui/app/build/components/table-view/CardAvatar.vue +59 -0
  243. package/frontend-vue/layers/dms-ui/app/build/components/table-view/CardGridSkeleton.vue +19 -2
  244. package/frontend-vue/layers/dms-ui/app/build/components/table-view/CardRowActions.vue +38 -0
  245. package/frontend-vue/layers/dms-ui/app/build/components/table-view/CardsDisplay.vue +148 -104
  246. package/frontend-vue/layers/dms-ui/app/build/components/table-view/LiveHoldBanner.vue +17 -2
  247. package/frontend-vue/layers/dms-ui/app/build/composables/actions/bulkSelection.ts +43 -9
  248. package/frontend-vue/layers/dms-ui/app/build/composables/actions/rowComponentOptions.ts +76 -0
  249. package/frontend-vue/layers/dms-ui/app/build/composables/actions/rowNavigation.ts +172 -27
  250. package/frontend-vue/layers/dms-ui/app/build/composables/actions/useActionTargets.ts +270 -46
  251. package/frontend-vue/layers/dms-ui/app/build/composables/confirm/confirmDialogTexts.ts +2 -0
  252. package/frontend-vue/layers/dms-ui/app/build/composables/containers/closingLayers.ts +186 -0
  253. package/frontend-vue/layers/dms-ui/app/build/composables/containers/context.ts +14 -0
  254. package/frontend-vue/layers/dms-ui/app/build/composables/containers/drawerSize.ts +229 -0
  255. package/frontend-vue/layers/dms-ui/app/build/composables/containers/useResizableSize.ts +175 -0
  256. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/metricCells.ts +18 -35
  257. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/numberSuffix.ts +47 -0
  258. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/registerDefaults.ts +99 -24
  259. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/registerFieldTypes.ts +2 -1
  260. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/relationFilter.ts +75 -0
  261. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/relationRows.ts +71 -0
  262. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/relationValue.ts +39 -0
  263. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/twoLineCell.ts +3 -3
  264. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/useColumnValueRenderer.ts +6 -2
  265. package/frontend-vue/layers/dms-ui/app/build/composables/form/createFormFields.ts +15 -0
  266. package/frontend-vue/layers/dms-ui/app/build/composables/form/fieldSaved.ts +40 -0
  267. package/frontend-vue/layers/dms-ui/app/build/composables/form/fieldValueRule.ts +62 -0
  268. package/frontend-vue/layers/dms-ui/app/build/composables/form/formEntryContext.ts +5 -0
  269. package/frontend-vue/layers/dms-ui/app/build/composables/form/formFooter.ts +42 -7
  270. package/frontend-vue/layers/dms-ui/app/build/composables/form/formLayout.ts +106 -4
  271. package/frontend-vue/layers/dms-ui/app/build/composables/form/formSections.ts +32 -0
  272. package/frontend-vue/layers/dms-ui/app/build/composables/form/readonlyValue.ts +19 -0
  273. package/frontend-vue/layers/dms-ui/app/build/composables/form/registerFormFunctions.ts +27 -0
  274. package/frontend-vue/layers/dms-ui/app/build/composables/form/repeaterTable.ts +65 -0
  275. package/frontend-vue/layers/dms-ui/app/build/composables/form/serverFieldErrors.ts +49 -0
  276. package/frontend-vue/layers/dms-ui/app/build/composables/form/toasterClearance.ts +118 -0
  277. package/frontend-vue/layers/dms-ui/app/build/composables/form/useInstantForm.ts +12 -1
  278. package/frontend-vue/layers/dms-ui/app/build/composables/live/currentPageLive.ts +41 -3
  279. package/frontend-vue/layers/dms-ui/app/build/composables/live/livePoller.ts +10 -2
  280. package/frontend-vue/layers/dms-ui/app/build/composables/live/pageReadClock.ts +28 -0
  281. package/frontend-vue/layers/dms-ui/app/build/composables/live/shortcuts.ts +1 -1
  282. package/frontend-vue/layers/dms-ui/app/build/composables/live/usePageLiveHost.ts +17 -3
  283. package/frontend-vue/layers/dms-ui/app/build/composables/live/useUpdatedLabel.ts +38 -0
  284. package/frontend-vue/layers/dms-ui/app/build/composables/page-toolbar/pageToolbarContext.ts +75 -0
  285. package/frontend-vue/layers/dms-ui/app/build/composables/page-toolbar/toolbarControlHeight.ts +29 -0
  286. package/frontend-vue/layers/dms-ui/app/build/composables/period/periodComparison.ts +52 -0
  287. package/frontend-vue/layers/dms-ui/app/build/composables/period/periodDisplay.ts +21 -0
  288. package/frontend-vue/layers/dms-ui/app/build/composables/period/periodShortLabels.ts +28 -0
  289. package/frontend-vue/layers/dms-ui/app/build/composables/period/usePeriodDraft.ts +1 -0
  290. package/frontend-vue/layers/dms-ui/app/build/composables/table/useRelationFilterLabels.ts +73 -0
  291. package/frontend-vue/layers/dms-ui/app/build/composables/table/useRowScrollAnchor.ts +106 -0
  292. package/frontend-vue/layers/dms-ui/app/build/composables/table/useTable.ts +9 -0
  293. package/frontend-vue/layers/dms-ui/app/build/composables/table/useTableColumns.ts +1 -2
  294. package/frontend-vue/layers/dms-ui/app/build/composables/table/utils/archivedCount.ts +31 -0
  295. package/frontend-vue/layers/dms-ui/app/build/composables/table/utils/filterEditorValue.ts +38 -0
  296. package/frontend-vue/layers/dms-ui/app/build/composables/table/utils/formatFilterValue.ts +33 -2
  297. package/frontend-vue/layers/dms-ui/app/build/composables/table/utils/scrollEdges.ts +31 -0
  298. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useCardFields.ts +33 -0
  299. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useMobileDisplay.ts +48 -0
  300. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useScrolledPastTop.ts +41 -18
  301. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableFooter.ts +3 -2
  302. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableLinkedVisit.ts +78 -0
  303. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableLiveHold.ts +135 -22
  304. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableRows.ts +56 -25
  305. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViewRowActions.ts +77 -32
  306. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViews.ts +86 -10
  307. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/bulkActions.ts +14 -4
  308. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/card.ts +127 -2
  309. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/compareValues.ts +13 -0
  310. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/expandedRows.ts +28 -0
  311. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/groupedRows.ts +114 -1
  312. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/linkedArrival.ts +308 -0
  313. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/locateRow.ts +106 -0
  314. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/pageArrival.ts +34 -0
  315. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/pinnedRows.ts +17 -0
  316. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/quickFilters.ts +41 -33
  317. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/recordLink.ts +24 -8
  318. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/tableCounts.ts +113 -0
  319. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/tableQuery.ts +27 -4
  320. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/views.ts +22 -6
  321. package/frontend-vue/layers/dms-ui/app/build/utils/addressSuggestions.ts +49 -0
  322. package/frontend-vue/layers/dms-ui/app/build/utils/eyebrow.ts +12 -0
  323. package/frontend-vue/layers/dms-ui/app/build/utils/fieldOptionsI18n.ts +51 -11
  324. package/frontend-vue/layers/dms-ui/app/build/utils/inputWidth.ts +53 -0
  325. package/frontend-vue/layers/dms-ui/app/build/utils/numberAdjustment.ts +6 -3
  326. package/frontend-vue/layers/dms-ui/app/build/utils/relativeDate.ts +19 -5
  327. package/frontend-vue/layers/dms-ui/app/build/utils/scrollWithin.ts +99 -0
  328. package/frontend-vue/layers/dms-ui/app/build/utils/urlVariables.ts +36 -14
  329. package/frontend-vue/layers/dms-ui/app/components/PasswordStrength.vue +1 -1
  330. package/frontend-vue/layers/dms-ui/app/components/RecursiveComponent.vue +9 -1
  331. package/frontend-vue/layers/dms-ui/app/components/blocks/LiveToggleBlock.vue +80 -32
  332. package/frontend-vue/layers/dms-ui/app/components/blocks/PageToolbarBlock.vue +137 -0
  333. package/frontend-vue/layers/dms-ui/app/components/blocks/StatGroupBlock.vue +5 -0
  334. package/frontend-vue/layers/dms-ui/app/components/chart/ApexChartHost.vue +16 -2
  335. package/frontend-vue/layers/dms-ui/app/components/chart/Chart.vue +24 -5
  336. package/frontend-vue/layers/dms-ui/app/components/chart/ChartCard.vue +41 -23
  337. package/frontend-vue/layers/dms-ui/app/components/confirm/ConfirmModal.vue +1 -1
  338. package/frontend-vue/layers/dms-ui/app/components/container-footer/ContainerFooter.vue +34 -0
  339. package/frontend-vue/layers/dms-ui/app/components/field-row/FieldRow.vue +25 -2
  340. package/frontend-vue/layers/dms-ui/app/components/flow-canvas/FlowCanvas.vue +2 -2
  341. package/frontend-vue/layers/dms-ui/app/components/form/Form.vue +118 -36
  342. package/frontend-vue/layers/dms-ui/app/components/form/components/DatePicker.vue +20 -1
  343. package/frontend-vue/layers/dms-ui/app/components/form/components/Display.vue +13 -1
  344. package/frontend-vue/layers/dms-ui/app/components/form/components/Image.vue +8 -3
  345. package/frontend-vue/layers/dms-ui/app/components/form/components/InputAddress.vue +53 -5
  346. package/frontend-vue/layers/dms-ui/app/components/form/components/InputCode.vue +29 -5
  347. package/frontend-vue/layers/dms-ui/app/components/form/components/InputNumber.vue +45 -8
  348. package/frontend-vue/layers/dms-ui/app/components/form/components/InputPercentage.vue +28 -5
  349. package/frontend-vue/layers/dms-ui/app/components/form/components/InputSecret.vue +5 -0
  350. package/frontend-vue/layers/dms-ui/app/components/form/components/InputText.vue +19 -2
  351. package/frontend-vue/layers/dms-ui/app/components/form/components/InputTime.vue +2 -1
  352. package/frontend-vue/layers/dms-ui/app/components/form/components/Relation.vue +205 -65
  353. package/frontend-vue/layers/dms-ui/app/components/form/components/Repeater.vue +80 -30
  354. package/frontend-vue/layers/dms-ui/app/components/form/components/RepeaterTable.vue +203 -0
  355. package/frontend-vue/layers/dms-ui/app/components/form/components/Select.vue +4 -2
  356. package/frontend-vue/layers/dms-ui/app/components/grid/GridRow.vue +11 -2
  357. package/frontend-vue/layers/dms-ui/app/components/key-value-list/KeyValueList.vue +79 -70
  358. package/frontend-vue/layers/dms-ui/app/components/kpi/KpiCard.vue +117 -21
  359. package/frontend-vue/layers/dms-ui/app/components/meter/Meter.vue +2 -1
  360. package/frontend-vue/layers/dms-ui/app/components/period/PeriodSelector.vue +113 -34
  361. package/frontend-vue/layers/dms-ui/app/components/save-bar/SaveBar.vue +14 -2
  362. package/frontend-vue/layers/dms-ui/app/components/section-header/Eyebrow.vue +6 -1
  363. package/frontend-vue/layers/dms-ui/app/components/segmented/Segmented.vue +13 -0
  364. package/frontend-vue/layers/dms-ui/app/components/stat-group/StatGroup.vue +52 -3
  365. package/frontend-vue/layers/dms-ui/app/components/tab/Tab.vue +2 -1
  366. package/frontend-vue/layers/dms-ui/app/components/table-view/KanbanBoard.vue +45 -35
  367. package/frontend-vue/layers/dms-ui/app/components/table-view/KanbanDisplay.vue +4 -1
  368. package/frontend-vue/layers/dms-ui/app/components/table-view/TableView.vue +410 -96
  369. package/frontend-vue/layers/dms-ui/app/components/top-list/TopListCard.vue +33 -5
  370. package/frontend-vue/layers/dms-ui/app/composables/chart/apexCategoryLabels.ts +68 -0
  371. package/frontend-vue/layers/dms-ui/app/composables/chart/apexDatetimeBuckets.ts +306 -0
  372. package/frontend-vue/layers/dms-ui/app/composables/chart/formatValue.ts +4 -3
  373. package/frontend-vue/layers/dms-ui/app/composables/chart/heatmapColors.ts +93 -0
  374. package/frontend-vue/layers/dms-ui/app/composables/chart/localizeSeries.ts +62 -0
  375. package/frontend-vue/layers/dms-ui/app/composables/chart/useApexChart.ts +116 -8
  376. package/frontend-vue/layers/dms-ui/app/composables/chart/useApexChart.types.ts +7 -0
  377. package/frontend-vue/layers/dms-ui/app/composables/containers/types.ts +84 -1
  378. package/frontend-vue/layers/dms-ui/app/composables/containers/useDrawer.ts +69 -8
  379. package/frontend-vue/layers/dms-ui/app/composables/form/types/field-saved.ts +11 -0
  380. package/frontend-vue/layers/dms-ui/app/composables/form/types/field.ts +4 -0
  381. package/frontend-vue/layers/dms-ui/app/composables/form/types/functions.ts +1 -0
  382. package/frontend-vue/layers/dms-ui/app/composables/form/useForm.ts +142 -21
  383. package/frontend-vue/layers/dms-ui/app/composables/live/usePageLive.ts +11 -4
  384. package/frontend-vue/layers/dms-ui/app/composables/page-toolbar/usePageToolbarSize.ts +30 -0
  385. package/frontend-vue/layers/dms-ui/app/composables/table-view/kanban.ts +4 -2
  386. package/frontend-vue/layers/dms-ui/app/composables/table-view/types/action-target.ts +22 -3
  387. package/frontend-vue/layers/dms-ui/app/composables/table-view/types/config.ts +4 -0
  388. package/frontend-vue/layers/dms-ui/app/composables/table-view/types/display.ts +24 -1
  389. package/frontend-vue/layers/dms-ui/app/plugins/closing-layers.client.ts +10 -0
  390. package/frontend-vue/layers/dms-ui/app/plugins/live-pause-source.ts +15 -0
  391. package/frontend-vue/layers/dms-ui/app/plugins/register.ts +2 -0
  392. package/frontend-vue/layers/dms-ui/app/types/input-number.ts +3 -2
  393. package/frontend-vue/layers/dms-ui/app/types/page-toolbar.ts +7 -0
  394. package/frontend-vue/layers/dms-ui/app/types/row-action.ts +0 -2
  395. package/frontend-vue/layers/dms-ui/app/types/row-navigation.ts +17 -0
  396. package/frontend-vue/layers/dms-ui/app/types/table/query.ts +2 -1
  397. package/frontend-vue/layers/dms-ui/app/utils/blockRefresh.ts +3 -1
  398. package/frontend-vue/layers/dms-ui/app/utils/componentTree.ts +24 -0
  399. package/frontend-vue/layers/dms-ui/i18n/locales/ui-en-GB.json +16 -4
  400. package/frontend-vue/layers/dms-ui/i18n/locales/ui-fr-FR.json +16 -4
  401. package/frontend-vue/package.json +2 -2
  402. package/package.json +7 -6
  403. package/skills/dms/SKILL.md +1 -1
  404. package/skills/dms-auth/SKILL.md +5 -3
  405. package/skills/dms-best-practices/REFERENCE.md +2 -0
  406. package/skills/dms-best-practices/SKILL.md +4 -0
  407. package/skills/dms-module/SKILL.md +3 -2
  408. package/skills/dms-pages/REFERENCE.md +125 -16
  409. package/skills/dms-pages/SKILL.md +1 -1
  410. package/skills/dms-project/DEV-SERVERS.md +23 -1
  411. package/skills/dms-project/REFERENCE.md +1 -1
  412. package/skills/dms-project/SKILL.md +4 -1
  413. package/skills/dms-project/scripts/dms-dev.sh +205 -17
  414. package/frontend-vue/layers/dms-core/app/composables/period/periodRenderNow.ts +0 -42
@@ -0,0 +1,73 @@
1
+ import { onMounted, shallowRef, watch } from "vue";
2
+ import type { DataTypeSelectSource } from "#dms-core/app/composables/data-types/useDataType";
3
+ import { unlabeledRelationIds } from "../data-types/relationValue";
4
+ import {
5
+ relationRowLabel,
6
+ relationRowValue,
7
+ useRelationRows,
8
+ } from "../data-types/relationRows";
9
+
10
+ /** A relation filter of a table: where its rows are read, and its value. */
11
+ export interface RelationFilterSource extends DataTypeSelectSource {
12
+ value: unknown;
13
+ }
14
+
15
+ const labelKey = (searchUrl: string, id: string) => `${searchUrl}\n${id}`;
16
+
17
+ // The plain ids a filter value holds, each once.
18
+ const NO_LABELS: ReadonlySet<string> = new Set();
19
+ const idsOf = (value: unknown) => unlabeledRelationIds(value, NO_LABELS);
20
+
21
+ /**
22
+ * The labels of the ids a table's relation filters hold: a chip names the
23
+ * related row ("Acme"), not the raw id it filters on ("cust-060"). Each id is
24
+ * read once from the relation's `select` route, as its picker does; an id the
25
+ * route does not answer (deleted, out of reach) keeps showing as is.
26
+ */
27
+ export function useRelationFilterLabels(sources: () => RelationFilterSource[]) {
28
+ const { readRelationRows } = useRelationRows();
29
+ const labels = shallowRef<Record<string, string>>({});
30
+ const requested = new Set<string>();
31
+
32
+ async function loadLabels(source: RelationFilterSource, ids: string[]) {
33
+ for (const id of ids) requested.add(labelKey(source.searchUrl, id));
34
+ const rows = await readRelationRows(source, ids);
35
+ const found: Record<string, string> = {};
36
+ for (const row of rows) {
37
+ const id = relationRowValue(source, row);
38
+ const label = relationRowLabel(source, row);
39
+ if (id === undefined || label === undefined) continue;
40
+ found[labelKey(source.searchUrl, String(id))] = String(label);
41
+ }
42
+ if (Object.keys(found).length) {
43
+ labels.value = { ...labels.value, ...found };
44
+ }
45
+ }
46
+
47
+ const pending = () =>
48
+ sources().flatMap((source) => {
49
+ const ids = idsOf(source.value).filter(
50
+ (id) => !requested.has(labelKey(source.searchUrl, id)),
51
+ );
52
+ return ids.length ? [{ source, ids }] : [];
53
+ });
54
+
55
+ // In the browser only: a server render draws the chips with their ids.
56
+ onMounted(() => {
57
+ watch(
58
+ pending,
59
+ (batches) => {
60
+ for (const { source, ids } of batches) void loadLabels(source, ids);
61
+ },
62
+ { immediate: true },
63
+ );
64
+ });
65
+
66
+ /** The labels of a relation filter's ids, in order; an unknown id as is. */
67
+ const labelsOf = (source: RelationFilterSource): string[] =>
68
+ idsOf(source.value).map(
69
+ (id) => labels.value[labelKey(source.searchUrl, id)] ?? id,
70
+ );
71
+
72
+ return { labelsOf };
73
+ }
@@ -0,0 +1,106 @@
1
+ import { watch, type Ref } from "vue";
2
+
3
+ /** The attribute naming a body row's id, which the anchor finds it by. */
4
+ export const ROW_ID_ATTRIBUTE = "data-row-id";
5
+
6
+ const SCROLLABLE_OVERFLOW = /(auto|scroll)/;
7
+
8
+ /** The row the reader has at the top of the view, before an update. */
9
+ interface RowAnchor {
10
+ id: string;
11
+ row: Element;
12
+ scroller: HTMLElement;
13
+ /** Where the row stood, in viewport pixels. */
14
+ top: number;
15
+ /** Its position among the rows. */
16
+ index: number;
17
+ previousIds: ReadonlySet<string>;
18
+ }
19
+
20
+ /** The element the table scrolls in: itself, or its nearest scrolled ancestor. */
21
+ function findScroller(start: HTMLElement): HTMLElement | null {
22
+ for (
23
+ let element: HTMLElement | null = start;
24
+ element;
25
+ element = element.parentElement
26
+ ) {
27
+ if (
28
+ SCROLLABLE_OVERFLOW.test(getComputedStyle(element).overflowY) &&
29
+ element.scrollHeight > element.clientHeight
30
+ )
31
+ return element;
32
+ }
33
+ return null;
34
+ }
35
+
36
+ function captureAnchor(
37
+ root: HTMLElement | null,
38
+ ids: readonly string[],
39
+ ): RowAnchor | null {
40
+ if (!root) return null;
41
+ const scroller = findScroller(root);
42
+ // At the top of the list, new rows land in view: what a live list is for.
43
+ if (!scroller || scroller.scrollTop <= 0) return null;
44
+ const viewTop = scroller.getBoundingClientRect().top;
45
+ for (const row of root.querySelectorAll(`tr[${ROW_ID_ATTRIBUTE}]`)) {
46
+ const top = row.getBoundingClientRect().top;
47
+ if (top < viewTop) continue;
48
+ const id = row.getAttribute(ROW_ID_ATTRIBUTE)!;
49
+ return {
50
+ id,
51
+ row,
52
+ scroller,
53
+ top,
54
+ index: ids.indexOf(id),
55
+ previousIds: new Set(ids),
56
+ };
57
+ }
58
+ return null;
59
+ }
60
+
61
+ // Rows were only added above the anchor (a live list's new rows): a sort, a
62
+ // filter or another page moves the reader on, and is left alone.
63
+ function isPrepend(anchor: RowAnchor, ids: readonly string[]): boolean {
64
+ const index = ids.indexOf(anchor.id);
65
+ if (anchor.index < 0 || index <= anchor.index) return false;
66
+ const added = ids
67
+ .slice(0, index)
68
+ .filter((id) => !anchor.previousIds.has(id)).length;
69
+ return added === index - anchor.index;
70
+ }
71
+
72
+ /**
73
+ * Keeps the rows the reader is looking at where they are when rows are added
74
+ * above them (a live list's new rows), by scrolling as far as the rows push
75
+ * them, in the frame they land. The browser's own scroll anchoring does not
76
+ * hold a table's rows. At the top of the list nothing is held: the new rows
77
+ * show, as a live list means them to.
78
+ *
79
+ * @param root The table, whose body rows carry `ROW_ID_ATTRIBUTE`
80
+ * @param rowIds The ids of the body rows, in order
81
+ */
82
+ export function useRowScrollAnchor(
83
+ root: Readonly<Ref<HTMLElement | null>>,
84
+ rowIds: () => readonly string[],
85
+ ): void {
86
+ if (typeof window === "undefined") return;
87
+ let anchor: RowAnchor | null = null;
88
+ watch(
89
+ rowIds,
90
+ (_next, previous) => {
91
+ anchor = captureAnchor(root.value, previous ?? []);
92
+ },
93
+ { flush: "pre" },
94
+ );
95
+ watch(
96
+ rowIds,
97
+ (next) => {
98
+ const held = anchor;
99
+ anchor = null;
100
+ if (!held || !held.row.isConnected || !isPrepend(held, next)) return;
101
+ const moved = held.row.getBoundingClientRect().top - held.top;
102
+ if (moved !== 0) held.scroller.scrollTop += moved;
103
+ },
104
+ { flush: "post" },
105
+ );
106
+ }
@@ -185,6 +185,15 @@ export const useTable = <T extends Data>(props: UseTableProps<T>) => {
185
185
  },
186
186
  );
187
187
 
188
+ // A display that cannot sort (a hand-ordered table) leaves every header
189
+ // without its sort button, arrow and menu items.
190
+ watch(
191
+ () => props.tableProps.sortingOptions?.enableSorting,
192
+ (enableSorting) => {
193
+ table.setOptions((prev) => ({ ...prev, enableSorting }));
194
+ },
195
+ );
196
+
188
197
  const labeledColumns: ComputedRef<LabeledColumn<T>[]> = computed(() => {
189
198
  return table
190
199
  .getAllColumns()
@@ -471,9 +471,8 @@ export const useTableColumns = <T extends Data>(config: ColumnConfig<T>) => {
471
471
 
472
472
  const isShowingArchived = () => config.showArchived?.value === true;
473
473
 
474
- // The backend serializes `isVisible`; `visible` is the former spelling.
475
474
  const isCustomActionInline = (action: CustomRowAction): boolean =>
476
- action.isVisible === true || action.visible === true;
475
+ action.isVisible === true;
477
476
 
478
477
  const { confirmAction } = useActionConfirm();
479
478
 
@@ -0,0 +1,31 @@
1
+ type Translate = (
2
+ key: string,
3
+ named: Record<string, unknown>,
4
+ plural: number,
5
+ ) => string;
6
+
7
+ /**
8
+ * The caption count while a table lists its archived rows. The default
9
+ * ("Archived: 13", "Archives : 13") agrees with no noun, so it reads right
10
+ * over invoices as over members; a table view's `archivedCountLabel` names
11
+ * its rows instead: a `$` key receiving `{ count }` and pluralized on it, or
12
+ * a text whose `{count}` is replaced.
13
+ */
14
+ export function formatArchivedCount(
15
+ translate: Translate,
16
+ count: number,
17
+ formattedCount: string,
18
+ label?: string,
19
+ ): string {
20
+ if (!label) {
21
+ return translate(
22
+ "dms.table.archived_count",
23
+ { count: formattedCount },
24
+ count,
25
+ );
26
+ }
27
+ if (label.startsWith("$")) {
28
+ return translate(label.slice(1), { count: formattedCount }, count);
29
+ }
30
+ return label.replaceAll("{count}", formattedCount);
31
+ }
@@ -0,0 +1,38 @@
1
+ import type {
2
+ DataType,
3
+ DataTypeFilterEditorValue,
4
+ } from "#dms-core/app/composables/data-types/useDataType";
5
+
6
+ // A view or tab declares its filter value as text (`TableViewTabFilter.value`),
7
+ // the shape the query sends. The chip's value editor wants the value its
8
+ // column's data type holds there ({@link DataType.filterEditorValue}). The
9
+ // editor reads the converted value; the filter keeps its text until the user
10
+ // edits it, so the query is unchanged.
11
+
12
+ const LIST_SEPARATOR = ",";
13
+ const BETWEEN_MODE = "is_between";
14
+
15
+ /** A number type's filter: the number a numeric text names, else the text. */
16
+ export const numberFilterEditorValue: DataTypeFilterEditorValue = (text) => {
17
+ const parsed = Number(text);
18
+ return text.trim() !== "" && Number.isFinite(parsed) ? parsed : text;
19
+ };
20
+
21
+ /** A date's filter: the two days of a range, a single day as written. */
22
+ export const dateFilterEditorValue: DataTypeFilterEditorValue = (text, mode) =>
23
+ mode === BETWEEN_MODE
24
+ ? text.split(LIST_SEPARATOR).map((item) => item.trim())
25
+ : text;
26
+
27
+ /**
28
+ * The value a filter's editor reads: a declared text value converted by its
29
+ * column's data type, any other value as it is.
30
+ */
31
+ export function filterEditorValue(
32
+ dataType: Pick<DataType, "filterEditorValue"> | undefined,
33
+ mode: string,
34
+ value: unknown,
35
+ ): unknown {
36
+ if (typeof value !== "string" || !dataType?.filterEditorValue) return value;
37
+ return dataType.filterEditorValue(value, mode);
38
+ }
@@ -6,6 +6,32 @@ interface FilterColumn {
6
6
  type?: DataTypeConfig;
7
7
  }
8
8
 
9
+ const DAY_MS = 24 * 60 * 60 * 1000;
10
+ // `{{now}}`, `{{now-7d}}`, `{{now+14d}}`: the tokens the server resolves in a
11
+ // filter value (`filter-tokens.ts`), read here with the same rule.
12
+ const NOW_TOKEN = /\{\{\s*now(?:\s*([+-])\s*(\d+)d)?\s*\}\}/g;
13
+ const BETWEEN_MODE = "is_between";
14
+ const BETWEEN_SEPARATOR = ",";
15
+
16
+ /** A filter value with its `{{now±Nd}}` tokens replaced by that instant. */
17
+ export function resolveNowTokens(value: string, now = Date.now()): string {
18
+ return value.replace(NOW_TOKEN, (_match, sign?: string, days?: string) => {
19
+ const offset = sign ? Number(`${sign}${days}`) * DAY_MS : 0;
20
+ return new Date(now + offset).toISOString();
21
+ });
22
+ }
23
+
24
+ /**
25
+ * The value as the formatters read it: tokens resolved, and an `is_between`
26
+ * bound pair written as text (`a,b`, the way the query sends it) split.
27
+ */
28
+ const readableValue = (filter: TableFilter): unknown => {
29
+ if (typeof filter.value !== "string") return filter.value;
30
+ const value = resolveNowTokens(filter.value);
31
+ if (filter.mode !== BETWEEN_MODE) return value;
32
+ return value.split(BETWEEN_SEPARATOR).map((bound) => bound.trim());
33
+ };
34
+
9
35
  export function formatFilterValue(
10
36
  filter: TableFilter,
11
37
  columns: FilterColumn[],
@@ -23,9 +49,14 @@ export function formatFilterValue(
23
49
  const formatter =
24
50
  dataType.formatter[filter.mode] || dataType.formatter.default;
25
51
 
52
+ // A filter's value need not have the column's shape (a tag typed against
53
+ // a tags column, whose formatter draws a list): a formatter that writes
54
+ // nothing for it leaves the value as typed.
26
55
  try {
27
- const result = formatter(filter.value, locale);
28
- return typeof result === "string" ? result : String(filter.value);
56
+ const result = formatter(readableValue(filter), locale);
57
+ return typeof result === "string" && result !== ""
58
+ ? result
59
+ : String(filter.value);
29
60
  } catch {
30
61
  return String(filter.value);
31
62
  }
@@ -0,0 +1,31 @@
1
+ /** Which sides of a scroll area hide content the reader can scroll to. */
2
+ export interface ScrollEdges {
3
+ start: boolean;
4
+ end: boolean;
5
+ }
6
+
7
+ /** What {@link horizontalScrollEdges} reads off a scroll area. */
8
+ export interface HorizontalScroll {
9
+ scrollLeft: number;
10
+ scrollWidth: number;
11
+ clientWidth: number;
12
+ }
13
+
14
+ // Sub-pixel widths leave a fraction of a pixel to scroll at either end.
15
+ const EDGE_TOLERANCE_PX = 1;
16
+
17
+ /**
18
+ * The sides of a table wider than its card that still hide columns: a phone
19
+ * shows the first columns, and nothing else would tell that the rest are a
20
+ * swipe away. The scroll position is read as the reader sees it, so a
21
+ * right-to-left page (negative `scrollLeft`) reads the same.
22
+ */
23
+ export function horizontalScrollEdges(area: HorizontalScroll): ScrollEdges {
24
+ const hidden = area.scrollWidth - area.clientWidth;
25
+ if (hidden <= EDGE_TOLERANCE_PX) return { start: false, end: false };
26
+ const scrolled = Math.abs(area.scrollLeft);
27
+ return {
28
+ start: scrolled > EDGE_TOLERANCE_PX,
29
+ end: scrolled < hidden - EDGE_TOLERANCE_PX,
30
+ };
31
+ }
@@ -0,0 +1,33 @@
1
+ import { get } from "@nuxt/ui/runtime/utils/index.js";
2
+ import { h, type VNodeChild } from "vue";
3
+ import type { TableViewColumn } from "../../../composables/table-view/types/column";
4
+ import { useColumnValueRenderer } from "../data-types/useColumnValueRenderer";
5
+
6
+ type Row = Record<string, unknown>;
7
+
8
+ /**
9
+ * Draws a row's fields on a card (the cards display, the kanban board) the
10
+ * way the grid draws its cells: through the column's `display`, else its data
11
+ * type, handed the row for what a display reads beside the value (a progress
12
+ * bar's total, a tone, an avatar).
13
+ */
14
+ export function useCardFields() {
15
+ const { renderColumnValue } = useColumnValueRenderer();
16
+
17
+ const renderField = (column: TableViewColumn, item: Row): VNodeChild => {
18
+ const rendered = renderColumnValue(column, item, "cell");
19
+ if (typeof rendered !== "string" && typeof rendered !== "number") {
20
+ return rendered;
21
+ }
22
+ const value = get(item, column.accessorKey);
23
+ // The fallback of a missing value reads dimmed, as an empty cell does.
24
+ const isMissing = value === null || value === undefined;
25
+ return h(
26
+ "span",
27
+ { class: isMissing ? "text-dimmed truncate" : "truncate" },
28
+ String(rendered),
29
+ );
30
+ };
31
+
32
+ return { renderField };
33
+ }
@@ -0,0 +1,48 @@
1
+ import { onMounted, type Ref } from "vue";
2
+ import type { TableViewViewStateConfig } from "../../../composables/table-view/types";
3
+
4
+ /** The viewports a table view opens on its `mobileDisplay`: a phone's. */
5
+ export const PHONE_VIEWPORT_QUERY = "(max-width: 767px)";
6
+
7
+ export interface MobileDisplayOptions {
8
+ /** The table view's `mobileDisplay`, if any. */
9
+ mobileDisplay: string | undefined;
10
+ /**
11
+ * The display the table counts as its default, starting at its resolved
12
+ * `defaultDisplay`: the phone's once applied, so it is not saved as the
13
+ * user's choice, and a view that sets no display opens on it.
14
+ */
15
+ defaultDisplay: Ref<string>;
16
+ /** The ids of the displays the table view offers. */
17
+ offered: ReadonlySet<string>;
18
+ /** The display shown. */
19
+ display: Ref<string>;
20
+ /**
21
+ * The view open, if any. One that fixes a display keeps it; one that
22
+ * leaves the display out (a `defaultView` showing every row) does not.
23
+ */
24
+ activeView: Ref<TableViewViewStateConfig | undefined>;
25
+ /**
26
+ * The display shown is someone's choice — the user saved one, the default
27
+ * display is the only way in: a phone keeps it.
28
+ */
29
+ isDisplayChosen: () => boolean;
30
+ }
31
+
32
+ /**
33
+ * Opens a phone on the table view's `mobileDisplay` once mounted, the
34
+ * server render having drawn the default display, when nobody chose one.
35
+ * Called after the table's views, so a view resolved once mounted is open.
36
+ */
37
+ export function useMobileDisplay(options: MobileDisplayOptions): void {
38
+ onMounted(() => {
39
+ const { mobileDisplay, offered } = options;
40
+ if (!mobileDisplay || !offered.has(mobileDisplay)) return;
41
+ if (options.isDisplayChosen()) return;
42
+ const viewDisplay = options.activeView.value?.display;
43
+ if (viewDisplay && offered.has(viewDisplay)) return;
44
+ if (!window.matchMedia?.(PHONE_VIEWPORT_QUERY).matches) return;
45
+ options.defaultDisplay.value = mobileDisplay;
46
+ options.display.value = mobileDisplay;
47
+ });
48
+ }
@@ -1,20 +1,7 @@
1
1
  import { onBeforeUnmount, onMounted, ref, type Ref } from "vue";
2
+ import { scrollParent } from "../../utils/scrollWithin";
2
3
 
3
4
  const FIRST_ROW_SELECTOR = "tbody tr";
4
- const SCROLLABLE_OVERFLOW = /(auto|scroll)/;
5
-
6
- function scrollParent(element: HTMLElement): HTMLElement | null {
7
- for (let node = element.parentElement; node; node = node.parentElement) {
8
- const { overflowY } = getComputedStyle(node);
9
- if (
10
- SCROLLABLE_OVERFLOW.test(overflowY) &&
11
- node.scrollHeight > node.clientHeight
12
- ) {
13
- return node;
14
- }
15
- }
16
- return null;
17
- }
18
5
 
19
6
  /**
20
7
  * Whether the first row of the table in `element` is out of view above the
@@ -29,6 +16,21 @@ export function isFirstRowScrolledPast(element: HTMLElement): boolean {
29
16
  return row.getBoundingClientRect().bottom <= viewTop;
30
17
  }
31
18
 
19
+ /**
20
+ * Whether the table in `element` is on screen or above it: its top stands
21
+ * above the bottom of the area it scrolls in (its scrolling parent, else the
22
+ * window). Rows added to such a table move rows the user can see, or did.
23
+ *
24
+ * @internal
25
+ */
26
+ export function isTableReached(element: HTMLElement): boolean {
27
+ const scroller = scrollParent(element);
28
+ const viewBottom = scroller
29
+ ? scroller.getBoundingClientRect().bottom
30
+ : window.innerHeight;
31
+ return element.getBoundingClientRect().top < viewBottom;
32
+ }
33
+
32
34
  /**
33
35
  * Follows whether the user scrolled the table's first row out of view, on
34
36
  * any scroll of the page (read once per frame). Inert when `isEnabled` is
@@ -40,13 +42,34 @@ export function useScrolledPastTop(
40
42
  element: () => HTMLElement | null,
41
43
  isEnabled: boolean,
42
44
  ): Ref<boolean> {
43
- const isScrolled = ref(false);
44
- if (!isEnabled || typeof window === "undefined") return isScrolled;
45
+ return useScrollMeasure(element, isEnabled, isFirstRowScrolledPast);
46
+ }
47
+
48
+ /**
49
+ * Follows whether the table is on screen or scrolled past (`isTableReached`),
50
+ * like `useScrolledPastTop`.
51
+ *
52
+ * @internal
53
+ */
54
+ export function useTableReached(
55
+ element: () => HTMLElement | null,
56
+ isEnabled: boolean,
57
+ ): Ref<boolean> {
58
+ return useScrollMeasure(element, isEnabled, isTableReached);
59
+ }
60
+
61
+ function useScrollMeasure(
62
+ element: () => HTMLElement | null,
63
+ isEnabled: boolean,
64
+ test: (element: HTMLElement) => boolean,
65
+ ): Ref<boolean> {
66
+ const isMatching = ref(false);
67
+ if (!isEnabled || typeof window === "undefined") return isMatching;
45
68
  let frame: number | null = null;
46
69
  const measure = () => {
47
70
  frame = null;
48
71
  const target = element();
49
- isScrolled.value = !!target && isFirstRowScrolledPast(target);
72
+ isMatching.value = !!target && test(target);
50
73
  };
51
74
  const onScroll = () => {
52
75
  frame ??= requestAnimationFrame(measure);
@@ -62,5 +85,5 @@ export function useScrolledPastTop(
62
85
  window.removeEventListener("scroll", onScroll, { capture: true });
63
86
  if (frame !== null) cancelAnimationFrame(frame);
64
87
  });
65
- return isScrolled;
88
+ return isMatching;
66
89
  }
@@ -1,3 +1,4 @@
1
+ import { readableNumberFormat } from "#dms-core/app/utils/formatter";
1
2
  import type { ComputedRef } from "vue";
2
3
  import type {
3
4
  TableViewColumn,
@@ -69,7 +70,7 @@ export function useTableFooter(options: TableFooterOptions) {
69
70
  ) => {
70
71
  if (value === undefined) return undefined;
71
72
  if (summary.format) {
72
- return new Intl.NumberFormat(locale.value, summary.format).format(value);
73
+ return readableNumberFormat(locale.value, summary.format).format(value);
73
74
  }
74
75
  const column =
75
76
  summary.op === "sum"
@@ -77,7 +78,7 @@ export function useTableFooter(options: TableFooterOptions) {
77
78
  : undefined;
78
79
  return column
79
80
  ? renderValue(column, value, undefined)
80
- : new Intl.NumberFormat(locale.value).format(value);
81
+ : readableNumberFormat(locale.value).format(value);
81
82
  };
82
83
 
83
84
  const legendColumn = columns.find(
@@ -0,0 +1,78 @@
1
+ import { router as inertiaRouter } from "@inertiajs/vue3";
2
+ import { onBeforeUnmount, onMounted } from "vue";
3
+ import type { QueryParamFilters } from "../../../composables/table-view/types/config";
4
+ import { isLinkedVisit } from "./utils/linkedArrival";
5
+ import { queryOfUrl } from "./utils/urlFilters";
6
+ import type { TableUrlScope } from "./utils/views";
7
+
8
+ const URL_BASE = "http://dms.invalid";
9
+
10
+ /** What `useTableLinkedVisit` reads a visit against, and what it calls. */
11
+ export interface TableLinkedVisitOptions {
12
+ route: { path: string; query: Record<string, unknown> };
13
+ urlScope: TableUrlScope;
14
+ queryParamFilters: QueryParamFilters | undefined;
15
+ activeView: () => string | undefined;
16
+ activeTab: () => string;
17
+ canOpenView: (id: string) => boolean;
18
+ canOpenTab: (id: string) => boolean;
19
+ /** Opens the table on what the visit's link names, as an arrival would. */
20
+ follow: (query: Record<string, unknown>) => void;
21
+ }
22
+
23
+ /**
24
+ * Follows a link to the page the table is already shown on (a notification,
25
+ * a menu entry, back and forward): Inertia keeps the page mounted, so the
26
+ * table reads the visit's query as an arrival would (`isLinkedVisit`). A
27
+ * link clicked is announced as a client visit pushed, even to the URL shown,
28
+ * which Inertia turns into a replace without a `navigate` event; a page
29
+ * control rebuilding the query replaces it, and is no link. A visit to
30
+ * another page is left alone: the table is about to unmount.
31
+ */
32
+ export function useTableLinkedVisit(options: TableLinkedVisitOptions): void {
33
+ // The page the table is shown on, as the visit that mounted it reached it.
34
+ const pathname = options.route.path;
35
+ let previousQuery: Record<string, unknown> = { ...options.route.query };
36
+ // A pushed visit announces itself twice when it changes the URL.
37
+ let followedVisitId: string | undefined;
38
+ const stops: Array<() => void> = [];
39
+
40
+ const read = (
41
+ url: string,
42
+ visitId: string | undefined,
43
+ isLinkClick: boolean,
44
+ ) => {
45
+ const query = queryOfUrl(url);
46
+ const lastQuery = previousQuery;
47
+ previousQuery = query;
48
+ if (new URL(url, URL_BASE).pathname !== pathname) return;
49
+ if (visitId !== undefined && visitId === followedVisitId) return;
50
+ const isLink = isLinkedVisit({
51
+ query,
52
+ isLinkClick,
53
+ previousQuery: lastQuery,
54
+ urlScope: options.urlScope,
55
+ queryParamFilters: options.queryParamFilters,
56
+ activeView: options.activeView(),
57
+ activeTab: options.activeTab(),
58
+ canOpenView: options.canOpenView,
59
+ canOpenTab: options.canOpenTab,
60
+ });
61
+ if (!isLink) return;
62
+ followedVisitId = visitId;
63
+ options.follow(query);
64
+ };
65
+
66
+ onMounted(() => {
67
+ stops.push(
68
+ inertiaRouter.on("navigate", (event) =>
69
+ read(event.detail.page.url, event.detail.visitId, false),
70
+ ),
71
+ inertiaRouter.on("clientVisit", (event) => {
72
+ if (event.detail.replace) return;
73
+ read(event.detail.page.url, event.detail.visitId, true);
74
+ }),
75
+ );
76
+ });
77
+ onBeforeUnmount(() => stops.forEach((stop) => stop()));
78
+ }