原子维护素材选择与锚点
将选中素材集合和范围锚点合并到同一个状态 让单击、全选、可见协调、框选和模式切换返回纯状态转移 新增可见范围协调后 Shift 选择的回归测试
This commit is contained in:
@@ -215,6 +215,14 @@ function AssetLibraryHarness({
|
||||
>
|
||||
toggle all
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
assetLibrary.reconcileSelectedAssets(['asset-b', 'asset-c'])
|
||||
}
|
||||
>
|
||||
reconcile to b c
|
||||
</button>
|
||||
<button type="button" onClick={assetLibrary.deleteSelectedAssets}>
|
||||
delete selected
|
||||
</button>
|
||||
@@ -518,6 +526,34 @@ describe('useImageCanvasAssetLibrary', () => {
|
||||
expect(screen.getByTestId('selected-assets').textContent).toBe('asset-c');
|
||||
});
|
||||
|
||||
it('reconciles selected ids and their range anchor atomically', async () => {
|
||||
loadEditorAssetLibraryMock.mockResolvedValueOnce({
|
||||
folders: [
|
||||
{
|
||||
folderId: 'project',
|
||||
label: '项目素材',
|
||||
sortOrder: 0,
|
||||
collapsed: false,
|
||||
systemDefault: true,
|
||||
},
|
||||
],
|
||||
assets: [
|
||||
createAssetSnapshot({ assetId: 'asset-a', label: '素材A' }),
|
||||
createAssetSnapshot({ assetId: 'asset-b', label: '素材B' }),
|
||||
createAssetSnapshot({ assetId: 'asset-c', label: '素材C' }),
|
||||
],
|
||||
});
|
||||
render(<AssetLibraryHarness />);
|
||||
|
||||
fireEvent.click(await screen.findByRole('button', { name: '素材A' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: 'reconcile to b c' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '素材C' }), {
|
||||
shiftKey: true,
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('selected-assets').textContent).toBe('asset-c');
|
||||
});
|
||||
|
||||
it('submits a generated asset to showcase review', async () => {
|
||||
loadEditorAssetLibraryMock.mockResolvedValueOnce({
|
||||
folders: [
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
} from '../../services/image-editor/editorProjectClient';
|
||||
import {
|
||||
type AssetSelectionInteraction,
|
||||
type AssetSelectionState,
|
||||
createAssetMarqueeFromPointer,
|
||||
createAssetMarqueeSelectionRect,
|
||||
createLocalAssetFolder,
|
||||
@@ -76,6 +77,13 @@ function formatShowcaseSubmitError(error: unknown) {
|
||||
return '提交精选审核失败,请稍后重试';
|
||||
}
|
||||
|
||||
function createEmptyAssetSelection(): AssetSelectionState {
|
||||
return {
|
||||
selectedAssetIds: new Set(),
|
||||
anchorAssetId: null,
|
||||
};
|
||||
}
|
||||
|
||||
const ASSET_FOLDER_SELECTOR = '[data-asset-folder-id]';
|
||||
const ASSET_ITEM_SELECTOR = '[data-asset-id]';
|
||||
const ASSET_MARQUEE_BLOCKED_TARGET_SELECTOR =
|
||||
@@ -140,10 +148,10 @@ export function useImageCanvasAssetLibrary({
|
||||
const [newFolderName, setNewFolderName] = useState('');
|
||||
const [activeUploadFolderId, setActiveUploadFolderId] = useState('project');
|
||||
const [isAssetSelectionMode, setIsAssetSelectionMode] = useState(false);
|
||||
const [selectedAssetIds, setSelectedAssetIds] = useState<Set<string>>(
|
||||
() => new Set(),
|
||||
const [assetSelection, setAssetSelection] = useState<AssetSelectionState>(
|
||||
createEmptyAssetSelection,
|
||||
);
|
||||
const assetSelectionAnchorRef = useRef<string | null>(null);
|
||||
const { selectedAssetIds } = assetSelection;
|
||||
const marqueeBaseSelectionRef = useRef<Set<string>>(new Set());
|
||||
const [assetMarquee, setAssetMarquee] = useState<AssetMarqueeState | null>(
|
||||
null,
|
||||
@@ -485,40 +493,34 @@ export function useImageCanvasAssetLibrary({
|
||||
|
||||
const toggleAssetSelected = useCallback(
|
||||
(assetId: string, interaction: AssetSelectionInteraction) => {
|
||||
setSelectedAssetIds((currentIds) => {
|
||||
const nextSelection = resolveAssetSelection({
|
||||
selectedAssetIds: currentIds,
|
||||
anchorAssetId: assetSelectionAnchorRef.current,
|
||||
setAssetSelection((currentSelection) =>
|
||||
resolveAssetSelection({
|
||||
...currentSelection,
|
||||
targetAssetId: assetId,
|
||||
...interaction,
|
||||
});
|
||||
assetSelectionAnchorRef.current = nextSelection.anchorAssetId;
|
||||
return nextSelection.selectedAssetIds;
|
||||
});
|
||||
}),
|
||||
);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const toggleAllAssetsSelected = useCallback((visibleAssetIds: string[]) => {
|
||||
assetSelectionAnchorRef.current = null;
|
||||
setSelectedAssetIds((currentIds) =>
|
||||
resolveVisibleAssetSelectionToggle({
|
||||
selectedAssetIds: currentIds,
|
||||
setAssetSelection((currentSelection) => ({
|
||||
selectedAssetIds: resolveVisibleAssetSelectionToggle({
|
||||
selectedAssetIds: currentSelection.selectedAssetIds,
|
||||
visibleAssetIds,
|
||||
}),
|
||||
);
|
||||
anchorAssetId: null,
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const reconcileSelectedAssets = useCallback((visibleAssetIds: string[]) => {
|
||||
setSelectedAssetIds((currentIds) => {
|
||||
const nextSelection = reconcileAssetSelection({
|
||||
selectedAssetIds: currentIds,
|
||||
anchorAssetId: assetSelectionAnchorRef.current,
|
||||
setAssetSelection((currentSelection) =>
|
||||
reconcileAssetSelection({
|
||||
...currentSelection,
|
||||
visibleAssetIds,
|
||||
});
|
||||
assetSelectionAnchorRef.current = nextSelection.anchorAssetId;
|
||||
return nextSelection.selectedAssetIds;
|
||||
});
|
||||
}),
|
||||
);
|
||||
}, []);
|
||||
|
||||
const deleteSelectedAssets = useCallback(() => {
|
||||
@@ -532,8 +534,7 @@ export function useImageCanvasAssetLibrary({
|
||||
removeSelectedAssets(currentAssets, selectedAssetIds).assets,
|
||||
);
|
||||
onDeleteAssets?.(deletedAssets);
|
||||
setSelectedAssetIds(new Set());
|
||||
assetSelectionAnchorRef.current = null;
|
||||
setAssetSelection(createEmptyAssetSelection());
|
||||
ids.forEach((assetId) => {
|
||||
void deleteEditorAsset(assetId);
|
||||
});
|
||||
@@ -557,18 +558,16 @@ export function useImageCanvasAssetLibrary({
|
||||
|
||||
const closeAssetSelectionMode = useCallback(() => {
|
||||
setIsAssetSelectionMode(false);
|
||||
setSelectedAssetIds(new Set());
|
||||
assetSelectionAnchorRef.current = null;
|
||||
setAssetSelection(createEmptyAssetSelection());
|
||||
marqueeBaseSelectionRef.current = new Set();
|
||||
setAssetMarquee(null);
|
||||
}, []);
|
||||
|
||||
const openAssetSelectionMode = useCallback(() => {
|
||||
assetSelectionAnchorRef.current = null;
|
||||
marqueeBaseSelectionRef.current = new Set();
|
||||
setRenamingAsset(null);
|
||||
setRenamingFolder(null);
|
||||
setSelectedAssetIds(new Set());
|
||||
setAssetSelection(createEmptyAssetSelection());
|
||||
setAssetMarquee(null);
|
||||
setIsAssetSelectionMode(true);
|
||||
}, []);
|
||||
@@ -585,9 +584,13 @@ export function useImageCanvasAssetLibrary({
|
||||
assetTargets: readAssetHitTargets(assetListRef.current),
|
||||
selectionRect,
|
||||
});
|
||||
setSelectedAssetIds(
|
||||
new Set([...marqueeBaseSelectionRef.current, ...marqueeSelection]),
|
||||
);
|
||||
setAssetSelection({
|
||||
selectedAssetIds: new Set([
|
||||
...marqueeBaseSelectionRef.current,
|
||||
...marqueeSelection,
|
||||
]),
|
||||
anchorAssetId: null,
|
||||
});
|
||||
},
|
||||
[assetListRef, assets],
|
||||
);
|
||||
@@ -617,8 +620,10 @@ export function useImageCanvasAssetLibrary({
|
||||
);
|
||||
marqueeBaseSelectionRef.current =
|
||||
event.ctrlKey || event.metaKey ? new Set(selectedAssetIds) : new Set();
|
||||
assetSelectionAnchorRef.current = null;
|
||||
setSelectedAssetIds(new Set(marqueeBaseSelectionRef.current));
|
||||
setAssetSelection({
|
||||
selectedAssetIds: new Set(marqueeBaseSelectionRef.current),
|
||||
anchorAssetId: null,
|
||||
});
|
||||
},
|
||||
[assetListRef, isAssetSelectionMode, selectedAssetIds],
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user