From a981a8bba468bbc3268812653597e1f306a3a7f9 Mon Sep 17 00:00:00 2001 From: Doug Beatty <123423698+wcatbb@users.noreply.github.com> Date: Sun, 12 Apr 2026 00:12:56 -0500 Subject: [PATCH 1/2] Improve template template loading and debounced search --- apps/web/src/pages/workflows/[id].tsx | 125 +++++++++++++++++++------- 1 file changed, 92 insertions(+), 33 deletions(-) diff --git a/apps/web/src/pages/workflows/[id].tsx b/apps/web/src/pages/workflows/[id].tsx index 4288b89a..b3c38e8e 100644 --- a/apps/web/src/pages/workflows/[id].tsx +++ b/apps/web/src/pages/workflows/[id].tsx @@ -56,7 +56,7 @@ import { } from 'lucide-react'; import Link from 'next/link'; import {useRouter} from 'next/router'; -import {useEffect, useState} from 'react'; +import {useCallback, useEffect, useRef, useState} from 'react'; import {toast} from 'sonner'; import useSWR from 'swr'; import {WorkflowBuilder} from '../../components/WorkflowBuilder'; @@ -1004,7 +1004,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 +1340,7 @@ function AddStepDialog({open, onOpenChange, workflowId, onSuccess}: AddStepDialo - +

The email template to use for this step

@@ -1872,6 +1858,93 @@ function AddStepDialog({open, onOpenChange, workflowId, onSuccess}: AddStepDialo ); } +// TemplateSearchPicker — server-side debounced search, replaces the static { 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 +
+ )} +
+ )} + + ); +} + // Edit Step Dialog Component interface EditStepDialogProps { step: WorkflowStep & {template?: {id: string; name: string} | null}; @@ -2073,7 +2146,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 +2432,7 @@ function EditStepDialog({step, workflowId, open, onOpenChange, onSuccess}: EditS - +

The email template to use for this step

From df18979e5e814f7196e9ef3d84425d30cc5201ad Mon Sep 17 00:00:00 2001 From: Doug Beatty <123423698+wcatbb@users.noreply.github.com> Date: Sun, 12 Apr 2026 10:14:34 -0500 Subject: [PATCH 2/2] extract TemplateSearchPicker to fix workflow template pagination bug --- .../src/components/TemplateSearchPicker.tsx | 104 ++++++++++++++++++ apps/web/src/pages/workflows/[id].tsx | 90 +-------------- 2 files changed, 106 insertions(+), 88 deletions(-) create mode 100644 apps/web/src/components/TemplateSearchPicker.tsx 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 b3c38e8e..e9039abe 100644 --- a/apps/web/src/pages/workflows/[id].tsx +++ b/apps/web/src/pages/workflows/[id].tsx @@ -56,10 +56,11 @@ import { } from 'lucide-react'; import Link from 'next/link'; import {useRouter} from 'next/router'; -import {useCallback, useEffect, useRef, useState} from 'react'; +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'; @@ -1858,93 +1859,6 @@ function AddStepDialog({open, onOpenChange, workflowId, onSuccess}: AddStepDialo ); } -// TemplateSearchPicker — server-side debounced search, replaces the static { 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 -
- )} -
- )} - - ); -} - // Edit Step Dialog Component interface EditStepDialogProps { step: WorkflowStep & {template?: {id: string; name: string} | null};