feat: allow flow name to be edited from the main page or by clicking header (#5525)
* Added flowData to allow changing flow name without being on the flow * Added being able to edit the flow name just by clicking the name * Added handleEdit and edit details option on flow dropdown * Added flow settings modal to grid and list * Added flowData type and select option * Update src/frontend/src/components/core/appHeaderComponent/components/FlowMenu/index.tsx Co-authored-by: Cristhian Zanforlin Lousa <cristhian.lousa@gmail.com> * Update src/frontend/src/components/core/appHeaderComponent/components/FlowMenu/index.tsx Co-authored-by: Cristhian Zanforlin Lousa <cristhian.lousa@gmail.com> * Update src/frontend/src/components/core/appHeaderComponent/components/FlowMenu/index.tsx Co-authored-by: Cristhian Zanforlin Lousa <cristhian.lousa@gmail.com> * Fixed useCallback import * Implemented details boolean to not show endpoint, changed title of flow settings modal * Changed Flow Settings to Edit Details * Changed Flow Settings modal description * Fixed empty name not appearing, fixed naming conditions not considered * 📝 (editFlowSettingsComponent/index.tsx): add data-testid attribute to input element for flow name for testing purposes ✨ (edit-flow-name.spec.ts): create test to ensure user can edit flow name by clicking on the header or main page, with various random names generated for testing purposes * Fixed tests that used Flow Settings * Fixed tests that rely on flow name to open edit details * Fixed tests --------- Co-authored-by: Cristhian Zanforlin Lousa <cristhian.lousa@gmail.com> Co-authored-by: anovazzi1 <otavio2204@gmail.com>
This commit is contained in:
parent
a87bd8b57b
commit
3e482dbc29
14 changed files with 456 additions and 162 deletions
|
|
@ -1,4 +1,4 @@
|
|||
import { useMemo, useState } from "react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
|
||||
import { useCustomNavigate } from "@/customization/hooks/use-custom-navigate";
|
||||
import useAddFlow from "@/hooks/flows/use-add-flow";
|
||||
|
|
@ -17,6 +17,7 @@ import {
|
|||
DropdownMenuLabel,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { UPLOAD_ERROR_ALERT } from "@/constants/alerts_constants";
|
||||
import { SAVED_HOVER } from "@/constants/constants";
|
||||
import { useGetRefreshFlowsQuery } from "@/controllers/API/queries/flows/use-get-refresh-flows-query";
|
||||
|
|
@ -54,8 +55,26 @@ export const MenuBar = ({}: {}): 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(currentFlow?.name ?? "");
|
||||
const [isInvalidName, setIsInvalidName] = useState(false);
|
||||
const nameInputRef = useRef<HTMLInputElement>(null);
|
||||
const [inputWidth, setInputWidth] = useState<number>(0);
|
||||
const measureRef = useRef<HTMLSpanElement>(null);
|
||||
|
||||
const { data: folders, isFetched: isFoldersFetched } = useGetFoldersQuery();
|
||||
const flows = useFlowsManagerStore((state) => state.flows);
|
||||
const [nameLists, setNameList] = useState<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (flows) {
|
||||
const tempNameList: string[] = [];
|
||||
flows.forEach((flow) => {
|
||||
tempNameList.push(flow.name);
|
||||
});
|
||||
setNameList(tempNameList.filter((name) => name !== currentFlow?.name));
|
||||
}
|
||||
}, [flows, currentFlow?.name]);
|
||||
|
||||
useGetRefreshFlowsQuery(
|
||||
{
|
||||
|
|
@ -73,6 +92,12 @@ export const MenuBar = ({}: {}): JSX.Element => {
|
|||
const changesNotSaved =
|
||||
customStringify(currentFlow) !== customStringify(currentSavedFlow);
|
||||
|
||||
useEffect(() => {
|
||||
if (measureRef.current) {
|
||||
setInputWidth(measureRef.current.offsetWidth);
|
||||
}
|
||||
}, [flowName]);
|
||||
|
||||
function handleAddFlow() {
|
||||
try {
|
||||
addFlow().then((id) => {
|
||||
|
|
@ -113,6 +138,80 @@ export const MenuBar = ({}: {}): JSX.Element => {
|
|||
const changes = useShortcutsStore((state) => state.changesSave);
|
||||
useHotkeys(changes, handleSave, { preventDefault: true });
|
||||
|
||||
const handleEditName = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const { value } = e.target;
|
||||
let invalid = false;
|
||||
for (let i = 0; i < nameLists.length; i++) {
|
||||
if (value === nameLists[i]) {
|
||||
invalid = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
setIsInvalidName(invalid);
|
||||
setFlowName(value);
|
||||
},
|
||||
[nameLists],
|
||||
);
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === "Escape") {
|
||||
setEditingName(false);
|
||||
setFlowName(currentFlow?.name ?? "");
|
||||
setIsInvalidName(false);
|
||||
}
|
||||
if (e.key === "Enter") {
|
||||
nameInputRef.current?.blur();
|
||||
}
|
||||
},
|
||||
[currentFlow?.name],
|
||||
);
|
||||
|
||||
const handleNameSubmit = useCallback(() => {
|
||||
if (
|
||||
flowName.trim() !== "" &&
|
||||
flowName !== currentFlow?.name &&
|
||||
!isInvalidName
|
||||
) {
|
||||
const newFlow = {
|
||||
...currentFlow!,
|
||||
name: flowName,
|
||||
id: currentFlow!.id,
|
||||
};
|
||||
setCurrentFlow(newFlow);
|
||||
saveFlow(newFlow)
|
||||
.then(() => {
|
||||
setSuccessData({ title: "Flow name updated successfully" });
|
||||
})
|
||||
.catch((error) => {
|
||||
setErrorData({
|
||||
title: "Error updating flow name",
|
||||
list: [(error as Error).message],
|
||||
});
|
||||
setFlowName(currentFlow?.name ?? "");
|
||||
});
|
||||
} else if (isInvalidName) {
|
||||
setErrorData({
|
||||
title: "Invalid flow name",
|
||||
list: ["Name already exists"],
|
||||
});
|
||||
setFlowName(currentFlow?.name ?? "");
|
||||
} else {
|
||||
setFlowName(currentFlow?.name ?? "");
|
||||
}
|
||||
setEditingName(false);
|
||||
setIsInvalidName(false);
|
||||
}, [
|
||||
flowName,
|
||||
currentFlow,
|
||||
setCurrentFlow,
|
||||
saveFlow,
|
||||
setSuccessData,
|
||||
setErrorData,
|
||||
isInvalidName,
|
||||
]);
|
||||
|
||||
return currentFlow && onFlowPage ? (
|
||||
<div className="flex items-center justify-center gap-2 truncate">
|
||||
<div className="header-menu-bar hidden w-20 max-w-fit grow justify-end truncate md:flex">
|
||||
|
|
@ -138,148 +237,191 @@ export const MenuBar = ({}: {}): JSX.Element => {
|
|||
</div>
|
||||
|
||||
<div className="overflow-hidden truncate text-sm sm:whitespace-normal">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<div className="header-menu-bar-display-2 group truncate">
|
||||
<div className="header-menu-bar-display-2 truncate">
|
||||
<div
|
||||
className="header-menu-flow-name-2 truncate"
|
||||
data-testid="flow-configuration-button"
|
||||
>
|
||||
<span
|
||||
ref={measureRef}
|
||||
className="invisible absolute font-semibold"
|
||||
style={{ whiteSpace: "pre" }}
|
||||
>
|
||||
{flowName}
|
||||
</span>
|
||||
{editingName ? (
|
||||
<>
|
||||
<Input
|
||||
className={cn(
|
||||
"h-6 px-0 font-semibold focus:border-0",
|
||||
isInvalidName &&
|
||||
"border-status-red focus-visible:ring-status-red",
|
||||
)}
|
||||
style={{ width: `${inputWidth + 1}px` }}
|
||||
onChange={handleEditName}
|
||||
maxLength={38}
|
||||
ref={nameInputRef}
|
||||
onKeyDown={handleKeyDown}
|
||||
autoFocus={true}
|
||||
onBlur={handleNameSubmit}
|
||||
value={flowName}
|
||||
id="input-flow-name"
|
||||
data-testid="input-flow-name"
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<div
|
||||
className="header-menu-flow-name-2 truncate"
|
||||
data-testid="flow-configuration-button"
|
||||
className="truncate font-semibold text-primary"
|
||||
data-testid="flow_name"
|
||||
onClick={() => {
|
||||
setEditingName(true);
|
||||
setFlowName(currentFlow.name);
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="truncate font-semibold group-hover:text-primary dark:text-[white]"
|
||||
data-testid="flow_name"
|
||||
>
|
||||
{currentFlow.name}
|
||||
</div>
|
||||
{currentFlow.name}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
className="group"
|
||||
data-testid="flow_menu_trigger"
|
||||
>
|
||||
<IconComponent
|
||||
name="ChevronDown"
|
||||
className="flex h-5 w-5 text-muted-foreground group-hover:text-primary"
|
||||
className="flex h-5 w-5 text-muted-foreground hover:text-primary"
|
||||
/>
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-44 bg-white dark:bg-background">
|
||||
<DropdownMenuLabel>Options</DropdownMenuLabel>
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
handleAddFlow();
|
||||
}}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<IconComponent name="Plus" className="header-menu-options" />
|
||||
New
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-44 bg-white dark:bg-background">
|
||||
<DropdownMenuLabel>Options</DropdownMenuLabel>
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
handleAddFlow();
|
||||
}}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<IconComponent name="Plus" className="header-menu-options" />
|
||||
New
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
setOpenSettings(true);
|
||||
}}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<IconComponent name="Settings2" className="header-menu-options" />
|
||||
Flow Settings
|
||||
</DropdownMenuItem>
|
||||
{!autoSaving && (
|
||||
<DropdownMenuItem onClick={handleSave} className="cursor-pointer">
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
setOpenSettings(true);
|
||||
}}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<IconComponent
|
||||
name="SquarePen"
|
||||
className="header-menu-options"
|
||||
/>
|
||||
Edit Details
|
||||
</DropdownMenuItem>
|
||||
{!autoSaving && (
|
||||
<DropdownMenuItem
|
||||
onClick={handleSave}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<ToolbarSelectItem
|
||||
value="Save"
|
||||
icon="Save"
|
||||
dataTestId=""
|
||||
shortcut={
|
||||
shortcuts.find(
|
||||
(s) => s.name.toLowerCase() === "changes save",
|
||||
)?.shortcut!
|
||||
}
|
||||
/>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
setOpenLogs(true);
|
||||
}}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<IconComponent
|
||||
name="ScrollText"
|
||||
className="header-menu-options"
|
||||
/>
|
||||
Logs
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
onClick={() => {
|
||||
uploadFlow({ position: { x: 300, y: 100 } })
|
||||
.then(() => {
|
||||
setSuccessData({
|
||||
title: "Uploaded successfully",
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
setErrorData({
|
||||
title: UPLOAD_ERROR_ALERT,
|
||||
list: [(error as Error).message],
|
||||
});
|
||||
});
|
||||
}}
|
||||
>
|
||||
<IconComponent name="FileUp" className="header-menu-options" />
|
||||
Import
|
||||
</DropdownMenuItem>
|
||||
<ExportModal>
|
||||
<div className="header-menubar-item">
|
||||
<IconComponent
|
||||
name="FileDown"
|
||||
className="header-menu-options"
|
||||
/>
|
||||
Export
|
||||
</div>
|
||||
</ExportModal>
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
undo();
|
||||
}}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<ToolbarSelectItem
|
||||
value="Save"
|
||||
icon="Save"
|
||||
value="Undo"
|
||||
icon="Undo"
|
||||
dataTestId=""
|
||||
shortcut={
|
||||
shortcuts.find(
|
||||
(s) => s.name.toLowerCase() === "changes save",
|
||||
)?.shortcut!
|
||||
shortcuts.find((s) => s.name.toLowerCase() === "undo")
|
||||
?.shortcut!
|
||||
}
|
||||
/>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
setOpenLogs(true);
|
||||
}}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<IconComponent
|
||||
name="ScrollText"
|
||||
className="header-menu-options"
|
||||
/>
|
||||
Logs
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
onClick={() => {
|
||||
uploadFlow({ position: { x: 300, y: 100 } })
|
||||
.then(() => {
|
||||
setSuccessData({
|
||||
title: "Uploaded successfully",
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
setErrorData({
|
||||
title: UPLOAD_ERROR_ALERT,
|
||||
list: [(error as Error).message],
|
||||
});
|
||||
});
|
||||
}}
|
||||
>
|
||||
<IconComponent name="FileUp" className="header-menu-options" />
|
||||
Import
|
||||
</DropdownMenuItem>
|
||||
<ExportModal>
|
||||
<div className="header-menubar-item">
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
redo();
|
||||
}}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<ToolbarSelectItem
|
||||
value="Redo"
|
||||
icon="Redo"
|
||||
dataTestId=""
|
||||
shortcut={
|
||||
shortcuts.find((s) => s.name.toLowerCase() === "redo")
|
||||
?.shortcut!
|
||||
}
|
||||
/>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
handleReloadComponents();
|
||||
}}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<IconComponent
|
||||
name="FileDown"
|
||||
name="RefreshCcw"
|
||||
className="header-menu-options"
|
||||
/>
|
||||
Export
|
||||
</div>
|
||||
</ExportModal>
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
undo();
|
||||
}}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<ToolbarSelectItem
|
||||
value="Undo"
|
||||
icon="Undo"
|
||||
dataTestId=""
|
||||
shortcut={
|
||||
shortcuts.find((s) => s.name.toLowerCase() === "undo")
|
||||
?.shortcut!
|
||||
}
|
||||
/>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
redo();
|
||||
}}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<ToolbarSelectItem
|
||||
value="Redo"
|
||||
icon="Redo"
|
||||
dataTestId=""
|
||||
shortcut={
|
||||
shortcuts.find((s) => s.name.toLowerCase() === "redo")
|
||||
?.shortcut!
|
||||
}
|
||||
/>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
handleReloadComponents();
|
||||
}}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<IconComponent
|
||||
name="RefreshCcw"
|
||||
className="header-menu-options"
|
||||
/>
|
||||
Refresh All
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
Refresh All
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
|
||||
<FlowSettingsModal
|
||||
open={openSettings}
|
||||
setOpen={setOpenSettings}
|
||||
|
|
|
|||
|
|
@ -80,6 +80,7 @@ export const EditFlowSettings: React.FC<InputProps> = ({
|
|||
onDoubleClickCapture={(event) => {
|
||||
handleFocus(event);
|
||||
}}
|
||||
data-testid="input-flow-name"
|
||||
/>
|
||||
) : (
|
||||
<span className="font-normal text-muted-foreground word-break-break-word">
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@ export const EXPORT_DIALOG_SUBTITLE = "Export flow as JSON file.";
|
|||
* @constant
|
||||
*/
|
||||
export const SETTINGS_DIALOG_SUBTITLE =
|
||||
"Customize workspace settings and preferences.";
|
||||
"Customize your flow details and settings.";
|
||||
|
||||
/**
|
||||
* The base text for subtitle of Flow Logs (Menubar)
|
||||
|
|
|
|||
|
|
@ -15,29 +15,30 @@ import BaseModal from "../baseModal";
|
|||
export default function FlowSettingsModal({
|
||||
open,
|
||||
setOpen,
|
||||
flowData,
|
||||
details,
|
||||
}: FlowSettingsPropsType): JSX.Element {
|
||||
const saveFlow = useSaveFlow();
|
||||
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 flow = flowData ?? currentFlow;
|
||||
useEffect(() => {
|
||||
setName(currentFlow!.name);
|
||||
setDescription(currentFlow!.description);
|
||||
}, [currentFlow?.name, currentFlow?.description, open]);
|
||||
setName(flow!.name);
|
||||
setDescription(flow!.description);
|
||||
}, [flow?.name, flow?.description, open]);
|
||||
|
||||
const [name, setName] = useState(currentFlow!.name);
|
||||
const [description, setDescription] = useState(currentFlow!.description);
|
||||
const [endpoint_name, setEndpointName] = useState(
|
||||
currentFlow!.endpoint_name ?? "",
|
||||
);
|
||||
const [name, setName] = useState(flow!.name);
|
||||
const [description, setDescription] = useState(flow!.description);
|
||||
const [endpoint_name, setEndpointName] = useState(flow!.endpoint_name ?? "");
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [disableSave, setDisableSave] = useState(true);
|
||||
const autoSaving = useFlowsManagerStore((state) => state.autoSaving);
|
||||
function handleClick(): void {
|
||||
setIsSaving(true);
|
||||
if (!currentFlow) return;
|
||||
const newFlow = cloneDeep(currentFlow);
|
||||
if (!flow) return;
|
||||
const newFlow = cloneDeep(flow);
|
||||
newFlow.name = name;
|
||||
newFlow.description = description;
|
||||
newFlow.endpoint_name =
|
||||
|
|
@ -67,22 +68,22 @@ export default function FlowSettingsModal({
|
|||
flows.forEach((flow: FlowType) => {
|
||||
tempNameList.push(flow.name);
|
||||
});
|
||||
setNameList(tempNameList.filter((name) => name !== currentFlow!.name));
|
||||
setNameList(tempNameList.filter((name) => name !== flow!.name));
|
||||
}
|
||||
}, [flows]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
(!nameLists.includes(name) && currentFlow?.name !== name) ||
|
||||
currentFlow?.description !== description ||
|
||||
((currentFlow?.endpoint_name ?? "") !== endpoint_name &&
|
||||
(!nameLists.includes(name) && flow?.name !== name) ||
|
||||
flow?.description !== description ||
|
||||
((flow?.endpoint_name ?? "") !== endpoint_name &&
|
||||
isEndpointNameValid(endpoint_name ?? "", 50))
|
||||
) {
|
||||
setDisableSave(false);
|
||||
} else {
|
||||
setDisableSave(true);
|
||||
}
|
||||
}, [nameLists, currentFlow, description, endpoint_name, name]);
|
||||
}, [nameLists, flow, description, endpoint_name, name]);
|
||||
return (
|
||||
<BaseModal
|
||||
open={open}
|
||||
|
|
@ -91,8 +92,8 @@ export default function FlowSettingsModal({
|
|||
onSubmit={handleClick}
|
||||
>
|
||||
<BaseModal.Header description={SETTINGS_DIALOG_SUBTITLE}>
|
||||
<span className="pr-2">Settings</span>
|
||||
<IconComponent name="Settings2" className="mr-2 h-4 w-4" />
|
||||
<span className="pr-2">Details</span>
|
||||
<IconComponent name="SquarePen" className="mr-2 h-4 w-4" />
|
||||
</BaseModal.Header>
|
||||
<BaseModal.Content>
|
||||
<EditFlowSettings
|
||||
|
|
@ -102,7 +103,7 @@ export default function FlowSettingsModal({
|
|||
endpointName={endpoint_name}
|
||||
setName={setName}
|
||||
setDescription={setDescription}
|
||||
setEndpointName={setEndpointName}
|
||||
setEndpointName={details ? undefined : setEndpointName}
|
||||
/>
|
||||
</BaseModal.Content>
|
||||
|
||||
|
|
|
|||
|
|
@ -10,11 +10,13 @@ type DropdownComponentProps = {
|
|||
flowData: FlowType;
|
||||
setOpenDelete: (open: boolean) => void;
|
||||
handlePlaygroundClick?: () => void;
|
||||
handleEdit: () => void;
|
||||
};
|
||||
|
||||
const DropdownComponent = ({
|
||||
flowData,
|
||||
setOpenDelete,
|
||||
handleEdit,
|
||||
}: DropdownComponentProps) => {
|
||||
const setSuccessData = useAlertStore((state) => state.setSuccessData);
|
||||
const setErrorData = useAlertStore((state) => state.setErrorData);
|
||||
|
|
@ -29,17 +31,32 @@ const DropdownComponent = ({
|
|||
downloadFlow(flowData, flowData.name, flowData.description);
|
||||
setSuccessData({ title: `${flowData.name} exported successfully` });
|
||||
};
|
||||
|
||||
const { handleSelectOptionsChange } = useSelectOptionsChange(
|
||||
[flowData.id],
|
||||
setErrorData,
|
||||
setOpenDelete,
|
||||
handleDuplicate,
|
||||
handleExport,
|
||||
handleEdit,
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleSelectOptionsChange("edit");
|
||||
}}
|
||||
className="cursor-pointer"
|
||||
data-testid="btn-edit-flow"
|
||||
>
|
||||
<ForwardedIconComponent
|
||||
name="SquarePen"
|
||||
aria-hidden="true"
|
||||
className="mr-2 h-4 w-4"
|
||||
/>
|
||||
Edit details
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
|
|
|
|||
|
|
@ -10,10 +10,10 @@ import {
|
|||
import { useCustomNavigate } from "@/customization/hooks/use-custom-navigate";
|
||||
import useDeleteFlow from "@/hooks/flows/use-delete-flow";
|
||||
import DeleteConfirmationModal from "@/modals/deleteConfirmationModal";
|
||||
import FlowSettingsModal from "@/modals/flowSettingsModal";
|
||||
import useAlertStore from "@/stores/alertStore";
|
||||
import useFlowsManagerStore from "@/stores/flowsManagerStore";
|
||||
import { FlowType } from "@/types/flow";
|
||||
import { getInputsAndOutputs } from "@/utils/storeUtils";
|
||||
import { swatchColors } from "@/utils/styleUtils";
|
||||
import { cn, getNumberFromString } from "@/utils/utils";
|
||||
import { useState } from "react";
|
||||
|
|
@ -27,6 +27,7 @@ const GridComponent = ({ flowData }: { flowData: FlowType }) => {
|
|||
const navigate = useCustomNavigate();
|
||||
|
||||
const [openDelete, setOpenDelete] = useState(false);
|
||||
const [openSettings, setOpenSettings] = useState(false);
|
||||
const setSuccessData = useAlertStore((state) => state.setSuccessData);
|
||||
const { deleteFlow } = useDeleteFlow();
|
||||
|
||||
|
|
@ -124,6 +125,9 @@ const GridComponent = ({ flowData }: { flowData: FlowType }) => {
|
|||
<DropdownComponent
|
||||
flowData={flowData}
|
||||
setOpenDelete={setOpenDelete}
|
||||
handleEdit={() => {
|
||||
setOpenSettings(true);
|
||||
}}
|
||||
/>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
|
@ -145,6 +149,12 @@ const GridComponent = ({ flowData }: { flowData: FlowType }) => {
|
|||
<></>
|
||||
</DeleteConfirmationModal>
|
||||
)}
|
||||
<FlowSettingsModal
|
||||
open={openSettings}
|
||||
setOpen={setOpenSettings}
|
||||
flowData={flowData}
|
||||
details
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import {
|
|||
import { useCustomNavigate } from "@/customization/hooks/use-custom-navigate";
|
||||
import useDeleteFlow from "@/hooks/flows/use-delete-flow";
|
||||
import DeleteConfirmationModal from "@/modals/deleteConfirmationModal";
|
||||
import FlowSettingsModal from "@/modals/flowSettingsModal";
|
||||
import useAlertStore from "@/stores/alertStore";
|
||||
import useFlowsManagerStore from "@/stores/flowsManagerStore";
|
||||
import { FlowType } from "@/types/flow";
|
||||
|
|
@ -30,6 +31,7 @@ const ListComponent = ({ flowData }: { flowData: FlowType }) => {
|
|||
const { deleteFlow } = useDeleteFlow();
|
||||
const setErrorData = useAlertStore((state) => state.setErrorData);
|
||||
const { folderId } = useParams();
|
||||
const [openSettings, setOpenSettings] = useState(false);
|
||||
const isComponent = flowData.is_component ?? false;
|
||||
const setFlowToCanvas = useFlowsManagerStore(
|
||||
(state) => state.setFlowToCanvas,
|
||||
|
|
@ -144,6 +146,9 @@ const ListComponent = ({ flowData }: { flowData: FlowType }) => {
|
|||
<DropdownComponent
|
||||
flowData={flowData}
|
||||
setOpenDelete={setOpenDelete}
|
||||
handleEdit={() => {
|
||||
setOpenSettings(true);
|
||||
}}
|
||||
handlePlaygroundClick={() => {
|
||||
// handlePlaygroundClick();
|
||||
}}
|
||||
|
|
@ -163,6 +168,12 @@ const ListComponent = ({ flowData }: { flowData: FlowType }) => {
|
|||
<></>
|
||||
</DeleteConfirmationModal>
|
||||
)}
|
||||
<FlowSettingsModal
|
||||
open={openSettings}
|
||||
setOpen={setOpenSettings}
|
||||
flowData={flowData}
|
||||
details
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ const useSelectOptionsChange = (
|
|||
setOpenDelete: (value: boolean) => void,
|
||||
handleDuplicate: () => void,
|
||||
handleExport: () => void,
|
||||
handleEdit: () => void,
|
||||
) => {
|
||||
const handleSelectOptionsChange = useCallback(
|
||||
(action) => {
|
||||
|
|
@ -23,6 +24,8 @@ const useSelectOptionsChange = (
|
|||
handleDuplicate();
|
||||
} else if (action === "export") {
|
||||
handleExport();
|
||||
} else if (action === "edit") {
|
||||
handleEdit();
|
||||
}
|
||||
},
|
||||
[
|
||||
|
|
@ -31,6 +34,7 @@ const useSelectOptionsChange = (
|
|||
setOpenDelete,
|
||||
handleDuplicate,
|
||||
handleExport,
|
||||
handleEdit,
|
||||
],
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -672,6 +672,8 @@ export type buttonBoxPropsType = {
|
|||
export type FlowSettingsPropsType = {
|
||||
open: boolean;
|
||||
setOpen: (open: boolean) => void;
|
||||
details?: boolean;
|
||||
flowData?: FlowType;
|
||||
};
|
||||
|
||||
export type groupDataType = {
|
||||
|
|
|
|||
|
|
@ -128,8 +128,8 @@ test(
|
|||
await page.getByTestId("fit_view").click();
|
||||
await page.getByTestId("zoom_out").click();
|
||||
|
||||
await page.getByTestId("flow-configuration-button").click();
|
||||
await page.getByText("Flow Settings", { exact: true }).last().click();
|
||||
await page.getByTestId("flow_menu_trigger").click();
|
||||
await page.getByText("Edit Details", { exact: true }).last().click();
|
||||
|
||||
await page.getByPlaceholder("Flow Name").fill(randomFlowName);
|
||||
|
||||
|
|
@ -201,8 +201,8 @@ test(
|
|||
await page.getByTestId("fit_view").click();
|
||||
await page.getByTestId("zoom_out").click();
|
||||
|
||||
await page.getByTestId("flow-configuration-button").click();
|
||||
await page.getByText("Flow Settings", { exact: true }).last().click();
|
||||
await page.getByTestId("flow_menu_trigger").click();
|
||||
await page.getByText("Edit Details", { exact: true }).last().click();
|
||||
|
||||
await page.getByPlaceholder("Flow Name").fill(secondRandomFlowName);
|
||||
|
||||
|
|
|
|||
|
|
@ -116,8 +116,8 @@ test("should share component with share button", async ({ page }) => {
|
|||
await page.getByRole("heading", { name: "Basic Prompting" }).click();
|
||||
await page.waitForTimeout(1000);
|
||||
const flowName = await page.getByTestId("flow_name").innerText();
|
||||
await page.getByTestId("flow_name").click();
|
||||
await page.getByText("Flow Settings").click();
|
||||
await page.getByTestId("flow_menu_trigger").click();
|
||||
await page.getByText("Edit Details").click();
|
||||
const flowDescription = await page
|
||||
.getByPlaceholder("Flow description")
|
||||
.inputValue();
|
||||
|
|
|
|||
|
|
@ -25,8 +25,8 @@ test(
|
|||
|
||||
await page.getByTestId("fit_view").click();
|
||||
|
||||
await page.getByTestId("flow-configuration-button").click();
|
||||
await page.getByText("Flow Settings").click();
|
||||
await page.getByTestId("flow_menu_trigger").click();
|
||||
await page.getByText("Edit Details").click();
|
||||
await page.getByPlaceholder("Flow name").fill(randomName);
|
||||
await page.getByText("Save").last().click();
|
||||
await page.getByTestId("icon-ChevronLeft").last().click();
|
||||
|
|
@ -72,8 +72,8 @@ test(
|
|||
|
||||
await page.getByTestId(`card-${randomName}`).first().click();
|
||||
|
||||
await page.getByTestId("flow-configuration-button").click();
|
||||
await page.getByText("Flow Settings").click();
|
||||
await page.getByTestId("flow_menu_trigger").click();
|
||||
await page.getByText("Edit Details").click();
|
||||
await page.getByPlaceholder("Flow name").fill(secondRandomName);
|
||||
await page.getByText("Save").last().click();
|
||||
await page.getByTestId("icon-ChevronLeft").last().click();
|
||||
|
|
|
|||
106
src/frontend/tests/extended/features/edit-flow-name.spec.ts
Normal file
106
src/frontend/tests/extended/features/edit-flow-name.spec.ts
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
import { expect, test } from "@playwright/test";
|
||||
import { readFileSync } from "fs";
|
||||
import { awaitBootstrapTest } from "../../utils/await-bootstrap-test";
|
||||
import { simulateDragAndDrop } from "../../utils/simulate-drag-and-drop";
|
||||
test(
|
||||
"user should be able to edit flow name by clicking on the header or on the main page",
|
||||
{ tag: ["@release"] },
|
||||
async ({ page }) => {
|
||||
const randomName = Math.random().toString(36).substring(2, 15);
|
||||
const randomName2 = Math.random().toString(36).substring(2, 15);
|
||||
const randomName3 = Math.random().toString(36).substring(2, 15);
|
||||
const randomName4 = Math.random().toString(36).substring(2, 15);
|
||||
|
||||
await awaitBootstrapTest(page);
|
||||
|
||||
await page.getByRole("heading", { name: "Basic Prompting" }).click();
|
||||
|
||||
await page.getByTestId("flow_name").click();
|
||||
|
||||
await page.getByTestId("input-flow-name").fill(randomName);
|
||||
|
||||
await page.keyboard.press("Enter");
|
||||
|
||||
let flowName = await page.getByTestId("flow_name").textContent();
|
||||
|
||||
expect(flowName).toBe(randomName);
|
||||
|
||||
await page.getByTestId("icon-ChevronLeft").first().click();
|
||||
|
||||
await page.waitForSelector('[data-testid="home-dropdown-menu"]', {
|
||||
timeout: 5000,
|
||||
});
|
||||
|
||||
await page.waitForSelector(`text=${randomName}`, {
|
||||
timeout: 3000,
|
||||
state: "visible",
|
||||
});
|
||||
|
||||
expect(await page.getByText(randomName).count()).toBe(1);
|
||||
|
||||
await page.getByText(randomName).click();
|
||||
|
||||
await page.getByTestId("flow_name").click();
|
||||
|
||||
await page.getByTestId("input-flow-name").fill(randomName2);
|
||||
|
||||
await page.keyboard.press("Enter");
|
||||
|
||||
flowName = await page.getByTestId("flow_name").textContent();
|
||||
|
||||
expect(flowName).toBe(randomName2);
|
||||
|
||||
await page.getByTestId("icon-ChevronLeft").first().click();
|
||||
|
||||
await page.waitForSelector('[data-testid="home-dropdown-menu"]', {
|
||||
timeout: 5000,
|
||||
});
|
||||
|
||||
await page.waitForSelector(`text=${randomName2}`, {
|
||||
timeout: 3000,
|
||||
state: "visible",
|
||||
});
|
||||
|
||||
expect(await page.getByText(randomName2).count()).toBe(1);
|
||||
|
||||
await page.getByTestId("home-dropdown-menu").first().click();
|
||||
|
||||
await page.getByTestId("btn-edit-flow").click();
|
||||
|
||||
await page.getByTestId("input-flow-name").fill(randomName3);
|
||||
|
||||
await page.getByTestId("save-flow-settings").click();
|
||||
|
||||
await page.waitForSelector(`text=${randomName3}`, {
|
||||
timeout: 3000,
|
||||
state: "visible",
|
||||
});
|
||||
|
||||
expect(await page.getByText(randomName3).count()).toBe(1);
|
||||
|
||||
await page.getByText(randomName3).click();
|
||||
|
||||
await page.getByTestId("flow_name").click();
|
||||
|
||||
await page.getByTestId("input-flow-name").fill(randomName4);
|
||||
|
||||
await page.keyboard.press("Enter");
|
||||
|
||||
flowName = await page.getByTestId("flow_name").textContent();
|
||||
|
||||
expect(flowName).toBe(randomName4);
|
||||
|
||||
await page.getByTestId("icon-ChevronLeft").first().click();
|
||||
|
||||
await page.waitForSelector('[data-testid="home-dropdown-menu"]', {
|
||||
timeout: 5000,
|
||||
});
|
||||
|
||||
await page.waitForSelector(`text=${randomName4}`, {
|
||||
timeout: 3000,
|
||||
state: "visible",
|
||||
});
|
||||
|
||||
expect(await page.getByText(randomName4).count()).toBe(1);
|
||||
},
|
||||
);
|
||||
|
|
@ -15,8 +15,8 @@ test(
|
|||
timeout: 3000,
|
||||
});
|
||||
|
||||
await page.getByTestId("flow_name").click();
|
||||
await page.getByText("Flow Settings").first().click();
|
||||
await page.getByTestId("flow_menu_trigger").click();
|
||||
await page.getByText("Edit Details").first().click();
|
||||
await page
|
||||
.getByPlaceholder("Flow name")
|
||||
.fill(
|
||||
|
|
@ -39,8 +39,8 @@ test(
|
|||
|
||||
await page.getByText("Changes saved successfully").isVisible();
|
||||
|
||||
await page.getByTestId("flow_name").click();
|
||||
await page.getByText("Flow Settings").first().click();
|
||||
await page.getByTestId("flow_menu_trigger").click();
|
||||
await page.getByText("Edit Details").first().click();
|
||||
|
||||
const flowName = await page.getByPlaceholder("Flow name").inputValue();
|
||||
const flowDescription = await page
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue