11'use client'
22
33import { useCallback , useEffect , useMemo , useRef , useState } from 'react'
4- import { ChevronDown , ChevronRight , ChevronUp , Search , X } from 'lucide-react'
4+ import { ChevronDown , ChevronRight , ChevronUp , X } from 'lucide-react'
55import { useParams } from 'next/navigation'
66import { Button , Input } from '@/components/emcn'
77import { getWorkflowSearchDependentClears } from '@/lib/workflows/search-replace/dependencies'
@@ -17,6 +17,8 @@ import {
1717 workflowSearchMatchMatchesQuery ,
1818} from '@/lib/workflows/search-replace/resource-resolvers'
1919import { getWorkflowSearchBlocks } from '@/lib/workflows/search-replace/state'
20+ import { WORKFLOW_SEARCH_SUBFLOW_FIELD_IDS } from '@/lib/workflows/search-replace/subflow-fields'
21+ import type { WorkflowSearchReplaceSubflowUpdate } from '@/lib/workflows/search-replace/types'
2022import { useRegisterGlobalCommands } from '@/app/workspace/[workspaceId]/providers/global-commands-provider'
2123import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
2224import { createCommand } from '@/app/workspace/[workspaceId]/utils/commands-utils'
@@ -29,6 +31,9 @@ import {
2931} from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/float'
3032import { useCurrentWorkflow } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-current-workflow'
3133import { getBlock } from '@/blocks'
34+ import { useFolderMap } from '@/hooks/queries/folders'
35+ import { isWorkflowEffectivelyLocked } from '@/hooks/queries/utils/folder-tree'
36+ import { useWorkflowMap } from '@/hooks/queries/workflows'
3237import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow'
3338import { useNotificationStore } from '@/stores/notifications/store'
3439import { usePanelEditorStore } from '@/stores/panel'
@@ -37,8 +42,8 @@ import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
3742import { useSubBlockStore } from '@/stores/workflows/subblock/store'
3843
3944const SEARCH_PANEL_WIDTH = 360
40- const SEARCH_PANEL_COLLAPSED_HEIGHT = 104
41- const SEARCH_PANEL_EXPANDED_HEIGHT = 190
45+ const SEARCH_PANEL_COLLAPSED_HEIGHT = 82
46+ const SEARCH_PANEL_EXPANDED_HEIGHT = 156
4247
4348function getDefaultSearchPanelPosition ( ) {
4449 if ( typeof window === 'undefined' ) return { x : 100 , y : 100 }
@@ -83,9 +88,23 @@ export function WorkflowSearchReplace() {
8388 const workflowSubblockValues = useSubBlockStore ( ( state ) =>
8489 workflowId ? state . workflowValues [ workflowId ] : undefined
8590 )
91+ const { data : workflows = { } } = useWorkflowMap ( workspaceId )
92+ const { data : folders = { } } = useFolderMap ( workspaceId )
93+ const workflowMetadata = workflowId ? workflows [ workflowId ] : undefined
94+ const workflowLocked = isWorkflowEffectivelyLocked ( workflowMetadata , folders )
95+ const searchReadOnly = currentWorkflow . isSnapshotView || workflowLocked
96+ const readonlyReason = currentWorkflow . isSnapshotView
97+ ? 'Snapshot view is readonly'
98+ : workflowLocked
99+ ? 'Workflow is locked'
100+ : undefined
86101 const userPermissions = useUserPermissionsContext ( )
87102 const addNotification = useNotificationStore ( ( state ) => state . addNotification )
88- const { collaborativeBatchSetSubblockValues } = useCollaborativeWorkflow ( )
103+ const {
104+ collaborativeBatchSetSubblockValues,
105+ collaborativeUpdateIterationCollection,
106+ collaborativeUpdateIterationCount,
107+ } = useCollaborativeWorkflow ( )
89108 const searchInputRef = useRef < HTMLInputElement > ( null )
90109 const [ isApplying , setIsApplying ] = useState ( false )
91110 const [ isReplaceExpanded , setIsReplaceExpanded ] = useState ( false )
@@ -135,10 +154,20 @@ export function WorkflowSearchReplace() {
135154 mode : 'all' ,
136155 includeResourceMatchesWithoutQuery : true ,
137156 isSnapshotView : currentWorkflow . isSnapshotView ,
157+ isReadOnly : searchReadOnly ,
158+ readonlyReason,
138159 workspaceId,
139160 workflowId,
140161 } ) ,
141- [ currentWorkflow . isSnapshotView , query , searchBlocks , workspaceId , workflowId ]
162+ [
163+ currentWorkflow . isSnapshotView ,
164+ query ,
165+ readonlyReason ,
166+ searchBlocks ,
167+ searchReadOnly ,
168+ workspaceId ,
169+ workflowId ,
170+ ]
142171 )
143172
144173 const allHydratedMatches = useWorkflowSearchReferenceHydration ( {
@@ -158,7 +187,10 @@ export function WorkflowSearchReplace() {
158187 )
159188
160189 useEffect ( ( ) => {
161- if ( ! isOpen ) return
190+ if ( ! isOpen ) {
191+ usePanelEditorStore . getState ( ) . setActiveSearchTarget ( null )
192+ return
193+ }
162194 searchInputRef . current ?. focus ( )
163195 searchInputRef . current ?. select ( )
164196 } , [ isOpen ] )
@@ -212,8 +244,17 @@ export function WorkflowSearchReplace() {
212244 if ( isConstrainedResourceMatch ( activeMatch ) ) {
213245 return getWorkflowSearchCompatibleResourceMatches ( activeMatch , hydratedMatches )
214246 }
247+ if ( activeMatch . kind === 'workflow-reference' ) {
248+ return hydratedMatches . filter (
249+ ( match ) => match . kind === 'workflow-reference' && match . editable
250+ )
251+ }
252+
253+ if ( activeMatch . kind === 'text' ) {
254+ return hydratedMatches . filter ( ( match ) => match . kind === 'text' && match . editable )
255+ }
215256
216- return hydratedMatches . filter ( ( match ) => match . kind === 'text' && match . editable )
257+ return [ ]
217258 } , [ activeMatch , hydratedMatches ] )
218259 const eligibleMatchIds = useMemo (
219260 ( ) => replaceAllTargetMatches . map ( ( match ) => match . id ) ,
@@ -241,6 +282,24 @@ export function WorkflowSearchReplace() {
241282 resourceOptions,
242283 } )
243284 : 'No replaceable matches.'
285+
286+ const applySubflowUpdate = useCallback (
287+ ( update : WorkflowSearchReplaceSubflowUpdate ) => {
288+ if ( update . fieldId === WORKFLOW_SEARCH_SUBFLOW_FIELD_IDS . iterations ) {
289+ if ( typeof update . nextValue !== 'number' ) return
290+ collaborativeUpdateIterationCount ( update . blockId , update . blockType , update . nextValue )
291+ return
292+ }
293+
294+ collaborativeUpdateIterationCollection (
295+ update . blockId ,
296+ update . blockType ,
297+ String ( update . nextValue )
298+ )
299+ } ,
300+ [ collaborativeUpdateIterationCollection , collaborativeUpdateIterationCount ]
301+ )
302+
244303 useEffect ( ( ) => {
245304 if ( ! isOpen ) return
246305
@@ -265,7 +324,7 @@ export function WorkflowSearchReplace() {
265324 }
266325
267326 const handleApply = ( matchIds : string [ ] ) => {
268- if ( ! workflowId || isApplying ) return
327+ if ( ! workflowId || isApplying || searchReadOnly ) return
269328 setIsApplying ( true )
270329
271330 try {
@@ -326,7 +385,7 @@ export function WorkflowSearchReplace() {
326385 }
327386 }
328387
329- if ( batchUpdates . length === 0 ) {
388+ if ( batchUpdates . length === 0 && plan . subflowUpdates . length === 0 ) {
330389 addNotification ( {
331390 level : 'info' ,
332391 message : 'No eligible matches to replace.' ,
@@ -335,7 +394,8 @@ export function WorkflowSearchReplace() {
335394 return
336395 }
337396
338- const applied = collaborativeBatchSetSubblockValues ( batchUpdates )
397+ const applied =
398+ batchUpdates . length === 0 ? true : collaborativeBatchSetSubblockValues ( batchUpdates )
339399 if ( ! applied ) {
340400 addNotification ( {
341401 level : 'error' ,
@@ -345,9 +405,14 @@ export function WorkflowSearchReplace() {
345405 return
346406 }
347407
408+ for ( const update of plan . subflowUpdates ) {
409+ applySubflowUpdate ( update )
410+ }
411+
412+ const replacedCount = plan . updates . length + plan . subflowUpdates . length
348413 addNotification ( {
349414 level : 'info' ,
350- message : `Replaced ${ plan . updates . length } field${ plan . updates . length === 1 ? '' : 's' } .` ,
415+ message : `Replaced ${ replacedCount } field${ replacedCount === 1 ? '' : 's' } .` ,
351416 workflowId,
352417 } )
353418 } finally {
@@ -382,8 +447,7 @@ export function WorkflowSearchReplace() {
382447 className = 'flex h-[32px] flex-shrink-0 cursor-grab items-center justify-between gap-2.5 bg-[var(--surface-1)] p-0 active:cursor-grabbing'
383448 onMouseDown = { handleMouseDown }
384449 >
385- < div className = 'flex min-w-0 items-center gap-2' >
386- < Search className = 'h-4 w-4 shrink-0' />
450+ < div className = 'flex min-w-0 items-center' >
387451 < span className = 'truncate font-medium text-[var(--text-primary)] text-sm' >
388452 Search and replace
389453 </ span >
@@ -392,7 +456,7 @@ export function WorkflowSearchReplace() {
392456 className = 'flex shrink-0 items-center gap-2'
393457 onMouseDown = { ( event ) => event . stopPropagation ( ) }
394458 >
395- < span className = 'text-muted-foreground text-xs' > { matchCountLabel } </ span >
459+ < span className = 'text-[var(--text-muted)] text-xs' > { matchCountLabel } </ span >
396460 < Button variant = 'ghost' className = '!p-1.5 -m-1.5' onClick = { close } >
397461 < X className = 'h-[16px] w-[16px]' />
398462 </ Button >
@@ -445,7 +509,7 @@ export function WorkflowSearchReplace() {
445509 compatibleResourceOptions = { compatibleResourceOptions }
446510 usesResourceReplacement = { usesResourceReplacement }
447511 eligibleCount = { eligibleMatchIds . length }
448- disabled = { ! userPermissions . canEdit || currentWorkflow . isSnapshotView }
512+ disabled = { ! userPermissions . canEdit || searchReadOnly }
449513 isApplying = { isApplying }
450514 canReplaceActive = { Boolean (
451515 activeMatch ?. editable && hasReplacement && ! activeReplacementIssue
0 commit comments