改进生成中占位图删除确认
生成中占位图删除前提示泥点不返还 占位图增加独立删除按钮并接入键盘删除请求 补充生成中占位图删除确认回归测试 记录本轮画布删除确认验证进度
This commit is contained in:
@@ -144,6 +144,7 @@
|
||||
- 2026-06-17 前端拆分第十九执行批次:新增 `useImageCanvasCanvasDropWorkflow`,把画布区域 drag over / drag leave / drop 分流从主视图抽出,覆盖素材库图片拖入画布、本地文件拖入画布、无关拖拽不拦截、默认文件夹选择和画布遮罩清理;主视图继续注入素材建层、文件上传、drop 点换算和素材移动高亮清理。新增 hook 单测覆盖拖拽入画布细节,主视图从 1452 行降至 1405 行。验证命令:`npm run test -- src/components/image-editor/useImageCanvasCanvasDropWorkflow.test.tsx src/components/image-editor/useImageCanvasStageInteractions.test.tsx src/components/image-editor/useImageCanvasViewportControls.test.tsx src/components/image-editor/ImageCanvasInteractionModel.test.ts src/components/image-editor/useCanvasHistory.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10003/editor/canvas` 登录态刷新后素材 / 画布 / 小地图和底部工具栏可见,真实鼠标拖拽素材库图片到画布时出现 `添加到画布` 遮罩,松手后画布图层数量从 4 增至 5;`画布背景设置` 点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)` 且 `background-image: none`;抓手工具可切回选择工具,登录后控制台无前端 error。
|
||||
- 2026-06-17 前端拆分第二十执行批次:新增 `ImageCanvasMetadataModalView`,把图片信息弹窗从主视图抽出,承载图片类型、生成输入、参考图、模型、分辨率、Provider、Task 和 Object 信息渲染;主视图只保留 `metadataLayer` 状态和关闭回调。同步修复未登录进入编辑器时项目 / 素材接口抢跑 401、`重置画布视图` 点击事件误传给适合视图函数的问题。新增组件单测覆盖生成图 metadata、上传图 fallback 和关闭回调,新增 hook / 主视图测试覆盖未登录不请求受保护素材 / 工程数据和重置按钮回归;主视图从 1405 行降至 1337 行。验证命令:`npm run test -- src/components/image-editor/useImageCanvasAssetLibrary.test.tsx src/components/image-editor/useImageCanvasProjectPersistence.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx src/components/image-editor/ImageCanvasMetadataModalView.test.tsx src/components/image-editor/useImageCanvasEditorChrome.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10003/editor/canvas` 清空会话后直接弹出 `账号入口`,且未登录状态下没有发起 `/api/editor/*` 请求;登录临时开发账号后 `重置画布视图` 无控制台错误,`画布背景设置` 保持 Lovart 式白色浮层,点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)`,上传素材可加入画布,右上角图片信息按钮可打开不透明白底元数据弹窗,关闭后 `AI画布工具栏` 仍可见。
|
||||
- 2026-06-17 前端拆分第二十一执行批次:新增 `useImageCanvasKeyboardShortcuts`,把 Ctrl / Cmd + Z 撤销、Ctrl / Cmd + Shift + Z 重做、Shift 状态、Backspace / Delete 删除、Escape 关闭临时面板和 Space 临时抓手从主视图抽出;主视图继续注入图层删除、生成对话框、快速编辑和 chrome 面板 setter。新增 hook 单测覆盖输入框忽略快捷键、删除选中图层、删除生成占位、Escape 保留生成中面板、Space 和 Shift;主视图从 1337 行降至 1250 行。验证命令:`npm run test -- src/components/image-editor/useImageCanvasKeyboardShortcuts.test.tsx src/components/image-editor/ImageCanvasMetadataModalView.test.tsx src/components/image-editor/useImageCanvasAssetLibrary.test.tsx src/components/image-editor/useImageCanvasProjectPersistence.test.tsx src/components/image-editor/useImageCanvasCanvasDropWorkflow.test.tsx src/components/image-editor/useImageCanvasStageInteractions.test.tsx src/components/image-editor/useImageCanvasViewportControls.test.tsx src/components/image-editor/ImageCanvasInteractionModel.test.ts src/components/image-editor/useCanvasHistory.test.tsx src/components/image-editor/useImageCanvasEditorChrome.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10003/editor/canvas` 未登录直接弹出 `账号入口` 且未抢跑 `/api/editor/*`,登录后 `/api/editor/assets/library` 和 `/api/editor/projects/recent` 为 200,`AI画布工具栏` 与 `画布面板入口` 可见,viewport 背景为 `rgb(248, 250, 252)` 且 `background-image: none`;按住 Space 从 `文字工具` 临时切到 `抓手工具`,松开恢复 `文字工具`;`画布背景设置` 点击 `暖灰` 后背景变为 `rgb(243, 240, 234)`;点击 `生成工具` 后 `Image Generator` 占位框、`生成图片` 对话框和 `AI画布工具栏` 同时可见,登录后控制台无前端 error。
|
||||
- 2026-06-18 生成中占位图删除确认:画布生成占位框增加删除按钮,删除 `generating` 状态占位图时统一弹出 `删除生成中的占位图` 二次确认,并提示 `已消耗的泥点不会返还`;空闲占位仍直接删除。键盘 Backspace / Delete 删除生成占位也改走同一请求入口,避免绕过提示;确认删除后清理选中态、右键菜单并切回选择工具,已删除的生成任务即使后续返回结果也不会落图。验证命令:`npm run test -- src/components/image-editor/ImageCanvasWorldView.test.tsx src/components/image-editor/useImageCanvasKeyboardShortcuts.test.tsx src/components/image-editor/ImageCanvasEditorGenerationIntegration.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:本地 Vite + Chrome CDP 打开 `/editor/canvas`,点击 `生成图片` 创建生成中占位,删除按钮弹出确认框,取消后占位保留,确认后占位移除且提示包含泥点不返还。
|
||||
- 2026-06-17 前端拆分第二十二执行批次:新增 `useImageCanvasAssetPointerDragBridge`,把素材卡片 pointer 拖拽到画布 / 文件夹的全局监听、拖拽激活、画布 drop 提示、文件夹高亮、移动素材、加入画布和点击抑制从主视图抽出;主视图继续负责素材库事实、画布建层、历史和工程资源持久化。同步恢复 `账号入口` 认证弹窗 portal 渲染,避免全屏画布遮住登录弹窗;`画布背景设置` 调整为独立白色浮层,包含当前色、色域、色相、自定义颜色、预设网格、HEX 输入和恢复默认。验证命令:`npm run test -- src/components/image-editor/useImageCanvasAssetPointerDragBridge.test.tsx src/components/image-editor/useImageCanvasKeyboardShortcuts.test.tsx src/components/image-editor/ImageCanvasMetadataModalView.test.tsx src/components/image-editor/useImageCanvasAssetLibrary.test.tsx src/components/image-editor/useImageCanvasProjectPersistence.test.tsx src/components/image-editor/useImageCanvasCanvasDropWorkflow.test.tsx src/components/image-editor/useImageCanvasStageInteractions.test.tsx src/components/image-editor/useImageCanvasViewportControls.test.tsx src/components/image-editor/ImageCanvasInteractionModel.test.ts src/components/image-editor/useCanvasHistory.test.tsx src/components/image-editor/useImageCanvasEditorChrome.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx src/components/auth/PlatformAuthModalShell.test.tsx src/components/auth/AuthGate.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10007/editor/canvas` 未登录打开直接显示 `账号入口`;登录临时开发账号后 `画布背景设置` 显示当前色、色域、色相、自定义颜色、预设、HEX 和恢复默认,点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)` 且 `background-image: none`;上传图片进入 `项目素材`,真实 pointer 拖拽素材到画布后图层数从 0 到 1 且 `AI画布工具栏` 保持可见;新建 `SmokeFolder` 后真实 pointer 拖拽素材到文件夹,`项目素材` 变 0、`SmokeFolder` 变 1,素材执行移动而非拷贝。控制台仅有未登录 refresh 401,登录后编辑器 API 均为 200。
|
||||
- 2026-06-17 前端拆分第二十三执行批次:新增 `ImageCanvasOverlayModel`,把生成输入框锚定、图标素材生成面板宽度、选中图片工具栏边界、快速编辑面板和角色动画面板定位从主视图抽出为纯模型;主视图继续保留生成 / quick edit / 角色动画状态机和舞台编排。新增模型单测覆盖锚定优先级、生成中隐藏、icon 宽度、工具栏 clamp、quick edit / 角色动画边界和生成 dialog 模式识别;主视图从 1182 行降至 1133 行。验证命令:`npm run test -- src/components/image-editor/ImageCanvasOverlayModel.test.ts src/components/image-editor/ImageCanvasEditorView.test.tsx`、`npm run test -- src/components/image-editor/useImageCanvasAssetPointerDragBridge.test.tsx src/components/image-editor/useImageCanvasEditorChrome.test.tsx src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx`、`npm run test -- src/components/image-editor/ImageCanvasOverlayModel.test.ts src/components/image-editor/useImageCanvasAssetPointerDragBridge.test.tsx src/components/image-editor/useImageCanvasKeyboardShortcuts.test.tsx src/components/image-editor/ImageCanvasMetadataModalView.test.tsx src/components/image-editor/useImageCanvasAssetLibrary.test.tsx src/components/image-editor/useImageCanvasProjectPersistence.test.tsx src/components/image-editor/useImageCanvasCanvasDropWorkflow.test.tsx src/components/image-editor/useImageCanvasStageInteractions.test.tsx src/components/image-editor/useImageCanvasViewportControls.test.tsx src/components/image-editor/ImageCanvasInteractionModel.test.ts src/components/image-editor/useCanvasHistory.test.tsx src/components/image-editor/useImageCanvasEditorChrome.test.tsx src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx src/components/auth/PlatformAuthModalShell.test.tsx src/components/auth/AuthGate.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10007/editor/canvas` 未登录打开显示 `账号入口`,登录后 `画布背景设置` 点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)` 且 `background-image: none`,点击 `生成工具` 后 `Image Generator`、`生成图片` 对话框和 `AI画布工具栏` 均可见;素材库已有素材真实 pointer 拖入画布后图层数从 0 到 1,工具栏保持可见,截图留存于 `output/playwright/editor-overlay-model-regression-20260617.png`。
|
||||
- 2026-06-17 前端拆分第二十四执行批次:新增 `useImageCanvasAssetCanvasBridge`,把删除素材清理画布图层、素材加入画布、素材 pointer 拖入画布 / 文件夹、画布 drag/drop 分流和文件拖入画布参数组装从主视图抽成素材到画布桥接 hook;主视图继续保留素材库事实、上传读取、工程资源持久化和历史触发。新增 hook 单测覆盖素材建层、pointer drop 入画布和删除素材清理关联图层 / 选中态;主视图从 1133 行降至 1086 行。验证命令:`npm run test -- src/components/image-editor/useImageCanvasAssetCanvasBridge.test.tsx src/components/image-editor/useImageCanvasAssetPointerDragBridge.test.tsx src/components/image-editor/useImageCanvasCanvasDropWorkflow.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10007/editor/canvas` 清空会话后未登录直接显示 `账号入口`;登录临时开发账号后 `画布背景设置` 是完整设置面板,包含当前色、色域、色相、自定义颜色、预设、HEX 和恢复默认,点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)` 且 `background-image: none`,HEX 输入 `#ffffff` 后变为白色,按 Escape 关闭面板;登录后上传图片请求 `/api/editor/assets` 返回 200,素材库出现上传素材,`AI画布工具栏` 保持可见。
|
||||
|
||||
@@ -497,6 +497,68 @@ describe('ImageCanvasEditorView generation integration', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('confirms before deleting a generating placeholder because mud points are not refunded', async () => {
|
||||
let resolveGeneration!: (value: unknown) => void;
|
||||
generateEditorImageMock.mockReturnValueOnce(
|
||||
new Promise((resolve) => {
|
||||
resolveGeneration = resolve;
|
||||
}),
|
||||
);
|
||||
render(<ImageCanvasEditorView />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(loadOrCreateRecentEditorProjectMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '生成图片' }));
|
||||
fireEvent.change(screen.getByLabelText('生成提示词'), {
|
||||
target: { value: '准备删除的生成中图片' },
|
||||
});
|
||||
fireEvent.click(screen.getByRole('button', { name: '生成' }));
|
||||
|
||||
const frame = screen.getByLabelText('图像生成占位图');
|
||||
expect(frame.className).toContain(
|
||||
'image-canvas-editor__generation-frame--generating',
|
||||
);
|
||||
|
||||
fireEvent.keyDown(window, { key: 'Delete', code: 'Delete' });
|
||||
|
||||
const confirmDialog = screen.getByRole('dialog', {
|
||||
name: '删除生成中的占位图',
|
||||
});
|
||||
expect(
|
||||
within(confirmDialog).getByText(
|
||||
'删除后会停止在画布中显示这个生成任务,已消耗的泥点不会返还。',
|
||||
),
|
||||
).toBeTruthy();
|
||||
expect(screen.getByLabelText('图像生成占位图')).toBeTruthy();
|
||||
|
||||
fireEvent.click(within(confirmDialog).getByRole('button', { name: '取消' }));
|
||||
expect(screen.queryByRole('dialog', { name: '删除生成中的占位图' })).toBeNull();
|
||||
expect(screen.getByLabelText('图像生成占位图')).toBeTruthy();
|
||||
|
||||
fireEvent.keyDown(window, { key: 'Delete', code: 'Delete' });
|
||||
fireEvent.click(screen.getByRole('button', { name: '确认删除' }));
|
||||
|
||||
expect(screen.queryByLabelText('图像生成占位图')).toBeNull();
|
||||
|
||||
await act(async () => {
|
||||
resolveGeneration({
|
||||
imageSrc: 'data:image/png;base64,ZGVsZXRlZC1wbGFjZWhvbGRlcg==',
|
||||
width: 1024,
|
||||
height: 1024,
|
||||
sourceType: 'generated',
|
||||
prompt: '准备删除的生成中图片',
|
||||
actualPrompt: '准备删除的生成中图片',
|
||||
model: 'gpt-image-2',
|
||||
provider: 'VectorEngine',
|
||||
taskId: 'editor-deleted-placeholder-1',
|
||||
});
|
||||
});
|
||||
|
||||
expect(screen.queryByAltText(/画布图片:生成图片/)).toBeNull();
|
||||
});
|
||||
|
||||
it('hides the generation composer when selecting another image but keeps the placeholder', () => {
|
||||
render(<ImageCanvasEditorView />);
|
||||
|
||||
|
||||
@@ -159,6 +159,7 @@ function createStageProps(): ImageCanvasStageViewProps {
|
||||
onOpenLayerMetadata: vi.fn(),
|
||||
onGenerationFramePointerDown: vi.fn(),
|
||||
onActivateGenerationDialog: vi.fn(),
|
||||
onDeleteGenerationDialog: vi.fn(),
|
||||
onOpenQuickEditPanel: vi.fn(),
|
||||
onOpenRedrawPanel: vi.fn(),
|
||||
onOpenCropExpandPanel: vi.fn(),
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
createEditorProjectResource,
|
||||
} from '../../services/image-editor/editorProjectClient';
|
||||
import { useAuthUi } from '../auth/AuthUiContext';
|
||||
import { PlatformDangerConfirmDialog } from '../common/PlatformDangerConfirmDialog';
|
||||
import { resolveContextMenuPosition } from './ImageCanvasEditorModel';
|
||||
import { ImageCanvasEditorShellView } from './ImageCanvasEditorShellView';
|
||||
import type {
|
||||
@@ -70,6 +71,8 @@ export function ImageCanvasEditorView() {
|
||||
const [selectedLayerIds, setSelectedLayerIds] = useState<string[]>([]);
|
||||
const [hoveredLayerId, setHoveredLayerId] = useState<string | null>(null);
|
||||
const [metadataLayer, setMetadataLayer] = useState<CanvasLayer | null>(null);
|
||||
const [pendingGenerationDeleteDialog, setPendingGenerationDeleteDialog] =
|
||||
useState<CanvasGenerationDialogState | null>(null);
|
||||
const [imageContextMenu, setImageContextMenu] =
|
||||
useState<ImageContextMenuState | null>(null);
|
||||
const [contextMenu, setContextMenu] = useState<CanvasContextMenuState | null>(
|
||||
@@ -664,6 +667,45 @@ export function ImageCanvasEditorView() {
|
||||
(layerId: string | null) => deleteLayerByIdRef.current(layerId),
|
||||
[],
|
||||
);
|
||||
const removeCanvasGenerationDialog = useCallback(
|
||||
(dialogId: string) => {
|
||||
captureCanvasHistory();
|
||||
removeCanvasGenerationDialogById(dialogId);
|
||||
setSelectedLayerId(null);
|
||||
setSelectedLayerIds([]);
|
||||
setImageContextMenu(null);
|
||||
setContextMenu(null);
|
||||
setActiveTool('select');
|
||||
},
|
||||
[
|
||||
captureCanvasHistory,
|
||||
removeCanvasGenerationDialogById,
|
||||
setActiveTool,
|
||||
setContextMenu,
|
||||
setImageContextMenu,
|
||||
setSelectedLayerId,
|
||||
setSelectedLayerIds,
|
||||
],
|
||||
);
|
||||
const requestRemoveCanvasGenerationDialog = useCallback(
|
||||
(dialog: CanvasGenerationDialogState) => {
|
||||
if (dialog.status === 'generating') {
|
||||
activateCanvasGenerationDialog(dialog);
|
||||
setPendingGenerationDeleteDialog(dialog);
|
||||
return;
|
||||
}
|
||||
removeCanvasGenerationDialog(dialog.id);
|
||||
},
|
||||
[activateCanvasGenerationDialog, removeCanvasGenerationDialog],
|
||||
);
|
||||
const confirmRemoveGeneratingDialog = useCallback(() => {
|
||||
const dialog = pendingGenerationDeleteDialog;
|
||||
if (!dialog) {
|
||||
return;
|
||||
}
|
||||
setPendingGenerationDeleteDialog(null);
|
||||
removeCanvasGenerationDialog(dialog.id);
|
||||
}, [pendingGenerationDeleteDialog, removeCanvasGenerationDialog]);
|
||||
|
||||
useImageCanvasKeyboardShortcuts({
|
||||
generateDialogRef,
|
||||
@@ -671,11 +713,7 @@ export function ImageCanvasEditorView() {
|
||||
redoCanvasChange,
|
||||
undoCanvasChange,
|
||||
deleteLayerById: deleteLayerByIdFromShortcut,
|
||||
removeCanvasGenerationDialogById: (dialogId: string) => {
|
||||
captureCanvasHistory();
|
||||
removeCanvasGenerationDialogById(dialogId);
|
||||
setActiveTool('select');
|
||||
},
|
||||
requestRemoveCanvasGenerationDialog,
|
||||
setActiveTool,
|
||||
setGenerateDialog,
|
||||
setImageContextMenu,
|
||||
@@ -897,6 +935,7 @@ export function ImageCanvasEditorView() {
|
||||
},
|
||||
onGenerationFramePointerDown: handleGenerationFramePointerDown,
|
||||
onActivateGenerationDialog: activateCanvasGenerationDialog,
|
||||
onDeleteGenerationDialog: requestRemoveCanvasGenerationDialog,
|
||||
onOpenQuickEditPanel: openQuickEditPanel,
|
||||
onOpenRedrawPanel: openRedrawPanel,
|
||||
onOpenCropExpandPanel: openCropExpandPanel,
|
||||
@@ -936,20 +975,31 @@ export function ImageCanvasEditorView() {
|
||||
};
|
||||
|
||||
return (
|
||||
<ImageCanvasEditorShellView
|
||||
editorRootRef={editorRootRef}
|
||||
uploadInputRef={uploadInputRef}
|
||||
uploadAccept={getEditorUploadAccept(uploadTarget)}
|
||||
onUploadInputChange={handleUploadInputChange}
|
||||
assetDragPreview={assetDragPreview}
|
||||
sidebarProps={sidebarProps}
|
||||
topbarProps={topbarProps}
|
||||
stageProps={stageProps}
|
||||
metadataProps={{
|
||||
layer: metadataLayer,
|
||||
onClose: () => setMetadataLayer(null),
|
||||
}}
|
||||
/>
|
||||
<>
|
||||
<ImageCanvasEditorShellView
|
||||
editorRootRef={editorRootRef}
|
||||
uploadInputRef={uploadInputRef}
|
||||
uploadAccept={getEditorUploadAccept(uploadTarget)}
|
||||
onUploadInputChange={handleUploadInputChange}
|
||||
assetDragPreview={assetDragPreview}
|
||||
sidebarProps={sidebarProps}
|
||||
topbarProps={topbarProps}
|
||||
stageProps={stageProps}
|
||||
metadataProps={{
|
||||
layer: metadataLayer,
|
||||
onClose: () => setMetadataLayer(null),
|
||||
}}
|
||||
/>
|
||||
<PlatformDangerConfirmDialog
|
||||
open={Boolean(pendingGenerationDeleteDialog)}
|
||||
title="删除生成中的占位图"
|
||||
confirmLabel="确认删除"
|
||||
onClose={() => setPendingGenerationDeleteDialog(null)}
|
||||
onConfirm={confirmRemoveGeneratingDialog}
|
||||
>
|
||||
删除后会停止在画布中显示这个生成任务,已消耗的泥点不会返还。
|
||||
</PlatformDangerConfirmDialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -90,6 +90,7 @@ export type ImageCanvasStageViewProps = {
|
||||
dialog: CanvasGenerationDialogState,
|
||||
) => void;
|
||||
onActivateGenerationDialog: (dialog: CanvasGenerationDialogState) => void;
|
||||
onDeleteGenerationDialog: (dialog: CanvasGenerationDialogState) => void;
|
||||
onOpenQuickEditPanel: (layer: CanvasLayer) => void;
|
||||
onOpenRedrawPanel: (layer: CanvasLayer) => void;
|
||||
onOpenCropExpandPanel: (layer: CanvasLayer) => void;
|
||||
@@ -177,6 +178,7 @@ export function ImageCanvasStageView({
|
||||
onOpenLayerMetadata,
|
||||
onGenerationFramePointerDown,
|
||||
onActivateGenerationDialog,
|
||||
onDeleteGenerationDialog,
|
||||
onOpenQuickEditPanel,
|
||||
onOpenRedrawPanel,
|
||||
onOpenCropExpandPanel,
|
||||
@@ -256,6 +258,7 @@ export function ImageCanvasStageView({
|
||||
onOpenLayerMetadata={onOpenLayerMetadata}
|
||||
onGenerationFramePointerDown={onGenerationFramePointerDown}
|
||||
onActivateGenerationDialog={onActivateGenerationDialog}
|
||||
onDeleteGenerationDialog={onDeleteGenerationDialog}
|
||||
/>
|
||||
|
||||
<ImageCanvasSelectedLayerToolbarView
|
||||
|
||||
@@ -70,6 +70,7 @@ function renderWorldView(
|
||||
onOpenLayerMetadata: vi.fn(),
|
||||
onGenerationFramePointerDown: vi.fn(),
|
||||
onActivateGenerationDialog: vi.fn(),
|
||||
onDeleteGenerationDialog: vi.fn(),
|
||||
...overrides,
|
||||
};
|
||||
|
||||
@@ -215,12 +216,16 @@ describe('ImageCanvasWorldView', () => {
|
||||
|
||||
fireEvent.pointerDown(frame);
|
||||
fireEvent.doubleClick(frame);
|
||||
fireEvent.click(
|
||||
within(frame).getByRole('button', { name: '删除图标素材生成占位图' }),
|
||||
);
|
||||
|
||||
expect(props.onGenerationFramePointerDown).toHaveBeenCalledWith(
|
||||
expect.any(Object),
|
||||
dialog,
|
||||
);
|
||||
expect(props.onActivateGenerationDialog).toHaveBeenCalledWith(dialog);
|
||||
expect(props.onDeleteGenerationDialog).toHaveBeenCalledWith(dialog);
|
||||
expect(screen.queryByText('dialog-without-placeholder')).toBeNull();
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
import { Film, ImageIcon, Info, Music2, UserRound, Volume2 } from 'lucide-react';
|
||||
import {
|
||||
Film,
|
||||
ImageIcon,
|
||||
Info,
|
||||
Music2,
|
||||
Trash2,
|
||||
UserRound,
|
||||
Volume2,
|
||||
} from 'lucide-react';
|
||||
import type {
|
||||
CSSProperties,
|
||||
MouseEvent as ReactMouseEvent,
|
||||
@@ -111,6 +119,7 @@ function buildInverseScaleStyle(
|
||||
'--image-canvas-editor-kind-offset': `calc(0.38rem * ${inverseScale})`,
|
||||
'--image-canvas-editor-beside-kind-offset': `calc(3.95rem * ${inverseScale})`,
|
||||
'--image-canvas-editor-frame-label-offset': `calc(-1.35rem * ${inverseScale})`,
|
||||
'--image-canvas-editor-frame-action-offset': `calc(-0.75rem * ${inverseScale})`,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -145,6 +154,7 @@ export type ImageCanvasWorldViewProps = {
|
||||
dialog: CanvasGenerationDialogState,
|
||||
) => void;
|
||||
onActivateGenerationDialog: (dialog: CanvasGenerationDialogState) => void;
|
||||
onDeleteGenerationDialog: (dialog: CanvasGenerationDialogState) => void;
|
||||
};
|
||||
|
||||
export function ImageCanvasWorldView({
|
||||
@@ -166,6 +176,7 @@ export function ImageCanvasWorldView({
|
||||
onOpenLayerMetadata,
|
||||
onGenerationFramePointerDown,
|
||||
onActivateGenerationDialog,
|
||||
onDeleteGenerationDialog,
|
||||
}: ImageCanvasWorldViewProps) {
|
||||
const inverseScale = getInverseViewportScale(viewport);
|
||||
const inverseScaleStyle = buildInverseScaleStyle(inverseScale);
|
||||
@@ -382,6 +393,21 @@ export function ImageCanvasWorldView({
|
||||
{getGenerationFrameLabel(dialog)}
|
||||
</span>
|
||||
) : null}
|
||||
{showFocusedChrome ? (
|
||||
<PlatformIconButton
|
||||
variant="darkMini"
|
||||
className="image-canvas-editor__generation-frame-delete"
|
||||
label={`删除${getGenerationFrameAriaLabel(dialog)}`}
|
||||
icon={<Trash2 className="h-3 w-3" />}
|
||||
style={inverseScaleStyle}
|
||||
onPointerDown={(event) => event.stopPropagation()}
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
onDeleteGenerationDialog(dialog);
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
{placeholderMeta.badgeLabel ? (
|
||||
<span
|
||||
className={`image-canvas-editor__kind-badge image-canvas-editor__kind-badge--${placeholderMeta.badgeClassName}`}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useRef, useState } from 'react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type {
|
||||
CanvasGenerationDialogState,
|
||||
CanvasTool,
|
||||
GenerateDialogState,
|
||||
QuickEditPanelState,
|
||||
@@ -59,7 +60,7 @@ function KeyboardShortcutsHarness({
|
||||
undoCanvasChange = vi.fn(),
|
||||
redoCanvasChange = vi.fn(),
|
||||
deleteLayerById = vi.fn(),
|
||||
removeCanvasGenerationDialogById = vi.fn(),
|
||||
requestRemoveCanvasGenerationDialog = vi.fn(),
|
||||
closeEditorChromePanels = vi.fn(),
|
||||
}: {
|
||||
selectedLayerId?: string | null;
|
||||
@@ -69,7 +70,9 @@ function KeyboardShortcutsHarness({
|
||||
undoCanvasChange?: () => void;
|
||||
redoCanvasChange?: () => void;
|
||||
deleteLayerById?: (layerId: string | null) => void;
|
||||
removeCanvasGenerationDialogById?: (dialogId: string) => void;
|
||||
requestRemoveCanvasGenerationDialog?: (
|
||||
dialog: CanvasGenerationDialogState,
|
||||
) => void;
|
||||
closeEditorChromePanels?: () => void;
|
||||
}) {
|
||||
const [activeTool, setActiveTool] = useState<CanvasTool>(initialTool);
|
||||
@@ -110,7 +113,7 @@ function KeyboardShortcutsHarness({
|
||||
redoCanvasChange,
|
||||
undoCanvasChange,
|
||||
deleteLayerById,
|
||||
removeCanvasGenerationDialogById,
|
||||
requestRemoveCanvasGenerationDialog,
|
||||
setActiveTool: (tool) => setActiveTool(tool),
|
||||
setGenerateDialog: (updater) =>
|
||||
setGenerateDialogState((currentDialog) =>
|
||||
@@ -229,13 +232,14 @@ describe('useImageCanvasKeyboardShortcuts', () => {
|
||||
expect(deleteLayerById).toHaveBeenCalledWith('layer-selected');
|
||||
});
|
||||
|
||||
it('removes editable generation placeholders with Backspace', () => {
|
||||
const removeCanvasGenerationDialogById = vi.fn();
|
||||
it('requests deleting editable generation placeholders with Backspace', () => {
|
||||
const requestRemoveCanvasGenerationDialog = vi.fn();
|
||||
const dialog = createPlaceholderDialog('character');
|
||||
render(
|
||||
<KeyboardShortcutsHarness
|
||||
initialTool="character"
|
||||
initialGenerateDialog={createPlaceholderDialog('character')}
|
||||
removeCanvasGenerationDialogById={removeCanvasGenerationDialogById}
|
||||
initialGenerateDialog={dialog}
|
||||
requestRemoveCanvasGenerationDialog={requestRemoveCanvasGenerationDialog}
|
||||
/>,
|
||||
);
|
||||
|
||||
@@ -243,7 +247,7 @@ describe('useImageCanvasKeyboardShortcuts', () => {
|
||||
fireEvent.keyDown(window, { key: 'Backspace', code: 'Backspace' });
|
||||
});
|
||||
|
||||
expect(removeCanvasGenerationDialogById).toHaveBeenCalledWith('dialog-1');
|
||||
expect(requestRemoveCanvasGenerationDialog).toHaveBeenCalledWith(dialog);
|
||||
expect(screen.getByTestId('generate-dialog').textContent).toBe(
|
||||
'character:idle:true',
|
||||
);
|
||||
@@ -254,15 +258,15 @@ describe('useImageCanvasKeyboardShortcuts', () => {
|
||||
expect(screen.getByTestId('icon-picking').textContent).toBe('false');
|
||||
});
|
||||
|
||||
it('removes generating placeholders with Delete', () => {
|
||||
const removeCanvasGenerationDialogById = vi.fn();
|
||||
it('requests deleting generating placeholders with Delete', () => {
|
||||
const requestRemoveCanvasGenerationDialog = vi.fn();
|
||||
const generatingDialog = createPlaceholderDialog('generate');
|
||||
generatingDialog.status = 'generating';
|
||||
render(
|
||||
<KeyboardShortcutsHarness
|
||||
initialTool="generate"
|
||||
initialGenerateDialog={generatingDialog}
|
||||
removeCanvasGenerationDialogById={removeCanvasGenerationDialogById}
|
||||
requestRemoveCanvasGenerationDialog={requestRemoveCanvasGenerationDialog}
|
||||
/>,
|
||||
);
|
||||
|
||||
@@ -270,7 +274,9 @@ describe('useImageCanvasKeyboardShortcuts', () => {
|
||||
fireEvent.keyDown(window, { key: 'Delete', code: 'Delete' });
|
||||
});
|
||||
|
||||
expect(removeCanvasGenerationDialogById).toHaveBeenCalledWith('dialog-1');
|
||||
expect(requestRemoveCanvasGenerationDialog).toHaveBeenCalledWith(
|
||||
generatingDialog,
|
||||
);
|
||||
expect(screen.getByTestId('active-tool').textContent).toBe('select');
|
||||
});
|
||||
|
||||
|
||||
@@ -12,7 +12,9 @@ type UseImageCanvasKeyboardShortcutsOptions = {
|
||||
redoCanvasChange: () => void;
|
||||
undoCanvasChange: () => void;
|
||||
deleteLayerById: (layerId: string | null) => void;
|
||||
removeCanvasGenerationDialogById?: (dialogId: string) => void;
|
||||
requestRemoveCanvasGenerationDialog?: (
|
||||
dialog: CanvasGenerationDialogState,
|
||||
) => void;
|
||||
setActiveTool: (tool: 'select') => void;
|
||||
setGenerateDialog: (
|
||||
updater:
|
||||
@@ -68,7 +70,9 @@ function isCanvasGenerationPlaceholderDialog(
|
||||
dialog?.mode === 'character' ||
|
||||
dialog?.mode === 'icon' ||
|
||||
dialog?.mode === 'ui-design' ||
|
||||
dialog?.mode === 'video')
|
||||
dialog?.mode === 'video' ||
|
||||
dialog?.mode === 'audio-sound-effect' ||
|
||||
dialog?.mode === 'audio-background-music')
|
||||
);
|
||||
}
|
||||
|
||||
@@ -84,7 +88,7 @@ export function useImageCanvasKeyboardShortcuts({
|
||||
redoCanvasChange,
|
||||
undoCanvasChange,
|
||||
deleteLayerById,
|
||||
removeCanvasGenerationDialogById,
|
||||
requestRemoveCanvasGenerationDialog,
|
||||
setActiveTool,
|
||||
setGenerateDialog,
|
||||
setImageContextMenu,
|
||||
@@ -182,9 +186,9 @@ export function useImageCanvasKeyboardShortcuts({
|
||||
const currentDialog = generateDialogRef.current;
|
||||
if (
|
||||
isCanvasGenerationDialogState(currentDialog) &&
|
||||
removeCanvasGenerationDialogById
|
||||
requestRemoveCanvasGenerationDialog
|
||||
) {
|
||||
removeCanvasGenerationDialogById(currentDialog.id);
|
||||
requestRemoveCanvasGenerationDialog(currentDialog);
|
||||
} else {
|
||||
setGenerateDialog(null);
|
||||
}
|
||||
@@ -233,7 +237,7 @@ export function useImageCanvasKeyboardShortcuts({
|
||||
closeEditorChromePanels,
|
||||
deleteLayerById,
|
||||
generateDialogRef,
|
||||
removeCanvasGenerationDialogById,
|
||||
requestRemoveCanvasGenerationDialog,
|
||||
redoCanvasChange,
|
||||
selectedLayerIdRef,
|
||||
setActiveTool,
|
||||
|
||||
@@ -4139,6 +4139,17 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock {
|
||||
transform-origin: bottom right;
|
||||
}
|
||||
|
||||
.image-canvas-editor__generation-frame-delete {
|
||||
position: absolute;
|
||||
right: var(--image-canvas-editor-frame-action-offset, -0.75rem);
|
||||
top: var(--image-canvas-editor-frame-action-offset, -0.75rem);
|
||||
z-index: 5;
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
transform: scale(var(--image-canvas-editor-inverse-scale, 1));
|
||||
transform-origin: top right;
|
||||
}
|
||||
|
||||
.image-canvas-editor__generation-frame-icon {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
|
||||
Reference in New Issue
Block a user