@antelopejs/dms 0.8.3 → 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 (351) hide show
  1. package/CHANGELOG.md +20 -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/pages/settings/member-roles-field.test.js +1 -1
  118. package/dist/test/unit/pages/settings/member-roles-field.test.js.map +1 -1
  119. package/dist/test/unit/pages/settings/role-delete-confirm.test.js.map +1 -1
  120. package/dist/test/unit/realtime/menu-topics.test.d.ts +1 -0
  121. package/dist/test/unit/realtime/menu-topics.test.js +26 -0
  122. package/dist/test/unit/realtime/menu-topics.test.js.map +1 -0
  123. package/frontend-vue/deferred-plugins.ts +3 -4
  124. package/frontend-vue/dms.email.ts +5 -1
  125. package/frontend-vue/dms.frontend.ts +15 -5
  126. package/frontend-vue/dms.server.ts +74 -0
  127. package/frontend-vue/layers/dms-auth/app/composables/useLogout.ts +1 -1
  128. package/frontend-vue/layers/dms-auth/app/composables/usePostLoginRedirect.ts +16 -3
  129. package/frontend-vue/layers/dms-auth/app/custom-pages/auth/2fa.vue +3 -1
  130. package/frontend-vue/layers/dms-auth/app/custom-pages/auth/backup.vue +3 -1
  131. package/frontend-vue/layers/dms-auth/app/custom-pages/auth/forgot-validation.vue +1 -1
  132. package/frontend-vue/layers/dms-auth/app/custom-pages/auth/oauth-complete.vue +26 -3
  133. package/frontend-vue/layers/dms-auth/app/custom-pages/auth/recover.vue +1 -1
  134. package/frontend-vue/layers/dms-auth/app/custom-pages/auth/signup.vue +22 -25
  135. package/frontend-vue/layers/dms-auth/app/custom-pages/auth/validate.vue +1 -1
  136. package/frontend-vue/layers/dms-auth/app/middleware/auth.ts +5 -7
  137. package/frontend-vue/layers/dms-auth/app/plugins/permissions-preload.ts +17 -0
  138. package/frontend-vue/layers/dms-auth/server/accounts.ts +135 -0
  139. package/frontend-vue/layers/dms-auth/server/actions.ts +251 -0
  140. package/frontend-vue/layers/dms-auth/server/oauth.ts +260 -0
  141. package/frontend-vue/layers/dms-auth/server/session.ts +158 -0
  142. package/frontend-vue/layers/dms-core/app/build/utils/asyncDataScope.ts +47 -0
  143. package/frontend-vue/layers/dms-core/app/build/utils/permission-preview.ts +6 -2
  144. package/frontend-vue/layers/dms-core/app/composables/auth/useApiData.ts +148 -0
  145. package/frontend-vue/layers/dms-core/app/composables/auth/useAuthFetch.ts +7 -20
  146. package/frontend-vue/layers/dms-core/app/composables/auth/useCurrentUser.ts +2 -2
  147. package/frontend-vue/layers/dms-core/app/composables/auth/useMultiAccount.ts +18 -5
  148. package/frontend-vue/layers/dms-core/app/composables/auth/usePermissions.ts +35 -6
  149. package/frontend-vue/layers/dms-core/app/composables/auth/useSessionRecovery.ts +1 -1
  150. package/frontend-vue/layers/dms-core/app/composables/period/compareResolvers.ts +20 -14
  151. package/frontend-vue/layers/dms-core/app/composables/period/periodClock.ts +121 -0
  152. package/frontend-vue/layers/dms-core/app/composables/period/periodRenderNow.ts +42 -0
  153. package/frontend-vue/layers/dms-core/app/composables/period/periodTimeZone.ts +31 -0
  154. package/frontend-vue/layers/dms-core/app/composables/period/presetResolvers.ts +48 -19
  155. package/frontend-vue/layers/dms-core/app/composables/period/usePeriod.ts +33 -4
  156. package/frontend-vue/layers/dms-core/app/composables/period/usePeriodScope.ts +78 -25
  157. package/frontend-vue/layers/dms-core/app/composables/realtime/usePageRealtime.ts +3 -0
  158. package/frontend-vue/layers/dms-core/app/composables/realtime/useUserRealtime.ts +36 -6
  159. package/frontend-vue/layers/dms-core/app/composables/user/useUserRegionalPreferences.ts +1 -1
  160. package/frontend-vue/layers/dms-core/app/plugins/async-data-scope.ts +26 -0
  161. package/frontend-vue/layers/dms-core/app/utils/pageRoutes.ts +22 -0
  162. package/frontend-vue/layers/dms-core/app/utils/regional.ts +36 -3
  163. package/frontend-vue/layers/dms-core/app/utils/trailingDeduper.ts +10 -1
  164. package/frontend-vue/layers/dms-core/shared/types/app-config.ts +48 -4
  165. package/frontend-vue/layers/dms-layout/app/app.config.ts +42 -23
  166. package/frontend-vue/layers/dms-layout/app/assets/css/main.css +72 -20
  167. package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardFrame.vue +9 -0
  168. package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardSidebar.vue +0 -3
  169. package/frontend-vue/layers/dms-layout/app/build/components/notification/NotificationPopover.vue +9 -3
  170. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/members/MemberRolePicker.vue +13 -12
  171. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/notification/NotificationInboxDisplay.vue +17 -7
  172. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/notification/NotificationPreferencesForm.vue +0 -1
  173. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/profile/ProfilePersonalInfo.vue +36 -33
  174. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/profile/ProfileSecuritySummary.vue +1 -2
  175. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/roles/role-types.ts +5 -4
  176. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/security/SecuritySessions.vue +35 -28
  177. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/security/SecurityStatus.vue +0 -1
  178. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/security/SecurityTwoFactor.vue +1 -1
  179. package/frontend-vue/layers/dms-layout/app/build/composables/dev-reload/useDevReloadHolder.ts +7 -6
  180. package/frontend-vue/layers/dms-layout/app/build/composables/layout/sidePanelState.ts +1 -1
  181. package/frontend-vue/layers/dms-layout/app/build/composables/navigation/useVisitNavBadges.ts +26 -0
  182. package/frontend-vue/layers/dms-layout/app/build/composables/notification/useNotificationCatalog.ts +25 -23
  183. package/frontend-vue/layers/dms-layout/app/build/composables/notification/useNotificationPreferences.ts +44 -23
  184. package/frontend-vue/layers/dms-layout/app/build/composables/security/useSessionHandoff.ts +1 -1
  185. package/frontend-vue/layers/dms-layout/app/build/composables/settings/profile/preferencesSummary.ts +1 -1
  186. package/frontend-vue/layers/dms-layout/app/build/composables/settings/profile/useAccountData.ts +3 -2
  187. package/frontend-vue/layers/dms-layout/app/build/composables/settings/profile/useProfileAvatar.ts +1 -18
  188. package/frontend-vue/layers/dms-layout/app/build/composables/settings/profile/useProfilePreferences.ts +19 -23
  189. package/frontend-vue/layers/dms-layout/app/build/composables/settings/security/useSecurityFormat.ts +6 -3
  190. package/frontend-vue/layers/dms-layout/app/build/composables/settings/security/useSecurityOverview.ts +13 -18
  191. package/frontend-vue/layers/dms-layout/app/build/utils/modules-catalog.ts +8 -6
  192. package/frontend-vue/layers/dms-layout/app/build/utils/publicErrorMessage.ts +19 -0
  193. package/frontend-vue/layers/dms-layout/app/components/navigation/CollapsedMenuItem.vue +0 -11
  194. package/frontend-vue/layers/dms-layout/app/components/navigation/NavigationMenu.vue +3 -13
  195. package/frontend-vue/layers/dms-layout/app/composables/general/useSystemState.ts +7 -11
  196. package/frontend-vue/layers/dms-layout/app/composables/notification/useNotifications.ts +103 -22
  197. package/frontend-vue/layers/dms-layout/app/composables/page/useModulesListing.ts +25 -102
  198. package/frontend-vue/layers/dms-layout/app/composables/page/usePageComponentPreload.ts +0 -10
  199. package/frontend-vue/layers/dms-layout/app/composables/page/useSiteLayout.ts +177 -96
  200. package/frontend-vue/layers/dms-layout/app/composables/profile/useUserLanguage.ts +3 -2
  201. package/frontend-vue/layers/dms-layout/app/custom-pages/modules.vue +6 -28
  202. package/frontend-vue/layers/dms-layout/app/emails/components/EmailLayout.vue +31 -11
  203. package/frontend-vue/layers/dms-layout/app/error.vue +5 -2
  204. package/frontend-vue/layers/dms-layout/app/middleware/module-routing.global.ts +1 -1
  205. package/frontend-vue/layers/dms-layout/app/middleware/page-leave-guard.global.ts +2 -2
  206. package/frontend-vue/layers/dms-layout/app/pages/[...slug].vue +12 -9
  207. package/frontend-vue/layers/dms-layout/app/plugins/dms-menu-sync.client.ts +31 -3
  208. package/frontend-vue/layers/dms-layout/app/plugins/favicon.ts +58 -0
  209. package/frontend-vue/layers/dms-layout/app/plugins/notification-stream.client.ts +15 -7
  210. package/frontend-vue/layers/dms-layout/app/plugins/profile-sync.client.ts +5 -1
  211. package/frontend-vue/layers/dms-layout/app/plugins/regional-preferences.ts +10 -4
  212. package/frontend-vue/layers/dms-layout/app/plugins/time-zone-cookie.client.ts +12 -0
  213. package/frontend-vue/layers/dms-layout/app/types/page.ts +13 -0
  214. package/frontend-vue/layers/dms-layout/server/html-render.ts +58 -0
  215. package/frontend-vue/layers/dms-layout/server/render-token.ts +95 -0
  216. package/frontend-vue/layers/dms-onboarding/app/composables/onboarding/useOnboarding.ts +25 -21
  217. package/frontend-vue/layers/dms-onboarding/app/middleware/onboarding.global.ts +1 -1
  218. package/frontend-vue/layers/dms-onboarding/app/plugins/onboarding-preload.ts +9 -0
  219. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/registerDefaults.ts +4 -1
  220. package/frontend-vue/layers/dms-ui/app/build/composables/files/useFileMetadata.ts +162 -0
  221. package/frontend-vue/layers/dms-ui/app/build/composables/form/formRecordLoad.ts +23 -4
  222. package/frontend-vue/layers/dms-ui/app/build/composables/live/usePageDataRefresh.ts +101 -0
  223. package/frontend-vue/layers/dms-ui/app/build/composables/navigation/useNavBadges.ts +33 -6
  224. package/frontend-vue/layers/dms-ui/app/build/composables/period/periodDisplay.ts +19 -16
  225. package/frontend-vue/layers/dms-ui/app/build/composables/period/periodQuery.ts +40 -20
  226. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableFooter.ts +6 -8
  227. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableListData.ts +42 -14
  228. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableRows.ts +19 -8
  229. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViews.ts +1 -1
  230. package/frontend-vue/layers/dms-ui/app/components/chart/ChartCard.vue +5 -2
  231. package/frontend-vue/layers/dms-ui/app/components/flow-canvas/FlowCanvas.vue +9 -9
  232. package/frontend-vue/layers/dms-ui/app/components/form/Form.vue +20 -18
  233. package/frontend-vue/layers/dms-ui/app/components/form/components/Cascader.vue +2 -1
  234. package/frontend-vue/layers/dms-ui/app/components/form/components/File.vue +26 -132
  235. package/frontend-vue/layers/dms-ui/app/components/form/components/Image.vue +40 -105
  236. package/frontend-vue/layers/dms-ui/app/components/form/components/InputAddress.vue +2 -0
  237. package/frontend-vue/layers/dms-ui/app/components/form/components/Relation.vue +4 -4
  238. package/frontend-vue/layers/dms-ui/app/components/period/PeriodSelector.vue +9 -13
  239. package/frontend-vue/layers/dms-ui/app/components/table-view/CascaderPath.vue +2 -1
  240. package/frontend-vue/layers/dms-ui/app/components/table-view/FilePreview.vue +12 -24
  241. package/frontend-vue/layers/dms-ui/app/components/table-view/ImagePreview.vue +17 -29
  242. package/frontend-vue/layers/dms-ui/app/components/table-view/KanbanBoard.vue +7 -3
  243. package/frontend-vue/layers/dms-ui/app/components/table-view/TableView.vue +20 -8
  244. package/frontend-vue/layers/dms-ui/app/components/top-list/TopListCard.vue +8 -11
  245. package/frontend-vue/layers/dms-ui/app/composables/chart/useChartFetch.ts +146 -182
  246. package/frontend-vue/layers/dms-ui/app/composables/live/usePageLiveRefresh.ts +8 -2
  247. package/frontend-vue/layers/dms-ui/app/composables/useFileReadUrls.ts +11 -99
  248. package/frontend-vue/layers/dms-ui/app/utils/blockRefresh.ts +32 -3
  249. package/frontend-vue/package.json +2 -2
  250. package/frontend-vue/pnpm-lock.yaml +5 -5
  251. package/frontend-vue/server/private-options.ts +27 -0
  252. package/frontend-vue/tests/async-data-scope.test.ts +121 -0
  253. package/frontend-vue/tests/auth-middleware-error.test.ts +3 -3
  254. package/frontend-vue/tests/banner-block-fetch.test.ts +3 -0
  255. package/frontend-vue/tests/block-fetch-options-forwarding.test.ts +3 -0
  256. package/frontend-vue/tests/block-items-period-realtime.test.ts +3 -0
  257. package/frontend-vue/tests/block-not-found.test.ts +16 -2
  258. package/frontend-vue/tests/block-page-refresh.test.ts +3 -0
  259. package/frontend-vue/tests/block-route-params.test.ts +3 -0
  260. package/frontend-vue/tests/branding-defaults.test.ts +34 -0
  261. package/frontend-vue/tests/builtin-emails.test.ts +23 -2
  262. package/frontend-vue/tests/chart-live-pause.test.ts +4 -1
  263. package/frontend-vue/tests/chart-value-propagation.test.ts +4 -1
  264. package/frontend-vue/tests/composed-text-blocks.test.ts +3 -0
  265. package/frontend-vue/tests/email-layout.test.ts +66 -4
  266. package/frontend-vue/tests/error-page-message.test.ts +25 -2
  267. package/frontend-vue/tests/favicon.test.ts +131 -0
  268. package/frontend-vue/tests/file-fields-ssr.test.ts +145 -0
  269. package/frontend-vue/tests/file-read-urls.test.ts +145 -34
  270. package/frontend-vue/tests/file-unavailable.test.ts +5 -1
  271. package/frontend-vue/tests/fixtures/dataEngine.ts +44 -0
  272. package/frontend-vue/tests/form-load-error.test.ts +55 -1
  273. package/frontend-vue/tests/helpers/api-data-runtime.ts +77 -0
  274. package/frontend-vue/tests/image-hydration-ids.test.ts +21 -16
  275. package/frontend-vue/tests/image-reference-preservation.test.ts +59 -53
  276. package/frontend-vue/tests/input-number-adjust.test.ts +14 -0
  277. package/frontend-vue/tests/key-value-list-copy.test.ts +3 -0
  278. package/frontend-vue/tests/member-role-picker.test.ts +4 -2
  279. package/frontend-vue/tests/menu-sync-reconnect.test.ts +47 -0
  280. package/frontend-vue/tests/metadata-retry.test.ts +5 -1
  281. package/frontend-vue/tests/module-routing.test.ts +2 -5
  282. package/frontend-vue/tests/modules-catalog.test.ts +17 -4
  283. package/frontend-vue/tests/modules-listing-transfer.test.ts +95 -0
  284. package/frontend-vue/tests/multi-account-validate.test.ts +23 -1
  285. package/frontend-vue/tests/nav-badge-tones.test.ts +66 -3
  286. package/frontend-vue/tests/notification-counts-from-writes.test.ts +223 -0
  287. package/frontend-vue/tests/notification-counts-transfer.test.ts +88 -0
  288. package/frontend-vue/tests/notification-inbox-display.test.ts +57 -5
  289. package/frontend-vue/tests/notification-popover-errors.test.ts +21 -1
  290. package/frontend-vue/tests/oauth-complete-handoff.test.ts +209 -0
  291. package/frontend-vue/tests/onboarding-middleware.test.ts +1 -1
  292. package/frontend-vue/tests/onboarding-transfer.test.ts +106 -0
  293. package/frontend-vue/tests/page-data-refresh.test.ts +141 -0
  294. package/frontend-vue/tests/page-fill-height.test.ts +16 -3
  295. package/frontend-vue/tests/page-no-visible-blocks.test.ts +12 -2
  296. package/frontend-vue/tests/page-realtime-remount.test.ts +1 -1
  297. package/frontend-vue/tests/page-record-label.test.ts +0 -7
  298. package/frontend-vue/tests/page-validation-redirect.test.ts +12 -2
  299. package/frontend-vue/tests/period-hydration-now.test.ts +185 -0
  300. package/frontend-vue/tests/period-live-pause.test.ts +3 -0
  301. package/frontend-vue/tests/period-reader-zone.test.ts +137 -0
  302. package/frontend-vue/tests/period-scope-first-fetch.test.ts +17 -1
  303. package/frontend-vue/tests/period-ssr.test.ts +195 -0
  304. package/frontend-vue/tests/permission-preview-partial.test.ts +0 -2
  305. package/frontend-vue/tests/permissions-transfer.test.ts +194 -0
  306. package/frontend-vue/tests/post-login-redirect.test.ts +10 -5
  307. package/frontend-vue/tests/profile-personal-info-ssr.test.ts +150 -0
  308. package/frontend-vue/tests/profile-preferences-summary.test.ts +15 -11
  309. package/frontend-vue/tests/realtime-reconnect.test.ts +22 -3
  310. package/frontend-vue/tests/roles-name-errors.test.ts +1 -1
  311. package/frontend-vue/tests/row-side-panel.test.ts +1 -1
  312. package/frontend-vue/tests/security-sessions-load-error.test.ts +3 -1
  313. package/frontend-vue/tests/server-routes-vs-pages.test.ts +26 -0
  314. package/frontend-vue/tests/server-routes.test.ts +582 -0
  315. package/frontend-vue/tests/session-handoff.test.ts +1 -1
  316. package/frontend-vue/tests/settings-reads-ssr.test.ts +232 -0
  317. package/frontend-vue/tests/side-panels.test.ts +8 -1
  318. package/frontend-vue/tests/sign-in-exit-history.test.ts +219 -0
  319. package/frontend-vue/tests/signup-invitation-link.test.ts +56 -8
  320. package/frontend-vue/tests/site-layout-categories.test.ts +42 -0
  321. package/frontend-vue/tests/site-layout-hydration.test.ts +3 -3
  322. package/frontend-vue/tests/site-layout-rendered.test.ts +103 -0
  323. package/frontend-vue/tests/site-layout-resync.test.ts +87 -0
  324. package/frontend-vue/tests/ssr-guard-preload.test.ts +188 -0
  325. package/frontend-vue/tests/table-cell-tones.test.ts +36 -1
  326. package/frontend-vue/tests/table-data-ssr.test.ts +195 -0
  327. package/frontend-vue/tests/table-list-data-requests.test.ts +154 -0
  328. package/frontend-vue/tests/table-retry-loading.test.ts +37 -0
  329. package/frontend-vue/tests/table-source-navigation.test.ts +0 -1
  330. package/frontend-vue/tests/table-source-period-scope.test.ts +0 -1
  331. package/frontend-vue/tests/table-view-views.test.ts +1 -1
  332. package/frontend-vue/tests/top-list-card-actions.test.ts +3 -0
  333. package/frontend-vue/tests/top-list-card-period-badge.test.ts +79 -0
  334. package/frontend-vue/tests/top-list-card-value-suffix.test.ts +3 -0
  335. package/frontend-vue/tests/trailing-deduper.test.ts +22 -0
  336. package/frontend-vue/tests/use-api-data-update.test.ts +119 -0
  337. package/frontend-vue/tests/use-api-data.test.ts +410 -0
  338. package/frontend-vue/tests/user-language.test.ts +8 -5
  339. package/frontend-vue/tests/widget-label-i18n.test.ts +4 -1
  340. package/frontend-vue/vitest.config.ts +6 -0
  341. package/package.json +2 -2
  342. package/skills/dms/SKILL.md +2 -1
  343. package/skills/dms-module/SKILL.md +1 -1
  344. package/skills/dms-pages/SKILL.md +4 -1
  345. package/frontend-vue/layers/dms-core/app/build/composables/page/usePageRecordLabel.ts +0 -9
  346. package/frontend-vue/layers/dms-layout/app/composables/page/usePrefetch.ts +0 -21
  347. package/frontend-vue/layers/dms-ui/app/build/composables/form/useControlError.ts +0 -10
  348. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useServerRenderedAsyncData.ts +0 -23
  349. package/frontend-vue/tests/control-error-path.test.ts +0 -11
  350. package/frontend-vue/tests/modules-listing-per-request.test.ts +0 -104
  351. package/frontend-vue/tests/page-layout-background-refresh.test.ts +0 -321
