@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
@@ -0,0 +1,137 @@
1
+ /**
2
+ * A server render resolves periods and writes dates in the time zone of the
3
+ * browser it renders for (its `dms-time-zone` cookie), whatever the server's
4
+ * own: the same bounds and the same text as the browser's, so hydration
5
+ * matches. Without the cookie, or in the server's own zone, nothing changes.
6
+ */
7
+ import { afterEach, describe, expect, it } from "vitest";
8
+ import {
9
+ browserTimeZone,
10
+ regionalDateTimeFormat,
11
+ regionalDayKey,
12
+ setReaderTimeZoneSource,
13
+ } from "../layers/dms-core/app/utils/regional";
14
+ import {
15
+ localClock,
16
+ zonedClock,
17
+ } from "../layers/dms-core/app/composables/period/periodClock";
18
+ import { resolvePresetRange } from "../layers/dms-core/app/composables/period/presetResolvers";
19
+ import { resolveComparisonRange } from "../layers/dms-core/app/composables/period/compareResolvers";
20
+ import {
21
+ formatPeriodDate,
22
+ parsePeriodDate,
23
+ } from "../layers/dms-ui/app/build/composables/period/periodQuery";
24
+ import { formatPeriodRange } from "../layers/dms-ui/app/build/composables/period/periodDisplay";
25
+
26
+ // A zone far from any CI's, and one with a daylight saving change.
27
+ const READER_ZONE =
28
+ browserTimeZone() === "Pacific/Kiritimati"
29
+ ? "Etc/GMT+12"
30
+ : "Pacific/Kiritimati";
31
+ const NEW_YORK = "America/New_York";
32
+ const NOW = new Date("2026-10-14T21:30:00.000Z");
33
+ const FALLBACK = { from: NOW, to: NOW };
34
+
35
+ function readFor(zone: string | null): void {
36
+ setReaderTimeZoneSource(() => zone);
37
+ }
38
+
39
+ function inZone(zone: string, date: Date) {
40
+ const { year, month, day, hours, minutes } = zonedClock(zone).wall(date);
41
+ return `${year}-${month + 1}-${day} ${hours}:${minutes}`;
42
+ }
43
+
44
+ afterEach(() => readFor(null));
45
+
46
+ describe("the clock a period is resolved in", () => {
47
+ it("reads and builds wall times in a zone, across a daylight saving change", () => {
48
+ const clock = zonedClock(NEW_YORK);
49
+ // 8 March 2026: New York moves from UTC-5 to UTC-4 at 2:00.
50
+ const midnight = clock.instant({
51
+ year: 2026,
52
+ month: 2,
53
+ day: 8,
54
+ hours: 0,
55
+ minutes: 0,
56
+ seconds: 0,
57
+ milliseconds: 0,
58
+ });
59
+ expect(midnight.toISOString()).toBe("2026-03-08T05:00:00.000Z");
60
+ const evening = clock.instant({ ...clock.wall(midnight), hours: 23 });
61
+ expect(evening.toISOString()).toBe("2026-03-09T03:00:00.000Z");
62
+ // Day 0 rolls back to the last day of the month before, as Date's does.
63
+ const rolled = clock.wall(
64
+ clock.instant({ ...clock.wall(midnight), month: 3, day: 0 }),
65
+ );
66
+ expect([rolled.month, rolled.day]).toEqual([2, 31]);
67
+ });
68
+
69
+ it("matches Date's own calendar in the zone the code runs in", () => {
70
+ const local = zonedClock(browserTimeZone());
71
+ expect(local.wall(NOW)).toEqual(localClock.wall(NOW));
72
+ const wall = { ...localClock.wall(NOW), day: 40, hours: 0 };
73
+ expect(local.instant(wall).getTime()).toBe(
74
+ localClock.instant(wall).getTime(),
75
+ );
76
+ });
77
+ });
78
+
79
+ describe("a server render for a browser in another zone", () => {
80
+ it("resolves presets and comparisons on the browser's calendar days", () => {
81
+ readFor(READER_ZONE);
82
+ const range = resolvePresetRange("this-month", NOW, FALLBACK)!;
83
+ const reader = zonedClock(READER_ZONE);
84
+ expect(inZone(READER_ZONE, range.from)).toMatch(/-1 0:0$/);
85
+ expect(reader.wall(range.to).hours).toBe(23);
86
+ expect(reader.wall(range.to).month).toBe(reader.wall(range.from).month);
87
+ const previous = resolveComparisonRange("previous-year", range, null)!;
88
+ expect(reader.wall(previous.from).year).toBe(
89
+ reader.wall(range.from).year - 1,
90
+ );
91
+ expect(inZone(READER_ZONE, previous.from)).toMatch(/-1 0:0$/);
92
+ });
93
+
94
+ it("reads and writes the URL's days as the browser's", () => {
95
+ readFor(READER_ZONE);
96
+ const from = parsePeriodDate("2026-10-01", "start")!;
97
+ expect(inZone(READER_ZONE, from)).toBe("2026-10-1 0:0");
98
+ expect(formatPeriodDate(from, "start")).toBe("2026-10-01");
99
+ expect(parsePeriodDate("2026-02-30", "start")).toBeNull();
100
+ });
101
+
102
+ it("writes the period's label and other dates in the browser's zone", () => {
103
+ readFor(READER_ZONE);
104
+ const range = resolvePresetRange("this-month", NOW, FALLBACK)!;
105
+ const expected = new Intl.DateTimeFormat("en", {
106
+ month: "short",
107
+ day: "numeric",
108
+ timeZone: READER_ZONE,
109
+ }).format(range.from);
110
+ expect(formatPeriodRange(range, "en")).toContain(expected);
111
+ expect(
112
+ regionalDateTimeFormat("en", { dateStyle: "medium" }).format(NOW),
113
+ ).toBe(
114
+ new Intl.DateTimeFormat("en", {
115
+ dateStyle: "medium",
116
+ timeZone: READER_ZONE,
117
+ }).format(NOW),
118
+ );
119
+ });
120
+
121
+ it("files dates under the browser's calendar days", () => {
122
+ readFor(READER_ZONE);
123
+ const { year, month, day } = zonedClock(READER_ZONE).wall(NOW);
124
+ const pad = (value: number) => String(value).padStart(2, "0");
125
+ expect(regionalDayKey(NOW)).toBe(`${year}-${pad(month + 1)}-${pad(day)}`);
126
+ });
127
+
128
+ it("changes nothing without the browser's zone, or in the server's own", () => {
129
+ for (const zone of [null, browserTimeZone()]) {
130
+ readFor(zone);
131
+ const range = resolvePresetRange("this-month", NOW, FALLBACK)!;
132
+ expect(range.from).toEqual(
133
+ new Date(NOW.getFullYear(), NOW.getMonth(), 1),
134
+ );
135
+ }
136
+ });
137
+ });
@@ -19,6 +19,7 @@ import {
19
19
  } from "vue";
