@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
|
@@ -36,7 +36,7 @@ import { Button } from '@/components/ui/button'
|
|
|
36
36
|
import { ConfirmDialog } from '@/components/confirm-dialog';
|
|
37
37
|
import { useSession } from '@/lib/auth/auth-client'
|
|
38
38
|
import { listConversationsFn, createConversationFn, updateConversationFn, deleteConversationFn, forkConversationFn } from '@/server/serverFns/chat/conversations'
|
|
39
|
-
import {
|
|
39
|
+
import { agentsQueryOptions } from '@/lib/queries/agents'
|
|
40
40
|
import { useSelectedModel } from '@/lib/ai/use-selected-model'
|
|
41
41
|
import { useSearchCapability } from '@/lib/ai/use-search-capability'
|
|
42
42
|
import { useCodeModeReadiness } from '@/lib/ai/use-code-mode-readiness'
|
|
@@ -109,8 +109,7 @@ export default function HarnessWorkbenchPage({ initialThreadId, initialMessages
|
|
|
109
109
|
const { workspace } = useWorkspace()
|
|
110
110
|
|
|
111
111
|
const { data: agents } = useQuery({
|
|
112
|
-
|
|
113
|
-
queryFn: () => listAgentsFn(),
|
|
112
|
+
...agentsQueryOptions(),
|
|
114
113
|
staleTime: 30_000,
|
|
115
114
|
enabled: initialThreadId == null,
|
|
116
115
|
})
|
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
RotateCcw,
|
|
24
24
|
} from "lucide-react";
|
|
25
25
|
import { useCallback, useRef, useState } from "react";
|
|
26
|
-
import { useHotkey } from "@tanstack/react-hotkeys";
|
|
26
|
+
import { useAppHotkeys, useHotkey } from "@tanstack/react-hotkeys";
|
|
27
27
|
import { Button } from "@/components/ui/button";
|
|
28
28
|
import {
|
|
29
29
|
DropdownMenu,
|
|
@@ -52,10 +52,12 @@ export function MessageActions({
|
|
|
52
52
|
}: MessageActionsProps) {
|
|
53
53
|
const [isOpen, setIsOpen] = useState(false);
|
|
54
54
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
55
|
-
|
|
55
|
+
// 声明即绑定:close-panel / copy-message 条目单一来源(lib/hotkeys/registry.ts)
|
|
56
|
+
const hk = useAppHotkeys();
|
|
57
|
+
useHotkey(hk["close-panel"], () => setIsOpen(false), {
|
|
56
58
|
enabled: isOpen,
|
|
57
59
|
});
|
|
58
|
-
useHotkey("
|
|
60
|
+
useHotkey(hk["copy-message"], () => {
|
|
59
61
|
onCopy();
|
|
60
62
|
setIsOpen(false);
|
|
61
63
|
}, {
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
17
|
|
|
18
18
|
import { useCallback, useState } from "react"
|
|
19
|
-
import { useHotkey } from "@tanstack/react-hotkeys"
|
|
19
|
+
import { useAppHotkeyList, useAppHotkeys, useHotkey } from "@tanstack/react-hotkeys"
|
|
20
20
|
import {
|
|
21
21
|
Dialog,
|
|
22
22
|
DialogContent,
|
|
@@ -26,27 +26,38 @@ import {
|
|
|
26
26
|
import { Button } from "@/components/ui/button"
|
|
27
27
|
import { KeyboardMusic, X } from "lucide-react"
|
|
28
28
|
import { ShortcutBadge } from "./shortcut-badge"
|
|
29
|
-
import {
|
|
29
|
+
import {
|
|
30
|
+
formatAppHotkeyForBadge,
|
|
31
|
+
CATEGORY_ORDER,
|
|
32
|
+
HOTKEY_CATEGORY_LABEL_KEYS,
|
|
33
|
+
} from "@/lib/hotkeys/registry"
|
|
34
|
+
import type { HotkeyCategory } from "@/lib/hotkeys/types"
|
|
30
35
|
import { t } from "@/lib/i18n/resolver"
|
|
31
36
|
import { m } from "@/paraglide/messages.js";
|
|
32
37
|
|
|
33
38
|
export function ShortcutsPanel() {
|
|
34
39
|
const [open, setOpen] = useState(false)
|
|
35
40
|
|
|
36
|
-
|
|
41
|
+
const hk = useAppHotkeys()
|
|
42
|
+
useHotkey(hk["shortcuts-panel"], () => setOpen((prev) => !prev), {
|
|
37
43
|
ignoreInputs: false,
|
|
38
44
|
})
|
|
39
45
|
|
|
46
|
+
// 官方 useAppHotkeyList:声明顺序返回全部应用热键(含未挂载绑定的 inactive 条目)
|
|
47
|
+
const items = useAppHotkeyList()
|
|
48
|
+
|
|
40
49
|
const grouped = useCallback(() => {
|
|
41
|
-
const map = new Map<
|
|
50
|
+
const map = new Map<HotkeyCategory, typeof items>()
|
|
42
51
|
for (const category of CATEGORY_ORDER) {
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
52
|
+
const inCategory = items.filter(
|
|
53
|
+
({ appHotkey }) => appHotkey.meta?.category === category,
|
|
54
|
+
)
|
|
55
|
+
if (inCategory.length > 0) {
|
|
56
|
+
map.set(category, inCategory)
|
|
46
57
|
}
|
|
47
58
|
}
|
|
48
59
|
return map
|
|
49
|
-
}, [])
|
|
60
|
+
}, [items])
|
|
50
61
|
|
|
51
62
|
return (
|
|
52
63
|
<Dialog open={open} onOpenChange={setOpen}>
|
|
@@ -69,16 +80,20 @@ export function ShortcutsPanel() {
|
|
|
69
80
|
{Array.from(grouped()).map(([category, shortcuts]) => (
|
|
70
81
|
<div key={category}>
|
|
71
82
|
<h4 className="mb-2 text-xs font-medium text-muted-foreground">
|
|
72
|
-
{t(HOTKEY_CATEGORY_LABEL_KEYS[category
|
|
83
|
+
{t(HOTKEY_CATEGORY_LABEL_KEYS[category])}
|
|
73
84
|
</h4>
|
|
74
85
|
<div className="space-y-1.5">
|
|
75
|
-
{shortcuts.map((
|
|
86
|
+
{shortcuts.map(({ appHotkey }) => (
|
|
76
87
|
<div
|
|
77
|
-
key={
|
|
88
|
+
key={appHotkey.id}
|
|
78
89
|
className="flex items-center justify-between rounded-md px-2 py-1.5 hover:bg-muted/50"
|
|
79
90
|
>
|
|
80
|
-
<span className="text-sm">
|
|
81
|
-
|
|
91
|
+
<span className="text-sm">
|
|
92
|
+
{appHotkey.meta?.descriptionKey
|
|
93
|
+
? t(appHotkey.meta.descriptionKey)
|
|
94
|
+
: ""}
|
|
95
|
+
</span>
|
|
96
|
+
<ShortcutBadge keys={formatAppHotkeyForBadge(appHotkey.hotkey)} />
|
|
82
97
|
</div>
|
|
83
98
|
))}
|
|
84
99
|
</div>
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
3
|
+
// AllBlue - 理想之海
|
|
4
|
+
// Copyright (C) 2026 AllBlue Contributors
|
|
5
|
+
//
|
|
6
|
+
// This program is free software: you can redistribute it and/or modify
|
|
7
|
+
// it under the terms of the GNU Affero General Public License as published by
|
|
8
|
+
// the Free Software Foundation, either version 3 of the License, or
|
|
9
|
+
// (at your option) any later version.
|
|
10
|
+
//
|
|
11
|
+
// This program is distributed in the hope that it will be useful,
|
|
12
|
+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
13
|
+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
14
|
+
// GNU Affero General Public License for more details.
|
|
15
|
+
//
|
|
16
|
+
// You should have received a copy of the GNU Affero General Public License
|
|
17
|
+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
18
|
+
import { render, screen } from "@testing-library/react";
|
|
19
|
+
import { describe, expect, it } from "vitest";
|
|
20
|
+
import { BurndownChart } from "../burndown-chart";
|
|
21
|
+
import type { BurndownResult } from "@/lib/iteration/burndown";
|
|
22
|
+
|
|
23
|
+
// leading:末点 pending < ideal(与 computeBurndown 的状态判定语义一致)
|
|
24
|
+
const leadingData: BurndownResult & { timezone: string } = {
|
|
25
|
+
timezone: "Asia/Shanghai",
|
|
26
|
+
status: "leading",
|
|
27
|
+
points: [
|
|
28
|
+
{ date: "2026-01-01", scope: 10, pending: 10, ideal: 10 },
|
|
29
|
+
{ date: "2026-01-02", scope: 10, pending: 6, ideal: 7 },
|
|
30
|
+
{ date: "2026-01-03", scope: 11, pending: 3, ideal: 4 },
|
|
31
|
+
],
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const trailingData: BurndownResult & { timezone: string } = {
|
|
35
|
+
...leadingData,
|
|
36
|
+
status: "trailing",
|
|
37
|
+
points: [
|
|
38
|
+
{ date: "2026-02-01", scope: 10, pending: 10, ideal: 10 },
|
|
39
|
+
{ date: "2026-02-02", scope: 10, pending: 9, ideal: 7 },
|
|
40
|
+
{ date: "2026-02-03", scope: 11, pending: 8, ideal: 4 },
|
|
41
|
+
],
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
function chartSvg(container: HTMLElement): SVGSVGElement {
|
|
45
|
+
const svg = container.querySelector("svg.ts-chart");
|
|
46
|
+
expect(svg).not.toBeNull();
|
|
47
|
+
return svg as SVGSVGElement;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
describe("BurndownChart 数据→图形", () => {
|
|
51
|
+
it("无数据点时渲染空态且不出 SVG", () => {
|
|
52
|
+
const { container } = render(
|
|
53
|
+
<BurndownChart data={{ ...leadingData, points: [] }} />,
|
|
54
|
+
);
|
|
55
|
+
expect(screen.getByText("暂无数据,添加需求后生成")).toBeInTheDocument();
|
|
56
|
+
expect(container.querySelector("svg")).toBeNull();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("按数据点绘制图形:5 条网格线 + 双折线 + 面积 + 每点一个圆点", () => {
|
|
60
|
+
const { container } = render(<BurndownChart data={leadingData} />);
|
|
61
|
+
const svg = chartSvg(container);
|
|
62
|
+
// 无障碍:图表 svg 以标题文案作为 aria-label
|
|
63
|
+
expect(svg.getAttribute("aria-label")).toBe("燃尽图");
|
|
64
|
+
|
|
65
|
+
// ruleY 网格线:0/25%/50%/75%/100% 五条水平线,全宽
|
|
66
|
+
const rules = svg.querySelectorAll("g.ts-chart__rule-y line");
|
|
67
|
+
expect(rules).toHaveLength(5);
|
|
68
|
+
const ruleYs = Array.from(rules).map((l) => Number(l.getAttribute("y1")));
|
|
69
|
+
expect([...ruleYs].sort((a, b) => a - b)).toEqual([16, 58, 100, 142, 184]);
|
|
70
|
+
|
|
71
|
+
// ideal 线:虚线、muted-foreground
|
|
72
|
+
const idealPath = svg.querySelector('path[stroke="var(--muted-foreground)"]');
|
|
73
|
+
expect(idealPath).not.toBeNull();
|
|
74
|
+
expect(idealPath!.getAttribute("stroke-dasharray")).toBe("4 3");
|
|
75
|
+
expect(idealPath!.getAttribute("stroke-width")).toBe("1.5");
|
|
76
|
+
|
|
77
|
+
// pending 线:实线、primary、宽 2
|
|
78
|
+
const pendingPath = svg.querySelector('path[stroke="var(--primary)"]:not([stroke-dasharray])');
|
|
79
|
+
expect(pendingPath).not.toBeNull();
|
|
80
|
+
expect(pendingPath!.getAttribute("stroke-width")).toBe("2");
|
|
81
|
+
|
|
82
|
+
// 面积:pending 与 ideal 之间的多边形,leading 为绿色 15% 透明度
|
|
83
|
+
const areaPath = svg.querySelector("g.ts-chart__area path");
|
|
84
|
+
expect(areaPath).not.toBeNull();
|
|
85
|
+
expect(areaPath!.getAttribute("fill")).toBe("var(--color-green-600)");
|
|
86
|
+
expect(areaPath!.getAttribute("fill-opacity")).toBe("0.15");
|
|
87
|
+
// 面积路径几何闭合(Z 收尾)且覆盖两个数据系列
|
|
88
|
+
expect(areaPath!.getAttribute("d")).toMatch(/Z$/);
|
|
89
|
+
|
|
90
|
+
// 圆点:每个 pending 数据点一个,r=2.5、primary 填充
|
|
91
|
+
const dots = svg.querySelectorAll("g.ts-chart__dot circle");
|
|
92
|
+
expect(dots).toHaveLength(leadingData.points.length);
|
|
93
|
+
for (const dot of dots) {
|
|
94
|
+
expect(dot.getAttribute("fill")).toBe("var(--primary)");
|
|
95
|
+
expect(Number(dot.getAttribute("r"))).toBe(2.5);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("trailing 状态面积切换为红色", () => {
|
|
100
|
+
const { container } = render(<BurndownChart data={trailingData} />);
|
|
101
|
+
const areaPath = chartSvg(container).querySelector("g.ts-chart__area path");
|
|
102
|
+
expect(areaPath!.getAttribute("fill")).toBe("var(--color-red-600)");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("首尾日期标注渲染为文本", () => {
|
|
106
|
+
render(<BurndownChart data={leadingData} />);
|
|
107
|
+
expect(screen.getByText("2026-01-01")).toBeInTheDocument();
|
|
108
|
+
expect(screen.getByText("2026-01-03")).toBeInTheDocument();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("状态文案随 status 切换(领先/滞后)", () => {
|
|
112
|
+
const { unmount } = render(<BurndownChart data={leadingData} />);
|
|
113
|
+
expect(screen.getByText("领先")).toBeInTheDocument();
|
|
114
|
+
unmount();
|
|
115
|
+
render(<BurndownChart data={trailingData} />);
|
|
116
|
+
expect(screen.getByText("滞后")).toBeInTheDocument();
|
|
117
|
+
});
|
|
118
|
+
});
|
|
@@ -14,21 +14,62 @@
|
|
|
14
14
|
//
|
|
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
|
+
import { areaY, defineChart, dot, lineY, ruleY } from "@tanstack/charts";
|
|
18
|
+
import { scaleLinear } from "@tanstack/charts/scales/linear";
|
|
19
|
+
import { scalePoint } from "@tanstack/charts/scales/point";
|
|
20
|
+
import { tooltip } from "@tanstack/charts/tooltip";
|
|
21
|
+
import { Chart } from "@tanstack/charts/react";
|
|
22
|
+
import { useMemo } from "react";
|
|
17
23
|
import type { BurndownResult } from "@/lib/iteration/burndown";
|
|
18
24
|
import { m } from "@/paraglide/messages.js";
|
|
19
25
|
|
|
20
26
|
export function BurndownChart({ data }: { data: BurndownResult & { timezone: string } }) {
|
|
27
|
+
const leading = data.status === "leading";
|
|
28
|
+
const definition = useMemo(() => {
|
|
29
|
+
const points = data.points;
|
|
30
|
+
const maxVal = Math.max(1, ...points.map((p) => Math.max(p.scope, p.pending, p.ideal)));
|
|
31
|
+
return defineChart({
|
|
32
|
+
// 原手写 SVG 无坐标轴刻度,仅 5 条横向网格线,故整体关闭 guides 由 ruleY 复刻
|
|
33
|
+
guides: false,
|
|
34
|
+
margin: 16,
|
|
35
|
+
marks: [
|
|
36
|
+
ruleY([0, maxVal / 4, maxVal / 2, (maxVal * 3) / 4, maxVal], {
|
|
37
|
+
stroke: "var(--border)",
|
|
38
|
+
strokeWidth: 1,
|
|
39
|
+
strokeOpacity: 1,
|
|
40
|
+
}),
|
|
41
|
+
areaY(points, {
|
|
42
|
+
x: "date",
|
|
43
|
+
y1: "ideal",
|
|
44
|
+
y2: "pending",
|
|
45
|
+
// --color-green-600/--color-red-600 由头部状态文案的 text-* 工具类保活
|
|
46
|
+
//(Tailwind v4 会 tree-shake 未被引用的调色板变量)
|
|
47
|
+
fill: leading ? "var(--color-green-600)" : "var(--color-red-600)",
|
|
48
|
+
fillOpacity: 0.15,
|
|
49
|
+
}),
|
|
50
|
+
lineY(points, {
|
|
51
|
+
x: "date",
|
|
52
|
+
y: "ideal",
|
|
53
|
+
stroke: "var(--muted-foreground)",
|
|
54
|
+
strokeWidth: 1.5,
|
|
55
|
+
strokeDasharray: "4 3",
|
|
56
|
+
}),
|
|
57
|
+
lineY(points, { x: "date", y: "pending", stroke: "var(--primary)", strokeWidth: 2 }),
|
|
58
|
+
dot(points, { x: "date", y: "pending", r: 2.5, fill: "var(--primary)" }),
|
|
59
|
+
],
|
|
60
|
+
scales: {
|
|
61
|
+
x: { scale: () => scalePoint<string>().padding(0) },
|
|
62
|
+
y: { scale: scaleLinear().domain([0, maxVal]) },
|
|
63
|
+
},
|
|
64
|
+
svgAnimation: true,
|
|
65
|
+
tooltip,
|
|
66
|
+
});
|
|
67
|
+
}, [data.points, leading]);
|
|
21
68
|
if (data.points.length === 0) {
|
|
22
69
|
return <div className="flex h-48 items-center justify-center text-xs text-muted-foreground">{m.iterations_burndown_noData()}</div>;
|
|
23
70
|
}
|
|
24
|
-
const
|
|
25
|
-
const
|
|
26
|
-
const x = (i: number) => PAD + (i * (W - PAD * 2)) / Math.max(1, data.points.length - 1);
|
|
27
|
-
const y = (v: number) => H - PAD - (v * (H - PAD * 2)) / maxVal;
|
|
28
|
-
const line = (key: "pending" | "ideal") => data.points.map((p, i) => `${i === 0 ? "M" : "L"}${x(i)},${y(p[key])}`).join(" ");
|
|
29
|
-
const fill = data.points.map((p, i) => `${i === 0 ? "M" : "L"}${x(i)},${y(p.pending)}`).join(" ")
|
|
30
|
-
+ " " + [...data.points].reverse().map((p, i) => `L${x(data.points.length - 1 - i)},${y(p.ideal)}`).join(" ");
|
|
31
|
-
const leading = data.status === "leading";
|
|
71
|
+
const first = data.points[0];
|
|
72
|
+
const last = data.points.at(-1)!;
|
|
32
73
|
return (
|
|
33
74
|
<div className="space-y-2">
|
|
34
75
|
<div className="flex items-center justify-between text-xs">
|
|
@@ -37,21 +78,11 @@ export function BurndownChart({ data }: { data: BurndownResult & { timezone: str
|
|
|
37
78
|
{leading ? m.iterations_burndown_leading() : data.status === "trailing" ? m.iterations_burndown_trailing() : m.iterations_burndown_pending()}
|
|
38
79
|
</span>
|
|
39
80
|
</div>
|
|
40
|
-
<
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
<path d={line("ideal")} className="stroke-muted-foreground" strokeWidth={1.5} strokeDasharray="4 3" fill="none" />
|
|
46
|
-
<path d={line("pending")} className="stroke-primary" strokeWidth={2} fill="none" />
|
|
47
|
-
{data.points.map((p, i) => (
|
|
48
|
-
<circle key={p.date} cx={x(i)} cy={y(p.pending)} r={2.5} className="fill-primary">
|
|
49
|
-
<title>{`${p.date}: ${p.pending}`}</title>
|
|
50
|
-
</circle>
|
|
51
|
-
))}
|
|
52
|
-
<text x={PAD} y={H - 8} className="fill-muted-foreground text-[10px]">{data.points[0].date}</text>
|
|
53
|
-
<text x={W - PAD} y={H - 8} textAnchor="end" className="fill-muted-foreground text-[10px]">{data.points.at(-1)!.date}</text>
|
|
54
|
-
</svg>
|
|
81
|
+
<Chart definition={definition} height={200} ariaLabel={m.iterations_burndown_title()} />
|
|
82
|
+
<div className="flex justify-between text-xs text-muted-foreground">
|
|
83
|
+
<span>{first.date}</span>
|
|
84
|
+
<span>{last.date}</span>
|
|
85
|
+
</div>
|
|
55
86
|
<div className="flex gap-4 text-xs text-muted-foreground">
|
|
56
87
|
<span className="flex items-center gap-1"><span className="inline-block h-0.5 w-4 bg-primary" />{m.iterations_burndown_pending()}</span>
|
|
57
88
|
<span className="flex items-center gap-1"><span className="inline-block h-0.5 w-4 bg-muted-foreground" style={{ backgroundImage: "linear-gradient(90deg, currentColor 50%, transparent 50%)", backgroundSize: "4px 2px" }} />{m.iterations_burndown_ideal()}</span>
|
|
@@ -108,6 +108,31 @@ vi.mock('@/components/shared-kit/auth/auth-client', () => ({
|
|
|
108
108
|
useSession: mocks.useSession,
|
|
109
109
|
}))
|
|
110
110
|
|
|
111
|
+
// KnowledgeDocEditor 挂载 use-unsaved-guard(任务 2):无 RouterProvider 的
|
|
112
|
+
// 单测环境把 useBlocker 替换为 idle resolver(真实 blocker 注册属 router
|
|
113
|
+
// 集成行为,见 hooks/__tests__/use-unsaved-guard.test.tsx);opts 捕获槽供
|
|
114
|
+
// dirty 信号回归测试直接驱动 shouldBlockFn
|
|
115
|
+
const blockerCapture = vi.hoisted(() => ({
|
|
116
|
+
opts: null as null | {
|
|
117
|
+
shouldBlockFn: (args: unknown) => boolean | Promise<boolean>
|
|
118
|
+
enableBeforeUnload: boolean | (() => boolean)
|
|
119
|
+
},
|
|
120
|
+
}))
|
|
121
|
+
vi.mock('@tanstack/react-router', async (importOriginal) => ({
|
|
122
|
+
...(await importOriginal<Record<string, unknown>>()),
|
|
123
|
+
useBlocker: (opts: NonNullable<typeof blockerCapture.opts>) => {
|
|
124
|
+
blockerCapture.opts = opts
|
|
125
|
+
return {
|
|
126
|
+
status: 'idle' as const,
|
|
127
|
+
current: undefined,
|
|
128
|
+
next: undefined,
|
|
129
|
+
action: undefined,
|
|
130
|
+
proceed: undefined,
|
|
131
|
+
reset: undefined,
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
}))
|
|
135
|
+
|
|
111
136
|
// 预热 lazy 的 NotionDocEditor 动态 import(模块图大,渲染前先解析完)
|
|
112
137
|
const notionLikeReady = import('@/components/tiptap/notion-like')
|
|
113
138
|
|
|
@@ -540,3 +565,74 @@ function bannerText(testId: string): string | null {
|
|
|
540
565
|
}
|
|
541
566
|
const screenBannerText = () => bannerText('collab-offline-banner')
|
|
542
567
|
const connectingBannerText = () => bannerText('collab-connecting-banner')
|
|
568
|
+
|
|
569
|
+
// ---------------------------------------------------------------------------
|
|
570
|
+
// 未保存拦截 dirty 信号(任务 2 审查修复回归)
|
|
571
|
+
// ① NotionLikeEditor onChange 经 EDITOR_CHANGE_DEBOUNCE_MS=500 防抖才通知
|
|
572
|
+
// 父级 setDirty——最后一次击键后 500ms 内导航 guard 读到 false,内容静默
|
|
573
|
+
// 丢失。修复:击键同步置位 pendingChangeRef,guard 读「同步 ref || dirty」。
|
|
574
|
+
// ② 文档刚打开时 UniqueID onCreate 的初始化 ID 归一化触发一次 onChange →
|
|
575
|
+
// setDirty(true),init 保存(~2.5s)完成前导航误弹「未保存」。修复:
|
|
576
|
+
// 无同步击键信号的 init 批次免 dirty(防抖保存照常,ID 持久化保留)。
|
|
577
|
+
// ---------------------------------------------------------------------------
|
|
578
|
+
|
|
579
|
+
describe('未保存拦截 dirty 信号(审查修复回归)', () => {
|
|
580
|
+
const SHOULD_BLOCK_ARGS = { action: 'PUSH', current: {}, next: {} }
|
|
581
|
+
|
|
582
|
+
it('① 击键后 500ms onChange 防抖窗口内导航必须 blocked(同步置位),落库完成后归零', async () => {
|
|
583
|
+
const { editor } = await renderUntilEditorReady({ collab: false })
|
|
584
|
+
expect(editor).not.toBeNull()
|
|
585
|
+
// init 批次已随 ready 轮询处理完(无击键不拦——② 的断言前提)
|
|
586
|
+
expect(blockerCapture.opts?.shouldBlockFn(SHOULD_BLOCK_ARGS)).toBe(false)
|
|
587
|
+
|
|
588
|
+
await insertText(editor!, 'X')
|
|
589
|
+
// 关键断言:onChange 500ms 防抖尚未到期,guard 已须拦截(同步置位)
|
|
590
|
+
expect(blockerCapture.opts?.shouldBlockFn(SHOULD_BLOCK_ARGS)).toBe(true)
|
|
591
|
+
|
|
592
|
+
// 防抖落库完成后归零(保存成功清 dirty + 同步 ref,无缝衔接)
|
|
593
|
+
await act(async () => {
|
|
594
|
+
await vi.advanceTimersByTimeAsync(500)
|
|
595
|
+
await vi.advanceTimersByTimeAsync(2000)
|
|
596
|
+
await Promise.resolve()
|
|
597
|
+
})
|
|
598
|
+
expect(mockUpdateDocumentFn).toHaveBeenCalled()
|
|
599
|
+
expect(blockerCapture.opts?.shouldBlockFn(SHOULD_BLOCK_ARGS)).toBe(false)
|
|
600
|
+
})
|
|
601
|
+
|
|
602
|
+
it('② 打开后 init 归一化(UniqueID)保存完成前导航必须不 blocked', async () => {
|
|
603
|
+
const { editor } = await renderUntilEditorReady({ collab: false })
|
|
604
|
+
expect(editor).not.toBeNull()
|
|
605
|
+
expect(blockerCapture.opts?.shouldBlockFn(SHOULD_BLOCK_ARGS)).toBe(false)
|
|
606
|
+
// init onChange 交付(500ms 防抖到期)→ init 保存(2s 防抖)完成前:
|
|
607
|
+
// 纯 init 批次免 dirty,guard 不拦(修复前此处 dirty=true 误拦——审查②)
|
|
608
|
+
await act(async () => {
|
|
609
|
+
await vi.advanceTimersByTimeAsync(500)
|
|
610
|
+
await Promise.resolve()
|
|
611
|
+
})
|
|
612
|
+
expect(blockerCapture.opts?.shouldBlockFn(SHOULD_BLOCK_ARGS)).toBe(false)
|
|
613
|
+
// init 防抖保存照常落库(ID 持久化行为保留),期间仍不拦
|
|
614
|
+
await act(async () => {
|
|
615
|
+
await vi.advanceTimersByTimeAsync(2000)
|
|
616
|
+
await Promise.resolve()
|
|
617
|
+
})
|
|
618
|
+
expect(mockUpdateDocumentFn).toHaveBeenCalled()
|
|
619
|
+
expect(blockerCapture.opts?.shouldBlockFn(SHOULD_BLOCK_ARGS)).toBe(false)
|
|
620
|
+
})
|
|
621
|
+
|
|
622
|
+
it('④ 降级期击键(未落库)→ 恢复协同:pendingChangeRef 归零,guard 不 dirty', async () => {
|
|
623
|
+
// offline 降级:DB 链路接管,击键同步置位 pendingChangeRef(collabActive=false)
|
|
624
|
+
fakeProvider = buildFakeProvider('offline')
|
|
625
|
+
const { editor } = await renderUntilEditorReady({ collab: true, collabSnapshot: null })
|
|
626
|
+
expect(editor).not.toBeNull()
|
|
627
|
+
|
|
628
|
+
await insertText(editor!, '降级期编辑')
|
|
629
|
+
expect(blockerCapture.opts?.shouldBlockFn(SHOULD_BLOCK_ARGS)).toBe(true)
|
|
630
|
+
|
|
631
|
+
// 2s 防抖保存到期前恢复协同(审查④:恢复分支漏归零 → 该实例存活期
|
|
632
|
+
// guard 恒 dirty——协同态保存被 guard 短路,唯一归零点永不执行)
|
|
633
|
+
await act(async () => {
|
|
634
|
+
fakeProvider!.__setStatus('synced')
|
|
635
|
+
})
|
|
636
|
+
expect(blockerCapture.opts?.shouldBlockFn(SHOULD_BLOCK_ARGS)).toBe(false)
|
|
637
|
+
})
|
|
638
|
+
})
|
|
@@ -31,6 +31,7 @@ import { useEditorWidth, type EditorWidth, EDITOR_WIDTH_MAX_WIDTH } from '@/comp
|
|
|
31
31
|
import { DOC_EDITOR_WIDTH_VARS } from './knowledge-doc-editor-vars'
|
|
32
32
|
import { DocHeader } from '@/components/knowledge/editor/doc-header'
|
|
33
33
|
import { useAutoSaveChannels } from '@/components/knowledge/editor/use-auto-save-channels'
|
|
34
|
+
import { useUnsavedGuard } from '@/hooks/use-unsaved-guard'
|
|
34
35
|
import { DocumentProperties, type PropertyField, type PropertyValues } from '@/components/knowledge/doc-properties'
|
|
35
36
|
import type { NotionDocEditorProps, DocumentSuggestion } from '@/components/tiptap/notion-like'
|
|
36
37
|
import type { MindmapDocEditorProps } from '@/components/knowledge/editor/mindmap-doc-editor'
|
|
@@ -304,7 +305,7 @@ function KnowledgeDocEditorBody(bodyProps: KnowledgeDocEditorProps & { collabEna
|
|
|
304
305
|
},
|
|
305
306
|
}), [document.kbId])
|
|
306
307
|
|
|
307
|
-
const { setDirty, triggerDebouncedSave } = useAutoSaveChannels<{ html: string; markdown: string }>({
|
|
308
|
+
const { dirty, setDirty, triggerDebouncedSave, saveIfContent } = useAutoSaveChannels<{ html: string; markdown: string }>({
|
|
308
309
|
getContent: () => latestContent.current,
|
|
309
310
|
// 协同写手活跃时 DB 保存一律短路(Y 是唯一写手;降级期恢复前绝不双写)
|
|
310
311
|
guard: () => collabActiveRef.current,
|
|
@@ -314,6 +315,8 @@ function KnowledgeDocEditorBody(bodyProps: KnowledgeDocEditorProps & { collabEna
|
|
|
314
315
|
const saver = saveHandlers?.content ?? ((c: { html: string; markdown: string }) => updateDocumentFn({ data: { id: docId.current, content: c } }))
|
|
315
316
|
await saver(content)
|
|
316
317
|
setDirty(false)
|
|
318
|
+
// 同步击键信号与 dirty 同点归零(任务 2 审查修复①):无接缝盲区
|
|
319
|
+
pendingChangeRef.current = false
|
|
317
320
|
setLastSavedAt(new Date())
|
|
318
321
|
} catch (e) {
|
|
319
322
|
console.error('Auto-save failed:', e instanceof Error ? e.message : String(e))
|
|
@@ -323,6 +326,38 @@ function KnowledgeDocEditorBody(bodyProps: KnowledgeDocEditorProps & { collabEna
|
|
|
323
326
|
},
|
|
324
327
|
})
|
|
325
328
|
|
|
329
|
+
// ── 未保存拦截 dirty 信号(任务 2 审查修复①②)──────────────────────
|
|
330
|
+
// pendingChangeRef:击键同步置位(NotionDocEditor.onDirtySignal,先于
|
|
331
|
+
// 500ms onChange 防抖),保存成功时与 dirty 同点归零——消灭「最后一次
|
|
332
|
+
// 击键后 500ms 内导航不拦」的数据丢失窗口。协同态不置位(Y 是唯一写手)。
|
|
333
|
+
// sawFirstChangeRef:挂载后首个 onChange 若无同步击键信号,即 UniqueID
|
|
334
|
+
// 初始化 ID 归一化批次(appendTransaction 插件事务,非用户编辑)——照常
|
|
335
|
+
// 刷新 latestContent 并武装防抖保存(ID 持久化行为不变),但不置 dirty,
|
|
336
|
+
// init 保存 settle 前 guard 保持 clean(审查修复②:打开即误拦)。
|
|
337
|
+
const pendingChangeRef = useRef(false)
|
|
338
|
+
const sawFirstChangeRef = useRef(false)
|
|
339
|
+
|
|
340
|
+
// SPA 导航未保存拦截(任务 2):dirty 时弹 AlertDialog,「离开」先 flush
|
|
341
|
+
// 尽力保存(saveIfContent)再放行——比现状(SPA 导航静默丢防抖窗口内容)
|
|
342
|
+
// 更安全;浏览器关闭/刷新的通道三尽力保存仍由 useAutoSaveChannels 保留
|
|
343
|
+
const unsavedGuard = useUnsavedGuard({
|
|
344
|
+
isDirty: () => pendingChangeRef.current || dirty,
|
|
345
|
+
onSaveAndLeave: () => {
|
|
346
|
+
// flush 前从编辑器实例取回防抖窗口内未交付的最新内容(可能含
|
|
347
|
+
// <500ms 击键),否则「离开」保存的是 stale latestContent
|
|
348
|
+
const ed = editorRef.current
|
|
349
|
+
if (ed && !ed.isDestroyed) {
|
|
350
|
+
latestContent.current = {
|
|
351
|
+
html: ed.getHTML(),
|
|
352
|
+
markdown:
|
|
353
|
+
(ed.storage.markdown as { manager?: { serialize?: (json: unknown) => string } } | undefined)
|
|
354
|
+
?.manager?.serialize?.(ed.getJSON()) ?? '',
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
return saveIfContent()
|
|
358
|
+
},
|
|
359
|
+
})
|
|
360
|
+
|
|
326
361
|
// 降级/恢复切换语义(核心不变式:任一时刻只有一条保存链路活跃):
|
|
327
362
|
// 切 offline 瞬间——把编辑器当前内容(含协同期 Y 独占、未落库的编辑)
|
|
328
363
|
// 搬进 DB 保存链路的 latestContent 并置 dirty,同时武装 2s 防抖保存,
|
|
@@ -347,6 +382,10 @@ function KnowledgeDocEditorBody(bodyProps: KnowledgeDocEditorProps & { collabEna
|
|
|
347
382
|
} else {
|
|
348
383
|
latestContent.current = null
|
|
349
384
|
setDirty(false)
|
|
385
|
+
// 恢复协同必须同点归零同步击键信号(审查修复④):降级期击键置位后
|
|
386
|
+
// 若防抖保存到期前恢复,协同态保存被短路、保存成功归零点永不执行,
|
|
387
|
+
// 该实例存活期 guard 恒 dirty
|
|
388
|
+
pendingChangeRef.current = false
|
|
350
389
|
}
|
|
351
390
|
}, [collabState.collabActive, collabEnabled, setDirty, triggerDebouncedSave])
|
|
352
391
|
|
|
@@ -356,6 +395,16 @@ function KnowledgeDocEditorBody(bodyProps: KnowledgeDocEditorProps & { collabEna
|
|
|
356
395
|
// 通道一守卫:协同态(含远端 update 触发的 onChange)不进 DB 保存链路
|
|
357
396
|
if (collabActiveRef.current) return
|
|
358
397
|
latestContent.current = content
|
|
398
|
+
// init 批次豁免(任务 2 审查修复②):首个 onChange 且无同步击键信号
|
|
399
|
+
// = 初始化归一化(非用户编辑),只刷新内容 + 照常武装防抖保存,不置
|
|
400
|
+
// dirty(守卫 clean);真实击键(含 init 窗口内)走正常 dirty 路径
|
|
401
|
+
if (!sawFirstChangeRef.current) {
|
|
402
|
+
sawFirstChangeRef.current = true
|
|
403
|
+
if (!pendingChangeRef.current) {
|
|
404
|
+
triggerDebouncedSave()
|
|
405
|
+
return
|
|
406
|
+
}
|
|
407
|
+
}
|
|
359
408
|
setDirty(true)
|
|
360
409
|
triggerDebouncedSave()
|
|
361
410
|
},
|
|
@@ -449,6 +498,7 @@ function KnowledgeDocEditorBody(bodyProps: KnowledgeDocEditorProps & { collabEna
|
|
|
449
498
|
{m.docEditor_saving()}
|
|
450
499
|
</div>
|
|
451
500
|
)}
|
|
501
|
+
{unsavedGuard.dialog}
|
|
452
502
|
<div className="flex flex-col h-full" style={DOC_EDITOR_WIDTH_VARS[editorWidth] as React.CSSProperties}>
|
|
453
503
|
<div className="px-20 pt-6 pb-2 mx-auto w-full" style={{ maxWidth: EDITOR_WIDTH_MAX_WIDTH[editorWidth] }}>
|
|
454
504
|
<DocHeader
|
|
@@ -493,6 +543,11 @@ function KnowledgeDocEditorBody(bodyProps: KnowledgeDocEditorProps & { collabEna
|
|
|
493
543
|
<NotionDocEditor
|
|
494
544
|
content={collabState.config ? undefined : content}
|
|
495
545
|
onChange={handleContentChange}
|
|
546
|
+
onDirtySignal={() => {
|
|
547
|
+
// 同步击键信号(审查修复①):协同态不置位(Y 是唯一写手,
|
|
548
|
+
// 与通道一守卫同口径)
|
|
549
|
+
if (!collabActiveRef.current) pendingChangeRef.current = true
|
|
550
|
+
}}
|
|
496
551
|
placeholder={m.docEditor_contentPlaceholder()}
|
|
497
552
|
showTocSidebar
|
|
498
553
|
kbId={document.kbId}
|
|
@@ -23,6 +23,7 @@ import { DocHeader } from "@/components/knowledge/editor/doc-header"
|
|
|
23
23
|
import { parseMindmapContent, createDefaultMindmapData } from "@/components/tiptap/mindmap/lib/default-mindmap-data"
|
|
24
24
|
import type { MindElixirData } from "mind-elixir"
|
|
25
25
|
import { m } from "@/paraglide/messages.js";
|
|
26
|
+
import { useUnsavedGuard } from "@/hooks/use-unsaved-guard";
|
|
26
27
|
|
|
27
28
|
const MindMapLazy = lazy(() =>
|
|
28
29
|
import("@/components/tiptap/mindmap/mind-map").then((m) => ({ default: m.MindMap })),
|
|
@@ -93,6 +94,16 @@ export function MindmapDocEditor({ document, kbName }: MindmapDocEditorProps) {
|
|
|
93
94
|
{ wait: EDITOR_AUTO_SAVE_MS },
|
|
94
95
|
)
|
|
95
96
|
|
|
97
|
+
// SPA 导航未保存拦截(任务 2):「离开」先 flush 当前内容(doSaveRef)再
|
|
98
|
+
// 放行;下方 beforeunload 尽力保存保留(浏览器关闭/刷新兜底)
|
|
99
|
+
const unsavedGuard = useUnsavedGuard({
|
|
100
|
+
isDirty: () => dirty,
|
|
101
|
+
onSaveAndLeave: () => {
|
|
102
|
+
const c = latestContent.current
|
|
103
|
+
if (c) return doSaveRef.current(c)
|
|
104
|
+
},
|
|
105
|
+
})
|
|
106
|
+
|
|
96
107
|
const handleChange = useCallback((newData: MindElixirData, _operation: unknown) => {
|
|
97
108
|
latestContent.current = newData
|
|
98
109
|
setMindmapData(newData)
|
|
@@ -143,6 +154,7 @@ export function MindmapDocEditor({ document, kbName }: MindmapDocEditorProps) {
|
|
|
143
154
|
{m.mindmap_saving()}
|
|
144
155
|
</div>
|
|
145
156
|
)}
|
|
157
|
+
{unsavedGuard.dialog}
|
|
146
158
|
<div className="px-20 pt-6 pb-2 mx-auto w-full" style={{ maxWidth: headerMaxWidth }}>
|
|
147
159
|
<DocHeader
|
|
148
160
|
title={displayTitle}
|
|
@@ -89,5 +89,7 @@ export function useAutoSaveChannels<T>(opts: AutoSaveChannelsOptions<T>) {
|
|
|
89
89
|
return () => window.removeEventListener("beforeunload", handleBeforeUnload)
|
|
90
90
|
}, [saveIfContent])
|
|
91
91
|
|
|
92
|
-
|
|
92
|
+
// saveIfContent 一并透出:use-unsaved-guard 的「离开」flush(任务 2)与
|
|
93
|
+
// 通道三复用同一保存入口
|
|
94
|
+
return { dirty, setDirty, triggerDebouncedSave, saveIfContent }
|
|
93
95
|
}
|
|
@@ -53,10 +53,6 @@ vi.mock("@/lib/utils", () => ({
|
|
|
53
53
|
cn: (...classes: string[]) => classes.filter(Boolean).join(" "),
|
|
54
54
|
}));
|
|
55
55
|
|
|
56
|
-
vi.mock("@/lib/collections/reference/workspaces-collection", () => ({
|
|
57
|
-
WORKSPACES_QUERY_KEY: ["workspaces"],
|
|
58
|
-
}));
|
|
59
|
-
|
|
60
56
|
vi.mock("@/server/serverFns/workspace", () => ({
|
|
61
57
|
listPinnedWorkspacesBriefFn: vi.fn().mockResolvedValue([]),
|
|
62
58
|
toggleWorkspacePinFn: vi.fn().mockResolvedValue({ success: true }),
|