@antelopejs/dms 0.8.2 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (354) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/dist/implementations/dms/frontend-backend-proxies.d.ts +3 -0
  3. package/dist/implementations/dms/frontend-backend-proxies.js +39 -0
  4. package/dist/implementations/dms/frontend-backend-proxies.js.map +1 -0
  5. package/dist/implementations/dms/frontend-bootstrap.d.ts +1 -0
  6. package/dist/implementations/dms/frontend-bootstrap.js +13 -0
  7. package/dist/implementations/dms/frontend-bootstrap.js.map +1 -1
  8. package/dist/implementations/dms/frontend-modules.d.ts +4 -2
  9. package/dist/implementations/dms/frontend-modules.js +8 -7
  10. package/dist/implementations/dms/frontend-modules.js.map +1 -1
  11. package/dist/implementations/dms/manifest.d.ts +6 -0
  12. package/dist/implementations/dms/manifest.js +13 -0
  13. package/dist/implementations/dms/manifest.js.map +1 -1
  14. package/dist/implementations/dms/menu-categories.d.ts +16 -0
  15. package/dist/implementations/dms/menu-categories.js +30 -0
  16. package/dist/implementations/dms/menu-categories.js.map +1 -0
  17. package/dist/implementations/dms/nav-badges.d.ts +0 -8
  18. package/dist/implementations/dms/nav-badges.js +0 -16
  19. package/dist/implementations/dms/nav-badges.js.map +1 -1
  20. package/dist/implementations/dms/page.d.ts +11 -5
  21. package/dist/implementations/dms/page.js +56 -41
  22. package/dist/implementations/dms/page.js.map +1 -1
  23. package/dist/implementations/dms-html-render/index.js +5 -2
  24. package/dist/implementations/dms-html-render/index.js.map +1 -1
  25. package/dist/implementations/dms-notifications/index.d.ts +4 -3
  26. package/dist/implementations/dms-notifications/index.js +14 -6
  27. package/dist/implementations/dms-notifications/index.js.map +1 -1
  28. package/dist/pages/settings/settings-index.js +3 -2
  29. package/dist/pages/settings/settings-index.js.map +1 -1
  30. package/dist/pages/settings/users/invites.js +2 -2
  31. package/dist/pages/settings/users/invites.js.map +1 -1
  32. package/dist/pages/settings/users/member-change-notifications.js +17 -6
  33. package/dist/pages/settings/users/member-change-notifications.js.map +1 -1
  34. package/dist/pages/settings/users/member-invite-form.d.ts +3 -2
  35. package/dist/pages/settings/users/member-invite-form.js +7 -4
  36. package/dist/pages/settings/users/member-invite-form.js.map +1 -1
  37. package/dist/pages/settings/users/member-roles-field.js +2 -1
  38. package/dist/pages/settings/users/member-roles-field.js.map +1 -1
  39. package/dist/pages/settings/users/members.js +4 -1
  40. package/dist/pages/settings/users/members.js.map +1 -1
  41. package/dist/pages/settings/users/notifications.d.ts +6 -0
  42. package/dist/pages/settings/users/notifications.js +23 -15
  43. package/dist/pages/settings/users/notifications.js.map +1 -1
  44. package/dist/pages/settings/users/profile.js +1 -1
  45. package/dist/pages/settings/users/profile.js.map +1 -1
  46. package/dist/pages/settings/users/region-form.d.ts +1 -1
  47. package/dist/pages/settings/users/region-form.js +2 -1
  48. package/dist/pages/settings/users/region-form.js.map +1 -1
  49. package/dist/pages/settings/users/role-editor-store.d.ts +3 -1
  50. package/dist/pages/settings/users/role-editor-store.js +17 -4
  51. package/dist/pages/settings/users/role-editor-store.js.map +1 -1
  52. package/dist/pages/settings/users/roles.js +1 -0
  53. package/dist/pages/settings/users/roles.js.map +1 -1
  54. package/dist/realtime/registry.d.ts +1 -0
  55. package/dist/realtime/registry.js +5 -0
  56. package/dist/realtime/registry.js.map +1 -1
  57. package/dist/routes/realtime.d.ts +5 -1
  58. package/dist/routes/realtime.js +16 -9
  59. package/dist/routes/realtime.js.map +1 -1
  60. package/dist/test/integration/account-deletion.test.js +2 -2
  61. package/dist/test/integration/account-deletion.test.js.map +1 -1
  62. package/dist/test/integration/auth/auth-key-rotation.test.js +2 -1
  63. package/dist/test/integration/auth/auth-key-rotation.test.js.map +1 -1
  64. package/dist/test/integration/auth/current-password.test.js +2 -1
  65. package/dist/test/integration/auth/current-password.test.js.map +1 -1
  66. package/dist/test/integration/auth/session-revocation.test.js +2 -1
  67. package/dist/test/integration/auth/session-revocation.test.js.map +1 -1
  68. package/dist/test/integration/auth/sign-in-country.test.js +2 -1
  69. package/dist/test/integration/auth/sign-in-country.test.js.map +1 -1
  70. package/dist/test/integration/auth/two-factor-codes.test.js +2 -1
  71. package/dist/test/integration/auth/two-factor-codes.test.js.map +1 -1
  72. package/dist/test/integration/auth/two-factor-password.test.js +3 -2
  73. package/dist/test/integration/auth/two-factor-password.test.js.map +1 -1
  74. package/dist/test/integration/email-change.test.js +2 -1
  75. package/dist/test/integration/email-change.test.js.map +1 -1
  76. package/dist/test/integration/files/component-files.test.js +2 -2
  77. package/dist/test/integration/files/component-files.test.js.map +1 -1
  78. package/dist/test/integration/member-settings-access.test.js +8 -7
  79. package/dist/test/integration/member-settings-access.test.js.map +1 -1
  80. package/dist/test/integration/menu-access-events.test.d.ts +1 -0
  81. package/dist/test/integration/menu-access-events.test.js +148 -0
  82. package/dist/test/integration/menu-access-events.test.js.map +1 -0
  83. package/dist/test/integration/notification-inbox.test.js +4 -1
  84. package/dist/test/integration/notification-inbox.test.js.map +1 -1
  85. package/dist/test/integration/notification-locked-subjects.test.js +3 -2
  86. package/dist/test/integration/notification-locked-subjects.test.js.map +1 -1
  87. package/dist/test/integration/notification-unread-badge.test.js +51 -5
  88. package/dist/test/integration/notification-unread-badge.test.js.map +1 -1
  89. package/dist/test/integration/page-layout-route-guard.test.d.ts +1 -0
  90. package/dist/test/integration/page-layout-route-guard.test.js +56 -0
  91. package/dist/test/integration/page-layout-route-guard.test.js.map +1 -0
  92. package/dist/test/integration/permission-warnings.test.js +3 -2
  93. package/dist/test/integration/permission-warnings.test.js.map +1 -1
  94. package/dist/test/integration/removed-member-notifications.test.js +3 -2
  95. package/dist/test/integration/removed-member-notifications.test.js.map +1 -1
  96. package/dist/test/integration/role-delete-reassign.test.js +2 -1
  97. package/dist/test/integration/role-delete-reassign.test.js.map +1 -1
  98. package/dist/test/integration/role-permission-ancestors.test.js +2 -1
  99. package/dist/test/integration/role-permission-ancestors.test.js.map +1 -1
  100. package/dist/test/unit/frontend-bootstrap.test.js +13 -0
  101. package/dist/test/unit/frontend-bootstrap.test.js.map +1 -1
  102. package/dist/test/unit/implementations/dms/category-slug-registry.test.js +14 -5
  103. package/dist/test/unit/implementations/dms/category-slug-registry.test.js.map +1 -1
  104. package/dist/test/unit/implementations/dms/frontend-backend-proxies.test.d.ts +1 -0
  105. package/dist/test/unit/implementations/dms/frontend-backend-proxies.test.js +66 -0
  106. package/dist/test/unit/implementations/dms/frontend-backend-proxies.test.js.map +1 -0
  107. package/dist/test/unit/implementations/dms/manifest-strip.test.js +26 -0
  108. package/dist/test/unit/implementations/dms/manifest-strip.test.js.map +1 -1
  109. package/dist/test/unit/implementations/dms/nav-badges.test.js +23 -9
  110. package/dist/test/unit/implementations/dms/nav-badges.test.js.map +1 -1
  111. package/dist/test/unit/implementations/dms-html-render/generate-html.test.js +22 -4
  112. package/dist/test/unit/implementations/dms-html-render/generate-html.test.js.map +1 -1
  113. package/dist/test/unit/interfaces/dms/page-layout-query.test.js +12 -0
  114. package/dist/test/unit/interfaces/dms/page-layout-query.test.js.map +1 -1
  115. package/dist/test/unit/interfaces/dms/page-teardown.test.js +1 -1
  116. package/dist/test/unit/interfaces/dms/page-teardown.test.js.map +1 -1
  117. package/dist/test/unit/interfaces/dms-base/table-and-card-catalog.test.js +25 -0
  118. package/dist/test/unit/interfaces/dms-base/table-and-card-catalog.test.js.map +1 -1
  119. package/dist/test/unit/pages/settings/member-roles-field.test.js +1 -1
  120. package/dist/test/unit/pages/settings/member-roles-field.test.js.map +1 -1
  121. package/dist/test/unit/pages/settings/role-delete-confirm.test.js.map +1 -1
  122. package/dist/test/unit/realtime/menu-topics.test.d.ts +1 -0
  123. package/dist/test/unit/realtime/menu-topics.test.js +26 -0
  124. package/dist/test/unit/realtime/menu-topics.test.js.map +1 -0
  125. package/dist/validation/regional-preferences.schema.d.ts +2 -2
  126. package/frontend-vue/deferred-plugins.ts +3 -4
  127. package/frontend-vue/dms.email.ts +5 -1
  128. package/frontend-vue/dms.frontend.ts +15 -5
  129. package/frontend-vue/dms.server.ts +74 -0
  130. package/frontend-vue/layers/dms-auth/app/composables/useLogout.ts +1 -1
  131. package/frontend-vue/layers/dms-auth/app/composables/usePostLoginRedirect.ts +16 -3
  132. package/frontend-vue/layers/dms-auth/app/custom-pages/auth/2fa.vue +3 -1
  133. package/frontend-vue/layers/dms-auth/app/custom-pages/auth/backup.vue +3 -1
  134. package/frontend-vue/layers/dms-auth/app/custom-pages/auth/forgot-validation.vue +1 -1
  135. package/frontend-vue/layers/dms-auth/app/custom-pages/auth/oauth-complete.vue +26 -3
  136. package/frontend-vue/layers/dms-auth/app/custom-pages/auth/recover.vue +1 -1
  137. package/frontend-vue/layers/dms-auth/app/custom-pages/auth/signup.vue +22 -25
  138. package/frontend-vue/layers/dms-auth/app/custom-pages/auth/validate.vue +1 -1
  139. package/frontend-vue/layers/dms-auth/app/middleware/auth.ts +5 -7
  140. package/frontend-vue/layers/dms-auth/app/plugins/permissions-preload.ts +17 -0
  141. package/frontend-vue/layers/dms-auth/server/accounts.ts +135 -0
  142. package/frontend-vue/layers/dms-auth/server/actions.ts +251 -0
  143. package/frontend-vue/layers/dms-auth/server/oauth.ts +260 -0
  144. package/frontend-vue/layers/dms-auth/server/session.ts +158 -0
  145. package/frontend-vue/layers/dms-core/app/build/utils/asyncDataScope.ts +47 -0
  146. package/frontend-vue/layers/dms-core/app/build/utils/permission-preview.ts +6 -2
  147. package/frontend-vue/layers/dms-core/app/composables/auth/useApiData.ts +148 -0
  148. package/frontend-vue/layers/dms-core/app/composables/auth/useAuthFetch.ts +7 -20
  149. package/frontend-vue/layers/dms-core/app/composables/auth/useCurrentUser.ts +2 -2
  150. package/frontend-vue/layers/dms-core/app/composables/auth/useMultiAccount.ts +18 -5
  151. package/frontend-vue/layers/dms-core/app/composables/auth/usePermissions.ts +35 -6
  152. package/frontend-vue/layers/dms-core/app/composables/auth/useSessionRecovery.ts +1 -1
  153. package/frontend-vue/layers/dms-core/app/composables/period/compareResolvers.ts +20 -14
  154. package/frontend-vue/layers/dms-core/app/composables/period/periodClock.ts +121 -0
  155. package/frontend-vue/layers/dms-core/app/composables/period/periodRenderNow.ts +42 -0
  156. package/frontend-vue/layers/dms-core/app/composables/period/periodTimeZone.ts +31 -0
  157. package/frontend-vue/layers/dms-core/app/composables/period/presetResolvers.ts +48 -19
  158. package/frontend-vue/layers/dms-core/app/composables/period/usePeriod.ts +33 -4
  159. package/frontend-vue/layers/dms-core/app/composables/period/usePeriodScope.ts +78 -25
  160. package/frontend-vue/layers/dms-core/app/composables/realtime/usePageRealtime.ts +3 -0
  161. package/frontend-vue/layers/dms-core/app/composables/realtime/useUserRealtime.ts +36 -6
  162. package/frontend-vue/layers/dms-core/app/composables/user/useUserRegionalPreferences.ts +1 -1
  163. package/frontend-vue/layers/dms-core/app/plugins/async-data-scope.ts +26 -0
  164. package/frontend-vue/layers/dms-core/app/utils/pageRoutes.ts +22 -0
  165. package/frontend-vue/layers/dms-core/app/utils/regional.ts +36 -3
  166. package/frontend-vue/layers/dms-core/app/utils/trailingDeduper.ts +10 -1
  167. package/frontend-vue/layers/dms-core/shared/types/app-config.ts +48 -4
  168. package/frontend-vue/layers/dms-layout/app/app.config.ts +42 -23
  169. package/frontend-vue/layers/dms-layout/app/assets/css/main.css +72 -20
  170. package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardFrame.vue +9 -0
  171. package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardSidebar.vue +0 -3
  172. package/frontend-vue/layers/dms-layout/app/build/components/notification/NotificationPopover.vue +9 -3
  173. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/members/MemberRolePicker.vue +13 -12
  174. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/notification/NotificationInboxDisplay.vue +17 -7
  175. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/notification/NotificationPreferencesForm.vue +0 -1
  176. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/profile/ProfilePersonalInfo.vue +36 -33
  177. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/profile/ProfileSecuritySummary.vue +1 -2
  178. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/roles/role-types.ts +5 -4
  179. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/security/SecuritySessions.vue +35 -28
  180. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/security/SecurityStatus.vue +0 -1
  181. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/security/SecurityTwoFactor.vue +1 -1
  182. package/frontend-vue/layers/dms-layout/app/build/composables/dev-reload/useDevReloadHolder.ts +7 -6
  183. package/frontend-vue/layers/dms-layout/app/build/composables/layout/sidePanelState.ts +1 -1
  184. package/frontend-vue/layers/dms-layout/app/build/composables/navigation/useVisitNavBadges.ts +26 -0
  185. package/frontend-vue/layers/dms-layout/app/build/composables/notification/useNotificationCatalog.ts +25 -23
  186. package/frontend-vue/layers/dms-layout/app/build/composables/notification/useNotificationPreferences.ts +44 -23
  187. package/frontend-vue/layers/dms-layout/app/build/composables/security/useSessionHandoff.ts +1 -1
  188. package/frontend-vue/layers/dms-layout/app/build/composables/settings/profile/preferencesSummary.ts +1 -1
  189. package/frontend-vue/layers/dms-layout/app/build/composables/settings/profile/useAccountData.ts +3 -2
  190. package/frontend-vue/layers/dms-layout/app/build/composables/settings/profile/useProfileAvatar.ts +1 -18
  191. package/frontend-vue/layers/dms-layout/app/build/composables/settings/profile/useProfilePreferences.ts +19 -23
  192. package/frontend-vue/layers/dms-layout/app/build/composables/settings/security/useSecurityFormat.ts +6 -3
  193. package/frontend-vue/layers/dms-layout/app/build/composables/settings/security/useSecurityOverview.ts +13 -18
  194. package/frontend-vue/layers/dms-layout/app/build/utils/modules-catalog.ts +8 -6
  195. package/frontend-vue/layers/dms-layout/app/build/utils/publicErrorMessage.ts +19 -0
  196. package/frontend-vue/layers/dms-layout/app/components/navigation/CollapsedMenuItem.vue +0 -11
  197. package/frontend-vue/layers/dms-layout/app/components/navigation/NavigationMenu.vue +3 -13
  198. package/frontend-vue/layers/dms-layout/app/composables/general/useSystemState.ts +7 -11
  199. package/frontend-vue/layers/dms-layout/app/composables/notification/useNotifications.ts +103 -22
  200. package/frontend-vue/layers/dms-layout/app/composables/page/useModulesListing.ts +25 -102
  201. package/frontend-vue/layers/dms-layout/app/composables/page/usePageComponentPreload.ts +0 -10
  202. package/frontend-vue/layers/dms-layout/app/composables/page/useSiteLayout.ts +177 -96
  203. package/frontend-vue/layers/dms-layout/app/composables/profile/useUserLanguage.ts +3 -2
  204. package/frontend-vue/layers/dms-layout/app/custom-pages/modules.vue +6 -28
  205. package/frontend-vue/layers/dms-layout/app/emails/components/EmailLayout.vue +31 -11
  206. package/frontend-vue/layers/dms-layout/app/error.vue +5 -2
  207. package/frontend-vue/layers/dms-layout/app/middleware/module-routing.global.ts +1 -1
  208. package/frontend-vue/layers/dms-layout/app/middleware/page-leave-guard.global.ts +2 -2
  209. package/frontend-vue/layers/dms-layout/app/pages/[...slug].vue +12 -9
  210. package/frontend-vue/layers/dms-layout/app/plugins/dms-menu-sync.client.ts +31 -3
  211. package/frontend-vue/layers/dms-layout/app/plugins/favicon.ts +58 -0
  212. package/frontend-vue/layers/dms-layout/app/plugins/notification-stream.client.ts +15 -7
  213. package/frontend-vue/layers/dms-layout/app/plugins/profile-sync.client.ts +5 -1
  214. package/frontend-vue/layers/dms-layout/app/plugins/regional-preferences.ts +10 -4
  215. package/frontend-vue/layers/dms-layout/app/plugins/time-zone-cookie.client.ts +12 -0
  216. package/frontend-vue/layers/dms-layout/app/types/page.ts +13 -0
  217. package/frontend-vue/layers/dms-layout/server/html-render.ts +58 -0
  218. package/frontend-vue/layers/dms-layout/server/render-token.ts +95 -0
  219. package/frontend-vue/layers/dms-onboarding/app/composables/onboarding/useOnboarding.ts +25 -21
  220. package/frontend-vue/layers/dms-onboarding/app/middleware/onboarding.global.ts +1 -1
  221. package/frontend-vue/layers/dms-onboarding/app/plugins/onboarding-preload.ts +9 -0
  222. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/registerDefaults.ts +4 -1
  223. package/frontend-vue/layers/dms-ui/app/build/composables/files/useFileMetadata.ts +162 -0
  224. package/frontend-vue/layers/dms-ui/app/build/composables/form/formRecordLoad.ts +23 -4
  225. package/frontend-vue/layers/dms-ui/app/build/composables/live/usePageDataRefresh.ts +101 -0
  226. package/frontend-vue/layers/dms-ui/app/build/composables/navigation/useNavBadges.ts +33 -6
  227. package/frontend-vue/layers/dms-ui/app/build/composables/period/periodDisplay.ts +19 -16
  228. package/frontend-vue/layers/dms-ui/app/build/composables/period/periodQuery.ts +40 -20
  229. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableFooter.ts +6 -8
  230. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableListData.ts +42 -14
  231. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableRows.ts +19 -8
  232. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViews.ts +1 -1
  233. package/frontend-vue/layers/dms-ui/app/components/chart/ChartCard.vue +5 -2
  234. package/frontend-vue/layers/dms-ui/app/components/flow-canvas/FlowCanvas.vue +9 -9
  235. package/frontend-vue/layers/dms-ui/app/components/form/Form.vue +20 -18
  236. package/frontend-vue/layers/dms-ui/app/components/form/components/Cascader.vue +2 -1
  237. package/frontend-vue/layers/dms-ui/app/components/form/components/File.vue +26 -132
  238. package/frontend-vue/layers/dms-ui/app/components/form/components/Image.vue +40 -105
  239. package/frontend-vue/layers/dms-ui/app/components/form/components/InputAddress.vue +2 -0
  240. package/frontend-vue/layers/dms-ui/app/components/form/components/Relation.vue +4 -4
  241. package/frontend-vue/layers/dms-ui/app/components/period/PeriodSelector.vue +9 -13
  242. package/frontend-vue/layers/dms-ui/app/components/table-view/CascaderPath.vue +2 -1
  243. package/frontend-vue/layers/dms-ui/app/components/table-view/FilePreview.vue +12 -24
  244. package/frontend-vue/layers/dms-ui/app/components/table-view/ImagePreview.vue +17 -29
  245. package/frontend-vue/layers/dms-ui/app/components/table-view/KanbanBoard.vue +7 -3
  246. package/frontend-vue/layers/dms-ui/app/components/table-view/TableView.vue +20 -8
  247. package/frontend-vue/layers/dms-ui/app/components/top-list/TopListCard.vue +8 -11
  248. package/frontend-vue/layers/dms-ui/app/composables/chart/useChartFetch.ts +146 -182
  249. package/frontend-vue/layers/dms-ui/app/composables/live/usePageLiveRefresh.ts +8 -2
  250. package/frontend-vue/layers/dms-ui/app/composables/useFileReadUrls.ts +11 -99
  251. package/frontend-vue/layers/dms-ui/app/utils/blockRefresh.ts +32 -3
  252. package/frontend-vue/package.json +2 -2
  253. package/frontend-vue/pnpm-lock.yaml +5 -5
  254. package/frontend-vue/server/private-options.ts +27 -0
  255. package/frontend-vue/tests/async-data-scope.test.ts +121 -0
  256. package/frontend-vue/tests/auth-middleware-error.test.ts +3 -3
  257. package/frontend-vue/tests/banner-block-fetch.test.ts +3 -0
  258. package/frontend-vue/tests/block-fetch-options-forwarding.test.ts +3 -0
  259. package/frontend-vue/tests/block-items-period-realtime.test.ts +3 -0
  260. package/frontend-vue/tests/block-not-found.test.ts +16 -2
  261. package/frontend-vue/tests/block-page-refresh.test.ts +3 -0
  262. package/frontend-vue/tests/block-route-params.test.ts +3 -0
  263. package/frontend-vue/tests/branding-defaults.test.ts +34 -0
  264. package/frontend-vue/tests/builtin-emails.test.ts +23 -2
  265. package/frontend-vue/tests/chart-live-pause.test.ts +4 -1
  266. package/frontend-vue/tests/chart-value-propagation.test.ts +4 -1
  267. package/frontend-vue/tests/composed-text-blocks.test.ts +3 -0
  268. package/frontend-vue/tests/email-layout.test.ts +66 -4
  269. package/frontend-vue/tests/error-page-message.test.ts +25 -2
  270. package/frontend-vue/tests/favicon.test.ts +131 -0
  271. package/frontend-vue/tests/file-fields-ssr.test.ts +145 -0
  272. package/frontend-vue/tests/file-read-urls.test.ts +145 -34
  273. package/frontend-vue/tests/file-unavailable.test.ts +5 -1
  274. package/frontend-vue/tests/fixtures/dataEngine.ts +44 -0
  275. package/frontend-vue/tests/form-load-error.test.ts +55 -1
  276. package/frontend-vue/tests/helpers/api-data-runtime.ts +77 -0
  277. package/frontend-vue/tests/image-hydration-ids.test.ts +21 -16
  278. package/frontend-vue/tests/image-reference-preservation.test.ts +59 -53
  279. package/frontend-vue/tests/input-number-adjust.test.ts +14 -0
  280. package/frontend-vue/tests/key-value-list-copy.test.ts +3 -0
  281. package/frontend-vue/tests/member-role-picker.test.ts +4 -2
  282. package/frontend-vue/tests/menu-sync-reconnect.test.ts +47 -0
  283. package/frontend-vue/tests/metadata-retry.test.ts +5 -1
  284. package/frontend-vue/tests/module-routing.test.ts +2 -5
  285. package/frontend-vue/tests/modules-catalog.test.ts +17 -4
  286. package/frontend-vue/tests/modules-listing-transfer.test.ts +95 -0
  287. package/frontend-vue/tests/multi-account-validate.test.ts +23 -1
  288. package/frontend-vue/tests/nav-badge-tones.test.ts +66 -3
  289. package/frontend-vue/tests/notification-counts-from-writes.test.ts +223 -0
  290. package/frontend-vue/tests/notification-counts-transfer.test.ts +88 -0
  291. package/frontend-vue/tests/notification-inbox-display.test.ts +57 -5
  292. package/frontend-vue/tests/notification-popover-errors.test.ts +21 -1
  293. package/frontend-vue/tests/oauth-complete-handoff.test.ts +209 -0
  294. package/frontend-vue/tests/onboarding-middleware.test.ts +1 -1
  295. package/frontend-vue/tests/onboarding-transfer.test.ts +106 -0
  296. package/frontend-vue/tests/page-data-refresh.test.ts +141 -0
  297. package/frontend-vue/tests/page-fill-height.test.ts +16 -3
  298. package/frontend-vue/tests/page-no-visible-blocks.test.ts +12 -2
  299. package/frontend-vue/tests/page-realtime-remount.test.ts +1 -1
  300. package/frontend-vue/tests/page-record-label.test.ts +0 -7
  301. package/frontend-vue/tests/page-validation-redirect.test.ts +12 -2
  302. package/frontend-vue/tests/period-hydration-now.test.ts +185 -0
  303. package/frontend-vue/tests/period-live-pause.test.ts +3 -0
  304. package/frontend-vue/tests/period-reader-zone.test.ts +137 -0
  305. package/frontend-vue/tests/period-scope-first-fetch.test.ts +17 -1
  306. package/frontend-vue/tests/period-ssr.test.ts +195 -0
  307. package/frontend-vue/tests/permission-preview-partial.test.ts +0 -2
  308. package/frontend-vue/tests/permissions-transfer.test.ts +194 -0
  309. package/frontend-vue/tests/post-login-redirect.test.ts +10 -5
  310. package/frontend-vue/tests/profile-personal-info-ssr.test.ts +150 -0
  311. package/frontend-vue/tests/profile-preferences-summary.test.ts +15 -11
  312. package/frontend-vue/tests/realtime-reconnect.test.ts +22 -3
  313. package/frontend-vue/tests/roles-name-errors.test.ts +1 -1
  314. package/frontend-vue/tests/row-side-panel.test.ts +1 -1
  315. package/frontend-vue/tests/security-sessions-load-error.test.ts +3 -1
  316. package/frontend-vue/tests/server-routes-vs-pages.test.ts +26 -0
  317. package/frontend-vue/tests/server-routes.test.ts +582 -0
  318. package/frontend-vue/tests/session-handoff.test.ts +1 -1
  319. package/frontend-vue/tests/settings-reads-ssr.test.ts +232 -0
  320. package/frontend-vue/tests/side-panels.test.ts +8 -1
  321. package/frontend-vue/tests/sign-in-exit-history.test.ts +219 -0
  322. package/frontend-vue/tests/signup-invitation-link.test.ts +56 -8
  323. package/frontend-vue/tests/site-layout-categories.test.ts +42 -0
  324. package/frontend-vue/tests/site-layout-hydration.test.ts +3 -3
  325. package/frontend-vue/tests/site-layout-rendered.test.ts +103 -0
  326. package/frontend-vue/tests/site-layout-resync.test.ts +87 -0
  327. package/frontend-vue/tests/ssr-guard-preload.test.ts +188 -0
  328. package/frontend-vue/tests/table-cell-tones.test.ts +36 -1
  329. package/frontend-vue/tests/table-data-ssr.test.ts +195 -0
  330. package/frontend-vue/tests/table-list-data-requests.test.ts +154 -0
  331. package/frontend-vue/tests/table-retry-loading.test.ts +37 -0
  332. package/frontend-vue/tests/table-source-navigation.test.ts +0 -1
  333. package/frontend-vue/tests/table-source-period-scope.test.ts +0 -1
  334. package/frontend-vue/tests/table-view-views.test.ts +1 -1
  335. package/frontend-vue/tests/top-list-card-actions.test.ts +3 -0
  336. package/frontend-vue/tests/top-list-card-period-badge.test.ts +79 -0
  337. package/frontend-vue/tests/top-list-card-value-suffix.test.ts +3 -0
  338. package/frontend-vue/tests/trailing-deduper.test.ts +22 -0
  339. package/frontend-vue/tests/use-api-data-update.test.ts +119 -0
  340. package/frontend-vue/tests/use-api-data.test.ts +410 -0
  341. package/frontend-vue/tests/user-language.test.ts +8 -5
  342. package/frontend-vue/tests/widget-label-i18n.test.ts +4 -1
  343. package/frontend-vue/vitest.config.ts +6 -0
  344. package/package.json +2 -2
  345. package/skills/dms/SKILL.md +2 -1
  346. package/skills/dms-module/SKILL.md +1 -1
  347. package/skills/dms-pages/SKILL.md +4 -1
  348. package/frontend-vue/layers/dms-core/app/build/composables/page/usePageRecordLabel.ts +0 -9
  349. package/frontend-vue/layers/dms-layout/app/composables/page/usePrefetch.ts +0 -21
  350. package/frontend-vue/layers/dms-ui/app/build/composables/form/useControlError.ts +0 -10
  351. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useServerRenderedAsyncData.ts +0 -23
  352. package/frontend-vue/tests/control-error-path.test.ts +0 -11
  353. package/frontend-vue/tests/modules-listing-per-request.test.ts +0 -104
  354. package/frontend-vue/tests/page-layout-background-refresh.test.ts +0 -321
