Auto update nodes (#218)
This new feature will ensure that any changes made to a template will be automatically reflected in all nodes related to that template. This will save time and effort, as users will no longer need to manually update each node individually. Additionally, this feature will help to reduce the risk of errors and inconsistencies across nodes and flows. To implement this feature, I have created a new function that checks the last template related to each node and compares it with the current template version. If there is a difference, the node will be automatically updated with the latest version of the template.
This commit is contained in:
commit
ed312f1011
10 changed files with 162 additions and 69 deletions
|
|
@ -11,15 +11,15 @@ export default function ContextWrapper({ children }: { children: ReactNode }) {
|
|||
return (
|
||||
<>
|
||||
<DarkProvider>
|
||||
<LocationProvider>
|
||||
<TypesProvider>
|
||||
<TypesProvider>
|
||||
<LocationProvider>
|
||||
<AlertProvider>
|
||||
<TabsProvider>
|
||||
<PopUpProvider>{children}</PopUpProvider>
|
||||
</TabsProvider>
|
||||
</AlertProvider>
|
||||
</TypesProvider>
|
||||
</LocationProvider>
|
||||
</LocationProvider>
|
||||
</TypesProvider>
|
||||
</DarkProvider>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,11 +1,20 @@
|
|||
import { createContext, useEffect, useState, useRef, ReactNode, useContext } from "react";
|
||||
import {
|
||||
createContext,
|
||||
useEffect,
|
||||
useState,
|
||||
useRef,
|
||||
ReactNode,
|
||||
useContext,
|
||||
} from "react";
|
||||
import { FlowType } from "../types/flow";
|
||||
import { TabsContextType } from "../types/tabs";
|
||||
import { normalCaseToSnakeCase } from "../utils";
|
||||
import { LangFlowState, TabsContextType } from "../types/tabs";
|
||||
import { normalCaseToSnakeCase, updateObject } from "../utils";
|
||||
import { alertContext } from "./alertContext";
|
||||
import { typesContext } from "./typesContext";
|
||||
import { TemplateVariableType } from "../types/api";
|
||||
|
||||
const TabsContextInitialValue: TabsContextType = {
|
||||
save:()=>{},
|
||||
save: () => {},
|
||||
tabIndex: 0,
|
||||
setTabIndex: (index: number) => {},
|
||||
flows: [],
|
||||
|
|
@ -13,11 +22,11 @@ const TabsContextInitialValue: TabsContextType = {
|
|||
addFlow: (flowData?: any) => {},
|
||||
updateFlow: (newFlow: FlowType) => {},
|
||||
incrementNodeId: () => 0,
|
||||
downloadFlow: (flow:FlowType) => {},
|
||||
downloadFlow: (flow: FlowType) => {},
|
||||
uploadFlow: () => {},
|
||||
lockChat: false,
|
||||
setLockChat:(prevState:boolean)=>{},
|
||||
hardReset:()=>{},
|
||||
setLockChat: (prevState: boolean) => {},
|
||||
hardReset: () => {},
|
||||
};
|
||||
|
||||
export const TabsContext = createContext<TabsContextType>(
|
||||
|
|
@ -25,51 +34,65 @@ export const TabsContext = createContext<TabsContextType>(
|
|||
);
|
||||
|
||||
export function TabsProvider({ children }: { children: ReactNode }) {
|
||||
const {setNoticeData} = useContext(alertContext)
|
||||
const { setNoticeData } = useContext(alertContext);
|
||||
const [tabIndex, setTabIndex] = useState(0);
|
||||
const [flows, setFlows] = useState<Array<FlowType>>([]);
|
||||
const [id, setId] = useState(0);
|
||||
const [lockChat, setLockChat] = useState(false);
|
||||
const { templates } = useContext(typesContext);
|
||||
|
||||
const newNodeId = useRef(0);
|
||||
function incrementNodeId() {
|
||||
newNodeId.current = newNodeId.current + 1;
|
||||
return newNodeId.current;
|
||||
}
|
||||
function save(){
|
||||
function save() {
|
||||
console.log("save");
|
||||
if (flows.length !== 0)
|
||||
window.localStorage.setItem(
|
||||
"tabsData",
|
||||
JSON.stringify({ tabIndex, flows, id, nodeId: newNodeId.current })
|
||||
);
|
||||
window.localStorage.setItem(
|
||||
"tabsData",
|
||||
JSON.stringify({ tabIndex, flows, id, nodeId: newNodeId.current })
|
||||
);
|
||||
}
|
||||
useEffect(() => {
|
||||
//save tabs locally
|
||||
save()
|
||||
save();
|
||||
}, [flows, id, tabIndex, newNodeId]);
|
||||
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
//get tabs locally saved
|
||||
let cookie = window.localStorage.getItem("tabsData");
|
||||
if (cookie) {
|
||||
let cookieObject = JSON.parse(cookie);
|
||||
if (cookie && Object.keys(templates).length > 0) {
|
||||
let cookieObject: LangFlowState = JSON.parse(cookie);
|
||||
cookieObject.flows.forEach((flow) => {
|
||||
flow.data.nodes.forEach((node) => {
|
||||
if (Object.keys(templates[node.data.type]["template"]).length>0) {
|
||||
node.data.node.template = updateObject(
|
||||
node.data.node.template as TemplateVariableType,
|
||||
templates[node.data.type][
|
||||
"template"
|
||||
] as unknown as TemplateVariableType
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
setTabIndex(cookieObject.tabIndex);
|
||||
setFlows(cookieObject.flows);
|
||||
setId(cookieObject.id);
|
||||
newNodeId.current = cookieObject.nodeId;
|
||||
}
|
||||
}, []);
|
||||
function hardReset(){
|
||||
newNodeId.current=0;
|
||||
setTabIndex(0);setFlows([]);setId(0);
|
||||
}, [templates]);
|
||||
function hardReset() {
|
||||
newNodeId.current = 0;
|
||||
setTabIndex(0);
|
||||
setFlows([]);
|
||||
setId(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads the current flow as a JSON file
|
||||
*/
|
||||
function downloadFlow(flow:FlowType) {
|
||||
function downloadFlow(flow: FlowType) {
|
||||
// create a data URI with the current flow data
|
||||
const jsonString = `data:text/json;chatset=utf-8,${encodeURIComponent(
|
||||
JSON.stringify(flow)
|
||||
|
|
@ -82,7 +105,9 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
|||
|
||||
// simulate a click on the link element to trigger the download
|
||||
link.click();
|
||||
setNoticeData({title:"Warning: Critical data,JSON file may including API keys."})
|
||||
setNoticeData({
|
||||
title: "Warning: Critical data,JSON file may including API keys.",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -103,7 +128,19 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
|||
// read the file as text
|
||||
file.text().then((text) => {
|
||||
// parse the text into a JSON object
|
||||
addFlow(JSON.parse(text));
|
||||
let flow: FlowType = JSON.parse(text);
|
||||
flow.data.nodes.forEach((node) => {
|
||||
if (Object.keys(templates[node.data.type]["template"]).length>0) {
|
||||
node.data.node.template = updateObject(
|
||||
node.data.node.template as TemplateVariableType,
|
||||
templates[node.data.type][
|
||||
"template"
|
||||
] as unknown as TemplateVariableType
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
addFlow();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -139,7 +176,7 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
|||
function addFlow(flow?: FlowType) {
|
||||
// Get data from the flow or set it to null if there's no flow provided.
|
||||
const data = flow?.data ? flow.data : null;
|
||||
const description = flow?.description?flow.description:""
|
||||
const description = flow?.description ? flow.description : "";
|
||||
|
||||
// Create a new flow with a default name if no flow is provided.
|
||||
let newFlow: FlowType = {
|
||||
|
|
@ -171,7 +208,7 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
|||
const newFlows = [...prevState];
|
||||
const index = newFlows.findIndex((flow) => flow.id === newFlow.id);
|
||||
if (index !== -1) {
|
||||
newFlows[index].description = newFlow.description??""
|
||||
newFlows[index].description = newFlow.description ?? "";
|
||||
newFlows[index].data = newFlow.data;
|
||||
newFlows[index].name = newFlow.name;
|
||||
newFlows[index].chat = newFlow.chat;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import { createContext, ReactNode, useState } from "react";
|
||||
import { createContext, ReactNode, useEffect, useState } from "react";
|
||||
import { Node} from "reactflow";
|
||||
import { typesContextType } from "../types/typesContext";
|
||||
import { getAll } from "../controllers/API";
|
||||
import { APIKindType } from "../types/api";
|
||||
|
||||
//context to share types adn functions from nodes to flow
|
||||
|
||||
|
|
@ -10,6 +12,10 @@ const initialValue:typesContextType = {
|
|||
deleteNode: () => {},
|
||||
types: {},
|
||||
setTypes: () => {},
|
||||
templates: {},
|
||||
setTemplates: () => {},
|
||||
data:{},
|
||||
setData:()=>{}
|
||||
};
|
||||
|
||||
export const typesContext = createContext<typesContextType>(initialValue);
|
||||
|
|
@ -17,6 +23,42 @@ export const typesContext = createContext<typesContextType>(initialValue);
|
|||
export function TypesProvider({ children }:{children:ReactNode}) {
|
||||
const [types, setTypes] = useState({});
|
||||
const [reactFlowInstance, setReactFlowInstance] = useState(null);
|
||||
const [templates, setTemplates] = useState({});
|
||||
const [data, setData] = useState({});
|
||||
|
||||
useEffect(() => {
|
||||
async function getTypes(): Promise<void> {
|
||||
// Make an asynchronous API call to retrieve all data.
|
||||
let result = await getAll();
|
||||
|
||||
// Update the state of the component with the retrieved data.
|
||||
setData(result.data);
|
||||
setTemplates(
|
||||
Object.keys(result.data).reduce((acc, curr) => {
|
||||
Object.keys(result.data[curr]).forEach((c: keyof APIKindType)=>{
|
||||
acc[c] = result.data[curr][c]
|
||||
})
|
||||
return acc;
|
||||
},{})
|
||||
);
|
||||
// Set the types by reducing over the keys of the result data and updating the accumulator.
|
||||
setTypes(
|
||||
Object.keys(result.data).reduce((acc, curr) => {
|
||||
Object.keys(result.data[curr]).forEach((c: keyof APIKindType) => {
|
||||
acc[c] = curr;
|
||||
// Add the base classes to the accumulator as well.
|
||||
result.data[curr][c].base_classes?.forEach((b) => {
|
||||
acc[b] = curr;
|
||||
});
|
||||
});
|
||||
return acc;
|
||||
}, {})
|
||||
);
|
||||
}
|
||||
// Call the getTypes function.
|
||||
getTypes();
|
||||
}, [setTypes]);
|
||||
|
||||
function deleteNode(idx:string) {
|
||||
reactFlowInstance.setNodes(
|
||||
reactFlowInstance.getNodes().filter((n:Node) => n.id !== idx)
|
||||
|
|
@ -31,6 +73,10 @@ export function TypesProvider({ children }:{children:ReactNode}) {
|
|||
reactFlowInstance,
|
||||
setReactFlowInstance,
|
||||
deleteNode,
|
||||
setTemplates,
|
||||
templates,
|
||||
data,
|
||||
setData
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
|
|
|||
|
|
@ -2,43 +2,14 @@ import { Bars2Icon } from "@heroicons/react/24/outline";
|
|||
import DisclosureComponent from "../DisclosureComponent";
|
||||
import { nodeColors, nodeIcons, nodeNames } from "../../../../utils";
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { getAll } from "../../../../controllers/API";
|
||||
import { typesContext } from "../../../../contexts/typesContext";
|
||||
import {
|
||||
APIClassType,
|
||||
APIKindType,
|
||||
APIObjectType,
|
||||
} from "../../../../types/api";
|
||||
|
||||
export default function ExtraSidebar() {
|
||||
const [data, setData] = useState({});
|
||||
const { setTypes } = useContext(typesContext);
|
||||
|
||||
useEffect(() => {
|
||||
async function getTypes(): Promise<void> {
|
||||
// Make an asynchronous API call to retrieve all data.
|
||||
let result = await getAll();
|
||||
|
||||
// Update the state of the component with the retrieved data.
|
||||
setData(result.data);
|
||||
|
||||
// Set the types by reducing over the keys of the result data and updating the accumulator.
|
||||
setTypes(
|
||||
Object.keys(result.data).reduce((acc, curr) => {
|
||||
Object.keys(result.data[curr]).forEach((c: keyof APIKindType) => {
|
||||
acc[c] = curr;
|
||||
// Add the base classes to the accumulator as well.
|
||||
result.data[curr][c].base_classes?.forEach((b) => {
|
||||
acc[b] = curr;
|
||||
});
|
||||
});
|
||||
return acc;
|
||||
}, {})
|
||||
);
|
||||
}
|
||||
// Call the getTypes function.
|
||||
getTypes();
|
||||
}, [setTypes]);
|
||||
const {data} = useContext(typesContext)
|
||||
|
||||
function onDragStart(
|
||||
event: React.DragEvent<any>,
|
||||
|
|
@ -84,7 +55,9 @@ export default function ExtraSidebar() {
|
|||
</div>
|
||||
</div>
|
||||
))}
|
||||
{Object.keys(data[d]).length===0 && <div className="text-gray-400 text-center">Coming soon</div>}
|
||||
{Object.keys(data[d]).length === 0 && (
|
||||
<div className="text-gray-400 text-center">Coming soon</div>
|
||||
)}
|
||||
</div>
|
||||
</DisclosureComponent>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -16,21 +16,23 @@ import AlertDropdown from "../../../../alerts/alertDropDown";
|
|||
import { alertContext } from "../../../../contexts/alertContext";
|
||||
import ImportModal from "../../../../modals/importModal";
|
||||
import ExportModal from "../../../../modals/exportModal";
|
||||
import { typesContext } from "../../../../contexts/typesContext";
|
||||
|
||||
export default function TabsManagerComponent() {
|
||||
const { flows, addFlow, tabIndex, setTabIndex, uploadFlow, downloadFlow } =
|
||||
useContext(TabsContext);
|
||||
const { openPopUp } = useContext(PopUpContext);
|
||||
const {templates} = useContext(typesContext)
|
||||
const AlertWidth = 256;
|
||||
const { dark, setDark } = useContext(darkContext);
|
||||
const { notificationCenter, setNotificationCenter } =
|
||||
useContext(alertContext);
|
||||
useEffect(() => {
|
||||
//create the first flow
|
||||
if (flows.length === 0) {
|
||||
if (flows.length === 0&& Object.keys(templates).length>0) {
|
||||
addFlow();
|
||||
}
|
||||
}, [addFlow, flows.length]);
|
||||
}, [addFlow, flows.length,templates]);
|
||||
|
||||
return (
|
||||
<div className="h-full w-full flex flex-col">
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ var _ = require("lodash");
|
|||
export default function FlowPage({ flow }:{flow:FlowType}) {
|
||||
let { updateFlow, incrementNodeId} =
|
||||
useContext(TabsContext);
|
||||
const { types, reactFlowInstance, setReactFlowInstance } =
|
||||
const { types, reactFlowInstance, setReactFlowInstance, templates } =
|
||||
useContext(typesContext);
|
||||
const reactFlowWrapper = useRef(null);
|
||||
|
||||
|
|
@ -180,7 +180,7 @@ export default function FlowPage({ flow }:{flow:FlowType}) {
|
|||
|
||||
return (
|
||||
<div className="w-full h-full" ref={reactFlowWrapper}>
|
||||
{Object.keys(types).length > 0 ? (
|
||||
{Object.keys(templates).length > 0 && Object.keys(types).length > 0 ? (
|
||||
<>
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
|
|
|
|||
|
|
@ -14,4 +14,6 @@ export type TabsContextType = {
|
|||
lockChat:boolean;
|
||||
setLockChat:(prevState:boolean)=>void;
|
||||
hardReset:()=>void;
|
||||
};
|
||||
};
|
||||
|
||||
export type LangFlowState={ tabIndex:number, flows:FlowType[], id:number, nodeId:number }
|
||||
7
src/frontend/src/types/templatesContext/index.ts
Normal file
7
src/frontend/src/types/templatesContext/index.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
|
||||
const template:{[char: string]: string}={}
|
||||
|
||||
export type TemplateContextType = {
|
||||
templates: typeof template;
|
||||
setTemplates: (newState: {}) => void;
|
||||
};
|
||||
|
|
@ -1,6 +1,9 @@
|
|||
import { ReactFlowInstance } from "reactflow";
|
||||
|
||||
const types:{[char: string]: string}={}
|
||||
const types:{[char: string]: string}={};
|
||||
const template:{[char: string]: string}={}
|
||||
const data:{[char: string]: string}={}
|
||||
|
||||
|
||||
export type typesContextType = {
|
||||
reactFlowInstance: ReactFlowInstance|null;
|
||||
|
|
@ -8,4 +11,8 @@ export type typesContextType = {
|
|||
deleteNode: (idx: string) => void;
|
||||
types: typeof types;
|
||||
setTypes: (newState: {}) => void;
|
||||
templates: typeof template;
|
||||
setTemplates: (newState: {}) => void;
|
||||
data: typeof data;
|
||||
setData: (newState: {}) => void;
|
||||
};
|
||||
|
|
@ -387,4 +387,23 @@ export function removeApiKeys(flow:FlowType):FlowType{
|
|||
}
|
||||
})
|
||||
return cleanFLow
|
||||
}
|
||||
|
||||
export function updateObject<T extends Record<string, any>>(reference: T, objectToUpdate: T): T {
|
||||
let clonedObject = _.cloneDeep(objectToUpdate)
|
||||
// Loop through each key in the object to update
|
||||
for (const key in clonedObject) {
|
||||
// If the key is not in the reference object, delete it
|
||||
if (!(key in reference)) {
|
||||
delete clonedObject[key];
|
||||
}
|
||||
}
|
||||
// Loop through each key in the reference object
|
||||
for (const key in reference) {
|
||||
// If the key is not in the object to update, add it
|
||||
if (!(key in clonedObject)) {
|
||||
clonedObject[key] = reference[key];
|
||||
}
|
||||
}
|
||||
return clonedObject;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue