@antelopejs/dms 0.9.7 → 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 (414) hide show
  1. package/CHANGELOG.md +114 -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 -45
  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/realtime.d.ts +13 -1
  117. package/dist/routes/realtime.js +114 -29
  118. package/dist/routes/realtime.js.map +1 -1
  119. package/dist/startup-config.d.ts +2 -0
  120. package/dist/startup-config.js +62 -0
  121. package/dist/startup-config.js.map +1 -0
  122. package/dist/store/interfaces.d.ts +3 -2
  123. package/dist/store/interfaces.js +16 -6
  124. package/dist/store/interfaces.js.map +1 -1
  125. package/dist/store/listing.js +24 -13
  126. package/dist/store/listing.js.map +1 -1
  127. package/dist/store/operations.js +4 -0
  128. package/dist/store/operations.js.map +1 -1
  129. package/dist/store/requirements.js +3 -2
  130. package/dist/store/requirements.js.map +1 -1
  131. package/dist/store/runtime.d.ts +10 -1
  132. package/dist/store/runtime.js +33 -6
  133. package/dist/store/runtime.js.map +1 -1
  134. package/dist/store/types.d.ts +10 -0
  135. package/dist/validation/auth.schema.d.ts +2 -2
  136. package/dist/validation/member-invite.schema.d.ts +2 -10
  137. package/dist/validation/member-invite.schema.js +2 -15
  138. package/dist/validation/member-invite.schema.js.map +1 -1
  139. package/frontend-vue/dms.frontend.ts +24 -5
  140. package/frontend-vue/dms.server.ts +17 -0
  141. package/frontend-vue/layers/dms-auth/server/accounts.ts +3 -4
  142. package/frontend-vue/layers/dms-core/app/build/composables/renderNow.ts +34 -0
  143. package/frontend-vue/layers/dms-core/app/build/composables/user/tablePreferences.ts +5 -1
  144. package/frontend-vue/layers/dms-core/app/build/utils/crossTab.ts +199 -0
  145. package/frontend-vue/layers/dms-core/app/build/utils/sharedRealtimeConnection.ts +410 -0
  146. package/frontend-vue/layers/dms-core/app/composables/auth/useApiData.ts +77 -12
  147. package/frontend-vue/layers/dms-core/app/composables/data-types/useDataType.ts +26 -0
  148. package/frontend-vue/layers/dms-core/app/composables/hydration/isHydratingComponent.ts +29 -0
  149. package/frontend-vue/layers/dms-core/app/composables/live/livePauseSource.ts +29 -0
  150. package/frontend-vue/layers/dms-core/app/composables/period/compareResolvers.ts +150 -9
  151. package/frontend-vue/layers/dms-core/app/composables/period/usePeriod.ts +6 -4
  152. package/frontend-vue/layers/dms-core/app/composables/period/usePeriodScope.ts +27 -1
  153. package/frontend-vue/layers/dms-core/app/composables/realtime/useUserRealtime.ts +256 -119
  154. package/frontend-vue/layers/dms-core/app/composables/translation/useComposedText.ts +21 -8
  155. package/frontend-vue/layers/dms-core/app/composables/translation/useTranslation.ts +22 -5
  156. package/frontend-vue/layers/dms-core/app/plugins/dev-error-log.ts +16 -0
  157. package/frontend-vue/layers/dms-core/app/types/user.ts +6 -0
  158. package/frontend-vue/layers/dms-core/app/utils/composedText.ts +7 -2
  159. package/frontend-vue/layers/dms-core/app/utils/formatter.ts +166 -35
  160. package/frontend-vue/layers/dms-core/app/utils/url-interpolation.ts +18 -1
  161. package/frontend-vue/layers/dms-core/i18n/locales/core-en-GB.json +134 -2
  162. package/frontend-vue/layers/dms-core/i18n/locales/core-fr-FR.json +134 -2
  163. package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardBanners.vue +3 -2
  164. package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardHeader.vue +41 -21
  165. package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardPageBody.vue +9 -0
  166. package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardSidebar.vue +4 -19
  167. package/frontend-vue/layers/dms-layout/app/build/components/layout/Footer.vue +11 -3
  168. package/frontend-vue/layers/dms-layout/app/build/components/layout/GithubStarPrompt.vue +18 -7
  169. package/frontend-vue/layers/dms-layout/app/build/components/layout/PageHeaderActionBar.vue +8 -0
  170. package/frontend-vue/layers/dms-layout/app/build/components/layout/PageHeaderActionsOutlet.vue +14 -0
  171. package/frontend-vue/layers/dms-layout/app/build/components/layout/RowSidePanel.vue +43 -82
  172. package/frontend-vue/layers/dms-layout/app/build/components/layout/SidebarUserMenu.vue +44 -3
  173. package/frontend-vue/layers/dms-layout/app/build/components/layout/UserIdentity.vue +11 -3
  174. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleCatalogTile.vue +132 -35
  175. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleManageMenu.vue +59 -0
  176. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleSourceLabel.vue +25 -0
  177. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleStoreBanners.vue +73 -0
  178. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleStoreOperation.vue +116 -0
  179. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleStoreRequirements.vue +8 -3
  180. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleStoreTile.vue +85 -207
  181. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleTile.vue +10 -3
  182. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleTileSkeleton.vue +68 -0
  183. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/notification/NotificationInboxDisplay.vue +24 -8
  184. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/notification/NotificationInboxItem.vue +8 -5
  185. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/region/RegionTimeZoneInput.vue +1 -0
  186. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/roles/role-permissions.ts +19 -0
  187. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/roles/useRoleEditor.ts +2 -1
  188. package/frontend-vue/layers/dms-layout/app/build/composables/layout/useSidePanelWidth.ts +31 -70
  189. package/frontend-vue/layers/dms-layout/app/build/composables/notification/useNotificationGroupsOpen.ts +11 -1
  190. package/frontend-vue/layers/dms-layout/app/build/utils/dev-reload-stream.ts +81 -0
  191. package/frontend-vue/layers/dms-layout/app/build/utils/module-store-catalog.ts +232 -36
  192. package/frontend-vue/layers/dms-layout/app/build/utils/modules-catalog.ts +122 -14
  193. package/frontend-vue/layers/dms-layout/app/composables/layout/useLayoutBanners.ts +32 -3
  194. package/frontend-vue/layers/dms-layout/app/composables/layout/usePageRecord.ts +50 -0
  195. package/frontend-vue/layers/dms-layout/app/composables/layout/useServedHeaderActions.ts +25 -7
  196. package/frontend-vue/layers/dms-layout/app/composables/page/useModuleHistory.ts +3 -2
  197. package/frontend-vue/layers/dms-layout/app/composables/page/useModuleStore.ts +51 -4
  198. package/frontend-vue/layers/dms-layout/app/composables/page/useModuleStoreActions.ts +194 -0
  199. package/frontend-vue/layers/dms-layout/app/composables/page/usePageComponentPreload.ts +23 -3
  200. package/frontend-vue/layers/dms-layout/app/composables/page/usePageLayout.ts +4 -2
  201. package/frontend-vue/layers/dms-layout/app/composables/page/useSiteLayout.ts +6 -1
  202. package/frontend-vue/layers/dms-layout/app/custom-layouts/DefaultLayout.vue +9 -0
  203. package/frontend-vue/layers/dms-layout/app/custom-layouts/SettingsLayout.vue +9 -0
  204. package/frontend-vue/layers/dms-layout/app/custom-pages/modules-store.vue +291 -0
  205. package/frontend-vue/layers/dms-layout/app/custom-pages/modules.vue +87 -474
  206. package/frontend-vue/layers/dms-layout/app/middleware/module-routing.global.ts +3 -0
  207. package/frontend-vue/layers/dms-layout/app/pages/[...slug].vue +6 -22
  208. package/frontend-vue/layers/dms-layout/app/plugins/command-palette-navigation.client.ts +2 -1
  209. package/frontend-vue/layers/dms-layout/app/plugins/command-palette-personal-pages.client.ts +14 -44
  210. package/frontend-vue/layers/dms-layout/app/plugins/dms-dev-reload.client.ts +14 -9
  211. package/frontend-vue/layers/dms-layout/app/theme/form-controls.ts +9 -4
  212. package/frontend-vue/layers/dms-layout/app/theme/surfaces.ts +22 -6
  213. package/frontend-vue/layers/dms-layout/app/types/module-store.ts +9 -0
  214. package/frontend-vue/layers/dms-layout/app/types/page.ts +15 -1
  215. package/frontend-vue/layers/dms-layout/app/utils/account-menu.ts +11 -0
  216. package/frontend-vue/layers/dms-layout/app/utils/breadcrumb.ts +57 -0
  217. package/frontend-vue/layers/dms-layout/app/utils/pageQuery.ts +26 -0
  218. package/frontend-vue/layers/dms-layout/app/utils/pageRecord.ts +71 -0
  219. package/frontend-vue/layers/dms-layout/i18n/locales/layout-en-GB.json +39 -14
  220. package/frontend-vue/layers/dms-layout/i18n/locales/layout-fr-FR.json +38 -13
  221. package/frontend-vue/layers/dms-ui/app/build/components/containers/TargetComponentTree.vue +31 -0
  222. package/frontend-vue/layers/dms-ui/app/build/components/containers/constants.ts +25 -0
  223. package/frontend-vue/layers/dms-ui/app/build/components/containers/drawer/DynamicDrawer.vue +244 -15
  224. package/frontend-vue/layers/dms-ui/app/build/components/containers/modal/DynamicModal.vue +42 -25
  225. package/frontend-vue/layers/dms-ui/app/build/components/containers/row/RowHeader.vue +153 -0
  226. package/frontend-vue/layers/dms-ui/app/build/components/containers/row/RowNotFound.vue +44 -0
  227. package/frontend-vue/layers/dms-ui/app/build/components/containers/row/drawerRowHeader.ts +35 -0
  228. package/frontend-vue/layers/dms-ui/app/build/components/form/FormEntries.vue +44 -9
  229. package/frontend-vue/layers/dms-ui/app/build/components/form/FormErrorSummary.vue +11 -4
  230. package/frontend-vue/layers/dms-ui/app/build/components/form/FormFieldControl.vue +38 -5
  231. package/frontend-vue/layers/dms-ui/app/build/components/form/FormFieldNotes.vue +29 -5
  232. package/frontend-vue/layers/dms-ui/app/build/components/form/FormSectionNav.vue +3 -1
  233. package/frontend-vue/layers/dms-ui/app/build/components/form/FormSections.vue +106 -27
  234. package/frontend-vue/layers/dms-ui/app/build/components/table/Actions.vue +12 -4
  235. package/frontend-vue/layers/dms-ui/app/build/components/table/Empty.vue +4 -2
  236. package/frontend-vue/layers/dms-ui/app/build/components/table/FiltersRow.vue +38 -1
  237. package/frontend-vue/layers/dms-ui/app/build/components/table/Pagination.vue +55 -54
  238. package/frontend-vue/layers/dms-ui/app/build/components/table/RowSelection.vue +2 -1
  239. package/frontend-vue/layers/dms-ui/app/build/components/table/Table.vue +396 -288
  240. package/frontend-vue/layers/dms-ui/app/build/components/table/Tabs.vue +2 -1
  241. package/frontend-vue/layers/dms-ui/app/build/components/table/Views.vue +2 -1
  242. package/frontend-vue/layers/dms-ui/app/build/components/table-view/CardAvatar.vue +59 -0
  243. package/frontend-vue/layers/dms-ui/app/build/components/table-view/CardGridSkeleton.vue +19 -2
  244. package/frontend-vue/layers/dms-ui/app/build/components/table-view/CardRowActions.vue +38 -0
  245. package/frontend-vue/layers/dms-ui/app/build/components/table-view/CardsDisplay.vue +148 -104
  246. package/frontend-vue/layers/dms-ui/app/build/components/table-view/LiveHoldBanner.vue +17 -2
  247. package/frontend-vue/layers/dms-ui/app/build/composables/actions/bulkSelection.ts +43 -9
  248. package/frontend-vue/layers/dms-ui/app/build/composables/actions/rowComponentOptions.ts +76 -0
  249. package/frontend-vue/layers/dms-ui/app/build/composables/actions/rowNavigation.ts +172 -27
  250. package/frontend-vue/layers/dms-ui/app/build/composables/actions/useActionTargets.ts +270 -46
  251. package/frontend-vue/layers/dms-ui/app/build/composables/confirm/confirmDialogTexts.ts +2 -0
  252. package/frontend-vue/layers/dms-ui/app/build/composables/containers/closingLayers.ts +186 -0
  253. package/frontend-vue/layers/dms-ui/app/build/composables/containers/context.ts +14 -0
  254. package/frontend-vue/layers/dms-ui/app/build/composables/containers/drawerSize.ts +229 -0
  255. package/frontend-vue/layers/dms-ui/app/build/composables/containers/useResizableSize.ts +175 -0
  256. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/metricCells.ts +18 -35
  257. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/numberSuffix.ts +47 -0
  258. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/registerDefaults.ts +99 -24
  259. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/registerFieldTypes.ts +2 -1
  260. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/relationFilter.ts +75 -0
  261. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/relationRows.ts +71 -0
  262. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/relationValue.ts +39 -0
  263. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/twoLineCell.ts +3 -3
  264. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/useColumnValueRenderer.ts +6 -2
  265. package/frontend-vue/layers/dms-ui/app/build/composables/form/createFormFields.ts +15 -0
  266. package/frontend-vue/layers/dms-ui/app/build/composables/form/fieldSaved.ts +40 -0
  267. package/frontend-vue/layers/dms-ui/app/build/composables/form/fieldValueRule.ts +62 -0
  268. package/frontend-vue/layers/dms-ui/app/build/composables/form/formEntryContext.ts +5 -0
  269. package/frontend-vue/layers/dms-ui/app/build/composables/form/formFooter.ts +42 -7
  270. package/frontend-vue/layers/dms-ui/app/build/composables/form/formLayout.ts +106 -4
  271. package/frontend-vue/layers/dms-ui/app/build/composables/form/formSections.ts +32 -0
  272. package/frontend-vue/layers/dms-ui/app/build/composables/form/readonlyValue.ts +19 -0
  273. package/frontend-vue/layers/dms-ui/app/build/composables/form/registerFormFunctions.ts +27 -0
  274. package/frontend-vue/layers/dms-ui/app/build/composables/form/repeaterTable.ts +65 -0
  275. package/frontend-vue/layers/dms-ui/app/build/composables/form/serverFieldErrors.ts +49 -0
  276. package/frontend-vue/layers/dms-ui/app/build/composables/form/toasterClearance.ts +118 -0
  277. package/frontend-vue/layers/dms-ui/app/build/composables/form/useInstantForm.ts +12 -1
  278. package/frontend-vue/layers/dms-ui/app/build/composables/live/currentPageLive.ts +41 -3
  279. package/frontend-vue/layers/dms-ui/app/build/composables/live/livePoller.ts +10 -2
  280. package/frontend-vue/layers/dms-ui/app/build/composables/live/pageReadClock.ts +28 -0
  281. package/frontend-vue/layers/dms-ui/app/build/composables/live/shortcuts.ts +1 -1
  282. package/frontend-vue/layers/dms-ui/app/build/composables/live/usePageLiveHost.ts +17 -3
  283. package/frontend-vue/layers/dms-ui/app/build/composables/live/useUpdatedLabel.ts +38 -0
  284. package/frontend-vue/layers/dms-ui/app/build/composables/page-toolbar/pageToolbarContext.ts +75 -0
  285. package/frontend-vue/layers/dms-ui/app/build/composables/page-toolbar/toolbarControlHeight.ts +29 -0
  286. package/frontend-vue/layers/dms-ui/app/build/composables/period/periodComparison.ts +52 -0
  287. package/frontend-vue/layers/dms-ui/app/build/composables/period/periodDisplay.ts +21 -0
  288. package/frontend-vue/layers/dms-ui/app/build/composables/period/periodShortLabels.ts +28 -0
  289. package/frontend-vue/layers/dms-ui/app/build/composables/period/usePeriodDraft.ts +1 -0
  290. package/frontend-vue/layers/dms-ui/app/build/composables/table/useRelationFilterLabels.ts +73 -0
  291. package/frontend-vue/layers/dms-ui/app/build/composables/table/useRowScrollAnchor.ts +106 -0
  292. package/frontend-vue/layers/dms-ui/app/build/composables/table/useTable.ts +9 -0
  293. package/frontend-vue/layers/dms-ui/app/build/composables/table/useTableColumns.ts +1 -2
  294. package/frontend-vue/layers/dms-ui/app/build/composables/table/utils/archivedCount.ts +31 -0
  295. package/frontend-vue/layers/dms-ui/app/build/composables/table/utils/filterEditorValue.ts +38 -0
  296. package/frontend-vue/layers/dms-ui/app/build/composables/table/utils/formatFilterValue.ts +33 -2
  297. package/frontend-vue/layers/dms-ui/app/build/composables/table/utils/scrollEdges.ts +31 -0
  298. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useCardFields.ts +33 -0
  299. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useMobileDisplay.ts +48 -0
  300. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useScrolledPastTop.ts +41 -18
  301. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableFooter.ts +3 -2
  302. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableLinkedVisit.ts +78 -0
  303. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableLiveHold.ts +135 -22
  304. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableRows.ts +56 -25
  305. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViewRowActions.ts +77 -32
  306. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViews.ts +86 -10
  307. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/bulkActions.ts +14 -4
  308. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/card.ts +127 -2
  309. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/compareValues.ts +13 -0
  310. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/expandedRows.ts +28 -0
  311. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/groupedRows.ts +114 -1
  312. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/linkedArrival.ts +308 -0
  313. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/locateRow.ts +106 -0
  314. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/pageArrival.ts +34 -0
  315. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/pinnedRows.ts +17 -0
  316. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/quickFilters.ts +41 -33
  317. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/recordLink.ts +24 -8
  318. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/tableCounts.ts +113 -0
  319. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/tableQuery.ts +27 -4
  320. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/views.ts +22 -6
  321. package/frontend-vue/layers/dms-ui/app/build/utils/addressSuggestions.ts +49 -0
  322. package/frontend-vue/layers/dms-ui/app/build/utils/eyebrow.ts +12 -0
  323. package/frontend-vue/layers/dms-ui/app/build/utils/fieldOptionsI18n.ts +51 -11
  324. package/frontend-vue/layers/dms-ui/app/build/utils/inputWidth.ts +53 -0
  325. package/frontend-vue/layers/dms-ui/app/build/utils/numberAdjustment.ts +6 -3
  326. package/frontend-vue/layers/dms-ui/app/build/utils/relativeDate.ts +19 -5
  327. package/frontend-vue/layers/dms-ui/app/build/utils/scrollWithin.ts +99 -0
  328. package/frontend-vue/layers/dms-ui/app/build/utils/urlVariables.ts +36 -14
  329. package/frontend-vue/layers/dms-ui/app/components/PasswordStrength.vue +1 -1
  330. package/frontend-vue/layers/dms-ui/app/components/RecursiveComponent.vue +9 -1
  331. package/frontend-vue/layers/dms-ui/app/components/blocks/LiveToggleBlock.vue +80 -32
  332. package/frontend-vue/layers/dms-ui/app/components/blocks/PageToolbarBlock.vue +137 -0
  333. package/frontend-vue/layers/dms-ui/app/components/blocks/StatGroupBlock.vue +5 -0
  334. package/frontend-vue/layers/dms-ui/app/components/chart/ApexChartHost.vue +16 -2
  335. package/frontend-vue/layers/dms-ui/app/components/chart/Chart.vue +24 -5
  336. package/frontend-vue/layers/dms-ui/app/components/chart/ChartCard.vue +41 -23
  337. package/frontend-vue/layers/dms-ui/app/components/confirm/ConfirmModal.vue +1 -1
  338. package/frontend-vue/layers/dms-ui/app/components/container-footer/ContainerFooter.vue +34 -0
  339. package/frontend-vue/layers/dms-ui/app/components/field-row/FieldRow.vue +25 -2
  340. package/frontend-vue/layers/dms-ui/app/components/flow-canvas/FlowCanvas.vue +2 -2
  341. package/frontend-vue/layers/dms-ui/app/components/form/Form.vue +118 -36
  342. package/frontend-vue/layers/dms-ui/app/components/form/components/DatePicker.vue +20 -1
  343. package/frontend-vue/layers/dms-ui/app/components/form/components/Display.vue +13 -1
  344. package/frontend-vue/layers/dms-ui/app/components/form/components/Image.vue +8 -3
  345. package/frontend-vue/layers/dms-ui/app/components/form/components/InputAddress.vue +53 -5
  346. package/frontend-vue/layers/dms-ui/app/components/form/components/InputCode.vue +29 -5
  347. package/frontend-vue/layers/dms-ui/app/components/form/components/InputNumber.vue +45 -8
  348. package/frontend-vue/layers/dms-ui/app/components/form/components/InputPercentage.vue +28 -5
  349. package/frontend-vue/layers/dms-ui/app/components/form/components/InputSecret.vue +5 -0
  350. package/frontend-vue/layers/dms-ui/app/components/form/components/InputText.vue +19 -2
  351. package/frontend-vue/layers/dms-ui/app/components/form/components/InputTime.vue +2 -1
  352. package/frontend-vue/layers/dms-ui/app/components/form/components/Relation.vue +205 -65
  353. package/frontend-vue/layers/dms-ui/app/components/form/components/Repeater.vue +80 -30
  354. package/frontend-vue/layers/dms-ui/app/components/form/components/RepeaterTable.vue +203 -0
  355. package/frontend-vue/layers/dms-ui/app/components/form/components/Select.vue +4 -2
  356. package/frontend-vue/layers/dms-ui/app/components/grid/GridRow.vue +11 -2
  357. package/frontend-vue/layers/dms-ui/app/components/key-value-list/KeyValueList.vue +79 -70
  358. package/frontend-vue/layers/dms-ui/app/components/kpi/KpiCard.vue +117 -21
  359. package/frontend-vue/layers/dms-ui/app/components/meter/Meter.vue +2 -1
  360. package/frontend-vue/layers/dms-ui/app/components/period/PeriodSelector.vue +113 -34
  361. package/frontend-vue/layers/dms-ui/app/components/save-bar/SaveBar.vue +14 -2
  362. package/frontend-vue/layers/dms-ui/app/components/section-header/Eyebrow.vue +6 -1
  363. package/frontend-vue/layers/dms-ui/app/components/segmented/Segmented.vue +13 -0
  364. package/frontend-vue/layers/dms-ui/app/components/stat-group/StatGroup.vue +52 -3
  365. package/frontend-vue/layers/dms-ui/app/components/tab/Tab.vue +2 -1
  366. package/frontend-vue/layers/dms-ui/app/components/table-view/KanbanBoard.vue +45 -35
  367. package/frontend-vue/layers/dms-ui/app/components/table-view/KanbanDisplay.vue +4 -1
  368. package/frontend-vue/layers/dms-ui/app/components/table-view/TableView.vue +410 -96
  369. package/frontend-vue/layers/dms-ui/app/components/top-list/TopListCard.vue +33 -5
  370. package/frontend-vue/layers/dms-ui/app/composables/chart/apexCategoryLabels.ts +68 -0
  371. package/frontend-vue/layers/dms-ui/app/composables/chart/apexDatetimeBuckets.ts +306 -0
  372. package/frontend-vue/layers/dms-ui/app/composables/chart/formatValue.ts +4 -3
  373. package/frontend-vue/layers/dms-ui/app/composables/chart/heatmapColors.ts +93 -0
  374. package/frontend-vue/layers/dms-ui/app/composables/chart/localizeSeries.ts +62 -0
  375. package/frontend-vue/layers/dms-ui/app/composables/chart/useApexChart.ts +116 -8
  376. package/frontend-vue/layers/dms-ui/app/composables/chart/useApexChart.types.ts +7 -0
  377. package/frontend-vue/layers/dms-ui/app/composables/containers/types.ts +84 -1
  378. package/frontend-vue/layers/dms-ui/app/composables/containers/useDrawer.ts +69 -8
  379. package/frontend-vue/layers/dms-ui/app/composables/form/types/field-saved.ts +11 -0
  380. package/frontend-vue/layers/dms-ui/app/composables/form/types/field.ts +4 -0
  381. package/frontend-vue/layers/dms-ui/app/composables/form/types/functions.ts +1 -0
  382. package/frontend-vue/layers/dms-ui/app/composables/form/useForm.ts +142 -21
  383. package/frontend-vue/layers/dms-ui/app/composables/live/usePageLive.ts +11 -4
  384. package/frontend-vue/layers/dms-ui/app/composables/page-toolbar/usePageToolbarSize.ts +30 -0
  385. package/frontend-vue/layers/dms-ui/app/composables/table-view/kanban.ts +4 -2
  386. package/frontend-vue/layers/dms-ui/app/composables/table-view/types/action-target.ts +22 -3
  387. package/frontend-vue/layers/dms-ui/app/composables/table-view/types/config.ts +4 -0
  388. package/frontend-vue/layers/dms-ui/app/composables/table-view/types/display.ts +24 -1
  389. package/frontend-vue/layers/dms-ui/app/plugins/closing-layers.client.ts +10 -0
  390. package/frontend-vue/layers/dms-ui/app/plugins/live-pause-source.ts +15 -0
  391. package/frontend-vue/layers/dms-ui/app/plugins/register.ts +2 -0
  392. package/frontend-vue/layers/dms-ui/app/types/input-number.ts +3 -2
  393. package/frontend-vue/layers/dms-ui/app/types/page-toolbar.ts +7 -0
  394. package/frontend-vue/layers/dms-ui/app/types/row-action.ts +0 -2
  395. package/frontend-vue/layers/dms-ui/app/types/row-navigation.ts +17 -0
  396. package/frontend-vue/layers/dms-ui/app/types/table/query.ts +2 -1
  397. package/frontend-vue/layers/dms-ui/app/utils/blockRefresh.ts +3 -1
  398. package/frontend-vue/layers/dms-ui/app/utils/componentTree.ts +24 -0
  399. package/frontend-vue/layers/dms-ui/i18n/locales/ui-en-GB.json +16 -4
  400. package/frontend-vue/layers/dms-ui/i18n/locales/ui-fr-FR.json +16 -4
  401. package/frontend-vue/package.json +2 -2
  402. package/package.json +7 -6
  403. package/skills/dms/SKILL.md +1 -1
  404. package/skills/dms-auth/SKILL.md +5 -3
  405. package/skills/dms-best-practices/REFERENCE.md +2 -0
  406. package/skills/dms-best-practices/SKILL.md +4 -0
  407. package/skills/dms-module/SKILL.md +3 -2
  408. package/skills/dms-pages/REFERENCE.md +125 -16
  409. package/skills/dms-pages/SKILL.md +1 -1
  410. package/skills/dms-project/DEV-SERVERS.md +23 -1
  411. package/skills/dms-project/REFERENCE.md +1 -1
  412. package/skills/dms-project/SKILL.md +4 -1
  413. package/skills/dms-project/scripts/dms-dev.sh +205 -17
  414. package/frontend-vue/layers/dms-core/app/composables/period/periodRenderNow.ts +0 -42