@@ -125,11 +125,23 @@
125
125
 
126
126
  /* Accent for text, icons and lines. The light theme needs the deep dms-700
127
127
  to stay readable on white; solid fills keep the bright dms-400 through
128
- --dms-accent-fill instead, with dark ink on top, in both themes. */
128
+ --dms-accent-fill instead, with dark ink on top, in both themes. The
129
+ whole accent family derives from the primary palette, so pointing
130
+ `ui.colors.primary` elsewhere leaves no cyan behind. The fill hover and
131
+ its ink sit between shades: on the dms palette they come within a few
132
+ units of the v2 design's #5fe0ec and #04181b. */
129
133
  --ui-primary: var(--ui-color-primary-700);
130
134
  --dms-accent-fill: var(--ui-color-primary-400);
131
- --dms-accent-fill-hover: #5fe0ec;
132
- --dms-accent-on-fill: #04181b;
135
+ --dms-accent-fill-hover: color-mix(
136
+ in srgb,
137
+ var(--ui-color-primary-400) 75%,
138
+ var(--ui-color-primary-200)
139
+ );
140
+ --dms-accent-on-fill: color-mix(
141
+ in srgb,
142
+ var(--ui-color-primary-950) 50%,
143
+ black
144
+ );
133
145
 
134
146
  /* v2 status inks: the light theme uses the 700 shades so success, warning
135
147
  and error text stays readable on white; tints and lines stay bright. */
