diff --git a/packages/connect-react/src/components/SelectApp.tsx b/packages/connect-react/src/components/SelectApp.tsx index 0042e4b0446ec..652ecc5fb1232 100644 --- a/packages/connect-react/src/components/SelectApp.tsx +++ b/packages/connect-react/src/components/SelectApp.tsx @@ -24,13 +24,20 @@ export function SelectApp({ const instanceId = useId(); - // Debounce the search query + // Debounce the search query with cleanup guard useEffect(() => { + let cancelled = false; + const timer = setTimeout(() => { - setQ(inputValue); + if (!cancelled) { + setQ(inputValue); + } }, 300); // 300ms delay - return () => clearTimeout(timer); + return () => { + cancelled = true; + clearTimeout(timer); + }; }, [ inputValue, ]); @@ -106,7 +113,7 @@ export function SelectApp({ getOptionLabel={(o) => o.name || o.name_slug} // TODO fetch initial value app so we show name getOptionValue={(o) => o.name_slug} value={selectedValue} - onChange={(o) => onChange?.((o as AppResponse) || undefined)} + onChange={(o) => onChange?.(o ? (o as AppResponse) : undefined)} onInputChange={(v, { action }) => { // Only update on user input, not on blur/menu-close/etc if (action === "input-change") { diff --git a/packages/connect-react/src/components/SelectComponent.tsx b/packages/connect-react/src/components/SelectComponent.tsx index b20a6073da49c..b871785dcdf80 100644 --- a/packages/connect-react/src/components/SelectComponent.tsx +++ b/packages/connect-react/src/components/SelectComponent.tsx @@ -1,6 +1,6 @@ -import { useId } from "react"; -import Select from "react-select"; -import { useComponents } from "../hooks/use-components"; +import { useId, useState, useEffect, useMemo } from "react"; +import Select, { components } from "react-select"; +import { useComponentsWithPagination } from "../hooks/use-components"; import { AppResponse, V1Component, } from "@pipedream/sdk"; @@ -19,28 +19,107 @@ export function SelectComponent({ onChange, }: SelectComponentProps) { const instanceId = useId(); + const [searchQuery, setSearchQuery] = useState(""); + const [debouncedSearchQuery, setDebouncedSearchQuery] = useState(""); + + // Debounce search query to avoid excessive API calls + useEffect(() => { + let cancelled = false; + + const timer = setTimeout(() => { + if (!cancelled) { + setDebouncedSearchQuery(searchQuery); + } + }, 300); + + return () => { + cancelled = true; + clearTimeout(timer); + }; + }, [searchQuery]); + const { - isLoading, components, - } = useComponents({ + isLoading, + components: allComponents, + hasMore, + loadMore, + reset, + isLoadingMore, + } = useComponentsWithPagination({ app: app?.name_slug, componentType, }); - const selectedValue = components?.find((c) => c.key === value?.key) || null; + // Filter components based on search query (client-side) + const componentList = useMemo(() => { + if (!debouncedSearchQuery) { + return allComponents; + } + const query = debouncedSearchQuery.toLowerCase(); + return allComponents.filter(component => + component.name?.toLowerCase().includes(query) || + component.key?.toLowerCase().includes(query) + ); + }, [allComponents, debouncedSearchQuery]); + + const selectedValue = useMemo(() => { + // If we have a value but it's not in the current components list, + // preserve it for display purposes + const foundComponent = componentList?.find((c) => c.key === value?.key); + if (foundComponent) { + return foundComponent; + } else if (value?.key) { + // Return the partial value to maintain selection display + return value as V1Component; + } + return null; + }, [componentList, value]); + + // Custom MenuList component for infinite scroll - memoized to prevent recreation + const MenuList = useMemo(() => { + return (props: any) => ( + + {props.children} + {hasMore && !isLoading && ( +
{ + if (!isLoadingMore) { + loadMore(); + } + }} + > + {isLoadingMore ? 'Loading more...' : 'Load more'} +
+ )} +
+ ); + }, [hasMore, isLoading, isLoadingMore, loadMore]); return (