diff --git a/apps/web/src/components/TemplateSearchPicker.tsx b/apps/web/src/components/TemplateSearchPicker.tsx new file mode 100644 index 00000000..6fe9e349 --- /dev/null +++ b/apps/web/src/components/TemplateSearchPicker.tsx @@ -0,0 +1,104 @@ +import {Input} from '@plunk/ui'; +import type {Template} from '@plunk/db'; +import type {PaginatedResponse} from '@plunk/types'; +import {Command, CommandGroup, CommandItem, CommandList} from '@plunk/ui'; +import {useCallback, useEffect, useRef, useState} from 'react'; +import useSWR from 'swr'; + +interface TemplateSearchPickerProps { + /** Currently selected template ID */ + value: string; + /** Display name for the pre-selected template (avoids a fetch just to show the name) */ + initialName?: string; + onChange: (id: string) => void; +} + +/** + * Inline combobox for picking a template. + * Fires a debounced server-side search (/templates?search=…&pageSize=20) + * so it works correctly regardless of how many templates exist. + */ +export function TemplateSearchPicker({value, initialName, onChange}: TemplateSearchPickerProps) { + const [query, setQuery] = useState(initialName ?? ''); + const [debouncedQuery, setDebouncedQuery] = useState(''); + const [open, setOpen] = useState(false); + const debounceRef = useRef | null>(null); + + // When the dialog re-opens with an existing selection, sync the display name + useEffect(() => { + setQuery(initialName ?? ''); + }, [initialName]); + + const handleInput = useCallback((e: React.ChangeEvent) => { + const val = e.target.value; + setQuery(val); + setOpen(true); + if (debounceRef.current) clearTimeout(debounceRef.current); + debounceRef.current = setTimeout(() => setDebouncedQuery(val), 300); + }, []); + + const {data, isLoading} = useSWR>( + open || debouncedQuery + ? `/templates?pageSize=20${debouncedQuery ? `&search=${encodeURIComponent(debouncedQuery)}` : ''}` + : null, + {revalidateOnFocus: false}, + ); + + // When closed, show the selected template's name rather than the raw query + const displayValue = open + ? query + : (value ? (data?.data.find(t => t.id === value)?.name ?? initialName ?? value) : ''); + + return ( +
+ { + setOpen(true); + setDebouncedQuery(query); + }} + onBlur={() => setTimeout(() => setOpen(false), 150)} + placeholder="Search templates…" + autoComplete="off" + /> + + {open && ( +
+ {isLoading ? ( +
Searching…
+ ) : !data?.data.length ? ( +
No templates found
+ ) : ( + + + + {data.data.map(t => ( + { + onChange(t.id); + setQuery(t.name); + setOpen(false); + }} + > + {t.name} + {t.type} + + ))} + + + + )} + {(data?.total ?? 0) > 20 && ( +
+ Showing 20 of {data!.total} — type to narrow results +
+ )} +
+ )} +
+ ); +} diff --git a/apps/web/src/pages/workflows/[id].tsx b/apps/web/src/pages/workflows/[id].tsx index 4288b89a..e9039abe 100644 --- a/apps/web/src/pages/workflows/[id].tsx +++ b/apps/web/src/pages/workflows/[id].tsx @@ -60,6 +60,7 @@ import {useEffect, useState} from 'react'; import {toast} from 'sonner'; import useSWR from 'swr'; import {WorkflowBuilder} from '../../components/WorkflowBuilder'; +import {TemplateSearchPicker} from '../../components/TemplateSearchPicker'; import {ReactFlowProvider} from '@xyflow/react'; import {WorkflowSchemas} from '@plunk/shared'; import dayjs from 'dayjs'; @@ -1004,7 +1005,7 @@ function AddStepDialog({open, onOpenChange, workflowId, onSuccess}: AddStepDialo const [isSubmitting, setIsSubmitting] = useState(false); - const {data: templatesData} = useSWR>('/templates?pageSize=100'); + // templates fetched on-demand by TemplateSearchPicker const {data: workflow} = useSWR(workflowId ? `/workflows/${workflowId}` : null); // Fetch available event names when dialog opens @@ -1340,21 +1341,7 @@ function AddStepDialog({open, onOpenChange, workflowId, onSuccess}: AddStepDialo - +

The email template to use for this step

@@ -2073,7 +2060,7 @@ function EditStepDialog({step, workflowId, open, onOpenChange, onSuccess}: EditS // EXIT fields const [exitReason, setExitReason] = useState(String(config?.reason || 'completed')); - const {data: templatesData} = useSWR>('/templates?pageSize=100'); + // templates fetched on-demand by TemplateSearchPicker const {data: workflow} = useSWR(workflowId ? `/workflows/${workflowId}` : null); // Fetch available event names when dialog opens @@ -2359,21 +2346,7 @@ function EditStepDialog({step, workflowId, open, onOpenChange, onSuccess}: EditS - +

The email template to use for this step