fix: 保留跨搜索/折叠选择,隐藏选择参与删除时必须明确确认。

更新行为文档
补充测试
This commit is contained in:
2026-07-31 20:04:48 +08:00
parent 8526063cea
commit 2da3dc0544
8 changed files with 125 additions and 51 deletions
@@ -13,9 +13,11 @@ import type {
RefObject,
SetStateAction,
} from 'react';
import { useState } from 'react';
import { PlatformActionButton } from '../common/PlatformActionButton';
import { PlatformBatchActionToolbar } from '../common/PlatformBatchActionToolbar';
import { PlatformDangerConfirmDialog } from '../common/PlatformDangerConfirmDialog';
import { PlatformTextField } from '../common/PlatformTextField';
import { ImageCanvasAssetFolderSectionView } from './ImageCanvasAssetFolderSectionView';
import { EditorIconButton } from './ImageCanvasEditorPrimitives';
@@ -26,7 +28,10 @@ import type {
EditorAssetFolder,
UploadTarget,
} from './ImageCanvasEditorTypes';
import type { AssetSelectionInteraction } from './useImageCanvasAssetSelection';
import {
type AssetSelectionInteraction,
countHiddenSelectedAssets,
} from './useImageCanvasAssetSelection';
export type GroupedEditorAssetFolder = EditorAssetFolder & {
assets: EditorAsset[];
@@ -152,6 +157,10 @@ export function ImageCanvasAssetLibraryPanelView({
deleteSelectedAssets,
closeAssetSelectionMode,
}: ImageCanvasAssetLibraryPanelViewProps) {
const [
hiddenSelectionDeleteConfirmOpen,
setHiddenSelectionDeleteConfirmOpen,
] = useState(false);
let selectionToggleActionText = '全选';
if (allVisibleAssetsSelected) {
selectionToggleActionText = '取消全选';
@@ -160,6 +169,18 @@ export function ImageCanvasAssetLibraryPanelView({
if (selectedAssetIds.size > 0) {
selectionToggleLabel = `${selectionToggleActionText} · 已选 ${selectedAssetIds.size}`;
}
const hiddenSelectedAssetCount = countHiddenSelectedAssets(
selectedAssetIds,
visibleAssetIds,
);
const requestDeleteSelectedAssets = () => {
if (hiddenSelectedAssetCount > 0) {
setHiddenSelectionDeleteConfirmOpen(true);
return;
}
deleteSelectedAssets();
};
return (
<div className="image-canvas-editor__asset-library">
@@ -304,7 +325,7 @@ export function ImageCanvasAssetLibraryPanelView({
tone="warning"
size="sm"
disabled={selectedAssetIds.size === 0 || isExportingSelectedAssets}
onClick={deleteSelectedAssets}
onClick={requestDeleteSelectedAssets}
>
<Trash2 className="h-4 w-4" />
@@ -320,6 +341,19 @@ export function ImageCanvasAssetLibraryPanelView({
</PlatformActionButton>
</PlatformBatchActionToolbar>
) : null}
<PlatformDangerConfirmDialog
open={hiddenSelectionDeleteConfirmOpen}
title="删除全部已选素材"
description={`将删除 ${selectedAssetIds.size} 个已选素材,其中 ${hiddenSelectedAssetCount} 个当前未显示。`}
confirmLabel="确认删除全部已选素材"
onClose={() => setHiddenSelectionDeleteConfirmOpen(false)}
onConfirm={() => {
setHiddenSelectionDeleteConfirmOpen(false);
deleteSelectedAssets();
}}
>
</PlatformDangerConfirmDialog>
</div>
);
}
@@ -924,9 +924,7 @@ describe('ImageCanvasEditorView asset library integration', () => {
fireEvent.click(screen.getByText('账号素材A'));
expect(within(toolbar).getByText(/ 2/u)).toBeTruthy();
fireEvent.click(
within(toolbar).getByRole('button', { name: /^/u }),
);
fireEvent.click(within(toolbar).getByRole('button', { name: /^/u }));
expect(within(toolbar).getByText(/ 3/u)).toBeTruthy();
fireEvent.click(
within(toolbar).getByRole('button', { name: /^/u }),
@@ -946,6 +944,26 @@ describe('ImageCanvasEditorView asset library integration', () => {
await user.click(screen.getByRole('button', { name: '展开其它素材' }));
expect(foldedAssetList).toHaveProperty('hidden', false);
expect(within(toolbar).getByText(/ 2/u)).toBeTruthy();
await user.click(screen.getByRole('button', { name: '折叠其它素材' }));
await user.click(within(toolbar).getByRole('button', { name: '删除' }));
const hiddenDeleteDialog = screen.getByRole('dialog', {
name: '删除全部已选素材',
});
expect(
within(hiddenDeleteDialog).getByText(
'将删除 2 个已选素材,其中 1 个当前未显示。',
),
).toBeTruthy();
expect(deleteEditorAssetMock).not.toHaveBeenCalled();
await user.click(
within(hiddenDeleteDialog).getByRole('button', {
name: '确认删除全部已选素材',
}),
);
expect(deleteEditorAssetMock).toHaveBeenCalledWith('asset-b');
expect(deleteEditorAssetMock).toHaveBeenCalledWith('asset-c');
expect(deleteEditorAssetMock).not.toHaveBeenCalledWith('asset-a');
});
it('removes canvas layers linked to deleted assets', async () => {
@@ -1014,37 +1032,39 @@ describe('ImageCanvasEditorView asset library integration', () => {
it('saves the canvas layout after deleting an asset linked by source resource id', async () => {
const user = userEvent.setup();
loadOrCreateRecentEditorProjectMock.mockResolvedValueOnce(withEditorProjectCanvasRevision({
projectId: 'editor-project-default',
title: '生成资源项目',
viewport: { x: 0, y: 0, scale: 1 },
layers: [
{
layerId: 'layer-generated-a',
resourceId: 'resource-generated-a',
title: '生成资源A',
x: 100,
y: 120,
width: 320,
height: 240,
originalWidth: 320,
originalHeight: 240,
zIndex: 1,
sourceType: 'generated',
},
],
resources: [
{
resourceId: 'resource-generated-a',
projectId: 'editor-project-default',
imageSrc: '/editor-test/project/generated-a.png',
width: 320,
height: 240,
sourceType: 'generated',
},
],
updatedAt: '2026-06-12T00:00:00.000Z',
}));
loadOrCreateRecentEditorProjectMock.mockResolvedValueOnce(
withEditorProjectCanvasRevision({
projectId: 'editor-project-default',
title: '生成资源项目',
viewport: { x: 0, y: 0, scale: 1 },
layers: [
{
layerId: 'layer-generated-a',
resourceId: 'resource-generated-a',
title: '生成资源A',
x: 100,
y: 120,
width: 320,
height: 240,
originalWidth: 320,
originalHeight: 240,
zIndex: 1,
sourceType: 'generated',
},
],
resources: [
{
resourceId: 'resource-generated-a',
projectId: 'editor-project-default',
imageSrc: '/editor-test/project/generated-a.png',
width: 320,
height: 240,
sourceType: 'generated',
},
],
updatedAt: '2026-06-12T00:00:00.000Z',
}),
);
loadEditorAssetLibraryMock.mockResolvedValueOnce({
folders: [
{
@@ -1118,14 +1138,16 @@ describe('ImageCanvasEditorView asset library integration', () => {
height: 240,
sourceType: 'uploaded',
});
loadOrCreateRecentEditorProjectMock.mockResolvedValueOnce(withEditorProjectCanvasRevision({
projectId: 'editor-project-default',
title: '空画布项目',
viewport: { x: 0, y: 0, scale: 1 },
layers: [],
resources: [],
updatedAt: '2026-06-12T00:00:00.000Z',
}));
loadOrCreateRecentEditorProjectMock.mockResolvedValueOnce(
withEditorProjectCanvasRevision({
projectId: 'editor-project-default',
title: '空画布项目',
viewport: { x: 0, y: 0, scale: 1 },
layers: [],
resources: [],
updatedAt: '2026-06-12T00:00:00.000Z',
}),
);
loadEditorAssetLibraryMock.mockResolvedValueOnce({
folders: [
{
@@ -6,6 +6,7 @@ import { describe, expect, it, vi } from 'vitest';
import type { EditorAsset } from './ImageCanvasEditorTypes';
import {
countHiddenSelectedAssets,
createAssetMarqueeFromPointer,
createAssetMarqueeSelectionRect,
getSelectableAssets,
@@ -30,6 +31,14 @@ function createAsset(overrides: Partial<EditorAsset> = {}): EditorAsset {
}
describe('useImageCanvasAssetSelection', () => {
it('counts selected assets outside the current visible scope', () => {
expect(
countHiddenSelectedAssets(new Set(['asset-a', 'asset-b', 'asset-c']), [
'asset-a',
]),
).toBe(2);
});
it('owns selectable-asset validity', () => {
expect(
getSelectableAssets([
@@ -44,6 +44,15 @@ export function getSelectableAssets(assets: EditorAsset[]) {
);
}
export function countHiddenSelectedAssets(
selectedAssetIds: Set<string>,
visibleAssetIds: string[],
) {
const visibleIds = new Set(visibleAssetIds);
return [...selectedAssetIds].filter((assetId) => !visibleIds.has(assetId))
.length;
}
function doRectsIntersect(rect: ClientRectLike, selectionRect: ClientRectLike) {
return (
rect.left <= selectionRect.right &&