mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-28 03:00:29 -04:00
fix(subblock-param-mapping): consolidate resolution of advanced / basic mode params using canonicalParamId (#1274)
* fix(serializer): block's params mapper not running first * fix * fix * revert * add canonicalParamId * fix * fix tests * fix discord * fix condition checking * edit condition check * fix * fix subblock config check * fix * add logging * add more logs * fix * fix * attempt * fix discord * remove unused discord code * mark as required correctly
This commit is contained in:
committed by
GitHub
parent
1e14743391
commit
ced64129da
@@ -85,7 +85,8 @@ export async function POST(request: Request) {
|
||||
|
||||
logger.info(`Fetching all Discord channels for server: ${serverId}`)
|
||||
|
||||
// Fetch all channels from Discord API
|
||||
// Listing guild channels with a bot token is allowed if the bot is in the guild.
|
||||
// Keep the request, but if unauthorized, return an empty list so the selector doesn't hard fail.
|
||||
const response = await fetch(`https://discord.com/api/v10/guilds/${serverId}/channels`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
@@ -95,20 +96,14 @@ export async function POST(request: Request) {
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
logger.error('Discord API error:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
})
|
||||
|
||||
let errorMessage
|
||||
try {
|
||||
const errorData = await response.json()
|
||||
logger.error('Error details:', errorData)
|
||||
errorMessage = errorData.message || `Failed to fetch channels (${response.status})`
|
||||
} catch (_e) {
|
||||
errorMessage = `Failed to fetch channels: ${response.status} ${response.statusText}`
|
||||
}
|
||||
return NextResponse.json({ error: errorMessage }, { status: response.status })
|
||||
logger.warn(
|
||||
'Discord API returned non-OK for channels; returning empty list to avoid UX break',
|
||||
{
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
}
|
||||
)
|
||||
return NextResponse.json({ channels: [] })
|
||||
}
|
||||
|
||||
const channels = (await response.json()) as DiscordChannel[]
|
||||
|
||||
@@ -64,46 +64,14 @@ export async function POST(request: Request) {
|
||||
})
|
||||
}
|
||||
|
||||
// Otherwise, fetch all servers the bot is in
|
||||
logger.info('Fetching all Discord servers')
|
||||
|
||||
const response = await fetch('https://discord.com/api/v10/users/@me/guilds', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Bot ${botToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
logger.error('Discord API error:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
})
|
||||
|
||||
let errorMessage
|
||||
try {
|
||||
const errorData = await response.json()
|
||||
logger.error('Error details:', errorData)
|
||||
errorMessage = errorData.message || `Failed to fetch servers (${response.status})`
|
||||
} catch (_e) {
|
||||
errorMessage = `Failed to fetch servers: ${response.status} ${response.statusText}`
|
||||
}
|
||||
return NextResponse.json({ error: errorMessage }, { status: response.status })
|
||||
}
|
||||
|
||||
const servers = (await response.json()) as DiscordServer[]
|
||||
logger.info(`Successfully fetched ${servers.length} servers`)
|
||||
|
||||
return NextResponse.json({
|
||||
servers: servers.map((server: DiscordServer) => ({
|
||||
id: server.id,
|
||||
name: server.name,
|
||||
icon: server.icon
|
||||
? `https://cdn.discordapp.com/icons/${server.id}/${server.icon}.png`
|
||||
: null,
|
||||
})),
|
||||
})
|
||||
// Listing guilds via REST requires a user OAuth2 access token with the 'guilds' scope.
|
||||
// A bot token cannot call /users/@me/guilds and will return 401.
|
||||
// Since this selector only has a bot token, return an empty list instead of erroring
|
||||
// and let users provide a Server ID in advanced mode.
|
||||
logger.info(
|
||||
'Skipping guild listing: bot token cannot list /users/@me/guilds; returning empty list'
|
||||
)
|
||||
return NextResponse.json({ servers: [] })
|
||||
} catch (error) {
|
||||
logger.error('Error processing request:', error)
|
||||
return NextResponse.json(
|
||||
|
||||
@@ -195,14 +195,31 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
||||
|
||||
// Process blocks
|
||||
blocks.forEach((block) => {
|
||||
const parentId = block.parentId || null
|
||||
const extent = block.extent || null
|
||||
const blockData = {
|
||||
...(block.data || {}),
|
||||
...(parentId && { parentId }),
|
||||
...(extent && { extent }),
|
||||
}
|
||||
|
||||
blocksMap[block.id] = {
|
||||
id: block.id,
|
||||
type: block.type,
|
||||
name: block.name,
|
||||
position: { x: Number(block.positionX), y: Number(block.positionY) },
|
||||
data: block.data,
|
||||
data: blockData,
|
||||
enabled: block.enabled,
|
||||
subBlocks: block.subBlocks || {},
|
||||
// Preserve execution-relevant flags so serializer behavior matches manual runs
|
||||
isWide: block.isWide ?? false,
|
||||
advancedMode: block.advancedMode ?? false,
|
||||
triggerMode: block.triggerMode ?? false,
|
||||
outputs: block.outputs || {},
|
||||
horizontalHandles: block.horizontalHandles ?? true,
|
||||
height: Number(block.height || 0),
|
||||
parentId,
|
||||
extent,
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { eq } from 'drizzle-orm'
|
||||
import type { NextRequest } from 'next/server'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/db-helpers'
|
||||
import { hasWorkflowChanged } from '@/lib/workflows/utils'
|
||||
import { validateWorkflowAccess } from '@/app/api/workflows/middleware'
|
||||
import { createErrorResponse, createSuccessResponse } from '@/app/api/workflows/utils'
|
||||
import { db } from '@/db'
|
||||
import { workflowBlocks, workflowEdges, workflowSubflows } from '@/db/schema'
|
||||
|
||||
const logger = createLogger('WorkflowStatusAPI')
|
||||
|
||||
@@ -24,72 +22,12 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
|
||||
// Check if the workflow has meaningful changes that would require redeployment
|
||||
let needsRedeployment = false
|
||||
if (validation.workflow.isDeployed && validation.workflow.deployedState) {
|
||||
// Get current state from normalized tables (same logic as deployment API)
|
||||
const blocks = await db.select().from(workflowBlocks).where(eq(workflowBlocks.workflowId, id))
|
||||
|
||||
const edges = await db.select().from(workflowEdges).where(eq(workflowEdges.workflowId, id))
|
||||
|
||||
const subflows = await db
|
||||
.select()
|
||||
.from(workflowSubflows)
|
||||
.where(eq(workflowSubflows.workflowId, id))
|
||||
|
||||
// Build current state from normalized data
|
||||
const blocksMap: Record<string, any> = {}
|
||||
const loops: Record<string, any> = {}
|
||||
const parallels: Record<string, any> = {}
|
||||
|
||||
// Process blocks
|
||||
blocks.forEach((block) => {
|
||||
blocksMap[block.id] = {
|
||||
id: block.id,
|
||||
type: block.type,
|
||||
name: block.name,
|
||||
position: { x: Number(block.positionX), y: Number(block.positionY) },
|
||||
data: block.data,
|
||||
enabled: block.enabled,
|
||||
subBlocks: block.subBlocks || {},
|
||||
}
|
||||
})
|
||||
|
||||
// Process subflows (loops and parallels)
|
||||
subflows.forEach((subflow) => {
|
||||
const config = (subflow.config as any) || {}
|
||||
if (subflow.type === 'loop') {
|
||||
loops[subflow.id] = {
|
||||
id: subflow.id,
|
||||
nodes: config.nodes || [],
|
||||
iterations: config.iterations || 1,
|
||||
loopType: config.loopType || 'for',
|
||||
forEachItems: config.forEachItems || '',
|
||||
}
|
||||
} else if (subflow.type === 'parallel') {
|
||||
parallels[subflow.id] = {
|
||||
id: subflow.id,
|
||||
nodes: config.nodes || [],
|
||||
count: config.count || 2,
|
||||
distribution: config.distribution || '',
|
||||
parallelType: config.parallelType || 'count',
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Convert edges to the expected format
|
||||
const edgesArray = edges.map((edge) => ({
|
||||
id: edge.id,
|
||||
source: edge.sourceBlockId,
|
||||
target: edge.targetBlockId,
|
||||
sourceHandle: edge.sourceHandle,
|
||||
targetHandle: edge.targetHandle,
|
||||
type: 'default',
|
||||
data: {},
|
||||
}))
|
||||
|
||||
const normalizedData = await loadWorkflowFromNormalizedTables(id)
|
||||
const currentState = {
|
||||
blocks: blocksMap,
|
||||
edges: edgesArray,
|
||||
loops,
|
||||
parallels,
|
||||
blocks: normalizedData?.blocks || {},
|
||||
edges: normalizedData?.edges || [],
|
||||
loops: normalizedData?.loops || {},
|
||||
parallels: normalizedData?.parallels || {},
|
||||
lastSaved: Date.now(),
|
||||
}
|
||||
|
||||
|
||||
@@ -45,6 +45,7 @@ import {
|
||||
useKeyboardShortcuts,
|
||||
} from '@/app/workspace/[workspaceId]/w/hooks/use-keyboard-shortcuts'
|
||||
import { useFolderStore } from '@/stores/folders/store'
|
||||
import { useOperationQueueStore } from '@/stores/operation-queue/store'
|
||||
import { usePanelStore } from '@/stores/panel/store'
|
||||
import { useGeneralStore } from '@/stores/settings/general/store'
|
||||
import { useSubscriptionStore } from '@/stores/subscription/store'
|
||||
@@ -258,17 +259,23 @@ export function ControlBar({ hasValidationErrors = false }: ControlBarProps) {
|
||||
|
||||
// Get current store state for change detection
|
||||
const currentBlocks = useWorkflowStore((state) => state.blocks)
|
||||
const currentEdges = useWorkflowStore((state) => state.edges)
|
||||
const subBlockValues = useSubBlockStore((state) =>
|
||||
activeWorkflowId ? state.workflowValues[activeWorkflowId] : null
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
// Avoid off-by-one false positives: wait until operation queue is idle
|
||||
const { operations, isProcessing } = useOperationQueueStore.getState()
|
||||
const hasPendingOps =
|
||||
isProcessing || operations.some((op) => op.status === 'pending' || op.status === 'processing')
|
||||
|
||||
if (!activeWorkflowId || !deployedState) {
|
||||
setChangeDetected(false)
|
||||
return
|
||||
}
|
||||
|
||||
if (isLoadingDeployedState) {
|
||||
if (isLoadingDeployedState || hasPendingOps) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -291,7 +298,16 @@ export function ControlBar({ hasValidationErrors = false }: ControlBarProps) {
|
||||
}
|
||||
|
||||
checkForChanges()
|
||||
}, [activeWorkflowId, deployedState, currentBlocks, subBlockValues, isLoadingDeployedState])
|
||||
}, [
|
||||
activeWorkflowId,
|
||||
deployedState,
|
||||
currentBlocks,
|
||||
currentEdges,
|
||||
subBlockValues,
|
||||
isLoadingDeployedState,
|
||||
useOperationQueueStore.getState().isProcessing,
|
||||
useOperationQueueStore.getState().operations.length,
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
if (session?.user?.id && !isRegistryLoading) {
|
||||
|
||||
@@ -1,322 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { Check, ChevronDown, RefreshCw, X } from 'lucide-react'
|
||||
import { DiscordIcon } from '@/components/icons'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from '@/components/ui/command'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
|
||||
const logger = createLogger('DiscordChannelSelector')
|
||||
|
||||
export interface DiscordChannelInfo {
|
||||
id: string
|
||||
name: string
|
||||
type: number
|
||||
}
|
||||
|
||||
interface DiscordChannelSelectorProps {
|
||||
value: string
|
||||
onChange: (value: string, channelInfo?: DiscordChannelInfo) => void
|
||||
botToken: string
|
||||
serverId: string
|
||||
label?: string
|
||||
disabled?: boolean
|
||||
showPreview?: boolean
|
||||
onChannelInfoChange?: (info: DiscordChannelInfo | null) => void
|
||||
}
|
||||
|
||||
export function DiscordChannelSelector({
|
||||
value,
|
||||
onChange,
|
||||
botToken,
|
||||
serverId,
|
||||
label = 'Select Discord channel',
|
||||
disabled = false,
|
||||
showPreview = true,
|
||||
onChannelInfoChange,
|
||||
}: DiscordChannelSelectorProps) {
|
||||
const [open, setOpen] = useState(false)
|
||||
const [channels, setChannels] = useState<DiscordChannelInfo[]>([])
|
||||
const [selectedChannelId, setSelectedChannelId] = useState(value)
|
||||
const [selectedChannel, setSelectedChannel] = useState<DiscordChannelInfo | null>(null)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [initialFetchDone, setInitialFetchDone] = useState(false)
|
||||
|
||||
// Fetch channels from Discord API
|
||||
const fetchChannels = useCallback(async () => {
|
||||
if (!botToken || !serverId) {
|
||||
setError(!botToken ? 'Bot token is required' : 'Server ID is required')
|
||||
return
|
||||
}
|
||||
|
||||
setIsLoading(true)
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/tools/discord/channels', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ botToken, serverId }),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json()
|
||||
throw new Error(errorData.error || 'Failed to fetch Discord channels')
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
setChannels(data.channels || [])
|
||||
|
||||
// If we have a selected channel ID, find the channel info
|
||||
const currentSelectedId = selectedChannelId // Store in local variable
|
||||
if (currentSelectedId) {
|
||||
const channelInfo = data.channels?.find(
|
||||
(channel: DiscordChannelInfo) => channel.id === currentSelectedId
|
||||
)
|
||||
if (channelInfo) {
|
||||
setSelectedChannel(channelInfo)
|
||||
onChannelInfoChange?.(channelInfo)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error fetching channels:', error)
|
||||
setError((error as Error).message)
|
||||
setChannels([])
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
setInitialFetchDone(true)
|
||||
}
|
||||
}, [botToken, serverId, selectedChannelId, onChannelInfoChange])
|
||||
|
||||
// Handle open change - only fetch channels when the dropdown is opened
|
||||
const handleOpenChange = (isOpen: boolean) => {
|
||||
setOpen(isOpen)
|
||||
|
||||
// Only fetch channels when opening the dropdown and if we have valid token and server
|
||||
if (isOpen && botToken && serverId && (!initialFetchDone || channels.length === 0)) {
|
||||
fetchChannels()
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch only the selected channel info when component mounts or when selectedChannelId changes
|
||||
// This is more efficient than fetching all channels
|
||||
const fetchSelectedChannelInfo = useCallback(async () => {
|
||||
if (!botToken || !serverId || !selectedChannelId) return
|
||||
|
||||
setIsLoading(true)
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
// Only fetch the specific channel by ID instead of all channels
|
||||
const response = await fetch('/api/tools/discord/channels', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
botToken,
|
||||
serverId,
|
||||
channelId: selectedChannelId,
|
||||
}),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json()
|
||||
throw new Error(errorData.error || 'Failed to fetch Discord channel')
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
if (data.channel) {
|
||||
setSelectedChannel(data.channel)
|
||||
onChannelInfoChange?.(data.channel)
|
||||
} else if (data.channels && data.channels.length > 0) {
|
||||
const channelInfo = data.channels.find(
|
||||
(channel: DiscordChannelInfo) => channel.id === selectedChannelId
|
||||
)
|
||||
if (channelInfo) {
|
||||
setSelectedChannel(channelInfo)
|
||||
onChannelInfoChange?.(channelInfo)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error fetching channel info:', error)
|
||||
setError((error as Error).message)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}, [botToken, serverId, selectedChannelId, onChannelInfoChange])
|
||||
|
||||
// Fetch selected channel info when component mounts or dependencies change
|
||||
useEffect(() => {
|
||||
if (value && botToken && serverId && (!selectedChannel || selectedChannel.id !== value)) {
|
||||
fetchSelectedChannelInfo()
|
||||
}
|
||||
}, [value, botToken, serverId, selectedChannel, fetchSelectedChannelInfo])
|
||||
|
||||
// Sync with external value
|
||||
useEffect(() => {
|
||||
if (value !== selectedChannelId) {
|
||||
setSelectedChannelId(value)
|
||||
|
||||
// Find channel info for the new value
|
||||
if (value && channels.length > 0) {
|
||||
const channelInfo = channels.find((channel) => channel.id === value)
|
||||
setSelectedChannel(channelInfo || null)
|
||||
onChannelInfoChange?.(channelInfo || null)
|
||||
} else if (value) {
|
||||
// If we have a value but no channel info, we might need to fetch it
|
||||
if (!selectedChannel || selectedChannel.id !== value) {
|
||||
fetchSelectedChannelInfo()
|
||||
}
|
||||
} else {
|
||||
setSelectedChannel(null)
|
||||
onChannelInfoChange?.(null)
|
||||
}
|
||||
}
|
||||
}, [
|
||||
value,
|
||||
channels,
|
||||
selectedChannelId,
|
||||
selectedChannel,
|
||||
fetchSelectedChannelInfo,
|
||||
onChannelInfoChange,
|
||||
])
|
||||
|
||||
// Handle channel selection
|
||||
const handleSelectChannel = (channel: DiscordChannelInfo) => {
|
||||
setSelectedChannelId(channel.id)
|
||||
setSelectedChannel(channel)
|
||||
onChange(channel.id, channel)
|
||||
onChannelInfoChange?.(channel)
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
// Clear selection
|
||||
const handleClearSelection = () => {
|
||||
setSelectedChannelId('')
|
||||
setSelectedChannel(null)
|
||||
onChange('', undefined)
|
||||
onChannelInfoChange?.(null)
|
||||
setError(null)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='space-y-2'>
|
||||
<Popover open={open} onOpenChange={handleOpenChange}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant='outline'
|
||||
role='combobox'
|
||||
aria-expanded={open}
|
||||
className='h-10 w-full min-w-0 justify-between'
|
||||
disabled={disabled || !botToken || !serverId}
|
||||
>
|
||||
<div className='flex min-w-0 items-center gap-2 overflow-hidden'>
|
||||
{selectedChannel ? (
|
||||
<>
|
||||
<span className='text-muted-foreground'>#</span>
|
||||
<span className='truncate font-normal'>{selectedChannel.name}</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<DiscordIcon className='h-4 w-4' />
|
||||
<span className='truncate text-muted-foreground'>{label}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<ChevronDown className='ml-2 h-4 w-4 shrink-0 opacity-50' />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className='w-[300px] p-0' align='start'>
|
||||
<Command>
|
||||
<CommandInput placeholder='Search channels...' />
|
||||
<CommandList>
|
||||
<CommandEmpty>
|
||||
{isLoading ? (
|
||||
<div className='flex items-center justify-center p-4'>
|
||||
<RefreshCw className='h-4 w-4 animate-spin' />
|
||||
<span className='ml-2'>Loading channels...</span>
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className='p-4 text-center'>
|
||||
<p className='text-destructive text-sm'>{error}</p>
|
||||
</div>
|
||||
) : channels.length === 0 ? (
|
||||
<div className='p-4 text-center'>
|
||||
<p className='font-medium text-sm'>No channels found</p>
|
||||
<p className='text-muted-foreground text-xs'>
|
||||
The bot needs access to view channels in this server
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className='p-4 text-center'>
|
||||
<p className='font-medium text-sm'>No matching channels</p>
|
||||
</div>
|
||||
)}
|
||||
</CommandEmpty>
|
||||
|
||||
{channels.length > 0 && (
|
||||
<CommandGroup>
|
||||
<div className='px-2 py-1.5 font-medium text-muted-foreground text-xs'>
|
||||
Channels
|
||||
</div>
|
||||
{channels.map((channel) => (
|
||||
<CommandItem
|
||||
key={channel.id}
|
||||
value={`channel-${channel.id}-${channel.name}`}
|
||||
onSelect={() => handleSelectChannel(channel)}
|
||||
className='cursor-pointer'
|
||||
>
|
||||
<div className='flex items-center gap-2 overflow-hidden'>
|
||||
<span className='text-muted-foreground'>#</span>
|
||||
<span className='truncate font-normal'>{channel.name}</span>
|
||||
</div>
|
||||
{channel.id === selectedChannelId && <Check className='ml-auto h-4 w-4' />}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
)}
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
{/* Channel preview */}
|
||||
{showPreview && selectedChannel && (
|
||||
<div className='relative mt-2 rounded-md border border-muted bg-muted/10 p-2'>
|
||||
<div className='absolute top-2 right-2'>
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='icon'
|
||||
className='h-5 w-5 hover:bg-muted'
|
||||
onClick={handleClearSelection}
|
||||
>
|
||||
<X className='h-3 w-3' />
|
||||
</Button>
|
||||
</div>
|
||||
<div className='flex items-center gap-3 pr-4'>
|
||||
<div className='flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-muted/20'>
|
||||
<span className='font-semibold text-muted-foreground'>#</span>
|
||||
</div>
|
||||
<div className='min-w-0 flex-1 overflow-hidden'>
|
||||
<h4 className='truncate font-medium text-xs'>{selectedChannel.name}</h4>
|
||||
<div className='text-muted-foreground text-xs'>Channel ID: {selectedChannel.id}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
export type { ConfluenceFileInfo } from './confluence-file-selector'
|
||||
export { ConfluenceFileSelector } from './confluence-file-selector'
|
||||
export type { DiscordChannelInfo } from './discord-channel-selector'
|
||||
export { DiscordChannelSelector } from './discord-channel-selector'
|
||||
export type { GoogleCalendarInfo } from './google-calendar-selector'
|
||||
export { GoogleCalendarSelector } from './google-calendar-selector'
|
||||
export type { FileInfo } from './google-drive-picker'
|
||||
|
||||
@@ -6,7 +6,6 @@ import { getEnv } from '@/lib/env'
|
||||
import { getProviderIdFromServiceId } from '@/lib/oauth'
|
||||
import {
|
||||
ConfluenceFileSelector,
|
||||
DiscordChannelSelector,
|
||||
GoogleCalendarSelector,
|
||||
GoogleDrivePicker,
|
||||
JiraIssueSelector,
|
||||
@@ -70,8 +69,6 @@ export function FileSelectorInput({
|
||||
const [planIdValue] = useSubBlockValue(blockId, 'planId')
|
||||
const [teamIdValue] = useSubBlockValue(blockId, 'teamId')
|
||||
const [operationValue] = useSubBlockValue(blockId, 'operation')
|
||||
const [serverIdValue] = useSubBlockValue(blockId, 'serverId')
|
||||
const [botTokenValue] = useSubBlockValue(blockId, 'botToken')
|
||||
|
||||
// Determine if the persisted credential belongs to the current viewer
|
||||
// Use service providerId where available (e.g., onedrive/sharepoint) instead of base provider ("microsoft")
|
||||
@@ -87,7 +84,6 @@ export function FileSelectorInput({
|
||||
const provider = subBlock.provider || 'google-drive'
|
||||
const isConfluence = provider === 'confluence'
|
||||
const isJira = provider === 'jira'
|
||||
const isDiscord = provider === 'discord'
|
||||
const isMicrosoftTeams = provider === 'microsoft-teams'
|
||||
const isMicrosoftExcel = provider === 'microsoft-excel'
|
||||
const isMicrosoftWord = provider === 'microsoft-word'
|
||||
@@ -108,13 +104,7 @@ export function FileSelectorInput({
|
||||
''
|
||||
: ''
|
||||
|
||||
// For Discord, we need the bot token and server ID
|
||||
const botToken = isDiscord
|
||||
? (isPreview && previewContextValues?.botToken?.value) || (botTokenValue as string) || ''
|
||||
: ''
|
||||
const serverId = isDiscord
|
||||
? (isPreview && previewContextValues?.serverId?.value) || (serverIdValue as string) || ''
|
||||
: ''
|
||||
// Discord channel selector removed; no special values used here
|
||||
|
||||
// Use preview value when in preview mode, otherwise use store value
|
||||
const value = isPreview ? previewValue : storeValue
|
||||
@@ -154,31 +144,6 @@ export function FileSelectorInput({
|
||||
)
|
||||
}
|
||||
|
||||
// Render Discord channel selector
|
||||
if (isDiscord) {
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className='w-full'>
|
||||
<DiscordChannelSelector
|
||||
value={coerceToIdString(
|
||||
(isPreview && previewValue !== undefined ? previewValue : storeValue) as any
|
||||
)}
|
||||
onChange={(channelId) => setStoreValue(channelId)}
|
||||
botToken={botToken}
|
||||
serverId={serverId}
|
||||
label={subBlock.placeholder || 'Select Discord channel'}
|
||||
disabled={finalDisabled}
|
||||
showPreview={true}
|
||||
/>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)
|
||||
}
|
||||
|
||||
// Render the appropriate picker based on provider
|
||||
if (isConfluence) {
|
||||
const credential = (connectedCredential as string) || ''
|
||||
|
||||
@@ -1,324 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { Check, ChevronDown, RefreshCw, X } from 'lucide-react'
|
||||
import { DiscordIcon } from '@/components/icons'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from '@/components/ui/command'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
|
||||
const logger = createLogger('DiscordServerSelector')
|
||||
|
||||
export interface DiscordServerInfo {
|
||||
id: string
|
||||
name: string
|
||||
icon?: string | null
|
||||
}
|
||||
|
||||
interface DiscordServerSelectorProps {
|
||||
value: string
|
||||
onChange: (value: string, serverInfo?: DiscordServerInfo) => void
|
||||
botToken: string
|
||||
label?: string
|
||||
disabled?: boolean
|
||||
showPreview?: boolean
|
||||
}
|
||||
|
||||
export function DiscordServerSelector({
|
||||
value,
|
||||
onChange,
|
||||
botToken,
|
||||
label = 'Select Discord server',
|
||||
disabled = false,
|
||||
showPreview = true,
|
||||
}: DiscordServerSelectorProps) {
|
||||
const [open, setOpen] = useState(false)
|
||||
const [servers, setServers] = useState<DiscordServerInfo[]>([])
|
||||
const [selectedServerId, setSelectedServerId] = useState(value)
|
||||
const [selectedServer, setSelectedServer] = useState<DiscordServerInfo | null>(null)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [initialFetchDone, setInitialFetchDone] = useState(false)
|
||||
|
||||
// Fetch servers from Discord API
|
||||
const fetchServers = useCallback(async () => {
|
||||
if (!botToken) {
|
||||
setError('Bot token is required')
|
||||
return
|
||||
}
|
||||
|
||||
setIsLoading(true)
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/tools/discord/servers', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ botToken }),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json()
|
||||
throw new Error(errorData.error || 'Failed to fetch Discord servers')
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
setServers(data.servers || [])
|
||||
|
||||
// If we have a selected server ID, find the server info
|
||||
if (selectedServerId) {
|
||||
const serverInfo = data.servers?.find(
|
||||
(server: DiscordServerInfo) => server.id === selectedServerId
|
||||
)
|
||||
if (serverInfo) {
|
||||
setSelectedServer(serverInfo)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error fetching servers:', error)
|
||||
setError((error as Error).message)
|
||||
setServers([])
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
setInitialFetchDone(true)
|
||||
}
|
||||
}, [botToken, selectedServerId])
|
||||
|
||||
// Handle open change - only fetch servers when the dropdown is opened
|
||||
const handleOpenChange = (isOpen: boolean) => {
|
||||
setOpen(isOpen)
|
||||
|
||||
// Only fetch servers when opening the dropdown and if we have a valid token
|
||||
if (isOpen && botToken && (!initialFetchDone || servers.length === 0)) {
|
||||
fetchServers()
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch only the selected server info when component mounts or when selectedServerId changes
|
||||
// This is more efficient than fetching all servers
|
||||
const fetchSelectedServerInfo = useCallback(async () => {
|
||||
if (!botToken || !selectedServerId) return
|
||||
|
||||
setIsLoading(true)
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
// Only fetch the specific server by ID instead of all servers
|
||||
const response = await fetch('/api/tools/discord/servers', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
botToken,
|
||||
serverId: selectedServerId,
|
||||
}),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json()
|
||||
throw new Error(errorData.error || 'Failed to fetch Discord server')
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
if (data.server) {
|
||||
setSelectedServer(data.server)
|
||||
} else if (data.servers && data.servers.length > 0) {
|
||||
const serverInfo = data.servers.find(
|
||||
(server: DiscordServerInfo) => server.id === selectedServerId
|
||||
)
|
||||
if (serverInfo) {
|
||||
setSelectedServer(serverInfo)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error fetching server info:', error)
|
||||
setError((error as Error).message)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}, [botToken, selectedServerId])
|
||||
|
||||
// Fetch selected server info when component mounts or selectedServerId changes
|
||||
useEffect(() => {
|
||||
if (value && botToken && (!selectedServer || selectedServer.id !== value)) {
|
||||
fetchSelectedServerInfo()
|
||||
}
|
||||
}, [value, botToken, selectedServer, fetchSelectedServerInfo])
|
||||
|
||||
// Sync with external value
|
||||
useEffect(() => {
|
||||
if (value !== selectedServerId) {
|
||||
setSelectedServerId(value)
|
||||
|
||||
// Find server info for the new value
|
||||
if (value && servers.length > 0) {
|
||||
const serverInfo = servers.find((server) => server.id === value)
|
||||
setSelectedServer(serverInfo || null)
|
||||
} else if (value) {
|
||||
// If we have a value but no server info, we might need to fetch it
|
||||
if (!selectedServer || selectedServer.id !== value) {
|
||||
fetchSelectedServerInfo()
|
||||
}
|
||||
} else {
|
||||
setSelectedServer(null)
|
||||
}
|
||||
}
|
||||
}, [value, servers, selectedServerId, selectedServer, fetchSelectedServerInfo])
|
||||
|
||||
// Handle server selection
|
||||
const handleSelectServer = (server: DiscordServerInfo) => {
|
||||
setSelectedServerId(server.id)
|
||||
setSelectedServer(server)
|
||||
onChange(server.id, server)
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
// Clear selection
|
||||
const handleClearSelection = () => {
|
||||
setSelectedServerId('')
|
||||
setSelectedServer(null)
|
||||
onChange('', undefined)
|
||||
setError(null)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='space-y-2'>
|
||||
<Popover open={open} onOpenChange={handleOpenChange}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant='outline'
|
||||
role='combobox'
|
||||
aria-expanded={open}
|
||||
className='w-full justify-between'
|
||||
disabled={disabled || !botToken}
|
||||
>
|
||||
{selectedServer ? (
|
||||
<div className='flex items-center gap-2 overflow-hidden'>
|
||||
{selectedServer.icon ? (
|
||||
<img
|
||||
src={selectedServer.icon}
|
||||
alt={selectedServer.name}
|
||||
className='h-4 w-4 rounded-full'
|
||||
/>
|
||||
) : (
|
||||
<DiscordIcon className='h-4 w-4' />
|
||||
)}
|
||||
<span className='truncate font-normal'>{selectedServer.name}</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className='flex items-center gap-2'>
|
||||
<DiscordIcon className='h-4 w-4' />
|
||||
<span className='text-muted-foreground'>{label}</span>
|
||||
</div>
|
||||
)}
|
||||
<ChevronDown className='ml-2 h-4 w-4 shrink-0 opacity-50' />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className='w-[300px] p-0' align='start'>
|
||||
<Command>
|
||||
<CommandInput placeholder='Search servers...' />
|
||||
<CommandList>
|
||||
<CommandEmpty>
|
||||
{isLoading ? (
|
||||
<div className='flex items-center justify-center p-4'>
|
||||
<RefreshCw className='h-4 w-4 animate-spin' />
|
||||
<span className='ml-2'>Loading servers...</span>
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className='p-4 text-center'>
|
||||
<p className='text-destructive text-sm'>{error}</p>
|
||||
</div>
|
||||
) : servers.length === 0 ? (
|
||||
<div className='p-4 text-center'>
|
||||
<p className='font-medium text-sm'>No servers found</p>
|
||||
<p className='text-muted-foreground text-xs'>
|
||||
Make sure your bot is added to at least one server
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className='p-4 text-center'>
|
||||
<p className='font-medium text-sm'>No matching servers</p>
|
||||
</div>
|
||||
)}
|
||||
</CommandEmpty>
|
||||
|
||||
{servers.length > 0 && (
|
||||
<CommandGroup>
|
||||
<div className='px-2 py-1.5 font-medium text-muted-foreground text-xs'>
|
||||
Servers
|
||||
</div>
|
||||
{servers.map((server) => (
|
||||
<CommandItem
|
||||
key={server.id}
|
||||
value={`server-${server.id}-${server.name}`}
|
||||
onSelect={() => handleSelectServer(server)}
|
||||
className='cursor-pointer'
|
||||
>
|
||||
<div className='flex items-center gap-2 overflow-hidden'>
|
||||
{server.icon ? (
|
||||
<img
|
||||
src={server.icon}
|
||||
alt={server.name}
|
||||
className='h-4 w-4 rounded-full'
|
||||
/>
|
||||
) : (
|
||||
<DiscordIcon className='h-4 w-4' />
|
||||
)}
|
||||
<span className='truncate font-normal'>{server.name}</span>
|
||||
</div>
|
||||
{server.id === selectedServerId && <Check className='ml-auto h-4 w-4' />}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
)}
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
{/* Server preview */}
|
||||
{showPreview && selectedServer && (
|
||||
<div className='relative mt-2 rounded-md border border-muted bg-muted/10 p-2'>
|
||||
<div className='absolute top-2 right-2'>
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='icon'
|
||||
className='h-5 w-5 hover:bg-muted'
|
||||
onClick={handleClearSelection}
|
||||
>
|
||||
<X className='h-3 w-3' />
|
||||
</Button>
|
||||
</div>
|
||||
<div className='flex items-center gap-3 pr-4'>
|
||||
<div className='flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-muted/20'>
|
||||
{selectedServer.icon ? (
|
||||
<img
|
||||
src={selectedServer.icon}
|
||||
alt={selectedServer.name}
|
||||
className='h-4 w-4 rounded-full'
|
||||
/>
|
||||
) : (
|
||||
<DiscordIcon className='h-4 w-4' />
|
||||
)}
|
||||
</div>
|
||||
<div className='min-w-0 flex-1 overflow-hidden'>
|
||||
<h4 className='truncate font-medium text-xs'>{selectedServer.name}</h4>
|
||||
<div className='text-muted-foreground text-xs'>Server ID: {selectedServer.id}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -2,10 +2,6 @@
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import {
|
||||
type DiscordServerInfo,
|
||||
DiscordServerSelector,
|
||||
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/project-selector/components/discord-server-selector'
|
||||
import {
|
||||
type JiraProjectInfo,
|
||||
JiraProjectSelector,
|
||||
@@ -44,7 +40,7 @@ export function ProjectSelectorInput({
|
||||
}: ProjectSelectorInputProps) {
|
||||
const { collaborativeSetSubblockValue } = useCollaborativeWorkflow()
|
||||
const [selectedProjectId, setSelectedProjectId] = useState<string>('')
|
||||
const [_projectInfo, setProjectInfo] = useState<JiraProjectInfo | DiscordServerInfo | null>(null)
|
||||
const [_projectInfo, setProjectInfo] = useState<any | null>(null)
|
||||
// Use the proper hook to get the current value and setter
|
||||
const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlock.id)
|
||||
const [connectedCredential] = useSubBlockValue(blockId, 'credential')
|
||||
@@ -60,14 +56,12 @@ export function ProjectSelectorInput({
|
||||
|
||||
// Get provider-specific values
|
||||
const provider = subBlock.provider || 'jira'
|
||||
const isDiscord = provider === 'discord'
|
||||
const isLinear = provider === 'linear'
|
||||
|
||||
// Jira/Discord upstream fields
|
||||
const [jiraDomain] = useSubBlockValue(blockId, 'domain')
|
||||
const [jiraCredential] = useSubBlockValue(blockId, 'credential')
|
||||
const domain = (jiraDomain as string) || ''
|
||||
const botToken = ''
|
||||
|
||||
// Verify Jira credential belongs to current user; if not, treat as absent
|
||||
|
||||
@@ -85,7 +79,7 @@ export function ProjectSelectorInput({
|
||||
// Handle project selection
|
||||
const handleProjectChange = (
|
||||
projectId: string,
|
||||
info?: JiraProjectInfo | DiscordServerInfo | LinearTeamInfo | LinearProjectInfo
|
||||
info?: JiraProjectInfo | LinearTeamInfo | LinearProjectInfo
|
||||
) => {
|
||||
setSelectedProjectId(projectId)
|
||||
setProjectInfo(info || null)
|
||||
@@ -94,34 +88,7 @@ export function ProjectSelectorInput({
|
||||
onProjectSelect?.(projectId)
|
||||
}
|
||||
|
||||
// Render Discord server selector if provider is discord
|
||||
if (isDiscord) {
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className='w-full'>
|
||||
<DiscordServerSelector
|
||||
value={selectedProjectId}
|
||||
onChange={(serverId: string, serverInfo?: DiscordServerInfo) => {
|
||||
handleProjectChange(serverId, serverInfo)
|
||||
}}
|
||||
botToken={botToken}
|
||||
label={subBlock.placeholder || 'Select Discord server'}
|
||||
disabled={disabled || !botToken}
|
||||
showPreview={true}
|
||||
/>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
{!botToken && (
|
||||
<TooltipContent side='top'>
|
||||
<p>Please enter a Bot Token first</p>
|
||||
</TooltipContent>
|
||||
)}
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)
|
||||
}
|
||||
// Discord no longer uses a server selector; fall through to other providers
|
||||
|
||||
// Render Linear team/project selector if provider is linear
|
||||
if (isLinear) {
|
||||
|
||||
@@ -154,7 +154,7 @@ export const AirtableBlock: BlockConfig<AirtableResponse> = {
|
||||
|
||||
// Construct parameters based on operation
|
||||
const baseParams = {
|
||||
accessToken: credential,
|
||||
credential,
|
||||
...rest,
|
||||
}
|
||||
|
||||
|
||||
@@ -55,6 +55,7 @@ export const ConfluenceBlock: BlockConfig<ConfluenceResponse> = {
|
||||
title: 'Select Page',
|
||||
type: 'file-selector',
|
||||
layout: 'full',
|
||||
canonicalParamId: 'pageId',
|
||||
provider: 'confluence',
|
||||
serviceId: 'confluence',
|
||||
placeholder: 'Select Confluence page',
|
||||
@@ -67,6 +68,7 @@ export const ConfluenceBlock: BlockConfig<ConfluenceResponse> = {
|
||||
title: 'Page ID',
|
||||
type: 'short-input',
|
||||
layout: 'full',
|
||||
canonicalParamId: 'pageId',
|
||||
placeholder: 'Enter Confluence page ID',
|
||||
mode: 'advanced',
|
||||
},
|
||||
@@ -112,7 +114,7 @@ export const ConfluenceBlock: BlockConfig<ConfluenceResponse> = {
|
||||
}
|
||||
|
||||
return {
|
||||
accessToken: credential,
|
||||
credential,
|
||||
pageId: effectivePageId,
|
||||
...rest,
|
||||
}
|
||||
|
||||
@@ -34,56 +34,30 @@ export const DiscordBlock: BlockConfig<DiscordResponse> = {
|
||||
password: true,
|
||||
required: true,
|
||||
},
|
||||
// Server selector (basic mode)
|
||||
{
|
||||
id: 'serverId',
|
||||
title: 'Server',
|
||||
type: 'project-selector',
|
||||
layout: 'full',
|
||||
provider: 'discord',
|
||||
serviceId: 'discord',
|
||||
placeholder: 'Select Discord server',
|
||||
dependsOn: ['botToken'],
|
||||
mode: 'basic',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['discord_send_message', 'discord_get_messages', 'discord_get_server'],
|
||||
},
|
||||
},
|
||||
// Manual server ID input (advanced mode)
|
||||
{
|
||||
id: 'manualServerId',
|
||||
title: 'Server ID',
|
||||
type: 'short-input',
|
||||
layout: 'full',
|
||||
placeholder: 'Enter Discord server ID',
|
||||
mode: 'advanced',
|
||||
required: true,
|
||||
provider: 'discord',
|
||||
serviceId: 'discord',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['discord_send_message', 'discord_get_messages', 'discord_get_server'],
|
||||
},
|
||||
},
|
||||
// Channel selector (basic mode)
|
||||
// Channel ID (single input used in all modes)
|
||||
{
|
||||
id: 'channelId',
|
||||
title: 'Channel',
|
||||
type: 'file-selector',
|
||||
layout: 'full',
|
||||
provider: 'discord',
|
||||
serviceId: 'discord',
|
||||
placeholder: 'Select Discord channel',
|
||||
dependsOn: ['botToken', 'serverId'],
|
||||
mode: 'basic',
|
||||
condition: { field: 'operation', value: ['discord_send_message', 'discord_get_messages'] },
|
||||
},
|
||||
// Manual channel ID input (advanced mode)
|
||||
{
|
||||
id: 'manualChannelId',
|
||||
title: 'Channel ID',
|
||||
type: 'short-input',
|
||||
layout: 'full',
|
||||
placeholder: 'Enter Discord channel ID',
|
||||
mode: 'advanced',
|
||||
required: true,
|
||||
provider: 'discord',
|
||||
serviceId: 'discord',
|
||||
condition: { field: 'operation', value: ['discord_send_message', 'discord_get_messages'] },
|
||||
},
|
||||
{
|
||||
@@ -139,56 +113,44 @@ export const DiscordBlock: BlockConfig<DiscordResponse> = {
|
||||
if (!params.botToken) throw new Error('Bot token required for this operation')
|
||||
commonParams.botToken = params.botToken
|
||||
|
||||
// Handle server ID (selector or manual)
|
||||
const effectiveServerId = (params.serverId || params.manualServerId || '').trim()
|
||||
|
||||
// Handle channel ID (selector or manual)
|
||||
const effectiveChannelId = (params.channelId || params.manualChannelId || '').trim()
|
||||
// Single inputs
|
||||
const serverId = (params.serverId || '').trim()
|
||||
const channelId = (params.channelId || '').trim()
|
||||
|
||||
switch (params.operation) {
|
||||
case 'discord_send_message':
|
||||
if (!effectiveServerId) {
|
||||
throw new Error(
|
||||
'Server ID is required. Please select a server or enter a server ID manually.'
|
||||
)
|
||||
if (!serverId) {
|
||||
throw new Error('Server ID is required.')
|
||||
}
|
||||
if (!effectiveChannelId) {
|
||||
throw new Error(
|
||||
'Channel ID is required. Please select a channel or enter a channel ID manually.'
|
||||
)
|
||||
if (!channelId) {
|
||||
throw new Error('Channel ID is required.')
|
||||
}
|
||||
return {
|
||||
...commonParams,
|
||||
serverId: effectiveServerId,
|
||||
channelId: effectiveChannelId,
|
||||
serverId,
|
||||
channelId,
|
||||
content: params.content,
|
||||
}
|
||||
case 'discord_get_messages':
|
||||
if (!effectiveServerId) {
|
||||
throw new Error(
|
||||
'Server ID is required. Please select a server or enter a server ID manually.'
|
||||
)
|
||||
if (!serverId) {
|
||||
throw new Error('Server ID is required.')
|
||||
}
|
||||
if (!effectiveChannelId) {
|
||||
throw new Error(
|
||||
'Channel ID is required. Please select a channel or enter a channel ID manually.'
|
||||
)
|
||||
if (!channelId) {
|
||||
throw new Error('Channel ID is required.')
|
||||
}
|
||||
return {
|
||||
...commonParams,
|
||||
serverId: effectiveServerId,
|
||||
channelId: effectiveChannelId,
|
||||
serverId,
|
||||
channelId,
|
||||
limit: params.limit ? Math.min(Math.max(1, Number(params.limit)), 100) : 10,
|
||||
}
|
||||
case 'discord_get_server':
|
||||
if (!effectiveServerId) {
|
||||
throw new Error(
|
||||
'Server ID is required. Please select a server or enter a server ID manually.'
|
||||
)
|
||||
if (!serverId) {
|
||||
throw new Error('Server ID is required.')
|
||||
}
|
||||
return {
|
||||
...commonParams,
|
||||
serverId: effectiveServerId,
|
||||
serverId,
|
||||
}
|
||||
case 'discord_get_user':
|
||||
return {
|
||||
@@ -205,9 +167,7 @@ export const DiscordBlock: BlockConfig<DiscordResponse> = {
|
||||
operation: { type: 'string', description: 'Operation to perform' },
|
||||
botToken: { type: 'string', description: 'Discord bot token' },
|
||||
serverId: { type: 'string', description: 'Discord server identifier' },
|
||||
manualServerId: { type: 'string', description: 'Manual server identifier' },
|
||||
channelId: { type: 'string', description: 'Discord channel identifier' },
|
||||
manualChannelId: { type: 'string', description: 'Manual channel identifier' },
|
||||
content: { type: 'string', description: 'Message content' },
|
||||
limit: { type: 'number', description: 'Message limit' },
|
||||
userId: { type: 'string', description: 'Discord user identifier' },
|
||||
|
||||
@@ -99,6 +99,7 @@ export const GmailBlock: BlockConfig<GmailToolResponse> = {
|
||||
title: 'Label',
|
||||
type: 'folder-selector',
|
||||
layout: 'full',
|
||||
canonicalParamId: 'folder',
|
||||
provider: 'google-email',
|
||||
serviceId: 'gmail',
|
||||
requiredScopes: [
|
||||
@@ -116,6 +117,7 @@ export const GmailBlock: BlockConfig<GmailToolResponse> = {
|
||||
title: 'Label/Folder',
|
||||
type: 'short-input',
|
||||
layout: 'full',
|
||||
canonicalParamId: 'folder',
|
||||
placeholder: 'Enter Gmail label name (e.g., INBOX, SENT, or custom label)',
|
||||
mode: 'advanced',
|
||||
condition: { field: 'operation', value: 'read_gmail' },
|
||||
@@ -195,13 +197,11 @@ export const GmailBlock: BlockConfig<GmailToolResponse> = {
|
||||
}
|
||||
},
|
||||
params: (params) => {
|
||||
// Pass the credential directly from the credential field
|
||||
const { credential, folder, manualFolder, ...rest } = params
|
||||
|
||||
// Handle folder input (selector or manual)
|
||||
// Handle both selector and manual folder input
|
||||
const effectiveFolder = (folder || manualFolder || '').trim()
|
||||
|
||||
// Ensure folder is always provided for read_gmail operation
|
||||
if (rest.operation === 'read_gmail') {
|
||||
rest.folder = effectiveFolder || 'INBOX'
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@ export const GoogleCalendarBlock: BlockConfig<GoogleCalendarResponse> = {
|
||||
title: 'Calendar',
|
||||
type: 'file-selector',
|
||||
layout: 'full',
|
||||
canonicalParamId: 'calendarId',
|
||||
provider: 'google-calendar',
|
||||
serviceId: 'google-calendar',
|
||||
requiredScopes: ['https://www.googleapis.com/auth/calendar'],
|
||||
@@ -57,6 +58,7 @@ export const GoogleCalendarBlock: BlockConfig<GoogleCalendarResponse> = {
|
||||
title: 'Calendar ID',
|
||||
type: 'short-input',
|
||||
layout: 'full',
|
||||
canonicalParamId: 'calendarId',
|
||||
placeholder: 'Enter calendar ID (e.g., primary or calendar@gmail.com)',
|
||||
mode: 'advanced',
|
||||
},
|
||||
@@ -269,7 +271,7 @@ export const GoogleCalendarBlock: BlockConfig<GoogleCalendarResponse> = {
|
||||
}
|
||||
|
||||
return {
|
||||
accessToken: credential,
|
||||
credential,
|
||||
...processedParams,
|
||||
}
|
||||
},
|
||||
|
||||
@@ -44,6 +44,7 @@ export const GoogleDocsBlock: BlockConfig<GoogleDocsResponse> = {
|
||||
title: 'Select Document',
|
||||
type: 'file-selector',
|
||||
layout: 'full',
|
||||
canonicalParamId: 'documentId',
|
||||
provider: 'google-drive',
|
||||
serviceId: 'google-drive',
|
||||
requiredScopes: [],
|
||||
@@ -59,6 +60,7 @@ export const GoogleDocsBlock: BlockConfig<GoogleDocsResponse> = {
|
||||
title: 'Document ID',
|
||||
type: 'short-input',
|
||||
layout: 'full',
|
||||
canonicalParamId: 'documentId',
|
||||
placeholder: 'Enter document ID',
|
||||
dependsOn: ['credential'],
|
||||
mode: 'advanced',
|
||||
@@ -80,6 +82,7 @@ export const GoogleDocsBlock: BlockConfig<GoogleDocsResponse> = {
|
||||
title: 'Select Parent Folder',
|
||||
type: 'file-selector',
|
||||
layout: 'full',
|
||||
canonicalParamId: 'folderId',
|
||||
provider: 'google-drive',
|
||||
serviceId: 'google-drive',
|
||||
requiredScopes: [],
|
||||
@@ -95,6 +98,7 @@ export const GoogleDocsBlock: BlockConfig<GoogleDocsResponse> = {
|
||||
title: 'Parent Folder ID',
|
||||
type: 'short-input',
|
||||
layout: 'full',
|
||||
canonicalParamId: 'folderId',
|
||||
placeholder: 'Enter parent folder ID (leave empty for root folder)',
|
||||
dependsOn: ['credential'],
|
||||
mode: 'advanced',
|
||||
@@ -139,13 +143,14 @@ export const GoogleDocsBlock: BlockConfig<GoogleDocsResponse> = {
|
||||
const { credential, documentId, manualDocumentId, folderSelector, folderId, ...rest } =
|
||||
params
|
||||
|
||||
// Handle both selector and manual inputs
|
||||
const effectiveDocumentId = (documentId || manualDocumentId || '').trim()
|
||||
const effectiveFolderId = (folderSelector || folderId || '').trim()
|
||||
|
||||
return {
|
||||
...rest,
|
||||
documentId: effectiveDocumentId,
|
||||
folderId: effectiveFolderId,
|
||||
documentId: effectiveDocumentId || undefined,
|
||||
folderId: effectiveFolderId || undefined,
|
||||
credential,
|
||||
}
|
||||
},
|
||||
|
||||
@@ -76,6 +76,7 @@ export const GoogleDriveBlock: BlockConfig<GoogleDriveResponse> = {
|
||||
title: 'Select Parent Folder',
|
||||
type: 'file-selector',
|
||||
layout: 'full',
|
||||
canonicalParamId: 'folderId',
|
||||
provider: 'google-drive',
|
||||
serviceId: 'google-drive',
|
||||
requiredScopes: ['https://www.googleapis.com/auth/drive.file'],
|
||||
@@ -90,6 +91,7 @@ export const GoogleDriveBlock: BlockConfig<GoogleDriveResponse> = {
|
||||
title: 'Parent Folder ID',
|
||||
type: 'short-input',
|
||||
layout: 'full',
|
||||
canonicalParamId: 'folderId',
|
||||
placeholder: 'Enter parent folder ID (leave empty for root folder)',
|
||||
mode: 'advanced',
|
||||
condition: { field: 'operation', value: 'upload' },
|
||||
@@ -150,6 +152,7 @@ export const GoogleDriveBlock: BlockConfig<GoogleDriveResponse> = {
|
||||
title: 'Select Parent Folder',
|
||||
type: 'file-selector',
|
||||
layout: 'full',
|
||||
canonicalParamId: 'folderId',
|
||||
provider: 'google-drive',
|
||||
serviceId: 'google-drive',
|
||||
requiredScopes: ['https://www.googleapis.com/auth/drive.file'],
|
||||
@@ -165,6 +168,7 @@ export const GoogleDriveBlock: BlockConfig<GoogleDriveResponse> = {
|
||||
title: 'Parent Folder ID',
|
||||
type: 'short-input',
|
||||
layout: 'full',
|
||||
canonicalParamId: 'folderId',
|
||||
placeholder: 'Enter parent folder ID (leave empty for root folder)',
|
||||
mode: 'advanced',
|
||||
condition: { field: 'operation', value: 'create_folder' },
|
||||
@@ -175,6 +179,7 @@ export const GoogleDriveBlock: BlockConfig<GoogleDriveResponse> = {
|
||||
title: 'Select Folder',
|
||||
type: 'file-selector',
|
||||
layout: 'full',
|
||||
canonicalParamId: 'folderId',
|
||||
provider: 'google-drive',
|
||||
serviceId: 'google-drive',
|
||||
requiredScopes: ['https://www.googleapis.com/auth/drive.file'],
|
||||
@@ -190,6 +195,7 @@ export const GoogleDriveBlock: BlockConfig<GoogleDriveResponse> = {
|
||||
title: 'Folder ID',
|
||||
type: 'short-input',
|
||||
layout: 'full',
|
||||
canonicalParamId: 'folderId',
|
||||
placeholder: 'Enter folder ID (leave empty for root folder)',
|
||||
mode: 'advanced',
|
||||
condition: { field: 'operation', value: 'list' },
|
||||
@@ -233,8 +239,8 @@ export const GoogleDriveBlock: BlockConfig<GoogleDriveResponse> = {
|
||||
const effectiveFolderId = (folderSelector || manualFolderId || '').trim()
|
||||
|
||||
return {
|
||||
accessToken: credential,
|
||||
folderId: effectiveFolderId,
|
||||
credential,
|
||||
folderId: effectiveFolderId || undefined,
|
||||
pageSize: rest.pageSize ? Number.parseInt(rest.pageSize as string, 10) : undefined,
|
||||
mimeType: mimeType,
|
||||
...rest,
|
||||
|
||||
@@ -45,6 +45,7 @@ export const GoogleSheetsBlock: BlockConfig<GoogleSheetsResponse> = {
|
||||
title: 'Select Sheet',
|
||||
type: 'file-selector',
|
||||
layout: 'full',
|
||||
canonicalParamId: 'spreadsheetId',
|
||||
provider: 'google-drive',
|
||||
serviceId: 'google-drive',
|
||||
requiredScopes: [],
|
||||
@@ -59,6 +60,7 @@ export const GoogleSheetsBlock: BlockConfig<GoogleSheetsResponse> = {
|
||||
title: 'Spreadsheet ID',
|
||||
type: 'short-input',
|
||||
layout: 'full',
|
||||
canonicalParamId: 'spreadsheetId',
|
||||
placeholder: 'ID of the spreadsheet (from URL)',
|
||||
dependsOn: ['credential'],
|
||||
mode: 'advanced',
|
||||
@@ -174,18 +176,13 @@ export const GoogleSheetsBlock: BlockConfig<GoogleSheetsResponse> = {
|
||||
params: (params) => {
|
||||
const { credential, values, spreadsheetId, manualSpreadsheetId, ...rest } = params
|
||||
|
||||
// Parse values from JSON string to array if it exists
|
||||
const parsedValues = values ? JSON.parse(values as string) : undefined
|
||||
|
||||
// Use the selected spreadsheet ID or the manually entered one
|
||||
// If spreadsheetId is provided, it's from the file selector and contains the file ID
|
||||
// If not, fall back to manually entered ID
|
||||
// Handle both selector and manual input
|
||||
const effectiveSpreadsheetId = (spreadsheetId || manualSpreadsheetId || '').trim()
|
||||
|
||||
if (!effectiveSpreadsheetId) {
|
||||
throw new Error(
|
||||
'Spreadsheet ID is required. Please select a spreadsheet or enter an ID manually.'
|
||||
)
|
||||
throw new Error('Spreadsheet ID is required.')
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -59,6 +59,7 @@ export const JiraBlock: BlockConfig<JiraResponse> = {
|
||||
title: 'Select Project',
|
||||
type: 'project-selector',
|
||||
layout: 'full',
|
||||
canonicalParamId: 'projectId',
|
||||
provider: 'jira',
|
||||
serviceId: 'jira',
|
||||
placeholder: 'Select Jira project',
|
||||
@@ -71,6 +72,7 @@ export const JiraBlock: BlockConfig<JiraResponse> = {
|
||||
title: 'Project ID',
|
||||
type: 'short-input',
|
||||
layout: 'full',
|
||||
canonicalParamId: 'projectId',
|
||||
placeholder: 'Enter Jira project ID',
|
||||
dependsOn: ['credential', 'domain'],
|
||||
mode: 'advanced',
|
||||
@@ -81,6 +83,7 @@ export const JiraBlock: BlockConfig<JiraResponse> = {
|
||||
title: 'Select Issue',
|
||||
type: 'file-selector',
|
||||
layout: 'full',
|
||||
canonicalParamId: 'issueKey',
|
||||
provider: 'jira',
|
||||
serviceId: 'jira',
|
||||
placeholder: 'Select Jira issue',
|
||||
@@ -94,6 +97,7 @@ export const JiraBlock: BlockConfig<JiraResponse> = {
|
||||
title: 'Issue Key',
|
||||
type: 'short-input',
|
||||
layout: 'full',
|
||||
canonicalParamId: 'issueKey',
|
||||
placeholder: 'Enter Jira issue key',
|
||||
dependsOn: ['credential', 'domain', 'projectId'],
|
||||
condition: { field: 'operation', value: ['read', 'update'] },
|
||||
@@ -139,19 +143,15 @@ export const JiraBlock: BlockConfig<JiraResponse> = {
|
||||
params: (params) => {
|
||||
const { credential, projectId, manualProjectId, issueKey, manualIssueKey, ...rest } = params
|
||||
|
||||
// Base params that are always needed
|
||||
// Use the selected IDs or the manually entered ones
|
||||
const effectiveProjectId = (projectId || manualProjectId || '').trim()
|
||||
const effectiveIssueKey = (issueKey || manualIssueKey || '').trim()
|
||||
|
||||
const baseParams = {
|
||||
accessToken: credential,
|
||||
credential,
|
||||
domain: params.domain,
|
||||
}
|
||||
|
||||
// Use the selected project ID or the manually entered one
|
||||
const effectiveProjectId = (projectId || manualProjectId || '').trim()
|
||||
|
||||
// Use the selected issue key or the manually entered one
|
||||
const effectiveIssueKey = (issueKey || manualIssueKey || '').trim()
|
||||
|
||||
// Define allowed parameters for each operation
|
||||
switch (params.operation) {
|
||||
case 'write': {
|
||||
if (!effectiveProjectId) {
|
||||
@@ -159,8 +159,6 @@ export const JiraBlock: BlockConfig<JiraResponse> = {
|
||||
'Project ID is required. Please select a project or enter a project ID manually.'
|
||||
)
|
||||
}
|
||||
|
||||
// For write operations, only include write-specific fields
|
||||
const writeParams = {
|
||||
projectId: effectiveProjectId,
|
||||
summary: params.summary || '',
|
||||
@@ -168,7 +166,6 @@ export const JiraBlock: BlockConfig<JiraResponse> = {
|
||||
issueType: params.issueType || 'Task',
|
||||
parent: params.parentIssue ? { key: params.parentIssue } : undefined,
|
||||
}
|
||||
|
||||
return {
|
||||
...baseParams,
|
||||
...writeParams,
|
||||
@@ -185,15 +182,12 @@ export const JiraBlock: BlockConfig<JiraResponse> = {
|
||||
'Issue Key is required. Please select an issue or enter an issue key manually.'
|
||||
)
|
||||
}
|
||||
|
||||
// For update operations, only include update-specific fields
|
||||
const updateParams = {
|
||||
projectId: effectiveProjectId,
|
||||
issueKey: effectiveIssueKey,
|
||||
summary: params.summary || '',
|
||||
description: params.description || '',
|
||||
}
|
||||
|
||||
return {
|
||||
...baseParams,
|
||||
...updateParams,
|
||||
@@ -205,8 +199,6 @@ export const JiraBlock: BlockConfig<JiraResponse> = {
|
||||
'Issue Key is required. Please select an issue or enter an issue key manually.'
|
||||
)
|
||||
}
|
||||
|
||||
// For read operations, only include read-specific fields
|
||||
return {
|
||||
...baseParams,
|
||||
issueKey: effectiveIssueKey,
|
||||
@@ -218,8 +210,6 @@ export const JiraBlock: BlockConfig<JiraResponse> = {
|
||||
'Project ID is required. Please select a project or enter a project ID manually.'
|
||||
)
|
||||
}
|
||||
|
||||
// For read-bulk operations, only include read-bulk-specific fields
|
||||
return {
|
||||
...baseParams,
|
||||
projectId: effectiveProjectId,
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { LinearIcon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import type { BlockConfig, BlockIcon } from '@/blocks/types'
|
||||
import type { LinearResponse } from '@/tools/linear/types'
|
||||
|
||||
const LinearBlockIcon: BlockIcon = (props) => LinearIcon(props as any)
|
||||
|
||||
export const LinearBlock: BlockConfig<LinearResponse> = {
|
||||
type: 'linear',
|
||||
name: 'Linear',
|
||||
@@ -9,7 +11,7 @@ export const LinearBlock: BlockConfig<LinearResponse> = {
|
||||
longDescription:
|
||||
'Integrate with Linear to fetch, filter, and create issues directly from your workflow.',
|
||||
category: 'tools',
|
||||
icon: LinearIcon,
|
||||
icon: LinearBlockIcon,
|
||||
bgColor: '#5E6AD2',
|
||||
subBlocks: [
|
||||
{
|
||||
@@ -39,6 +41,7 @@ export const LinearBlock: BlockConfig<LinearResponse> = {
|
||||
title: 'Team',
|
||||
type: 'project-selector',
|
||||
layout: 'full',
|
||||
canonicalParamId: 'teamId',
|
||||
provider: 'linear',
|
||||
serviceId: 'linear',
|
||||
placeholder: 'Select a team',
|
||||
@@ -50,6 +53,7 @@ export const LinearBlock: BlockConfig<LinearResponse> = {
|
||||
title: 'Project',
|
||||
type: 'project-selector',
|
||||
layout: 'full',
|
||||
canonicalParamId: 'projectId',
|
||||
provider: 'linear',
|
||||
serviceId: 'linear',
|
||||
placeholder: 'Select a project',
|
||||
@@ -62,6 +66,7 @@ export const LinearBlock: BlockConfig<LinearResponse> = {
|
||||
title: 'Team ID',
|
||||
type: 'short-input',
|
||||
layout: 'full',
|
||||
canonicalParamId: 'teamId',
|
||||
placeholder: 'Enter Linear team ID',
|
||||
mode: 'advanced',
|
||||
},
|
||||
@@ -71,6 +76,7 @@ export const LinearBlock: BlockConfig<LinearResponse> = {
|
||||
title: 'Project ID',
|
||||
type: 'short-input',
|
||||
layout: 'full',
|
||||
canonicalParamId: 'projectId',
|
||||
placeholder: 'Enter Linear project ID',
|
||||
mode: 'advanced',
|
||||
},
|
||||
@@ -96,19 +102,15 @@ export const LinearBlock: BlockConfig<LinearResponse> = {
|
||||
tool: (params) =>
|
||||
params.operation === 'write' ? 'linear_create_issue' : 'linear_read_issues',
|
||||
params: (params) => {
|
||||
// Handle team ID (selector or manual)
|
||||
// Handle both selector and manual inputs
|
||||
const effectiveTeamId = (params.teamId || params.manualTeamId || '').trim()
|
||||
|
||||
// Handle project ID (selector or manual)
|
||||
const effectiveProjectId = (params.projectId || params.manualProjectId || '').trim()
|
||||
|
||||
if (!effectiveTeamId) {
|
||||
throw new Error('Team ID is required. Please select a team or enter a team ID manually.')
|
||||
throw new Error('Team ID is required.')
|
||||
}
|
||||
if (!effectiveProjectId) {
|
||||
throw new Error(
|
||||
'Project ID is required. Please select a project or enter a project ID manually.'
|
||||
)
|
||||
throw new Error('Project ID is required.')
|
||||
}
|
||||
|
||||
if (params.operation === 'write') {
|
||||
|
||||
@@ -41,6 +41,7 @@ export const MicrosoftExcelBlock: BlockConfig<MicrosoftExcelResponse> = {
|
||||
title: 'Select Sheet',
|
||||
type: 'file-selector',
|
||||
layout: 'full',
|
||||
canonicalParamId: 'spreadsheetId',
|
||||
provider: 'microsoft-excel',
|
||||
serviceId: 'microsoft-excel',
|
||||
requiredScopes: [],
|
||||
@@ -54,6 +55,7 @@ export const MicrosoftExcelBlock: BlockConfig<MicrosoftExcelResponse> = {
|
||||
title: 'Spreadsheet ID',
|
||||
type: 'short-input',
|
||||
layout: 'full',
|
||||
canonicalParamId: 'spreadsheetId',
|
||||
placeholder: 'Enter spreadsheet ID',
|
||||
dependsOn: ['credential'],
|
||||
mode: 'advanced',
|
||||
@@ -147,6 +149,9 @@ export const MicrosoftExcelBlock: BlockConfig<MicrosoftExcelResponse> = {
|
||||
const { credential, values, spreadsheetId, manualSpreadsheetId, tableName, ...rest } =
|
||||
params
|
||||
|
||||
// Handle both selector and manual input
|
||||
const effectiveSpreadsheetId = (spreadsheetId || manualSpreadsheetId || '').trim()
|
||||
|
||||
// Parse values from JSON string to array if it exists
|
||||
let parsedValues
|
||||
try {
|
||||
@@ -155,13 +160,8 @@ export const MicrosoftExcelBlock: BlockConfig<MicrosoftExcelResponse> = {
|
||||
throw new Error('Invalid JSON format for values')
|
||||
}
|
||||
|
||||
// Use the selected spreadsheet ID or the manually entered one
|
||||
const effectiveSpreadsheetId = (spreadsheetId || manualSpreadsheetId || '').trim()
|
||||
|
||||
if (!effectiveSpreadsheetId) {
|
||||
throw new Error(
|
||||
'Spreadsheet ID is required. Please select a spreadsheet or enter an ID manually.'
|
||||
)
|
||||
throw new Error('Spreadsheet ID is required.')
|
||||
}
|
||||
|
||||
// For table operations, ensure tableName is provided
|
||||
|
||||
@@ -73,11 +73,12 @@ export const MicrosoftPlannerBlock: BlockConfig<MicrosoftPlannerResponse> = {
|
||||
condition: { field: 'operation', value: ['read_task'] },
|
||||
dependsOn: ['credential', 'planId'],
|
||||
mode: 'basic',
|
||||
canonicalParamId: 'taskId',
|
||||
},
|
||||
|
||||
// Advanced mode
|
||||
{
|
||||
id: 'taskId',
|
||||
id: 'manualTaskId',
|
||||
title: 'Manual Task ID',
|
||||
type: 'short-input',
|
||||
layout: 'full',
|
||||
@@ -85,6 +86,7 @@ export const MicrosoftPlannerBlock: BlockConfig<MicrosoftPlannerResponse> = {
|
||||
condition: { field: 'operation', value: ['read_task'] },
|
||||
dependsOn: ['credential', 'planId'],
|
||||
mode: 'advanced',
|
||||
canonicalParamId: 'taskId',
|
||||
},
|
||||
|
||||
{
|
||||
@@ -147,6 +149,7 @@ export const MicrosoftPlannerBlock: BlockConfig<MicrosoftPlannerResponse> = {
|
||||
operation,
|
||||
planId,
|
||||
taskId,
|
||||
manualTaskId,
|
||||
title,
|
||||
description,
|
||||
dueDateTime,
|
||||
@@ -160,13 +163,16 @@ export const MicrosoftPlannerBlock: BlockConfig<MicrosoftPlannerResponse> = {
|
||||
credential,
|
||||
}
|
||||
|
||||
// Handle both selector and manual task ID
|
||||
const effectiveTaskId = (taskId || manualTaskId || '').trim()
|
||||
|
||||
// For read operations
|
||||
if (operation === 'read_task') {
|
||||
const readParams: MicrosoftPlannerBlockParams = { ...baseParams }
|
||||
|
||||
// If taskId is provided, add it (highest priority - get specific task)
|
||||
if (taskId?.trim()) {
|
||||
readParams.taskId = taskId.trim()
|
||||
if (effectiveTaskId) {
|
||||
readParams.taskId = effectiveTaskId
|
||||
}
|
||||
// If no taskId but planId is provided, add planId (get tasks from plan)
|
||||
else if (planId?.trim()) {
|
||||
@@ -220,6 +226,7 @@ export const MicrosoftPlannerBlock: BlockConfig<MicrosoftPlannerResponse> = {
|
||||
credential: { type: 'string', description: 'Microsoft account credential' },
|
||||
planId: { type: 'string', description: 'Plan ID' },
|
||||
taskId: { type: 'string', description: 'Task ID' },
|
||||
manualTaskId: { type: 'string', description: 'Manual Task ID' },
|
||||
title: { type: 'string', description: 'Task title' },
|
||||
description: { type: 'string', description: 'Task description' },
|
||||
dueDateTime: { type: 'string', description: 'Due date' },
|
||||
|
||||
@@ -57,6 +57,7 @@ export const MicrosoftTeamsBlock: BlockConfig<MicrosoftTeamsResponse> = {
|
||||
title: 'Select Team',
|
||||
type: 'file-selector',
|
||||
layout: 'full',
|
||||
canonicalParamId: 'teamId',
|
||||
provider: 'microsoft-teams',
|
||||
serviceId: 'microsoft-teams',
|
||||
requiredScopes: [],
|
||||
@@ -70,6 +71,7 @@ export const MicrosoftTeamsBlock: BlockConfig<MicrosoftTeamsResponse> = {
|
||||
title: 'Team ID',
|
||||
type: 'short-input',
|
||||
layout: 'full',
|
||||
canonicalParamId: 'teamId',
|
||||
placeholder: 'Enter team ID',
|
||||
mode: 'advanced',
|
||||
condition: { field: 'operation', value: ['read_channel', 'write_channel'] },
|
||||
@@ -79,6 +81,7 @@ export const MicrosoftTeamsBlock: BlockConfig<MicrosoftTeamsResponse> = {
|
||||
title: 'Select Chat',
|
||||
type: 'file-selector',
|
||||
layout: 'full',
|
||||
canonicalParamId: 'chatId',
|
||||
provider: 'microsoft-teams',
|
||||
serviceId: 'microsoft-teams',
|
||||
requiredScopes: [],
|
||||
@@ -92,6 +95,7 @@ export const MicrosoftTeamsBlock: BlockConfig<MicrosoftTeamsResponse> = {
|
||||
title: 'Chat ID',
|
||||
type: 'short-input',
|
||||
layout: 'full',
|
||||
canonicalParamId: 'chatId',
|
||||
placeholder: 'Enter chat ID',
|
||||
mode: 'advanced',
|
||||
condition: { field: 'operation', value: ['read_chat', 'write_chat'] },
|
||||
@@ -101,6 +105,7 @@ export const MicrosoftTeamsBlock: BlockConfig<MicrosoftTeamsResponse> = {
|
||||
title: 'Select Channel',
|
||||
type: 'file-selector',
|
||||
layout: 'full',
|
||||
canonicalParamId: 'channelId',
|
||||
provider: 'microsoft-teams',
|
||||
serviceId: 'microsoft-teams',
|
||||
requiredScopes: [],
|
||||
@@ -114,6 +119,7 @@ export const MicrosoftTeamsBlock: BlockConfig<MicrosoftTeamsResponse> = {
|
||||
title: 'Channel ID',
|
||||
type: 'short-input',
|
||||
layout: 'full',
|
||||
canonicalParamId: 'channelId',
|
||||
placeholder: 'Enter channel ID',
|
||||
mode: 'advanced',
|
||||
condition: { field: 'operation', value: ['read_channel', 'write_channel'] },
|
||||
@@ -177,42 +183,27 @@ export const MicrosoftTeamsBlock: BlockConfig<MicrosoftTeamsResponse> = {
|
||||
const effectiveChatId = (chatId || manualChatId || '').trim()
|
||||
const effectiveChannelId = (channelId || manualChannelId || '').trim()
|
||||
|
||||
// Build the parameters based on operation type
|
||||
const baseParams = {
|
||||
...rest,
|
||||
credential,
|
||||
}
|
||||
|
||||
// For chat operations, we need chatId
|
||||
if (operation === 'read_chat' || operation === 'write_chat') {
|
||||
// Don't pass empty chatId - let the tool handle the error
|
||||
if (!effectiveChatId) {
|
||||
throw new Error(
|
||||
'Chat ID is required for chat operations. Please select a chat or enter a chat ID manually.'
|
||||
)
|
||||
}
|
||||
return {
|
||||
...baseParams,
|
||||
chatId: effectiveChatId,
|
||||
throw new Error('Chat ID is required. Please select a chat or enter a chat ID.')
|
||||
}
|
||||
return { ...baseParams, chatId: effectiveChatId }
|
||||
}
|
||||
|
||||
// For channel operations, we need teamId and channelId
|
||||
if (operation === 'read_channel' || operation === 'write_channel') {
|
||||
if (!effectiveTeamId) {
|
||||
throw new Error(
|
||||
'Team ID is required for channel operations. Please select a team or enter a team ID manually.'
|
||||
)
|
||||
throw new Error('Team ID is required for channel operations.')
|
||||
}
|
||||
if (!effectiveChannelId) {
|
||||
throw new Error(
|
||||
'Channel ID is required for channel operations. Please select a channel or enter a channel ID manually.'
|
||||
)
|
||||
}
|
||||
return {
|
||||
...baseParams,
|
||||
teamId: effectiveTeamId,
|
||||
channelId: effectiveChannelId,
|
||||
throw new Error('Channel ID is required for channel operations.')
|
||||
}
|
||||
return { ...baseParams, teamId: effectiveTeamId, channelId: effectiveChannelId }
|
||||
}
|
||||
|
||||
return baseParams
|
||||
|
||||
@@ -282,7 +282,7 @@ export const NotionBlock: BlockConfig<NotionResponse> = {
|
||||
|
||||
return {
|
||||
...rest,
|
||||
accessToken: credential,
|
||||
credential,
|
||||
...(parsedProperties ? { properties: parsedProperties } : {}),
|
||||
...(parsedFilter ? { filter: JSON.stringify(parsedFilter) } : {}),
|
||||
...(parsedSorts ? { sorts: JSON.stringify(parsedSorts) } : {}),
|
||||
|
||||
@@ -66,6 +66,7 @@ export const OneDriveBlock: BlockConfig<OneDriveResponse> = {
|
||||
title: 'Select Parent Folder',
|
||||
type: 'file-selector',
|
||||
layout: 'full',
|
||||
canonicalParamId: 'folderId',
|
||||
provider: 'microsoft',
|
||||
serviceId: 'onedrive',
|
||||
requiredScopes: [
|
||||
@@ -87,6 +88,7 @@ export const OneDriveBlock: BlockConfig<OneDriveResponse> = {
|
||||
title: 'Parent Folder ID',
|
||||
type: 'short-input',
|
||||
layout: 'full',
|
||||
canonicalParamId: 'folderId',
|
||||
placeholder: 'Enter parent folder ID (leave empty for root folder)',
|
||||
dependsOn: ['credential'],
|
||||
mode: 'advanced',
|
||||
@@ -105,6 +107,7 @@ export const OneDriveBlock: BlockConfig<OneDriveResponse> = {
|
||||
title: 'Select Parent Folder',
|
||||
type: 'file-selector',
|
||||
layout: 'full',
|
||||
canonicalParamId: 'folderId',
|
||||
provider: 'microsoft',
|
||||
serviceId: 'onedrive',
|
||||
requiredScopes: [
|
||||
@@ -127,6 +130,7 @@ export const OneDriveBlock: BlockConfig<OneDriveResponse> = {
|
||||
title: 'Parent Folder ID',
|
||||
type: 'short-input',
|
||||
layout: 'full',
|
||||
canonicalParamId: 'folderId',
|
||||
placeholder: 'Enter parent folder ID (leave empty for root folder)',
|
||||
dependsOn: ['credential'],
|
||||
mode: 'advanced',
|
||||
@@ -138,6 +142,7 @@ export const OneDriveBlock: BlockConfig<OneDriveResponse> = {
|
||||
title: 'Select Folder',
|
||||
type: 'file-selector',
|
||||
layout: 'full',
|
||||
canonicalParamId: 'folderId',
|
||||
provider: 'microsoft',
|
||||
serviceId: 'onedrive',
|
||||
requiredScopes: [
|
||||
@@ -160,6 +165,7 @@ export const OneDriveBlock: BlockConfig<OneDriveResponse> = {
|
||||
title: 'Folder ID',
|
||||
type: 'short-input',
|
||||
layout: 'full',
|
||||
canonicalParamId: 'folderId',
|
||||
placeholder: 'Enter folder ID (leave empty for root folder)',
|
||||
dependsOn: ['credential'],
|
||||
mode: 'advanced',
|
||||
@@ -200,12 +206,13 @@ export const OneDriveBlock: BlockConfig<OneDriveResponse> = {
|
||||
params: (params) => {
|
||||
const { credential, folderSelector, manualFolderId, mimeType, ...rest } = params
|
||||
|
||||
// Use folderSelector if provided, otherwise use manualFolderId
|
||||
const effectiveFolderId = (folderSelector || manualFolderId || '').trim()
|
||||
|
||||
return {
|
||||
credential,
|
||||
...rest,
|
||||
accessToken: credential,
|
||||
// Pass both; tools will prioritize manualFolderId over folderSelector
|
||||
folderSelector,
|
||||
manualFolderId,
|
||||
folderId: effectiveFolderId || undefined,
|
||||
pageSize: rest.pageSize ? Number.parseInt(rest.pageSize as string, 10) : undefined,
|
||||
mimeType: mimeType,
|
||||
}
|
||||
|
||||
@@ -142,6 +142,7 @@ export const OutlookBlock: BlockConfig<OutlookResponse> = {
|
||||
title: 'Folder',
|
||||
type: 'folder-selector',
|
||||
layout: 'full',
|
||||
canonicalParamId: 'folder',
|
||||
provider: 'outlook',
|
||||
serviceId: 'outlook',
|
||||
requiredScopes: ['Mail.ReadWrite', 'Mail.ReadBasic', 'Mail.Read'],
|
||||
@@ -156,6 +157,7 @@ export const OutlookBlock: BlockConfig<OutlookResponse> = {
|
||||
title: 'Folder',
|
||||
type: 'short-input',
|
||||
layout: 'full',
|
||||
canonicalParamId: 'folder',
|
||||
placeholder: 'Enter Outlook folder name (e.g., INBOX, SENT, or custom folder)',
|
||||
mode: 'advanced',
|
||||
condition: { field: 'operation', value: 'read_outlook' },
|
||||
@@ -196,13 +198,11 @@ export const OutlookBlock: BlockConfig<OutlookResponse> = {
|
||||
}
|
||||
},
|
||||
params: (params) => {
|
||||
// Pass the credential directly from the credential field
|
||||
const { credential, folder, manualFolder, ...rest } = params
|
||||
|
||||
// Handle folder input (selector or manual)
|
||||
// Handle both selector and manual folder input
|
||||
const effectiveFolder = (folder || manualFolder || '').trim()
|
||||
|
||||
// Set default folder to INBOX if not specified
|
||||
if (rest.operation === 'read_outlook') {
|
||||
rest.folder = effectiveFolder || 'INBOX'
|
||||
}
|
||||
|
||||
@@ -49,6 +49,7 @@ export const SharepointBlock: BlockConfig<SharepointResponse> = {
|
||||
title: 'Select Site',
|
||||
type: 'file-selector',
|
||||
layout: 'full',
|
||||
canonicalParamId: 'siteId',
|
||||
provider: 'microsoft',
|
||||
serviceId: 'sharepoint',
|
||||
requiredScopes: [
|
||||
@@ -99,6 +100,7 @@ export const SharepointBlock: BlockConfig<SharepointResponse> = {
|
||||
title: 'Site ID',
|
||||
type: 'short-input',
|
||||
layout: 'full',
|
||||
canonicalParamId: 'siteId',
|
||||
placeholder: 'Enter site ID (leave empty for root site)',
|
||||
dependsOn: ['credential'],
|
||||
mode: 'advanced',
|
||||
@@ -127,8 +129,8 @@ export const SharepointBlock: BlockConfig<SharepointResponse> = {
|
||||
const effectiveSiteId = (siteSelector || manualSiteId || '').trim()
|
||||
|
||||
return {
|
||||
accessToken: credential,
|
||||
siteId: effectiveSiteId,
|
||||
credential,
|
||||
siteId: effectiveSiteId || undefined,
|
||||
pageSize: rest.pageSize ? Number.parseInt(rest.pageSize as string, 10) : undefined,
|
||||
mimeType: mimeType,
|
||||
...rest,
|
||||
|
||||
@@ -78,6 +78,7 @@ export const SlackBlock: BlockConfig<SlackResponse> = {
|
||||
title: 'Channel',
|
||||
type: 'channel-selector',
|
||||
layout: 'full',
|
||||
canonicalParamId: 'channel',
|
||||
provider: 'slack',
|
||||
placeholder: 'Select Slack channel',
|
||||
mode: 'basic',
|
||||
@@ -89,6 +90,7 @@ export const SlackBlock: BlockConfig<SlackResponse> = {
|
||||
title: 'Channel ID',
|
||||
type: 'short-input',
|
||||
layout: 'full',
|
||||
canonicalParamId: 'channel',
|
||||
placeholder: 'Enter Slack channel ID (e.g., C1234567890)',
|
||||
mode: 'advanced',
|
||||
},
|
||||
@@ -192,13 +194,11 @@ export const SlackBlock: BlockConfig<SlackResponse> = {
|
||||
...rest
|
||||
} = params
|
||||
|
||||
// Handle channel input (selector or manual)
|
||||
// Handle both selector and manual channel input
|
||||
const effectiveChannel = (channel || manualChannel || '').trim()
|
||||
|
||||
if (!effectiveChannel) {
|
||||
throw new Error(
|
||||
'Channel is required. Please select a channel or enter a channel ID manually.'
|
||||
)
|
||||
throw new Error('Channel is required.')
|
||||
}
|
||||
|
||||
const baseParams: Record<string, any> = {
|
||||
|
||||
@@ -57,6 +57,7 @@ export const WealthboxBlock: BlockConfig<WealthboxResponse> = {
|
||||
layout: 'full',
|
||||
placeholder: 'Enter Contact ID',
|
||||
mode: 'basic',
|
||||
canonicalParamId: 'contactId',
|
||||
condition: { field: 'operation', value: ['read_contact', 'write_task', 'write_note'] },
|
||||
},
|
||||
{
|
||||
@@ -64,6 +65,7 @@ export const WealthboxBlock: BlockConfig<WealthboxResponse> = {
|
||||
title: 'Contact ID',
|
||||
type: 'short-input',
|
||||
layout: 'full',
|
||||
canonicalParamId: 'contactId',
|
||||
placeholder: 'Enter Contact ID',
|
||||
mode: 'advanced',
|
||||
condition: { field: 'operation', value: ['read_contact', 'write_task', 'write_note'] },
|
||||
@@ -75,6 +77,7 @@ export const WealthboxBlock: BlockConfig<WealthboxResponse> = {
|
||||
layout: 'full',
|
||||
placeholder: 'Enter Task ID',
|
||||
mode: 'basic',
|
||||
canonicalParamId: 'taskId',
|
||||
condition: { field: 'operation', value: ['read_task'] },
|
||||
},
|
||||
{
|
||||
@@ -82,6 +85,7 @@ export const WealthboxBlock: BlockConfig<WealthboxResponse> = {
|
||||
title: 'Task ID',
|
||||
type: 'short-input',
|
||||
layout: 'full',
|
||||
canonicalParamId: 'taskId',
|
||||
placeholder: 'Enter Task ID',
|
||||
mode: 'advanced',
|
||||
condition: { field: 'operation', value: ['read_task'] },
|
||||
@@ -180,19 +184,15 @@ export const WealthboxBlock: BlockConfig<WealthboxResponse> = {
|
||||
const { credential, operation, contactId, manualContactId, taskId, manualTaskId, ...rest } =
|
||||
params
|
||||
|
||||
// Handle contact ID input (selector or manual)
|
||||
// Handle both selector and manual inputs
|
||||
const effectiveContactId = (contactId || manualContactId || '').trim()
|
||||
|
||||
// Handle task ID input (selector or manual)
|
||||
const effectiveTaskId = (taskId || manualTaskId || '').trim()
|
||||
|
||||
// Build the parameters based on operation type
|
||||
const baseParams = {
|
||||
...rest,
|
||||
credential,
|
||||
}
|
||||
|
||||
// For note operations, we need noteId
|
||||
if (operation === 'read_note' || operation === 'write_note') {
|
||||
return {
|
||||
...baseParams,
|
||||
@@ -200,8 +200,6 @@ export const WealthboxBlock: BlockConfig<WealthboxResponse> = {
|
||||
contactId: effectiveContactId,
|
||||
}
|
||||
}
|
||||
|
||||
// For contact operations, we need contactId
|
||||
if (operation === 'read_contact') {
|
||||
if (!effectiveContactId) {
|
||||
throw new Error('Contact ID is required for contact operations')
|
||||
@@ -211,26 +209,22 @@ export const WealthboxBlock: BlockConfig<WealthboxResponse> = {
|
||||
contactId: effectiveContactId,
|
||||
}
|
||||
}
|
||||
|
||||
// For task operations, we need taskId
|
||||
if (operation === 'read_task') {
|
||||
if (!effectiveTaskId) {
|
||||
if (!taskId?.trim()) {
|
||||
throw new Error('Task ID is required for task operations')
|
||||
}
|
||||
return {
|
||||
...baseParams,
|
||||
taskId: effectiveTaskId,
|
||||
taskId: taskId.trim(),
|
||||
}
|
||||
}
|
||||
|
||||
// For write_task and write_note operations, we need contactId
|
||||
if (operation === 'write_task' || operation === 'write_note') {
|
||||
if (!effectiveContactId) {
|
||||
if (!contactId?.trim()) {
|
||||
throw new Error('Contact ID is required for this operation')
|
||||
}
|
||||
return {
|
||||
...baseParams,
|
||||
contactId: effectiveContactId,
|
||||
contactId: contactId.trim(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -101,6 +101,7 @@ export interface SubBlockConfig {
|
||||
type: SubBlockType
|
||||
layout?: SubBlockLayout
|
||||
mode?: 'basic' | 'advanced' | 'both' // Default is 'both' if not specified
|
||||
canonicalParamId?: string
|
||||
required?: boolean
|
||||
options?:
|
||||
| { label: string; id: string; icon?: React.ComponentType<{ className?: string }> }[]
|
||||
|
||||
@@ -248,15 +248,54 @@ export class Serializer {
|
||||
blockConfig.subBlocks.forEach((subBlockConfig) => {
|
||||
const id = subBlockConfig.id
|
||||
if (
|
||||
params[id] === null &&
|
||||
(params[id] === null || params[id] === undefined) &&
|
||||
subBlockConfig.value &&
|
||||
shouldIncludeField(subBlockConfig, isAdvancedMode)
|
||||
) {
|
||||
// If the value is null and there's a default value function, use it
|
||||
// If the value is absent and there's a default value function, use it
|
||||
params[id] = subBlockConfig.value(params)
|
||||
}
|
||||
})
|
||||
|
||||
// Finally, consolidate canonical parameters (e.g., selector and manual ID into a single param)
|
||||
const canonicalGroups: Record<string, { basic?: string; advanced: string[] }> = {}
|
||||
blockConfig.subBlocks.forEach((sb) => {
|
||||
if (!sb.canonicalParamId) return
|
||||
const key = sb.canonicalParamId
|
||||
if (!canonicalGroups[key]) canonicalGroups[key] = { basic: undefined, advanced: [] }
|
||||
if (sb.mode === 'advanced') canonicalGroups[key].advanced.push(sb.id)
|
||||
else canonicalGroups[key].basic = sb.id
|
||||
})
|
||||
|
||||
Object.entries(canonicalGroups).forEach(([canonicalKey, group]) => {
|
||||
const basicId = group.basic
|
||||
const advancedIds = group.advanced
|
||||
const basicVal = basicId ? params[basicId] : undefined
|
||||
const advancedVal = advancedIds
|
||||
.map((id) => params[id])
|
||||
.find(
|
||||
(v) => v !== undefined && v !== null && (typeof v !== 'string' || v.trim().length > 0)
|
||||
)
|
||||
|
||||
let chosen: any
|
||||
if (advancedVal !== undefined && basicVal !== undefined) {
|
||||
chosen = isAdvancedMode ? advancedVal : basicVal
|
||||
} else if (advancedVal !== undefined) {
|
||||
chosen = advancedVal
|
||||
} else if (basicVal !== undefined) {
|
||||
chosen = isAdvancedMode ? undefined : basicVal
|
||||
} else {
|
||||
chosen = undefined
|
||||
}
|
||||
|
||||
const sourceIds = [basicId, ...advancedIds].filter(Boolean) as string[]
|
||||
sourceIds.forEach((id) => {
|
||||
if (id !== canonicalKey) delete params[id]
|
||||
})
|
||||
if (chosen !== undefined) params[canonicalKey] = chosen
|
||||
else delete params[canonicalKey]
|
||||
})
|
||||
|
||||
return params
|
||||
}
|
||||
|
||||
|
||||
@@ -435,7 +435,6 @@ export const useWorkflowStore = create<WorkflowStoreWithHistory>()(
|
||||
set(newState)
|
||||
pushHistory(set, get, newState, 'Remove connection')
|
||||
get().updateLastSaved()
|
||||
// get().sync.markDirty() // Disabled: Using socket-based sync
|
||||
},
|
||||
|
||||
clear: () => {
|
||||
|
||||
Reference in New Issue
Block a user