@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
@@ -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
 
@@ -0,0 +1,106 @@
1
+ // @vitest-environment jsdom
2
+ /**
3
+ * The onboarding status the global middleware reads 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 asks nothing.
6
+ */
7
+ import { createApp, createSSRApp, defineComponent, h, nextTick } from "vue";
8
+ import { renderToString } from "vue/server-renderer";
9
+ import { afterEach, beforeEach, expect, it, vi } from "vitest";
10
+ import type { App } from "vue";
11
+ import type { DmsFrontendRuntime } from "#dms/frontend-module";
12
+
13
+ const engine = await import("#dms/frontend-module");
14
+ vi.stubGlobal("useDmsState", engine.useDmsState);
15
+ vi.stubGlobal("defineDmsMiddleware", engine.defineDmsMiddleware);
16
+ vi.stubGlobal("navigateDms", engine.navigateDms);
17
+ vi.stubGlobal("useDmsUserSession", () => ({ loggedIn: { value: true } }));
18
+ vi.stubGlobal("useHomepage", () => "/dashboard");
19
+
20
+ const $authFetch = vi.fn(async () => ({ has_onboarded: true }));
21
+ vi.stubGlobal("useAuthFetch", () => ({ $authFetch }));
22
+ // The status is read once per app however many callers ask (the preload
23
+ // started with the app, the middleware).
24
+ // No DMS app runs here: each runtime holds the deduper on a state of its own.
25
+ vi.stubGlobal(
26
+ "useDmsApp",
27
+ () => engine.useDmsState("onboarding-test-app", () => ({})).value,
28
+ );
29
+ const { createTrailingDeduper } = await import(
30
+ "../layers/dms-core/app/utils/trailingDeduper"
31
+ );
32
+ vi.stubGlobal("createTrailingDeduper", createTrailingDeduper);
33
+ const { useTrailingDeduper } = await import(
34
+ "../layers/dms-core/app/composables/useTrailingDeduper"
35
+ );
36
+ vi.stubGlobal("useTrailingDeduper", useTrailingDeduper);
37
+
38
+ const { useOnboarding } = await import(
39
+ "../layers/dms-onboarding/app/composables/onboarding/useOnboarding"
40
+ );
41
+ vi.stubGlobal("useOnboarding", useOnboarding);
42
+ const { default: onboardingMiddleware } = await import(
43
+ "../layers/dms-onboarding/app/middleware/onboarding.global"
44
+ );
45
+
46
+ const Page = defineComponent({
47
+ setup() {
48
+ engine.defineDmsPageMeta({});
49
+ const info = engine.useDmsState<{ hasOnboarded: boolean } | null>(
50
+ "dms-onboarding-info",
51
+ );
52
+ return () => h("p", info.value?.hasOnboarded ? "onboarded" : "unknown");
53
+ },
54
+ });
55
+
56
+ const mounted: App[] = [];
57
+
58
+ function attach(app: App, runtime: DmsFrontendRuntime): void {
59
+ runtime.route.path = "/dashboard";
60
+ runtime.route.fullPath = "/dashboard";
61
+ engine.provideDmsFrontendRuntime(app, runtime);
62
+ }
63
+
64
+ beforeEach(async () => {
65
+ $authFetch.mockClear();
66
+ await engine.setupFrontendModules([
67
+ {
68
+ options: { public: {} },
69
+ module: {
70
+ setup(sdk) {
71
+ sdk.registerMiddleware("onboarding", onboardingMiddleware, {
72
+ global: true,
73
+ });
74
+ },
75
+ },
76
+ },
77
+ ]);
78
+ });
79
+
80
+ afterEach(() => {
81
+ mounted.splice(0).forEach((app) => app.unmount());
82
+ });
83
+
84
+ it("hands the onboarding status the server read to the browser", async () => {
85
+ const server = engine.createDmsFrontendRuntime(undefined, {}, true);
86
+ const serverApp = createSSRApp(Page);
87
+ attach(serverApp, server);
88
+ expect(await renderToString(serverApp)).toBe("<p>onboarded</p>");
89
+ expect($authFetch).toHaveBeenCalledTimes(1);
90
+ const payload = structuredClone(engine.serializeDmsPayload(server));
91
+ expect(payload.state["dms-onboarding-info"]).toEqual({ hasOnboarded: true });
92
+
93
+ $authFetch.mockClear();
94
+ const browser = engine.createDmsFrontendRuntime(undefined, {});
95
+ engine.restoreDmsPayload(browser, payload);
96
+ const browserApp = createApp(Page);
97
+ attach(browserApp, browser);
98
+ const host = document.createElement("div");
99
+ browserApp.mount(host);
100
+ mounted.push(browserApp);
101
+ await nextTick();
102
+ await new Promise((done) => setTimeout(done, 0));
103
+
104
+ expect($authFetch).not.toHaveBeenCalled();
105
+ expect(host.textContent).toBe("onboarded");
106
+ });
@@ -0,0 +1,141 @@
1
+ // @vitest-environment jsdom
2
+ /**
3
+ * A page's `useApiData` reads follow the page: `refreshPageBlocks()` (an
4
+ * action of the user) and an event on one of the page's realtime topics read
5
+ * them again through `refreshDmsPageData()`, without a `refresh()` of their
6
+ * own. A read that refreshes on its own triggers opts out.
7
+ */
8
+ import { type App, createApp, defineComponent, h, nextTick, ref } from "vue";
9
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
10
+ import type { RealtimeTopicHandler } from "../layers/dms-core/app/composables/realtime/useUserRealtime";
11
+ import { useRealDataEngine } from "./fixtures/dataEngine";
12
+ import { refreshPageBlocks } from "../layers/dms-ui/app/utils/blockRefresh";
13
+
14
+ const requests: string[] = [];
15
+ // Holds the answers back while set.
16
+ let held: Promise<void> | undefined;
17
+ vi.stubGlobal("useAuthFetch", () => ({
18
+ $authFetch: async (url: string) => {
19
+ requests.push(url);
20
+ await held;
21
+ return { url };
22
+ },
23
+ }));
24
+
25
+ const { useApiData } = await import(
26
+ "../layers/dms-core/app/composables/auth/useApiData"
27
+ );
28
+ const { usePageDataRefresh } = await import(
29
+ "../layers/dms-ui/app/build/composables/live/usePageDataRefresh"
30
+ );
31
+
32
+ // Past the one-frame window a burst of refreshes collapses into.
33
+ const settle = () => new Promise((resolve) => setTimeout(resolve, 40));
34
+
35
+ const pageTopics = ref<string[]>([]);
36
+ const handlers = new Map<string, Set<RealtimeTopicHandler>>();
37
+ function subscribe(topic: string, handler: RealtimeTopicHandler) {
38
+ if (!handlers.has(topic)) handlers.set(topic, new Set());
39
+ handlers.get(topic)!.add(handler);
40
+ return () => handlers.get(topic)?.delete(handler);
41
+ }
42
+ function publish(topic: string, event: object): void {
43
+ for (const handler of handlers.get(topic) ?? [])
44
+ handler(event as Parameters<RealtimeTopicHandler>[0]);
45
+ }
46
+
47
+ const mounted: App[] = [];
48
+
49
+ async function mountPage(): Promise<void> {
50
+ const Page = defineComponent({
51
+ setup() {
52
+ usePageDataRefresh({ pageTopics, subscribe });
53
+ useApiData("/api/orders");
54
+ useApiData("/api/own-triggers", { refreshOnPageRefresh: false });
55
+ return () => h("div");
56
+ },
57
+ });
58
+ const app = createApp(Page);
59
+ app.mount(document.createElement("div"));
60
+ mounted.push(app);
61
+ await settle();
62
+ }
63
+
64
+ beforeEach(() => {
65
+ useRealDataEngine();
66
+ requests.length = 0;
67
+ pageTopics.value = [];
68
+ handlers.clear();
69
+ });
70
+
71
+ afterEach(() => {
72
+ mounted.splice(0).forEach((app) => app.unmount());
73
+ });
74
+
75
+ describe("the page's data refresh", () => {
76
+ it("reads the page's data again on refreshPageBlocks()", async () => {
77
+ await mountPage();
78
+ expect(requests.sort()).toEqual(["/api/orders", "/api/own-triggers"]);
79
+ requests.length = 0;
80
+
81
+ refreshPageBlocks();
82
+ await settle();
83
+
84
+ expect(requests).toEqual(["/api/orders"]);
85
+ });
86
+
87
+ it("resolves refreshPageBlocks() once the page's data is read again", async () => {
88
+ await mountPage();
89
+ requests.length = 0;
90
+ let release!: () => void;
91
+ held = new Promise<void>((resolve) => (release = resolve));
92
+ let done = false;
93
+ const refreshed = refreshPageBlocks().then(() => (done = true));
94
+ await settle();
95
+ expect(requests).toEqual(["/api/orders"]);
96
+ expect(done).toBe(false);
97
+
98
+ release();
99
+ held = undefined;
100
+ await refreshed;
101
+ expect(done).toBe(true);
102
+ });
103
+
104
+ it("joins the refreshes of one burst", async () => {
105
+ await mountPage();
106
+ requests.length = 0;
107
+ await Promise.all([refreshPageBlocks(), refreshPageBlocks()]);
108
+ expect(requests).toEqual(["/api/orders"]);
109
+ });
110
+
111
+ it("reads it again on an event of one of the page's topics", async () => {
112
+ await mountPage();
113
+ pageTopics.value = ["table:orders"];
114
+ await nextTick();
115
+ requests.length = 0;
116
+
117
+ publish("table:orders", { topic: "table:orders", type: "updated" });
118
+ publish("table:orders", { topic: "table:orders", type: "updated" });
119
+ await settle();
120
+ expect(requests).toEqual(["/api/orders"]);
121
+
122
+ // A presence snapshot rides the topic too, and changes no data.
123
+ requests.length = 0;
124
+ publish("table:orders", { topic: "table:orders", entries: [] });
125
+ await settle();
126
+ expect(requests).toEqual([]);
127
+ });
128
+
129
+ it("stops following the topics the page no longer has", async () => {
130
+ await mountPage();
131
+ pageTopics.value = ["table:orders"];
132
+ await nextTick();
133
+ pageTopics.value = ["table:invoices"];
134
+ await nextTick();
135
+ requests.length = 0;
136
+
137
+ publish("table:orders", { topic: "table:orders", type: "updated" });
138
+ await settle();
139
+ expect(requests).toEqual([]);
140
+ });
141
+ });
@@ -28,6 +28,16 @@ import {
28
28
  usePageFillHeight,
29
29
  } from "../layers/dms-layout/app/composables/layout/usePageFillHeight";