20
20
  import { renderToString } from "vue/server-renderer";
21
21
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
22
+ import { useRealDataEngine } from "./fixtures/dataEngine";
22
23
  import type { PeriodState } from "../layers/dms-core/app/composables/period/types";
23
24
 
24
25
  const requests: string[] = [];
@@ -143,7 +144,13 @@ async function ssrThenHydrate(
143
144
  const container = document.createElement("div");
144
145
  container.innerHTML = html;
145
146
  document.body.appendChild(container);
147
+ // The browser starts from what the page carried, as it does after a server
148
+ // render: its first mount takes the data over instead of reading it again.
149
+ const payload = structuredClone(engine.serializeDmsPayload(renderRuntime));
150
+ const browser = engine.createDmsFrontendRuntime(undefined, {});
151
+ engine.restoreDmsPayload(browser, payload);
146
152
  const app = withComponents(createSSRApp(root as never), components);
153
+ engine.provideDmsFrontendRuntime(app, browser);
147
154
  app.mount(container);
148
155
  mounted.push(app);
149
156
  const deadline = Date.now() + SETTLE_MS;
@@ -163,6 +170,13 @@ function withComponents<
163
170
  return app;
164
171
  }
165
172
 
173
+ const engine = await import("#dms/frontend-module");
174
+ let renderRuntime: ReturnType<typeof useRealDataEngine>;
175
+
176
+ beforeEach(() => {
177
+ renderRuntime = useRealDataEngine();
178
+ });
179
+
166
180
  beforeEach(() => {
167
181
  requests.length = 0;
168
182
  });
