diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/editor.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/editor.tsx index ac65eab57fa..2c531a80a83 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/editor.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/editor.tsx @@ -32,6 +32,7 @@ import { SubBlock, SubflowEditor, } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components' +import { WORKFLOW_SEARCH_HIGHLIGHT_CLASS } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/constants' import { useBlockConnections, useConnectionsResize, @@ -104,6 +105,8 @@ export function Editor() { const currentBlock = currentBlockId ? currentWorkflow.getBlockById(currentBlockId) : null const blockConfig = currentBlock ? getBlock(currentBlock.type) : null const title = currentBlock?.name || 'Editor' + const isBlockNameSearchHighlighted = + activeSearchTarget?.targetKind === 'block-name' && activeSearchTarget.blockId === currentBlockId const isSubflow = currentBlock && (currentBlock.type === 'loop' || currentBlock.type === 'parallel') @@ -253,6 +256,7 @@ export function Editor() { useEffect(() => { if (!activeSearchTarget || activeSearchTarget.blockId !== currentBlockId) return + if (activeSearchTarget.targetKind === 'block-name') return const container = subBlocksRef.current if (!container) return @@ -402,7 +406,11 @@ export function Editor() { } }} > - {title} + {isBlockNameSearchHighlighted ? ( + {title} + ) : ( + title + )} )} 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 aad657f5643..d67fcf896ea 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 @@ -265,6 +265,7 @@ export function WorkflowSearchReplace() { canonicalSubBlockId: match.canonicalSubBlockId, valuePath: match.valuePath, kind: match.kind, + targetKind: match.target.kind, resourceGroupKey: match.resource?.resourceGroupKey, }) }, diff --git a/apps/sim/lib/workflows/search-replace/indexer.test.ts b/apps/sim/lib/workflows/search-replace/indexer.test.ts index 53975b65ad5..31e45f3715e 100644 --- a/apps/sim/lib/workflows/search-replace/indexer.test.ts +++ b/apps/sim/lib/workflows/search-replace/indexer.test.ts @@ -31,6 +31,39 @@ describe('indexWorkflowSearchMatches', () => { expect(matches.at(-1)?.reason).toBe('Block is locked') }) + it('finds matches in block names', () => { + const workflow = createSearchReplaceWorkflowFixture() + + const matches = indexWorkflowSearchMatches({ + workflow, + query: 'agent', + mode: 'text', + blockConfigs: SEARCH_REPLACE_BLOCK_CONFIGS, + }) + + const blockNameMatches = matches.filter((match) => match.target.kind === 'block-name') + expect(blockNameMatches.map((match) => [match.blockId, match.rawValue])).toEqual([ + ['agent-1', 'Agent'], + ['locked-1', 'Agent'], + ]) + expect(blockNameMatches.every((match) => match.editable === false)).toBe(true) + expect(blockNameMatches.every((match) => match.navigable === true)).toBe(true) + expect(blockNameMatches[0]?.fieldTitle).toBe('Block name') + }) + + it('does not include block-name matches in resource-only mode', () => { + const workflow = createSearchReplaceWorkflowFixture() + + const matches = indexWorkflowSearchMatches({ + workflow, + query: 'agent', + mode: 'resource', + blockConfigs: SEARCH_REPLACE_BLOCK_CONFIGS, + }) + + expect(matches.some((match) => match.target.kind === 'block-name')).toBe(false) + }) + it('does not index internal row metadata in structured subblock values', () => { const workflow = createSearchReplaceWorkflowFixture() diff --git a/apps/sim/lib/workflows/search-replace/indexer.ts b/apps/sim/lib/workflows/search-replace/indexer.ts index 198b5c6567f..a839bd39008 100644 --- a/apps/sim/lib/workflows/search-replace/indexer.ts +++ b/apps/sim/lib/workflows/search-replace/indexer.ts @@ -982,6 +982,32 @@ export function indexWorkflowSearchMatches( const protectedByLock = isWorkflowBlockProtected(block.id, workflow.blocks) const editable = !protectedByLock && !isReadOnly + if (mode !== 'resource' && query && typeof block.name === 'string' && block.name.length > 0) { + const blockNameRanges = findTextRanges(block.name, query, caseSensitive) + blockNameRanges.forEach((range, occurrenceIndex) => { + matches.push({ + id: createMatchId(['block-name', block.id, range.start, occurrenceIndex]), + blockId: block.id, + blockName: block.name, + blockType: block.type, + subBlockId: '', + canonicalSubBlockId: '', + subBlockType: 'short-input', + fieldTitle: 'Block name', + valuePath: [], + target: { kind: 'block-name' }, + kind: 'text', + rawValue: block.name.slice(range.start, range.end), + searchText: block.name, + range, + editable: false, + navigable: true, + protected: protectedByLock, + reason: 'Block names cannot be edited via replace', + }) + }) + } + if (mode !== 'resource') { for (const field of getWorkflowSearchSubflowFields(block)) { const fieldEditable = editable && field.editable diff --git a/apps/sim/lib/workflows/search-replace/types.ts b/apps/sim/lib/workflows/search-replace/types.ts index 835cf43d71d..9298430318d 100644 --- a/apps/sim/lib/workflows/search-replace/types.ts +++ b/apps/sim/lib/workflows/search-replace/types.ts @@ -45,6 +45,7 @@ export interface WorkflowSearchResourceMeta { export type WorkflowSearchTarget = | { kind: 'subblock' } | { kind: 'subflow'; fieldId: WorkflowSearchSubflowFieldId } + | { kind: 'block-name' } export interface WorkflowSearchMatch { id: string diff --git a/apps/sim/stores/panel/editor/store.ts b/apps/sim/stores/panel/editor/store.ts index d76019befea..9a0f6d1e8fd 100644 --- a/apps/sim/stores/panel/editor/store.ts +++ b/apps/sim/stores/panel/editor/store.ts @@ -2,11 +2,14 @@ import { create } from 'zustand' import { persist } from 'zustand/middleware' +import type { WorkflowSearchTarget } from '@/lib/workflows/search-replace/types' import { EDITOR_CONNECTIONS_HEIGHT } from '@/stores/constants' import { usePanelStore } from '../store' let renameCallback: (() => void) | null = null +export type ActiveSearchTargetKind = WorkflowSearchTarget['kind'] + export interface ActiveSearchTarget { matchId: string blockId: string @@ -14,6 +17,7 @@ export interface ActiveSearchTarget { canonicalSubBlockId: string valuePath: Array kind: string + targetKind: ActiveSearchTargetKind resourceGroupKey?: string }