diff --git a/apps/server/src/modules/migration/app-data-v22.mapper.spec.ts b/apps/server/src/modules/migration/app-data-v22.mapper.spec.ts index 80893b6..6b37eff 100644 --- a/apps/server/src/modules/migration/app-data-v22.mapper.spec.ts +++ b/apps/server/src/modules/migration/app-data-v22.mapper.spec.ts @@ -49,7 +49,7 @@ function buildAppData(): Record { typeId: 'type-feature', status: 'adopted', priority: 'P1', - creator: 'member-pm', + creator: 'Product manager', createdAt: '2026-01-02T08:00:00.000Z', updatedAt: '2026-01-02T09:00:00.000Z', }, diff --git a/apps/server/src/modules/migration/app-data-v22.mapper.ts b/apps/server/src/modules/migration/app-data-v22.mapper.ts index 813381a..3184349 100644 --- a/apps/server/src/modules/migration/app-data-v22.mapper.ts +++ b/apps/server/src/modules/migration/app-data-v22.mapper.ts @@ -371,6 +371,7 @@ export function mapAppDataToV22Rows(appData: Record): V22Mapped updatedAt: stringField(member, 'updatedAt') ?? stringField(member, 'createdAt'), }); } + const userIdByReference = buildUserIdByReference(users); const defaultProductId = products[0]?.id; const requirementCode = scopedCodeFactory('REQ'); @@ -400,7 +401,7 @@ export function mapAppDataToV22Rows(appData: Record): V22Mapped sourceType: stringField(requirement, 'sourceType'), sourceTarget: stringField(requirement, 'sourceTarget'), platform: platforms.join(',') || stringField(requirement, 'platform'), - creatorId: stringField(requirement, 'creatorId') ?? stringField(requirement, 'creator'), + creatorId: resolveUserId(stringField(requirement, 'creatorId') ?? stringField(requirement, 'creator'), userIdByReference), createdAt: stringField(requirement, 'createdAt'), updatedAt: stringField(requirement, 'updatedAt') ?? stringField(requirement, 'createdAt'), }; @@ -437,7 +438,7 @@ export function mapAppDataToV22Rows(appData: Record): V22Mapped type: stringField(plan, 'type') ?? 'product', title: stringField(plan, 'title') ?? id, status: stringField(plan, 'status') ?? 'pending', - ownerId: stringField(plan, 'ownerId') ?? stringField(plan, 'owner'), + ownerId: resolveUserId(stringField(plan, 'ownerId') ?? stringField(plan, 'owner'), userIdByReference), expectedStartAt: stringField(plan, 'expectedStartAt') ?? stringField(plan, 'startTime'), expectedEndAt: stringField(plan, 'expectedEndAt') ?? stringField(plan, 'endTime'), actualStartAt: stringField(plan, 'actualStartAt'), @@ -475,8 +476,8 @@ export function mapAppDataToV22Rows(appData: Record): V22Mapped description: stringField(task, 'description') ?? '', status: stringField(task, 'status') ?? 'todo', priority: priorityRank(task.priority), - assigneeId: stringField(task, 'assigneeId'), - creatorId: stringField(task, 'creatorId') ?? stringField(task, 'createdBy'), + assigneeId: resolveUserId(stringField(task, 'assigneeId'), userIdByReference), + creatorId: resolveUserId(stringField(task, 'creatorId') ?? stringField(task, 'createdBy'), userIdByReference), isBlocked: booleanField(task, 'isBlocked') ?? false, blockReason: stringField(task, 'blockReason'), expectedStartAt: stringField(task, 'expectedStartAt'), @@ -519,8 +520,8 @@ export function mapAppDataToV22Rows(appData: Record): V22Mapped status: stringField(testCase, 'status') ?? 'pending', roundNo: integerField(testCase, 'roundNo') ?? 1, priority: priorityRank(testCase.priority), - assigneeId: stringField(testCase, 'assigneeId'), - creatorId: stringField(testCase, 'creatorId') ?? stringField(testCase, 'createdBy'), + assigneeId: resolveUserId(stringField(testCase, 'assigneeId'), userIdByReference), + creatorId: resolveUserId(stringField(testCase, 'creatorId') ?? stringField(testCase, 'createdBy'), userIdByReference), plannedTestAt: stringField(testCase, 'plannedTestAt'), plannedEndAt: stringField(testCase, 'plannedEndAt'), startedAt: stringField(testCase, 'startedAt'), @@ -564,8 +565,8 @@ export function mapAppDataToV22Rows(appData: Record): V22Mapped status: stringField(bug, 'status') ?? 'open', severity: stringField(bug, 'severity') ?? 'minor', priority: priorityRank(bug.priority), - assigneeId: stringField(bug, 'assigneeId'), - reporterId: stringField(bug, 'reporterId') ?? stringField(bug, 'reportedBy'), + assigneeId: resolveUserId(stringField(bug, 'assigneeId'), userIdByReference), + reporterId: resolveUserId(stringField(bug, 'reporterId') ?? stringField(bug, 'reportedBy'), userIdByReference), plannedFixAt: stringField(bug, 'plannedFixAt'), resolvedAt: stringField(bug, 'resolvedAt'), closedAt: stringField(bug, 'closedAt'), @@ -858,6 +859,20 @@ function uniqueEmailFactory() { }; } +function buildUserIdByReference(users: UserRow[]): Map { + const refs = new Map(); + for (const user of users) { + refs.set(user.id, user.id); + refs.set(user.name, user.id); + } + return refs; +} + +function resolveUserId(ref: string | undefined, userIdByReference: Map): string | undefined { + if (!ref) return undefined; + return userIdByReference.get(ref); +} + function normalizeMemberEmail(email: string | undefined, username: string, id: string): string { if (email?.includes('@')) return email; const base = email || username || id; diff --git a/apps/server/src/modules/v22-query/v22-query.service.spec.ts b/apps/server/src/modules/v22-query/v22-query.service.spec.ts index 02de549..a42d150 100644 --- a/apps/server/src/modules/v22-query/v22-query.service.spec.ts +++ b/apps/server/src/modules/v22-query/v22-query.service.spec.ts @@ -44,6 +44,7 @@ describe('V22QueryService', () => { expect(result.devTasks).toEqual([{ id: 'dev-1' }]); expect(prisma.requirement.findMany).toHaveBeenCalledWith({ where: { versionId: 'version-1' }, + include: { creator: { select: { id: true, name: true } } }, orderBy: { createdAt: 'desc' }, }); expect(prisma.devTask.findMany).toHaveBeenCalledWith({ @@ -88,6 +89,7 @@ describe('V22QueryService', () => { { title: { contains: 'login', mode: 'insensitive' } }, ], }, + include: { creator: { select: { id: true, name: true } } }, orderBy: { createdAt: 'asc' }, take: 2, cursor: { id_productId: { id: 'req-3', productId: 'product-1' } }, diff --git a/apps/server/src/modules/v22-query/v22-query.service.ts b/apps/server/src/modules/v22-query/v22-query.service.ts index 165ac6a..bade2a2 100644 --- a/apps/server/src/modules/v22-query/v22-query.service.ts +++ b/apps/server/src/modules/v22-query/v22-query.service.ts @@ -19,6 +19,10 @@ interface XiaobaoWarningQuery { manager?: string; } +const REQUIREMENT_CREATOR_INCLUDE = { + creator: { select: { id: true, name: true } }, +} as const; + @Injectable() export class V22QueryService { constructor(private readonly prisma: PrismaService) {} @@ -30,6 +34,7 @@ export class V22QueryService { const [requirements, versionPlans, devTasks, testCases, bugs] = await Promise.all([ this.prisma.requirement.findMany({ where: { versionId }, + include: REQUIREMENT_CREATOR_INCLUDE, orderBy: { createdAt: 'desc' }, }), this.prisma.versionPlan.findMany({ @@ -80,6 +85,7 @@ export class V22QueryService { } : {}), }, + include: REQUIREMENT_CREATOR_INCLUDE, orderBy: { createdAt: sortDirection }, take: limit + 1, ...(query.cursor diff --git a/apps/web/app/requirements/page.tsx b/apps/web/app/requirements/page.tsx index 789c1d7..84332b1 100644 --- a/apps/web/app/requirements/page.tsx +++ b/apps/web/app/requirements/page.tsx @@ -406,12 +406,20 @@ function RequirementsPageContent() { return list; }, [scopedRequirements, search, statusFilter, priorityFilter, typeFilter, versionFilter, dateSort, requirementDevStatusMap]); - const appDataRequirementIds = useMemo(() => new Set(requirements.map((item) => item.id)), [requirements]); + const appDataRequirementById = useMemo(() => new Map(requirements.map((item) => [item.id, item])), [requirements]); + const appDataRequirementIds = useMemo(() => new Set(appDataRequirementById.keys()), [appDataRequirementById]); const usingV22Requirements = Boolean(v22RequirementQuery) && !v22RequirementsFailed; const appTotalPages = Math.max(1, Math.ceil(filtered.length / pageSize)); const appPage = Math.min(page, appTotalPages); const appPaged = filtered.slice((appPage - 1) * pageSize, appPage * pageSize); - const displayedRequirements = usingV22Requirements ? v22Requirements : appPaged; + const displayedRequirements = useMemo(() => { + if (!usingV22Requirements) return appPaged; + return v22Requirements.map((req) => { + if (req.creator) return req; + const appDataRequirement = appDataRequirementById.get(req.id); + return appDataRequirement?.creator ? { ...req, creator: appDataRequirement.creator } : req; + }); + }, [appDataRequirementById, appPaged, usingV22Requirements, v22Requirements]); const total = usingV22Requirements ? (page - 1) * pageSize + v22Requirements.length + (v22NextCursor ? 1 : 0) : filtered.length; diff --git a/apps/web/lib/v22-api.test.ts b/apps/web/lib/v22-api.test.ts index c55dee1..7abed3f 100644 --- a/apps/web/lib/v22-api.test.ts +++ b/apps/web/lib/v22-api.test.ts @@ -45,6 +45,7 @@ test('V2.2 version detail API maps relation rows to existing frontend scope type sourceTarget: 'ACME', platform: 'web, ios', creatorId: 'member-1', + creator: { id: 'member-1', name: 'Product manager' }, createdAt: '2026-01-01T00:00:00.000Z', }], versionPlans: [{ @@ -129,7 +130,7 @@ test('V2.2 version detail API maps relation rows to existing frontend scope type assert.equal(result.scope.requirements[0].typeId, 'feature'); assert.deepEqual(result.scope.requirements[0].platforms, ['web', 'ios']); assert.equal(result.scope.requirements[0].priority, 'P1'); - assert.equal(result.scope.requirements[0].creator, 'member-1'); + assert.equal(result.scope.requirements[0].creator, 'Product manager'); assert.equal(result.scope.devTasks[0].taskNo, 'DEV-001'); assert.equal(result.scope.devTasks[0].actualStartAt, '2026-01-02T09:00:00.000Z'); assert.equal(result.scope.devTasks[0].actualEndAt, undefined); diff --git a/apps/web/lib/v22-api.ts b/apps/web/lib/v22-api.ts index d1f8b83..da4efb7 100644 --- a/apps/web/lib/v22-api.ts +++ b/apps/web/lib/v22-api.ts @@ -39,6 +39,8 @@ interface V22RequirementRow { sourceTarget?: string | null; platform?: string | null; creatorId?: string | null; + creatorName?: string | null; + creator?: { id?: string | null; name?: string | null } | null; createdAt?: V22DateValue; updatedAt?: V22DateValue; } @@ -284,7 +286,7 @@ function mapRequirement(row: V22RequirementRow): Requirement { status: toRequirementStatus(row.status), priority: toPriority(row.priority), effort: 'M', - creator: row.creatorId ?? '', + creator: row.creator?.name ?? row.creatorName ?? row.creatorId ?? '', createdAt: requiredIso(row.createdAt), }; }