mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
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:
co-authored by
Claude Opus 4.7
parent
6c755cbb59
commit
d7d58cec36
+9
-1
@@ -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>
|
||||
|
||||
+1
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user