@antelopejs/dms 0.9.8 → 0.10.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 (420) hide show
  1. package/CHANGELOG.md +102 -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 -0
  21. package/dist/config.js +5 -3
  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 +22 -89
  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 +1 -2
  41. package/dist/implementations/dms/manifest.js +2 -0
  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 +28 -48
  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 +24 -13
  129. package/dist/store/listing.js.map +1 -1
  130. package/dist/store/operations.js +4 -0
  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/validation/auth.schema.d.ts +2 -2
  139. package/dist/validation/member-invite.schema.d.ts +2 -10
  140. package/dist/validation/member-invite.schema.js +2 -15
  141. package/dist/validation/member-invite.schema.js.map +1 -1
  142. package/frontend-vue/dms.frontend.ts +24 -5
  143. package/frontend-vue/dms.server.ts +17 -0
  144. package/frontend-vue/layers/dms-auth/server/accounts.ts +3 -4
  145. package/frontend-vue/layers/dms-core/app/build/composables/renderNow.ts +34 -0
  146. package/frontend-vue/layers/dms-core/app/build/composables/user/tablePreferences.ts +5 -1
  147. package/frontend-vue/layers/dms-core/app/build/utils/crossTab.ts +199 -0
  148. package/frontend-vue/layers/dms-core/app/build/utils/sharedRealtimeConnection.ts +410 -0
  149. package/frontend-vue/layers/dms-core/app/composables/auth/useApiData.ts +77 -12
  150. package/frontend-vue/layers/dms-core/app/composables/data-types/useDataType.ts +26 -0
  151. package/frontend-vue/layers/dms-core/app/composables/hydration/isHydratingComponent.ts +29 -0
  152. package/frontend-vue/layers/dms-core/app/composables/live/livePauseSource.ts +29 -0
  153. package/frontend-vue/layers/dms-core/app/composables/period/compareResolvers.ts +150 -9
  154. package/frontend-vue/layers/dms-core/app/composables/period/usePeriod.ts +6 -4
  155. package/frontend-vue/layers/dms-core/app/composables/period/usePeriodScope.ts +27 -1
  156. package/frontend-vue/layers/dms-core/app/composables/realtime/useUserRealtime.ts +256 -119
  157. package/frontend-vue/layers/dms-core/app/composables/translation/useComposedText.ts +21 -8
  158. package/frontend-vue/layers/dms-core/app/composables/translation/useTranslation.ts +22 -5
  159. package/frontend-vue/layers/dms-core/app/plugins/dev-error-log.ts +16 -0
  160. package/frontend-vue/layers/dms-core/app/types/user.ts +6 -0
  161. package/frontend-vue/layers/dms-core/app/utils/composedText.ts +7 -2
  162. package/frontend-vue/layers/dms-core/app/utils/formatter.ts +166 -35
  163. package/frontend-vue/layers/dms-core/app/utils/url-interpolation.ts +18 -1
  164. package/frontend-vue/layers/dms-core/i18n/locales/core-en-GB.json +134 -2
  165. package/frontend-vue/layers/dms-core/i18n/locales/core-fr-FR.json +134 -2
  166. package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardBanners.vue +3 -2
  167. package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardHeader.vue +41 -21
  168. package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardPageBody.vue +9 -0
  169. package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardSidebar.vue +4 -19
  170. package/frontend-vue/layers/dms-layout/app/build/components/layout/Footer.vue +11 -3
  171. package/frontend-vue/layers/dms-layout/app/build/components/layout/GithubStarPrompt.vue +18 -7
  172. package/frontend-vue/layers/dms-layout/app/build/components/layout/PageHeaderActionBar.vue +8 -0
  173. package/frontend-vue/layers/dms-layout/app/build/components/layout/PageHeaderActionsOutlet.vue +14 -0
  174. package/frontend-vue/layers/dms-layout/app/build/components/layout/RowSidePanel.vue +43 -82
  175. package/frontend-vue/layers/dms-layout/app/build/components/layout/SidebarUserMenu.vue +44 -3
  176. package/frontend-vue/layers/dms-layout/app/build/components/layout/UserIdentity.vue +11 -3
  177. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleCatalogTile.vue +132 -35
  178. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleManageMenu.vue +59 -0
  179. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleSourceLabel.vue +25 -0
  180. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleStoreBanners.vue +73 -0
  181. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleStoreOperation.vue +116 -0
  182. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleStoreRequirements.vue +8 -3
  183. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleStoreTile.vue +85 -207
  184. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleTile.vue +10 -3
  185. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleTileSkeleton.vue +68 -0
  186. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/notification/NotificationInboxDisplay.vue +24 -8
  187. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/notification/NotificationInboxItem.vue +8 -5
  188. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/region/RegionTimeZoneInput.vue +1 -0
  189. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/roles/role-permissions.ts +19 -0
  190. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/roles/useRoleEditor.ts +2 -1
  191. package/frontend-vue/layers/dms-layout/app/build/composables/layout/useSidePanelWidth.ts +31 -70
  192. package/frontend-vue/layers/dms-layout/app/build/composables/notification/useNotificationGroupsOpen.ts +11 -1
  193. package/frontend-vue/layers/dms-layout/app/build/utils/dev-reload-stream.ts +81 -0
  194. package/frontend-vue/layers/dms-layout/app/build/utils/module-store-catalog.ts +232 -36
  195. package/frontend-vue/layers/dms-layout/app/build/utils/modules-catalog.ts +122 -14
  196. package/frontend-vue/layers/dms-layout/app/composables/layout/useLayoutBanners.ts +32 -3
  197. package/frontend-vue/layers/dms-layout/app/composables/layout/usePageRecord.ts +50 -0
  198. package/frontend-vue/layers/dms-layout/app/composables/layout/useServedHeaderActions.ts +25 -7
  199. package/frontend-vue/layers/dms-layout/app/composables/page/useModuleHistory.ts +3 -2
  200. package/frontend-vue/layers/dms-layout/app/composables/page/useModuleStore.ts +51 -4
  201. package/frontend-vue/layers/dms-layout/app/composables/page/useModuleStoreActions.ts +194 -0
  202. package/frontend-vue/layers/dms-layout/app/composables/page/usePageComponentPreload.ts +23 -3
  203. package/frontend-vue/layers/dms-layout/app/composables/page/usePageLayout.ts +4 -2
  204. package/frontend-vue/layers/dms-layout/app/composables/page/useSiteLayout.ts +6 -1
  205. package/frontend-vue/layers/dms-layout/app/custom-layouts/DefaultLayout.vue +9 -0
  206. package/frontend-vue/layers/dms-layout/app/custom-layouts/SettingsLayout.vue +9 -0
  207. package/frontend-vue/layers/dms-layout/app/custom-pages/modules-store.vue +291 -0
  208. package/frontend-vue/layers/dms-layout/app/custom-pages/modules.vue +87 -474
  209. package/frontend-vue/layers/dms-layout/app/middleware/module-routing.global.ts +3 -0
  210. package/frontend-vue/layers/dms-layout/app/pages/[...slug].vue +6 -22
  211. package/frontend-vue/layers/dms-layout/app/plugins/command-palette-navigation.client.ts +2 -1
  212. package/frontend-vue/layers/dms-layout/app/plugins/command-palette-personal-pages.client.ts +14 -44
  213. package/frontend-vue/layers/dms-layout/app/plugins/dms-dev-reload.client.ts +14 -9
  214. package/frontend-vue/layers/dms-layout/app/theme/form-controls.ts +9 -4
  215. package/frontend-vue/layers/dms-layout/app/theme/surfaces.ts +22 -6
  216. package/frontend-vue/layers/dms-layout/app/types/module-store.ts +9 -0
  217. package/frontend-vue/layers/dms-layout/app/types/page.ts +15 -1
  218. package/frontend-vue/layers/dms-layout/app/utils/account-menu.ts +11 -0
  219. package/frontend-vue/layers/dms-layout/app/utils/breadcrumb.ts +57 -0
  220. package/frontend-vue/layers/dms-layout/app/utils/pageQuery.ts +26 -0
  221. package/frontend-vue/layers/dms-layout/app/utils/pageRecord.ts +71 -0
  222. package/frontend-vue/layers/dms-layout/i18n/locales/layout-en-GB.json +39 -14
  223. package/frontend-vue/layers/dms-layout/i18n/locales/layout-fr-FR.json +38 -13
  224. package/frontend-vue/layers/dms-ui/app/build/components/containers/TargetComponentTree.vue +31 -0
  225. package/frontend-vue/layers/dms-ui/app/build/components/containers/constants.ts +25 -0
  226. package/frontend-vue/layers/dms-ui/app/build/components/containers/drawer/DynamicDrawer.vue +244 -15
  227. package/frontend-vue/layers/dms-ui/app/build/components/containers/modal/DynamicModal.vue +42 -25
  228. package/frontend-vue/layers/dms-ui/app/build/components/containers/row/RowHeader.vue +153 -0
  229. package/frontend-vue/layers/dms-ui/app/build/components/containers/row/RowNotFound.vue +44 -0
  230. package/frontend-vue/layers/dms-ui/app/build/components/containers/row/drawerRowHeader.ts +35 -0
  231. package/frontend-vue/layers/dms-ui/app/build/components/form/FormEntries.vue +44 -9
  232. package/frontend-vue/layers/dms-ui/app/build/components/form/FormErrorSummary.vue +11 -4
  233. package/frontend-vue/layers/dms-ui/app/build/components/form/FormFieldControl.vue +38 -5
  234. package/frontend-vue/layers/dms-ui/app/build/components/form/FormFieldNotes.vue +29 -5
  235. package/frontend-vue/layers/dms-ui/app/build/components/form/FormSectionNav.vue +3 -1
  236. package/frontend-vue/layers/dms-ui/app/build/components/form/FormSections.vue +106 -27
  237. package/frontend-vue/layers/dms-ui/app/build/components/table/Actions.vue +12 -4
  238. package/frontend-vue/layers/dms-ui/app/build/components/table/Empty.vue +4 -2
  239. package/frontend-vue/layers/dms-ui/app/build/components/table/FiltersRow.vue +38 -1
  240. package/frontend-vue/layers/dms-ui/app/build/components/table/Pagination.vue +55 -54
  241. package/frontend-vue/layers/dms-ui/app/build/components/table/RowSelection.vue +2 -1
  242. package/frontend-vue/layers/dms-ui/app/build/components/table/Table.vue +396 -288
  243. package/frontend-vue/layers/dms-ui/app/build/components/table/Tabs.vue +2 -1
  244. package/frontend-vue/layers/dms-ui/app/build/components/table/Views.vue +2 -1
  245. package/frontend-vue/layers/dms-ui/app/build/components/table-view/CardAvatar.vue +59 -0
  246. package/frontend-vue/layers/dms-ui/app/build/components/table-view/CardGridSkeleton.vue +19 -2
  247. package/frontend-vue/layers/dms-ui/app/build/components/table-view/CardRowActions.vue +38 -0
  248. package/frontend-vue/layers/dms-ui/app/build/components/table-view/CardsDisplay.vue +148 -104
  249. package/frontend-vue/layers/dms-ui/app/build/components/table-view/LiveHoldBanner.vue +17 -2
  250. package/frontend-vue/layers/dms-ui/app/build/composables/actions/bulkSelection.ts +43 -9
  251. package/frontend-vue/layers/dms-ui/app/build/composables/actions/rowComponentOptions.ts +76 -0
  252. package/frontend-vue/layers/dms-ui/app/build/composables/actions/rowNavigation.ts +172 -27
  253. package/frontend-vue/layers/dms-ui/app/build/composables/actions/useActionTargets.ts +270 -46
  254. package/frontend-vue/layers/dms-ui/app/build/composables/confirm/confirmDialogTexts.ts +2 -0
  255. package/frontend-vue/layers/dms-ui/app/build/composables/containers/closingLayers.ts +186 -0
  256. package/frontend-vue/layers/dms-ui/app/build/composables/containers/context.ts +14 -0
  257. package/frontend-vue/layers/dms-ui/app/build/composables/containers/drawerSize.ts +229 -0
  258. package/frontend-vue/layers/dms-ui/app/build/composables/containers/useResizableSize.ts +175 -0
  259. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/metricCells.ts +18 -35
  260. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/numberSuffix.ts +47 -0
  261. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/registerDefaults.ts +99 -24
  262. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/registerFieldTypes.ts +2 -1
  263. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/relationFilter.ts +75 -0
  264. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/relationRows.ts +71 -0
  265. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/relationValue.ts +39 -0
  266. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/twoLineCell.ts +3 -3
  267. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/useColumnValueRenderer.ts +6 -2
  268. package/frontend-vue/layers/dms-ui/app/build/composables/form/createFormFields.ts +15 -0
  269. package/frontend-vue/layers/dms-ui/app/build/composables/form/fieldSaved.ts +40 -0
  270. package/frontend-vue/layers/dms-ui/app/build/composables/form/fieldValueRule.ts +62 -0
  271. package/frontend-vue/layers/dms-ui/app/build/composables/form/formEntryContext.ts +5 -0
  272. package/frontend-vue/layers/dms-ui/app/build/composables/form/formFooter.ts +42 -7
  273. package/frontend-vue/layers/dms-ui/app/build/composables/form/formLayout.ts +106 -4
  274. package/frontend-vue/layers/dms-ui/app/build/composables/form/formSections.ts +32 -0
  275. package/frontend-vue/layers/dms-ui/app/build/composables/form/readonlyValue.ts +19 -0
  276. package/frontend-vue/layers/dms-ui/app/build/composables/form/registerFormFunctions.ts +27 -0
  277. package/frontend-vue/layers/dms-ui/app/build/composables/form/repeaterTable.ts +65 -0
  278. package/frontend-vue/layers/dms-ui/app/build/composables/form/serverFieldErrors.ts +49 -0
  279. package/frontend-vue/layers/dms-ui/app/build/composables/form/toasterClearance.ts +118 -0
  280. package/frontend-vue/layers/dms-ui/app/build/composables/form/useInstantForm.ts +12 -1
  281. package/frontend-vue/layers/dms-ui/app/build/composables/live/currentPageLive.ts +41 -3
  282. package/frontend-vue/layers/dms-ui/app/build/composables/live/livePoller.ts +10 -2
  283. package/frontend-vue/layers/dms-ui/app/build/composables/live/pageReadClock.ts +28 -0
  284. package/frontend-vue/layers/dms-ui/app/build/composables/live/shortcuts.ts +1 -1
  285. package/frontend-vue/layers/dms-ui/app/build/composables/live/usePageLiveHost.ts +17 -3
  286. package/frontend-vue/layers/dms-ui/app/build/composables/live/useUpdatedLabel.ts +38 -0
  287. package/frontend-vue/layers/dms-ui/app/build/composables/page-toolbar/pageToolbarContext.ts +75 -0
  288. package/frontend-vue/layers/dms-ui/app/build/composables/page-toolbar/toolbarControlHeight.ts +29 -0
  289. package/frontend-vue/layers/dms-ui/app/build/composables/period/periodComparison.ts +52 -0
  290. package/frontend-vue/layers/dms-ui/app/build/composables/period/periodDisplay.ts +21 -0
  291. package/frontend-vue/layers/dms-ui/app/build/composables/period/periodShortLabels.ts +28 -0
  292. package/frontend-vue/layers/dms-ui/app/build/composables/period/usePeriodDraft.ts +1 -0
  293. package/frontend-vue/layers/dms-ui/app/build/composables/table/useRelationFilterLabels.ts +73 -0
  294. package/frontend-vue/layers/dms-ui/app/build/composables/table/useRowScrollAnchor.ts +106 -0
  295. package/frontend-vue/layers/dms-ui/app/build/composables/table/useTable.ts +9 -0
  296. package/frontend-vue/layers/dms-ui/app/build/composables/table/useTableColumns.ts +1 -2
  297. package/frontend-vue/layers/dms-ui/app/build/composables/table/utils/archivedCount.ts +31 -0
  298. package/frontend-vue/layers/dms-ui/app/build/composables/table/utils/filterEditorValue.ts +38 -0
  299. package/frontend-vue/layers/dms-ui/app/build/composables/table/utils/formatFilterValue.ts +33 -2
  300. package/frontend-vue/layers/dms-ui/app/build/composables/table/utils/scrollEdges.ts +31 -0
  301. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useCardFields.ts +33 -0
  302. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useMobileDisplay.ts +48 -0
  303. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useScrolledPastTop.ts +41 -18
  304. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableFooter.ts +3 -2
  305. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableLinkedVisit.ts +78 -0
  306. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableLiveHold.ts +135 -22
  307. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableRows.ts +56 -25
  308. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViewRowActions.ts +77 -32
  309. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViews.ts +86 -10
  310. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/bulkActions.ts +14 -4
  311. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/card.ts +127 -2
  312. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/compareValues.ts +13 -0
  313. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/expandedRows.ts +28 -0
  314. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/groupedRows.ts +114 -1
  315. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/linkedArrival.ts +308 -0
  316. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/locateRow.ts +106 -0
  317. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/pageArrival.ts +34 -0
  318. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/pinnedRows.ts +17 -0
  319. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/quickFilters.ts +41 -33
  320. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/recordLink.ts +24 -8
  321. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/tableCounts.ts +113 -0
  322. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/tableQuery.ts +27 -4
  323. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/views.ts +22 -6
  324. package/frontend-vue/layers/dms-ui/app/build/utils/addressSuggestions.ts +49 -0
  325. package/frontend-vue/layers/dms-ui/app/build/utils/eyebrow.ts +12 -0
  326. package/frontend-vue/layers/dms-ui/app/build/utils/fieldOptionsI18n.ts +51 -11
  327. package/frontend-vue/layers/dms-ui/app/build/utils/inputWidth.ts +53 -0
  328. package/frontend-vue/layers/dms-ui/app/build/utils/numberAdjustment.ts +6 -3
  329. package/frontend-vue/layers/dms-ui/app/build/utils/relativeDate.ts +19 -5
  330. package/frontend-vue/layers/dms-ui/app/build/utils/scrollWithin.ts +99 -0
  331. package/frontend-vue/layers/dms-ui/app/build/utils/urlVariables.ts +36 -14
  332. package/frontend-vue/layers/dms-ui/app/components/PasswordStrength.vue +1 -1
  333. package/frontend-vue/layers/dms-ui/app/components/RecursiveComponent.vue +9 -1
  334. package/frontend-vue/layers/dms-ui/app/components/blocks/LiveToggleBlock.vue +80 -32
  335. package/frontend-vue/layers/dms-ui/app/components/blocks/PageToolbarBlock.vue +137 -0
  336. package/frontend-vue/layers/dms-ui/app/components/blocks/StatGroupBlock.vue +5 -0
  337. package/frontend-vue/layers/dms-ui/app/components/chart/ApexChartHost.vue +16 -2
  338. package/frontend-vue/layers/dms-ui/app/components/chart/Chart.vue +24 -5
  339. package/frontend-vue/layers/dms-ui/app/components/chart/ChartCard.vue +41 -23
  340. package/frontend-vue/layers/dms-ui/app/components/confirm/ConfirmModal.vue +1 -1
  341. package/frontend-vue/layers/dms-ui/app/components/container-footer/ContainerFooter.vue +34 -0
  342. package/frontend-vue/layers/dms-ui/app/components/field-row/FieldRow.vue +25 -2
  343. package/frontend-vue/layers/dms-ui/app/components/flow-canvas/FlowCanvas.vue +2 -2
  344. package/frontend-vue/layers/dms-ui/app/components/form/Form.vue +118 -36
  345. package/frontend-vue/layers/dms-ui/app/components/form/components/DatePicker.vue +20 -1
  346. package/frontend-vue/layers/dms-ui/app/components/form/components/Display.vue +13 -1
  347. package/frontend-vue/layers/dms-ui/app/components/form/components/Image.vue +8 -3
  348. package/frontend-vue/layers/dms-ui/app/components/form/components/InputAddress.vue +53 -5
  349. package/frontend-vue/layers/dms-ui/app/components/form/components/InputCode.vue +29 -5
  350. package/frontend-vue/layers/dms-ui/app/components/form/components/InputNumber.vue +45 -8
  351. package/frontend-vue/layers/dms-ui/app/components/form/components/InputPercentage.vue +28 -5
  352. package/frontend-vue/layers/dms-ui/app/components/form/components/InputSecret.vue +5 -0
  353. package/frontend-vue/layers/dms-ui/app/components/form/components/InputText.vue +19 -2
  354. package/frontend-vue/layers/dms-ui/app/components/form/components/InputTime.vue +2 -1
  355. package/frontend-vue/layers/dms-ui/app/components/form/components/Relation.vue +205 -65
  356. package/frontend-vue/layers/dms-ui/app/components/form/components/Repeater.vue +80 -30
  357. package/frontend-vue/layers/dms-ui/app/components/form/components/RepeaterTable.vue +203 -0
  358. package/frontend-vue/layers/dms-ui/app/components/form/components/Select.vue +4 -2
  359. package/frontend-vue/layers/dms-ui/app/components/grid/GridRow.vue +11 -2
  360. package/frontend-vue/layers/dms-ui/app/components/key-value-list/KeyValueList.vue +79 -70
  361. package/frontend-vue/layers/dms-ui/app/components/kpi/KpiCard.vue +117 -21
  362. package/frontend-vue/layers/dms-ui/app/components/meter/Meter.vue +2 -1
  363. package/frontend-vue/layers/dms-ui/app/components/period/PeriodSelector.vue +113 -34
  364. package/frontend-vue/layers/dms-ui/app/components/save-bar/SaveBar.vue +14 -2
  365. package/frontend-vue/layers/dms-ui/app/components/section-header/Eyebrow.vue +6 -1
  366. package/frontend-vue/layers/dms-ui/app/components/segmented/Segmented.vue +13 -0
  367. package/frontend-vue/layers/dms-ui/app/components/stat-group/StatGroup.vue +52 -3
  368. package/frontend-vue/layers/dms-ui/app/components/tab/Tab.vue +2 -1
  369. package/frontend-vue/layers/dms-ui/app/components/table-view/KanbanBoard.vue +45 -35
  370. package/frontend-vue/layers/dms-ui/app/components/table-view/KanbanDisplay.vue +4 -1
  371. package/frontend-vue/layers/dms-ui/app/components/table-view/TableView.vue +410 -96
  372. package/frontend-vue/layers/dms-ui/app/components/top-list/TopListCard.vue +33 -5
  373. package/frontend-vue/layers/dms-ui/app/composables/chart/apexCategoryLabels.ts +68 -0
  374. package/frontend-vue/layers/dms-ui/app/composables/chart/apexDatetimeBuckets.ts +306 -0
  375. package/frontend-vue/layers/dms-ui/app/composables/chart/formatValue.ts +4 -3
  376. package/frontend-vue/layers/dms-ui/app/composables/chart/heatmapColors.ts +93 -0
  377. package/frontend-vue/layers/dms-ui/app/composables/chart/localizeSeries.ts +62 -0
  378. package/frontend-vue/layers/dms-ui/app/composables/chart/useApexChart.ts +116 -8
  379. package/frontend-vue/layers/dms-ui/app/composables/chart/useApexChart.types.ts +7 -0
  380. package/frontend-vue/layers/dms-ui/app/composables/containers/types.ts +84 -1
  381. package/frontend-vue/layers/dms-ui/app/composables/containers/useDrawer.ts +69 -8
  382. package/frontend-vue/layers/dms-ui/app/composables/form/types/field-saved.ts +11 -0
  383. package/frontend-vue/layers/dms-ui/app/composables/form/types/field.ts +4 -0
  384. package/frontend-vue/layers/dms-ui/app/composables/form/types/functions.ts +1 -0
  385. package/frontend-vue/layers/dms-ui/app/composables/form/useForm.ts +142 -21
  386. package/frontend-vue/layers/dms-ui/app/composables/live/usePageLive.ts +11 -4
  387. package/frontend-vue/layers/dms-ui/app/composables/page-toolbar/usePageToolbarSize.ts +30 -0
  388. package/frontend-vue/layers/dms-ui/app/composables/table-view/kanban.ts +4 -2
  389. package/frontend-vue/layers/dms-ui/app/composables/table-view/types/action-target.ts +22 -3
  390. package/frontend-vue/layers/dms-ui/app/composables/table-view/types/config.ts +4 -0
  391. package/frontend-vue/layers/dms-ui/app/composables/table-view/types/display.ts +24 -1
  392. package/frontend-vue/layers/dms-ui/app/plugins/closing-layers.client.ts +10 -0
  393. package/frontend-vue/layers/dms-ui/app/plugins/live-pause-source.ts +15 -0
  394. package/frontend-vue/layers/dms-ui/app/plugins/register.ts +2 -0
  395. package/frontend-vue/layers/dms-ui/app/types/input-number.ts +3 -2
  396. package/frontend-vue/layers/dms-ui/app/types/page-toolbar.ts +7 -0
  397. package/frontend-vue/layers/dms-ui/app/types/row-action.ts +0 -2
  398. package/frontend-vue/layers/dms-ui/app/types/row-navigation.ts +17 -0
  399. package/frontend-vue/layers/dms-ui/app/types/table/query.ts +2 -1
  400. package/frontend-vue/layers/dms-ui/app/utils/blockRefresh.ts +3 -1
  401. package/frontend-vue/layers/dms-ui/app/utils/componentTree.ts +24 -0
  402. package/frontend-vue/layers/dms-ui/i18n/locales/ui-en-GB.json +16 -4
  403. package/frontend-vue/layers/dms-ui/i18n/locales/ui-fr-FR.json +16 -4
  404. package/frontend-vue/package.json +2 -2
  405. package/package.json +7 -6
  406. package/skills/dms/SKILL.md +1 -1
  407. package/skills/dms-auth/SKILL.md +5 -3
  408. package/skills/dms-best-practices/REFERENCE.md +2 -0
  409. package/skills/dms-best-practices/SKILL.md +4 -0
  410. package/skills/dms-module/SKILL.md +3 -2
  411. package/skills/dms-pages/REFERENCE.md +125 -16
  412. package/skills/dms-pages/SKILL.md +1 -1
  413. package/skills/dms-project/DEV-SERVERS.md +23 -1
  414. package/skills/dms-project/REFERENCE.md +1 -1
  415. package/skills/dms-project/SKILL.md +4 -1
  416. package/skills/dms-project/scripts/dms-dev.sh +205 -17
  417. package/dist/routes/startup-gate.d.ts +0 -7
  418. package/dist/routes/startup-gate.js +0 -39
  419. package/dist/routes/startup-gate.js.map +0 -1
  420. package/frontend-vue/layers/dms-core/app/composables/period/periodRenderNow.ts +0 -42
