fix: resolve merge type errors — smart quotes, nullable surfaceOption, remove stale test

This commit is contained in:
Codex 2026-04-10 21:03:06 +08:00
parent 7e77da5e7c
commit 9959f43dfb
4 changed files with 73 additions and 204 deletions

View File

@ -369,7 +369,7 @@ export default function MaterialLibraryClient({
categoryText: s.categoryName,
environmentGradeId: s.environmentGradeId,
thicknessSpecId: s.thicknessSpecId,
surfaceOptionId: s.surfaceOptionId,
surfaceOptionId: s.surfaceOptionId ?? '',
systemNameOptionId: s.systemNameOptionId,
suggestedPrice: s.suggestedPrice != null ? String(s.suggestedPrice) : '',
weightKg: s.weightKg != null ? String(s.weightKg) : '',
@ -956,37 +956,37 @@ export default function MaterialLibraryClient({
<>
<PlatformSplitLayout
title="基础资料库"
description=”饰面花色和板材基材的基础数据维护。”
description="饰面花色和板材基材的基础数据维护。"
metrics={metrics}
rightPanelClassName=”w-[680px] max-w-[58vw]”
rightPanelClassName="w-[680px] max-w-[58vw]"
leftContent={
/* ---- LEFT: List panel ---- */
<div className=”flex h-full flex-col gap-3”>
<div className="flex h-full flex-col gap-3">
{/* Tab switcher */}
<div className=”flex items-center gap-1 rounded-apple-lg bg-apple-bg-tertiary p-1”>
<button type=”button” onClick={() => { setActiveTab('finish'); setPanel({ mode: 'empty' }) }}
<div className="flex items-center gap-1 rounded-apple-lg bg-apple-bg-tertiary p-1">
<button type="button" onClick={() => { setActiveTab('finish'); setPanel({ mode: 'empty' }) }}
className={cn('flex flex-1 items-center justify-center gap-1.5 rounded-apple-md px-3 py-2 text-xs font-apple-medium transition', activeTab === 'finish' ? 'bg-apple-bg-primary text-apple-label-primary shadow-apple-xs' : 'text-apple-label-secondary')}>
<SwatchBook className=”h-3.5 w-3.5” /> 饰面花色
<SwatchBook className="h-3.5 w-3.5" /> 饰面花色
</button>
<button type=”button” onClick={() => { setActiveTab('substrate'); setPanel({ mode: 'empty' }) }}
<button type="button" onClick={() => { setActiveTab('substrate'); setPanel({ mode: 'empty' }) }}
className={cn('flex flex-1 items-center justify-center gap-1.5 rounded-apple-md px-3 py-2 text-xs font-apple-medium transition', activeTab === 'substrate' ? 'bg-apple-bg-primary text-apple-label-primary shadow-apple-xs' : 'text-apple-label-secondary')}>
<Package2 className=”h-3.5 w-3.5” /> 基材录入
<Package2 className="h-3.5 w-3.5" /> 基材录入
</button>
</div>
{/* Search + new button */}
<div className=”flex items-center gap-2”>
<div className=”relative flex-1”>
<Search className=”absolute left-3 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-apple-label-tertiary” />
<input type=”text” value={searchText} onChange={(e) => setSearchText(e.target.value)}
<div className="flex items-center gap-2">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-apple-label-tertiary" />
<input type="text" value={searchText} onChange={(e) => setSearchText(e.target.value)}
placeholder={activeTab === 'finish' ? '搜索花色...' : '搜索基材...'}
className=”w-full rounded-apple-lg border border-apple-separator bg-apple-bg-primary py-2 pl-9 pr-3 text-sm text-apple-label-primary” />
className="w-full rounded-apple-lg border border-apple-separator bg-apple-bg-primary py-2 pl-9 pr-3 text-sm text-apple-label-primary" />
{searchText && (
<button type=”button” onClick={() => setSearchText('')} className=”absolute right-2 top-1/2 -translate-y-1/2 rounded-full p-0.5 text-apple-label-secondary”><X className=”h-3.5 w-3.5” /></button>
<button type="button" onClick={() => setSearchText('')} className="absolute right-2 top-1/2 -translate-y-1/2 rounded-full p-0.5 text-apple-label-secondary"><X className="h-3.5 w-3.5" /></button>
)}
</div>
{activeTab === 'finish' ? (
<Button variant=”tinted” size=”small” onClick={startCreateFinish}><Plus className=”h-3.5 w-3.5” /> 新建</Button>
<Button variant="tinted" size="small" onClick={startCreateFinish}><Plus className="h-3.5 w-3.5" /> 新建</Button>
) : (
<Button variant="tinted" size="small" onClick={startCreateSubstrate}><Plus className="h-3.5 w-3.5" /> 新建</Button>
)}

View File

@ -1,143 +0,0 @@
import { assetCenterService } from '../asset-center.service'
import {
materialCategoryRepository,
materialRepository,
substrateRepository,
} from '../../repositories'
jest.mock('../../repositories', () => ({
assetFamilyRepository: {
findById: jest.fn(),
findManyWithRelations: jest.fn(),
},
assetVariantRepository: {
findByCode: jest.fn(),
findByIdentityKey: jest.fn(),
createWithRelations: jest.fn(),
findManyByFamilyIds: jest.fn(),
},
substrateRepository: {
findById: jest.fn(),
findByCode: jest.fn(),
create: jest.fn(),
findMany: jest.fn(),
},
materialRepository: {
findById: jest.fn(),
findByNameAndCategory: jest.fn(),
create: jest.fn(),
},
materialCategoryRepository: {
findByName: jest.fn(),
create: jest.fn(),
},
categoryRepository: {
findById: jest.fn(),
},
}))
describe('assetCenterService board library defaults', () => {
const mockedSubstrateRepository = jest.mocked(substrateRepository)
const mockedMaterialRepository = jest.mocked(materialRepository)
const mockedMaterialCategoryRepository = jest.mocked(materialCategoryRepository)
beforeEach(() => {
jest.clearAllMocks()
mockedSubstrateRepository.findByCode.mockResolvedValue(null)
mockedSubstrateRepository.create.mockImplementation(async (input) => ({
id: `substrate-${String(input.code).toLowerCase()}`,
code: input.code,
name: input.name,
description: input.description ?? null,
densityKgM3: input.densityKgM3 ?? null,
metadata: input.metadata ?? null,
isActive: true,
createdAt: new Date('2026-04-08T00:00:00.000Z'),
updatedAt: new Date('2026-04-08T00:00:00.000Z'),
}) as never)
mockedMaterialRepository.findByNameAndCategory.mockResolvedValue(null)
mockedMaterialRepository.create.mockImplementation(async (input) => ({
id: `finish-${String(input.name)}`,
name: input.name,
categoryId: 'category-board-finish',
thumbnailUrl: input.thumbnailUrl,
textureUrl: input.textureUrl ?? null,
normalMapUrl: input.normalMapUrl ?? null,
color: input.color,
metalness: input.metalness,
roughness: input.roughness,
opacity: input.opacity,
textureRepeat: input.textureRepeat,
textureRotation: input.textureRotation,
textureSizeMm: input.textureSizeMm,
isPreset: input.isPreset,
isActive: input.isActive,
}) as never)
mockedMaterialCategoryRepository.findByName.mockResolvedValue(null)
mockedMaterialCategoryRepository.create.mockResolvedValue({
id: 'category-board-finish',
name: '板材花色',
icon: 'Palette',
sortOrder: 10,
isActive: true,
} as never)
})
it('ensures confirmed board defaults and synced finishes before listing asset center data', async () => {
const result = await assetCenterService.ensureBoardLibraryDefaults()
expect(mockedMaterialCategoryRepository.findByName).toHaveBeenCalledWith('板材花色')
expect(mockedMaterialCategoryRepository.create).toHaveBeenCalledWith(
expect.objectContaining({
name: '板材花色',
}),
)
expect(mockedMaterialRepository.create).toHaveBeenCalled()
expect(mockedSubstrateRepository.create).toHaveBeenCalled()
expect(result.finishNames).toEqual(
expect.arrayContaining(['冰川白石纹色', '胡桃木色', '黑色']),
)
expect(result.substrates).toEqual(
expect.arrayContaining([
expect.objectContaining({ code: 'BOARD-DENSITY-E2-12-SINGLE-REGULAR' }),
expect.objectContaining({ code: 'BOARD-RUBBERWOOD-ENF-18-DOUBLE-COLORS' }),
]),
)
})
it('does not reuse the old ambiguous substrate names when seeding default board rules', async () => {
const legacyAmbiguousName = '12密度板'
mockedSubstrateRepository.create.mockImplementation(async (input) => {
if (input.name === legacyAmbiguousName) {
throw new Error('Unique constraint failed on the fields: (name)')
}
return {
id: `substrate-${String(input.code).toLowerCase()}`,
code: input.code,
name: input.name,
description: input.description ?? null,
densityKgM3: input.densityKgM3 ?? null,
metadata: input.metadata ?? null,
isActive: true,
createdAt: new Date('2026-04-08T00:00:00.000Z'),
updatedAt: new Date('2026-04-08T00:00:00.000Z'),
} as never
})
await expect(assetCenterService.ensureBoardLibraryDefaults()).resolves.toEqual(
expect.objectContaining({
substrates: expect.any(Array),
}),
)
expect(mockedSubstrateRepository.create).not.toHaveBeenCalledWith(
expect.objectContaining({
name: legacyAmbiguousName,
}),
)
})
})

View File

@ -593,12 +593,12 @@ class AssetCenterService {
categoryId: input.categoryId,
environmentGradeId: input.environmentGradeId,
thicknessSpecId: input.thicknessSpecId,
surfaceOptionId: input.surfaceOptionId,
surfaceOptionId: input.surfaceOptionId ?? undefined,
systemNameOptionId: input.systemNameOptionId,
isActive: true,
},
include: { finishes: { select: { finishId: true } } },
})
}) as Array<Substrate & { finishes: Array<{ finishId: string }> }>
for (const existing of existingWithSameSpec) {
const existingFinishIds = new Set(existing.finishes.map((f) => f.finishId))
if (
@ -626,14 +626,19 @@ class AssetCenterService {
systemNameOptionId: input.systemNameOptionId,
finishIds: input.finishIds ?? [],
})
: await buildSubstratePayload({
categoryId: input.categoryId,
environmentGradeId: input.environmentGradeId,
thicknessSpecId: input.thicknessSpecId,
surfaceOptionId: input.surfaceOptionId!,
systemNameOptionId: input.systemNameOptionId,
}).then(async (p) => {
: await (async () => {
// For solid wood (no surface option), build name without surface
const [category, environmentGrade, thicknessSpec, systemNameOption] = await Promise.all([
prisma.substrateCategory.findUnique({ where: { id: input.categoryId } }),
prisma.substrateEnvironmentGrade.findUnique({ where: { id: input.environmentGradeId } }),
prisma.substrateThicknessSpec.findUnique({ where: { id: input.thicknessSpecId } }),
prisma.substrateSystemNameOption.findUnique({ where: { id: input.systemNameOptionId } }),
])
if (!category) throw new AppError(ErrorCode.SUBSTRATE_NOT_FOUND, '材质主类不存在')
if (!environmentGrade) throw new AppError(ErrorCode.SUBSTRATE_NOT_FOUND, '环保等级不存在')
if (!thicknessSpec) throw new AppError(ErrorCode.SUBSTRATE_NOT_FOUND, '厚度规格不存在')
if (!systemNameOption) throw new AppError(ErrorCode.SUBSTRATE_NOT_FOUND, '系统名称不存在')
const finishNames: string[] = []
if ((input.finishIds ?? []).length > 0) {
const finishRows = await prisma.material.findMany({
@ -642,33 +647,38 @@ class AssetCenterService {
})
finishNames.push(...finishRows.map((f) => f.name))
}
const thickness = p.parts.thicknessSpec.name.toUpperCase()
const envGrade = p.parts.environmentGrade.name
const category = p.parts.category.name
const thickness = thicknessSpec.name.toUpperCase()
const envGrade = environmentGrade.name
const catName = category.name
const finishPart = finishNames.length > 0 ? finishNames.join(' ') : ''
const generatedName = [thickness, envGrade, category, finishPart].filter(Boolean).join(' ')
return { generatedName, parts: p.parts }
})
const generatedName = [thickness, envGrade, catName, finishPart].filter(Boolean).join(' ')
const parts = { category, environmentGrade, thicknessSpec, systemNameOption }
return { generatedName, parts }
})()
const baseCreateData = {
code,
name: payload.generatedName,
description: trimOrNull(input.description),
category: { connect: { id: input.categoryId } } as const,
environmentGrade: { connect: { id: input.environmentGradeId } } as const,
thicknessSpec: { connect: { id: input.thicknessSpecId } } as const,
systemNameOption: { connect: { id: input.systemNameOptionId } } as const,
suggestedPrice: input.suggestedPrice ?? null,
weightKg: input.weightKg ?? null,
finishes: input.finishIds?.length
? { create: input.finishIds.map((finishId) => ({ finish: { connect: { id: finishId } } })) }
: undefined,
metadata: toJsonValue(input.metadata),
isActive: true as const,
creator: input.createdBy ? { connect: { id: input.createdBy } } : undefined,
}
const createData = input.surfaceOptionId
? { ...baseCreateData, surfaceOption: { connect: { id: input.surfaceOptionId } } }
: baseCreateData
const created = await prisma.substrate.create({
data: {
code,
name: payload.generatedName,
description: trimOrNull(input.description),
category: { connect: { id: input.categoryId } },
environmentGrade: { connect: { id: input.environmentGradeId } },
thicknessSpec: { connect: { id: input.thicknessSpecId } },
surfaceOption: input.surfaceOptionId ? { connect: { id: input.surfaceOptionId } } : undefined,
systemNameOption: { connect: { id: input.systemNameOptionId } },
suggestedPrice: input.suggestedPrice ?? null,
weightKg: input.weightKg ?? null,
finishes: input.finishIds?.length
? { create: input.finishIds.map((finishId) => ({ finish: { connect: { id: finishId } } })) }
: undefined,
metadata: toJsonValue(input.metadata),
isActive: true,
creator: input.createdBy ? { connect: { id: input.createdBy } } : undefined,
},
data: createData as Prisma.SubstrateCreateInput,
include: {
category: { select: { id: true, name: true } },
environmentGrade: { select: { id: true, name: true } },
@ -705,7 +715,7 @@ class AssetCenterService {
const nextThicknessSpecId = input.thicknessSpecId ?? existing.thicknessSpecId
const nextSurfaceOptionId = input.surfaceOptionId !== undefined ? input.surfaceOptionId : existing.surfaceOptionId
const nextSystemNameOptionId = input.systemNameOptionId ?? existing.systemNameOptionId
const nextFinishIds = input.finishIds ?? existing.finishes.map((f) => f.finishId)
const nextFinishIds = input.finishIds ?? existing.finishes.map((f) => f.finish.id)
// Build name — with or without surface option (solid wood has no surface)
let generatedName: string
@ -720,13 +730,16 @@ class AssetCenterService {
})
generatedName = payload.generatedName
} else {
const parts = await loadSubstrateParts({
categoryId: nextCategoryId,
environmentGradeId: nextEnvironmentGradeId,
thicknessSpecId: nextThicknessSpecId,
surfaceOptionId: existing.surfaceOptionId ?? (await prisma.substrateSurfaceOption.findFirst({ orderBy: { sortOrder: 'asc' } }))?.id ?? '',
systemNameOptionId: nextSystemNameOptionId,
})
// For solid wood (no surface option), build name without surface
const [category, environmentGrade, thicknessSpec] = await Promise.all([
prisma.substrateCategory.findUnique({ where: { id: nextCategoryId } }),
prisma.substrateEnvironmentGrade.findUnique({ where: { id: nextEnvironmentGradeId } }),
prisma.substrateThicknessSpec.findUnique({ where: { id: nextThicknessSpecId } }),
])
if (!category) throw new AppError(ErrorCode.SUBSTRATE_NOT_FOUND, '材质主类不存在')
if (!environmentGrade) throw new AppError(ErrorCode.SUBSTRATE_NOT_FOUND, '环保等级不存在')
if (!thicknessSpec) throw new AppError(ErrorCode.SUBSTRATE_NOT_FOUND, '厚度规格不存在')
const finishNames: string[] = []
if (nextFinishIds.length > 0) {
const finishRows = await prisma.material.findMany({
@ -735,9 +748,9 @@ class AssetCenterService {
})
finishNames.push(...finishRows.map((f) => f.name))
}
const thickness = parts.thicknessSpec.name.toUpperCase()
const thickness = thicknessSpec.name.toUpperCase()
const finishPart = finishNames.length > 0 ? finishNames.join(' ') : ''
generatedName = [thickness, parts.environmentGrade.name, parts.category.name, finishPart].filter(Boolean).join(' ')
generatedName = [thickness, environmentGrade.name, category.name, finishPart].filter(Boolean).join(' ')
}
const updated = await prisma.substrate.update({
@ -774,7 +787,7 @@ class AssetCenterService {
: undefined,
metadata: input.metadata !== undefined ? toJsonValue(input.metadata) : undefined,
isActive: input.isActive,
},
} as Prisma.SubstrateUpdateInput,
include: {
category: { select: { id: true, name: true } },
environmentGrade: { select: { id: true, name: true } },

View File

@ -163,7 +163,6 @@ export type {
SimilarAssetVariantCandidateDTO,
CreateSubstrateDictionaryInput,
CreateSubstrateInput,
EnsureBoardLibraryDefaultsResult,
SubstrateDTO,
SubstrateDictionaryBundleDTO,
SubstrateDictionaryOptionDTO,