Upload and Download Flows done, fixed deleting flow bug

This commit is contained in:
Lucas Oliveira 2023-06-13 10:51:10 -03:00
commit d76d0b2d51
9 changed files with 142 additions and 82 deletions

Binary file not shown.

View file

@ -21,12 +21,6 @@ export default function Header() {
const [rename, setRename] = useState(false); const [rename, setRename] = useState(false);
const { notificationCenter, setNotificationCenter, setErrorData } = const { notificationCenter, setNotificationCenter, setErrorData } =
useContext(alertContext); useContext(alertContext);
useEffect(() => {
//create the first flow
if (flows.length === 0 && Object.keys(templates).length > 0) {
addFlow();
}
}, [addFlow, flows.length, templates]);
return ( return (
<div className="w-full h-12 flex justify-between items-center border-b bg-muted"> <div className="w-full h-12 flex justify-between items-center border-b bg-muted">
<div className="flex gap-2 justify-start items-center w-96"> <div className="flex gap-2 justify-start items-center w-96">

View file

@ -19,11 +19,13 @@ import { typesContext } from "./typesContext";
import { APITemplateType, TemplateVariableType } from "../types/api"; import { APITemplateType, TemplateVariableType } from "../types/api";
import { v4 as uuidv4 } from "uuid"; import { v4 as uuidv4 } from "uuid";
import { addEdge } from "reactflow"; import { addEdge } from "reactflow";
import _ from "lodash"; import _, { flow } from "lodash";
import { import {
readFlowsFromDatabase, readFlowsFromDatabase,
deleteFlowFromDatabase, deleteFlowFromDatabase,
saveFlowToDatabase, saveFlowToDatabase,
downloadFlowsFromDatabase,
uploadFlowsToDatabase,
} from "../controllers/API"; } from "../controllers/API";
const TabsContextInitialValue: TabsContextType = { const TabsContextInitialValue: TabsContextType = {
@ -35,6 +37,8 @@ const TabsContextInitialValue: TabsContextType = {
updateFlow: (newFlow: FlowType) => {}, updateFlow: (newFlow: FlowType) => {},
incrementNodeId: () => uuidv4(), incrementNodeId: () => uuidv4(),
downloadFlow: (flow: FlowType) => {}, downloadFlow: (flow: FlowType) => {},
downloadFlows: () => {},
uploadFlows: () => {},
uploadFlow: () => {}, uploadFlow: () => {},
hardReset: () => {}, hardReset: () => {},
disableCopyPaste: false, disableCopyPaste: false,
@ -56,7 +60,7 @@ export const TabsContext = createContext<TabsContextType>(
export function TabsProvider({ children }: { children: ReactNode }) { export function TabsProvider({ children }: { children: ReactNode }) {
const { setErrorData, setNoticeData } = useContext(alertContext); const { setErrorData, setNoticeData } = useContext(alertContext);
const [tabId, setTabId] = useState(""); const [tabId, setTabId] = useState("");
const [flows, setFlows] = useState<Array<FlowType>>([]); const [flows, setFlows] = useState([]);
const [id, setId] = useState(uuidv4()); const [id, setId] = useState(uuidv4());
const { templates, reactFlowInstance } = useContext(typesContext); const { templates, reactFlowInstance } = useContext(typesContext);
const [lastCopiedSelection, setLastCopiedSelection] = useState(null); const [lastCopiedSelection, setLastCopiedSelection] = useState(null);
@ -114,10 +118,7 @@ export function TabsProvider({ children }: { children: ReactNode }) {
// } // }
// } // }
useEffect(() => { function refreshFlows() {
// get data from db
//get tabs locally saved
// let tabsData = getLocalStorageTabsData();
getTabsDataFromDB().then((DbData) => { getTabsDataFromDB().then((DbData) => {
if (DbData && Object.keys(templates).length > 0) { if (DbData && Object.keys(templates).length > 0) {
try { try {
@ -128,6 +129,13 @@ export function TabsProvider({ children }: { children: ReactNode }) {
} }
} }
}); });
}
useEffect(() => {
// get data from db
//get tabs locally saved
// let tabsData = getLocalStorageTabsData();
refreshFlows();
}, [templates]); }, [templates]);
function getTabsDataFromDB() { function getTabsDataFromDB() {
@ -148,20 +156,6 @@ export function TabsProvider({ children }: { children: ReactNode }) {
}); });
} }
function processTabsData(tabsData) {
tabsData.flows.forEach((flow) => {
try {
if (!flow.data) {
return;
}
processFlowEdges(flow);
processFlowNodes(flow);
} catch (e) {
console.error(e);
}
});
}
function processFlowEdges(flow) { function processFlowEdges(flow) {
flow.data.edges.forEach((edge) => { flow.data.edges.forEach((edge) => {
edge.className = ""; edge.className = "";
@ -244,6 +238,22 @@ export function TabsProvider({ children }: { children: ReactNode }) {
}); });
} }
function downloadFlows() {
downloadFlowsFromDatabase().then((flows) => {
const jsonString = `data:text/json;chatset=utf-8,${encodeURIComponent(
JSON.stringify(flows)
)}`;
// create a link element and set its properties
const link = document.createElement("a");
link.href = jsonString;
link.download = `flows.json`;
// simulate a click on the link element to trigger the download
link.click();
});
}
function getNodeId() { function getNodeId() {
return `dndnode_` + incrementNodeId(); return `dndnode_` + incrementNodeId();
} }
@ -275,30 +285,41 @@ export function TabsProvider({ children }: { children: ReactNode }) {
// trigger the file input click event to open the file dialog // trigger the file input click event to open the file dialog
input.click(); input.click();
} }
function uploadFlows() {
// create a file input
const input = document.createElement("input");
input.type = "file";
// add a change event listener to the file input
input.onchange = (e: Event) => {
// check if the file type is application/json
if ((e.target as HTMLInputElement).files[0].type === "application/json") {
// get the file from the file input
const file = (e.target as HTMLInputElement).files[0];
// read the file as text
const formData = new FormData();
formData.append("file", file);
uploadFlowsToDatabase(formData).then(() => {
refreshFlows();
});
}
};
// trigger the file input click event to open the file dialog
input.click();
}
/** /**
* Removes a flow from an array of flows based on its id. * Removes a flow from an array of flows based on its id.
* Updates the state of flows and tabIndex using setFlows and setTabIndex hooks. * Updates the state of flows and tabIndex using setFlows and setTabIndex hooks.
* @param {string} id - The id of the flow to remove. * @param {string} id - The id of the flow to remove.
*/ */
function removeFlow(id: string) { function removeFlow(id: string) {
setFlows((prevState) => { const index = flows.findIndex((flow) => flow.id === id);
const newFlows = [...prevState]; console.log(index);
const index = newFlows.findIndex((flow) => flow.id === id);
if (index >= 0) { if (index >= 0) {
deleteFlowFromDatabase(id).then(() => { deleteFlowFromDatabase(id).then(() => {
let tabIndex = flows.findIndex((flow) => flow.id === tabId); setFlows(flows.filter((flow) => flow.id !== id));
if (index === tabIndex) {
setTabId(flows[flows.length - 2].id);
newFlows.splice(index, 1);
} else {
let flowId = flows[tabIndex].id;
newFlows.splice(index, 1);
setTabId(flowId);
}
}); });
} }
return newFlows;
});
} }
/** /**
* Add a new flow to the list of flows. * Add a new flow to the list of flows.
@ -510,6 +531,8 @@ export function TabsProvider({ children }: { children: ReactNode }) {
addFlow, addFlow,
updateFlow, updateFlow,
downloadFlow, downloadFlow,
downloadFlows,
uploadFlows,
uploadFlow, uploadFlow,
getNodeId, getNodeId,
paste, paste,

View file

@ -148,6 +148,35 @@ export async function readFlowsFromDatabase() {
} }
} }
export async function downloadFlowsFromDatabase() {
try {
const response = await fetch("/flows/download/");
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
} catch (error) {
console.error(error);
throw error;
}
}
export async function uploadFlowsToDatabase(flows) {
try {
const response = await fetch(`/flows/upload/`, {
method: "POST", // Or "PATCH" depending on your backend API
body: flows,
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
} catch (error) {
console.error(error);
throw error;
}
}
/** /**
* Deletes a flow from the database. * Deletes a flow from the database.
* *
@ -198,7 +227,7 @@ export async function getFlowFromDatabase(flowId: number) {
*/ */
export async function getFlowStylesFromDatabase() { export async function getFlowStylesFromDatabase() {
try { try {
const response = await fetch("/flows_styles/"); const response = await fetch("/flow_styles/");
if (!response.ok) { if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`); throw new Error(`HTTP error! status: ${response.status}`);
} }
@ -218,7 +247,7 @@ export async function getFlowStylesFromDatabase() {
*/ */
export async function saveFlowStyleToDatabase(flowStyle: FlowStyleType) { export async function saveFlowStyleToDatabase(flowStyle: FlowStyleType) {
try { try {
const response = await fetch("/flows_styles/", { const response = await fetch("/flow_styles/", {
method: "POST", method: "POST",
headers: { headers: {
accept: "application/json", accept: "application/json",

View file

@ -30,7 +30,7 @@ export default function ExportModal() {
const { closePopUp } = useContext(PopUpContext); const { closePopUp } = useContext(PopUpContext);
const ref = useRef(); const ref = useRef();
const { setErrorData } = useContext(alertContext); const { setErrorData } = useContext(alertContext);
const { flows, tabIndex, updateFlow, downloadFlow } = useContext(TabsContext); const { flows, tabId, updateFlow, downloadFlow } = useContext(TabsContext);
function setModalOpen(x: boolean) { function setModalOpen(x: boolean) {
setOpen(x); setOpen(x);
if (x === false) { if (x === false) {
@ -40,7 +40,7 @@ export default function ExportModal() {
} }
} }
const [checked, setChecked] = useState(true); const [checked, setChecked] = useState(true);
const [name, setName] = useState(flows[tabIndex].name); const [name, setName] = useState(flows.find((f) => f.id === tabId).name);
return ( return (
<Dialog open={true} onOpenChange={setModalOpen}> <Dialog open={true} onOpenChange={setModalOpen}>
<DialogTrigger asChild></DialogTrigger> <DialogTrigger asChild></DialogTrigger>
@ -63,7 +63,7 @@ export default function ExportModal() {
className="mt-2" className="mt-2"
onChange={(event) => { onChange={(event) => {
if (event.target.value != "") { if (event.target.value != "") {
let newFlow = flows[tabIndex]; let newFlow = flows.find((f) => f.id === tabId);
newFlow.name = event.target.value; newFlow.name = event.target.value;
setName(event.target.value); setName(event.target.value);
updateFlow(newFlow); updateFlow(newFlow);
@ -84,11 +84,11 @@ export default function ExportModal() {
name="description" name="description"
id="description" id="description"
onChange={(event) => { onChange={(event) => {
let newFlow = flows[tabIndex]; let newFlow = flows.find((f) => f.id === tabId);
newFlow.description = event.target.value; newFlow.description = event.target.value;
updateFlow(newFlow); updateFlow(newFlow);
}} }}
value={flows[tabIndex].description ?? null} value={flows.find((f) => f.id === tabId).description ?? null}
placeholder="Flow description" placeholder="Flow description"
className="max-h-[100px] mt-2" className="max-h-[100px] mt-2"
rows={3} rows={3}
@ -112,8 +112,8 @@ export default function ExportModal() {
<DialogFooter> <DialogFooter>
<Button <Button
onClick={() => { onClick={() => {
if (checked) downloadFlow(flows[tabIndex]); if (checked) downloadFlow(flows.find((f) => f.id === tabId));
else downloadFlow(removeApiKeys(flows[tabIndex])); else downloadFlow(removeApiKeys(flows.find((f) => f.id === tabId)));
}} }}
type="submit" type="submit"
> >

View file

@ -51,23 +51,7 @@ export default function ExtraSidebar() {
return ( return (
<div className="flex flex-col overflow-auto scrollbar-hide h-full border-r"> <div className="flex flex-col overflow-auto scrollbar-hide h-full border-r">
<div className="relative mt-2 flex items-center mb-2 mx-2"> <div className="mt-2 w-full flex gap-2 justify-between px-2 items-center">
<input
type="text"
name="search"
id="search"
placeholder="Search nodes"
className="dark:text-white focus:outline-none block w-full rounded-md py-1.5 ps-3 pr-9 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 sm:text-sm sm:leading-6 dark:ring-0 dark:bg-[#2d3747] dark:focus:outline-none"
onChange={(e) => {
handleSearchInput(e.target.value);
setSearch(e.target.value);
}}
/>
<div className="absolute inset-y-0 right-0 flex py-1.5 pr-3 items-center">
<MagnifyingGlassIcon className="h-5 w-5 dark:text-white"></MagnifyingGlassIcon>
</div>
</div>
<div className="mb-2 w-full flex gap-2 justify-between px-2 items-center">
<ShadTooltip delayDuration={1000} content="Import" side="top"> <ShadTooltip delayDuration={1000} content="Import" side="top">
<button <button
className="hover:dark:hover:bg-[#242f47] text-gray-700 w-full justify-center shadow-sm transition-all duration-500 ease-in-out dark:bg-gray-800 dark:text-gray-300 relative inline-flex items-center rounded-md bg-white px-2 py-2 ring-1 ring-inset ring-gray-300 hover:bg-gray-50" className="hover:dark:hover:bg-[#242f47] text-gray-700 w-full justify-center shadow-sm transition-all duration-500 ease-in-out dark:bg-gray-800 dark:text-gray-300 relative inline-flex items-center rounded-md bg-white px-2 py-2 ring-1 ring-inset ring-gray-300 hover:bg-gray-50"
@ -110,6 +94,23 @@ export default function ExtraSidebar() {
</button> </button>
</ShadTooltip> </ShadTooltip>
</div> </div>
<div className="relative mt-2 flex items-center mb-2 mx-2">
<input
type="text"
name="search"
id="search"
placeholder="Search nodes"
className="dark:text-white focus:outline-none block w-full rounded-md py-1.5 ps-3 pr-9 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 sm:text-sm sm:leading-6 dark:ring-0 dark:bg-[#2d3747] dark:focus:outline-none"
onChange={(e) => {
handleSearchInput(e.target.value);
setSearch(e.target.value);
}}
/>
<div className="absolute inset-y-0 right-0 flex py-1.5 pr-3 items-center">
<MagnifyingGlassIcon className="h-5 w-5 dark:text-white"></MagnifyingGlassIcon>
</div>
</div>
<div className="w-full"> <div className="w-full">

View file

@ -24,16 +24,12 @@ import { Link } from "react-router-dom";
export const CardComponent = ({ export const CardComponent = ({
flow, flow,
id, id,
removeFlow,
setTabId,
}: { }: {
flow: FlowType; flow: FlowType;
id: string; id: string;
removeFlow: (id: string) => void;
setTabId: (id: string) => void;
}) => { }) => {
const { setErrorData } = useContext(alertContext); const { setErrorData } = useContext(alertContext);
const { updateFlow } = useContext(TabsContext); const { updateFlow, removeFlow, setTabId } = useContext(TabsContext);
function handleSaveFlow(flow) { function handleSaveFlow(flow) {
try { try {
updateFlowInDatabase(flow); updateFlowInDatabase(flow);
@ -73,12 +69,11 @@ export const CardComponent = ({
}} }}
/> />
</Link> </Link>
<button onClick={() => {removeFlow(flow.id)}}>
<Trash <Trash
className="w-4" className="w-4"
onClick={() => {
removeFlow(flow.id);
}}
/> />
</button>
</div> </div>
</CardTitle> </CardTitle>
<CardDescription className="pt-2 pb-2 h-10"> <CardDescription className="pt-2 pb-2 h-10">

View file

@ -8,7 +8,7 @@ import ExtraSidebar from "../../components/ExtraSidebarComponent";
import { ReactFlowProvider } from "reactflow"; import { ReactFlowProvider } from "reactflow";
import FlowPage from "../FlowPage"; import FlowPage from "../FlowPage";
import { useContext, useEffect, useState } from "react"; import { useContext, useEffect, useState } from "react";
import { SunIcon, MoonIcon, BellIcon, GithubIcon } from "lucide-react"; import { SunIcon, MoonIcon, BellIcon, GithubIcon, Download, Upload } from "lucide-react";
import { TabsContext } from "../../contexts/tabsContext"; import { TabsContext } from "../../contexts/tabsContext";
import AlertDropdown from "../../alerts/alertDropDown"; import AlertDropdown from "../../alerts/alertDropDown";
import { alertContext } from "../../contexts/alertContext"; import { alertContext } from "../../contexts/alertContext";
@ -20,14 +20,15 @@ import { FaGithub } from "react-icons/fa";
import _ from "lodash"; import _ from "lodash";
import { updateFlowInDatabase } from "../../controllers/API"; import { updateFlowInDatabase, uploadFlowsToDatabase } from "../../controllers/API";
import { CardComponent } from "./components/cardComponent"; import { CardComponent } from "./components/cardComponent";
import { MenuBar } from "../../components/headerComponent/components/menuBar"; import { MenuBar } from "../../components/headerComponent/components/menuBar";
export default function HomePage() { export default function HomePage() {
const { const {
flows, flows,
removeFlow,
setTabId, setTabId,
downloadFlows,
uploadFlows,
} = useContext(TabsContext); } = useContext(TabsContext);
useEffect(() => { useEffect(() => {
setTabId(""); setTabId("");
@ -36,13 +37,28 @@ export default function HomePage() {
<div <div
className="w-full h-full flex overflow-auto flex-col bg-muted" className="w-full h-full flex overflow-auto flex-col bg-muted"
> >
<div className="w-full flex justify-between py-12 px-8">
<span className="text-xl font-semibold">
Flows
</span>
<div className="flex gap-2">
<Button variant="outline" onClick={() => {
downloadFlows();
}}>
<Download className="w-4 mr-2" />
Download Database
</Button>
<Button variant="outline" onClick={() => {uploadFlows();}}>
<Upload className="w-4 mr-2" />
Upload Database
</Button>
</div>
</div>
<div className="w-full p-4 grid gap-4 md:grid-cols-2 lg:grid-cols-4"> <div className="w-full p-4 grid gap-4 md:grid-cols-2 lg:grid-cols-4">
{Object.keys(flows).map((flow, idx) => ( {flows.map((flow, idx) => (
<CardComponent <CardComponent
flow={flows[flow]} flow={flow}
id={flow} id={flow.id}
removeFlow={removeFlow}
setTabId={setTabId}
/> />
))} ))}
</div> </div>

View file

@ -9,6 +9,8 @@ export type TabsContextType = {
updateFlow: (newFlow: FlowType) => void; updateFlow: (newFlow: FlowType) => void;
incrementNodeId: () => string; incrementNodeId: () => string;
downloadFlow: (flow: FlowType) => void; downloadFlow: (flow: FlowType) => void;
downloadFlows: () => void;
uploadFlows: () => void;
uploadFlow: (newFlow?: boolean) => void; uploadFlow: (newFlow?: boolean) => void;
hardReset: () => void; hardReset: () => void;
//disable CopyPaste //disable CopyPaste