Feature: Folders, Logs, Records Output and more (#1938)
* Add Folder model and related classes * Add Folder model and related classes * Add default folder creation for new users * Add folders_router to APIRouter in router.py * Add FolderReadWithFlows * Added Logs button on MenuBar and Logs modal with tabs * Refactor monitor.py to include response_model for /transactions endpoint * chore: Add getTransactionTable function to API controller * starting folder project * add packages * ✨ (index.tsx): add ConfirmationModal component to allow users to delete selected flows with a confirmation prompt * ⬆️ (frontend/package.json): upgrade react-hook-form dependency to version 7.51.4 to ensure compatibility with other packages and improve functionality * update tableType and getTransactionTable * Feat: Create Record Output component file * chore: Remove unnecessary comma in AddNewVariableButton component * refactor: Update FormControl and FormField imports in cardComponent * refactor: Update FormControl and FormField imports in cardComponent * feat: Add CustomInputPopover component for inputComponent * refactor: Update headerComponent to use anchor tag instead of Button component for select all functionality * refactor: Update SidebarNav component to handle both link and button items * chore: Add FolderIcon and FolderPlusIcon to nodeIconsLucide * refactor: Create Form component and related UI components for form handling * feat: Add FolderForms component for creating new folders * refactor: Add objectOptions and isObjectOption to InputComponentType * add resolvers * feat: Add flow_id parameter to get_transactions API endpoint * Refactor vertex logging to use shared log_transaction function * 🔄 (utils.py): refactor build_clean_params function to be shared between edge and vertex modules for code reusability and maintainability 📝 (utils.py): add build_clean_params function to vertex module to avoid circular import and improve code organization * ♻️ (utils.py): move import of Vertex class to be under TYPE_CHECKING block to improve code readability and maintainability * Feat: Add Record Output into backend * Added Table Output and a mock * Added data that comes from the Backend * Fixed types * mock transaction table * Refactor log_transaction function to include flow_id parameter * 📝 (endpoints.py): import `col` from `sqlmodel` to fix reference error in the code ✨ (endpoints.py): add new endpoint `delete_multiple_flows` to delete multiple flows by their IDs 📝 (endpoints.py): add documentation for the `delete_multiple_flows` endpoint 📝 (folderAccordionComponent/index.tsx): create a new component `FolderAccordionComponent` to display a folder accordion with options 📝 (cardComponent/index.tsx): add conditional rendering for `CardFooter` component to display a form field only if `control` prop is provided 📝 (sidebarComponent/index.tsx): add a new array `folderArray` to store folder data for rendering in the sidebar ✨ (sidebarComponent/index.tsx): add new functionality to handle changing folders in the sidebar ✨ (custom-accordion.tsx): add a custom accordion component to the UI library for better user experience and organization of content ✨ (index.tsx): create a new component called ComponentsComponent to display and manage components or flows in the main page of the application ✨ (emptyComponent/index.tsx): add a new component called EmptyComponent to display a message when there are no flows or components created. It includes a button to create a new flow. ✨ (myCollectionComponent/components/headerTabsSearchComponent/index.tsx): add a new component called HeaderTabsSearchComponent to display tabs for switching between "Flows" and "Components" and a search input. ✨ (myCollectionComponent/components/inputSearchComponent/index.tsx): add a new component called InputSearchComponent to display an input field for searching flows or components. ✨ (myCollectionComponent/components/tabsComponent/index.tsx): add a new component called TabsSearchComponent to display tabs for switching between "Flows" and "Components". ♻️ (myCollectionComponent/index.tsx): refactor MyCollectionComponent to include the new HeaderTabsSearchComponent and wrap ComponentsComponent in a div. ✨ (hooks/on-file-drop.tsx): add a new hook called useFileDrop to handle file drop events and upload flows or components. ✨ (MainPage/index.tsx): remove unused imports and sidebarNavItems for "Flows" and "Components" to simplify code and improve maintainability 📝 (MainPage/index.tsx): add handleChangeFolder function to navigate to the selected folder when clicking on a sidebar item ✨ (MainPage/utils/sort-flows.ts): add utility function to sort flows based on updated_at and date_created properties 📝 (routes.tsx): update routes for "flows" and "components" to include sub-routes ♻️ (flowsManagerStore.ts): refactor flowsManagerStore to remove unused code, add allFlows state and setAllFlows action, add setSearchFlowsComponents action ✨ (index.ts): add new properties and methods to FlowsManagerStoreType interface to support search functionality for flows components * 📝 (endpoints.py): reformat delete_multiple_flows function definition for better readability and adherence to PEP 8 style guide * feat: Add TableAutoCellRender component for automatic cell rendering in tables * 📝 (index.tsx): Add useEffect import to sidebarComponent/index.tsx to fix missing dependency warning ✨ (index.tsx): Add getFolders import to sidebarComponent/index.tsx to fetch folders data ♻️ (index.tsx): Refactor SidebarNav component in sidebarComponent/index.tsx to use useEffect hook for fetching folders data 📝 (index.tsx): Add FolderType entity definition to entities/index.tsx ✨ (index.tsx): Create MainPage component in pages/mainPage/index.tsx to display user projects 📝 (index.tsx): Add imports and types to MainPage component in pages/mainPage/index.tsx ✨ (index.tsx): Add useEffect hook to MainPage component in pages/mainPage/index.tsx to set current flow id to null ✨ (index.tsx): Add navigate function to MainPage component in pages/mainPage/index.tsx to navigate to flow page ✨ (index.tsx): Add openModal and openFolderModal states to MainPage component in pages/mainPage/index.tsx to control modals ✨ (index.tsx): Add dropdownOptions array to MainPage component in pages/mainPage/index.tsx for dropdown button options ✨ (index.tsx): Add sidebarNavItems array to MainPage component in pages/mainPage/index.tsx for sidebar navigation items ♻️ (index.tsx): Refactor MainPage component in pages/mainPage/index.tsx to use PageLayout component and render sidebar and outlet ✨ (index.tsx): Add NewFlowModal and FoldersModal components to MainPage component in pages/mainPage/index.tsx 📝 (index.tsx): Add getFolders import to services/index.tsx to fetch folders data ✨ (index.tsx): Add addFolder, updateFolder, deleteFolder, and getFolderById functions to services/index.tsx for CRUD operations on folders ♻️ (index.tsx): Refactor routes.tsx to import MainPage component from pages/mainPage/index.tsx * Fixed data gathering, now getting from flowpool * feat: Update get_transactions API endpoint to return TransactionModelResponse objects * refactor(schema.py): remove redundant __str__ method and improve Record class string representation by returning a JSON string of the data attributes fix record table * 🐛 (langflow/__main__.py): fix create_default_folder_if_it_doesnt_exist function call by passing user.id instead of user object ✨ (endpoints.py): add delete_multiple_flows endpoint to delete multiple flows by their IDs 📝 (flows.py): add download_file endpoint to download all flows as a file 🔧 (folders.py): add read_starter_folders endpoint to read starter folders 🔧 (login.py): fix create_default_folder_if_it_doesnt_exist function call by passing user.id instead of user object 🔧 (users.py): fix create_default_folder_if_it_doesnt_exist function call by passing user.id instead of user object ✨ (setup.py): add folder_id field to Flow model and update create_new_project function to include folder_id parameter 📝 (utils.py): import necessary modules and update function signature to use UUID instead of User object ♻️ (utils.py): refactor create_default_folder_if_it_doesnt_exist function to use user_id instead of User object and update SQL query to use UUID 📝 (index.tsx): import useFolderStore from foldersStore ♻️ (index.tsx): refactor useEffect to use useFolderStore instead of getFolders function 📝 (index.tsx): import useFolderStore from foldersStore ♻️ (index.tsx): refactor useEffect to use useFolderStore instead of getFolders function 📝 (index.tsx): import useFolderStore from foldersStore ♻️ (index.tsx): refactor useEffect to use useFolderStore instead of getFolders function 📝 (index.tsx): import useFolderStore from foldersStore ♻️ (index.tsx): refactor useEffect to use useFolderStore instead of getFolders function 📝 (index.tsx): import useFolderStore from foldersStore ♻️ (index.tsx): refactor useEffect to use useFolderStore instead of getFolders function 📝 (index.tsx): import useFolderStore from foldersStore ♻️ (index.tsx): refactor useEffect to use useFolderStore instead of getFolders function 📝 (index.tsx): import useFolderStore from foldersStore ♻️ (index.tsx): refactor useEffect to use useFolderStore instead of getFolders function 📝 (index.tsx): import useFolderStore from foldersStore ♻️ (index.tsx): refactor useEffect to use useFolderStore instead of getFolders function 📝 (index.tsx): import useFolderStore from foldersStore ♻️ (index.tsx): refactor useEffect to use useFolderStore instead of getFolders function 📝 (index.tsx): import useFolderStore from foldersStore ♻️ (index.tsx): refactor useEffect to use useFolderStore instead of getFolders function 📝 (index.tsx): import useFolderStore from foldersStore ♻️ (index.tsx): refactor useEffect to use useFolderStore instead of getFolders function 📝 (index.tsx): import useFolderStore from foldersStore ♻️ (index.tsx): refactor useEffect to use useFolderStore instead of getFolders function 📝 (index.tsx): import useFolderStore from foldersStore ♻️ (index.tsx): refactor useEffect to use useFolderStore instead of getFolders function 📝 (index.tsx): import useFolderStore from foldersStore ♻️ (index.tsx): refactor useEffect to use useFolderStore instead of getFolders function 📝 (index.tsx): import useFolderStore from foldersStore ♻️ (index.tsx): refactor useEffect to use useFolderStore instead of getFolders function 📝 (index.tsx): import useFolderStore from foldersStore ♻️ (index.tsx): refactor useEffect to use useFolderStore instead of getFolders function 📝 (index.tsx): import useFolderStore from foldersStore ♻️ (index.tsx): refactor useEffect to use useFolderStore instead of getFolders function 📝 (index.tsx): import useFolderStore from foldersStore ♻️ (index.tsx): refactor useEffect to use useFolderStore instead of getFolders function 📝 (index.tsx): import useFolderStore from foldersStore ♻️ (index.tsx): refactor useEffect to use useFolderStore instead of getFolders function 📝 (index.tsx): import useFolderStore from foldersStore ♻️ (index.tsx): refactor useEffect to use useFolderStore instead of getFolders function 📝 (index.tsx): import useFolderStore from foldersStore ♻️ (index.tsx): refactor useEffect to use useFolderStore instead of getFolders function 📝 (index.tsx): import useFolderStore from foldersStore ♻️ (index.tsx): refactor useEffect to use useFolderStore instead of getFolders function 📝 (index.tsx): import useFolderStore from foldersStore ♻️ (index.tsx): refactor useEffect to use useFolderStore instead of getFolders function 📝 (index.tsx): import useFolderStore from foldersStore ♻️ (index.tsx): refactor useEffect to use useFolderStore instead of getFolders function 📝 (index.tsx): import useFolderStore from foldersStore ♻️ (index.tsx): refactor useEffect to use useFolderStore instead of getFolders function 📝 (index.tsx): import useFolderStore from foldersStore ♻️ ( ✨ (services/index.tsx): update API endpoints for getting, adding, and updating folders to match backend routes 🚀 (flowsManagerStore.ts): add support for fetching starter projects and filtering them out from the list of flows ♻️ (foldersStore.tsx): refactor folder store to use Zustand for state management 📝 (types/zustand/folders/index.ts): add types for the folder store in Zustand * 📝 (App.tsx): Add import statement for useFolderStore from foldersStore to use the getFoldersApi and loadingFolders variables 📝 (App.tsx): Add useEffect hook to call getFoldersApi on component mount 📝 (ComponentsComponent/index.tsx): Add import statement for FlowType from types/flow 📝 (ComponentsComponent/index.tsx): Add import statement for useFolderStore from foldersStore to use the myCollectionFlows variable 📝 (ComponentsComponent/index.tsx): Add const flowsFromFolder to get the flows from the selected folder in useFolderStore 📝 (ComponentsComponent/index.tsx): Add useEffect hook to set the allFlows state to the flowsFromFolder on component mount 📝 (ComponentsComponent/index.tsx): Add useEffect hook to set the allFlows state to the myCollectionFlows.flows on myCollectionFlows change 📝 (ComponentsComponent/index.tsx): Add useEffect hook to filter the flows based on the searchFlowsComponents state 📝 (ComponentsComponent/index.tsx): Add useEffect hook to call getFolderById and setAllFlows on folderId change 📝 (ComponentsComponent/index.tsx): Add isLoadingFolders variable to isLoading in the conditional rendering of the loading page panel 📝 (ComponentsComponent/index.tsx): Add useEffect hook to call getFoldersApi on component mount 📝 (entities/index.tsx): Add import statement for FlowType from types/flow 📝 (sort-flows.ts): Add optional chaining to flows and f in the filter function 📝 (foldersStore.tsx): Add getMyCollectionFolder function to get the My Collection folder and set the myCollectionFlows state 📝 (foldersStore.tsx): Add setMyCollectionFlow function to set the myCollectionFlows state 📝 (foldersStore.tsx): Add myCollectionFlows state to store the My Collection folder and its flows 📝 (foldersStore.tsx): Call getMyCollectionFolder in the getFolders function to get the My Collection folder on folders load 📝 (foldersStore.tsx): Call getMyCollectionFolder in the setFolders function to get the My Collection folder on folders update 📝 (foldersStore.tsx): Call getMyCollectionFolder in the setLoading function to get the My Collection folder on loading change 📝 (foldersStore.tsx): Call getMyCollectionFolder in the setLoadingById function to get the My Collection folder on loadingById change 📝 (foldersStore.tsx): Add myCollectionFlows state to store the My Collection folder and its flows 📝 (foldersStore.tsx): Call getMyCollectionFolder in the getFolders function to get the My Collection folder on folders load 📝 (foldersStore.tsx): Call getMyCollectionFolder in the setFolders function to get the My Collection folder on folders update 📝 (foldersStore.tsx): Call getMyCollectionFolder in the setLoading function to get the My Collection folder on loading change 📝 (foldersStore.tsx): Call getMyCollectionFolder in the setLoadingById function to get the My Collection folder on loadingById change 📝 (foldersStore.tsx): Add myCollectionFlows state to store the My Collection folder and its flows 📝 (foldersStore.tsx): Call getMyCollectionFolder in the getFolders function to get the My Collection folder on folders load 📝 (foldersStore.tsx): Call getMyCollectionFolder in the setFolders function to get the My Collection folder on folders update 📝 (foldersStore.tsx): Call getMyCollectionFolder in the setLoading function to get the My Collection folder on loading change 📝 (foldersStore.tsx): Call getMyCollectionFolder in the setLoadingById function to get the My Collection folder on loadingById change 📝 (foldersStore.tsx): Add myCollectionFlows state to store the My Collection folder and its flows 📝 (foldersStore.tsx): Call getMyCollectionFolder in the getFolders function to get the My Collection folder on folders load 📝 (foldersStore.tsx): Call getMyCollectionFolder in the setFolders function to get the My Collection folder on folders update 📝 (foldersStore.tsx): Call getMyCollectionFolder in the setLoading function to get the My Collection folder on loading change 📝 (foldersStore.tsx): Call getMyCollectionFolder in the setLoadingById function to get the My Collection folder on loadingById change 📝 (foldersStore.tsx): Add myCollectionFlows state to store the My Collection folder and its flows 📝 (foldersStore.tsx): Call getMyCollectionFolder in the getFolders function to get the My * 🐛 (flows.py): set default folder for flows without a folder_id to "My Collection" folder if it exists ✨ (componentsComponent/index.tsx): add isLoadingFolder state to track loading status of folder data 📝 (componentsComponent/index.tsx): remove console.log statement ♻️ (componentsComponent/index.tsx): refactor useEffect to setAllFlows only when folderId changes ♻️ (componentsComponent/index.tsx): refactor useEffect to log allFlows when it changes ♻️ (foldersStore.tsx): refactor getMyCollectionFolder to set myCollectionId state ♻️ (foldersStore.tsx): refactor setMyCollectionId to set myCollectionId state * Feat: create date and string logs components * ✨ (App.tsx): add autoLogin as a dependency to useEffect to trigger the effect when autoLogin changes ✨ (sideBarButtons/index.tsx): create a new component SideBarButtonsComponent to handle rendering of sidebar buttons ✨ (sideBarFolderButtons/index.tsx): create a new component SideBarFoldersButtonsComponent to handle rendering of sidebar folder buttons ♻️ (index.tsx): refactor SidebarNav component to use SideBarButtonsComponent and SideBarFoldersButtonsComponent for rendering buttons ✨ (index.tsx): add support for editing existing folders by passing folderToEdit prop to FolderForms component 📝 (index.tsx): add form validation using zod schema and zodResolver ♻️ (index.tsx): refactor form handling to use react-hook-form useForm hook and zodResolver for validation ✨ (submit-folder.tsx): create custom hook useFolderSubmit to handle form submission and API calls for adding and updating folders 📝 (entities/index.ts): add zod schema for folder form validation ♻️ (component/index.tsx): refactor imports and remove unused imports ♻️ (component/index.tsx): refactor FolderForms component to use destructuring for props and remove unused imports ♻️ (component/index.tsx): refactor useEffect to handle folderToEdit prop and set form values accordingly ♻️ (component/index.tsx): refactor FormField components to use FormItem and FormMessage components for better form structure and error handling ♻️ (component/index.tsx): refactor FormField components to use name prop instead of deprecated defaultValue prop ♻️ (component/index.tsx): refactor FormField components to use name prop instead of deprecated defaultValue prop 🔧 (component/index.tsx): add missing import for FormMessage component 🔧 (component/index.tsx): add missing import for FormMessage component 🔧 (component/index.tsx): add missing import for FormMessage component 🔧 (component/index.tsx): add missing import for FormMessage component 🔧 (component/index.tsx): add missing import for FormMessage component 🔧 (component/index.tsx): add missing import for FormMessage component 🔧 (component/index.tsx): add missing import for FormMessage component 🔧 (component/index.tsx): add missing import for FormMessage component 🔧 (component/index.tsx): add missing import for FormMessage component 🔧 (component/index.tsx): add missing import for FormMessage component 🔧 (component/index.tsx): add missing import for FormMessage component 🔧 (component/index.tsx): add missing import for FormMessage component 🔧 (component/index.tsx): add missing import for FormMessage component 🔧 (component/index.tsx): add missing import for FormMessage component 🔧 (component/index.tsx): add missing import for FormMessage component 🔧 (component/index.tsx): add missing import for FormMessage component 🔧 (component/index.tsx): add missing import for FormMessage component 🔧 (component/index.tsx): add missing import for FormMessage component 🔧 (component/index.tsx): add missing import for FormMessage component 🔧 (component/index.tsx): add missing import for FormMessage component 🔧 (component/index.tsx): add missing import for FormMessage component 🔧 (component/index.tsx): add missing import for FormMessage component 🔧 (component/index.tsx): add missing import for FormMessage component 🔧 (component/index.tsx): add missing import for FormMessage component 🔧 (component/index.tsx): add missing import for FormMessage component 🔧 (component/index.tsx): add missing import for FormMessage component 🔧 (component/index.tsx): add missing import for FormMessage component 🔧 (component/index.tsx): add missing import for FormMessage component 🔧 (component/index.tsx): add missing import for FormMessage component 🔧 (component/index.tsx): add missing import for FormMessage component 🔧 (component/index.tsx): add missing import for FormMessage component 🔧 (component/index.tsx): add missing import for FormMessage component 🔧 (component/index.tsx): add missing import for FormMessage component 🔧 (component/index.tsx): add missing import for FormMessage component 🔧 (component/index.tsx): add missing import for FormMessage component 🔧 (component/index.tsx): add missing import for FormMessage component 🔧 (component/index.tsx): add missing import for FormMessage component 🔧 (component/index.tsx): add missing import for FormMessage component 🔧 (component/index.tsx): add missing import for FormMessage component 🔧 (component/index.tsx): add missing import for FormMessage component 🔧 (component/index.tsx): add missing import for FormMessage component 🔧 (component/index.tsx): add missing import for FormMessage component 🔧 (component/index.tsx): add missing import for FormMessage component 🔧 (component/index.tsx): add missing import for FormMessage component ✨ (componentsComponent/index.tsx): remove unused isLoadingFolder variable ♻️ (componentsComponent/index.tsx): refactor useEffect to handle folderId and myCollectionId logic separately for better readability ♻️ (componentsComponent/index.tsx): refactor useEffect to setAllFlows with a delay of 500ms for smoother rendering 📝 (componentsComponent/index.tsx): remove console.log statement ✨ (modalsComponent/index.tsx): add ModalsComponent to handle different modals in ComponentsComponent ✨ (inputSearchComponent/index.tsx): add allFlows dependency to disable search input when there are no flows ✨ (delete-folder.tsx): add useDeleteFolder hook to handle folder deletion logic ✨ (dropdown-options.tsx): add useDropdownOptions hook to handle dropdown options for import from JSON 📝 (index.tsx): refactor MainPage's index.tsx to improve code readability and maintainability ✨ (index.tsx): introduce ModalsComponent to handle modals in MainPage ♻️ (foldersStore.tsx): refactor getFoldersApi function in foldersStore to allow refetching of folders ♻️ (foldersStore.tsx): refactor setFolderToEdit function in foldersStore to improve semantics ♻️ (index.ts): refactor FoldersStoreType in types/zustand/folders/index.ts to improve semantics ♻️ (tailwind.config.js): refactor tailwind.config.js to add display variant for group-hover * 🐛 (flows.py): import `col` from `sqlmodel` to fix reference error 🐛 (flows.py): change route method from DELETE to POST for deleting multiple flows 🐛 (flows.py): fix reference error in `delete_multiple_flows` function 📝 (schemas.py): add `FlowListIds` schema to handle flow ids for multiple delete ✨ (index.tsx): remove trailing commas in useState calls ♻️ (index.tsx): remove unnecessary ternary operator in className ♻️ (index.tsx): remove unnecessary arrow function in onDelete prop ♻️ (index.tsx): remove unnecessary props in DeleteConfirmationModal component ♻️ (index.tsx): remove unnecessary props in Button component ♻️ (index.tsx): remove unnecessary props in Icon component ♻️ (index.tsx): remove unnecessary props in Spinner component ♻️ (index.tsx): remove unnecessary props in IconButton component ♻️ (index.tsx): remove unnecessary props in Tooltip component ♻️ (index.tsx): remove unnecessary props in Text component ♻️ (index.tsx): remove unnecessary props in Flex component ♻️ (index.tsx): remove unnecessary props in Box component ♻️ (index.tsx): remove unnecessary props in Avatar component ♻️ (index.tsx): remove unnecessary props in Badge component ♻️ (index.tsx): remove unnecessary props in Image component ♻️ (index.tsx): remove unnecessary props in Heading component ♻️ (index.tsx): remove unnecessary props in Divider component ♻️ (index.tsx): remove unnecessary props in Spacer component ♻️ (index.tsx): remove unnecessary props in Stack component ♻️ (index.tsx): remove unnecessary props in Collapse component ♻️ (index.tsx): remove unnecessary props in Modal component ♻️ (index.tsx): remove unnecessary props in Portal component ♻️ (index.tsx): remove unnecessary props in Transition component ♻️ (index.tsx): remove unnecessary props in useFlowsManagerStore hook ♻️ (index.tsx): remove unnecessary props in useDisclosure hook ♻️ (index.tsx): remove unnecessary props in useToast hook ♻️ (index.tsx): remove unnecessary props in useColorModeValue hook ♻️ (index.tsx): remove unnecessary props in useBreakpointValue hook ♻️ (index.tsx): remove unnecessary props in useMediaQuery hook ♻️ (index.tsx): remove unnecessary props in useBoolean hook ♻️ (index.tsx): remove unnecessary props in useOutsideClick hook ♻️ (index.tsx): remove unnecessary props in useClipboard hook ♻️ (index.tsx): remove unnecessary props in useMergeRefs hook ♻️ (index.tsx): remove unnecessary props in useSafeLayoutEffect hook ♻️ (index.tsx): remove unnecessary props in useUpdateEffect hook ♻️ (index.tsx): remove unnecessary props in usePrevious hook ♻️ (index.tsx): remove unnecessary props in useTimeout hook ♻️ (index.tsx): remove unnecessary props in useDebounce hook ♻️ (index.tsx): remove unnecessary props in useThrottle hook ♻️ (index.tsx): remove unnecessary props in useWindowSize hook ♻️ (index.tsx): remove unnecessary props in useHover hook ♻️ (index.tsx): remove unnecessary props in useFocusWithin hook ♻️ (index.tsx): remove unnecessary props in useIntersect hook ♻️ (index.tsx): remove unnecessary props in useInViewport hook ♻️ (index.tsx): remove unnecessary props in useMeasure hook ♻️ (index.tsx): remove unnecessary props in useMotionValue hook ♻️ (index.tsx): remove unnecessary props in useTransform hook ♻️ (index.tsx): remove unnecessary props in useSpring hook ♻️ (index.tsx): remove unnecessary props in useDragControls hook ♻️ (index.tsx): remove unnecessary props in usePanGesture hook ♻️ (index.tsx): remove unnecessary props in useScrollControls hook ♻️ (index.tsx): remove unnecessary props in useViewportScroll hook ♻️ (index.tsx): remove unnecessary props in useAnimation hook ♻️ (index.tsx): remove unnecessary props in useCycle hook ♻️ (index.tsx): remove unnecessary props in useLottie hook ♻️ (index.tsx): remove unnecessary props in useMotionConfig hook ♻️ (index.tsx): remove unnecessary props in usePresence hook ♻ ✨ (componentsComponent/index.tsx): import multipleDeleteFlowsComponents from API controller to enable multiple deletion of flows and components ✨ (componentsComponent/index.tsx): add handleDelete function to handle individual deletion of flows and components ✨ (componentsComponent/index.tsx): add handleDeleteMultiple function to handle multiple deletion of flows and components ✨ (componentsComponent/index.tsx): add description prop to DeleteConfirmationModal to specify the type of item being deleted 📝 (modalsComponent/index.tsx): add description prop to DeleteConfirmationModal to specify the type of item being deleted * feat: Add JSON string representation to Record attributes feat: fix table view for Record * feat(frontend): add ArrayReader, NumberReader, ObjectRender components feat(frontend): add DateReader component to format date strings fix(frontend): fix component naming conventions for consistency feat(frontend): update TableAutoCellRender to use new components for rendering feat(frontend): update FlowLogsModal to use pagination and adjust modal size based on content * refactor(utils): update timestamp regex to handle optional milliseconds * refactor(api): update /monitor/messages endpoint to return MessageModel objects * refactor(api): update /monitor/messages endpoint to return List[MessageModel] * feat(modals): enable fake column editing in FlowLogsModal * update recordsOutput to expect object instead of string * refactor: update RecordsOutput to expect object instead of string * refactor: update RecordsOutputComponent to use extracted columns from rows and get multiple records * ✨ (flows.py): set default folder for newly created flows to "My Collection" if no folder is specified ♻️ (sideBarButtons/index.tsx): remove unused import and refactor code to simplify rendering of sidebar buttons ♻️ (sideBarFolderButtons/index.tsx): refactor code to simplify rendering of sidebar folder buttons and improve readability ♻️ (index.ts): refactor saveFlowToDatabase function to handle null folder_id values correctly ♻️ (NewFlowCardComponent/index.tsx): refactor code to set folder URL when creating a new flow ♻️ (undrawCards/index.tsx): refactor code to set folder URL when creating a new flow and remove unused import 📝 (newFlowModal/index.tsx): remove commented out code for IconComponent to improve code readability 📝 (newFlowModal/index.tsx): remove commented out code for examples.map to improve code readability 📝 (newFlowModal/index.tsx): change key values for UndrawCardComponent to improve uniqueness 📝 (FlowPage/index.tsx): remove unused import for useDarkStore to improve code cleanliness 📝 (FlowPage/index.tsx): remove extra whitespace to improve code readability 📝 (ComponentsComponent/index.tsx): add setFolderUrl function to set the folderUrl state in the folder store 📝 (ComponentsComponent/index.tsx): remove unnecessary whitespace to improve code readability 📝 (tabsComponent/index.tsx): add folderUrl state to navigate function to maintain folder state when changing tabs 📝 (routes.tsx): add nested route for /flow/:id/ to render FlowPage component 📝 (flowsManagerStore.ts): add folder_id property to newFlow object to store the current folder URL 📝 (foldersStore.tsx): remove unnecessary comma to fix syntax error 📝 (foldersStore.tsx): add folderUrl state and setFolderUrl function to store the current folder URL 🐛 (reactflowUtils.ts): remove unused parameter 'edges' in isValidConnection function ♻️ (reactflowUtils.ts): refactor scapeJSONParse and scapeJSONStringfy functions to remove unnecessary exclamation marks 🐛 (reactflowUtils.ts): fix bug in updateIds function where selectionIds could be undefined 🐛 (reactflowUtils.ts): fix bug in updateIds function where edge.sourceHandle could be undefined 🐛 (reactflowUtils.ts): fix bug in updateIds function where edge.targetHandle could be undefined 🐛 (reactflowUtils.ts): fix bug in validateNode function where scapeJSONParse was called twice 🐛 (reactflowUtils.ts): fix bug in validateNode function where scapeJSONParse was called twice 🐛 (reactflowUtils.ts): fix bug in validateNode function where scapeJSONParse was called twice 🐛 (reactflowUtils.ts): fix bug in validateNode function where scapeJSONParse was called twice 🐛 (reactflowUtils.ts): fix bug in validateNode function where scapeJSONParse was called twice 🐛 (reactflowUtils.ts): fix bug in validateNode function where scapeJSONParse was called twice 🐛 (reactflowUtils.ts): fix bug in validateNode function where scapeJSONParse was called twice 🐛 (reactflowUtils.ts): fix bug in validateNode function where scapeJSONParse was called twice 🐛 (reactflowUtils.ts): fix bug in validateNode function where scapeJSONParse was called twice 🐛 (reactflowUtils.ts): fix bug in validateNode function where scapeJSONParse was called twice 🐛 (reactflowUtils.ts): fix bug in validateNode function where scapeJSONParse was called twice 🐛 (reactflowUtils.ts): fix bug in validateNode function where scapeJSONParse was called twice 🐛 (reactflowUtils.ts): fix bug in validateNode function where scapeJSONParse was called twice 🐛 (reactflowUtils.ts): fix bug in validateNode function where scapeJSONParse was called twice 🐛 (reactflowUtils.ts): fix bug in validateNode function where scapeJSONParse was called twice 🐛 (reactflowUtils.ts): fix bug in validateNode function where scapeJSONParse was called twice 🐛 (reactflowUtils.ts): fix bug in validateNode function where scapeJSONParse was called twice 🐛 (reactflowUtils.ts): fix bug in validateNode function where scapeJSONParse was called twice 🐛 (reactflowUtils.ts): fix bug in validateNode function where scapeJSONParse was called twice 🐛 (reactflowUtils.ts): fix bug in validateNode function where scapeJSONParse was called twice 🐛 (reactflowUtils.ts): fix bug in validateNode function where scapeJSONParse was called twice 🐛 (reactflowUtils.ts): fix bug in validateNode function where scapeJSONParse was called twice 🐛 (reactflowUtils.ts): fix bug in validateNode function where scapeJSONParse was called twice 🐛 (reactflowUtils.ts): fix bug in validateNode function where scapeJSONParse was called twice 🐛 (reactflowUtils.ts): fix bug in validateNode function where scapeJSONParse was called twice 🐛 (reactflowUtils.ts): fix bug in validateNode function where scapeJSONParse was called twice 🐛 (reactflowUtils.ts): fix bug in validateNode function where scapeJSONParse was called twice 🐛 (reactflowUtils.ts): fix bug in validateNode function where scapeJSONParse was called twice 🐛 (reactflowUtils.ts): fix bug in validateNode function where scapeJSONParse was called twice 🐛 (reactflowUtils.ts): fix bug in validateNode function where scapeJSONParse was called twice 🐛 (reactflowUtils.ts): fix bug in validateNode function where scapeJSONParse was called twice 🐛 (reactflowUtils.ts): fix bug in validateNode function where scapeJSONParse was called twice 🐛 (reactflowUtils.ts): fix bug in validateNode function where scapeJSONParse was called twice 🐛 (reactflowUtils.ts): fix bug in validateNode function where scapeJSONParse was called twice 🐛 (reactflowUtils.ts): fix bug in validateNode function where scapeJSONParse was called twice 🐛 (reactflowUtils.ts): fix bug in validateNode function where scapeJSONParse was called twice 🐛 (reactflowUtils.ts): fix bug in validateNode function where scapeJSONParse was called twice 🐛 (reactflowUtils.ts): fix bug in validateNode function where scapeJSONParse was called twice 🐛 (reactflowUtils.ts): fix bug in 📝 (file): update line 785 to fix a typo or improve code readability ✨ (reactflowUtils.ts): remove unnecessary comma at the end of the line ♻️ (reactflowUtils.ts): refactor code to improve readability and remove unnecessary code 📝 (reactflowUtils.ts): add missing JSDoc comments to functions ♻️ (reactflowUtils.ts): refactor code to improve readability and remove unnecessary code ♻️ (reactflowUtils.ts): refactor code to improve readability and remove unnecessary code ♻️ (reactflowUtils.ts): refactor code to improve readability and remove unnecessary code ♻️ (reactflowUtils.ts): refactor code to improve readability and remove unnecessary code ♻️ (reactflowUtils.ts): refactor code to improve readability and remove unnecessary code ♻️ (reactflowUtils.ts): refactor code to improve readability and remove unnecessary code ♻️ (reactflowUtils.ts): refactor code to improve readability and remove unnecessary code ♻️ (reactflowUtils.ts): refactor code to improve readability and remove unnecessary code ♻️ (reactflowUtils.ts): refactor code to improve readability and remove unnecessary code ♻️ (reactflowUtils.ts): refactor code to improve readability and remove unnecessary code ♻️ (reactflowUtils.ts): refactor code to improve readability and remove unnecessary code ♻️ (reactflowUtils.ts): refactor code to improve readability and remove unnecessary code ♻️ (reactflowUtils.ts): refactor code to improve readability and remove unnecessary code ♻️ (reactflowUtils.ts): refactor code to improve readability and remove unnecessary code ♻️ (reactflowUtils.ts): refactor code to improve readability and remove unnecessary code ♻️ (reactflowUtils.ts): refactor code to improve readability and remove unnecessary code ♻️ (reactflowUtils.ts): refactor code to improve readability and remove unnecessary code ♻️ (reactflowUtils.ts): refactor code to improve readability and remove unnecessary code ♻️ (reactflowUtils.ts): refactor code to improve readability and remove unnecessary code ♻️ (reactflowUtils.ts): refactor code to improve readability and remove unnecessary code ♻️ (reactflowUtils.ts): refactor code to improve readability and remove unnecessary code ♻️ (reactflowUtils.ts): refactor code to improve readability and remove unnecessary code ♻️ (reactflowUtils.ts): refactor code to improve readability and remove unnecessary code ♻️ (reactflowUtils.ts): refactor code to improve readability and remove unnecessary code ♻️ (reactflowUtils.ts): refactor code to improve readability and remove unnecessary code ♻️ (reactflowUtils.ts): refactor code to improve readability and remove unnecessary code ♻️ (reactflowUtils.ts): refactor code to improve readability and remove unnecessary code ♻️ (reactflowUtils.ts): refactor code to improve readability and remove unnecessary code ♻️ (reactflowUtils.ts): refactor code to improve readability and remove unnecessary code ♻️ (reactflowUtils.ts): refactor code to improve readability and remove unnecessary code ♻️ (reactflowUtils.ts): refactor code to improve readability and remove unnecessary code ♻️ (reactflowUtils.ts): refactor code to improve readability and remove unnecessary code ♻️ (reactflowUtils.ts): refactor code to improve readability and remove unnecessary code ♻️ (reactflowUtils.ts): refactor code to improve readability and remove unnecessary code ♻️ (reactflowUtils.ts): refactor code to improve readability and remove unnecessary code ♻️ (reactflowUtils.ts): refactor code to improve readability and remove unnecessary code ♻️ (reactflowUtils.ts): refactor code to improve readability and remove unnecessary code ♻️ (reactflowUtils.ts): refactor code to improve readability and remove unnecessary code ♻️ (reactflowUtils.ts): refactor code to improve readability and remove unnecessary code ♻️ (reactflowUtils.ts): refactor code to improve readability and remove unnecessary code ♻️ (reactflowUtils.ts): refactor code to improve readability and remove unnecessary code ♻️ (reactflowUtils.ts): refactor code to improve readability and remove unnecessary code ♻️ (reactflowUtils.ts): refactor code to improve readability and remove unnecessary code ♻️ (reactflowUtils.ts): refactor code to improve readability and remove unnecessary code ♻️ (reactflowUtils.ts): refactor code to improve readability and remove unnecessary code ♻️ (reactflowUtils.ts): refactor code to improve readability and remove unnecessary code ♻️ (reactflowUtils.ts): refactor code to improve readability and remove unnecessary code ♻️ (reactflowUtils.ts): refactor code to improve readability and remove unnecessary code ♻️ (reactflowUtils.ts): refactor code to improve readability and remove unnecessary code ♻️ (reactflowUtils.ts): refactor code to improve readability * refactor: Update FlowLogsModal to fetch and display messages table based on active tab * update package lock * 🐛 (folders.py): import missing dependencies and update code to handle folder components and flows 🐛 (folders.py): fix typo in update statement 🐛 (folders.py): fix typo in update statement 🐛 (folders.py): fix typo in update statement 🐛 (folders.py): fix typo in update statement 🐛 (folders.py): fix typo in update statement 🐛 (folders.py): fix typo in update statement 🐛 (folders.py): fix typo in update statement 🐛 (folders.py): fix typo in update statement 🐛 (folders.py): fix typo in update statement 🐛 (folders.py): fix typo in update statement 🐛 (folders.py): fix typo in update statement 🐛 (folders.py): fix typo in update statement 🐛 (folders.py): fix typo in update statement 🐛 (folders.py): fix typo in update statement 🐛 (folders.py): fix typo in update statement 🐛 (folders.py): fix typo in update statement 🐛 (folders.py): fix typo in update statement 🐛 (folders.py): fix typo in update statement 🐛 (folders.py): fix typo in update statement 🐛 (folders.py): fix typo in update statement 🐛 (folders.py): fix typo in update statement 🐛 (folders.py): fix typo in update statement 🐛 (folders.py): fix typo in update statement 🐛 (folders.py): fix typo in update statement 🐛 (folders.py): fix typo in update statement 🐛 (folders.py): fix typo in update statement 🐛 (folders.py): fix typo in update statement 🐛 (folders.py): fix typo in update statement 🐛 (folders.py): fix typo in update statement 🐛 (folders.py): fix typo in update statement 🐛 (folders.py): fix typo in update statement 🐛 (folders.py): fix typo in update statement 🐛 (folders.py): fix typo in update statement 🐛 (folders.py): fix typo in update statement 🐛 (folders.py): fix typo in update statement 🐛 (folders.py): fix typo in update statement 🐛 (folders.py): fix typo in update statement 🐛 (folders.py): fix typo in update statement 🐛 (folders.py): fix typo in update statement 🐛 (folders.py): fix typo in update statement 🐛 (folders.py): fix typo in update statement 🐛 (folders.py): fix typo in update statement 🐛 (folders.py): fix typo in update statement 🐛 (folders.py): fix typo in update statement 🐛 (folders.py): fix typo in update statement 🐛 (folders.py): fix typo in update statement 🐛 (folders.py): fix typo in update statement 🐛 (folders.py): fix typo in update statement 🐛 (folders.py): fix typo in update statement 🐛 (folders.py): fix typo in update statement 🐛 (folders.py): fix typo in update statement 🐛 (folders.py): fix typo in update statement 🐛 (folders.py): fix typo in update statement 🐛 (folders.py): fix typo in update statement 🐛 (folders.py): fix typo in update statement 🐛 (folders.py): fix typo in update statement 🐛 (folders.py): fix typo in update statement 🐛 (folders.py): fix typo in update statement 🐛 (folders.py): fix typo in update statement 🐛 (folders.py): fix typo in update statement 🐛 (folders.py): fix typo in update statement 🐛 (folders.py): fix typo in update statement 🐛 (folders.py): fix typo in update statement 🐛 (folders.py): fix typo in update statement 🐛 (folders.py): fix typo in update statement 🐛 (folders.py): fix typo in update statement 🐛 (folders.py): fix typo in update statement 🐛 (folders.py): fix typo in update statement 🐛 (folders.py): fix typo in update statement 🐛 (folders.py): fix typo in update statement 🐛 (folders.py): fix typo in update statement 🐛 (folders.py): fix typo in update statement 🐛 (folders.py): fix typo in update statement 🐛 (folders.py): fix typo in update statement 🐛 (folders.py): fix typo in update statement 🐛 (folders ✨ (componentsComponent/index.tsx): add support for selecting and deselecting multiple flows/components 🔧 (componentsComponent/index.tsx): update import statement for react-hook-form to include useWatch ♻️ (componentsComponent/index.tsx): refactor handleSelectAll function to only select flows from folder ♻️ (componentsComponent/index.tsx): refactor handleSelectOptionsChange function to check selectedFlowsComponentsCards length ♻️ (componentsComponent/index.tsx): refactor handleDeleteMultiple function to use selectedFlowsComponentsCards ✨ (componentsComponent/index.tsx): update selectedFlowsComponentsCards state when form values change ♻️ (componentsComponent/index.tsx): refactor getDescriptionModal to use useMemo 🐛 (inputSearchComponent/index.tsx): disable input search when loading, no flows, or no searchFlowsComponents ♻️ (flowsManagerStore.ts): add selectedFlowsComponentsCards state and setSelectedFlowsComponentsCards function 📝 (zustand/flowsManager/index.ts): update FlowsManagerStoreType to include selectedFlowsComponentsCards state and setSelectedFlowsComponentsCards function 📝 (deleteComponentFlows.spec.ts): update confirmation message for deleting a component * 🐛 (index.tsx): filter out flows without a folder_id to prevent errors when mapping over flows 🐛 (index.ts): add folder_id property to FlowType to properly handle flows with a folder_id * 📝 (sidebarComponent): remove console.log statement for items variable ♻️ (sidebarComponent): refactor sideBarButtons component to fix button width and improve styling ♻️ (sidebarComponent): refactor sideBarFolderButtons component to fix folder name truncation and improve styling ♻️ (sidebarComponent): refactor sidebarNav component to fix className prop ♻️ (mainPage): refactor HomePage component to remove unnecessary parentheses and fix indentation * 📝 (on-file-drop.tsx): import `useLocation` from `react-router-dom` to use location state in the component ♻️ (on-file-drop.tsx): refactor `useFlowsManagerStore` to `useFolderStore` to use the correct store for getting folder data ✨ (on-file-drop.tsx): add `location` and `folderId` variables to get the folder id from the location state ✨ (on-file-drop.tsx): call `getFolderById` function instead of `setAllFlows` to update the folder data after successful upload * Implemented Dict modal on Cell Editor for objects * ✨ (sideBarFolderButtons/index.tsx): add support for file drop functionality in the sidebar folder buttons component 📝 (use-on-file-drop.tsx): create a custom hook for handling file drop functionality in the sidebar component 📝 (componentsComponent/index.tsx): update import statement for the useFileDrop hook in the components component 📝 (use-delete-folder.tsx): create a custom hook for handling folder deletion in the MainPage component 📝 (use-dropdown-options.tsx): create a custom hook for generating dropdown options in the MainPage component ✨ (use-on-file-drop.tsx): add a new hook for handling file drop functionality in the MainPage component ✨ (index.tsx): update import paths for hooks in the MainPage component ♻️ (flowsManagerStore.ts): refactor the addFlow function to include a new parameter 'fromDragAndDrop' to differentiate between adding a flow from drag and drop or other methods ♻️ (foldersStore.tsx): refactor the folder store to include a new state 'folderDragging' to store the folder being dragged ♻️ (index.ts): refactor the types in the flowsManager and folders store to include the new 'fromDragAndDrop' parameter * 📝 (App.tsx): Remove unnecessary line breaks and trailing commas for better code readability ♻️ (App.tsx): Refactor code to remove unused variables and dependencies ✨ (App.tsx): Add support for fetching folders on login and error handling ♻️ (popoverObject/index.tsx): Refactor code to remove unnecessary ternary operators and improve code readability ♻️ (foldersModal/component/index.tsx): Refactor code to improve code readability and consistency ♻️ (foldersModal/index.tsx): Refactor code to improve code readability and consistency ✨ (actionsMainPage.spec.ts): add end-to-end tests for selecting and deleting all items, and searching flows and components ✨ (folders.spec.ts): add end-to-end tests for CRUD operations on folders and adding a folder by drag and drop * Refactor: Change the no data table screen to a better version * refactor: Update ObjectRender component to display truncated object and provide option to see more * style(objectRender): add hover effect to object render component for better user experience style(tailwind.config.js): add slow-wiggle animation to tailwind config for smoother animation effect * 📝 (inputComponent/index.tsx): remove unnecessary whitespace in className prop to improve code readability 📝 (inputComponent/index.tsx): update id prop value to include object id for better identification 📝 (inputComponent/index.tsx): update id prop value to include object id for better identification 📝 (inputComponent/index.tsx): remove unnecessary whitespace in className prop to improve code readability 📝 (inputComponent/index.tsx): remove unnecessary whitespace in className prop to improve code readability 📝 (inputComponent/index.tsx): remove unnecessary whitespace in className prop to improve code readability 📝 (inputComponent/index.tsx): remove unnecessary whitespace in className prop to improve code readability 📝 (inputComponent/index.tsx): remove unnecessary whitespace in className prop to improve code readability 📝 (inputComponent/index.tsx): remove unnecessary whitespace in className prop to improve code readability 📝 (inputComponent/index.tsx): remove unnecessary whitespace in className prop to improve code readability 📝 (inputComponent/index.tsx): remove unnecessary whitespace in className prop to improve code readability 📝 (inputComponent/index.tsx): remove unnecessary whitespace in className prop to improve code readability 📝 (sidebarComponent/components/sideBarButtons/index.tsx): remove unused item.icon prop 📝 (sidebarComponent/index.tsx): add isFolderPath variable to check if current path is a folder path 📝 (sidebarComponent/index.tsx): add isFolderPath variable to check if current path is a folder path 📝 (foldersModal/component/index.tsx): update id prop value for flow input component 📝 (foldersModal/component/index.tsx): update id prop value for component input component 📝 (end-to-end/actionsMainPage.spec.ts): update getByText assertions to include { exact: true } option for more accurate matching ✨ (chatInputOutput.spec.ts): update test case to improve readability and remove unnecessary characters in input text 📝 (chatInputOutput.spec.ts): update test case to improve readability and remove unnecessary characters in input text ✨ (chatInputOutput.spec.ts): update test case to improve readability and remove unnecessary characters in input text 📝 (chatInputOutput.spec.ts): update test case to improve readability and remove unnecessary characters in input text 📝 (chatInputOutput.spec.ts): update test case to improve readability and remove unnecessary characters in input text 📝 (chatInputOutput.spec.ts): update test case to improve readability and remove unnecessary characters in input text 📝 (chatInputOutput.spec.ts): update test case to improve readability and remove unnecessary characters in input text 📝 (chatInputOutput.spec.ts): update test case to improve readability and remove unnecessary characters in input text 📝 (chatInputOutput.spec.ts): update test case to improve readability and remove unnecessary characters in input text 📝 (chatInputOutput.spec.ts): update test case to improve readability and remove unnecessary characters in input text 📝 (chatInputOutput.spec.ts): update test case to improve readability and remove unnecessary characters in input text 📝 (chatInputOutput.spec.ts): update test case to improve readability and remove unnecessary characters in input text 📝 (chatInputOutput.spec.ts): update test case to improve readability and remove unnecessary characters in input text 📝 (chatInputOutput.spec.ts): update test case to improve readability and remove unnecessary characters in input text 📝 (chatInputOutput.spec.ts): update test case to improve readability and remove unnecessary characters in input text 📝 (chatInputOutput.spec.ts): update test case to improve readability and remove unnecessary characters in input text 📝 (chatInputOutput.spec.ts): update test case to improve readability and remove unnecessary characters in input text 📝 (chatInputOutput.spec.ts): update test case to improve readability and remove unnecessary characters in input text 📝 (chatInputOutput.spec.ts): update test case to improve readability and remove unnecessary characters in input text 📝 (chatInputOutput.spec.ts): update test case to improve readability and remove unnecessary characters in input text 📝 (chatInputOutput.spec.ts): update test case to improve readability and remove unnecessary characters in input text 📝 (chatInputOutput.spec.ts): update test case to improve readability and remove unnecessary characters in input text 📝 (chatInputOutput.spec.ts): update test case to improve readability and remove unnecessary characters in input text 📝 (chatInputOutput.spec.ts): update test case to improve readability and remove unnecessary characters in input text 📝 (chatInputOutput.spec.ts): update test case to improve readability and remove unnecessary characters in input text 📝 (chatInputOutput.spec.ts): update test case to improve readability and remove unnecessary characters in input text 📝 (chatInputOutput.spec.ts): update test case to improve readability and remove unnecessary characters in input text 📝 (chatInputOutput.spec.ts): update test case to improve readability and remove unnecessary characters in input text 📝 (chatInputOutput.spec.ts): update test case to improve readability and remove unnecessary characters in input text 📝 (chatInputOutput.spec.ts): update test case to improve readability and remove unnecessary characters in input text 📝 (chatInputOutput.spec.ts): update test case to improve readability and remove unnecessary characters in input text 📝 (chatInputOutput.spec.ts): update test case to improve readability and remove unnecessary characters in input text 📝 (chatInputOutput.spec.ts): update test case to improve readability and remove unnecessary characters in input text 📝 (chatInputOutput.spec.ts): update test case to improve readability and remove unnecessary characters in input text 📝 (chatInputOutput.spec.ts): update test case to improve readability and remove unnecessary characters in input text 📝 (chatInputOutput.spec.ts): update test case to improve readability and remove unnecessary characters in input text 📝 (chatInputOutput.spec.ts): update test case to improve readability and remove unnecessary characters in input text 📝 (chatInputOutput.spec.ts): update test case to improve readability and remove unnecessary characters in input text 📝 (chatInputOutput.spec.ts): update test case to improve readability and remove unnecessary characters in input text 📝 (chatInputOutput.spec.ts): update test case to improve readability and remove unnecessary characters in input text 📝 (chatInputOutput.spec.ts): update test case to improve readability and remove unnecessary characters in input text 📝 (chatInputOutput.spec.ts): update test case to improve readability and remove ✅ (nestedComponent.spec.ts): remove unnecessary code related to showpinecone_env checkbox ✅ (store.spec.ts): use environment variable STORE_API_KEY instead of hardcoding the API key * 📝 (inputComponent/index.tsx): update id prop value to include "popover-anchor-" prefix for better identification and accessibility ✨ (sidebarComponent/components/sideBarButtons/index.tsx): add react-router-dom Link component to wrap each sidebar button item for navigation functionality ♻️ (sidebarComponent/index.tsx): refactor isFolderPath logic to use array of path values and check if any of them is included in the current pathname for better readability and maintainability * Refactor: use shadcn alert when there is no data * Remove unnecessary quotes * Refactor: add border to no data alert * Fix: record output not using table as it should * 📝 (cardComponent/index.tsx): remove redundant "selected" from description prop in DeleteConfirmationModal component 📝 (componentsComponent/index.tsx): remove redundant "selected" from getDescriptionModal function * ✨ (logs.spec.ts): add end-to-end test for viewing and interacting with logs in the frontend 📝 (logs.spec.ts): add documentation comments to improve code readability and maintainability * 📝 (folders.py): add support for downloading all flows from a folder as a file 📝 (folders.py): add support for uploading flows from a file to a folder ✨ (index.tsx): add handleDownloadFolderFn utility function to handle downloading flows from a folder ✨ (index.tsx): add handleUploadFlowsToFolder function to handle uploading flows to a folder 📝 (services/index.ts): add downloadFlowsFromFolders function to make API call for downloading flows from a folder 📝 (services/index.ts): add uploadFlowsFromFolders function to make API call for uploading flows to a folder 📝 (handle-download-folder.ts): create handleDownloadFolderFn utility function to handle downloading flows from a folder ✨ (foldersStore.tsx): add support for uploading flows from folders 📝 (foldersStore.tsx): update types to include uploadFolder function in FoldersStoreType * 📝 (folders.py): remove unnecessary whitespace to improve code readability 📝 (folders.py): remove unnecessary whitespace to improve code readability * style: update CSS in App.css to improve scrollbar appearance feat: add TableComponent to CsvOutputComponent for better table rendering refactor: remove unused code and improve readability in CsvOutputComponent refactor: simplify logic in TableAutoCellRender component feat: add autoHeight property to columns in extractColumnsFromRows utility function * 🐛 (folders.py): fix issue where components and flows were not being assigned to the new folder 🐛 (folders.py): fix issue where components and flows were not being assigned to the new folder 🐛 (sideBarFolderButtons/index.tsx): fix issue where folder buttons were not taking up full width 🐛 (use-on-file-drop.tsx): fix issue where folder dragging was not being reset on drag leave 🐛 (use-on-file-drop.tsx): fix issue where folder dragging was not being reset on drag leave 🐛 (use-on-file-drop.tsx): fix issue where folder dragging was not being reset on drag leave 🐛 (entities/index.tsx): fix issue where AddFolderType was missing flows and components properties 🐛 (services/index.ts): fix issue where addFolder function was not correctly sending flows and components data * 📝 (model.py): remove unnecessary whitespace 📝 (index.tsx): remove unused 'pathname' prop 📝 (index.tsx): add 'handleAddFolder' prop to SideBarFoldersButtonsComponent 📝 (index.tsx): remove unused 'handleAddFolder' prop from SideBarButtonsComponent 📝 (index.tsx): remove unused import of DropdownButton in sideBarFolderButtons 📝 (index.tsx): add DropdownButton component to SideBarFoldersButtonsComponent 📝 (index.tsx): add 'handleAddFolder' prop to SideBarFoldersButtonsComponent 📝 (use-on-file-drop.tsx): remove console.log statements 📝 (index.tsx): remove console.log statements 📝 (index.tsx): remove unused import of FolderPlusIcon in mainPage 📝 (index.tsx): remove unused sidebarNavItems array in mainPage * Refactor: make select all look more like a button * feat: Add first step of drag and drop functionality to CollectionCardComponent * 📝 (langflow-pre.db): add new langflow-pre.db file to the backend/base/langflow directory ✨ (index.tsx): improve modal header description by dynamically displaying "Edit a folder" or "Add a new folder" based on the presence of folderToEdit prop * remove api key * remove api key * remove api key * Refactor: Update downloadFlowsFromFolders function to include folder name in response * add type to folder function * Refactor: Update chatComponent and sideBarFolderButtons components This commit refactors the chatComponent and sideBarFolderButtons components. In chatComponent: - Moved the declaration of the 'currentFlow' variable to ensure it is defined before being used. - Removed the unused 'hasIO' and 'hasStore' variables. - Reordered the imports for better organization. In sideBarFolderButtons: - Added imports for 'useStoreStore' and 'ShadTooltip' components. - Removed the unused 'hasStore', 'validApiKey', and 'hasApiKey' variables. - Removed the unused 'handleEditFolder' function. - Added a new button with an icon for sharing as a bundle, with a tooltip indicating the need to review the API key before sharing. These changes improve the code structure and remove unused code, enhancing the overall maintainability and user experience of the application. * copy folder modal structure to start bundle modal * new lock * refactor: Move no data alert rendering logic to a separate function * refactor: Move no data alert rendering logic to a separate function * add truncate to json objects * Refactor: store flow_id in ChatComponent's records in ChatComponent * 📝 (folders.py): add missing import for FolderBase model 🐛 (folders.py): fix issue where flows were not being fetched for a folder 🐛 (folders.py): fix issue where flows were not being deleted when a folder is deleted 🐛 (folders.py): fix issue where folder description was not being returned when downloading flows ✨ (folders.py): add support for uploading flows from a file 🐛 (schemas.py): fix issue where folder description was not included in FlowListReadWithFolderName schema ♻️ (sideBarButtons/index.tsx): refactor handleOpenNewFolderModal prop to be optional ✨ (sideBarFolderButtons/index.tsx): make handleChangeFolder, handleEditFolder, handleDeleteFolder, handleAddFolder optional to improve component reusability ♻️ (sideBarFolderButtons/index.tsx): refactor useFileDrop hook to use async/await syntax and separate file upload logic into a separate function ✨ (sideBarFolderButtons/index.tsx): add support for uploading flows from folders using the uploadFlowsFromFolders API ♻️ (sideBarFolderButtons/index.tsx): refactor handleFileDrop function to handle multiple files and use FormData to send file data to the server ♻️ (sideBarFolderButtons/index.tsx): refactor dragOver, dragEnter, dragLeave, and onDrop functions to remove unnecessary folderId parameter and set folderDragging state to a boolean value instead of an empty string 📝 (sidebarComponent/index.tsx): make handleOpenNewFolderModal, handleChangeFolder, handleEditFolder, handleDeleteFolder optional to allow flexibility in using the component 📝 (foldersModal/component/index.tsx): add allFlows variable to get all flows from the store and use it to filter components and flows on the folder being edited 📝 (foldersModal/hooks/submit-folder.tsx): import useNavigate from react-router-dom and use it to navigate to the folder page after creating or updating a folder 📝 (pages/MainPage/entities/index.tsx): add StarterProjectsType to define the type of starter projects 📝 (pages/MainPage/pages/mainPage/index.tsx): import useAlertStore from stores/alertStore and use it to set error data when trying to download an empty folder 📝 (services/index.ts): add StarterProjectsType import to support the new entity in the code 📝 (services/index.ts): add return type to updateFolder function to improve code clarity 📝 (services/index.ts): add return type to getFolderById function to improve code clarity 📝 (services/index.ts): add return type to getStarterProjects function to improve code clarity 📝 (services/index.ts): add folder_description property to the return type of downloadFlowsFromFolders function to provide additional information about the folder 📝 (services/index.ts): remove folderId parameter from uploadFlowsFromFolders function as it is not needed 📝 (utils/handle-download-folder.ts): add folder_name and folder_description properties to the data object to provide additional information about the folder being downloaded 📝 (SettingsPage/index.tsx): remove commented out code for unused settings options 📝 (stores/flowsManagerStore.ts): add missing comma in setCurrentFlowId function 📝 (stores/flowsManagerStore.ts): add return type to saveFlow function to improve code clarity 📝 (stores/flowsManagerStore.ts): add return type to updateFlow function to improve code clarity 📝 (stores/flowsManagerStore.ts): add return type to addFlow function to improve code clarity 📝 (stores/flowsManagerStore.ts): add return type to deleteFlow function to improve code clarity 📝 (stores/flowsManagerStore.ts): add return type to addFlowComponent function to improve code clarity 📝 (stores/flowsManagerStore.ts): add return type to takeSnapshot function to improve code clarity 📝 (stores/flowsManagerStore.ts): add return type to undo function to improve code clarity 📝 (stores/flowsManagerStore.ts): add return type to redo function to improve code clarity ♻️ (foldersStore.tsx): change folderDragging variable type from string to boolean to improve semantics and consistency ♻️ (foldersStore.tsx): remove unused folderId parameter from uploadFolder function ♻️ (foldersStore.tsx): remove unused setAllFlows function call ♻️ (folders/index.ts): change folderDragging variable type from string to boolean to match the updated type in foldersStore.tsx * 🐛 (folders.py): fix indentation and remove unnecessary whitespace ✨ (folders.py): add logic to handle duplicate folder names by appending a number to the folder name 📝 (folders.py): update comments and documentation * 🐛 (submit-folder.tsx): remove unnecessary comma after closing curly brace in error handling function 🐛 (index.tsx): remove unnecessary comma after closing parenthesis in state selectors ♻️ (index.tsx): refactor code to simplify logic for getting folder by ID and handling default case * refactor: Update add_row_to_table function to use list comprehension for values * Refactor: Update placeholder text capitalization in headerComponent and inputSearchComponent In headerComponent: - Changed "Select all" to "Select All" for better consistency and readability. In inputSearchComponent: - Changed "Search flows" to "Search Flows" and "Search components" to "Search Components" for better consistency and readability. These changes improve the user experience and maintain consistency in the application. * Modularized scroll fade and added it to folders * refactor(componentsComponent): remove unnecessary switch statement in handleSelectOptionsChange function feat(headerComponent): replace Select component with a Button component for delete action feat(headerComponent): add disableDelete prop to Button component to handle delete button state based on selected items * Made selector not disappear after hover if selected * fixed selector * 🐛 (folders.py): fix updating folder components and flows logic ✨ (folders.py): add support for moving excluded flows to "My Collection" folder * ♻️ (folders.py): remove unnecessary whitespace 🐛 (folders.py): fix indentation issue in update_folder function * refactor(headerComponent): replace Select component with Button component for delete action * refactor: Handle float conversion errors in validate_id method * Fix adding primary key * Refactor: remove trash from card and make checkbox always visible * Refactor: add padding on card title to avoid bugs * chore: Add h-full class to sideBarFolderButtons component * 📝 (api.tsx): add import statement for useUtilityStore from utilityStore to use the utility store in the API interceptor 📝 (api.tsx): add lastUrlCalled and setLastUrlCalled variables to store and retrieve the last URL called in the API interceptor 📝 (api.tsx): add logic to check for duplicate requests in the API interceptor based on the last URL called 📝 (api.tsx): add localStorage to store the last URL called in the API interceptor 📝 (api.tsx): add logic to add access token to every request in the API interceptor ♻️ (index.tsx): refactor selectedFolder?.flows to remove unnecessary parentheses in ComponentsComponent ♻️ (index.tsx): refactor state.searchFlowsComponents.toLowerCase() to remove unnecessary parentheses in ComponentsComponent ♻️ (index.tsx): refactor state.selectedFlowsComponentsCards to remove unnecessary parentheses in ComponentsComponent ♻️ (index.tsx): refactor (f.is_component ?? false) === is_component to remove unnecessary parentheses in ComponentsComponent 📝 (utilityStore.ts): add lastUrlCalled and setLastUrlCalled variables to utility store to store and retrieve the last URL called * 📝 (api.tsx): remove unused import of useUtilityStore from utilityStore ♻️ (api.tsx): remove unused variables lastUrlCalled and setLastUrlCalled from useUtilityStore 📝 (utilityStore.ts): remove unused variable lastUrlCalled and setLastUrlCalled from utilityStore * refactor: Add flow_id parameter to log_message function * fix undefined bug * refactor: Update add_row_to_table function to use list comprehension for values * refactor: Add flow_id field to FlowCreate and FlowRead models * refactor: Update FlowCreate and FlowRead models to use folder_id instead of flow_id * Refactor: make drag n drop works in the entire screen * ⬆️ (frontend/package.json): upgrade "@playwright/test" dependency from version 1.43.1 to 1.44.0 ✨ (frontend/tests/end-to-end/actionsMainPage.spec.ts): replace the usage of XPath locator with text locator for better readability and maintainability ✨ (frontend/tests/end-to-end/actionsMainPage.spec.ts): replace the usage of "Actions" and "Delete" text locators with "icon-Trash2" locator for better specificity ✨ (frontend/tests/end-to-end/actionsMainPage.spec.ts): replace the usage of "Delete" text locator with "Delete" button locator for better specificity ✨ (frontend/tests/end-to-end/actionsMainPage.spec.ts): replace the usage of "Select All" text locator with "Select All" button locator for better specificity ✨ (frontend/tests/end-to-end/actionsMainPage.spec.ts): replace the usage of "Unselect All" text locator with "Unselect All" button locator for better specificity ✨ (frontend/tests/end-to-end/actionsMainPage.spec.ts): replace the usage of "Actions" text locator with "icon-Trash2" locator for better specificity ✨ (frontend/tests/end-to-end/actionsMainPage.spec.ts): replace the usage of "Delete" text locator with "Delete" button locator for better specificity ✨ (frontend/tests/end-to-end/actionsMainPage.spec.ts): replace the usage of "Select All" text locator with "Select All" button locator for better specificity ✨ (frontend/tests/end-to-end/actionsMainPage.spec.ts): replace the usage of "New Project" XPath locator with "New Project" text locator for better readability and maintainability ✨ (frontend/tests/end-to-end/actionsMainPage.spec.ts): replace the usage of "New Project" XPath locator with "New Project" text locator for better readability and maintainability ✨ (frontend/tests/end-to-end/actionsMainPage.spec.ts): replace the usage of "New Project" XPath locator with "New Project" text locator for better readability and maintainability ✨ (frontend/tests/end-to-end/actionsMainPage.spec.ts): replace the usage of "New Project" XPath locator with "New Project" text locator for better readability and maintainability ✨ (frontend/tests/end-to-end/actionsMainPage.spec.ts): replace the usage of "New Project" XPath locator with "New Project" text locator for better readability and maintainability ✨ (frontend/tests/end-to-end/actionsMainPage.spec.ts): replace the usage of "New Project" XPath locator with "New Project" text locator for better readability and maintainability ✨ (frontend/tests/end-to-end/actionsMainPage.spec.ts): replace the usage of "New Project" XPath locator with "New Project" text locator for better readability and maintainability ✨ (frontend/tests/end-to-end/actionsMainPage.spec.ts): replace the usage of "New Project" XPath locator with "New Project" text locator for better readability and maintainability ✨ (frontend/tests/end-to-end/actionsMainPage.spec.ts): replace the usage of "New Project" XPath locator with "New Project" text locator for better readability and maintainability ✨ (frontend/tests/end-to-end/actionsMainPage.spec.ts): replace the usage of "New Project" XPath locator with "New Project" text locator for better readability and maintainability ✨ (frontend/tests/end-to-end/actionsMainPage.spec.ts): replace the usage of "New Project" XPath locator with "New Project" text locator for better readability and maintainability ✨ (frontend/tests/end-to-end/actionsMainPage.spec.ts): replace the usage of "New Project" XPath locator with "New Project" text locator for better readability and maintainability ✨ (frontend/tests/end-to-end/actionsMainPage.spec.ts): replace the usage of "New Project" XPath locator with "New Project" text locator for better readability and maintainability ✨ (frontend/tests/end-to-end/actionsMainPage.spec.ts): replace the usage of "New Project" XPath locator with "New Project" text locator for better readability and maintainability ✨ (frontend/tests/end-to-end/actionsMainPage.spec.ts): replace the usage of "New Project" XPath locator with "New Project" text locator for better readability and maintainability ✨ (frontend/tests/end-to-end/actionsMainPage.spec.ts): replace the usage of "New Project" XPath locator with "New Project" text locator for better readability and maintainability ✨ (frontend/tests/end-to-end/actionsMainPage.spec.ts): replace the usage of "New Project" XPath locator with "New Project" text locator for better readability and maintainability ✨ (frontend/tests/end-to-end/actionsMainPage.spec.ts): replace the usage of "New Project" XPath locator with "New Project" text locator for better readability and maintainability ✨ (frontend/tests/end ✨ (floatComponent.spec.ts): update selector for clicking "New Project" button to improve test reliability ✨ (flowPage.spec.ts): update selector for clicking "New Project" button to improve test reliability ✨ (flowSettings.spec.ts): update selector for clicking "New Project" button to improve test reliability ✨ (folders.spec.ts): update selector for clicking "New Project" button to improve test reliability ✨ (folders.spec.ts): update selector for clicking "New Folder" button to improve test reliability ✨ (folders.spec.ts): update selector for clicking "Edit Folder" button to improve test reliability ✨ (folders.spec.ts): update selector for dispatching drop event to improve test reliability ✨ (globalVariables.spec.ts): update selector for clicking "New Project" button to improve test reliability ✨ (group.spec.ts): update selector for clicking "New Project" button to improve test reliability ✨ (inputComponent.spec.ts): update selector for clicking "New Project" button to improve test reliability ✨ (inputListComponent.spec.ts): update selector for clicking "New Project" button to improve test reliability ✨ (intComponent.spec.ts): update selector for clicking "New Project" button to improve test reliability ✨ (keyPairListComponent.spec.ts): update selector for clicking "New Project" button to improve test reliability ✨ (langflowShortcuts.spec.ts): update selector for clicking "New Project" button to improve test reliability ✨ (nestedComponent.spec.ts): update selector for clicking "New Project" button to improve test reliability ✨ (promptModalComponent.spec.ts): update selector for clicking "New Project" button to improve test reliability ✨ (python_api_generation.spec.ts): update selector for clicking "New Project" button to improve test reliability ✨ (saveComponents.spec.ts): update selector for clicking "New Project" button to improve test reliability and maintainability ✨ (store.spec.ts): update selector for clicking "New Project" button to improve test reliability and maintainability ✨ (textAreaModalComponent.spec.ts): update selector for clicking "New Project" button to improve test reliability and maintainability ✨ (textInputOutput.spec.ts): update selector for clicking "New Project" button to improve test reliability and maintainability ✨ (toggleComponent.spec.ts): update selector for clicking "New Project" button to improve test reliability and maintainability ✨ (tweaks_test.spec.ts): update selector for clicking "New Project" button to improve test reliability and maintainability 📝 (test-results/.last-run.json): add .last-run.json file to track test run status * Refactor: make drag n drop only happen in the folder div * refactor: Fix incorrect variable assignment in memory.py * refactor: Update data retrieval in InterfaceVertex to use record data instead of model_dump * refactor: Update DateReader component to use 12-hour time format * refactor: Update activeTab state variable in FlowLogsModal component * refactor: Update FlowCreate and FlowRead models to use folder_id instead of flow_id * refactor: Update hover animation in ObjectRender component * refactor: Update icon in FlowLogsModal component * add table preview on IO * refactor: Add truncate class to StringReader component * refactor: Add TableAutoCellRender support for displaying badges * refactor: Add filter option to extractColumnsFromRows function * update card width * style(IOFieldView): update className condition to dynamically set height based on 'left' prop value * fix(IOFieldView): update height class value from "h-36" to "h-56" for better UI consistency fix(FlowLogsModal): update BaseModal.Header description based on activeTab value for dynamic content display * Update BaseModal.Header description in FlowLogsModal component * refactor: Update dict_values_to_string function to use deepcopy for dictionary copy * 📝 (sideBarFolderButtons): Remove unused variables and improve code readability 📝 (api): Remove unnecessary error handling and improve code readability 📝 (componentsComponent): Remove unused variables and improve code readability 📝 (foldersStore): Remove unnecessary error handling and improve code readability * 📝 (App.tsx): remove unnecessary call to getFoldersApi() before setting loading state to false ♻️ (App.tsx): refactor code to navigate to "/all" instead of "/flows" when window location pathname is "/" * refactor: Update FlowCreate and FlowRead models to use folder_id instead of flow_id * refactor: Update FlowCreate and FlowRead models to use folder_id instead of flow_id * refactor: Update MyCollectionComponent to use "type" prop instead of "is_component" * refactor: Update error handling in API interceptor * refactor: Update StoreGuard component to navigate to "/all" instead of "/flows" when there is no store * chore(constants.ts): add DEFAULT_FOLDER constant for improved code readability refactor(index.tsx): update title and description logic to use constants for consistency feat(foldersStore.tsx): utilize DEFAULT_FOLDER constant for folder name comparison to improve maintainability and readability * lint * reduce navbar size * chore: Update className in mainPage/index.tsx to use relative width for folder button * refactor: Remove unnecessary call to getFoldersApi() and refactor code in App.tsx * refactor: Update default column width in TableComponent * Refactor: Change folders actions buttons to another location * ✨ (cardComponent/index.tsx): refactor useState calls to remove unnecessary commas and improve code readability 📝 (cardComponent/index.tsx): remove unnecessary semicolon and fix indentation ♻️ (cardComponent/index.tsx): remove unnecessary semicolon and fix indentation ✨ (cardComponent/index.tsx): refactor onClick handler to remove unnecessary ternary operator ♻️ (cardComponent/index.tsx): remove unnecessary semicolon and fix indentation ✨ (cardComponent/index.tsx): refactor onClick handler to remove unnecessary ternary operator ♻️ (cardComponent/index.tsx): remove unnecessary semicolon and fix indentation ✨ (cardComponent/index.tsx): refactor onClick handler to remove unnecessary ternary operator ♻️ (cardComponent/index.tsx): remove unnecessary semicolon and fix indentation ✨ (cardComponent/index.tsx): refactor onClick handler to remove unnecessary ternary operator ♻️ (cardComponent/index.tsx): remove unnecessary semicolon and fix indentation ✨ (cardComponent/index.tsx): refactor onClick handler to remove unnecessary ternary operator ♻️ (cardComponent/index.tsx): remove unnecessary semicolon and fix indentation ✨ (cardComponent/index.tsx): refactor onClick handler to remove unnecessary ternary operator ♻️ (cardComponent/index.tsx): remove unnecessary semicolon and fix indentation ✨ (cardComponent/index.tsx): refactor onClick handler to remove unnecessary ternary operator ♻️ (cardComponent/index.tsx): remove unnecessary semicolon and fix indentation ✨ (cardComponent/index.tsx): refactor onClick handler to remove unnecessary ternary operator ♻️ (cardComponent/index.tsx): remove unnecessary semicolon and fix indentation ✨ (cardComponent/index.tsx): refactor onClick handler to remove unnecessary ternary operator ♻️ (cardComponent/index.tsx): remove unnecessary semicolon and fix indentation ✨ (cardComponent/index.tsx): refactor onClick handler to remove unnecessary ternary operator ♻️ (cardComponent/index.tsx): remove unnecessary semicolon and fix indentation ✨ (cardComponent/index.tsx): refactor onClick handler to remove unnecessary ternary operator ♻️ (cardComponent/index.tsx): remove unnecessary semicolon and fix indentation ✨ (cardComponent/index.tsx): refactor onClick handler to remove unnecessary ternary operator ✨ (use-on-file-drop.tsx): refactor handleFileDrop function to use uploadFormData function for better code organization and readability * get error from folder * refactor: Handle error when updating folder in submit-folder.tsx * refactor: Update submit-folder.tsx to handle folder submission and error handling consistently * Refactor: Rename folders buttons and add search input icon * Fixed padding on select * Fix: Store tags displaying as a column * fixed checkbox color on card * Implemented draggable small folder * Refactor: Add padding to search input * fixed flow not dropping * Fixed flow and component dropping bugs * Refactor: Update ComponentsComponent to improve code readability and remove unnecessary code * Fixed deleting issue when it doesnt update on creating new folder * refactor: Update activeTab name in FlowLogsModal component * Removed onDelete of card component * update logs modal postion * Refactor: Make folders buttons the same size * Refactor: Position download folder button in a better parent * Refactor: Update folder_id when moving a flow to a different folder * Refactor: Update folder_id when moving a flow to a different folder * Refactor: Update folder_id when moving a flow to a different folder * chore: Remove unnecessary comma in API interceptor code * Refactor: Remove unused code and improve folder button behavior * Refactor: Improve code readability and remove unnecessary code in ComponentsComponent * Refactor: Update folder_id when moving a flow to a different folder * feat(sidebarComponent): add support for downloading folders with flows fix(constants): change DEFAULT_FOLDER constant value to "My Projects" for clarity refactor(emptyComponent): update text color and alignment for better readability style(headerTabsSearchComponent): remove download button from header tabs search component style(inputSearchComponent): adjust width of input search component for better UI consistency * merge on dev * fixing migration * removing db * 📝 (use-on-file-drop.tsx): add import statement for useFlowsManagerStore to use the refreshFlows function ✨ (use-on-file-drop.tsx): call refreshFlows function after uploading flows to update the flows list 📝 (foldersStore.tsx): remove unnecessary comma and fix indentation ✨ (foldersStore.tsx): call refreshFlows function after uploading flows to update the flows list * feat(modals): update folder modal title and icon * fix(cardsWrapComponent): add useEffect hook to handle visibility change when tab becomes visible to reset hover state and improve user experience * 🐛 (popover/index.tsx): fix indentation and remove unnecessary ternary operator 🐛 (popover/index.tsx): fix className prop to prevent it from being undefined 🐛 (inputComponent/index.tsx): prevent event propagation and default behavior when clicking on the button inside InputComponent * ✨ (index.tsx): add useEffect import to fix missing dependency warning and improve code readability ♻️ (index.tsx): remove unused useEffect function implementation to clean up code * ✨ (foldersStore.tsx): add call to refreshFlows() method in useFlowsManagerStore to update flows after loading folders * fix(langflow): add missing index 'ix_flow_folder_id' on 'flow' table to improve database performance * fix: add missing index 'ix_flow_folder_id' on 'flow' table * refactor(foldersModal): improve folder icon naming for better clarity and consistency * feat: add kill command to stop backend server * 📝 (App.tsx): remove unnecessary trailing commas in the useAlertStore and useGlobalVariablesStore hooks 📝 (mainPage/index.tsx): remove unused import and useEffect hook that fetches folders ♻️ (temp): delete unused temp folder * ✨ (submit-folder.tsx): update navigate path to use "all" instead of "flows" to improve consistency and clarity ✨ (mainPage/index.tsx): add call to getFoldersApi on page load to ensure folders are up to date 🐛 (chatInputOutput.spec.ts): fix selector for input-openai_api_key to use popover-anchor-input-openai_api_key 🐛 (chatInputOutput.spec.ts): fix selector for input-sender_name to use popover-anchor-input-sender_name ♻️ (chatInputOutput.spec.ts): refactor code to improve readability and remove unnecessary code ♻️ (folders.spec.ts): refactor code to improve readability and remove unnecessary code ✨ (inputComponent.spec.ts): update selectors for input elements to match changes in the frontend code ✨ (inputComponent.spec.ts): update selectors for input elements to match changes in the frontend code ✨ (inputComponent.spec.ts): update selectors for input elements to match changes in the frontend code ✨ (inputComponent.spec.ts): update selectors for input elements to match changes in the frontend code ✨ (inputComponent.spec.ts): update selectors for input elements to match changes in the frontend code ✨ (inputComponent.spec.ts): update selectors for input elements to match changes in the frontend code ✨ (inputComponent.spec.ts): update selectors for input elements to match changes in the frontend code ✨ (inputComponent.spec.ts): update selectors for input elements to match changes in the frontend code ✨ (inputComponent.spec.ts): update selectors for input elements to match changes in the frontend code ✨ (inputComponent.spec.ts): update selectors for input elements to match changes in the frontend code ✨ (inputComponent.spec.ts): update selectors for input elements to match changes in the frontend code ✨ (inputComponent.spec.ts): update selectors for input elements to match changes in the frontend code ✨ (inputComponent.spec.ts): update selectors for input elements to match changes in the frontend code ✨ (inputComponent.spec.ts): update selectors for input elements to match changes in the frontend code ✨ (inputComponent.spec.ts): update selectors for input elements to match changes in the frontend code ✨ (inputComponent.spec.ts): update selectors for input elements to match changes in the frontend code ✨ (inputComponent.spec.ts): update selectors for input elements to match changes in the frontend code ✨ (inputComponent.spec.ts): update selectors for input elements to match changes in the frontend code ✨ (inputComponent.spec.ts): update selectors for input elements to match changes in the frontend code ✨ (inputComponent.spec.ts): update selectors for input elements to match changes in the frontend code ✨ (inputComponent.spec.ts): update selectors for input elements to match changes in the frontend code ✨ (inputComponent.spec.ts): update selectors for input elements to match changes in the frontend code ✨ (inputComponent.spec.ts): update selectors for input elements to match changes in the frontend code ✨ (inputComponent.spec.ts): update selectors for input elements to match changes in the frontend code ✨ (inputComponent.spec.ts): update selectors for input elements to match changes in the frontend code ✨ (inputComponent.spec.ts): update selectors for input elements to match changes in the frontend code ✨ (inputComponent.spec.ts): update selectors for input elements to match changes in the frontend code ✨ (inputComponent.spec.ts): update selectors for input elements to match changes in the frontend code ✨ (inputComponent.spec.ts): update selectors for input elements to match changes in the frontend code ✨ (inputComponent.spec.ts): update selectors for input elements to match changes in the frontend code ✨ (inputComponent.spec.ts): update selectors for input elements to match changes in the frontend code ✨ (inputComponent.spec.ts): update selectors for input elements to match changes in the frontend code ✨ (inputComponent.spec.ts): update selectors for input elements to match changes in the frontend code ✨ (inputComponent.spec.ts): update selectors for input elements to match changes in the frontend code ✨ (inputComponent.spec.ts): update selectors for input elements to match changes in the frontend code ✨ (inputComponent.spec.ts): update selectors for input elements to match changes in the frontend code ✨ (inputComponent.spec.ts): update selectors for input elements to match changes in the frontend code ✨ (inputComponent.spec.ts): update selectors for input elements to match changes in the frontend code ✨ (inputComponent.spec.ts): update selectors for input elements to match changes in the frontend code ✨ (inputComponent.spec.ts): update selectors for input elements to match changes in the frontend code ✨ (inputComponent.spec.ts): update selectors for input elements to match changes in the frontend code ✨ (inputComponent.spec.ts): update selectors for input elements to match changes in the frontend code ✨ (inputComponent.spec.ts): update selectors for input elements to match changes in the frontend code ✨ (inputComponent.spec.ts): update selectors for input elements to match changes in the frontend code ✨ (inputComponent.spec.ts): update selectors for input elements to match changes in the frontend code ✨ (inputComponent.spec.ts): update selectors for input elements to match changes in the frontend code ✨ (inputComponent.spec.ts): update selectors for input elements to match changes in the frontend code ✨ (inputComponent.spec.ts): update selectors for input elements 🐛 (tweaks_test.spec.ts): fix selectors for input fields to match updated HTML structure * 🐛 (api.tsx): remove unnecessary comma at the end of the function 🐛 (api.tsx): remove unnecessary comma at the end of the function 🐛 (api.tsx): remove unnecessary comma at the end of the function 🐛 (api.tsx): remove unnecessary comma at the end of the function 🐛 (api.tsx): remove unnecessary comma at the end of the function 🐛 (api.tsx): remove unnecessary comma at the end of the function 🐛 (api.tsx): remove unnecessary comma at the end of the function 🐛 (api.tsx): remove unnecessary comma at the end of the function 🐛 (api.tsx): remove unnecessary comma at the end of the function 🐛 (api.tsx): remove unnecessary comma at the end of the function 🐛 (api.tsx): remove unnecessary comma at the end of the function 🐛 (api.tsx): remove unnecessary comma at the end of the function 🐛 (api.tsx): remove unnecessary comma at the end of the function 🐛 (api.tsx): remove unnecessary comma at the end of the function 🐛 (api.tsx): remove unnecessary comma at the end of the function 🐛 (api.tsx): remove unnecessary comma at the end of the function 🐛 (api.tsx): remove unnecessary comma at the end of the function 🐛 (api.tsx): remove unnecessary comma at the end of the function 🐛 (api.tsx): remove unnecessary comma at the end of the function 🐛 (api.tsx): remove unnecessary comma at the end of the function 🐛 (api.tsx): remove unnecessary comma at the end of the function 🐛 (api.tsx): remove unnecessary comma at the end of the function 🐛 (api.tsx): remove unnecessary comma at the end of the function 🐛 (api.tsx): remove unnecessary comma at the end of the function 🐛 (api.tsx): remove unnecessary comma at the end of the function 🐛 (api.tsx): remove unnecessary comma at the end of the function 🐛 (api.tsx): remove unnecessary comma at the end of the function 🐛 (api.tsx): remove unnecessary comma at the end of the function 🐛 (api.tsx): remove unnecessary comma at the end of the function 🐛 (api.tsx): remove unnecessary comma at the end of the function 🐛 (api.tsx): remove unnecessary comma at the end of the function 🐛 (api.tsx): remove unnecessary comma at the end of the function 🐛 (api.tsx): remove unnecessary comma at the end of the function 🐛 (api.tsx): remove unnecessary comma at the end of the function 🐛 (api.tsx): remove unnecessary comma at the end of the function 🐛 (api.tsx): remove unnecessary comma at the end of the function 🐛 (api.tsx): remove unnecessary comma at the end of the function 🐛 (api.tsx): remove unnecessary comma at the end of the function 🐛 (api.tsx): remove unnecessary comma at the end of the function 🐛 (api.tsx): remove unnecessary comma at the end of the function 🐛 (api.tsx): remove unnecessary comma at the end of the function 🐛 (api.tsx): remove unnecessary comma at the end of the function 🐛 (api.tsx): remove unnecessary comma at the end of the function 🐛 (api.tsx): remove unnecessary comma at the end of the function 🐛 (api.tsx): remove unnecessary comma at the end of the function 🐛 (api.tsx): remove unnecessary comma at the end of the function 🐛 (api.tsx): remove unnecessary comma at the end of the function 🐛 (api.tsx): remove unnecessary comma at the end of the function 🐛 (api.tsx): remove unnecessary comma at the end of the function 🐛 (api.tsx): remove unnecessary comma at the end of the function 🐛 (api.tsx): remove unnecessary comma at the end of the function 🐛 (api.tsx): remove unnecessary comma at the end of the function 🐛 (api.tsx): remove unnecessary comma at the end of the function 🐛 (api.tsx): remove unnecessary comma at the end of the function 🐛 (api.tsx): remove unnecessary comma at the end of the function 🐛 (api.tsx): remove unnecessary * refactor(modals): remove commented out code in FolderForms component * 📝 (cardComponent/index.tsx): add aria-label to checkbox component for accessibility improvement ✨ (deleteComponentFlows.spec.ts): update delete flow and delete component tests to use checkbox component instead of hovering over card and clicking trash icon for better test stability and reliability ✨ (group.spec.ts): update group node test to use popover anchor input for editing group title instead of directly editing the title for better test stability and reliability ✨ (logs.spec.ts): update logs test to click on "New Project" button by text instead of using locator for better test stability and reliability * ✅ (folders.spec.ts): remove unnecessary code that was clicking on elements and pressing the Escape key ♻️ (folders.spec.ts): refactor code to improve readability and remove unused variables ✨ (folders.spec.ts): add test to verify the ability to change the flow folder * fix(folders.py): handle case where no flows are found by setting flows to an empty list instead of raising a 404 error * ♻️ (folders.py): rename the function `update_folder` to `move_to_folder` to improve clarity and consistency with the endpoint URL * 📝 (folders.py): remove unused move_to_folder endpoint 🔧 (use-on-file-drop.tsx): update import statements for API controllers and services ♻️ (use-on-file-drop.tsx): refactor uploadFromDragCard function to use updateFlowInDatabase function instead of moveFlowToFolder function ♻️ (index.ts): refactor updateFlowInDatabase function to handle null folder_id values correctly ♻️ (index.tsx): refactor HomePage component to remove unnecessary setTimeout function and reduce delay for getFoldersApi function call * refactor(pyproject.toml): update version to 1.0.0a35 * refactor: remove duplicate logout response in login.py * Bump langflow-base version to 0.0.46 and annotated-types version to 0.7.0 --------- Co-authored-by: Gabriel Luiz Freitas Almeida <gabriel@logspace.ai> Co-authored-by: Lucas Oliveira <lucas.edu.oli@hotmail.com> Co-authored-by: cristhianzl <cristhian.lousa@gmail.com> Co-authored-by: igorrCarvalho <igorsilvabhz6@gmail.com> Co-authored-by: ogabrielluiz <gabriel@langflow.org>
This commit is contained in:
parent
eb60b039fb
commit
dd5bad0926
175 changed files with 6388 additions and 2903 deletions
1735
src/frontend/package-lock.json
generated
1735
src/frontend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -4,6 +4,7 @@
|
|||
"private": true,
|
||||
"dependencies": {
|
||||
"@headlessui/react": "^1.7.17",
|
||||
"@hookform/resolvers": "^3.3.4",
|
||||
"@million/lint": "^0.0.73",
|
||||
"@radix-ui/react-accordion": "^1.1.2",
|
||||
"@radix-ui/react-checkbox": "^1.0.4",
|
||||
|
|
@ -50,6 +51,7 @@
|
|||
"react-cookie": "^4.1.1",
|
||||
"react-dom": "^18.2.21",
|
||||
"react-error-boundary": "^4.0.11",
|
||||
"react-hook-form": "^7.51.4",
|
||||
"react-icons": "^5.0.1",
|
||||
"react-laag": "^2.0.5",
|
||||
"react-markdown": "^8.0.7",
|
||||
|
|
@ -68,6 +70,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": {
|
||||
|
|
@ -101,7 +104,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,6 +19,7 @@ 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";
|
||||
|
|
@ -47,13 +48,13 @@ export default function App() {
|
|||
const setGlobalVariables = useGlobalVariablesStore(
|
||||
(state) => state.setGlobalVariables
|
||||
);
|
||||
const setUnavailableFields = useGlobalVariablesStore(
|
||||
(state) => state.setUnavaliableFields
|
||||
);
|
||||
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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -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";
|
||||
|
|
@ -22,7 +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 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" : ""
|
||||
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 ? (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -15,8 +15,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);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ 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 !== "") {
|
||||
|
|
|
|||
|
|
@ -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}>
|
||||
|
|
@ -107,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>
|
||||
|
|
|
|||
|
|
@ -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,13 +31,13 @@ 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("", true);
|
||||
|
|
@ -93,182 +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"
|
||||
avoidCollisions={false}
|
||||
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}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
|
|
@ -280,8 +151,10 @@ export default function InputComponent({
|
|||
)}
|
||||
>
|
||||
<button
|
||||
onClick={() => {
|
||||
onClick={(e) => {
|
||||
setShowOptions(!showOptions);
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
className={cn(
|
||||
selectedOption !== ""
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
@ -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"
|
||||
/>
|
||||
|
|
|
|||
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>
|
||||
);
|
||||
}
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
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.";
|
||||
|
|
@ -792,3 +801,7 @@ export const NATIVE_CATEGORIES = [
|
|||
];
|
||||
|
||||
export const SAVE_DEBOUNCE_TIME = 300;
|
||||
|
||||
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,3 +1,4 @@
|
|||
import { ColDef, ColGroupDef } from "ag-grid-community";
|
||||
import { AxiosRequestConfig, AxiosResponse } from "axios";
|
||||
import { Edge, Node, ReactFlowJsonObject } from "reactflow";
|
||||
import { BASE_URL_API } from "../../constants/constants";
|
||||
|
|
@ -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,
|
||||
|
|
@ -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) {
|
||||
|
|
@ -152,6 +156,7 @@ export async function updateFlowInDatabase(
|
|||
name: updatedFlow.name,
|
||||
data: updatedFlow.data,
|
||||
description: updatedFlow.description,
|
||||
folder_id: updatedFlow.folder_id === "" ? null : updatedFlow.folder_id,
|
||||
});
|
||||
|
||||
if (response?.status !== 200) {
|
||||
|
|
@ -992,3 +997,41 @@ export async function deleteFlowPool(
|
|||
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 };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,14 +54,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);
|
||||
|
|
@ -118,7 +118,7 @@ export default function GenericNode({
|
|||
|
||||
updateNodeInternals(data.id);
|
||||
},
|
||||
[data.id, data.node, setNode, setIsOutdated]
|
||||
[data.id, data.node, setNode, setIsOutdated],
|
||||
);
|
||||
|
||||
if (!data.node!.template) {
|
||||
|
|
@ -258,7 +258,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" />;
|
||||
|
|
@ -299,7 +299,7 @@ export default function GenericNode({
|
|||
};
|
||||
const getSpecificClassFromBuildStatus = (
|
||||
buildStatus: BuildStatus | undefined,
|
||||
validationStatus: validationStatusType | null
|
||||
validationStatus: validationStatusType | null,
|
||||
) => {
|
||||
let isInvalid = validationStatus && !validationStatus.valid;
|
||||
|
||||
|
|
@ -323,11 +323,11 @@ 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);
|
||||
|
|
@ -336,7 +336,7 @@ export default function GenericNode({
|
|||
baseBorderClass,
|
||||
nodeSizeClass,
|
||||
"generic-node-div",
|
||||
specificClassFromBuildStatus
|
||||
specificClassFromBuildStatus,
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -392,7 +392,7 @@ export default function GenericNode({
|
|||
selected,
|
||||
showNode,
|
||||
buildStatus,
|
||||
validationStatus
|
||||
validationStatus,
|
||||
)}
|
||||
>
|
||||
{data.node?.beta && showNode && (
|
||||
|
|
@ -537,7 +537,7 @@ export default function GenericNode({
|
|||
}
|
||||
title={getFieldTitle(
|
||||
data.node?.template!,
|
||||
templateField
|
||||
templateField,
|
||||
)}
|
||||
info={data.node?.template[templateField].info}
|
||||
name={templateField}
|
||||
|
|
@ -565,7 +565,7 @@ export default function GenericNode({
|
|||
proxy={data.node?.template[templateField].proxy}
|
||||
showNode={showNode}
|
||||
/>
|
||||
)
|
||||
),
|
||||
)}
|
||||
<ParameterComponent
|
||||
key={scapedJSONStringfy({
|
||||
|
|
@ -722,7 +722,7 @@ export default function GenericNode({
|
|||
!data.node?.description) &&
|
||||
nameEditable
|
||||
? "font-light italic"
|
||||
: ""
|
||||
: "",
|
||||
)}
|
||||
onDoubleClick={(e) => {
|
||||
setInputDescription(true);
|
||||
|
|
@ -784,13 +784,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}
|
||||
|
|
@ -817,7 +817,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",
|
||||
)}
|
||||
>
|
||||
{" "}
|
||||
|
|
|
|||
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>,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
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,21 +22,14 @@ 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)"
|
||||
|
|
@ -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")!}
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import ExtraSidebar from "./components/extraSidebarComponent";
|
|||
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
105
src/frontend/src/pages/MainPage/pages/mainPage/index.tsx
Normal file
105
src/frontend/src/pages/MainPage/pages/mainPage/index.tsx
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import { Outlet, useLocation, useNavigate } from "react-router-dom";
|
||||
import DropdownButton from "../../../../components/dropdownButtonComponent";
|
||||
import PageLayout from "../../../../components/pageLayout";
|
||||
import SidebarNav from "../../../../components/sidebarComponent";
|
||||
import {
|
||||
MY_COLLECTION_DESC,
|
||||
USER_PROJECTS_HEADER,
|
||||
} from "../../../../constants/constants";
|
||||
import useFlowsManagerStore from "../../../../stores/flowsManagerStore";
|
||||
import { useFolderStore } from "../../../../stores/foldersStore";
|
||||
import ModalsComponent from "../../components/modalsComponent";
|
||||
import useDeleteFolder from "../../hooks/use-delete-folder";
|
||||
import useDropdownOptions from "../../hooks/use-dropdown-options";
|
||||
|
||||
export default function HomePage(): JSX.Element {
|
||||
const uploadFlow = useFlowsManagerStore((state) => state.uploadFlow);
|
||||
const setCurrentFlowId = useFlowsManagerStore(
|
||||
(state) => state.setCurrentFlowId
|
||||
);
|
||||
|
||||
const location = useLocation();
|
||||
const pathname = location.pathname;
|
||||
const [openModal, setOpenModal] = useState(false);
|
||||
const [openFolderModal, setOpenFolderModal] = useState(false);
|
||||
const [openDeleteFolderModal, setOpenDeleteFolderModal] = useState(false);
|
||||
const is_component = pathname === "/components";
|
||||
const getFoldersApi = useFolderStore((state) => state.getFoldersApi);
|
||||
const setFolderToEdit = useFolderStore((state) => state.setFolderToEdit);
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
getFoldersApi();
|
||||
}, 300);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setCurrentFlowId("");
|
||||
}, [pathname]);
|
||||
|
||||
const dropdownOptions = useDropdownOptions({
|
||||
uploadFlow,
|
||||
navigate,
|
||||
is_component,
|
||||
});
|
||||
|
||||
const { handleDeleteFolder } = useDeleteFolder({ navigate, getFoldersApi });
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageLayout
|
||||
title={USER_PROJECTS_HEADER}
|
||||
description={MY_COLLECTION_DESC}
|
||||
button={
|
||||
<div className="flex gap-2">
|
||||
<DropdownButton
|
||||
firstButtonName="New Project"
|
||||
onFirstBtnClick={() => setOpenModal(true)}
|
||||
options={dropdownOptions}
|
||||
plusButton={true}
|
||||
dropdownOptions={false}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="flex h-full w-full space-y-8 md:flex-col lg:flex-row lg:space-x-8 lg:space-y-0">
|
||||
<aside className="flex h-fit w-fit flex-col space-y-6">
|
||||
<SidebarNav
|
||||
handleOpenNewFolderModal={() => {
|
||||
setFolderToEdit(null);
|
||||
setOpenFolderModal(true);
|
||||
}}
|
||||
items={[]}
|
||||
handleChangeFolder={(id: string) => {
|
||||
navigate(`all/folder/${id}`, { state: { folderId: id } });
|
||||
}}
|
||||
handleEditFolder={(item) => {
|
||||
setFolderToEdit(item);
|
||||
setOpenFolderModal(true);
|
||||
}}
|
||||
handleDeleteFolder={(item) => {
|
||||
setFolderToEdit(item);
|
||||
setOpenDeleteFolderModal(true);
|
||||
}}
|
||||
className="w-[20vw]"
|
||||
/>
|
||||
</aside>
|
||||
<div className="relative h-full w-full flex-1">
|
||||
<Outlet />
|
||||
</div>
|
||||
</div>
|
||||
</PageLayout>
|
||||
<ModalsComponent
|
||||
openModal={openModal}
|
||||
setOpenModal={setOpenModal}
|
||||
openFolderModal={openFolderModal}
|
||||
setOpenFolderModal={setOpenFolderModal}
|
||||
openDeleteFolderModal={openDeleteFolderModal}
|
||||
setOpenDeleteFolderModal={setOpenDeleteFolderModal}
|
||||
handleDeleteFolder={handleDeleteFolder}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
121
src/frontend/src/pages/MainPage/services/index.ts
Normal file
121
src/frontend/src/pages/MainPage/services/index.ts
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
import { BASE_URL_API } from "../../../constants/constants";
|
||||
import { api } from "../../../controllers/API/api";
|
||||
import { FlowType } from "../../../types/flow";
|
||||
import { AddFolderType, FolderType, StarterProjectsType } from "../entities";
|
||||
|
||||
export async function getFolders(): Promise<FolderType[]> {
|
||||
try {
|
||||
const response = await api.get(`${BASE_URL_API}folders/`);
|
||||
return response?.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function addFolder(data: AddFolderType): Promise<FolderType> {
|
||||
const body = {
|
||||
name: data.name,
|
||||
description: data.description,
|
||||
flows_list: data.flows ?? [],
|
||||
components_list: data.components ?? [],
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await api.post(`${BASE_URL_API}folders/`, body);
|
||||
return response?.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateFolder(
|
||||
body: FolderType,
|
||||
folderId: string,
|
||||
): Promise<FolderType> {
|
||||
try {
|
||||
const response = await api.patch(
|
||||
`${BASE_URL_API}folders/${folderId}`,
|
||||
body,
|
||||
);
|
||||
return response?.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteFolder(folderId: string) {
|
||||
try {
|
||||
const response = await api.delete(`${BASE_URL_API}folders/${folderId}`);
|
||||
return response?.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getFolderById(folderId: string): Promise<FolderType> {
|
||||
try {
|
||||
const response = await api.get(`${BASE_URL_API}folders/${folderId}`);
|
||||
return response?.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getStarterProjects(): Promise<StarterProjectsType> {
|
||||
try {
|
||||
const response = await api.get(`${BASE_URL_API}folders/starter-projects`);
|
||||
return response?.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function downloadFlowsFromFolders(folderId: string): Promise<{
|
||||
flows: FlowType[];
|
||||
folder_name: string;
|
||||
folder_description: string;
|
||||
}> {
|
||||
try {
|
||||
const response = await api.get(
|
||||
`${BASE_URL_API}folders/download/${folderId}`,
|
||||
);
|
||||
if (response?.status !== 200) {
|
||||
throw new Error(`HTTP error! status: ${response?.status}`);
|
||||
}
|
||||
console.log(response.data);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function uploadFlowsFromFolders(
|
||||
flows: FormData,
|
||||
): Promise<FlowType[]> {
|
||||
try {
|
||||
const response = await api.post(`${BASE_URL_API}folders/upload/`, flows);
|
||||
|
||||
if (response?.status !== 201) {
|
||||
throw new Error(`HTTP error! status: ${response?.status}`);
|
||||
}
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function moveFlowToFolder(
|
||||
flowId: string,
|
||||
folderId: string,
|
||||
): Promise<FlowType> {
|
||||
try {
|
||||
const response = await api.patch(
|
||||
`${BASE_URL_API}folders/move_to_folder/${flowId}/${folderId}`,
|
||||
);
|
||||
return response?.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
10
src/frontend/src/pages/MainPage/utils/get-name-by-type.ts
Normal file
10
src/frontend/src/pages/MainPage/utils/get-name-by-type.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
export const getNameByType = (type: string) => {
|
||||
switch (type) {
|
||||
case "all":
|
||||
return "Component or Flow";
|
||||
case "component":
|
||||
return "Component";
|
||||
default:
|
||||
return "Flow";
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
import { useFolderStore } from "../../../stores/foldersStore";
|
||||
import { downloadFlowsFromFolders } from "../services";
|
||||
|
||||
export function handleDownloadFolderFn(folderId: string) {
|
||||
downloadFlowsFromFolders(folderId).then((data) => {
|
||||
const folders = useFolderStore.getState().folders;
|
||||
|
||||
const folder = folders.find((f) => f.id === folderId);
|
||||
|
||||
data.folder_name = folder?.name || "folder";
|
||||
data.folder_description = folder?.description || "";
|
||||
|
||||
const jsonString = `data:text/json;chatset=utf-8,${encodeURIComponent(
|
||||
JSON.stringify(data),
|
||||
)}`;
|
||||
|
||||
const link = document.createElement("a");
|
||||
link.href = jsonString;
|
||||
link.download = `${data.folder_name}.json`;
|
||||
|
||||
link.click();
|
||||
});
|
||||
}
|
||||
25
src/frontend/src/pages/MainPage/utils/sort-flows.ts
Normal file
25
src/frontend/src/pages/MainPage/utils/sort-flows.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
export const sortFlows = (flows, type) => {
|
||||
const isComponent = type === "component";
|
||||
|
||||
const sortByDate = (a, b) => {
|
||||
const dateA = a?.updated_at || a?.date_created;
|
||||
const dateB = b?.updated_at || b?.date_created;
|
||||
|
||||
if (dateA && dateB) {
|
||||
return new Date(dateB).getTime() - new Date(dateA).getTime();
|
||||
} else if (dateA) {
|
||||
return 1;
|
||||
} else if (dateB) {
|
||||
return -1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
const filteredFlows =
|
||||
type === "all"
|
||||
? flows
|
||||
: flows?.filter((f) => (f?.is_component ?? false) === isComponent);
|
||||
|
||||
return filteredFlows?.sort(sortByDate) ?? [];
|
||||
};
|
||||
|
|
@ -8,7 +8,7 @@ import useFlowsManagerStore from "../../stores/flowsManagerStore";
|
|||
export default function SettingsPage(): JSX.Element {
|
||||
const pathname = location.pathname;
|
||||
const setCurrentFlowId = useFlowsManagerStore(
|
||||
(state) => state.setCurrentFlowId
|
||||
(state) => state.setCurrentFlowId,
|
||||
);
|
||||
useEffect(() => {
|
||||
setCurrentFlowId("");
|
||||
|
|
@ -25,36 +25,7 @@ export default function SettingsPage(): JSX.Element {
|
|||
/>
|
||||
),
|
||||
},
|
||||
/* {
|
||||
title: "Theme",
|
||||
href: "/settings/theme",
|
||||
icon: (
|
||||
<ForwardedIconComponent
|
||||
name="Palette"
|
||||
className="mx-[0.08rem] w-[1.1rem] stroke-[1.5]"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Bundles",
|
||||
href: "/settings/bundles",
|
||||
icon: (
|
||||
<ForwardedIconComponent
|
||||
name="Boxes"
|
||||
className="mx-[0.08rem] w-[1.1rem] stroke-[1.5]"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Integrations",
|
||||
href: "/settings/integrations",
|
||||
icon: (
|
||||
<ForwardedIconComponent
|
||||
name="Blocks"
|
||||
className="mx-[0.08rem] w-[1.1rem] stroke-[1.5]"
|
||||
/>
|
||||
),
|
||||
}, */
|
||||
|
||||
{
|
||||
title: "Global Variables",
|
||||
href: "/settings/global-variables",
|
||||
|
|
|
|||
|
|
@ -32,11 +32,11 @@ import { gradients } from "../../../../utils/styleUtils";
|
|||
|
||||
export default function GeneralPage() {
|
||||
const setCurrentFlowId = useFlowsManagerStore(
|
||||
(state) => state.setCurrentFlowId
|
||||
(state) => state.setCurrentFlowId,
|
||||
);
|
||||
|
||||
const [inputState, setInputState] = useState<patchUserInputStateType>(
|
||||
CONTROL_PATCH_USER_STATE
|
||||
CONTROL_PATCH_USER_STATE,
|
||||
);
|
||||
|
||||
const { autoLogin } = useContext(AuthContext);
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ export default function GlobalVariablesPage() {
|
|||
name: string;
|
||||
default_fields: string | undefined;
|
||||
}[]
|
||||
>();
|
||||
>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const rows: Array<{
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@ import ApiKeysPage from "./pages/ApiKeysPage";
|
|||
import DeleteAccountPage from "./pages/DeleteAccountPage";
|
||||
import FlowPage from "./pages/FlowPage";
|
||||
import LoginPage from "./pages/LoginPage";
|
||||
import HomePage from "./pages/MainPage";
|
||||
import ComponentsComponent from "./pages/MainPage/components/components";
|
||||
import MyCollectionComponent from "./pages/MainPage/components/myCollectionComponent";
|
||||
import HomePage from "./pages/MainPage/pages/mainPage";
|
||||
import PlaygroundPage from "./pages/Playground";
|
||||
import SettingsPage from "./pages/SettingsPage";
|
||||
import GeneralPage from "./pages/SettingsPage/pages/GeneralPage";
|
||||
|
|
@ -32,14 +32,18 @@ const Router = () => {
|
|||
</ProtectedRoute>
|
||||
}
|
||||
>
|
||||
<Route index element={<Navigate replace to={"flows"} />} />
|
||||
<Route index element={<Navigate replace to={"all"} />} />
|
||||
<Route
|
||||
path="flows"
|
||||
element={<ComponentsComponent key="flows" is_component={false} />}
|
||||
path="flows/*"
|
||||
element={<MyCollectionComponent key="flows" type="flow" />}
|
||||
/>
|
||||
<Route
|
||||
path="components"
|
||||
element={<ComponentsComponent key="components" />}
|
||||
path="components/*"
|
||||
element={<MyCollectionComponent key="components" type="component" />}
|
||||
/>
|
||||
<Route
|
||||
path="all/*"
|
||||
element={<MyCollectionComponent key="all" type="all" />}
|
||||
/>
|
||||
</Route>
|
||||
<Route
|
||||
|
|
@ -89,6 +93,14 @@ const Router = () => {
|
|||
}
|
||||
</Route>
|
||||
<Route path="/flow/:id/">
|
||||
<Route
|
||||
path="*"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<FlowPage />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path=""
|
||||
element={
|
||||
|
|
|
|||
|
|
@ -2,17 +2,16 @@ import { AxiosError } from "axios";
|
|||
import { cloneDeep, debounce } from "lodash";
|
||||
import { Edge, Node, Viewport, XYPosition } from "reactflow";
|
||||
import { create } from "zustand";
|
||||
import {
|
||||
SAVE_DEBOUNCE_TIME,
|
||||
STARTER_FOLDER_NAME,
|
||||
} from "../constants/constants";
|
||||
import { SAVE_DEBOUNCE_TIME } from "../constants/constants";
|
||||
import {
|
||||
deleteFlowFromDatabase,
|
||||
multipleDeleteFlowsComponents,
|
||||
readFlowsFromDatabase,
|
||||
saveFlowToDatabase,
|
||||
updateFlowInDatabase,
|
||||
uploadFlowsToDatabase,
|
||||
} from "../controllers/API";
|
||||
import { getStarterProjects } from "../pages/MainPage/services";
|
||||
import { FlowType, NodeDataType } from "../types/flow";
|
||||
import {
|
||||
FlowsManagerStoreType,
|
||||
|
|
@ -29,6 +28,7 @@ import {
|
|||
import useAlertStore from "./alertStore";
|
||||
import { useDarkStore } from "./darkStore";
|
||||
import useFlowStore from "./flowStore";
|
||||
import { useFolderStore } from "./foldersStore";
|
||||
import { useTypesStore } from "./typesStore";
|
||||
|
||||
let saveTimeoutId: NodeJS.Timeout | null = null;
|
||||
|
|
@ -63,6 +63,10 @@ const useFlowsManagerStore = create<FlowsManagerStoreType>((set, get) => ({
|
|||
}));
|
||||
},
|
||||
flows: [],
|
||||
allFlows: [],
|
||||
setAllFlows: (allFlows: FlowType[]) => {
|
||||
set({ allFlows });
|
||||
},
|
||||
setFlows: (flows: FlowType[]) => {
|
||||
set({
|
||||
flows,
|
||||
|
|
@ -76,38 +80,39 @@ const useFlowsManagerStore = create<FlowsManagerStoreType>((set, get) => ({
|
|||
refreshFlows: () => {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
set({ isLoading: true });
|
||||
readFlowsFromDatabase()
|
||||
.then((dbData) => {
|
||||
if (dbData) {
|
||||
const { data, flows } = processFlows(dbData, false);
|
||||
get().setExamples(
|
||||
flows.filter(
|
||||
(f) => f.folder === STARTER_FOLDER_NAME && !f.user_id
|
||||
)
|
||||
);
|
||||
get().setFlows(
|
||||
flows.filter(
|
||||
(f) => !(f.folder === STARTER_FOLDER_NAME && !f.user_id)
|
||||
)
|
||||
);
|
||||
useTypesStore.setState((state) => ({
|
||||
data: { ...state.data, ["saved_components"]: data },
|
||||
ComponentFields: extractFieldsFromComponenents({
|
||||
...state.data,
|
||||
["saved_components"]: data,
|
||||
}),
|
||||
}));
|
||||
|
||||
getStarterProjects().then((starterProjects) => {
|
||||
get().setExamples(starterProjects?.flows!);
|
||||
|
||||
readFlowsFromDatabase()
|
||||
.then((dbData) => {
|
||||
if (dbData) {
|
||||
const { data, flows } = processFlows(dbData, false);
|
||||
const starterProjectsIds = starterProjects.flows!.map(
|
||||
(flow) => flow.id
|
||||
);
|
||||
get().setFlows(
|
||||
flows.filter((f) => !starterProjectsIds.includes(f.id))
|
||||
);
|
||||
useTypesStore.setState((state) => ({
|
||||
data: { ...state.data, ["saved_components"]: data },
|
||||
ComponentFields: extractFieldsFromComponenents({
|
||||
...state.data,
|
||||
["saved_components"]: data,
|
||||
}),
|
||||
}));
|
||||
set({ isLoading: false });
|
||||
resolve();
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
set({ isLoading: false });
|
||||
resolve();
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
set({ isLoading: false });
|
||||
useAlertStore.getState().setErrorData({
|
||||
title: "Could not load flows from database",
|
||||
useAlertStore.getState().setErrorData({
|
||||
title: "Could not load flows from database",
|
||||
});
|
||||
reject(e);
|
||||
});
|
||||
reject(e);
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
autoSaveCurrentFlow: (nodes: Node[], edges: Edge[], viewport: Viewport) => {
|
||||
|
|
@ -190,7 +195,8 @@ const useFlowsManagerStore = create<FlowsManagerStoreType>((set, get) => ({
|
|||
newProject: Boolean,
|
||||
flow?: FlowType,
|
||||
override?: boolean,
|
||||
position?: XYPosition
|
||||
position?: XYPosition,
|
||||
fromDragAndDrop?: boolean
|
||||
): Promise<string | undefined> => {
|
||||
if (newProject) {
|
||||
let flowData = flow
|
||||
|
|
@ -202,6 +208,7 @@ const useFlowsManagerStore = create<FlowsManagerStoreType>((set, get) => ({
|
|||
if (override) {
|
||||
get().deleteComponent(flow!.name);
|
||||
const newFlow = createNewFlow(flowData!, flow!);
|
||||
newFlow.folder_id = useFolderStore.getState().folderUrl;
|
||||
const { id } = await saveFlowToDatabase(newFlow);
|
||||
newFlow.id = id;
|
||||
//setTimeout to prevent update state with wrong state
|
||||
|
|
@ -226,6 +233,8 @@ const useFlowsManagerStore = create<FlowsManagerStoreType>((set, get) => ({
|
|||
const newName = addVersionToDuplicates(newFlow, get().flows);
|
||||
|
||||
newFlow.name = newName;
|
||||
newFlow.folder_id = useFolderStore.getState().folderUrl;
|
||||
|
||||
try {
|
||||
const { id } = await saveFlowToDatabase(newFlow);
|
||||
// Change the id to the new id.
|
||||
|
|
@ -258,25 +267,47 @@ const useFlowsManagerStore = create<FlowsManagerStoreType>((set, get) => ({
|
|||
);
|
||||
}
|
||||
},
|
||||
removeFlow: async (id: string) => {
|
||||
return new Promise<void>((resolve) => {
|
||||
const index = get().flows.findIndex((flow) => flow.id === id);
|
||||
if (index >= 0) {
|
||||
deleteFlowFromDatabase(id).then(() => {
|
||||
const { data, flows } = processFlows(
|
||||
get().flows.filter((flow) => flow.id !== id)
|
||||
);
|
||||
get().setFlows(flows);
|
||||
set({ isLoading: false });
|
||||
useTypesStore.setState((state) => ({
|
||||
data: { ...state.data, ["saved_components"]: data },
|
||||
ComponentFields: extractFieldsFromComponenents({
|
||||
...state.data,
|
||||
["saved_components"]: data,
|
||||
}),
|
||||
}));
|
||||
resolve();
|
||||
});
|
||||
removeFlow: async (id: string | string[]) => {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
if (Array.isArray(id)) {
|
||||
multipleDeleteFlowsComponents(id)
|
||||
.then(() => {
|
||||
const { data, flows } = processFlows(
|
||||
get().flows.filter((flow) => !id.includes(flow.id))
|
||||
);
|
||||
get().setFlows(flows);
|
||||
set({ isLoading: false });
|
||||
useTypesStore.setState((state) => ({
|
||||
data: { ...state.data, ["saved_components"]: data },
|
||||
ComponentFields: extractFieldsFromComponenents({
|
||||
...state.data,
|
||||
["saved_components"]: data,
|
||||
}),
|
||||
}));
|
||||
resolve();
|
||||
})
|
||||
.catch((e) => reject(e));
|
||||
} else {
|
||||
const index = get().flows.findIndex((flow) => flow.id === id);
|
||||
if (index >= 0) {
|
||||
deleteFlowFromDatabase(id)
|
||||
.then(() => {
|
||||
const { data, flows } = processFlows(
|
||||
get().flows.filter((flow) => flow.id !== id)
|
||||
);
|
||||
get().setFlows(flows);
|
||||
set({ isLoading: false });
|
||||
useTypesStore.setState((state) => ({
|
||||
data: { ...state.data, ["saved_components"]: data },
|
||||
ComponentFields: extractFieldsFromComponenents({
|
||||
...state.data,
|
||||
["saved_components"]: data,
|
||||
}),
|
||||
}));
|
||||
resolve();
|
||||
})
|
||||
.catch((e) => reject(e));
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
|
@ -299,12 +330,12 @@ const useFlowsManagerStore = create<FlowsManagerStoreType>((set, get) => ({
|
|||
uploadFlow: async ({
|
||||
newProject,
|
||||
file,
|
||||
isComponent = false,
|
||||
isComponent,
|
||||
position = { x: 10, y: 10 },
|
||||
}: {
|
||||
newProject: boolean;
|
||||
file?: File;
|
||||
isComponent?: boolean;
|
||||
isComponent: boolean | null;
|
||||
position?: XYPosition;
|
||||
}): Promise<string | never> => {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
|
|
@ -314,6 +345,7 @@ const useFlowsManagerStore = create<FlowsManagerStoreType>((set, get) => ({
|
|||
let fileData = JSON.parse(text);
|
||||
if (
|
||||
newProject &&
|
||||
isComponent !== null &&
|
||||
((!fileData.is_component && isComponent === true) ||
|
||||
(fileData.is_component !== undefined &&
|
||||
fileData.is_component !== isComponent))
|
||||
|
|
@ -326,7 +358,13 @@ const useFlowsManagerStore = create<FlowsManagerStoreType>((set, get) => ({
|
|||
});
|
||||
resolve("");
|
||||
} else {
|
||||
id = await get().addFlow(newProject, fileData, undefined, position);
|
||||
id = await get().addFlow(
|
||||
newProject,
|
||||
fileData,
|
||||
undefined,
|
||||
position,
|
||||
true
|
||||
);
|
||||
resolve(id);
|
||||
}
|
||||
}
|
||||
|
|
@ -435,6 +473,14 @@ const useFlowsManagerStore = create<FlowsManagerStoreType>((set, get) => ({
|
|||
newState.setEdges(futureState.edges);
|
||||
}
|
||||
},
|
||||
searchFlowsComponents: "",
|
||||
setSearchFlowsComponents: (searchFlowsComponents: string) => {
|
||||
set({ searchFlowsComponents });
|
||||
},
|
||||
selectedFlowsComponentsCards: [],
|
||||
setSelectedFlowsComponentsCards: (selectedFlowsComponentsCards: string[]) => {
|
||||
set({ selectedFlowsComponentsCards });
|
||||
},
|
||||
}));
|
||||
|
||||
export default useFlowsManagerStore;
|
||||
|
|
|
|||
104
src/frontend/src/stores/foldersStore.tsx
Normal file
104
src/frontend/src/stores/foldersStore.tsx
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
import { create } from "zustand";
|
||||
import { DEFAULT_FOLDER } from "../constants/constants";
|
||||
import {
|
||||
getFolderById,
|
||||
getFolders,
|
||||
uploadFlowsFromFolders,
|
||||
} from "../pages/MainPage/services";
|
||||
import { FoldersStoreType } from "../types/zustand/folders";
|
||||
import useFlowsManagerStore from "./flowsManagerStore";
|
||||
|
||||
export const useFolderStore = create<FoldersStoreType>((set, get) => ({
|
||||
folders: [],
|
||||
getFoldersApi: (refetch = false) => {
|
||||
if (get()?.folders.length === 0 || refetch === true) {
|
||||
get().setLoading(true);
|
||||
getFolders().then(
|
||||
(res) => {
|
||||
set({ folders: res });
|
||||
const myCollectionId = res?.find(
|
||||
(f) => f.name === DEFAULT_FOLDER
|
||||
)?.id;
|
||||
set({ myCollectionId });
|
||||
get().setLoading(false);
|
||||
useFlowsManagerStore.getState().refreshFlows();
|
||||
},
|
||||
() => {
|
||||
set({ folders: [] });
|
||||
get().setLoading(false);
|
||||
}
|
||||
);
|
||||
}
|
||||
},
|
||||
setFolders: (folders) => set(() => ({ folders: folders })),
|
||||
loading: false,
|
||||
setLoading: (loading) => set(() => ({ loading: loading })),
|
||||
getFolderById: (id) => {
|
||||
get().setLoadingById(true);
|
||||
if (id) {
|
||||
getFolderById(id).then(
|
||||
(res) => {
|
||||
const setAllFlows = useFlowsManagerStore.getState().setAllFlows;
|
||||
setAllFlows(res.flows);
|
||||
set({ selectedFolder: res });
|
||||
get().setLoadingById(false);
|
||||
},
|
||||
() => {
|
||||
get().setLoadingById(false);
|
||||
}
|
||||
);
|
||||
}
|
||||
},
|
||||
selectedFolder: null,
|
||||
loadingById: false,
|
||||
setLoadingById: (loading) => set(() => ({ loadingById: loading })),
|
||||
getMyCollectionFolder: () => {
|
||||
const folders = get().folders;
|
||||
const myCollectionId = folders?.find((f) => f.name === DEFAULT_FOLDER)?.id;
|
||||
if (myCollectionId) {
|
||||
getFolderById(myCollectionId).then((res) => {
|
||||
set({ myCollectionFlows: res });
|
||||
});
|
||||
}
|
||||
},
|
||||
setMyCollectionFlow: (folder) => set(() => ({ myCollectionFlows: folder })),
|
||||
myCollectionFlows: null,
|
||||
setMyCollectionId: () => {
|
||||
const folders = get().folders;
|
||||
const myCollectionId = folders?.find((f) => f.name === DEFAULT_FOLDER)?.id;
|
||||
if (myCollectionId) {
|
||||
set({ myCollectionId });
|
||||
}
|
||||
},
|
||||
myCollectionId: "",
|
||||
folderToEdit: null,
|
||||
setFolderToEdit: (folder) => set(() => ({ folderToEdit: folder })),
|
||||
folderUrl: "",
|
||||
setFolderUrl: (url) => set(() => ({ folderUrl: url })),
|
||||
folderDragging: false,
|
||||
setFolderDragging: (folder) => set(() => ({ folderDragging: folder })),
|
||||
folderIdDragging: "",
|
||||
setFolderIdDragging: (id) => set(() => ({ folderIdDragging: id })),
|
||||
uploadFolder: () => {
|
||||
return new Promise<void>(() => {
|
||||
const input = document.createElement("input");
|
||||
input.type = "file";
|
||||
input.onchange = (event: Event) => {
|
||||
if (
|
||||
(event.target as HTMLInputElement).files![0].type ===
|
||||
"application/json"
|
||||
) {
|
||||
const file = (event.target as HTMLInputElement).files![0];
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
uploadFlowsFromFolders(formData).then(() => {
|
||||
get().getFoldersApi(true);
|
||||
useFlowsManagerStore.getState().refreshFlows();
|
||||
});
|
||||
useFlowsManagerStore.getState().setAllFlows;
|
||||
}
|
||||
};
|
||||
input.click();
|
||||
});
|
||||
},
|
||||
}));
|
||||
14
src/frontend/src/stores/utilityStore.ts
Normal file
14
src/frontend/src/stores/utilityStore.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import { create } from "zustand";
|
||||
|
||||
export const useUtilityStore = create<any>((set, get) => ({
|
||||
selectedItems: [],
|
||||
setSelectedItems: (itemId) => {
|
||||
if (get().selectedItems.includes(itemId)) {
|
||||
set({
|
||||
selectedItems: get().selectedItems.filter((item) => item !== itemId),
|
||||
});
|
||||
} else {
|
||||
set({ selectedItems: get().selectedItems.concat(itemId) });
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
|
@ -28,6 +28,7 @@
|
|||
--ring: 215 20.2% 65.1%; /* hsl(215 20% 65%) */
|
||||
--round-btn-shadow: #00000063;
|
||||
--ice: #31a3cc;
|
||||
--selected: #2196f3;
|
||||
|
||||
--error-background: #fef2f2;
|
||||
--error-foreground: #991b1b;
|
||||
|
|
@ -50,9 +51,8 @@
|
|||
|
||||
--component-icon: #d8598a;
|
||||
--flow-icon: #2f67d0;
|
||||
--hover: #F2F4F5;
|
||||
--hover: #f2f4f5;
|
||||
--disabled-run: #6366f1;
|
||||
|
||||
|
||||
/* Colors that are shared in dark and light mode */
|
||||
--blur-shared: #151923de;
|
||||
|
|
@ -71,9 +71,10 @@
|
|||
.dark {
|
||||
--background: 224 35% 7.5%; /* hsl(224 40% 10%) */
|
||||
--foreground: 213 31% 80%; /* hsl(213 31% 91%) */
|
||||
--ice: #60A5FA;
|
||||
--hover: #1A202E;
|
||||
--ice: #60a5fa;
|
||||
--hover: #1a202e;
|
||||
--disabled-run: #6366f1;
|
||||
--selected: #2196f3;
|
||||
|
||||
--muted: 223 27% 11%; /* hsl(223 27% 11%) */
|
||||
--muted-foreground: 215.4 16.3% 56.9%; /* hsl(215 16% 56%) */
|
||||
|
|
|
|||
|
|
@ -30,6 +30,8 @@ export type InputComponentType = {
|
|||
setSelectedOption?: (value: string) => void;
|
||||
selectedOptions?: string[];
|
||||
setSelectedOptions?: (value: string[]) => void;
|
||||
objectOptions?: Array<{ name: string; id: string }>;
|
||||
isObjectOption?: boolean;
|
||||
};
|
||||
export type ToggleComponentType = {
|
||||
enabled: boolean;
|
||||
|
|
@ -220,6 +222,7 @@ export type AccordionComponentType = {
|
|||
keyValue?: string;
|
||||
openDisc?: boolean;
|
||||
sideBar?: boolean;
|
||||
options?: { title: string; icon: string }[];
|
||||
};
|
||||
export type Side = "top" | "right" | "bottom" | "left";
|
||||
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ export type FlowType = {
|
|||
user_id?: string;
|
||||
icon?: string;
|
||||
icon_bg_color?: string;
|
||||
folder_id?: string;
|
||||
};
|
||||
|
||||
export type NodeType = {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ import { FlowType } from "../../flow";
|
|||
export type FlowsManagerStoreType = {
|
||||
getFlowById: (id: string) => FlowType | undefined;
|
||||
flows: Array<FlowType>;
|
||||
allFlows: Array<FlowType>;
|
||||
setAllFlows: (flows: FlowType[]) => void;
|
||||
setFlows: (flows: FlowType[]) => void;
|
||||
currentFlow: FlowType | undefined;
|
||||
currentFlowId: string;
|
||||
|
|
@ -15,12 +17,12 @@ export type FlowsManagerStoreType = {
|
|||
saveFlow: (flow: FlowType, silent?: boolean) => Promise<void> | undefined;
|
||||
saveFlowDebounce: (
|
||||
flow: FlowType,
|
||||
silent?: boolean
|
||||
silent?: boolean,
|
||||
) => Promise<void> | undefined;
|
||||
autoSaveCurrentFlow: (
|
||||
nodes: Node[],
|
||||
edges: Edge[],
|
||||
viewport: Viewport
|
||||
viewport: Viewport,
|
||||
) => void;
|
||||
uploadFlows: () => Promise<void>;
|
||||
uploadFlow: ({
|
||||
|
|
@ -31,20 +33,21 @@ export type FlowsManagerStoreType = {
|
|||
}: {
|
||||
newProject: boolean;
|
||||
file?: File;
|
||||
isComponent?: boolean;
|
||||
isComponent: boolean | null;
|
||||
position?: XYPosition;
|
||||
}) => Promise<string | never>;
|
||||
addFlow: (
|
||||
newProject: boolean,
|
||||
flow?: FlowType,
|
||||
override?: boolean,
|
||||
position?: XYPosition
|
||||
position?: XYPosition,
|
||||
fromDragAndDrop?: boolean,
|
||||
) => Promise<string | undefined>;
|
||||
deleteComponent: (key: string) => Promise<void>;
|
||||
removeFlow: (id: string) => Promise<void>;
|
||||
removeFlow: (id: string | string[]) => Promise<void>;
|
||||
saveComponent: (
|
||||
component: any,
|
||||
override: boolean
|
||||
override: boolean,
|
||||
) => Promise<string | undefined>;
|
||||
undo: () => void;
|
||||
redo: () => void;
|
||||
|
|
@ -52,6 +55,10 @@ export type FlowsManagerStoreType = {
|
|||
examples: Array<FlowType>;
|
||||
setExamples: (examples: FlowType[]) => void;
|
||||
setCurrentFlow: (flow: FlowType) => void;
|
||||
setSearchFlowsComponents: (search: string) => void;
|
||||
searchFlowsComponents: string;
|
||||
selectedFlowsComponentsCards: string[];
|
||||
setSelectedFlowsComponentsCards: (selected: string[]) => void;
|
||||
};
|
||||
|
||||
export type UseUndoRedoOptions = {
|
||||
|
|
|
|||
26
src/frontend/src/types/zustand/folders/index.ts
Normal file
26
src/frontend/src/types/zustand/folders/index.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import { FolderType } from "../../../pages/MainPage/entities";
|
||||
|
||||
export type FoldersStoreType = {
|
||||
folders: FolderType[];
|
||||
getFoldersApi: (refetch?: boolean) => void;
|
||||
setFolders: (folders: FolderType[]) => void;
|
||||
loading: boolean;
|
||||
setLoading: (loading: boolean) => void;
|
||||
selectedFolder: FolderType | null;
|
||||
getFolderById: (id: string) => void;
|
||||
loadingById: boolean;
|
||||
setLoadingById: (loading: boolean) => void;
|
||||
getMyCollectionFolder: () => void;
|
||||
myCollectionFlows: FolderType | null;
|
||||
myCollectionId: string | null;
|
||||
setMyCollectionId: () => void;
|
||||
folderToEdit: FolderType | null;
|
||||
setFolderToEdit: (folder: FolderType | null) => void;
|
||||
folderUrl: string;
|
||||
setFolderUrl: (folderUrl: string) => void;
|
||||
folderDragging: boolean;
|
||||
setFolderDragging: (set: boolean) => void;
|
||||
uploadFolder: (folderId: string) => void;
|
||||
folderIdDragging: string;
|
||||
setFolderIdDragging: (id: string) => void;
|
||||
};
|
||||
|
|
@ -21,7 +21,7 @@ export type GlobalVariablesStore = {
|
|||
id: string,
|
||||
type?: string,
|
||||
default_fields?: string[],
|
||||
value?: string
|
||||
value?: string,
|
||||
) => void;
|
||||
removeGlobalVariable: (name: string) => Promise<void>;
|
||||
getVariableId: (name: string) => string | undefined;
|
||||
|
|
|
|||
|
|
@ -1299,6 +1299,7 @@ export const createNewFlow = (
|
|||
data: flowData,
|
||||
id: "",
|
||||
is_component: flow?.is_component ?? false,
|
||||
folder_id: "",
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -53,7 +53,9 @@ import {
|
|||
FileType2,
|
||||
FileUp,
|
||||
FlaskConical,
|
||||
FolderIcon,
|
||||
FolderPlus,
|
||||
FolderPlusIcon,
|
||||
FormInput,
|
||||
Forward,
|
||||
Gift,
|
||||
|
|
@ -105,6 +107,7 @@ import {
|
|||
SaveAll,
|
||||
Scissors,
|
||||
ScreenShare,
|
||||
ScrollText,
|
||||
Search,
|
||||
Settings2,
|
||||
Share,
|
||||
|
|
@ -141,9 +144,8 @@ import {
|
|||
XCircle,
|
||||
Zap,
|
||||
} from "lucide-react";
|
||||
import { FaApple, FaGithub } from "react-icons/fa";
|
||||
import { FaApple, FaDiscord, FaGithub } from "react-icons/fa";
|
||||
import { AWSIcon } from "../icons/AWS";
|
||||
import { FaDiscord } from "react-icons/fa";
|
||||
import { AirbyteIcon } from "../icons/Airbyte";
|
||||
import { AnthropicIcon } from "../icons/Anthropic";
|
||||
import { AstraDBIcon } from "../icons/AstraDB";
|
||||
|
|
@ -385,6 +387,7 @@ export const nodeIconsLucide: iconsType = {
|
|||
Keyboard: Keyboard,
|
||||
embeddings: Binary,
|
||||
saved_components: GradientSave,
|
||||
ScrollText,
|
||||
documentloaders: Paperclip,
|
||||
vectorstores: Layers,
|
||||
vectorsearch: TextSearch,
|
||||
|
|
@ -518,5 +521,7 @@ export const nodeIconsLucide: iconsType = {
|
|||
Command,
|
||||
ArrowBigUp,
|
||||
Dot,
|
||||
FolderPlusIcon,
|
||||
FolderIcon,
|
||||
Discord: FaDiscord,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import { ColDef, ColGroupDef } from "ag-grid-community";
|
||||
import clsx, { ClassValue } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import TableAutoCellRender from "../components/tableAutoCellRender";
|
||||
import { priorityFields } from "../constants/constants";
|
||||
import { ADJECTIVES, DESCRIPTIONS, NOUNS } from "../flow_constants";
|
||||
import {
|
||||
|
|
@ -698,3 +700,55 @@ export function convertTestName(name: string): string {
|
|||
export function sortByName(stringList: string[]): string[] {
|
||||
return stringList.sort((a, b) => a.localeCompare(b));
|
||||
}
|
||||
|
||||
export function isTimeStampString(str: string): boolean {
|
||||
const timestampRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3}Z)?$/;
|
||||
return timestampRegex.test(str);
|
||||
}
|
||||
|
||||
export function extractColumnsFromRows(
|
||||
rows: object[],
|
||||
mode: "intersection" | "union",
|
||||
): (ColDef<any> | ColGroupDef<any>)[] {
|
||||
const columnsKeys: { [key: string]: ColDef<any> | ColGroupDef<any> } = {};
|
||||
if (rows.length === 0) {
|
||||
return [];
|
||||
}
|
||||
function intersection() {
|
||||
for (const key in rows[0]) {
|
||||
columnsKeys[key] = {
|
||||
headerName: key,
|
||||
field: key,
|
||||
cellRenderer: TableAutoCellRender,
|
||||
filter: true,
|
||||
autoHeight: true,
|
||||
};
|
||||
}
|
||||
for (const row of rows) {
|
||||
for (const key in columnsKeys) {
|
||||
if (!row[key]) {
|
||||
delete columnsKeys[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
function union() {
|
||||
for (const row of rows) {
|
||||
for (const key in row) {
|
||||
columnsKeys[key] = {
|
||||
headerName: key,
|
||||
field: key,
|
||||
filter: true,
|
||||
cellRenderer: TableAutoCellRender,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
if (mode === "intersection") {
|
||||
intersection();
|
||||
} else {
|
||||
union();
|
||||
}
|
||||
|
||||
return Object.values(columnsKeys);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,11 @@ import plugin from "tailwindcss/plugin";
|
|||
|
||||
// ! Check if removing the other module.exports made sense
|
||||
module.exports = {
|
||||
variants: {
|
||||
extend: {
|
||||
display: ["group-hover"],
|
||||
},
|
||||
},
|
||||
darkMode: ["class"],
|
||||
content: [
|
||||
"app/**/*.{ts,tsx}",
|
||||
|
|
@ -36,6 +41,7 @@ module.exports = {
|
|||
},
|
||||
animation: {
|
||||
wiggle: "wiggle 150ms ease-in-out 1",
|
||||
"slow-wiggle": "wiggle 500ms ease-in-out 1",
|
||||
},
|
||||
colors: {
|
||||
"component-icon": "var(--component-icon)",
|
||||
|
|
@ -88,6 +94,7 @@ module.exports = {
|
|||
"chat-bot-icon": "var(--chat-bot-icon)",
|
||||
"chat-user-icon": "var(--chat-user-icon)",
|
||||
ice: "var(--ice)",
|
||||
selected: "var(--selected)",
|
||||
hover: "var(--hover)",
|
||||
white: "var(--white)",
|
||||
border: "hsl(var(--border))",
|
||||
|
|
|
|||
124
src/frontend/tests/end-to-end/actionsMainPage.spec.ts
Normal file
124
src/frontend/tests/end-to-end/actionsMainPage.spec.ts
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
import { test } from "@playwright/test";
|
||||
|
||||
test("select and delete all", 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.getByTestId("icon-ChevronLeft").first().click();
|
||||
|
||||
await page.getByText("Select All").click();
|
||||
await page.getByText("Unselect All").isVisible();
|
||||
await page.getByTestId("icon-Trash2").click();
|
||||
await page.getByText("Delete").last().click();
|
||||
|
||||
await page.waitForTimeout(1000);
|
||||
await page.getByText("Selected items deleted successfully").isVisible();
|
||||
});
|
||||
|
||||
test("search flows", 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.getByTestId("icon-ChevronLeft").first().click();
|
||||
|
||||
await page.getByText("Select All").isVisible();
|
||||
await page.getByText("New Project", { exact: true }).click();
|
||||
await page.getByRole("heading", { name: "Memory Chatbot" }).click();
|
||||
await page.getByTestId("icon-ChevronLeft").first().click();
|
||||
await page.getByText("New Project", { exact: true }).click();
|
||||
await page.getByRole("heading", { name: "Document QA" }).click();
|
||||
await page.getByTestId("icon-ChevronLeft").first().click();
|
||||
await page.getByPlaceholder("Search flows").fill("Memory Chatbot");
|
||||
await page.getByText("Memory Chatbot", { exact: true }).isVisible();
|
||||
await page.getByText("Document QA", { exact: true }).isHidden();
|
||||
await page.getByText("Basic Prompting", { exact: true }).isHidden();
|
||||
});
|
||||
|
||||
test("search components", 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").first().click();
|
||||
await page.getByTestId("icon-SaveAll").first().click();
|
||||
await page.keyboard.press("Escape");
|
||||
await page
|
||||
.getByText("Prompt", {
|
||||
exact: true,
|
||||
})
|
||||
.first()
|
||||
.click();
|
||||
await page.getByTestId("icon-SaveAll").first().click();
|
||||
await page.keyboard.press("Escape");
|
||||
|
||||
await page
|
||||
.getByText("OpenAI", {
|
||||
exact: true,
|
||||
})
|
||||
.first()
|
||||
.click();
|
||||
await page.getByTestId("icon-SaveAll").first().click();
|
||||
await page.keyboard.press("Escape");
|
||||
await page.getByTestId("icon-ChevronLeft").first().click();
|
||||
|
||||
await page
|
||||
.getByText("Components", {
|
||||
exact: true,
|
||||
})
|
||||
.click();
|
||||
|
||||
await page.getByPlaceholder("Search components").fill("Chat Input");
|
||||
await page.getByText("Chat Input", { exact: true }).isVisible();
|
||||
await page.getByText("Prompt", { exact: true }).isHidden();
|
||||
await page.getByText("OpenAI", { exact: true }).isHidden();
|
||||
});
|
||||
|
|
@ -3,24 +3,24 @@ import { test } from "@playwright/test";
|
|||
test.describe("Auto_login tests", () => {
|
||||
test("auto_login sign in", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await page.locator('//*[@id="new-project-btn"]').click();
|
||||
await page.getByText("New Project", { exact: true }).click();
|
||||
});
|
||||
|
||||
test("auto_login block_admin", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await page.locator('//*[@id="new-project-btn"]').click();
|
||||
await page.getByText("New Project", { exact: true }).click();
|
||||
await page.waitForTimeout(5000);
|
||||
|
||||
await page.goto("/login");
|
||||
await page.locator('//*[@id="new-project-btn"]').click();
|
||||
await page.getByText("New Project", { exact: true }).click();
|
||||
await page.waitForTimeout(5000);
|
||||
|
||||
await page.goto("/admin");
|
||||
await page.locator('//*[@id="new-project-btn"]').click();
|
||||
await page.getByText("New Project", { exact: true }).click();
|
||||
await page.waitForTimeout(5000);
|
||||
|
||||
await page.goto("/admin/login");
|
||||
await page.locator('//*[@id="new-project-btn"]').click();
|
||||
await page.getByText("New Project", { exact: true }).click();
|
||||
await page.waitForTimeout(5000);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
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