diff --git a/apps/sim/app/api/tools/onedrive/upload/route.ts b/apps/sim/app/api/tools/onedrive/upload/route.ts index f1276e3ad43..1f4a0f9f49a 100644 --- a/apps/sim/app/api/tools/onedrive/upload/route.ts +++ b/apps/sim/app/api/tools/onedrive/upload/route.ts @@ -6,6 +6,7 @@ import { createLogger } from '@/lib/logs/console/logger' import { processSingleFileToUserFile } from '@/lib/uploads/utils/file-utils' import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server' import { generateRequestId } from '@/lib/utils' +import { normalizeExcelValues } from '@/tools/onedrive/utils' export const dynamic = 'force-dynamic' @@ -13,6 +14,14 @@ const logger = createLogger('OneDriveUploadAPI') const MICROSOFT_GRAPH_BASE = 'https://graph.microsoft.com/v1.0' +const ExcelCellSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]) +const ExcelRowSchema = z.array(ExcelCellSchema) +const ExcelValuesSchema = z.union([ + z.string(), + z.array(ExcelRowSchema), + z.array(z.record(ExcelCellSchema)), +]) + const OneDriveUploadSchema = z.object({ accessToken: z.string().min(1, 'Access token is required'), fileName: z.string().min(1, 'File name is required'), @@ -20,7 +29,7 @@ const OneDriveUploadSchema = z.object({ folderId: z.string().optional().nullable(), mimeType: z.string().optional(), // Optional Excel write-after-create inputs - values: z.array(z.array(z.union([z.string(), z.number(), z.boolean(), z.null()]))).optional(), + values: ExcelValuesSchema.optional(), }) export async function POST(request: NextRequest) { @@ -46,6 +55,7 @@ export async function POST(request: NextRequest) { const body = await request.json() const validatedData = OneDriveUploadSchema.parse(body) + const excelValues = normalizeExcelValues(validatedData.values) let fileBuffer: Buffer let mimeType: string @@ -180,7 +190,7 @@ export async function POST(request: NextRequest) { // If this is an Excel creation and values were provided, write them using the Excel API let excelWriteResult: any | undefined const shouldWriteExcelContent = - isExcelCreation && Array.isArray(validatedData.values) && validatedData.values.length > 0 + isExcelCreation && Array.isArray(excelValues) && excelValues.length > 0 if (shouldWriteExcelContent) { try { @@ -232,7 +242,7 @@ export async function POST(request: NextRequest) { logger.warn(`[${requestId}] Error listing worksheets, using default Sheet1`, listError) } - let processedValues: any = validatedData.values || [] + let processedValues: any = excelValues || [] if ( Array.isArray(processedValues) && diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/code/code.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/code/code.tsx index 02092f9eb8a..8fec01358a4 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/code/code.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/code/code.tsx @@ -221,17 +221,26 @@ export function Code({ // Derived state const effectiveLanguage = (languageValue as 'javascript' | 'python' | 'json') || language + const trimmedCode = code.trim() + const containsReferencePlaceholders = + trimmedCode.includes('{{') || + trimmedCode.includes('}}') || + trimmedCode.includes('<') || + trimmedCode.includes('>') + + const shouldValidateJson = effectiveLanguage === 'json' && !containsReferencePlaceholders + const isValidJson = useMemo(() => { - if (subBlockId !== 'responseFormat' || !code.trim()) { + if (!shouldValidateJson || !trimmedCode) { return true } try { - JSON.parse(code) + JSON.parse(trimmedCode) return true } catch { return false } - }, [subBlockId, code]) + }, [shouldValidateJson, trimmedCode]) const gutterWidthPx = useMemo(() => { const lineCount = code.split('\n').length @@ -309,14 +318,29 @@ export function Code({ : storeValue // Effects: JSON validation + const lastValidationStatus = useRef(true) + useEffect(() => { - if (onValidationChange && subBlockId === 'responseFormat') { - const timeoutId = setTimeout(() => { - onValidationChange(isValidJson) - }, 150) - return () => clearTimeout(timeoutId) + if (!onValidationChange) return + + const nextStatus = shouldValidateJson ? isValidJson : true + if (lastValidationStatus.current === nextStatus) { + return } - }, [isValidJson, onValidationChange, subBlockId]) + + lastValidationStatus.current = nextStatus + + if (!shouldValidateJson) { + onValidationChange(nextStatus) + return + } + + const timeoutId = setTimeout(() => { + onValidationChange(nextStatus) + }, 150) + + return () => clearTimeout(timeoutId) + }, [isValidJson, onValidationChange, shouldValidateJson]) // Effects: AI stream handlers setup useEffect(() => { diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/sub-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/sub-block.tsx index 8328eb7be9d..03c03b9335e 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/sub-block.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/sub-block.tsx @@ -190,7 +190,7 @@ const renderLabel = (
{config.title} {required && *} - {config.id === 'responseFormat' && ( + {config.type === 'code' && config.language === 'json' && ( = { { id: 'values', title: 'Values', - type: 'long-input', - placeholder: - 'Enter values as JSON array of arrays (e.g., [["A1","B1"],["A2","B2"]]) or an array of objects', + type: 'code', + language: 'json', + generationType: 'json-object', + placeholder: 'Enter a JSON array of rows (e.g., [["A1","B1"],["A2","B2"]])', condition: { field: 'operation', value: 'create_file', @@ -89,6 +91,13 @@ export const OneDriveBlock: BlockConfig = { value: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', }, }, + wandConfig: { + enabled: true, + prompt: + 'Generate a JSON array of arrays that can be written directly into an Excel worksheet.', + placeholder: 'Describe the table you want to generate...', + generationType: 'json-object', + }, required: false, }, // File upload (basic mode) @@ -351,17 +360,15 @@ export const OneDriveBlock: BlockConfig = { params: (params) => { const { credential, folderId, fileId, mimeType, values, downloadFileName, ...rest } = params - let parsedValues - try { - parsedValues = values ? JSON.parse(values as string) : undefined - } catch (error) { - throw new Error('Invalid JSON format for values') + let normalizedValues: ReturnType + if (values !== undefined) { + normalizedValues = normalizeExcelValuesForToolParams(values) } return { credential, ...rest, - values: parsedValues, + values: normalizedValues, folderId: folderId || undefined, fileId: fileId || undefined, pageSize: rest.pageSize ? Number.parseInt(rest.pageSize as string, 10) : undefined, @@ -380,7 +387,7 @@ export const OneDriveBlock: BlockConfig = { fileReference: { type: 'json', description: 'File reference from previous block' }, content: { type: 'string', description: 'Text content to upload' }, mimeType: { type: 'string', description: 'MIME type of file to create' }, - values: { type: 'string', description: 'Cell values for new Excel as JSON' }, + values: { type: 'json', description: 'Cell values for new Excel as JSON' }, fileId: { type: 'string', description: 'File ID to download' }, downloadFileName: { type: 'string', description: 'File name override for download' }, folderId: { type: 'string', description: 'Folder ID' }, diff --git a/apps/sim/blocks/types.ts b/apps/sim/blocks/types.ts index 568c32100a7..ac2c65bad61 100644 --- a/apps/sim/blocks/types.ts +++ b/apps/sim/blocks/types.ts @@ -210,7 +210,7 @@ export interface SubBlockConfig { } }) // Props specific to 'code' sub-block type - language?: 'javascript' | 'json' + language?: 'javascript' | 'json' | 'python' generationType?: GenerationType collapsible?: boolean // Whether the code block can be collapsed defaultCollapsed?: boolean // Whether the code block is collapsed by default diff --git a/apps/sim/tools/onedrive/types.ts b/apps/sim/tools/onedrive/types.ts index cd20b6e2f07..6ceb005c5e3 100644 --- a/apps/sim/tools/onedrive/types.ts +++ b/apps/sim/tools/onedrive/types.ts @@ -99,7 +99,9 @@ export interface OneDriveToolParams { pageToken?: string exportMimeType?: string // Optional Excel write parameters (used when creating an .xlsx without file content) - values?: (string | number | boolean | null)[][] + values?: + | (string | number | boolean | null)[][] + | Array> } export type OneDriveResponse = diff --git a/apps/sim/tools/onedrive/utils.ts b/apps/sim/tools/onedrive/utils.ts new file mode 100644 index 00000000000..e1b595f944b --- /dev/null +++ b/apps/sim/tools/onedrive/utils.ts @@ -0,0 +1,49 @@ +import type { OneDriveToolParams } from '@/tools/onedrive/types' + +export type ExcelCell = string | number | boolean | null +export type ExcelArrayValues = ExcelCell[][] +export type ExcelObjectValues = Array> +export type NormalizedExcelValues = ExcelArrayValues | ExcelObjectValues + +/** + * Ensures Excel values are always represented as arrays before hitting downstream tooling. + * Accepts JSON strings, array-of-arrays, or array-of-objects and normalizes them. + */ +export function normalizeExcelValues(values: unknown): NormalizedExcelValues | undefined { + if (values === null || values === undefined) { + return undefined + } + + if (typeof values === 'string') { + const trimmed = values.trim() + if (!trimmed) { + return undefined + } + + try { + const parsed = JSON.parse(trimmed) + if (!Array.isArray(parsed)) { + throw new Error('Excel values must be an array of rows or array of objects') + } + return parsed as NormalizedExcelValues + } catch (_error) { + throw new Error('Invalid JSON format for values') + } + } + + if (Array.isArray(values)) { + return values as NormalizedExcelValues + } + + throw new Error('Excel values must be an array of rows or array of objects') +} + +/** + * Convenience helper for contexts that expect the narrower ToolParams typing. + */ +export function normalizeExcelValuesForToolParams( + values: unknown +): OneDriveToolParams['values'] | undefined { + const normalized = normalizeExcelValues(values) + return normalized as OneDriveToolParams['values'] | undefined +}