Skip to content

Commit 304a7bb

Browse files
committed
fix tool input scenarios and add dependsOn UI handling
1 parent 1ccbe7f commit 304a7bb

18 files changed

Lines changed: 2190 additions & 24 deletions

File tree

apps/sim/app/api/workspaces/[id]/fork/diff/route.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { getSession } from '@/lib/auth'
66
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
77
import { loadSourceDeployedStates } from '@/lib/workspaces/fork/copy/deploy-bridge'
88
import { assertCanPromote } from '@/lib/workspaces/fork/lineage/authz'
9+
import { collectForkDependentReconfigs } from '@/lib/workspaces/fork/mapping/dependent-reconfigs'
910
import { computeForkPromotePlan } from '@/lib/workspaces/fork/promote/promote-plan'
1011

1112
export const GET = withRouteHandler(
@@ -80,6 +81,7 @@ export const GET = withRouteHandler(
8081
unmappedOptional: plan.unmappedOptional.map(toRef),
8182
mcpReauthServerIds: plan.mcpReauthServerIds,
8283
inlineSecretSources: plan.inlineSecretSources,
84+
dependentReconfigs: collectForkDependentReconfigs(plan.items, sourceStates),
8385
drift: plan.drift,
8486
})
8587
}

apps/sim/app/api/workspaces/[id]/fork/promote/route.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export const POST = withRouteHandler(
2525
const parsed = await parseRequest(promoteForkContract, req, context)
2626
if (!parsed.success) return parsed.response
2727
const { id } = parsed.data.params
28-
const { otherWorkspaceId, direction, force } = parsed.data.body
28+
const { otherWorkspaceId, direction, force, dependentOverrides } = parsed.data.body
2929

3030
const auth = await assertCanPromote(id, otherWorkspaceId, direction, session.user.id)
3131

@@ -36,6 +36,7 @@ export const POST = withRouteHandler(
3636
direction,
3737
force,
3838
userId: session.user.id,
39+
dependentOverrides,
3940
requestId,
4041
})
4142

@@ -47,6 +48,8 @@ export const POST = withRouteHandler(
4748
redeployed: result.redeployed,
4849
deployFailed: result.deployFailed,
4950
unmappedRequired: result.unmappedRequired,
51+
needsConfiguration: result.needsConfiguration,
52+
clearedOptional: result.clearedOptional,
5053
drift: result.drift,
5154
}
5255

