diff --git a/.server-changes/search-input-rerender.md b/.server-changes/search-input-rerender.md new file mode 100644 index 0000000000..aea6f07c98 --- /dev/null +++ b/.server-changes/search-input-rerender.md @@ -0,0 +1,6 @@ +--- +area: webapp +type: fix +--- + +Fix the task page search bar clearing or resetting while typing, caused by a re-render race between the input sync effect and the activity charts. diff --git a/apps/webapp/app/components/primitives/SearchInput.tsx b/apps/webapp/app/components/primitives/SearchInput.tsx index 6b24e015e5..ef1fdb8a49 100644 --- a/apps/webapp/app/components/primitives/SearchInput.tsx +++ b/apps/webapp/app/components/primitives/SearchInput.tsx @@ -47,18 +47,27 @@ export function SearchInput({ const [text, setText] = useState(initialSearch); const [isFocused, setIsFocused] = useState(false); + // Compare against a ref, not `text`, so the effect stays off the keystroke path. + // Trade-off: controlled mode assumes the parent accepts onValueChange; it won't + // re-sync `text` if the parent rejects a change and holds `value` unchanged. + const lastSyncedRef = useRef(initialSearch); + useEffect(() => { if (isControlled) { - if (controlledValue !== undefined && controlledValue !== text) { + if (controlledValue !== undefined && controlledValue !== lastSyncedRef.current) { + lastSyncedRef.current = controlledValue; setText(controlledValue); } return; } const urlSearch = value(paramName) ?? ""; - if (urlSearch !== text && !isFocused) { + if (urlSearch === lastSyncedRef.current) return; + // Only mark synced once we actually apply it, so a URL change during focus still syncs on blur. + if (!isFocused) { + lastSyncedRef.current = urlSearch; setText(urlSearch); } - }, [isControlled, controlledValue, value, text, isFocused, paramName]); + }, [isControlled, controlledValue, value, isFocused, paramName]); const updateText = (next: string) => { setText(next); diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam._index/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam._index/route.tsx index 6a48f32402..6bb6062e6a 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam._index/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam._index/route.tsx @@ -193,7 +193,7 @@ export default function Page() { }, [streamedEvents]); // eslint-disable-line react-hooks/exhaustive-deps const [showUsefulLinks, setShowUsefulLinks] = useState(usefulLinksPreference ?? true); - // Unmount the charts while the side panel animates; 25 SVGs in a reflowing table tanks perf. + // Hide (don't unmount) the charts during the panel animation; 25 reflowing SVGs tank the resize. const [isPanelAnimating, setIsPanelAnimating] = useState(false); const animatingTimerRef = useRef | null>(null); const usefulLinksPanelRef = useRef(null); @@ -436,24 +436,21 @@ function TaskRow({ - {/* Reserve the cell footprint while the chart unmounts during the panel animation. */}
- {!isPanelAnimating && ( -
- }> - }> - {(data) => { - const taskData = data[item.slug]; - return taskData && taskData.length > 0 ? ( - - ) : ( - - ); - }} - - -
- )} +