@@ -1,9 +1,30 @@
1
1
  import type {
2
2
  ModuleCatalogEntry,
3
3
  ModuleStatus,
4
+ ModuleWithAccess,
4
5
  PageInfo,
5
6
  SiteLayout,
7
+ SiteLayoutTree,
6
8
  } from "../../types/page";
9
+ import { resolveModuleLandingSlug } from "../../utils/moduleLanding";
10
+
11
+ /** The Modules page, which lists and manages the installed modules. */
12
+ export const MODULES_PAGE_PATH = "/modules";
13
+ /** The module store, its own view under the Modules page. */
14
+ export const MODULE_STORE_PATH = `${MODULES_PAGE_PATH}/store`;
15
+
16
+ /**
17
+ * The grid of both module views, installed and store: three columns on a
18
+ * laptop, so a tile's description, readout and footer line have room. The
19
+ * min() keeps one column from overflowing a phone.
20
+ */
21
+ export const MODULE_TILE_GRID_CLASS =
22
+ "grid grid-cols-[repeat(auto-fill,minmax(min(340px,100%),1fr))] gap-4";
23
+
24
+ /** Where an installed module's tile is, the one that manages it. */
25
+ export function installedModuleLink(moduleId: string): string {
26
+ return `${MODULES_PAGE_PATH}#module-${moduleId}`;
27
+ }
7
28
 
