fix(requirement): 修复需求池录入人员显示
This commit is contained in:
@@ -49,7 +49,7 @@ function buildAppData(): Record<string, any> {
|
|||||||
typeId: 'type-feature',
|
typeId: 'type-feature',
|
||||||
status: 'adopted',
|
status: 'adopted',
|
||||||
priority: 'P1',
|
priority: 'P1',
|
||||||
creator: 'member-pm',
|
creator: 'Product manager',
|
||||||
createdAt: '2026-01-02T08:00:00.000Z',
|
createdAt: '2026-01-02T08:00:00.000Z',
|
||||||
updatedAt: '2026-01-02T09:00:00.000Z',
|
updatedAt: '2026-01-02T09:00:00.000Z',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -371,6 +371,7 @@ export function mapAppDataToV22Rows(appData: Record<string, unknown>): V22Mapped
|
|||||||
updatedAt: stringField(member, 'updatedAt') ?? stringField(member, 'createdAt'),
|
updatedAt: stringField(member, 'updatedAt') ?? stringField(member, 'createdAt'),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
const userIdByReference = buildUserIdByReference(users);
|
||||||
|
|
||||||
const defaultProductId = products[0]?.id;
|
const defaultProductId = products[0]?.id;
|
||||||
const requirementCode = scopedCodeFactory('REQ');
|
const requirementCode = scopedCodeFactory('REQ');
|
||||||
@@ -400,7 +401,7 @@ export function mapAppDataToV22Rows(appData: Record<string, unknown>): V22Mapped
|
|||||||
sourceType: stringField(requirement, 'sourceType'),
|
sourceType: stringField(requirement, 'sourceType'),
|
||||||
sourceTarget: stringField(requirement, 'sourceTarget'),
|
sourceTarget: stringField(requirement, 'sourceTarget'),
|
||||||
platform: platforms.join(',') || stringField(requirement, 'platform'),
|
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'),
|
createdAt: stringField(requirement, 'createdAt'),
|
||||||
updatedAt: stringField(requirement, 'updatedAt') ?? stringField(requirement, 'createdAt'),
|
updatedAt: stringField(requirement, 'updatedAt') ?? stringField(requirement, 'createdAt'),
|
||||||
};
|
};
|
||||||
@@ -437,7 +438,7 @@ export function mapAppDataToV22Rows(appData: Record<string, unknown>): V22Mapped
|
|||||||
type: stringField(plan, 'type') ?? 'product',
|
type: stringField(plan, 'type') ?? 'product',
|
||||||
title: stringField(plan, 'title') ?? id,
|
title: stringField(plan, 'title') ?? id,
|
||||||
status: stringField(plan, 'status') ?? 'pending',
|
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'),
|
expectedStartAt: stringField(plan, 'expectedStartAt') ?? stringField(plan, 'startTime'),
|
||||||
expectedEndAt: stringField(plan, 'expectedEndAt') ?? stringField(plan, 'endTime'),
|
expectedEndAt: stringField(plan, 'expectedEndAt') ?? stringField(plan, 'endTime'),
|
||||||
actualStartAt: stringField(plan, 'actualStartAt'),
|
actualStartAt: stringField(plan, 'actualStartAt'),
|
||||||
@@ -475,8 +476,8 @@ export function mapAppDataToV22Rows(appData: Record<string, unknown>): V22Mapped
|
|||||||
description: stringField(task, 'description') ?? '',
|
description: stringField(task, 'description') ?? '',
|
||||||
status: stringField(task, 'status') ?? 'todo',
|
status: stringField(task, 'status') ?? 'todo',
|
||||||
priority: priorityRank(task.priority),
|
priority: priorityRank(task.priority),
|
||||||
assigneeId: stringField(task, 'assigneeId'),
|
assigneeId: resolveUserId(stringField(task, 'assigneeId'), userIdByReference),
|
||||||
creatorId: stringField(task, 'creatorId') ?? stringField(task, 'createdBy'),
|
creatorId: resolveUserId(stringField(task, 'creatorId') ?? stringField(task, 'createdBy'), userIdByReference),
|
||||||
isBlocked: booleanField(task, 'isBlocked') ?? false,
|
isBlocked: booleanField(task, 'isBlocked') ?? false,
|
||||||
blockReason: stringField(task, 'blockReason'),
|
blockReason: stringField(task, 'blockReason'),
|
||||||
expectedStartAt: stringField(task, 'expectedStartAt'),
|
expectedStartAt: stringField(task, 'expectedStartAt'),
|
||||||
@@ -519,8 +520,8 @@ export function mapAppDataToV22Rows(appData: Record<string, unknown>): V22Mapped
|
|||||||
status: stringField(testCase, 'status') ?? 'pending',
|
status: stringField(testCase, 'status') ?? 'pending',
|
||||||
roundNo: integerField(testCase, 'roundNo') ?? 1,
|
roundNo: integerField(testCase, 'roundNo') ?? 1,
|
||||||
priority: priorityRank(testCase.priority),
|
priority: priorityRank(testCase.priority),
|
||||||
assigneeId: stringField(testCase, 'assigneeId'),
|
assigneeId: resolveUserId(stringField(testCase, 'assigneeId'), userIdByReference),
|
||||||
creatorId: stringField(testCase, 'creatorId') ?? stringField(testCase, 'createdBy'),
|
creatorId: resolveUserId(stringField(testCase, 'creatorId') ?? stringField(testCase, 'createdBy'), userIdByReference),
|
||||||
plannedTestAt: stringField(testCase, 'plannedTestAt'),
|
plannedTestAt: stringField(testCase, 'plannedTestAt'),
|
||||||
plannedEndAt: stringField(testCase, 'plannedEndAt'),
|
plannedEndAt: stringField(testCase, 'plannedEndAt'),
|
||||||
startedAt: stringField(testCase, 'startedAt'),
|
startedAt: stringField(testCase, 'startedAt'),
|
||||||
@@ -564,8 +565,8 @@ export function mapAppDataToV22Rows(appData: Record<string, unknown>): V22Mapped
|
|||||||
status: stringField(bug, 'status') ?? 'open',
|
status: stringField(bug, 'status') ?? 'open',
|
||||||
severity: stringField(bug, 'severity') ?? 'minor',
|
severity: stringField(bug, 'severity') ?? 'minor',
|
||||||
priority: priorityRank(bug.priority),
|
priority: priorityRank(bug.priority),
|
||||||
assigneeId: stringField(bug, 'assigneeId'),
|
assigneeId: resolveUserId(stringField(bug, 'assigneeId'), userIdByReference),
|
||||||
reporterId: stringField(bug, 'reporterId') ?? stringField(bug, 'reportedBy'),
|
reporterId: resolveUserId(stringField(bug, 'reporterId') ?? stringField(bug, 'reportedBy'), userIdByReference),
|
||||||
plannedFixAt: stringField(bug, 'plannedFixAt'),
|
plannedFixAt: stringField(bug, 'plannedFixAt'),
|
||||||
resolvedAt: stringField(bug, 'resolvedAt'),
|
resolvedAt: stringField(bug, 'resolvedAt'),
|
||||||
closedAt: stringField(bug, 'closedAt'),
|
closedAt: stringField(bug, 'closedAt'),
|
||||||
@@ -858,6 +859,20 @@ function uniqueEmailFactory() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildUserIdByReference(users: UserRow[]): Map<string, string> {
|
||||||
|
const refs = new Map<string, string>();
|
||||||
|
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, string>): string | undefined {
|
||||||
|
if (!ref) return undefined;
|
||||||
|
return userIdByReference.get(ref);
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeMemberEmail(email: string | undefined, username: string, id: string): string {
|
function normalizeMemberEmail(email: string | undefined, username: string, id: string): string {
|
||||||
if (email?.includes('@')) return email;
|
if (email?.includes('@')) return email;
|
||||||
const base = email || username || id;
|
const base = email || username || id;
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ describe('V22QueryService', () => {
|
|||||||
expect(result.devTasks).toEqual([{ id: 'dev-1' }]);
|
expect(result.devTasks).toEqual([{ id: 'dev-1' }]);
|
||||||
expect(prisma.requirement.findMany).toHaveBeenCalledWith({
|
expect(prisma.requirement.findMany).toHaveBeenCalledWith({
|
||||||
where: { versionId: 'version-1' },
|
where: { versionId: 'version-1' },
|
||||||
|
include: { creator: { select: { id: true, name: true } } },
|
||||||
orderBy: { createdAt: 'desc' },
|
orderBy: { createdAt: 'desc' },
|
||||||
});
|
});
|
||||||
expect(prisma.devTask.findMany).toHaveBeenCalledWith({
|
expect(prisma.devTask.findMany).toHaveBeenCalledWith({
|
||||||
@@ -88,6 +89,7 @@ describe('V22QueryService', () => {
|
|||||||
{ title: { contains: 'login', mode: 'insensitive' } },
|
{ title: { contains: 'login', mode: 'insensitive' } },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
include: { creator: { select: { id: true, name: true } } },
|
||||||
orderBy: { createdAt: 'asc' },
|
orderBy: { createdAt: 'asc' },
|
||||||
take: 2,
|
take: 2,
|
||||||
cursor: { id_productId: { id: 'req-3', productId: 'product-1' } },
|
cursor: { id_productId: { id: 'req-3', productId: 'product-1' } },
|
||||||
|
|||||||
@@ -19,6 +19,10 @@ interface XiaobaoWarningQuery {
|
|||||||
manager?: string;
|
manager?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const REQUIREMENT_CREATOR_INCLUDE = {
|
||||||
|
creator: { select: { id: true, name: true } },
|
||||||
|
} as const;
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class V22QueryService {
|
export class V22QueryService {
|
||||||
constructor(private readonly prisma: PrismaService) {}
|
constructor(private readonly prisma: PrismaService) {}
|
||||||
@@ -30,6 +34,7 @@ export class V22QueryService {
|
|||||||
const [requirements, versionPlans, devTasks, testCases, bugs] = await Promise.all([
|
const [requirements, versionPlans, devTasks, testCases, bugs] = await Promise.all([
|
||||||
this.prisma.requirement.findMany({
|
this.prisma.requirement.findMany({
|
||||||
where: { versionId },
|
where: { versionId },
|
||||||
|
include: REQUIREMENT_CREATOR_INCLUDE,
|
||||||
orderBy: { createdAt: 'desc' },
|
orderBy: { createdAt: 'desc' },
|
||||||
}),
|
}),
|
||||||
this.prisma.versionPlan.findMany({
|
this.prisma.versionPlan.findMany({
|
||||||
@@ -80,6 +85,7 @@ export class V22QueryService {
|
|||||||
}
|
}
|
||||||
: {}),
|
: {}),
|
||||||
},
|
},
|
||||||
|
include: REQUIREMENT_CREATOR_INCLUDE,
|
||||||
orderBy: { createdAt: sortDirection },
|
orderBy: { createdAt: sortDirection },
|
||||||
take: limit + 1,
|
take: limit + 1,
|
||||||
...(query.cursor
|
...(query.cursor
|
||||||
|
|||||||
@@ -406,12 +406,20 @@ function RequirementsPageContent() {
|
|||||||
return list;
|
return list;
|
||||||
}, [scopedRequirements, search, statusFilter, priorityFilter, typeFilter, versionFilter, dateSort, requirementDevStatusMap]);
|
}, [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 usingV22Requirements = Boolean(v22RequirementQuery) && !v22RequirementsFailed;
|
||||||
const appTotalPages = Math.max(1, Math.ceil(filtered.length / pageSize));
|
const appTotalPages = Math.max(1, Math.ceil(filtered.length / pageSize));
|
||||||
const appPage = Math.min(page, appTotalPages);
|
const appPage = Math.min(page, appTotalPages);
|
||||||
const appPaged = filtered.slice((appPage - 1) * pageSize, appPage * pageSize);
|
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
|
const total = usingV22Requirements
|
||||||
? (page - 1) * pageSize + v22Requirements.length + (v22NextCursor ? 1 : 0)
|
? (page - 1) * pageSize + v22Requirements.length + (v22NextCursor ? 1 : 0)
|
||||||
: filtered.length;
|
: filtered.length;
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ test('V2.2 version detail API maps relation rows to existing frontend scope type
|
|||||||
sourceTarget: 'ACME',
|
sourceTarget: 'ACME',
|
||||||
platform: 'web, ios',
|
platform: 'web, ios',
|
||||||
creatorId: 'member-1',
|
creatorId: 'member-1',
|
||||||
|
creator: { id: 'member-1', name: 'Product manager' },
|
||||||
createdAt: '2026-01-01T00:00:00.000Z',
|
createdAt: '2026-01-01T00:00:00.000Z',
|
||||||
}],
|
}],
|
||||||
versionPlans: [{
|
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.equal(result.scope.requirements[0].typeId, 'feature');
|
||||||
assert.deepEqual(result.scope.requirements[0].platforms, ['web', 'ios']);
|
assert.deepEqual(result.scope.requirements[0].platforms, ['web', 'ios']);
|
||||||
assert.equal(result.scope.requirements[0].priority, 'P1');
|
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].taskNo, 'DEV-001');
|
||||||
assert.equal(result.scope.devTasks[0].actualStartAt, '2026-01-02T09:00:00.000Z');
|
assert.equal(result.scope.devTasks[0].actualStartAt, '2026-01-02T09:00:00.000Z');
|
||||||
assert.equal(result.scope.devTasks[0].actualEndAt, undefined);
|
assert.equal(result.scope.devTasks[0].actualEndAt, undefined);
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ interface V22RequirementRow {
|
|||||||
sourceTarget?: string | null;
|
sourceTarget?: string | null;
|
||||||
platform?: string | null;
|
platform?: string | null;
|
||||||
creatorId?: string | null;
|
creatorId?: string | null;
|
||||||
|
creatorName?: string | null;
|
||||||
|
creator?: { id?: string | null; name?: string | null } | null;
|
||||||
createdAt?: V22DateValue;
|
createdAt?: V22DateValue;
|
||||||
updatedAt?: V22DateValue;
|
updatedAt?: V22DateValue;
|
||||||
}
|
}
|
||||||
@@ -284,7 +286,7 @@ function mapRequirement(row: V22RequirementRow): Requirement {
|
|||||||
status: toRequirementStatus(row.status),
|
status: toRequirementStatus(row.status),
|
||||||
priority: toPriority(row.priority),
|
priority: toPriority(row.priority),
|
||||||
effort: 'M',
|
effort: 'M',
|
||||||
creator: row.creatorId ?? '',
|
creator: row.creator?.name ?? row.creatorName ?? row.creatorId ?? '',
|
||||||
createdAt: requiredIso(row.createdAt),
|
createdAt: requiredIso(row.createdAt),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user