@antelopejs/dms 0.8.2 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (354) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/dist/implementations/dms/frontend-backend-proxies.d.ts +3 -0
  3. package/dist/implementations/dms/frontend-backend-proxies.js +39 -0
  4. package/dist/implementations/dms/frontend-backend-proxies.js.map +1 -0
  5. package/dist/implementations/dms/frontend-bootstrap.d.ts +1 -0
  6. package/dist/implementations/dms/frontend-bootstrap.js +13 -0
  7. package/dist/implementations/dms/frontend-bootstrap.js.map +1 -1
  8. package/dist/implementations/dms/frontend-modules.d.ts +4 -2
  9. package/dist/implementations/dms/frontend-modules.js +8 -7
  10. package/dist/implementations/dms/frontend-modules.js.map +1 -1
  11. package/dist/implementations/dms/manifest.d.ts +6 -0
  12. package/dist/implementations/dms/manifest.js +13 -0
  13. package/dist/implementations/dms/manifest.js.map +1 -1
  14. package/dist/implementations/dms/menu-categories.d.ts +16 -0
  15. package/dist/implementations/dms/menu-categories.js +30 -0
  16. package/dist/implementations/dms/menu-categories.js.map +1 -0
  17. package/dist/implementations/dms/nav-badges.d.ts +0 -8
  18. package/dist/implementations/dms/nav-badges.js +0 -16
  19. package/dist/implementations/dms/nav-badges.js.map +1 -1
  20. package/dist/implementations/dms/page.d.ts +11 -5
  21. package/dist/implementations/dms/page.js +56 -41
  22. package/dist/implementations/dms/page.js.map +1 -1
  23. package/dist/implementations/dms-html-render/index.js +5 -2
  24. package/dist/implementations/dms-html-render/index.js.map +1 -1
  25. package/dist/implementations/dms-notifications/index.d.ts +4 -3
  26. package/dist/implementations/dms-notifications/index.js +14 -6
  27. package/dist/implementations/dms-notifications/index.js.map +1 -1
  28. package/dist/pages/settings/settings-index.js +3 -2
  29. package/dist/pages/settings/settings-index.js.map +1 -1
  30. package/dist/pages/settings/users/invites.js +2 -2
  31. package/dist/pages/settings/users/invites.js.map +1 -1
  32. package/dist/pages/settings/users/member-change-notifications.js +17 -6
  33. package/dist/pages/settings/users/member-change-notifications.js.map +1 -1
  34. package/dist/pages/settings/users/member-invite-form.d.ts +3 -2
  35. package/dist/pages/settings/users/member-invite-form.js +7 -4
  36. package/dist/pages/settings/users/member-invite-form.js.map +1 -1
  37. package/dist/pages/settings/users/member-roles-field.js +2 -1
  38. package/dist/pages/settings/users/member-roles-field.js.map +1 -1
  39. package/dist/pages/settings/users/members.js +4 -1
  40. package/dist/pages/settings/users/members.js.map +1 -1
  41. package/dist/pages/settings/users/notifications.d.ts +6 -0
  42. package/dist/pages/settings/users/notifications.js +23 -15
  43. package/dist/pages/settings/users/notifications.js.map +1 -1
  44. package/dist/pages/settings/users/profile.js +1 -1
  45. package/dist/pages/settings/users/profile.js.map +1 -1
  46. package/dist/pages/settings/users/region-form.d.ts +1 -1
  47. package/dist/pages/settings/users/region-form.js +2 -1
  48. package/dist/pages/settings/users/region-form.js.map +1 -1
  49. package/dist/pages/settings/users/role-editor-store.d.ts +3 -1
  50. package/dist/pages/settings/users/role-editor-store.js +17 -4
  51. package/dist/pages/settings/users/role-editor-store.js.map +1 -1
  52. package/dist/pages/settings/users/roles.js +1 -0
  53. package/dist/pages/settings/users/roles.js.map +1 -1
  54. package/dist/realtime/registry.d.ts +1 -0
  55. package/dist/realtime/registry.js +5 -0
  56. package/dist/realtime/registry.js.map +1 -1
  57. package/dist/routes/realtime.d.ts +5 -1
  58. package/dist/routes/realtime.js +16 -9
  59. package/dist/routes/realtime.js.map +1 -1
  60. package/dist/test/integration/account-deletion.test.js +2 -2
  61. package/dist/test/integration/account-deletion.test.js.map +1 -1
  62. package/dist/test/integration/auth/auth-key-rotation.test.js +2 -1
  63. package/dist/test/integration/auth/auth-key-rotation.test.js.map +1 -1
  64. package/dist/test/integration/auth/current-password.test.js +2 -1
  65. package/dist/test/integration/auth/current-password.test.js.map +1 -1
  66. package/dist/test/integration/auth/session-revocation.test.js +2 -1
  67. package/dist/test/integration/auth/session-revocation.test.js.map +1 -1
  68. package/dist/test/integration/auth/sign-in-country.test.js +2 -1
  69. package/dist/test/integration/auth/sign-in-country.test.js.map +1 -1
  70. package/dist/test/integration/auth/two-factor-codes.test.js +2 -1
  71. package/dist/test/integration/auth/two-factor-codes.test.js.map +1 -1
  72. package/dist/test/integration/auth/two-factor-password.test.js +3 -2
  73. package/dist/test/integration/auth/two-factor-password.test.js.map +1 -1
  74. package/dist/test/integration/email-change.test.js +2 -1
  75. package/dist/test/integration/email-change.test.js.map +1 -1
  76. package/dist/test/integration/files/component-files.test.js +2 -2
  77. package/dist/test/integration/files/component-files.test.js.map +1 -1
  78. package/dist/test/integration/member-settings-access.test.js +8 -7
  79. package/dist/test/integration/member-settings-access.test.js.map +1 -1
  80. package/dist/test/integration/menu-access-events.test.d.ts +1 -0
  81. package/dist/test/integration/menu-access-events.test.js +148 -0
  82. package/dist/test/integration/menu-access-events.test.js.map +1 -0
  83. package/dist/test/integration/notification-inbox.test.js +4 -1
  84. package/dist/test/integration/notification-inbox.test.js.map +1 -1
  85. package/dist/test/integration/notification-locked-subjects.test.js +3 -2
  86. package/dist/test/integration/notification-locked-subjects.test.js.map +1 -1
  87. package/dist/test/integration/notification-unread-badge.test.js +51 -5
  88. package/dist/test/integration/notification-unread-badge.test.js.map +1 -1
  89. package/dist/test/integration/page-layout-route-guard.test.d.ts +1 -0
  90. package/dist/test/integration/page-layout-route-guard.test.js +56 -0
  91. package/dist/test/integration/page-layout-route-guard.test.js.map +1 -0
  92. package/dist/test/integration/permission-warnings.test.js +3 -2
  93. package/dist/test/integration/permission-warnings.test.js.map +1 -1
  94. package/dist/test/integration/removed-member-notifications.test.js +3 -2
  95. package/dist/test/integration/removed-member-notifications.test.js.map +1 -1
  96. package/dist/test/integration/role-delete-reassign.test.js +2 -1
  97. package/dist/test/integration/role-delete-reassign.test.js.map +1 -1
  98. package/dist/test/integration/role-permission-ancestors.test.js +2 -1
  99. package/dist/test/integration/role-permission-ancestors.test.js.map +1 -1
  100. package/dist/test/unit/frontend-bootstrap.test.js +13 -0
  101. package/dist/test/unit/frontend-bootstrap.test.js.map +1 -1
  102. package/dist/test/unit/implementations/dms/category-slug-registry.test.js +14 -5
  103. package/dist/test/unit/implementations/dms/category-slug-registry.test.js.map +1 -1
  104. package/dist/test/unit/implementations/dms/frontend-backend-proxies.test.d.ts +1 -0
  105. package/dist/test/unit/implementations/dms/frontend-backend-proxies.test.js +66 -0
  106. package/dist/test/unit/implementations/dms/frontend-backend-proxies.test.js.map +1 -0
  107. package/dist/test/unit/implementations/dms/manifest-strip.test.js +26 -0
  108. package/dist/test/unit/implementations/dms/manifest-strip.test.js.map +1 -1
  109. package/dist/test/unit/implementations/dms/nav-badges.test.js +23 -9
  110. package/dist/test/unit/implementations/dms/nav-badges.test.js.map +1 -1
  111. package/dist/test/unit/implementations/dms-html-render/generate-html.test.js +22 -4
  112. package/dist/test/unit/implementations/dms-html-render/generate-html.test.js.map +1 -1
  113. package/dist/test/unit/interfaces/dms/page-layout-query.test.js +12 -0
  114. package/dist/test/unit/interfaces/dms/page-layout-query.test.js.map +1 -1
  115. package/dist/test/unit/interfaces/dms/page-teardown.test.js +1 -1
  116. package/dist/test/unit/interfaces/dms/page-teardown.test.js.map +1 -1
  117. package/dist/test/unit/interfaces/dms-base/table-and-card-catalog.test.js +25 -0
  118. package/dist/test/unit/interfaces/dms-base/table-and-card-catalog.test.js.map +1 -1
  119. package/dist/test/unit/pages/settings/member-roles-field.test.js +1 -1
  120. package/dist/test/unit/pages/settings/member-roles-field.test.js.map +1 -1
  121. package/dist/test/unit/pages/settings/role-delete-confirm.test.js.map +1 -1
  122. package/dist/test/unit/realtime/menu-topics.test.d.ts +1 -0
  123. package/dist/test/unit/realtime/menu-topics.test.js +26 -0
  124. package/dist/test/unit/realtime/menu-topics.test.js.map +1 -0
  125. package/dist/validation/regional-preferences.schema.d.ts +2 -2
  126. package/frontend-vue/deferred-plugins.ts +3 -4
  127. package/frontend-vue/dms.email.ts +5 -1
  128. package/frontend-vue/dms.frontend.ts +15 -5
  129. package/frontend-vue/dms.server.ts +74 -0
  130. package/frontend-vue/layers/dms-auth/app/composables/useLogout.ts +1 -1
  131. package/frontend-vue/layers/dms-auth/app/composables/usePostLoginRedirect.ts +16 -3
  132. package/frontend-vue/layers/dms-auth/app/custom-pages/auth/2fa.vue +3 -1
  133. package/frontend-vue/layers/dms-auth/app/custom-pages/auth/backup.vue +3 -1
  134. package/frontend-vue/layers/dms-auth/app/custom-pages/auth/forgot-validation.vue +1 -1
  135. package/frontend-vue/layers/dms-auth/app/custom-pages/auth/oauth-complete.vue +26 -3
  136. package/frontend-vue/layers/dms-auth/app/custom-pages/auth/recover.vue +1 -1
  137. package/frontend-vue/layers/dms-auth/app/custom-pages/auth/signup.vue +22 -25
  138. package/frontend-vue/layers/dms-auth/app/custom-pages/auth/validate.vue +1 -1
  139. package/frontend-vue/layers/dms-auth/app/middleware/auth.ts +5 -7
  140. package/frontend-vue/layers/dms-auth/app/plugins/permissions-preload.ts +17 -0
  141. package/frontend-vue/layers/dms-auth/server/accounts.ts +135 -0
  142. package/frontend-vue/layers/dms-auth/server/actions.ts +251 -0
  143. package/frontend-vue/layers/dms-auth/server/oauth.ts +260 -0
  144. package/frontend-vue/layers/dms-auth/server/session.ts +158 -0
  145. package/frontend-vue/layers/dms-core/app/build/utils/asyncDataScope.ts +47 -0
  146. package/frontend-vue/layers/dms-core/app/build/utils/permission-preview.ts +6 -2
  147. package/frontend-vue/layers/dms-core/app/composables/auth/useApiData.ts +148 -0
  148. package/frontend-vue/layers/dms-core/app/composables/auth/useAuthFetch.ts +7 -20
  149. package/frontend-vue/layers/dms-core/app/composables/auth/useCurrentUser.ts +2 -2
  150. package/frontend-vue/layers/dms-core/app/composables/auth/useMultiAccount.ts +18 -5
  151. package/frontend-vue/layers/dms-core/app/composables/auth/usePermissions.ts +35 -6
  152. package/frontend-vue/layers/dms-core/app/composables/auth/useSessionRecovery.ts +1 -1
  153. package/frontend-vue/layers/dms-core/app/composables/period/compareResolvers.ts +20 -14
  154. package/frontend-vue/layers/dms-core/app/composables/period/periodClock.ts +121 -0
  155. package/frontend-vue/layers/dms-core/app/composables/period/periodRenderNow.ts +42 -0
  156. package/frontend-vue/layers/dms-core/app/composables/period/periodTimeZone.ts +31 -0
  157. package/frontend-vue/layers/dms-core/app/composables/period/presetResolvers.ts +48 -19
  158. package/frontend-vue/layers/dms-core/app/composables/period/usePeriod.ts +33 -4
  159. package/frontend-vue/layers/dms-core/app/composables/period/usePeriodScope.ts +78 -25
  160. package/frontend-vue/layers/dms-core/app/composables/realtime/usePageRealtime.ts +3 -0
  161. package/frontend-vue/layers/dms-core/app/composables/realtime/useUserRealtime.ts +36 -6
  162. package/frontend-vue/layers/dms-core/app/composables/user/useUserRegionalPreferences.ts +1 -1
  163. package/frontend-vue/layers/dms-core/app/plugins/async-data-scope.ts +26 -0
  164. package/frontend-vue/layers/dms-core/app/utils/pageRoutes.ts +22 -0
  165. package/frontend-vue/layers/dms-core/app/utils/regional.ts +36 -3
  166. package/frontend-vue/layers/dms-core/app/utils/trailingDeduper.ts +10 -1
  167. package/frontend-vue/layers/dms-core/shared/types/app-config.ts +48 -4
  168. package/frontend-vue/layers/dms-layout/app/app.config.ts +42 -23
  169. package/frontend-vue/layers/dms-layout/app/assets/css/main.css +72 -20
  170. package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardFrame.vue +9 -0
  171. package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardSidebar.vue +0 -3
  172. package/frontend-vue/layers/dms-layout/app/build/components/notification/NotificationPopover.vue +9 -3
  173. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/members/MemberRolePicker.vue +13 -12
  174. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/notification/NotificationInboxDisplay.vue +17 -7
  175. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/notification/NotificationPreferencesForm.vue +0 -1
  176. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/profile/ProfilePersonalInfo.vue +36 -33
  177. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/profile/ProfileSecuritySummary.vue +1 -2
  178. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/roles/role-types.ts +5 -4
  179. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/security/SecuritySessions.vue +35 -28
  180. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/security/SecurityStatus.vue +0 -1
  181. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/security/SecurityTwoFactor.vue +1 -1
  182. package/frontend-vue/layers/dms-layout/app/build/composables/dev-reload/useDevReloadHolder.ts +7 -6
  183. package/frontend-vue/layers/dms-layout/app/build/composables/layout/sidePanelState.ts +1 -1
  184. package/frontend-vue/layers/dms-layout/app/build/composables/navigation/useVisitNavBadges.ts +26 -0
  185. package/frontend-vue/layers/dms-layout/app/build/composables/notification/useNotificationCatalog.ts +25 -23
  186. package/frontend-vue/layers/dms-layout/app/build/composables/notification/useNotificationPreferences.ts +44 -23
  187. package/frontend-vue/layers/dms-layout/app/build/composables/security/useSessionHandoff.ts +1 -1
  188. package/frontend-vue/layers/dms-layout/app/build/composables/settings/profile/preferencesSummary.ts +1 -1
  189. package/frontend-vue/layers/dms-layout/app/build/composables/settings/profile/useAccountData.ts +3 -2
  190. package/frontend-vue/layers/dms-layout/app/build/composables/settings/profile/useProfileAvatar.ts +1 -18
  191. package/frontend-vue/layers/dms-layout/app/build/composables/settings/profile/useProfilePreferences.ts +19 -23
  192. package/frontend-vue/layers/dms-layout/app/build/composables/settings/security/useSecurityFormat.ts +6 -3
  193. package/frontend-vue/layers/dms-layout/app/build/composables/settings/security/useSecurityOverview.ts +13 -18
  194. package/frontend-vue/layers/dms-layout/app/build/utils/modules-catalog.ts +8 -6
  195. package/frontend-vue/layers/dms-layout/app/build/utils/publicErrorMessage.ts +19 -0
  196. package/frontend-vue/layers/dms-layout/app/components/navigation/CollapsedMenuItem.vue +0 -11
  197. package/frontend-vue/layers/dms-layout/app/components/navigation/NavigationMenu.vue +3 -13
  198. package/frontend-vue/layers/dms-layout/app/composables/general/useSystemState.ts +7 -11
  199. package/frontend-vue/layers/dms-layout/app/composables/notification/useNotifications.ts +103 -22
  200. package/frontend-vue/layers/dms-layout/app/composables/page/useModulesListing.ts +25 -102
  201. package/frontend-vue/layers/dms-layout/app/composables/page/usePageComponentPreload.ts +0 -10
  202. package/frontend-vue/layers/dms-layout/app/composables/page/useSiteLayout.ts +177 -96
  203. package/frontend-vue/layers/dms-layout/app/composables/profile/useUserLanguage.ts +3 -2
  204. package/frontend-vue/layers/dms-layout/app/custom-pages/modules.vue +6 -28
  205. package/frontend-vue/layers/dms-layout/app/emails/components/EmailLayout.vue +31 -11
  206. package/frontend-vue/layers/dms-layout/app/error.vue +5 -2
  207. package/frontend-vue/layers/dms-layout/app/middleware/module-routing.global.ts +1 -1
  208. package/frontend-vue/layers/dms-layout/app/middleware/page-leave-guard.global.ts +2 -2
  209. package/frontend-vue/layers/dms-layout/app/pages/[...slug].vue +12 -9
  210. package/frontend-vue/layers/dms-layout/app/plugins/dms-menu-sync.client.ts +31 -3
  211. package/frontend-vue/layers/dms-layout/app/plugins/favicon.ts +58 -0
  212. package/frontend-vue/layers/dms-layout/app/plugins/notification-stream.client.ts +15 -7
  213. package/frontend-vue/layers/dms-layout/app/plugins/profile-sync.client.ts +5 -1
  214. package/frontend-vue/layers/dms-layout/app/plugins/regional-preferences.ts +10 -4
  215. package/frontend-vue/layers/dms-layout/app/plugins/time-zone-cookie.client.ts +12 -0
  216. package/frontend-vue/layers/dms-layout/app/types/page.ts +13 -0
  217. package/frontend-vue/layers/dms-layout/server/html-render.ts +58 -0
  218. package/frontend-vue/layers/dms-layout/server/render-token.ts +95 -0
  219. package/frontend-vue/layers/dms-onboarding/app/composables/onboarding/useOnboarding.ts +25 -21
  220. package/frontend-vue/layers/dms-onboarding/app/middleware/onboarding.global.ts +1 -1
  221. package/frontend-vue/layers/dms-onboarding/app/plugins/onboarding-preload.ts +9 -0
  222. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/registerDefaults.ts +4 -1
  223. package/frontend-vue/layers/dms-ui/app/build/composables/files/useFileMetadata.ts +162 -0
  224. package/frontend-vue/layers/dms-ui/app/build/composables/form/formRecordLoad.ts +23 -4
  225. package/frontend-vue/layers/dms-ui/app/build/composables/live/usePageDataRefresh.ts +101 -0
  226. package/frontend-vue/layers/dms-ui/app/build/composables/navigation/useNavBadges.ts +33 -6
  227. package/frontend-vue/layers/dms-ui/app/build/composables/period/periodDisplay.ts +19 -16
  228. package/frontend-vue/layers/dms-ui/app/build/composables/period/periodQuery.ts +40 -20
  229. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableFooter.ts +6 -8
  230. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableListData.ts +42 -14
  231. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableRows.ts +19 -8
  232. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViews.ts +1 -1
  233. package/frontend-vue/layers/dms-ui/app/components/chart/ChartCard.vue +5 -2
  234. package/frontend-vue/layers/dms-ui/app/components/flow-canvas/FlowCanvas.vue +9 -9
  235. package/frontend-vue/layers/dms-ui/app/components/form/Form.vue +20 -18
  236. package/frontend-vue/layers/dms-ui/app/components/form/components/Cascader.vue +2 -1
  237. package/frontend-vue/layers/dms-ui/app/components/form/components/File.vue +26 -132
  238. package/frontend-vue/layers/dms-ui/app/components/form/components/Image.vue +40 -105
  239. package/frontend-vue/layers/dms-ui/app/components/form/components/InputAddress.vue +2 -0
  240. package/frontend-vue/layers/dms-ui/app/components/form/components/Relation.vue +4 -4
  241. package/frontend-vue/layers/dms-ui/app/components/period/PeriodSelector.vue +9 -13
  242. package/frontend-vue/layers/dms-ui/app/components/table-view/CascaderPath.vue +2 -1
  243. package/frontend-vue/layers/dms-ui/app/components/table-view/FilePreview.vue +12 -24
  244. package/frontend-vue/layers/dms-ui/app/components/table-view/ImagePreview.vue +17 -29
  245. package/frontend-vue/layers/dms-ui/app/components/table-view/KanbanBoard.vue +7 -3
  246. package/frontend-vue/layers/dms-ui/app/components/table-view/TableView.vue +20 -8
  247. package/frontend-vue/layers/dms-ui/app/components/top-list/TopListCard.vue +8 -11
  248. package/frontend-vue/layers/dms-ui/app/composables/chart/useChartFetch.ts +146 -182
  249. package/frontend-vue/layers/dms-ui/app/composables/live/usePageLiveRefresh.ts +8 -2
  250. package/frontend-vue/layers/dms-ui/app/composables/useFileReadUrls.ts +11 -99
  251. package/frontend-vue/layers/dms-ui/app/utils/blockRefresh.ts +32 -3
  252. package/frontend-vue/package.json +2 -2
  253. package/frontend-vue/pnpm-lock.yaml +5 -5
  254. package/frontend-vue/server/private-options.ts +27 -0
  255. package/frontend-vue/tests/async-data-scope.test.ts +121 -0
  256. package/frontend-vue/tests/auth-middleware-error.test.ts +3 -3
  257. package/frontend-vue/tests/banner-block-fetch.test.ts +3 -0
  258. package/frontend-vue/tests/block-fetch-options-forwarding.test.ts +3 -0
  259. package/frontend-vue/tests/block-items-period-realtime.test.ts +3 -0
  260. package/frontend-vue/tests/block-not-found.test.ts +16 -2
  261. package/frontend-vue/tests/block-page-refresh.test.ts +3 -0
  262. package/frontend-vue/tests/block-route-params.test.ts +3 -0
  263. package/frontend-vue/tests/branding-defaults.test.ts +34 -0
  264. package/frontend-vue/tests/builtin-emails.test.ts +23 -2
  265. package/frontend-vue/tests/chart-live-pause.test.ts +4 -1
  266. package/frontend-vue/tests/chart-value-propagation.test.ts +4 -1
  267. package/frontend-vue/tests/composed-text-blocks.test.ts +3 -0
  268. package/frontend-vue/tests/email-layout.test.ts +66 -4
  269. package/frontend-vue/tests/error-page-message.test.ts +25 -2
  270. package/frontend-vue/tests/favicon.test.ts +131 -0
  271. package/frontend-vue/tests/file-fields-ssr.test.ts +145 -0
  272. package/frontend-vue/tests/file-read-urls.test.ts +145 -34
  273. package/frontend-vue/tests/file-unavailable.test.ts +5 -1
  274. package/frontend-vue/tests/fixtures/dataEngine.ts +44 -0
  275. package/frontend-vue/tests/form-load-error.test.ts +55 -1
  276. package/frontend-vue/tests/helpers/api-data-runtime.ts +77 -0
  277. package/frontend-vue/tests/image-hydration-ids.test.ts +21 -16
  278. package/frontend-vue/tests/image-reference-preservation.test.ts +59 -53
  279. package/frontend-vue/tests/input-number-adjust.test.ts +14 -0
  280. package/frontend-vue/tests/key-value-list-copy.test.ts +3 -0
  281. package/frontend-vue/tests/member-role-picker.test.ts +4 -2
  282. package/frontend-vue/tests/menu-sync-reconnect.test.ts +47 -0
  283. package/frontend-vue/tests/metadata-retry.test.ts +5 -1
  284. package/frontend-vue/tests/module-routing.test.ts +2 -5
  285. package/frontend-vue/tests/modules-catalog.test.ts +17 -4
  286. package/frontend-vue/tests/modules-listing-transfer.test.ts +95 -0
  287. package/frontend-vue/tests/multi-account-validate.test.ts +23 -1
  288. package/frontend-vue/tests/nav-badge-tones.test.ts +66 -3
  289. package/frontend-vue/tests/notification-counts-from-writes.test.ts +223 -0
  290. package/frontend-vue/tests/notification-counts-transfer.test.ts +88 -0
  291. package/frontend-vue/tests/notification-inbox-display.test.ts +57 -5
  292. package/frontend-vue/tests/notification-popover-errors.test.ts +21 -1
  293. package/frontend-vue/tests/oauth-complete-handoff.test.ts +209 -0
  294. package/frontend-vue/tests/onboarding-middleware.test.ts +1 -1
  295. package/frontend-vue/tests/onboarding-transfer.test.ts +106 -0
  296. package/frontend-vue/tests/page-data-refresh.test.ts +141 -0
  297. package/frontend-vue/tests/page-fill-height.test.ts +16 -3
  298. package/frontend-vue/tests/page-no-visible-blocks.test.ts +12 -2
  299. package/frontend-vue/tests/page-realtime-remount.test.ts +1 -1
  300. package/frontend-vue/tests/page-record-label.test.ts +0 -7
  301. package/frontend-vue/tests/page-validation-redirect.test.ts +12 -2
  302. package/frontend-vue/tests/period-hydration-now.test.ts +185 -0
  303. package/frontend-vue/tests/period-live-pause.test.ts +3 -0
  304. package/frontend-vue/tests/period-reader-zone.test.ts +137 -0
  305. package/frontend-vue/tests/period-scope-first-fetch.test.ts +17 -1
  306. package/frontend-vue/tests/period-ssr.test.ts +195 -0
  307. package/frontend-vue/tests/permission-preview-partial.test.ts +0 -2
  308. package/frontend-vue/tests/permissions-transfer.test.ts +194 -0
  309. package/frontend-vue/tests/post-login-redirect.test.ts +10 -5
  310. package/frontend-vue/tests/profile-personal-info-ssr.test.ts +150 -0
  311. package/frontend-vue/tests/profile-preferences-summary.test.ts +15 -11
  312. package/frontend-vue/tests/realtime-reconnect.test.ts +22 -3
  313. package/frontend-vue/tests/roles-name-errors.test.ts +1 -1
  314. package/frontend-vue/tests/row-side-panel.test.ts +1 -1
  315. package/frontend-vue/tests/security-sessions-load-error.test.ts +3 -1
  316. package/frontend-vue/tests/server-routes-vs-pages.test.ts +26 -0
  317. package/frontend-vue/tests/server-routes.test.ts +582 -0
  318. package/frontend-vue/tests/session-handoff.test.ts +1 -1
  319. package/frontend-vue/tests/settings-reads-ssr.test.ts +232 -0
  320. package/frontend-vue/tests/side-panels.test.ts +8 -1
  321. package/frontend-vue/tests/sign-in-exit-history.test.ts +219 -0
  322. package/frontend-vue/tests/signup-invitation-link.test.ts +56 -8
  323. package/frontend-vue/tests/site-layout-categories.test.ts +42 -0
  324. package/frontend-vue/tests/site-layout-hydration.test.ts +3 -3
  325. package/frontend-vue/tests/site-layout-rendered.test.ts +103 -0
  326. package/frontend-vue/tests/site-layout-resync.test.ts +87 -0
  327. package/frontend-vue/tests/ssr-guard-preload.test.ts +188 -0
  328. package/frontend-vue/tests/table-cell-tones.test.ts +36 -1
  329. package/frontend-vue/tests/table-data-ssr.test.ts +195 -0
  330. package/frontend-vue/tests/table-list-data-requests.test.ts +154 -0
  331. package/frontend-vue/tests/table-retry-loading.test.ts +37 -0
  332. package/frontend-vue/tests/table-source-navigation.test.ts +0 -1
  333. package/frontend-vue/tests/table-source-period-scope.test.ts +0 -1
  334. package/frontend-vue/tests/table-view-views.test.ts +1 -1
  335. package/frontend-vue/tests/top-list-card-actions.test.ts +3 -0
  336. package/frontend-vue/tests/top-list-card-period-badge.test.ts +79 -0
  337. package/frontend-vue/tests/top-list-card-value-suffix.test.ts +3 -0
  338. package/frontend-vue/tests/trailing-deduper.test.ts +22 -0
  339. package/frontend-vue/tests/use-api-data-update.test.ts +119 -0
  340. package/frontend-vue/tests/use-api-data.test.ts +410 -0
  341. package/frontend-vue/tests/user-language.test.ts +8 -5
  342. package/frontend-vue/tests/widget-label-i18n.test.ts +4 -1
  343. package/frontend-vue/vitest.config.ts +6 -0
  344. package/package.json +2 -2
  345. package/skills/dms/SKILL.md +2 -1
  346. package/skills/dms-module/SKILL.md +1 -1
  347. package/skills/dms-pages/SKILL.md +4 -1
  348. package/frontend-vue/layers/dms-core/app/build/composables/page/usePageRecordLabel.ts +0 -9
  349. package/frontend-vue/layers/dms-layout/app/composables/page/usePrefetch.ts +0 -21
  350. package/frontend-vue/layers/dms-ui/app/build/composables/form/useControlError.ts +0 -10
  351. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useServerRenderedAsyncData.ts +0 -23
  352. package/frontend-vue/tests/control-error-path.test.ts +0 -11
  353. package/frontend-vue/tests/modules-listing-per-request.test.ts +0 -104
  354. package/frontend-vue/tests/page-layout-background-refresh.test.ts +0 -321
