refactor: optimize flow saving functionality and implement manual saving (#3283)

* fixed patch update flow

* fixed update flow patch to receive id by payload

* created save flow hook with auto save and manual save functions

* fix poetry lock

* added auto save check with environment variable

* removed unused user

* separated autosave and put the flow as a creation with nodes and edges

* removed set nodes that skipped saving

* implemented auto save hook

* removed autosave from setNodes and setEdges

* added auto save hook and saved on viewport move and added useEffect to save on nodes and edges changed

* changed type of setNodes

* removed unused var

* removed deletion of empty flow

* Added saving of flow on button when autoSave is disabled

* disable saving when the nodes are empty

* removed save loading as false when the access token is renewed

* implemented useDebounce

* added save loading to save flow hook

* removed setting nodes and edges on fetching, since they are set when the current flow is updated

* removed unused var

* use debounce hook to save flow

* set nodes and edges on current flow id change

* removed useplaygroundeffect

* removed unused import

* put set save loading before the If

* removed flow cleaning and inputs setting, since the inputs and outputs are set on the ResetFlow function

* updated to use ResetFlow function to update everything regarding flow

* removed flow pool get on resetFlow, for it to be fetched only if the user is inside the flow

* updated packagelock

* Changed router to outlet on app.tsx to use createRouter

* Created authSettingsGuard to guard the general settings

* Fixed routes to use createBrowserRouter to allow the use of useBlocker

* Changed index.tsx to use RouterProvider and the router just created

* Changed flowStore to have a local flow state

* Implemented setting the current flow state when saving the flow

* Added the update of current flow when auto saving

* changed current flow to use the current flow from Flow Store instead of Flows Manager Store

* Changed codeTabsComponent Tweaks check to show if its checked

* Removed unused variables

* Removed browser router from context wrapper

* Removed unused console.log

* Changed initialSetup to just run when opening the modal

* changed confirmationModal to have destructiveCancel and to only call onCancel if the other buttons were not pressed

* Created a SaveChangesModal that confirms if the user wants to save their changes

* Get folder by id when folder id changes too

* Changed reset flow calls to store whole flow

* Added check if user is exiting page to prevent him when there are unsaved changes

* Added new types on ConfirmationModalType

* Implement save on clicking the save button on the header

* added save component shortcut to use save shortcut as save flow

* added save component shortcut on shortcutsStore type

* changed save shortcut to save component on node toolbar

* added save shortcut to header menubar

* changed shortcuts name to be compatible with existing ones

* changed shortcuts to be backwards compatible

* changed save to changes to maintain retrocompatibility

* changed save_component to save to maintain retrocompatibility

* Changed time difference to unsaved changes

* changed the toolbar select item to get the right save shortcut

* Changed save flow to use current flow from useFlowStore instead of the previous saved flow

* changed changesNotSaved to include flow name and metadata

* Added way of saving the flow settings just locally instead of directly to database

* Changed shareModal to save flow with hook

* removed old auto saving on connect

* Removed save functions from flowsManagerStore

* refactor: Remove unused imports and state variables in EditFlowSettings component

* use current flow not saved one and refactored page to not receive flow

* added check of isFlowPage to display the menubar

* Added checks to render playground if API key is valid and if Flows exists

* Added check to not display X on chat on playground page

* Updated flows variable to be undefined by start to prevent things from loading before flows initialize

* Implemented log builds parameter to not allow the builds to be logged if user not on flowPage
This commit is contained in:
Lucas Oliveira 2024-08-12 19:21:52 -03:00 • committed by GitHub
commit 7264028e41
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
62 changed files with 734 additions and 790 deletions

View file

@ -1079,7 +1079,6 @@
}, },
"node_modules/@clack/prompts/node_modules/is-unicode-supported": { "node_modules/@clack/prompts/node_modules/is-unicode-supported": {
"version": "1.3.0", "version": "1.3.0",
"extraneous": true,
"inBundle": true, "inBundle": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {

View file

@ -1,6 +1,7 @@
import { useContext, useEffect } from "react"; import { useContext, useEffect } from "react";
import { Cookies } from "react-cookie"; import { Cookies } from "react-cookie";
import { ErrorBoundary } from "react-error-boundary"; import { ErrorBoundary } from "react-error-boundary";
import { Outlet } from "react-router-dom";
import "reactflow/dist/style.css"; import "reactflow/dist/style.css";
import "./App.css"; import "./App.css";
import AlertDisplayArea from "./alerts/displayArea"; import AlertDisplayArea from "./alerts/displayArea";
@ -22,7 +23,6 @@ import { useGetHealthQuery } from "./controllers/API/queries/health";
import { useGetVersionQuery } from "./controllers/API/queries/version"; import { useGetVersionQuery } from "./controllers/API/queries/version";
import { setupAxiosDefaults } from "./controllers/API/utils"; import { setupAxiosDefaults } from "./controllers/API/utils";
import useTrackLastVisitedPath from "./hooks/use-track-last-visited-path"; import useTrackLastVisitedPath from "./hooks/use-track-last-visited-path";
import Router from "./routes";
import useAlertStore from "./stores/alertStore"; import useAlertStore from "./stores/alertStore";
import useAuthStore from "./stores/authStore"; import useAuthStore from "./stores/authStore";
import { useDarkStore } from "./stores/darkStore"; import { useDarkStore } from "./stores/darkStore";
@ -172,8 +172,7 @@ export default function App() {
> >
<LoadingComponent remSize={50} /> <LoadingComponent remSize={50} />
</div> </div>
<Outlet />
<Router />
</> </>
</ErrorBoundary> </ErrorBoundary>
<div></div> <div></div>

View file

@ -1,17 +1,5 @@
import { cloneDeep } from "lodash";
import { useUpdateNodeInternals } from "reactflow";
import ForwardedIconComponent from "../../../../components/genericIconComponent";
import ShadTooltip from "../../../../components/shadTooltipComponent"; import ShadTooltip from "../../../../components/shadTooltipComponent";
import { Button } from "../../../../components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "../../../../components/ui/dropdown-menu";
import useFlowStore from "../../../../stores/flowStore";
import { outputComponentType } from "../../../../types/components"; import { outputComponentType } from "../../../../types/components";
import { NodeDataType } from "../../../../types/flow";
import { cn } from "../../../../utils/utils"; import { cn } from "../../../../utils/utils";
export default function OutputComponent({ export default function OutputComponent({
@ -23,9 +11,6 @@ export default function OutputComponent({
name, name,
proxy, proxy,
}: outputComponentType) { }: outputComponentType) {
const setNode = useFlowStore((state) => state.setNode);
const updateNodeInternals = useUpdateNodeInternals();
const displayProxy = (children) => { const displayProxy = (children) => {
if (proxy) { if (proxy) {
return ( return (

View file

@ -289,7 +289,6 @@ export default function GenericNode({
function handlePlayWShortcut() { function handlePlayWShortcut() {
if (buildStatus === BuildStatus.BUILDING || isBuilding || !selected) return; if (buildStatus === BuildStatus.BUILDING || isBuilding || !selected) return;
setValidationStatus(null); setValidationStatus(null);
console.log(data.node?.display_name);
buildFlow({ stopNodeId: data.id }); buildFlow({ stopNodeId: data.id });
} }

View file

@ -0,0 +1,19 @@
import FeatureFlags from "@/../feature-config.json";
import useAuthStore from "@/stores/authStore";
import { useStoreStore } from "@/stores/storeStore";
import { Navigate } from "react-router-dom";
export const AuthSettingsGuard = ({ children }) => {
const autoLogin = useAuthStore((state) => state.autoLogin);
const hasStore = useStoreStore((state) => state.hasStore);
// Hides the General settings if there is nothing to show
const showGeneralSettings =
FeatureFlags.ENABLE_PROFILE_ICONS || hasStore || !autoLogin;
if (showGeneralSettings) {
return children;
} else {
return <Navigate replace to="global-variables" />;
}
};

View file

@ -1,27 +0,0 @@
import { useEffect } from "react";
import { FlowType } from "../../../types/flow";
const usePlaygroundEffect = (
currentFlowId: string,
playground: boolean,
openPlayground: boolean,
currentFlow: FlowType | undefined,
setNodes: (value: any, value2: boolean) => void,
setEdges: (value: any, value2: boolean) => void,
cleanFlowPool: () => void,
) => {
useEffect(() => {
if (currentFlowId && playground) {
if (openPlayground) {
setNodes(currentFlow?.data?.nodes ?? [], true);
setEdges(currentFlow?.data?.edges ?? [], true);
} else {
setNodes([], true);
setEdges([], true);
}
cleanFlowPool();
}
}, [openPlayground]);
};
export default usePlaygroundEffect;

View file

@ -1,12 +1,10 @@
import { usePostLikeComponent } from "@/controllers/API/queries/store"; import { usePostLikeComponent } from "@/controllers/API/queries/store";
import { useEffect, useState } from "react"; import { useState } from "react";
import { createRoot } from "react-dom/client";
import { Control } from "react-hook-form"; import { Control } from "react-hook-form";
import { getComponent, postLikeComponent } from "../../controllers/API"; import { getComponent } from "../../controllers/API";
import IOModal from "../../modals/IOModal"; import IOModal from "../../modals/IOModal";
import DeleteConfirmationModal from "../../modals/deleteConfirmationModal"; import DeleteConfirmationModal from "../../modals/deleteConfirmationModal";
import useAlertStore from "../../stores/alertStore"; import useAlertStore from "../../stores/alertStore";
import useFlowStore from "../../stores/flowStore";
import useFlowsManagerStore from "../../stores/flowsManagerStore"; import useFlowsManagerStore from "../../stores/flowsManagerStore";
import { useStoreStore } from "../../stores/storeStore"; import { useStoreStore } from "../../stores/storeStore";
import { FlowType } from "../../types/flow"; import { FlowType } from "../../types/flow";
@ -31,7 +29,6 @@ import Loading from "../ui/loading";
import useDataEffect from "./hooks/use-data-effect"; import useDataEffect from "./hooks/use-data-effect";
import useInstallComponent from "./hooks/use-handle-install"; import useInstallComponent from "./hooks/use-handle-install";
import useDragStart from "./hooks/use-on-drag-start"; import useDragStart from "./hooks/use-on-drag-start";
import usePlaygroundEffect from "./hooks/use-playground-effect";
import { convertTestName } from "./utils/convert-test-name"; import { convertTestName } from "./utils/convert-test-name";
export default function CollectionCardComponent({ export default function CollectionCardComponent({
@ -56,7 +53,6 @@ export default function CollectionCardComponent({
const setSuccessData = useAlertStore((state) => state.setSuccessData); const setSuccessData = useAlertStore((state) => state.setSuccessData);
const setErrorData = useAlertStore((state) => state.setErrorData); const setErrorData = useAlertStore((state) => state.setErrorData);
const setValidApiKey = useStoreStore((state) => state.updateValidApiKey); const setValidApiKey = useStoreStore((state) => state.updateValidApiKey);
const cleanFlowPool = useFlowStore((state) => state.CleanFlowPool);
const isStore = false; const isStore = false;
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [likedByUser, setLikedByUser] = useState(data?.liked_by_user ?? false); const [likedByUser, setLikedByUser] = useState(data?.liked_by_user ?? false);
@ -64,16 +60,9 @@ export default function CollectionCardComponent({
const [downloadsCount, setDownloadsCount] = useState( const [downloadsCount, setDownloadsCount] = useState(
data?.downloads_count ?? 0, data?.downloads_count ?? 0,
); );
const currentFlow = useFlowsManagerStore((state) => state.currentFlow);
const setCurrentFlow = useFlowsManagerStore((state) => state.setCurrentFlow); const setCurrentFlow = useFlowsManagerStore((state) => state.setCurrentFlow);
const getFlowById = useFlowsManagerStore((state) => state.getFlowById); const getFlowById = useFlowsManagerStore((state) => state.getFlowById);
const currentFlowId = useFlowsManagerStore((state) => state.currentFlowId);
const setNodes = useFlowStore((state) => state.setNodes);
const setEdges = useFlowStore((state) => state.setEdges);
const [openPlayground, setOpenPlayground] = useState(false); const [openPlayground, setOpenPlayground] = useState(false);
const setCurrentFlowId = useFlowsManagerStore(
(state) => state.setCurrentFlowId,
);
const [loadingPlayground, setLoadingPlayground] = useState(false); const [loadingPlayground, setLoadingPlayground] = useState(false);
const selectedFlowsComponentsCards = useFlowsManagerStore( const selectedFlowsComponentsCards = useFlowsManagerStore(
@ -96,16 +85,6 @@ export default function CollectionCardComponent({
return inputs.length > 0 || outputs.length > 0; return inputs.length > 0 || outputs.length > 0;
} }
usePlaygroundEffect(
currentFlowId,
playground!,
openPlayground,
currentFlow,
setNodes,
setEdges,
cleanFlowPool,
);
useDataEffect(data, setLikedByUser, setLikesCount, setDownloadsCount); useDataEffect(data, setLikedByUser, setLikesCount, setDownloadsCount);
const { handleInstall } = useInstallComponent( const { handleInstall } = useInstallComponent(
@ -317,7 +296,7 @@ export default function CollectionCardComponent({
setLoadingPlayground(false); setLoadingPlayground(false);
return; return;
} }
setCurrentFlowId(data.id); setCurrentFlow(flow);
setOpenPlayground(true); setOpenPlayground(true);
setLoadingPlayground(false); setLoadingPlayground(false);
} else { } else {
@ -473,7 +452,7 @@ export default function CollectionCardComponent({
setLoadingPlayground(false); setLoadingPlayground(false);
return; return;
} }
setCurrentFlowId(data.id); setCurrentFlow(flow);
setOpenPlayground(true); setOpenPlayground(true);
setLoadingPlayground(false); setLoadingPlayground(false);
} else { } else {
@ -510,6 +489,7 @@ export default function CollectionCardComponent({
</Card> </Card>
{openPlayground && ( {openPlayground && (
<IOModal <IOModal
key={data.id}
cleanOnClose={true} cleanOnClose={true}
open={openPlayground} open={openPlayground}
setOpen={setOpenPlayground} setOpen={setOpenPlayground}

View file

@ -1,12 +1,11 @@
import FeatureFlags from "@/../feature-config.json"; import FeatureFlags from "@/../feature-config.json";
import { Transition } from "@headlessui/react"; import { Transition } from "@headlessui/react";
import { useMemo, useRef, useState } from "react"; import { useMemo, useState } from "react";
import { useHotkeys } from "react-hotkeys-hook"; import { useHotkeys } from "react-hotkeys-hook";
import IOModal from "../../modals/IOModal"; import IOModal from "../../modals/IOModal";
import ApiModal from "../../modals/apiModal"; import ApiModal from "../../modals/apiModal";
import ShareModal from "../../modals/shareModal"; import ShareModal from "../../modals/shareModal";
import useFlowStore from "../../stores/flowStore"; import useFlowStore from "../../stores/flowStore";
import useFlowsManagerStore from "../../stores/flowsManagerStore";
import { useShortcutsStore } from "../../stores/shortcuts"; import { useShortcutsStore } from "../../stores/shortcuts";
import { useStoreStore } from "../../stores/storeStore"; import { useStoreStore } from "../../stores/storeStore";
import { classNames, isThereModal } from "../../utils/utils"; import { classNames, isThereModal } from "../../utils/utils";
@ -47,9 +46,7 @@ export default function FlowToolbar(): JSX.Element {
const hasStore = useStoreStore((state) => state.hasStore); const hasStore = useStoreStore((state) => state.hasStore);
const validApiKey = useStoreStore((state) => state.validApiKey); const validApiKey = useStoreStore((state) => state.validApiKey);
const hasApiKey = useStoreStore((state) => state.hasApiKey); const hasApiKey = useStoreStore((state) => state.hasApiKey);
const currentFlow = useFlowsManagerStore((state) => state.currentFlow); const currentFlow = useFlowStore((state) => state.currentFlow);
const prevNodesRef = useRef<any[] | undefined>();
const ModalMemo = useMemo( const ModalMemo = useMemo(
() => ( () => (

View file

@ -86,6 +86,7 @@ export default function CodeTabsComponent({
}} }}
id="tweaks-switch" id="tweaks-switch"
onCheckedChange={setActiveTweaks} onCheckedChange={setActiveTweaks}
checked={activeTweaks}
autoFocus={false} autoFocus={false}
/> />
<Label <Label

View file

@ -2,7 +2,6 @@ import React, { ChangeEvent, useState } from "react";
import { Input } from "../../components/ui/input"; import { Input } from "../../components/ui/input";
import { Label } from "../../components/ui/label"; import { Label } from "../../components/ui/label";
import { Textarea } from "../../components/ui/textarea"; import { Textarea } from "../../components/ui/textarea";
import useFlowsManagerStore from "../../stores/flowsManagerStore";
import { InputProps } from "../../types/components"; import { InputProps } from "../../types/components";
import { cn, isEndpointNameValid } from "../../utils/utils"; import { cn, isEndpointNameValid } from "../../utils/utils";
@ -19,7 +18,6 @@ export const EditFlowSettings: React.FC<InputProps> = ({
const [isMaxLength, setIsMaxLength] = useState(false); const [isMaxLength, setIsMaxLength] = useState(false);
const [validEndpointName, setValidEndpointName] = useState(true); const [validEndpointName, setValidEndpointName] = useState(true);
const [isInvalidName, setIsInvalidName] = useState(false); const [isInvalidName, setIsInvalidName] = useState(false);
const currentFlow = useFlowsManagerStore((state) => state.currentFlow);
const handleNameChange = (event: ChangeEvent<HTMLInputElement>) => { const handleNameChange = (event: ChangeEvent<HTMLInputElement>) => {
const { value } = event.target; const { value } = event.target;

View file

@ -8,7 +8,10 @@ import {
} from "../../../ui/dropdown-menu"; } from "../../../ui/dropdown-menu";
import useAddFlow from "@/hooks/flows/use-add-flow"; import useAddFlow from "@/hooks/flows/use-add-flow";
import useSaveFlow from "@/hooks/flows/use-save-flow";
import useUploadFlow from "@/hooks/flows/use-upload-flow"; import useUploadFlow from "@/hooks/flows/use-upload-flow";
import { customStringify } from "@/utils/reactflowUtils";
import { useHotkeys } from "react-hotkeys-hook";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { UPLOAD_ERROR_ALERT } from "../../../../constants/alerts_constants"; import { UPLOAD_ERROR_ALERT } from "../../../../constants/alerts_constants";
import { SAVED_HOVER } from "../../../../constants/constants"; import { SAVED_HOVER } from "../../../../constants/constants";
@ -29,7 +32,6 @@ import { Button } from "../../../ui/button";
export const MenuBar = ({}: {}): JSX.Element => { export const MenuBar = ({}: {}): JSX.Element => {
const shortcuts = useShortcutsStore((state) => state.shortcuts); const shortcuts = useShortcutsStore((state) => state.shortcuts);
const addFlow = useAddFlow(); const addFlow = useAddFlow();
const currentFlow = useFlowsManagerStore((state) => state.currentFlow);
const setErrorData = useAlertStore((state) => state.setErrorData); const setErrorData = useAlertStore((state) => state.setErrorData);
const setSuccessData = useAlertStore((state) => state.setSuccessData); const setSuccessData = useAlertStore((state) => state.setSuccessData);
const setLockChat = useFlowStore((state) => state.setLockChat); const setLockChat = useFlowStore((state) => state.setLockChat);
@ -46,6 +48,24 @@ export const MenuBar = ({}: {}): JSX.Element => {
const navigate = useNavigate(); const navigate = useNavigate();
const isBuilding = useFlowStore((state) => state.isBuilding); const isBuilding = useFlowStore((state) => state.isBuilding);
const getTypes = useTypesStore((state) => state.getTypes); const getTypes = useTypesStore((state) => state.getTypes);
const saveFlow = useSaveFlow();
const shouldAutosave = process.env.LANGFLOW_AUTO_SAVE !== "false";
const currentFlow = useFlowStore((state) => state.currentFlow);
const currentSavedFlow = useFlowsManagerStore((state) => state.currentFlow);
const updatedAt = currentSavedFlow?.updated_at;
const onFlowPage = useFlowStore((state) => state.onFlowPage);
const changesNotSaved =
customStringify(currentFlow) !== customStringify(currentSavedFlow) &&
!shouldAutosave;
const savedText =
updatedAt && changesNotSaved
? new Date(updatedAt).toLocaleString("en-US", {
hour: "numeric",
minute: "numeric",
})
: "Saved";
function handleAddFlow() { function handleAddFlow() {
try { try {
@ -69,10 +89,19 @@ export const MenuBar = ({}: {}): JSX.Element => {
} else if (saveLoading) { } else if (saveLoading) {
return "Saving..."; return "Saving...";
} }
return "Saved"; return savedText;
} }
return currentFlow ? ( const handleSave = () => {
saveFlow().then(() => {
setSuccessData({ title: "Saved successfully" });
});
};
const changes = useShortcutsStore((state) => state.changes);
useHotkeys(changes, handleSave, { preventDefault: true });
return currentFlow && onFlowPage ? (
<div className="round-button-div"> <div className="round-button-div">
<div className="header-menu-bar"> <div className="header-menu-bar">
<DropdownMenu> <DropdownMenu>
@ -112,6 +141,20 @@ export const MenuBar = ({}: {}): JSX.Element => {
<IconComponent name="Settings2" className="header-menu-options" /> <IconComponent name="Settings2" className="header-menu-options" />
Settings Settings
</DropdownMenuItem> </DropdownMenuItem>
{!shouldAutosave && (
<DropdownMenuItem onClick={handleSave} className="cursor-pointer">
<ToolbarSelectItem
value="Save"
icon="Save"
dataTestId=""
shortcut={
shortcuts.find(
(s) => s.name.toLowerCase() === "changes save",
)?.shortcut!
}
/>
</DropdownMenuItem>
)}
<DropdownMenuItem <DropdownMenuItem
onClick={() => { onClick={() => {
setOpenLogs(true); setOpenLogs(true);
@ -205,11 +248,11 @@ export const MenuBar = ({}: {}): JSX.Element => {
></FlowSettingsModal> ></FlowSettingsModal>
<FlowLogsModal open={openLogs} setOpen={setOpenLogs}></FlowLogsModal> <FlowLogsModal open={openLogs} setOpen={setOpenLogs}></FlowLogsModal>
</div> </div>
{(currentFlow.updated_at || saveLoading) && ( {(updatedAt || saveLoading) && (
<ShadTooltip <ShadTooltip
content={ content={
SAVED_HOVER + SAVED_HOVER +
new Date(currentFlow.updated_at ?? "").toLocaleString("en-US", { new Date(updatedAt ?? "").toLocaleString("en-US", {
hour: "numeric", hour: "numeric",
minute: "numeric", minute: "numeric",
second: "numeric", second: "numeric",
@ -220,13 +263,32 @@ export const MenuBar = ({}: {}): JSX.Element => {
> >
<div className="flex cursor-default items-center gap-2 text-sm text-muted-foreground transition-all"> <div className="flex cursor-default items-center gap-2 text-sm text-muted-foreground transition-all">
<div className="flex cursor-default items-center gap-1.5 text-sm text-muted-foreground transition-all"> <div className="flex cursor-default items-center gap-1.5 text-sm text-muted-foreground transition-all">
<IconComponent <Button
name={isBuilding || saveLoading ? "Loader2" : "CheckCircle2"} unstyled
disabled={shouldAutosave || !changesNotSaved}
className={cn( className={cn(
"h-4 w-4", !shouldAutosave && changesNotSaved
isBuilding || saveLoading ? "animate-spin" : "animate-wiggle", ? "hover:text-primary"
: "",
)} )}
/> onClick={handleSave}
>
<IconComponent
name={
isBuilding || saveLoading
? "Loader2"
: changesNotSaved
? "Save"
: "CheckCircle2"
}
className={cn(
"h-4 w-4",
isBuilding || saveLoading
? "animate-spin"
: "animate-wiggle",
)}
/>
</Button>
<div>{printByBuildStatus()}</div> <div>{printByBuildStatus()}</div>
</div> </div>
<button <button

View file

@ -1,7 +1,7 @@
import { useContext } from "react"; import { useContext } from "react";
import { FaDiscord, FaGithub } from "react-icons/fa"; import { FaDiscord, FaGithub } from "react-icons/fa";
import { RiTwitterXFill } from "react-icons/ri"; import { RiTwitterXFill } from "react-icons/ri";
import { Link, useLocation, useNavigate, useParams } from "react-router-dom"; import { Link, useLocation, useNavigate } from "react-router-dom";
import AlertDropdown from "../../alerts/alertDropDown"; import AlertDropdown from "../../alerts/alertDropDown";
import { import {
BASE_URL_API, BASE_URL_API,
@ -12,11 +12,9 @@ import { AuthContext } from "../../contexts/authContext";
import FeatureFlags from "@/../feature-config.json"; import FeatureFlags from "@/../feature-config.json";
import { useLogout } from "@/controllers/API/queries/auth"; import { useLogout } from "@/controllers/API/queries/auth";
import useDeleteFlow from "@/hooks/flows/use-delete-flow";
import useAuthStore from "@/stores/authStore"; import useAuthStore from "@/stores/authStore";
import useAlertStore from "../../stores/alertStore"; import useAlertStore from "../../stores/alertStore";
import { useDarkStore } from "../../stores/darkStore"; import { useDarkStore } from "../../stores/darkStore";
import useFlowStore from "../../stores/flowStore";
import { useLocationStore } from "../../stores/locationStore"; import { useLocationStore } from "../../stores/locationStore";
import { useStoreStore } from "../../stores/storeStore"; import { useStoreStore } from "../../stores/storeStore";
import IconComponent, { ForwardedIconComponent } from "../genericIconComponent"; import IconComponent, { ForwardedIconComponent } from "../genericIconComponent";
@ -44,10 +42,7 @@ export default function Header(): JSX.Element {
const logout = useAuthStore((state) => state.logout); const logout = useAuthStore((state) => state.logout);
const navigate = useNavigate(); const navigate = useNavigate();
const deleteFlow = useDeleteFlow();
const hasStore = useStoreStore((state) => state.hasStore); const hasStore = useStoreStore((state) => state.hasStore);
const { id } = useParams();
const nodes = useFlowStore((state) => state.nodes);
const dark = useDarkStore((state) => state.dark); const dark = useDarkStore((state) => state.dark);
const setDark = useDarkStore((state) => state.setDark); const setDark = useDarkStore((state) => state.setDark);
@ -58,11 +53,6 @@ export default function Header(): JSX.Element {
const profileImageUrl = `${BASE_URL_API}files/profile_pictures/${ const profileImageUrl = `${BASE_URL_API}files/profile_pictures/${
userData?.profile_image ?? "Space/046-rocket.svg" userData?.profile_image ?? "Space/046-rocket.svg"
}`; }`;
async function checkForChanges(): Promise<void> {
if (nodes.length === 0 && id) {
await deleteFlow({ id });
}
}
const redirectToLastLocation = () => { const redirectToLastLocation = () => {
const lastFlowVisitedIndex = routeHistory const lastFlowVisitedIndex = routeHistory
@ -100,14 +90,13 @@ export default function Header(): JSX.Element {
return ( return (
<div className="header-arrangement"> <div className="header-arrangement">
<div className="header-start-display lg:w-[407px]"> <div className="header-start-display lg:w-[407px]">
<Link to="/all" className="cursor-pointer" onClick={checkForChanges}> <Link to="/all" className="cursor-pointer">
<span className="ml-4 text-2xl">⛓️</span> <span className="ml-4 text-2xl">⛓️</span>
</Link> </Link>
{showArrowReturnIcon && ( {showArrowReturnIcon && (
<Button <Button
unstyled unstyled
onClick={() => { onClick={() => {
checkForChanges();
redirectToLastLocation(); redirectToLastLocation();
}} }}
> >
@ -129,7 +118,6 @@ export default function Header(): JSX.Element {
: "secondary" : "secondary"
} }
size="sm" size="sm"
onClick={checkForChanges}
> >
<IconComponent name="Home" className="h-4 w-4" /> <IconComponent name="Home" className="h-4 w-4" />
<div className="hidden flex-1 md:block">{USER_PROJECTS_HEADER}</div> <div className="hidden flex-1 md:block">{USER_PROJECTS_HEADER}</div>
@ -142,7 +130,6 @@ export default function Header(): JSX.Element {
className="gap-2" className="gap-2"
variant={location.pathname === "/store" ? "primary" : "secondary"} variant={location.pathname === "/store" ? "primary" : "secondary"}
size="sm" size="sm"
onClick={checkForChanges}
data-testid="button-store" data-testid="button-store"
> >
<IconComponent name="Store" className="h-4 w-4" /> <IconComponent name="Store" className="h-4 w-4" />

View file

@ -1,8 +1,8 @@
import useSaveFlow from "@/hooks/flows/use-save-flow";
import { import {
UPLOAD_ALERT_LIST, UPLOAD_ALERT_LIST,
WRONG_FILE_ERROR_ALERT, WRONG_FILE_ERROR_ALERT,
} from "../../../constants/alerts_constants"; } from "../../../constants/alerts_constants";
import { updateFlowInDatabase } from "../../../controllers/API";
import { uploadFlowToFolder } from "../../../pages/MainPage/services"; import { uploadFlowToFolder } from "../../../pages/MainPage/services";
import useAlertStore from "../../../stores/alertStore"; import useAlertStore from "../../../stores/alertStore";
import useFlowsManagerStore from "../../../stores/flowsManagerStore"; import useFlowsManagerStore from "../../../stores/flowsManagerStore";
@ -21,6 +21,7 @@ const useFileDrop = (
const setErrorData = useAlertStore((state) => state.setErrorData); const setErrorData = useAlertStore((state) => state.setErrorData);
const refreshFolders = useFolderStore((state) => state.refreshFolders); const refreshFolders = useFolderStore((state) => state.refreshFolders);
const flows = useFlowsManagerStore((state) => state.flows); const flows = useFlowsManagerStore((state) => state.flows);
const saveFlow = useSaveFlow();
const triggerFolderChange = (folderId) => { const triggerFolderChange = (folderId) => {
if (folderChangeCallback) { if (folderChangeCallback) {
@ -106,21 +107,21 @@ const useFileDrop = (
}; };
const uploadFromDragCard = (flowId, folderId) => { const uploadFromDragCard = (flowId, folderId) => {
const selectedFlow = flows.find((flow) => flow.id === flowId); const selectedFlow = flows?.find((flow) => flow.id === flowId);
if (!selectedFlow) { if (!selectedFlow) {
throw new Error("Flow not found"); throw new Error("Flow not found");
} }
const updatedFlow = { ...selectedFlow, folder_id: folderId }; const updatedFlow = { ...selectedFlow, folder_id: folderId };
const newName = addVersionToDuplicates(updatedFlow, flows); const newName = addVersionToDuplicates(updatedFlow, flows ?? []);
updatedFlow.name = newName; updatedFlow.name = newName;
setFolderDragging(false); setFolderDragging(false);
setFolderIdDragging(""); setFolderIdDragging("");
updateFlowInDatabase(updatedFlow).then(() => { saveFlow(updatedFlow).then(() => {
refreshFolders(); refreshFolders();
triggerFolderChange(folderId); triggerFolderChange(folderId);
}); });

View file

@ -770,9 +770,13 @@ export const defaultShortcuts = [
shortcut: `${IS_MAC ? "Cmd" : "Ctrl"} + Shift + D`, shortcut: `${IS_MAC ? "Cmd" : "Ctrl"} + Shift + D`,
}, },
{ {
name: "Save", name: "Changes Save",
shortcut: `${IS_MAC ? "Cmd" : "Ctrl"} + S`, shortcut: `${IS_MAC ? "Cmd" : "Ctrl"} + S`,
}, },
{
name: "Save Component",
shortcut: `${IS_MAC ? "Cmd" : "Ctrl"} + Alt + S`,
},
{ {
name: "Delete", name: "Delete",
shortcut: "Backspace", shortcut: "Backspace",

View file

@ -5,11 +5,8 @@ import {
} from "@/constants/constants"; } from "@/constants/constants";
import { useGetUserData } from "@/controllers/API/queries/auth"; import { useGetUserData } from "@/controllers/API/queries/auth";
import useAuthStore from "@/stores/authStore"; import useAuthStore from "@/stores/authStore";
import useFlowsManagerStore from "@/stores/flowsManagerStore";
import { createContext, useEffect, useState } from "react"; import { createContext, useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import Cookies from "universal-cookie"; import Cookies from "universal-cookie";
import { getLoggedUser, requestLogout } from "../controllers/API";
import useAlertStore from "../stores/alertStore"; import useAlertStore from "../stores/alertStore";
import { useFolderStore } from "../stores/foldersStore"; import { useFolderStore } from "../stores/foldersStore";
import { useStoreStore } from "../stores/storeStore"; import { useStoreStore } from "../stores/storeStore";
@ -31,7 +28,6 @@ const initialValue: AuthContextType = {
export const AuthContext = createContext<AuthContextType>(initialValue); export const AuthContext = createContext<AuthContextType>(initialValue);
export function AuthProvider({ children }): React.ReactElement { export function AuthProvider({ children }): React.ReactElement {
const navigate = useNavigate();
const cookies = new Cookies(); const cookies = new Cookies();
const [accessToken, setAccessToken] = useState<string | null>( const [accessToken, setAccessToken] = useState<string | null>(
cookies.get(LANGFLOW_ACCESS_TOKEN) ?? null, cookies.get(LANGFLOW_ACCESS_TOKEN) ?? null,
@ -46,12 +42,7 @@ export function AuthProvider({ children }): React.ReactElement {
const checkHasStore = useStoreStore((state) => state.checkHasStore); const checkHasStore = useStoreStore((state) => state.checkHasStore);
const fetchApiData = useStoreStore((state) => state.fetchApiData); const fetchApiData = useStoreStore((state) => state.fetchApiData);
const setAllFlows = useFlowsManagerStore((state) => state.setAllFlows);
const setSelectedFolder = useFolderStore((state) => state.setSelectedFolder);
const setIsAuthenticated = useAuthStore((state) => state.setIsAuthenticated); const setIsAuthenticated = useAuthStore((state) => state.setIsAuthenticated);
const setIsAdmin = useAuthStore((state) => state.setIsAdmin);
const setIsLoading = useFlowsManagerStore((state) => state.setIsLoading);
const autoLogin = useAuthStore((state) => state.autoLogin);
const { mutate: mutateLoggedUser } = useGetUserData(); const { mutate: mutateLoggedUser } = useGetUserData();

View file

@ -1,6 +1,5 @@
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactNode } from "react"; import { ReactNode } from "react";
import { BrowserRouter } from "react-router-dom";
import { ReactFlowProvider } from "reactflow"; import { ReactFlowProvider } from "reactflow";
import { TooltipProvider } from "../components/ui/tooltip"; import { TooltipProvider } from "../components/ui/tooltip";
import { ApiInterceptor } from "../controllers/API/api"; import { ApiInterceptor } from "../controllers/API/api";
@ -11,18 +10,16 @@ export default function ContextWrapper({ children }: { children: ReactNode }) {
//element to wrap all context //element to wrap all context
return ( return (
<> <>
<BrowserRouter> <QueryClientProvider client={queryClient}>
<QueryClientProvider client={queryClient}> <AuthProvider>
<AuthProvider> <TooltipProvider skipDelayDuration={0}>
<TooltipProvider skipDelayDuration={0}> <ReactFlowProvider>
<ReactFlowProvider> <ApiInterceptor />
<ApiInterceptor /> {children}
{children} </ReactFlowProvider>
</ReactFlowProvider> </TooltipProvider>
</TooltipProvider> </AuthProvider>
</AuthProvider> </QueryClientProvider>
</QueryClientProvider>
</BrowserRouter>
</> </>
); );
} }

View file

@ -1,6 +1,5 @@
import { LANGFLOW_ACCESS_TOKEN } from "@/constants/constants"; import { LANGFLOW_ACCESS_TOKEN } from "@/constants/constants";
import useAuthStore from "@/stores/authStore"; import useAuthStore from "@/stores/authStore";
import useFlowsManagerStore from "@/stores/flowsManagerStore";
import axios, { AxiosError, AxiosInstance, AxiosRequestConfig } from "axios"; import axios, { AxiosError, AxiosInstance, AxiosRequestConfig } from "axios";
import { useContext, useEffect } from "react"; import { useContext, useEffect } from "react";
import { Cookies } from "react-cookie"; import { Cookies } from "react-cookie";
@ -21,8 +20,6 @@ function ApiInterceptor() {
const autoLogin = useAuthStore((state) => state.autoLogin); const autoLogin = useAuthStore((state) => state.autoLogin);
const setErrorData = useAlertStore((state) => state.setErrorData); const setErrorData = useAlertStore((state) => state.setErrorData);
let { accessToken, authenticationErrorCount } = useContext(AuthContext); let { accessToken, authenticationErrorCount } = useContext(AuthContext);
const setSaveLoading = useFlowsManagerStore((state) => state.setSaveLoading);
const { mutate: mutationLogout } = useLogout(); const { mutate: mutationLogout } = useLogout();
const { mutate: mutationRenewAccessToken } = useRefreshAccessToken(); const { mutate: mutationRenewAccessToken } = useRefreshAccessToken();
const logout = useAuthStore((state) => state.logout); const logout = useAuthStore((state) => state.logout);
@ -152,7 +149,6 @@ function ApiInterceptor() {
onSuccess: async (data) => { onSuccess: async (data) => {
authenticationErrorCount = 0; authenticationErrorCount = 0;
await remakeRequest(error); await remakeRequest(error);
setSaveLoading(false);
authenticationErrorCount = 0; authenticationErrorCount = 0;
}, },
onError: (error) => { onError: (error) => {

View file

@ -141,34 +141,6 @@ export async function saveFlowToDatabase(newFlow: {
throw error; throw error;
} }
} }
/**
* Updates an existing flow in the database.
*
* @param {FlowType} updatedFlow - The updated flow data.
* @returns {Promise<any>} The updated flow data.
* @throws Will throw an error if the update fails.
*/
export async function updateFlowInDatabase(
updatedFlow: FlowType,
): Promise<FlowType> {
try {
const response = await api.patch(`${BASE_URL_API}flows/${updatedFlow.id}`, {
name: updatedFlow.name,
data: updatedFlow.data,
description: updatedFlow.description,
folder_id: updatedFlow.folder_id === "" ? null : updatedFlow.folder_id,
endpoint_name: updatedFlow.endpoint_name,
});
if (response && response?.status !== 200) {
throw new Error(`HTTP error! status: ${response?.status}`);
}
return response?.data;
} catch (error) {
console.error(error);
throw error;
}
}
/** /**
* Reads all flows from the database. * Reads all flows from the database.

View file

@ -1,5 +1,4 @@
import useFlowStore from "@/stores/flowStore"; import useFlowStore from "@/stores/flowStore";
import useFlowsManagerStore from "@/stores/flowsManagerStore";
import { FlowPoolType } from "@/types/zustand/flow"; import { FlowPoolType } from "@/types/zustand/flow";
import { keepPreviousData } from "@tanstack/react-query"; import { keepPreviousData } from "@tanstack/react-query";
import { AxiosResponse } from "axios"; import { AxiosResponse } from "axios";
@ -20,7 +19,7 @@ export const useGetBuildsQuery: useQueryFunctionType<
const { query } = UseRequestProcessor(); const { query } = UseRequestProcessor();
const setFlowPool = useFlowStore((state) => state.setFlowPool); const setFlowPool = useFlowStore((state) => state.setFlowPool);
const currentFlow = useFlowsManagerStore((state) => state.currentFlow); const currentFlow = useFlowStore((state) => state.currentFlow);
const getBuildsFn = async ( const getBuildsFn = async (
params: BuildsQueryParams, params: BuildsQueryParams,

View file

@ -6,41 +6,34 @@ import { getURL } from "../../helpers/constants";
import { UseRequestProcessor } from "../../services/request-processor"; import { UseRequestProcessor } from "../../services/request-processor";
interface IPatchUpdateFlow { interface IPatchUpdateFlow {
id: string;
name: string; name: string;
data: ReactFlowJsonObject; data: ReactFlowJsonObject;
description: string; description: string;
folder_id: string; folder_id: string | null | undefined;
endpoint_name: string; endpoint_name: string | null | undefined;
}
interface IPatchUpdateFlowParams {
id: string;
} }
export const usePatchUpdateFlow: useMutationFunctionType< export const usePatchUpdateFlow: useMutationFunctionType<
IPatchUpdateFlowParams, undefined,
IPatchUpdateFlow IPatchUpdateFlow
> = (params, options?) => { > = (options?) => {
const { mutate } = UseRequestProcessor(); const { mutate } = UseRequestProcessor();
const PatchUpdateFlowFn = async (payload: IPatchUpdateFlow): Promise<any> => { const PatchUpdateFlowFn = async (payload: IPatchUpdateFlow): Promise<any> => {
const response = await api.patch(`${getURL("FLOWS")}/${params}`, { const response = await api.patch(`${getURL("FLOWS")}/${payload.id}`, {
name: payload.name, name: payload.name,
data: payload.data, data: payload.data,
description: payload.description, description: payload.description,
folder_id: payload.folder_id === "" ? null : payload.folder_id, folder_id: payload.folder_id || null,
endpoint_name: payload.endpoint_name, endpoint_name: payload.endpoint_name || null,
}); });
return response.data; return response.data;
}; };
const mutation: UseMutationResult<IPatchUpdateFlow, any, IPatchUpdateFlow> = const mutation: UseMutationResult<IPatchUpdateFlow, any, IPatchUpdateFlow> =
mutate( mutate(["usePatchUpdateFlow"], PatchUpdateFlowFn, options);
["usePatchUpdateFlow", { id: params.id }],
PatchUpdateFlowFn,
options,
);
return mutation; return mutation;
}; };

View file

@ -49,7 +49,7 @@ const useAddFlow = () => {
const my_collection_id = useFolderStore.getState().myCollectionId; const my_collection_id = useFolderStore.getState().myCollectionId;
if (params?.override && flow) { if (params?.override && flow) {
const flowId = flows.find((f) => f.name === flow.name); const flowId = flows?.find((f) => f.name === flow.name);
if (flowId) { if (flowId) {
await deleteFlow({ id: flowId.id }); await deleteFlow({ id: flowId.id });
} }
@ -60,7 +60,7 @@ const useAddFlow = () => {
flow, flow,
); );
const newName = addVersionToDuplicates(newFlow, flows); const newName = addVersionToDuplicates(newFlow, flows ?? []);
newFlow.name = newName; newFlow.name = newName;
newFlow.folder_id = useFolderStore.getState().folderUrl; newFlow.folder_id = useFolderStore.getState().folderUrl;
@ -68,7 +68,10 @@ const useAddFlow = () => {
onSuccess: ({ id }) => { onSuccess: ({ id }) => {
newFlow.id = id; newFlow.id = id;
// Add the new flow to the list of flows. // Add the new flow to the list of flows.
const { data, flows: myFlows } = processFlows([newFlow, ...flows]); const { data, flows: myFlows } = processFlows([
newFlow,
...(flows ?? []),
]);
setFlows(myFlows); setFlows(myFlows);
useTypesStore.setState((state) => ({ useTypesStore.setState((state) => ({
data: { ...state.data, ["saved_components"]: data }, data: { ...state.data, ["saved_components"]: data },

View file

@ -0,0 +1,19 @@
import { SAVE_DEBOUNCE_TIME } from "@/constants/constants";
import { FlowType } from "@/types/flow";
import { useDebounce } from "../use-debounce";
import useSaveFlow from "./use-save-flow";
const useAutoSaveFlow = () => {
const saveFlow = useSaveFlow();
const shouldAutosave = process.env.LANGFLOW_AUTO_SAVE !== "false";
const autoSaveFlow = shouldAutosave
? useDebounce((flow?: FlowType) => {
saveFlow(flow);
}, SAVE_DEBOUNCE_TIME)
: () => {};
return autoSaveFlow;
};
export default useAutoSaveFlow;

View file

@ -27,7 +27,7 @@ const useDeleteFlow = () => {
{ {
onSuccess: () => { onSuccess: () => {
const { data, flows: myFlows } = processFlows( const { data, flows: myFlows } = processFlows(
flows.filter((flow) => !id.includes(flow.id)), (flows ?? []).filter((flow) => !id.includes(flow.id)),
); );
setFlows(myFlows); setFlows(myFlows);
setIsLoading(false); setIsLoading(false);

View file

@ -0,0 +1,100 @@
import { usePatchUpdateFlow } from "@/controllers/API/queries/flows/use-patch-update-flow";
import useAlertStore from "@/stores/alertStore";
import useFlowsManagerStore from "@/stores/flowsManagerStore";
import useFlowStore from "@/stores/flowStore";
import { FlowType } from "@/types/flow";
const useSaveFlow = () => {
const flows = useFlowsManagerStore((state) => state.flows);
const setFlows = useFlowsManagerStore((state) => state.setFlows);
const setErrorData = useAlertStore((state) => state.setErrorData);
const reactFlowInstance = useFlowStore((state) => state.reactFlowInstance);
const nodes = useFlowStore((state) => state.nodes);
const edges = useFlowStore((state) => state.edges);
const setSaveLoading = useFlowsManagerStore((state) => state.setSaveLoading);
const setCurrentFlow = useFlowStore((state) => state.setCurrentFlow);
const currentSavedFlow = useFlowsManagerStore((state) => state.currentFlow);
const currentFlow = useFlowStore((state) => state.currentFlow);
const flowData = currentFlow?.data;
const { mutate } = usePatchUpdateFlow();
const saveFlow = async (flow?: FlowType): Promise<void> => {
setSaveLoading(true);
return new Promise<void>((resolve, reject) => {
if (currentFlow) {
flow = flow || {
...currentFlow,
data: {
...flowData,
nodes,
edges,
viewport: reactFlowInstance?.getViewport() ?? {
zoom: 1,
x: 0,
y: 0,
},
},
};
}
if (flow && flow.data) {
const { id, name, data, description, folder_id, endpoint_name } = flow;
if (!currentSavedFlow?.data?.nodes.length || data.nodes.length > 0) {
mutate(
{ id, name, data, description, folder_id, endpoint_name },
{
onSuccess: (updatedFlow) => {
setSaveLoading(false);
if (flows) {
// updates flow in state
setFlows(
flows.map((flow) => {
if (flow.id === updatedFlow.id) {
return updatedFlow;
}
return flow;
}),
);
setCurrentFlow(updatedFlow);
resolve();
} else {
setErrorData({
title: "Failed to save flow",
list: ["Flows variable undefined"],
});
reject(new Error("Flows variable undefined"));
}
},
onError: (e) => {
setErrorData({
title: "Failed to save flow",
list: [e.message],
});
setSaveLoading(false);
reject(e);
},
},
);
} else {
setErrorData({
title: "Failed to save flow",
list: ["Can't save empty flow"],
});
setSaveLoading(false);
reject(new Error("Can't save empty flow"));
}
} else {
setErrorData({
title: "Failed to save flow",
list: ["Flow not found"],
});
reject(new Error("Flow not found"));
}
});
};
return saveFlow;
};
export default useSaveFlow;

View file

@ -0,0 +1,13 @@
import { debounce } from "lodash";
import { useLayoutEffect, useMemo, useRef } from "react";
export function useDebounce(callback, delay) {
const callbackRef = useRef(callback);
useLayoutEffect(() => {
callbackRef.current = callback;
});
return useMemo(
() => debounce((...args) => callbackRef.current(...args), delay),
[delay],
);
}

View file

@ -1,5 +1,4 @@
import ReactDOM from "react-dom/client"; import ReactDOM from "react-dom/client";
import App from "./App";
import ContextWrapper from "./contexts"; import ContextWrapper from "./contexts";
import reportWebVitals from "./reportWebVitals"; import reportWebVitals from "./reportWebVitals";
@ -8,6 +7,10 @@ import "./style/index.css";
// @ts-ignore // @ts-ignore
import "./style/applies.css"; import "./style/applies.css";
// @ts-ignore // @ts-ignore
import { Suspense } from "react";
import { RouterProvider } from "react-router-dom";
import LoadingComponent from "./components/loadingComponent";
import router from "./routes";
import "./style/classes.css"; import "./style/classes.css";
const root = ReactDOM.createRoot( const root = ReactDOM.createRoot(
@ -15,7 +18,15 @@ const root = ReactDOM.createRoot(
); );
root.render( root.render(
<ContextWrapper> <ContextWrapper>
<App /> <Suspense
fallback={
<div className="loading-page-panel">
<LoadingComponent remSize={50} />
</div>
}
>
<RouterProvider router={router} />
</Suspense>
</ContextWrapper>, </ContextWrapper>,
); );
reportWebVitals(); reportWebVitals();

View file

@ -11,7 +11,6 @@ import {
FS_ERROR_TEXT, FS_ERROR_TEXT,
SN_ERROR_TEXT, SN_ERROR_TEXT,
} from "../../../../constants/constants"; } from "../../../../constants/constants";
import { deleteFlowPool } from "../../../../controllers/API";
import useAlertStore from "../../../../stores/alertStore"; import useAlertStore from "../../../../stores/alertStore";
import useFlowStore from "../../../../stores/flowStore"; import useFlowStore from "../../../../stores/flowStore";
import useFlowsManagerStore from "../../../../stores/flowsManagerStore"; import useFlowsManagerStore from "../../../../stores/flowsManagerStore";

View file

@ -33,6 +33,7 @@ export default function IOModal({
open, open,
setOpen, setOpen,
disable, disable,
isPlayground,
}: IOModalPropsType): JSX.Element { }: IOModalPropsType): JSX.Element {
const allNodes = useFlowStore((state) => state.nodes); const allNodes = useFlowStore((state) => state.nodes);
const inputs = useFlowStore((state) => state.inputs).filter( const inputs = useFlowStore((state) => state.inputs).filter(
@ -107,7 +108,7 @@ export default function IOModal({
const setLockChat = useFlowStore((state) => state.setLockChat); const setLockChat = useFlowStore((state) => state.setLockChat);
const [chatValue, setChatValue] = useState(""); const [chatValue, setChatValue] = useState("");
const isBuilding = useFlowStore((state) => state.isBuilding); const isBuilding = useFlowStore((state) => state.isBuilding);
const currentFlow = useFlowsManagerStore((state) => state.currentFlow); const currentFlowId = useFlowsManagerStore((state) => state.currentFlowId);
const setNode = useFlowStore((state) => state.setNode); const setNode = useFlowStore((state) => state.setNode);
const [sessions, setSessions] = useState<string[]>([]); const [sessions, setSessions] = useState<string[]>([]);
const messages = useMessagesStore((state) => state.messages); const messages = useMessagesStore((state) => state.messages);
@ -115,7 +116,7 @@ export default function IOModal({
const { refetch } = useGetMessagesQuery({ const { refetch } = useGetMessagesQuery({
mode: "union", mode: "union",
id: currentFlow?.id, id: currentFlowId,
}); });
async function sendMessage({ async function sendMessage({
@ -164,7 +165,7 @@ export default function IOModal({
useEffect(() => { useEffect(() => {
const sessions = new Set<string>(); const sessions = new Set<string>();
messages messages
.filter((message) => message.flow_id === currentFlow!.id) .filter((message) => message.flow_id === currentFlowId)
.forEach((row) => { .forEach((row) => {
sessions.add(row.session_id); sessions.add(row.session_id);
}); });
@ -178,6 +179,7 @@ export default function IOModal({
open={open} open={open}
setOpen={setOpen} setOpen={setOpen}
disable={disable} disable={disable}
type={isPlayground ? "modal" : undefined}
onSubmit={() => sendMessage({ repeat: 1 })} onSubmit={() => sendMessage({ repeat: 1 })}
> >
<BaseModal.Trigger>{children}</BaseModal.Trigger> <BaseModal.Trigger>{children}</BaseModal.Trigger>
@ -375,7 +377,7 @@ export default function IOModal({
size="md" size="md"
className="block truncate" className="block truncate"
> >
{session === currentFlow?.id {session === currentFlowId
? "Default Session" ? "Default Session"
: session} : session}
</Badge> </Badge>
@ -491,7 +493,7 @@ export default function IOModal({
) && ( ) && (
<SessionView <SessionView
session={selectedViewField.id} session={selectedViewField.id}
id={currentFlow!.id} id={currentFlowId}
/> />
)} )}
</div> </div>

View file

@ -35,9 +35,6 @@ export default function ApiModal({
useEffect(() => { useEffect(() => {
if (open) initialSetup(autoLogin, flow); if (open) initialSetup(autoLogin, flow);
}, [open, flow?.data?.nodes]);
useEffect(() => {
setActiveTab("0"); setActiveTab("0");
}, [open]); }, [open]);

View file

@ -33,6 +33,7 @@ function ConfirmationModal({
confirmationText, confirmationText,
children, children,
destructive = false, destructive = false,
destructiveCancel = false,
icon, icon,
data, data,
index, index,
@ -44,13 +45,18 @@ function ConfirmationModal({
}: ConfirmationModalType) { }: ConfirmationModalType) {
const Icon: any = nodeIconsLucide[icon]; const Icon: any = nodeIconsLucide[icon];
const [modalOpen, setModalOpen] = useState(open ?? false); const [modalOpen, setModalOpen] = useState(open ?? false);
const [flag, setFlag] = useState(false);
useEffect(() => { useEffect(() => {
if (open) setModalOpen(open); if (open) setModalOpen(open);
}, [open]); }, [open]);
useEffect(() => { useEffect(() => {
if (onClose) onClose!(modalOpen); if (onClose && modalOpen === false && !flag) {
onClose();
} else if (flag) {
setFlag(false);
}
}, [modalOpen]); }, [modalOpen]);
const triggerChild = React.Children.toArray(children).find( const triggerChild = React.Children.toArray(children).find(
@ -61,7 +67,7 @@ function ConfirmationModal({
); );
return ( return (
<BaseModal size={size} open={open} setOpen={setModalOpen}> <BaseModal size={size} open={modalOpen} setOpen={setModalOpen}>
<BaseModal.Trigger>{triggerChild}</BaseModal.Trigger> <BaseModal.Trigger>{triggerChild}</BaseModal.Trigger>
<BaseModal.Header description={titleHeader ?? null}> <BaseModal.Header description={titleHeader ?? null}>
<span className="pr-2">{title}</span> <span className="pr-2">{title}</span>
@ -86,6 +92,7 @@ function ConfirmationModal({
className="ml-3" className="ml-3"
variant={destructive ? "destructive" : "default"} variant={destructive ? "destructive" : "default"}
onClick={() => { onClick={() => {
setFlag(true);
setModalOpen(false); setModalOpen(false);
onConfirm(index, data); onConfirm(index, data);
}} }}
@ -96,8 +103,9 @@ function ConfirmationModal({
<Button <Button
className="" className=""
variant="outline" variant={destructiveCancel ? "destructive" : "outline"}
onClick={() => { onClick={() => {
setFlag(true);
if (onCancel) onCancel(); if (onCancel) onCancel();
setModalOpen(false); setModalOpen(false);
}} }}

View file

@ -1,3 +1,4 @@
import useFlowStore from "@/stores/flowStore";
import { ReactNode, forwardRef, useEffect, useState } from "react"; import { ReactNode, forwardRef, useEffect, useState } from "react";
import EditFlowSettings from "../../components/editFlowSettingsComponent"; import EditFlowSettings from "../../components/editFlowSettingsComponent";
import IconComponent from "../../components/genericIconComponent"; import IconComponent from "../../components/genericIconComponent";
@ -10,7 +11,6 @@ import {
} from "../../constants/constants"; } from "../../constants/constants";
import useAlertStore from "../../stores/alertStore"; import useAlertStore from "../../stores/alertStore";
import { useDarkStore } from "../../stores/darkStore"; import { useDarkStore } from "../../stores/darkStore";
import useFlowsManagerStore from "../../stores/flowsManagerStore";
import { downloadFlow, removeApiKeys } from "../../utils/reactflowUtils"; import { downloadFlow, removeApiKeys } from "../../utils/reactflowUtils";
import BaseModal from "../baseModal"; import BaseModal from "../baseModal";
@ -19,7 +19,7 @@ const ExportModal = forwardRef(
const version = useDarkStore((state) => state.version); const version = useDarkStore((state) => state.version);
const setNoticeData = useAlertStore((state) => state.setNoticeData); const setNoticeData = useAlertStore((state) => state.setNoticeData);
const [checked, setChecked] = useState(false); const [checked, setChecked] = useState(false);
const currentFlow = useFlowsManagerStore((state) => state.currentFlow); const currentFlow = useFlowStore((state) => state.currentFlow);
useEffect(() => { useEffect(() => {
setName(currentFlow!.name); setName(currentFlow!.name);
setDescription(currentFlow!.description); setDescription(currentFlow!.description);

View file

@ -1,8 +1,11 @@
import useSaveFlow from "@/hooks/flows/use-save-flow";
import useAlertStore from "@/stores/alertStore";
import useFlowStore from "@/stores/flowStore";
import { cloneDeep } from "lodash";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import EditFlowSettings from "../../components/editFlowSettingsComponent"; import EditFlowSettings from "../../components/editFlowSettingsComponent";
import IconComponent from "../../components/genericIconComponent"; import IconComponent from "../../components/genericIconComponent";
import { SETTINGS_DIALOG_SUBTITLE } from "../../constants/constants"; import { SETTINGS_DIALOG_SUBTITLE } from "../../constants/constants";
import useAlertStore from "../../stores/alertStore";
import useFlowsManagerStore from "../../stores/flowsManagerStore"; import useFlowsManagerStore from "../../stores/flowsManagerStore";
import { FlowSettingsPropsType } from "../../types/components"; import { FlowSettingsPropsType } from "../../types/components";
import { FlowType } from "../../types/flow"; import { FlowType } from "../../types/flow";
@ -13,8 +16,10 @@ export default function FlowSettingsModal({
open, open,
setOpen, setOpen,
}: FlowSettingsPropsType): JSX.Element { }: FlowSettingsPropsType): JSX.Element {
const saveFlow = useFlowsManagerStore((state) => state.saveFlow); const saveFlow = useSaveFlow();
const currentFlow = useFlowsManagerStore((state) => state.currentFlow); const currentFlow = useFlowStore((state) => state.currentFlow);
const setCurrentFlow = useFlowStore((state) => state.setCurrentFlow);
const setSuccessData = useAlertStore((state) => state.setSuccessData);
const flows = useFlowsManagerStore((state) => state.flows); const flows = useFlowsManagerStore((state) => state.flows);
useEffect(() => { useEffect(() => {
setName(currentFlow!.name); setName(currentFlow!.name);
@ -28,35 +33,43 @@ export default function FlowSettingsModal({
); );
const [isSaving, setIsSaving] = useState(false); const [isSaving, setIsSaving] = useState(false);
const [disableSave, setDisableSave] = useState(true); const [disableSave, setDisableSave] = useState(true);
const shouldAutosave = process.env.LANGFLOW_AUTO_SAVE !== "false";
function handleClick(): void { function handleClick(): void {
setIsSaving(true); setIsSaving(true);
currentFlow!.name = name; if (!currentFlow) return;
currentFlow!.description = description; const newFlow = cloneDeep(currentFlow);
currentFlow!.endpoint_name = newFlow.name = name;
newFlow.description = description;
newFlow.endpoint_name =
endpoint_name && endpoint_name.length > 0 ? endpoint_name : null; endpoint_name && endpoint_name.length > 0 ? endpoint_name : null;
saveFlow(currentFlow!) if (shouldAutosave) {
?.then(() => { saveFlow(newFlow)
setOpen(false); ?.then(() => {
setIsSaving(false); setOpen(false);
}) setIsSaving(false);
.catch((err) => { setSuccessData({ title: "Changes saved successfully" });
useAlertStore.getState().setErrorData({ })
title: "Error while saving changes", .catch(() => {
list: [err?.response?.data.detail ?? ""], setIsSaving(false);
}); });
console.error(err); } else {
setIsSaving(false); setCurrentFlow(newFlow);
}); setOpen(false);
setIsSaving(false);
}
} }
const [nameLists, setNameList] = useState<string[]>([]); const [nameLists, setNameList] = useState<string[]>([]);
useEffect(() => { useEffect(() => {
const tempNameList: string[] = []; if (flows) {
flows.forEach((flow: FlowType) => { const tempNameList: string[] = [];
if ((flow.is_component ?? false) === false) tempNameList.push(flow.name); flows.forEach((flow: FlowType) => {
}); if ((flow.is_component ?? false) === false)
setNameList(tempNameList.filter((name) => name !== currentFlow!.name)); tempNameList.push(flow.name);
});
setNameList(tempNameList.filter((name) => name !== currentFlow!.name));
}
}, [flows]); }, [flows]);
useEffect(() => { useEffect(() => {

View file

@ -0,0 +1,22 @@
import ConfirmationModal from "../confirmationModal";
export function SaveChangesModal({ onSave, onProceed, onCancel }) {
return (
<ConfirmationModal
open={true}
onClose={onCancel}
destructiveCancel
title={"Exit without saving?"}
cancelText={"Exit anyway"}
confirmationText={"Save and Exit"}
icon={"Save"}
onConfirm={onSave}
onCancel={onProceed}
size="x-small"
>
<ConfirmationModal.Content>
You have unsaved changes. Would you like to save them before exiting?
</ConfirmationModal.Content>
</ConfirmationModal>
);
}

View file

@ -1,4 +1,5 @@
import { useGetTagsQuery } from "@/controllers/API/queries/store"; import { useGetTagsQuery } from "@/controllers/API/queries/store";
import useSaveFlow from "@/hooks/flows/use-save-flow";
import { cloneDeep } from "lodash"; import { cloneDeep } from "lodash";
import { ReactNode, useEffect, useMemo, useState } from "react"; import { ReactNode, useEffect, useMemo, useState } from "react";
import EditFlowSettings from "../../components/editFlowSettingsComponent"; import EditFlowSettings from "../../components/editFlowSettingsComponent";
@ -8,13 +9,11 @@ import { Button } from "../../components/ui/button";
import { Checkbox } from "../../components/ui/checkbox"; import { Checkbox } from "../../components/ui/checkbox";
import { import {
getStoreComponents, getStoreComponents,
getStoreTags,
saveFlowStore, saveFlowStore,
updateFlowStore, updateFlowStore,
} from "../../controllers/API"; } from "../../controllers/API";
import useAlertStore from "../../stores/alertStore"; import useAlertStore from "../../stores/alertStore";
import { useDarkStore } from "../../stores/darkStore"; import { useDarkStore } from "../../stores/darkStore";
import useFlowsManagerStore from "../../stores/flowsManagerStore";
import { useStoreStore } from "../../stores/storeStore"; import { useStoreStore } from "../../stores/storeStore";
import { FlowType } from "../../types/flow"; import { FlowType } from "../../types/flow";
import { import {
@ -59,7 +58,7 @@ export default function ShareModal({
const [unavaliableNames, setUnavaliableNames] = useState< const [unavaliableNames, setUnavaliableNames] = useState<
{ id: string; name: string }[] { id: string; name: string }[]
>([]); >([]);
const saveFlow = useFlowsManagerStore((state) => state.saveFlow); const saveFlow = useSaveFlow();
const { data, isFetching } = useGetTagsQuery(); const { data, isFetching } = useGetTagsQuery();
const [loadingNames, setLoadingNames] = useState(false); const [loadingNames, setLoadingNames] = useState(false);
@ -105,7 +104,7 @@ export default function ShareModal({
function successShare() { function successShare() {
if (!is_component) { if (!is_component) {
saveFlow(flow!, true); saveFlow(flow);
} }
setSuccessData({ setSuccessData({
title: `${is_component ? "Component" : "Flow"} shared successfully!`, title: `${is_component ? "Component" : "Flow"} shared successfully!`,

View file

@ -38,7 +38,6 @@ import { AuthContext } from "../../contexts/authContext";
import ConfirmationModal from "../../modals/confirmationModal"; import ConfirmationModal from "../../modals/confirmationModal";
import UserManagementModal from "../../modals/userManagementModal"; import UserManagementModal from "../../modals/userManagementModal";
import useAlertStore from "../../stores/alertStore"; import useAlertStore from "../../stores/alertStore";
import useFlowsManagerStore from "../../stores/flowsManagerStore";
import { Users } from "../../types/api"; import { Users } from "../../types/api";
import { UserInputType } from "../../types/components"; import { UserInputType } from "../../types/components";
@ -51,19 +50,11 @@ export default function AdminPage() {
const setErrorData = useAlertStore((state) => state.setErrorData); const setErrorData = useAlertStore((state) => state.setErrorData);
const { userData } = useContext(AuthContext); const { userData } = useContext(AuthContext);
const [totalRowsCount, setTotalRowsCount] = useState(0); const [totalRowsCount, setTotalRowsCount] = useState(0);
const setCurrentFlowId = useFlowsManagerStore(
(state) => state.setCurrentFlowId,
);
const { mutate: mutateDeleteUser } = useDeleteUsers(); const { mutate: mutateDeleteUser } = useDeleteUsers();
const { mutate: mutateUpdateUser } = useUpdateUser(); const { mutate: mutateUpdateUser } = useUpdateUser();
const { mutate: mutateAddUser } = useAddUser(); const { mutate: mutateAddUser } = useAddUser();
// set null id
useEffect(() => {
setCurrentFlowId("");
}, []);
const userList = useRef([]); const userList = useRef([]);
useEffect(() => { useEffect(() => {

View file

@ -1,7 +1,7 @@
import LoadingComponent from "@/components/loadingComponent"; import LoadingComponent from "@/components/loadingComponent";
import { useGetBuildsQuery } from "@/controllers/API/queries/_builds"; import { useGetBuildsQuery } from "@/controllers/API/queries/_builds";
import useAutoSaveFlow from "@/hooks/flows/use-autosave-flow";
import useUploadFlow from "@/hooks/flows/use-upload-flow"; import useUploadFlow from "@/hooks/flows/use-upload-flow";
import { getInputsAndOutputs } from "@/utils/storeUtils";
import _, { cloneDeep } from "lodash"; import _, { cloneDeep } from "lodash";
import { import {
KeyboardEvent, KeyboardEvent,
@ -36,10 +36,9 @@ import useFlowsManagerStore from "../../../../stores/flowsManagerStore";
import { useShortcutsStore } from "../../../../stores/shortcuts"; import { useShortcutsStore } from "../../../../stores/shortcuts";
import { useTypesStore } from "../../../../stores/typesStore"; import { useTypesStore } from "../../../../stores/typesStore";
import { APIClassType } from "../../../../types/api"; import { APIClassType } from "../../../../types/api";
import { FlowType, NodeType } from "../../../../types/flow"; import { NodeType } from "../../../../types/flow";
import { import {
checkOldComponents, checkOldComponents,
cleanEdges,
generateFlow, generateFlow,
generateNodeFromFlow, generateNodeFromFlow,
getNodeId, getNodeId,
@ -57,17 +56,9 @@ const nodeTypes = {
genericNode: GenericNode, genericNode: GenericNode,
}; };
export default function Page({ export default function Page({ view }: { view?: boolean }): JSX.Element {
flow,
view,
}: {
flow: FlowType;
view?: boolean;
}): JSX.Element {
const uploadFlow = useUploadFlow(); const uploadFlow = useUploadFlow();
const autoSaveCurrentFlow = useFlowsManagerStore( const autoSaveFlow = useAutoSaveFlow();
(state) => state.autoSaveCurrentFlow,
);
const types = useTypesStore((state) => state.types); const types = useTypesStore((state) => state.types);
const templates = useTypesStore((state) => state.templates); const templates = useTypesStore((state) => state.templates);
const setFilterEdge = useFlowStore((state) => state.setFilterEdge); const setFilterEdge = useFlowStore((state) => state.setFilterEdge);
@ -83,7 +74,6 @@ export default function Page({
const onEdgesChange = useFlowStore((state) => state.onEdgesChange); const onEdgesChange = useFlowStore((state) => state.onEdgesChange);
const setNodes = useFlowStore((state) => state.setNodes); const setNodes = useFlowStore((state) => state.setNodes);
const setEdges = useFlowStore((state) => state.setEdges); const setEdges = useFlowStore((state) => state.setEdges);
const cleanFlow = useFlowStore((state) => state.cleanFlow);
const deleteNode = useFlowStore((state) => state.deleteNode); const deleteNode = useFlowStore((state) => state.deleteNode);
const deleteEdge = useFlowStore((state) => state.deleteEdge); const deleteEdge = useFlowStore((state) => state.deleteEdge);
const undo = useFlowsManagerStore((state) => state.undo); const undo = useFlowsManagerStore((state) => state.undo);
@ -97,22 +87,17 @@ export default function Page({
(state) => state.setLastCopiedSelection, (state) => state.setLastCopiedSelection,
); );
const onConnect = useFlowStore((state) => state.onConnect); const onConnect = useFlowStore((state) => state.onConnect);
const currentFlowId = useFlowsManagerStore((state) => state.currentFlowId); const currentSavedFlow = useFlowsManagerStore((state) => state.currentFlow);
const setErrorData = useAlertStore((state) => state.setErrorData); const setErrorData = useAlertStore((state) => state.setErrorData);
const setNoticeData = useAlertStore((state) => state.setNoticeData); const setNoticeData = useAlertStore((state) => state.setNoticeData);
const updateCurrentFlow = useFlowStore((state) => state.updateCurrentFlow);
const [selectionMenuVisible, setSelectionMenuVisible] = useState(false); const [selectionMenuVisible, setSelectionMenuVisible] = useState(false);
const edgeUpdateSuccessful = useRef(true); const edgeUpdateSuccessful = useRef(true);
const position = useRef({ x: 0, y: 0 }); const position = useRef({ x: 0, y: 0 });
const [lastSelection, setLastSelection] = const [lastSelection, setLastSelection] =
useState<OnSelectionChangeParams | null>(null); useState<OnSelectionChangeParams | null>(null);
const currentFlowId = useFlowsManagerStore((state) => state.currentFlowId);
const setFlowState = useFlowStore((state) => state.setFlowState);
const setInputs = useFlowStore((state) => state.setInputs);
const setOutputs = useFlowStore((state) => state.setOutputs);
const setHasIO = useFlowStore((state) => state.setHasIO);
const { inputs, outputs } = getInputsAndOutputs(flow.data!.nodes);
const viewport = flow?.data?.viewport ?? { zoom: 1, x: 0, y: 0 };
function handleGroupNode() { function handleGroupNode() {
takeSnapshot(); takeSnapshot();
@ -159,16 +144,6 @@ export default function Page({
}; };
}, [lastCopiedSelection, lastSelection, takeSnapshot, selectionMenuVisible]); }, [lastCopiedSelection, lastSelection, takeSnapshot, selectionMenuVisible]);
useEffect(() => {
if (reactFlowInstance && currentFlowId) {
if (viewport.x == 0 && viewport.y == 0) {
reactFlowInstance.fitView();
} else {
reactFlowInstance.setViewport(viewport);
}
}
}, [currentFlowId, reactFlowInstance]);
const { isFetching, refetch } = useGetBuildsQuery({}); const { isFetching, refetch } = useGetBuildsQuery({});
const showCanvas = const showCanvas =
@ -177,19 +152,9 @@ export default function Page({
!isFetching; !isFetching;
useEffect(() => { useEffect(() => {
if (!isFetching) { refetch();
let newEdges = cleanEdges(flow.data!.nodes, flow.data!.edges); useFlowStore.setState({ autoSaveFlow });
setNodes(flow.data!.nodes); if (checkOldComponents({ nodes })) {
setEdges(newEdges);
setFlowState(undefined);
setInputs(inputs);
setOutputs(outputs);
setHasIO(inputs.length > 0 || outputs.length > 0);
}
}, [isFetching]);
useEffect(() => {
if (checkOldComponents({ nodes: flow?.data?.nodes ?? [] })) {
setNoticeData({ setNoticeData({
title: title:
"Components created before Langflow 1.0 may be unstable. Ensure components are up to date.", "Components created before Langflow 1.0 may be unstable. Ensure components are up to date.",
@ -197,13 +162,6 @@ export default function Page({
} }
}, [currentFlowId]); }, [currentFlowId]);
useEffect(() => {
refetch();
return () => {
cleanFlow();
};
}, [currentFlowId]);
function handleUndo(e: KeyboardEvent) { function handleUndo(e: KeyboardEvent) {
if (!isWrappedWithClass(e, "noflow")) { if (!isWrappedWithClass(e, "noflow")) {
e.preventDefault(); e.preventDefault();
@ -338,15 +296,17 @@ export default function Page({
// 👉 you can place your event handlers here // 👉 you can place your event handlers here
}, [takeSnapshot]); }, [takeSnapshot]);
const onNodeDragStop: NodeDragHandler = useCallback(() => {
autoSaveCurrentFlow(nodes, edges, reactFlowInstance?.getViewport()!);
// 👉 you can place your event handlers here
}, [takeSnapshot, autoSaveCurrentFlow, nodes, edges, reactFlowInstance]);
const onMoveEnd: OnMove = useCallback(() => { const onMoveEnd: OnMove = useCallback(() => {
// 👇 make moving the canvas undoable // 👇 make moving the canvas undoable
autoSaveCurrentFlow(nodes, edges, reactFlowInstance?.getViewport()!); autoSaveFlow();
}, [takeSnapshot, autoSaveCurrentFlow, nodes, edges, reactFlowInstance]); updateCurrentFlow({ viewport: reactFlowInstance?.getViewport() });
}, [takeSnapshot, autoSaveFlow, nodes, edges, reactFlowInstance]);
const onNodeDragStop: NodeDragHandler = useCallback(() => {
// 👇 make moving the canvas undoable
autoSaveFlow();
updateCurrentFlow({ nodes });
}, [takeSnapshot, autoSaveFlow, nodes, edges, reactFlowInstance]);
const onSelectionDragStart: SelectionDragHandler = useCallback(() => { const onSelectionDragStart: SelectionDragHandler = useCallback(() => {
// 👇 make dragging a selection undoable // 👇 make dragging a selection undoable
@ -497,13 +457,13 @@ export default function Page({
onEdgeUpdateStart={onEdgeUpdateStart} onEdgeUpdateStart={onEdgeUpdateStart}
onEdgeUpdateEnd={onEdgeUpdateEnd} onEdgeUpdateEnd={onEdgeUpdateEnd}
onNodeDragStart={onNodeDragStart} onNodeDragStart={onNodeDragStart}
onNodeDragStop={onNodeDragStop}
onSelectionDragStart={onSelectionDragStart} onSelectionDragStart={onSelectionDragStart}
onSelectionEnd={onSelectionEnd} onSelectionEnd={onSelectionEnd}
onSelectionStart={onSelectionStart} onSelectionStart={onSelectionStart}
connectionLineComponent={ConnectionLineComponent} connectionLineComponent={ConnectionLineComponent}
onDragOver={onDragOver} onDragOver={onDragOver}
onMoveEnd={onMoveEnd} onMoveEnd={onMoveEnd}
onNodeDragStop={onNodeDragStop}
onDrop={onDrop} onDrop={onDrop}
onSelectionChange={onSelectionChange} onSelectionChange={onSelectionChange}
deleteKeyCode={[]} deleteKeyCode={[]}

View file

@ -1,17 +1,13 @@
import { cloneDeep } from "lodash"; import { cloneDeep } from "lodash";
import { LinkIcon, SparklesIcon } from "lucide-react"; import { LinkIcon, SparklesIcon } from "lucide-react";
import { Fragment, useEffect, useMemo, useState } from "react"; import { Fragment, useEffect, useState } from "react";
import IconComponent from "../../../../components/genericIconComponent"; import IconComponent from "../../../../components/genericIconComponent";
import ShadTooltip from "../../../../components/shadTooltipComponent"; import ShadTooltip from "../../../../components/shadTooltipComponent";
import { Input } from "../../../../components/ui/input"; import { Input } from "../../../../components/ui/input";
import { Separator } from "../../../../components/ui/separator"; import { Separator } from "../../../../components/ui/separator";
import { PRIORITY_SIDEBAR_ORDER } from "../../../../constants/constants"; import { PRIORITY_SIDEBAR_ORDER } from "../../../../constants/constants";
import ExportModal from "../../../../modals/exportModal";
import ShareModal from "../../../../modals/shareModal";
import useAlertStore from "../../../../stores/alertStore"; import useAlertStore from "../../../../stores/alertStore";
import useFlowStore from "../../../../stores/flowStore"; import useFlowStore from "../../../../stores/flowStore";
import useFlowsManagerStore from "../../../../stores/flowsManagerStore";
import { useStoreStore } from "../../../../stores/storeStore";
import { useTypesStore } from "../../../../stores/typesStore"; import { useTypesStore } from "../../../../stores/typesStore";
import { APIClassType, APIObjectType } from "../../../../types/api"; import { APIClassType, APIObjectType } from "../../../../types/api";
import { import {
@ -19,7 +15,7 @@ import {
nodeIconsLucide, nodeIconsLucide,
nodeNames, nodeNames,
} from "../../../../utils/styleUtils"; } from "../../../../utils/styleUtils";
import { classNames, removeCountFromString } from "../../../../utils/utils"; import { removeCountFromString } from "../../../../utils/utils";
import DisclosureComponent from "../DisclosureComponent"; import DisclosureComponent from "../DisclosureComponent";
import ParentDisclosureComponent from "../ParentDisclosureComponent"; import ParentDisclosureComponent from "../ParentDisclosureComponent";
import SidebarDraggableComponent from "./sideBarDraggableComponent"; import SidebarDraggableComponent from "./sideBarDraggableComponent";
@ -31,10 +27,6 @@ export default function ExtraSidebar(): JSX.Element {
const templates = useTypesStore((state) => state.templates); const templates = useTypesStore((state) => state.templates);
const getFilterEdge = useFlowStore((state) => state.getFilterEdge); const getFilterEdge = useFlowStore((state) => state.getFilterEdge);
const setFilterEdge = useFlowStore((state) => state.setFilterEdge); const setFilterEdge = useFlowStore((state) => state.setFilterEdge);
const currentFlow = useFlowsManagerStore((state) => state.currentFlow);
const hasStore = useStoreStore((state) => state.hasStore);
const hasApiKey = useStoreStore((state) => state.hasApiKey);
const validApiKey = useStoreStore((state) => state.validApiKey);
const setErrorData = useAlertStore((state) => state.setErrorData); const setErrorData = useAlertStore((state) => state.setErrorData);
const [dataFilter, setFilterData] = useState(data); const [dataFilter, setFilterData] = useState(data);
@ -187,63 +179,6 @@ export default function ExtraSidebar(): JSX.Element {
} }
}, [getFilterEdge, data]); }, [getFilterEdge, data]);
const ModalMemo = useMemo(
() => (
<ShareModal
is_component={false}
component={currentFlow!}
disabled={!hasApiKey || !validApiKey || !hasStore}
>
<button
disabled={!hasApiKey || !validApiKey || !hasStore}
className={classNames(
"extra-side-bar-buttons gap-[4px] text-sm font-semibold",
!hasApiKey || !validApiKey || !hasStore
? "button-disable cursor-default text-muted-foreground"
: "",
)}
>
<IconComponent
name="Share3"
className={classNames(
"-m-0.5 -ml-1 h-6 w-6",
!hasApiKey || !validApiKey || !hasStore
? "extra-side-bar-save-disable"
: "",
)}
/>
Share
</button>
</ShareModal>
),
[hasApiKey, validApiKey, currentFlow, hasStore],
);
const ExportMemo = useMemo(
() => (
<ExportModal>
<button className={classNames("extra-side-bar-buttons")}>
<IconComponent name="FileDown" className="side-bar-button-size" />
</button>
</ExportModal>
),
[],
);
const getIcon = useMemo(() => {
return (SBSectionName: string) => {
if (nodeIconsLucide[SBSectionName]) {
return (
<IconComponent
name={SBSectionName}
strokeWidth={1.5}
className="w-[22px] text-primary"
/>
);
}
};
}, []);
return ( return (
<div className="side-bar-arrangement"> <div className="side-bar-arrangement">
<div className="side-bar-search-div-placement"> <div className="side-bar-search-div-placement">

View file

@ -72,7 +72,7 @@ export const SidebarDraggableComponent = forwardRef(
); );
break; break;
case "delete": case "delete":
const flowId = flows.find((f) => f.name === display_name); const flowId = flows?.find((f) => f.name === display_name);
if (flowId) deleteFlow({ id: flowId.id }); if (flowId) deleteFlow({ id: flowId.id });
break; break;
} }

View file

@ -82,7 +82,7 @@ export default function NodeToolbarComponent({
const validApiKey = useStoreStore((state) => state.validApiKey); const validApiKey = useStoreStore((state) => state.validApiKey);
const shortcuts = useShortcutsStore((state) => state.shortcuts); const shortcuts = useShortcutsStore((state) => state.shortcuts);
const unselectAll = useFlowStore((state) => state.unselectAll); const unselectAll = useFlowStore((state) => state.unselectAll);
const currentFlow = useFlowsManagerStore((state) => state.currentFlow); const currentFlowId = useFlowsManagerStore((state) => state.currentFlowId);
const addFlow = useAddFlow(); const addFlow = useAddFlow();
function handleMinimizeWShortcut(e: KeyboardEvent) { function handleMinimizeWShortcut(e: KeyboardEvent) {
@ -179,7 +179,7 @@ export default function NodeToolbarComponent({
function handleFreezeAll(e: KeyboardEvent) { function handleFreezeAll(e: KeyboardEvent) {
if (isWrappedWithClass(e, "noflow")) return; if (isWrappedWithClass(e, "noflow")) return;
e.preventDefault(); e.preventDefault();
FreezeAllVertices({ flowId: currentFlow!.id, stopNodeId: data.id }); FreezeAllVertices({ flowId: currentFlowId, stopNodeId: data.id });
} }
const advanced = useShortcutsStore((state) => state.advanced); const advanced = useShortcutsStore((state) => state.advanced);
@ -280,7 +280,7 @@ export default function NodeToolbarComponent({
})); }));
break; break;
case "freezeAll": case "freezeAll":
FreezeAllVertices({ flowId: currentFlow!.id, stopNodeId: data.id }); FreezeAllVertices({ flowId: currentFlowId, stopNodeId: data.id });
break; break;
case "code": case "code":
setOpenModal(!openModal); setOpenModal(!openModal);
@ -355,7 +355,7 @@ export default function NodeToolbarComponent({
} }
}; };
const isSaved = flows.some((flow) => const isSaved = flows?.some((flow) =>
Object.values(flow).includes(data.node?.display_name!), Object.values(flow).includes(data.node?.display_name!),
); );
@ -480,7 +480,7 @@ export default function NodeToolbarComponent({
event.preventDefault(); event.preventDefault();
takeSnapshot(); takeSnapshot();
FreezeAllVertices({ FreezeAllVertices({
flowId: currentFlow!.id, flowId: currentFlowId,
stopNodeId: data.id, stopNodeId: data.id,
}); });
}} }}
@ -543,7 +543,8 @@ export default function NodeToolbarComponent({
<SelectItem value={"save"}> <SelectItem value={"save"}>
<ToolbarSelectItem <ToolbarSelectItem
shortcut={ shortcut={
shortcuts.find((obj) => obj.name === "Save")?.shortcut! shortcuts.find((obj) => obj.name === "Save Component")
?.shortcut!
} }
value={"Save"} value={"Save"}
icon={"SaveAll"} icon={"SaveAll"}
@ -712,7 +713,7 @@ export default function NodeToolbarComponent({
}); });
setSuccessData({ title: `${data.id} successfully overridden!` }); setSuccessData({ title: `${data.id} successfully overridden!` });
}} }}
onClose={setShowOverrideModal} onClose={() => setShowOverrideModal(false)}
onCancel={() => { onCancel={() => {
addFlow({ addFlow({
flow: flowComponent, flow: flowComponent,

View file

@ -1,7 +1,10 @@
import FeatureFlags from "@/../feature-config.json"; import FeatureFlags from "@/../feature-config.json";
import { useGetGlobalVariables } from "@/controllers/API/queries/variables"; import { useGetGlobalVariables } from "@/controllers/API/queries/variables";
import useSaveFlow from "@/hooks/flows/use-save-flow";
import { SaveChangesModal } from "@/modals/saveChangesModal";
import { customStringify } from "@/utils/reactflowUtils";
import { useEffect } from "react"; import { useEffect } from "react";
import { useNavigate, useParams } from "react-router-dom"; import { useBlocker, useNavigate, useParams } from "react-router-dom";
import FlowToolbar from "../../components/chatComponent"; import FlowToolbar from "../../components/chatComponent";
import Header from "../../components/headerComponent"; import Header from "../../components/headerComponent";
import { useDarkStore } from "../../stores/darkStore"; import { useDarkStore } from "../../stores/darkStore";
@ -11,34 +14,66 @@ import Page from "./components/PageComponent";
import ExtraSidebar from "./components/extraSidebarComponent"; import ExtraSidebar from "./components/extraSidebarComponent";
export default function FlowPage({ view }: { view?: boolean }): JSX.Element { export default function FlowPage({ view }: { view?: boolean }): JSX.Element {
const setCurrentFlowId = useFlowsManagerStore( const setCurrentFlow = useFlowsManagerStore((state) => state.setCurrentFlow);
(state) => state.setCurrentFlowId, const currentFlow = useFlowStore((state) => state.currentFlow);
); const currentSavedFlow = useFlowsManagerStore((state) => state.currentFlow);
const changesNotSaved =
customStringify(currentFlow) !== customStringify(currentSavedFlow);
const blocker = useBlocker(changesNotSaved);
const version = useDarkStore((state) => state.version); const version = useDarkStore((state) => state.version);
const setOnFlowPage = useFlowStore((state) => state.setOnFlowPage); const setOnFlowPage = useFlowStore((state) => state.setOnFlowPage);
const currentFlow = useFlowsManagerStore((state) => state.currentFlow);
const { id } = useParams(); const { id } = useParams();
const navigate = useNavigate(); const navigate = useNavigate();
useGetGlobalVariables(); useGetGlobalVariables();
const saveFlow = useSaveFlow();
const flows = useFlowsManagerStore((state) => state.flows); const flows = useFlowsManagerStore((state) => state.flows);
const currentFlowId = useFlowsManagerStore((state) => state.currentFlowId);
const handleSave = () => {
saveFlow().then(() => (blocker.proceed ? blocker.proceed() : null));
};
useEffect(() => {
const handleBeforeUnload = (event: BeforeUnloadEvent) => {
if (changesNotSaved) {
event.preventDefault();
event.returnValue = ""; // Required for Chrome
}
};
window.addEventListener("beforeunload", handleBeforeUnload);
return () => {
window.removeEventListener("beforeunload", handleBeforeUnload);
};
}, [changesNotSaved, navigate]);
// Set flow tab id // Set flow tab id
useEffect(() => { useEffect(() => {
const isAnExistingFlow = flows.some((flow) => flow.id === id); if (flows && currentFlowId === "") {
const isAnExistingFlow = flows.find((flow) => flow.id === id);
if (!isAnExistingFlow) { if (!isAnExistingFlow) {
navigate("/all"); navigate("/all");
return; return;
}
setCurrentFlow(isAnExistingFlow);
} }
}, [id, flows]);
setCurrentFlowId(id!); useEffect(() => {
setOnFlowPage(true); setOnFlowPage(true);
return () => { return () => {
setOnFlowPage(false); setOnFlowPage(false);
setCurrentFlow();
}; };
}, [id]); }, [id]);
return ( return (
<> <>
<Header /> <Header />
@ -49,7 +84,7 @@ export default function FlowPage({ view }: { view?: boolean }): JSX.Element {
<main className="flex flex-1"> <main className="flex flex-1">
{/* Primary column */} {/* Primary column */}
<div className="h-full w-full"> <div className="h-full w-full">
<Page flow={currentFlow} /> <Page />
</div> </div>
{!view && <FlowToolbar />} {!view && <FlowToolbar />}
</main> </main>
@ -66,6 +101,13 @@ export default function FlowPage({ view }: { view?: boolean }): JSX.Element {
<div className={version ? "mt-2" : "mt-1"}>⛓️ v{version}</div> <div className={version ? "mt-2" : "mt-1"}>⛓️ v{version}</div>
</a> </a>
</div> </div>
{blocker.state === "blocked" && (
<SaveChangesModal
onSave={handleSave}
onCancel={() => (blocker.reset ? blocker.reset() : null)}
onProceed={() => (blocker.proceed ? blocker.proceed() : null)}
/>
)}
</> </>
); );
} }

View file

@ -89,7 +89,7 @@ export default function ComponentsComponent({
handleSelectAll(false); handleSelectAll(false);
setShouldSelectAll(true); setShouldSelectAll(true);
getFolderById(folderId ? folderId : myCollectionId); getFolderById(folderId ? folderId : myCollectionId);
}, [location]); }, [location, folderId, myCollectionId]);
useFilteredFlows(flowsFromFolder!, searchFlowsComponents, setAllFlows); useFilteredFlows(flowsFromFolder!, searchFlowsComponents, setAllFlows);

View file

@ -1,6 +1,6 @@
import { useDeleteFolders } from "@/controllers/API/queries/folders"; import { useDeleteFolders } from "@/controllers/API/queries/folders";
import useAlertStore from "@/stores/alertStore"; import useAlertStore from "@/stores/alertStore";
import { useEffect, useState } from "react"; import { useState } from "react";
import { Outlet, useLocation, useNavigate } from "react-router-dom"; import { Outlet, useLocation, useNavigate } from "react-router-dom";
import DropdownButton from "../../../../components/dropdownButtonComponent"; import DropdownButton from "../../../../components/dropdownButtonComponent";
import PageLayout from "../../../../components/pageLayout"; import PageLayout from "../../../../components/pageLayout";
@ -9,17 +9,12 @@ import {
MY_COLLECTION_DESC, MY_COLLECTION_DESC,
USER_PROJECTS_HEADER, USER_PROJECTS_HEADER,
} from "../../../../constants/constants"; } from "../../../../constants/constants";
import useFlowsManagerStore from "../../../../stores/flowsManagerStore";
import { useFolderStore } from "../../../../stores/foldersStore"; import { useFolderStore } from "../../../../stores/foldersStore";
import ModalsComponent from "../../components/modalsComponent"; import ModalsComponent from "../../components/modalsComponent";
import useDropdownOptions from "../../hooks/use-dropdown-options"; import useDropdownOptions from "../../hooks/use-dropdown-options";
import { getFolderById } from "../../services"; import { getFolderById } from "../../services";
export default function HomePage(): JSX.Element { export default function HomePage(): JSX.Element {
const setCurrentFlowId = useFlowsManagerStore(
(state) => state.setCurrentFlowId,
);
const location = useLocation(); const location = useLocation();
const pathname = location.pathname; const pathname = location.pathname;
const [openModal, setOpenModal] = useState(false); const [openModal, setOpenModal] = useState(false);
@ -35,10 +30,6 @@ export default function HomePage(): JSX.Element {
const myCollectionId = useFolderStore((state) => state.myCollectionId); const myCollectionId = useFolderStore((state) => state.myCollectionId);
const getFoldersApi = useFolderStore((state) => state.getFoldersApi); const getFoldersApi = useFolderStore((state) => state.getFoldersApi);
useEffect(() => {
setCurrentFlowId("");
}, [pathname]);
const dropdownOptions = useDropdownOptions({ const dropdownOptions = useDropdownOptions({
navigate, navigate,
is_component, is_component,

View file

@ -1,26 +1,20 @@
import { useEffect, useState } from "react"; import { useStoreStore } from "@/stores/storeStore";
import { useEffect } from "react";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import LoadingComponent from "../../components/loadingComponent"; import LoadingComponent from "../../components/loadingComponent";
import { getComponent } from "../../controllers/API"; import { getComponent } from "../../controllers/API";
import IOModal from "../../modals/IOModal"; import IOModal from "../../modals/IOModal";
import useFlowStore from "../../stores/flowStore";
import useFlowsManagerStore from "../../stores/flowsManagerStore"; import useFlowsManagerStore from "../../stores/flowsManagerStore";
import cloneFLowWithParent from "../../utils/storeUtils"; import cloneFLowWithParent from "../../utils/storeUtils";
export default function PlaygroundPage() { export default function PlaygroundPage() {
const currentFlow = useFlowsManagerStore((state) => state.currentFlow);
const getFlowById = useFlowsManagerStore((state) => state.getFlowById); const getFlowById = useFlowsManagerStore((state) => state.getFlowById);
const setCurrentFlowId = useFlowsManagerStore( const flows = useFlowsManagerStore((state) => state.flows);
(state) => state.setCurrentFlowId,
);
const currentFlowId = useFlowsManagerStore((state) => state.currentFlowId);
const setCurrentFlow = useFlowsManagerStore((state) => state.setCurrentFlow); const setCurrentFlow = useFlowsManagerStore((state) => state.setCurrentFlow);
const setNodes = useFlowStore((state) => state.setNodes); const currentSavedFlow = useFlowsManagerStore((state) => state.currentFlow);
const setEdges = useFlowStore((state) => state.setEdges); const validApiKey = useStoreStore((state) => state.validApiKey);
const cleanFlowPool = useFlowStore((state) => state.CleanFlowPool);
const { id } = useParams(); const { id } = useParams();
const [loading, setLoading] = useState(true);
async function getFlowData() { async function getFlowData() {
const res = await getComponent(id!); const res = await getComponent(id!);
const newFlow = cloneFLowWithParent(res, res.id, false, true); const newFlow = cloneFLowWithParent(res, res.id, false, true);
@ -29,32 +23,22 @@ export default function PlaygroundPage() {
// Set flow tab id // Set flow tab id
useEffect(() => { useEffect(() => {
if (getFlowById(id!)) { if (flows) {
setCurrentFlowId(id!); const flow = getFlowById(id!);
} else { if (flow) {
getFlowData().then((flow) => {
setCurrentFlow(flow); setCurrentFlow(flow);
}); } else {
if (validApiKey)
getFlowData().then((flow) => {
setCurrentFlow(flow);
});
}
} }
}, [id]); }, [id, flows, validApiKey]);
useEffect(() => {
if (currentFlow) {
setNodes(currentFlow?.data?.nodes ?? [], true);
setEdges(currentFlow?.data?.edges ?? [], true);
cleanFlowPool();
setLoading(false);
}
return () => {
setNodes([], true);
setEdges([], true);
cleanFlowPool();
};
}, [currentFlow]);
return ( return (
<div className="flex h-full w-full flex-col items-center justify-center align-middle"> <div className="flex h-full w-full flex-col items-center justify-center align-middle">
{loading ? ( {!currentSavedFlow ? (
<div> <div>
<LoadingComponent remSize={24}></LoadingComponent> <LoadingComponent remSize={24}></LoadingComponent>
</div> </div>

View file

@ -4,7 +4,7 @@ import {
} from "@/controllers/API/queries/auth"; } from "@/controllers/API/queries/auth";
import * as Form from "@radix-ui/react-form"; import * as Form from "@radix-ui/react-form";
import { cloneDeep } from "lodash"; import { cloneDeep } from "lodash";
import { useContext, useEffect, useState } from "react"; import { useContext, useState } from "react";
import IconComponent from "../../components/genericIconComponent"; import IconComponent from "../../components/genericIconComponent";
import Header from "../../components/headerComponent"; import Header from "../../components/headerComponent";
import InputComponent from "../../components/inputComponent"; import InputComponent from "../../components/inputComponent";
@ -18,7 +18,6 @@ import {
import { CONTROL_PATCH_USER_STATE } from "../../constants/constants"; import { CONTROL_PATCH_USER_STATE } from "../../constants/constants";
import { AuthContext } from "../../contexts/authContext"; import { AuthContext } from "../../contexts/authContext";
import useAlertStore from "../../stores/alertStore"; import useAlertStore from "../../stores/alertStore";
import useFlowsManagerStore from "../../stores/flowsManagerStore";
import { import {
inputHandlerEventType, inputHandlerEventType,
patchUserInputStateType, patchUserInputStateType,
@ -26,18 +25,9 @@ import {
import { gradients } from "../../utils/styleUtils"; import { gradients } from "../../utils/styleUtils";
import GradientChooserComponent from "../SettingsPage/pages/GeneralPage/components/ProfilePictureForm/components/profilePictureChooserComponent"; import GradientChooserComponent from "../SettingsPage/pages/GeneralPage/components/ProfilePictureForm/components/profilePictureChooserComponent";
export default function ProfileSettingsPage(): JSX.Element { export default function ProfileSettingsPage(): JSX.Element {
const setCurrentFlowId = useFlowsManagerStore(
(state) => state.setCurrentFlowId,
);
const [inputState, setInputState] = useState<patchUserInputStateType>( const [inputState, setInputState] = useState<patchUserInputStateType>(
CONTROL_PATCH_USER_STATE, CONTROL_PATCH_USER_STATE,
); );
// set null id
useEffect(() => {
setCurrentFlowId("");
}, []);
const setSuccessData = useAlertStore((state) => state.setSuccessData); const setSuccessData = useAlertStore((state) => state.setSuccessData);
const setErrorData = useAlertStore((state) => state.setErrorData); const setErrorData = useAlertStore((state) => state.setErrorData);
const { userData, setUserData } = useContext(AuthContext); const { userData, setUserData } = useContext(AuthContext);

View file

@ -1,19 +1,12 @@
import FeatureFlags from "@/../feature-config.json"; import FeatureFlags from "@/../feature-config.json";
import useAuthStore from "@/stores/authStore"; import useAuthStore from "@/stores/authStore";
import { useStoreStore } from "@/stores/storeStore"; import { useStoreStore } from "@/stores/storeStore";
import { useEffect } from "react";
import { Outlet } from "react-router-dom"; import { Outlet } from "react-router-dom";
import ForwardedIconComponent from "../../components/genericIconComponent"; import ForwardedIconComponent from "../../components/genericIconComponent";
import PageLayout from "../../components/pageLayout"; import PageLayout from "../../components/pageLayout";
import SidebarNav from "../../components/sidebarComponent"; import SidebarNav from "../../components/sidebarComponent";
import useFlowsManagerStore from "../../stores/flowsManagerStore";
export default function SettingsPage(): JSX.Element { export default function SettingsPage(): JSX.Element {
const pathname = location.pathname;
const setCurrentFlowId = useFlowsManagerStore(
(state) => state.setCurrentFlowId,
);
const autoLogin = useAuthStore((state) => state.autoLogin); const autoLogin = useAuthStore((state) => state.autoLogin);
const hasStore = useStoreStore((state) => state.hasStore); const hasStore = useStoreStore((state) => state.hasStore);
@ -21,10 +14,6 @@ export default function SettingsPage(): JSX.Element {
const showGeneralSettings = const showGeneralSettings =
FeatureFlags.ENABLE_PROFILE_ICONS || hasStore || !autoLogin; FeatureFlags.ENABLE_PROFILE_ICONS || hasStore || !autoLogin;
useEffect(() => {
setCurrentFlowId("");
}, [pathname]);
const sidebarNavItems: { const sidebarNavItems: {
href?: string; href?: string;
title: string; title: string;

View file

@ -18,7 +18,6 @@ import { useParams } from "react-router-dom";
import { CONTROL_PATCH_USER_STATE } from "../../../../constants/constants"; import { CONTROL_PATCH_USER_STATE } from "../../../../constants/constants";
import { AuthContext } from "../../../../contexts/authContext"; import { AuthContext } from "../../../../contexts/authContext";
import useAlertStore from "../../../../stores/alertStore"; import useAlertStore from "../../../../stores/alertStore";
import useFlowsManagerStore from "../../../../stores/flowsManagerStore";
import { useStoreStore } from "../../../../stores/storeStore"; import { useStoreStore } from "../../../../stores/storeStore";
import { import {
inputHandlerEventType, inputHandlerEventType,
@ -31,10 +30,6 @@ import ProfilePictureFormComponent from "./components/ProfilePictureForm";
import StoreApiKeyFormComponent from "./components/StoreApiKeyForm"; import StoreApiKeyFormComponent from "./components/StoreApiKeyForm";
export const GeneralPage = () => { export const GeneralPage = () => {
const setCurrentFlowId = useFlowsManagerStore(
(state) => state.setCurrentFlowId,
);
const { scrollId } = useParams(); const { scrollId } = useParams();
const [inputState, setInputState] = useState<patchUserInputStateType>( const [inputState, setInputState] = useState<patchUserInputStateType>(
@ -112,7 +107,7 @@ export const GeneralPage = () => {
} }
}; };
useScrollToElement(scrollId, setCurrentFlowId); useScrollToElement(scrollId);
const { mutate } = usePostAddApiKey({ const { mutate } = usePostAddApiKey({
onSuccess: () => { onSuccess: () => {

View file

@ -1,9 +1,6 @@
import { useEffect } from "react"; import { useEffect } from "react";
const useScrollToElement = ( const useScrollToElement = (scrollId: string | null | undefined) => {
scrollId: string | null | undefined,
setCurrentFlowId: (currentFlowId: string) => void,
) => {
useEffect(() => { useEffect(() => {
const element = document.getElementById(scrollId ?? "null"); const element = document.getElementById(scrollId ?? "null");
if (element) { if (element) {
@ -11,10 +8,6 @@ const useScrollToElement = (
element.scrollIntoView({ behavior: "smooth" }); element.scrollIntoView({ behavior: "smooth" });
} }
}, [scrollId]); }, [scrollId]);
useEffect(() => {
setCurrentFlowId("");
}, [setCurrentFlowId]);
}; };
export default useScrollToElement; export default useScrollToElement;

View file

@ -28,7 +28,7 @@ import {
} from "../../constants/alerts_constants"; } from "../../constants/alerts_constants";
import { STORE_DESC, STORE_TITLE } from "../../constants/constants"; import { STORE_DESC, STORE_TITLE } from "../../constants/constants";
import { AuthContext } from "../../contexts/authContext"; import { AuthContext } from "../../contexts/authContext";
import { getStoreComponents, getStoreTags } from "../../controllers/API"; import { getStoreComponents } from "../../controllers/API";
import useAlertStore from "../../stores/alertStore"; import useAlertStore from "../../stores/alertStore";
import useFlowsManagerStore from "../../stores/flowsManagerStore"; import useFlowsManagerStore from "../../stores/flowsManagerStore";
import { useStoreStore } from "../../stores/storeStore"; import { useStoreStore } from "../../stores/storeStore";
@ -46,9 +46,6 @@ export default function StorePage(): JSX.Element {
const { apiKey } = useContext(AuthContext); const { apiKey } = useContext(AuthContext);
const setErrorData = useAlertStore((state) => state.setErrorData); const setErrorData = useAlertStore((state) => state.setErrorData);
const setCurrentFlowId = useFlowsManagerStore(
(state) => state.setCurrentFlowId,
);
const currentFlowId = useFlowsManagerStore((state) => state.currentFlowId); const currentFlowId = useFlowsManagerStore((state) => state.currentFlowId);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const { id } = useParams(); const { id } = useParams();
@ -148,11 +145,6 @@ export default function StorePage(): JSX.Element {
}); });
} }
// Set a null id
useEffect(() => {
setCurrentFlowId("");
}, []);
function resetPagination() { function resetPagination() {
setPageIndex(1); setPageIndex(1);
setPageSize(12); setPageSize(12);

View file

@ -1,23 +1,47 @@
import { useGetGlobalVariables } from "@/controllers/API/queries/variables";
import useFlowStore from "@/stores/flowStore";
import { useEffect } from "react"; import { useEffect } from "react";
import { useParams } from "react-router-dom"; import { useNavigate, useParams } from "react-router-dom";
import useFlowsManagerStore from "../../stores/flowsManagerStore"; import useFlowsManagerStore from "../../stores/flowsManagerStore";
import Page from "../FlowPage/components/PageComponent"; import Page from "../FlowPage/components/PageComponent";
export default function ViewPage() { export default function ViewPage() {
const currentFlow = useFlowsManagerStore((state) => state.currentFlow); const setCurrentFlow = useFlowsManagerStore((state) => state.setCurrentFlow);
const setCurrentFlowId = useFlowsManagerStore(
(state) => state.setCurrentFlowId, const setOnFlowPage = useFlowStore((state) => state.setOnFlowPage);
);
const { id } = useParams(); const { id } = useParams();
const navigate = useNavigate();
useGetGlobalVariables();
const flows = useFlowsManagerStore((state) => state.flows);
const currentFlowId = useFlowsManagerStore((state) => state.currentFlowId);
// Set flow tab id // Set flow tab id
useEffect(() => { useEffect(() => {
setCurrentFlowId(id!); if (flows && currentFlowId === "") {
const isAnExistingFlow = flows.find((flow) => flow.id === id);
if (!isAnExistingFlow) {
navigate("/all");
return;
}
setCurrentFlow(isAnExistingFlow);
}
}, [id, flows]);
useEffect(() => {
setOnFlowPage(true);
return () => {
setOnFlowPage(false);
setCurrentFlow();
};
}, [id]); }, [id]);
return ( return (
<div className="flow-page-positioning"> <div className="flow-page-positioning">
{currentFlow && <Page view flow={currentFlow} />} <Page view />
</div> </div>
); );
} }

View file

@ -1,16 +1,20 @@
import FeatureFlags from "@/../feature-config.json"; import { lazy } from "react";
import useAuthStore from "@/stores/authStore"; import {
import { useStoreStore } from "@/stores/storeStore"; createBrowserRouter,
import { Suspense, lazy } from "react"; createRoutesFromElements,
import { Navigate, Route, Routes } from "react-router-dom"; Navigate,
Route,
} from "react-router-dom";
import App from "./App";
import { ProtectedAdminRoute } from "./components/authAdminGuard"; import { ProtectedAdminRoute } from "./components/authAdminGuard";
import { ProtectedRoute } from "./components/authGuard"; import { ProtectedRoute } from "./components/authGuard";
import { ProtectedLoginRoute } from "./components/authLoginGuard"; import { ProtectedLoginRoute } from "./components/authLoginGuard";
import { AuthSettingsGuard } from "./components/authSettingsGuard";
import { CatchAllRoute } from "./components/catchAllRoutes"; import { CatchAllRoute } from "./components/catchAllRoutes";
import LoadingComponent from "./components/loadingComponent";
import { StoreGuard } from "./components/storeGuard"; import { StoreGuard } from "./components/storeGuard";
import MessagesPage from "./pages/SettingsPage/pages/messagesPage"; const MessagesPage = lazy(
() => import("./pages/SettingsPage/pages/messagesPage"),
);
const AdminPage = lazy(() => import("./pages/AdminPage")); const AdminPage = lazy(() => import("./pages/AdminPage"));
const LoginAdminPage = lazy(() => import("./pages/AdminPage/LoginPage")); const LoginAdminPage = lazy(() => import("./pages/AdminPage/LoginPage"));
const ApiKeysPage = lazy( const ApiKeysPage = lazy(
@ -38,184 +42,161 @@ const SignUp = lazy(() => import("./pages/SignUpPage"));
const StorePage = lazy(() => import("./pages/StorePage")); const StorePage = lazy(() => import("./pages/StorePage"));
const ViewPage = lazy(() => import("./pages/ViewPage")); const ViewPage = lazy(() => import("./pages/ViewPage"));
const Router = () => { const router = createBrowserRouter(
const autoLogin = useAuthStore((state) => state.autoLogin); createRoutesFromElements([
const hasStore = useStoreStore((state) => state.hasStore); <Route path="" element={<App />}>
<Route
// Hides the General settings if there is nothing to show path="/"
const showGeneralSettings = element={
FeatureFlags.ENABLE_PROFILE_ICONS || hasStore || !autoLogin; <ProtectedRoute>
<HomePage />
return ( </ProtectedRoute>
<Suspense }
fallback={ >
<div className="loading-page-panel"> <Route index element={<Navigate replace to={"all"} />} />
<LoadingComponent remSize={50} />
</div>
}
>
<Routes>
<Route <Route
path="/" path="flows/*"
element={<MyCollectionComponent key="flows" type="flow" />}
/>
<Route
path="components/*"
element={<MyCollectionComponent key="components" type="component" />}
/>
<Route
path="all/*"
element={<MyCollectionComponent key="all" type="all" />}
/>
</Route>
<Route
path="/settings"
element={
<ProtectedRoute>
<SettingsPage />
</ProtectedRoute>
}
>
<Route index element={<Navigate replace to={"general"} />} />
<Route path="global-variables" element={<GlobalVariablesPage />} />
<Route path="api-keys" element={<ApiKeysPage />} />
<Route
path="general/:scrollId?"
element={ element={
<ProtectedRoute> <AuthSettingsGuard>
<HomePage /> <GeneralPage />
</ProtectedRoute> </AuthSettingsGuard>
} }
> />
<Route index element={<Navigate replace to={"all"} />} /> <Route path="shortcuts" element={<ShortcutsPage />} />
<Route <Route path="messages" element={<MessagesPage />} />
path="flows/*" </Route>
element={<MyCollectionComponent key="flows" type="flow" />} <Route
/> path="/store"
<Route element={
path="components/*" <ProtectedRoute>
element={ <StoreGuard>
<MyCollectionComponent key="components" type="component" /> <StorePage />
} </StoreGuard>
/> </ProtectedRoute>
<Route }
path="all/*" />
element={<MyCollectionComponent key="all" type="all" />} <Route
/> path="/store/:id/"
</Route> element={
<ProtectedRoute>
<StoreGuard>
<StorePage />
</StoreGuard>
</ProtectedRoute>
}
/>
<Route path="/playground/:id/">
<Route <Route
path="/settings" path=""
element={ element={
<ProtectedRoute> <ProtectedRoute>
<SettingsPage /> <PlaygroundPage />
</ProtectedRoute>
}
>
<Route
index
element={
<Navigate
replace
to={showGeneralSettings ? "general" : "global-variables"}
/>
}
/>
<Route path="global-variables" element={<GlobalVariablesPage />} />
<Route path="api-keys" element={<ApiKeysPage />} />
{showGeneralSettings && (
<Route path="general/:scrollId?" element={<GeneralPage />} />
)}
<Route path="shortcuts" element={<ShortcutsPage />} />
<Route path="messages" element={<MessagesPage />} />
</Route>
<Route
path="/store"
element={
<ProtectedRoute>
<StoreGuard>
<StorePage />
</StoreGuard>
</ProtectedRoute> </ProtectedRoute>
} }
/> />
<Route </Route>
path="/store/:id/" <Route path="/flow/:id/">
element={
<ProtectedRoute>
<StoreGuard>
<StorePage />
</StoreGuard>
</ProtectedRoute>
}
/>
<Route path="/playground/:id/">
<Route
path=""
element={
<ProtectedRoute>
<PlaygroundPage />
</ProtectedRoute>
}
/>
</Route>
<Route path="/flow/:id/">
<Route
path="*"
element={
<ProtectedRoute>
<FlowPage />
</ProtectedRoute>
}
/>
<Route
path=""
element={
<ProtectedRoute>
<FlowPage />
</ProtectedRoute>
}
/>
<Route
path="view"
element={
<ProtectedRoute>
<ViewPage />
</ProtectedRoute>
}
/>
</Route>
<Route <Route
path="*" path="*"
element={ element={
<ProtectedRoute> <ProtectedRoute>
<CatchAllRoute /> <FlowPage />
</ProtectedRoute> </ProtectedRoute>
} }
/> />
<Route <Route
path="/login" path=""
element={ element={
<ProtectedLoginRoute> <ProtectedRoute>
<LoginPage /> <FlowPage />
</ProtectedLoginRoute> </ProtectedRoute>
} }
/> />
<Route <Route
path="/signup" path="view"
element={ element={
<ProtectedLoginRoute> <ProtectedRoute>
<SignUp /> <ViewPage />
</ProtectedLoginRoute> </ProtectedRoute>
} }
/> />
</Route>
<Route
path="*"
element={
<ProtectedRoute>
<CatchAllRoute />
</ProtectedRoute>
}
/>
<Route
path="/login"
element={
<ProtectedLoginRoute>
<LoginPage />
</ProtectedLoginRoute>
}
/>
<Route
path="/signup"
element={
<ProtectedLoginRoute>
<SignUp />
</ProtectedLoginRoute>
}
/>
<Route
path="/login/admin"
element={
<ProtectedLoginRoute>
<LoginAdminPage />
</ProtectedLoginRoute>
}
/>
<Route
path="/admin"
element={
<ProtectedAdminRoute>
<AdminPage />
</ProtectedAdminRoute>
}
/>
<Route path="/account">
<Route <Route
path="/login/admin" path="delete"
element={ element={
<ProtectedLoginRoute> <ProtectedRoute>
<LoginAdminPage /> <DeleteAccountPage />
</ProtectedLoginRoute> </ProtectedRoute>
} }
/> ></Route>
</Route>
</Route>,
]),
);
<Route export default router;
path="/admin"
element={
<ProtectedAdminRoute>
<AdminPage />
</ProtectedAdminRoute>
}
/>
<Route path="/account">
<Route
path="delete"
element={
<ProtectedRoute>
<DeleteAccountPage />
</ProtectedRoute>
}
></Route>
</Route>
</Routes>
</Suspense>
);
};
export default Router;

View file

@ -1,5 +1,5 @@
import { create } from "zustand"; import { create } from "zustand";
import { getRepoStars, getVersion } from "../controllers/API"; import { getRepoStars } from "../controllers/API";
import { DarkStoreType } from "../types/zustand/dark"; import { DarkStoreType } from "../types/zustand/dark";
const startedStars = Number(window.localStorage.getItem("githubStars")) ?? 0; const startedStars = Number(window.localStorage.getItem("githubStars")) ?? 0;

View file

@ -19,7 +19,6 @@ import {
MISSED_ERROR_ALERT, MISSED_ERROR_ALERT,
} from "../constants/alerts_constants"; } from "../constants/alerts_constants";
import { BuildStatus } from "../constants/enums"; import { BuildStatus } from "../constants/enums";
import { getFlowPool } from "../controllers/API";
import { VertexBuildTypeAPI } from "../types/api"; import { VertexBuildTypeAPI } from "../types/api";
import { ChatInputType, ChatOutputType } from "../types/chat"; import { ChatInputType, ChatOutputType } from "../types/chat";
import { import {
@ -51,6 +50,7 @@ import { useTypesStore } from "./typesStore";
// this is our useStore hook that we can use in our components to get parts of the store and call actions // this is our useStore hook that we can use in our components to get parts of the store and call actions
const useFlowStore = create<FlowStoreType>((set, get) => ({ const useFlowStore = create<FlowStoreType>((set, get) => ({
autoSaveFlow: undefined,
componentsToUpdate: false, componentsToUpdate: false,
updateComponentsToUpdate: (nodes) => { updateComponentsToUpdate: (nodes) => {
let outdatedNodes = false; let outdatedNodes = false;
@ -153,8 +153,10 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
setPending: (isPending) => { setPending: (isPending) => {
set({ isPending }); set({ isPending });
}, },
resetFlow: ({ nodes, edges, viewport }) => { resetFlow: (flow) => {
const currentFlow = useFlowsManagerStore.getState().currentFlow; const nodes = flow?.data?.nodes ?? [];
const edges = flow?.data?.edges ?? [];
const viewport = flow?.data?.viewport ?? { zoom: 1, x: 0, y: 0 };
let brokenEdges = detectBrokenEdgesEdges(nodes, edges); let brokenEdges = detectBrokenEdgesEdges(nodes, edges);
if (brokenEdges.length > 0) { if (brokenEdges.length > 0) {
useAlertStore.getState().setErrorData({ useAlertStore.getState().setErrorData({
@ -172,13 +174,10 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
inputs, inputs,
outputs, outputs,
hasIO: inputs.length > 0 || outputs.length > 0, hasIO: inputs.length > 0 || outputs.length > 0,
flowPool: {},
currentFlow: flow,
}); });
get().reactFlowInstance!.setViewport(viewport); get().reactFlowInstance?.setViewport(viewport);
if (currentFlow) {
getFlowPool({ flowId: currentFlow.id }).then((flowPool) => {
set({ flowPool: flowPool.data.vertex_builds });
});
}
}, },
setIsBuilding: (isBuilding) => { setIsBuilding: (isBuilding) => {
set({ isBuilding }); set({ isBuilding });
@ -195,6 +194,16 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
}, },
setReactFlowInstance: (newState) => { setReactFlowInstance: (newState) => {
set({ reactFlowInstance: newState }); set({ reactFlowInstance: newState });
const viewport = get().currentFlow?.data?.viewport ?? {
zoom: 1,
x: 0,
y: 0,
};
if (viewport.x == 0 && viewport.y == 0) {
newState.fitView();
} else {
newState.setViewport(viewport);
}
}, },
onNodesChange: (changes: NodeChange[]) => { onNodesChange: (changes: NodeChange[]) => {
set({ set({
@ -206,7 +215,7 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
edges: applyEdgeChanges(changes, get().edges), edges: applyEdgeChanges(changes, get().edges),
}); });
}, },
setNodes: (change, skipSave = false) => { setNodes: (change) => {
let newChange = typeof change === "function" ? change(get().nodes) : change; let newChange = typeof change === "function" ? change(get().nodes) : change;
let newEdges = cleanEdges(newChange, get().edges); let newEdges = cleanEdges(newChange, get().edges);
const { inputs, outputs } = getInputsAndOutputs(newChange); const { inputs, outputs } = getInputsAndOutputs(newChange);
@ -219,30 +228,18 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
outputs, outputs,
hasIO: inputs.length > 0 || outputs.length > 0, hasIO: inputs.length > 0 || outputs.length > 0,
}); });
if (get().autoSaveFlow) {
const flowsManager = useFlowsManagerStore.getState(); get().autoSaveFlow!();
if (!get().isBuilding && !skipSave && get().onFlowPage) {
flowsManager.autoSaveCurrentFlow(
newChange,
newEdges,
get().reactFlowInstance?.getViewport() ?? { x: 0, y: 0, zoom: 1 },
);
} }
}, },
setEdges: (change, skipSave = false) => { setEdges: (change) => {
let newChange = typeof change === "function" ? change(get().edges) : change; let newChange = typeof change === "function" ? change(get().edges) : change;
set({ set({
edges: newChange, edges: newChange,
flowState: undefined, flowState: undefined,
}); });
if (get().autoSaveFlow) {
const flowsManager = useFlowsManagerStore.getState(); get().autoSaveFlow!();
if (!get().isBuilding && !skipSave && get().onFlowPage) {
flowsManager.autoSaveCurrentFlow(
get().nodes,
newChange,
get().reactFlowInstance?.getViewport() ?? { x: 0, y: 0, zoom: 1 },
);
} }
}, },
setNode: ( setNode: (
@ -485,13 +482,6 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
return newEdges; return newEdges;
}); });
useFlowsManagerStore
.getState()
.autoSaveCurrentFlow(
get().nodes,
newEdges,
get().reactFlowInstance?.getViewport() ?? { x: 0, y: 0, zoom: 1 },
);
}, },
unselectAll: () => { unselectAll: () => {
let newNodes = cloneDeep(get().nodes); let newNodes = cloneDeep(get().nodes);
@ -672,8 +662,9 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
useFlowStore.getState().updateBuildStatus(idList, BuildStatus.BUILDING); useFlowStore.getState().updateBuildStatus(idList, BuildStatus.BUILDING);
}, },
onValidateNodes: validateSubgraph, onValidateNodes: validateSubgraph,
nodes: get().onFlowPage ? get().nodes : undefined, nodes: get().nodes || undefined,
edges: get().onFlowPage ? get().edges : undefined, edges: get().edges || undefined,
logBuilds: get().onFlowPage,
}); });
get().setIsBuilding(false); get().setIsBuilding(false);
get().setLockChat(false); get().setLockChat(false);
@ -744,6 +735,27 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
}); });
set({ flowBuildStatus: newFlowBuildStatus }); set({ flowBuildStatus: newFlowBuildStatus });
}, },
currentFlow: undefined,
setCurrentFlow: (flow) => {
set({ currentFlow: flow });
},
updateCurrentFlow: ({ nodes, edges, viewport }) => {
set({
currentFlow: {
...get().currentFlow!,
data: {
nodes: nodes ?? get().currentFlow?.data?.nodes ?? [],
edges: edges ?? get().currentFlow?.data?.edges ?? [],
viewport: viewport ??
get().currentFlow?.data?.viewport ?? {
x: 0,
y: 0,
zoom: 1,
},
},
},
});
},
})); }));
export default useFlowStore; export default useFlowStore;

View file

@ -1,13 +1,6 @@
import { AxiosError } from "axios";
import { cloneDeep } from "lodash"; import { cloneDeep } from "lodash";
import pDebounce from "p-debounce";
import { Edge, Node, Viewport } from "reactflow";
import { create } from "zustand"; import { create } from "zustand";
import { SAVE_DEBOUNCE_TIME } from "../constants/constants"; import { readFlowsFromDatabase } from "../controllers/API";
import {
readFlowsFromDatabase,
updateFlowInDatabase,
} from "../controllers/API";
import { FlowType } from "../types/flow"; import { FlowType } from "../types/flow";
import { import {
FlowsManagerStoreType, FlowsManagerStoreType,
@ -36,22 +29,17 @@ const useFlowsManagerStore = create<FlowsManagerStoreType>((set, get) => ({
set({ examples }); set({ examples });
}, },
currentFlowId: "", currentFlowId: "",
setCurrentFlow: (flow: FlowType) => { setCurrentFlow: (flow: FlowType | undefined) => {
set((state) => ({ set({
currentFlow: flow, currentFlow: flow,
currentFlowId: flow.id, currentFlowId: flow?.id ?? "",
})); });
useFlowStore.getState().resetFlow(flow);
}, },
getFlowById: (id: string) => { getFlowById: (id: string) => {
return get().flows.find((flow) => flow.id === id); return get().flows?.find((flow) => flow.id === id);
}, },
setCurrentFlowId: (currentFlowId: string) => { flows: undefined,
set((state) => ({
currentFlowId,
currentFlow: state.flows.find((flow) => flow.id === currentFlowId),
}));
},
flows: [],
allFlows: [], allFlows: [],
setAllFlows: (allFlows: FlowType[]) => { setAllFlows: (allFlows: FlowType[]) => {
set({ allFlows }); set({ allFlows });
@ -107,60 +95,6 @@ const useFlowsManagerStore = create<FlowsManagerStoreType>((set, get) => ({
}); });
}); });
}, },
autoSaveCurrentFlow: (nodes: Node[], edges: Edge[], viewport: Viewport) => {
if (get().currentFlow) {
get().saveFlow(
{ ...get().currentFlow!, data: { nodes, edges, viewport } },
true,
);
}
},
saveFlow: (flow: FlowType, silent?: boolean) => {
set({ saveLoading: true }); // set saveLoading true immediately
return get().saveFlowDebounce(flow, silent); // call the debounced function directly
},
saveFlowDebounce: pDebounce((flow: FlowType, silent?: boolean) => {
const folderUrl = useFolderStore.getState().folderUrl;
const hasFolderUrl = folderUrl != null && folderUrl !== "";
flow.folder_id = hasFolderUrl
? useFolderStore.getState().folderUrl
: useFolderStore.getState().myCollectionId ?? "";
set({ saveLoading: true });
return new Promise<void>((resolve, reject) => {
updateFlowInDatabase(flow)
.then((updatedFlow) => {
if (updatedFlow) {
// updates flow in state
if (!silent) {
useAlertStore
.getState()
.setSuccessData({ title: "Changes saved successfully" });
}
get().setFlows(
get().flows.map((flow) => {
if (flow.id === updatedFlow.id) {
return updatedFlow;
}
return flow;
}),
);
//update tabs state
resolve();
set({ saveLoading: false });
}
})
.catch((err) => {
useAlertStore.getState().setErrorData({
title: "Error while saving changes",
list: [(err as AxiosError).message],
});
reject(err);
});
});
}, SAVE_DEBOUNCE_TIME),
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

View file

@ -20,7 +20,8 @@ export const useShortcutsStore = create<shortcutsStoreType>((set, get) => ({
duplicate: "mod+d", duplicate: "mod+d",
component: "mod+shift+s", component: "mod+shift+s",
docs: "mod+shift+d", docs: "mod+shift+d",
save: "mod+s", changes: "mod+s",
save: "mod+alt+s",
delete: "backspace", delete: "backspace",
group: "mod+g", group: "mod+g",
cut: "mod+x", cut: "mod+x",

View file

@ -435,6 +435,7 @@ export type ConfirmationModalType = {
title: string; title: string;
titleHeader?: string; titleHeader?: string;
destructive?: boolean; destructive?: boolean;
destructiveCancel?: boolean;
modalContentTitle?: string; modalContentTitle?: string;
cancelText: string; cancelText: string;
confirmationText: string; confirmationText: string;
@ -446,7 +447,7 @@ export type ConfirmationModalType = {
index?: number; index?: number;
onConfirm: (index, data) => void; onConfirm: (index, data) => void;
open?: boolean; open?: boolean;
onClose?: (close: boolean) => void; onClose?: () => void;
size?: size?:
| "x-small" | "x-small"
| "smaller" | "smaller"

View file

@ -38,6 +38,7 @@ export type shortcutsStoreType = {
duplicate: string; duplicate: string;
component: string; component: string;
docs: string; docs: string;
changes: string;
save: string; save: string;
delete: string; delete: string;
update: string; update: string;

View file

@ -1,3 +1,4 @@
import { FlowType } from "@/types/flow";
import { import {
Connection, Connection,
Edge, Edge,
@ -53,6 +54,7 @@ export type FlowPoolType = {
}; };
export type FlowStoreType = { export type FlowStoreType = {
autoSaveFlow: (() => void) | undefined;
componentsToUpdate: boolean; componentsToUpdate: boolean;
updateComponentsToUpdate: (nodes: Node[]) => void; updateComponentsToUpdate: (nodes: Node[]) => void;
onFlowPage: boolean; onFlowPage: boolean;
@ -83,11 +85,7 @@ export type FlowStoreType = {
isPending: boolean; isPending: boolean;
setIsBuilding: (isBuilding: boolean) => void; setIsBuilding: (isBuilding: boolean) => void;
setPending: (isPending: boolean) => void; setPending: (isPending: boolean) => void;
resetFlow: (flow: { resetFlow: (flow: FlowType | undefined) => void;
nodes: Node[];
edges: Edge[];
viewport: Viewport;
}) => void;
reactFlowInstance: ReactFlowInstance | null; reactFlowInstance: ReactFlowInstance | null;
setReactFlowInstance: (newState: ReactFlowInstance) => void; setReactFlowInstance: (newState: ReactFlowInstance) => void;
flowState: FlowState | undefined; flowState: FlowState | undefined;
@ -101,14 +99,8 @@ export type FlowStoreType = {
edges: Edge[]; edges: Edge[];
onNodesChange: OnNodesChange; onNodesChange: OnNodesChange;
onEdgesChange: OnEdgesChange; onEdgesChange: OnEdgesChange;
setNodes: ( setNodes: (update: Node[] | ((oldState: Node[]) => Node[])) => void;
update: Node[] | ((oldState: Node[]) => Node[]), setEdges: (update: Edge[] | ((oldState: Edge[]) => Edge[])) => void;
skipSave?: boolean,
) => void;
setEdges: (
update: Edge[] | ((oldState: Edge[]) => Edge[]),
skipSave?: boolean,
) => void;
setNode: ( setNode: (
id: string, id: string,
update: Node | ((oldState: Node) => Node), update: Node | ((oldState: Node) => Node),
@ -177,4 +169,15 @@ export type FlowStoreType = {
setLockChat: (lock: boolean) => void; setLockChat: (lock: boolean) => void;
lockChat: boolean; lockChat: boolean;
updateFreezeStatus: (nodeIds: string[], freeze: boolean) => void; updateFreezeStatus: (nodeIds: string[], freeze: boolean) => void;
currentFlow: FlowType | undefined;
setCurrentFlow: (flow: FlowType | undefined) => void;
updateCurrentFlow: ({
nodes,
edges,
viewport,
}: {
nodes?: Node[];
edges?: Edge[];
viewport?: Viewport;
}) => void;
}; };

View file

@ -1,41 +1,24 @@
import { Edge, Node, Viewport } from "reactflow";
import { FlowType } from "../../flow"; import { FlowType } from "../../flow";
export type FlowsManagerStoreType = { export type FlowsManagerStoreType = {
getFlowById: (id: string) => FlowType | undefined; getFlowById: (id: string) => FlowType | undefined;
flows: Array<FlowType>; flows: Array<FlowType> | undefined;
allFlows: Array<FlowType>; allFlows: Array<FlowType>;
setAllFlows: (flows: FlowType[]) => void; setAllFlows: (flows: FlowType[]) => void;
setFlows: (flows: FlowType[]) => void; setFlows: (flows: FlowType[]) => void;
currentFlow: FlowType | undefined; currentFlow: FlowType | undefined;
currentFlowId: string; currentFlowId: string;
setCurrentFlowId: (currentFlowId: string) => void;
saveLoading: boolean; saveLoading: boolean;
setSaveLoading: (saveLoading: boolean) => void; setSaveLoading: (saveLoading: boolean) => void;
isLoading: boolean; isLoading: boolean;
setIsLoading: (isLoading: boolean) => void; setIsLoading: (isLoading: boolean) => void;
refreshFlows: () => Promise<void>; refreshFlows: () => Promise<void>;
saveFlow: (
flow: FlowType,
silent?: boolean,
folderId?: string,
) => Promise<void> | undefined;
saveFlowDebounce: (
flow: FlowType,
silent?: boolean,
folderId?: string,
) => Promise<void> | undefined;
autoSaveCurrentFlow: (
nodes: Node[],
edges: Edge[],
viewport: Viewport,
) => void;
undo: () => void; undo: () => void;
redo: () => void; redo: () => void;
takeSnapshot: () => void; takeSnapshot: () => void;
examples: Array<FlowType>; examples: Array<FlowType>;
setExamples: (examples: FlowType[]) => void; setExamples: (examples: FlowType[]) => void;
setCurrentFlow: (flow: FlowType) => void; setCurrentFlow: (flow?: FlowType) => void;
setSearchFlowsComponents: (search: string) => void; setSearchFlowsComponents: (search: string) => void;
searchFlowsComponents: string; searchFlowsComponents: string;
selectedFlowsComponentsCards: string[]; selectedFlowsComponentsCards: string[];

View file

@ -30,6 +30,7 @@ type BuildVerticesParams = {
onValidateNodes?: (nodes: string[]) => void; onValidateNodes?: (nodes: string[]) => void;
nodes?: Node[]; nodes?: Node[];
edges?: Edge[]; edges?: Edge[];
logBuilds?: boolean;
}; };
function getInactiveVertexData(vertexId: string): VertexBuildTypeAPI { function getInactiveVertexData(vertexId: string): VertexBuildTypeAPI {
@ -146,6 +147,7 @@ export async function buildFlowVertices({
onValidateNodes, onValidateNodes,
nodes, nodes,
edges, edges,
logBuilds,
setLockChat, setLockChat,
}: BuildVerticesParams) { }: BuildVerticesParams) {
let url = `${BASE_URL_API}build/${flowId}/flow?`; let url = `${BASE_URL_API}build/${flowId}/flow?`;
@ -155,6 +157,9 @@ export async function buildFlowVertices({
if (stopNodeId) { if (stopNodeId) {
url = `${url}&stop_component_id=${stopNodeId}`; url = `${url}&stop_component_id=${stopNodeId}`;
} }
if (logBuilds !== undefined) {
url = `${url}&log_builds=${logBuilds}`;
}
const postData = {}; const postData = {};
if (typeof input_value !== "undefined") { if (typeof input_value !== "undefined") {
postData["inputs"] = { input_value: input_value }; postData["inputs"] = { input_value: input_value };

View file

@ -47,10 +47,6 @@ test("flowSettings", async ({ page }) => {
await page.getByTestId("save-flow-settings").click(); await page.getByTestId("save-flow-settings").click();
await page.getByText("Close").last().click();
await page.waitForTimeout(1000);
await page.getByText("Changes saved successfully").isVisible(); await page.getByText("Changes saved successfully").isVisible();
await page.getByTestId("flow_name").click(); await page.getByTestId("flow_name").click();

View file

@ -1,4 +1,4 @@
import { expect, test } from "@playwright/test"; import { test } from "@playwright/test";
import * as dotenv from "dotenv"; import * as dotenv from "dotenv";
import path from "path"; import path from "path";
@ -111,7 +111,6 @@ test("should share component with share button", async ({ page }) => {
.inputValue(); .inputValue();
await page.getByPlaceholder("Flow name").fill(randomName); await page.getByPlaceholder("Flow name").fill(randomName);
await page.getByText("Save").last().click(); await page.getByText("Save").last().click();
await page.getByText("Close").last().click();
await page.waitForSelector('[data-testid="shared-button-flow"]', { await page.waitForSelector('[data-testid="shared-button-flow"]', {
timeout: 100000, timeout: 100000,

View file

@ -31,6 +31,9 @@ export default defineConfig(({ mode }) => {
outDir: "build", outDir: "build",
}, },
define: { define: {
"process.env.LANGFLOW_AUTO_SAVE": JSON.stringify(
process.env.LANGFLOW_AUTO_SAVE,
),
"process.env.BACKEND_URL": JSON.stringify(process.env.BACKEND_URL), "process.env.BACKEND_URL": JSON.stringify(process.env.BACKEND_URL),
"process.env.ACCESS_TOKEN_EXPIRE_SECONDS": JSON.stringify( "process.env.ACCESS_TOKEN_EXPIRE_SECONDS": JSON.stringify(
process.env.ACCESS_TOKEN_EXPIRE_SECONDS, process.env.ACCESS_TOKEN_EXPIRE_SECONDS,