Merge dev into shortcuts_settings
This commit is contained in:
commit
ec5d5f2b38
275 changed files with 19166 additions and 14947 deletions
1043
src/frontend/package-lock.json
generated
1043
src/frontend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
9
src/frontend/src/boilerplate/raw-component/index.tsx
Normal file
9
src/frontend/src/boilerplate/raw-component/index.tsx
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
type RawComponentProps = {};
|
||||
const RawComponent = ({}: RawComponentProps) => {
|
||||
return (
|
||||
<>
|
||||
<div>RawComponent</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default RawComponent;
|
||||
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
12
src/frontend/src/components/arrayReaderComponent/index.tsx
Normal file
12
src/frontend/src/components/arrayReaderComponent/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 ? (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -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>();
|
||||
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
/>
|
||||
|
|
|
|||
21
src/frontend/src/components/dateReaderComponent/index.tsx
Normal file
21
src/frontend/src/components/dateReaderComponent/index.tsx
Normal 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>;
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ export default function FloatComponent({
|
|||
// Clear component state
|
||||
useEffect(() => {
|
||||
if (disabled && value !== "") {
|
||||
onChange("");
|
||||
onChange("", true);
|
||||
}
|
||||
}, [disabled]);
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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()}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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 !== ""
|
||||
|
|
|
|||
|
|
@ -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"}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ export default function IntComponent({
|
|||
// Clear component state
|
||||
useEffect(() => {
|
||||
if (disabled && value !== "") {
|
||||
onChange("");
|
||||
onChange("", true);
|
||||
}
|
||||
}, [disabled, onChange]);
|
||||
|
||||
|
|
|
|||
7
src/frontend/src/components/numberReader/index.tsx
Normal file
7
src/frontend/src/components/numberReader/index.tsx
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
export default function NumberReader({
|
||||
number,
|
||||
}: {
|
||||
number: number;
|
||||
}): JSX.Element {
|
||||
return <span>{number}</span>;
|
||||
}
|
||||
13
src/frontend/src/components/objectRender/index.tsx
Normal file
13
src/frontend/src/components/objectRender/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -18,7 +18,7 @@ export default function PromptAreaComponent({
|
|||
}: PromptAreaComponentType): JSX.Element {
|
||||
useEffect(() => {
|
||||
if (disabled && value !== "") {
|
||||
onChange("");
|
||||
onChange("", true);
|
||||
}
|
||||
}, [disabled]);
|
||||
|
||||
|
|
|
|||
34
src/frontend/src/components/recordsOutputComponent/index.tsx
Normal file
34
src/frontend/src/components/recordsOutputComponent/index.tsx
Normal 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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
export default function StringReader({
|
||||
string,
|
||||
}: {
|
||||
string: string;
|
||||
}): JSX.Element {
|
||||
return <span className="truncate">{string}</span>;
|
||||
}
|
||||
60
src/frontend/src/components/tableAutoCellRender/index.tsx
Normal file
60
src/frontend/src/components/tableAutoCellRender/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ export default function TextAreaComponent({
|
|||
// Clear text area
|
||||
useEffect(() => {
|
||||
if (disabled && value !== "") {
|
||||
onChange("");
|
||||
onChange("", true);
|
||||
}
|
||||
}, [disabled]);
|
||||
|
||||
|
|
|
|||
58
src/frontend/src/components/ui/alert.tsx
Normal file
58
src/frontend/src/components/ui/alert.tsx
Normal 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 };
|
||||
|
|
@ -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 && (
|
||||
|
|
|
|||
55
src/frontend/src/components/ui/custom-accordion.tsx
Normal file
55
src/frontend/src/components/ui/custom-accordion.tsx
Normal 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 };
|
||||
176
src/frontend/src/components/ui/form.tsx
Normal file
176
src/frontend/src/components/ui/form.tsx
Normal 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,
|
||||
};
|
||||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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",
|
||||
)}
|
||||
>
|
||||
{" "}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
75
src/frontend/src/customNodes/hooks/use-handle-new-value.tsx
Normal file
75
src/frontend/src/customNodes/hooks/use-handle-new-value.tsx
Normal 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;
|
||||
40
src/frontend/src/customNodes/hooks/use-handle-node-class.tsx
Normal file
40
src/frontend/src/customNodes/hooks/use-handle-node-class.tsx
Normal 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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
140
src/frontend/src/modals/BundleModal/component/index.tsx
Normal file
140
src/frontend/src/modals/BundleModal/component/index.tsx
Normal 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;
|
||||
10
src/frontend/src/modals/BundleModal/entities/index.ts
Normal file
10
src/frontend/src/modals/BundleModal/entities/index.ts
Normal 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()),
|
||||
});
|
||||
53
src/frontend/src/modals/BundleModal/hooks/submit-folder.tsx
Normal file
53
src/frontend/src/modals/BundleModal/hooks/submit-folder.tsx
Normal 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;
|
||||
79
src/frontend/src/modals/BundleModal/index.tsx
Normal file
79
src/frontend/src/modals/BundleModal/index.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
10
src/frontend/src/modals/apiModal/utils/build-content.tsx
Normal file
10
src/frontend/src/modals/apiModal/utils/build-content.tsx
Normal 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;
|
||||
8
src/frontend/src/modals/apiModal/utils/build-tweaks.ts
Normal file
8
src/frontend/src/modals/apiModal/utils/build-tweaks.ts
Normal 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;
|
||||
}, {});
|
||||
}
|
||||
|
|
@ -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"
|
||||
);
|
||||
};
|
||||
26
src/frontend/src/modals/apiModal/utils/get-changes-types.ts
Normal file
26
src/frontend/src/modals/apiModal/utils/get-changes-types.ts
Normal 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;
|
||||
};
|
||||
|
|
@ -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;
|
||||
};
|
||||
29
src/frontend/src/modals/apiModal/utils/get-value.ts
Normal file
29
src/frontend/src/modals/apiModal/utils/get-value.ts
Normal 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;
|
||||
};
|
||||
212
src/frontend/src/modals/apiModal/views/index.tsx
Normal file
212
src/frontend/src/modals/apiModal/views/index.tsx
Normal 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;
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
104
src/frontend/src/modals/flowLogsModal/index.tsx
Normal file
104
src/frontend/src/modals/flowLogsModal/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
148
src/frontend/src/modals/foldersModal/component/index.tsx
Normal file
148
src/frontend/src/modals/foldersModal/component/index.tsx
Normal 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;
|
||||
10
src/frontend/src/modals/foldersModal/entities/index.ts
Normal file
10
src/frontend/src/modals/foldersModal/entities/index.ts
Normal 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()),
|
||||
});
|
||||
60
src/frontend/src/modals/foldersModal/hooks/submit-folder.tsx
Normal file
60
src/frontend/src/modals/foldersModal/hooks/submit-folder.tsx
Normal 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;
|
||||
79
src/frontend/src/modals/foldersModal/index.tsx
Normal file
79
src/frontend/src/modals/foldersModal/index.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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")!}
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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={() => {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
26
src/frontend/src/pages/MainPage/entities/index.tsx
Normal file
26
src/frontend/src/pages/MainPage/entities/index.tsx
Normal 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;
|
||||
};
|
||||
32
src/frontend/src/pages/MainPage/hooks/use-delete-folder.tsx
Normal file
32
src/frontend/src/pages/MainPage/hooks/use-delete-folder.tsx
Normal 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;
|
||||
|
|
@ -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;
|
||||
64
src/frontend/src/pages/MainPage/hooks/use-on-file-drop.tsx
Normal file
64
src/frontend/src/pages/MainPage/hooks/use-on-file-drop.tsx
Normal 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;
|
||||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue