@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
@@ -3,8 +3,16 @@ import { createSSRApp, defineComponent, h } from "vue";
3
3
  import { renderToString } from "vue/server-renderer";
4
4
  import EmailLayout from "../layers/dms-layout/app/emails/components/EmailLayout.vue";
5
5
  import { emailLogoUrl } from "../layers/dms-layout/app/emails/components/index";
6
+ import type { DmsLogoSources } from "../layers/dms-core/shared/types/app-config";
7
+
8
+ /** `branding.logo` as an email reads it: a path stands for `default.light`. */
9
+ interface EmailLogos {
10
+ default?: Partial<DmsLogoSources>;
11
+ email?: Partial<DmsLogoSources>;
12
+ }
6
13
 
7
14
  const PUBLIC_BASE = "https://admin.example.com";
15
+ const DARK_SOURCE_MEDIA = 'media="(prefers-color-scheme: dark)"';
8
16
  const ELEMENTS: Record<string, string> = {
9
17
  EHtml: "html",
10
18
  EHead: "head",
@@ -29,12 +37,11 @@ function element(tag: string) {
29
37
 
30
38
  async function renderLayout(
31
39
  props: Record<string, unknown>,
32
- logo = "/images/logo.png",
40
+ logo: string | EmailLogos = "/images/logo.png",
33
41
  clientBaseUrl = PUBLIC_BASE,
34
42
  ): Promise<string> {
35
- vi.stubGlobal("useDmsAppConfig", () => ({
36
- branding: { logo: { default: { light: logo } } },
37
- }));
43
+ const logos = typeof logo === "string" ? { default: { light: logo } } : logo;
44
+ vi.stubGlobal("useDmsAppConfig", () => ({ branding: { logo: logos } }));
38
45
  vi.stubGlobal("useDmsRuntimeConfig", () => ({
39
46
  public: { dms: { clientBaseUrl } },
40
47
  }));
@@ -92,6 +99,61 @@ describe("EmailLayout", () => {
92
99
  });
93
100
  });
94
101
 
102
+ describe("EmailLayout email logo", () => {
103
+ const guarded = { title: "Hi", publicLogoOnly: true };
104
+
105
+ it("draws the email logo in place of the default one", async () => {
106
+ const html = await renderLayout(guarded, {
107
+ default: { light: "/images/logo.svg" },
108
+ email: { light: "/branding/email.png" },
109
+ });
110
+ expect(html).toContain(
111
+ `<img src="${PUBLIC_BASE}/branding/email.png" alt="Logo">`,
112
+ );
113
+ expect(html).not.toContain("<picture>");
114
+ });
115
+
116
+ it("offers the dark email logo to the clients in dark mode", async () => {
117
+ const html = await renderLayout(guarded, {
118
+ email: { light: "/branding/email.png", dark: "/branding/email-dark.png" },
119
+ });
120
+ expect(html).toContain(
121
+ `<picture><source ${DARK_SOURCE_MEDIA} srcset="${PUBLIC_BASE}/branding/email-dark.png"><img src="${PUBLIC_BASE}/branding/email.png" alt="Logo"></picture>`,
122
+ );
123
+ });
124
+
125
+ it("keeps a single image when the dark logo is the same or unusable", async () => {
126
+ for (const dark of ["/branding/email.png", "/branding/email-dark.svg"]) {
127
+ const html = await renderLayout(guarded, {
128
+ email: { light: "/branding/email.png", dark },
129
+ });
130
+ expect(html).not.toContain("<picture>");
131
+ expect(html).toContain(
132
+ `<img src="${PUBLIC_BASE}/branding/email.png" alt="Logo">`,
133
+ );
134
+ }
135
+ });
136
+
137
+ it("draws no dark logo without a light one", async () => {
138
+ const html = await renderLayout(guarded, {
139
+ default: { light: "/images/logo.svg" },
140
+ email: { light: "", dark: "/branding/email-dark.png" },
141
+ });
142
+ expect(html).not.toContain("<img");
143
+ expect(html).not.toContain("<source");
144
+ });
145
+
146
+ it("falls back to the default logo without an email one", async () => {
147
+ const html = await renderLayout(guarded, {
148
+ default: { light: "/images/logo.png", dark: "/images/logo-dark.png" },
149
+ });
150
+ expect(html).not.toContain("<picture>");
151
+ expect(html).toContain(
152
+ `<img src="${PUBLIC_BASE}/images/logo.png" alt="Logo">`,
153
+ );
154
+ });
155
+ });
156
+
95
157
  describe("emailLogoUrl", () => {
96
158
  it("resolves a root path against the public base URL", () => {
97
159
  expect(emailLogoUrl("/logo.png", PUBLIC_BASE)).toBe(
@@ -61,9 +61,9 @@ function installRuntime() {
61
61
  }));
62
62
  }
63
63
 
64
- function mountError(statusCode: number) {
64
+ function mountError(statusCode: number, message = TECHNICAL_MESSAGE) {
65
65
  app = createApp(ErrorPage, {
66
- error: { statusCode, message: TECHNICAL_MESSAGE },
66
+ error: { statusCode, message },
67
67
  });
68
68
  app.config.globalProperties.$t = (key: string) => key;
69
69
  app.component("DmsAppLogo", Passthrough);
@@ -96,3 +96,26 @@ it.each([HTTP_NOT_FOUND, HTTP_FORBIDDEN])(
96
96
  it("still shows the message of an unexpected error", () => {
97
97
  expect(mountError(500)).toContain(TECHNICAL_MESSAGE);
98
98
  });
99
+
100
+ it("never shows the backend's address a failed request names", () => {
101
+ const text = mountError(
102
+ 502,
103
+ '[GET] "http://10.0.0.4:5010/dms/page?path=%2Fprojects": 502 Bad Gateway',
104
+ );
105
+ expect(text).toContain("502 Bad Gateway");
106
+ expect(text).not.toContain("10.0.0.4");
107
+ expect(text).not.toContain("/dms/page");
108
+ });
109
+
110
+ it("drops an address anywhere in the message, and an empty remainder", async () => {
111
+ const { publicErrorMessage } = await import(
112
+ "../layers/dms-layout/app/build/utils/publicErrorMessage"
113
+ );
114
+ expect(
115
+ publicErrorMessage(
116
+ "fetch failed: connect ECONNREFUSED http://127.0.0.1:5010/api",
117
+ ),
118
+ ).toBe("fetch failed: connect ECONNREFUSED");
119
+ expect(publicErrorMessage("http://127.0.0.1:5010/api")).toBeUndefined();
120
+ expect(publicErrorMessage(undefined)).toBeUndefined();
121
+ });
@@ -0,0 +1,131 @@
1
+ // @vitest-environment jsdom
2
+ /**
3
+ * The favicon plugin: the `<link rel="icon">` tags it declares from
4
+ * `branding.logo.favicon`, their type and color-scheme media, and the icon
5
+ * following an app config the loader replaces in development.
6
+ */
7
+ import { useHead } from "@unhead/vue";
8
+ import {
9
+ createHead as createClientHead,
10
+ renderDOMHead,
11
+ } from "@unhead/vue/client";
12
+ import { createHead, renderSSRHead } from "@unhead/vue/server";
13
+ import { createApp, createSSRApp, nextTick, ref } from "vue";
14
+ import { beforeEach, describe, expect, it, vi } from "vitest";
15
+ import layoutAppConfig from "../layers/dms-layout/app/app.config";
16
+ import type { DmsLogoSources } from "../layers/dms-core/shared/types/app-config";
17
+
18
+ interface FaviconAppConfig {
19
+ branding: { logo?: { favicon?: Partial<DmsLogoSources> } };
20
+ }
21
+
22
+ const ANTELOPE_ICON = "/images/antelope-logo/icon.svg";
23
+ const ICON_TAG_START = '<link rel="icon"';
24
+ // The loader keeps the merged app config in a ref and swaps it on a
25
+ // development update; `useDmsAppConfig()` returns its current value.
26
+ const appConfig = ref<FaviconAppConfig>({ branding: {} });
27
+
28
+ vi.stubGlobal("defineDmsPlugin", (setup: unknown) => setup);
29
+ vi.stubGlobal("useHead", useHead);
30
+ vi.stubGlobal("useDmsAppConfig", () => appConfig.value);
31
+
32
+ const { default: faviconPlugin } = await import(
33
+ "../layers/dms-layout/app/plugins/favicon"
34
+ );
35
+
36
+ function useFavicon(favicon?: Partial<DmsLogoSources>): void {
37
+ appConfig.value = { branding: { logo: { favicon } } };
38
+ }
39
+
40
+ // unhead adds its own charset and viewport tags around them.
41
+ async function renderIconTags(): Promise<string[]> {
42
+ const app = createSSRApp({ render: () => null });
43
+ const head = createHead();
44
+ app.use(head);
45
+ app.runWithContext(() => faviconPlugin({} as never));
46
+ const { headTags } = await renderSSRHead(head);
47
+ return headTags.split("\n").filter((tag) => tag.startsWith(ICON_TAG_START));
48
+ }
49
+
50
+ beforeEach(() => {
51
+ appConfig.value = { branding: {} };
52
+ document.head.innerHTML = "";
53
+ });
54
+
55
+ describe("server render", () => {
56
+ it("declares the DMS's collapsed icon by default", async () => {
57
+ appConfig.value = layoutAppConfig;
58
+
59
+ expect(await renderIconTags()).toEqual([
60
+ `<link rel="icon" href="${ANTELOPE_ICON}" type="image/svg+xml">`,
61
+ ]);
62
+ });
63
+
64
+ it("declares a light and a dark icon that follow the system scheme", async () => {
65
+ useFavicon({
66
+ light: "/branding/icon-light.png",
67
+ dark: "/branding/icon-dark.png",
68
+ });
69
+
70
+ expect(await renderIconTags()).toEqual([
71
+ '<link rel="icon" href="/branding/icon-light.png" type="image/png" media="(prefers-color-scheme: light)">',
72
+ '<link rel="icon" href="/branding/icon-dark.png" type="image/png" media="(prefers-color-scheme: dark)">',
73
+ ]);
74
+ });
75
+
76
+ it.each([
77
+ ["/favicon.ico", "image/x-icon"],
78
+ ["/branding/icon.WEBP", "image/webp"],
79
+ ["https://cdn.example.com/icon.svg?v=2", "image/svg+xml"],
80
+ ])("types %s as %s", async (icon, type) => {
81
+ useFavicon({ light: icon, dark: icon });
82
+
83
+ expect(await renderIconTags()).toEqual([
84
+ `<link rel="icon" href="${icon}" type="${type}">`,
85
+ ]);
86
+ });
87
+
88
+ it("leaves the type out for an extension it does not know", async () => {
89
+ useFavicon({ light: "/icon", dark: "/icon" });
90
+
91
+ expect(await renderIconTags()).toEqual(['<link rel="icon" href="/icon">']);
92
+ });
93
+
94
+ it("declares the one file it has when the other mode has none", async () => {
95
+ useFavicon({ light: "/icon.png", dark: "" });
96
+
97
+ expect(await renderIconTags()).toEqual([
98
+ '<link rel="icon" href="/icon.png" type="image/png">',
99
+ ]);
100
+ });
101
+
102
+ it("declares no icon without a favicon, leaving the loader's placeholder", async () => {
103
+ expect(await renderIconTags()).toEqual([]);
104
+ });
105
+ });
106
+
107
+ describe("in the browser", () => {
108
+ it("swaps the icon when the app config is replaced", async () => {
109
+ useFavicon({ light: "/before.png", dark: "/before.png" });
110
+ const app = createApp({ render: () => null });
111
+ const head = createClientHead();
112
+ app.use(head);
113
+ app.runWithContext(() => faviconPlugin({} as never));
114
+ await renderDOMHead(head);
115
+ expect(
116
+ document.head.querySelector("link[rel=icon]")?.getAttribute("href"),
117
+ ).toBe("/before.png");
118
+
119
+ useFavicon({ light: "/after.svg", dark: "/after-dark.svg" });
120
+ await nextTick();
121
+ await renderDOMHead(head);
122
+
123
+ const icons = [...document.head.querySelectorAll("link[rel=icon]")].map(
124
+ (link) => [link.getAttribute("href"), link.getAttribute("media")],
125
+ );
126
+ expect(icons).toEqual([
127
+ ["/after.svg", "(prefers-color-scheme: light)"],
128
+ ["/after-dark.svg", "(prefers-color-scheme: dark)"],
129
+ ]);
130
+ });
131
+ });
@@ -0,0 +1,145 @@
1
+ // @vitest-environment jsdom
2
+ /**
3
+ * The stored files of the file and image fields and of their table previews,
4
+ * through the runtime's real async data: a server render reads their
5
+ * metadata and renders the names and pictures, and the browser hydrates with
6
+ * it without asking again.
7
+ */
8
+ import {
9
+ type App,
10
+ type Component,
11
+ computed,
12
+ type FunctionalComponent,
13
+ h,
14
+ inject,
15
+ nextTick,
16
+ onBeforeUnmount,
17
+ onScopeDispose,
18
+ ref,
19
+ shallowReactive,
20
+ useId,
21
+ watch,
22
+ } from "vue";
23
+ import { afterEach, beforeEach, expect, it, vi } from "vitest";
24
+ import File from "../layers/dms-ui/app/components/form/components/File.vue";
25
+ import Image from "../layers/dms-ui/app/components/form/components/Image.vue";
26
+ import FilePreview from "../layers/dms-ui/app/components/table-view/FilePreview.vue";
27
+ import ImagePreview from "../layers/dms-ui/app/components/table-view/ImagePreview.vue";
28
+ import { hydrate, serverRender, stubApiData } from "./helpers/api-data-runtime";
29
+
30
+ const METADATA: Record<string, Record<string, unknown>> = {
31
+ "documents/brief.pdf": {
32
+ resourceKey: "documents/brief.pdf",
33
+ filename: "brief.pdf",
34
+ size: 2048,
35
+ mimetype: "application/pdf",
36
+ url: "https://files.test/brief.pdf",
37
+ },
38
+ "covers/cover.png": {
39
+ resourceKey: "covers/cover.png",
40
+ filename: "cover.png",
41
+ size: 4096,
42
+ mimetype: "image/png",
43
+ url: "https://files.test/cover.png",
44
+ },
45
+ };
46
+
47
+ const authFetch = vi.fn(
48
+ async (_url: string, options: { query: { resourceKey: string } }) =>
49
+ METADATA[options.query.resourceKey],
50
+ );
51
+ const mounted: App[] = [];
52
+
53
+ const passthrough: FunctionalComponent = (_, { slots }) =>
54
+ h("span", slots.default?.());
55
+ const renderTag =
56
+ (tag: string): FunctionalComponent =>
57
+ (props) =>
58
+ h(tag, props.src ? { src: props.src } : {});
59
+
60
+ function register(app: App): void {
61
+ app.config.globalProperties.$t = (key: string) => key;
62
+ for (const name of ["UFileUpload", "ULink", "UTooltip"])
63
+ app.component(name, passthrough);
64
+ for (const name of ["UButton", "UIcon", "UInput"])
65
+ app.component(name, renderTag("i"));
66
+ app.component("UAvatar", renderTag("img"));
67
+ app.component("UModal", () => null);
68
+ }
69
+
70
+ beforeEach(() => {
71
+ vi.stubGlobal("ref", ref);
72
+ vi.stubGlobal("computed", computed);
73
+ vi.stubGlobal("watch", watch);
74
+ vi.stubGlobal("inject", inject);
75
+ vi.stubGlobal("useId", useId);
76
+ vi.stubGlobal("onBeforeUnmount", onBeforeUnmount);
77
+ vi.stubGlobal("onScopeDispose", onScopeDispose);
78
+ vi.stubGlobal("shallowReactive", shallowReactive);
79
+ vi.stubGlobal("FORM_FIELD_LOADING_KEY", Symbol());
80
+ vi.stubGlobal("CONTENT_LANGUAGE_HEADER", "x-content-language");
81
+ stubApiData(authFetch as never);
82
+ vi.stubGlobal("useUploadWithProgress", () => ({
83
+ uploadWithProgress: vi.fn(),
84
+ }));
85
+ vi.stubGlobal("useFormField", () => ({
86
+ emitFormChange: vi.fn(),
87
+ color: ref(undefined),
88
+ highlight: ref(undefined),
89
+ ariaAttrs: ref(undefined),
90
+ }));
91
+ vi.stubGlobal("useI18n", () => ({ t: (key: string) => key }));
92
+ vi.stubGlobal(
93
+ "useDmsCookie",
94
+ (_key: string, options: { default: () => unknown }) =>
95
+ ref(options.default()),
96
+ );
97
+ vi.stubGlobal("useToast", () => ({ add: vi.fn() }));
98
+ });
99
+
100
+ afterEach(() => {
101
+ mounted.splice(0).forEach((app) => app.unmount());
102
+ vi.unstubAllEnvs();
103
+ vi.unstubAllGlobals();
104
+ authFetch.mockClear();
105
+ });
106
+
107
+ it.each<[string, Component, Record<string, unknown>, string]>([
108
+ ["a file field", File, { modelValue: "documents/brief.pdf" }, "brief.pdf"],
109
+ [
110
+ "an image field",
111
+ Image,
112
+ { modelValue: { key: "covers/cover.png", alt: "Cover" } },
113
+ "https://files.test/cover.png",
114
+ ],
115
+ [
116
+ "a table's file cell",
117
+ FilePreview,
118
+ { resourceKey: "documents/brief.pdf" },
119
+ "brief.pdf",
120
+ ],
121
+ [
122
+ "a table's image cell",
123
+ ImagePreview,
124
+ { images: [{ key: "covers/cover.png" }] },
125
+ "https://files.test/cover.png",
126
+ ],
127
+ ])(
128
+ "renders %s's stored file on the server, and the browser does not read it again",
129
+ async (_name, component, props, shown) => {
130
+ const root = { render: () => h(component, props) };
131
+ vi.stubEnv("SSR", true);
132
+ const { html, payload } = await serverRender(root, register);
133
+ vi.stubEnv("SSR", false);
134
+ expect(html).toContain(shown);
135
+ expect(authFetch).toHaveBeenCalledTimes(1);
136
+
137
+ authFetch.mockClear();
138
+ const { app, container } = hydrate(root, html, payload, register);
139
+ mounted.push(app);
140
+ await nextTick();
141
+ await new Promise((done) => setTimeout(done, 0));
142
+ expect(authFetch).not.toHaveBeenCalled();
143
+ expect(container.innerHTML).toContain(shown);
144
+ },
145
+ );
@@ -1,31 +1,116 @@
1
- import { effectScope, onScopeDispose, ref } from "vue";
1
+ // @vitest-environment jsdom
2
+ /**
3
+ * Stored files' read URLs through the runtime's real async data: a key
4
+ * resolved in a component's setup is read with the page (the server waits for
5
+ * it and hands it over), every component showing a file shares its read, and
6
+ * in the browser the URL is read again before it expires and a failure that
7
+ * may heal is retried.
8
+ */
9
+ import {
10
+ computed,
11
+ defineComponent,
12
+ effectScope,
13
+ h,
14
+ inject,
15
+ nextTick,
16
+ onScopeDispose,
17
+ ref,
18
+ shallowReactive,
19
+ watch,
20
+ type App,
21
+ } from "vue";
2
22
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
23
+ import {
24
+ browserRuntime,
25
+ hydrate,
26
+ mountInBrowser,
27
+ serverRender,
28
+ stubApiData,
29
+ } from "./helpers/api-data-runtime";
3
30
  import { useFileReadUrls } from "../layers/dms-ui/app/composables/useFileReadUrls";
31
+ import { FORM_CONTENT_LANGUAGE_KEY } from "../layers/dms-ui/app/composables/form/types/content-language";
4
32
 
5
33
  const RESOURCE_KEY = "localized/private.png";
6
34
  const START_TIME = new Date("2026-09-14T12:00:00Z");
7
35
  const URL_TTL_MS = 60000;
8
36
 
9
37
  const authFetch = vi.fn();
38
+ const mounted: App[] = [];
10
39
 
11
- describe("file read URLs", () => {
12
- beforeEach(() => {
40
+ /** An avatar as the identity cell draws it: resolved from an immediate watcher. */
41
+ const Avatar = defineComponent({
42
+ props: { fileKey: { type: String, required: true } },
43
+ setup(props) {
44
+ const { getUrl, resolve } = useFileReadUrls();
45
+ watch(
46
+ () => props.fileKey,
47
+ (key) => void resolve(key),
48
+ { immediate: true },
49
+ );
50
+ return () => h("img", { src: getUrl(props.fileKey) ?? "" });
51
+ },
52
+ });
53
+
54
+ const metadataCalls = () =>
55
+ authFetch.mock.calls.filter(([url]) => url === "/api/files/metadata");
56
+
57
+ beforeEach(() => {
58
+ vi.stubEnv("SSR", false);
59
+ vi.stubGlobal("CONTENT_LANGUAGE_HEADER", "x-content-language");
60
+ vi.stubGlobal("computed", computed);
61
+ vi.stubGlobal("watch", watch);
62
+ vi.stubGlobal("onScopeDispose", onScopeDispose);
63
+ vi.stubGlobal("shallowReactive", shallowReactive);
64
+ vi.stubGlobal("inject", inject);
65
+ stubApiData(authFetch);
66
+ });
67
+
68
+ afterEach(() => {
69
+ mounted.splice(0).forEach((app) => app.unmount());
70
+ vi.unstubAllEnvs();
71
+ vi.useRealTimers();
72
+ vi.unstubAllGlobals();
73
+ vi.resetAllMocks();
74
+ });
75
+
76
+ describe("file read URLs in a server render", () => {
77
+ it("renders the URL a setup resolves, and the browser hydrates without asking again", async () => {
78
+ authFetch.mockResolvedValue({
79
+ url: "https://example.test/avatar",
80
+ expiresAt: Date.now() + URL_TTL_MS,
81
+ });
82
+ const Row = defineComponent({
83
+ setup: () => () =>
84
+ h("div", [
85
+ h(Avatar, { fileKey: RESOURCE_KEY }),
86
+ h(Avatar, { fileKey: RESOURCE_KEY }),
87
+ ]),
88
+ });
89
+
90
+ vi.stubEnv("SSR", true);
91
+ const { html, payload } = await serverRender(Row);
13
92
  vi.stubEnv("SSR", false);
14
- vi.useFakeTimers();
15
- vi.setSystemTime(START_TIME);
16
- vi.stubGlobal("ref", ref);
17
- vi.stubGlobal("onScopeDispose", onScopeDispose);
18
- vi.stubGlobal("inject", () => "*");
19
- vi.stubGlobal("FORM_CONTENT_LANGUAGE_KEY", Symbol());
20
- vi.stubGlobal("CONTENT_LANGUAGE_HEADER", "x-content-language");
21
- vi.stubGlobal("useAuthFetch", () => ({ $authFetch: authFetch }));
93
+ expect(html).toBe(
94
+ '<div><img src="https://example.test/avatar"><img src="https://example.test/avatar"></div>',
95
+ );
96
+ // Two avatars of one file: one read.
97
+ expect(metadataCalls()).toHaveLength(1);
98
+
99
+ authFetch.mockClear();
100
+ const { app, container } = hydrate(Row, html, payload);
101
+ mounted.push(app);
102
+ await nextTick();
103
+ await new Promise((done) => setTimeout(done, 0));
104
+ expect(authFetch).not.toHaveBeenCalled();
105
+ expect(container.innerHTML).toBe(html);
22
106
  });
107
+ });
23
108
 
24
- afterEach(() => {
25
- vi.unstubAllEnvs();
26
- vi.useRealTimers();
27
- vi.unstubAllGlobals();
28
- vi.resetAllMocks();
109
+ describe("file read URLs in the browser", () => {
110
+ beforeEach(() => {
111
+ vi.useFakeTimers();
112
+ vi.setSystemTime(START_TIME);
113
+ browserRuntime();
29
114
  });
30
115
 
31
116
  it("preserves expiry and refreshes a visible URL before it expires", async () => {
@@ -74,15 +159,23 @@ describe("file read URLs", () => {
74
159
 
75
160
  it("uses the full localized form language for metadata authorization", async () => {
76
161
  authFetch.mockResolvedValue({ url: "https://example.test/file" });
77
- const scope = effectScope();
78
- const urls = scope.run(() => useFileReadUrls("private"))!;
79
- await urls.resolve(RESOURCE_KEY);
80
-
162
+ const Field = defineComponent({
163
+ setup() {
164
+ const { getUrl, resolve } = useFileReadUrls("private");
165
+ void resolve(RESOURCE_KEY);
166
+ return () => h("img", { src: getUrl(RESOURCE_KEY) ?? "" });
167
+ },
168
+ });
169
+ const { app } = mountInBrowser(Field, (application) =>
170
+ application.provide(FORM_CONTENT_LANGUAGE_KEY, "*"),
171
+ );
172
+ mounted.push(app);
173
+ await vi.advanceTimersByTimeAsync(0);
81
174
  expect(authFetch).toHaveBeenCalledWith("/api/files/metadata", {
82
175
  query: { resourceKey: RESOURCE_KEY, storage: "private" },
83
176
  headers: { "x-content-language": "*" },
177
+ signal: expect.any(AbortSignal),
84
178
  });
85
- scope.stop();
86
179
  });
87
180
 
88
181
  it("retries the first failed lookup while the component stays mounted", async () => {
@@ -91,13 +184,26 @@ describe("file read URLs", () => {
91
184
  .mockResolvedValueOnce({ url: "https://example.test/recovered" });
92
185
  const scope = effectScope();
93
186
  const urls = scope.run(() => useFileReadUrls())!;
94
- await urls.resolve(RESOURCE_KEY);
187
+ await urls.resolve(RESOURCE_KEY).catch(() => {});
95
188
  expect(urls.getUrl(RESOURCE_KEY)).toBeUndefined();
96
189
  await vi.advanceTimersByTimeAsync(5000);
97
190
  expect(urls.getUrl(RESOURCE_KEY)).toBe("https://example.test/recovered");
98
191
  scope.stop();
99
192
  });
100
193
 
194
+ it("does not retry a file the server refuses", async () => {
195
+ authFetch.mockRejectedValue(
196
+ Object.assign(new Error("Not found"), { statusCode: 404 }),
197
+ );
198
+ const scope = effectScope();
199
+ const urls = scope.run(() => useFileReadUrls())!;
200
+ await urls.resolve(RESOURCE_KEY).catch(() => {});
201
+ await vi.advanceTimersByTimeAsync(60000);
202
+ expect(authFetch).toHaveBeenCalledTimes(1);
203
+ expect(urls.getUrl(RESOURCE_KEY)).toBeUndefined();
204
+ scope.stop();
205
+ });
206
+
101
207
  it("does not restart refresh timers when an in-flight lookup finishes after disposal", async () => {
102
208
  let finish!: (value: unknown) => void;
103
209
  authFetch.mockReturnValue(
@@ -113,23 +219,28 @@ describe("file read URLs", () => {
113
219
  url: "https://example.test/late",
114
220
  expiresAt: Date.now() + URL_TTL_MS,
115
221
  });
116
- await pending;
222
+ await pending.catch(() => {});
117
223
  await vi.advanceTimersByTimeAsync(URL_TTL_MS * 2);
118
224
  expect(authFetch).toHaveBeenCalledTimes(1);
119
225
  expect(vi.getTimerCount()).toBe(0);
120
226
  });
121
227
 
122
- it("resolves metadata during SSR without scheduling browser refresh timers", async () => {
123
- vi.stubEnv("SSR", true);
124
- authFetch.mockResolvedValue({
125
- url: "https://example.test/ssr",
126
- expiresAt: Date.now() + URL_TTL_MS,
228
+ it("reads a key resolved after the setup, as an upload's", async () => {
229
+ authFetch.mockResolvedValue({ url: "https://example.test/uploaded" });
230
+ const key = ref<string | null>(null);
231
+ const Uploaded = defineComponent({
232
+ setup() {
233
+ const { getUrl, resolve } = useFileReadUrls();
234
+ watch(key, (value) => value && void resolve(value));
235
+ return () => h("img", { src: key.value ? getUrl(key.value) : "" });
236
+ },
127
237
  });
128
- const scope = effectScope();
129
- const urls = scope.run(() => useFileReadUrls())!;
130
- await urls.resolve(RESOURCE_KEY);
131
- expect(urls.getUrl(RESOURCE_KEY)).toBe("https://example.test/ssr");
132
- expect(vi.getTimerCount()).toBe(0);
133
- scope.stop();
238
+ const { app, container } = mountInBrowser(Uploaded);
239
+ mounted.push(app);
240
+ key.value = "uploads/new.png";
241
+ await vi.advanceTimersByTimeAsync(0);
242
+ expect(container.querySelector("img")?.getAttribute("src")).toBe(
243
+ "https://example.test/uploaded",
244
+ );
134
245
  });
135
246
  });
@@ -7,11 +7,13 @@ import {
7
7
  inject,
8
8
  onScopeDispose,
9
9
  ref,
10
+ shallowReactive,
10
11
  watch,
11
12
  type App,
12
13
  } from "vue";
13
14
  import { afterEach, beforeEach, expect, it, vi } from "vitest";
14
15
  import File from "../layers/dms-ui/app/components/form/components/File.vue";
16
+ import { browserRuntime, stubApiData } from "./helpers/api-data-runtime";
15
17
 
16
18
  const STORED_KEY = "files/profile/avatar.pdf";
17
19
  const authFetch = vi.fn();
@@ -29,6 +31,7 @@ function mountFile(): { app: App; container: HTMLElement } {
29
31
  render: () => h(File, { modelValue: STORED_KEY }),
30
32
  });
31
33
  app.config.globalProperties.$t = (key: string) => key;
34
+ browserRuntime(app);
32
35
  app.component("UFileUpload", passthrough);
33
36
  app.component("ULink", passthrough);
34
37
  app.component("UButton", defineComponent({ setup: () => () => h("button") }));
@@ -54,7 +57,8 @@ beforeEach(() => {
54
57
  vi.stubGlobal("FORM_FIELD_LOADING_KEY", Symbol());
55
58
  vi.stubGlobal("FORM_CONTENT_LANGUAGE_KEY", Symbol());
56
59
  vi.stubGlobal("CONTENT_LANGUAGE_HEADER", "x-content-language");
57
- vi.stubGlobal("useAuthFetch", () => ({ $authFetch: authFetch }));
60
+ vi.stubGlobal("shallowReactive", shallowReactive);
61
+ stubApiData(authFetch);
58
62
  vi.stubGlobal("useUploadWithProgress", () => ({
59
63
  uploadWithProgress: vi.fn(),
60
64
  }));