Skip to content

Commit 3ada406

Browse files
committed
fix loops/parallel badge case
1 parent 536d976 commit 3ada406

10 files changed

Lines changed: 602 additions & 132 deletions

File tree

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/editor.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,11 +167,23 @@ export function Editor() {
167167
[subBlocksForCanonical]
168168
)
169169
const canonicalModeOverrides = currentBlock?.data?.canonicalModes
170+
const activeSearchTargetNeedsAdvanced = useMemo(() => {
171+
if (!activeSearchTarget || activeSearchTarget.blockId !== currentBlockId) return false
172+
173+
return subBlocksForCanonical.some(
174+
(subBlock) =>
175+
subBlock.mode === 'advanced' &&
176+
(activeSearchTarget.subBlockId === subBlock.id ||
177+
activeSearchTarget.canonicalSubBlockId === (subBlock.canonicalParamId ?? subBlock.id))
178+
)
179+
}, [activeSearchTarget, currentBlockId, subBlocksForCanonical])
170180
const advancedValuesPresent = useMemo(
171181
() => hasAdvancedValues(subBlocksForCanonical, blockSubBlockValues, canonicalIndex),
172182
[subBlocksForCanonical, blockSubBlockValues, canonicalIndex]
173183
)
174-
const displayAdvancedOptions = canEditBlock ? advancedMode : advancedMode || advancedValuesPresent
184+
const displayAdvancedOptions = canEditBlock
185+
? advancedMode || activeSearchTargetNeedsAdvanced
186+
: advancedMode || advancedValuesPresent || activeSearchTargetNeedsAdvanced
175187

176188
const hasAdvancedOnlyFields = useMemo(() => {
177189
for (const subBlock of subBlocksForCanonical) {

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/search-replace/replacement-controls.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export function ReplacementControls({
4141
}))}
4242
value={replacement}
4343
onChange={onReplacementChange}
44-
placeholder='Choose replacement resource...'
44+
placeholder='Choose replacement...'
4545
searchable
4646
searchPlaceholder='Search resources...'
4747
emptyMessage='No valid replacements available'
@@ -58,7 +58,7 @@ export function ReplacementControls({
5858
</div>
5959

6060
<div className='flex items-center justify-between gap-2'>
61-
<span className='text-muted-foreground text-xs'>
61+
<span className='text-[var(--text-muted)] text-xs'>
6262
{eligibleCount} replaceable match{eligibleCount === 1 ? '' : 'es'}
6363
</span>
6464
<div className='flex gap-1.5'>

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/search-replace/workflow-search-replace.tsx

Lines changed: 79 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
'use client'
22

33
import { 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'
55
import { useParams } from 'next/navigation'
66
import { Button, Input } from '@/components/emcn'
77
import { getWorkflowSearchDependentClears } from '@/lib/workflows/search-replace/dependencies'
@@ -17,6 +17,8 @@ import {
1717
workflowSearchMatchMatchesQuery,
1818
} from '@/lib/workflows/search-replace/resource-resolvers'
1919
import { 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'
2022
import { useRegisterGlobalCommands } from '@/app/workspace/[workspaceId]/providers/global-commands-provider'
2123
import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
2224
import { createCommand } from '@/app/workspace/[workspaceId]/utils/commands-utils'
@@ -29,6 +31,9 @@ import {
2931
} from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/float'
3032
import { useCurrentWorkflow } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-current-workflow'
3133
import { 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'
3237
import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow'
3338
import { useNotificationStore } from '@/stores/notifications/store'
3439
import { usePanelEditorStore } from '@/stores/panel'
@@ -37,8 +42,8 @@ import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
3742
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
3843

3944
const 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

4348
function 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

apps/sim/lib/workflows/search-replace/indexer.test.ts

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,27 @@ describe('indexWorkflowSearchMatches', () => {
4444
expect(matches).toEqual([])
4545
})
4646

47+
it('indexes non-string scalar values as searchable but not editable', () => {
48+
const workflow = createSearchReplaceWorkflowFixture()
49+
workflow.blocks['api-1'].subBlocks.body.value = { count: 2, enabled: true }
50+
51+
const matches = indexWorkflowSearchMatches({
52+
workflow,
53+
query: '2',
54+
mode: 'text',
55+
blockConfigs: SEARCH_REPLACE_BLOCK_CONFIGS,
56+
}).filter((match) => match.blockId === 'api-1')
57+
58+
expect(matches).toEqual([
59+
expect.objectContaining({
60+
valuePath: ['count'],
61+
rawValue: '2',
62+
editable: false,
63+
reason: 'Only text values can be replaced',
64+
}),
65+
])
66+
})
67+
4768
it('indexes loop and parallel editor settings for navigation', () => {
4869
const workflow = createSearchReplaceWorkflowFixture()
4970
workflow.blocks['parallel-1'] = {
@@ -93,7 +114,11 @@ describe('indexWorkflowSearchMatches', () => {
93114
subBlockId: WORKFLOW_SEARCH_SUBFLOW_FIELD_IDS.iterations,
94115
canonicalSubBlockId: WORKFLOW_SEARCH_SUBFLOW_FIELD_IDS.iterations,
95116
fieldTitle: 'Parallel Iterations',
96-
editable: false,
117+
editable: true,
118+
target: {
119+
kind: 'subflow',
120+
fieldId: WORKFLOW_SEARCH_SUBFLOW_FIELD_IDS.iterations,
121+
},
97122
}),
98123
])
99124
)
@@ -104,7 +129,11 @@ describe('indexWorkflowSearchMatches', () => {
104129
subBlockId: WORKFLOW_SEARCH_SUBFLOW_FIELD_IDS.items,
105130
canonicalSubBlockId: WORKFLOW_SEARCH_SUBFLOW_FIELD_IDS.items,
106131
fieldTitle: 'Collection Items',
107-
editable: false,
132+
editable: true,
133+
target: {
134+
kind: 'subflow',
135+
fieldId: WORKFLOW_SEARCH_SUBFLOW_FIELD_IDS.items,
136+
},
108137
}),
109138
])
110139
)
@@ -235,4 +264,20 @@ describe('indexWorkflowSearchMatches', () => {
235264
expect(matches.every((match) => !match.editable)).toBe(true)
236265
expect(matches.every((match) => match.reason === 'Snapshot view is readonly')).toBe(true)
237266
})
267+
268+
it('marks readonly workflow matches as searchable but not editable', () => {
269+
const workflow = createSearchReplaceWorkflowFixture()
270+
271+
const matches = indexWorkflowSearchMatches({
272+
workflow,
273+
query: 'email',
274+
mode: 'text',
275+
isReadOnly: true,
276+
readonlyReason: 'Workflow is locked',
277+
blockConfigs: SEARCH_REPLACE_BLOCK_CONFIGS,
278+
})
279+
280+
expect(matches.every((match) => !match.editable)).toBe(true)
281+
expect(matches.every((match) => match.reason === 'Workflow is locked')).toBe(true)
282+
})
238283
})

0 commit comments

Comments
 (0)