资源画布新增组织操作撤销重做(WP4)
新增 features/resource-canvas/resourceCanvasHistoryModel:纯函数有界快照栈(capture/push/undo/redo/clear + 只回写差异位置),快照只含资源卡布局坐标,不回滚素材内容 新增 tests/resourceCanvasHistoryModel.test.ts 覆盖排序稳定、相同快照不入栈、栈上限、push 清空重做分支、撤销重做互逆、空栈返回 null、差异回写与已删除资源不重建 资源卡拖动持久化成功前先落快照,撤销/重做复用现役 commitPosition 手动 CAS 写链 画布缩放集群新增撤销/重做按钮(不可用时禁用),并支持 Ctrl/Cmd+Z 与 Ctrl/Cmd+Shift+Z、Ctrl+Y(输入框内不拦截) 项目切换时清空布局历史,避免跨项目回滚
This commit is contained in:
+208
@@ -0,0 +1,208 @@
|
||||
/**
|
||||
* 资源画布的组织操作历史:只记录资源卡的布局坐标,**不记录、也不回滚素材内容**。
|
||||
*
|
||||
* 素材不可变是硬约束,所以撤销/重做只作用于「画布上卡片怎么排」,绝不反向修改
|
||||
* manifest 条目或素材文件。持久化仍走现役的手动 CAS 写链(`commitPosition`),
|
||||
* 本模块只做纯函数的快照栈。
|
||||
*/
|
||||
|
||||
import type { ProjectResourceCanvasSection } from '../../../../../packages/shared/src/contracts/gameCreationApp';
|
||||
|
||||
export const MAX_RESOURCE_CANVAS_HISTORY_STEPS = 40;
|
||||
|
||||
export type ResourceCanvasLayoutSnapshotEntry = {
|
||||
resourceId: string;
|
||||
section: ProjectResourceCanvasSection;
|
||||
x: number;
|
||||
y: number;
|
||||
manuallyPlaced: boolean;
|
||||
};
|
||||
|
||||
export type ResourceCanvasLayoutSnapshot = {
|
||||
entries: ResourceCanvasLayoutSnapshotEntry[];
|
||||
};
|
||||
|
||||
export type ResourceCanvasHistoryEntry = {
|
||||
id: number;
|
||||
label: string;
|
||||
snapshot: ResourceCanvasLayoutSnapshot;
|
||||
createdAt: number;
|
||||
};
|
||||
|
||||
export type ResourceCanvasHistory = {
|
||||
undoStack: ResourceCanvasHistoryEntry[];
|
||||
redoStack: ResourceCanvasHistoryEntry[];
|
||||
nextEntryId: number;
|
||||
};
|
||||
|
||||
export function createResourceCanvasHistory(): ResourceCanvasHistory {
|
||||
return { undoStack: [], redoStack: [], nextEntryId: 1 };
|
||||
}
|
||||
|
||||
/**
|
||||
* 按 resourceId 排序后落快照,保证同一组坐标无论 map 顺序如何都比较得出相等。
|
||||
*/
|
||||
export function captureResourceCanvasSnapshot(
|
||||
positions: readonly ResourceCanvasLayoutSnapshotEntry[],
|
||||
): ResourceCanvasLayoutSnapshot {
|
||||
return {
|
||||
entries: positions
|
||||
.map((position) => ({
|
||||
resourceId: position.resourceId,
|
||||
section: position.section,
|
||||
x: position.x,
|
||||
y: position.y,
|
||||
manuallyPlaced: position.manuallyPlaced,
|
||||
}))
|
||||
.sort((left, right) => left.resourceId.localeCompare(right.resourceId)),
|
||||
};
|
||||
}
|
||||
|
||||
export function resourceCanvasSnapshotsEqual(
|
||||
left: ResourceCanvasLayoutSnapshot,
|
||||
right: ResourceCanvasLayoutSnapshot,
|
||||
) {
|
||||
if (left.entries.length !== right.entries.length) {
|
||||
return false;
|
||||
}
|
||||
return left.entries.every((entry, index) => {
|
||||
const other = right.entries[index];
|
||||
return (
|
||||
other !== undefined &&
|
||||
entry.resourceId === other.resourceId &&
|
||||
entry.section === other.section &&
|
||||
entry.x === other.x &&
|
||||
entry.y === other.y &&
|
||||
entry.manuallyPlaced === other.manuallyPlaced
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export function pushResourceCanvasHistory(
|
||||
history: ResourceCanvasHistory,
|
||||
{
|
||||
label,
|
||||
snapshot,
|
||||
createdAt = 0,
|
||||
}: {
|
||||
label: string;
|
||||
snapshot: ResourceCanvasLayoutSnapshot;
|
||||
createdAt?: number;
|
||||
},
|
||||
): ResourceCanvasHistory {
|
||||
const previous = history.undoStack.at(-1);
|
||||
if (previous && resourceCanvasSnapshotsEqual(previous.snapshot, snapshot)) {
|
||||
return history;
|
||||
}
|
||||
const entry: ResourceCanvasHistoryEntry = {
|
||||
id: history.nextEntryId,
|
||||
label,
|
||||
snapshot,
|
||||
createdAt,
|
||||
};
|
||||
return {
|
||||
undoStack: [
|
||||
...history.undoStack.slice(-(MAX_RESOURCE_CANVAS_HISTORY_STEPS - 1)),
|
||||
entry,
|
||||
],
|
||||
// 新的操作会让原来的重做分支失效。
|
||||
redoStack: [],
|
||||
nextEntryId: history.nextEntryId + 1,
|
||||
};
|
||||
}
|
||||
|
||||
export type ResourceCanvasHistoryStep = {
|
||||
history: ResourceCanvasHistory;
|
||||
label: string;
|
||||
snapshot: ResourceCanvasLayoutSnapshot;
|
||||
};
|
||||
|
||||
export function undoResourceCanvasHistory(
|
||||
history: ResourceCanvasHistory,
|
||||
currentSnapshot: ResourceCanvasLayoutSnapshot,
|
||||
): ResourceCanvasHistoryStep | null {
|
||||
const entry = history.undoStack.at(-1);
|
||||
if (!entry) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
history: {
|
||||
undoStack: history.undoStack.slice(0, -1),
|
||||
redoStack: [
|
||||
...history.redoStack.slice(-(MAX_RESOURCE_CANVAS_HISTORY_STEPS - 1)),
|
||||
{
|
||||
...entry,
|
||||
snapshot: currentSnapshot,
|
||||
},
|
||||
],
|
||||
nextEntryId: history.nextEntryId,
|
||||
},
|
||||
label: entry.label,
|
||||
snapshot: entry.snapshot,
|
||||
};
|
||||
}
|
||||
|
||||
export function redoResourceCanvasHistory(
|
||||
history: ResourceCanvasHistory,
|
||||
currentSnapshot: ResourceCanvasLayoutSnapshot,
|
||||
): ResourceCanvasHistoryStep | null {
|
||||
const entry = history.redoStack.at(-1);
|
||||
if (!entry) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
history: {
|
||||
undoStack: [
|
||||
...history.undoStack.slice(-(MAX_RESOURCE_CANVAS_HISTORY_STEPS - 1)),
|
||||
{
|
||||
...entry,
|
||||
snapshot: currentSnapshot,
|
||||
},
|
||||
],
|
||||
redoStack: history.redoStack.slice(0, -1),
|
||||
nextEntryId: history.nextEntryId,
|
||||
},
|
||||
label: entry.label,
|
||||
snapshot: entry.snapshot,
|
||||
};
|
||||
}
|
||||
|
||||
export function clearResourceCanvasHistory(
|
||||
history: ResourceCanvasHistory,
|
||||
): ResourceCanvasHistory {
|
||||
if (history.undoStack.length === 0 && history.redoStack.length === 0) {
|
||||
return history;
|
||||
}
|
||||
return { ...history, undoStack: [], redoStack: [] };
|
||||
}
|
||||
|
||||
export function canUndoResourceCanvasHistory(history: ResourceCanvasHistory) {
|
||||
return history.undoStack.length > 0;
|
||||
}
|
||||
|
||||
export function canRedoResourceCanvasHistory(history: ResourceCanvasHistory) {
|
||||
return history.redoStack.length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 只回写和快照不一致的位置,避免撤销时把没动过的卡片也当成一次手动摆放写回后端。
|
||||
*/
|
||||
export function resolveResourceCanvasRestoreEntries(
|
||||
snapshot: ResourceCanvasLayoutSnapshot,
|
||||
current: readonly ResourceCanvasLayoutSnapshotEntry[],
|
||||
): ResourceCanvasLayoutSnapshotEntry[] {
|
||||
const currentById = new Map(
|
||||
current.map((entry) => [entry.resourceId, entry] as const),
|
||||
);
|
||||
return snapshot.entries.filter((entry) => {
|
||||
const existing = currentById.get(entry.resourceId);
|
||||
if (!existing) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
existing.section !== entry.section ||
|
||||
existing.x !== entry.x ||
|
||||
existing.y !== entry.y
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -29,11 +29,13 @@ import {
|
||||
Pause,
|
||||
Play,
|
||||
Plus,
|
||||
Redo2,
|
||||
RotateCcw,
|
||||
Search,
|
||||
Settings2,
|
||||
SlidersHorizontal,
|
||||
Sparkles,
|
||||
Undo2,
|
||||
X,
|
||||
ZoomOut,
|
||||
} from 'lucide-react';
|
||||
@@ -80,6 +82,18 @@ import {
|
||||
RESOURCE_REFERENCE_FILTERS,
|
||||
type ResourceReferenceFilter,
|
||||
} from '../../features/project-workspace/resourceReferences';
|
||||
import {
|
||||
canRedoResourceCanvasHistory,
|
||||
canUndoResourceCanvasHistory,
|
||||
captureResourceCanvasSnapshot,
|
||||
clearResourceCanvasHistory,
|
||||
createResourceCanvasHistory,
|
||||
pushResourceCanvasHistory,
|
||||
redoResourceCanvasHistory,
|
||||
resolveResourceCanvasRestoreEntries,
|
||||
type ResourceCanvasLayoutSnapshot,
|
||||
undoResourceCanvasHistory,
|
||||
} from '../../features/resource-canvas/resourceCanvasHistoryModel';
|
||||
import { createResourceQuickEditPanelDraft } from '../../features/resource-canvas/resourceCanvasQuickEditModel';
|
||||
import {
|
||||
canNormalizeResourceIntoManifestAsset,
|
||||
@@ -1195,6 +1209,10 @@ export default function ProjectDevelopmentView({
|
||||
useState<QuickEditPanelState | null>(null);
|
||||
const [resourceCanvasMarquee, setResourceCanvasMarquee] =
|
||||
useState<CanvasMarqueeState | null>(null);
|
||||
/** 资源卡组织操作历史:只回滚布局坐标,不回滚素材。 */
|
||||
const [resourceCanvasHistory, setResourceCanvasHistory] = useState(
|
||||
createResourceCanvasHistory,
|
||||
);
|
||||
const [uiEditorRoute, setUiEditorRoute] = useState<UiEditorRoute | null>(
|
||||
null,
|
||||
);
|
||||
@@ -2966,6 +2984,7 @@ export default function ProjectDevelopmentView({
|
||||
setSelectedResourceIds([]);
|
||||
setQuickEditPanel(null);
|
||||
setQuickEditSourceLayer(null);
|
||||
setResourceCanvasHistory(clearResourceCanvasHistory);
|
||||
setActiveResourceCategory(null);
|
||||
const defaultViewports = defaultResourceCanvasViewports();
|
||||
resourceCanvasViewportTargetsRef.current = defaultViewports;
|
||||
@@ -3246,6 +3265,91 @@ export default function ProjectDevelopmentView({
|
||||
}
|
||||
}, [resourceCardPreviews.previews, stopActiveCardMedia]);
|
||||
|
||||
const applyResourceCanvasLayoutSnapshot = useCallback(
|
||||
(snapshot: ResourceCanvasLayoutSnapshot) => {
|
||||
resolveResourceCanvasRestoreEntries(
|
||||
snapshot,
|
||||
activeResourceLayout.layout.positions,
|
||||
).forEach((entry) => {
|
||||
activeResourceLayout.commitPosition(
|
||||
entry.resourceId,
|
||||
entry.section,
|
||||
entry.x,
|
||||
entry.y,
|
||||
);
|
||||
});
|
||||
},
|
||||
[activeResourceLayout],
|
||||
);
|
||||
|
||||
const undoSaveResourceCanvasLayout = useCallback(() => {
|
||||
const step = undoResourceCanvasHistory(
|
||||
resourceCanvasHistory,
|
||||
captureResourceCanvasSnapshot(activeResourceLayout.layout.positions),
|
||||
);
|
||||
if (!step) {
|
||||
return;
|
||||
}
|
||||
setResourceCanvasHistory(step.history);
|
||||
applyResourceCanvasLayoutSnapshot(step.snapshot);
|
||||
}, [
|
||||
activeResourceLayout.layout.positions,
|
||||
applyResourceCanvasLayoutSnapshot,
|
||||
resourceCanvasHistory,
|
||||
]);
|
||||
|
||||
const redoResourceCanvasLayout = useCallback(() => {
|
||||
const step = redoResourceCanvasHistory(
|
||||
resourceCanvasHistory,
|
||||
captureResourceCanvasSnapshot(activeResourceLayout.layout.positions),
|
||||
);
|
||||
if (!step) {
|
||||
return;
|
||||
}
|
||||
setResourceCanvasHistory(step.history);
|
||||
applyResourceCanvasLayoutSnapshot(step.snapshot);
|
||||
}, [
|
||||
activeResourceLayout.layout.positions,
|
||||
applyResourceCanvasLayoutSnapshot,
|
||||
resourceCanvasHistory,
|
||||
]);
|
||||
|
||||
const canUndoResourceCanvas = canUndoResourceCanvasHistory(
|
||||
resourceCanvasHistory,
|
||||
);
|
||||
const canRedoResourceCanvas = canRedoResourceCanvasHistory(
|
||||
resourceCanvasHistory,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const handleHistoryShortcut = (event: KeyboardEvent) => {
|
||||
if (!(event.ctrlKey || event.metaKey) || event.altKey) {
|
||||
return;
|
||||
}
|
||||
const key = event.key.toLowerCase();
|
||||
const isUndo = key === 'z' && !event.shiftKey;
|
||||
const isRedo = (key === 'z' && event.shiftKey) || key === 'y';
|
||||
if (!isUndo && !isRedo) {
|
||||
return;
|
||||
}
|
||||
const target = event.target;
|
||||
if (
|
||||
target instanceof HTMLElement &&
|
||||
target.closest('input, textarea, [contenteditable="true"]')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
if (isUndo) {
|
||||
undoSaveResourceCanvasLayout();
|
||||
} else {
|
||||
redoResourceCanvasLayout();
|
||||
}
|
||||
};
|
||||
window.addEventListener('keydown', handleHistoryShortcut);
|
||||
return () => window.removeEventListener('keydown', handleHistoryShortcut);
|
||||
}, [redoResourceCanvasLayout, undoSaveResourceCanvasLayout]);
|
||||
|
||||
const handleResourceSelect = useCallback(
|
||||
(resourceId: string, options: { append?: boolean } = {}) => {
|
||||
if (skipNextResourceCardClickRef.current === resourceId) {
|
||||
@@ -3591,6 +3695,15 @@ export default function ProjectDevelopmentView({
|
||||
: null;
|
||||
setResourceCardDragPreview(null);
|
||||
if (commit && drag.changed && preview?.resourceId === drag.resourceId) {
|
||||
// 先落"拖动前"的快照,撤销才有回退点;它只含布局坐标。
|
||||
setResourceCanvasHistory((history) =>
|
||||
pushResourceCanvasHistory(history, {
|
||||
label: '移动资源卡',
|
||||
snapshot: captureResourceCanvasSnapshot(
|
||||
activeResourceLayout.layout.positions,
|
||||
),
|
||||
}),
|
||||
);
|
||||
activeResourceLayout.commitPosition(
|
||||
drag.resourceId,
|
||||
drag.section,
|
||||
@@ -5010,6 +5123,26 @@ export default function ProjectDevelopmentView({
|
||||
onWheel={handleResourceBookWheel}
|
||||
/>
|
||||
<div className="game-resource-book-zoom">
|
||||
<button
|
||||
type="button"
|
||||
className="game-resource-book-zoom-button"
|
||||
aria-label="撤销画布操作"
|
||||
title="撤销(Ctrl/Cmd+Z)"
|
||||
disabled={!canUndoResourceCanvas}
|
||||
onClick={undoSaveResourceCanvasLayout}
|
||||
>
|
||||
<Undo2 size={16} aria-hidden="true" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="game-resource-book-zoom-button"
|
||||
aria-label="重做画布操作"
|
||||
title="重做(Ctrl/Cmd+Shift+Z)"
|
||||
disabled={!canRedoResourceCanvas}
|
||||
onClick={redoResourceCanvasLayout}
|
||||
>
|
||||
<Redo2 size={16} aria-hidden="true" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="game-resource-book-zoom-button"
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
canRedoResourceCanvasHistory,
|
||||
canUndoResourceCanvasHistory,
|
||||
captureResourceCanvasSnapshot,
|
||||
clearResourceCanvasHistory,
|
||||
createResourceCanvasHistory,
|
||||
MAX_RESOURCE_CANVAS_HISTORY_STEPS,
|
||||
pushResourceCanvasHistory,
|
||||
redoResourceCanvasHistory,
|
||||
resolveResourceCanvasRestoreEntries,
|
||||
undoResourceCanvasHistory,
|
||||
} from '../src/features/resource-canvas/resourceCanvasHistoryModel';
|
||||
|
||||
function positions(
|
||||
entries: Array<[string, number, number, boolean?]>,
|
||||
): ReturnType<typeof captureResourceCanvasSnapshot>['entries'] {
|
||||
return entries.map(([resourceId, x, y, manuallyPlaced = true]) => ({
|
||||
resourceId,
|
||||
section: 'document',
|
||||
x,
|
||||
y,
|
||||
manuallyPlaced,
|
||||
}));
|
||||
}
|
||||
|
||||
describe('resource canvas history model', () => {
|
||||
it('按 resourceId 排序落快照,map 顺序不影响相等判定', () => {
|
||||
const left = captureResourceCanvasSnapshot(
|
||||
positions([
|
||||
['asset:b', 2, 2],
|
||||
['asset:a', 1, 1],
|
||||
]),
|
||||
);
|
||||
const right = captureResourceCanvasSnapshot(
|
||||
positions([
|
||||
['asset:a', 1, 1],
|
||||
['asset:b', 2, 2],
|
||||
]),
|
||||
);
|
||||
|
||||
expect(left.entries.map((entry) => entry.resourceId)).toEqual([
|
||||
'asset:a',
|
||||
'asset:b',
|
||||
]);
|
||||
expect(left).toEqual(right);
|
||||
});
|
||||
|
||||
it('push 连续相同快照不产生新历史条目', () => {
|
||||
const snapshot = captureResourceCanvasSnapshot(
|
||||
positions([['asset:a', 1, 1]]),
|
||||
);
|
||||
const first = pushResourceCanvasHistory(createResourceCanvasHistory(), {
|
||||
label: '移动卡片',
|
||||
snapshot,
|
||||
});
|
||||
const second = pushResourceCanvasHistory(first, {
|
||||
label: '移动卡片',
|
||||
snapshot: captureResourceCanvasSnapshot(positions([['asset:a', 1, 1]])),
|
||||
});
|
||||
|
||||
expect(first.undoStack).toHaveLength(1);
|
||||
expect(second).toBe(first);
|
||||
});
|
||||
|
||||
it('push 后重做分支失效,撤回栈有界', () => {
|
||||
let history = createResourceCanvasHistory();
|
||||
for (
|
||||
let index = 0;
|
||||
index < MAX_RESOURCE_CANVAS_HISTORY_STEPS + 5;
|
||||
index += 1
|
||||
) {
|
||||
history = pushResourceCanvasHistory(history, {
|
||||
label: `移动 ${index}`,
|
||||
snapshot: captureResourceCanvasSnapshot(
|
||||
positions([['asset:a', index, index]]),
|
||||
),
|
||||
});
|
||||
}
|
||||
expect(history.undoStack).toHaveLength(MAX_RESOURCE_CANVAS_HISTORY_STEPS);
|
||||
expect(history.undoStack[0]?.label).toBe('移动 5');
|
||||
|
||||
const undone = undoResourceCanvasHistory(
|
||||
history,
|
||||
captureResourceCanvasSnapshot(positions([['asset:a', 99, 99]])),
|
||||
)!;
|
||||
expect(canRedoResourceCanvasHistory(undone.history)).toBe(true);
|
||||
|
||||
const pushed = pushResourceCanvasHistory(undone.history, {
|
||||
label: '新的移动',
|
||||
snapshot: captureResourceCanvasSnapshot(
|
||||
positions([['asset:a', 120, 120]]),
|
||||
),
|
||||
});
|
||||
expect(canRedoResourceCanvasHistory(pushed)).toBe(false);
|
||||
});
|
||||
|
||||
it('撤销与重做互为逆操作,并回填当前快照', () => {
|
||||
const moved = captureResourceCanvasSnapshot(
|
||||
positions([['asset:a', 10, 20]]),
|
||||
);
|
||||
let history = pushResourceCanvasHistory(createResourceCanvasHistory(), {
|
||||
label: '移动卡片',
|
||||
snapshot: moved,
|
||||
});
|
||||
|
||||
const current = captureResourceCanvasSnapshot(
|
||||
positions([['asset:a', 60, 70]]),
|
||||
);
|
||||
const undone = undoResourceCanvasHistory(history, current)!;
|
||||
expect(undone.snapshot).toEqual(moved);
|
||||
expect(undone.label).toBe('移动卡片');
|
||||
expect(canUndoResourceCanvasHistory(undone.history)).toBe(false);
|
||||
|
||||
const redone = redoResourceCanvasHistory(undone.history, moved)!;
|
||||
expect(redone.snapshot).toEqual(current);
|
||||
expect(canRedoResourceCanvasHistory(redone.history)).toBe(false);
|
||||
expect(canUndoResourceCanvasHistory(redone.history)).toBe(true);
|
||||
history = redone.history;
|
||||
expect(history.redoStack).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('空历史撤销/重做返回 null,清空后两个栈都为空', () => {
|
||||
const empty = createResourceCanvasHistory();
|
||||
expect(undoResourceCanvasHistory(empty, { entries: [] })).toBeNull();
|
||||
expect(redoResourceCanvasHistory(empty, { entries: [] })).toBeNull();
|
||||
|
||||
const history = pushResourceCanvasHistory(empty, {
|
||||
label: '移动卡片',
|
||||
snapshot: captureResourceCanvasSnapshot(positions([['asset:a', 1, 1]])),
|
||||
});
|
||||
const cleared = clearResourceCanvasHistory(history);
|
||||
expect(cleared.undoStack).toHaveLength(0);
|
||||
expect(cleared.redoStack).toHaveLength(0);
|
||||
expect(clearResourceCanvasHistory(cleared)).toBe(cleared);
|
||||
});
|
||||
|
||||
it('只把与当前不一致的位置写回后端', () => {
|
||||
const snapshot = captureResourceCanvasSnapshot(
|
||||
positions([
|
||||
['asset:a', 10, 20],
|
||||
['asset:b', 30, 40],
|
||||
]),
|
||||
);
|
||||
const restore = resolveResourceCanvasRestoreEntries(
|
||||
snapshot,
|
||||
positions([
|
||||
['asset:a', 10, 20],
|
||||
['asset:b', 99, 98],
|
||||
['asset:c', 1, 1],
|
||||
]),
|
||||
);
|
||||
|
||||
expect(restore.map((entry) => entry.resourceId)).toEqual(['asset:b']);
|
||||
});
|
||||
|
||||
it('快照里不存在的资源不会在撤销时被新建出来', () => {
|
||||
const snapshot = captureResourceCanvasSnapshot(
|
||||
positions([['asset:deleted', 10, 20]]),
|
||||
);
|
||||
expect(
|
||||
resolveResourceCanvasRestoreEntries(snapshot, positions([])),
|
||||
).toEqual([]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user