@antelopejs/dms 0.8.3 → 0.9.1

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 (424) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/data/module-store/catalog.json +71 -0
  3. package/dist/config.d.ts +7 -0
  4. package/dist/config.js +8 -0
  5. package/dist/config.js.map +1 -1
  6. package/dist/implementations/dms/frontend-backend-proxies.d.ts +3 -0
  7. package/dist/implementations/dms/frontend-backend-proxies.js +39 -0
  8. package/dist/implementations/dms/frontend-backend-proxies.js.map +1 -0
  9. package/dist/implementations/dms/frontend-bootstrap.d.ts +1 -0
  10. package/dist/implementations/dms/frontend-bootstrap.js +13 -0
  11. package/dist/implementations/dms/frontend-bootstrap.js.map +1 -1
  12. package/dist/implementations/dms/frontend-modules.d.ts +6 -2
  13. package/dist/implementations/dms/frontend-modules.js +37 -8
  14. package/dist/implementations/dms/frontend-modules.js.map +1 -1
  15. package/dist/implementations/dms/frontend-ready.d.ts +8 -0
  16. package/dist/implementations/dms/frontend-ready.js +34 -0
  17. package/dist/implementations/dms/frontend-ready.js.map +1 -0
  18. package/dist/implementations/dms/manifest.d.ts +7 -0
  19. package/dist/implementations/dms/manifest.js +13 -0
  20. package/dist/implementations/dms/manifest.js.map +1 -1
  21. package/dist/implementations/dms/menu-categories.d.ts +16 -0
  22. package/dist/implementations/dms/menu-categories.js +30 -0
  23. package/dist/implementations/dms/menu-categories.js.map +1 -0
  24. package/dist/implementations/dms/nav-badges.d.ts +0 -8
  25. package/dist/implementations/dms/nav-badges.js +0 -16
  26. package/dist/implementations/dms/nav-badges.js.map +1 -1
  27. package/dist/implementations/dms/page.d.ts +13 -5
  28. package/dist/implementations/dms/page.js +72 -41
  29. package/dist/implementations/dms/page.js.map +1 -1
  30. package/dist/implementations/dms-html-render/index.js +5 -2
  31. package/dist/implementations/dms-html-render/index.js.map +1 -1
  32. package/dist/implementations/dms-notifications/index.d.ts +4 -3
  33. package/dist/implementations/dms-notifications/index.js +14 -6
  34. package/dist/implementations/dms-notifications/index.js.map +1 -1
  35. package/dist/pages/settings/settings-index.js +3 -2
  36. package/dist/pages/settings/settings-index.js.map +1 -1
  37. package/dist/pages/settings/users/invites.js +2 -2
  38. package/dist/pages/settings/users/invites.js.map +1 -1
  39. package/dist/pages/settings/users/member-change-notifications.js +17 -6
  40. package/dist/pages/settings/users/member-change-notifications.js.map +1 -1
  41. package/dist/pages/settings/users/member-invite-form.d.ts +3 -2
  42. package/dist/pages/settings/users/member-invite-form.js +7 -4
  43. package/dist/pages/settings/users/member-invite-form.js.map +1 -1
  44. package/dist/pages/settings/users/member-roles-field.js +2 -1
  45. package/dist/pages/settings/users/member-roles-field.js.map +1 -1
  46. package/dist/pages/settings/users/members.js +4 -1
  47. package/dist/pages/settings/users/members.js.map +1 -1
  48. package/dist/pages/settings/users/notifications.d.ts +6 -0
  49. package/dist/pages/settings/users/notifications.js +23 -15
  50. package/dist/pages/settings/users/notifications.js.map +1 -1
  51. package/dist/pages/settings/users/profile.js +1 -1
  52. package/dist/pages/settings/users/profile.js.map +1 -1
  53. package/dist/pages/settings/users/region-form.d.ts +1 -1
  54. package/dist/pages/settings/users/region-form.js +2 -1
  55. package/dist/pages/settings/users/region-form.js.map +1 -1
  56. package/dist/pages/settings/users/role-editor-store.d.ts +3 -1
  57. package/dist/pages/settings/users/role-editor-store.js +17 -4
  58. package/dist/pages/settings/users/role-editor-store.js.map +1 -1
  59. package/dist/pages/settings/users/roles.js +1 -0
  60. package/dist/pages/settings/users/roles.js.map +1 -1
  61. package/dist/realtime/registry.d.ts +1 -0
  62. package/dist/realtime/registry.js +5 -0
  63. package/dist/realtime/registry.js.map +1 -1
  64. package/dist/routes/index.d.ts +1 -0
  65. package/dist/routes/index.js +1 -0
  66. package/dist/routes/index.js.map +1 -1
  67. package/dist/routes/realtime.d.ts +5 -1
  68. package/dist/routes/realtime.js +16 -9
  69. package/dist/routes/realtime.js.map +1 -1
  70. package/dist/routes/store.d.ts +21 -0
  71. package/dist/routes/store.js +130 -0
  72. package/dist/routes/store.js.map +1 -0
  73. package/dist/store/activity.d.ts +2 -0
  74. package/dist/store/activity.js +12 -0
  75. package/dist/store/activity.js.map +1 -0
  76. package/dist/store/cache.d.ts +8 -0
  77. package/dist/store/cache.js +29 -0
  78. package/dist/store/cache.js.map +1 -0
  79. package/dist/store/catalog.d.ts +4 -0
  80. package/dist/store/catalog.js +69 -0
  81. package/dist/store/catalog.js.map +1 -0
  82. package/dist/store/compatibility.d.ts +3 -0
  83. package/dist/store/compatibility.js +45 -0
  84. package/dist/store/compatibility.js.map +1 -0
  85. package/dist/store/declarations.d.ts +18 -0
  86. package/dist/store/declarations.js +116 -0
  87. package/dist/store/declarations.js.map +1 -0
  88. package/dist/store/frontend-compatibility.d.ts +18 -0
  89. package/dist/store/frontend-compatibility.js +93 -0
  90. package/dist/store/frontend-compatibility.js.map +1 -0
  91. package/dist/store/interfaces.d.ts +6 -0
  92. package/dist/store/interfaces.js +126 -0
  93. package/dist/store/interfaces.js.map +1 -0
  94. package/dist/store/listing.d.ts +2 -0
  95. package/dist/store/listing.js +180 -0
  96. package/dist/store/listing.js.map +1 -0
  97. package/dist/store/operations.d.ts +8 -0
  98. package/dist/store/operations.js +254 -0
  99. package/dist/store/operations.js.map +1 -0
  100. package/dist/store/registry.d.ts +4 -0
  101. package/dist/store/registry.js +61 -0
  102. package/dist/store/registry.js.map +1 -0
  103. package/dist/store/requirements.d.ts +14 -0
  104. package/dist/store/requirements.js +81 -0
  105. package/dist/store/requirements.js.map +1 -0
  106. package/dist/store/runtime.d.ts +11 -0
  107. package/dist/store/runtime.js +78 -0
  108. package/dist/store/runtime.js.map +1 -0
  109. package/dist/store/tarball.d.ts +2 -0
  110. package/dist/store/tarball.js +80 -0
  111. package/dist/store/tarball.js.map +1 -0
  112. package/dist/store/types.d.ts +98 -0
  113. package/dist/store/types.js +3 -0
  114. package/dist/store/types.js.map +1 -0
  115. package/dist/test/integration/account-deletion.test.js +2 -2
  116. package/dist/test/integration/account-deletion.test.js.map +1 -1
  117. package/dist/test/integration/auth/auth-key-rotation.test.js +2 -1
  118. package/dist/test/integration/auth/auth-key-rotation.test.js.map +1 -1
  119. package/dist/test/integration/auth/current-password.test.js +2 -1
  120. package/dist/test/integration/auth/current-password.test.js.map +1 -1
  121. package/dist/test/integration/auth/session-revocation.test.js +2 -1
  122. package/dist/test/integration/auth/session-revocation.test.js.map +1 -1
  123. package/dist/test/integration/auth/sign-in-country.test.js +2 -1
  124. package/dist/test/integration/auth/sign-in-country.test.js.map +1 -1
  125. package/dist/test/integration/auth/two-factor-codes.test.js +2 -1
  126. package/dist/test/integration/auth/two-factor-codes.test.js.map +1 -1
  127. package/dist/test/integration/auth/two-factor-password.test.js +3 -2
  128. package/dist/test/integration/auth/two-factor-password.test.js.map +1 -1
  129. package/dist/test/integration/email-change.test.js +2 -1
  130. package/dist/test/integration/email-change.test.js.map +1 -1
  131. package/dist/test/integration/files/component-files.test.js +2 -2
  132. package/dist/test/integration/files/component-files.test.js.map +1 -1
  133. package/dist/test/integration/member-settings-access.test.js +8 -7
  134. package/dist/test/integration/member-settings-access.test.js.map +1 -1
  135. package/dist/test/integration/menu-access-events.test.d.ts +1 -0
  136. package/dist/test/integration/menu-access-events.test.js +148 -0
  137. package/dist/test/integration/menu-access-events.test.js.map +1 -0
  138. package/dist/test/integration/notification-inbox.test.js +4 -1
  139. package/dist/test/integration/notification-inbox.test.js.map +1 -1
  140. package/dist/test/integration/notification-locked-subjects.test.js +3 -2
  141. package/dist/test/integration/notification-locked-subjects.test.js.map +1 -1
  142. package/dist/test/integration/notification-unread-badge.test.js +51 -5
  143. package/dist/test/integration/notification-unread-badge.test.js.map +1 -1
  144. package/dist/test/integration/page-layout-route-guard.test.d.ts +1 -0
  145. package/dist/test/integration/page-layout-route-guard.test.js +56 -0
  146. package/dist/test/integration/page-layout-route-guard.test.js.map +1 -0
  147. package/dist/test/integration/permission-warnings.test.js +3 -2
  148. package/dist/test/integration/permission-warnings.test.js.map +1 -1
  149. package/dist/test/integration/removed-member-notifications.test.js +3 -2
  150. package/dist/test/integration/removed-member-notifications.test.js.map +1 -1
  151. package/dist/test/integration/role-delete-reassign.test.js +2 -1
  152. package/dist/test/integration/role-delete-reassign.test.js.map +1 -1
  153. package/dist/test/integration/role-permission-ancestors.test.js +2 -1
  154. package/dist/test/integration/role-permission-ancestors.test.js.map +1 -1
  155. package/dist/test/unit/frontend-bootstrap.test.js +13 -0
  156. package/dist/test/unit/frontend-bootstrap.test.js.map +1 -1
  157. package/dist/test/unit/implementations/dms/category-slug-registry.test.js +14 -5
  158. package/dist/test/unit/implementations/dms/category-slug-registry.test.js.map +1 -1
  159. package/dist/test/unit/implementations/dms/frontend-backend-proxies.test.d.ts +1 -0
  160. package/dist/test/unit/implementations/dms/frontend-backend-proxies.test.js +66 -0
  161. package/dist/test/unit/implementations/dms/frontend-backend-proxies.test.js.map +1 -0
  162. package/dist/test/unit/implementations/dms/manifest-strip.test.js +26 -0
  163. package/dist/test/unit/implementations/dms/manifest-strip.test.js.map +1 -1
  164. package/dist/test/unit/implementations/dms/nav-badges.test.js +23 -9
  165. package/dist/test/unit/implementations/dms/nav-badges.test.js.map +1 -1
  166. package/dist/test/unit/implementations/dms-html-render/generate-html.test.js +22 -4
  167. package/dist/test/unit/implementations/dms-html-render/generate-html.test.js.map +1 -1
  168. package/dist/test/unit/interfaces/dms/page-layout-query.test.js +12 -0
  169. package/dist/test/unit/interfaces/dms/page-layout-query.test.js.map +1 -1
  170. package/dist/test/unit/interfaces/dms/page-teardown.test.js +1 -1
  171. package/dist/test/unit/interfaces/dms/page-teardown.test.js.map +1 -1
  172. package/dist/test/unit/pages/settings/member-roles-field.test.js +1 -1
  173. package/dist/test/unit/pages/settings/member-roles-field.test.js.map +1 -1
  174. package/dist/test/unit/pages/settings/role-delete-confirm.test.js.map +1 -1
  175. package/dist/test/unit/realtime/menu-topics.test.d.ts +1 -0
  176. package/dist/test/unit/realtime/menu-topics.test.js +26 -0
  177. package/dist/test/unit/realtime/menu-topics.test.js.map +1 -0
  178. package/dist/test/unit/store/store.test.d.ts +1 -0
  179. package/dist/test/unit/store/store.test.js +127 -0
  180. package/dist/test/unit/store/store.test.js.map +1 -0
  181. package/dist/validation/store.schema.d.ts +39 -0
  182. package/dist/validation/store.schema.js +27 -0
  183. package/dist/validation/store.schema.js.map +1 -0
  184. package/frontend-vue/deferred-plugins.ts +3 -4
  185. package/frontend-vue/dms.email.ts +5 -1
  186. package/frontend-vue/dms.frontend.ts +15 -5
  187. package/frontend-vue/dms.server.ts +74 -0
  188. package/frontend-vue/layers/dms-auth/app/composables/useLogout.ts +1 -1
  189. package/frontend-vue/layers/dms-auth/app/composables/usePostLoginRedirect.ts +16 -3
  190. package/frontend-vue/layers/dms-auth/app/custom-pages/auth/2fa.vue +3 -1
  191. package/frontend-vue/layers/dms-auth/app/custom-pages/auth/backup.vue +3 -1
  192. package/frontend-vue/layers/dms-auth/app/custom-pages/auth/forgot-validation.vue +1 -1
  193. package/frontend-vue/layers/dms-auth/app/custom-pages/auth/oauth-complete.vue +26 -3
  194. package/frontend-vue/layers/dms-auth/app/custom-pages/auth/recover.vue +1 -1
  195. package/frontend-vue/layers/dms-auth/app/custom-pages/auth/signup.vue +22 -25
  196. package/frontend-vue/layers/dms-auth/app/custom-pages/auth/validate.vue +1 -1
  197. package/frontend-vue/layers/dms-auth/app/middleware/auth.ts +5 -7
  198. package/frontend-vue/layers/dms-auth/app/plugins/permissions-preload.ts +17 -0
  199. package/frontend-vue/layers/dms-auth/server/accounts.ts +135 -0
  200. package/frontend-vue/layers/dms-auth/server/actions.ts +251 -0
  201. package/frontend-vue/layers/dms-auth/server/oauth.ts +260 -0
  202. package/frontend-vue/layers/dms-auth/server/session.ts +158 -0
  203. package/frontend-vue/layers/dms-core/app/build/utils/asyncDataScope.ts +47 -0
  204. package/frontend-vue/layers/dms-core/app/build/utils/permission-preview.ts +6 -2
  205. package/frontend-vue/layers/dms-core/app/composables/auth/useApiData.ts +148 -0
  206. package/frontend-vue/layers/dms-core/app/composables/auth/useAuthFetch.ts +7 -20
  207. package/frontend-vue/layers/dms-core/app/composables/auth/useCurrentUser.ts +2 -2
  208. package/frontend-vue/layers/dms-core/app/composables/auth/useMultiAccount.ts +18 -5
  209. package/frontend-vue/layers/dms-core/app/composables/auth/usePermissions.ts +35 -6
  210. package/frontend-vue/layers/dms-core/app/composables/auth/useSessionRecovery.ts +1 -1
  211. package/frontend-vue/layers/dms-core/app/composables/period/compareResolvers.ts +20 -14
  212. package/frontend-vue/layers/dms-core/app/composables/period/periodClock.ts +121 -0
  213. package/frontend-vue/layers/dms-core/app/composables/period/periodRenderNow.ts +42 -0
  214. package/frontend-vue/layers/dms-core/app/composables/period/periodTimeZone.ts +31 -0
  215. package/frontend-vue/layers/dms-core/app/composables/period/presetResolvers.ts +48 -19
  216. package/frontend-vue/layers/dms-core/app/composables/period/usePeriod.ts +33 -4
  217. package/frontend-vue/layers/dms-core/app/composables/period/usePeriodScope.ts +78 -25
  218. package/frontend-vue/layers/dms-core/app/composables/realtime/usePageRealtime.ts +3 -0
  219. package/frontend-vue/layers/dms-core/app/composables/realtime/useUserRealtime.ts +36 -6
  220. package/frontend-vue/layers/dms-core/app/composables/user/useUserRegionalPreferences.ts +1 -1
  221. package/frontend-vue/layers/dms-core/app/plugins/async-data-scope.ts +26 -0
  222. package/frontend-vue/layers/dms-core/app/utils/pageRoutes.ts +22 -0
  223. package/frontend-vue/layers/dms-core/app/utils/regional.ts +36 -3
  224. package/frontend-vue/layers/dms-core/app/utils/trailingDeduper.ts +10 -1
  225. package/frontend-vue/layers/dms-core/shared/types/app-config.ts +48 -4
  226. package/frontend-vue/layers/dms-layout/app/app.config.ts +42 -23
  227. package/frontend-vue/layers/dms-layout/app/assets/css/main.css +72 -20
  228. package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardFrame.vue +9 -0
  229. package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardSidebar.vue +0 -3
  230. package/frontend-vue/layers/dms-layout/app/build/components/notification/NotificationPopover.vue +9 -3
  231. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleStoreDependencyPicker.vue +136 -0
  232. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleStoreRequirements.vue +98 -0
  233. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleStoreTile.vue +247 -29
  234. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleTile.vue +19 -2
  235. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/members/MemberRolePicker.vue +13 -12
  236. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/notification/NotificationInboxDisplay.vue +17 -7
  237. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/notification/NotificationPreferencesForm.vue +0 -1
  238. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/profile/ProfilePersonalInfo.vue +36 -33
  239. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/profile/ProfileSecuritySummary.vue +1 -2
  240. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/roles/role-types.ts +5 -4
  241. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/security/SecuritySessions.vue +35 -28
  242. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/security/SecurityStatus.vue +0 -1
  243. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/security/SecurityTwoFactor.vue +1 -1
  244. package/frontend-vue/layers/dms-layout/app/build/composables/dev-reload/useDevReloadHolder.ts +7 -6
  245. package/frontend-vue/layers/dms-layout/app/build/composables/layout/sidePanelState.ts +1 -1
  246. package/frontend-vue/layers/dms-layout/app/build/composables/navigation/useVisitNavBadges.ts +26 -0
  247. package/frontend-vue/layers/dms-layout/app/build/composables/notification/useNotificationCatalog.ts +25 -23
  248. package/frontend-vue/layers/dms-layout/app/build/composables/notification/useNotificationPreferences.ts +44 -23
  249. package/frontend-vue/layers/dms-layout/app/build/composables/security/useSessionHandoff.ts +1 -1
  250. package/frontend-vue/layers/dms-layout/app/build/composables/settings/profile/preferencesSummary.ts +1 -1
  251. package/frontend-vue/layers/dms-layout/app/build/composables/settings/profile/useAccountData.ts +3 -2
  252. package/frontend-vue/layers/dms-layout/app/build/composables/settings/profile/useProfileAvatar.ts +1 -18
  253. package/frontend-vue/layers/dms-layout/app/build/composables/settings/profile/useProfilePreferences.ts +19 -23
  254. package/frontend-vue/layers/dms-layout/app/build/composables/settings/security/useSecurityFormat.ts +6 -3
  255. package/frontend-vue/layers/dms-layout/app/build/composables/settings/security/useSecurityOverview.ts +13 -18
  256. package/frontend-vue/layers/dms-layout/app/build/utils/module-store-catalog.ts +186 -120
  257. package/frontend-vue/layers/dms-layout/app/build/utils/module-store-install.ts +146 -0
  258. package/frontend-vue/layers/dms-layout/app/build/utils/modules-catalog.ts +8 -6
  259. package/frontend-vue/layers/dms-layout/app/build/utils/publicErrorMessage.ts +19 -0
  260. package/frontend-vue/layers/dms-layout/app/components/navigation/CollapsedMenuItem.vue +0 -11
  261. package/frontend-vue/layers/dms-layout/app/components/navigation/NavigationMenu.vue +3 -13
  262. package/frontend-vue/layers/dms-layout/app/composables/general/useSystemState.ts +7 -11
  263. package/frontend-vue/layers/dms-layout/app/composables/notification/useNotifications.ts +103 -22
  264. package/frontend-vue/layers/dms-layout/app/composables/page/useModuleStore.ts +109 -0
  265. package/frontend-vue/layers/dms-layout/app/composables/page/useModulesListing.ts +25 -102
  266. package/frontend-vue/layers/dms-layout/app/composables/page/usePageComponentPreload.ts +0 -10
  267. package/frontend-vue/layers/dms-layout/app/composables/page/useSiteLayout.ts +177 -96
  268. package/frontend-vue/layers/dms-layout/app/composables/profile/useUserLanguage.ts +3 -2
  269. package/frontend-vue/layers/dms-layout/app/custom-pages/modules.vue +341 -62
  270. package/frontend-vue/layers/dms-layout/app/emails/components/EmailLayout.vue +31 -11
  271. package/frontend-vue/layers/dms-layout/app/error.vue +5 -2
  272. package/frontend-vue/layers/dms-layout/app/middleware/module-routing.global.ts +1 -1
  273. package/frontend-vue/layers/dms-layout/app/middleware/page-leave-guard.global.ts +2 -2
  274. package/frontend-vue/layers/dms-layout/app/pages/[...slug].vue +12 -9
  275. package/frontend-vue/layers/dms-layout/app/plugins/dms-menu-sync.client.ts +31 -3
  276. package/frontend-vue/layers/dms-layout/app/plugins/favicon.ts +58 -0
  277. package/frontend-vue/layers/dms-layout/app/plugins/notification-stream.client.ts +15 -7
  278. package/frontend-vue/layers/dms-layout/app/plugins/profile-sync.client.ts +5 -1
  279. package/frontend-vue/layers/dms-layout/app/plugins/regional-preferences.ts +10 -4
  280. package/frontend-vue/layers/dms-layout/app/plugins/time-zone-cookie.client.ts +12 -0
  281. package/frontend-vue/layers/dms-layout/app/types/module-store.ts +134 -0
  282. package/frontend-vue/layers/dms-layout/app/types/page.ts +13 -0
  283. package/frontend-vue/layers/dms-layout/i18n/locales/layout-en-GB.json +107 -9
  284. package/frontend-vue/layers/dms-layout/i18n/locales/layout-fr-FR.json +107 -9
  285. package/frontend-vue/layers/dms-layout/server/html-render.ts +58 -0
  286. package/frontend-vue/layers/dms-layout/server/render-token.ts +95 -0
  287. package/frontend-vue/layers/dms-onboarding/app/composables/onboarding/useOnboarding.ts +25 -21
  288. package/frontend-vue/layers/dms-onboarding/app/middleware/onboarding.global.ts +1 -1
  289. package/frontend-vue/layers/dms-onboarding/app/plugins/onboarding-preload.ts +9 -0
  290. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/registerDefaults.ts +4 -1
  291. package/frontend-vue/layers/dms-ui/app/build/composables/files/useFileMetadata.ts +162 -0
  292. package/frontend-vue/layers/dms-ui/app/build/composables/form/formRecordLoad.ts +23 -4
  293. package/frontend-vue/layers/dms-ui/app/build/composables/live/usePageDataRefresh.ts +101 -0
  294. package/frontend-vue/layers/dms-ui/app/build/composables/navigation/useNavBadges.ts +33 -6
  295. package/frontend-vue/layers/dms-ui/app/build/composables/period/periodDisplay.ts +19 -16
  296. package/frontend-vue/layers/dms-ui/app/build/composables/period/periodQuery.ts +40 -20
  297. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableFooter.ts +6 -8
  298. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableListData.ts +42 -14
  299. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableRows.ts +19 -8
  300. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViews.ts +1 -1
  301. package/frontend-vue/layers/dms-ui/app/components/chart/ChartCard.vue +5 -2
  302. package/frontend-vue/layers/dms-ui/app/components/flow-canvas/FlowCanvas.vue +9 -9
  303. package/frontend-vue/layers/dms-ui/app/components/form/Form.vue +20 -18
  304. package/frontend-vue/layers/dms-ui/app/components/form/components/Cascader.vue +2 -1
  305. package/frontend-vue/layers/dms-ui/app/components/form/components/File.vue +26 -132
  306. package/frontend-vue/layers/dms-ui/app/components/form/components/Image.vue +40 -105
  307. package/frontend-vue/layers/dms-ui/app/components/form/components/InputAddress.vue +2 -0
  308. package/frontend-vue/layers/dms-ui/app/components/form/components/Relation.vue +4 -4
  309. package/frontend-vue/layers/dms-ui/app/components/period/PeriodSelector.vue +9 -13
  310. package/frontend-vue/layers/dms-ui/app/components/table-view/CascaderPath.vue +2 -1
  311. package/frontend-vue/layers/dms-ui/app/components/table-view/FilePreview.vue +12 -24
  312. package/frontend-vue/layers/dms-ui/app/components/table-view/ImagePreview.vue +17 -29
  313. package/frontend-vue/layers/dms-ui/app/components/table-view/KanbanBoard.vue +7 -3
  314. package/frontend-vue/layers/dms-ui/app/components/table-view/TableView.vue +20 -8
  315. package/frontend-vue/layers/dms-ui/app/components/top-list/TopListCard.vue +8 -11
  316. package/frontend-vue/layers/dms-ui/app/composables/chart/useChartFetch.ts +146 -182
  317. package/frontend-vue/layers/dms-ui/app/composables/live/usePageLiveRefresh.ts +8 -2
  318. package/frontend-vue/layers/dms-ui/app/composables/useFileReadUrls.ts +11 -99
  319. package/frontend-vue/layers/dms-ui/app/utils/blockRefresh.ts +32 -3
  320. package/frontend-vue/package.json +3 -3
  321. package/frontend-vue/pnpm-lock.yaml +16 -22
  322. package/frontend-vue/server/private-options.ts +27 -0
  323. package/frontend-vue/tests/async-data-scope.test.ts +121 -0
  324. package/frontend-vue/tests/auth-middleware-error.test.ts +3 -3
  325. package/frontend-vue/tests/banner-block-fetch.test.ts +3 -0
  326. package/frontend-vue/tests/block-fetch-options-forwarding.test.ts +3 -0
  327. package/frontend-vue/tests/block-items-period-realtime.test.ts +3 -0
  328. package/frontend-vue/tests/block-not-found.test.ts +16 -2
  329. package/frontend-vue/tests/block-page-refresh.test.ts +3 -0
  330. package/frontend-vue/tests/block-route-params.test.ts +3 -0
  331. package/frontend-vue/tests/branding-defaults.test.ts +34 -0
  332. package/frontend-vue/tests/builtin-emails.test.ts +23 -2
  333. package/frontend-vue/tests/chart-live-pause.test.ts +4 -1
  334. package/frontend-vue/tests/chart-value-propagation.test.ts +4 -1
  335. package/frontend-vue/tests/composed-text-blocks.test.ts +3 -0
  336. package/frontend-vue/tests/email-layout.test.ts +66 -4
  337. package/frontend-vue/tests/error-page-message.test.ts +25 -2
  338. package/frontend-vue/tests/favicon.test.ts +131 -0
  339. package/frontend-vue/tests/file-fields-ssr.test.ts +145 -0
  340. package/frontend-vue/tests/file-read-urls.test.ts +145 -34
  341. package/frontend-vue/tests/file-unavailable.test.ts +5 -1
  342. package/frontend-vue/tests/fixtures/dataEngine.ts +44 -0
  343. package/frontend-vue/tests/form-load-error.test.ts +55 -1
  344. package/frontend-vue/tests/helpers/api-data-runtime.ts +77 -0
  345. package/frontend-vue/tests/image-hydration-ids.test.ts +21 -16
  346. package/frontend-vue/tests/image-reference-preservation.test.ts +59 -53
  347. package/frontend-vue/tests/input-number-adjust.test.ts +14 -0
  348. package/frontend-vue/tests/key-value-list-copy.test.ts +3 -0
  349. package/frontend-vue/tests/member-role-picker.test.ts +4 -2
  350. package/frontend-vue/tests/menu-sync-reconnect.test.ts +47 -0
  351. package/frontend-vue/tests/metadata-retry.test.ts +5 -1
  352. package/frontend-vue/tests/module-routing.test.ts +2 -5
  353. package/frontend-vue/tests/module-store-catalog.test.ts +270 -51
  354. package/frontend-vue/tests/modules-catalog.test.ts +17 -4
  355. package/frontend-vue/tests/modules-listing-transfer.test.ts +95 -0
  356. package/frontend-vue/tests/multi-account-validate.test.ts +23 -1
  357. package/frontend-vue/tests/nav-badge-tones.test.ts +66 -3
  358. package/frontend-vue/tests/notification-counts-from-writes.test.ts +223 -0
  359. package/frontend-vue/tests/notification-counts-transfer.test.ts +88 -0
  360. package/frontend-vue/tests/notification-inbox-display.test.ts +57 -5
  361. package/frontend-vue/tests/notification-popover-errors.test.ts +21 -1
  362. package/frontend-vue/tests/oauth-complete-handoff.test.ts +209 -0
  363. package/frontend-vue/tests/onboarding-middleware.test.ts +1 -1
  364. package/frontend-vue/tests/onboarding-transfer.test.ts +106 -0
  365. package/frontend-vue/tests/page-data-refresh.test.ts +141 -0
  366. package/frontend-vue/tests/page-fill-height.test.ts +16 -3
  367. package/frontend-vue/tests/page-no-visible-blocks.test.ts +12 -2
  368. package/frontend-vue/tests/page-realtime-remount.test.ts +1 -1
  369. package/frontend-vue/tests/page-record-label.test.ts +0 -7
  370. package/frontend-vue/tests/page-validation-redirect.test.ts +12 -2
  371. package/frontend-vue/tests/period-hydration-now.test.ts +185 -0
  372. package/frontend-vue/tests/period-live-pause.test.ts +3 -0
  373. package/frontend-vue/tests/period-reader-zone.test.ts +137 -0
  374. package/frontend-vue/tests/period-scope-first-fetch.test.ts +17 -1
  375. package/frontend-vue/tests/period-ssr.test.ts +195 -0
  376. package/frontend-vue/tests/permission-preview-partial.test.ts +0 -2
  377. package/frontend-vue/tests/permissions-transfer.test.ts +194 -0
  378. package/frontend-vue/tests/post-login-redirect.test.ts +10 -5
  379. package/frontend-vue/tests/profile-personal-info-ssr.test.ts +150 -0
  380. package/frontend-vue/tests/profile-preferences-summary.test.ts +15 -11
  381. package/frontend-vue/tests/realtime-reconnect.test.ts +22 -3
  382. package/frontend-vue/tests/roles-name-errors.test.ts +1 -1
  383. package/frontend-vue/tests/row-side-panel.test.ts +1 -1
  384. package/frontend-vue/tests/security-sessions-load-error.test.ts +3 -1
  385. package/frontend-vue/tests/server-routes-vs-pages.test.ts +26 -0
  386. package/frontend-vue/tests/server-routes.test.ts +582 -0
  387. package/frontend-vue/tests/session-handoff.test.ts +1 -1
  388. package/frontend-vue/tests/settings-reads-ssr.test.ts +232 -0
  389. package/frontend-vue/tests/side-panels.test.ts +8 -1
  390. package/frontend-vue/tests/sign-in-exit-history.test.ts +219 -0
  391. package/frontend-vue/tests/signup-invitation-link.test.ts +56 -8
  392. package/frontend-vue/tests/site-layout-categories.test.ts +42 -0
  393. package/frontend-vue/tests/site-layout-hydration.test.ts +3 -3
  394. package/frontend-vue/tests/site-layout-rendered.test.ts +103 -0
  395. package/frontend-vue/tests/site-layout-resync.test.ts +87 -0
  396. package/frontend-vue/tests/ssr-guard-preload.test.ts +188 -0
  397. package/frontend-vue/tests/table-cell-tones.test.ts +36 -1
  398. package/frontend-vue/tests/table-data-ssr.test.ts +195 -0
  399. package/frontend-vue/tests/table-list-data-requests.test.ts +154 -0
  400. package/frontend-vue/tests/table-retry-loading.test.ts +37 -0
  401. package/frontend-vue/tests/table-source-navigation.test.ts +0 -1
  402. package/frontend-vue/tests/table-source-period-scope.test.ts +0 -1
  403. package/frontend-vue/tests/table-view-views.test.ts +1 -1
  404. package/frontend-vue/tests/top-list-card-actions.test.ts +3 -0
  405. package/frontend-vue/tests/top-list-card-period-badge-ssr.test.ts +212 -0
  406. package/frontend-vue/tests/top-list-card-period-badge.test.ts +79 -0
  407. package/frontend-vue/tests/top-list-card-value-suffix.test.ts +3 -0
  408. package/frontend-vue/tests/trailing-deduper.test.ts +22 -0
  409. package/frontend-vue/tests/use-api-data-update.test.ts +119 -0
  410. package/frontend-vue/tests/use-api-data.test.ts +410 -0
  411. package/frontend-vue/tests/user-language.test.ts +8 -5
  412. package/frontend-vue/tests/widget-label-i18n.test.ts +4 -1
  413. package/frontend-vue/vitest.config.ts +6 -0
  414. package/package.json +2 -2
  415. package/skills/dms/SKILL.md +2 -1
  416. package/skills/dms-module/SKILL.md +1 -1
  417. package/skills/dms-pages/SKILL.md +4 -1
  418. package/frontend-vue/layers/dms-core/app/build/composables/page/usePageRecordLabel.ts +0 -9
  419. package/frontend-vue/layers/dms-layout/app/composables/page/usePrefetch.ts +0 -21
  420. package/frontend-vue/layers/dms-ui/app/build/composables/form/useControlError.ts +0 -10
  421. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useServerRenderedAsyncData.ts +0 -23
  422. package/frontend-vue/tests/control-error-path.test.ts +0 -11
  423. package/frontend-vue/tests/modules-listing-per-request.test.ts +0 -104
  424. package/frontend-vue/tests/page-layout-background-refresh.test.ts +0 -321
