@antelopejs/dms 0.8.3 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (424) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/data/module-store/catalog.json +71 -0
  3. package/dist/config.d.ts +7 -0
  4. package/dist/config.js +8 -0
  5. package/dist/config.js.map +1 -1
  6. package/dist/implementations/dms/frontend-backend-proxies.d.ts +3 -0
  7. package/dist/implementations/dms/frontend-backend-proxies.js +39 -0
  8. package/dist/implementations/dms/frontend-backend-proxies.js.map +1 -0
  9. package/dist/implementations/dms/frontend-bootstrap.d.ts +1 -0
  10. package/dist/implementations/dms/frontend-bootstrap.js +13 -0
  11. package/dist/implementations/dms/frontend-bootstrap.js.map +1 -1
  12. package/dist/implementations/dms/frontend-modules.d.ts +6 -2
  13. package/dist/implementations/dms/frontend-modules.js +37 -8
  14. package/dist/implementations/dms/frontend-modules.js.map +1 -1
  15. package/dist/implementations/dms/frontend-ready.d.ts +8 -0
  16. package/dist/implementations/dms/frontend-ready.js +34 -0
  17. package/dist/implementations/dms/frontend-ready.js.map +1 -0
  18. package/dist/implementations/dms/manifest.d.ts +7 -0
  19. package/dist/implementations/dms/manifest.js +13 -0
  20. package/dist/implementations/dms/manifest.js.map +1 -1
  21. package/dist/implementations/dms/menu-categories.d.ts +16 -0
  22. package/dist/implementations/dms/menu-categories.js +30 -0
  23. package/dist/implementations/dms/menu-categories.js.map +1 -0
  24. package/dist/implementations/dms/nav-badges.d.ts +0 -8
  25. package/dist/implementations/dms/nav-badges.js +0 -16
  26. package/dist/implementations/dms/nav-badges.js.map +1 -1
  27. package/dist/implementations/dms/page.d.ts +13 -5
  28. package/dist/implementations/dms/page.js +72 -41
  29. package/dist/implementations/dms/page.js.map +1 -1
  30. package/dist/implementations/dms-html-render/index.js +5 -2
  31. package/dist/implementations/dms-html-render/index.js.map +1 -1
  32. package/dist/implementations/dms-notifications/index.d.ts +4 -3
  33. package/dist/implementations/dms-notifications/index.js +14 -6
  34. package/dist/implementations/dms-notifications/index.js.map +1 -1
  35. package/dist/pages/settings/settings-index.js +3 -2
  36. package/dist/pages/settings/settings-index.js.map +1 -1
  37. package/dist/pages/settings/users/invites.js +2 -2
  38. package/dist/pages/settings/users/invites.js.map +1 -1
  39. package/dist/pages/settings/users/member-change-notifications.js +17 -6
  40. package/dist/pages/settings/users/member-change-notifications.js.map +1 -1
  41. package/dist/pages/settings/users/member-invite-form.d.ts +3 -2
  42. package/dist/pages/settings/users/member-invite-form.js +7 -4
  43. package/dist/pages/settings/users/member-invite-form.js.map +1 -1
  44. package/dist/pages/settings/users/member-roles-field.js +2 -1
  45. package/dist/pages/settings/users/member-roles-field.js.map +1 -1
  46. package/dist/pages/settings/users/members.js +4 -1
  47. package/dist/pages/settings/users/members.js.map +1 -1
  48. package/dist/pages/settings/users/notifications.d.ts +6 -0
  49. package/dist/pages/settings/users/notifications.js +23 -15
  50. package/dist/pages/settings/users/notifications.js.map +1 -1
  51. package/dist/pages/settings/users/profile.js +1 -1
  52. package/dist/pages/settings/users/profile.js.map +1 -1
  53. package/dist/pages/settings/users/region-form.d.ts +1 -1
  54. package/dist/pages/settings/users/region-form.js +2 -1
  55. package/dist/pages/settings/users/region-form.js.map +1 -1
  56. package/dist/pages/settings/users/role-editor-store.d.ts +3 -1
  57. package/dist/pages/settings/users/role-editor-store.js +17 -4
  58. package/dist/pages/settings/users/role-editor-store.js.map +1 -1
  59. package/dist/pages/settings/users/roles.js +1 -0
  60. package/dist/pages/settings/users/roles.js.map +1 -1
  61. package/dist/realtime/registry.d.ts +1 -0
  62. package/dist/realtime/registry.js +5 -0
  63. package/dist/realtime/registry.js.map +1 -1
  64. package/dist/routes/index.d.ts +1 -0
  65. package/dist/routes/index.js +1 -0
  66. package/dist/routes/index.js.map +1 -1
  67. package/dist/routes/realtime.d.ts +5 -1
  68. package/dist/routes/realtime.js +16 -9
  69. package/dist/routes/realtime.js.map +1 -1
  70. package/dist/routes/store.d.ts +21 -0
  71. package/dist/routes/store.js +130 -0
  72. package/dist/routes/store.js.map +1 -0
  73. package/dist/store/activity.d.ts +2 -0
  74. package/dist/store/activity.js +12 -0
  75. package/dist/store/activity.js.map +1 -0
  76. package/dist/store/cache.d.ts +8 -0
  77. package/dist/store/cache.js +29 -0
  78. package/dist/store/cache.js.map +1 -0
  79. package/dist/store/catalog.d.ts +4 -0
  80. package/dist/store/catalog.js +69 -0
  81. package/dist/store/catalog.js.map +1 -0
  82. package/dist/store/compatibility.d.ts +3 -0
  83. package/dist/store/compatibility.js +45 -0
  84. package/dist/store/compatibility.js.map +1 -0
  85. package/dist/store/declarations.d.ts +18 -0
  86. package/dist/store/declarations.js +116 -0
  87. package/dist/store/declarations.js.map +1 -0
  88. package/dist/store/frontend-compatibility.d.ts +18 -0
  89. package/dist/store/frontend-compatibility.js +93 -0
  90. package/dist/store/frontend-compatibility.js.map +1 -0
  91. package/dist/store/interfaces.d.ts +6 -0
  92. package/dist/store/interfaces.js +126 -0
  93. package/dist/store/interfaces.js.map +1 -0
  94. package/dist/store/listing.d.ts +2 -0
  95. package/dist/store/listing.js +180 -0
  96. package/dist/store/listing.js.map +1 -0
  97. package/dist/store/operations.d.ts +8 -0
  98. package/dist/store/operations.js +254 -0
  99. package/dist/store/operations.js.map +1 -0
  100. package/dist/store/registry.d.ts +4 -0
  101. package/dist/store/registry.js +61 -0
  102. package/dist/store/registry.js.map +1 -0
  103. package/dist/store/requirements.d.ts +14 -0
  104. package/dist/store/requirements.js +81 -0
  105. package/dist/store/requirements.js.map +1 -0
  106. package/dist/store/runtime.d.ts +11 -0
  107. package/dist/store/runtime.js +78 -0
  108. package/dist/store/runtime.js.map +1 -0
  109. package/dist/store/tarball.d.ts +2 -0
  110. package/dist/store/tarball.js +80 -0
  111. package/dist/store/tarball.js.map +1 -0
  112. package/dist/store/types.d.ts +98 -0
  113. package/dist/store/types.js +3 -0
  114. package/dist/store/types.js.map +1 -0
  115. package/dist/test/integration/account-deletion.test.js +2 -2
  116. package/dist/test/integration/account-deletion.test.js.map +1 -1
  117. package/dist/test/integration/auth/auth-key-rotation.test.js +2 -1
  118. package/dist/test/integration/auth/auth-key-rotation.test.js.map +1 -1
  119. package/dist/test/integration/auth/current-password.test.js +2 -1
  120. package/dist/test/integration/auth/current-password.test.js.map +1 -1
  121. package/dist/test/integration/auth/session-revocation.test.js +2 -1
  122. package/dist/test/integration/auth/session-revocation.test.js.map +1 -1
  123. package/dist/test/integration/auth/sign-in-country.test.js +2 -1
  124. package/dist/test/integration/auth/sign-in-country.test.js.map +1 -1
  125. package/dist/test/integration/auth/two-factor-codes.test.js +2 -1
  126. package/dist/test/integration/auth/two-factor-codes.test.js.map +1 -1
  127. package/dist/test/integration/auth/two-factor-password.test.js +3 -2
  128. package/dist/test/integration/auth/two-factor-password.test.js.map +1 -1
  129. package/dist/test/integration/email-change.test.js +2 -1
  130. package/dist/test/integration/email-change.test.js.map +1 -1
  131. package/dist/test/integration/files/component-files.test.js +2 -2
  132. package/dist/test/integration/files/component-files.test.js.map +1 -1
  133. package/dist/test/integration/member-settings-access.test.js +8 -7
  134. package/dist/test/integration/member-settings-access.test.js.map +1 -1
  135. package/dist/test/integration/menu-access-events.test.d.ts +1 -0
  136. package/dist/test/integration/menu-access-events.test.js +148 -0
  137. package/dist/test/integration/menu-access-events.test.js.map +1 -0
  138. package/dist/test/integration/notification-inbox.test.js +4 -1
  139. package/dist/test/integration/notification-inbox.test.js.map +1 -1
  140. package/dist/test/integration/notification-locked-subjects.test.js +3 -2
  141. package/dist/test/integration/notification-locked-subjects.test.js.map +1 -1
  142. package/dist/test/integration/notification-unread-badge.test.js +51 -5
  143. package/dist/test/integration/notification-unread-badge.test.js.map +1 -1
  144. package/dist/test/integration/page-layout-route-guard.test.d.ts +1 -0
  145. package/dist/test/integration/page-layout-route-guard.test.js +56 -0
  146. package/dist/test/integration/page-layout-route-guard.test.js.map +1 -0
  147. package/dist/test/integration/permission-warnings.test.js +3 -2
  148. package/dist/test/integration/permission-warnings.test.js.map +1 -1
  149. package/dist/test/integration/removed-member-notifications.test.js +3 -2
  150. package/dist/test/integration/removed-member-notifications.test.js.map +1 -1
  151. package/dist/test/integration/role-delete-reassign.test.js +2 -1
  152. package/dist/test/integration/role-delete-reassign.test.js.map +1 -1
  153. package/dist/test/integration/role-permission-ancestors.test.js +2 -1
  154. package/dist/test/integration/role-permission-ancestors.test.js.map +1 -1
  155. package/dist/test/unit/frontend-bootstrap.test.js +13 -0
  156. package/dist/test/unit/frontend-bootstrap.test.js.map +1 -1
  157. package/dist/test/unit/implementations/dms/category-slug-registry.test.js +14 -5
  158. package/dist/test/unit/implementations/dms/category-slug-registry.test.js.map +1 -1
  159. package/dist/test/unit/implementations/dms/frontend-backend-proxies.test.d.ts +1 -0
  160. package/dist/test/unit/implementations/dms/frontend-backend-proxies.test.js +66 -0
  161. package/dist/test/unit/implementations/dms/frontend-backend-proxies.test.js.map +1 -0
  162. package/dist/test/unit/implementations/dms/manifest-strip.test.js +26 -0
  163. package/dist/test/unit/implementations/dms/manifest-strip.test.js.map +1 -1
  164. package/dist/test/unit/implementations/dms/nav-badges.test.js +23 -9
  165. package/dist/test/unit/implementations/dms/nav-badges.test.js.map +1 -1
  166. package/dist/test/unit/implementations/dms-html-render/generate-html.test.js +22 -4
  167. package/dist/test/unit/implementations/dms-html-render/generate-html.test.js.map +1 -1
  168. package/dist/test/unit/interfaces/dms/page-layout-query.test.js +12 -0
  169. package/dist/test/unit/interfaces/dms/page-layout-query.test.js.map +1 -1
  170. package/dist/test/unit/interfaces/dms/page-teardown.test.js +1 -1
  171. package/dist/test/unit/interfaces/dms/page-teardown.test.js.map +1 -1
  172. package/dist/test/unit/pages/settings/member-roles-field.test.js +1 -1
  173. package/dist/test/unit/pages/settings/member-roles-field.test.js.map +1 -1
  174. package/dist/test/unit/pages/settings/role-delete-confirm.test.js.map +1 -1
  175. package/dist/test/unit/realtime/menu-topics.test.d.ts +1 -0
  176. package/dist/test/unit/realtime/menu-topics.test.js +26 -0
  177. package/dist/test/unit/realtime/menu-topics.test.js.map +1 -0
  178. package/dist/test/unit/store/store.test.d.ts +1 -0
  179. package/dist/test/unit/store/store.test.js +127 -0
  180. package/dist/test/unit/store/store.test.js.map +1 -0
  181. package/dist/validation/store.schema.d.ts +39 -0
  182. package/dist/validation/store.schema.js +27 -0
  183. package/dist/validation/store.schema.js.map +1 -0
  184. package/frontend-vue/deferred-plugins.ts +3 -4
  185. package/frontend-vue/dms.email.ts +5 -1
  186. package/frontend-vue/dms.frontend.ts +15 -5
  187. package/frontend-vue/dms.server.ts +74 -0
  188. package/frontend-vue/layers/dms-auth/app/composables/useLogout.ts +1 -1
  189. package/frontend-vue/layers/dms-auth/app/composables/usePostLoginRedirect.ts +16 -3
  190. package/frontend-vue/layers/dms-auth/app/custom-pages/auth/2fa.vue +3 -1
  191. package/frontend-vue/layers/dms-auth/app/custom-pages/auth/backup.vue +3 -1
  192. package/frontend-vue/layers/dms-auth/app/custom-pages/auth/forgot-validation.vue +1 -1
  193. package/frontend-vue/layers/dms-auth/app/custom-pages/auth/oauth-complete.vue +26 -3
  194. package/frontend-vue/layers/dms-auth/app/custom-pages/auth/recover.vue +1 -1
  195. package/frontend-vue/layers/dms-auth/app/custom-pages/auth/signup.vue +22 -25
  196. package/frontend-vue/layers/dms-auth/app/custom-pages/auth/validate.vue +1 -1
  197. package/frontend-vue/layers/dms-auth/app/middleware/auth.ts +5 -7
  198. package/frontend-vue/layers/dms-auth/app/plugins/permissions-preload.ts +17 -0
  199. package/frontend-vue/layers/dms-auth/server/accounts.ts +135 -0
  200. package/frontend-vue/layers/dms-auth/server/actions.ts +251 -0
  201. package/frontend-vue/layers/dms-auth/server/oauth.ts +260 -0
  202. package/frontend-vue/layers/dms-auth/server/session.ts +158 -0
  203. package/frontend-vue/layers/dms-core/app/build/utils/asyncDataScope.ts +47 -0
  204. package/frontend-vue/layers/dms-core/app/build/utils/permission-preview.ts +6 -2
  205. package/frontend-vue/layers/dms-core/app/composables/auth/useApiData.ts +148 -0
  206. package/frontend-vue/layers/dms-core/app/composables/auth/useAuthFetch.ts +7 -20
  207. package/frontend-vue/layers/dms-core/app/composables/auth/useCurrentUser.ts +2 -2
  208. package/frontend-vue/layers/dms-core/app/composables/auth/useMultiAccount.ts +18 -5
  209. package/frontend-vue/layers/dms-core/app/composables/auth/usePermissions.ts +35 -6
  210. package/frontend-vue/layers/dms-core/app/composables/auth/useSessionRecovery.ts +1 -1
  211. package/frontend-vue/layers/dms-core/app/composables/period/compareResolvers.ts +20 -14
  212. package/frontend-vue/layers/dms-core/app/composables/period/periodClock.ts +121 -0
  213. package/frontend-vue/layers/dms-core/app/composables/period/periodRenderNow.ts +42 -0
  214. package/frontend-vue/layers/dms-core/app/composables/period/periodTimeZone.ts +31 -0
  215. package/frontend-vue/layers/dms-core/app/composables/period/presetResolvers.ts +48 -19
  216. package/frontend-vue/layers/dms-core/app/composables/period/usePeriod.ts +33 -4
  217. package/frontend-vue/layers/dms-core/app/composables/period/usePeriodScope.ts +78 -25
  218. package/frontend-vue/layers/dms-core/app/composables/realtime/usePageRealtime.ts +3 -0
  219. package/frontend-vue/layers/dms-core/app/composables/realtime/useUserRealtime.ts +36 -6
  220. package/frontend-vue/layers/dms-core/app/composables/user/useUserRegionalPreferences.ts +1 -1
  221. package/frontend-vue/layers/dms-core/app/plugins/async-data-scope.ts +26 -0
  222. package/frontend-vue/layers/dms-core/app/utils/pageRoutes.ts +22 -0
  223. package/frontend-vue/layers/dms-core/app/utils/regional.ts +36 -3
  224. package/frontend-vue/layers/dms-core/app/utils/trailingDeduper.ts +10 -1
  225. package/frontend-vue/layers/dms-core/shared/types/app-config.ts +48 -4
  226. package/frontend-vue/layers/dms-layout/app/app.config.ts +42 -23
  227. package/frontend-vue/layers/dms-layout/app/assets/css/main.css +72 -20
  228. package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardFrame.vue +9 -0
  229. package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardSidebar.vue +0 -3
  230. package/frontend-vue/layers/dms-layout/app/build/components/notification/NotificationPopover.vue +9 -3
  231. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleStoreDependencyPicker.vue +136 -0
  232. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleStoreRequirements.vue +98 -0
  233. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleStoreTile.vue +247 -29
  234. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleTile.vue +19 -2
  235. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/members/MemberRolePicker.vue +13 -12
  236. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/notification/NotificationInboxDisplay.vue +17 -7
  237. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/notification/NotificationPreferencesForm.vue +0 -1
  238. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/profile/ProfilePersonalInfo.vue +36 -33
  239. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/profile/ProfileSecuritySummary.vue +1 -2
  240. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/roles/role-types.ts +5 -4
  241. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/security/SecuritySessions.vue +35 -28
  242. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/security/SecurityStatus.vue +0 -1
  243. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/security/SecurityTwoFactor.vue +1 -1
  244. package/frontend-vue/layers/dms-layout/app/build/composables/dev-reload/useDevReloadHolder.ts +7 -6
  245. package/frontend-vue/layers/dms-layout/app/build/composables/layout/sidePanelState.ts +1 -1
  246. package/frontend-vue/layers/dms-layout/app/build/composables/navigation/useVisitNavBadges.ts +26 -0
  247. package/frontend-vue/layers/dms-layout/app/build/composables/notification/useNotificationCatalog.ts +25 -23
  248. package/frontend-vue/layers/dms-layout/app/build/composables/notification/useNotificationPreferences.ts +44 -23
  249. package/frontend-vue/layers/dms-layout/app/build/composables/security/useSessionHandoff.ts +1 -1
  250. package/frontend-vue/layers/dms-layout/app/build/composables/settings/profile/preferencesSummary.ts +1 -1
  251. package/frontend-vue/layers/dms-layout/app/build/composables/settings/profile/useAccountData.ts +3 -2
  252. package/frontend-vue/layers/dms-layout/app/build/composables/settings/profile/useProfileAvatar.ts +1 -18
  253. package/frontend-vue/layers/dms-layout/app/build/composables/settings/profile/useProfilePreferences.ts +19 -23
  254. package/frontend-vue/layers/dms-layout/app/build/composables/settings/security/useSecurityFormat.ts +6 -3
  255. package/frontend-vue/layers/dms-layout/app/build/composables/settings/security/useSecurityOverview.ts +13 -18
  256. package/frontend-vue/layers/dms-layout/app/build/utils/module-store-catalog.ts +186 -120
  257. package/frontend-vue/layers/dms-layout/app/build/utils/module-store-install.ts +146 -0
  258. package/frontend-vue/layers/dms-layout/app/build/utils/modules-catalog.ts +8 -6
  259. package/frontend-vue/layers/dms-layout/app/build/utils/publicErrorMessage.ts +19 -0
  260. package/frontend-vue/layers/dms-layout/app/components/navigation/CollapsedMenuItem.vue +0 -11
  261. package/frontend-vue/layers/dms-layout/app/components/navigation/NavigationMenu.vue +3 -13
  262. package/frontend-vue/layers/dms-layout/app/composables/general/useSystemState.ts +7 -11
  263. package/frontend-vue/layers/dms-layout/app/composables/notification/useNotifications.ts +103 -22
  264. package/frontend-vue/layers/dms-layout/app/composables/page/useModuleStore.ts +109 -0
  265. package/frontend-vue/layers/dms-layout/app/composables/page/useModulesListing.ts +25 -102
  266. package/frontend-vue/layers/dms-layout/app/composables/page/usePageComponentPreload.ts +0 -10
  267. package/frontend-vue/layers/dms-layout/app/composables/page/useSiteLayout.ts +177 -96
  268. package/frontend-vue/layers/dms-layout/app/composables/profile/useUserLanguage.ts +3 -2
  269. package/frontend-vue/layers/dms-layout/app/custom-pages/modules.vue +341 -62
  270. package/frontend-vue/layers/dms-layout/app/emails/components/EmailLayout.vue +31 -11
  271. package/frontend-vue/layers/dms-layout/app/error.vue +5 -2
  272. package/frontend-vue/layers/dms-layout/app/middleware/module-routing.global.ts +1 -1
  273. package/frontend-vue/layers/dms-layout/app/middleware/page-leave-guard.global.ts +2 -2
  274. package/frontend-vue/layers/dms-layout/app/pages/[...slug].vue +12 -9
  275. package/frontend-vue/layers/dms-layout/app/plugins/dms-menu-sync.client.ts +31 -3
  276. package/frontend-vue/layers/dms-layout/app/plugins/favicon.ts +58 -0
  277. package/frontend-vue/layers/dms-layout/app/plugins/notification-stream.client.ts +15 -7
  278. package/frontend-vue/layers/dms-layout/app/plugins/profile-sync.client.ts +5 -1
  279. package/frontend-vue/layers/dms-layout/app/plugins/regional-preferences.ts +10 -4
  280. package/frontend-vue/layers/dms-layout/app/plugins/time-zone-cookie.client.ts +12 -0
  281. package/frontend-vue/layers/dms-layout/app/types/module-store.ts +134 -0
  282. package/frontend-vue/layers/dms-layout/app/types/page.ts +13 -0
  283. package/frontend-vue/layers/dms-layout/i18n/locales/layout-en-GB.json +107 -9
  284. package/frontend-vue/layers/dms-layout/i18n/locales/layout-fr-FR.json +107 -9
  285. package/frontend-vue/layers/dms-layout/server/html-render.ts +58 -0
  286. package/frontend-vue/layers/dms-layout/server/render-token.ts +95 -0
  287. package/frontend-vue/layers/dms-onboarding/app/composables/onboarding/useOnboarding.ts +25 -21
  288. package/frontend-vue/layers/dms-onboarding/app/middleware/onboarding.global.ts +1 -1
  289. package/frontend-vue/layers/dms-onboarding/app/plugins/onboarding-preload.ts +9 -0
  290. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/registerDefaults.ts +4 -1
  291. package/frontend-vue/layers/dms-ui/app/build/composables/files/useFileMetadata.ts +162 -0
  292. package/frontend-vue/layers/dms-ui/app/build/composables/form/formRecordLoad.ts +23 -4
  293. package/frontend-vue/layers/dms-ui/app/build/composables/live/usePageDataRefresh.ts +101 -0
  294. package/frontend-vue/layers/dms-ui/app/build/composables/navigation/useNavBadges.ts +33 -6
  295. package/frontend-vue/layers/dms-ui/app/build/composables/period/periodDisplay.ts +19 -16
  296. package/frontend-vue/layers/dms-ui/app/build/composables/period/periodQuery.ts +40 -20
  297. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableFooter.ts +6 -8
  298. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableListData.ts +42 -14
  299. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableRows.ts +19 -8
  300. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViews.ts +1 -1
  301. package/frontend-vue/layers/dms-ui/app/components/chart/ChartCard.vue +5 -2
  302. package/frontend-vue/layers/dms-ui/app/components/flow-canvas/FlowCanvas.vue +9 -9
  303. package/frontend-vue/layers/dms-ui/app/components/form/Form.vue +20 -18
  304. package/frontend-vue/layers/dms-ui/app/components/form/components/Cascader.vue +2 -1
  305. package/frontend-vue/layers/dms-ui/app/components/form/components/File.vue +26 -132
  306. package/frontend-vue/layers/dms-ui/app/components/form/components/Image.vue +40 -105
  307. package/frontend-vue/layers/dms-ui/app/components/form/components/InputAddress.vue +2 -0
  308. package/frontend-vue/layers/dms-ui/app/components/form/components/Relation.vue +4 -4
  309. package/frontend-vue/layers/dms-ui/app/components/period/PeriodSelector.vue +9 -13
  310. package/frontend-vue/layers/dms-ui/app/components/table-view/CascaderPath.vue +2 -1
  311. package/frontend-vue/layers/dms-ui/app/components/table-view/FilePreview.vue +12 -24
  312. package/frontend-vue/layers/dms-ui/app/components/table-view/ImagePreview.vue +17 -29
  313. package/frontend-vue/layers/dms-ui/app/components/table-view/KanbanBoard.vue +7 -3
  314. package/frontend-vue/layers/dms-ui/app/components/table-view/TableView.vue +20 -8
  315. package/frontend-vue/layers/dms-ui/app/components/top-list/TopListCard.vue +8 -11
  316. package/frontend-vue/layers/dms-ui/app/composables/chart/useChartFetch.ts +146 -182
  317. package/frontend-vue/layers/dms-ui/app/composables/live/usePageLiveRefresh.ts +8 -2
  318. package/frontend-vue/layers/dms-ui/app/composables/useFileReadUrls.ts +11 -99
  319. package/frontend-vue/layers/dms-ui/app/utils/blockRefresh.ts +32 -3
  320. package/frontend-vue/package.json +3 -3
  321. package/frontend-vue/pnpm-lock.yaml +16 -22
  322. package/frontend-vue/server/private-options.ts +27 -0
  323. package/frontend-vue/tests/async-data-scope.test.ts +121 -0
  324. package/frontend-vue/tests/auth-middleware-error.test.ts +3 -3
  325. package/frontend-vue/tests/banner-block-fetch.test.ts +3 -0
  326. package/frontend-vue/tests/block-fetch-options-forwarding.test.ts +3 -0
  327. package/frontend-vue/tests/block-items-period-realtime.test.ts +3 -0
  328. package/frontend-vue/tests/block-not-found.test.ts +16 -2
  329. package/frontend-vue/tests/block-page-refresh.test.ts +3 -0
  330. package/frontend-vue/tests/block-route-params.test.ts +3 -0
  331. package/frontend-vue/tests/branding-defaults.test.ts +34 -0
  332. package/frontend-vue/tests/builtin-emails.test.ts +23 -2
  333. package/frontend-vue/tests/chart-live-pause.test.ts +4 -1
  334. package/frontend-vue/tests/chart-value-propagation.test.ts +4 -1
  335. package/frontend-vue/tests/composed-text-blocks.test.ts +3 -0
  336. package/frontend-vue/tests/email-layout.test.ts +66 -4
  337. package/frontend-vue/tests/error-page-message.test.ts +25 -2
  338. package/frontend-vue/tests/favicon.test.ts +131 -0
  339. package/frontend-vue/tests/file-fields-ssr.test.ts +145 -0
  340. package/frontend-vue/tests/file-read-urls.test.ts +145 -34
  341. package/frontend-vue/tests/file-unavailable.test.ts +5 -1
  342. package/frontend-vue/tests/fixtures/dataEngine.ts +44 -0
  343. package/frontend-vue/tests/form-load-error.test.ts +55 -1
  344. package/frontend-vue/tests/helpers/api-data-runtime.ts +77 -0
  345. package/frontend-vue/tests/image-hydration-ids.test.ts +21 -16
  346. package/frontend-vue/tests/image-reference-preservation.test.ts +59 -53
  347. package/frontend-vue/tests/input-number-adjust.test.ts +14 -0
  348. package/frontend-vue/tests/key-value-list-copy.test.ts +3 -0
  349. package/frontend-vue/tests/member-role-picker.test.ts +4 -2
  350. package/frontend-vue/tests/menu-sync-reconnect.test.ts +47 -0
  351. package/frontend-vue/tests/metadata-retry.test.ts +5 -1
  352. package/frontend-vue/tests/module-routing.test.ts +2 -5
  353. package/frontend-vue/tests/module-store-catalog.test.ts +270 -51
  354. package/frontend-vue/tests/modules-catalog.test.ts +17 -4
  355. package/frontend-vue/tests/modules-listing-transfer.test.ts +95 -0
  356. package/frontend-vue/tests/multi-account-validate.test.ts +23 -1
  357. package/frontend-vue/tests/nav-badge-tones.test.ts +66 -3
  358. package/frontend-vue/tests/notification-counts-from-writes.test.ts +223 -0
  359. package/frontend-vue/tests/notification-counts-transfer.test.ts +88 -0
  360. package/frontend-vue/tests/notification-inbox-display.test.ts +57 -5
  361. package/frontend-vue/tests/notification-popover-errors.test.ts +21 -1
  362. package/frontend-vue/tests/oauth-complete-handoff.test.ts +209 -0
  363. package/frontend-vue/tests/onboarding-middleware.test.ts +1 -1
  364. package/frontend-vue/tests/onboarding-transfer.test.ts +106 -0
  365. package/frontend-vue/tests/page-data-refresh.test.ts +141 -0
  366. package/frontend-vue/tests/page-fill-height.test.ts +16 -3
  367. package/frontend-vue/tests/page-no-visible-blocks.test.ts +12 -2
  368. package/frontend-vue/tests/page-realtime-remount.test.ts +1 -1
  369. package/frontend-vue/tests/page-record-label.test.ts +0 -7
  370. package/frontend-vue/tests/page-validation-redirect.test.ts +12 -2
  371. package/frontend-vue/tests/period-hydration-now.test.ts +185 -0
  372. package/frontend-vue/tests/period-live-pause.test.ts +3 -0
  373. package/frontend-vue/tests/period-reader-zone.test.ts +137 -0
  374. package/frontend-vue/tests/period-scope-first-fetch.test.ts +17 -1
  375. package/frontend-vue/tests/period-ssr.test.ts +195 -0
  376. package/frontend-vue/tests/permission-preview-partial.test.ts +0 -2
  377. package/frontend-vue/tests/permissions-transfer.test.ts +194 -0
  378. package/frontend-vue/tests/post-login-redirect.test.ts +10 -5
  379. package/frontend-vue/tests/profile-personal-info-ssr.test.ts +150 -0
  380. package/frontend-vue/tests/profile-preferences-summary.test.ts +15 -11
  381. package/frontend-vue/tests/realtime-reconnect.test.ts +22 -3
  382. package/frontend-vue/tests/roles-name-errors.test.ts +1 -1
  383. package/frontend-vue/tests/row-side-panel.test.ts +1 -1
  384. package/frontend-vue/tests/security-sessions-load-error.test.ts +3 -1
  385. package/frontend-vue/tests/server-routes-vs-pages.test.ts +26 -0
  386. package/frontend-vue/tests/server-routes.test.ts +582 -0
  387. package/frontend-vue/tests/session-handoff.test.ts +1 -1
  388. package/frontend-vue/tests/settings-reads-ssr.test.ts +232 -0
  389. package/frontend-vue/tests/side-panels.test.ts +8 -1
  390. package/frontend-vue/tests/sign-in-exit-history.test.ts +219 -0
  391. package/frontend-vue/tests/signup-invitation-link.test.ts +56 -8
  392. package/frontend-vue/tests/site-layout-categories.test.ts +42 -0
  393. package/frontend-vue/tests/site-layout-hydration.test.ts +3 -3
  394. package/frontend-vue/tests/site-layout-rendered.test.ts +103 -0
  395. package/frontend-vue/tests/site-layout-resync.test.ts +87 -0
  396. package/frontend-vue/tests/ssr-guard-preload.test.ts +188 -0
  397. package/frontend-vue/tests/table-cell-tones.test.ts +36 -1
  398. package/frontend-vue/tests/table-data-ssr.test.ts +195 -0
  399. package/frontend-vue/tests/table-list-data-requests.test.ts +154 -0
  400. package/frontend-vue/tests/table-retry-loading.test.ts +37 -0
  401. package/frontend-vue/tests/table-source-navigation.test.ts +0 -1
  402. package/frontend-vue/tests/table-source-period-scope.test.ts +0 -1
  403. package/frontend-vue/tests/table-view-views.test.ts +1 -1
  404. package/frontend-vue/tests/top-list-card-actions.test.ts +3 -0
  405. package/frontend-vue/tests/top-list-card-period-badge-ssr.test.ts +212 -0
  406. package/frontend-vue/tests/top-list-card-period-badge.test.ts +79 -0
  407. package/frontend-vue/tests/top-list-card-value-suffix.test.ts +3 -0
  408. package/frontend-vue/tests/trailing-deduper.test.ts +22 -0
  409. package/frontend-vue/tests/use-api-data-update.test.ts +119 -0
  410. package/frontend-vue/tests/use-api-data.test.ts +410 -0
  411. package/frontend-vue/tests/user-language.test.ts +8 -5
  412. package/frontend-vue/tests/widget-label-i18n.test.ts +4 -1
  413. package/frontend-vue/vitest.config.ts +6 -0
  414. package/package.json +2 -2
  415. package/skills/dms/SKILL.md +2 -1
  416. package/skills/dms-module/SKILL.md +1 -1
  417. package/skills/dms-pages/SKILL.md +4 -1
  418. package/frontend-vue/layers/dms-core/app/build/composables/page/usePageRecordLabel.ts +0 -9
  419. package/frontend-vue/layers/dms-layout/app/composables/page/usePrefetch.ts +0 -21
  420. package/frontend-vue/layers/dms-ui/app/build/composables/form/useControlError.ts +0 -10
  421. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useServerRenderedAsyncData.ts +0 -23
  422. package/frontend-vue/tests/control-error-path.test.ts +0 -11
  423. package/frontend-vue/tests/modules-listing-per-request.test.ts +0 -104
  424. package/frontend-vue/tests/page-layout-background-refresh.test.ts +0 -321
