@antelopejs/dms 0.8.3 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (424) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/data/module-store/catalog.json +71 -0
  3. package/dist/config.d.ts +7 -0
  4. package/dist/config.js +8 -0
  5. package/dist/config.js.map +1 -1
  6. package/dist/implementations/dms/frontend-backend-proxies.d.ts +3 -0
  7. package/dist/implementations/dms/frontend-backend-proxies.js +39 -0
  8. package/dist/implementations/dms/frontend-backend-proxies.js.map +1 -0
  9. package/dist/implementations/dms/frontend-bootstrap.d.ts +1 -0
  10. package/dist/implementations/dms/frontend-bootstrap.js +13 -0
  11. package/dist/implementations/dms/frontend-bootstrap.js.map +1 -1
  12. package/dist/implementations/dms/frontend-modules.d.ts +6 -2
  13. package/dist/implementations/dms/frontend-modules.js +37 -8
  14. package/dist/implementations/dms/frontend-modules.js.map +1 -1
  15. package/dist/implementations/dms/frontend-ready.d.ts +8 -0
  16. package/dist/implementations/dms/frontend-ready.js +34 -0
  17. package/dist/implementations/dms/frontend-ready.js.map +1 -0
  18. package/dist/implementations/dms/manifest.d.ts +7 -0
  19. package/dist/implementations/dms/manifest.js +13 -0
  20. package/dist/implementations/dms/manifest.js.map +1 -1
  21. package/dist/implementations/dms/menu-categories.d.ts +16 -0
  22. package/dist/implementations/dms/menu-categories.js +30 -0
  23. package/dist/implementations/dms/menu-categories.js.map +1 -0
  24. package/dist/implementations/dms/nav-badges.d.ts +0 -8
  25. package/dist/implementations/dms/nav-badges.js +0 -16
  26. package/dist/implementations/dms/nav-badges.js.map +1 -1
  27. package/dist/implementations/dms/page.d.ts +13 -5
  28. package/dist/implementations/dms/page.js +72 -41
  29. package/dist/implementations/dms/page.js.map +1 -1
  30. package/dist/implementations/dms-html-render/index.js +5 -2
  31. package/dist/implementations/dms-html-render/index.js.map +1 -1
  32. package/dist/implementations/dms-notifications/index.d.ts +4 -3
  33. package/dist/implementations/dms-notifications/index.js +14 -6
  34. package/dist/implementations/dms-notifications/index.js.map +1 -1
  35. package/dist/pages/settings/settings-index.js +3 -2
  36. package/dist/pages/settings/settings-index.js.map +1 -1
  37. package/dist/pages/settings/users/invites.js +2 -2
  38. package/dist/pages/settings/users/invites.js.map +1 -1
  39. package/dist/pages/settings/users/member-change-notifications.js +17 -6
  40. package/dist/pages/settings/users/member-change-notifications.js.map +1 -1
  41. package/dist/pages/settings/users/member-invite-form.d.ts +3 -2
  42. package/dist/pages/settings/users/member-invite-form.js +7 -4
  43. package/dist/pages/settings/users/member-invite-form.js.map +1 -1
  44. package/dist/pages/settings/users/member-roles-field.js +2 -1
  45. package/dist/pages/settings/users/member-roles-field.js.map +1 -1
  46. package/dist/pages/settings/users/members.js +4 -1
  47. package/dist/pages/settings/users/members.js.map +1 -1
  48. package/dist/pages/settings/users/notifications.d.ts +6 -0
  49. package/dist/pages/settings/users/notifications.js +23 -15
  50. package/dist/pages/settings/users/notifications.js.map +1 -1
  51. package/dist/pages/settings/users/profile.js +1 -1
  52. package/dist/pages/settings/users/profile.js.map +1 -1
  53. package/dist/pages/settings/users/region-form.d.ts +1 -1
  54. package/dist/pages/settings/users/region-form.js +2 -1
  55. package/dist/pages/settings/users/region-form.js.map +1 -1
  56. package/dist/pages/settings/users/role-editor-store.d.ts +3 -1
  57. package/dist/pages/settings/users/role-editor-store.js +17 -4
  58. package/dist/pages/settings/users/role-editor-store.js.map +1 -1
  59. package/dist/pages/settings/users/roles.js +1 -0
  60. package/dist/pages/settings/users/roles.js.map +1 -1
  61. package/dist/realtime/registry.d.ts +1 -0
  62. package/dist/realtime/registry.js +5 -0
  63. package/dist/realtime/registry.js.map +1 -1
  64. package/dist/routes/index.d.ts +1 -0
  65. package/dist/routes/index.js +1 -0
  66. package/dist/routes/index.js.map +1 -1
  67. package/dist/routes/realtime.d.ts +5 -1
  68. package/dist/routes/realtime.js +16 -9
  69. package/dist/routes/realtime.js.map +1 -1
  70. package/dist/routes/store.d.ts +21 -0
  71. package/dist/routes/store.js +130 -0
  72. package/dist/routes/store.js.map +1 -0
  73. package/dist/store/activity.d.ts +2 -0
  74. package/dist/store/activity.js +12 -0
  75. package/dist/store/activity.js.map +1 -0
  76. package/dist/store/cache.d.ts +8 -0
  77. package/dist/store/cache.js +29 -0
  78. package/dist/store/cache.js.map +1 -0
  79. package/dist/store/catalog.d.ts +4 -0
  80. package/dist/store/catalog.js +69 -0
  81. package/dist/store/catalog.js.map +1 -0
  82. package/dist/store/compatibility.d.ts +3 -0
  83. package/dist/store/compatibility.js +45 -0
  84. package/dist/store/compatibility.js.map +1 -0
  85. package/dist/store/declarations.d.ts +18 -0
  86. package/dist/store/declarations.js +116 -0
  87. package/dist/store/declarations.js.map +1 -0
  88. package/dist/store/frontend-compatibility.d.ts +18 -0
  89. package/dist/store/frontend-compatibility.js +93 -0
  90. package/dist/store/frontend-compatibility.js.map +1 -0
  91. package/dist/store/interfaces.d.ts +6 -0
  92. package/dist/store/interfaces.js +126 -0
  93. package/dist/store/interfaces.js.map +1 -0
  94. package/dist/store/listing.d.ts +2 -0
  95. package/dist/store/listing.js +180 -0
  96. package/dist/store/listing.js.map +1 -0
  97. package/dist/store/operations.d.ts +8 -0
  98. package/dist/store/operations.js +254 -0
  99. package/dist/store/operations.js.map +1 -0
  100. package/dist/store/registry.d.ts +4 -0
  101. package/dist/store/registry.js +61 -0
  102. package/dist/store/registry.js.map +1 -0
  103. package/dist/store/requirements.d.ts +14 -0
  104. package/dist/store/requirements.js +81 -0
  105. package/dist/store/requirements.js.map +1 -0
  106. package/dist/store/runtime.d.ts +11 -0
  107. package/dist/store/runtime.js +78 -0
  108. package/dist/store/runtime.js.map +1 -0
  109. package/dist/store/tarball.d.ts +2 -0
  110. package/dist/store/tarball.js +80 -0
  111. package/dist/store/tarball.js.map +1 -0
  112. package/dist/store/types.d.ts +98 -0
  113. package/dist/store/types.js +3 -0
  114. package/dist/store/types.js.map +1 -0
  115. package/dist/test/integration/account-deletion.test.js +2 -2
  116. package/dist/test/integration/account-deletion.test.js.map +1 -1
  117. package/dist/test/integration/auth/auth-key-rotation.test.js +2 -1
  118. package/dist/test/integration/auth/auth-key-rotation.test.js.map +1 -1
  119. package/dist/test/integration/auth/current-password.test.js +2 -1
  120. package/dist/test/integration/auth/current-password.test.js.map +1 -1
  121. package/dist/test/integration/auth/session-revocation.test.js +2 -1
  122. package/dist/test/integration/auth/session-revocation.test.js.map +1 -1
  123. package/dist/test/integration/auth/sign-in-country.test.js +2 -1
  124. package/dist/test/integration/auth/sign-in-country.test.js.map +1 -1
  125. package/dist/test/integration/auth/two-factor-codes.test.js +2 -1
  126. package/dist/test/integration/auth/two-factor-codes.test.js.map +1 -1
  127. package/dist/test/integration/auth/two-factor-password.test.js +3 -2
  128. package/dist/test/integration/auth/two-factor-password.test.js.map +1 -1
  129. package/dist/test/integration/email-change.test.js +2 -1
  130. package/dist/test/integration/email-change.test.js.map +1 -1
  131. package/dist/test/integration/files/component-files.test.js +2 -2
  132. package/dist/test/integration/files/component-files.test.js.map +1 -1
  133. package/dist/test/integration/member-settings-access.test.js +8 -7
  134. package/dist/test/integration/member-settings-access.test.js.map +1 -1
  135. package/dist/test/integration/menu-access-events.test.d.ts +1 -0
  136. package/dist/test/integration/menu-access-events.test.js +148 -0
  137. package/dist/test/integration/menu-access-events.test.js.map +1 -0
  138. package/dist/test/integration/notification-inbox.test.js +4 -1
  139. package/dist/test/integration/notification-inbox.test.js.map +1 -1
  140. package/dist/test/integration/notification-locked-subjects.test.js +3 -2
  141. package/dist/test/integration/notification-locked-subjects.test.js.map +1 -1
  142. package/dist/test/integration/notification-unread-badge.test.js +51 -5
  143. package/dist/test/integration/notification-unread-badge.test.js.map +1 -1
  144. package/dist/test/integration/page-layout-route-guard.test.d.ts +1 -0
  145. package/dist/test/integration/page-layout-route-guard.test.js +56 -0
  146. package/dist/test/integration/page-layout-route-guard.test.js.map +1 -0
  147. package/dist/test/integration/permission-warnings.test.js +3 -2
  148. package/dist/test/integration/permission-warnings.test.js.map +1 -1
  149. package/dist/test/integration/removed-member-notifications.test.js +3 -2
  150. package/dist/test/integration/removed-member-notifications.test.js.map +1 -1
  151. package/dist/test/integration/role-delete-reassign.test.js +2 -1
  152. package/dist/test/integration/role-delete-reassign.test.js.map +1 -1
  153. package/dist/test/integration/role-permission-ancestors.test.js +2 -1
  154. package/dist/test/integration/role-permission-ancestors.test.js.map +1 -1
  155. package/dist/test/unit/frontend-bootstrap.test.js +13 -0
  156. package/dist/test/unit/frontend-bootstrap.test.js.map +1 -1
  157. package/dist/test/unit/implementations/dms/category-slug-registry.test.js +14 -5
  158. package/dist/test/unit/implementations/dms/category-slug-registry.test.js.map +1 -1
  159. package/dist/test/unit/implementations/dms/frontend-backend-proxies.test.d.ts +1 -0
  160. package/dist/test/unit/implementations/dms/frontend-backend-proxies.test.js +66 -0
  161. package/dist/test/unit/implementations/dms/frontend-backend-proxies.test.js.map +1 -0
  162. package/dist/test/unit/implementations/dms/manifest-strip.test.js +26 -0
  163. package/dist/test/unit/implementations/dms/manifest-strip.test.js.map +1 -1
  164. package/dist/test/unit/implementations/dms/nav-badges.test.js +23 -9
  165. package/dist/test/unit/implementations/dms/nav-badges.test.js.map +1 -1
  166. package/dist/test/unit/implementations/dms-html-render/generate-html.test.js +22 -4
  167. package/dist/test/unit/implementations/dms-html-render/generate-html.test.js.map +1 -1
  168. package/dist/test/unit/interfaces/dms/page-layout-query.test.js +12 -0
  169. package/dist/test/unit/interfaces/dms/page-layout-query.test.js.map +1 -1
  170. package/dist/test/unit/interfaces/dms/page-teardown.test.js +1 -1
  171. package/dist/test/unit/interfaces/dms/page-teardown.test.js.map +1 -1
  172. package/dist/test/unit/pages/settings/member-roles-field.test.js +1 -1
  173. package/dist/test/unit/pages/settings/member-roles-field.test.js.map +1 -1
  174. package/dist/test/unit/pages/settings/role-delete-confirm.test.js.map +1 -1
  175. package/dist/test/unit/realtime/menu-topics.test.d.ts +1 -0
  176. package/dist/test/unit/realtime/menu-topics.test.js +26 -0
  177. package/dist/test/unit/realtime/menu-topics.test.js.map +1 -0
  178. package/dist/test/unit/store/store.test.d.ts +1 -0
  179. package/dist/test/unit/store/store.test.js +127 -0
  180. package/dist/test/unit/store/store.test.js.map +1 -0
  181. package/dist/validation/store.schema.d.ts +39 -0
  182. package/dist/validation/store.schema.js +27 -0
  183. package/dist/validation/store.schema.js.map +1 -0
  184. package/frontend-vue/deferred-plugins.ts +3 -4
  185. package/frontend-vue/dms.email.ts +5 -1
  186. package/frontend-vue/dms.frontend.ts +15 -5
  187. package/frontend-vue/dms.server.ts +74 -0
  188. package/frontend-vue/layers/dms-auth/app/composables/useLogout.ts +1 -1
  189. package/frontend-vue/layers/dms-auth/app/composables/usePostLoginRedirect.ts +16 -3
  190. package/frontend-vue/layers/dms-auth/app/custom-pages/auth/2fa.vue +3 -1
  191. package/frontend-vue/layers/dms-auth/app/custom-pages/auth/backup.vue +3 -1
  192. package/frontend-vue/layers/dms-auth/app/custom-pages/auth/forgot-validation.vue +1 -1
  193. package/frontend-vue/layers/dms-auth/app/custom-pages/auth/oauth-complete.vue +26 -3
  194. package/frontend-vue/layers/dms-auth/app/custom-pages/auth/recover.vue +1 -1
  195. package/frontend-vue/layers/dms-auth/app/custom-pages/auth/signup.vue +22 -25
  196. package/frontend-vue/layers/dms-auth/app/custom-pages/auth/validate.vue +1 -1
  197. package/frontend-vue/layers/dms-auth/app/middleware/auth.ts +5 -7
  198. package/frontend-vue/layers/dms-auth/app/plugins/permissions-preload.ts +17 -0
  199. package/frontend-vue/layers/dms-auth/server/accounts.ts +135 -0
  200. package/frontend-vue/layers/dms-auth/server/actions.ts +251 -0
  201. package/frontend-vue/layers/dms-auth/server/oauth.ts +260 -0
  202. package/frontend-vue/layers/dms-auth/server/session.ts +158 -0
  203. package/frontend-vue/layers/dms-core/app/build/utils/asyncDataScope.ts +47 -0
  204. package/frontend-vue/layers/dms-core/app/build/utils/permission-preview.ts +6 -2
  205. package/frontend-vue/layers/dms-core/app/composables/auth/useApiData.ts +148 -0
  206. package/frontend-vue/layers/dms-core/app/composables/auth/useAuthFetch.ts +7 -20
  207. package/frontend-vue/layers/dms-core/app/composables/auth/useCurrentUser.ts +2 -2
  208. package/frontend-vue/layers/dms-core/app/composables/auth/useMultiAccount.ts +18 -5
  209. package/frontend-vue/layers/dms-core/app/composables/auth/usePermissions.ts +35 -6
  210. package/frontend-vue/layers/dms-core/app/composables/auth/useSessionRecovery.ts +1 -1
  211. package/frontend-vue/layers/dms-core/app/composables/period/compareResolvers.ts +20 -14
  212. package/frontend-vue/layers/dms-core/app/composables/period/periodClock.ts +121 -0
  213. package/frontend-vue/layers/dms-core/app/composables/period/periodRenderNow.ts +42 -0
  214. package/frontend-vue/layers/dms-core/app/composables/period/periodTimeZone.ts +31 -0
  215. package/frontend-vue/layers/dms-core/app/composables/period/presetResolvers.ts +48 -19
  216. package/frontend-vue/layers/dms-core/app/composables/period/usePeriod.ts +33 -4
  217. package/frontend-vue/layers/dms-core/app/composables/period/usePeriodScope.ts +78 -25
  218. package/frontend-vue/layers/dms-core/app/composables/realtime/usePageRealtime.ts +3 -0
  219. package/frontend-vue/layers/dms-core/app/composables/realtime/useUserRealtime.ts +36 -6
  220. package/frontend-vue/layers/dms-core/app/composables/user/useUserRegionalPreferences.ts +1 -1
  221. package/frontend-vue/layers/dms-core/app/plugins/async-data-scope.ts +26 -0
  222. package/frontend-vue/layers/dms-core/app/utils/pageRoutes.ts +22 -0
  223. package/frontend-vue/layers/dms-core/app/utils/regional.ts +36 -3
  224. package/frontend-vue/layers/dms-core/app/utils/trailingDeduper.ts +10 -1
  225. package/frontend-vue/layers/dms-core/shared/types/app-config.ts +48 -4
  226. package/frontend-vue/layers/dms-layout/app/app.config.ts +42 -23
  227. package/frontend-vue/layers/dms-layout/app/assets/css/main.css +72 -20
  228. package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardFrame.vue +9 -0
  229. package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardSidebar.vue +0 -3
  230. package/frontend-vue/layers/dms-layout/app/build/components/notification/NotificationPopover.vue +9 -3
  231. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleStoreDependencyPicker.vue +136 -0
  232. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleStoreRequirements.vue +98 -0
  233. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleStoreTile.vue +247 -29
  234. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleTile.vue +19 -2
  235. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/members/MemberRolePicker.vue +13 -12
  236. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/notification/NotificationInboxDisplay.vue +17 -7
  237. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/notification/NotificationPreferencesForm.vue +0 -1
  238. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/profile/ProfilePersonalInfo.vue +36 -33
  239. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/profile/ProfileSecuritySummary.vue +1 -2
  240. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/roles/role-types.ts +5 -4
  241. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/security/SecuritySessions.vue +35 -28
  242. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/security/SecurityStatus.vue +0 -1
  243. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/security/SecurityTwoFactor.vue +1 -1
  244. package/frontend-vue/layers/dms-layout/app/build/composables/dev-reload/useDevReloadHolder.ts +7 -6
  245. package/frontend-vue/layers/dms-layout/app/build/composables/layout/sidePanelState.ts +1 -1
  246. package/frontend-vue/layers/dms-layout/app/build/composables/navigation/useVisitNavBadges.ts +26 -0
  247. package/frontend-vue/layers/dms-layout/app/build/composables/notification/useNotificationCatalog.ts +25 -23
  248. package/frontend-vue/layers/dms-layout/app/build/composables/notification/useNotificationPreferences.ts +44 -23
  249. package/frontend-vue/layers/dms-layout/app/build/composables/security/useSessionHandoff.ts +1 -1
  250. package/frontend-vue/layers/dms-layout/app/build/composables/settings/profile/preferencesSummary.ts +1 -1
  251. package/frontend-vue/layers/dms-layout/app/build/composables/settings/profile/useAccountData.ts +3 -2
  252. package/frontend-vue/layers/dms-layout/app/build/composables/settings/profile/useProfileAvatar.ts +1 -18
  253. package/frontend-vue/layers/dms-layout/app/build/composables/settings/profile/useProfilePreferences.ts +19 -23
  254. package/frontend-vue/layers/dms-layout/app/build/composables/settings/security/useSecurityFormat.ts +6 -3
  255. package/frontend-vue/layers/dms-layout/app/build/composables/settings/security/useSecurityOverview.ts +13 -18
  256. package/frontend-vue/layers/dms-layout/app/build/utils/module-store-catalog.ts +186 -120
  257. package/frontend-vue/layers/dms-layout/app/build/utils/module-store-install.ts +146 -0
  258. package/frontend-vue/layers/dms-layout/app/build/utils/modules-catalog.ts +8 -6
  259. package/frontend-vue/layers/dms-layout/app/build/utils/publicErrorMessage.ts +19 -0
  260. package/frontend-vue/layers/dms-layout/app/components/navigation/CollapsedMenuItem.vue +0 -11
  261. package/frontend-vue/layers/dms-layout/app/components/navigation/NavigationMenu.vue +3 -13
  262. package/frontend-vue/layers/dms-layout/app/composables/general/useSystemState.ts +7 -11
  263. package/frontend-vue/layers/dms-layout/app/composables/notification/useNotifications.ts +103 -22
  264. package/frontend-vue/layers/dms-layout/app/composables/page/useModuleStore.ts +109 -0
  265. package/frontend-vue/layers/dms-layout/app/composables/page/useModulesListing.ts +25 -102
  266. package/frontend-vue/layers/dms-layout/app/composables/page/usePageComponentPreload.ts +0 -10
  267. package/frontend-vue/layers/dms-layout/app/composables/page/useSiteLayout.ts +177 -96
  268. package/frontend-vue/layers/dms-layout/app/composables/profile/useUserLanguage.ts +3 -2
  269. package/frontend-vue/layers/dms-layout/app/custom-pages/modules.vue +341 -62
  270. package/frontend-vue/layers/dms-layout/app/emails/components/EmailLayout.vue +31 -11
  271. package/frontend-vue/layers/dms-layout/app/error.vue +5 -2
  272. package/frontend-vue/layers/dms-layout/app/middleware/module-routing.global.ts +1 -1
  273. package/frontend-vue/layers/dms-layout/app/middleware/page-leave-guard.global.ts +2 -2
  274. package/frontend-vue/layers/dms-layout/app/pages/[...slug].vue +12 -9
  275. package/frontend-vue/layers/dms-layout/app/plugins/dms-menu-sync.client.ts +31 -3
  276. package/frontend-vue/layers/dms-layout/app/plugins/favicon.ts +58 -0
  277. package/frontend-vue/layers/dms-layout/app/plugins/notification-stream.client.ts +15 -7
  278. package/frontend-vue/layers/dms-layout/app/plugins/profile-sync.client.ts +5 -1
  279. package/frontend-vue/layers/dms-layout/app/plugins/regional-preferences.ts +10 -4
  280. package/frontend-vue/layers/dms-layout/app/plugins/time-zone-cookie.client.ts +12 -0
  281. package/frontend-vue/layers/dms-layout/app/types/module-store.ts +134 -0
  282. package/frontend-vue/layers/dms-layout/app/types/page.ts +13 -0
  283. package/frontend-vue/layers/dms-layout/i18n/locales/layout-en-GB.json +107 -9
  284. package/frontend-vue/layers/dms-layout/i18n/locales/layout-fr-FR.json +107 -9
  285. package/frontend-vue/layers/dms-layout/server/html-render.ts +58 -0
  286. package/frontend-vue/layers/dms-layout/server/render-token.ts +95 -0
  287. package/frontend-vue/layers/dms-onboarding/app/composables/onboarding/useOnboarding.ts +25 -21
  288. package/frontend-vue/layers/dms-onboarding/app/middleware/onboarding.global.ts +1 -1
  289. package/frontend-vue/layers/dms-onboarding/app/plugins/onboarding-preload.ts +9 -0
  290. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/registerDefaults.ts +4 -1
  291. package/frontend-vue/layers/dms-ui/app/build/composables/files/useFileMetadata.ts +162 -0
  292. package/frontend-vue/layers/dms-ui/app/build/composables/form/formRecordLoad.ts +23 -4
  293. package/frontend-vue/layers/dms-ui/app/build/composables/live/usePageDataRefresh.ts +101 -0
  294. package/frontend-vue/layers/dms-ui/app/build/composables/navigation/useNavBadges.ts +33 -6
  295. package/frontend-vue/layers/dms-ui/app/build/composables/period/periodDisplay.ts +19 -16
  296. package/frontend-vue/layers/dms-ui/app/build/composables/period/periodQuery.ts +40 -20
  297. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableFooter.ts +6 -8
  298. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableListData.ts +42 -14
  299. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableRows.ts +19 -8
  300. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViews.ts +1 -1
  301. package/frontend-vue/layers/dms-ui/app/components/chart/ChartCard.vue +5 -2
  302. package/frontend-vue/layers/dms-ui/app/components/flow-canvas/FlowCanvas.vue +9 -9
  303. package/frontend-vue/layers/dms-ui/app/components/form/Form.vue +20 -18
  304. package/frontend-vue/layers/dms-ui/app/components/form/components/Cascader.vue +2 -1
  305. package/frontend-vue/layers/dms-ui/app/components/form/components/File.vue +26 -132
  306. package/frontend-vue/layers/dms-ui/app/components/form/components/Image.vue +40 -105
  307. package/frontend-vue/layers/dms-ui/app/components/form/components/InputAddress.vue +2 -0
  308. package/frontend-vue/layers/dms-ui/app/components/form/components/Relation.vue +4 -4
  309. package/frontend-vue/layers/dms-ui/app/components/period/PeriodSelector.vue +9 -13
  310. package/frontend-vue/layers/dms-ui/app/components/table-view/CascaderPath.vue +2 -1
  311. package/frontend-vue/layers/dms-ui/app/components/table-view/FilePreview.vue +12 -24
  312. package/frontend-vue/layers/dms-ui/app/components/table-view/ImagePreview.vue +17 -29
  313. package/frontend-vue/layers/dms-ui/app/components/table-view/KanbanBoard.vue +7 -3
  314. package/frontend-vue/layers/dms-ui/app/components/table-view/TableView.vue +20 -8
  315. package/frontend-vue/layers/dms-ui/app/components/top-list/TopListCard.vue +8 -11
  316. package/frontend-vue/layers/dms-ui/app/composables/chart/useChartFetch.ts +146 -182
  317. package/frontend-vue/layers/dms-ui/app/composables/live/usePageLiveRefresh.ts +8 -2
  318. package/frontend-vue/layers/dms-ui/app/composables/useFileReadUrls.ts +11 -99
  319. package/frontend-vue/layers/dms-ui/app/utils/blockRefresh.ts +32 -3
  320. package/frontend-vue/package.json +3 -3
  321. package/frontend-vue/pnpm-lock.yaml +16 -22
  322. package/frontend-vue/server/private-options.ts +27 -0
  323. package/frontend-vue/tests/async-data-scope.test.ts +121 -0
  324. package/frontend-vue/tests/auth-middleware-error.test.ts +3 -3
  325. package/frontend-vue/tests/banner-block-fetch.test.ts +3 -0
  326. package/frontend-vue/tests/block-fetch-options-forwarding.test.ts +3 -0
  327. package/frontend-vue/tests/block-items-period-realtime.test.ts +3 -0
  328. package/frontend-vue/tests/block-not-found.test.ts +16 -2
  329. package/frontend-vue/tests/block-page-refresh.test.ts +3 -0
  330. package/frontend-vue/tests/block-route-params.test.ts +3 -0
  331. package/frontend-vue/tests/branding-defaults.test.ts +34 -0
  332. package/frontend-vue/tests/builtin-emails.test.ts +23 -2
  333. package/frontend-vue/tests/chart-live-pause.test.ts +4 -1
  334. package/frontend-vue/tests/chart-value-propagation.test.ts +4 -1
  335. package/frontend-vue/tests/composed-text-blocks.test.ts +3 -0
  336. package/frontend-vue/tests/email-layout.test.ts +66 -4
  337. package/frontend-vue/tests/error-page-message.test.ts +25 -2
  338. package/frontend-vue/tests/favicon.test.ts +131 -0
  339. package/frontend-vue/tests/file-fields-ssr.test.ts +145 -0
  340. package/frontend-vue/tests/file-read-urls.test.ts +145 -34
  341. package/frontend-vue/tests/file-unavailable.test.ts +5 -1
  342. package/frontend-vue/tests/fixtures/dataEngine.ts +44 -0
  343. package/frontend-vue/tests/form-load-error.test.ts +55 -1
  344. package/frontend-vue/tests/helpers/api-data-runtime.ts +77 -0
  345. package/frontend-vue/tests/image-hydration-ids.test.ts +21 -16
  346. package/frontend-vue/tests/image-reference-preservation.test.ts +59 -53
  347. package/frontend-vue/tests/input-number-adjust.test.ts +14 -0
  348. package/frontend-vue/tests/key-value-list-copy.test.ts +3 -0
  349. package/frontend-vue/tests/member-role-picker.test.ts +4 -2
  350. package/frontend-vue/tests/menu-sync-reconnect.test.ts +47 -0
  351. package/frontend-vue/tests/metadata-retry.test.ts +5 -1
  352. package/frontend-vue/tests/module-routing.test.ts +2 -5
  353. package/frontend-vue/tests/module-store-catalog.test.ts +270 -51
  354. package/frontend-vue/tests/modules-catalog.test.ts +17 -4
  355. package/frontend-vue/tests/modules-listing-transfer.test.ts +95 -0
  356. package/frontend-vue/tests/multi-account-validate.test.ts +23 -1
  357. package/frontend-vue/tests/nav-badge-tones.test.ts +66 -3
  358. package/frontend-vue/tests/notification-counts-from-writes.test.ts +223 -0
  359. package/frontend-vue/tests/notification-counts-transfer.test.ts +88 -0
  360. package/frontend-vue/tests/notification-inbox-display.test.ts +57 -5
  361. package/frontend-vue/tests/notification-popover-errors.test.ts +21 -1
  362. package/frontend-vue/tests/oauth-complete-handoff.test.ts +209 -0
  363. package/frontend-vue/tests/onboarding-middleware.test.ts +1 -1
  364. package/frontend-vue/tests/onboarding-transfer.test.ts +106 -0
  365. package/frontend-vue/tests/page-data-refresh.test.ts +141 -0
  366. package/frontend-vue/tests/page-fill-height.test.ts +16 -3
  367. package/frontend-vue/tests/page-no-visible-blocks.test.ts +12 -2
  368. package/frontend-vue/tests/page-realtime-remount.test.ts +1 -1
  369. package/frontend-vue/tests/page-record-label.test.ts +0 -7
  370. package/frontend-vue/tests/page-validation-redirect.test.ts +12 -2
  371. package/frontend-vue/tests/period-hydration-now.test.ts +185 -0
  372. package/frontend-vue/tests/period-live-pause.test.ts +3 -0
  373. package/frontend-vue/tests/period-reader-zone.test.ts +137 -0
  374. package/frontend-vue/tests/period-scope-first-fetch.test.ts +17 -1
  375. package/frontend-vue/tests/period-ssr.test.ts +195 -0
  376. package/frontend-vue/tests/permission-preview-partial.test.ts +0 -2
  377. package/frontend-vue/tests/permissions-transfer.test.ts +194 -0
  378. package/frontend-vue/tests/post-login-redirect.test.ts +10 -5
  379. package/frontend-vue/tests/profile-personal-info-ssr.test.ts +150 -0
  380. package/frontend-vue/tests/profile-preferences-summary.test.ts +15 -11
  381. package/frontend-vue/tests/realtime-reconnect.test.ts +22 -3
  382. package/frontend-vue/tests/roles-name-errors.test.ts +1 -1
  383. package/frontend-vue/tests/row-side-panel.test.ts +1 -1
  384. package/frontend-vue/tests/security-sessions-load-error.test.ts +3 -1
  385. package/frontend-vue/tests/server-routes-vs-pages.test.ts +26 -0
  386. package/frontend-vue/tests/server-routes.test.ts +582 -0
  387. package/frontend-vue/tests/session-handoff.test.ts +1 -1
  388. package/frontend-vue/tests/settings-reads-ssr.test.ts +232 -0
  389. package/frontend-vue/tests/side-panels.test.ts +8 -1
  390. package/frontend-vue/tests/sign-in-exit-history.test.ts +219 -0
  391. package/frontend-vue/tests/signup-invitation-link.test.ts +56 -8
  392. package/frontend-vue/tests/site-layout-categories.test.ts +42 -0
  393. package/frontend-vue/tests/site-layout-hydration.test.ts +3 -3
  394. package/frontend-vue/tests/site-layout-rendered.test.ts +103 -0
  395. package/frontend-vue/tests/site-layout-resync.test.ts +87 -0
  396. package/frontend-vue/tests/ssr-guard-preload.test.ts +188 -0
  397. package/frontend-vue/tests/table-cell-tones.test.ts +36 -1
  398. package/frontend-vue/tests/table-data-ssr.test.ts +195 -0
  399. package/frontend-vue/tests/table-list-data-requests.test.ts +154 -0
  400. package/frontend-vue/tests/table-retry-loading.test.ts +37 -0
  401. package/frontend-vue/tests/table-source-navigation.test.ts +0 -1
  402. package/frontend-vue/tests/table-source-period-scope.test.ts +0 -1
  403. package/frontend-vue/tests/table-view-views.test.ts +1 -1
  404. package/frontend-vue/tests/top-list-card-actions.test.ts +3 -0
  405. package/frontend-vue/tests/top-list-card-period-badge-ssr.test.ts +212 -0
  406. package/frontend-vue/tests/top-list-card-period-badge.test.ts +79 -0
  407. package/frontend-vue/tests/top-list-card-value-suffix.test.ts +3 -0
  408. package/frontend-vue/tests/trailing-deduper.test.ts +22 -0
  409. package/frontend-vue/tests/use-api-data-update.test.ts +119 -0
  410. package/frontend-vue/tests/use-api-data.test.ts +410 -0
  411. package/frontend-vue/tests/user-language.test.ts +8 -5
  412. package/frontend-vue/tests/widget-label-i18n.test.ts +4 -1
  413. package/frontend-vue/vitest.config.ts +6 -0
  414. package/package.json +2 -2
  415. package/skills/dms/SKILL.md +2 -1
  416. package/skills/dms-module/SKILL.md +1 -1
  417. package/skills/dms-pages/SKILL.md +4 -1
  418. package/frontend-vue/layers/dms-core/app/build/composables/page/usePageRecordLabel.ts +0 -9
  419. package/frontend-vue/layers/dms-layout/app/composables/page/usePrefetch.ts +0 -21
  420. package/frontend-vue/layers/dms-ui/app/build/composables/form/useControlError.ts +0 -10
  421. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useServerRenderedAsyncData.ts +0 -23
  422. package/frontend-vue/tests/control-error-path.test.ts +0 -11
  423. package/frontend-vue/tests/modules-listing-per-request.test.ts +0 -104
  424. package/frontend-vue/tests/page-layout-background-refresh.test.ts +0 -321
