@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
@@ -1,7 +1,11 @@
1
+ import { router as inertiaRouter } from "@inertiajs/vue3";
1
2
  import { layoutUrlWithQuery } from "../../utils/pageQuery";
3
+ import { onBeforeMount, toRaw } from "vue";
2
4
  import { withoutTrailingSlash } from "ufo";
3
5
 
4
6
  const STALE_THRESHOLD_MS = 300_000;
7
+ const SITE_LAYOUT_ENDPOINT = "/dms/sitelayout";
8
+ const RENDERED_SITE_LAYOUT_KEY = "dms-rendered-site-layout";
5
9
  const IN_FLIGHT_SITE_LAYOUT_KEY = "__dmsInFlightSiteLayout";
6
10
  const IN_FLIGHT_PAGE_LAYOUTS_KEY = "__dmsInFlightPageLayouts";
7
11
 
@@ -10,28 +14,35 @@ interface PageLayoutRequest {
10
14
  promise: Promise<PageLayout>;
11
15
  }
12
16
 
13
- function getCachedLayoutUrls(
14
- previousSite: SiteLayout | undefined,
15
- previousCache: Record<string, PageLayout>,
16
- nextSite: SiteLayout,
17
- ): string[] {
18
- const urls = Object.entries(previousSite?.pages ?? {}).flatMap(
19
- ([path, page]) => {
20
- const nextPage = nextSite.pages[path];
21
- return previousCache[page.layoutUrl] &&
22
- nextPage?.layoutUrl &&
23
- nextPage.hasAccess !== false
24
- ? [nextPage.layoutUrl]
25
- : [];
26
- },
27
- );
28
- return [...new Set(urls)];
29
- }
30
-
31
17
  interface MatchRouteOptions {
32
18
  includeCategories: boolean;
33
19
  }
34
20
 
