@antelopejs/dms 0.8.2 → 0.9.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 +36 -0
- package/dist/implementations/dms/frontend-backend-proxies.d.ts +3 -0
- package/dist/implementations/dms/frontend-backend-proxies.js +39 -0
- package/dist/implementations/dms/frontend-backend-proxies.js.map +1 -0
- package/dist/implementations/dms/frontend-bootstrap.d.ts +1 -0
- package/dist/implementations/dms/frontend-bootstrap.js +13 -0
- package/dist/implementations/dms/frontend-bootstrap.js.map +1 -1
- package/dist/implementations/dms/frontend-modules.d.ts +4 -2
- package/dist/implementations/dms/frontend-modules.js +8 -7
- package/dist/implementations/dms/frontend-modules.js.map +1 -1
- package/dist/implementations/dms/manifest.d.ts +6 -0
- package/dist/implementations/dms/manifest.js +13 -0
- package/dist/implementations/dms/manifest.js.map +1 -1
- package/dist/implementations/dms/menu-categories.d.ts +16 -0
- package/dist/implementations/dms/menu-categories.js +30 -0
- package/dist/implementations/dms/menu-categories.js.map +1 -0
- package/dist/implementations/dms/nav-badges.d.ts +0 -8
- package/dist/implementations/dms/nav-badges.js +0 -16
- package/dist/implementations/dms/nav-badges.js.map +1 -1
- package/dist/implementations/dms/page.d.ts +11 -5
- package/dist/implementations/dms/page.js +56 -41
- package/dist/implementations/dms/page.js.map +1 -1
- package/dist/implementations/dms-html-render/index.js +5 -2
- package/dist/implementations/dms-html-render/index.js.map +1 -1
- package/dist/implementations/dms-notifications/index.d.ts +4 -3
- package/dist/implementations/dms-notifications/index.js +14 -6
- package/dist/implementations/dms-notifications/index.js.map +1 -1
- package/dist/pages/settings/settings-index.js +3 -2
- package/dist/pages/settings/settings-index.js.map +1 -1
- package/dist/pages/settings/users/invites.js +2 -2
- package/dist/pages/settings/users/invites.js.map +1 -1
- package/dist/pages/settings/users/member-change-notifications.js +17 -6
- package/dist/pages/settings/users/member-change-notifications.js.map +1 -1
- package/dist/pages/settings/users/member-invite-form.d.ts +3 -2
- package/dist/pages/settings/users/member-invite-form.js +7 -4
- package/dist/pages/settings/users/member-invite-form.js.map +1 -1
- package/dist/pages/settings/users/member-roles-field.js +2 -1
- package/dist/pages/settings/users/member-roles-field.js.map +1 -1
- package/dist/pages/settings/users/members.js +4 -1
- package/dist/pages/settings/users/members.js.map +1 -1
- package/dist/pages/settings/users/notifications.d.ts +6 -0
- package/dist/pages/settings/users/notifications.js +23 -15
- package/dist/pages/settings/users/notifications.js.map +1 -1
- package/dist/pages/settings/users/profile.js +1 -1
- package/dist/pages/settings/users/profile.js.map +1 -1
- package/dist/pages/settings/users/region-form.d.ts +1 -1
- package/dist/pages/settings/users/region-form.js +2 -1
- package/dist/pages/settings/users/region-form.js.map +1 -1
- package/dist/pages/settings/users/role-editor-store.d.ts +3 -1
- package/dist/pages/settings/users/role-editor-store.js +17 -4
- package/dist/pages/settings/users/role-editor-store.js.map +1 -1
- package/dist/pages/settings/users/roles.js +1 -0
- package/dist/pages/settings/users/roles.js.map +1 -1
- package/dist/realtime/registry.d.ts +1 -0
- package/dist/realtime/registry.js +5 -0
- package/dist/realtime/registry.js.map +1 -1
- package/dist/routes/realtime.d.ts +5 -1
- package/dist/routes/realtime.js +16 -9
- package/dist/routes/realtime.js.map +1 -1
- package/dist/test/integration/account-deletion.test.js +2 -2
- package/dist/test/integration/account-deletion.test.js.map +1 -1
- package/dist/test/integration/auth/auth-key-rotation.test.js +2 -1
- package/dist/test/integration/auth/auth-key-rotation.test.js.map +1 -1
- package/dist/test/integration/auth/current-password.test.js +2 -1
- package/dist/test/integration/auth/current-password.test.js.map +1 -1
- package/dist/test/integration/auth/session-revocation.test.js +2 -1
- package/dist/test/integration/auth/session-revocation.test.js.map +1 -1
- package/dist/test/integration/auth/sign-in-country.test.js +2 -1
- package/dist/test/integration/auth/sign-in-country.test.js.map +1 -1
- package/dist/test/integration/auth/two-factor-codes.test.js +2 -1
- package/dist/test/integration/auth/two-factor-codes.test.js.map +1 -1
- package/dist/test/integration/auth/two-factor-password.test.js +3 -2
- package/dist/test/integration/auth/two-factor-password.test.js.map +1 -1
- package/dist/test/integration/email-change.test.js +2 -1
- package/dist/test/integration/email-change.test.js.map +1 -1
- package/dist/test/integration/files/component-files.test.js +2 -2
- package/dist/test/integration/files/component-files.test.js.map +1 -1
- package/dist/test/integration/member-settings-access.test.js +8 -7
- package/dist/test/integration/member-settings-access.test.js.map +1 -1
- package/dist/test/integration/menu-access-events.test.d.ts +1 -0
- package/dist/test/integration/menu-access-events.test.js +148 -0
- package/dist/test/integration/menu-access-events.test.js.map +1 -0
- package/dist/test/integration/notification-inbox.test.js +4 -1
- package/dist/test/integration/notification-inbox.test.js.map +1 -1
- package/dist/test/integration/notification-locked-subjects.test.js +3 -2
- package/dist/test/integration/notification-locked-subjects.test.js.map +1 -1
- package/dist/test/integration/notification-unread-badge.test.js +51 -5
- package/dist/test/integration/notification-unread-badge.test.js.map +1 -1
- package/dist/test/integration/page-layout-route-guard.test.d.ts +1 -0
- package/dist/test/integration/page-layout-route-guard.test.js +56 -0
- package/dist/test/integration/page-layout-route-guard.test.js.map +1 -0
- package/dist/test/integration/permission-warnings.test.js +3 -2
- package/dist/test/integration/permission-warnings.test.js.map +1 -1
- package/dist/test/integration/removed-member-notifications.test.js +3 -2
- package/dist/test/integration/removed-member-notifications.test.js.map +1 -1
- package/dist/test/integration/role-delete-reassign.test.js +2 -1
- package/dist/test/integration/role-delete-reassign.test.js.map +1 -1
- package/dist/test/integration/role-permission-ancestors.test.js +2 -1
- package/dist/test/integration/role-permission-ancestors.test.js.map +1 -1
- package/dist/test/unit/frontend-bootstrap.test.js +13 -0
- package/dist/test/unit/frontend-bootstrap.test.js.map +1 -1
- package/dist/test/unit/implementations/dms/category-slug-registry.test.js +14 -5
- package/dist/test/unit/implementations/dms/category-slug-registry.test.js.map +1 -1
- package/dist/test/unit/implementations/dms/frontend-backend-proxies.test.d.ts +1 -0
- package/dist/test/unit/implementations/dms/frontend-backend-proxies.test.js +66 -0
- package/dist/test/unit/implementations/dms/frontend-backend-proxies.test.js.map +1 -0
- package/dist/test/unit/implementations/dms/manifest-strip.test.js +26 -0
- package/dist/test/unit/implementations/dms/manifest-strip.test.js.map +1 -1
- package/dist/test/unit/implementations/dms/nav-badges.test.js +23 -9
- package/dist/test/unit/implementations/dms/nav-badges.test.js.map +1 -1
- package/dist/test/unit/implementations/dms-html-render/generate-html.test.js +22 -4
- package/dist/test/unit/implementations/dms-html-render/generate-html.test.js.map +1 -1
- package/dist/test/unit/interfaces/dms/page-layout-query.test.js +12 -0
- package/dist/test/unit/interfaces/dms/page-layout-query.test.js.map +1 -1
- package/dist/test/unit/interfaces/dms/page-teardown.test.js +1 -1
- package/dist/test/unit/interfaces/dms/page-teardown.test.js.map +1 -1
- package/dist/test/unit/interfaces/dms-base/table-and-card-catalog.test.js +25 -0
- package/dist/test/unit/interfaces/dms-base/table-and-card-catalog.test.js.map +1 -1
- package/dist/test/unit/pages/settings/member-roles-field.test.js +1 -1
- package/dist/test/unit/pages/settings/member-roles-field.test.js.map +1 -1
- package/dist/test/unit/pages/settings/role-delete-confirm.test.js.map +1 -1
- package/dist/test/unit/realtime/menu-topics.test.d.ts +1 -0
- package/dist/test/unit/realtime/menu-topics.test.js +26 -0
- package/dist/test/unit/realtime/menu-topics.test.js.map +1 -0
- package/dist/validation/regional-preferences.schema.d.ts +2 -2
- package/frontend-vue/deferred-plugins.ts +3 -4
- package/frontend-vue/dms.email.ts +5 -1
- package/frontend-vue/dms.frontend.ts +15 -5
- package/frontend-vue/dms.server.ts +74 -0
- package/frontend-vue/layers/dms-auth/app/composables/useLogout.ts +1 -1
- package/frontend-vue/layers/dms-auth/app/composables/usePostLoginRedirect.ts +16 -3
- package/frontend-vue/layers/dms-auth/app/custom-pages/auth/2fa.vue +3 -1
- package/frontend-vue/layers/dms-auth/app/custom-pages/auth/backup.vue +3 -1
- package/frontend-vue/layers/dms-auth/app/custom-pages/auth/forgot-validation.vue +1 -1
- package/frontend-vue/layers/dms-auth/app/custom-pages/auth/oauth-complete.vue +26 -3
- package/frontend-vue/layers/dms-auth/app/custom-pages/auth/recover.vue +1 -1
- package/frontend-vue/layers/dms-auth/app/custom-pages/auth/signup.vue +22 -25
- package/frontend-vue/layers/dms-auth/app/custom-pages/auth/validate.vue +1 -1
- package/frontend-vue/layers/dms-auth/app/middleware/auth.ts +5 -7
- package/frontend-vue/layers/dms-auth/app/plugins/permissions-preload.ts +17 -0
- package/frontend-vue/layers/dms-auth/server/accounts.ts +135 -0
- package/frontend-vue/layers/dms-auth/server/actions.ts +251 -0
- package/frontend-vue/layers/dms-auth/server/oauth.ts +260 -0
- package/frontend-vue/layers/dms-auth/server/session.ts +158 -0
- package/frontend-vue/layers/dms-core/app/build/utils/asyncDataScope.ts +47 -0
- package/frontend-vue/layers/dms-core/app/build/utils/permission-preview.ts +6 -2
- package/frontend-vue/layers/dms-core/app/composables/auth/useApiData.ts +148 -0
- package/frontend-vue/layers/dms-core/app/composables/auth/useAuthFetch.ts +7 -20
- package/frontend-vue/layers/dms-core/app/composables/auth/useCurrentUser.ts +2 -2
- package/frontend-vue/layers/dms-core/app/composables/auth/useMultiAccount.ts +18 -5
- package/frontend-vue/layers/dms-core/app/composables/auth/usePermissions.ts +35 -6
- package/frontend-vue/layers/dms-core/app/composables/auth/useSessionRecovery.ts +1 -1
- package/frontend-vue/layers/dms-core/app/composables/period/compareResolvers.ts +20 -14
- package/frontend-vue/layers/dms-core/app/composables/period/periodClock.ts +121 -0
- package/frontend-vue/layers/dms-core/app/composables/period/periodRenderNow.ts +42 -0
- package/frontend-vue/layers/dms-core/app/composables/period/periodTimeZone.ts +31 -0
- package/frontend-vue/layers/dms-core/app/composables/period/presetResolvers.ts +48 -19
- package/frontend-vue/layers/dms-core/app/composables/period/usePeriod.ts +33 -4
- package/frontend-vue/layers/dms-core/app/composables/period/usePeriodScope.ts +78 -25
- package/frontend-vue/layers/dms-core/app/composables/realtime/usePageRealtime.ts +3 -0
- package/frontend-vue/layers/dms-core/app/composables/realtime/useUserRealtime.ts +36 -6
- package/frontend-vue/layers/dms-core/app/composables/user/useUserRegionalPreferences.ts +1 -1
- package/frontend-vue/layers/dms-core/app/plugins/async-data-scope.ts +26 -0
- package/frontend-vue/layers/dms-core/app/utils/pageRoutes.ts +22 -0
- package/frontend-vue/layers/dms-core/app/utils/regional.ts +36 -3
- package/frontend-vue/layers/dms-core/app/utils/trailingDeduper.ts +10 -1
- package/frontend-vue/layers/dms-core/shared/types/app-config.ts +48 -4
- package/frontend-vue/layers/dms-layout/app/app.config.ts +42 -23
- package/frontend-vue/layers/dms-layout/app/assets/css/main.css +72 -20
- package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardFrame.vue +9 -0
- package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardSidebar.vue +0 -3
- package/frontend-vue/layers/dms-layout/app/build/components/notification/NotificationPopover.vue +9 -3
- package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/members/MemberRolePicker.vue +13 -12
- package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/notification/NotificationInboxDisplay.vue +17 -7
- package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/notification/NotificationPreferencesForm.vue +0 -1
- package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/profile/ProfilePersonalInfo.vue +36 -33
- package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/profile/ProfileSecuritySummary.vue +1 -2
- package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/roles/role-types.ts +5 -4
- package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/security/SecuritySessions.vue +35 -28
- package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/security/SecurityStatus.vue +0 -1
- package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/security/SecurityTwoFactor.vue +1 -1
- package/frontend-vue/layers/dms-layout/app/build/composables/dev-reload/useDevReloadHolder.ts +7 -6
- package/frontend-vue/layers/dms-layout/app/build/composables/layout/sidePanelState.ts +1 -1
- package/frontend-vue/layers/dms-layout/app/build/composables/navigation/useVisitNavBadges.ts +26 -0
- package/frontend-vue/layers/dms-layout/app/build/composables/notification/useNotificationCatalog.ts +25 -23
- package/frontend-vue/layers/dms-layout/app/build/composables/notification/useNotificationPreferences.ts +44 -23
- package/frontend-vue/layers/dms-layout/app/build/composables/security/useSessionHandoff.ts +1 -1
- package/frontend-vue/layers/dms-layout/app/build/composables/settings/profile/preferencesSummary.ts +1 -1
- package/frontend-vue/layers/dms-layout/app/build/composables/settings/profile/useAccountData.ts +3 -2
- package/frontend-vue/layers/dms-layout/app/build/composables/settings/profile/useProfileAvatar.ts +1 -18
- package/frontend-vue/layers/dms-layout/app/build/composables/settings/profile/useProfilePreferences.ts +19 -23
- package/frontend-vue/layers/dms-layout/app/build/composables/settings/security/useSecurityFormat.ts +6 -3
- package/frontend-vue/layers/dms-layout/app/build/composables/settings/security/useSecurityOverview.ts +13 -18
- package/frontend-vue/layers/dms-layout/app/build/utils/modules-catalog.ts +8 -6
- package/frontend-vue/layers/dms-layout/app/build/utils/publicErrorMessage.ts +19 -0
- package/frontend-vue/layers/dms-layout/app/components/navigation/CollapsedMenuItem.vue +0 -11
- package/frontend-vue/layers/dms-layout/app/components/navigation/NavigationMenu.vue +3 -13
- package/frontend-vue/layers/dms-layout/app/composables/general/useSystemState.ts +7 -11
- package/frontend-vue/layers/dms-layout/app/composables/notification/useNotifications.ts +103 -22
- package/frontend-vue/layers/dms-layout/app/composables/page/useModulesListing.ts +25 -102
- package/frontend-vue/layers/dms-layout/app/composables/page/usePageComponentPreload.ts +0 -10
- package/frontend-vue/layers/dms-layout/app/composables/page/useSiteLayout.ts +177 -96
- package/frontend-vue/layers/dms-layout/app/composables/profile/useUserLanguage.ts +3 -2
- package/frontend-vue/layers/dms-layout/app/custom-pages/modules.vue +6 -28
- package/frontend-vue/layers/dms-layout/app/emails/components/EmailLayout.vue +31 -11
- package/frontend-vue/layers/dms-layout/app/error.vue +5 -2
- package/frontend-vue/layers/dms-layout/app/middleware/module-routing.global.ts +1 -1
- package/frontend-vue/layers/dms-layout/app/middleware/page-leave-guard.global.ts +2 -2
- package/frontend-vue/layers/dms-layout/app/pages/[...slug].vue +12 -9
- package/frontend-vue/layers/dms-layout/app/plugins/dms-menu-sync.client.ts +31 -3
- package/frontend-vue/layers/dms-layout/app/plugins/favicon.ts +58 -0
- package/frontend-vue/layers/dms-layout/app/plugins/notification-stream.client.ts +15 -7
- package/frontend-vue/layers/dms-layout/app/plugins/profile-sync.client.ts +5 -1
- package/frontend-vue/layers/dms-layout/app/plugins/regional-preferences.ts +10 -4
- package/frontend-vue/layers/dms-layout/app/plugins/time-zone-cookie.client.ts +12 -0
- package/frontend-vue/layers/dms-layout/app/types/page.ts +13 -0
- package/frontend-vue/layers/dms-layout/server/html-render.ts +58 -0
- package/frontend-vue/layers/dms-layout/server/render-token.ts +95 -0
- package/frontend-vue/layers/dms-onboarding/app/composables/onboarding/useOnboarding.ts +25 -21
- package/frontend-vue/layers/dms-onboarding/app/middleware/onboarding.global.ts +1 -1
- package/frontend-vue/layers/dms-onboarding/app/plugins/onboarding-preload.ts +9 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/data-types/registerDefaults.ts +4 -1
- package/frontend-vue/layers/dms-ui/app/build/composables/files/useFileMetadata.ts +162 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/form/formRecordLoad.ts +23 -4
- package/frontend-vue/layers/dms-ui/app/build/composables/live/usePageDataRefresh.ts +101 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/navigation/useNavBadges.ts +33 -6
- package/frontend-vue/layers/dms-ui/app/build/composables/period/periodDisplay.ts +19 -16
- package/frontend-vue/layers/dms-ui/app/build/composables/period/periodQuery.ts +40 -20
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableFooter.ts +6 -8
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableListData.ts +42 -14
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableRows.ts +19 -8
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViews.ts +1 -1
- package/frontend-vue/layers/dms-ui/app/components/chart/ChartCard.vue +5 -2
- package/frontend-vue/layers/dms-ui/app/components/flow-canvas/FlowCanvas.vue +9 -9
- package/frontend-vue/layers/dms-ui/app/components/form/Form.vue +20 -18
- package/frontend-vue/layers/dms-ui/app/components/form/components/Cascader.vue +2 -1
- package/frontend-vue/layers/dms-ui/app/components/form/components/File.vue +26 -132
- package/frontend-vue/layers/dms-ui/app/components/form/components/Image.vue +40 -105
- package/frontend-vue/layers/dms-ui/app/components/form/components/InputAddress.vue +2 -0
- package/frontend-vue/layers/dms-ui/app/components/form/components/Relation.vue +4 -4
- package/frontend-vue/layers/dms-ui/app/components/period/PeriodSelector.vue +9 -13
- package/frontend-vue/layers/dms-ui/app/components/table-view/CascaderPath.vue +2 -1
- package/frontend-vue/layers/dms-ui/app/components/table-view/FilePreview.vue +12 -24
- package/frontend-vue/layers/dms-ui/app/components/table-view/ImagePreview.vue +17 -29
- package/frontend-vue/layers/dms-ui/app/components/table-view/KanbanBoard.vue +7 -3
- package/frontend-vue/layers/dms-ui/app/components/table-view/TableView.vue +20 -8
- package/frontend-vue/layers/dms-ui/app/components/top-list/TopListCard.vue +8 -11
- package/frontend-vue/layers/dms-ui/app/composables/chart/useChartFetch.ts +146 -182
- package/frontend-vue/layers/dms-ui/app/composables/live/usePageLiveRefresh.ts +8 -2
- package/frontend-vue/layers/dms-ui/app/composables/useFileReadUrls.ts +11 -99
- package/frontend-vue/layers/dms-ui/app/utils/blockRefresh.ts +32 -3
- package/frontend-vue/package.json +2 -2
- package/frontend-vue/pnpm-lock.yaml +5 -5
- package/frontend-vue/server/private-options.ts +27 -0
- package/frontend-vue/tests/async-data-scope.test.ts +121 -0
- package/frontend-vue/tests/auth-middleware-error.test.ts +3 -3
- package/frontend-vue/tests/banner-block-fetch.test.ts +3 -0
- package/frontend-vue/tests/block-fetch-options-forwarding.test.ts +3 -0
- package/frontend-vue/tests/block-items-period-realtime.test.ts +3 -0
- package/frontend-vue/tests/block-not-found.test.ts +16 -2
- package/frontend-vue/tests/block-page-refresh.test.ts +3 -0
- package/frontend-vue/tests/block-route-params.test.ts +3 -0
- package/frontend-vue/tests/branding-defaults.test.ts +34 -0
- package/frontend-vue/tests/builtin-emails.test.ts +23 -2
- package/frontend-vue/tests/chart-live-pause.test.ts +4 -1
- package/frontend-vue/tests/chart-value-propagation.test.ts +4 -1
- package/frontend-vue/tests/composed-text-blocks.test.ts +3 -0
- package/frontend-vue/tests/email-layout.test.ts +66 -4
- package/frontend-vue/tests/error-page-message.test.ts +25 -2
- package/frontend-vue/tests/favicon.test.ts +131 -0
- package/frontend-vue/tests/file-fields-ssr.test.ts +145 -0
- package/frontend-vue/tests/file-read-urls.test.ts +145 -34
- package/frontend-vue/tests/file-unavailable.test.ts +5 -1
- package/frontend-vue/tests/fixtures/dataEngine.ts +44 -0
- package/frontend-vue/tests/form-load-error.test.ts +55 -1
- package/frontend-vue/tests/helpers/api-data-runtime.ts +77 -0
- package/frontend-vue/tests/image-hydration-ids.test.ts +21 -16
- package/frontend-vue/tests/image-reference-preservation.test.ts +59 -53
- package/frontend-vue/tests/input-number-adjust.test.ts +14 -0
- package/frontend-vue/tests/key-value-list-copy.test.ts +3 -0
- package/frontend-vue/tests/member-role-picker.test.ts +4 -2
- package/frontend-vue/tests/menu-sync-reconnect.test.ts +47 -0
- package/frontend-vue/tests/metadata-retry.test.ts +5 -1
- package/frontend-vue/tests/module-routing.test.ts +2 -5
- package/frontend-vue/tests/modules-catalog.test.ts +17 -4
- package/frontend-vue/tests/modules-listing-transfer.test.ts +95 -0
- package/frontend-vue/tests/multi-account-validate.test.ts +23 -1
- package/frontend-vue/tests/nav-badge-tones.test.ts +66 -3
- package/frontend-vue/tests/notification-counts-from-writes.test.ts +223 -0
- package/frontend-vue/tests/notification-counts-transfer.test.ts +88 -0
- package/frontend-vue/tests/notification-inbox-display.test.ts +57 -5
- package/frontend-vue/tests/notification-popover-errors.test.ts +21 -1
- package/frontend-vue/tests/oauth-complete-handoff.test.ts +209 -0
- package/frontend-vue/tests/onboarding-middleware.test.ts +1 -1
- package/frontend-vue/tests/onboarding-transfer.test.ts +106 -0
- package/frontend-vue/tests/page-data-refresh.test.ts +141 -0
- package/frontend-vue/tests/page-fill-height.test.ts +16 -3
- package/frontend-vue/tests/page-no-visible-blocks.test.ts +12 -2
- package/frontend-vue/tests/page-realtime-remount.test.ts +1 -1
- package/frontend-vue/tests/page-record-label.test.ts +0 -7
- package/frontend-vue/tests/page-validation-redirect.test.ts +12 -2
- package/frontend-vue/tests/period-hydration-now.test.ts +185 -0
- package/frontend-vue/tests/period-live-pause.test.ts +3 -0
- package/frontend-vue/tests/period-reader-zone.test.ts +137 -0
- package/frontend-vue/tests/period-scope-first-fetch.test.ts +17 -1
- package/frontend-vue/tests/period-ssr.test.ts +195 -0
- package/frontend-vue/tests/permission-preview-partial.test.ts +0 -2
- package/frontend-vue/tests/permissions-transfer.test.ts +194 -0
- package/frontend-vue/tests/post-login-redirect.test.ts +10 -5
- package/frontend-vue/tests/profile-personal-info-ssr.test.ts +150 -0
- package/frontend-vue/tests/profile-preferences-summary.test.ts +15 -11
- package/frontend-vue/tests/realtime-reconnect.test.ts +22 -3
- package/frontend-vue/tests/roles-name-errors.test.ts +1 -1
- package/frontend-vue/tests/row-side-panel.test.ts +1 -1
- package/frontend-vue/tests/security-sessions-load-error.test.ts +3 -1
- package/frontend-vue/tests/server-routes-vs-pages.test.ts +26 -0
- package/frontend-vue/tests/server-routes.test.ts +582 -0
- package/frontend-vue/tests/session-handoff.test.ts +1 -1
- package/frontend-vue/tests/settings-reads-ssr.test.ts +232 -0
- package/frontend-vue/tests/side-panels.test.ts +8 -1
- package/frontend-vue/tests/sign-in-exit-history.test.ts +219 -0
- package/frontend-vue/tests/signup-invitation-link.test.ts +56 -8
- package/frontend-vue/tests/site-layout-categories.test.ts +42 -0
- package/frontend-vue/tests/site-layout-hydration.test.ts +3 -3
- package/frontend-vue/tests/site-layout-rendered.test.ts +103 -0
- package/frontend-vue/tests/site-layout-resync.test.ts +87 -0
- package/frontend-vue/tests/ssr-guard-preload.test.ts +188 -0
- package/frontend-vue/tests/table-cell-tones.test.ts +36 -1
- package/frontend-vue/tests/table-data-ssr.test.ts +195 -0
- package/frontend-vue/tests/table-list-data-requests.test.ts +154 -0
- package/frontend-vue/tests/table-retry-loading.test.ts +37 -0
- package/frontend-vue/tests/table-source-navigation.test.ts +0 -1
- package/frontend-vue/tests/table-source-period-scope.test.ts +0 -1
- package/frontend-vue/tests/table-view-views.test.ts +1 -1
- package/frontend-vue/tests/top-list-card-actions.test.ts +3 -0
- package/frontend-vue/tests/top-list-card-period-badge.test.ts +79 -0
- package/frontend-vue/tests/top-list-card-value-suffix.test.ts +3 -0
- package/frontend-vue/tests/trailing-deduper.test.ts +22 -0
- package/frontend-vue/tests/use-api-data-update.test.ts +119 -0
- package/frontend-vue/tests/use-api-data.test.ts +410 -0
- package/frontend-vue/tests/user-language.test.ts +8 -5
- package/frontend-vue/tests/widget-label-i18n.test.ts +4 -1
- package/frontend-vue/vitest.config.ts +6 -0
- package/package.json +2 -2
- package/skills/dms/SKILL.md +2 -1
- package/skills/dms-module/SKILL.md +1 -1
- package/skills/dms-pages/SKILL.md +4 -1
- package/frontend-vue/layers/dms-core/app/build/composables/page/usePageRecordLabel.ts +0 -9
- package/frontend-vue/layers/dms-layout/app/composables/page/usePrefetch.ts +0 -21
- package/frontend-vue/layers/dms-ui/app/build/composables/form/useControlError.ts +0 -10
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useServerRenderedAsyncData.ts +0 -23
- package/frontend-vue/tests/control-error-path.test.ts +0 -11
- package/frontend-vue/tests/modules-listing-per-request.test.ts +0 -104
- package/frontend-vue/tests/page-layout-background-refresh.test.ts +0 -321
|
@@ -1,38 +1,26 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
|
|
3
|
-
resourceKey: string;
|
|
4
|
-
filename: string;
|
|
5
|
-
size: number;
|
|
6
|
-
mimetype: string;
|
|
7
|
-
url: string;
|
|
8
|
-
expiresAt?: number;
|
|
9
|
-
}
|
|
2
|
+
import { useFileMetadata } from "../../build/composables/files/useFileMetadata";
|
|
10
3
|
|
|
11
4
|
const props = defineProps<{
|
|
12
5
|
resourceKey: string;
|
|
13
6
|
storage?: string;
|
|
14
7
|
}>();
|
|
15
8
|
|
|
16
|
-
|
|
17
|
-
const
|
|
18
|
-
|
|
9
|
+
// Read with the page: the server renders the file's name and link.
|
|
10
|
+
const files = useFileMetadata(props.storage);
|
|
11
|
+
watch(
|
|
12
|
+
() => props.resourceKey,
|
|
13
|
+
(key) => void files.resolve(key),
|
|
14
|
+
{ immediate: true },
|
|
15
|
+
);
|
|
16
|
+
const metadata = computed(() => files.metadata(props.resourceKey));
|
|
17
|
+
const isLoading = computed(
|
|
18
|
+
() => !metadata.value && !files.isUnavailable(props.resourceKey),
|
|
19
|
+
);
|
|
19
20
|
|
|
20
21
|
const isImage = computed(
|
|
21
22
|
() => metadata.value?.mimetype.startsWith("image/") ?? false,
|
|
22
23
|
);
|
|
23
|
-
|
|
24
|
-
onMounted(async () => {
|
|
25
|
-
try {
|
|
26
|
-
metadata.value = await $authFetch<FileMetadataResponse>(
|
|
27
|
-
`/api/files/metadata`,
|
|
28
|
-
{ query: { resourceKey: props.resourceKey, storage: props.storage } },
|
|
29
|
-
);
|
|
30
|
-
} catch {
|
|
31
|
-
metadata.value = null;
|
|
32
|
-
} finally {
|
|
33
|
-
isLoading.value = false;
|
|
34
|
-
}
|
|
35
|
-
});
|
|
36
24
|
</script>
|
|
37
25
|
|
|
38
26
|
<template>
|
|
@@ -1,12 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
|
|
3
|
-
resourceKey: string;
|
|
4
|
-
filename: string;
|
|
5
|
-
size: number;
|
|
6
|
-
mimetype: string;
|
|
7
|
-
url: string;
|
|
8
|
-
expiresAt?: number;
|
|
9
|
-
}
|
|
2
|
+
import { useFileMetadata } from "../../build/composables/files/useFileMetadata";
|
|
10
3
|
|
|
11
4
|
const props = defineProps<{
|
|
12
5
|
images: ImageItemValue[];
|
|
@@ -15,9 +8,7 @@ const props = defineProps<{
|
|
|
15
8
|
|
|
16
9
|
const MAX_THUMBNAILS = 3;
|
|
17
10
|
|
|
18
|
-
const
|
|
19
|
-
const urls = ref<Map<string, string>>(new Map());
|
|
20
|
-
const isLoading = ref(true);
|
|
11
|
+
const files = useFileMetadata(props.storage);
|
|
21
12
|
|
|
22
13
|
const sortedImages = computed(() =>
|
|
23
14
|
[...props.images].sort(
|
|
@@ -33,22 +24,19 @@ const overflowCount = computed(
|
|
|
33
24
|
() => props.images.length - visibleImages.value.length,
|
|
34
25
|
);
|
|
35
26
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
);
|
|
50
|
-
isLoading.value = false;
|
|
51
|
-
});
|
|
27
|
+
// Read with the page: the server renders the thumbnails.
|
|
28
|
+
watch(
|
|
29
|
+
visibleImages,
|
|
30
|
+
(images) => images.forEach((image) => void files.resolve(image.key)),
|
|
31
|
+
{ immediate: true },
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
const urlOf = (key: string): string | undefined => files.metadata(key)?.url;
|
|
35
|
+
const isLoading = computed(() =>
|
|
36
|
+
visibleImages.value.some(
|
|
37
|
+
(image) => !urlOf(image.key) && !files.isUnavailable(image.key),
|
|
38
|
+
),
|
|
39
|
+
);
|
|
52
40
|
</script>
|
|
53
41
|
|
|
54
42
|
<template>
|
|
@@ -61,8 +49,8 @@ onMounted(async () => {
|
|
|
61
49
|
class="ring-bg bg-elevated inline-block size-7 overflow-hidden rounded-md ring-2"
|
|
62
50
|
>
|
|
63
51
|
<img
|
|
64
|
-
v-if="
|
|
65
|
-
:src="
|
|
52
|
+
v-if="urlOf(image.key)"
|
|
53
|
+
:src="urlOf(image.key)"
|
|
66
54
|
:alt="image.alt ?? ''"
|
|
67
55
|
class="h-full w-full object-cover"
|
|
68
56
|
/>
|
|
@@ -11,7 +11,6 @@ import {
|
|
|
11
11
|
buildKanbanColumns,
|
|
12
12
|
type KanbanColumnDef,
|
|
13
13
|
} from "../../composables/table-view/kanban";
|
|
14
|
-
import { useServerRenderedAsyncData } from "../../build/composables/table-view/useServerRenderedAsyncData";
|
|
15
14
|
import type { TableViewDisplayContext } from "../../composables/table-view/types/display";
|
|
16
15
|
import {
|
|
17
16
|
buildCardProps,
|
|
@@ -128,7 +127,7 @@ const boardColumnsKey = computed(() =>
|
|
|
128
127
|
// `refresh` is assigned below and only called once setup has completed.
|
|
129
128
|
defineExpose({ refresh: () => refresh() });
|
|
130
129
|
|
|
131
|
-
const { data, status, refresh } = await
|
|
130
|
+
const { data, status, refresh } = await useDmsAsyncData(
|
|
132
131
|
`kanban-${props.componentId}-${props.pageId}-${instanceId}`,
|
|
133
132
|
async () => {
|
|
134
133
|
const pages = await Promise.all(
|
|
@@ -138,7 +137,12 @@ const { data, status, refresh } = await useServerRenderedAsyncData(
|
|
|
138
137
|
boardColumns.value.map((col, index) => [col.value, pages[index]]),
|
|
139
138
|
);
|
|
140
139
|
},
|
|
141
|
-
{
|
|
140
|
+
{
|
|
141
|
+
watch: [boardColumnsKey, () => props.baseQuery, pageSize],
|
|
142
|
+
lazy: true,
|
|
143
|
+
// Its table view refreshes it, on the table's own triggers.
|
|
144
|
+
refreshOnPageRefresh: false,
|
|
145
|
+
},
|
|
142
146
|
);
|
|
143
147
|
|
|
144
148
|
// Nothing fetched yet (a client navigation paints first): the columns hold
|
|
@@ -865,7 +865,9 @@ const liveHold = useTableLiveHold({
|
|
|
865
865
|
},
|
|
866
866
|
});
|
|
867
867
|
|
|
868
|
-
|
|
868
|
+
// The list, its counters and its footer figures load in parallel: a server
|
|
869
|
+
// render waits for each before rendering, whether or not the setup awaits it.
|
|
870
|
+
const { data, status, error, refresh } = useTableListData<T>({
|
|
869
871
|
componentId,
|
|
870
872
|
pageId,
|
|
871
873
|
rows: tableRows,
|
|
@@ -875,9 +877,18 @@ const { data, status, error, refresh } = await useTableListData<T>({
|
|
|
875
877
|
heldQuery: liveHold.heldQuery,
|
|
876
878
|
});
|
|
877
879
|
|
|
880
|
+
// The failure of the last answer. The async data keeps it while the next run
|
|
881
|
+
// is in flight; the table shows that run (its skeleton, or the rows it has)
|
|
882
|
+
// rather than the failure it may replace.
|
|
883
|
+
const settledError = computed(() =>
|
|
884
|
+
status.value === "pending" ? null : error.value,
|
|
885
|
+
);
|
|
886
|
+
|
|
878
887
|
// Nothing listed yet (a client navigation paints before the first page
|
|
879
888
|
// arrives): the table draws its skeleton, never the empty state.
|
|
880
|
-
const isFirstPageLoading = computed(
|
|
889
|
+
const isFirstPageLoading = computed(
|
|
890
|
+
() => data.value === null && !settledError.value,
|
|
891
|
+
);
|
|
881
892
|
const isListLoading = computed(
|
|
882
893
|
() => status.value === "pending" || isFirstPageLoading.value,
|
|
883
894
|
);
|
|
@@ -890,10 +901,11 @@ const isListLoading = computed(
|
|
|
890
901
|
// or HTML, which must never reach the screen); the HTTP status is the fallback
|
|
891
902
|
// that keeps a refusal distinguishable from a server failure.
|
|
892
903
|
const listLoadError = computed(() => {
|
|
893
|
-
|
|
894
|
-
|
|
904
|
+
const failure = settledError.value;
|
|
905
|
+
if (!failure) return undefined;
|
|
906
|
+
const message = resolveApiErrorMessage(failure);
|
|
895
907
|
if (te(`${message}.description`) || te(message)) return message;
|
|
896
|
-
const status = (
|
|
908
|
+
const status = (failure as { statusCode?: number }).statusCode;
|
|
897
909
|
return status && te(`error.${status}.description`)
|
|
898
910
|
? `error.${status}`
|
|
899
911
|
: "error.500";
|
|
@@ -1017,7 +1029,7 @@ const tabCountsQuery = computed(() => [
|
|
|
1017
1029
|
// No default: `null` until the counts arrive, so the tabs draw placeholders
|
|
1018
1030
|
// instead of a count of nothing.
|
|
1019
1031
|
const { data: tabCountsData, refresh: refreshTabCounts } =
|
|
1020
|
-
|
|
1032
|
+
useTableCountsData<T>({
|
|
1021
1033
|
componentId,
|
|
1022
1034
|
pageId,
|
|
1023
1035
|
rows: tableRows,
|
|
@@ -1069,7 +1081,7 @@ const baseQuery = computed<Record<string, unknown>>(() => {
|
|
|
1069
1081
|
return { ...query, ...archiveQuery.value };
|
|
1070
1082
|
});
|
|
1071
1083
|
|
|
1072
|
-
const { footer: resolvedFooter, refreshSummaries } =
|
|
1084
|
+
const { footer: resolvedFooter, refreshSummaries } = useTableFooter({
|
|
1073
1085
|
footer: props.footer,
|
|
1074
1086
|
columns: props.columns,
|
|
1075
1087
|
location,
|
|
@@ -1431,7 +1443,7 @@ const realtimePresenceTopic = computed(() =>
|
|
|
1431
1443
|
);
|
|
1432
1444
|
|
|
1433
1445
|
const presenceByRow = ref<RealtimePresenceMap>({});
|
|
1434
|
-
const { user: realtimeUser } =
|
|
1446
|
+
const { user: realtimeUser } = useDmsUserSession();
|
|
1435
1447
|
|
|
1436
1448
|
const getRowId = (row: T): string | undefined => {
|
|
1437
1449
|
const value = (row as Record<string, unknown>)[rowIdKey];
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { computed, nextTick, onMounted, ref, watch } from "vue";
|
|
3
|
-
import { useMounted } from "@vueuse/core";
|
|
4
3
|
import DmsEmptyState from "../empty-state/EmptyState.vue";
|
|
5
4
|
import DmsSectionHeader from "../section-header/SectionHeader.vue";
|
|
6
5
|
import DmsBlockActions, {
|
|
@@ -13,7 +12,7 @@ import DmsTopListRow from "./internal/TopListRow.vue";
|
|
|
13
12
|
import { useChartFetch } from "../../composables/chart/useChartFetch";
|
|
14
13
|
import { formatValue } from "../../composables/chart/formatValue";
|
|
15
14
|
import { resolveSparklineAccent } from "../../composables/chart/resolveSparklineAccent";
|
|
16
|
-
import {
|
|
15
|
+
import { usePeriodScopePreset } from "#dms-core/app/composables/period/usePeriodScope";
|
|
17
16
|
import {
|
|
18
17
|
hasUrlVariables,
|
|
19
18
|
resolveLinkVariables,
|
|
@@ -217,16 +216,14 @@ function sparklineAccentFor(item: TopListItem): string {
|
|
|
217
216
|
});
|
|
218
217
|
}
|
|
219
218
|
|
|
220
|
-
|
|
221
|
-
//
|
|
222
|
-
//
|
|
223
|
-
|
|
224
|
-
const isMounted = useMounted();
|
|
219
|
+
// The preset is a name, the same on both sides: the server prints the badge
|
|
220
|
+
// of a selector set up before the card, and the browser hydrates it. A
|
|
221
|
+
// selector set up after it draws a skeleton in its place until it registers.
|
|
222
|
+
const scopePreset = usePeriodScopePreset(props.periodScope);
|
|
225
223
|
const badgeLabel = computed<string | null>(() => {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
return t(`${PRESET_LABEL_KEY_PREFIX}.${state.preset}`, state.preset);
|
|
224
|
+
const preset = scopePreset.value;
|
|
225
|
+
if (!props.periodScope || !preset) return null;
|
|
226
|
+
return t(`${PRESET_LABEL_KEY_PREFIX}.${preset}`, preset);
|
|
230
227
|
});
|
|
231
228
|
|
|
232
229
|
const isFirstLoad = computed(() => isLoading.value && data.value === null);
|
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
computed,
|
|
3
3
|
onMounted,
|
|
4
4
|
ref,
|
|
5
|
+
shallowRef,
|
|
5
6
|
watch,
|
|
6
7
|
type ComputedRef,
|
|
7
8
|
type Ref,
|
|
@@ -10,13 +11,13 @@ import {
|
|
|
10
11
|
appendPeriodToUrl,
|
|
11
12
|
usePeriodScope,
|
|
12
13
|
} from "../../../../dms-core/app/composables/period/usePeriodScope";
|
|
13
|
-
import type { PeriodState } from "../../../../dms-core/app/composables/period/types";
|
|
14
14
|
import {
|
|
15
15
|
hasUrlVariables,
|
|
16
16
|
resolveUrlVariables,
|
|
17
17
|
} from "../../build/utils/urlVariables";
|
|
18
18
|
import { useRefreshTriggers } from "../../build/composables/blocks/useRefreshTriggers";
|
|
19
19
|
import { readFetchNotFound } from "../../build/utils/fetchNotFound";
|
|
20
|
+
import type { UseApiDataOptions } from "../../../../dms-core/app/composables/auth/useApiData";
|
|
20
21
|
|
|
21
22
|
export interface UseChartFetchOptions<T> {
|
|
22
23
|
/**
|
|
@@ -51,7 +52,6 @@ export interface UseChartFetchReturn<T> {
|
|
|
51
52
|
}
|
|
52
53
|
|
|
53
54
|
const DEBOUNCE_FRAME_MS = 180;
|
|
54
|
-
const DEFAULT_HTTP_METHOD = "GET";
|
|
55
55
|
// How long a card waits for its period scope to register before it stops
|
|
56
56
|
// claiming to load. The selector registers the scope from its own lazily
|
|
57
57
|
// loaded chunk, which on a slow network lands seconds after the card mounts:
|
|
@@ -59,78 +59,15 @@ const DEFAULT_HTTP_METHOD = "GET";
|
|
|
59
59
|
// scope is merely late. Only a scope no selector ever registers reaches it.
|
|
60
60
|
export const PENDING_SCOPE_TIMEOUT_MS = 10_000;
|
|
61
61
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
if (!url) return url;
|
|
68
|
-
if (!scope) return url;
|
|
69
|
-
return appendPeriodToUrl(url, scopeState);
|
|
62
|
+
/** What a card asks for: its URL with its period, and the state it watches. */
|
|
63
|
+
interface ChartRequest {
|
|
64
|
+
/** `null` while a token or the period scope has no value yet. */
|
|
65
|
+
url: string | null;
|
|
66
|
+
watched: string;
|
|
70
67
|
}
|
|
71
68
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
url: string | undefined;
|
|
75
|
-
method: string | undefined;
|
|
76
|
-
periodScope: string | undefined;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
interface FetchRunner {
|
|
80
|
-
hasFetchedOnce: boolean;
|
|
81
|
-
/** Inputs the last fetch was started for, to collapse duplicate triggers. */
|
|
82
|
-
lastInputs: string | null;
|
|
83
|
-
pendingTimer: ReturnType<typeof setTimeout> | null;
|
|
84
|
-
activeAbort: AbortController | null;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
type AuthFetcher = <T>(
|
|
88
|
-
url: string,
|
|
89
|
-
opts: { method: string; signal: AbortSignal },
|
|
90
|
-
) => Promise<T>;
|
|
91
|
-
|
|
92
|
-
async function runFetch<T>(
|
|
93
|
-
authFetch: AuthFetcher,
|
|
94
|
-
request: FetchRequest,
|
|
95
|
-
scopeState: PeriodState | null,
|
|
96
|
-
runner: FetchRunner,
|
|
97
|
-
data: Ref<T | null>,
|
|
98
|
-
isLoading: Ref<boolean>,
|
|
99
|
-
error: Ref<unknown>,
|
|
100
|
-
): Promise<void> {
|
|
101
|
-
if (!request.url) return;
|
|
102
|
-
if (request.periodScope && !scopeState) return;
|
|
103
|
-
const url = buildFinalUrl(request.url, scopeState, request.periodScope);
|
|
104
|
-
if (!url) return;
|
|
105
|
-
if (runner.activeAbort) runner.activeAbort.abort();
|
|
106
|
-
const controller = new AbortController();
|
|
107
|
-
runner.activeAbort = controller;
|
|
108
|
-
isLoading.value = true;
|
|
109
|
-
error.value = null;
|
|
110
|
-
try {
|
|
111
|
-
const response = await authFetch<T>(url, {
|
|
112
|
-
method: request.method || DEFAULT_HTTP_METHOD,
|
|
113
|
-
signal: controller.signal,
|
|
114
|
-
});
|
|
115
|
-
if (runner.activeAbort !== controller) return;
|
|
116
|
-
data.value = response;
|
|
117
|
-
} catch (err) {
|
|
118
|
-
if (runner.activeAbort !== controller) return;
|
|
119
|
-
if ((err as { name?: string })?.name === "AbortError") return;
|
|
120
|
-
error.value = err;
|
|
121
|
-
// A 404 says what the block showed is gone (another tab deleted it, the
|
|
122
|
-
// URL now names another record): its last values must not stay on screen
|
|
123
|
-
// as if they still belonged to it. Any other failure keeps them.
|
|
124
|
-
if (readFetchNotFound(err)) data.value = null;
|
|
125
|
-
} finally {
|
|
126
|
-
// A request a newer one superseded leaves the loading flag to that one:
|
|
127
|
-
// clearing it here would show the empty state while the newer answer is
|
|
128
|
-
// still on its way.
|
|
129
|
-
if (runner.activeAbort === controller) {
|
|
130
|
-
runner.activeAbort = null;
|
|
131
|
-
isLoading.value = false;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
69
|
+
function isSameRequest(left: ChartRequest, right: ChartRequest): boolean {
|
|
70
|
+
return left.url === right.url && left.watched === right.watched;
|
|
134
71
|
}
|
|
135
72
|
|
|
136
73
|
function readStaticData<T>(
|
|
@@ -158,131 +95,158 @@ function useResolvedFetchUrl<T>(
|
|
|
158
95
|
);
|
|
159
96
|
}
|
|
160
97
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
// period-scoped card renders server-side with no scope and no data. Start
|
|
170
|
-
// in the loading state so consumers show a skeleton instead of a hard zero
|
|
171
|
-
// for the whole hydrate-then-fetch window.
|
|
172
|
-
const resolvedUrl = useResolvedFetchUrl(options);
|
|
173
|
-
const isLoading = ref(Boolean(resolvedUrl.value));
|
|
174
|
-
const error = ref<unknown>(null);
|
|
175
|
-
const scopeState = usePeriodScope(options.periodScope);
|
|
176
|
-
const { $authFetch } = useAuthFetch();
|
|
177
|
-
const runner: FetchRunner = {
|
|
178
|
-
hasFetchedOnce: false,
|
|
179
|
-
lastInputs: null,
|
|
180
|
-
pendingTimer: null,
|
|
181
|
-
activeAbort: null,
|
|
182
|
-
};
|
|
183
|
-
|
|
184
|
-
const performFetch = () =>
|
|
185
|
-
runFetch(
|
|
186
|
-
$authFetch as AuthFetcher,
|
|
187
|
-
{
|
|
188
|
-
url: resolvedUrl.value,
|
|
189
|
-
method: options.fetchUrlMethod,
|
|
190
|
-
periodScope: options.periodScope,
|
|
191
|
-
},
|
|
192
|
-
scopeState.value,
|
|
193
|
-
runner,
|
|
194
|
-
data,
|
|
195
|
-
isLoading,
|
|
196
|
-
error,
|
|
197
|
-
);
|
|
198
|
-
|
|
199
|
-
const refresh = async () => {
|
|
200
|
-
if (runner.pendingTimer) clearTimeout(runner.pendingTimer);
|
|
201
|
-
runner.pendingTimer = null;
|
|
202
|
-
await performFetch();
|
|
203
|
-
};
|
|
204
|
-
|
|
205
|
-
const currentInputs = () =>
|
|
206
|
-
`${resolvedUrl.value}|${scopeState.value?.key ?? ""}|${JSON.stringify(options.watchSource?.() ?? null)}`;
|
|
207
|
-
|
|
208
|
-
const fetchScheduledInputs = () => {
|
|
209
|
-
runner.pendingTimer = null;
|
|
210
|
-
runner.lastInputs = currentInputs();
|
|
211
|
-
void performFetch();
|
|
212
|
-
};
|
|
98
|
+
interface ScheduledRequest {
|
|
99
|
+
/** The request the card is on. */
|
|
100
|
+
current: Ref<ChartRequest>;
|
|
101
|
+
/** Inputs changed, and their request waits for them to hold still. */
|
|
102
|
+
isScheduled: Ref<boolean>;
|
|
103
|
+
/** Moves to the latest inputs at once; whether the request changed. */
|
|
104
|
+
flush(): boolean;
|
|
105
|
+
}
|
|
213
106
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
107
|
+
/**
|
|
108
|
+
* The request the card is on: the first one at once, each later one once the
|
|
109
|
+
* inputs have held still for a frame, so a period changed and changed back
|
|
110
|
+
* requests nothing. A server render keeps the one its setup saw.
|
|
111
|
+
*/
|
|
112
|
+
function useScheduledRequest(
|
|
113
|
+
target: ComputedRef<ChartRequest>,
|
|
114
|
+
): ScheduledRequest {
|
|
115
|
+
const current = shallowRef(target.value);
|
|
116
|
+
const isScheduled = ref(false);
|
|
117
|
+
if (typeof window === "undefined")
|
|
118
|
+
return { current, isScheduled, flush: () => false };
|
|
119
|
+
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
120
|
+
const flush = () => {
|
|
121
|
+
if (timer) clearTimeout(timer);
|
|
122
|
+
timer = null;
|
|
123
|
+
isScheduled.value = false;
|
|
124
|
+
if (isSameRequest(current.value, target.value)) return false;
|
|
125
|
+
current.value = target.value;
|
|
126
|
+
return true;
|
|
218
127
|
};
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
// inputs the last fetch was started for collapses that into one request.
|
|
225
|
-
const scheduleRefreshWith = (announcePending: boolean) => {
|
|
226
|
-
const hadPending = runner.pendingTimer !== null;
|
|
227
|
-
if (runner.pendingTimer) clearTimeout(runner.pendingTimer);
|
|
228
|
-
runner.pendingTimer = null;
|
|
229
|
-
if (!resolvedUrl.value || (options.periodScope && !scopeState.value)) {
|
|
230
|
-
settleCancelledRefresh(hadPending);
|
|
231
|
-
return;
|
|
232
|
-
}
|
|
233
|
-
const inputs = currentInputs();
|
|
234
|
-
if (inputs === runner.lastInputs) {
|
|
235
|
-
settleCancelledRefresh(hadPending);
|
|
128
|
+
watch(target, (next) => {
|
|
129
|
+
if (timer) clearTimeout(timer);
|
|
130
|
+
timer = null;
|
|
131
|
+
if (isSameRequest(next, current.value)) {
|
|
132
|
+
isScheduled.value = false;
|
|
236
133
|
return;
|
|
237
134
|
}
|
|
238
|
-
if (
|
|
239
|
-
|
|
240
|
-
fetchScheduledInputs();
|
|
135
|
+
if (current.value.url === null) {
|
|
136
|
+
flush();
|
|
241
137
|
return;
|
|
242
138
|
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
const scheduleRefresh = () => scheduleRefreshWith(true);
|
|
249
|
-
|
|
250
|
-
// A realtime event or a page refresh (refreshPageBlocks) means the data
|
|
251
|
-
// behind unchanged inputs moved, so it has to bypass the deduplication
|
|
252
|
-
// above. The inputs did not change, so the values on screen stay current
|
|
253
|
-
// until the new answer replaces them.
|
|
254
|
-
const refreshSameInputs = () => {
|
|
255
|
-
runner.lastInputs = null;
|
|
256
|
-
scheduleRefreshWith(false);
|
|
257
|
-
};
|
|
139
|
+
isScheduled.value = true;
|
|
140
|
+
timer = setTimeout(flush, DEBOUNCE_FRAME_MS);
|
|
141
|
+
});
|
|
142
|
+
return { current, isScheduled, flush };
|
|
143
|
+
}
|
|
258
144
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
145
|
+
/**
|
|
146
|
+
* Whether a card bound to a period scope is still waiting for it to register:
|
|
147
|
+
* until a selector does, or `PENDING_SCOPE_TIMEOUT_MS` after mounting.
|
|
148
|
+
*/
|
|
149
|
+
function useScopeWait(isWaiting: () => boolean): ComputedRef<boolean> {
|
|
150
|
+
const gaveUp = ref(false);
|
|
151
|
+
onMounted(() => {
|
|
152
|
+
if (!isWaiting()) return;
|
|
266
153
|
setTimeout(() => {
|
|
267
|
-
|
|
154
|
+
gaveUp.value = true;
|
|
268
155
|
}, PENDING_SCOPE_TIMEOUT_MS);
|
|
156
|
+
});
|
|
157
|
+
return computed(() => isWaiting() && !gaveUp.value);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* The data of a block that reads `fetchUrl`, through `useApiData`: a server
|
|
162
|
+
* render requests it and hands it to the browser, unless the URL waits for a
|
|
163
|
+
* period the server cannot resolve as the browser would (see
|
|
164
|
+
* `usePeriodScope`); the browser then requests it once mounted. The period of
|
|
165
|
+
* `periodScope` is sent with the request, and the block reads again when the
|
|
166
|
+
* period, the watched state or the URL's tokens change (once they hold still
|
|
167
|
+
* for a frame) and on each event of `realtimeTopic`.
|
|
168
|
+
*/
|
|
169
|
+
export function useChartFetch<T>(
|
|
170
|
+
options: UseChartFetchOptions<T>,
|
|
171
|
+
): UseChartFetchReturn<T> {
|
|
172
|
+
if (!options.fetchUrl) return useStaticChartData(options);
|
|
173
|
+
const resolvedUrl = useResolvedFetchUrl(options);
|
|
174
|
+
const scopeState = usePeriodScope(options.periodScope);
|
|
175
|
+
const target = computed<ChartRequest>(() => {
|
|
176
|
+
const url = resolvedUrl.value;
|
|
177
|
+
const waitsForScope = !!options.periodScope && !scopeState.value;
|
|
178
|
+
return {
|
|
179
|
+
url:
|
|
180
|
+
!url || waitsForScope
|
|
181
|
+
? null
|
|
182
|
+
: (appendPeriodToUrl(url, scopeState.value) ?? null),
|
|
183
|
+
watched: JSON.stringify(options.watchSource?.() ?? null),
|
|
184
|
+
};
|
|
185
|
+
});
|
|
186
|
+
const request = useScheduledRequest(target);
|
|
187
|
+
const loaded = useApiData<T>(() => request.current.value.url, {
|
|
188
|
+
method: options.fetchUrlMethod as UseApiDataOptions<T>["method"],
|
|
189
|
+
// A block never holds a navigation: it draws its skeleton meanwhile.
|
|
190
|
+
lazy: true,
|
|
191
|
+
default: () => readStaticData(options.staticData) as T,
|
|
192
|
+
watch: [() => request.current.value.watched],
|
|
193
|
+
// Read again on its own triggers (realtime topic, page refresh, live
|
|
194
|
+
// gate): left out of the page's data refresh, which would read it twice.
|
|
195
|
+
refreshOnPageRefresh: false,
|
|
196
|
+
});
|
|
197
|
+
const { data, status, error } = loaded;
|
|
198
|
+
// A 404 says what the block showed is gone (another tab deleted it, the
|
|
199
|
+
// URL now names another record): its last values must not stay on screen
|
|
200
|
+
// as if they still belonged to it. Any other failure keeps them.
|
|
201
|
+
watch(error, (failure) => {
|
|
202
|
+
if (failure && readFetchNotFound(failure)) data.value = null;
|
|
203
|
+
});
|
|
204
|
+
const isWaitingForScope = useScopeWait(
|
|
205
|
+
() => !!resolvedUrl.value && !!options.periodScope && !scopeState.value,
|
|
206
|
+
);
|
|
207
|
+
const isLoading = computed(
|
|
208
|
+
() =>
|
|
209
|
+
request.isScheduled.value ||
|
|
210
|
+
status.value === "pending" ||
|
|
211
|
+
(status.value === "idle" && request.current.value.url !== null) ||
|
|
212
|
+
isWaitingForScope.value,
|
|
213
|
+
);
|
|
214
|
+
const refresh = async () => {
|
|
215
|
+
// A request that changed loads as it moves: joined, not started twice.
|
|
216
|
+
await loaded.refresh(request.flush() ? { dedupe: "defer" } : undefined);
|
|
269
217
|
};
|
|
218
|
+
// A realtime event or a page refresh (refreshPageBlocks) means the data
|
|
219
|
+
// behind unchanged inputs moved: read again once a burst holds still. The
|
|
220
|
+
// values on screen stay current until the new answer replaces them.
|
|
221
|
+
let refreshTimer: ReturnType<typeof setTimeout> | null = null;
|
|
222
|
+
useRefreshTriggers(options.realtimeTopic, () => {
|
|
223
|
+
if (refreshTimer) clearTimeout(refreshTimer);
|
|
224
|
+
refreshTimer = setTimeout(() => {
|
|
225
|
+
refreshTimer = null;
|
|
226
|
+
void loaded.refresh();
|
|
227
|
+
}, DEBOUNCE_FRAME_MS);
|
|
228
|
+
});
|
|
229
|
+
return { data: data as Ref<T | null>, isLoading, error, refresh };
|
|
230
|
+
}
|
|
270
231
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
232
|
+
/** A block without `fetchUrl`: its `staticData`, followed as it changes. */
|
|
233
|
+
function useStaticChartData<T>(
|
|
234
|
+
options: UseChartFetchOptions<T>,
|
|
235
|
+
): UseChartFetchReturn<T> {
|
|
236
|
+
const data = ref<T | null>(
|
|
237
|
+
readStaticData(options.staticData),
|
|
238
|
+
) as Ref<T | null>;
|
|
239
|
+
if (typeof options.staticData === "function")
|
|
279
240
|
watch(
|
|
280
241
|
() => readStaticData(options.staticData),
|
|
281
242
|
(next) => {
|
|
282
243
|
data.value = next;
|
|
283
244
|
},
|
|
284
245
|
);
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
246
|
+
return {
|
|
247
|
+
data,
|
|
248
|
+
isLoading: ref(false),
|
|
249
|
+
error: ref<unknown>(null),
|
|
250
|
+
refresh: async () => {},
|
|
251
|
+
};
|
|
288
252
|
}
|
|
@@ -49,9 +49,15 @@ export interface PageLiveRefresh {
|
|
|
49
49
|
* of the page. Call it from a component's setup; the subscriptions end with
|
|
50
50
|
* the component.
|
|
51
51
|
*
|
|
52
|
+
* A `useApiData` read already follows the page's refreshes and the events of
|
|
53
|
+
* the page's own topics: this is for a topic of the component's own, or an
|
|
54
|
+
* idle refresh. Its read then opts out of the page's data refresh, which
|
|
55
|
+
* would read it a second time.
|
|
56
|
+
*
|
|
52
57
|
* ```ts
|
|
53
|
-
* const {
|
|
54
|
-
*
|
|
58
|
+
* const { data, refresh } = useApiData<Health>("/api/health", {
|
|
59
|
+
* refreshOnPageRefresh: false,
|
|
60
|
+
* });
|
|
55
61
|
* usePageLiveRefresh(() => void refresh(), {
|
|
56
62
|
* realtimeTopic: "api:requests",
|
|
57
63
|
* idleRefresh: 60,
|