@@ -367,41 +367,41 @@ defineExpose(flow);
367
367
  halo (which only targets the built-in node types), keeping node and edge
368
368
  selection states visually consistent. */
369
369
  .dms-flow-canvas .vue-flow__node.selected {
370
- box-shadow: 0 0 0 1.5px var(--color-dms-500);
370
+ box-shadow: 0 0 0 1.5px var(--ui-color-primary-500);
371
371
  }
372
372
 
373
- /* Handles: DMS cyan accent, ringed with the card surface for contrast. */
373
+ /* Handles: primary accent, ringed with the card surface for contrast. */
374
374
  .dms-flow-canvas .vue-flow__handle {
375
375
  width: 9px;
376
376
  height: 9px;
377
- background: var(--color-dms-500);
377
+ background: var(--ui-color-primary-500);
378
378
  border: 2px solid var(--dms-surface-card);
379
379
  border-radius: 9999px;
380
380
  }
381
381
 
382
382
  .dms-flow-canvas .vue-flow__handle:hover {
383
- background: var(--color-dms-400);
383
+ background: var(--ui-color-primary-400);
384
384
  }
385
385
 
386
- /* Edges: hairline by default, cyan accent on selection / animation. */
386
+ /* Edges: hairline by default, primary accent on selection / animation. */
387
387
  .dms-flow-canvas .vue-flow__edge-path {
388
388
  stroke: var(--ui-border-accented);
389
389
  stroke-width: 1.5;
390
390
  }