21
+ // The categories a URL resolves against, by slug, per served layout: the
22
+ // layout lists them by full id, and a `urlTransparent` one answers at its
23
+ // parent's slug, so it is never a URL's target.
24
+ const navigableCategoriesByLayout = new WeakMap<
25
+ SiteLayout,
26
+ Map<string, CategoryInfo>
27
+ >();
28
+
29
+ /** The category of the layout a URL resolves to, by its slug. */
30
+ export function findNavigableCategory(
31
+ layout: SiteLayout,
32
+ slug: string,
33
+ ): CategoryInfo | undefined {
34
+ let bySlug = navigableCategoriesByLayout.get(layout);
35
+ if (!bySlug) {
36
+ bySlug = new Map(
37
+ Object.values(layout.categories)
38
+ .filter((category) => !category.urlTransparent)
39
+ .map((category) => [category.fullSlug, category]),
40
+ );
41
+ navigableCategoriesByLayout.set(layout, bySlug);
42
+ }
43
+ return bySlug.get(slug);
44
+ }
45
+
35
46
  interface MatchRouteResult {
36
47
  pattern: string;
37
48
  metadata: PageInfo | CategoryInfo;
@@ -99,24 +110,84 @@ export function extractRouteParams(
99
110
  return buildParamBag(paramNames, match.slice(1));
100
111
  }
101
112
 
113
+ /**
114
+ * A partial reload of the page on screen: the server sends the site layout
115
+ * and the page fresh, and they replace the browser's copies, the layout
116
+ * Inertia carries from page to page included. A failure rejects and leaves
117
+ * the page as it is: Inertia would otherwise show the error in its place.
118
+ */
119
+ function reloadSiteLayoutAndPage(): Promise<void> {
120
+ return new Promise((resolve, reject) => {
121
+ let failure: unknown;
122
+ inertiaRouter.reload({
123
+ only: ["shared", "page"],
124
+ onHttpException: (response) => {
125
+ failure = new Error(`Site layout reload failed (${response.status})`);
126
+ return false;
127
+ },
128
+ onNetworkError: (error) => {
129
+ failure = error;
130
+ return false;
131
+ },
132
+ onFinish: () => (failure ? reject(failure) : resolve()),
133
+ });
134
+ });
135
+ }
136
+
137
+ interface SiteLayoutResponse {
138
+ siteLayout: SiteLayout;
139
+ siteLayoutTree: SiteLayoutTree;
140
+ quickActions: QuickActionsPayload;
141
+ modules: Record<string, ModuleWithAccess>;
142
+ isOwner: boolean;
143
+ }
144
+
145
+ /** The shared site layout, as a page's props or `/dms/sitelayout` set it. */
146
+ function useSiteLayoutState() {
147
+ return {
148
+ siteLayout: useDmsState<SiteLayout | undefined>(
149
+ "dms-siteLayout",
150
+ () => undefined,
151
+ ),
152
+ siteLayoutTree: useDmsState<SiteLayoutTree | undefined>(
153
+ "dms-pageTree",
154
+ () => undefined,
155
+ ),
156
+ quickActions: useDmsState<QuickActionsPayload | undefined>(
157
+ "dms-quickActions",
158
+ () => undefined,
159
+ ),
160
+ modules: useDmsState<Record<string, ModuleWithAccess> | undefined>(
161
+ "dms-modules",
162
+ () => undefined,
163
+ ),
164
+ isOwner: useDmsState<boolean>("dms-isOwner", () => false),
165
+ lastRefreshTime: useDmsState<number>("dms-lastRefreshTime", () => 0),
166
+ };
167
+ }
168
+
169
+ function applySiteLayoutResponse(
170
+ state: ReturnType<typeof useSiteLayoutState>,
171
+ result: SiteLayoutResponse,
172
+ ): void {
173
+ state.siteLayout.value = result.siteLayout;
174
+ state.siteLayoutTree.value = result.siteLayoutTree;
175
+ state.quickActions.value = result.quickActions;
176
+ state.modules.value = result.modules;
177
+ state.isOwner.value = result.isOwner;
178
+ state.lastRefreshTime.value = Date.now();
179
+ }
180
+
102
181
  export const useSiteLayout = () => {
103
- const siteLayout = useDmsState<SiteLayout | undefined>(
104
- "dms-siteLayout",
105
- () => undefined,
106
- );
107
- const siteLayoutTree = useDmsState<SiteLayoutTree | undefined>(
108
- "dms-pageTree",
109
- () => undefined,
110
- );
111
- const quickActions = useDmsState<QuickActionsPayload | undefined>(
112
- "dms-quickActions",
113
- () => undefined,
114
- );
115
- const modules = useDmsState<Record<string, ModuleWithAccess> | undefined>(
116
- "dms-modules",
117
- () => undefined,
118
- );
119
- const isOwner = useDmsState<boolean>("dms-isOwner", () => false);
182
+ const state = useSiteLayoutState();
183
+ const {
184
+ siteLayout,
185
+ siteLayoutTree,
186
+ quickActions,
187
+ modules,
188
+ isOwner,
189
+ lastRefreshTime,
190
+ } = state;
120
191
 
121
192
  const isLoading = useDmsState<boolean>("dms-loading", () => false);
122
193
  const loadingError = useDmsState<string | null>("dms-error", () => null);
@@ -125,7 +196,6 @@ export const useSiteLayout = () => {
125
196
  "dms-pageLayouts",
126
197
  () => ({}),
127
198
  );
128
- const lastRefreshTime = useDmsState<number>("dms-lastRefreshTime", () => 0);
129
199
 
130
200
  // Held on the DMS app rather than in a per-call ref: every caller of
131
201
  // useSiteLayout writes the same shared layout state, so they have to see the
@@ -148,79 +218,40 @@ export const useSiteLayout = () => {
148
218
  },
149
219
  };
150
220
 
151
- interface SiteLayoutResponse {
152
- siteLayout: SiteLayout;
153
- siteLayoutTree: SiteLayoutTree;
154
- quickActions: QuickActionsPayload;
155
- modules: Record<string, ModuleWithAccess>;
156
- isOwner: boolean;
157
- }
158
-
159
- const SITE_LAYOUT_ENDPOINT = "/dms/sitelayout";
160
221
  const SITE_LAYOUT_ERROR_MESSAGE = "Error loading siteLayout";
161
222
  const UNKNOWN_SITE_LAYOUT_ERROR = "Unknown error loading siteLayout";
162
223
  const HTTP_INTERNAL_ERROR = 500;
163
224
 
164
- const applySiteLayoutResponse = (result: SiteLayoutResponse) => {
165
- siteLayout.value = result.siteLayout;
166
- siteLayoutTree.value = result.siteLayoutTree;
167
- quickActions.value = result.quickActions;
168
- modules.value = result.modules;
169
- isOwner.value = result.isOwner;
170
- lastRefreshTime.value = Date.now();
171
- };
172
-
173
225
  const handleSiteLayoutError = (error: unknown) => {
174
226
  const errorMessage =
175
227
  error instanceof Error ? error.message : UNKNOWN_SITE_LAYOUT_ERROR;
176
228
  loadingError.value = errorMessage;
177
- showError({
229
+ showDmsError({
178
230
  statusCode: HTTP_INTERNAL_ERROR,
179
231
  statusMessage: SITE_LAYOUT_ERROR_MESSAGE,
180
232
  message: errorMessage,
181
233
  });
182
234
  };
183
235
 
184
- const requestSiteLayout = async (fatal: boolean): Promise<boolean> => {
236
+ const requestSiteLayout = async (): Promise<boolean> => {
185
237
  const initialLayout = siteLayout.value;
186
238
  const initialCache = pageLayouts.value;
239
+ // A layout another request committed meanwhile is newer than this one.
187
240
  const isCurrent = () =>
188
241
  siteLayout.value === initialLayout && pageLayouts.value === initialCache;
189
242
  const { $authFetch } = useAuthFetch();
190
243
  try {
191
244
  const result = await $authFetch<SiteLayoutResponse>(SITE_LAYOUT_ENDPOINT);
192
- const nextCache: Record<string, PageLayout> = {};
193
- // Initial page loads mutate this cache in place; identity guards cannot detect those additions.
194
- while (isCurrent()) {
195
- const urls = fatal
196
- ? []
197
- : getCachedLayoutUrls(
198
- initialLayout,
199
- initialCache,
200
- result.siteLayout,
201
- ).filter((url) => !nextCache[url]);
202
- if (!urls.length) {
203
- if (!fatal) pageLayouts.value = nextCache;
204
- applySiteLayoutResponse(result);
205
- return true;
206
- }
207
- const layouts = await Promise.all(
208
- urls.map(
209
- async (url) => [url, await $authFetch<PageLayout>(url)] as const,
210
- ),
211
- );
212
- Object.assign(nextCache, Object.fromEntries(layouts));
213
- }
245
+ if (isCurrent()) applySiteLayoutResponse(state, result);
214
246
  return true;
215
247
  } catch (error) {
216
- // Failed background refreshes must not leave a fatal error for the next navigation.
217
- if (fatal && isCurrent()) handleSiteLayoutError(error);
248
+ if (isCurrent()) handleSiteLayoutError(error);
218
249
  throw error;
219
250
  }
220
251
  };
221
252
 
222
- const startSiteLayoutFetch = (fatal = true): Promise<boolean> => {
223
- const started = requestSiteLayout(fatal).finally(() => {
253
+ const startSiteLayoutFetch = (): Promise<boolean> => {
254
+ const started = requestSiteLayout().finally(() => {
224
255
  isLoading.value = false;
225
256
  // A waiting refresh may already have started its replacement request.
226
257
  if (loadingPromise.value === started) loadingPromise.value = null;
@@ -246,12 +277,9 @@ export const useSiteLayout = () => {
246
277
  pageLayouts.value = {};
247
278
  }
248
279
 
249
- // `fatal: false` keeps a failed refetch from swapping the app for the error
250
- // page — for background resyncs (a realtime menu invalidation), where a
251
- // network blip must not destroy a working session.
252
- const refresh = async (options?: { fatal?: boolean }) => {
253
- // Background refreshes stage page layouts before committing; identity refreshes clear immediately.
254
- if (options?.fatal !== false) clearPageLayoutCaches();
280
+ // A background resync (a realtime menu invalidation) goes through `resync`.
281
+ const refresh = async () => {
282
+ clearPageLayoutCaches();
255
283
 
256
284
  // A fetch already in flight was started before whatever prompted this
257
285
  // call, so its answer cannot contain the change: joining it would report
@@ -270,11 +298,26 @@ export const useSiteLayout = () => {
270
298
  if (!loadingPromise.value) {
271
299
  isLoading.value = true;
272
300
  loadingError.value = null;
273
- loadingPromise.value = startSiteLayoutFetch(options?.fatal ?? true);
301
+ loadingPromise.value = startSiteLayoutFetch();
274
302
  }
275
303
  await loadingPromise.value;
276
304
  };
277
305
 
306
+ /**
307
+ * Re-reads the menu and the page on screen from the server, for a change
308
+ * the backend announced (a realtime menu invalidation) or a switch of
309
+ * session. The pages prefetched on hover are dropped first: each carries
310
+ * the layout and the access of before. A navigation that starts meanwhile
311
+ * drops the reload's answer, and the page it opens still carries the layout
312
+ * the reload meant to replace: that page is reloaded in turn.
313
+ */
314
+ const resync = async (): Promise<void> => {
315
+ inertiaRouter.flushAll();
316
+ const path = window.location.pathname;
317
+ await reloadSiteLayoutAndPage();
318
+ if (window.location.pathname !== path) await reloadSiteLayoutAndPage();
319
+ };
320
+
278
321
  const refreshIfStale = async () => {
279
322
  const elapsed = Date.now() - lastRefreshTime.value;
280
323
  if (elapsed < STALE_THRESHOLD_MS) {
@@ -335,12 +378,11 @@ export const useSiteLayout = () => {
335
378
  };
336
379
  }
337
380
 
338
- if (options.includeCategories && layout.categories[normalizedPath]) {
339
- return {
340
- pattern: normalizedPath,
341
- metadata: layout.categories[normalizedPath],
342
- params: {},
343
- };
381
+ const category = options.includeCategories
382
+ ? findNavigableCategory(layout, normalizedPath)
383
+ : undefined;
384
+ if (category) {
385
+ return { pattern: normalizedPath, metadata: category, params: {} };
344
386
  }
345
387
 
346
388
  for (const [pattern, metadata] of Object.entries(layout.pages)) {
@@ -420,7 +462,7 @@ export const useSiteLayout = () => {
420
462
  return false;
421
463
  }
422
464
  clearPageLayoutCaches();
423
- applySiteLayoutResponse(result);
465
+ applySiteLayoutResponse(state, result);
424
466
  return true;
425
467
  } catch {
426
468
  return false;
@@ -440,9 +482,48 @@ export const useSiteLayout = () => {
440
482
  loadSiteLayout,
441
483
  loadPageLayout,
442
484
  refresh,
485
+ resync,
443
486
  refreshIfStale,
444
487
  findMatchingRoute,
445
488
  findMatchingRouteOrCategory,
446
489
  probeAndCommitRoute,
447
490
  };
448
491
  };
492
+
493
+ /**
494
+ * The site layout for the component rendering it (the sidebar), called in
495
+ * its setup. A page the backend describes brings it with its props; a page
496
+ * only a frontend layer knows (a custom page) does not, and this loads it
497
+ * with the page then: a server render waits for it before rendering the
498
+ * component, and hands it to the browser, which takes it over before the
499
+ * component mounts instead of asking again.
500
+ */
501
+ export function useRenderedSiteLayout(): void {
502
+ const state = useSiteLayoutState();
503
+ if (state.siteLayout.value) return;
504
+ const layout = useSiteLayout();
505
+ // Through `loadSiteLayout`, which sets the state: a middleware loading it
506
+ // at the same time (the auth guard of a custom page) joins this request.
507
+ const { data } = useApiData<SiteLayoutResponse | null>(
508
+ RENDERED_SITE_LAYOUT_KEY,
509
+ async () => {
510
+ await layout.loadSiteLayout();
511
+ const { siteLayout, siteLayoutTree, quickActions, modules, isOwner } =
512
+ state;
513
+ // What the page payload carries: the plain values, not the state.
514
+ return siteLayout.value
515
+ ? {
516
+ siteLayout: toRaw(siteLayout.value),
517
+ siteLayoutTree: toRaw(siteLayoutTree.value!),
518
+ quickActions: toRaw(quickActions.value!),
519
+ modules: toRaw(modules.value!),
520
+ isOwner: isOwner.value,
521
+ }
522
+ : null;
523
+ },
524
+ );
525
+ onBeforeMount(() => {
526
+ if (data.value && !state.siteLayout.value)
527
+ applySiteLayoutResponse(state, data.value);
528
+ });
529
+ }
@@ -1,4 +1,5 @@
1
- const PROFILE_ENDPOINT = "/settings/user/profile";
1
+ import { pageRoutePath } from "#dms-core/app/utils/pageRoutes";
2
+ const PROFILE_ENDPOINT = pageRoutePath("/settings/user/profile");
2
3
 
3
4
  /**
4
5
  * Switches the interface language and stores it on the user's profile, then
@@ -9,7 +10,7 @@ const PROFILE_ENDPOINT = "/settings/user/profile";
9
10
  export function useUserLanguage() {
10
11
  const dmsApp = useDmsApp();
11
12
  const { $authFetch } = useAuthFetch();
12
- const { user, fetch: refreshUser } = useUserSession();
13
+ const { user, fetch: refreshUser } = useDmsUserSession();
13
14
 
14
15
  async function changeLanguage(language: string): Promise<void> {
15
16
  await dmsApp.$i18n.setLocale(language);
@@ -36,7 +36,6 @@ import {
36
36
  import DmsSearchInput from "#dms-ui/app/build/components/form/SearchInput.vue";
37
37
 
38
38
  const SKELETON_PLACEHOLDER_COUNT = 6;
39
- const MODULES_LISTING_DATA_KEY = "modules-page-listing";
40
39
 
41
40
  const { t } = useI18n();
42
41
  const { processI18n } = useTranslation();
@@ -85,37 +84,16 @@ if (!siteLayout.siteLayout.value) {
85
84
  // 403 page, the same answer as any page they may not open (the entry is not in
86
85
  // their menu either).
87
86
  if (!isOwner.value) {
88
- throw showError({
87
+ throw showDmsError({
89
88
  statusCode: HTTP_FORBIDDEN,
90
89
  statusMessage: "Access denied",
91
90
  });
92
91
  }
93
92
 
94
- // Whatever is cached from an earlier visit shows at once; stale live figures
95
- // are then refreshed in the background on mount. The hydration reuses the
96
- // listing the server rendered: fetched again before hydrating, its live
97
- // readouts (an uptime a minute on) would no longer match the markup.
98
- if (isOwner.value) {
99
- const { data: renderedListing } = await useDmsAsyncData<
100
- ModuleCatalogEntry[] | null
101
- >(MODULES_LISTING_DATA_KEY, async () => {
102
- await modulesListing.loadModulesListing();
103
- return modulesListing.modules.value ?? null;
104
- });
105
- if (modulesListing.modules.value === undefined) {
106
- if (renderedListing.value) {
107
- modulesListing.modules.value = renderedListing.value;
108
- } else {
109
- await modulesListing.loadModulesListing();
110
- }
111
- }
112
- }
113
-
114
- onMounted(() => {
115
- if (isOwner.value && modulesListing.modules.value !== undefined) {
116
- void modulesListing.revalidate();
117
- }
118
- });
93
+ // Read with the page, which the server renders and hands over: the
94
+ // hydration reuses the listing the server rendered (read again, its live
95
+ // readouts, an uptime a minute on, would no longer match the markup).
96
+ await modulesListing.loadModulesListing();
119
97
 
120
98
  const isForbidden = computed(
121
99
  () =>
@@ -204,7 +182,7 @@ const trimmedQuery = computed(() => query.value.trim());
204
182
  const pageMatch = computed(() => {
205
183
  if (visibleModules.value.length > 0 || !trimmedQuery.value) return null;
206
184
  const match = findModulePageMatch(
207
- siteLayout.siteLayout.value?.pages,
185
+ siteLayout.siteLayout.value,
208
186
  trimmedQuery.value,
209
187
  new Set(modules.value.map((entry) => entry.id)),
210
188
  processI18n,
@@ -1,16 +1,20 @@
1
1
  <script setup lang="ts">
2
+ import type { DmsLogoSources } from "#dms-core/shared/types/app-config";
2
3
  import { emailLogoUrl, emailStyles } from "./index";
3
4
 
5
+ interface EmailLogoConfig {
6
+ default?: Partial<DmsLogoSources>;
7
+ email?: Partial<DmsLogoSources>;
8
+ }
9
+
4
10
  interface EmailAppConfig {
5
11
  branding?: {
6
- logo?: {
7
- default?: {
8
- light?: string;
9
- };
10
- };
12
+ logo?: EmailLogoConfig;
11
13
  };
12
14
  }
13
15
 
16
+ const DARK_SCHEME_MEDIA = "(prefers-color-scheme: dark)";
17
+
14
18
  const props = withDefaults(
15
19
  defineProps<{
16
20
  /** Shown as the email's heading; no heading when omitted. */
@@ -33,13 +37,20 @@ const props = withDefaults(
33
37
  const appConfig = useDmsAppConfig() as EmailAppConfig;
34
38
  const runtimeConfig = useDmsRuntimeConfig();
35
39
 
36
- const logoPath = appConfig.branding?.logo?.default?.light;
40
+ const logo = appConfig.branding?.logo;
37
41
  const clientBaseUrl = runtimeConfig.public.dms?.clientBaseUrl ?? "";
38
- const logoUrl = props.publicLogoOnly
39
- ? emailLogoUrl(logoPath, clientBaseUrl)
40
- : logoPath?.startsWith("/")
41
- ? `${clientBaseUrl}${logoPath}`
42
- : logoPath;
42
+
43
+ function logoSource(path: string | undefined): string {
44
+ if (props.publicLogoOnly) return emailLogoUrl(path, clientBaseUrl);
45
+ return path?.startsWith("/") ? `${clientBaseUrl}${path}` : (path ?? "");
46
+ }
47
+
48
+ const logoUrl = logoSource(logo?.email?.light || logo?.default?.light);
49
+ // Offered to the mail clients that follow the reader's dark mode; the others
50
+ // skip the picture's source and draw the light image. Taken from the email
51
+ // slot only, so an instance that sets none keeps its single image.
52
+ const darkCandidate = logoSource(logo?.email?.dark);
53
+ const darkLogoUrl = darkCandidate === logoUrl ? "" : darkCandidate;
43
54
  </script>
44
55
 
45
56
  <template>
@@ -52,7 +63,16 @@ const logoUrl = props.publicLogoOnly
52
63
  <EContainer :style="emailStyles.container">
53
64
  <ESection :style="emailStyles.card">
54
65
  <ESection v-if="logoUrl" :style="emailStyles.logo">
66
+ <picture v-if="darkLogoUrl">
67
+ <source :media="DARK_SCHEME_MEDIA" :srcset="darkLogoUrl" />
68
+ <EImg
69
+ :src="logoUrl"
70
+ :alt="logoAlt"
71
+ :style="emailStyles.logoImage"
72
+ />
73
+ </picture>
55
74
  <EImg
75
+ v-else
56
76
  :src="logoUrl"
57
77
  :alt="logoAlt"
58
78
  :style="emailStyles.logoImage"
@@ -4,6 +4,7 @@ import StageCard, {
4
4
  type StageCardTone,
5
5
  } from "./build/components/layout/StageCard.vue";
6
6
  import EmptyLayout from "./custom-layouts/EmptyLayout.vue";
7
+ import { publicErrorMessage } from "./build/utils/publicErrorMessage";
7
8
 
8
9
  interface ErrorConfig {
9
10
  icon: string;
@@ -66,10 +67,12 @@ const statusCode = computed(
66
67
  () => props.error.statusCode || DEFAULT_STATUS_CODE,
67
68
  );
68
69
 
70
+ // Never the backend's address a failed request names (see
71
+ // `publicErrorMessage`).
69
72
  const visibleErrorMessage = computed(() =>
70
73
  STATUSES_WITHOUT_ERROR_MESSAGE.has(statusCode.value)
71
74
  ? undefined
72
- : props.error.message,
75
+ : publicErrorMessage(props.error.message),
73
76
  );
74
77
 
75
78
  const errorConfig = computed(
@@ -89,7 +92,7 @@ const description = computed(() =>
89
92
  );
90
93
 
91
94
  const handleError = async () => {
92
- clearError();
95
+ clearDmsError();
93
96
  if (!loggedIn.value) {
94
97
  await redirectToAuth(homepage);
95
98
  } else {
@@ -89,7 +89,7 @@ export default defineDmsMiddleware(async (to, from) => {
89
89
  const moduleEntry = siteLayout.modules.value?.[moduleId];
90
90
 
91
91
  if (!moduleId || !moduleEntry) {
92
- throw createError({
92
+ throw createDmsError({
93
93
  statusCode: MODULE_NOT_FOUND_STATUS,
94
94
  statusMessage: MODULE_NOT_FOUND_MESSAGE,
95
95
  data: { kind: MODULE_NOT_FOUND_KIND },
@@ -8,7 +8,7 @@ export default defineDmsMiddleware(async (to, from) => {
8
8
  // Unsaved changes anywhere (the page's forms, an open drawer or modal, a
9
9
  // settings panel): one warning dialog, whatever holds them.
10
10
  if (!(await confirmLeave())) {
11
- return abortNavigation();
11
+ return abortDmsNavigation();
12
12
  }
13
13
 
14
14
  const { executeGuards } = useLeaveGuard();
@@ -17,6 +17,6 @@ export default defineDmsMiddleware(async (to, from) => {
17
17
  const canLeave = await executeGuards(pageKey);
18
18
 
19
19
  if (!canLeave) {
20
- return abortNavigation();
20
+ return abortDmsNavigation();
21
21
  }
22
22
  });
@@ -3,6 +3,7 @@ import { withoutTrailingSlash } from "ufo";
3
3
  import { holdPageHeaderActions } from "../composables/layout/usePageHeaderActions";
4
4
  import { useDevReloading } from "../build/composables/dev-reload/useDevReloadHolder";
5
5
  import { usePageLiveHost } from "#dms-ui/app/build/composables/live/usePageLiveHost";
6
+ import { usePageDataRefresh } from "#dms-ui/app/build/composables/live/usePageDataRefresh";
6
7
  import { pageValidationRedirect } from "../build/utils/pageValidationRedirect";
7
8
 
8
9
  defineOptions({ inheritAttrs: false });
@@ -42,7 +43,7 @@ async function checkPageMetadata(
42
43
  if (import.meta.env.DEV && devReloading.value) {
43
44
  return;
44
45
  }
45
- throw showError({
46
+ throw showDmsError({
46
47
  statusCode: HTTP_NOT_FOUND,
47
48
  statusMessage: "Page not found",
48
49
  });
@@ -76,7 +77,7 @@ async function recheckAccessWithFreshSession(): Promise<void> {
76
77
  if (import.meta.env.DEV && devReloading.value) {
77
78
  return;
78
79
  }
79
- throw showError({
80
+ throw showDmsError({
80
81
  statusCode: HTTP_NOT_FOUND,
81
82
  statusMessage: "Page not found",
82
83
  });
@@ -90,7 +91,7 @@ async function recheckAccessWithFreshSession(): Promise<void> {
90
91
  return;
91
92
  }
92
93
 
93
- throw showError({
94
+ throw showDmsError({
94
95
  statusCode: HTTP_FORBIDDEN,
95
96
  statusMessage: "Access denied",
96
97
  });
@@ -140,7 +141,7 @@ async function enforcePageValidation(): Promise<void> {
140
141
  routeQuery: query,
141
142
  });
142
143
  if (!redirect) {
143
- throw createError({
144
+ throw createDmsError({
144
145
  statusCode: HTTP_NOT_FOUND,
145
146
  statusMessage: "Page not found",
146
147
  });
@@ -172,7 +173,7 @@ const pagelayoutMetadata = computed(() => routeMatch.value?.metadata);
172
173
  const routeParams = computed(() => routeMatch.value?.params || {});
173
174
 
174
175
  if (siteLayout.loadingError && siteLayout.loadingError.value) {
175
- throw createError({
176
+ throw createDmsError({
176
177
  statusCode: HTTP_INTERNAL_SERVER_ERROR,
177
178
  statusMessage: "Error loading site layout",
178
179
  message: siteLayout.loadingError.value || "Unknown error occurred",
@@ -186,7 +187,7 @@ await checkPageMetadata(pagelayoutMetadata.value);
186
187
  // flight the page is unregistered for a moment, and the plugin is already
187
188
  // waiting for it to come back.
188
189
  if (!pagelayoutMetadata.value && !(import.meta.env.DEV && devReloading.value)) {
189
- throw createError({
190
+ throw createDmsError({
190
191
  statusCode: HTTP_NOT_FOUND,
191
192
  statusMessage: "Page not found",
192
193
  });
@@ -202,9 +203,9 @@ if (import.meta.env.DEV) {
202
203
  // the flag back with nobody watching.
203
204
  watch(devReloading, (reloading) => {
204
205
  if (!reloading && !pagelayoutMetadata.value) {
205
- // `showError` commits the error itself; throwing on top of it would only
206
+ // `showDmsError` commits the error itself; throwing on top of it would only
206
207
  // add a second report of the same thing.
207
- showError({
208
+ showDmsError({
208
209
  statusCode: HTTP_NOT_FOUND,
209
210
  statusMessage: "Page not found",
210
211
  });
@@ -246,6 +247,8 @@ usePageLiveHost(
246
247
  }),
247
248
  { isStreamConnected: () => !!realtime.sessionId.value },
248
249
  );
250
+ // The page's `useApiData` reads follow its refreshes and its realtime events.
251
+ usePageDataRefresh(realtime);
249
252
 
250
253
  function buildPageSetupContext(
251
254
  pageInfo: PageInfo,
@@ -495,7 +498,7 @@ if (import.meta.env.DEV) {
495
498
  void navigateDms(moved[0]);
496
499
  return;
497
500
  }
498
- showError({
501
+ showDmsError({
499
502
  statusCode: HTTP_NOT_FOUND,
500
503
  statusMessage: "Page not found",
501
504
  });