Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .server-changes/search-input-rerender.md
Original file line number Diff line number Diff line change
@@ -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.
15 changes: 12 additions & 3 deletions apps/webapp/app/components/primitives/SearchInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Comment thread
samejr marked this conversation as resolved.

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);
}
Comment thread
samejr marked this conversation as resolved.
}, [isControlled, controlledValue, value, text, isFocused, paramName]);
}, [isControlled, controlledValue, value, isFocused, paramName]);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Comment thread
samejr marked this conversation as resolved.

const updateText = (next: string) => {
setText(next);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<ReturnType<typeof setTimeout> | null>(null);
const usefulLinksPanelRef = useRef<PanelHandle>(null);
Expand Down Expand Up @@ -436,24 +436,21 @@ function TaskRow({
</Suspense>
</TableCell>
<TableCell to={rowPath} actionClassName="py-1.5">
{/* Reserve the cell footprint while the chart unmounts during the panel animation. */}
<div style={{ width: ACTIVITY_CELL_WIDTH, height: ACTIVITY_CHART_HEIGHT }}>
{!isPanelAnimating && (
<div className="duration-100 animate-in fade-in">
<Suspense fallback={<TaskActivityBlankState />}>
<TypedAwait resolve={hourlyActivity} errorElement={<FailedToLoadStats />}>
{(data) => {
const taskData = data[item.slug];
return taskData && taskData.length > 0 ? (
<TaskActivityGraph activity={taskData} />
) : (
<TaskActivityBlankState />
);
}}
</TypedAwait>
</Suspense>
</div>
)}
<div hidden={isPanelAnimating}>
<Suspense fallback={<TaskActivityBlankState />}>
<TypedAwait resolve={hourlyActivity} errorElement={<FailedToLoadStats />}>
{(data) => {
const taskData = data[item.slug];
return taskData && taskData.length > 0 ? (
<TaskActivityGraph activity={taskData} />
) : (
<TaskActivityBlankState />
);
}}
</TypedAwait>
</Suspense>
</div>
</div>
</TableCell>
<TableCellMenu
Expand Down