@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,223 @@
1
+ // @vitest-environment jsdom
2
+ /**
3
+ * The bell's counts after a change of the feed: a write (mark read or
4
+ * unread, delete) answers them and its realtime event carries them, so the
5
+ * tab that wrote and every other tab of the user take them as they are. The
6
+ * write's own event, which comes before or after its answer, changes
7
+ * nothing and reads nothing. Through the real composable, its stream plugin
8
+ * and the runtime's state, over a fake feed.
9
+ */
10
+ import { defineComponent, h, nextTick, ref, type App } from "vue";
11
+ import { afterEach, beforeEach, expect, it, vi } from "vitest";
12
+ import { mountInBrowser, stubApiData } from "./helpers/api-data-runtime";
13
+
14
+ type Tone = "neutral" | "primary" | "warning" | "error";
15
+ interface Row {
16
+ _id: string;
17
+ isRead: boolean;
18
+ tone: Tone;
19
+ }
20
+ interface Counts {
21
+ all: number;
22
+ unread: number;
23
+ unreadTone?: Tone;
24
+ }
25
+ type StreamEvent = { type: string; payload?: Record<string, unknown> };
26
+
27
+ const TONE_ORDER: Tone[] = ["error", "warning", "primary", "neutral"];
28
+ const feed: Row[] = [];
29
+
30
+ /** The whole feed's counts, as `countFeedWithUnreadTone` gives them. */
31
+ function countsOf(): Counts {
32
+ const unread = feed.filter((row) => !row.isRead);
33
+ const tone = TONE_ORDER.find((t) => unread.some((row) => row.tone === t));
34
+ return tone
35
+ ? { all: feed.length, unread: unread.length, unreadTone: tone }
36
+ : { all: feed.length, unread: unread.length };
37
+ }
38
+
39
+ const requests: string[] = [];
40
+ // A write's answer can be held, to deliver its echo first.
41
+ let holdWrites: Promise<void> | undefined;
42
+ let failWrites = false;
43
+ const authFetch = vi.fn(async (url: string, options?: { method?: string }) => {
44
+ const method = options?.method ?? "GET";
45
+ requests.push(`${method} ${url}`);
46
+ if (method === "GET" && url.endsWith("/counts")) return countsOf();
47
+ if (failWrites) throw new Error("refused");
48
+ const id = url.split("/").pop();
49
+ const row = feed.find((entry) => entry._id === id);
50
+ if (url.includes("/mark-read/")) row!.isRead = true;
51
+ else if (url.includes("/mark-unread/")) row!.isRead = false;
52
+ else if (url.includes("/delete/")) feed.splice(feed.indexOf(row!), 1);
53
+ const counts = countsOf();
54
+ await holdWrites;
55
+ return { success: true, counts };
56
+ });
57
+
58
+ let streamHandler: ((event: unknown) => void) | undefined;
59
+ stubApiData(authFetch as never);
60
+ vi.stubGlobal("useComponentEvent", () => ({ sendComponentEvent: vi.fn() }));
61
+ vi.stubGlobal("useDmsUserSession", () => ({
62
+ user: ref({ _id: "u1" }),
63
+ loggedIn: ref(true),
64
+ }));
65
+ vi.stubGlobal("useUserRealtime", () => ({
66
+ subscribe: (_topic: string, handler: (event: unknown) => void) => {
67
+ streamHandler = handler;
68
+ return () => {
69
+ streamHandler = undefined;
70
+ };
71
+ },
72
+ }));
73
+ vi.stubGlobal("defineDmsPlugin", (setup: () => void) => setup);
74
+ vi.stubGlobal("watch", (await import("vue")).watch);
75
+ const { useNotifications } = await import(
76
+ "../layers/dms-layout/app/composables/notification/useNotifications"
77
+ );
78
+ vi.stubGlobal("useNotifications", useNotifications);
79
+ const { default: notificationStream } = await import(
80
+ "../layers/dms-layout/app/plugins/notification-stream.client"
81
+ );
82
+
83
+ const mounted: App[] = [];
84
+ let bell: ReturnType<typeof useNotifications>;
85
+
86
+ /** A tab: the bell's state and the stream plugin, on one runtime. */
87
+ function openTab(): HTMLElement {
88
+ const Tab = defineComponent({
89
+ setup() {
90
+ bell = useNotifications();
91
+ (notificationStream as unknown as () => void)();
92
+ return () =>
93
+ h("span", `${bell.unreadCount.value}:${bell.unreadTone.value ?? "-"}`);
94
+ },
95
+ });
96
+ const { app, container } = mountInBrowser(Tab);
97
+ mounted.push(app);
98
+ return container;
99
+ }
100
+
101
+ /** The server's event for a read state change, with the counts after it. */
102
+ const echo = (type: string, ids: string[]): StreamEvent => ({
103
+ type,
104
+ payload: { ids, counts: countsOf() },
105
+ });
106
+
107
+ async function settle(): Promise<void> {
108
+ await nextTick();
109
+ await new Promise((done) => setTimeout(done, 0));
110
+ }
111
+
112
+ beforeEach(async () => {
113
+ feed.splice(
114
+ 0,
115
+ feed.length,
116
+ { _id: "risk", isRead: false, tone: "error" },
117
+ { _id: "check", isRead: false, tone: "warning" },
118
+ { _id: "old", isRead: true, tone: "neutral" },
119
+ );
120
+ requests.length = 0;
121
+ holdWrites = undefined;
122
+ failWrites = false;
123
+ const tab = openTab();
124
+ await bell.fetchBellCounts();
125
+ await settle();
126
+ expect(tab.textContent).toBe("2:error");
127
+ requests.length = 0;
128
+ });
129
+
130
+ afterEach(() => {
131
+ mounted.splice(0).forEach((app) => app.unmount());
132
+ });
133
+
134
+ it("takes the counts a write answers, and reads nothing for its echo after the answer", async () => {
135
+ await bell.markAsRead("risk");
136
+ await settle();
137
+ expect(bell.unreadCount.value).toBe(1);
138
+ // The next most important unread one gives the tone: only the server knows it.
139
+ expect(bell.unreadTone.value).toBe("warning");
140
+
141
+ streamHandler!(echo("notification:read", ["risk"]));
142
+ await settle();
143
+ expect(requests).toEqual([
144
+ "PUT /dms/pages/settings/user/notifications/mark-read/risk",
145
+ ]);
146
+ expect(bell.unreadCount.value).toBe(1);
147
+
148
+ await bell.markAsUnread("risk");
149
+ await bell.deleteNotification("check");
150
+ await settle();
151
+ expect(requests).toEqual([
152
+ "PUT /dms/pages/settings/user/notifications/mark-read/risk",
153
+ "PUT /dms/pages/settings/user/notifications/mark-unread/risk",
154
+ "DELETE /dms/pages/settings/user/notifications/delete/check",
155
+ ]);
156
+ expect([bell.unreadCount.value, bell.unreadTone.value]).toEqual([1, "error"]);
157
+ });
158
+
159
+ it("reads nothing when the write's echo comes before its answer", async () => {
160
+ let release!: () => void;
161
+ holdWrites = new Promise((done) => {
162
+ release = done;
163
+ });
164
+ const writing = bell.markAsRead("check");
165
+ await settle();
166
+ streamHandler!(echo("notification:read", ["check"]));
167
+ await settle();
168
+ expect([bell.unreadCount.value, bell.unreadTone.value]).toEqual([1, "error"]);
169
+ release();
170
+ await writing;
171
+ await settle();
172
+ expect(requests).toEqual([
173
+ "PUT /dms/pages/settings/user/notifications/mark-read/check",
174
+ ]);
175
+ expect(bell.unreadCount.value).toBe(1);
176
+ });
177
+
178
+ it("takes another tab's changes from their events, without reading", async () => {
179
+ feed[0]!.isRead = true;
180
+ streamHandler!(echo("notification:read", ["risk"]));
181
+ await settle();
182
+ expect([bell.unreadCount.value, bell.unreadTone.value]).toEqual([
183
+ 1,
184
+ "warning",
185
+ ]);
186
+
187
+ feed[0]!.isRead = false;
188
+ streamHandler!(echo("notification:unread", ["risk"]));
189
+ await settle();
190
+ expect(bell.unreadTone.value).toBe("error");
191
+
192
+ for (const row of feed) row.isRead = true;
193
+ streamHandler!({
194
+ type: "notification:all-read",
195
+ payload: { counts: countsOf() },
196
+ });
197
+ await settle();
198
+ expect([bell.unreadCount.value, bell.unreadTone.value]).toEqual([
199
+ 0,
200
+ undefined,
201
+ ]);
202
+ expect(requests).toEqual([]);
203
+ });
204
+
205
+ it("reads the counts when an event carries none", async () => {
206
+ feed[0]!.isRead = true;
207
+ streamHandler!({ type: "notification:read", payload: { ids: ["risk"] } });
208
+ await settle();
209
+ expect(requests).toEqual([
210
+ "GET /dms/pages/settings/user/notifications/counts",
211
+ ]);
212
+ expect(bell.unreadCount.value).toBe(1);
213
+ });
214
+
215
+ it("keeps the counts when a write fails", async () => {
216
+ failWrites = true;
217
+ await expect(bell.markAsRead("risk")).rejects.toThrow("refused");
218
+ await settle();
219
+ expect(requests).toEqual([
220
+ "PUT /dms/pages/settings/user/notifications/mark-read/risk",
221
+ ]);
222
+ expect([bell.unreadCount.value, bell.unreadTone.value]).toEqual([2, "error"]);
223
+ });
@@ -0,0 +1,88 @@
1
+ // @vitest-environment jsdom
2
+ /**
3
+ * The header bell's counts as the dashboard frame reads them with the page,
4
+ * through the runtime's real async data: a server render shows the badge,
5
+ * and the browser's bell, and whatever else shows the counts, start from the
6
+ * same counts without asking again. A signed-out visit reads nothing.
7
+ */
8
+ import { defineComponent, h, nextTick, ref, type App } from "vue";
9
+ import { afterEach, beforeEach, expect, it, vi } from "vitest";
10
+ import {
11
+ hydrate,
12
+ mountInBrowser,
13
+ serverRender,
14
+ stubApiData,
15
+ } from "./helpers/api-data-runtime";
16
+
17
+ const COUNTS = { all: 5, unread: 3, unreadTone: "warning" };
18
+ const authFetch = vi.fn(async () => structuredClone(COUNTS));
19
+ const loggedIn = ref(true);
20
+ const mounted: App[] = [];
21
+
22
+ stubApiData(authFetch as never);
23
+ vi.stubGlobal("useDmsUserSession", () => ({ loggedIn }));
24
+ vi.stubGlobal("useComponentEvent", () => ({ sendComponentEvent: vi.fn() }));
25
+ const { useNotifications, useRenderedBellCounts } = await import(
26
+ "../layers/dms-layout/app/composables/notification/useNotifications"
27
+ );
28
+ vi.stubGlobal("useNotifications", useNotifications);
29
+
30
+ /** The bell's badge, or the profile's summary: the shared counts. */
31
+ const Counts = defineComponent({
32
+ setup() {
33
+ const { unreadCount, unreadTone, areCountsLoaded } = useNotifications();
34
+ return () =>
35
+ h(
36
+ "span",
37
+ areCountsLoaded.value
38
+ ? `${unreadCount.value}:${unreadTone.value}`
39
+ : "unknown",
40
+ );
41
+ },
42
+ });
43
+ // As the dashboard frame: in the persistent layout, outside the page's
44
+ // <Suspense>, around the header and the page.
45
+ const Frame = defineComponent({
46
+ setup() {
47
+ useRenderedBellCounts();
48
+ return () => h("div", [h(Counts), h(Counts)]);
49
+ },
50
+ });
51
+
52
+ beforeEach(() => {
53
+ loggedIn.value = true;
54
+ authFetch.mockClear();
55
+ });
56
+
57
+ afterEach(() => {
58
+ mounted.splice(0).forEach((app) => app.unmount());
59
+ vi.unstubAllEnvs();
60
+ });
61
+
62
+ it("renders the bell's counts on the server, and the browser starts from them without asking", async () => {
63
+ vi.stubEnv("SSR", true);
64
+ const { html, payload } = await serverRender(Frame);
65
+ vi.stubEnv("SSR", false);
66
+ expect(html).toBe("<div><span>3:warning</span><span>3:warning</span></div>");
67
+ expect(authFetch).toHaveBeenCalledTimes(1);
68
+
69
+ authFetch.mockClear();
70
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
71
+ const { app, container } = hydrate(Frame, html, payload);
72
+ mounted.push(app);
73
+ await nextTick();
74
+ await new Promise((done) => setTimeout(done, 0));
75
+ expect(authFetch).not.toHaveBeenCalled();
76
+ expect(container.innerHTML).toBe(html);
77
+ expect(warn.mock.calls.flat().join(" ")).not.toMatch(/hydration/i);
78
+ warn.mockRestore();
79
+ });
80
+
81
+ it("reads nothing for a signed-out visit", async () => {
82
+ loggedIn.value = false;
83
+ const { app, container } = mountInBrowser(Frame);
84
+ mounted.push(app);
85
+ await nextTick();
86
+ expect(authFetch).not.toHaveBeenCalled();
87
+ expect(container.textContent).toBe("unknownunknown");
88
+ });
@@ -4,11 +4,13 @@ import {
4
4
  createApp,
5
5
  defineComponent,
6
6
  h,
7
+ nextTick,
7
8
  onMounted,
8
9
  ref,
9
10
  watch,
10
11
  type App,
11
12
  type Component,
13
+ type Ref,
12
14
  } from "vue";
13
15
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
14
16
  import NotificationInboxDisplay from "../layers/dms-layout/app/build/components/pages/settings/notification/NotificationInboxDisplay.vue";
@@ -19,16 +21,20 @@ const notifications = vi.hoisted(() => ({
19
21
  deleteNotification: vi.fn(async () => {}),
20
22
  }));
21
23
 
24
+ // The bell's state the display watches, shared like `useDmsState`.
25
+ const bell = vi.hoisted(() => ({
26
+ unreadCount: undefined as unknown as Ref<number>,
27
+ notifications: undefined as unknown as Ref<{ _id: string }[]>,
28
+ }));
29
+
22
30
  vi.mock(
23
31
  "../layers/dms-layout/app/composables/notification/useNotifications",
24
32
  async () => {
25
33
  const vue = await import("vue");
34
+ bell.unreadCount = vue.ref(0);
35
+ bell.notifications = vue.ref([]);
26
36
  return {
27
- useNotifications: () => ({
28
- ...notifications,
29
- unreadCount: vue.ref(0),
30
- notifications: vue.ref([]),
31
- }),
37
+ useNotifications: () => ({ ...notifications, ...bell }),
32
38
  };
33
39
  },
34
40
  );
@@ -113,6 +119,8 @@ function mount(component: Component, props: Record<string, unknown>) {
113
119
  }
