@antelopejs/dms 0.9.8 → 0.11.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 (451) hide show
  1. package/CHANGELOG.md +171 -0
  2. package/README.md +1 -1
  3. package/data/module-store/catalog.json +2 -2
  4. package/dist/attachments/table-view.js +2 -2
  5. package/dist/attachments/table-view.js.map +1 -1
  6. package/dist/automation/actions.d.ts +2 -0
  7. package/dist/automation/actions.js +116 -71
  8. package/dist/automation/actions.js.map +1 -1
  9. package/dist/automation/i18n.d.ts +4 -0
  10. package/dist/automation/i18n.js +19 -0
  11. package/dist/automation/i18n.js.map +1 -0
  12. package/dist/automation/send-notification.d.ts +21 -0
  13. package/dist/automation/send-notification.js +163 -0
  14. package/dist/automation/send-notification.js.map +1 -0
  15. package/dist/automation/triggers.js +19 -25
  16. package/dist/automation/triggers.js.map +1 -1
  17. package/dist/client-origin-cors.d.ts +1 -0
  18. package/dist/client-origin-cors.js +19 -0
  19. package/dist/client-origin-cors.js.map +1 -0
  20. package/dist/config.d.ts +3 -2
  21. package/dist/config.js +5 -7
  22. package/dist/config.js.map +1 -1
  23. package/dist/frontend-module.d.ts +4 -0
  24. package/dist/frontend-module.js +37 -0
  25. package/dist/frontend-module.js.map +1 -0
  26. package/dist/implementations/dms/client-ip.js +1 -1
  27. package/dist/implementations/dms/client-ip.js.map +1 -1
  28. package/dist/implementations/dms/dev-reload.d.ts +5 -0
  29. package/dist/implementations/dms/dev-reload.js +23 -3
  30. package/dist/implementations/dms/dev-reload.js.map +1 -1
  31. package/dist/implementations/dms/frontend-modules.d.ts +2 -4
  32. package/dist/implementations/dms/frontend-modules.js +38 -94
  33. package/dist/implementations/dms/frontend-modules.js.map +1 -1
  34. package/dist/implementations/dms/frontend-ready.d.ts +12 -2
  35. package/dist/implementations/dms/frontend-ready.js +34 -4
  36. package/dist/implementations/dms/frontend-ready.js.map +1 -1
  37. package/dist/implementations/dms/layout-banners.d.ts +2 -1
  38. package/dist/implementations/dms/layout-banners.js +9 -0
  39. package/dist/implementations/dms/layout-banners.js.map +1 -1
  40. package/dist/implementations/dms/manifest.d.ts +7 -2
  41. package/dist/implementations/dms/manifest.js +12 -7
  42. package/dist/implementations/dms/manifest.js.map +1 -1
  43. package/dist/implementations/dms/page.d.ts +4 -3
  44. package/dist/implementations/dms/page.js +49 -18
  45. package/dist/implementations/dms/page.js.map +1 -1
  46. package/dist/implementations/dms/permissions-resolver.d.ts +2 -2
  47. package/dist/implementations/dms/permissions-resolver.js +2 -2
  48. package/dist/implementations/dms/permissions-resolver.js.map +1 -1
  49. package/dist/implementations/dms/realtime.js +1 -0
  50. package/dist/implementations/dms/realtime.js.map +1 -1
  51. package/dist/implementations/dms-base/bulk-selection.d.ts +1 -1
  52. package/dist/implementations/dms-base/bulk-selection.js +23 -1
  53. package/dist/implementations/dms-base/bulk-selection.js.map +1 -1
  54. package/dist/implementations/dms-base/filter-tokens.js +5 -3
  55. package/dist/implementations/dms-base/filter-tokens.js.map +1 -1
  56. package/dist/implementations/dms-base/search-route.d.ts +1 -0
  57. package/dist/implementations/dms-base/search-route.js +35 -13
  58. package/dist/implementations/dms-base/search-route.js.map +1 -1
  59. package/dist/implementations/dms-base/table-view.js +11 -6
  60. package/dist/implementations/dms-base/table-view.js.map +1 -1
  61. package/dist/implementations/dms-notifications/index.d.ts +2 -2
  62. package/dist/implementations/dms-notifications/index.js +5 -5
  63. package/dist/implementations/dms-notifications/index.js.map +1 -1
  64. package/dist/index.js +32 -55
  65. package/dist/index.js.map +1 -1
  66. package/dist/pages/modules/index.d.ts +1 -0
  67. package/dist/pages/modules/index.js +1 -0
  68. package/dist/pages/modules/index.js.map +1 -1
  69. package/dist/pages/modules/modules-store.d.ts +9 -0
  70. package/dist/pages/modules/modules-store.js +30 -0
  71. package/dist/pages/modules/modules-store.js.map +1 -0
  72. package/dist/pages/settings/users/account-credentials.js +2 -2
  73. package/dist/pages/settings/users/account-credentials.js.map +1 -1
  74. package/dist/pages/settings/users/account-data-store.js +25 -8
  75. package/dist/pages/settings/users/account-data-store.js.map +1 -1
  76. package/dist/pages/settings/users/invite-extension-routes.js +9 -3
  77. package/dist/pages/settings/users/invite-extension-routes.js.map +1 -1
  78. package/dist/pages/settings/users/member-management.js +13 -14
  79. package/dist/pages/settings/users/member-management.js.map +1 -1
  80. package/dist/pages/settings/users/profile.d.ts +1 -4
  81. package/dist/pages/settings/users/profile.js +0 -35
  82. package/dist/pages/settings/users/profile.js.map +1 -1
  83. package/dist/pages/settings/users/role-editor-store.js +26 -18
  84. package/dist/pages/settings/users/role-editor-store.js.map +1 -1
  85. package/dist/pages/settings/users/role-editor.d.ts +0 -1
  86. package/dist/pages/settings/users/role-editor.js +1 -2
  87. package/dist/pages/settings/users/role-editor.js.map +1 -1
  88. package/dist/pages/settings/users/role-preview.js +1 -2
  89. package/dist/pages/settings/users/role-preview.js.map +1 -1
  90. package/dist/pages/settings/users/roles.js +2 -2
  91. package/dist/pages/settings/users/roles.js.map +1 -1
  92. package/dist/pages/settings/users/two-factor-operations.js.map +1 -1
  93. package/dist/realtime/broker.d.ts +6 -0
  94. package/dist/realtime/broker.js +8 -1
  95. package/dist/realtime/broker.js.map +1 -1
  96. package/dist/realtime/presence.d.ts +8 -3
  97. package/dist/realtime/presence.js +8 -4
  98. package/dist/realtime/presence.js.map +1 -1
  99. package/dist/realtime/sessions.d.ts +11 -1
  100. package/dist/realtime/sessions.js +21 -3
  101. package/dist/realtime/sessions.js.map +1 -1
  102. package/dist/realtime/table-view.d.ts +2 -2
  103. package/dist/realtime/table-view.js +4 -3
  104. package/dist/realtime/table-view.js.map +1 -1
  105. package/dist/routes/auth/client-ip.d.ts +1 -0
  106. package/dist/routes/auth/client-ip.js +18 -0
  107. package/dist/routes/auth/client-ip.js.map +1 -1
  108. package/dist/routes/auth/oauth/config.d.ts +2 -1
  109. package/dist/routes/auth/oauth/config.js +5 -1
  110. package/dist/routes/auth/oauth/config.js.map +1 -1
  111. package/dist/routes/auth/session-handoff.d.ts +0 -1
  112. package/dist/routes/auth/session-handoff.js +0 -2
  113. package/dist/routes/auth/session-handoff.js.map +1 -1
  114. package/dist/routes/auth.js +3 -2
  115. package/dist/routes/auth.js.map +1 -1
  116. package/dist/routes/index.d.ts +0 -1
  117. package/dist/routes/index.js +0 -1
  118. package/dist/routes/index.js.map +1 -1
  119. package/dist/routes/realtime.d.ts +13 -1
  120. package/dist/routes/realtime.js +114 -29
  121. package/dist/routes/realtime.js.map +1 -1
  122. package/dist/startup-config.d.ts +2 -0
  123. package/dist/startup-config.js +62 -0
  124. package/dist/startup-config.js.map +1 -0
  125. package/dist/store/interfaces.d.ts +3 -2
  126. package/dist/store/interfaces.js +16 -6
  127. package/dist/store/interfaces.js.map +1 -1
  128. package/dist/store/listing.js +25 -15
  129. package/dist/store/listing.js.map +1 -1
  130. package/dist/store/operations.js +5 -2
  131. package/dist/store/operations.js.map +1 -1
  132. package/dist/store/requirements.js +3 -2
  133. package/dist/store/requirements.js.map +1 -1
  134. package/dist/store/runtime.d.ts +10 -1
  135. package/dist/store/runtime.js +33 -6
  136. package/dist/store/runtime.js.map +1 -1
  137. package/dist/store/types.d.ts +10 -0
  138. package/dist/utils/ensure-default-tenant.d.ts +2 -1
  139. package/dist/utils/ensure-default-tenant.js +13 -1
  140. package/dist/utils/ensure-default-tenant.js.map +1 -1
  141. package/dist/utils/export-jobs.js +2 -1
  142. package/dist/utils/export-jobs.js.map +1 -1
  143. package/dist/validation/auth.schema.d.ts +2 -2
  144. package/dist/validation/member-invite.schema.d.ts +2 -10
  145. package/dist/validation/member-invite.schema.js +2 -15
  146. package/dist/validation/member-invite.schema.js.map +1 -1
  147. package/frontend-vue/dms.frontend.ts +24 -5
  148. package/frontend-vue/dms.server.ts +17 -0
  149. package/frontend-vue/layers/dms-auth/server/accounts.ts +3 -4
  150. package/frontend-vue/layers/dms-core/app/build/composables/renderNow.ts +33 -0
  151. package/frontend-vue/layers/dms-core/app/build/composables/user/tablePreferences.ts +5 -1
  152. package/frontend-vue/layers/dms-core/app/build/utils/crossTab.ts +199 -0
  153. package/frontend-vue/layers/dms-core/app/build/utils/sharedRealtimeConnection.ts +410 -0
  154. package/frontend-vue/layers/dms-core/app/composables/auth/useApiData.ts +77 -12
  155. package/frontend-vue/layers/dms-core/app/composables/data-types/useDataType.ts +26 -0
  156. package/frontend-vue/layers/dms-core/app/composables/live/livePauseSource.ts +29 -0
  157. package/frontend-vue/layers/dms-core/app/composables/period/compareResolvers.ts +150 -9
  158. package/frontend-vue/layers/dms-core/app/composables/period/usePeriod.ts +6 -4
  159. package/frontend-vue/layers/dms-core/app/composables/period/usePeriodScope.ts +27 -1
  160. package/frontend-vue/layers/dms-core/app/composables/realtime/useUserRealtime.ts +256 -119
  161. package/frontend-vue/layers/dms-core/app/composables/translation/useComposedText.ts +21 -8
  162. package/frontend-vue/layers/dms-core/app/composables/translation/useTranslation.ts +25 -5
  163. package/frontend-vue/layers/dms-core/app/composables/useApiError.ts +3 -5
  164. package/frontend-vue/layers/dms-core/app/composables/useFieldErrors.ts +61 -26
  165. package/frontend-vue/layers/dms-core/app/plugins/dev-error-log.ts +16 -0
  166. package/frontend-vue/layers/dms-core/app/types/user.ts +6 -0
  167. package/frontend-vue/layers/dms-core/app/utils/composedText.ts +7 -2
  168. package/frontend-vue/layers/dms-core/app/utils/formatter.ts +134 -35
  169. package/frontend-vue/layers/dms-core/app/utils/url-interpolation.ts +18 -1
  170. package/frontend-vue/layers/dms-core/i18n/locales/core-en-GB.json +136 -2
  171. package/frontend-vue/layers/dms-core/i18n/locales/core-fr-FR.json +136 -2
  172. package/frontend-vue/layers/dms-core/shared/types/app-config.ts +0 -2
  173. package/frontend-vue/layers/dms-layout/app/app.config.ts +0 -1
  174. package/frontend-vue/layers/dms-layout/app/assets/css/main.css +11 -1
  175. package/frontend-vue/layers/dms-layout/app/assets/fonts/Geist-Italic-Variable.woff2 +0 -0
  176. package/frontend-vue/layers/dms-layout/app/assets/fonts/Geist-Variable.woff2 +0 -0
  177. package/frontend-vue/layers/dms-layout/app/assets/fonts/GeistMono-Italic-Variable.woff2 +0 -0
  178. package/frontend-vue/layers/dms-layout/app/assets/fonts/GeistMono-Variable.woff2 +0 -0
  179. package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardBanners.vue +3 -2
  180. package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardHeader.vue +41 -21
  181. package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardPageBody.vue +6 -2
  182. package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardSidebar.vue +4 -19
  183. package/frontend-vue/layers/dms-layout/app/build/components/layout/Footer.vue +11 -107
  184. package/frontend-vue/layers/dms-layout/app/build/components/layout/GithubStarPrompt.vue +18 -7
  185. package/frontend-vue/layers/dms-layout/app/build/components/layout/PageHeaderActionsOutlet.vue +4 -0
  186. package/frontend-vue/layers/dms-layout/app/build/components/layout/RowSidePanel.vue +43 -82
  187. package/frontend-vue/layers/dms-layout/app/build/components/layout/SidebarUserMenu.vue +44 -3
  188. package/frontend-vue/layers/dms-layout/app/build/components/layout/UserIdentity.vue +11 -3
  189. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleCatalogTile.vue +132 -35
  190. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleManageMenu.vue +59 -0
  191. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleSourceLabel.vue +25 -0
  192. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleStoreBanners.vue +73 -0
  193. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleStoreOperation.vue +116 -0
  194. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleStoreRequirements.vue +8 -3
  195. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleStoreTile.vue +85 -207
  196. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleTile.vue +10 -3
  197. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleTileSkeleton.vue +68 -0
  198. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/notification/NotificationInboxDisplay.vue +24 -8
  199. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/notification/NotificationInboxItem.vue +8 -5
  200. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/region/RegionTimeZoneInput.vue +1 -0
  201. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/roles/role-permissions.ts +19 -0
  202. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/roles/useRoleEditor.ts +2 -1
  203. package/frontend-vue/layers/dms-layout/app/build/composables/layout/useSidePanelWidth.ts +31 -70
  204. package/frontend-vue/layers/dms-layout/app/build/composables/notification/useNotificationGroupsOpen.ts +11 -1
  205. package/frontend-vue/layers/dms-layout/app/build/utils/dev-reload-stream.ts +81 -0
  206. package/frontend-vue/layers/dms-layout/app/build/utils/github-repository.ts +2 -2
  207. package/frontend-vue/layers/dms-layout/app/build/utils/module-store-catalog.ts +232 -36
  208. package/frontend-vue/layers/dms-layout/app/build/utils/modules-catalog.ts +122 -14
  209. package/frontend-vue/layers/dms-layout/app/composables/layout/useLayoutBanners.ts +32 -3
  210. package/frontend-vue/layers/dms-layout/app/composables/layout/useServedHeaderActions.ts +49 -11
  211. package/frontend-vue/layers/dms-layout/app/composables/page/useModuleHistory.ts +3 -2
  212. package/frontend-vue/layers/dms-layout/app/composables/page/useModuleStore.ts +59 -4
  213. package/frontend-vue/layers/dms-layout/app/composables/page/useModuleStoreActions.ts +194 -0
  214. package/frontend-vue/layers/dms-layout/app/composables/page/usePageComponentPreload.ts +23 -3
  215. package/frontend-vue/layers/dms-layout/app/composables/page/usePageLayout.ts +4 -2
  216. package/frontend-vue/layers/dms-layout/app/composables/page/usePageRecordDeletion.ts +162 -0
  217. package/frontend-vue/layers/dms-layout/app/composables/page/useSiteLayout.ts +6 -1
  218. package/frontend-vue/layers/dms-layout/app/custom-layouts/DefaultLayout.vue +6 -2
  219. package/frontend-vue/layers/dms-layout/app/custom-layouts/SettingsLayout.vue +6 -2
  220. package/frontend-vue/layers/dms-layout/app/custom-pages/modules-store.vue +291 -0
  221. package/frontend-vue/layers/dms-layout/app/custom-pages/modules.vue +87 -474
  222. package/frontend-vue/layers/dms-layout/app/middleware/module-routing.global.ts +3 -0
  223. package/frontend-vue/layers/dms-layout/app/pages/[...slug].vue +13 -22
  224. package/frontend-vue/layers/dms-layout/app/plugins/command-palette-navigation.client.ts +11 -10
  225. package/frontend-vue/layers/dms-layout/app/plugins/command-palette-personal-pages.client.ts +22 -52
  226. package/frontend-vue/layers/dms-layout/app/plugins/command-palette-quick-actions.client.ts +7 -7
  227. package/frontend-vue/layers/dms-layout/app/plugins/command-palette-session.client.ts +11 -10
  228. package/frontend-vue/layers/dms-layout/app/plugins/dms-dev-reload.client.ts +14 -9
  229. package/frontend-vue/layers/dms-layout/app/theme/actions.ts +5 -4
  230. package/frontend-vue/layers/dms-layout/app/theme/form-controls.ts +9 -4
  231. package/frontend-vue/layers/dms-layout/app/theme/surfaces.ts +22 -6
  232. package/frontend-vue/layers/dms-layout/app/types/module-store.ts +9 -0
  233. package/frontend-vue/layers/dms-layout/app/types/page.ts +15 -1
  234. package/frontend-vue/layers/dms-layout/app/utils/account-menu.ts +11 -0
  235. package/frontend-vue/layers/dms-layout/app/utils/breadcrumb.ts +57 -0
  236. package/frontend-vue/layers/dms-layout/app/utils/pageQuery.ts +26 -0
  237. package/frontend-vue/layers/dms-layout/i18n/locales/layout-en-GB.json +45 -18
  238. package/frontend-vue/layers/dms-layout/i18n/locales/layout-fr-FR.json +44 -17
  239. package/frontend-vue/layers/dms-ui/app/build/components/containers/TargetComponentTree.vue +31 -0
  240. package/frontend-vue/layers/dms-ui/app/build/components/containers/constants.ts +25 -0
  241. package/frontend-vue/layers/dms-ui/app/build/components/containers/drawer/DynamicDrawer.vue +244 -15
  242. package/frontend-vue/layers/dms-ui/app/build/components/containers/modal/DynamicModal.vue +42 -25
  243. package/frontend-vue/layers/dms-ui/app/build/components/containers/row/RowHeader.vue +153 -0
  244. package/frontend-vue/layers/dms-ui/app/build/components/containers/row/RowNotFound.vue +44 -0
  245. package/frontend-vue/layers/dms-ui/app/build/components/containers/row/drawerRowHeader.ts +35 -0
  246. package/frontend-vue/layers/dms-ui/app/build/components/form/FormEntries.vue +44 -9
  247. package/frontend-vue/layers/dms-ui/app/build/components/form/FormErrorSummary.vue +11 -4
  248. package/frontend-vue/layers/dms-ui/app/build/components/form/FormFieldControl.vue +38 -5
  249. package/frontend-vue/layers/dms-ui/app/build/components/form/FormFieldNotes.vue +29 -5
  250. package/frontend-vue/layers/dms-ui/app/build/components/form/FormLoadError.vue +5 -1
  251. package/frontend-vue/layers/dms-ui/app/build/components/form/FormSectionNav.vue +3 -1
  252. package/frontend-vue/layers/dms-ui/app/build/components/form/FormSections.vue +106 -27
  253. package/frontend-vue/layers/dms-ui/app/build/components/table/Actions.vue +12 -4
  254. package/frontend-vue/layers/dms-ui/app/build/components/table/Empty.vue +4 -2
  255. package/frontend-vue/layers/dms-ui/app/build/components/table/FiltersRow.vue +48 -1
  256. package/frontend-vue/layers/dms-ui/app/build/components/table/Pagination.vue +55 -54
  257. package/frontend-vue/layers/dms-ui/app/build/components/table/RowSelection.vue +2 -1
  258. package/frontend-vue/layers/dms-ui/app/build/components/table/Table.vue +396 -288
  259. package/frontend-vue/layers/dms-ui/app/build/components/table/Tabs.vue +2 -1
  260. package/frontend-vue/layers/dms-ui/app/build/components/table/Views.vue +2 -1
  261. package/frontend-vue/layers/dms-ui/app/build/components/table-view/CardAvatar.vue +59 -0
  262. package/frontend-vue/layers/dms-ui/app/build/components/table-view/CardGridSkeleton.vue +19 -2
  263. package/frontend-vue/layers/dms-ui/app/build/components/table-view/CardRowActions.vue +38 -0
  264. package/frontend-vue/layers/dms-ui/app/build/components/table-view/CardsDisplay.vue +148 -104
  265. package/frontend-vue/layers/dms-ui/app/build/components/table-view/LiveHoldBanner.vue +17 -2
  266. package/frontend-vue/layers/dms-ui/app/build/composables/actions/bulkSelection.ts +43 -9
  267. package/frontend-vue/layers/dms-ui/app/build/composables/actions/rowComponentOptions.ts +76 -0
  268. package/frontend-vue/layers/dms-ui/app/build/composables/actions/rowNavigation.ts +172 -27
  269. package/frontend-vue/layers/dms-ui/app/build/composables/actions/useActionTargets.ts +270 -46
  270. package/frontend-vue/layers/dms-ui/app/build/composables/confirm/actionError.ts +6 -1
  271. package/frontend-vue/layers/dms-ui/app/build/composables/confirm/confirmDialogTexts.ts +2 -0
  272. package/frontend-vue/layers/dms-ui/app/build/composables/containers/closingLayers.ts +186 -0
  273. package/frontend-vue/layers/dms-ui/app/build/composables/containers/context.ts +14 -0
  274. package/frontend-vue/layers/dms-ui/app/build/composables/containers/drawerSize.ts +229 -0
  275. package/frontend-vue/layers/dms-ui/app/build/composables/containers/useResizableSize.ts +175 -0
  276. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/metricCells.ts +18 -35
  277. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/numberSuffix.ts +44 -0
  278. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/registerDefaults.ts +123 -27
  279. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/registerFieldTypes.ts +2 -1
  280. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/relationFilter.ts +75 -0
  281. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/relationRows.ts +92 -0
  282. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/relationValue.ts +39 -0
  283. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/twoLineCell.ts +3 -3
  284. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/useColumnValueRenderer.ts +6 -2
  285. package/frontend-vue/layers/dms-ui/app/build/composables/form/createFormFields.ts +15 -0
  286. package/frontend-vue/layers/dms-ui/app/build/composables/form/fieldSaved.ts +40 -0
  287. package/frontend-vue/layers/dms-ui/app/build/composables/form/fieldValueRule.ts +62 -0
  288. package/frontend-vue/layers/dms-ui/app/build/composables/form/formEntryContext.ts +5 -0
  289. package/frontend-vue/layers/dms-ui/app/build/composables/form/formFooter.ts +42 -7
  290. package/frontend-vue/layers/dms-ui/app/build/composables/form/formLayout.ts +106 -4
  291. package/frontend-vue/layers/dms-ui/app/build/composables/form/formRecordLoad.ts +11 -5
  292. package/frontend-vue/layers/dms-ui/app/build/composables/form/formSections.ts +32 -0
  293. package/frontend-vue/layers/dms-ui/app/build/composables/form/formValidation.ts +36 -4
  294. package/frontend-vue/layers/dms-ui/app/build/composables/form/readonlyValue.ts +19 -0
  295. package/frontend-vue/layers/dms-ui/app/build/composables/form/registerFormFunctions.ts +27 -0
  296. package/frontend-vue/layers/dms-ui/app/build/composables/form/repeaterTable.ts +78 -0
  297. package/frontend-vue/layers/dms-ui/app/build/composables/form/serverFieldErrors.ts +49 -0
  298. package/frontend-vue/layers/dms-ui/app/build/composables/form/toasterClearance.ts +118 -0
  299. package/frontend-vue/layers/dms-ui/app/build/composables/form/useInstantForm.ts +12 -1
  300. package/frontend-vue/layers/dms-ui/app/build/composables/live/currentPageLive.ts +41 -3
  301. package/frontend-vue/layers/dms-ui/app/build/composables/live/livePoller.ts +10 -2
  302. package/frontend-vue/layers/dms-ui/app/build/composables/live/pageReadClock.ts +28 -0
  303. package/frontend-vue/layers/dms-ui/app/build/composables/live/shortcuts.ts +1 -1
  304. package/frontend-vue/layers/dms-ui/app/build/composables/live/usePageLiveHost.ts +17 -3
  305. package/frontend-vue/layers/dms-ui/app/build/composables/live/useUpdatedLabel.ts +38 -0
  306. package/frontend-vue/layers/dms-ui/app/build/composables/page-toolbar/pageToolbarContext.ts +75 -0
  307. package/frontend-vue/layers/dms-ui/app/build/composables/page-toolbar/toolbarControlHeight.ts +29 -0
  308. package/frontend-vue/layers/dms-ui/app/build/composables/period/periodComparison.ts +52 -0
  309. package/frontend-vue/layers/dms-ui/app/build/composables/period/periodDisplay.ts +21 -0
  310. package/frontend-vue/layers/dms-ui/app/build/composables/period/periodShortLabels.ts +28 -0
  311. package/frontend-vue/layers/dms-ui/app/build/composables/period/usePeriodDraft.ts +1 -0
  312. package/frontend-vue/layers/dms-ui/app/build/composables/table/useRelationFilterLabels.ts +97 -0
  313. package/frontend-vue/layers/dms-ui/app/build/composables/table/useRowScrollAnchor.ts +106 -0
  314. package/frontend-vue/layers/dms-ui/app/build/composables/table/useTable.ts +9 -0
  315. package/frontend-vue/layers/dms-ui/app/build/composables/table/useTableColumns.ts +1 -2
  316. package/frontend-vue/layers/dms-ui/app/build/composables/table/utils/archivedCount.ts +31 -0
  317. package/frontend-vue/layers/dms-ui/app/build/composables/table/utils/filterEditorValue.ts +38 -0
  318. package/frontend-vue/layers/dms-ui/app/build/composables/table/utils/formatFilterValue.ts +33 -2
  319. package/frontend-vue/layers/dms-ui/app/build/composables/table/utils/scrollEdges.ts +31 -0
  320. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useCardFields.ts +33 -0
  321. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useMobileDisplay.ts +48 -0
  322. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useScrolledPastTop.ts +41 -18
  323. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableFooter.ts +3 -2
  324. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableLinkedVisit.ts +78 -0
  325. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableLiveHold.ts +135 -22
  326. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableRows.ts +56 -25
  327. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViewRowActions.ts +90 -35
  328. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViews.ts +86 -10
  329. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/bulkActions.ts +14 -4
  330. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/card.ts +127 -2
  331. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/compareValues.ts +13 -0
  332. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/expandedRows.ts +28 -0
  333. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/groupedRows.ts +114 -1
  334. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/linkedArrival.ts +308 -0
  335. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/locateRow.ts +106 -0
  336. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/pageArrival.ts +34 -0
  337. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/pinnedRows.ts +17 -0
  338. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/quickFilters.ts +41 -33
  339. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/recordLink.ts +24 -8
  340. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/tableCounts.ts +113 -0
  341. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/tableQuery.ts +27 -4
  342. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/views.ts +22 -6
  343. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/writeError.ts +21 -0
  344. package/frontend-vue/layers/dms-ui/app/build/utils/addressSuggestions.ts +49 -0
  345. package/frontend-vue/layers/dms-ui/app/build/utils/codeHighlight.ts +67 -29
  346. package/frontend-vue/layers/dms-ui/app/build/utils/eyebrow.ts +12 -0
  347. package/frontend-vue/layers/dms-ui/app/build/utils/fieldOptionsI18n.ts +51 -11
  348. package/frontend-vue/layers/dms-ui/app/build/utils/htmlGrammar.ts +79 -0
  349. package/frontend-vue/layers/dms-ui/app/build/utils/inputWidth.ts +53 -0
  350. package/frontend-vue/layers/dms-ui/app/build/utils/numberAdjustment.ts +6 -3
  351. package/frontend-vue/layers/dms-ui/app/build/utils/relativeDate.ts +19 -5
  352. package/frontend-vue/layers/dms-ui/app/build/utils/scrollWithin.ts +99 -0
  353. package/frontend-vue/layers/dms-ui/app/build/utils/shellParser.ts +1 -1
  354. package/frontend-vue/layers/dms-ui/app/build/utils/streamHighlight.ts +90 -0
  355. package/frontend-vue/layers/dms-ui/app/build/utils/urlVariables.ts +36 -14
  356. package/frontend-vue/layers/dms-ui/app/components/PasswordStrength.vue +1 -1
  357. package/frontend-vue/layers/dms-ui/app/components/RecursiveComponent.vue +9 -1
  358. package/frontend-vue/layers/dms-ui/app/components/blocks/LiveToggleBlock.vue +80 -32
  359. package/frontend-vue/layers/dms-ui/app/components/blocks/PageToolbarBlock.vue +137 -0
  360. package/frontend-vue/layers/dms-ui/app/components/blocks/StatGroupBlock.vue +5 -0
  361. package/frontend-vue/layers/dms-ui/app/components/chart/ApexChartHost.vue +16 -2
  362. package/frontend-vue/layers/dms-ui/app/components/chart/Chart.vue +24 -5
  363. package/frontend-vue/layers/dms-ui/app/components/chart/ChartCard.vue +41 -23
  364. package/frontend-vue/layers/dms-ui/app/components/code-snippet/CodeSnippet.vue +23 -12
  365. package/frontend-vue/layers/dms-ui/app/components/confirm/ConfirmModal.vue +1 -1
  366. package/frontend-vue/layers/dms-ui/app/components/container-footer/ContainerFooter.vue +34 -0
  367. package/frontend-vue/layers/dms-ui/app/components/field-row/FieldRow.vue +25 -2
  368. package/frontend-vue/layers/dms-ui/app/components/flow-canvas/FlowCanvas.vue +2 -2
  369. package/frontend-vue/layers/dms-ui/app/components/form/Form.vue +119 -36
  370. package/frontend-vue/layers/dms-ui/app/components/form/components/Cascader.vue +53 -22
  371. package/frontend-vue/layers/dms-ui/app/components/form/components/DatePicker.vue +20 -1
  372. package/frontend-vue/layers/dms-ui/app/components/form/components/Display.vue +13 -1
  373. package/frontend-vue/layers/dms-ui/app/components/form/components/Image.vue +8 -3
  374. package/frontend-vue/layers/dms-ui/app/components/form/components/InputAddress.vue +53 -5
  375. package/frontend-vue/layers/dms-ui/app/components/form/components/InputCode.vue +29 -5
  376. package/frontend-vue/layers/dms-ui/app/components/form/components/InputNumber.vue +45 -8
  377. package/frontend-vue/layers/dms-ui/app/components/form/components/InputPercentage.vue +28 -5
  378. package/frontend-vue/layers/dms-ui/app/components/form/components/InputSecret.vue +5 -0
  379. package/frontend-vue/layers/dms-ui/app/components/form/components/InputText.vue +19 -2
  380. package/frontend-vue/layers/dms-ui/app/components/form/components/InputTime.vue +2 -1
  381. package/frontend-vue/layers/dms-ui/app/components/form/components/Relation.vue +226 -65
  382. package/frontend-vue/layers/dms-ui/app/components/form/components/Repeater.vue +80 -30
  383. package/frontend-vue/layers/dms-ui/app/components/form/components/RepeaterTable.vue +210 -0
  384. package/frontend-vue/layers/dms-ui/app/components/form/components/Select.vue +4 -2
  385. package/frontend-vue/layers/dms-ui/app/components/grid/GridRow.vue +11 -2
  386. package/frontend-vue/layers/dms-ui/app/components/key-value-list/KeyValueList.vue +79 -70
  387. package/frontend-vue/layers/dms-ui/app/components/kpi/KpiCard.vue +117 -21
  388. package/frontend-vue/layers/dms-ui/app/components/meter/Meter.vue +2 -1
  389. package/frontend-vue/layers/dms-ui/app/components/period/PeriodSelector.vue +113 -34
  390. package/frontend-vue/layers/dms-ui/app/components/save-bar/SaveBar.vue +14 -2
  391. package/frontend-vue/layers/dms-ui/app/components/section-header/Eyebrow.vue +6 -1
  392. package/frontend-vue/layers/dms-ui/app/components/segmented/Segmented.vue +13 -0
  393. package/frontend-vue/layers/dms-ui/app/components/stat-group/StatGroup.vue +52 -3
  394. package/frontend-vue/layers/dms-ui/app/components/tab/Tab.vue +2 -1
  395. package/frontend-vue/layers/dms-ui/app/components/table-view/CascaderPath.vue +74 -25
  396. package/frontend-vue/layers/dms-ui/app/components/table-view/KanbanBoard.vue +48 -44
  397. package/frontend-vue/layers/dms-ui/app/components/table-view/KanbanDisplay.vue +4 -1
  398. package/frontend-vue/layers/dms-ui/app/components/table-view/TableView.vue +426 -97
  399. package/frontend-vue/layers/dms-ui/app/components/top-list/TopListCard.vue +33 -5
  400. package/frontend-vue/layers/dms-ui/app/composables/chart/apexCategoryLabels.ts +68 -0
  401. package/frontend-vue/layers/dms-ui/app/composables/chart/apexDatetimeBuckets.ts +306 -0
  402. package/frontend-vue/layers/dms-ui/app/composables/chart/formatValue.ts +4 -3
  403. package/frontend-vue/layers/dms-ui/app/composables/chart/heatmapColors.ts +93 -0
  404. package/frontend-vue/layers/dms-ui/app/composables/chart/localizeSeries.ts +62 -0
  405. package/frontend-vue/layers/dms-ui/app/composables/chart/useApexChart.ts +116 -8
  406. package/frontend-vue/layers/dms-ui/app/composables/chart/useApexChart.types.ts +7 -0
  407. package/frontend-vue/layers/dms-ui/app/composables/containers/types.ts +84 -1
  408. package/frontend-vue/layers/dms-ui/app/composables/containers/useDrawer.ts +69 -8
  409. package/frontend-vue/layers/dms-ui/app/composables/form/types/field-saved.ts +11 -0
  410. package/frontend-vue/layers/dms-ui/app/composables/form/types/field.ts +16 -0
  411. package/frontend-vue/layers/dms-ui/app/composables/form/types/functions.ts +1 -0
  412. package/frontend-vue/layers/dms-ui/app/composables/form/useForm.ts +189 -28
  413. package/frontend-vue/layers/dms-ui/app/composables/live/usePageLive.ts +29 -12
  414. package/frontend-vue/layers/dms-ui/app/composables/page-toolbar/usePageToolbarSize.ts +30 -0
  415. package/frontend-vue/layers/dms-ui/app/composables/table-view/kanban.ts +4 -2
  416. package/frontend-vue/layers/dms-ui/app/composables/table-view/types/action-target.ts +22 -3
  417. package/frontend-vue/layers/dms-ui/app/composables/table-view/types/config.ts +4 -0
  418. package/frontend-vue/layers/dms-ui/app/composables/table-view/types/display.ts +24 -1
  419. package/frontend-vue/layers/dms-ui/app/plugins/closing-layers.client.ts +10 -0
  420. package/frontend-vue/layers/dms-ui/app/plugins/live-pause-source.ts +15 -0
  421. package/frontend-vue/layers/dms-ui/app/plugins/register.ts +2 -0
  422. package/frontend-vue/layers/dms-ui/app/types/input-number.ts +3 -2
  423. package/frontend-vue/layers/dms-ui/app/types/page-toolbar.ts +7 -0
  424. package/frontend-vue/layers/dms-ui/app/types/row-action.ts +0 -2
  425. package/frontend-vue/layers/dms-ui/app/types/row-navigation.ts +17 -0
  426. package/frontend-vue/layers/dms-ui/app/types/table/query.ts +2 -1
  427. package/frontend-vue/layers/dms-ui/app/utils/blockRefresh.ts +3 -1
  428. package/frontend-vue/layers/dms-ui/app/utils/cascader.ts +94 -3
  429. package/frontend-vue/layers/dms-ui/app/utils/componentTree.ts +24 -0
  430. package/frontend-vue/layers/dms-ui/i18n/locales/ui-en-GB.json +20 -5
  431. package/frontend-vue/layers/dms-ui/i18n/locales/ui-fr-FR.json +20 -5
  432. package/frontend-vue/package.json +6 -2
  433. package/package.json +11 -10
  434. package/skills/dms/SKILL.md +1 -1
  435. package/skills/dms-auth/SKILL.md +5 -3
  436. package/skills/dms-best-practices/REFERENCE.md +2 -0
  437. package/skills/dms-best-practices/SKILL.md +4 -0
  438. package/skills/dms-module/SKILL.md +3 -2
  439. package/skills/dms-pages/REFERENCE.md +129 -16
  440. package/skills/dms-pages/SKILL.md +1 -1
  441. package/skills/dms-project/DEV-SERVERS.md +23 -1
  442. package/skills/dms-project/REFERENCE.md +1 -1
  443. package/skills/dms-project/SKILL.md +4 -1
  444. package/skills/dms-project/scripts/dms-dev.sh +205 -17
  445. package/dist/hooks/tenant-instances.d.ts +0 -1
  446. package/dist/hooks/tenant-instances.js +0 -13
  447. package/dist/hooks/tenant-instances.js.map +0 -1
  448. package/dist/routes/startup-gate.d.ts +0 -7
  449. package/dist/routes/startup-gate.js +0 -39
  450. package/dist/routes/startup-gate.js.map +0 -1
  451. package/frontend-vue/layers/dms-core/app/composables/period/periodRenderNow.ts +0 -42
