From 50461fac03cd902ba0edeedad1c0919845859e75 Mon Sep 17 00:00:00 2001 From: waleed Date: Sat, 30 May 2026 18:10:12 -0700 Subject: [PATCH 1/7] fix(search-replace): don't auto-navigate when content edits invalidate the active match --- .../workflow-search-replace.tsx | 107 +++++++++++------- 1 file changed, 66 insertions(+), 41 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/search-replace/workflow-search-replace.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/search-replace/workflow-search-replace.tsx index db04fadf60..d88510e8de 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/search-replace/workflow-search-replace.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/search-replace/workflow-search-replace.tsx @@ -169,6 +169,9 @@ export function WorkflowSearchReplace() { setActiveMatchId: state.setActiveMatchId, })) ) + const prevQueryRef = useRef(query) + const prevIsOpenRef = useRef(false) + const afterReplaceIndexRef = useRef(null) const { data: workspaceCredentials } = useWorkspaceCredentials({ workspaceId, enabled: isOpen }) useRegisterGlobalCommands([ @@ -311,10 +314,7 @@ export function WorkflowSearchReplace() { return [] }, [activeMatch, hydratedMatches]) - const eligibleMatchIds = useMemo( - () => replaceAllTargetMatches.map((match) => match.id), - [replaceAllTargetMatches] - ) + const eligibleMatchIds = replaceAllTargetMatches.map((match) => match.id) const controlTargetMatches = activeMatch ? [activeMatch] : [] const usesResourceReplacement = controlTargetMatches.some(isConstrainedResourceMatch) const resourceReplacementContextKey = @@ -324,23 +324,20 @@ export function WorkflowSearchReplace() { const replacement = resourceReplacementContextKey ? (resourceReplacementByContext[resourceReplacementContextKey] ?? '') : textReplacement - const handleReplacementChange = useCallback( - (nextReplacement: string) => { - if (!resourceReplacementContextKey) { - setReplacement(nextReplacement) - return - } + const handleReplacementChange = (nextReplacement: string) => { + if (!resourceReplacementContextKey) { + setReplacement(nextReplacement) + return + } - setResourceReplacementByContext((current) => ({ - ...current, - [resourceReplacementContextKey]: nextReplacement, - })) - }, - [resourceReplacementContextKey, setReplacement] - ) - const compatibleResourceOptions = useMemo( - () => getCompatibleResourceReplacementOptions(controlTargetMatches, resourceOptions), - [controlTargetMatches, resourceOptions] + setResourceReplacementByContext((current) => ({ + ...current, + [resourceReplacementContextKey]: nextReplacement, + })) + } + const compatibleResourceOptions = getCompatibleResourceReplacementOptions( + controlTargetMatches, + resourceOptions ) const hasReplacement = replacement.trim().length > 0 const activeReplacementIssue = activeMatch @@ -359,25 +356,31 @@ export function WorkflowSearchReplace() { }) : 'No replaceable matches.' - const applySubflowUpdate = useCallback( - (update: WorkflowSearchReplaceSubflowUpdate) => { - if (update.fieldId === WORKFLOW_SEARCH_SUBFLOW_FIELD_IDS.iterations) { - if (typeof update.nextValue !== 'number') return - collaborativeUpdateIterationCount(update.blockId, update.blockType, update.nextValue) - return - } + const applySubflowUpdate = (update: WorkflowSearchReplaceSubflowUpdate) => { + if (update.fieldId === WORKFLOW_SEARCH_SUBFLOW_FIELD_IDS.iterations) { + if (typeof update.nextValue !== 'number') return + collaborativeUpdateIterationCount(update.blockId, update.blockType, update.nextValue) + return + } - collaborativeUpdateIterationCollection( - update.blockId, - update.blockType, - String(update.nextValue) - ) - }, - [collaborativeUpdateIterationCollection, collaborativeUpdateIterationCount] - ) + collaborativeUpdateIterationCollection( + update.blockId, + update.blockType, + String(update.nextValue) + ) + } useEffect(() => { - if (!isOpen) return + if (!isOpen) { + prevIsOpenRef.current = false + usePanelEditorSearchStore.getState().setActiveSearchTarget(null) + return + } + + const justOpened = !prevIsOpenRef.current + prevIsOpenRef.current = true + const queryChanged = prevQueryRef.current !== query + prevQueryRef.current = query if (hydratedMatches.length === 0) { if (activeMatchId) setActiveMatchId(null) @@ -386,7 +389,22 @@ export function WorkflowSearchReplace() { } if (!activeMatchId || !hydratedMatches.some((match) => match.id === activeMatchId)) { - handleSelectMatch(hydratedMatches[0].id) + const replaceIndex = afterReplaceIndexRef.current + afterReplaceIndexRef.current = null + + if (queryChanged || justOpened) { + // Intentional navigation: panel opened or query changed — go to first match. + handleSelectMatch(hydratedMatches[0].id) + } else if (replaceIndex !== null) { + // Replace button was clicked: advance to the match now at the same position (clamped), + // which is the next match after the one that was just replaced. + handleSelectMatch(hydratedMatches[Math.min(replaceIndex, hydratedMatches.length - 1)].id) + } else { + // Content edited manually — don't steal focus or switch panels. + // Deselect so the user can navigate explicitly with the arrow keys. + setActiveMatchId(null) + usePanelEditorSearchStore.getState().setActiveSearchTarget(null) + } return } @@ -401,8 +419,12 @@ export function WorkflowSearchReplace() { const handleMoveActiveMatch = (delta: number) => { if (hydratedMatches.length === 0) return - const currentIndex = activeMatchIndex >= 0 ? activeMatchIndex : 0 - const nextIndex = (currentIndex + delta + hydratedMatches.length) % hydratedMatches.length + if (activeMatchIndex < 0) { + // Nothing selected: ↓ goes to first match, ↑ goes to last. + handleSelectMatch(hydratedMatches[delta > 0 ? 0 : hydratedMatches.length - 1].id) + return + } + const nextIndex = (activeMatchIndex + delta + hydratedMatches.length) % hydratedMatches.length handleSelectMatch(hydratedMatches[nextIndex].id) } @@ -512,6 +534,7 @@ export function WorkflowSearchReplace() { const handleReplaceActive = () => { if (!activeMatch) return + afterReplaceIndexRef.current = activeMatchIndex handleApply([activeMatch.id]) } @@ -522,7 +545,9 @@ export function WorkflowSearchReplace() { const matchCountLabel = hydratedMatches.length === 0 ? 'No results' - : `${activeMatchIndex >= 0 ? activeMatchIndex + 1 : 1} of ${hydratedMatches.length}` + : activeMatchIndex >= 0 + ? `${activeMatchIndex + 1} of ${hydratedMatches.length}` + : `0 of ${hydratedMatches.length}` return (
From d23279ddcaf4eefee964dccd9d5d101b157a3822 Mon Sep 17 00:00:00 2001 From: waleed Date: Sat, 30 May 2026 18:15:23 -0700 Subject: [PATCH 2/7] fix(search-replace): clear afterReplaceIndexRef on apply failure and zero matches --- .../components/search-replace/workflow-search-replace.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/search-replace/workflow-search-replace.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/search-replace/workflow-search-replace.tsx index d88510e8de..23055bc40f 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/search-replace/workflow-search-replace.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/search-replace/workflow-search-replace.tsx @@ -383,6 +383,7 @@ export function WorkflowSearchReplace() { prevQueryRef.current = query if (hydratedMatches.length === 0) { + afterReplaceIndexRef.current = null if (activeMatchId) setActiveMatchId(null) usePanelEditorSearchStore.getState().setActiveSearchTarget(null) return @@ -431,6 +432,7 @@ export function WorkflowSearchReplace() { const handleApply = (matchIds: string[]) => { if (!workflowId || isApplying || searchReadOnly) return setIsApplying(true) + let committed = false try { const selectedIds = new Set(matchIds) @@ -527,8 +529,10 @@ export function WorkflowSearchReplace() { message: `Replaced ${replacedCount} field${replacedCount === 1 ? '' : 's'}.`, workflowId, }) + committed = true } finally { setIsApplying(false) + if (!committed) afterReplaceIndexRef.current = null } } From 3aeb9dc7e0c7bf67e4c144cfa9391715f81c4578 Mon Sep 17 00:00:00 2001 From: waleed Date: Sat, 30 May 2026 18:16:50 -0700 Subject: [PATCH 3/7] fix(search-replace): remove duplicate setActiveSearchTarget(null) on close --- .../components/search-replace/workflow-search-replace.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/search-replace/workflow-search-replace.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/search-replace/workflow-search-replace.tsx index 23055bc40f..743b7d527b 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/search-replace/workflow-search-replace.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/search-replace/workflow-search-replace.tsx @@ -251,10 +251,7 @@ export function WorkflowSearchReplace() { ) useEffect(() => { - if (!isOpen) { - usePanelEditorSearchStore.getState().setActiveSearchTarget(null) - return - } + if (!isOpen) return searchInputRef.current?.focus() searchInputRef.current?.select() }, [isOpen]) From 3475fe40bd97b7636f798d08a5071728902f1fc7 Mon Sep 17 00:00:00 2001 From: waleed Date: Sat, 30 May 2026 18:23:01 -0700 Subject: [PATCH 4/7] fix(search-replace): move afterReplaceIndexRef write inside handleApply past the guard --- .../components/search-replace/workflow-search-replace.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/search-replace/workflow-search-replace.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/search-replace/workflow-search-replace.tsx index 743b7d527b..52131d87d2 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/search-replace/workflow-search-replace.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/search-replace/workflow-search-replace.tsx @@ -426,8 +426,9 @@ export function WorkflowSearchReplace() { handleSelectMatch(hydratedMatches[nextIndex].id) } - const handleApply = (matchIds: string[]) => { + const handleApply = (matchIds: string[], replaceActiveIndex?: number) => { if (!workflowId || isApplying || searchReadOnly) return + if (replaceActiveIndex !== undefined) afterReplaceIndexRef.current = replaceActiveIndex setIsApplying(true) let committed = false @@ -535,8 +536,7 @@ export function WorkflowSearchReplace() { const handleReplaceActive = () => { if (!activeMatch) return - afterReplaceIndexRef.current = activeMatchIndex - handleApply([activeMatch.id]) + handleApply([activeMatch.id], activeMatchIndex) } const handleReplaceAll = () => { From 2c46622937d7cc97ed05f8e91ed12ac18a3324a9 Mon Sep 17 00:00:00 2001 From: waleed Date: Sat, 30 May 2026 18:44:32 -0700 Subject: [PATCH 5/7] fix(search-replace): auto-navigate when hydration resolves with no prior active match --- .../components/search-replace/workflow-search-replace.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/search-replace/workflow-search-replace.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/search-replace/workflow-search-replace.tsx index 52131d87d2..331760197f 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/search-replace/workflow-search-replace.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/search-replace/workflow-search-replace.tsx @@ -390,8 +390,9 @@ export function WorkflowSearchReplace() { const replaceIndex = afterReplaceIndexRef.current afterReplaceIndexRef.current = null - if (queryChanged || justOpened) { - // Intentional navigation: panel opened or query changed — go to first match. + if (queryChanged || justOpened || !activeMatchId) { + // Intentional navigation: query changed, panel just opened, or nothing was + // ever selected (e.g. hydration resolved after open with no matches) — go to first match. handleSelectMatch(hydratedMatches[0].id) } else if (replaceIndex !== null) { // Replace button was clicked: advance to the match now at the same position (clamped), From 5e3ccbef75a0bd62ff798100732803ef597c2be0 Mon Sep 17 00:00:00 2001 From: waleed Date: Sat, 30 May 2026 18:45:29 -0700 Subject: [PATCH 6/7] chore(search-replace): remove inline comments --- .../components/search-replace/workflow-search-replace.tsx | 7 ------- 1 file changed, 7 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/search-replace/workflow-search-replace.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/search-replace/workflow-search-replace.tsx index 331760197f..238e532110 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/search-replace/workflow-search-replace.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/search-replace/workflow-search-replace.tsx @@ -391,16 +391,10 @@ export function WorkflowSearchReplace() { afterReplaceIndexRef.current = null if (queryChanged || justOpened || !activeMatchId) { - // Intentional navigation: query changed, panel just opened, or nothing was - // ever selected (e.g. hydration resolved after open with no matches) — go to first match. handleSelectMatch(hydratedMatches[0].id) } else if (replaceIndex !== null) { - // Replace button was clicked: advance to the match now at the same position (clamped), - // which is the next match after the one that was just replaced. handleSelectMatch(hydratedMatches[Math.min(replaceIndex, hydratedMatches.length - 1)].id) } else { - // Content edited manually — don't steal focus or switch panels. - // Deselect so the user can navigate explicitly with the arrow keys. setActiveMatchId(null) usePanelEditorSearchStore.getState().setActiveSearchTarget(null) } @@ -419,7 +413,6 @@ export function WorkflowSearchReplace() { const handleMoveActiveMatch = (delta: number) => { if (hydratedMatches.length === 0) return if (activeMatchIndex < 0) { - // Nothing selected: ↓ goes to first match, ↑ goes to last. handleSelectMatch(hydratedMatches[delta > 0 ? 0 : hydratedMatches.length - 1].id) return } From 680fa11ac5d0fcafde4f8836a3d1c546bab5d218 Mon Sep 17 00:00:00 2001 From: waleed Date: Sat, 30 May 2026 18:50:13 -0700 Subject: [PATCH 7/7] fix(search-replace): revert !activeMatchId guard that caused immediate re-navigation after deselect --- .../components/search-replace/workflow-search-replace.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/search-replace/workflow-search-replace.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/search-replace/workflow-search-replace.tsx index 238e532110..81e15ac22c 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/search-replace/workflow-search-replace.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/search-replace/workflow-search-replace.tsx @@ -390,7 +390,7 @@ export function WorkflowSearchReplace() { const replaceIndex = afterReplaceIndexRef.current afterReplaceIndexRef.current = null - if (queryChanged || justOpened || !activeMatchId) { + if (queryChanged || justOpened) { handleSelectMatch(hydratedMatches[0].id) } else if (replaceIndex !== null) { handleSelectMatch(hydratedMatches[Math.min(replaceIndex, hydratedMatches.length - 1)].id)