@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.
- package/CHANGELOG.md +114 -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 -45
- 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/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/frontend-vue/layers/dms-core/app/composables/period/periodRenderNow.ts +0 -42
|
@@ -10,9 +10,12 @@ import DmsBlockActions, {
|
|
|
10
10
|
// would draw an empty card for the moment that chunk takes.
|
|
11
11
|
import DmsTopListRow from "./internal/TopListRow.vue";
|
|
12
12
|
import { useChartFetch } from "../../composables/chart/useChartFetch";
|
|
13
|
+
import { readFetchNotFound } from "../../build/utils/fetchNotFound";
|
|
14
|
+
import DmsBlockInlineStatus from "../../build/components/blocks/BlockInlineStatus.vue";
|
|
13
15
|
import { formatValue } from "../../composables/chart/formatValue";
|
|
14
16
|
import { resolveSparklineAccent } from "../../composables/chart/resolveSparklineAccent";
|
|
15
17
|
import { usePeriodScopePreset } from "#dms-core/app/composables/period/usePeriodScope";
|
|
18
|
+
import { usePeriodComparisonDisplay } from "../../build/composables/period/periodComparison";
|
|
16
19
|
import {
|
|
17
20
|
hasUrlVariables,
|
|
18
21
|
resolveLinkVariables,
|
|
@@ -119,7 +122,7 @@ const staticData = computed<TopListCardResponse | null>(() =>
|
|
|
119
122
|
props.staticItems ? { items: props.staticItems } : null,
|
|
120
123
|
);
|
|
121
124
|
|
|
122
|
-
const { data, isLoading } = useChartFetch<TopListCardResponse>({
|
|
125
|
+
const { data, isLoading, error, refresh } = useChartFetch<TopListCardResponse>({
|
|
123
126
|
fetchUrl: props.fetchUrl,
|
|
124
127
|
fetchUrlMethod: props.fetchUrlMethod,
|
|
125
128
|
routeParams: () => props.routeParams,
|
|
@@ -226,11 +229,26 @@ const badgeLabel = computed<string | null>(() => {
|
|
|
226
229
|
return t(`${PRESET_LABEL_KEY_PREFIX}.${preset}`, preset);
|
|
227
230
|
});
|
|
228
231
|
|
|
232
|
+
// With no comparison selected on the scope, the rows show no change: their
|
|
233
|
+
// trend column goes rather than staying blank.
|
|
234
|
+
const comparison = usePeriodComparisonDisplay(props.periodScope);
|
|
235
|
+
const showRowDelta = computed(() => props.showDelta && !comparison.isOff.value);
|
|
236
|
+
|
|
229
237
|
const isFirstLoad = computed(() => isLoading.value && data.value === null);
|
|
230
238
|
// New inputs (a period applied) are on their way: the rows on screen still
|
|
231
239
|
// belong to the previous ones, so they dim like the KPI values do.
|
|
232
240
|
const isRefreshing = computed(() => isLoading.value && data.value !== null);
|
|
233
241
|
|
|
242
|
+
// A failed first load shows a message and a retry, never the empty state (a
|
|
243
|
+
// list that loaded with no rows); a failed refetch keeps the last rows.
|
|
244
|
+
const hasError = computed(
|
|
245
|
+
() => !isLoading.value && !data.value && Boolean(error.value),
|
|
246
|
+
);
|
|
247
|
+
// A 404 is a status too, but nothing to retry: the list does not exist.
|
|
248
|
+
const notFound = computed(() =>
|
|
249
|
+
hasError.value ? readFetchNotFound(error.value) : null,
|
|
250
|
+
);
|
|
251
|
+
|
|
234
252
|
const emptyLabelDisplay = computed(() =>
|
|
235
253
|
props.emptyLabel
|
|
236
254
|
? processI18n(props.emptyLabel)
|
|
@@ -262,7 +280,7 @@ function columnsTemplate(sparkline: boolean, icon: boolean): string {
|
|
|
262
280
|
columns.push(TITLE_COLUMN_WIDTH);
|
|
263
281
|
columns.push(VALUE_COLUMN_WIDTH);
|
|
264
282
|
if (sparkline && props.showSparkline) columns.push(SPARKLINE_COLUMN_WIDTH);
|
|
265
|
-
if (
|
|
283
|
+
if (showRowDelta.value) columns.push(TREND_COLUMN_WIDTH);
|
|
266
284
|
columns.push(EDGE_COLUMN_WIDTH);
|
|
267
285
|
return columns.join(" ");
|
|
268
286
|
}
|
|
@@ -338,7 +356,7 @@ const gridStyle = computed(() => ({
|
|
|
338
356
|
:has-any-icon="hasAnyIcon"
|
|
339
357
|
:show-sparkline="showSparkline"
|
|
340
358
|
:sparkline-accent="sparklineAccentFor(item)"
|
|
341
|
-
:show-delta="
|
|
359
|
+
:show-delta="showRowDelta"
|
|
342
360
|
:invert="invert"
|
|
343
361
|
:formatted-value="formattedValue(item)"
|
|
344
362
|
:bar-percent="barPercent(item)"
|
|
@@ -346,6 +364,14 @@ const gridStyle = computed(() => ({
|
|
|
346
364
|
/>
|
|
347
365
|
</ul>
|
|
348
366
|
|
|
367
|
+
<DmsBlockInlineStatus
|
|
368
|
+
v-else-if="hasError"
|
|
369
|
+
:state="notFound ? 'not-found' : 'error'"
|
|
370
|
+
:not-found="notFound"
|
|
371
|
+
class="min-h-48 px-[18px]"
|
|
372
|
+
@retry="refresh()"
|
|
373
|
+
/>
|
|
374
|
+
|
|
349
375
|
<DmsEmptyState
|
|
350
376
|
v-else-if="!isFirstLoad"
|
|
351
377
|
:icon="EMPTY_ICON"
|
|
@@ -359,10 +385,12 @@ const gridStyle = computed(() => ({
|
|
|
359
385
|
class="divide-y divide-(--ui-border-muted) py-1"
|
|
360
386
|
aria-busy="true"
|
|
361
387
|
>
|
|
388
|
+
<!-- A loaded row's height: 60px with its bar, 56px without. -->
|
|
362
389
|
<div
|
|
363
390
|
v-for="row in props.skeletonCount"
|
|
364
391
|
:key="row"
|
|
365
|
-
class="flex
|
|
392
|
+
class="flex items-center gap-3 px-[18px]"
|
|
393
|
+
:class="props.showBar ? 'h-[60px]' : 'h-14'"
|
|
366
394
|
>
|
|
367
395
|
<USkeleton
|
|
368
396
|
:aria-label="t('dms.a11y.loading')"
|
|
@@ -379,7 +407,7 @@ const gridStyle = computed(() => ({
|
|
|
379
407
|
<USkeleton :aria-label="t('dms.a11y.loading')" class="h-3 w-14" />
|
|
380
408
|
<USkeleton
|
|
381
409
|
:aria-label="t('dms.a11y.loading')"
|
|
382
|
-
v-if="
|
|
410
|
+
v-if="showRowDelta"
|
|
383
411
|
class="h-3 w-[46px]"
|
|
384
412
|
/>
|
|
385
413
|
</div>
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { ChartXAxisFormatter } from "./types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Width of one character of the axis font (11px, monospace) in pixels, with a
|
|
5
|
+
* little room for a proportional fallback font.
|
|
6
|
+
*/
|
|
7
|
+
const AXIS_CHAR_WIDTH = 7;
|
|
8
|
+
/** Blank space kept between two neighbouring labels. */
|
|
9
|
+
const LABEL_GAP = 8;
|
|
10
|
+
|
|
11
|
+
/** What Apex hands an x-axis label formatter (`xaxis.labels.formatter`). */
|
|
12
|
+
interface ApexLabelFormatterOpts {
|
|
13
|
+
/** The label's column; only set while Apex draws the axis. */
|
|
14
|
+
i?: number;
|
|
15
|
+
w?: { globals?: { gridWidth?: number; labels?: unknown[] } };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type ApexLabelFormatter = (
|
|
19
|
+
value: string,
|
|
20
|
+
timestamp?: number,
|
|
21
|
+
opts?: ApexLabelFormatterOpts,
|
|
22
|
+
) => string;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* How many columns each drawn label stands for: 1 while every label fits its
|
|
26
|
+
* column, more once the longest one is wider than a column.
|
|
27
|
+
*/
|
|
28
|
+
export function categoryLabelStep(
|
|
29
|
+
gridWidth: number,
|
|
30
|
+
labelCount: number,
|
|
31
|
+
longestLabel: number,
|
|
32
|
+
): number {
|
|
33
|
+
if (labelCount <= 1 || gridWidth <= 0) return 1;
|
|
34
|
+
const columnWidth = gridWidth / labelCount;
|
|
35
|
+
const labelWidth = longestLabel * AXIS_CHAR_WIDTH + LABEL_GAP;
|
|
36
|
+
return Math.max(1, Math.ceil(labelWidth / columnWidth));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* A heatmap gives every column a label and ApexCharts does not thin them
|
|
41
|
+
* (`hideOverlappingLabels` only acts on other charts), so on a phone they run
|
|
42
|
+
* into each other ("07:0009:00"). This formatter keeps one label every few
|
|
43
|
+
* columns, from the plot's width, and leaves the tooltip's (no column index)
|
|
44
|
+
* whole.
|
|
45
|
+
*/
|
|
46
|
+
export function thinnedCategoryFormatter(
|
|
47
|
+
format: ChartXAxisFormatter | undefined,
|
|
48
|
+
): ApexLabelFormatter {
|
|
49
|
+
const write = (value: string, timestamp?: number): string =>
|
|
50
|
+
format ? format(value, timestamp) : String(value ?? "");
|
|
51
|
+
return (value, timestamp, opts) => {
|
|
52
|
+
const label = write(value, timestamp);
|
|
53
|
+
const index = opts?.i;
|
|
54
|
+
const globals = opts?.w?.globals;
|
|
55
|
+
if (typeof index !== "number" || !globals) return label;
|
|
56
|
+
const labels = globals.labels ?? [];
|
|
57
|
+
const longest = labels.reduce<number>(
|
|
58
|
+
(max, entry) => Math.max(max, write(String(entry ?? "")).length),
|
|
59
|
+
0,
|
|
60
|
+
);
|
|
61
|
+
const step = categoryLabelStep(
|
|
62
|
+
globals.gridWidth ?? 0,
|
|
63
|
+
labels.length,
|
|
64
|
+
longest,
|
|
65
|
+
);
|
|
66
|
+
return index % step === 0 ? label : "";
|
|
67
|
+
};
|
|
68
|
+
}
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import type { ChartSeries } from "./types";
|
|
2
|
+
import type { UseApexChartInput } from "./useApexChart.types";
|
|
3
|
+
|
|
4
|
+
/** The calendar step a series of dates moves by. */
|
|
5
|
+
export type CalendarBucket = "month" | "day";
|
|
6
|
+
|
|
7
|
+
/** How a bucket's dates are written: on the axis, then in the tooltip. */
|
|
8
|
+
interface BucketFormats {
|
|
9
|
+
/** ApexCharts tokens, as its own datetime axis writes them. */
|
|
10
|
+
axis: string;
|
|
11
|
+
/** Intl options, for the tooltip and wherever Apex lends no formatter. */
|
|
12
|
+
axisIntl: Intl.DateTimeFormatOptions;
|
|
13
|
+
tooltipIntl: Intl.DateTimeFormatOptions;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const BUCKET_FORMATS: Record<CalendarBucket, BucketFormats> = {
|
|
17
|
+
month: {
|
|
18
|
+
axis: "MMM 'yy",
|
|
19
|
+
axisIntl: { month: "short", year: "2-digit" },
|
|
20
|
+
tooltipIntl: { month: "long", year: "numeric" },
|
|
21
|
+
},
|
|
22
|
+
day: {
|
|
23
|
+
axis: "dd MMM",
|
|
24
|
+
axisIntl: { day: "2-digit", month: "short" },
|
|
25
|
+
tooltipIntl: { day: "numeric", month: "short", year: "numeric" },
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/** Beyond this many buckets the datetime axis thins its own labels better. */
|
|
30
|
+
const MAX_BUCKETS = 120;
|
|
31
|
+
const DATE_ONLY = /^(\d{4})-(\d{2})-(\d{2})$/;
|
|
32
|
+
const BUCKETED_TYPES = new Set(["column"]);
|
|
33
|
+
|
|
34
|
+
interface CalendarDay {
|
|
35
|
+
year: number;
|
|
36
|
+
month: number;
|
|
37
|
+
day: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
type ApexDateFormatter = (date: Date, format: string) => string;
|
|
41
|
+
|
|
42
|
+
/** What Apex hands an axis label formatter, past the value. */
|
|
43
|
+
interface ApexLabelOptions {
|
|
44
|
+
dateFormatter?: ApexDateFormatter;
|
|
45
|
+
w?: unknown;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** A column chart's dates turned into one category per calendar bucket. */
|
|
49
|
+
export interface DatetimeBuckets {
|
|
50
|
+
bucket: CalendarBucket;
|
|
51
|
+
/** Every bucket from the first date to the last, as `YYYY-MM-DD`. */
|
|
52
|
+
keys: string[];
|
|
53
|
+
/**
|
|
54
|
+
* Per series, the index in the route's `data` of the point in each bucket,
|
|
55
|
+
* `-1` for an empty bucket: a click names the point the route sent.
|
|
56
|
+
*/
|
|
57
|
+
sourceIndexes: number[][];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function pad(value: number): string {
|
|
61
|
+
return String(value).padStart(2, "0");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function keyOf(day: CalendarDay): string {
|
|
65
|
+
return `${day.year}-${pad(day.month)}-${pad(day.day)}`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function dateOfKey(key: string): Date {
|
|
69
|
+
const [, year, month, day] = DATE_ONLY.exec(key)!;
|
|
70
|
+
return new Date(Number(year), Number(month) - 1, Number(day));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function localDay(date: Date): CalendarDay | null {
|
|
74
|
+
const isMidnight =
|
|
75
|
+
date.getHours() === 0 && date.getMinutes() === 0 && date.getSeconds() === 0;
|
|
76
|
+
if (!isMidnight) return null;
|
|
77
|
+
return {
|
|
78
|
+
year: date.getFullYear(),
|
|
79
|
+
month: date.getMonth() + 1,
|
|
80
|
+
day: date.getDate(),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function utcDay(date: Date): CalendarDay | null {
|
|
85
|
+
const isMidnight =
|
|
86
|
+
date.getUTCHours() === 0 &&
|
|
87
|
+
date.getUTCMinutes() === 0 &&
|
|
88
|
+
date.getUTCSeconds() === 0;
|
|
89
|
+
if (!isMidnight) return null;
|
|
90
|
+
return {
|
|
91
|
+
year: date.getUTCFullYear(),
|
|
92
|
+
month: date.getUTCMonth() + 1,
|
|
93
|
+
day: date.getUTCDate(),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The calendar day a point stands for. A date-only string is that day; a
|
|
99
|
+
* timestamp counts when it falls on a midnight, the browser's or UTC's: a
|
|
100
|
+
* route bucketing by month often sends `2026-10-01T00:00:00Z`.
|
|
101
|
+
*/
|
|
102
|
+
function calendarDayOf(
|
|
103
|
+
x: unknown,
|
|
104
|
+
readDay: typeof localDay,
|
|
105
|
+
): CalendarDay | null {
|
|
106
|
+
if (typeof x === "string") {
|
|
107
|
+
const match = DATE_ONLY.exec(x);
|
|
108
|
+
if (match) {
|
|
109
|
+
return {
|
|
110
|
+
year: Number(match[1]),
|
|
111
|
+
month: Number(match[2]),
|
|
112
|
+
day: Number(match[3]),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (typeof x !== "string" && typeof x !== "number" && !(x instanceof Date)) {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
const date = new Date(x);
|
|
120
|
+
return Number.isNaN(date.getTime()) ? null : readDay(date);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function pointX(point: ChartSeries["data"][number]): unknown {
|
|
124
|
+
return typeof point === "object" && point !== null ? point.x : undefined;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Each series' days, in order, or `null` when a point is not a day. */
|
|
128
|
+
function readDays(
|
|
129
|
+
series: ChartSeries[],
|
|
130
|
+
readDay: typeof localDay,
|
|
131
|
+
): CalendarDay[][] | null {
|
|
132
|
+
const days: CalendarDay[][] = [];
|
|
133
|
+
for (const entry of series) {
|
|
134
|
+
const seriesDays: CalendarDay[] = [];
|
|
135
|
+
for (const point of entry.data) {
|
|
136
|
+
const day = calendarDayOf(pointX(point), readDay);
|
|
137
|
+
if (!day) return null;
|
|
138
|
+
seriesDays.push(day);
|
|
139
|
+
}
|
|
140
|
+
days.push(seriesDays);
|
|
141
|
+
}
|
|
142
|
+
return days.some((entry) => entry.length > 0) ? days : null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function nextBucket(day: CalendarDay, bucket: CalendarBucket): CalendarDay {
|
|
146
|
+
const date =
|
|
147
|
+
bucket === "month"
|
|
148
|
+
? new Date(day.year, day.month, 1)
|
|
149
|
+
: new Date(day.year, day.month - 1, day.day + 1);
|
|
150
|
+
return {
|
|
151
|
+
year: date.getFullYear(),
|
|
152
|
+
month: date.getMonth() + 1,
|
|
153
|
+
day: date.getDate(),
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Every bucket from `first` to `last`, or `null` past the bucket cap. */
|
|
158
|
+
function bucketKeys(
|
|
159
|
+
first: string,
|
|
160
|
+
last: string,
|
|
161
|
+
bucket: CalendarBucket,
|
|
162
|
+
): string[] | null {
|
|
163
|
+
const keys: string[] = [];
|
|
164
|
+
let cursor = calendarDayOf(first, localDay)!;
|
|
165
|
+
while (keyOf(cursor) <= last) {
|
|
166
|
+
keys.push(keyOf(cursor));
|
|
167
|
+
if (keys.length > MAX_BUCKETS) return null;
|
|
168
|
+
cursor = nextBucket(cursor, bucket);
|
|
169
|
+
}
|
|
170
|
+
return keys;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Where each series' points fall among `keys`, or `null` when a series holds
|
|
175
|
+
* two points in one bucket or a date between buckets (the 15th on a monthly
|
|
176
|
+
* axis): those keep the datetime axis.
|
|
177
|
+
*/
|
|
178
|
+
function sourceIndexesOf(
|
|
179
|
+
days: CalendarDay[][],
|
|
180
|
+
keys: string[],
|
|
181
|
+
): number[][] | null {
|
|
182
|
+
const positions = new Map(keys.map((key, position) => [key, position]));
|
|
183
|
+
const indexes: number[][] = [];
|
|
184
|
+
for (const seriesDays of days) {
|
|
185
|
+
const slots = keys.map(() => -1);
|
|
186
|
+
for (const [index, day] of seriesDays.entries()) {
|
|
187
|
+
const position = positions.get(keyOf(day));
|
|
188
|
+
if (position === undefined || slots[position] !== -1) return null;
|
|
189
|
+
slots[position] = index;
|
|
190
|
+
}
|
|
191
|
+
indexes.push(slots);
|
|
192
|
+
}
|
|
193
|
+
return indexes;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Whether two dates sit one bucket apart somewhere: weekly or quarterly
|
|
198
|
+
* points have none, and the datetime axis spaces them better than buckets
|
|
199
|
+
* mostly left empty.
|
|
200
|
+
*/
|
|
201
|
+
function hasAdjacentBuckets(sourceIndexes: number[][]): boolean {
|
|
202
|
+
return sourceIndexes.some((slots) =>
|
|
203
|
+
slots.some(
|
|
204
|
+
(index, position) =>
|
|
205
|
+
index !== -1 &&
|
|
206
|
+
slots[position + 1] !== undefined &&
|
|
207
|
+
slots[position + 1] !== -1,
|
|
208
|
+
),
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function layoutBuckets(
|
|
213
|
+
days: CalendarDay[][],
|
|
214
|
+
bucket: CalendarBucket,
|
|
215
|
+
): DatetimeBuckets | null {
|
|
216
|
+
const sorted = days.flat().map(keyOf).sort();
|
|
217
|
+
const keys = bucketKeys(sorted[0]!, sorted.at(-1)!, bucket);
|
|
218
|
+
if (!keys) return null;
|
|
219
|
+
const sourceIndexes = sourceIndexesOf(days, keys);
|
|
220
|
+
if (!sourceIndexes) return null;
|
|
221
|
+
const isSingleBucket = keys.length === 1;
|
|
222
|
+
if (!isSingleBucket && !hasAdjacentBuckets(sourceIndexes)) return null;
|
|
223
|
+
return { bucket, keys, sourceIndexes };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Whether a column chart on a datetime axis plots calendar buckets — every
|
|
228
|
+
* point on a day, or on the first of a month — and which ones. Apex places
|
|
229
|
+
* such columns by time and ticks the axis on its own: months of uneven length
|
|
230
|
+
* leave uneven gaps, and a tick every half month repeats each month's name.
|
|
231
|
+
*/
|
|
232
|
+
export function detectDatetimeBuckets(
|
|
233
|
+
input: UseApexChartInput,
|
|
234
|
+
): DatetimeBuckets | null {
|
|
235
|
+
if (input.xaxisType !== "datetime") return null;
|
|
236
|
+
if (!BUCKETED_TYPES.has(input.type)) return null;
|
|
237
|
+
const days =
|
|
238
|
+
readDays(input.series, localDay) ?? readDays(input.series, utcDay);
|
|
239
|
+
if (!days) return null;
|
|
240
|
+
const bucket: CalendarBucket = days.flat().every((day) => day.day === 1)
|
|
241
|
+
? "month"
|
|
242
|
+
: "day";
|
|
243
|
+
return layoutBuckets(days, bucket);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* The series on the bucket axis: one point per bucket, named by it, an empty
|
|
248
|
+
* bucket holding `null` so it keeps its slot.
|
|
249
|
+
*/
|
|
250
|
+
export function toBucketSeries(
|
|
251
|
+
series: ChartSeries[],
|
|
252
|
+
buckets: DatetimeBuckets,
|
|
253
|
+
): ChartSeries[] {
|
|
254
|
+
return series.map((entry, seriesIndex) => ({
|
|
255
|
+
...entry,
|
|
256
|
+
data: buckets.keys.map((key, position) => {
|
|
257
|
+
const point = entry.data[buckets.sourceIndexes[seriesIndex]![position]!];
|
|
258
|
+
return typeof point === "object"
|
|
259
|
+
? { ...point, x: key }
|
|
260
|
+
: { x: key, y: null };
|
|
261
|
+
}),
|
|
262
|
+
}));
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function intlFormat(
|
|
266
|
+
key: string,
|
|
267
|
+
locale: string | undefined,
|
|
268
|
+
options: Intl.DateTimeFormatOptions,
|
|
269
|
+
): string {
|
|
270
|
+
return new Intl.DateTimeFormat(locale, options).format(dateOfKey(key));
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* The axis label of a bucket: in Apex's own tokens (`xAxis.datetimeFormat`,
|
|
275
|
+
* else what its datetime axis writes for that step) when Apex lends its
|
|
276
|
+
* formatter, which speaks the chart's locale, else through Intl.
|
|
277
|
+
*/
|
|
278
|
+
export function bucketAxisFormatter(
|
|
279
|
+
buckets: DatetimeBuckets,
|
|
280
|
+
input: UseApexChartInput,
|
|
281
|
+
): (value: string, timestamp?: unknown, opts?: ApexLabelOptions) => string {
|
|
282
|
+
const formats = BUCKET_FORMATS[buckets.bucket];
|
|
283
|
+
return (value, _timestamp, opts) => {
|
|
284
|
+
if (!DATE_ONLY.test(String(value))) return String(value ?? "");
|
|
285
|
+
if (input.xAxisFormatter) {
|
|
286
|
+
return input.xAxisFormatter(value, dateOfKey(value).getTime());
|
|
287
|
+
}
|
|
288
|
+
if (opts?.dateFormatter && opts.w) {
|
|
289
|
+
const format = input.xAxis?.datetimeFormat ?? formats.axis;
|
|
290
|
+
return opts.dateFormatter.call({ w: opts.w }, dateOfKey(value), format);
|
|
291
|
+
}
|
|
292
|
+
return intlFormat(value, input.locale, formats.axisIntl);
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** The tooltip title of a bucket: the whole month, or the whole day. */
|
|
297
|
+
export function bucketTooltipFormatter(
|
|
298
|
+
buckets: DatetimeBuckets,
|
|
299
|
+
input: UseApexChartInput,
|
|
300
|
+
): (value: string) => string {
|
|
301
|
+
const formats = BUCKET_FORMATS[buckets.bucket];
|
|
302
|
+
return (value) =>
|
|
303
|
+
DATE_ONLY.test(String(value))
|
|
304
|
+
? intlFormat(value, input.locale, formats.tooltipIntl)
|
|
305
|
+
: String(value ?? "");
|
|
306
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readableNumberFormat } from "#dms-core/app/utils/formatter";
|
|
1
2
|
import type { ValueFormat, ValuePrecision } from "./types";
|
|
2
3
|
|
|
3
4
|
const PERCENT_DIVISOR = 100;
|
|
@@ -81,7 +82,7 @@ export function formatValue(
|
|
|
81
82
|
precision?: ValuePrecision,
|
|
82
83
|
): string {
|
|
83
84
|
const config = resolveConfig(format);
|
|
84
|
-
return
|
|
85
|
+
return readableNumberFormat(
|
|
85
86
|
locale,
|
|
86
87
|
precisionOptions(config.options(currency), precision),
|
|
87
88
|
).format(config.transform(value));
|
|
@@ -98,7 +99,7 @@ export function formatDeltaPercent(
|
|
|
98
99
|
delta: number,
|
|
99
100
|
locale = DEFAULT_LOCALE,
|
|
100
101
|
): string {
|
|
101
|
-
return
|
|
102
|
+
return readableNumberFormat(locale, {
|
|
102
103
|
style: "percent",
|
|
103
104
|
minimumFractionDigits: PERCENT_FRACTION_DIGITS,
|
|
104
105
|
maximumFractionDigits: PERCENT_FRACTION_DIGITS,
|
|
@@ -144,7 +145,7 @@ export function formatValueParts(
|
|
|
144
145
|
};
|
|
145
146
|
}
|
|
146
147
|
|
|
147
|
-
const parts =
|
|
148
|
+
const parts = readableNumberFormat(
|
|
148
149
|
locale,
|
|
149
150
|
precisionOptions(config.options(currency), precision),
|
|
150
151
|
).formatToParts(config.transform(value));
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
const HEX_SHORT_LENGTH = 3;
|
|
2
|
+
const HEX_LONG_LENGTH = 6;
|
|
3
|
+
const HEX_RADIX = 16;
|
|
4
|
+
const COLOR_CHANNEL_MAX = 255;
|
|
5
|
+
/** Below this relative luminance a card surface reads as a dark theme. */
|
|
6
|
+
const DARK_SURFACE_LUMINANCE = 0.5;
|
|
7
|
+
const LUMINANCE_WEIGHTS = { red: 0.2126, green: 0.7152, blue: 0.0722 };
|
|
8
|
+
const RGB_FUNCTION = /^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)/i;
|
|
9
|
+
|
|
10
|
+
interface RgbColor {
|
|
11
|
+
red: number;
|
|
12
|
+
green: number;
|
|
13
|
+
blue: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function parseHex(color: string): RgbColor | null {
|
|
17
|
+
const digits = color.slice(1);
|
|
18
|
+
const long =
|
|
19
|
+
digits.length === HEX_SHORT_LENGTH
|
|
20
|
+
? [...digits].map((digit) => digit + digit).join("")
|
|
21
|
+
: digits;
|
|
22
|
+
if (long.length !== HEX_LONG_LENGTH) return null;
|
|
23
|
+
const value = Number.parseInt(long, HEX_RADIX);
|
|
24
|
+
if (Number.isNaN(value)) return null;
|
|
25
|
+
return {
|
|
26
|
+
red: (value >> 16) & COLOR_CHANNEL_MAX,
|
|
27
|
+
green: (value >> 8) & COLOR_CHANNEL_MAX,
|
|
28
|
+
blue: value & COLOR_CHANNEL_MAX,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The channels of a resolved chart colour (`#hex` or comma `rgb()`). */
|
|
33
|
+
function parseColor(color: string): RgbColor | null {
|
|
34
|
+
if (color.startsWith("#")) return parseHex(color);
|
|
35
|
+
const match = RGB_FUNCTION.exec(color);
|
|
36
|
+
if (!match) return null;
|
|
37
|
+
return {
|
|
38
|
+
red: Number(match[1]),
|
|
39
|
+
green: Number(match[2]),
|
|
40
|
+
blue: Number(match[3]),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Whether the card surface is a dark theme's. */
|
|
45
|
+
export function isDarkSurface(surface: string): boolean {
|
|
46
|
+
const rgb = parseColor(surface);
|
|
47
|
+
if (!rgb) return false;
|
|
48
|
+
const luminance =
|
|
49
|
+
(LUMINANCE_WEIGHTS.red * rgb.red +
|
|
50
|
+
LUMINANCE_WEIGHTS.green * rgb.green +
|
|
51
|
+
LUMINANCE_WEIGHTS.blue * rgb.blue) /
|
|
52
|
+
COLOR_CHANNEL_MAX;
|
|
53
|
+
return luminance < DARK_SURFACE_LUMINANCE;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* `color` laid over the card surface at `weight` (0 = the surface, 1 = the
|
|
58
|
+
* colour): a faint cell fades into the card in both themes, rather than
|
|
59
|
+
* towards white. A colour that cannot be read is kept as it is.
|
|
60
|
+
*/
|
|
61
|
+
function mixOverSurface(
|
|
62
|
+
color: string,
|
|
63
|
+
surface: string,
|
|
64
|
+
weight: number,
|
|
65
|
+
): string {
|
|
66
|
+
const front = parseColor(color);
|
|
67
|
+
const back = parseColor(surface);
|
|
68
|
+
if (!front || !back) return color;
|
|
69
|
+
const channel = (key: keyof RgbColor) =>
|
|
70
|
+
Math.round(back[key] + (front[key] - back[key]) * weight);
|
|
71
|
+
return `rgb(${channel("red")}, ${channel("green")}, ${channel("blue")})`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* The colours of a heatmap's value ranges, lowest first: each palette colour
|
|
76
|
+
* over the surface, from `1 - intensity` of it for the lowest range to the
|
|
77
|
+
* full colour for the highest.
|
|
78
|
+
*/
|
|
79
|
+
export function heatmapRangeColors(
|
|
80
|
+
palette: string[],
|
|
81
|
+
surface: string,
|
|
82
|
+
intensity: number,
|
|
83
|
+
): string[] {
|
|
84
|
+
const faintest = 1 - Math.min(Math.max(intensity, 0), 1);
|
|
85
|
+
const lastIndex = Math.max(palette.length - 1, 1);
|
|
86
|
+
return palette.map((color, index) =>
|
|
87
|
+
mixOverSurface(
|
|
88
|
+
color,
|
|
89
|
+
surface,
|
|
90
|
+
faintest + ((1 - faintest) * index) / lastIndex,
|
|
91
|
+
),
|
|
92
|
+
);
|
|
93
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { ChartSeries, DonutRecord } from "./types";
|
|
2
|
+
|
|
3
|
+
/** Writes one display string in the reader's language (`processI18n`). */
|
|
4
|
+
export type ChartTextTranslate = (text: string) => string;
|
|
5
|
+
|
|
6
|
+
type ChartSeriesEntry = ChartSeries["data"][number];
|
|
7
|
+
|
|
8
|
+
interface TextPoint {
|
|
9
|
+
x?: unknown;
|
|
10
|
+
label?: unknown;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function translateText(value: unknown, translate: ChartTextTranslate): unknown {
|
|
14
|
+
return typeof value === "string" ? translate(value) : value;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* A point's category (`x`) and `label` are what Apex prints on the axis, in
|
|
19
|
+
* the tooltip and on a slice: a `$` key there is translated like a name. A
|
|
20
|
+
* date or a number is left alone, and so is any string without the `$`.
|
|
21
|
+
*/
|
|
22
|
+
function localizePoint(
|
|
23
|
+
point: ChartSeriesEntry,
|
|
24
|
+
translate: ChartTextTranslate,
|
|
25
|
+
): ChartSeriesEntry {
|
|
26
|
+
if (typeof point !== "object" || point === null) return point;
|
|
27
|
+
const text = point as TextPoint;
|
|
28
|
+
const localized: Record<string, unknown> = { ...point };
|
|
29
|
+
if ("x" in text) localized.x = translateText(text.x, translate);
|
|
30
|
+
if ("label" in text) localized.label = translateText(text.label, translate);
|
|
31
|
+
return localized as unknown as ChartSeriesEntry;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* A series as the reader sees it: its `name`, and the categories and labels
|
|
36
|
+
* of its points, translated when they follow the `$`-prefix convention.
|
|
37
|
+
* Apex prints all of them verbatim, so a route's `$` keys are resolved here,
|
|
38
|
+
* in the browser, in the language on screen.
|
|
39
|
+
*/
|
|
40
|
+
export function localizeSeries(
|
|
41
|
+
series: ChartSeries,
|
|
42
|
+
translate: ChartTextTranslate,
|
|
43
|
+
): ChartSeries {
|
|
44
|
+
return {
|
|
45
|
+
...series,
|
|
46
|
+
name: translateText(series.name, translate) as string,
|
|
47
|
+
data: Array.isArray(series.data)
|
|
48
|
+
? series.data.map((point) => localizePoint(point, translate))
|
|
49
|
+
: series.data,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** The slices of a circular chart, their labels translated. */
|
|
54
|
+
export function localizeDonutRecords(
|
|
55
|
+
records: DonutRecord[],
|
|
56
|
+
translate: ChartTextTranslate,
|
|
57
|
+
): DonutRecord[] {
|
|
58
|
+
return records.map((record) => ({
|
|
59
|
+
...record,
|
|
60
|
+
label: translateText(record.label, translate) as string,
|
|
61
|
+
}));
|
|
62
|
+
}
|