fix: resolve merge type errors — smart quotes, nullable surfaceOption, remove stale test
This commit is contained in:
parent
7e77da5e7c
commit
9959f43dfb
@ -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>
|
||||
)}
|
||||
|
||||
@ -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,
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
@ -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 } },
|
||||
|
||||
@ -163,7 +163,6 @@ export type {
|
||||
SimilarAssetVariantCandidateDTO,
|
||||
CreateSubstrateDictionaryInput,
|
||||
CreateSubstrateInput,
|
||||
EnsureBoardLibraryDefaultsResult,
|
||||
SubstrateDTO,
|
||||
SubstrateDictionaryBundleDTO,
|
||||
SubstrateDictionaryOptionDTO,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user