@antelopejs/dms 0.8.2 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (354) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/dist/implementations/dms/frontend-backend-proxies.d.ts +3 -0
  3. package/dist/implementations/dms/frontend-backend-proxies.js +39 -0
  4. package/dist/implementations/dms/frontend-backend-proxies.js.map +1 -0
  5. package/dist/implementations/dms/frontend-bootstrap.d.ts +1 -0
  6. package/dist/implementations/dms/frontend-bootstrap.js +13 -0
  7. package/dist/implementations/dms/frontend-bootstrap.js.map +1 -1
  8. package/dist/implementations/dms/frontend-modules.d.ts +4 -2
  9. package/dist/implementations/dms/frontend-modules.js +8 -7
  10. package/dist/implementations/dms/frontend-modules.js.map +1 -1
  11. package/dist/implementations/dms/manifest.d.ts +6 -0
  12. package/dist/implementations/dms/manifest.js +13 -0
  13. package/dist/implementations/dms/manifest.js.map +1 -1
  14. package/dist/implementations/dms/menu-categories.d.ts +16 -0
  15. package/dist/implementations/dms/menu-categories.js +30 -0
  16. package/dist/implementations/dms/menu-categories.js.map +1 -0
  17. package/dist/implementations/dms/nav-badges.d.ts +0 -8
  18. package/dist/implementations/dms/nav-badges.js +0 -16
  19. package/dist/implementations/dms/nav-badges.js.map +1 -1
  20. package/dist/implementations/dms/page.d.ts +11 -5
  21. package/dist/implementations/dms/page.js +56 -41
  22. package/dist/implementations/dms/page.js.map +1 -1
  23. package/dist/implementations/dms-html-render/index.js +5 -2
  24. package/dist/implementations/dms-html-render/index.js.map +1 -1
  25. package/dist/implementations/dms-notifications/index.d.ts +4 -3
  26. package/dist/implementations/dms-notifications/index.js +14 -6
  27. package/dist/implementations/dms-notifications/index.js.map +1 -1
  28. package/dist/pages/settings/settings-index.js +3 -2
  29. package/dist/pages/settings/settings-index.js.map +1 -1
  30. package/dist/pages/settings/users/invites.js +2 -2
  31. package/dist/pages/settings/users/invites.js.map +1 -1
  32. package/dist/pages/settings/users/member-change-notifications.js +17 -6
  33. package/dist/pages/settings/users/member-change-notifications.js.map +1 -1
  34. package/dist/pages/settings/users/member-invite-form.d.ts +3 -2
  35. package/dist/pages/settings/users/member-invite-form.js +7 -4
  36. package/dist/pages/settings/users/member-invite-form.js.map +1 -1
  37. package/dist/pages/settings/users/member-roles-field.js +2 -1
  38. package/dist/pages/settings/users/member-roles-field.js.map +1 -1
  39. package/dist/pages/settings/users/members.js +4 -1
  40. package/dist/pages/settings/users/members.js.map +1 -1
  41. package/dist/pages/settings/users/notifications.d.ts +6 -0
  42. package/dist/pages/settings/users/notifications.js +23 -15
  43. package/dist/pages/settings/users/notifications.js.map +1 -1
  44. package/dist/pages/settings/users/profile.js +1 -1
  45. package/dist/pages/settings/users/profile.js.map +1 -1
  46. package/dist/pages/settings/users/region-form.d.ts +1 -1
  47. package/dist/pages/settings/users/region-form.js +2 -1
  48. package/dist/pages/settings/users/region-form.js.map +1 -1
  49. package/dist/pages/settings/users/role-editor-store.d.ts +3 -1
  50. package/dist/pages/settings/users/role-editor-store.js +17 -4
  51. package/dist/pages/settings/users/role-editor-store.js.map +1 -1
  52. package/dist/pages/settings/users/roles.js +1 -0
  53. package/dist/pages/settings/users/roles.js.map +1 -1
  54. package/dist/realtime/registry.d.ts +1 -0
  55. package/dist/realtime/registry.js +5 -0
  56. package/dist/realtime/registry.js.map +1 -1
  57. package/dist/routes/realtime.d.ts +5 -1
  58. package/dist/routes/realtime.js +16 -9
  59. package/dist/routes/realtime.js.map +1 -1
  60. package/dist/test/integration/account-deletion.test.js +2 -2
  61. package/dist/test/integration/account-deletion.test.js.map +1 -1
  62. package/dist/test/integration/auth/auth-key-rotation.test.js +2 -1
  63. package/dist/test/integration/auth/auth-key-rotation.test.js.map +1 -1
  64. package/dist/test/integration/auth/current-password.test.js +2 -1
  65. package/dist/test/integration/auth/current-password.test.js.map +1 -1
  66. package/dist/test/integration/auth/session-revocation.test.js +2 -1
  67. package/dist/test/integration/auth/session-revocation.test.js.map +1 -1
  68. package/dist/test/integration/auth/sign-in-country.test.js +2 -1
  69. package/dist/test/integration/auth/sign-in-country.test.js.map +1 -1
  70. package/dist/test/integration/auth/two-factor-codes.test.js +2 -1
  71. package/dist/test/integration/auth/two-factor-codes.test.js.map +1 -1
  72. package/dist/test/integration/auth/two-factor-password.test.js +3 -2
  73. package/dist/test/integration/auth/two-factor-password.test.js.map +1 -1
  74. package/dist/test/integration/email-change.test.js +2 -1
  75. package/dist/test/integration/email-change.test.js.map +1 -1
  76. package/dist/test/integration/files/component-files.test.js +2 -2
  77. package/dist/test/integration/files/component-files.test.js.map +1 -1
  78. package/dist/test/integration/member-settings-access.test.js +8 -7
  79. package/dist/test/integration/member-settings-access.test.js.map +1 -1
  80. package/dist/test/integration/menu-access-events.test.d.ts +1 -0
  81. package/dist/test/integration/menu-access-events.test.js +148 -0
  82. package/dist/test/integration/menu-access-events.test.js.map +1 -0
  83. package/dist/test/integration/notification-inbox.test.js +4 -1
  84. package/dist/test/integration/notification-inbox.test.js.map +1 -1
  85. package/dist/test/integration/notification-locked-subjects.test.js +3 -2
  86. package/dist/test/integration/notification-locked-subjects.test.js.map +1 -1
  87. package/dist/test/integration/notification-unread-badge.test.js +51 -5
  88. package/dist/test/integration/notification-unread-badge.test.js.map +1 -1
  89. package/dist/test/integration/page-layout-route-guard.test.d.ts +1 -0
  90. package/dist/test/integration/page-layout-route-guard.test.js +56 -0
  91. package/dist/test/integration/page-layout-route-guard.test.js.map +1 -0
  92. package/dist/test/integration/permission-warnings.test.js +3 -2
  93. package/dist/test/integration/permission-warnings.test.js.map +1 -1
  94. package/dist/test/integration/removed-member-notifications.test.js +3 -2
  95. package/dist/test/integration/removed-member-notifications.test.js.map +1 -1
  96. package/dist/test/integration/role-delete-reassign.test.js +2 -1
  97. package/dist/test/integration/role-delete-reassign.test.js.map +1 -1
  98. package/dist/test/integration/role-permission-ancestors.test.js +2 -1
  99. package/dist/test/integration/role-permission-ancestors.test.js.map +1 -1
  100. package/dist/test/unit/frontend-bootstrap.test.js +13 -0
  101. package/dist/test/unit/frontend-bootstrap.test.js.map +1 -1
  102. package/dist/test/unit/implementations/dms/category-slug-registry.test.js +14 -5
  103. package/dist/test/unit/implementations/dms/category-slug-registry.test.js.map +1 -1
  104. package/dist/test/unit/implementations/dms/frontend-backend-proxies.test.d.ts +1 -0
  105. package/dist/test/unit/implementations/dms/frontend-backend-proxies.test.js +66 -0
  106. package/dist/test/unit/implementations/dms/frontend-backend-proxies.test.js.map +1 -0
  107. package/dist/test/unit/implementations/dms/manifest-strip.test.js +26 -0
  108. package/dist/test/unit/implementations/dms/manifest-strip.test.js.map +1 -1
  109. package/dist/test/unit/implementations/dms/nav-badges.test.js +23 -9
  110. package/dist/test/unit/implementations/dms/nav-badges.test.js.map +1 -1
  111. package/dist/test/unit/implementations/dms-html-render/generate-html.test.js +22 -4
  112. package/dist/test/unit/implementations/dms-html-render/generate-html.test.js.map +1 -1
  113. package/dist/test/unit/interfaces/dms/page-layout-query.test.js +12 -0
  114. package/dist/test/unit/interfaces/dms/page-layout-query.test.js.map +1 -1
  115. package/dist/test/unit/interfaces/dms/page-teardown.test.js +1 -1
  116. package/dist/test/unit/interfaces/dms/page-teardown.test.js.map +1 -1
  117. package/dist/test/unit/interfaces/dms-base/table-and-card-catalog.test.js +25 -0
  118. package/dist/test/unit/interfaces/dms-base/table-and-card-catalog.test.js.map +1 -1
  119. package/dist/test/unit/pages/settings/member-roles-field.test.js +1 -1
  120. package/dist/test/unit/pages/settings/member-roles-field.test.js.map +1 -1
  121. package/dist/test/unit/pages/settings/role-delete-confirm.test.js.map +1 -1
  122. package/dist/test/unit/realtime/menu-topics.test.d.ts +1 -0
  123. package/dist/test/unit/realtime/menu-topics.test.js +26 -0
  124. package/dist/test/unit/realtime/menu-topics.test.js.map +1 -0
  125. package/dist/validation/regional-preferences.schema.d.ts +2 -2
  126. package/frontend-vue/deferred-plugins.ts +3 -4
  127. package/frontend-vue/dms.email.ts +5 -1
  128. package/frontend-vue/dms.frontend.ts +15 -5
  129. package/frontend-vue/dms.server.ts +74 -0
  130. package/frontend-vue/layers/dms-auth/app/composables/useLogout.ts +1 -1
  131. package/frontend-vue/layers/dms-auth/app/composables/usePostLoginRedirect.ts +16 -3
  132. package/frontend-vue/layers/dms-auth/app/custom-pages/auth/2fa.vue +3 -1
  133. package/frontend-vue/layers/dms-auth/app/custom-pages/auth/backup.vue +3 -1
  134. package/frontend-vue/layers/dms-auth/app/custom-pages/auth/forgot-validation.vue +1 -1
  135. package/frontend-vue/layers/dms-auth/app/custom-pages/auth/oauth-complete.vue +26 -3
  136. package/frontend-vue/layers/dms-auth/app/custom-pages/auth/recover.vue +1 -1
  137. package/frontend-vue/layers/dms-auth/app/custom-pages/auth/signup.vue +22 -25
  138. package/frontend-vue/layers/dms-auth/app/custom-pages/auth/validate.vue +1 -1
  139. package/frontend-vue/layers/dms-auth/app/middleware/auth.ts +5 -7
  140. package/frontend-vue/layers/dms-auth/app/plugins/permissions-preload.ts +17 -0
  141. package/frontend-vue/layers/dms-auth/server/accounts.ts +135 -0
  142. package/frontend-vue/layers/dms-auth/server/actions.ts +251 -0
  143. package/frontend-vue/layers/dms-auth/server/oauth.ts +260 -0
  144. package/frontend-vue/layers/dms-auth/server/session.ts +158 -0
  145. package/frontend-vue/layers/dms-core/app/build/utils/asyncDataScope.ts +47 -0
  146. package/frontend-vue/layers/dms-core/app/build/utils/permission-preview.ts +6 -2
  147. package/frontend-vue/layers/dms-core/app/composables/auth/useApiData.ts +148 -0
  148. package/frontend-vue/layers/dms-core/app/composables/auth/useAuthFetch.ts +7 -20
  149. package/frontend-vue/layers/dms-core/app/composables/auth/useCurrentUser.ts +2 -2
  150. package/frontend-vue/layers/dms-core/app/composables/auth/useMultiAccount.ts +18 -5
  151. package/frontend-vue/layers/dms-core/app/composables/auth/usePermissions.ts +35 -6
  152. package/frontend-vue/layers/dms-core/app/composables/auth/useSessionRecovery.ts +1 -1
  153. package/frontend-vue/layers/dms-core/app/composables/period/compareResolvers.ts +20 -14
  154. package/frontend-vue/layers/dms-core/app/composables/period/periodClock.ts +121 -0
  155. package/frontend-vue/layers/dms-core/app/composables/period/periodRenderNow.ts +42 -0
  156. package/frontend-vue/layers/dms-core/app/composables/period/periodTimeZone.ts +31 -0
  157. package/frontend-vue/layers/dms-core/app/composables/period/presetResolvers.ts +48 -19
  158. package/frontend-vue/layers/dms-core/app/composables/period/usePeriod.ts +33 -4
  159. package/frontend-vue/layers/dms-core/app/composables/period/usePeriodScope.ts +78 -25
  160. package/frontend-vue/layers/dms-core/app/composables/realtime/usePageRealtime.ts +3 -0
  161. package/frontend-vue/layers/dms-core/app/composables/realtime/useUserRealtime.ts +36 -6
  162. package/frontend-vue/layers/dms-core/app/composables/user/useUserRegionalPreferences.ts +1 -1
  163. package/frontend-vue/layers/dms-core/app/plugins/async-data-scope.ts +26 -0
  164. package/frontend-vue/layers/dms-core/app/utils/pageRoutes.ts +22 -0
  165. package/frontend-vue/layers/dms-core/app/utils/regional.ts +36 -3
  166. package/frontend-vue/layers/dms-core/app/utils/trailingDeduper.ts +10 -1
  167. package/frontend-vue/layers/dms-core/shared/types/app-config.ts +48 -4
  168. package/frontend-vue/layers/dms-layout/app/app.config.ts +42 -23
  169. package/frontend-vue/layers/dms-layout/app/assets/css/main.css +72 -20
  170. package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardFrame.vue +9 -0
  171. package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardSidebar.vue +0 -3
  172. package/frontend-vue/layers/dms-layout/app/build/components/notification/NotificationPopover.vue +9 -3
  173. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/members/MemberRolePicker.vue +13 -12
  174. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/notification/NotificationInboxDisplay.vue +17 -7
  175. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/notification/NotificationPreferencesForm.vue +0 -1
  176. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/profile/ProfilePersonalInfo.vue +36 -33
  177. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/profile/ProfileSecuritySummary.vue +1 -2
  178. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/roles/role-types.ts +5 -4
  179. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/security/SecuritySessions.vue +35 -28
  180. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/security/SecurityStatus.vue +0 -1
  181. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/security/SecurityTwoFactor.vue +1 -1
  182. package/frontend-vue/layers/dms-layout/app/build/composables/dev-reload/useDevReloadHolder.ts +7 -6
  183. package/frontend-vue/layers/dms-layout/app/build/composables/layout/sidePanelState.ts +1 -1
  184. package/frontend-vue/layers/dms-layout/app/build/composables/navigation/useVisitNavBadges.ts +26 -0
  185. package/frontend-vue/layers/dms-layout/app/build/composables/notification/useNotificationCatalog.ts +25 -23
  186. package/frontend-vue/layers/dms-layout/app/build/composables/notification/useNotificationPreferences.ts +44 -23
  187. package/frontend-vue/layers/dms-layout/app/build/composables/security/useSessionHandoff.ts +1 -1
  188. package/frontend-vue/layers/dms-layout/app/build/composables/settings/profile/preferencesSummary.ts +1 -1
  189. package/frontend-vue/layers/dms-layout/app/build/composables/settings/profile/useAccountData.ts +3 -2
  190. package/frontend-vue/layers/dms-layout/app/build/composables/settings/profile/useProfileAvatar.ts +1 -18
  191. package/frontend-vue/layers/dms-layout/app/build/composables/settings/profile/useProfilePreferences.ts +19 -23
  192. package/frontend-vue/layers/dms-layout/app/build/composables/settings/security/useSecurityFormat.ts +6 -3
  193. package/frontend-vue/layers/dms-layout/app/build/composables/settings/security/useSecurityOverview.ts +13 -18
  194. package/frontend-vue/layers/dms-layout/app/build/utils/modules-catalog.ts +8 -6
  195. package/frontend-vue/layers/dms-layout/app/build/utils/publicErrorMessage.ts +19 -0
  196. package/frontend-vue/layers/dms-layout/app/components/navigation/CollapsedMenuItem.vue +0 -11
  197. package/frontend-vue/layers/dms-layout/app/components/navigation/NavigationMenu.vue +3 -13
  198. package/frontend-vue/layers/dms-layout/app/composables/general/useSystemState.ts +7 -11
  199. package/frontend-vue/layers/dms-layout/app/composables/notification/useNotifications.ts +103 -22
  200. package/frontend-vue/layers/dms-layout/app/composables/page/useModulesListing.ts +25 -102
  201. package/frontend-vue/layers/dms-layout/app/composables/page/usePageComponentPreload.ts +0 -10
  202. package/frontend-vue/layers/dms-layout/app/composables/page/useSiteLayout.ts +177 -96
  203. package/frontend-vue/layers/dms-layout/app/composables/profile/useUserLanguage.ts +3 -2
  204. package/frontend-vue/layers/dms-layout/app/custom-pages/modules.vue +6 -28
  205. package/frontend-vue/layers/dms-layout/app/emails/components/EmailLayout.vue +31 -11
  206. package/frontend-vue/layers/dms-layout/app/error.vue +5 -2
  207. package/frontend-vue/layers/dms-layout/app/middleware/module-routing.global.ts +1 -1
  208. package/frontend-vue/layers/dms-layout/app/middleware/page-leave-guard.global.ts +2 -2
  209. package/frontend-vue/layers/dms-layout/app/pages/[...slug].vue +12 -9
  210. package/frontend-vue/layers/dms-layout/app/plugins/dms-menu-sync.client.ts +31 -3
  211. package/frontend-vue/layers/dms-layout/app/plugins/favicon.ts +58 -0
  212. package/frontend-vue/layers/dms-layout/app/plugins/notification-stream.client.ts +15 -7
  213. package/frontend-vue/layers/dms-layout/app/plugins/profile-sync.client.ts +5 -1
  214. package/frontend-vue/layers/dms-layout/app/plugins/regional-preferences.ts +10 -4
  215. package/frontend-vue/layers/dms-layout/app/plugins/time-zone-cookie.client.ts +12 -0
  216. package/frontend-vue/layers/dms-layout/app/types/page.ts +13 -0
  217. package/frontend-vue/layers/dms-layout/server/html-render.ts +58 -0
  218. package/frontend-vue/layers/dms-layout/server/render-token.ts +95 -0
  219. package/frontend-vue/layers/dms-onboarding/app/composables/onboarding/useOnboarding.ts +25 -21
  220. package/frontend-vue/layers/dms-onboarding/app/middleware/onboarding.global.ts +1 -1
  221. package/frontend-vue/layers/dms-onboarding/app/plugins/onboarding-preload.ts +9 -0
  222. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/registerDefaults.ts +4 -1
  223. package/frontend-vue/layers/dms-ui/app/build/composables/files/useFileMetadata.ts +162 -0
  224. package/frontend-vue/layers/dms-ui/app/build/composables/form/formRecordLoad.ts +23 -4
  225. package/frontend-vue/layers/dms-ui/app/build/composables/live/usePageDataRefresh.ts +101 -0
  226. package/frontend-vue/layers/dms-ui/app/build/composables/navigation/useNavBadges.ts +33 -6
  227. package/frontend-vue/layers/dms-ui/app/build/composables/period/periodDisplay.ts +19 -16
  228. package/frontend-vue/layers/dms-ui/app/build/composables/period/periodQuery.ts +40 -20
  229. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableFooter.ts +6 -8
  230. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableListData.ts +42 -14
  231. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableRows.ts +19 -8
  232. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViews.ts +1 -1
  233. package/frontend-vue/layers/dms-ui/app/components/chart/ChartCard.vue +5 -2
  234. package/frontend-vue/layers/dms-ui/app/components/flow-canvas/FlowCanvas.vue +9 -9
  235. package/frontend-vue/layers/dms-ui/app/components/form/Form.vue +20 -18
  236. package/frontend-vue/layers/dms-ui/app/components/form/components/Cascader.vue +2 -1
  237. package/frontend-vue/layers/dms-ui/app/components/form/components/File.vue +26 -132
  238. package/frontend-vue/layers/dms-ui/app/components/form/components/Image.vue +40 -105
  239. package/frontend-vue/layers/dms-ui/app/components/form/components/InputAddress.vue +2 -0
  240. package/frontend-vue/layers/dms-ui/app/components/form/components/Relation.vue +4 -4
  241. package/frontend-vue/layers/dms-ui/app/components/period/PeriodSelector.vue +9 -13
  242. package/frontend-vue/layers/dms-ui/app/components/table-view/CascaderPath.vue +2 -1
  243. package/frontend-vue/layers/dms-ui/app/components/table-view/FilePreview.vue +12 -24
  244. package/frontend-vue/layers/dms-ui/app/components/table-view/ImagePreview.vue +17 -29
  245. package/frontend-vue/layers/dms-ui/app/components/table-view/KanbanBoard.vue +7 -3
  246. package/frontend-vue/layers/dms-ui/app/components/table-view/TableView.vue +20 -8
  247. package/frontend-vue/layers/dms-ui/app/components/top-list/TopListCard.vue +8 -11
  248. package/frontend-vue/layers/dms-ui/app/composables/chart/useChartFetch.ts +146 -182
  249. package/frontend-vue/layers/dms-ui/app/composables/live/usePageLiveRefresh.ts +8 -2
  250. package/frontend-vue/layers/dms-ui/app/composables/useFileReadUrls.ts +11 -99
  251. package/frontend-vue/layers/dms-ui/app/utils/blockRefresh.ts +32 -3
  252. package/frontend-vue/package.json +2 -2
  253. package/frontend-vue/pnpm-lock.yaml +5 -5
  254. package/frontend-vue/server/private-options.ts +27 -0
  255. package/frontend-vue/tests/async-data-scope.test.ts +121 -0
  256. package/frontend-vue/tests/auth-middleware-error.test.ts +3 -3
  257. package/frontend-vue/tests/banner-block-fetch.test.ts +3 -0
  258. package/frontend-vue/tests/block-fetch-options-forwarding.test.ts +3 -0
  259. package/frontend-vue/tests/block-items-period-realtime.test.ts +3 -0
  260. package/frontend-vue/tests/block-not-found.test.ts +16 -2
  261. package/frontend-vue/tests/block-page-refresh.test.ts +3 -0
  262. package/frontend-vue/tests/block-route-params.test.ts +3 -0
  263. package/frontend-vue/tests/branding-defaults.test.ts +34 -0
  264. package/frontend-vue/tests/builtin-emails.test.ts +23 -2
  265. package/frontend-vue/tests/chart-live-pause.test.ts +4 -1
  266. package/frontend-vue/tests/chart-value-propagation.test.ts +4 -1
  267. package/frontend-vue/tests/composed-text-blocks.test.ts +3 -0
  268. package/frontend-vue/tests/email-layout.test.ts +66 -4
  269. package/frontend-vue/tests/error-page-message.test.ts +25 -2
  270. package/frontend-vue/tests/favicon.test.ts +131 -0
  271. package/frontend-vue/tests/file-fields-ssr.test.ts +145 -0
  272. package/frontend-vue/tests/file-read-urls.test.ts +145 -34
  273. package/frontend-vue/tests/file-unavailable.test.ts +5 -1
  274. package/frontend-vue/tests/fixtures/dataEngine.ts +44 -0
  275. package/frontend-vue/tests/form-load-error.test.ts +55 -1
  276. package/frontend-vue/tests/helpers/api-data-runtime.ts +77 -0
  277. package/frontend-vue/tests/image-hydration-ids.test.ts +21 -16
  278. package/frontend-vue/tests/image-reference-preservation.test.ts +59 -53
  279. package/frontend-vue/tests/input-number-adjust.test.ts +14 -0
  280. package/frontend-vue/tests/key-value-list-copy.test.ts +3 -0
  281. package/frontend-vue/tests/member-role-picker.test.ts +4 -2
  282. package/frontend-vue/tests/menu-sync-reconnect.test.ts +47 -0
  283. package/frontend-vue/tests/metadata-retry.test.ts +5 -1
  284. package/frontend-vue/tests/module-routing.test.ts +2 -5
  285. package/frontend-vue/tests/modules-catalog.test.ts +17 -4
  286. package/frontend-vue/tests/modules-listing-transfer.test.ts +95 -0
  287. package/frontend-vue/tests/multi-account-validate.test.ts +23 -1
  288. package/frontend-vue/tests/nav-badge-tones.test.ts +66 -3
  289. package/frontend-vue/tests/notification-counts-from-writes.test.ts +223 -0
  290. package/frontend-vue/tests/notification-counts-transfer.test.ts +88 -0
  291. package/frontend-vue/tests/notification-inbox-display.test.ts +57 -5
  292. package/frontend-vue/tests/notification-popover-errors.test.ts +21 -1
  293. package/frontend-vue/tests/oauth-complete-handoff.test.ts +209 -0
  294. package/frontend-vue/tests/onboarding-middleware.test.ts +1 -1
  295. package/frontend-vue/tests/onboarding-transfer.test.ts +106 -0
  296. package/frontend-vue/tests/page-data-refresh.test.ts +141 -0
  297. package/frontend-vue/tests/page-fill-height.test.ts +16 -3
  298. package/frontend-vue/tests/page-no-visible-blocks.test.ts +12 -2
  299. package/frontend-vue/tests/page-realtime-remount.test.ts +1 -1
  300. package/frontend-vue/tests/page-record-label.test.ts +0 -7
  301. package/frontend-vue/tests/page-validation-redirect.test.ts +12 -2
  302. package/frontend-vue/tests/period-hydration-now.test.ts +185 -0
  303. package/frontend-vue/tests/period-live-pause.test.ts +3 -0
  304. package/frontend-vue/tests/period-reader-zone.test.ts +137 -0
  305. package/frontend-vue/tests/period-scope-first-fetch.test.ts +17 -1
  306. package/frontend-vue/tests/period-ssr.test.ts +195 -0
  307. package/frontend-vue/tests/permission-preview-partial.test.ts +0 -2
  308. package/frontend-vue/tests/permissions-transfer.test.ts +194 -0
  309. package/frontend-vue/tests/post-login-redirect.test.ts +10 -5
  310. package/frontend-vue/tests/profile-personal-info-ssr.test.ts +150 -0
  311. package/frontend-vue/tests/profile-preferences-summary.test.ts +15 -11
  312. package/frontend-vue/tests/realtime-reconnect.test.ts +22 -3
  313. package/frontend-vue/tests/roles-name-errors.test.ts +1 -1
  314. package/frontend-vue/tests/row-side-panel.test.ts +1 -1
  315. package/frontend-vue/tests/security-sessions-load-error.test.ts +3 -1
  316. package/frontend-vue/tests/server-routes-vs-pages.test.ts +26 -0
  317. package/frontend-vue/tests/server-routes.test.ts +582 -0
  318. package/frontend-vue/tests/session-handoff.test.ts +1 -1
  319. package/frontend-vue/tests/settings-reads-ssr.test.ts +232 -0
  320. package/frontend-vue/tests/side-panels.test.ts +8 -1
  321. package/frontend-vue/tests/sign-in-exit-history.test.ts +219 -0
  322. package/frontend-vue/tests/signup-invitation-link.test.ts +56 -8
  323. package/frontend-vue/tests/site-layout-categories.test.ts +42 -0
  324. package/frontend-vue/tests/site-layout-hydration.test.ts +3 -3
  325. package/frontend-vue/tests/site-layout-rendered.test.ts +103 -0
  326. package/frontend-vue/tests/site-layout-resync.test.ts +87 -0
  327. package/frontend-vue/tests/ssr-guard-preload.test.ts +188 -0
  328. package/frontend-vue/tests/table-cell-tones.test.ts +36 -1
  329. package/frontend-vue/tests/table-data-ssr.test.ts +195 -0
  330. package/frontend-vue/tests/table-list-data-requests.test.ts +154 -0
  331. package/frontend-vue/tests/table-retry-loading.test.ts +37 -0
  332. package/frontend-vue/tests/table-source-navigation.test.ts +0 -1
  333. package/frontend-vue/tests/table-source-period-scope.test.ts +0 -1
  334. package/frontend-vue/tests/table-view-views.test.ts +1 -1
  335. package/frontend-vue/tests/top-list-card-actions.test.ts +3 -0
  336. package/frontend-vue/tests/top-list-card-period-badge.test.ts +79 -0
  337. package/frontend-vue/tests/top-list-card-value-suffix.test.ts +3 -0
  338. package/frontend-vue/tests/trailing-deduper.test.ts +22 -0
  339. package/frontend-vue/tests/use-api-data-update.test.ts +119 -0
  340. package/frontend-vue/tests/use-api-data.test.ts +410 -0
  341. package/frontend-vue/tests/user-language.test.ts +8 -5
  342. package/frontend-vue/tests/widget-label-i18n.test.ts +4 -1
  343. package/frontend-vue/vitest.config.ts +6 -0
  344. package/package.json +2 -2
  345. package/skills/dms/SKILL.md +2 -1
  346. package/skills/dms-module/SKILL.md +1 -1
  347. package/skills/dms-pages/SKILL.md +4 -1
  348. package/frontend-vue/layers/dms-core/app/build/composables/page/usePageRecordLabel.ts +0 -9
  349. package/frontend-vue/layers/dms-layout/app/composables/page/usePrefetch.ts +0 -21
  350. package/frontend-vue/layers/dms-ui/app/build/composables/form/useControlError.ts +0 -10
  351. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useServerRenderedAsyncData.ts +0 -23
  352. package/frontend-vue/tests/control-error-path.test.ts +0 -11
  353. package/frontend-vue/tests/modules-listing-per-request.test.ts +0 -104
  354. package/frontend-vue/tests/page-layout-background-refresh.test.ts +0 -321