@@ -1,8 +1,17 @@
1
- import { computed, shallowRef, watch, type ComputedRef } from "vue";
1
+ import {
2
+ computed,
3
+ onScopeDispose,
4
+ shallowRef,
5
+ watch,
6
+ type ComputedRef,
7
+ } from "vue";
8
+ import { onPageBlocksRefresh } from "#dms-ui/app/utils/blockRefresh";
2
9
  import type { LayoutHeaderAction } from "../../build/components/layout/PageHeaderActionBar.vue";
3
10
  import type { PageLayout } from "../../types/page";
4
11
  import {
5
12
  isSamePageQuery,
13
+ isSameRouteParams,
14
+ fillRouteParams,
6
15
  layoutUrlWithQuery,
7
16
  pageQueryValues,
8
17
  } from "../../utils/pageQuery";
@@ -11,10 +20,12 @@ interface ServedHeaderActionsOptions {
11
20
  /** The buttons the page layout was served with. */
12
21
  actions: () => LayoutHeaderAction[];
13
22
  /**
14
- * The page query their availability was resolved for, set when one of them
15
- * declares an `availability` (`headerActionsQuery`).
23
+ * The page query they were served for, set when one of them declares an
24
+ * `availability` or the page reads a `record` (`headerActionsQuery`).
16
25
  */
17
26
  servedQuery: () => Record<string, string> | undefined;
27
+ /** The route parameters (`:id`) their availability was resolved for. */
28
+ servedParams?: () => Record<string, string> | undefined;
18
29
  }