@@ -90,10 +90,20 @@ export interface BulkActionOutcome {
90
90
 
91
91
  // The count each bulk route answers with: delete sends the number of deleted
92
92
  // rows (or `{ deleted }` when a row rule filtered out every id), archive and
93
- // restore send `{ archivedCount }` / `{ restoredCount }`.
94
- const COUNT_KEYS = ["deleted", "archivedCount", "restoredCount"] as const;
93
+ // restore send `{ archivedCount }` / `{ restoredCount }`, and a custom bulk
94
+ // route `{ count }`.
95
+ const COUNT_KEYS = [
96
+ "count",
97
+ "deleted",
98
+ "archivedCount",
99
+ "restoredCount",
100
+ ] as const;
95
101
 
96
- const readCount = (response: unknown): number | undefined => {
102
+ /**
103
+ * The number of rows a bulk route says it changed: a bare number or one of
104
+ * its count keys, `undefined` when it does not say.
105
+ */
106
+ export const readBulkCount = (response: unknown): number | undefined => {
97
107
  if (typeof response === "number") return response;
98
108
  if (!response || typeof response !== "object") return undefined;
99
109
  for (const key of COUNT_KEYS) {
@@ -112,7 +122,7 @@ export function bulkActionOutcome(
112
122
  response: unknown,
113
123
  requested: number,
114
124
  ): BulkActionOutcome {
115
- const count = readCount(response);
125
+ const count = readBulkCount(response);
116
126
  const processed =
117
127
  count === undefined ? requested : Math.min(Math.max(count, 0), requested);
118
128
  return { processed, skipped: requested - processed };
@@ -1,3 +1,4 @@
1
+ import type { CSSProperties } from "vue";
1
2
  import { get } from "@nuxt/ui/runtime/utils/index.js";
2
3
  import type {
3
4
  TableViewCardProps,
@@ -68,8 +69,132 @@ export function cardFieldColumns(
68
69
  ]);
69
70
  return columns
70
71
  .filter(
71
- (column) =>
72
- column.listable !== false && !skipped.has(column.accessorKey),
72
+ (column) => column.listable !== false && !skipped.has(column.accessorKey),
73
73
  )
74
74
  .slice(0, FALLBACK_CARD_FIELD_COUNT);
75
75
  }
76
+
77
+ /** The id of the data type drawing a name with its avatar and subtitle. */
78
+ const IDENTITY_DISPLAY_TYPE = "identity";
79
+
80
+ /** The database id: a technical key, never worth a line on a card. */
81
+ const DATABASE_ID_KEY = "_id";
82
+
83
+ /**
84
+ * What a built-in card reads off its row besides its title: the subtitle and
85
+ * the avatar the title column's `identity` display declares, so a card shows
86
+ * the row as its grid cell does.
87
+ */
88
+ export interface CardIdentity {
89
+ /** Row field drawn under the title. */
90
+ subtitleField?: string;
91
+ /** Row field holding the avatar: an image value or a URL. */
92
+ avatarField?: string;
93
+ /** File storage the avatar is read from. */
94
+ storage?: string;
95
+ }
96
+
97
+ const optionString = (
98
+ options: Record<string, unknown> | undefined,
99
+ key: string,
100
+ ): string | undefined =>
101
+ typeof options?.[key] === "string" ? options[key] : undefined;
102
+
103
+ /** The identity the title column (`labelKey`) declares, if it is one. */
104
+ export function cardIdentity(
105
+ columns: TableViewColumn[],
106
+ labelKey: string | undefined,
107
+ ): CardIdentity {
108
+ if (!labelKey) return {};
109
+ const display = columns.find(
110
+ (column) => column.accessorKey === labelKey,
111
+ )?.display;
112
+ if (display?.type !== IDENTITY_DISPLAY_TYPE) return {};
113
+ return {
114
+ subtitleField: optionString(display.options, "subtitleField"),
115
+ avatarField: optionString(display.options, "avatarField"),
116
+ storage: optionString(display.options, "storage"),
117
+ };
118
+ }
119
+
120
+ const hasValue = (value: unknown): boolean =>
121
+ value !== undefined && value !== null && value !== "";
122
+
123
+ /**
124
+ * The line a card draws under its title, as the row holds it: the identity's
125
+ * subtitle, else the row's id when the table names it with a key of its own
126
+ * (`rowIdKey: "code"`) and the title is not it already. The database `_id`
127
+ * is never shown: a UUID or `proj-074` tells the reader nothing.
128
+ */
129
+ export function cardSubtitle<T>(
130
+ item: T,
131
+ identity: CardIdentity,
132
+ shown: CardShownKeys,
133
+ ): unknown {
134
+ const row = item as Record<string, unknown>;
135
+ if (identity.subtitleField) {
136
+ const subtitle: unknown = get(row, identity.subtitleField);
137
+ if (hasValue(subtitle)) return subtitle;
138
+ }
139
+ if (shown.rowIdKey === DATABASE_ID_KEY) return undefined;
140
+ if (!shown.labelKey || !hasValue(get(row, shown.labelKey))) return undefined;
141
+ const id = cardRowId(item, shown.rowIdKey);
142
+ return id || undefined;
143
+ }
144
+
145
+ /** The avatar a card draws in place of the title's initials, if any. */
146
+ export function cardAvatar<T>(
147
+ item: T,
148
+ identity: CardIdentity,
149
+ ): { key: string } | string | undefined {
150
+ if (!identity.avatarField) return undefined;
151
+ const avatar: unknown = get(
152
+ item as Record<string, unknown>,
153
+ identity.avatarField,
154
+ );
155
+ if (typeof avatar === "string") return avatar || undefined;
156
+ if (avatar && typeof avatar === "object" && "key" in avatar) {
157
+ const { key } = avatar as { key: unknown };
158
+ return typeof key === "string" && key ? { key } : undefined;
159
+ }
160
+ return undefined;
161
+ }
162
+
163
+ /** The narrowest a card of the `cards` display gets (backend `card.minWidth`). */
164
+ export const DEFAULT_CARD_MIN_WIDTH = "230px";
165
+
166
+ // The grid's gap and side padding, as `CARD_GRID_CLASS` sets them: the width
167
+ // cap below adds them to the cards' own.
168
+ const CARD_GRID_GAP = "0.75rem";
169
+ const CARD_GRID_PADDING = "36px";
170
+
171
+ // How much wider than `card.minWidth` a card may stretch when the row has room
172
+ // for more cards than the list holds. A full row hardly reaches it: n cards
173
+ // fit, but not n + 1, so each one stays under twice the minimum, give or take
174
+ // a gap.
175
+ const CARD_MAX_WIDTH_FACTOR = 2;
176
+
177
+ /**
178
+ * The cards grid: auto-fit tracks of at least `--dms-card-min-width` (the
179
+ * whole row on a narrower screen), so a short list's empty tracks collapse
180
+ * and its cards stretch across the row instead of leaving a hole at its end.
181
+ */
182
+ export const CARD_GRID_CLASS =
183
+ "grid grid-cols-[repeat(auto-fit,minmax(min(var(--dms-card-min-width),100%),1fr))] gap-3 px-[18px] pt-4 pb-[18px]";
184
+
185
+ /**
186
+ * The style of a cards grid holding `count` cards of at least `minWidth` (a
187
+ * CSS length): the width its tracks start from, and a cap keeping one or two
188
+ * cards from stretching across a whole wide screen.
189
+ */
190
+ export function cardGridStyle(
191
+ minWidth: string | undefined,
192
+ count: number,
193
+ ): CSSProperties {
194
+ const width = minWidth || DEFAULT_CARD_MIN_WIDTH;
195
+ const cards = Math.max(1, count);
196
+ return {
197
+ "--dms-card-min-width": width,
198
+ maxWidth: `calc(${cards} * ${CARD_MAX_WIDTH_FACTOR} * ${width} + ${cards - 1} * ${CARD_GRID_GAP} + ${CARD_GRID_PADDING})`,
199
+ };
200
+ }
@@ -0,0 +1,13 @@
1
+ /** The text of a cell value, as the browser searches and sorts it. */
2
+ export const textOf = (value: unknown): string =>
3
+ value === null || value === undefined
4
+ ? ""
5
+ : typeof value === "object"
6
+ ? JSON.stringify(value)
7
+ : String(value);
8
+
9
+ /** Orders two cell values the browser sorts: numbers by value, else as text. */
10
+ export const compareValues = (a: unknown, b: unknown): number => {
11
+ if (typeof a === "number" && typeof b === "number") return a - b;
12
+ return textOf(a).localeCompare(textOf(b), undefined, { numeric: true });
13
+ };
@@ -49,3 +49,31 @@ export function keepListedRows(
49
49
  Object.entries(expanded).filter(([id]) => listed.has(id)),
50
50
  );
51
51
  }
52
+
53
+ /** A table's open rows, and the listed set (its query) they belong to. */
54
+ export interface ExpandedRowsState {
55
+ expanded: ExpandedRowMap;
56
+ listedKey?: string;
57
+ }
58
+
59
+ /**
60
+ * The open rows once the listed rows change: a newly listed set (another
61
+ * `listedKey`: page, filter, search, sort, tab, archive view) opens with
62
+ * `defaultExpanded`; a refresh of the same set keeps what the user opened,
63
+ * minus rows no longer listed. Rows that have not arrived yet (`undefined`,
64
+ * the list still loading, as on a first load) list no set: the default waits
65
+ * for them instead of being spent on an empty table.
66
+ */
67
+ export function expandedRowsOnListing(
68
+ state: ExpandedRowsState,
69
+ rowIds: string[] | undefined,
70
+ listedKey: string,
71
+ mode?: ExpandedDefault,
72
+ single?: boolean,
73
+ ): ExpandedRowsState {
74
+ if (!rowIds) return state;
75
+ if (listedKey === state.listedKey) {
76
+ return { listedKey, expanded: keepListedRows(state.expanded, rowIds) };
77
+ }
78
+ return { listedKey, expanded: defaultExpandedRows(rowIds, mode, single) };
79
+ }
@@ -1,4 +1,5 @@
1
- import type { SortingState } from "@tanstack/vue-table";
1
+ import { get } from "@nuxt/ui/runtime/utils/index.js";
2
+ import type { ColumnSort, SortingState } from "@tanstack/vue-table";
2
3
  import {
3
4
  regionalDateTimeFormat,
4
5
  regionalDayKey,
@@ -13,6 +14,7 @@ import type {
13
14
  TableViewGroupedConfig,
14
15
  } from "../../../../composables/table-view/types";
15
16
  import type { TableFilter } from "../../../components/table/Table.vue";
17
+ import { compareValues } from "./compareValues";
16
18
  import { sortValueKind } from "./sortableColumns";
17
19
 
18
20
  /** Key of the group of rows without a value. */
@@ -146,6 +148,91 @@ export function groupedSorting(
146
148
  return [{ id: groupByField, desc: own ? !!own.desc : by !== "value" }];
147
149
  }
148
150
 
151
+ /**
152
+ * The sort a group's rows follow on the grouped display. The list route sorts
153
+ * on one field, the grouped column, so the sort picked on another column —
154
+ * by the user or a view, or the hand order of a `reorder` table — orders each
155
+ * group's rows in the browser instead, over the page listed.
156
+ */
157
+ export function withinGroupSort(
158
+ picked: SortingState,
159
+ grouped: TableViewGroupedConfig,
160
+ ): ColumnSort | undefined {
161
+ return picked.find((entry) => entry.id !== grouped.groupByField);
162
+ }
163
+
164
+ /** What decides the order a table view lists its rows in. */
165
+ export interface TableSortingInput {
166
+ /** The order field of a hand-ordered table (`reorder`). */
167
+ reorderField?: string;
168
+ /** The grouped options, while the grouped display shows. */
169
+ grouped?: TableViewGroupedConfig;
170
+ /** The display shown offers the sort picker. */
171
+ canSort: boolean;
172
+ /** The sort the user picked or a view opened with. */
173
+ sorting: SortingState;
174
+ /** The table's default sort. */
175
+ defaultSorting: SortingState;
176
+ }
177
+
178
+ /**
179
+ * The order the rows are wanted in: the hand order of a `reorder` table,
180
+ * else the picked sort. The grouped display hides the sort picker but keeps
181
+ * the sort a view opened it with: it orders the rows of each group.
182
+ */
183
+ export function pickedTableSorting(input: TableSortingInput): SortingState {
184
+ if (input.reorderField) return [{ id: input.reorderField, desc: false }];
185
+ return input.canSort || input.grouped ? input.sorting : input.defaultSorting;
186
+ }
187
+
188
+ /**
189
+ * The sort the list route is asked for (the route sorts on one field): on the
190
+ * grouped display, the grouped column, so that a group's rows follow each
191
+ * other — the picked order then sorts each group, see
192
+ * {@link orderGroupedRows}; else the picked order.
193
+ */
194
+ export function listTableSorting(input: TableSortingInput): SortingState {
195
+ const picked = pickedTableSorting(input);
196
+ if (input.grouped) return groupedSorting(picked, input.grouped);
197
+ return picked;
198
+ }
199
+
200
+ /**
201
+ * The rows of a page listed on the grouped column, each group's rows in the
202
+ * order `picked` gives them (see {@link withinGroupSort}). The groups keep
203
+ * the order the list gave them; rows the sort cannot tell apart keep theirs.
204
+ */
205
+ export function orderGroupedRows<T>(
206
+ rows: T[],
207
+ grouped: TableViewGroupedConfig,
208
+ picked: SortingState,
209
+ locale: string,
210
+ ): T[] {
211
+ const sort = withinGroupSort(picked, grouped);
212
+ if (!sort) return rows;
213
+ const by = grouped.by ?? "value";
214
+ const groupOf = new Map<string, number>();
215
+ const entries = rows.map((row, index) => {
216
+ const key = rowGroupKey(
217
+ get(row as Record<string, unknown>, grouped.groupByField),
218
+ by,
219
+ locale,
220
+ );
221
+ if (!groupOf.has(key)) groupOf.set(key, groupOf.size);
222
+ return { row, index, group: groupOf.get(key)! };
223
+ });
224
+ const direction = sort.desc ? -1 : 1;
225
+ const valueOf = (row: T) => get(row as Record<string, unknown>, sort.id);
226
+ return entries
227
+ .sort(
228
+ (a, b) =>
229
+ a.group - b.group ||
230
+ direction * compareValues(valueOf(a.row), valueOf(b.row)) ||
231
+ a.index - b.index,
232
+ )
233
+ .map((entry) => entry.row);
234
+ }
235
+
149
236
  /** How a date column picked in the options menu is cut, unless configured. */
150
237
  const DEFAULT_DATE_GROUP_BY: TableViewGroupBy = "day";
151
238
 
@@ -178,3 +265,29 @@ export function groupedOptionsFor(
178
265
  grouped.by && grouped.by !== "value" ? grouped.by : DEFAULT_DATE_GROUP_BY;
179
266
  return { ...grouped, groupByField: field, by };
180
267
  }
268
+
269
+ /** The rows of one group, under its key. */
270
+ export interface RowGroup<T> {
271
+ key: string;
272
+ rows: T[];
273
+ }
274
+
275
+ /**
276
+ * The rows filed under their group, each group once, in the order its first
277
+ * row comes. The list is sorted on the grouped column, but rows listed in
278
+ * another order — the previous sort, still shown while the grouped one loads
279
+ * — must not open a group twice: two headers under one key.
280
+ */
281
+ export function groupRows<T>(
282
+ rows: readonly T[],
283
+ keyOf: (row: T) => string,
284
+ ): RowGroup<T>[] {
285
+ const groups = new Map<string, T[]>();
286
+ for (const row of rows) {
287
+ const key = keyOf(row);
288
+ const group = groups.get(key);
289
+ if (group) group.push(row);
290
+ else groups.set(key, [row]);
291
+ }
292
+ return [...groups].map(([key, grouped]) => ({ key, rows: grouped }));
293
+ }
@@ -0,0 +1,308 @@
1
+ import { nextTick } from "vue";
2
+ import {
3
+ hashText,
4
+ type TableField,
5
+ } from "#dms-core/app/build/composables/user/tablePreferences";
6
+ import type { QueryParamFilters } from "../../../../composables/table-view/types/config";
7
+ import type { TablePreferences } from "../useTablePreferences";
8
+ import { RECORD_URL_KEY } from "./recordLink";
9
+ import { readTableUrlKey, type TableUrlScope, writeTableUrlKey } from "./views";
10
+
11
+ /**
12
+ * The kept fields that decide which rows a table lists: a link that names
13
+ * part of that state replaces all of it, not only what it names.
14
+ */
15
+ const LINKED_STATE_FIELDS: ReadonlySet<TableField> = new Set<TableField>([
16
+ "activeView",
17
+ "activeTab",
18
+ "columnFilters",
19
+ "sorting",
20
+ "pagination",
21
+ "paginationScope",
22
+ ]);
23
+
24
+ /**
25
+ * The listing choices a user makes on a link's table (a view, a tab, a
26
+ * filter, a sort): the first one makes the state shown their own.
27
+ */
28
+ const USER_CHOICE_FIELDS: ReadonlySet<TableField> = new Set<TableField>([
29
+ "activeView",
30
+ "activeTab",
31
+ "columnFilters",
32
+ "sorting",
33
+ ]);
34
+
35
+ const VIEW_URL_KEY = "view";
36
+ const TAB_URL_KEY = "tab";
37
+
38
+ /**
39
+ * What `query` names as the table's view: an id, `""` for no view at all
40
+ * (`?view=`), or `undefined` when it names none.
41
+ */
42
+ const namedView = (
43
+ query: Record<string, unknown>,
44
+ urlScope: TableUrlScope,
45
+ ): string | undefined => {
46
+ const reading = readTableUrlKey(query, urlScope, VIEW_URL_KEY);
47
+ return reading.isCleared ? "" : reading.value;
48
+ };
49
+
50
+ /** What the URL a table arrives on may name, and how it was reached. */
51
+ export interface TableLinkArrival {
52
+ query: Record<string, unknown>;
53
+ urlScope: TableUrlScope;
54
+ queryParamFilters: QueryParamFilters | undefined;
55
+ /** The page was reached through a visit, not loaded or gone back to. */
56
+ isVisit: boolean;
57
+ /** The tab kept from the last visit. */
58
+ rememberedTab: string;
59
+ /** The URL filters the kept state was chosen under (`urlFilterScope`). */
60
+ keptLinkScope: string | undefined;
61
+ /** The link the table last showed, unclaimed by the user (`linkKeyScope`). */
62
+ shownLink: string | undefined;
63
+ }
64
+
65
+ const namesUrlFilter = (
66
+ query: Record<string, unknown>,
67
+ queryParamFilters: QueryParamFilters | undefined,
68
+ isNamed: (param: string) => boolean,
69
+ ): boolean =>
70
+ Object.keys(queryParamFilters ?? {}).some(
71
+ (param) => query[param] !== undefined && isNamed(param),
72
+ );
73
+
74
+ /**
75
+ * The `queryParamFilters` values `query` carries, as one key, or `undefined`
76
+ * when it carries none: what a kept state chosen under them is tied to.
77
+ */
78
+ export function urlFilterScope(
79
+ query: Record<string, unknown>,
80
+ queryParamFilters: QueryParamFilters | undefined,
81
+ ): string | undefined {
82
+ const values = Object.keys(queryParamFilters ?? {})
83
+ .filter((param) => query[param] !== undefined)
84
+ .sort()
85
+ .map((param) => [param, query[param]]);
86
+ return values.length > 0 ? hashText(JSON.stringify(values)) : undefined;
87
+ }
88
+
89
+ /**
90
+ * What `query` names for the table (its view, tab and row, and the filters
91
+ * declared in `queryParamFilters`), as one key, or `undefined` when it names
92
+ * none of them: a URL spelling a key short or prefixed reads the same.
93
+ */
94
+ export function linkKeyScope(
95
+ query: Record<string, unknown>,
96
+ urlScope: TableUrlScope,
97
+ queryParamFilters: QueryParamFilters | undefined,
98
+ ): string | undefined {
99
+ const named = [
100
+ [VIEW_URL_KEY, namedView(query, urlScope)],
101
+ ...[TAB_URL_KEY, RECORD_URL_KEY].map((key) => [
102
+ key,
103
+ readTableUrlKey(query, urlScope, key).value,
104
+ ]),
105
+ ].filter(([, value]) => value !== undefined);
106
+ const filters = Object.keys(queryParamFilters ?? {})
107
+ .filter((param) => query[param] !== undefined)
108
+ .sort()
109
+ .map((param) => [`?${param}`, query[param]]);
110
+ const values = [...named, ...filters];
111
+ return values.length > 0 ? hashText(JSON.stringify(values)) : undefined;
112
+ }
113
+
114
+ /**
115
+ * Whether the table was reached through a link that names what it lists: a
116
+ * view, a record, a filter declared in `queryParamFilters`, or a tab. Reached
117
+ * through a visit (a link clicked, a notification), it always is, even when
118
+ * what it names is what the user left. Loaded in full (a reload) or gone back
119
+ * to, the URL may be the one the table wrote for the user's own choices: it
120
+ * is a link again when it is the link the table last showed, and when it
121
+ * names a view or a record, or a tab or URL filters other than the kept
122
+ * ones. A server render cannot tell a reload from a URL typed or opened in a
123
+ * new browser tab, so a full load naming only the kept tab reads as one.
124
+ */
125
+ export function isLinkedArrival(arrival: TableLinkArrival): boolean {
126
+ const { query, urlScope, queryParamFilters } = arrival;
127
+ const linkScope = linkKeyScope(query, urlScope, queryParamFilters);
128
+ if (linkScope === undefined) return false;
129
+ if (arrival.isVisit || linkScope === arrival.shownLink) return true;
130
+ const names = (key: string) => !!readTableUrlKey(query, urlScope, key).value;
131
+ if (namedView(query, urlScope) !== undefined || names(RECORD_URL_KEY)) {
132
+ return true;
133
+ }
134
+ const filterScope = urlFilterScope(query, queryParamFilters);
135
+ if (filterScope !== undefined && filterScope !== arrival.keptLinkScope) {
136
+ return true;
137
+ }
138
+ const tab = readTableUrlKey(query, urlScope, TAB_URL_KEY).value;
139
+ return tab !== undefined && tab !== arrival.rememberedTab;
140
+ }
141
+
142
+ /**
143
+ * The table's preferences as a link arrival reads them: the listing state
144
+ * (`LINKED_STATE_FIELDS`) reads as its defaults, so the link applies over
145
+ * the table's own default state rather than over the last visit's. The
146
+ * kept state is only replaced once the user makes the state shown their own
147
+ * (`linkFollowingPreferences`).
148
+ */
149
+ export function linkedArrivalPreferences(
150
+ preferences: TablePreferences,
151
+ ): TablePreferences {
152
+ return {
153
+ ...preferences,
154
+ get: (field, defaultValue) =>
155
+ LINKED_STATE_FIELDS.has(field)
156
+ ? defaultValue
157
+ : preferences.get(field, defaultValue),
158
+ };
159
+ }
160
+
161
+ /** A visit to the page the table is already shown on. */
162
+ export interface TableLinkVisit {
163
+ query: Record<string, unknown>;
164
+ /**
165
+ * A link pushed on the page (one clicked, a notification): it is followed
166
+ * even when it names what the table shows.
167
+ */
168
+ isLinkClick: boolean;
169
+ /** The query of the visit the table was last reached through. */
170
+ previousQuery: Record<string, unknown>;
171
+ urlScope: TableUrlScope;
172
+ queryParamFilters: QueryParamFilters | undefined;
173
+ /** The view and the tab the table shows. */
174
+ activeView: string | undefined;
175
+ activeTab: string;
176
+ canOpenView: (id: string) => boolean;
177
+ canOpenTab: (id: string) => boolean;
178
+ }
179
+
180
+ /**
181
+ * Whether a visit that keeps the table mounted (a link to the page already
182
+ * shown, back and forward) is a link to follow as an arrival would. A link
183
+ * clicked on the page counts whenever it names a view or a tab the table can
184
+ * open, or a URL filter, even what the table already shows: the state around
185
+ * it may hide it. Otherwise (back and forward), the view and the tab are
186
+ * written or kept in place, so they count when they differ from what the
187
+ * table shows; a URL filter when it differs from the last visit. A row named
188
+ * (`?record=`) is opened on the table as it stands, its page included: it is
189
+ * no listing link, and a link naming one counts only for a view, a tab or a
190
+ * URL filter that changes what the table lists. A visit repeating what the
191
+ * table already reads (the one that mounted it) is not a link.
192
+ */
193
+ export function isLinkedVisit(visit: TableLinkVisit): boolean {
194
+ const { query, previousQuery, urlScope, isLinkClick } = visit;
195
+ const read = (from: Record<string, unknown>, key: string) =>
196
+ readTableUrlKey(from, urlScope, key).value;
197
+ // A link clicked counts even when it names what is already there, unless
198
+ // it opens a row: the table it is opened on stays as it stands.
199
+ const isRowLink = read(query, RECORD_URL_KEY) !== undefined;
200
+ const isNew = (value: unknown, current: unknown) =>
201
+ (isLinkClick && !isRowLink) || value !== current;
202
+ const view = namedView(query, urlScope);
203
+ if (view === "" && isNew(undefined, visit.activeView)) return true;
204
+ if (view && isNew(view, visit.activeView) && visit.canOpenView(view)) {
205
+ return true;
206
+ }
207
+ const tab = read(query, TAB_URL_KEY);
208
+ if (tab && isNew(tab, visit.activeTab) && visit.canOpenTab(tab)) return true;
209
+ return namesUrlFilter(query, visit.queryParamFilters, (param) =>
210
+ isNew(query[param], previousQuery[param]),
211
+ );
212
+ }
213
+
214
+ /** A kept field of the listing state, with its value shown and default. */
215
+ export type KeptListingField = [
216
+ field: TableField,
217
+ value: unknown,
218
+ defaultValue: unknown,
219
+ ];
220
+
221
+ /** What `linkFollowingPreferences` reads the table's link state from. */
222
+ export interface LinkFollowingOptions {
223
+ /** The table arrived on a link (`isLinkedArrival`). */
224
+ isLinkedArrival: boolean;
225
+ urlScope: TableUrlScope;
226
+ /** The URL filters the table lists under now (`urlFilterScope`). */
227
+ linkScope: () => string | undefined;
228
+ /** What the URL shown names for the table now (`linkKeyScope`). */
229
+ linkKeys: () => string | undefined;
230
+ /** The listing state shown, as it is kept (`LINKED_STATE_FIELDS`). */
231
+ listingState: () => KeptListingField[];
232
+ }
233
+
234
+ /** Preferences that leave a link's listing state unkept until it is the user's. */
235
+ export interface LinkFollowingPreferences extends TablePreferences {
236
+ /**
237
+ * Runs `change`, which opens a link on the table already shown: as on a
238
+ * linked arrival, the state it sets is not kept.
239
+ */
240
+ followLink: (change: () => void) => Promise<void>;
241
+ /** Called once the table is mounted: what changes from then is the user's. */
242
+ settleArrival: () => Promise<void>;
243
+ }
244
+
245
+ /**
246
+ * Wraps the table's preferences so the listing state a link opens (on
247
+ * arrival or on the table already shown) does not overwrite the kept one,
248
+ * which stays for the next plain visit. The table keeps its state from
249
+ * watchers, which run on the next flush: what the link and the arrival set
250
+ * is settled a tick later. The user's first choice from there (a view, a
251
+ * tab, a filter, a sort) makes the state shown theirs: it is kept whole,
252
+ * with the URL filters it was chosen under, and the view the link named
253
+ * leaves the URL, which then reads as a plain visit on reload. Until then,
254
+ * the link shown is kept too (`shownLink`), so a reload shows it again.
255
+ */
256
+ export function linkFollowingPreferences(
257
+ preferences: TablePreferences,
258
+ options: LinkFollowingOptions,
259
+ ): LinkFollowingPreferences {
260
+ let isShowingLink = options.isLinkedArrival;
261
+ let isSettled = false;
262
+
263
+ const keepLinkScope = () => {
264
+ preferences.set("linkScope", options.linkScope(), undefined);
265
+ preferences.set("shownLink", undefined, undefined);
266
+ };
267
+ const keepShownLink = () =>
268
+ preferences.set("shownLink", options.linkKeys(), undefined);
269
+
270
+ const takeOver = () => {
271
+ isShowingLink = false;
272
+ for (const [field, value, defaultValue] of options.listingState()) {
273
+ preferences.set(field, value, defaultValue);
274
+ }
275
+ keepLinkScope();
276
+ writeTableUrlKey(options.urlScope, VIEW_URL_KEY);
277
+ };
278
+
279
+ return {
280
+ ...preferences,
281
+ set: (field, value, defaultValue) => {
282
+ if (!LINKED_STATE_FIELDS.has(field)) {
283
+ preferences.set(field, value, defaultValue);
284
+ } else if (!isShowingLink) {
285
+ preferences.set(field, value, defaultValue);
286
+ keepLinkScope();
287
+ } else if (isSettled && USER_CHOICE_FIELDS.has(field)) {
288
+ takeOver();
289
+ }
290
+ },
291
+ followLink: async (change) => {
292
+ isShowingLink = true;
293
+ isSettled = false;
294
+ try {
295
+ change();
296
+ await nextTick();
297
+ keepShownLink();
298
+ } finally {
299
+ isSettled = true;
300
+ }
301
+ },
302
+ settleArrival: async () => {
303
+ await nextTick();
304
+ if (isShowingLink) keepShownLink();
305
+ isSettled = true;
306
+ },
307
+ };
308
+ }