@@ -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
  }));
@@ -0,0 +1,150 @@
1
+ // @vitest-environment jsdom
2
+ /**
3
+ * The profile's personal information through the runtime's real async data:
4
+ * a server render reads the profile and the picture's URL and renders the
5
+ * name, the address with its verification and the picture; the browser
6
+ * hydrates with them without asking again, and the form edits a copy.
7
+ */
8
+ import {
9
+ computed,
10
+ defineComponent,
11
+ h,
12
+ inject,
13
+ nextTick,
14
+ onScopeDispose,
15
+ ref,
16
+ shallowReactive,
17
+ Suspense,
18
+ watch,
19
+ type App,
20
+ } from "vue";
21
+ import { afterEach, beforeEach, expect, it, vi } from "vitest";
22
+ import { hydrate, serverRender, stubApiData } from "./helpers/api-data-runtime";
23
+ import { useFileReadUrls } from "../layers/dms-ui/app/composables/useFileReadUrls";
24
+
25
+ vi.mock("#dms-core/app/composables/useFieldErrors", () => ({
26
+ useFieldErrors: () => ({
27
+ errors: {},
28
+ aria: () => ({}),
29
+ errorId: (field: string) => `${field}-error`,
30
+ clear: () => {},
31
+ setError: async () => {},
32
+ handleApiError: async () => {},
33
+ }),
34
+ fieldErrorId: (id: string) => `${id}-error`,
35
+ }));
36
+ vi.mock("#dms-ui/app/composables/unsaved-changes/useUnsavedChanges", () => ({
37
+ useUnsavedChanges: () => {},
38
+ }));
39
+
40
+ const ENDPOINT = "/settings/user/profile";
41
+ const PROFILE = {
42
+ name: "Bea Owner",
43
+ email: "bea@example.test",
44
+ avatar: { key: "avatars/bea.png" },
45
+ isValidated: true,
46
+ };
47
+
48
+ const authFetch = vi.fn(async (url: string) =>
49
+ url === ENDPOINT
50
+ ? structuredClone(PROFILE)
51
+ : { url: "https://files.test/bea.png", filename: "bea.png" },
52
+ );
53
+ const mounted: App[] = [];
54
+
55
+ const Input = defineComponent({
56
+ props: { modelValue: String, id: String },
57
+ setup: (props) => () => h("input", { id: props.id, value: props.modelValue }),
58
+ });
59
+ const Avatar = defineComponent({
60
+ props: { src: String, name: String },
61
+ setup: (props) => () => h("img", { src: props.src ?? "", alt: props.name }),
62
+ });
63
+ const Badge = defineComponent({
64
+ props: { label: String },
65
+ setup: (props) => () => h("span", props.label),
66
+ });
67
+ const Slots = defineComponent({
68
+ setup:
69
+ (_, { slots }) =>
70
+ () =>
71
+ h("div", [slots.label?.(), slots.default?.()]),
72
+ });
73
+
74
+ function register(app: App): void {
75
+ for (const name of ["DmsCard", "DmsFieldRow", "DmsLink", "DmsSaveBar"])
76
+ app.component(name, Slots);
77
+ for (const name of ["UButton", "UIcon", "USkeleton", "DmsFieldError"])
78
+ app.component(name, () => null);
79
+ app.component("UInput", Input);
80
+ app.component("UBadge", Badge);
81
+ }
82
+
83
+ beforeEach(async () => {
84
+ vi.stubGlobal("computed", computed);
85
+ vi.stubGlobal("ref", ref);
86
+ vi.stubGlobal("watch", watch);
87
+ vi.stubGlobal("inject", inject);
88
+ vi.stubGlobal("onScopeDispose", onScopeDispose);
89
+ vi.stubGlobal("shallowReactive", shallowReactive);
90
+ vi.stubGlobal("useTemplateRef", () => ref(null));
91
+ vi.stubGlobal("CONTENT_LANGUAGE_HEADER", "x-content-language");
92
+ vi.stubGlobal("useI18n", () => ({ t: (key: string) => key }));
93
+ vi.stubGlobal("useToast", () => ({ add: vi.fn() }));
94
+ vi.stubGlobal("useTranslation", () => ({
95
+ processApiMessage: (key: string) => key,
96
+ }));
97
+ vi.stubGlobal("useCurrentUser", () => ({
98
+ user: ref({ name: "Session name", email: "bea@example.test" }),
99
+ refresh: async () => {},
100
+ }));
101
+ vi.stubGlobal("useProfileAvatar", () => ({ upload: vi.fn() }));
102
+ vi.stubGlobal("useUploadWithProgress", () => ({}));
103
+ vi.stubGlobal("useFileReadUrls", useFileReadUrls);
104
+ stubApiData(authFetch as never);
105
+ });
106
+
107
+ afterEach(() => {
108
+ mounted.splice(0).forEach((app) => app.unmount());
109
+ vi.unstubAllEnvs();
110
+ vi.unstubAllGlobals();
111
+ authFetch.mockClear();
112
+ });
113
+
114
+ it("renders the saved profile and its picture on the server, and the browser does not read them again", async () => {
115
+ vi.doMock(
116
+ "../layers/dms-layout/app/build/components/pages/settings/profile/ProfileAvatar.vue",
117
+ () => ({ default: Avatar }),
118
+ );
119
+ const { default: ProfilePersonalInfo } = await import(
120
+ "../layers/dms-layout/app/build/components/pages/settings/profile/ProfilePersonalInfo.vue"
121
+ );
122
+ // As the page renders it: inside the page's <Suspense>.
123
+ const Page = defineComponent({
124
+ setup: () => () =>
125
+ h(Suspense, null, {
126
+ default: () => h(ProfilePersonalInfo, { endpoint: ENDPOINT }),
127
+ }),
128
+ });
129
+
130
+ vi.stubEnv("SSR", true);
131
+ const { html, payload } = await serverRender(Page, register);
132
+ vi.stubEnv("SSR", false);
133
+ expect(html).toContain('value="Bea Owner"');
134
+ expect(html).toContain('src="https://files.test/bea.png"');
135
+ expect(html).toContain("page.settings.security.verified");
136
+ expect(authFetch.mock.calls.map(([url]) => url)).toEqual([
137
+ ENDPOINT,
138
+ "/api/files/metadata",
139
+ ]);
140
+
141
+ authFetch.mockClear();
142
+ const { app, container } = hydrate(Page, html, payload, register);
143
+ mounted.push(app);
144
+ await nextTick();
145
+ await new Promise((done) => setTimeout(done, 0));
146
+ expect(authFetch).not.toHaveBeenCalled();
147
+ expect(
148
+ container.querySelector<HTMLInputElement>("#profile-name")?.value,
149
+ ).toBe("Bea Owner");
150
+ });
@@ -13,6 +13,7 @@ import {
13
13
  } from "../layers/dms-layout/app/build/composables/settings/profile/preferencesSummary";
