improvement(selectors): make selectorKeys declarative (#3374)

* fix(webflow): resolution for selectors

* remove unecessary fallback'

* fix teams selector resolution

* make selector keys declarative

* selectors fixes
This commit is contained in:
Vikhyath Mondreti
2026-02-27 07:56:35 -08:00
committed by GitHub
parent b3424e2047
commit e3ff595a84
25 changed files with 185 additions and 216 deletions
@@ -2,12 +2,16 @@
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 { 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 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
@@ -28,15 +32,41 @@ export function DocumentSelector({
previewValue,
previewContextValues,
}: DocumentSelectorProps) {
const { activeWorkflowId } = useWorkflowRegistry()
const { finalDisabled } = useDependsOnGate(blockId, subBlock, {
disabled,
isPreview,
previewContextValues,
})
const [knowledgeBaseIdFromStore] = useSubBlockValue(blockId, 'knowledgeBaseId')
const knowledgeBaseIdValue = previewContextValues
? resolvePreviewContextValue(previewContextValues.knowledgeBaseId)
: knowledgeBaseIdFromStore
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
@@ -13,15 +13,20 @@ 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 { 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
@@ -60,11 +65,26 @@ 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,
@@ -77,10 +97,18 @@ export function DocumentTagEntry({
disabled,
})
const [knowledgeBaseIdFromStore] = useSubBlockValue(blockId, 'knowledgeBaseId')
const knowledgeBaseIdValue = previewContextValues
? resolvePreviewContextValue(previewContextValues.knowledgeBaseId)
: knowledgeBaseIdFromStore
const knowledgeBaseIdValue = useMemo(
() =>
previewContextValues
? resolvePreviewContextValue(previewContextValues.knowledgeBaseId)
: resolveDependencyValue(
'knowledgeBaseId',
blockValues,
canonicalIndex,
canonicalModeOverrides
),
[previewContextValues, blockValues, canonicalIndex, canonicalModeOverrides]
)
const knowledgeBaseId =
typeof knowledgeBaseIdValue === 'string' && knowledgeBaseIdValue.trim().length > 0
? knowledgeBaseIdValue
@@ -1,6 +1,6 @@
'use client'
import { useRef } from 'react'
import { useMemo, useRef } from 'react'
import { Plus } from 'lucide-react'
import {
Badge,
@@ -14,14 +14,19 @@ 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 { 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 {
@@ -64,15 +69,38 @@ 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 [knowledgeBaseIdFromStore] = useSubBlockValue(blockId, 'knowledgeBaseId')
const knowledgeBaseIdValue = previewContextValues
? resolvePreviewContextValue(previewContextValues.knowledgeBaseId)
: knowledgeBaseIdFromStore
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 knowledgeBaseId =
typeof knowledgeBaseIdValue === 'string' && knowledgeBaseIdValue.trim().length > 0
? knowledgeBaseIdValue
+2
View File
@@ -104,6 +104,7 @@ export const ConfluenceBlock: BlockConfig<ConfluenceResponse> = {
type: 'file-selector',
canonicalParamId: 'pageId',
serviceId: 'confluence',
selectorKey: 'confluence.pages',
placeholder: 'Select Confluence page',
dependsOn: ['credential', 'domain'],
mode: 'basic',
@@ -527,6 +528,7 @@ export const ConfluenceV2Block: BlockConfig<ConfluenceResponse> = {
type: 'file-selector',
canonicalParamId: 'pageId',
serviceId: 'confluence',
selectorKey: 'confluence.pages',
placeholder: 'Select Confluence page',
dependsOn: ['credential', 'domain'],
mode: 'basic',
+4
View File
@@ -221,6 +221,7 @@ Return ONLY the email body - no explanations, no extra text.`,
type: 'folder-selector',
canonicalParamId: 'folder',
serviceId: 'gmail',
selectorKey: 'gmail.labels',
requiredScopes: ['https://www.googleapis.com/auth/gmail.labels'],
placeholder: 'Select Gmail label/folder',
dependsOn: ['credential'],
@@ -301,6 +302,7 @@ Return ONLY the search query - no explanations, no extra text.`,
type: 'folder-selector',
canonicalParamId: 'addLabelIds',
serviceId: 'gmail',
selectorKey: 'gmail.labels',
requiredScopes: ['https://www.googleapis.com/auth/gmail.labels'],
placeholder: 'Select destination label',
dependsOn: ['credential'],
@@ -326,6 +328,7 @@ Return ONLY the search query - no explanations, no extra text.`,
type: 'folder-selector',
canonicalParamId: 'removeLabelIds',
serviceId: 'gmail',
selectorKey: 'gmail.labels',
requiredScopes: ['https://www.googleapis.com/auth/gmail.labels'],
placeholder: 'Select label to remove',
dependsOn: ['credential'],
@@ -378,6 +381,7 @@ Return ONLY the search query - no explanations, no extra text.`,
type: 'folder-selector',
canonicalParamId: 'manageLabelId',
serviceId: 'gmail',
selectorKey: 'gmail.labels',
requiredScopes: ['https://www.googleapis.com/auth/gmail.labels'],
placeholder: 'Select label',
dependsOn: ['credential'],
@@ -62,6 +62,8 @@ export const GoogleCalendarBlock: BlockConfig<GoogleCalendarResponse> = {
type: 'file-selector',
canonicalParamId: 'calendarId',
serviceId: 'google-calendar',
selectorKey: 'google.calendar',
selectorAllowSearch: false,
requiredScopes: ['https://www.googleapis.com/auth/calendar'],
placeholder: 'Select calendar',
dependsOn: ['credential'],
@@ -326,6 +328,8 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
type: 'file-selector',
canonicalParamId: 'destinationCalendarId',
serviceId: 'google-calendar',
selectorKey: 'google.calendar',
selectorAllowSearch: false,
requiredScopes: ['https://www.googleapis.com/auth/calendar'],
placeholder: 'Select destination calendar',
dependsOn: ['credential'],
+2
View File
@@ -58,6 +58,7 @@ export const GoogleDocsBlock: BlockConfig<GoogleDocsResponse> = {
type: 'file-selector',
canonicalParamId: 'documentId',
serviceId: 'google-docs',
selectorKey: 'google.drive',
requiredScopes: [],
mimeType: 'application/vnd.google-apps.document',
placeholder: 'Select a document',
@@ -100,6 +101,7 @@ Return ONLY the document title - no explanations, no extra text.`,
type: 'file-selector',
canonicalParamId: 'folderId',
serviceId: 'google-docs',
selectorKey: 'google.drive',
requiredScopes: [],
mimeType: 'application/vnd.google-apps.folder',
placeholder: 'Select a parent folder',
+13
View File
@@ -137,6 +137,7 @@ Return ONLY the file content - no explanations, no markdown code blocks, no extr
type: 'file-selector',
canonicalParamId: 'uploadFolderId',
serviceId: 'google-drive',
selectorKey: 'google.drive',
requiredScopes: [
'https://www.googleapis.com/auth/drive.file',
'https://www.googleapis.com/auth/drive',
@@ -209,6 +210,7 @@ Return ONLY the file content - no explanations, no markdown code blocks, no extr
type: 'file-selector',
canonicalParamId: 'createFolderParentId',
serviceId: 'google-drive',
selectorKey: 'google.drive',
requiredScopes: [
'https://www.googleapis.com/auth/drive.file',
'https://www.googleapis.com/auth/drive',
@@ -236,6 +238,7 @@ Return ONLY the file content - no explanations, no markdown code blocks, no extr
type: 'file-selector',
canonicalParamId: 'listFolderId',
serviceId: 'google-drive',
selectorKey: 'google.drive',
requiredScopes: [
'https://www.googleapis.com/auth/drive.file',
'https://www.googleapis.com/auth/drive',
@@ -295,6 +298,7 @@ Return ONLY the query string - no explanations, no quotes around the whole thing
type: 'file-selector',
canonicalParamId: 'downloadFileId',
serviceId: 'google-drive',
selectorKey: 'google.drive',
requiredScopes: [
'https://www.googleapis.com/auth/drive.file',
'https://www.googleapis.com/auth/drive',
@@ -356,6 +360,7 @@ Return ONLY the query string - no explanations, no quotes around the whole thing
type: 'file-selector',
canonicalParamId: 'getFileId',
serviceId: 'google-drive',
selectorKey: 'google.drive',
requiredScopes: [
'https://www.googleapis.com/auth/drive.file',
'https://www.googleapis.com/auth/drive',
@@ -383,6 +388,7 @@ Return ONLY the query string - no explanations, no quotes around the whole thing
type: 'file-selector',
canonicalParamId: 'copyFileId',
serviceId: 'google-drive',
selectorKey: 'google.drive',
requiredScopes: [
'https://www.googleapis.com/auth/drive.file',
'https://www.googleapis.com/auth/drive',
@@ -416,6 +422,7 @@ Return ONLY the query string - no explanations, no quotes around the whole thing
type: 'file-selector',
canonicalParamId: 'copyDestFolderId',
serviceId: 'google-drive',
selectorKey: 'google.drive',
requiredScopes: [
'https://www.googleapis.com/auth/drive.file',
'https://www.googleapis.com/auth/drive',
@@ -442,6 +449,7 @@ Return ONLY the query string - no explanations, no quotes around the whole thing
type: 'file-selector',
canonicalParamId: 'updateFileId',
serviceId: 'google-drive',
selectorKey: 'google.drive',
requiredScopes: [
'https://www.googleapis.com/auth/drive.file',
'https://www.googleapis.com/auth/drive',
@@ -520,6 +528,7 @@ Return ONLY the description text - no explanations, no quotes, no extra text.`,
type: 'file-selector',
canonicalParamId: 'trashFileId',
serviceId: 'google-drive',
selectorKey: 'google.drive',
requiredScopes: [
'https://www.googleapis.com/auth/drive.file',
'https://www.googleapis.com/auth/drive',
@@ -547,6 +556,7 @@ Return ONLY the description text - no explanations, no quotes, no extra text.`,
type: 'file-selector',
canonicalParamId: 'deleteFileId',
serviceId: 'google-drive',
selectorKey: 'google.drive',
requiredScopes: [
'https://www.googleapis.com/auth/drive.file',
'https://www.googleapis.com/auth/drive',
@@ -574,6 +584,7 @@ Return ONLY the description text - no explanations, no quotes, no extra text.`,
type: 'file-selector',
canonicalParamId: 'shareFileId',
serviceId: 'google-drive',
selectorKey: 'google.drive',
requiredScopes: [
'https://www.googleapis.com/auth/drive.file',
'https://www.googleapis.com/auth/drive',
@@ -688,6 +699,7 @@ Return ONLY the message text - no subject line, no greetings/signatures, no extr
type: 'file-selector',
canonicalParamId: 'unshareFileId',
serviceId: 'google-drive',
selectorKey: 'google.drive',
requiredScopes: [
'https://www.googleapis.com/auth/drive.file',
'https://www.googleapis.com/auth/drive',
@@ -723,6 +735,7 @@ Return ONLY the message text - no subject line, no greetings/signatures, no extr
type: 'file-selector',
canonicalParamId: 'listPermissionsFileId',
serviceId: 'google-drive',
selectorKey: 'google.drive',
requiredScopes: [
'https://www.googleapis.com/auth/drive.file',
'https://www.googleapis.com/auth/drive',
+1
View File
@@ -64,6 +64,7 @@ export const GoogleFormsBlock: BlockConfig = {
canonicalParamId: 'formId',
required: true,
serviceId: 'google-forms',
selectorKey: 'google.drive',
requiredScopes: [],
mimeType: 'application/vnd.google-apps.form',
placeholder: 'Select a form',
+4
View File
@@ -62,6 +62,7 @@ export const GoogleSheetsBlock: BlockConfig<GoogleSheetsResponse> = {
type: 'file-selector',
canonicalParamId: 'spreadsheetId',
serviceId: 'google-sheets',
selectorKey: 'google.drive',
requiredScopes: [
'https://www.googleapis.com/auth/drive.file',
'https://www.googleapis.com/auth/drive',
@@ -360,6 +361,7 @@ export const GoogleSheetsV2Block: BlockConfig<GoogleSheetsV2Response> = {
type: 'file-selector',
canonicalParamId: 'spreadsheetId',
serviceId: 'google-sheets',
selectorKey: 'google.drive',
requiredScopes: [
'https://www.googleapis.com/auth/drive.file',
'https://www.googleapis.com/auth/drive',
@@ -388,6 +390,8 @@ export const GoogleSheetsV2Block: BlockConfig<GoogleSheetsV2Response> = {
type: 'sheet-selector',
canonicalParamId: 'sheetName',
serviceId: 'google-sheets',
selectorKey: 'google.sheets',
selectorAllowSearch: false,
placeholder: 'Select a sheet',
required: true,
dependsOn: { all: ['credential'], any: ['spreadsheetId', 'manualSpreadsheetId'] },
+2
View File
@@ -72,6 +72,7 @@ export const GoogleSlidesBlock: BlockConfig<GoogleSlidesResponse> = {
type: 'file-selector',
canonicalParamId: 'presentationId',
serviceId: 'google-drive',
selectorKey: 'google.drive',
requiredScopes: [],
mimeType: 'application/vnd.google-apps.presentation',
placeholder: 'Select a presentation',
@@ -185,6 +186,7 @@ Return ONLY the title - no explanations, no quotes, no extra text.`,
type: 'file-selector',
canonicalParamId: 'folderId',
serviceId: 'google-drive',
selectorKey: 'google.drive',
requiredScopes: [],
mimeType: 'application/vnd.google-apps.folder',
placeholder: 'Select a parent folder',
+2
View File
@@ -114,6 +114,7 @@ export const JiraBlock: BlockConfig<JiraResponse> = {
type: 'project-selector',
canonicalParamId: 'projectId',
serviceId: 'jira',
selectorKey: 'jira.projects',
placeholder: 'Select Jira project',
dependsOn: ['credential', 'domain'],
mode: 'basic',
@@ -137,6 +138,7 @@ export const JiraBlock: BlockConfig<JiraResponse> = {
type: 'file-selector',
canonicalParamId: 'issueKey',
serviceId: 'jira',
selectorKey: 'jira.issues',
placeholder: 'Select Jira issue',
dependsOn: ['credential', 'domain', 'projectId'],
condition: {
+1
View File
@@ -75,6 +75,7 @@ export const KnowledgeBlock: BlockConfig = {
id: 'documentId',
title: 'Document',
type: 'document-selector',
selectorKey: 'knowledge.documents',
placeholder: 'Select document',
dependsOn: ['knowledgeBaseId'],
required: true,
+2
View File
@@ -152,6 +152,7 @@ export const LinearBlock: BlockConfig<LinearResponse> = {
type: 'project-selector',
canonicalParamId: 'teamId',
serviceId: 'linear',
selectorKey: 'linear.teams',
placeholder: 'Select a team',
dependsOn: ['credential'],
mode: 'basic',
@@ -228,6 +229,7 @@ export const LinearBlock: BlockConfig<LinearResponse> = {
type: 'project-selector',
canonicalParamId: 'projectId',
serviceId: 'linear',
selectorKey: 'linear.projects',
placeholder: 'Select a project',
dependsOn: ['credential', 'teamId'],
mode: 'basic',
@@ -65,6 +65,7 @@ export const MicrosoftExcelBlock: BlockConfig<MicrosoftExcelResponse> = {
type: 'file-selector',
canonicalParamId: 'spreadsheetId',
serviceId: 'microsoft-excel',
selectorKey: 'microsoft.excel',
requiredScopes: [],
mimeType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
placeholder: 'Select a spreadsheet',
@@ -392,6 +393,7 @@ export const MicrosoftExcelV2Block: BlockConfig<MicrosoftExcelV2Response> = {
type: 'file-selector',
canonicalParamId: 'spreadsheetId',
serviceId: 'microsoft-excel',
selectorKey: 'microsoft.excel',
requiredScopes: [],
mimeType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
placeholder: 'Select a spreadsheet',
@@ -415,6 +417,8 @@ export const MicrosoftExcelV2Block: BlockConfig<MicrosoftExcelV2Response> = {
type: 'sheet-selector',
canonicalParamId: 'sheetName',
serviceId: 'microsoft-excel',
selectorKey: 'microsoft.excel.sheets',
selectorAllowSearch: false,
placeholder: 'Select a sheet',
required: true,
dependsOn: { all: ['credential'], any: ['spreadsheetId', 'manualSpreadsheetId'] },
@@ -108,6 +108,7 @@ export const MicrosoftPlannerBlock: BlockConfig<MicrosoftPlannerResponse> = {
type: 'file-selector',
placeholder: 'Select a task',
serviceId: 'microsoft-planner',
selectorKey: 'microsoft.planner',
condition: { field: 'operation', value: ['read_task'] },
dependsOn: ['credential', 'planId'],
mode: 'basic',
@@ -87,6 +87,8 @@ export const MicrosoftTeamsBlock: BlockConfig<MicrosoftTeamsResponse> = {
type: 'file-selector',
canonicalParamId: 'teamId',
serviceId: 'microsoft-teams',
selectorKey: 'microsoft.teams',
selectorAllowSearch: false,
requiredScopes: [],
placeholder: 'Select a team',
dependsOn: ['credential'],
@@ -132,6 +134,8 @@ export const MicrosoftTeamsBlock: BlockConfig<MicrosoftTeamsResponse> = {
type: 'file-selector',
canonicalParamId: 'chatId',
serviceId: 'microsoft-teams',
selectorKey: 'microsoft.chats',
selectorAllowSearch: false,
requiredScopes: [],
placeholder: 'Select a chat',
dependsOn: ['credential'],
@@ -161,6 +165,8 @@ export const MicrosoftTeamsBlock: BlockConfig<MicrosoftTeamsResponse> = {
type: 'file-selector',
canonicalParamId: 'channelId',
serviceId: 'microsoft-teams',
selectorKey: 'microsoft.channels',
selectorAllowSearch: false,
requiredScopes: [],
placeholder: 'Select a channel',
dependsOn: ['credential', 'teamSelector'],
+5
View File
@@ -155,6 +155,7 @@ export const OneDriveBlock: BlockConfig<OneDriveResponse> = {
type: 'file-selector',
canonicalParamId: 'uploadFolderId',
serviceId: 'onedrive',
selectorKey: 'onedrive.folders',
requiredScopes: [
'openid',
'profile',
@@ -192,6 +193,7 @@ export const OneDriveBlock: BlockConfig<OneDriveResponse> = {
type: 'file-selector',
canonicalParamId: 'createFolderParentId',
serviceId: 'onedrive',
selectorKey: 'onedrive.folders',
requiredScopes: [
'openid',
'profile',
@@ -224,6 +226,7 @@ export const OneDriveBlock: BlockConfig<OneDriveResponse> = {
type: 'file-selector',
canonicalParamId: 'listFolderId',
serviceId: 'onedrive',
selectorKey: 'onedrive.folders',
requiredScopes: [
'openid',
'profile',
@@ -270,6 +273,7 @@ export const OneDriveBlock: BlockConfig<OneDriveResponse> = {
type: 'file-selector',
canonicalParamId: 'downloadFileId',
serviceId: 'onedrive',
selectorKey: 'onedrive.files',
requiredScopes: [
'openid',
'profile',
@@ -310,6 +314,7 @@ export const OneDriveBlock: BlockConfig<OneDriveResponse> = {
type: 'file-selector',
canonicalParamId: 'deleteFileId',
serviceId: 'onedrive',
selectorKey: 'onedrive.files',
requiredScopes: [
'openid',
'profile',
+3
View File
@@ -187,6 +187,7 @@ export const OutlookBlock: BlockConfig<OutlookResponse> = {
type: 'folder-selector',
canonicalParamId: 'folder',
serviceId: 'outlook',
selectorKey: 'outlook.folders',
requiredScopes: ['Mail.ReadWrite', 'Mail.ReadBasic', 'Mail.Read'],
placeholder: 'Select Outlook folder',
dependsOn: ['credential'],
@@ -232,6 +233,7 @@ export const OutlookBlock: BlockConfig<OutlookResponse> = {
type: 'folder-selector',
canonicalParamId: 'destinationId',
serviceId: 'outlook',
selectorKey: 'outlook.folders',
requiredScopes: ['Mail.ReadWrite', 'Mail.ReadBasic', 'Mail.Read'],
placeholder: 'Select destination folder',
dependsOn: ['credential'],
@@ -278,6 +280,7 @@ export const OutlookBlock: BlockConfig<OutlookResponse> = {
type: 'folder-selector',
canonicalParamId: 'copyDestinationId',
serviceId: 'outlook',
selectorKey: 'outlook.folders',
requiredScopes: ['Mail.ReadWrite', 'Mail.ReadBasic', 'Mail.Read'],
placeholder: 'Select destination folder',
dependsOn: ['credential'],
+1
View File
@@ -67,6 +67,7 @@ export const SharepointBlock: BlockConfig<SharepointResponse> = {
type: 'file-selector',
canonicalParamId: 'siteId',
serviceId: 'sharepoint',
selectorKey: 'sharepoint.sites',
requiredScopes: [
'openid',
'profile',
+2
View File
@@ -130,6 +130,7 @@ export const SlackBlock: BlockConfig<SlackResponse> = {
type: 'channel-selector',
canonicalParamId: 'channel',
serviceId: 'slack',
selectorKey: 'slack.channels',
placeholder: 'Select Slack channel',
mode: 'basic',
dependsOn: { all: ['authMethod'], any: ['credential', 'botToken'] },
@@ -182,6 +183,7 @@ export const SlackBlock: BlockConfig<SlackResponse> = {
type: 'user-selector',
canonicalParamId: 'dmUserId',
serviceId: 'slack',
selectorKey: 'slack.users',
placeholder: 'Select Slack user',
mode: 'basic',
dependsOn: { all: ['authMethod'], any: ['credential', 'botToken'] },
+1
View File
@@ -61,6 +61,7 @@ export const WealthboxBlock: BlockConfig<WealthboxResponse> = {
title: 'Select Contact',
type: 'file-selector',
serviceId: 'wealthbox',
selectorKey: 'wealthbox.contacts',
requiredScopes: ['login', 'data'],
placeholder: 'Enter Contact ID',
mode: 'basic',
+5
View File
@@ -56,6 +56,8 @@ export const WebflowBlock: BlockConfig<WebflowResponse> = {
type: 'project-selector',
canonicalParamId: 'siteId',
serviceId: 'webflow',
selectorKey: 'webflow.sites',
selectorAllowSearch: false,
placeholder: 'Select Webflow site',
dependsOn: ['credential'],
mode: 'basic',
@@ -76,6 +78,8 @@ export const WebflowBlock: BlockConfig<WebflowResponse> = {
type: 'file-selector',
canonicalParamId: 'collectionId',
serviceId: 'webflow',
selectorKey: 'webflow.collections',
selectorAllowSearch: false,
placeholder: 'Select collection',
dependsOn: ['credential', 'siteSelector'],
mode: 'basic',
@@ -96,6 +100,7 @@ export const WebflowBlock: BlockConfig<WebflowResponse> = {
type: 'file-selector',
canonicalParamId: 'itemId',
serviceId: 'webflow',
selectorKey: 'webflow.items',
placeholder: 'Select item',
dependsOn: ['credential', 'collectionSelector'],
mode: 'basic',
+4
View File
@@ -1,4 +1,5 @@
import type { JSX, SVGProps } from 'react'
import type { SelectorKey } from '@/hooks/selectors/types'
import type { ToolResponse } from '@/tools/types'
export type BlockIcon = (props: SVGProps<SVGSVGElement>) => JSX.Element
@@ -290,6 +291,9 @@ export interface SubBlockConfig {
requiredScopes?: string[]
// Whether this credential selector supports credential sets (for trigger blocks)
supportsCredentialSets?: boolean
// Selector properties — declarative mapping to a SelectorKey
selectorKey?: SelectorKey
selectorAllowSearch?: boolean
// File selector specific properties
mimeType?: string
// File upload specific properties
+16 -202
View File
@@ -20,212 +20,26 @@ export interface SelectorResolutionArgs {
spreadsheetId?: string
}
const defaultContext: SelectorContext = {}
export function resolveSelectorForSubBlock(
subBlock: SubBlockConfig,
args: SelectorResolutionArgs
): SelectorResolution | null {
switch (subBlock.type) {
case 'file-selector':
return resolveFileSelector(subBlock, args)
case 'sheet-selector':
return resolveSheetSelector(subBlock, args)
case 'folder-selector':
return resolveFolderSelector(subBlock, args)
case 'channel-selector':
return resolveChannelSelector(subBlock, args)
case 'user-selector':
return resolveUserSelector(subBlock, args)
case 'project-selector':
return resolveProjectSelector(subBlock, args)
case 'document-selector':
return resolveDocumentSelector(subBlock, args)
default:
return null
}
}
function buildBaseContext(
args: SelectorResolutionArgs,
extra?: Partial<SelectorContext>
): SelectorContext {
if (!subBlock.selectorKey) return null
return {
...defaultContext,
workflowId: args.workflowId,
credentialId: args.credentialId,
domain: args.domain,
projectId: args.projectId,
planId: args.planId,
teamId: args.teamId,
knowledgeBaseId: args.knowledgeBaseId,
siteId: args.siteId,
collectionId: args.collectionId,
spreadsheetId: args.spreadsheetId,
...extra,
}
}
function resolveFileSelector(
subBlock: SubBlockConfig,
args: SelectorResolutionArgs
): SelectorResolution {
const context = buildBaseContext(args, {
mimeType: subBlock.mimeType,
})
// Use serviceId as the canonical identifier
const serviceId = subBlock.serviceId || ''
switch (serviceId) {
case 'google-calendar':
return { key: 'google.calendar', context, allowSearch: false }
case 'confluence':
return { key: 'confluence.pages', context, allowSearch: true }
case 'jira':
return { key: 'jira.issues', context, allowSearch: true }
case 'microsoft-teams':
// Route to the correct selector based on what type of resource is being selected
if (subBlock.id === 'chatId') {
return { key: 'microsoft.chats', context, allowSearch: false }
}
if (subBlock.id === 'channelId') {
return { key: 'microsoft.channels', context, allowSearch: false }
}
// Default to teams selector for teamId
return { key: 'microsoft.teams', context, allowSearch: false }
case 'wealthbox':
return { key: 'wealthbox.contacts', context, allowSearch: true }
case 'microsoft-planner':
return { key: 'microsoft.planner', context, allowSearch: true }
case 'microsoft-excel':
return { key: 'microsoft.excel', context, allowSearch: true }
case 'microsoft-word':
return { key: 'microsoft.word', context, allowSearch: true }
case 'google-drive':
return { key: 'google.drive', context, allowSearch: true }
case 'google-sheets':
return { key: 'google.drive', context, allowSearch: true }
case 'google-docs':
return { key: 'google.drive', context, allowSearch: true }
case 'google-slides':
return { key: 'google.drive', context, allowSearch: true }
case 'google-forms':
return { key: 'google.drive', context, allowSearch: true }
case 'onedrive': {
const key: SelectorKey = subBlock.mimeType === 'file' ? 'onedrive.files' : 'onedrive.folders'
return { key, context, allowSearch: true }
}
case 'sharepoint':
return { key: 'sharepoint.sites', context, allowSearch: true }
case 'webflow':
if (subBlock.id === 'collectionId') {
return { key: 'webflow.collections', context, allowSearch: false }
}
if (subBlock.id === 'itemId') {
return { key: 'webflow.items', context, allowSearch: true }
}
return { key: null, context, allowSearch: true }
default:
return { key: null, context, allowSearch: true }
}
}
function resolveSheetSelector(
subBlock: SubBlockConfig,
args: SelectorResolutionArgs
): SelectorResolution {
const serviceId = subBlock.serviceId
const context = buildBaseContext(args)
switch (serviceId) {
case 'google-sheets':
return { key: 'google.sheets', context, allowSearch: false }
case 'microsoft-excel':
return { key: 'microsoft.excel.sheets', context, allowSearch: false }
default:
return { key: null, context, allowSearch: false }
}
}
function resolveFolderSelector(
subBlock: SubBlockConfig,
args: SelectorResolutionArgs
): SelectorResolution {
const serviceId = subBlock.serviceId?.toLowerCase()
if (!serviceId) {
return { key: null, context: buildBaseContext(args), allowSearch: true }
}
switch (serviceId) {
case 'gmail':
return { key: 'gmail.labels', context: buildBaseContext(args), allowSearch: true }
case 'outlook':
return { key: 'outlook.folders', context: buildBaseContext(args), allowSearch: true }
default:
return { key: null, context: buildBaseContext(args), allowSearch: true }
}
}
function resolveChannelSelector(
subBlock: SubBlockConfig,
args: SelectorResolutionArgs
): SelectorResolution {
const serviceId = subBlock.serviceId
if (serviceId !== 'slack') {
return { key: null, context: buildBaseContext(args), allowSearch: true }
}
return {
key: 'slack.channels',
context: buildBaseContext(args),
allowSearch: true,
}
}
function resolveUserSelector(
subBlock: SubBlockConfig,
args: SelectorResolutionArgs
): SelectorResolution {
const serviceId = subBlock.serviceId
if (serviceId !== 'slack') {
return { key: null, context: buildBaseContext(args), allowSearch: true }
}
return {
key: 'slack.users',
context: buildBaseContext(args),
allowSearch: true,
}
}
function resolveProjectSelector(
subBlock: SubBlockConfig,
args: SelectorResolutionArgs
): SelectorResolution {
const serviceId = subBlock.serviceId
const context = buildBaseContext(args)
const selectorId = subBlock.canonicalParamId ?? subBlock.id
switch (serviceId) {
case 'linear': {
const key: SelectorKey = selectorId === 'teamId' ? 'linear.teams' : 'linear.projects'
return { key, context, allowSearch: true }
}
case 'jira':
return { key: 'jira.projects', context, allowSearch: true }
case 'webflow':
return { key: 'webflow.sites', context, allowSearch: false }
default:
return { key: null, context, allowSearch: true }
}
}
function resolveDocumentSelector(
_subBlock: SubBlockConfig,
args: SelectorResolutionArgs
): SelectorResolution {
return {
key: 'knowledge.documents',
context: buildBaseContext(args),
allowSearch: true,
key: subBlock.selectorKey,
context: {
workflowId: args.workflowId,
credentialId: args.credentialId,
domain: args.domain,
projectId: args.projectId,
planId: args.planId,
teamId: args.teamId,
knowledgeBaseId: args.knowledgeBaseId,
siteId: args.siteId,
collectionId: args.collectionId,
spreadsheetId: args.spreadsheetId,
mimeType: subBlock.mimeType,
},
allowSearch: subBlock.selectorAllowSearch ?? true,
}
}