Skip to content

Commit fed57aa

Browse files
committed
canonical modes search replace counterpart
1 parent 917fe42 commit fed57aa

8 files changed

Lines changed: 375 additions & 45 deletions

File tree

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import {
3131
} from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/float'
3232
import { useCurrentWorkflow } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-current-workflow'
3333
import { getBlock } from '@/blocks'
34+
import { useWorkspaceCredentials } from '@/hooks/queries/credentials'
3435
import { useFolderMap } from '@/hooks/queries/folders'
3536
import { isWorkflowEffectivelyLocked } from '@/hooks/queries/utils/folder-tree'
3637
import { useWorkflowMap } from '@/hooks/queries/workflows'
@@ -125,6 +126,7 @@ export function WorkflowSearchReplace() {
125126
setReplacement,
126127
setActiveMatchId,
127128
} = useWorkflowSearchReplaceStore()
129+
const { data: workspaceCredentials } = useWorkspaceCredentials({ workspaceId, enabled: isOpen })
128130

129131
useRegisterGlobalCommands([
130132
createCommand({
@@ -149,6 +151,14 @@ export function WorkflowSearchReplace() {
149151
[currentWorkflow.blocks, currentWorkflow.isSnapshotView, workflowSubblockValues]
150152
)
151153

154+
const credentialTypeById = useMemo(
155+
() =>
156+
Object.fromEntries(
157+
(workspaceCredentials ?? []).map((credential) => [credential.id, credential.type])
158+
),
159+
[workspaceCredentials]
160+
)
161+
152162
const matches = useMemo(
153163
() =>
154164
indexWorkflowSearchMatches({
@@ -161,9 +171,11 @@ export function WorkflowSearchReplace() {
161171
readonlyReason,
162172
workspaceId,
163173
workflowId,
174+
credentialTypeById,
164175
}),
165176
[
166177
currentWorkflow.isSnapshotView,
178+
credentialTypeById,
167179
query,
168180
readonlyReason,
169181
searchBlocks,

apps/sim/hooks/queries/workflow-search-replace.ts

Lines changed: 67 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,10 @@ import {
2020
} from '@/lib/api/contracts/workspace-files'
2121
import { createMcpToolId } from '@/lib/mcp/shared'
2222
import type { Credential } from '@/lib/oauth'
23-
import { stableStringifyWorkflowSearchValue } from '@/lib/workflows/search-replace/resources'
23+
import {
24+
getWorkflowSearchMatchResourceGroupKey,
25+
stableStringifyWorkflowSearchValue,
26+
} from '@/lib/workflows/search-replace/resources'
2427
import type {
2528
WorkflowSearchMatch,
2629
WorkflowSearchReplacementOption,
@@ -151,6 +154,22 @@ function uniqueSelectorOptionGroups(matches: WorkflowSearchMatch[]): WorkflowSea
151154
})
152155
}
153156

157+
function uniqueResourceOptionGroups(
158+
matches: WorkflowSearchMatch[],
159+
kind: WorkflowSearchMatch['kind'],
160+
predicate?: (match: WorkflowSearchMatch) => boolean
161+
): WorkflowSearchMatch[] {
162+
const seen = new Set<string>()
163+
return matches.filter((match) => {
164+
if (match.kind !== kind || predicate?.(match) === false) return false
165+
166+
const key = getWorkflowSearchMatchResourceGroupKey(match)
167+
if (seen.has(key)) return false
168+
seen.add(key)
169+
return true
170+
})
171+
}
172+
154173
export function useWorkflowSearchOAuthCredentialDetails(
155174
matches: WorkflowSearchMatch[],
156175
workflowId?: string
@@ -445,7 +464,7 @@ export function useWorkflowSearchTableReplacementOptions(
445464
matches: WorkflowSearchMatch[],
446465
workspaceId?: string
447466
) {
448-
const tableMatch = useMemo(() => matches.find((match) => match.kind === 'table'), [matches])
467+
const tableGroups = useMemo(() => uniqueResourceOptionGroups(matches, 'table'), [matches])
449468

450469
return useQueries({
451470
queries: [
@@ -456,15 +475,17 @@ export function useWorkflowSearchTableReplacementOptions(
456475
query: { workspaceId: workspaceId as string, scope: 'active' },
457476
signal,
458477
}),
459-
enabled: Boolean(workspaceId && tableMatch),
478+
enabled: Boolean(workspaceId && tableGroups.length > 0),
460479
staleTime: 60 * 1000,
461480
select: (response: ListTablesResponse): WorkflowSearchReplacementOption[] =>
462-
response.data.tables.map((table) => ({
463-
kind: 'table',
464-
value: table.id,
465-
label: table.name,
466-
resourceGroupKey: tableMatch?.resource?.resourceGroupKey,
467-
})),
481+
tableGroups.flatMap((match) =>
482+
response.data.tables.map((table) => ({
483+
kind: 'table',
484+
value: table.id,
485+
label: table.name,
486+
resourceGroupKey: match.resource?.resourceGroupKey,
487+
}))
488+
),
468489
},
469490
],
470491
})
@@ -474,8 +495,8 @@ export function useWorkflowSearchFileReplacementOptions(
474495
matches: WorkflowSearchMatch[],
475496
workspaceId?: string
476497
) {
477-
const fileMatch = useMemo(
478-
() => matches.find((match) => match.kind === 'file' && !match.resource?.selectorKey),
498+
const fileGroups = useMemo(
499+
() => uniqueResourceOptionGroups(matches, 'file', (match) => !match.resource?.selectorKey),
479500
[matches]
480501
)
481502

@@ -489,21 +510,23 @@ export function useWorkflowSearchFileReplacementOptions(
489510
query: { scope: 'active' },
490511
signal,
491512
}),
492-
enabled: Boolean(workspaceId && fileMatch),
513+
enabled: Boolean(workspaceId && fileGroups.length > 0),
493514
staleTime: 60 * 1000,
494515
select: (response: ListWorkspaceFilesResponse): WorkflowSearchReplacementOption[] =>
495-
response.files.map((file) => ({
496-
kind: 'file',
497-
value: JSON.stringify({
498-
name: file.name,
499-
path: file.path,
500-
key: file.key,
501-
size: file.size,
502-
type: file.type,
503-
}),
504-
label: file.name,
505-
resourceGroupKey: fileMatch?.resource?.resourceGroupKey,
506-
})),
516+
fileGroups.flatMap((match) =>
517+
response.files.map((file) => ({
518+
kind: 'file',
519+
value: JSON.stringify({
520+
name: file.name,
521+
path: file.path,
522+
key: file.key,
523+
size: file.size,
524+
type: file.type,
525+
}),
526+
label: file.name,
527+
resourceGroupKey: match.resource?.resourceGroupKey,
528+
}))
529+
),
507530
},
508531
],
509532
})
@@ -513,7 +536,7 @@ export function useWorkflowSearchMcpServerReplacementOptions(
513536
matches: WorkflowSearchMatch[],
514537
workspaceId?: string
515538
) {
516-
const serverMatch = useMemo(() => matches.find((match) => match.kind === 'mcp-server'), [matches])
539+
const serverGroups = useMemo(() => uniqueResourceOptionGroups(matches, 'mcp-server'), [matches])
517540

518541
return useQueries({
519542
queries: [
@@ -524,15 +547,17 @@ export function useWorkflowSearchMcpServerReplacementOptions(
524547
query: { workspaceId: workspaceId as string },
525548
signal,
526549
}),
527-
enabled: Boolean(workspaceId && serverMatch),
550+
enabled: Boolean(workspaceId && serverGroups.length > 0),
528551
staleTime: 60 * 1000,
529552
select: (response: ListMcpServersResponse): WorkflowSearchReplacementOption[] =>
530-
response.data.servers.map((server) => ({
531-
kind: 'mcp-server',
532-
value: server.id,
533-
label: server.name,
534-
resourceGroupKey: serverMatch?.resource?.resourceGroupKey,
535-
})),
553+
serverGroups.flatMap((match) =>
554+
response.data.servers.map((server) => ({
555+
kind: 'mcp-server',
556+
value: server.id,
557+
label: server.name,
558+
resourceGroupKey: match.resource?.resourceGroupKey,
559+
}))
560+
),
536561
},
537562
],
538563
})
@@ -542,7 +567,7 @@ export function useWorkflowSearchMcpToolReplacementOptions(
542567
matches: WorkflowSearchMatch[],
543568
workspaceId?: string
544569
) {
545-
const toolMatch = useMemo(() => matches.find((match) => match.kind === 'mcp-tool'), [matches])
570+
const toolGroups = useMemo(() => uniqueResourceOptionGroups(matches, 'mcp-tool'), [matches])
546571

547572
return useQueries({
548573
queries: [
@@ -553,15 +578,17 @@ export function useWorkflowSearchMcpToolReplacementOptions(
553578
query: { workspaceId: workspaceId as string },
554579
signal,
555580
}),
556-
enabled: Boolean(workspaceId && toolMatch),
581+
enabled: Boolean(workspaceId && toolGroups.length > 0),
557582
staleTime: 60 * 1000,
558583
select: (response: DiscoverMcpToolsResponse): WorkflowSearchReplacementOption[] =>
559-
response.data.tools.map((tool) => ({
560-
kind: 'mcp-tool',
561-
value: createMcpToolId(tool.serverId, tool.name),
562-
label: `${tool.serverName}: ${tool.name}`,
563-
resourceGroupKey: toolMatch?.resource?.resourceGroupKey,
564-
})),
584+
toolGroups.flatMap((match) =>
585+
response.data.tools.map((tool) => ({
586+
kind: 'mcp-tool',
587+
value: createMcpToolId(tool.serverId, tool.name),
588+
label: `${tool.serverName}: ${tool.name}`,
589+
resourceGroupKey: match.resource?.resourceGroupKey,
590+
}))
591+
),
565592
},
566593
],
567594
})

