@antelopejs/dms 0.8.3 → 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 +20 -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/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/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,14 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Global reactive access to the signed-in user's identity (name, email, ...).
|
|
3
3
|
*
|
|
4
|
-
* `user` is the shared session state from `
|
|
4
|
+
* `user` is the shared session state from `useDmsUserSession`, so every consumer
|
|
5
5
|
* (header menu, switch-account page, ...) re-renders as soon as it changes.
|
|
6
6
|
* `refresh` re-fetches the session through the frontend server session hook
|
|
7
7
|
* pulls a fresh `/api/auth/me`) and syncs the stored multi-account entry, so a
|
|
8
8
|
* profile edit propagates immediately without re-logging in.
|
|
9
9
|
*/
|
|
10
10
|
export function useCurrentUser() {
|
|
11
|
-
const { user, fetch: refreshSession } =
|
|
11
|
+
const { user, fetch: refreshSession } = useDmsUserSession<User>();
|
|
12
12
|
const { syncActiveAccount } = useMultiAccount();
|
|
13
13
|
|
|
14
14
|
async function refresh(): Promise<void> {
|
|
@@ -116,13 +116,23 @@ async function requestAccountSwitch(account: StoredAccount) {
|
|
|
116
116
|
account.isExpired = false;
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
-
|
|
119
|
+
/**
|
|
120
|
+
* Moves what the browser shows to the session it switched to. The session
|
|
121
|
+
* read again moves the data scope (the previous account's data is dropped)
|
|
122
|
+
* and drops the prefetched pages; the site layout and the page come back
|
|
123
|
+
* through Inertia, whose once prop is keyed on the account and workspace.
|
|
124
|
+
*
|
|
125
|
+
* @internal
|
|
126
|
+
*/
|
|
127
|
+
export async function refreshSessionSurfaces(
|
|
128
|
+
refreshUser: () => Promise<unknown>,
|
|
129
|
+
): Promise<void> {
|
|
120
130
|
await refreshUser();
|
|
121
131
|
|
|
122
|
-
const {
|
|
132
|
+
const { resync: reloadSiteLayout } = useSiteLayout();
|
|
123
133
|
const { clear: clearPermissions } = usePermissions();
|
|
124
134
|
clearPermissions();
|
|
125
|
-
await
|
|
135
|
+
await reloadSiteLayout();
|
|
126
136
|
}
|
|
127
137
|
|
|
128
138
|
export async function validateAccount(account: StoredAccount) {
|
|
@@ -187,8 +197,11 @@ function createValidateAllAccounts(
|
|
|
187
197
|
|
|
188
198
|
export function useMultiAccount() {
|
|
189
199
|
const accounts = useAccountsStorage();
|
|
190
|
-
const {
|
|
191
|
-
|
|
200
|
+
const {
|
|
201
|
+
user,
|
|
202
|
+
session,
|
|
203
|
+
fetch: refreshUser,
|
|
204
|
+
} = useDmsUserSession<User, BrowserSession>();
|
|
192
205
|
|
|
193
206
|
const getActiveAccount = (): StoredAccount | undefined =>
|
|
194
207
|
buildActiveAccount(
|
|
@@ -9,11 +9,29 @@ interface PermissionsState {
|
|
|
9
9
|
fetchGeneration: Ref<number>;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
// The set a server render loads (the `auth` middleware preloads it before the
|
|
13
|
+
// page renders) is the one the page renders with: the browser starts from it
|
|
14
|
+
// rather than asking again, and its age keeps counting from the server's load.
|
|
15
|
+
// The generation only guards the runs in flight of one side.
|
|
16
|
+
const TRANSFERRED = { transfer: true };
|
|
17
|
+
|
|
12
18
|
function usePermissionsState(): PermissionsState {
|
|
13
19
|
return {
|
|
14
|
-
permissions: useDmsState<Set<string>>(
|
|
15
|
-
|
|
16
|
-
|
|
20
|
+
permissions: useDmsState<Set<string>>(
|
|
21
|
+
"dms-user-permissions",
|
|
22
|
+
() => new Set(),
|
|
23
|
+
TRANSFERRED,
|
|
24
|
+
),
|
|
25
|
+
isLoaded: useDmsState<boolean>(
|
|
26
|
+
"dms-permissions-loaded",
|
|
27
|
+
() => false,
|
|
28
|
+
TRANSFERRED,
|
|
29
|
+
),
|
|
30
|
+
lastFetchTime: useDmsState<number>(
|
|
31
|
+
"dms-permissions-lastFetch",
|
|
32
|
+
() => 0,
|
|
33
|
+
TRANSFERRED,
|
|
34
|
+
),
|
|
17
35
|
fetchGeneration: useDmsState<number>("dms-permissions-gen", () => 0),
|
|
18
36
|
};
|
|
19
37
|
}
|
|
@@ -30,15 +48,25 @@ function createFetchPermissions(state: PermissionsState) {
|
|
|
30
48
|
};
|
|
31
49
|
|
|
32
50
|
// A refetch asked while one is in flight gets its own pass instead of the
|
|
33
|
-
// older response, so an invalidation is never swallowed. Scoped to the DMS
|
|
51
|
+
// older response, so an invalidation is never swallowed. Scoped to the DMS
|
|
34
52
|
// app, so concurrent server-rendered requests never share a pass.
|
|
35
53
|
const deduper = useTrailingDeduper("permissions");
|
|
36
|
-
|
|
54
|
+
const fetchPermissions = (): Promise<void> => deduper.run(runFetch);
|
|
55
|
+
// A caller that only needs a set has nothing to invalidate: it joins the
|
|
56
|
+
// load in flight instead of queueing a second one.
|
|
57
|
+
const loadPermissions = async (): Promise<void> => {
|
|
58
|
+
if (state.isLoaded.value) return;
|
|
59
|
+
await deduper.join(runFetch);
|
|
60
|
+
// The run it joined was started before a `clear()` (another account):
|
|
61
|
+
// its answer was dropped, and this caller still needs one.
|
|
62
|
+
if (!state.isLoaded.value) await deduper.join(runFetch);
|
|
63
|
+
};
|
|
64
|
+
return { fetchPermissions, loadPermissions };
|
|
37
65
|
}
|
|
38
66
|
|
|
39
67
|
export function usePermissions() {
|
|
40
68
|
const state = usePermissionsState();
|
|
41
|
-
const fetchPermissions = createFetchPermissions(state);
|
|
69
|
+
const { fetchPermissions, loadPermissions } = createFetchPermissions(state);
|
|
42
70
|
|
|
43
71
|
async function refreshIfStale() {
|
|
44
72
|
const elapsed = Date.now() - state.lastFetchTime.value;
|
|
@@ -66,6 +94,7 @@ export function usePermissions() {
|
|
|
66
94
|
permissions: state.permissions,
|
|
67
95
|
isLoaded: state.isLoaded,
|
|
68
96
|
fetchPermissions,
|
|
97
|
+
loadPermissions,
|
|
69
98
|
refreshIfStale,
|
|
70
99
|
hasPermission,
|
|
71
100
|
clear,
|
|
@@ -5,7 +5,7 @@ const IN_FLIGHT_REFRESH_KEY = "__dmsAuthInFlightRefresh";
|
|
|
5
5
|
|
|
6
6
|
export const useSessionRecovery = () => {
|
|
7
7
|
const dmsApp = useDmsApp();
|
|
8
|
-
const { loggedIn, fetch: fetchSession } =
|
|
8
|
+
const { loggedIn, fetch: fetchSession } = useDmsUserSession();
|
|
9
9
|
// useDmsRoute() must not be called here: this composable runs inside route
|
|
10
10
|
// middleware (via useAuthFetch), where the DMS runtime warns that useDmsRoute is
|
|
11
11
|
// misleading. Resolve the route lazily from the router instead.
|
|
@@ -1,16 +1,25 @@
|
|
|
1
|
+
import { readerClock } from "./periodClock";
|
|
1
2
|
import type { PeriodComparison, PeriodRange } from "./types";
|
|
2
3
|
|
|
3
4
|
const NON_OVERLAP_GAP_MS = 1;
|
|
4
5
|
|
|
5
6
|
const MS_PER_DAY = 24 * 60 * 60 * 1000;
|
|
6
7
|
|
|
8
|
+
// In the reader's calendar (`readerClock`), as the range was resolved.
|
|
7
9
|
function isLocalMidnight(date: Date): boolean {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
const { hours, minutes, seconds, milliseconds } = readerClock().wall(date);
|
|
11
|
+
return hours === 0 && minutes === 0 && seconds === 0 && milliseconds === 0;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** `date` moved by whole calendar days and years, its time of day kept. */
|
|
15
|
+
function shiftCalendar(date: Date, days: number, years: number): Date {
|
|
16
|
+
const clock = readerClock();
|
|
17
|
+
const wall = clock.wall(date);
|
|
18
|
+
return clock.instant({
|
|
19
|
+
...wall,
|
|
20
|
+
year: wall.year + years,
|
|
21
|
+
day: wall.day + days,
|
|
22
|
+
});
|
|
14
23
|
}
|
|
15
24
|
|
|
16
25
|
/** A range of whole local days: midnight to the last millisecond of a day. */
|
|
@@ -27,10 +36,8 @@ function shiftPreviousPeriod(range: PeriodRange): PeriodRange {
|
|
|
27
36
|
if (isWholeDayRange(range)) {
|
|
28
37
|
const end = range.to.getTime() + NON_OVERLAP_GAP_MS;
|
|
29
38
|
const days = Math.round((end - range.from.getTime()) / MS_PER_DAY);
|
|
30
|
-
const from = new Date(range.from);
|
|
31
|
-
from.setDate(from.getDate() - days);
|
|
32
39
|
return {
|
|
33
|
-
from,
|
|
40
|
+
from: shiftCalendar(range.from, -days, 0),
|
|
34
41
|
to: new Date(range.from.getTime() - NON_OVERLAP_GAP_MS),
|
|
35
42
|
};
|
|
36
43
|
}
|
|
@@ -44,11 +51,10 @@ function shiftPreviousPeriod(range: PeriodRange): PeriodRange {
|
|
|
44
51
|
}
|
|
45
52
|
|
|
46
53
|
function shiftPreviousYear(range: PeriodRange): PeriodRange {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
return { from: previousFrom, to: previousTo };
|
|
54
|
+
return {
|
|
55
|
+
from: shiftCalendar(range.from, 0, -1),
|
|
56
|
+
to: shiftCalendar(range.to, 0, -1),
|
|
57
|
+
};
|
|
52
58
|
}
|
|
53
59
|
|
|
54
60
|
const COMPARISON_RESOLVERS: Record<
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { readerTimeZone } from "../../utils/regional";
|
|
2
|
+
|
|
3
|
+
/** The fields of a wall-clock time, months counted from 0 as `Date` does. */
|
|
4
|
+
export interface WallTime {
|
|
5
|
+
year: number;
|
|
6
|
+
month: number;
|
|
7
|
+
day: number;
|
|
8
|
+
hours: number;
|
|
9
|
+
minutes: number;
|
|
10
|
+
seconds: number;
|
|
11
|
+
milliseconds: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A calendar in one time zone: the wall-clock time of an instant, and the
|
|
16
|
+
* instant of a wall-clock time. Fields out of range roll over as `Date`'s do
|
|
17
|
+
* (day 0 is the last day of the month before).
|
|
18
|
+
*/
|
|
19
|
+
export interface PeriodClock {
|
|
20
|
+
wall(date: Date): WallTime;
|
|
21
|
+
instant(wall: WallTime): Date;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** The calendar of the zone the code runs in, through `Date`'s own methods. */
|
|
25
|
+
export const localClock: PeriodClock = {
|
|
26
|
+
wall: (date) => ({
|
|
27
|
+
year: date.getFullYear(),
|
|
28
|
+
month: date.getMonth(),
|
|
29
|
+
day: date.getDate(),
|
|
30
|
+
hours: date.getHours(),
|
|
31
|
+
minutes: date.getMinutes(),
|
|
32
|
+
seconds: date.getSeconds(),
|
|
33
|
+
milliseconds: date.getMilliseconds(),
|
|
34
|
+
}),
|
|
35
|
+
instant: (wall) =>
|
|
36
|
+
new Date(
|
|
37
|
+
wall.year,
|
|
38
|
+
wall.month,
|
|
39
|
+
wall.day,
|
|
40
|
+
wall.hours,
|
|
41
|
+
wall.minutes,
|
|
42
|
+
wall.seconds,
|
|
43
|
+
wall.milliseconds,
|
|
44
|
+
),
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const WALL_PARTS = ["year", "month", "day", "hour", "minute", "second"];
|
|
48
|
+
const zonedFormatters = new Map<string, Intl.DateTimeFormat>();
|
|
49
|
+
|
|
50
|
+
function zonedFormatter(timeZone: string): Intl.DateTimeFormat {
|
|
51
|
+
let formatter = zonedFormatters.get(timeZone);
|
|
52
|
+
if (!formatter) {
|
|
53
|
+
formatter = new Intl.DateTimeFormat("en-US", {
|
|
54
|
+
timeZone,
|
|
55
|
+
hourCycle: "h23",
|
|
56
|
+
year: "numeric",
|
|
57
|
+
month: "numeric",
|
|
58
|
+
day: "numeric",
|
|
59
|
+
hour: "numeric",
|
|
60
|
+
minute: "numeric",
|
|
61
|
+
second: "numeric",
|
|
62
|
+
});
|
|
63
|
+
zonedFormatters.set(timeZone, formatter);
|
|
64
|
+
}
|
|
65
|
+
return formatter;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function utcOf(wall: WallTime): number {
|
|
69
|
+
return Date.UTC(
|
|
70
|
+
wall.year,
|
|
71
|
+
wall.month,
|
|
72
|
+
wall.day,
|
|
73
|
+
wall.hours,
|
|
74
|
+
wall.minutes,
|
|
75
|
+
wall.seconds,
|
|
76
|
+
wall.milliseconds,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** The calendar of `timeZone`, through `Intl` whatever zone the code runs in. */
|
|
81
|
+
export function zonedClock(timeZone: string): PeriodClock {
|
|
82
|
+
const wall = (date: Date): WallTime => {
|
|
83
|
+
const parts = Object.fromEntries(
|
|
84
|
+
zonedFormatter(timeZone)
|
|
85
|
+
.formatToParts(date)
|
|
86
|
+
.filter((part) => WALL_PARTS.includes(part.type))
|
|
87
|
+
.map((part) => [part.type, Number(part.value)]),
|
|
88
|
+
);
|
|
89
|
+
return {
|
|
90
|
+
year: parts.year!,
|
|
91
|
+
month: parts.month! - 1,
|
|
92
|
+
day: parts.day!,
|
|
93
|
+
hours: parts.hour!,
|
|
94
|
+
minutes: parts.minute!,
|
|
95
|
+
seconds: parts.second!,
|
|
96
|
+
milliseconds: date.getUTCMilliseconds(),
|
|
97
|
+
};
|
|
98
|
+
};
|
|
99
|
+
// The zone's offset at an instant: how far its wall clock is from UTC.
|
|
100
|
+
const offsetAt = (time: number) => utcOf(wall(new Date(time))) - time;
|
|
101
|
+
return {
|
|
102
|
+
wall,
|
|
103
|
+
instant: (target) => {
|
|
104
|
+
const asUtc = utcOf(target);
|
|
105
|
+
// Twice: the offset at the first guess is the right one unless the
|
|
106
|
+
// guess and the answer fall on either side of a daylight saving change.
|
|
107
|
+
const guess = asUtc - offsetAt(asUtc);
|
|
108
|
+
return new Date(asUtc - offsetAt(guess));
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* The calendar a period is resolved in: the reader's browser's zone. That is
|
|
115
|
+
* the code's own in the browser; on a server, the zone the browser's cookie
|
|
116
|
+
* names (see `readerTimeZone`), or the server's own without one.
|
|
117
|
+
*/
|
|
118
|
+
export function readerClock(): PeriodClock {
|
|
119
|
+
const zone = readerTimeZone();
|
|
120
|
+
return zone ? zonedClock(zone) : localClock;
|
|
121
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { getCurrentInstance } from "vue";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The instant a server render resolves its periods from, handed to the
|
|
5
|
+
* browser with the page.
|
|
6
|
+
*/
|
|
7
|
+
const RENDER_NOW_STATE_KEY = "dms-period-render-now";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Whether the component setting up is hydrating the server's HTML: Vue binds
|
|
11
|
+
* a hydrating component's vnode to its server-rendered node before its setup
|
|
12
|
+
* runs, where a fresh mount has no node yet.
|
|
13
|
+
*/
|
|
14
|
+
function isHydratingSetup(): boolean {
|
|
15
|
+
return !!getCurrentInstance()?.vnode.el;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The instant a period set up now resolves from, or `null` for the clock's.
|
|
20
|
+
*
|
|
21
|
+
* A server render resolves every period of the request from one instant,
|
|
22
|
+
* which it transfers; the browser hydrating that render resolves from the
|
|
23
|
+
* same one, so a relative preset (`last-24h`) or a calendar one read across
|
|
24
|
+
* midnight keeps the bounds, key and labels the server rendered, and nothing
|
|
25
|
+
* reads again on hydration. Anywhere else (a client navigation, a call
|
|
26
|
+
* outside a component) it is `null`: the period follows the clock.
|
|
27
|
+
*/
|
|
28
|
+
export function periodRenderNow(): Date | null {
|
|
29
|
+
if (!getCurrentInstance()) return null;
|
|
30
|
+
if (typeof window === "undefined") {
|
|
31
|
+
const now = useDmsState<number>(RENDER_NOW_STATE_KEY, () => Date.now(), {
|
|
32
|
+
transfer: true,
|
|
33
|
+
});
|
|
34
|
+
return new Date(now.value);
|
|
35
|
+
}
|
|
36
|
+
if (!isHydratingSetup()) return null;
|
|
37
|
+
const transferred = useDmsState<number | null>(
|
|
38
|
+
RENDER_NOW_STATE_KEY,
|
|
39
|
+
() => null,
|
|
40
|
+
).value;
|
|
41
|
+
return typeof transferred === "number" ? new Date(transferred) : null;
|
|
42
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Ref } from "vue";
|
|
2
|
+
import { isKnownTimeZone } from "../../utils/regional";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The browser's time zone, kept in a cookie for a server render: a period is
|
|
6
|
+
* made of calendar days in the browser's zone, which the server has no other
|
|
7
|
+
* way to know.
|
|
8
|
+
*/
|
|
9
|
+
export const TIME_ZONE_COOKIE = "dms-time-zone";
|
|
10
|
+
const TIME_ZONE_COOKIE_MAX_AGE = 60 * 60 * 24 * 365;
|
|
11
|
+
|
|
12
|
+
/** The browser's time zone as its cookie holds it; `null` before it is written. */
|
|
13
|
+
export function useTimeZoneCookie(): Ref<string | null> {
|
|
14
|
+
return useDmsCookie<string | null>(TIME_ZONE_COOKIE, {
|
|
15
|
+
default: () => null,
|
|
16
|
+
maxAge: TIME_ZONE_COOKIE_MAX_AGE,
|
|
17
|
+
path: "/",
|
|
18
|
+
sameSite: "lax",
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Whether this render resolves a period as the browser does. The browser
|
|
24
|
+
* always does; a server once the browser's cookie names its time zone, in
|
|
25
|
+
* which the presets then resolve (`readerClock`).
|
|
26
|
+
*/
|
|
27
|
+
export function resolvesPeriodsAsBrowser(): boolean {
|
|
28
|
+
if (typeof window !== "undefined") return true;
|
|
29
|
+
const zone = useTimeZoneCookie().value;
|
|
30
|
+
return !!zone && isKnownTimeZone(zone);
|
|
31
|
+
}
|
|
@@ -8,22 +8,39 @@ import {
|
|
|
8
8
|
type PeriodPreset,
|
|
9
9
|
type PeriodRange,
|
|
10
10
|
} from "./types";
|
|
11
|
+
import { readerClock } from "./periodClock";
|
|
12
|
+
|
|
13
|
+
// Resolved in the reader's calendar (`readerClock`): the browser's zone, on
|
|
14
|
+
// the server as in the browser.
|
|
15
|
+
function atTime(
|
|
16
|
+
date: Date,
|
|
17
|
+
hours: number,
|
|
18
|
+
minutes: number,
|
|
19
|
+
seconds: number,
|
|
20
|
+
milliseconds: number,
|
|
21
|
+
): Date {
|
|
22
|
+
const clock = readerClock();
|
|
23
|
+
return clock.instant({
|
|
24
|
+
...clock.wall(date),
|
|
25
|
+
hours,
|
|
26
|
+
minutes,
|
|
27
|
+
seconds,
|
|
28
|
+
milliseconds,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
11
31
|
|
|
12
32
|
function startOfDay(date: Date): Date {
|
|
13
|
-
|
|
14
|
-
next.setHours(0, 0, 0, 0);
|
|
15
|
-
return next;
|
|
33
|
+
return atTime(date, 0, 0, 0, 0);
|
|
16
34
|
}
|
|
17
35
|
|
|
18
36
|
function endOfDay(date: Date): Date {
|
|
19
|
-
|
|
20
|
-
|
|
37
|
+
return atTime(
|
|
38
|
+
date,
|
|
21
39
|
END_OF_DAY_HOURS,
|
|
22
40
|
END_OF_DAY_MINUTES,
|
|
23
41
|
END_OF_DAY_SECONDS,
|
|
24
42
|
END_OF_DAY_MS,
|
|
25
43
|
);
|
|
26
|
-
return next;
|
|
27
44
|
}
|
|
28
45
|
|
|
29
46
|
function addDays(date: Date, amount: number): Date {
|
|
@@ -34,35 +51,47 @@ function addHours(date: Date, amount: number): Date {
|
|
|
34
51
|
return new Date(date.getTime() + amount * MS_PER_HOUR);
|
|
35
52
|
}
|
|
36
53
|
|
|
54
|
+
/** Midnight of a day given as year, month and day; they roll over as `Date`'s. */
|
|
55
|
+
function dayStart(year: number, month: number, day: number): Date {
|
|
56
|
+
return readerClock().instant({
|
|
57
|
+
year,
|
|
58
|
+
month,
|
|
59
|
+
day,
|
|
60
|
+
hours: 0,
|
|
61
|
+
minutes: 0,
|
|
62
|
+
seconds: 0,
|
|
63
|
+
milliseconds: 0,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
37
67
|
function startOfMonth(date: Date, monthOffset = 0): Date {
|
|
38
|
-
|
|
68
|
+
const { year, month } = readerClock().wall(date);
|
|
69
|
+
return dayStart(year, month + monthOffset, 1);
|
|
39
70
|
}
|
|
40
71
|
|
|
41
72
|
function endOfMonth(date: Date, monthOffset = 0): Date {
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
date.getMonth() + monthOffset + 1,
|
|
45
|
-
0,
|
|
46
|
-
);
|
|
47
|
-
return endOfDay(next);
|
|
73
|
+
const { year, month } = readerClock().wall(date);
|
|
74
|
+
return endOfDay(dayStart(year, month + monthOffset + 1, 0));
|
|
48
75
|
}
|
|
49
76
|
|
|
50
77
|
function startOfQuarter(date: Date, quarterOffset = 0): Date {
|
|
51
|
-
const
|
|
52
|
-
|
|
78
|
+
const { year, month } = readerClock().wall(date);
|
|
79
|
+
const quarter = Math.floor(month / 3) + quarterOffset;
|
|
80
|
+
return dayStart(year, quarter * 3, 1);
|
|
53
81
|
}
|
|
54
82
|
|
|
55
83
|
function endOfQuarter(date: Date, quarterOffset = 0): Date {
|
|
56
|
-
const
|
|
57
|
-
|
|
84
|
+
const { year, month } = readerClock().wall(date);
|
|
85
|
+
const quarter = Math.floor(month / 3) + quarterOffset;
|
|
86
|
+
return endOfDay(dayStart(year, quarter * 3 + 3, 0));
|
|
58
87
|
}
|
|
59
88
|
|
|
60
89
|
function startOfYear(date: Date, yearOffset = 0): Date {
|
|
61
|
-
return
|
|
90
|
+
return dayStart(readerClock().wall(date).year + yearOffset, 0, 1);
|
|
62
91
|
}
|
|
63
92
|
|
|
64
93
|
function endOfYear(date: Date, yearOffset = 0): Date {
|
|
65
|
-
return endOfDay(
|
|
94
|
+
return endOfDay(dayStart(readerClock().wall(date).year + yearOffset, 11, 31));
|
|
66
95
|
}
|
|
67
96
|
|
|
68
97
|
const PRESET_RANGE_RESOLVERS: Record<
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
computed,
|
|
3
3
|
getCurrentScope,
|
|
4
|
+
onMounted,
|
|
4
5
|
onScopeDispose,
|
|
5
6
|
ref,
|
|
7
|
+
shallowRef,
|
|
6
8
|
watch,
|
|
7
9
|
type ComputedRef,
|
|
8
10
|
type Ref,
|
|
@@ -23,6 +25,7 @@ import {
|
|
|
23
25
|
resolvePresetRange,
|
|
24
26
|
} from "./presetResolvers";
|
|
25
27
|
import { resolveComparisonRange } from "./compareResolvers";
|
|
28
|
+
import { periodRenderNow } from "./periodRenderNow";
|
|
26
29
|
|
|
27
30
|
export interface UsePeriodOptions {
|
|
28
31
|
defaultPreset?: PeriodPreset;
|
|
@@ -47,6 +50,11 @@ export interface UsePeriodReturn {
|
|
|
47
50
|
presets: ComputedRef<PeriodPreset[]>;
|
|
48
51
|
comparisons: ComputedRef<PeriodComparison[]>;
|
|
49
52
|
state: ComputedRef<PeriodState>;
|
|
53
|
+
/**
|
|
54
|
+
* The instant `state` was resolved from: what a label relative to now
|
|
55
|
+
* reads, so it holds still with the period (see `periodRenderNow`).
|
|
56
|
+
*/
|
|
57
|
+
resolvedAt: ComputedRef<Date>;
|
|
50
58
|
setPreset: (preset: PeriodPreset) => void;
|
|
51
59
|
setComparison: (comparison: PeriodComparison) => void;
|
|
52
60
|
setCustomRange: (range: PeriodRange | null) => void;
|
|
@@ -135,8 +143,7 @@ interface PeriodRefs {
|
|
|
135
143
|
customCompareRange: Ref<PeriodRange | null>;
|
|
136
144
|
}
|
|
137
145
|
|
|
138
|
-
function buildState(refs: PeriodRefs): PeriodState {
|
|
139
|
-
const now = new Date();
|
|
146
|
+
function buildState(refs: PeriodRefs, now: Date): PeriodState {
|
|
140
147
|
const fallback = refs.customRange.value ?? defaultFallbackRange(now);
|
|
141
148
|
const range = resolvePresetRange(refs.preset.value, now, fallback);
|
|
142
149
|
const compareRange = resolveComparisonRange(
|
|
@@ -176,6 +183,9 @@ function makeSetters(refs: PeriodRefs) {
|
|
|
176
183
|
* Reactive period state machine behind `PeriodSelector` — presets,
|
|
177
184
|
* comparisons and custom ranges resolved into a publishable `state`.
|
|
178
185
|
*
|
|
186
|
+
* A server render and the browser hydrating it resolve from the same instant
|
|
187
|
+
* (`periodRenderNow`), so both render the same bounds.
|
|
188
|
+
*
|
|
179
189
|
* Relative presets (`last-hour`, `last-24h`) re-resolve every minute, and
|
|
180
190
|
* calendar presets (`today`, `last-7-days`, `this-month`...) at each local
|
|
181
191
|
* midnight, so a page left open overnight moves on to the new day.
|
|
@@ -201,10 +211,28 @@ export function usePeriod(options: UsePeriodOptions = {}): UsePeriodReturn {
|
|
|
201
211
|
() => options.comparisons ?? DEFAULT_COMPARISONS,
|
|
202
212
|
);
|
|
203
213
|
const presetClock = usePresetClock(refs.preset, options.holdClock);
|
|
204
|
-
|
|
214
|
+
// The server render's instant, until the clock ticks or the period changes
|
|
215
|
+
// once on screen: what the setup applies (the URL's period) the server
|
|
216
|
+
// applied as well.
|
|
217
|
+
const renderNow = shallowRef(periodRenderNow());
|
|
218
|
+
if (renderNow.value) {
|
|
219
|
+
onMounted(() => {
|
|
220
|
+
watch(
|
|
221
|
+
[presetClock, ...Object.values(refs)],
|
|
222
|
+
() => {
|
|
223
|
+
renderNow.value = null;
|
|
224
|
+
},
|
|
225
|
+
{ flush: "sync" },
|
|
226
|
+
);
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
const resolution = computed(() => {
|
|
205
230
|
void presetClock.value;
|
|
206
|
-
|
|
231
|
+
const now = renderNow.value ?? new Date();
|
|
232
|
+
return { now, state: buildState(refs, now) };
|
|
207
233
|
});
|
|
234
|
+
const state = computed<PeriodState>(() => resolution.value.state);
|
|
235
|
+
const resolvedAt = computed<Date>(() => resolution.value.now);
|
|
208
236
|
const setters = makeSetters(refs);
|
|
209
237
|
|
|
210
238
|
return {
|
|
@@ -212,6 +240,7 @@ export function usePeriod(options: UsePeriodOptions = {}): UsePeriodReturn {
|
|
|
212
240
|
presets,
|
|
213
241
|
comparisons,
|
|
214
242
|
state,
|
|
243
|
+
resolvedAt,
|
|
215
244
|
...setters,
|
|
216
245
|
};
|
|
217
246
|
}
|