@@ -0,0 +1,154 @@
1
+ // @vitest-environment jsdom
2
+ /**
3
+ * The requests behind a table view's list and counters, through the
4
+ * runtime's real async data: a query rebuilt equal by value reads nothing
5
+ * again, one that changes reads again, and each request carries the run's
6
+ * abort signal, so a run replaced or dropped cancels its request.
7
+ */
8
+ import { computed, createApp, defineComponent, h, nextTick, ref } from "vue";
9
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
10
+ import type { TableViewColumn } from "../layers/dms-ui/app/composables/table-view/types";
11
+
12
+ const engine = await import("#dms/frontend-module");
13
+ vi.stubGlobal("computed", computed);
14
+ vi.stubGlobal("useDmsAsyncData", engine.useDmsAsyncData);
15
+
16
+ const { useTableRows } = await import(
17
+ "../layers/dms-ui/app/build/composables/table-view/useTableRows"
18
+ );
19
+ const { useTableCountsData, useTableListData } = await import(
20
+ "../layers/dms-ui/app/build/composables/table-view/useTableListData"
21
+ );
22
+
23
+ const LOCATION = "/api/orders";
24
+ const COLUMNS = [{ id: "total", accessorKey: "total" }] as TableViewColumn[];
25
+
26
+ interface Call {
27
+ url: string;
28
+ query?: Record<string, unknown>;
29
+ signal?: AbortSignal;
30
+ }
31
+
32
+ const calls: Call[] = [];
33
+ const api = (async (url: string, options: Omit<Call, "url"> = {}) => {
34
+ calls.push({ url, ...options });
35
+ return url.endsWith("/count/batch")
36
+ ? { all: 1 }
37
+ : { results: [], total: 0, offset: 0, limit: 10 };
38
+ }) as never;
39
+
40
+ const unmounts: Array<() => void> = [];
41
+
42
+ beforeEach(() => {
43
+ calls.length = 0;
44
+ });
45
+
46
+ afterEach(() => {
47
+ unmounts.splice(0).forEach((unmount) => unmount());
48
+ });
49
+
50
+ async function settle() {
51
+ await nextTick();
52
+ await new Promise((done) => setTimeout(done, 0));
53
+ await nextTick();
54
+ }
55
+
56
+ /** A table whose query is rebuilt on each `tick`, to `limit`'s value. */
57
+ async function mountTable() {
58
+ const tick = ref(0);
59
+ const limit = ref(10);
60
+ const Table = defineComponent({
61
+ setup() {
62
+ const rows = useTableRows({ api, location: LOCATION, columns: COLUMNS });
63
+ const keys = { componentId: "orders", pageId: "sales", rows };
64
+ const archiveQuery = computed(() => (void tick.value, {}));
65
+ useTableListData({
66
+ ...keys,
67
+ query: computed(() => (void tick.value, { limit: limit.value })),
68
+ archiveQuery,
69
+ isSelfManaged: computed(() => false),
70
+ });
71
+ useTableCountsData({
72
+ ...keys,
73
+ queries: computed(() => (void tick.value, [{ id: "all", query: {} }])),
74
+ archiveQuery,
75
+ });
76
+ return () => h("i");
77
+ },
78
+ });
79
+ const app = createApp(Table);
80
+ engine.provideDmsFrontendRuntime(
81
+ app,
82
+ engine.createDmsFrontendRuntime(undefined, {}),
83
+ );
84
+ app.mount(document.createElement("div"));
85
+ unmounts.push(() => app.unmount());
86
+ await settle();
87
+ return { tick, limit };
88
+ }
89
+
90
+ describe("a table view's list and counters", () => {
91
+ it("read nothing again for a query rebuilt equal", async () => {
92
+ const { tick } = await mountTable();
93
+ expect(calls.map(({ url }) => url).sort()).toEqual([
94
+ `${LOCATION}/count/batch`,
95
+ `${LOCATION}/list`,
96
+ ]);
97
+ calls.length = 0;
98
+
99
+ tick.value += 1;
100
+ await settle();
101
+
102
+ expect(calls).toEqual([]);
103
+ });
104
+
105
+ it("read the list again for a query that changes", async () => {
106
+ const { limit } = await mountTable();
107
+ calls.length = 0;
108
+
109
+ limit.value = 20;
110
+ await settle();
111
+
112
+ expect(calls.map(({ url, query }) => [url, query?.limit])).toEqual([
113
+ [`${LOCATION}/list`, 20],
114
+ ]);
115
+ });
116
+
117
+ it("send each request with its run's abort signal", async () => {
118
+ await mountTable();
119
+
120
+ expect(calls).toHaveLength(2);
121
+ for (const call of calls) expect(call.signal).toBeInstanceOf(AbortSignal);
122
+ });
123
+ });
124
+
125
+ describe("a source table's requests", () => {
126
+ const source = {
127
+ fetchUrl: "/api/report",
128
+ capabilities: {
129
+ filter: false,
130
+ search: false,
131
+ sort: false,
132
+ paginate: false,
133
+ },
134
+ };
135
+
136
+ it("carry the signal given to the list and the counters", async () => {
137
+ const rows = useTableRows({
138
+ api,
139
+ location: LOCATION,
140
+ columns: COLUMNS,
141
+ source: source as never,
142
+ });
143
+ const controller = new AbortController();
144
+ await rows.list({}, controller.signal).catch(() => undefined);
145
+ await rows
146
+ .countBatch([{ id: "all", query: {} }], controller.signal)
147
+ .catch(() => undefined);
148
+
149
+ expect(calls.map(({ signal }) => signal)).toEqual([
150
+ controller.signal,
151
+ controller.signal,
152
+ ]);
153
+ });
154
+ });
@@ -0,0 +1,37 @@
1
+ /**
2
+ * The renderer keeps a failed list's `error` while the next run is in flight,
3
+ * as Nuxt does. A table retrying its list (its retry button, a filter, a page)
4
+ * shows that run, its skeleton or the rows it has, rather than the error panel
5
+ * of the answer it may replace: the failure the table reads is the settled
6
+ * one.
7
+ */
8
+ import { readFileSync } from "node:fs";
9
+ import { resolve } from "node:path";
10
+ import { describe, expect, it } from "vitest";
11
+
12
+ const tableView = readFileSync(
13
+ resolve(__dirname, "../layers/dms-ui/app/components/table-view/TableView.vue"),
14
+ "utf8",
15
+ );
16
+
17
+ describe("a table retrying its list after a failure", () => {
18
+ it("reads the failure of settled runs only", () => {
19
+ expect(tableView).toMatch(
20
+ /const settledError = computed\(\(\) =>\s*status\.value === "pending" \? null : error\.value,?\s*\);/,
21
+ );
22
+ });
23
+
24
+ it("draws its skeleton and hides its error panel while the retry runs", () => {
25
+ const firstPage = tableView.slice(
26
+ tableView.indexOf("const isFirstPageLoading"),
27
+ );
28
+ expect(firstPage.slice(0, firstPage.indexOf(";"))).toMatch(
29
+ /!settledError\.value/,
30
+ );
31
+ const panel = tableView.slice(tableView.indexOf("const listLoadError"));
32
+ expect(panel.slice(0, panel.indexOf("});"))).toMatch(
33
+ /const failure = settledError\.value;\s*if \(!failure\) return undefined;/,
34
+ );
35
+ expect(panel.slice(0, panel.indexOf("});"))).not.toMatch(/error\.value/);
36
+ });
37
+ });
@@ -19,7 +19,6 @@ import type { TableViewColumn } from "../layers/dms-ui/app/composables/table-vie
19
19
 
