@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
|
@@ -367,41 +367,41 @@ defineExpose(flow);
|
|
|
367
367
|
halo (which only targets the built-in node types), keeping node and edge
|
|
368
368
|
selection states visually consistent. */
|
|
369
369
|
.dms-flow-canvas .vue-flow__node.selected {
|
|
370
|
-
box-shadow: 0 0 0 1.5px var(--color-
|
|
370
|
+
box-shadow: 0 0 0 1.5px var(--ui-color-primary-500);
|
|
371
371
|
}
|
|
372
372
|
|
|
373
|
-
/* Handles:
|
|
373
|
+
/* Handles: primary accent, ringed with the card surface for contrast. */
|
|
374
374
|
.dms-flow-canvas .vue-flow__handle {
|
|
375
375
|
width: 9px;
|
|
376
376
|
height: 9px;
|
|
377
|
-
background: var(--color-
|
|
377
|
+
background: var(--ui-color-primary-500);
|
|
378
378
|
border: 2px solid var(--dms-surface-card);
|
|
379
379
|
border-radius: 9999px;
|
|
380
380
|
}
|
|
381
381
|
|
|
382
382
|
.dms-flow-canvas .vue-flow__handle:hover {
|
|
383
|
-
background: var(--color-
|
|
383
|
+
background: var(--ui-color-primary-400);
|
|
384
384
|
}
|
|
385
385
|
|
|
386
|
-
/* Edges: hairline by default,
|
|
386
|
+
/* Edges: hairline by default, primary accent on selection / animation. */
|
|
387
387
|
.dms-flow-canvas .vue-flow__edge-path {
|
|
388
388
|
stroke: var(--ui-border-accented);
|
|
389
389
|
stroke-width: 1.5;
|
|
390
390
|
}
|
|
391
391
|
|
|
392
392
|
.dms-flow-canvas .vue-flow__edge.animated .vue-flow__edge-path {
|
|
393
|
-
stroke: var(--color-
|
|
393
|
+
stroke: var(--ui-color-primary-500);
|
|
394
394
|
}
|
|
395
395
|
|
|
396
396
|
.dms-flow-canvas .vue-flow__edge.selected .vue-flow__edge-path,
|
|
397
397
|
.dms-flow-canvas .vue-flow__edge:focus .vue-flow__edge-path,
|
|
398
398
|
.dms-flow-canvas .vue-flow__edge:focus-visible .vue-flow__edge-path {
|
|
399
|
-
stroke: var(--color-
|
|
399
|
+
stroke: var(--ui-color-primary-500);
|
|
400
400
|
stroke-width: 2;
|
|
401
401
|
}
|
|
402
402
|
|
|
403
403
|
.dms-flow-canvas .vue-flow__connection-path {
|
|
404
|
-
stroke: var(--color-
|
|
404
|
+
stroke: var(--ui-color-primary-500);
|
|
405
405
|
stroke-width: 2;
|
|
406
406
|
}
|
|
407
407
|
|
|
@@ -514,7 +514,7 @@ defineExpose(flow);
|
|
|
514
514
|
}
|
|
515
515
|
|
|
516
516
|
.dms-flow-canvas__delete-button:focus-visible {
|
|
517
|
-
outline: 2px solid var(--color-
|
|
517
|
+
outline: 2px solid var(--ui-color-primary-500);
|
|
518
518
|
outline-offset: 1px;
|
|
519
519
|
}
|
|
520
520
|
</style>
|
|
@@ -549,30 +549,32 @@ allFields.value.forEach((field) => {
|
|
|
549
549
|
|
|
550
550
|
let recordLoad: FormRecordLoad | undefined;
|
|
551
551
|
if (props.fetchUrl) {
|
|
552
|
+
// Keyed on the record too: the load outlives a drawer or modal, and the
|
|
553
|
+
// next opening on another row must not show this one's values. A reopening
|
|
554
|
+
// on the same row shows the cached values at once and reads them again.
|
|
552
555
|
const formLoad = await useDmsAsyncData(
|
|
553
|
-
`form-${props.componentId}-${props.pageId}`,
|
|
556
|
+
`form-${props.componentId}-${props.pageId}-${resolvedFetchUrl.value ?? ""}`,
|
|
554
557
|
async () => ({
|
|
555
558
|
values: await fetchData(),
|
|
556
559
|
initial: initialValues.value,
|
|
557
560
|
}),
|
|
558
561
|
);
|
|
559
562
|
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
});
|
|
563
|
+
recordLoad = trackFormRecordLoad(
|
|
564
|
+
formLoad,
|
|
565
|
+
(payload) => {
|
|
566
|
+
if (payload.values && Object.keys(payload.values).length > 0) {
|
|
567
|
+
Object.assign(state.value, payload.values);
|
|
568
|
+
}
|
|
569
|
+
if (payload.initial && payload.initial !== initialValues.value) {
|
|
570
|
+
initialValues.value = { ...initialValues.value, ...payload.initial };
|
|
571
|
+
}
|
|
572
|
+
},
|
|
573
|
+
{
|
|
574
|
+
isEditing: () => changedFields.value.length > 0,
|
|
575
|
+
onReloaded: markFormClean,
|
|
576
|
+
},
|
|
577
|
+
);
|
|
576
578
|
}
|
|
577
579
|
|
|
578
580
|
const loadFailure = computed(
|
|
@@ -810,7 +812,7 @@ const realtimeContext = computed(() =>
|
|
|
810
812
|
parseRealtimeContext(resolvedFetchUrl.value),
|
|
811
813
|
);
|
|
812
814
|
const showConcurrentEditBanner = ref(false);
|
|
813
|
-
const { user: realtimeUser } =
|
|
815
|
+
const { user: realtimeUser } = useDmsUserSession();
|
|
814
816
|
const { $authFetch } = useAuthFetch();
|
|
815
817
|
const pageRealtime = useUserRealtime();
|
|
816
818
|
const acquired = ref<{
|
|
@@ -83,7 +83,8 @@ const { data, status, execute } = await useDmsAsyncData(
|
|
|
83
83
|
},
|
|
84
84
|
});
|
|
85
85
|
},
|
|
86
|
-
|
|
86
|
+
// Options to pick from, not the page's data.
|
|
87
|
+
{ lazy: true, immediate: false, refreshOnPageRefresh: false },
|
|
87
88
|
);
|
|
88
89
|
|
|
89
90
|
const keyMapping = computed(() => resolveCascaderKeyMapping(props.keyMapping));
|
|
@@ -3,7 +3,6 @@ import { useId } from "vue";
|
|
|
3
3
|
import DmsFieldError from "../../field-error/FieldError.vue";
|
|
4
4
|
import { fieldErrorId } from "#dms-core/app/composables/useFieldErrors";
|
|
5
5
|
import { FORM_FIELD_LOADING_KEY } from "../../../composables/form/types/field-loading";
|
|
6
|
-
import { FORM_CONTENT_LANGUAGE_KEY } from "../../../composables/form/types/content-language";
|
|
7
6
|
import type { PresignResponse } from "../../../composables/form/useUploadWithProgress";
|
|
8
7
|
import {
|
|
9
8
|
checkFileConstraints,
|
|
@@ -11,9 +10,9 @@ import {
|
|
|
11
10
|
type FileFieldConstraints,
|
|
12
11
|
} from "../../../utils/fileConstraints";
|
|
13
12
|
import {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
} from "../../../
|
|
13
|
+
type FileMetadata,
|
|
14
|
+
useFileMetadata,
|
|
15
|
+
} from "../../../build/composables/files/useFileMetadata";
|
|
17
16
|
|
|
18
17
|
export type FileConstraints = FileFieldConstraints;
|
|
19
18
|
|
|
@@ -24,15 +23,6 @@ interface UploadingFile {
|
|
|
24
23
|
error?: string;
|
|
25
24
|
}
|
|
26
25
|
|
|
27
|
-
interface FileMetadataResponse {
|
|
28
|
-
resourceKey: string;
|
|
29
|
-
filename: string;
|
|
30
|
-
size: number;
|
|
31
|
-
mimetype: string;
|
|
32
|
-
url: string;
|
|
33
|
-
expiresAt?: number;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
26
|
interface FileProps {
|
|
37
27
|
modelValue?: string | string[] | null;
|
|
38
28
|
multiple?: boolean;
|
|
@@ -70,14 +60,8 @@ const { uploadWithProgress } = useUploadWithProgress();
|
|
|
70
60
|
const selectedFiles = ref<File | File[] | null>(null);
|
|
71
61
|
const uploadingFiles = ref<UploadingFile[]>([]);
|
|
72
62
|
const uploadedKeys = ref<string[]>([]);
|
|
73
|
-
|
|
74
|
-
const
|
|
75
|
-
const metadataTimers = new Map<string, ReturnType<typeof setTimeout>>();
|
|
76
|
-
const metadataFailures = new Map<string, number>();
|
|
77
|
-
const pendingMetadata = new Map<string, Promise<void>>();
|
|
78
|
-
let isDisposed = false;
|
|
79
|
-
const contentLanguage = inject(FORM_CONTENT_LANGUAGE_KEY, undefined);
|
|
80
|
-
const URL_REFRESH_LEAD_MS = 5000;
|
|
63
|
+
// Read with the page for the stored value, in the browser for a new one.
|
|
64
|
+
const files = useFileMetadata(props.storage);
|
|
81
65
|
|
|
82
66
|
const initializeFromModelValue = (
|
|
83
67
|
val: string | string[] | null | undefined,
|
|
@@ -93,118 +77,26 @@ const initializeFromModelValue = (
|
|
|
93
77
|
|
|
94
78
|
initializeFromModelValue(props.modelValue);
|
|
95
79
|
|
|
96
|
-
const scheduleMetadataRefresh = (resourceKey: string, expiresAt?: number) => {
|
|
97
|
-
if (import.meta.env.SSR) return;
|
|
98
|
-
if (isDisposed || !uploadedKeys.value.includes(resourceKey)) return;
|
|
99
|
-
const currentTimer = metadataTimers.get(resourceKey);
|
|
100
|
-
if (currentTimer) clearTimeout(currentTimer);
|
|
101
|
-
if (expiresAt === undefined) return;
|
|
102
|
-
const remaining = Math.max(expiresAt - Date.now(), 0);
|
|
103
|
-
const refreshLead = Math.min(URL_REFRESH_LEAD_MS, remaining / 2);
|
|
104
|
-
metadataTimers.set(
|
|
105
|
-
resourceKey,
|
|
106
|
-
setTimeout(() => fetchMetadata(resourceKey, true), remaining - refreshLead),
|
|
107
|
-
);
|
|
108
|
-
};
|
|
109
|
-
|
|
110
|
-
const fetchMetadata = async (
|
|
111
|
-
resourceKey: string,
|
|
112
|
-
force = false,
|
|
113
|
-
): Promise<void> => {
|
|
114
|
-
if (isDisposed) return;
|
|
115
|
-
const pending = pendingMetadata.get(resourceKey);
|
|
116
|
-
if (pending) {
|
|
117
|
-
await pending;
|
|
118
|
-
if (force) await fetchMetadata(resourceKey, true);
|
|
119
|
-
return;
|
|
120
|
-
}
|
|
121
|
-
if (!force && fileMetadata.value.has(resourceKey)) return;
|
|
122
|
-
|
|
123
|
-
const request = requestMetadata(resourceKey);
|
|
124
|
-
pendingMetadata.set(resourceKey, request);
|
|
125
|
-
await request;
|
|
126
|
-
};
|
|
127
|
-
|
|
128
|
-
const markUnavailable = (resourceKey: string, error: unknown) => {
|
|
129
|
-
const cached = fileMetadata.value.get(resourceKey);
|
|
130
|
-
const isExpired =
|
|
131
|
-
cached?.expiresAt !== undefined && cached.expiresAt <= Date.now();
|
|
132
|
-
if (cached && !isExpired && !isDefinitiveMetadataFailure(error)) return;
|
|
133
|
-
fileMetadata.value.set(resourceKey, {
|
|
134
|
-
resourceKey,
|
|
135
|
-
filename: cached?.filename ?? resourceKey,
|
|
136
|
-
size: cached?.size ?? 0,
|
|
137
|
-
mimetype: cached?.mimetype ?? "application/octet-stream",
|
|
138
|
-
url: "",
|
|
139
|
-
});
|
|
140
|
-
unavailableKeys.value.add(resourceKey);
|
|
141
|
-
};
|
|
142
|
-
|
|
143
|
-
const handleMetadataFailure = (resourceKey: string, error: unknown) => {
|
|
144
|
-
markUnavailable(resourceKey, error);
|
|
145
|
-
const failures = (metadataFailures.get(resourceKey) ?? 0) + 1;
|
|
146
|
-
metadataFailures.set(resourceKey, failures);
|
|
147
|
-
const delay = resolveMetadataRetryDelay(error, failures);
|
|
148
|
-
if (delay === null || import.meta.env.SSR) return;
|
|
149
|
-
metadataTimers.set(
|
|
150
|
-
resourceKey,
|
|
151
|
-
setTimeout(() => fetchMetadata(resourceKey, true), delay),
|
|
152
|
-
);
|
|
153
|
-
};
|
|
154
|
-
|
|
155
|
-
const requestMetadata = async (resourceKey: string): Promise<void> => {
|
|
156
|
-
try {
|
|
157
|
-
const metadata = await $authFetch<FileMetadataResponse>(
|
|
158
|
-
`/api/files/metadata`,
|
|
159
|
-
{
|
|
160
|
-
query: {
|
|
161
|
-
resourceKey,
|
|
162
|
-
storage: props.storage,
|
|
163
|
-
},
|
|
164
|
-
headers: contentLanguage
|
|
165
|
-
? { [CONTENT_LANGUAGE_HEADER]: contentLanguage }
|
|
166
|
-
: undefined,
|
|
167
|
-
},
|
|
168
|
-
);
|
|
169
|
-
if (isDisposed || !uploadedKeys.value.includes(resourceKey)) return;
|
|
170
|
-
fileMetadata.value.set(resourceKey, metadata);
|
|
171
|
-
unavailableKeys.value.delete(resourceKey);
|
|
172
|
-
metadataFailures.delete(resourceKey);
|
|
173
|
-
scheduleMetadataRefresh(resourceKey, metadata.expiresAt);
|
|
174
|
-
} catch (error) {
|
|
175
|
-
if (isDisposed || !uploadedKeys.value.includes(resourceKey)) return;
|
|
176
|
-
handleMetadataFailure(resourceKey, error);
|
|
177
|
-
} finally {
|
|
178
|
-
pendingMetadata.delete(resourceKey);
|
|
179
|
-
}
|
|
180
|
-
};
|
|
181
|
-
|
|
182
80
|
const openFile = async (resourceKey: string) => {
|
|
183
|
-
await
|
|
184
|
-
const url =
|
|
81
|
+
await files.resolve(resourceKey, true);
|
|
82
|
+
const url = files.metadata(resourceKey)?.url;
|
|
185
83
|
if (url) window.open(url, "_blank", "noopener,noreferrer");
|
|
186
84
|
};
|
|
187
85
|
|
|
188
|
-
const fetchAllMetadata = async (keys: string[]) => {
|
|
189
|
-
await Promise.all(keys.map((key) => fetchMetadata(key)));
|
|
190
|
-
};
|
|
191
|
-
|
|
192
86
|
watch(
|
|
193
87
|
() => props.modelValue,
|
|
194
88
|
(val) => {
|
|
195
89
|
const newKeys = !val ? [] : typeof val === "string" ? [val] : val;
|
|
196
90
|
if (newKeys.join(",") !== uploadedKeys.value.join(",")) {
|
|
197
91
|
uploadedKeys.value = newKeys;
|
|
198
|
-
fetchAllMetadata(newKeys);
|
|
199
92
|
}
|
|
200
93
|
},
|
|
201
94
|
);
|
|
202
95
|
|
|
96
|
+
// A copy: an upload into a multiple field pushes its key.
|
|
203
97
|
watch(
|
|
204
|
-
uploadedKeys,
|
|
205
|
-
(keys) =>
|
|
206
|
-
fetchAllMetadata(keys);
|
|
207
|
-
},
|
|
98
|
+
() => [...uploadedKeys.value],
|
|
99
|
+
(keys) => keys.forEach((key) => void files.resolve(key)),
|
|
208
100
|
{ immediate: true },
|
|
209
101
|
);
|
|
210
102
|
|
|
@@ -312,20 +204,27 @@ const removeUploadingFile = (id: string) => {
|
|
|
312
204
|
|
|
313
205
|
const removeUploadedFile = (key: string) => {
|
|
314
206
|
uploadedKeys.value = uploadedKeys.value.filter((k) => k !== key);
|
|
315
|
-
fileMetadata.value.delete(key);
|
|
316
|
-
unavailableKeys.value.delete(key);
|
|
317
|
-
metadataFailures.delete(key);
|
|
318
|
-
const timer = metadataTimers.get(key);
|
|
319
|
-
if (timer) clearTimeout(timer);
|
|
320
207
|
emitValue();
|
|
321
208
|
};
|
|
322
209
|
|
|
323
|
-
const
|
|
324
|
-
|
|
210
|
+
const isUnavailable = (key: string): boolean => files.isUnavailable(key);
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* What a stored file shows: its metadata, or, once it cannot be read, its
|
|
214
|
+
* last known name (the key otherwise) without a link.
|
|
215
|
+
*/
|
|
216
|
+
const getMetadata = (key: string): FileMetadata | undefined => {
|
|
217
|
+
if (!isUnavailable(key)) return files.metadata(key);
|
|
218
|
+
const known = files.lastRead(key);
|
|
219
|
+
return {
|
|
220
|
+
resourceKey: key,
|
|
221
|
+
filename: known?.filename ?? key,
|
|
222
|
+
size: known?.size ?? 0,
|
|
223
|
+
mimetype: known?.mimetype ?? "application/octet-stream",
|
|
224
|
+
url: "",
|
|
225
|
+
};
|
|
325
226
|
};
|
|
326
227
|
|
|
327
|
-
const isUnavailable = (key: string): boolean => unavailableKeys.value.has(key);
|
|
328
|
-
|
|
329
228
|
const isImage = (mimetype: string): boolean => {
|
|
330
229
|
return mimetype.startsWith("image/");
|
|
331
230
|
};
|
|
@@ -363,11 +262,6 @@ const maxSizeFormatted = computed(() => {
|
|
|
363
262
|
const isUploading = computed(() => uploadingFiles.value.length > 0);
|
|
364
263
|
|
|
365
264
|
const setFieldLoading = inject(FORM_FIELD_LOADING_KEY, null);
|
|
366
|
-
|
|
367
|
-
onScopeDispose(() => {
|
|
368
|
-
isDisposed = true;
|
|
369
|
-
metadataTimers.forEach(clearTimeout);
|
|
370
|
-
});
|
|
371
265
|
</script>
|
|
372
266
|
|
|
373
267
|
<template>
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
import DmsFieldError from "../../field-error/FieldError.vue";
|
|
3
3
|
import { fieldErrorId } from "#dms-core/app/composables/useFieldErrors";
|
|
4
4
|
import { FORM_FIELD_LOADING_KEY } from "../../../composables/form/types/field-loading";
|
|
5
|
-
import { FORM_CONTENT_LANGUAGE_KEY } from "../../../composables/form/types/content-language";
|
|
6
5
|
import type { PresignResponse } from "../../../composables/form/useUploadWithProgress";
|
|
7
6
|
import { useKeyboardPlatform } from "../../../composables/global/keyboardPlatform";
|
|
8
7
|
import {
|
|
@@ -13,10 +12,7 @@ import {
|
|
|
13
12
|
type ImageResizeBounds,
|
|
14
13
|
resizeImageToBounds,
|
|
15
14
|
} from "../../../utils/imageResize";
|
|
16
|
-
import {
|
|
17
|
-
isDefinitiveMetadataFailure,
|
|
18
|
-
resolveMetadataRetryDelay,
|
|
19
|
-
} from "../../../utils/metadataRetry";
|
|
15
|
+
import { useFileMetadata } from "../../../build/composables/files/useFileMetadata";
|
|
20
16
|
|
|
21
17
|
export type ImageConstraints = FileFieldConstraints;
|
|
22
18
|
|
|
@@ -51,16 +47,6 @@ interface GalleryItem {
|
|
|
51
47
|
principal: boolean;
|
|
52
48
|
status: ItemStatus;
|
|
53
49
|
progress: number;
|
|
54
|
-
expiresAt?: number;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
interface FileMetadataResponse {
|
|
58
|
-
resourceKey: string;
|
|
59
|
-
filename: string;
|
|
60
|
-
size: number;
|
|
61
|
-
mimetype: string;
|
|
62
|
-
url: string;
|
|
63
|
-
expiresAt?: number;
|
|
64
50
|
}
|
|
65
51
|
|
|
66
52
|
const props = defineProps<ImageProps>();
|
|
@@ -79,10 +65,10 @@ const { formatShortcut } = useKeyboardPlatform();
|
|
|
79
65
|
const pasteShortcut = computed(() => formatShortcut(["meta", "v"]));
|
|
80
66
|
const { uploadWithProgress } = useUploadWithProgress();
|
|
81
67
|
const setFieldLoading = inject(FORM_FIELD_LOADING_KEY, null);
|
|
82
|
-
|
|
68
|
+
// Read with the page for the stored value, in the browser for a new one.
|
|
69
|
+
const files = useFileMetadata(props.storage);
|
|
83
70
|
|
|
84
71
|
const DEFAULT_ACCEPT = "image/*";
|
|
85
|
-
const URL_REFRESH_LEAD_MS = 5000;
|
|
86
72
|
const FAILURE_LABEL_KEYS: Partial<Record<ItemStatus, string>> = {
|
|
87
73
|
error: "dms.form.image.upload_failed",
|
|
88
74
|
unavailable: "dms.form.image.file_unavailable",
|
|
@@ -114,10 +100,6 @@ const dragItemId = ref<string | null>(null);
|
|
|
114
100
|
const dragOverItemId = ref<string | null>(null);
|
|
115
101
|
const detailItemId = ref<string | null>(null);
|
|
116
102
|
const detailAltDraft = ref("");
|
|
117
|
-
const metadataTimers = new Map<string, ReturnType<typeof setTimeout>>();
|
|
118
|
-
const metadataFailures = new Map<string, number>();
|
|
119
|
-
const pendingMetadata = new Set<string>();
|
|
120
|
-
let isDisposed = false;
|
|
121
103
|
|
|
122
104
|
const toValueArray = (
|
|
123
105
|
value: ImageItemValue | ImageItemValue[] | null | undefined,
|
|
@@ -126,84 +108,43 @@ const toValueArray = (
|
|
|
126
108
|
return Array.isArray(value) ? value : [value];
|
|
127
109
|
};
|
|
128
110
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
item.id,
|
|
151
|
-
setTimeout(() => fetchItemMetadata(item, true), delay),
|
|
152
|
-
);
|
|
153
|
-
};
|
|
154
|
-
|
|
155
|
-
const scheduleItemExpiryRefresh = (item: GalleryItem) => {
|
|
156
|
-
if (item.expiresAt === undefined) return;
|
|
157
|
-
const remaining = Math.max(item.expiresAt - Date.now(), 0);
|
|
158
|
-
const refreshLead = Math.min(URL_REFRESH_LEAD_MS, remaining / 2);
|
|
159
|
-
scheduleItemMetadataRefresh(item, remaining - refreshLead);
|
|
160
|
-
};
|
|
161
|
-
|
|
162
|
-
const handleItemMetadataFailure = (item: GalleryItem, error: unknown) => {
|
|
163
|
-
if (
|
|
164
|
-
isDefinitiveMetadataFailure(error) ||
|
|
165
|
-
!item.url ||
|
|
166
|
-
(item.expiresAt !== undefined && item.expiresAt <= Date.now())
|
|
167
|
-
) {
|
|
168
|
-
item.url = "";
|
|
169
|
-
item.status = "unavailable";
|
|
170
|
-
}
|
|
171
|
-
const failures = (metadataFailures.get(item.id) ?? 0) + 1;
|
|
172
|
-
metadataFailures.set(item.id, failures);
|
|
173
|
-
const delay = resolveMetadataRetryDelay(error, failures);
|
|
174
|
-
if (delay !== null) scheduleItemMetadataRefresh(item, delay);
|
|
175
|
-
};
|
|
176
|
-
|
|
177
|
-
const fetchItemMetadata = async (item: GalleryItem, force = false) => {
|
|
178
|
-
if (isDisposed) return;
|
|
179
|
-
if (!item.key || (!force && item.url) || pendingMetadata.has(item.id)) return;
|
|
180
|
-
|
|
181
|
-
pendingMetadata.add(item.id);
|
|
182
|
-
try {
|
|
183
|
-
const metadata = await $authFetch<FileMetadataResponse>(
|
|
184
|
-
"/api/files/metadata",
|
|
185
|
-
{
|
|
186
|
-
query: { resourceKey: item.key, storage: props.storage },
|
|
187
|
-
headers: contentLanguage
|
|
188
|
-
? { [CONTENT_LANGUAGE_HEADER]: contentLanguage }
|
|
189
|
-
: undefined,
|
|
111
|
+
/**
|
|
112
|
+
* A stored image: what the tile shows of the file (its URL, name, size,
|
|
113
|
+
* whether it can be read) follows its shared metadata, so the server renders
|
|
114
|
+
* it and the URL stays fresh; the rest is the field's own.
|
|
115
|
+
*/
|
|
116
|
+
const createItemFromValue = (value: ImageItemValue): GalleryItem =>
|
|
117
|
+
Object.defineProperties(
|
|
118
|
+
{
|
|
119
|
+
id: crypto.randomUUID(),
|
|
120
|
+
key: value.key,
|
|
121
|
+
file: null,
|
|
122
|
+
width: null,
|
|
123
|
+
height: null,
|
|
124
|
+
alt: value.alt ?? "",
|
|
125
|
+
principal: value.principal ?? false,
|
|
126
|
+
progress: 100,
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
url: {
|
|
130
|
+
get: () => files.metadata(value.key)?.url ?? "",
|
|
131
|
+
enumerable: true,
|
|
190
132
|
},
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
};
|
|
133
|
+
filename: {
|
|
134
|
+
get: () => files.lastRead(value.key)?.filename ?? value.key,
|
|
135
|
+
enumerable: true,
|
|
136
|
+
},
|
|
137
|
+
size: {
|
|
138
|
+
get: () => files.lastRead(value.key)?.size ?? null,
|
|
139
|
+
enumerable: true,
|
|
140
|
+
},
|
|
141
|
+
status: {
|
|
142
|
+
get: (): ItemStatus =>
|
|
143
|
+
files.isUnavailable(value.key) ? "unavailable" : "done",
|
|
144
|
+
enumerable: true,
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
) as GalleryItem;
|
|
207
148
|
|
|
208
149
|
const valueSignature = (values: ImageItemValue[]): string =>
|
|
209
150
|
JSON.stringify(
|
|
@@ -225,17 +166,13 @@ const currentSignature = (): string =>
|
|
|
225
166
|
|
|
226
167
|
const releaseItem = (item: GalleryItem) => {
|
|
227
168
|
if (item.file && item.url) URL.revokeObjectURL(item.url);
|
|
228
|
-
const timer = metadataTimers.get(item.id);
|
|
229
|
-
if (timer) clearTimeout(timer);
|
|
230
|
-
metadataTimers.delete(item.id);
|
|
231
|
-
metadataFailures.delete(item.id);
|
|
232
169
|
};
|
|
233
170
|
|
|
234
171
|
const initializeFromModelValue = () => {
|
|
235
172
|
items.value.forEach(releaseItem);
|
|
236
173
|
const values = toValueArray(props.modelValue);
|
|
237
174
|
items.value = values.map(createItemFromValue);
|
|
238
|
-
|
|
175
|
+
values.forEach((value) => void files.resolve(value.key));
|
|
239
176
|
};
|
|
240
177
|
|
|
241
178
|
initializeFromModelValue();
|
|
@@ -594,9 +531,7 @@ const itemMeta = (item: GalleryItem): string => {
|
|
|
594
531
|
};
|
|
595
532
|
|
|
596
533
|
onBeforeUnmount(() => {
|
|
597
|
-
isDisposed = true;
|
|
598
534
|
items.value.forEach(releaseItem);
|
|
599
|
-
metadataTimers.forEach(clearTimeout);
|
|
600
535
|
});
|
|
601
536
|
</script>
|
|
602
537
|
|
|
@@ -271,6 +271,8 @@ const dynamicCountryData = props.searchUrl
|
|
|
271
271
|
lazy: true,
|
|
272
272
|
immediate: false,
|
|
273
273
|
watch: [countrySearchTermDebounced],
|
|
274
|
+
// Options to pick from, not the page's data.
|
|
275
|
+
refreshOnPageRefresh: false,
|
|
274
276
|
})
|
|
275
277
|
: { data: ref(null), status: ref("idle"), execute: () => {} };
|
|
276
278
|
|
|
@@ -107,13 +107,11 @@ const { $authFetch } = useAuthFetch();
|
|
|
107
107
|
const toast = useToast();
|
|
108
108
|
const { t } = useI18n();
|
|
109
109
|
const { processI18n } = useTranslation();
|
|
110
|
-
const { hasPermission,
|
|
110
|
+
const { hasPermission, loadPermissions } = usePermissions();
|
|
111
111
|
const { open: openDrawer } = useDrawer();
|
|
112
112
|
const { clearGuards } = useLeaveGuard();
|
|
113
113
|
|
|
114
|
-
|
|
115
|
-
await fetchPermissions().catch(() => {});
|
|
116
|
-
}
|
|
114
|
+
await loadPermissions().catch(() => {});
|
|
117
115
|
|
|
118
116
|
const addContainerId = `relation-add-${useId()}`;
|
|
119
117
|
// Shallow: `SelectMenuItem` carries a string index signature, so a deeply
|
|
@@ -237,6 +235,8 @@ const { data, status, execute } = await useDmsAsyncData(
|
|
|
237
235
|
lazy: true,
|
|
238
236
|
immediate: false,
|
|
239
237
|
watch: [searchTermDebounced],
|
|
238
|
+
// Options to pick from, not the page's data.
|
|
239
|
+
refreshOnPageRefresh: false,
|
|
240
240
|
},
|
|
241
241
|
);
|
|
242
242
|
|
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
type ComparisonControl,
|
|
20
20
|
resolveComparisonControl,
|
|
21
21
|
} from "../../build/composables/period/periodComparisonControl";
|
|
22
|
-
import {
|
|
22
|
+
import { readerClock } from "#dms-core/app/composables/period/periodClock";
|
|
23
23
|
import { usePeriodLocation } from "../../build/composables/period/usePeriodLocation";
|
|
24
24
|
import {
|
|
25
25
|
type CalendarRange,
|
|
@@ -332,20 +332,14 @@ const committedRangeLabel = computed(() => {
|
|
|
332
332
|
return range ? formatPeriodRange(range, locale.value) : null;
|
|
333
333
|
});
|
|
334
334
|
|
|
335
|
-
/**
|
|
336
|
-
* The instant a relative label is read from: a rolling window's own end, so
|
|
337
|
-
* it holds still with the window while the page is paused.
|
|
338
|
-
*/
|
|
339
|
-
function labelNow(preset: PeriodPreset, range: PeriodRange): Date {
|
|
340
|
-
return isRelativePreset(preset) ? range.to : new Date();
|
|
341
|
-
}
|
|
342
|
-
|
|
343
335
|
// Only a window reaching now reads "since …"; one that has ended keeps its
|
|
344
|
-
// bounds.
|
|
336
|
+
// bounds. "Now" is the instant the period was resolved from: it holds still
|
|
337
|
+
// with the period while the page is paused, and is the server's while the
|
|
338
|
+
// browser hydrates its render.
|
|
345
339
|
const relativeRangeLabel = computed<string | null>(() => {
|
|
346
|
-
const {
|
|
340
|
+
const { range } = period.state.value;
|
|
347
341
|
if (props.rangeLabel !== RELATIVE_RANGE_LABEL || !range) return null;
|
|
348
|
-
const now =
|
|
342
|
+
const now = period.resolvedAt.value;
|
|
349
343
|
if (range.to.getTime() < now.getTime()) return null;
|
|
350
344
|
const from = formatRelativeInstant(range.from, now, locale.value);
|
|
351
345
|
return t("dms.period.since", { from }, `since ${from}`);
|
|
@@ -421,7 +415,9 @@ function comparisonKeyClass(active: boolean): string {
|
|
|
421
415
|
}
|
|
422
416
|
|
|
423
417
|
function compareRangeText(compare: PeriodRange, main: PeriodRange): string {
|
|
424
|
-
|
|
418
|
+
// In the reader's calendar, on the server as in the browser.
|
|
419
|
+
const clock = readerClock();
|
|
420
|
+
const sameYear = clock.wall(compare.to).year === clock.wall(main.to).year;
|
|
425
421
|
return formatPeriodRange(compare, locale.value, !sameYear);
|
|
426
422
|
}
|
|
427
423
|
|
|
@@ -47,7 +47,8 @@ const { data } = await useDmsAsyncData(
|
|
|
47
47
|
params: { limit: CASCADER_FETCH_LIMIT },
|
|
48
48
|
})
|
|
49
49
|
: Promise.resolve(undefined),
|
|
50
|
-
|
|
50
|
+
// The labels of a path, not the page's data.
|
|
51
|
+
{ lazy: true, refreshOnPageRefresh: false },
|
|
51
52
|
);
|
|
52
53
|
|
|
53
54
|
const pathOf = (item: unknown): string[] =>
|