@antelopejs/dms 0.8.3 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (424) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/data/module-store/catalog.json +71 -0
  3. package/dist/config.d.ts +7 -0
  4. package/dist/config.js +8 -0
  5. package/dist/config.js.map +1 -1
  6. package/dist/implementations/dms/frontend-backend-proxies.d.ts +3 -0
  7. package/dist/implementations/dms/frontend-backend-proxies.js +39 -0
  8. package/dist/implementations/dms/frontend-backend-proxies.js.map +1 -0
  9. package/dist/implementations/dms/frontend-bootstrap.d.ts +1 -0
  10. package/dist/implementations/dms/frontend-bootstrap.js +13 -0
  11. package/dist/implementations/dms/frontend-bootstrap.js.map +1 -1
  12. package/dist/implementations/dms/frontend-modules.d.ts +6 -2
  13. package/dist/implementations/dms/frontend-modules.js +37 -8
  14. package/dist/implementations/dms/frontend-modules.js.map +1 -1
  15. package/dist/implementations/dms/frontend-ready.d.ts +8 -0
  16. package/dist/implementations/dms/frontend-ready.js +34 -0
  17. package/dist/implementations/dms/frontend-ready.js.map +1 -0
  18. package/dist/implementations/dms/manifest.d.ts +7 -0
  19. package/dist/implementations/dms/manifest.js +13 -0
  20. package/dist/implementations/dms/manifest.js.map +1 -1
  21. package/dist/implementations/dms/menu-categories.d.ts +16 -0
  22. package/dist/implementations/dms/menu-categories.js +30 -0
  23. package/dist/implementations/dms/menu-categories.js.map +1 -0
  24. package/dist/implementations/dms/nav-badges.d.ts +0 -8
  25. package/dist/implementations/dms/nav-badges.js +0 -16
  26. package/dist/implementations/dms/nav-badges.js.map +1 -1
  27. package/dist/implementations/dms/page.d.ts +13 -5
  28. package/dist/implementations/dms/page.js +72 -41
  29. package/dist/implementations/dms/page.js.map +1 -1
  30. package/dist/implementations/dms-html-render/index.js +5 -2
  31. package/dist/implementations/dms-html-render/index.js.map +1 -1
  32. package/dist/implementations/dms-notifications/index.d.ts +4 -3
  33. package/dist/implementations/dms-notifications/index.js +14 -6
  34. package/dist/implementations/dms-notifications/index.js.map +1 -1
  35. package/dist/pages/settings/settings-index.js +3 -2
  36. package/dist/pages/settings/settings-index.js.map +1 -1
  37. package/dist/pages/settings/users/invites.js +2 -2
  38. package/dist/pages/settings/users/invites.js.map +1 -1
  39. package/dist/pages/settings/users/member-change-notifications.js +17 -6
  40. package/dist/pages/settings/users/member-change-notifications.js.map +1 -1
  41. package/dist/pages/settings/users/member-invite-form.d.ts +3 -2
  42. package/dist/pages/settings/users/member-invite-form.js +7 -4
  43. package/dist/pages/settings/users/member-invite-form.js.map +1 -1
  44. package/dist/pages/settings/users/member-roles-field.js +2 -1
  45. package/dist/pages/settings/users/member-roles-field.js.map +1 -1
  46. package/dist/pages/settings/users/members.js +4 -1
  47. package/dist/pages/settings/users/members.js.map +1 -1
  48. package/dist/pages/settings/users/notifications.d.ts +6 -0
  49. package/dist/pages/settings/users/notifications.js +23 -15
  50. package/dist/pages/settings/users/notifications.js.map +1 -1
  51. package/dist/pages/settings/users/profile.js +1 -1
  52. package/dist/pages/settings/users/profile.js.map +1 -1
  53. package/dist/pages/settings/users/region-form.d.ts +1 -1
  54. package/dist/pages/settings/users/region-form.js +2 -1
  55. package/dist/pages/settings/users/region-form.js.map +1 -1
  56. package/dist/pages/settings/users/role-editor-store.d.ts +3 -1
  57. package/dist/pages/settings/users/role-editor-store.js +17 -4
  58. package/dist/pages/settings/users/role-editor-store.js.map +1 -1
  59. package/dist/pages/settings/users/roles.js +1 -0
  60. package/dist/pages/settings/users/roles.js.map +1 -1
  61. package/dist/realtime/registry.d.ts +1 -0
  62. package/dist/realtime/registry.js +5 -0
  63. package/dist/realtime/registry.js.map +1 -1
  64. package/dist/routes/index.d.ts +1 -0
  65. package/dist/routes/index.js +1 -0
  66. package/dist/routes/index.js.map +1 -1
  67. package/dist/routes/realtime.d.ts +5 -1
  68. package/dist/routes/realtime.js +16 -9
  69. package/dist/routes/realtime.js.map +1 -1
  70. package/dist/routes/store.d.ts +21 -0
  71. package/dist/routes/store.js +130 -0
  72. package/dist/routes/store.js.map +1 -0
  73. package/dist/store/activity.d.ts +2 -0
  74. package/dist/store/activity.js +12 -0
  75. package/dist/store/activity.js.map +1 -0
  76. package/dist/store/cache.d.ts +8 -0
  77. package/dist/store/cache.js +29 -0
  78. package/dist/store/cache.js.map +1 -0
  79. package/dist/store/catalog.d.ts +4 -0
  80. package/dist/store/catalog.js +69 -0
  81. package/dist/store/catalog.js.map +1 -0
  82. package/dist/store/compatibility.d.ts +3 -0
  83. package/dist/store/compatibility.js +45 -0
  84. package/dist/store/compatibility.js.map +1 -0
  85. package/dist/store/declarations.d.ts +18 -0
  86. package/dist/store/declarations.js +116 -0
  87. package/dist/store/declarations.js.map +1 -0
  88. package/dist/store/frontend-compatibility.d.ts +18 -0
  89. package/dist/store/frontend-compatibility.js +93 -0
  90. package/dist/store/frontend-compatibility.js.map +1 -0
  91. package/dist/store/interfaces.d.ts +6 -0
  92. package/dist/store/interfaces.js +126 -0
  93. package/dist/store/interfaces.js.map +1 -0
  94. package/dist/store/listing.d.ts +2 -0
  95. package/dist/store/listing.js +180 -0
  96. package/dist/store/listing.js.map +1 -0
  97. package/dist/store/operations.d.ts +8 -0
  98. package/dist/store/operations.js +254 -0
  99. package/dist/store/operations.js.map +1 -0
  100. package/dist/store/registry.d.ts +4 -0
  101. package/dist/store/registry.js +61 -0
  102. package/dist/store/registry.js.map +1 -0
  103. package/dist/store/requirements.d.ts +14 -0
  104. package/dist/store/requirements.js +81 -0
  105. package/dist/store/requirements.js.map +1 -0
  106. package/dist/store/runtime.d.ts +11 -0
  107. package/dist/store/runtime.js +78 -0
  108. package/dist/store/runtime.js.map +1 -0
  109. package/dist/store/tarball.d.ts +2 -0
  110. package/dist/store/tarball.js +80 -0
  111. package/dist/store/tarball.js.map +1 -0
  112. package/dist/store/types.d.ts +98 -0
  113. package/dist/store/types.js +3 -0
  114. package/dist/store/types.js.map +1 -0
  115. package/dist/test/integration/account-deletion.test.js +2 -2
  116. package/dist/test/integration/account-deletion.test.js.map +1 -1
  117. package/dist/test/integration/auth/auth-key-rotation.test.js +2 -1
  118. package/dist/test/integration/auth/auth-key-rotation.test.js.map +1 -1
  119. package/dist/test/integration/auth/current-password.test.js +2 -1
  120. package/dist/test/integration/auth/current-password.test.js.map +1 -1
  121. package/dist/test/integration/auth/session-revocation.test.js +2 -1
  122. package/dist/test/integration/auth/session-revocation.test.js.map +1 -1
  123. package/dist/test/integration/auth/sign-in-country.test.js +2 -1
  124. package/dist/test/integration/auth/sign-in-country.test.js.map +1 -1
  125. package/dist/test/integration/auth/two-factor-codes.test.js +2 -1
  126. package/dist/test/integration/auth/two-factor-codes.test.js.map +1 -1
  127. package/dist/test/integration/auth/two-factor-password.test.js +3 -2
  128. package/dist/test/integration/auth/two-factor-password.test.js.map +1 -1
  129. package/dist/test/integration/email-change.test.js +2 -1
  130. package/dist/test/integration/email-change.test.js.map +1 -1
  131. package/dist/test/integration/files/component-files.test.js +2 -2
  132. package/dist/test/integration/files/component-files.test.js.map +1 -1
  133. package/dist/test/integration/member-settings-access.test.js +8 -7
  134. package/dist/test/integration/member-settings-access.test.js.map +1 -1
  135. package/dist/test/integration/menu-access-events.test.d.ts +1 -0
  136. package/dist/test/integration/menu-access-events.test.js +148 -0
  137. package/dist/test/integration/menu-access-events.test.js.map +1 -0
  138. package/dist/test/integration/notification-inbox.test.js +4 -1
  139. package/dist/test/integration/notification-inbox.test.js.map +1 -1
  140. package/dist/test/integration/notification-locked-subjects.test.js +3 -2
  141. package/dist/test/integration/notification-locked-subjects.test.js.map +1 -1
  142. package/dist/test/integration/notification-unread-badge.test.js +51 -5
  143. package/dist/test/integration/notification-unread-badge.test.js.map +1 -1
  144. package/dist/test/integration/page-layout-route-guard.test.d.ts +1 -0
  145. package/dist/test/integration/page-layout-route-guard.test.js +56 -0
  146. package/dist/test/integration/page-layout-route-guard.test.js.map +1 -0
  147. package/dist/test/integration/permission-warnings.test.js +3 -2
  148. package/dist/test/integration/permission-warnings.test.js.map +1 -1
  149. package/dist/test/integration/removed-member-notifications.test.js +3 -2
  150. package/dist/test/integration/removed-member-notifications.test.js.map +1 -1
  151. package/dist/test/integration/role-delete-reassign.test.js +2 -1
  152. package/dist/test/integration/role-delete-reassign.test.js.map +1 -1
  153. package/dist/test/integration/role-permission-ancestors.test.js +2 -1
  154. package/dist/test/integration/role-permission-ancestors.test.js.map +1 -1
  155. package/dist/test/unit/frontend-bootstrap.test.js +13 -0
  156. package/dist/test/unit/frontend-bootstrap.test.js.map +1 -1
  157. package/dist/test/unit/implementations/dms/category-slug-registry.test.js +14 -5
  158. package/dist/test/unit/implementations/dms/category-slug-registry.test.js.map +1 -1
  159. package/dist/test/unit/implementations/dms/frontend-backend-proxies.test.d.ts +1 -0
  160. package/dist/test/unit/implementations/dms/frontend-backend-proxies.test.js +66 -0
  161. package/dist/test/unit/implementations/dms/frontend-backend-proxies.test.js.map +1 -0
  162. package/dist/test/unit/implementations/dms/manifest-strip.test.js +26 -0
  163. package/dist/test/unit/implementations/dms/manifest-strip.test.js.map +1 -1
  164. package/dist/test/unit/implementations/dms/nav-badges.test.js +23 -9
  165. package/dist/test/unit/implementations/dms/nav-badges.test.js.map +1 -1
  166. package/dist/test/unit/implementations/dms-html-render/generate-html.test.js +22 -4
  167. package/dist/test/unit/implementations/dms-html-render/generate-html.test.js.map +1 -1
  168. package/dist/test/unit/interfaces/dms/page-layout-query.test.js +12 -0
  169. package/dist/test/unit/interfaces/dms/page-layout-query.test.js.map +1 -1
  170. package/dist/test/unit/interfaces/dms/page-teardown.test.js +1 -1
  171. package/dist/test/unit/interfaces/dms/page-teardown.test.js.map +1 -1
  172. package/dist/test/unit/pages/settings/member-roles-field.test.js +1 -1
  173. package/dist/test/unit/pages/settings/member-roles-field.test.js.map +1 -1
  174. package/dist/test/unit/pages/settings/role-delete-confirm.test.js.map +1 -1
  175. package/dist/test/unit/realtime/menu-topics.test.d.ts +1 -0
  176. package/dist/test/unit/realtime/menu-topics.test.js +26 -0
  177. package/dist/test/unit/realtime/menu-topics.test.js.map +1 -0
  178. package/dist/test/unit/store/store.test.d.ts +1 -0
  179. package/dist/test/unit/store/store.test.js +127 -0
  180. package/dist/test/unit/store/store.test.js.map +1 -0
  181. package/dist/validation/store.schema.d.ts +39 -0
  182. package/dist/validation/store.schema.js +27 -0
  183. package/dist/validation/store.schema.js.map +1 -0
  184. package/frontend-vue/deferred-plugins.ts +3 -4
  185. package/frontend-vue/dms.email.ts +5 -1
  186. package/frontend-vue/dms.frontend.ts +15 -5
  187. package/frontend-vue/dms.server.ts +74 -0
  188. package/frontend-vue/layers/dms-auth/app/composables/useLogout.ts +1 -1
  189. package/frontend-vue/layers/dms-auth/app/composables/usePostLoginRedirect.ts +16 -3
  190. package/frontend-vue/layers/dms-auth/app/custom-pages/auth/2fa.vue +3 -1
  191. package/frontend-vue/layers/dms-auth/app/custom-pages/auth/backup.vue +3 -1
  192. package/frontend-vue/layers/dms-auth/app/custom-pages/auth/forgot-validation.vue +1 -1
  193. package/frontend-vue/layers/dms-auth/app/custom-pages/auth/oauth-complete.vue +26 -3
  194. package/frontend-vue/layers/dms-auth/app/custom-pages/auth/recover.vue +1 -1
  195. package/frontend-vue/layers/dms-auth/app/custom-pages/auth/signup.vue +22 -25
  196. package/frontend-vue/layers/dms-auth/app/custom-pages/auth/validate.vue +1 -1
  197. package/frontend-vue/layers/dms-auth/app/middleware/auth.ts +5 -7
  198. package/frontend-vue/layers/dms-auth/app/plugins/permissions-preload.ts +17 -0
  199. package/frontend-vue/layers/dms-auth/server/accounts.ts +135 -0
  200. package/frontend-vue/layers/dms-auth/server/actions.ts +251 -0
  201. package/frontend-vue/layers/dms-auth/server/oauth.ts +260 -0
  202. package/frontend-vue/layers/dms-auth/server/session.ts +158 -0
  203. package/frontend-vue/layers/dms-core/app/build/utils/asyncDataScope.ts +47 -0
  204. package/frontend-vue/layers/dms-core/app/build/utils/permission-preview.ts +6 -2
  205. package/frontend-vue/layers/dms-core/app/composables/auth/useApiData.ts +148 -0
  206. package/frontend-vue/layers/dms-core/app/composables/auth/useAuthFetch.ts +7 -20
  207. package/frontend-vue/layers/dms-core/app/composables/auth/useCurrentUser.ts +2 -2
  208. package/frontend-vue/layers/dms-core/app/composables/auth/useMultiAccount.ts +18 -5
  209. package/frontend-vue/layers/dms-core/app/composables/auth/usePermissions.ts +35 -6
  210. package/frontend-vue/layers/dms-core/app/composables/auth/useSessionRecovery.ts +1 -1
  211. package/frontend-vue/layers/dms-core/app/composables/period/compareResolvers.ts +20 -14
  212. package/frontend-vue/layers/dms-core/app/composables/period/periodClock.ts +121 -0
  213. package/frontend-vue/layers/dms-core/app/composables/period/periodRenderNow.ts +42 -0
  214. package/frontend-vue/layers/dms-core/app/composables/period/periodTimeZone.ts +31 -0
  215. package/frontend-vue/layers/dms-core/app/composables/period/presetResolvers.ts +48 -19
  216. package/frontend-vue/layers/dms-core/app/composables/period/usePeriod.ts +33 -4
  217. package/frontend-vue/layers/dms-core/app/composables/period/usePeriodScope.ts +78 -25
  218. package/frontend-vue/layers/dms-core/app/composables/realtime/usePageRealtime.ts +3 -0
  219. package/frontend-vue/layers/dms-core/app/composables/realtime/useUserRealtime.ts +36 -6
  220. package/frontend-vue/layers/dms-core/app/composables/user/useUserRegionalPreferences.ts +1 -1
  221. package/frontend-vue/layers/dms-core/app/plugins/async-data-scope.ts +26 -0
  222. package/frontend-vue/layers/dms-core/app/utils/pageRoutes.ts +22 -0
  223. package/frontend-vue/layers/dms-core/app/utils/regional.ts +36 -3
  224. package/frontend-vue/layers/dms-core/app/utils/trailingDeduper.ts +10 -1
  225. package/frontend-vue/layers/dms-core/shared/types/app-config.ts +48 -4
  226. package/frontend-vue/layers/dms-layout/app/app.config.ts +42 -23
  227. package/frontend-vue/layers/dms-layout/app/assets/css/main.css +72 -20
  228. package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardFrame.vue +9 -0
  229. package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardSidebar.vue +0 -3
  230. package/frontend-vue/layers/dms-layout/app/build/components/notification/NotificationPopover.vue +9 -3
  231. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleStoreDependencyPicker.vue +136 -0
  232. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleStoreRequirements.vue +98 -0
  233. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleStoreTile.vue +247 -29
  234. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleTile.vue +19 -2
  235. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/members/MemberRolePicker.vue +13 -12
  236. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/notification/NotificationInboxDisplay.vue +17 -7
  237. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/notification/NotificationPreferencesForm.vue +0 -1
  238. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/profile/ProfilePersonalInfo.vue +36 -33
  239. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/profile/ProfileSecuritySummary.vue +1 -2
  240. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/roles/role-types.ts +5 -4
  241. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/security/SecuritySessions.vue +35 -28
  242. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/security/SecurityStatus.vue +0 -1
  243. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/security/SecurityTwoFactor.vue +1 -1
  244. package/frontend-vue/layers/dms-layout/app/build/composables/dev-reload/useDevReloadHolder.ts +7 -6
  245. package/frontend-vue/layers/dms-layout/app/build/composables/layout/sidePanelState.ts +1 -1
  246. package/frontend-vue/layers/dms-layout/app/build/composables/navigation/useVisitNavBadges.ts +26 -0
  247. package/frontend-vue/layers/dms-layout/app/build/composables/notification/useNotificationCatalog.ts +25 -23
  248. package/frontend-vue/layers/dms-layout/app/build/composables/notification/useNotificationPreferences.ts +44 -23
  249. package/frontend-vue/layers/dms-layout/app/build/composables/security/useSessionHandoff.ts +1 -1
  250. package/frontend-vue/layers/dms-layout/app/build/composables/settings/profile/preferencesSummary.ts +1 -1
  251. package/frontend-vue/layers/dms-layout/app/build/composables/settings/profile/useAccountData.ts +3 -2
  252. package/frontend-vue/layers/dms-layout/app/build/composables/settings/profile/useProfileAvatar.ts +1 -18
  253. package/frontend-vue/layers/dms-layout/app/build/composables/settings/profile/useProfilePreferences.ts +19 -23
  254. package/frontend-vue/layers/dms-layout/app/build/composables/settings/security/useSecurityFormat.ts +6 -3
  255. package/frontend-vue/layers/dms-layout/app/build/composables/settings/security/useSecurityOverview.ts +13 -18
  256. package/frontend-vue/layers/dms-layout/app/build/utils/module-store-catalog.ts +186 -120
  257. package/frontend-vue/layers/dms-layout/app/build/utils/module-store-install.ts +146 -0
  258. package/frontend-vue/layers/dms-layout/app/build/utils/modules-catalog.ts +8 -6
  259. package/frontend-vue/layers/dms-layout/app/build/utils/publicErrorMessage.ts +19 -0
  260. package/frontend-vue/layers/dms-layout/app/components/navigation/CollapsedMenuItem.vue +0 -11
  261. package/frontend-vue/layers/dms-layout/app/components/navigation/NavigationMenu.vue +3 -13
  262. package/frontend-vue/layers/dms-layout/app/composables/general/useSystemState.ts +7 -11
  263. package/frontend-vue/layers/dms-layout/app/composables/notification/useNotifications.ts +103 -22
  264. package/frontend-vue/layers/dms-layout/app/composables/page/useModuleStore.ts +109 -0
  265. package/frontend-vue/layers/dms-layout/app/composables/page/useModulesListing.ts +25 -102
  266. package/frontend-vue/layers/dms-layout/app/composables/page/usePageComponentPreload.ts +0 -10
  267. package/frontend-vue/layers/dms-layout/app/composables/page/useSiteLayout.ts +177 -96
  268. package/frontend-vue/layers/dms-layout/app/composables/profile/useUserLanguage.ts +3 -2
  269. package/frontend-vue/layers/dms-layout/app/custom-pages/modules.vue +341 -62
  270. package/frontend-vue/layers/dms-layout/app/emails/components/EmailLayout.vue +31 -11
  271. package/frontend-vue/layers/dms-layout/app/error.vue +5 -2
  272. package/frontend-vue/layers/dms-layout/app/middleware/module-routing.global.ts +1 -1
  273. package/frontend-vue/layers/dms-layout/app/middleware/page-leave-guard.global.ts +2 -2
  274. package/frontend-vue/layers/dms-layout/app/pages/[...slug].vue +12 -9
  275. package/frontend-vue/layers/dms-layout/app/plugins/dms-menu-sync.client.ts +31 -3
  276. package/frontend-vue/layers/dms-layout/app/plugins/favicon.ts +58 -0
  277. package/frontend-vue/layers/dms-layout/app/plugins/notification-stream.client.ts +15 -7
  278. package/frontend-vue/layers/dms-layout/app/plugins/profile-sync.client.ts +5 -1
  279. package/frontend-vue/layers/dms-layout/app/plugins/regional-preferences.ts +10 -4
  280. package/frontend-vue/layers/dms-layout/app/plugins/time-zone-cookie.client.ts +12 -0
  281. package/frontend-vue/layers/dms-layout/app/types/module-store.ts +134 -0
  282. package/frontend-vue/layers/dms-layout/app/types/page.ts +13 -0
  283. package/frontend-vue/layers/dms-layout/i18n/locales/layout-en-GB.json +107 -9
  284. package/frontend-vue/layers/dms-layout/i18n/locales/layout-fr-FR.json +107 -9
  285. package/frontend-vue/layers/dms-layout/server/html-render.ts +58 -0
  286. package/frontend-vue/layers/dms-layout/server/render-token.ts +95 -0
  287. package/frontend-vue/layers/dms-onboarding/app/composables/onboarding/useOnboarding.ts +25 -21
  288. package/frontend-vue/layers/dms-onboarding/app/middleware/onboarding.global.ts +1 -1
  289. package/frontend-vue/layers/dms-onboarding/app/plugins/onboarding-preload.ts +9 -0
  290. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/registerDefaults.ts +4 -1
  291. package/frontend-vue/layers/dms-ui/app/build/composables/files/useFileMetadata.ts +162 -0
  292. package/frontend-vue/layers/dms-ui/app/build/composables/form/formRecordLoad.ts +23 -4
  293. package/frontend-vue/layers/dms-ui/app/build/composables/live/usePageDataRefresh.ts +101 -0
  294. package/frontend-vue/layers/dms-ui/app/build/composables/navigation/useNavBadges.ts +33 -6
  295. package/frontend-vue/layers/dms-ui/app/build/composables/period/periodDisplay.ts +19 -16
  296. package/frontend-vue/layers/dms-ui/app/build/composables/period/periodQuery.ts +40 -20
  297. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableFooter.ts +6 -8
  298. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableListData.ts +42 -14
  299. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableRows.ts +19 -8
  300. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViews.ts +1 -1
  301. package/frontend-vue/layers/dms-ui/app/components/chart/ChartCard.vue +5 -2
  302. package/frontend-vue/layers/dms-ui/app/components/flow-canvas/FlowCanvas.vue +9 -9
  303. package/frontend-vue/layers/dms-ui/app/components/form/Form.vue +20 -18
  304. package/frontend-vue/layers/dms-ui/app/components/form/components/Cascader.vue +2 -1
  305. package/frontend-vue/layers/dms-ui/app/components/form/components/File.vue +26 -132
  306. package/frontend-vue/layers/dms-ui/app/components/form/components/Image.vue +40 -105
  307. package/frontend-vue/layers/dms-ui/app/components/form/components/InputAddress.vue +2 -0
  308. package/frontend-vue/layers/dms-ui/app/components/form/components/Relation.vue +4 -4
  309. package/frontend-vue/layers/dms-ui/app/components/period/PeriodSelector.vue +9 -13
  310. package/frontend-vue/layers/dms-ui/app/components/table-view/CascaderPath.vue +2 -1
  311. package/frontend-vue/layers/dms-ui/app/components/table-view/FilePreview.vue +12 -24
  312. package/frontend-vue/layers/dms-ui/app/components/table-view/ImagePreview.vue +17 -29
  313. package/frontend-vue/layers/dms-ui/app/components/table-view/KanbanBoard.vue +7 -3
  314. package/frontend-vue/layers/dms-ui/app/components/table-view/TableView.vue +20 -8
  315. package/frontend-vue/layers/dms-ui/app/components/top-list/TopListCard.vue +8 -11
  316. package/frontend-vue/layers/dms-ui/app/composables/chart/useChartFetch.ts +146 -182
  317. package/frontend-vue/layers/dms-ui/app/composables/live/usePageLiveRefresh.ts +8 -2
  318. package/frontend-vue/layers/dms-ui/app/composables/useFileReadUrls.ts +11 -99
  319. package/frontend-vue/layers/dms-ui/app/utils/blockRefresh.ts +32 -3
  320. package/frontend-vue/package.json +3 -3
  321. package/frontend-vue/pnpm-lock.yaml +16 -22
  322. package/frontend-vue/server/private-options.ts +27 -0
  323. package/frontend-vue/tests/async-data-scope.test.ts +121 -0
  324. package/frontend-vue/tests/auth-middleware-error.test.ts +3 -3
  325. package/frontend-vue/tests/banner-block-fetch.test.ts +3 -0
  326. package/frontend-vue/tests/block-fetch-options-forwarding.test.ts +3 -0
  327. package/frontend-vue/tests/block-items-period-realtime.test.ts +3 -0
  328. package/frontend-vue/tests/block-not-found.test.ts +16 -2
  329. package/frontend-vue/tests/block-page-refresh.test.ts +3 -0
  330. package/frontend-vue/tests/block-route-params.test.ts +3 -0
  331. package/frontend-vue/tests/branding-defaults.test.ts +34 -0
  332. package/frontend-vue/tests/builtin-emails.test.ts +23 -2
  333. package/frontend-vue/tests/chart-live-pause.test.ts +4 -1
  334. package/frontend-vue/tests/chart-value-propagation.test.ts +4 -1
  335. package/frontend-vue/tests/composed-text-blocks.test.ts +3 -0
  336. package/frontend-vue/tests/email-layout.test.ts +66 -4
  337. package/frontend-vue/tests/error-page-message.test.ts +25 -2
  338. package/frontend-vue/tests/favicon.test.ts +131 -0
  339. package/frontend-vue/tests/file-fields-ssr.test.ts +145 -0
  340. package/frontend-vue/tests/file-read-urls.test.ts +145 -34
  341. package/frontend-vue/tests/file-unavailable.test.ts +5 -1
  342. package/frontend-vue/tests/fixtures/dataEngine.ts +44 -0
  343. package/frontend-vue/tests/form-load-error.test.ts +55 -1
  344. package/frontend-vue/tests/helpers/api-data-runtime.ts +77 -0
  345. package/frontend-vue/tests/image-hydration-ids.test.ts +21 -16
  346. package/frontend-vue/tests/image-reference-preservation.test.ts +59 -53
  347. package/frontend-vue/tests/input-number-adjust.test.ts +14 -0
  348. package/frontend-vue/tests/key-value-list-copy.test.ts +3 -0
  349. package/frontend-vue/tests/member-role-picker.test.ts +4 -2
  350. package/frontend-vue/tests/menu-sync-reconnect.test.ts +47 -0
  351. package/frontend-vue/tests/metadata-retry.test.ts +5 -1
  352. package/frontend-vue/tests/module-routing.test.ts +2 -5
  353. package/frontend-vue/tests/module-store-catalog.test.ts +270 -51
  354. package/frontend-vue/tests/modules-catalog.test.ts +17 -4
  355. package/frontend-vue/tests/modules-listing-transfer.test.ts +95 -0
  356. package/frontend-vue/tests/multi-account-validate.test.ts +23 -1
  357. package/frontend-vue/tests/nav-badge-tones.test.ts +66 -3
  358. package/frontend-vue/tests/notification-counts-from-writes.test.ts +223 -0
  359. package/frontend-vue/tests/notification-counts-transfer.test.ts +88 -0
  360. package/frontend-vue/tests/notification-inbox-display.test.ts +57 -5
  361. package/frontend-vue/tests/notification-popover-errors.test.ts +21 -1
  362. package/frontend-vue/tests/oauth-complete-handoff.test.ts +209 -0
  363. package/frontend-vue/tests/onboarding-middleware.test.ts +1 -1
  364. package/frontend-vue/tests/onboarding-transfer.test.ts +106 -0
  365. package/frontend-vue/tests/page-data-refresh.test.ts +141 -0
  366. package/frontend-vue/tests/page-fill-height.test.ts +16 -3
  367. package/frontend-vue/tests/page-no-visible-blocks.test.ts +12 -2
  368. package/frontend-vue/tests/page-realtime-remount.test.ts +1 -1
  369. package/frontend-vue/tests/page-record-label.test.ts +0 -7
  370. package/frontend-vue/tests/page-validation-redirect.test.ts +12 -2
  371. package/frontend-vue/tests/period-hydration-now.test.ts +185 -0
  372. package/frontend-vue/tests/period-live-pause.test.ts +3 -0
  373. package/frontend-vue/tests/period-reader-zone.test.ts +137 -0
  374. package/frontend-vue/tests/period-scope-first-fetch.test.ts +17 -1
  375. package/frontend-vue/tests/period-ssr.test.ts +195 -0
  376. package/frontend-vue/tests/permission-preview-partial.test.ts +0 -2
  377. package/frontend-vue/tests/permissions-transfer.test.ts +194 -0
  378. package/frontend-vue/tests/post-login-redirect.test.ts +10 -5
  379. package/frontend-vue/tests/profile-personal-info-ssr.test.ts +150 -0
  380. package/frontend-vue/tests/profile-preferences-summary.test.ts +15 -11
  381. package/frontend-vue/tests/realtime-reconnect.test.ts +22 -3
  382. package/frontend-vue/tests/roles-name-errors.test.ts +1 -1
  383. package/frontend-vue/tests/row-side-panel.test.ts +1 -1
  384. package/frontend-vue/tests/security-sessions-load-error.test.ts +3 -1
  385. package/frontend-vue/tests/server-routes-vs-pages.test.ts +26 -0
  386. package/frontend-vue/tests/server-routes.test.ts +582 -0
  387. package/frontend-vue/tests/session-handoff.test.ts +1 -1
  388. package/frontend-vue/tests/settings-reads-ssr.test.ts +232 -0
  389. package/frontend-vue/tests/side-panels.test.ts +8 -1
  390. package/frontend-vue/tests/sign-in-exit-history.test.ts +219 -0
  391. package/frontend-vue/tests/signup-invitation-link.test.ts +56 -8
  392. package/frontend-vue/tests/site-layout-categories.test.ts +42 -0
  393. package/frontend-vue/tests/site-layout-hydration.test.ts +3 -3
  394. package/frontend-vue/tests/site-layout-rendered.test.ts +103 -0
  395. package/frontend-vue/tests/site-layout-resync.test.ts +87 -0
  396. package/frontend-vue/tests/ssr-guard-preload.test.ts +188 -0
  397. package/frontend-vue/tests/table-cell-tones.test.ts +36 -1
  398. package/frontend-vue/tests/table-data-ssr.test.ts +195 -0
  399. package/frontend-vue/tests/table-list-data-requests.test.ts +154 -0
  400. package/frontend-vue/tests/table-retry-loading.test.ts +37 -0
  401. package/frontend-vue/tests/table-source-navigation.test.ts +0 -1
  402. package/frontend-vue/tests/table-source-period-scope.test.ts +0 -1
  403. package/frontend-vue/tests/table-view-views.test.ts +1 -1
  404. package/frontend-vue/tests/top-list-card-actions.test.ts +3 -0
  405. package/frontend-vue/tests/top-list-card-period-badge-ssr.test.ts +212 -0
  406. package/frontend-vue/tests/top-list-card-period-badge.test.ts +79 -0
  407. package/frontend-vue/tests/top-list-card-value-suffix.test.ts +3 -0
  408. package/frontend-vue/tests/trailing-deduper.test.ts +22 -0
  409. package/frontend-vue/tests/use-api-data-update.test.ts +119 -0
  410. package/frontend-vue/tests/use-api-data.test.ts +410 -0
  411. package/frontend-vue/tests/user-language.test.ts +8 -5
  412. package/frontend-vue/tests/widget-label-i18n.test.ts +4 -1
  413. package/frontend-vue/vitest.config.ts +6 -0
  414. package/package.json +2 -2
  415. package/skills/dms/SKILL.md +2 -1
  416. package/skills/dms-module/SKILL.md +1 -1
  417. package/skills/dms-pages/SKILL.md +4 -1
  418. package/frontend-vue/layers/dms-core/app/build/composables/page/usePageRecordLabel.ts +0 -9
  419. package/frontend-vue/layers/dms-layout/app/composables/page/usePrefetch.ts +0 -21
  420. package/frontend-vue/layers/dms-ui/app/build/composables/form/useControlError.ts +0 -10
  421. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useServerRenderedAsyncData.ts +0 -23
  422. package/frontend-vue/tests/control-error-path.test.ts +0 -11
  423. package/frontend-vue/tests/modules-listing-per-request.test.ts +0 -104
  424. package/frontend-vue/tests/page-layout-background-refresh.test.ts +0 -321
@@ -0,0 +1,47 @@
1
+ /** The part of the session the data scope follows. */
2
+ export interface ScopedSession {
3
+ accountId?: string;
4
+ activeTenantId?: string;
5
+ }
6
+
7
+ const HASH_SEED_LOW = 0xdeadbeef;
8
+ const HASH_SEED_HIGH = 0x41c6ce57;
9
+ const HASH_MULTIPLIER_LOW = 2_654_435_761;
10
+ const HASH_MULTIPLIER_HIGH = 1_597_334_677;
11
+ const HASH_MIX_LOW = 2_246_822_507;
12
+ const HASH_MIX_HIGH = 3_266_489_909;
13
+ const HIGH_WORD = 2 ** 32;
14
+ const HIGH_BITS_MASK = 2_097_151;
15
+ const RADIX = 36;
16
+
17
+ /**
18
+ * A 53-bit digest of `text` (cyrb53): the same in a server render and in the
19
+ * browser, and synchronous, so the scope is set before the page renders.
20
+ * Not a secret: it only keeps the identifiers out of the page.
21
+ */
22
+ function digest(text: string): string {
23
+ let low = HASH_SEED_LOW;
24
+ let high = HASH_SEED_HIGH;
25
+ for (let index = 0; index < text.length; index++) {
26
+ const code = text.charCodeAt(index);
27
+ low = Math.imul(low ^ code, HASH_MULTIPLIER_LOW);
28
+ high = Math.imul(high ^ code, HASH_MULTIPLIER_HIGH);
29
+ }
30
+ low = Math.imul(low ^ (low >>> 16), HASH_MIX_LOW);
31
+ low ^= Math.imul(high ^ (high >>> 13), HASH_MIX_HIGH);
32
+ high = Math.imul(high ^ (high >>> 16), HASH_MIX_LOW);
33
+ high ^= Math.imul(low ^ (low >>> 13), HASH_MIX_HIGH);
34
+ return (HIGH_WORD * (HIGH_BITS_MASK & high) + (low >>> 0)).toString(RADIX);
35
+ }
36
+
37
+ /**
38
+ * The scope the async data of a session is kept under: a digest of its
39
+ * account and workspace, or none when signed out. The same key read by
40
+ * another account, or in another workspace, is another entry.
41
+ */
42
+ export function asyncDataScopeOf(
43
+ session: ScopedSession | null | undefined,
44
+ ): string | undefined {
45
+ if (!session?.accountId) return undefined;
46
+ return digest(`${session.accountId}\n${session.activeTenantId ?? ""}`);
47
+ }
@@ -1,3 +1,4 @@
1
+ import { pageRoutePath } from "#dms-core/app/utils/pageRoutes";
1
2
  // "Preview as role" (roles editor): the shapes the preview endpoint answers