@@ -191,11 +203,13 @@
191
203
  0 0 0 1px color-mix(in srgb, var(--dms-accent) 30%, transparent),
192
204
  0 0 24px -4px color-mix(in srgb, var(--dms-accent) 45%, transparent);
193
205
  --dms-focus-ring:
194
- 0 0 0 2px var(--ui-bg-muted), 0 0 0 4px rgb(27 162 175 / 0.55);
206
+ 0 0 0 2px var(--ui-bg-muted),
207
+ 0 0 0 4px color-mix(in srgb, var(--ui-color-primary-600) 55%, transparent);
195
208
  /* Command palette: the deepest shadow, plus an accent halo ring. */
196
209
  --dms-shadow-cmdk:
197
210
  0 1px 2px rgb(11 17 20 / 0.06), 0 30px 80px rgb(11 17 20 / 0.18);
198
- --dms-halo-accent: 0 0 0 6px rgb(27 162 175 / 0.12);
211
+ --dms-halo-accent: 0 0 0 6px
212
+ color-mix(in srgb, var(--ui-color-primary-600) 12%, transparent);
199
213
  /* Assistant mode swaps the accent halo for a violet (secondary) one,
200
214
  and tints its surfaces and lines with the same violet. Plain rgb():
201
215
  Chromium fringes the anti-aliased edge of an oklab colour-mix border
@@ -213,16 +227,30 @@
213
227
  0 1px 2px rgb(11 17 20 / 0.06), 0 24px 64px rgb(11 17 20 / 0.16);
214
228
  --dms-overlay: rgb(11 17 20 / 0.28);
215
229
  --dms-glow-accent:
216
- 0 0 0 1px var(--ui-color-primary-600), 0 4px 16px rgb(27 162 175 / 0.18);
230
+ 0 0 0 1px var(--ui-color-primary-600),
231
+ 0 4px 16px
232
+ color-mix(in srgb, var(--ui-color-primary-600) 18%, transparent);
217
233
 
218
234
  --dms-accent: var(--ui-color-primary-500);
219
235
 
220
236
  /* Form controls (v2 --accent-tint*, --error-tint, --border-top,
221
237
  --btn-highlight): the 3px focus halo around fields, the strongest border
222
238
  (unchecked boxes, dropzones) and the highlight on accent fills. */
