@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.
- package/CHANGELOG.md +102 -0
- package/README.md +1 -1
- package/data/module-store/catalog.json +2 -2
- package/dist/attachments/table-view.js +2 -2
- package/dist/attachments/table-view.js.map +1 -1
- package/dist/automation/actions.d.ts +2 -0
- package/dist/automation/actions.js +116 -71
- package/dist/automation/actions.js.map +1 -1
- package/dist/automation/i18n.d.ts +4 -0
- package/dist/automation/i18n.js +19 -0
- package/dist/automation/i18n.js.map +1 -0
- package/dist/automation/send-notification.d.ts +21 -0
- package/dist/automation/send-notification.js +163 -0
- package/dist/automation/send-notification.js.map +1 -0
- package/dist/automation/triggers.js +19 -25
- package/dist/automation/triggers.js.map +1 -1
- package/dist/client-origin-cors.d.ts +1 -0
- package/dist/client-origin-cors.js +19 -0
- package/dist/client-origin-cors.js.map +1 -0
- package/dist/config.d.ts +3 -0
- package/dist/config.js +5 -3
- package/dist/config.js.map +1 -1
- package/dist/frontend-module.d.ts +4 -0
- package/dist/frontend-module.js +37 -0
- package/dist/frontend-module.js.map +1 -0
- package/dist/implementations/dms/client-ip.js +1 -1
- package/dist/implementations/dms/client-ip.js.map +1 -1
- package/dist/implementations/dms/dev-reload.d.ts +5 -0
- package/dist/implementations/dms/dev-reload.js +23 -3
- package/dist/implementations/dms/dev-reload.js.map +1 -1
- package/dist/implementations/dms/frontend-modules.d.ts +2 -4
- package/dist/implementations/dms/frontend-modules.js +22 -89
- package/dist/implementations/dms/frontend-modules.js.map +1 -1
- package/dist/implementations/dms/frontend-ready.d.ts +12 -2
- package/dist/implementations/dms/frontend-ready.js +34 -4
- package/dist/implementations/dms/frontend-ready.js.map +1 -1
- package/dist/implementations/dms/layout-banners.d.ts +2 -1
- package/dist/implementations/dms/layout-banners.js +9 -0
- package/dist/implementations/dms/layout-banners.js.map +1 -1
- package/dist/implementations/dms/manifest.d.ts +1 -2
- package/dist/implementations/dms/manifest.js +2 -0
- package/dist/implementations/dms/manifest.js.map +1 -1
- package/dist/implementations/dms/page.d.ts +4 -3
- package/dist/implementations/dms/page.js +49 -18
- package/dist/implementations/dms/page.js.map +1 -1
- package/dist/implementations/dms/permissions-resolver.d.ts +2 -2
- package/dist/implementations/dms/permissions-resolver.js +2 -2
- package/dist/implementations/dms/permissions-resolver.js.map +1 -1
- package/dist/implementations/dms/realtime.js +1 -0
- package/dist/implementations/dms/realtime.js.map +1 -1
- package/dist/implementations/dms-base/bulk-selection.d.ts +1 -1
- package/dist/implementations/dms-base/bulk-selection.js +23 -1
- package/dist/implementations/dms-base/bulk-selection.js.map +1 -1
- package/dist/implementations/dms-base/filter-tokens.js +5 -3
- package/dist/implementations/dms-base/filter-tokens.js.map +1 -1
- package/dist/implementations/dms-base/search-route.d.ts +1 -0
- package/dist/implementations/dms-base/search-route.js +35 -13
- package/dist/implementations/dms-base/search-route.js.map +1 -1
- package/dist/implementations/dms-base/table-view.js +11 -6
- package/dist/implementations/dms-base/table-view.js.map +1 -1
- package/dist/implementations/dms-notifications/index.d.ts +2 -2
- package/dist/implementations/dms-notifications/index.js +5 -5
- package/dist/implementations/dms-notifications/index.js.map +1 -1
- package/dist/index.js +28 -48
- package/dist/index.js.map +1 -1
- package/dist/pages/modules/index.d.ts +1 -0
- package/dist/pages/modules/index.js +1 -0
- package/dist/pages/modules/index.js.map +1 -1
- package/dist/pages/modules/modules-store.d.ts +9 -0
- package/dist/pages/modules/modules-store.js +30 -0
- package/dist/pages/modules/modules-store.js.map +1 -0
- package/dist/pages/settings/users/account-credentials.js +2 -2
- package/dist/pages/settings/users/account-credentials.js.map +1 -1
- package/dist/pages/settings/users/account-data-store.js +25 -8
- package/dist/pages/settings/users/account-data-store.js.map +1 -1
- package/dist/pages/settings/users/invite-extension-routes.js +9 -3
- package/dist/pages/settings/users/invite-extension-routes.js.map +1 -1
- package/dist/pages/settings/users/member-management.js +13 -14
- package/dist/pages/settings/users/member-management.js.map +1 -1
- package/dist/pages/settings/users/profile.d.ts +1 -4
- package/dist/pages/settings/users/profile.js +0 -35
- package/dist/pages/settings/users/profile.js.map +1 -1
- package/dist/pages/settings/users/role-editor-store.js +26 -18
- package/dist/pages/settings/users/role-editor-store.js.map +1 -1
- package/dist/pages/settings/users/role-editor.d.ts +0 -1
- package/dist/pages/settings/users/role-editor.js +1 -2
- package/dist/pages/settings/users/role-editor.js.map +1 -1
- package/dist/pages/settings/users/role-preview.js +1 -2
- package/dist/pages/settings/users/role-preview.js.map +1 -1
- package/dist/pages/settings/users/roles.js +2 -2
- package/dist/pages/settings/users/roles.js.map +1 -1
- package/dist/pages/settings/users/two-factor-operations.js.map +1 -1
- package/dist/realtime/broker.d.ts +6 -0
- package/dist/realtime/broker.js +8 -1
- package/dist/realtime/broker.js.map +1 -1
- package/dist/realtime/presence.d.ts +8 -3
- package/dist/realtime/presence.js +8 -4
- package/dist/realtime/presence.js.map +1 -1
- package/dist/realtime/sessions.d.ts +11 -1
- package/dist/realtime/sessions.js +21 -3
- package/dist/realtime/sessions.js.map +1 -1
- package/dist/realtime/table-view.d.ts +2 -2
- package/dist/realtime/table-view.js +4 -3
- package/dist/realtime/table-view.js.map +1 -1
- package/dist/routes/auth/client-ip.d.ts +1 -0
- package/dist/routes/auth/client-ip.js +18 -0
- package/dist/routes/auth/client-ip.js.map +1 -1
- package/dist/routes/auth/oauth/config.d.ts +2 -1
- package/dist/routes/auth/oauth/config.js +5 -1
- package/dist/routes/auth/oauth/config.js.map +1 -1
- package/dist/routes/auth/session-handoff.d.ts +0 -1
- package/dist/routes/auth/session-handoff.js +0 -2
- package/dist/routes/auth/session-handoff.js.map +1 -1
- package/dist/routes/auth.js +3 -2
- package/dist/routes/auth.js.map +1 -1
- package/dist/routes/index.d.ts +0 -1
- package/dist/routes/index.js +0 -1
- package/dist/routes/index.js.map +1 -1
- package/dist/routes/realtime.d.ts +13 -1
- package/dist/routes/realtime.js +114 -29
- package/dist/routes/realtime.js.map +1 -1
- package/dist/startup-config.d.ts +2 -0
- package/dist/startup-config.js +62 -0
- package/dist/startup-config.js.map +1 -0
- package/dist/store/interfaces.d.ts +3 -2
- package/dist/store/interfaces.js +16 -6
- package/dist/store/interfaces.js.map +1 -1
- package/dist/store/listing.js +24 -13
- package/dist/store/listing.js.map +1 -1
- package/dist/store/operations.js +4 -0
- package/dist/store/operations.js.map +1 -1
- package/dist/store/requirements.js +3 -2
- package/dist/store/requirements.js.map +1 -1
- package/dist/store/runtime.d.ts +10 -1
- package/dist/store/runtime.js +33 -6
- package/dist/store/runtime.js.map +1 -1
- package/dist/store/types.d.ts +10 -0
- package/dist/validation/auth.schema.d.ts +2 -2
- package/dist/validation/member-invite.schema.d.ts +2 -10
- package/dist/validation/member-invite.schema.js +2 -15
- package/dist/validation/member-invite.schema.js.map +1 -1
- package/frontend-vue/dms.frontend.ts +24 -5
- package/frontend-vue/dms.server.ts +17 -0
- package/frontend-vue/layers/dms-auth/server/accounts.ts +3 -4
- package/frontend-vue/layers/dms-core/app/build/composables/renderNow.ts +34 -0
- package/frontend-vue/layers/dms-core/app/build/composables/user/tablePreferences.ts +5 -1
- package/frontend-vue/layers/dms-core/app/build/utils/crossTab.ts +199 -0
- package/frontend-vue/layers/dms-core/app/build/utils/sharedRealtimeConnection.ts +410 -0
- package/frontend-vue/layers/dms-core/app/composables/auth/useApiData.ts +77 -12
- package/frontend-vue/layers/dms-core/app/composables/data-types/useDataType.ts +26 -0
- package/frontend-vue/layers/dms-core/app/composables/hydration/isHydratingComponent.ts +29 -0
- package/frontend-vue/layers/dms-core/app/composables/live/livePauseSource.ts +29 -0
- package/frontend-vue/layers/dms-core/app/composables/period/compareResolvers.ts +150 -9
- package/frontend-vue/layers/dms-core/app/composables/period/usePeriod.ts +6 -4
- package/frontend-vue/layers/dms-core/app/composables/period/usePeriodScope.ts +27 -1
- package/frontend-vue/layers/dms-core/app/composables/realtime/useUserRealtime.ts +256 -119
- package/frontend-vue/layers/dms-core/app/composables/translation/useComposedText.ts +21 -8
- package/frontend-vue/layers/dms-core/app/composables/translation/useTranslation.ts +22 -5
- package/frontend-vue/layers/dms-core/app/plugins/dev-error-log.ts +16 -0
- package/frontend-vue/layers/dms-core/app/types/user.ts +6 -0
- package/frontend-vue/layers/dms-core/app/utils/composedText.ts +7 -2
- package/frontend-vue/layers/dms-core/app/utils/formatter.ts +166 -35
- package/frontend-vue/layers/dms-core/app/utils/url-interpolation.ts +18 -1
- package/frontend-vue/layers/dms-core/i18n/locales/core-en-GB.json +134 -2
- package/frontend-vue/layers/dms-core/i18n/locales/core-fr-FR.json +134 -2
- package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardBanners.vue +3 -2
- package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardHeader.vue +41 -21
- package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardPageBody.vue +9 -0
- package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardSidebar.vue +4 -19
- package/frontend-vue/layers/dms-layout/app/build/components/layout/Footer.vue +11 -3
- package/frontend-vue/layers/dms-layout/app/build/components/layout/GithubStarPrompt.vue +18 -7
- package/frontend-vue/layers/dms-layout/app/build/components/layout/PageHeaderActionBar.vue +8 -0
- package/frontend-vue/layers/dms-layout/app/build/components/layout/PageHeaderActionsOutlet.vue +14 -0
- package/frontend-vue/layers/dms-layout/app/build/components/layout/RowSidePanel.vue +43 -82
- package/frontend-vue/layers/dms-layout/app/build/components/layout/SidebarUserMenu.vue +44 -3
- package/frontend-vue/layers/dms-layout/app/build/components/layout/UserIdentity.vue +11 -3
- package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleCatalogTile.vue +132 -35
- package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleManageMenu.vue +59 -0
- package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleSourceLabel.vue +25 -0
- package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleStoreBanners.vue +73 -0
- package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleStoreOperation.vue +116 -0
- package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleStoreRequirements.vue +8 -3
- package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleStoreTile.vue +85 -207
- package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleTile.vue +10 -3
- package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleTileSkeleton.vue +68 -0
- package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/notification/NotificationInboxDisplay.vue +24 -8
- package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/notification/NotificationInboxItem.vue +8 -5
- package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/region/RegionTimeZoneInput.vue +1 -0
- package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/roles/role-permissions.ts +19 -0
- package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/roles/useRoleEditor.ts +2 -1
- package/frontend-vue/layers/dms-layout/app/build/composables/layout/useSidePanelWidth.ts +31 -70
- package/frontend-vue/layers/dms-layout/app/build/composables/notification/useNotificationGroupsOpen.ts +11 -1
- package/frontend-vue/layers/dms-layout/app/build/utils/dev-reload-stream.ts +81 -0
- package/frontend-vue/layers/dms-layout/app/build/utils/module-store-catalog.ts +232 -36
- package/frontend-vue/layers/dms-layout/app/build/utils/modules-catalog.ts +122 -14
- package/frontend-vue/layers/dms-layout/app/composables/layout/useLayoutBanners.ts +32 -3
- package/frontend-vue/layers/dms-layout/app/composables/layout/usePageRecord.ts +50 -0
- package/frontend-vue/layers/dms-layout/app/composables/layout/useServedHeaderActions.ts +25 -7
- package/frontend-vue/layers/dms-layout/app/composables/page/useModuleHistory.ts +3 -2
- package/frontend-vue/layers/dms-layout/app/composables/page/useModuleStore.ts +51 -4
- package/frontend-vue/layers/dms-layout/app/composables/page/useModuleStoreActions.ts +194 -0
- package/frontend-vue/layers/dms-layout/app/composables/page/usePageComponentPreload.ts +23 -3
- package/frontend-vue/layers/dms-layout/app/composables/page/usePageLayout.ts +4 -2
- package/frontend-vue/layers/dms-layout/app/composables/page/useSiteLayout.ts +6 -1
- package/frontend-vue/layers/dms-layout/app/custom-layouts/DefaultLayout.vue +9 -0
- package/frontend-vue/layers/dms-layout/app/custom-layouts/SettingsLayout.vue +9 -0
- package/frontend-vue/layers/dms-layout/app/custom-pages/modules-store.vue +291 -0
- package/frontend-vue/layers/dms-layout/app/custom-pages/modules.vue +87 -474
- package/frontend-vue/layers/dms-layout/app/middleware/module-routing.global.ts +3 -0
- package/frontend-vue/layers/dms-layout/app/pages/[...slug].vue +6 -22
- package/frontend-vue/layers/dms-layout/app/plugins/command-palette-navigation.client.ts +2 -1
- package/frontend-vue/layers/dms-layout/app/plugins/command-palette-personal-pages.client.ts +14 -44
- package/frontend-vue/layers/dms-layout/app/plugins/dms-dev-reload.client.ts +14 -9
- package/frontend-vue/layers/dms-layout/app/theme/form-controls.ts +9 -4
- package/frontend-vue/layers/dms-layout/app/theme/surfaces.ts +22 -6
- package/frontend-vue/layers/dms-layout/app/types/module-store.ts +9 -0
- package/frontend-vue/layers/dms-layout/app/types/page.ts +15 -1
- package/frontend-vue/layers/dms-layout/app/utils/account-menu.ts +11 -0
- package/frontend-vue/layers/dms-layout/app/utils/breadcrumb.ts +57 -0
- package/frontend-vue/layers/dms-layout/app/utils/pageQuery.ts +26 -0
- package/frontend-vue/layers/dms-layout/app/utils/pageRecord.ts +71 -0
- package/frontend-vue/layers/dms-layout/i18n/locales/layout-en-GB.json +39 -14
- package/frontend-vue/layers/dms-layout/i18n/locales/layout-fr-FR.json +38 -13
- package/frontend-vue/layers/dms-ui/app/build/components/containers/TargetComponentTree.vue +31 -0
- package/frontend-vue/layers/dms-ui/app/build/components/containers/constants.ts +25 -0
- package/frontend-vue/layers/dms-ui/app/build/components/containers/drawer/DynamicDrawer.vue +244 -15
- package/frontend-vue/layers/dms-ui/app/build/components/containers/modal/DynamicModal.vue +42 -25
- package/frontend-vue/layers/dms-ui/app/build/components/containers/row/RowHeader.vue +153 -0
- package/frontend-vue/layers/dms-ui/app/build/components/containers/row/RowNotFound.vue +44 -0
- package/frontend-vue/layers/dms-ui/app/build/components/containers/row/drawerRowHeader.ts +35 -0
- package/frontend-vue/layers/dms-ui/app/build/components/form/FormEntries.vue +44 -9
- package/frontend-vue/layers/dms-ui/app/build/components/form/FormErrorSummary.vue +11 -4
- package/frontend-vue/layers/dms-ui/app/build/components/form/FormFieldControl.vue +38 -5
- package/frontend-vue/layers/dms-ui/app/build/components/form/FormFieldNotes.vue +29 -5
- package/frontend-vue/layers/dms-ui/app/build/components/form/FormSectionNav.vue +3 -1
- package/frontend-vue/layers/dms-ui/app/build/components/form/FormSections.vue +106 -27
- package/frontend-vue/layers/dms-ui/app/build/components/table/Actions.vue +12 -4
- package/frontend-vue/layers/dms-ui/app/build/components/table/Empty.vue +4 -2
- package/frontend-vue/layers/dms-ui/app/build/components/table/FiltersRow.vue +38 -1
- package/frontend-vue/layers/dms-ui/app/build/components/table/Pagination.vue +55 -54
- package/frontend-vue/layers/dms-ui/app/build/components/table/RowSelection.vue +2 -1
- package/frontend-vue/layers/dms-ui/app/build/components/table/Table.vue +396 -288
- package/frontend-vue/layers/dms-ui/app/build/components/table/Tabs.vue +2 -1
- package/frontend-vue/layers/dms-ui/app/build/components/table/Views.vue +2 -1
- package/frontend-vue/layers/dms-ui/app/build/components/table-view/CardAvatar.vue +59 -0
- package/frontend-vue/layers/dms-ui/app/build/components/table-view/CardGridSkeleton.vue +19 -2
- package/frontend-vue/layers/dms-ui/app/build/components/table-view/CardRowActions.vue +38 -0
- package/frontend-vue/layers/dms-ui/app/build/components/table-view/CardsDisplay.vue +148 -104
- package/frontend-vue/layers/dms-ui/app/build/components/table-view/LiveHoldBanner.vue +17 -2
- package/frontend-vue/layers/dms-ui/app/build/composables/actions/bulkSelection.ts +43 -9
- package/frontend-vue/layers/dms-ui/app/build/composables/actions/rowComponentOptions.ts +76 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/actions/rowNavigation.ts +172 -27
- package/frontend-vue/layers/dms-ui/app/build/composables/actions/useActionTargets.ts +270 -46
- package/frontend-vue/layers/dms-ui/app/build/composables/confirm/confirmDialogTexts.ts +2 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/containers/closingLayers.ts +186 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/containers/context.ts +14 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/containers/drawerSize.ts +229 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/containers/useResizableSize.ts +175 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/data-types/metricCells.ts +18 -35
- package/frontend-vue/layers/dms-ui/app/build/composables/data-types/numberSuffix.ts +47 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/data-types/registerDefaults.ts +99 -24
- package/frontend-vue/layers/dms-ui/app/build/composables/data-types/registerFieldTypes.ts +2 -1
- package/frontend-vue/layers/dms-ui/app/build/composables/data-types/relationFilter.ts +75 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/data-types/relationRows.ts +71 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/data-types/relationValue.ts +39 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/data-types/twoLineCell.ts +3 -3
- package/frontend-vue/layers/dms-ui/app/build/composables/data-types/useColumnValueRenderer.ts +6 -2
- package/frontend-vue/layers/dms-ui/app/build/composables/form/createFormFields.ts +15 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/form/fieldSaved.ts +40 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/form/fieldValueRule.ts +62 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/form/formEntryContext.ts +5 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/form/formFooter.ts +42 -7
- package/frontend-vue/layers/dms-ui/app/build/composables/form/formLayout.ts +106 -4
- package/frontend-vue/layers/dms-ui/app/build/composables/form/formSections.ts +32 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/form/readonlyValue.ts +19 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/form/registerFormFunctions.ts +27 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/form/repeaterTable.ts +65 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/form/serverFieldErrors.ts +49 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/form/toasterClearance.ts +118 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/form/useInstantForm.ts +12 -1
- package/frontend-vue/layers/dms-ui/app/build/composables/live/currentPageLive.ts +41 -3
- package/frontend-vue/layers/dms-ui/app/build/composables/live/livePoller.ts +10 -2
- package/frontend-vue/layers/dms-ui/app/build/composables/live/pageReadClock.ts +28 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/live/shortcuts.ts +1 -1
- package/frontend-vue/layers/dms-ui/app/build/composables/live/usePageLiveHost.ts +17 -3
- package/frontend-vue/layers/dms-ui/app/build/composables/live/useUpdatedLabel.ts +38 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/page-toolbar/pageToolbarContext.ts +75 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/page-toolbar/toolbarControlHeight.ts +29 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/period/periodComparison.ts +52 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/period/periodDisplay.ts +21 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/period/periodShortLabels.ts +28 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/period/usePeriodDraft.ts +1 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/table/useRelationFilterLabels.ts +73 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/table/useRowScrollAnchor.ts +106 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/table/useTable.ts +9 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/table/useTableColumns.ts +1 -2
- package/frontend-vue/layers/dms-ui/app/build/composables/table/utils/archivedCount.ts +31 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/table/utils/filterEditorValue.ts +38 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/table/utils/formatFilterValue.ts +33 -2
- package/frontend-vue/layers/dms-ui/app/build/composables/table/utils/scrollEdges.ts +31 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useCardFields.ts +33 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useMobileDisplay.ts +48 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useScrolledPastTop.ts +41 -18
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableFooter.ts +3 -2
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableLinkedVisit.ts +78 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableLiveHold.ts +135 -22
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableRows.ts +56 -25
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViewRowActions.ts +77 -32
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViews.ts +86 -10
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/bulkActions.ts +14 -4
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/card.ts +127 -2
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/compareValues.ts +13 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/expandedRows.ts +28 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/groupedRows.ts +114 -1
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/linkedArrival.ts +308 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/locateRow.ts +106 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/pageArrival.ts +34 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/pinnedRows.ts +17 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/quickFilters.ts +41 -33
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/recordLink.ts +24 -8
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/tableCounts.ts +113 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/tableQuery.ts +27 -4
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/views.ts +22 -6
- package/frontend-vue/layers/dms-ui/app/build/utils/addressSuggestions.ts +49 -0
- package/frontend-vue/layers/dms-ui/app/build/utils/eyebrow.ts +12 -0
- package/frontend-vue/layers/dms-ui/app/build/utils/fieldOptionsI18n.ts +51 -11
- package/frontend-vue/layers/dms-ui/app/build/utils/inputWidth.ts +53 -0
- package/frontend-vue/layers/dms-ui/app/build/utils/numberAdjustment.ts +6 -3
- package/frontend-vue/layers/dms-ui/app/build/utils/relativeDate.ts +19 -5
- package/frontend-vue/layers/dms-ui/app/build/utils/scrollWithin.ts +99 -0
- package/frontend-vue/layers/dms-ui/app/build/utils/urlVariables.ts +36 -14
- package/frontend-vue/layers/dms-ui/app/components/PasswordStrength.vue +1 -1
- package/frontend-vue/layers/dms-ui/app/components/RecursiveComponent.vue +9 -1
- package/frontend-vue/layers/dms-ui/app/components/blocks/LiveToggleBlock.vue +80 -32
- package/frontend-vue/layers/dms-ui/app/components/blocks/PageToolbarBlock.vue +137 -0
- package/frontend-vue/layers/dms-ui/app/components/blocks/StatGroupBlock.vue +5 -0
- package/frontend-vue/layers/dms-ui/app/components/chart/ApexChartHost.vue +16 -2
- package/frontend-vue/layers/dms-ui/app/components/chart/Chart.vue +24 -5
- package/frontend-vue/layers/dms-ui/app/components/chart/ChartCard.vue +41 -23
- package/frontend-vue/layers/dms-ui/app/components/confirm/ConfirmModal.vue +1 -1
- package/frontend-vue/layers/dms-ui/app/components/container-footer/ContainerFooter.vue +34 -0
- package/frontend-vue/layers/dms-ui/app/components/field-row/FieldRow.vue +25 -2
- package/frontend-vue/layers/dms-ui/app/components/flow-canvas/FlowCanvas.vue +2 -2
- package/frontend-vue/layers/dms-ui/app/components/form/Form.vue +118 -36
- package/frontend-vue/layers/dms-ui/app/components/form/components/DatePicker.vue +20 -1
- package/frontend-vue/layers/dms-ui/app/components/form/components/Display.vue +13 -1
- package/frontend-vue/layers/dms-ui/app/components/form/components/Image.vue +8 -3
- package/frontend-vue/layers/dms-ui/app/components/form/components/InputAddress.vue +53 -5
- package/frontend-vue/layers/dms-ui/app/components/form/components/InputCode.vue +29 -5
- package/frontend-vue/layers/dms-ui/app/components/form/components/InputNumber.vue +45 -8
- package/frontend-vue/layers/dms-ui/app/components/form/components/InputPercentage.vue +28 -5
- package/frontend-vue/layers/dms-ui/app/components/form/components/InputSecret.vue +5 -0
- package/frontend-vue/layers/dms-ui/app/components/form/components/InputText.vue +19 -2
- package/frontend-vue/layers/dms-ui/app/components/form/components/InputTime.vue +2 -1
- package/frontend-vue/layers/dms-ui/app/components/form/components/Relation.vue +205 -65
- package/frontend-vue/layers/dms-ui/app/components/form/components/Repeater.vue +80 -30
- package/frontend-vue/layers/dms-ui/app/components/form/components/RepeaterTable.vue +203 -0
- package/frontend-vue/layers/dms-ui/app/components/form/components/Select.vue +4 -2
- package/frontend-vue/layers/dms-ui/app/components/grid/GridRow.vue +11 -2
- package/frontend-vue/layers/dms-ui/app/components/key-value-list/KeyValueList.vue +79 -70
- package/frontend-vue/layers/dms-ui/app/components/kpi/KpiCard.vue +117 -21
- package/frontend-vue/layers/dms-ui/app/components/meter/Meter.vue +2 -1
- package/frontend-vue/layers/dms-ui/app/components/period/PeriodSelector.vue +113 -34
- package/frontend-vue/layers/dms-ui/app/components/save-bar/SaveBar.vue +14 -2
- package/frontend-vue/layers/dms-ui/app/components/section-header/Eyebrow.vue +6 -1
- package/frontend-vue/layers/dms-ui/app/components/segmented/Segmented.vue +13 -0
- package/frontend-vue/layers/dms-ui/app/components/stat-group/StatGroup.vue +52 -3
- package/frontend-vue/layers/dms-ui/app/components/tab/Tab.vue +2 -1
- package/frontend-vue/layers/dms-ui/app/components/table-view/KanbanBoard.vue +45 -35
- package/frontend-vue/layers/dms-ui/app/components/table-view/KanbanDisplay.vue +4 -1
- package/frontend-vue/layers/dms-ui/app/components/table-view/TableView.vue +410 -96
- package/frontend-vue/layers/dms-ui/app/components/top-list/TopListCard.vue +33 -5
- package/frontend-vue/layers/dms-ui/app/composables/chart/apexCategoryLabels.ts +68 -0
- package/frontend-vue/layers/dms-ui/app/composables/chart/apexDatetimeBuckets.ts +306 -0
- package/frontend-vue/layers/dms-ui/app/composables/chart/formatValue.ts +4 -3
- package/frontend-vue/layers/dms-ui/app/composables/chart/heatmapColors.ts +93 -0
- package/frontend-vue/layers/dms-ui/app/composables/chart/localizeSeries.ts +62 -0
- package/frontend-vue/layers/dms-ui/app/composables/chart/useApexChart.ts +116 -8
- package/frontend-vue/layers/dms-ui/app/composables/chart/useApexChart.types.ts +7 -0
- package/frontend-vue/layers/dms-ui/app/composables/containers/types.ts +84 -1
- package/frontend-vue/layers/dms-ui/app/composables/containers/useDrawer.ts +69 -8
- package/frontend-vue/layers/dms-ui/app/composables/form/types/field-saved.ts +11 -0
- package/frontend-vue/layers/dms-ui/app/composables/form/types/field.ts +4 -0
- package/frontend-vue/layers/dms-ui/app/composables/form/types/functions.ts +1 -0
- package/frontend-vue/layers/dms-ui/app/composables/form/useForm.ts +142 -21
- package/frontend-vue/layers/dms-ui/app/composables/live/usePageLive.ts +11 -4
- package/frontend-vue/layers/dms-ui/app/composables/page-toolbar/usePageToolbarSize.ts +30 -0
- package/frontend-vue/layers/dms-ui/app/composables/table-view/kanban.ts +4 -2
- package/frontend-vue/layers/dms-ui/app/composables/table-view/types/action-target.ts +22 -3
- package/frontend-vue/layers/dms-ui/app/composables/table-view/types/config.ts +4 -0
- package/frontend-vue/layers/dms-ui/app/composables/table-view/types/display.ts +24 -1
- package/frontend-vue/layers/dms-ui/app/plugins/closing-layers.client.ts +10 -0
- package/frontend-vue/layers/dms-ui/app/plugins/live-pause-source.ts +15 -0
- package/frontend-vue/layers/dms-ui/app/plugins/register.ts +2 -0
- package/frontend-vue/layers/dms-ui/app/types/input-number.ts +3 -2
- package/frontend-vue/layers/dms-ui/app/types/page-toolbar.ts +7 -0
- package/frontend-vue/layers/dms-ui/app/types/row-action.ts +0 -2
- package/frontend-vue/layers/dms-ui/app/types/row-navigation.ts +17 -0
- package/frontend-vue/layers/dms-ui/app/types/table/query.ts +2 -1
- package/frontend-vue/layers/dms-ui/app/utils/blockRefresh.ts +3 -1
- package/frontend-vue/layers/dms-ui/app/utils/componentTree.ts +24 -0
- package/frontend-vue/layers/dms-ui/i18n/locales/ui-en-GB.json +16 -4
- package/frontend-vue/layers/dms-ui/i18n/locales/ui-fr-FR.json +16 -4
- package/frontend-vue/package.json +2 -2
- package/package.json +7 -6
- package/skills/dms/SKILL.md +1 -1
- package/skills/dms-auth/SKILL.md +5 -3
- package/skills/dms-best-practices/REFERENCE.md +2 -0
- package/skills/dms-best-practices/SKILL.md +4 -0
- package/skills/dms-module/SKILL.md +3 -2
- package/skills/dms-pages/REFERENCE.md +125 -16
- package/skills/dms-pages/SKILL.md +1 -1
- package/skills/dms-project/DEV-SERVERS.md +23 -1
- package/skills/dms-project/REFERENCE.md +1 -1
- package/skills/dms-project/SKILL.md +4 -1
- package/skills/dms-project/scripts/dms-dev.sh +205 -17
- package/dist/routes/startup-gate.d.ts +0 -7
- package/dist/routes/startup-gate.js +0 -39
- package/dist/routes/startup-gate.js.map +0 -1
- package/frontend-vue/layers/dms-core/app/composables/period/periodRenderNow.ts +0 -42
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isUnauthorizedStreamError,
|
|
3
|
+
openSseStream,
|
|
4
|
+
type SseStreamHandle,
|
|
5
|
+
} from "../../composables/realtime/useSseStream";
|
|
6
|
+
import {
|
|
7
|
+
browserCrossTabEnvironment,
|
|
8
|
+
type CrossTabEnvironment,
|
|
9
|
+
electLeader,
|
|
10
|
+
holdLock,
|
|
11
|
+
openTabBus,
|
|
12
|
+
type TabBus,
|
|
13
|
+
tabLockName,
|
|
14
|
+
whenReleased,
|
|
15
|
+
} from "./crossTab";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The realtime connection one browser shares between its tabs: the elected tab
|
|
19
|
+
* holds `GET /api/realtime/connection` and relays it, each tab opens its own
|
|
20
|
+
* session on it. See `crossTab.ts` for why.
|
|
21
|
+
*
|
|
22
|
+
* @internal
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const HELLO_EVENT = "hello";
|
|
26
|
+
const SESSION_EVENT = "session";
|
|
27
|
+
const SESSION_HEADER = "x-realtime-session";
|
|
28
|
+
const CONNECTION_HEADER = "x-realtime-connection";
|
|
29
|
+
const RECONNECT_INITIAL_DELAY_MS = 500;
|
|
30
|
+
const RECONNECT_MAX_DELAY_MS = 30_000;
|
|
31
|
+
const RECONNECT_MULTIPLIER = 2;
|
|
32
|
+
|
|
33
|
+
interface ConnectionMessage {
|
|
34
|
+
kind: "connection";
|
|
35
|
+
/** `null` while the leader has no stream open. */
|
|
36
|
+
connectionId: string | null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface FrameMessage {
|
|
40
|
+
kind: "frame";
|
|
41
|
+
connectionId: string;
|
|
42
|
+
/** Unset on a frame of the connection itself, which every tab receives. */
|
|
43
|
+
sessionId?: string;
|
|
44
|
+
event: string;
|
|
45
|
+
data: unknown;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** A tab joining asks the leader for the connection it holds. */
|
|
49
|
+
interface QueryMessage {
|
|
50
|
+
kind: "query";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** A tab's session on the connection, for the leader to end once it goes. */
|
|
54
|
+
interface SessionMessage {
|
|
55
|
+
kind: "session";
|
|
56
|
+
connectionId: string;
|
|
57
|
+
sessionId: string;
|
|
58
|
+
tabId: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* A tab whose session request the backend answered without knowing the
|
|
63
|
+
* connection asks the leader to open a new one.
|
|
64
|
+
*/
|
|
65
|
+
interface ReopenMessage {
|
|
66
|
+
kind: "reopen";
|
|
67
|
+
connectionId: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
type HubMessage =
|
|
71
|
+
| ConnectionMessage
|
|
72
|
+
| FrameMessage
|
|
73
|
+
| QueryMessage
|
|
74
|
+
| SessionMessage
|
|
75
|
+
| ReopenMessage;
|
|
76
|
+
|
|
77
|
+
export interface StreamRecovery {
|
|
78
|
+
refreshSession: () => Promise<boolean>;
|
|
79
|
+
redirectToAuth: () => Promise<void>;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface SharedRealtimeConnectionOptions extends StreamRecovery {
|
|
83
|
+
/** Scopes the election and the relay: one connection per name. */
|
|
84
|
+
name: string;
|
|
85
|
+
streamUrl: string;
|
|
86
|
+
sessionCloseUrl: string;
|
|
87
|
+
/** The connection to open a session on; `undefined` while there is none. */
|
|
88
|
+
onConnection: (connectionId: string | undefined) => void;
|
|
89
|
+
/**
|
|
90
|
+
* A frame of one tab's session, or of the connection itself (the dev
|
|
91
|
+
* reload signal) when `sessionId` is unset.
|
|
92
|
+
*/
|
|
93
|
+
onFrame: (
|
|
94
|
+
sessionId: string | undefined,
|
|
95
|
+
eventName: string,
|
|
96
|
+
data: unknown,
|
|
97
|
+
) => void;
|
|
98
|
+
environment?: CrossTabEnvironment;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface SharedRealtimeConnection {
|
|
102
|
+
/** Hands this tab's session to the leader, which ends it once the tab goes. */
|
|
103
|
+
announceSession: (connectionId: string, sessionId: string) => void;
|
|
104
|
+
/**
|
|
105
|
+
* Asks the leader to replace `connectionId` with a new stream: the backend
|
|
106
|
+
* answered a session request on it as a connection it does not know.
|
|
107
|
+
*/
|
|
108
|
+
reopen: (connectionId: string) => void;
|
|
109
|
+
leave: () => void;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export interface Reconnector {
|
|
113
|
+
isCancelled(): boolean;
|
|
114
|
+
schedule(open: () => void): void;
|
|
115
|
+
resetDelay(): void;
|
|
116
|
+
cancel(): void;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export const createReconnector = (): Reconnector => {
|
|
120
|
+
let isCancelled = false;
|
|
121
|
+
let delay = RECONNECT_INITIAL_DELAY_MS;
|
|
122
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
123
|
+
return {
|
|
124
|
+
isCancelled: () => isCancelled,
|
|
125
|
+
schedule: (open) => {
|
|
126
|
+
// A pending reconnect absorbs concurrent triggers: one attempt at a time.
|
|
127
|
+
if (isCancelled || timer) return;
|
|
128
|
+
timer = setTimeout(() => {
|
|
129
|
+
timer = undefined;
|
|
130
|
+
open();
|
|
131
|
+
}, delay);
|
|
132
|
+
delay = Math.min(delay * RECONNECT_MULTIPLIER, RECONNECT_MAX_DELAY_MS);
|
|
133
|
+
},
|
|
134
|
+
resetDelay: () => {
|
|
135
|
+
delay = RECONNECT_INITIAL_DELAY_MS;
|
|
136
|
+
},
|
|
137
|
+
cancel: () => {
|
|
138
|
+
isCancelled = true;
|
|
139
|
+
if (timer) clearTimeout(timer);
|
|
140
|
+
timer = undefined;
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
/** Retries after a failure, refreshing the browser session first on a 401. */
|
|
146
|
+
export const recoverFromError = (
|
|
147
|
+
error: unknown,
|
|
148
|
+
retry: () => void,
|
|
149
|
+
recovery: StreamRecovery,
|
|
150
|
+
): void => {
|
|
151
|
+
if (!isUnauthorizedStreamError(error)) {
|
|
152
|
+
retry();
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
void recovery.refreshSession().then((isRefreshed) => {
|
|
156
|
+
if (isRefreshed) {
|
|
157
|
+
retry();
|
|
158
|
+
} else {
|
|
159
|
+
void recovery.redirectToAuth();
|
|
160
|
+
}
|
|
161
|
+
}, retry);
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
interface StreamCallbacks extends StreamRecovery {
|
|
165
|
+
onEvent: (eventName: string, data: unknown) => void;
|
|
166
|
+
onDisconnected: () => void;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
interface KeptStream {
|
|
170
|
+
/** Drops the open stream and opens a new one. */
|
|
171
|
+
reopen: () => void;
|
|
172
|
+
stop: () => void;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Keeps one stream open until cancelled. The server ends the stream when it
|
|
177
|
+
* stops (a dev reload included), so a clean EOF reconnects just like an error
|
|
178
|
+
* does: the stream has no business completion.
|
|
179
|
+
*/
|
|
180
|
+
const connectWithBackoff = (
|
|
181
|
+
url: string,
|
|
182
|
+
callbacks: StreamCallbacks,
|
|
183
|
+
): KeptStream => {
|
|
184
|
+
const reconnector = createReconnector();
|
|
185
|
+
let activeHandle: SseStreamHandle | undefined;
|
|
186
|
+
const open = () => {
|
|
187
|
+
if (reconnector.isCancelled()) return;
|
|
188
|
+
activeHandle?.close();
|
|
189
|
+
const handle: SseStreamHandle = openSseStream({
|
|
190
|
+
url,
|
|
191
|
+
onEvent: (eventName, data) => {
|
|
192
|
+
if (handle !== activeHandle) return;
|
|
193
|
+
reconnector.resetDelay();
|
|
194
|
+
callbacks.onEvent(eventName, data);
|
|
195
|
+
},
|
|
196
|
+
onError: (error) => {
|
|
197
|
+
if (!retire(handle)) return;
|
|
198
|
+
recoverFromError(error, reconnect, callbacks);
|
|
199
|
+
},
|
|
200
|
+
onClose: (reason) => {
|
|
201
|
+
if (reason !== "eof" || !retire(handle)) return;
|
|
202
|
+
reconnect();
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
activeHandle = handle;
|
|
206
|
+
};
|
|
207
|
+
const reconnect = () => reconnector.schedule(open);
|
|
208
|
+
const retire = (handle: SseStreamHandle): boolean => {
|
|
209
|
+
if (handle !== activeHandle || reconnector.isCancelled()) return false;
|
|
210
|
+
activeHandle = undefined;
|
|
211
|
+
callbacks.onDisconnected();
|
|
212
|
+
return true;
|
|
213
|
+
};
|
|
214
|
+
open();
|
|
215
|
+
return {
|
|
216
|
+
reopen: () => {
|
|
217
|
+
const handle = activeHandle;
|
|
218
|
+
if (!handle || !retire(handle)) return;
|
|
219
|
+
handle.close();
|
|
220
|
+
reconnect();
|
|
221
|
+
},
|
|
222
|
+
stop: () => {
|
|
223
|
+
reconnector.cancel();
|
|
224
|
+
activeHandle?.close();
|
|
225
|
+
activeHandle = undefined;
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
const readConnectionId = (data: unknown): string | null => {
|
|
231
|
+
const connectionId = (data as { connectionId?: unknown } | null)
|
|
232
|
+
?.connectionId;
|
|
233
|
+
return typeof connectionId === "string" ? connectionId : null;
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
interface SessionFrame {
|
|
237
|
+
sessionId?: unknown;
|
|
238
|
+
event?: unknown;
|
|
239
|
+
data?: unknown;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
interface LeaderContext {
|
|
243
|
+
options: SharedRealtimeConnectionOptions;
|
|
244
|
+
environment: CrossTabEnvironment;
|
|
245
|
+
bus: TabBus;
|
|
246
|
+
tabId: string;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const closeSession = (
|
|
250
|
+
url: string,
|
|
251
|
+
connectionId: string,
|
|
252
|
+
sessionId: string,
|
|
253
|
+
): void => {
|
|
254
|
+
void fetch(url, {
|
|
255
|
+
method: "POST",
|
|
256
|
+
headers: { [CONNECTION_HEADER]: connectionId, [SESSION_HEADER]: sessionId },
|
|
257
|
+
credentials: "include",
|
|
258
|
+
}).catch(() => {});
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* The elected tab's share: the stream, relayed on the bus, and the sessions
|
|
263
|
+
* of the other tabs, each ended once its tab is gone.
|
|
264
|
+
*/
|
|
265
|
+
const lead = ({ options, environment, bus, tabId }: LeaderContext) => {
|
|
266
|
+
const watchers = new AbortController();
|
|
267
|
+
const sessionsByTab = new Map<string, Set<string>>();
|
|
268
|
+
let connectionId: string | null = null;
|
|
269
|
+
|
|
270
|
+
const publish = (next: string | null) => {
|
|
271
|
+
connectionId = next;
|
|
272
|
+
bus.post({ kind: "connection", connectionId: next });
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
const endSessionsOf = (otherTabId: string) => {
|
|
276
|
+
const sessionIds = sessionsByTab.get(otherTabId) ?? new Set<string>();
|
|
277
|
+
sessionsByTab.delete(otherTabId);
|
|
278
|
+
// The sessions of a connection that went ended with it.
|
|
279
|
+
if (!connectionId) return;
|
|
280
|
+
for (const sessionId of sessionIds) {
|
|
281
|
+
closeSession(options.sessionCloseUrl, connectionId, sessionId);
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
const watchSession = (message: SessionMessage) => {
|
|
286
|
+
// This tab's own sessions end with the stream it holds.
|
|
287
|
+
if (message.tabId === tabId || message.connectionId !== connectionId) {
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
const known = sessionsByTab.get(message.tabId);
|
|
291
|
+
if (known) {
|
|
292
|
+
known.add(message.sessionId);
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
sessionsByTab.set(message.tabId, new Set([message.sessionId]));
|
|
296
|
+
whenReleased(
|
|
297
|
+
tabLockName(options.name, message.tabId),
|
|
298
|
+
environment,
|
|
299
|
+
() => endSessionsOf(message.tabId),
|
|
300
|
+
watchers.signal,
|
|
301
|
+
);
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
const unlisten = bus.listen((raw) => {
|
|
305
|
+
const message = raw as HubMessage;
|
|
306
|
+
if (message.kind === "query")
|
|
307
|
+
bus.post({ kind: "connection", connectionId });
|
|
308
|
+
if (message.kind === "session") watchSession(message);
|
|
309
|
+
if (message.kind === "reopen" && message.connectionId === connectionId) {
|
|
310
|
+
stream.reopen();
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
// The tabs may still hold the connection of a leader that went: they drop it.
|
|
315
|
+
publish(null);
|
|
316
|
+
const stream = connectWithBackoff(options.streamUrl, {
|
|
317
|
+
onEvent: (eventName, data) => {
|
|
318
|
+
if (eventName === HELLO_EVENT) {
|
|
319
|
+
sessionsByTab.clear();
|
|
320
|
+
publish(readConnectionId(data));
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
if (!connectionId) return;
|
|
324
|
+
if (eventName !== SESSION_EVENT) {
|
|
325
|
+
bus.post({ kind: "frame", connectionId, event: eventName, data });
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
const frame = data as SessionFrame | null;
|
|
329
|
+
if (typeof frame?.sessionId !== "string") return;
|
|
330
|
+
if (typeof frame.event !== "string") return;
|
|
331
|
+
bus.post({
|
|
332
|
+
kind: "frame",
|
|
333
|
+
connectionId,
|
|
334
|
+
sessionId: frame.sessionId,
|
|
335
|
+
event: frame.event,
|
|
336
|
+
data: frame.data,
|
|
337
|
+
});
|
|
338
|
+
},
|
|
339
|
+
onDisconnected: () => publish(null),
|
|
340
|
+
refreshSession: options.refreshSession,
|
|
341
|
+
redirectToAuth: options.redirectToAuth,
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
return () => {
|
|
345
|
+
watchers.abort();
|
|
346
|
+
unlisten();
|
|
347
|
+
stream.stop();
|
|
348
|
+
publish(null);
|
|
349
|
+
};
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
const createTabId = (): string =>
|
|
353
|
+
globalThis.crypto?.randomUUID?.() ??
|
|
354
|
+
`${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Joins the browser's realtime connection for `options.name`, electing this
|
|
358
|
+
* tab to hold it when no other tab does.
|
|
359
|
+
*/
|
|
360
|
+
export function joinSharedRealtimeConnection(
|
|
361
|
+
options: SharedRealtimeConnectionOptions,
|
|
362
|
+
): SharedRealtimeConnection {
|
|
363
|
+
const environment = options.environment ?? browserCrossTabEnvironment();
|
|
364
|
+
const tabId = createTabId();
|
|
365
|
+
const bus = openTabBus(options.name, environment);
|
|
366
|
+
// Held before any session is announced: the leader waits on it to learn
|
|
367
|
+
// that this tab went.
|
|
368
|
+
const tabLock = holdLock(tabLockName(options.name, tabId), environment);
|
|
369
|
+
let connectionId: string | undefined;
|
|
370
|
+
|
|
371
|
+
const unlisten = bus.listen((raw) => {
|
|
372
|
+
const message = raw as HubMessage;
|
|
373
|
+
if (message.kind === "connection") {
|
|
374
|
+
const next = message.connectionId ?? undefined;
|
|
375
|
+
if (next === connectionId) return;
|
|
376
|
+
connectionId = next;
|
|
377
|
+
options.onConnection(next);
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
if (message.kind === "frame" && message.connectionId === connectionId) {
|
|
381
|
+
options.onFrame(message.sessionId, message.event, message.data);
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
const resign = electLeader(options.name, environment, () =>
|
|
386
|
+
lead({ options, environment, bus, tabId }),
|
|
387
|
+
);
|
|
388
|
+
bus.post({ kind: "query" });
|
|
389
|
+
|
|
390
|
+
return {
|
|
391
|
+
announceSession: (sessionConnectionId, sessionId) => {
|
|
392
|
+
void tabLock.held.then(() =>
|
|
393
|
+
bus.post({
|
|
394
|
+
kind: "session",
|
|
395
|
+
connectionId: sessionConnectionId,
|
|
396
|
+
sessionId,
|
|
397
|
+
tabId,
|
|
398
|
+
}),
|
|
399
|
+
);
|
|
400
|
+
},
|
|
401
|
+
reopen: (reopenedConnectionId) =>
|
|
402
|
+
bus.post({ kind: "reopen", connectionId: reopenedConnectionId }),
|
|
403
|
+
leave: () => {
|
|
404
|
+
resign();
|
|
405
|
+
unlisten();
|
|
406
|
+
tabLock.release();
|
|
407
|
+
bus.close();
|
|
408
|
+
},
|
|
409
|
+
};
|
|
410
|
+
}
|
|
@@ -6,7 +6,8 @@ import type {
|
|
|
6
6
|
UseDmsPagedDataOptions,
|
|
7
7
|
UseFetchOptions,
|
|
8
8
|
} from "#dms/frontend-module";
|
|
9
|
-
import type
|
|
9
|
+
import { getCurrentInstance, toValue, watch, type MaybeRefOrGetter } from "vue";
|
|
10
|
+
import { useLivePauseSource } from "../live/livePauseSource";
|
|
10
11
|
|
|
11
12
|
/** The authenticated fetch `useAuthFetch` returns. */
|
|
12
13
|
type AuthFetch = ReturnType<typeof useAuthFetch>["$authFetch"];
|
|
@@ -61,7 +62,9 @@ export type ApiDataResult<T> = DmsAsyncDataResult<T>;
|
|
|
61
62
|
* - `refreshInterval: ms` reads again on an interval while the caller shows
|
|
62
63
|
* the data and the page is visible: polling. A ref or a getter changes it,
|
|
63
64
|
* and `0` or `null` stops it; a call from a plugin polls for as long as
|
|
64
|
-
* the application runs.
|
|
65
|
+
* the application runs. On a page declared with `live`, a component's
|
|
66
|
+
* polling stops while the page is paused, and reads once on resume if a
|
|
67
|
+
* poll fell due meanwhile.
|
|
65
68
|
* - The page's refreshes (`refreshPageBlocks()`) and the events of its
|
|
66
69
|
* realtime topics read the data again; `refreshOnPageRefresh: false` opts
|
|
67
70
|
* out, for data with refresh triggers of its own.
|
|
@@ -88,15 +91,74 @@ export function useApiData<T>(
|
|
|
88
91
|
): ApiDataResult<T> {
|
|
89
92
|
const { $authFetch } = useAuthFetch();
|
|
90
93
|
if (typeof handlerOrOptions === "function")
|
|
91
|
-
return
|
|
92
|
-
|
|
93
|
-
|
|
94
|
+
return followLivePause(
|
|
95
|
+
useDmsAsyncData<T>(
|
|
96
|
+
source,
|
|
97
|
+
({ signal }) => handlerOrOptions({ $authFetch, signal }),
|
|
98
|
+
gateLivePolling(options),
|
|
99
|
+
),
|
|
94
100
|
options,
|
|
95
101
|
);
|
|
96
|
-
return
|
|
97
|
-
|
|
98
|
-
|
|
102
|
+
return followLivePause(
|
|
103
|
+
useDmsFetch<T>(source, {
|
|
104
|
+
...gateLivePolling(handlerOrOptions),
|
|
105
|
+
$fetch: $authFetch as UseFetchOptions<T>["$fetch"],
|
|
106
|
+
}),
|
|
107
|
+
handlerOrOptions,
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** The options of a read that polls, as far as its live gate goes. */
|
|
112
|
+
interface PolledReadOptions {
|
|
113
|
+
refreshInterval?: MaybeRefOrGetter<number | null | undefined>;
|
|
114
|
+
immediate?: boolean;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// A component's polling is one more read nobody asked for: on a live page it
|
|
118
|
+
// stops while the page is paused, as the blocks' refetches do. A plugin's
|
|
119
|
+
// polling (the shell's, outside any page) keeps running. The pause is read
|
|
120
|
+
// through the app's `LIVE_PAUSE_SOURCE`, which the UI layer provides.
|
|
121
|
+
function isGatedPolling(
|
|
122
|
+
options: PolledReadOptions | undefined,
|
|
123
|
+
): options is PolledReadOptions {
|
|
124
|
+
return options?.refreshInterval !== undefined && !!getCurrentInstance();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** `options` with a `refreshInterval` that is `null` while the page is paused. */
|
|
128
|
+
function gateLivePolling<O extends PolledReadOptions>(
|
|
129
|
+
options: O | undefined,
|
|
130
|
+
): O | undefined {
|
|
131
|
+
if (!isGatedPolling(options)) return options;
|
|
132
|
+
const interval = options.refreshInterval;
|
|
133
|
+
const isPaused = useLivePauseSource();
|
|
134
|
+
return {
|
|
135
|
+
...options,
|
|
136
|
+
refreshInterval: () => (isPaused() ? null : toValue(interval)),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Reads once as the page resumes when a poll fell due while it was paused:
|
|
142
|
+
* the polling restarts with a full interval, which would show the data of
|
|
143
|
+
* before the pause that long.
|
|
144
|
+
*/
|
|
145
|
+
function followLivePause<
|
|
146
|
+
R extends Pick<DmsAsyncDataResult<unknown>, "status" | "refresh">,
|
|
147
|
+
>(result: R, options: PolledReadOptions | undefined): R {
|
|
148
|
+
if (typeof window === "undefined" || !isGatedPolling(options)) return result;
|
|
149
|
+
let pausedAt = 0;
|
|
150
|
+
watch(useLivePauseSource(), (isPaused) => {
|
|
151
|
+
if (isPaused) {
|
|
152
|
+
pausedAt = Date.now();
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
const period = toValue(options.refreshInterval);
|
|
156
|
+
if (!period || period <= 0 || Date.now() - pausedAt < period) return;
|
|
157
|
+
// Data a caller waits to `execute()` is not read before it is.
|
|
158
|
+
if (result.status.value === "idle" && options.immediate === false) return;
|
|
159
|
+
void result.refresh({ dedupe: "defer" });
|
|
99
160
|
});
|
|
161
|
+
return result;
|
|
100
162
|
}
|
|
101
163
|
|
|
102
164
|
/** What a handler reading one page of API data receives. */
|
|
@@ -139,10 +201,13 @@ export function useApiPagedData<TPage, TCursor = unknown>(
|
|
|
139
201
|
options: UseDmsPagedDataOptions<TPage, TCursor>,
|
|
140
202
|
): ApiPagedDataResult<TPage, TCursor> {
|
|
141
203
|
const { $authFetch } = useAuthFetch();
|
|
142
|
-
return
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
204
|
+
return followLivePause(
|
|
205
|
+
useDmsPagedData<TPage, TCursor>(
|
|
206
|
+
key,
|
|
207
|
+
({ cursor, index, signal }) =>
|
|
208
|
+
handler({ $authFetch, cursor, index, signal }),
|
|
209
|
+
gateLivePolling(options) ?? options,
|
|
210
|
+
),
|
|
146
211
|
options,
|
|
147
212
|
);
|
|
148
213
|
}
|
|
@@ -36,6 +36,24 @@ export type DataTypeValueComparer = (
|
|
|
36
36
|
options?: unknown,
|
|
37
37
|
) => boolean;
|
|
38
38
|
|
|
39
|
+
/**
|
|
40
|
+
* The value a filter chip's editor holds for a filter value a view declared
|
|
41
|
+
* as text (`"0"`, `"2026-10-01,2026-10-15"`), under a compare mode: a number
|
|
42
|
+
* input refuses `"0"`, a date range wants its two days. The filter itself
|
|
43
|
+
* keeps its text, so the query is unchanged.
|
|
44
|
+
*/
|
|
45
|
+
export type DataTypeFilterEditorValue = (text: string, mode: string) => unknown;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Where the rows a value names are read: the `select` route of the table a
|
|
49
|
+
* relation points to, and the keys its rows carry their label and value under
|
|
50
|
+
* (`label` and `value` unless mapped).
|
|
51
|
+
*/
|
|
52
|
+
export interface DataTypeSelectSource {
|
|
53
|
+
searchUrl: string;
|
|
54
|
+
keyMapping?: { label?: string; value?: string };
|
|
55
|
+
}
|
|
56
|
+
|
|
39
57
|
export interface DataType {
|
|
40
58
|
id: string;
|
|
41
59
|
formatter?: RecordWithDefault<DataTypeFormatter>;
|
|
@@ -46,6 +64,14 @@ export interface DataType {
|
|
|
46
64
|
* or whose order does not count. Without it, values compare deeply.
|
|
47
65
|
*/
|
|
48
66
|
isSameValue?: DataTypeValueComparer;
|
|
67
|
+
/** Without it, a filter's editor reads a declared text value as is. */
|
|
68
|
+
filterEditorValue?: DataTypeFilterEditorValue;
|
|
69
|
+
/**
|
|
70
|
+
* For a type whose value holds the ids of rows another table serves (a
|
|
71
|
+
* relation): where those rows are read, from the options of the type's
|
|
72
|
+
* control. A filter chip or a read-only list names the ids through it.
|
|
73
|
+
*/
|
|
74
|
+
selectSource?: (options: unknown) => DataTypeSelectSource | undefined;
|
|
49
75
|
displayComponent?: Component;
|
|
50
76
|
}
|
|
51
77
|
const dataTypes: Record<string, DataType> = {};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { getCurrentInstance } from "vue";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Whether the component setting up or rendering now is hydrating the
|
|
5
|
+
* server's HTML.
|
|
6
|
+
*
|
|
7
|
+
* Vue binds a hydrating component's vnode to its server-rendered node before
|
|
8
|
+
* its setup runs, where a fresh mount has no node until it is mounted; once
|
|
9
|
+
* mounted, the component no longer hydrates. So this is true from the setup
|
|
10
|
+
* to the first render of a component the browser takes over from the server,
|
|
11
|
+
* and false in a client navigation, on a later re-render, on the server, and
|
|
12
|
+
* outside a component.
|
|
13
|
+
*
|
|
14
|
+
* Read it where the browser must draw what the server drew: a part drawn
|
|
15
|
+
* in the browser only, or a value only the browser knows (local storage, the
|
|
16
|
+
* clock), waits until mounted while hydrating, and shows at once anywhere
|
|
17
|
+
* else. Call it before the setup's first `await`, while the component is
|
|
18
|
+
* still Vue's current instance. Data needs no check: `useApiData` hands the
|
|
19
|
+
* server's answer to the browser on its own.
|
|
20
|
+
*
|
|
21
|
+
* ```ts
|
|
22
|
+
* const isDrawn = ref(!import.meta.env.SSR && !isHydratingComponent());
|
|
23
|
+
* onMounted(() => (isDrawn.value = true));
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export function isHydratingComponent(): boolean {
|
|
27
|
+
const instance = getCurrentInstance();
|
|
28
|
+
return !!instance && !instance.isMounted && !!instance.vnode.el;
|
|
29
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { inject, type InjectionKey } from "vue";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Whether the page on screen is paused right now (by the user, a hold or a
|
|
5
|
+
* hidden tab). Reactive: it reads reactive state, so a `watch` or a
|
|
6
|
+
* `computed` over it follows the pause.
|
|
7
|
+
*/
|
|
8
|
+
export type LivePauseSource = () => boolean;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The extension point the core layer reads a page's pause through. The core
|
|
12
|
+
* owns `useApiData` but not the page's live controller: the layer that owns
|
|
13
|
+
* the controller provides a source at the app level
|
|
14
|
+
* (`app.provide(LIVE_PAUSE_SOURCE, source)`), so every component reads the
|
|
15
|
+
* same page, drawers and modals included. Without one, nothing is paused.
|
|
16
|
+
*/
|
|
17
|
+
export const LIVE_PAUSE_SOURCE: InjectionKey<LivePauseSource> = Symbol(
|
|
18
|
+
"dms-live-pause-source",
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
const NEVER_PAUSED: LivePauseSource = () => false;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The pause source of the app the calling component belongs to; never
|
|
25
|
+
* paused without one. Call it in a component setup.
|
|
26
|
+
*/
|
|
27
|
+
export function useLivePauseSource(): LivePauseSource {
|
|
28
|
+
return inject(LIVE_PAUSE_SOURCE, NEVER_PAUSED);
|
|
29
|
+
}
|