@antelopejs/dms 0.9.8 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (451) hide show
  1. package/CHANGELOG.md +171 -0
  2. package/README.md +1 -1
  3. package/data/module-store/catalog.json +2 -2
  4. package/dist/attachments/table-view.js +2 -2
  5. package/dist/attachments/table-view.js.map +1 -1
  6. package/dist/automation/actions.d.ts +2 -0
  7. package/dist/automation/actions.js +116 -71
  8. package/dist/automation/actions.js.map +1 -1
  9. package/dist/automation/i18n.d.ts +4 -0
  10. package/dist/automation/i18n.js +19 -0
  11. package/dist/automation/i18n.js.map +1 -0
  12. package/dist/automation/send-notification.d.ts +21 -0
  13. package/dist/automation/send-notification.js +163 -0
  14. package/dist/automation/send-notification.js.map +1 -0
  15. package/dist/automation/triggers.js +19 -25
  16. package/dist/automation/triggers.js.map +1 -1
  17. package/dist/client-origin-cors.d.ts +1 -0
  18. package/dist/client-origin-cors.js +19 -0
  19. package/dist/client-origin-cors.js.map +1 -0
  20. package/dist/config.d.ts +3 -2
  21. package/dist/config.js +5 -7
  22. package/dist/config.js.map +1 -1
  23. package/dist/frontend-module.d.ts +4 -0
  24. package/dist/frontend-module.js +37 -0
  25. package/dist/frontend-module.js.map +1 -0
  26. package/dist/implementations/dms/client-ip.js +1 -1
  27. package/dist/implementations/dms/client-ip.js.map +1 -1
  28. package/dist/implementations/dms/dev-reload.d.ts +5 -0
  29. package/dist/implementations/dms/dev-reload.js +23 -3
  30. package/dist/implementations/dms/dev-reload.js.map +1 -1
  31. package/dist/implementations/dms/frontend-modules.d.ts +2 -4
  32. package/dist/implementations/dms/frontend-modules.js +38 -94
  33. package/dist/implementations/dms/frontend-modules.js.map +1 -1
  34. package/dist/implementations/dms/frontend-ready.d.ts +12 -2
  35. package/dist/implementations/dms/frontend-ready.js +34 -4
  36. package/dist/implementations/dms/frontend-ready.js.map +1 -1
  37. package/dist/implementations/dms/layout-banners.d.ts +2 -1
  38. package/dist/implementations/dms/layout-banners.js +9 -0
  39. package/dist/implementations/dms/layout-banners.js.map +1 -1
  40. package/dist/implementations/dms/manifest.d.ts +7 -2
  41. package/dist/implementations/dms/manifest.js +12 -7
  42. package/dist/implementations/dms/manifest.js.map +1 -1
  43. package/dist/implementations/dms/page.d.ts +4 -3
  44. package/dist/implementations/dms/page.js +49 -18
  45. package/dist/implementations/dms/page.js.map +1 -1
  46. package/dist/implementations/dms/permissions-resolver.d.ts +2 -2
  47. package/dist/implementations/dms/permissions-resolver.js +2 -2
  48. package/dist/implementations/dms/permissions-resolver.js.map +1 -1
  49. package/dist/implementations/dms/realtime.js +1 -0
  50. package/dist/implementations/dms/realtime.js.map +1 -1
  51. package/dist/implementations/dms-base/bulk-selection.d.ts +1 -1
  52. package/dist/implementations/dms-base/bulk-selection.js +23 -1
  53. package/dist/implementations/dms-base/bulk-selection.js.map +1 -1
  54. package/dist/implementations/dms-base/filter-tokens.js +5 -3
  55. package/dist/implementations/dms-base/filter-tokens.js.map +1 -1
  56. package/dist/implementations/dms-base/search-route.d.ts +1 -0
  57. package/dist/implementations/dms-base/search-route.js +35 -13
  58. package/dist/implementations/dms-base/search-route.js.map +1 -1
  59. package/dist/implementations/dms-base/table-view.js +11 -6
  60. package/dist/implementations/dms-base/table-view.js.map +1 -1
  61. package/dist/implementations/dms-notifications/index.d.ts +2 -2
  62. package/dist/implementations/dms-notifications/index.js +5 -5
  63. package/dist/implementations/dms-notifications/index.js.map +1 -1
  64. package/dist/index.js +32 -55
  65. package/dist/index.js.map +1 -1
  66. package/dist/pages/modules/index.d.ts +1 -0
  67. package/dist/pages/modules/index.js +1 -0
  68. package/dist/pages/modules/index.js.map +1 -1
  69. package/dist/pages/modules/modules-store.d.ts +9 -0
  70. package/dist/pages/modules/modules-store.js +30 -0
  71. package/dist/pages/modules/modules-store.js.map +1 -0
  72. package/dist/pages/settings/users/account-credentials.js +2 -2
  73. package/dist/pages/settings/users/account-credentials.js.map +1 -1
  74. package/dist/pages/settings/users/account-data-store.js +25 -8
  75. package/dist/pages/settings/users/account-data-store.js.map +1 -1
  76. package/dist/pages/settings/users/invite-extension-routes.js +9 -3
  77. package/dist/pages/settings/users/invite-extension-routes.js.map +1 -1
  78. package/dist/pages/settings/users/member-management.js +13 -14
  79. package/dist/pages/settings/users/member-management.js.map +1 -1
  80. package/dist/pages/settings/users/profile.d.ts +1 -4
  81. package/dist/pages/settings/users/profile.js +0 -35
  82. package/dist/pages/settings/users/profile.js.map +1 -1
  83. package/dist/pages/settings/users/role-editor-store.js +26 -18
  84. package/dist/pages/settings/users/role-editor-store.js.map +1 -1
  85. package/dist/pages/settings/users/role-editor.d.ts +0 -1
  86. package/dist/pages/settings/users/role-editor.js +1 -2
  87. package/dist/pages/settings/users/role-editor.js.map +1 -1
  88. package/dist/pages/settings/users/role-preview.js +1 -2
  89. package/dist/pages/settings/users/role-preview.js.map +1 -1
  90. package/dist/pages/settings/users/roles.js +2 -2
  91. package/dist/pages/settings/users/roles.js.map +1 -1
  92. package/dist/pages/settings/users/two-factor-operations.js.map +1 -1
  93. package/dist/realtime/broker.d.ts +6 -0
  94. package/dist/realtime/broker.js +8 -1
  95. package/dist/realtime/broker.js.map +1 -1
  96. package/dist/realtime/presence.d.ts +8 -3
  97. package/dist/realtime/presence.js +8 -4
  98. package/dist/realtime/presence.js.map +1 -1
  99. package/dist/realtime/sessions.d.ts +11 -1
  100. package/dist/realtime/sessions.js +21 -3
  101. package/dist/realtime/sessions.js.map +1 -1
  102. package/dist/realtime/table-view.d.ts +2 -2
  103. package/dist/realtime/table-view.js +4 -3
  104. package/dist/realtime/table-view.js.map +1 -1
  105. package/dist/routes/auth/client-ip.d.ts +1 -0
  106. package/dist/routes/auth/client-ip.js +18 -0
  107. package/dist/routes/auth/client-ip.js.map +1 -1
  108. package/dist/routes/auth/oauth/config.d.ts +2 -1
  109. package/dist/routes/auth/oauth/config.js +5 -1
  110. package/dist/routes/auth/oauth/config.js.map +1 -1
  111. package/dist/routes/auth/session-handoff.d.ts +0 -1
  112. package/dist/routes/auth/session-handoff.js +0 -2
  113. package/dist/routes/auth/session-handoff.js.map +1 -1
  114. package/dist/routes/auth.js +3 -2
  115. package/dist/routes/auth.js.map +1 -1
  116. package/dist/routes/index.d.ts +0 -1
  117. package/dist/routes/index.js +0 -1
  118. package/dist/routes/index.js.map +1 -1
  119. package/dist/routes/realtime.d.ts +13 -1
  120. package/dist/routes/realtime.js +114 -29
  121. package/dist/routes/realtime.js.map +1 -1
  122. package/dist/startup-config.d.ts +2 -0
  123. package/dist/startup-config.js +62 -0
  124. package/dist/startup-config.js.map +1 -0
  125. package/dist/store/interfaces.d.ts +3 -2
  126. package/dist/store/interfaces.js +16 -6
  127. package/dist/store/interfaces.js.map +1 -1
  128. package/dist/store/listing.js +25 -15
  129. package/dist/store/listing.js.map +1 -1
  130. package/dist/store/operations.js +5 -2
  131. package/dist/store/operations.js.map +1 -1
  132. package/dist/store/requirements.js +3 -2
  133. package/dist/store/requirements.js.map +1 -1
  134. package/dist/store/runtime.d.ts +10 -1
  135. package/dist/store/runtime.js +33 -6
  136. package/dist/store/runtime.js.map +1 -1
  137. package/dist/store/types.d.ts +10 -0
  138. package/dist/utils/ensure-default-tenant.d.ts +2 -1
  139. package/dist/utils/ensure-default-tenant.js +13 -1
  140. package/dist/utils/ensure-default-tenant.js.map +1 -1
  141. package/dist/utils/export-jobs.js +2 -1
  142. package/dist/utils/export-jobs.js.map +1 -1
  143. package/dist/validation/auth.schema.d.ts +2 -2
  144. package/dist/validation/member-invite.schema.d.ts +2 -10
  145. package/dist/validation/member-invite.schema.js +2 -15
  146. package/dist/validation/member-invite.schema.js.map +1 -1
  147. package/frontend-vue/dms.frontend.ts +24 -5
  148. package/frontend-vue/dms.server.ts +17 -0
  149. package/frontend-vue/layers/dms-auth/server/accounts.ts +3 -4
  150. package/frontend-vue/layers/dms-core/app/build/composables/renderNow.ts +33 -0
  151. package/frontend-vue/layers/dms-core/app/build/composables/user/tablePreferences.ts +5 -1
  152. package/frontend-vue/layers/dms-core/app/build/utils/crossTab.ts +199 -0
  153. package/frontend-vue/layers/dms-core/app/build/utils/sharedRealtimeConnection.ts +410 -0
  154. package/frontend-vue/layers/dms-core/app/composables/auth/useApiData.ts +77 -12
  155. package/frontend-vue/layers/dms-core/app/composables/data-types/useDataType.ts +26 -0
  156. package/frontend-vue/layers/dms-core/app/composables/live/livePauseSource.ts +29 -0
  157. package/frontend-vue/layers/dms-core/app/composables/period/compareResolvers.ts +150 -9
  158. package/frontend-vue/layers/dms-core/app/composables/period/usePeriod.ts +6 -4
  159. package/frontend-vue/layers/dms-core/app/composables/period/usePeriodScope.ts +27 -1
  160. package/frontend-vue/layers/dms-core/app/composables/realtime/useUserRealtime.ts +256 -119
  161. package/frontend-vue/layers/dms-core/app/composables/translation/useComposedText.ts +21 -8
  162. package/frontend-vue/layers/dms-core/app/composables/translation/useTranslation.ts +25 -5
  163. package/frontend-vue/layers/dms-core/app/composables/useApiError.ts +3 -5
  164. package/frontend-vue/layers/dms-core/app/composables/useFieldErrors.ts +61 -26
  165. package/frontend-vue/layers/dms-core/app/plugins/dev-error-log.ts +16 -0
  166. package/frontend-vue/layers/dms-core/app/types/user.ts +6 -0
  167. package/frontend-vue/layers/dms-core/app/utils/composedText.ts +7 -2
  168. package/frontend-vue/layers/dms-core/app/utils/formatter.ts +134 -35
  169. package/frontend-vue/layers/dms-core/app/utils/url-interpolation.ts +18 -1
  170. package/frontend-vue/layers/dms-core/i18n/locales/core-en-GB.json +136 -2
  171. package/frontend-vue/layers/dms-core/i18n/locales/core-fr-FR.json +136 -2
  172. package/frontend-vue/layers/dms-core/shared/types/app-config.ts +0 -2
  173. package/frontend-vue/layers/dms-layout/app/app.config.ts +0 -1
  174. package/frontend-vue/layers/dms-layout/app/assets/css/main.css +11 -1
  175. package/frontend-vue/layers/dms-layout/app/assets/fonts/Geist-Italic-Variable.woff2 +0 -0
  176. package/frontend-vue/layers/dms-layout/app/assets/fonts/Geist-Variable.woff2 +0 -0
  177. package/frontend-vue/layers/dms-layout/app/assets/fonts/GeistMono-Italic-Variable.woff2 +0 -0
  178. package/frontend-vue/layers/dms-layout/app/assets/fonts/GeistMono-Variable.woff2 +0 -0
  179. package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardBanners.vue +3 -2
  180. package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardHeader.vue +41 -21
  181. package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardPageBody.vue +6 -2
  182. package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardSidebar.vue +4 -19
  183. package/frontend-vue/layers/dms-layout/app/build/components/layout/Footer.vue +11 -107
  184. package/frontend-vue/layers/dms-layout/app/build/components/layout/GithubStarPrompt.vue +18 -7
  185. package/frontend-vue/layers/dms-layout/app/build/components/layout/PageHeaderActionsOutlet.vue +4 -0
  186. package/frontend-vue/layers/dms-layout/app/build/components/layout/RowSidePanel.vue +43 -82
  187. package/frontend-vue/layers/dms-layout/app/build/components/layout/SidebarUserMenu.vue +44 -3
  188. package/frontend-vue/layers/dms-layout/app/build/components/layout/UserIdentity.vue +11 -3
  189. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleCatalogTile.vue +132 -35
  190. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleManageMenu.vue +59 -0
  191. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleSourceLabel.vue +25 -0
  192. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleStoreBanners.vue +73 -0
  193. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleStoreOperation.vue +116 -0
  194. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleStoreRequirements.vue +8 -3
  195. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleStoreTile.vue +85 -207
  196. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleTile.vue +10 -3
  197. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleTileSkeleton.vue +68 -0
  198. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/notification/NotificationInboxDisplay.vue +24 -8
  199. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/notification/NotificationInboxItem.vue +8 -5
  200. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/region/RegionTimeZoneInput.vue +1 -0
  201. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/roles/role-permissions.ts +19 -0
  202. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/roles/useRoleEditor.ts +2 -1
  203. package/frontend-vue/layers/dms-layout/app/build/composables/layout/useSidePanelWidth.ts +31 -70
  204. package/frontend-vue/layers/dms-layout/app/build/composables/notification/useNotificationGroupsOpen.ts +11 -1
  205. package/frontend-vue/layers/dms-layout/app/build/utils/dev-reload-stream.ts +81 -0
  206. package/frontend-vue/layers/dms-layout/app/build/utils/github-repository.ts +2 -2
  207. package/frontend-vue/layers/dms-layout/app/build/utils/module-store-catalog.ts +232 -36
  208. package/frontend-vue/layers/dms-layout/app/build/utils/modules-catalog.ts +122 -14
  209. package/frontend-vue/layers/dms-layout/app/composables/layout/useLayoutBanners.ts +32 -3
  210. package/frontend-vue/layers/dms-layout/app/composables/layout/useServedHeaderActions.ts +49 -11
  211. package/frontend-vue/layers/dms-layout/app/composables/page/useModuleHistory.ts +3 -2
  212. package/frontend-vue/layers/dms-layout/app/composables/page/useModuleStore.ts +59 -4
  213. package/frontend-vue/layers/dms-layout/app/composables/page/useModuleStoreActions.ts +194 -0
  214. package/frontend-vue/layers/dms-layout/app/composables/page/usePageComponentPreload.ts +23 -3
  215. package/frontend-vue/layers/dms-layout/app/composables/page/usePageLayout.ts +4 -2
  216. package/frontend-vue/layers/dms-layout/app/composables/page/usePageRecordDeletion.ts +162 -0
  217. package/frontend-vue/layers/dms-layout/app/composables/page/useSiteLayout.ts +6 -1
  218. package/frontend-vue/layers/dms-layout/app/custom-layouts/DefaultLayout.vue +6 -2
  219. package/frontend-vue/layers/dms-layout/app/custom-layouts/SettingsLayout.vue +6 -2
  220. package/frontend-vue/layers/dms-layout/app/custom-pages/modules-store.vue +291 -0
  221. package/frontend-vue/layers/dms-layout/app/custom-pages/modules.vue +87 -474
  222. package/frontend-vue/layers/dms-layout/app/middleware/module-routing.global.ts +3 -0
  223. package/frontend-vue/layers/dms-layout/app/pages/[...slug].vue +13 -22
  224. package/frontend-vue/layers/dms-layout/app/plugins/command-palette-navigation.client.ts +11 -10
  225. package/frontend-vue/layers/dms-layout/app/plugins/command-palette-personal-pages.client.ts +22 -52
  226. package/frontend-vue/layers/dms-layout/app/plugins/command-palette-quick-actions.client.ts +7 -7
  227. package/frontend-vue/layers/dms-layout/app/plugins/command-palette-session.client.ts +11 -10
  228. package/frontend-vue/layers/dms-layout/app/plugins/dms-dev-reload.client.ts +14 -9
  229. package/frontend-vue/layers/dms-layout/app/theme/actions.ts +5 -4
  230. package/frontend-vue/layers/dms-layout/app/theme/form-controls.ts +9 -4
  231. package/frontend-vue/layers/dms-layout/app/theme/surfaces.ts +22 -6
  232. package/frontend-vue/layers/dms-layout/app/types/module-store.ts +9 -0
  233. package/frontend-vue/layers/dms-layout/app/types/page.ts +15 -1
  234. package/frontend-vue/layers/dms-layout/app/utils/account-menu.ts +11 -0
  235. package/frontend-vue/layers/dms-layout/app/utils/breadcrumb.ts +57 -0
  236. package/frontend-vue/layers/dms-layout/app/utils/pageQuery.ts +26 -0
  237. package/frontend-vue/layers/dms-layout/i18n/locales/layout-en-GB.json +45 -18
  238. package/frontend-vue/layers/dms-layout/i18n/locales/layout-fr-FR.json +44 -17
  239. package/frontend-vue/layers/dms-ui/app/build/components/containers/TargetComponentTree.vue +31 -0
  240. package/frontend-vue/layers/dms-ui/app/build/components/containers/constants.ts +25 -0
  241. package/frontend-vue/layers/dms-ui/app/build/components/containers/drawer/DynamicDrawer.vue +244 -15
  242. package/frontend-vue/layers/dms-ui/app/build/components/containers/modal/DynamicModal.vue +42 -25
  243. package/frontend-vue/layers/dms-ui/app/build/components/containers/row/RowHeader.vue +153 -0
  244. package/frontend-vue/layers/dms-ui/app/build/components/containers/row/RowNotFound.vue +44 -0
  245. package/frontend-vue/layers/dms-ui/app/build/components/containers/row/drawerRowHeader.ts +35 -0
  246. package/frontend-vue/layers/dms-ui/app/build/components/form/FormEntries.vue +44 -9
  247. package/frontend-vue/layers/dms-ui/app/build/components/form/FormErrorSummary.vue +11 -4
  248. package/frontend-vue/layers/dms-ui/app/build/components/form/FormFieldControl.vue +38 -5
  249. package/frontend-vue/layers/dms-ui/app/build/components/form/FormFieldNotes.vue +29 -5
  250. package/frontend-vue/layers/dms-ui/app/build/components/form/FormLoadError.vue +5 -1
  251. package/frontend-vue/layers/dms-ui/app/build/components/form/FormSectionNav.vue +3 -1
  252. package/frontend-vue/layers/dms-ui/app/build/components/form/FormSections.vue +106 -27
  253. package/frontend-vue/layers/dms-ui/app/build/components/table/Actions.vue +12 -4
  254. package/frontend-vue/layers/dms-ui/app/build/components/table/Empty.vue +4 -2
  255. package/frontend-vue/layers/dms-ui/app/build/components/table/FiltersRow.vue +48 -1
  256. package/frontend-vue/layers/dms-ui/app/build/components/table/Pagination.vue +55 -54
  257. package/frontend-vue/layers/dms-ui/app/build/components/table/RowSelection.vue +2 -1
  258. package/frontend-vue/layers/dms-ui/app/build/components/table/Table.vue +396 -288
  259. package/frontend-vue/layers/dms-ui/app/build/components/table/Tabs.vue +2 -1
  260. package/frontend-vue/layers/dms-ui/app/build/components/table/Views.vue +2 -1
  261. package/frontend-vue/layers/dms-ui/app/build/components/table-view/CardAvatar.vue +59 -0
  262. package/frontend-vue/layers/dms-ui/app/build/components/table-view/CardGridSkeleton.vue +19 -2
  263. package/frontend-vue/layers/dms-ui/app/build/components/table-view/CardRowActions.vue +38 -0
  264. package/frontend-vue/layers/dms-ui/app/build/components/table-view/CardsDisplay.vue +148 -104
  265. package/frontend-vue/layers/dms-ui/app/build/components/table-view/LiveHoldBanner.vue +17 -2
  266. package/frontend-vue/layers/dms-ui/app/build/composables/actions/bulkSelection.ts +43 -9
  267. package/frontend-vue/layers/dms-ui/app/build/composables/actions/rowComponentOptions.ts +76 -0
  268. package/frontend-vue/layers/dms-ui/app/build/composables/actions/rowNavigation.ts +172 -27
  269. package/frontend-vue/layers/dms-ui/app/build/composables/actions/useActionTargets.ts +270 -46
  270. package/frontend-vue/layers/dms-ui/app/build/composables/confirm/actionError.ts +6 -1
  271. package/frontend-vue/layers/dms-ui/app/build/composables/confirm/confirmDialogTexts.ts +2 -0
  272. package/frontend-vue/layers/dms-ui/app/build/composables/containers/closingLayers.ts +186 -0
  273. package/frontend-vue/layers/dms-ui/app/build/composables/containers/context.ts +14 -0
  274. package/frontend-vue/layers/dms-ui/app/build/composables/containers/drawerSize.ts +229 -0
  275. package/frontend-vue/layers/dms-ui/app/build/composables/containers/useResizableSize.ts +175 -0
  276. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/metricCells.ts +18 -35
  277. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/numberSuffix.ts +44 -0
  278. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/registerDefaults.ts +123 -27
  279. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/registerFieldTypes.ts +2 -1
  280. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/relationFilter.ts +75 -0
  281. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/relationRows.ts +92 -0
  282. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/relationValue.ts +39 -0
  283. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/twoLineCell.ts +3 -3
  284. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/useColumnValueRenderer.ts +6 -2
  285. package/frontend-vue/layers/dms-ui/app/build/composables/form/createFormFields.ts +15 -0
  286. package/frontend-vue/layers/dms-ui/app/build/composables/form/fieldSaved.ts +40 -0
  287. package/frontend-vue/layers/dms-ui/app/build/composables/form/fieldValueRule.ts +62 -0
  288. package/frontend-vue/layers/dms-ui/app/build/composables/form/formEntryContext.ts +5 -0
  289. package/frontend-vue/layers/dms-ui/app/build/composables/form/formFooter.ts +42 -7
  290. package/frontend-vue/layers/dms-ui/app/build/composables/form/formLayout.ts +106 -4
  291. package/frontend-vue/layers/dms-ui/app/build/composables/form/formRecordLoad.ts +11 -5
  292. package/frontend-vue/layers/dms-ui/app/build/composables/form/formSections.ts +32 -0
  293. package/frontend-vue/layers/dms-ui/app/build/composables/form/formValidation.ts +36 -4
  294. package/frontend-vue/layers/dms-ui/app/build/composables/form/readonlyValue.ts +19 -0
  295. package/frontend-vue/layers/dms-ui/app/build/composables/form/registerFormFunctions.ts +27 -0
  296. package/frontend-vue/layers/dms-ui/app/build/composables/form/repeaterTable.ts +78 -0
  297. package/frontend-vue/layers/dms-ui/app/build/composables/form/serverFieldErrors.ts +49 -0
  298. package/frontend-vue/layers/dms-ui/app/build/composables/form/toasterClearance.ts +118 -0
  299. package/frontend-vue/layers/dms-ui/app/build/composables/form/useInstantForm.ts +12 -1
  300. package/frontend-vue/layers/dms-ui/app/build/composables/live/currentPageLive.ts +41 -3
  301. package/frontend-vue/layers/dms-ui/app/build/composables/live/livePoller.ts +10 -2
  302. package/frontend-vue/layers/dms-ui/app/build/composables/live/pageReadClock.ts +28 -0
  303. package/frontend-vue/layers/dms-ui/app/build/composables/live/shortcuts.ts +1 -1
  304. package/frontend-vue/layers/dms-ui/app/build/composables/live/usePageLiveHost.ts +17 -3
  305. package/frontend-vue/layers/dms-ui/app/build/composables/live/useUpdatedLabel.ts +38 -0
  306. package/frontend-vue/layers/dms-ui/app/build/composables/page-toolbar/pageToolbarContext.ts +75 -0
  307. package/frontend-vue/layers/dms-ui/app/build/composables/page-toolbar/toolbarControlHeight.ts +29 -0
  308. package/frontend-vue/layers/dms-ui/app/build/composables/period/periodComparison.ts +52 -0
  309. package/frontend-vue/layers/dms-ui/app/build/composables/period/periodDisplay.ts +21 -0
  310. package/frontend-vue/layers/dms-ui/app/build/composables/period/periodShortLabels.ts +28 -0
  311. package/frontend-vue/layers/dms-ui/app/build/composables/period/usePeriodDraft.ts +1 -0
  312. package/frontend-vue/layers/dms-ui/app/build/composables/table/useRelationFilterLabels.ts +97 -0
  313. package/frontend-vue/layers/dms-ui/app/build/composables/table/useRowScrollAnchor.ts +106 -0
  314. package/frontend-vue/layers/dms-ui/app/build/composables/table/useTable.ts +9 -0
  315. package/frontend-vue/layers/dms-ui/app/build/composables/table/useTableColumns.ts +1 -2
  316. package/frontend-vue/layers/dms-ui/app/build/composables/table/utils/archivedCount.ts +31 -0
  317. package/frontend-vue/layers/dms-ui/app/build/composables/table/utils/filterEditorValue.ts +38 -0
  318. package/frontend-vue/layers/dms-ui/app/build/composables/table/utils/formatFilterValue.ts +33 -2
  319. package/frontend-vue/layers/dms-ui/app/build/composables/table/utils/scrollEdges.ts +31 -0
  320. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useCardFields.ts +33 -0
  321. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useMobileDisplay.ts +48 -0
  322. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useScrolledPastTop.ts +41 -18
  323. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableFooter.ts +3 -2
  324. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableLinkedVisit.ts +78 -0
  325. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableLiveHold.ts +135 -22
  326. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableRows.ts +56 -25
  327. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViewRowActions.ts +90 -35
  328. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViews.ts +86 -10
  329. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/bulkActions.ts +14 -4
  330. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/card.ts +127 -2
  331. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/compareValues.ts +13 -0
  332. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/expandedRows.ts +28 -0
  333. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/groupedRows.ts +114 -1
  334. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/linkedArrival.ts +308 -0
  335. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/locateRow.ts +106 -0
  336. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/pageArrival.ts +34 -0
  337. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/pinnedRows.ts +17 -0
  338. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/quickFilters.ts +41 -33
  339. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/recordLink.ts +24 -8
  340. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/tableCounts.ts +113 -0
  341. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/tableQuery.ts +27 -4
  342. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/views.ts +22 -6
  343. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/writeError.ts +21 -0
  344. package/frontend-vue/layers/dms-ui/app/build/utils/addressSuggestions.ts +49 -0
  345. package/frontend-vue/layers/dms-ui/app/build/utils/codeHighlight.ts +67 -29
  346. package/frontend-vue/layers/dms-ui/app/build/utils/eyebrow.ts +12 -0
  347. package/frontend-vue/layers/dms-ui/app/build/utils/fieldOptionsI18n.ts +51 -11
  348. package/frontend-vue/layers/dms-ui/app/build/utils/htmlGrammar.ts +79 -0
  349. package/frontend-vue/layers/dms-ui/app/build/utils/inputWidth.ts +53 -0
  350. package/frontend-vue/layers/dms-ui/app/build/utils/numberAdjustment.ts +6 -3
  351. package/frontend-vue/layers/dms-ui/app/build/utils/relativeDate.ts +19 -5
  352. package/frontend-vue/layers/dms-ui/app/build/utils/scrollWithin.ts +99 -0
  353. package/frontend-vue/layers/dms-ui/app/build/utils/shellParser.ts +1 -1
  354. package/frontend-vue/layers/dms-ui/app/build/utils/streamHighlight.ts +90 -0
  355. package/frontend-vue/layers/dms-ui/app/build/utils/urlVariables.ts +36 -14
  356. package/frontend-vue/layers/dms-ui/app/components/PasswordStrength.vue +1 -1
  357. package/frontend-vue/layers/dms-ui/app/components/RecursiveComponent.vue +9 -1
  358. package/frontend-vue/layers/dms-ui/app/components/blocks/LiveToggleBlock.vue +80 -32
  359. package/frontend-vue/layers/dms-ui/app/components/blocks/PageToolbarBlock.vue +137 -0
  360. package/frontend-vue/layers/dms-ui/app/components/blocks/StatGroupBlock.vue +5 -0
  361. package/frontend-vue/layers/dms-ui/app/components/chart/ApexChartHost.vue +16 -2
  362. package/frontend-vue/layers/dms-ui/app/components/chart/Chart.vue +24 -5
  363. package/frontend-vue/layers/dms-ui/app/components/chart/ChartCard.vue +41 -23
  364. package/frontend-vue/layers/dms-ui/app/components/code-snippet/CodeSnippet.vue +23 -12
  365. package/frontend-vue/layers/dms-ui/app/components/confirm/ConfirmModal.vue +1 -1
  366. package/frontend-vue/layers/dms-ui/app/components/container-footer/ContainerFooter.vue +34 -0
  367. package/frontend-vue/layers/dms-ui/app/components/field-row/FieldRow.vue +25 -2
  368. package/frontend-vue/layers/dms-ui/app/components/flow-canvas/FlowCanvas.vue +2 -2
  369. package/frontend-vue/layers/dms-ui/app/components/form/Form.vue +119 -36
  370. package/frontend-vue/layers/dms-ui/app/components/form/components/Cascader.vue +53 -22
  371. package/frontend-vue/layers/dms-ui/app/components/form/components/DatePicker.vue +20 -1
  372. package/frontend-vue/layers/dms-ui/app/components/form/components/Display.vue +13 -1
  373. package/frontend-vue/layers/dms-ui/app/components/form/components/Image.vue +8 -3
  374. package/frontend-vue/layers/dms-ui/app/components/form/components/InputAddress.vue +53 -5
  375. package/frontend-vue/layers/dms-ui/app/components/form/components/InputCode.vue +29 -5
  376. package/frontend-vue/layers/dms-ui/app/components/form/components/InputNumber.vue +45 -8
  377. package/frontend-vue/layers/dms-ui/app/components/form/components/InputPercentage.vue +28 -5
  378. package/frontend-vue/layers/dms-ui/app/components/form/components/InputSecret.vue +5 -0
  379. package/frontend-vue/layers/dms-ui/app/components/form/components/InputText.vue +19 -2
  380. package/frontend-vue/layers/dms-ui/app/components/form/components/InputTime.vue +2 -1
  381. package/frontend-vue/layers/dms-ui/app/components/form/components/Relation.vue +226 -65
  382. package/frontend-vue/layers/dms-ui/app/components/form/components/Repeater.vue +80 -30
  383. package/frontend-vue/layers/dms-ui/app/components/form/components/RepeaterTable.vue +210 -0
  384. package/frontend-vue/layers/dms-ui/app/components/form/components/Select.vue +4 -2
  385. package/frontend-vue/layers/dms-ui/app/components/grid/GridRow.vue +11 -2
  386. package/frontend-vue/layers/dms-ui/app/components/key-value-list/KeyValueList.vue +79 -70
  387. package/frontend-vue/layers/dms-ui/app/components/kpi/KpiCard.vue +117 -21
  388. package/frontend-vue/layers/dms-ui/app/components/meter/Meter.vue +2 -1
  389. package/frontend-vue/layers/dms-ui/app/components/period/PeriodSelector.vue +113 -34
  390. package/frontend-vue/layers/dms-ui/app/components/save-bar/SaveBar.vue +14 -2
  391. package/frontend-vue/layers/dms-ui/app/components/section-header/Eyebrow.vue +6 -1
  392. package/frontend-vue/layers/dms-ui/app/components/segmented/Segmented.vue +13 -0
  393. package/frontend-vue/layers/dms-ui/app/components/stat-group/StatGroup.vue +52 -3
  394. package/frontend-vue/layers/dms-ui/app/components/tab/Tab.vue +2 -1
  395. package/frontend-vue/layers/dms-ui/app/components/table-view/CascaderPath.vue +74 -25
  396. package/frontend-vue/layers/dms-ui/app/components/table-view/KanbanBoard.vue +48 -44
  397. package/frontend-vue/layers/dms-ui/app/components/table-view/KanbanDisplay.vue +4 -1
  398. package/frontend-vue/layers/dms-ui/app/components/table-view/TableView.vue +426 -97
  399. package/frontend-vue/layers/dms-ui/app/components/top-list/TopListCard.vue +33 -5
  400. package/frontend-vue/layers/dms-ui/app/composables/chart/apexCategoryLabels.ts +68 -0
  401. package/frontend-vue/layers/dms-ui/app/composables/chart/apexDatetimeBuckets.ts +306 -0
  402. package/frontend-vue/layers/dms-ui/app/composables/chart/formatValue.ts +4 -3
  403. package/frontend-vue/layers/dms-ui/app/composables/chart/heatmapColors.ts +93 -0
  404. package/frontend-vue/layers/dms-ui/app/composables/chart/localizeSeries.ts +62 -0
  405. package/frontend-vue/layers/dms-ui/app/composables/chart/useApexChart.ts +116 -8
  406. package/frontend-vue/layers/dms-ui/app/composables/chart/useApexChart.types.ts +7 -0
  407. package/frontend-vue/layers/dms-ui/app/composables/containers/types.ts +84 -1
  408. package/frontend-vue/layers/dms-ui/app/composables/containers/useDrawer.ts +69 -8
  409. package/frontend-vue/layers/dms-ui/app/composables/form/types/field-saved.ts +11 -0
  410. package/frontend-vue/layers/dms-ui/app/composables/form/types/field.ts +16 -0
  411. package/frontend-vue/layers/dms-ui/app/composables/form/types/functions.ts +1 -0
  412. package/frontend-vue/layers/dms-ui/app/composables/form/useForm.ts +189 -28
  413. package/frontend-vue/layers/dms-ui/app/composables/live/usePageLive.ts +29 -12
  414. package/frontend-vue/layers/dms-ui/app/composables/page-toolbar/usePageToolbarSize.ts +30 -0
  415. package/frontend-vue/layers/dms-ui/app/composables/table-view/kanban.ts +4 -2
  416. package/frontend-vue/layers/dms-ui/app/composables/table-view/types/action-target.ts +22 -3
  417. package/frontend-vue/layers/dms-ui/app/composables/table-view/types/config.ts +4 -0
  418. package/frontend-vue/layers/dms-ui/app/composables/table-view/types/display.ts +24 -1
  419. package/frontend-vue/layers/dms-ui/app/plugins/closing-layers.client.ts +10 -0
  420. package/frontend-vue/layers/dms-ui/app/plugins/live-pause-source.ts +15 -0
  421. package/frontend-vue/layers/dms-ui/app/plugins/register.ts +2 -0
  422. package/frontend-vue/layers/dms-ui/app/types/input-number.ts +3 -2
  423. package/frontend-vue/layers/dms-ui/app/types/page-toolbar.ts +7 -0
  424. package/frontend-vue/layers/dms-ui/app/types/row-action.ts +0 -2
  425. package/frontend-vue/layers/dms-ui/app/types/row-navigation.ts +17 -0
  426. package/frontend-vue/layers/dms-ui/app/types/table/query.ts +2 -1
  427. package/frontend-vue/layers/dms-ui/app/utils/blockRefresh.ts +3 -1
  428. package/frontend-vue/layers/dms-ui/app/utils/cascader.ts +94 -3
  429. package/frontend-vue/layers/dms-ui/app/utils/componentTree.ts +24 -0
  430. package/frontend-vue/layers/dms-ui/i18n/locales/ui-en-GB.json +20 -5
  431. package/frontend-vue/layers/dms-ui/i18n/locales/ui-fr-FR.json +20 -5
  432. package/frontend-vue/package.json +6 -2
  433. package/package.json +11 -10
  434. package/skills/dms/SKILL.md +1 -1
  435. package/skills/dms-auth/SKILL.md +5 -3
  436. package/skills/dms-best-practices/REFERENCE.md +2 -0
  437. package/skills/dms-best-practices/SKILL.md +4 -0
  438. package/skills/dms-module/SKILL.md +3 -2
  439. package/skills/dms-pages/REFERENCE.md +129 -16
  440. package/skills/dms-pages/SKILL.md +1 -1
  441. package/skills/dms-project/DEV-SERVERS.md +23 -1
  442. package/skills/dms-project/REFERENCE.md +1 -1
  443. package/skills/dms-project/SKILL.md +4 -1
  444. package/skills/dms-project/scripts/dms-dev.sh +205 -17
  445. package/dist/hooks/tenant-instances.d.ts +0 -1
  446. package/dist/hooks/tenant-instances.js +0 -13
  447. package/dist/hooks/tenant-instances.js.map +0 -1
  448. package/dist/routes/startup-gate.d.ts +0 -7
  449. package/dist/routes/startup-gate.js +0 -39
  450. package/dist/routes/startup-gate.js.map +0 -1
  451. package/frontend-vue/layers/dms-core/app/composables/period/periodRenderNow.ts +0 -42
