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..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 @@ -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([ @@ -248,10 +251,7 @@ export function WorkflowSearchReplace() { ) useEffect(() => { - if (!isOpen) { - usePanelEditorSearchStore.getState().setActiveSearchTarget(null) - return - } + if (!isOpen) return searchInputRef.current?.focus() searchInputRef.current?.select() }, [isOpen]) @@ -311,10 +311,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 +321,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,34 +353,51 @@ 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) { + afterReplaceIndexRef.current = null if (activeMatchId) setActiveMatchId(null) usePanelEditorSearchStore.getState().setActiveSearchTarget(null) return } if (!activeMatchId || !hydratedMatches.some((match) => match.id === activeMatchId)) { - handleSelectMatch(hydratedMatches[0].id) + const replaceIndex = afterReplaceIndexRef.current + afterReplaceIndexRef.current = null + + if (queryChanged || justOpened) { + handleSelectMatch(hydratedMatches[0].id) + } else if (replaceIndex !== null) { + handleSelectMatch(hydratedMatches[Math.min(replaceIndex, hydratedMatches.length - 1)].id) + } else { + setActiveMatchId(null) + usePanelEditorSearchStore.getState().setActiveSearchTarget(null) + } return } @@ -401,14 +412,19 @@ 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) { + handleSelectMatch(hydratedMatches[delta > 0 ? 0 : hydratedMatches.length - 1].id) + return + } + const nextIndex = (activeMatchIndex + delta + hydratedMatches.length) % hydratedMatches.length 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 try { const selectedIds = new Set(matchIds) @@ -505,14 +521,16 @@ export function WorkflowSearchReplace() { message: `Replaced ${replacedCount} field${replacedCount === 1 ? '' : 's'}.`, workflowId, }) + committed = true } finally { setIsApplying(false) + if (!committed) afterReplaceIndexRef.current = null } } const handleReplaceActive = () => { if (!activeMatch) return - handleApply([activeMatch.id]) + handleApply([activeMatch.id], activeMatchIndex) } const handleReplaceAll = () => { @@ -522,7 +540,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 (