@@ -238,6 +252,8 @@ describe("period-scoped cards on first paint", () => {
238
252
  }
239
253
  });
240
254
 
255
+ // Its period unknown to the server (no selector registered it before the
256
+ // card set up), a card renders its placeholder, never an empty value.
241
257
  it("shows a loading placeholder server-side rather than an empty value", async () => {
242
258
  const app = createSSRApp({
243
259
  render: () =>
@@ -246,7 +262,7 @@ describe("period-scoped cards on first paint", () => {
246
262
  componentId: "card",
247
263
  pageId: "page",
248
264
  fetchUrl: "/api/metrics/kpi/revenue",
249
- periodScope: SCOPE,
265
+ periodScope: "unregistered-scope",
250
266
  }),
251
267
  });
252
268
  withComponents(app, {
@@ -0,0 +1,195 @@
1
+ /**
2
+ * A server render of a dashboard, through the runtime's real async data: a
3
+ * card bound to a period scope renders its data when the server resolves the
4
+ * period as the browser will (in the browser's time zone, read in its cookie;
5
+ * a selector set up before the card), and its placeholder
6
+ * otherwise, requesting nothing. Each request has its scopes.
7
+ */
8
+ import { createSSRApp, defineComponent, h, ref, type Component } from "vue";
9
+ import { renderToString } from "vue/server-renderer";
10
+ import { beforeEach, describe, expect, it, vi } from "vitest";
11
+ import type { PeriodPreset } from "../layers/dms-core/app/composables/period/types";
12
+ import {
13
+ browserTimeZone,
14
+ setReaderTimeZoneSource,
15
+ } from "../layers/dms-core/app/utils/regional";
16
+
17
+ const engine = await import("#dms/frontend-module");
18
+ vi.stubGlobal("useDmsAsyncData", engine.useDmsAsyncData);
19
+ vi.stubGlobal("useDmsFetch", engine.useDmsFetch);
20
+ vi.stubGlobal("useDmsState", engine.useDmsState);
21
+ vi.stubGlobal("useDmsCookie", engine.useDmsCookie);
22
+
23
+ const requests: string[] = [];
24
+ vi.stubGlobal("useAuthFetch", () => ({
25
+ $authFetch: async (url: string) => {
26
+ requests.push(url);
27
+ await new Promise((done) => setTimeout(done, 5));
28
+ return {
29
+ value: new URL(url, "http://dms.local").searchParams.get("preset"),
30
+ };
31
+ },
32
+ }));
33
+
34
+ const { useApiData } = await import(
35
+ "../layers/dms-core/app/composables/auth/useApiData"
36
+ );
37
+ vi.stubGlobal("useApiData", useApiData);
38
+ const { usePeriod } = await import(
39
+ "../layers/dms-core/app/composables/period/usePeriod"
40
+ );
41
+ const { registerPeriodScope } = await import(
42
+ "../layers/dms-core/app/composables/period/usePeriodScope"
43
+ );
44
+ const { TIME_ZONE_COOKIE, useTimeZoneCookie } = await import(
45
+ "../layers/dms-core/app/composables/period/periodTimeZone"
46
+ );
47
+ const { zonedClock } = await import(
48
+ "../layers/dms-core/app/composables/period/periodClock"
49
+ );
50
+ // What the regional-preferences plugin sets up.
51
+ setReaderTimeZoneSource(() => useTimeZoneCookie().value);
52
+ const { useChartFetch } = await import(
53
+ "../layers/dms-ui/app/composables/chart/useChartFetch"
54
+ );
55
+
56
+ const SCOPE = "dashboard";
57
+
58
+ function selector(preset: PeriodPreset): Component {
59
+ return defineComponent({
60
+ setup() {
61
+ const period = usePeriod({ defaultPreset: preset });
62
+ registerPeriodScope(SCOPE, period.state);
63
+ return () => h("i");
64
+ },
65
+ });
66
+ }
67
+
68
+ const Card = defineComponent({
69
+ setup() {
70
+ const { data, isLoading } = useChartFetch<{ value: string }>({
71
+ fetchUrl: "/api/kpi",
72
+ periodScope: SCOPE,
73
+ });
74
+ return () =>
75
+ h("p", isLoading.value ? "SKELETON" : `value:${data.value?.value}`);
76
+ },
77
+ });
78
+
79
+ function zoneCookie(zone: string): string {
80
+ return `${TIME_ZONE_COOKIE}=${encodeURIComponent(JSON.stringify(zone))}`;
81
+ }
82
+
83
+ async function serverRender(children: Component[], cookies?: string) {
84
+ const runtime = engine.createDmsFrontendRuntime(undefined, {}, true, cookies);
85
+ const app = createSSRApp({
86
+ render: () =>
87
+ h(
88
+ "main",
89
+ children.map((c) => h(c)),
90
+ ),
91
+ });
92
+ engine.provideDmsFrontendRuntime(app, runtime);
93
+ engine.setDmsServerRuntimeResolver(() => runtime);
94
+ const html = await renderToString(app);
95
+ return { html, payload: engine.serializeDmsPayload(runtime) };
96
+ }
97
+
98
+ beforeEach(() => {
99
+ requests.length = 0;
100
+ });
101
+
102
+ describe("a period-scoped card in a server render", () => {
103
+ it("renders its data with the period the browser resolves, and hands it over", async () => {
104
+ const { html, payload } = await serverRender(
105
+ [selector("this-month"), Card],
106
+ zoneCookie(browserTimeZone()),
107
+ );
108
+
109
+ expect(html).toContain("value:this-month");
110
+ expect(requests).toHaveLength(1);
111
+ expect(requests[0]).toMatch(
112
+ /^\/api\/kpi\?from=.*&preset=this-month&comparison=none$/,
113
+ );
114
+ expect(Object.values(payload.data)).toEqual([{ value: "this-month" }]);
115
+ });
116
+
117
+ it("leaves it to the browser without the browser's time zone", async () => {
118
+ const { html } = await serverRender([selector("this-month"), Card]);
119
+
120
+ expect(html).toContain("SKELETON");
121
+ expect(requests).toEqual([]);
122
+ });
123
+
124
+ it("resolves the period in the browser's zone when it is not the server's", async () => {
125
+ const elsewhere =
126
+ browserTimeZone() === "Pacific/Kiritimati"
127
+ ? "Etc/GMT+12"
128
+ : "Pacific/Kiritimati";
129
+ const { html } = await serverRender(
130
+ [selector("this-month"), Card],
131
+ zoneCookie(elsewhere),
132
+ );
133
+
134
+ expect(html).toContain("value:this-month");
135
+ const query = new URL(requests[0]!, "http://dms.local").searchParams;
136
+ const from = zonedClock(elsewhere).wall(new Date(query.get("from")!));
137
+ const to = zonedClock(elsewhere).wall(new Date(query.get("to")!));
138
+ expect([from.day, from.hours, from.minutes]).toEqual([1, 0, 0]);
139
+ expect([to.hours, to.minutes, to.milliseconds]).toEqual([23, 59, 999]);
140
+ expect(to.month).toBe(from.month);
141
+ });
142
+
143
+ it("renders a relative preset too, from the instant it hands the browser", async () => {
144
+ const { html, payload } = await serverRender(
145
+ [selector("last-24h"), Card],
146
+ zoneCookie(browserTimeZone()),
147
+ );
148
+
149
+ expect(html).toContain("value:last-24h");
150
+ expect(requests).toHaveLength(1);
151
+ const query = new URL(requests[0]!, "http://dms.local").searchParams;
152
+ expect(payload.state["dms-period-render-now"]).toBe(
153
+ new Date(query.get("to")!).getTime(),
154
+ );
155
+ });
156
+
157
+ it("leaves it to the browser when its selector sets up after it", async () => {
158
+ const { html } = await serverRender(
159
+ [Card, selector("this-month")],
160
+ zoneCookie(browserTimeZone()),
161
+ );
162
+
163
+ expect(html).toContain("SKELETON");
164
+ expect(requests).toEqual([]);
165
+ });
166
+
167
+ it("keeps the scopes of concurrent requests apart", async () => {
168
+ const cookie = zoneCookie(browserTimeZone());
169
+ const [month, year] = await Promise.all([
170
+ serverRender([selector("this-month"), Card], cookie),
171
+ serverRender([selector("last-year"), Card], cookie),
172
+ ]);
173
+
174
+ expect(month.html).toContain("value:this-month");
175
+ expect(year.html).toContain("value:last-year");
176
+ });
177
+ });
178
+
179
+ describe("a card without a period in a server render", () => {
180
+ it("renders its data", async () => {
181
+ const Plain = defineComponent({
182
+ setup() {
183
+ const { data } = useChartFetch<{ value: string | null }>({
184
+ fetchUrl: "/api/plain",
185
+ watchSource: () => ref(1).value,
186
+ });
187
+ return () => h("p", `plain:${data.value ? "loaded" : "none"}`);
188
+ },
189
+ });
190
+ const { html } = await serverRender([Plain]);
191
+
192
+ expect(html).toContain("plain:loaded");
193
+ expect(requests).toEqual(["/api/plain"]);
194
+ });
195
+ });
@@ -198,8 +198,6 @@ describe("NavigationMenu in a role preview", () => {
198
198
  if (!state.has(key)) state.set(key, ref(initial()));
199
199
  return state.get(key);
200
200
  });
201
- vi.stubGlobal("usePrefetch", () => ({ prefetchPageLayout: vi.fn() }));
202
- vi.stubGlobal("stripQueryAndHash", (path: string) => path);
203
201
  vi.stubGlobal("MENU_STATUS_TEXT_CLASSES", MENU_STATUS_TEXT_CLASSES);
204
202
  host = document.createElement("div");
205
203
  document.body.append(host);
@@ -0,0 +1,194 @@
1
+ // @vitest-environment jsdom
2
+ /**
3
+ * The permission set the `auth` middleware preloads while a server renders a
4
+ * page reaches the browser with the page, through the runtime's real state
5
+ * transfer: the browser's run of the middleware finds it and asks nothing.
6
+ * A caller that only needs a set joins a load in flight.
7
+ */
8
+ import {
9
+ createApp,
10
+ createSSRApp,
11
+ defineComponent,
12
+ h,
13
+ nextTick,
14
+ type App,
15
+ } from "vue";
16
+ import { renderToString } from "vue/server-renderer";
17
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
18
+ import type { DmsFrontendRuntime } from "#dms/frontend-module";
19
+
20
+ const engine = await import("#dms/frontend-module");
21
+ const { createTrailingDeduper } = await import(
22
+ "../layers/dms-core/app/utils/trailingDeduper"
23
+ );
24
+ vi.stubGlobal("useDmsState", engine.useDmsState);
25
+ vi.stubGlobal("useDmsApp", engine.useDmsApp);
26
+ vi.stubGlobal("defineDmsMiddleware", engine.defineDmsMiddleware);
27
+ vi.stubGlobal("useDmsError", engine.useDmsError);
28
+ vi.stubGlobal("abortDmsNavigation", engine.abortDmsNavigation);
29
+ vi.stubGlobal("navigateDms", engine.navigateDms);
30
+ vi.stubGlobal("createTrailingDeduper", createTrailingDeduper);
31
+ vi.stubGlobal("useDmsRuntimeConfig", () => ({ public: { dms: {} } }));
32
+ vi.stubGlobal("useSiteLayout", () => ({
33
+ siteLayout: { value: {} },
34
+ findMatchingRoute: () => ({ metadata: {} }),
35
+ }));
36
+ vi.stubGlobal("useDmsUserSession", () => ({
37
+ loggedIn: { value: true },
38
+ user: { value: { isValidated: true } },
39
+ }));
40
+
41
+ const { useTrailingDeduper } = await import(
42
+ "../layers/dms-core/app/composables/useTrailingDeduper"
43
+ );
44
+ vi.stubGlobal("useTrailingDeduper", useTrailingDeduper);
45
+ const { usePermissions } = await import(
46
+ "../layers/dms-core/app/composables/auth/usePermissions"
47
+ );
48
+ vi.stubGlobal("usePermissions", usePermissions);
49
+ const { default: authMiddleware } = await import(
50
+ "../layers/dms-auth/app/middleware/auth"
51
+ );
52
+
53
+ const PERMISSION = "orders.read";
54
+
55
+ interface PendingRequest {
56
+ resolve: (ids: string[]) => void;
57
+ }
58
+
59
+ const requests: PendingRequest[] = [];
60
+ // Answers each request once the test releases it, in order.
61
+ const $authFetch = vi.fn(
62
+ () =>
63
+ new Promise<string[]>((resolve) => {
64
+ requests.push({ resolve });
65
+ }),
66
+ );
67
+ vi.stubGlobal("useAuthFetch", () => ({ $authFetch }));
68
+
69
+ const mounted: App[] = [];
70
+
71
+ const settle = async () => {
72
+ await nextTick();
73
+ await new Promise((resolve) => setTimeout(resolve, 0));
74
+ };
75
+
76
+ async function answerRequests(): Promise<void> {
77
+ for (let round = 0; round < 3; round++) {
78
+ await settle();
79
+ for (const request of requests.splice(0)) request.resolve([PERMISSION]);
80
+ }
81
+ }
82
+
83
+ const Page = defineComponent({
84
+ setup() {
85
+ defineDmsPageMeta({ auth: true });
86
+ const { hasPermission } = usePermissions();
87
+ return () => h("p", hasPermission(PERMISSION) ? "can read" : "cannot read");
88
+ },
89
+ });
90
+ const { defineDmsPageMeta } = engine;
91
+
92
+ /** A runtime whose app carries the DMS app context, as the real app's does. */
93
+ function attach(app: App, runtime: DmsFrontendRuntime): void {
94
+ const context = {} as DmsFrontendRuntime["appContext"];
95
+ app.config.globalProperties.$dms = context;
96
+ runtime.appContext = context;
97
+ runtime.route.matched = [{}];
98
+ engine.provideDmsFrontendRuntime(app, runtime);
99
+ }
100
+
101
+ beforeEach(async () => {
102
+ requests.length = 0;
103
+ $authFetch.mockClear();
104
+ await engine.setupFrontendModules([
105
+ {
106
+ options: { public: {} },
107
+ module: {
108
+ setup(sdk) {
109
+ sdk.registerMiddleware("auth", authMiddleware);
110
+ },
111
+ },
112
+ },
113
+ ]);
114
+ });
115
+
116
+ afterEach(() => {
117
+ mounted.splice(0).forEach((app) => app.unmount());
118
+ });
119
+
120
+ describe("permissions handed to the browser", () => {
121
+ it("renders the page with the set the server loaded, and the browser asks nothing", async () => {
122
+ const server = engine.createDmsFrontendRuntime(undefined, {}, true);
123
+ const serverApp = createSSRApp(Page);
124
+ attach(serverApp, server);
125
+ const rendering = renderToString(serverApp);
126
+ await answerRequests();
127
+ expect(await rendering).toContain("can read");
128
+ expect($authFetch).toHaveBeenCalledTimes(1);
129
+
130
+ const payload = structuredClone(engine.serializeDmsPayload(server));
131
+ expect(payload.state["dms-user-permissions"]).toEqual(
132
+ new Set([PERMISSION]),
133
+ );
134
+
135
+ $authFetch.mockClear();
136
+ const browser = engine.createDmsFrontendRuntime(undefined, {});
137
+ engine.restoreDmsPayload(browser, payload);
138
+ const browserApp = createApp(Page);
139
+ attach(browserApp, browser);
140
+ const host = document.createElement("div");
141
+ browserApp.mount(host);
142
+ mounted.push(browserApp);
143
+ await answerRequests();
144
+
145
+ expect($authFetch).not.toHaveBeenCalled();
146
+ expect(host.textContent).toBe("can read");
147
+ });
148
+ });
149
+
150
+ describe("loading the permission set", () => {
151
+ function browserPermissions() {
152
+ const runtime = engine.createDmsFrontendRuntime();
153
+ const app = createApp({ render: () => null });
154
+ attach(app, runtime);
155
+ return app.runWithContext(() => usePermissions());
156
+ }
157
+
158
+ it("joins a first load in flight instead of loading twice", async () => {
159
+ const permissions = browserPermissions();
160
+ const loads = Promise.all([
161
+ permissions.loadPermissions(),
162
+ permissions.loadPermissions(),
163
+ ]);
164
+ await answerRequests();
165
+ await loads;
166
+
167
+ expect($authFetch).toHaveBeenCalledTimes(1);
168
+ expect(permissions.hasPermission(PERMISSION)).toBe(true);
169
+ });
170
+
171
+ it("still gives a refresh asked during a load its own pass", async () => {
172
+ const permissions = browserPermissions();
173
+ const runs = Promise.all([
174
+ permissions.loadPermissions(),
175
+ permissions.fetchPermissions(),
176
+ ]);
177
+ await answerRequests();
178
+ await runs;
179
+
180
+ expect($authFetch).toHaveBeenCalledTimes(2);
181
+ });
182
+
183
+ it("loads again when the load it joined was cleared meanwhile", async () => {
184
+ const permissions = browserPermissions();
185
+ const first = permissions.loadPermissions();
186
+ permissions.clear();
187
+ const second = permissions.loadPermissions();
188
+ await answerRequests();
189
+ await Promise.all([first, second]);
190
+
191
+ expect($authFetch).toHaveBeenCalledTimes(2);
192
+ expect(permissions.isLoaded.value).toBe(true);
193
+ });
194
+ });
@@ -9,7 +9,7 @@ const navigateTo = vi.fn();
9
9
 
10
10
  beforeEach(() => {
11
11
  vi.stubGlobal("useDmsApp", () => ({ $i18n: { setLocale } }));
12
- vi.stubGlobal("useUserSession", () => ({
12
+ vi.stubGlobal("useDmsUserSession", () => ({
13
13
  fetch: refreshUser,
14
14
  user: { value: { language: "fr" } },
15
15
  }));
@@ -37,7 +37,7 @@ describe("post-login redirect", () => {
37
37
  await usePostLoginRedirect(resolveDestination);
38
38
 
39
39
  expect(resolveDestination).toHaveBeenCalledOnce();
40
- expect(navigateTo).toHaveBeenCalledWith("/dashboard");
40
+ expect(navigateTo).toHaveBeenCalledWith("/dashboard", {});
41
41
  expect(refreshUser.mock.invocationCallOrder[0]).toBeLessThan(
42
42
  refreshLayout.mock.invocationCallOrder[0]!,
43
43
  );
@@ -45,12 +45,17 @@ describe("post-login redirect", () => {
45
45
 
46
46
  it("preserves explicit login destinations", async () => {
47
47
  await usePostLoginRedirect("/reports");
48
- expect(navigateTo).toHaveBeenCalledWith("/reports");
48
+ expect(navigateTo).toHaveBeenCalledWith("/reports", {});
49
+ });
50
+
51
+ it("opens the destination in place of the current page when asked", async () => {
52
+ await usePostLoginRedirect("/reports", { replace: true });
53
+ expect(navigateTo).toHaveBeenCalledWith("/reports", { replace: true });
49
54
  });
50
55
 
51
56
  it("falls back to the homepage when no page is accessible", async () => {
52
57
  await usePostLoginRedirect(() => undefined);
53
- expect(navigateTo).toHaveBeenCalledWith("/");
58
+ expect(navigateTo).toHaveBeenCalledWith("/", {});
54
59
  });
55
60
 
56
61
  it("does not navigate when session refresh fails", async () => {
@@ -61,7 +66,7 @@ describe("post-login redirect", () => {
61
66
  });
62
67
 
63
68
  it("rejects a session refresh that resolves without an authenticated user", async () => {
64
- vi.stubGlobal("useUserSession", () => ({
69
+ vi.stubGlobal("useDmsUserSession", () => ({
65
70
  fetch: refreshUser,
66
71
  user: { value: null },
67
72
  }));