@allbluecn/web-app 0.22.4 → 0.23.1
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/package.json +4 -14
- package/src/__tests__/integration/login-email-acceptance.test.ts +16 -0
- package/src/__tests__/story-view.test.ts +33 -1
- package/src/components/agents/__tests__/agent-bindings-tab.test.tsx +221 -0
- package/src/components/agents/__tests__/agents-page.test.tsx +284 -0
- package/src/components/agents/agent-bindings-tab.tsx +59 -45
- package/src/components/agents/agent-form-dialog.tsx +3 -3
- package/src/components/agents/agent-selector.tsx +2 -3
- package/src/components/agents/agents-page.tsx +66 -31
- package/src/components/agents/slash-skill-menu.tsx +2 -3
- package/src/components/auth/__tests__/session-guard.test.tsx +74 -0
- package/src/components/auth/session-guard.tsx +8 -0
- package/src/components/billing/login-prompt-dialog.tsx +1 -1
- package/src/components/billing/subscription-provider.tsx +2 -16
- package/src/components/bug/__tests__/peek-modes.test.tsx +8 -0
- package/src/components/bug/__tests__/subgroup-header.test.tsx +202 -40
- package/src/components/bug/peek-panel.tsx +5 -3
- package/src/components/bug/views/bug-list/group.tsx +12 -2
- package/src/components/bug/views/bug-list/index.tsx +94 -67
- package/src/components/command/command-palette.tsx +10 -9
- package/src/components/design/canvas/document/frame-custom-presets.ts +9 -1
- package/src/components/design/canvas/panels/toolbar.tsx +21 -22
- package/src/components/design/canvas/react/__tests__/canvas-context-load-echo.test.tsx +199 -0
- package/src/components/design/canvas/react/__tests__/hooks.test.tsx +17 -3
- package/src/components/design/canvas/react/canvas-context.tsx +47 -26
- package/src/components/design/canvas/react/hooks.ts +39 -34
- package/src/components/design/editor/layout/__tests__/collab-param-wiring.test.tsx +17 -0
- package/src/components/design/editor/layout/__tests__/context-menu.test.tsx +17 -1
- package/src/components/design/editor/layout/__tests__/no-page-save-toast.test.tsx +17 -0
- package/src/components/design/editor/layout/__tests__/persistence-restore.test.tsx +17 -1
- package/src/components/design/editor/layout/__tests__/ssr-localstorage-smoke.test.tsx +17 -1
- package/src/components/design/editor/layout/design-editor-layout.tsx +13 -2
- package/src/components/design/editor/layout/hooks/use-design-auto-save.ts +3 -19
- package/src/components/devtools/devtools-panel.tsx +56 -0
- package/src/components/forms/__tests__/create-app-form.test.tsx +131 -0
- package/src/components/forms/app-field-error.tsx +21 -1
- package/src/components/forms/app-field.tsx +8 -8
- package/src/components/forms/app-submit.tsx +12 -9
- package/src/components/forms/create-app-form.tsx +124 -0
- package/src/components/forms/index.ts +2 -3
- package/src/components/harness/__tests__/gallery-behavior.test.tsx +37 -1
- package/src/components/harness/__tests__/harness-workbench-first-send.test.tsx +4 -1
- package/src/components/harness/__tests__/message-actions.test.tsx +7 -6
- package/src/components/harness/__tests__/use-video-generation.test.tsx +165 -0
- package/src/components/harness/ai-chat-history/index.tsx +5 -3
- package/src/components/harness/gallery/gallery-card.tsx +57 -35
- package/src/components/harness/gallery/gallery-grid.tsx +32 -15
- package/src/components/harness/gallery/gallery-types.ts +19 -0
- package/src/components/harness/gallery/gallery-video-card.tsx +54 -0
- package/src/components/harness/gallery/types.ts +2 -0
- package/src/components/harness/gallery/use-video-generation.ts +95 -0
- package/src/components/harness/harness-workbench-page.tsx +2 -3
- package/src/components/harness/message-actions.tsx +5 -3
- package/src/components/hotkeys/shortcuts-panel.tsx +28 -13
- package/src/components/iteration/__tests__/burndown-chart.test.tsx +118 -0
- package/src/components/iteration/burndown-chart.tsx +54 -23
- package/src/components/knowledge/editor/__tests__/knowledge-doc-editor-collab.test.tsx +96 -0
- package/src/components/knowledge/editor/knowledge-doc-editor.tsx +56 -1
- package/src/components/knowledge/editor/mindmap-doc-editor.tsx +12 -0
- package/src/components/knowledge/editor/use-auto-save-channels.ts +3 -1
- package/src/components/layout/sidebar-03/__tests__/nav-workspace.test.tsx +0 -4
- package/src/components/layout/sidebar-03/nav-knowledge.tsx +11 -11
- package/src/components/layout/sidebar-03/nav-main.tsx +12 -15
- package/src/components/layout/sidebar-03/nav-notifications.tsx +5 -8
- package/src/components/layout/sidebar-03/nav-workspace.tsx +11 -9
- package/src/components/notifications/__tests__/settings-notifications.test.tsx +158 -0
- package/src/components/notifications/settings-notifications.tsx +37 -13
- package/src/components/notifications/team-notifications.tsx +10 -14
- package/src/components/settings/personal-team/account/__tests__/account-client.test.tsx +170 -0
- package/src/components/settings/personal-team/account/account-client.tsx +44 -7
- package/src/components/settings/personal-team/team/team-client.tsx +17 -19
- package/src/components/shared-kit/list-kit/__tests__/frozen-header.test.ts +210 -41
- package/src/components/shared-kit/list-kit/frozen-header.ts +134 -21
- package/src/components/shared-kit/list-kit/kanban/frozen-header.ts +20 -27
- package/src/components/story/__tests__/peek-panel-patch-sync.test.tsx +8 -0
- package/src/components/story/__tests__/story-list-table.test.tsx +214 -0
- package/src/components/story/columns.tsx +11 -2
- package/src/components/story/peek-panel.tsx +4 -2
- package/src/components/story/story-list-table.tsx +16 -1
- package/src/components/story/views/story-list/__tests__/frozen-headers.test.tsx +186 -0
- package/src/components/story/views/story-list/group.tsx +12 -1
- package/src/components/story/views/story-list/index.tsx +101 -68
- package/src/components/testcase/__tests__/case-row.test.tsx +134 -2
- package/src/components/testcase/__tests__/peek-panel.test.tsx +8 -0
- package/src/components/testcase/__tests__/step-editor.test.tsx +106 -0
- package/src/components/testcase/list/grouped-list.tsx +50 -22
- package/src/components/testcase/peek-panel.tsx +7 -4
- package/src/components/testcase/step-editor.tsx +196 -150
- package/src/components/tiptap/notion-like/editor-extensions.ts +0 -3
- package/src/components/tiptap/notion-like/notion-like-editor-header.tsx +1 -31
- package/src/components/tiptap/notion-like/notion-like-editor.tsx +32 -1
- package/src/components/workspace/__tests__/dialogs/create-workspace-dialog.test.tsx +2 -28
- package/src/components/workspace/__tests__/dialogs/edit-workspace-dialog.test.tsx +2 -28
- package/src/components/workspace/dialogs/create-workspace-dialog.tsx +45 -43
- package/src/components/workspace/dialogs/edit-workspace-dialog.tsx +38 -36
- package/src/design-engine/engine/engine-decor.ts +2 -0
- package/src/design-engine/engine/engine-path.ts +2 -0
- package/src/hooks/__tests__/use-unsaved-guard.test.tsx +180 -0
- package/src/hooks/use-notifications.ts +9 -16
- package/src/hooks/use-unsaved-guard.tsx +87 -0
- package/src/lib/__tests__/pacer.test.ts +33 -96
- package/src/lib/__tests__/query-defaults.test.ts +169 -0
- package/src/lib/__tests__/query-global-error.test.ts +256 -0
- package/src/lib/anyhub/anyhub-store.ts +2 -2
- package/src/lib/changelog/sdk-versions.ts +2 -10
- package/src/lib/collab/presence-store.ts +5 -5
- package/src/lib/collab/use-presence-list-refresh.ts +2 -2
- package/src/lib/collab/use-share-presence.ts +2 -2
- package/src/lib/collections/query-keys.ts +7 -2
- package/src/lib/commands/__tests__/command-palette-state.test.ts +12 -14
- package/src/lib/commands/__tests__/registry.test.ts +6 -3
- package/src/lib/commands/command-palette-state.ts +8 -31
- package/src/lib/hotkeys/__tests__/registry.test.ts +71 -18
- package/src/lib/hotkeys/registry.ts +188 -85
- package/src/lib/hotkeys/types.ts +1 -13
- package/src/lib/pacer.ts +1 -79
- package/src/lib/queries/README.md +67 -0
- package/src/lib/queries/__tests__/agents-queries.test.ts +101 -0
- package/src/lib/queries/__tests__/compliance-queries.test.ts +136 -0
- package/src/lib/queries/__tests__/notifications-queries.test.ts +175 -0
- package/src/lib/queries/__tests__/team-queries.test.ts +87 -0
- package/src/lib/queries/agents.ts +40 -0
- package/src/lib/queries/compliance.ts +69 -0
- package/src/lib/queries/notifications.ts +63 -0
- package/src/lib/queries/team.ts +39 -0
- package/src/lib/query-defaults.ts +8 -0
- package/src/lib/query-global-error.ts +128 -0
- package/src/lib/remote-auth-client.ts +16 -0
- package/src/paraglide/README.md +1 -1
- package/src/paraglide/messages/_index.d.ts +11 -0
- package/src/paraglide/messages/_index.js +5659 -88285
- package/src/paraglide/messages/common_error_generic.d.ts +16 -0
- package/src/paraglide/messages/common_error_generic.js +29 -0
- package/src/paraglide/messages/common_unsaved_body.d.ts +16 -0
- package/src/paraglide/messages/common_unsaved_body.js +29 -0
- package/src/paraglide/messages/common_unsaved_leave.d.ts +16 -0
- package/src/paraglide/messages/common_unsaved_leave.js +29 -0
- package/src/paraglide/messages/common_unsaved_stay.d.ts +16 -0
- package/src/paraglide/messages/common_unsaved_stay.js +29 -0
- package/src/paraglide/messages/common_unsaved_title.d.ts +16 -0
- package/src/paraglide/messages/common_unsaved_title.js +29 -0
- package/src/paraglide/messages/gallery_videofailed1.d.ts +17 -0
- package/src/paraglide/messages/gallery_videofailed1.js +30 -0
- package/src/paraglide/messages/gallery_videogenerating1.d.ts +17 -0
- package/src/paraglide/messages/gallery_videogenerating1.js +30 -0
- package/src/paraglide/messages/gallery_videomissingprompt2.d.ts +17 -0
- package/src/paraglide/messages/gallery_videomissingprompt2.js +30 -0
- package/src/paraglide/messages/gallery_videoprogress1.d.ts +19 -0
- package/src/paraglide/messages/gallery_videoprogress1.js +30 -0
- package/src/paraglide/messages/gallery_videostop1.d.ts +17 -0
- package/src/paraglide/messages/gallery_videostop1.js +30 -0
- package/src/paraglide/messages/story_table_resizecolumn1.d.ts +17 -0
- package/src/paraglide/messages/story_table_resizecolumn1.js +30 -0
- package/src/providers/index.tsx +5 -50
- package/src/routeTree.gen.ts +21 -0
- package/src/router.tsx +21 -8
- package/src/routes/__root.tsx +2 -1
- package/src/routes/__tests__/-edition-routing.test.tsx +6 -1
- package/src/routes/_login/forgot-password/route.lazy.tsx +7 -6
- package/src/routes/_login/login/route.lazy.tsx +10 -9
- package/src/routes/_login/register/route.lazy.tsx +19 -18
- package/src/routes/_login/reset-password/route.lazy.tsx +10 -9
- package/src/routes/_nav/knowledge/compliance/$id.lazy.tsx +9 -12
- package/src/routes/_nav/knowledge/compliance/index.lazy.tsx +16 -13
- package/src/routes/_nav/knowledge/laws/$id.lazy.tsx +1 -1
- package/src/routes/_nav/notifications/route.tsx +6 -16
- package/src/routes/_nav/settings/-settings-tabs.tsx +9 -12
- package/src/routes/api/generate/__tests__/media.test.ts +378 -0
- package/src/routes/api/generate/media.ts +179 -0
- package/src/server/__tests__/agent-client-graph.test.ts +0 -1
- package/src/server/__tests__/agent-crud.test.ts +37 -4
- package/src/server/__tests__/bug-groups-two-level.test.ts +34 -1
- package/src/server/__tests__/chat-conversations.test.ts +37 -4
- package/src/server/__tests__/chat-list-messages.test.ts +40 -7
- package/src/server/__tests__/csv-export.test.ts +36 -3
- package/src/server/__tests__/desktop-bootstrap.test.ts +189 -0
- package/src/server/__tests__/remote-auth.test.ts +72 -0
- package/src/server/__tests__/skill-crud.test.ts +37 -4
- package/src/server/__tests__/sso-domain.test.ts +1 -1
- package/src/server/__tests__/sso-rate-limit-singleton.test.ts +123 -0
- package/src/server/__tests__/task-serverfn-crud.test.ts +35 -2
- package/src/server/__tests__/task-serverfn-queries.test.ts +35 -2
- package/src/server/__tests__/testcase-board.test.ts +38 -5
- package/src/server/__tests__/testcase-bulk.test.ts +38 -5
- package/src/server/__tests__/testcase-list-scope.test.ts +38 -5
- package/src/server/ai-gateway/__tests__/ai-output-guard.test.ts +126 -2
- package/src/server/ai-gateway/__tests__/chat-stream-builder-code-mode.test.ts +4 -0
- package/src/server/ai-gateway/__tests__/chat-stream-builder-memory.test.ts +4 -0
- package/src/server/ai-gateway/__tests__/chat-stream-builder-sandbox.test.ts +4 -0
- package/src/server/ai-gateway/__tests__/chat-stream-builder-skills.test.ts +4 -0
- package/src/server/ai-gateway/__tests__/pgvector-memory-adapter.test.ts +30 -0
- package/src/server/ai-gateway/__tests__/pgvector-memory-conformance.test.ts +92 -0
- package/src/server/ai-gateway/__tests__/source-mode.test.ts +49 -8
- package/src/server/ai-gateway/ai-output-guard.ts +50 -2
- package/src/server/ai-gateway/chat-stream-builder.ts +3 -0
- package/src/server/ai-gateway/media/index.ts +36 -29
- package/src/server/ai-gateway/media/trim-input.ts +52 -0
- package/src/server/ai-gateway/memory/pgvector-adapter.ts +27 -13
- package/src/server/ai-gateway/sandbox/__tests__/harness-sandbox.test.ts +252 -41
- package/src/server/ai-gateway/sandbox/harness-sandbox.ts +6 -67
- package/src/server/ai-gateway/source-mode.ts +24 -8
- package/src/server/desktop-bootstrap.ts +74 -0
- package/src/server/lib/__tests__/rate-limit.test.ts +0 -1
- package/src/server/lib/rate-limit.ts +0 -2
- package/src/server/middlewares/__tests__/auth-context.test.ts +156 -0
- package/src/server/middlewares/auth-context.ts +62 -0
- package/src/server/remote-auth.ts +58 -31
- package/src/server/serverFns/__tests__/ai-hosted-models.test.ts +47 -15
- package/src/server/serverFns/__tests__/ai-source-mode.test.ts +45 -15
- package/src/server/serverFns/__tests__/billing.test.ts +37 -2
- package/src/server/serverFns/__tests__/collab-snapshot.test.ts +41 -14
- package/src/server/serverFns/__tests__/collab.test.ts +41 -14
- package/src/server/serverFns/__tests__/compliance.test.ts +33 -1
- package/src/server/serverFns/__tests__/email-verification.test.ts +35 -1
- package/src/server/serverFns/__tests__/knowledge-breadcrumb.test.ts +46 -6
- package/src/server/serverFns/__tests__/knowledge-delete.test.ts +42 -14
- package/src/server/serverFns/__tests__/knowledge-kb-projects.test.ts +46 -6
- package/src/server/serverFns/__tests__/knowledge-public-compliance.test.ts +36 -2
- package/src/server/serverFns/__tests__/knowledge-readable-kb.test.ts +42 -10
- package/src/server/serverFns/__tests__/knowledge-search.test.ts +45 -9
- package/src/server/serverFns/__tests__/legal-code-canonical.test.ts +36 -2
- package/src/server/serverFns/__tests__/link-preview.test.ts +38 -4
- package/src/server/serverFns/__tests__/notifications-list.test.ts +36 -3
- package/src/server/serverFns/__tests__/ownership-merge.test.ts +38 -4
- package/src/server/serverFns/__tests__/project-delete-transaction.test.ts +39 -2
- package/src/server/serverFns/__tests__/project-kb.test.ts +34 -1
- package/src/server/serverFns/__tests__/project-member-idor.test.ts +34 -1
- package/src/server/serverFns/__tests__/project-team-assign.test.ts +34 -1
- package/src/server/serverFns/__tests__/read-write-merge.test.ts +38 -4
- package/src/server/serverFns/__tests__/story-comments.test.ts +38 -4
- package/src/server/serverFns/__tests__/story-crud-archive-detail.test.ts +38 -4
- package/src/server/serverFns/__tests__/story-crud-labels-users.test.ts +37 -3
- package/src/server/serverFns/__tests__/story-crud.test.ts +36 -3
- package/src/server/serverFns/__tests__/story-links.test.ts +38 -4
- package/src/server/serverFns/__tests__/story-reactions.test.ts +38 -4
- package/src/server/serverFns/__tests__/story-tree.test.ts +38 -4
- package/src/server/serverFns/__tests__/team-accept.test.ts +34 -1
- package/src/server/serverFns/__tests__/team-invitation.test.ts +34 -1
- package/src/server/serverFns/__tests__/team-join-request.test.ts +34 -1
- package/src/server/serverFns/__tests__/team-members.test.ts +34 -1
- package/src/server/serverFns/__tests__/team-ownership-merge.test.ts +34 -1
- package/src/server/serverFns/__tests__/update-bug-guard.test.ts +37 -3
- package/src/server/serverFns/__tests__/update-document-guard.test.ts +38 -4
- package/src/server/serverFns/__tests__/update-page-guard.test.ts +40 -4
- package/src/server/serverFns/__tests__/update-seats.test.ts +33 -1
- package/src/server/serverFns/__tests__/update-story-guard.test.ts +36 -3
- package/src/server/serverFns/__tests__/update-testcase-guard.test.ts +37 -3
- package/src/server/serverFns/__tests__/workspace-kb-idor.test.ts +36 -2
- package/src/server/serverFns/__tests__/workspace-kb.test.ts +36 -2
- package/src/server/serverFns/__tests__/workspace.test.ts +34 -1
- package/src/server/serverFns/activity.ts +10 -14
- package/src/server/serverFns/agents/agent-crud.ts +16 -15
- package/src/server/serverFns/agents/bindings.ts +12 -13
- package/src/server/serverFns/agents/skill-crud.ts +16 -15
- package/src/server/serverFns/ai/code-mode-readiness.ts +3 -4
- package/src/server/serverFns/ai/search-capability.ts +3 -4
- package/src/server/serverFns/anyhub/app.ts +10 -14
- package/src/server/serverFns/anyhub/index.ts +7 -10
- package/src/server/serverFns/anyhub/install.ts +4 -4
- package/src/server/serverFns/anyhub/permissions.ts +10 -14
- package/src/server/serverFns/anyhub/proxy.ts +4 -6
- package/src/server/serverFns/auth/email-verification.ts +4 -5
- package/src/server/serverFns/auth/session.ts +9 -0
- package/src/server/serverFns/billing.ts +18 -28
- package/src/server/serverFns/blob-upload.ts +6 -9
- package/src/server/serverFns/bug/__tests__/bug-access-tokens.test.ts +34 -1
- package/src/server/serverFns/bug/__tests__/bug-activities.test.ts +34 -1
- package/src/server/serverFns/bug/__tests__/bug-comments.test.ts +34 -1
- package/src/server/serverFns/bug/__tests__/bug-crud.test.ts +34 -1
- package/src/server/serverFns/bug/__tests__/bug-fix.test.ts +34 -1
- package/src/server/serverFns/bug/__tests__/bug-groups.test.ts +34 -1
- package/src/server/serverFns/bug/__tests__/bug-reactions.test.ts +34 -1
- package/src/server/serverFns/bug/__tests__/bug-reorder.test.ts +39 -5
- package/src/server/serverFns/bug/__tests__/bug-repro.test.ts +34 -1
- package/src/server/serverFns/bug/__tests__/bug-story.test.ts +34 -1
- package/src/server/serverFns/bug/__tests__/bug-webhooks.test.ts +34 -1
- package/src/server/serverFns/bug/bug-access-tokens.ts +16 -22
- package/src/server/serverFns/bug/bug-activities.ts +4 -6
- package/src/server/serverFns/bug/bug-collect-tokens.ts +13 -18
- package/src/server/serverFns/bug/bug-comments.ts +13 -18
- package/src/server/serverFns/bug/bug-crud/detail.ts +10 -14
- package/src/server/serverFns/bug/bug-crud.ts +28 -38
- package/src/server/serverFns/bug/bug-display-options.ts +8 -11
- package/src/server/serverFns/bug/bug-export.ts +5 -7
- package/src/server/serverFns/bug/bug-fix.ts +10 -14
- package/src/server/serverFns/bug/bug-groups.ts +10 -14
- package/src/server/serverFns/bug/bug-reactions.ts +10 -14
- package/src/server/serverFns/bug/bug-reorder.ts +4 -6
- package/src/server/serverFns/bug/bug-repro.ts +8 -11
- package/src/server/serverFns/bug/bug-story-bulk.ts +13 -18
- package/src/server/serverFns/bug/bug-story.ts +10 -14
- package/src/server/serverFns/bug/bug-views.ts +12 -18
- package/src/server/serverFns/bug/bug-webhooks.ts +13 -18
- package/src/server/serverFns/bug/test-bug-confirm.ts +7 -10
- package/src/server/serverFns/changelog/__tests__/fetch.test.ts +40 -1
- package/src/server/serverFns/changelog/fetch.ts +4 -6
- package/src/server/serverFns/chat/conversations.ts +17 -28
- package/src/server/serverFns/chat/list-messages.ts +4 -7
- package/src/server/serverFns/collab-presence.ts +11 -13
- package/src/server/serverFns/collab-snapshot.ts +6 -7
- package/src/server/serverFns/collab.ts +7 -7
- package/src/server/serverFns/compliance.ts +8 -8
- package/src/server/serverFns/design/__tests__/comments.test.ts +54 -10
- package/src/server/serverFns/design/__tests__/designs.test.ts +39 -16
- package/src/server/serverFns/design/__tests__/page-versions.test.ts +52 -14
- package/src/server/serverFns/design/__tests__/shares-password.test.ts +44 -14
- package/src/server/serverFns/design/__tests__/shares.test.ts +44 -14
- package/src/server/serverFns/design/__tests__/versions.test.ts +44 -14
- package/src/server/serverFns/design/ai/__tests__/agent-conversations.test.ts +50 -21
- package/src/server/serverFns/design/ai/__tests__/providers.test.ts +42 -8
- package/src/server/serverFns/design/ai/agent-conversations.ts +19 -26
- package/src/server/serverFns/design/ai/fill-annotation.ts +5 -7
- package/src/server/serverFns/design/ai/generate-prototype.ts +5 -7
- package/src/server/serverFns/design/ai/hosted-models.ts +4 -5
- package/src/server/serverFns/design/ai/providers.ts +30 -39
- package/src/server/serverFns/design/ai/source-mode.ts +10 -12
- package/src/server/serverFns/design/comments.ts +14 -13
- package/src/server/serverFns/design/designs.ts +13 -19
- package/src/server/serverFns/design/page-versions.ts +17 -18
- package/src/server/serverFns/design/pages.ts +17 -18
- package/src/server/serverFns/design/reviews.ts +8 -9
- package/src/server/serverFns/design/shares-password.ts +13 -19
- package/src/server/serverFns/design/shares.ts +22 -31
- package/src/server/serverFns/design/versions.ts +8 -9
- package/src/server/serverFns/harness/__tests__/attach-prototype.spec.ts +52 -2
- package/src/server/serverFns/harness/__tests__/attach-to-kb.spec.ts +52 -2
- package/src/server/serverFns/harness/__tests__/attach-to-story-ssrf.spec.ts +52 -2
- package/src/server/serverFns/harness/__tests__/attach-to-story.spec.ts +52 -2
- package/src/server/serverFns/harness/__tests__/cancel-run.test.ts +40 -15
- package/src/server/serverFns/harness/__tests__/extract-attachment.spec.ts +52 -2
- package/src/server/serverFns/harness/__tests__/gallery.spec.ts +76 -12
- package/src/server/serverFns/harness/__tests__/github-workspace.spec.ts +38 -6
- package/src/server/serverFns/harness/__tests__/media-prototype.spec.ts +50 -6
- package/src/server/serverFns/harness/__tests__/media.spec.ts +55 -11
- package/src/server/serverFns/harness/__tests__/sandbox-policy.test.ts +41 -16
- package/src/server/serverFns/harness/__tests__/workspace-files.test.ts +41 -16
- package/src/server/serverFns/harness/attach-prototype.ts +4 -4
- package/src/server/serverFns/harness/attach-to-kb.ts +4 -4
- package/src/server/serverFns/harness/attach-to-story.ts +4 -4
- package/src/server/serverFns/harness/cancel-run.ts +4 -6
- package/src/server/serverFns/harness/extract-attachment.ts +3 -4
- package/src/server/serverFns/harness/gallery.ts +19 -23
- package/src/server/serverFns/harness/github-workspace.ts +14 -21
- package/src/server/serverFns/harness/media.ts +18 -117
- package/src/server/serverFns/harness/sandbox-policy.ts +11 -10
- package/src/server/serverFns/harness/search-mention-candidates.ts +4 -6
- package/src/server/serverFns/harness/workspace-files.ts +19 -26
- package/src/server/serverFns/iteration/__tests__/iteration-auto-schedule.test.ts +35 -3
- package/src/server/serverFns/iteration/__tests__/iteration-crud.test.ts +35 -3
- package/src/server/serverFns/iteration/__tests__/iteration-stories.test.ts +35 -3
- package/src/server/serverFns/iteration/__tests__/iteration-transfer.test.ts +35 -3
- package/src/server/serverFns/iteration/iteration-auto-schedule.ts +7 -10
- package/src/server/serverFns/iteration/iteration-burndown.ts +4 -6
- package/src/server/serverFns/iteration/iteration-crud.ts +26 -35
- package/src/server/serverFns/iteration/iteration-stories.ts +10 -14
- package/src/server/serverFns/iteration/iteration-transfer.ts +4 -6
- package/src/server/serverFns/knowledge/public/bookmarks/categories.ts +11 -15
- package/src/server/serverFns/knowledge/public/bookmarks/create.ts +4 -6
- package/src/server/serverFns/knowledge/public/bookmarks/delete.ts +5 -7
- package/src/server/serverFns/knowledge/public/bookmarks/detail.ts +4 -6
- package/src/server/serverFns/knowledge/public/bookmarks/favorite.ts +4 -6
- package/src/server/serverFns/knowledge/public/bookmarks/list.ts +4 -6
- package/src/server/serverFns/knowledge/public/bookmarks/refresh-favicon.ts +4 -6
- package/src/server/serverFns/knowledge/public/bookmarks/share.ts +4 -6
- package/src/server/serverFns/knowledge/public/bookmarks/sync-github.ts +5 -7
- package/src/server/serverFns/knowledge/public/bookmarks/unshare.ts +4 -6
- package/src/server/serverFns/knowledge/public/bookmarks/update.ts +6 -8
- package/src/server/serverFns/knowledge/public/compliance/create.ts +4 -6
- package/src/server/serverFns/knowledge/public/compliance/delete.ts +5 -7
- package/src/server/serverFns/knowledge/public/compliance/detail.ts +4 -6
- package/src/server/serverFns/knowledge/public/compliance/list.ts +4 -6
- package/src/server/serverFns/knowledge/public/compliance/share.ts +4 -6
- package/src/server/serverFns/knowledge/public/compliance/unshare.ts +4 -6
- package/src/server/serverFns/knowledge/public/compliance/update.ts +5 -7
- package/src/server/serverFns/knowledge/public/dictionary/create.ts +4 -6
- package/src/server/serverFns/knowledge/public/dictionary/delete.ts +5 -7
- package/src/server/serverFns/knowledge/public/dictionary/detail.ts +4 -6
- package/src/server/serverFns/knowledge/public/dictionary/list.ts +4 -6
- package/src/server/serverFns/knowledge/public/dictionary/share.ts +4 -6
- package/src/server/serverFns/knowledge/public/dictionary/unshare.ts +4 -6
- package/src/server/serverFns/knowledge/public/dictionary/update.ts +5 -7
- package/src/server/serverFns/knowledge/public/entity-tags.ts +4 -6
- package/src/server/serverFns/knowledge/public/favorite.ts +7 -10
- package/src/server/serverFns/knowledge/public/legal/create.ts +4 -6
- package/src/server/serverFns/knowledge/public/legal/delete.ts +5 -7
- package/src/server/serverFns/knowledge/public/legal/detail.ts +4 -6
- package/src/server/serverFns/knowledge/public/legal/list.ts +4 -6
- package/src/server/serverFns/knowledge/public/legal/share.ts +4 -6
- package/src/server/serverFns/knowledge/public/legal/unshare.ts +4 -6
- package/src/server/serverFns/knowledge/public/legal/update.ts +5 -7
- package/src/server/serverFns/knowledge-index.ts +12 -16
- package/src/server/serverFns/knowledge.ts +50 -67
- package/src/server/serverFns/notifications.ts +25 -34
- package/src/server/serverFns/plugins/harness-plugins.ts +16 -23
- package/src/server/serverFns/project/module-overview.ts +7 -10
- package/src/server/serverFns/project.ts +48 -68
- package/src/server/serverFns/references/create.ts +4 -6
- package/src/server/serverFns/references/resolve.ts +4 -6
- package/src/server/serverFns/references/search.ts +4 -6
- package/src/server/serverFns/share/design/[designId]/events.ts +4 -6
- package/src/server/serverFns/sso.ts +1 -1
- package/src/server/serverFns/story/__tests__/bulk.test.ts +34 -1
- package/src/server/serverFns/story/__tests__/story-assoc.test.ts +34 -1
- package/src/server/serverFns/story/__tests__/story-attachments.test.ts +34 -1
- package/src/server/serverFns/story/__tests__/story-crud-date-key.test.ts +42 -17
- package/src/server/serverFns/story/__tests__/story-dependencies.test.ts +34 -1
- package/src/server/serverFns/story/__tests__/story-gantt-deps.test.ts +34 -1
- package/src/server/serverFns/story/__tests__/story-groups.test.ts +34 -1
- package/src/server/serverFns/story/__tests__/story-read-path-authz.test.ts +40 -4
- package/src/server/serverFns/story/__tests__/story-search.test.ts +34 -1
- package/src/server/serverFns/story/__tests__/story-transfer.test.ts +34 -1
- package/src/server/serverFns/story/link-preview.ts +5 -7
- package/src/server/serverFns/story/story-assoc.ts +28 -38
- package/src/server/serverFns/story/story-attachments.ts +7 -10
- package/src/server/serverFns/story/story-children.ts +4 -6
- package/src/server/serverFns/story/story-comments.ts +13 -18
- package/src/server/serverFns/story/story-crud/activities.ts +4 -6
- package/src/server/serverFns/story/story-crud/archive.ts +7 -10
- package/src/server/serverFns/story/story-crud/bulk.ts +10 -14
- package/src/server/serverFns/story/story-crud/detail.ts +7 -10
- package/src/server/serverFns/story/story-crud/label-suggest.ts +4 -6
- package/src/server/serverFns/story/story-crud/labels.ts +6 -9
- package/src/server/serverFns/story/story-crud/user-brief.ts +4 -6
- package/src/server/serverFns/story/story-crud.ts +13 -18
- package/src/server/serverFns/story/story-dependencies.ts +16 -22
- package/src/server/serverFns/story/story-groups.ts +10 -14
- package/src/server/serverFns/story/story-links.ts +13 -18
- package/src/server/serverFns/story/story-reactions.ts +13 -18
- package/src/server/serverFns/story/story-search.ts +7 -10
- package/src/server/serverFns/story/story-transfer.ts +10 -14
- package/src/server/serverFns/story/story-tree.ts +7 -10
- package/src/server/serverFns/story-display-options.ts +7 -8
- package/src/server/serverFns/story-view.ts +12 -18
- package/src/server/serverFns/tasks/task-crud.ts +13 -17
- package/src/server/serverFns/tasks/task-queries.ts +12 -18
- package/src/server/serverFns/team.ts +46 -66
- package/src/server/serverFns/testcase/__tests__/automation.test.ts +38 -5
- package/src/server/serverFns/testcase/__tests__/evidence-upload.test.ts +37 -3
- package/src/server/serverFns/testcase/__tests__/generation.test.ts +38 -5
- package/src/server/serverFns/testcase/__tests__/plans.test.ts +36 -3
- package/src/server/serverFns/testcase/__tests__/regression.test.ts +36 -3
- package/src/server/serverFns/testcase/__tests__/run-cases.test.ts +36 -3
- package/src/server/serverFns/testcase/__tests__/runs.test.ts +36 -3
- package/src/server/serverFns/testcase/__tests__/stats.test.ts +36 -3
- package/src/server/serverFns/testcase/__tests__/testcase-board.test.ts +39 -5
- package/src/server/serverFns/testcase/__tests__/testcase-crud.test.ts +36 -3
- package/src/server/serverFns/testcase/__tests__/testcase-folders.test.ts +36 -3
- package/src/server/serverFns/testcase/__tests__/testcase-groups.test.ts +38 -5
- package/src/server/serverFns/testcase/automation.ts +13 -17
- package/src/server/serverFns/testcase/bulk.ts +10 -14
- package/src/server/serverFns/testcase/evidence-upload.ts +5 -7
- package/src/server/serverFns/testcase/generation.ts +10 -13
- package/src/server/serverFns/testcase/plans.ts +23 -31
- package/src/server/serverFns/testcase/regression.ts +7 -10
- package/src/server/serverFns/testcase/run-cases.ts +13 -18
- package/src/server/serverFns/testcase/runs.ts +13 -18
- package/src/server/serverFns/testcase/stats.ts +13 -18
- package/src/server/serverFns/testcase/testcase-activities.ts +4 -6
- package/src/server/serverFns/testcase/testcase-board.ts +4 -6
- package/src/server/serverFns/testcase/testcase-crud.ts +20 -27
- package/src/server/serverFns/testcase/testcase-display-options.ts +8 -11
- package/src/server/serverFns/testcase/testcase-export.ts +5 -7
- package/src/server/serverFns/testcase/testcase-filter-views.ts +12 -18
- package/src/server/serverFns/testcase/testcase-folders.ts +16 -22
- package/src/server/serverFns/testcase/testcase-groups.ts +4 -6
- package/src/server/serverFns/testcase/testcase-reorder.ts +4 -6
- package/src/server/serverFns/translation.ts +4 -6
- package/src/server/serverFns/user/avatar.ts +4 -6
- package/src/server/serverFns/user/settings.ts +4 -6
- package/src/server/serverFns/user/update-locale.ts +4 -6
- package/src/server/serverFns/workspace-kb.ts +26 -35
- package/src/server/serverFns/workspace.ts +43 -59
- package/src/server/serverFns/ws.ts +4 -6
- package/src/server/serverfn-public-allowlist.ts +117 -0
- package/src/server/story/types.ts +0 -1
- package/src/server/sync/__tests__/scopes.test.ts +64 -0
- package/src/server/sync/scopes.ts +7 -0
- package/src/server/testcase/__tests__/auto-bug.test.ts +44 -12
- package/src/__tests__/bench/baselines/format-timestamp.json +0 -44
- package/src/__typeprobe__.d.ts +0 -4
- package/src/components/forms/app-form-dialog.tsx +0 -126
- package/src/components/forms/hooks/use-form-submit.ts +0 -57
- package/src/components/forms/types.ts +0 -34
- package/src/components/harness/code/workspace-picker.tsx +0 -239
- package/src/components/tiptap/extensions/local-file/insert-local-file.ts +0 -35
- package/src/components/tiptap/extensions/local-file/local-file-node.ts +0 -71
- package/src/devframe-docks/ai-chat-iframe.tsx +0 -29
- package/src/devframe-docks/session-iframe.tsx +0 -90
- package/src/lib/__tests__/desktop-detect.test.ts +0 -19
- package/src/lib/collections/index.ts +0 -22
- package/src/lib/collections/knowledge/index.ts +0 -18
- package/src/lib/collections/knowledge/knowledge-bases-collection.ts +0 -34
- package/src/lib/collections/reference/bookmark-categories-collection.ts +0 -31
- package/src/lib/collections/reference/labels-collection.ts +0 -31
- package/src/lib/collections/reference/projects-collection.ts +0 -31
- package/src/lib/collections/reference/users-brief-collection.ts +0 -35
- package/src/lib/collections/reference/workspaces-collection.ts +0 -34
- package/src/lib/desktop-detect.ts +0 -29
- package/src/paraglide/messages/en.js +0 -28260
- package/src/paraglide/messages/zh.js +0 -28258
|
@@ -15,9 +15,8 @@
|
|
|
15
15
|
// You should have received a copy of the GNU Affero General Public License
|
|
16
16
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
17
|
|
|
18
|
-
import { useContext
|
|
18
|
+
import { useContext } from "react"
|
|
19
19
|
import { EditorContext } from "@tiptap/react"
|
|
20
|
-
import { FolderOpen } from "lucide-react"
|
|
21
20
|
|
|
22
21
|
// --- Tiptap UI ---
|
|
23
22
|
import { UndoRedoButton } from "../ui/undo-redo-button/index.tsx"
|
|
@@ -26,27 +25,11 @@ import { UndoRedoButton } from "../ui/undo-redo-button/index.tsx"
|
|
|
26
25
|
import { Spacer } from "../ui-primitive/spacer/index.tsx"
|
|
27
26
|
import { ButtonGroup } from "../ui-primitive/button-group/index.tsx"
|
|
28
27
|
|
|
29
|
-
// --- Desktop integration ---
|
|
30
|
-
import { isDesktop } from "@/lib/desktop-detect"
|
|
31
|
-
import { insertLocalFile } from "../extensions/local-file/insert-local-file.ts"
|
|
32
|
-
import { Button } from "@/components/ui/button"
|
|
33
|
-
import {
|
|
34
|
-
Tooltip,
|
|
35
|
-
TooltipContent,
|
|
36
|
-
TooltipTrigger,
|
|
37
|
-
} from "@/components/ui/tooltip"
|
|
38
|
-
|
|
39
28
|
// --- Styles ---
|
|
40
29
|
import "./notion-like-editor-header.scss"
|
|
41
|
-
import { m } from "@/paraglide/messages.js";
|
|
42
30
|
|
|
43
31
|
export function NotionEditorHeader() {
|
|
44
32
|
const { editor } = useContext(EditorContext)!
|
|
45
|
-
const [showLocalFileBtn, setShowLocalFileBtn] = useState(false)
|
|
46
|
-
|
|
47
|
-
useEffect(() => {
|
|
48
|
-
setShowLocalFileBtn(isDesktop())
|
|
49
|
-
}, [])
|
|
50
33
|
|
|
51
34
|
return (
|
|
52
35
|
<header className="notion-like-editor-header">
|
|
@@ -56,19 +39,6 @@ export function NotionEditorHeader() {
|
|
|
56
39
|
<UndoRedoButton action="undo" />
|
|
57
40
|
<UndoRedoButton action="redo" />
|
|
58
41
|
</ButtonGroup>
|
|
59
|
-
|
|
60
|
-
{showLocalFileBtn && editor && (
|
|
61
|
-
<ButtonGroup>
|
|
62
|
-
<Tooltip>
|
|
63
|
-
<TooltipTrigger render={<Button variant="ghost"
|
|
64
|
-
size="sm"
|
|
65
|
-
onClick={() => insertLocalFile(editor)} />}>
|
|
66
|
-
<FolderOpen className="size-4" />
|
|
67
|
-
</TooltipTrigger>
|
|
68
|
-
<TooltipContent> {m['editor_insert-local-file']() } </TooltipContent>
|
|
69
|
-
</Tooltip>
|
|
70
|
-
</ButtonGroup>
|
|
71
|
-
)}
|
|
72
42
|
</div>
|
|
73
43
|
</header>
|
|
74
44
|
)
|
|
@@ -20,6 +20,7 @@ import type { DocumentSuggestion, MentionUserSource, AiModelState } from "./use-
|
|
|
20
20
|
import { useCallback, useContext, useEffect, useMemo, useRef, useState, createContext } from "react"
|
|
21
21
|
import { useDebouncedCallback, EDITOR_CHANGE_DEBOUNCE_MS } from "@/lib/pacer"
|
|
22
22
|
import { EditorContent, EditorContext, useEditor, type Editor } from "@tiptap/react"
|
|
23
|
+
import type { Transaction } from "@tiptap/pm/state"
|
|
23
24
|
import { createPortal } from "react-dom"
|
|
24
25
|
|
|
25
26
|
// --- Extensions(装配单一来源,见 editor-extensions.ts 头注) ---
|
|
@@ -85,6 +86,15 @@ export interface DocumentPreviewData {
|
|
|
85
86
|
export interface NotionDocEditorProps {
|
|
86
87
|
content?: { html: string; markdown: string } | { text: string } | string
|
|
87
88
|
onChange?: (content: { html: string; markdown: string }) => void
|
|
89
|
+
/**
|
|
90
|
+
* 用户编辑同步信号(任务 2 审查修复①):onUpdate 同步触发,先于
|
|
91
|
+
* EDITOR_CHANGE_DEBOUNCE_MS(500ms) 的 onChange——消费方以此在防抖窗口内
|
|
92
|
+
* 维持「有未落库编辑」判定,堵导航拦截的 dirty 盲区。仅用户编辑触发:
|
|
93
|
+
* addToHistory=false 的事务(y-sync 远端同步等)与 UniqueID 插件追加的
|
|
94
|
+
* 初始化 ID 归一化(__uniqueIDTransaction,审查修复② 的来源)不触发;
|
|
95
|
+
* undo/redo 走 historyKey meta 不受影响。
|
|
96
|
+
*/
|
|
97
|
+
onDirtySignal?: () => void
|
|
88
98
|
placeholder?: string
|
|
89
99
|
readOnly?: boolean
|
|
90
100
|
showTocSidebar?: boolean
|
|
@@ -257,6 +267,7 @@ export function EditorContentArea({ hideDragHandle, hideInsertButton, hideAi }:
|
|
|
257
267
|
function NotionDocEditorContent({
|
|
258
268
|
content,
|
|
259
269
|
onChange,
|
|
270
|
+
onDirtySignal,
|
|
260
271
|
placeholder,
|
|
261
272
|
readOnly = false,
|
|
262
273
|
showTocSidebar = false,
|
|
@@ -279,6 +290,9 @@ function NotionDocEditorContent({
|
|
|
279
290
|
const onChangeRef = useRef(onChange)
|
|
280
291
|
onChangeRef.current = onChange
|
|
281
292
|
|
|
293
|
+
const onDirtySignalRef = useRef(onDirtySignal)
|
|
294
|
+
onDirtySignalRef.current = onDirtySignal
|
|
295
|
+
|
|
282
296
|
const getInitialContent = useCallback(
|
|
283
297
|
() => resolveContentHtml(content),
|
|
284
298
|
[content]
|
|
@@ -312,8 +326,23 @@ function NotionDocEditorContent({
|
|
|
312
326
|
onTocContent: setTocContent,
|
|
313
327
|
collab,
|
|
314
328
|
}),
|
|
315
|
-
onUpdate: ({ editor: ed }: { editor: Editor }) => {
|
|
329
|
+
onUpdate: ({ editor: ed, transaction, appendedTransactions }: { editor: Editor; transaction: Transaction; appendedTransactions?: Transaction[] }) => {
|
|
316
330
|
if (!onChangeRef.current) return
|
|
331
|
+
// 同步 dirty 信号:先于 500ms onChange 防抖。仅用户编辑触发——排除两
|
|
332
|
+
// 类程序性 docChanged 事务:addToHistory=false(y-sync 远端同步等)与
|
|
333
|
+
// UniqueID 插件追加的 ID 归一化(__uniqueIDTransaction,挂载初始化即
|
|
334
|
+
// 审查修复② 的来源)。Enter 建新段时用户事务与 uniqueID 追加并存,
|
|
335
|
+
// 由「基础事务 docChanged」兜底不误吞。
|
|
336
|
+
const baseIsUserEdit =
|
|
337
|
+
transaction.docChanged &&
|
|
338
|
+
transaction.getMeta("addToHistory") !== false &&
|
|
339
|
+
transaction.getMeta("__uniqueIDTransaction") !== true
|
|
340
|
+
const appendedUserEdit = appendedTransactions?.some(
|
|
341
|
+
(t) => t.docChanged && t.getMeta("__uniqueIDTransaction") !== true
|
|
342
|
+
)
|
|
343
|
+
if (baseIsUserEdit || appendedUserEdit) {
|
|
344
|
+
onDirtySignalRef.current?.()
|
|
345
|
+
}
|
|
317
346
|
debouncedSave(ed)
|
|
318
347
|
},
|
|
319
348
|
}), [readOnly, editorClassName, getInitialContent, placeholder, setTocContent, collab])
|
|
@@ -396,6 +425,7 @@ export function NotionDocEditor({
|
|
|
396
425
|
content,
|
|
397
426
|
placeholder,
|
|
398
427
|
onChange,
|
|
428
|
+
onDirtySignal,
|
|
399
429
|
readOnly = false,
|
|
400
430
|
showTocSidebar = false,
|
|
401
431
|
tocSidebarClassName,
|
|
@@ -419,6 +449,7 @@ export function NotionDocEditor({
|
|
|
419
449
|
content={content}
|
|
420
450
|
placeholder={resolvedPlaceholder}
|
|
421
451
|
onChange={onChange}
|
|
452
|
+
onDirtySignal={onDirtySignal}
|
|
422
453
|
readOnly={readOnly}
|
|
423
454
|
showTocSidebar={showTocSidebar}
|
|
424
455
|
tocSidebarClassName={tocSidebarClassName}
|
|
@@ -27,34 +27,8 @@ vi.mock("@tanstack/react-router", () => ({
|
|
|
27
27
|
Link: ({ children }: any) => <a>{children}</a>,
|
|
28
28
|
}));
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
value,
|
|
33
|
-
handleChange: vi.fn(),
|
|
34
|
-
handleBlur: vi.fn(),
|
|
35
|
-
meta: { errors: [] },
|
|
36
|
-
});
|
|
37
|
-
return {
|
|
38
|
-
useForm: () => {
|
|
39
|
-
const fields = {
|
|
40
|
-
name: makeField(""),
|
|
41
|
-
icon: makeField("folder"),
|
|
42
|
-
description: makeField(""),
|
|
43
|
-
};
|
|
44
|
-
return {
|
|
45
|
-
handleSubmit: (e: any) => e.preventDefault(),
|
|
46
|
-
setFieldValue: () => {},
|
|
47
|
-
state: { values: { name: "", icon: "folder", description: "" } },
|
|
48
|
-
fields,
|
|
49
|
-
Field: ({ name, children }: { name: "name" | "icon" | "description"; children: unknown }) =>
|
|
50
|
-
typeof children === "function"
|
|
51
|
-
? (children as (field: unknown) => unknown)(fields[name])
|
|
52
|
-
: null,
|
|
53
|
-
};
|
|
54
|
-
},
|
|
55
|
-
};
|
|
56
|
-
});
|
|
57
|
-
|
|
30
|
+
// 表单走真实 create-app-form 工厂(@tanstack/react-form 原生跑在 jsdom),
|
|
31
|
+
// 不再 mock useForm——顺带覆盖 field.AppFieldError 命名空间消费路径。
|
|
58
32
|
describe("CreateWorkspaceDialog", () => {
|
|
59
33
|
it("打开时渲染标题", () => {
|
|
60
34
|
render(<CreateWorkspaceDialog open={true} onOpenChange={() => {}} />);
|
|
@@ -26,34 +26,8 @@ vi.mock("@tanstack/react-router", () => ({
|
|
|
26
26
|
useRouter: () => ({ navigate: vi.fn() }),
|
|
27
27
|
}));
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
value,
|
|
32
|
-
handleChange: vi.fn(),
|
|
33
|
-
handleBlur: vi.fn(),
|
|
34
|
-
meta: { errors: [] },
|
|
35
|
-
});
|
|
36
|
-
return {
|
|
37
|
-
useForm: () => {
|
|
38
|
-
const fields = {
|
|
39
|
-
name: makeField("测试"),
|
|
40
|
-
icon: makeField("folder"),
|
|
41
|
-
description: makeField(""),
|
|
42
|
-
};
|
|
43
|
-
return {
|
|
44
|
-
handleSubmit: (e: any) => e.preventDefault(),
|
|
45
|
-
setFieldValue: () => {},
|
|
46
|
-
state: { values: { name: "测试", icon: "folder", description: "" } },
|
|
47
|
-
fields,
|
|
48
|
-
Field: ({ name, children }: { name: "name" | "icon" | "description"; children: unknown }) =>
|
|
49
|
-
typeof children === "function"
|
|
50
|
-
? (children as (field: unknown) => unknown)(fields[name])
|
|
51
|
-
: null,
|
|
52
|
-
};
|
|
53
|
-
},
|
|
54
|
-
};
|
|
55
|
-
});
|
|
56
|
-
|
|
29
|
+
// 表单走真实 create-app-form 工厂(@tanstack/react-form 原生跑在 jsdom),
|
|
30
|
+
// 不再 mock useForm——顺带覆盖 field.AppFieldError 命名空间消费路径。
|
|
57
31
|
describe("EditWorkspaceDialog", () => {
|
|
58
32
|
it("打开时渲染标题", () => {
|
|
59
33
|
render(
|
|
@@ -16,7 +16,6 @@
|
|
|
16
16
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
17
|
|
|
18
18
|
import { useState } from "react";
|
|
19
|
-
import { useForm } from "@tanstack/react-form";
|
|
20
19
|
import { Link, useRouter } from "@tanstack/react-router";
|
|
21
20
|
import { Button } from "@/components/ui/button";
|
|
22
21
|
import { Input } from "@/components/ui/input";
|
|
@@ -28,7 +27,7 @@ import {
|
|
|
28
27
|
DialogHeader,
|
|
29
28
|
DialogTitle,
|
|
30
29
|
} from "@/components/ui/dialog";
|
|
31
|
-
import {
|
|
30
|
+
import { useAppForm } from "@/components/forms/create-app-form";
|
|
32
31
|
import { LucideIconPicker } from "@/components/shared-kit/icons/lucide-icon-picker";
|
|
33
32
|
import { createWorkspaceFn } from "@/server/serverFns/workspace";
|
|
34
33
|
import { workspaceFormSchema } from "../schemas/create-workspace";
|
|
@@ -48,53 +47,49 @@ export function CreateWorkspaceDialog({
|
|
|
48
47
|
onSuccess,
|
|
49
48
|
}: CreateWorkspaceDialogProps) {
|
|
50
49
|
const router = useRouter();
|
|
51
|
-
const [loading, setLoading] = useState(false);
|
|
52
50
|
const [error, setError] = useState("");
|
|
53
51
|
const [showUpgrade, setShowUpgrade] = useState(false);
|
|
54
52
|
|
|
55
|
-
const form =
|
|
53
|
+
const form = useAppForm({
|
|
56
54
|
defaultValues: {
|
|
57
55
|
name: "",
|
|
58
56
|
description: "",
|
|
59
57
|
icon: "",
|
|
60
58
|
},
|
|
61
59
|
validators: [{ run: workspaceFormSchema, triggers: ['change'] }],
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
setError(m.workspace_createWorkspace_nameRequired());
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
setLoading(true);
|
|
70
|
-
setError("");
|
|
71
|
-
try {
|
|
72
|
-
const workspace = await createWorkspaceFn({
|
|
73
|
-
data: {
|
|
74
|
-
icon: form.state.values.icon,
|
|
75
|
-
name: form.state.values.name.trim(),
|
|
76
|
-
description: form.state.values.description.trim(),
|
|
77
|
-
},
|
|
78
|
-
});
|
|
79
|
-
onOpenChange(false);
|
|
80
|
-
onSuccess?.();
|
|
81
|
-
emitEvent(AppEvents.workspace.created);
|
|
82
|
-
await router.navigate({ to: "/workspaces/$workspaceId", params: { workspaceId: workspace.id } });
|
|
83
|
-
} catch (err) {
|
|
84
|
-
// serverFn RPC 错误经序列化只保留 message(name/resource 属性丢失),
|
|
85
|
-
// ResourceLimitError 的 message 格式为 "Resource <key> limit reached (u/l)"
|
|
86
|
-
const isWorkspaceLimit =
|
|
87
|
-
err instanceof Error &&
|
|
88
|
-
err.message.startsWith("Resource workspaces limit reached");
|
|
89
|
-
if (isWorkspaceLimit) {
|
|
90
|
-
setShowUpgrade(true);
|
|
60
|
+
onSubmit: async ({ value }) => {
|
|
61
|
+
// schema min(1) 放行纯空白串(" "),保留原提交前守卫兜底
|
|
62
|
+
if (!value.name.trim()) {
|
|
63
|
+
setError(m.workspace_createWorkspace_nameRequired());
|
|
91
64
|
return;
|
|
92
65
|
}
|
|
93
|
-
setError(
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
66
|
+
setError("");
|
|
67
|
+
try {
|
|
68
|
+
const workspace = await createWorkspaceFn({
|
|
69
|
+
data: {
|
|
70
|
+
icon: value.icon,
|
|
71
|
+
name: value.name.trim(),
|
|
72
|
+
description: value.description.trim(),
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
onOpenChange(false);
|
|
76
|
+
onSuccess?.();
|
|
77
|
+
emitEvent(AppEvents.workspace.created);
|
|
78
|
+
await router.navigate({ to: "/workspaces/$workspaceId", params: { workspaceId: workspace.id } });
|
|
79
|
+
} catch (err) {
|
|
80
|
+
// serverFn RPC 错误经序列化只保留 message(name/resource 属性丢失),
|
|
81
|
+
// ResourceLimitError 的 message 格式为 "Resource <key> limit reached (u/l)"
|
|
82
|
+
const isWorkspaceLimit =
|
|
83
|
+
err instanceof Error &&
|
|
84
|
+
err.message.startsWith("Resource workspaces limit reached");
|
|
85
|
+
if (isWorkspaceLimit) {
|
|
86
|
+
setShowUpgrade(true);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
setError(errorMessage(err, m.workspace_createWorkspace_createFailed()));
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
});
|
|
98
93
|
|
|
99
94
|
const handleOpenChange = (newOpen: boolean) => {
|
|
100
95
|
if (newOpen) {
|
|
@@ -145,7 +140,7 @@ export function CreateWorkspaceDialog({
|
|
|
145
140
|
maxLength={100}
|
|
146
141
|
className="h-10 w-full"
|
|
147
142
|
/>
|
|
148
|
-
<AppFieldError
|
|
143
|
+
<field.AppFieldError />
|
|
149
144
|
</div>
|
|
150
145
|
)} />
|
|
151
146
|
</div>
|
|
@@ -160,7 +155,7 @@ export function CreateWorkspaceDialog({
|
|
|
160
155
|
maxLength={500}
|
|
161
156
|
className="flex-1 min-h-0 resize-none border-0 shadow-none bg-transparent px-3 py-2 text-sm outline-none placeholder:text-muted-foreground focus-visible:ring-0 focus-visible:border-0"
|
|
162
157
|
/>
|
|
163
|
-
<AppFieldError
|
|
158
|
+
<field.AppFieldError />
|
|
164
159
|
</div>
|
|
165
160
|
)} />
|
|
166
161
|
|
|
@@ -173,9 +168,16 @@ export function CreateWorkspaceDialog({
|
|
|
173
168
|
<Button variant="outline" onClick={() => handleOpenChange(false)}>
|
|
174
169
|
{m.workspace_createWorkspace_cancel()}
|
|
175
170
|
</Button>
|
|
176
|
-
<
|
|
177
|
-
{
|
|
178
|
-
|
|
171
|
+
<form.Subscribe selector={(state) => state.isSubmitting}>
|
|
172
|
+
{(isSubmitting) => (
|
|
173
|
+
<Button
|
|
174
|
+
onClick={() => form.handleSubmit()}
|
|
175
|
+
disabled={isSubmitting}
|
|
176
|
+
>
|
|
177
|
+
{isSubmitting ? m.workspace_createWorkspace_creating() : m.workspace_createWorkspace_create()}
|
|
178
|
+
</Button>
|
|
179
|
+
)}
|
|
180
|
+
</form.Subscribe>
|
|
179
181
|
</DialogFooter>
|
|
180
182
|
</DialogContent>
|
|
181
183
|
</Dialog>
|
|
@@ -16,7 +16,6 @@
|
|
|
16
16
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
17
|
|
|
18
18
|
import { useState } from "react";
|
|
19
|
-
import { useForm } from "@tanstack/react-form";
|
|
20
19
|
import { useRouter } from "@tanstack/react-router";
|
|
21
20
|
import { Button } from "@/components/ui/button";
|
|
22
21
|
import { Input } from "@/components/ui/input";
|
|
@@ -28,7 +27,7 @@ import {
|
|
|
28
27
|
DialogHeader,
|
|
29
28
|
DialogTitle,
|
|
30
29
|
} from "@/components/ui/dialog";
|
|
31
|
-
import {
|
|
30
|
+
import { useAppForm } from "@/components/forms/create-app-form";
|
|
32
31
|
import { LucideIconPicker } from "@/components/shared-kit/icons/lucide-icon-picker";
|
|
33
32
|
import { updateWorkspaceFn } from "@/server/serverFns/workspace";
|
|
34
33
|
import { workspaceFormSchema } from "../schemas/create-workspace";
|
|
@@ -50,45 +49,41 @@ export function EditWorkspaceDialog({
|
|
|
50
49
|
onSuccess,
|
|
51
50
|
}: EditWorkspaceDialogProps) {
|
|
52
51
|
const router = useRouter();
|
|
53
|
-
const [loading, setLoading] = useState(false);
|
|
54
52
|
const [error, setError] = useState("");
|
|
55
53
|
|
|
56
|
-
const form =
|
|
54
|
+
const form = useAppForm({
|
|
57
55
|
defaultValues: {
|
|
58
56
|
name: workspace.name,
|
|
59
57
|
description: workspace.description ?? "",
|
|
60
58
|
icon: workspace.icon,
|
|
61
59
|
},
|
|
62
60
|
validators: [{ run: workspaceFormSchema, triggers: ['change'] }],
|
|
61
|
+
onSubmit: async ({ value }) => {
|
|
62
|
+
// schema min(1) 放行纯空白串(" "),保留原提交前守卫兜底
|
|
63
|
+
if (!value.name.trim()) {
|
|
64
|
+
setError(m.workspace_createWorkspace_nameRequired());
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
setError("");
|
|
68
|
+
try {
|
|
69
|
+
await updateWorkspaceFn({
|
|
70
|
+
data: {
|
|
71
|
+
workspaceId: workspace.id,
|
|
72
|
+
name: value.name.trim(),
|
|
73
|
+
icon: value.icon,
|
|
74
|
+
description: value.description.trim() || null,
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
onOpenChange(false);
|
|
78
|
+
onSuccess?.();
|
|
79
|
+
emitEvent(AppEvents.workspace.updated);
|
|
80
|
+
await router.invalidate();
|
|
81
|
+
} catch (err) {
|
|
82
|
+
setError(errorMessage(err, m.workspace_createWorkspace_createFailed()));
|
|
83
|
+
}
|
|
84
|
+
},
|
|
63
85
|
});
|
|
64
86
|
|
|
65
|
-
const handleSubmit = async () => {
|
|
66
|
-
if (!form.state.values.name.trim()) {
|
|
67
|
-
setError(m.workspace_createWorkspace_nameRequired());
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
|
-
setLoading(true);
|
|
71
|
-
setError("");
|
|
72
|
-
try {
|
|
73
|
-
await updateWorkspaceFn({
|
|
74
|
-
data: {
|
|
75
|
-
workspaceId: workspace.id,
|
|
76
|
-
name: form.state.values.name.trim(),
|
|
77
|
-
icon: form.state.values.icon,
|
|
78
|
-
description: form.state.values.description.trim() || null,
|
|
79
|
-
},
|
|
80
|
-
});
|
|
81
|
-
onOpenChange(false);
|
|
82
|
-
onSuccess?.();
|
|
83
|
-
emitEvent(AppEvents.workspace.updated);
|
|
84
|
-
await router.invalidate();
|
|
85
|
-
} catch (err) {
|
|
86
|
-
setError(errorMessage(err, m.workspace_createWorkspace_createFailed()));
|
|
87
|
-
} finally {
|
|
88
|
-
setLoading(false);
|
|
89
|
-
}
|
|
90
|
-
};
|
|
91
|
-
|
|
92
87
|
const handleOpenChange = (newOpen: boolean) => {
|
|
93
88
|
if (newOpen) {
|
|
94
89
|
form.setFieldValue("name", workspace.name);
|
|
@@ -125,7 +120,7 @@ export function EditWorkspaceDialog({
|
|
|
125
120
|
maxLength={100}
|
|
126
121
|
className="h-10 w-full"
|
|
127
122
|
/>
|
|
128
|
-
<AppFieldError
|
|
123
|
+
<field.AppFieldError />
|
|
129
124
|
</div>
|
|
130
125
|
)} />
|
|
131
126
|
</div>
|
|
@@ -140,7 +135,7 @@ export function EditWorkspaceDialog({
|
|
|
140
135
|
maxLength={500}
|
|
141
136
|
className="flex-1 min-h-0 resize-none border-0 shadow-none bg-transparent px-3 py-2 text-sm outline-none placeholder:text-muted-foreground focus-visible:ring-0 focus-visible:border-0"
|
|
142
137
|
/>
|
|
143
|
-
<AppFieldError
|
|
138
|
+
<field.AppFieldError />
|
|
144
139
|
</div>
|
|
145
140
|
)} />
|
|
146
141
|
|
|
@@ -151,9 +146,16 @@ export function EditWorkspaceDialog({
|
|
|
151
146
|
<Button variant="outline" onClick={() => handleOpenChange(false)}>
|
|
152
147
|
{m.workspace_createWorkspace_cancel()}
|
|
153
148
|
</Button>
|
|
154
|
-
<
|
|
155
|
-
{
|
|
156
|
-
|
|
149
|
+
<form.Subscribe selector={(state) => state.isSubmitting}>
|
|
150
|
+
{(isSubmitting) => (
|
|
151
|
+
<Button
|
|
152
|
+
onClick={() => form.handleSubmit()}
|
|
153
|
+
disabled={isSubmitting}
|
|
154
|
+
>
|
|
155
|
+
{isSubmitting ? m.workspace_createWorkspace_creating() : m.common_button_save()}
|
|
156
|
+
</Button>
|
|
157
|
+
)}
|
|
158
|
+
</form.Subscribe>
|
|
157
159
|
</DialogFooter>
|
|
158
160
|
</DialogContent>
|
|
159
161
|
</Dialog>
|
|
@@ -107,6 +107,8 @@ export class DecorEngine extends LeaferEngineBase {
|
|
|
107
107
|
this.measurePointer = { clientX: e.clientX, clientY: e.clientY }
|
|
108
108
|
this.measureRefresh()
|
|
109
109
|
}
|
|
110
|
+
// 豁免热键收编(audit 2026-10-31):engine 内部标注量测键状态机(非 React
|
|
111
|
+
// 组件层、keyup/blur 联动清位),useHotkey 表达不了引擎生命周期挂载
|
|
110
112
|
window.addEventListener("keydown", onKeyDown)
|
|
111
113
|
window.addEventListener("keyup", onKeyUp)
|
|
112
114
|
window.addEventListener("blur", onBlur)
|
|
@@ -572,6 +572,8 @@ export class PathEditEngine extends DocEngine {
|
|
|
572
572
|
host.addEventListener("pointerup", onPointerUp)
|
|
573
573
|
host.addEventListener("click", swallow, true)
|
|
574
574
|
host.addEventListener("dblclick", onDblClick, true)
|
|
575
|
+
// 豁免热键收编(audit 2026-10-31):路径编辑锚点单键(1-4)在 window
|
|
576
|
+
// 捕获相、仅路径编辑态生效,与 React 层 useHotkey 的冒泡相注册时序不兼容
|
|
575
577
|
window.addEventListener("keydown", onKey, true)
|
|
576
578
|
this.pathEditInteractionIds = [
|
|
577
579
|
() => host.removeEventListener("pointerdown", onPointerDown, true),
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* useUnsavedGuard(useBlocker 统一未保存拦截,TanStack 优化计划任务 2):
|
|
5
|
+
*
|
|
6
|
+
* ① dirty 态 SPA 导航被拦 → 渲染 AlertDialog(shadcn,非 window.confirm),
|
|
7
|
+
* shouldBlockFn 随 dirty 翻转(过度拦截防线:仅 dirty 时 block);
|
|
8
|
+
* ② 「离开」先执行 onSaveAndLeave(flush 保存)再 proceed(顺序断言);
|
|
9
|
+
* ③ 「留下」调 reset,不触发 proceed / onSaveAndLeave;
|
|
10
|
+
* ④ clean 态零弹窗、shouldBlockFn=false(零拦截);
|
|
11
|
+
* ⑤ beforeunload:dirty preventDefault(enableBeforeUnload 生效)、clean 放行;
|
|
12
|
+
* ⑥ SSR 安全:服务端渲染(effects 不跑)不炸且零弹窗输出。
|
|
13
|
+
*
|
|
14
|
+
* useBlocker mock 按 @tanstack/react-router 1.170.46 的 .d.ts 实际形态:
|
|
15
|
+
* 默认形态返回 void,withResolver: true 才返回 BlockerResolver
|
|
16
|
+
* (status: 'blocked' | 'idle' + proceed/reset)。beforeunload 语义复刻
|
|
17
|
+
* @tanstack/history 的 onBeforeUnload:enableBeforeUnload 为 boolean 时恒
|
|
18
|
+
* preventDefault,为 function 时在卸载时调用决定。
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
22
|
+
import { render, screen, fireEvent, waitFor, act } from '@testing-library/react'
|
|
23
|
+
import { renderToString } from 'react-dom/server'
|
|
24
|
+
import { useUnsavedGuard } from '@/hooks/use-unsaved-guard'
|
|
25
|
+
import { m } from '@/paraglide/messages.js'
|
|
26
|
+
|
|
27
|
+
type BlockerResolverLike = {
|
|
28
|
+
status: 'blocked' | 'idle'
|
|
29
|
+
current: undefined
|
|
30
|
+
next: undefined
|
|
31
|
+
action: undefined
|
|
32
|
+
proceed: (() => void) | undefined
|
|
33
|
+
reset: (() => void) | undefined
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const blockerState = vi.hoisted(() => ({
|
|
37
|
+
resolver: {
|
|
38
|
+
status: 'idle',
|
|
39
|
+
current: undefined,
|
|
40
|
+
next: undefined,
|
|
41
|
+
action: undefined,
|
|
42
|
+
proceed: undefined,
|
|
43
|
+
reset: undefined,
|
|
44
|
+
} as {
|
|
45
|
+
status: 'blocked' | 'idle'
|
|
46
|
+
current: undefined
|
|
47
|
+
next: undefined
|
|
48
|
+
action: undefined
|
|
49
|
+
proceed: (() => void) | undefined
|
|
50
|
+
reset: (() => void) | undefined
|
|
51
|
+
},
|
|
52
|
+
capturedOpts: null as {
|
|
53
|
+
shouldBlockFn: (args: unknown) => boolean | Promise<boolean>
|
|
54
|
+
enableBeforeUnload: boolean | (() => boolean)
|
|
55
|
+
withResolver?: boolean
|
|
56
|
+
disabled?: boolean
|
|
57
|
+
} | null,
|
|
58
|
+
}))
|
|
59
|
+
|
|
60
|
+
vi.mock('@tanstack/react-router', async () => {
|
|
61
|
+
const React = await import('react')
|
|
62
|
+
return {
|
|
63
|
+
useBlocker: (opts: NonNullable<typeof blockerState.capturedOpts>) => {
|
|
64
|
+
blockerState.capturedOpts = opts
|
|
65
|
+
// 复刻真实 useBlocker:挂载期注册 history.block 的 beforeunload 面
|
|
66
|
+
// (blockerFn 的 SPA 拦截部分由 resolver 状态承载,测试直接驱动)
|
|
67
|
+
React.useEffect(() => {
|
|
68
|
+
if (opts.disabled) return
|
|
69
|
+
const handler = (e: Event) => {
|
|
70
|
+
const enable =
|
|
71
|
+
typeof opts.enableBeforeUnload === 'function'
|
|
72
|
+
? opts.enableBeforeUnload()
|
|
73
|
+
: (opts.enableBeforeUnload ?? true)
|
|
74
|
+
if (enable) e.preventDefault()
|
|
75
|
+
}
|
|
76
|
+
window.addEventListener('beforeunload', handler)
|
|
77
|
+
return () => window.removeEventListener('beforeunload', handler)
|
|
78
|
+
}, [opts.shouldBlockFn, opts.enableBeforeUnload, opts.disabled])
|
|
79
|
+
return blockerState.resolver
|
|
80
|
+
},
|
|
81
|
+
}
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
function idleResolver(): BlockerResolverLike {
|
|
85
|
+
return { status: 'idle', current: undefined, next: undefined, action: undefined, proceed: undefined, reset: undefined }
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function blockedResolver(over: { proceed: () => void; reset: () => void }): BlockerResolverLike {
|
|
89
|
+
return { status: 'blocked', current: undefined, next: undefined, action: undefined, proceed: over.proceed, reset: over.reset }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function HookProbe(props: { isDirty: () => boolean; onSaveAndLeave?: () => Promise<void> | void }) {
|
|
93
|
+
const { dialog } = useUnsavedGuard(props)
|
|
94
|
+
return <>{dialog}</>
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const SHOULD_BLOCK_ARGS = { action: 'PUSH', current: {}, next: {} }
|
|
98
|
+
|
|
99
|
+
beforeEach(() => {
|
|
100
|
+
blockerState.resolver = idleResolver()
|
|
101
|
+
blockerState.capturedOpts = null
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
describe('useUnsavedGuard(useBlocker 统一未保存拦截)', () => {
|
|
105
|
+
it('① dirty 态渲染 AlertDialog(title/body/离开/留下),shouldBlockFn 随 dirty 翻转', () => {
|
|
106
|
+
blockerState.resolver = blockedResolver({ proceed: vi.fn(), reset: vi.fn() })
|
|
107
|
+
render(<HookProbe isDirty={() => true} />)
|
|
108
|
+
|
|
109
|
+
expect(screen.getByRole('alertdialog')).toBeInTheDocument()
|
|
110
|
+
expect(screen.getByText(m.common_unsaved_title())).toBeInTheDocument()
|
|
111
|
+
expect(screen.getByText(m.common_unsaved_body())).toBeInTheDocument()
|
|
112
|
+
expect(screen.getByRole('button', { name: m.common_unsaved_leave() })).toBeInTheDocument()
|
|
113
|
+
expect(screen.getByRole('button', { name: m.common_unsaved_stay() })).toBeInTheDocument()
|
|
114
|
+
// 过度拦截防线:dirty 时 block(透传给 useBlocker 的 shouldBlockFn 生效)
|
|
115
|
+
expect(blockerState.capturedOpts?.shouldBlockFn(SHOULD_BLOCK_ARGS)).toBe(true)
|
|
116
|
+
expect(blockerState.capturedOpts?.withResolver).toBe(true)
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it('② 点「离开」先执行 onSaveAndLeave(flush 保存)再 proceed', async () => {
|
|
120
|
+
const order: string[] = []
|
|
121
|
+
blockerState.resolver = blockedResolver({
|
|
122
|
+
proceed: () => order.push('proceed'),
|
|
123
|
+
reset: vi.fn(),
|
|
124
|
+
})
|
|
125
|
+
render(
|
|
126
|
+
<HookProbe
|
|
127
|
+
isDirty={() => true}
|
|
128
|
+
onSaveAndLeave={async () => {
|
|
129
|
+
order.push('save')
|
|
130
|
+
}}
|
|
131
|
+
/>,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
fireEvent.click(screen.getByRole('button', { name: m.common_unsaved_leave() }))
|
|
135
|
+
await waitFor(() => expect(order).toContain('proceed'))
|
|
136
|
+
expect(order).toEqual(['save', 'proceed'])
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
it('③ 点「留下」调 reset,不触发 proceed / onSaveAndLeave', async () => {
|
|
140
|
+
const proceed = vi.fn()
|
|
141
|
+
const reset = vi.fn()
|
|
142
|
+
const onSaveAndLeave = vi.fn()
|
|
143
|
+
blockerState.resolver = blockedResolver({ proceed, reset })
|
|
144
|
+
render(<HookProbe isDirty={() => true} onSaveAndLeave={onSaveAndLeave} />)
|
|
145
|
+
|
|
146
|
+
fireEvent.click(screen.getByRole('button', { name: m.common_unsaved_stay() }))
|
|
147
|
+
await waitFor(() => expect(reset).toHaveBeenCalled())
|
|
148
|
+
expect(proceed).not.toHaveBeenCalled()
|
|
149
|
+
expect(onSaveAndLeave).not.toHaveBeenCalled()
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
it('④ clean 态零弹窗、shouldBlockFn=false(零拦截)', () => {
|
|
153
|
+
render(<HookProbe isDirty={() => false} onSaveAndLeave={vi.fn()} />)
|
|
154
|
+
|
|
155
|
+
expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument()
|
|
156
|
+
expect(blockerState.capturedOpts?.shouldBlockFn(SHOULD_BLOCK_ARGS)).toBe(false)
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
it('⑤ beforeunload:dirty preventDefault(enableBeforeUnload 函数形态生效)、clean 放行', () => {
|
|
160
|
+
const dirty = render(<HookProbe isDirty={() => true} />)
|
|
161
|
+
const dirtyEvent = new Event('beforeunload', { cancelable: true })
|
|
162
|
+
act(() => {
|
|
163
|
+
window.dispatchEvent(dirtyEvent)
|
|
164
|
+
})
|
|
165
|
+
expect(dirtyEvent.defaultPrevented).toBe(true)
|
|
166
|
+
dirty.unmount()
|
|
167
|
+
|
|
168
|
+
render(<HookProbe isDirty={() => false} />)
|
|
169
|
+
const cleanEvent = new Event('beforeunload', { cancelable: true })
|
|
170
|
+
act(() => {
|
|
171
|
+
window.dispatchEvent(cleanEvent)
|
|
172
|
+
})
|
|
173
|
+
expect(cleanEvent.defaultPrevented).toBe(false)
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
it('⑥ SSR 安全:renderToString(effects 不跑)不炸且零弹窗输出', () => {
|
|
177
|
+
const html = renderToString(<HookProbe isDirty={() => true} onSaveAndLeave={vi.fn()} />)
|
|
178
|
+
expect(html).toBe('')
|
|
179
|
+
})
|
|
180
|
+
})
|