fix(需求池): 修复列表换行和版本解绑状态
This commit is contained in:
22
apps/web/lib/requirement-table-layout.test.ts
Normal file
22
apps/web/lib/requirement-table-layout.test.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
|
||||
import {
|
||||
REQUIREMENT_TABLE_BADGE_CLASS,
|
||||
getRequirementTableTextClass,
|
||||
} from './requirement-table-layout';
|
||||
|
||||
test('keeps requirement table badges on one line', () => {
|
||||
assert.match(REQUIREMENT_TABLE_BADGE_CLASS, /\bwhitespace-nowrap\b/);
|
||||
assert.match(REQUIREMENT_TABLE_BADGE_CLASS, /\bshrink-0\b/);
|
||||
});
|
||||
|
||||
test('clips long requirement source project type and creator cells', () => {
|
||||
for (const column of ['source', 'project', 'type', 'creator'] as const) {
|
||||
const className = getRequirementTableTextClass(column);
|
||||
|
||||
assert.match(className, /\btruncate\b/);
|
||||
assert.match(className, /\bblock\b/);
|
||||
assert.match(className, /\bmax-w-\[/);
|
||||
}
|
||||
});
|
||||
20
apps/web/lib/requirement-table-layout.ts
Normal file
20
apps/web/lib/requirement-table-layout.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
export type RequirementTableTextColumn = 'source' | 'project' | 'type' | 'creator';
|
||||
|
||||
const TEXT_COLUMN_MAX_WIDTH: Record<RequirementTableTextColumn, string> = {
|
||||
source: 'max-w-[170px]',
|
||||
project: 'max-w-[120px]',
|
||||
type: 'max-w-[96px]',
|
||||
creator: 'max-w-[96px]',
|
||||
};
|
||||
|
||||
export const REQUIREMENT_TABLE_NOWRAP_CELL_CLASS = 'px-4 py-3 whitespace-nowrap';
|
||||
|
||||
export const REQUIREMENT_TABLE_BADGE_CLASS =
|
||||
'inline-flex h-6 min-w-[3.25rem] shrink-0 items-center justify-center whitespace-nowrap rounded-md px-2 py-0.5 text-[11px] font-medium';
|
||||
|
||||
export const REQUIREMENT_TABLE_SMALL_BADGE_CLASS =
|
||||
'inline-flex h-5 min-w-[2.75rem] shrink-0 items-center justify-center whitespace-nowrap rounded-md px-2 py-0.5 text-[10px] font-medium';
|
||||
|
||||
export function getRequirementTableTextClass(column: RequirementTableTextColumn): string {
|
||||
return `block truncate ${TEXT_COLUMN_MAX_WIDTH[column]}`;
|
||||
}
|
||||
63
apps/web/lib/requirement-version-link.test.ts
Normal file
63
apps/web/lib/requirement-version-link.test.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
|
||||
import type { Requirement } from './requirement';
|
||||
import {
|
||||
getRequirementVersionSelectionPatch,
|
||||
getRequirementVersionUnlinkPatch,
|
||||
} from './requirement-version-link';
|
||||
|
||||
function requirement(patch: Partial<Requirement> = {}): Requirement {
|
||||
return {
|
||||
id: 'req-1',
|
||||
code: 'REQ-001',
|
||||
title: 'Requirement',
|
||||
description: '',
|
||||
productId: 'product-1',
|
||||
projectId: 'project-1',
|
||||
versionId: 'version-1',
|
||||
sourceType: 'internal',
|
||||
sourceTarget: '',
|
||||
platforms: [],
|
||||
typeId: 'type-1',
|
||||
status: 'planned',
|
||||
priority: 'P2',
|
||||
effort: 'M',
|
||||
creator: 'tester',
|
||||
createdAt: '2026-07-02T00:00:00.000Z',
|
||||
addedToVersionBy: 'pm',
|
||||
...patch,
|
||||
};
|
||||
}
|
||||
|
||||
test('unlinking a version-scoped requirement returns it to adopted', () => {
|
||||
for (const status of ['planned', 'developing', 'testing', 'released'] as const) {
|
||||
assert.deepEqual(getRequirementVersionUnlinkPatch(requirement({ status })), {
|
||||
versionId: undefined,
|
||||
addedToVersionBy: undefined,
|
||||
status: 'adopted',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test('unlinking keeps other requirement statuses unchanged', () => {
|
||||
assert.deepEqual(getRequirementVersionUnlinkPatch(requirement({ status: 'adopted' })), {
|
||||
versionId: undefined,
|
||||
addedToVersionBy: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
test('clearing version from requirement edit uses the same unlink semantics', () => {
|
||||
assert.deepEqual(getRequirementVersionSelectionPatch(requirement(), ''), {
|
||||
versionId: undefined,
|
||||
addedToVersionBy: undefined,
|
||||
status: 'adopted',
|
||||
});
|
||||
});
|
||||
|
||||
test('selecting a version preserves the current business status', () => {
|
||||
assert.deepEqual(getRequirementVersionSelectionPatch(requirement({ status: 'adopted' }), 'version-2', 'pm2'), {
|
||||
versionId: 'version-2',
|
||||
addedToVersionBy: 'pm2',
|
||||
});
|
||||
});
|
||||
29
apps/web/lib/requirement-version-link.ts
Normal file
29
apps/web/lib/requirement-version-link.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import type { Requirement } from './requirement';
|
||||
|
||||
type RequirementVersionPatch = Pick<Partial<Requirement>, 'versionId' | 'addedToVersionBy' | 'status'>;
|
||||
const VERSION_SCOPED_REQUIREMENT_STATUSES = new Set<Requirement['status']>(['planned', 'developing', 'testing', 'released']);
|
||||
|
||||
export function getRequirementVersionUnlinkPatch(
|
||||
requirement: Pick<Requirement, 'status'>,
|
||||
): RequirementVersionPatch {
|
||||
return {
|
||||
versionId: undefined,
|
||||
addedToVersionBy: undefined,
|
||||
...(VERSION_SCOPED_REQUIREMENT_STATUSES.has(requirement.status) ? { status: 'adopted' as const } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
export function getRequirementVersionSelectionPatch(
|
||||
requirement: Pick<Requirement, 'status' | 'addedToVersionBy'>,
|
||||
versionId?: string,
|
||||
addedBy?: string,
|
||||
): RequirementVersionPatch {
|
||||
if (!versionId) {
|
||||
return getRequirementVersionUnlinkPatch(requirement);
|
||||
}
|
||||
|
||||
return {
|
||||
versionId,
|
||||
addedToVersionBy: addedBy || requirement.addedToVersionBy,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user