Merge dev into shortcuts_settings

This commit is contained in:
igorrCarvalho 2024-05-23 15:40:53 -03:00
commit ec5d5f2b38
275 changed files with 19166 additions and 14947 deletions

File diff suppressed because it is too large Load diff

View file

@ -4,7 +4,9 @@
"private": true,
"dependencies": {
"@headlessui/react": "^1.7.17",
"@hookform/resolvers": "^3.3.4",
"@million/lint": "^0.0.73",
"react-hotkeys-hook": "^4.5.0",
"@radix-ui/react-accordion": "^1.1.2",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-dialog": "^1.0.4",
@ -50,7 +52,7 @@
"react-cookie": "^4.1.1",
"react-dom": "^18.2.21",
"react-error-boundary": "^4.0.11",
"react-hotkeys-hook": "^4.5.0",
"react-hook-form": "^7.51.4",
"react-icons": "^5.0.1",
"react-laag": "^2.0.5",
"react-markdown": "^8.0.7",
@ -69,6 +71,7 @@
"uuid": "^9.0.0",
"vite-plugin-svgr": "^3.2.0",
"web-vitals": "^2.1.4",
"zod": "^3.23.7",
"zustand": "^4.4.7"
},
"scripts": {
@ -102,7 +105,7 @@
},
"proxy": "http://127.0.0.1:7860",
"devDependencies": {
"@playwright/test": "^1.43.1",
"@playwright/test": "^1.44.0",
"@swc/cli": "^0.1.62",
"@swc/core": "^1.3.80",
"@tailwindcss/typography": "^0.5.9",

View file

@ -11,6 +11,13 @@ body {
text-align: center;
}
.label {
user-select: none;
-webkit-user-select: none; /* Safari */
-moz-user-select: none; /* Firefox */
-ms-user-select: none;
}
.react-flow__node {
width: auto;
height: auto;
@ -76,6 +83,25 @@ body {
height: 8px !important;
border-radius: 10px;
}
*::-webkit-scrollbar {
width: 8px !important;
height: 8px !important;
border-radius: 10px;
}
::-webkit-scrollbar-track {
background-color: #f1f1f1 !important;
border-radius: 10px;
}
::-webkit-scrollbar-thumb {
background-color: #ccc !important;
border-radius: 999px !important;
}
::-webkit-scrollbar-thumb:hover {
background-color: #bbb !important;
}
.jv-indent::-webkit-scrollbar-track {
background-color: #f1f1f1 !important;
@ -111,3 +137,30 @@ body {
.json-view-flow .json-view {
background-color: #bbb !important;
}
.ag-body-horizontal-scroll-viewport,
.ag-body-vertical-scroll-viewport {
cursor: auto;
}
.ag-body-horizontal-scroll-viewport::-webkit-scrollbar,
.ag-body-vertical-scroll-viewport::-webkit-scrollbar {
width: 8px;
height: 8px;
}
.ag-body-horizontal-scroll-viewport::-webkit-scrollbar-track,
.ag-body-vertical-scroll-viewport::-webkit-scrollbar-track {
background-color: #f1f1f1;
}
.ag-body-horizontal-scroll-viewport::-webkit-scrollbar-thumb,
.ag-body-vertical-scroll-viewport::-webkit-scrollbar-thumb {
background-color: #ccc;
border-radius: 999px;
}
.ag-body-horizontal-scroll-viewport::-webkit-scrollbar-thumb:hover,
.ag-body-vertical-scroll-viewport::-webkit-scrollbar-thumb:hover {
background-color: #bbb;
}

View file

@ -19,15 +19,16 @@ import Router from "./routes";
import useAlertStore from "./stores/alertStore";
import { useDarkStore } from "./stores/darkStore";
import useFlowsManagerStore from "./stores/flowsManagerStore";
import { useFolderStore } from "./stores/foldersStore";
import { useGlobalVariablesStore } from "./stores/globalVariables";
import { useStoreStore } from "./stores/storeStore";
import { useTypesStore } from "./stores/typesStore";
export default function App() {
const removeFromTempNotificationList = useAlertStore(
(state) => state.removeFromTempNotificationList
(state) => state.removeFromTempNotificationList,
);
const tempNotificationList = useAlertStore(
(state) => state.tempNotificationList
(state) => state.tempNotificationList,
);
const [fetchError, setFetchError] = useState(false);
const isLoading = useFlowsManagerStore((state) => state.isLoading);
@ -45,15 +46,15 @@ export default function App() {
const refreshVersion = useDarkStore((state) => state.refreshVersion);
const refreshStars = useDarkStore((state) => state.refreshStars);
const setGlobalVariables = useGlobalVariablesStore(
(state) => state.setGlobalVariables
);
const setUnavailableFields = useGlobalVariablesStore(
(state) => state.setUnavaliableFields
(state) => state.setGlobalVariables,
);
const checkHasStore = useStoreStore((state) => state.checkHasStore);
const navigate = useNavigate();
const dark = useDarkStore((state) => state.dark);
const getFoldersApi = useFolderStore((state) => state.getFoldersApi);
const loadingFolders = useFolderStore((state) => state.loading);
const [isLoadingHealth, setIsLoadingHealth] = useState(false);
useEffect(() => {
@ -76,7 +77,7 @@ export default function App() {
setUserData(user);
setAutoLogin(true);
setLoading(false);
await Promise.all([refreshStars(), refreshVersion(), fetchData()]);
fetchAllData();
}
})
.catch(async (error) => {
@ -84,7 +85,7 @@ export default function App() {
setAutoLogin(false);
if (isAuthenticated && !isLoginPage) {
getUser();
await Promise.all([refreshStars(), refreshVersion(), fetchData()]);
fetchAllData();
} else {
setLoading(false);
useFlowsManagerStore.setState({ isLoading: false });
@ -92,14 +93,21 @@ export default function App() {
}
});
/*
Abort the request as it isn't needed anymore, the component being
/*
Abort the request as it isn't needed anymore, the component being
unmounted. It helps avoid, among other things, the well-known "can't
perform a React state update on an unmounted component" warning.
*/
return () => abortController.abort();
}, []);
const fetchAllData = async () => {
setTimeout(async () => {
await Promise.all([refreshStars(), refreshVersion(), fetchData()]);
getFoldersApi();
}, 1000);
};
const fetchData = async () => {
return new Promise<void>(async (resolve, reject) => {
if (isAuthenticated) {
@ -157,7 +165,7 @@ export default function App() {
setFetchError(false);
//This condition is necessary to avoid infinite loop on starter page when the application is not healthy
if (isLoading === true && window.location.pathname === "/") {
navigate("/flows");
navigate("/all");
window.location.reload();
}
};
@ -184,7 +192,7 @@ export default function App() {
></FetchErrorComponent>
}
{isLoading ? (
{isLoading || loadingFolders ? (
<div className="loading-page-panel">
<LoadingComponent remSize={50} />
</div>

View file

@ -0,0 +1,9 @@
type RawComponentProps = {};
const RawComponent = ({}: RawComponentProps) => {
return (
<>
<div>RawComponent</div>
</>
);
};
export default RawComponent;

View file

@ -0,0 +1,72 @@
import { useState } from "react";
import IconComponent from "../../../../components/genericIconComponent";
import { AccordionComponentType } from "../../../../types/components";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "../../../ui/custom-accordion";
export default function FolderAccordionComponent({
trigger,
open = [],
keyValue,
options,
}: AccordionComponentType): JSX.Element {
const [value, setValue] = useState(
open.length === 0 ? "" : getOpenAccordion(),
);
function getOpenAccordion(): string {
let value = "";
open.forEach((el) => {
if (el == trigger) {
value = trigger;
}
});
return value;
}
function handleClick(): void {
value === "" ? setValue(keyValue!) : setValue("");
}
return (
<>
<Accordion
type="single"
className="w-full"
value={value}
onValueChange={setValue}
>
<AccordionItem value={keyValue!} className="">
<AccordionTrigger
onClick={() => {
handleClick();
}}
className="px-2"
>
{trigger}
</AccordionTrigger>
<AccordionContent>
{options!.map((option, index) => (
<div
key={index}
className="flex cursor-pointer px-2 py-1 hover:bg-muted-foreground/10"
>
<IconComponent
name={option.icon}
className="relative top-[1.5px] mr-2 h-4 w-4"
aria-hidden="true"
/>
<span className="truncate">{option.title}</span>
</div>
))}
</AccordionContent>
</AccordionItem>
</Accordion>
</>
);
}

View file

@ -15,17 +15,16 @@ export default function AccordionComponent({
sideBar,
}: AccordionComponentType): JSX.Element {
const [value, setValue] = useState(
open.length === 0 ? "" : getOpenAccordion()
open.length === 0 ? "" : getOpenAccordion(),
);
function getOpenAccordion(): string {
let value = "";
open.forEach((el) => {
if (el == trigger) {
value = trigger;
if (el == keyValue) {
value = keyValue;
}
});
return value;
}

View file

@ -130,8 +130,8 @@ export default function AddNewVariableButton({ children }): JSX.Element {
<InputComponent
setSelectedOptions={(value) => setFields(value)}
selectedOptions={fields}
password={false}
options={availableFields()}
password={false}
placeholder="Choose a field for the variable..."
id={"apply-to-fields"}
></InputComponent>

View file

@ -0,0 +1,12 @@
export default function ArrayReader({ array }: { array: any[] }): JSX.Element {
//TODO check array type
return (
<div>
<ul>
{array.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
</div>
);
}

View file

@ -0,0 +1,39 @@
import { storeComponent } from "../../../../types/store";
import { cn } from "../../../../utils/utils";
import ForwardedIconComponent from "../../../genericIconComponent";
import ShadTooltip from "../../../shadTooltipComponent";
import { Card, CardHeader, CardTitle } from "../../../ui/card";
export default function DragCardComponent({ data }: { data: storeComponent }) {
return (
<>
<Card
draggable
//TODO check color schema
className={cn(
"group relative flex flex-col justify-between overflow-hidden transition-all hover:bg-muted/50 hover:shadow-md hover:dark:bg-[#ffffff10]",
)}
>
<div>
<CardHeader>
<div>
<CardTitle className="flex w-full items-start justify-between gap-3 text-xl">
<ForwardedIconComponent
className={cn(
"visible flex-shrink-0",
data.is_component
? "mx-0.5 h-6 w-6 text-component-icon"
: "h-7 w-7 flex-shrink-0 text-flow-icon",
)}
name={data.is_component ? "ToyBrick" : "Group"}
/>
<div className="w-full truncate pr-3">{data.name}</div>
</CardTitle>
</div>
</CardHeader>
</div>
</Card>
</>
);
}

View file

@ -1,4 +1,6 @@
import { useEffect, useState } from "react";
import { createRoot } from "react-dom/client";
import { Control } from "react-hook-form";
import { getComponent, postLikeComponent } from "../../controllers/API";
import IOModal from "../../modals/IOModal";
import DeleteConfirmationModal from "../../modals/deleteConfirmationModal";
@ -6,6 +8,7 @@ import useAlertStore from "../../stores/alertStore";
import useFlowStore from "../../stores/flowStore";
import useFlowsManagerStore from "../../stores/flowsManagerStore";
import { useStoreStore } from "../../stores/storeStore";
import { FlowType } from "../../types/flow";
import { storeComponent } from "../../types/store";
import cloneFLowWithParent, {
getInputsAndOutputs,
@ -21,8 +24,10 @@ import {
CardHeader,
CardTitle,
} from "../ui/card";
import { Checkbox } from "../ui/checkbox";
import { FormControl, FormField } from "../ui/form";
import Loading from "../ui/loading";
import { FlowType } from "../../types/flow";
import DragCardComponent from "./components/dragCardComponent";
export default function CollectionCardComponent({
data,
@ -32,6 +37,8 @@ export default function CollectionCardComponent({
onClick,
onDelete,
playground,
control,
is_component,
}: {
data: storeComponent;
authorized?: boolean;
@ -40,6 +47,8 @@ export default function CollectionCardComponent({
button?: JSX.Element;
playground?: boolean;
onDelete?: () => void;
control?: Control<any, any>;
is_component?: boolean;
}) {
const addFlow = useFlowsManagerStore((state) => state.addFlow);
const setSuccessData = useAlertStore((state) => state.setSuccessData);
@ -69,6 +78,10 @@ export default function CollectionCardComponent({
);
const [loadingPlayground, setLoadingPlayground] = useState(false);
const selectedFlowsComponentsCards = useFlowsManagerStore(
(state) => state.selectedFlowsComponentsCards,
);
const name = data.is_component ? "Component" : "Flow";
async function getFlowData() {
@ -82,7 +95,6 @@ export default function CollectionCardComponent({
return false;
}
const { inputs, outputs } = getInputsAndOutputs(flow?.data?.nodes ?? []);
console.log(inputs, outputs);
return inputs.length > 0 || outputs.length > 0;
}
@ -177,36 +189,59 @@ export default function CollectionCardComponent({
}
}
const isSelectedCard =
selectedFlowsComponentsCards?.includes(data?.id) ?? false;
function onDragStart(event: React.DragEvent<any>) {
let image: JSX.Element = <DragCardComponent data={data} />; // <== whatever you want here
var ghost = document.createElement("div");
ghost.style.transform = "translate(-10000px, -10000px)";
ghost.style.position = "absolute";
document.body.appendChild(ghost);
event.dataTransfer.setDragImage(ghost, 0, 0);
const root = createRoot(ghost);
root.render(image);
const flow = getFlowById(data.id);
if (flow) {
event.dataTransfer.setData("flow", JSON.stringify(data));
}
}
return (
<>
<Card
onDragStart={onDragStart}
draggable
data-testid={`card-${convertTestName(data.name)}`}
//TODO check color schema
className={cn(
"group relative flex min-h-[11rem] flex-col justify-between overflow-hidden transition-all hover:bg-muted/50 hover:shadow-md hover:dark:bg-[#ffffff10]",
disabled ? "pointer-events-none opacity-50" : "",
onClick ? "cursor-pointer" : "",
isSelectedCard ? "border border-selected" : "",
)}
onClick={onClick}
>
<div>
<CardHeader>
<div>
<CardTitle className="flex w-full items-center justify-between gap-3 text-xl">
<CardTitle className="flex w-full items-start justify-between gap-3 text-xl">
<IconComponent
className={cn(
"flex-shrink-0",
"visible flex-shrink-0",
data.is_component
? "mx-0.5 h-6 w-6 text-component-icon"
: "h-7 w-7 flex-shrink-0 text-flow-icon",
)}
name={data.is_component ? "ToyBrick" : "Group"}
/>
<ShadTooltip content={data.name}>
<div className="w-full truncate">{data.name}</div>
<div className="w-full truncate pr-3">{data.name}</div>
</ShadTooltip>
{data?.metadata !== undefined && (
<div className="flex gap-3">
<div className="flex items-center gap-3">
{data.private && (
<ShadTooltip content="Private">
<span className="flex items-center gap-1.5 text-xs text-muted-foreground">
@ -249,19 +284,29 @@ export default function CollectionCardComponent({
</div>
)}
{onDelete && data?.metadata === undefined && (
<button
className="z-50"
{control && (
<div
className="flex"
onClick={(e) => {
e.stopPropagation();
setOpenDelete(true);
}}
>
<IconComponent
name="Trash2"
className="h-5 w-5 text-primary opacity-0 transition-all hover:text-destructive group-hover:opacity-100"
<FormField
control={control}
name={`${data.id}`}
defaultValue={false}
render={({ field }) => (
<FormControl>
<Checkbox
aria-label="checkbox-component"
checked={field.value}
onCheckedChange={field.onChange}
className="h-5 w-5 border border-ring data-[state=checked]:border-selected data-[state=checked]:bg-selected"
/>
</FormControl>
)}
/>
</button>
</div>
)}
</CardTitle>
</div>
@ -368,11 +413,7 @@ export default function CollectionCardComponent({
authorized ? "Delete" : "Please review your API key."
}
>
<DeleteConfirmationModal
onConfirm={() => {
onDelete();
}}
>
<DeleteConfirmationModal onConfirm={onDelete}>
<Button
variant="ghost"
size="icon"
@ -540,6 +581,7 @@ export default function CollectionCardComponent({
onConfirm={() => {
if (onDelete) onDelete();
}}
description={` ${is_component ? "component" : "flow"}`}
>
<></>
</DeleteConfirmationModal>

View file

@ -1,5 +1,6 @@
import { useState } from "react";
import { useEffect, useState } from "react";
import IconComponent from "../../components/genericIconComponent";
import { cn } from "../../utils/utils";
export default function CardsWrapComponent({
onFileDrop,
@ -11,6 +12,23 @@ export default function CardsWrapComponent({
dragMessage?: string;
}) {
const [isDragging, setIsDragging] = useState(false);
useEffect(() => {
// Function to handle visibility change
const handleVisibilityChange = () => {
if (document.visibilityState === "visible") {
// Reset hover state or perform any necessary actions when the tab becomes visible again
setIsDragging(false);
}
};
// Add event listener for visibility change
document.addEventListener("visibilitychange", handleVisibilityChange);
// Cleanup event listener on component unmount
return () => {
document.removeEventListener("visibilitychange", handleVisibilityChange);
};
}, []);
const dragOver = (e) => {
e.preventDefault();
@ -43,12 +61,12 @@ export default function CardsWrapComponent({
onDragEnter={dragEnter}
onDragLeave={dragLeave}
onDrop={onDrop}
className={
"h-full w-full " +
(isDragging
? "mb-24 flex flex-col items-center justify-center gap-4 text-2xl font-light"
: "")
}
className={cn(
"h-full w-full",
isDragging
? "mb-36 flex flex-col items-center justify-center gap-4 text-2xl font-light"
: "",
)}
>
{isDragging ? (
<>

View file

@ -2,7 +2,7 @@ import { Transition } from "@headlessui/react";
import { useHotkeys } from "react-hotkeys-hook";
import { useEffect, useMemo, useRef, useState } from "react";
import IOModal from "../../modals/IOModal";
import ApiModal from "../../modals/apiModal";
import ApiModal from "../../modals/apiModal/views";
import ShareModal from "../../modals/shareModal";
import useFlowStore from "../../stores/flowStore";
import useFlowsManagerStore from "../../stores/flowsManagerStore";
@ -36,8 +36,8 @@ export default function FlowToolbar(): JSX.Element {
const hasIO = useFlowStore((state) => state.hasIO);
const hasStore = useStoreStore((state) => state.hasStore);
const validApiKey = useStoreStore((state) => state.validApiKey);
const currentFlow = useFlowsManagerStore((state) => state.currentFlow);
const hasApiKey = useStoreStore((state) => state.hasApiKey);
const currentFlow = useFlowsManagerStore((state) => state.currentFlow);
const prevNodesRef = useRef<any[] | undefined>();

View file

@ -18,12 +18,12 @@ export default function CodeAreaComponent({
setOpen,
}: CodeAreaComponentType) {
const [myValue, setMyValue] = useState(
typeof value == "string" ? value : JSON.stringify(value)
typeof value == "string" ? value : JSON.stringify(value),
);
useEffect(() => {
if (disabled && myValue !== "") {
setMyValue("");
onChange("");
onChange("", true);
}
}, [disabled]);

View file

@ -26,6 +26,8 @@ import {
TabsTrigger,
} from "../../components/ui/tabs";
import { LANGFLOW_SUPPORTED_TYPES } from "../../constants/constants";
import ExportModal from "../../modals/exportModal";
import { Case } from "../../shared/components/caseComponent";
import { useDarkStore } from "../../stores/darkStore";
import useFlowStore from "../../stores/flowStore";
import { codeTabsPropsType } from "../../types/components";
@ -41,6 +43,8 @@ import IconComponent from "../genericIconComponent";
import InputComponent from "../inputComponent";
import KeypairListComponent from "../keypairListComponent";
import ShadTooltip from "../shadTooltipComponent";
import { Label } from "../ui/label";
import { Switch } from "../ui/switch";
export default function CodeTabsComponent({
flow,
@ -49,14 +53,14 @@ export default function CodeTabsComponent({
setActiveTab,
isMessage,
tweaks,
setActiveTweaks,
activeTweaks,
allowExport = false,
}: codeTabsPropsType) {
const [isCopied, setIsCopied] = useState<Boolean>(false);
const [data, setData] = useState(flow ? flow["data"]!["nodes"] : null);
const [openAccordion, setOpenAccordion] = useState<string[]>([]);
const dark = useDarkStore((state) => state.dark);
const unselectAll = useFlowStore((state) => state.unselectAll);
const setNode = useFlowStore((state) => state.setNode);
const [errorDuplicateKey, setErrorDuplicateKey] = useState(false);
useEffect(() => {
@ -85,6 +89,10 @@ export default function CodeTabsComponent({
});
};
const type = (node, templateParam) => {
return node.data.node.template[templateParam].type;
};
const downloadAsFile = () => {
const fileExtension = tabs[activeTab].language || ".txt";
const suggestedFileName = `${"generated-code."}${fileExtension}`;
@ -107,21 +115,6 @@ export default function CodeTabsComponent({
URL.revokeObjectURL(url);
};
function openAccordions() {
let accordionsToOpen: string[] = [];
tweaks?.tweak!.current.forEach((el) => {
Object.keys(el).forEach((key) => {
if (Object.keys(el[key]).length > 0) {
accordionsToOpen.push(key);
setOpenAccordion(accordionsToOpen);
}
});
});
if (accordionsToOpen.length == 0) {
setOpenAccordion([]);
}
}
return (
<Tabs
value={activeTab}
@ -132,9 +125,6 @@ export default function CodeTabsComponent({
}
onValueChange={(value) => {
setActiveTab(value);
if (value === "3") {
openAccordions();
}
}}
>
<div className="api-modal-tablist-div">
@ -155,27 +145,68 @@ export default function CodeTabsComponent({
) : (
<div></div>
)}
{Number(activeTab) < 4 && (
<div className="float-right mx-1 mb-1 mt-2 flex gap-2">
<button
className="flex items-center gap-1.5 rounded bg-none p-1 text-xs text-gray-500 dark:text-gray-300"
onClick={copyToClipboard}
<div className="float-right mx-1 mb-1 mt-2 flex gap-2">
{tweaks && (
<div
className={
Number(activeTab) > 2
? "hidden"
: "relative top-[2.5px] flex gap-2"
}
>
{isCopied ? (
<IconComponent name="Check" className="h-4 w-4" />
) : (
<IconComponent name="Clipboard" className="h-4 w-4" />
)}
{isCopied ? "Copied!" : "Copy Code"}
</button>
<button
className="flex items-center gap-1.5 rounded bg-none p-1 text-xs text-gray-500 dark:text-gray-300"
onClick={downloadAsFile}
>
<IconComponent name="Download" className="h-5 w-5" />
</button>
</div>
)}
<Switch
style={{
transform: `scaleX(${0.7}) scaleY(${0.7})`,
}}
id="tweaks-switch"
onCheckedChange={setActiveTweaks}
autoFocus={false}
/>
<Label
className={
"relative right-1 top-[4px] text-xs font-medium text-gray-500 dark:text-gray-300 " +
(activeTweaks
? "font-bold text-black dark:text-white"
: "font-medium")
}
htmlFor="tweaks-switch"
>
Tweaks
</Label>
</div>
)}
{allowExport && (
<ExportModal>
<div className="flex cursor-pointer items-center gap-1.5 rounded bg-none p-1 text-xs text-gray-500 dark:text-gray-300">
<IconComponent name="FileDown" className="h-4 w-4" />
Export Flow
</div>
</ExportModal>
)}
{Number(activeTab) < 4 && (
<>
<button
className="flex items-center gap-1.5 rounded bg-none p-1 text-xs text-gray-500 dark:text-gray-300"
onClick={copyToClipboard}
>
{isCopied ? (
<IconComponent name="Check" className="h-4 w-4" />
) : (
<IconComponent name="Clipboard" className="h-4 w-4" />
)}
{isCopied ? "Copied!" : "Copy Code"}
</button>
<button
className="flex items-center gap-1.5 rounded bg-none p-1 text-xs text-gray-500 dark:text-gray-300"
onClick={downloadAsFile}
>
<IconComponent name="Download" className="h-5 w-5" />
</button>
</>
)}
</div>
</div>
{tabs.map((tab, idx) => (
@ -210,9 +241,7 @@ export default function CodeTabsComponent({
>
{data?.map((node: any, i) => (
<div className="px-3" key={i}>
{tweaks?.tweaksList!.current.includes(
node["data"]["id"],
) && (
{tweaks?.tweaksList!.includes(node["data"]["id"]) && (
<AccordionComponent
trigger={
<ShadTooltip
@ -223,7 +252,6 @@ export default function CodeTabsComponent({
<div>{node["data"]["node"]["display_name"]}</div>
</ShadTooltip>
}
open={openAccordion}
keyValue={node["data"]["id"]}
>
<div className="api-modal-table-arrangement">
@ -258,107 +286,71 @@ export default function CodeTabsComponent({
</TableCell>
<TableCell className="p-0 text-xs text-foreground">
<div className="m-auto w-[250px]">
{node.data.node.template[
templateField
].type === "str" &&
!node.data.node.template[
templateField
].options ? (
<div className="mx-auto">
{node.data.node.template[
<Case
condition={
type(node, templateField) ===
"str" &&
!node.data.node.template[
templateField
]?.list ? (
<InputListComponent
componentName={
templateField
}
editNode={true}
disabled={false}
value={
!node.data.node.template[
templateField
].value ||
node.data.node.template[
templateField
].value === ""
? [""]
: node.data.node
.template[
templateField
].value
}
onChange={(target) => {
setData((old) => {
let newInputList =
cloneDeep(old);
newInputList![
i
].data.node.template[
templateField
].value = target;
return newInputList;
});
tweaks.buildTweakObject!(
node["data"]["id"],
target,
node.data.node.template[
templateField
],
);
}}
/>
) : node.data.node.template[
].options
}
>
<Case
condition={
node.data.node.template[
templateField
].multiline ? (
<div>
<TextAreaComponent
disabled={false}
editNode={true}
value={
!node.data.node
.template[
]?.list
}
>
<InputListComponent
componentName={templateField}
editNode={true}
disabled={false}
value={
!node.data.node.template[
templateField
].value ||
node.data.node.template[
templateField
].value === ""
? [""]
: node.data.node.template[
templateField
].value ||
node.data.node.template[
templateField
].value === ""
? ""
: node.data.node
.template[
templateField
].value
}
onChange={(target) => {
setData((old) => {
let newInputList =
cloneDeep(old);
newInputList![
i
].data.node.template[
templateField
].value = target;
return newInputList;
});
tweaks.buildTweakObject!(
node["data"]["id"],
target,
node.data.node
.template[
templateField
],
);
}}
/>
</div>
) : (
<InputComponent
editNode={true}
disabled={false}
password={
].value
}
onChange={(target) => {
setData((old) => {
let newInputList =
cloneDeep(old);
newInputList![
i
].data.node.template[
templateField
].value = target;
return newInputList;
});
tweaks?.buildTweakObject!(
node["data"]["id"],
target,
node.data.node.template[
templateField
].password ?? false
}
],
);
}}
/>
</Case>
<Case
condition={
node.data.node.template[
templateField
].multiline
}
>
<div>
<TextAreaComponent
disabled={false}
editNode={true}
value={
!node.data.node.template[
templateField
@ -383,7 +375,7 @@ export default function CodeTabsComponent({
].value = target;
return newInputList;
});
tweaks.buildTweakObject!(
tweaks?.buildTweakObject!(
node["data"]["id"],
target,
node.data.node.template[
@ -392,11 +384,68 @@ export default function CodeTabsComponent({
);
}}
/>
)}
</div>
) : node.data.node.template[
templateField
].type === "bool" ? (
</div>
</Case>
<Case
condition={
!node.data.node.template[
templateField
].multiline &&
!node.data.node.template[
templateField
].list
}
>
<InputComponent
editNode={true}
disabled={false}
password={
node.data.node.template[
templateField
].password ?? false
}
value={
!node.data.node.template[
templateField
].value ||
node.data.node.template[
templateField
].value === ""
? ""
: node.data.node.template[
templateField
].value
}
onChange={(target) => {
setData((old) => {
let newInputList =
cloneDeep(old);
newInputList![
i
].data.node.template[
templateField
].value = target;
return newInputList;
});
tweaks?.buildTweakObject!(
node["data"]["id"],
target,
node.data.node.template[
templateField
],
);
}}
/>
</Case>
</Case>
<Case
condition={
type(node, templateField) ===
"bool"
}
>
<div className="ml-auto">
{" "}
<ToggleShadComponent
@ -416,7 +465,7 @@ export default function CodeTabsComponent({
].value = e;
return newInputList;
});
tweaks.buildTweakObject!(
tweaks?.buildTweakObject!(
node["data"]["id"],
e,
node.data.node.template[
@ -428,9 +477,14 @@ export default function CodeTabsComponent({
disabled={false}
/>
</div>
) : node.data.node.template[
templateField
].type === "file" ? (
</Case>
<Case
condition={
type(node, templateField) ===
"file"
}
>
<div className="mx-auto">
<InputFileComponent
editNode={true}
@ -455,9 +509,14 @@ export default function CodeTabsComponent({
}}
></InputFileComponent>
</div>
) : node.data.node.template[
templateField
].type === "float" ? (
</Case>
<Case
condition={
type(node, templateField) ===
"float"
}
>
<div className="mx-auto">
<FloatComponent
disabled={false}
@ -490,7 +549,7 @@ export default function CodeTabsComponent({
].value = target;
return newInputList;
});
tweaks.buildTweakObject!(
tweaks?.buildTweakObject!(
node["data"]["id"],
target,
node.data.node.template[
@ -500,12 +559,17 @@ export default function CodeTabsComponent({
}}
/>
</div>
) : node.data.node.template[
templateField
].type === "str" &&
node.data.node.template[
templateField
].options ? (
</Case>
<Case
condition={
type(node, templateField) ===
"str" &&
node.data.node.template[
templateField
].options
}
>
<div className="mx-auto">
<Dropdown
editNode={true}
@ -525,7 +589,7 @@ export default function CodeTabsComponent({
].value = target;
return newInputList;
});
tweaks.buildTweakObject!(
tweaks?.buildTweakObject!(
node["data"]["id"],
target,
node.data.node.template[
@ -547,9 +611,14 @@ export default function CodeTabsComponent({
}
></Dropdown>
</div>
) : node.data.node.template[
templateField
].type === "int" ? (
</Case>
<Case
condition={
type(node, templateField) ===
"int"
}
>
<div className="mx-auto">
<IntComponent
disabled={false}
@ -582,7 +651,7 @@ export default function CodeTabsComponent({
].value = target;
return newInputList;
});
tweaks.buildTweakObject!(
tweaks?.buildTweakObject!(
node["data"]["id"],
target,
node.data.node.template[
@ -592,9 +661,14 @@ export default function CodeTabsComponent({
}}
/>
</div>
) : node.data.node.template[
templateField
].type === "prompt" ? (
</Case>
<Case
condition={
type(node, templateField) ===
"prompt"
}
>
<div className="mx-auto">
<PromptAreaComponent
readonly={true}
@ -623,7 +697,7 @@ export default function CodeTabsComponent({
].value = target;
return newInputList;
});
tweaks.buildTweakObject!(
tweaks?.buildTweakObject!(
node["data"]["id"],
target,
node.data.node.template[
@ -633,9 +707,14 @@ export default function CodeTabsComponent({
}}
/>
</div>
) : node.data.node.template[
templateField
].type === "code" ? (
</Case>
<Case
condition={
type(node, templateField) ===
"code"
}
>
<div className="mx-auto">
<CodeAreaComponent
disabled={false}
@ -664,7 +743,7 @@ export default function CodeTabsComponent({
].value = target;
return newInputList;
});
tweaks.buildTweakObject!(
tweaks?.buildTweakObject!(
node["data"]["id"],
target,
node.data.node.template[
@ -674,9 +753,14 @@ export default function CodeTabsComponent({
}}
/>
</div>
) : node.data.node.template[
templateField
].type === "dict" ? (
</Case>
<Case
condition={
type(node, templateField) ===
"dict"
}
>
<div className="mx-auto overflow-auto custom-scroll">
<KeypairListComponent
disabled={false}
@ -694,6 +778,10 @@ export default function CodeTabsComponent({
.template[
templateField
].value,
type(
node,
templateField,
),
)
}
duplicateKey={
@ -722,7 +810,7 @@ export default function CodeTabsComponent({
].value = target;
return newInputList;
});
tweaks.buildTweakObject!(
tweaks?.buildTweakObject!(
node["data"]["id"],
target,
node.data.node.template[
@ -737,9 +825,14 @@ export default function CodeTabsComponent({
}
/>
</div>
) : node.data.node.template[
templateField
].type === "NestedDict" ? (
</Case>
<Case
condition={
type(node, templateField) ===
"NestedDict"
}
>
<div className="mx-auto">
<DictComponent
disabled={false}
@ -747,7 +840,7 @@ export default function CodeTabsComponent({
value={
node.data.node!.template[
templateField
].value.toString() === "{}"
].value?.toString() === "{}"
? {
// yourkey: "value",
}
@ -767,7 +860,7 @@ export default function CodeTabsComponent({
].value = target;
return newInputList;
});
tweaks.buildTweakObject!(
tweaks?.buildTweakObject!(
node["data"]["id"],
target,
node.data.node.template[
@ -777,13 +870,16 @@ export default function CodeTabsComponent({
}}
/>
</div>
) : node.data.node.template[
templateField
].type === "Any" ? (
"-"
) : (
<div className="hidden"></div>
)}
</Case>
<Case
condition={
type(node, templateField) ===
"Any"
}
>
<>-</>
</Case>
</div>
</TableCell>
</TableRow>
@ -795,7 +891,7 @@ export default function CodeTabsComponent({
</AccordionComponent>
)}
{tweaks?.tweaksList!.current.length === 0 && (
{tweaks?.tweaksList!.length === 0 && (
<>
<div className="pt-3">
No tweaks are available for this flow.

View file

@ -1,7 +1,6 @@
import "ag-grid-community/styles/ag-grid.css"; // Mandatory CSS required by the grid
import "ag-grid-community/styles/ag-theme-balham.css"; // Optional Theme applied to the grid
import { AgGridReact } from "ag-grid-react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useEffect, useMemo, useState } from "react";
import {
CSVError,
CSVNoDataError,
@ -11,6 +10,7 @@ import { useDarkStore } from "../../stores/darkStore";
import { FlowPoolObjectType } from "../../types/chat";
import { NodeType } from "../../types/flow";
import ForwardedIconComponent from "../genericIconComponent";
import TableComponent from "../tableComponent";
import Loading from "../ui/loading";
import { convertCSVToData } from "./helpers/convert-data-function";
@ -54,8 +54,6 @@ function CsvOutputComponent({
const [colDefs, setColDefs] = useState([]);
const [status, setStatus] = useState("loading");
var currentRowHeight: number;
var minRowHeight = 25;
const defaultColDef = useMemo(() => {
return {
width: 200,
@ -82,48 +80,6 @@ function CsvOutputComponent({
}
}, [separator]);
const getRowHeight = useCallback(() => {
return currentRowHeight;
}, []);
const onGridReady = useCallback((params: any) => {
minRowHeight = params.api.getSizesForCurrentTheme().rowHeight;
currentRowHeight = minRowHeight;
}, []);
const updateRowHeight = (params: { api: any }) => {
const bodyViewport = document.querySelector(".ag-body-viewport");
if (!bodyViewport) {
return;
}
var gridHeight = bodyViewport.clientHeight;
var renderedRowCount = params.api.getDisplayedRowCount();
if (renderedRowCount * minRowHeight >= gridHeight) {
if (currentRowHeight !== minRowHeight) {
currentRowHeight = minRowHeight;
params.api.resetRowHeights();
}
} else {
currentRowHeight = Math.floor(gridHeight / renderedRowCount);
params.api.resetRowHeights();
}
};
const onFirstDataRendered = useCallback(
(params: any) => {
updateRowHeight(params);
},
[updateRowHeight],
);
const onGridSizeChanged = useCallback(
(params: any) => {
updateRowHeight(params);
},
[updateRowHeight],
);
return (
<div className=" h-full rounded-md border bg-muted">
{status === "nodata" && (
@ -158,14 +114,10 @@ function CsvOutputComponent({
className={`${dark ? "ag-theme-balham-dark" : "ag-theme-balham"}`}
style={{ height: "100%", width: "100%" }}
>
<AgGridReact
<TableComponent
rowData={rowData}
columnDefs={colDefs}
defaultColDef={defaultColDef}
getRowHeight={getRowHeight}
onGridReady={onGridReady}
onFirstDataRendered={onFirstDataRendered}
onGridSizeChanged={onGridSizeChanged}
scrollbarWidth={8}
overlayNoRowsTemplate="No data available"
/>

View file

@ -0,0 +1,21 @@
export default function DateReader({
date: dateString,
}: {
date: string;
}): JSX.Element {
const date = new Date(dateString);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0"); // Months are 0-indexed in JavaScript
const day = String(date.getDate()).padStart(2, "0");
const hours = date.getHours();
const minutes = String(date.getMinutes()).padStart(2, "0");
const ampm = hours >= 12 ? "PM" : "AM";
const hours12 = hours > 12 ? hours - 12 : hours === 0 ? 12 : hours; // Convert to 12-hour format
const formattedDate = `${year}-${month}-${day} ${hours12}:${minutes} ${ampm}`;
return <span>{formattedDate}</span>;
}

View file

@ -59,7 +59,7 @@ export default function Dropdown({
? "dropdown-component-outline"
: "dropdown-component-false-outline",
"w-full justify-between font-normal",
editNode ? "input-edit-node" : "py-2"
editNode ? "input-edit-node" : "py-2",
)}
>
<span data-testid={`value-dropdown-` + id}>
@ -78,6 +78,8 @@ export default function Dropdown({
</PopoverTrigger>
)}
<PopoverContentDropdown
side="bottom"
avoidCollisions={!!children}
className="nocopy nowheel nopan nodelete nodrag noundo p-0"
style={
children
@ -105,7 +107,7 @@ export default function Dropdown({
name="Check"
className={cn(
"ml-auto h-4 w-4 text-primary",
value === option ? "opacity-100" : "opacity-0"
value === option ? "opacity-100" : "opacity-0",
)}
/>
</CommandItem>

View file

@ -16,7 +16,7 @@ export default function FloatComponent({
// Clear component state
useEffect(() => {
if (disabled && value !== "") {
onChange("");
onChange("", true);
}
}, [disabled]);

View file

@ -7,7 +7,7 @@ import Loading from "../ui/loading";
import { useEffect, useState } from "react";
const ForwardedIconComponent = memo(
export const ForwardedIconComponent = memo(
forwardRef(
(
{
@ -18,7 +18,7 @@ const ForwardedIconComponent = memo(
strokeWidth,
id = "",
}: IconComponentProps,
ref
ref,
) => {
const [showFallback, setShowFallback] = useState(false);
@ -65,8 +65,8 @@ const ForwardedIconComponent = memo(
/>
</Suspense>
);
}
)
},
),
);
export default ForwardedIconComponent;

View file

@ -12,6 +12,7 @@ import { Node } from "reactflow";
import { UPLOAD_ERROR_ALERT } from "../../../../constants/alerts_constants";
import { SAVED_HOVER } from "../../../../constants/constants";
import ExportModal from "../../../../modals/exportModal";
import FlowLogsModal from "../../../../modals/flowLogsModal";
import FlowSettingsModal from "../../../../modals/flowSettingsModal";
import useAlertStore from "../../../../stores/alertStore";
import useFlowStore from "../../../../stores/flowStore";
@ -34,6 +35,7 @@ export const MenuBar = ({
const redo = useFlowsManagerStore((state) => state.redo);
const saveLoading = useFlowsManagerStore((state) => state.saveLoading);
const [openSettings, setOpenSettings] = useState(false);
const [openLogs, setOpenLogs] = useState(false);
const nodes = useFlowStore((state) => state.nodes);
const uploadFlow = useFlowsManagerStore((state) => state.uploadFlow);
const navigate = useNavigate();
@ -123,6 +125,18 @@ export const MenuBar = ({
/>
Settings
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
setOpenLogs(true);
}}
className="cursor-pointer"
>
<IconComponent
name="ScrollText"
className="header-menu-options "
/>
Logs
</DropdownMenuItem>
<DropdownMenuItem
className="cursor-pointer"
onClick={() => {
@ -132,7 +146,7 @@ export const MenuBar = ({
title: UPLOAD_ERROR_ALERT,
list: [error],
});
}
},
);
}}
>
@ -194,6 +208,7 @@ export const MenuBar = ({
open={openSettings}
setOpen={setOpenSettings}
></FlowSettingsModal>
<FlowLogsModal open={openLogs} setOpen={setOpenLogs}></FlowLogsModal>
</div>
{(currentFlow.updated_at || saveLoading) && (
<ShadTooltip
@ -213,7 +228,7 @@ export const MenuBar = ({
name={isBuilding || saveLoading ? "Loader2" : "CheckCircle2"}
className={cn(
"h-4 w-4",
isBuilding || saveLoading ? "animate-spin" : "animate-wiggle"
isBuilding || saveLoading ? "animate-spin" : "animate-wiggle",
)}
/>
{printByBuildStatus()}

View file

@ -59,7 +59,7 @@ export default function Header(): JSX.Element {
<Button
className="gap-2"
variant={
location.pathname === "/flows" ||
location.pathname === "/all" ||
location.pathname === "/components"
? "primary"
: "secondary"
@ -73,18 +73,7 @@ export default function Header(): JSX.Element {
<div className="hidden flex-1 md:block">{USER_PROJECTS_HEADER}</div>
</Button>
</Link>
{/* <Link to="/community">
<Button
className="gap-2"
variant={
location.pathname === "/community" ? "primary" : "secondary"
}
size="sm"
>
<IconComponent name="Users2" className="h-4 w-4" />
<div className="flex-1">Community Examples</div>
</Button>
</Link> */}
{hasStore && (
<Link to="/store">
<Button

View file

@ -0,0 +1,61 @@
import { useEffect, useRef, useState } from "react";
export default function HorizontalScrollFadeComponent({
children,
isFolder = true,
}) {
const scrollContainerRef = useRef<HTMLDivElement>(null);
const fadeContainerRef = useRef<HTMLDivElement>(null);
const [divWidth, setDivWidth] = useState<number>(0);
useEffect(() => {
const handleResize = () => {
if (scrollContainerRef.current) {
setDivWidth(scrollContainerRef.current.clientWidth);
}
};
window.addEventListener("resize", handleResize);
handleResize(); // call the function at start to get the initial width
return () => window.removeEventListener("resize", handleResize);
}, []);
useEffect(() => {
const handleScroll = () => {
if (!scrollContainerRef.current || !fadeContainerRef.current) return;
const { scrollLeft, scrollWidth, clientWidth } =
scrollContainerRef.current;
const atStart = scrollLeft === 0;
const atEnd = scrollLeft === scrollWidth - clientWidth;
const isScrollable = scrollWidth > clientWidth;
fadeContainerRef.current.classList.toggle(
"fade-left",
isScrollable && !atStart,
);
fadeContainerRef.current.classList.toggle(
"fade-right",
isScrollable && !atEnd,
);
};
const scrollContainer = scrollContainerRef.current;
if (scrollContainer) {
scrollContainer.addEventListener("scroll", handleScroll);
// Delay the initial scroll event dispatch to ensure correct calculation
scrollContainer.dispatchEvent(new Event("scroll"));
return () => scrollContainer.removeEventListener("scroll", handleScroll);
}
}, [divWidth, children]); // Depend on divWidth
return isFolder ? (
<div className="hidden w-full flex-col gap-2 lg:flex">{children}</div>
) : (
<div ref={fadeContainerRef} className="fade-container flex">
<div ref={scrollContainerRef} className="scroll-container flex gap-2">
{children}
</div>
</div>
);
}

View file

@ -0,0 +1,192 @@
import { PopoverAnchor } from "@radix-ui/react-popover";
import useAlertStore from "../../../../stores/alertStore";
import { classNames, cn } from "../../../../utils/utils";
import ForwardedIconComponent from "../../../genericIconComponent";
import {
Command,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "../../../ui/command";
import { Input } from "../../../ui/input";
import { Popover, PopoverContentWithoutPortal } from "../../../ui/popover";
const CustomInputPopover = ({
id,
refInput,
onInputLostFocus,
selectedOption,
setSelectedOption,
selectedOptions,
setSelectedOptions,
value,
autoFocus,
disabled,
setShowOptions,
required,
className,
password,
pwdVisible,
editNode,
placeholder,
onChange,
blurOnEnter,
options,
optionsPlaceholder,
optionButton,
optionsButton,
handleKeyDown,
showOptions,
}) => {
const setErrorData = useAlertStore.getState().setErrorData;
const handleInputChange = (e) => {
if (password) {
if (
e.target.value.split("").every((char) => char === "•") &&
e.target.value !== ""
) {
setErrorData({
title: `Invalid characters: ${e.target.value}`,
list: [
"It seems you are trying to paste a password. Make sure the value is visible before copying from another field.",
],
});
}
}
onChange && onChange(e.target.value);
};
return (
<Popover modal open={showOptions} onOpenChange={setShowOptions}>
<PopoverAnchor>
<Input
id={id}
ref={refInput}
type="text"
onBlur={onInputLostFocus}
value={
(selectedOption !== "" || !onChange) && setSelectedOption
? selectedOption
: (selectedOptions?.length !== 0 || !onChange) &&
setSelectedOptions
? selectedOptions?.join(", ")
: value
}
autoFocus={autoFocus}
disabled={disabled}
onClick={() => {
(((selectedOption !== "" || !onChange) && setSelectedOption) ||
((selectedOptions?.length !== 0 || !onChange) &&
setSelectedOptions)) &&
setShowOptions(true);
}}
required={required}
className={classNames(
password &&
(!setSelectedOption || selectedOption === "") &&
!pwdVisible &&
value !== ""
? " text-clip password "
: "",
editNode ? " input-edit-node " : "",
password && (setSelectedOption || setSelectedOptions)
? "pr-[62.9px]"
: "",
(!password && (setSelectedOption || setSelectedOptions)) ||
(password && !(setSelectedOption || setSelectedOptions))
? "pr-8"
: "",
className!,
)}
placeholder={password && editNode ? "Key" : placeholder}
onChange={handleInputChange}
onKeyDown={(e) => {
handleKeyDown(e);
if (blurOnEnter && e.key === "Enter") refInput.current?.blur();
}}
data-testid={editNode ? id + "-edit" : id}
/>
</PopoverAnchor>
<PopoverContentWithoutPortal
className="nocopy nopan nodelete nodrag noundo p-0"
style={{ minWidth: refInput?.current?.clientWidth ?? "200px" }}
side="bottom"
align="center"
>
<Command
filter={(value, search) => {
if (
value.toLowerCase().includes(search.toLowerCase()) ||
value.includes("doNotFilter-")
)
return 1;
return 0;
}}
>
<CommandInput placeholder={optionsPlaceholder} />
<CommandList>
<CommandGroup defaultChecked={false}>
{options.map((option, id) => (
<CommandItem
className="group"
key={option + id}
value={option}
onSelect={(currentValue) => {
setSelectedOption &&
setSelectedOption(
currentValue === selectedOption ? "" : currentValue,
);
setSelectedOptions &&
setSelectedOptions(
selectedOptions?.includes(currentValue)
? selectedOptions.filter(
(item) => item !== currentValue,
)
: [...selectedOptions, currentValue],
);
!setSelectedOptions && setShowOptions(false);
}}
>
<div className="group flex w-full items-center justify-between">
<div className="flex items-center">
<div
className={cn(
"relative mr-2 h-4 w-4",
selectedOption === option ||
selectedOptions?.includes(option)
? "opacity-100"
: "opacity-0",
)}
>
<div className="absolute opacity-100 transition-all group-hover:opacity-0">
<ForwardedIconComponent
name="Check"
className="mr-2 h-4 w-4 text-primary"
aria-hidden="true"
/>
</div>
<div className="absolute opacity-0 transition-all group-hover:opacity-100">
<ForwardedIconComponent
name="X"
className="mr-2 h-4 w-4 text-status-red"
aria-hidden="true"
/>
</div>
</div>
{option}
</div>
{optionButton && optionButton(option)}
</div>
</CommandItem>
))}
{optionsButton && optionsButton}
</CommandGroup>
</CommandList>
</Command>
</PopoverContentWithoutPortal>
</Popover>
);
};
export default CustomInputPopover;

View file

@ -0,0 +1,167 @@
import { PopoverAnchor } from "@radix-ui/react-popover";
import { classNames, cn } from "../../../../utils/utils";
import ForwardedIconComponent from "../../../genericIconComponent";
import {
Command,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "../../../ui/command";
import { Input } from "../../../ui/input";
import { Popover, PopoverContentWithoutPortal } from "../../../ui/popover";
const CustomInputPopoverObject = ({
id,
refInput,
onInputLostFocus,
selectedOption,
setSelectedOption,
selectedOptions,
setSelectedOptions,
value,
autoFocus,
disabled,
setShowOptions,
required,
className,
placeholder,
onChange,
blurOnEnter,
options,
optionsPlaceholder,
optionButton,
optionsButton,
handleKeyDown,
showOptions,
}) => {
const handleInputChange = (e) => {
onChange && onChange(e.target.value);
};
return (
<Popover modal open={showOptions} onOpenChange={setShowOptions}>
<PopoverAnchor>
<Input
id={id}
ref={refInput}
type="text"
onBlur={onInputLostFocus}
value={
(selectedOption !== "" || !onChange) && setSelectedOption
? options.find((option) => option.id === selectedOption)?.name ||
""
: (selectedOptions?.length !== 0 || !onChange) &&
setSelectedOptions
? selectedOptions
.map(
(optionId) =>
options.find((option) => option.id === optionId)?.name,
)
.join(", ")
: value
}
autoFocus={autoFocus}
disabled={disabled}
onClick={() => {
(((selectedOption !== "" || !onChange) && setSelectedOption) ||
((selectedOptions?.length !== 0 || !onChange) &&
setSelectedOptions)) &&
setShowOptions(true);
}}
required={required}
className={classNames(className!)}
placeholder={placeholder}
onChange={handleInputChange}
onKeyDown={(e) => {
handleKeyDown(e);
if (blurOnEnter && e.key === "Enter") refInput.current?.blur();
}}
data-testid={id}
/>
</PopoverAnchor>
<PopoverContentWithoutPortal
className="nocopy nopan nodelete nodrag noundo p-0"
style={{ minWidth: refInput?.current?.clientWidth ?? "200px" }}
side="bottom"
align="center"
>
<Command
filter={(value, search) => {
if (
value.toLowerCase().includes(search.toLowerCase()) ||
value.includes("doNotFilter-")
)
return 1;
return 0;
}}
>
<CommandInput placeholder={optionsPlaceholder} />
<CommandList>
<CommandGroup defaultChecked={false}>
{options.map((option, index) => (
<CommandItem
className="group"
key={option.id}
value={option.id}
onSelect={(currentValue) => {
setSelectedOption &&
setSelectedOption(
currentValue === selectedOption ? "" : currentValue,
);
setSelectedOptions &&
setSelectedOptions(
selectedOptions?.includes(currentValue)
? selectedOptions.filter(
(item) => item !== currentValue,
)
: [...selectedOptions, currentValue],
);
!setSelectedOptions && setShowOptions(false);
}}
>
<div className="group flex w-full items-center justify-between">
<div className="flex items-center">
<div
className={cn(
"relative mr-2 h-4 w-4",
selectedOption === option.id ||
selectedOptions?.includes(option.id)
? "opacity-100"
: "opacity-0",
)}
>
<div className="absolute opacity-100 transition-all group-hover:opacity-0">
<ForwardedIconComponent
name="Check"
className="mr-2 h-4 w-4 text-primary"
aria-hidden="true"
/>
</div>
<div className="absolute opacity-0 transition-all group-hover:opacity-100">
<ForwardedIconComponent
name="X"
className="mr-2 h-4 w-4 text-status-red"
aria-hidden="true"
/>
</div>
</div>
<span data-testid={`option-${index}`}>
{option.name}{" "}
</span>
{/* Display the name property of the option */}
</div>
{optionButton && optionButton(option)}
</div>
</CommandItem>
))}
{optionsButton && optionsButton}
</CommandGroup>
</CommandList>
</Command>
</PopoverContentWithoutPortal>
</Popover>
);
};
export default CustomInputPopoverObject;

View file

@ -1,20 +1,12 @@
import * as Form from "@radix-ui/react-form";
import { PopoverAnchor } from "@radix-ui/react-popover";
import { useEffect, useRef, useState } from "react";
import useAlertStore from "../../stores/alertStore";
import { InputComponentType } from "../../types/components";
import { handleKeyDown } from "../../utils/reactflowUtils";
import { classNames, cn } from "../../utils/utils";
import ForwardedIconComponent from "../genericIconComponent";
import {
Command,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "../ui/command";
import { Input } from "../ui/input";
import { Popover, PopoverContentWithoutPortal } from "../ui/popover";
import CustomInputPopover from "./components/popover";
import CustomInputPopoverObject from "./components/popoverObject";
export default function InputComponent({
autoFocus = false,
@ -39,16 +31,16 @@ export default function InputComponent({
optionsPlaceholder = "Search options...",
optionsButton,
optionButton,
objectOptions,
isObjectOption = false,
}: InputComponentType): JSX.Element {
const setErrorData = useAlertStore.getState().setErrorData;
const [pwdVisible, setPwdVisible] = useState(false);
const refInput = useRef<HTMLInputElement>(null);
const [showOptions, setShowOptions] = useState<boolean>(false);
// Clear component state
useEffect(() => {
if (disabled && value && onChange && value !== "") {
onChange("");
onChange("", true);
}
}, [disabled]);
@ -93,181 +85,61 @@ export default function InputComponent({
</Form.Control>
) : (
<>
<Popover modal open={showOptions} onOpenChange={setShowOptions}>
<PopoverAnchor>
<Input
id={id}
ref={refInput}
type="text"
onBlur={onInputLostFocus}
value={
(selectedOption !== "" || !onChange) && setSelectedOption
? selectedOption
: (selectedOptions?.length !== 0 || !onChange) &&
setSelectedOptions
? selectedOptions?.join(", ")
: value
}
autoFocus={autoFocus}
disabled={disabled}
onClick={() => {
(((selectedOption !== "" || !onChange) &&
setSelectedOption) ||
((selectedOptions?.length !== 0 || !onChange) &&
setSelectedOptions)) &&
setShowOptions(true);
}}
required={required}
className={classNames(
password &&
(!setSelectedOption || selectedOption === "") &&
!pwdVisible &&
value !== ""
? " text-clip password "
: "",
editNode ? " input-edit-node " : "",
password && (setSelectedOption || setSelectedOptions)
? "pr-[62.9px]"
: "",
(!password && (setSelectedOption || setSelectedOptions)) ||
(password && !(setSelectedOption || setSelectedOptions))
? "pr-8"
: "",
className!,
)}
placeholder={password && editNode ? "Key" : placeholder}
onChange={(e) => {
// if the user copies a password from another input
// it might come as ••••••••••• it causes errors
// in ascii encoding, so we need to handle it
if (password) {
// check if all chars are •
if (
e.target.value.split("").every((char) => char === "•") &&
e.target.value !== ""
) {
setErrorData({
title: `Invalid characters: ${e.target.value}`,
list: [
"It seems you are trying to paste a password. Make sure the value is visible before copying from another field.",
],
});
}
}
onChange && onChange(e.target.value);
}}
onKeyDown={(e) => {
handleKeyDown(e, value, "");
if (blurOnEnter && e.key === "Enter")
refInput.current?.blur();
}}
data-testid={editNode ? id + "-edit" : id}
/>
</PopoverAnchor>
<PopoverContentWithoutPortal
className="nocopy nopan nodelete nodrag noundo p-0"
style={{ minWidth: refInput?.current?.clientWidth ?? "200px" }}
side="bottom"
align="center"
>
<Command
filter={(value, search) => {
if (
value.toLowerCase().includes(search.toLowerCase()) ||
value.includes("doNotFilter-")
)
return 1; // ensures items arent filtered
return 0;
}}
>
<CommandInput placeholder={optionsPlaceholder} />
<CommandList>
<CommandGroup defaultChecked={false}>
{options.map((option, id) => (
<CommandItem
className="group"
key={option + id}
value={option}
onSelect={(currentValue) => {
setSelectedOption &&
setSelectedOption(
currentValue === selectedOption
? ""
: currentValue,
);
setSelectedOptions &&
setSelectedOptions(
selectedOptions?.includes(currentValue)
? selectedOptions.filter(
(item) => item !== currentValue,
)
: [...selectedOptions, currentValue],
);
!setSelectedOptions && setShowOptions(false);
}}
>
<div className="group flex w-full items-center justify-between">
<div className="flex items-center">
<div
className={cn(
"relative mr-2 h-4 w-4",
selectedOption === option ||
selectedOptions?.includes(option)
? "opacity-100"
: "opacity-0",
)}
>
<div className="absolute opacity-100 transition-all group-hover:opacity-0">
<ForwardedIconComponent
name="Check"
className="mr-2 h-4 w-4 text-primary"
aria-hidden="true"
/>
</div>
<div className="absolute opacity-0 transition-all group-hover:opacity-100">
<ForwardedIconComponent
name="X"
className="mr-2 h-4 w-4 text-status-red"
aria-hidden="true"
/>
</div>
</div>
{option}
</div>
{optionButton && optionButton(option)}
</div>
</CommandItem>
))}
{optionsButton && optionsButton}
</CommandGroup>
</CommandList>
</Command>
</PopoverContentWithoutPortal>
</Popover>
<div
data-testid={"popover-anchor-" + id}
className={cn(
"pointer-events-auto absolute inset-y-0 h-full w-full cursor-pointer",
((selectedOption !== "" || !onChange) && setSelectedOption) ||
((selectedOptions?.length !== 0 || !onChange) &&
setSelectedOptions)
? ""
: "hidden",
)}
onClick={
((selectedOption !== "" || !onChange) && setSelectedOption) ||
((selectedOptions?.length !== 0 || !onChange) &&
setSelectedOptions)
? (e) => {
setShowOptions((old) => !old);
e.preventDefault();
e.stopPropagation();
}
: () => {}
}
></div>
{isObjectOption ? (
// Content to render when isObjectOption is true
<CustomInputPopoverObject
refInput={refInput}
handleKeyDown={handleKeyDown}
optionButton={optionButton}
optionsButton={optionsButton}
showOptions={showOptions}
onChange={onChange}
id={`object-${id}`}
onInputLostFocus={onInputLostFocus}
selectedOption={selectedOption}
setSelectedOption={setSelectedOption}
selectedOptions={selectedOptions}
setSelectedOptions={setSelectedOptions}
options={objectOptions}
value={value}
autoFocus={autoFocus}
disabled={disabled}
setShowOptions={setShowOptions}
required={required}
placeholder={placeholder}
blurOnEnter={blurOnEnter}
optionsPlaceholder={optionsPlaceholder}
className={className}
/>
) : (
<CustomInputPopover
refInput={refInput}
handleKeyDown={handleKeyDown}
optionButton={optionButton}
optionsButton={optionsButton}
showOptions={showOptions}
onChange={onChange}
id={`popover-anchor-${id}`}
onInputLostFocus={onInputLostFocus}
selectedOption={selectedOption}
setSelectedOption={setSelectedOption}
selectedOptions={selectedOptions}
setSelectedOptions={setSelectedOptions}
value={value}
autoFocus={autoFocus}
disabled={disabled}
setShowOptions={setShowOptions}
required={required}
password={password}
pwdVisible={pwdVisible}
editNode={editNode}
placeholder={placeholder}
blurOnEnter={blurOnEnter}
options={options}
optionsPlaceholder={optionsPlaceholder}
className={className}
/>
)}
</>
)}
@ -279,8 +151,10 @@ export default function InputComponent({
)}
>
<button
onClick={() => {
onClick={(e) => {
setShowOptions(!showOptions);
e.preventDefault();
e.stopPropagation();
}}
className={cn(
selectedOption !== ""

View file

@ -27,7 +27,7 @@ export default function InputFileComponent({
useEffect(() => {
if (disabled && value !== "") {
setMyValue("");
onChange("");
onChange("", true);
onFileChange("");
}
}, [disabled, onChange]);
@ -106,8 +106,8 @@ export default function InputFileComponent({
editNode
? "input-edit-node input-dialog text-muted-foreground"
: disabled
? "input-disable input-dialog primary-input"
: "input-dialog primary-input text-muted-foreground"
? "input-disable input-dialog primary-input"
: "input-dialog primary-input text-muted-foreground"
}
>
{myValue !== "" ? myValue : "No file"}

View file

@ -19,15 +19,15 @@ export default function InputGlobalComponent({
editNode = false,
}: InputGlobalComponentType): JSX.Element {
const globalVariablesEntries = useGlobalVariablesStore(
(state) => state.globalVariablesEntries
(state) => state.globalVariablesEntries,
);
const getVariableId = useGlobalVariablesStore((state) => state.getVariableId);
const unavaliableFields = useGlobalVariablesStore(
(state) => state.unavaliableFields
(state) => state.unavaliableFields,
);
const removeGlobalVariable = useGlobalVariablesStore(
(state) => state.removeGlobalVariable
(state) => state.removeGlobalVariable,
);
const setErrorData = useAlertStore((state) => state.setErrorData);
@ -38,7 +38,7 @@ export default function InputGlobalComponent({
data.node?.template[name].load_from_db
) {
setTimeout(() => {
onChange("");
onChange("", true);
setDb(false);
}, 100);
}
@ -129,7 +129,7 @@ export default function InputGlobalComponent({
<ForwardedIconComponent
name="Trash2"
className={cn(
"h-4 w-4 text-primary opacity-0 hover:text-status-red group-hover:opacity-100"
"h-4 w-4 text-primary opacity-0 hover:text-status-red group-hover:opacity-100",
)}
aria-hidden="true"
/>
@ -146,8 +146,8 @@ export default function InputGlobalComponent({
onChange(value);
setDb(value !== "" ? true : false);
}}
onChange={(value) => {
onChange(value);
onChange={(value, skipSnapshot) => {
onChange(value, skipSnapshot);
setDb(false);
}}
/>

View file

@ -19,7 +19,7 @@ export default function IntComponent({
// Clear component state
useEffect(() => {
if (disabled && value !== "") {
onChange("");
onChange("", true);
}
}, [disabled, onChange]);

View file

@ -0,0 +1,7 @@
export default function NumberReader({
number,
}: {
number: number;
}): JSX.Element {
return <span>{number}</span>;
}

View file

@ -0,0 +1,13 @@
import DictAreaModal from "../../modals/dictAreaModal";
export default function ObjectRender({ object }: { object: any }): JSX.Element {
//TODO check object type
return (
<DictAreaModal value={object}>
<div className="flex h-full w-full items-center align-middle transition-all hover:scale-105">
<div className="truncate">{JSON.stringify(object)}</div>
</div>
</DictAreaModal>
);
}

View file

@ -18,7 +18,7 @@ export default function PromptAreaComponent({
}: PromptAreaComponentType): JSX.Element {
useEffect(() => {
if (disabled && value !== "") {
onChange("");
onChange("", true);
}
}, [disabled]);

View file

@ -0,0 +1,34 @@
import { ColDef, ColGroupDef } from "ag-grid-community";
import "ag-grid-community/styles/ag-grid.css"; // Mandatory CSS required by the grid
import "ag-grid-community/styles/ag-theme-balham.css"; // Optional Theme applied to the grid
import { FlowPoolObjectType } from "../../types/chat";
import TableComponent from "../tableComponent";
import { extractColumnsFromRows } from "../../utils/utils";
function RecordsOutputComponent({
flowPool,
pagination,
}: {
flowPool: FlowPoolObjectType;
pagination: boolean;
}) {
const rows = flowPool?.data?.artifacts?.records ?? [];
const columns = extractColumnsFromRows(rows, "union");
const columnDefs = columns.map((col, idx) => ({
...col,
resizable: idx !== columns.length - 1,
flex: idx !== columns.length - 1 ? 1 : 2,
})) as (ColDef<any> | ColGroupDef<any>)[];
return (
<TableComponent
overlayNoRowsTemplate="No data available"
suppressRowClickSelection={true}
pagination={pagination}
columnDefs={columnDefs}
rowData={rows}
/>
);
}
export default RecordsOutputComponent;

View file

@ -0,0 +1,38 @@
import { Link } from "react-router-dom";
import { cn } from "../../../../utils/utils";
import { buttonVariants } from "../../../ui/button";
type SideBarButtonsComponentProps = {
items: {
href?: string;
title: string;
icon: React.ReactNode;
}[];
pathname: string;
handleOpenNewFolderModal?: () => void;
};
const SideBarButtonsComponent = ({
items,
handleOpenNewFolderModal,
}: SideBarButtonsComponentProps) => {
return (
<>
{items.map((item) => (
<Link to={item.href!}>
<div
key={item.title}
data-testid={`sidebar-nav-${item.title}`}
className={cn(
buttonVariants({ variant: "ghost" }),
"!w-[200px] cursor-pointer justify-start gap-2 border border-transparent hover:border-border hover:bg-transparent",
)}
onClick={handleOpenNewFolderModal}
>
{item.title}
</div>
</Link>
))}
</>
);
};
export default SideBarButtonsComponent;

View file

@ -0,0 +1,170 @@
import { useLocation } from "react-router-dom";
import { FolderType } from "../../../../pages/MainPage/entities";
import { useFolderStore } from "../../../../stores/foldersStore";
import { cn } from "../../../../utils/utils";
import DropdownButton from "../../../dropdownButtonComponent";
import IconComponent, {
ForwardedIconComponent,
} from "../../../genericIconComponent";
import { Button, buttonVariants } from "../../../ui/button";
import useFileDrop from "../../hooks/use-on-file-drop";
import useFlowsManagerStore from "../../../../stores/flowsManagerStore";
import { handleDownloadFolderFn } from "../../../../pages/MainPage/utils/handle-download-folder";
import useAlertStore from "../../../../stores/alertStore";
type SideBarFoldersButtonsComponentProps = {
folders: FolderType[];
pathname: string;
handleChangeFolder?: (id: string) => void;
handleEditFolder?: (item: FolderType) => void;
handleDeleteFolder?: (item: FolderType) => void;
handleAddFolder?: () => void;
};
const SideBarFoldersButtonsComponent = ({
folders,
pathname,
handleAddFolder,
handleChangeFolder,
handleEditFolder,
handleDeleteFolder,
}: SideBarFoldersButtonsComponentProps) => {
const uploadFolder = useFolderStore((state) => state.uploadFolder);
const currentFolder = pathname.split("/");
const urlWithoutPath = pathname.split("/").length < 4;
const myCollectionId = useFolderStore((state) => state.myCollectionId);
const allFlows = useFlowsManagerStore((state) => state.allFlows);
const setErrorData = useAlertStore((state) => state.setErrorData);
const checkPathName = (itemId: string) => {
if (urlWithoutPath && itemId === myCollectionId) {
return true;
}
return currentFolder.includes(itemId);
};
const location = useLocation();
const folderId = location?.state?.folderId ?? myCollectionId;
const getFolderById = useFolderStore((state) => state.getFolderById);
const handleFolderChange = (folderId: string) => {
getFolderById(folderId);
};
const { dragOver, dragEnter, dragLeave, onDrop } = useFileDrop(
folderId,
handleFolderChange,
);
const handleUploadFlowsToFolder = () => {
uploadFolder(folderId);
};
const handleDownloadFolder = (id: string) => {
handleDownloadFolderFn(id);
};
return (
<>
<div className="flex shrink-0 items-center justify-between">
<DropdownButton
firstButtonName="New Folder"
onFirstBtnClick={handleAddFolder!}
options={[]}
plusButton={true}
dropdownOptions={false}
/>
<Button
variant="primary"
onClick={handleUploadFlowsToFolder}
className="px-7"
>
<ForwardedIconComponent
name="Upload"
className="main-page-nav-button"
/>
Upload
</Button>
</div>
<div className="flex gap-2 overflow-auto lg:h-[70vh] lg:flex-col">
<>
{folders.map((item, index) => (
<div
onDragOver={dragOver}
onDragEnter={dragEnter}
onDragLeave={dragLeave}
onDrop={(e) => onDrop(e, item.id!)}
key={item.id}
data-testid={`sidebar-nav-${item.name}`}
className={cn(
buttonVariants({ variant: "ghost" }),
checkPathName(item.id!)
? "border border-border bg-muted hover:bg-muted"
: "border hover:bg-transparent lg:border-transparent lg:hover:border-border",
"group flex w-full shrink-0 cursor-pointer gap-2 opacity-100 lg:min-w-full",
)}
onClick={() => handleChangeFolder!(item.id!)}
>
<div className="flex w-full items-center gap-2">
<IconComponent
name={"folder"}
className="mr-2 w-4 flex-shrink-0 justify-start stroke-[1.5] opacity-100"
/>
<span className="block max-w-full truncate opacity-100">
{item.name}
</span>
<div className="flex-1" />
{index > 0 && (
<Button
className="hidden p-0 hover:bg-white group-hover:block hover:dark:bg-[#0c101a00]"
onClick={(e) => {
handleDeleteFolder!(item);
e.stopPropagation();
e.preventDefault();
}}
variant={"ghost"}
>
<IconComponent
name={"trash"}
className=" w-4 stroke-[1.5]"
/>
</Button>
)}
{index > 0 && (
<Button
className="hidden p-0 hover:bg-white group-hover:block hover:dark:bg-[#0c101a00]"
onClick={(e) => {
handleEditFolder!(item);
e.stopPropagation();
e.preventDefault();
}}
variant={"ghost"}
>
<IconComponent
name={"pencil"}
className=" w-4 stroke-[1.5] text-white "
/>
</Button>
)}
<Button
className="hidden p-0 hover:bg-white group-hover:block hover:dark:bg-[#0c101a00]"
onClick={(e) => {
handleDownloadFolder(item.id!);
e.stopPropagation();
e.preventDefault();
}}
variant={"ghost"}
>
<IconComponent
name={"Download"}
className=" w-4 stroke-[1.5] text-white "
/>
</Button>
</div>
</div>
))}
</>
</div>
</>
);
};
export default SideBarFoldersButtonsComponent;

View file

@ -0,0 +1,134 @@
import {
UPLOAD_ALERT_LIST,
WRONG_FILE_ERROR_ALERT,
} from "../../../constants/alerts_constants";
import { updateFlowInDatabase } from "../../../controllers/API";
import { uploadFlowsFromFolders } from "../../../pages/MainPage/services";
import useAlertStore from "../../../stores/alertStore";
import useFlowsManagerStore from "../../../stores/flowsManagerStore";
import { useFolderStore } from "../../../stores/foldersStore";
import { FlowType } from "../../../types/flow";
const useFileDrop = (folderId, folderChangeCallback) => {
const setFolderDragging = useFolderStore((state) => state.setFolderDragging);
const setErrorData = useAlertStore((state) => state.setErrorData);
const getFoldersApi = useFolderStore((state) => state.getFoldersApi);
const refreshFlows = useFlowsManagerStore((state) => state.refreshFlows);
const flows = useFlowsManagerStore((state) => state.flows);
const triggerFolderChange = (folderId) => {
if (folderChangeCallback) {
folderChangeCallback(folderId);
}
};
const handleFileDrop = async (e) => {
if (e.dataTransfer.types.some((type) => type === "Files")) {
if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
const firstFile = e.dataTransfer.files[0];
if (firstFile.type === "application/json") {
uploadFormData(firstFile);
} else {
setErrorData({
title: WRONG_FILE_ERROR_ALERT,
list: [UPLOAD_ALERT_LIST],
});
}
}
}
};
const dragOver = (
e:
| React.DragEvent<HTMLDivElement>
| React.DragEvent<HTMLButtonElement>
| React.DragEvent<HTMLAnchorElement>,
) => {
e.preventDefault();
if (e.dataTransfer.types.some((types) => types === "Files")) {
setFolderDragging(true);
}
};
const dragEnter = (
e:
| React.DragEvent<HTMLDivElement>
| React.DragEvent<HTMLButtonElement>
| React.DragEvent<HTMLAnchorElement>,
) => {
if (e.dataTransfer.types.some((types) => types === "Files")) {
setFolderDragging(true);
}
e.preventDefault();
};
const dragLeave = (
e:
| React.DragEvent<HTMLDivElement>
| React.DragEvent<HTMLButtonElement>
| React.DragEvent<HTMLAnchorElement>,
) => {
e.preventDefault();
if (e.target === e.currentTarget) {
setFolderDragging(false);
}
};
const onDrop = (
e:
| React.DragEvent<HTMLDivElement>
| React.DragEvent<HTMLButtonElement>
| React.DragEvent<HTMLAnchorElement>,
folderId: string,
) => {
if (e?.dataTransfer?.getData("flow")) {
const data = JSON.parse(e?.dataTransfer?.getData("flow"));
if (data) {
uploadFromDragCard(data.id, folderId);
return;
}
}
e.preventDefault();
handleFileDrop(e);
setFolderDragging(false);
};
const uploadFromDragCard = (flowId, folderId) => {
const selectedFlow = flows.find((flow) => flow.id === flowId);
if (!selectedFlow) {
throw new Error("Flow not found");
}
const updatedFlow: FlowType = {
...selectedFlow,
folder_id: folderId,
};
updateFlowInDatabase(updatedFlow).then(() => {
getFoldersApi(true);
triggerFolderChange(folderId);
});
};
const uploadFormData = (data) => {
const formData = new FormData();
formData.append("file", data);
uploadFlowsFromFolders(formData).then(() => {
getFoldersApi(true);
triggerFolderChange(folderId);
refreshFlows();
});
};
return {
dragOver,
dragEnter,
dragLeave,
onDrop,
};
};
export default useFileDrop;

View file

@ -1,48 +1,61 @@
import { Link, useLocation } from "react-router-dom";
import { useLocation } from "react-router-dom";
import { FolderType } from "../../pages/MainPage/entities";
import { useFolderStore } from "../../stores/foldersStore";
import { cn } from "../../utils/utils";
import { buttonVariants } from "../ui/button";
import HorizontalScrollFadeComponent from "../horizontalScrollFadeComponent";
import SideBarButtonsComponent from "./components/sideBarButtons";
import SideBarFoldersButtonsComponent from "./components/sideBarFolderButtons";
interface SidebarNavProps extends React.HTMLAttributes<HTMLElement> {
type SidebarNavProps = {
items: {
href: string;
href?: string;
title: string;
icon: React.ReactNode;
}[];
}
handleOpenNewFolderModal?: () => void;
handleChangeFolder?: (id: string) => void;
handleEditFolder?: (item: FolderType) => void;
handleDeleteFolder?: (item: FolderType) => void;
className?: string;
};
export default function SidebarNav({
className,
items,
handleOpenNewFolderModal,
handleChangeFolder,
handleEditFolder,
handleDeleteFolder,
...props
}: SidebarNavProps) {
const location = useLocation();
const pathname = location.pathname;
const loadingFolders = useFolderStore((state) => state.loading);
const folders = useFolderStore((state) => state.folders);
const pathValues = ["folder", "components", "flows", "all"];
const isFolderPath = pathValues.some((value) => pathname.includes(value));
return (
<nav
className={cn(
"flex space-x-2 lg:flex-col lg:space-x-0 lg:space-y-1",
className
)}
{...props}
>
{items.map((item) => (
<Link
data-testid={`sidebar-nav-${item.title}`}
key={item.href}
to={item.href}
className={cn(
buttonVariants({ variant: "ghost" }),
pathname === item.href
? "border border-border bg-muted hover:bg-muted"
: "border border-transparent hover:border-border hover:bg-transparent",
"justify-start gap-2"
)}
>
{item.icon}
{item.title}
</Link>
))}
<nav className={cn(className)} {...props}>
<HorizontalScrollFadeComponent>
<SideBarButtonsComponent
items={items}
pathname={pathname}
handleOpenNewFolderModal={handleOpenNewFolderModal}
/>
{!loadingFolders && folders?.length > 0 && isFolderPath && (
<SideBarFoldersButtonsComponent
folders={folders}
pathname={pathname}
handleChangeFolder={handleChangeFolder}
handleEditFolder={handleEditFolder}
handleDeleteFolder={handleDeleteFolder}
handleAddFolder={handleOpenNewFolderModal}
/>
)}
</HorizontalScrollFadeComponent>
</nav>
);
}

View file

@ -5,7 +5,7 @@ export const StoreGuard = ({ children }) => {
const hasStore = useStoreStore((state) => state.hasStore);
if (!hasStore) {
return <Navigate to="/flows" replace />;
return <Navigate to="/all" replace />;
}
return children;

View file

@ -0,0 +1,7 @@
export default function StringReader({
string,
}: {
string: string;
}): JSX.Element {
return <span className="truncate">{string}</span>;
}

View file

@ -0,0 +1,60 @@
import { CustomCellRendererProps } from "ag-grid-react";
import { cn, isTimeStampString } from "../../utils/utils";
import ArrayReader from "../arrayReaderComponent";
import DateReader from "../dateReaderComponent";
import NumberReader from "../numberReader";
import ObjectRender from "../objectRender";
import StringReader from "../stringReaderComponent";
import { Label } from "../ui/label";
import { Badge } from "../ui/badge";
export default function TableAutoCellRender({
value,
}: CustomCellRendererProps) {
function getCellType() {
switch (typeof value) {
case "object":
if (value === null) {
return String(value);
} else if (Array.isArray(value)) {
return <ArrayReader array={value} />;
} else if (value.definitions) {
// use a custom render defined by the sender
} else {
return <ObjectRender object={value} />;
}
break;
case "string":
if (isTimeStampString(value)) {
return <DateReader date={value} />;
}
//TODO: REFACTOR FOR ANY LABEL NOT HARDCODED
else if (value === "success") {
return (
<Badge
variant="outline"
size="sq"
className={cn(
"min-w-min bg-success-background text-success-foreground hover:bg-success-background",
)}
>
{value}
</Badge>
);
} else {
return <StringReader string={value} />;
}
break;
case "number":
return <NumberReader number={value} />;
default:
return String(value);
}
}
return (
<div className="group flex h-full w-full items-center align-middle">
{getCellType()}
</div>
);
}

View file

@ -1,27 +1,119 @@
import "ag-grid-community/styles/ag-grid.css"; // Mandatory CSS required by the grid
import "ag-grid-community/styles/ag-theme-quartz.css"; // Optional Theme applied to the grid
import { AgGridReact } from "ag-grid-react";
import { ComponentPropsWithoutRef, ElementRef, forwardRef } from "react";
import { AgGridReact, AgGridReactProps } from "ag-grid-react";
import { ElementRef, forwardRef, useCallback } from "react";
import {
DEFAULT_TABLE_ALERT_MSG,
DEFAULT_TABLE_ALERT_TITLE,
} from "../../constants/constants";
import { useDarkStore } from "../../stores/darkStore";
import "../../style/ag-theme-shadcn.css"; // Custom CSS applied to the grid
import { cn } from "../../utils/utils";
import ForwardedIconComponent from "../genericIconComponent";
import { Alert, AlertDescription, AlertTitle } from "../ui/alert";
interface TableComponentProps extends AgGridReactProps {
columnDefs: NonNullable<AgGridReactProps["columnDefs"]>;
rowData: NonNullable<AgGridReactProps["rowData"]>;
alertTitle?: string;
alertDescription?: string;
}
const TableComponent = forwardRef<
ElementRef<typeof AgGridReact>,
ComponentPropsWithoutRef<typeof AgGridReact>
>(({ ...props }, ref) => {
const dark = useDarkStore((state) => state.dark);
TableComponentProps
>(
(
{
alertTitle = DEFAULT_TABLE_ALERT_TITLE,
alertDescription = DEFAULT_TABLE_ALERT_MSG,
...props
},
ref,
) => {
const dark = useDarkStore((state) => state.dark);
var currentRowHeight: number;
var minRowHeight = 25;
return (
<div
className={cn(
dark ? "ag-theme-quartz-dark" : "ag-theme-quartz",
"ag-theme-shadcn flex h-full flex-col",
)} // applying the grid theme
>
<AgGridReact ref={ref} {...props} />
</div>
);
});
const getRowHeight = useCallback(() => {
return currentRowHeight;
}, []);
const onGridReady = useCallback((params: any) => {
minRowHeight = params.api.getSizesForCurrentTheme().rowHeight;
currentRowHeight = minRowHeight;
}, []);
const updateRowHeight = (params: { api: any }) => {
const bodyViewport = document.querySelector(".ag-body-viewport");
if (!bodyViewport) {
return;
}
var gridHeight = bodyViewport.clientHeight;
var renderedRowCount = params.api.getDisplayedRowCount();
if (renderedRowCount * minRowHeight >= gridHeight) {
if (currentRowHeight !== minRowHeight) {
currentRowHeight = minRowHeight;
params.api.resetRowHeights();
}
} else {
currentRowHeight = Math.floor(gridHeight / renderedRowCount);
params.api.resetRowHeights();
}
};
const onFirstDataRendered = useCallback(
(params: any) => {
updateRowHeight(params);
},
[updateRowHeight],
);
const onGridSizeChanged = useCallback(
(params: any) => {
updateRowHeight(params);
},
[updateRowHeight],
);
if (props.rowData.length === 0) {
return (
<div className="flex h-full w-full items-center justify-center rounded-md border">
<Alert variant={"default"} className="w-fit">
<ForwardedIconComponent
name="AlertCircle"
className="h-5 w-5 text-primary"
/>
<AlertTitle>{alertTitle}</AlertTitle>
<AlertDescription>{alertDescription}</AlertDescription>
</Alert>
</div>
);
}
return (
<div
className={cn(
dark ? "ag-theme-quartz-dark" : "ag-theme-quartz",
"ag-theme-shadcn flex h-full flex-col",
)} // applying the grid theme
>
<AgGridReact
{...props}
className={cn(props.className, "custom-scroll")}
getRowHeight={getRowHeight}
onGridReady={onGridReady}
onFirstDataRendered={onFirstDataRendered}
onGridSizeChanged={onGridSizeChanged}
defaultColDef={{
minWidth: 100,
}}
ref={ref}
/>
</div>
);
},
);
export default TableComponent;

View file

@ -1,5 +1,5 @@
import { useEffect, useRef, useState } from "react";
import { cn } from "../../utils/utils";
import HorizontalScrollFadeComponent from "../horizontalScrollFadeComponent";
import { Badge } from "../ui/badge";
export function TagsSelector({
@ -24,87 +24,37 @@ export function TagsSelector({
setSelectedTags(newArray);
};
const scrollContainerRef = useRef<HTMLDivElement>(null);
const fadeContainerRef = useRef<HTMLDivElement>(null);
const [divWidth, setDivWidth] = useState<number>(0);
useEffect(() => {
const handleResize = () => {
if (scrollContainerRef.current) {
setDivWidth(scrollContainerRef.current.clientWidth);
}
};
window.addEventListener("resize", handleResize);
handleResize(); // call the function at start to get the initial width
return () => window.removeEventListener("resize", handleResize);
}, []);
useEffect(() => {
const handleScroll = () => {
if (!scrollContainerRef.current || !fadeContainerRef.current) return;
const { scrollLeft, scrollWidth, clientWidth } =
scrollContainerRef.current;
const atStart = scrollLeft === 0;
const atEnd = scrollLeft === scrollWidth - clientWidth;
const isScrollable = scrollWidth > clientWidth;
fadeContainerRef.current.classList.toggle(
"fade-left",
isScrollable && !atStart
);
fadeContainerRef.current.classList.toggle(
"fade-right",
isScrollable && !atEnd
);
};
const scrollContainer = scrollContainerRef.current;
if (scrollContainer) {
scrollContainer.addEventListener("scroll", handleScroll);
// Delay the initial scroll event dispatch to ensure correct calculation
scrollContainer.dispatchEvent(new Event("scroll"));
return () => scrollContainer.removeEventListener("scroll", handleScroll);
}
}, [divWidth, loadingTags]); // Depend on divWidth
return (
<div ref={fadeContainerRef} className="fade-container">
<div
ref={scrollContainerRef}
className="scroll-container flex min-w-min gap-2"
>
{!loadingTags &&
tags.map((tag, idx) => (
<button
disabled={disabled}
className={
disabled
? "cursor-not-allowed"
: " overflow-hidden whitespace-nowrap"
}
onClick={() => {
updateTags(tag.name);
}}
<HorizontalScrollFadeComponent isFolder={false}>
{!loadingTags &&
tags.map((tag, idx) => (
<button
disabled={disabled}
className={
disabled
? "cursor-not-allowed"
: " overflow-hidden whitespace-nowrap"
}
onClick={() => {
updateTags(tag.name);
}}
key={idx}
data-testid={`tag-selector-${tag.name}`}
>
<Badge
key={idx}
data-testid={`tag-selector-${tag.name}`}
variant="outline"
size="sq"
className={cn(
selectedTags.some((category) => category === tag.name)
? "min-w-min bg-beta-foreground text-background hover:bg-beta-foreground"
: "",
)}
>
<Badge
key={idx}
variant="outline"
size="sq"
className={cn(
selectedTags.some((category) => category === tag.name)
? "min-w-min bg-beta-foreground text-background hover:bg-beta-foreground"
: ""
)}
>
{tag.name}
</Badge>
</button>
))}
</div>
</div>
{tag.name}
</Badge>
</button>
))}
</HorizontalScrollFadeComponent>
);
}

View file

@ -16,7 +16,7 @@ export default function TextAreaComponent({
// Clear text area
useEffect(() => {
if (disabled && value !== "") {
onChange("");
onChange("", true);
}
}, [disabled]);

View file

@ -0,0 +1,58 @@
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "../../utils/utils";
const alertVariants = cva(
"relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
{
variants: {
variant: {
default: "bg-background text-foreground",
destructive:
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
},
},
defaultVariants: {
variant: "default",
},
},
);
const Alert = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
>(({ className, variant, ...props }, ref) => (
<div
ref={ref}
role="alert"
className={cn(alertVariants({ variant }), className)}
{...props}
/>
));
Alert.displayName = "Alert";
const AlertTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h5
ref={ref}
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
{...props}
/>
));
AlertTitle.displayName = "AlertTitle";
const AlertDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("text-sm [&_p]:leading-relaxed", className)}
{...props}
/>
));
AlertDescription.displayName = "AlertDescription";
export { Alert, AlertTitle, AlertDescription };

View file

@ -13,14 +13,14 @@ const Checkbox = React.forwardRef<
ref={ref}
className={cn(
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
className
className,
)}
{...props}
>
<CheckboxPrimitive.Indicator
className={cn("flex items-center justify-center text-current")}
>
<IconComponent name="Check" className="h-4 w-4" />
<IconComponent name="Check" className="h-4 w-4 stroke-2" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
));
@ -37,7 +37,7 @@ const CheckBoxDiv = ({
className={cn(
className,
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
checked ? "bg-primary text-primary-foreground" : ""
checked ? "bg-primary text-primary-foreground" : "",
)}
>
{checked && (

View file

@ -0,0 +1,55 @@
"use client";
import * as AccordionPrimitive from "@radix-ui/react-accordion";
import { ChevronDownIcon } from "@radix-ui/react-icons";
import * as React from "react";
import { cn } from "../../utils/utils";
const Accordion = AccordionPrimitive.Root;
const AccordionItem = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
>(({ className, ...props }, ref) => (
<AccordionPrimitive.Item ref={ref} className={cn("", className)} {...props} />
));
AccordionItem.displayName = "AccordionItem";
const AccordionTrigger = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger asChild ref={ref} {...props}>
<div
className={cn(
" flex flex-1 cursor-pointer items-center justify-between border-[1px] py-2 text-sm font-medium data-[state=closed]:rounded-md data-[state=open]:rounded-t-md data-[state=open]:border-b-0 data-[state=open]:bg-muted [&[data-state=open]>svg]:rotate-180",
className,
)}
>
{children}
<ChevronDownIcon className="h-4 w-4 font-bold text-primary transition-transform duration-200" />
</div>
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
));
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
const AccordionContent = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Content
ref={ref}
className={cn(
"data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden border-[1px] text-sm data-[state=open]:rounded-b-md data-[state=open]:border-t-0 data-[state=open]:bg-muted",
className,
)}
{...props}
>
<div className="pt-0">{children}</div>
</AccordionPrimitive.Content>
));
AccordionContent.displayName = AccordionPrimitive.Content.displayName;
export { Accordion, AccordionContent, AccordionItem, AccordionTrigger };

View file

@ -0,0 +1,176 @@
import * as LabelPrimitive from "@radix-ui/react-label";
import { Slot } from "@radix-ui/react-slot";
import * as React from "react";
import {
Controller,
ControllerProps,
FieldPath,
FieldValues,
FormProvider,
useFormContext,
} from "react-hook-form";
import { cn } from "../../utils/utils";
import { Label } from "./label";
const Form = FormProvider;
type FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
> = {
name: TName;
};
const FormFieldContext = React.createContext<FormFieldContextValue>(
{} as FormFieldContextValue,
);
const FormField = <
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
>({
...props
}: ControllerProps<TFieldValues, TName>) => {
return (
<FormFieldContext.Provider value={{ name: props.name }}>
<Controller {...props} />
</FormFieldContext.Provider>
);
};
const useFormField = () => {
const fieldContext = React.useContext(FormFieldContext);
const itemContext = React.useContext(FormItemContext);
const { getFieldState, formState } = useFormContext();
const fieldState = getFieldState(fieldContext.name, formState);
if (!fieldContext) {
throw new Error("useFormField should be used within <FormField>");
}
const { id } = itemContext;
return {
id,
name: fieldContext.name,
formItemId: `${id}-form-item`,
formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`,
...fieldState,
};
};
type FormItemContextValue = {
id: string;
};
const FormItemContext = React.createContext<FormItemContextValue>(
{} as FormItemContextValue,
);
const FormItem = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
const id = React.useId();
return (
<FormItemContext.Provider value={{ id }}>
<div ref={ref} className={cn("space-y-2", className)} {...props} />
</FormItemContext.Provider>
);
});
FormItem.displayName = "FormItem";
const FormLabel = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
>(({ className, ...props }, ref) => {
const { error, formItemId } = useFormField();
return (
<Label
ref={ref}
className={cn(error && "text-destructive", className)}
htmlFor={formItemId}
{...props}
/>
);
});
FormLabel.displayName = "FormLabel";
const FormControl = React.forwardRef<
React.ElementRef<typeof Slot>,
React.ComponentPropsWithoutRef<typeof Slot>
>(({ ...props }, ref) => {
const { error, formItemId, formDescriptionId, formMessageId } =
useFormField();
return (
<Slot
ref={ref}
id={formItemId}
aria-describedby={
!error
? `${formDescriptionId}`
: `${formDescriptionId} ${formMessageId}`
}
aria-invalid={!!error}
{...props}
/>
);
});
FormControl.displayName = "FormControl";
const FormDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => {
const { formDescriptionId } = useFormField();
return (
<p
ref={ref}
id={formDescriptionId}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
);
});
FormDescription.displayName = "FormDescription";
const FormMessage = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, children, ...props }, ref) => {
const { error, formMessageId } = useFormField();
const body = error ? String(error?.message) : children;
if (!body) {
return null;
}
return (
<p
ref={ref}
id={formMessageId}
className={cn("text-sm font-medium text-destructive", className)}
{...props}
>
{body}
</p>
);
});
FormMessage.displayName = "FormMessage";
export {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
useFormField,
};

View file

@ -95,6 +95,12 @@ export const EXPORT_DIALOG_SUBTITLE = "Export flow as JSON file.";
*/
export const SETTINGS_DIALOG_SUBTITLE = "Edit details about your project.";
/**
* The base text for subtitle of Flow Logs (Menubar)
* @constant
*/
export const LOGS_DIALOG_SUBTITLE = "Check out information about your flow.";
/**
* The base text for subtitle of Code Dialog (Toolbar)
* @constant
@ -125,7 +131,6 @@ export const CODE_PROMPT_DIALOG_SUBTITLE =
export const CODE_DICT_DIALOG_SUBTITLE =
"Edit your dictionary. This dialog allows you to create your own customized dictionary. You can add as many key-value pairs as you want. While in edit mode, you can enter ({}) or ([]), and this will result in adding a new object or array.";
/**
* The base text for subtitle of Prompt Dialog
* @constant
@ -533,6 +538,8 @@ export const NOUNS: string[] = [
*/
export const USER_PROJECTS_HEADER = "My Collection";
export const DEFAULT_FOLDER = "My Projects";
/**
* Header text for admin page
* @constant
@ -727,6 +734,8 @@ export const OUTPUT_TYPES = new Set([
"JsonOutput",
"KeyPairOutput",
"StringListOutput",
"RecordsOutput",
"TableOutput",
]);
export const CHAT_FIRST_INITIAL_TEXT =
@ -746,8 +755,8 @@ export const EDIT_TEXT_MODAL_TITLE = "Edit Text";
export const EDIT_TEXT_PLACEHOLDER = "Type message here.";
export const INPUT_HANDLER_HOVER = "Avaliable input components:";
export const OUTPUT_HANDLER_HOVER = "Avaliable output components:";
export const TEXT_INPUT_MODAL_TITLE = "Text Inputs";
export const OUTPUTS_MODAL_TITLE = "Text Outputs";
export const TEXT_INPUT_MODAL_TITLE = "Inputs";
export const OUTPUTS_MODAL_TITLE = "Outputs";
export const LANGFLOW_CHAT_TITLE = "Langflow Chat";
export const CHAT_INPUT_PLACEHOLDER =
"No chat input variables found. Click to run your flow.";
@ -888,3 +897,6 @@ export const unavailableShortcutss = [
"CTRL + J",
"CTRL + U",
];
export const DEFAULT_TABLE_ALERT_MSG = `Oops! It seems there's no data to display right now. Please check back later.`;
export const DEFAULT_TABLE_ALERT_TITLE = "No Data Available";

View file

@ -22,7 +22,10 @@ function ApiInterceptor() {
const interceptor = api.interceptors.response.use(
(response) => response,
async (error: AxiosError) => {
if (error.response?.status === 403 || error.response?.status === 401) {
if (
error?.response?.status === 403 ||
error?.response?.status === 401
) {
if (!autoLogin) {
if (error?.config?.url?.includes("github")) {
return Promise.reject(error);
@ -44,7 +47,7 @@ function ApiInterceptor() {
}
await clearBuildVerticesState(error);
return Promise.reject(error);
}
},
);
const isAuthorizedURL = (url) => {
@ -61,10 +64,10 @@ function ApiInterceptor() {
const parsedURL = new URL(url);
const isDomainAllowed = authorizedDomains.some(
(domain) => parsedURL.origin === new URL(domain).origin
(domain) => parsedURL.origin === new URL(domain).origin,
);
const isEndpointAllowed = authorizedEndpoints.some((endpoint) =>
parsedURL.pathname.includes(endpoint)
parsedURL.pathname.includes(endpoint),
);
return isDomainAllowed || isEndpointAllowed;
@ -77,6 +80,18 @@ function ApiInterceptor() {
// Request interceptor to add access token to every request
const requestInterceptor = api.interceptors.request.use(
(config) => {
const lastUrl = localStorage.getItem("lastUrlCalled");
if (
config?.url === lastUrl &&
config?.url !== "/health" &&
config?.method === "get"
) {
return Promise.reject("Duplicate request");
}
localStorage.setItem("lastUrlCalled", config.url ?? "");
const accessToken = cookies.get("access_token_lf");
if (accessToken && !isAuthorizedURL(config?.url)) {
config.headers["Authorization"] = `Bearer ${accessToken}`;
@ -86,7 +101,7 @@ function ApiInterceptor() {
},
(error) => {
return Promise.reject(error);
}
},
);
return () => {
@ -118,7 +133,7 @@ function ApiInterceptor() {
if (error?.config?.headers) {
delete error.config.headers["Authorization"];
error.config.headers["Authorization"] = `Bearer ${cookies.get(
"access_token_lf"
"access_token_lf",
)}`;
const response = await axios.request(error.config);
return response;

View file

@ -1,5 +1,6 @@
import { ColDef, ColGroupDef } from "ag-grid-community";
import { AxiosRequestConfig, AxiosResponse } from "axios";
import { Edge, ReactFlowJsonObject,Node } from "reactflow";
import { Edge, Node, ReactFlowJsonObject } from "reactflow";
import { BASE_URL_API } from "../../constants/constants";
import { api } from "../../controllers/API/api";
import {
@ -18,6 +19,7 @@ import { UserInputType } from "../../types/components";
import { FlowStyleType, FlowType } from "../../types/flow";
import { StoreComponentResponse } from "../../types/store";
import { FlowPoolType } from "../../types/zustand/flow";
import { extractColumnsFromRows } from "../../utils/utils";
import {
APIClassType,
BuildStatusTypeAPI,
@ -59,7 +61,7 @@ export async function sendAll(data: sendAllProps) {
}
export async function postValidateCode(
code: string
code: string,
): Promise<AxiosResponse<errorsTypeAPI>> {
return await api.post(`${BASE_URL_API}validate/code`, { code });
}
@ -74,7 +76,7 @@ export async function postValidateCode(
export async function postValidatePrompt(
name: string,
template: string,
frontend_node: APIClassType
frontend_node: APIClassType,
): Promise<AxiosResponse<PromptTypeAPI>> {
return api.post(`${BASE_URL_API}validate/prompt`, {
name,
@ -119,6 +121,7 @@ export async function saveFlowToDatabase(newFlow: {
description: string;
style?: FlowStyleType;
is_component?: boolean;
folder_id?: string;
}): Promise<FlowType> {
try {
const response = await api.post(`${BASE_URL_API}flows/`, {
@ -126,6 +129,7 @@ export async function saveFlowToDatabase(newFlow: {
data: newFlow.data,
description: newFlow.description,
is_component: newFlow.is_component,
folder_id: newFlow.folder_id === "" ? null : newFlow.folder_id,
});
if (response.status !== 201) {
@ -145,13 +149,14 @@ export async function saveFlowToDatabase(newFlow: {
* @throws Will throw an error if the update fails.
*/
export async function updateFlowInDatabase(
updatedFlow: FlowType
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,
});
if (response?.status !== 200) {
@ -321,7 +326,7 @@ export async function getHealth() {
*
*/
export async function getBuildStatus(
flowId: string
flowId: string,
): Promise<AxiosResponse<BuildStatusTypeAPI>> {
return await api.get(`${BASE_URL_API}build/${flowId}/status`);
}
@ -334,7 +339,7 @@ export async function getBuildStatus(
*
*/
export async function postBuildInit(
flow: FlowType
flow: FlowType,
): Promise<AxiosResponse<InitTypeAPI>> {
return await api.post(`${BASE_URL_API}build/init/${flow.id}`, flow);
}
@ -350,7 +355,7 @@ export async function postBuildInit(
*/
export async function uploadFile(
file: File,
id: string
id: string,
): Promise<AxiosResponse<UploadFileTypeAPI>> {
const formData = new FormData();
formData.append("file", file);
@ -359,7 +364,7 @@ export async function uploadFile(
export async function postCustomComponent(
code: string,
apiClass: APIClassType
apiClass: APIClassType,
): Promise<AxiosResponse<APIClassType>> {
// let template = apiClass.template;
return await api.post(`${BASE_URL_API}custom_component`, {
@ -372,7 +377,7 @@ export async function postCustomComponentUpdate(
code: string,
template: APITemplateType,
field: string,
field_value: any
field_value: any,
): Promise<AxiosResponse<APIClassType>> {
return await api.post(`${BASE_URL_API}custom_component/update`, {
code,
@ -394,7 +399,7 @@ export async function onLogin(user: LoginType) {
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
}
},
);
if (response.status === 200) {
@ -456,11 +461,11 @@ export async function addUser(user: UserInputType): Promise<Array<Users>> {
export async function getUsersPage(
skip: number,
limit: number
limit: number,
): Promise<Array<Users>> {
try {
const res = await api.get(
`${BASE_URL_API}users/?skip=${skip}&limit=${limit}`
`${BASE_URL_API}users/?skip=${skip}&limit=${limit}`,
);
if (res.status === 200) {
return res.data;
@ -497,7 +502,7 @@ export async function resetPassword(user_id: string, user: resetPasswordType) {
try {
const res = await api.patch(
`${BASE_URL_API}users/${user_id}/reset-password`,
user
user,
);
if (res.status === 200) {
return res.data;
@ -571,7 +576,7 @@ export async function saveFlowStore(
last_tested_version?: string;
},
tags: string[],
publicFlow = false
publicFlow = false,
): Promise<FlowType> {
try {
const response = await api.post(`${BASE_URL_API}store/components/`, {
@ -700,7 +705,7 @@ export async function postStoreComponents(component: Component) {
export async function getComponent(component_id: string) {
try {
const res = await api.get(
`${BASE_URL_API}store/components/${component_id}`
`${BASE_URL_API}store/components/${component_id}`,
);
if (res.status === 200) {
return res.data;
@ -715,7 +720,7 @@ export async function searchComponent(
page?: number | null,
limit?: number | null,
status?: string | null,
tags?: string[]
tags?: string[],
): Promise<StoreComponentResponse | undefined> {
try {
let url = `${BASE_URL_API}store/components/`;
@ -827,7 +832,7 @@ export async function updateFlowStore(
},
tags: string[],
publicFlow = false,
id: string
id: string,
): Promise<FlowType> {
try {
const response = await api.patch(`${BASE_URL_API}store/components/${id}`, {
@ -911,7 +916,7 @@ export async function deleteGlobalVariable(id: string) {
export async function updateGlobalVariable(
name: string,
value: string,
id: string
id: string,
) {
try {
const response = api.patch(`${BASE_URL_API}variables/${id}`, {
@ -929,36 +934,40 @@ export async function getVerticesOrder(
flowId: string,
startNodeId?: string | null,
stopNodeId?: string | null,
nodes?:Node[],
Edges?:Edge[]
nodes?: Node[],
Edges?: Edge[],
): Promise<AxiosResponse<VerticesOrderTypeAPI>> {
// nodeId is optional and is a query parameter
// if nodeId is not provided, the API will return all vertices
const config:AxiosRequestConfig<any> = {};
const config: AxiosRequestConfig<any> = {};
if (stopNodeId) {
config["params"] = { stop_component_id: stopNodeId };
} else if (startNodeId) {
config["params"] = { start_component_id: startNodeId };
}
const data = {
data:{}
data: {},
};
if (nodes && Edges) {
data["data"]["nodes"] = nodes;
data["data"]["edges"] = Edges;
}
if(nodes && Edges){
data["data"]["nodes"] = nodes
data["data"]["edges"] = Edges
}
return await api.post(`${BASE_URL_API}build/${flowId}/vertices`,data, config);
return await api.post(
`${BASE_URL_API}build/${flowId}/vertices`,
data,
config,
);
}
export async function postBuildVertex(
flowId: string,
vertexId: string,
input_value: string
input_value: string,
): Promise<AxiosResponse<VertexBuildTypeAPI>> {
// input_value is optional and is a query parameter
return await api.post(
`${BASE_URL_API}build/${flowId}/vertices/${vertexId}`,
input_value ? { inputs: { input_value: input_value } } : undefined
input_value ? { inputs: { input_value: input_value } } : undefined,
);
}
@ -982,9 +991,47 @@ export async function getFlowPool({
}
export async function deleteFlowPool(
flowId: string
flowId: string,
): Promise<AxiosResponse<any>> {
const config = {};
config["params"] = { flow_id: flowId };
return await api.delete(`${BASE_URL_API}monitor/builds`, config);
}
export async function multipleDeleteFlowsComponents(
flowIds: string[],
): Promise<AxiosResponse<any>> {
return await api.post(`${BASE_URL_API}flows/multiple_delete/`, {
flow_ids: flowIds,
});
}
export async function getTransactionTable(
id: string,
mode: "intersection" | "union",
params = {},
): Promise<{ rows: Array<object>; columns: Array<ColDef | ColGroupDef> }> {
const config = {};
config["params"] = { flow_id: id };
if (params) {
config["params"] = { ...config["params"], ...params };
}
const rows = await api.get(`${BASE_URL_API}monitor/transactions`, config);
const columns = extractColumnsFromRows(rows.data, mode);
return { rows: rows.data, columns };
}
export async function getMessagesTable(
id: string,
mode: "intersection" | "union",
params = {},
): Promise<{ rows: Array<object>; columns: Array<ColDef | ColGroupDef> }> {
const config = {};
config["params"] = { flow_id: id };
if (params) {
config["params"] = { ...config["params"], ...params };
}
const rows = await api.get(`${BASE_URL_API}monitor/messages`, config);
const columns = extractColumnsFromRows(rows.data, mode);
return { rows: rows.data, columns };
}

View file

@ -1,5 +1,5 @@
import { cloneDeep } from "lodash";
import React, { ReactNode, useEffect, useRef, useState } from "react";
import { ReactNode, useEffect, useRef, useState } from "react";
import { Handle, Position, useUpdateNodeInternals } from "reactflow";
import CodeAreaComponent from "../../../../components/codeAreaComponent";
import DictComponent from "../../../../components/dictComponent";
@ -18,20 +18,15 @@ import ToggleShadComponent from "../../../../components/toggleShadComponent";
import { Button } from "../../../../components/ui/button";
import { RefreshButton } from "../../../../components/ui/refreshButton";
import {
INPUT_HANDLER_HOVER,
LANGFLOW_SUPPORTED_TYPES,
OUTPUT_HANDLER_HOVER,
TOOLTIP_EMPTY,
} from "../../../../constants/constants";
import { Case } from "../../../../shared/components/caseComponent";
import useAlertStore from "../../../../stores/alertStore";
import useFlowStore from "../../../../stores/flowStore";
import useFlowsManagerStore from "../../../../stores/flowsManagerStore";
import { useTypesStore } from "../../../../stores/typesStore";
import {
APIClassType,
ResponseErrorDetailAPI,
ResponseErrorTypeAPI,
} from "../../../../types/api";
import { APIClassType } from "../../../../types/api";
import { ParameterComponentType } from "../../../../types/components";
import {
debouncedHandleUpdateValues,
@ -44,12 +39,13 @@ import {
isValidConnection,
scapedJSONStringfy,
} from "../../../../utils/reactflowUtils";
import {
nodeColors,
nodeIconsLucide,
nodeNames,
} from "../../../../utils/styleUtils";
import { nodeColors } from "../../../../utils/styleUtils";
import { classNames, groupByFamily } from "../../../../utils/utils";
import useFetchDataOnMount from "../../../hooks/use-fetch-data-on-mount";
import useHandleOnNewValue from "../../../hooks/use-handle-new-value";
import useHandleNodeClass from "../../../hooks/use-handle-node-class";
import useHandleRefreshButtonPress from "../../../hooks/use-handle-refresh-buttons";
import TooltipRenderComponent from "../tooltipRenderComponent";
export default function ParameterComponent({
left,
@ -75,159 +71,69 @@ export default function ParameterComponent({
const nodes = useFlowStore((state) => state.nodes);
const edges = useFlowStore((state) => state.edges);
const setNode = useFlowStore((state) => state.setNode);
const myData = useTypesStore((state) => state.data);
const takeSnapshot = useFlowsManagerStore((state) => state.takeSnapshot);
const [isLoading, setIsLoading] = useState(false);
const updateNodeInternals = useUpdateNodeInternals();
const [errorDuplicateKey, setErrorDuplicateKey] = useState(false);
const flow = currentFlow?.data?.nodes ?? null;
const groupedEdge = useRef(null);
const setFilterEdge = useFlowStore((state) => state.setFilterEdge);
const { handleOnNewValue: handleOnNewValueHook } = useHandleOnNewValue(
data,
name,
takeSnapshot,
handleUpdateValues,
debouncedHandleUpdateValues,
setNode,
renderTooltips,
isLoading,
setIsLoading,
);
const { handleNodeClass: handleNodeClassHook } = useHandleNodeClass(
data,
name,
takeSnapshot,
setNode,
updateNodeInternals,
renderTooltips,
);
const { handleRefreshButtonPress: handleRefreshButtonPressHook } =
useHandleRefreshButtonPress(setIsLoading, setNode, renderTooltips);
let disabled =
edges.some(
(edge) =>
edge.targetHandle === scapedJSONStringfy(proxy ? { ...id, proxy } : id),
) ?? false;
const myData = useTypesStore((state) => state.data);
const takeSnapshot = useFlowsManagerStore((state) => state.takeSnapshot);
const handleRefreshButtonPress = async (name, data) => {
setIsLoading(true);
try {
let newTemplate = await handleUpdateValues(name, data);
if (newTemplate) {
setNode(data.id, (oldNode) => {
let newNode = cloneDeep(oldNode);
newNode.data = {
...newNode.data,
};
newNode.data.node.template = newTemplate;
return newNode;
});
}
} catch (error) {
let responseError = error as ResponseErrorDetailAPI;
setErrorData({
title: "Error while updating the Component",
list: [responseError.response.data.detail ?? "Unknown error"],
});
}
setIsLoading(false);
renderTooltips();
handleRefreshButtonPressHook(name, data);
};
useEffect(() => {
async function fetchData() {
if (
(data.node?.template[name]?.real_time_refresh ||
data.node?.template[name]?.refresh_button) &&
// options can be undefined but not an empty array
(data.node?.template[name]?.options?.length ?? 0) === 0
) {
setIsLoading(true);
try {
let newTemplate = await handleUpdateValues(name, data);
if (newTemplate) {
setNode(data.id, (oldNode) => {
let newNode = cloneDeep(oldNode);
newNode.data = {
...newNode.data,
};
newNode.data.node.template = newTemplate;
return newNode;
});
}
} catch (error) {
let responseError = error as ResponseErrorDetailAPI;
useFetchDataOnMount(
data,
name,
handleUpdateValues,
setNode,
renderTooltips,
setIsLoading,
);
setErrorData({
title: "Error while updating the Component",
list: [responseError.response.data.detail ?? "Unknown error"],
});
}
setIsLoading(false);
renderTooltips();
}
}
fetchData();
}, []);
const handleOnNewValue = async (
newValue: string | string[] | boolean | Object[],
skipSnapshot: boolean | undefined = false,
): Promise<void> => {
if (data.node!.template[name].value !== newValue) {
takeSnapshot();
}
const shouldUpdate =
data.node?.template[name].real_time_refresh &&
!data.node?.template[name].refresh_button &&
data.node!.template[name].value !== newValue;
data.node!.template[name].value = newValue; // necessary to enable ctrl+z inside the input
let newTemplate;
if (shouldUpdate) {
setIsLoading(true);
try {
newTemplate = await debouncedHandleUpdateValues(name, data);
} catch (error) {
let responseError = error as ResponseErrorTypeAPI;
setErrorData({
title: "Error while updating the Component",
list: [responseError.response.data.detail.error ?? "Unknown error"],
});
}
setIsLoading(false);
// this de
}
setNode(data.id, (oldNode) => {
let newNode = cloneDeep(oldNode);
newNode.data = {
...newNode.data,
};
if (data.node?.template[name].real_time_refresh && newTemplate) {
newNode.data.node.template = newTemplate;
} else newNode.data.node.template[name].value = newValue;
return newNode;
});
renderTooltips();
handleOnNewValueHook(newValue, skipSnapshot);
};
const updateNodeInternals = useUpdateNodeInternals();
const handleNodeClass = (newNodeClass: APIClassType, code?: string): void => {
if (!data.node) return;
if (data.node!.template[name].value !== code) {
takeSnapshot();
}
setNode(data.id, (oldNode) => {
let newNode = cloneDeep(oldNode);
newNode.data = {
...newNode.data,
node: newNodeClass,
description: newNodeClass.description ?? data.node!.description,
display_name: newNodeClass.display_name ?? data.node!.display_name,
};
newNode.data.node.template[name].value = code;
return newNode;
});
updateNodeInternals(data.id);
renderTooltips();
handleNodeClassHook(newNodeClass, code);
};
const [errorDuplicateKey, setErrorDuplicateKey] = useState(false);
useEffect(() => {
// @ts-ignore
infoHtml.current = (
@ -248,88 +154,7 @@ export default function ParameterComponent({
if (groupedObj && groupedObj.length > 0) {
//@ts-ignore
refHtml.current = groupedObj.map((item, index) => {
const Icon: any =
nodeIconsLucide[item.family] ?? nodeIconsLucide["unknown"];
return (
<div
key={index}
data-testid={`available-${left ? "input" : "output"}-${
item.family
}`}
>
{index === 0 && (
<span>{left ? INPUT_HANDLER_HOVER : OUTPUT_HANDLER_HOVER}</span>
)}
<span
key={index}
className={classNames(
index > 0 ? "mt-2 flex items-center" : "mt-3 flex items-center",
)}
>
<div
className="h-5 w-5"
style={{
color: nodeColors[item.family],
}}
>
<Icon
className="h-5 w-5"
strokeWidth={1.5}
style={{
color: nodeColors[item.family] ?? nodeColors.unknown,
}}
/>
</div>
<span
className="ps-2 text-xs text-foreground"
data-testid={`tooltip-${nodeNames[item.family] ?? "Other"}`}
>
{nodeNames[item.family] ?? "Other"}{" "}
{item?.display_name && item?.display_name?.length > 0 ? (
<span
className="text-xs"
data-testid={`tooltip-${item?.display_name}`}
>
{" "}
{item.display_name === "" ? "" : " - "}
{item.display_name.split(", ").length > 2
? item.display_name.split(", ").map((el, index) => (
<React.Fragment key={el + name}>
<span>
{index ===
item.display_name.split(", ").length - 1
? el
: (el += `, `)}
</span>
</React.Fragment>
))
: item.display_name}
</span>
) : (
<span
className="text-xs"
data-testid={`tooltip-${item?.type}`}
>
{" "}
{item.type === "" ? "" : " - "}
{item.type.split(", ").length > 2
? item.type.split(", ").map((el, index) => (
<React.Fragment key={el + name}>
<span>
{index === item.type.split(", ").length - 1
? el
: (el += `, `)}
</span>
</React.Fragment>
))
: item.type}
</span>
)}
</span>
</span>
</div>
);
return <TooltipRenderComponent index={index} item={item} left={left} />;
});
} else {
//@ts-ignore
@ -338,6 +163,7 @@ export default function ParameterComponent({
);
}
}
// If optionalHandle is an empty list, then it is not an optional handle
if (optionalHandle && optionalHandle.length === 0) {
optionalHandle = null;
@ -346,6 +172,9 @@ export default function ParameterComponent({
useEffect(() => {
renderTooltips();
}, [tooltipTitle, flow]);
console.log(left === true && type === "dict");
return !showNode ? (
left && LANGFLOW_SUPPORTED_TYPES.has(type ?? "") && !optionalHandle ? (
<></>
@ -411,11 +240,12 @@ export default function ParameterComponent({
(left ? "" : " justify-end")
}
>
{!left && data.node?.frozen && (
<Case condition={left && data.node?.frozen}>
<div className="pr-1">
<IconComponent className="h-5 w-5 text-ice" name={"Snowflake"} />
</div>
)}
</Case>
{proxy ? (
<ShadTooltip content={<span>{proxy.id}</span>}>
<span className={!left && data.node?.frozen ? " text-ice" : ""}>
@ -461,45 +291,37 @@ export default function ParameterComponent({
}`}
type={left ? "target" : "source"}
position={left ? Position.Left : Position.Right}
key={
proxy
? scapedJSONStringfy({ ...id, proxy })
: scapedJSONStringfy(id)
}
id={
proxy
? scapedJSONStringfy({ ...id, proxy })
: scapedJSONStringfy(id)
}
key={scapedJSONStringfy(proxy ? { ...id, proxy } : id)}
id={scapedJSONStringfy(proxy ? { ...id, proxy } : id)}
isValidConnection={(connection) =>
isValidConnection(connection, nodes, edges)
}
className={classNames(
left ? "-ml-0.5 " : "-mr-0.5 ",
left ? "-ml-0.5" : "-mr-0.5",
"h-3 w-3 rounded-full border-2 bg-background",
)}
style={{
borderColor: color ?? nodeColors.unknown,
}}
onClick={() => {
setFilterEdge(groupedEdge.current);
}}
></Handle>
style={{ borderColor: color ?? nodeColors.unknown }}
onClick={() => setFilterEdge(groupedEdge.current)}
/>
</ShadTooltip>
</div>
</Button>
)}
{left === true &&
type === "str" &&
!data.node?.template[name].options ? (
<Case
condition={
left === true &&
type === "str" &&
!data.node?.template[name]?.options
}
>
<div className="w-full">
{data.node?.template[name].list ? (
<Case condition={data.node?.template[name]?.list}>
<div
className={
// Commenting this out until we have a better
// way to display
// (data.node?.template[name].refresh ? "w-5/6 " : "") +
// (data.node?.template[name]?.refresh ? "w-5/6 " : "") +
"flex-grow"
}
>
@ -507,39 +329,27 @@ export default function ParameterComponent({
componentName={name}
disabled={disabled}
value={
!data.node.template[name].value ||
data.node.template[name].value === ""
!data.node!.template[name]?.value ||
data.node!.template[name]?.value === ""
? [""]
: data.node.template[name].value
: data.node!.template[name]?.value
}
onChange={handleOnNewValue}
/>
{/* {data.node?.template[name].refresh_button && (
<div className="w-1/6">
<RefreshButton
isLoading={isLoading}
disabled={disabled}
name={name}
data={data}
className="extra-side-bar-buttons ml-2 mt-1"
handleUpdateValues={handleRefreshButtonPress}
id={"refresh-button-" + name}
/>
</div>
)} */}
</div>
) : data.node?.template[name].multiline ? (
</Case>
<Case condition={data.node?.template[name]?.multiline}>
<div className="mt-2 flex w-full flex-col ">
<div className="flex-grow">
<TextAreaComponent
disabled={disabled}
value={data.node.template[name].value ?? ""}
value={data.node!.template[name]?.value ?? ""}
onChange={handleOnNewValue}
id={"textarea-" + data.node.template[name].name}
data-testid={"textarea-" + data.node.template[name].name}
id={"textarea-" + data.node!.template[name]?.name}
data-testid={"textarea-" + data.node!.template[name]?.name}
/>
</div>
{data.node?.template[name].refresh_button && (
{data.node?.template[name]?.refresh_button && (
<div className="flex-grow">
<RefreshButton
isLoading={isLoading}
@ -547,7 +357,7 @@ export default function ParameterComponent({
name={name}
data={data}
button_text={
data.node?.template[name].refresh_button_text ??
data.node?.template[name]?.refresh_button_text ??
"Refresh"
}
className="extra-side-bar-buttons mt-1"
@ -557,12 +367,18 @@ export default function ParameterComponent({
</div>
)}
</div>
) : (
</Case>
<Case
condition={
!data.node?.template[name]?.multiline &&
!data.node?.template[name]?.list
}
>
<div className="mt-2 flex w-full items-center">
<div
className={
"flex-grow " +
(data.node?.template[name].refresh_button ? "w-5/6" : "")
(data.node?.template[name]?.refresh_button ? "w-5/6" : "")
}
>
<InputGlobalComponent
@ -582,7 +398,7 @@ export default function ParameterComponent({
data={data}
/>
</div>
{data.node?.template[name].refresh_button && (
{data.node?.template[name]?.refresh_button && (
<div className="w-1/6">
<RefreshButton
isLoading={isLoading}
@ -590,7 +406,7 @@ export default function ParameterComponent({
name={name}
data={data}
button_text={
data.node?.template[name].refresh_button_text ??
data.node?.template[name]?.refresh_button_text ??
"Refresh"
}
className="extra-side-bar-buttons ml-2 mt-1"
@ -600,52 +416,61 @@ export default function ParameterComponent({
</div>
)}
</div>
)}
</Case>
</div>
) : left === true && type === "bool" ? (
</Case>
<Case condition={left === true && type === "bool"}>
<div className="mt-2 w-full">
<ToggleShadComponent
id={"toggle-" + name}
disabled={disabled}
enabled={data.node?.template[name].value ?? false}
enabled={data.node?.template[name]?.value ?? false}
setEnabled={handleOnNewValue}
size="large"
editNode={false}
/>
</div>
) : left === true && type === "float" ? (
</Case>
<Case condition={left === true && type === "float"}>
<div className="mt-2 w-full">
<FloatComponent
disabled={disabled}
value={data.node?.template[name].value ?? ""}
value={data.node?.template[name]?.value ?? ""}
rangeSpec={data.node?.template[name]?.rangeSpec}
onChange={handleOnNewValue}
/>
</div>
) : left === true &&
type === "str" &&
(data.node?.template[name].options ||
data.node?.template[name]?.real_time_refresh) ? (
// TODO: Improve CSS
</Case>
<Case
condition={
left === true &&
type === "str" &&
(data.node?.template[name]?.options ||
data.node?.template[name]?.real_time_refresh)
}
>
<div className="mt-2 flex w-full items-center">
<div className="w-5/6 flex-grow">
<Dropdown
disabled={disabled}
isLoading={isLoading}
options={data.node.template[name].options}
options={data.node!.template[name]?.options}
onSelect={handleOnNewValue}
value={data.node.template[name].value}
value={data.node!.template[name]?.value}
id={"dropdown-" + name}
/>
</div>
{data.node?.template[name].refresh_button && (
{data.node?.template[name]?.refresh_button && (
<div className="w-1/6">
<RefreshButton
isLoading={isLoading}
disabled={disabled}
name={name}
data={data}
button_text={data.node?.template[name].refresh_button_text}
button_text={data.node?.template[name]?.refresh_button_text}
className="extra-side-bar-buttons ml-2 mt-1"
handleUpdateValues={handleRefreshButtonPress}
id={"refresh-button-" + name}
@ -653,46 +478,54 @@ export default function ParameterComponent({
</div>
)}
</div>
) : left === true && type === "code" ? (
</Case>
<Case condition={left === true && type === "code"}>
<div className="mt-2 w-full">
<CodeAreaComponent
readonly={
data.node?.flow && data.node.template[name].dynamic
data.node?.flow && data.node.template[name]?.dynamic
? true
: false
}
dynamic={data.node?.template[name].dynamic ?? false}
dynamic={data.node?.template[name]?.dynamic ?? false}
setNodeClass={handleNodeClass}
nodeClass={data.node}
disabled={disabled}
value={data.node?.template[name].value ?? ""}
value={data.node?.template[name]?.value ?? ""}
onChange={handleOnNewValue}
id={"code-input-" + name}
/>
</div>
) : left === true && type === "file" ? (
</Case>
<Case condition={left === true && type === "file"}>
<div className="mt-2 w-full">
<InputFileComponent
disabled={disabled}
value={data.node?.template[name].value ?? ""}
value={data.node?.template[name]?.value ?? ""}
onChange={handleOnNewValue}
fileTypes={data.node?.template[name].fileTypes}
fileTypes={data.node?.template[name]?.fileTypes}
onFileChange={(filePath: string) => {
data.node!.template[name].file_path = filePath;
}}
></InputFileComponent>
</div>
) : left === true && type === "int" ? (
</Case>
<Case condition={left === true && type === "int"}>
<div className="mt-2 w-full">
<IntComponent
rangeSpec={data.node?.template[name].rangeSpec}
rangeSpec={data.node?.template[name]?.rangeSpec}
disabled={disabled}
value={data.node?.template[name].value ?? ""}
value={data.node?.template[name]?.value ?? ""}
onChange={handleOnNewValue}
id={"int-input-" + name}
/>
</div>
) : left === true && type === "prompt" ? (
</Case>
<Case condition={left === true && type === "prompt"}>
<div className="mt-2 w-full">
<PromptAreaComponent
readonly={data.node?.flow ? true : false}
@ -700,57 +533,59 @@ export default function ParameterComponent({
setNodeClass={handleNodeClass}
nodeClass={data.node}
disabled={disabled}
value={data.node?.template[name].value ?? ""}
value={data.node?.template[name]?.value ?? ""}
onChange={handleOnNewValue}
id={"prompt-input-" + name}
data-testid={"prompt-input-" + name}
/>
</div>
) : left === true && type === "NestedDict" ? (
</Case>
<Case condition={left === true && type === "NestedDict"}>
<div className="mt-2 w-full">
<DictComponent
disabled={disabled}
editNode={false}
value={
!data.node!.template[name].value ||
data.node!.template[name].value?.toString() === "{}"
!data.node!.template[name]?.value ||
data.node!.template[name]?.value?.toString() === "{}"
? {
// yourkey: "value",
}
: data.node!.template[name].value
: data.node!.template[name]?.value
}
onChange={handleOnNewValue}
id="div-dict-input"
/>
</div>
) : left === true && type === "dict" ? (
</Case>
<Case condition={left === true && type === "dict"}>
<div className="mt-2 w-full">
<KeypairListComponent
disabled={disabled}
editNode={false}
value={
data.node!.template[name].value?.length === 0 ||
!data.node!.template[name].value
data.node!.template[name]?.value?.length === 0 ||
!data.node!.template[name]?.value
? [{ "": "" }]
: convertObjToArray(data.node!.template[name].value)
: convertObjToArray(data.node!.template[name]?.value, type!)
}
duplicateKey={errorDuplicateKey}
onChange={(newValue) => {
const valueToNumbers = convertValuesToNumbers(newValue);
setErrorDuplicateKey(hasDuplicateKeys(valueToNumbers));
// if data.node?.template[name].list is true, then the value is an array of objects
// if data.node?.template[name]?.list is true, then the value is an array of objects
// else we need to get the first object of the array
if (data.node?.template[name].list) {
if (data.node?.template[name]?.list) {
handleOnNewValue(valueToNumbers);
} else handleOnNewValue(valueToNumbers[0]);
}}
isList={data.node?.template[name].list ?? false}
isList={data.node?.template[name]?.list ?? false}
/>
</div>
) : (
<></>
)}
</Case>
</>
</div>
);

View file

@ -0,0 +1,91 @@
import React from "react";
import {
INPUT_HANDLER_HOVER,
OUTPUT_HANDLER_HOVER,
} from "../../../../constants/constants";
import {
nodeColors,
nodeIconsLucide,
nodeNames,
} from "../../../../utils/styleUtils";
import { classNames } from "../../../../utils/utils";
const TooltipRenderComponent = ({ item, index, left }) => {
const Icon = nodeIconsLucide[item.family] ?? nodeIconsLucide["unknown"];
return (
<div
key={index}
data-testid={`available-${left ? "input" : "output"}-${item.family}`}
>
{index === 0 && (
<span>{left ? INPUT_HANDLER_HOVER : OUTPUT_HANDLER_HOVER}</span>
)}
<span
key={index}
className={classNames(
index > 0 ? "mt-2 flex items-center" : "mt-3 flex items-center",
)}
>
<div
className="h-5 w-5"
style={{
color: nodeColors[item.family],
}}
>
<Icon
className="h-5 w-5"
strokeWidth={1.5}
style={{
color: nodeColors[item.family] ?? nodeColors.unknown,
}}
/>
</div>
<span
className="ps-2 text-xs text-foreground"
data-testid={`tooltip-${nodeNames[item.family] ?? "Other"}`}
>
{nodeNames[item.family] ?? "Other"}{" "}
{item?.display_name && item?.display_name?.length > 0 ? (
<span
className="text-xs"
data-testid={`tooltip-${item?.display_name}`}
>
{" "}
{item.display_name === "" ? "" : " - "}
{item.display_name.split(", ").length > 2
? item.display_name.split(", ").map((el, index) => (
<React.Fragment key={el + name}>
<span>
{index === item.display_name.split(", ").length - 1
? el
: (el += `, `)}
</span>
</React.Fragment>
))
: item.display_name}
</span>
) : (
<span className="text-xs" data-testid={`tooltip-${item?.type}`}>
{" "}
{item.type === "" ? "" : " - "}
{item.type.split(", ").length > 2
? item.type.split(", ").map((el, index) => (
<React.Fragment key={el + name}>
<span>
{index === item.type.split(", ").length - 1
? el
: (el += `, `)}
</span>
</React.Fragment>
))
: item.type}
</span>
)}
</span>
</span>
</div>
);
};
export default TooltipRenderComponent;

View file

@ -60,14 +60,14 @@ export default function GenericNode({
const [nodeName, setNodeName] = useState(data.node!.display_name);
const [inputDescription, setInputDescription] = useState(false);
const [nodeDescription, setNodeDescription] = useState(
data.node?.description!
data.node?.description!,
);
const [isOutdated, setIsOutdated] = useState(false);
const buildStatus = useFlowStore(
(state) => state.flowBuildStatus[data.id]?.status
(state) => state.flowBuildStatus[data.id]?.status,
);
const lastRunTime = useFlowStore(
(state) => state.flowBuildStatus[data.id]?.timestamp
(state) => state.flowBuildStatus[data.id]?.timestamp,
);
const [validationStatus, setValidationStatus] =
useState<validationStatusType | null>(null);
@ -124,7 +124,7 @@ export default function GenericNode({
updateNodeInternals(data.id);
},
[data.id, data.node, setNode, setIsOutdated]
[data.id, data.node, setNode, setIsOutdated],
);
if (!data.node!.template) {
@ -264,7 +264,7 @@ export default function GenericNode({
const isDark = useDarkStore((state) => state.dark);
const renderIconStatus = (
buildStatus: BuildStatus | undefined,
validationStatus: validationStatusType | null
validationStatus: validationStatusType | null,
) => {
if (buildStatus === BuildStatus.BUILDING) {
return <Loading className="text-medium-indigo" />;
@ -305,11 +305,11 @@ export default function GenericNode({
};
const getSpecificClassFromBuildStatus = (
buildStatus: BuildStatus | undefined,
validationStatus: validationStatusType | null
validationStatus: validationStatusType | null,
) => {
let isInvalid = validationStatus && !validationStatus.valid;
if (buildStatus === BuildStatus.INACTIVE && isInvalid) {
if (buildStatus === BuildStatus.INACTIVE) {
// INACTIVE should have its own class
return "inactive-status";
}
@ -329,19 +329,20 @@ export default function GenericNode({
selected: boolean,
showNode: boolean,
buildStatus: BuildStatus | undefined,
validationStatus: validationStatusType | null
validationStatus: validationStatusType | null,
) => {
const specificClassFromBuildStatus = getSpecificClassFromBuildStatus(
buildStatus,
validationStatus
validationStatus,
);
const baseBorderClass = getBaseBorderClass(selected);
const nodeSizeClass = getNodeSizeClass(showNode);
return classNames(
baseBorderClass,
nodeSizeClass,
"generic-node-div",
specificClassFromBuildStatus
specificClassFromBuildStatus,
);
};
@ -395,7 +396,6 @@ export default function GenericNode({
openWDoubleCLick,
setOpenWDoubleCLick,
]);
return (
<>
{memoizedNodeToolbarComponent}
@ -408,7 +408,7 @@ export default function GenericNode({
selected,
showNode,
buildStatus,
validationStatus
validationStatus,
)}
>
{data.node?.beta && showNode && (
@ -553,7 +553,7 @@ export default function GenericNode({
}
title={getFieldTitle(
data.node?.template!,
templateField
templateField,
)}
info={data.node?.template[templateField].info}
name={templateField}
@ -581,7 +581,7 @@ export default function GenericNode({
proxy={data.node?.template[templateField].proxy}
showNode={showNode}
/>
)
),
)}
<ParameterComponent
key={scapedJSONStringfy({
@ -738,7 +738,7 @@ export default function GenericNode({
!data.node?.description) &&
nameEditable
? "font-light italic"
: ""
: "",
)}
onDoubleClick={(e) => {
setInputDescription(true);
@ -800,13 +800,13 @@ export default function GenericNode({
}
title={getFieldTitle(
data.node?.template!,
templateField
templateField,
)}
info={data.node?.template[templateField].info}
name={templateField}
tooltipTitle={
data.node?.template[templateField].input_types?.join(
"\n"
"\n",
) ?? data.node?.template[templateField].type
}
required={data.node!.template[templateField].required}
@ -833,7 +833,7 @@ export default function GenericNode({
<div
className={classNames(
Object.keys(data.node!.template).length < 1 ? "hidden" : "",
"flex-max-width justify-center"
"flex-max-width justify-center",
)}
>
{" "}

View file

@ -0,0 +1,54 @@
import { cloneDeep } from "lodash";
import { useEffect } from "react";
import useAlertStore from "../../stores/alertStore";
import { ResponseErrorDetailAPI } from "../../types/api";
const useFetchDataOnMount = (
data,
name,
handleUpdateValues,
setNode,
renderTooltips,
setIsLoading,
) => {
const setErrorData = useAlertStore((state) => state.setErrorData);
useEffect(() => {
async function fetchData() {
if (
(data.node?.template[name]?.real_time_refresh ||
data.node?.template[name]?.refresh_button) &&
// options can be undefined but not an empty array
(data.node?.template[name]?.options?.length ?? 0) === 0
) {
setIsLoading(true);
try {
let newTemplate = await handleUpdateValues(name, data);
if (newTemplate) {
setNode(data.id, (oldNode) => {
let newNode = cloneDeep(oldNode);
newNode.data = {
...newNode.data,
};
newNode.data.node.template = newTemplate;
return newNode;
});
}
} catch (error) {
let responseError = error as ResponseErrorDetailAPI;
setErrorData({
title: "Error while updating the Component",
list: [responseError.response.data.detail ?? "Unknown error"],
});
}
setIsLoading(false);
renderTooltips();
}
}
fetchData();
}, []); // Empty dependency array ensures that this effect runs only once, on mount
};
export default useFetchDataOnMount;

View file

@ -0,0 +1,75 @@
import { cloneDeep } from "lodash";
import useAlertStore from "../../stores/alertStore";
import { ResponseErrorTypeAPI } from "../../types/api";
const useHandleOnNewValue = (
data,
name,
takeSnapshot,
handleUpdateValues,
debouncedHandleUpdateValues,
setNode,
renderTooltips,
isLoading,
setIsLoading,
) => {
const setErrorData = useAlertStore((state) => state.setErrorData);
const handleOnNewValue = async (newValue, skipSnapshot = false) => {
const nodeTemplate = data.node!.template[name];
const currentValue = nodeTemplate.value;
if (currentValue !== newValue && !skipSnapshot) {
takeSnapshot();
}
const shouldUpdate =
data.node?.template[name].real_time_refresh &&
!data.node?.template[name].refresh_button &&
currentValue !== newValue;
const typeToDebounce = nodeTemplate.type;
nodeTemplate.value = newValue;
let newTemplate;
if (shouldUpdate) {
setIsLoading(true);
try {
if (["int"].includes(typeToDebounce)) {
newTemplate = await handleUpdateValues(name, data);
} else {
newTemplate = await debouncedHandleUpdateValues(name, data);
}
} catch (error) {
let responseError = error as ResponseErrorTypeAPI;
setErrorData({
title: "Error while updating the Component",
list: [responseError.response.data.detail.error ?? "Unknown error"],
});
}
setIsLoading(false);
}
setNode(data.id, (oldNode) => {
const newNode = cloneDeep(oldNode);
newNode.data = {
...newNode.data,
};
if (data.node?.template[name].real_time_refresh && newTemplate) {
newNode.data.node.template = newTemplate;
} else {
newNode.data.node.template[name].value = newValue;
}
return newNode;
});
renderTooltips();
};
return { handleOnNewValue };
};
export default useHandleOnNewValue;

View file

@ -0,0 +1,40 @@
import { cloneDeep } from "lodash";
const useHandleNodeClass = (
data,
name,
takeSnapshot,
setNode,
updateNodeInternals,
renderTooltips,
) => {
const handleNodeClass = (newNodeClass, code) => {
if (!data.node) return;
if (data.node!.template[name].value !== code) {
takeSnapshot();
}
setNode(data.id, (oldNode) => {
let newNode = cloneDeep(oldNode);
newNode.data = {
...newNode.data,
node: newNodeClass,
description: newNodeClass.description ?? data.node!.description,
display_name: newNodeClass.display_name ?? data.node!.display_name,
};
newNode.data.node.template[name].value = code;
return newNode;
});
updateNodeInternals(data.id);
renderTooltips();
};
return { handleNodeClass };
};
export default useHandleNodeClass;

View file

@ -0,0 +1,39 @@
import { cloneDeep } from "lodash";
import useAlertStore from "../../stores/alertStore";
import { ResponseErrorDetailAPI } from "../../types/api";
import { handleUpdateValues } from "../../utils/parameterUtils";
const useHandleRefreshButtonPress = (setIsLoading, setNode, renderTooltips) => {
const setErrorData = useAlertStore((state) => state.setErrorData);
const handleRefreshButtonPress = async (name, data) => {
setIsLoading(true);
try {
let newTemplate = await handleUpdateValues(name, data);
if (newTemplate) {
setNode(data.id, (oldNode) => {
let newNode = cloneDeep(oldNode);
newNode.data = {
...newNode.data,
};
newNode.data.node.template = newTemplate;
return newNode;
});
}
} catch (error) {
let responseError = error as ResponseErrorDetailAPI;
setErrorData({
title: "Error while updating the Component",
list: [responseError.response.data.detail ?? "Unknown error"],
});
}
setIsLoading(false);
renderTooltips();
};
return { handleRefreshButtonPress };
};
export default useHandleRefreshButtonPress;

View file

@ -56,7 +56,11 @@ export const GradientGroup = (props) => {
</linearGradient>
</defs>
</svg>
<ForwardedIconComponent name="Combine" stroke="url(#grad3)" {...props} />
<ForwardedIconComponent
name="Combine"
stroke={`${props.disabled ? "#64748B" : "url(#grad3)"}`}
{...props}
/>
</>
);
};

View file

@ -0,0 +1,140 @@
import { useEffect, useState } from "react";
import InputComponent from "../../../components/inputComponent";
import {
FormControl,
FormField,
FormItem,
FormMessage,
} from "../../../components/ui/form";
import { Input } from "../../../components/ui/input";
import { Label } from "../../../components/ui/label";
import { Textarea } from "../../../components/ui/textarea";
import useFlowsManagerStore from "../../../stores/flowsManagerStore";
type FolderFormsProps = {
control: any;
setValue: any;
folderToEdit: any;
};
export const FolderForms = ({
control,
setValue,
folderToEdit,
}: FolderFormsProps): JSX.Element => {
const flows = useFlowsManagerStore((state) => state.flows);
const [selectedComponents, setSelectedComponents] = useState<string[]>([]);
const [selectedFlows, setSelectedFlows] = useState<string[]>([]);
const componentsList = flows
.filter((flow) => flow.is_component && flow.folder_id !== null)
.map((flow) => ({ id: flow.id, name: flow.name }));
const flowsList = flows
.filter((flow) => !flow.is_component && flow.folder_id !== null)
.map((flow) => ({ id: flow.id, name: flow.name }));
useEffect(() => {
setValue("components", selectedComponents);
setValue("flows", selectedFlows);
}, [selectedComponents, selectedFlows]);
useEffect(() => {
if (folderToEdit) {
setValue("name", folderToEdit.name);
setValue("description", folderToEdit.description);
console.log(folderToEdit);
// setSelectedComponents(folderToEdit.components);
// setSelectedFlows(folderToEdit.flows);
return;
}
setValue("name", "");
setValue("description", "");
setSelectedComponents([]);
setSelectedFlows([]);
}, [folderToEdit]);
return (
<>
<div className="flex h-full w-full flex-col gap-4 align-middle">
<Label>Folder Name</Label>
<FormField
control={control}
name="name"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
value={field.value}
onChange={field.onChange}
placeholder="Insert a name for the folder..."
></Input>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Label>Description (optional) </Label>
<FormField
control={control}
name="description"
render={({ field }) => (
<FormControl>
<Textarea
value={field.value}
onChange={field.onChange}
placeholder="Insert a description for the folder..."
></Textarea>
</FormControl>
)}
/>
<Label>Add Flows</Label>
<FormField
control={control}
name="flows"
render={() => (
<FormControl>
<InputComponent
isObjectOption
password={false}
objectOptions={flowsList}
placeholder="Choose a flow to add..."
id="input-flow"
setSelectedOptions={(value: any) => setSelectedFlows(value)}
selectedOptions={selectedFlows}
></InputComponent>
</FormControl>
)}
/>
<Label>Add Components</Label>
<FormField
control={control}
name="components"
render={() => (
<FormControl>
<InputComponent
isObjectOption
password={false}
objectOptions={componentsList}
placeholder="Choose a component to add..."
id="input-component"
setSelectedOptions={(value) => setSelectedComponents(value)}
selectedOptions={selectedComponents}
></InputComponent>
</FormControl>
)}
/>
</div>
</>
);
};
export default FolderForms;

View file

@ -0,0 +1,10 @@
import { z } from "zod";
export const FolderFormsSchema = z.object({
name: z.string().min(1, {
message: "Name must be at least 1 characters.",
}),
description: z.string().optional(),
components: z.array(z.string()),
flows: z.array(z.string()),
});

View file

@ -0,0 +1,53 @@
import { addFolder, updateFolder } from "../../../pages/MainPage/services";
import useAlertStore from "../../../stores/alertStore";
import { useFolderStore } from "../../../stores/foldersStore";
const useFolderSubmit = (setOpen, folderToEdit) => {
const setSuccessData = useAlertStore((state) => state.setSuccessData);
const setErrorData = useAlertStore((state) => state.setErrorData);
const getFoldersApi = useFolderStore((state) => state.getFoldersApi);
const onSubmit = (data) => {
if (folderToEdit) {
updateFolder(data, folderToEdit?.id!).then(
() => {
setSuccessData({
title: "Folder updated successfully.",
});
getFoldersApi(true);
setOpen(false);
},
(reason) => {
if (reason) {
setErrorData({
title: `Error updating folder.`,
});
console.error(reason);
} else {
getFoldersApi(true);
setOpen(false);
}
},
);
} else {
addFolder(data).then(
() => {
setSuccessData({
title: "Folder created successfully.",
});
getFoldersApi(true);
setOpen(false);
},
() => {
setErrorData({
title: `Error creating folder.`,
});
},
);
}
};
return { onSubmit, open, setOpen };
};
export default useFolderSubmit;

View file

@ -0,0 +1,79 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { z } from "zod";
import ForwardedIconComponent from "../../components/genericIconComponent";
import { Button } from "../../components/ui/button";
import { Form } from "../../components/ui/form";
import { useFolderStore } from "../../stores/foldersStore";
import BaseModal from "../baseModal";
import FolderForms from "./component";
import { FolderFormsSchema } from "./entities";
import useFolderSubmit from "./hooks/submit-folder";
type FoldersModalProps = {
open: boolean;
setOpen: (open: boolean) => void;
};
export default function FoldersModal({
open,
setOpen,
}: FoldersModalProps): JSX.Element {
const form = useForm<z.infer<typeof FolderFormsSchema>>({
resolver: zodResolver(FolderFormsSchema),
defaultValues: {
name: "",
description: "",
components: [],
flows: [],
},
mode: "all",
});
const folderToEdit = useFolderStore((state) => state.folderToEdit);
const { onSubmit: onSubmitFolder } = useFolderSubmit(setOpen, folderToEdit);
const onSubmit = (data) => {
onSubmitFolder(data);
};
return (
<>
<BaseModal size="x-small" open={open} setOpen={setOpen}>
<BaseModal.Header
description={`${folderToEdit ? "Edit a folder" : "Add a new folder"}`}
>
<span className="pr-2" data-testid="modal-title">
{folderToEdit ? "Edit" : "New"} Folder
</span>
<ForwardedIconComponent
name="Plus"
className="h-6 w-6 pl-1 text-primary "
aria-hidden="true"
/>
</BaseModal.Header>
<BaseModal.Content>
<div>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<FolderForms
folderToEdit={folderToEdit}
control={form.control}
setValue={form.setValue}
/>
<Button
className="float-right mt-6"
type="submit"
disabled={!form.formState.isValid}
>
{folderToEdit ? "Edit" : "Save"} Folder
</Button>
</form>
</Form>
</div>
</BaseModal.Content>
</BaseModal>
</>
);
}

View file

@ -4,6 +4,7 @@ import ImageViewer from "../../../../components/ImageViewer";
import CsvOutputComponent from "../../../../components/csvOutputComponent";
import InputListComponent from "../../../../components/inputListComponent";
import PdfViewer from "../../../../components/pdfViewer";
import RecordsOutputComponent from "../../../../components/recordsOutputComponent";
import {
Select,
SelectContent,
@ -47,6 +48,7 @@ export default function IOFieldView({
}
}
};
const [errorDuplicateKey, setErrorDuplicateKey] = useState(false);
function handleOutputType() {
@ -204,7 +206,7 @@ export default function IOFieldView({
<SelectItem key={separator} value={separator}>
{separator}
</SelectItem>
)
),
)}
</SelectGroup>
</SelectContent>
@ -280,6 +282,15 @@ export default function IOFieldView({
/>
</>
);
case "RecordsOutput":
return (
<div className={left ? "h-56" : "h-full"}>
<RecordsOutputComponent
flowPool={flowPoolNode}
pagination={!left}
/>
</div>
);
default:
return (

View file

@ -114,19 +114,19 @@ export default function ChatMessage({
<div
className={classNames(
"form-modal-chat-position",
chat.isSend ? "" : " "
chat.isSend ? "" : " ",
)}
>
<div
className={classNames(
"mr-3 mt-1 flex w-24 flex-col items-center gap-1 overflow-hidden px-3 pb-3"
"mr-3 mt-1 flex w-24 flex-col items-center gap-1 overflow-hidden px-3 pb-3",
)}
>
<div className="flex flex-col items-center gap-1">
<div
className={cn(
"relative flex h-8 w-8 items-center justify-center overflow-hidden rounded-md p-5 text-2xl",
!chat.isSend ? "bg-chat-bot-icon" : "bg-chat-user-icon"
!chat.isSend ? "bg-chat-bot-icon" : "bg-chat-user-icon",
)}
>
<img
@ -210,12 +210,12 @@ dark:prose-invert"
children[0] = (children[0] as string).replace(
"`▍`",
"▍"
"▍",
);
}
const match = /language-(\w+)/.exec(
className || ""
className || "",
);
return !inline ? (
@ -230,7 +230,7 @@ dark:prose-invert"
language: (match && match[1]) || "",
code: String(children).replace(
/\n$/,
""
"",
),
},
]}
@ -248,7 +248,7 @@ dark:prose-invert"
{chatMessage}
</Markdown>
),
[chat.message, chatMessage]
[chat.message, chatMessage],
)}
</div>
{chat.files && (
@ -306,7 +306,7 @@ dark:prose-invert"
parts.push(
<span className="chat-message-highlight">
{chat.message[match[1]]}
</span>
</span>,
);
}
@ -324,7 +324,7 @@ dark:prose-invert"
</>
) : (
<span
className="prose text-primary word-break-break-word dark:prose-invert"
className="prose text-primary word-break-break-word dark:prose-invert"
data-testid={
"chat-message-" + chat.sender_name + "-" + chatMessage
}

View file

@ -0,0 +1,10 @@
export function buildContent(value: string) {
const htmlContent = (
<div className="w-[200px]">
<span>{value != null && value != "" ? value : "None"}</span>
</div>
);
return htmlContent;
}
export default buildContent;

View file

@ -0,0 +1,8 @@
import { FlowType } from "../../../types/flow";
export function buildTweaks(flow: FlowType) {
return flow.data!.nodes.reduce((acc, node) => {
acc[node.data.id] = {};
return acc;
}, {});
}

View file

@ -0,0 +1,13 @@
import { LANGFLOW_SUPPORTED_TYPES } from "../../../constants/constants";
export const checkCanBuildTweakObject = (element, templateField) => {
return (
element.data.node.template[templateField] &&
templateField.charAt(0) !== "_" &&
element.data.node.template[templateField].show &&
LANGFLOW_SUPPORTED_TYPES.has(
element.data.node.template[templateField].type,
) &&
templateField !== "code"
);
};

View file

@ -0,0 +1,26 @@
import { TemplateVariableType } from "../../../types/api";
import { convertArrayToObj } from "../../../utils/reactflowUtils";
export const getChangesType = (
changes: string | string[] | boolean | number | Object[] | Object,
template: TemplateVariableType,
) => {
if (typeof changes === "string" && template.type === "float") {
changes = parseFloat(changes);
}
if (typeof changes === "string" && template.type === "int") {
changes = parseInt(changes);
}
if (template.list === true && Array.isArray(changes)) {
changes = changes?.filter((x) => x !== "");
}
if (template.type === "dict" && Array.isArray(changes)) {
changes = convertArrayToObj(changes);
}
if (template.type === "NestedDict") {
changes = JSON.stringify(changes);
}
return changes;
};

View file

@ -0,0 +1,28 @@
import { LANGFLOW_SUPPORTED_TYPES } from "../../../constants/constants";
export const getNodesWithDefaultValue = (flow) => {
let arrNodesWithValues: string[] = [];
flow["data"]!["nodes"].forEach((node) => {
if (!node["data"]["node"]["template"]) {
return;
}
Object.keys(node["data"]["node"]["template"])
.filter(
(templateField) =>
templateField.charAt(0) !== "_" &&
node.data.node.template[templateField].show &&
LANGFLOW_SUPPORTED_TYPES.has(
node.data.node.template[templateField].type,
),
)
.map((n, i) => {
arrNodesWithValues.push(node["id"]);
});
});
const tweaksListFiltered = arrNodesWithValues.filter((value, index, self) => {
return self.indexOf(value) === index;
});
return tweaksListFiltered;
};

View file

@ -0,0 +1,29 @@
import { TemplateVariableType } from "../../../types/api";
import { NodeType } from "../../../types/flow";
export const getValue = (
value: string,
node: NodeType,
template: TemplateVariableType,
tweak: Object[],
) => {
let returnValue = value ?? "";
if (tweak.length > 0) {
for (const obj of tweak) {
Object.keys(obj).forEach((key) => {
const value = obj[key];
if (key == node["id"]) {
Object.keys(value).forEach((key) => {
if (key == template["name"]) {
returnValue = value[key];
}
});
}
});
}
} else {
return value ?? "";
}
return returnValue;
};

View file

@ -0,0 +1,212 @@
import "ace-builds/src-noconflict/ext-language_tools";
import "ace-builds/src-noconflict/mode-python";
import "ace-builds/src-noconflict/theme-github";
import "ace-builds/src-noconflict/theme-twilight";
import { ReactNode, forwardRef, useContext, useEffect, useState } from "react";
// import "ace-builds/webpack-resolver";
import { cloneDeep } from "lodash";
import CodeTabsComponent from "../../../components/codeTabsComponent";
import IconComponent from "../../../components/genericIconComponent";
import { EXPORT_CODE_DIALOG } from "../../../constants/constants";
import { AuthContext } from "../../../contexts/authContext";
import { useTweaksStore } from "../../../stores/tweaksStore";
import { TemplateVariableType } from "../../../types/api";
import { uniqueTweakType } from "../../../types/components";
import { FlowType } from "../../../types/flow/index";
import {
getCurlCode,
getPythonApiCode,
getPythonCode,
getWidgetCode,
tabsArray,
} from "../../../utils/utils";
import BaseModal from "../../baseModal";
import { buildContent } from "../utils/build-content";
import { buildTweaks } from "../utils/build-tweaks";
import { checkCanBuildTweakObject } from "../utils/check-can-build-tweak-object";
import { getChangesType } from "../utils/get-changes-types";
import { getNodesWithDefaultValue } from "../utils/get-nodes-with-default-value";
import { getValue } from "../utils/get-value";
const ApiModal = forwardRef(
(
{
flow,
children,
}: {
flow: FlowType;
children: ReactNode;
},
ref,
) => {
const tweak = useTweaksStore((state) => state.tweak);
const addTweaks = useTweaksStore((state) => state.setTweak);
const setTweaksList = useTweaksStore((state) => state.setTweaksList);
const tweaksList = useTweaksStore((state) => state.tweaksList);
const [activeTweaks, setActiveTweaks] = useState(false);
const { autoLogin } = useContext(AuthContext);
const [open, setOpen] = useState(false);
const [activeTab, setActiveTab] = useState("0");
const pythonApiCode = getPythonApiCode(flow?.id, autoLogin, tweak);
const curl_code = getCurlCode(flow?.id, autoLogin, tweak);
const pythonCode = getPythonCode(flow?.name, tweak);
const widgetCode = getWidgetCode(flow?.id, flow?.name, autoLogin);
const tweaksCode = buildTweaks(flow);
const codesArray = [
curl_code,
pythonApiCode,
pythonCode,
widgetCode,
pythonCode,
];
const [tabs, setTabs] = useState(tabsArray(codesArray, 0));
const canShowTweaks =
flow &&
flow["data"] &&
flow["data"]!["nodes"] &&
tweak &&
tweak?.length > 0 &&
activeTweaks === true;
const buildTweaksInitialState = () => {
const newTweak: any = [];
const t = buildTweaks(flow);
newTweak.push(t);
addTweaks(newTweak);
addCodes(newTweak);
};
useEffect(() => {
if (flow["data"]!["nodes"].length == 0) {
addTweaks([]);
setTweaksList([]);
} else {
buildTweaksInitialState();
}
filterNodes();
if (Object.keys(tweaksCode).length > 0) {
setActiveTab("0");
setTabs(tabsArray(codesArray, 1));
} else {
setTabs(tabsArray(codesArray, 1));
}
}, [flow["data"]!["nodes"], open]);
useEffect(() => {
if (canShowTweaks) {
const nodes = flow["data"]!["nodes"];
nodes.forEach((element) => {
const nodeId = element["id"];
const template = element["data"]["node"]["template"];
Object.keys(template).forEach((templateField) => {
if (checkCanBuildTweakObject(element, templateField)) {
buildTweakObject(
nodeId,
element.data.node.template[templateField].value,
element.data.node.template[templateField],
);
}
});
});
} else {
buildTweaksInitialState();
}
}, [activeTweaks]);
const filterNodes = () => {
setTweaksList(getNodesWithDefaultValue(flow));
};
async function buildTweakObject(
tw: string,
changes: string | string[] | boolean | number | Object[] | Object,
template: TemplateVariableType,
) {
changes = getChangesType(changes, template);
const existingTweak = tweak.find((element) => element.hasOwnProperty(tw));
if (existingTweak) {
existingTweak[tw][template["name"]!] = changes as string;
if (existingTweak[tw][template["name"]!] == template.value) {
tweak.forEach((element) => {
if (element[tw] && Object.keys(element[tw])?.length === 0) {
const filteredTweaks = tweak.filter((obj) => {
const prop = obj[Object.keys(obj)[0]].prop;
return prop !== undefined && prop !== null && prop !== "";
});
addTweaks(filteredTweaks);
}
});
}
} else {
const newTweak = {
[tw]: {
[template["name"]!]: changes,
},
} as uniqueTweakType;
tweak.push(newTweak);
}
if (tweak && tweak.length > 0) {
const cloneTweak = cloneDeep(tweak);
addCodes(cloneTweak);
addTweaks(cloneTweak);
}
}
const addCodes = (cloneTweak) => {
const pythonApiCode = getPythonApiCode(flow?.id, autoLogin, cloneTweak);
const curl_code = getCurlCode(flow?.id, autoLogin, cloneTweak);
const pythonCode = getPythonCode(flow?.name, cloneTweak);
const widgetCode = getWidgetCode(flow?.id, flow?.name, autoLogin);
if (tabs && tabs?.length > 0) {
tabs![0].code = curl_code;
tabs![1].code = pythonApiCode;
tabs![2].code = pythonCode;
tabs![3].code = widgetCode;
}
};
return (
<BaseModal open={open} setOpen={setOpen}>
<BaseModal.Trigger asChild>{children}</BaseModal.Trigger>
<BaseModal.Header description={EXPORT_CODE_DIALOG}>
<span className="pr-2">API</span>
<IconComponent
name="Code2"
className="h-6 w-6 pl-1 text-gray-800 dark:text-white"
aria-hidden="true"
/>
</BaseModal.Header>
<BaseModal.Content>
<CodeTabsComponent
flow={flow}
tabs={tabs!}
activeTab={activeTab}
setActiveTab={setActiveTab}
tweaks={{
tweak,
tweaksList,
buildContent,
buildTweakObject,
getValue,
}}
activeTweaks={activeTweaks}
setActiveTweaks={setActiveTweaks}
allowExport
/>
</BaseModal.Content>
</BaseModal>
);
},
);
export default ApiModal;

View file

@ -25,15 +25,21 @@ type TriggerProps = {
children: ReactNode;
asChild?: boolean;
disable?: boolean;
className?: string;
};
const Content: React.FC<ContentProps> = ({ children }) => {
return <div className="flex h-full w-full flex-col">{children}</div>;
};
const Trigger: React.FC<TriggerProps> = ({ children, asChild, disable }) => {
const Trigger: React.FC<TriggerProps> = ({
children,
asChild,
disable,
className,
}) => {
return (
<DialogTrigger
className={asChild ? "" : "w-full"}
className={asChild ? "" : cn("w-full", className)}
hidden={children ? false : true}
disabled={disable}
asChild={asChild}
@ -113,7 +119,7 @@ function BaseModal({
switch (size) {
case "x-small":
minWidth = "min-w-[20vw]";
height = " ";
height = "h-full";
break;
case "smaller":
minWidth = "min-w-[40vw]";
@ -129,6 +135,7 @@ function BaseModal({
break;
case "small-h-full":
minWidth = "min-w-[40vw]";
height = "h-full";
break;
case "medium":
minWidth = "min-w-[60vw]";
@ -136,6 +143,8 @@ function BaseModal({
break;
case "medium-h-full":
minWidth = "min-w-[60vw]";
height = "h-full";
break;
case "large":
minWidth = "min-w-[85vw]";
@ -162,6 +171,7 @@ function BaseModal({
case "large-h-full":
minWidth = "min-w-[80vw]";
height = "h-full";
break;
default:
minWidth = "min-w-[80vw]";
@ -186,7 +196,7 @@ function BaseModal({
{headerChild}
</div>
<div
className={`flex flex-col ${height!} w-full transition-all duration-300`}
className={`flex flex-col ${height} w-full transition-all duration-300`}
>
{ContentChild}
</div>
@ -203,7 +213,7 @@ function BaseModal({
{headerChild}
</div>
<div
className={`flex flex-col ${height!} w-full transition-all duration-300`}
className={`flex flex-col ${height} w-full transition-all duration-300`}
>
{ContentChild}
</div>

View file

@ -17,6 +17,7 @@ export default function DeleteConfirmationModal({
asChild,
open,
setOpen,
note = "",
}: {
children: JSX.Element;
onConfirm: (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
@ -24,6 +25,7 @@ export default function DeleteConfirmationModal({
asChild?: boolean;
open?: boolean;
setOpen?: (open: boolean) => void;
note?: string;
}) {
return (
<Dialog open={open} onOpenChange={setOpen}>
@ -43,7 +45,14 @@ export default function DeleteConfirmationModal({
</DialogTitle>
</DialogHeader>
<span>
Confirm deletion of {description ?? "component"}?<br></br>
Are you sure you want to delete the selected{" "}
{description ?? "component"}?<br></br>
{note && (
<>
{note}
<br></br>
</>
)}
Note: This action is irreversible.
</span>
<DialogFooter>

View file

@ -18,6 +18,10 @@ export default function DictAreaModal({
children,
onChange,
value,
}: {
children: JSX.Element;
onChange?: (value: Object) => void;
value: Object;
}): JSX.Element {
const [open, setOpen] = useState(false);
const isDark = useDarkStore((state) => state.dark);
@ -29,9 +33,13 @@ export default function DictAreaModal({
return (
<BaseModal size="medium-h-full" open={open} setOpen={setOpen}>
<BaseModal.Trigger>{children}</BaseModal.Trigger>
<BaseModal.Header description={CODE_DICT_DIALOG_SUBTITLE}>
<span className="pr-2">Edit Dictionary</span>
<BaseModal.Trigger className="h-full">{children}</BaseModal.Trigger>
<BaseModal.Header
description={onChange ? CODE_DICT_DIALOG_SUBTITLE : null}
>
<span className="pr-2">
{onChange ? "Edit Dictionary" : "View Dictionary"}
</span>
<IconComponent
name="BookMarked"
className="h-6 w-6 pl-1 text-primary "
@ -44,7 +52,7 @@ export default function DictAreaModal({
theme="vscode"
dark={isDark}
className={!isDark ? "json-view-white" : "json-view-dark"}
editable
editable={!!onChange}
enableClipboard
onEdit={(edit) => {
ref.current = edit["src"];
@ -54,19 +62,21 @@ export default function DictAreaModal({
}}
src={ref.current}
/>
<div className="flex h-fit w-full justify-end">
<Button
data-testid="save-dict-button"
className="mt-3"
type="submit"
onClick={() => {
onChange(ref.current);
setOpen(false);
}}
>
Save
</Button>
</div>
{onChange && (
<div className="flex h-fit w-full justify-end">
<Button
data-testid="save-dict-button"
className="mt-3"
type="submit"
onClick={() => {
onChange(ref.current);
setOpen(false);
}}
>
Save
</Button>
</div>
)}
</div>
</BaseModal.Content>
</BaseModal>

View file

@ -28,6 +28,7 @@ import {
LANGFLOW_SUPPORTED_TYPES,
limitScrollFieldsModal,
} from "../../constants/constants";
import { Case } from "../../shared/components/caseComponent";
import useFlowStore from "../../stores/flowStore";
import { NodeDataType } from "../../types/flow";
import {
@ -92,6 +93,10 @@ const EditNodeModal = forwardRef(
const [errorDuplicateKey, setErrorDuplicateKey] = useState(false);
const type = (templateParam) => {
return myData.node?.template[templateParam].type;
};
return (
<BaseModal
key={data.id}
@ -146,7 +151,7 @@ const EditNodeModal = forwardRef(
templateParam.charAt(0) !== "_" &&
myData.node?.template[templateParam].show &&
LANGFLOW_SUPPORTED_TYPES.has(
myData.node.template[templateParam].type,
myData.node!.template[templateParam].type,
),
)
.map((templateParam, index) => {
@ -178,8 +183,7 @@ const EditNodeModal = forwardRef(
className={
"h-10 " +
((templateParam === "code" &&
myData.node?.template[templateParam].type ===
"code") ||
type(templateParam) === "code") ||
(templateParam.includes("code") &&
myData.node?.template[templateParam].proxy)
? " hidden "
@ -198,7 +202,7 @@ const EditNodeModal = forwardRef(
<span>
{myData.node?.template[templateParam]
.display_name
? myData.node.template[templateParam]
? myData.node!.template[templateParam]
.display_name
: myData.node?.template[templateParam]
.name}
@ -206,23 +210,27 @@ const EditNodeModal = forwardRef(
</ShadTooltip>
</TableCell>
<TableCell className="w-[300px] p-0 text-center text-xs text-foreground ">
{myData.node?.template[templateParam].type ===
"str" &&
!myData.node.template[templateParam].options ? (
<Case
condition={
type(templateParam) === "str" &&
!myData.node!.template[templateParam]
.options
}
>
<div className="mx-auto">
{myData.node.template[templateParam]
{myData.node!.template[templateParam]
?.list ? (
<InputListComponent
componentName={templateParam}
editNode={true}
disabled={disabled}
value={
!myData.node.template[templateParam]
!myData.node!.template[templateParam]
.value ||
myData.node.template[templateParam]
myData.node!.template[templateParam]
.value === ""
? [""]
: myData.node.template[
: myData.node!.template[
templateParam
].value
}
@ -233,23 +241,23 @@ const EditNodeModal = forwardRef(
);
}}
/>
) : myData.node.template[templateParam]
) : myData.node!.template[templateParam]
.multiline ? (
<TextAreaComponent
id={
"textarea-edit-" +
myData.node.template[templateParam]
myData.node!.template[templateParam]
.name
}
data-testid={
"textarea-edit-" +
myData.node.template[templateParam]
myData.node!.template[templateParam]
.name
}
disabled={disabled}
editNode={true}
value={
myData.node.template[templateParam]
myData.node!.template[templateParam]
.value ?? ""
}
onChange={(
@ -282,8 +290,13 @@ const EditNodeModal = forwardRef(
/>
)}
</div>
) : myData.node?.template[templateParam]
.type === "NestedDict" ? (
</Case>
<Case
condition={
type(templateParam) === "NestedDict"
}
>
<div className=" w-full">
<DictComponent
disabled={disabled}
@ -310,8 +323,11 @@ const EditNodeModal = forwardRef(
id="editnode-div-dict-input"
/>
</div>
) : myData.node?.template[templateParam]
.type === "dict" ? (
</Case>
<Case
condition={type(templateParam) === "dict"}
>
<div
className={classNames(
"max-h-48 w-full overflow-auto custom-scroll",
@ -334,6 +350,7 @@ const EditNodeModal = forwardRef(
myData.node!.template[
templateParam
].value,
type(templateParam)!,
)
}
duplicateKey={errorDuplicateKey}
@ -357,18 +374,22 @@ const EditNodeModal = forwardRef(
}
/>
</div>
) : myData.node?.template[templateParam]
.type === "bool" ? (
</Case>
<Case
condition={type(templateParam) === "bool"}
>
<div className="ml-auto">
{" "}
<ToggleShadComponent
id={
"toggle-edit-" +
myData.node.template[templateParam].name
myData.node!.template[templateParam]
.name
}
disabled={disabled}
enabled={
myData.node.template[templateParam]
myData.node!.template[templateParam]
.value
}
setEnabled={(isEnabled) => {
@ -381,8 +402,11 @@ const EditNodeModal = forwardRef(
editNode={true}
/>
</div>
) : myData.node?.template[templateParam]
.type === "float" ? (
</Case>
<Case
condition={type(templateParam) === "float"}
>
<div className="mx-auto">
<FloatComponent
disabled={disabled}
@ -392,7 +416,7 @@ const EditNodeModal = forwardRef(
.rangeSpec
}
value={
myData.node.template[templateParam]
myData.node!.template[templateParam]
.value ?? ""
}
onChange={(value) => {
@ -400,32 +424,38 @@ const EditNodeModal = forwardRef(
}}
/>
</div>
) : myData.node?.template[templateParam]
.type === "str" &&
myData.node.template[templateParam]
.options ? (
</Case>
<Case
condition={
type(templateParam) === "str" &&
myData.node!.template[templateParam].options
}
>
<div className="mx-auto">
<Dropdown
editNode={true}
options={
myData.node.template[templateParam]
myData.node!.template[templateParam]
.options
}
onSelect={(value) =>
handleOnNewValue(value, templateParam)
}
value={
myData.node.template[templateParam]
myData.node!.template[templateParam]
.value ?? "Choose an option"
}
id={
"dropdown-edit-" +
myData.node.template[templateParam].name
myData.node!.template[templateParam]
.name
}
></Dropdown>
</div>
) : myData.node?.template[templateParam]
.type === "int" ? (
</Case>
<Case condition={type(templateParam) === "int"}>
<div className="mx-auto">
<IntComponent
rangeSpec={
@ -434,12 +464,13 @@ const EditNodeModal = forwardRef(
}
id={
"edit-int-input-" +
myData.node.template[templateParam].name
myData.node!.template[templateParam]
.name
}
disabled={disabled}
editNode={true}
value={
myData.node.template[templateParam]
myData.node!.template[templateParam]
.value ?? ""
}
onChange={(value) => {
@ -447,21 +478,24 @@ const EditNodeModal = forwardRef(
}}
/>
</div>
) : myData.node?.template[templateParam]
.type === "file" ? (
</Case>
<Case
condition={type(templateParam) === "file"}
>
<div className="mx-auto">
<InputFileComponent
editNode={true}
disabled={disabled}
value={
myData.node.template[templateParam]
myData.node!.template[templateParam]
.value ?? ""
}
onChange={(value: string | string[]) => {
handleOnNewValue(value, templateParam);
}}
fileTypes={
myData.node.template[templateParam]
myData.node!.template[templateParam]
.fileTypes
}
onFileChange={(filePath: string) => {
@ -471,8 +505,11 @@ const EditNodeModal = forwardRef(
}}
></InputFileComponent>
</div>
) : myData.node?.template[templateParam]
.type === "prompt" ? (
</Case>
<Case
condition={type(templateParam) === "prompt"}
>
<div className="mx-auto">
<PromptAreaComponent
readonly={
@ -486,7 +523,7 @@ const EditNodeModal = forwardRef(
myData.node = nodeClass;
}}
value={
myData.node.template[templateParam]
myData.node!.template[templateParam]
.value ?? ""
}
onChange={(value: string | string[]) => {
@ -494,21 +531,26 @@ const EditNodeModal = forwardRef(
}}
id={
"prompt-area-edit-" +
myData.node.template[templateParam].name
myData.node!.template[templateParam]
.name
}
data-testid={
"modal-prompt-input-" +
myData.node.template[templateParam].name
myData.node!.template[templateParam]
.name
}
/>
</div>
) : myData.node?.template[templateParam]
.type === "code" ? (
</Case>
<Case
condition={type(templateParam) === "code"}
>
<div className="mx-auto">
<CodeAreaComponent
readonly={
myData.node?.flow &&
myData.node.template[templateParam]
myData.node!.template[templateParam]
.dynamic
? true
: false
@ -524,7 +566,7 @@ const EditNodeModal = forwardRef(
disabled={disabled}
editNode={true}
value={
myData.node.template[templateParam]
myData.node!.template[templateParam]
.value ?? ""
}
onChange={(value: string | string[]) => {
@ -532,16 +574,16 @@ const EditNodeModal = forwardRef(
}}
id={
"code-area-edit" +
myData.node.template[templateParam].name
myData.node!.template[templateParam]
.name
}
/>
</div>
) : myData.node?.template[templateParam]
.type === "Any" ? (
"-"
) : (
<div className="hidden"></div>
)}
</Case>
<Case condition={type(templateParam) === "Any"}>
<>-</>
</Case>
</TableCell>
<TableCell className="p-0 text-right">
<div className="items-center text-center">

View file

@ -0,0 +1,104 @@
import { useEffect, useState } from "react";
import IconComponent from "../../components/genericIconComponent";
import { Tabs, TabsList, TabsTrigger } from "../../components/ui/tabs";
import useFlowsManagerStore from "../../stores/flowsManagerStore";
import { FlowSettingsPropsType } from "../../types/components";
import { FlowType } from "../../types/flow";
import BaseModal from "../baseModal";
import TableComponent from "../../components/tableComponent";
import { getMessagesTable, getTransactionTable } from "../../controllers/API";
import {
ColDef,
ColGroupDef,
SizeColumnsToFitGridStrategy,
} from "ag-grid-community";
export default function FlowLogsModal({
open,
setOpen,
}: FlowSettingsPropsType): JSX.Element {
const saveFlow = useFlowsManagerStore((state) => state.saveFlow);
const currentFlow = useFlowsManagerStore((state) => state.currentFlow);
const currentFlowId = useFlowsManagerStore((state) => state.currentFlowId);
const flows = useFlowsManagerStore((state) => state.flows);
useEffect(() => {
setName(currentFlow!.name);
setDescription(currentFlow!.description);
}, [currentFlow!.name, currentFlow!.description, open]);
const [name, setName] = useState(currentFlow!.name);
const [description, setDescription] = useState(currentFlow!.description);
const [columns, setColumns] = useState<Array<ColDef | ColGroupDef>>([]);
const [rows, setRows] = useState<any>([]);
const [activeTab, setActiveTab] = useState("Executions");
function handleClick(): void {
currentFlow!.name = name;
currentFlow!.description = description;
saveFlow(currentFlow!);
setOpen(false);
}
useEffect(() => {
if (activeTab === "Executions") {
getTransactionTable(currentFlowId, "union").then((data) => {
const { columns, rows } = data;
setColumns(columns.map((col) => ({ ...col, editable: true })));
setRows(rows);
});
} else if (activeTab === "Messages") {
getMessagesTable(currentFlowId, "union").then((data) => {
const { columns, rows } = data;
setColumns(columns.map((col) => ({ ...col, editable: true })));
setRows(rows);
});
}
}, [open, activeTab]);
const [nameLists, setNameList] = useState<string[]>([]);
useEffect(() => {
const tempNameList: string[] = [];
flows.forEach((flow: FlowType) => {
if ((flow.is_component ?? false) === false) tempNameList.push(flow.name);
});
setNameList(tempNameList.filter((name) => name !== currentFlow!.name));
}, [flows]);
return (
<BaseModal open={open} setOpen={setOpen} size="large">
<BaseModal.Header description="Inspect component executions and monitor sent messages in the playground.">
<div className="flex w-full justify-between">
<div className="flex h-fit w-32 items-center">
<span className="pr-2">Logs</span>
<IconComponent name="ScrollText" className="mr-2 h-4 w-4 " />
</div>
<div className="flex h-fit w-32 items-center"></div>
</div>
</BaseModal.Header>
<BaseModal.Content>
<Tabs
value={activeTab}
onValueChange={setActiveTab}
className={
"text-center; inset-0 m-0 mb-2 flex flex-col self-center overflow-hidden rounded-md border bg-muted pb-1"
}
>
<TabsList>
<TabsTrigger value={"Executions"}>Executions</TabsTrigger>
<TabsTrigger value={"Messages"}>Messages</TabsTrigger>
</TabsList>
</Tabs>
<TableComponent
readOnlyEdit
className="h-max-full h-full w-full"
pagination={rows.length === 0 ? false : true}
columnDefs={columns}
autoSizeStrategy={{ type: "fitGridWidth" }}
rowData={rows}
headerHeight={rows.length === 0 ? 0 : undefined}
></TableComponent>
</BaseModal.Content>
</BaseModal>
);
}

View file

@ -0,0 +1,148 @@
import { useEffect, useState } from "react";
import InputComponent from "../../../components/inputComponent";
import {
FormControl,
FormField,
FormItem,
FormMessage,
} from "../../../components/ui/form";
import { Input } from "../../../components/ui/input";
import { Label } from "../../../components/ui/label";
import { Textarea } from "../../../components/ui/textarea";
import useFlowsManagerStore from "../../../stores/flowsManagerStore";
type FolderFormsProps = {
control: any;
setValue: any;
folderToEdit: any;
};
export const FolderForms = ({
control,
setValue,
folderToEdit,
}: FolderFormsProps): JSX.Element => {
const flows = useFlowsManagerStore((state) => state.flows);
const [selectedComponents, setSelectedComponents] = useState<string[]>([]);
const [selectedFlows, setSelectedFlows] = useState<string[]>([]);
const allFlows = useFlowsManagerStore((state) => state.allFlows);
const componentsList = flows
.filter((flow) => flow.is_component && flow.folder_id !== null)
.map((flow) => ({ id: flow.id, name: flow.name }));
const flowsList = flows
.filter((flow) => !flow.is_component && flow.folder_id !== null)
.map((flow) => ({ id: flow.id, name: flow.name }));
const componentsOnFolder = allFlows
.filter((flow) => flow.is_component && flow.folder_id === folderToEdit?.id)
.map((flow) => flow.id);
const flowsOnFolder = allFlows
.filter((flow) => !flow.is_component && flow.folder_id === folderToEdit?.id)
.map((flow) => flow.id);
useEffect(() => {
setValue("components", selectedComponents);
setValue("flows", selectedFlows);
}, [selectedComponents, selectedFlows]);
useEffect(() => {
if (folderToEdit) {
setValue("name", folderToEdit.name);
setValue("description", folderToEdit.description);
setSelectedComponents(componentsOnFolder);
setSelectedFlows(flowsOnFolder);
return;
}
setValue("name", "");
setValue("description", "");
setSelectedComponents([]);
setSelectedFlows([]);
}, [folderToEdit]);
return (
<>
<div className="flex h-full w-full flex-col gap-4 align-middle">
<Label>Folder Name</Label>
<FormField
control={control}
name="name"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
value={field.value}
onChange={field.onChange}
placeholder="Insert a name for the folder..."
></Input>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Label>Description (optional) </Label>
<FormField
control={control}
name="description"
render={({ field }) => (
<FormControl>
<Textarea
value={field.value}
onChange={field.onChange}
placeholder="Insert a description for the folder..."
></Textarea>
</FormControl>
)}
/>
{/*
<Label>Add Flows</Label>
<FormField
control={control}
name="flows"
render={() => (
<FormControl>
<InputComponent
isObjectOption
password={false}
objectOptions={flowsList}
placeholder="Choose a flow to add..."
id="input-flow"
setSelectedOptions={(value: any) => setSelectedFlows(value)}
selectedOptions={selectedFlows}
></InputComponent>
</FormControl>
)}
/>
<Label>Add Components</Label>
<FormField
control={control}
name="components"
render={() => (
<FormControl>
<InputComponent
isObjectOption
password={false}
objectOptions={componentsList}
placeholder="Choose a component to add..."
id="input-component"
setSelectedOptions={(value) => setSelectedComponents(value)}
selectedOptions={selectedComponents}
></InputComponent>
</FormControl>
)}
/> */}
</div>
</>
);
};
export default FolderForms;

View file

@ -0,0 +1,10 @@
import { z } from "zod";
export const FolderFormsSchema = z.object({
name: z.string().min(1, {
message: "Name must be at least 1 characters.",
}),
description: z.string().optional(),
components: z.array(z.string()),
flows: z.array(z.string()),
});

View file

@ -0,0 +1,60 @@
import { useNavigate } from "react-router-dom";
import { addFolder, updateFolder } from "../../../pages/MainPage/services";
import useAlertStore from "../../../stores/alertStore";
import { useFolderStore } from "../../../stores/foldersStore";
const useFolderSubmit = (setOpen, folderToEdit) => {
const setSuccessData = useAlertStore((state) => state.setSuccessData);
const setErrorData = useAlertStore((state) => state.setErrorData);
const getFoldersApi = useFolderStore((state) => state.getFoldersApi);
const navigate = useNavigate();
const onSubmit = (data) => {
if (folderToEdit) {
updateFolder(data, folderToEdit?.id!).then(
() => {
setSuccessData({
title: "Folder updated successfully.",
});
getFoldersApi(true);
setOpen(false);
navigate(`all/folder/${folderToEdit.id}`, {
state: { folderId: folderToEdit.id },
});
},
//TODO: LOOK THIS ERRO MORE CAREFULLY
(reason) => {
if (reason) {
setErrorData({
title: `Error updating folder.`,
});
console.error(reason);
} else {
getFoldersApi(true);
setOpen(false);
}
},
);
} else {
addFolder(data).then(
(res) => {
setSuccessData({
title: "Folder created successfully.",
});
getFoldersApi(true);
setOpen(false);
navigate(`all/folder/${res.id}`, { state: { folderId: res.id } });
},
() => {
setErrorData({
title: `Error creating folder.`,
});
},
);
}
};
return { onSubmit, open, setOpen };
};
export default useFolderSubmit;

View file

@ -0,0 +1,79 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { z } from "zod";
import ForwardedIconComponent from "../../components/genericIconComponent";
import { Button } from "../../components/ui/button";
import { Form } from "../../components/ui/form";
import { useFolderStore } from "../../stores/foldersStore";
import BaseModal from "../baseModal";
import FolderForms from "./component";
import { FolderFormsSchema } from "./entities";
import useFolderSubmit from "./hooks/submit-folder";
type FoldersModalProps = {
open: boolean;
setOpen: (open: boolean) => void;
};
export default function FoldersModal({
open,
setOpen,
}: FoldersModalProps): JSX.Element {
const form = useForm<z.infer<typeof FolderFormsSchema>>({
resolver: zodResolver(FolderFormsSchema),
defaultValues: {
name: "",
description: "",
components: [],
flows: [],
},
mode: "all",
});
const folderToEdit = useFolderStore((state) => state.folderToEdit);
const { onSubmit: onSubmitFolder } = useFolderSubmit(setOpen, folderToEdit);
const onSubmit = (data) => {
onSubmitFolder(data);
};
return (
<>
<BaseModal size="x-small" open={open} setOpen={setOpen}>
<BaseModal.Header
description={`${folderToEdit ? "Edit a folder" : "Add a new folder"}`}
>
<span className="pr-2" data-testid="modal-title">
{folderToEdit ? "Edit" : "New"} Folder
</span>
<ForwardedIconComponent
name={folderToEdit ? "Pencil" : "Plus"}
className="h-6 w-6 pl-1 text-primary "
aria-hidden="true"
/>
</BaseModal.Header>
<BaseModal.Content>
<div>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<FolderForms
folderToEdit={folderToEdit}
control={form.control}
setValue={form.setValue}
/>
<Button
className="float-right mt-6"
type="submit"
disabled={!form.formState.isValid}
>
{folderToEdit ? "Edit" : "Save"} Folder
</Button>
</form>
</Form>
</div>
</BaseModal.Content>
</BaseModal>
</>
);
}

View file

@ -1,4 +1,4 @@
import { useNavigate } from "react-router-dom";
import { useLocation, useNavigate } from "react-router-dom";
import {
Card,
CardContent,
@ -6,15 +6,21 @@ import {
CardTitle,
} from "../../../../components/ui/card";
import useFlowsManagerStore from "../../../../stores/flowsManagerStore";
import { useFolderStore } from "../../../../stores/foldersStore";
export default function NewFlowCardComponent() {
const addFlow = useFlowsManagerStore((state) => state.addFlow);
const navigate = useNavigate();
const location = useLocation();
const folderId = location?.state?.folderId;
const setFolderUrl = useFolderStore((state) => state.setFolderUrl);
return (
<Card
onClick={() => {
addFlow(true).then((id) => {
navigate("/flow/" + id);
setFolderUrl(folderId ?? "");
navigate(`/flow/${id}${folderId ? `/folder/${folderId}` : ""}`);
});
}}
className="h-64 w-80 cursor-pointer bg-background pt-4"

View file

@ -1,4 +1,4 @@
import { useNavigate } from "react-router-dom";
import { useLocation, useNavigate } from "react-router-dom";
/// <reference types="vite-plugin-svgr/client" />
//@ts-ignore
import { ReactComponent as TransferFiles } from "../../../../assets/undraw_transfer_files_re_a2a9.svg";
@ -22,6 +22,7 @@ import {
CardTitle,
} from "../../../../components/ui/card";
import useFlowsManagerStore from "../../../../stores/flowsManagerStore";
import { useFolderStore } from "../../../../stores/foldersStore";
import { UndrawCardComponentProps } from "../../../../types/components";
import { updateIds } from "../../../../utils/reactflowUtils";
@ -30,6 +31,9 @@ export default function UndrawCardComponent({
}: UndrawCardComponentProps): JSX.Element {
const addFlow = useFlowsManagerStore((state) => state.addFlow);
const navigate = useNavigate();
const location = useLocation();
const folderId = location?.state?.folderId;
const setFolderUrl = useFolderStore((state) => state.setFolderUrl);
function selectImage() {
switch (flow.name) {
@ -111,7 +115,8 @@ export default function UndrawCardComponent({
onClick={() => {
updateIds(flow.data!);
addFlow(true, flow).then((id) => {
navigate("/flow/" + id);
setFolderUrl(folderId ?? "");
navigate(`/flow/${id}${folderId ? `/folder/${folderId}` : ""}`);
});
}}
className="h-64 w-80 cursor-pointer bg-background pt-4"

View file

@ -10,7 +10,7 @@ export default function NewFlowModal({
}: newFlowModalPropsType): JSX.Element {
const examples = useFlowsManagerStore((state) => state.examples);
examples.forEach((example) => {
examples?.forEach((example) => {
if (example.name === "Blog Writter") {
example.name = "Blog Writer";
}
@ -22,24 +22,17 @@ export default function NewFlowModal({
<span className="pr-2" data-testid="modal-title">
Get Started
</span>
{/* <IconComponent
name="Group"
className="h-6 w-6 stroke-2 text-primary "
aria-hidden="true"
/> */}
</BaseModal.Header>
<BaseModal.Content>
<div className=" mb-5 grid h-fit w-full grid-cols-3 gap-4 overflow-auto pb-6 custom-scroll">
<NewFlowCardComponent />
{/* {examples.map((example, idx) => {
return <UndrawCardComponent key={idx} flow={example} />;
})} */}
{examples.find((e) => e.name == "Basic Prompting (Hello, World)") && (
<UndrawCardComponent
key={1}
key={0}
flow={
examples.find(
(e) => e.name == "Basic Prompting (Hello, World)"
(e) => e.name == "Basic Prompting (Hello, World)",
)!
}
/>
@ -52,19 +45,19 @@ export default function NewFlowModal({
)}
{examples.find((e) => e.name == "Document QA") && (
<UndrawCardComponent
key={1}
key={2}
flow={examples.find((e) => e.name == "Document QA")!}
/>
)}
{examples.find((e) => e.name == "Blog Writer") && (
<UndrawCardComponent
key={1}
key={3}
flow={examples.find((e) => e.name == "Blog Writer")!}
/>
)}
{examples.find((e) => e.name == "Vector Store RAG") && (
<UndrawCardComponent
key={1}
key={4}
flow={examples.find((e) => e.name == "Vector Store RAG")!}
/>
)}

View file

@ -60,19 +60,19 @@ export default function Page({
}): JSX.Element {
const uploadFlow = useFlowsManagerStore((state) => state.uploadFlow);
const autoSaveCurrentFlow = useFlowsManagerStore(
(state) => state.autoSaveCurrentFlow
(state) => state.autoSaveCurrentFlow,
);
const types = useTypesStore((state) => state.types);
const templates = useTypesStore((state) => state.templates);
const setFilterEdge = useFlowStore((state) => state.setFilterEdge);
const reactFlowWrapper = useRef<HTMLDivElement>(null);
const [showCanvas, setSHowCanvas] = useState(
Object.keys(templates).length > 0 && Object.keys(types).length > 0
Object.keys(templates).length > 0 && Object.keys(types).length > 0,
);
const reactFlowInstance = useFlowStore((state) => state.reactFlowInstance);
const setReactFlowInstance = useFlowStore(
(state) => state.setReactFlowInstance
(state) => state.setReactFlowInstance,
);
const nodes = useFlowStore((state) => state.nodes);
const edges = useFlowStore((state) => state.edges);
@ -89,10 +89,10 @@ export default function Page({
const paste = useFlowStore((state) => state.paste);
const resetFlow = useFlowStore((state) => state.resetFlow);
const lastCopiedSelection = useFlowStore(
(state) => state.lastCopiedSelection
(state) => state.lastCopiedSelection,
);
const setLastCopiedSelection = useFlowStore(
(state) => state.setLastCopiedSelection
(state) => state.setLastCopiedSelection,
);
const onConnect = useFlowStore((state) => state.onConnect);
const currentFlowId = useFlowsManagerStore((state) => state.currentFlowId);
@ -115,7 +115,7 @@ export default function Page({
clonedSelection!,
clonedNodes,
clonedEdges,
getRandomName()
getRandomName(),
);
const newGroupNode = generateNodeFromFlow(newFlow, getNodeId);
const newEdges = reconnectEdges(newGroupNode, removedEdges);
@ -123,8 +123,8 @@ export default function Page({
...clonedNodes.filter(
(oldNodes) =>
!clonedSelection?.nodes.some(
(selectionNode) => selectionNode.id === oldNodes.id
)
(selectionNode) => selectionNode.id === oldNodes.id,
),
),
newGroupNode,
]);
@ -134,8 +134,8 @@ export default function Page({
!clonedSelection!.nodes.some(
(selectionNode) =>
selectionNode.id === oldEdge.target ||
selectionNode.id === oldEdge.source
)
selectionNode.id === oldEdge.source,
),
),
...newEdges,
]);
@ -206,7 +206,7 @@ export default function Page({
{
x: position.current.x,
y: position.current.y,
}
},
);
}
}
@ -265,20 +265,28 @@ export default function Page({
const groupAction = useShortcutsStore((state) => state.group);
const cutAction = useShortcutsStore((state) => state.cut);
const pasteAction = useShortcutsStore((state) => state.paste);
//@ts-ignore
useHotkeys(undoAction, handleUndo);
//@ts-ignore
useHotkeys(redoAction, handleRedo);
//@ts-ignore
useHotkeys(groupAction, handleGroup);
//@ts-ignore
useHotkeys(duplicate, handleDuplicate);
//@ts-ignore
useHotkeys(copyAction, handleCopy);
//@ts-ignore
useHotkeys(cutAction, handleCut);
//@ts-ignore
useHotkeys(pasteAction, handlePaste);
//@ts-ignore
useHotkeys(deleteAction, handleDelete);
//@ts-ignore
useHotkeys("delete", handleDelete);
useEffect(() => {
setSHowCanvas(
Object.keys(templates).length > 0 && Object.keys(types).length > 0
Object.keys(templates).length > 0 && Object.keys(types).length > 0,
);
}, [templates, types]);
@ -287,7 +295,7 @@ export default function Page({
takeSnapshot();
onConnect(params);
},
[takeSnapshot, onConnect]
[takeSnapshot, onConnect],
);
const onNodeDragStart: NodeDragHandler = useCallback(() => {
@ -328,7 +336,7 @@ export default function Page({
// Extract the data from the drag event and parse it as a JSON object
const data: { type: string; node?: APIClassType } = JSON.parse(
event.dataTransfer.getData("nodedata")
event.dataTransfer.getData("nodedata"),
);
const newId = getNodeId(data.type);
@ -344,7 +352,7 @@ export default function Page({
};
paste(
{ nodes: [newNode], edges: [] },
{ x: event.clientX, y: event.clientY }
{ x: event.clientX, y: event.clientY },
);
} else if (event.dataTransfer.types.some((types) => types === "Files")) {
takeSnapshot();
@ -373,7 +381,7 @@ export default function Page({
}
},
// Specify dependencies for useCallback
[getNodeId, setNodes, takeSnapshot, paste]
[getNodeId, setNodes, takeSnapshot, paste],
);
const onEdgeUpdateStart = useCallback(() => {
@ -389,7 +397,7 @@ export default function Page({
setEdges((els) => updateEdge(oldEdge, newConnection, els));
}
},
[setEdges]
[setEdges],
);
const onEdgeUpdateEnd = useCallback((_, edge: Edge): void => {
@ -422,7 +430,7 @@ export default function Page({
(flow: OnSelectionChangeParams): void => {
setLastSelection(flow);
},
[]
[],
);
const onPaneClick = useCallback((flow) => {
@ -486,6 +494,7 @@ export default function Page({
></Controls>
)}
<SelectionMenu
lastSelection={lastSelection}
isVisible={selectionMenuVisible}
nodes={lastSelection?.nodes}
onClick={() => {

View file

@ -1,11 +1,31 @@
import { useEffect, useState } from "react";
import { NodeToolbar } from "reactflow";
import { GradientGroup } from "../../../../icons/GradientSparkles";
export default function SelectionMenu({ onClick, nodes, isVisible }) {
import useFlowStore from "../../../../stores/flowStore";
import { validateSelection } from "../../../../utils/reactflowUtils";
export default function SelectionMenu({
onClick,
nodes,
isVisible,
lastSelection,
}) {
const edges = useFlowStore((state) => state.edges);
const [disable, setDisable] = useState<boolean>(
lastSelection && edges.length > 0
? validateSelection(lastSelection!, edges).length > 0
: false,
);
const [isOpen, setIsOpen] = useState(false);
const [isTransitioning, setIsTransitioning] = useState(false);
const [lastNodes, setLastNodes] = useState(nodes);
useEffect(() => {
if (isOpen) {
return setDisable(validateSelection(lastSelection!, edges).length > 0);
}
setDisable(false);
}, [isOpen, setIsOpen]);
// nodes get saved to not be gone after the toolbar closes
useEffect(() => {
setLastNodes(nodes);
@ -42,13 +62,19 @@ export default function SelectionMenu({ onClick, nodes, isVisible }) {
}
>
<button
className="flex h-full w-full items-center justify-between text-sm hover:text-indigo-500"
className={`${
disable
? "flex h-full w-full cursor-not-allowed items-center justify-between text-sm text-muted-foreground"
: "flex h-full w-full items-center justify-between text-sm hover:text-indigo-500"
}`}
onClick={onClick}
disabled={disable}
>
<GradientGroup
strokeWidth={1.5}
size={22}
className="text-primary"
disabled={disable}
/>
Group
</button>

View file

@ -3,14 +3,14 @@ import { useParams } from "react-router-dom";
import FlowToolbar from "../../components/chatComponent";
import Header from "../../components/headerComponent";
import { useDarkStore } from "../../stores/darkStore";
import useFlowStore from "../../stores/flowStore";
import useFlowsManagerStore from "../../stores/flowsManagerStore";
import Page from "./components/PageComponent";
import ExtraSidebar from "./components/extraSidebarComponent";
import useFlowStore from "../../stores/flowStore";
export default function FlowPage({ view }: { view?: boolean }): JSX.Element {
const setCurrentFlowId = useFlowsManagerStore(
(state) => state.setCurrentFlowId
(state) => state.setCurrentFlowId,
);
const version = useDarkStore((state) => state.version);
const setOnFlowPage = useFlowStore((state) => state.setOnFlowPage);
@ -21,7 +21,7 @@ export default function FlowPage({ view }: { view?: boolean }): JSX.Element {
useEffect(() => {
setCurrentFlowId(id!);
setOnFlowPage(true);
return () => {
setOnFlowPage(false);
};

View file

@ -1,207 +0,0 @@
import { useState } from "react";
import { Link, useNavigate } from "react-router-dom";
import CollectionCardComponent from "../../../../components/cardComponent";
import CardsWrapComponent from "../../../../components/cardsWrapComponent";
import IconComponent from "../../../../components/genericIconComponent";
import PaginatorComponent from "../../../../components/paginatorComponent";
import { SkeletonCardComponent } from "../../../../components/skeletonCardComponent";
import { Button } from "../../../../components/ui/button";
import {
CONSOLE_ERROR_MSG,
UPLOAD_ALERT_LIST,
WRONG_FILE_ERROR_ALERT,
} from "../../../../constants/alerts_constants";
import useAlertStore from "../../../../stores/alertStore";
import useFlowsManagerStore from "../../../../stores/flowsManagerStore";
import { FlowType } from "../../../../types/flow";
export default function ComponentsComponent({
is_component = true,
}: {
is_component?: boolean;
}) {
const addFlow = useFlowsManagerStore((state) => state.addFlow);
const uploadFlow = useFlowsManagerStore((state) => state.uploadFlow);
const removeFlow = useFlowsManagerStore((state) => state.removeFlow);
const isLoading = useFlowsManagerStore((state) => state.isLoading);
const flows = useFlowsManagerStore((state) => state.flows);
const setSuccessData = useAlertStore((state) => state.setSuccessData);
const setErrorData = useAlertStore((state) => state.setErrorData);
const [pageSize, setPageSize] = useState(20);
const [pageIndex, setPageIndex] = useState(1);
const navigate = useNavigate();
const all: FlowType[] = flows
.filter((f) => (f.is_component ?? false) === is_component)
.sort((a, b) => {
if (a?.updated_at && b?.updated_at) {
return (
new Date(b?.updated_at!).getTime() -
new Date(a?.updated_at!).getTime()
);
} else if (a?.updated_at && !b?.updated_at) {
return 1;
} else if (!a?.updated_at && b?.updated_at) {
return -1;
} else {
return (
new Date(b?.date_created!).getTime() -
new Date(a?.date_created!).getTime()
);
}
});
const start = (pageIndex - 1) * pageSize;
const end = start + pageSize;
const data: FlowType[] = all.slice(start, end);
const name = is_component ? "Component" : "Flow";
const onFileDrop = (e) => {
e.preventDefault();
if (e.dataTransfer.types.some((types) => types === "Files")) {
if (e.dataTransfer.files.item(0).type === "application/json") {
uploadFlow({
newProject: true,
file: e.dataTransfer.files.item(0)!,
isComponent: is_component,
})
.then(() => {
setSuccessData({
title: `${
is_component ? "Component" : "Flow"
} uploaded successfully`,
});
})
.catch((error) => {
setErrorData({
title: CONSOLE_ERROR_MSG,
list: [error],
});
});
} else {
setErrorData({
title: WRONG_FILE_ERROR_ALERT,
list: [UPLOAD_ALERT_LIST],
});
}
}
};
function resetFilter() {
setPageIndex(1);
setPageSize(20);
}
return (
<CardsWrapComponent
onFileDrop={onFileDrop}
dragMessage={`Drag your ${name} here`}
>
<div className="flex h-full w-full flex-col justify-between">
<div className="flex w-full flex-col gap-4">
{!isLoading && data.length === 0 ? (
<div className="mt-6 flex w-full items-center justify-center text-center">
<div className="flex-max-width h-full flex-col">
<div className="flex w-full flex-col gap-4">
<div className="grid w-full gap-4">
Flows and components can be created using Langflow.
</div>
<div className="align-center flex w-full justify-center gap-1">
<span>New?</span>
<span className="transition-colors hover:text-muted-foreground">
<button
onClick={() => {
addFlow(true).then((id) => {
navigate("/flow/" + id);
});
}}
className="underline"
>
Start Here
</button>
.
</span>
<span className="animate-pulse">🚀</span>
</div>
</div>
</div>
</div>
) : (
<div className="grid w-full gap-4 md:grid-cols-2 lg:grid-cols-2">
{isLoading === false && data?.length > 0 ? (
data?.map((item, idx) => (
<CollectionCardComponent
onDelete={() => {
removeFlow(item.id);
setSuccessData({
title: `${
item.is_component ? "Component" : "Flow"
} deleted successfully!`,
});
resetFilter();
}}
key={idx}
data={{ is_component: item.is_component ?? false, ...item }}
disabled={isLoading}
data-testid={"edit-flow-button-" + item.id + "-" + idx}
button={
!is_component ? (
<Link to={"/flow/" + item.id}>
<Button
tabIndex={-1}
variant="outline"
size="sm"
className="whitespace-nowrap "
data-testid={
"edit-flow-button-" + item.id + "-" + idx
}
>
<IconComponent
name="ExternalLink"
className="main-page-nav-button select-none"
/>
Edit Flow
</Button>
</Link>
) : (
<></>
)
}
onClick={
!is_component
? () => {
navigate("/flow/" + item.id);
}
: undefined
}
playground={!is_component}
/>
))
) : (
<>
<SkeletonCardComponent />
<SkeletonCardComponent />
</>
)}
</div>
)}
</div>
{!isLoading && data.length > 0 && (
<div className="relative py-6">
<PaginatorComponent
storeComponent={true}
pageIndex={pageIndex}
pageSize={pageSize}
rowsCount={[10, 20, 50, 100]}
totalRowsCount={
flows.filter((f) => (f.is_component ?? false) === is_component)
.length
}
paginate={(pageSize, pageIndex) => {
setPageIndex(pageIndex);
setPageSize(pageSize);
}}
></PaginatorComponent>
</div>
)}
</div>
</CardsWrapComponent>
);
}

View file

@ -0,0 +1,303 @@
import { cloneDeep } from "lodash";
import { useEffect, useMemo, useState } from "react";
import { FormProvider, useForm, useWatch } from "react-hook-form";
import { Link, useLocation, useNavigate } from "react-router-dom";
import CollectionCardComponent from "../../../../components/cardComponent";
import CardsWrapComponent from "../../../../components/cardsWrapComponent";
import IconComponent from "../../../../components/genericIconComponent";
import PaginatorComponent from "../../../../components/paginatorComponent";
import { SkeletonCardComponent } from "../../../../components/skeletonCardComponent";
import { Button } from "../../../../components/ui/button";
import DeleteConfirmationModal from "../../../../modals/deleteConfirmationModal";
import useAlertStore from "../../../../stores/alertStore";
import useFlowsManagerStore from "../../../../stores/flowsManagerStore";
import { useFolderStore } from "../../../../stores/foldersStore";
import { FlowType } from "../../../../types/flow";
import useFileDrop from "../../hooks/use-on-file-drop";
import { getNameByType } from "../../utils/get-name-by-type";
import { sortFlows } from "../../utils/sort-flows";
import EmptyComponent from "../emptyComponent";
import HeaderComponent from "../headerComponent";
export default function ComponentsComponent({
type = "all",
}: {
type?: string;
}) {
const uploadFlow = useFlowsManagerStore((state) => state.uploadFlow);
const removeFlow = useFlowsManagerStore((state) => state.removeFlow);
const isLoading = useFlowsManagerStore((state) => state.isLoading);
const setAllFlows = useFlowsManagerStore((state) => state.setAllFlows);
const allFlows = useFlowsManagerStore((state) => state.allFlows);
const flowsFromFolder = useFolderStore(
(state) => state.selectedFolder?.flows,
);
const setSuccessData = useAlertStore((state) => state.setSuccessData);
const setErrorData = useAlertStore((state) => state.setErrorData);
const [openDelete, setOpenDelete] = useState(false);
const searchFlowsComponents = useFlowsManagerStore(
(state) => state.searchFlowsComponents,
);
const setSelectedFlowsComponentsCards = useFlowsManagerStore(
(state) => state.setSelectedFlowsComponentsCards,
);
const selectedFlowsComponentsCards = useFlowsManagerStore(
(state) => state.selectedFlowsComponentsCards,
);
const [handleFileDrop] = useFileDrop(uploadFlow, type)!;
const [pageSize, setPageSize] = useState(20);
const [pageIndex, setPageIndex] = useState(1);
const navigate = useNavigate();
const location = useLocation();
const all: FlowType[] = sortFlows(allFlows, type);
const start = (pageIndex - 1) * pageSize;
const end = start + pageSize;
const data: FlowType[] = all?.slice(start, end);
const name = getNameByType(type);
const folderId = location?.state?.folderId;
const getFolderById = useFolderStore((state) => state.getFolderById);
const myCollectionId = useFolderStore((state) => state.myCollectionId);
const getFoldersApi = useFolderStore((state) => state.getFoldersApi);
const setFolderUrl = useFolderStore((state) => state.setFolderUrl);
useEffect(() => {
setFolderUrl(folderId ?? "");
setSelectedFlowsComponentsCards([]);
handleSelectAll(false);
getFolderById(folderId ? folderId : myCollectionId);
}, [location]);
useEffect(() => {
const newFlows = cloneDeep(flowsFromFolder!);
const filteredFlows = newFlows?.filter(
(f) =>
f.name.toLowerCase().includes(searchFlowsComponents.toLowerCase()) ||
f.description
.toLowerCase()
.includes(searchFlowsComponents.toLowerCase()),
);
if (searchFlowsComponents === "") {
setAllFlows(flowsFromFolder!);
}
setAllFlows(filteredFlows);
}, [searchFlowsComponents]);
const resetFilter = () => {
setPageIndex(1);
setPageSize(20);
};
const { getValues, control, setValue } = useForm();
const entireFormValues = useWatch({ control });
const methods = useForm();
const handleSelectAll = (select) => {
const flowsFromFolderIds = flowsFromFolder?.map((f) => f.id);
if (select) {
Object.keys(getValues()).forEach((key) => {
if (!flowsFromFolderIds?.includes(key)) return;
setValue(key, true);
});
return;
}
Object.keys(getValues()).forEach((key) => {
setValue(key, false);
});
};
const handleSelectOptionsChange = () => {
const hasSelected = selectedFlowsComponentsCards?.length > 0;
if (!hasSelected) {
setErrorData({
title: "No items selected",
list: ["Please select items to delete"],
});
return;
}
setOpenDelete(true);
};
const handleDeleteMultiple = () => {
removeFlow(selectedFlowsComponentsCards)
.then(() => {
resetFilter();
getFoldersApi(true);
if (!folderId || folderId === myCollectionId) {
getFolderById(folderId ? folderId : myCollectionId);
}
setSuccessData({
title: "Selected items deleted successfully!",
});
})
.catch(() => {
setErrorData({
title: "Error deleting items",
list: ["Please try again"],
});
});
};
useEffect(() => {
if (!entireFormValues || Object.keys(entireFormValues).length === 0) return;
const selectedFlows: string[] = Object.keys(entireFormValues).filter(
(key) => {
if (entireFormValues[key] === true) {
return true;
}
return false;
},
);
setSelectedFlowsComponentsCards(selectedFlows);
}, [entireFormValues]);
const getDescriptionModal = useMemo(() => {
const getTypeLabel = (type) => {
const labels = {
all: "item",
component: "component",
flow: "flow",
};
return labels[type] || "";
};
const getPluralizedLabel = (type) => {
const labels = {
all: "items",
component: "components",
flow: "flows",
};
return labels[type] || "";
};
if (selectedFlowsComponentsCards?.length === 1) {
return getTypeLabel(type);
}
return getPluralizedLabel(type);
}, [selectedFlowsComponentsCards, type]);
const getTotalRowsCount = () => {
if (type === "all") return allFlows?.length;
return allFlows?.filter(
(f) => (f.is_component ?? false) === (type === "component"),
)?.length;
};
return (
<>
{allFlows?.length > 0 && (
<HeaderComponent
handleDelete={handleSelectOptionsChange}
handleSelectAll={handleSelectAll}
disableDelete={!(selectedFlowsComponentsCards?.length > 0)}
/>
)}
<CardsWrapComponent
onFileDrop={handleFileDrop}
dragMessage={`Drag your ${name} here`}
>
<div className="flex h-full w-full flex-col justify-between">
<div className="flex w-full flex-col gap-4">
{!isLoading && data?.length === 0 ? (
<EmptyComponent />
) : (
<div className="grid w-full gap-4 md:grid-cols-2 lg:grid-cols-2">
{isLoading === false && data?.length > 0 ? (
<>
{data?.map((item) => (
<FormProvider {...methods} key={item.id}>
<form>
<CollectionCardComponent
is_component={type === "component"}
data={{
is_component: item.is_component ?? false,
...item,
}}
disabled={isLoading}
data-testid={"edit-flow-button-" + item.id}
button={
!item.is_component ? (
<Link to={"/flow/" + item.id}>
<Button
tabIndex={-1}
variant="outline"
size="sm"
className="whitespace-nowrap"
data-testid={"edit-flow-button-" + item.id}
>
<IconComponent
name="ExternalLink"
className="main-page-nav-button select-none"
/>
Edit Flow
</Button>
</Link>
) : (
<></>
)
}
onClick={
!item.is_component
? () => {
navigate("/flow/" + item.id);
}
: undefined
}
playground={!item.is_component}
control={control}
/>
</form>
</FormProvider>
))}
</>
) : (
<>
<SkeletonCardComponent />
<SkeletonCardComponent />
</>
)}
</div>
)}
</div>
{!isLoading && data?.length > 0 && (
<div className="relative py-6">
<PaginatorComponent
storeComponent={true}
pageIndex={pageIndex}
pageSize={pageSize}
rowsCount={[10, 20, 50, 100]}
totalRowsCount={getTotalRowsCount()}
paginate={(pageSize, pageIndex) => {
setPageIndex(pageIndex);
setPageSize(pageSize);
}}
></PaginatorComponent>
</div>
)}
</div>
</CardsWrapComponent>
{openDelete && (
<DeleteConfirmationModal
open={openDelete}
setOpen={setOpenDelete}
onConfirm={handleDeleteMultiple}
description={getDescriptionModal}
>
<></>
</DeleteConfirmationModal>
)}
</>
);
}

View file

@ -0,0 +1,41 @@
import { useNavigate } from "react-router-dom";
import useFlowsManagerStore from "../../../../stores/flowsManagerStore";
type EmptyComponentProps = {};
const EmptyComponent = ({}: EmptyComponentProps) => {
const addFlow = useFlowsManagerStore((state) => state.addFlow);
const navigate = useNavigate();
return (
<>
<div className="mt-6 flex w-full items-center justify-center text-center">
<div className="flex-max-width h-full flex-col">
<div className="flex w-full flex-col gap-4">
<div className="grid w-full gap-4 text-muted-foreground">
Flows and components can be created using Langflow.
</div>
<div className="align-center flex w-full justify-center gap-1 ">
<span className="text-muted-foreground">New?</span>
<span className="transition-colors hover:text-muted-foreground">
<button
onClick={() => {
addFlow(true).then((id) => {
navigate("/flow/" + id);
});
}}
className="underline"
>
Start Here
</button>
.
</span>
<span className="animate-pulse">🚀</span>
</div>
</div>
</div>
</div>
</>
);
};
export default EmptyComponent;

View file

@ -0,0 +1,85 @@
import { useState } from "react";
import IconComponent from "../../../../components/genericIconComponent";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from "../../../../components/ui/select";
import { Checkbox } from "../../../../components/ui/checkbox";
import { Button } from "../../../../components/ui/button";
import { cn } from "../../../../utils/utils";
import ShadTooltip from "../../../../components/shadTooltipComponent";
type HeaderComponentProps = {
handleSelectAll: (select) => void;
handleDelete: () => void;
disableDelete: boolean;
};
const HeaderComponent = ({
handleSelectAll,
handleDelete,
disableDelete,
}: HeaderComponentProps) => {
const [shouldSelectAll, setShouldSelectAll] = useState(true);
const handleClick = () => {
handleSelectAll(shouldSelectAll);
setShouldSelectAll((prevState) => !prevState);
};
return (
<>
<div className="grid grid-cols-3 pb-5">
<div className="col-auto grid-cols-1 self-center justify-self-start">
<a onClick={handleClick} className="text-sm">
<div className="header-menu-bar-display ">
<div
className="header-menu-flow-name"
data-testid="select_all_collection"
>
<div className="flex items-center space-x-2">
<Checkbox checked={!shouldSelectAll} id="terms" />
<label
onClick={handleClick}
htmlFor="terms"
className="label cursor-pointer text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
{shouldSelectAll ? "Select All" : "Unselect All"}
</label>
</div>
</div>
</div>
</a>
</div>
<div className="col-span-2 grid-cols-1 justify-self-end">
<div>
<ShadTooltip
content={
disableDelete ? (
<span>Select items to delete</span>
) : (
<span>Delete selected items</span>
)
}
>
<button onClick={handleDelete} disabled={disableDelete}>
<IconComponent
name="Trash2"
className={cn(
"h-5 w-5 text-primary transition-all",
disableDelete ? "" : "hover:text-destructive",
)}
/>
</button>
</ShadTooltip>
</div>
</div>
</div>
</>
);
};
export default HeaderComponent;

View file

@ -0,0 +1,58 @@
// Modals.tsx
import IconComponent from "../../../../components/genericIconComponent";
import { Button } from "../../../../components/ui/button";
import DeleteConfirmationModal from "../../../../modals/deleteConfirmationModal";
import FoldersModal from "../../../../modals/foldersModal";
import NewFlowModal from "../../../../modals/newFlowModal";
import { cn } from "../../../../utils/utils";
interface ModalsProps {
openModal: boolean;
setOpenModal: (value: boolean) => void;
openFolderModal: boolean;
setOpenFolderModal: (value: boolean) => void;
openDeleteFolderModal: boolean;
setOpenDeleteFolderModal: (value: boolean) => void;
handleDeleteFolder: () => void;
}
const ModalsComponent = ({
openModal,
setOpenModal,
openFolderModal,
setOpenFolderModal,
openDeleteFolderModal,
setOpenDeleteFolderModal,
handleDeleteFolder,
}: ModalsProps) => (
<>
{openModal && <NewFlowModal open={openModal} setOpen={setOpenModal} />}
{openFolderModal && (
<FoldersModal open={openFolderModal} setOpen={setOpenFolderModal} />
)}
{openDeleteFolderModal && (
<DeleteConfirmationModal
open={openDeleteFolderModal}
setOpen={setOpenDeleteFolderModal}
onConfirm={() => {
handleDeleteFolder();
setOpenDeleteFolderModal(false);
}}
description="folder"
note={
"Deleting the selected folder will remove all associated flows and components."
}
>
<Button variant="ghost" size="icon" className={"whitespace-nowrap"}>
<IconComponent
data-testid={`delete-folder`}
name="Trash2"
className={cn("h-5 w-5")}
/>
</Button>
</DeleteConfirmationModal>
)}
</>
);
export default ModalsComponent;

View file

@ -0,0 +1,49 @@
import { useState } from "react";
import useFlowsManagerStore from "../../../../../../stores/flowsManagerStore";
import InputSearchComponent from "../inputSearchComponent";
import TabsSearchComponent from "../tabsComponent";
import { Button } from "../../../../../../components/ui/button";
import ForwardedIconComponent from "../../../../../../components/genericIconComponent";
import useAlertStore from "../../../../../../stores/alertStore";
import { handleDownloadFolderFn } from "../../../../utils/handle-download-folder";
import { useFolderStore } from "../../../../../../stores/foldersStore";
import { useLocation } from "react-router-dom";
type HeaderTabsSearchComponentProps = {};
const HeaderTabsSearchComponent = ({}: HeaderTabsSearchComponentProps) => {
const location = useLocation();
const myCollectionId = useFolderStore((state) => state.myCollectionId);
const folderId = location?.state?.folderId || myCollectionId;
const isLoading = useFlowsManagerStore((state) => state.isLoading);
const [tabActive, setTabActive] = useState("Flows");
const setErrorData = useAlertStore((state) => state.setErrorData);
const allFlows = useFlowsManagerStore((state) => state.allFlows);
const handleDownloadFolder = () => {
if (allFlows.length === 0) {
setErrorData({
title: "Folder is empty",
list: [],
});
return;
}
handleDownloadFolderFn(folderId);
};
return (
<>
<div className="relative flex items-end gap-4">
<InputSearchComponent loading={isLoading} />
<TabsSearchComponent
tabsOptions={["All", "Flows", "Components"]}
setActiveTab={setTabActive}
loading={isLoading}
tabActive={tabActive}
/>
</div>
</>
);
};
export default HeaderTabsSearchComponent;

View file

@ -0,0 +1,72 @@
import { useState } from "react";
import { Input } from "../../../../../../components/ui/input";
import useFlowsManagerStore from "../../../../../../stores/flowsManagerStore";
import ForwardedIconComponent from "../../../../../../components/genericIconComponent";
type InputSearchComponentProps = {
loading: boolean;
};
const InputSearchComponent = ({ loading }: InputSearchComponentProps) => {
const pagePath = window.location.pathname;
const [inputValue, setInputValue] = useState("");
const allFlows = useFlowsManagerStore((state) => state.allFlows);
const setSearchFlowsComponents = useFlowsManagerStore(
(state) => state.setSearchFlowsComponents,
);
const searchFlowsComponents = useFlowsManagerStore(
(state) => state.searchFlowsComponents,
);
const disableInputSearch =
loading ||
!allFlows ||
(allFlows?.length === 0 && searchFlowsComponents === "");
const getSearchPlaceholder = () => {
if (pagePath.includes("flows")) {
return "Search Flows";
} else if (pagePath.includes("components")) {
return "Search Components";
} else {
return "Search Flows and Components";
}
};
return (
<>
<div className="relative h-12 w-[60%]">
<Input
data-testid="search-store-input"
disabled={disableInputSearch}
placeholder={getSearchPlaceholder()}
className="absolute h-12 pl-5 pr-12"
onChange={(e) => {
setSearchFlowsComponents(e.target.value);
setInputValue(e.target.value);
}}
onKeyDown={(e) => {
if (e.key === "Enter") {
setSearchFlowsComponents(inputValue);
}
}}
value={inputValue}
/>
<button
disabled={loading}
className="absolute bottom-0 right-4 top-0 my-auto h-6 cursor-pointer stroke-1 text-muted-foreground"
data-testid="search-store-button"
>
<ForwardedIconComponent
name={loading ? "Loader2" : "Search"}
className={loading ? " animate-spin cursor-not-allowed" : ""}
/>
</button>
</div>
</>
);
};
export default InputSearchComponent;

View file

@ -0,0 +1,77 @@
import { useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { useFolderStore } from "../../../../../../stores/foldersStore";
type TabsSearchComponentProps = {
tabsOptions: string[];
setActiveTab: (tab: string) => void;
loading: boolean;
tabActive: string;
};
const TabsSearchComponent = ({
tabsOptions,
setActiveTab,
loading,
tabActive,
}: TabsSearchComponentProps) => {
const navigate = useNavigate();
const folderUrl = useFolderStore((state) => state.folderUrl);
const changeLocation = (tabOption) => {
const location = window.location.pathname;
let newLocation = "";
switch (tabOption) {
case "Flows":
newLocation = location.replace(/components|all/, "flows");
break;
case "Components":
newLocation = location.replace(/flows|all/, "components");
break;
default:
newLocation = location.replace(/flows|components/, "all");
break;
}
navigate(newLocation, { state: { folderId: folderUrl } });
};
useEffect(() => {
const path = window.location.pathname;
if (path.includes("components")) {
setActiveTab("Components");
} else if (path.includes("flows")) {
setActiveTab("Flows");
} else {
setActiveTab("All");
}
}, [window.location.pathname]);
return (
<>
<div className="ml-4 flex w-full gap-2 border-b border-border">
{tabsOptions.map((tabOption, index) => {
return (
<button
key={index}
data-testid={`${tabOption}-button-store`}
disabled={loading}
onClick={() => {
changeLocation(tabOption);
}}
className={
(tabActive === tabOption
? "border-b-2 border-primary p-3"
: " border-b-2 border-transparent p-3 text-muted-foreground hover:text-primary") +
(loading ? " cursor-not-allowed " : "")
}
>
{tabOption}
</button>
);
})}
</div>
</>
);
};
export default TabsSearchComponent;

View file

@ -0,0 +1,19 @@
import ComponentsComponent from "../componentsComponent";
import HeaderTabsSearchComponent from "./components/headerTabsSearchComponent";
type MyCollectionComponentProps = {
key: string;
type: string;
};
const MyCollectionComponent = ({ key, type }: MyCollectionComponentProps) => {
return (
<>
<HeaderTabsSearchComponent />
<div className="mt-5 flex h-full flex-col">
<ComponentsComponent key={key} type={type} />
</div>
</>
);
};
export default MyCollectionComponent;

View file

@ -0,0 +1,26 @@
import { FlowType } from "../../../types/flow";
export type FolderType = {
name: string;
description: string;
id?: string | null;
parent_id: string;
flows: FlowType[];
};
export type AddFolderType = {
name: string;
description: string;
id?: string | null;
parent_id: string;
flows?: string[];
components?: string[];
};
export type StarterProjectsType = {
name?: string;
description?: string;
flows?: FlowType[];
id: string;
parent_id: string;
};

View file

@ -0,0 +1,32 @@
import useAlertStore from "../../../stores/alertStore";
import { useFolderStore } from "../../../stores/foldersStore";
import { deleteFolder, getFolderById } from "../services";
const useDeleteFolder = ({ navigate, getFoldersApi }) => {
const setSuccessData = useAlertStore((state) => state.setSuccessData);
const setErrorData = useAlertStore((state) => state.setErrorData);
const folderToEdit = useFolderStore((state) => state.folderToEdit);
const myCollectionId = useFolderStore((state) => state.myCollectionId);
const handleDeleteFolder = () => {
deleteFolder(folderToEdit?.id!)
.then(() => {
setSuccessData({
title: "Folder deleted successfully.",
});
getFolderById(myCollectionId!);
getFoldersApi(true);
navigate("/all");
})
.catch((err) => {
console.error(err);
setErrorData({
title: "Error deleting folder.",
});
});
};
return { handleDeleteFolder };
};
export default useDeleteFolder;

View file

@ -0,0 +1,36 @@
import { CONSOLE_ERROR_MSG } from "../../../constants/alerts_constants";
import useAlertStore from "../../../stores/alertStore";
const useDropdownOptions = ({ uploadFlow, navigate, is_component }) => {
const setSuccessData = useAlertStore((state) => state.setSuccessData);
const setErrorData = useAlertStore((state) => state.setErrorData);
const handleImportFromJSON = () => {
uploadFlow({
newProject: true,
isComponent: is_component,
})
.then((id) => {
setSuccessData({
title: `${is_component ? "Component" : "Flow"} uploaded successfully`,
});
if (!is_component) navigate("/flow/" + id);
})
.catch((error) => {
setErrorData({
title: CONSOLE_ERROR_MSG,
list: [error],
});
});
};
const dropdownOptions = [
{
name: "Import from JSON",
onBtnClick: handleImportFromJSON,
},
];
return [...dropdownOptions];
};
export default useDropdownOptions;

View file

@ -0,0 +1,64 @@
import { useLocation } from "react-router-dom";
import {
CONSOLE_ERROR_MSG,
UPLOAD_ALERT_LIST,
WRONG_FILE_ERROR_ALERT,
} from "../../../constants/alerts_constants";
import useAlertStore from "../../../stores/alertStore";
import { useFolderStore } from "../../../stores/foldersStore";
const useFileDrop = (uploadFlow, type) => {
const location = useLocation();
const setSuccessData = useAlertStore((state) => state.setSuccessData);
const setErrorData = useAlertStore((state) => state.setErrorData);
const getFolderById = useFolderStore((state) => state.getFolderById);
const folderId = location?.state?.folderId;
const myCollectionId = useFolderStore((state) => state.myCollectionId);
const handleFileDrop = (e) => {
e.preventDefault();
if (e.dataTransfer.types.some((type) => type === "Files")) {
const file = e.dataTransfer.files.item(0);
if (file.type === "application/json") {
const reader = new FileReader();
reader.onload = (event) => {
const fileContent = event.target!.result;
const fileContentJson = JSON.parse(fileContent as string);
const is_component = fileContentJson.is_component;
uploadFlow({
newProject: true,
file: file,
isComponent: type === "all" ? null : type === "component",
})
.then(() => {
setSuccessData({
title: `${
is_component ? "Component" : "Flow"
} uploaded successfully`,
});
getFolderById(folderId ? folderId : myCollectionId);
})
.catch((error) => {
setErrorData({
title: CONSOLE_ERROR_MSG,
list: [error],
});
});
};
reader.readAsText(file);
} else {
setErrorData({
title: WRONG_FILE_ERROR_ALERT,
list: [UPLOAD_ALERT_LIST],
});
}
}
};
return [handleFileDrop];
};
export default useFileDrop;

View file

@ -1,121 +0,0 @@
import { Group, ToyBrick } from "lucide-react";
import { useEffect, useState } from "react";
import { Outlet, useLocation, useNavigate } from "react-router-dom";
import DropdownButton from "../../components/dropdownButtonComponent";
import IconComponent from "../../components/genericIconComponent";
import PageLayout from "../../components/pageLayout";
import SidebarNav from "../../components/sidebarComponent";
import { Button } from "../../components/ui/button";
import { CONSOLE_ERROR_MSG } from "../../constants/alerts_constants";
import {
MY_COLLECTION_DESC,
USER_PROJECTS_HEADER,
} from "../../constants/constants";
import NewFlowModal from "../../modals/newFlowModal";
import useAlertStore from "../../stores/alertStore";
import useFlowsManagerStore from "../../stores/flowsManagerStore";
import { downloadFlows } from "../../utils/reactflowUtils";
export default function HomePage(): JSX.Element {
const uploadFlow = useFlowsManagerStore((state) => state.uploadFlow);
const setCurrentFlowId = useFlowsManagerStore(
(state) => state.setCurrentFlowId
);
const uploadFlows = useFlowsManagerStore((state) => state.uploadFlows);
const setSuccessData = useAlertStore((state) => state.setSuccessData);
const setErrorData = useAlertStore((state) => state.setErrorData);
const location = useLocation();
const pathname = location.pathname;
const [openModal, setOpenModal] = useState(false);
const is_component = pathname === "/components";
const dropdownOptions = [
{
name: "Import from JSON",
onBtnClick: () => {
uploadFlow({
newProject: true,
isComponent: is_component,
})
.then((id) => {
setSuccessData({
title: `${
is_component ? "Component" : "Flow"
} uploaded successfully`,
});
if (!is_component) navigate("/flow/" + id);
})
.catch((error) => {
setErrorData({
title: CONSOLE_ERROR_MSG,
list: [error],
});
});
},
},
];
const sidebarNavItems = [
{
title: "Flows",
href: "/flows",
icon: <Group className="w-5 stroke-[1.5]" />,
},
{
title: "Components",
href: "/components",
icon: <ToyBrick className="mx-[0.08rem] w-[1.1rem] stroke-[1.5]" />,
},
];
// Set a null id
useEffect(() => {
setCurrentFlowId("");
}, [pathname]);
const navigate = useNavigate();
// Personal flows display
return (
<PageLayout
title={USER_PROJECTS_HEADER}
description={MY_COLLECTION_DESC}
button={
<div className="flex gap-2">
<Button
variant="primary"
onClick={() => {
downloadFlows();
}}
>
<IconComponent name="Download" className="main-page-nav-button" />
Download Collection
</Button>
<Button
variant="primary"
onClick={() => {
uploadFlows();
}}
>
<IconComponent name="Upload" className="main-page-nav-button" />
Upload Collection
</Button>
<DropdownButton
firstButtonName="New Project"
onFirstBtnClick={() => setOpenModal(true)}
options={dropdownOptions}
plusButton={true}
dropdownOptions={false}
/>
</div>
}
>
<div className="flex h-full w-full space-y-8 lg:flex-row lg:space-x-8 lg:space-y-0">
<aside className="flex h-full flex-col space-y-6 lg:w-1/5">
<SidebarNav items={sidebarNavItems} />
</aside>
<div className="h-full w-full flex-1">
<Outlet />
</div>
</div>
<NewFlowModal open={openModal} setOpen={setOpenModal} />
</PageLayout>
);
}

Some files were not shown because too many files have changed in this diff Show more