Skip to content

Commit 6eb6104

Browse files
WIP
1 parent 30b97f4 commit 6eb6104

File tree

6 files changed

+233
-28
lines changed

6 files changed

+233
-28
lines changed

packages/connect-react/src/components/SelectApp.tsx

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,20 @@ export function SelectApp({
2424

2525
const instanceId = useId();
2626

27-
// Debounce the search query
27+
// Debounce the search query with cleanup guard
2828
useEffect(() => {
29+
let cancelled = false;
30+
2931
const timer = setTimeout(() => {
30-
setQ(inputValue);
32+
if (!cancelled) {
33+
setQ(inputValue);
34+
}
3135
}, 300); // 300ms delay
3236

33-
return () => clearTimeout(timer);
37+
return () => {
38+
cancelled = true;
39+
clearTimeout(timer);
40+
};
3441
}, [
3542
inputValue,
3643
]);
@@ -106,7 +113,7 @@ export function SelectApp({
106113
getOptionLabel={(o) => o.name || o.name_slug} // TODO fetch initial value app so we show name
107114
getOptionValue={(o) => o.name_slug}
108115
value={selectedValue}
109-
onChange={(o) => onChange?.((o as AppResponse) || undefined)}
116+
onChange={(o) => onChange?.(o ? (o as AppResponse) : undefined)}
110117
onInputChange={(v, { action }) => {
111118
// Only update on user input, not on blur/menu-close/etc
112119
if (action === "input-change") {

packages/connect-react/src/components/SelectComponent.tsx

Lines changed: 87 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { useId } from "react";
2-
import Select from "react-select";
3-
import { useComponents } from "../hooks/use-components";
1+
import { useId, useState, useEffect, useMemo } from "react";
2+
import Select, { components } from "react-select";
3+
import { useComponentsWithPagination } from "../hooks/use-components";
44
import {
55
AppResponse, V1Component,
66
} from "@pipedream/sdk";
@@ -19,28 +19,107 @@ export function SelectComponent({
1919
onChange,
2020
}: SelectComponentProps) {
2121
const instanceId = useId();
22+
const [searchQuery, setSearchQuery] = useState("");
23+
const [debouncedSearchQuery, setDebouncedSearchQuery] = useState("");
24+
25+
// Debounce search query to avoid excessive API calls
26+
useEffect(() => {
27+
let cancelled = false;
28+
29+
const timer = setTimeout(() => {
30+
if (!cancelled) {
31+
setDebouncedSearchQuery(searchQuery);
32+
}
33+
}, 300);
34+
35+
return () => {
36+
cancelled = true;
37+
clearTimeout(timer);
38+
};
39+
}, [searchQuery]);
40+
2241
const {
23-
isLoading, components,
24-
} = useComponents({
42+
isLoading,
43+
components: allComponents,
44+
hasMore,
45+
loadMore,
46+
reset,
47+
isLoadingMore,
48+
} = useComponentsWithPagination({
2549
app: app?.name_slug,
2650
componentType,
2751
});
2852

29-
const selectedValue = components?.find((c) => c.key === value?.key) || null;
53+
// Filter components based on search query (client-side)
54+
const componentList = useMemo(() => {
55+
if (!debouncedSearchQuery) {
56+
return allComponents;
57+
}
58+
const query = debouncedSearchQuery.toLowerCase();
59+
return allComponents.filter(component =>
60+
component.name?.toLowerCase().includes(query) ||
61+
component.key?.toLowerCase().includes(query)
62+
);
63+
}, [allComponents, debouncedSearchQuery]);
64+
65+
const selectedValue = useMemo(() => {
66+
// If we have a value but it's not in the current components list,
67+
// preserve it for display purposes
68+
const foundComponent = componentList?.find((c) => c.key === value?.key);
69+
if (foundComponent) {
70+
return foundComponent;
71+
} else if (value?.key) {
72+
// Return the partial value to maintain selection display
73+
return value as V1Component;
74+
}
75+
return null;
76+
}, [componentList, value]);
77+
78+
// Custom MenuList component for infinite scroll - memoized to prevent recreation
79+
const MenuList = useMemo(() => {
80+
return (props: any) => (
81+
<components.MenuList {...props}>
82+
{props.children}
83+
{hasMore && !isLoading && (
84+
<div
85+
style={{ padding: '8px', textAlign: 'center', cursor: 'pointer' }}
86+
onClick={() => {
87+
if (!isLoadingMore) {
88+
loadMore();
89+
}
90+
}}
91+
>
92+
{isLoadingMore ? 'Loading more...' : 'Load more'}
93+
</div>
94+
)}
95+
</components.MenuList>
96+
);
97+
}, [hasMore, isLoading, isLoadingMore, loadMore]);
3098

3199
return (
32100
<Select
33101
instanceId={instanceId}
34102
className="react-select-container text-sm"
35103
classNamePrefix="react-select"
36-
options={components}
104+
options={componentList}
37105
getOptionLabel={(o) => o.name || o.key}
38106
getOptionValue={(o) => o.key}
39107
value={selectedValue}
40-
onChange={(o) => onChange?.((o as V1Component) || undefined)}
108+
onChange={(o) => onChange?.(o ? (o as V1Component) : undefined)}
41109
isLoading={isLoading}
110+
onInputChange={(inputValue, { action }) => {
111+
if (action === "input-change") {
112+
setSearchQuery(inputValue);
113+
}
114+
}}
115+
noOptionsMessage={({ inputValue }) =>
116+
isLoading ? "Loading..." :
117+
inputValue ? `No components found for "${inputValue}"` :
118+
"No components available"
119+
}
42120
components={{
43121
IndicatorSeparator: () => null,
122+
MenuList,
44123
}}
45124
/>
46125
);

packages/connect-react/src/hooks/use-apps.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,11 @@ export const useApps = (input?: GetAppsOpts) => {
1010
const query = useQuery({
1111
queryKey: [
1212
"apps",
13-
input,
13+
input?.q || "", // Stable key even if input is undefined
1414
],
1515
queryFn: () => client.apps(input),
16+
staleTime: 60000, // Consider data fresh for 1 minute
17+
gcTime: 300000, // Keep in cache for 5 minutes (formerly cacheTime)
1618
});
1719

1820
return {

packages/connect-react/src/hooks/use-component.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,14 @@ export const useComponent = (
1515
const query = useQuery({
1616
queryKey: [
1717
"component",
18-
key,
18+
key || "",
1919
],
2020
queryFn: () => client.component({
2121
key: key!,
2222
}),
2323
enabled: !!key,
24+
staleTime: 60000, // Consider data fresh for 1 minute
25+
gcTime: 300000, // Keep in cache for 5 minutes
2426
...opts?.useQueryOpts,
2527
});
2628

packages/connect-react/src/hooks/use-components.tsx

Lines changed: 121 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { useQuery } from "@tanstack/react-query";
2-
import type { GetComponentOpts } from "@pipedream/sdk";
2+
import { useState, useCallback, useEffect } from "react";
3+
import type { GetComponentOpts, V1Component } from "@pipedream/sdk";
34
import { useFrontendClient } from "./frontend-client-context";
45

56
/**
@@ -10,13 +11,131 @@ export const useComponents = (input?: GetComponentOpts) => {
1011
const query = useQuery({
1112
queryKey: [
1213
"components",
13-
input,
14+
input?.app || "",
15+
input?.componentType || "",
16+
input?.q || "",
17+
input?.limit || 100,
18+
input?.after || "",
1419
],
1520
queryFn: () => client.components(input),
21+
staleTime: 60000, // Consider data fresh for 1 minute
22+
gcTime: 300000, // Keep in cache for 5 minutes
1623
});
1724

1825
return {
1926
...query,
2027
components: query.data?.data || [],
2128
};
2229
};
30+
31+
/**
32+
* Get list of components with pagination support
33+
*/
34+
export const useComponentsWithPagination = (input?: Omit<GetComponentOpts, 'limit' | 'after' | 'q'>) => {
35+
const client = useFrontendClient();
36+
const [allComponents, setAllComponents] = useState<V1Component[]>([]);
37+
const [hasMore, setHasMore] = useState(true);
38+
const [cursor, setCursor] = useState<string | undefined>(undefined);
39+
const [isLoadingMore, setIsLoadingMore] = useState(false);
40+
41+
// Create stable query params object
42+
const queryParams = useCallback(() => ({
43+
...input,
44+
limit: 50,
45+
after: cursor,
46+
}), [input?.app, input?.componentType, cursor]);
47+
48+
const query = useQuery({
49+
queryKey: [
50+
"components-paginated",
51+
input?.app || "",
52+
input?.componentType || "",
53+
cursor || "",
54+
],
55+
queryFn: () => client.components(queryParams()),
56+
staleTime: 60000, // Consider data fresh for 1 minute
57+
gcTime: 300000, // Keep in cache for 5 minutes
58+
});
59+
60+
// Handle successful data fetch with cleanup guard
61+
useEffect(() => {
62+
let cancelled = false;
63+
64+
if (query.isSuccess && query.data && !cancelled) {
65+
const data = query.data;
66+
67+
if (cursor) {
68+
// This is a "load more" request, append to existing components
69+
setAllComponents(prev => {
70+
if (cancelled) return prev;
71+
const existingKeys = new Set(prev.map(c => c.key));
72+
const newComponents = data.data?.filter((c: V1Component) => !existingKeys.has(c.key)) || [];
73+
return [...prev, ...newComponents];
74+
});
75+
} else {
76+
// This is initial load, replace all components
77+
if (!cancelled) {
78+
setAllComponents(data.data || []);
79+
}
80+
}
81+
82+
// Update pagination state
83+
if (!cancelled) {
84+
const pageInfo = data.page_info;
85+
setHasMore(pageInfo ? (pageInfo.count >= 50) : false);
86+
setIsLoadingMore(false);
87+
}
88+
}
89+
90+
if (query.isError && !cancelled) {
91+
setIsLoadingMore(false);
92+
}
93+
94+
return () => {
95+
cancelled = true;
96+
};
97+
}, [query.isSuccess, query.isError, query.data, cursor]);
98+
99+
// Load more function - don't depend on entire query object
100+
const loadMore = useCallback(() => {
101+
if (hasMore && !query.isFetching && !isLoadingMore && query.data?.page_info?.end_cursor) {
102+
setIsLoadingMore(true);
103+
setCursor(query.data.page_info.end_cursor);
104+
}
105+
}, [hasMore, query.isFetching, query.data?.page_info?.end_cursor, isLoadingMore]);
106+
107+
const reset = useCallback(() => {
108+
setAllComponents([]);
109+
setHasMore(true);
110+
setCursor(undefined);
111+
setIsLoadingMore(false);
112+
}, []);
113+
114+
// Reset when input changes (e.g., different app or componentType)
115+
useEffect(() => {
116+
let cancelled = false;
117+
118+
// Use a microtask to avoid state updates during render
119+
queueMicrotask(() => {
120+
if (!cancelled) {
121+
setAllComponents([]);
122+
setHasMore(true);
123+
setCursor(undefined);
124+
setIsLoadingMore(false);
125+
}
126+
});
127+
128+
return () => {
129+
cancelled = true;
130+
};
131+
}, [input?.app, input?.componentType]);
132+
133+
return {
134+
...query,
135+
components: allComponents,
136+
hasMore,
137+
loadMore,
138+
reset,
139+
isLoadingMore,
140+
};
141+
};

pnpm-lock.yaml

Lines changed: 8 additions & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)