14
14
  import type { NotificationSubject } from "../layers/dms-layout/app/build/composables/notification/useNotificationCatalog";
15
15
  import { DEFAULT_ACCESSIBILITY_PREFERENCES } from "../layers/dms-ui/app/build/utils/accessibilityPreferences";
16
+ import { browserRuntime, stubApiData } from "./helpers/api-data-runtime";
16
17
 
17
18
  const KEY = "page.settings.profile.preferences";
18
19
 
@@ -221,7 +222,8 @@ const colorMode = ref<"light" | "dark" | "system">("dark");
221
222
  const prefersDark = ref(false);
222
223
  let authFetch: ReturnType<typeof vi.fn>;
223
224
  let resolveAccess: (value: unknown) => void;
224
- const loadPreferences = vi.fn(async () => {});
225
+ // Whether the section reads the notification preferences (its row shows).
226
+ let readsPreferences: () => boolean = () => false;
225
227
 
226
228
  vi.mock("@vueuse/core", () => ({ usePreferredDark: () => prefersDark }));
227
229
  vi.mock("#dms-ui/app/components/icon-well/IconWell.vue", async () => {
@@ -308,12 +310,14 @@ vi.mock(
308
310
  async () => {
309
311
  const { ref } = await import("vue");
310
312
  return {
311
- useNotificationPreferences: () => ({
312
- isLoading: preferencesLoading,
313
- loadFailed: ref(false),
314
- isEnabled: (s: { id: string }) => s.id !== "c",
315
- load: loadPreferences,
316
- }),
313
+ useNotificationPreferences: (enabled: () => boolean) => {
314
+ readsPreferences = enabled;
315
+ return {
316
+ isLoading: preferencesLoading,
317
+ loadFailed: ref(false),
318
+ isEnabled: (s: { id: string }) => s.id !== "c",
319
+ };
320
+ },
317
321
  };
318
322
  },
319
323
  );
@@ -357,6 +361,7 @@ async function mountSection(): Promise<void> {
357
361
  "../layers/dms-layout/app/build/components/pages/settings/profile/ProfilePreferencesSummary.vue"
358
362
  );
359
363
  app = createApp(ProfilePreferencesSummary);
364
+ browserRuntime(app);
360
365
  app.component(
361
366
  "USkeleton",
362
367
  defineComponent({
@@ -403,7 +408,6 @@ beforeEach(() => {
403
408
  preferencesLoading.value = true;
404
409
  colorMode.value = "dark";
405
410
  prefersDark.value = false;
406
- loadPreferences.mockClear();
407
411
  authFetch = vi.fn(
408
412
  () =>
409
413
  new Promise((resolve) => {
@@ -411,7 +415,7 @@ beforeEach(() => {
411
415
  }),
412
416
  );
413
417
  vi.stubGlobal("useI18n", () => ({ t, locale: ref("en-GB") }));
414
- vi.stubGlobal("useAuthFetch", () => ({ $authFetch: authFetch }));
418
+ stubApiData((...args: unknown[]) => authFetch(...(args as [])));
415
419
  host = document.createElement("div");
416
420
  document.body.append(host);
417
421
  });
@@ -440,7 +444,7 @@ describe("Preferences & access section", () => {
440
444
  "/settings/user/notifications",
441
445
  "/settings/user/appearance",
442
446
  ]);
443
- expect(loadPreferences).toHaveBeenCalledOnce();
447
+ expect(readsPreferences()).toBe(true);
444
448
 
445
449
  // The preferences answer first: still a skeleton until the bell's count.
446
450
  preferencesLoading.value = false;
@@ -502,7 +506,7 @@ describe("Preferences & access section", () => {
502
506
  await mountSection();
503
507
  expect(host.querySelectorAll(":scope > div > div")).toHaveLength(1);
504
508
  expect(buttons()).toEqual([]);
505
- expect(loadPreferences).not.toHaveBeenCalled();
509
+ expect(readsPreferences()).toBe(false);
506
510
 
507
511
  resolveAccess({ roles: [], workspaceOwner: false, platformOwner: false });
508
512
  await flush();
@@ -74,11 +74,16 @@ const installFetch = (server: FakeServer): void => {
74
74
  return openUserStream(server, init!.signal!);
75
75
  }
76
76
  const headers = init?.headers as Record<string, string>;
77
+ const pageId = url.slice(SUBSCRIBE_URL_PREFIX.length);
77
78
  server.subscriptions.push({
78
- pageId: url.slice(SUBSCRIBE_URL_PREFIX.length),
79
+ pageId,
79
80
  sessionId: headers[SESSION_HEADER]!,
80
81
  });
81
- return { ok: true, status: 200 };
82
+ return {
83
+ ok: true,
84
+ status: 200,
85
+ json: async () => ({ ok: true, topics: [`table:${pageId}`] }),
86
+ };
82
87
  }) as unknown as typeof fetch;
83
88
  };
84
89
 
@@ -113,7 +118,7 @@ beforeEach(() => {
113
118
  vi.stubGlobal("useDmsRuntimeConfig", () => ({
114
119
  public: { dms: { baseURL: BASE_URL } },
115
120
  }));
116
- vi.stubGlobal("useUserSession", () => ({ loggedIn }));
121
+ vi.stubGlobal("useDmsUserSession", () => ({ loggedIn }));
117
122
  vi.stubGlobal("useSessionRecovery", () => ({
118
123
  refreshSession: async () => true,
119
124
  redirectToAuth: async () => {},
@@ -159,6 +164,20 @@ describe("useUserRealtime reconnection", () => {
159
164
  ]);
160
165
  });
161
166
 
167
+ it("holds the page's topics its subscription answered, until the stream drops", async () => {
168
+ const api = await loadApi();
169
+ await settle();
170
+ await api.setPage(PAGE_ID);
171
+ expect(api.pageTopics.value).toEqual([`table:${PAGE_ID}`]);
172
+
173
+ lastStream().end();
174
+ await settle();
175
+ expect(api.pageTopics.value).toEqual([]);
176
+ await vi.advanceTimersByTimeAsync(INITIAL_DELAY_MS);
177
+ await settle();
178
+ expect(api.pageTopics.value).toEqual([`table:${PAGE_ID}`]);
179
+ });
180
+
162
181
  it("backs off further while the server stays unreachable", async () => {
163
182
  await loadApi();
164
183
  await settle();
@@ -282,7 +282,7 @@ it("asks the delete dialog the server words, then deletes with the picked role",
282
282
  await flush();
283
283
 
284
284
  expect(authFetch).toHaveBeenCalledWith(
285
- "/settings/workspace/roles/role-1/delete-confirm",
285
+ "/dms/pages/settings/workspace/roles/role-1/delete-confirm",
286
286
  );
287
287
  expect(api.deleteRole).toHaveBeenCalledWith("role-1", {
288
288
  reassignTo: "role-2",
@@ -142,7 +142,7 @@ function installRuntime(): void {
142
142
  params ? `${value}(${params.index}/${params.total})` : value,
143
143
  processApiMessage: (value: string) => value,
144
144
  }),
145
- useUserSession: () => ({ loggedIn: ref(true) }),
145
+ useDmsUserSession: () => ({ loggedIn: ref(true) }),
146
146
  useToast: () => ({ add: vi.fn() }),
147
147
  useModal: () => ({ open: vi.fn() }),
148
148
  useDrawer: () => ({ open: vi.fn() }),
@@ -11,6 +11,7 @@ import {
11
11
  watch,
12
12
  type App,
13
13
  } from "vue";
14
+ import { browserRuntime, stubApiData } from "./helpers/api-data-runtime";
14
15
 
15
16
  const overview = ref({ activeSessions: 1 });
16
17
  const authFetch = vi.fn();
@@ -77,6 +78,7 @@ async function mount(): Promise<void> {
77
78
  "../layers/dms-layout/app/build/components/pages/settings/security/SecuritySessions.vue"
78
79
  );
79
80
  app = createApp(SecuritySessions);
81
+ browserRuntime(app);
80
82
  app.component("DmsSection", Passthrough);
81
83
  app.component("DmsEmptyState", EmptyState);
82
84
  for (const name of ["USkeleton", "UButton", "DmsListRow", "UModal"]) {
@@ -93,7 +95,7 @@ beforeEach(() => {
93
95
  vi.stubGlobal("onMounted", onMounted);
94
96
  vi.stubGlobal("useI18n", () => ({ t: (key: string) => key }));
95
97
  vi.stubGlobal("useToast", () => ({ add: vi.fn() }));
96
- vi.stubGlobal("useAuthFetch", () => ({ $authFetch: authFetch }));
98
+ stubApiData(authFetch);
97
99
  authFetch.mockReset();
98
100
  host = document.createElement("div");
99
101
  document.body.append(host);
@@ -0,0 +1,26 @@
1
+ import { readdirSync, readFileSync, statSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { expect, it } from "vitest";
4
+
5
+ // A module route owns its path for every method: one on a page's path makes
6
+ // the page unreachable (the invite link `GET /auth/signup` answered 405).
7
+ const serverEntry = readFileSync(join(__dirname, "../dms.server.ts"), "utf8");
8
+ const routePaths = [
9
+ ...serverEntry.matchAll(/registerRoute\(\s*"[A-Z]+",\s*"([^"]+)"/g),
10
+ ].map((match) => match[1]);
11
+
12
+ function pageSlugs(dir: string): string[] {
13
+ return readdirSync(dir).flatMap((name) => {
14
+ const path = join(dir, name);
15
+ if (statSync(path).isDirectory()) return pageSlugs(path);
16
+ const slug = /urlSlug:\s*"([^"]+)"/.exec(readFileSync(path, "utf8"));
17
+ return slug ? [`/${slug[1]}`] : [];
18
+ });
19
+ }
20
+
21
+ it("serves no module route on the path of a public page", () => {
22
+ const pages = pageSlugs(join(__dirname, "../../src/pages/public"));
23
+ expect(routePaths.length).toBeGreaterThan(0);
24
+ expect(pages).toContain("/auth/signup");
25
+ expect(routePaths.filter((path) => pages.includes(path))).toEqual([]);
26
+ });