refactor: add useGetRefreshFlows mutation and update code to use it (#3475)
* Added refresh flows mutation * Changed places that used refreshFlows to use the mutation * removed old refreshFlows * removed readFlowsFromDatabase api call * Removed unused API calls from API.ts * Removed getFlowFromDatabase call
This commit is contained in:
parent
0586d1e1d5
commit
3a408c8141
8 changed files with 79 additions and 207 deletions
|
|
@ -55,157 +55,6 @@ export async function sendAll(data: sendAllProps) {
|
||||||
return await api.post(`${BASE_URL_API}predict`, data);
|
return await api.post(`${BASE_URL_API}predict`, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetches a list of JSON files from a GitHub repository and returns their contents as an array of FlowType objects.
|
|
||||||
*
|
|
||||||
* @returns {Promise<FlowType[]>} A promise that resolves to an array of FlowType objects.
|
|
||||||
*/
|
|
||||||
export async function getExamples(): Promise<FlowType[]> {
|
|
||||||
const url =
|
|
||||||
"https://api.github.com/repos/langflow-ai/langflow_examples/contents/examples?ref=main";
|
|
||||||
const response = await api.get(url);
|
|
||||||
|
|
||||||
const jsonFiles = response?.data.filter((file: any) => {
|
|
||||||
return file.name.endsWith(".json");
|
|
||||||
});
|
|
||||||
|
|
||||||
const contentsPromises = jsonFiles.map(async (file: any) => {
|
|
||||||
const contentResponse = await api.get(file.download_url);
|
|
||||||
return contentResponse.data;
|
|
||||||
});
|
|
||||||
|
|
||||||
return await Promise.all(contentsPromises);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Saves a new flow to the database.
|
|
||||||
*
|
|
||||||
* @param {FlowType} newFlow - The flow data to save.
|
|
||||||
* @returns {Promise<any>} The saved flow data.
|
|
||||||
* @throws Will throw an error if saving fails.
|
|
||||||
*/
|
|
||||||
export async function saveFlowToDatabase(newFlow: {
|
|
||||||
name: string;
|
|
||||||
id: string;
|
|
||||||
data: ReactFlowJsonObject | null;
|
|
||||||
description: string;
|
|
||||||
style?: FlowStyleType;
|
|
||||||
is_component?: boolean;
|
|
||||||
folder_id?: string;
|
|
||||||
endpoint_name?: string;
|
|
||||||
}): Promise<FlowType> {
|
|
||||||
try {
|
|
||||||
const response = await api.post(`${BASE_URL_API}flows/`, {
|
|
||||||
name: newFlow.name,
|
|
||||||
data: newFlow.data,
|
|
||||||
description: newFlow.description,
|
|
||||||
is_component: newFlow.is_component,
|
|
||||||
folder_id: newFlow.folder_id === "" ? null : newFlow.folder_id,
|
|
||||||
endpoint_name: newFlow.endpoint_name,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response?.status !== 201) {
|
|
||||||
throw new Error(`HTTP error! status: ${response?.status}`);
|
|
||||||
}
|
|
||||||
return response?.data;
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reads all flows from the database.
|
|
||||||
*
|
|
||||||
* @returns {Promise<any>} The flows data.
|
|
||||||
* @throws Will throw an error if reading fails.
|
|
||||||
*/
|
|
||||||
export async function readFlowsFromDatabase() {
|
|
||||||
try {
|
|
||||||
const response = await api.get(`${BASE_URL_API}flows/`);
|
|
||||||
if (response && response?.status !== 200) {
|
|
||||||
throw new Error(`HTTP error! status: ${response?.status}`);
|
|
||||||
}
|
|
||||||
return response?.data;
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function uploadFlowsToDatabase(flows: FormData) {
|
|
||||||
try {
|
|
||||||
const response = await api.post(`${BASE_URL_API}flows/upload/`, flows);
|
|
||||||
|
|
||||||
if (response?.status !== 201) {
|
|
||||||
throw new Error(`HTTP error! status: ${response?.status}`);
|
|
||||||
}
|
|
||||||
return response?.data;
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deletes a flow from the database.
|
|
||||||
*
|
|
||||||
* @param {string} flowId - The ID of the flow to delete.
|
|
||||||
* @returns {Promise<any>} The deleted flow data.
|
|
||||||
* @throws Will throw an error if deletion fails.
|
|
||||||
*/
|
|
||||||
export async function deleteFlowFromDatabase(flowId: string) {
|
|
||||||
try {
|
|
||||||
const response = await api.delete(`${BASE_URL_API}flows/${flowId}`);
|
|
||||||
if (response.status !== 200) {
|
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
|
||||||
}
|
|
||||||
return response?.data;
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetches a flow from the database by ID.
|
|
||||||
*
|
|
||||||
* @param {number} flowId - The ID of the flow to fetch.
|
|
||||||
* @returns {Promise<any>} The flow data.
|
|
||||||
* @throws Will throw an error if fetching fails.
|
|
||||||
*/
|
|
||||||
export async function getFlowFromDatabase(flowId: number) {
|
|
||||||
try {
|
|
||||||
const response = await api.get(`${BASE_URL_API}flows/${flowId}`);
|
|
||||||
if (response && response?.status !== 200) {
|
|
||||||
throw new Error(`HTTP error! status: ${response?.status}`);
|
|
||||||
}
|
|
||||||
return response?.data;
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetches flow styles from the database.
|
|
||||||
*
|
|
||||||
* @returns {Promise<any>} The flow styles data.
|
|
||||||
* @throws Will throw an error if fetching fails.
|
|
||||||
*/
|
|
||||||
export async function getFlowStylesFromDatabase() {
|
|
||||||
try {
|
|
||||||
const response = await api.get(`${BASE_URL_API}flow_styles/`);
|
|
||||||
if (response.status !== 200) {
|
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
|
||||||
}
|
|
||||||
return response?.data;
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches the version of the API.
|
* Fetches the version of the API.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,67 @@
|
||||||
|
import useAlertStore from "@/stores/alertStore";
|
||||||
|
import useFlowsManagerStore from "@/stores/flowsManagerStore";
|
||||||
|
import { useFolderStore } from "@/stores/foldersStore";
|
||||||
|
import { useTypesStore } from "@/stores/typesStore";
|
||||||
|
import { useMutationFunctionType } from "@/types/api";
|
||||||
|
import { FlowType } from "@/types/flow";
|
||||||
|
import {
|
||||||
|
extractFieldsFromComponenents,
|
||||||
|
processFlows,
|
||||||
|
} from "@/utils/reactflowUtils";
|
||||||
|
import { UseMutationResult } from "@tanstack/react-query";
|
||||||
|
import { api } from "../../api";
|
||||||
|
import { getURL } from "../../helpers/constants";
|
||||||
|
import { UseRequestProcessor } from "../../services/request-processor";
|
||||||
|
|
||||||
|
export const useGetRefreshFlows: useMutationFunctionType<
|
||||||
|
undefined,
|
||||||
|
undefined
|
||||||
|
> = (options?) => {
|
||||||
|
const { mutate } = UseRequestProcessor();
|
||||||
|
|
||||||
|
const setExamples = useFlowsManagerStore((state) => state.setExamples);
|
||||||
|
const setFlows = useFlowsManagerStore((state) => state.setFlows);
|
||||||
|
const setErrorData = useAlertStore((state) => state.setErrorData);
|
||||||
|
const starterProjectId = useFolderStore((state) => state.starterProjectId);
|
||||||
|
|
||||||
|
const getRefreshFlowsFn = async (): Promise<void> => {
|
||||||
|
try {
|
||||||
|
} catch (e) {
|
||||||
|
setErrorData({
|
||||||
|
title: "Could not load flows from database",
|
||||||
|
});
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
const response = await api.get<FlowType[]>(`${getURL("FLOWS")}/`);
|
||||||
|
const dbData = response.data;
|
||||||
|
if (dbData) {
|
||||||
|
const { data, flows } = processFlows(dbData);
|
||||||
|
const examples = flows.filter(
|
||||||
|
(flow) => flow.folder_id === starterProjectId,
|
||||||
|
);
|
||||||
|
setExamples(examples);
|
||||||
|
|
||||||
|
const flowsWithoutStarterFolder = flows.filter(
|
||||||
|
(flow) => flow.folder_id !== starterProjectId,
|
||||||
|
);
|
||||||
|
|
||||||
|
setFlows(flowsWithoutStarterFolder);
|
||||||
|
useTypesStore.setState((state) => ({
|
||||||
|
data: { ...state.data, ["saved_components"]: data },
|
||||||
|
ComponentFields: extractFieldsFromComponenents({
|
||||||
|
...state.data,
|
||||||
|
["saved_components"]: data,
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const mutation: UseMutationResult<void, any, undefined> = mutate(
|
||||||
|
["useGetRefreshFlows"],
|
||||||
|
getRefreshFlowsFn,
|
||||||
|
options,
|
||||||
|
);
|
||||||
|
|
||||||
|
return mutation;
|
||||||
|
};
|
||||||
|
|
@ -1,18 +1,19 @@
|
||||||
import { DEFAULT_FOLDER, STARTER_FOLDER_NAME } from "@/constants/constants";
|
import { DEFAULT_FOLDER, STARTER_FOLDER_NAME } from "@/constants/constants";
|
||||||
import { FolderType } from "@/pages/MainPage/entities";
|
import { FolderType } from "@/pages/MainPage/entities";
|
||||||
import useFlowsManagerStore from "@/stores/flowsManagerStore";
|
|
||||||
import { useFolderStore } from "@/stores/foldersStore";
|
import { useFolderStore } from "@/stores/foldersStore";
|
||||||
import { useTypesStore } from "@/stores/typesStore";
|
import { useTypesStore } from "@/stores/typesStore";
|
||||||
import { useQueryFunctionType } from "@/types/api";
|
import { useQueryFunctionType } from "@/types/api";
|
||||||
import { api } from "../../api";
|
import { api } from "../../api";
|
||||||
import { getURL } from "../../helpers/constants";
|
import { getURL } from "../../helpers/constants";
|
||||||
import { UseRequestProcessor } from "../../services/request-processor";
|
import { UseRequestProcessor } from "../../services/request-processor";
|
||||||
|
import { useGetRefreshFlows } from "../flows/use-get-refresh-flows";
|
||||||
|
|
||||||
export const useGetFoldersQuery: useQueryFunctionType<
|
export const useGetFoldersQuery: useQueryFunctionType<
|
||||||
undefined,
|
undefined,
|
||||||
FolderType[]
|
FolderType[]
|
||||||
> = (options) => {
|
> = (options) => {
|
||||||
const { query } = UseRequestProcessor();
|
const { query } = UseRequestProcessor();
|
||||||
|
const { mutateAsync: refreshFlows } = useGetRefreshFlows();
|
||||||
|
|
||||||
const setStarterProjectId = useFolderStore(
|
const setStarterProjectId = useFolderStore(
|
||||||
(state) => state.setStarterProjectId,
|
(state) => state.setStarterProjectId,
|
||||||
|
|
@ -36,10 +37,9 @@ export const useGetFoldersQuery: useQueryFunctionType<
|
||||||
const myCollectionId = data?.find((f) => f.name === DEFAULT_FOLDER)?.id;
|
const myCollectionId = data?.find((f) => f.name === DEFAULT_FOLDER)?.id;
|
||||||
setMyCollectionId(myCollectionId);
|
setMyCollectionId(myCollectionId);
|
||||||
|
|
||||||
const { refreshFlows } = useFlowsManagerStore.getState();
|
|
||||||
const { getTypes } = useTypesStore.getState();
|
const { getTypes } = useTypesStore.getState();
|
||||||
|
|
||||||
await refreshFlows();
|
await refreshFlows(undefined);
|
||||||
await getTypes();
|
await getTypes();
|
||||||
|
|
||||||
return foldersWithoutStarterProjects;
|
return foldersWithoutStarterProjects;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
|
import { useGetRefreshFlows } from "@/controllers/API/queries/flows/use-get-refresh-flows";
|
||||||
import { createFileUpload } from "@/helpers/create-file-upload";
|
import { createFileUpload } from "@/helpers/create-file-upload";
|
||||||
import { getObjectsFromFilelist } from "@/helpers/get-objects-from-filelist";
|
import { getObjectsFromFilelist } from "@/helpers/get-objects-from-filelist";
|
||||||
import useFlowStore from "@/stores/flowStore";
|
import useFlowStore from "@/stores/flowStore";
|
||||||
import useFlowsManagerStore from "@/stores/flowsManagerStore";
|
|
||||||
import { FlowType } from "@/types/flow";
|
import { FlowType } from "@/types/flow";
|
||||||
import { processDataFromFlow } from "@/utils/reactflowUtils";
|
import { processDataFromFlow } from "@/utils/reactflowUtils";
|
||||||
import useAddFlow from "./use-add-flow";
|
import useAddFlow from "./use-add-flow";
|
||||||
|
|
@ -9,7 +9,7 @@ import useAddFlow from "./use-add-flow";
|
||||||
const useUploadFlow = () => {
|
const useUploadFlow = () => {
|
||||||
const addFlow = useAddFlow();
|
const addFlow = useAddFlow();
|
||||||
const paste = useFlowStore((state) => state.paste);
|
const paste = useFlowStore((state) => state.paste);
|
||||||
const refreshFlows = useFlowsManagerStore((state) => state.refreshFlows);
|
const { mutate: refreshFlows } = useGetRefreshFlows();
|
||||||
|
|
||||||
const getFlowsFromFiles = async ({
|
const getFlowsFromFiles = async ({
|
||||||
files,
|
files,
|
||||||
|
|
@ -90,7 +90,7 @@ const useUploadFlow = () => {
|
||||||
throw new Error("Invalid flow data");
|
throw new Error("Invalid flow data");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
refreshFlows();
|
refreshFlows(undefined);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw e;
|
throw e;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import FeatureFlags from "@/../feature-config.json";
|
import FeatureFlags from "@/../feature-config.json";
|
||||||
|
import { useGetRefreshFlows } from "@/controllers/API/queries/flows/use-get-refresh-flows";
|
||||||
import { useGetGlobalVariables } from "@/controllers/API/queries/variables";
|
import { useGetGlobalVariables } from "@/controllers/API/queries/variables";
|
||||||
import useSaveFlow from "@/hooks/flows/use-save-flow";
|
import useSaveFlow from "@/hooks/flows/use-save-flow";
|
||||||
import { SaveChangesModal } from "@/modals/saveChangesModal";
|
import { SaveChangesModal } from "@/modals/saveChangesModal";
|
||||||
|
|
@ -33,7 +34,7 @@ export default function FlowPage({ view }: { view?: boolean }): JSX.Element {
|
||||||
|
|
||||||
const flows = useFlowsManagerStore((state) => state.flows);
|
const flows = useFlowsManagerStore((state) => state.flows);
|
||||||
const currentFlowId = useFlowsManagerStore((state) => state.currentFlowId);
|
const currentFlowId = useFlowsManagerStore((state) => state.currentFlowId);
|
||||||
const refreshFlows = useFlowsManagerStore((state) => state.refreshFlows);
|
const { mutateAsync: refreshFlows } = useGetRefreshFlows();
|
||||||
const setIsLoading = useFlowsManagerStore((state) => state.setIsLoading);
|
const setIsLoading = useFlowsManagerStore((state) => state.setIsLoading);
|
||||||
const getTypes = useTypesStore((state) => state.getTypes);
|
const getTypes = useTypesStore((state) => state.getTypes);
|
||||||
|
|
||||||
|
|
@ -74,7 +75,7 @@ export default function FlowPage({ view }: { view?: boolean }): JSX.Element {
|
||||||
setCurrentFlow(isAnExistingFlow);
|
setCurrentFlow(isAnExistingFlow);
|
||||||
} else if (!flows) {
|
} else if (!flows) {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
await refreshFlows();
|
await refreshFlows(undefined);
|
||||||
await getTypes();
|
await getTypes();
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { useGetRefreshFlows } from "@/controllers/API/queries/flows/use-get-refresh-flows";
|
||||||
import { useGetGlobalVariables } from "@/controllers/API/queries/variables";
|
import { useGetGlobalVariables } from "@/controllers/API/queries/variables";
|
||||||
import useFlowStore from "@/stores/flowStore";
|
import useFlowStore from "@/stores/flowStore";
|
||||||
import { useTypesStore } from "@/stores/typesStore";
|
import { useTypesStore } from "@/stores/typesStore";
|
||||||
|
|
@ -16,7 +17,7 @@ export default function ViewPage() {
|
||||||
|
|
||||||
const flows = useFlowsManagerStore((state) => state.flows);
|
const flows = useFlowsManagerStore((state) => state.flows);
|
||||||
const currentFlowId = useFlowsManagerStore((state) => state.currentFlowId);
|
const currentFlowId = useFlowsManagerStore((state) => state.currentFlowId);
|
||||||
const refreshFlows = useFlowsManagerStore((state) => state.refreshFlows);
|
const { mutateAsync: refreshFlows } = useGetRefreshFlows();
|
||||||
const setIsLoading = useFlowsManagerStore((state) => state.setIsLoading);
|
const setIsLoading = useFlowsManagerStore((state) => state.setIsLoading);
|
||||||
const getTypes = useTypesStore((state) => state.getTypes);
|
const getTypes = useTypesStore((state) => state.getTypes);
|
||||||
|
|
||||||
|
|
@ -34,7 +35,7 @@ export default function ViewPage() {
|
||||||
setCurrentFlow(isAnExistingFlow);
|
setCurrentFlow(isAnExistingFlow);
|
||||||
} else if (!flows) {
|
} else if (!flows) {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
await refreshFlows();
|
await refreshFlows(undefined);
|
||||||
await getTypes();
|
await getTypes();
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,11 @@
|
||||||
import { cloneDeep } from "lodash";
|
import { cloneDeep } from "lodash";
|
||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
import { readFlowsFromDatabase } from "../controllers/API";
|
|
||||||
import { FlowType } from "../types/flow";
|
import { FlowType } from "../types/flow";
|
||||||
import {
|
import {
|
||||||
FlowsManagerStoreType,
|
FlowsManagerStoreType,
|
||||||
UseUndoRedoOptions,
|
UseUndoRedoOptions,
|
||||||
} from "../types/zustand/flowsManager";
|
} from "../types/zustand/flowsManager";
|
||||||
import {
|
|
||||||
extractFieldsFromComponenents,
|
|
||||||
processFlows,
|
|
||||||
} from "../utils/reactflowUtils";
|
|
||||||
import useAlertStore from "./alertStore";
|
|
||||||
import useFlowStore from "./flowStore";
|
import useFlowStore from "./flowStore";
|
||||||
import { useFolderStore } from "./foldersStore";
|
|
||||||
import { useTypesStore } from "./typesStore";
|
|
||||||
|
|
||||||
const defaultOptions: UseUndoRedoOptions = {
|
const defaultOptions: UseUndoRedoOptions = {
|
||||||
maxHistorySize: 100,
|
maxHistorySize: 100,
|
||||||
|
|
@ -53,43 +45,6 @@ const useFlowsManagerStore = create<FlowsManagerStoreType>((set, get) => ({
|
||||||
setSaveLoading: (saveLoading: boolean) => set({ saveLoading }),
|
setSaveLoading: (saveLoading: boolean) => set({ saveLoading }),
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
setIsLoading: (isLoading: boolean) => set({ isLoading }),
|
setIsLoading: (isLoading: boolean) => set({ isLoading }),
|
||||||
refreshFlows: () => {
|
|
||||||
return new Promise<void>((resolve, reject) => {
|
|
||||||
const starterFolderId = useFolderStore.getState().starterProjectId;
|
|
||||||
|
|
||||||
readFlowsFromDatabase()
|
|
||||||
.then((dbData) => {
|
|
||||||
if (dbData) {
|
|
||||||
const { data, flows } = processFlows(dbData);
|
|
||||||
const examples = flows.filter(
|
|
||||||
(flow) => flow.folder_id === starterFolderId,
|
|
||||||
);
|
|
||||||
get().setExamples(examples);
|
|
||||||
|
|
||||||
const flowsWithoutStarterFolder = flows.filter(
|
|
||||||
(flow) => flow.folder_id !== starterFolderId,
|
|
||||||
);
|
|
||||||
|
|
||||||
get().setFlows(flowsWithoutStarterFolder);
|
|
||||||
useTypesStore.setState((state) => ({
|
|
||||||
data: { ...state.data, ["saved_components"]: data },
|
|
||||||
ComponentFields: extractFieldsFromComponenents({
|
|
||||||
...state.data,
|
|
||||||
["saved_components"]: data,
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
set({ isLoading: false });
|
|
||||||
useAlertStore.getState().setErrorData({
|
|
||||||
title: "Could not load flows from database",
|
|
||||||
});
|
|
||||||
reject(e);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
takeSnapshot: () => {
|
takeSnapshot: () => {
|
||||||
const currentFlowId = get().currentFlowId;
|
const currentFlowId = get().currentFlowId;
|
||||||
// push the current graph to the past state
|
// push the current graph to the past state
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@ export type FlowsManagerStoreType = {
|
||||||
setSaveLoading: (saveLoading: boolean) => void;
|
setSaveLoading: (saveLoading: boolean) => void;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
setIsLoading: (isLoading: boolean) => void;
|
setIsLoading: (isLoading: boolean) => void;
|
||||||
refreshFlows: () => Promise<void>;
|
|
||||||
undo: () => void;
|
undo: () => void;
|
||||||
redo: () => void;
|
redo: () => void;
|
||||||
takeSnapshot: () => void;
|
takeSnapshot: () => void;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue