Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,77 +1,18 @@
import { useCallback, useMemo } from 'react'
import type { CanonicalModeOverrides } from '@/lib/workflows/subblocks/visibility'
import {
buildCanonicalIndex,
evaluateSubBlockCondition,
isSubBlockFeatureEnabled,
isSubBlockHidden,
isSubBlockVisibleForMode,
resolveDependencyValue,
} from '@/lib/workflows/subblocks/visibility'
import type { BlockConfig, SubBlockConfig, SubBlockType } from '@/blocks/types'
import { useWorkspaceCredential } from '@/hooks/queries/credentials'
import { usePermissionConfig } from '@/hooks/use-permission-config'
import { useReactiveConditions } from '@/hooks/use-reactive-conditions'
import { useWorkflowDiffStore } from '@/stores/workflow-diff'
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
import { mergeSubblockState } from '@/stores/workflows/utils'
import { useWorkflowStore } from '@/stores/workflows/workflow/store'

/**
* Evaluates reactive conditions for subblocks. Always calls the same hooks
* regardless of whether a reactive condition exists (Rules of Hooks).
*
* Returns a Set of subblock IDs that should be hidden.
*/
function useReactiveConditions(
subBlocks: SubBlockConfig[],
blockId: string,
activeWorkflowId: string | null,
canonicalModeOverrides?: CanonicalModeOverrides
): Set<string> {
const reactiveSubBlock = useMemo(() => subBlocks.find((sb) => sb.reactiveCondition), [subBlocks])
const reactiveCond = reactiveSubBlock?.reactiveCondition

const canonicalIndex = useMemo(() => buildCanonicalIndex(subBlocks), [subBlocks])

// Resolve watchFields through canonical index to get the active credential value
const watchedCredentialId = useSubBlockStore(
useCallback(
(state) => {
if (!reactiveCond || !activeWorkflowId) return ''
const blockValues = state.workflowValues[activeWorkflowId]?.[blockId] ?? {}
for (const field of reactiveCond.watchFields) {
const val = resolveDependencyValue(
field,
blockValues,
canonicalIndex,
canonicalModeOverrides
)
if (val && typeof val === 'string') return val
}
return ''
},
[reactiveCond, activeWorkflowId, blockId, canonicalIndex, canonicalModeOverrides]
)
)

// Always call useWorkspaceCredential (stable hook count), disable when not needed
const { data: credential } = useWorkspaceCredential(
watchedCredentialId || undefined,
Boolean(reactiveCond && watchedCredentialId)
)

return useMemo(() => {
const hidden = new Set<string>()
if (!reactiveSubBlock || !reactiveCond) return hidden

const conditionMet = credential?.type === reactiveCond.requiredType
if (!conditionMet) {
hidden.add(reactiveSubBlock.id)
}
return hidden
}, [reactiveSubBlock, reactiveCond, credential?.type])
}