223
- --dms-accent-tint: rgb(27 162 175 / 0.09);
224
- --dms-accent-tint-strong: rgb(27 162 175 / 0.15);
225
- --dms-accent-line: rgb(27 162 175 / 0.35);
239
+ --dms-accent-tint: color-mix(
240
+ in srgb,
241
+ var(--ui-color-primary-600) 9%,
242
+ transparent
243
+ );
244
+ --dms-accent-tint-strong: color-mix(
245
+ in srgb,
246
+ var(--ui-color-primary-600) 15%,
247
+ transparent
248
+ );
249
+ --dms-accent-line: color-mix(
250
+ in srgb,
251
+ var(--ui-color-primary-600) 35%,
252
+ transparent
253
+ );
226
254
  --dms-error-tint: rgb(220 38 38 / 0.08);
227
255
  --dms-border-top: #c9d1d7;
228
256
  --dms-field-border-hover: color-mix(
@@ -231,7 +259,8 @@
231
259
  var(--ui-text-muted)
232
260
  );
233
261
  --dms-fill-highlight:
234
- inset 0 1px 0 rgb(255 255 255 / 0.45), 0 1px 2px rgb(6 46 53 / 0.18);
262
+ inset 0 1px 0 rgb(255 255 255 / 0.45),
263
+ 0 1px 2px color-mix(in srgb, var(--ui-color-primary-950) 18%, transparent);
235
264
 
