feat: history/undo & redo
This commit is contained in:
parent
f02977b096
commit
ac9ac3d454
9 changed files with 237 additions and 29 deletions
|
|
@ -4,6 +4,7 @@ import * as PIXI from "pixi.js";
|
||||||
import { Container, Stage } from "@pixi/react";
|
import { Container, Stage } from "@pixi/react";
|
||||||
|
|
||||||
import { CanvasContext } from "@/context/Canvas";
|
import { CanvasContext } from "@/context/Canvas";
|
||||||
|
import { HistoryContext } from "@/context/History";
|
||||||
import { SelectionContext } from "@/context/Selection";
|
import { SelectionContext } from "@/context/Selection";
|
||||||
import { SettingsContext } from "@/context/Settings";
|
import { SettingsContext } from "@/context/Settings";
|
||||||
import { TexturesContext } from "@/context/Textures";
|
import { TexturesContext } from "@/context/Textures";
|
||||||
|
|
@ -41,6 +42,7 @@ PIXI.settings.SCALE_MODE = PIXI.SCALE_MODES.NEAREST;
|
||||||
|
|
||||||
function Canvas() {
|
function Canvas() {
|
||||||
const { stageSize, canvasSize, blocks, coords, scale, version, setStageSize, setBlocks, setCoords, setScale } = useContext(CanvasContext);
|
const { stageSize, canvasSize, blocks, coords, scale, version, setStageSize, setBlocks, setCoords, setScale } = useContext(CanvasContext);
|
||||||
|
const { addHistory, undo, redo } = useContext(HistoryContext);
|
||||||
const { selectionCoords, selectionLayerBlocks, setSelectionCoords, setSelectionLayerBlocks } = useContext(SelectionContext);
|
const { selectionCoords, selectionLayerBlocks, setSelectionCoords, setSelectionLayerBlocks } = useContext(SelectionContext);
|
||||||
const { settings } = useContext(SettingsContext);
|
const { settings } = useContext(SettingsContext);
|
||||||
const { missingTexture } = useContext(TexturesContext);
|
const { missingTexture } = useContext(TexturesContext);
|
||||||
|
|
@ -62,6 +64,9 @@ function Canvas() {
|
||||||
const oldToolRef = useRef<Tool>();
|
const oldToolRef = useRef<Tool>();
|
||||||
const [cssCursor, setCssCursor] = useState("crosshair");
|
const [cssCursor, setCssCursor] = useState("crosshair");
|
||||||
|
|
||||||
|
const startBlocksRef = useRef<Block[]>([]);
|
||||||
|
const startSelectionCoordsRef = useRef<CoordinateArray>([]);
|
||||||
|
|
||||||
const zoom = useCallback(
|
const zoom = useCallback(
|
||||||
(newScale: number) => {
|
(newScale: number) => {
|
||||||
setScale(newScale);
|
setScale(newScale);
|
||||||
|
|
@ -180,15 +185,40 @@ function Canvas() {
|
||||||
updateCssCursor();
|
updateCssCursor();
|
||||||
|
|
||||||
dragStartCoordsRef.current = mouseCoords;
|
dragStartCoordsRef.current = mouseCoords;
|
||||||
|
startBlocksRef.current = [...blocks];
|
||||||
|
startSelectionCoordsRef.current = [...selectionCoords];
|
||||||
|
|
||||||
// Clear selection on click
|
// Clear selection on click
|
||||||
if (tool === "rectangle-select") setSelectionCoords([]);
|
if (tool === "rectangle-select") setSelectionCoords([]);
|
||||||
}, [onToolUse, updateCssCursor, mouseCoords, tool, setSelectionCoords]);
|
}, [onToolUse, updateCssCursor, mouseCoords, blocks, selectionCoords, tool, setSelectionCoords]);
|
||||||
|
|
||||||
const onMouseUp = useCallback(() => {
|
const onMouseUp = useCallback(() => {
|
||||||
setDragging(false);
|
setDragging(false);
|
||||||
updateCssCursor();
|
updateCssCursor();
|
||||||
}, [updateCssCursor]);
|
|
||||||
|
// History entries for pencil and eraser
|
||||||
|
if (tool == "pencil" || tool == "eraser") {
|
||||||
|
// startBlocksRef will mutate if we pass it directly
|
||||||
|
const prevBlocks = [...startBlocksRef.current];
|
||||||
|
|
||||||
|
addHistory(
|
||||||
|
tool == "pencil" ? "Pencil" : "Eraser",
|
||||||
|
() => setBlocks([...blocks]),
|
||||||
|
() => setBlocks([...prevBlocks])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tool == "rectangle-select" || tool == "magic-wand" || tool == "lasso") {
|
||||||
|
// startSelectionCoordsRef will mutate if we pass it directly
|
||||||
|
const prevSelection = [...startSelectionCoordsRef.current];
|
||||||
|
|
||||||
|
addHistory(
|
||||||
|
tool == "rectangle-select" ? "Rectangle Select" : tool == "lasso" ? "Lasso" : "Magic Wand",
|
||||||
|
() => setSelectionCoords([...selectionCoords]),
|
||||||
|
() => setSelectionCoords([...prevSelection])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [updateCssCursor, blocks, tool, addHistory, setBlocks, selectionCoords, setSelectionCoords]);
|
||||||
|
|
||||||
const onWheel = useCallback(
|
const onWheel = useCallback(
|
||||||
(e: React.WheelEvent) => {
|
(e: React.WheelEvent) => {
|
||||||
|
|
@ -233,10 +263,18 @@ function Canvas() {
|
||||||
holdingAltRef.current = true;
|
holdingAltRef.current = true;
|
||||||
if (tool === "zoom") setCssCursor("zoom-out");
|
if (tool === "zoom") setCssCursor("zoom-out");
|
||||||
break;
|
break;
|
||||||
case "Delete": {
|
case "Delete":
|
||||||
setBlocks((prev) => prev.filter((b) => !selectionCoords.some(([x2, y2]) => x2 === b.x && y2 === b.y)));
|
setBlocks((prev) => {
|
||||||
|
const deletedBlocks = prev.filter((b) => !selectionCoords.some(([x2, y2]) => x2 === b.x && y2 === b.y));
|
||||||
|
addHistory(
|
||||||
|
"Delete",
|
||||||
|
() => setBlocks(deletedBlocks),
|
||||||
|
() => setBlocks(prev)
|
||||||
|
);
|
||||||
|
|
||||||
|
return deletedBlocks;
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
case "a": {
|
case "a": {
|
||||||
if (!e.ctrlKey) return;
|
if (!e.ctrlKey) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
@ -252,16 +290,22 @@ function Canvas() {
|
||||||
setSelectionCoords(newSelection);
|
setSelectionCoords(newSelection);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "c": {
|
case "z":
|
||||||
|
if (!e.ctrlKey) return;
|
||||||
|
undo();
|
||||||
|
break;
|
||||||
|
case "y":
|
||||||
|
if (!e.ctrlKey) return;
|
||||||
|
redo();
|
||||||
|
break;
|
||||||
|
case "c":
|
||||||
if (!e.ctrlKey) return;
|
if (!e.ctrlKey) return;
|
||||||
clipboard.copy(selectionCoords, blocks);
|
clipboard.copy(selectionCoords, blocks);
|
||||||
break;
|
break;
|
||||||
}
|
case "v":
|
||||||
case "v": {
|
|
||||||
if (!e.ctrlKey) return;
|
if (!e.ctrlKey) return;
|
||||||
clipboard.paste(setSelectionLayerBlocks, setSelectionCoords, setTool);
|
clipboard.paste(setSelectionLayerBlocks, setSelectionCoords, setTool);
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
case "1":
|
case "1":
|
||||||
setTool("hand");
|
setTool("hand");
|
||||||
break;
|
break;
|
||||||
|
|
@ -321,6 +365,8 @@ function Canvas() {
|
||||||
setSelectionCoords,
|
setSelectionCoords,
|
||||||
setSelectionLayerBlocks,
|
setSelectionLayerBlocks,
|
||||||
setTool,
|
setTool,
|
||||||
|
redo,
|
||||||
|
undo,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -383,7 +429,6 @@ function Canvas() {
|
||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener("beforeunload", onBeforeUnload);
|
window.addEventListener("beforeunload", onBeforeUnload);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener("beforeunload", onBeforeUnload);
|
window.removeEventListener("beforeunload", onBeforeUnload);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,9 @@ function ViewMenu() {
|
||||||
</MenubarCheckboxItem>
|
</MenubarCheckboxItem>
|
||||||
|
|
||||||
<MenubarSeparator />
|
<MenubarSeparator />
|
||||||
|
<MenubarCheckboxItem checked={settings.historyPanel} onCheckedChange={onCheckedChange("historyPanel")}>
|
||||||
|
History Panel
|
||||||
|
</MenubarCheckboxItem>
|
||||||
<MenubarCheckboxItem checked={settings.colorPicker} onCheckedChange={onCheckedChange("colorPicker")}>
|
<MenubarCheckboxItem checked={settings.colorPicker} onCheckedChange={onCheckedChange("colorPicker")}>
|
||||||
Color Picker
|
Color Picker
|
||||||
</MenubarCheckboxItem>
|
</MenubarCheckboxItem>
|
||||||
|
|
|
||||||
30
src/components/tool-settings/History.tsx
Normal file
30
src/components/tool-settings/History.tsx
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
import { useContext } from "react";
|
||||||
|
import { HistoryContext } from "@/context/History";
|
||||||
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
|
|
||||||
|
function History() {
|
||||||
|
const { history, currentIndex, jumpTo } = useContext(HistoryContext);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ScrollArea key={history.length} className="h-48 border border-zinc-200 dark:border-zinc-800 rounded-md">
|
||||||
|
<div className="flex flex-col">
|
||||||
|
{history.map(({ name }, index) => (
|
||||||
|
<button
|
||||||
|
key={index}
|
||||||
|
onClick={() => jumpTo(index)}
|
||||||
|
className={`w-full border-b border-zinc-200 dark:border-zinc-800
|
||||||
|
// Current entry
|
||||||
|
${index == currentIndex ? "bg-zinc-200 dark:bg-zinc-800 cursor-auto" : "bg-zinc-100 dark:bg-zinc-900"}
|
||||||
|
// Ghost entries
|
||||||
|
${index > currentIndex ? "bg-zinc-300 dark:bg-zinc-950 text-zinc-500 dark:text-zinc-600" : ""}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<span>{name}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default History;
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { useContext, useEffect, useRef, useState } from "react";
|
import { useContext, useEffect, useRef, useState } from "react";
|
||||||
|
import { GripVerticalIcon } from "lucide-react";
|
||||||
|
|
||||||
import { SettingsContext } from "@/context/Settings";
|
import { SettingsContext } from "@/context/Settings";
|
||||||
|
|
||||||
|
|
@ -6,11 +7,11 @@ import { Input } from "@/components/ui/input";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
|
|
||||||
|
import History from "./History";
|
||||||
import ColorPicker from "./ColorPicker";
|
import ColorPicker from "./ColorPicker";
|
||||||
import Replace from "./Replace";
|
import Replace from "./Replace";
|
||||||
import Radius from "./Radius";
|
import Radius from "./Radius";
|
||||||
import BlockSelector from "./BlockSelector";
|
import BlockSelector from "./BlockSelector";
|
||||||
import { GripVerticalIcon } from "lucide-react";
|
|
||||||
|
|
||||||
function ToolSettings() {
|
function ToolSettings() {
|
||||||
const { settings } = useContext(SettingsContext);
|
const { settings } = useContext(SettingsContext);
|
||||||
|
|
@ -82,6 +83,14 @@ function ToolSettings() {
|
||||||
<GripVerticalIcon />
|
<GripVerticalIcon />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{settings.historyPanel && (
|
||||||
|
<>
|
||||||
|
{/* <span className="text-xs text-zinc-600">History</span> */}
|
||||||
|
<History />
|
||||||
|
<Separator />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
{settings.colorPicker && (
|
{settings.colorPicker && (
|
||||||
<>
|
<>
|
||||||
<ColorPicker />
|
<ColorPicker />
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import React, { createContext, ReactNode, useMemo, useState } from "react";
|
import React, { createContext, ReactNode, useContext, useEffect, useMemo, useState } from "react";
|
||||||
|
|
||||||
|
import { HistoryContext } from "./History";
|
||||||
import welcomeBlocksData from "@/data/welcome.json";
|
import welcomeBlocksData from "@/data/welcome.json";
|
||||||
|
|
||||||
interface Context {
|
interface Context {
|
||||||
|
|
@ -24,6 +25,8 @@ interface Props {
|
||||||
export const CanvasContext = createContext<Context>({} as Context);
|
export const CanvasContext = createContext<Context>({} as Context);
|
||||||
|
|
||||||
export const CanvasProvider = ({ children }: Props) => {
|
export const CanvasProvider = ({ children }: Props) => {
|
||||||
|
const { addHistory } = useContext(HistoryContext);
|
||||||
|
|
||||||
const [stageSize, setStageSize] = useState<Dimension>({ width: 0, height: 0 });
|
const [stageSize, setStageSize] = useState<Dimension>({ width: 0, height: 0 });
|
||||||
const [blocks, setBlocks] = useState<Block[]>(welcomeBlocksData);
|
const [blocks, setBlocks] = useState<Block[]>(welcomeBlocksData);
|
||||||
const [coords, setCoords] = useState<Position>({ x: 0, y: 0 });
|
const [coords, setCoords] = useState<Position>({ x: 0, y: 0 });
|
||||||
|
|
@ -84,9 +87,30 @@ export const CanvasProvider = ({ children }: Props) => {
|
||||||
setCoords({ x: newX, y: newY });
|
setCoords({ x: newX, y: newY });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
addHistory(
|
||||||
|
"New Canvas",
|
||||||
|
() => setBlocks(welcomeBlocksData),
|
||||||
|
() => setBlocks([])
|
||||||
|
);
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CanvasContext.Provider
|
<CanvasContext.Provider
|
||||||
value={{ stageSize, canvasSize, blocks, coords, scale, version, setStageSize, setBlocks, setCoords, setScale, setVersion, centerCanvas }}
|
value={{
|
||||||
|
stageSize,
|
||||||
|
canvasSize,
|
||||||
|
blocks,
|
||||||
|
coords,
|
||||||
|
scale,
|
||||||
|
version,
|
||||||
|
setStageSize,
|
||||||
|
setBlocks,
|
||||||
|
setCoords,
|
||||||
|
setScale,
|
||||||
|
setVersion,
|
||||||
|
centerCanvas,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</CanvasContext.Provider>
|
</CanvasContext.Provider>
|
||||||
|
|
|
||||||
86
src/context/History.tsx
Normal file
86
src/context/History.tsx
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
import { createContext, ReactNode, useState } from "react";
|
||||||
|
|
||||||
|
interface Context {
|
||||||
|
history: HistoryEntry[];
|
||||||
|
currentIndex: number;
|
||||||
|
isUndoAvailable: boolean;
|
||||||
|
isRedoAvailable: boolean;
|
||||||
|
addHistory: (name: string, apply: () => void, revert: () => void) => void;
|
||||||
|
undo: () => void;
|
||||||
|
redo: () => void;
|
||||||
|
jumpTo: (index: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const HistoryContext = createContext<Context>({} as Context);
|
||||||
|
|
||||||
|
export const HistoryProvider = ({ children }: Props) => {
|
||||||
|
const [history, setHistory] = useState<HistoryEntry[]>([]);
|
||||||
|
const [currentIndex, setCurrentIndex] = useState(-1);
|
||||||
|
const isUndoAvailable = currentIndex > 0;
|
||||||
|
const isRedoAvailable = currentIndex < history.length - 1;
|
||||||
|
|
||||||
|
const MAX_HISTORY = 20;
|
||||||
|
|
||||||
|
const addHistory = (name: string, apply: () => void, revert: () => void) => {
|
||||||
|
const newHistory = history.slice(0, currentIndex + 1);
|
||||||
|
newHistory.push({ name, apply, revert });
|
||||||
|
|
||||||
|
if (newHistory.length > MAX_HISTORY) {
|
||||||
|
newHistory.shift();
|
||||||
|
setCurrentIndex((prev) => Math.max(prev - 1, 0));
|
||||||
|
} else {
|
||||||
|
setCurrentIndex(newHistory.length - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
setHistory(newHistory);
|
||||||
|
};
|
||||||
|
|
||||||
|
const undo = () => {
|
||||||
|
if (!isUndoAvailable) return;
|
||||||
|
history[currentIndex].revert();
|
||||||
|
setCurrentIndex(currentIndex - 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const redo = () => {
|
||||||
|
if (!isRedoAvailable) return;
|
||||||
|
history[currentIndex + 1].apply();
|
||||||
|
setCurrentIndex(currentIndex + 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const jumpTo = (index: number) => {
|
||||||
|
if (index == currentIndex) return;
|
||||||
|
|
||||||
|
if (index > currentIndex) {
|
||||||
|
for (let i = currentIndex + 1; i <= index; i++) {
|
||||||
|
history[i].apply();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (let i = currentIndex; i > index; i--) {
|
||||||
|
history[i].revert();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setCurrentIndex(index);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<HistoryContext.Provider
|
||||||
|
value={{
|
||||||
|
history,
|
||||||
|
currentIndex,
|
||||||
|
isUndoAvailable,
|
||||||
|
isRedoAvailable,
|
||||||
|
addHistory,
|
||||||
|
undo,
|
||||||
|
redo,
|
||||||
|
jumpTo,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</HistoryContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -12,8 +12,9 @@ interface Props {
|
||||||
const defaultSettings: Settings = {
|
const defaultSettings: Settings = {
|
||||||
grid: true,
|
grid: true,
|
||||||
canvasBorder: false,
|
canvasBorder: false,
|
||||||
|
historyPanel: true,
|
||||||
colorPicker: false,
|
colorPicker: false,
|
||||||
blockReplacer: true,
|
blockReplacer: false,
|
||||||
radiusChanger: true,
|
radiusChanger: true,
|
||||||
blockSelector: true,
|
blockSelector: true,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { CanvasProvider } from "@/context/Canvas";
|
import { CanvasProvider } from "@/context/Canvas";
|
||||||
|
import { HistoryProvider } from "@/context/History";
|
||||||
import { LoadingProvider } from "@/context/Loading";
|
import { LoadingProvider } from "@/context/Loading";
|
||||||
import { SelectionProvider } from "@/context/Selection";
|
import { SelectionProvider } from "@/context/Selection";
|
||||||
import { SettingsProvider } from "@/context/Settings";
|
import { SettingsProvider } from "@/context/Settings";
|
||||||
|
|
@ -13,12 +14,13 @@ import ToolSettings from "@/components/tool-settings";
|
||||||
|
|
||||||
function AppPage() {
|
function AppPage() {
|
||||||
return (
|
return (
|
||||||
<CanvasProvider>
|
|
||||||
<LoadingProvider>
|
<LoadingProvider>
|
||||||
<SelectionProvider>
|
|
||||||
<SettingsProvider>
|
<SettingsProvider>
|
||||||
|
<HistoryProvider>
|
||||||
|
<CanvasProvider>
|
||||||
<TexturesProvider>
|
<TexturesProvider>
|
||||||
<ToolProvider>
|
<ToolProvider>
|
||||||
|
<SelectionProvider>
|
||||||
<MobileNotice />
|
<MobileNotice />
|
||||||
|
|
||||||
<main className="overflow-y-hidden h-screen grid grid-rows-[2.5rem_minmax(0,1fr)] grid-cols-[2.5rem_minmax(0,1fr)_auto]">
|
<main className="overflow-y-hidden h-screen grid grid-rows-[2.5rem_minmax(0,1fr)] grid-cols-[2.5rem_minmax(0,1fr)_auto]">
|
||||||
|
|
@ -27,12 +29,13 @@ function AppPage() {
|
||||||
<Canvas />
|
<Canvas />
|
||||||
<ToolSettings />
|
<ToolSettings />
|
||||||
</main>
|
</main>
|
||||||
|
</SelectionProvider>
|
||||||
</ToolProvider>
|
</ToolProvider>
|
||||||
</TexturesProvider>
|
</TexturesProvider>
|
||||||
</SettingsProvider>
|
|
||||||
</SelectionProvider>
|
|
||||||
</LoadingProvider>
|
|
||||||
</CanvasProvider>
|
</CanvasProvider>
|
||||||
|
</HistoryProvider>
|
||||||
|
</SettingsProvider>
|
||||||
|
</LoadingProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
7
src/types.d.ts
vendored
7
src/types.d.ts
vendored
|
|
@ -28,6 +28,7 @@ type Tool = "hand" | "move" | "rectangle-select" | "lasso" | "magic-wand" | "pen
|
||||||
interface Settings {
|
interface Settings {
|
||||||
grid: boolean;
|
grid: boolean;
|
||||||
canvasBorder: boolean;
|
canvasBorder: boolean;
|
||||||
|
historyPanel: boolean;
|
||||||
colorPicker: boolean;
|
colorPicker: boolean;
|
||||||
blockReplacer: boolean;
|
blockReplacer: boolean;
|
||||||
radiusChanger: boolean;
|
radiusChanger: boolean;
|
||||||
|
|
@ -53,3 +54,9 @@ type BlockData = Record<
|
||||||
properties?: Record<string, string>;
|
properties?: Record<string, string>;
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
interface HistoryEntry {
|
||||||
|
name: string;
|
||||||
|
apply: () => void;
|
||||||
|
revert: () => void;
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue