fix: replace only selected detail image slice
This commit is contained in:
parent
99fbce0bcb
commit
bdc7458038
@ -83,6 +83,7 @@ import {
|
||||
ServerTrustedTaskDetailStagedFile,
|
||||
TaskDetailAssetEditError,
|
||||
batchEditTaskDetailImages,
|
||||
replaceTaskDetailImage,
|
||||
} from '@/lib/task-detail-asset-edit'
|
||||
import { cleanupConsumedUploadSession } from '@/lib/temp-cleanup'
|
||||
import { getOwnedUploadSession, getUploadedFile } from '@/lib/upload-sessions'
|
||||
@ -210,6 +211,34 @@ describe('task-detail asset edit batch data-plane', () => {
|
||||
return { filePath, size: buffer.length, buffer }
|
||||
}
|
||||
|
||||
async function createStripedLongImage(fileName: string) {
|
||||
const width = 1200
|
||||
const heights = [1500, 1500, 1451]
|
||||
const colors = [
|
||||
{ r: 230, g: 20, b: 20 },
|
||||
{ r: 20, g: 210, b: 20 },
|
||||
{ r: 20, g: 20, b: 230 },
|
||||
]
|
||||
const bands = await Promise.all(heights.map((height, index) => sharp({
|
||||
create: { width, height, channels: 3, background: colors[index] },
|
||||
}).png().toBuffer()))
|
||||
const buffer = await sharp({
|
||||
create: {
|
||||
width,
|
||||
height: heights.reduce((sum, height) => sum + height, 0),
|
||||
channels: 3,
|
||||
background: { r: 0, g: 0, b: 0 },
|
||||
},
|
||||
}).composite([
|
||||
{ input: bands[0], top: 0, left: 0 },
|
||||
{ input: bands[1], top: heights[0], left: 0 },
|
||||
{ input: bands[2], top: heights[0] + heights[1], left: 0 },
|
||||
]).png().toBuffer()
|
||||
const filePath = path.join(tempRoot, fileName)
|
||||
await writeFile(filePath, buffer)
|
||||
return { filePath, size: buffer.length, heights, colors }
|
||||
}
|
||||
|
||||
function configureUploadSources(files: Record<string, { path: string; size: number; fileName: string }>) {
|
||||
mockGetOwnedUploadSession.mockImplementation(async (uploadId: string) => {
|
||||
const item = files[uploadId]
|
||||
@ -713,6 +742,190 @@ describe('task-detail asset edit batch data-plane', () => {
|
||||
}
|
||||
})
|
||||
|
||||
test.each([
|
||||
{ targetIndex: 0, expectedNames: ['long.png', 'long__bottom.png'] },
|
||||
{ targetIndex: 1, expectedNames: ['long__top.png', 'long.png', 'long__bottom.png'] },
|
||||
{ targetIndex: 2, expectedNames: ['long__top.png', 'long.png'] },
|
||||
])('详情长图精确替换第 $targetIndex 张切片并保留其它原图区段', async ({ targetIndex, expectedNames }) => {
|
||||
const long = await createStripedLongImage('long.png')
|
||||
const previews = await Promise.all(long.heights.map((height, index) =>
|
||||
createSolidImage(`legacy-${index}.webp`, 1200, height, 'webp')))
|
||||
const replacement = await createSolidImage(`replacement-${targetIndex}.png`, 900, 600)
|
||||
configureUploadSources({
|
||||
replacement: { path: replacement.filePath, size: replacement.size, fileName: `replacement-${targetIndex}.png` },
|
||||
})
|
||||
configureAssetQueries({
|
||||
previewRows: previews.map((preview, index) => ({
|
||||
id: `slice-${index}`,
|
||||
assetType: 'detail-preview',
|
||||
fileUrl: preview.filePath,
|
||||
fileName: `detail-preview-1788790000000-0-slice${index}-legacy.webp`,
|
||||
sortOrder: index,
|
||||
})),
|
||||
zipRows: [{ id: 'zip-detail', assetType: 'detail', fileUrl: path.join(tempRoot, 'detail.zip'), fileName: 'detail.zip' }],
|
||||
})
|
||||
mockListZipEntries.mockResolvedValue([{ name: 'long.png' }])
|
||||
mockExtractZipEntries.mockResolvedValue([{ entryName: 'long.png', filePath: long.filePath, size: long.size }])
|
||||
|
||||
let capturedNames: string[] = []
|
||||
let capturedFragments: Array<{ name: string; width: number; height: number; pixel: number[] }> = []
|
||||
mockCreateZipFromFiles.mockImplementationOnce(async (entries: Array<{ path: string; name: string }>) => {
|
||||
capturedNames = entries.map((entry) => entry.name)
|
||||
capturedFragments = await Promise.all(entries
|
||||
.filter((entry) => entry.name.includes('__top') || entry.name.includes('__bottom'))
|
||||
.map(async (entry) => {
|
||||
const metadata = await sharp(entry.path).metadata()
|
||||
const pixel = await sharp(entry.path).extract({ left: 0, top: 0, width: 1, height: 1 }).raw().toBuffer()
|
||||
return {
|
||||
name: entry.name,
|
||||
width: Number(metadata.width),
|
||||
height: Number(metadata.height),
|
||||
pixel: Array.from(pixel.slice(0, 3)),
|
||||
}
|
||||
}))
|
||||
return { filePath: 'uploads/materials/group.zip', fileName: 'group.zip', fileSize: 123 }
|
||||
})
|
||||
|
||||
const result = await batchEditTaskDetailImages({
|
||||
materialId: 'material-1',
|
||||
operatorUserId: 'operator-1',
|
||||
operations: [{ kind: 'replace', assetId: `slice-${targetIndex}`, uploadId: 'replacement' }],
|
||||
})
|
||||
|
||||
expect(result.entryCount).toBe(expectedNames.length)
|
||||
expect(capturedNames).toEqual(expectedNames)
|
||||
expect((prisma.materialAsset.createMany as jest.Mock).mock.calls[0][0].data).toHaveLength(3)
|
||||
if (targetIndex === 1) {
|
||||
expect(capturedFragments).toEqual([
|
||||
expect.objectContaining({ name: 'long__top.png', width: 1200, height: 1500 }),
|
||||
expect.objectContaining({ name: 'long__bottom.png', width: 1200, height: 1451 }),
|
||||
])
|
||||
expect(capturedFragments[0].pixel[0]).toBeGreaterThan(200)
|
||||
expect(capturedFragments[0].pixel[1]).toBeLessThan(40)
|
||||
expect(capturedFragments[1].pixel[2]).toBeGreaterThan(200)
|
||||
expect(capturedFragments[1].pixel[0]).toBeLessThan(40)
|
||||
}
|
||||
})
|
||||
|
||||
test('同一详情长图可在一次批量中精确替换多张不同切片', async () => {
|
||||
const long = await createStripedLongImage('multi-replace-long.png')
|
||||
const previews = await Promise.all(long.heights.map((height, index) =>
|
||||
createSolidImage(`multi-replace-${index}.webp`, 1200, height, 'webp')))
|
||||
const first = await createSolidImage('replace-first.png', 700, 500)
|
||||
const last = await createSolidImage('replace-last.png', 800, 700)
|
||||
configureUploadSources({
|
||||
first: { path: first.filePath, size: first.size, fileName: 'replace-first.png' },
|
||||
last: { path: last.filePath, size: last.size, fileName: 'replace-last.png' },
|
||||
})
|
||||
configureAssetQueries({
|
||||
previewRows: previews.map((preview, index) => ({
|
||||
id: `multi-slice-${index}`,
|
||||
assetType: 'detail-preview',
|
||||
fileUrl: preview.filePath,
|
||||
fileName: `multi-replace-long-${String(index + 1).padStart(3, '0')}.webp`,
|
||||
sortOrder: index,
|
||||
})),
|
||||
zipRows: [{ id: 'zip-detail', assetType: 'detail', fileUrl: path.join(tempRoot, 'detail.zip'), fileName: 'detail.zip' }],
|
||||
})
|
||||
mockListZipEntries.mockResolvedValue([{ name: 'multi-replace-long.png' }])
|
||||
mockExtractZipEntries.mockResolvedValue([{ entryName: 'multi-replace-long.png', filePath: long.filePath, size: long.size }])
|
||||
|
||||
const result = await batchEditTaskDetailImages({
|
||||
materialId: 'material-1',
|
||||
operatorUserId: 'operator-1',
|
||||
operations: [
|
||||
{ kind: 'replace', assetId: 'multi-slice-0', uploadId: 'first' },
|
||||
{ kind: 'replace', assetId: 'multi-slice-2', uploadId: 'last' },
|
||||
],
|
||||
})
|
||||
|
||||
expect(result).toMatchObject({ success: true, appliedCount: 2, entryCount: 3 })
|
||||
const entries = mockCreateZipFromFiles.mock.calls[0][0] as Array<{ path: string; name: string }>
|
||||
expect(entries.map((entry) => entry.name)).toEqual([
|
||||
'multi-replace-long.png',
|
||||
'multi-replace-long__bottom__top.png',
|
||||
'multi-replace-long__bottom.png',
|
||||
])
|
||||
expect(entries[0].path).toBe(first.filePath)
|
||||
expect(entries[2].path).toBe(last.filePath)
|
||||
})
|
||||
|
||||
test('单张替换接口复用精确切片链,后续可继续编辑已拆分长图', async () => {
|
||||
const top = await createSolidImage('continued__top.png', 1200, 3000)
|
||||
const replacementEntry = await createSolidImage('continued.png', 900, 600)
|
||||
const bottom = await createSolidImage('continued__bottom.png', 1200, 1451)
|
||||
const previews = await Promise.all([1500, 1500, 600, 1451].map((height, index) =>
|
||||
createSolidImage(`continued-preview-${index}.webp`, index === 2 ? 900 : 1200, height, 'webp')))
|
||||
const replacement = await createSolidImage('continued-second-replacement.png', 640, 480)
|
||||
configureUploadSources({
|
||||
replacement: { path: replacement.filePath, size: replacement.size, fileName: 'continued-second-replacement.png' },
|
||||
})
|
||||
configureAssetQueries({
|
||||
previewRows: [
|
||||
{ id: 'continued-0', assetType: 'detail-preview', fileUrl: previews[0].filePath, fileName: 'continued__top__srv-1788790000000-0-slice0-a.webp', sortOrder: 0 },
|
||||
{ id: 'continued-1', assetType: 'detail-preview', fileUrl: previews[1].filePath, fileName: 'continued__top__srv-1788790000000-0-slice1-b.webp', sortOrder: 1 },
|
||||
{ id: 'continued-replaced', assetType: 'detail-preview', fileUrl: previews[2].filePath, fileName: 'continued__srv-1788790000000-1-c.webp', sortOrder: 2 },
|
||||
{ id: 'continued-bottom', assetType: 'detail-preview', fileUrl: previews[3].filePath, fileName: 'continued__bottom__srv-1788790000000-2-d.webp', sortOrder: 3 },
|
||||
],
|
||||
zipRows: [{ id: 'zip-detail', assetType: 'detail', fileUrl: path.join(tempRoot, 'detail.zip'), fileName: 'detail.zip' }],
|
||||
})
|
||||
mockListZipEntries.mockResolvedValue([
|
||||
{ name: 'continued__top.png' },
|
||||
{ name: 'continued.png' },
|
||||
{ name: 'continued__bottom.png' },
|
||||
])
|
||||
mockExtractZipEntries.mockResolvedValue([
|
||||
{ entryName: 'continued__top.png', filePath: top.filePath, size: top.size },
|
||||
{ entryName: 'continued.png', filePath: replacementEntry.filePath, size: replacementEntry.size },
|
||||
{ entryName: 'continued__bottom.png', filePath: bottom.filePath, size: bottom.size },
|
||||
])
|
||||
|
||||
const result = await replaceTaskDetailImage({
|
||||
materialId: 'material-1',
|
||||
assetId: 'continued-1',
|
||||
operatorUserId: 'operator-1',
|
||||
uploadId: 'replacement',
|
||||
})
|
||||
|
||||
expect(result).toMatchObject({ success: true, entryName: 'continued__top.png' })
|
||||
expect(mockCreateZipFromFiles.mock.calls[0][0].map((entry: { name: string }) => entry.name)).toEqual([
|
||||
'continued__top__top.png',
|
||||
'continued__top.png',
|
||||
'continued.png',
|
||||
'continued__bottom.png',
|
||||
])
|
||||
})
|
||||
|
||||
test('详情长图与旧预览切片几何不一致时拒绝精确替换', async () => {
|
||||
const long = await createSolidImage('geometry-long.png', 1200, 3000)
|
||||
const previews = await Promise.all([1500, 1400].map((height, index) =>
|
||||
createSolidImage(`geometry-${index}.webp`, 1200, height, 'webp')))
|
||||
const replacement = await createSolidImage('geometry-replacement.png', 500, 500)
|
||||
configureUploadSources({
|
||||
replacement: { path: replacement.filePath, size: replacement.size, fileName: 'geometry-replacement.png' },
|
||||
})
|
||||
configureAssetQueries({
|
||||
previewRows: previews.map((preview, index) => ({
|
||||
id: `geometry-${index}`,
|
||||
assetType: 'detail-preview',
|
||||
fileUrl: preview.filePath,
|
||||
fileName: `geometry-long-${String(index + 1).padStart(3, '0')}.webp`,
|
||||
sortOrder: index,
|
||||
})),
|
||||
zipRows: [{ id: 'zip-detail', assetType: 'detail', fileUrl: path.join(tempRoot, 'detail.zip'), fileName: 'detail.zip' }],
|
||||
})
|
||||
mockListZipEntries.mockResolvedValue([{ name: 'geometry-long.png' }])
|
||||
mockExtractZipEntries.mockResolvedValue([{ entryName: 'geometry-long.png', filePath: long.filePath, size: long.size }])
|
||||
|
||||
await expect(batchEditTaskDetailImages({
|
||||
materialId: 'material-1',
|
||||
operatorUserId: 'operator-1',
|
||||
operations: [{ kind: 'replace', assetId: 'geometry-0', uploadId: 'replacement' }],
|
||||
})).rejects.toThrow('预览切片总高度与原图缩放规则不一致')
|
||||
expect(mockUploadPreview).not.toHaveBeenCalled()
|
||||
expect(mockCreateZipFromFiles).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test('追加后超过图片组上限会拒绝提交', async () => {
|
||||
const existingRows: PreviewRow[] = []
|
||||
const entryNames: Array<{ name: string; filePath: string }> = []
|
||||
|
||||
@ -113,6 +113,7 @@ interface EditContext {
|
||||
allZipAsset: { id: string; fileUrl: string; fileName: string | null } | null
|
||||
entryNames: string[]
|
||||
runs: PreviewRun[]
|
||||
runEntryIndexes: number[]
|
||||
targetRunIndex: number
|
||||
targetEntryIndex: number
|
||||
}
|
||||
@ -690,6 +691,7 @@ async function loadEditContext(options: {
|
||||
allZipAsset,
|
||||
entryNames: [],
|
||||
runs: [],
|
||||
runEntryIndexes: [],
|
||||
targetRunIndex: -1,
|
||||
targetEntryIndex: -1,
|
||||
}
|
||||
@ -776,6 +778,7 @@ async function loadEditContext(options: {
|
||||
allZipAsset,
|
||||
entryNames,
|
||||
runs,
|
||||
runEntryIndexes,
|
||||
targetRunIndex,
|
||||
targetEntryIndex,
|
||||
}
|
||||
@ -1753,9 +1756,12 @@ function flattenBatchRows(runs: BatchRunState[]): Array<{ runIndex: number; rowI
|
||||
function findBatchRowByAssetId(
|
||||
runs: BatchRunState[],
|
||||
assetId: string,
|
||||
): { runIndex: number; rowIndex: number; row: ExistingBatchRowState } | null {
|
||||
): { runIndex: number; rowIndex: number; row: BatchRowState } | null {
|
||||
for (const located of flattenBatchRows(runs)) {
|
||||
if (located.row.kind === 'existing' && located.row.id === assetId) {
|
||||
const matches = located.row.kind === 'existing'
|
||||
? located.row.id === assetId
|
||||
: located.row.sourceAssetId === assetId
|
||||
if (matches) {
|
||||
return {
|
||||
runIndex: located.runIndex,
|
||||
rowIndex: located.rowIndex,
|
||||
@ -1766,6 +1772,122 @@ function findBatchRowByAssetId(
|
||||
return null
|
||||
}
|
||||
|
||||
function getBatchRowSourceAssetId(row: BatchRowState): string | null {
|
||||
return row.kind === 'existing' ? row.id : row.sourceAssetId || null
|
||||
}
|
||||
|
||||
function resolveBatchRunPreviewRows(
|
||||
context: EditContext,
|
||||
run: BatchRunState,
|
||||
): PreviewRowLike[] {
|
||||
const originalRows = new Map(
|
||||
context.rowsByGroup[context.group].map((row) => [row.id, row]),
|
||||
)
|
||||
return run.rows.map((row) => {
|
||||
const sourceAssetId = getBatchRowSourceAssetId(row)
|
||||
const original = sourceAssetId ? originalRows.get(sourceAssetId) : null
|
||||
if (!original) {
|
||||
throw new TaskDetailAssetEditError('长图切片已被本次批量操作改变,请刷新后重试', 409)
|
||||
}
|
||||
return original
|
||||
})
|
||||
}
|
||||
|
||||
async function replaceDetailSliceInBatchRun(options: {
|
||||
context: EditContext
|
||||
run: BatchRunState
|
||||
rowIndex: number
|
||||
replacement: ResolvedMutationSource
|
||||
finalEntryName: string
|
||||
runIndex: number
|
||||
workspaceDir: string
|
||||
existingEntryNames: string[]
|
||||
generatedPreviewLocations: string[]
|
||||
}): Promise<BatchRunState[]> {
|
||||
const previewRows = resolveBatchRunPreviewRows(options.context, options.run)
|
||||
|
||||
const sourceAssetIds = options.run.rows.map((row) => getBatchRowSourceAssetId(row))
|
||||
if (sourceAssetIds.some((assetId) => !assetId)) {
|
||||
throw new TaskDetailAssetEditError('长图切片已被本次批量操作改变,请刷新后重试', 409)
|
||||
}
|
||||
|
||||
const previewRun: PreviewRun = {
|
||||
key: options.run.entry.name,
|
||||
rows: previewRows,
|
||||
legacyIndex: null,
|
||||
}
|
||||
let topSource: (ResolvedMutationSource & { entryName: string }) | null = null
|
||||
let bottomSource: (ResolvedMutationSource & { entryName: string }) | null = null
|
||||
|
||||
if (options.rowIndex > 0) {
|
||||
const split = await splitRunSourceAtDetailSeam({
|
||||
run: previewRun,
|
||||
entry: options.run.entry,
|
||||
afterRowIndex: options.rowIndex - 1,
|
||||
workspaceDir: options.workspaceDir,
|
||||
existingEntryNames: options.existingEntryNames,
|
||||
})
|
||||
topSource = split.top
|
||||
}
|
||||
if (options.rowIndex < options.run.rows.length - 1) {
|
||||
const split = await splitRunSourceAtDetailSeam({
|
||||
run: previewRun,
|
||||
entry: options.run.entry,
|
||||
afterRowIndex: options.rowIndex,
|
||||
workspaceDir: options.workspaceDir,
|
||||
existingEntryNames: options.existingEntryNames,
|
||||
})
|
||||
bottomSource = split.bottom
|
||||
}
|
||||
|
||||
const replacementRuns: BatchRunState[] = []
|
||||
if (topSource) {
|
||||
const topRun = await buildNewBatchRunState({
|
||||
source: topSource,
|
||||
entryName: topSource.entryName,
|
||||
group: 'detail',
|
||||
materialId: options.context.materialId,
|
||||
isPrivate: options.context.taskDetailPrivate,
|
||||
entryIndex: options.runIndex,
|
||||
workspaceDir: options.workspaceDir,
|
||||
generatedPreviewLocations: options.generatedPreviewLocations,
|
||||
sourceAssetIds: sourceAssetIds.slice(0, options.rowIndex) as string[],
|
||||
})
|
||||
topRun.sourceRunIndex = options.run.sourceRunIndex
|
||||
replacementRuns.push(topRun)
|
||||
}
|
||||
|
||||
const replacementRun = await buildNewBatchRunState({
|
||||
source: options.replacement,
|
||||
entryName: options.finalEntryName,
|
||||
group: 'detail',
|
||||
materialId: options.context.materialId,
|
||||
isPrivate: options.context.taskDetailPrivate,
|
||||
entryIndex: options.runIndex + replacementRuns.length,
|
||||
workspaceDir: options.workspaceDir,
|
||||
generatedPreviewLocations: options.generatedPreviewLocations,
|
||||
})
|
||||
replacementRuns.push(replacementRun)
|
||||
|
||||
if (bottomSource) {
|
||||
const bottomRun = await buildNewBatchRunState({
|
||||
source: bottomSource,
|
||||
entryName: bottomSource.entryName,
|
||||
group: 'detail',
|
||||
materialId: options.context.materialId,
|
||||
isPrivate: options.context.taskDetailPrivate,
|
||||
entryIndex: options.runIndex + replacementRuns.length,
|
||||
workspaceDir: options.workspaceDir,
|
||||
generatedPreviewLocations: options.generatedPreviewLocations,
|
||||
sourceAssetIds: sourceAssetIds.slice(options.rowIndex + 1) as string[],
|
||||
})
|
||||
bottomRun.sourceRunIndex = options.run.sourceRunIndex
|
||||
replacementRuns.push(bottomRun)
|
||||
}
|
||||
|
||||
return replacementRuns
|
||||
}
|
||||
|
||||
function countSourceRunsForGroupRows(rows: PreviewRowLike[]): number {
|
||||
if (rows.length === 0) return 0
|
||||
const anchoredRuns = groupPreviewRowsIntoRuns(rows)
|
||||
@ -1932,15 +2054,18 @@ async function executeBatchRebuild(
|
||||
)
|
||||
: []
|
||||
|
||||
let finalRuns: BatchRunState[] = context.runs.map((run, index) => ({
|
||||
sourceRunIndex: index,
|
||||
entry: {
|
||||
path: extracted[index]?.filePath || '',
|
||||
name: entryNames[index],
|
||||
size: extracted[index]?.size || 0,
|
||||
},
|
||||
rows: run.rows.map((row) => createExistingBatchRowState(row)),
|
||||
}))
|
||||
let finalRuns: BatchRunState[] = context.runs.map((run, index) => {
|
||||
const entryIndex = context.runEntryIndexes[index]
|
||||
return {
|
||||
sourceRunIndex: index,
|
||||
entry: {
|
||||
path: extracted[entryIndex]?.filePath || '',
|
||||
name: entryNames[entryIndex],
|
||||
size: extracted[entryIndex]?.size || 0,
|
||||
},
|
||||
rows: run.rows.map((row) => createExistingBatchRowState(row)),
|
||||
}
|
||||
})
|
||||
|
||||
const handledAssetIds = new Set<string>()
|
||||
const repeatedInsertBoundaries = new Map<string, BatchRunState>()
|
||||
@ -2172,6 +2297,23 @@ async function executeBatchRebuild(
|
||||
if (conflict) {
|
||||
throw new TaskDetailAssetEditError(`替换后的「${finalEntryName}」与包内其他文件重名,请先改名再替换`, 409)
|
||||
}
|
||||
|
||||
if (group === 'detail' && run.rows.length > 1) {
|
||||
const replacementRuns = await replaceDetailSliceInBatchRun({
|
||||
context,
|
||||
run,
|
||||
rowIndex: located.rowIndex,
|
||||
replacement: operation.mutation.source,
|
||||
finalEntryName,
|
||||
runIndex: located.runIndex,
|
||||
workspaceDir: workspace.dir,
|
||||
existingEntryNames: survivingEntryNames(),
|
||||
generatedPreviewLocations,
|
||||
})
|
||||
finalRuns.splice(located.runIndex, 1, ...replacementRuns)
|
||||
continue
|
||||
}
|
||||
|
||||
const barcodes = group === 'sku'
|
||||
? await loadRunBarcodes(
|
||||
run.rows
|
||||
@ -2720,12 +2862,14 @@ export async function replaceTaskDetailImage(input: ReplaceTaskDetailImageInput)
|
||||
actionLabel: '替换图片',
|
||||
})
|
||||
|
||||
const outcome = await executeRebuild(context, {
|
||||
kind: 'replace',
|
||||
filePath: source.filePath,
|
||||
replacementFileName: source.fileName,
|
||||
buffer: source.buffer,
|
||||
})
|
||||
const entryName = buildFinalEntryName(
|
||||
context.entryNames[context.targetEntryIndex],
|
||||
source.fileName,
|
||||
)
|
||||
const outcome = await executeBatchRebuild(context, [{
|
||||
assetId: input.assetId,
|
||||
mutation: { kind: 'replace', source },
|
||||
}])
|
||||
const { nextIsFullSet } = await commitRebuildWithCleanup(context, outcome, 'REPLACE_IMAGE')
|
||||
await cleanupAfterCommit(context, outcome)
|
||||
if (input.uploadId) {
|
||||
@ -2757,7 +2901,7 @@ export async function replaceTaskDetailImage(input: ReplaceTaskDetailImageInput)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
entryName: outcome.nextEntryNames[context.targetRunIndex],
|
||||
entryName,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user