30
30
 
31
+ // The page's data refresh and the visit's navigation badges read the
32
+ // realtime session and Inertia's page, which these tests do not run.
33
+ vi.mock(
34
+ "../layers/dms-ui/app/build/composables/live/usePageDataRefresh",
35
+ () => ({ usePageDataRefresh: () => {} }),
36
+ );
37
+ vi.mock(
38
+ "../layers/dms-layout/app/build/composables/navigation/useVisitNavBadges",
39
+ () => ({ useVisitNavBadges: () => {} }),
40
+ );
31
41
  vi.mock(
32
42
  "../layers/dms-layout/app/build/components/layout/DashboardSidebar.vue",
33
43
  () => ({ default: () => null }),
@@ -151,7 +161,7 @@ function installRuntime(): void {
151
161
  PAGE_FILL_HEIGHT_CLASSES,
152
162
  providePageFillHeight,
153
163
  usePageFillHeight,
154
- createError: vi.fn(),
164
+ createDmsError: vi.fn(),
155
165
  defineDmsPageMeta: vi.fn(),
156
166
  firstAccessiblePagePath: vi.fn(),
157
167
  navigateDms: vi.fn(),
@@ -159,7 +169,7 @@ function installRuntime(): void {
159
169
  providePageRealtime: vi.fn(),
160
170
  resolveDmsComponent: () => "div",
161
171
  runCustomValidation: vi.fn(),
162
- showError: vi.fn(),
172
+ showDmsError: vi.fn(),
163
173
  validateRequiredQueryParams: vi.fn(),
164
174
  useDefinedFunctions: () => ({ getFunction: () => undefined }),
165
175
  useDmsCookie: <T>(_name: string, options: { default?: () => T } = {}) =>
@@ -179,6 +189,9 @@ function installRuntime(): void {
179
189
  reconcileSession: async () => {},
180
190
  redirectToAuth: async () => {},
181
191
  }),
192
+ // The page brought its site layout.
193
+ useRenderedSiteLayout: () => {},
194
+ useRenderedBellCounts: () => {},
182
195
  useSiteLayout: () => ({
183
196
  siteLayout: ref({ pages: {} }),
184
197
  siteLayoutTree: ref([]),
@@ -197,7 +210,7 @@ function installRuntime(): void {
197
210
  }),
198
211
  useI18n: () => ({ t: (key: string) => key }),
199
212
  useTranslation: () => ({ processI18n: (value: string) => value }),
200
- useUserSession: () => ({ loggedIn: ref(true) }),
213
+ useDmsUserSession: () => ({ loggedIn: ref(true) }),
201
214
  };
202
215
  Object.entries(runtime).forEach(([name, value]) =>
203
216
  vi.stubGlobal(name, value),