mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-06 03:00:16 -04:00
improvement(selectors): consolidate selector input logic (#3375)
This commit is contained in:
committed by
GitHub
parent
e3ff595a84
commit
49db3ca50b
@@ -1,118 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { Tooltip } from '@/components/emcn'
|
||||
import { buildCanonicalIndex, resolveDependencyValue } from '@/lib/workflows/subblocks/visibility'
|
||||
import { SelectorCombobox } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/selector-combobox/selector-combobox'
|
||||
import { useDependsOnGate } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-depends-on-gate'
|
||||
import { resolvePreviewContextValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/utils'
|
||||
import { getBlock } from '@/blocks/registry'
|
||||
import type { SubBlockConfig } from '@/blocks/types'
|
||||
import type { SelectorContext } from '@/hooks/selectors/types'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
|
||||
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
|
||||
|
||||
interface DocumentSelectorProps {
|
||||
blockId: string
|
||||
subBlock: SubBlockConfig
|
||||
disabled?: boolean
|
||||
onDocumentSelect?: (documentId: string) => void
|
||||
isPreview?: boolean
|
||||
previewValue?: string | null
|
||||
previewContextValues?: Record<string, unknown>
|
||||
}
|
||||
|
||||
export function DocumentSelector({
|
||||
blockId,
|
||||
subBlock,
|
||||
disabled = false,
|
||||
onDocumentSelect,
|
||||
isPreview = false,
|
||||
previewValue,
|
||||
previewContextValues,
|
||||
}: DocumentSelectorProps) {
|
||||
const { activeWorkflowId } = useWorkflowRegistry()
|
||||
|
||||
const { finalDisabled } = useDependsOnGate(blockId, subBlock, {
|
||||
disabled,
|
||||
isPreview,
|
||||
previewContextValues,
|
||||
})
|
||||
|
||||
const blockState = useWorkflowStore((state) => state.blocks[blockId])
|
||||
const blockConfig = blockState?.type ? getBlock(blockState.type) : null
|
||||
const canonicalIndex = useMemo(
|
||||
() => buildCanonicalIndex(blockConfig?.subBlocks || []),
|
||||
[blockConfig?.subBlocks]
|
||||
)
|
||||
const canonicalModeOverrides = blockState?.data?.canonicalModes
|
||||
|
||||
const blockValues = useSubBlockStore((state) => {
|
||||
if (!activeWorkflowId) return {}
|
||||
const workflowValues = state.workflowValues[activeWorkflowId] || {}
|
||||
return (workflowValues as Record<string, Record<string, unknown>>)[blockId] || {}
|
||||
})
|
||||
|
||||
const knowledgeBaseIdValue = useMemo(
|
||||
() =>
|
||||
previewContextValues
|
||||
? resolvePreviewContextValue(previewContextValues.knowledgeBaseId)
|
||||
: resolveDependencyValue(
|
||||
'knowledgeBaseId',
|
||||
blockValues,
|
||||
canonicalIndex,
|
||||
canonicalModeOverrides
|
||||
),
|
||||
[previewContextValues, blockValues, canonicalIndex, canonicalModeOverrides]
|
||||
)
|
||||
|
||||
const normalizedKnowledgeBaseId =
|
||||
typeof knowledgeBaseIdValue === 'string' && knowledgeBaseIdValue.trim().length > 0
|
||||
? knowledgeBaseIdValue
|
||||
: null
|
||||
|
||||
const selectorContext = useMemo<SelectorContext>(
|
||||
() => ({
|
||||
knowledgeBaseId: normalizedKnowledgeBaseId ?? undefined,
|
||||
}),
|
||||
[normalizedKnowledgeBaseId]
|
||||
)
|
||||
|
||||
const handleDocumentChange = useCallback(
|
||||
(documentId: string) => {
|
||||
if (isPreview) return
|
||||
onDocumentSelect?.(documentId)
|
||||
},
|
||||
[isPreview, onDocumentSelect]
|
||||
)
|
||||
|
||||
const missingKnowledgeBase = !normalizedKnowledgeBaseId
|
||||
const isDisabled = finalDisabled || missingKnowledgeBase
|
||||
const placeholder = subBlock.placeholder || 'Select document'
|
||||
|
||||
return (
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger asChild>
|
||||
<div className='w-full'>
|
||||
<SelectorCombobox
|
||||
blockId={blockId}
|
||||
subBlock={subBlock}
|
||||
selectorKey='knowledge.documents'
|
||||
selectorContext={selectorContext}
|
||||
disabled={isDisabled}
|
||||
isPreview={isPreview}
|
||||
previewValue={previewValue ?? null}
|
||||
placeholder={placeholder}
|
||||
onOptionChange={handleDocumentChange}
|
||||
/>
|
||||
</div>
|
||||
</Tooltip.Trigger>
|
||||
{missingKnowledgeBase && (
|
||||
<Tooltip.Content side='top'>
|
||||
<p>Select a knowledge base first.</p>
|
||||
</Tooltip.Content>
|
||||
)}
|
||||
</Tooltip.Root>
|
||||
)
|
||||
}
|
||||
@@ -13,20 +13,15 @@ import {
|
||||
} from '@/components/emcn'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
import { FIELD_TYPE_LABELS, getPlaceholderForFieldType } from '@/lib/knowledge/constants'
|
||||
import { buildCanonicalIndex, resolveDependencyValue } from '@/lib/workflows/subblocks/visibility'
|
||||
import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/formatted-text'
|
||||
import { TagDropdown } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown'
|
||||
import { useDependsOnGate } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-depends-on-gate'
|
||||
import { useSubBlockInput } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-input'
|
||||
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
|
||||
import { resolvePreviewContextValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/utils'
|
||||
import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes'
|
||||
import { getBlock } from '@/blocks/registry'
|
||||
import type { SubBlockConfig } from '@/blocks/types'
|
||||
import { useKnowledgeBaseTagDefinitions } from '@/hooks/kb/use-knowledge-base-tag-definitions'
|
||||
import { useTagSelection } from '@/hooks/kb/use-tag-selection'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
|
||||
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
|
||||
|
||||
interface DocumentTag {
|
||||
id: string
|
||||
@@ -65,26 +60,11 @@ export function DocumentTagEntry({
|
||||
previewValue,
|
||||
previewContextValues,
|
||||
}: DocumentTagEntryProps) {
|
||||
const { activeWorkflowId } = useWorkflowRegistry()
|
||||
const [storeValue, setStoreValue] = useSubBlockValue<string>(blockId, subBlock.id)
|
||||
const accessiblePrefixes = useAccessibleReferencePrefixes(blockId)
|
||||
const valueInputRefs = useRef<Record<string, HTMLInputElement>>({})
|
||||
const overlayRefs = useRef<Record<string, HTMLDivElement>>({})
|
||||
|
||||
const blockState = useWorkflowStore((state) => state.blocks[blockId])
|
||||
const blockConfig = blockState?.type ? getBlock(blockState.type) : null
|
||||
const canonicalIndex = useMemo(
|
||||
() => buildCanonicalIndex(blockConfig?.subBlocks || []),
|
||||
[blockConfig?.subBlocks]
|
||||
)
|
||||
const canonicalModeOverrides = blockState?.data?.canonicalModes
|
||||
|
||||
const blockValues = useSubBlockStore((state) => {
|
||||
if (!activeWorkflowId) return {}
|
||||
const workflowValues = state.workflowValues[activeWorkflowId] || {}
|
||||
return (workflowValues as Record<string, Record<string, unknown>>)[blockId] || {}
|
||||
})
|
||||
|
||||
const inputController = useSubBlockInput({
|
||||
blockId,
|
||||
subBlockId: subBlock.id,
|
||||
@@ -97,18 +77,12 @@ export function DocumentTagEntry({
|
||||
disabled,
|
||||
})
|
||||
|
||||
const knowledgeBaseIdValue = useMemo(
|
||||
() =>
|
||||
previewContextValues
|
||||
? resolvePreviewContextValue(previewContextValues.knowledgeBaseId)
|
||||
: resolveDependencyValue(
|
||||
'knowledgeBaseId',
|
||||
blockValues,
|
||||
canonicalIndex,
|
||||
canonicalModeOverrides
|
||||
),
|
||||
[previewContextValues, blockValues, canonicalIndex, canonicalModeOverrides]
|
||||
)
|
||||
const { dependencyValues } = useDependsOnGate(blockId, subBlock, {
|
||||
disabled,
|
||||
isPreview,
|
||||
previewContextValues,
|
||||
})
|
||||
const knowledgeBaseIdValue = dependencyValues.knowledgeBaseSelector
|
||||
const knowledgeBaseId =
|
||||
typeof knowledgeBaseIdValue === 'string' && knowledgeBaseIdValue.trim().length > 0
|
||||
? knowledgeBaseIdValue
|
||||
|
||||
@@ -1,209 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { useMemo } from 'react'
|
||||
import { useParams } from 'next/navigation'
|
||||
import { Tooltip } from '@/components/emcn'
|
||||
import { getProviderIdFromServiceId } from '@/lib/oauth'
|
||||
import { buildCanonicalIndex, resolveDependencyValue } from '@/lib/workflows/subblocks/visibility'
|
||||
import { SelectorCombobox } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/selector-combobox/selector-combobox'
|
||||
import { useDependsOnGate } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-depends-on-gate'
|
||||
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
|
||||
import { resolvePreviewContextValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/utils'
|
||||
import { getBlock } from '@/blocks/registry'
|
||||
import type { SubBlockConfig } from '@/blocks/types'
|
||||
import { isDependency } from '@/blocks/utils'
|
||||
import { resolveSelectorForSubBlock, type SelectorResolution } from '@/hooks/selectors/resolution'
|
||||
import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
|
||||
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
|
||||
|
||||
interface FileSelectorInputProps {
|
||||
blockId: string
|
||||
subBlock: SubBlockConfig
|
||||
disabled: boolean
|
||||
isPreview?: boolean
|
||||
previewValue?: any | null
|
||||
previewContextValues?: Record<string, any>
|
||||
}
|
||||
|
||||
export function FileSelectorInput({
|
||||
blockId,
|
||||
subBlock,
|
||||
disabled,
|
||||
isPreview = false,
|
||||
previewValue,
|
||||
previewContextValues,
|
||||
}: FileSelectorInputProps) {
|
||||
const { collaborativeSetSubblockValue } = useCollaborativeWorkflow()
|
||||
const { activeWorkflowId } = useWorkflowRegistry()
|
||||
const params = useParams()
|
||||
const workflowIdFromUrl = (params?.workflowId as string) || activeWorkflowId || ''
|
||||
|
||||
const { finalDisabled } = useDependsOnGate(blockId, subBlock, {
|
||||
disabled,
|
||||
isPreview,
|
||||
previewContextValues,
|
||||
})
|
||||
|
||||
const blockState = useWorkflowStore((state) => state.blocks[blockId])
|
||||
const blockConfig = blockState?.type ? getBlock(blockState.type) : null
|
||||
const canonicalIndex = useMemo(
|
||||
() => buildCanonicalIndex(blockConfig?.subBlocks || []),
|
||||
[blockConfig?.subBlocks]
|
||||
)
|
||||
const canonicalModeOverrides = blockState?.data?.canonicalModes
|
||||
|
||||
const blockValues = useSubBlockStore((state) => {
|
||||
if (!activeWorkflowId) return {}
|
||||
const workflowValues = state.workflowValues[activeWorkflowId] || {}
|
||||
return (workflowValues as Record<string, Record<string, unknown>>)[blockId] || {}
|
||||
})
|
||||
|
||||
const [domainValueFromStore] = useSubBlockValue(blockId, 'domain')
|
||||
|
||||
const connectedCredential = previewContextValues
|
||||
? resolvePreviewContextValue(previewContextValues.credential)
|
||||
: blockValues.credential
|
||||
const domainValue = previewContextValues
|
||||
? resolvePreviewContextValue(previewContextValues.domain)
|
||||
: domainValueFromStore
|
||||
|
||||
const teamIdValue = useMemo(
|
||||
() =>
|
||||
previewContextValues
|
||||
? resolvePreviewContextValue(previewContextValues.teamId)
|
||||
: resolveDependencyValue('teamId', blockValues, canonicalIndex, canonicalModeOverrides),
|
||||
[previewContextValues, blockValues, canonicalIndex, canonicalModeOverrides]
|
||||
)
|
||||
|
||||
const siteIdValue = useMemo(
|
||||
() =>
|
||||
previewContextValues
|
||||
? resolvePreviewContextValue(previewContextValues.siteId)
|
||||
: resolveDependencyValue('siteId', blockValues, canonicalIndex, canonicalModeOverrides),
|
||||
[previewContextValues, blockValues, canonicalIndex, canonicalModeOverrides]
|
||||
)
|
||||
|
||||
const collectionIdValue = useMemo(
|
||||
() =>
|
||||
previewContextValues
|
||||
? resolvePreviewContextValue(previewContextValues.collectionId)
|
||||
: resolveDependencyValue(
|
||||
'collectionId',
|
||||
blockValues,
|
||||
canonicalIndex,
|
||||
canonicalModeOverrides
|
||||
),
|
||||
[previewContextValues, blockValues, canonicalIndex, canonicalModeOverrides]
|
||||
)
|
||||
|
||||
const projectIdValue = useMemo(
|
||||
() =>
|
||||
previewContextValues
|
||||
? resolvePreviewContextValue(previewContextValues.projectId)
|
||||
: resolveDependencyValue('projectId', blockValues, canonicalIndex, canonicalModeOverrides),
|
||||
[previewContextValues, blockValues, canonicalIndex, canonicalModeOverrides]
|
||||
)
|
||||
|
||||
const planIdValue = useMemo(
|
||||
() =>
|
||||
previewContextValues
|
||||
? resolvePreviewContextValue(previewContextValues.planId)
|
||||
: resolveDependencyValue('planId', blockValues, canonicalIndex, canonicalModeOverrides),
|
||||
[previewContextValues, blockValues, canonicalIndex, canonicalModeOverrides]
|
||||
)
|
||||
|
||||
const normalizedCredentialId =
|
||||
typeof connectedCredential === 'string'
|
||||
? connectedCredential
|
||||
: typeof connectedCredential === 'object' && connectedCredential !== null
|
||||
? ((connectedCredential as Record<string, any>).id ?? '')
|
||||
: ''
|
||||
|
||||
const serviceId = subBlock.serviceId || ''
|
||||
const effectiveProviderId = useMemo(() => getProviderIdFromServiceId(serviceId), [serviceId])
|
||||
|
||||
const selectorResolution = useMemo<SelectorResolution | null>(() => {
|
||||
return resolveSelectorForSubBlock(subBlock, {
|
||||
workflowId: workflowIdFromUrl,
|
||||
credentialId: normalizedCredentialId,
|
||||
domain: (domainValue as string) || undefined,
|
||||
projectId: (projectIdValue as string) || undefined,
|
||||
planId: (planIdValue as string) || undefined,
|
||||
teamId: (teamIdValue as string) || undefined,
|
||||
siteId: (siteIdValue as string) || undefined,
|
||||
collectionId: (collectionIdValue as string) || undefined,
|
||||
})
|
||||
}, [
|
||||
subBlock,
|
||||
workflowIdFromUrl,
|
||||
normalizedCredentialId,
|
||||
domainValue,
|
||||
projectIdValue,
|
||||
planIdValue,
|
||||
teamIdValue,
|
||||
siteIdValue,
|
||||
collectionIdValue,
|
||||
])
|
||||
|
||||
const missingCredential = !normalizedCredentialId
|
||||
const missingDomain =
|
||||
selectorResolution?.key &&
|
||||
(selectorResolution.key === 'confluence.pages' || selectorResolution.key === 'jira.issues') &&
|
||||
!selectorResolution.context.domain
|
||||
const missingProject =
|
||||
selectorResolution?.key === 'jira.issues' &&
|
||||
isDependency(subBlock.dependsOn, 'projectId') &&
|
||||
!selectorResolution.context.projectId
|
||||
const missingPlan =
|
||||
selectorResolution?.key === 'microsoft.planner' && !selectorResolution.context.planId
|
||||
const missingSite =
|
||||
selectorResolution?.key === 'webflow.collections' && !selectorResolution.context.siteId
|
||||
const missingCollection =
|
||||
selectorResolution?.key === 'webflow.items' && !selectorResolution.context.collectionId
|
||||
|
||||
const disabledReason =
|
||||
finalDisabled ||
|
||||
missingCredential ||
|
||||
missingDomain ||
|
||||
missingProject ||
|
||||
missingPlan ||
|
||||
missingSite ||
|
||||
missingCollection ||
|
||||
!selectorResolution?.key
|
||||
|
||||
if (!selectorResolution?.key) {
|
||||
return (
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger asChild>
|
||||
<div className='w-full rounded border p-4 text-center text-muted-foreground text-sm'>
|
||||
File selector not supported for service: {serviceId || 'unknown'}
|
||||
</div>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content side='top'>
|
||||
<p>This file selector is not implemented for {serviceId || 'unknown'}</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<SelectorCombobox
|
||||
blockId={blockId}
|
||||
subBlock={subBlock}
|
||||
selectorKey={selectorResolution.key}
|
||||
selectorContext={selectorResolution.context}
|
||||
disabled={disabledReason}
|
||||
isPreview={isPreview}
|
||||
previewValue={previewValue ?? null}
|
||||
placeholder={subBlock.placeholder || 'Select resource'}
|
||||
allowSearch={selectorResolution.allowSearch}
|
||||
onOptionChange={(value) => {
|
||||
if (!isPreview) {
|
||||
collaborativeSetSubblockValue(blockId, subBlock.id, value)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -1,124 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { getProviderIdFromServiceId } from '@/lib/oauth'
|
||||
import { SelectorCombobox } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/selector-combobox/selector-combobox'
|
||||
import { useDependsOnGate } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-depends-on-gate'
|
||||
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
|
||||
import { resolvePreviewContextValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/utils'
|
||||
import type { SubBlockConfig } from '@/blocks/types'
|
||||
import { resolveSelectorForSubBlock } from '@/hooks/selectors/resolution'
|
||||
import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
|
||||
interface FolderSelectorInputProps {
|
||||
blockId: string
|
||||
subBlock: SubBlockConfig
|
||||
disabled?: boolean
|
||||
isPreview?: boolean
|
||||
previewValue?: any | null
|
||||
previewContextValues?: Record<string, unknown>
|
||||
}
|
||||
|
||||
export function FolderSelectorInput({
|
||||
blockId,
|
||||
subBlock,
|
||||
disabled = false,
|
||||
isPreview = false,
|
||||
previewValue,
|
||||
previewContextValues,
|
||||
}: FolderSelectorInputProps) {
|
||||
const [storeValue] = useSubBlockValue(blockId, subBlock.id)
|
||||
const [credentialFromStore] = useSubBlockValue(blockId, 'credential')
|
||||
const connectedCredential = previewContextValues
|
||||
? resolvePreviewContextValue(previewContextValues.credential)
|
||||
: credentialFromStore
|
||||
const { collaborativeSetSubblockValue } = useCollaborativeWorkflow()
|
||||
const { activeWorkflowId } = useWorkflowRegistry()
|
||||
const [selectedFolderId, setSelectedFolderId] = useState<string>('')
|
||||
|
||||
// Derive provider from serviceId using OAuth config (same pattern as credential-selector)
|
||||
const serviceId = subBlock.serviceId || ''
|
||||
const effectiveProviderId = useMemo(() => getProviderIdFromServiceId(serviceId), [serviceId])
|
||||
const providerKey = serviceId.toLowerCase()
|
||||
|
||||
const isCopyDestinationSelector =
|
||||
subBlock.canonicalParamId === 'copyDestinationId' ||
|
||||
subBlock.id === 'copyDestinationFolder' ||
|
||||
subBlock.id === 'manualCopyDestinationFolder'
|
||||
|
||||
// Central dependsOn gating
|
||||
const { finalDisabled } = useDependsOnGate(blockId, subBlock, {
|
||||
disabled,
|
||||
isPreview,
|
||||
previewContextValues,
|
||||
})
|
||||
|
||||
// Get the current value from the store or prop value if in preview mode
|
||||
useEffect(() => {
|
||||
if (finalDisabled) return
|
||||
if (isPreview && previewValue !== undefined) {
|
||||
setSelectedFolderId(previewValue)
|
||||
return
|
||||
}
|
||||
const current = storeValue as string | undefined
|
||||
if (current) {
|
||||
setSelectedFolderId(current)
|
||||
return
|
||||
}
|
||||
const shouldDefaultInbox = providerKey === 'gmail' && !isCopyDestinationSelector
|
||||
if (shouldDefaultInbox) {
|
||||
setSelectedFolderId('INBOX')
|
||||
if (!isPreview) {
|
||||
collaborativeSetSubblockValue(blockId, subBlock.id, 'INBOX')
|
||||
}
|
||||
}
|
||||
}, [
|
||||
blockId,
|
||||
subBlock.id,
|
||||
storeValue,
|
||||
collaborativeSetSubblockValue,
|
||||
isPreview,
|
||||
previewValue,
|
||||
finalDisabled,
|
||||
providerKey,
|
||||
isCopyDestinationSelector,
|
||||
])
|
||||
|
||||
const credentialId = (connectedCredential as string) || ''
|
||||
const missingCredential = credentialId.length === 0
|
||||
const selectorResolution = useMemo(
|
||||
() =>
|
||||
resolveSelectorForSubBlock(subBlock, {
|
||||
credentialId: credentialId || undefined,
|
||||
workflowId: activeWorkflowId || undefined,
|
||||
}),
|
||||
[subBlock, credentialId, activeWorkflowId]
|
||||
)
|
||||
|
||||
const handleChange = useCallback(
|
||||
(value: string) => {
|
||||
setSelectedFolderId(value)
|
||||
if (!isPreview) {
|
||||
collaborativeSetSubblockValue(blockId, subBlock.id, value)
|
||||
}
|
||||
},
|
||||
[blockId, subBlock.id, collaborativeSetSubblockValue, isPreview]
|
||||
)
|
||||
|
||||
return (
|
||||
<SelectorCombobox
|
||||
blockId={blockId}
|
||||
subBlock={subBlock}
|
||||
selectorKey={selectorResolution?.key ?? 'gmail.labels'}
|
||||
selectorContext={
|
||||
selectorResolution?.context ?? { credentialId, workflowId: activeWorkflowId || '' }
|
||||
}
|
||||
disabled={finalDisabled || missingCredential || !selectorResolution?.key}
|
||||
isPreview={isPreview}
|
||||
previewValue={previewValue ?? null}
|
||||
placeholder={subBlock.placeholder || 'Select folder'}
|
||||
onOptionChange={handleChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -3,14 +3,11 @@ export { Code } from './code/code'
|
||||
export { ComboBox } from './combobox/combobox'
|
||||
export { ConditionInput } from './condition-input/condition-input'
|
||||
export { CredentialSelector } from './credential-selector/credential-selector'
|
||||
export { DocumentSelector } from './document-selector/document-selector'
|
||||
export { DocumentTagEntry } from './document-tag-entry/document-tag-entry'
|
||||
export { Dropdown } from './dropdown/dropdown'
|
||||
export { EvalInput } from './eval-input/eval-input'
|
||||
export { FileSelectorInput } from './file-selector/file-selector-input'
|
||||
export { FileUpload } from './file-upload/file-upload'
|
||||
export { FilterBuilder } from './filter-builder/filter-builder'
|
||||
export { FolderSelectorInput } from './folder-selector/components/folder-selector-input'
|
||||
export { GroupedCheckboxList } from './grouped-checkbox-list/grouped-checkbox-list'
|
||||
export { InputMapping } from './input-mapping/input-mapping'
|
||||
export { KnowledgeBaseSelector } from './knowledge-base-selector/knowledge-base-selector'
|
||||
@@ -20,13 +17,11 @@ export { McpDynamicArgs } from './mcp-dynamic-args/mcp-dynamic-args'
|
||||
export { McpServerSelector } from './mcp-server-modal/mcp-server-selector'
|
||||
export { McpToolSelector } from './mcp-server-modal/mcp-tool-selector'
|
||||
export { MessagesInput } from './messages-input/messages-input'
|
||||
export { ProjectSelectorInput } from './project-selector/project-selector-input'
|
||||
export { ResponseFormat } from './response/response-format'
|
||||
export { ScheduleInfo } from './schedule-info/schedule-info'
|
||||
export { SheetSelectorInput } from './sheet-selector/sheet-selector-input'
|
||||
export { SelectorInput, type SelectorOverrides } from './selector-input/selector-input'
|
||||
export { ShortInput } from './short-input/short-input'
|
||||
export { SkillInput } from './skill-input/skill-input'
|
||||
export { SlackSelectorInput } from './slack-selector/slack-selector-input'
|
||||
export { SliderInput } from './slider-input/slider-input'
|
||||
export { SortBuilder } from './sort-builder/sort-builder'
|
||||
export { InputFormat } from './starter/input-format'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { useMemo, useRef } from 'react'
|
||||
import { useRef } from 'react'
|
||||
import { Plus } from 'lucide-react'
|
||||
import {
|
||||
Badge,
|
||||
@@ -14,19 +14,14 @@ import {
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
import { FIELD_TYPE_LABELS, getPlaceholderForFieldType } from '@/lib/knowledge/constants'
|
||||
import { type FilterFieldType, getOperatorsForFieldType } from '@/lib/knowledge/filters/types'
|
||||
import { buildCanonicalIndex, resolveDependencyValue } from '@/lib/workflows/subblocks/visibility'
|
||||
import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/formatted-text'
|
||||
import { TagDropdown } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown'
|
||||
import { useDependsOnGate } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-depends-on-gate'
|
||||
import { useSubBlockInput } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-input'
|
||||
import { resolvePreviewContextValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/utils'
|
||||
import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes'
|
||||
import { getBlock } from '@/blocks/registry'
|
||||
import type { SubBlockConfig } from '@/blocks/types'
|
||||
import { useKnowledgeBaseTagDefinitions } from '@/hooks/kb/use-knowledge-base-tag-definitions'
|
||||
import { useTagSelection } from '@/hooks/kb/use-tag-selection'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
|
||||
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
|
||||
import { useSubBlockValue } from '../../hooks/use-sub-block-value'
|
||||
|
||||
interface TagFilter {
|
||||
@@ -69,38 +64,17 @@ export function KnowledgeTagFilters({
|
||||
previewValue,
|
||||
previewContextValues,
|
||||
}: KnowledgeTagFiltersProps) {
|
||||
const { activeWorkflowId } = useWorkflowRegistry()
|
||||
const [storeValue, setStoreValue] = useSubBlockValue<string | null>(blockId, subBlock.id)
|
||||
const emitTagSelection = useTagSelection(blockId, subBlock.id)
|
||||
const valueInputRefs = useRef<Record<string, HTMLInputElement>>({})
|
||||
const overlayRefs = useRef<Record<string, HTMLDivElement>>({})
|
||||
|
||||
const blockState = useWorkflowStore((state) => state.blocks[blockId])
|
||||
const blockConfig = blockState?.type ? getBlock(blockState.type) : null
|
||||
const canonicalIndex = useMemo(
|
||||
() => buildCanonicalIndex(blockConfig?.subBlocks || []),
|
||||
[blockConfig?.subBlocks]
|
||||
)
|
||||
const canonicalModeOverrides = blockState?.data?.canonicalModes
|
||||
|
||||
const blockValues = useSubBlockStore((state) => {
|
||||
if (!activeWorkflowId) return {}
|
||||
const workflowValues = state.workflowValues[activeWorkflowId] || {}
|
||||
return (workflowValues as Record<string, Record<string, unknown>>)[blockId] || {}
|
||||
const { dependencyValues } = useDependsOnGate(blockId, subBlock, {
|
||||
disabled,
|
||||
isPreview,
|
||||
previewContextValues,
|
||||
})
|
||||
|
||||
const knowledgeBaseIdValue = useMemo(
|
||||
() =>
|
||||
previewContextValues
|
||||
? resolvePreviewContextValue(previewContextValues.knowledgeBaseId)
|
||||
: resolveDependencyValue(
|
||||
'knowledgeBaseId',
|
||||
blockValues,
|
||||
canonicalIndex,
|
||||
canonicalModeOverrides
|
||||
),
|
||||
[previewContextValues, blockValues, canonicalIndex, canonicalModeOverrides]
|
||||
)
|
||||
const knowledgeBaseIdValue = dependencyValues.knowledgeBaseSelector
|
||||
const knowledgeBaseId =
|
||||
typeof knowledgeBaseIdValue === 'string' && knowledgeBaseIdValue.trim().length > 0
|
||||
? knowledgeBaseIdValue
|
||||
|
||||
@@ -1,140 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useParams } from 'next/navigation'
|
||||
import { Tooltip } from '@/components/emcn'
|
||||
import { getProviderIdFromServiceId } from '@/lib/oauth'
|
||||
import { buildCanonicalIndex, resolveDependencyValue } from '@/lib/workflows/subblocks/visibility'
|
||||
import { SelectorCombobox } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/selector-combobox/selector-combobox'
|
||||
import { useDependsOnGate } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-depends-on-gate'
|
||||
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
|
||||
import { resolvePreviewContextValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/utils'
|
||||
import { getBlock } from '@/blocks/registry'
|
||||
import type { SubBlockConfig } from '@/blocks/types'
|
||||
import { resolveSelectorForSubBlock } from '@/hooks/selectors/resolution'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
|
||||
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
|
||||
|
||||
interface ProjectSelectorInputProps {
|
||||
blockId: string
|
||||
subBlock: SubBlockConfig
|
||||
disabled?: boolean
|
||||
onProjectSelect?: (projectId: string) => void
|
||||
isPreview?: boolean
|
||||
previewValue?: any | null
|
||||
previewContextValues?: Record<string, any>
|
||||
}
|
||||
|
||||
export function ProjectSelectorInput({
|
||||
blockId,
|
||||
subBlock,
|
||||
disabled = false,
|
||||
onProjectSelect,
|
||||
isPreview = false,
|
||||
previewValue,
|
||||
previewContextValues,
|
||||
}: ProjectSelectorInputProps) {
|
||||
const params = useParams()
|
||||
const activeWorkflowId = useWorkflowRegistry((s) => s.activeWorkflowId) as string | null
|
||||
const [selectedProjectId, setSelectedProjectId] = useState<string>('')
|
||||
const [storeValue] = useSubBlockValue(blockId, subBlock.id)
|
||||
const [jiraDomainFromStore] = useSubBlockValue(blockId, 'domain')
|
||||
|
||||
const blockState = useWorkflowStore((state) => state.blocks[blockId])
|
||||
const blockConfig = blockState?.type ? getBlock(blockState.type) : null
|
||||
const canonicalIndex = useMemo(
|
||||
() => buildCanonicalIndex(blockConfig?.subBlocks || []),
|
||||
[blockConfig?.subBlocks]
|
||||
)
|
||||
const canonicalModeOverrides = blockState?.data?.canonicalModes
|
||||
|
||||
const blockValues = useSubBlockStore((state) => {
|
||||
if (!activeWorkflowId) return {}
|
||||
const workflowValues = state.workflowValues[activeWorkflowId] || {}
|
||||
return (workflowValues as Record<string, Record<string, unknown>>)[blockId] || {}
|
||||
})
|
||||
|
||||
const connectedCredential = previewContextValues
|
||||
? resolvePreviewContextValue(previewContextValues.credential)
|
||||
: blockValues.credential
|
||||
const jiraDomain = previewContextValues
|
||||
? resolvePreviewContextValue(previewContextValues.domain)
|
||||
: jiraDomainFromStore
|
||||
|
||||
const linearTeamId = useMemo(
|
||||
() =>
|
||||
previewContextValues
|
||||
? resolvePreviewContextValue(previewContextValues.teamId)
|
||||
: resolveDependencyValue('teamId', blockValues, canonicalIndex, canonicalModeOverrides),
|
||||
[previewContextValues, blockValues, canonicalIndex, canonicalModeOverrides]
|
||||
)
|
||||
|
||||
const serviceId = subBlock.serviceId || ''
|
||||
const effectiveProviderId = useMemo(() => getProviderIdFromServiceId(serviceId), [serviceId])
|
||||
const workflowIdFromUrl = (params?.workflowId as string) || activeWorkflowId || ''
|
||||
const { finalDisabled } = useDependsOnGate(blockId, subBlock, {
|
||||
disabled,
|
||||
isPreview,
|
||||
previewContextValues,
|
||||
})
|
||||
|
||||
const domain = (jiraDomain as string) || ''
|
||||
|
||||
useEffect(() => {
|
||||
if (isPreview && previewValue !== undefined) {
|
||||
setSelectedProjectId(previewValue)
|
||||
} else if (typeof storeValue === 'string') {
|
||||
setSelectedProjectId(storeValue)
|
||||
} else {
|
||||
setSelectedProjectId('')
|
||||
}
|
||||
}, [isPreview, previewValue, storeValue])
|
||||
|
||||
const selectorResolution = useMemo(() => {
|
||||
return resolveSelectorForSubBlock(subBlock, {
|
||||
workflowId: workflowIdFromUrl || undefined,
|
||||
credentialId: (connectedCredential as string) || undefined,
|
||||
domain,
|
||||
teamId: (linearTeamId as string) || undefined,
|
||||
})
|
||||
}, [subBlock, workflowIdFromUrl, connectedCredential, domain, linearTeamId])
|
||||
|
||||
const missingCredential = !selectorResolution?.context.credentialId
|
||||
|
||||
const handleChange = (value: string) => {
|
||||
setSelectedProjectId(value)
|
||||
onProjectSelect?.(value)
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger asChild>
|
||||
<div className='w-full'>
|
||||
{selectorResolution?.key ? (
|
||||
<SelectorCombobox
|
||||
blockId={blockId}
|
||||
subBlock={subBlock}
|
||||
selectorKey={selectorResolution.key}
|
||||
selectorContext={selectorResolution.context}
|
||||
disabled={finalDisabled || missingCredential}
|
||||
isPreview={isPreview}
|
||||
previewValue={previewValue ?? null}
|
||||
placeholder={subBlock.placeholder || 'Select project'}
|
||||
onOptionChange={handleChange}
|
||||
/>
|
||||
) : (
|
||||
<div className='w-full rounded border p-4 text-center text-muted-foreground text-sm'>
|
||||
Project selector not supported for service: {serviceId}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Tooltip.Trigger>
|
||||
{missingCredential && (
|
||||
<Tooltip.Content side='top'>
|
||||
<p>Please select an account first</p>
|
||||
</Tooltip.Content>
|
||||
)}
|
||||
</Tooltip.Root>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { Tooltip } from '@/components/emcn'
|
||||
import { SelectorCombobox } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/selector-combobox/selector-combobox'
|
||||
import { useSelectorSetup } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-selector-setup'
|
||||
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
|
||||
import type { SubBlockConfig } from '@/blocks/types'
|
||||
import type { SelectorContext } from '@/hooks/selectors/types'
|
||||
import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow'
|
||||
|
||||
export interface SelectorOverrides {
|
||||
transformContext?: (context: SelectorContext, deps: Record<string, unknown>) => SelectorContext
|
||||
getDefaultValue?: (subBlock: SubBlockConfig) => string | null
|
||||
}
|
||||
|
||||
interface SelectorInputProps {
|
||||
blockId: string
|
||||
subBlock: SubBlockConfig
|
||||
disabled?: boolean
|
||||
isPreview?: boolean
|
||||
previewValue?: any
|
||||
previewContextValues?: Record<string, any>
|
||||
overrides?: SelectorOverrides
|
||||
}
|
||||
|
||||
export function SelectorInput({
|
||||
blockId,
|
||||
subBlock,
|
||||
disabled = false,
|
||||
isPreview = false,
|
||||
previewValue,
|
||||
previewContextValues,
|
||||
overrides,
|
||||
}: SelectorInputProps) {
|
||||
const { collaborativeSetSubblockValue } = useCollaborativeWorkflow()
|
||||
const [storeValue] = useSubBlockValue(blockId, subBlock.id)
|
||||
const defaultAppliedRef = useRef(false)
|
||||
|
||||
const {
|
||||
selectorKey,
|
||||
selectorContext: autoContext,
|
||||
allowSearch,
|
||||
disabled: selectorDisabled,
|
||||
dependencyValues,
|
||||
} = useSelectorSetup(blockId, subBlock, { disabled, isPreview, previewContextValues })
|
||||
|
||||
const selectorContext = overrides?.transformContext
|
||||
? overrides.transformContext(autoContext, dependencyValues)
|
||||
: autoContext
|
||||
|
||||
useEffect(() => {
|
||||
if (defaultAppliedRef.current || isPreview || selectorDisabled) return
|
||||
if (storeValue) return
|
||||
|
||||
const defaultValue = overrides?.getDefaultValue?.(subBlock)
|
||||
if (defaultValue) {
|
||||
defaultAppliedRef.current = true
|
||||
collaborativeSetSubblockValue(blockId, subBlock.id, defaultValue)
|
||||
}
|
||||
}, [
|
||||
blockId,
|
||||
subBlock,
|
||||
storeValue,
|
||||
isPreview,
|
||||
selectorDisabled,
|
||||
overrides,
|
||||
collaborativeSetSubblockValue,
|
||||
])
|
||||
|
||||
const serviceId = subBlock.serviceId || 'unknown'
|
||||
|
||||
if (!selectorKey) {
|
||||
return (
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger asChild>
|
||||
<div className='w-full rounded border p-4 text-center text-muted-foreground text-sm'>
|
||||
Selector not supported for service: {serviceId}
|
||||
</div>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content side='top'>
|
||||
<p>This selector is not implemented for {serviceId}</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<SelectorCombobox
|
||||
blockId={blockId}
|
||||
subBlock={subBlock}
|
||||
selectorKey={selectorKey}
|
||||
selectorContext={selectorContext}
|
||||
disabled={selectorDisabled}
|
||||
isPreview={isPreview}
|
||||
previewValue={previewValue ?? null}
|
||||
placeholder={subBlock.placeholder || 'Select resource'}
|
||||
allowSearch={allowSearch}
|
||||
onOptionChange={(value) => {
|
||||
if (!isPreview) {
|
||||
collaborativeSetSubblockValue(blockId, subBlock.id, value)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -1,136 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { useMemo } from 'react'
|
||||
import { useParams } from 'next/navigation'
|
||||
import { Tooltip } from '@/components/emcn'
|
||||
import { getProviderIdFromServiceId } from '@/lib/oauth'
|
||||
import { buildCanonicalIndex, resolveDependencyValue } from '@/lib/workflows/subblocks/visibility'
|
||||
import { SelectorCombobox } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/selector-combobox/selector-combobox'
|
||||
import { useDependsOnGate } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-depends-on-gate'
|
||||
import { resolvePreviewContextValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/utils'
|
||||
import { getBlock } from '@/blocks/registry'
|
||||
import type { SubBlockConfig } from '@/blocks/types'
|
||||
import { resolveSelectorForSubBlock, type SelectorResolution } from '@/hooks/selectors/resolution'
|
||||
import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
|
||||
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
|
||||
|
||||
interface SheetSelectorInputProps {
|
||||
blockId: string
|
||||
subBlock: SubBlockConfig
|
||||
disabled: boolean
|
||||
isPreview?: boolean
|
||||
previewValue?: any | null
|
||||
previewContextValues?: Record<string, any>
|
||||
}
|
||||
|
||||
export function SheetSelectorInput({
|
||||
blockId,
|
||||
subBlock,
|
||||
disabled,
|
||||
isPreview = false,
|
||||
previewValue,
|
||||
previewContextValues,
|
||||
}: SheetSelectorInputProps) {
|
||||
const { collaborativeSetSubblockValue } = useCollaborativeWorkflow()
|
||||
const { activeWorkflowId } = useWorkflowRegistry()
|
||||
const params = useParams()
|
||||
const workflowIdFromUrl = (params?.workflowId as string) || activeWorkflowId || ''
|
||||
|
||||
const { finalDisabled } = useDependsOnGate(blockId, subBlock, {
|
||||
disabled,
|
||||
isPreview,
|
||||
previewContextValues,
|
||||
})
|
||||
|
||||
const blockState = useWorkflowStore((state) => state.blocks[blockId])
|
||||
const blockConfig = blockState?.type ? getBlock(blockState.type) : null
|
||||
const canonicalIndex = useMemo(
|
||||
() => buildCanonicalIndex(blockConfig?.subBlocks || []),
|
||||
[blockConfig?.subBlocks]
|
||||
)
|
||||
const canonicalModeOverrides = blockState?.data?.canonicalModes
|
||||
|
||||
const blockValues = useSubBlockStore((state) => {
|
||||
if (!activeWorkflowId) return {}
|
||||
const workflowValues = state.workflowValues[activeWorkflowId] || {}
|
||||
return (workflowValues as Record<string, Record<string, unknown>>)[blockId] || {}
|
||||
})
|
||||
|
||||
const connectedCredentialFromStore = blockValues.credential
|
||||
|
||||
const spreadsheetIdFromStore = useMemo(
|
||||
() =>
|
||||
resolveDependencyValue('spreadsheetId', blockValues, canonicalIndex, canonicalModeOverrides),
|
||||
[blockValues, canonicalIndex, canonicalModeOverrides]
|
||||
)
|
||||
|
||||
const connectedCredential = previewContextValues
|
||||
? resolvePreviewContextValue(previewContextValues.credential)
|
||||
: connectedCredentialFromStore
|
||||
const spreadsheetId = previewContextValues
|
||||
? (resolvePreviewContextValue(previewContextValues.spreadsheetId) ??
|
||||
resolvePreviewContextValue(previewContextValues.manualSpreadsheetId))
|
||||
: spreadsheetIdFromStore
|
||||
|
||||
const normalizedCredentialId =
|
||||
typeof connectedCredential === 'string'
|
||||
? connectedCredential
|
||||
: typeof connectedCredential === 'object' && connectedCredential !== null
|
||||
? ((connectedCredential as Record<string, any>).id ?? '')
|
||||
: ''
|
||||
|
||||
const normalizedSpreadsheetId = typeof spreadsheetId === 'string' ? spreadsheetId.trim() : ''
|
||||
|
||||
const serviceId = subBlock.serviceId || ''
|
||||
const effectiveProviderId = useMemo(() => getProviderIdFromServiceId(serviceId), [serviceId])
|
||||
|
||||
const selectorResolution = useMemo<SelectorResolution | null>(() => {
|
||||
return resolveSelectorForSubBlock(subBlock, {
|
||||
workflowId: workflowIdFromUrl,
|
||||
credentialId: normalizedCredentialId,
|
||||
spreadsheetId: normalizedSpreadsheetId,
|
||||
})
|
||||
}, [subBlock, workflowIdFromUrl, normalizedCredentialId, normalizedSpreadsheetId])
|
||||
|
||||
const missingCredential = !normalizedCredentialId
|
||||
const missingSpreadsheet = !normalizedSpreadsheetId
|
||||
|
||||
const disabledReason =
|
||||
finalDisabled || missingCredential || missingSpreadsheet || !selectorResolution?.key
|
||||
|
||||
if (!selectorResolution?.key) {
|
||||
return (
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger asChild>
|
||||
<div className='w-full rounded border p-4 text-center text-muted-foreground text-sm'>
|
||||
Sheet selector not supported for service: {serviceId || 'unknown'}
|
||||
</div>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content side='top'>
|
||||
<p>This sheet selector is not implemented for {serviceId || 'unknown'}</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<SelectorCombobox
|
||||
blockId={blockId}
|
||||
subBlock={subBlock}
|
||||
selectorKey={selectorResolution.key}
|
||||
selectorContext={selectorResolution.context}
|
||||
disabled={disabledReason}
|
||||
isPreview={isPreview}
|
||||
previewValue={previewValue ?? null}
|
||||
placeholder={subBlock.placeholder || 'Select sheet'}
|
||||
allowSearch={selectorResolution.allowSearch}
|
||||
onOptionChange={(value) => {
|
||||
if (!isPreview) {
|
||||
collaborativeSetSubblockValue(blockId, subBlock.id, value)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -1,148 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useParams } from 'next/navigation'
|
||||
import { Tooltip } from '@/components/emcn'
|
||||
import { getProviderIdFromServiceId } from '@/lib/oauth'
|
||||
import { SelectorCombobox } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/selector-combobox/selector-combobox'
|
||||
import { useDependsOnGate } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-depends-on-gate'
|
||||
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
|
||||
import { resolvePreviewContextValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/utils'
|
||||
import type { SubBlockConfig } from '@/blocks/types'
|
||||
import type { SelectorContext, SelectorKey } from '@/hooks/selectors/types'
|
||||
|
||||
type SlackSelectorType = 'channel-selector' | 'user-selector'
|
||||
|
||||
const SELECTOR_CONFIG: Record<
|
||||
SlackSelectorType,
|
||||
{ selectorKey: SelectorKey; placeholder: string; label: string }
|
||||
> = {
|
||||
'channel-selector': {
|
||||
selectorKey: 'slack.channels',
|
||||
placeholder: 'Select Slack channel',
|
||||
label: 'Channel',
|
||||
},
|
||||
'user-selector': {
|
||||
selectorKey: 'slack.users',
|
||||
placeholder: 'Select Slack user',
|
||||
label: 'User',
|
||||
},
|
||||
}
|
||||
|
||||
interface SlackSelectorInputProps {
|
||||
blockId: string
|
||||
subBlock: SubBlockConfig
|
||||
disabled?: boolean
|
||||
onSelect?: (value: string) => void
|
||||
isPreview?: boolean
|
||||
previewValue?: any | null
|
||||
previewContextValues?: Record<string, any>
|
||||
}
|
||||
|
||||
export function SlackSelectorInput({
|
||||
blockId,
|
||||
subBlock,
|
||||
disabled = false,
|
||||
onSelect,
|
||||
isPreview = false,
|
||||
previewValue,
|
||||
previewContextValues,
|
||||
}: SlackSelectorInputProps) {
|
||||
const selectorType = subBlock.type as SlackSelectorType
|
||||
const config = SELECTOR_CONFIG[selectorType]
|
||||
|
||||
const params = useParams()
|
||||
const workflowIdFromUrl = (params?.workflowId as string) || ''
|
||||
const [storeValue] = useSubBlockValue(blockId, subBlock.id)
|
||||
const [authMethod] = useSubBlockValue(blockId, 'authMethod')
|
||||
const [botToken] = useSubBlockValue(blockId, 'botToken')
|
||||
const [connectedCredential] = useSubBlockValue(blockId, 'credential')
|
||||
|
||||
const effectiveAuthMethod = previewContextValues
|
||||
? resolvePreviewContextValue(previewContextValues.authMethod)
|
||||
: authMethod
|
||||
const effectiveBotToken = previewContextValues
|
||||
? resolvePreviewContextValue(previewContextValues.botToken)
|
||||
: botToken
|
||||
const effectiveCredential = previewContextValues
|
||||
? resolvePreviewContextValue(previewContextValues.credential)
|
||||
: connectedCredential
|
||||
const [_selectedValue, setSelectedValue] = useState<string | null>(null)
|
||||
|
||||
const serviceId = subBlock.serviceId || ''
|
||||
const effectiveProviderId = useMemo(() => getProviderIdFromServiceId(serviceId), [serviceId])
|
||||
const isSlack = serviceId === 'slack'
|
||||
|
||||
const { finalDisabled, dependsOn } = useDependsOnGate(blockId, subBlock, {
|
||||
disabled,
|
||||
isPreview,
|
||||
previewContextValues,
|
||||
})
|
||||
|
||||
const credential: string =
|
||||
(effectiveAuthMethod as string) === 'bot_token'
|
||||
? (effectiveBotToken as string) || ''
|
||||
: (effectiveCredential as string) || ''
|
||||
|
||||
useEffect(() => {
|
||||
const val = isPreview && previewValue !== undefined ? previewValue : storeValue
|
||||
if (typeof val === 'string') {
|
||||
setSelectedValue(val)
|
||||
}
|
||||
}, [isPreview, previewValue, storeValue])
|
||||
|
||||
const requiresCredential = dependsOn.includes('credential')
|
||||
const missingCredential = !credential || credential.trim().length === 0
|
||||
const shouldForceDisable = requiresCredential && missingCredential
|
||||
|
||||
const context: SelectorContext = useMemo(
|
||||
() => ({
|
||||
credentialId: credential,
|
||||
workflowId: workflowIdFromUrl,
|
||||
}),
|
||||
[credential, workflowIdFromUrl]
|
||||
)
|
||||
|
||||
if (!isSlack) {
|
||||
return (
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger asChild>
|
||||
<div className='w-full rounded border p-4 text-center text-muted-foreground text-sm'>
|
||||
{config.label} selector not supported for service: {serviceId || 'unknown'}
|
||||
</div>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content side='top'>
|
||||
<p>
|
||||
This {config.label.toLowerCase()} selector is not yet implemented for{' '}
|
||||
{serviceId || 'unknown'}
|
||||
</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger asChild>
|
||||
<div className='w-full'>
|
||||
<SelectorCombobox
|
||||
blockId={blockId}
|
||||
subBlock={subBlock}
|
||||
selectorKey={config.selectorKey}
|
||||
selectorContext={context}
|
||||
disabled={finalDisabled || shouldForceDisable}
|
||||
isPreview={isPreview}
|
||||
previewValue={previewValue ?? null}
|
||||
placeholder={subBlock.placeholder || config.placeholder}
|
||||
onOptionChange={(value) => {
|
||||
setSelectedValue(value)
|
||||
if (!isPreview) {
|
||||
onSelect?.(value)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Tooltip.Trigger>
|
||||
</Tooltip.Root>
|
||||
)
|
||||
}
|
||||
@@ -177,5 +177,7 @@ export function useDependsOnGate(
|
||||
depsSatisfied,
|
||||
blocked,
|
||||
finalDisabled,
|
||||
dependencyValues: dependencyValuesMap,
|
||||
canonicalIndex,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
'use client'
|
||||
|
||||
import { useMemo } from 'react'
|
||||
import { useParams } from 'next/navigation'
|
||||
import type { SubBlockConfig } from '@/blocks/types'
|
||||
import type { SelectorContext, SelectorKey } from '@/hooks/selectors/types'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
import { useDependsOnGate } from './use-depends-on-gate'
|
||||
|
||||
/**
|
||||
* Resolves all selector configuration from a sub-block's declarative properties.
|
||||
*
|
||||
* Builds a `SelectorContext` by mapping each `dependsOn` entry through the
|
||||
* canonical index to its `canonicalParamId`, which maps directly to
|
||||
* `SelectorContext` field names (e.g. `siteId`, `teamId`, `collectionId`).
|
||||
* The one special case is `oauthCredential` which maps to `credentialId`.
|
||||
*
|
||||
* @param blockId - The block containing the selector sub-block
|
||||
* @param subBlock - The sub-block config (must have `selectorKey` set)
|
||||
* @param opts - Standard disabled/preview/previewContextValues options
|
||||
* @returns Everything `SelectorCombobox` needs: key, context, disabled, allowSearch, plus raw dependency values
|
||||
*/
|
||||
export function useSelectorSetup(
|
||||
blockId: string,
|
||||
subBlock: SubBlockConfig,
|
||||
opts?: { disabled?: boolean; isPreview?: boolean; previewContextValues?: Record<string, any> }
|
||||
) {
|
||||
const params = useParams()
|
||||
const activeWorkflowId = useWorkflowRegistry((s) => s.activeWorkflowId)
|
||||
const workflowId = (params?.workflowId as string) || activeWorkflowId || ''
|
||||
|
||||
const { finalDisabled, dependencyValues, canonicalIndex } = useDependsOnGate(
|
||||
blockId,
|
||||
subBlock,
|
||||
opts
|
||||
)
|
||||
|
||||
const selectorContext = useMemo<SelectorContext>(() => {
|
||||
const context: SelectorContext = {
|
||||
workflowId,
|
||||
mimeType: subBlock.mimeType,
|
||||
}
|
||||
|
||||
for (const [depKey, value] of Object.entries(dependencyValues)) {
|
||||
if (value === null || value === undefined) continue
|
||||
const strValue = String(value)
|
||||
if (!strValue) continue
|
||||
|
||||
const canonicalParamId = canonicalIndex.canonicalIdBySubBlockId[depKey] ?? depKey
|
||||
|
||||
if (canonicalParamId === 'oauthCredential') {
|
||||
context.credentialId = strValue
|
||||
} else if (canonicalParamId in CONTEXT_FIELD_SET) {
|
||||
;(context as Record<string, unknown>)[canonicalParamId] = strValue
|
||||
}
|
||||
}
|
||||
|
||||
return context
|
||||
}, [dependencyValues, canonicalIndex, workflowId, subBlock.mimeType])
|
||||
|
||||
return {
|
||||
selectorKey: (subBlock.selectorKey ?? null) as SelectorKey | null,
|
||||
selectorContext,
|
||||
allowSearch: subBlock.selectorAllowSearch ?? true,
|
||||
disabled: finalDisabled || !subBlock.selectorKey,
|
||||
dependencyValues,
|
||||
}
|
||||
}
|
||||
|
||||
const CONTEXT_FIELD_SET: Record<string, true> = {
|
||||
credentialId: true,
|
||||
domain: true,
|
||||
teamId: true,
|
||||
projectId: true,
|
||||
knowledgeBaseId: true,
|
||||
planId: true,
|
||||
siteId: true,
|
||||
collectionId: true,
|
||||
spreadsheetId: true,
|
||||
fileId: true,
|
||||
}
|
||||
@@ -18,14 +18,11 @@ import {
|
||||
ComboBox,
|
||||
ConditionInput,
|
||||
CredentialSelector,
|
||||
DocumentSelector,
|
||||
DocumentTagEntry,
|
||||
Dropdown,
|
||||
EvalInput,
|
||||
FileSelectorInput,
|
||||
FileUpload,
|
||||
FilterBuilder,
|
||||
FolderSelectorInput,
|
||||
GroupedCheckboxList,
|
||||
InputFormat,
|
||||
InputMapping,
|
||||
@@ -36,13 +33,12 @@ import {
|
||||
McpServerSelector,
|
||||
McpToolSelector,
|
||||
MessagesInput,
|
||||
ProjectSelectorInput,
|
||||
ResponseFormat,
|
||||
ScheduleInfo,
|
||||
SheetSelectorInput,
|
||||
SelectorInput,
|
||||
type SelectorOverrides,
|
||||
ShortInput,
|
||||
SkillInput,
|
||||
SlackSelectorInput,
|
||||
SliderInput,
|
||||
SortBuilder,
|
||||
Switch,
|
||||
@@ -58,6 +54,23 @@ import { useDependsOnGate } from '@/app/workspace/[workspaceId]/w/[workflowId]/c
|
||||
import type { SubBlockConfig } from '@/blocks/types'
|
||||
import { useWebhookManagement } from '@/hooks/use-webhook-management'
|
||||
|
||||
const SLACK_OVERRIDES: SelectorOverrides = {
|
||||
transformContext: (context, deps) => {
|
||||
const authMethod = deps.authMethod as string
|
||||
const credentialId =
|
||||
authMethod === 'bot_token' ? String(deps.botToken ?? '') : String(deps.credential ?? '')
|
||||
return { ...context, credentialId }
|
||||
},
|
||||
}
|
||||
|
||||
const FOLDER_OVERRIDES: SelectorOverrides = {
|
||||
getDefaultValue: (subBlock) => {
|
||||
const isGmail = subBlock.serviceId === 'gmail'
|
||||
const isCopyDest = subBlock.canonicalParamId === 'copyDestinationId'
|
||||
return isGmail && !isCopyDest ? 'INBOX' : null
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for wand control handlers exposed by sub-block inputs
|
||||
*/
|
||||
@@ -901,32 +914,10 @@ function SubBlockComponent({
|
||||
)
|
||||
|
||||
case 'file-selector':
|
||||
return (
|
||||
<FileSelectorInput
|
||||
blockId={blockId}
|
||||
subBlock={config}
|
||||
disabled={isDisabled}
|
||||
isPreview={isPreview}
|
||||
previewValue={previewValue}
|
||||
previewContextValues={contextValues}
|
||||
/>
|
||||
)
|
||||
|
||||
case 'sheet-selector':
|
||||
return (
|
||||
<SheetSelectorInput
|
||||
blockId={blockId}
|
||||
subBlock={config}
|
||||
disabled={isDisabled}
|
||||
isPreview={isPreview}
|
||||
previewValue={previewValue}
|
||||
previewContextValues={contextValues}
|
||||
/>
|
||||
)
|
||||
|
||||
case 'project-selector':
|
||||
return (
|
||||
<ProjectSelectorInput
|
||||
<SelectorInput
|
||||
blockId={blockId}
|
||||
subBlock={config}
|
||||
disabled={isDisabled}
|
||||
@@ -938,13 +929,14 @@ function SubBlockComponent({
|
||||
|
||||
case 'folder-selector':
|
||||
return (
|
||||
<FolderSelectorInput
|
||||
<SelectorInput
|
||||
blockId={blockId}
|
||||
subBlock={config}
|
||||
disabled={isDisabled}
|
||||
isPreview={isPreview}
|
||||
previewValue={previewValue}
|
||||
previewContextValues={contextValues}
|
||||
overrides={FOLDER_OVERRIDES}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -985,12 +977,12 @@ function SubBlockComponent({
|
||||
|
||||
case 'document-selector':
|
||||
return (
|
||||
<DocumentSelector
|
||||
<SelectorInput
|
||||
blockId={blockId}
|
||||
subBlock={config}
|
||||
disabled={isDisabled}
|
||||
isPreview={isPreview}
|
||||
previewValue={previewValue as any}
|
||||
previewValue={previewValue}
|
||||
previewContextValues={contextValues}
|
||||
/>
|
||||
)
|
||||
@@ -1068,13 +1060,14 @@ function SubBlockComponent({
|
||||
case 'channel-selector':
|
||||
case 'user-selector':
|
||||
return (
|
||||
<SlackSelectorInput
|
||||
<SelectorInput
|
||||
blockId={blockId}
|
||||
subBlock={config}
|
||||
disabled={isDisabled}
|
||||
isPreview={isPreview}
|
||||
previewValue={previewValue}
|
||||
previewContextValues={contextValues}
|
||||
overrides={SLACK_OVERRIDES}
|
||||
/>
|
||||
)
|
||||
|
||||
|
||||
@@ -69,6 +69,7 @@ export const KnowledgeBlock: BlockConfig = {
|
||||
title: 'Tag Filters',
|
||||
type: 'knowledge-tag-filters',
|
||||
placeholder: 'Add tag filters',
|
||||
dependsOn: ['knowledgeBaseSelector'],
|
||||
condition: { field: 'operation', value: 'search' },
|
||||
},
|
||||
{
|
||||
@@ -112,6 +113,7 @@ export const KnowledgeBlock: BlockConfig = {
|
||||
id: 'documentTags',
|
||||
title: 'Document Tags',
|
||||
type: 'document-tag-entry',
|
||||
dependsOn: ['knowledgeBaseSelector'],
|
||||
condition: { field: 'operation', value: 'create_document' },
|
||||
},
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user