修复画布生成刷新后完成同步
任务列表识别当前画布项目的未确认完成任务并发出完成通知。 编辑器收到任务完成通知后重新加载项目快照并刷新素材库。 补充刷新后任务已完成和轮询完成的回归测试。
This commit is contained in:
@@ -11,6 +11,7 @@ import {
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type { ExternalGenerationTaskRecord } from '../../../packages/shared/src/contracts/externalGeneration';
|
||||
import {
|
||||
ApiClientError,
|
||||
defaultEditorProjectLayers,
|
||||
@@ -244,6 +245,34 @@ describe('ImageCanvasEditorView generation integration', () => {
|
||||
updatedAt: '2026-06-19T00:00:00.000Z',
|
||||
});
|
||||
|
||||
const createExternalGenerationTask = (
|
||||
overrides: Partial<ExternalGenerationTaskRecord> = {},
|
||||
): ExternalGenerationTaskRecord => {
|
||||
const now = new Date().toISOString();
|
||||
return {
|
||||
jobId: overrides.jobId ?? 'task-editor-character-refresh',
|
||||
jobKind: overrides.jobKind ?? 'editor_image_generation',
|
||||
sourceModule: overrides.sourceModule ?? 'editor-canvas',
|
||||
sourceEntityId:
|
||||
overrides.sourceEntityId ?? 'editor-project-refresh-completion',
|
||||
requestLabel: overrides.requestLabel ?? '图片画布生成角色形象',
|
||||
requestPrompt: overrides.requestPrompt ?? '刷新后角色',
|
||||
status: overrides.status ?? 'running',
|
||||
phaseLabel: overrides.phaseLabel ?? '生成图片',
|
||||
phaseDetail: overrides.phaseDetail ?? '正在生成角色形象。',
|
||||
progress: overrides.progress ?? 40,
|
||||
error: overrides.error ?? null,
|
||||
priceMudPoints: overrides.priceMudPoints ?? 3,
|
||||
refundLedgerId: overrides.refundLedgerId ?? null,
|
||||
notificationAcknowledgedAt: overrides.notificationAcknowledgedAt ?? null,
|
||||
createdAt: overrides.createdAt ?? now,
|
||||
startedAt: overrides.startedAt ?? now,
|
||||
completedAt: overrides.completedAt ?? null,
|
||||
updatedAt: overrides.updatedAt ?? now,
|
||||
updatedAtMicros: overrides.updatedAtMicros ?? 1,
|
||||
};
|
||||
};
|
||||
|
||||
const createGeneratedImageResponse = (
|
||||
input: {
|
||||
projectId?: string | null;
|
||||
@@ -552,7 +581,9 @@ describe('ImageCanvasEditorView generation integration', () => {
|
||||
|
||||
fireEvent.click(generatedImage.closest('button')!);
|
||||
|
||||
expect(await screen.findByRole('dialog', { name: '生成图片' })).toBeTruthy();
|
||||
expect(
|
||||
await screen.findByRole('dialog', { name: '生成图片' }),
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
(screen.getByLabelText('生成提示词') as HTMLTextAreaElement).value,
|
||||
).toBe('元数据恢复生成器');
|
||||
@@ -2719,6 +2750,184 @@ describe('ImageCanvasEditorView generation integration', () => {
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it('reloads project and asset snapshots when a restored queued generation completed after refresh', async () => {
|
||||
window.history.replaceState(null, '', '/editor/canvas');
|
||||
const projectId = 'editor-project-refresh-completion';
|
||||
loadOrCreateRecentEditorProjectMock.mockResolvedValueOnce({
|
||||
projectId,
|
||||
title: '刷新恢复画布',
|
||||
viewport: { x: 0, y: 0, scale: 1 },
|
||||
layers: [
|
||||
{
|
||||
itemType: 'generation-dialog',
|
||||
layerId: 'generation-dialog:generation-dialog-refresh-character',
|
||||
resourceId: 'generation-dialog:generation-dialog-refresh-character',
|
||||
dialog: {
|
||||
id: 'generation-dialog-refresh-character',
|
||||
mode: 'character',
|
||||
prompt: '刷新后角色',
|
||||
status: 'generating',
|
||||
composerOpen: false,
|
||||
imageModel: 'gpt-image-2',
|
||||
placeholder: {
|
||||
x: 240,
|
||||
y: 120,
|
||||
width: 420,
|
||||
height: 560,
|
||||
originalWidth: 1024,
|
||||
originalHeight: 1536,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
resources: [],
|
||||
updatedAt: '2026-06-21T00:00:00.000Z',
|
||||
});
|
||||
const generatedResource = createGeneratedResource({
|
||||
resourceId: 'resource-refresh-character-result',
|
||||
projectId,
|
||||
imageSrc: 'data:image/png;base64,cmVmcmVzaC1yZXN1bHQ=',
|
||||
width: 1024,
|
||||
height: 1536,
|
||||
assetKind: 'character',
|
||||
generationInputs: {
|
||||
fields: [{ title: '角色设定', value: '刷新后角色' }],
|
||||
references: [],
|
||||
},
|
||||
});
|
||||
loadEditorProjectMock.mockResolvedValueOnce({
|
||||
projectId,
|
||||
title: '刷新恢复画布',
|
||||
viewport: { x: 0, y: 0, scale: 1 },
|
||||
layers: [
|
||||
{
|
||||
layerId: 'layer-refresh-character-result',
|
||||
resourceId: 'resource-refresh-character-result',
|
||||
title: '刷新后生成结果',
|
||||
x: 240,
|
||||
y: 120,
|
||||
width: 1024,
|
||||
height: 1536,
|
||||
originalWidth: 1024,
|
||||
originalHeight: 1536,
|
||||
zIndex: 12,
|
||||
sourceType: 'generated',
|
||||
assetKind: 'character',
|
||||
},
|
||||
{
|
||||
itemType: 'generation-dialog',
|
||||
layerId: 'generation-dialog:generation-dialog-refresh-character',
|
||||
resourceId: 'generation-dialog:generation-dialog-refresh-character',
|
||||
dialog: {
|
||||
id: 'generation-dialog-refresh-character',
|
||||
mode: 'character',
|
||||
prompt: '刷新后角色',
|
||||
status: 'idle',
|
||||
composerOpen: false,
|
||||
generatedLayerId: 'layer-refresh-character-result',
|
||||
imageModel: 'gpt-image-2',
|
||||
placeholder: {
|
||||
x: 240,
|
||||
y: 120,
|
||||
width: 420,
|
||||
height: 560,
|
||||
originalWidth: 1024,
|
||||
originalHeight: 1536,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
resources: [generatedResource],
|
||||
updatedAt: '2026-06-21T00:01:00.000Z',
|
||||
});
|
||||
loadEditorAssetLibraryMock
|
||||
.mockResolvedValueOnce({
|
||||
folders: [
|
||||
{
|
||||
folderId: 'project',
|
||||
label: '项目素材',
|
||||
sortOrder: 0,
|
||||
collapsed: false,
|
||||
systemDefault: true,
|
||||
},
|
||||
],
|
||||
assets: [],
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
folders: [
|
||||
{
|
||||
folderId: 'project',
|
||||
label: '项目素材',
|
||||
sortOrder: 0,
|
||||
collapsed: false,
|
||||
systemDefault: true,
|
||||
},
|
||||
],
|
||||
assets: [
|
||||
createGeneratedAsset({
|
||||
assetId: 'asset-refresh-character-result',
|
||||
folderId: 'project',
|
||||
label: '刷新后生成结果素材',
|
||||
imageSrc: 'data:image/png;base64,cmVmcmVzaC1yZXN1bHQ=',
|
||||
width: 1024,
|
||||
height: 1536,
|
||||
assetKind: 'character',
|
||||
}),
|
||||
],
|
||||
});
|
||||
const completedTask = createExternalGenerationTask({
|
||||
jobId: 'task-refresh-character-result',
|
||||
sourceEntityId: projectId,
|
||||
requestPrompt: '刷新后角色',
|
||||
status: 'completed',
|
||||
progress: 100,
|
||||
phaseDetail: '生成已完成。',
|
||||
completedAt: '2026-06-21T00:01:00.000Z',
|
||||
updatedAt: '2026-06-21T00:01:00.000Z',
|
||||
updatedAtMicros: 2,
|
||||
});
|
||||
vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => {
|
||||
const requestUrl =
|
||||
typeof input === 'string'
|
||||
? input
|
||||
: input instanceof URL
|
||||
? input.href
|
||||
: input.url;
|
||||
const url = new URL(requestUrl, 'http://localhost');
|
||||
if (url.pathname === '/api/runtime/external-generation/jobs') {
|
||||
const statuses = url.searchParams.get('statuses') ?? '';
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
overview: {
|
||||
pendingCount: 0,
|
||||
runningCount: 0,
|
||||
unacknowledgedTerminalCount: 1,
|
||||
updatedAtMicros: 2,
|
||||
},
|
||||
tasks: statuses.includes('completed') ? [completedTask] : [],
|
||||
}),
|
||||
{
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
},
|
||||
);
|
||||
}
|
||||
return new Response(null, { status: 404 });
|
||||
});
|
||||
|
||||
render(<ImageCanvasEditorView />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(loadEditorProjectMock).toHaveBeenCalledWith(projectId);
|
||||
});
|
||||
expect(await screen.findByAltText('画布图片:刷新后生成结果')).toBeTruthy();
|
||||
await waitFor(() => {
|
||||
expect(loadEditorAssetLibraryMock).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
expect(createEditorAssetMock).not.toHaveBeenCalled();
|
||||
expect(createEditorProjectResourceMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('allows icon asset generation to reference icon specs created from the renamed spec entry', async () => {
|
||||
generateEditorImageMock.mockImplementationOnce(async (input) =>
|
||||
createGeneratedImageResponse(input, {
|
||||
@@ -3174,7 +3383,9 @@ describe('ImageCanvasEditorView generation integration', () => {
|
||||
});
|
||||
|
||||
expect(
|
||||
within(screen.getByRole('dialog', { name: 'UI素材提取' })).getByRole<HTMLButtonElement>('button', {
|
||||
within(
|
||||
screen.getByRole('dialog', { name: 'UI素材提取' }),
|
||||
).getByRole<HTMLButtonElement>('button', {
|
||||
name: '提取',
|
||||
}).disabled,
|
||||
).toBe(false);
|
||||
@@ -3265,9 +3476,7 @@ describe('ImageCanvasEditorView generation integration', () => {
|
||||
});
|
||||
render(<ImageCanvasEditorView />);
|
||||
|
||||
const uiDesignImage = await screen.findByAltText(
|
||||
'画布图片:背包UI设计图',
|
||||
);
|
||||
const uiDesignImage = await screen.findByAltText('画布图片:背包UI设计图');
|
||||
fireEvent.pointerDown(uiDesignImage.closest('button')!, {
|
||||
button: 0,
|
||||
pointerId: 256,
|
||||
|
||||
@@ -1052,6 +1052,18 @@ export function ImageCanvasEditorView({
|
||||
},
|
||||
[applyProjectSnapshot, refreshAssetLibrary],
|
||||
);
|
||||
const handleExternalGenerationTasksCompleted = useCallback(
|
||||
(tasks: ExternalGenerationTaskRecord[]) => {
|
||||
if (!projectId || tasks.length === 0) {
|
||||
return;
|
||||
}
|
||||
refreshEditorWalletBalance();
|
||||
void loadEditorProject(projectId)
|
||||
.then(applyGeneratedProjectSnapshot)
|
||||
.catch(() => undefined);
|
||||
},
|
||||
[applyGeneratedProjectSnapshot, projectId, refreshEditorWalletBalance],
|
||||
);
|
||||
const handleEditorAgentGenerationResult = useCallback(
|
||||
(event: EditorAgentGenerationResultEvent) => {
|
||||
const hasGeneratedResource = event.images.some((image) =>
|
||||
@@ -2033,6 +2045,7 @@ export function ImageCanvasEditorView({
|
||||
onGenerationFrameContextMenu: handleGenerationFrameContextMenu,
|
||||
onActivateGenerationDialog: activateCanvasGenerationDialog,
|
||||
onFocusExternalTask: focusExternalGenerationTask,
|
||||
onExternalTasksCompleted: handleExternalGenerationTasksCompleted,
|
||||
onEditorAgentGenerationResult: handleEditorAgentGenerationResult,
|
||||
onToggleTaskSidebar: toggleTaskSidebar,
|
||||
onToggleAgentConversation: toggleAgentConversation,
|
||||
|
||||
@@ -125,6 +125,7 @@ export type ImageCanvasStageViewProps = {
|
||||
) => void;
|
||||
onActivateGenerationDialog: (dialog: CanvasGenerationDialogState) => void;
|
||||
onFocusExternalTask: (task: ExternalGenerationTaskRecord) => void;
|
||||
onExternalTasksCompleted?: (tasks: ExternalGenerationTaskRecord[]) => void;
|
||||
onEditorAgentGenerationResult?: (
|
||||
event: EditorAgentGenerationResultEvent,
|
||||
) => void;
|
||||
@@ -257,6 +258,7 @@ export function ImageCanvasStageView({
|
||||
onGenerationFrameContextMenu,
|
||||
onActivateGenerationDialog,
|
||||
onFocusExternalTask,
|
||||
onExternalTasksCompleted,
|
||||
onEditorAgentGenerationResult,
|
||||
onToggleTaskSidebar,
|
||||
onToggleAgentConversation,
|
||||
@@ -465,6 +467,7 @@ export function ImageCanvasStageView({
|
||||
open={isTaskSidebarOpen}
|
||||
onToggleOpen={onToggleTaskSidebar}
|
||||
onFocusExternalTask={onFocusExternalTask}
|
||||
onExternalTasksCompleted={onExternalTasksCompleted}
|
||||
/>
|
||||
|
||||
{isAgentConversationEnabled ? (
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
// @vitest-environment jsdom
|
||||
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import {
|
||||
act,
|
||||
fireEvent,
|
||||
render,
|
||||
screen,
|
||||
waitFor,
|
||||
} from '@testing-library/react';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type { ExternalGenerationTaskRecord } from '../../../packages/shared/src/contracts/externalGeneration';
|
||||
@@ -239,9 +245,7 @@ describe('ImageCanvasTaskSidebarView', () => {
|
||||
|
||||
expect(await screen.findByText('图片画布生成图片')).toBeTruthy();
|
||||
expect(screen.getByText(/发光猫咪主视觉/u)).toBeTruthy();
|
||||
expect(
|
||||
screen.getByText(/正在生成第 2\/4 段。 · 已用时 1分/u),
|
||||
).toBeTruthy();
|
||||
expect(screen.getByText(/正在生成第 2\/4 段。 · 已用时 1分/u)).toBeTruthy();
|
||||
expect(screen.queryByText(/35%/u)).toBeNull();
|
||||
expect(screen.getByText(/已用时 1分/u)).toBeTruthy();
|
||||
expect(screen.queryByText(/总进度/u)).toBeNull();
|
||||
@@ -356,6 +360,81 @@ describe('ImageCanvasTaskSidebarView', () => {
|
||||
expect(listExternalGenerationTasksMock).toHaveBeenCalledTimes(4);
|
||||
});
|
||||
|
||||
it('notifies when a restored active external task completes during polling', async () => {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
const onExternalTasksCompleted = vi.fn();
|
||||
let activeRequestCount = 0;
|
||||
const runningTask = createExternalTask({
|
||||
jobId: 'restored-character-task',
|
||||
requestLabel: '角色图片',
|
||||
status: 'running',
|
||||
});
|
||||
const completedTask = createExternalTask({
|
||||
...runningTask,
|
||||
status: 'completed',
|
||||
progress: 100,
|
||||
phaseDetail: '生成已完成。',
|
||||
completedAt: new Date().toISOString(),
|
||||
});
|
||||
listExternalGenerationTasksMock.mockImplementation(
|
||||
(options: Parameters<typeof listExternalGenerationTasks>[0] = {}) => {
|
||||
if (options.statuses?.includes('running')) {
|
||||
activeRequestCount += 1;
|
||||
return Promise.resolve({
|
||||
overview: {
|
||||
pendingCount: activeRequestCount === 1 ? 1 : 0,
|
||||
runningCount: activeRequestCount === 1 ? 1 : 0,
|
||||
unacknowledgedTerminalCount: activeRequestCount === 1 ? 0 : 1,
|
||||
updatedAtMicros: activeRequestCount,
|
||||
},
|
||||
tasks: activeRequestCount === 1 ? [runningTask] : [],
|
||||
});
|
||||
}
|
||||
return Promise.resolve({
|
||||
overview: {
|
||||
pendingCount: 0,
|
||||
runningCount: 0,
|
||||
unacknowledgedTerminalCount: activeRequestCount > 1 ? 1 : 0,
|
||||
updatedAtMicros: activeRequestCount,
|
||||
},
|
||||
tasks: activeRequestCount > 1 ? [completedTask] : [],
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
render(
|
||||
<ImageCanvasTaskSidebarView
|
||||
open
|
||||
onToggleOpen={vi.fn()}
|
||||
onFocusExternalTask={vi.fn()}
|
||||
onExternalTasksCompleted={onExternalTasksCompleted}
|
||||
/>,
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
});
|
||||
expect(screen.getByText('角色图片')).toBeTruthy();
|
||||
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(4000);
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(onExternalTasksCompleted).toHaveBeenCalledWith([
|
||||
expect.objectContaining({
|
||||
jobId: 'restored-character-task',
|
||||
status: 'completed',
|
||||
}),
|
||||
]);
|
||||
expect(onExternalTasksCompleted).toHaveBeenCalledTimes(1);
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it('loads more completed tasks while scrolling and keeps the request capped', async () => {
|
||||
const completedTasks = Array.from({ length: 120 }, (_, index) => {
|
||||
const completedAt = new Date(Date.now() - index * 1000).toISOString();
|
||||
|
||||
@@ -47,6 +47,7 @@ type ImageCanvasTaskSidebarViewProps = {
|
||||
open: boolean;
|
||||
onToggleOpen: () => void;
|
||||
onFocusExternalTask: (task: ExternalGenerationTaskRecord) => void;
|
||||
onExternalTasksCompleted?: (tasks: ExternalGenerationTaskRecord[]) => void;
|
||||
};
|
||||
|
||||
function parseTaskTimeMs(value?: string | null) {
|
||||
@@ -117,9 +118,9 @@ function externalTaskProgressDetail(
|
||||
if (!isActiveTaskStatus(status)) {
|
||||
return null;
|
||||
}
|
||||
const values = [
|
||||
task.phaseDetail.trim(),
|
||||
].filter((value): value is string => Boolean(value));
|
||||
const values = [task.phaseDetail.trim()].filter((value): value is string =>
|
||||
Boolean(value),
|
||||
);
|
||||
return values.length > 0 ? values.join(' · ') : null;
|
||||
}
|
||||
|
||||
@@ -205,6 +206,10 @@ function isTerminalExternalTask(task: ExternalGenerationTaskRecord) {
|
||||
return task.status === 'completed' || task.status === 'failed';
|
||||
}
|
||||
|
||||
function isCompletedExternalTask(task: ExternalGenerationTaskRecord) {
|
||||
return task.status === 'completed';
|
||||
}
|
||||
|
||||
function isEditorCanvasTaskForProject(
|
||||
task: ExternalGenerationTaskRecord,
|
||||
projectId?: string | null,
|
||||
@@ -256,6 +261,7 @@ export function ImageCanvasTaskSidebarView({
|
||||
open,
|
||||
onToggleOpen,
|
||||
onFocusExternalTask,
|
||||
onExternalTasksCompleted,
|
||||
}: ImageCanvasTaskSidebarViewProps) {
|
||||
const projectId = useImageCanvasContextStore((state) => state.projectId);
|
||||
const normalizedProjectId = projectId?.trim() ?? '';
|
||||
@@ -273,6 +279,8 @@ export function ImageCanvasTaskSidebarView({
|
||||
? completedListState.limit
|
||||
: COMPLETED_TASK_LIST_LIMIT;
|
||||
const previousActiveItemCountRef = useRef(0);
|
||||
const activeExternalTaskIdsRef = useRef<Set<string>>(new Set());
|
||||
const completedRefreshNotifiedTaskIdsRef = useRef<Set<string>>(new Set());
|
||||
const filterVisibleExternalTasks = useCallback(
|
||||
(tasks: ExternalGenerationTaskRecord[]) =>
|
||||
tasks.filter((task) =>
|
||||
@@ -280,6 +288,39 @@ export function ImageCanvasTaskSidebarView({
|
||||
),
|
||||
[normalizedProjectId],
|
||||
);
|
||||
const notifyCompletedExternalTasks = useCallback(
|
||||
(
|
||||
tasks: ExternalGenerationTaskRecord[],
|
||||
options: {
|
||||
activeTaskIds?: ReadonlySet<string>;
|
||||
unacknowledgedOnly?: boolean;
|
||||
} = {},
|
||||
) => {
|
||||
const newlyCompletedTasks = tasks.filter((task) => {
|
||||
if (
|
||||
!isCompletedExternalTask(task) ||
|
||||
completedRefreshNotifiedTaskIdsRef.current.has(task.jobId)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (options.activeTaskIds && !options.activeTaskIds.has(task.jobId)) {
|
||||
return false;
|
||||
}
|
||||
if (options.unacknowledgedOnly && task.notificationAcknowledgedAt) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
if (!newlyCompletedTasks.length) {
|
||||
return;
|
||||
}
|
||||
for (const task of newlyCompletedTasks) {
|
||||
completedRefreshNotifiedTaskIdsRef.current.add(task.jobId);
|
||||
}
|
||||
onExternalTasksCompleted?.(newlyCompletedTasks);
|
||||
},
|
||||
[onExternalTasksCompleted],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setCompletedListState((state) =>
|
||||
@@ -287,6 +328,7 @@ export function ImageCanvasTaskSidebarView({
|
||||
? state
|
||||
: { limit: COMPLETED_TASK_LIST_LIMIT, projectId: normalizedProjectId },
|
||||
);
|
||||
completedRefreshNotifiedTaskIdsRef.current = new Set();
|
||||
}, [normalizedProjectId]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -310,12 +352,18 @@ export function ImageCanvasTaskSidebarView({
|
||||
if (disposed) {
|
||||
return;
|
||||
}
|
||||
const visibleActiveTasks = filterVisibleExternalTasks(
|
||||
activeResponse.tasks,
|
||||
);
|
||||
const visibleCompletedTasks = filterVisibleExternalTasks(
|
||||
completedResponse.tasks,
|
||||
);
|
||||
notifyCompletedExternalTasks(visibleCompletedTasks, {
|
||||
unacknowledgedOnly: true,
|
||||
});
|
||||
setExternalTasks(
|
||||
trimStoredExternalTasks(
|
||||
mergeExternalTasks(
|
||||
filterVisibleExternalTasks(activeResponse.tasks),
|
||||
filterVisibleExternalTasks(completedResponse.tasks),
|
||||
),
|
||||
mergeExternalTasks(visibleActiveTasks, visibleCompletedTasks),
|
||||
completedListLimit,
|
||||
),
|
||||
);
|
||||
@@ -329,7 +377,12 @@ export function ImageCanvasTaskSidebarView({
|
||||
disposed = true;
|
||||
controller.abort();
|
||||
};
|
||||
}, [completedListLimit, filterVisibleExternalTasks, refreshKey]);
|
||||
}, [
|
||||
completedListLimit,
|
||||
filterVisibleExternalTasks,
|
||||
notifyCompletedExternalTasks,
|
||||
refreshKey,
|
||||
]);
|
||||
|
||||
const activeExternalTaskIds = useMemo(
|
||||
() =>
|
||||
@@ -340,6 +393,10 @@ export function ImageCanvasTaskSidebarView({
|
||||
);
|
||||
const activeExternalTaskKey = activeExternalTaskIds.join('|');
|
||||
|
||||
useEffect(() => {
|
||||
activeExternalTaskIdsRef.current = new Set(activeExternalTaskIds);
|
||||
}, [activeExternalTaskKey, activeExternalTaskIds]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!activeExternalTaskIds.length) {
|
||||
return undefined;
|
||||
@@ -371,6 +428,9 @@ export function ImageCanvasTaskSidebarView({
|
||||
filterVisibleExternalTasks(activeResponse.tasks),
|
||||
filterVisibleExternalTasks(completedResponse.tasks),
|
||||
);
|
||||
notifyCompletedExternalTasks(refreshedTasks, {
|
||||
activeTaskIds: activeExternalTaskIdsRef.current,
|
||||
});
|
||||
setExternalTasks((currentTasks) =>
|
||||
trimStoredExternalTasks(
|
||||
mergeExternalTaskRefresh(currentTasks, refreshedTasks),
|
||||
@@ -407,6 +467,7 @@ export function ImageCanvasTaskSidebarView({
|
||||
activeExternalTaskKey,
|
||||
completedListLimit,
|
||||
filterVisibleExternalTasks,
|
||||
notifyCompletedExternalTasks,
|
||||
]);
|
||||
|
||||
const handleTaskListScroll = useCallback(
|
||||
|
||||
Reference in New Issue
Block a user