20
20
  const engine = await import("#dms/frontend-module");
21
21
  vi.stubGlobal("useDmsAsyncData", engine.useDmsAsyncData);
22
- vi.stubGlobal("useDmsLazyAsyncData", engine.useDmsLazyAsyncData);
23
22
 
24
23
  const { useTableRows } = await import(
25
24
  "../layers/dms-ui/app/build/composables/table-view/useTableRows"
@@ -19,7 +19,6 @@ import type { TableViewColumn } from "../layers/dms-ui/app/composables/table-vie
19
19
 
20
20
  const engine = await import("#dms/frontend-module");
21
21
  vi.stubGlobal("useDmsAsyncData", engine.useDmsAsyncData);
22
- vi.stubGlobal("useDmsLazyAsyncData", engine.useDmsLazyAsyncData);
23
22
 
24
23
  const { useTableRows } = await import(
25
24
  "../layers/dms-ui/app/build/composables/table-view/useTableRows"
@@ -228,7 +228,7 @@ describe("useTableViews", () => {
228
228
  });
229
229
  },
230
230
  };
231
- vi.stubGlobal("useUserSession", () => ({
231
+ vi.stubGlobal("useDmsUserSession", () => ({
232
232
  user: ref({ _id: options.userId ?? "u1" }),
233
233
  }));
234
234
  vi.stubGlobal("useDmsRoute", () => ({ query: options.query ?? {} }));
@@ -6,6 +6,7 @@
6
6
  import { createSSRApp, defineComponent, h, ref } from "vue";
7
7
  import { renderToString } from "vue/server-renderer";
8
8
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
9
+ import { useRealDataEngine } from "./fixtures/dataEngine";
9
10
  import TopListCard from "../layers/dms-ui/app/components/top-list/TopListCard.vue";
10
11
 
11
12
  vi.mock("../layers/dms-ui/app/composables/chart/useChartFetch", () => ({
@@ -55,6 +56,8 @@ async function render(props: Record<string, unknown>): Promise<string> {
55
56
  return renderToString(app);
56
57
  }
57
58
 
59
+ beforeEach(() => useRealDataEngine());
60
+
58
61
  beforeEach(() => {
59
62
  vi.stubGlobal("useI18n", () => ({ locale: ref("en-US"), t: String }));
60
63
  vi.stubGlobal("useTranslation", () => ({
@@ -0,0 +1,212 @@
1
+ // @vitest-environment jsdom
2
+ /**
3
+ * A TopListCard's period badge across a server render and its hydration,
4
+ * through the runtime's real payload: once its selector set up before it, the
5
+ * server prints the preset whatever it knows of the browser's time zone (a
6
+ * preset reads the same in every zone, and a relative one at every instant),
7
+ * and the browser hydrates it without a mismatch. A selector further down the
8
+ * page leaves a skeleton on both sides, which the label replaces once mounted.
9
+ */
10
+ import {
11
+ createSSRApp,
12
+ defineComponent,
13
+ h,
14
+ nextTick,
15
+ onBeforeUnmount,
16
+ ref,
17
+ type App,
18
+ type Component,
19
+ } from "vue";
20
+ import { renderToString } from "vue/server-renderer";
21
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
22
+ import type { PeriodPreset } from "../layers/dms-core/app/composables/period/types";
23
+ import { browserTimeZone } from "../layers/dms-core/app/utils/regional";
24
+ import TopListCard from "../layers/dms-ui/app/components/top-list/TopListCard.vue";
25
+
26
+ vi.mock("../layers/dms-ui/app/composables/chart/useChartFetch", () => ({
27
+ useChartFetch: () => ({
28
+ data: ref({ items: [{ id: "pro", title: "Pro", value: 12 }] }),
29
+ isLoading: ref(false),
30
+ }),
31
+ }));
32
+
33
+ const engine = await import("#dms/frontend-module");
34
+ const { usePeriod } = await import(
35
+ "../layers/dms-core/app/composables/period/usePeriod"
36
+ );
37
+ const { registerPeriodScope } = await import(
38
+ "../layers/dms-core/app/composables/period/usePeriodScope"
39
+ );
40
+ const { TIME_ZONE_COOKIE } = await import(
41
+ "../layers/dms-core/app/composables/period/periodTimeZone"
42
+ );
43
+
44
+ const SCOPE = "dashboard";
45
+ const SKELETON = "SKELETON";
46
+
47
+ function selector(preset: PeriodPreset): Component {
48
+ return defineComponent({
49
+ setup() {
50
+ const period = usePeriod({ defaultPreset: preset });
51
+ onBeforeUnmount(registerPeriodScope(SCOPE, period.state));
52
+ return () => h("i");
53
+ },
54
+ });
55
+ }
56
+
57
+ // A skeleton carries an aria-label; the badge renders its label.
58
+ const Slot = defineComponent({
59
+ props: { ariaLabel: String },
60
+ setup:
61
+ (props, { slots }) =>
62
+ () =>
63
+ h("span", props.ariaLabel ? SKELETON : slots.default?.()),
64
+ });
65
+
66
+ const Card = defineComponent({
67
+ setup: () => () =>
68
+ h(TopListCard, {
69
+ title: "Top plans",
70
+ componentId: "plans",
71
+ pageId: "dashboard",
72
+ fetchUrl: "/api/plans/top",
73
+ periodScope: SCOPE,
74
+ }),
75
+ });
76
+
77
+ function page(children: Component[]): App {
78
+ const app = createSSRApp({
79
+ render: () =>
80
+ h(
81
+ "main",
82
+ children.map((child) => h(child)),
83
+ ),
84
+ });
85
+ for (const name of ["DmsCard", "UBadge", "USkeleton", "DmsTopListRow"])
86
+ app.component(name, Slot);
87
+ return app;
88
+ }
89
+
90
+ function zoneCookie(zone: string): string {
91
+ return `${TIME_ZONE_COOKIE}=${encodeURIComponent(JSON.stringify(zone))}`;
92
+ }
93
+
94
+ async function serverRender(children: Component[], cookies?: string) {
95
+ const runtime = engine.createDmsFrontendRuntime(undefined, {}, true, cookies);
96
+ const app = page(children);
97
+ engine.provideDmsFrontendRuntime(app, runtime);
98
+ engine.setDmsServerRuntimeResolver(() => runtime);
99
+ // What the server sees: no `window`.
100
+ const browserWindow = globalThis.window;
101
+ vi.stubGlobal("window", undefined);
102
+ try {
103
+ const html = await renderToString(app);
104
+ return { html, payload: engine.serializeDmsPayload(runtime) };
105
+ } finally {
106
+ vi.stubGlobal("window", browserWindow);
107
+ }
108
+ }
109
+
110
+ const mounted: App[] = [];
111
+
112
+ async function hydrate(
113
+ children: Component[],
114
+ { html, payload }: { html: string; payload: unknown },
115
+ ) {
116
+ const browser = engine.createDmsFrontendRuntime(undefined, {});
117
+ engine.restoreDmsPayload(
118
+ browser,
119
+ structuredClone(payload) as Parameters<typeof engine.restoreDmsPayload>[1],
120
+ );
121
+ const container = document.createElement("div");
122
+ container.innerHTML = html;
123
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
124
+ const error = vi.spyOn(console, "error").mockImplementation(() => {});
125
+ try {
126
+ const app = page(children);
127
+ engine.provideDmsFrontendRuntime(app, browser);
128
+ app.mount(container);
129
+ mounted.push(app);
130
+ await nextTick();
131
+ const mismatches = [...warn.mock.calls, ...error.mock.calls].filter(
132
+ (call) => /hydration|mismatch/i.test(String(call[0])),
133
+ );
134
+ return { container, mismatches };
135
+ } finally {
136
+ warn.mockRestore();
137
+ error.mockRestore();
138
+ }
139
+ }
140
+
141
+ /** The card head's badge: its preset label, or its skeleton. */
142
+ function badge(html: string): string {
143
+ const label = /dms\.period\.presets\.[a-z0-9-]+/.exec(html)?.[0];
144
+ if (label) return label;
145
+ return html.includes(SKELETON) ? SKELETON : "none";
146
+ }
147
+
148
+ beforeEach(() => {
149
+ vi.stubGlobal("useI18n", () => ({ locale: ref("en-US"), t: String }));
150
+ vi.stubGlobal("useTranslation", () => ({
151
+ processI18n: (text: string) => text,
152
+ }));
153
+ vi.stubGlobal("useComponentEvent", () => ({}));
154
+ vi.stubGlobal("useWatch", () => ({ state: ref({}) }));
155
+ vi.stubGlobal("useDmsState", engine.useDmsState);
156
+ vi.stubGlobal("useDmsCookie", engine.useDmsCookie);
157
+ });
158
+
159
+ afterEach(() => {
160
+ mounted.splice(0).forEach((app) => app.unmount());
161
+ vi.unstubAllGlobals();
162
+ });
163
+
164
+ describe("a TopListCard's period badge across hydration", () => {
165
+ it("is in the server's HTML, and hydrates without a mismatch", async () => {
166
+ const children = [selector("this-month"), Card];
167
+ const rendered = await serverRender(
168
+ children,
169
+ zoneCookie(browserTimeZone()),
170
+ );
171
+ expect(badge(rendered.html)).toBe("dms.period.presets.this-month");
172
+
173
+ const { container, mismatches } = await hydrate(children, rendered);
174
+ expect(mismatches).toEqual([]);
175
+ expect(badge(container.innerHTML)).toBe("dms.period.presets.this-month");
176
+ });
177
+
178
+ it("is rendered before the browser's time zone is known", async () => {
179
+ const children = [selector("this-month"), Card];
180
+ const rendered = await serverRender(children);
181
+ expect(badge(rendered.html)).toBe("dms.period.presets.this-month");
182
+
183
+ const { mismatches } = await hydrate(children, rendered);
184
+ expect(mismatches).toEqual([]);
185
+ });
186
+
187
+ it("names a relative preset too", async () => {
188
+ const children = [selector("last-24h"), Card];
189
+ const rendered = await serverRender(
190
+ children,
191
+ zoneCookie(browserTimeZone()),
192
+ );
193
+ expect(badge(rendered.html)).toBe("dms.period.presets.last-24h");
194
+
195
+ const { mismatches } = await hydrate(children, rendered);
196
+ expect(mismatches).toEqual([]);
197
+ });
198
+
199
+ it("waits for a selector set up after the card, on both sides", async () => {
200
+ const children = [Card, selector("last-year")];
201
+ const rendered = await serverRender(
202
+ children,
203
+ zoneCookie(browserTimeZone()),
204
+ );
205
+ expect(badge(rendered.html)).toBe(SKELETON);
206
+
207
+ const { container, mismatches } = await hydrate(children, rendered);
208
+ expect(mismatches).toEqual([]);
209
+ await nextTick();
210
+ expect(badge(container.innerHTML)).toBe("dms.period.presets.last-year");
211
+ });
212
+ });
@@ -0,0 +1,79 @@
1
+ /**
2
+ * A TopListCard tied to a period selector names the selector's preset in its
3
+ * head. The preset is the same name on both sides: the server prints it, and
4
+ * the browser hydrates it, rather than a skeleton until the card mounts.
5
+ */
6
+ import { computed, createSSRApp, defineComponent, h, ref } from "vue";
7
+ import { renderToString } from "vue/server-renderer";
8
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
9
+ import { useRealDataEngine } from "./fixtures/dataEngine";
10
+ import { registerPeriodScope } from "../layers/dms-core/app/composables/period/usePeriodScope";
11
+ import type { PeriodState } from "../layers/dms-core/app/composables/period/types";
12
+ import TopListCard from "../layers/dms-ui/app/components/top-list/TopListCard.vue";
13
+
14
+ vi.mock("../layers/dms-ui/app/composables/chart/useChartFetch", () => ({
15
+ useChartFetch: () => ({
16
+ data: ref({ items: [{ id: "pro", title: "Pro", value: 12 }] }),
17
+ isLoading: ref(false),
18
+ }),
19
+ }));
20
+
21
+ const Tagged = (tag: string) =>
22
+ defineComponent({
23
+ setup:
24
+ (_, { slots }) =>
25
+ () =>
26
+ h("div", { [`data-${tag}`]: "" }, slots.default?.()),
27
+ });
28
+
29
+ async function render(beforeCard?: () => void): Promise<string> {
30
+ const app = createSSRApp({
31
+ setup() {
32
+ beforeCard?.();
33
+ return () =>
34
+ h(TopListCard, {
35
+ title: "Top plans",
36
+ componentId: "plans",
37
+ pageId: "dashboard",
38
+ periodScope: "dashboard-period",
39
+ });
40
+ },
41
+ });
42
+ app.component("DmsCard", Tagged("card"));
43
+ app.component("UBadge", Tagged("badge"));
44
+ app.component("USkeleton", Tagged("skeleton"));
45
+ app.component("UIcon", Tagged("icon"));
46
+ app.component("DmsTopListRow", Tagged("row"));
47
+ return renderToString(app);
48
+ }
49
+
50
+ beforeEach(() => {
51
+ useRealDataEngine();
52
+ vi.stubGlobal("useI18n", () => ({ locale: ref("en-US"), t: String }));
53
+ vi.stubGlobal("useTranslation", () => ({
54
+ processI18n: (text: string) => text,
55
+ }));
56
+ vi.stubGlobal("useComponentEvent", () => ({}));
57
+ vi.stubGlobal("useWatch", () => ({ state: ref({}) }));
58
+ });
59
+
60
+ afterEach(() => vi.unstubAllGlobals());
61
+
62
+ describe("TopListCard period badge", () => {
63
+ it("prints the preset of a selector set up before it", async () => {
64
+ const html = await render(() => {
65
+ registerPeriodScope(
66
+ "dashboard-period",
67
+ computed(() => ({ preset: "last_30_days" }) as PeriodState),
68
+ );
69
+ });
70
+ expect(html).toContain("dms.period.presets.last_30_days");
71
+ expect(html).not.toContain("data-skeleton");
72
+ });
73
+
74
+ it("holds the badge's place while no selector is known", async () => {
75
+ const html = await render();
76
+ expect(html).toContain("data-skeleton");
77
+ expect(html).not.toContain("data-badge");
78
+ });
79
+ });
@@ -5,6 +5,7 @@
5
5
  import { createSSRApp, defineComponent, h, ref } from "vue";
6
6
  import { renderToString } from "vue/server-renderer";
7
7
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
8
+ import { useRealDataEngine } from "./fixtures/dataEngine";
8
9
  import TopListCard from "../layers/dms-ui/app/components/top-list/TopListCard.vue";
9
10
 
10
11
  const rows = ref<Array<Record<string, unknown>>>([]);
@@ -55,6 +56,8 @@ function values(html: string): string[] {
55
56
  );
56
57
  }
57
58
 
59
+ beforeEach(() => useRealDataEngine());
60
+
58
61
  beforeEach(() => {
59
62
  rows.value = [
60
63
  { id: "a", title: "GET /orders", value: 1864 },
@@ -71,4 +71,26 @@ describe("createTrailingDeduper", () => {
71
71
 
72
72
  expect(task).toHaveBeenCalledTimes(2);
73
73
  });
74
+
75
+ it("lets a join share the run in flight without queueing a pass", async () => {
76
+ const first = deferred();
77
+ const task = vi.fn(() => first.promise);
78
+ const deduper = createTrailingDeduper();
79
+
80
+ const inFlight = deduper.run(task);
81
+ const joined = deduper.join(task);
82
+ first.resolve();
83
+ await Promise.all([inFlight, joined]);
84
+
85
+ expect(task).toHaveBeenCalledTimes(1);
86
+ });
87
+
88
+ it("starts a run for a join when nothing is in flight", async () => {
89
+ const task = vi.fn(async () => undefined);
90
+ const deduper = createTrailingDeduper();
91
+
92
+ await deduper.join(task);
93
+
94
+ expect(task).toHaveBeenCalledTimes(1);
95
+ });
74
96
  });
@@ -0,0 +1,119 @@
1
+ // @vitest-environment jsdom
2
+ /**
3
+ * Updating what `useApiData` read, through the runtime's real async data:
4
+ * `useDmsData(key)` puts a write's answer in place (and back when the write
5
+ * fails) or applies a realtime change, in every component showing the read,
6
+ * without reading it again. The URL form names its key with `key`.
7
+ */
8
+ import { createApp, defineComponent, h, nextTick, type App } from "vue";
9
+ import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from "vitest";
10
+
11
+ interface Task {
12
+ id: string;
13
+ title: string;
14
+ }
15
+
16
+ const engine = await import("#dms/frontend-module");
17
+ vi.stubGlobal("useDmsFetch", engine.useDmsFetch);
18
+ vi.stubGlobal("useDmsAsyncData", engine.useDmsAsyncData);
19
+ const requests: string[] = [];
20
+ const backend: Record<string, Task[]> = {
21
+ "/api/tasks": [{ id: "1", title: "Write" }],
22
+ "/api/archive": [{ id: "9", title: "Old" }],
23
+ };
24
+ vi.stubGlobal("useAuthFetch", () => ({
25
+ $authFetch: async (url: string) => {
26
+ requests.push(url);
27
+ return structuredClone(backend[url]);
28
+ },
29
+ }));
30
+ const { useApiData } = await import(
31
+ "../layers/dms-core/app/composables/auth/useApiData"
32
+ );
33
+
34
+ const settle = () => new Promise((done) => setTimeout(done, 10));
35
+ const apps: App[] = [];
36
+
37
+ function mount(setup: () => () => ReturnType<typeof h>): HTMLElement {
38
+ const runtime = engine.createDmsFrontendRuntime(undefined, {});
39
+ const app = createApp(defineComponent({ setup }));
40
+ engine.provideDmsFrontendRuntime(app, runtime);
41
+ const element = document.createElement("div");
42
+ app.mount(element);
43
+ apps.push(app);
44
+ return element;
45
+ }
46
+
47
+ const titles = (tasks: Task[] | null | undefined) =>
48
+ (tasks ?? []).map((task) => task.title).join(",");
49
+
50
+ beforeEach(() => {
51
+ requests.length = 0;
52
+ });
53
+
54
+ afterEach(() => {
55
+ apps.splice(0).forEach((app) => app.unmount());
56
+ });
57
+
58
+ afterAll(() => vi.unstubAllGlobals());
59
+
60
+ describe("useDmsData on a useApiData read", () => {
61
+ it("puts a write's answer in place, and back when the write fails, without reading again", async () => {
62
+ let tasks: ReturnType<typeof engine.useDmsData<Task[]>> | undefined;
63
+ const element = mount(() => {
64
+ const list = useApiData<Task[]>("/api/tasks", { key: "tasks" });
65
+ const other = useApiData<Task[]>("/api/tasks", { key: "tasks" });
66
+ tasks = engine.useDmsData<Task[]>("tasks");
67
+ return () =>
68
+ h("p", `${titles(list.data.value)}|${titles(other.data.value)}`);
69
+ });
70
+ await settle();
71
+ expect(element.textContent).toBe("Write|Write");
72
+
73
+ const previous = tasks!.data.value!;
74
+ tasks!.data.value = [...previous, { id: "2", title: "Review" }];
75
+ await nextTick();
76
+ expect(element.textContent).toBe("Write,Review|Write,Review");
77
+
78
+ // The write failed: what was there comes back.
79
+ tasks!.data.value = previous;
80
+ await nextTick();
81
+ expect(element.textContent).toBe("Write|Write");
82
+ expect(requests).toEqual(["/api/tasks"]);
83
+ });
84
+
85
+ it("applies a realtime change to a read another component shows", async () => {
86
+ const Archive = defineComponent({
87
+ setup() {
88
+ // In place changes need a deep read: reads are shallow by default.
89
+ const { data } = useApiData<Task[]>(
90
+ "task-archive",
91
+ ({ $authFetch, signal }) =>
92
+ $authFetch<Task[]>("/api/archive", { signal }),
93
+ { deep: true },
94
+ );
95
+ return () => h("p", titles(data.value));
96
+ },
97
+ });
98
+ let archive: ReturnType<typeof engine.useDmsData<Task[]>> | undefined;
99
+ const element = mount(() => {
100
+ // A realtime listener of the page, which reads nothing itself.
101
+ archive = engine.useDmsData<Task[]>("task-archive");
102
+ return () => h("div", [h(Archive)]);
103
+ });
104
+ expect(archive!.data.value).toBeUndefined();
105
+ await settle();
106
+ expect(element.textContent).toBe("Old");
107
+
108
+ // The event names the task it renamed.
109
+ archive!.data.value = archive!.data.value!.map((task) =>
110
+ task.id === "9" ? { ...task, title: "Renamed" } : task,
111
+ );
112
+ await nextTick();
113
+ expect(element.textContent).toBe("Renamed");
114
+ archive!.data.value![0]!.title = "Renamed again";
115
+ await nextTick();
116
+ expect(element.textContent).toBe("Renamed again");
117
+ expect(requests).toEqual(["/api/archive"]);
118
+ });
119
+ });