391
391
 
392
392
  .dms-flow-canvas .vue-flow__edge.animated .vue-flow__edge-path {
393
- stroke: var(--color-dms-500);
393
+ stroke: var(--ui-color-primary-500);
394
394
  }
395
395
 
396
396
  .dms-flow-canvas .vue-flow__edge.selected .vue-flow__edge-path,
397
397
  .dms-flow-canvas .vue-flow__edge:focus .vue-flow__edge-path,
398
398
  .dms-flow-canvas .vue-flow__edge:focus-visible .vue-flow__edge-path {
399
- stroke: var(--color-dms-500);
399
+ stroke: var(--ui-color-primary-500);
400
400
  stroke-width: 2;
401
401
  }
402
402
 
403
403
  .dms-flow-canvas .vue-flow__connection-path {
404
- stroke: var(--color-dms-500);
404
+ stroke: var(--ui-color-primary-500);
405
405
  stroke-width: 2;
406
406
  }
407
407
 
@@ -514,7 +514,7 @@ defineExpose(flow);
514
514
  }
515
515
 
516
516
  .dms-flow-canvas__delete-button:focus-visible {
517
- outline: 2px solid var(--color-dms-500);
517
+ outline: 2px solid var(--ui-color-primary-500);
518
518
  outline-offset: 1px;
519
519
  }
