feat: Composio Component Upgrade (#6905)

* feat: Add button component and update dropdown rendering

* feat: Implement ListSelectionComponent and update ButtonComponent

- Add new ListSelectionComponent for selecting actions with search functionality
- Refactor ButtonComponent to use ListSelectionComponent
- Update ParameterRenderComponent to simplify button rendering
- Modify DropdownComponent to prepare for future changes

* feat: Add Sortable.js for drag-and-drop functionality and grid icon

- Integrated Sortable.js library in index.html and package files
- Added GridHorizontalIcon for drag handle visualization
- Updated ButtonComponent with commented sortable list example
- Improved input styling in ListSelectionComponent

* feat: Add ButtonInput for Composio API and update input types

- Introduced ButtonInput class in inputs module
- Added new button input fields to ComposioAPIComponent
- Updated FieldTypes enum to include BUTTON type
- Modified frontend ButtonComponent layout
- Removed commented-out code in DropdownComponent

* [autofix.ci] apply automated fixes

* feat: Enhance ButtonComponent with dynamic type and improved UI

- Add type prop to ButtonComponent to support different rendering modes
- Implement tool name and actions button layouts
- Add sample action data for actions button type
- Improve sortable list styling and interaction
- Refactor ButtonComponent to handle different use cases

* refactor: Improve ButtonComponent layout and Sortable configuration

- Adjust Sortable animation duration
- Modify list item layout and positioning
- Remove unnecessary CSS classes
- Simplify button and icon positioning

* feat: Integrate react-sortablejs for improved drag-and-drop functionality

- Replace Sortable.js with react-sortablejs library
- Add TypeScript types for SortableJS
- Update ButtonComponent to use ReactSortable component
- Improve list item rendering and styling
- Remove manual Sortable initialization

* feat: Enhance ButtonComponent with authentication and dynamic action data

- Add isAuthenticated state to control button visibility
- Introduce initialActionData and actionData state for dynamic list management
- Update button styling and icon for unauthenticated state
- Enable ReactSortable to modify action data list

* style: Update ButtonComponent styling with accent-themed colors

* style: Refine button styling with amber accent theme

* feat: Enhance ButtonComponent with dynamic rendering and improved UX

* feat: Add external link to DataStax Wikipedia page on button click

* feat: Implement delete action for button component action data

* feat: Enhance ButtonComponent with authentication and UI improvements

* feat: Enhance ListSelectionComponent with dynamic data and multi-select functionality

* feat: Improve ListSelectionComponent and ButtonComponent interaction and authentication flow

* refactor: Replace ButtonInput with ListSelectionInput in Composio API and input components

* [autofix.ci] apply automated fixes

* refactor: Optimize ListComponent with memoization and improved state management

* feat: Add selection type support to Composio API and ListSelectionComponent for improved user interaction

* refactor: Rename action-related props in ListSelectionComponent and ListComponent for clarity and consistency

* feat: Expand ListSelectionInput options and enhance ListSelectionComponent with dynamic data and metadata support

* refactor: Clean up comments and improve code readability in ListSelectionComponent and ListComponent

* [autofix.ci] apply automated fixes

* chore: Remove debug log from ParameterRenderComponent and add text/plain type to DRAG_EVENTS_CUSTOM_TYPES

* feat: Introduce ConnectionInput and SortableListInput components, replacing ListSelectionInput; enhance ComposioAPIComponent with new input types and improve parameter rendering with connection support

* [autofix.ci] apply automated fixes

* feat: Enhance ConnectionComponent with new state management and selection handling; update SortableListComponent to utilize new props for improved functionality

* feat: Implement SearchBarComponent to enhance ListSelectionComponent with improved search functionality and category selection

* feat: Update ComposioAPIComponent with external links and enhance ConnectionComponent with improved state management and dynamic link handling

* fix: Update ConnectionComponent layout and button styles for improved UI consistency

* chore: Remove unused Sortable.js script from index.html to streamline frontend resources

* Update index.tsx

* [autofix.ci] apply automated fixes

* [autofix.ci] apply automated fixes (attempt 2/3)

* Add backend support for composio actions

* [autofix.ci] apply automated fixes

* Update composio_api.py

* Update composio_api.py

* [autofix.ci] apply automated fixes

* fix: enhance ListSelectionComponent styling and functionality

- Updated ListItem component to accept a className prop for better customization.
- Improved item name display with truncation for better UI handling.
- Adjusted DialogContent styling for responsive width and layout.
- Enhanced overflow handling in the selection list for better user experience.

* feat: enhance ListSelectionComponent and ConnectionComponent functionality

- Added onSelection prop to ListSelectionComponent for handling item selection actions.
- Improved item selection logic to trigger onSelection callback.
- Refactored connection handling in ConnectionComponent to manage loading state and authentication.
- Updated connection link handling to ensure proper link retrieval and usage.

* fix: reset search input in ListSelectionComponent after selection

- Added functionality to clear the search input by setting it to an empty string when an item is selected. This improves user experience by ensuring the search field is reset after a selection is made.

* fix: improve selection logic and styling in ListSelectionComponent and ConnectionComponent

- Enhanced ListItem component to conditionally apply a green color for validated items.
- Updated selection logic in ListSelectionComponent to consider items with a validated link as selected.
- Initialized authentication state in ConnectionComponent based on the connection link status.
- Adjusted connection handling to update authentication state when the link changes.

* fix: refine item selection handling in ConnectionComponent

- Removed the setting of selectedItem state in the handleSelection function to streamline the selection process.
- Updated logic to focus on handling new value updates without maintaining a separate selected item state.

* fix: enhance ConnectionComponent and ListSelectionComponent functionality

- Removed console log from ListSelectionComponent to clean up the code.
- Added nodeId, nodeClass, and name props to ConnectionComponent for improved data handling.
- Implemented polling logic in ConnectionComponent to validate connections and manage loading states effectively.
- Updated loading state handling in the button to reflect polling status.

* fix: refactor ConnectionComponent for improved state management and polling logic

- Streamlined prop destructuring for better readability.
- Enhanced state management for polling and authentication.
- Added cleanup effects for polling intervals and timeouts.
- Improved event handling for connection button clicks and selection dialog.
- Updated comments for clarity and organization.

* Support handling the auth parameter

* [autofix.ci] apply automated fixes

* fix: improve key handling and state management in ListSelection and SortableList components

* Updated key assignment in ListSelectionComponent to include index for uniqueness.
* Enhanced SortableListComponent with useEffect to initialize listData from props and improved remove handler to update state correctly.
* Refactored setListData to ensure proper state updates and value handling.

* fix: update selected item handling in ConnectionComponent (#7280)

* Added useEffect to set selected item based on value and options.
* Modified handleSelection to ensure selected item structure is consistent.

* Fix output of component in actions

* fix: enhance connection handling in ConnectionComponent

* Updated handleConnectionButtonClick to accept a parameter for connection checking.
* Modified event handler to call handleConnectionButtonClick with the appropriate argument based on connection state.
* Ensured that the connection link opens in a new tab only when the connection check is true.

* [autofix.ci] apply automated fixes

* Remove the search categories

* fix: update list data handling in SortableListComponent

* Refactored useEffect to correctly set listData when value changes.
* Introduced a temporary variable to hold the value before updating state.

* refactor: optimize list data management in SortableListComponent

* Replaced useEffect with useMemo for listData initialization.
* Simplified state update logic in createRemoveHandler and setListDataHandler.
* Improved key assignment for SortableListItem to handle potential undefined names.

* Check if validated before passing link

* [autofix.ci] apply automated fixes

* refactor: improve connection handling and state management in ConnectionComponent

* Enhanced state management for tracking connection status and UI states.
* Updated polling management to prevent memory leaks and ensure proper cleanup.
* Simplified event handlers for connection button clicks and selection handling.
* Improved comments for better code clarity and understanding.

* fix: improve connection link handling in ConnectionComponent

* Added logging for connectionLink to aid in debugging.
* Updated useEffect to set the link only if connectionLink is not empty.
* Implemented authentication state change when connectionLink is validated.

* Clear list of actions when changing tools

* Update composio_api.py

* [autofix.ci] apply automated fixes

* feat: enhance error handling in ConnectionComponent

* Introduced ShadTooltip for displaying error messages.
* Added state management for error visibility and updated connection link handling based on error data.
* Modified button behavior and icon display based on connection status.

* refactor: simplify error handling in ConnectionComponent

* Removed unused state for error tooltip visibility.
* Updated error handling logic to include connection link status in addition to error data.

* Properly indicate error status in component

* Better management of tool name helper text

* refactor: streamline selected item initialization in ConnectionComponent

* Simplified the logic for setting the selected item based on the value prop.
* Ensured that the selected item is updated correctly when value or options change.

* fix: update authentication status in ConnectionComponent

* Added logic to set authentication status based on the selected option.
* Ensured that authentication state is updated when no option is selected.

* feat: update composio package (#7325)

update composio package

* Update Gmail Agent.json

* refactor: optimize InputGlobalComponent logic

* Removed unused imports and streamlined the useEffect and useMemo hooks for better performance.
* Enhanced the handling of unavailable fields and initial load completion state.
* Simplified the logic for setting the selected option based on the value and load_from_db status.

* refactor: improve error handling in ConnectionComponent

* Removed dependency on errorData in useEffect for setting link state.
* Streamlined error handling logic to focus solely on connectionLink status.

* refactor: enhance connection handling in ConnectionComponent

* Merged error handling logic into a single useEffect for improved clarity.
* Updated link state management to reflect the selected item's validation status.
* Ensured authentication state is set correctly based on the selected item.

* Filter list of available tools

* refactor: update ListSelectionComponent to use limit instead of selection type

* Removed the SelectionMode type and replaced it with a limit prop for selection.
* Adjusted selection logic to enforce the limit on selected items.
* Updated related components to reflect the new limit prop instead of type.

* [autofix.ci] apply automated fixes

* Add limit field and santiziation

* Update inputs.py

* Update composio_api.py

* Show actions only if a tool is selected

* [autofix.ci] apply automated fixes

* refactor: add limit prop to ParameterRenderComponent and SortableListComponent

* Introduced a limit prop to both components to control item rendering and selection behavior.
* Updated SortableListItem to conditionally render elements based on the limit value.
* Adjusted button visibility and styles in SortableListComponent according to the limit prop.

* fix: adjust styles in SortableListItem based on limit prop

* Updated height and padding styles for SortableListItem when limit is set to 1.
* Ensured consistent spacing and visibility of elements based on the limit value.

* Push latest template

* style: enhance SortableListItem styles for improved interaction

* Updated class names to ensure proper styling for hover and group states.
* Improved visibility of the remove button based on the limit prop, enhancing user experience.

* Update composio_api.py

* style: update SortableListItem max-width for improved layout

* Adjusted max-width class for SortableListItem when limit is set to 1, enhancing the component's visual consistency.

* Update Gmail Agent.json

* style: enhance ListItem component for better interaction

* Updated class names to improve hover effects and selected state styling.
* Added rounded corners and background color changes for better visual feedback.

* style: refine ListSelectionComponent and SortableListComponent layout

* Simplified class names in ListItem for cleaner styling.
* Moved HelperTextComponent into a dedicated div in SortableListComponent for improved layout and spacing.

* refactor: clean up ConnectionComponent code

* Removed unused imports and comments for better readability.
* Updated polling timeout duration from 30 seconds to 9 seconds to prevent indefinite polling.

* Fix bug with link on refresh

* Update Gmail Agent.json

* Update Gmail Agent.json

* Update Gmail Agent.json

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Eric Hare <ericrhare@gmail.com>
Co-authored-by: Edwin Jose <edwin.jose@datastax.com>
This commit is contained in:
Deon Sanchez 2025-03-29 04:12:51 -06:00 • committed by GitHub
commit d3104e15ae
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 1251 additions and 645 deletions

View file

@ -76,6 +76,7 @@
"react-markdown": "^8.0.7",
"react-pdf": "^9.0.0",
"react-router-dom": "^6.23.1",
"react-sortablejs": "^6.1.4",
"react-syntax-highlighter": "^15.5.0",
"reactflow": "^11.11.3",
"rehype-mathjax": "^4.0.3",
@ -84,6 +85,7 @@
"remark-math": "^6.0.0",
"shadcn-ui": "^0.9.4",
"short-unique-id": "^5.2.0",
"sortablejs": "^1.15.6",
"tailwind-merge": "^2.3.0",
"tailwindcss-animate": "^1.0.7",
"uuid": "^10.0.0",
@ -107,6 +109,7 @@
"@types/node": "^20.14.2",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@types/sortablejs": "^1.15.8",
"@types/uuid": "^9.0.8",
"@vitejs/plugin-react-swc": "^3.7.0",
"autoprefixer": "^10.4.19",
@ -5562,6 +5565,11 @@
"@types/react": "^18.0.0"
}
},
"node_modules/@types/sortablejs": {
"version": "1.15.8",
"resolved": "https://registry.npmjs.org/@types/sortablejs/-/sortablejs-1.15.8.tgz",
"integrity": "sha512-b79830lW+RZfwaztgs1aVPgbasJ8e7AXtZYHTELNXZPsERt4ymJdjV4OccDbHQAvHrCcFpbF78jkm0R6h/pZVg=="
},
"node_modules/@types/stack-utils": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz",
@ -6669,6 +6677,11 @@
"resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz",
"integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w=="
},
"node_modules/classnames": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz",
"integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA=="
},
"node_modules/cli-boxes": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz",
@ -13126,6 +13139,26 @@
"sisteransi": "^1.0.5"
}
},
"node_modules/react-sortablejs": {
"version": "6.1.4",
"resolved": "https://registry.npmjs.org/react-sortablejs/-/react-sortablejs-6.1.4.tgz",
"integrity": "sha512-fc7cBosfhnbh53Mbm6a45W+F735jwZ1UFIYSrIqcO/gRIFoDyZeMtgKlpV4DdyQfbCzdh5LoALLTDRxhMpTyXQ==",
"dependencies": {
"classnames": "2.3.1",
"tiny-invariant": "1.2.0"
},
"peerDependencies": {
"@types/sortablejs": "1",
"react": ">=16.9.0",
"react-dom": ">=16.9.0",
"sortablejs": "1"
}
},
"node_modules/react-sortablejs/node_modules/tiny-invariant": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.2.0.tgz",
"integrity": "sha512-1Uhn/aqw5C6RI4KejVeTg6mIS7IqxnLJ8Mv2tV5rTc0qWobay7pDUz6Wi392Cnc8ak1H0F2cjoRzb2/AW4+Fvg=="
},
"node_modules/react-style-singleton": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz",
@ -14297,6 +14330,11 @@
"node": ">=0.10.0"
}
},
"node_modules/sortablejs": {
"version": "1.15.6",
"resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.15.6.tgz",
"integrity": "sha512-aNfiuwMEpfBM/CN6LY0ibyhxPfPbyFeBTYJKCvzkJ2GkUpazIt3H+QIPAMHwqQ7tMKaHz1Qj+rJJCqljnf4p3A=="
},
"node_modules/source-map": {
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",

View file

@ -71,6 +71,7 @@
"react-markdown": "^8.0.7",
"react-pdf": "^9.0.0",
"react-router-dom": "^6.23.1",
"react-sortablejs": "^6.1.4",
"react-syntax-highlighter": "^15.5.0",
"reactflow": "^11.11.3",
"rehype-mathjax": "^4.0.3",
@ -79,6 +80,7 @@
"remark-math": "^6.0.0",
"shadcn-ui": "^0.9.4",
"short-unique-id": "^5.2.0",
"sortablejs": "^1.15.6",
"tailwind-merge": "^2.3.0",
"tailwindcss-animate": "^1.0.7",
"uuid": "^10.0.0",
@ -130,6 +132,7 @@
"@types/node": "^20.14.2",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@types/sortablejs": "^1.15.8",
"@types/uuid": "^9.0.8",
"@vitejs/plugin-react-swc": "^3.7.0",
"autoprefixer": "^10.4.19",

View file

@ -0,0 +1,170 @@
import ForwardedIconComponent from "@/components/common/genericIconComponent";
import SearchBarComponent from "@/components/core/parameterRenderComponent/components/searchBarComponent";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent } from "@/components/ui/dialog-with-no-close";
import { cn } from "@/utils/utils";
import { useCallback, useMemo, useState } from "react";
// Update interface with better types
interface ListSelectionComponentProps {
open: boolean;
options: any[];
onClose: () => void;
setSelectedList: (action: any[]) => void;
selectedList: any[];
searchCategories?: string[];
onSelection?: (action: any) => void;
limit?: number;
}
const ListItem = ({
item,
isSelected,
onClick,
className,
}: {
item: any;
isSelected: boolean;
onClick: () => void;
className?: string;
}) => (
<Button
key={item.id}
unstyled
size="sm"
className={cn("w-full rounded-md py-3 pl-3 pr-3 hover:bg-muted", className)}
onClick={onClick}
>
<div className="flex items-center gap-2">
{item.icon && (
<ForwardedIconComponent name={item.icon} className="h-5 w-5" />
)}
<span className="truncate font-semibold">{item.name}</span>
{"metaData" in item && item.metaData && (
<span className="text-gray-500">{item.metaData}</span>
)}
{isSelected ? (
<ForwardedIconComponent
name="check"
className={cn(
"ml-auto flex h-4 w-4",
item.link === "validated" && "text-green-500",
)}
/>
) : (
<span className="ml-auto flex h-4 w-4" />
)}
</div>
</Button>
);
const ListSelectionComponent = ({
open,
onClose,
searchCategories = [],
onSelection,
setSelectedList = () => {},
selectedList = [],
options,
limit = 1,
}: ListSelectionComponentProps) => {
const [search, setSearch] = useState("");
const filteredList = useMemo(() => {
if (!search.trim()) {
return options;
}
const searchTerm = search.toLowerCase();
return options.filter((item) =>
item.name.toLowerCase().includes(searchTerm),
);
}, [options, search]);
const handleSelectAction = useCallback(
(action: any) => {
if (limit !== 1) {
// Multiple selection mode
const isAlreadySelected = selectedList.some(
(selectedItem) => selectedItem.name === action.name,
);
if (isAlreadySelected) {
setSelectedList(
selectedList.filter(
(selectedItem) => selectedItem.name !== action.name,
),
);
} else {
// Check if we've reached the selection limit
if (selectedList.length < limit) {
setSelectedList([...selectedList, action]);
}
}
} else {
// Single selection mode
setSelectedList([
{
name: action.name,
icon: "icon" in action ? action.icon : undefined,
link: "link" in action ? action.link : undefined,
},
]);
onClose();
setSearch("");
}
},
[selectedList, setSelectedList, onClose, limit],
);
const handleCloseDialog = useCallback(() => {
onClose();
}, [onClose]);
return (
<Dialog open={open} onOpenChange={handleCloseDialog}>
<DialogContent className="flex !w-auto w-fit min-w-[20vw] max-w-[50vw] flex-col">
<div className="flex items-center justify-between">
<SearchBarComponent
searchCategories={searchCategories}
search={search}
setSearch={setSearch}
/>
<Button
unstyled
size="icon"
className="ml-auto h-[38px]"
onClick={handleCloseDialog}
>
<ForwardedIconComponent name="x" />
</Button>
</div>
<div className="flex max-h-[80vh] flex-col gap-1 overflow-y-auto">
{filteredList.length > 0 ? (
filteredList.map((item, index) => (
<ListItem
key={`${item.name}-${index}`}
item={item}
isSelected={
selectedList.some(
(selected) => selected.name === item.name,
) || item.link === "validated"
}
onClick={() => {
handleSelectAction(item);
onSelection?.(item);
}}
/>
))
) : (
<div className="py-3 text-center text-gray-500">
No items match your search
</div>
)}
</div>
</DialogContent>
</Dialog>
);
};
export default ListSelectionComponent;