@@ -0,0 +1,103 @@
1
+ // @vitest-environment jsdom
2
+ /**
3
+ * The site layout of a page that brings none with its props (a custom page)
4
+ * as the sidebar reads it, through the runtime's real async data: the server
5
+ * waits for it and renders the menu, and the browser takes it over without
6
+ * asking again. A page that brought it reads nothing.
7
+ */
8
+ import {
9
+ defineComponent,
10
+ getCurrentInstance,
11
+ h,
12
+ nextTick,
13
+ type App,
14
+ } from "vue";
15
+ import { afterEach, beforeEach, expect, it, vi } from "vitest";
16
+ import {
17
+ engine,
18
+ hydrate,
19
+ serverRender,
20
+ stubApiData,
21
+ } from "./helpers/api-data-runtime";
22
+ import {
23
+ useRenderedSiteLayout,
24
+ useSiteLayout,
25
+ } from "../layers/dms-layout/app/composables/page/useSiteLayout";
26
+
27
+ const RESPONSE = {
28
+ siteLayout: { pages: { "/orders": { fullId: "orders" } }, categories: {} },
29
+ siteLayoutTree: { children: {} },
30
+ quickActions: { actions: [] },
31
+ modules: {},
32
+ isOwner: true,
33
+ };
34
+
35
+ const authFetch = vi.fn();
36
+ const mounted: App[] = [];
37
+
38
+ // The menu as the header and the sidebar draw it: rendered by the frame, in
39
+ // the persistent layout, outside the page's <Suspense>.
40
+ const Menu = defineComponent({
41
+ setup() {
42
+ const { siteLayout } = useSiteLayout();
43
+ return () =>
44
+ h("nav", Object.keys(siteLayout.value?.pages ?? {}).join(",") || "-");
45
+ },
46
+ });
47
+ const Frame = defineComponent({
48
+ setup() {
49
+ useRenderedSiteLayout();
50
+ return () => h("div", [h(Menu), h(Menu)]);
51
+ },
52
+ });
53
+ const Layout = defineComponent({ setup: () => () => h("main", [h(Frame)]) });
54
+
55
+ beforeEach(() => {
56
+ stubApiData(authFetch);
57
+ // The application holds the in-flight request its callers join.
58
+ vi.stubGlobal(
59
+ "useDmsApp",
60
+ () => getCurrentInstance()!.appContext.config.globalProperties,
61
+ );
62
+ vi.stubGlobal("showDmsError", vi.fn());
63
+ });
64
+
65
+ afterEach(() => {
66
+ mounted.splice(0).forEach((app) => app.unmount());
67
+ vi.unstubAllGlobals();
68
+ vi.resetAllMocks();
69
+ });
70
+
71
+ it("renders the menu of a page without a site layout, and the browser does not read it again", async () => {
72
+ authFetch.mockResolvedValue(RESPONSE);
73
+ const { html, payload } = await serverRender(Layout);
74
+ expect(html).toBe(
75
+ "<main><div><nav>/orders</nav><nav>/orders</nav></div></main>",
76
+ );
77
+ expect(authFetch).toHaveBeenCalledTimes(1);
78
+ expect(authFetch).toHaveBeenCalledWith("/dms/sitelayout");
79
+
80
+ authFetch.mockClear();
81
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
82
+ const { app, container } = hydrate(Layout, html, payload);
83
+ mounted.push(app);
84
+ await nextTick();
85
+ await new Promise((done) => setTimeout(done, 0));
86
+ expect(authFetch).not.toHaveBeenCalled();
87
+ expect(container.innerHTML).toBe(html);
88
+ // Taken over before the frame's children render: they hydrate the menu.
89
+ expect(warn.mock.calls.flat().join(" ")).not.toMatch(/hydration/i);
90
+ });
91
+
92
+ it("reads nothing when the page brought its site layout", async () => {
93
+ const { html, payload } = await serverRender(Layout, (app) =>
94
+ app.runWithContext(() => {
95
+ engine.useDmsState("dms-siteLayout").value = RESPONSE.siteLayout;
96
+ }),
97
+ );
98
+ expect(html).toBe(
99
+ "<main><div><nav>/orders</nav><nav>/orders</nav></div></main>",
100
+ );
101
+ expect(authFetch).not.toHaveBeenCalled();
102
+ expect(payload.data).toEqual({});
103
+ });
@@ -0,0 +1,87 @@
1
+ // @vitest-environment jsdom
2
+ import { ref, type Ref } from "vue";
3
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
4
+
5
+ // What a realtime menu invalidation does: a partial reload of the page on
6
+ // screen through Inertia, which replaces the site layout every visit carries
7
+ // over (a once prop) along with the state.
8
+ const reload = vi.hoisted(() => vi.fn());
9
+ const flushAll = vi.hoisted(() => vi.fn());
10
+ vi.mock("@inertiajs/vue3", () => ({ router: { reload, flushAll } }));
11
+
12
+ const { useSiteLayout } = await import(
13
+ "../layers/dms-layout/app/composables/page/useSiteLayout"
14
+ );
15
+
16
+ type ReloadOptions = {
17
+ only: string[];
18
+ onHttpException: (response: { status: number }) => boolean | void;
19
+ onNetworkError: (error: Error) => boolean | void;
20
+ onFinish: () => void;
21
+ };
22
+
23
+ const state = new Map<string, Ref>();
24
+
25
+ beforeEach(() => {
26
+ state.clear();
27
+ const app = {};
28
+ vi.stubGlobal("useDmsApp", () => app);
29
+ vi.stubGlobal("useDmsState", (key: string, initial: () => unknown) => {
30
+ if (!state.has(key)) state.set(key, ref(initial()));
31
+ return state.get(key);
32
+ });
33
+ vi.stubGlobal("useAuthFetch", () => ({ $authFetch: vi.fn() }));
34
+ window.history.replaceState(null, "", "/projects");
35
+ });
36
+
37
+ afterEach(() => {
38
+ vi.unstubAllGlobals();
39
+ reload.mockReset();
40
+ flushAll.mockReset();
41
+ });
42
+
43
+ describe("useSiteLayout resync", () => {
44
+ it("reloads the site layout and the page through Inertia", async () => {
45
+ reload.mockImplementation((options: ReloadOptions) => options.onFinish());
46
+ await useSiteLayout().resync();
47
+ expect(reload).toHaveBeenCalledTimes(1);
48
+ expect(reload.mock.calls[0]![0].only).toEqual(["shared", "page"]);
49
+ });
50
+
51
+ it("drops the pages prefetched with the previous menu before reloading", async () => {
52
+ reload.mockImplementation((options: ReloadOptions) => options.onFinish());
53
+ await useSiteLayout().resync();
54
+ expect(flushAll).toHaveBeenCalledOnce();
55
+ expect(flushAll.mock.invocationCallOrder[0]).toBeLessThan(
56
+ reload.mock.invocationCallOrder[0]!,
57
+ );
58
+ });
59
+
60
+ it("rejects a failed reload and keeps the page on screen", async () => {
61
+ let swapsPage: boolean | void = true;
62
+ reload.mockImplementation((options: ReloadOptions) => {
63
+ swapsPage = options.onHttpException({ status: 502 });
64
+ options.onFinish();
65
+ });
66
+ await expect(useSiteLayout().resync()).rejects.toThrow("502");
67
+ expect(swapsPage).toBe(false);
68
+
69
+ reload.mockImplementation((options: ReloadOptions) => {
70
+ swapsPage = options.onNetworkError(new Error("offline"));
71
+ options.onFinish();
72
+ });
73
+ await expect(useSiteLayout().resync()).rejects.toThrow("offline");
74
+ expect(swapsPage).toBe(false);
75
+ });
76
+
77
+ it("reloads the page a navigation opened while the reload was in flight", async () => {
78
+ reload.mockImplementationOnce((options: ReloadOptions) => {
79
+ // Inertia drops an async answer for a page the user has left.
80
+ window.history.replaceState(null, "", "/tasks");
81
+ options.onFinish();
82
+ });
83
+ reload.mockImplementation((options: ReloadOptions) => options.onFinish());
84
+ await useSiteLayout().resync();
85
+ expect(reload).toHaveBeenCalledTimes(2);
86
+ });
87
+ });
@@ -0,0 +1,188 @@
1
+ // @vitest-environment jsdom
2
+ /**
3
+ * A server render of a protected page waits on two requests made by its
4
+ * middlewares, one after the other: the onboarding status (`onboarding`),
5
+ * then the permission set (`auth`). Both start with the app instead, through
6
+ * the real renderer runtime, middlewares and composables, and each middleware
7
+ * joins the request already in flight.
8
+ */
9
+ import { createSSRApp, defineComponent, h, nextTick, type App } from "vue";
10
+ import { renderToString } from "vue/server-renderer";
11
+ import { beforeEach, describe, expect, it, vi } from "vitest";
12
+ import type { DmsAppContext, DmsFrontendRuntime } from "#dms/frontend-module";
13
+
14
+ const engine = await import("#dms/frontend-module");
15
+ const { createTrailingDeduper } = await import(
16
+ "../layers/dms-core/app/utils/trailingDeduper"
17
+ );
18
+ for (const name of [
19
+ "useDmsState",
20
+ "useDmsApp",
21
+ "useDmsRoute",
22
+ "defineDmsMiddleware",
23
+ "defineDmsPlugin",
24
+ "useDmsError",
25
+ "abortDmsNavigation",
26
+ "navigateDms",
27
+ ] as const) {
28
+ vi.stubGlobal(name, engine[name]);
29
+ }
30
+ vi.stubGlobal("createTrailingDeduper", createTrailingDeduper);
31
+ vi.stubGlobal("useDmsRuntimeConfig", () => ({ public: { dms: {} } }));
32
+ vi.stubGlobal("useHomepage", () => "/home");
33
+
34
+ const session = { loggedIn: { value: true } };
35
+ vi.stubGlobal("useDmsUserSession", () => ({
36
+ ...session,
37
+ user: { value: { isValidated: true } },
38
+ }));
39
+ let pagePublic = false;
40
+ vi.stubGlobal("useSiteLayout", () => ({
41
+ siteLayout: { value: {} },
42
+ findMatchingRoute: (path: string) =>
43
+ path === "/orders" ? { metadata: { publicAccess: pagePublic } } : null,
44
+ }));
45
+
46
+ const { useTrailingDeduper } = await import(
47
+ "../layers/dms-core/app/composables/useTrailingDeduper"
48
+ );
49
+ vi.stubGlobal("useTrailingDeduper", useTrailingDeduper);
50
+ const { usePermissions } = await import(
51
+ "../layers/dms-core/app/composables/auth/usePermissions"
52
+ );
53
+ vi.stubGlobal("usePermissions", usePermissions);
54
+ const { useOnboarding } = await import(
55
+ "../layers/dms-onboarding/app/composables/onboarding/useOnboarding"
56
+ );
57
+ vi.stubGlobal("useOnboarding", useOnboarding);
58
+ const { default: onboardingMiddleware } = await import(
59
+ "../layers/dms-onboarding/app/middleware/onboarding.global"
60
+ );
61
+ const { default: authMiddleware } = await import(
62
+ "../layers/dms-auth/app/middleware/auth"
63
+ );
64
+ const { default: onboardingPreload } = await import(
65
+ "../layers/dms-onboarding/app/plugins/onboarding-preload"
66
+ );
67
+ const { default: permissionsPreload } = await import(
68
+ "../layers/dms-auth/app/plugins/permissions-preload"
69
+ );
70
+
71
+ const ONBOARDING = "/api/onboarding/informations";
72
+ const PERMISSIONS = "/dms/permissions";
73
+ const ANSWERS: Record<string, unknown> = {
74
+ [ONBOARDING]: { has_onboarded: true },
75
+ [PERMISSIONS]: ["orders.read"],
76
+ };
77
+
78
+ interface PendingRequest {
79
+ url: string;
80
+ resolve: (value: unknown) => void;
81
+ }
82
+
83
+ const pending: PendingRequest[] = [];
84
+ const asked: string[] = [];
85
+ // Each request waits for the test to answer it.
86
+ const $authFetch = vi.fn(
87
+ (url: string) =>
88
+ new Promise((resolve) => {
89
+ asked.push(url);
90
+ pending.push({ url, resolve });
91
+ }),
92
+ );
93
+ vi.stubGlobal("useAuthFetch", () => ({ $authFetch }));
94
+
95
+ const settle = async () => {
96
+ await nextTick();
97
+ await new Promise((resolve) => setTimeout(resolve, 0));
98
+ };
99
+
100
+ /** The requests waiting at once, each round, until none is left. */
101
+ async function answerRounds(): Promise<string[][]> {
102
+ const rounds: string[][] = [];
103
+ for (let round = 0; round < 6; round++) {
104
+ await settle();
105
+ const waiting = pending.splice(0);
106
+ if (waiting.length === 0) continue;
107
+ rounds.push(waiting.map((request) => request.url).sort());
108
+ for (const request of waiting) request.resolve(ANSWERS[request.url]);
109
+ }
110
+ return rounds;
111
+ }
112
+
113
+ const Page = defineComponent({
114
+ setup() {
115
+ engine.defineDmsPageMeta({ auth: true });
116
+ const { hasPermission } = usePermissions();
117
+ return () =>
118
+ h("p", hasPermission("orders.read") ? "can read" : "cannot read");
119
+ },
120
+ });
121
+
122
+ /** A server runtime, its app set up the way the DMS module registers it. */
123
+ function serverRender(path: string): { app: App; html: Promise<string> } {
124
+ const runtime = engine.createDmsFrontendRuntime(undefined, {}, true);
125
+ const app = createSSRApp(Page);
126
+ const context = {
127
+ runWithContext: (callback: () => unknown) => app.runWithContext(callback),
128
+ } as unknown as DmsAppContext;
129
+ app.config.globalProperties.$dms = context;
130
+ runtime.appContext = context;
131
+ runtime.route.path = path;
132
+ runtime.route.fullPath = path;
133
+ runtime.route.matched = [{}];
134
+ engine.provideDmsFrontendRuntime(app, runtime as DmsFrontendRuntime);
135
+ // The plugins run before the page renders, as `installDmsPlugins` does.
136
+ for (const plugin of [onboardingPreload, permissionsPreload])
137
+ app.runWithContext(() => plugin(context));
138
+ return { app, html: renderToString(app) };
139
+ }
140
+
141
+ beforeEach(async () => {
142
+ pending.length = 0;
143
+ asked.length = 0;
144
+ $authFetch.mockClear();
145
+ session.loggedIn.value = true;
146
+ pagePublic = false;
147
+ await engine.setupFrontendModules([
148
+ {
149
+ options: { public: {} },
150
+ module: {
151
+ setup(sdk) {
152
+ sdk.registerMiddleware("auth", authMiddleware);
153
+ sdk.registerMiddleware("onboarding", onboardingMiddleware, {
154
+ global: true,
155
+ });
156
+ },
157
+ },
158
+ },
159
+ ]);
160
+ });
161
+
162
+ describe("the requests a server render's middlewares wait on", () => {
163
+ it("start together, and each middleware joins its own", async () => {
164
+ const { html } = serverRender("/orders");
165
+ const rounds = await answerRounds();
166
+
167
+ expect(rounds).toEqual([[PERMISSIONS, ONBOARDING].sort()]);
168
+ expect(asked.sort()).toEqual([PERMISSIONS, ONBOARDING].sort());
169
+ expect(await html).toContain("can read");
170
+ });
171
+
172
+ it("ask for no permission set the middleware would not load", async () => {
173
+ pagePublic = true;
174
+ const { html } = serverRender("/orders");
175
+ await answerRounds();
176
+ expect(asked).toEqual([ONBOARDING]);
177
+ expect(await html).toContain("cannot read");
178
+
179
+ pending.length = 0;
180
+ asked.length = 0;
181
+ pagePublic = false;
182
+ session.loggedIn.value = false;
183
+ const signedOut = serverRender("/orders");
184
+ await answerRounds();
185
+ expect(asked).toEqual([ONBOARDING]);
186
+ await signedOut.html.catch(() => undefined);
187
+ });
188
+ });
@@ -5,7 +5,15 @@
5
5
  * one keeps the default look. A duration may also be written in milliseconds
