Skip to content

Commit 9e1d103

Browse files
author
Theodore Li
committed
Shift conditional render out of subblock
1 parent ab50c2d commit 9e1d103

5 files changed

Lines changed: 83 additions & 47 deletions

File tree

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

Lines changed: 0 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,6 @@ import {
5252
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components'
5353
import { useDependsOnGate } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-depends-on-gate'
5454
import type { SubBlockConfig } from '@/blocks/types'
55-
import { useWorkspaceCredential } from '@/hooks/queries/credentials'
56-
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
57-
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
5855
import { useWebhookManagement } from '@/hooks/use-webhook-management'
5956

6057
const SLACK_OVERRIDES: SelectorOverrides = {
@@ -618,36 +615,6 @@ function SubBlockComponent({
618615
previewContextValues: contextValues,
619616
})
620617

621-
// Credential-type visibility gate: reactively fetches the credential by ID
622-
// and hides the field unless it matches the required type.
623-
const credTypeCond = config.credentialTypeCondition
624-
const activeWfId = useWorkflowRegistry((s) => (credTypeCond ? s.activeWorkflowId : null))
625-
626-
const watchedCredentialId = useSubBlockStore(
627-
useCallback(
628-
(state) => {
629-
if (!credTypeCond || !activeWfId) return ''
630-
const blockValues = state.workflowValues[activeWfId]?.[blockId] ?? {}
631-
const merged = { ...(dependencyContext ?? {}), ...blockValues }
632-
for (const field of credTypeCond.watchFields) {
633-
const val = merged[field]
634-
if (val && typeof val === 'string') return val
635-
}
636-
return ''
637-
},
638-
[credTypeCond, activeWfId, blockId, dependencyContext]
639-
)
640-
)
641-
642-
const { data: watchedCredential } = useWorkspaceCredential(
643-
watchedCredentialId || undefined,
644-
Boolean(credTypeCond && watchedCredentialId)
645-
)
646-
647-
if (credTypeCond && watchedCredential?.type !== credTypeCond.requiredType) {
648-
return <></> as unknown as JSX.Element
649-
}
650-
651618
const isDisabled = gatedDisabled
652619

653620
/**

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/hooks/use-editor-subblock-layout.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,66 @@ import {
77
isSubBlockVisibleForMode,
88
} from '@/lib/workflows/subblocks/visibility'
99
import type { BlockConfig, SubBlockConfig, SubBlockType } from '@/blocks/types'
10+
import { useWorkspaceCredential } from '@/hooks/queries/credentials'
1011
import { usePermissionConfig } from '@/hooks/use-permission-config'
1112
import { useWorkflowDiffStore } from '@/stores/workflow-diff'
13+
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
1214
import { mergeSubblockState } from '@/stores/workflows/utils'
1315
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
1416

17+
/**
18+
* Evaluates reactive conditions for subblocks. Always calls the same hooks
19+
* regardless of whether a reactive condition exists (Rules of Hooks).
20+
*
21+
* Returns a Set of subblock IDs that should be hidden.
22+
*/
23+
function useReactiveConditions(
24+
subBlocks: SubBlockConfig[],
25+
blockId: string,
26+
activeWorkflowId: string | null,
27+
blockSubBlockValues: Record<string, unknown>
28+
): Set<string> {
29+
const reactiveSubBlock = useMemo(
30+
() => subBlocks.find((sb) => sb.reactiveCondition),
31+
[subBlocks]
32+
)
33+
const reactiveCond = reactiveSubBlock?.reactiveCondition
34+
35+
// Subscribe to watched field values — always called (stable hook count)
36+
const watchedCredentialId = useSubBlockStore(
37+
useCallback(
38+
(state) => {
39+
if (!reactiveCond || !activeWorkflowId) return ''
40+
const blockValues = state.workflowValues[activeWorkflowId]?.[blockId] ?? {}
41+
const merged = { ...blockSubBlockValues, ...blockValues }
42+
for (const field of reactiveCond.watchFields) {
43+
const val = merged[field]
44+
if (val && typeof val === 'string') return val
45+
}
46+
return ''
47+
},
48+
[reactiveCond, activeWorkflowId, blockId, blockSubBlockValues]
49+
)
50+
)
51+
52+
// Always call useWorkspaceCredential (stable hook count), disable when not needed
53+
const { data: credential } = useWorkspaceCredential(
54+
watchedCredentialId || undefined,
55+
Boolean(reactiveCond && watchedCredentialId)
56+
)
57+
58+
return useMemo(() => {
59+
const hidden = new Set<string>()
60+
if (!reactiveSubBlock || !reactiveCond) return hidden
61+
62+
const conditionMet = credential?.type === reactiveCond.requiredType
63+
if (!conditionMet) {
64+
hidden.add(reactiveSubBlock.id)
65+
}
66+
return hidden
67+
}, [reactiveSubBlock, reactiveCond, credential?.type])
68+
}
69+
1570
/**
1671
* Custom hook for computing subblock layout in the editor panel.
1772
* Determines which subblocks should be visible based on mode, conditions, and feature flags.
@@ -39,6 +94,14 @@ export function useEditorSubblockLayout(
3994
)
4095
const { config: permissionConfig } = usePermissionConfig()
4196

97+
// Evaluate reactive conditions (hooks-based, must be called before useMemo)
98+
const hiddenByReactiveCondition = useReactiveConditions(
99+
config?.subBlocks || [],
100+
blockId,
101+
activeWorkflowId,
102+
blockSubBlockValues
103+
)
104+
42105
return useMemo(() => {
43106
// Guard against missing config or block selection
44107
if (!config || !Array.isArray((config as any).subBlocks) || !blockId) {
@@ -109,6 +172,9 @@ export function useEditorSubblockLayout(
109172
const visibleSubBlocks = (config.subBlocks || []).filter((block) => {
110173
if (block.hidden) return false
111174

175+
// Filter by reactive condition (evaluated via hooks before useMemo)
176+
if (hiddenByReactiveCondition.has(block.id)) return false
177+
112178
// Hide skill-input subblock when skills are disabled via permissions
113179
if (block.type === 'skill-input' && permissionConfig.disableSkills) return false
114180

@@ -164,6 +230,7 @@ export function useEditorSubblockLayout(
164230
activeWorkflowId,
165231
isSnapshotView,
166232
blockDataFromStore,
233+
hiddenByReactiveCondition,
167234
permissionConfig.disableSkills,
168235
])
169236
}

apps/sim/blocks/types.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -357,16 +357,19 @@ export interface SubBlockConfig {
357357
}
358358
})
359359
/**
360-
* Declarative credential-type visibility gate. The SubBlock watches the specified
361-
* fields for a credential ID, fetches the credential via React Query, and hides
362-
* the field unless the credential type matches.
360+
* Reactive visibility condition evaluated via hooks at the layout level.
363361
*
364-
* Works in both block editor and tool-input contexts without side effects.
362+
* - `watchFields`: subblock IDs whose values to watch. The first non-empty string
363+
* value is used as a credential ID to look up via the workspace credentials API.
364+
* - `requiredType`: the credential type that must match for the field to be visible
365+
* (e.g. `'service_account'`).
366+
*
367+
* The layout hook calls `useWorkspaceCredential` (always, for stable hook count)
368+
* and filters the subblock out if the type doesn't match. The serializer skips
369+
* this check — it always serializes the field if it has a value.
365370
*/
366-
credentialTypeCondition?: {
367-
/** Subblock IDs (or canonical param IDs) to read the credential ID from, in priority order. */
371+
reactiveCondition?: {
368372
watchFields: string[]
369-
/** Required credential type for the field to be visible (e.g. 'service_account'). */
370373
requiredType: string
371374
}
372375
// Props specific to 'code' sub-block type

apps/sim/blocks/utils.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,8 @@ import { useProvidersStore } from '@/stores/providers/store'
1010

1111
/**
1212
* Standard subblocks for Google service account impersonation.
13-
* The `impersonateUserEmail` field fetches the credential by ID to check if it's
14-
* a service account, using `asyncCondition` so it works in both block editor and
15-
* agent tool-input contexts without cross-subblock state propagation.
13+
* Uses a reactive condition that fetches the credential by ID to check if it's
14+
* a service account — works in both block editor and agent tool-input contexts.
1615
*/
1716
export const SERVICE_ACCOUNT_SUBBLOCKS: SubBlockConfig[] = [
1817
{
@@ -21,7 +20,7 @@ export const SERVICE_ACCOUNT_SUBBLOCKS: SubBlockConfig[] = [
2120
type: 'short-input',
2221
placeholder: 'Email to impersonate (for service accounts)',
2322
paramVisibility: 'user-only',
24-
credentialTypeCondition: {
23+
reactiveCondition: {
2524
watchFields: ['credential', 'oauthCredential', 'manualCredential'],
2625
requiredType: 'service_account',
2726
},

apps/sim/tools/params.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -990,9 +990,9 @@ export function getSubBlocksForToolInput(
990990
if (visibility === 'hidden' || visibility === 'llm-only') continue
991991

992992
// Evaluate condition against current values.
993-
// Skip sync condition check for subblocks with credentialTypeCondition — visibility
994-
// is handled at render time by the SubBlock component via React Query.
995-
if (sb.condition && !sb.credentialTypeCondition) {
993+
// Skip sync condition check for subblocks with reactiveCondition — visibility
994+
// is handled at render time by the SubBlock component.
995+
if (sb.condition && !sb.reactiveCondition) {
996996
const conditionMet = evaluateSubBlockCondition(
997997
sb.condition as SubBlockCondition,
998998
valuesWithOperation

0 commit comments

Comments
 (0)