View file

@ -0,0 +1,244 @@
import ForwardedIconComponent from "@/components/common/genericIconComponent";
import { Button } from "@/components/ui/button";
import { usePostTemplateValue } from "@/controllers/API/queries/nodes/use-post-template-value";
import ListSelectionComponent from "@/CustomNodes/GenericNode/components/ListSelectionComponent";
import { mutateTemplate } from "@/CustomNodes/helpers/mutate-template";
import useAlertStore from "@/stores/alertStore";
import { APIClassType } from "@/types/api";
import { cn } from "@/utils/utils";
import { memo, useEffect, useRef, useState } from "react";
import { InputProps } from "../../types";
import HelperTextComponent from "../helperTextComponent";
type ConnectionComponentProps = {
tooltip?: string;
name: string;
helperText?: string;
helperMetadata?: any;
options?: any[];
searchCategory?: string[];
buttonMetadata?: { variant?: string; icon?: string };
connectionLink?: string;
nodeClass: APIClassType;
nodeId: string;
};
const ConnectionComponent = ({
tooltip = "",
name,
helperText = "",
helperMetadata = { icon: undefined, variant: "muted-foreground" },
options = [],
searchCategory = [],
buttonMetadata = { variant: "destructive", icon: "unplug" },
connectionLink = "",
...baseInputProps
}: InputProps<any, ConnectionComponentProps>) => {
const {
value,
handleOnNewValue,
handleNodeClass,
nodeClass,
nodeId,
placeholder,
} = baseInputProps;
const setErrorData = useAlertStore((state) => state.setErrorData);
const [isAuthenticated, setIsAuthenticated] = useState(
connectionLink === "validated",
);
const [link, setLink] = useState("");
const [isPolling, setIsPolling] = useState(false);
const [open, setOpen] = useState(false);
const [selectedItem, setSelectedItem] = useState<any[]>([]);
const pollingInterval = useRef<NodeJS.Timeout | null>(null);
const pollingTimeout = useRef<NodeJS.Timeout | null>(null);
const postTemplateValue = usePostTemplateValue({
parameterId: name,
nodeId: nodeId,
node: nodeClass,
});
// Initialize selected item from value on component mount
useEffect(() => {
const selectedOption = value
? options.find((option) => option.name === value)
: null;
setSelectedItem([
selectedOption
? { name: selectedOption.name, icon: selectedOption.icon }
: { name: "", icon: "" },
]);
// Update authentication status based on selected option
if (!selectedOption) {
setIsAuthenticated(false);
}
}, [value, options]);
useEffect(() => {
if (connectionLink !== "") {
setLink(connectionLink);
if (connectionLink === "validated") {
setIsAuthenticated(true);
}
}
if (connectionLink === "error") {
setLink("error");
}
}, [connectionLink]);
// Handles the connection button click to open connection in new tab and start polling
const handleConnectionButtonClick = () => {
if (selectedItem?.length === 0) return;
window.open(link, "_blank");
startPolling();
};
// Initiates polling to check connection status periodically
const startPolling = () => {
if (!selectedItem[0]?.name) return;
setLink("loading");
// Initialize polling
setIsPolling(true);
// Clear existing timers
stopPolling();
// Set up polling interval - check connection status every 3 seconds
pollingInterval.current = setInterval(() => {
mutateTemplate(
{ validate: selectedItem[0]?.name || "" },
nodeClass,
handleNodeClass,
postTemplateValue,
setErrorData,
name,
() => {
// Check if the connection was successful
if (connectionLink === "validated") {
stopPolling();
setIsAuthenticated(true);
}
},
nodeClass.tool_mode,
);
}, 3000);
// Set timeout to stop polling after 9 seconds to prevent indefinite polling
pollingTimeout.current = setTimeout(() => {
stopPolling(link !== "");
// If we timed out and link is still loading, reset it
}, 9000);
};
// Cleans up polling timers to prevent memory leaks
const stopPolling = (resetLink = false) => {
setIsPolling(false);
if (resetLink) {
setLink(connectionLink || "");
}
if (pollingInterval.current) clearInterval(pollingInterval.current);
if (pollingTimeout.current) clearTimeout(pollingTimeout.current);
};
// Updates selected item and triggers parent component update
const handleSelection = (item: any) => {
setIsAuthenticated(false);
setSelectedItem([{ name: item.name }]);
setLink(item.link === "validated" ? "validated" : "loading");
if (item.link === "validated") {
setIsAuthenticated(true);
}
handleOnNewValue({ value: item.name }, { skipSnapshot: true });
};
// Dialog control handlers
const handleOpenListSelectionDialog = () => setOpen(true);
const handleCloseListSelectionDialog = () => setOpen(false);
// Render component
return (
<div className="flex w-full flex-col gap-2">
<div className="flex w-full flex-row items-center gap-2">
<Button
variant="primary"
size="xs"
role="combobox"
onClick={handleOpenListSelectionDialog}
className="dropdown-component-outline input-edit-node w-full py-2"
>
<div className={cn("flex w-full items-center justify-start text-sm")}>
{selectedItem[0]?.icon && (
<ForwardedIconComponent
name={selectedItem[0]?.icon}
className="h-5 w-5"
/>
)}
<span className="ml-2 truncate">
{selectedItem[0]?.name || placeholder}
</span>
<ForwardedIconComponent
name="ChevronsUpDown"
className="ml-auto h-5 w-5"
/>
</div>
</Button>
{!isAuthenticated && (
<Button
size="icon"
variant="ghost"
loading={link === "loading" || isPolling}
disabled={!selectedItem[0]?.name || link === "" || link === "error"}
className={cn(
"h-9 w-10 rounded-md border disabled:opacity-50",
buttonMetadata.variant && `border-${buttonMetadata.variant}`,
)}
onClick={link === "error" ? undefined : handleConnectionButtonClick}
>
<ForwardedIconComponent
name={
link === "error"
? "triangle-alert"
: buttonMetadata.icon || "unplug"
}
className={cn(
"h-5 w-5",
buttonMetadata.variant && `text-${buttonMetadata.variant}`,
)}
/>
</Button>
)}
</div>
{helperText && (
<HelperTextComponent
helperText={helperText}
helperMetadata={helperMetadata}
/>
)}
<ListSelectionComponent
open={open}
onSelection={handleSelection}
onClose={handleCloseListSelectionDialog}
searchCategories={searchCategory}
setSelectedList={setSelectedItem}
selectedList={selectedItem}
options={options}
/>
</div>
);
};
export default memo(ConnectionComponent);

