Files
simstudioai--sim/apps/sim/hooks/queries/workflow-search-replace.ts
T
wehub-resource-sync d25d482dc2
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:20:55 +08:00

664 lines
24 KiB
TypeScript

import { useMemo } from 'react'
import { useQueries, useQuery } from '@tanstack/react-query'
import { requestJson } from '@/lib/api/client/request'
import type { KnowledgeBaseData } from '@/lib/api/contracts/knowledge'
import {
type DiscoverMcpToolsResponse,
discoverMcpToolsContract,
type ListMcpServersResponse,
listMcpServersContract,
} from '@/lib/api/contracts/mcp'
import {
type GetTableResponse,
getTableContract,
type ListTablesResponse,
listTablesContract,
} from '@/lib/api/contracts/tables'
import {
type ListWorkspaceFilesResponse,
listWorkspaceFilesContract,
} from '@/lib/api/contracts/workspace-files'
import { createMcpToolId } from '@/lib/mcp/shared'
import type { Credential } from '@/lib/oauth'
import {
getWorkflowSearchMatchResourceGroupKey,
stableStringifyWorkflowSearchValue,
} from '@/lib/workflows/search-replace/resources'
import type {
WorkflowSearchMatch,
WorkflowSearchReplacementOption,
} from '@/lib/workflows/search-replace/types'
import { fetchKnowledgeBase, fetchKnowledgeBases } from '@/hooks/queries/kb/knowledge'
import {
fetchOAuthCredentialDetail,
fetchOAuthCredentials,
} from '@/hooks/queries/oauth/oauth-credentials'
import { getSelectorDefinition, loadAllSelectorOptions } from '@/hooks/selectors/registry'
import type { SelectorKey, SelectorOption } from '@/hooks/selectors/types'
export interface WorkflowSearchResolvedResource {
matchRawValue: string
resourceGroupKey?: string
label: string
resolved: boolean
inaccessible: boolean
}
export const workflowSearchReplaceKeys = {
all: ['workflow-search-replace'] as const,
resourceDetails: () => [...workflowSearchReplaceKeys.all, 'resource-detail'] as const,
oauthDetails: (workflowId?: string) =>
[...workflowSearchReplaceKeys.resourceDetails(), 'oauth', workflowId ?? ''] as const,
oauthDetail: (credentialId?: string, workflowId?: string) =>
[...workflowSearchReplaceKeys.oauthDetails(workflowId), credentialId ?? ''] as const,
replacementOptions: () => [...workflowSearchReplaceKeys.all, 'replacement-options'] as const,
oauthReplacementOptions: (providerId?: string, workspaceId?: string, workflowId?: string) =>
[
...workflowSearchReplaceKeys.replacementOptions(),
'oauth',
providerId ?? '',
workspaceId ?? '',
workflowId ?? '',
] as const,
knowledgeDetails: () => [...workflowSearchReplaceKeys.resourceDetails(), 'knowledge'] as const,
knowledgeDetail: (knowledgeBaseId?: string) =>
[...workflowSearchReplaceKeys.knowledgeDetails(), knowledgeBaseId ?? ''] as const,
tableDetails: () => [...workflowSearchReplaceKeys.resourceDetails(), 'table'] as const,
tableDetail: (workspaceId?: string, tableId?: string) =>
[...workflowSearchReplaceKeys.tableDetails(), workspaceId ?? '', tableId ?? ''] as const,
tableReplacementOptions: (workspaceId?: string) =>
[...workflowSearchReplaceKeys.replacementOptions(), 'table', workspaceId ?? ''] as const,
fileDetails: () => [...workflowSearchReplaceKeys.resourceDetails(), 'file'] as const,
fileListDetails: (workspaceId?: string) =>
[...workflowSearchReplaceKeys.fileDetails(), 'list', workspaceId ?? ''] as const,
fileReplacementOptions: (workspaceId?: string) =>
[...workflowSearchReplaceKeys.replacementOptions(), 'file', workspaceId ?? ''] as const,
mcpServerDetails: () => [...workflowSearchReplaceKeys.resourceDetails(), 'mcp-server'] as const,
mcpServerListDetails: (workspaceId?: string) =>
[...workflowSearchReplaceKeys.mcpServerDetails(), 'list', workspaceId ?? ''] as const,
mcpServerReplacementOptions: (workspaceId?: string) =>
[...workflowSearchReplaceKeys.replacementOptions(), 'mcp-server', workspaceId ?? ''] as const,
mcpToolDetails: () => [...workflowSearchReplaceKeys.resourceDetails(), 'mcp-tool'] as const,
mcpToolListDetails: (workspaceId?: string) =>
[...workflowSearchReplaceKeys.mcpToolDetails(), 'list', workspaceId ?? ''] as const,
mcpToolReplacementOptions: (workspaceId?: string) =>
[...workflowSearchReplaceKeys.replacementOptions(), 'mcp-tool', workspaceId ?? ''] as const,
knowledgeReplacementOptions: (workspaceId?: string) =>
[...workflowSearchReplaceKeys.replacementOptions(), 'knowledge', workspaceId ?? ''] as const,
selectorDetails: () => [...workflowSearchReplaceKeys.resourceDetails(), 'selector'] as const,
selectorDetail: (selectorKey?: string, contextKey?: string, value?: string) =>
[
...workflowSearchReplaceKeys.selectorDetails(),
selectorKey ?? '',
contextKey ?? '',
value ?? '',
] as const,
selectorReplacementOptions: (selectorKey?: string, contextKey?: string) =>
[
...workflowSearchReplaceKeys.replacementOptions(),
'selector',
selectorKey ?? '',
contextKey ?? '',
] as const,
}
export const WORKFLOW_SEARCH_OAUTH_DETAIL_STALE_TIME = 60 * 1000
export const WORKFLOW_SEARCH_KNOWLEDGE_DETAIL_STALE_TIME = 60 * 1000
export const WORKFLOW_SEARCH_TABLE_DETAIL_STALE_TIME = 60 * 1000
export const WORKFLOW_SEARCH_FILE_LIST_STALE_TIME = 60 * 1000
export const WORKFLOW_SEARCH_MCP_SERVER_LIST_STALE_TIME = 60 * 1000
export const WORKFLOW_SEARCH_MCP_TOOL_LIST_STALE_TIME = 60 * 1000
export const WORKFLOW_SEARCH_SELECTOR_DETAIL_STALE_TIME = 60 * 1000
export const WORKFLOW_SEARCH_OAUTH_REPLACEMENT_STALE_TIME = 60 * 1000
export const WORKFLOW_SEARCH_KNOWLEDGE_REPLACEMENT_STALE_TIME = 60 * 1000
export const WORKFLOW_SEARCH_TABLE_REPLACEMENT_STALE_TIME = 60 * 1000
export const WORKFLOW_SEARCH_FILE_REPLACEMENT_STALE_TIME = 60 * 1000
export const WORKFLOW_SEARCH_MCP_SERVER_REPLACEMENT_STALE_TIME = 60 * 1000
export const WORKFLOW_SEARCH_MCP_TOOL_REPLACEMENT_STALE_TIME = 60 * 1000
export const WORKFLOW_SEARCH_SELECTOR_REPLACEMENT_STALE_TIME = 60 * 1000
function uniqueMatches(
matches: WorkflowSearchMatch[],
kind: WorkflowSearchMatch['kind']
): WorkflowSearchMatch[] {
const seen = new Set<string>()
return matches.filter((match) => {
if (match.kind !== kind || !match.rawValue || seen.has(match.rawValue)) return false
seen.add(match.rawValue)
return true
})
}
function selectorContextKey(match: WorkflowSearchMatch): string {
return stableStringifyWorkflowSearchValue(match.resource?.selectorContext ?? {})
}
function uniqueSelectorDetailMatches(matches: WorkflowSearchMatch[]): WorkflowSearchMatch[] {
const seen = new Set<string>()
return matches.filter((match) => {
const selectorKey = match.resource?.selectorKey
if (!selectorKey || !match.rawValue) return false
const key = `${selectorKey}:${selectorContextKey(match)}:${match.rawValue}`
if (seen.has(key)) return false
seen.add(key)
return true
})
}
function uniqueSelectorOptionGroups(matches: WorkflowSearchMatch[]): WorkflowSearchMatch[] {
const seen = new Set<string>()
return matches.filter((match) => {
const selectorKey = match.resource?.selectorKey
if (!selectorKey) return false
const key = `${match.kind}:${selectorKey}:${selectorContextKey(match)}`
if (seen.has(key)) return false
seen.add(key)
return true
})
}
function uniqueResourceOptionGroups(
matches: WorkflowSearchMatch[],
kind: WorkflowSearchMatch['kind'],
predicate?: (match: WorkflowSearchMatch) => boolean
): WorkflowSearchMatch[] {
const seen = new Set<string>()
return matches.filter((match) => {
if (match.kind !== kind || predicate?.(match) === false) return false
const key = getWorkflowSearchMatchResourceGroupKey(match)
if (seen.has(key)) return false
seen.add(key)
return true
})
}
export function useWorkflowSearchOAuthCredentialDetails(
matches: WorkflowSearchMatch[],
workflowId?: string
) {
const oauthMatches = useMemo(() => uniqueMatches(matches, 'oauth-credential'), [matches])
return useQueries({
queries: oauthMatches.map((match) => ({
queryKey: workflowSearchReplaceKeys.oauthDetail(match.rawValue, workflowId),
queryFn: ({ signal }: { signal: AbortSignal }) =>
fetchOAuthCredentialDetail(match.rawValue, workflowId, signal),
enabled: Boolean(match.rawValue),
staleTime: WORKFLOW_SEARCH_OAUTH_DETAIL_STALE_TIME,
select: (credentials: Credential[]): WorkflowSearchResolvedResource => {
const credential = credentials[0]
return {
matchRawValue: match.rawValue,
resourceGroupKey: match.resource?.resourceGroupKey,
label: credential?.name ?? `OAuth credential ${match.rawValue.slice(0, 8)}`,
resolved: Boolean(credential?.name),
inaccessible: credentials.length === 0,
}
},
})),
})
}
export function useWorkflowSearchKnowledgeBaseDetails(matches: WorkflowSearchMatch[]) {
const knowledgeMatches = useMemo(() => uniqueMatches(matches, 'knowledge-base'), [matches])
return useQueries({
queries: knowledgeMatches.map((match) => ({
queryKey: workflowSearchReplaceKeys.knowledgeDetail(match.rawValue),
queryFn: ({ signal }: { signal: AbortSignal }) => fetchKnowledgeBase(match.rawValue, signal),
enabled: Boolean(match.rawValue),
staleTime: WORKFLOW_SEARCH_KNOWLEDGE_DETAIL_STALE_TIME,
select: (knowledgeBase: KnowledgeBaseData): WorkflowSearchResolvedResource => ({
matchRawValue: match.rawValue,
resourceGroupKey: match.resource?.resourceGroupKey,
label: knowledgeBase.name,
resolved: true,
inaccessible: false,
}),
})),
})
}
export function useWorkflowSearchTableDetails(
matches: WorkflowSearchMatch[],
workspaceId?: string
) {
const tableMatches = useMemo(() => uniqueMatches(matches, 'table'), [matches])
return useQueries({
queries: tableMatches.map((match) => ({
queryKey: workflowSearchReplaceKeys.tableDetail(workspaceId, match.rawValue),
queryFn: ({ signal }: { signal: AbortSignal }) =>
requestJson(getTableContract, {
params: { tableId: match.rawValue },
query: { workspaceId: workspaceId as string },
signal,
}),
enabled: Boolean(workspaceId && match.rawValue),
staleTime: WORKFLOW_SEARCH_TABLE_DETAIL_STALE_TIME,
select: (response: GetTableResponse): WorkflowSearchResolvedResource => ({
matchRawValue: match.rawValue,
resourceGroupKey: match.resource?.resourceGroupKey,
label: response.data.table.name,
resolved: true,
inaccessible: false,
}),
})),
})
}
export function useWorkflowSearchFileDetails(matches: WorkflowSearchMatch[], workspaceId?: string) {
const fileMatches = useMemo(
() =>
uniqueMatches(
matches.filter((match) => !match.resource?.selectorKey),
'file'
),
[matches]
)
const filesQuery = useQuery({
queryKey: workflowSearchReplaceKeys.fileListDetails(workspaceId),
queryFn: ({ signal }: { signal: AbortSignal }) =>
requestJson(listWorkspaceFilesContract, {
params: { id: workspaceId as string },
query: { scope: 'active' },
signal,
}),
enabled: Boolean(workspaceId && fileMatches.length > 0),
staleTime: WORKFLOW_SEARCH_FILE_LIST_STALE_TIME,
})
return useMemo(
() =>
fileMatches.map((match) => {
const file = filesQuery.data?.files.find((item) =>
[item.id, item.key, item.path, item.name].includes(match.rawValue)
)
return {
data: filesQuery.data
? {
matchRawValue: match.rawValue,
resourceGroupKey: match.resource?.resourceGroupKey,
label: file?.name ?? match.rawValue,
resolved: Boolean(file),
inaccessible: false,
}
: undefined,
}
}),
[fileMatches, filesQuery.data]
)
}
export function useWorkflowSearchMcpServerDetails(
matches: WorkflowSearchMatch[],
workspaceId?: string
) {
const serverMatches = useMemo(() => uniqueMatches(matches, 'mcp-server'), [matches])
const serversQuery = useQuery({
queryKey: workflowSearchReplaceKeys.mcpServerListDetails(workspaceId),
queryFn: ({ signal }: { signal: AbortSignal }) =>
requestJson(listMcpServersContract, {
query: { workspaceId: workspaceId as string },
signal,
}),
enabled: Boolean(workspaceId && serverMatches.length > 0),
staleTime: WORKFLOW_SEARCH_MCP_SERVER_LIST_STALE_TIME,
})
return useMemo(
() =>
serverMatches.map((match) => {
const server = serversQuery.data?.data.servers.find((item) => item.id === match.rawValue)
return {
data: serversQuery.data
? {
matchRawValue: match.rawValue,
resourceGroupKey: match.resource?.resourceGroupKey,
label: server?.name ?? match.rawValue,
resolved: Boolean(server),
inaccessible: false,
}
: undefined,
}
}),
[serverMatches, serversQuery.data]
)
}
export function useWorkflowSearchMcpToolDetails(
matches: WorkflowSearchMatch[],
workspaceId?: string
) {
const toolMatches = useMemo(() => uniqueMatches(matches, 'mcp-tool'), [matches])
const toolsQuery = useQuery({
queryKey: workflowSearchReplaceKeys.mcpToolListDetails(workspaceId),
queryFn: ({ signal }: { signal: AbortSignal }) =>
requestJson(discoverMcpToolsContract, {
query: { workspaceId: workspaceId as string },
signal,
}),
enabled: Boolean(workspaceId && toolMatches.length > 0),
staleTime: WORKFLOW_SEARCH_MCP_TOOL_LIST_STALE_TIME,
})
return useMemo(
() =>
toolMatches.map((match) => {
const tool = toolsQuery.data?.data.tools.find(
(item) => createMcpToolId(item.serverId, item.name) === match.rawValue
)
return {
data: toolsQuery.data
? {
matchRawValue: match.rawValue,
resourceGroupKey: match.resource?.resourceGroupKey,
label: tool ? `${tool.serverName}: ${tool.name}` : match.rawValue,
resolved: Boolean(tool),
inaccessible: false,
}
: undefined,
}
}),
[toolMatches, toolsQuery.data]
)
}
export function useWorkflowSearchSelectorDetails(matches: WorkflowSearchMatch[]) {
const selectorMatches = useMemo(() => uniqueSelectorDetailMatches(matches), [matches])
return useQueries({
queries: selectorMatches.map((match) => {
const selectorKey = match.resource?.selectorKey as SelectorKey
const context = match.resource?.selectorContext ?? {}
const contextKey = selectorContextKey(match)
const definition = getSelectorDefinition(selectorKey)
const queryArgs = { key: selectorKey, context, detailId: match.rawValue }
const baseEnabled = definition.enabled ? definition.enabled(queryArgs) : true
return {
queryKey: workflowSearchReplaceKeys.selectorDetail(selectorKey, contextKey, match.rawValue),
queryFn: async ({ signal }: { signal: AbortSignal }): Promise<SelectorOption | null> => {
if (definition.fetchById) {
return definition.fetchById({ ...queryArgs, signal })
}
const options = await loadAllSelectorOptions(definition, {
key: selectorKey,
context,
signal,
})
return options.find((option) => option.id === match.rawValue) ?? null
},
enabled: Boolean(selectorKey && match.rawValue && baseEnabled),
staleTime: definition.staleTime ?? WORKFLOW_SEARCH_SELECTOR_DETAIL_STALE_TIME,
select: (option: SelectorOption | null): WorkflowSearchResolvedResource => ({
matchRawValue: match.rawValue,
resourceGroupKey: match.resource?.resourceGroupKey,
label: option?.label ?? match.rawValue,
resolved: Boolean(option),
inaccessible: false,
}),
}
}),
})
}
export function useWorkflowSearchOAuthReplacementOptions(
matches: WorkflowSearchMatch[],
workspaceId?: string,
workflowId?: string
) {
const providerIds = useMemo(() => {
const ids = new Set<string>()
matches.forEach((match) => {
if (match.kind === 'oauth-credential' && match.resource?.providerId) {
ids.add(match.resource.providerId)
}
})
return [...ids].sort()
}, [matches])
return useQueries({
queries: providerIds.map((providerId) => ({
queryKey: workflowSearchReplaceKeys.oauthReplacementOptions(
providerId,
workspaceId,
workflowId
),
queryFn: ({ signal }: { signal: AbortSignal }) =>
fetchOAuthCredentials({ providerId, workspaceId, workflowId }, signal),
enabled: Boolean(providerId && workspaceId),
staleTime: WORKFLOW_SEARCH_OAUTH_REPLACEMENT_STALE_TIME,
select: (credentials: Credential[]): WorkflowSearchReplacementOption[] =>
credentials.map((credential) => ({
kind: 'oauth-credential',
value: credential.id,
label: credential.name,
providerId,
serviceId: providerId,
})),
})),
})
}
export function useWorkflowSearchKnowledgeReplacementOptions(
matches: WorkflowSearchMatch[],
workspaceId?: string
) {
const knowledgeGroups = useMemo(
() => uniqueResourceOptionGroups(matches, 'knowledge-base'),
[matches]
)
return useQueries({
queries: [
{
queryKey: workflowSearchReplaceKeys.knowledgeReplacementOptions(workspaceId),
queryFn: ({ signal }: { signal: AbortSignal }) =>
fetchKnowledgeBases(workspaceId, 'active', signal),
enabled: Boolean(workspaceId && knowledgeGroups.length > 0),
staleTime: WORKFLOW_SEARCH_KNOWLEDGE_REPLACEMENT_STALE_TIME,
placeholderData: (previous: KnowledgeBaseData[] | undefined) => previous,
select: (knowledgeBases: KnowledgeBaseData[]): WorkflowSearchReplacementOption[] =>
knowledgeGroups.flatMap((match) =>
knowledgeBases.map((knowledgeBase) => ({
kind: 'knowledge-base',
value: knowledgeBase.id,
label: knowledgeBase.name,
resourceGroupKey: match.resource?.resourceGroupKey,
}))
),
},
],
})
}
export function useWorkflowSearchTableReplacementOptions(
matches: WorkflowSearchMatch[],
workspaceId?: string
) {
const tableGroups = useMemo(() => uniqueResourceOptionGroups(matches, 'table'), [matches])
return useQueries({
queries: [
{
queryKey: workflowSearchReplaceKeys.tableReplacementOptions(workspaceId),
queryFn: ({ signal }: { signal: AbortSignal }) =>
requestJson(listTablesContract, {
query: { workspaceId: workspaceId as string, scope: 'active' },
signal,
}),
enabled: Boolean(workspaceId && tableGroups.length > 0),
staleTime: WORKFLOW_SEARCH_TABLE_REPLACEMENT_STALE_TIME,
select: (response: ListTablesResponse): WorkflowSearchReplacementOption[] =>
tableGroups.flatMap((match) =>
response.data.tables.map((table) => ({
kind: 'table',
value: table.id,
label: table.name,
resourceGroupKey: match.resource?.resourceGroupKey,
}))
),
},
],
})
}
export function useWorkflowSearchFileReplacementOptions(
matches: WorkflowSearchMatch[],
workspaceId?: string
) {
const fileGroups = useMemo(
() => uniqueResourceOptionGroups(matches, 'file', (match) => !match.resource?.selectorKey),
[matches]
)
return useQueries({
queries: [
{
queryKey: workflowSearchReplaceKeys.fileReplacementOptions(workspaceId),
queryFn: ({ signal }: { signal: AbortSignal }) =>
requestJson(listWorkspaceFilesContract, {
params: { id: workspaceId as string },
query: { scope: 'active' },
signal,
}),
enabled: Boolean(workspaceId && fileGroups.length > 0),
staleTime: WORKFLOW_SEARCH_FILE_REPLACEMENT_STALE_TIME,
select: (response: ListWorkspaceFilesResponse): WorkflowSearchReplacementOption[] =>
fileGroups.flatMap((match) =>
response.files.map((file) => ({
kind: 'file',
value: JSON.stringify({
name: file.name,
path: file.path,
key: file.key,
size: file.size,
type: file.type,
}),
label: file.name,
resourceGroupKey: match.resource?.resourceGroupKey,
}))
),
},
],
})
}
export function useWorkflowSearchMcpServerReplacementOptions(
matches: WorkflowSearchMatch[],
workspaceId?: string
) {
const serverGroups = useMemo(() => uniqueResourceOptionGroups(matches, 'mcp-server'), [matches])
return useQueries({
queries: [
{
queryKey: workflowSearchReplaceKeys.mcpServerReplacementOptions(workspaceId),
queryFn: ({ signal }: { signal: AbortSignal }) =>
requestJson(listMcpServersContract, {
query: { workspaceId: workspaceId as string },
signal,
}),
enabled: Boolean(workspaceId && serverGroups.length > 0),
staleTime: WORKFLOW_SEARCH_MCP_SERVER_REPLACEMENT_STALE_TIME,
select: (response: ListMcpServersResponse): WorkflowSearchReplacementOption[] =>
serverGroups.flatMap((match) =>
response.data.servers.map((server) => ({
kind: 'mcp-server',
value: server.id,
label: server.name,
resourceGroupKey: match.resource?.resourceGroupKey,
}))
),
},
],
})
}
export function buildWorkflowSearchMcpToolReplacementOptions(
toolGroups: WorkflowSearchMatch[],
tools: DiscoverMcpToolsResponse['data']['tools']
): WorkflowSearchReplacementOption[] {
return toolGroups.flatMap((match) => {
const serverId = match.resource?.selectorContext?.mcpServerId
return tools
.filter((tool) => !serverId || tool.serverId === serverId)
.map((tool) => ({
kind: 'mcp-tool',
value: createMcpToolId(tool.serverId, tool.name),
label: `${tool.serverName}: ${tool.name}`,
resourceGroupKey: match.resource?.resourceGroupKey,
}))
})
}
export function useWorkflowSearchMcpToolReplacementOptions(
matches: WorkflowSearchMatch[],
workspaceId?: string
) {
const toolGroups = useMemo(() => uniqueResourceOptionGroups(matches, 'mcp-tool'), [matches])
return useQueries({
queries: [
{
queryKey: workflowSearchReplaceKeys.mcpToolReplacementOptions(workspaceId),
queryFn: ({ signal }: { signal: AbortSignal }) =>
requestJson(discoverMcpToolsContract, {
query: { workspaceId: workspaceId as string },
signal,
}),
enabled: Boolean(workspaceId && toolGroups.length > 0),
staleTime: WORKFLOW_SEARCH_MCP_TOOL_REPLACEMENT_STALE_TIME,
select: (response: DiscoverMcpToolsResponse): WorkflowSearchReplacementOption[] =>
buildWorkflowSearchMcpToolReplacementOptions(toolGroups, response.data.tools),
},
],
})
}
export function useWorkflowSearchSelectorReplacementOptions(matches: WorkflowSearchMatch[]) {
const selectorGroups = useMemo(() => uniqueSelectorOptionGroups(matches), [matches])
return useQueries({
queries: selectorGroups.map((match) => {
const selectorKey = match.resource?.selectorKey as SelectorKey
const context = match.resource?.selectorContext ?? {}
const contextKey = selectorContextKey(match)
const definition = getSelectorDefinition(selectorKey)
const queryArgs = { key: selectorKey, context }
const baseEnabled = definition.enabled ? definition.enabled(queryArgs) : true
return {
queryKey: workflowSearchReplaceKeys.selectorReplacementOptions(selectorKey, contextKey),
queryFn: ({ signal }: { signal: AbortSignal }) =>
loadAllSelectorOptions(definition, { ...queryArgs, signal }),
enabled: Boolean(selectorKey && baseEnabled),
staleTime: definition.staleTime ?? WORKFLOW_SEARCH_SELECTOR_REPLACEMENT_STALE_TIME,
select: (options: SelectorOption[]): WorkflowSearchReplacementOption[] =>
options.map((option) => ({
kind: match.kind,
value: option.id,
label: option.label,
selectorKey,
selectorContext: context,
resourceGroupKey: match.resource?.resourceGroupKey,
})),
}
}),
})
}
export function flattenWorkflowSearchReplacementOptions(
optionGroups: Array<{ data?: WorkflowSearchReplacementOption[] }>
): WorkflowSearchReplacementOption[] {
return optionGroups.flatMap((group) => group.data ?? [])
}