apps/sim/hooks/use-collaborative-workflow.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
} from '@sim/realtime-protocol/constants'
1313
import { generateId } from '@sim/utils/id'
1414
import { useQueryClient } from '@tanstack/react-query'
15+
import { isEqual } from 'es-toolkit'
1516
import type { Edge } from 'reactflow'
1617
import { useShallow } from 'zustand/react/shallow'
1718
import { requestJson } from '@/lib/api/client/request'
@@ -1566,6 +1567,19 @@ export function useCollaborativeWorkflow() {
15661567
return false
15671568
}
15681569

1570+
const staleUpdate = updates.find((update) => {
1571+
if (!Object.hasOwn(update, 'expectedValue')) return false
1572+
const currentValue = useSubBlockStore.getState().getValue(update.blockId, update.subblockId)
1573+
return !isEqual(currentValue, update.expectedValue)
1574+
})
1575+
if (staleUpdate) {
1576+
logger.warn('Skipping batch subblock update because expected value changed', {
1577+
blockId: staleUpdate.blockId,
1578+
subblockId: staleUpdate.subblockId,
1579+
})
1580+
return false
1581+
}
1582+
15691583
if (updates.length > 0) {
15701584
updates.forEach((update) => {
15711585
useSubBlockStore.getState().setValue(update.blockId, update.subblockId, update.value)
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/**
2+
* @vitest-environment node
3+
*/
4+
import { describe, expect, it } from 'vitest'
5+
import { getWorkflowSearchDependentClears } from '@/lib/workflows/search-replace/dependencies'
6+
import type { SubBlockConfig } from '@/blocks/types'
7+
8+
describe('getWorkflowSearchDependentClears', () => {
9+
it('returns transitive dependents without cycling', () => {
10+
const subBlocks: SubBlockConfig[] = [
11+
{ id: 'credential', title: 'Credential', type: 'oauth-input' },
12+
{ id: 'project', title: 'Project', type: 'project-selector', dependsOn: ['credential'] },
13+
{ id: 'issue', title: 'Issue', type: 'file-selector', dependsOn: ['project'] },
14+
{ id: 'assignee', title: 'Assignee', type: 'user-selector', dependsOn: ['issue'] },
15+
{ id: 'unrelated', title: 'Unrelated', type: 'short-input' },
16+
]
17+
18+
expect(getWorkflowSearchDependentClears(subBlocks, 'credential')).toEqual([
19+
{ subBlockId: 'project', reason: 'project depends on credential' },
20+
{ subBlockId: 'issue', reason: 'issue depends on project' },
21+
{ subBlockId: 'assignee', reason: 'assignee depends on issue' },
22+
])
23+
})
24+
})

apps/sim/lib/workflows/search-replace/dependencies.ts

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,24 @@ export function getWorkflowSearchDependentClears(
1010
allSubBlocks: SubBlockConfig[],
1111
changedSubBlockId: string
1212
): DependentClear[] {
13-
return getSubBlocksDependingOnChange(allSubBlocks, changedSubBlockId).map((subBlock) => ({
14-
subBlockId: subBlock.id,
15-
reason: `${subBlock.id} depends on ${changedSubBlockId}`,
16-
}))
13+
const clears: DependentClear[] = []
14+
const visited = new Set<string>([changedSubBlockId])
15+
const queue = [changedSubBlockId]
16+
17+
while (queue.length > 0) {
18+
const currentSubBlockId = queue.shift()
19+
if (!currentSubBlockId) continue
20+
21+
for (const subBlock of getSubBlocksDependingOnChange(allSubBlocks, currentSubBlockId)) {
22+
if (!subBlock.id || visited.has(subBlock.id)) continue
23+
visited.add(subBlock.id)
24+
clears.push({
25+
subBlockId: subBlock.id,
26+
reason: `${subBlock.id} depends on ${currentSubBlockId}`,
27+
})
28+
queue.push(subBlock.id)
29+
}
30+
}
31+
32+
return clears
1733
}

0 commit comments

Comments
 (0)