520
520
  </style>
@@ -549,30 +549,32 @@ allFields.value.forEach((field) => {
549
549
 
550
550
  let recordLoad: FormRecordLoad | undefined;
551
551
  if (props.fetchUrl) {
552
+ // Keyed on the record too: the load outlives a drawer or modal, and the
553
+ // next opening on another row must not show this one's values. A reopening
554
+ // on the same row shows the cached values at once and reads them again.
552
555
  const formLoad = await useDmsAsyncData(
553
- `form-${props.componentId}-${props.pageId}`,
556
+ `form-${props.componentId}-${props.pageId}-${resolvedFetchUrl.value ?? ""}`,
554
557
  async () => ({
555
558
  values: await fetchData(),
556
559
  initial: initialValues.value,
557
560
  }),
558
561
  );
559
562
 
560
- // The load is cached under this key, and outlives a drawer or modal: a
561
- // reopening would show the values of the previous one, stale if the row
562
- // changed since (here or elsewhere). Marked idle once read, every mounting
563
- // fetches the row again.
564
- onMounted(() => {
565
- formLoad.status.value = "idle";
566
- });
567
-
568
- recordLoad = trackFormRecordLoad(formLoad, (payload) => {
569
- if (payload.values && Object.keys(payload.values).length > 0) {
570
- Object.assign(state.value, payload.values);
571
- }
572
- if (payload.initial && payload.initial !== initialValues.value) {
573
- initialValues.value = { ...initialValues.value, ...payload.initial };
574
- }
575
- });
563
+ recordLoad = trackFormRecordLoad(
564
+ formLoad,
565
+ (payload) => {
566
+ if (payload.values && Object.keys(payload.values).length > 0) {
567
+ Object.assign(state.value, payload.values);
568
+ }
569
+ if (payload.initial && payload.initial !== initialValues.value) {
570
+ initialValues.value = { ...initialValues.value, ...payload.initial };
571
+ }
572
+ },
573
+ {
574
+ isEditing: () => changedFields.value.length > 0,
575
+ onReloaded: markFormClean,
576
+ },
577
+ );
576
578
  }
577
579
 
578
580
  const loadFailure = computed(
@@ -810,7 +812,7 @@ const realtimeContext = computed(() =>
810
812
  parseRealtimeContext(resolvedFetchUrl.value),
811
813
  );
812
814
  const showConcurrentEditBanner = ref(false);
813
- const { user: realtimeUser } = useUserSession();
815
+ const { user: realtimeUser } = useDmsUserSession();
814
816
  const { $authFetch } = useAuthFetch();
815
817
  const pageRealtime = useUserRealtime();
816
818
  const acquired = ref<{
@@ -83,7 +83,8 @@ const { data, status, execute } = await useDmsAsyncData(
83
83
  },
84
84
  });
85
85
  },
86
- { lazy: true, immediate: false },
86
+ // Options to pick from, not the page's data.
87
+ { lazy: true, immediate: false, refreshOnPageRefresh: false },
87
88
  );