@@ -1,20 +1,26 @@
1
1
  import {
2
- isUnauthorizedStreamError,
3
- openSseStream,
4
- type SseStreamHandle,
5
- } from "./useSseStream";
2
+ createReconnector,
3
+ joinSharedRealtimeConnection,
4
+ recoverFromError,
5
+ type StreamRecovery,
6
+ } from "../../build/utils/sharedRealtimeConnection";
7
+ import { HTTP_UNAUTHORIZED } from "../../utils/http-status";
6
8
 
7
- const REALTIME_USER_PATH = "/api/realtime/user";
9
+ const REALTIME_CONNECTION_PATH = "/api/realtime/connection";
10
+ const REALTIME_SESSION_PATH = "/api/realtime/session";
11
+ const REALTIME_SESSION_CLOSE_PATH = "/api/realtime/session/close";
8
12
  const REALTIME_SUBSCRIBE_PATH_PREFIX = "/api/realtime/subscribe/";
9
13
  const REALTIME_UNSUBSCRIBE_PATH = "/api/realtime/unsubscribe";
10
14
  const SESSION_HEADER = "x-realtime-session";
15
+ const CONNECTION_HEADER = "x-realtime-connection";
16
+ const CONNECTION_NAME_PREFIX = "dms-realtime";
11
17
  const SESSION_STATE_KEY = "dms.realtime.sessionId";
