diff --git a/src/frontend/src/components/core/appHeaderComponent/components/FlowMenu/index.tsx b/src/frontend/src/components/core/appHeaderComponent/components/FlowMenu/index.tsx index 4a915a025..7aad08241 100644 --- a/src/frontend/src/components/core/appHeaderComponent/components/FlowMenu/index.tsx +++ b/src/frontend/src/components/core/appHeaderComponent/components/FlowMenu/index.tsx @@ -1,55 +1,37 @@ -import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react"; - -import { useCustomNavigate } from "@/customization/hooks/use-custom-navigate"; -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 { useHotkeys } from "react-hotkeys-hook"; +import { memo, useMemo, useRef, useState } from "react"; import IconComponent from "@/components/common/genericIconComponent"; import ShadTooltip from "@/components/common/shadTooltipComponent"; +import FlowSettingsComponent from "@/components/core/flowSettingsComponent"; import { Button } from "@/components/ui/button"; import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; -import { Input } from "@/components/ui/input"; -import { UPLOAD_ERROR_ALERT } from "@/constants/alerts_constants"; + Popover, + PopoverAnchor, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; import { SAVED_HOVER } from "@/constants/constants"; import { useGetRefreshFlowsQuery } from "@/controllers/API/queries/flows/use-get-refresh-flows-query"; import { useGetFoldersQuery } from "@/controllers/API/queries/folders/use-get-folders"; +import { useCustomNavigate } from "@/customization/hooks/use-custom-navigate"; +import useSaveFlow from "@/hooks/flows/use-save-flow"; import { useUnsavedChanges } from "@/hooks/use-unsaved-changes"; -import ExportModal from "@/modals/exportModal"; -import FlowLogsModal from "@/modals/flowLogsModal"; -import FlowSettingsModal from "@/modals/flowSettingsModal"; -import ToolbarSelectItem from "@/pages/FlowPage/components/nodeToolbarComponent/toolbarSelectItem"; import useAlertStore from "@/stores/alertStore"; import useFlowsManagerStore from "@/stores/flowsManagerStore"; import useFlowStore from "@/stores/flowStore"; import { useShortcutsStore } from "@/stores/shortcuts"; import { swatchColors } from "@/utils/styleUtils"; import { cn, getNumberFromString } from "@/utils/utils"; -import { useQueryClient } from "@tanstack/react-query"; +import { useHotkeys } from "react-hotkeys-hook"; import { useShallow } from "zustand/react/shallow"; export const MenuBar = memo((): JSX.Element => { - const shortcuts = useShortcutsStore((state) => state.shortcuts); - const addFlow = useAddFlow(); - const setErrorData = useAlertStore((state) => state.setErrorData); const setSuccessData = useAlertStore((state) => state.setSuccessData); - const undo = useFlowsManagerStore((state) => state.undo); - const redo = useFlowsManagerStore((state) => state.redo); const saveLoading = useFlowsManagerStore((state) => state.saveLoading); const [openSettings, setOpenSettings] = useState(false); - const [openLogs, setOpenLogs] = useState(false); - const uploadFlow = useUploadFlow(); const navigate = useCustomNavigate(); const isBuilding = useFlowStore((state) => state.isBuilding); const saveFlow = useSaveFlow(); - const queryClient = useQueryClient(); const autoSaving = useFlowsManagerStore((state) => state.autoSaving); const { currentFlowName, @@ -72,19 +54,10 @@ export const MenuBar = memo((): JSX.Element => { })), ); const onFlowPage = useFlowStore((state) => state.onFlowPage); - const setCurrentFlow = useFlowsManagerStore((state) => state.setCurrentFlow); - const stopBuilding = useFlowStore((state) => state.stopBuilding); - const [editingName, setEditingName] = useState(false); - const [flowName, setFlowName] = useState(currentFlowName ?? ""); - const [isInvalidName, setIsInvalidName] = useState(false); - const nameInputRef = useRef(null); - const [inputWidth, setInputWidth] = useState(0); const measureRef = useRef(null); const changesNotSaved = useUnsavedChanges(); - const [flowNames, setFlowNames] = useState([]); const { data: folders, isFetched: isFoldersFetched } = useGetFoldersQuery(); - const flows = useFlowsManagerStore((state) => state.flows); useGetRefreshFlowsQuery( { @@ -99,41 +72,6 @@ export const MenuBar = memo((): JSX.Element => { [folders, currentFlowFolderId], ); - function handleAddFlow() { - try { - addFlow().then((id) => { - setCurrentFlow(undefined); // Reset current flow for useEffect of flowPage to update the current flow - navigate("/flow/" + id); - }); - } catch (err) { - setErrorData(err as { title: string; list?: Array }); - } - } - - function handleReloadComponents() { - queryClient.prefetchQuery({ queryKey: ["useGetTypes"] }).then(() => { - setSuccessData({ title: "Components reloaded successfully" }); - }); - } - - function printByBuildStatus() { - if (isBuilding) { - return
Building...
; - } else if (saveLoading) { - return
Saving...
; - } - // return savedText; - return ( - - ); - } - const handleSave = () => { saveFlow().then(() => { setSuccessData({ title: "Saved successfully" }); @@ -143,90 +81,6 @@ export const MenuBar = memo((): JSX.Element => { const changes = useShortcutsStore((state) => state.changesSave); useHotkeys(changes, handleSave, { preventDefault: true }); - const handleEditName = useCallback( - (e: React.ChangeEvent) => { - const { value } = e.target; - const invalid = flowNames.includes(value); - setIsInvalidName(invalid); - setFlowName(value); - }, - [flowNames], - ); - - const handleKeyDown = useCallback( - (e: React.KeyboardEvent) => { - if (e.key === "Escape") { - setEditingName(false); - setFlowName(currentFlowName ?? ""); - setIsInvalidName(false); - } - if (e.key === "Enter") { - nameInputRef.current?.blur(); - } - }, - [currentFlowName], - ); - - const handleNameSubmit = useCallback(async () => { - if ( - flowName.trim() !== "" && - flowName !== currentFlowName && - !isInvalidName - ) { - const currentFlowSnapshot = useFlowStore.getState().currentFlow; - - const newFlow = { - ...currentFlowSnapshot!, - name: flowName, - id: currentFlowId!, - }; - - saveFlow(newFlow) - .then(() => { - setCurrentFlow(newFlow); - setSuccessData({ title: "Flow name updated successfully" }); - }) - .catch((error) => { - setErrorData({ - title: "Error updating flow name", - list: [(error as Error).message], - }); - setFlowName(currentFlowName ?? ""); - }); - } else if (isInvalidName) { - setErrorData({ - title: "Invalid flow name", - list: ["Name already exists"], - }); - setFlowName(currentFlowName ?? ""); - } else { - setFlowName(currentFlowName ?? ""); - } - setEditingName(false); - setIsInvalidName(false); - }, [ - flowName, - currentFlowName, - currentFlowId, - setCurrentFlow, - saveFlow, - setSuccessData, - setErrorData, - isInvalidName, - ]); - - useEffect(() => { - if (!editingName) { - setFlowName(currentFlowName ?? "Untitled Flow"); - } - }, [currentFlowName, editingName]); - - useEffect(() => { - if (measureRef.current) { - setInputWidth(measureRef.current.offsetWidth + 10); - } - }, [flowName, onFlowPage]); - const swatchIndex = (currentFlowGradient && !isNaN(parseInt(currentFlowGradient)) ? parseInt(currentFlowGradient) @@ -234,331 +88,118 @@ export const MenuBar = memo((): JSX.Element => { swatchColors.length; return onFlowPage ? ( -
- -
- / -
-
- -
- -
+ +
+
+ / +
+
+ +
+
- { - setEditingName(true); - setFlowName(currentFlowName ?? "Untitled Flow"); - const flows = useFlowsManagerStore.getState().flows; - setFlowNames( - flows - ?.map((flow) => flow.name) - .filter((name) => name !== currentFlowName) ?? [], - ); - }} - onBlur={handleNameSubmit} - value={flowName} - id="input-flow-name" - data-testid="input-flow-name" - placeholder="Untitled Flow" - /> -
-
- - + - - - Options - { - handleAddFlow(); - }} - className="cursor-pointer" - data-testid="menu_new_flow_button" - id="menu_new_flow_button" - > - - New - - - { - setOpenSettings(true); - }} - className="cursor-pointer" - data-testid="menu_edit_flow_button" - id="menu_edit_flow_button" - > - - Edit Details - - {!autoSaving && ( - - s.name.toLowerCase() === "changes save", - )?.shortcut! - } - /> - - )} - { - setOpenLogs(true); - }} - className="cursor-pointer" - data-testid="menu_logs_flow_button" - id="menu_logs_flow_button" - > - - Logs - - { - uploadFlow({ position: { x: 300, y: 100 } }) - .then(() => { - setSuccessData({ - title: "Uploaded successfully", - }); - }) - .catch((error) => { - setErrorData({ - title: UPLOAD_ERROR_ALERT, - list: [(error as Error).message], - }); - }); - }} - data-testid="menu_import_flow_button" - id="menu_import_flow_button" - > - - Import - - -
- - Export -
-
- { - undo(); - }} - className="cursor-pointer" - data-testid="menu_undo_flow_button" - id="menu_undo_flow_button" - > - s.name.toLowerCase() === "undo") - ?.shortcut! - } - /> - - { - redo(); - }} - className="cursor-pointer" - data-testid="menu_redo_flow_button" - id="menu_redo_flow_button" - > - s.name.toLowerCase() === "redo") - ?.shortcut! - } - /> - - { - handleReloadComponents(); - }} - className="cursor-pointer" - data-testid="menu_refresh_flow_button" - id="menu_refresh_flow_button" - > - - Refresh All - -
-
-
- - - -
-
- {!autoSaving && ( - - )} - -

