Skip to content

Commit 1ae1afb

Browse files
authored
fix(search-replace): don't auto-navigate when content edits invalidate the active match (#4819)
* fix(search-replace): don't auto-navigate when content edits invalidate the active match * fix(search-replace): clear afterReplaceIndexRef on apply failure and zero matches * fix(search-replace): remove duplicate setActiveSearchTarget(null) on close * fix(search-replace): move afterReplaceIndexRef write inside handleApply past the guard * fix(search-replace): auto-navigate when hydration resolves with no prior active match * chore(search-replace): remove inline comments * fix(search-replace): revert !activeMatchId guard that caused immediate re-navigation after deselect
1 parent 20a8d85 commit 1ae1afb

1 file changed

Lines changed: 67 additions & 47 deletions

File tree

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

Lines changed: 67 additions & 47 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([
@@ -248,10 +251,7 @@ export function WorkflowSearchReplace() {
248251
)
249252

250253
useEffect(() => {
251-
if (!isOpen) {
252-
usePanelEditorSearchStore.getState().setActiveSearchTarget(null)
253-
return
254-
}
254+
if (!isOpen) return
255255
searchInputRef.current?.focus()
256256
searchInputRef.current?.select()
257257
}, [isOpen])
@@ -311,10 +311,7 @@ export function WorkflowSearchReplace() {
311311

312312
return []
313313
}, [activeMatch, hydratedMatches])
314-
const eligibleMatchIds = useMemo(
315-
() => replaceAllTargetMatches.map((match) => match.id),
316-
[replaceAllTargetMatches]
317-
)
314+
const eligibleMatchIds = replaceAllTargetMatches.map((match) => match.id)
318315
const controlTargetMatches = activeMatch ? [activeMatch] : []
319316
const usesResourceReplacement = controlTargetMatches.some(isConstrainedResourceMatch)
320317
const resourceReplacementContextKey =
@@ -324,23 +321,20 @@ export function WorkflowSearchReplace() {
324321
const replacement = resourceReplacementContextKey
325322
? (resourceReplacementByContext[resourceReplacementContextKey] ?? '')
326323
: textReplacement
327-
const handleReplacementChange = useCallback(
328-
(nextReplacement: string) => {
329-
if (!resourceReplacementContextKey) {
330-
setReplacement(nextReplacement)
331-
return
332-
}
324+
const handleReplacementChange = (nextReplacement: string) => {
325+
if (!resourceReplacementContextKey) {
326+
setReplacement(nextReplacement)
327+
return
328+
}
333329

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]
330+
setResourceReplacementByContext((current) => ({
331+
...current,
332+
[resourceReplacementContextKey]: nextReplacement,
333+
}))
334+
}
335+
const compatibleResourceOptions = getCompatibleResourceReplacementOptions(
336+
controlTargetMatches,
337+
resourceOptions
344338
)
345339
const hasReplacement = replacement.trim().length > 0
346340
const activeReplacementIssue = activeMatch
@@ -359,34 +353,51 @@ export function WorkflowSearchReplace() {
359353
})
360354
: 'No replaceable matches.'
361355

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-
}
356+
const applySubflowUpdate = (update: WorkflowSearchReplaceSubflowUpdate) => {
357+
if (update.fieldId === WORKFLOW_SEARCH_SUBFLOW_FIELD_IDS.iterations) {
358+
if (typeof update.nextValue !== 'number') return
359+
collaborativeUpdateIterationCount(update.blockId, update.blockType, update.nextValue)
360+
return
361+
}
369362

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

379370
useEffect(() => {
380-
if (!isOpen) return
371+
if (!isOpen) {
372+
prevIsOpenRef.current = false
373+
usePanelEditorSearchStore.getState().setActiveSearchTarget(null)
374+
return
375+
}
376+
377+
const justOpened = !prevIsOpenRef.current
378+
prevIsOpenRef.current = true
379+
const queryChanged = prevQueryRef.current !== query
380+
prevQueryRef.current = query
381381

382382
if (hydratedMatches.length === 0) {
383+
afterReplaceIndexRef.current = null
383384
if (activeMatchId) setActiveMatchId(null)
384385
usePanelEditorSearchStore.getState().setActiveSearchTarget(null)
385386
return
386387
}
387388

388389
if (!activeMatchId || !hydratedMatches.some((match) => match.id === activeMatchId)) {
389-
handleSelectMatch(hydratedMatches[0].id)
390+
const replaceIndex = afterReplaceIndexRef.current
391+
afterReplaceIndexRef.current = null
392+
393+
if (queryChanged || justOpened) {
394+
handleSelectMatch(hydratedMatches[0].id)
395+
} else if (replaceIndex !== null) {
396+
handleSelectMatch(hydratedMatches[Math.min(replaceIndex, hydratedMatches.length - 1)].id)
397+
} else {
398+
setActiveMatchId(null)
399+
usePanelEditorSearchStore.getState().setActiveSearchTarget(null)
400+
}
390401
return
391402
}
392403

@@ -401,14 +412,19 @@ export function WorkflowSearchReplace() {
401412

402413
const handleMoveActiveMatch = (delta: number) => {
403414
if (hydratedMatches.length === 0) return
404-
const currentIndex = activeMatchIndex >= 0 ? activeMatchIndex : 0
405-
const nextIndex = (currentIndex + delta + hydratedMatches.length) % hydratedMatches.length
415+
if (activeMatchIndex < 0) {
416+
handleSelectMatch(hydratedMatches[delta > 0 ? 0 : hydratedMatches.length - 1].id)
417+
return
418+
}
419+
const nextIndex = (activeMatchIndex + delta + hydratedMatches.length) % hydratedMatches.length
406420
handleSelectMatch(hydratedMatches[nextIndex].id)
407421
}
408422

409-
const handleApply = (matchIds: string[]) => {
423+
const handleApply = (matchIds: string[], replaceActiveIndex?: number) => {
410424
if (!workflowId || isApplying || searchReadOnly) return
425+
if (replaceActiveIndex !== undefined) afterReplaceIndexRef.current = replaceActiveIndex
411426
setIsApplying(true)
427+
let committed = false
412428

413429
try {
414430
const selectedIds = new Set(matchIds)
@@ -505,14 +521,16 @@ export function WorkflowSearchReplace() {
505521
message: `Replaced ${replacedCount} field${replacedCount === 1 ? '' : 's'}.`,
506522
workflowId,
507523
})
524+
committed = true
508525
} finally {
509526
setIsApplying(false)
527+
if (!committed) afterReplaceIndexRef.current = null
510528
}
511529
}
512530

513531
const handleReplaceActive = () => {
514532
if (!activeMatch) return
515-
handleApply([activeMatch.id])
533+
handleApply([activeMatch.id], activeMatchIndex)
516534
}
517535

518536
const handleReplaceAll = () => {
@@ -522,7 +540,9 @@ export function WorkflowSearchReplace() {
522540
const matchCountLabel =
523541
hydratedMatches.length === 0
524542
? 'No results'
525-
: `${activeMatchIndex >= 0 ? activeMatchIndex + 1 : 1} of ${hydratedMatches.length}`
543+
: activeMatchIndex >= 0
544+
? `${activeMatchIndex + 1} of ${hydratedMatches.length}`
545+
: `0 of ${hydratedMatches.length}`
526546
return (
527547
<div
528548
role='dialog'
@@ -566,7 +586,7 @@ export function WorkflowSearchReplace() {
566586
>
567587
<ChevronRight
568588
className={cn(
569-
'h-[14px] w-[14px] text-[var(--text-icon)] transition-transform',
589+
'size-[14px] text-[var(--text-icon)] transition-transform',
570590
isReplaceExpanded && 'rotate-90'
571591
)}
572592
/>

0 commit comments

Comments
 (0)