12
18
  const MENU_TOPICS_STATE_KEY = "dms.realtime.menuTopics";
13
- const HELLO_EVENT = "hello";
14
19
  const SNAPSHOT_EVENT = "snapshot";
15
- const RECONNECT_INITIAL_DELAY_MS = 500;
16
- const RECONNECT_MAX_DELAY_MS = 30_000;
17
- const RECONNECT_MULTIPLIER = 2;
20
+ /** The server ended this tab's session alone: the tab opens a new one. */
21
+ const SESSION_ENDED_EVENT = "session-ended";
22
+ /** Frames held while a session is being opened, every tab's included. */
23
+ const MAX_BUFFERED_FRAMES = 256;
18
24
 
19
25
  export interface RealtimeEvent {
20
26
  topic: string;
@@ -56,6 +62,13 @@ export interface UserRealtimeApi {
56
62
  * answered them: an event on one means the page's data moved.
57
63
  */
58
64
  pageTopics: Ref<string[]>;
65
+ /**
66
+ * The topic the open connection carries the backend's development reload
67
+ * signal under, as the session's greeting named it: the tab then needs no
68
+ * reload stream of its own. Unset while no session is open, and in
69
+ * production.
70
+ */
71
+ devReloadTopic: Ref<string | undefined>;
59
72
  }
60
73
 
61
74
  interface UserRealtimeState {
@@ -63,15 +76,45 @@ interface UserRealtimeState {
63
76
  pageId: Ref<string | undefined>;
64
77
  menuTopics: Ref<string[]>;
65
78
  pageTopics: Ref<string[]>;
79
+ devReloadTopic: Ref<string | undefined>;
66
80
  appliedPageId: Ref<string | null>;
67
81
  handlers: Map<string, Set<RealtimeTopicHandler>>;
68
82
  snapshots: Map<string, RealtimeSnapshotEvent>;
69
83
  reconciling: Promise<void>;
70
- cancelConnect?: () => void;
84
+ connection?: TabConnection;
85
+ }
86
+
87
+ /** This tab's hold on the shared connection. */
88
+ interface TabConnection {
89
+ /** Opens a new session for the tab, in the browser session's workspace. */
90
+ renewSession: () => void;
91
+ cancel: () => void;
71
92
  }
72
93
 
73
94
  let singletonApi: UserRealtimeApi | undefined;
74
95
 
96
+ /** The user field the shared connection is keyed by. */
97
+ interface ConnectionUser {
98
+ _id?: string;
99
+ }
100
+
101
+ /** The session fields a realtime session is bound to on the server. */
102
+ interface SessionScopeFields {
103
+ accountId?: string;
104
+ activeTenantId?: string;
105
+ }
106
+
107
+ /**
108
+ * The account and workspace a realtime session is opened in. The server binds
109
+ * a session to those of the request that opened it, so a switch of either
110
+ * needs a new session: the old one would keep delivering the previous
111
+ * workspace's events. The connection, keyed by user, stays.
112
+ */
113
+ const sessionScopeOf = (
114
+ session: SessionScopeFields | null | undefined,
115
+ ): string =>
116
+ JSON.stringify([session?.accountId ?? "", session?.activeTenantId ?? ""]);
117
+
75
118
  const dispatchToHandlers = (
76
119
  state: UserRealtimeState,
77
120
  topic: string,
@@ -105,19 +148,19 @@ const readMenuTopics = (data: object): string[] => {
105
148
  );
106
149
  };
107
150
 
151
+ /** Takes the session a greeting names, with the menu topics it announces. */
108
152
  const handleHello = (state: UserRealtimeState, data: unknown): void => {
109
153
  if (!data || typeof data !== "object" || !("sessionId" in data)) return;
110
154
  state.sessionId.value = String((data as { sessionId: unknown }).sessionId);
111
155
  state.menuTopics.value = readMenuTopics(data);
156
+ const { devReloadTopic } = data as { devReloadTopic?: unknown };
157
+ state.devReloadTopic.value =
158
+ typeof devReloadTopic === "string" ? devReloadTopic : undefined;
112
159
  };
113
160
 
114
161
  const handleEvent =
115
162
  (state: UserRealtimeState) =>
116
163
  (eventName: string, data: unknown): void => {
117
- if (eventName === HELLO_EVENT) {
118
- handleHello(state, data);
119
- return;
120
- }
121
164
  if (isSnapshotEvent(eventName, data)) {
122
165
  state.snapshots.set(data.topic, data);
123
166
  dispatchToHandlers(state, data.topic, data);
@@ -128,111 +171,187 @@ const handleEvent =
128
171
  }
129
172
  };
130
173
 
131
- interface ConnectionCallbacks {
174
+ interface ConnectionCallbacks extends StreamRecovery {
132
175
  onConnected: () => void;
133
176
  onDisconnected: () => void;
134
- refreshSession: () => Promise<boolean>;
135
- redirectToAuth: () => Promise<void>;
136
177
  }
137
178
 
138
- interface Reconnector {
139
- isCancelled(): boolean;
140
- schedule(open: () => void): void;
141
- resetDelay(): void;
142
- cancel(): void;
179
+ /** The answer of `POST /api/realtime/session`: a greeting, or `ok: false`. */
180
+ interface SessionGreeting {
181
+ sessionId?: unknown;
182
+ menuTopics?: unknown;
143
183
  }
144
184
 
145
- const createReconnector = (): Reconnector => {
146
- let isCancelled = false;
147
- let delay = RECONNECT_INITIAL_DELAY_MS;
148
- let timer: ReturnType<typeof setTimeout> | undefined;
149
- return {
150
- isCancelled: () => isCancelled,
151
- schedule: (open) => {
152
- // A pending reconnect absorbs concurrent triggers: one stream per tab.
153
- if (isCancelled || timer) return;
154
- timer = setTimeout(() => {
155
- timer = undefined;
156
- open();
157
- }, delay);
158
- delay = Math.min(delay * RECONNECT_MULTIPLIER, RECONNECT_MAX_DELAY_MS);
159
- },
160
- resetDelay: () => {
161
- delay = RECONNECT_INITIAL_DELAY_MS;
162
- },
163
- cancel: () => {
164
- isCancelled = true;
165
- if (timer) clearTimeout(timer);
166
- timer = undefined;
167
- },
168
- };
169
- };
185
+ interface BufferedFrame {
186
+ sessionId: string;
187
+ eventName: string;
188
+ data: unknown;
189
+ }
170
190
 
171
- const recoverFromError = (
172
- error: unknown,
173
- reconnect: () => void,
174
- callbacks: ConnectionCallbacks,
175
- ): void => {
176
- if (!isUnauthorizedStreamError(error)) {
177
- reconnect();
178
- return;
191
+ const readGreeting = async (
192
+ response: Response,
193
+ ): Promise<SessionGreeting | null> => {
194
+ try {
195
+ return (await response.json()) as SessionGreeting | null;
196
+ } catch {
197
+ return null;
179
198
  }
180
- void callbacks.refreshSession().then((isRefreshed) => {
181
- if (isRefreshed) {
182
- reconnect();
183
- } else {
184
- void callbacks.redirectToAuth();
185
- }
186
- }, reconnect);
187
199
  };
188
200
 
201
+ const unauthorizedError = (): Error =>
202
+ Object.assign(new Error("Realtime session refused"), {
203
+ status: HTTP_UNAUTHORIZED,
204
+ });
205
+
189
206
  /**
190
- * Keeps one user stream open until cancelled. The server ends the stream when
191
- * it stops (a dev reload included), so a clean EOF reconnects just like an
192
- * error does: the user stream has no business completion.
207
+ * Keeps a session of this tab on the browser's shared realtime connection
208
+ * until cancelled: a new one each time the connection changes, its leader tab
209
+ * having gone or its stream having dropped, and each time it is renewed.
193
210
  */
194
- const connectWithBackoff = (
195
- url: string,
211
+ const connectTab = (
196
212
  state: UserRealtimeState,
213
+ baseUrl: string,
214
+ userKey: string,
197
215
  callbacks: ConnectionCallbacks,
198
- ): (() => void) => {
199
- const reconnector = createReconnector();
216
+ ): TabConnection => {
200
217
  const dispatch = handleEvent(state);
201
- let activeHandle: SseStreamHandle | undefined;
202
- const open = () => {
203
- if (reconnector.isCancelled()) return;
204
- activeHandle?.close();
205
- const handle: SseStreamHandle = openSseStream({
206
- url,
207
- onEvent: (eventName, data) => {
208
- if (handle !== activeHandle) return;
209
- reconnector.resetDelay();
210
- dispatch(eventName, data);
211
- if (eventName === HELLO_EVENT) callbacks.onConnected();
212
- },
213
- onError: (error) => {
214
- if (!retire(handle)) return;
215
- recoverFromError(error, reconnect, callbacks);
216
- },
217
- onClose: (reason) => {
218
- if (reason !== "eof" || !retire(handle)) return;
219
- reconnect();
218
+ const retries = createReconnector();
219
+ const sessionCloseUrl = `${baseUrl}${REALTIME_SESSION_CLOSE_PATH}`;
220
+ let connectionId: string | undefined;
221
+ // Each opening attempt supersedes the previous one, which a renewal may
222
+ // leave in flight on the same connection.
223
+ let attempt = 0;
224
+ // The session's first frames may come before the answer that names it.
225
+ let buffered: BufferedFrame[] | undefined;
226
+
227
+ // The backend ends only a session of the connection the request names.
228
+ const closeStraySession = (forConnection: string, sessionId: string) => {
229
+ void fetch(sessionCloseUrl, {
230
+ method: "POST",
231
+ headers: {
232
+ [CONNECTION_HEADER]: forConnection,
233
+ [SESSION_HEADER]: sessionId,
220
234
  },
221
- });
222
- activeHandle = handle;
235
+ credentials: "include",
236
+ }).catch(() => {});
223
237
  };
224
- const reconnect = () => reconnector.schedule(open);
225
- const retire = (handle: SseStreamHandle): boolean => {
226
- if (handle !== activeHandle || reconnector.isCancelled()) return false;
227
- activeHandle = undefined;
228
- callbacks.onDisconnected();
229
- return true;
238
+
239
+ const adopt = (forConnection: string, greeting: SessionGreeting) => {
240
+ const sessionId = String(greeting.sessionId);
241
+ handleHello(state, greeting);
242
+ const pending = buffered ?? [];
243
+ buffered = undefined;
244
+ for (const frame of pending) {
245
+ if (frame.sessionId === sessionId) dispatch(frame.eventName, frame.data);
246
+ }
247
+ retries.resetDelay();
248
+ connection.announceSession(forConnection, sessionId);
249
+ callbacks.onConnected();
230
250
  };
231
- open();
232
- return () => {
233
- reconnector.cancel();
234
- activeHandle?.close();
235
- activeHandle = undefined;
251
+
252
+ const openSession = async (forConnection: string): Promise<void> => {
253
+ const ownAttempt = ++attempt;
254
+ const isCurrent = () =>
255
+ !retries.isCancelled() &&
256
+ connectionId === forConnection &&
257
+ attempt === ownAttempt;
258
+ const retry = () =>
259
+ retries.schedule(() => {
260
+ if (isCurrent()) void openSession(forConnection);
261
+ });
262
+ buffered = [];
263
+ let response: Response;
264
+ try {
265
+ response = await fetch(`${baseUrl}${REALTIME_SESSION_PATH}`, {
266
+ method: "POST",
267
+ // The dashboard's backend proxy relays this header by name.
268
+ headers: { [CONNECTION_HEADER]: forConnection },
269
+ credentials: "include",
270
+ });
271
+ } catch (error) {
272
+ if (isCurrent()) recoverFromError(error, retry, callbacks);
273
+ return;
274
+ }
275
+ const greeting = response.ok ? await readGreeting(response) : null;
276
+ const sessionId = greeting?.sessionId;
277
+ if (!isCurrent()) {
278
+ if (typeof sessionId === "string") {
279
+ closeStraySession(forConnection, sessionId);
280
+ }
281
+ return;
282
+ }
283
+ if (response.status === HTTP_UNAUTHORIZED) {
284
+ recoverFromError(unauthorizedError(), retry, callbacks);
285
+ return;
286
+ }
287
+ if (!response.ok) {
288
+ retry();
289
+ return;
290
+ }
291
+ // The backend does not know the connection: it went, or this request
292
+ // reached an instance that does not hold it. The leader opens a new one,
293
+ // which this tab opens its session on; the backoff spaces the attempts.
294
+ if (typeof sessionId !== "string") {
295
+ buffered = undefined;
296
+ retries.schedule(() => {
297
+ if (isCurrent()) connection.reopen(forConnection);
298
+ });
299
+ return;
300
+ }
301
+ adopt(forConnection, greeting!);
302
+ };
303
+
304
+ // A new session on the connection the tab keeps: the backend ended the
305
+ // current one alone, or the tab's account or workspace changed.
306
+ const renewSession = () => {
307
+ if (retries.isCancelled() || !connectionId) return;
308
+ const sessionId = state.sessionId.value;
309
+ if (sessionId) {
310
+ closeStraySession(connectionId, sessionId);
311
+ callbacks.onDisconnected();
312
+ }
313
+ void openSession(connectionId);
314
+ };
315
+
316
+ const connection = joinSharedRealtimeConnection({
317
+ name: `${CONNECTION_NAME_PREFIX}:${baseUrl}:${userKey}`,
318
+ streamUrl: `${baseUrl}${REALTIME_CONNECTION_PATH}`,
319
+ sessionCloseUrl,
320
+ onConnection: (next) => {
321
+ if (retries.isCancelled()) return;
322
+ if (state.sessionId.value) callbacks.onDisconnected();
323
+ buffered = undefined;
324
+ connectionId = next;
325
+ if (next) void openSession(next);
326
+ },
327
+ onFrame: (sessionId, eventName, data) => {
328
+ // The connection's own frames reach a tab once it has a session on it.
329
+ if (sessionId === undefined) {
330
+ if (state.sessionId.value) dispatch(eventName, data);
331
+ return;
332
+ }
333
+ if (sessionId === state.sessionId.value) {
334
+ if (eventName === SESSION_ENDED_EVENT) {
335
+ renewSession();
336
+ return;
337
+ }
338
+ dispatch(eventName, data);
339
+ return;
340
+ }
341
+ if (buffered && buffered.length < MAX_BUFFERED_FRAMES) {
342
+ buffered.push({ sessionId, eventName, data });
343
+ }
344
+ },
345
+ refreshSession: callbacks.refreshSession,
346
+ redirectToAuth: callbacks.redirectToAuth,
347
+ });
348
+
349
+ return {
350
+ renewSession,
351
+ cancel: () => {
352
+ retries.cancel();
353
+ connection.leave();
354
+ },
236
355
  };
237
356
  };
238
357
 
@@ -316,6 +435,7 @@ const resetSession = (state: UserRealtimeState): void => {
316
435
  state.sessionId.value = undefined;
317
436
  state.appliedPageId.value = null;
318
437
  state.pageTopics.value = [];
438
+ state.devReloadTopic.value = undefined;
319
439
  state.snapshots.clear();
320
440
  };
321
441
 
@@ -328,11 +448,13 @@ const buildApi = (): UserRealtimeApi => {
328
448
  const appliedPageId = ref<string | null>(null);
329
449
  const pageId = ref<string | undefined>(undefined);
330
450
  const pageTopics = ref<string[]>([]);
451
+ const devReloadTopic = ref<string | undefined>(undefined);
331
452
  const state: UserRealtimeState = {
332
453
  sessionId,
333
454
  pageId,
334
455
  menuTopics,
335
456
  pageTopics,
457
+ devReloadTopic,
336
458
  appliedPageId,
337
459
  handlers: new Map(),
338
460
  snapshots: new Map(),
@@ -340,34 +462,41 @@ const buildApi = (): UserRealtimeApi => {
340
462
  };
341
463
 
342
464
  const config = useDmsRuntimeConfig();
343
- const { loggedIn } = useDmsUserSession();
465
+ const { loggedIn, user, session } = useDmsUserSession<
466
+ ConnectionUser,
467
+ SessionScopeFields
468
+ >();
344
469
  const { refreshSession, redirectToAuth } = useSessionRecovery();
345
470
  const baseUrl = config.public.dms.baseURL as string;
346
471
 
347
472
  if (!import.meta.env.SSR) {
473
+ // One connection per user: the tabs of another user (an account
474
+ // switched in one tab) share theirs, and never read this one's events.
348
475
  watch(
349
- () => loggedIn.value,
350
- (isLoggedIn) => {
351
- state.cancelConnect?.();
352
- state.cancelConnect = undefined;
476
+ () => (loggedIn.value ? String(user.value?._id ?? "") : undefined),
477
+ (userKey) => {
478
+ state.connection?.cancel();
479
+ state.connection = undefined;
353
480
  resetSession(state);
354
481
  state.menuTopics.value = [];
355
- if (!isLoggedIn) return;
356
- state.cancelConnect = connectWithBackoff(
357
- `${baseUrl}${REALTIME_USER_PATH}`,
358
- state,
359
- {
360
- onConnected: () => {
361
- void reconcilePage(state, baseUrl);
362
- },
363
- onDisconnected: () => resetSession(state),
364
- refreshSession,
365
- redirectToAuth,
482
+ if (userKey === undefined) return;
483
+ state.connection = connectTab(state, baseUrl, userKey, {
484
+ onConnected: () => {
485
+ void reconcilePage(state, baseUrl);
366
486
  },
367
- );
487
+ onDisconnected: () => resetSession(state),
488
+ refreshSession,
489
+ redirectToAuth,
490
+ });
368
491
  },
369
492
  { immediate: true },
370
493
  );
494
+ // A switch of account or workspace within the same user: the tab's session
495
+ // is renewed on the connection it already shares.
496
+ watch(
497
+ () => sessionScopeOf(session?.value),
498
+ () => state.connection?.renewSession(),
499
+ );
371
500
  }
372
501
 
373
502
  const subscribe = (topic: string, handler: RealtimeTopicHandler) => {
@@ -396,7 +525,15 @@ const buildApi = (): UserRealtimeApi => {
396
525
  await reconcilePage(state, baseUrl);
397
526
  };
398
527
 
399
- return { subscribe, setPage, sessionId, pageId, menuTopics, pageTopics };
528
+ return {
529
+ subscribe,
530
+ setPage,
531
+ sessionId,
532
+ pageId,
533
+ menuTopics,
534
+ pageTopics,
535
+ devReloadTopic,
536
+ };
400
537
  };
401
538
 
402
539
  export function useUserRealtime(): UserRealtimeApi {
@@ -3,16 +3,11 @@ import {
3
3
  type ComposedTextTranslate,
4
4
  resolveComposedText,
5
5
  } from "../../utils/composedText";
6
+ import { getPluginI18n } from "./useTranslation";
6
7
 
7
- /**
8
- * Writes the texts a block receives — a plain or `$`-prefixed string, or a
9
- * `ComposedText` from its options or its route — in the reader's language,
10
- * through {@link resolveComposedText}. Each call reads the active locale, so a
11
- * text written in a `computed` follows a language switch.
12
- */
13
- export function useComposedText() {
14
- const { t, locale } = useI18n();
8
+ type ComposedTextI18n = Pick<ReturnType<typeof useI18n>, "t" | "locale">;
15
9
 
10
+ function composedTextOf({ t, locale }: ComposedTextI18n) {
16
11
  // vue-i18n reads a third argument as a plural count only when there is one.
17
12
  const translate: ComposedTextTranslate = (key, named, plural) =>
18
13
  plural === undefined ? t(key, named) : t(key, named, plural);
@@ -23,3 +18,21 @@ export function useComposedText() {
23
18
 
24
19
  return { processText };
25
20
  }
21
+
22
+ /**
23
+ * Writes the texts a block receives — a plain or `$`-prefixed string, or a
24
+ * `ComposedText` from its options or its route — in the reader's language,
25
+ * through {@link resolveComposedText}. Each call reads the active locale, so a
26
+ * text written in a `computed` follows a language switch.
27
+ */
28
+ export function useComposedText() {
29
+ return composedTextOf(useI18n());
30
+ }
31
+
32
+ /**
33
+ * {@link useComposedText} outside a component setup — in a data-type
34
+ * formatter, which runs wherever a value is drawn (see `getAppTranslation`).
35
+ */
36
+ export function getAppComposedText() {
37
+ return composedTextOf(getPluginI18n().i18n);
38
+ }
@@ -6,9 +6,10 @@ export type I18nTranslate = (
6
6
  ) => string;
7
7
 
8
8
  /**
9
- * i18n access for DMS app plugins, where `useI18n()` is unavailable: returns the
10
- * global composer plus an {@link I18nTranslate} bound to it. Inside
11
- * components prefer `useI18n` / {@link useTranslation}.
9
+ * i18n access for DMS app plugins, where `useI18n()` is unavailable: returns
10
+ * the global composer plus an {@link I18nTranslate} bound to it. Inside
11
+ * components prefer `useI18n` / {@link useTranslation}; in a data-type
12
+ * formatter, {@link getAppTranslation}.
12
13
  */
13
14
  export function getPluginI18n() {
14
15
  const { $i18n: i18n } = useDmsApp();
@@ -88,9 +89,9 @@ export function resolveOptionalI18nKey(
88
89
  return isUntranslatedKey ? undefined : resolved;
89
90
  }
90
91
 
91
- export const useTranslation = () => {
92
- const { t } = useI18n();
92
+ type ComposerTranslate = ReturnType<typeof useI18n>["t"];
93
93
 
94
+ function translationOf(t: ComposerTranslate) {
94
95
  function processI18n(key: string, params?: Record<string, unknown> | null) {
95
96
  return resolveI18nKey(t, key, params);
96
97
  }
@@ -106,4 +107,23 @@ export const useTranslation = () => {
106
107
  processI18n,
107
108
  processApiMessage,
108
109
  };
110
+ }
111
+
112
+ export const useTranslation = () => translationOf(useI18n().t);
113
+
114
+ /**
115
+ * {@link useTranslation} for code that runs wherever a value is drawn — a
116
+ * data-type formatter, a render function, a `computed` read by a watcher, an
117
+ * export — with no component setup active, where `useI18n()` throws. It reads
118
+ * the application's global composer (the one `useI18n()` returns), and each
119
+ * call reads its active locale, so a text written in a `computed` follows a
120
+ * language switch. `t` is that composer's own, for a key with no `$` prefix
121
+ * (a plural count included).
122
+ */
123
+ export const getAppTranslation = () => {
124
+ const { t } = getPluginI18n().i18n;
125
+ return { ...translationOf(t), t };
109
126
  };
127
+
128
+ /** What {@link getAppTranslation} returns. */
129
+ export type AppTranslation = ReturnType<typeof getAppTranslation>;
@@ -67,16 +67,14 @@ export function resolveApiErrorMessage(error: unknown): string {
67
67
 
68
68
  export function useApiError(error: unknown, options?: Options) {
69
69
  const toast = useToast();
70
- const dmsApp = useDmsApp();
71
- const { t } = dmsApp.$i18n;
72
- const translate = (message: string) => resolveApiMessage(t, message);
70
+ const { processApiMessage } = getAppTranslation();
73
71
 
74
72
  const title = options?.title || resolveApiErrorTitle(error);
75
73
  const description = options?.description || resolveApiErrorMessage(error);
76
74
 
77
75
  toast.add({
78
- title: translate(title),
79
- description: translate(description),
76
+ title: processApiMessage(title),
77
+ description: processApiMessage(description),
80
78
  color: "error",
81
79
  });
82
80
  }