统一生成占位右键删除保护
让 generation 占位右键删除在历史和选择副作用前进入统一请求入口。 保留未收口完美像素身份并恢复普通生成中的删除确认。 补充层命令定向测试并同步技术方案与决策记录。
This commit is contained in:
@@ -1801,6 +1801,48 @@ export function ImageCanvasEditorView({
|
||||
},
|
||||
[openLayerGenerationDialog],
|
||||
);
|
||||
const removeCanvasGenerationDialog = useCallback(
|
||||
(dialogId: string) => {
|
||||
captureCanvasHistory({ type: 'delete-generation-result', count: 1 });
|
||||
removeCanvasGenerationDialogById(dialogId);
|
||||
setSelectedLayerId(null);
|
||||
setSelectedLayerIds([]);
|
||||
setImageContextMenu(null);
|
||||
setContextMenu(null);
|
||||
setActiveTool('select');
|
||||
},
|
||||
[
|
||||
captureCanvasHistory,
|
||||
removeCanvasGenerationDialogById,
|
||||
setActiveTool,
|
||||
setContextMenu,
|
||||
setImageContextMenu,
|
||||
setSelectedLayerId,
|
||||
setSelectedLayerIds,
|
||||
],
|
||||
);
|
||||
const requestRemoveCanvasGenerationDialog = useCallback(
|
||||
(dialog: CanvasGenerationDialogState) => {
|
||||
if (isUnsettledPerfectPixelOperationDialog(dialog)) {
|
||||
activateCanvasGenerationDialog(dialog);
|
||||
showGenerationWarning(
|
||||
'完美像素结果尚未确认,当前占位用于保留原操作身份;请继续核对或按原操作重试。',
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (dialog.status === 'generating') {
|
||||
activateCanvasGenerationDialog(dialog);
|
||||
setPendingGenerationDeleteDialog(dialog);
|
||||
return;
|
||||
}
|
||||
removeCanvasGenerationDialog(dialog.id);
|
||||
},
|
||||
[
|
||||
activateCanvasGenerationDialog,
|
||||
removeCanvasGenerationDialog,
|
||||
showGenerationWarning,
|
||||
],
|
||||
);
|
||||
const contextMenuLayer =
|
||||
contextMenu?.kind === 'layer'
|
||||
? (layers.find((layer) => layer.id === contextMenu.layerId) ?? null)
|
||||
@@ -1842,6 +1884,7 @@ export function ImageCanvasEditorView({
|
||||
selectSingleLayer,
|
||||
onDeleteLayerSideEffects: clearDeletedLayerGenerationState,
|
||||
onDeleteGenerationDialogSideEffects: removeCanvasGenerationDialogById,
|
||||
onRequestDeleteGenerationDialog: requestRemoveCanvasGenerationDialog,
|
||||
exportLayerImage,
|
||||
});
|
||||
const {
|
||||
@@ -1940,26 +1983,6 @@ export function ImageCanvasEditorView({
|
||||
(layerId: string | null) => deleteLayerByIdRef.current(layerId),
|
||||
[],
|
||||
);
|
||||
const removeCanvasGenerationDialog = useCallback(
|
||||
(dialogId: string) => {
|
||||
captureCanvasHistory({ type: 'delete-generation-result', count: 1 });
|
||||
removeCanvasGenerationDialogById(dialogId);
|
||||
setSelectedLayerId(null);
|
||||
setSelectedLayerIds([]);
|
||||
setImageContextMenu(null);
|
||||
setContextMenu(null);
|
||||
setActiveTool('select');
|
||||
},
|
||||
[
|
||||
captureCanvasHistory,
|
||||
removeCanvasGenerationDialogById,
|
||||
setActiveTool,
|
||||
setContextMenu,
|
||||
setImageContextMenu,
|
||||
setSelectedLayerId,
|
||||
setSelectedLayerIds,
|
||||
],
|
||||
);
|
||||
// 中文注释:加载期的剥离只跑一次,当时未到期而被保留的孤儿占位需要这里补上到期清理,
|
||||
// 否则它会一直转到用户下一次加载。两条路径共用同一条文案,用户感知一致。
|
||||
useInlineGenerationPlaceholderExpiry({
|
||||
@@ -1969,28 +1992,6 @@ export function ImageCanvasEditorView({
|
||||
onPlaceholdersExpired: handleInlinePlaceholdersExpired,
|
||||
});
|
||||
|
||||
const requestRemoveCanvasGenerationDialog = useCallback(
|
||||
(dialog: CanvasGenerationDialogState) => {
|
||||
if (isUnsettledPerfectPixelOperationDialog(dialog)) {
|
||||
activateCanvasGenerationDialog(dialog);
|
||||
showGenerationWarning(
|
||||
'完美像素结果尚未确认,当前占位用于保留原操作身份;请继续核对或按原操作重试。',
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (dialog.status === 'generating') {
|
||||
activateCanvasGenerationDialog(dialog);
|
||||
setPendingGenerationDeleteDialog(dialog);
|
||||
return;
|
||||
}
|
||||
removeCanvasGenerationDialog(dialog.id);
|
||||
},
|
||||
[
|
||||
activateCanvasGenerationDialog,
|
||||
removeCanvasGenerationDialog,
|
||||
showGenerationWarning,
|
||||
],
|
||||
);
|
||||
const confirmRemoveGeneratingDialog = useCallback(() => {
|
||||
const dialog = pendingGenerationDeleteDialog;
|
||||
if (!dialog) {
|
||||
|
||||
@@ -109,8 +109,10 @@ function LayerCommandsHarness({
|
||||
exportLayerImage = vi.fn(),
|
||||
onDeleteLayerSideEffects = vi.fn(),
|
||||
onDeleteGenerationDialogSideEffects = vi.fn(),
|
||||
onRequestDeleteGenerationDialog,
|
||||
onCaptureCanvasHistory = vi.fn(),
|
||||
initialLayers,
|
||||
initialContextMenu,
|
||||
initialSelectedLayerId = 'first',
|
||||
initialSelectedLayerIds = ['first', 'second'],
|
||||
initialCanvasGenerationDialogs,
|
||||
@@ -121,8 +123,12 @@ function LayerCommandsHarness({
|
||||
) => void;
|
||||
onDeleteLayerSideEffects?: (layerId: string) => void;
|
||||
onDeleteGenerationDialogSideEffects?: (dialogId: string) => void;
|
||||
onRequestDeleteGenerationDialog?: (
|
||||
dialog: CanvasGenerationDialogState,
|
||||
) => void;
|
||||
onCaptureCanvasHistory?: (action: CanvasHistoryAction) => void;
|
||||
initialLayers?: CanvasLayer[];
|
||||
initialContextMenu?: CanvasContextMenuState;
|
||||
initialSelectedLayerId?: string | null;
|
||||
initialSelectedLayerIds?: string[];
|
||||
initialCanvasGenerationDialogs?: CanvasGenerationDialogState[];
|
||||
@@ -163,7 +169,7 @@ function LayerCommandsHarness({
|
||||
);
|
||||
const [hoveredLayerId, setHoveredLayerId] = useState<string | null>('first');
|
||||
const [contextMenu, setContextMenu] = useState<CanvasContextMenuState | null>(
|
||||
{
|
||||
initialContextMenu ?? {
|
||||
kind: 'layer',
|
||||
layerId: 'first',
|
||||
x: 0,
|
||||
@@ -202,6 +208,7 @@ function LayerCommandsHarness({
|
||||
selectSingleLayer,
|
||||
onDeleteLayerSideEffects,
|
||||
onDeleteGenerationDialogSideEffects,
|
||||
onRequestDeleteGenerationDialog,
|
||||
exportLayerImage,
|
||||
});
|
||||
|
||||
@@ -666,6 +673,48 @@ describe('useImageCanvasLayerCommands', () => {
|
||||
},
|
||||
);
|
||||
|
||||
it('delegates a generation-dialog context delete before history and selection side effects', () => {
|
||||
const dialog = createDurablePerfectPixelDialog(
|
||||
'perfect-pixel-context',
|
||||
'pending-confirmation',
|
||||
);
|
||||
const selectionId = `generation-dialog:${dialog.id}`;
|
||||
const onCaptureCanvasHistory = vi.fn();
|
||||
const onDeleteGenerationDialogSideEffects = vi.fn();
|
||||
const onRequestDeleteGenerationDialog = vi.fn();
|
||||
render(
|
||||
<LayerCommandsHarness
|
||||
initialContextMenu={{
|
||||
kind: 'generation-dialog',
|
||||
dialogId: dialog.id,
|
||||
x: 0,
|
||||
y: 0,
|
||||
canvasPoint: { x: 0, y: 0 },
|
||||
}}
|
||||
initialSelectedLayerId={selectionId}
|
||||
initialSelectedLayerIds={[selectionId]}
|
||||
initialCanvasGenerationDialogs={[dialog]}
|
||||
onCaptureCanvasHistory={onCaptureCanvasHistory}
|
||||
onDeleteGenerationDialogSideEffects={
|
||||
onDeleteGenerationDialogSideEffects
|
||||
}
|
||||
onRequestDeleteGenerationDialog={onRequestDeleteGenerationDialog}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '删除右键目标' }));
|
||||
|
||||
expect(onRequestDeleteGenerationDialog).toHaveBeenCalledTimes(1);
|
||||
expect(onRequestDeleteGenerationDialog).toHaveBeenCalledWith(dialog);
|
||||
expect(screen.getByTestId('history').textContent).toBe('0');
|
||||
expect(screen.getByTestId('selection').textContent).toBe(
|
||||
`${selectionId}:${selectionId}`,
|
||||
);
|
||||
expect(screen.getByTestId('context').textContent).toBe('closed');
|
||||
expect(onCaptureCanvasHistory).not.toHaveBeenCalled();
|
||||
expect(onDeleteGenerationDialogSideEffects).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('deletes and counts only deletable targets in a mixed durable-dialog selection', () => {
|
||||
const dialog = createDurablePerfectPixelDialog(
|
||||
'perfect-pixel-pending',
|
||||
|
||||
@@ -57,6 +57,9 @@ type LayerCommandsOptions = {
|
||||
selectSingleLayer: (layerId: string | null) => void;
|
||||
onDeleteLayerSideEffects: (targetLayerId: string) => void;
|
||||
onDeleteGenerationDialogSideEffects?: (targetDialogId: string) => void;
|
||||
onRequestDeleteGenerationDialog?: (
|
||||
targetDialog: CanvasGenerationDialogState,
|
||||
) => void;
|
||||
exportLayerImage: (
|
||||
layer: CanvasLayer | null,
|
||||
options?: { mode?: ImageSequenceExportMode },
|
||||
@@ -146,6 +149,7 @@ export function useImageCanvasLayerCommands({
|
||||
selectSingleLayer,
|
||||
onDeleteLayerSideEffects,
|
||||
onDeleteGenerationDialogSideEffects,
|
||||
onRequestDeleteGenerationDialog,
|
||||
exportLayerImage,
|
||||
}: LayerCommandsOptions) {
|
||||
const [canvasClipboard, setCanvasClipboard] =
|
||||
@@ -527,12 +531,27 @@ export function useImageCanvasLayerCommands({
|
||||
const deleteContextLayers = useCallback(() => {
|
||||
const targetIds = getContextTargetLayerIds();
|
||||
const targetLayerIds = getSelectedLayerIds(targetIds);
|
||||
const targetDialogIds = targetIds
|
||||
const targetDialogs = targetIds
|
||||
.map(getCanvasGenerationDialogIdFromSelectionId)
|
||||
.filter((dialogId): dialogId is string => Boolean(dialogId))
|
||||
.filter((dialogId) =>
|
||||
canvasGenerationDialogs.some((dialog) => dialog.id === dialogId),
|
||||
);
|
||||
.flatMap((dialogId) => {
|
||||
const dialog = canvasGenerationDialogs.find(
|
||||
(candidate) => candidate.id === dialogId,
|
||||
);
|
||||
return dialog ? [dialog] : [];
|
||||
});
|
||||
if (
|
||||
!targetLayerIds.length &&
|
||||
targetDialogs.length &&
|
||||
onRequestDeleteGenerationDialog
|
||||
) {
|
||||
closeContextMenus();
|
||||
targetDialogs.forEach((dialog) => {
|
||||
onRequestDeleteGenerationDialog(dialog);
|
||||
});
|
||||
return;
|
||||
}
|
||||
const targetDialogIds = targetDialogs.map((dialog) => dialog.id);
|
||||
if (!targetLayerIds.length && !targetDialogIds.length) {
|
||||
return;
|
||||
}
|
||||
@@ -566,6 +585,7 @@ export function useImageCanvasLayerCommands({
|
||||
getContextTargetLayerIds,
|
||||
onDeleteGenerationDialogSideEffects,
|
||||
onDeleteLayerSideEffects,
|
||||
onRequestDeleteGenerationDialog,
|
||||
selectSingleLayer,
|
||||
setHoveredLayerId,
|
||||
setLayers,
|
||||
|
||||
Reference in New Issue
Block a user