@@ -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,
@@ -256,7 +256,8 @@ async function awaitRoute(
256
256
  }
257
257
 
258
258
  // Deadline spent waiting on someone else's pass.
259
- if (state.refreshing || deps.now() >= deadline) return deps.isRouteKnown(path);
259
+ if (state.refreshing || deps.now() >= deadline)
260
+ return deps.isRouteKnown(path);
260
261
 
261
262
  const committed = await runRefresh(state, path, deadline);
262
263
  // A trailing re-run booked over ours may be the one that brought the route
@@ -293,14 +294,14 @@ export function createDevReloadCoordinator(
293
294
  // has been (re)committed for the current route. Kept separate from the layout
294
295
  // commit so the commit can stay atomic with its route check (see
295
296
  // probeAndCommitRoute) — the 404-prone part was committing the layout, not this.
296
- async function postLayoutRefresh(currentRoutePath: string): Promise<void> {
297
+ // The page layouts reload with the layout commit, which empties their cache:
298
+ // no async data key holds them.
299
+ async function postLayoutRefresh(): Promise<void> {
297
300
  const permissions = usePermissions();
298
301
  // fetchPermissions() refetches and swaps the set atomically, so there is no
299
302
  // need to clear() first — clearing would briefly empty permissions and flash
300
303
  // permission-gated UI.
301
304
  await permissions.fetchPermissions();
302
- await refreshDmsData(currentRoutePath);
303
- await refreshDmsData(`layout-${currentRoutePath}`);
304
305
  }
305
306
 
306
307
  interface DevReloadHolder {
@@ -326,7 +327,7 @@ export function useDevReloadHolder(): DevReloadHolder {
326
327
  const reloading = import.meta.env.DEV ? useDevReloading() : ref(false);
327
328
  const siteLayout = useSiteLayout();
328
329
  const router = useDmsRouter();
329
- const currentError = useError();
330
+ const currentError = useDmsError();
330
331
 
331
332
  const coordinator = createDevReloadCoordinator({
332
333
  isDev: import.meta.env.DEV,
@@ -343,7 +344,7 @@ export function useDevReloadHolder(): DevReloadHolder {
343
344
  if (router.currentRoute.value.path !== recoveredPath) return;
344
345
  const err = currentError.value;
345
346
  if (!err || err.statusCode !== HTTP_NOT_FOUND) return;
346
- void clearError();
347
+ void clearDmsError();
347
348
  },
348
349
  setReloading: (value) => {
349
350
  reloading.value = value;
@@ -172,7 +172,7 @@ export function useActiveSidePanel(): ActiveSidePanel {
172
172
  const { panels } = useAppSidePanels();
173
173
  const preferences = useSidePanelPreferences();
174
174
  const dragWidth = useSidePanelDragWidth();
175
- const { loggedIn } = useUserSession();
175
+ const { loggedIn } = useDmsUserSession();
176
176
  const panel = computed(() => {
177
177
  if (!loggedIn.value) return null;
178
178
  const { openId } = readSidePanelPreferences(preferences.value);
@@ -0,0 +1,26 @@
1
+ import { watch } from "vue";
2
+ import { useDmsInertiaPage } from "#dms/frontend-module";
3
+ import {
4
+ type CountedNavBadge,
5
+ useNavBadges,
6
+ } from "#dms-ui/app/build/composables/navigation/useNavBadges";
7
+
8
+ interface CountedPage {
9
+ navBadges?: Record<string, CountedNavBadge>;
10
+ }
11
+
12
+ /**
13
+ * Draws the navigation badges each page visit carries (`navBadges`): they
14
+ * are counted for the visit and travel with the page, not with the site
15
+ * layout a browser keeps for minutes. The dashboard frame calls it, so the
16
+ * server render draws them as the browser does.
17
+ */
18
+ export function useVisitNavBadges(): void {
19
+ const inertiaPage = useDmsInertiaPage();
20
+ const { applyCountedNavBadges } = useNavBadges();
21
+ watch(
22
+ () => (inertiaPage.props.page as CountedPage | undefined)?.navBadges,
23
+ applyCountedNavBadges,
24
+ { immediate: true },
25
+ );
26
+ }
@@ -1,3 +1,4 @@
1
+ import { pageRoutePath } from "#dms-core/app/utils/pageRoutes";
1
2
  export type NotificationTogglePermission = "allowed" | "forbidden" | "default";
2
3
 
3
4
  export interface NotificationCategory {
@@ -32,34 +33,34 @@ export const buildPreferenceKey = (categoryId: string, subjectId: string) =>
32
33
  export const subjectPreferenceKey = (subject: NotificationSubject) =>
33
34
  buildPreferenceKey(subject.category.id, subject.id);
34
35
 
36
+ const CATALOG_URL = pageRoutePath(
37
+ "/settings/user/notifications",
38
+ "/categories",
39
+ );
40
+
41
+ /** Whether a read has answered with a failure, and no retry is running. */
42
+ export const hasFailed = (read: ApiDataResult<unknown>): boolean =>
43
+ !!read.error.value && read.status.value !== "pending";
44
+
35
45
  /**
36
- * The registered notification categories and subjects, loaded once and
37
- * shared by the preferences matrix and the inbox (which tags each
46
+ * The registered notification categories and subjects, read once with the
47
+ * page and shared by the preferences matrix and the inbox (which tags each
38
48
  * notification with its source).
49
+ *
50
+ * @param enabled Whether the page shows them: nothing is read otherwise
39
51
  */
40
- export const useNotificationCatalog = () => {
41
- const { $authFetch } = useAuthFetch();
42
- const categories = useDmsState<NotificationCategory[]>(
43
- "notification-catalog-categories",
44
- () => [],
45
- );
46
- const subjects = useDmsState<NotificationSubject[]>(
47
- "notification-catalog-subjects",
48
- () => [],
49
- );
50
- const isLoaded = useDmsState<boolean>(
51
- "notification-catalog-loaded",
52
- () => false,
52
+ export const useNotificationCatalog = (enabled: () => boolean = () => true) => {
53
+ const catalog = useApiData<NotificationCatalogResponse>(() =>
54
+ enabled() ? CATALOG_URL : null,
53
55
  );
56
+ const categories = computed(() => catalog.data.value?.categories ?? []);
57
+ const subjects = computed(() => catalog.data.value?.subjects ?? []);
58
+ const isLoaded = computed(() => !!catalog.data.value);
59
+ const loadFailed = computed(() => !isLoaded.value && hasFailed(catalog));
54
60
 
55
- const loadCatalog = async () => {
56
- const response = await $authFetch<NotificationCatalogResponse>(
57
- "/settings/user/notifications/categories",
58
- );
59
- categories.value = response.categories;
60
- subjects.value = response.subjects;
61
- isLoaded.value = true;
62
- };
61
+ /** Reads the catalog again (a retry). */
62
+ const loadCatalog = (): Promise<void> =>
63
+ catalog.refresh({ dedupe: "defer" });
63
64
 
64
65
  const subjectsOf = (categoryId: string) =>
65
66
  subjects.value.filter((subject) => subject.category.id === categoryId);
@@ -77,6 +78,7 @@ export const useNotificationCatalog = () => {
77
78
  categories,
78
79
  subjects,
79
80
  isLoaded,
81
+ loadFailed,
80
82
  loadCatalog,
81
83
  subjectsOf,
82
84
  findCategory,
@@ -1,6 +1,8 @@
1
+ import { pageRoutePath } from "#dms-core/app/utils/pageRoutes";
1
2
  import type { SaveStatusState } from "#dms-ui/app/components/save-bar/SaveStatus.vue";
2
3
  import { useInstantSave } from "#dms-ui/app/build/composables/instant-save/useInstantSave";
3
4
  import {
5
+ hasFailed,
4
6
  type NotificationCategory,
5
7
  type NotificationSubject,
6
8
  subjectPreferenceKey,
@@ -9,19 +11,45 @@ import {
9
11
 
10
12
  type PreferenceMap = Record<string, boolean>;
11
13
 
12
- const PREFERENCES_URL = "/settings/user/notifications/preferences";
14
+ const PREFERENCES_URL = pageRoutePath(
15
+ "/settings/user/notifications",
16
+ "/preferences",
17
+ );
13
18
 
14
19
  /**
15
- * State of the notification preferences matrix. Each switch saves on its
16
- * own through the shared instant save (a PATCH of the keys it changes); a
17
- * failed save puts the switches back and keeps the change for a retry.
20
+ * State of the notification preferences matrix, read with the page (the
21
+ * stored preferences and the catalog). Each switch saves on its own through
22
+ * the shared instant save (a PATCH of the keys it changes); a failed save
23
+ * puts the switches back and keeps the change for a retry.
24
+ *
25
+ * @param enabled Whether the page shows them: nothing is read otherwise
18
26
  */
19
- export const useNotificationPreferences = () => {
27
+ export const useNotificationPreferences = (
28
+ enabled: () => boolean = () => true,
29
+ ) => {
20
30
  const { $authFetch } = useAuthFetch();
21
- const catalog = useNotificationCatalog();
22
- const preferences = ref<PreferenceMap>({});
23
- const isLoading = ref(true);
24
- const loadFailed = ref(false);
31
+ const catalog = useNotificationCatalog(enabled);
32
+ const stored = useApiData<PreferenceMap>(() =>
33
+ enabled() ? PREFERENCES_URL : null,
34
+ );
35
+ // The switches edit the read's data in place.
36
+ const preferences = computed<PreferenceMap>({
37
+ get: () => stored.data.value ?? {},
38
+ set: (value) => {
39
+ stored.data.value = value;
40
+ },
41
+ });
42
+ // A user who never changed a switch has no stored preferences (null).
43
+ const isStoredRead = computed(
44
+ () => stored.status.value === "success" || stored.data.value != null,
45
+ );
46
+ const loadFailed = computed(
47
+ () => (!isStoredRead.value && hasFailed(stored)) || catalog.loadFailed.value,
48
+ );
49
+ const isLoading = computed(
50
+ () =>
51
+ !(isStoredRead.value && catalog.isLoaded.value) && !loadFailed.value,
52
+ );
25
53
 
26
54
  const instant = useInstantSave<PreferenceMap>({
27
55
  read: (key) => preferences.value[key] ?? true,
@@ -54,21 +82,14 @@ export const useNotificationPreferences = () => {
54
82
  ),
55
83
  );
56
84
 
85
+ /** Reads the preferences and the catalog again (a retry). */
57
86
  const load = async () => {
58
- isLoading.value = true;
59
- loadFailed.value = false;
60
- try {
61
- const [stored] = await Promise.all([
62
- $authFetch<PreferenceMap>(PREFERENCES_URL),
63
- catalog.loadCatalog(),
64
- ]);
65
- preferences.value = stored ?? {};
66
- instant.confirm(preferences.value);
67
- } catch {
68
- loadFailed.value = true;
69
- } finally {
70
- isLoading.value = false;
71
- }
87
+ await Promise.all([
88
+ stored.refresh({ dedupe: "defer" }),
89
+ catalog.loadCatalog(),
90
+ ]);
91
+ // A failed save then puts back what the server holds now.
92
+ if (stored.status.value === "success") instant.confirm(preferences.value);
72
93
  };
73
94
 
74
95
  return {
@@ -28,7 +28,7 @@ interface BrowserSession {
28
28
  * account entry, and the old entry — holding a dead refresh token — is dropped.
29
29
  */
30
30
  export function useSessionHandoff() {
31
- const { session, fetch: refreshUser } = useUserSession<
31
+ const { session, fetch: refreshUser } = useDmsUserSession<
32
32
  unknown,
33
33
  BrowserSession
34
34
  >();
@@ -59,7 +59,7 @@ export function resolvePreferenceRows(
59
59
  };
60
60
  }
61
61
 
62
- /** What `GET /settings/user/profile/access` answers. */
62
+ /** What `GET /dms/pages/settings/user/profile/access` answers. */
63
63
  export interface ProfileAccess {
64
64
  roles: string[];
65
65
  workspaceOwner: boolean;
@@ -1,3 +1,4 @@
1
+ import { pageRoutePath } from "#dms-core/app/utils/pageRoutes";
1
2
  import UFormField from "@nuxt/ui/components/FormField.vue";
2
3
  import UIcon from "@nuxt/ui/runtime/vue/components/Icon.vue";
3
4
  import UInput from "@nuxt/ui/components/Input.vue";
@@ -12,7 +13,7 @@ import { resolveFieldErrors } from "#dms-core/app/composables/useFieldErrors";
12
13
  import { REQUIRED_MESSAGE } from "#dms-core/app/composables/useFormValidation";
13
14
  import { useSecurityFormat } from "../security/useSecurityFormat";
14
15
 
15
- const PROFILE_ENDPOINT = "/settings/user/profile";
16
+ const PROFILE_ENDPOINT = pageRoutePath("/settings/user/profile");
16
17
  const EXPORT_ENDPOINT = `${PROFILE_ENDPOINT}/account-export`;
17
18
  const DELETION_ENDPOINT = `${PROFILE_ENDPOINT}/account-deletion`;
18
19
  const EXPORT_FILE_PREFIX = "dms-account-export";
@@ -64,7 +65,7 @@ export function accountExportFilename(at: Date = new Date()): string {
64
65
  export function useAccountData() {
65
66
  const { $authFetch } = useAuthFetch();
66
67
  const { confirm } = useConfirm();
67
- const { user, clear } = useUserSession<User>();
68
+ const { user, clear } = useDmsUserSession<User>();
68
69
  const { removeAccount } = useMultiAccount();
69
70
  const { processI18n, processApiMessage } = useTranslation();
70
71
  const { errorMessage, errorCode } = useSecurityFormat();
@@ -12,7 +12,6 @@ import {
12
12
  } from "#dms-ui/app/utils/imageResize";
13
13
 
14
14
  const PRESIGN_URL = "/api/files/presign";
15
- const METADATA_URL = "/api/files/metadata";
16
15
 
17
16
  /** Upload options the backend serialized for the avatar image field. */
18
17
  export interface AvatarFieldOptions {
@@ -25,10 +24,6 @@ export interface AvatarFieldOptions {
25
24
  /** Why a picked file was refused before any upload. */
26
25
  export type AvatarRejection = "mimetype" | "size";
27
26
 
28
- interface FileMetadataResponse {
29
- url: string;
30
- }
31
-
32
27
  /** Outcome of an avatar upload: a staged key, or why it was refused. */
33
28
  export interface AvatarUploadResult {
34
29
  key?: string;
@@ -79,17 +74,5 @@ export function useProfileAvatar(options: () => AvatarFieldOptions) {
79
74
  return { key: presign.resourceKey };
80
75
  }
81
76
 
82
- /** @returns A short-lived URL to display a stored avatar */
83
- async function resolveUrl(key: string): Promise<string | null> {
84
- try {
85
- const metadata = await $authFetch<FileMetadataResponse>(METADATA_URL, {
86
- query: { resourceKey: key, storage: options().storage },
87
- });
88
- return metadata.url;
89
- } catch {
90
- return null;
91
- }
92
- }
93
-
94
- return { progress, upload, resolveUrl };
77
+ return { progress, upload };
95
78
  }
@@ -1,3 +1,4 @@
1
+ import { pageRoutePath } from "#dms-core/app/utils/pageRoutes";
1
2
  import { usePreferredDark } from "@vueuse/core";
2
3
  import { computed, onMounted, ref } from "vue";
3
4
  import { useUserRegionalPreferences } from "#dms-core/app/composables/user/useUserRegionalPreferences";
@@ -24,7 +25,10 @@ import {
24
25
  tallySubjects,
25
26
  } from "./preferencesSummary";
26
27
 
27
- export const PROFILE_ACCESS_ENDPOINT = "/settings/user/profile/access";
28
+ export const PROFILE_ACCESS_ENDPOINT = pageRoutePath(
29
+ "/settings/user/profile",
30
+ "/access",
31
+ );
28
32
  // Sample values the skeletons are shaped from.
29
33
  const SAMPLE_ACCESS: ProfileAccess = {
30
34
  roles: ["Admin", "Finance"],
@@ -50,16 +54,15 @@ const MONDAY = 1;
50
54
  * and each one's sentence, `undefined` while it is not known yet (the row
51
55
  * shows a skeleton then, never a made-up "0 unread").
52
56
  *
53
- * Nothing is awaited before render. The roles and the notification
54
- * preferences are fetched after mount; the unread count is the header bell's
55
- * own; the regional and appearance sentences, which depend on the browser
56
- * (time zone, today's date, the system theme), are written after mount so
57
- * the server and the hydrating browser render the same skeleton.
57
+ * The roles and the notification preferences are read with the page (a
58
+ * server render shows them and hands them over); the unread count is the
59
+ * header bell's own; the regional and appearance sentences, which depend on
60
+ * the browser (time zone, today's date, the system theme), are written after
61
+ * mount so the server and the hydrating browser render the same skeleton.
58
62
  */
59
63
  export function useProfilePreferences() {
60
64
  const { t: translate, locale } = useI18n();
61
65
  const t = translate as SummaryTranslate;
62
- const { $authFetch } = useAuthFetch();
63
66
  const { groups } = useSettingsNavigation();
64
67
  const isOwner = useIsOwner();
65
68
  const isMounted = ref(false);
@@ -76,23 +79,14 @@ export function useProfilePreferences() {
76
79
  ),
77
80
  );
78
81
 
79
- const access = ref<ProfileAccess>();
80
- const accessFailed = ref(false);
82
+ const access = useApiData<ProfileAccess>(PROFILE_ACCESS_ENDPOINT);
81
83
  const accessSummary = computed(() => {
82
- if (access.value) return buildAccessSummary(t, access.value);
83
- return accessFailed.value
84
+ if (access.data.value) return buildAccessSummary(t, access.data.value);
85
+ return access.error.value
84
86
  ? t("page.settings.profile.preferences.access_unavailable")
85
87
  : undefined;
86
88
  });
87
89
 
88
- async function loadAccess(): Promise<void> {
89
- try {
90
- access.value = await $authFetch<ProfileAccess>(PROFILE_ACCESS_ENDPOINT);
91
- } catch {
92
- accessFailed.value = true;
93
- }
94
- }
95
-
96
90
  const { uniqueLocales } = useUniqueLocales();
97
91
  const regional = useUserRegionalPreferences();
98
92
  // Frozen at mount: the row shows formats, not a ticking clock.
@@ -117,8 +111,12 @@ export function useProfilePreferences() {
117
111
  });
118
112
  });
119
113
 
120
- const notificationPreferences = useNotificationPreferences();
121
- const { subjects } = useNotificationCatalog();
114
+ const notificationPreferences = useNotificationPreferences(
115
+ () => rows.value.notifications.visible,
116
+ );
117
+ const { subjects } = useNotificationCatalog(
118
+ () => rows.value.notifications.visible,
119
+ );
122
120
  const { unreadCount, unreadTone, areCountsLoaded } = useNotifications();
123
121
  const isNotificationsLoading = computed(
124
122
  () => notificationPreferences.isLoading.value || !areCountsLoaded.value,
@@ -188,8 +186,6 @@ export function useProfilePreferences() {
188
186
  onMounted(() => {
189
187
  today = new Date();
190
188
  isMounted.value = true;
191
- void loadAccess();
192
- if (rows.value.notifications.visible) void notificationPreferences.load();
193
189
  });
194
190
 
195
191
  return {
@@ -53,9 +53,12 @@ export function useSecurityFormat() {
53
53
  Math.floor((Date.now() - new Date(value).getTime()) / MS_PER_DAY),
54
54
  );
55
55
 
56
- /** "3 days ago", "in an hour"… or "now" under a minute. */
57
- const formatRelative = (value: DateInput): string => {
58
- const elapsed = new Date(value).getTime() - Date.now();
56
+ /**
57
+ * "3 days ago", "in an hour"… or "now" under a minute, as of `now` (a
58
+ * list's read time, which the server and the hydrating browser share).
59
+ */
60
+ const formatRelative = (value: DateInput, now = Date.now()): string => {
61
+ const elapsed = new Date(value).getTime() - now;
59
62
  const formatter = new Intl.RelativeTimeFormat(locale.value, {
60
63
  numeric: "auto",
61
64
  });
@@ -1,8 +1,9 @@
1
+ import { pageRoutePath } from "#dms-core/app/utils/pageRoutes";
1
2
  import { useNavBadges } from "#dms-ui/app/build/composables/navigation/useNavBadges";
2
3
  import type { Tone } from "#dms-ui/app/types/tone";
3
4
 
4
5
  /** Base URL of the Security page API. */
5
- export const SECURITY_ENDPOINT = "/settings/user/security";
6
+ export const SECURITY_ENDPOINT = pageRoutePath("/settings/user/security");
6
7
  /** Route of the Security page. */
7
8
  export const SECURITY_PAGE_PATH = "/settings/user/security";
8
9
  const SECURITY_PAGE_ID = "settings.user.security";
@@ -68,21 +69,19 @@ const ATTENTION_TONE_ORDER: Tone[] = [
68
69
 
69
70
  /**
70
71
  * Shared Security summary: the status strip, every Security block and the
71
- * profile pointer card read the same state, and any change refreshes it for
72
- * all of them. It also keeps the navigation badge of the Security page, the
72
+ * profile pointer card read the same request, with the page (a server
73
+ * render shows it and hands it over), and any change refreshes it for all
74
+ * of them. It also keeps the navigation badge of the Security page, the
73
75
  * count of what needs attention in the tone the server gives it, in sync
74
76
  * after a change.
75
77
  */
76
78
  export function useSecurityOverview() {
77
- const overview = useDmsState<SecurityOverview | null>(
78
- "dms-security-overview",
79
- () => null,
79
+ const summary = useApiData<SecurityOverview>(SECURITY_ENDPOINT);
80
+ const overview = computed(() => summary.data.value ?? null);
81
+ // A retry in progress is not a failure to show.
82
+ const isUnavailable = computed(
83
+ () => !!summary.error.value && summary.status.value !== "pending",
80
84
  );
81
- const isUnavailable = useDmsState<boolean>(
82
- "dms-security-overview-unavailable",
83
- () => false,
84
- );
85
- const { $authFetch } = useAuthFetch();
86
85
  const { setNavBadge } = useNavBadges();
87
86
 
88
87
  const attention = computed<SecurityAttention[]>(
@@ -105,14 +104,10 @@ export function useSecurityOverview() {
105
104
  return ATTENTION_TONE_ORDER.find((tone) => present.includes(tone));
106
105
  }
107
106
 
107
+ /** Reads the summary again after a change: every block follows. */
108
108
  async function refresh(): Promise<void> {
109
- try {
110
- overview.value = await $authFetch<SecurityOverview>(SECURITY_ENDPOINT);
111
- isUnavailable.value = false;
112
- } catch {
113
- isUnavailable.value = true;
114
- return;
115
- }
109
+ await summary.refresh({ dedupe: "defer" });
110
+ if (summary.error.value) return;
116
111
  const count = attention.value.length;
117
112
  setNavBadge(
118
113
  SECURITY_PAGE_ID,