From b32c02671d3932e52f1ec0c2e246930c2e796d23 Mon Sep 17 00:00:00 2001 From: anovazzi1 Date: Wed, 5 Jun 2024 15:46:52 -0300 Subject: [PATCH 01/13] add hsitory tab section to playground --- src/frontend/src/modals/IOModal/index.tsx | 337 +++++++++++----------- 1 file changed, 173 insertions(+), 164 deletions(-) diff --git a/src/frontend/src/modals/IOModal/index.tsx b/src/frontend/src/modals/IOModal/index.tsx index 001ea4b9e..2bac1f2cb 100644 --- a/src/frontend/src/modals/IOModal/index.tsx +++ b/src/frontend/src/modals/IOModal/index.tsx @@ -19,7 +19,7 @@ import { InputOutput } from "../../constants/enums"; import useFlowStore from "../../stores/flowStore"; import useFlowsManagerStore from "../../stores/flowsManagerStore"; import { IOModalPropsType } from "../../types/components"; -import { NodeType } from "../../types/flow"; +import { NodeDataType, NodeType } from "../../types/flow"; import { updateVerticesOrder } from "../../utils/buildUtils"; import { cn } from "../../utils/utils"; import BaseModal from "../baseModal"; @@ -113,13 +113,17 @@ export default function IOModal({ setSelectedTab(inputs.length > 0 ? 1 : outputs.length > 0 ? 2 : 0); }, [allNodes.length]); + const flow_sessions = allNodes + .map((node) => (node.data as NodeDataType).node?.template["session_id"]) + .filter((session) => session !== undefined); + useEffect(() => { setSelectedViewField(startView()); - // if (haveChat) { - // getSessions().then((sessions) => { - // setSessions(sessions); - // }); - // } + if (haveChat) { + getSessions().then((sessions) => { + setSessions(sessions); + }); + } }, [open]); return ( @@ -144,175 +148,180 @@ export default function IOModal({
- {selectedTab !== 0 && ( -
+ { + setSelectedTab(Number(value)); + }} > - { - setSelectedTab(Number(value)); - }} - > -
- - {inputs.length > 0 && ( - Inputs - )} - {outputs.length > 0 && ( - Outputs - )} - {/* {haveChat && ( - History - )} */} - -
+
+ + {inputs.length > 0 && ( + Inputs + )} + {outputs.length > 0 && ( + Outputs + )} + {haveChat && History} + +
- -
- - {TEXT_INPUT_MODAL_TITLE} -
- {nodes - .filter((node) => - inputs.some((input) => input.id === node.id), - ) - .map((node, index) => { - const input = inputs.find( - (input) => input.id === node.id, - )!; - return ( -
- - -
- - {node.data.node.display_name} - -
-
-
{ - event.stopPropagation(); - setSelectedViewField(input); - }} - > - + + {nodes + .filter((node) => + inputs.some((input) => input.id === node.id), + ) + .map((node, index) => { + const input = inputs.find( + (input) => input.id === node.id, + )!; + return ( +
+ + +
+ + {node.data.node.display_name} +
-
- } - key={index} - keyValue={input.id} - > -
-
- {input && ( - - )} + +
{ + event.stopPropagation(); + setSelectedViewField(input); + }} + > +
- -
- ); - })} -
- -
- - {OUTPUTS_MODAL_TITLE} -
- {nodes - .filter((node) => - outputs.some((output) => output.id === node.id), - ) - .map((node, index) => { - const output = outputs.find( - (output) => output.id === node.id, - )!; - return ( -
- - -
- - {node.data.node.display_name} - -
-
-
{ - event.stopPropagation(); - setSelectedViewField(output); - }} - > - +
+
+ {input && ( + + )} +
+
+ +
+ ); + })} + + + {nodes + .filter((node) => + outputs.some((output) => output.id === node.id), + ) + .map((node, index) => { + const output = outputs.find( + (output) => output.id === node.id, + )!; + return ( +
+ + +
+ + {node.data.node.display_name} +
-
- } - key={index} - keyValue={output.id} - > -
-
- {output && ( - - )} + +
{ + event.stopPropagation(); + setSelectedViewField(output); + }} + > +
- + } + key={index} + keyValue={output.id} + > +
+
+ {output && ( + + )} +
+
+ +
+ ); + })} +
+ + {sessions.map((session, index) => { + return ( +
+ {session} +
+ +
+ +
+
- ); - })} - - -
- )} - +
+
+ ); + })} +
+ +
{selectedViewField && (
Date: Wed, 5 Jun 2024 18:36:17 -0300 Subject: [PATCH 02/13] Refactor: Update IOModal to handle missing session IDs in flow_sessions --- src/frontend/src/modals/IOModal/index.tsx | 26 ++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/src/frontend/src/modals/IOModal/index.tsx b/src/frontend/src/modals/IOModal/index.tsx index 2bac1f2cb..cbb8cb0de 100644 --- a/src/frontend/src/modals/IOModal/index.tsx +++ b/src/frontend/src/modals/IOModal/index.tsx @@ -113,9 +113,15 @@ export default function IOModal({ setSelectedTab(inputs.length > 0 ? 1 : outputs.length > 0 ? 2 : 0); }, [allNodes.length]); - const flow_sessions = allNodes - .map((node) => (node.data as NodeDataType).node?.template["session_id"]) - .filter((session) => session !== undefined); + const flow_sessions = allNodes.map((node) => { + if ((node.data as NodeDataType).node?.template["session_id"]) { + return { + id: node.id, + session_id: (node.data as NodeDataType).node?.template["session_id"] + .value, + }; + } + }); useEffect(() => { setSelectedViewField(startView()); @@ -311,8 +317,18 @@ export default function IOModal({
- -
+ +
+ f_session?.session_id === session, + ) + ? "bg-status-green" + : "bg-slate-500", + )} + >
From cca64780403f09e174975729d8c5f644330e1007 Mon Sep 17 00:00:00 2001 From: anovazzi1 Date: Wed, 5 Jun 2024 22:26:09 -0300 Subject: [PATCH 03/13] Refactor IOModal to improve session display and functionality --- src/frontend/src/modals/IOModal/index.tsx | 61 +++++++++++------------ 1 file changed, 30 insertions(+), 31 deletions(-) diff --git a/src/frontend/src/modals/IOModal/index.tsx b/src/frontend/src/modals/IOModal/index.tsx index cbb8cb0de..36d495d95 100644 --- a/src/frontend/src/modals/IOModal/index.tsx +++ b/src/frontend/src/modals/IOModal/index.tsx @@ -133,12 +133,7 @@ export default function IOModal({ }, [open]); return ( - + {children} {/* TODO ADAPT TO ALL TYPES OF INPUTS AND OUTPUTS */} @@ -305,31 +300,35 @@ export default function IOModal({ {sessions.map((session, index) => { return ( -
- {session} -
- -
- -
- f_session?.session_id === session, - ) - ? "bg-status-green" - : "bg-slate-500", - )} - >
-
+
+
+ + {session} + +
+ +
+ +
+ f_session?.session_id === session, + ) + ? "bg-status-green" + : "bg-slate-500", + )} + >
+
+
From 2888c2c30bdf70b3edc6459088755cf461e96c8c Mon Sep 17 00:00:00 2001 From: anovazzi1 Date: Thu, 6 Jun 2024 16:54:41 -0300 Subject: [PATCH 04/13] almost fixing, miss delte session and delete session message --- src/frontend/src/controllers/API/index.ts | 17 ++--- .../IOModal/components/SessionView/index.tsx | 68 +++++++++++++++++++ src/frontend/src/modals/IOModal/index.tsx | 37 ++++++++-- 3 files changed, 106 insertions(+), 16 deletions(-) create mode 100644 src/frontend/src/modals/IOModal/components/SessionView/index.tsx diff --git a/src/frontend/src/controllers/API/index.ts b/src/frontend/src/controllers/API/index.ts index 8f01d1908..62cf63381 100644 --- a/src/frontend/src/controllers/API/index.ts +++ b/src/frontend/src/controllers/API/index.ts @@ -1027,7 +1027,11 @@ export async function getMessagesTable( id?: string, excludedFields?: string[], params = {}, -): Promise<{ rows: Array; columns: Array }> { +): Promise<{ + rows: Array; + columns: Array; + sessions: Array; +}> { const config = {}; if (id) { config["params"] = { flow_id: id }; @@ -1037,20 +1041,11 @@ export async function getMessagesTable( } const rows = await api.get(`${BASE_URL_API}monitor/messages`, config); const columns = extractColumnsFromRows(rows.data, mode, excludedFields); - return { rows: rows.data, columns }; -} - -export async function getSessions(id?: string): Promise> { - const config = {}; - if (id) { - config["params"] = { flow_id: id }; - } - const rows = await api.get(`${BASE_URL_API}monitor/messages`, config); const sessions = new Set(); rows.data.forEach((row) => { sessions.add(row.session_id); }); - return Array.from(sessions); + return { rows: rows.data, columns, sessions: Array.from(sessions) }; } export async function deleteMessagesFn(ids: number[]) { diff --git a/src/frontend/src/modals/IOModal/components/SessionView/index.tsx b/src/frontend/src/modals/IOModal/components/SessionView/index.tsx new file mode 100644 index 000000000..273766889 --- /dev/null +++ b/src/frontend/src/modals/IOModal/components/SessionView/index.tsx @@ -0,0 +1,68 @@ +import { + CellEditRequestEvent, + ColDef, + ColGroupDef, + SelectionChangedEvent, +} from "ag-grid-community"; +import { useState } from "react"; +import TableComponent from "../../../../components/tableComponent"; +import { Card, CardContent } from "../../../../components/ui/card"; +import useAlertStore from "../../../../stores/alertStore"; +import { useMessagesStore } from "../../../../stores/messagesStore"; +import useUpdateMessage from "../../../../pages/SettingsPage/pages/messagesPage/hooks/use-updateMessage"; +import useRemoveMessages from "../../../../pages/SettingsPage/pages/messagesPage/hooks/use-remove-messages"; +import useMessagesTable from "../../../../pages/SettingsPage/pages/messagesPage/hooks/use-messages-table"; +import HeaderMessagesComponent from "../../../../pages/SettingsPage/pages/messagesPage/components/headerMessages"; + +export default function SessionView({ + session, + columns, +}: { + session: string; + columns: Array; +}) { + const messages = useMessagesStore((state) => state.messages); + const setErrorData = useAlertStore((state) => state.setErrorData); + const setSuccessData = useAlertStore((state) => state.setSuccessData); + + const [selectedRows, setSelectedRows] = useState([]); + + const { handleRemoveMessages } = useRemoveMessages( + setSelectedRows, + setSuccessData, + setErrorData, + selectedRows, + ); + + const { handleUpdate } = useUpdateMessage(setSuccessData, setErrorData); + + function handleUpdateMessage(event: CellEditRequestEvent) { + const newValue = event.newValue; + const field = event.column.getColId(); + const row = event.data; + const data = { + ...row, + [field]: newValue, + }; + handleUpdate(data); + } + + return ( + { + handleUpdateMessage(event); + }} + editable={["Sender Name", "Message"]} + overlayNoRowsTemplate="No data available" + onSelectionChanged={(event: SelectionChangedEvent) => { + setSelectedRows(event.api.getSelectedRows().map((row) => row.index)); + }} + rowSelection="multiple" + suppressRowClickSelection={true} + pagination={true} + columnDefs={columns} + rowData={messages.filter((message) => message.session_id === session)} + /> + ); +} diff --git a/src/frontend/src/modals/IOModal/index.tsx b/src/frontend/src/modals/IOModal/index.tsx index 36d495d95..bf60384d2 100644 --- a/src/frontend/src/modals/IOModal/index.tsx +++ b/src/frontend/src/modals/IOModal/index.tsx @@ -25,7 +25,10 @@ import { cn } from "../../utils/utils"; import BaseModal from "../baseModal"; import IOFieldView from "./components/IOFieldView"; import ChatView from "./components/chatView"; -import { getSessions } from "../../controllers/API"; +import { getMessagesTable } from "../../controllers/API"; +import { useMessagesStore } from "../../stores/messagesStore"; +import { ColDef, ColGroupDef } from "ag-grid-community"; +import SessionView from "./components/SessionView"; export default function IOModal({ children, @@ -34,6 +37,7 @@ export default function IOModal({ disable, }: IOModalPropsType): JSX.Element { const allNodes = useFlowStore((state) => state.nodes); + const setMessages = useMessagesStore((state) => state.setMessages); const inputs = useFlowStore((state) => state.inputs).filter( (input) => input.type !== "ChatInput", ); @@ -80,6 +84,7 @@ export default function IOModal({ const currentFlow = useFlowsManagerStore((state) => state.currentFlow); const setNode = useFlowStore((state) => state.setNode); const [sessions, setSessions] = useState([]); + const [columns, setColumns] = useState>([]); async function updateVertices() { return updateVerticesOrder(currentFlow!.id, null); @@ -126,8 +131,10 @@ export default function IOModal({ useEffect(() => { setSelectedViewField(startView()); if (haveChat) { - getSessions().then((sessions) => { + getMessagesTable("union").then(({ sessions, rows, columns }) => { setSessions(sessions); + setMessages(rows); + setColumns(columns); }); } }, [open]); @@ -300,7 +307,16 @@ export default function IOModal({ {sessions.map((session, index) => { return ( -
+
{ + event.stopPropagation(); + setSelectedViewField({ + id: session, + type: "Session", + }); + }} + >
{session} @@ -362,14 +378,17 @@ export default function IOModal({
{inputs.some( (input) => input.id === selectedViewField.id, - ) ? ( + ) && ( - ) : ( + )} + {outputs.some( + (output) => output.id === selectedViewField.id, + ) && ( )} + {sessions.some( + (session) => session === selectedViewField.id, + ) && ( + + )}
)} From 6fcf99aed9ca2dfacf40812314a93064246b9bbe Mon Sep 17 00:00:00 2001 From: anovazzi1 Date: Thu, 6 Jun 2024 18:54:00 -0300 Subject: [PATCH 05/13] workin session managment in playground, need to add tooltips --- src/frontend/src/controllers/API/index.ts | 3 +- .../components/SessionView/hooks/index.tsx | 29 ++++++++ .../IOModal/components/SessionView/index.tsx | 68 ++++++++++++------- src/frontend/src/modals/IOModal/index.tsx | 43 +++++++++--- src/frontend/src/stores/messagesStore.ts | 12 ++++ .../src/types/zustand/messages/index.ts | 4 ++ 6 files changed, 124 insertions(+), 35 deletions(-) create mode 100644 src/frontend/src/modals/IOModal/components/SessionView/hooks/index.tsx diff --git a/src/frontend/src/controllers/API/index.ts b/src/frontend/src/controllers/API/index.ts index 62cf63381..96cfaef12 100644 --- a/src/frontend/src/controllers/API/index.ts +++ b/src/frontend/src/controllers/API/index.ts @@ -1030,7 +1030,6 @@ export async function getMessagesTable( ): Promise<{ rows: Array; columns: Array; - sessions: Array; }> { const config = {}; if (id) { @@ -1045,7 +1044,7 @@ export async function getMessagesTable( rows.data.forEach((row) => { sessions.add(row.session_id); }); - return { rows: rows.data, columns, sessions: Array.from(sessions) }; + return { rows: rows.data, columns }; } export async function deleteMessagesFn(ids: number[]) { diff --git a/src/frontend/src/modals/IOModal/components/SessionView/hooks/index.tsx b/src/frontend/src/modals/IOModal/components/SessionView/hooks/index.tsx new file mode 100644 index 000000000..e8e638def --- /dev/null +++ b/src/frontend/src/modals/IOModal/components/SessionView/hooks/index.tsx @@ -0,0 +1,29 @@ +import { deleteMessagesFn } from "../../../../../controllers/API"; +import { useMessagesStore } from "../../../../../stores/messagesStore"; + +const useRemoveSession = (setSuccessData, setErrorData) => { + const deleteSession = useMessagesStore((state) => state.deleteSession); + const messages = useMessagesStore((state) => state.messages); + + const handleRemoveSession = async (session_id: string) => { + try { + await deleteMessagesFn( + messages + .filter((msg) => msg.session_id === session_id) + .map((msg) => msg.index), + ); + deleteSession(session_id); + setSuccessData({ + title: "Session deleted successfully.", + }); + } catch (error) { + setErrorData({ + title: "Error deleting Session.", + }); + } + }; + + return { handleRemoveSession }; +}; + +export default useRemoveSession; diff --git a/src/frontend/src/modals/IOModal/components/SessionView/index.tsx b/src/frontend/src/modals/IOModal/components/SessionView/index.tsx index 273766889..b21a786a1 100644 --- a/src/frontend/src/modals/IOModal/components/SessionView/index.tsx +++ b/src/frontend/src/modals/IOModal/components/SessionView/index.tsx @@ -11,17 +11,13 @@ import useAlertStore from "../../../../stores/alertStore"; import { useMessagesStore } from "../../../../stores/messagesStore"; import useUpdateMessage from "../../../../pages/SettingsPage/pages/messagesPage/hooks/use-updateMessage"; import useRemoveMessages from "../../../../pages/SettingsPage/pages/messagesPage/hooks/use-remove-messages"; -import useMessagesTable from "../../../../pages/SettingsPage/pages/messagesPage/hooks/use-messages-table"; import HeaderMessagesComponent from "../../../../pages/SettingsPage/pages/messagesPage/components/headerMessages"; +import { Button } from "../../../../components/ui/button"; +import ForwardedIconComponent from "../../../../components/genericIconComponent"; +import { cn } from "../../../../utils/utils"; -export default function SessionView({ - session, - columns, -}: { - session: string; - columns: Array; -}) { - const messages = useMessagesStore((state) => state.messages); +export default function SessionView({ rows }: { rows: Array }) { + const columns = useMessagesStore((state) => state.columns); const setErrorData = useAlertStore((state) => state.setErrorData); const setSuccessData = useAlertStore((state) => state.setSuccessData); @@ -48,21 +44,43 @@ export default function SessionView({ } return ( - { - handleUpdateMessage(event); - }} - editable={["Sender Name", "Message"]} - overlayNoRowsTemplate="No data available" - onSelectionChanged={(event: SelectionChangedEvent) => { - setSelectedRows(event.api.getSelectedRows().map((row) => row.index)); - }} - rowSelection="multiple" - suppressRowClickSelection={true} - pagination={true} - columnDefs={columns} - rowData={messages.filter((message) => message.session_id === session)} - /> +
+ <> +
+
+ +
+
+ + { + handleUpdateMessage(event); + }} + editable={["Sender Name", "Message"]} + overlayNoRowsTemplate="No data available" + onSelectionChanged={(event: SelectionChangedEvent) => { + setSelectedRows(event.api.getSelectedRows().map((row) => row.index)); + }} + rowSelection="multiple" + suppressRowClickSelection={true} + pagination={true} + columnDefs={columns} + rowData={rows} + /> +
); } diff --git a/src/frontend/src/modals/IOModal/index.tsx b/src/frontend/src/modals/IOModal/index.tsx index bf60384d2..725c5be72 100644 --- a/src/frontend/src/modals/IOModal/index.tsx +++ b/src/frontend/src/modals/IOModal/index.tsx @@ -27,8 +27,9 @@ import IOFieldView from "./components/IOFieldView"; import ChatView from "./components/chatView"; import { getMessagesTable } from "../../controllers/API"; import { useMessagesStore } from "../../stores/messagesStore"; -import { ColDef, ColGroupDef } from "ag-grid-community"; import SessionView from "./components/SessionView"; +import useRemoveSession from "./components/SessionView/hooks"; +import useAlertStore from "../../stores/alertStore"; export default function IOModal({ children, @@ -59,6 +60,8 @@ export default function IOModal({ const [selectedTab, setSelectedTab] = useState( inputs.length > 0 ? 1 : outputs.length > 0 ? 2 : 0, ); + const setErrorData = useAlertStore((state) => state.setErrorData); + const setSuccessData = useAlertStore((state) => state.setSuccessData); function startView() { if (!chatInput && !chatOutput) { @@ -84,8 +87,8 @@ export default function IOModal({ const currentFlow = useFlowsManagerStore((state) => state.currentFlow); const setNode = useFlowStore((state) => state.setNode); const [sessions, setSessions] = useState([]); - const [columns, setColumns] = useState>([]); - + const messages = useMessagesStore((state) => state.messages); + const setColumns = useMessagesStore((state) => state.setColumns); async function updateVertices() { return updateVerticesOrder(currentFlow!.id, null); } @@ -114,6 +117,11 @@ export default function IOModal({ } } + const { handleRemoveSession } = useRemoveSession( + setSuccessData, + setErrorData, + ); + useEffect(() => { setSelectedTab(inputs.length > 0 ? 1 : outputs.length > 0 ? 2 : 0); }, [allNodes.length]); @@ -131,14 +139,22 @@ export default function IOModal({ useEffect(() => { setSelectedViewField(startView()); if (haveChat) { - getMessagesTable("union").then(({ sessions, rows, columns }) => { - setSessions(sessions); + getMessagesTable("union").then(({ rows, columns }) => { setMessages(rows); setColumns(columns); }); } }, [open]); + useEffect(() => { + const sessions = new Set(); + messages.forEach((row) => { + sessions.add(row.session_id); + }); + setSessions(Array.from(sessions)); + sessions; + }, [messages]); + return ( {children} @@ -322,7 +338,16 @@ export default function IOModal({ {session}
-
diff --git a/src/frontend/src/stores/messagesStore.ts b/src/frontend/src/stores/messagesStore.ts index df1a3c15c..349a1c447 100644 --- a/src/frontend/src/stores/messagesStore.ts +++ b/src/frontend/src/stores/messagesStore.ts @@ -2,6 +2,18 @@ import { create } from "zustand"; import { MessagesStoreType } from "../types/zustand/messages"; export const useMessagesStore = create((set, get) => ({ + deleteSession: (id) => { + set((state) => { + const updatedMessages = state.messages.filter( + (msg) => msg.session_id !== id, + ); + return { messages: updatedMessages }; + }); + }, + columns: [], + setColumns: (columns) => { + set(() => ({ columns: columns })); + }, messages: [], setMessages: (messages) => { set(() => ({ messages: messages })); diff --git a/src/frontend/src/types/zustand/messages/index.ts b/src/frontend/src/types/zustand/messages/index.ts index 44915d2c3..b3ebd50d1 100644 --- a/src/frontend/src/types/zustand/messages/index.ts +++ b/src/frontend/src/types/zustand/messages/index.ts @@ -1,3 +1,4 @@ +import { ColDef, ColGroupDef } from "ag-grid-community"; import { Message } from "../../messages"; export type MessagesStoreType = { @@ -8,4 +9,7 @@ export type MessagesStoreType = { updateMessage: (message: Message) => void; clearMessages: () => void; removeMessages: (ids: number[]) => void; + columns: Array; + setColumns: (columns: Array) => void; + deleteSession: (id: string) => void; }; From 8063fd9a97139461eee23a23dd0d4c875116b510 Mon Sep 17 00:00:00 2001 From: anovazzi1 Date: Thu, 6 Jun 2024 18:59:52 -0300 Subject: [PATCH 06/13] Refactor IOModal to improve session management and functionality --- src/frontend/src/modals/IOModal/index.tsx | 27 ++++++++++++++++++----- 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/src/frontend/src/modals/IOModal/index.tsx b/src/frontend/src/modals/IOModal/index.tsx index 725c5be72..5f6d455be 100644 --- a/src/frontend/src/modals/IOModal/index.tsx +++ b/src/frontend/src/modals/IOModal/index.tsx @@ -348,15 +348,30 @@ export default function IOModal({ setSelectedViewField(undefined); }} > - - + +
+ +
- + + f_session?.session_id === session, + ) + ? "Active Session" + : "Inactive Session" + } + >
Date: Thu, 6 Jun 2024 19:04:26 -0300 Subject: [PATCH 07/13] filter using flow Id --- src/frontend/src/modals/IOModal/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/frontend/src/modals/IOModal/index.tsx b/src/frontend/src/modals/IOModal/index.tsx index 5f6d455be..3e28552d5 100644 --- a/src/frontend/src/modals/IOModal/index.tsx +++ b/src/frontend/src/modals/IOModal/index.tsx @@ -139,7 +139,7 @@ export default function IOModal({ useEffect(() => { setSelectedViewField(startView()); if (haveChat) { - getMessagesTable("union").then(({ rows, columns }) => { + getMessagesTable("union", currentFlow!.id).then(({ rows, columns }) => { setMessages(rows); setColumns(columns); }); From fe371d650b00654315b0fc0fa47eebfb4ac684b8 Mon Sep 17 00:00:00 2001 From: cristhianzl Date: Fri, 7 Jun 2024 10:38:32 -0300 Subject: [PATCH 08/13] =?UTF-8?q?=E2=9C=A8=20(sideBarFolderButtons):=20add?= =?UTF-8?q?=20success=20alert=20for=20folder=20upload?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ♻️ (sideBarFolderButtons): remove unnecessary commas and improve code readability ✨ (hooks): add useDeleteMultipleFlows hook for deleting multiple flows ✨ (hooks): add useDescriptionModal hook for dynamic modal descriptions ✨ (hooks): add useFilteredFlows hook for filtering flows ✨ (hooks): add useDuplicateFlows hook for duplicating flows ✨ (hooks): add custom hooks for handling export, select all, and option changes - Add `useExportFlows` for exporting selected flows - Add `useSelectAll` for handling select all functionality - Add `useSelectOptionsChange` for handling option changes - Add `useSelectedFlows` for managing selected flows state ♻️ (index.tsx): refactor to use custom hooks for better code modularity and readability 💡 (index.tsx): add cardTypes memoization to determine the type of cards displayed --- .../components/sideBarFolderButtons/index.tsx | 4 + .../hooks/use-delete-multiple.tsx | 47 ++++ .../hooks/use-description-modal.tsx | 32 +++ .../hooks/use-filtered-flows.tsx | 27 ++ .../hooks/use-handle-duplicate.tsx | 53 ++++ .../hooks/use-handle-export.tsx | 50 ++++ .../hooks/use-handle-select-all.tsx | 25 ++ .../hooks/use-select-options-change.tsx | 40 +++ .../hooks/use-selected-flows.tsx | 18 ++ .../components/componentsComponent/index.tsx | 245 ++++++------------ 10 files changed, 374 insertions(+), 167 deletions(-) create mode 100644 src/frontend/src/pages/MainPage/components/componentsComponent/hooks/use-delete-multiple.tsx create mode 100644 src/frontend/src/pages/MainPage/components/componentsComponent/hooks/use-description-modal.tsx create mode 100644 src/frontend/src/pages/MainPage/components/componentsComponent/hooks/use-filtered-flows.tsx create mode 100644 src/frontend/src/pages/MainPage/components/componentsComponent/hooks/use-handle-duplicate.tsx create mode 100644 src/frontend/src/pages/MainPage/components/componentsComponent/hooks/use-handle-export.tsx create mode 100644 src/frontend/src/pages/MainPage/components/componentsComponent/hooks/use-handle-select-all.tsx create mode 100644 src/frontend/src/pages/MainPage/components/componentsComponent/hooks/use-select-options-change.tsx create mode 100644 src/frontend/src/pages/MainPage/components/componentsComponent/hooks/use-selected-flows.tsx diff --git a/src/frontend/src/components/sidebarComponent/components/sideBarFolderButtons/index.tsx b/src/frontend/src/components/sidebarComponent/components/sideBarFolderButtons/index.tsx index de394d2e1..4289c7cbe 100644 --- a/src/frontend/src/components/sidebarComponent/components/sideBarFolderButtons/index.tsx +++ b/src/frontend/src/components/sidebarComponent/components/sideBarFolderButtons/index.tsx @@ -51,6 +51,7 @@ const SideBarFoldersButtonsComponent = ({ const folderId = location?.state?.folderId ?? myCollectionId; const getFolderById = useFolderStore((state) => state.getFolderById); const setErrorData = useAlertStore((state) => state.setErrorData); + const setSuccessData = useAlertStore((state) => state.setSuccessData); const handleFolderChange = (folderId: string) => { getFolderById(folderId); @@ -65,6 +66,9 @@ const SideBarFoldersButtonsComponent = ({ uploadFolder(folderId) .then(() => { getFolderById(folderId); + setSuccessData({ + title: "Uploaded successfully", + }); }) .catch((err) => { console.log(err); diff --git a/src/frontend/src/pages/MainPage/components/componentsComponent/hooks/use-delete-multiple.tsx b/src/frontend/src/pages/MainPage/components/componentsComponent/hooks/use-delete-multiple.tsx new file mode 100644 index 000000000..395088193 --- /dev/null +++ b/src/frontend/src/pages/MainPage/components/componentsComponent/hooks/use-delete-multiple.tsx @@ -0,0 +1,47 @@ +import { useCallback } from "react"; + +const useDeleteMultipleFlows = ( + selectedFlowsComponentsCards, + removeFlow, + resetFilter, + getFoldersApi, + folderId, + myCollectionId, + getFolderById, + setSuccessData, + setErrorData, +) => { + const handleDeleteMultiple = useCallback(() => { + 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"], + }); + }); + }, [ + selectedFlowsComponentsCards, + removeFlow, + resetFilter, + getFoldersApi, + folderId, + myCollectionId, + getFolderById, + setSuccessData, + setErrorData, + ]); + + return { handleDeleteMultiple }; +}; + +export default useDeleteMultipleFlows; diff --git a/src/frontend/src/pages/MainPage/components/componentsComponent/hooks/use-description-modal.tsx b/src/frontend/src/pages/MainPage/components/componentsComponent/hooks/use-description-modal.tsx new file mode 100644 index 000000000..6de2ebb6d --- /dev/null +++ b/src/frontend/src/pages/MainPage/components/componentsComponent/hooks/use-description-modal.tsx @@ -0,0 +1,32 @@ +import { useMemo } from "react"; + +const useDescriptionModal = (selectedFlowsComponentsCards, type) => { + 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]); + + return getDescriptionModal; +}; + +export default useDescriptionModal; diff --git a/src/frontend/src/pages/MainPage/components/componentsComponent/hooks/use-filtered-flows.tsx b/src/frontend/src/pages/MainPage/components/componentsComponent/hooks/use-filtered-flows.tsx new file mode 100644 index 000000000..96b1757ff --- /dev/null +++ b/src/frontend/src/pages/MainPage/components/componentsComponent/hooks/use-filtered-flows.tsx @@ -0,0 +1,27 @@ +import cloneDeep from "lodash/cloneDeep"; +import { useEffect } from "react"; + +const useFilteredFlows = ( + flowsFromFolder, + searchFlowsComponents, + setAllFlows, +) => { + 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); + } else { + setAllFlows(filteredFlows); + } + }, [flowsFromFolder, searchFlowsComponents, setAllFlows]); +}; + +export default useFilteredFlows; diff --git a/src/frontend/src/pages/MainPage/components/componentsComponent/hooks/use-handle-duplicate.tsx b/src/frontend/src/pages/MainPage/components/componentsComponent/hooks/use-handle-duplicate.tsx new file mode 100644 index 000000000..fc49fc0d1 --- /dev/null +++ b/src/frontend/src/pages/MainPage/components/componentsComponent/hooks/use-handle-duplicate.tsx @@ -0,0 +1,53 @@ +import { useCallback } from "react"; + +const useDuplicateFlows = ( + selectedFlowsComponentsCards, + addFlow, + allFlows, + resetFilter, + getFoldersApi, + folderId, + myCollectionId, + getFolderById, + setSuccessData, + setSelectedFlowsComponentsCards, + handleSelectAll, + cardTypes, +) => { + const handleDuplicate = useCallback(() => { + Promise.all( + selectedFlowsComponentsCards.map((selectedFlow) => + addFlow( + true, + allFlows.find((flow) => flow.id === selectedFlow), + ), + ), + ).then(() => { + resetFilter(); + getFoldersApi(true); + if (!folderId || folderId === myCollectionId) { + getFolderById(folderId ? folderId : myCollectionId); + } + setSuccessData({ title: `${cardTypes} duplicated successfully` }); + setSelectedFlowsComponentsCards([]); + handleSelectAll(false); + }); + }, [ + selectedFlowsComponentsCards, + addFlow, + allFlows, + resetFilter, + getFoldersApi, + folderId, + myCollectionId, + getFolderById, + setSuccessData, + setSelectedFlowsComponentsCards, + handleSelectAll, + cardTypes, + ]); + + return { handleDuplicate }; +}; + +export default useDuplicateFlows; diff --git a/src/frontend/src/pages/MainPage/components/componentsComponent/hooks/use-handle-export.tsx b/src/frontend/src/pages/MainPage/components/componentsComponent/hooks/use-handle-export.tsx new file mode 100644 index 000000000..bd742045b --- /dev/null +++ b/src/frontend/src/pages/MainPage/components/componentsComponent/hooks/use-handle-export.tsx @@ -0,0 +1,50 @@ +import { useCallback } from "react"; + +const useExportFlows = ( + selectedFlowsComponentsCards, + allFlows, + downloadFlow, + removeApiKeys, + version, + setSuccessData, + setSelectedFlowsComponentsCards, + handleSelectAll, + cardTypes, +) => { + const handleExport = useCallback(() => { + selectedFlowsComponentsCards.forEach((selectedFlowId) => { + const selectedFlow = allFlows.find((flow) => flow.id === selectedFlowId); + if (selectedFlow) { + downloadFlow( + removeApiKeys({ + id: selectedFlow.id, + data: selectedFlow.data, + description: selectedFlow.description, + name: selectedFlow.name, + last_tested_version: version, + is_component: false, + }), + selectedFlow.name, + selectedFlow.description, + ); + } + }); + setSuccessData({ title: `${cardTypes} exported successfully` }); + setSelectedFlowsComponentsCards([]); + handleSelectAll(false); + }, [ + selectedFlowsComponentsCards, + allFlows, + downloadFlow, + removeApiKeys, + version, + setSuccessData, + setSelectedFlowsComponentsCards, + handleSelectAll, + cardTypes, + ]); + + return { handleExport }; +}; + +export default useExportFlows; diff --git a/src/frontend/src/pages/MainPage/components/componentsComponent/hooks/use-handle-select-all.tsx b/src/frontend/src/pages/MainPage/components/componentsComponent/hooks/use-handle-select-all.tsx new file mode 100644 index 000000000..a81515e0a --- /dev/null +++ b/src/frontend/src/pages/MainPage/components/componentsComponent/hooks/use-handle-select-all.tsx @@ -0,0 +1,25 @@ +import { useCallback } from "react"; + +const useSelectAll = (flowsFromFolder, getValues, setValue) => { + const handleSelectAll = useCallback( + (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); + }); + }, + [flowsFromFolder, getValues, setValue], + ); + + return { handleSelectAll }; +}; + +export default useSelectAll; diff --git a/src/frontend/src/pages/MainPage/components/componentsComponent/hooks/use-select-options-change.tsx b/src/frontend/src/pages/MainPage/components/componentsComponent/hooks/use-select-options-change.tsx new file mode 100644 index 000000000..56dc204c7 --- /dev/null +++ b/src/frontend/src/pages/MainPage/components/componentsComponent/hooks/use-select-options-change.tsx @@ -0,0 +1,40 @@ +import { useCallback } from "react"; + +const useSelectOptionsChange = ( + selectedFlowsComponentsCards, + setErrorData, + setOpenDelete, + handleDuplicate, + handleExport, +) => { + const handleSelectOptionsChange = useCallback( + (action) => { + const hasSelected = selectedFlowsComponentsCards?.length > 0; + if (!hasSelected) { + setErrorData({ + title: "No items selected", + list: ["Please select items to delete"], + }); + return; + } + if (action === "delete") { + setOpenDelete(true); + } else if (action === "duplicate") { + handleDuplicate(); + } else if (action === "export") { + handleExport(); + } + }, + [ + selectedFlowsComponentsCards, + setErrorData, + setOpenDelete, + handleDuplicate, + handleExport, + ], + ); + + return { handleSelectOptionsChange }; +}; + +export default useSelectOptionsChange; diff --git a/src/frontend/src/pages/MainPage/components/componentsComponent/hooks/use-selected-flows.tsx b/src/frontend/src/pages/MainPage/components/componentsComponent/hooks/use-selected-flows.tsx new file mode 100644 index 000000000..b6f00934e --- /dev/null +++ b/src/frontend/src/pages/MainPage/components/componentsComponent/hooks/use-selected-flows.tsx @@ -0,0 +1,18 @@ +import { useEffect } from "react"; + +const useSelectedFlows = ( + entireFormValues, + setSelectedFlowsComponentsCards, +) => { + useEffect(() => { + if (!entireFormValues || Object.keys(entireFormValues).length === 0) return; + + const selectedFlows = Object.keys(entireFormValues).filter((key) => { + return entireFormValues[key] === true; + }); + + setSelectedFlowsComponentsCards(selectedFlows); + }, [entireFormValues, setSelectedFlowsComponentsCards]); +}; + +export default useSelectedFlows; diff --git a/src/frontend/src/pages/MainPage/components/componentsComponent/index.tsx b/src/frontend/src/pages/MainPage/components/componentsComponent/index.tsx index 754848256..af13e2bb4 100644 --- a/src/frontend/src/pages/MainPage/components/componentsComponent/index.tsx +++ b/src/frontend/src/pages/MainPage/components/componentsComponent/index.tsx @@ -1,4 +1,3 @@ -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"; @@ -8,7 +7,6 @@ import IconComponent from "../../../../components/genericIconComponent"; import PaginatorComponent from "../../../../components/paginatorComponent"; import { SkeletonCardComponent } from "../../../../components/skeletonCardComponent"; import { Button } from "../../../../components/ui/button"; -import { UPLOAD_ERROR_ALERT } from "../../../../constants/alerts_constants"; import DeleteConfirmationModal from "../../../../modals/deleteConfirmationModal"; import useAlertStore from "../../../../stores/alertStore"; import { useDarkStore } from "../../../../stores/darkStore"; @@ -21,6 +19,14 @@ import { getNameByType } from "../../utils/get-name-by-type"; import { sortFlows } from "../../utils/sort-flows"; import EmptyComponent from "../emptyComponent"; import HeaderComponent from "../headerComponent"; +import useDeleteMultipleFlows from "./hooks/use-delete-multiple"; +import useDescriptionModal from "./hooks/use-description-modal"; +import useFilteredFlows from "./hooks/use-filtered-flows"; +import useDuplicateFlows from "./hooks/use-handle-duplicate"; +import useExportFlows from "./hooks/use-handle-export"; +import useSelectAll from "./hooks/use-handle-select-all"; +import useSelectOptionsChange from "./hooks/use-select-options-change"; +import useSelectedFlows from "./hooks/use-selected-flows"; export default function ComponentsComponent({ type = "all", @@ -34,22 +40,22 @@ export default function ComponentsComponent({ const allFlows = useFlowsManagerStore((state) => state.allFlows); const flowsFromFolder = useFolderStore( - (state) => state.selectedFolder?.flows + (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 + (state) => state.searchFlowsComponents, ); const setSelectedFlowsComponentsCards = useFlowsManagerStore( - (state) => state.setSelectedFlowsComponentsCards + (state) => state.setSelectedFlowsComponentsCards, ); const selectedFlowsComponentsCards = useFlowsManagerStore( - (state) => state.selectedFlowsComponentsCards + (state) => state.selectedFlowsComponentsCards, ); const [handleFileDrop] = useFileDrop(uploadFlow, type)!; @@ -71,6 +77,16 @@ export default function ComponentsComponent({ const setFolderUrl = useFolderStore((state) => state.setFolderUrl); const addFlow = useFlowsManagerStore((state) => state.addFlow); + const cardTypes = useMemo(() => { + if (window.location.pathname.includes("components")) { + return "Components"; + } + if (window.location.pathname.includes("flows")) { + return "Flows"; + } + return "Items"; + }, [window.location]); + useEffect(() => { setFolderUrl(folderId ?? ""); setSelectedFlowsComponentsCards([]); @@ -78,22 +94,7 @@ export default function ComponentsComponent({ 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]); + useFilteredFlows(flowsFromFolder, searchFlowsComponents, setAllFlows); const resetFilter = () => { setPageIndex(1); @@ -104,164 +105,74 @@ export default function ComponentsComponent({ 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 { handleSelectAll } = useSelectAll( + flowsFromFolder, + getValues, + setValue, + ); - const handleSelectOptionsChange = (action: string) => { - const hasSelected = selectedFlowsComponentsCards?.length > 0; - if (!hasSelected) { - setErrorData({ - title: "No items selected", - list: ["Please select items to delete"], - }); - return; - } - if (action === "delete") { - setOpenDelete(true); - } else if (action === "duplicate") { - handleDuplicate(); - } else if (action === "export") { - handleExport(); - } - }; - - const handleDuplicate = () => { - Promise.all( - selectedFlowsComponentsCards.map((selectedFlow) => - addFlow( - true, - allFlows.find((flow) => flow.id === selectedFlow) - ) - ) - ).then(() => { - resetFilter(); - getFoldersApi(true); - if (!folderId || folderId === myCollectionId) { - getFolderById(folderId ? folderId : myCollectionId); - } - setSelectedFlowsComponentsCards([]); - - setSuccessData({ title: "Flows duplicated successfully" }); - }); - }; - - const handleImport = () => { - uploadFlow({ newProject: true, isComponent: false }) - .then(() => { - resetFilter(); - getFoldersApi(true); - if (!folderId || folderId === myCollectionId) { - getFolderById(folderId ? folderId : myCollectionId); - } - setSelectedFlowsComponentsCards([]); - - setSuccessData({ title: "Flows imported successfully" }); - }) - .catch((error) => { - setErrorData({ - title: UPLOAD_ERROR_ALERT, - list: [error], - }); - }); - }; + const { handleDuplicate } = useDuplicateFlows( + selectedFlowsComponentsCards, + addFlow, + allFlows, + resetFilter, + getFoldersApi, + folderId, + myCollectionId, + getFolderById, + setSuccessData, + setSelectedFlowsComponentsCards, + handleSelectAll, + cardTypes, + ); const version = useDarkStore((state) => state.version); - const handleExport = () => { - selectedFlowsComponentsCards.map((selectedFlowId) => { - const selectedFlow = allFlows.find((flow) => flow.id === selectedFlowId); - downloadFlow( - removeApiKeys({ - id: selectedFlow!.id, - data: selectedFlow!.data!, - description: selectedFlow!.description, - name: selectedFlow!.name, - last_tested_version: version, - is_component: false, - }), - selectedFlow!.name, - selectedFlow!.description - ); - }); - setSuccessData({ title: "Flows exported successfully" }); - }; + const { handleExport } = useExportFlows( + selectedFlowsComponentsCards, + allFlows, + downloadFlow, + removeApiKeys, + version, + setSuccessData, + setSelectedFlowsComponentsCards, + handleSelectAll, + cardTypes, + ); - 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"], - }); - }); - }; + const { handleSelectOptionsChange } = useSelectOptionsChange( + selectedFlowsComponentsCards, + setErrorData, + setOpenDelete, + handleDuplicate, + handleExport, + ); - 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; - } - ); + const { handleDeleteMultiple } = useDeleteMultipleFlows( + selectedFlowsComponentsCards, + removeFlow, + resetFilter, + getFoldersApi, + folderId, + myCollectionId, + getFolderById, + setSuccessData, + setErrorData, + ); - setSelectedFlowsComponentsCards(selectedFlows); - }, [entireFormValues]); + useSelectedFlows(entireFormValues, setSelectedFlowsComponentsCards); - 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 descriptionModal = useDescriptionModal( + selectedFlowsComponentsCards, + type, + ); const getTotalRowsCount = () => { if (type === "all") return allFlows?.length; return allFlows?.filter( - (f) => (f.is_component ?? false) === (type === "component") + (f) => (f.is_component ?? false) === (type === "component"), )?.length; }; @@ -368,7 +279,7 @@ export default function ComponentsComponent({ open={openDelete} setOpen={setOpenDelete} onConfirm={handleDeleteMultiple} - description={getDescriptionModal} + description={descriptionModal} > <> From 5e2a75458928b8eeaf6a00ca9bb1dbb31ba0f935 Mon Sep 17 00:00:00 2001 From: cristhianzl Date: Fri, 7 Jun 2024 10:38:44 -0300 Subject: [PATCH 09/13] =?UTF-8?q?=E2=9C=85=20(actionsMainPage.spec.ts):=20?= =?UTF-8?q?add=20end-to-end=20tests=20for=20downloading,=20uploading,=20an?= =?UTF-8?q?d=20duplicating=20flows=20and=20components?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tests/end-to-end/actionsMainPage.spec.ts | 110 ++++++++++++++++++ 1 file changed, 110 insertions(+) diff --git a/src/frontend/tests/end-to-end/actionsMainPage.spec.ts b/src/frontend/tests/end-to-end/actionsMainPage.spec.ts index 0b3021d4f..916d09718 100644 --- a/src/frontend/tests/end-to-end/actionsMainPage.spec.ts +++ b/src/frontend/tests/end-to-end/actionsMainPage.spec.ts @@ -122,3 +122,113 @@ test("search components", async ({ page }) => { await page.getByText("Prompt", { exact: true }).isHidden(); await page.getByText("OpenAI", { exact: true }).isHidden(); }); + +test("user should be able to download a flow or a component", async ({ + page, +}) => { + await page.goto("/"); + await page.waitForTimeout(2000); + + let modalCount = 0; + try { + const modalTitleElement = await page?.getByTestId("modal-title"); + if (modalTitleElement) { + modalCount = await modalTitleElement.count(); + } + } catch (error) { + modalCount = 0; + } + + while (modalCount === 0) { + await page.getByText("New Project", { exact: true }).click(); + await page.waitForTimeout(5000); + modalCount = await page.getByTestId("modal-title")?.count(); + } + + await page.getByRole("heading", { name: "Basic Prompting" }).click(); + await page.getByText("Chat Input", { exact: true }).click(); + await page.getByTestId("icon-SaveAll").first().click(); + await page.waitForTimeout(3000); + + if (await page.getByTestId("replace-button").isVisible()) { + await page.getByTestId("replace-button").click(); + } + await page.waitForTimeout(3000); + + await page.getByTestId("icon-ChevronLeft").last().click(); + await page.getByRole("checkbox").nth(1).click(); + await page.getByTestId("icon-FileDown").last().click(); + await page.waitForTimeout(1000); + await page.getByText("Items exported successfully").isVisible(); + + await page.getByText("Flows", { exact: true }).click(); + await page.getByRole("checkbox").nth(1).click(); + await page.getByTestId("icon-FileDown").last().click(); + await page.waitForTimeout(1000); + await page.getByText("Items exported successfully").isVisible(); + + await page.getByText("Components", { exact: true }).click(); + await page.getByRole("checkbox").nth(1).click(); + await page.getByTestId("icon-FileDown").last().click(); + await page.waitForTimeout(1000); + await page.getByText("Components exported successfully").isVisible(); +}); + +test("user should be able to upload a flow or a component", async ({ + page, +}) => { + await page.goto("/"); + await page.waitForTimeout(2000); + + let modalCount = 0; + try { + const modalTitleElement = await page?.getByTestId("modal-title"); + if (modalTitleElement) { + modalCount = await modalTitleElement.count(); + } + } catch (error) { + modalCount = 0; + } + + await page.getByTestId("upload-folder-button").last().click(); +}); + +test("user should be able to duplicate a flow or a component", async ({ + page, +}) => { + await page.goto("/"); + await page.waitForTimeout(2000); + + let modalCount = 0; + try { + const modalTitleElement = await page?.getByTestId("modal-title"); + if (modalTitleElement) { + modalCount = await modalTitleElement.count(); + } + } catch (error) { + modalCount = 0; + } + + while (modalCount === 0) { + await page.getByText("New Project", { exact: true }).click(); + await page.waitForTimeout(5000); + modalCount = await page.getByTestId("modal-title")?.count(); + } + + await page.getByRole("heading", { name: "Basic Prompting" }).click(); + await page.getByText("Chat Input", { exact: true }).click(); + await page.getByTestId("icon-SaveAll").first().click(); + await page.waitForTimeout(3000); + + if (await page.getByTestId("replace-button").isVisible()) { + await page.getByTestId("replace-button").click(); + } + await page.waitForTimeout(3000); + + await page.getByTestId("icon-ChevronLeft").last().click(); + await page.getByRole("checkbox").nth(1).click(); + + await page.getByTestId("icon-Copy").last().click(); + await page.waitForTimeout(1000); + await page.getByText("Items duplicated successfully").isVisible(); +}); From a38a0d7eebbae268449cb51a299f98f2d9dfef12 Mon Sep 17 00:00:00 2001 From: anovazzi1 Date: Fri, 7 Jun 2024 10:51:17 -0300 Subject: [PATCH 10/13] add fixex to IO modal --- src/frontend/src/modals/IOModal/index.tsx | 25 ++++++++++++++--------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/src/frontend/src/modals/IOModal/index.tsx b/src/frontend/src/modals/IOModal/index.tsx index 9b4a8556c..8eab95a30 100644 --- a/src/frontend/src/modals/IOModal/index.tsx +++ b/src/frontend/src/modals/IOModal/index.tsx @@ -29,6 +29,7 @@ import { useMessagesStore } from "../../stores/messagesStore"; import SessionView from "./components/SessionView"; import useRemoveSession from "./components/SessionView/hooks"; import useAlertStore from "../../stores/alertStore"; +import { Button } from "../../components/ui/button"; export default function IOModal({ children, @@ -39,25 +40,25 @@ export default function IOModal({ const allNodes = useFlowStore((state) => state.nodes); const setMessages = useMessagesStore((state) => state.setMessages); const inputs = useFlowStore((state) => state.inputs).filter( - (input) => input.type !== "ChatInput" + (input) => input.type !== "ChatInput", ); const chatInput = useFlowStore((state) => state.inputs).find( - (input) => input.type === "ChatInput" + (input) => input.type === "ChatInput", ); const outputs = useFlowStore((state) => state.outputs).filter( - (output) => output.type !== "ChatOutput" + (output) => output.type !== "ChatOutput", ); const chatOutput = useFlowStore((state) => state.outputs).find( - (output) => output.type === "ChatOutput" + (output) => output.type === "ChatOutput", ); const nodes = useFlowStore((state) => state.nodes).filter( (node) => inputs.some((input) => input.id === node.id) || - outputs.some((output) => output.id === node.id) + outputs.some((output) => output.id === node.id), ); const haveChat = chatInput || chatOutput; const [selectedTab, setSelectedTab] = useState( - inputs.length > 0 ? 1 : outputs.length > 0 ? 2 : 0 + inputs.length > 0 ? 1 : outputs.length > 0 ? 2 : 0, ); const setErrorData = useAlertStore((state) => state.setErrorData); const setSuccessData = useAlertStore((state) => state.setSuccessData); @@ -170,7 +171,7 @@ export default function IOModal({ return (
@@ -471,7 +476,7 @@ export default function IOModal({
{haveChat ? ( @@ -503,7 +508,7 @@ export default function IOModal({ "h-4 w-4", isBuilding ? "animate-spin" - : "fill-current text-medium-indigo" + : "fill-current text-medium-indigo", )} /> ), From d309c2bd85e5e6f133bca36e127764795cfd7350 Mon Sep 17 00:00:00 2001 From: anovazzi1 Date: Fri, 7 Jun 2024 11:08:57 -0300 Subject: [PATCH 11/13] chore: Add onDelete and onDuplicate props to TableComponent --- src/frontend/src/components/tableComponent/index.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/frontend/src/components/tableComponent/index.tsx b/src/frontend/src/components/tableComponent/index.tsx index 3b0b6a824..139392922 100644 --- a/src/frontend/src/components/tableComponent/index.tsx +++ b/src/frontend/src/components/tableComponent/index.tsx @@ -20,6 +20,8 @@ interface TableComponentProps extends AgGridReactProps { alertTitle?: string; alertDescription?: string; editable?: boolean | string[]; + onDelete?: (selectedRows: any) => void; + onDuplicate?: (selectedRows: any) => void; } const TableComponent = forwardRef< From 83a1264835c1d3b5373f1d62a8e50abd9a809d6d Mon Sep 17 00:00:00 2001 From: cristhianzl Date: Fri, 7 Jun 2024 11:24:26 -0300 Subject: [PATCH 12/13] =?UTF-8?q?=E2=9C=A8=20(GenericNode):=20add=20emoji?= =?UTF-8?q?=20validation=20using=20emoji-regex=20library=20=E2=99=BB?= =?UTF-8?q?=EF=B8=8F=20(GenericNode):=20refactor=20code=20to=20improve=20r?= =?UTF-8?q?eadability=20and=20consistency?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/CustomNodes/GenericNode/index.tsx | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/frontend/src/CustomNodes/GenericNode/index.tsx b/src/frontend/src/CustomNodes/GenericNode/index.tsx index ca1fe5aa2..0dd8d9694 100644 --- a/src/frontend/src/CustomNodes/GenericNode/index.tsx +++ b/src/frontend/src/CustomNodes/GenericNode/index.tsx @@ -1,3 +1,4 @@ +import emojiRegex from "emoji-regex"; import { useEffect, useMemo, useState } from "react"; import { NodeToolbar, useUpdateNodeInternals } from "reactflow"; import IconComponent from "../../components/genericIconComponent"; @@ -54,10 +55,10 @@ export default function GenericNode({ const setErrorData = useAlertStore((state) => state.setErrorData); const isDark = useDarkStore((state) => state.dark); 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 takeSnapshot = useFlowsManagerStore((state) => state.takeSnapshot); @@ -65,7 +66,7 @@ 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 [validationStatus, setValidationStatus] = @@ -83,7 +84,7 @@ export default function GenericNode({ data.node!, setNode, setIsOutdated, - updateNodeInternals + updateNodeInternals, ); const name = nodeIconsLucide[data.type] ? data.type : types[data.type]; @@ -114,12 +115,12 @@ export default function GenericNode({ selected: boolean, showNode: boolean, buildStatus: BuildStatus | undefined, - validationStatus: VertexBuildTypeAPI | null + validationStatus: VertexBuildTypeAPI | null, ) => { const specificClassFromBuildStatus = getSpecificClassFromBuildStatus( buildStatus, validationStatus, - isDark + isDark, ); const baseBorderClass = getBaseBorderClass(selected); @@ -128,7 +129,7 @@ export default function GenericNode({ baseBorderClass, nodeSizeClass, "generic-node-div group/node", - specificClassFromBuildStatus + specificClassFromBuildStatus, ); return names; }; @@ -143,8 +144,7 @@ export default function GenericNode({ showNode ? "w-96 rounded-lg" : "w-26 h-26 rounded-full"; const nameEditable = true; - const emojiRegex = /\p{Emoji}/u; - const isEmoji = emojiRegex.test(data?.node?.icon!); + const isEmoji = emojiRegex().test(data?.node?.icon!); if (!data.node!.template) { setErrorData({ @@ -170,7 +170,7 @@ export default function GenericNode({ showNode, isEmoji, nodeIconFragment, - checkNodeIconFragment + checkNodeIconFragment, ); function countHandles(): void { @@ -247,7 +247,7 @@ export default function GenericNode({ selected, showNode, buildStatus, - validationStatus + validationStatus, )} > {data.node?.beta && showNode && ( @@ -378,7 +378,7 @@ export default function GenericNode({ } title={getFieldTitle( data.node?.template!, - templateField + templateField, )} info={data.node?.template[templateField].info} name={templateField} @@ -406,7 +406,7 @@ export default function GenericNode({ proxy={data.node?.template[templateField].proxy} showNode={showNode} /> - ) + ), )} { setInputDescription(true); @@ -614,13 +614,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} @@ -647,7 +647,7 @@ export default function GenericNode({
{" "} From 016a7dc6884e2af605bfabb4ca570627aa875ff5 Mon Sep 17 00:00:00 2001 From: cristhianzl Date: Fri, 7 Jun 2024 11:24:40 -0300 Subject: [PATCH 13/13] =?UTF-8?q?=E2=9C=A8=20(folders.py,=20flow.py):=20ad?= =?UTF-8?q?d=20unique=20name=20generation=20for=20folders=20and=20flows?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ♻️ (folders.py): refactor folder name generation logic to use helper function ♻️ (flow.py): refactor flow name generation logic to use helper function --- src/backend/base/langflow/api/v1/folders.py | 15 +++++-------- src/backend/base/langflow/helpers/flow.py | 21 ++++++++++++++++++ src/backend/base/langflow/helpers/folders.py | 23 ++++++++++++++++++++ 3 files changed, 50 insertions(+), 9 deletions(-) create mode 100644 src/backend/base/langflow/helpers/folders.py diff --git a/src/backend/base/langflow/api/v1/folders.py b/src/backend/base/langflow/api/v1/folders.py index 7402881c7..d55f9bd15 100644 --- a/src/backend/base/langflow/api/v1/folders.py +++ b/src/backend/base/langflow/api/v1/folders.py @@ -1,5 +1,7 @@ from typing import List +from langflow.helpers.flow import generate_unique_flow_name +from langflow.helpers.folders import generate_unique_folder_name import orjson from fastapi import APIRouter, Depends, File, HTTPException, Response, UploadFile, status from sqlalchemy import or_, update @@ -203,16 +205,9 @@ async def upload_file( if not data: raise HTTPException(status_code=400, detail="No flows found in the file") - folder_results = session.exec( - select(Folder).where( - Folder.name == data["folder_name"], - Folder.user_id == current_user.id, - ) - ) - existing_folder_names = [folder.name for folder in folder_results] + folder_name = generate_unique_folder_name(data["folder_name"], current_user.id, session) - if existing_folder_names: - data["folder_name"] = f"{data['folder_name']} ({len(existing_folder_names) + 1})" + data["folder_name"] = folder_name folder = FolderCreate(name=data["folder_name"], description=data["folder_description"]) @@ -232,6 +227,8 @@ async def upload_file( raise HTTPException(status_code=400, detail="No flows found in the data") # Now we set the user_id for all flows for flow in flow_list.flows: + flow_name = generate_unique_flow_name(flow.name, current_user.id, session) + flow.name = flow_name flow.user_id = current_user.id flow.folder_id = new_folder.id diff --git a/src/backend/base/langflow/helpers/flow.py b/src/backend/base/langflow/helpers/flow.py index 7eb901274..6ce3b2048 100644 --- a/src/backend/base/langflow/helpers/flow.py +++ b/src/backend/base/langflow/helpers/flow.py @@ -259,3 +259,24 @@ def get_flow_by_id_or_endpoint_name( raise HTTPException(status_code=404, detail=f"Flow identifier {flow_id_or_name} not found") return flow + + +def generate_unique_flow_name(flow_name, user_id, session): + original_name = flow_name + n = 1 + while True: + # Check if a flow with the given name exists + existing_flow = session.exec( + select(Flow).where( + Flow.name == flow_name, + Flow.user_id == user_id, + ) + ).first() + + # If no flow with the given name exists, return the name + if not existing_flow: + return flow_name + + # If a flow with the name already exists, append (n) to the name and increment n + flow_name = f"{original_name} ({n})" + n += 1 \ No newline at end of file diff --git a/src/backend/base/langflow/helpers/folders.py b/src/backend/base/langflow/helpers/folders.py new file mode 100644 index 000000000..fa6f27fcc --- /dev/null +++ b/src/backend/base/langflow/helpers/folders.py @@ -0,0 +1,23 @@ +from langflow.services.database.models.folder.model import Folder +from sqlalchemy import select + + +def generate_unique_folder_name(folder_name, user_id, session): + original_name = folder_name + n = 1 + while True: + # Check if a folder with the given name exists + existing_folder = session.exec( + select(Folder).where( + Folder.name == folder_name, + Folder.user_id == user_id, + ) + ).first() + + # If no folder with the given name exists, return the name + if not existing_folder: + return folder_name + + # If a folder with the name already exists, append (n) to the name and increment n + folder_name = f"{original_name} ({n})" + n += 1 \ No newline at end of file