@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
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
// You should have received a copy of the GNU Affero General Public License
|
|
16
16
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
17
|
|
|
18
|
-
import { useState, useRef, useLayoutEffect, useEffect } from "react";
|
|
18
|
+
import { useState, useRef, useLayoutEffect, useEffect, useMemo } from "react";
|
|
19
19
|
import type { ReactNode } from "react";
|
|
20
20
|
import { UserAvatar } from "@/components/shared-kit";
|
|
21
21
|
import {
|
|
@@ -28,10 +28,10 @@ import {
|
|
|
28
28
|
PRIORITY_ICON,
|
|
29
29
|
type StoryState,
|
|
30
30
|
type StoryPriority,
|
|
31
|
-
} from "@/
|
|
31
|
+
} from "@/shared/story/constants";
|
|
32
32
|
import { LucideIconByName } from "@/components/shared-kit/icons/lucide-icon-picker/lucide-icon-by-name";
|
|
33
33
|
import { listProjectsBriefFn } from "@/server/serverFns/project";
|
|
34
|
-
import type { ActivityItem, UserBrief } from "@/
|
|
34
|
+
import type { ActivityItem, UserBrief } from "@/shared/story/types";
|
|
35
35
|
import { groupActivities, type ActivityGroup } from "@/lib/story/activity-grouper";
|
|
36
36
|
import { ArrowUpFromDot, ArrowRight, ChevronDown, Dot, Sparkles, Layers, UserRound } from "lucide-react";
|
|
37
37
|
import { sanitize } from "@/lib/sanitize";
|
|
@@ -422,18 +422,37 @@ interface DiffState {
|
|
|
422
422
|
|
|
423
423
|
export function ActivityTimeline({ activities, users }: ActivityTimelineProps) {
|
|
424
424
|
const [diffStates, setDiffStates] = useState<Record<string, DiffState>>({});
|
|
425
|
-
const [
|
|
425
|
+
const [projectBriefs, setProjectBriefs] = useState<Array<{ id: string; name: string; icon: string }>>([]);
|
|
426
426
|
|
|
427
427
|
useEffect(() => {
|
|
428
428
|
listProjectsBriefFn()
|
|
429
|
-
.then(
|
|
430
|
-
const map: Record<string, string> = {};
|
|
431
|
-
for (const r of rows) map[r.name] = r.icon;
|
|
432
|
-
setProjectIconByName(map);
|
|
433
|
-
})
|
|
429
|
+
.then(setProjectBriefs)
|
|
434
430
|
.catch(() => { });
|
|
435
431
|
}, []);
|
|
436
432
|
|
|
433
|
+
// S13:project_transferred 的 value 已改为存项目 id;id map 供新值解析名称/图标,
|
|
434
|
+
// name map 兼容存量行(value=项目名)回退命中——重名错配仅残留于存量数据
|
|
435
|
+
const { projectById, projectIconByName } = useMemo(() => {
|
|
436
|
+
const byId = new Map<string, { name: string; icon: string }>();
|
|
437
|
+
const byName = new Map<string, string>();
|
|
438
|
+
for (const p of projectBriefs) {
|
|
439
|
+
byId.set(p.id, { name: p.name, icon: p.icon });
|
|
440
|
+
if (!byName.has(p.name)) byName.set(p.name, p.icon);
|
|
441
|
+
}
|
|
442
|
+
return { projectById: byId, projectIconByName: byName };
|
|
443
|
+
}, [projectBriefs]);
|
|
444
|
+
|
|
445
|
+
// 仅 project_transferred 走 id→名 解析(其余动作值不进 id map,防误命中)
|
|
446
|
+
const displayValue = (action: string, value: string | null): string | null => {
|
|
447
|
+
if (action !== "project_transferred" || !value) return value;
|
|
448
|
+
return projectById.get(value)?.name ?? value;
|
|
449
|
+
};
|
|
450
|
+
const resolveProjectIcon = (action: string, value: string | null | undefined): string | undefined => {
|
|
451
|
+
if (!value) return undefined;
|
|
452
|
+
if (action === "project_transferred") return projectById.get(value)?.icon ?? projectIconByName.get(value);
|
|
453
|
+
return projectIconByName.get(value);
|
|
454
|
+
};
|
|
455
|
+
|
|
437
456
|
if (activities.length === 0) {
|
|
438
457
|
return (
|
|
439
458
|
<p className="py-8 text-center text-sm text-muted-foreground">
|
|
@@ -548,16 +567,16 @@ export function ActivityTimeline({ activities, users }: ActivityTimelineProps) {
|
|
|
548
567
|
</div>
|
|
549
568
|
{hasDiff ? (
|
|
550
569
|
<DiffCompare
|
|
551
|
-
oldValue={formatDiffValue(a.action, g.displayOldValue!)}
|
|
552
|
-
newValue={formatDiffValue(a.action, g.displayNewValue!)}
|
|
553
|
-
oldDecorator={getDiffDecorator(a.action, g.displayOldValue,
|
|
554
|
-
newDecorator={getDiffDecorator(a.action, g.displayNewValue,
|
|
570
|
+
oldValue={formatDiffValue(a.action, displayValue(a.action, g.displayOldValue)!)}
|
|
571
|
+
newValue={formatDiffValue(a.action, displayValue(a.action, g.displayNewValue)!)}
|
|
572
|
+
oldDecorator={getDiffDecorator(a.action, displayValue(a.action, g.displayOldValue), resolveProjectIcon(a.action, g.displayOldValue), users?.find((u) => u.username === g.displayOldValue))}
|
|
573
|
+
newDecorator={getDiffDecorator(a.action, displayValue(a.action, g.displayNewValue), resolveProjectIcon(a.action, g.displayNewValue), users?.find((u) => u.username === g.displayNewValue))}
|
|
555
574
|
plain={a.action !== "description_changed"}
|
|
556
575
|
blocks={isBlocks ? g.items.map((it) => ({
|
|
557
|
-
oldValue: formatDiffValue(a.action, it.oldValue),
|
|
558
|
-
newValue: formatDiffValue(a.action, it.newValue),
|
|
559
|
-
oldDecorator: getDiffDecorator(a.action, it.oldValue,
|
|
560
|
-
newDecorator: getDiffDecorator(a.action, it.newValue,
|
|
576
|
+
oldValue: formatDiffValue(a.action, displayValue(a.action, it.oldValue)),
|
|
577
|
+
newValue: formatDiffValue(a.action, displayValue(a.action, it.newValue)),
|
|
578
|
+
oldDecorator: getDiffDecorator(a.action, displayValue(a.action, it.oldValue), resolveProjectIcon(a.action, it.oldValue), users?.find((u) => u.username === it.oldValue)),
|
|
579
|
+
newDecorator: getDiffDecorator(a.action, displayValue(a.action, it.newValue), resolveProjectIcon(a.action, it.newValue), users?.find((u) => u.username === it.newValue)),
|
|
561
580
|
})) : undefined}
|
|
562
581
|
collapsed={state.collapsed}
|
|
563
582
|
isTall={state.isTall}
|
|
@@ -21,7 +21,7 @@ import { Button } from "@/components/ui/button";
|
|
|
21
21
|
import { Textarea } from "@/components/ui/textarea";
|
|
22
22
|
import { createCommentFn } from "@/server/serverFns/story";
|
|
23
23
|
import { toast } from "@/components/ui/toast";
|
|
24
|
-
import type { CommentWithReplies } from "
|
|
24
|
+
import type { CommentWithReplies } from "@/shared/story/types";
|
|
25
25
|
import { m } from "@/paraglide/messages.js";
|
|
26
26
|
|
|
27
27
|
interface CommentInputProps {
|
|
@@ -29,7 +29,7 @@ import {
|
|
|
29
29
|
} from "@/components/ui/dropdown-menu";
|
|
30
30
|
import { deleteCommentFn, updateCommentFn } from "@/server/serverFns/story";
|
|
31
31
|
import { toast } from "@/components/ui/toast";
|
|
32
|
-
import type { CommentWithReplies } from "
|
|
32
|
+
import type { CommentWithReplies } from "@/shared/story/types";
|
|
33
33
|
import { m } from "@/paraglide/messages.js";
|
|
34
34
|
|
|
35
35
|
interface CommentItemProps {
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
import { CommentItem } from "./comment-item";
|
|
19
19
|
import { CommentInput } from "./comment-input";
|
|
20
|
-
import type { CommentWithReplies } from "
|
|
20
|
+
import type { CommentWithReplies } from "@/shared/story/types";
|
|
21
21
|
import { m } from '@/paraglide/messages.js'
|
|
22
22
|
import { formatTimestamp } from "@/lib/datetime";
|
|
23
23
|
|
|
@@ -19,7 +19,7 @@ import { Plus } from "lucide-react";
|
|
|
19
19
|
import { Button } from "@/components/ui/button";
|
|
20
20
|
import { cn } from "@/lib/utils";
|
|
21
21
|
import { EmojiPicker } from "@/components/shared-kit/comments/emoji-picker";
|
|
22
|
-
import type { ReactionGroup } from "@/
|
|
22
|
+
import type { ReactionGroup } from "@/shared/story/types";
|
|
23
23
|
import { m } from "@/paraglide/messages.js";
|
|
24
24
|
|
|
25
25
|
interface ReactionBarProps {
|
|
@@ -35,9 +35,9 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip
|
|
|
35
35
|
import { updateStoryFn, listStoryReactionsFn, toggleStoryReactionFn } from "@/server/serverFns/story";
|
|
36
36
|
import { formatTimestamp } from "@/lib/datetime";
|
|
37
37
|
import { toast } from "@/components/ui/toast";
|
|
38
|
-
import type { StoryDetail, UserBrief, CommentWithReplies, ActivityItem, ReactionGroup, RelationsData } from "
|
|
39
|
-
import { parseStoryDescription, serializeStoryDescription } from "
|
|
40
|
-
import type { StoryState } from "
|
|
38
|
+
import type { StoryDetail, UserBrief, CommentWithReplies, ActivityItem, ReactionGroup, RelationsData } from "@/shared/story/types";
|
|
39
|
+
import { parseStoryDescription, serializeStoryDescription } from "@/shared/story/types";
|
|
40
|
+
import type { StoryState } from "@/shared/story/constants";
|
|
41
41
|
import type { StoryPatch } from "../display/group-fields";
|
|
42
42
|
import { ActivityTabs } from "./activity-tabs";
|
|
43
43
|
import { EmojiPicker } from "@/components/shared-kit/comments/emoji-picker";
|
|
@@ -233,7 +233,7 @@ export interface StoryDetailBodyProps {
|
|
|
233
233
|
/** 字段落库成功后的补丁回调(PeekPanel 回写列表/看板做乐观更新) */
|
|
234
234
|
onStoryPatch?: (storyId: string, patch: StoryPatch) => void;
|
|
235
235
|
onNavigateInPanel?: (targetId: string, targetIdentifier?: string, relationKind?: "child" | "dependency") => void;
|
|
236
|
-
onOpenEntity?: (entity: import("
|
|
236
|
+
onOpenEntity?: (entity: import("@/components/story/entity-overlay").StoryEntityOverlay) => void;
|
|
237
237
|
/** 用例/BUG 行的面板内叠加打开(peek 传入;缺省整页跳转) */
|
|
238
238
|
/** 内嵌面板导航面包屑(在面板内跳转时替代单层 identifier 显示) */
|
|
239
239
|
breadcrumbItems?: Array<{ identifier?: string; projectId?: string; label: string; onClick?: () => void }>;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// AllBlue - 理想之海
|
|
3
|
+
// Copyright (C) 2026 AllBlue Contributors
|
|
4
|
+
//
|
|
5
|
+
// This program is free software: you can redistribute it and/or modify
|
|
6
|
+
// it under the terms of the GNU Affero General Public License as published by
|
|
7
|
+
// the Free Software Foundation, either version 3 of the License, or
|
|
8
|
+
// (at your option) any later version.
|
|
9
|
+
//
|
|
10
|
+
// This program is distributed in the hope that it will be useful,
|
|
11
|
+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
+
// GNU Affero General Public License for more details.
|
|
14
|
+
//
|
|
15
|
+
// You should have received a copy of the GNU Affero General Public License
|
|
16
|
+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
|
+
|
|
18
|
+
// S12(审计 A7x-14 ⚪)附件面板级缓存回归:
|
|
19
|
+
// - 同 storageUrl 并发/跨实例只 fetch 一次(网格 + 全屏弹窗复用);
|
|
20
|
+
// - LRU 上限淘汰最旧条目;
|
|
21
|
+
// - 超大附件不驻留内存;
|
|
22
|
+
// - 失败不缓存,下次重试。
|
|
23
|
+
// 注:jsdom 的 Response 会把 Blob 体当字符串化,测试统一用文本体控制字节数。
|
|
24
|
+
|
|
25
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
26
|
+
import { loadAttachmentFile } from "../associated-attachments-panel";
|
|
27
|
+
import type { StoryAttachmentItem } from "@/shared/story/types";
|
|
28
|
+
|
|
29
|
+
const attachment = (over: Partial<StoryAttachmentItem> = {}): StoryAttachmentItem => ({
|
|
30
|
+
id: "att-1",
|
|
31
|
+
fileName: "a.png",
|
|
32
|
+
fileSize: 10,
|
|
33
|
+
mimeType: "image/png",
|
|
34
|
+
storageUrl: "https://cdn.test/att-1",
|
|
35
|
+
...over,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const fetchMock = vi.fn(async () => new Response("{}", { status: 200 }));
|
|
39
|
+
|
|
40
|
+
beforeEach(() => {
|
|
41
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
afterEach(() => {
|
|
45
|
+
vi.unstubAllGlobals();
|
|
46
|
+
vi.restoreAllMocks();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe("loadAttachmentFile(S12 面板级缓存)", () => {
|
|
50
|
+
it("同 storageUrl 两次调用只发一次 fetch,File 名/类型正确", async () => {
|
|
51
|
+
const [f1, f2] = await Promise.all([
|
|
52
|
+
loadAttachmentFile(attachment()),
|
|
53
|
+
loadAttachmentFile(attachment()),
|
|
54
|
+
]);
|
|
55
|
+
|
|
56
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
57
|
+
expect(f1.name).toBe("a.png");
|
|
58
|
+
expect(f1.type).toBe("image/png");
|
|
59
|
+
expect(f2).toBe(f1);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("超过 20MB 的大附件不驻留缓存(下次重拉)", async () => {
|
|
63
|
+
const big = attachment({ id: "att-big", storageUrl: "https://cdn.test/big" });
|
|
64
|
+
const BIG = "x".repeat(20 * 1024 * 1024 + 1);
|
|
65
|
+
fetchMock.mockImplementation(async () => new Response(BIG, { status: 200 }));
|
|
66
|
+
|
|
67
|
+
await loadAttachmentFile(big);
|
|
68
|
+
await loadAttachmentFile(big);
|
|
69
|
+
|
|
70
|
+
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("失败不缓存: rejected 后再次调用重新 fetch", async () => {
|
|
74
|
+
const bad = attachment({ id: "att-bad", storageUrl: "https://cdn.test/bad" });
|
|
75
|
+
fetchMock.mockImplementationOnce(async () => new Response("no", { status: 404 }));
|
|
76
|
+
fetchMock.mockImplementationOnce(async () => new Response("abcd", { status: 200 }));
|
|
77
|
+
|
|
78
|
+
await expect(loadAttachmentFile(bad)).rejects.toThrow("加载失败");
|
|
79
|
+
const ok = await loadAttachmentFile(bad);
|
|
80
|
+
expect(ok.size).toBe(4);
|
|
81
|
+
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -49,7 +49,7 @@ import { sanitize } from "@/lib/sanitize";
|
|
|
49
49
|
import { formatFileSize, getFaviconUrl, getFileTypeIcon, extractLinkTitle, getMimeTypeLabel, getMimeTypeLabelKey, getMimeTypeLabelByKey, MIME_TYPE_LABEL_ORDER, type MimeTypeLabelKey } from "../../relations/format";
|
|
50
50
|
import { generalGroupLabel } from "../../relations/general-groups";
|
|
51
51
|
import { LucideIconByName } from "@/components/shared-kit/icons/lucide-icon-picker/lucide-icon-by-name";
|
|
52
|
-
import type { AssociatedItem, RelationsData, StoryLinkItem, StoryAttachmentItem } from "
|
|
52
|
+
import type { AssociatedItem, RelationsData, StoryLinkItem, StoryAttachmentItem } from "@/shared/story/types";
|
|
53
53
|
import { m } from "@/paraglide/messages.js";
|
|
54
54
|
import { toastError } from "@/lib/toast-error";
|
|
55
55
|
import { useStoryPanelOpen } from "@/lib/hooks/use-story-panel-open";
|
|
@@ -61,7 +61,7 @@ export interface AssociatedAttachmentsPanelProps {
|
|
|
61
61
|
data: RelationsData;
|
|
62
62
|
onRefresh: (next: (prev: RelationsData) => RelationsData) => void;
|
|
63
63
|
onOpenDialog: (kind: AssociatedDialogKind, editLink?: StoryLinkItem, editAttachment?: StoryAttachmentItem) => void;
|
|
64
|
-
onAppendActivities?: (items: import("
|
|
64
|
+
onAppendActivities?: (items: import("@/shared/story/types").ActivityItem[]) => void;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
// 记住列表/预览显示方式
|
|
@@ -70,20 +70,61 @@ const VIEW_MODE_STORAGE_KEY = "associated-attachments-view-mode";
|
|
|
70
70
|
// 全屏浮窗尺寸与 peek-panel 面板的 full-screen 模式一致(四边 inset-4)
|
|
71
71
|
const FULLSCREEN_CONTENT_CLASS = "inset-4 w-auto h-auto sm:max-w-none";
|
|
72
72
|
|
|
73
|
+
// ===== 附件面板级缓存(S12,审计 A7x-14 ⚪)=====
|
|
74
|
+
// 网格视图与全屏弹窗各自挂 AttachmentFileView,此前对同一附件重复 fetch。
|
|
75
|
+
// 以 storageUrl 为键缓存 in-flight/已完成 的 File Promise:并发去重 + 跨实例复用。
|
|
76
|
+
// 内存策略:LRU 上限 12 条;超过 20MB 的大附件不驻留(走浏览器 HTTP 缓存),
|
|
77
|
+
// 失败的 Promise 不缓存(下次挂载可重试)。File 由 GC 回收,无需手动 revoke。
|
|
78
|
+
const MAX_CACHED_ATTACHMENTS = 12;
|
|
79
|
+
const MAX_CACHEABLE_BYTES = 20 * 1024 * 1024;
|
|
80
|
+
|
|
81
|
+
const attachmentFileCache = new Map<string, Promise<File>>();
|
|
82
|
+
|
|
83
|
+
async function fetchAttachmentFile(attachment: StoryAttachmentItem): Promise<File> {
|
|
84
|
+
const res = await fetch(attachment.storageUrl);
|
|
85
|
+
if (!res.ok) throw new Error("加载失败");
|
|
86
|
+
const blob = await res.blob();
|
|
87
|
+
return new File([blob], attachment.fileName, { type: attachment.mimeType });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function loadAttachmentFile(attachment: StoryAttachmentItem): Promise<File> {
|
|
91
|
+
const hit = attachmentFileCache.get(attachment.storageUrl);
|
|
92
|
+
if (hit) {
|
|
93
|
+
// LRU touch:删除重插维护插入序
|
|
94
|
+
attachmentFileCache.delete(attachment.storageUrl);
|
|
95
|
+
attachmentFileCache.set(attachment.storageUrl, hit);
|
|
96
|
+
return hit;
|
|
97
|
+
}
|
|
98
|
+
// 先以 in-flight Promise 占位(并发去重),完成后按大小决定去留
|
|
99
|
+
const promise = fetchAttachmentFile(attachment).then((file) => {
|
|
100
|
+
if (file.size > MAX_CACHEABLE_BYTES) {
|
|
101
|
+
// 大附件不驻留:本次直返,后续重拉走浏览器 HTTP 缓存
|
|
102
|
+
attachmentFileCache.delete(attachment.storageUrl);
|
|
103
|
+
} else {
|
|
104
|
+
attachmentFileCache.set(attachment.storageUrl, promise);
|
|
105
|
+
while (attachmentFileCache.size > MAX_CACHED_ATTACHMENTS) {
|
|
106
|
+
const oldest = attachmentFileCache.keys().next().value;
|
|
107
|
+
if (oldest === undefined) break;
|
|
108
|
+
attachmentFileCache.delete(oldest);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return file;
|
|
112
|
+
});
|
|
113
|
+
attachmentFileCache.set(attachment.storageUrl, promise);
|
|
114
|
+
promise.catch(() => attachmentFileCache.delete(attachment.storageUrl));
|
|
115
|
+
return promise;
|
|
116
|
+
}
|
|
117
|
+
|
|
73
118
|
function useAttachmentFile(attachment: StoryAttachmentItem) {
|
|
74
119
|
const [file, setFile] = React.useState<File | null>(null);
|
|
75
120
|
const [error, setError] = React.useState<string | null>(null);
|
|
76
121
|
|
|
77
122
|
React.useEffect(() => {
|
|
78
123
|
let cancelled = false;
|
|
79
|
-
|
|
80
|
-
.then((
|
|
81
|
-
if (!res.ok) throw new Error("加载失败");
|
|
82
|
-
return res.blob();
|
|
83
|
-
})
|
|
84
|
-
.then((blob) => {
|
|
124
|
+
loadAttachmentFile(attachment)
|
|
125
|
+
.then((f) => {
|
|
85
126
|
if (cancelled) return;
|
|
86
|
-
setFile(
|
|
127
|
+
setFile(f);
|
|
87
128
|
})
|
|
88
129
|
.catch((e) => {
|
|
89
130
|
if (cancelled) return;
|
|
@@ -47,7 +47,7 @@ import { VirtualGroupHeader } from "../../views/story-list/group";
|
|
|
47
47
|
import { DisplayOptionsBody } from "../../display/display-dropdown";
|
|
48
48
|
import { StoryRefRow } from "../../relations/story-ref-row";
|
|
49
49
|
import type { DisplayOptions } from "../../display/types";
|
|
50
|
-
import type { RelationsData, StoryListItem, UserBrief } from "
|
|
50
|
+
import type { RelationsData, StoryListItem, UserBrief } from "@/shared/story/types";
|
|
51
51
|
import type { StoryPatch } from "../../display/group-fields";
|
|
52
52
|
import { m } from "@/paraglide/messages.js";
|
|
53
53
|
import { toastError } from "@/lib/toast-error";
|
|
@@ -284,9 +284,9 @@ export function StoryDetailChildrenPanel({
|
|
|
284
284
|
}
|
|
285
285
|
};
|
|
286
286
|
|
|
287
|
-
const handleTransfer = (updatedStories?: Array<{ id: string; identifier: string; projectId: string; parentId: string | null }>) => {
|
|
287
|
+
const handleTransfer = (updatedStories?: Array<{ id: string; identifier: string; projectId: string | null; parentId: string | null }>) => {
|
|
288
288
|
if (!updatedStories?.length) return;
|
|
289
|
-
const updatedMap = new Map<string, { identifier: string; projectId: string }>();
|
|
289
|
+
const updatedMap = new Map<string, { identifier: string; projectId: string | null }>();
|
|
290
290
|
for (const s of updatedStories) {
|
|
291
291
|
updatedMap.set(s.id, { identifier: s.identifier, projectId: s.projectId });
|
|
292
292
|
}
|
|
@@ -29,7 +29,7 @@ import {
|
|
|
29
29
|
bulkDeleteStoriesFn,
|
|
30
30
|
bulkUpdateStoriesFn,
|
|
31
31
|
} from "@/server/serverFns/story";
|
|
32
|
-
import { DEPENDENCY_TYPE_OPTIONS, toDependencyEdge, type DependencyTypeOption } from "
|
|
32
|
+
import { DEPENDENCY_TYPE_OPTIONS, toDependencyEdge, type DependencyTypeOption } from "@/shared/story/constants";
|
|
33
33
|
import { BatchToolbar } from "../../views/story-list/batch-toolbar";
|
|
34
34
|
import { useGroupedStories } from "../../display/use-grouped-stories";
|
|
35
35
|
import { VirtualGroupHeader } from "../../views/story-list/group";
|
|
@@ -37,7 +37,8 @@ import { DisplayOptionsBody } from "../../display/display-dropdown";
|
|
|
37
37
|
import { StoryRefRow } from "../../relations/story-ref-row";
|
|
38
38
|
import type { DisplayOptions, GroupNode } from "../../display/types";
|
|
39
39
|
import type { StoryPatch } from "../../display/group-fields";
|
|
40
|
-
import type {
|
|
40
|
+
import type { StoryRelationPatch } from "../../relations/story-ref-row";
|
|
41
|
+
import type { RelationsData, UserBrief, DependencyRow } from "@/shared/story/types";
|
|
41
42
|
import { m } from "@/paraglide/messages.js";
|
|
42
43
|
import { toastError } from "@/lib/toast-error";
|
|
43
44
|
import {
|
|
@@ -77,7 +78,7 @@ export interface StoryDetailDependencyPanelProps {
|
|
|
77
78
|
projects?: { id: string; name: string; icon: string; displayId: string | null }[];
|
|
78
79
|
labels?: { id: string; name: string; color: string }[];
|
|
79
80
|
onOpenAdd: () => void;
|
|
80
|
-
onAppendActivities?: (items: import("
|
|
81
|
+
onAppendActivities?: (items: import("@/shared/story/types").ActivityItem[]) => void;
|
|
81
82
|
onNavigateInPanel?: (targetId: string, targetIdentifier?: string, relationKind?: "child" | "dependency") => void;
|
|
82
83
|
}
|
|
83
84
|
|
|
@@ -377,13 +378,13 @@ export function StoryDetailDependencyPanel({
|
|
|
377
378
|
return count > 0 ? "indeterminate" : false;
|
|
378
379
|
};
|
|
379
380
|
|
|
380
|
-
const handleStoryRefresh = (rowId: string, patch:
|
|
381
|
+
const handleStoryRefresh = (rowId: string, patch: StoryRelationPatch) => {
|
|
381
382
|
for (const group of ["dependsOn", "dependents"] as const) {
|
|
382
383
|
if (patch.deleted) {
|
|
383
384
|
applyRowsUpdate((rows) => rows.filter((r) => r.id !== rowId), group);
|
|
384
385
|
} else {
|
|
385
386
|
applyRowsUpdate(
|
|
386
|
-
(rows) => rows.map((r) => (r.id === rowId ?
|
|
387
|
+
(rows) => rows.map((r) => (r.id === rowId ? { ...r, ...patch } : r)),
|
|
387
388
|
group,
|
|
388
389
|
);
|
|
389
390
|
}
|
|
@@ -420,7 +421,7 @@ export function StoryDetailDependencyPanel({
|
|
|
420
421
|
/>
|
|
421
422
|
);
|
|
422
423
|
|
|
423
|
-
const renderGroupItems = (node: GroupNode
|
|
424
|
+
const renderGroupItems = (node: GroupNode<DependencyRow>, group: DependencySection, depth: number, collapseKey: string) => {
|
|
424
425
|
const isCollapsed = collapsed[collapseKey] ?? false;
|
|
425
426
|
const ids = node.items.map((s) => s.id);
|
|
426
427
|
return (
|
|
@@ -461,7 +462,7 @@ export function StoryDetailDependencyPanel({
|
|
|
461
462
|
),
|
|
462
463
|
)
|
|
463
464
|
) : (
|
|
464
|
-
node.items.map((s) => renderRow(s
|
|
465
|
+
node.items.map((s) => renderRow(s, group))
|
|
465
466
|
)}
|
|
466
467
|
</React.Fragment>
|
|
467
468
|
)}
|
|
@@ -473,7 +474,7 @@ export function StoryDetailDependencyPanel({
|
|
|
473
474
|
titleKey: string,
|
|
474
475
|
sectionKey: DependencySection,
|
|
475
476
|
rows: DependencyRow[],
|
|
476
|
-
groups: GroupNode[],
|
|
477
|
+
groups: GroupNode<DependencyRow>[],
|
|
477
478
|
) => {
|
|
478
479
|
const isCollapsed = collapsed[`${sectionKey}:root`] ?? false;
|
|
479
480
|
return (
|
|
@@ -50,9 +50,9 @@ import { getLinksFn, getAttachmentsFn, getLinkedDocsFn, getLinkedDesignsFn } fro
|
|
|
50
50
|
import { StorySelectDialog } from "../../inline-editors/story-select-dialog";
|
|
51
51
|
import { CreateStoryDialog } from "../../create-story-dialog";
|
|
52
52
|
import { addDependenciesFn, getDependenciesFn, listChildStoriesFn } from "@/server/serverFns/story";
|
|
53
|
-
import { DEPENDENCY_TYPE_OPTIONS, toDependencyEdge, type DependencyTypeOption } from "
|
|
53
|
+
import { DEPENDENCY_TYPE_OPTIONS, toDependencyEdge, type DependencyTypeOption } from "@/shared/story/constants";
|
|
54
54
|
import { useToolbarOverflow } from "../../ai/use-toolbar-overflow";
|
|
55
|
-
import type { RelationsData, UserBrief } from "
|
|
55
|
+
import type { RelationsData, UserBrief } from "@/shared/story/types";
|
|
56
56
|
import { m } from "@/paraglide/messages.js";
|
|
57
57
|
import { toastError } from "@/lib/toast-error";
|
|
58
58
|
|
|
@@ -72,13 +72,13 @@ interface StoryDetailToolbarProps {
|
|
|
72
72
|
data: RelationsData;
|
|
73
73
|
onRefresh: (next: (prev: RelationsData) => RelationsData) => void;
|
|
74
74
|
/** 关联类操作产生的活动即时追加到时间线 */
|
|
75
|
-
onAppendActivities?: (items: import("
|
|
75
|
+
onAppendActivities?: (items: import("@/shared/story/types").ActivityItem[]) => void;
|
|
76
76
|
users?: UserBrief[];
|
|
77
77
|
projects?: { id: string; name: string; icon: string; displayId: string | null }[];
|
|
78
78
|
labels?: { id: string; name: string; color: string }[];
|
|
79
79
|
onNavigateInPanel?: (targetId: string, targetIdentifier?: string, relationKind?: "child" | "dependency") => void;
|
|
80
80
|
/** 用例/BUG 行的面板内叠加打开(peek 传入;缺省整页跳转) */
|
|
81
|
-
onOpenEntity?: (entity: import("
|
|
81
|
+
onOpenEntity?: (entity: import("@/components/story/entity-overlay").StoryEntityOverlay) => void;
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
export function StoryDetailToolbar({ storyId, storyIdentifier, projectId, storyUpdatedAt, data, onRefresh, onAppendActivities, users, projects, labels, onNavigateInPanel, onOpenEntity }: StoryDetailToolbarProps) {
|
|
@@ -86,8 +86,8 @@ export function StoryDetailToolbar({ storyId, storyIdentifier, projectId, storyU
|
|
|
86
86
|
const [dependencyDialogOpen, setDependencyDialogOpen] = React.useState(false);
|
|
87
87
|
const [assocDialogOpen, setAssocDialogOpen] = React.useState<AssociatedDialogKind | null>(null);
|
|
88
88
|
const [designDialog, setDesignDialog] = React.useState<{ open: boolean; mode: DesignSelectMode }>({ open: false, mode: "design" });
|
|
89
|
-
const [linkInitialEdit, setLinkInitialEdit] = React.useState<import("
|
|
90
|
-
const [editAttachment, setEditAttachment] = React.useState<import("
|
|
89
|
+
const [linkInitialEdit, setLinkInitialEdit] = React.useState<import("@/shared/story/types").StoryLinkItem | null>(null);
|
|
90
|
+
const [editAttachment, setEditAttachment] = React.useState<import("@/shared/story/types").StoryAttachmentItem | null>(null);
|
|
91
91
|
const [dependencyType, setDependencyType] = React.useState<DependencyTypeOption["key"] | undefined>(undefined);
|
|
92
92
|
// 关联测试用例提交成功 → bump 刷新测试用例面板(面板数据自持,refreshKey 变化重拉)
|
|
93
93
|
const [casesRefreshKey, setCasesRefreshKey] = React.useState(0);
|
|
@@ -226,7 +226,7 @@ export function StoryDetailToolbar({ storyId, storyIdentifier, projectId, storyU
|
|
|
226
226
|
|
|
227
227
|
const handleCreateSuccess = (
|
|
228
228
|
_story: { id: string; name: string; identifier?: string },
|
|
229
|
-
activities?: import("
|
|
229
|
+
activities?: import("@/shared/story/types").ActivityItem[],
|
|
230
230
|
) => {
|
|
231
231
|
setActionOpen(null);
|
|
232
232
|
refreshChildren();
|
|
@@ -273,7 +273,7 @@ export function StoryDetailToolbar({ storyId, storyIdentifier, projectId, storyU
|
|
|
273
273
|
setDesignDialog({ open: true, mode });
|
|
274
274
|
};
|
|
275
275
|
|
|
276
|
-
const handleOpenAssocDialog = (kind: AssociatedDialogKind, editLink?: import("
|
|
276
|
+
const handleOpenAssocDialog = (kind: AssociatedDialogKind, editLink?: import("@/shared/story/types").StoryLinkItem, editAttachmentItem?: import("@/shared/story/types").StoryAttachmentItem) => {
|
|
277
277
|
if (kind === "design") {
|
|
278
278
|
handleOpenDesignDialog("design");
|
|
279
279
|
return;
|
|
@@ -75,7 +75,7 @@ import { TEST_CASE_PRIORITY_META } from "@/components/testcase/constants";
|
|
|
75
75
|
import { extractTextColors } from "@/components/shared-kit/list-kit/level-badge";
|
|
76
76
|
import { GenerateTestCasesDialog } from "@/components/testcase/ai/generate-dialog";
|
|
77
77
|
import { SelectCasesDialog } from "@/components/testcase/detail/select-cases-dialog";
|
|
78
|
-
import type { UserBrief } from "
|
|
78
|
+
import type { UserBrief } from "@/shared/story/types";
|
|
79
79
|
import { m } from "@/paraglide/messages.js";
|
|
80
80
|
import { toast } from "@/components/ui/toast";
|
|
81
81
|
import { toastError } from "@/lib/toast-error";
|
|
@@ -256,7 +256,7 @@ export function StoryDetailTestCases({
|
|
|
256
256
|
/** 需求最近更新时间:变化即重判 stale(编辑保存后即时刷新提示) */
|
|
257
257
|
storyUpdatedAt?: string | null;
|
|
258
258
|
/** peek 内叠加打开(传入时点击行在 peek 内打开,缺省整页跳转) */
|
|
259
|
-
onOpenEntity?: (entity: import("
|
|
259
|
+
onOpenEntity?: (entity: import("@/components/story/entity-overlay").StoryEntityOverlay) => void;
|
|
260
260
|
}) {
|
|
261
261
|
const [panelOpen, setPanelOpen] = useStoryPanelOpen(storyId, "testcases");
|
|
262
262
|
const navigate = useNavigate();
|
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
import { GitPullRequestArrow, Tag, CalendarRange } from "lucide-react";
|
|
19
19
|
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
|
|
20
20
|
import { ChevronDown } from "lucide-react";
|
|
21
|
-
import type { StoryState } from "
|
|
22
|
-
import type { ActivityItem } from "
|
|
21
|
+
import type { StoryState } from "@/shared/story/constants";
|
|
22
|
+
import type { ActivityItem } from "@/shared/story/types";
|
|
23
23
|
import { ParentStoryEditor } from "../inline-editors/parent-story-editor";
|
|
24
24
|
import { LabelsEditor } from "../inline-editors/labels-editor";
|
|
25
25
|
import { IterationEditor } from "@/components/iteration/iteration-editor";
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
// 回归:peek prev/next 须按当前视图展示顺序(分组 → 子组)导航,而非平坦 stories.items。
|
|
19
19
|
import { describe, expect, it } from "vitest";
|
|
20
20
|
import { flattenStoryGroups } from "../flatten-story-groups";
|
|
21
|
-
import type { StoryListItem } from "
|
|
21
|
+
import type { StoryListItem } from "@/shared/story/types";
|
|
22
22
|
|
|
23
23
|
const s = (id: string) => ({ id } as StoryListItem);
|
|
24
24
|
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
import { describe, it, expect } from "vitest";
|
|
19
19
|
import { GROUP_FIELD, toGroupFieldPatch, applyStoryPatchToItem } from "../group-fields";
|
|
20
|
-
import type { StoryListItem, UserBrief } from "
|
|
20
|
+
import type { StoryListItem, UserBrief } from "@/shared/story/types";
|
|
21
21
|
|
|
22
22
|
describe("GROUP_FIELD", () => {
|
|
23
23
|
it("state 映射到 state", () => {
|
|
@@ -24,9 +24,9 @@ import {
|
|
|
24
24
|
PRIORITY_COLUMN_TINT,
|
|
25
25
|
STATE_COLUMN_TINT,
|
|
26
26
|
} from "../group-resolver";
|
|
27
|
-
import { STATE_COLOR } from "
|
|
27
|
+
import { STATE_COLOR } from "@/shared/story/constants";
|
|
28
28
|
import { PRIORITY_COLOR } from "../../priority-icon";
|
|
29
|
-
import type { StoryListItem } from "
|
|
29
|
+
import type { StoryListItem } from "@/shared/story/types";
|
|
30
30
|
|
|
31
31
|
const makeStory = (over: Partial<StoryListItem> & { id: string }): StoryListItem => ({
|
|
32
32
|
identifier: "STR-1", name: "t", description: null, state: "todo", priority: "medium", archived: false,
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
import { describe, it, expect } from "vitest";
|
|
19
19
|
import { applyOrder } from "../order-resolver";
|
|
20
|
-
import type { StoryListItem } from "
|
|
20
|
+
import type { StoryListItem } from "@/shared/story/types";
|
|
21
21
|
|
|
22
22
|
const makeStory = (over: Partial<StoryListItem>): StoryListItem => ({
|
|
23
23
|
id: "id1", identifier: "STR-1", name: "test", description: null,
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
import { describe, it, expect } from "vitest";
|
|
19
19
|
import { renderHook } from "@testing-library/react";
|
|
20
20
|
import { useGroupedStories } from "../use-grouped-stories";
|
|
21
|
-
import type { StoryListItem } from "
|
|
21
|
+
import type { StoryListItem } from "@/shared/story/types";
|
|
22
22
|
import { DEFAULT_DISPLAY_OPTIONS } from "../types";
|
|
23
23
|
|
|
24
24
|
const makeStory = (over: Partial<StoryListItem> & { id: string }): StoryListItem => ({
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
19
19
|
import { renderHook, waitFor, act } from "@testing-library/react";
|
|
20
|
-
import type { StoryListItem } from "
|
|
20
|
+
import type { StoryListItem } from "@/shared/story/types";
|
|
21
21
|
import { DEFAULT_DISPLAY_OPTIONS } from "../types";
|
|
22
22
|
import type { PagedStoriesArgs } from "../use-paged-story-groups";
|
|
23
23
|
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
// You should have received a copy of the GNU Affero General Public License
|
|
16
16
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
17
|
|
|
18
|
-
import type { StoryListItem } from "
|
|
18
|
+
import type { StoryListItem } from "@/shared/story/types";
|
|
19
19
|
|
|
20
20
|
/** 顶层/子层分组都只需 items 与 subGroups(GroupNode / PagedGroupNode 结构兼容) */
|
|
21
21
|
type GroupLike = { items: StoryListItem[]; subGroups?: { items: StoryListItem[] }[] };
|
|
@@ -15,9 +15,9 @@
|
|
|
15
15
|
// You should have received a copy of the GNU Affero General Public License
|
|
16
16
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
17
|
|
|
18
|
-
import type { StoryState, StoryPriority } from "
|
|
18
|
+
import type { StoryState, StoryPriority } from "@/shared/story/constants";
|
|
19
19
|
import type { GroupKey } from "./types";
|
|
20
|
-
import type { StoryListItem, UserBrief } from "
|
|
20
|
+
import type { StoryListItem, UserBrief } from "@/shared/story/types";
|
|
21
21
|
|
|
22
22
|
/** 列表行可更新的字段(与 updateStoryFn 入参保持一致),消除动态字段拼接的 as never。
|
|
23
23
|
* name 由详情面板(PeekPanel / 详情页)改动后回写视图,供列表/看板实时乐观更新。 */
|
|
@@ -19,9 +19,9 @@ import { Sprout, CircleUser } from "lucide-react";
|
|
|
19
19
|
import { UserAvatar } from "@/components/shared-kit";
|
|
20
20
|
import { LucideIconByName } from "@/components/shared-kit/icons/lucide-icon-picker/lucide-icon-by-name";
|
|
21
21
|
import { cn } from "@/lib/utils";
|
|
22
|
-
import { STORY_STATES, STATE_LABEL, STATE_COLOR, STATE_ICON, STORY_PRIORITIES, PRIORITY_LABEL, PRIORITY_ICON } from "
|
|
22
|
+
import { STORY_STATES, STATE_LABEL, STATE_COLOR, STATE_ICON, STORY_PRIORITIES, PRIORITY_LABEL, PRIORITY_ICON } from "@/shared/story/constants";
|
|
23
23
|
import { PRIORITY_COLOR } from "../priority-icon";
|
|
24
|
-
import type { StoryListItem } from "
|
|
24
|
+
import type { StoryListItem } from "@/shared/story/types";
|
|
25
25
|
import type { GroupContext, GroupKey, GroupNode } from "./types";
|
|
26
26
|
import { GROUP_FIELD } from "./group-fields";
|
|
27
27
|
import { m } from "@/paraglide/messages.js";
|
|
@@ -43,7 +43,7 @@ const RESOLVERS: Record<Exclude<GroupKey, "none" | "iteration">, KeyResolver> =
|
|
|
43
43
|
project: (item) => [item.projectId ?? "no-project"],
|
|
44
44
|
};
|
|
45
45
|
|
|
46
|
-
export function groupBy(items:
|
|
46
|
+
export function groupBy<T extends StoryListItem = StoryListItem>(items: T[], key: GroupKey, ctx: GroupContext, includeEmpty = false): GroupNode<T>[] {
|
|
47
47
|
if (key === "none") {
|
|
48
48
|
return [{ key: "all", label: m.story_group_all(), items: [...items] }];
|
|
49
49
|
}
|
|
@@ -53,7 +53,7 @@ export function groupBy(items: StoryListItem[], key: GroupKey, ctx: GroupContext
|
|
|
53
53
|
return [{ key: "iteration", label: m.iterations_title(), items: [...items] }];
|
|
54
54
|
}
|
|
55
55
|
const resolver = RESOLVERS[key];
|
|
56
|
-
const buckets = new Map<string,
|
|
56
|
+
const buckets = new Map<string, T[]>();
|
|
57
57
|
const order: string[] = [];
|
|
58
58
|
for (const item of items) {
|
|
59
59
|
const keys = resolver(item);
|