修复素材选择模式交互边界
阻断选择模式下的素材移动与外部文件投放并清理拖拽状态 固定隐藏素材删除确认目标并在选择范围漂移时关闭弹窗 收敛选择交互契约与素材行标签分支 修正短视口侧栏高度并补充定向回归测试和文档
This commit is contained in:
@@ -117,6 +117,17 @@ export function ImageCanvasAssetFolderSectionView({
|
||||
aria-label={folder.label}
|
||||
data-asset-folder-id={folder.id}
|
||||
onDragOver={(event) => {
|
||||
if (isAssetSelectionMode) {
|
||||
if (
|
||||
hasDataTransferType(event.dataTransfer, ASSET_DRAG_MIME_TYPE) ||
|
||||
hasDataTransferType(event.dataTransfer, 'Files')
|
||||
) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
event.dataTransfer.dropEffect = 'none';
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (hasDataTransferType(event.dataTransfer, ASSET_DRAG_MIME_TYPE)) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
@@ -133,6 +144,18 @@ export function ImageCanvasAssetFolderSectionView({
|
||||
}
|
||||
}}
|
||||
onDrop={(event) => {
|
||||
if (isAssetSelectionMode) {
|
||||
if (
|
||||
getDraggedAssetId(event.dataTransfer) ||
|
||||
event.dataTransfer.files.length > 0
|
||||
) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setUploadDropTarget(null);
|
||||
updateAssetMoveDropFolder(null);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const movingAssetId = getDraggedAssetId(event.dataTransfer);
|
||||
if (movingAssetId) {
|
||||
event.preventDefault();
|
||||
|
||||
@@ -13,7 +13,7 @@ import type {
|
||||
RefObject,
|
||||
SetStateAction,
|
||||
} from 'react';
|
||||
import { useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { PlatformActionButton } from '../common/PlatformActionButton';
|
||||
import { PlatformBatchActionToolbar } from '../common/PlatformBatchActionToolbar';
|
||||
@@ -110,7 +110,7 @@ export type ImageCanvasAssetLibraryPanelViewProps = {
|
||||
onDownloadAsset: (asset: EditorAsset) => void;
|
||||
toggleAllAssetsSelected: (visibleAssetIds: string[]) => void;
|
||||
downloadSelectedAssets: () => void | Promise<void>;
|
||||
deleteSelectedAssets: () => void;
|
||||
deleteSelectedAssets: (assetIds?: ReadonlySet<string>) => void;
|
||||
closeAssetSelectionMode: () => void;
|
||||
};
|
||||
|
||||
@@ -165,10 +165,12 @@ export function ImageCanvasAssetLibraryPanelView({
|
||||
deleteSelectedAssets,
|
||||
closeAssetSelectionMode,
|
||||
}: ImageCanvasAssetLibraryPanelViewProps) {
|
||||
const [
|
||||
hiddenSelectionDeleteConfirmOpen,
|
||||
setHiddenSelectionDeleteConfirmOpen,
|
||||
] = useState(false);
|
||||
const [hiddenSelectionDeleteSnapshot, setHiddenSelectionDeleteSnapshot] =
|
||||
useState<{
|
||||
assetIds: Set<string>;
|
||||
visibleAssetIds: string[];
|
||||
hiddenAssetCount: number;
|
||||
} | null>(null);
|
||||
let selectionToggleActionText = '全选';
|
||||
if (allVisibleAssetsSelected) {
|
||||
selectionToggleActionText = '取消全选';
|
||||
@@ -182,9 +184,42 @@ export function ImageCanvasAssetLibraryPanelView({
|
||||
visibleAssetIds,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!hiddenSelectionDeleteSnapshot) {
|
||||
return;
|
||||
}
|
||||
const selectedIdsUnchanged =
|
||||
hiddenSelectionDeleteSnapshot.assetIds.size === selectedAssetIds.size &&
|
||||
[...hiddenSelectionDeleteSnapshot.assetIds].every((assetId) =>
|
||||
selectedAssetIds.has(assetId),
|
||||
);
|
||||
const visibleIdsUnchanged =
|
||||
hiddenSelectionDeleteSnapshot.visibleAssetIds.length ===
|
||||
visibleAssetIds.length &&
|
||||
hiddenSelectionDeleteSnapshot.visibleAssetIds.every(
|
||||
(assetId, index) => assetId === visibleAssetIds[index],
|
||||
);
|
||||
if (
|
||||
!isAssetSelectionMode ||
|
||||
!selectedIdsUnchanged ||
|
||||
!visibleIdsUnchanged
|
||||
) {
|
||||
setHiddenSelectionDeleteSnapshot(null);
|
||||
}
|
||||
}, [
|
||||
hiddenSelectionDeleteSnapshot,
|
||||
isAssetSelectionMode,
|
||||
selectedAssetIds,
|
||||
visibleAssetIds,
|
||||
]);
|
||||
|
||||
const requestDeleteSelectedAssets = () => {
|
||||
if (hiddenSelectedAssetCount > 0) {
|
||||
setHiddenSelectionDeleteConfirmOpen(true);
|
||||
setHiddenSelectionDeleteSnapshot({
|
||||
assetIds: new Set(selectedAssetIds),
|
||||
visibleAssetIds: [...visibleAssetIds],
|
||||
hiddenAssetCount: hiddenSelectedAssetCount,
|
||||
});
|
||||
return;
|
||||
}
|
||||
deleteSelectedAssets();
|
||||
@@ -351,14 +386,17 @@ export function ImageCanvasAssetLibraryPanelView({
|
||||
</PlatformBatchActionToolbar>
|
||||
) : null}
|
||||
<PlatformDangerConfirmDialog
|
||||
open={hiddenSelectionDeleteConfirmOpen}
|
||||
open={isAssetSelectionMode && hiddenSelectionDeleteSnapshot !== null}
|
||||
title="删除全部已选素材"
|
||||
description={`将删除 ${selectedAssetIds.size} 个已选素材,其中 ${hiddenSelectedAssetCount} 个当前未显示。`}
|
||||
description={`将删除 ${hiddenSelectionDeleteSnapshot?.assetIds.size ?? 0} 个已选素材,其中 ${hiddenSelectionDeleteSnapshot?.hiddenAssetCount ?? 0} 个当前未显示。`}
|
||||
confirmLabel="确认删除全部已选素材"
|
||||
onClose={() => setHiddenSelectionDeleteConfirmOpen(false)}
|
||||
onClose={() => setHiddenSelectionDeleteSnapshot(null)}
|
||||
onConfirm={() => {
|
||||
setHiddenSelectionDeleteConfirmOpen(false);
|
||||
deleteSelectedAssets();
|
||||
const targetAssetIds = hiddenSelectionDeleteSnapshot?.assetIds;
|
||||
setHiddenSelectionDeleteSnapshot(null);
|
||||
if (targetAssetIds) {
|
||||
deleteSelectedAssets(targetAssetIds);
|
||||
}
|
||||
}}
|
||||
>
|
||||
删除后不可恢复。
|
||||
|
||||
@@ -220,9 +220,6 @@ describe('ImageCanvasAssetRowView', () => {
|
||||
expect(toggleAssetSelected).toHaveBeenCalledWith('asset-1', {
|
||||
visibleAssetIds: ['asset-1'],
|
||||
shiftKey: false,
|
||||
ctrlKey: false,
|
||||
metaKey: false,
|
||||
pointerKind: 'keyboard',
|
||||
});
|
||||
expect(addAssetLayer).not.toHaveBeenCalled();
|
||||
expect(
|
||||
@@ -246,7 +243,7 @@ describe('ImageCanvasAssetRowView', () => {
|
||||
expect(toggleAssetSelected).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('forwards modifier keys to selection', () => {
|
||||
it('forwards only the shift modifier used by selection semantics', () => {
|
||||
const toggleAssetSelected = vi.fn();
|
||||
renderAssetRow({ isAssetSelectionMode: true, toggleAssetSelected });
|
||||
fireEvent.click(screen.getByText('账号素材A'), {
|
||||
@@ -254,30 +251,42 @@ describe('ImageCanvasAssetRowView', () => {
|
||||
shiftKey: true,
|
||||
});
|
||||
|
||||
expect(toggleAssetSelected).toHaveBeenCalledWith(
|
||||
'asset-1',
|
||||
expect.objectContaining({
|
||||
ctrlKey: true,
|
||||
shiftKey: true,
|
||||
pointerKind: 'keyboard',
|
||||
}),
|
||||
);
|
||||
expect(toggleAssetSelected).toHaveBeenCalledWith('asset-1', {
|
||||
visibleAssetIds: ['asset-1'],
|
||||
shiftKey: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('uses click detail to recognize keyboard selection after a stale touch gesture', () => {
|
||||
const toggleAssetSelected = vi.fn();
|
||||
renderAssetRow({ isAssetSelectionMode: true, toggleAssetSelected });
|
||||
const primaryButton = screen.getByRole('button', {
|
||||
name: '选择素材账号素材A',
|
||||
it('blocks internal moves and external file drops in selection mode', () => {
|
||||
const { container, props } = renderAssetRow({
|
||||
isAssetSelectionMode: true,
|
||||
});
|
||||
|
||||
fireEvent.pointerDown(primaryButton, { pointerType: 'touch' });
|
||||
fireEvent.click(primaryButton, { detail: 0 });
|
||||
|
||||
expect(toggleAssetSelected).toHaveBeenCalledWith(
|
||||
'asset-1',
|
||||
expect.objectContaining({ pointerKind: 'keyboard' }),
|
||||
const row = container.querySelector<HTMLElement>(
|
||||
'[data-asset-id="asset-1"]',
|
||||
);
|
||||
if (!row) {
|
||||
throw new Error('asset row should exist');
|
||||
}
|
||||
const fileTransfer = {
|
||||
types: ['Files'],
|
||||
files: [new File(['image'], '新增素材.png', { type: 'image/png' })],
|
||||
dropEffect: 'copy',
|
||||
getData: vi.fn(() => ''),
|
||||
};
|
||||
fireEvent.dragOver(row, { dataTransfer: fileTransfer });
|
||||
fireEvent.drop(row, { dataTransfer: fileTransfer });
|
||||
|
||||
const assetTransfer = {
|
||||
types: ['application/x-genarrative-editor-asset'],
|
||||
files: [],
|
||||
dropEffect: 'move',
|
||||
getData: vi.fn(() => 'asset-other'),
|
||||
};
|
||||
fireEvent.dragOver(row, { dataTransfer: assetTransfer });
|
||||
fireEvent.drop(row, { dataTransfer: assetTransfer });
|
||||
|
||||
expect(props.addUploadedFiles).not.toHaveBeenCalled();
|
||||
expect(props.moveAssetToFolder).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('renders rename input and commits with Enter', () => {
|
||||
|
||||
@@ -90,8 +90,6 @@ export function ImageCanvasAssetRowView({
|
||||
onDownloadAsset,
|
||||
}: ImageCanvasAssetRowViewProps) {
|
||||
const rowRef = useRef<HTMLDivElement | null>(null);
|
||||
const selectionPointerKindRef =
|
||||
useRef<AssetSelectionInteraction['pointerKind']>('keyboard');
|
||||
const [menuPosition, setMenuPosition] = useState<{
|
||||
x: number;
|
||||
y: number;
|
||||
@@ -118,9 +116,6 @@ export function ImageCanvasAssetRowView({
|
||||
|
||||
const selectAssetFromEvent = useCallback(
|
||||
(event: ReactMouseEvent<HTMLElement>) => {
|
||||
const pointerKind =
|
||||
event.detail === 0 ? 'keyboard' : selectionPointerKindRef.current;
|
||||
selectionPointerKindRef.current = 'keyboard';
|
||||
if (
|
||||
!isAssetSelectionMode ||
|
||||
!canSelectAsset ||
|
||||
@@ -132,9 +127,6 @@ export function ImageCanvasAssetRowView({
|
||||
toggleAssetSelected(asset.id, {
|
||||
visibleAssetIds,
|
||||
shiftKey: event.shiftKey,
|
||||
ctrlKey: event.ctrlKey,
|
||||
metaKey: event.metaKey,
|
||||
pointerKind,
|
||||
});
|
||||
},
|
||||
[
|
||||
@@ -211,6 +203,16 @@ export function ImageCanvasAssetRowView({
|
||||
<Video className="h-5 w-5" />
|
||||
</div>
|
||||
) : undefined;
|
||||
let primaryLabel = `添加${asset.label}`;
|
||||
if (isUploadingAsset) {
|
||||
primaryLabel = `上传中${asset.label}`;
|
||||
} else if (isFailedUpload) {
|
||||
primaryLabel = `上传失败${asset.label}`;
|
||||
} else if (isAssetSelectionMode) {
|
||||
primaryLabel = canSelectAsset
|
||||
? `选择素材${asset.label}`
|
||||
: `素材${asset.label}不可选择`;
|
||||
}
|
||||
const titleNode = isRenaming ? (
|
||||
<PlatformTextField
|
||||
aria-label={`重命名素材${asset.label}`}
|
||||
@@ -312,17 +314,7 @@ export function ImageCanvasAssetRowView({
|
||||
title={asset.label}
|
||||
detail={mediaDetail}
|
||||
imageAlt={`素材:${asset.label}`}
|
||||
primaryLabel={
|
||||
isUploadingAsset
|
||||
? `上传中${asset.label}`
|
||||
: isFailedUpload
|
||||
? `上传失败${asset.label}`
|
||||
: isAssetSelectionMode
|
||||
? canSelectAsset
|
||||
? `选择素材${asset.label}`
|
||||
: `素材${asset.label}不可选择`
|
||||
: `添加${asset.label}`
|
||||
}
|
||||
primaryLabel={primaryLabel}
|
||||
primaryDisabled={isAssetSelectionMode && !canSelectAsset}
|
||||
onPrimaryClick={() => {
|
||||
if (isUploadingAsset || isFailedUpload) {
|
||||
@@ -400,7 +392,12 @@ export function ImageCanvasAssetRowView({
|
||||
) : undefined
|
||||
}
|
||||
onDragStart={(event) => {
|
||||
if (isRenaming || isUploadingAsset || isFailedUpload) {
|
||||
if (
|
||||
isAssetSelectionMode ||
|
||||
isRenaming ||
|
||||
isUploadingAsset ||
|
||||
isFailedUpload
|
||||
) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
@@ -420,10 +417,6 @@ export function ImageCanvasAssetRowView({
|
||||
}}
|
||||
onPointerDown={(event) => {
|
||||
if (isAssetSelectionMode) {
|
||||
selectionPointerKindRef.current =
|
||||
event.pointerType === 'touch' || event.pointerType === 'pen'
|
||||
? event.pointerType
|
||||
: 'mouse';
|
||||
return;
|
||||
}
|
||||
if (event.button !== 0) {
|
||||
@@ -467,6 +460,17 @@ export function ImageCanvasAssetRowView({
|
||||
setAssetPointerDrag(nextDrag);
|
||||
}}
|
||||
onDragOver={(event) => {
|
||||
if (isAssetSelectionMode) {
|
||||
if (
|
||||
hasDataTransferType(event.dataTransfer, ASSET_DRAG_MIME_TYPE) ||
|
||||
hasDataTransferType(event.dataTransfer, 'Files')
|
||||
) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
event.dataTransfer.dropEffect = 'none';
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (hasDataTransferType(event.dataTransfer, ASSET_DRAG_MIME_TYPE)) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
@@ -483,6 +487,18 @@ export function ImageCanvasAssetRowView({
|
||||
}
|
||||
}}
|
||||
onDrop={(event) => {
|
||||
if (isAssetSelectionMode) {
|
||||
if (
|
||||
getDraggedAssetId(event.dataTransfer) ||
|
||||
event.dataTransfer.files.length > 0
|
||||
) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setUploadDropTarget(null);
|
||||
updateAssetMoveDropFolder(null);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const movingAssetId = getDraggedAssetId(event.dataTransfer);
|
||||
if (movingAssetId) {
|
||||
event.preventDefault();
|
||||
|
||||
@@ -592,6 +592,61 @@ describe('ImageCanvasEditorView asset library integration', () => {
|
||||
expect(createEditorAssetMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('clears drag targets and blocks folder drops after entering asset selection mode', async () => {
|
||||
loadEditorAssetLibraryMock.mockResolvedValueOnce({
|
||||
folders: [
|
||||
{
|
||||
folderId: 'project',
|
||||
label: '项目素材',
|
||||
sortOrder: 0,
|
||||
collapsed: false,
|
||||
systemDefault: true,
|
||||
},
|
||||
{
|
||||
folderId: 'folder-role',
|
||||
label: '角色',
|
||||
sortOrder: 100,
|
||||
collapsed: false,
|
||||
systemDefault: false,
|
||||
},
|
||||
],
|
||||
assets: [
|
||||
createTestImageAsset({ assetId: 'asset-puzzle', label: '拼图素材' }),
|
||||
],
|
||||
});
|
||||
render(<ImageCanvasEditorView />);
|
||||
|
||||
const roleFolder = await screen.findByRole('region', { name: '角色' });
|
||||
const internalTransfer = createDataTransferStub();
|
||||
internalTransfer.setData(
|
||||
'application/x-genarrative-editor-asset',
|
||||
'asset-puzzle',
|
||||
);
|
||||
fireEvent.dragOver(roleFolder, { dataTransfer: internalTransfer });
|
||||
expect(roleFolder.className).toContain(
|
||||
'image-canvas-editor__asset-folder--move-target',
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '素材选择模式' }));
|
||||
await waitFor(() => {
|
||||
expect(roleFolder.className).not.toContain(
|
||||
'image-canvas-editor__asset-folder--move-target',
|
||||
);
|
||||
});
|
||||
fireEvent.drop(roleFolder, { dataTransfer: internalTransfer });
|
||||
|
||||
const fileTransfer = createDataTransferStub();
|
||||
fileTransfer.types.push('Files');
|
||||
(fileTransfer.files as File[]).push(
|
||||
new File(['image'], '新增素材.png', { type: 'image/png' }),
|
||||
);
|
||||
fireEvent.dragOver(roleFolder, { dataTransfer: fileTransfer });
|
||||
fireEvent.drop(roleFolder, { dataTransfer: fileTransfer });
|
||||
|
||||
expect(updateEditorAssetMock).not.toHaveBeenCalled();
|
||||
expect(createEditorAssetMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('uploads multiple files as account-level assets without adding canvas layers', async () => {
|
||||
render(<ImageCanvasEditorView />);
|
||||
|
||||
@@ -1039,8 +1094,24 @@ describe('ImageCanvasEditorView asset library integration', () => {
|
||||
),
|
||||
).toBeTruthy();
|
||||
expect(deleteEditorAssetMock).not.toHaveBeenCalled();
|
||||
fireEvent.change(screen.getByLabelText('搜索素材'), {
|
||||
target: { value: '账号素材B' },
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.queryByRole('dialog', { name: '删除全部已选素材' }),
|
||||
).toBeNull();
|
||||
});
|
||||
expect(deleteEditorAssetMock).not.toHaveBeenCalled();
|
||||
fireEvent.change(screen.getByLabelText('搜索素材'), {
|
||||
target: { value: '' },
|
||||
});
|
||||
await user.click(within(toolbar).getByRole('button', { name: '删除' }));
|
||||
const refreshedHiddenDeleteDialog = screen.getByRole('dialog', {
|
||||
name: '删除全部已选素材',
|
||||
});
|
||||
await user.click(
|
||||
within(hiddenDeleteDialog).getByRole('button', {
|
||||
within(refreshedHiddenDeleteDialog).getByRole('button', {
|
||||
name: '确认删除全部已选素材',
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -748,9 +748,11 @@ export function ImageCanvasEditorView({
|
||||
refreshAssetLibrary,
|
||||
} = useImageCanvasAssetLibrary({
|
||||
assetListRef,
|
||||
assetPointerDragRef,
|
||||
canAccessProtectedData: authUi ? authUi.canAccessProtectedData : true,
|
||||
openEditorLoginModal,
|
||||
onDeleteAssets: removeCanvasLayersLinkedToAssets,
|
||||
onOpenSelectionMode: () => setUploadDropTarget(null),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -106,7 +106,7 @@ export type ImageCanvasSidebarViewProps = {
|
||||
onDownloadAsset: (asset: EditorAsset) => void;
|
||||
toggleAllAssetsSelected: (visibleAssetIds: string[]) => void;
|
||||
downloadSelectedAssets: () => void | Promise<void>;
|
||||
deleteSelectedAssets: () => void;
|
||||
deleteSelectedAssets: (assetIds?: ReadonlySet<string>) => void;
|
||||
openAssetSelectionMode: () => void;
|
||||
closeAssetSelectionMode: () => void;
|
||||
groupSelectedLayers: () => void;
|
||||
|
||||
@@ -12,7 +12,10 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { ApiClientError } from '../../services/apiClient';
|
||||
import type { EditorAssetSnapshot } from '../../services/image-editor/editorProjectClient';
|
||||
import type { EditorAsset } from './ImageCanvasEditorTypes';
|
||||
import type {
|
||||
AssetPointerDragState,
|
||||
EditorAsset,
|
||||
} from './ImageCanvasEditorTypes';
|
||||
import { useImageCanvasAssetLibrary } from './useImageCanvasAssetLibrary';
|
||||
|
||||
const createEditorAssetFolderMock = vi.hoisted(() => vi.fn());
|
||||
@@ -85,8 +88,10 @@ function AssetLibraryHarness({
|
||||
onDeleteAssets?: (assets: EditorAsset[]) => boolean | void;
|
||||
}) {
|
||||
const assetListRef = useRef<HTMLDivElement | null>(null);
|
||||
const assetPointerDragRef = useRef<AssetPointerDragState | null>(null);
|
||||
const assetLibrary = useImageCanvasAssetLibrary({
|
||||
assetListRef,
|
||||
assetPointerDragRef,
|
||||
canAccessProtectedData,
|
||||
openEditorLoginModal,
|
||||
onDeleteAssets,
|
||||
@@ -108,10 +113,7 @@ function AssetLibraryHarness({
|
||||
visibleAssetIds: assetLibrary.selectableAssets.map(
|
||||
(item) => item.id,
|
||||
),
|
||||
ctrlKey: event.ctrlKey,
|
||||
metaKey: event.metaKey,
|
||||
shiftKey: event.shiftKey,
|
||||
pointerKind: 'mouse',
|
||||
})
|
||||
}
|
||||
>
|
||||
@@ -216,7 +218,7 @@ function AssetLibraryHarness({
|
||||
>
|
||||
toggle all
|
||||
</button>
|
||||
<button type="button" onClick={assetLibrary.deleteSelectedAssets}>
|
||||
<button type="button" onClick={() => assetLibrary.deleteSelectedAssets()}>
|
||||
delete selected
|
||||
</button>
|
||||
<button type="button" onClick={assetLibrary.openAssetSelectionMode}>
|
||||
|
||||
@@ -85,14 +85,18 @@ function readAssetFolderHitTargets(listElement: ParentNode | null) {
|
||||
|
||||
export function useImageCanvasAssetLibrary({
|
||||
assetListRef,
|
||||
assetPointerDragRef,
|
||||
canAccessProtectedData,
|
||||
openEditorLoginModal,
|
||||
onDeleteAssets,
|
||||
onOpenSelectionMode,
|
||||
}: {
|
||||
assetListRef: RefObject<HTMLDivElement | null>;
|
||||
assetPointerDragRef: RefObject<AssetPointerDragState | null>;
|
||||
canAccessProtectedData: boolean;
|
||||
openEditorLoginModal: (postLoginAction?: (() => void) | null) => void;
|
||||
onDeleteAssets?: (assets: EditorAsset[]) => boolean | void;
|
||||
onOpenSelectionMode?: () => void;
|
||||
}) {
|
||||
const [assetFolders, setAssetFolders] =
|
||||
useState<EditorAssetFolder[]>(EDITOR_ASSET_FOLDERS);
|
||||
@@ -125,6 +129,14 @@ export function useImageCanvasAssetLibrary({
|
||||
setRenamingAsset(null);
|
||||
setRenamingFolder(null);
|
||||
}, []);
|
||||
const prepareAssetSelectionMode = useCallback(() => {
|
||||
clearAssetRenameState();
|
||||
assetPointerDragRef.current = null;
|
||||
setAssetPointerDrag(null);
|
||||
setAssetMoveDropFolderId(null);
|
||||
setPinnedAssetMoveFolderId(null);
|
||||
onOpenSelectionMode?.();
|
||||
}, [assetPointerDragRef, clearAssetRenameState, onOpenSelectionMode]);
|
||||
const {
|
||||
selectableAssets,
|
||||
selectedAssets,
|
||||
@@ -144,7 +156,7 @@ export function useImageCanvasAssetLibrary({
|
||||
} = useImageCanvasAssetSelection({
|
||||
assetListRef,
|
||||
assets,
|
||||
onOpenSelectionMode: clearAssetRenameState,
|
||||
onOpenSelectionMode: prepareAssetSelectionMode,
|
||||
});
|
||||
|
||||
const applyAssetLibrarySnapshot = useCallback(
|
||||
@@ -469,20 +481,27 @@ export function useImageCanvasAssetLibrary({
|
||||
[assetFolders],
|
||||
);
|
||||
|
||||
const deleteSelectedAssets = useCallback(() => {
|
||||
const ids = selectedAssets.map((asset) => asset.id);
|
||||
if (onDeleteAssets?.(selectedAssets) === false) {
|
||||
return;
|
||||
}
|
||||
setAssets(
|
||||
(currentAssets) =>
|
||||
removeSelectedAssets(currentAssets, selectedAssetIds).assets,
|
||||
);
|
||||
clearAssetSelection();
|
||||
ids.forEach((assetId) => {
|
||||
void deleteEditorAsset(assetId);
|
||||
});
|
||||
}, [clearAssetSelection, onDeleteAssets, selectedAssetIds, selectedAssets]);
|
||||
const deleteSelectedAssets = useCallback(
|
||||
(requestedAssetIds?: ReadonlySet<string>) => {
|
||||
const targetAssetIds = new Set(requestedAssetIds ?? selectedAssetIds);
|
||||
const targetAssets = assets.filter((asset) =>
|
||||
targetAssetIds.has(asset.id),
|
||||
);
|
||||
const ids = targetAssets.map((asset) => asset.id);
|
||||
if (onDeleteAssets?.(targetAssets) === false) {
|
||||
return;
|
||||
}
|
||||
setAssets(
|
||||
(currentAssets) =>
|
||||
removeSelectedAssets(currentAssets, targetAssetIds).assets,
|
||||
);
|
||||
clearAssetSelection();
|
||||
ids.forEach((assetId) => {
|
||||
void deleteEditorAsset(assetId);
|
||||
});
|
||||
},
|
||||
[assets, clearAssetSelection, onDeleteAssets, selectedAssetIds],
|
||||
);
|
||||
|
||||
const moveAssetToFolder = useCallback(
|
||||
(assetId: string, folderId: string) => {
|
||||
|
||||
@@ -22,14 +22,9 @@ type AssetHitTarget = {
|
||||
rect: ClientRectLike;
|
||||
};
|
||||
|
||||
export type AssetSelectionPointerKind = 'keyboard' | 'mouse' | 'pen' | 'touch';
|
||||
|
||||
export type AssetSelectionInteraction = {
|
||||
visibleAssetIds: string[];
|
||||
shiftKey?: boolean;
|
||||
ctrlKey?: boolean;
|
||||
metaKey?: boolean;
|
||||
pointerKind?: AssetSelectionPointerKind;
|
||||
};
|
||||
|
||||
type AssetSelectionState = {
|
||||
|
||||
Reference in New Issue
Block a user