19
30
 
20
31
  /**
@@ -22,20 +33,25 @@ interface ServedHeaderActionsOptions {
22
33
  * `availability` depends on the query (`?workspace=` names the record the
23
34
  * page acts on) was resolved for the query the layout was loaded with: when
24
35
  * the URL's query moves away from it, the header asks the page layout again
25
- * for that query and takes its buttons, keeping the last ones meanwhile.
26
- * Buttons without an availability never cause a request.
36
+ * for that query and takes its buttons, keeping the last ones meanwhile. The
37
+ * same goes for the route parameters: the layout of `/projects/project/:id`
38
+ * is cached once for the page, whichever record it was first served for.
39
+ * A write on the page (`refreshPageBlocks()`, after a header button or a
40
+ * form) asks again too: it may have changed the record the buttons read, or
41
+ * what their availability checks. Buttons without an availability, on a page
42
+ * reading no record, never cause a request.
27
43
  */
28
44
  export function useServedHeaderActions({
29
45
  actions,
30
46
  servedQuery,
47
+ servedParams = () => undefined,
31
48
  }: ServedHeaderActionsOptions): ComputedRef<LayoutHeaderAction[]> {
32
49
  const route = useDmsRoute();
33
50
  const siteLayout = useSiteLayout();
34
51
  const refreshed = shallowRef<LayoutHeaderAction[] | null>(null);
35
52
  let latestRequest = 0;
36
53
 
37
- const layoutUrl = () =>
38
- siteLayout.findMatchingRoute(route.path)?.metadata?.layoutUrl;
54
+ const pageRoute = () => siteLayout.findMatchingRoute(route.path);
39
55
 
40
56
  async function askFor(url: string, query: Record<string, string>) {
41
57
  const request = ++latestRequest;
@@ -58,17 +74,39 @@ export function useServedHeaderActions({
58
74
  function follow() {
59
75
  const served = servedQuery();
60
76
  const current = pageQueryValues(route.query as Record<string, unknown>);
61
- const url = layoutUrl();
62
- if (!served || !url || isSamePageQuery(served, current)) {
77
+ const match = pageRoute();
78
+ const layoutUrl = match?.metadata?.layoutUrl;
79
+ const params = match?.params ?? {};
80
+ const paramsServed = servedParams();
81
+ if (
82
+ !served ||
83
+ !layoutUrl ||
84
+ (isSamePageQuery(served, current) &&
85
+ (!paramsServed || isSameRouteParams(paramsServed, params)))
86
+ ) {
63
87
  latestRequest++;
64
88
  refreshed.value = null;
65
89
  return;
66
90
  }
67
- void askFor(url, current);
91
+ void askFor(fillRouteParams(layoutUrl, params), current);
92
+ }
93
+
94
+ function askAgain() {
95
+ const layoutUrl = pageRoute()?.metadata?.layoutUrl;
96
+ if (!servedQuery() || !layoutUrl) return;
97
+ void askFor(
98
+ fillRouteParams(layoutUrl, pageRoute()?.params ?? {}),
99
+ pageQueryValues(route.query as Record<string, unknown>),
100
+ );
68
101
  }
69
102
 
70
103
  if (!import.meta.env.SSR) {
71
- watch([() => route.query, servedQuery], follow, { immediate: true });
104
+ watch(
105
+ [() => route.query, () => route.path, servedQuery, servedParams],
106
+ follow,
107
+ { immediate: true },
108
+ );
109
+ onScopeDispose(onPageBlocksRefresh(askAgain));
72
110
  }
73
111
 
74
112
  return computed(() => refreshed.value ?? actions());
@@ -44,8 +44,9 @@ function normalizeHistory(raw: unknown): StoredHistory {
44
44
 
45
45
  /**
46
46
  * Last page visited in each module, kept in a cookie so it survives reloads
47
- * and the server renders the same catalog links and "recently used" order as
48
- * the browser.
47
+ * and the server renders the same "recently used" order as the browser. It
48
+ * feeds that order and the command palette's recent modules; it never decides
49
+ * where entering a module lands, which is always the module's landing page.
49
50
  */
50
51
  export const useModuleHistory = (): ModuleHistoryApi => {
51
52
  const cookie = useDmsCookie<StoredHistory>(MODULE_HISTORY_COOKIE, {
@@ -16,13 +16,35 @@ const FOLLOW_INTERVAL_MS = 1000;
16
16
  * follow-up that reads the listing again while something moves. An operation
17
17
  * the owner started here reloads the page once the frontend serves its
18
18
  * result, so a new module's pages appear without a manual refresh.
19
+ *
20
+ * The first read comes with the page (`useApiData`): a server render waits
21
+ * for it and hands it over, so the tiles render with their store details and
22
+ * never move once mounted.
19
23
  */
20
24
  export const useModuleStore = () => {
21
25
  // Taken once, in setup: the follow-up reads the listing from a timer,
22
26
  // outside any component context.
23
27
  const { $authFetch } = useAuthFetch();
24
- const listing = ref<StoreListing | undefined>(undefined);
25
- const loadingError = ref<unknown>(null);
28
+ const isOwner = useIsOwner();
29
+ // Platform owners only: a member's read would be refused.
30
+ const initial = useApiData<StoreListing>(() =>
31
+ isOwner.value ? ENDPOINT_STORE_LISTING : null,
32
+ );
33
+ // The follow-up reads write into the same data, so a later visit shows
34
+ // the latest listing at once.
35
+ const listing = computed<StoreListing | undefined>({
36
+ get: () => initial.data.value ?? undefined,
37
+ set: (value) => {
38
+ initial.data.value = value ?? null;
39
+ },
40
+ });
41
+ /** The last follow-up read's failure; `null` once one succeeds. */
42
+ const followError = ref<unknown>(undefined);
43
+ const loadingError = computed<unknown>(() =>
44
+ followError.value === undefined
45
+ ? (initial.error.value ?? null)
46
+ : followError.value,
47
+ );
26
48
  /** Modules an operation from this page changed, until the frontend follows. */
27
49
  const awaitingReload = ref(new Set<string>());
28
50
  let followTimer: ReturnType<typeof setTimeout> | undefined;
@@ -38,9 +60,38 @@ export const useModuleStore = () => {
38
60
  async function load(): Promise<void> {
39
61
  try {
40
62
  listing.value = await $authFetch<StoreListing>(ENDPOINT_STORE_LISTING);
41
- loadingError.value = null;
63
+ followError.value = null;
42
64
  } catch (error) {
43
- loadingError.value = error;
65
+ followError.value = error;
66
+ }
67
+ reloadWhenServed();
68
+ scheduleFollow();
69
+ }
70
+
71
+ let isStopped = false;
72
+ let stopWaiting: (() => void) | undefined;
73
+
74
+ /**
75
+ * Starts following once mounted: reads again only when the first read
76
+ * failed, or something moves; a listing already on screen stays as is.
77
+ * A read still in flight (a client navigation, or the background read of a
78
+ * return to the page) is waited for until it settles.
79
+ */
80
+ function start(): void {
81
+ isStopped = false;
82
+ stopWaiting?.();
83
+ if (initial.pending.value) {
84
+ stopWaiting = watch(initial.pending, (pending) => {
85
+ if (pending) return;
86
+ stopWaiting?.();
87
+ stopWaiting = undefined;
88
+ if (!isStopped) start();
89
+ });
90
+ return;
91
+ }
92
+ if (!listing.value) {
93
+ void load();
94
+ return;
44
95
  }
45
96
  reloadWhenServed();
46
97
  scheduleFollow();
@@ -93,6 +144,9 @@ export const useModuleStore = () => {
93
144
  }
94
145
 
95
146
  function stop(): void {
147
+ isStopped = true;
148
+ stopWaiting?.();
149
+ stopWaiting = undefined;
96
150
  if (followTimer) clearTimeout(followTimer);
97
151
  followTimer = undefined;
98
152
  }
@@ -103,6 +157,7 @@ export const useModuleStore = () => {
103
157
  isFollowing,
104
158
  awaitingReload,
105
159
  load,
160
+ start,
106
161
  run,
107
162
  stop,
108
163
  };
@@ -0,0 +1,194 @@
1
+ import type { Ref, VNode } from "vue";
2
+ import ModuleStoreDependencyPicker from "../../build/components/pages/modules/ModuleStoreDependencyPicker.vue";
3
+ import {
4
+ ConfirmActionError,
5
+ useConfirm,
6
+ } from "#dms-ui/app/composables/confirm";
7
+ import {
8
+ type ModuleStoreEntry,
9
+ isAwaitingFrontend,
10
+ needsDependencyChoice,
11
+ } from "../../build/utils/module-store-catalog";
12
+ import {
13
+ type DependencyDraft,
14
+ areDependencyDraftsValid,
15
+ buildInstallBody,
16
+ createDependencyDrafts,
17
+ } from "../../build/utils/module-store-install";
18
+ import type {
19
+ StoreActionBody,
20
+ StoreOperationKind,
21
+ } from "../../types/module-store";
22
+ import type { useModuleStore } from "./useModuleStore";
23
+
24
+ /** What a refused store request carries: the server's error key, if any. */
25
+ interface StoreRequestError {
26
+ data?: unknown;
27
+ message?: string;
28
+ }
29
+
30
+ /** What the frontend banner says while it lags behind the backend. */
31
+ export interface StoreFrontendNotice {
32
+ key: "dev" | "production" | "unknown";
33
+ names: string;
34
+ }
35
+
36
+ type ModuleStore = ReturnType<typeof useModuleStore>;
37
+
38
+ /**
39
+ * The store's actions as both Modules views run them: the confirmations,
40
+ * the install dialog that picks the modules for missing interfaces, and a
41
+ * refusal worded from the server's error key. Also what the frontend banner
42
+ * says while the interface lags behind the backend.
43
+ */
44
+ export const useModuleStoreActions = (
45
+ moduleStore: ModuleStore,
46
+ entries: Ref<ModuleStoreEntry[]>,
47
+ ) => {
48
+ const { t } = useI18n();
49
+ const { processI18n } = useTranslation();
50
+ const toast = useToast();
51
+ const { confirm } = useConfirm();
52
+
53
+ function resolveStoreError(error: unknown): string {
54
+ const details = error as StoreRequestError | undefined;
55
+ const key = typeof details?.data === "string" ? details.data : undefined;
56
+ if (!key) return details?.message ?? "";
57
+ return t(`modules.store.errors.${key.replace("error.", "")}`);
58
+ }
59
+
60
+ /**
61
+ * Runs `kind` from a dialog: a refusal is worded as the toast words it and
62
+ * shown in the dialog, which stays open.
63
+ */
64
+ async function runFromDialog(
65
+ kind: StoreOperationKind,
66
+ entry: ModuleStoreEntry,
67
+ body?: StoreActionBody,
68
+ ): Promise<void> {
69
+ try {
70
+ await moduleStore.run(kind, entry, body);
71
+ } catch (error) {
72
+ throw new ConfirmActionError({
73
+ title: t("modules.store.action_failed", {
74
+ module: processI18n(entry.title),
75
+ }),
76
+ description: resolveStoreError(error),
77
+ });
78
+ }
79
+ }
80
+
81
+ /** What the owner last picked in a module's install dialog, for a retry. */
82
+ const lastDependencyDrafts = new Map<string, DependencyDraft[]>();
83
+
84
+ function renderDependencyPicker(
85
+ entry: ModuleStoreEntry,
86
+ drafts: Ref<DependencyDraft[]>,
87
+ showErrors: boolean,
88
+ ): VNode {
89
+ return h(ModuleStoreDependencyPicker, {
90
+ missing: entry.missingInterfaces,
91
+ drafts: drafts.value,
92
+ showErrors,
93
+ onUpdate: (index: number, patch: Partial<DependencyDraft>) => {
94
+ drafts.value = drafts.value.map((draft, position) =>
95
+ position === index ? { ...draft, ...patch } : draft,
96
+ );
97
+ },
98
+ });
99
+ }
100
+
101
+ /**
102
+ * Asks which modules implement the interfaces the release misses, then
103
+ * installs them with it. A refusal shows in the dialog, kept open.
104
+ */
105
+ async function installWithDependencies(
106
+ entry: ModuleStoreEntry,
107
+ ): Promise<void> {
108
+ const title = processI18n(entry.title);
109
+ const drafts = ref(
110
+ createDependencyDrafts(
111
+ entry.missingInterfaces,
112
+ lastDependencyDrafts.get(entry.id),
113
+ ),
114
+ );
115
+ const showErrors = ref(false);
116
+ await confirm({
117
+ title: t("modules.store.install_dialog.title", { module: title }),
118
+ description: t("modules.store.install_dialog.description", {
119
+ module: title,
120
+ }),
121
+ confirmLabel: t("modules.store.actions.install"),
122
+ confirmIcon: "i-ph-download-simple",
123
+ icon: "i-ph-puzzle-piece",
124
+ body: () => renderDependencyPicker(entry, drafts, showErrors.value),
125
+ validate: () => {
126
+ showErrors.value = true;
127
+ return areDependencyDraftsValid(drafts.value);
128
+ },
129
+ onConfirm: async () => {
130
+ lastDependencyDrafts.set(entry.id, drafts.value);
131
+ await runFromDialog(
132
+ "install",
133
+ entry,
134
+ buildInstallBody(entry.id, drafts.value),
135
+ );
136
+ },
137
+ });
138
+ }
139
+
140
+ async function runStoreAction(
141
+ kind: StoreOperationKind,
142
+ entry: ModuleStoreEntry,
143
+ ): Promise<void> {
144
+ const title = processI18n(entry.title);
145
+ if (needsDependencyChoice(entry, kind)) {
146
+ await installWithDependencies(entry);
147
+ return;
148
+ }
149
+ if (kind === "uninstall") {
150
+ await confirm({
151
+ title: t("modules.store.uninstall_confirm.title", { module: title }),
152
+ description: t("modules.store.uninstall_confirm.description", {
153
+ module: title,
154
+ }),
155
+ confirmLabel: t("modules.store.actions.uninstall"),
156
+ color: "error",
157
+ icon: "i-ph-trash",
158
+ onConfirm: () => runFromDialog(kind, entry),
159
+ });
160
+ return;
161
+ }
162
+ try {
163
+ await moduleStore.run(kind, entry);
164
+ } catch (error) {
165
+ toast.add({
166
+ color: "error",
167
+ icon: "i-ph-warning-circle",
168
+ title: t("modules.store.action_failed", { module: title }),
169
+ description: resolveStoreError(error),
170
+ });
171
+ }
172
+ }
173
+
174
+ /** Store modules whose interface the running frontend does not match yet. */
175
+ const frontendNotice = computed<StoreFrontendNotice | null>(() => {
176
+ const lagging = entries.value.filter(
177
+ (entry) =>
178
+ isAwaitingFrontend(entry) ||
179
+ (entry.frontend === "unknown" &&
180
+ moduleStore.awaitingReload.value.has(entry.id)),
181
+ );
182
+ if (lagging.length === 0) return null;
183
+ const names = lagging.map((entry) => processI18n(entry.title)).join(", ");
184
+ const hasReported = lagging.every((entry) => entry.frontend !== "unknown");
185
+ const key = !hasReported
186
+ ? "unknown"
187
+ : moduleStore.listing.value?.dev
188
+ ? "dev"
189
+ : "production";
190
+ return { key, names };
191
+ });
192
+
193
+ return { runStoreAction, frontendNotice };
194
+ };
@@ -49,16 +49,36 @@ function loadPageLayoutComponents(
49
49
  );
50
50
  }
51
51
 
52
+ // How long a navigation waits for the chunks of the page's blocks: on any
53
+ // usable network they land well within it, and one that hangs does not hold
54
+ // the page for long (its block then loads as it renders).
55
+ export const PAGE_COMPONENTS_WAIT_MS = 3_000;
56
+
52
57
  /**
53
58
  * Start fetching the chunks of every component a page layout references,
54
59
  * in parallel, instead of letting each <Component :is> trigger its own
55
60
  * fetch when it mounts (which waterfalls for nested children).
56
61
  * No-op on the server: SSR resolves async components during render.
62
+ *
63
+ * Settles once they are all in, or failed, or after
64
+ * `PAGE_COMPONENTS_WAIT_MS`: the page's setup awaits it, so a client
65
+ * navigation shows the page with its blocks' code there. A lazy block
66
+ * renders nothing until its chunk lands, then pushes the page down and
67
+ * draws its skeleton again.
57
68
  */
58
69
  export function preloadPageLayoutComponents(
59
70
  layout: PageLayout | null | undefined,
60
- ): void {
61
- if (import.meta.env.SSR) return;
71
+ ): Promise<void> {
72
+ if (import.meta.env.SSR) return Promise.resolve();
62
73
 
63
- loadPageLayoutComponents(layout).catch(() => {});
74
+ const loaded = loadPageLayoutComponents(layout).then(
75
+ () => {},
76
+ () => {},
77
+ );
78
+ return Promise.race([
79
+ loaded,
80
+ new Promise<void>((resolve) =>
81
+ setTimeout(resolve, PAGE_COMPONENTS_WAIT_MS),
82
+ ),
83
+ ]);
64
84
  }
@@ -1,4 +1,4 @@
1
- import { pageQueryValues } from "../../utils/pageQuery";
1
+ import { fillRouteParams, pageQueryValues } from "../../utils/pageQuery";
2
2
 
3
3
  /**
4
4
  * Reads the same page cache that Inertia hydrates before navigation. Missing
@@ -8,8 +8,9 @@ export async function usePageLayout() {
8
8
  const route = useDmsRoute();
9
9
  const siteLayout = useSiteLayout();
10
10
 
11
+ const match = computed(() => siteLayout.findMatchingRoute(route.path));
11
12
  const layoutUrl = computed(() => {
12
- const metadata = siteLayout.findMatchingRoute(route.path)?.metadata;
13
+ const metadata = match.value?.metadata;
13
14
  return metadata?.hasAccess === false ? undefined : metadata?.layoutUrl;
14
15
  });
15
16
  const pageLayout = computed(() =>
@@ -24,6 +25,7 @@ export async function usePageLayout() {
24
25
  await siteLayout.loadPageLayout(
25
26
  layoutUrl.value,
26
27
  pageQueryValues(route.query),
28
+ fillRouteParams(layoutUrl.value, match.value?.params ?? {}),
27
29
  );
28
30
  }
29
31
  };
@@ -0,0 +1,162 @@
1
+ import { onScopeDispose, type Ref, watch } from "vue";
2
+ import type { RealtimeTopicHandler } from "#dms-core/app/composables/realtime/useUserRealtime";
3
+ import type { PageLayout } from "../../types/page";
4
+ import {
5
+ fillRouteParams,
6
+ isSamePageQuery,
7
+ isSameRouteParams,
8
+ isViewStateKey,
9
+ layoutUrlWithQuery,
10
+ pageQueryValues,
11
+ } from "../../utils/pageQuery";
12
+
13
+ // One frame: the events of one burst (a bulk delete) read the record once.
14
+ const BURST_WINDOW_MS = 16;
15
+
16
+ /** How the server read the record a page shows (`DefaultLayout({ record })`). */
17
+ type PageRecordStatus = "found" | "missing" | "error";
18
+
19
+ interface RecordLayoutOptions {
20
+ recordStatus?: PageRecordStatus;
21
+ headerActionsQuery?: Record<string, string>;
22
+ headerActionsParams?: Record<string, string>;
23
+ }
24
+
25
+ /** What the page's record deletion listens to. */
26
+ export interface PageRecordDeletionSource {
27
+ /** The page's layout as it was first served. */
28
+ pageLayout: () => PageLayout | null;
29
+ /** The page's realtime topics, once its session is subscribed to it. */
30
+ pageTopics: Ref<string[]>;
31
+ subscribe(topic: string, handler: RealtimeTopicHandler): () => void;
32
+ }
33
+
34
+ const optionsOf = (layout: PageLayout | null | undefined) =>
35
+ layout?.layout.options as RecordLayoutOptions | undefined;
36
+
37
+ /**
38
+ * Leaves a detail page whose record was deleted, in every tab and for every
39
+ * member on it. The layout of a page reading a record (`DefaultLayout({
40
+ * record })`) says how the server read it (`recordStatus`); on each event of
41
+ * one of the page's realtime topics, the page asks its layout again, and a
42
+ * record it had `found` that is now `missing` was deleted: a toast says so,
43
+ * and the page opens its list (`MenuOptions.parent`), or stays on its
44
+ * not-found blocks without one. A page reading no record never asks.
45
+ */
46
+ export function usePageRecordDeletion(source: PageRecordDeletionSource): void {
47
+ if (import.meta.env.SSR) return;
48
+ const route = useDmsRoute();
49
+ const siteLayout = useSiteLayout();
50
+ const toast = useToast();
51
+ const { t } = useI18n();
52
+
53
+ const pageRoute = () => siteLayout.findMatchingRoute(route.path);
54
+ const currentQuery = () =>
55
+ pageQueryValues(route.query as Record<string, unknown>);
56
+ /** The page URL the record is named by, leaving out the view's state. */
57
+ const recordKey = () =>
58
+ JSON.stringify([
59
+ route.path,
60
+ Object.entries(currentQuery()).filter(([name]) => !isViewStateKey(name)),
61
+ ]);
62
+
63
+ let known: { key: string; status: PageRecordStatus } | undefined;
64
+ let latestRead = 0;
65
+
66
+ async function readStatus(): Promise<PageRecordStatus | undefined> {
67
+ const match = pageRoute();
68
+ const layoutUrl = match?.metadata?.layoutUrl;
69
+ if (!layoutUrl) return undefined;
70
+ const { $authFetch } = useAuthFetch();
71
+ try {
72
+ const layout = await $authFetch<PageLayout>(
73
+ layoutUrlWithQuery(
74
+ fillRouteParams(layoutUrl, match.params ?? {}),
75
+ currentQuery(),
76
+ ),
77
+ );
78
+ return optionsOf(layout)?.recordStatus;
79
+ } catch {
80
+ // Unknown: the record is not taken for deleted on a failed read.
81
+ return undefined;
82
+ }
83
+ }
84
+
85
+ async function leave(): Promise<void> {
86
+ toast.add({
87
+ title: t("page.record_deleted.title"),
88
+ description: t("page.record_deleted.description"),
89
+ color: "warning",
90
+ icon: "i-ph-trash",
91
+ });
92
+ const match = pageRoute();
93
+ const parentSlug = match?.metadata?.parentSlug;
94
+ if (!parentSlug) return;
95
+ const path = fillRouteParams(parentSlug, match.params ?? {});
96
+ if (path.includes(":")) return;
97
+ const parent = siteLayout.findMatchingRoute(path)?.metadata;
98
+ if (!parent || parent.hasAccess === false) return;
99
+ await navigateDms(path, { replace: true });
100
+ }
101
+
102
+ async function check(): Promise<void> {
103
+ const key = recordKey();
104
+ const read = ++latestRead;
105
+ const status = await readStatus();
106
+ if (read !== latestRead || key !== recordKey() || !status) return;
107
+ const previous = known?.key === key ? known.status : undefined;
108
+ known = { key, status };
109
+ if (previous === "found" && status === "missing") await leave();
110
+ }
111
+
112
+ const declaresRecord = () =>
113
+ optionsOf(source.pageLayout())?.recordStatus !== undefined;
114
+
115
+ // What the page was served with stands for its URL when it was served for
116
+ // it; another record of the same page is read once to know where it stands.
117
+ function follow(): void {
118
+ if (known?.key === recordKey()) return;
119
+ latestRead++;
120
+ known = undefined;
121
+ const options = optionsOf(source.pageLayout());
122
+ if (!options?.recordStatus) return;
123
+ const served =
124
+ isSamePageQuery(options.headerActionsQuery ?? {}, currentQuery()) &&
125
+ isSameRouteParams(
126
+ options.headerActionsParams ?? {},
127
+ pageRoute()?.params ?? {},
128
+ );
129
+ if (served) known = { key: recordKey(), status: options.recordStatus };
130
+ else void check();
131
+ }
132
+
133
+ let timer: ReturnType<typeof setTimeout> | undefined;
134
+ // Presence snapshots ride the same topics: they carry no `type`.
135
+ const onEvent: RealtimeTopicHandler = (event) => {
136
+ if (!("type" in event) || !declaresRecord()) return;
137
+ clearTimeout(timer);
138
+ timer = setTimeout(() => void check(), BURST_WINDOW_MS);
139
+ };
140
+
141
+ let unsubscribes: Array<() => void> = [];
142
+ const unsubscribeAll = () => {
143
+ for (const unsubscribe of unsubscribes) unsubscribe();
144
+ unsubscribes = [];
145
+ };
146
+ watch(
147
+ source.pageTopics,
148
+ (topics) => {
149
+ unsubscribeAll();
150
+ unsubscribes = topics.map((topic) => source.subscribe(topic, onEvent));
151
+ },
152
+ { immediate: true },
153
+ );
154
+ watch([() => route.path, () => route.query, source.pageLayout], follow, {
155
+ immediate: true,
156
+ });
157
+ onScopeDispose(() => {
158
+ unsubscribeAll();
159
+ clearTimeout(timer);
160
+ latestRead++;
161
+ });
162
+ }
@@ -331,9 +331,14 @@ export const useSiteLayout = () => {
331
331
  * URL's, which the first request carries for the header buttons whose
332
332
  * availability reads it (the header asks again for another query).
333
333
  */
334
+ /**
335
+ * The layout of the page at `slug`, cached under it. `url`, the layout
336
+ * route with the page's `:id` filled in, is what is asked when it differs.
337
+ */
334
338
  const loadPageLayout = async (
335
339
  slug: string,
336
340
  query?: Record<string, string>,
341
+ url = slug,
337
342
  ): Promise<PageLayout> => {
338
343
  if (pageLayouts.value[slug]) {
339
344
  return pageLayouts.value[slug];
@@ -347,7 +352,7 @@ export const useSiteLayout = () => {
347
352
 
348
353
  const { $authFetch } = useAuthFetch();
349
354
  const request = $authFetch<PageLayout>(
350
- layoutUrlWithQuery(slug, query ?? {}),
355
+ layoutUrlWithQuery(url, query ?? {}),
351
356
  ).then((result) => {
352
357
  if (pageLayouts.value === cache) cache[slug] = result;
353
358
  return result;