88
89
 
89
90
  const keyMapping = computed(() => resolveCascaderKeyMapping(props.keyMapping));
@@ -3,7 +3,6 @@ import { useId } from "vue";
3
3
  import DmsFieldError from "../../field-error/FieldError.vue";
4
4
  import { fieldErrorId } from "#dms-core/app/composables/useFieldErrors";
5
5
  import { FORM_FIELD_LOADING_KEY } from "../../../composables/form/types/field-loading";
6
- import { FORM_CONTENT_LANGUAGE_KEY } from "../../../composables/form/types/content-language";
7
6
  import type { PresignResponse } from "../../../composables/form/useUploadWithProgress";
8
7
  import {
9
8
  checkFileConstraints,
@@ -11,9 +10,9 @@ import {
11
10
  type FileFieldConstraints,
12
11
  } from "../../../utils/fileConstraints";
13
12
  import {
14
- isDefinitiveMetadataFailure,
15
- resolveMetadataRetryDelay,
16
- } from "../../../utils/metadataRetry";
13
+ type FileMetadata,
14
+ useFileMetadata,
15
+ } from "../../../build/composables/files/useFileMetadata";
17
16
 
18
17
  export type FileConstraints = FileFieldConstraints;
19
18
 
@@ -24,15 +23,6 @@ interface UploadingFile {
24
23
  error?: string;
25
24
  }