236
265
  /* Charts and dashboards (v2 --chart-*, --sparkline, --skeleton): step 1
237
266
  is the main series, step 2 (violet) always the comparison. Lines use
@@ -285,12 +314,12 @@
285
314
  --dms-bg-muted: #0d1114;
286
315
  --dms-canvas-glow: radial-gradient(
287
316
  900px 600px at 85% -10%,
288
- rgb(77 217 230 / 0.055),
317
+ color-mix(in srgb, var(--ui-color-primary-400) 5.5%, transparent),
289
318
  transparent 70%
290
319
  );
291
320
  --dms-stage-glow: radial-gradient(
292
321
  closest-side,
293
- rgb(77 217 230 / 0.1),
322
+ color-mix(in srgb, var(--ui-color-primary-400) 10%, transparent),
294
323
  transparent
295
324
  );
296
325
 
@@ -316,10 +345,12 @@
316
345
  0 0 0 1px color-mix(in srgb, var(--dms-accent) 35%, transparent),
317
346
  0 0 32px -4px color-mix(in srgb, var(--dms-accent) 45%, transparent);
318
347
  --dms-focus-ring:
319
- 0 0 0 2px var(--ui-bg-muted), 0 0 0 4px rgb(77 217 230 / 0.6);
348
+ 0 0 0 2px var(--ui-bg-muted),
349
+ 0 0 0 4px color-mix(in srgb, var(--ui-color-primary-400) 60%, transparent);
320
350
  --dms-shadow-cmdk:
321
351
  0 50px 140px rgb(0 0 0 / 0.7), inset 0 1px 0 rgb(255 255 255 / 0.04);
322
- --dms-halo-accent: 0 0 0 8px rgb(77 217 230 / 0.07);
352
+ --dms-halo-accent: 0 0 0 8px
353
+ color-mix(in srgb, var(--ui-color-primary-400) 7%, transparent);
323
354
  --dms-assistant-halo: 0 0 0 8px rgb(167 139 250 / 0.09);
324
355
  --dms-assistant-tint: rgb(167 139 250 / 0.12);
325
356
  --dms-assistant-line: rgb(167 139 250 / 0.4);
@@ -329,12 +360,25 @@
329
360
  0 40px 120px rgb(0 0 0 / 0.7), inset 0 1px 0 rgb(255 255 255 / 0.04);
330
361
  --dms-overlay: rgb(3 5 6 / 0.62);
331
362
  --dms-glow-accent:
332
- 0 0 0 1px var(--ui-color-primary-400), 0 0 40px rgb(77 217 230 / 0.25);
363
+ 0 0 0 1px var(--ui-color-primary-400),
364
+ 0 0 40px color-mix(in srgb, var(--ui-color-primary-400) 25%, transparent);
333
365
  --dms-accent: var(--ui-color-primary-400);
334
366
 
335
- --dms-accent-tint: rgb(77 217 230 / 0.1);
336
- --dms-accent-tint-strong: rgb(77 217 230 / 0.16);
337
- --dms-accent-line: rgb(77 217 230 / 0.35);
367
+ --dms-accent-tint: color-mix(
368
+ in srgb,
369
+ var(--ui-color-primary-400) 10%,
370
+ transparent
371
+ );
372
+ --dms-accent-tint-strong: color-mix(
373
+ in srgb,
374
+ var(--ui-color-primary-400) 16%,
375
+ transparent
376
+ );
377
+ --dms-accent-line: color-mix(
378
+ in srgb,
379
+ var(--ui-color-primary-400) 35%,
380
+ transparent
381
+ );
338
382
  --dms-error-tint: rgb(248 113 113 / 0.13);
339
383
  --dms-border-top: #3a4550;
340
384
  --dms-field-border-hover: color-mix(
@@ -380,7 +424,11 @@
380
424
  --ui-border-accented: #8a949d;
381
425
  --dms-border-top: #8a949d;
382
426
  --dms-chart-grid: #dfe4e8;
383
- --dms-accent-line: rgb(27 162 175 / 0.6);
427
+ --dms-accent-line: color-mix(
428
+ in srgb,
429
+ var(--ui-color-primary-600) 60%,
430
+ transparent
431
+ );
384
432
  --dms-success-line: rgb(22 163 74 / 0.55);
385
433
  --dms-warning-line: rgb(217 119 6 / 0.6);
386
434
  --dms-error-line: rgb(220 38 38 / 0.55);
@@ -398,7 +446,11 @@
398
446
  --ui-border-accented: #5f6b76;
399
447
  --dms-border-top: #6b7782;
400
448
  --dms-chart-grid: #262e36;
401
- --dms-accent-line: rgb(77 217 230 / 0.6);
449
+ --dms-accent-line: color-mix(
450
+ in srgb,
451
+ var(--ui-color-primary-400) 60%,
452
+ transparent
453
+ );
402
454
  --dms-success-line: rgb(74 222 128 / 0.6);
403
455
  --dms-warning-line: rgb(251 191 36 / 0.6);
404
456
  --dms-error-line: rgb(248 113 113 / 0.6);
@@ -6,6 +6,7 @@ import DashboardBanners from "./DashboardBanners.vue";
6
6
  import GithubStarPrompt from "./GithubStarPrompt.vue";
7
7
  import RolePreviewBar from "./RolePreviewBar.vue";
8
8
  import { useAppWidgets } from "../../../composables/useAppWidgets";
9
+ import { useVisitNavBadges } from "../../composables/navigation/useVisitNavBadges";
9
10
  import {
10
11
  sidePanelWidthStyle,
11
12
  useActiveSidePanel,
@@ -45,6 +46,14 @@ const props = withDefaults(defineProps<Props>(), {
45
46
  });
46
47
 
47
48
  providePageFillHeight(toRef(props, "fillHeight"));
49
+ // A custom page brings no site layout: read with the page, and taken over by
50
+ // the browser before the sidebar, the header and the banners render it.
51
+ useRenderedSiteLayout();
52
+ // The bell's counts, read with the page too, before the header and the page
53
+ // (the inbox, the profile's summary) show them.
54
+ useRenderedBellCounts();
55
+
56
+ useVisitNavBadges();
48
57
 
49
58
  const { widgets: appWidgets } = useAppWidgets();
50
59
  // A fill-height page has nothing to scroll past the dock: no clearance.
@@ -84,9 +84,6 @@ onBeforeUnmount(() =>
84
84
  );
85
85
 
86
86
  const siteLayout = useSiteLayout();
87
- if (!siteLayout.siteLayout.value) {
88
- void siteLayout.loadSiteLayout();
89
- }
90
87
 
91
88
  const favoritePages = useFavoritePages();
92
89
 
@@ -3,7 +3,10 @@ import { formatRelativeTime } from "#dms-core/app/utils/formatter";
3
3
  import NotificationCard from "./NotificationCard.vue";
4
4
  import { useNavBadges } from "#dms-ui/app/build/composables/navigation/useNavBadges";
5
5
  import { settleWidgetRequest } from "./widgetRequest";
6
- import type { UserNotification } from "../../../composables/notification/useNotifications";
6
+ import {
7
+ settledRenderedBellCounts,
8
+ type UserNotification,
9
+ } from "../../../composables/notification/useNotifications";
7
10
  import { resolveNotificationTone } from "../pages/settings/notification/notificationDisplay";
8
11
  import { DMS_TONE_SOLID, iconWellToneOf } from "#dms-ui/app/build/utils/tone";
9
12
  import DmsRowSkeleton from "#dms-ui/app/build/components/skeleton/RowSkeleton.vue";
@@ -20,11 +23,12 @@ const SKELETON_ROW_COUNT = 4;
20
23
  const NOTIFICATIONS_SETTINGS_PAGE = "settings.user.notifications";
21
24
 
22
25
  const { t, locale } = useI18n();
23
- const { loggedIn } = useUserSession();
26
+ const { loggedIn } = useDmsUserSession();
24
27
  const { processI18n } = useTranslation();
25
28
  const {
26
29
  unreadCount,
27
30
  unreadTone,
31
+ areCountsLoaded,
28
32
  notifications,
29
33
  hasMore,
30
34
  fetchBellCounts,
@@ -89,7 +93,9 @@ const handleFormSubmitSuccess = async (event: Event) => {
89
93
 
90
94
  onMounted(async () => {
91
95
  if (!loggedIn.value) return;
92
- await refreshCounts();
96
+ // Read with the page (`useRenderedBellCounts`) unless that read failed.
97
+ if (!areCountsLoaded.value) await settledRenderedBellCounts();
98
+ if (!areCountsLoaded.value) await refreshCounts();
93
99
 
94
100
  window.addEventListener(
95
101
  FormEvents.SUBMIT_SUCCESS,
@@ -67,7 +67,6 @@ const props = defineProps<Props>();
67
67
  const model = defineModel<string[] | null | undefined>();
68
68
 
69
69
  const { t } = useI18n();
70
- const { $authFetch } = useAuthFetch();
71
70
  // The wrapping UFormField's error ("pick at least one role") marks the group
72
71
  // invalid, describes it and rings the pills in the error ink.
73
72
  const { ariaAttrs } = useFormField();
@@ -84,17 +83,19 @@ const showsOwnerHint = computed(() =>
84
83
  props.ownerField ? isOwner.value : props.disabled,
85
84
  );
86
85
 
87
- const options = ref<RoleOptionsResponse>({ roles: [], totalPermissions: 0 });
88
- const isFetching = ref(true);
89
- const isLoading = computed(() => isFetching.value || props.loading);
90
-
91
- onMounted(async () => {
92
- try {
93
- options.value = await $authFetch<RoleOptionsResponse>(props.rolesUrl);
94
- } finally {
95
- isFetching.value = false;
96
- }
97
- });
86
+ // Read with the form: a page-mode form renders the pills on the server; a
87
+ // drawer opened again shows the roles at once and reads them again in the
88
+ // background (one may have been added since).
89
+ const roleOptions = useApiData<RoleOptionsResponse>(() => props.rolesUrl);
90
+ const options = computed<RoleOptionsResponse>(
91
+ () => roleOptions.data.value ?? { roles: [], totalPermissions: 0 },
92
+ );
93
+ const isLoading = computed(
94
+ () =>
95
+ props.loading ||
96
+ roleOptions.status.value === "idle" ||
97
+ roleOptions.status.value === "pending",
98
+ );
98
99
 
99
100
  const selectedIds = computed(() => new Set(model.value ?? []));
100
101
  const roleItems = computed<ChipGroupItem[]>(() =>
@@ -1,4 +1,5 @@
1
1
  <script setup lang="ts">
2
+ import { nextTick } from "vue";
2
3
  import { formatRelativeTime } from "#dms-core/app/utils/formatter";
3
4
  import TableEmpty from "#dms-ui/app/build/components/table/Empty.vue";
4
5
  import TablePagination from "#dms-ui/app/build/components/table/Pagination.vue";
@@ -63,12 +64,21 @@ const sourceOf = (
63
64
  return labelKey ? { label: t(labelKey), isModule: false } : undefined;
64
65
  };
65
66
 
66
- // A change of a row is listed again, from the server.
67
+ // A change made here moves the counts too (the write's answer, then its
68
+ // realtime echo): the watcher below leaves it to the one read that follows it.
69
+ let ownChanges = 0;
70
+
71
+ // A change of a row is listed again, from the server, once.
67
72
  const runThenRefresh = async (action: () => Promise<unknown>) => {
73
+ ownChanges++;
68
74
  try {
69
75
  await action();
76
+ // What the action changed reaches the watcher before the guard drops.
77
+ await nextTick();
70
78
  } catch {
71
79
  toast.add({ title: t("dms.form.error_title"), color: "error" });
80
+ } finally {
81
+ ownChanges--;
72
82
  }
73
83
  await props.context.refresh();
74
84
  };
@@ -88,15 +98,15 @@ const toggleRead = (notification: UserNotification) =>
88
98
  );
89
99
 
90
100
  // What the stream changes elsewhere (a notification arriving, read in
91
- // another tab) is listed again here.
101
+ // another tab) is listed again here: once per change of the count or of the
102
+ // newest notification, not each time the bell's lists are rebuilt.
92
103
  watch(
93
- () => [unreadCount.value, notifications.value[0]?._id],
94
- () => void props.context.refresh(),
104
+ [unreadCount, () => notifications.value[0]?._id],
105
+ () => {
106
+ if (ownChanges === 0) void props.context.refresh();
107
+ },
95
108
  );
96
109
 
97
- onMounted(() => {
98
- if (!catalog.isLoaded.value) void catalog.loadCatalog();
99
- });
100
110
  </script>
101
111
 
102
112
  <template>
@@ -65,7 +65,6 @@ const failureMessage = computed(() => {
65
65
  : t("page.settings.notifications.save_failed_on");
66
66
  });
67
67
 
68
- onMounted(load);
69
68
  </script>
70
69
 
71
70
  <template>
@@ -47,9 +47,9 @@ const toast = useToast();
47
47
  const { $authFetch } = useAuthFetch();
48
48
  const { processApiMessage } = useTranslation();
49
49
  const { user, refresh: refreshSession } = useCurrentUser();
50
- const { upload, resolveUrl } = useProfileAvatar(
51
- () => props.avatarField?.component?.options ?? {},
52
- );
50
+ const avatarOptions = () => props.avatarField?.component?.options ?? {};
51
+ const { upload } = useProfileAvatar(avatarOptions);
52
+ const avatarUrls = useFileReadUrls(avatarOptions().storage);
53
53
  // A refused name or picture shows under its field; a failed request is a
54
54
  // toast.
55
55
  const fieldErrors = useFieldErrors({
@@ -57,13 +57,29 @@ const fieldErrors = useFieldErrors({
57
57
  codes: { [INVALID_AVATAR]: "avatar" },
58
58
  });
59
59
 
60
- const saved = ref<ProfileResponse>({
61
- name: user.value?.name,
62
- email: user.value?.email,
63
- });
60
+ // Read with the page: the server renders the name, the address with its
61
+ // verification, and the picture. The form edits a copy of it.
62
+ const profile = await useApiData<ProfileResponse>(() => props.endpoint);
63
+ const isProfileLoaded = computed(
64
+ () => profile.status.value === "success" || profile.status.value === "error",
65
+ );
66
+
67
+ const saved = ref<ProfileResponse>(
68
+ profile.data.value ?? { name: user.value?.name, email: user.value?.email },
69
+ );
64
70
  const name = ref(saved.value.name ?? "");
65
- const avatar = ref<AvatarValue | null>(null);
66
- const avatarUrl = ref<string | null>(null);
71
+ const avatar = ref<AvatarValue | null>(saved.value.avatar ?? null);
72
+ // A picture picked here shows from the file until the profile is saved.
73
+ const pickedAvatarUrl = ref<string | null>(null);
74
+ const avatarUrl = computed(() => {
75
+ if (!avatar.value) return null;
76
+ return pickedAvatarUrl.value ?? avatarUrls.getUrl(avatar.value.key) ?? null;
77
+ });
78
+ watch(
79
+ () => avatar.value?.key,
80
+ (key) => key && void avatarUrls.resolve(key),
81
+ { immediate: true },
82
+ );
67
83
  const isUploading = ref(false);
68
84
  const isSaving = ref(false);
69
85
  const fileInput = useTemplateRef<HTMLInputElement>("fileInput");
@@ -86,14 +102,12 @@ const canSave = computed(() => !!name.value.trim() && !isUploading.value);
86
102
 
87
103
  watch(name, () => fieldErrors.clear("name"));
88
104
 
89
- async function applyProfile(profile: ProfileResponse): Promise<void> {
105
+ function applyProfile(answer: ProfileResponse): void {
90
106
  fieldErrors.clear();
91
- saved.value = profile;
92
- name.value = profile.name ?? "";
93
- avatar.value = profile.avatar ?? null;
94
- avatarUrl.value = profile.avatar
95
- ? await resolveUrl(profile.avatar.key)
96
- : null;
107
+ saved.value = answer;
108
+ name.value = answer.name ?? "";
109
+ avatar.value = answer.avatar ?? null;
110
+ pickedAvatarUrl.value = null;
97
111
  }
98
112
 
99
113
  function pickFile(): void {
@@ -117,7 +131,7 @@ async function onFileChange(event: Event): Promise<void> {
117
131
  return;
118
132
  }
119
133
  avatar.value = { key: result.key! };
120
- avatarUrl.value = URL.createObjectURL(file);
134
+ pickedAvatarUrl.value = URL.createObjectURL(file);
121
135
  } catch {
122
136
  toast.add({
123
137
  title: t("page.settings.profile.avatar_error"),
@@ -131,11 +145,11 @@ async function onFileChange(event: Event): Promise<void> {
131
145
  function removeAvatar(): void {
132
146
  fieldErrors.clear("avatar");
133
147
  avatar.value = null;
134
- avatarUrl.value = null;
148
+ pickedAvatarUrl.value = null;
135
149
  }
136
150
 
137
- async function discard(): Promise<void> {
138
- await applyProfile(saved.value);
151
+ function discard(): void {
152
+ applyProfile(saved.value);
139
153
  }
140
154
 
141
155
  async function save(): Promise<void> {
@@ -146,11 +160,11 @@ async function save(): Promise<void> {
146
160
  if (!canSave.value) return;
147
161
  isSaving.value = true;
148
162
  try {
149
- const profile = await $authFetch<ProfileResponse>(props.endpoint, {
163
+ const answer = await $authFetch<ProfileResponse>(props.endpoint, {
150
164
  method: "POST",
151
165
  body: { name: name.value.trim(), avatar: avatar.value },
152
166
  });
153
- await applyProfile(profile);
167
+ applyProfile(answer);
154
168
  toast.add({ title: t("page.settings.profile.saved"), color: "success" });
155
169
  await refreshSession();
156
170
  } catch (error) {
@@ -162,17 +176,6 @@ async function save(): Promise<void> {
162
176
  }
163
177
  }
164
178
 
165
- // The verification state comes with the profile, fetched after mount: a
166
- // skeleton holds the badge's place until then.
167
- const isProfileLoaded = ref(false);
168
-
169
- onMounted(async () => {
170
- try {
171
- await applyProfile(await $authFetch<ProfileResponse>(props.endpoint));
172
- } finally {
173
- isProfileLoaded.value = true;
174
- }
175
- });
176
179
  </script>
177
180
 
178
181
  <template>
@@ -7,7 +7,7 @@ import { iconWellToneOf } from "#dms-ui/app/build/utils/tone";
7
7
  import ProfileSummaryRow from "./ProfileSummaryRow.vue";
8
8
 
9
9
  const { t, locale } = useI18n();
10
- const { overview, attention, attentionTone, isUnavailable, refresh } =
10
+ const { overview, attention, attentionTone, isUnavailable } =
11
11
  useSecurityOverview();
12
12
 
13
13
  const needsAttention = computed(() => attention.value.length > 0);
@@ -52,7 +52,6 @@ const placeholder = computed(
52
52
  )}`,
53
53
  );
54
54
 
55
- onMounted(refresh);
56
55
  </script>
57
56
 
58
57
  <template>
@@ -1,4 +1,5 @@
1
- /** A permission of the roles editor tree (`GET /settings/workspace/roles/editor-tree`). */
1
+ import { pageRoutePath } from "#dms-core/app/utils/pageRoutes";
2
+ /** A permission of the roles editor tree (`GET /dms/pages/settings/workspace/roles/editor-tree`). */
2
3
  export interface RolePermissionNode {
3
4
  id: string;
4
5
  label: string;
@@ -17,7 +18,7 @@ export interface RoleMemberPreview {
17
18
  name: string;
18
19
  }
19
20
 
20
- /** One role of the roles list (`GET /settings/workspace/roles/overview`). */
21
+ /** One role of the roles list (`GET /dms/pages/settings/workspace/roles/overview`). */
21
22
  export interface RoleSummary {
22
23
  _id: string;
23
24
  name: string;
@@ -42,7 +43,7 @@ export interface RoleEditorCapabilities {
42
43
  canDelete: boolean;
43
44
  }
44
45
 
45
- /** Payload of `GET /settings/workspace/roles/overview`. */
46
+ /** Payload of `GET /dms/pages/settings/workspace/roles/overview`. */
46
47
  export interface RolesOverview {
47
48
  roles: RoleSummary[];
48
49
  owners: RoleOwnersSummary;
@@ -78,7 +79,7 @@ export const OWNER_ENTRY_ID = "__owner__";
78
79
  export const NEW_ROLE_ENTRY_ID = "__new__";
79
80
 
80
81
  /** Base path of the roles page API. */
81
- export const ROLES_API_PATH = "/settings/workspace/roles";
82
+ export const ROLES_API_PATH = pageRoutePath("/settings/workspace/roles");
82
83
 
83
84
  /** The roles' table data routes, which delete a role. */
84
85
  export const ROLES_TABLE_API_PATH = "/api/tables/roles";
@@ -23,6 +23,12 @@ interface SessionInfo {
23
23
  isCurrent: boolean;
24
24
  }
25
25
 
26
+ /** The sessions, and when they were read: what "active 5 minutes ago" counts from. */
27
+ interface SessionsRead {
28
+ sessions: SessionInfo[];
29
+ readAt: number;
30
+ }
31
+
26
32
  interface SessionsRevokedResponse {
27
33
  count: number;
28
34
  sessionHandoff?: SessionHandoff;
@@ -47,10 +53,25 @@ const adoptSessionHandoff = useSessionHandoff();
47
53
  const { overview, refresh: refreshOverview } = useSecurityOverview();
48
54
  const { formatDate, formatRelative, errorMessage } = useSecurityFormat();
49
55
 
50
- const isLoading = ref(true);
51
- // The list could not be loaded: shown as such, never as "no sessions".
52
- const hasLoadError = ref(false);
53
- const sessions = ref<SessionInfo[]>([]);
56
+ // Read with the page: the server renders the list and hands it over, and
57
+ // the browser says "active 5 minutes ago" as of the same read.
58
+ const list = useApiData<SessionsRead>(SESSIONS_URL, {
59
+ transform: (sessions) => ({
60
+ sessions: sessions as SessionInfo[],
61
+ readAt: Date.now(),
62
+ }),
63
+ });
64
+ const sessions = computed(() => list.data.value?.sessions ?? []);
65
+ const readAt = computed(() => list.data.value?.readAt);
66
+ // The list could not be loaded: shown as such, never as "no sessions". A
67
+ // failed reload keeps the list on screen.
68
+ const hasLoadError = computed(
69
+ () =>
70
+ !list.data.value &&
71
+ !!list.error.value &&
72
+ list.status.value !== "pending",
73
+ );
74
+ const isLoading = computed(() => !list.data.value && !hasLoadError.value);
54
75
  const revokingId = ref<string | null>(null);
55
76
  const isConfirmOpen = ref(false);
56
77
  const isSigningOutOthers = ref(false);
@@ -73,18 +94,17 @@ const sessionName = (session: SessionInfo): string =>
73
94
  const sessionPlace = (session: SessionInfo): string =>
74
95
  session.location || t("page.settings.sessions.unknown_location");
75
96
 
76
- async function fetchSessions(): Promise<void> {
77
- sessions.value = await $authFetch<SessionInfo[]>(SESSIONS_URL);
78
- hasLoadError.value = false;
79
- }
97
+ const fetchSessions = (): Promise<void> => list.refresh({ dedupe: "defer" });
80
98
 
81
99
  async function revoke(session: SessionInfo): Promise<void> {
82
100
  revokingId.value = session._id;
83
101
  try {
84
102
  await $authFetch(`${SESSIONS_URL}/${session._id}`, { method: "DELETE" });
85
- sessions.value = sessions.value.filter(
86
- (entry) => entry._id !== session._id,
87
- );
103
+ if (list.data.value)
104
+ list.data.value = {
105
+ ...list.data.value,
106
+ sessions: sessions.value.filter((entry) => entry._id !== session._id),
107
+ };
88
108
  toast.add({ title: t("page.settings.sessions.revoked"), color: "success" });
89
109
  await refreshOverview();
90
110
  } catch (error) {
@@ -131,34 +151,21 @@ watch(
131
151
  () => overview.value?.activeSessions,
132
152
  (count, previous) => {
133
153
  if (previous !== undefined && count !== sessions.value.length) {
134
- // A failed reload keeps the list on screen.
135
- fetchSessions().catch(() => {});
154
+ void fetchSessions();
136
155
  }
137
156
  },
138
157
  );
139
158
 
140
- async function loadSessions(): Promise<void> {
141
- isLoading.value = true;
142
- try {
143
- await fetchSessions();
144
- } catch {
145
- hasLoadError.value = true;
146
- } finally {
147
- isLoading.value = false;
148
- }
149
- }
150
-
151
159
  const retryActions = computed(() => [
152
160
  {
153
161
  label: t("dms.table.load_error_retry"),
154
162
  icon: "i-ph-arrows-clockwise",
155
163
  color: "neutral" as const,
156
164
  variant: "outline" as const,
157
- onClick: () => void loadSessions(),
165
+ onClick: () => void fetchSessions(),
158
166
  },
159
167
  ]);
160
168
 
161
- onMounted(loadSessions);
162
169
  </script>
163
170
 
164
171
  <template>
@@ -249,7 +256,7 @@ onMounted(loadSessions);
249
256
  <span v-else>
250
257
  {{
251
258
  t("page.settings.security.sessions.active_ago", {
252
- time: formatRelative(session.lastActiveAt),
259
+ time: formatRelative(session.lastActiveAt, readAt),
253
260
  })
254
261
  }}
255
262
  </span>
@@ -323,7 +330,7 @@ onMounted(loadSessions);
323
330
  {{
324
331
  session.isCurrent
325
332
  ? t("page.settings.security.sessions.active_now_lower")
326
- : formatRelative(session.lastActiveAt)
333
+ : formatRelative(session.lastActiveAt, readAt)
327
334
  }}
328
335
  </small>
329
336
  </span>
@@ -152,7 +152,6 @@ const retryActions = computed(() => [
152
152
  },
153
153
  ]);
154
154
 
155
- onMounted(refresh);
156
155
  </script>
157
156
 
158
157
  <template>
@@ -73,7 +73,7 @@ const { formatDate, errorMessage } = useSecurityFormat();
73
73
  const isProcessing = ref(false);
74
74
  const isTotpOpen = ref(false);
75
75
  const totpSetup = ref<TotpSetup | null>(null);
76
- const { user } = useUserSession<User>();
76
+ const { user } = useDmsUserSession<User>();
77
77
  const {
78
78
  codes,
79
79
  isRegenerated,