- Auto-saving is disabled -

-

- - Enable auto-saving - {" "} - to avoid losing progress. -

-
- ) - } - side="bottom" - styleClasses="cursor-default z-10" - > -
-
-
- {printByBuildStatus()} -
- + side="bottom" + styleClasses="cursor-default z-10" + > +
+ +
+ + )}
- - - + + + + Flow Details + setOpenSettings(false)} /> + + ) : ( <> ); diff --git a/src/frontend/src/components/core/appHeaderComponent/components/HeaderMenu/index.tsx b/src/frontend/src/components/core/appHeaderComponent/components/HeaderMenu/index.tsx index 62d75987d..451df44bd 100644 --- a/src/frontend/src/components/core/appHeaderComponent/components/HeaderMenu/index.tsx +++ b/src/frontend/src/components/core/appHeaderComponent/components/HeaderMenu/index.tsx @@ -16,7 +16,7 @@ export const HeaderMenu = ({ children }) => ( export const HeaderMenuToggle = ({ children }) => ( diff --git a/src/frontend/src/components/core/appHeaderComponent/index.tsx b/src/frontend/src/components/core/appHeaderComponent/index.tsx index e9f88b8ad..20f5a9237 100644 --- a/src/frontend/src/components/core/appHeaderComponent/index.tsx +++ b/src/frontend/src/components/core/appHeaderComponent/index.tsx @@ -14,9 +14,7 @@ import { useCustomNavigate } from "@/customization/hooks/use-custom-navigate"; import useTheme from "@/customization/hooks/use-custom-theme"; import useAlertStore from "@/stores/alertStore"; import { useEffect, useRef, useState } from "react"; -import { AccountMenu } from "./components/AccountMenu"; import FlowMenu from "./components/FlowMenu"; -import LangflowCounts from "./components/langflow-counts"; export default function AppHeader(): JSX.Element { const notificationCenter = useAlertStore((state) => state.notificationCenter); @@ -47,13 +45,13 @@ export default function AppHeader(): JSX.Element { const getNotificationBadge = () => { const baseClasses = "absolute h-1 w-1 rounded-full bg-destructive"; return notificationCenter - ? `${baseClasses} right-[5.1rem] top-[5px]` + ? `${baseClasses} right-[0.3rem] top-[5px]` : "hidden"; }; return (
{/* Left Section */} @@ -82,13 +80,13 @@ export default function AppHeader(): JSX.Element {
{/* Middle Section */} -
+
{/* Right Section */}
<> @@ -119,7 +117,7 @@ export default function AppHeader(): JSX.Element { } data-testid="notification_button" > -
+
diff --git a/src/frontend/src/components/core/canvasControlsComponent/index.tsx b/src/frontend/src/components/core/canvasControlsComponent/index.tsx index 5ec115f0d..b79160bc0 100644 --- a/src/frontend/src/components/core/canvasControlsComponent/index.tsx +++ b/src/frontend/src/components/core/canvasControlsComponent/index.tsx @@ -39,17 +39,20 @@ export const CustomControlButton = ({ return ( - +
@@ -109,7 +112,7 @@ const CanvasControls = ({ children }) => { return ( {/* Zoom In */} @@ -135,6 +138,7 @@ const CanvasControls = ({ children }) => { onClick={fitView} testId="fit_view" /> + {children} {/* Lock/Unlock */} { } testId="lock_unlock" /> - {children} ); }; diff --git a/src/frontend/src/components/core/editFlowSettingsComponent/index.tsx b/src/frontend/src/components/core/editFlowSettingsComponent/index.tsx index b2009d739..a67a85898 100644 --- a/src/frontend/src/components/core/editFlowSettingsComponent/index.tsx +++ b/src/frontend/src/components/core/editFlowSettingsComponent/index.tsx @@ -1,6 +1,6 @@ import React, { ChangeEvent, useState } from "react"; import { InputProps } from "../../../types/components"; -import { cn, isEndpointNameValid } from "../../../utils/utils"; +import { cn } from "../../../utils/utils"; import { Input } from "../../ui/input"; import { Label } from "../../ui/label"; import { Textarea } from "../../ui/textarea"; @@ -9,16 +9,15 @@ export const EditFlowSettings: React.FC = ({ name, invalidNameList = [], description, - endpointName, maxLength = 50, + descriptionMaxLength = 250, minLength = 1, setName, setDescription, - setEndpointName, }: InputProps): JSX.Element => { const [isMaxLength, setIsMaxLength] = useState(false); + const [isMaxDescriptionLength, setIsMaxDescriptionLength] = useState(false); const [isMinLength, setIsMinLength] = useState(false); - const [validEndpointName, setValidEndpointName] = useState(true); const [isInvalidName, setIsInvalidName] = useState(false); const handleNameChange = (event: ChangeEvent) => { @@ -43,34 +42,22 @@ export const EditFlowSettings: React.FC = ({ } setIsInvalidName(invalid); - // Only update the name if it's valid (not empty and not invalid) - if (value.length >= minLength && !invalid) { - setName!(value); - } else if (value.length === 0) { + setName!(value); + + if (value.length === 0) { // For empty string, update state but keep isMinLength true - setName!(""); setIsMinLength(true); } }; const handleDescriptionChange = (event: ChangeEvent) => { - setDescription!(event.target.value); - }; - - const handleEndpointNameChange = (event: ChangeEvent) => { const { value } = event.target; - // Validate the endpoint name - // use this regex r'^[a-zA-Z0-9_-]+$' - const isValid = isEndpointNameValid(event.target.value, maxLength); - setValidEndpointName(isValid); - - // Only update if valid and meets minimum length (if set) - if (isValid && value.length >= minLength) { - setEndpointName!(value); - } else if (value.length === 0) { - // Always allow empty endpoint name (it's optional) - setEndpointName!(""); + if (value.length >= descriptionMaxLength) { + setIsMaxDescriptionLength(true); + } else { + setIsMaxDescriptionLength(false); } + setDescription!(value); }; //this function is necessary to select the text when double clicking, this was not working with the onFocus event @@ -80,7 +67,7 @@ export const EditFlowSettings: React.FC = ({ <>