2
3
  // with, how a preview session travels from the editor tab to the preview tab,
3
4
  // and which requests a preview tab may still send. Pure: no Vue, no window.
@@ -9,7 +10,7 @@ export type PermissionPreviewBlock =
9
10
 
10
11
  export type PermissionPreviewState = PermissionPreviewBlock["state"];
11
12
 
12
- /** Answer of `POST /settings/workspace/roles/preview`. */
13
+ /** Answer of `POST /dms/pages/settings/workspace/roles/preview`. */
13
14
  export interface PermissionPreviewResult {
14
15
  page: { fullId: string; displayName: string; hidden: boolean } | null;
15
16
  /** Blocks keyed by layout path (component key, then child ids). */
@@ -44,7 +45,10 @@ export interface PermissionPreviewSession {
44
45
  }
45
46
 
46
47
  /** Endpoint the preview tab asks; a POST, so the request guard lets it through. */
47
- export const PERMISSION_PREVIEW_ENDPOINT = "/settings/workspace/roles/preview";
48
+ export const PERMISSION_PREVIEW_ENDPOINT = pageRoutePath(
49
+ "/settings/workspace/roles",
50
+ "/preview",
51
+ );
48
52
  /** Query parameter that hands a preview session to a new tab. */
49
53
  export const PERMISSION_PREVIEW_QUERY_KEY = "dms-preview";
50
54
  /** `localStorage` key prefix of the sessions the editor shares with its preview tabs. */
@@ -0,0 +1,148 @@
1
+ import type {
2
+ DmsAsyncDataKey,
3
+ DmsAsyncDataResult,
4
+ DmsPagedDataResult,
5
+ UseAsyncDataOptions,
6
+ UseDmsPagedDataOptions,
7
+ UseFetchOptions,
8
+ } from "#dms/frontend-module";
9
+ import type { MaybeRefOrGetter } from "vue";
10
+
11
+ /** The authenticated fetch `useAuthFetch` returns. */
12
+ type AuthFetch = ReturnType<typeof useAuthFetch>["$authFetch"];
13
+
14
+ /** What a handler loading API data receives. */
15
+ export interface ApiDataContext {
16
+ /** `useAuthFetch`'s fetch: the session retry, the language and realtime headers. */
17
+ $authFetch: AuthFetch;
18
+ /** Aborted once the result is no longer wanted: pass it to each request. */
19
+ signal: AbortSignal;
20
+ }
21
+
22
+ export type ApiDataHandler<T> = (context: ApiDataContext) => Promise<T>;
23
+
24
+ /** `useDmsFetch`'s options, the fetch function aside: it is `$authFetch`. */
25
+ export type UseApiDataOptions<T> = Omit<UseFetchOptions<T>, "$fetch">;
26
+
27
+ /** A backend URL to read, or nothing to read while it is null or undefined. */
28
+ export type ApiDataRequest = MaybeRefOrGetter<string | null | undefined>;
29
+
30
+ /**
31
+ * What `useApiData` returns: `data`, `status`, `pending`, `error`, the `key`
32
+ * the data is loaded under, `placeholder` (a reactive request still showing
33
+ * the previous answer), `execute()`, `refresh()` and `clear()`. Usable at
34
+ * once, or awaited until the data is there.
35
+ */
36
+ export type ApiDataResult<T> = DmsAsyncDataResult<T>;
37
+
38
+ /**
39
+ * Reads backend data for a component: the one way a module loads what it
40
+ * renders. A server render waits for it and hands it to the browser with the
41
+ * page, which then does not ask again; the callers of one request share it.
42
+ * Data read before shows at once and is read again in the background on each
43
+ * mount (stale while revalidate).
44
+ *
45
+ * - `useApiData(url, options)` reads one URL, like Nuxt's `useFetch`. The URL,
46
+ * `method`, `query`, `params` and `body` may be refs or getters, read again
47
+ * for each request: the data follows them. A URL that is null or undefined
48
+ * reads nothing until it has a value.
49
+ * - `useApiData(key, handler, options)` loads what the handler returns, like
50
+ * Nuxt's `useAsyncData`: for data made of several requests. The handler
51
+ * receives `$authFetch` and the `signal` to pass to each request.
52
+ *
53
+ * Requests go through `$authFetch` (session retry, language and realtime
54
+ * headers), and the data is kept per account and workspace: a switch of
55
+ * either reads it again. The options are the renderer's:
56
+ *
57
+ * - `lazy` spares a client navigation the wait; `server: false` leaves the
58
+ * data to the browser (for what only the browser knows).
59
+ * - `immediate: false` waits for `execute()`: a dialog or a drawer reads
60
+ * what it shows once it opens.
61
+ * - `refreshInterval: ms` reads again on an interval while the caller shows
62
+ * the data and the page is visible: polling. A ref or a getter changes it,
63
+ * and `0` or `null` stops it; a call from a plugin polls for as long as
64
+ * the application runs.
65
+ * - The page's refreshes (`refreshPageBlocks()`) and the events of its
66
+ * realtime topics read the data again; `refreshOnPageRefresh: false` opts
67
+ * out, for data with refresh triggers of its own.
68
+ * - `data` is a shallow ref: replace the value to change it, or pass
69
+ * `deep: true`.
70
+ *
71
+ * A write stays an `$authFetch` call; once it is done, the page's refresh
72
+ * reads what changed, or `useDmsData(key)` puts its answer in the data in
73
+ * place (the URL form names its key with `key`).
74
+ */
75
+ export function useApiData<T>(
76
+ url: ApiDataRequest,
77
+ options?: UseApiDataOptions<T>,
78
+ ): ApiDataResult<T>;
79
+ export function useApiData<T>(
80
+ key: MaybeRefOrGetter<DmsAsyncDataKey>,
81
+ handler: ApiDataHandler<T>,
82
+ options?: UseAsyncDataOptions<T>,
83
+ ): ApiDataResult<T>;
84
+ export function useApiData<T>(
85
+ source: ApiDataRequest,
86
+ handlerOrOptions?: ApiDataHandler<T> | UseApiDataOptions<T>,
87
+ options?: UseAsyncDataOptions<T>,
88
+ ): ApiDataResult<T> {
89
+ const { $authFetch } = useAuthFetch();
90
+ if (typeof handlerOrOptions === "function")
91
+ return useDmsAsyncData<T>(
92
+ source,
93
+ ({ signal }) => handlerOrOptions({ $authFetch, signal }),
94
+ options,
95
+ );
96
+ return useDmsFetch<T>(source, {
97
+ ...handlerOrOptions,
98
+ $fetch: $authFetch as UseFetchOptions<T>["$fetch"],
99
+ });
100
+ }
101
+
102
+ /** What a handler reading one page of API data receives. */
103
+ export interface ApiPagedDataContext<TCursor> extends ApiDataContext {
104
+ /** The page to read: `initialCursor` (none by default) for the first one. */
105
+ cursor: TCursor | undefined;
106
+ /** Its position, from `0`. */
107
+ index: number;
108
+ }
109
+
110
+ export type ApiPagedDataHandler<TPage, TCursor> = (
111
+ context: ApiPagedDataContext<TCursor>,
112
+ ) => Promise<TPage>;
113
+
114
+ /**
115
+ * What `useApiPagedData` returns: `useApiData`'s result over the pages
116
+ * loaded, with `pages`, `hasMore`, `loadingMore` and `loadMore()`.
117
+ */
118
+ export type ApiPagedDataResult<TPage, TCursor> = DmsPagedDataResult<
119
+ TPage,
120
+ TCursor
121
+ >;
122
+
123
+ /**
124
+ * Reads a backend list page by page under one key: a "load more" button, an
125
+ * infinite list. The handler reads the page at `cursor` through `$authFetch`;
126
+ * `nextCursor(page, pages)` gives the next page's cursor, or `null` at the
127
+ * end. A server render shows the first page, `loadMore()` appends the next
128
+ * one, and a revalidation (a mount, the page's refreshes, the polling)
129
+ * reads every page loaded so far again. Put what filters the list in the
130
+ * key (a ref or a getter for it): a new key starts from its first page.
131
+ *
132
+ * @param key The key, or a ref or getter for it
133
+ * @param handler Reads one page
134
+ * @param options `nextCursor`, `initialCursor` and `useApiData`'s options
135
+ */
136
+ export function useApiPagedData<TPage, TCursor = unknown>(
137
+ key: MaybeRefOrGetter<DmsAsyncDataKey>,
138
+ handler: ApiPagedDataHandler<TPage, TCursor>,
139
+ options: UseDmsPagedDataOptions<TPage, TCursor>,
140
+ ): ApiPagedDataResult<TPage, TCursor> {
141
+ const { $authFetch } = useAuthFetch();
142
+ return useDmsPagedData<TPage, TCursor>(
143
+ key,
144
+ ({ cursor, index, signal }) =>
145
+ handler({ $authFetch, cursor, index, signal }),
146
+ options,
147
+ );
148
+ }
@@ -1,5 +1,3 @@
1
- import type { UseFetchOptions } from "#dms/frontend-module";
2
-
3
1
  export const CONTENT_LANGUAGE_HEADER = "x-content-language";
4
2
 
5
3
  const RETRY_HEADER = "x-auth-retry";
@@ -38,7 +36,7 @@ function isUnauthorized(error: unknown): boolean {
38
36
  }
39
37
 
40
38
  export const useAuthFetch = () => {
41
- const { loggedIn: isLoggedIn } = useUserSession();
39
+ const { loggedIn: isLoggedIn } = useDmsUserSession();
42
40
  const { refreshSession, reconcileSession, redirectToAuth } =
43
41
  useSessionRecovery();
44
42
  const config = useDmsRuntimeConfig();
@@ -51,10 +49,12 @@ export const useAuthFetch = () => {
51
49
 
52
50
  const baseFetch = $fetch.create({
53
51
  baseURL: config.public.dms.baseURL,
54
- headers: {
55
- [CONTENT_LANGUAGE_HEADER]: dmsApp.$i18n.locale.value,
56
- },
57
52
  onRequest({ options }) {
53
+ // Read as each request leaves: a `$authFetch` kept across a language
54
+ // change asks in the new language. A header the caller sets wins.
55
+ if (!options.headers.has(CONTENT_LANGUAGE_HEADER)) {
56
+ options.headers.set(CONTENT_LANGUAGE_HEADER, dmsApp.$i18n.locale.value);
57
+ }
58
58
  if (realtimeSessionId.value) {
59
59
  options.headers.set(REALTIME_SESSION_HEADER, realtimeSessionId.value);
60
60
  }
@@ -117,18 +117,5 @@ export const useAuthFetch = () => {
117
117
  baseFetch.raw(request, options),
118
118
  ) as AuthFetch["raw"];
119
119
 
120
- function useFetchAuth<T>(
121
- url: string | (() => string),
122
- options?: UseFetchOptions<T>,
123
- ) {
124
- return useDmsFetch(url, {
125
- ...options,
126
- $fetch: $authFetch,
127
- });
128
- }
129
-
130
- return {
131
- $authFetch,
132
- useFetchAuth,
133
- };
120
+ return { $authFetch };
134
121
  };
@@ -1,14 +1,14 @@
1
1
  /**
2
2
  * Global reactive access to the signed-in user's identity (name, email, ...).
3
3
  *
4
- * `user` is the shared session state from `useUserSession`, so every consumer
4
+ * `user` is the shared session state from `useDmsUserSession`, so every consumer
5
5
  * (header menu, switch-account page, ...) re-renders as soon as it changes.
6
6
  * `refresh` re-fetches the session through the frontend server session hook
7
7
  * pulls a fresh `/api/auth/me`) and syncs the stored multi-account entry, so a
8
8
  * profile edit propagates immediately without re-logging in.
9
9
  */
10
10
  export function useCurrentUser() {
11
- const { user, fetch: refreshSession } = useUserSession<User>();
11
+ const { user, fetch: refreshSession } = useDmsUserSession<User>();
12
12
  const { syncActiveAccount } = useMultiAccount();
13
13
 
14
14
  async function refresh(): Promise<void> {
@@ -116,13 +116,23 @@ async function requestAccountSwitch(account: StoredAccount) {
116
116
  account.isExpired = false;
117
117
  }
118
118
 
119
- async function refreshSessionSurfaces(refreshUser: () => Promise<unknown>) {
119
+ /**
120
+ * Moves what the browser shows to the session it switched to. The session
121
+ * read again moves the data scope (the previous account's data is dropped)
122
+ * and drops the prefetched pages; the site layout and the page come back
123
+ * through Inertia, whose once prop is keyed on the account and workspace.
124
+ *
125
+ * @internal
126
+ */
127
+ export async function refreshSessionSurfaces(
128
+ refreshUser: () => Promise<unknown>,
129
+ ): Promise<void> {
120
130
  await refreshUser();
121
131
 
122
- const { refresh: refreshSiteLayout } = useSiteLayout();
132
+ const { resync: reloadSiteLayout } = useSiteLayout();
123
133
  const { clear: clearPermissions } = usePermissions();
124
134
  clearPermissions();
125
- await refreshSiteLayout();
135
+ await reloadSiteLayout();
126
136
  }
127
137
 
128
138
  export async function validateAccount(account: StoredAccount) {
@@ -187,8 +197,11 @@ function createValidateAllAccounts(
187
197
 
188
198
  export function useMultiAccount() {
189
199
  const accounts = useAccountsStorage();
190
- const { user, session, fetch: refreshUser } =
191
- useUserSession<User, BrowserSession>();
200
+ const {
201
+ user,
202
+ session,
203
+ fetch: refreshUser,
204
+ } = useDmsUserSession<User, BrowserSession>();
192
205
 
193
206
  const getActiveAccount = (): StoredAccount | undefined =>
194
207
  buildActiveAccount(
@@ -9,11 +9,29 @@ interface PermissionsState {
9
9
  fetchGeneration: Ref<number>;
10
10
  }
11
11
 
12
+ // The set a server render loads (the `auth` middleware preloads it before the
13
+ // page renders) is the one the page renders with: the browser starts from it
14
+ // rather than asking again, and its age keeps counting from the server's load.
15
+ // The generation only guards the runs in flight of one side.
16
+ const TRANSFERRED = { transfer: true };
17
+
12
18
  function usePermissionsState(): PermissionsState {
13
19
  return {
14
- permissions: useDmsState<Set<string>>("dms-user-permissions", () => new Set()),
15
- isLoaded: useDmsState<boolean>("dms-permissions-loaded", () => false),
16
- lastFetchTime: useDmsState<number>("dms-permissions-lastFetch", () => 0),
20
+ permissions: useDmsState<Set<string>>(
21
+ "dms-user-permissions",
22
+ () => new Set(),
23
+ TRANSFERRED,
24
+ ),
25
+ isLoaded: useDmsState<boolean>(
26
+ "dms-permissions-loaded",
27
+ () => false,
28
+ TRANSFERRED,
29
+ ),
30
+ lastFetchTime: useDmsState<number>(
31
+ "dms-permissions-lastFetch",
32
+ () => 0,
33
+ TRANSFERRED,
34
+ ),
17
35
  fetchGeneration: useDmsState<number>("dms-permissions-gen", () => 0),
18
36
  };
19
37
  }
@@ -30,15 +48,25 @@ function createFetchPermissions(state: PermissionsState) {
30
48
  };
31
49
 
32
50
  // A refetch asked while one is in flight gets its own pass instead of the
33
- // older response, so an invalidation is never swallowed. Scoped to the DMS app
51
+ // older response, so an invalidation is never swallowed. Scoped to the DMS
34
52
  // app, so concurrent server-rendered requests never share a pass.
35
53
  const deduper = useTrailingDeduper("permissions");
36
- return (): Promise<void> => deduper.run(runFetch);
54
+ const fetchPermissions = (): Promise<void> => deduper.run(runFetch);
55
+ // A caller that only needs a set has nothing to invalidate: it joins the
56
+ // load in flight instead of queueing a second one.
57
+ const loadPermissions = async (): Promise<void> => {
58
+ if (state.isLoaded.value) return;
59
+ await deduper.join(runFetch);
60
+ // The run it joined was started before a `clear()` (another account):
61
+ // its answer was dropped, and this caller still needs one.
62
+ if (!state.isLoaded.value) await deduper.join(runFetch);
63
+ };
64
+ return { fetchPermissions, loadPermissions };
37
65
  }
38
66
 
39
67
  export function usePermissions() {
40
68
  const state = usePermissionsState();
41
- const fetchPermissions = createFetchPermissions(state);
69
+ const { fetchPermissions, loadPermissions } = createFetchPermissions(state);
42
70
 
43
71
  async function refreshIfStale() {
44
72
  const elapsed = Date.now() - state.lastFetchTime.value;
@@ -66,6 +94,7 @@ export function usePermissions() {
66
94
  permissions: state.permissions,
67
95
  isLoaded: state.isLoaded,
68
96
  fetchPermissions,
97
+ loadPermissions,
69
98
  refreshIfStale,
70
99
  hasPermission,
71
100
  clear,
@@ -5,7 +5,7 @@ const IN_FLIGHT_REFRESH_KEY = "__dmsAuthInFlightRefresh";
5
5
 
6
6
  export const useSessionRecovery = () => {
7
7
  const dmsApp = useDmsApp();
8
- const { loggedIn, fetch: fetchSession } = useUserSession();
8
+ const { loggedIn, fetch: fetchSession } = useDmsUserSession();
9
9
  // useDmsRoute() must not be called here: this composable runs inside route
10
10
  // middleware (via useAuthFetch), where the DMS runtime warns that useDmsRoute is
11
11
  // misleading. Resolve the route lazily from the router instead.
@@ -1,16 +1,25 @@
1
+ import { readerClock } from "./periodClock";
1
2
  import type { PeriodComparison, PeriodRange } from "./types";
2
3
 
3
4
  const NON_OVERLAP_GAP_MS = 1;
4
5
 
5
6
  const MS_PER_DAY = 24 * 60 * 60 * 1000;
6
7
 
8
+ // In the reader's calendar (`readerClock`), as the range was resolved.
7
9
  function isLocalMidnight(date: Date): boolean {
8
- return (
9
- date.getHours() === 0 &&
10
- date.getMinutes() === 0 &&
11
- date.getSeconds() === 0 &&
12
- date.getMilliseconds() === 0
13
- );
10
+ const { hours, minutes, seconds, milliseconds } = readerClock().wall(date);
11
+ return hours === 0 && minutes === 0 && seconds === 0 && milliseconds === 0;
12
+ }
13
+
14
+ /** `date` moved by whole calendar days and years, its time of day kept. */
15
+ function shiftCalendar(date: Date, days: number, years: number): Date {
16
+ const clock = readerClock();
17
+ const wall = clock.wall(date);
18
+ return clock.instant({
19
+ ...wall,
20
+ year: wall.year + years,
21
+ day: wall.day + days,
22
+ });
14
23
  }
15
24
 
16
25
  /** A range of whole local days: midnight to the last millisecond of a day. */
@@ -27,10 +36,8 @@ function shiftPreviousPeriod(range: PeriodRange): PeriodRange {
27
36
  if (isWholeDayRange(range)) {
28
37
  const end = range.to.getTime() + NON_OVERLAP_GAP_MS;
29
38
  const days = Math.round((end - range.from.getTime()) / MS_PER_DAY);
30
- const from = new Date(range.from);
31
- from.setDate(from.getDate() - days);
32
39
  return {
33
- from,
40
+ from: shiftCalendar(range.from, -days, 0),
34
41
  to: new Date(range.from.getTime() - NON_OVERLAP_GAP_MS),
35
42
  };
36
43
  }
@@ -44,11 +51,10 @@ function shiftPreviousPeriod(range: PeriodRange): PeriodRange {
44
51
  }
45
52
 
46
53
  function shiftPreviousYear(range: PeriodRange): PeriodRange {
47
- const previousFrom = new Date(range.from);
48
- previousFrom.setFullYear(previousFrom.getFullYear() - 1);
49
- const previousTo = new Date(range.to);
50
- previousTo.setFullYear(previousTo.getFullYear() - 1);
51
- return { from: previousFrom, to: previousTo };
54
+ return {
55
+ from: shiftCalendar(range.from, 0, -1),
56
+ to: shiftCalendar(range.to, 0, -1),
57
+ };
52
58
  }
53
59
 
54
60
  const COMPARISON_RESOLVERS: Record<
@@ -0,0 +1,121 @@
1
+ import { readerTimeZone } from "../../utils/regional";
2
+
3
+ /** The fields of a wall-clock time, months counted from 0 as `Date` does. */
4
+ export interface WallTime {
5
+ year: number;
6
+ month: number;
7
+ day: number;
8
+ hours: number;
9
+ minutes: number;
10
+ seconds: number;
11
+ milliseconds: number;
12
+ }
13
+
14
+ /**
15
+ * A calendar in one time zone: the wall-clock time of an instant, and the
16
+ * instant of a wall-clock time. Fields out of range roll over as `Date`'s do
17
+ * (day 0 is the last day of the month before).
18
+ */
19
+ export interface PeriodClock {
20
+ wall(date: Date): WallTime;
21
+ instant(wall: WallTime): Date;
22
+ }
23
+
24
+ /** The calendar of the zone the code runs in, through `Date`'s own methods. */
25
+ export const localClock: PeriodClock = {
26
+ wall: (date) => ({
27
+ year: date.getFullYear(),
28
+ month: date.getMonth(),
29
+ day: date.getDate(),
30
+ hours: date.getHours(),
31
+ minutes: date.getMinutes(),
32
+ seconds: date.getSeconds(),
33
+ milliseconds: date.getMilliseconds(),
34
+ }),
35
+ instant: (wall) =>
36
+ new Date(
37
+ wall.year,
38
+ wall.month,
39
+ wall.day,
40
+ wall.hours,
41
+ wall.minutes,
42
+ wall.seconds,
43
+ wall.milliseconds,
44
+ ),
45
+ };
46
+
47
+ const WALL_PARTS = ["year", "month", "day", "hour", "minute", "second"];
48
+ const zonedFormatters = new Map<string, Intl.DateTimeFormat>();
49
+
50
+ function zonedFormatter(timeZone: string): Intl.DateTimeFormat {
51
+ let formatter = zonedFormatters.get(timeZone);
52
+ if (!formatter) {
53
+ formatter = new Intl.DateTimeFormat("en-US", {
54
+ timeZone,
55
+ hourCycle: "h23",
56
+ year: "numeric",
57
+ month: "numeric",
58
+ day: "numeric",
59
+ hour: "numeric",
60
+ minute: "numeric",
61
+ second: "numeric",
62
+ });
63
+ zonedFormatters.set(timeZone, formatter);
64
+ }
65
+ return formatter;
66
+ }
67
+
68
+ function utcOf(wall: WallTime): number {
69
+ return Date.UTC(
70
+ wall.year,
71
+ wall.month,
72
+ wall.day,
73
+ wall.hours,
74
+ wall.minutes,
75
+ wall.seconds,
76
+ wall.milliseconds,
77
+ );
78
+ }
79
+
80
+ /** The calendar of `timeZone`, through `Intl` whatever zone the code runs in. */
81
+ export function zonedClock(timeZone: string): PeriodClock {
82
+ const wall = (date: Date): WallTime => {
83
+ const parts = Object.fromEntries(
84
+ zonedFormatter(timeZone)
85
+ .formatToParts(date)
86
+ .filter((part) => WALL_PARTS.includes(part.type))
87
+ .map((part) => [part.type, Number(part.value)]),
88
+ );
89
+ return {
90
+ year: parts.year!,
91
+ month: parts.month! - 1,
92
+ day: parts.day!,
93
+ hours: parts.hour!,
94
+ minutes: parts.minute!,
95
+ seconds: parts.second!,
96
+ milliseconds: date.getUTCMilliseconds(),
97
+ };
98
+ };
99
+ // The zone's offset at an instant: how far its wall clock is from UTC.
100
+ const offsetAt = (time: number) => utcOf(wall(new Date(time))) - time;
101
+ return {
102
+ wall,
103
+ instant: (target) => {
104
+ const asUtc = utcOf(target);
105
+ // Twice: the offset at the first guess is the right one unless the
106
+ // guess and the answer fall on either side of a daylight saving change.
107
+ const guess = asUtc - offsetAt(asUtc);
108
+ return new Date(asUtc - offsetAt(guess));
109
+ },
110
+ };
111
+ }
112
+
113
+ /**
114
+ * The calendar a period is resolved in: the reader's browser's zone. That is
115
+ * the code's own in the browser; on a server, the zone the browser's cookie
116
+ * names (see `readerTimeZone`), or the server's own without one.
117
+ */
118
+ export function readerClock(): PeriodClock {
119
+ const zone = readerTimeZone();
120
+ return zone ? zonedClock(zone) : localClock;
121
+ }
@@ -0,0 +1,42 @@
1
+ import { getCurrentInstance } from "vue";
2
+
3
+ /**
4
+ * The instant a server render resolves its periods from, handed to the
5
+ * browser with the page.
6
+ */
7
+ const RENDER_NOW_STATE_KEY = "dms-period-render-now";
8
+
9
+ /**
10
+ * Whether the component setting up is hydrating the server's HTML: Vue binds
11
+ * a hydrating component's vnode to its server-rendered node before its setup
12
+ * runs, where a fresh mount has no node yet.
13
+ */
14
+ function isHydratingSetup(): boolean {
15
+ return !!getCurrentInstance()?.vnode.el;
16
+ }
17
+
18
+ /**
19
+ * The instant a period set up now resolves from, or `null` for the clock's.
20
+ *
21
+ * A server render resolves every period of the request from one instant,
22
+ * which it transfers; the browser hydrating that render resolves from the
23
+ * same one, so a relative preset (`last-24h`) or a calendar one read across
24
+ * midnight keeps the bounds, key and labels the server rendered, and nothing
25
+ * reads again on hydration. Anywhere else (a client navigation, a call
26
+ * outside a component) it is `null`: the period follows the clock.
27
+ */
28
+ export function periodRenderNow(): Date | null {
29
+ if (!getCurrentInstance()) return null;
30
+ if (typeof window === "undefined") {
31
+ const now = useDmsState<number>(RENDER_NOW_STATE_KEY, () => Date.now(), {
32
+ transfer: true,
33
+ });
34
+ return new Date(now.value);
35
+ }
36
+ if (!isHydratingSetup()) return null;
37
+ const transferred = useDmsState<number | null>(
38
+ RENDER_NOW_STATE_KEY,
39
+ () => null,
40
+ ).value;
41
+ return typeof transferred === "number" ? new Date(transferred) : null;
42
+ }
@@ -0,0 +1,31 @@
1
+ import type { Ref } from "vue";
2
+ import { isKnownTimeZone } from "../../utils/regional";
3
+
4
+ /**
5
+ * The browser's time zone, kept in a cookie for a server render: a period is
6
+ * made of calendar days in the browser's zone, which the server has no other
7
+ * way to know.
8
+ */
9
+ export const TIME_ZONE_COOKIE = "dms-time-zone";
10
+ const TIME_ZONE_COOKIE_MAX_AGE = 60 * 60 * 24 * 365;
11
+
12
+ /** The browser's time zone as its cookie holds it; `null` before it is written. */
13
+ export function useTimeZoneCookie(): Ref<string | null> {
14
+ return useDmsCookie<string | null>(TIME_ZONE_COOKIE, {
15
+ default: () => null,
16
+ maxAge: TIME_ZONE_COOKIE_MAX_AGE,
17
+ path: "/",
18
+ sameSite: "lax",
19
+ });
20
+ }
21
+
22
+ /**
23
+ * Whether this render resolves a period as the browser does. The browser
24
+ * always does; a server once the browser's cookie names its time zone, in
25
+ * which the presets then resolve (`readerClock`).
26
+ */
27
+ export function resolvesPeriodsAsBrowser(): boolean {
28
+ if (typeof window !== "undefined") return true;
29
+ const zone = useTimeZoneCookie().value;
30
+ return !!zone && isKnownTimeZone(zone);
31
+ }