/**
* Custom hook for computing subblock layout in the editor panel.
* Determines which subblocks should be visible based on mode, conditions, and feature flags.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import { useReactivateSchedule, useScheduleInfo } from '@/hooks/queries/schedule
import { useSkills } from '@/hooks/queries/skills'
import { useTablesList } from '@/hooks/queries/tables'
import { useWorkflowMap } from '@/hooks/queries/workflows'
import { useReactiveConditions } from '@/hooks/use-reactive-conditions'
import { useSelectorDisplayName } from '@/hooks/use-selector-display-name'
import { useVariablesStore } from '@/stores/panel'
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
Expand Down Expand Up @@ -942,6 +943,13 @@ export const WorkflowBlock = memo(function WorkflowBlock({
const canonicalIndex = useMemo(() => buildCanonicalIndex(config.subBlocks), [config.subBlocks])
const canonicalModeOverrides = currentStoreBlock?.data?.canonicalModes

const hiddenByReactiveCondition = useReactiveConditions(
config.subBlocks,
id,
activeWorkflowId,
canonicalModeOverrides
)

const subBlockRowsData = useMemo(() => {
const rows: SubBlockConfig[][] = []
let currentRow: SubBlockConfig[] = []
Expand Down Expand Up @@ -979,6 +987,7 @@ export const WorkflowBlock = memo(function WorkflowBlock({
const visibleSubBlocks = config.subBlocks.filter((block) => {
if (block.hidden) return false
if (block.hideFromPreview) return false
if (hiddenByReactiveCondition.has(block.id)) return false
if (!isSubBlockFeatureEnabled(block)) return false
if (isSubBlockHidden(block)) return false

Expand Down Expand Up @@ -1047,6 +1056,7 @@ export const WorkflowBlock = memo(function WorkflowBlock({
canonicalModeOverrides,
userPermissions.canEdit,
canonicalIndex,
hiddenByReactiveCondition,
blockSubBlockValues,
activeWorkflowId,
])
Expand Down
62 changes: 62 additions & 0 deletions apps/sim/hooks/use-reactive-conditions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { useCallback, useMemo } from 'react'
import type { CanonicalModeOverrides } from '@/lib/workflows/subblocks/visibility'
import { buildCanonicalIndex, resolveDependencyValue } from '@/lib/workflows/subblocks/visibility'
import type { SubBlockConfig } from '@/blocks/types'
import { useWorkspaceCredential } from '@/hooks/queries/credentials'
import { useSubBlockStore } from '@/stores/workflows/subblock/store'

/**
* Evaluates reactive conditions for subblocks. Always calls the same hooks
* regardless of whether a reactive condition exists (Rules of Hooks).
*
* Returns a Set of subblock IDs that should be hidden.
*/
export function useReactiveConditions(
subBlocks: SubBlockConfig[],
blockId: string,
activeWorkflowId: string | null,
canonicalModeOverrides?: CanonicalModeOverrides
): Set<string> {
const reactiveSubBlock = useMemo(() => subBlocks.find((sb) => sb.reactiveCondition), [subBlocks])
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Single reactive subblock limitation undocumented

This hook processes only the first subblock with a reactiveCondition (via .find()). All current Google blocks spread SERVICE_ACCOUNT_SUBBLOCKS exactly once per block config, so this is not a current bug. However, the constraint is implicit — a future block with multiple reactive subblocks would silently ignore all but the first. Consider a brief TSDoc note to document the assumption:

Suggested change
const reactiveSubBlock = useMemo(() => subBlocks.find((sb) => sb.reactiveCondition), [subBlocks])
const reactiveSubBlock = useMemo(
// NOTE: only the first subblock with a reactiveCondition is evaluated;
// blocks are expected to define at most one reactive subblock
() => subBlocks.find((sb) => sb.reactiveCondition),
[subBlocks]
)

const reactiveCond = reactiveSubBlock?.reactiveCondition

const canonicalIndex = useMemo(() => buildCanonicalIndex(subBlocks), [subBlocks])

// Resolve watchFields through canonical index to get the active credential value
const watchedCredentialId = useSubBlockStore(
useCallback(
(state) => {
if (!reactiveCond || !activeWorkflowId) return ''
const blockValues = state.workflowValues[activeWorkflowId]?.[blockId] ?? {}
for (const field of reactiveCond.watchFields) {
const val = resolveDependencyValue(
field,
blockValues,
canonicalIndex,
canonicalModeOverrides
)
if (val && typeof val === 'string') return val
}
return ''
},
[reactiveCond, activeWorkflowId, blockId, canonicalIndex, canonicalModeOverrides]
)
)

// Always call useWorkspaceCredential (stable hook count), disable when not needed
const { data: credential } = useWorkspaceCredential(
watchedCredentialId || undefined,
Boolean(reactiveCond && watchedCredentialId)
)

return useMemo(() => {
const hidden = new Set<string>()
if (!reactiveSubBlock || !reactiveCond) return hidden

const conditionMet = credential?.type === reactiveCond.requiredType
if (!conditionMet) {
hidden.add(reactiveSubBlock.id)
}
return hidden
}, [reactiveSubBlock, reactiveCond, credential?.type])
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { getCopilotToolDescription } from '@/lib/copilot/tool-descriptions'
import type { BaseServerTool } from '@/lib/copilot/tools/server/base-tool'
import { GetBlocksMetadataInput, GetBlocksMetadataResult } from '@/lib/copilot/tools/shared/schemas'
import { getAllowedIntegrationsFromEnv, isHosted } from '@/lib/core/config/feature-flags'
import { getServiceAccountProviderForProviderId } from '@/lib/oauth/utils'
import { registry as blockRegistry } from '@/blocks/registry'
import { AuthMode, type BlockConfig, isHiddenFromDisplay } from '@/blocks/types'
import { getUserPermissionConfig } from '@/ee/access-control/utils/permission-check'
Expand Down Expand Up @@ -342,6 +343,20 @@ function transformBlockMetadata(metadata: CopilotBlockMetadata): any {
service: metadata.id, // e.g., 'gmail', 'slack', etc.
description: `OAuth authentication required for ${metadata.name}`,
}

// Check if this service also supports service account credentials
const oauthSubBlock = metadata.inputSchema?.find(
(sb: CopilotSubblockMetadata) => sb.type === 'oauth-input' && sb.serviceId
)
if (oauthSubBlock?.serviceId) {
const serviceAccountProviderId = getServiceAccountProviderForProviderId(
oauthSubBlock.serviceId
)
if (serviceAccountProviderId) {
transformed.requiredCredentials.serviceAccountType = serviceAccountProviderId
transformed.requiredCredentials.description = `OAuth or service account authentication supported for ${metadata.name}`
}
}
} else if (metadata.authType === 'API Key') {
transformed.requiredCredentials = {
type: 'api_key',
Expand Down
Loading