8
29
  /** Category of the modules that declare none. */
9
30
  export const MODULE_CATEGORY_OTHER = "$modules.category_other";
@@ -12,6 +33,18 @@ export const MODULE_CATEGORY_ALL = "";
12
33
 
13
34
  export type ModuleSortMode = "recent" | "name";
14
35
 
36
+ /** The order the installed modules open in until the user picks another. */
37
+ export const DEFAULT_MODULE_SORT_MODE: ModuleSortMode = "name";
38
+
39
+ const MODULE_SORT_MODES: readonly ModuleSortMode[] = ["name", "recent"];
40
+
41
+ /** A remembered sort mode, or the default when it is missing or unknown. */
42
+ export function parseModuleSortMode(value: unknown): ModuleSortMode {
43
+ return (
44
+ MODULE_SORT_MODES.find((mode) => mode === value) ?? DEFAULT_MODULE_SORT_MODE
45
+ );
46
+ }
47
+
15
48
  /** Turns a display string (possibly a `$i18n.key`) into the shown text. */
16
49
  export type DisplayResolver = (value: string) => string;
17
50
 
@@ -78,23 +111,28 @@ export function summarizeModules(
78
111
  /**
79
112
  * Category filter options: the categories present, alphabetical, with
80
113
  * "Other" last. The "All" option is the caller's to add.
114
+ *
115
+ * Categories are told apart by their shown name: two modules that each name
116
+ * "Content" through their own i18n key share one option, whose value is the
117
+ * first key met.
81
118
  */
82
119
  export function moduleCategoryOptions(
83
120
  entries: readonly FilterableModule[],
84
121
  resolve: DisplayResolver,
85
122
  ): ModuleCategoryOption[] {
86
- const counts = new Map<string, number>();
123
+ const byLabel = new Map<string, ModuleCategoryOption>();
87
124
  for (const entry of entries) {
88
- const category = moduleCategory(entry);
89
- counts.set(category, (counts.get(category) ?? 0) + 1);
125
+ const value = moduleCategory(entry);
126
+ const label = resolve(value);
127
+ const option = byLabel.get(label);
128
+ if (option) option.count += 1;
129
+ else byLabel.set(label, { value, label, count: 1 });
90
130
  }
91
- return [...counts.entries()]
92
- .map(([value, count]) => ({ value, label: resolve(value), count }))
93
- .sort((a, b) => {
94
- if (a.value === MODULE_CATEGORY_OTHER) return 1;
95
- if (b.value === MODULE_CATEGORY_OTHER) return -1;
96
- return a.label.localeCompare(b.label);
97
- });
131
+ return [...byLabel.values()].sort((a, b) => {
132
+ if (a.value === MODULE_CATEGORY_OTHER) return 1;
133
+ if (b.value === MODULE_CATEGORY_OTHER) return -1;
134
+ return a.label.localeCompare(b.label);
135
+ });
98
136
  }
99
137
 
100
138
  function normalizeQuery(query: string): string {
@@ -106,10 +144,14 @@ export function filterModules<T extends FilterableModule>(
106
144
  options: { query: string; category: string; resolve: DisplayResolver },
107
145
  ): T[] {
108
146
  const query = normalizeQuery(options.query);
147
+ const category =
148
+ options.category === MODULE_CATEGORY_ALL
149
+ ? undefined
150
+ : options.resolve(options.category);
109
151
  return entries.filter((entry) => {
110
152
  if (
111
- options.category !== MODULE_CATEGORY_ALL &&
112
- moduleCategory(entry) !== options.category
153
+ category !== undefined &&
154
+ options.resolve(moduleCategory(entry)) !== category
113
155
  ) {
114
156
  return false;
115
157
  }
@@ -126,18 +168,36 @@ export function filterModules<T extends FilterableModule>(
126
168
  });
127
169
  }
128
170
 
171
+ /**
172
+ * Compares shown names as `locale` orders them: accents and case only break
173
+ * ties ("Mailing", "Marketing", "Médiathèque"), and digits read as numbers.
174
+ * An unknown locale falls back to the runtime's own.
175
+ */
176
+ export function compareDisplayNames(
177
+ locale?: string,
178
+ ): (a: string, b: string) => number {
179
+ const options: Intl.CollatorOptions = { numeric: true };
180
+ try {
181
+ return new Intl.Collator(locale, options).compare;
182
+ } catch {
183
+ return new Intl.Collator(undefined, options).compare;
184
+ }
185
+ }
186
+
129
187
  /**
130
188
  * `recent`: most recently opened first, never-opened modules after them by
131
- * name. `name`: alphabetical.
189
+ * name. `name`: alphabetical by shown name, in `locale`'s order.
132
190
  */
133
191
  export function sortModules(
134
192
  entries: readonly ModuleCatalogEntry[],
135
193
  mode: ModuleSortMode,
136
194
  visitedAt: ReadonlyMap<string, number>,
137
195
  resolve: DisplayResolver,
196
+ locale?: string,
138
197
  ): ModuleCatalogEntry[] {
198
+ const compare = compareDisplayNames(locale);
139
199
  const byName = (a: ModuleCatalogEntry, b: ModuleCatalogEntry) =>
140
- resolve(a.title).localeCompare(resolve(b.title));
200
+ compare(resolve(a.title), resolve(b.title));
141
201
  return [...entries].sort((a, b) => {
142
202
  if (mode === "recent") {
143
203
  const delta = (visitedAt.get(b.id) ?? -1) - (visitedAt.get(a.id) ?? -1);
@@ -197,3 +257,51 @@ export function findModulePageMatch(
197
257
  }
198
258
  return null;
199
259
  }
260
+
261
+ /** A visited module as the command palette's recent modules list it. */
262
+ export interface RecentModule {
263
+ moduleId: string;
264
+ /** Display name, possibly a `$i18n.key`. */
265
+ title: string;
266
+ icon: string;
267
+ /** The module's landing page, never the page it was left on. */
268
+ to: string;
269
+ }
270
+
271
+ /** A visit of the module history, oldest first as it keeps them. */
272
+ export interface ModuleVisit {
273
+ moduleId: string;
274
+ }
275
+
276
+ /**
277
+ * The visited modules the caller may still open, most recent first, each
278
+ * leading to its landing page: every way into a module lands there. A module
279
+ * with no page the caller may open is left out.
280
+ */
281
+ export function recentModules(
282
+ visits: readonly ModuleVisit[],
283
+ modules: Readonly<Record<string, ModuleWithAccess>> | undefined,
284
+ tree: SiteLayoutTree | undefined,
285
+ ): RecentModule[] {
286
+ return visits
287
+ .slice()
288
+ .reverse()
289
+ .flatMap((visit) => {
290
+ const module = modules?.[visit.moduleId];
291
+ if (!module?.hasAccess) return [];
292
+ const to = resolveModuleLandingSlug(
293
+ tree,
294
+ visit.moduleId,
295
+ module.landingPage,
296
+ );
297
+ if (!to) return [];
298
+ return [
299
+ {
300
+ moduleId: visit.moduleId,
301
+ title: module.title,
302
+ icon: module.icon,
303
+ to,
304
+ },
305
+ ];
306
+ });
307
+ }
@@ -27,11 +27,37 @@ export const resolveLayoutBannerPresentation = (
27
27
  return { ...presentation, icon: banner.icon ?? presentation.icon };
28
28
  };
29
29
 
30
+ /**
31
+ * Whether a path is in the module area: the modules page, the module store
32
+ * and every module's own pages, all under `MODULE_URL_PREFIX`.
33
+ */
34
+ export const isModuleAreaPath = (path: string): boolean =>
35
+ path === MODULE_URL_PREFIX || path.startsWith(`${MODULE_URL_PREFIX}/`);
36
+
37
+ const SCOPE_MATCHERS: Record<
38
+ LayoutBannerScope,
39
+ (isModuleArea: boolean) => boolean
40
+ > = {
41
+ app: (isModuleArea) => !isModuleArea,
42
+ modules: (isModuleArea) => isModuleArea,
43
+ everywhere: () => true,
44
+ };
45
+
46
+ /** Whether a banner's scope, as the backend serves it, covers a page. */
47
+ export const isLayoutBannerInScope = (
48
+ banner: LayoutBanner,
49
+ path: string,
50
+ ): boolean => SCOPE_MATCHERS[banner.scope](isModuleAreaPath(path));
51
+
30
52
  /**
31
53
  * The global banners rendered under the dashboard header, as resolved by the
32
54
  * server for this request (see `RegisterLayoutBanner` in
33
- * `@antelopejs/interface-dms/layout-banners`), minus the ones the user
34
- * dismissed.
55
+ * `@antelopejs/interface-dms/layout-banners`), keeping the ones whose scope
56
+ * covers the current route, minus the ones the user dismissed.
57
+ *
58
+ * The site layout is shared by every page, so the scope is applied here
59
+ * against the route: on the server-rendered page and again on each
60
+ * client-side navigation, without re-fetching the layout.
35
61
  *
36
62
  * Dismissals are kept in a session cookie rather than in local storage so the
37
63
  * server-rendered page already leaves a dismissed banner out, instead of
@@ -39,6 +65,7 @@ export const resolveLayoutBannerPresentation = (
39
65
  */
40
66
  export const useLayoutBanners = () => {
41
67
  const { siteLayout } = useSiteLayout();
68
+ const route = useDmsRoute();
42
69
  const dismissed = useDmsCookie<string[]>(DISMISSED_BANNERS_COOKIE, {
43
70
  default: () => [],
44
71
  sameSite: "lax",
@@ -46,7 +73,9 @@ export const useLayoutBanners = () => {
46
73
 
47
74
  const banners = computed<LayoutBanner[]>(() =>
48
75
  (siteLayout.value?.banners ?? []).filter(
49
- (banner) => !banner.dismissible || !dismissed.value.includes(banner.key),
76
+ (banner) =>
77
+ isLayoutBannerInScope(banner, route.path) &&
78
+ (!banner.dismissible || !dismissed.value.includes(banner.key)),
50
79
  ),
51
80
  );
52
81
 
@@ -0,0 +1,50 @@
1
+ import { computed, type ComputedRef } from "vue";
2
+ import { resolveUrlVariables } from "#dms-ui/app/build/utils/urlVariables";
3
+ import type { PageRecord } from "../../utils/pageRecord";
4
+
5
+ /** The record a page shows, as the layout declares it (`record`). */
6
+ export interface PageRecordSource {
7
+ /** `{{params.X}}` and `{{query.X}}` are filled in from the page URL. */
8
+ fetchUrl: string;
9
+ }
10
+
11
+ /**
12
+ * The record the page shows, for the header buttons acting on it: loading
13
+ * until it answers, missing when it does not, or when its URL names a value
14
+ * the page does not carry. Undefined for a page declaring no record.
15
+ *
16
+ * It is read through `useApiData`, keyed on its URL like a block's
17
+ * `fetchUrl`: a block of the page reading the same URL shares the one
18
+ * request, and a server render hands the record to the browser with the page.
19
+ */
20
+ export function usePageRecord(
21
+ source: () => PageRecordSource | undefined,
22
+ ): ComputedRef<PageRecord | undefined> {
23
+ const route = useDmsRoute();
24
+ const siteLayout = useSiteLayout();
25
+
26
+ // Null while the URL names a value the page does not carry.
27
+ const url = computed(() => {
28
+ const declared = source();
29
+ if (!declared) return undefined;
30
+ return (
31
+ resolveUrlVariables(declared.fetchUrl, {
32
+ routeParams: siteLayout.findMatchingRoute(route.path)?.params,
33
+ routeQuery: route.query as Record<string, unknown>,
34
+ }) ?? null
35
+ );
36
+ });
37
+ // The options a block's read takes (`useChartFetch`): a block never holds
38
+ // a navigation, and neither does the header.
39
+ const { data, error } = useApiData<Record<string, unknown>>(
40
+ () => url.value ?? null,
41
+ { lazy: true },
42
+ );
43
+
44
+ return computed<PageRecord | undefined>(() => {
45
+ if (url.value === undefined) return undefined;
46
+ if (url.value === null || error.value) return { status: "missing" };
47
+ if (data.value) return { status: "loaded", values: data.value };
48
+ return { status: "loading" };
49
+ });
50
+ }
@@ -3,6 +3,8 @@ import type { LayoutHeaderAction } from "../../build/components/layout/PageHeade
3
3
  import type { PageLayout } from "../../types/page";
4
4
  import {
5
5
  isSamePageQuery,
6
+ isSameRouteParams,
7
+ fillRouteParams,
6
8
  layoutUrlWithQuery,
7
9
  pageQueryValues,
8
10
  } from "../../utils/pageQuery";
@@ -15,6 +17,8 @@ interface ServedHeaderActionsOptions {
15
17
  * declares an `availability` (`headerActionsQuery`).
16
18
  */
17
19
  servedQuery: () => Record<string, string> | undefined;
20
+ /** The route parameters (`:id`) their availability was resolved for. */
21
+ servedParams?: () => Record<string, string> | undefined;
18
22
  }
19
23
 
20
24
  /**
@@ -22,20 +26,22 @@ interface ServedHeaderActionsOptions {
22
26
  * `availability` depends on the query (`?workspace=` names the record the
23
27
  * page acts on) was resolved for the query the layout was loaded with: when
24
28
  * 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.
29
+ * for that query and takes its buttons, keeping the last ones meanwhile. The
30
+ * same goes for the route parameters: the layout of `/projects/project/:id`
31
+ * is cached once for the page, whichever record it was first served for.
26
32
  * Buttons without an availability never cause a request.
27
33
  */
28
34
  export function useServedHeaderActions({
29
35
  actions,
30
36
  servedQuery,
37
+ servedParams = () => undefined,
31
38
  }: ServedHeaderActionsOptions): ComputedRef<LayoutHeaderAction[]> {
32
39
  const route = useDmsRoute();
33
40
  const siteLayout = useSiteLayout();
34
41
  const refreshed = shallowRef<LayoutHeaderAction[] | null>(null);
35
42
  let latestRequest = 0;
36
43
 
37
- const layoutUrl = () =>
38
- siteLayout.findMatchingRoute(route.path)?.metadata?.layoutUrl;
44
+ const pageRoute = () => siteLayout.findMatchingRoute(route.path);
39
45
 
40
46
  async function askFor(url: string, query: Record<string, string>) {
41
47
  const request = ++latestRequest;
@@ -58,17 +64,29 @@ export function useServedHeaderActions({
58
64
  function follow() {
59
65
  const served = servedQuery();
60
66
  const current = pageQueryValues(route.query as Record<string, unknown>);
61
- const url = layoutUrl();
62
- if (!served || !url || isSamePageQuery(served, current)) {
67
+ const match = pageRoute();
68
+ const layoutUrl = match?.metadata?.layoutUrl;
69
+ const params = match?.params ?? {};
70
+ const paramsServed = servedParams();
71
+ if (
72
+ !served ||
73
+ !layoutUrl ||
74
+ (isSamePageQuery(served, current) &&
75
+ (!paramsServed || isSameRouteParams(paramsServed, params)))
76
+ ) {
63
77
  latestRequest++;
64
78
  refreshed.value = null;
65
79
  return;
66
80
  }
67
- void askFor(url, current);
81
+ void askFor(fillRouteParams(layoutUrl, params), current);
68
82
  }
69
83
 
70
84
  if (!import.meta.env.SSR) {
71
- watch([() => route.query, servedQuery], follow, { immediate: true });
85
+ watch(
86
+ [() => route.query, () => route.path, servedQuery, servedParams],
87
+ follow,
88
+ { immediate: true },
89
+ );
72
90
  }
73
91
 
74
92
  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,32 @@ 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
+
73
+ /**
74
+ * Starts following once mounted: reads again only when the first read
75
+ * failed, or something moves; a listing already on screen stays as is.
76
+ * A first read still in flight (a client navigation) is waited for.
77
+ */
78
+ function start(): void {
79
+ isStopped = false;
80
+ if (initial.pending.value) {
81
+ void Promise.resolve(initial).then(() => {
82
+ if (!isStopped) start();
83
+ });
84
+ return;
85
+ }
86
+ if (!listing.value) {
87
+ void load();
88
+ return;
44
89
  }
45
90
  reloadWhenServed();
46
91
  scheduleFollow();
@@ -93,6 +138,7 @@ export const useModuleStore = () => {
93
138
  }
94
139
 
95
140
  function stop(): void {
141
+ isStopped = true;
96
142
  if (followTimer) clearTimeout(followTimer);
97
143
  followTimer = undefined;
98
144
  }
@@ -103,6 +149,7 @@ export const useModuleStore = () => {
103
149
  isFollowing,
104
150
  awaitingReload,
105
151
  load,
152
+ start,
106
153
  run,
107
154
  stop,
108
155
  };
@@ -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
+ };