mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
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
This commit is contained in:
+67
-47
@@ -169,6 +169,9 @@ export function WorkflowSearchReplace() {
|
||||
setActiveMatchId: state.setActiveMatchId,
|
||||
}))
|
||||
)
|
||||
const prevQueryRef = useRef(query)
|
||||
const prevIsOpenRef = useRef(false)
|
||||
const afterReplaceIndexRef = useRef<number | null>(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 (
|
||||
<div
|
||||
role='dialog'
|
||||
@@ -566,7 +586,7 @@ export function WorkflowSearchReplace() {
|
||||
>
|
||||
<ChevronRight
|
||||
className={cn(
|
||||
'h-[14px] w-[14px] text-[var(--text-icon)] transition-transform',
|
||||
'size-[14px] text-[var(--text-icon)] transition-transform',
|
||||
isReplaceExpanded && 'rotate-90'
|
||||
)}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user