114
120
 
115
121
  beforeEach(() => {
122
+ bell.unreadCount.value = 0;
123
+ bell.notifications.value = [];
116
124
  vi.stubGlobal("computed", computed);
117
125
  vi.stubGlobal("watch", watch);
118
126
  vi.stubGlobal("onMounted", onMounted);
@@ -166,4 +174,48 @@ describe("notifications inbox display", () => {
166
174
  expect(notifications.markAsUnread).toHaveBeenCalledWith("n2");
167
175
  expect(notifications.deleteNotification).toHaveBeenCalledWith("n2");
168
176
  });
177
+
178
+ it("lists the feed again once per change made here, though the change moves the counts", async () => {
179
+ const refresh = vi.fn(async () => {});
180
+ const container = mount(NotificationInboxDisplay as Component, {
181
+ context: contextOf(refresh),
182
+ });
183
+ // As the bell does: the write's echo, then its answer, move the count and
184
+ // rebuild the bell's lists.
185
+ notifications.markAsRead.mockImplementationOnce(async () => {
186
+ bell.unreadCount.value = 4;
187
+ bell.notifications.value = [{ _id: "n1" }];
188
+ await new Promise((done) => setTimeout(done, 0));
189
+ bell.unreadCount.value = 3;
190
+ bell.notifications.value = [{ _id: "n1" }];
191
+ });
192
+ container.querySelector<HTMLButtonElement>("article .toggle")!.click();
193
+ await vi.waitFor(() => expect(refresh).toHaveBeenCalledTimes(1));
194
+ await new Promise((done) => setTimeout(done, 10));
195
+ expect(refresh).toHaveBeenCalledTimes(1);
196
+ });
197
+
198
+ it("lists the feed again once per change the stream brings, not when the bell rebuilds its lists", async () => {
199
+ const refresh = vi.fn(async () => {});
200
+ mount(NotificationInboxDisplay as Component, {
201
+ context: contextOf(refresh),
202
+ });
203
+ bell.notifications.value = [{ _id: "n1" }, { _id: "n2" }];
204
+ await nextTick();
205
+ refresh.mockClear();
206
+
207
+ // A read state changing elsewhere rebuilds the lists, then the count moves.
208
+ bell.notifications.value = [{ _id: "n1" }, { _id: "n2" }];
209
+ await nextTick();
210
+ expect(refresh).not.toHaveBeenCalled();
211
+ bell.unreadCount.value += 1;
212
+ await nextTick();
213
+ expect(refresh).toHaveBeenCalledTimes(1);
214
+
215
+ // A notification arriving: listed first and counted, together.
216
+ bell.notifications.value = [{ _id: "n0" }, ...bell.notifications.value];
217
+ bell.unreadCount.value += 1;
218
+ await nextTick();
219
+ expect(refresh).toHaveBeenCalledTimes(2);
220
+ });
169
221
  });