@@ -8,22 +8,39 @@ import {
8
8
  type PeriodPreset,
9
9
  type PeriodRange,
10
10
  } from "./types";
11
+ import { readerClock } from "./periodClock";
12
+
13
+ // Resolved in the reader's calendar (`readerClock`): the browser's zone, on
14
+ // the server as in the browser.
15
+ function atTime(
16
+ date: Date,
17
+ hours: number,
18
+ minutes: number,
19
+ seconds: number,
20
+ milliseconds: number,
21
+ ): Date {
22
+ const clock = readerClock();
23
+ return clock.instant({
24
+ ...clock.wall(date),
25
+ hours,
26
+ minutes,
27
+ seconds,
28
+ milliseconds,
29
+ });
30
+ }
11
31
 
12
32
  function startOfDay(date: Date): Date {
13
- const next = new Date(date);
14
- next.setHours(0, 0, 0, 0);
15
- return next;
33
+ return atTime(date, 0, 0, 0, 0);
16
34
  }
17
35
 
18
36
  function endOfDay(date: Date): Date {
19
- const next = new Date(date);
20
- next.setHours(
37
+ return atTime(
38
+ date,
21
39
  END_OF_DAY_HOURS,
22
40
  END_OF_DAY_MINUTES,
23
41
  END_OF_DAY_SECONDS,
24
42
  END_OF_DAY_MS,
25
43
  );
26
- return next;
27
44
  }
28
45
 
29
46
  function addDays(date: Date, amount: number): Date {
@@ -34,35 +51,47 @@ function addHours(date: Date, amount: number): Date {
34
51
  return new Date(date.getTime() + amount * MS_PER_HOUR);
35
52
  }
36
53
 
54
+ /** Midnight of a day given as year, month and day; they roll over as `Date`'s. */
55
+ function dayStart(year: number, month: number, day: number): Date {
56
+ return readerClock().instant({
57
+ year,
58
+ month,
59
+ day,
60
+ hours: 0,
61
+ minutes: 0,
62
+ seconds: 0,
63
+ milliseconds: 0,
64
+ });
65
+ }
66
+
37
67
  function startOfMonth(date: Date, monthOffset = 0): Date {
38
- return new Date(date.getFullYear(), date.getMonth() + monthOffset, 1);
68
+ const { year, month } = readerClock().wall(date);
69
+ return dayStart(year, month + monthOffset, 1);
39
70
  }
40
71
 
41
72
  function endOfMonth(date: Date, monthOffset = 0): Date {
42
- const next = new Date(
43
- date.getFullYear(),
44
- date.getMonth() + monthOffset + 1,
45
- 0,
46
- );
47
- return endOfDay(next);
73
+ const { year, month } = readerClock().wall(date);
74
+ return endOfDay(dayStart(year, month + monthOffset + 1, 0));
48
75
  }
49
76
 
50
77
  function startOfQuarter(date: Date, quarterOffset = 0): Date {
51
- const quarter = Math.floor(date.getMonth() / 3) + quarterOffset;
52
- return new Date(date.getFullYear(), quarter * 3, 1);
78
+ const { year, month } = readerClock().wall(date);
79
+ const quarter = Math.floor(month / 3) + quarterOffset;
80
+ return dayStart(year, quarter * 3, 1);
53
81
  }
54
82
 
55
83
  function endOfQuarter(date: Date, quarterOffset = 0): Date {
56
- const quarter = Math.floor(date.getMonth() / 3) + quarterOffset;
57
- return endOfDay(new Date(date.getFullYear(), quarter * 3 + 3, 0));
84
+ const { year, month } = readerClock().wall(date);
85
+ const quarter = Math.floor(month / 3) + quarterOffset;
86
+ return endOfDay(dayStart(year, quarter * 3 + 3, 0));
58
87
  }
59
88
 
60
89
  function startOfYear(date: Date, yearOffset = 0): Date {
61
- return new Date(date.getFullYear() + yearOffset, 0, 1);
90
+ return dayStart(readerClock().wall(date).year + yearOffset, 0, 1);
62
91
  }
63
92
 
64
93
  function endOfYear(date: Date, yearOffset = 0): Date {
65
- return endOfDay(new Date(date.getFullYear() + yearOffset, 11, 31));
94
+ return endOfDay(dayStart(readerClock().wall(date).year + yearOffset, 11, 31));
66
95
  }
67
96
 
68
97
  const PRESET_RANGE_RESOLVERS: Record<
@@ -1,8 +1,10 @@
1
1
  import {
2
2
  computed,
3
3
  getCurrentScope,
4
+ onMounted,
4
5
  onScopeDispose,
5
6
  ref,
7
+ shallowRef,
6
8
  watch,
7
9
  type ComputedRef,
8
10
  type Ref,
@@ -23,6 +25,7 @@ import {
23
25
  resolvePresetRange,
24
26
  } from "./presetResolvers";
25
27
  import { resolveComparisonRange } from "./compareResolvers";
28
+ import { periodRenderNow } from "./periodRenderNow";
26
29
 
27
30
  export interface UsePeriodOptions {
28
31
  defaultPreset?: PeriodPreset;
@@ -47,6 +50,11 @@ export interface UsePeriodReturn {
47
50
  presets: ComputedRef<PeriodPreset[]>;
48
51
  comparisons: ComputedRef<PeriodComparison[]>;
49
52
  state: ComputedRef<PeriodState>;
53
+ /**
54
+ * The instant `state` was resolved from: what a label relative to now
55
+ * reads, so it holds still with the period (see `periodRenderNow`).
56
+ */
57
+ resolvedAt: ComputedRef<Date>;
50
58
  setPreset: (preset: PeriodPreset) => void;
51
59
  setComparison: (comparison: PeriodComparison) => void;
52
60
  setCustomRange: (range: PeriodRange | null) => void;
@@ -135,8 +143,7 @@ interface PeriodRefs {
135
143
  customCompareRange: Ref<PeriodRange | null>;
136
144
  }
137
145
 
138
- function buildState(refs: PeriodRefs): PeriodState {
139
- const now = new Date();
146
+ function buildState(refs: PeriodRefs, now: Date): PeriodState {
140
147
  const fallback = refs.customRange.value ?? defaultFallbackRange(now);
141
148
  const range = resolvePresetRange(refs.preset.value, now, fallback);
142
149
  const compareRange = resolveComparisonRange(
@@ -176,6 +183,9 @@ function makeSetters(refs: PeriodRefs) {
176
183
  * Reactive period state machine behind `PeriodSelector` — presets,
177
184
  * comparisons and custom ranges resolved into a publishable `state`.
178
185
  *
186
+ * A server render and the browser hydrating it resolve from the same instant
187
+ * (`periodRenderNow`), so both render the same bounds.
188
+ *
179
189
  * Relative presets (`last-hour`, `last-24h`) re-resolve every minute, and
180
190
  * calendar presets (`today`, `last-7-days`, `this-month`...) at each local
181
191
  * midnight, so a page left open overnight moves on to the new day.
@@ -201,10 +211,28 @@ export function usePeriod(options: UsePeriodOptions = {}): UsePeriodReturn {
201
211
  () => options.comparisons ?? DEFAULT_COMPARISONS,
202
212
  );
203
213
  const presetClock = usePresetClock(refs.preset, options.holdClock);
204
- const state = computed<PeriodState>(() => {
214
+ // The server render's instant, until the clock ticks or the period changes
215
+ // once on screen: what the setup applies (the URL's period) the server
216
+ // applied as well.
217
+ const renderNow = shallowRef(periodRenderNow());
218
+ if (renderNow.value) {
219
+ onMounted(() => {
220
+ watch(
221
+ [presetClock, ...Object.values(refs)],
222
+ () => {
223
+ renderNow.value = null;
224
+ },
225
+ { flush: "sync" },
226
+ );
227
+ });
228
+ }
229
+ const resolution = computed(() => {
205
230
  void presetClock.value;
206
- return buildState(refs);
231
+ const now = renderNow.value ?? new Date();
232
+ return { now, state: buildState(refs, now) };
207
233
  });
234
+ const state = computed<PeriodState>(() => resolution.value.state);
235
+ const resolvedAt = computed<Date>(() => resolution.value.now);
208
236
  const setters = makeSetters(refs);
209
237
 
210
238
  return {
@@ -212,6 +240,7 @@ export function usePeriod(options: UsePeriodOptions = {}): UsePeriodReturn {
212
240
  presets,
213
241
  comparisons,
214
242
  state,
243
+ resolvedAt,
215
244
  ...setters,
216
245
  };
217
246
  }
@@ -1,4 +1,5 @@
1
- import { computed, ref, type ComputedRef } from "vue";
1
+ import { computed, markRaw, ref, type ComputedRef, type Ref } from "vue";
2
+ import { resolvesPeriodsAsBrowser } from "./periodTimeZone";
2
3
  import type { PeriodState } from "./types";
3
4
 
4
5
  interface PeriodScopeRegistration {
@@ -6,65 +7,117 @@ interface PeriodScopeRegistration {
6
7
  refCount: number;
7
8
  }
8
9
 
9
- const periodScopeRegistry = new Map<string, PeriodScopeRegistration>();
10
- const registryVersion = ref(0);
10
+ interface PeriodScopeRegistry {
11
+ scopes: Map<string, PeriodScopeRegistration>;
12
+ version: Ref<number>;
13
+ }
14
+
15
+ const PERIOD_SCOPES_STATE_KEY = "dms-period-scopes";
11
16
 
12
- function bumpRegistry(): void {
13
- registryVersion.value += 1;
17
+ function createRegistry(): PeriodScopeRegistry {
18
+ return markRaw({ scopes: new Map(), version: ref(0) });
14
19
  }
15
20
 
16
- const noopUnregister = (): void => undefined;
21
+ // A browser tab runs one application, whose scopes every caller shares; a
22
+ // server renders many requests at once, each with its own scopes.
23
+ const browserRegistry = createRegistry();
24
+
25
+ function usePeriodScopeRegistry(): PeriodScopeRegistry {
26
+ if (typeof window !== "undefined") return browserRegistry;
27
+ return useDmsState(PERIOD_SCOPES_STATE_KEY, createRegistry).value;
28
+ }
17
29
 
18
30
  export function registerPeriodScope(
19
31
  id: string,
20
32
  state: ComputedRef<PeriodState>,
21
33
  ): () => void {
22
- if (typeof window === "undefined") return noopUnregister;
23
- const existing = periodScopeRegistry.get(id);
34
+ const registry = usePeriodScopeRegistry();
35
+ const existing = registry.scopes.get(id);
24
36
  if (existing) {
25
37
  existing.state = state;
26
38
  existing.refCount += 1;
27
39
  } else {
28
- periodScopeRegistry.set(id, { state, refCount: 1 });
40
+ registry.scopes.set(id, { state, refCount: 1 });
29
41
  }
30
- bumpRegistry();
42
+ registry.version.value += 1;
31
43
  return () => {
32
- const entry = periodScopeRegistry.get(id);
44
+ const entry = registry.scopes.get(id);
33
45
  if (!entry) return;
34
46
  entry.refCount -= 1;
35
- if (entry.refCount <= 0) periodScopeRegistry.delete(id);
36
- bumpRegistry();
47
+ if (entry.refCount <= 0) registry.scopes.delete(id);
48
+ registry.version.value += 1;
37
49
  };
38
50
  }
39
51
 
40
52
  const warnedMissingScopes = new Set<string>();
41
53
  const MISSING_SCOPE_WARN_DELAY_MS = 250;
42
54
 
43
- function deferMissingScopeWarn(id: string): void {
55
+ function deferMissingScopeWarn(
56
+ registry: PeriodScopeRegistry,
57
+ id: string,
58
+ ): void {
44
59
  if (warnedMissingScopes.has(id)) return;
45
60
  warnedMissingScopes.add(id);
46
61
  if (typeof window === "undefined") return;
47
62
  setTimeout(() => {
48
- if (periodScopeRegistry.has(id)) return;
63
+ if (registry.scopes.has(id)) return;
49
64
  console.warn(
50
65
  `[dms] periodScope "${id}" is not registered. The consumer will run without period filtering. Add a PeriodSelector with id="${id}" to bind it.`,
51
66
  );
52
67
  }, MISSING_SCOPE_WARN_DELAY_MS);
53
68
  }
54
69
 
70
+ function readPeriodScope(
71
+ registry: PeriodScopeRegistry,
72
+ id: string,
73
+ ): PeriodState | null {
74
+ void registry.version.value;
75
+ const entry = registry.scopes.get(id);
76
+ if (!entry) {
77
+ deferMissingScopeWarn(registry, id);
78
+ return null;
79
+ }
80
+ return entry.state.value;
81
+ }
82
+
83
+ /**
84
+ * The state of a period scope, `null` while no selector registers it.
85
+ *
86
+ * A server render reads it once, as the caller sets up: a selector that sets
87
+ * up later (further down the page) leaves the caller to the browser, whose
88
+ * first render must match the HTML. It also answers `null` for a period the
89
+ * browser could resolve to other bounds: before the browser's cookie names
90
+ * its time zone. A relative preset resolves from the instant the server
91
+ * hands the browser (`periodRenderNow`), so both read the same bounds.
92
+ */
55
93
  export function usePeriodScope(
56
94
  id: string | undefined,
57
95
  ): ComputedRef<PeriodState | null> {
58
- return computed(() => {
59
- if (!id) return null;
60
- void registryVersion.value;
61
- const entry = periodScopeRegistry.get(id);
62
- if (!entry) {
63
- deferMissingScopeWarn(id);
64
- return null;
65
- }
66
- return entry.state.value;
67
- });
96
+ if (!id) return computed(() => null);
97
+ const registry = usePeriodScopeRegistry();
98
+ if (typeof window !== "undefined")
99
+ return computed(() => readPeriodScope(registry, id));
100
+ const state = readPeriodScope(registry, id);
101
+ const resolvesAsBrowser = state !== null && resolvesPeriodsAsBrowser();
102
+ return computed(() => (resolvesAsBrowser ? state : null));
103
+ }
104
+
105
+ /**
106
+ * The preset of a period scope, `null` while no selector registers it: what
107
+ * a card's period badge names. A server render reads it once, as the caller
108
+ * sets up, like `usePeriodScope`, but whatever bounds the browser would
109
+ * resolve the period to: a preset is the same name on both sides, so the
110
+ * server prints the badge the browser hydrates.
111
+ */
112
+ export function usePeriodScopePreset(
113
+ id: string | undefined,
114
+ ): ComputedRef<PeriodState["preset"] | null> {
115
+ if (!id) return computed(() => null);
116
+ const registry = usePeriodScopeRegistry();
117
+ if (typeof window !== "undefined")
118
+ return computed(() => readPeriodScope(registry, id)?.preset ?? null);
119
+ const preset = readPeriodScope(registry, id)?.preset ?? null;
120
+ return computed(() => preset);
68
121
  }
69
122
 
70
123
  /**
@@ -10,6 +10,8 @@ export interface PageRealtimeApi {
10
10
  subscribe(topic: string, handler: RealtimeTopicHandler): () => void;
11
11
  sessionId: Ref<string | undefined>;
12
12
  pageId: Ref<string | undefined>;
13
+ /** The page's realtime topics, once the session is subscribed to it. */
14
+ pageTopics: Ref<string[]>;
13
15
  }
14
16
 
15
17
  interface PageClaim {
@@ -70,6 +72,7 @@ export function usePageRealtime(
70
72
  subscribe: user.subscribe,
71
73
  sessionId: user.sessionId,
72
74
  pageId: resolvedPageId,
75
+ pageTopics: user.pageTopics,
73
76
  };
74
77
  }
75
78
 
@@ -51,12 +51,18 @@ export interface UserRealtimeApi {
51
51
  * announced in the greeting.
52
52
  */
53
53
  menuTopics: Ref<string[]>;
54
+ /**
55
+ * The realtime topics of the page this session is on, as its subscription
56
+ * answered them: an event on one means the page's data moved.
57
+ */
58
+ pageTopics: Ref<string[]>;
54
59
  }
55
60
 
56
61
  interface UserRealtimeState {
57
62
  sessionId: Ref<string | undefined>;
58
63
  pageId: Ref<string | undefined>;
59
64
  menuTopics: Ref<string[]>;
65
+ pageTopics: Ref<string[]>;
60
66
  appliedPageId: Ref<string | null>;
61
67
  handlers: Map<string, Set<RealtimeTopicHandler>>;
62
68
  snapshots: Map<string, RealtimeSnapshotEvent>;
@@ -230,22 +236,42 @@ const connectWithBackoff = (
230
236
  };
231
237
  };
232
238
 
239
+ interface PageSubscriptionAnswer {
240
+ topics?: unknown;
241
+ }
242
+
243
+ /** The topics a page subscription answered, none when it answered none. */
244
+ const readPageTopics = async (response: Response): Promise<string[]> => {
245
+ let answer: PageSubscriptionAnswer | null;
246
+ try {
247
+ answer = (await response.json()) as PageSubscriptionAnswer | null;
248
+ } catch {
249
+ return [];
250
+ }
251
+ const topics = answer?.topics;
252
+ return Array.isArray(topics)
253
+ ? topics.filter((topic): topic is string => typeof topic === "string")
254
+ : [];
255
+ };
256
+
257
+ /** Subscribes the session to `pageId`, or to no page; returns its topics. */
233
258
  const callPageEndpoint = async (
234
259
  state: UserRealtimeState,
235
260
  pageId: string | null,
236
261
  baseUrl: string,
237
- ): Promise<void> => {
262
+ ): Promise<string[]> => {
238
263
  const sessionId = state.sessionId.value;
239
- if (!sessionId) return;
264
+ if (!sessionId) return [];
240
265
  const headers: Record<string, string> = { [SESSION_HEADER]: sessionId };
241
266
  const url = pageId
242
267
  ? `${baseUrl}${REALTIME_SUBSCRIBE_PATH_PREFIX}${pageId}`
243
268
  : `${baseUrl}${REALTIME_UNSUBSCRIBE_PATH}`;
244
- await fetch(url, {
269
+ const response = await fetch(url, {
245
270
  method: "POST",
246
271
  headers,
247
272
  credentials: "include",
248
273
  });
274
+ return pageId && response.ok ? readPageTopics(response) : [];
249
275
  };
250
276
 
251
277
  const applyDesiredPage = async (
@@ -256,11 +282,12 @@ const applyDesiredPage = async (
256
282
  const sessionId = state.sessionId.value;
257
283
  if (state.appliedPageId.value === desired) return;
258
284
  if (!sessionId) return;
259
- await callPageEndpoint(state, desired, baseUrl);
285
+ const topics = await callPageEndpoint(state, desired, baseUrl);
260
286
  // A reconnect during the call replaced the session: the page is applied to
261
287
  // the dead one, and the new one still needs it.
262
288
  if (state.sessionId.value !== sessionId) return;
263
289
  state.appliedPageId.value = desired;
290
+ state.pageTopics.value = topics;
264
291
  };
265
292
 
266
293
  /**
@@ -288,6 +315,7 @@ const reconcilePage = (
288
315
  const resetSession = (state: UserRealtimeState): void => {
289
316
  state.sessionId.value = undefined;
290
317
  state.appliedPageId.value = null;
318
+ state.pageTopics.value = [];
291
319
  state.snapshots.clear();
292
320
  };
293
321
 
@@ -299,10 +327,12 @@ const buildApi = (): UserRealtimeApi => {
299
327
  const menuTopics = useDmsState<string[]>(MENU_TOPICS_STATE_KEY, () => []);
300
328
  const appliedPageId = ref<string | null>(null);
301
329
  const pageId = ref<string | undefined>(undefined);
330
+ const pageTopics = ref<string[]>([]);
302
331
  const state: UserRealtimeState = {
303
332
  sessionId,
304
333
  pageId,
305
334
  menuTopics,
335
+ pageTopics,
306
336
  appliedPageId,
307
337
  handlers: new Map(),
308
338
  snapshots: new Map(),
@@ -310,7 +340,7 @@ const buildApi = (): UserRealtimeApi => {
310
340
  };
311
341
 
312
342
  const config = useDmsRuntimeConfig();
313
- const { loggedIn } = useUserSession();
343
+ const { loggedIn } = useDmsUserSession();
314
344
  const { refreshSession, redirectToAuth } = useSessionRecovery();
315
345
  const baseUrl = config.public.dms.baseURL as string;
316
346
 
@@ -366,7 +396,7 @@ const buildApi = (): UserRealtimeApi => {
366
396
  await reconcilePage(state, baseUrl);
367
397
  };
368
398
 
369
- return { subscribe, setPage, sessionId, pageId, menuTopics };
399
+ return { subscribe, setPage, sessionId, pageId, menuTopics, pageTopics };
370
400
  };
371
401
 
372
402
  export function useUserRealtime(): UserRealtimeApi {
@@ -27,7 +27,7 @@ function localeTimeFormat(locale: string): TimeFormatPreference {
27
27
  * the values themselves, such as a calendar's `weekStartsOn`.
28
28
  */
29
29
  export function useUserRegionalPreferences() {
30
- const { user } = useUserSession<RegionalPreferenceFields>();
30
+ const { user } = useDmsUserSession<RegionalPreferenceFields>();
31
31
  const { locale } = useI18n();
32
32
 
33
33
  const preferences = computed<RegionalPreferences>(() =>
@@ -0,0 +1,26 @@
1
+ import { watch } from "vue";
2
+ import {
3
+ defineDmsPlugin,
4
+ setDmsAsyncDataScope,
5
+ useDmsUserSession,
6
+ } from "#dms/frontend-module";
7
+ import {
8
+ asyncDataScopeOf,
9
+ type ScopedSession,
10
+ } from "../build/utils/asyncDataScope";
11
+
12
+ /**
13
+ * Keeps every `useApiData` / `useDmsAsyncData` key in the scope of the
14
+ * session's account and workspace, on both sides before the page renders:
15
+ * the browser finds the data the server transferred under the same scope,
16
+ * and a switch of account or workspace drops the data of the previous one
17
+ * instead of showing it under the new session.
18
+ */
19
+ export default defineDmsPlugin(() => {
20
+ const { session } = useDmsUserSession<unknown, ScopedSession>();
21
+ const scope = () => asyncDataScopeOf(session.value);
22
+ setDmsAsyncDataScope(scope());
23
+ // One app per request on a server: its session never changes there.
24
+ if (typeof window === "undefined") return;
25
+ watch(scope, (next) => setDmsAsyncDataScope(next));
26
+ });
@@ -0,0 +1,22 @@
1
+ // Mirror of PAGE_ROUTE_PREFIX and pageRoutePath from
2
+ // @antelopejs/interface-dms/page.
3
+
4
+ /**
5
+ * Where the backend serves the routes a page controller declares, and its
6
+ * layout: under the DMS's own prefix, which the frontend server proxies. The
7
+ * page's own path is the page, never a backend route.
8
+ */
9
+ export const PAGE_ROUTE_PREFIX = "/dms/pages";
10
+
11
+ /**
12
+ * The backend path of a page's route, as the browser calls it:
13
+ * `pageRoutePath("/settings/user/security", "/sessions")` is
14
+ * `/dms/pages/settings/user/security/sessions`.
15
+ *
16
+ * @param pageSlug The page's path, such as `/settings/user/security`
17
+ * @param route The route below the page; none for the page's own route
18
+ */
19
+ export function pageRoutePath(pageSlug: string, route = ""): string {
20
+ const path = `${PAGE_ROUTE_PREFIX}/${pageSlug}/${route}`.replace(/\/+/g, "/");
21
+ return path.endsWith("/") ? path.slice(0, -1) : path;
22
+ }
@@ -37,6 +37,7 @@ export interface RegionalOptionsSettings {
37
37
  }
38
38
 
39
39
  type PreferencesSource = () => RegionalPreferences;
40
+ type ReaderTimeZoneSource = () => string | null | undefined;
40
41
 
41
42
  const WEEK_STARTS: readonly number[] = [0, 1, 6];
42
43
  const TIME_FORMATS: readonly string[] = ["h23", "h12"];
@@ -73,6 +74,7 @@ const NUMERIC_DATE: Intl.DateTimeFormatOptions = {
73
74
 
74
75
  const knownTimeZones = new Map<string, boolean>();
75
76
  let preferencesSource: PreferencesSource | undefined;
77
+ let readerTimeZoneSource: ReaderTimeZoneSource | undefined;
76
78
 
77
79
  /** Whether the runtime knows `timeZone` (an unknown one would throw in Intl). */
78
80
  export function isKnownTimeZone(timeZone: string): boolean {
@@ -125,6 +127,35 @@ export function setRegionalPreferencesSource(source: PreferencesSource): void {
125
127
  preferencesSource = source;
126
128
  }
127
129
 
130
+ /**
131
+ * Tells a server render where to read the zone of the browser it renders for
132
+ * (its `dms-time-zone` cookie). Called on every read, so it resolves the
133
+ * request being rendered rather than a value captured once.
134
+ */
135
+ export function setReaderTimeZoneSource(source: ReaderTimeZoneSource): void {
136
+ readerTimeZoneSource = source;
137
+ }
138
+
139
+ /**
140
+ * The time zone of the browser the page is for, when it is not the zone the
141
+ * code runs in: `undefined` in the browser, where it is; on a server, the zone
142
+ * the browser's cookie names, if this runtime knows it. Dates computed or
143
+ * written for the browser use it, so the server renders what the browser
144
+ * renders.
145
+ */
146
+ export function readerTimeZone(): string | undefined {
147
+ if (typeof window !== "undefined") return undefined;
148
+ let zone: string | null | undefined;
149
+ try {
150
+ zone = readerTimeZoneSource?.();
151
+ } catch {
152
+ return undefined;
153
+ }
154
+ return zone && zone !== browserTimeZone() && isKnownTimeZone(zone)
155
+ ? zone
156
+ : undefined;
157
+ }
158
+
128
159
  /** The current user's preferences; none outside an application. */
129
160
  export function currentRegionalPreferences(): RegionalPreferences {
130
161
  try {
@@ -195,8 +226,10 @@ export function withRegionalOptions(
195
226
  let result = preferences.dateFormat
196
227
  ? DATE_FORMAT_REWRITES[preferences.dateFormat](options)
197
228
  : { ...options };
198
- if (preferences.timeZone && !settings.keepLocalZone && !result.timeZone) {
199
- result = { ...result, timeZone: preferences.timeZone };
229
+ const timeZone =
230
+ (!settings.keepLocalZone && preferences.timeZone) || readerTimeZone();
231
+ if (timeZone && !result.timeZone) {
232
+ result = { ...result, timeZone };
200
233
  }
201
234
  const statesClock =
202
235
  result.hour12 !== undefined || result.hourCycle !== undefined;
@@ -238,7 +271,7 @@ export function regionalDayNumber(
238
271
  preferences: RegionalPreferences = currentRegionalPreferences(),
239
272
  ): number {
240
273
  const parts = new Intl.DateTimeFormat("en-US", {
241
- timeZone: preferences.timeZone,
274
+ timeZone: preferences.timeZone ?? readerTimeZone(),
242
275
  year: "numeric",
243
276
  month: "numeric",
244
277
  day: "numeric",
@@ -1,5 +1,11 @@
1
1
  export interface TrailingDeduper {
2
2
  run(task: () => Promise<void>): Promise<void>;
3
+ /**
4
+ * Joins the run in flight, or starts one: for a caller that needs a result
5
+ * rather than a fresh one, such as a first load, which a run started
6
+ * before it answers as well as its own would.
7
+ */
8
+ join(task: () => Promise<void>): Promise<void>;
3
9
  }
4
10
 
5
11
  /**
@@ -38,5 +44,8 @@ export function createTrailingDeduper(): TrailingDeduper {
38
44
  return trailing;
39
45
  };
40
46
 
41
- return { run };
47
+ const join = (task: () => Promise<void>): Promise<void> =>
48
+ inFlight ?? run(task);
49
+
50
+ return { run, join };
42
51
  }