26
25
 
27
- interface FileMetadataResponse {
28
- resourceKey: string;
29
- filename: string;
30
- size: number;
31
- mimetype: string;
32
- url: string;
33
- expiresAt?: number;
34
- }
35
-
36
26
  interface FileProps {
37
27
  modelValue?: string | string[] | null;
38
28
  multiple?: boolean;
@@ -70,14 +60,8 @@ const { uploadWithProgress } = useUploadWithProgress();
70
60
  const selectedFiles = ref<File | File[] | null>(null);
71
61
  const uploadingFiles = ref<UploadingFile[]>([]);
72
62
  const uploadedKeys = ref<string[]>([]);
73
- const fileMetadata = ref<Map<string, FileMetadataResponse>>(new Map());
74
- const unavailableKeys = ref<Set<string>>(new Set());
75
- const metadataTimers = new Map<string, ReturnType<typeof setTimeout>>();
76
- const metadataFailures = new Map<string, number>();
77
- const pendingMetadata = new Map<string, Promise<void>>();
78
- let isDisposed = false;
79
- const contentLanguage = inject(FORM_CONTENT_LANGUAGE_KEY, undefined);
80
- const URL_REFRESH_LEAD_MS = 5000;
63
+ // Read with the page for the stored value, in the browser for a new one.
64
+ const files = useFileMetadata(props.storage);
81
65
 
82
66
  const initializeFromModelValue = (
83
67
  val: string | string[] | null | undefined,
@@ -93,118 +77,26 @@ const initializeFromModelValue = (
93
77
 
94
78
  initializeFromModelValue(props.modelValue);
95
79
 
96
- const scheduleMetadataRefresh = (resourceKey: string, expiresAt?: number) => {
97
- if (import.meta.env.SSR) return;
98
- if (isDisposed || !uploadedKeys.value.includes(resourceKey)) return;
99
- const currentTimer = metadataTimers.get(resourceKey);
100
- if (currentTimer) clearTimeout(currentTimer);
101
- if (expiresAt === undefined) return;
102
- const remaining = Math.max(expiresAt - Date.now(), 0);
103
- const refreshLead = Math.min(URL_REFRESH_LEAD_MS, remaining / 2);
104
- metadataTimers.set(
105
- resourceKey,
106
- setTimeout(() => fetchMetadata(resourceKey, true), remaining - refreshLead),
107
- );
108
- };
109
-
110
- const fetchMetadata = async (
111
- resourceKey: string,
112
- force = false,
113
- ): Promise<void> => {
114
- if (isDisposed) return;
115
- const pending = pendingMetadata.get(resourceKey);
116
- if (pending) {
117
- await pending;
118
- if (force) await fetchMetadata(resourceKey, true);
119
- return;
120
- }
121
- if (!force && fileMetadata.value.has(resourceKey)) return;
122
-
123
- const request = requestMetadata(resourceKey);
124
- pendingMetadata.set(resourceKey, request);
125
- await request;
126
- };
127
-
128
- const markUnavailable = (resourceKey: string, error: unknown) => {
129
- const cached = fileMetadata.value.get(resourceKey);
130
- const isExpired =
131
- cached?.expiresAt !== undefined && cached.expiresAt <= Date.now();
132
- if (cached && !isExpired && !isDefinitiveMetadataFailure(error)) return;
133
- fileMetadata.value.set(resourceKey, {
134
- resourceKey,
135
- filename: cached?.filename ?? resourceKey,
136
- size: cached?.size ?? 0,
137
- mimetype: cached?.mimetype ?? "application/octet-stream",
138
- url: "",
139
- });
140
- unavailableKeys.value.add(resourceKey);
141
- };
142
-
143
- const handleMetadataFailure = (resourceKey: string, error: unknown) => {
144
- markUnavailable(resourceKey, error);
145
- const failures = (metadataFailures.get(resourceKey) ?? 0) + 1;
146
- metadataFailures.set(resourceKey, failures);
147
- const delay = resolveMetadataRetryDelay(error, failures);
148
- if (delay === null || import.meta.env.SSR) return;
149
- metadataTimers.set(
150
- resourceKey,
151
- setTimeout(() => fetchMetadata(resourceKey, true), delay),
152
- );
153
- };
154
-
155
- const requestMetadata = async (resourceKey: string): Promise<void> => {
156
- try {
157
- const metadata = await $authFetch<FileMetadataResponse>(
158
- `/api/files/metadata`,
159
- {
160
- query: {
161
- resourceKey,
162
- storage: props.storage,
163
- },
164
- headers: contentLanguage
165
- ? { [CONTENT_LANGUAGE_HEADER]: contentLanguage }
166
- : undefined,
167
- },
168
- );
169
- if (isDisposed || !uploadedKeys.value.includes(resourceKey)) return;
170
- fileMetadata.value.set(resourceKey, metadata);
171
- unavailableKeys.value.delete(resourceKey);
172
- metadataFailures.delete(resourceKey);
173
- scheduleMetadataRefresh(resourceKey, metadata.expiresAt);
174
- } catch (error) {
175
- if (isDisposed || !uploadedKeys.value.includes(resourceKey)) return;
176
- handleMetadataFailure(resourceKey, error);
177
- } finally {
178
- pendingMetadata.delete(resourceKey);
179
- }
180
- };
181
-
182
80
  const openFile = async (resourceKey: string) => {
183
- await fetchMetadata(resourceKey, true);
184
- const url = fileMetadata.value.get(resourceKey)?.url;
81
+ await files.resolve(resourceKey, true);
82
+ const url = files.metadata(resourceKey)?.url;
185
83
  if (url) window.open(url, "_blank", "noopener,noreferrer");
186
84
  };
187
85
 
188
- const fetchAllMetadata = async (keys: string[]) => {
189
- await Promise.all(keys.map((key) => fetchMetadata(key)));
190
- };
191
-
192
86
  watch(
193
87
  () => props.modelValue,
194
88
  (val) => {
195
89
  const newKeys = !val ? [] : typeof val === "string" ? [val] : val;
196
90
  if (newKeys.join(",") !== uploadedKeys.value.join(",")) {
197
91
  uploadedKeys.value = newKeys;
198
- fetchAllMetadata(newKeys);
199
92
  }
200
93
  },
201
94
  );
202
95
 
96
+ // A copy: an upload into a multiple field pushes its key.
203
97
  watch(
204
- uploadedKeys,
205
- (keys) => {
206
- fetchAllMetadata(keys);
207
- },
98
+ () => [...uploadedKeys.value],
99
+ (keys) => keys.forEach((key) => void files.resolve(key)),
208
100
  { immediate: true },
209
101
  );
210
102
 
@@ -312,20 +204,27 @@ const removeUploadingFile = (id: string) => {
312
204
 
313
205
  const removeUploadedFile = (key: string) => {
314
206
  uploadedKeys.value = uploadedKeys.value.filter((k) => k !== key);
315
- fileMetadata.value.delete(key);
316
- unavailableKeys.value.delete(key);
317
- metadataFailures.delete(key);
318
- const timer = metadataTimers.get(key);
319
- if (timer) clearTimeout(timer);
320
207
  emitValue();
321
208
  };
322
209
 
323
- const getMetadata = (key: string): FileMetadataResponse | undefined => {
324
- return fileMetadata.value.get(key);
210
+ const isUnavailable = (key: string): boolean => files.isUnavailable(key);
211
+
212
+ /**
213
+ * What a stored file shows: its metadata, or, once it cannot be read, its
214
+ * last known name (the key otherwise) without a link.
215
+ */
216
+ const getMetadata = (key: string): FileMetadata | undefined => {
217
+ if (!isUnavailable(key)) return files.metadata(key);
218
+ const known = files.lastRead(key);
219
+ return {
220
+ resourceKey: key,
221
+ filename: known?.filename ?? key,
222
+ size: known?.size ?? 0,
223
+ mimetype: known?.mimetype ?? "application/octet-stream",
224
+ url: "",
225
+ };
325
226
  };
326
227
 
327
- const isUnavailable = (key: string): boolean => unavailableKeys.value.has(key);
328
-
329
228
  const isImage = (mimetype: string): boolean => {
330
229
  return mimetype.startsWith("image/");
331
230
  };
@@ -363,11 +262,6 @@ const maxSizeFormatted = computed(() => {
363
262
  const isUploading = computed(() => uploadingFiles.value.length > 0);
364
263
 
365
264
  const setFieldLoading = inject(FORM_FIELD_LOADING_KEY, null);
366
-
367
- onScopeDispose(() => {
368
- isDisposed = true;
369
- metadataTimers.forEach(clearTimeout);
370
- });
371
265
  </script>
372
266
 
373
267
  <template>
@@ -2,7 +2,6 @@
2
2
  import DmsFieldError from "../../field-error/FieldError.vue";
3
3
  import { fieldErrorId } from "#dms-core/app/composables/useFieldErrors";
4
4
  import { FORM_FIELD_LOADING_KEY } from "../../../composables/form/types/field-loading";
5
- import { FORM_CONTENT_LANGUAGE_KEY } from "../../../composables/form/types/content-language";
6
5
  import type { PresignResponse } from "../../../composables/form/useUploadWithProgress";
7
6
  import { useKeyboardPlatform } from "../../../composables/global/keyboardPlatform";
8
7
  import {
@@ -13,10 +12,7 @@ import {
13
12
  type ImageResizeBounds,
14
13
  resizeImageToBounds,
15
14
  } from "../../../utils/imageResize";
16
- import {
17
- isDefinitiveMetadataFailure,
18
- resolveMetadataRetryDelay,
19
- } from "../../../utils/metadataRetry";
15
+ import { useFileMetadata } from "../../../build/composables/files/useFileMetadata";
20
16
 
21
17
  export type ImageConstraints = FileFieldConstraints;
22
18
 
@@ -51,16 +47,6 @@ interface GalleryItem {
51
47
  principal: boolean;
52
48
  status: ItemStatus;
53
49
  progress: number;
54
- expiresAt?: number;
55
- }
56
-
57
- interface FileMetadataResponse {
58
- resourceKey: string;
59
- filename: string;
60
- size: number;
61
- mimetype: string;
62
- url: string;
63
- expiresAt?: number;
64
50
  }
65
51
 
66
52
  const props = defineProps<ImageProps>();
@@ -79,10 +65,10 @@ const { formatShortcut } = useKeyboardPlatform();
79
65
  const pasteShortcut = computed(() => formatShortcut(["meta", "v"]));
80
66
  const { uploadWithProgress } = useUploadWithProgress();
81
67
  const setFieldLoading = inject(FORM_FIELD_LOADING_KEY, null);
82
- const contentLanguage = inject(FORM_CONTENT_LANGUAGE_KEY, undefined);
68
+ // Read with the page for the stored value, in the browser for a new one.
69
+ const files = useFileMetadata(props.storage);
83
70
 
84
71
  const DEFAULT_ACCEPT = "image/*";
85
- const URL_REFRESH_LEAD_MS = 5000;
86
72
  const FAILURE_LABEL_KEYS: Partial<Record<ItemStatus, string>> = {
87
73
  error: "dms.form.image.upload_failed",
88
74
  unavailable: "dms.form.image.file_unavailable",
@@ -114,10 +100,6 @@ const dragItemId = ref<string | null>(null);
114
100
  const dragOverItemId = ref<string | null>(null);
115
101
  const detailItemId = ref<string | null>(null);
116
102
  const detailAltDraft = ref("");
117
- const metadataTimers = new Map<string, ReturnType<typeof setTimeout>>();
118
- const metadataFailures = new Map<string, number>();
119
- const pendingMetadata = new Set<string>();
120
- let isDisposed = false;
121
103
 
122
104
  const toValueArray = (
123
105
  value: ImageItemValue | ImageItemValue[] | null | undefined,
@@ -126,84 +108,43 @@ const toValueArray = (
126
108
  return Array.isArray(value) ? value : [value];
127
109
  };
128
110
 
129
- const createItemFromValue = (value: ImageItemValue): GalleryItem => ({
130
- id: crypto.randomUUID(),
131
- key: value.key,
132
- file: null,
133
- url: "",
134
- filename: value.key,
135
- size: null,
136
- width: null,
137
- height: null,
138
- alt: value.alt ?? "",
139
- principal: value.principal ?? false,
140
- status: "done",
141
- progress: 100,
142
- });
143
-
144
- const scheduleItemMetadataRefresh = (item: GalleryItem, delay: number) => {
145
- if (import.meta.env.SSR) return;
146
- if (isDisposed || !items.value.some(({ id }) => id === item.id)) return;
147
- const currentTimer = metadataTimers.get(item.id);
148
- if (currentTimer) clearTimeout(currentTimer);
149
- metadataTimers.set(
150
- item.id,
151
- setTimeout(() => fetchItemMetadata(item, true), delay),
152
- );
153
- };
154
-
155
- const scheduleItemExpiryRefresh = (item: GalleryItem) => {
156
- if (item.expiresAt === undefined) return;
157
- const remaining = Math.max(item.expiresAt - Date.now(), 0);
158
- const refreshLead = Math.min(URL_REFRESH_LEAD_MS, remaining / 2);
159
- scheduleItemMetadataRefresh(item, remaining - refreshLead);
160
- };
161
-
162
- const handleItemMetadataFailure = (item: GalleryItem, error: unknown) => {
163
- if (
164
- isDefinitiveMetadataFailure(error) ||
165
- !item.url ||
166
- (item.expiresAt !== undefined && item.expiresAt <= Date.now())
167
- ) {
168
- item.url = "";
169
- item.status = "unavailable";
170
- }
171
- const failures = (metadataFailures.get(item.id) ?? 0) + 1;
172
- metadataFailures.set(item.id, failures);
173
- const delay = resolveMetadataRetryDelay(error, failures);
174
- if (delay !== null) scheduleItemMetadataRefresh(item, delay);
175
- };
176
-
177
- const fetchItemMetadata = async (item: GalleryItem, force = false) => {
178
- if (isDisposed) return;
179
- if (!item.key || (!force && item.url) || pendingMetadata.has(item.id)) return;
180
-
181
- pendingMetadata.add(item.id);
182
- try {
183
- const metadata = await $authFetch<FileMetadataResponse>(
184
- "/api/files/metadata",
185
- {
186
- query: { resourceKey: item.key, storage: props.storage },
187
- headers: contentLanguage
188
- ? { [CONTENT_LANGUAGE_HEADER]: contentLanguage }
189
- : undefined,
111
+ /**
112
+ * A stored image: what the tile shows of the file (its URL, name, size,
113
+ * whether it can be read) follows its shared metadata, so the server renders
114
+ * it and the URL stays fresh; the rest is the field's own.
115
+ */
116
+ const createItemFromValue = (value: ImageItemValue): GalleryItem =>
117
+ Object.defineProperties(
118
+ {
119
+ id: crypto.randomUUID(),
120
+ key: value.key,
121
+ file: null,
122
+ width: null,
123
+ height: null,
124
+ alt: value.alt ?? "",
125
+ principal: value.principal ?? false,
126
+ progress: 100,
127
+ },
128
+ {
129
+ url: {
130
+ get: () => files.metadata(value.key)?.url ?? "",
131
+ enumerable: true,
190
132
  },
191
- );
192
- if (isDisposed || !items.value.some(({ id }) => id === item.id)) return;
193
- item.url = metadata.url;
194
- item.filename = metadata.filename;
195
- item.size = metadata.size;
196
- item.expiresAt = metadata.expiresAt;
197
- item.status = "done";
198
- metadataFailures.delete(item.id);
199
- scheduleItemExpiryRefresh(item);
200
- } catch (error) {
201
- if (isDisposed || !items.value.some(({ id }) => id === item.id)) return;
202
- handleItemMetadataFailure(item, error);
203
- } finally {
204
- pendingMetadata.delete(item.id);
205
- }
206
- };
133
+ filename: {
134
+ get: () => files.lastRead(value.key)?.filename ?? value.key,
135
+ enumerable: true,
136
+ },
137
+ size: {
138
+ get: () => files.lastRead(value.key)?.size ?? null,
139
+ enumerable: true,
140
+ },
141
+ status: {
142
+ get: (): ItemStatus =>
143
+ files.isUnavailable(value.key) ? "unavailable" : "done",
144
+ enumerable: true,
145
+ },
146
+ },
147
+ ) as GalleryItem;
207
148
 
208
149
  const valueSignature = (values: ImageItemValue[]): string =>
209
150
  JSON.stringify(
@@ -225,17 +166,13 @@ const currentSignature = (): string =>
225
166
 
226
167
  const releaseItem = (item: GalleryItem) => {
227
168
  if (item.file && item.url) URL.revokeObjectURL(item.url);
228
- const timer = metadataTimers.get(item.id);
229
- if (timer) clearTimeout(timer);
230
- metadataTimers.delete(item.id);
231
- metadataFailures.delete(item.id);
232
169
  };
233
170
 
234
171
  const initializeFromModelValue = () => {
235
172
  items.value.forEach(releaseItem);
236
173
  const values = toValueArray(props.modelValue);
237
174
  items.value = values.map(createItemFromValue);
238
- items.value.forEach((item) => fetchItemMetadata(item));
175
+ values.forEach((value) => void files.resolve(value.key));
239
176
  };
240
177
 
241
178
  initializeFromModelValue();
@@ -594,9 +531,7 @@ const itemMeta = (item: GalleryItem): string => {
594
531
  };
595
532
 
596
533
  onBeforeUnmount(() => {
597
- isDisposed = true;
598
534
  items.value.forEach(releaseItem);
599
- metadataTimers.forEach(clearTimeout);
600
535
  });
601
536
  </script>
602
537
 
@@ -271,6 +271,8 @@ const dynamicCountryData = props.searchUrl
271
271
  lazy: true,
272
272
  immediate: false,
273
273
  watch: [countrySearchTermDebounced],
274
+ // Options to pick from, not the page's data.
275
+ refreshOnPageRefresh: false,
274
276
  })
275
277
  : { data: ref(null), status: ref("idle"), execute: () => {} };
276
278
 
@@ -107,13 +107,11 @@ const { $authFetch } = useAuthFetch();
107
107
  const toast = useToast();
108
108
  const { t } = useI18n();
109
109
  const { processI18n } = useTranslation();
110
- const { hasPermission, isLoaded, fetchPermissions } = usePermissions();
110
+ const { hasPermission, loadPermissions } = usePermissions();
111
111
  const { open: openDrawer } = useDrawer();
112
112
  const { clearGuards } = useLeaveGuard();
113
113
 
114
- if (!isLoaded.value) {
115
- await fetchPermissions().catch(() => {});
116
- }
114
+ await loadPermissions().catch(() => {});
117
115
 
118
116
  const addContainerId = `relation-add-${useId()}`;
119
117
  // Shallow: `SelectMenuItem` carries a string index signature, so a deeply
@@ -237,6 +235,8 @@ const { data, status, execute } = await useDmsAsyncData(
237
235
  lazy: true,
238
236
  immediate: false,
239
237
  watch: [searchTermDebounced],
238
+ // Options to pick from, not the page's data.
239
+ refreshOnPageRefresh: false,
240
240
  },
241
241
  );
242
242
 
@@ -19,7 +19,7 @@ import {
19
19
  type ComparisonControl,
20
20
  resolveComparisonControl,
21
21
  } from "../../build/composables/period/periodComparisonControl";
22
- import { isRelativePreset } from "#dms-core/app/composables/period/presetResolvers";
22
+ import { readerClock } from "#dms-core/app/composables/period/periodClock";
23
23
  import { usePeriodLocation } from "../../build/composables/period/usePeriodLocation";
24
24
  import {
25
25
  type CalendarRange,
@@ -332,20 +332,14 @@ const committedRangeLabel = computed(() => {
332
332
  return range ? formatPeriodRange(range, locale.value) : null;
333
333
  });
334
334
 
335
- /**
336
- * The instant a relative label is read from: a rolling window's own end, so
337
- * it holds still with the window while the page is paused.
338
- */
339
- function labelNow(preset: PeriodPreset, range: PeriodRange): Date {
340
- return isRelativePreset(preset) ? range.to : new Date();
341
- }
342
-
343
335
  // Only a window reaching now reads "since …"; one that has ended keeps its
344
- // bounds.
336
+ // bounds. "Now" is the instant the period was resolved from: it holds still
337
+ // with the period while the page is paused, and is the server's while the
338
+ // browser hydrates its render.
345
339
  const relativeRangeLabel = computed<string | null>(() => {
346
- const { preset, range } = period.state.value;
340
+ const { range } = period.state.value;
347
341
  if (props.rangeLabel !== RELATIVE_RANGE_LABEL || !range) return null;
348
- const now = labelNow(preset, range);
342
+ const now = period.resolvedAt.value;
349
343
  if (range.to.getTime() < now.getTime()) return null;
350
344
  const from = formatRelativeInstant(range.from, now, locale.value);
351
345
  return t("dms.period.since", { from }, `since ${from}`);
@@ -421,7 +415,9 @@ function comparisonKeyClass(active: boolean): string {
421
415
  }
422
416
 
423
417
  function compareRangeText(compare: PeriodRange, main: PeriodRange): string {
424
- const sameYear = compare.to.getFullYear() === main.to.getFullYear();
418
+ // In the reader's calendar, on the server as in the browser.
419
+ const clock = readerClock();
420
+ const sameYear = clock.wall(compare.to).year === clock.wall(main.to).year;
425
421
  return formatPeriodRange(compare, locale.value, !sameYear);
426
422
  }
427
423
 
@@ -47,7 +47,8 @@ const { data } = await useDmsAsyncData(
47
47
  params: { limit: CASCADER_FETCH_LIMIT },
48
48
  })
49
49
  : Promise.resolve(undefined),
50
- { lazy: true },
50
+ // The labels of a path, not the page's data.
51
+ { lazy: true, refreshOnPageRefresh: false },
51
52
  );
52
53
 
53
54
  const pathOf = (item: unknown): string[] =>