improvement(workflow-search): include block names in in-workflow search (#4668)

* improvement(workflow-search): include block names in in-workflow search

Adds block names to the workflow search index alongside subblock content. Selecting a block-name match navigates to the block and highlights its title in the editor header with the same orange treatment used for subblock labels.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* refactor(panel-store): derive ActiveSearchTargetKind from WorkflowSearchTarget

Replaces the hand-maintained literal union with a derived type so adding a new target kind in search-replace/types.ts automatically propagates here.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Waleed
2026-05-19 15:44:34 -07:00
committed by GitHub
co-authored by Claude Opus 4.7
parent 6c755cbb59
commit d7d58cec36
6 changed files with 74 additions and 1 deletions
@@ -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 ? (
<mark className={WORKFLOW_SEARCH_HIGHLIGHT_CLASS}>{title}</mark>
) : (
title
)}
</h2>
)}
</div>
@@ -265,6 +265,7 @@ export function WorkflowSearchReplace() {
canonicalSubBlockId: match.canonicalSubBlockId,
valuePath: match.valuePath,
kind: match.kind,
targetKind: match.target.kind,
resourceGroupKey: match.resource?.resourceGroupKey,
})
},
@@ -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()
@@ -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
@@ -45,6 +45,7 @@ export interface WorkflowSearchResourceMeta {
export type WorkflowSearchTarget =
| { kind: 'subblock' }
| { kind: 'subflow'; fieldId: WorkflowSearchSubflowFieldId }
| { kind: 'block-name' }
export interface WorkflowSearchMatch {
id: string
+4
View File
@@ -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<string | number>
kind: string
targetKind: ActiveSearchTargetKind
resourceGroupKey?: string
}