@@ -84,7 +87,12 @@ export const POST = withRouteHandler(
8487
await recordBackgroundWork(db, {
8588
workspaceId: id,
8689
kind: 'fork_sync',
87-
status: result.deployFailed > 0 ? 'completed_with_warnings' : 'completed',
90+
status:
91+
result.deployFailed > 0 ||
92+
result.needsConfiguration.length > 0 ||
93+
result.clearedOptional.length > 0
94+
? 'completed_with_warnings'
95+
: 'completed',
8896
message: direction === 'pull' ? `Pulled from "${otherName}"` : `Pushed to "${otherName}"`,
8997
metadata: {
9098
actorName: session.user.name ?? undefined,
@@ -98,6 +106,8 @@ export const POST = withRouteHandler(
98106
updatedNames: result.updatedNames,
99107
createdNames: result.createdNames,
100108
archivedNames: result.archivedNames,
109+
needsConfiguration: result.needsConfiguration,
110+
clearedOptional: result.clearedOptional,
101111
},
102112
}).catch((error) =>
103113
logger.error(`[${requestId}] Failed to record sync activity`, {

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tools/sub-block-renderer.tsx

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
resolveToolParamSync,
77
} from '@/lib/workflows/tool-input/synthetic-subblocks'
88
import { parseStoredToolInputValue } from '@/lib/workflows/tool-input/types'
9+
import { DependencyBlockTypeProvider } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-dependency-block-type'
910
import { SubBlock } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block'
1011
import type { SubBlockConfig as BlockSubBlockConfig } from '@/blocks/types'
1112
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
@@ -17,6 +18,8 @@ interface ToolSubBlockRendererProps {
1718
toolIndex: number
1819
subBlock: BlockSubBlockConfig
1920
effectiveParamId: string
21+
/** The tool's block type (e.g. `gmail`), so its params' selectors resolve dependencies. */
22+
toolType: string
2023
toolParams: Record<string, string> | undefined
2124
onParamChange: (toolIndex: number, paramId: string, value: string) => void
2225
disabled: boolean
@@ -44,6 +47,7 @@ export function ToolSubBlockRenderer({
4447
toolIndex,
4548
subBlock,
4649
effectiveParamId,
50+
toolType,
4751
toolParams,
4852
onParamChange,
4953
disabled,
@@ -118,13 +122,15 @@ export function ToolSubBlockRenderer({
118122
}
119123

120124
return (
121-
<SubBlock
122-
blockId={blockId}
123-
config={config}
124-
isPreview={false}
125-
disabled={disabled}
126-
canonicalToggle={canonicalToggle}
127-
dependencyContext={toolParams}
128-
/>
125+
<DependencyBlockTypeProvider value={toolType}>
126+
<SubBlock
127+
blockId={blockId}
128+
config={config}
129+
isPreview={false}
130+
disabled={disabled}
131+
canonicalToggle={canonicalToggle}
132+
dependencyContext={toolParams}
133+
/>
134+
</DependencyBlockTypeProvider>
129135
)
130136
}

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2111,6 +2111,7 @@ export const ToolInput = memo(function ToolInput({
21112111
toolIndex={toolIndex}
21122112
subBlock={sbWithTitle}
21132113
effectiveParamId={effectiveParamId}
2114+
toolType={tool.type}
21142115
toolParams={tool.params}
21152116
onParamChange={handleParamChange}
21162117
disabled={disabled}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
'use client'
2+
3+
import { createContext, useContext } from 'react'
4+
5+
const DependencyBlockTypeContext = createContext<string | null>(null)
6+
7+
/**
8+
* Provider set by tool-input param rendering (value = the tool's block type, e.g. `gmail`).
9+
*/
10+
export const DependencyBlockTypeProvider = DependencyBlockTypeContext.Provider
11+
12+
/**
13+
* The block type whose config should drive dependency (`dependsOn`) canonical resolution
14+
* for the current subblock. Null for normal blocks (resolve against the host block). Set
15+
* to the tool's type for tool-input params, so a nested tool's selector resolves its
16+
* parents against the TOOL's config (e.g. a Gmail tool's `credential` -> `oauthCredential`,
17+
* which the host Agent block's subblocks don't define) and can fetch its options.
18+
*/
19+
export const useDependencyBlockType = () => useContext(DependencyBlockTypeContext)

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-depends-on-gate.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import type { SubBlockConfig } from '@/blocks/types'
1515
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
1616
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
1717
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
18+
import { useDependencyBlockType } from './use-dependency-block-type'
1819

1920
/**
2021
* Centralized dependsOn gating for sub-block components.
@@ -33,7 +34,13 @@ export function useDependsOnGate(
3334

3435
const activeWorkflowId = useWorkflowRegistry((s) => s.activeWorkflowId)
3536
const blockState = useWorkflowStore((state) => state.blocks[blockId])
36-
const blockConfig = blockState?.type ? getBlock(blockState.type) : null
37+
38+
const dependencyBlockType = useDependencyBlockType()
39+
const blockConfig = dependencyBlockType
40+
? getBlock(dependencyBlockType)
41+
: blockState?.type
42+
? getBlock(blockState.type)
43+
: null
3744
const canonicalIndex = useMemo(
3845
() => buildCanonicalIndex(blockConfig?.subBlocks || []),
3946
[blockConfig?.subBlocks]

apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/fork-activity-panel/fork-activity-panel.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,21 @@ function jobReport(job: BackgroundWorkItem): JobReport {
8484
])
8585
if (counts) notes.push({ value: counts })
8686
}
87+
if (m.needsConfiguration && m.needsConfiguration.length > 0) {
88+
for (const item of m.needsConfiguration) {
89+
notes.push({
90+
value: `${item.workflowName} — re-check ${item.blocks.join(', ')}`,
91+
warning: true,
92+
})
93+
}
94+
}
95+
if (m.clearedOptional && m.clearedOptional.length > 0) {
96+
for (const item of m.clearedOptional) {
97+
notes.push({
98+
value: `${item.workflowName} — optional cleared in ${item.blocks.join(', ')}`,
99+
})
100+
}
101+
}
87102
if (m.deployFailed && m.deployFailed > 0) {
88103
notes.push({ value: `${plural(m.deployFailed, 'workflow')} failed to deploy`, warning: true })
89104
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
'use client'
2+
3+
import { useMemo } from 'react'
4+
import { ChipCombobox, type ComboboxOption, Loader } from '@/components/emcn'
5+
import type { SelectorContext, SelectorKey } from '@/hooks/selectors/types'
6+
import { useSelectorOptions } from '@/hooks/selectors/use-selector-query'
7+
8+
interface DependentFieldSelectorProps {
9+
selectorKey: SelectorKey
10+
/** Full selector context, including the newly-chosen parent value. */
11+
context: Record<string, string>
12+
/** False until the parent (credential/KB) target is chosen. */
13+
enabled: boolean
14+
value: string
15+
onChange: (value: string) => void
16+
title: string
17+
}
18+
19+
/**
20+
* A controlled, standalone selector for the sync modal's pre-sync reconfigure: fetches
21+
* options via the shared selector data layer (the same `useSelectorOptions` registry the
22+
* canvas selectors use) without the canvas store/blockId coupling. Mirrors
23+
* {@link ConnectorSelectorField}.
24+
*/
25+
export function DependentFieldSelector({
26+
selectorKey,
27+
context,
28+
enabled,
29+
value,
30+
onChange,
31+
title,
32+
}: DependentFieldSelectorProps) {
33+
const selectorContext = useMemo<SelectorContext>(() => {
34+
const ctx: SelectorContext = {}
35+
Object.assign(ctx, context)
36+
return ctx
37+
}, [context])
38+
39+
const { data: options = [], isLoading } = useSelectorOptions(selectorKey, {
40+
context: selectorContext,
41+
enabled,
42+
})
43+
44+
const comboboxOptions = useMemo<ComboboxOption[]>(
45+
() => options.map((option) => ({ label: option.label, value: option.id })),
46+
[options]
47+
)
48+
49+
if (isLoading && enabled) {
50+
return (
51+
<div className='flex h-[30px] items-center gap-2 rounded-lg border border-[var(--border-1)] bg-[var(--surface-5)] px-2 font-medium text-[var(--text-muted)] text-small dark:bg-[var(--surface-4)]'>
52+
<Loader className='size-3.5' animate />
53+
Loading…
54+
</div>
55+
)
56+
}
57+
58+
return (
59+
<ChipCombobox
60+
className='w-full'
61+
options={comboboxOptions}
62+
value={value || undefined}
63+
onChange={(next) => onChange(next)}
64+
searchable
65+
searchPlaceholder={`Search ${title.toLowerCase()}...`}
66+
placeholder={`Select ${title.toLowerCase()}`}
67+
disabled={!enabled}
68+
emptyMessage={`No ${title.toLowerCase()} found`}
69+
/>
70+
)
71+
}

0 commit comments

Comments
 (0)