@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,410 @@
1
+ import {
2
+ isUnauthorizedStreamError,
3
+ openSseStream,
4
+ type SseStreamHandle,
5
+ } from "../../composables/realtime/useSseStream";
6
+ import {
7
+ browserCrossTabEnvironment,
8
+ type CrossTabEnvironment,
9
+ electLeader,
10
+ holdLock,
11
+ openTabBus,
12
+ type TabBus,
13
+ tabLockName,
14
+ whenReleased,
15
+ } from "./crossTab";
16
+
17
+ /**
18
+ * The realtime connection one browser shares between its tabs: the elected tab
19
+ * holds `GET /api/realtime/connection` and relays it, each tab opens its own
20
+ * session on it. See `crossTab.ts` for why.
21
+ *
22
+ * @internal
23
+ */
24
+
25
+ const HELLO_EVENT = "hello";
26
+ const SESSION_EVENT = "session";
27
+ const SESSION_HEADER = "x-realtime-session";
28
+ const CONNECTION_HEADER = "x-realtime-connection";
29
+ const RECONNECT_INITIAL_DELAY_MS = 500;
30
+ const RECONNECT_MAX_DELAY_MS = 30_000;
31
+ const RECONNECT_MULTIPLIER = 2;
32
+
33
+ interface ConnectionMessage {
34
+ kind: "connection";
35
+ /** `null` while the leader has no stream open. */
36
+ connectionId: string | null;
37
+ }
38
+
39
+ interface FrameMessage {
40
+ kind: "frame";
41
+ connectionId: string;
42
+ /** Unset on a frame of the connection itself, which every tab receives. */
43
+ sessionId?: string;
44
+ event: string;
45
+ data: unknown;
46
+ }
47
+
48
+ /** A tab joining asks the leader for the connection it holds. */
49
+ interface QueryMessage {
50
+ kind: "query";
51
+ }
52
+
53
+ /** A tab's session on the connection, for the leader to end once it goes. */
54
+ interface SessionMessage {
55
+ kind: "session";
56
+ connectionId: string;
57
+ sessionId: string;
58
+ tabId: string;
59
+ }
60
+
61
+ /**
62
+ * A tab whose session request the backend answered without knowing the
63
+ * connection asks the leader to open a new one.
64
+ */
65
+ interface ReopenMessage {
66
+ kind: "reopen";
67
+ connectionId: string;
68
+ }
69
+
70
+ type HubMessage =
71
+ | ConnectionMessage
72
+ | FrameMessage
73
+ | QueryMessage
74
+ | SessionMessage
75
+ | ReopenMessage;
76
+
77
+ export interface StreamRecovery {
78
+ refreshSession: () => Promise<boolean>;
79
+ redirectToAuth: () => Promise<void>;
80
+ }
81
+
82
+ export interface SharedRealtimeConnectionOptions extends StreamRecovery {
83
+ /** Scopes the election and the relay: one connection per name. */
84
+ name: string;
85
+ streamUrl: string;
86
+ sessionCloseUrl: string;
87
+ /** The connection to open a session on; `undefined` while there is none. */
88
+ onConnection: (connectionId: string | undefined) => void;
89
+ /**
90
+ * A frame of one tab's session, or of the connection itself (the dev
91
+ * reload signal) when `sessionId` is unset.
92
+ */
93
+ onFrame: (
94
+ sessionId: string | undefined,
95
+ eventName: string,
96
+ data: unknown,
97
+ ) => void;
98
+ environment?: CrossTabEnvironment;
99
+ }
100
+
101
+ export interface SharedRealtimeConnection {
102
+ /** Hands this tab's session to the leader, which ends it once the tab goes. */
103
+ announceSession: (connectionId: string, sessionId: string) => void;
104
+ /**
105
+ * Asks the leader to replace `connectionId` with a new stream: the backend
106
+ * answered a session request on it as a connection it does not know.
107
+ */
108
+ reopen: (connectionId: string) => void;
109
+ leave: () => void;
110
+ }
111
+
112
+ export interface Reconnector {
113
+ isCancelled(): boolean;
114
+ schedule(open: () => void): void;
115
+ resetDelay(): void;
116
+ cancel(): void;
117
+ }
118
+
119
+ export const createReconnector = (): Reconnector => {
120
+ let isCancelled = false;
121
+ let delay = RECONNECT_INITIAL_DELAY_MS;
122
+ let timer: ReturnType<typeof setTimeout> | undefined;
123
+ return {
124
+ isCancelled: () => isCancelled,
125
+ schedule: (open) => {
126
+ // A pending reconnect absorbs concurrent triggers: one attempt at a time.
127
+ if (isCancelled || timer) return;
128
+ timer = setTimeout(() => {
129
+ timer = undefined;
130
+ open();
131
+ }, delay);
132
+ delay = Math.min(delay * RECONNECT_MULTIPLIER, RECONNECT_MAX_DELAY_MS);
133
+ },
134
+ resetDelay: () => {
135
+ delay = RECONNECT_INITIAL_DELAY_MS;
136
+ },
137
+ cancel: () => {
138
+ isCancelled = true;
139
+ if (timer) clearTimeout(timer);
140
+ timer = undefined;
141
+ },
142
+ };
143
+ };
144
+
145
+ /** Retries after a failure, refreshing the browser session first on a 401. */
146
+ export const recoverFromError = (
147
+ error: unknown,
148
+ retry: () => void,
149
+ recovery: StreamRecovery,
150
+ ): void => {
151
+ if (!isUnauthorizedStreamError(error)) {
152
+ retry();
153
+ return;
154
+ }
155
+ void recovery.refreshSession().then((isRefreshed) => {
156
+ if (isRefreshed) {
157
+ retry();
158
+ } else {
159
+ void recovery.redirectToAuth();
160
+ }
161
+ }, retry);
162
+ };
163
+
164
+ interface StreamCallbacks extends StreamRecovery {
165
+ onEvent: (eventName: string, data: unknown) => void;
166
+ onDisconnected: () => void;
167
+ }
168
+
169
+ interface KeptStream {
170
+ /** Drops the open stream and opens a new one. */
171
+ reopen: () => void;
172
+ stop: () => void;
173
+ }
174
+
175
+ /**
176
+ * Keeps one stream open until cancelled. The server ends the stream when it
177
+ * stops (a dev reload included), so a clean EOF reconnects just like an error
178
+ * does: the stream has no business completion.
179
+ */
180
+ const connectWithBackoff = (
181
+ url: string,
182
+ callbacks: StreamCallbacks,
183
+ ): KeptStream => {
184
+ const reconnector = createReconnector();
185
+ let activeHandle: SseStreamHandle | undefined;
186
+ const open = () => {
187
+ if (reconnector.isCancelled()) return;
188
+ activeHandle?.close();
189
+ const handle: SseStreamHandle = openSseStream({
190
+ url,
191
+ onEvent: (eventName, data) => {
192
+ if (handle !== activeHandle) return;
193
+ reconnector.resetDelay();
194
+ callbacks.onEvent(eventName, data);
195
+ },
196
+ onError: (error) => {
197
+ if (!retire(handle)) return;
198
+ recoverFromError(error, reconnect, callbacks);
199
+ },
200
+ onClose: (reason) => {
201
+ if (reason !== "eof" || !retire(handle)) return;
202
+ reconnect();
203
+ },
204
+ });
205
+ activeHandle = handle;
206
+ };
207
+ const reconnect = () => reconnector.schedule(open);
208
+ const retire = (handle: SseStreamHandle): boolean => {
209
+ if (handle !== activeHandle || reconnector.isCancelled()) return false;
210
+ activeHandle = undefined;
211
+ callbacks.onDisconnected();
212
+ return true;
213
+ };
214
+ open();
215
+ return {
216
+ reopen: () => {
217
+ const handle = activeHandle;
218
+ if (!handle || !retire(handle)) return;
219
+ handle.close();
220
+ reconnect();
221
+ },
222
+ stop: () => {
223
+ reconnector.cancel();
224
+ activeHandle?.close();
225
+ activeHandle = undefined;
226
+ },
227
+ };
228
+ };
229
+
230
+ const readConnectionId = (data: unknown): string | null => {
231
+ const connectionId = (data as { connectionId?: unknown } | null)
232
+ ?.connectionId;
233
+ return typeof connectionId === "string" ? connectionId : null;
234
+ };
235
+
236
+ interface SessionFrame {
237
+ sessionId?: unknown;
238
+ event?: unknown;
239
+ data?: unknown;
240
+ }
241
+
242
+ interface LeaderContext {
243
+ options: SharedRealtimeConnectionOptions;
244
+ environment: CrossTabEnvironment;
245
+ bus: TabBus;
246
+ tabId: string;
247
+ }
248
+
249
+ const closeSession = (
250
+ url: string,
251
+ connectionId: string,
252
+ sessionId: string,
253
+ ): void => {
254
+ void fetch(url, {
255
+ method: "POST",
256
+ headers: { [CONNECTION_HEADER]: connectionId, [SESSION_HEADER]: sessionId },
257
+ credentials: "include",
258
+ }).catch(() => {});
259
+ };
260
+
261
+ /**
262
+ * The elected tab's share: the stream, relayed on the bus, and the sessions
263
+ * of the other tabs, each ended once its tab is gone.
264
+ */
265
+ const lead = ({ options, environment, bus, tabId }: LeaderContext) => {
266
+ const watchers = new AbortController();
267
+ const sessionsByTab = new Map<string, Set<string>>();
268
+ let connectionId: string | null = null;
269
+
270
+ const publish = (next: string | null) => {
271
+ connectionId = next;
272
+ bus.post({ kind: "connection", connectionId: next });
273
+ };
274
+
275
+ const endSessionsOf = (otherTabId: string) => {
276
+ const sessionIds = sessionsByTab.get(otherTabId) ?? new Set<string>();
277
+ sessionsByTab.delete(otherTabId);
278
+ // The sessions of a connection that went ended with it.
279
+ if (!connectionId) return;
280
+ for (const sessionId of sessionIds) {
281
+ closeSession(options.sessionCloseUrl, connectionId, sessionId);
282
+ }
283
+ };
284
+
285
+ const watchSession = (message: SessionMessage) => {
286
+ // This tab's own sessions end with the stream it holds.
287
+ if (message.tabId === tabId || message.connectionId !== connectionId) {
288
+ return;
289
+ }
290
+ const known = sessionsByTab.get(message.tabId);
291
+ if (known) {
292
+ known.add(message.sessionId);
293
+ return;
294
+ }
295
+ sessionsByTab.set(message.tabId, new Set([message.sessionId]));
296
+ whenReleased(
297
+ tabLockName(options.name, message.tabId),
298
+ environment,
299
+ () => endSessionsOf(message.tabId),
300
+ watchers.signal,
301
+ );
302
+ };
303
+
304
+ const unlisten = bus.listen((raw) => {
305
+ const message = raw as HubMessage;
306
+ if (message.kind === "query")
307
+ bus.post({ kind: "connection", connectionId });
308
+ if (message.kind === "session") watchSession(message);
309
+ if (message.kind === "reopen" && message.connectionId === connectionId) {
310
+ stream.reopen();
311
+ }
312
+ });
313
+
314
+ // The tabs may still hold the connection of a leader that went: they drop it.
315
+ publish(null);
316
+ const stream = connectWithBackoff(options.streamUrl, {
317
+ onEvent: (eventName, data) => {
318
+ if (eventName === HELLO_EVENT) {
319
+ sessionsByTab.clear();
320
+ publish(readConnectionId(data));
321
+ return;
322
+ }
323
+ if (!connectionId) return;
324
+ if (eventName !== SESSION_EVENT) {
325
+ bus.post({ kind: "frame", connectionId, event: eventName, data });
326
+ return;
327
+ }
328
+ const frame = data as SessionFrame | null;
329
+ if (typeof frame?.sessionId !== "string") return;
330
+ if (typeof frame.event !== "string") return;
331
+ bus.post({
332
+ kind: "frame",
333
+ connectionId,
334
+ sessionId: frame.sessionId,
335
+ event: frame.event,
336
+ data: frame.data,
337
+ });
338
+ },
339
+ onDisconnected: () => publish(null),
340
+ refreshSession: options.refreshSession,
341
+ redirectToAuth: options.redirectToAuth,
342
+ });
343
+
344
+ return () => {
345
+ watchers.abort();
346
+ unlisten();
347
+ stream.stop();
348
+ publish(null);
349
+ };
350
+ };
351
+
352
+ const createTabId = (): string =>
353
+ globalThis.crypto?.randomUUID?.() ??
354
+ `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
355
+
356
+ /**
357
+ * Joins the browser's realtime connection for `options.name`, electing this
358
+ * tab to hold it when no other tab does.
359
+ */
360
+ export function joinSharedRealtimeConnection(
361
+ options: SharedRealtimeConnectionOptions,
362
+ ): SharedRealtimeConnection {
363
+ const environment = options.environment ?? browserCrossTabEnvironment();
364
+ const tabId = createTabId();
365
+ const bus = openTabBus(options.name, environment);
366
+ // Held before any session is announced: the leader waits on it to learn
367
+ // that this tab went.
368
+ const tabLock = holdLock(tabLockName(options.name, tabId), environment);
369
+ let connectionId: string | undefined;
370
+
371
+ const unlisten = bus.listen((raw) => {
372
+ const message = raw as HubMessage;
373
+ if (message.kind === "connection") {
374
+ const next = message.connectionId ?? undefined;
375
+ if (next === connectionId) return;
376
+ connectionId = next;
377
+ options.onConnection(next);
378
+ return;
379
+ }
380
+ if (message.kind === "frame" && message.connectionId === connectionId) {
381
+ options.onFrame(message.sessionId, message.event, message.data);
382
+ }
383
+ });
384
+
385
+ const resign = electLeader(options.name, environment, () =>
386
+ lead({ options, environment, bus, tabId }),
387
+ );
388
+ bus.post({ kind: "query" });
389
+
390
+ return {
391
+ announceSession: (sessionConnectionId, sessionId) => {
392
+ void tabLock.held.then(() =>
393
+ bus.post({
394
+ kind: "session",
395
+ connectionId: sessionConnectionId,
396
+ sessionId,
397
+ tabId,
398
+ }),
399
+ );
400
+ },
401
+ reopen: (reopenedConnectionId) =>
402
+ bus.post({ kind: "reopen", connectionId: reopenedConnectionId }),
403
+ leave: () => {
404
+ resign();
405
+ unlisten();
406
+ tabLock.release();
407
+ bus.close();
408
+ },
409
+ };
410
+ }
@@ -6,7 +6,8 @@ import type {
6
6
  UseDmsPagedDataOptions,
7
7
  UseFetchOptions,
8
8
  } from "#dms/frontend-module";
9
- import type { MaybeRefOrGetter } from "vue";
9
+ import { getCurrentInstance, toValue, watch, type MaybeRefOrGetter } from "vue";
10
+ import { useLivePauseSource } from "../live/livePauseSource";
10
11
 
11
12
  /** The authenticated fetch `useAuthFetch` returns. */
12
13
  type AuthFetch = ReturnType<typeof useAuthFetch>["$authFetch"];
@@ -61,7 +62,9 @@ export type ApiDataResult<T> = DmsAsyncDataResult<T>;
61
62
  * - `refreshInterval: ms` reads again on an interval while the caller shows
62
63
  * the data and the page is visible: polling. A ref or a getter changes it,
63
64
  * and `0` or `null` stops it; a call from a plugin polls for as long as
64
- * the application runs.
65
+ * the application runs. On a page declared with `live`, a component's
66
+ * polling stops while the page is paused, and reads once on resume if a
67
+ * poll fell due meanwhile.
65
68
  * - The page's refreshes (`refreshPageBlocks()`) and the events of its
66
69
  * realtime topics read the data again; `refreshOnPageRefresh: false` opts
67
70
  * out, for data with refresh triggers of its own.
@@ -88,15 +91,74 @@ export function useApiData<T>(
88
91
  ): ApiDataResult<T> {
89
92
  const { $authFetch } = useAuthFetch();
90
93
  if (typeof handlerOrOptions === "function")
91
- return useDmsAsyncData<T>(
92
- source,
93
- ({ signal }) => handlerOrOptions({ $authFetch, signal }),
94
+ return followLivePause(
95
+ useDmsAsyncData<T>(
96
+ source,
97
+ ({ signal }) => handlerOrOptions({ $authFetch, signal }),
98
+ gateLivePolling(options),
99
+ ),
94
100
  options,
95
101
  );
96
- return useDmsFetch<T>(source, {
97
- ...handlerOrOptions,
98
- $fetch: $authFetch as UseFetchOptions<T>["$fetch"],
102
+ return followLivePause(
103
+ useDmsFetch<T>(source, {
104
+ ...gateLivePolling(handlerOrOptions),
105
+ $fetch: $authFetch as UseFetchOptions<T>["$fetch"],
106
+ }),
107
+ handlerOrOptions,
108
+ );
109
+ }
110
+
111
+ /** The options of a read that polls, as far as its live gate goes. */
112
+ interface PolledReadOptions {
113
+ refreshInterval?: MaybeRefOrGetter<number | null | undefined>;
114
+ immediate?: boolean;
115
+ }
116
+
117
+ // A component's polling is one more read nobody asked for: on a live page it
118
+ // stops while the page is paused, as the blocks' refetches do. A plugin's
119
+ // polling (the shell's, outside any page) keeps running. The pause is read
120
+ // through the app's `LIVE_PAUSE_SOURCE`, which the UI layer provides.
121
+ function isGatedPolling(
122
+ options: PolledReadOptions | undefined,
123
+ ): options is PolledReadOptions {
124
+ return options?.refreshInterval !== undefined && !!getCurrentInstance();
125
+ }
126
+
127
+ /** `options` with a `refreshInterval` that is `null` while the page is paused. */
128
+ function gateLivePolling<O extends PolledReadOptions>(
129
+ options: O | undefined,
130
+ ): O | undefined {
131
+ if (!isGatedPolling(options)) return options;
132
+ const interval = options.refreshInterval;
133
+ const isPaused = useLivePauseSource();
134
+ return {
135
+ ...options,
136
+ refreshInterval: () => (isPaused() ? null : toValue(interval)),
137
+ };
138
+ }
139
+
140
+ /**
141
+ * Reads once as the page resumes when a poll fell due while it was paused:
142
+ * the polling restarts with a full interval, which would show the data of
143
+ * before the pause that long.
144
+ */
145
+ function followLivePause<
146
+ R extends Pick<DmsAsyncDataResult<unknown>, "status" | "refresh">,
147
+ >(result: R, options: PolledReadOptions | undefined): R {
148
+ if (typeof window === "undefined" || !isGatedPolling(options)) return result;
149
+ let pausedAt = 0;
150
+ watch(useLivePauseSource(), (isPaused) => {
151
+ if (isPaused) {
152
+ pausedAt = Date.now();
153
+ return;
154
+ }
155
+ const period = toValue(options.refreshInterval);
156
+ if (!period || period <= 0 || Date.now() - pausedAt < period) return;
157
+ // Data a caller waits to `execute()` is not read before it is.
158
+ if (result.status.value === "idle" && options.immediate === false) return;
159
+ void result.refresh({ dedupe: "defer" });
99
160
  });
161
+ return result;
100
162
  }
101
163
 
102
164
  /** What a handler reading one page of API data receives. */
@@ -139,10 +201,13 @@ export function useApiPagedData<TPage, TCursor = unknown>(
139
201
  options: UseDmsPagedDataOptions<TPage, TCursor>,
140
202
  ): ApiPagedDataResult<TPage, TCursor> {
141
203
  const { $authFetch } = useAuthFetch();
142
- return useDmsPagedData<TPage, TCursor>(
143
- key,
144
- ({ cursor, index, signal }) =>
145
- handler({ $authFetch, cursor, index, signal }),
204
+ return followLivePause(
205
+ useDmsPagedData<TPage, TCursor>(
206
+ key,
207
+ ({ cursor, index, signal }) =>
208
+ handler({ $authFetch, cursor, index, signal }),
209
+ gateLivePolling(options) ?? options,
210
+ ),
146
211
  options,
147
212
  );
148
213
  }
@@ -36,6 +36,24 @@ export type DataTypeValueComparer = (
36
36
  options?: unknown,
37
37
  ) => boolean;
38
38
 
39
+ /**
40
+ * The value a filter chip's editor holds for a filter value a view declared
41
+ * as text (`"0"`, `"2026-10-01,2026-10-15"`), under a compare mode: a number
42
+ * input refuses `"0"`, a date range wants its two days. The filter itself
43
+ * keeps its text, so the query is unchanged.
44
+ */
45
+ export type DataTypeFilterEditorValue = (text: string, mode: string) => unknown;
46
+
47
+ /**
48
+ * Where the rows a value names are read: the `select` route of the table a
49
+ * relation points to, and the keys its rows carry their label and value under
50
+ * (`label` and `value` unless mapped).
51
+ */
52
+ export interface DataTypeSelectSource {
53
+ searchUrl: string;
54
+ keyMapping?: { label?: string; value?: string };
55
+ }
56
+
39
57
  export interface DataType {
40
58
  id: string;
41
59
  formatter?: RecordWithDefault<DataTypeFormatter>;
@@ -46,6 +64,14 @@ export interface DataType {
46
64
  * or whose order does not count. Without it, values compare deeply.
47
65
  */
48
66
  isSameValue?: DataTypeValueComparer;
67
+ /** Without it, a filter's editor reads a declared text value as is. */
68
+ filterEditorValue?: DataTypeFilterEditorValue;
69
+ /**
70
+ * For a type whose value holds the ids of rows another table serves (a
71
+ * relation): where those rows are read, from the options of the type's
72
+ * control. A filter chip or a read-only list names the ids through it.
73
+ */
74
+ selectSource?: (options: unknown) => DataTypeSelectSource | undefined;
49
75
  displayComponent?: Component;
50
76
  }
51
77
  const dataTypes: Record<string, DataType> = {};
@@ -0,0 +1,29 @@
1
+ import { getCurrentInstance } from "vue";
2
+
3
+ /**
4
+ * Whether the component setting up or rendering now is hydrating the
5
+ * server's HTML.
6
+ *
7
+ * Vue binds a hydrating component's vnode to its server-rendered node before
8
+ * its setup runs, where a fresh mount has no node until it is mounted; once
9
+ * mounted, the component no longer hydrates. So this is true from the setup
10
+ * to the first render of a component the browser takes over from the server,
11
+ * and false in a client navigation, on a later re-render, on the server, and
12
+ * outside a component.
13
+ *
14
+ * Read it where the browser must draw what the server drew: a part drawn
15
+ * in the browser only, or a value only the browser knows (local storage, the
16
+ * clock), waits until mounted while hydrating, and shows at once anywhere
17
+ * else. Call it before the setup's first `await`, while the component is
18
+ * still Vue's current instance. Data needs no check: `useApiData` hands the
19
+ * server's answer to the browser on its own.
20
+ *
21
+ * ```ts
22
+ * const isDrawn = ref(!import.meta.env.SSR && !isHydratingComponent());
23
+ * onMounted(() => (isDrawn.value = true));
24
+ * ```
25
+ */
26
+ export function isHydratingComponent(): boolean {
27
+ const instance = getCurrentInstance();
28
+ return !!instance && !instance.isMounted && !!instance.vnode.el;
29
+ }
@@ -0,0 +1,29 @@
1
+ import { inject, type InjectionKey } from "vue";
2
+
3
+ /**
4
+ * Whether the page on screen is paused right now (by the user, a hold or a
5
+ * hidden tab). Reactive: it reads reactive state, so a `watch` or a
6
+ * `computed` over it follows the pause.
7
+ */
8
+ export type LivePauseSource = () => boolean;
9
+
10
+ /**
11
+ * The extension point the core layer reads a page's pause through. The core
12
+ * owns `useApiData` but not the page's live controller: the layer that owns
13
+ * the controller provides a source at the app level
14
+ * (`app.provide(LIVE_PAUSE_SOURCE, source)`), so every component reads the
15
+ * same page, drawers and modals included. Without one, nothing is paused.
16
+ */
17
+ export const LIVE_PAUSE_SOURCE: InjectionKey<LivePauseSource> = Symbol(
18
+ "dms-live-pause-source",
19
+ );
20
+
21
+ const NEVER_PAUSED: LivePauseSource = () => false;
22
+
23
+ /**
24
+ * The pause source of the app the calling component belongs to; never
25
+ * paused without one. Call it in a component setup.
26
+ */
27
+ export function useLivePauseSource(): LivePauseSource {
28
+ return inject(LIVE_PAUSE_SOURCE, NEVER_PAUSED);
29
+ }