@allbluecn/web-app 0.19.5 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -3
- package/src/components/bug/__tests__/board-subgroups.test.tsx +1 -1
- package/src/components/bug/__tests__/board-view.test.tsx +1 -1
- package/src/components/bug/__tests__/comment-reactions.test.tsx +1 -1
- package/src/components/bug/__tests__/create-bug-dialog.test.tsx +2 -2
- package/src/components/bug/__tests__/filters-compat.test.tsx +1 -1
- package/src/components/bug/__tests__/fix-request-race.test.tsx +1 -1
- package/src/components/bug/__tests__/peek-modes.test.tsx +1 -1
- package/src/components/bug/__tests__/use-paged-bug-groups.test.tsx +1 -1
- package/src/components/bug/__tests__/view-type.test.tsx +1 -1
- package/src/components/bug/bug-list-view.tsx +2 -2
- package/src/components/bug/bug-toolbar.tsx +1 -1
- package/src/components/bug/create-bug-dialog.tsx +3 -3
- package/src/components/bug/detail/__tests__/activity-timeline.test.tsx +1 -1
- package/src/components/bug/detail/__tests__/bug-detail-collab.test.tsx +1 -1
- package/src/components/bug/detail/__tests__/repro-report-card.test.tsx +1 -1
- package/src/components/bug/detail/activity-tabs.tsx +34 -3
- package/src/components/bug/detail/activity-timeline.tsx +1 -1
- package/src/components/bug/detail/bug-detail-body.tsx +3 -3
- package/src/components/bug/detail/comment-input.tsx +1 -1
- package/src/components/bug/detail/comment-item.tsx +1 -1
- package/src/components/bug/detail/comment-reactions.tsx +1 -1
- package/src/components/bug/detail/comment-thread.tsx +1 -1
- package/src/components/bug/detail/props-section.tsx +2 -2
- package/src/components/bug/detail/repro-report-card.tsx +1 -1
- package/src/components/bug/detail/reproduce-steps-view.tsx +1 -1
- package/src/components/bug/display/use-paged-bug-groups.ts +2 -2
- package/src/components/bug/filters/field-configs.tsx +1 -1
- package/src/components/bug/inline-editors/assignee-editor.tsx +1 -1
- package/src/components/bug/peek-panel.tsx +2 -2
- package/src/components/bug/reproduce-steps-editor.tsx +1 -1
- package/src/components/bug/story-bugs-panel.tsx +10 -6
- package/src/components/bug/views/board-view.tsx +2 -2
- package/src/components/bug/views/bug-list/batch-toolbar.tsx +1 -1
- package/src/components/bug/views/bug-list/group.tsx +1 -1
- package/src/components/bug/views/bug-list/index.tsx +1 -1
- package/src/components/bug/views/bug-list/inline-bug-creator.tsx +1 -1
- package/src/components/bug/views/bug-list/row.tsx +2 -2
- package/src/components/bug/views/create-view-dialog.tsx +1 -1
- package/src/components/design/editor/versions/hooks/use-design-versions.ts +11 -4
- package/src/components/design/editor/versions/page-version-panel.tsx +30 -11
- package/src/components/harness/code/workspace-context.tsx +5 -1
- package/src/components/harness/code/workspace-picker.tsx +19 -6
- package/src/components/harness/composer/harness-composer.tsx +25 -3
- package/src/components/harness/composer/mention/mention-resolvers.ts +2 -0
- package/src/components/harness/composer/mention/mention-suggestion-menu.tsx +6 -3
- package/src/components/harness/composer/mention/use-mention-sources.ts +1 -1
- package/src/components/iteration/iteration-progress-bar.tsx +2 -2
- package/src/components/knowledge/public/legal-compliance-editor.tsx +1 -1
- package/src/components/layout/sidebar-03/nav-knowledge.tsx +1 -1
- package/src/components/layout/sidebar-03/nav-workspace.tsx +1 -1
- package/src/components/project/invite-member-dialog.tsx +2 -2
- package/src/components/project/project-member-manager.tsx +0 -3
- package/src/components/shared-kit/list-kit/kanban/__tests__/instance-key.test.ts +1 -1
- package/src/components/story/__tests__/constants.test.ts +1 -1
- package/src/components/story/__tests__/dependency-type-badge.test.ts +1 -1
- package/src/components/story/__tests__/peek-panel-patch-sync.test.tsx +1 -1
- package/src/components/story/__tests__/story-list-view-peek-sync.test.tsx +1 -1
- package/src/components/story/ai/story-ai-panel.tsx +1 -1
- package/src/components/story/ai/story-ai-preview-list.tsx +2 -2
- package/src/components/story/ai/use-story-ai-model.ts +17 -4
- package/src/components/story/columns.tsx +2 -2
- package/src/components/story/create-story-dialog.tsx +4 -4
- package/src/components/story/detail/__tests__/activity-timeline.test.tsx +1 -1
- package/src/components/story/detail/__tests__/associated-attachments-panel.test.tsx +1 -1
- package/src/components/story/detail/__tests__/story-collab.test.tsx +1 -1
- package/src/components/story/detail/activity-tabs.tsx +1 -1
- package/src/components/story/detail/activity-timeline.tsx +36 -17
- package/src/components/story/detail/comment-input.tsx +1 -1
- package/src/components/story/detail/comment-item.tsx +1 -1
- package/src/components/story/detail/comment-thread.tsx +1 -1
- package/src/components/story/detail/reaction-bar.tsx +1 -1
- package/src/components/story/detail/story-detail-body.tsx +4 -4
- package/src/components/story/detail/story-detail-toolbar/__tests__/attachment-cache.test.ts +83 -0
- package/src/components/story/detail/story-detail-toolbar/associated-attachments-panel.tsx +50 -9
- package/src/components/story/detail/story-detail-toolbar/children.tsx +3 -3
- package/src/components/story/detail/story-detail-toolbar/dependency-panel.tsx +9 -8
- package/src/components/story/detail/story-detail-toolbar/index.tsx +8 -8
- package/src/components/story/detail/story-detail-toolbar/related-test-cases.tsx +2 -2
- package/src/components/story/detail/story-properties.tsx +2 -2
- package/src/components/story/display/__tests__/flatten-story-groups.test.ts +1 -1
- package/src/components/story/display/__tests__/group-fields.test.ts +1 -1
- package/src/components/story/display/__tests__/group-resolver.test.ts +2 -2
- package/src/components/story/display/__tests__/order-resolver.test.ts +1 -1
- package/src/components/story/display/__tests__/use-grouped-stories.test.ts +1 -1
- package/src/components/story/display/__tests__/use-paged-story-groups.test.ts +1 -1
- package/src/components/story/display/flatten-story-groups.ts +1 -1
- package/src/components/story/display/group-fields.ts +2 -2
- package/src/components/story/display/group-resolver.tsx +4 -4
- package/src/components/story/display/order-resolver.ts +3 -3
- package/src/components/story/display/types.ts +5 -4
- package/src/components/story/display/use-grouped-stories.ts +5 -5
- package/src/components/story/display/use-paged-story-groups.ts +11 -4
- package/src/components/story/entity-overlay.ts +24 -0
- package/src/components/story/filters/field-configs.tsx +2 -2
- package/src/components/story/inline-editors/assignee-editor.tsx +1 -1
- package/src/components/story/inline-editors/dependency-type-select.tsx +1 -1
- package/src/components/story/inline-editors/parent-story-editor.tsx +2 -2
- package/src/components/story/inline-editors/priority-editor.tsx +2 -2
- package/src/components/story/inline-editors/state-editor.tsx +2 -2
- package/src/components/story/inline-editors/story-select-dialog.tsx +2 -2
- package/src/components/story/peek-panel.tsx +6 -5
- package/src/components/story/priority-icon.tsx +2 -2
- package/src/components/story/relations/__tests__/attachment-manage-dialog.test.tsx +1 -1
- package/src/components/story/relations/__tests__/design-select-dialog.test.tsx +1 -1
- package/src/components/story/relations/__tests__/knowledge-select-dialog.test.tsx +1 -1
- package/src/components/story/relations/__tests__/link-manage-dialog.test.tsx +1 -1
- package/src/components/story/relations/attachment-manage-dialog.tsx +4 -4
- package/src/components/story/relations/design-select-dialog.tsx +2 -2
- package/src/components/story/relations/knowledge-select-dialog.tsx +2 -2
- package/src/components/story/relations/link-manage-dialog.tsx +2 -2
- package/src/components/story/relations/story-ref-row.tsx +3 -3
- package/src/components/story/story-list-table.tsx +1 -1
- package/src/components/story/story-list-view.tsx +1 -1
- package/src/components/story/story-toolbar.tsx +1 -1
- package/src/components/story/transfer-story-popover.tsx +3 -2
- package/src/components/story/views/__tests__/calendar-view.test.tsx +1 -1
- package/src/components/story/views/__tests__/kanban-drop-subgroup.test.ts +1 -1
- package/src/components/story/views/__tests__/kanban-peek-sync.test.tsx +2 -2
- package/src/components/story/views/__tests__/kanban-view-membership-sync.test.tsx +1 -1
- package/src/components/story/views/__tests__/timeline-view.test.tsx +1 -1
- package/src/components/story/views/calendar/__tests__/mappers.test.ts +1 -1
- package/src/components/story/views/calendar/mappers.ts +1 -1
- package/src/components/story/views/calendar-view.tsx +1 -1
- package/src/components/story/views/create-view-dialog.tsx +1 -1
- package/src/components/story/views/kanban-view.tsx +2 -2
- package/src/components/story/views/story-list/batch-preview-dialog.tsx +2 -2
- package/src/components/story/views/story-list/batch-toolbar.tsx +3 -3
- package/src/components/story/views/story-list/group.tsx +18 -5
- package/src/components/story/views/story-list/index.tsx +4 -2
- package/src/components/story/views/story-list/inline-story-creator.tsx +2 -2
- package/src/components/story/views/story-list/row.tsx +2 -2
- package/src/components/story/views/story-list/use-flat-rows.ts +1 -1
- package/src/components/story/views/timeline/__tests__/mappers.test.ts +2 -2
- package/src/components/story/views/timeline/mappers.ts +2 -2
- package/src/components/story/views/timeline/use-date-commit.ts +1 -1
- package/src/components/story/views/timeline-view.tsx +2 -2
- package/src/components/testcase/__tests__/case-row.test.tsx +1 -1
- package/src/components/testcase/__tests__/create-dialog-scope.test.tsx +1 -1
- package/src/components/testcase/__tests__/display-options.test.tsx +1 -1
- package/src/components/testcase/__tests__/export-entry.test.tsx +1 -1
- package/src/components/testcase/__tests__/list-view.test.tsx +1 -1
- package/src/components/testcase/__tests__/step-editor.test.tsx +4 -2
- package/src/components/testcase/__tests__/toolbar.test.tsx +1 -1
- package/src/components/testcase/create-testcase-dialog.tsx +6 -5
- package/src/components/testcase/detail/case-detail-body.tsx +1 -1
- package/src/components/testcase/detail/properties.tsx +1 -1
- package/src/components/testcase/display/display-dropdown.tsx +2 -2
- package/src/components/testcase/display/use-testcase-display-options.ts +1 -1
- package/src/components/testcase/filters/field-configs.tsx +1 -1
- package/src/components/testcase/generation/draft-card.tsx +2 -2
- package/src/components/testcase/inline-editors/assignee-editor.tsx +1 -1
- package/src/components/testcase/list/case-row.tsx +1 -1
- package/src/components/testcase/list/grouped-list.tsx +1 -1
- package/src/components/testcase/list-view.tsx +2 -2
- package/src/components/testcase/runs/convert-bug-button.tsx +2 -2
- package/src/components/testcase/step-editor.tsx +13 -2
- package/src/components/testcase/testcase-toolbar.tsx +2 -2
- package/src/components/testcase/views/batch-toolbar.tsx +1 -1
- package/src/components/testcase/views/board-view.tsx +2 -2
- package/src/lib/changelog/sdk-versions.ts +2 -2
- package/src/lib/collections/knowledge/knowledge-bases-collection.ts +2 -1
- package/src/lib/collections/query-keys.ts +18 -0
- package/src/lib/collections/reference/users-brief-collection.ts +2 -1
- package/src/lib/collections/reference/workspaces-collection.ts +2 -1
- package/src/lib/story/__tests__/activity-grouper.test.ts +1 -1
- package/src/lib/story/activity-grouper.ts +1 -1
- package/src/paraglide/messages/_index.d.ts +2 -0
- package/src/paraglide/messages/_index.js +88269 -5642
- package/src/paraglide/messages/en.js +28255 -0
- package/src/paraglide/messages/story_group_loadfailed1.d.ts +17 -0
- package/src/paraglide/messages/story_group_loadfailed1.js +30 -0
- package/src/paraglide/messages/story_group_retry.d.ts +16 -0
- package/src/paraglide/messages/story_group_retry.js +29 -0
- package/src/paraglide/messages/zh.js +28253 -0
- package/src/routes/_nav/bugs/$bugId/route.lazy.tsx +2 -2
- package/src/routes/_nav/projects/$projectId/iterations/$iterationId/route.lazy.tsx +1 -1
- package/src/routes/_nav/route.tsx +1 -2
- package/src/routes/_nav/stories/$storyId/route.lazy.tsx +2 -2
- package/src/routes/_nav/stories/$storyId/route.tsx +1 -1
- package/src/routes/api/design/prototype/__tests__/route.test.ts +4 -1
- package/src/server/__tests__/task-executor.test.ts +7 -0
- package/src/server/ai-gateway/__tests__/ai-output-guard.test.ts +4 -4
- package/src/server/ai-gateway/__tests__/tools-authorization.test.ts +25 -0
- package/src/server/ai-gateway/tools/server.ts +24 -15
- package/src/server/knowledge/reindex-guard.ts +35 -0
- package/src/server/lib/__tests__/cron-auth.test.ts +1 -1
- package/src/server/serverFns/__tests__/legal-code-canonical.test.ts +169 -0
- package/src/server/serverFns/__tests__/story-tree.test.ts +0 -1
- package/src/server/serverFns/__tests__/story-types.test.ts +2 -1
- package/src/server/serverFns/__tests__/update-bug-guard.test.ts +1 -1
- package/src/server/serverFns/bug/__tests__/bug-comments.test.ts +95 -44
- package/src/server/serverFns/bug/__tests__/bug-crud.test.ts +46 -18
- package/src/server/serverFns/bug/__tests__/bug-groups.test.ts +1 -1
- package/src/server/serverFns/bug/bug-activities.ts +2 -2
- package/src/server/serverFns/bug/bug-comments.ts +44 -19
- package/src/server/serverFns/bug/bug-crud/detail-select.ts +1 -1
- package/src/server/serverFns/bug/bug-crud/detail.ts +2 -2
- package/src/server/serverFns/bug/bug-crud.ts +142 -110
- package/src/server/serverFns/bug/bug-group-query.ts +1 -1
- package/src/server/serverFns/bug/bug-groups.ts +2 -2
- package/src/server/serverFns/bug/bug-story-utils.ts +1 -1
- package/src/server/serverFns/chat/conversations.ts +27 -13
- package/src/server/serverFns/harness/github-workspace.ts +32 -25
- package/src/server/serverFns/harness/search-mention-candidates.ts +3 -1
- package/src/server/serverFns/iteration/iteration-crud.ts +1 -1
- package/src/server/serverFns/knowledge/public/legal/create.ts +3 -0
- package/src/server/serverFns/knowledge/public/legal/update.ts +8 -8
- package/src/server/serverFns/knowledge-index.ts +12 -5
- package/src/server/serverFns/project.ts +5 -11
- package/src/server/serverFns/story/__tests__/story-dependencies.test.ts +15 -4
- package/src/server/serverFns/story/__tests__/story-transfer.test.ts +20 -0
- package/src/server/serverFns/story/link-preview.ts +3 -2
- package/src/server/serverFns/story/story-children.ts +1 -1
- package/src/server/serverFns/story/story-comments.ts +1 -1
- package/src/server/serverFns/story/story-crud/bulk.ts +1 -1
- package/src/server/serverFns/story/story-crud.ts +1 -1
- package/src/server/serverFns/story/story-dependencies.ts +61 -69
- package/src/server/serverFns/story/story-groups.ts +1 -1
- package/src/server/serverFns/story/story-transfer.ts +8 -20
- package/src/server/serverFns/testcase/__tests__/automation.test.ts +21 -0
- package/src/server/serverFns/testcase/__tests__/testcase-groups.test.ts +1 -1
- package/src/server/serverFns/testcase/automation.ts +24 -1
- package/src/server/serverFns/testcase/testcase-board.ts +1 -1
- package/src/server/serverFns/testcase/testcase-crud.ts +1 -1
- package/src/server/serverFns/testcase/testcase-display-options.ts +1 -1
- package/src/server/serverFns/testcase/testcase-filter-views.ts +1 -1
- package/src/server/serverFns/testcase/types.ts +1 -1
- package/src/server/story/types.ts +7 -3
- package/src/shared/bug/__tests__/constants.test.ts +1 -1
- package/src/{components → shared}/bug/types.ts +4 -3
- package/src/{components → shared}/story/constants.ts +1 -1
- package/src/{components → shared}/story/types.ts +1 -7
- package/src/types/project.ts +1 -2
- /package/src/{components/testcase/display → shared/testcase}/field-configs.ts +0 -0
|
@@ -58,6 +58,8 @@ const mocks = vi.hoisted(() => ({
|
|
|
58
58
|
bugCount: vi.fn(),
|
|
59
59
|
bugDeleteMany: vi.fn(),
|
|
60
60
|
bugActivityCreate: vi.fn(),
|
|
61
|
+
bugActivityCreateMany: vi.fn(),
|
|
62
|
+
prismaTransaction: vi.fn(),
|
|
61
63
|
projectActivityCreate: vi.fn(),
|
|
62
64
|
projectFindUnique: vi.fn(),
|
|
63
65
|
projectMemberFindUnique: vi.fn(),
|
|
@@ -86,7 +88,7 @@ vi.mock("@allbluecn/database", () => {
|
|
|
86
88
|
count: mocks.bugCount,
|
|
87
89
|
deleteMany: mocks.bugDeleteMany,
|
|
88
90
|
},
|
|
89
|
-
bugActivity: { create: mocks.bugActivityCreate },
|
|
91
|
+
bugActivity: { create: mocks.bugActivityCreate, createMany: mocks.bugActivityCreateMany },
|
|
90
92
|
projectActivity: { create: mocks.projectActivityCreate },
|
|
91
93
|
project: { findUnique: mocks.projectFindUnique },
|
|
92
94
|
projectMember: { findUnique: mocks.projectMemberFindUnique, findMany: mocks.projectMemberFindMany },
|
|
@@ -102,10 +104,15 @@ vi.mock("@allbluecn/database", () => {
|
|
|
102
104
|
updateMany: mocks.collabDocumentUpdateMany,
|
|
103
105
|
deleteMany: mocks.collabDocumentDeleteMany,
|
|
104
106
|
},
|
|
105
|
-
// $transaction
|
|
106
|
-
$transaction:
|
|
107
|
-
Array.isArray(arg) ? Promise.all(arg) : arg(prisma),
|
|
107
|
+
// $transaction spy(默认实现见下方 mockImplementation;S40 批回滚用例可 once 覆写)
|
|
108
|
+
$transaction: mocks.prismaTransaction,
|
|
108
109
|
};
|
|
110
|
+
// $transaction 默认实现:数组形态 Promise.all;交互式形态回调传同一 prisma
|
|
111
|
+
mocks.prismaTransaction.mockImplementation((arg: unknown) =>
|
|
112
|
+
Array.isArray(arg)
|
|
113
|
+
? Promise.all(arg as Promise<unknown>[])
|
|
114
|
+
: (arg as (tx: typeof prisma) => unknown)(prisma),
|
|
115
|
+
);
|
|
109
116
|
return { prisma, Prisma: {} };
|
|
110
117
|
});
|
|
111
118
|
|
|
@@ -713,7 +720,7 @@ describe("bulkArchiveBugsFn", () => {
|
|
|
713
720
|
]);
|
|
714
721
|
mocks.projectMemberFindMany.mockResolvedValue([{ projectId: "p1" }]);
|
|
715
722
|
const out = await bulkArchiveBugsFn({ data: { bugIds: ["a", "b", "c"] } });
|
|
716
|
-
expect(out).toEqual({ archived: 2, skipped: 1 });
|
|
723
|
+
expect(out).toEqual({ archived: 2, skipped: 1, failed: 0 });
|
|
717
724
|
expect(mocks.projectMemberFindMany).toHaveBeenCalledWith({
|
|
718
725
|
where: { userId: "u1", projectId: { in: ["p1"] }, status: { not: "suspended" } },
|
|
719
726
|
select: { projectId: true },
|
|
@@ -723,11 +730,32 @@ describe("bulkArchiveBugsFn", () => {
|
|
|
723
730
|
select: { id: true, archived: true },
|
|
724
731
|
});
|
|
725
732
|
expect(mocks.bugUpdate).toHaveBeenCalledTimes(2);
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
733
|
+
// S40:活动随批 createMany(一条语句承载整批流水)
|
|
734
|
+
expect(mocks.bugActivityCreateMany).toHaveBeenCalledWith({
|
|
735
|
+
data: expect.arrayContaining([
|
|
736
|
+
expect.objectContaining({ bugId: "a", action: "archived" }),
|
|
737
|
+
expect.objectContaining({ bugId: "b", action: "archived" }),
|
|
738
|
+
]),
|
|
739
|
+
});
|
|
740
|
+
});
|
|
741
|
+
|
|
742
|
+
it("S40:某批事务失败不中断后续批,failed 计数返回", async () => {
|
|
743
|
+
// 101 条构出两个批次(100+1,批大小不可导出——serverFn 纯净性口径)
|
|
744
|
+
const rows = Array.from({ length: 101 }, (_, i) => ({
|
|
745
|
+
id: `bug-${i}`,
|
|
746
|
+
projectId: "p1",
|
|
747
|
+
archived: false,
|
|
748
|
+
}));
|
|
749
|
+
mockBugFindManyByScope(rows);
|
|
750
|
+
mocks.projectMemberFindMany.mockResolvedValue([{ projectId: "p1" }]);
|
|
751
|
+
mocks.bugUpdate.mockResolvedValue({ id: "x", updatedAt: new Date("2026-01-01T00:00:00Z") });
|
|
752
|
+
// 首批事务回滚;次批走默认实现(真执行回调)
|
|
753
|
+
mocks.prismaTransaction.mockRejectedValueOnce(new Error("tx fail"));
|
|
754
|
+
|
|
755
|
+
const out = await bulkArchiveBugsFn({ data: { bugIds: rows.map((r) => r.id) } });
|
|
756
|
+
expect(out).toEqual({ archived: 1, skipped: 0, failed: 100 });
|
|
757
|
+
// 首批 reject 即短路(回调不执行),仅次批的 1 条走到 update
|
|
758
|
+
expect(mocks.bugUpdate).toHaveBeenCalledTimes(1);
|
|
731
759
|
});
|
|
732
760
|
|
|
733
761
|
it("非项目成员批量归档被拒且不写库", async () => {
|
|
@@ -795,7 +823,7 @@ describe("bulkUpdateBugsFn", () => {
|
|
|
795
823
|
]);
|
|
796
824
|
mocks.projectMemberFindMany.mockResolvedValue([{ projectId: "p1" }]);
|
|
797
825
|
const out = await bulkUpdateBugsFn({ data: { bugIds: ["a", "b"], patch: { state: "in_progress" } } });
|
|
798
|
-
expect(out).toEqual({ updated: 2 });
|
|
826
|
+
expect(out).toEqual({ updated: 2, failed: 0 });
|
|
799
827
|
expect(mocks.bugUpdate).toHaveBeenCalledTimes(2);
|
|
800
828
|
expect(mocks.bugUpdate).toHaveBeenCalledWith(
|
|
801
829
|
expect.objectContaining({
|
|
@@ -803,7 +831,7 @@ describe("bulkUpdateBugsFn", () => {
|
|
|
803
831
|
data: expect.objectContaining({ state: "in_progress" }),
|
|
804
832
|
}),
|
|
805
833
|
);
|
|
806
|
-
const actions = mocks.
|
|
834
|
+
const actions = mocks.bugActivityCreateMany.mock.calls.flatMap((c) => c[0].data);
|
|
807
835
|
expect(actions).toEqual([
|
|
808
836
|
expect.objectContaining({ bugId: "a", action: "state_changed", oldValue: "open", newValue: "in_progress" }),
|
|
809
837
|
expect.objectContaining({ bugId: "b", action: "state_changed", oldValue: "fixed", newValue: "in_progress" }),
|
|
@@ -949,7 +977,7 @@ describe("bulkUpdateBugsFn × 协同门卫三态", () => {
|
|
|
949
977
|
try {
|
|
950
978
|
const out = await bulkUpdateBugsFn({ data: { bugIds: ["a", "b"], patch: { state: "in_progress" } } });
|
|
951
979
|
|
|
952
|
-
expect(out).toEqual({ updated: 2 });
|
|
980
|
+
expect(out).toEqual({ updated: 2, failed: 0 });
|
|
953
981
|
// 直写与活动剥离(collab 落库侧产出)——互吞防务本体
|
|
954
982
|
expect(mocks.bugUpdate).not.toHaveBeenCalled();
|
|
955
983
|
expect(mocks.bugActivityCreate).not.toHaveBeenCalled();
|
|
@@ -976,9 +1004,9 @@ describe("bulkUpdateBugsFn × 协同门卫三态", () => {
|
|
|
976
1004
|
try {
|
|
977
1005
|
const out = await bulkUpdateBugsFn({ data: { bugIds: ["a", "b"], patch: { state: "in_progress" } } });
|
|
978
1006
|
|
|
979
|
-
expect(out).toEqual({ updated: 2 });
|
|
1007
|
+
expect(out).toEqual({ updated: 2, failed: 0 });
|
|
980
1008
|
expect(mocks.bugUpdate).toHaveBeenCalledTimes(2);
|
|
981
|
-
expect(mocks.
|
|
1009
|
+
expect(mocks.bugActivityCreateMany).toHaveBeenCalledTimes(2);
|
|
982
1010
|
expect(vi.mocked(globalThis.fetch)).not.toHaveBeenCalled();
|
|
983
1011
|
// 直写全为 Y 属主面 → 不 touch(R11 旁路反超语义)
|
|
984
1012
|
expect(mocks.collabDocumentUpdateMany).not.toHaveBeenCalled();
|
|
@@ -996,9 +1024,9 @@ describe("bulkUpdateBugsFn × 协同门卫三态", () => {
|
|
|
996
1024
|
try {
|
|
997
1025
|
const out = await bulkUpdateBugsFn({ data: { bugIds: ["a", "b"], patch: { state: "in_progress" } } });
|
|
998
1026
|
|
|
999
|
-
expect(out).toEqual({ updated: 2 });
|
|
1027
|
+
expect(out).toEqual({ updated: 2, failed: 0 });
|
|
1000
1028
|
expect(mocks.bugUpdate).toHaveBeenCalledTimes(2);
|
|
1001
|
-
expect(mocks.
|
|
1029
|
+
expect(mocks.bugActivityCreateMany).toHaveBeenCalledTimes(2);
|
|
1002
1030
|
expect(vi.mocked(globalThis.fetch)).not.toHaveBeenCalled();
|
|
1003
1031
|
} finally {
|
|
1004
1032
|
vi.unstubAllGlobals();
|
|
@@ -1018,7 +1046,7 @@ describe("bulkUpdateBugsFn × 协同门卫三态", () => {
|
|
|
1018
1046
|
try {
|
|
1019
1047
|
const out = await bulkUpdateBugsFn({ data: { bugIds: ["a"], patch: { state: "in_progress" } } });
|
|
1020
1048
|
|
|
1021
|
-
expect(out).toEqual({ updated: 0 });
|
|
1049
|
+
expect(out).toEqual({ updated: 0, failed: 0 });
|
|
1022
1050
|
expect(fetchMock).not.toHaveBeenCalled();
|
|
1023
1051
|
expect(mocks.collabDocumentFindUnique).not.toHaveBeenCalled();
|
|
1024
1052
|
expect(mocks.bugUpdate).not.toHaveBeenCalled();
|
|
@@ -108,7 +108,7 @@ import {
|
|
|
108
108
|
DEFAULT_BUG_DISPLAY_OPTIONS,
|
|
109
109
|
} from "@/shared/bug/display-types";
|
|
110
110
|
import { BUG_STATES } from "@/shared/bug/constants";
|
|
111
|
-
import { BUG_LIST_SELECT } from "@/
|
|
111
|
+
import { BUG_LIST_SELECT } from "@/shared/bug/types";
|
|
112
112
|
import type { BugDisplayOptions } from "@/shared/bug/display-types";
|
|
113
113
|
|
|
114
114
|
const DATE = new Date("2026-01-01T00:00:00Z");
|
|
@@ -21,8 +21,8 @@ import { getRequest } from "@tanstack/react-start/server";
|
|
|
21
21
|
import { z } from "zod";
|
|
22
22
|
import { requireAuth } from "@/server/auth-helpers";
|
|
23
23
|
import type { AvatarConfig } from "@allbluecn/kernel";
|
|
24
|
-
import { BUG_USER_BRIEF_SELECT } from "@/
|
|
25
|
-
import type { BugActivityItem } from "@/
|
|
24
|
+
import { BUG_USER_BRIEF_SELECT } from "@/shared/bug/types";
|
|
25
|
+
import type { BugActivityItem } from "@/shared/bug/types";
|
|
26
26
|
import { assertBugProjectMember } from "@/server/bug/bug-permissions";
|
|
27
27
|
|
|
28
28
|
// BugActivity.userId 可空(SDK/agent 产生),user 关系 onDelete: SetNull
|
|
@@ -21,9 +21,9 @@ import { getRequest } from "@tanstack/react-start/server";
|
|
|
21
21
|
import { z } from "zod";
|
|
22
22
|
import { requireAuth } from "@/server/auth-helpers";
|
|
23
23
|
import type { AvatarConfig } from "@allbluecn/kernel";
|
|
24
|
-
import { BUG_USER_BRIEF_SELECT } from "@/
|
|
25
|
-
import type { BugCommentWithReplies } from "@/
|
|
26
|
-
import type { ReactionGroup } from "@/
|
|
24
|
+
import { BUG_USER_BRIEF_SELECT } from "@/shared/bug/types";
|
|
25
|
+
import type { BugCommentWithReplies } from "@/shared/bug/types";
|
|
26
|
+
import type { ReactionGroup } from "@/shared/story/types";
|
|
27
27
|
import { assertBugProjectMember } from "@/server/bug/bug-permissions";
|
|
28
28
|
import { aggregateReactions } from "@/server/story/types";
|
|
29
29
|
|
|
@@ -38,8 +38,18 @@ function castCommentUser(u: {
|
|
|
38
38
|
return { ...u, avatarConfig: (u.avatarConfig as AvatarConfig | null) ?? null };
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
// S39(审计 A2x ⚪):根评论游标分页——长串 bug 全量评论 + reactions 响应膨胀。
|
|
42
|
+
// 契约从裸数组扩展为 { items, nextCursor }(对齐 listBugActivitiesFn 口径);
|
|
43
|
+
// 回复树不单独分页:根页内所有回复整树随根返回,客户端渲染契约不变。
|
|
41
44
|
export const listCommentsFn = createServerFn({ method: "GET" })
|
|
42
|
-
.validator(
|
|
45
|
+
.validator(
|
|
46
|
+
z.object({
|
|
47
|
+
bugId: z.string(),
|
|
48
|
+
take: z.number().int().min(1).max(100).default(50),
|
|
49
|
+
/** 上一页最后一个根评论 id(keyset 锚点;缺省 = 第一页) */
|
|
50
|
+
cursor: z.string().optional(),
|
|
51
|
+
}),
|
|
52
|
+
)
|
|
43
53
|
.handler(async ({ data }) => {
|
|
44
54
|
const prisma = await getPrisma();
|
|
45
55
|
const request = getRequest();
|
|
@@ -48,21 +58,36 @@ export const listCommentsFn = createServerFn({ method: "GET" })
|
|
|
48
58
|
|
|
49
59
|
await assertBugProjectMember(data.bugId, userId);
|
|
50
60
|
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
61
|
+
const COMMENT_SELECT = {
|
|
62
|
+
id: true,
|
|
63
|
+
content: true,
|
|
64
|
+
parentId: true,
|
|
65
|
+
createdAt: true,
|
|
66
|
+
updatedAt: true,
|
|
67
|
+
user: { select: BUG_USER_BRIEF_SELECT },
|
|
68
|
+
reactions: { select: { emoji: true, userId: true } },
|
|
69
|
+
} as const;
|
|
70
|
+
|
|
71
|
+
// take+1 探测下一页;cursor 行用 skip:1 跳过(Prisma cursor 含锚行)
|
|
72
|
+
const roots = await prisma.bugComment.findMany({
|
|
73
|
+
where: { bugId: data.bugId, parentId: null },
|
|
74
|
+
orderBy: [{ createdAt: "asc" }, { id: "asc" }],
|
|
75
|
+
take: data.take + 1,
|
|
76
|
+
...(data.cursor ? { cursor: { id: data.cursor }, skip: 1 } : {}),
|
|
77
|
+
select: COMMENT_SELECT,
|
|
63
78
|
});
|
|
64
|
-
|
|
65
|
-
const
|
|
79
|
+
const nextCursor = roots.length > data.take ? roots[data.take].id : null;
|
|
80
|
+
const pageRoots = roots.slice(0, data.take);
|
|
81
|
+
const rootIds = pageRoots.map((r) => r.id);
|
|
82
|
+
const replies = rootIds.length
|
|
83
|
+
? await prisma.bugComment.findMany({
|
|
84
|
+
where: { bugId: data.bugId, parentId: { in: rootIds } },
|
|
85
|
+
orderBy: [{ createdAt: "asc" }, { id: "asc" }],
|
|
86
|
+
select: COMMENT_SELECT,
|
|
87
|
+
})
|
|
88
|
+
: [];
|
|
89
|
+
|
|
90
|
+
const enriched = [...pageRoots, ...replies].map((c) => ({
|
|
66
91
|
id: c.id,
|
|
67
92
|
content: c.content,
|
|
68
93
|
parentId: c.parentId,
|
|
@@ -87,7 +112,7 @@ export const listCommentsFn = createServerFn({ method: "GET" })
|
|
|
87
112
|
replies: (replyMap.get(c.id) ?? []).map(buildTree),
|
|
88
113
|
});
|
|
89
114
|
|
|
90
|
-
return enriched.filter((c) => !c.parentId).map(buildTree);
|
|
115
|
+
return { items: enriched.filter((c) => !c.parentId).map(buildTree), nextCursor };
|
|
91
116
|
});
|
|
92
117
|
|
|
93
118
|
const commentCreateSchema = z.object({
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
// 任务 2 裁定:详情扩展字段在 serverFns/bug 侧扩展,不改 components/bug/types.ts
|
|
22
22
|
// 的 BUG_LIST_SELECT——列表查询不多拉大字段。
|
|
23
23
|
import type { Prisma } from "@allbluecn/database";
|
|
24
|
-
import { BUG_LIST_SELECT } from "@/
|
|
24
|
+
import { BUG_LIST_SELECT } from "@/shared/bug/types";
|
|
25
25
|
|
|
26
26
|
export const BUG_DETAIL_SELECT = {
|
|
27
27
|
...BUG_LIST_SELECT,
|
|
@@ -20,8 +20,8 @@ import { createServerFn } from "@tanstack/react-start";
|
|
|
20
20
|
import { getRequest } from "@tanstack/react-start/server";
|
|
21
21
|
import { z } from "zod";
|
|
22
22
|
import { requireAuth } from "@/server/auth-helpers";
|
|
23
|
-
import { castBugUsers } from "@/
|
|
24
|
-
import type { BugDetail, ReproduceSteps } from "@/
|
|
23
|
+
import { castBugUsers } from "@/shared/bug/types";
|
|
24
|
+
import type { BugDetail, ReproduceSteps } from "@/shared/bug/types";
|
|
25
25
|
import { assertBugProjectMember } from "@/server/bug/bug-permissions";
|
|
26
26
|
import { BUG_DETAIL_SELECT, extractScreenshotBlobId } from "./detail-select";
|
|
27
27
|
|
|
@@ -31,7 +31,7 @@ import {
|
|
|
31
31
|
type BugSeverity,
|
|
32
32
|
type BugSource,
|
|
33
33
|
} from "@/shared/bug/constants";
|
|
34
|
-
import { BUG_LIST_SELECT, BUG_USER_BRIEF_SELECT, castBugUsers } from "@/
|
|
34
|
+
import { BUG_LIST_SELECT, BUG_USER_BRIEF_SELECT, castBugUsers } from "@/shared/bug/types";
|
|
35
35
|
import { buildListWhere } from "./bug-list-query";
|
|
36
36
|
import type { AvatarConfig } from "@allbluecn/kernel";
|
|
37
37
|
import type { Prisma } from "@allbluecn/database";
|
|
@@ -768,6 +768,12 @@ export const unarchiveBugFn = createServerFn({ method: "POST" })
|
|
|
768
768
|
|
|
769
769
|
const idListSchema = z.object({ bugIds: z.array(z.string()).min(1).max(500) });
|
|
770
770
|
|
|
771
|
+
// S40(审计 A2x 待决策)分批事务语义裁定:**按批 all-or-nothing、批间 best-effort
|
|
772
|
+
// 继续**——活动流水与实体写同批原子(批内失败活动不孤儿);某批失败不中断后续批,
|
|
773
|
+
// failed 计数随返回值交客户端提示。理由:500 条 × 3+ 语句的全量单事务长事务锁持
|
|
774
|
+
// 风险高;批间失败全量回滚会迫使用户重试整个大批。批大小 100。
|
|
775
|
+
const BULK_TX_BATCH_SIZE = 100;
|
|
776
|
+
|
|
771
777
|
export const bulkArchiveBugsFn = createServerFn({ method: "POST" })
|
|
772
778
|
.validator(idListSchema)
|
|
773
779
|
.handler(async ({ data }) => {
|
|
@@ -778,36 +784,47 @@ export const bulkArchiveBugsFn = createServerFn({ method: "POST" })
|
|
|
778
784
|
|
|
779
785
|
await assertBugsInMemberProjects(data.bugIds, userId);
|
|
780
786
|
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
787
|
+
const rows = await prisma.bug.findMany({
|
|
788
|
+
where: { id: { in: data.bugIds } },
|
|
789
|
+
select: { id: true, archived: true },
|
|
790
|
+
});
|
|
791
|
+
const toArchiveIds = rows.filter((r) => !r.archived).map((r) => r.id);
|
|
792
|
+
let archived = 0;
|
|
793
|
+
let failed = 0;
|
|
794
|
+
for (let i = 0; i < toArchiveIds.length; i += BULK_TX_BATCH_SIZE) {
|
|
795
|
+
const chunk = toArchiveIds.slice(i, i + BULK_TX_BATCH_SIZE);
|
|
796
|
+
try {
|
|
797
|
+
await prisma.$transaction(async (tx) => {
|
|
798
|
+
for (const id of chunk) {
|
|
799
|
+
const updated = await tx.bug.update({
|
|
800
|
+
where: { id },
|
|
801
|
+
data: { archived: true, archivedAt: new Date() },
|
|
802
|
+
});
|
|
803
|
+
// R11 touch:批量归档同不变式(逐条 touch,事务内)
|
|
804
|
+
await touchCollabSnapshot(
|
|
805
|
+
tx,
|
|
806
|
+
buildRoomName({ kind: "entity-bug", bugId: id }),
|
|
807
|
+
updated.updatedAt,
|
|
808
|
+
);
|
|
809
|
+
}
|
|
810
|
+
await tx.bugActivity.createMany({
|
|
811
|
+
data: chunk.map((id) => ({
|
|
812
|
+
bugId: id,
|
|
813
|
+
userId,
|
|
814
|
+
action: "archived",
|
|
815
|
+
field: "archived",
|
|
816
|
+
oldValue: "false",
|
|
817
|
+
newValue: "true",
|
|
818
|
+
})),
|
|
819
|
+
});
|
|
807
820
|
});
|
|
821
|
+
archived += chunk.length;
|
|
822
|
+
} catch (e) {
|
|
823
|
+
failed += chunk.length;
|
|
824
|
+
console.warn(`[bug-bulk] archive 批次失败(${chunk.length} 条)`, e);
|
|
808
825
|
}
|
|
809
|
-
|
|
810
|
-
}
|
|
826
|
+
}
|
|
827
|
+
return { archived, skipped: data.bugIds.length - toArchiveIds.length, failed };
|
|
811
828
|
});
|
|
812
829
|
|
|
813
830
|
export const bulkDeleteBugsFn = createServerFn({ method: "POST" })
|
|
@@ -883,6 +900,16 @@ export const bulkUpdateBugsFn = createServerFn({ method: "POST" })
|
|
|
883
900
|
}
|
|
884
901
|
}
|
|
885
902
|
|
|
903
|
+
// 标签名解析提升出循环(patch.labelIds 对所有 bug 同一值,S40 顺带收敛)
|
|
904
|
+
const patchLabelNames = new Map<string, string>();
|
|
905
|
+
if (data.patch.labelIds !== undefined && data.patch.labelIds.length > 0) {
|
|
906
|
+
const labelRows = await prisma.label.findMany({
|
|
907
|
+
where: { id: { in: data.patch.labelIds } },
|
|
908
|
+
select: { id: true, name: true },
|
|
909
|
+
});
|
|
910
|
+
for (const l of labelRows) patchLabelNames.set(l.id, l.name);
|
|
911
|
+
}
|
|
912
|
+
|
|
886
913
|
// per-bug 门卫:仅对「相对现值有变化」的 bug 转发(无变化零写零转发零
|
|
887
914
|
// 查询);变更面即门卫 fields 面(updateBugFn 同口径,键值与客户端 Y 写
|
|
888
915
|
// 端同形)。C1 属主预检:批量入口已 assertBugsInMemberProjects 圈定,
|
|
@@ -920,88 +947,93 @@ export const bulkUpdateBugsFn = createServerFn({ method: "POST" })
|
|
|
920
947
|
if (guard.kind === "forwarded") forwardedIds.add(bug.id);
|
|
921
948
|
}
|
|
922
949
|
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
oldValue:
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
950
|
+
// 写入分批(S40 语义见 bulkArchiveBugsFn 上方裁定):实体写与活动流水同批原子
|
|
951
|
+
let updated = 0;
|
|
952
|
+
let failed = 0;
|
|
953
|
+
for (let i = 0; i < rows.length; i += BULK_TX_BATCH_SIZE) {
|
|
954
|
+
const chunkRows = rows.slice(i, i + BULK_TX_BATCH_SIZE);
|
|
955
|
+
let chunkUpdated = 0;
|
|
956
|
+
try {
|
|
957
|
+
await prisma.$transaction(async (tx) => {
|
|
958
|
+
for (const bug of chunkRows) {
|
|
959
|
+
const updates: Prisma.BugUpdateInput = {};
|
|
960
|
+
const activities: Array<{ action: string; field: string; oldValue: string; newValue: string }> = [];
|
|
961
|
+
|
|
962
|
+
if (data.patch.state !== undefined && data.patch.state !== bug.state) {
|
|
963
|
+
activities.push({
|
|
964
|
+
action: "state_changed",
|
|
965
|
+
field: "state",
|
|
966
|
+
oldValue: bug.state,
|
|
967
|
+
newValue: data.patch.state,
|
|
968
|
+
});
|
|
969
|
+
updates.state = data.patch.state;
|
|
970
|
+
}
|
|
971
|
+
if (data.patch.severity !== undefined && data.patch.severity !== bug.severity) {
|
|
972
|
+
activities.push({
|
|
973
|
+
action: "severity_changed",
|
|
974
|
+
field: "severity",
|
|
975
|
+
oldValue: bug.severity,
|
|
976
|
+
newValue: data.patch.severity,
|
|
977
|
+
});
|
|
978
|
+
updates.severity = data.patch.severity;
|
|
979
|
+
}
|
|
980
|
+
if (data.patch.assigneeId !== undefined) {
|
|
981
|
+
const newId = data.patch.assigneeId;
|
|
982
|
+
const oldId = bug.assignee?.id ?? null;
|
|
983
|
+
if (newId !== oldId) {
|
|
984
|
+
updates.assignee = newId ? { connect: { id: newId } } : { disconnect: true };
|
|
985
|
+
activities.push({
|
|
986
|
+
action: "assigned",
|
|
987
|
+
field: "assigneeId",
|
|
988
|
+
oldValue: oldId ?? "",
|
|
989
|
+
newValue: newId ?? "",
|
|
990
|
+
});
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
if (data.patch.labelIds !== undefined) {
|
|
994
|
+
const oldIds = bug.labels.map((bl) => bl.labelId).sort().join(",");
|
|
995
|
+
const newIds = [...data.patch.labelIds].sort().join(",");
|
|
996
|
+
if (oldIds !== newIds) {
|
|
997
|
+
activities.push({
|
|
998
|
+
action: "label_changed",
|
|
999
|
+
field: "labelIds",
|
|
1000
|
+
oldValue: bug.labels.map((bl) => bl.label.name).join(", "),
|
|
1001
|
+
newValue: data.patch.labelIds.map((id) => patchLabelNames.get(id) ?? id).join(", "),
|
|
1002
|
+
});
|
|
1003
|
+
updates.labels = {
|
|
1004
|
+
deleteMany: {},
|
|
1005
|
+
create: data.patch.labelIds.map((labelId) => ({ labelId })),
|
|
1006
|
+
};
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
// 无字段变化跳过写(不触碰 updatedBy/updatedAt)
|
|
1011
|
+
if (Object.keys(updates).length === 0) continue;
|
|
1012
|
+
|
|
1013
|
+
// forwarded:批量 patch 全为 Y 属主字段,写入已由 collab 接管——剥离
|
|
1014
|
+
// 直写(防互吞)后零剩余写入:不 bump updatedAt、不写活动(collab
|
|
1015
|
+
// 落库侧产出,web 再写即重复流水)。编辑已经 collab 应用,计入 updated。
|
|
1016
|
+
if (forwardedIds.has(bug.id)) {
|
|
1017
|
+
chunkUpdated++;
|
|
1018
|
+
continue;
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
updates.updatedBy = { connect: { id: userId } };
|
|
1022
|
+
await tx.bug.update({ where: { id: bug.id }, data: updates });
|
|
1023
|
+
if (activities.length > 0) {
|
|
1024
|
+
await tx.bugActivity.createMany({
|
|
1025
|
+
data: activities.map((a) => ({ bugId: bug.id, userId, ...a })),
|
|
1026
|
+
});
|
|
1027
|
+
}
|
|
1028
|
+
chunkUpdated++;
|
|
958
1029
|
}
|
|
959
|
-
}
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
const labelRows = data.patch.labelIds.length
|
|
965
|
-
? await tx.label.findMany({
|
|
966
|
-
where: { id: { in: data.patch.labelIds } },
|
|
967
|
-
select: { id: true, name: true },
|
|
968
|
-
})
|
|
969
|
-
: [];
|
|
970
|
-
const idToName = new Map(labelRows.map((l) => [l.id, l.name]));
|
|
971
|
-
activities.push({
|
|
972
|
-
action: "label_changed",
|
|
973
|
-
field: "labelIds",
|
|
974
|
-
oldValue: bug.labels.map((bl) => bl.label.name).join(", "),
|
|
975
|
-
newValue: data.patch.labelIds.map((id) => idToName.get(id) ?? id).join(", "),
|
|
976
|
-
});
|
|
977
|
-
updates.labels = {
|
|
978
|
-
deleteMany: {},
|
|
979
|
-
create: data.patch.labelIds.map((labelId) => ({ labelId })),
|
|
980
|
-
};
|
|
981
|
-
}
|
|
982
|
-
}
|
|
983
|
-
|
|
984
|
-
// 无字段变化跳过写(不触碰 updatedBy/updatedAt)
|
|
985
|
-
if (Object.keys(updates).length === 0) continue;
|
|
986
|
-
|
|
987
|
-
// forwarded:批量 patch 全为 Y 属主字段,写入已由 collab 接管——剥离
|
|
988
|
-
// 直写(防互吞)后零剩余写入:不 bump updatedAt、不写活动(collab
|
|
989
|
-
// 落库侧产出,web 再写即重复流水)。编辑已经 collab 应用,计入 updated。
|
|
990
|
-
if (forwardedIds.has(bug.id)) {
|
|
991
|
-
updated++;
|
|
992
|
-
continue;
|
|
993
|
-
}
|
|
994
|
-
|
|
995
|
-
updates.updatedBy = { connect: { id: userId } };
|
|
996
|
-
await tx.bug.update({ where: { id: bug.id }, data: updates });
|
|
997
|
-
for (const a of activities) {
|
|
998
|
-
await tx.bugActivity.create({
|
|
999
|
-
data: { bugId: bug.id, userId, action: a.action, field: a.field, oldValue: a.oldValue, newValue: a.newValue },
|
|
1000
|
-
});
|
|
1001
|
-
}
|
|
1002
|
-
updated++;
|
|
1030
|
+
});
|
|
1031
|
+
updated += chunkUpdated;
|
|
1032
|
+
} catch (e) {
|
|
1033
|
+
failed += chunkUpdated;
|
|
1034
|
+
console.warn(`[bug-bulk] update 批次失败(${chunkRows.length} 条)`, e);
|
|
1003
1035
|
}
|
|
1036
|
+
}
|
|
1004
1037
|
|
|
1005
|
-
|
|
1006
|
-
});
|
|
1038
|
+
return { updated, failed };
|
|
1007
1039
|
});
|
|
@@ -25,7 +25,7 @@ import {
|
|
|
25
25
|
type BugGroupKey,
|
|
26
26
|
type BugOrderKey,
|
|
27
27
|
} from "@/shared/bug/display-types";
|
|
28
|
-
import { BUG_LIST_SELECT, type BugListRow } from "@/
|
|
28
|
+
import { BUG_LIST_SELECT, type BugListRow } from "@/shared/bug/types";
|
|
29
29
|
|
|
30
30
|
/** prisma.groupBy 的 by 字段映射(assignee/createdBy 用外键列分组计数) */
|
|
31
31
|
export const BUG_GROUP_BY_FIELD: Record<Exclude<BugGroupKey, "none">, Prisma.BugScalarFieldEnum> = {
|
|
@@ -31,7 +31,7 @@ import {
|
|
|
31
31
|
BUG_ORDER_DIRECTIONS,
|
|
32
32
|
type BugGroupKey,
|
|
33
33
|
} from "@/shared/bug/display-types";
|
|
34
|
-
import { BUG_LIST_SELECT, castBugUsers } from "@/
|
|
34
|
+
import { BUG_LIST_SELECT, castBugUsers } from "@/shared/bug/types";
|
|
35
35
|
import { buildListWhere } from "./bug-list-query";
|
|
36
36
|
import {
|
|
37
37
|
BUG_GROUP_BY_FIELD,
|
|
@@ -44,7 +44,7 @@ import {
|
|
|
44
44
|
fetchBugsInSeverityRankOrder,
|
|
45
45
|
sortBugGroupKeys,
|
|
46
46
|
} from "./bug-group-query";
|
|
47
|
-
import type { BugListItem } from "@/
|
|
47
|
+
import type { BugListItem } from "@/shared/bug/types";
|
|
48
48
|
import type { Prisma } from "@allbluecn/database";
|
|
49
49
|
|
|
50
50
|
const baseFilterSchema = {
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
// (历史数据可能是裸 HTML/markdown,见 lib/story/description-diff.ts 的兼容口径)。
|
|
21
21
|
// 转需求时把 Bug.reproduceSteps 序列化为「## 复现步骤」节追加到描述尾部,避免信息丢失
|
|
22
22
|
// (规格 §7:附件不复制,复现步骤必须随描述带走)。
|
|
23
|
-
import type { ReproduceSteps } from "@/
|
|
23
|
+
import type { ReproduceSteps } from "@/shared/bug/types";
|
|
24
24
|
|
|
25
25
|
function escapeHtml(text: string): string {
|
|
26
26
|
return text
|