@@ -22,6 +22,7 @@ const NOTIFICATIONS_PAGE = "settings.user.notifications";
22
22
  interface NotificationsStub {
23
23
  unreadCount: Ref<number>;
24
24
  unreadTone: Ref<string | undefined>;
25
+ areCountsLoaded: Ref<boolean>;
25
26
  notifications: Ref<unknown[]>;
26
27
  fetchBellCounts: ReturnType<typeof vi.fn>;
27
28
  fetchNotifications: ReturnType<typeof vi.fn>;
@@ -105,7 +106,7 @@ function installRuntime(): void {
105
106
  return state.get(key);
106
107
  });
107
108
  vi.stubGlobal("useI18n", () => ({ t: (key: string) => key, locale: "en" }));
108
- vi.stubGlobal("useUserSession", () => ({ loggedIn: ref(true) }));
109
+ vi.stubGlobal("useDmsUserSession", () => ({ loggedIn: ref(true) }));
109
110
  vi.stubGlobal("useTranslation", () => ({
110
111
  processI18n: (value: string) => value,
111
112
  }));
@@ -175,6 +176,9 @@ beforeEach(() => {
175
176
  notifications = {
176
177
  unreadCount: ref(0),
177
178
  unreadTone: ref(undefined),
179
+ // Not read with the page (signed in since the app started): the bell
180
+ // reads them once mounted.
181
+ areCountsLoaded: ref(false),
178
182
  notifications: ref([]),
179
183
  fetchBellCounts: vi.fn(),
180
184
  fetchNotifications: vi.fn(),
@@ -298,3 +302,19 @@ it("keeps the page alive when the notification list is refused on open", async (
298
302
  expect(errorHandler).not.toHaveBeenCalled();
299
303
  expect(host.textContent).toContain("notification.dropdown.no_notifications");
300
304
  });
305
+
306
+ it("does not read the counts again when the page brought them", async () => {
307
+ notifications.areCountsLoaded.value = true;
308
+ notifications.unreadCount.value = 2;
309
+ app = createApp(NotificationPopover);
310
+ app.config.globalProperties.$t = (key: string) => key;
311
+ for (const name of ["UButton", "USeparator", "UIcon"])
312
+ app.component(name, Passthrough);
313
+ app.component("UPopover", Popover);
314
+ app.component("UCard", Card);
315
+ app.mount(host);
316
+ await nextTick();
317
+ await new Promise((done) => setTimeout(done, 0));
318
+ expect(notifications.fetchBellCounts).not.toHaveBeenCalled();
319
+ expect(host.textContent).toContain("2");
320
+ });
@@ -0,0 +1,209 @@
1
+ // @vitest-environment jsdom
2
+ import {
3
+ computed,
4
+ createApp,
5
+ defineComponent,
6
+ h,
7
+ nextTick,
8
+ onMounted,
9
+ reactive,
10
+ ref,
11
+ Suspense,
12
+ watch,
13
+ type App,
14
+ type Ref,
15
+ } from "vue";
16
+ import { afterEach, beforeEach, expect, it, vi } from "vitest";
17
+ import { usePostLoginRedirect } from "../layers/dms-auth/app/composables/usePostLoginRedirect";
18
+ import { usePageLayout } from "../layers/dms-layout/app/composables/page/usePageLayout";
19
+ import { useSiteLayout } from "../layers/dms-layout/app/composables/page/useSiteLayout";
20
+ import type { PageLayout } from "../layers/dms-layout/app/types/page";
21
+
22
+ // The OAuth completion page, drawn by the real page layout cache: the
23
+ // post-login redirect refreshes the site layout for the new identity, which
24
+ // empties that cache and remounts the page while its layout comes back.
25
+
26
+ const { default: OAuthCompletePage } = await import(
27
+ "../layers/dms-auth/app/custom-pages/auth/oauth-complete.vue"
28
+ );
29
+
30
+ const PATH = "/auth/oauth/complete";
31
+ const LAYOUT_URL = `${PATH}/pagelayout`;
32
+ const SITE_LAYOUT_URL = "/dms/sitelayout";
33
+ const HANDOFF_URL = "/auth/oauth/handoff";
34
+ const HOMEPAGE = "/home";
35
+
36
+ const state = new Map<string, Ref>();
37
+ const authFetch = vi.fn();
38
+ const fetchHandoff = vi.fn();
39
+ const navigate = vi.fn();
40
+ let mounts = 0;
41
+ let route: { path: string; query: Record<string, string> };
42
+ let app: App;
43
+ let host: HTMLDivElement;
44
+
45
+ const completionLayout: PageLayout = {
46
+ components: { complete: { options: {} } },
47
+ layout: {},
48
+ } as unknown as PageLayout;
49
+
50
+ const siteResponse = {
51
+ siteLayout: {
52
+ pages: {
53
+ [PATH]: {
54
+ id: "auth-oauth-complete",
55
+ fullId: "auth-oauth-complete",
56
+ fullSlug: PATH,
57
+ displayName: "Completing",
58
+ layoutUrl: LAYOUT_URL,
59
+ publicAccess: true,
60
+ },
61
+ },
62
+ categories: {},
63
+ },
64
+ siteLayoutTree: undefined,
65
+ quickActions: undefined,
66
+ modules: {},
67
+ isOwner: false,
68
+ };
69
+
70
+ const Page = defineComponent({
71
+ async setup() {
72
+ const { pageLayout } = await usePageLayout();
73
+ return () =>
74
+ h(
75
+ "main",
76
+ Object.keys(pageLayout.value?.components ?? {}).map((key) =>
77
+ h(OAuthCompletePage, { key }),
78
+ ),
79
+ );
80
+ },
81
+ });
82
+
83
+ function installRuntime() {
84
+ const runtime = {
85
+ runWithContext: (fn: () => unknown) => fn(),
86
+ $i18n: { setLocale: vi.fn() },
87
+ };
88
+ Object.entries({ computed, ref, watch }).forEach(([key, value]) =>
89
+ vi.stubGlobal(key, value),
90
+ );
91
+ vi.stubGlobal("onMounted", (hook: () => unknown) =>
92
+ onMounted(() => {
93
+ mounts += 1;
94
+ return hook();
95
+ }),
96
+ );
97
+ vi.stubGlobal("useDmsApp", () => runtime);
98
+ vi.stubGlobal("useDmsRoute", () => route);
99
+ vi.stubGlobal("useDmsState", (key: string, initial: () => unknown) => {
100
+ if (!state.has(key)) state.set(key, ref(initial()));
101
+ return state.get(key);
102
+ });
103
+ vi.stubGlobal("useAuthFetch", () => ({ $authFetch: authFetch }));
104
+ vi.stubGlobal("useSiteLayout", useSiteLayout);
105
+ vi.stubGlobal("useHomepage", () => HOMEPAGE);
106
+ vi.stubGlobal("useDmsUserSession", () => ({
107
+ fetch: async () => undefined,
108
+ user: ref({ _id: "user" }),
109
+ }));
110
+ vi.stubGlobal("useMultiAccount", () => ({ addCurrentAccount: vi.fn() }));
111
+ vi.stubGlobal("usePostLoginRedirect", usePostLoginRedirect);
112
+ vi.stubGlobal("navigateDms", navigate);
113
+ vi.stubGlobal("$fetch", (url: string, options: unknown) =>
114
+ url === HANDOFF_URL ? fetchHandoff(options) : Promise.reject(url),
115
+ );
116
+ }
117
+
118
+ function mountCompletionPage() {
119
+ route.path = PATH;
120
+ const layout = useSiteLayout();
121
+ layout.siteLayout.value = siteResponse.siteLayout as never;
122
+ layout.pageLayouts.value = { [LAYOUT_URL]: completionLayout };
123
+ app = createApp({
124
+ render: () => h(Suspense, null, { default: () => h(Page) }),
125
+ });
126
+ app.config.globalProperties.$t = (key: string) => key;
127
+ app.component("UIcon", defineComponent({ setup: () => () => h("i") }));
128
+ app.mount(host);
129
+ }
130
+
131
+ const siteLayoutFetches = () =>
132
+ authFetch.mock.calls.filter(([url]) => url === SITE_LAYOUT_URL).length;
133
+
134
+ async function settle() {
135
+ for (let tick = 0; tick < 20; tick++) {
136
+ await Promise.resolve();
137
+ await nextTick();
138
+ }
139
+ }
140
+
141
+ beforeEach(() => {
142
+ state.clear();
143
+ mounts = 0;
144
+ // Answered on later tasks, like the network, the page layout first as on
145
+ // the playground: the page is drawn again before the redirect leaves it.
146
+ authFetch.mockImplementation(async (url: string) => {
147
+ const site = url === SITE_LAYOUT_URL;
148
+ await new Promise((resolve) => setTimeout(resolve, site ? 5 : 0));
149
+ return site ? siteResponse : completionLayout;
150
+ });
151
+ fetchHandoff.mockResolvedValue({ kind: "none" });
152
+ route = reactive({ path: PATH, query: {} });
153
+ // Leaving the page unmounts it, as the router does: the completion page
154
+ // has no layout on any other route.
155
+ navigate.mockImplementation(async (to: string | { path: string }) => {
156
+ route.path = typeof to === "string" ? to : to.path;
157
+ });
158
+ installRuntime();
159
+ host = document.createElement("div");
160
+ document.body.append(host);
161
+ });
162
+
163
+ afterEach(() => {
164
+ app?.unmount();
165
+ host.remove();
166
+ vi.unstubAllGlobals();
167
+ vi.resetAllMocks();
168
+ });
169
+
170
+ it("consumes the handoff once when the identity refresh remounts the page", async () => {
171
+ mountCompletionPage();
172
+ await vi.waitFor(() =>
173
+ expect(navigate).toHaveBeenCalledWith(HOMEPAGE, { replace: true }),
174
+ );
175
+ await settle();
176
+
177
+ expect(fetchHandoff).toHaveBeenCalledOnce();
178
+ expect(siteLayoutFetches()).toBe(1);
179
+ expect(navigate).toHaveBeenCalledOnce();
180
+ // The refresh did empty the cache and remount the page: the new instance
181
+ // joined the completion already running.
182
+ expect(mounts).toBe(2);
183
+ });
184
+
185
+ it("redirects to the sign-in page once when the handoff fails", async () => {
186
+ fetchHandoff.mockRejectedValue(new Error("offline"));
187
+ mountCompletionPage();
188
+ await vi.waitFor(() => expect(navigate).toHaveBeenCalled());
189
+ await settle();
190
+
191
+ expect(fetchHandoff).toHaveBeenCalledOnce();
192
+ expect(navigate).toHaveBeenCalledExactlyOnceWith(
193
+ { path: "/auth", query: { oauth_error: "error.oauth.failed" } },
194
+ { replace: true },
195
+ );
196
+ });
197
+
198
+ it("completes again on a later visit, once the previous one settled", async () => {
199
+ mountCompletionPage();
200
+ await vi.waitFor(() => expect(navigate).toHaveBeenCalledOnce());
201
+ await settle();
202
+ app.unmount();
203
+
204
+ mountCompletionPage();
205
+ await vi.waitFor(() => expect(navigate).toHaveBeenCalledTimes(2));
206
+ await settle();
207
+
208
+ expect(fetchHandoff).toHaveBeenCalledTimes(2);
209
+ });
@@ -12,7 +12,7 @@ beforeEach(() => {
12
12
  vi.stubGlobal("defineDmsMiddleware", (handler: Middleware) => handler);
13
13
  vi.stubGlobal("navigateDms", navigateDms);
14
14
  vi.stubGlobal("useOnboarding", onboarding);
15
- vi.stubGlobal("useUserSession", () => session);
15
+ vi.stubGlobal("useDmsUserSession", () => session);
16
16
  vi.stubGlobal("useHomepage", () => "/dashboard");
17
17
  });
18
18