6
6
  * whatever its size (`format: "ms"`).
7
7
  */
8
- import { createApp, defineComponent, h, type App, type VNodeChild } from "vue";
8
+ import {
9
+ createApp,
10
+ createSSRApp,
11
+ defineComponent,
12
+ h,
13
+ type App,
14
+ type VNodeChild,
15
+ } from "vue";
16
+ import { renderToString } from "vue/server-renderer";
9
17
  import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
10
18
  import type { DataType } from "../layers/dms-core/app/composables/data-types/useDataType";
11
19
  import { isObject } from "../layers/dms-core/app/utils/type-check";
@@ -174,3 +182,30 @@ describe("plain text cell", () => {
174
182
  }
175
183
  });
176
184
  });
185
+
186
+ describe("empty plain text cell", () => {
187
+ it("renders nothing, with or without a tone field", () => {
188
+ expect(cell("string", "", undefined)).toBeNull();
189
+ expect(
190
+ cell("string", "", { toneField: "messageTone" }, { messageTone: "ok" }),
191
+ ).toBeNull();
192
+ });
193
+
194
+ it("hydrates the server's HTML without a mismatch", async () => {
195
+ // As Table.vue draws it: the formatter's output is the cell's root.
196
+ const Cell = defineComponent({
197
+ setup: () => () => cell("string", "", undefined) as VNodeChild,
198
+ });
199
+ const Row = { render: () => h("div", [h("b", "id"), h(Cell)]) };
200
+ const container = document.createElement("div");
201
+ container.innerHTML = await renderToString(createSSRApp(Row));
202
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
203
+ app = createSSRApp(Row);
204
+ app.mount(container);
205
+ const mismatches = warn.mock.calls.filter((call) =>
206
+ /hydration/i.test(String(call[0])),
207
+ );
208
+ warn.mockRestore();
209
+ expect(mismatches).toEqual([]);
210
+ });
211
+ });
@@ -0,0 +1,195 @@
1
+ // @vitest-environment jsdom
2
+ /**
3
+ * A table view's list, tab counters and footer figures, through the runtime's
4
+ * real async data: a server render requests them in parallel and renders
5
+ * them, the browser hydrates with them without asking again and without a
6
+ * mismatch, and a client navigation does not wait for them.
7
+ */
8
+ import { readFileSync } from "node:fs";
9
+ import { resolve } from "node:path";
10
+ import {
11
+ computed,
12
+ createSSRApp,
13
+ defineComponent,
14
+ h,
15
+ nextTick,
16
+ type App,
17
+ } from "vue";
18
+ import { renderToString } from "vue/server-renderer";
19
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
20
+ import type { DmsFrontendRuntime } from "#dms/frontend-module";
21
+ import type { TableViewColumn } from "../layers/dms-ui/app/composables/table-view/types";
22
+
23
+ const engine = await import("#dms/frontend-module");
24
+ vi.stubGlobal("computed", computed);
25
+ vi.stubGlobal("useDmsAsyncData", engine.useDmsAsyncData);
26
+ vi.stubGlobal("useI18n", () => ({ locale: { value: "en" } }));
27
+ vi.stubGlobal("useTranslation", () => ({
28
+ processI18n: (text: string) => text,
29
+ }));
30
+ vi.stubGlobal("useDataTypes", () => ({ getDataType: () => undefined }));
31
+
32
+ const LOCATION = "/api/orders";
33
+ const COLUMNS = [
34
+ { id: "total", accessorKey: "total" },
35
+ ] as unknown as TableViewColumn[];
36
+ const DELAY_MS = 20;
37
+
38
+ interface Request {
39
+ url: string;
40
+ startedAt: number;
41
+ }
42
+
43
+ const requests: Request[] = [];
44
+ let inFlight = 0;
45
+ let mostInFlight = 0;
46
+
47
+ async function answer<T>(url: string, value: T): Promise<T> {
48
+ requests.push({ url, startedAt: performance.now() });
49
+ inFlight += 1;
50
+ mostInFlight = Math.max(mostInFlight, inFlight);
51
+ await new Promise((done) => setTimeout(done, DELAY_MS));
52
+ inFlight -= 1;
53
+ return value;
54
+ }
55
+
56
+ const api = (async (url: string) =>
57
+ url.endsWith("/count/batch")
58
+ ? answer(url, { all: 2, open: 1 })
59
+ : answer(url, { results: [{ _id: "a", total: 3 }], total: 1 })) as never;
60
+ vi.stubGlobal("useAuthFetch", () => ({
61
+ $authFetch: (url: string) => answer(url, { sum: 42 }),
62
+ }));
63
+
64
+ const { useApiData } = await import(
65
+ "../layers/dms-core/app/composables/auth/useApiData"
66
+ );
67
+ vi.stubGlobal("useApiData", useApiData);
68
+
69
+ const { useTableRows } = await import(
70
+ "../layers/dms-ui/app/build/composables/table-view/useTableRows"
71
+ );
72
+ const { useTableCountsData, useTableListData } = await import(
73
+ "../layers/dms-ui/app/build/composables/table-view/useTableListData"
74
+ );
75
+ const { useTableFooter } = await import(
76
+ "../layers/dms-ui/app/build/composables/table-view/useTableFooter"
77
+ );
78
+
79
+ const Table = defineComponent({
80
+ setup() {
81
+ const rows = useTableRows({ api, location: LOCATION, columns: COLUMNS });
82
+ const keys = { componentId: "orders", pageId: "sales", rows };
83
+ const archiveQuery = computed(() => ({}));
84
+ // As TableView: none of the three is awaited.
85
+ const list = useTableListData({
86
+ ...keys,
87
+ query: computed(() => ({ offset: 0, limit: 10 })),
88
+ archiveQuery,
89
+ isSelfManaged: computed(() => false),
90
+ });
91
+ const counts = useTableCountsData({
92
+ ...keys,
93
+ queries: computed(() => [
94
+ { id: "all", query: {} },
95
+ { id: "open", query: { filter_status: "is:open" } },
96
+ ]),
97
+ archiveQuery,
98
+ });
99
+ const { footer } = useTableFooter({
100
+ footer: {
101
+ summary: [{ id: "sum", op: "sum", field: "total", label: "Sum" }],
102
+ },
103
+ columns: COLUMNS,
104
+ location: LOCATION,
105
+ query: computed(() => ({})),
106
+ dataKey: "table-view-orders-sales",
107
+ } as never);
108
+ return () =>
109
+ h("p", [
110
+ `rows:${list.data.value?.total ?? "-"}`,
111
+ ` counts:${counts.data.value ? counts.data.value.all : "-"}`,
112
+ ` sum:${footer.value?.summaries[0]?.value ?? "-"}`,
113
+ ]);
114
+ },
115
+ });
116
+
117
+ let runtime: DmsFrontendRuntime;
118
+ const mounted: App[] = [];
119
+
120
+ beforeEach(() => {
121
+ requests.length = 0;
122
+ inFlight = 0;
123
+ mostInFlight = 0;
124
+ });
125
+
126
+ afterEach(() => {
127
+ mounted.splice(0).forEach((app) => app.unmount());
128
+ });
129
+
130
+ async function serverRender(): Promise<{ html: string; payload: unknown }> {
131
+ runtime = engine.createDmsFrontendRuntime(undefined, {}, true);
132
+ const app = createSSRApp(Table);
133
+ engine.provideDmsFrontendRuntime(app, runtime);
134
+ const html = await renderToString(app);
135
+ return { html, payload: engine.serializeDmsPayload(runtime) };
136
+ }
137
+
138
+ describe("a table view's data in a server render", () => {
139
+ it("requests the list, the counters and the footer in parallel and renders them", async () => {
140
+ const { html } = await serverRender();
141
+
142
+ expect(html).toBe("<p>rows:1 counts:2 sum:42</p>");
143
+ expect(requests.map(({ url }) => url).sort()).toEqual([
144
+ `${LOCATION}/count/batch`,
145
+ `${LOCATION}/list`,
146
+ `${LOCATION}/summary`,
147
+ ]);
148
+ expect(mostInFlight).toBe(3);
149
+ });
150
+
151
+ it("hands the browser the rendered data: no request, no mismatch", async () => {
152
+ const { html, payload } = await serverRender();
153
+ requests.length = 0;
154
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
155
+ const error = vi.spyOn(console, "error").mockImplementation(() => {});
156
+
157
+ const browser = engine.createDmsFrontendRuntime(undefined, {});
158
+ engine.restoreDmsPayload(
159
+ browser,
160
+ structuredClone(payload) as Parameters<
161
+ typeof engine.restoreDmsPayload
162
+ >[1],
163
+ );
164
+ const container = document.createElement("div");
165
+ container.innerHTML = html;
166
+ const app = createSSRApp(Table);
167
+ engine.provideDmsFrontendRuntime(app, browser);
168
+ app.mount(container);
169
+ mounted.push(app);
170
+ await nextTick();
171
+ await new Promise((done) => setTimeout(done, DELAY_MS * 2));
172
+
173
+ const mismatches = [...warn.mock.calls, ...error.mock.calls].filter(
174
+ (call) => /hydration/i.test(String(call[0])),
175
+ );
176
+ warn.mockRestore();
177
+ error.mockRestore();
178
+ expect(mismatches).toEqual([]);
179
+ expect(requests).toEqual([]);
180
+ expect(container.textContent).toBe("rows:1 counts:2 sum:42");
181
+ });
182
+
183
+ it("is not awaited by TableView, so the three start together", () => {
184
+ const tableView = readFileSync(
185
+ resolve(
186
+ __dirname,
187
+ "../layers/dms-ui/app/components/table-view/TableView.vue",
188
+ ),
189
+ "utf8",
190
+ );
191
+ expect(tableView).not.toMatch(
192
+ /await\s+(useTableListData|useTableCountsData|useTableFooter)\b/,
193
+ );
194
+ });
195
+ });