diff --git a/apps/sim/app/api/tools/monday/boards/route.ts b/apps/sim/app/api/tools/monday/boards/route.ts new file mode 100644 index 0000000000..ea7d412590 --- /dev/null +++ b/apps/sim/app/api/tools/monday/boards/route.ts @@ -0,0 +1,57 @@ +import { NextResponse } from 'next/server' +import { createLogger } from '@sim/logger' +import { generateRequestId } from '@/lib/core/utils/request' +import { executeMondayQuery, QUERIES } from '@/tools/monday/graphql' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('MondayBoardsAPI') + +interface MondayBoard { + id: string + name: string + description?: string + board_kind: string + state: string +} + +/** + * POST /api/tools/monday/boards + * Fetches active boards from a Monday.com account + * + * @param request - Request containing the Monday.com API key + * @returns JSON response with list of active boards + */ +export async function POST(request: Request) { + try { + const requestId = generateRequestId() + const body = await request.json() + const { apiKey } = body + + if (!apiKey) { + logger.error('Missing API key in request') + return NextResponse.json({ error: 'API key is required' }, { status: 400 }) + } + + logger.info('Fetching Monday.com boards', { requestId }) + + const data = await executeMondayQuery<{ boards: MondayBoard[] }>(apiKey, { + query: QUERIES.GET_BOARDS, + }) + + const boards = (data.boards || []) + .filter((board) => board.state === 'active') + .map((board) => ({ + id: board.id, + name: board.name, + description: board.description, + kind: board.board_kind, + })) + + logger.info(`Successfully fetched ${boards.length} Monday.com boards`, { requestId }) + return NextResponse.json({ items: boards }) + } catch (error) { + logger.error('Error fetching Monday.com boards:', error) + return NextResponse.json({ error: 'Failed to retrieve Monday.com boards' }, { status: 500 }) + } +} diff --git a/apps/sim/app/api/tools/monday/columns/route.ts b/apps/sim/app/api/tools/monday/columns/route.ts new file mode 100644 index 0000000000..11883473c7 --- /dev/null +++ b/apps/sim/app/api/tools/monday/columns/route.ts @@ -0,0 +1,62 @@ +import { NextResponse } from 'next/server' +import { createLogger } from '@sim/logger' +import { generateRequestId } from '@/lib/core/utils/request' +import { executeMondayQuery, QUERIES } from '@/tools/monday/graphql' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('MondayColumnsAPI') + +interface MondayColumn { + id: string + title: string + type: string + settings_str?: string +} + +export async function POST(request: Request) { + try { + const requestId = generateRequestId() + const body = await request.json() + const { apiKey, boardId } = body + + if (!apiKey) { + logger.error('Missing API key in request') + return NextResponse.json({ error: 'API key is required' }, { status: 400 }) + } + + if (!boardId) { + logger.error('Missing board ID in request') + return NextResponse.json({ error: 'Board ID is required' }, { status: 400 }) + } + + const parsedBoardId = parseInt(boardId, 10) + if (isNaN(parsedBoardId)) { + logger.error('Invalid board ID format', { boardId }) + return NextResponse.json({ error: 'Board ID must be a valid number' }, { status: 400 }) + } + + logger.info('Fetching Monday.com columns', { requestId, boardId: parsedBoardId }) + + const data = await executeMondayQuery<{ boards: Array<{ columns: MondayColumn[] }> }>( + apiKey, + { + query: QUERIES.GET_BOARD_COLUMNS, + variables: { boardId: [parsedBoardId] }, + } + ) + + const columns = data.boards?.[0]?.columns || [] + const formattedColumns = columns.map((col) => ({ + id: col.id, + name: col.title, + type: col.type, + })) + + logger.info(`Successfully fetched ${formattedColumns.length} columns`, { requestId }) + return NextResponse.json({ items: formattedColumns }) + } catch (error) { + logger.error('Error fetching Monday.com columns:', error) + return NextResponse.json({ error: 'Failed to retrieve columns' }, { status: 500 }) + } +} diff --git a/apps/sim/app/api/tools/monday/groups/route.ts b/apps/sim/app/api/tools/monday/groups/route.ts new file mode 100644 index 0000000000..60c56aaaac --- /dev/null +++ b/apps/sim/app/api/tools/monday/groups/route.ts @@ -0,0 +1,55 @@ +import { NextResponse } from 'next/server' +import { createLogger } from '@sim/logger' +import { generateRequestId } from '@/lib/core/utils/request' +import { executeMondayQuery, QUERIES } from '@/tools/monday/graphql' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('MondayGroupsAPI') + +interface MondayGroup { + id: string + title: string + color: string +} + +export async function POST(request: Request) { + try { + const requestId = generateRequestId() + const body = await request.json() + const { apiKey, boardId } = body + + if (!apiKey || !boardId) { + return NextResponse.json( + { error: 'API key and board ID are required' }, + { status: 400 } + ) + } + + logger.info('Fetching Monday.com groups', { requestId, boardId }) + + const data = await executeMondayQuery<{ boards: Array<{ groups: MondayGroup[] }> }>( + apiKey, + { + query: QUERIES.GET_BOARD_GROUPS, + variables: { boardId: [parseInt(boardId, 10)] }, + } + ) + + const groups = data.boards?.[0]?.groups || [] + const formattedGroups = groups.map((group) => ({ + id: group.id, + name: group.title, + color: group.color, + })) + + logger.info(`Successfully fetched ${formattedGroups.length} groups`, { requestId }) + return NextResponse.json({ items: formattedGroups }) + } catch (error) { + logger.error('Error fetching Monday.com groups:', error) + return NextResponse.json( + { error: 'Failed to retrieve groups', details: (error as Error).message }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/monday/items/route.ts b/apps/sim/app/api/tools/monday/items/route.ts new file mode 100644 index 0000000000..c977c53457 --- /dev/null +++ b/apps/sim/app/api/tools/monday/items/route.ts @@ -0,0 +1,85 @@ +import { createLogger } from '@sim/logger' +import { NextResponse } from 'next/server' +import { QUERIES } from '@/tools/monday/graphql' + +const logger = createLogger('MondayItemsAPI') + +interface MondayItem { + id: string + name: string +} + +/** + * POST /api/tools/monday/items + * Fetches items from a Monday.com board for selector dropdown + */ +export async function POST(request: Request) { + try { + const body = await request.json() + const { apiKey, boardId } = body + + if (!apiKey) { + logger.warn('Missing apiKey in request') + return NextResponse.json({ error: 'API key is required' }, { status: 400 }) + } + + if (!boardId) { + logger.warn('Missing boardId in request') + return NextResponse.json({ error: 'Board ID is required' }, { status: 400 }) + } + + logger.info('Fetching Monday.com items', { boardId }) + + const response = await fetch('https://api.monday.com/v2', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: apiKey, + 'API-Version': '2024-01', + }, + body: JSON.stringify({ + query: QUERIES.GET_BOARD_ITEMS, + variables: { + boardId: [parseInt(boardId, 10)], + limit: 100, + }, + }), + }) + + if (!response.ok) { + const errorText = await response.text() + logger.error('Monday.com API error', { + status: response.status, + error: errorText, + }) + return NextResponse.json( + { error: `Monday.com API error: ${response.status}` }, + { status: response.status } + ) + } + + const result = await response.json() + + if (result.errors) { + logger.error('Monday.com GraphQL errors', { errors: result.errors }) + return NextResponse.json( + { error: 'Failed to fetch items', details: result.errors }, + { status: 400 } + ) + } + + const items = result.data?.boards?.[0]?.items_page?.items || [] + + logger.info('Successfully fetched Monday.com items', { count: items.length }) + + return NextResponse.json({ + items: items.map((item: MondayItem) => ({ + id: item.id, + name: item.name, + })), + }) + } catch (error) { + logger.error('Unexpected error fetching Monday.com items', { error }) + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + } +} diff --git a/apps/sim/app/api/tools/monday/status-options/route.ts b/apps/sim/app/api/tools/monday/status-options/route.ts new file mode 100644 index 0000000000..aa26c63b44 --- /dev/null +++ b/apps/sim/app/api/tools/monday/status-options/route.ts @@ -0,0 +1,104 @@ +import { NextResponse } from 'next/server' +import { createLogger } from '@sim/logger' +import { generateRequestId } from '@/lib/core/utils/request' +import { executeMondayQuery, QUERIES } from '@/tools/monday/graphql' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('MondayStatusOptionsAPI') + +interface MondayColumn { + id: string + title: string + type: string + settings_str?: string +} + +interface StatusLabel { + id: string + label: string + color: string +} + +interface StatusLabelSettings { + label: string + color?: string +} + +export async function POST(request: Request) { + try { + const requestId = generateRequestId() + const body = await request.json() + const { apiKey, boardId, columnId } = body + + if (!apiKey || !boardId || !columnId) { + return NextResponse.json( + { error: 'API key, board ID, and column ID are required' }, + { status: 400 } + ) + } + + logger.info('Fetching Monday.com status options', { requestId, boardId, columnId }) + + const data = await executeMondayQuery<{ boards: Array<{ columns: MondayColumn[] }> }>( + apiKey, + { + query: QUERIES.GET_COLUMN_SETTINGS, + variables: { + boardId: [parseInt(boardId, 10)], + columnId, + }, + } + ) + + const column = data.boards?.[0]?.columns?.[0] + + if (!column) { + return NextResponse.json({ error: 'Column not found' }, { status: 404 }) + } + + if (column.type !== 'status' && column.type !== 'color') { + return NextResponse.json( + { error: `Column type ${column.type} does not have status options` }, + { status: 400 } + ) + } + + let statusOptions: StatusLabel[] = [] + + if (column.settings_str) { + try { + const settings = JSON.parse(column.settings_str) + const labels = settings.labels || {} + + statusOptions = Object.entries(labels).map(([id, label]: [string, StatusLabelSettings | string]) => { + if (typeof label === 'string') { + return { id, label, color: '#000000' } + } + return { + id, + label: label.label, + color: label.color || '#000000', + } + }) + } catch (parseError) { + logger.error('Failed to parse column settings', { + error: parseError, + settings_str: column.settings_str, + }) + } + } + + logger.info(`Successfully fetched ${statusOptions.length} status options`, { requestId }) + return NextResponse.json({ + items: statusOptions.map((option) => ({ + id: option.id, + name: option.label, + color: option.color, + })), + }) + } catch (error) { + logger.error('Error fetching Monday.com status options:', error) + return NextResponse.json({ error: 'Failed to retrieve status options' }, { status: 500 }) + } +} diff --git a/apps/sim/app/api/tools/monday/subitems/route.ts b/apps/sim/app/api/tools/monday/subitems/route.ts new file mode 100644 index 0000000000..52ead996b4 --- /dev/null +++ b/apps/sim/app/api/tools/monday/subitems/route.ts @@ -0,0 +1,84 @@ +import { createLogger } from '@sim/logger' +import { NextResponse } from 'next/server' +import { QUERIES } from '@/tools/monday/graphql' + +const logger = createLogger('MondaySubitemsAPI') + +interface MondaySubitem { + id: string + name: string +} + +/** + * POST /api/tools/monday/subitems + * Fetches subitems from a Monday.com item for selector dropdown + */ +export async function POST(request: Request) { + try { + const body = await request.json() + const { apiKey, itemId } = body + + if (!apiKey) { + logger.warn('Missing apiKey in request') + return NextResponse.json({ error: 'API key is required' }, { status: 400 }) + } + + if (!itemId) { + logger.warn('Missing itemId in request') + return NextResponse.json({ error: 'Item ID is required' }, { status: 400 }) + } + + logger.info('Fetching Monday.com subitems', { itemId }) + + const response = await fetch('https://api.monday.com/v2', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: apiKey, + 'API-Version': '2024-01', + }, + body: JSON.stringify({ + query: QUERIES.GET_ITEM_SUBITEMS, + variables: { + itemId: [parseInt(itemId, 10)], + }, + }), + }) + + if (!response.ok) { + const errorText = await response.text() + logger.error('Monday.com API error', { + status: response.status, + error: errorText, + }) + return NextResponse.json( + { error: `Monday.com API error: ${response.status}` }, + { status: response.status } + ) + } + + const result = await response.json() + + if (result.errors) { + logger.error('Monday.com GraphQL errors', { errors: result.errors }) + return NextResponse.json( + { error: 'Failed to fetch subitems', details: result.errors }, + { status: 400 } + ) + } + + const subitems = result.data?.items?.[0]?.subitems || [] + + logger.info('Successfully fetched Monday.com subitems', { count: subitems.length }) + + return NextResponse.json({ + items: subitems.map((subitem: MondaySubitem) => ({ + id: subitem.id, + name: subitem.name, + })), + }) + } catch (error) { + logger.error('Unexpected error fetching Monday.com subitems', { error }) + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + } +} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/file-selector/file-selector-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/file-selector/file-selector-input.tsx index 27415b31a4..d0ce42dc14 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/file-selector/file-selector-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/file-selector/file-selector-input.tsx @@ -49,6 +49,20 @@ export function FileSelectorInput({ const [teamIdValueFromStore] = useSubBlockValue(blockId, 'teamId') const [siteIdValueFromStore] = useSubBlockValue(blockId, 'siteId') const [collectionIdValueFromStore] = useSubBlockValue(blockId, 'collectionId') + const [apiKeyValueFromStore] = useSubBlockValue(blockId, 'apiKey') + const [boardIdValueFromStore] = useSubBlockValue(blockId, 'board_id') + const [boardIdCamelFromStore] = useSubBlockValue(blockId, 'boardId') + const [boardIdListFromStore] = useSubBlockValue(blockId, 'board_id_list') + const [boardIdUpdateFromStore] = useSubBlockValue(blockId, 'board_id_update') + const [boardIdGetFromStore] = useSubBlockValue(blockId, 'board_id_get') + const [groupIdValueFromStore] = useSubBlockValue(blockId, 'group_id') + const [groupIdCamelFromStore] = useSubBlockValue(blockId, 'groupId') + const [groupIdListFromStore] = useSubBlockValue(blockId, 'group_id_list') + const [columnIdValueFromStore] = useSubBlockValue(blockId, 'column_id') + const [columnIdCamelFromStore] = useSubBlockValue(blockId, 'columnId') + const [itemIdValueFromStore] = useSubBlockValue(blockId, 'item_id') + const [itemIdCamelFromStore] = useSubBlockValue(blockId, 'itemId') + const [itemIdGetFromStore] = useSubBlockValue(blockId, 'item_id_get') const connectedCredential = previewContextValues?.credential ?? connectedCredentialFromStore const domainValue = previewContextValues?.domain ?? domainValueFromStore @@ -57,6 +71,32 @@ export function FileSelectorInput({ const teamIdValue = previewContextValues?.teamId ?? teamIdValueFromStore const siteIdValue = previewContextValues?.siteId ?? siteIdValueFromStore const collectionIdValue = previewContextValues?.collectionId ?? collectionIdValueFromStore + const apiKeyValue = previewContextValues?.apiKey ?? apiKeyValueFromStore + const boardIdValue = + previewContextValues?.board_id ?? + previewContextValues?.boardId ?? + boardIdValueFromStore ?? + boardIdCamelFromStore ?? + boardIdListFromStore ?? + boardIdUpdateFromStore ?? + boardIdGetFromStore + const groupIdValue = + previewContextValues?.group_id ?? + previewContextValues?.groupId ?? + groupIdValueFromStore ?? + groupIdCamelFromStore ?? + groupIdListFromStore + const columnIdValue = + previewContextValues?.column_id ?? + previewContextValues?.columnId ?? + columnIdValueFromStore ?? + columnIdCamelFromStore + const itemIdValue = + previewContextValues?.item_id ?? + previewContextValues?.itemId ?? + itemIdValueFromStore ?? + itemIdCamelFromStore ?? + itemIdGetFromStore const normalizedCredentialId = typeof connectedCredential === 'string' @@ -81,6 +121,11 @@ export function FileSelectorInput({ teamId: (teamIdValue as string) || undefined, siteId: (siteIdValue as string) || undefined, collectionId: (collectionIdValue as string) || undefined, + apiKey: (apiKeyValue as string) || undefined, + boardId: (boardIdValue as string) || undefined, + groupId: (groupIdValue as string) || undefined, + columnId: (columnIdValue as string) || undefined, + itemId: (itemIdValue as string) || undefined, }) }, [ subBlock, @@ -92,9 +137,16 @@ export function FileSelectorInput({ teamIdValue, siteIdValue, collectionIdValue, + apiKeyValue, + boardIdValue, + groupIdValue, + columnIdValue, + itemIdValue, ]) - const missingCredential = !normalizedCredentialId + const isMondaySelector = selectorResolution?.key?.startsWith('monday.') + const missingCredential = !isMondaySelector && !normalizedCredentialId + const missingApiKey = isMondaySelector && !selectorResolution?.context.apiKey const missingDomain = selectorResolution?.key && (selectorResolution.key === 'confluence.pages' || selectorResolution.key === 'jira.issues') && @@ -109,16 +161,34 @@ export function FileSelectorInput({ selectorResolution?.key === 'webflow.collections' && !selectorResolution.context.siteId const missingCollection = selectorResolution?.key === 'webflow.items' && !selectorResolution.context.collectionId + const missingBoard = + isMondaySelector && + (selectorResolution?.key === 'monday.groups' || + selectorResolution?.key === 'monday.columns' || + selectorResolution?.key === 'monday.items') && + !selectorResolution?.context.boardId + const missingColumn = + isMondaySelector && + selectorResolution?.key === 'monday.status-options' && + !selectorResolution?.context.columnId + const missingItem = + isMondaySelector && + selectorResolution?.key === 'monday.subitems' && + !selectorResolution?.context.itemId const disabledReason = finalDisabled || isForeignCredential || missingCredential || + missingApiKey || missingDomain || missingProject || missingPlan || missingSite || missingCollection || + missingBoard || + missingColumn || + missingItem || !selectorResolution?.key if (!selectorResolution?.key) { diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/selector-combobox/selector-combobox.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/selector-combobox/selector-combobox.tsx index f72930bdc6..7ecef4f6df 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/selector-combobox/selector-combobox.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/selector-combobox/selector-combobox.tsx @@ -1,5 +1,6 @@ import type React from 'react' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { createLogger } from '@sim/logger' import { Combobox as EditableCombobox } from '@/components/emcn/components' import { SubBlockInputController } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/sub-block-input-controller' import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value' @@ -11,6 +12,8 @@ import { useSelectorOptions, } from '@/hooks/selectors/use-selector-query' +const logger = createLogger('SelectorCombobox') + interface SelectorComboboxProps { blockId: string subBlock: SubBlockConfig @@ -38,6 +41,60 @@ export function SelectorCombobox({ onOptionChange, allowSearch = true, }: SelectorComboboxProps) { + // For Monday.com selectors, read apiKey and boardId directly from block state + const [apiKeyFromBlock] = useSubBlockValue(blockId, 'apiKey') + const [boardIdFromBlock] = useSubBlockValue(blockId, 'board_id') + const [boardIdCamelFromBlock] = useSubBlockValue(blockId, 'boardId') + const [columnIdFromBlock] = useSubBlockValue(blockId, 'column_id') + const [columnIdCamelFromBlock] = useSubBlockValue(blockId, 'columnId') + + // Merge Monday.com specific values into context if they're missing + const enrichedContext = useMemo( + () => + selectorKey.startsWith('monday.') + ? { + ...selectorContext, + apiKey: selectorContext.apiKey || apiKeyFromBlock, + boardId: selectorContext.boardId || boardIdFromBlock || boardIdCamelFromBlock, + columnId: selectorContext.columnId || columnIdFromBlock || columnIdCamelFromBlock, + } + : selectorContext, + [ + selectorKey, + selectorContext, + apiKeyFromBlock, + boardIdFromBlock, + boardIdCamelFromBlock, + columnIdFromBlock, + columnIdCamelFromBlock, + ] + ) + + // For Monday selectors, override disabled if we have apiKey and required dependencies + let actualDisabled = disabled + if (selectorKey.startsWith('monday.')) { + if (selectorKey === 'monday.boards') { + // boards only needs apiKey + actualDisabled = !enrichedContext.apiKey + } else if (selectorKey === 'monday.columns' || selectorKey === 'monday.groups') { + // columns/groups need apiKey AND boardId + actualDisabled = !enrichedContext.apiKey || !enrichedContext.boardId + } else if (selectorKey === 'monday.status-options') { + // status-options need apiKey, boardId, AND columnId + actualDisabled = !enrichedContext.apiKey || !enrichedContext.boardId || !enrichedContext.columnId + } + } + + logger.info('SelectorCombobox render', { + subBlockId: subBlock.id, + selectorKey, + disabled, + actualDisabled, + hasApiKey: !!enrichedContext.apiKey, + apiKeyFromBlock, + enrichedContext, + }) + const [storeValueRaw, setStoreValue] = useSubBlockValue( blockId, subBlock.id @@ -52,11 +109,11 @@ export function SelectorCombobox({ isLoading, error, } = useSelectorOptions(selectorKey, { - context: selectorContext, + context: enrichedContext, search: allowSearch ? searchTerm : undefined, }) const { data: detailOption } = useSelectorOptionDetail(selectorKey, { - context: selectorContext, + context: enrichedContext, detailId: activeValue, }) const optionMap = useSelectorOptionMap(options, detailOption ?? undefined) @@ -89,12 +146,12 @@ export function SelectorCombobox({ const handleSelection = useCallback( (value: string) => { - if (readOnly || disabled) return + if (readOnly || actualDisabled) return setStoreValue(value) setIsEditing(false) onOptionChange?.(value) }, - [setStoreValue, onOptionChange, readOnly, disabled] + [setStoreValue, onOptionChange, readOnly, actualDisabled] ) return ( @@ -104,7 +161,7 @@ export function SelectorCombobox({ subBlockId={subBlock.id} config={subBlock} value={activeValue ?? ''} - disabled={disabled || readOnly} + disabled={actualDisabled || readOnly} isPreview={isPreview} > {({ ref, onDrop, onDragOver }) => ( @@ -127,7 +184,7 @@ export function SelectorCombobox({ } }} placeholder={placeholder || subBlock.placeholder || 'Select an option'} - disabled={disabled || readOnly} + disabled={actualDisabled || readOnly} editable={allowSearch} filterOptions={allowSearch} inputRef={ref as React.RefObject} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-depends-on-gate.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-depends-on-gate.ts index 3c145f52fc..3c22641474 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-depends-on-gate.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-depends-on-gate.ts @@ -1,10 +1,13 @@ 'use client' import { useMemo } from 'react' +import { createLogger } from '@sim/logger' import type { SubBlockConfig } from '@/blocks/types' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' import { useSubBlockStore } from '@/stores/workflows/subblock/store' +const logger = createLogger('useDependsOnGate') + type DependsOnConfig = string[] | { all?: string[]; any?: string[] } /** @@ -144,9 +147,22 @@ export function useDependsOnGate( const finalDisabled = disabledProp || isPreview || blocked + // Debug logging for dependency issues + if (typeof window !== 'undefined' && allDependsOnFields.includes('apiKey')) { + logger.info('Dependency gate debug', { + subBlockId: subBlock.id, + allDependsOnFields, + dependencyValuesMap, + depsSatisfied, + blocked, + finalDisabled, + }) + } + return { dependsOn, dependencyValues, + dependencyValuesMap, depsSatisfied, blocked, finalDisabled, diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block.tsx index 310c3df0dd..1478255db5 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block.tsx @@ -1,5 +1,6 @@ import { type JSX, type MouseEvent, memo, useRef, useState } from 'react' import { AlertTriangle, Wand2 } from 'lucide-react' +import { createLogger } from '@sim/logger' import { Label, Tooltip } from '@/components/emcn/components' import { Button } from '@/components/ui/button' import { cn } from '@/lib/core/utils/cn' @@ -44,6 +45,8 @@ import { import { useDependsOnGate } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-depends-on-gate' import type { SubBlockConfig } from '@/blocks/types' +const logger = createLogger('SubBlock') + /** * Interface for wand control handlers exposed by sub-block inputs */ @@ -392,7 +395,7 @@ function SubBlockComponent({ // Use dependsOn gating to compute final disabled state // Only pass previewContextValues when in preview mode to avoid format mismatches - const { finalDisabled: gatedDisabled } = useDependsOnGate(blockId, config, { + const { finalDisabled: gatedDisabled, dependencyValuesMap, depsSatisfied } = useDependsOnGate(blockId, config, { disabled, isPreview, previewContextValues: isPreview ? subBlockValues : undefined, @@ -400,6 +403,19 @@ function SubBlockComponent({ const isDisabled = gatedDisabled + // Debug logging for Monday selector issues + if (config.serviceId === 'monday' && typeof window !== 'undefined') { + logger.info('Sub-block debug', { + subBlockId: config.id, + type: config.type, + dependsOn: config.dependsOn, + dependencyValuesMap, + depsSatisfied, + gatedDisabled, + isDisabled, + }) + } + /** * Selects and renders the appropriate input component based on config.type. * @@ -656,6 +672,12 @@ function SubBlockComponent({ ) case 'file-selector': + logger.info('Rendering file-selector', { + subBlockId: config.id, + serviceId: config.serviceId, + isDisabled, + dependsOn: config.dependsOn, + }) return ( {config.note}

+ )} ) } diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/sub-block/components/file-selector/file-selector-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/sub-block/components/file-selector/file-selector-input.tsx index 3f4dab8519..6f664f600d 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/sub-block/components/file-selector/file-selector-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/sub-block/components/file-selector/file-selector-input.tsx @@ -31,6 +31,8 @@ export function FileSelectorInput({ previewValue, previewContextValues, }: FileSelectorInputProps) { + console.log('[FileSelectorInput RENDER]', { subBlockId: subBlock.id, serviceId: subBlock.serviceId }) + const { collaborativeSetSubblockValue } = useCollaborativeWorkflow() const { activeWorkflowId } = useWorkflowRegistry() const params = useParams() @@ -49,6 +51,16 @@ export function FileSelectorInput({ const [teamIdValueFromStore] = useSubBlockValue(blockId, 'teamId') const [siteIdValueFromStore] = useSubBlockValue(blockId, 'siteId') const [collectionIdValueFromStore] = useSubBlockValue(blockId, 'collectionId') + const [apiKeyValueFromStore] = useSubBlockValue(blockId, 'apiKey') + const [boardIdValueFromStore] = useSubBlockValue(blockId, 'board_id') + const [boardIdCamelFromStore] = useSubBlockValue(blockId, 'boardId') + const [boardIdListFromStore] = useSubBlockValue(blockId, 'board_id_list') + const [boardIdUpdateFromStore] = useSubBlockValue(blockId, 'board_id_update') + const [groupIdValueFromStore] = useSubBlockValue(blockId, 'group_id') + const [groupIdCamelFromStore] = useSubBlockValue(blockId, 'groupId') + const [groupIdListFromStore] = useSubBlockValue(blockId, 'group_id_list') + const [columnIdValueFromStore] = useSubBlockValue(blockId, 'column_id') + const [columnIdCamelFromStore] = useSubBlockValue(blockId, 'columnId') const connectedCredential = previewContextValues?.credential ?? connectedCredentialFromStore const domainValue = previewContextValues?.domain ?? domainValueFromStore @@ -57,6 +69,25 @@ export function FileSelectorInput({ const teamIdValue = previewContextValues?.teamId ?? teamIdValueFromStore const siteIdValue = previewContextValues?.siteId ?? siteIdValueFromStore const collectionIdValue = previewContextValues?.collectionId ?? collectionIdValueFromStore + const apiKeyValue = previewContextValues?.apiKey ?? apiKeyValueFromStore + const boardIdValue = + previewContextValues?.board_id ?? + previewContextValues?.boardId ?? + boardIdValueFromStore ?? + boardIdCamelFromStore ?? + boardIdListFromStore ?? + boardIdUpdateFromStore + const groupIdValue = + previewContextValues?.group_id ?? + previewContextValues?.groupId ?? + groupIdValueFromStore ?? + groupIdCamelFromStore ?? + groupIdListFromStore + const columnIdValue = + previewContextValues?.column_id ?? + previewContextValues?.columnId ?? + columnIdValueFromStore ?? + columnIdCamelFromStore const normalizedCredentialId = typeof connectedCredential === 'string' @@ -81,6 +112,10 @@ export function FileSelectorInput({ teamId: (teamIdValue as string) || undefined, siteId: (siteIdValue as string) || undefined, collectionId: (collectionIdValue as string) || undefined, + apiKey: (apiKeyValue as string) || undefined, + boardId: (boardIdValue as string) || undefined, + groupId: (groupIdValue as string) || undefined, + columnId: (columnIdValue as string) || undefined, }) }, [ subBlock, @@ -92,9 +127,43 @@ export function FileSelectorInput({ teamIdValue, siteIdValue, collectionIdValue, + apiKeyValue, + boardIdValue, + groupIdValue, + columnIdValue, + boardIdValueFromStore, + boardIdCamelFromStore, + boardIdListFromStore, + boardIdUpdateFromStore, + groupIdValueFromStore, + groupIdCamelFromStore, + groupIdListFromStore, + columnIdValueFromStore, + columnIdCamelFromStore, ]) - const missingCredential = !normalizedCredentialId + const isMondaySelector = selectorResolution?.key?.startsWith('monday.') + const missingCredential = !isMondaySelector && !normalizedCredentialId + const missingApiKey = isMondaySelector && !selectorResolution?.context.apiKey + + // Debug logging for Monday selectors + if (isMondaySelector && typeof window !== 'undefined') { + console.log('[Monday Selector Debug]', { + subBlockId: subBlock.id, + selectorKey: selectorResolution?.key, + apiKeyFromStore: apiKeyValueFromStore, + apiKeyValue: apiKeyValue, + contextApiKey: selectorResolution?.context.apiKey, + missingApiKey, + finalDisabled, + disabledReason: { + finalDisabled, + isForeignCredential, + missingCredential, + missingApiKey, + }, + }) + } const missingDomain = selectorResolution?.key && (selectorResolution.key === 'confluence.pages' || selectorResolution.key === 'jira.issues') && @@ -109,16 +178,21 @@ export function FileSelectorInput({ selectorResolution?.key === 'webflow.collections' && !selectorResolution?.context.siteId const missingCollection = selectorResolution?.key === 'webflow.items' && !selectorResolution?.context.collectionId + const missingBoardId = + (selectorResolution?.key === 'monday.columns' || selectorResolution?.key === 'monday.groups') && + !selectorResolution?.context.boardId const disabledReason = finalDisabled || - isForeignCredential || + (!isMondaySelector && isForeignCredential) || missingCredential || + missingApiKey || missingDomain || missingProject || missingPlan || missingSite || missingCollection || + missingBoardId || !selectorResolution?.key if (!selectorResolution?.key) { @@ -149,6 +223,12 @@ export function FileSelectorInput({ allowSearch={selectorResolution.allowSearch} onOptionChange={(value) => { if (!isPreview) { + console.log('[FileSelectorInput] Setting value', { + blockId, + subBlockId: subBlock.id, + value, + valueType: typeof value, + }) collaborativeSetSubblockValue(blockId, subBlock.id, value) } }} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts index 61af9f0624..8cdc7349a8 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts @@ -903,7 +903,12 @@ export function useWorkflowExecution() { loops: executionWorkflowState.loops, parallels: executionWorkflowState.parallels, } - : undefined, + : { + blocks: filteredStates, + edges: workflowEdges, + loops: latestWorkflowState.loops || {}, + parallels: latestWorkflowState.parallels || {}, + }, callbacks: { onExecutionStarted: (data) => { logger.info('Server execution started:', data) diff --git a/apps/sim/blocks/blocks/monday.ts b/apps/sim/blocks/blocks/monday.ts new file mode 100644 index 0000000000..b302dbcde0 --- /dev/null +++ b/apps/sim/blocks/blocks/monday.ts @@ -0,0 +1,220 @@ +import { MondayIcon } from '@/components/icons' +import type { BlockConfig } from '@/blocks/types' +import { AuthMode } from '@/blocks/types' +import type { MondayResponse } from '@/tools/monday/types' +import { getTrigger } from '@/triggers' + +export const MondayBlock: BlockConfig = { + type: 'monday', + name: 'Monday', + description: 'Create and manage items on Monday boards', + authMode: AuthMode.ApiKey, + triggerAllowed: true, + longDescription: + 'Integrate with Monday work management platform. Create items, update column values, list items, and manage your boards programmatically.', + docsLink: 'https://docs.monday.com/api', + category: 'tools', + bgColor: '#FFFFFF', + icon: MondayIcon, + subBlocks: [ + { + id: 'operation', + title: 'Operation', + type: 'dropdown', + options: [ + { label: 'Create Item', id: 'monday_create_item' }, + { label: 'Update Item', id: 'monday_update_item' }, + { label: 'Get Item', id: 'monday_get_item' }, + { label: 'List Items', id: 'monday_list_items' }, + ], + value: () => 'monday_create_item', + required: true, + }, + { + id: 'apiKey', + title: 'API Key', + type: 'short-input', + placeholder: 'Enter your Monday.com API key', + password: true, + required: true, + note: 'The API key can be found by logging into Monday.com, clicking on your profile picture, opening the Developers section. Then, click on Developers in the header menu and select My Access Tokens to get your personal API token.', + }, + // CREATE ITEM fields + { + id: 'board_id', + title: 'Board', + type: 'file-selector', + serviceId: 'monday', + placeholder: 'Select a Monday.com board', + required: true, + condition: { field: 'operation', value: 'monday_create_item' }, + dependsOn: ['apiKey'], + }, + { + id: 'group_id', + title: 'Group (Optional)', + type: 'file-selector', + serviceId: 'monday', + placeholder: 'Select a group/section (optional)', + required: false, + condition: { field: 'operation', value: 'monday_create_item' }, + dependsOn: ['apiKey', 'board_id'], + }, + { + id: 'item_name', + title: 'Item Name', + type: 'short-input', + placeholder: 'Enter item name', + required: true, + condition: { field: 'operation', value: 'monday_create_item' }, + }, + { + id: 'column_values', + title: 'Column Values (JSON)', + type: 'code', + language: 'json', + placeholder: '{"status": "Working on it", "text": "Example"}', + required: false, + condition: { field: 'operation', value: 'monday_create_item' }, + }, + // UPDATE ITEM fields + { + id: 'board_id_update', + title: 'Board', + type: 'file-selector', + serviceId: 'monday', + canonicalParamId: 'board_id', + placeholder: 'Select a Monday.com board', + required: true, + condition: { field: 'operation', value: 'monday_update_item' }, + dependsOn: ['apiKey'], + }, + { + id: 'item_id', + title: 'Item', + type: 'file-selector', + serviceId: 'monday', + placeholder: 'Select an item to update', + required: true, + condition: { field: 'operation', value: 'monday_update_item' }, + dependsOn: ['apiKey', 'board_id_update'], + }, + { + id: 'subitem_id', + title: 'Sub-Item (Optional)', + type: 'file-selector', + serviceId: 'monday', + placeholder: 'Select a sub-item to update (optional)', + required: false, + condition: { field: 'operation', value: 'monday_update_item' }, + dependsOn: ['apiKey', 'item_id'], + }, + { + id: 'column_values_update', + title: 'Column Values (JSON)', + type: 'code', + language: 'json', + canonicalParamId: 'column_values', + placeholder: '{"status": "Done", "text": "Updated"}', + required: true, + condition: { field: 'operation', value: 'monday_update_item' }, + }, + // GET ITEM fields + { + id: 'board_id_get', + title: 'Board', + type: 'file-selector', + serviceId: 'monday', + canonicalParamId: 'board_id', + placeholder: 'Select a Monday.com board', + required: true, + condition: { field: 'operation', value: 'monday_get_item' }, + dependsOn: ['apiKey'], + }, + { + id: 'item_id_get', + title: 'Item', + type: 'file-selector', + serviceId: 'monday', + canonicalParamId: 'item_id', + placeholder: 'Select an item to retrieve', + required: true, + condition: { field: 'operation', value: 'monday_get_item' }, + dependsOn: ['apiKey', 'board_id_get'], + }, + // LIST ITEMS fields + { + id: 'board_id_list', + title: 'Board', + type: 'file-selector', + serviceId: 'monday', + canonicalParamId: 'board_id', + placeholder: 'Select a board', + required: true, + condition: { field: 'operation', value: 'monday_list_items' }, + dependsOn: ['apiKey'], + }, + { + id: 'group_id_list', + title: 'Group (Optional)', + type: 'file-selector', + serviceId: 'monday', + canonicalParamId: 'group_id', + placeholder: 'Filter by group (optional)', + required: false, + condition: { field: 'operation', value: 'monday_list_items' }, + dependsOn: ['apiKey', 'board_id_list'], + }, + { + id: 'limit', + title: 'Limit', + type: 'slider', + min: 1, + max: 100, + step: 1, + defaultValue: 25, + required: false, + condition: { field: 'operation', value: 'monday_list_items' }, + }, + // Trigger subBlocks + // Include all fields from first trigger (has selectedTriggerId + all fields with conditions) + ...getTrigger('monday_new_item').subBlocks, + // Skip only selectedTriggerId from second trigger (index 0), keep apiKey and rest + ...getTrigger('monday_column_changed').subBlocks.slice(1), + ], + tools: { + access: [ + 'monday_create_item', + 'monday_update_item', + 'monday_get_item', + 'monday_list_items', + ], + config: { + tool: (params) => { + return params.operation || 'monday_create_item' + }, + params: (inputs) => { + const { operation, ...rest } = inputs + return rest + }, + }, + }, + inputs: { + operation: { type: 'string', description: 'Operation to perform' }, + apiKey: { type: 'string', description: 'Monday.com API key' }, + board_id: { type: 'string', description: 'Board ID' }, + group_id: { type: 'string', description: 'Group/section ID' }, + item_id: { type: 'string', description: 'Item ID' }, + subitem_id: { type: 'string', description: 'Sub-item ID' }, + item_name: { type: 'string', description: 'Item name' }, + column_values: { type: 'json', description: 'Column values as JSON' }, + limit: { type: 'number', description: 'Maximum number of items to return' }, + }, + outputs: { + success: { type: 'boolean', description: 'Whether operation succeeded' }, + item: { type: 'json', description: 'Single item object' }, + items: { type: 'array', description: 'Array of items (for list operation)' }, + item_id: { type: 'string', description: 'Item ID' }, + error: { type: 'string', description: 'Error message if failed' }, + }, +} diff --git a/apps/sim/blocks/registry.ts b/apps/sim/blocks/registry.ts index 737f09bacf..a291e09224 100644 --- a/apps/sim/blocks/registry.ts +++ b/apps/sim/blocks/registry.ts @@ -60,6 +60,7 @@ import { KnowledgeBlock } from '@/blocks/blocks/knowledge' import { LinearBlock } from '@/blocks/blocks/linear' import { LinkedInBlock } from '@/blocks/blocks/linkedin' import { LinkupBlock } from '@/blocks/blocks/linkup' +import { MondayBlock } from '@/blocks/blocks/monday' import { MailchimpBlock } from '@/blocks/blocks/mailchimp' import { MailgunBlock } from '@/blocks/blocks/mailgun' import { ManualTriggerBlock } from '@/blocks/blocks/manual_trigger' @@ -215,6 +216,7 @@ export const registry: Record = { microsoft_planner: MicrosoftPlannerBlock, microsoft_teams: MicrosoftTeamsBlock, mistral_parse: MistralParseBlock, + monday: MondayBlock, mongodb: MongoDBBlock, mysql: MySQLBlock, neo4j: Neo4jBlock, diff --git a/apps/sim/blocks/types.ts b/apps/sim/blocks/types.ts index 572593f677..0c761371f5 100644 --- a/apps/sim/blocks/types.ts +++ b/apps/sim/blocks/types.ts @@ -209,6 +209,7 @@ export interface SubBlockConfig { max?: number columns?: string[] placeholder?: string + note?: string password?: boolean readOnly?: boolean showCopyButton?: boolean diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx index f84923f2b7..17a197897e 100644 --- a/apps/sim/components/icons.tsx +++ b/apps/sim/components/icons.tsx @@ -253,6 +253,109 @@ export function MessagesIcon(props: SVGProps) { ) } +export function MondayIcon(props: SVGProps) { + return ( + + ) +} + export function NotificationsIcon(props: SVGProps) { return ( = { })) }, }, + 'monday.boards': { + key: 'monday.boards', + staleTime: SELECTOR_STALE, + getQueryKey: ({ context }: SelectorQueryArgs) => [ + 'selectors', + 'monday.boards', + context.apiKey ?? 'none', + ], + enabled: ({ context }) => Boolean(context.apiKey), + fetchList: async ({ context }: SelectorQueryArgs) => { + const body = JSON.stringify({ apiKey: context.apiKey }) + const data = await fetchJson<{ items: { id: string; name: string }[] }>( + '/api/tools/monday/boards', + { method: 'POST', body } + ) + return (data.items || []).map((board) => ({ + id: board.id, + label: board.name, + })) + }, + }, + 'monday.columns': { + key: 'monday.columns', + staleTime: SELECTOR_STALE, + getQueryKey: ({ context }: SelectorQueryArgs) => [ + 'selectors', + 'monday.columns', + context.apiKey ?? 'none', + context.boardId ?? 'none', + ], + enabled: ({ context }) => Boolean(context.apiKey && context.boardId), + fetchList: async ({ context }: SelectorQueryArgs) => { + const body = JSON.stringify({ + apiKey: context.apiKey, + boardId: context.boardId, + }) + const data = await fetchJson<{ items: { id: string; name: string; type: string }[] }>( + '/api/tools/monday/columns', + { method: 'POST', body } + ) + return (data.items || []).map((col) => ({ + id: col.id, + label: `${col.name} (${col.type})`, + })) + }, + }, + 'monday.groups': { + key: 'monday.groups', + staleTime: SELECTOR_STALE, + getQueryKey: ({ context }: SelectorQueryArgs) => [ + 'selectors', + 'monday.groups', + context.apiKey ?? 'none', + context.boardId ?? 'none', + ], + enabled: ({ context }) => Boolean(context.apiKey && context.boardId), + fetchList: async ({ context }: SelectorQueryArgs) => { + const body = JSON.stringify({ + apiKey: context.apiKey, + boardId: context.boardId, + }) + const data = await fetchJson<{ items: { id: string; name: string }[] }>( + '/api/tools/monday/groups', + { method: 'POST', body } + ) + return (data.items || []).map((group) => ({ + id: group.id, + label: group.name, + })) + }, + }, + 'monday.status-options': { + key: 'monday.status-options', + staleTime: SELECTOR_STALE, + getQueryKey: ({ context }: SelectorQueryArgs) => [ + 'selectors', + 'monday.status-options', + context.apiKey ?? 'none', + context.boardId ?? 'none', + context.columnId ?? 'none', + ], + enabled: ({ context }) => Boolean(context.apiKey && context.boardId && context.columnId), + fetchList: async ({ context }: SelectorQueryArgs) => { + const body = JSON.stringify({ + apiKey: context.apiKey, + boardId: context.boardId, + columnId: context.columnId, + }) + const data = await fetchJson<{ items: { id: string; name: string }[] }>( + '/api/tools/monday/status-options', + { method: 'POST', body } + ) + return (data.items || []).map((option) => ({ + id: option.id, + label: option.name, + })) + }, + }, + 'monday.items': { + key: 'monday.items', + staleTime: SELECTOR_STALE, + getQueryKey: ({ context }: SelectorQueryArgs) => [ + 'selectors', + 'monday.items', + context.apiKey ?? 'none', + context.boardId ?? 'none', + ], + enabled: ({ context }) => Boolean(context.apiKey && context.boardId), + fetchList: async ({ context }: SelectorQueryArgs) => { + const body = JSON.stringify({ + apiKey: context.apiKey, + boardId: context.boardId, + }) + const data = await fetchJson<{ items: { id: string; name: string }[] }>( + '/api/tools/monday/items', + { method: 'POST', body } + ) + return (data.items || []).map((item) => ({ + id: item.id, + label: item.name, + })) + }, + }, + 'monday.subitems': { + key: 'monday.subitems', + staleTime: SELECTOR_STALE, + getQueryKey: ({ context }: SelectorQueryArgs) => [ + 'selectors', + 'monday.subitems', + context.apiKey ?? 'none', + context.itemId ?? 'none', + ], + enabled: ({ context }) => Boolean(context.apiKey && context.itemId), + fetchList: async ({ context }: SelectorQueryArgs) => { + const body = JSON.stringify({ + apiKey: context.apiKey, + itemId: context.itemId, + }) + const data = await fetchJson<{ items: { id: string; name: string }[] }>( + '/api/tools/monday/subitems', + { method: 'POST', body } + ) + return (data.items || []).map((subitem) => ({ + id: subitem.id, + label: subitem.name, + })) + }, + }, } export function getSelectorDefinition(key: SelectorKey): SelectorDefinition { diff --git a/apps/sim/hooks/selectors/resolution.ts b/apps/sim/hooks/selectors/resolution.ts index 78af03f935..2569bdeecb 100644 --- a/apps/sim/hooks/selectors/resolution.ts +++ b/apps/sim/hooks/selectors/resolution.ts @@ -17,6 +17,10 @@ export interface SelectorResolutionArgs { knowledgeBaseId?: string siteId?: string collectionId?: string + apiKey?: string + boardId?: string + columnId?: string + itemId?: string } const defaultContext: SelectorContext = {} @@ -58,6 +62,10 @@ function buildBaseContext( knowledgeBaseId: args.knowledgeBaseId, siteId: args.siteId, collectionId: args.collectionId, + apiKey: args.apiKey, + boardId: args.boardId, + columnId: args.columnId, + itemId: args.itemId, ...extra, } } @@ -120,6 +128,36 @@ function resolveFileSelector( return { key: 'webflow.items', context, allowSearch: true } } return { key: null, context, allowSearch: true } + case 'monday': + if ( + subBlock.id === 'board_id' || + subBlock.id === 'boardId' || + subBlock.id === 'board_id_list' || + subBlock.id === 'board_id_update' || + subBlock.id === 'board_id_get' + ) { + return { key: 'monday.boards', context, allowSearch: true } + } + if (subBlock.id === 'column_id' || subBlock.id === 'columnId') { + return { key: 'monday.columns', context, allowSearch: true } + } + if ( + subBlock.id === 'group_id' || + subBlock.id === 'groupId' || + subBlock.id === 'group_id_list' + ) { + return { key: 'monday.groups', context, allowSearch: true } + } + if (subBlock.id === 'status_column' || subBlock.id === 'statusColumn') { + return { key: 'monday.status-options', context, allowSearch: true } + } + if (subBlock.id === 'item_id' || subBlock.id === 'itemId' || subBlock.id === 'item_id_get') { + return { key: 'monday.items', context, allowSearch: true } + } + if (subBlock.id === 'subitem_id' || subBlock.id === 'subitemId') { + return { key: 'monday.subitems', context, allowSearch: true } + } + return { key: null, context, allowSearch: true } default: return { key: null, context, allowSearch: true } } diff --git a/apps/sim/hooks/selectors/types.ts b/apps/sim/hooks/selectors/types.ts index e9da5996a2..e8408649aa 100644 --- a/apps/sim/hooks/selectors/types.ts +++ b/apps/sim/hooks/selectors/types.ts @@ -27,6 +27,13 @@ export type SelectorKey = | 'webflow.sites' | 'webflow.collections' | 'webflow.items' + | 'monday.boards' + | 'monday.columns' + | 'monday.groups' + | 'monday.status-options' + | 'monday.items' + | 'monday.subitems' + export interface SelectorOption { id: string @@ -49,6 +56,11 @@ export interface SelectorContext { fileId?: string siteId?: string collectionId?: string + apiKey?: string + boardId?: string + groupId?: string + columnId?: string + itemId?: string } export interface SelectorQueryArgs { diff --git a/apps/sim/lib/webhooks/processor.ts b/apps/sim/lib/webhooks/processor.ts index b197a8ef18..8a383a1c37 100644 --- a/apps/sim/lib/webhooks/processor.ts +++ b/apps/sim/lib/webhooks/processor.ts @@ -8,6 +8,7 @@ import { isTriggerDevEnabled } from '@/lib/core/config/feature-flags' import { preprocessExecution } from '@/lib/execution/preprocessing' import { convertSquareBracketsToTwiML } from '@/lib/webhooks/utils' import { + handleMondayChallenge, handleSlackChallenge, handleWhatsAppVerification, validateMicrosoftTeamsSignature, @@ -108,6 +109,11 @@ export async function handleProviderChallenges( return slackResponse } + const mondayResponse = handleMondayChallenge(body) + if (mondayResponse) { + return mondayResponse + } + const url = new URL(request.url) const mode = url.searchParams.get('hub.mode') const token = url.searchParams.get('hub.verify_token') diff --git a/apps/sim/lib/webhooks/provider-utils.ts b/apps/sim/lib/webhooks/provider-utils.ts index 2ae0681e94..742cd47e64 100644 --- a/apps/sim/lib/webhooks/provider-utils.ts +++ b/apps/sim/lib/webhooks/provider-utils.ts @@ -63,6 +63,18 @@ function extractAirtableIdentifier(body: any): string | null { return null } +function extractMondayIdentifier(body: any): string | null { + // Monday.com sends a triggerUuid for each webhook event + if (body.event?.triggerUuid) { + return body.event.triggerUuid + } + // Fallback to pulseId + triggerTime combination + if (body.event?.pulseId && body.event?.triggerTime) { + return `${body.event.pulseId}:${body.event.triggerTime}` + } + return null +} + const PROVIDER_EXTRACTORS: Record string | null> = { slack: extractSlackIdentifier, twilio: extractTwilioIdentifier, @@ -73,6 +85,7 @@ const PROVIDER_EXTRACTORS: Record string | null> = { jira: extractJiraIdentifier, 'microsoft-teams': extractMicrosoftTeamsIdentifier, airtable: extractAirtableIdentifier, + monday: extractMondayIdentifier, } export function extractProviderIdentifierFromBody(provider: string, body: any): string | null { diff --git a/apps/sim/lib/webhooks/utils.server.ts b/apps/sim/lib/webhooks/utils.server.ts index 49e69649df..a0131b52e9 100644 --- a/apps/sim/lib/webhooks/utils.server.ts +++ b/apps/sim/lib/webhooks/utils.server.ts @@ -69,6 +69,18 @@ export function handleSlackChallenge(body: any): NextResponse | null { return null } +/** + * Handles Monday.com webhook challenge verification + * Monday.com sends a challenge field that must be echoed back + */ +export function handleMondayChallenge(body: any): NextResponse | null { + if (body?.challenge) { + return NextResponse.json({ challenge: body.challenge }) + } + + return null +} + /** * Fetches a URL with DNS pinning to prevent DNS rebinding attacks * @param url - The URL to fetch diff --git a/apps/sim/lib/workflows/executor/execution-core.ts b/apps/sim/lib/workflows/executor/execution-core.ts index 403d690d76..312d880321 100644 --- a/apps/sim/lib/workflows/executor/execution-core.ts +++ b/apps/sim/lib/workflows/executor/execution-core.ts @@ -28,6 +28,11 @@ const logger = createLogger('ExecutionCore') const EnvVarsSchema = z.record(z.string()) +interface SubBlockState { + value: unknown + [key: string]: unknown +} + export interface ExecuteWorkflowCoreOptions { snapshot: ExecutionSnapshot callbacks: ExecutionCallbacks @@ -148,6 +153,20 @@ export async function executeWorkflowCore( loops = draftData.loops parallels = draftData.parallels + // Debug: Log Monday blocks loaded from database + Object.entries(blocks).forEach(([blockId, block]) => { + if (block.type === 'monday') { + logger.info(`[${requestId}] Monday block loaded from database`, { + blockId, + subBlockKeys: Object.keys(block.subBlocks || {}), + subBlockValues: Object.entries(block.subBlocks || {}).reduce((acc, [key, sb]) => { + acc[key] = (sb as SubBlockState).value + return acc + }, {} as Record), + }) + } + }) + logger.info( `[${requestId}] Using draft workflow state from normalized tables (client execution)` ) diff --git a/apps/sim/serializer/index.ts b/apps/sim/serializer/index.ts index bf996579fe..6296959f53 100644 --- a/apps/sim/serializer/index.ts +++ b/apps/sim/serializer/index.ts @@ -449,19 +449,26 @@ export class Serializer { }) // Finally, consolidate canonical parameters (e.g., selector and manual ID into a single param) - const canonicalGroups: Record = {} + const canonicalGroups: Record = {} blockConfig.subBlocks.forEach((sb) => { if (!sb.canonicalParamId) return const key = sb.canonicalParamId - if (!canonicalGroups[key]) canonicalGroups[key] = { basic: undefined, advanced: [] } + if (!canonicalGroups[key]) canonicalGroups[key] = { basic: [], advanced: [] } if (sb.mode === 'advanced') canonicalGroups[key].advanced.push(sb.id) - else canonicalGroups[key].basic = sb.id + else canonicalGroups[key].basic.push(sb.id) }) Object.entries(canonicalGroups).forEach(([canonicalKey, group]) => { - const basicId = group.basic + const basicIds = group.basic const advancedIds = group.advanced - const basicVal = basicId ? params[basicId] : undefined + + // Check all basic field IDs for a value (not just the last one) + const basicVal = basicIds + .map((id) => params[id]) + .find( + (v) => v !== undefined && v !== null && (typeof v !== 'string' || v.trim().length > 0) + ) + const advancedVal = advancedIds .map((id) => params[id]) .find( @@ -479,12 +486,26 @@ export class Serializer { chosen = undefined } - const sourceIds = [basicId, ...advancedIds].filter(Boolean) as string[] + // Preserve existing canonical key value if consolidation produces no better value + const existingCanonicalValue = params[canonicalKey] + const hasValidExistingValue = + existingCanonicalValue !== undefined && + existingCanonicalValue !== null && + (typeof existingCanonicalValue !== 'string' || existingCanonicalValue.trim().length > 0) + + const sourceIds = [...basicIds, ...advancedIds].filter(Boolean) as string[] sourceIds.forEach((id) => { if (id !== canonicalKey) delete params[id] }) - if (chosen !== undefined) params[canonicalKey] = chosen - else delete params[canonicalKey] + + if (chosen !== undefined) { + params[canonicalKey] = chosen + } else if (hasValidExistingValue) { + // Keep existing value if consolidation produces nothing + params[canonicalKey] = existingCanonicalValue + } else { + delete params[canonicalKey] + } }) return params diff --git a/apps/sim/tools/monday/create_item.ts b/apps/sim/tools/monday/create_item.ts new file mode 100644 index 0000000000..601cd03bd6 --- /dev/null +++ b/apps/sim/tools/monday/create_item.ts @@ -0,0 +1,114 @@ +import type { CreateItemParams, CreateItemResponse } from '@/tools/monday/types' +import type { ToolConfig } from '@/tools/types' +import { createLogger } from '@sim/logger' +import { QUERIES } from './graphql' + +const logger = createLogger('MondayCreateItem') + +export const createItemTool: ToolConfig = { + id: 'monday_create_item', + name: 'Create Monday.com Item', + description: 'Create a new item in a Monday.com board', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Monday.com API key', + }, + board_id: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the board to create the item in', + }, + group_id: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'The group (section) ID within the board (optional)', + }, + item_name: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The name of the item to create', + }, + column_values: { + type: 'json', + required: false, + visibility: 'user-or-llm', + description: 'Column values as JSON object (optional)', + }, + }, + + request: { + url: 'https://api.monday.com/v2', + method: 'POST', + headers: (params) => ({ + 'Content-Type': 'application/json', + Authorization: params.apiKey, + 'API-Version': '2024-01', + }), + body: (params) => ({ + query: QUERIES.CREATE_ITEM, + variables: { + boardId: parseInt(params.board_id, 10), + groupId: params.group_id, + itemName: params.item_name, + columnValues: params.column_values ? JSON.stringify(params.column_values) : undefined, + }, + }), + }, + + transformResponse: async (response: Response): Promise => { + if (!response.ok) { + const errorText = await response.text() + logger.error('Monday create item failed', { + status: response.status, + error: errorText, + }) + return { + success: false, + output: {}, + error: `Monday.com API error: ${response.status} - ${errorText}`, + } + } + + const result = await response.json() + + if (result.errors) { + logger.error('Monday GraphQL errors', { errors: result.errors }) + return { + success: false, + output: {}, + error: `GraphQL errors: ${JSON.stringify(result.errors)}`, + } + } + + const item = result.data?.create_item + + if (!item) { + return { + success: false, + output: {}, + error: 'No item returned from Monday.com', + } + } + + logger.info('Monday item created successfully', { + itemId: item.id, + boardId: item.board?.id, + }) + + return { + success: true, + output: { + item, + item_id: item.id, + }, + } + }, +} diff --git a/apps/sim/tools/monday/get_item.ts b/apps/sim/tools/monday/get_item.ts new file mode 100644 index 0000000000..a356de5438 --- /dev/null +++ b/apps/sim/tools/monday/get_item.ts @@ -0,0 +1,87 @@ +import type { GetItemParams, GetItemResponse } from '@/tools/monday/types' +import type { ToolConfig } from '@/tools/types' +import { createLogger } from '@sim/logger' +import { QUERIES } from './graphql' + +const logger = createLogger('MondayGetItem') + +export const getItemTool: ToolConfig = { + id: 'monday_get_item', + name: 'Get Monday.com Item', + description: 'Retrieve a Monday.com item by ID', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Monday.com API key', + }, + item_id: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the item to retrieve', + }, + }, + + request: { + url: 'https://api.monday.com/v2', + method: 'POST', + headers: (params) => ({ + 'Content-Type': 'application/json', + Authorization: params.apiKey, + 'API-Version': '2024-01', + }), + body: (params) => ({ + query: QUERIES.GET_ITEM, + variables: { + itemId: [parseInt(params.item_id, 10)], + }, + }), + }, + + transformResponse: async (response: Response): Promise => { + if (!response.ok) { + const errorText = await response.text() + logger.error('Monday get item failed', { + status: response.status, + error: errorText, + }) + return { + success: false, + output: {}, + error: `Monday.com API error: ${response.status} - ${errorText}`, + } + } + + const result = await response.json() + + if (result.errors) { + logger.error('Monday GraphQL errors', { errors: result.errors }) + return { + success: false, + output: {}, + error: `GraphQL errors: ${JSON.stringify(result.errors)}`, + } + } + + const item = result.data?.items?.[0] + + if (!item) { + return { + success: false, + output: {}, + error: 'Item not found', + } + } + + logger.info('Monday item retrieved successfully', { itemId: item.id }) + + return { + success: true, + output: { item }, + } + }, +} diff --git a/apps/sim/tools/monday/graphql.ts b/apps/sim/tools/monday/graphql.ts new file mode 100644 index 0000000000..9de0e595eb --- /dev/null +++ b/apps/sim/tools/monday/graphql.ts @@ -0,0 +1,211 @@ +import { createLogger } from '@sim/logger' + +const logger = createLogger('MondayGraphQL') + +/** + * GraphQL request structure + */ +export interface GraphQLRequest { + query: string + variables?: Record +} + +/** + * Execute a GraphQL query against the Monday.com API + */ +export async function executeMondayQuery( + apiKey: string, + request: GraphQLRequest +): Promise { + const response = await fetch('https://api.monday.com/v2', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: apiKey, + 'API-Version': '2024-01', + }, + body: JSON.stringify(request), + }) + + if (!response.ok) { + const errorText = await response.text() + logger.error('Monday.com API error', { + status: response.status, + error: errorText, + }) + throw new Error(`Monday.com API error: ${response.status} - ${errorText}`) + } + + const data = await response.json() + + if (data.errors) { + logger.error('Monday.com GraphQL errors', { errors: data.errors }) + throw new Error(`GraphQL errors: ${JSON.stringify(data.errors)}`) + } + + return data.data as T +} + +/** + * Common GraphQL queries for Monday.com + */ +export const QUERIES = { + GET_BOARDS: ` + query { + boards { + id + name + description + board_kind + state + } + } + `, + + GET_BOARD_COLUMNS: ` + query ($boardId: [ID!]!) { + boards(ids: $boardId) { + columns { + id + title + type + settings_str + } + } + } + `, + + GET_BOARD_GROUPS: ` + query ($boardId: [ID!]!) { + boards(ids: $boardId) { + groups { + id + title + color + } + } + } + `, + + CREATE_ITEM: ` + mutation ($boardId: ID!, $groupId: String, $itemName: String!, $columnValues: JSON) { + create_item( + board_id: $boardId + group_id: $groupId + item_name: $itemName + column_values: $columnValues + ) { + id + name + created_at + board { id } + group { id } + column_values { + id + type + text + value + } + } + } + `, + + UPDATE_ITEM: ` + mutation ($boardId: ID!, $itemId: ID!, $columnValues: JSON!) { + change_multiple_column_values( + board_id: $boardId + item_id: $itemId + column_values: $columnValues + ) { + id + name + updated_at + column_values { + id + type + text + value + } + } + } + `, + + GET_ITEM: ` + query ($itemId: [ID!]!) { + items(ids: $itemId) { + id + name + created_at + updated_at + board { id } + group { id } + column_values { + id + type + text + value + } + } + } + `, + + LIST_ITEMS: ` + query ($boardId: [ID!]!, $limit: Int) { + boards(ids: $boardId) { + items_page(limit: $limit) { + items { + id + name + created_at + updated_at + board { id } + group { id } + column_values { + id + type + text + value + } + } + } + } + } + `, + + GET_COLUMN_SETTINGS: ` + query ($boardId: [ID!]!, $columnId: String!) { + boards(ids: $boardId) { + columns(ids: [$columnId]) { + id + title + type + settings_str + } + } + } + `, + + GET_BOARD_ITEMS: ` + query ($boardId: [ID!]!, $limit: Int) { + boards(ids: $boardId) { + items_page(limit: $limit) { + items { + id + name + } + } + } + } + `, + + GET_ITEM_SUBITEMS: ` + query ($itemId: [ID!]!) { + items(ids: $itemId) { + subitems { + id + name + } + } + } + `, +} diff --git a/apps/sim/tools/monday/index.ts b/apps/sim/tools/monday/index.ts new file mode 100644 index 0000000000..c005cb6e73 --- /dev/null +++ b/apps/sim/tools/monday/index.ts @@ -0,0 +1,4 @@ +export { createItemTool as mondayCreateItemTool } from './create_item' +export { updateItemTool as mondayUpdateItemTool } from './update_item' +export { getItemTool as mondayGetItemTool } from './get_item' +export { listItemsTool as mondayListItemsTool } from './list_items' diff --git a/apps/sim/tools/monday/list_items.ts b/apps/sim/tools/monday/list_items.ts new file mode 100644 index 0000000000..ba1b5ea9f8 --- /dev/null +++ b/apps/sim/tools/monday/list_items.ts @@ -0,0 +1,99 @@ +import type { ListItemsParams, ListItemsResponse, MondayItem } from '@/tools/monday/types' +import type { ToolConfig } from '@/tools/types' +import { createLogger } from '@sim/logger' +import { QUERIES } from './graphql' + +const logger = createLogger('MondayListItems') + +export const listItemsTool: ToolConfig = { + id: 'monday_list_items', + name: 'List Monday.com Items', + description: 'List items from a Monday.com board', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Monday.com API key', + }, + board_id: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the board to list items from', + }, + group_id: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter by group ID (optional)', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of items to return (default: 25)', + }, + }, + + request: { + url: 'https://api.monday.com/v2', + method: 'POST', + headers: (params) => ({ + 'Content-Type': 'application/json', + Authorization: params.apiKey, + 'API-Version': '2024-01', + }), + body: (params) => { + return { + query: QUERIES.LIST_ITEMS, + variables: { + boardId: [parseInt(params.board_id, 10)], + limit: params.limit || 25, + }, + } + }, + }, + + transformResponse: async (response: Response, params): Promise => { + if (!response.ok) { + const errorText = await response.text() + logger.error('Monday list items failed', { + status: response.status, + error: errorText, + }) + return { + success: false, + output: {}, + error: `Monday.com API error: ${response.status} - ${errorText}`, + } + } + + const result = await response.json() + + if (result.errors) { + logger.error('Monday GraphQL errors', { errors: result.errors }) + return { + success: false, + output: {}, + error: `GraphQL errors: ${JSON.stringify(result.errors)}`, + } + } + + let items = result.data?.boards?.[0]?.items_page?.items || [] + + // Filter by group if group_id is provided + if (params.group_id) { + items = items.filter((item: MondayItem) => item.group?.id === params.group_id) + } + + logger.info('Monday items listed successfully', { count: items.length, filtered: !!params.group_id }) + + return { + success: true, + output: { items }, + } + }, +} diff --git a/apps/sim/tools/monday/types.ts b/apps/sim/tools/monday/types.ts new file mode 100644 index 0000000000..d61379fa89 --- /dev/null +++ b/apps/sim/tools/monday/types.ts @@ -0,0 +1,146 @@ +import type { ToolResponse } from '@/tools/types' + +/** + * Monday.com Board + */ +export interface MondayBoard { + id: string + name: string + description?: string + board_kind: string + state: string +} + +/** + * Monday.com Column + */ +export interface MondayColumn { + id: string + title: string + type: string + settings_str?: string +} + +/** + * Monday.com Group + */ +export interface MondayGroup { + id: string + title: string + color: string +} + +/** + * Monday.com Column Value + */ +export interface MondayColumnValue { + id: string + title: string + type: string + text?: string + value?: string +} + +/** + * Monday.com Item + */ +export interface MondayItem { + id: string + name: string + board: { id: string } + group: { id: string } + column_values: MondayColumnValue[] + created_at: string + updated_at: string +} + +/** + * Parameters for creating a Monday.com item + */ +export interface CreateItemParams { + apiKey: string + board_id: string + group_id?: string + item_name: string + column_values?: Record +} + +/** + * Response from creating a Monday.com item + */ +export interface CreateItemResponse extends ToolResponse { + output: { + item?: MondayItem + item_id?: string + } +} + +/** + * Parameters for updating a Monday.com item + */ +export interface UpdateItemParams { + apiKey: string + item_id: string + subitem_id?: string + board_id: string + column_values: Record +} + +/** + * Response from updating a Monday.com item + */ +export interface UpdateItemResponse extends ToolResponse { + output: { + item?: MondayItem + item_id?: string + } +} + +/** + * Parameters for getting a Monday.com item + */ +export interface GetItemParams { + apiKey: string + item_id: string +} + +/** + * Response from getting a Monday.com item + */ +export interface GetItemResponse extends ToolResponse { + output: { + item?: MondayItem + } +} + +/** + * Parameters for listing Monday.com items + */ +export interface ListItemsParams { + apiKey: string + board_id: string + group_id?: string + limit?: number +} + +/** + * Response from listing Monday.com items + */ +export interface ListItemsResponse extends ToolResponse { + output: { + items?: MondayItem[] + } +} + +/** + * Generic Monday.com response type for blocks + */ +export type MondayResponse = { + success: boolean + output: { + item?: MondayItem + items?: MondayItem[] + item_id?: string + } + error?: string +} diff --git a/apps/sim/tools/monday/update_item.ts b/apps/sim/tools/monday/update_item.ts new file mode 100644 index 0000000000..e64da9afce --- /dev/null +++ b/apps/sim/tools/monday/update_item.ts @@ -0,0 +1,114 @@ +import type { UpdateItemParams, UpdateItemResponse } from '@/tools/monday/types' +import type { ToolConfig } from '@/tools/types' +import { createLogger } from '@sim/logger' +import { QUERIES } from './graphql' + +const logger = createLogger('MondayUpdateItem') + +export const updateItemTool: ToolConfig = { + id: 'monday_update_item', + name: 'Update Monday.com Item', + description: 'Update column values in an existing Monday.com item or sub-item', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Monday.com API key', + }, + item_id: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the item to update', + }, + subitem_id: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'The ID of the sub-item to update (if updating a sub-item)', + }, + board_id: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The board ID containing the item', + }, + column_values: { + type: 'json', + required: true, + visibility: 'user-or-llm', + description: 'Column values to update as JSON object', + }, + }, + + request: { + url: 'https://api.monday.com/v2', + method: 'POST', + headers: (params) => ({ + 'Content-Type': 'application/json', + Authorization: params.apiKey, + 'API-Version': '2024-01', + }), + body: (params) => { + // Use subitem_id if provided, otherwise use item_id + const targetItemId = params.subitem_id || params.item_id + return { + query: QUERIES.UPDATE_ITEM, + variables: { + boardId: parseInt(params.board_id, 10), + itemId: parseInt(targetItemId, 10), + columnValues: JSON.stringify(params.column_values), + }, + } + }, + }, + + transformResponse: async (response: Response): Promise => { + if (!response.ok) { + const errorText = await response.text() + logger.error('Monday update item failed', { + status: response.status, + error: errorText, + }) + return { + success: false, + output: {}, + error: `Monday.com API error: ${response.status} - ${errorText}`, + } + } + + const result = await response.json() + + if (result.errors) { + logger.error('Monday GraphQL errors', { errors: result.errors }) + return { + success: false, + output: {}, + error: `GraphQL errors: ${JSON.stringify(result.errors)}`, + } + } + + const item = result.data?.change_multiple_column_values + + if (!item) { + return { + success: false, + output: {}, + error: 'No item returned from Monday.com', + } + } + + logger.info('Monday item updated successfully', { itemId: item.id }) + + return { + success: true, + output: { + item, + item_id: item.id, + }, + } + }, +} diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 4610808eb3..61f47421a1 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -599,6 +599,12 @@ import { import { linkedInGetProfileTool, linkedInSharePostTool } from '@/tools/linkedin' import { linkupSearchTool } from '@/tools/linkup' import { llmChatTool } from '@/tools/llm' +import { + mondayCreateItemTool, + mondayUpdateItemTool, + mondayGetItemTool, + mondayListItemsTool, +} from '@/tools/monday' import { mailchimpAddMemberTagsTool, mailchimpAddMemberTool, @@ -1430,6 +1436,10 @@ export const tools: Record = { linkup_search: linkupSearchTool, linkedin_share_post: linkedInSharePostTool, linkedin_get_profile: linkedInGetProfileTool, + monday_create_item: mondayCreateItemTool, + monday_update_item: mondayUpdateItemTool, + monday_get_item: mondayGetItemTool, + monday_list_items: mondayListItemsTool, resend_send: mailSendTool, sendgrid_send_mail: sendGridSendMailTool, sendgrid_add_contact: sendGridAddContactTool, diff --git a/apps/sim/triggers/monday/column_changed.ts b/apps/sim/triggers/monday/column_changed.ts new file mode 100644 index 0000000000..fde08cc971 --- /dev/null +++ b/apps/sim/triggers/monday/column_changed.ts @@ -0,0 +1,170 @@ +import { MondayIcon } from '@/components/icons' +import { createLogger } from '@sim/logger' +import type { TriggerConfig } from '@/triggers/types' +import { mondayTriggerOptions } from './utils' + +const logger = createLogger('MondayColumnChangedTrigger') + +export const mondayColumnChangedTrigger: TriggerConfig = { + id: 'monday_column_changed', + name: 'Monday.com Column Changed', + provider: 'monday', + description: 'Triggers when a specific column value changes in a Monday.com board', + version: '1.0.0', + icon: MondayIcon, + + subBlocks: [ + { + id: 'selectedTriggerId', + title: 'Trigger Type', + type: 'dropdown', + mode: 'trigger', + options: mondayTriggerOptions, + value: () => 'monday_column_changed', + required: true, + }, + { + id: 'apiKey', + title: 'API Key', + type: 'short-input', + description: 'Your Monday.com API key (get it from Admin > API section)', + placeholder: 'Enter API key', + password: true, + required: true, + mode: 'trigger', + condition: { + field: 'selectedTriggerId', + value: 'monday_column_changed', + }, + }, + { + id: 'boardId', + title: 'Board to Monitor', + type: 'file-selector', + serviceId: 'monday', + placeholder: 'Select a Monday.com board', + description: 'The board to monitor for column changes', + required: true, + dependsOn: ['apiKey'], + mode: 'trigger', + condition: { + field: 'selectedTriggerId', + value: 'monday_column_changed', + }, + }, + { + id: 'columnId', + title: 'Column to Monitor', + type: 'file-selector', + serviceId: 'monday', + placeholder: 'Select a column', + description: 'The specific column to monitor for changes', + required: true, + dependsOn: ['apiKey', 'boardId'], + mode: 'trigger', + condition: { + field: 'selectedTriggerId', + value: 'monday_column_changed', + }, + }, + { + id: 'specificValue', + title: 'Specific Value (Optional)', + type: 'short-input', + placeholder: 'e.g., "Done" or "Working on it"', + description: 'Only trigger when column changes to this specific value (optional)', + required: false, + mode: 'trigger', + condition: { + field: 'selectedTriggerId', + value: 'monday_column_changed', + }, + }, + { + id: 'webhookUrlDisplay', + title: 'Webhook URL', + type: 'short-input', + readOnly: true, + showCopyButton: true, + useWebhookUrl: true, + placeholder: 'Webhook URL will be generated', + mode: 'trigger', + condition: { + field: 'selectedTriggerId', + value: 'monday_column_changed', + }, + }, + { + id: 'triggerInstructions', + title: 'Setup Instructions', + hideFromPreview: true, + type: 'text', + defaultValue: [ + 'Get your Monday.com API key from Clicking your profile picture > Developers > API Access Tokens.', + 'Copy the Webhook URL above', + 'Select the board and click on automate on the right', + "search for webhook and paste the copied webhook url", + 'The webhook will send real-time notifications when column values change', + ] + .map((instruction, index) => `${index + 1}. ${instruction}`) + .join('\n'), + mode: 'trigger', + condition: { + field: 'selectedTriggerId', + value: 'monday_column_changed', + }, + }, + { + id: 'triggerSave', + title: '', + type: 'trigger-save', + hideFromPreview: true, + mode: 'trigger', + triggerId: 'monday_column_changed', + condition: { + field: 'selectedTriggerId', + value: 'monday_column_changed', + }, + }, + ], + + outputs: { + item: { + id: { type: 'string', description: 'Item ID' }, + name: { type: 'string', description: 'Item name' }, + board_id: { type: 'string', description: 'Board ID' }, + group_id: { type: 'string', description: 'Group ID' }, + column_values: { type: 'json', description: 'All column values' }, + created_at: { type: 'string', description: 'Creation timestamp' }, + updated_at: { type: 'string', description: 'Last update timestamp' }, + }, + old_value: { + type: 'string', + description: 'Previous column value', + }, + new_value: { + type: 'string', + description: 'New column value', + }, + column_id: { + type: 'string', + description: 'ID of the changed column', + }, + column_title: { + type: 'string', + description: 'Title of the changed column', + }, + timestamp: { + type: 'string', + description: 'Trigger timestamp', + }, + }, + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'User-Agent': 'monday.com', + }, + }, +} diff --git a/apps/sim/triggers/monday/index.ts b/apps/sim/triggers/monday/index.ts new file mode 100644 index 0000000000..89c1571a9b --- /dev/null +++ b/apps/sim/triggers/monday/index.ts @@ -0,0 +1,2 @@ +export { mondayNewItemTrigger } from './new_item' +export { mondayColumnChangedTrigger } from './column_changed' diff --git a/apps/sim/triggers/monday/new_item.ts b/apps/sim/triggers/monday/new_item.ts new file mode 100644 index 0000000000..29eb855ca2 --- /dev/null +++ b/apps/sim/triggers/monday/new_item.ts @@ -0,0 +1,138 @@ +import { MondayIcon } from '@/components/icons' +import type { TriggerConfig } from '@/triggers/types' +import { mondayTriggerOptions } from './utils' + +export const mondayNewItemTrigger: TriggerConfig = { + id: 'monday_new_item', + name: 'Monday.com New Item', + provider: 'monday', + description: 'Triggers when a new item is added to a Monday.com board', + version: '1.0.0', + icon: MondayIcon, + + subBlocks: [ + { + id: 'selectedTriggerId', + title: 'Trigger Type', + type: 'dropdown', + mode: 'trigger', + options: mondayTriggerOptions, + value: () => 'monday_new_item', + required: true, + }, + { + id: 'apiKey', + title: 'API Key', + type: 'short-input', + description: 'Your Monday.com API key (get it from Admin > API section)', + placeholder: 'Enter API key', + password: true, + required: true, + mode: 'trigger', + condition: { + field: 'selectedTriggerId', + value: 'monday_new_item', + }, + }, + { + id: 'boardId', + title: 'Board to Monitor', + type: 'file-selector', + serviceId: 'monday', + placeholder: 'Select a Monday.com board', + description: 'The board to monitor for new items', + required: true, + dependsOn: ['apiKey'], + mode: 'trigger', + condition: { + field: 'selectedTriggerId', + value: 'monday_new_item', + }, + }, + { + id: 'groupId', + title: 'Group Filter (Optional)', + type: 'file-selector', + serviceId: 'monday', + placeholder: 'All groups', + description: 'Filter by specific group (optional)', + required: false, + dependsOn: ['apiKey', 'boardId'], + mode: 'trigger', + condition: { + field: 'selectedTriggerId', + value: 'monday_new_item', + }, + }, + { + id: 'webhookUrlDisplay', + title: 'Webhook URL', + type: 'short-input', + readOnly: true, + showCopyButton: true, + useWebhookUrl: true, + placeholder: 'Webhook URL will be generated', + mode: 'trigger', + condition: { + field: 'selectedTriggerId', + value: 'monday_new_item', + }, + }, + { + id: 'triggerInstructions', + title: 'Setup Instructions', + hideFromPreview: true, + type: 'text', + defaultValue: [ + 'Get your Monday.com API key from Clicking your profile picture > Developers > API Access Tokens.', + 'Copy the Webhook URL above', + 'Select the board and click on automate on the right', + "search for webhook and paste the copied webhook url", + 'The webhook will send real-time notifications when new item is added', + ] + .map((instruction, index) => `${index + 1}. ${instruction}`) + .join('\n'), + mode: 'trigger', + condition: { + field: 'selectedTriggerId', + value: 'monday_new_item', + }, + }, + { + id: 'triggerSave', + title: '', + type: 'trigger-save', + hideFromPreview: true, + mode: 'trigger', + triggerId: 'monday_new_item', + condition: { + field: 'selectedTriggerId', + value: 'monday_new_item', + }, + }, + ], + + outputs: { + item: { + id: { type: 'string', description: 'Item ID' }, + name: { type: 'string', description: 'Item name' }, + board_id: { type: 'string', description: 'Board ID' }, + group_id: { type: 'string', description: 'Group ID' }, + column_values: { type: 'json', description: 'All column values' }, + created_at: { type: 'string', description: 'Creation timestamp' }, + updated_at: { type: 'string', description: 'Last update timestamp' }, + }, + timestamp: { + type: 'string', + description: 'Trigger timestamp', + }, + }, + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'User-Agent': 'monday.com', + }, + }, +} diff --git a/apps/sim/triggers/monday/utils.ts b/apps/sim/triggers/monday/utils.ts new file mode 100644 index 0000000000..4c2b1d4a1c --- /dev/null +++ b/apps/sim/triggers/monday/utils.ts @@ -0,0 +1,8 @@ +/** + * Shared utilities for Monday.com triggers + */ + +export const mondayTriggerOptions = [ + { label: 'New Item', id: 'monday_new_item' }, + { label: 'Column Changed', id: 'monday_column_changed' }, +] diff --git a/apps/sim/triggers/registry.ts b/apps/sim/triggers/registry.ts index 942ff0e018..60c2b21939 100644 --- a/apps/sim/triggers/registry.ts +++ b/apps/sim/triggers/registry.ts @@ -84,6 +84,7 @@ import { microsoftTeamsChatSubscriptionTrigger, microsoftTeamsWebhookTrigger, } from '@/triggers/microsoftteams' +import { mondayNewItemTrigger, mondayColumnChangedTrigger } from '@/triggers/monday' import { outlookPollingTrigger } from '@/triggers/outlook' import { rssPollingTrigger } from '@/triggers/rss' import { slackWebhookTrigger } from '@/triggers/slack' @@ -153,6 +154,8 @@ export const TRIGGER_REGISTRY: TriggerRegistry = { linear_customer_request_updated: linearCustomerRequestUpdatedTrigger, microsoftteams_webhook: microsoftTeamsWebhookTrigger, microsoftteams_chat_subscription: microsoftTeamsChatSubscriptionTrigger, + monday_new_item: mondayNewItemTrigger, + monday_column_changed: mondayColumnChangedTrigger, outlook_poller: outlookPollingTrigger, rss_poller: rssPollingTrigger, stripe_webhook: stripeWebhookTrigger,