Skip to content

Commit 50461fa

Browse files
committed
fix(search-replace): don't auto-navigate when content edits invalidate the active match
1 parent 1d4a277 commit 50461fa

1 file changed

Lines changed: 66 additions & 41 deletions

File tree

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/search-replace/workflow-search-replace.tsx

Lines changed: 66 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,9 @@ export function WorkflowSearchReplace() {
169169
setActiveMatchId: state.setActiveMatchId,
170170
}))
171171
)
172+
const prevQueryRef = useRef(query)
173+
const prevIsOpenRef = useRef(false)
174+
const afterReplaceIndexRef = useRef<number | null>(null)
172175
const { data: workspaceCredentials } = useWorkspaceCredentials({ workspaceId, enabled: isOpen })
173176

174177
useRegisterGlobalCommands([
@@ -311,10 +314,7 @@ export function WorkflowSearchReplace() {
311314

312315
return []
313316
}, [activeMatch, hydratedMatches])
314-
const eligibleMatchIds = useMemo(
315-
() => replaceAllTargetMatches.map((match) => match.id),
316-
[replaceAllTargetMatches]
317-
)
317+
const eligibleMatchIds = replaceAllTargetMatches.map((match) => match.id)
318318
const controlTargetMatches = activeMatch ? [activeMatch] : []
319319
const usesResourceReplacement = controlTargetMatches.some(isConstrainedResourceMatch)
320320
const resourceReplacementContextKey =
@@ -324,23 +324,20 @@ export function WorkflowSearchReplace() {
324324
const replacement = resourceReplacementContextKey
325325
? (resourceReplacementByContext[resourceReplacementContextKey] ?? '')
326326
: textReplacement
327-
const handleReplacementChange = useCallback(
328-
(nextReplacement: string) => {
329-
if (!resourceReplacementContextKey) {
330-
setReplacement(nextReplacement)
331-
return
332-
}
327+
const handleReplacementChange = (nextReplacement: string) => {
328+
if (!resourceReplacementContextKey) {
329+
setReplacement(nextReplacement)
330+
return
331+
}
333332

334-
setResourceReplacementByContext((current) => ({
335-
...current,
336-
[resourceReplacementContextKey]: nextReplacement,
337-
}))
338-
},
339-
[resourceReplacementContextKey, setReplacement]
340-
)
341-
const compatibleResourceOptions = useMemo(
342-
() => getCompatibleResourceReplacementOptions(controlTargetMatches, resourceOptions),
343-
[controlTargetMatches, resourceOptions]
333+
setResourceReplacementByContext((current) => ({
334+
...current,
335+
[resourceReplacementContextKey]: nextReplacement,
336+
}))
337+
}
338+
const compatibleResourceOptions = getCompatibleResourceReplacementOptions(
339+
controlTargetMatches,
340+
resourceOptions
344341
)
345342
const hasReplacement = replacement.trim().length > 0
346343
const activeReplacementIssue = activeMatch
@@ -359,25 +356,31 @@ export function WorkflowSearchReplace() {
359356
})
360357
: 'No replaceable matches.'
361358

362-
const applySubflowUpdate = useCallback(
363-
(update: WorkflowSearchReplaceSubflowUpdate) => {
364-
if (update.fieldId === WORKFLOW_SEARCH_SUBFLOW_FIELD_IDS.iterations) {
365-
if (typeof update.nextValue !== 'number') return
366-
collaborativeUpdateIterationCount(update.blockId, update.blockType, update.nextValue)
367-
return
368-
}
359+
const applySubflowUpdate = (update: WorkflowSearchReplaceSubflowUpdate) => {
360+
if (update.fieldId === WORKFLOW_SEARCH_SUBFLOW_FIELD_IDS.iterations) {
361+
if (typeof update.nextValue !== 'number') return
362+
collaborativeUpdateIterationCount(update.blockId, update.blockType, update.nextValue)
363+
return
364+
}
369365

370-
collaborativeUpdateIterationCollection(
371-
update.blockId,
372-
update.blockType,
373-
String(update.nextValue)
374-
)
375-
},
376-
[collaborativeUpdateIterationCollection, collaborativeUpdateIterationCount]
377-
)
366+
collaborativeUpdateIterationCollection(
367+
update.blockId,
368+
update.blockType,
369+
String(update.nextValue)
370+
)
371+
}
378372

379373
useEffect(() => {
380-
if (!isOpen) return
374+
if (!isOpen) {
375+
prevIsOpenRef.current = false
376+
usePanelEditorSearchStore.getState().setActiveSearchTarget(null)
377+
return
378+
}
379+
380+
const justOpened = !prevIsOpenRef.current
381+
prevIsOpenRef.current = true
382+
const queryChanged = prevQueryRef.current !== query
383+
prevQueryRef.current = query
381384

382385
if (hydratedMatches.length === 0) {
383386
if (activeMatchId) setActiveMatchId(null)
@@ -386,7 +389,22 @@ export function WorkflowSearchReplace() {
386389
}
387390

388391
if (!activeMatchId || !hydratedMatches.some((match) => match.id === activeMatchId)) {
389-
handleSelectMatch(hydratedMatches[0].id)
392+
const replaceIndex = afterReplaceIndexRef.current
393+
afterReplaceIndexRef.current = null
394+
395+
if (queryChanged || justOpened) {
396+
// Intentional navigation: panel opened or query changed — go to first match.
397+
handleSelectMatch(hydratedMatches[0].id)
398+
} else if (replaceIndex !== null) {
399+
// Replace button was clicked: advance to the match now at the same position (clamped),
400+
// which is the next match after the one that was just replaced.
401+
handleSelectMatch(hydratedMatches[Math.min(replaceIndex, hydratedMatches.length - 1)].id)
402+
} else {
403+
// Content edited manually — don't steal focus or switch panels.
404+
// Deselect so the user can navigate explicitly with the arrow keys.
405+
setActiveMatchId(null)
406+
usePanelEditorSearchStore.getState().setActiveSearchTarget(null)
407+
}
390408
return
391409
}
392410

@@ -401,8 +419,12 @@ export function WorkflowSearchReplace() {
401419

402420
const handleMoveActiveMatch = (delta: number) => {
403421
if (hydratedMatches.length === 0) return
404-
const currentIndex = activeMatchIndex >= 0 ? activeMatchIndex : 0
405-
const nextIndex = (currentIndex + delta + hydratedMatches.length) % hydratedMatches.length
422+
if (activeMatchIndex < 0) {
423+
// Nothing selected: ↓ goes to first match, ↑ goes to last.
424+
handleSelectMatch(hydratedMatches[delta > 0 ? 0 : hydratedMatches.length - 1].id)
425+
return
426+
}
427+
const nextIndex = (activeMatchIndex + delta + hydratedMatches.length) % hydratedMatches.length
406428
handleSelectMatch(hydratedMatches[nextIndex].id)
407429
}
408430

@@ -512,6 +534,7 @@ export function WorkflowSearchReplace() {
512534

513535
const handleReplaceActive = () => {
514536
if (!activeMatch) return
537+
afterReplaceIndexRef.current = activeMatchIndex
515538
handleApply([activeMatch.id])
516539
}
517540

@@ -522,7 +545,9 @@ export function WorkflowSearchReplace() {
522545
const matchCountLabel =
523546
hydratedMatches.length === 0
524547
? 'No results'
525-
: `${activeMatchIndex >= 0 ? activeMatchIndex + 1 : 1} of ${hydratedMatches.length}`
548+
: activeMatchIndex >= 0
549+
? `${activeMatchIndex + 1} of ${hydratedMatches.length}`
550+
: `0 of ${hydratedMatches.length}`
526551
return (
527552
<div
528553
role='dialog'
@@ -566,7 +591,7 @@ export function WorkflowSearchReplace() {
566591
>
567592
<ChevronRight
568593
className={cn(
569-
'h-[14px] w-[14px] text-[var(--text-icon)] transition-transform',
594+
'size-[14px] text-[var(--text-icon)] transition-transform',
570595
isReplaceExpanded && 'rotate-90'
571596
)}
572597
/>

0 commit comments

Comments
 (0)