fix(asset-center): scope hardware families by owning series

This commit is contained in:
WuYongJie 2026-06-03 17:13:40 +08:00
parent b6c2a8ec62
commit 252c744ddd
7 changed files with 80 additions and 29 deletions

View File

@ -26,6 +26,10 @@ jest.mock('next/link', () => ({
),
}))
jest.mock('next/navigation', () => ({
useRouter: () => ({ push: jest.fn() }),
}))
jest.mock('@/components/ui/Modal', () => ({
__esModule: true,
default: () => null,
@ -265,6 +269,7 @@ function createFamily(overrides?: Partial<AssetFamilyDTO>): AssetFamilyDTO {
description: null,
productCategoryId: null,
productCategoryName: null,
assetSeriesId: 'template-1',
compatibleSeries: [{ id: 'template-1', code: 'single-board-desk', name: '单板桌' }],
connectionRoles: [],
metadata: null,

View File

@ -247,6 +247,7 @@ function createFamily(overrides?: Partial<AssetFamilyDTO>): AssetFamilyDTO {
description: null,
productCategoryId: null,
productCategoryName: null,
assetSeriesId: 'template-1',
compatibleSeries: [{ id: 'template-1', code: 'single-board-desk', name: '单板桌' }],
connectionRoles: [],
metadata: null,

View File

@ -1258,16 +1258,11 @@ function getVisibleFamiliesForTab(
function familyBelongsToSeries(family: AssetFamilyDTO, series: AssetSeriesDTO | null): boolean {
if (!series) return false
if (family.assetSeriesId) {
return family.assetSeriesId === series.id
}
const compatibleSeries = family.compatibleSeries ?? []
return compatibleSeries.some((item) => item.id === series.id || item.code === series.code)
return family.assetSeriesId === series.id
}
function familyVisibleInSeriesContext(family: AssetFamilyDTO, series: AssetSeriesDTO | null): boolean {
if (isPackagingMaterialLibraryFamily(family)) return false
if (family.type === 'HARDWARE') return true
return familyBelongsToSeries(family, series)
}
@ -2250,7 +2245,6 @@ export default function AssetCenterClient({
const count = families.filter(
(family) =>
familyBelongsToSeries(family, series) &&
family.type !== 'HARDWARE' &&
!isPackagingMaterialLibraryFamily(family),
).length
map.set(series.id, count)
@ -2824,7 +2818,7 @@ export default function AssetCenterClient({
setFamilyRoleDefinitions(
mergeConnectionRoleDefinitions({
assetType: defaultAssetType,
seriesCode: defaultAssetType === 'HARDWARE' ? null : defaultSeriesCode,
seriesCode: defaultSeriesCode,
}),
)
setFamilyModalOpen(true)
@ -2969,9 +2963,7 @@ export default function AssetCenterClient({
const submittedAssetSeriesId = familyForm.assetSeriesId || null
const submittedCompatibleSeriesIds = submittedAssetSeriesId ? [submittedAssetSeriesId] : []
const primarySeriesCode =
familyForm.type === 'HARDWARE'
? null
: initialAssetSeries.find((series) => series.id === submittedAssetSeriesId)?.code ?? null
initialAssetSeries.find((series) => series.id === submittedAssetSeriesId)?.code ?? null
const hiddenRecommendedRoleValues = getStandardConnectionRoleDefinitions({
assetType: familyForm.type,
seriesCode: primarySeriesCode,
@ -4485,9 +4477,7 @@ export default function AssetCenterClient({
setFamilyRoleDefinitions((current) => {
const primarySeriesCode =
familyForm.type === 'HARDWARE'
? null
: initialAssetSeries.find((series) => series.id === familyForm.assetSeriesId)?.code ?? null
initialAssetSeries.find((series) => series.id === familyForm.assetSeriesId)?.code ?? null
const merged = mergeConnectionRoleDefinitions({
assetType: familyForm.type,
seriesCode: primarySeriesCode,
@ -6531,8 +6521,7 @@ export default function AssetCenterClient({
placeholder="例如 单板桌桌面板"
/>
</div>
{familyForm.type !== 'HARDWARE' ? (
<Select
<Select
label="归属系列"
required
searchable
@ -6562,8 +6551,7 @@ export default function AssetCenterClient({
label: `${series.name} · ${series.code}`,
}))}
placeholder="请选择归属系列"
/>
) : null}
/>
<details className="group rounded-apple-lg border border-apple-separator bg-apple-bg-secondary/30">
<summary className="flex cursor-pointer list-none items-center justify-between gap-3 px-4 py-3">
<div className="flex items-center gap-2">

View File

@ -21,6 +21,10 @@ jest.mock('next/link', () => ({
),
}))
jest.mock('next/navigation', () => ({
useRouter: () => ({ push: jest.fn() }),
}))
jest.mock('@/components/ui/Modal', () => ({
__esModule: true,
default: ({
@ -227,6 +231,7 @@ describe('AssetCenterClient workflow', () => {
description: null,
productCategoryId: null,
productCategoryName: null,
assetSeriesId: 'template-1',
compatibleSeries: [{ id: 'template-1', code: 'single-board-desk', name: '单板桌' }],
connectionRoles: ['desktop_surface'],
metadata: null,
@ -278,6 +283,7 @@ describe('AssetCenterClient workflow', () => {
description: null,
productCategoryId: null,
productCategoryName: null,
assetSeriesId: 'template-1',
compatibleSeries: [{ id: 'template-1', code: 'single-board-desk', name: '单板桌' }],
connectionRoles: ['desktop_surface'],
metadata: {
@ -350,7 +356,7 @@ describe('AssetCenterClient workflow', () => {
await actor.click(screen.getByRole('button', { name: 'CAD 导入到该资产族' }))
expect(screen.getByText('导入 CAD 规格款 · 板材')).toBeInTheDocument()
expect(screen.getByText('系统自动生成')).toBeInTheDocument()
expect(screen.getByPlaceholderText('例如 桌面板 1200 / 方管桌腿 / 十字连接件')).toHaveValue('BOARD-001-')
expect(screen.getByPlaceholderText('例如 桌面板 1200 / 方管桌腿 / 十字连接件')).toHaveValue('')
expect(screen.getByTestId('cad-import-target-family')).toHaveStyle({
backgroundColor: 'var(--obs-shell-surface-bg)',
borderColor: 'var(--obs-shell-border)',
@ -378,6 +384,7 @@ describe('AssetCenterClient workflow', () => {
description: null,
productCategoryId: null,
productCategoryName: null,
assetSeriesId: 'template-1',
compatibleSeries: [{ id: 'template-1', code: 'single-board-desk', name: '单板桌' }],
connectionRoles: [],
metadata: null,
@ -435,6 +442,7 @@ describe('AssetCenterClient workflow', () => {
description: null,
productCategoryId: null,
productCategoryName: null,
assetSeriesId: 'template-1',
compatibleSeries: [{ id: 'template-1', code: 'single-board-desk', name: '单板桌' }],
connectionRoles: ['cross_support'],
metadata: {
@ -467,6 +475,7 @@ describe('AssetCenterClient workflow', () => {
description: null,
productCategoryId: null,
productCategoryName: null,
assetSeriesId: 'template-1',
compatibleSeries: [{ id: 'template-1', code: 'single-board-desk', name: '单板桌' }],
connectionRoles: ['cross_support'],
metadata: {
@ -752,6 +761,7 @@ describe('AssetCenterClient workflow', () => {
description: null,
productCategoryId: null,
productCategoryName: null,
assetSeriesId: 'template-1',
compatibleSeries: [{ id: 'template-1', code: 'single-board-desk', name: '单板桌' }],
connectionRoles: ['desktop_surface'],
metadata: null,
@ -822,6 +832,7 @@ describe('AssetCenterClient workflow', () => {
description: null,
productCategoryId: null,
productCategoryName: null,
assetSeriesId: 'template-1',
compatibleSeries: [{ id: 'template-1', code: 'single-board-desk', name: '单板桌' }],
connectionRoles: ['desktop_surface'],
metadata: null,

View File

@ -137,8 +137,7 @@ export function FamilyEditModal({
placeholder="例如 单板桌桌面板"
/>
</div>
{form.type !== 'HARDWARE' ? (
<div className="space-y-2">
<div className="space-y-2">
<p className="text-xs font-bold text-[#2d3335]">归属系列</p>
<div className="grid grid-cols-2 gap-2">
{assetSeries.map((series) => {
@ -173,8 +172,7 @@ export function FamilyEditModal({
)
})}
</div>
</div>
) : null}
</div>
<details className="group rounded-apple-lg border border-apple-separator bg-apple-bg-secondary/30">
<summary className="flex cursor-pointer list-none items-center justify-between gap-3 px-4 py-3">
<div className="flex items-center gap-2">

View File

@ -0,0 +1,53 @@
import type { AssetFamilyDTO, AssetSeriesDTO } from '@/server/services'
import { familyVisibleInSeriesContext } from './asset-center-utils'
const dbzSeries = {
id: 'series-dbz',
code: 'DBZ',
name: 'single board desk',
} as AssetSeriesDTO
const cbzSeries = {
id: 'series-cbz',
code: 'CBZ',
name: 'bedside table',
} as AssetSeriesDTO
function createFamily(overrides?: Partial<AssetFamilyDTO>): AssetFamilyDTO {
return {
id: 'family-hardware-dbz',
type: 'HARDWARE',
code: 'DBZ-SPJK05',
name: 'hardware',
description: null,
productCategoryId: null,
productCategoryName: null,
assetSeriesId: dbzSeries.id,
compatibleSeries: [dbzSeries, cbzSeries],
connectionRoles: [],
metadata: null,
status: 'AVAILABLE',
createdBy: 'user-1',
creatorName: 'Test User',
workflow: {} as AssetFamilyDTO['workflow'],
createdAt: new Date('2026-06-03T00:00:00.000Z'),
updatedAt: new Date('2026-06-03T00:00:00.000Z'),
...overrides,
}
}
describe('asset center series visibility', () => {
it('keeps hardware visible only in its owning series', () => {
const dbzHardware = createFamily()
expect(familyVisibleInSeriesContext(dbzHardware, dbzSeries)).toBe(true)
expect(familyVisibleInSeriesContext(dbzHardware, cbzSeries)).toBe(false)
})
it('does not fall back to compatible series without an owning series', () => {
const legacyFamily = createFamily({ assetSeriesId: null })
expect(familyVisibleInSeriesContext(legacyFamily, dbzSeries)).toBe(false)
expect(familyVisibleInSeriesContext(legacyFamily, cbzSeries)).toBe(false)
})
})

View File

@ -517,16 +517,11 @@ export function getVisibleFamiliesForTab(
export function familyBelongsToSeries(family: AssetFamilyDTO, series: AssetSeriesDTO | null): boolean {
if (!series) return false
if (family.assetSeriesId) {
return family.assetSeriesId === series.id
}
const compatibleSeries = family.compatibleSeries ?? []
return compatibleSeries.some((item) => item.id === series.id || item.code === series.code)
return family.assetSeriesId === series.id
}
export function familyVisibleInSeriesContext(family: AssetFamilyDTO, series: AssetSeriesDTO | null): boolean {
if (isPackagingMaterialLibraryFamily(family)) return false
if (family.type === 'HARDWARE') return true
return familyBelongsToSeries(family, series)
}