View file

@ -12,6 +12,8 @@ export default function DropdownComponent({
name,
dialogInputs,
optionsMetaData,
nodeClass,
nodeId,
...baseInputProps
}: InputProps<string, DropDownComponentType>) {
const onChange = (value: any, dbValue?: boolean, skipSnapshot?: boolean) => {

View file

@ -0,0 +1,36 @@
import ForwardedIconComponent from "@/components/common/genericIconComponent";
import { cn } from "@/utils/utils";
type HelperTextComponentProps = {
helperText: string;
helperMetadata?: { icon: string | undefined; variant: string };
};
const HelperTextComponent = ({
helperText,
helperMetadata = { icon: undefined, variant: "muted-foreground" },
}: HelperTextComponentProps) => {
return (
<div className="flex w-full flex-row items-center gap-2">
{helperMetadata?.icon && (
<ForwardedIconComponent
name={helperMetadata?.icon}
className={cn(
`h-5 w-5`,
helperMetadata?.variant && `text-${helperMetadata?.variant}`,
)}
/>
)}
<div
className={cn(
"flex w-full flex-col text-xs",
helperMetadata?.variant && `text-${helperMetadata?.variant}`,
)}
>
{helperText}
</div>
</div>
);
};
export default HelperTextComponent;

View file

@ -1,13 +1,8 @@
import {
useDeleteGlobalVariables,
useGetGlobalVariables,
} from "@/controllers/API/queries/variables";
import { useGetGlobalVariables } from "@/controllers/API/queries/variables";
import GeneralDeleteConfirmationModal from "@/shared/components/delete-confirmation-modal";
import GeneralGlobalVariableModal from "@/shared/components/global-variable-modal";
import { useGlobalVariablesStore } from "@/stores/globalVariablesStore/globalVariables";
import { useEffect, useMemo } from "react";
import DeleteConfirmationModal from "../../../../../modals/deleteConfirmationModal";
import useAlertStore from "../../../../../stores/alertStore";
import { useEffect, useMemo, useRef } from "react";
import { cn } from "../../../../../utils/utils";
import ForwardedIconComponent from "../../../../common/genericIconComponent";
import { CommandItem } from "../../../../ui/command";
@ -34,34 +29,63 @@ export default function InputGlobalComponent({
(state) => state.unavailableFields,
);
useEffect(() => {
if (globalVariables && !disabled) {
if (
load_from_db &&
!globalVariables.find((variable) => variable.name === value)
) {
handleOnNewValue(
{ value: "", load_from_db: false },
{ skipSnapshot: true },
);
}
if (
!load_from_db &&
value === "" &&
unavailableFields &&
Object.keys(unavailableFields).includes(display_name ?? "")
) {
handleOnNewValue(
{ value: unavailableFields[display_name ?? ""], load_from_db: true },
{ skipSnapshot: true },
);
}
const initialLoadCompleted = useRef(false);
const valueExists = useMemo(() => {
return (
globalVariables?.some((variable) => variable.name === value) ?? false
);
}, [globalVariables, value]);
const unavailableField = useMemo(() => {
if (
display_name &&
unavailableFields &&
Object.keys(unavailableFields).includes(display_name)
) {
return unavailableFields[display_name];
}
}, [globalVariables, unavailableFields, disabled]);
return null;
}, [unavailableFields, display_name]);
useMemo(() => {
if (disabled) {
return;
}
if (load_from_db && globalVariables && !valueExists) {
handleOnNewValue(
{ value: "", load_from_db: false },
{ skipSnapshot: true },
);
}
}, [
globalVariables,
unavailableFields,
disabled,
load_from_db,
valueExists,
unavailableField,
value,
handleOnNewValue,
]);
useEffect(() => {
if (initialLoadCompleted.current || disabled || unavailableField === null) {
return;
}
handleOnNewValue(
{ value: unavailableField, load_from_db: true },
{ skipSnapshot: true },
);
initialLoadCompleted.current = true;
}, [unavailableField, disabled, load_from_db, value, handleOnNewValue]);
function handleDelete(key: string) {
if (value === key && load_from_db) {
handleOnNewValue({ value: "", load_from_db: false });
if (value === key) {
handleOnNewValue({ value: "", load_from_db: load_from_db });
}
}
@ -96,13 +120,7 @@ export default function InputGlobalComponent({
onConfirmDelete={() => handleDelete(option)}
/>
)}
selectedOption={
load_from_db &&
globalVariables &&
globalVariables?.map((variable) => variable.name).includes(value ?? "")
? value
: ""
}
selectedOption={load_from_db && valueExists ? value : ""}
setSelectedOption={(value) => {
handleOnNewValue({
value: value,

View file

@ -0,0 +1,76 @@
import { ForwardedIconComponent } from "@/components/common/genericIconComponent";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Input } from "@/components/ui/input";
import { useState } from "react";
interface SearchBarComponentProps {
searchCategories?: string[];
search: string;
setSearch: (search: string) => void;
placeholder?: string;
onCategoryChange?: (category: string) => void;
}
const SearchBarComponent = ({
searchCategories,
search,
setSearch,
placeholder = "Search tools...",
onCategoryChange,
}: SearchBarComponentProps) => {
const [selectedCategory, setSelectedCategory] = useState(
searchCategories?.[0] || "All",
);
const handleCategoryChange = (category: string) => {
setSelectedCategory(category);
if (onCategoryChange) {
onCategoryChange(category);
}
};
return (
<div className="mr-10 flex w-full items-center rounded-md border">
{searchCategories && searchCategories.length > 0 && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button className="flex items-center gap-2 pl-4 text-sm">
<span className="truncate">{selectedCategory}</span>
<ForwardedIconComponent
name="chevron-down"
className="flex h-4 w-4"
/>
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
{searchCategories.map((category) => (
<DropdownMenuItem
key={category}
onClick={() => handleCategoryChange(category)}
className="cursor-pointer"
>
<span className="flex items-center gap-2 truncate px-2 py-1 text-sm">
{category}
</span>
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
)}
<Input
icon="search"
placeholder={placeholder}
value={search}
onChange={(e) => setSearch(e.target.value)}
inputClassName="border-none focus:ring-0"
/>
</div>
);
};
export default SearchBarComponent;

View file

@ -0,0 +1,184 @@
import ForwardedIconComponent from "@/components/common/genericIconComponent";
import { Button } from "@/components/ui/button";
import ListSelectionComponent from "@/CustomNodes/GenericNode/components/ListSelectionComponent";
import { cn } from "@/utils/utils";
import { memo, useCallback, useMemo, useState } from "react";
import { ReactSortable } from "react-sortablejs";
import { InputProps } from "../../types";
import HelperTextComponent from "../helperTextComponent";
type SortableListComponentProps = {
tooltip?: string;
name?: string;
helperText?: string;
helperMetadata?: any;
options?: any[];
searchCategory?: string[];
icon?: string;
limit?: number;
};
const SortableListItem = memo(
({
data,
index,
onRemove,
limit = 1,
}: {
data: any;
index: number;
onRemove: () => void;
limit?: number;
}) => (
<li
className={cn(
"inline-flex h-12 w-full items-center gap-2 text-sm font-medium text-gray-800",
limit === 1 ? "h-10 rounded-md bg-muted" : "group cursor-grab",
)}
>
{limit !== 1 && (
<ForwardedIconComponent
name="grid-horizontal"
className="h-5 w-5 fill-gray-300 text-gray-300"
/>
)}
<div className="flex w-full items-center gap-x-2">
{limit !== 1 && (
<div className="flex h-5 w-5 items-center justify-center rounded-full bg-gray-400 text-center text-white">
{index + 1}
</div>
)}
<span
className={cn(
"truncate text-primary",
limit === 1 ? "max-w-56 pl-3" : "max-w-48",
)}
>
{data.name}
</span>
</div>
<Button
size="icon"
variant={limit !== 1 ? "outline" : "ghost"}
className={cn(
"ml-auto h-7 w-7 opacity-0 transition-opacity duration-200",
limit === 1
? "group pr-3 opacity-100"
: "hover:border hover:border-destructive hover:bg-transparent hover:opacity-100",
)}
onClick={onRemove}
>
<ForwardedIconComponent
name="x"
className={cn(
"h-6 w-6 text-red-500",
limit === 1 && "text-gray-500 group-hover:text-input",
)}
/>
</Button>
</li>
),
);
const SortableListComponent = ({
tooltip = "",
name,
helperText = "",
helperMetadata = { icon: undefined, variant: "muted-foreground" },
options = [],
searchCategory = [],
limit,
...baseInputProps
}: InputProps<any, SortableListComponentProps>) => {
const { placeholder, handleOnNewValue, value } = baseInputProps;
const [open, setOpen] = useState(false);
// Convert value to an array if it exists, otherwise use empty array
const listData = useMemo(() => (Array.isArray(value) ? value : []), [value]);
const createRemoveHandler = useCallback(
(index: number) => () => {
const newList = listData.filter((_, i) => i !== index);
handleOnNewValue({ value: newList });
},
[listData, handleOnNewValue],
);
const setListDataHandler = useCallback(
(newList: any[]) => {
handleOnNewValue({ value: newList });
},
[handleOnNewValue],
);
const handleCloseListSelectionDialog = useCallback(() => {
setOpen(false);
}, []);
const handleOpenListSelectionDialog = useCallback(() => {
setOpen(true);
}, []);
return (
<div className="flex w-full flex-col">
<div className="flex w-full flex-row gap-2">
{!(limit === 1 && listData.length === 1) && (
<Button
variant="default"
size="xs"
role="combobox"
onClick={handleOpenListSelectionDialog}
className="dropdown-component-outline input-edit-node w-full py-2"
>
<div className={cn("flex items-center text-sm font-semibold")}>
{placeholder}
</div>
</Button>
)}
</div>
{listData.length > 0 && (
<div className="flex w-full flex-col">
<ReactSortable
list={listData}
setList={setListDataHandler}
className={"flex w-full flex-col"}
>
{listData.map((data, index) => (
<SortableListItem
key={`${data?.name || "item"}-${index}`}
data={data}
index={index}
onRemove={createRemoveHandler(index)}
limit={limit}
/>
))}
</ReactSortable>
</div>
)}
{helperText && (
<div className="pt-2">
<HelperTextComponent
helperText={helperText}
helperMetadata={helperMetadata}
/>
</div>
)}
<ListSelectionComponent
open={open}
onClose={handleCloseListSelectionDialog}
searchCategories={searchCategory}
setSelectedList={setListDataHandler}
selectedList={listData}
options={options}
limit={limit}
/>
</div>
);
};
export default memo(SortableListComponent);

View file

@ -6,6 +6,7 @@ import TabComponent from "@/components/core/parameterRenderComponent/components/
import { TEXT_FIELD_TYPES } from "@/constants/constants";
import { APIClassType, InputFieldType } from "@/types/api";
import { useMemo } from "react";
import ConnectionComponent from "./components/connectionComponent";
import DictComponent from "./components/dictComponent";
import { EmptyParameterComponent } from "./components/emptyParameterComponent";
import FloatComponent from "./components/floatComponent";
@ -17,6 +18,7 @@ import LinkComponent from "./components/linkComponent";
import MultiselectComponent from "./components/multiselectComponent";
import PromptAreaComponent from "./components/promptComponent";
import { RefreshParameterComponent } from "./components/refreshParameterComponent";
import SortableListComponent from "./components/sortableListComponent";
import { StrRenderComponent } from "./components/strRenderComponent";
import ToggleShadComponent from "./components/toggleShadComponent";
import { InputProps, NodeInfoType } from "./types";
@ -213,6 +215,37 @@ export function ParameterRenderComponent({
id={`slider_${id}`}
/>
);
case "sortableList":
return (
<SortableListComponent
{...baseInputProps}
helperText={templateData?.helper_text}
helperMetadata={templateData?.helper_text_metadata}
options={templateData?.options}
searchCategory={templateData?.search_category}
limit={templateData?.limit}
/>
);
case "connect":
const link =
templateData?.options?.find(
(option: any) => option?.name === templateValue,
)?.link || "";
return (
<ConnectionComponent
{...baseInputProps}
name={name}
nodeId={nodeId}
nodeClass={nodeClass}
helperText={templateData?.helper_text}
helperMetadata={templateData?.helper_text_metadata}
options={templateData?.options}
searchCategory={templateData?.search_category}
buttonMetadata={templateData?.button_metadata}
connectionLink={link as string}
/>
);
case "tab":
return (
<TabComponent

View file

@ -964,6 +964,7 @@ export const REFETCH_SERVER_HEALTH_INTERVAL = 20000;
export const DRAG_EVENTS_CUSTOM_TYPESS = {
genericnode: "genericNode",
notenode: "noteNode",
"text/plain": "text/plain",
};
export const NOTE_NODE_MIN_WIDTH = 324;

View file

@ -0,0 +1,26 @@
const SVGGridHorizontalIcon = (props) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-grip-horizontal"
{...props}
>
<circle cx="12" cy="9" r="1" />
<circle cx="19" cy="9" r="1" />
<circle cx="5" cy="9" r="1" />
<circle cx="12" cy="15" r="1" />
<circle cx="19" cy="15" r="1" />
<circle cx="5" cy="15" r="1" />
</svg>
);
};
export default SVGGridHorizontalIcon;

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-grip-horizontal"><circle cx="12" cy="9" r="1"/><circle cx="19" cy="9" r="1"/><circle cx="5" cy="9" r="1"/><circle cx="12" cy="15" r="1"/><circle cx="19" cy="15" r="1"/><circle cx="5" cy="15" r="1"/></svg>

After

Width:  |  Height:  |  Size: 406 B

View file

@ -0,0 +1,9 @@
import { forwardRef } from "react";
import SVGGridHorizontalIcon from "./GridHorizontalIcon";
export const GridHorizontalIcon = forwardRef<
SVGSVGElement,
React.PropsWithChildren<{}>
>((props, ref) => {
return <SVGGridHorizontalIcon ref={ref} {...props} />;
});

View file

@ -28,6 +28,8 @@
--accent-foreground: 0 0% 0%; /* hsl(0, 0%, 0%) */
--destructive: 0 72% 51%; /* hsl(0, 72%, 51%) */
--destructive-foreground: 0 0% 100%; /* hsl(0, 0%, 100%) */
--accent-amber: 26 90% 37%; /* hsl(26, 90%, 37%) */
--accent-amber-foreground: 26 90% 37%; /* hsl(26, 90%, 37%) */
--ring: 0 0% 0%; /* hsl(0, 0%, 0%) */
--primary-hover: 240 4% 16%; /* hsl(240, 4%, 16%) */
--secondary-hover: 240 6% 90%; /* hsl(240, 6%, 90%) */
@ -39,6 +41,7 @@
--accent-emerald-hover: 152.4 76% 80.4%; /* hsl(152.4, 76%, 80.4%) */
--accent-indigo: 226 100% 94%; /* hsl(226, 100%, 94%) */
--accent-indigo-foreground: 243 75% 59%; /* hsl(243, 75%, 59%) */
--accent-pink: 326 78% 95%; /* hsl(326, 78%, 95%) */
--accent-pink-foreground: 333 71% 51%; /* hsl(333, 71%, 51%) */
--tooltip: 0 0% 0%; /* hsl(0, 0%, 0%) */

View file

@ -6,6 +6,7 @@ import { DuckDuckGoIcon } from "@/icons/DuckDuckGo";
import { ExaIcon } from "@/icons/Exa";
import { GleanIcon } from "@/icons/Glean";
import { GoogleDriveIcon } from "@/icons/GoogleDrive";
import { GridHorizontalIcon } from "@/icons/GridHorizontal";
import { JSIcon } from "@/icons/JSicon";
import { LangwatchIcon } from "@/icons/Langwatch";
import { MilvusIcon } from "@/icons/Milvus";
@ -725,6 +726,7 @@ export const nodeIconsLucide: iconsType = {
IFixitLoader: IFixIcon,
CrewAI: CrewAiIcon,
NotDiamond: NotDiamondIcon,
GridHorizontal: GridHorizontalIcon,
Composio: ComposioIcon,
Meta: MetaIcon,
Midjorney: MidjourneyIcon,