From 70ae4eb855f5dac01a6e7029e649dd14e7dbfd1c Mon Sep 17 00:00:00 2001 From: Adam Gough Date: Tue, 29 Jul 2025 16:08:22 -0700 Subject: [PATCH 01/23] first push --- apps/sim/blocks/blocks/onedrive.ts | 0 apps/sim/components/icons.tsx | 17 +++++++++++++++++ apps/sim/tools/onedrive/create_folder.ts | 0 apps/sim/tools/onedrive/get_content.ts | 0 apps/sim/tools/onedrive/index.ts | 0 apps/sim/tools/onedrive/list.ts | 0 apps/sim/tools/onedrive/types.ts | 0 apps/sim/tools/onedrive/upload.ts | 0 8 files changed, 17 insertions(+) create mode 100644 apps/sim/blocks/blocks/onedrive.ts create mode 100644 apps/sim/tools/onedrive/create_folder.ts create mode 100644 apps/sim/tools/onedrive/get_content.ts create mode 100644 apps/sim/tools/onedrive/index.ts create mode 100644 apps/sim/tools/onedrive/list.ts create mode 100644 apps/sim/tools/onedrive/types.ts create mode 100644 apps/sim/tools/onedrive/upload.ts diff --git a/apps/sim/blocks/blocks/onedrive.ts b/apps/sim/blocks/blocks/onedrive.ts new file mode 100644 index 00000000000..e69de29bb2d diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx index 42259f1309f..c743b572263 100644 --- a/apps/sim/components/icons.tsx +++ b/apps/sim/components/icons.tsx @@ -3181,3 +3181,20 @@ export function HunterIOIcon(props: SVGProps) { ) } + +export function MicrosoftOneDriveIcon(props: SVGProps) { + return ( + + + + + + + + + ) +} diff --git a/apps/sim/tools/onedrive/create_folder.ts b/apps/sim/tools/onedrive/create_folder.ts new file mode 100644 index 00000000000..e69de29bb2d diff --git a/apps/sim/tools/onedrive/get_content.ts b/apps/sim/tools/onedrive/get_content.ts new file mode 100644 index 00000000000..e69de29bb2d diff --git a/apps/sim/tools/onedrive/index.ts b/apps/sim/tools/onedrive/index.ts new file mode 100644 index 00000000000..e69de29bb2d diff --git a/apps/sim/tools/onedrive/list.ts b/apps/sim/tools/onedrive/list.ts new file mode 100644 index 00000000000..e69de29bb2d diff --git a/apps/sim/tools/onedrive/types.ts b/apps/sim/tools/onedrive/types.ts new file mode 100644 index 00000000000..e69de29bb2d diff --git a/apps/sim/tools/onedrive/upload.ts b/apps/sim/tools/onedrive/upload.ts new file mode 100644 index 00000000000..e69de29bb2d From 28732ca79f2b28e78a4ea479ddaf65b07e1650a8 Mon Sep 17 00:00:00 2001 From: Adam Gough Date: Tue, 29 Jul 2025 19:28:30 -0700 Subject: [PATCH 02/23] feat: finished onedrive tool --- .../app/api/tools/onedrive/folder/route.ts | 83 ++++++ .../app/api/tools/onedrive/folders/route.ts | 87 +++++++ .../components/microsoft-file-selector.tsx | 59 ++++- .../file-selector/file-selector-input.tsx | 33 +++ apps/sim/blocks/blocks/onedrive.ts | 245 ++++++++++++++++++ apps/sim/blocks/registry.ts | 2 + apps/sim/components/icons.tsx | 2 +- apps/sim/lib/auth.ts | 16 ++ apps/sim/lib/oauth/oauth.ts | 18 +- apps/sim/tools/onedrive/create_folder.ts | 88 +++++++ apps/sim/tools/onedrive/index.ts | 7 + apps/sim/tools/onedrive/list.ts | 117 +++++++++ apps/sim/tools/onedrive/types.ts | 52 ++++ apps/sim/tools/onedrive/upload.ts | 125 +++++++++ apps/sim/tools/registry.ts | 4 + 15 files changed, 926 insertions(+), 12 deletions(-) create mode 100644 apps/sim/app/api/tools/onedrive/folder/route.ts create mode 100644 apps/sim/app/api/tools/onedrive/folders/route.ts diff --git a/apps/sim/app/api/tools/onedrive/folder/route.ts b/apps/sim/app/api/tools/onedrive/folder/route.ts new file mode 100644 index 00000000000..17f5f6856ac --- /dev/null +++ b/apps/sim/app/api/tools/onedrive/folder/route.ts @@ -0,0 +1,83 @@ +import { NextRequest, NextResponse } from 'next/server' +import crypto from 'node:crypto' +import { eq } from 'drizzle-orm' +import { getSession } from '@/lib/auth' +import { db } from '@/db' +import { account } from '@/db/schema' +import { createLogger } from '@/lib/logs/console/logger' +import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('OneDriveFolderAPI') + +/** + * Get a single folder from Microsoft OneDrive + */ +export async function GET(request: NextRequest) { + const requestId = crypto.randomUUID().slice(0, 8) + + try { + const session = await getSession() + if (!session?.user?.id) { + return NextResponse.json({ error: 'User not authenticated' }, { status: 401 }) + } + + const { searchParams } = new URL(request.url) + const credentialId = searchParams.get('credentialId') + const fileId = searchParams.get('fileId') + + if (!credentialId || !fileId) { + return NextResponse.json({ error: 'Credential ID and File ID are required' }, { status: 400 }) + } + + const credentials = await db.select().from(account).where(eq(account.id, credentialId)).limit(1) + if (!credentials.length) { + return NextResponse.json({ error: 'Credential not found' }, { status: 404 }) + } + + const credential = credentials[0] + if (credential.userId !== session.user.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 403 }) + } + + const accessToken = await refreshAccessTokenIfNeeded(credentialId, session.user.id, requestId) + if (!accessToken) { + return NextResponse.json({ error: 'Failed to obtain valid access token' }, { status: 401 }) + } + + const response = await fetch( + `https://graph.microsoft.com/v1.0/me/drive/items/${fileId}?$select=id,name,folder,webUrl,createdDateTime,lastModifiedDateTime`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + } + ) + + if (!response.ok) { + const errorData = await response.json().catch(() => ({ error: { message: 'Unknown error' } })) + return NextResponse.json( + { error: errorData.error?.message || 'Failed to fetch folder from OneDrive' }, + { status: response.status } + ) + } + + const folder = await response.json() + + // Transform the response to match expected format + const transformedFolder = { + id: folder.id, + name: folder.name, + mimeType: 'application/vnd.microsoft.graph.folder', + webViewLink: folder.webUrl, + createdTime: folder.createdDateTime, + modifiedTime: folder.lastModifiedDateTime, + } + + return NextResponse.json({ file: transformedFolder }, { status: 200 }) + } catch (error) { + logger.error(`[${requestId}] Error fetching folder from OneDrive`, error) + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + } +} \ No newline at end of file diff --git a/apps/sim/app/api/tools/onedrive/folders/route.ts b/apps/sim/app/api/tools/onedrive/folders/route.ts new file mode 100644 index 00000000000..406ed7d9952 --- /dev/null +++ b/apps/sim/app/api/tools/onedrive/folders/route.ts @@ -0,0 +1,87 @@ +import { NextRequest, NextResponse } from 'next/server' +import crypto from 'node:crypto' +import { eq } from 'drizzle-orm' +import { getSession } from '@/lib/auth' +import { db } from '@/db' +import { account } from '@/db/schema' +import { createLogger } from '@/lib/logs/console/logger' +import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('OneDriveFoldersAPI') + +/** + * Get folders from Microsoft OneDrive + */ +export async function GET(request: NextRequest) { + const requestId = crypto.randomUUID().slice(0, 8) + + try { + const session = await getSession() + if (!session?.user?.id) { + return NextResponse.json({ error: 'User not authenticated' }, { status: 401 }) + } + + const { searchParams } = new URL(request.url) + const credentialId = searchParams.get('credentialId') + const query = searchParams.get('query') || '' + + if (!credentialId) { + return NextResponse.json({ error: 'Credential ID is required' }, { status: 400 }) + } + + const credentials = await db.select().from(account).where(eq(account.id, credentialId)).limit(1) + if (!credentials.length) { + return NextResponse.json({ error: 'Credential not found' }, { status: 404 }) + } + + const credential = credentials[0] + if (credential.userId !== session.user.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 403 }) + } + + const accessToken = await refreshAccessTokenIfNeeded(credentialId, session.user.id, requestId) + if (!accessToken) { + return NextResponse.json({ error: 'Failed to obtain valid access token' }, { status: 401 }) + } + + // Build URL for OneDrive folders + let url = `https://graph.microsoft.com/v1.0/me/drive/root/children?$filter=folder ne null&$select=id,name,folder,webUrl,createdDateTime,lastModifiedDateTime&$top=50` + + if (query) { + url += `&$search="${encodeURIComponent(query)}"` + } + + const response = await fetch(url, { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => ({ error: { message: 'Unknown error' } })) + return NextResponse.json( + { error: errorData.error?.message || 'Failed to fetch folders from OneDrive' }, + { status: response.status } + ) + } + + const data = await response.json() + const folders = (data.value || []) + .filter((item: any) => item.folder) // Only folders + .map((folder: any) => ({ + id: folder.id, + name: folder.name, + mimeType: 'application/vnd.microsoft.graph.folder', + webViewLink: folder.webUrl, + createdTime: folder.createdDateTime, + modifiedTime: folder.lastModifiedDateTime, + })) + + return NextResponse.json({ files: folders }, { status: 200 }) + } catch (error) { + logger.error(`[${requestId}] Error fetching folders from OneDrive`, error) + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + } +} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/microsoft-file-selector.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/microsoft-file-selector.tsx index 9549f305ef0..4c47ae7009a 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/microsoft-file-selector.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/microsoft-file-selector.tsx @@ -128,7 +128,7 @@ export function MicrosoftFileSelector({ } }, [provider, getProviderId, selectedCredentialId]) - // Fetch available Excel files for the selected credential + // Fetch available files for the selected credential const fetchAvailableFiles = useCallback(async () => { if (!selectedCredentialId) return @@ -143,7 +143,12 @@ export function MicrosoftFileSelector({ queryParams.append('query', searchQuery.trim()) } - const response = await fetch(`/api/auth/oauth/microsoft/files?${queryParams.toString()}`) + // Route to correct endpoint based on service + const endpoint = serviceId === 'onedrive' + ? `/api/tools/onedrive/folders?${queryParams.toString()}` + : `/api/auth/oauth/microsoft/files?${queryParams.toString()}` + + const response = await fetch(endpoint) if (response.ok) { const data = await response.json() @@ -160,7 +165,7 @@ export function MicrosoftFileSelector({ } finally { setIsLoadingFiles(false) } - }, [selectedCredentialId, searchQuery]) + }, [selectedCredentialId, searchQuery, serviceId]) // Fetch a single file by ID when we have a selectedFileId but no metadata const fetchFileById = useCallback( @@ -175,7 +180,12 @@ export function MicrosoftFileSelector({ fileId: fileId, }) - const response = await fetch(`/api/auth/oauth/microsoft/file?${queryParams.toString()}`) + // Route to correct endpoint based on service + const endpoint = serviceId === 'onedrive' + ? `/api/tools/onedrive/folder?${queryParams.toString()}` + : `/api/auth/oauth/microsoft/file?${queryParams.toString()}` + + const response = await fetch(endpoint) if (response.ok) { const data = await response.json() @@ -204,7 +214,7 @@ export function MicrosoftFileSelector({ setIsLoadingSelectedFile(false) } }, - [selectedCredentialId, onFileInfoChange] + [selectedCredentialId, onFileInfoChange, serviceId] ) // Fetch credentials on initial mount @@ -324,6 +334,14 @@ export function MicrosoftFileSelector({ return } + // Handle OneDrive specifically by checking serviceId + if (baseProvider === 'microsoft' && serviceId === 'onedrive') { + const onedriveService = baseProviderConfig.services['onedrive'] + if (onedriveService) { + return onedriveService.icon({ className: 'h-4 w-4' }) + } + } + // For compound providers, find the specific service if (providerName.includes('-')) { for (const service of Object.values(baseProviderConfig.services)) { @@ -397,6 +415,27 @@ export function MicrosoftFileSelector({ setSearchQuery(query) } + const getFileTypeTitleCase = () => { + return serviceId === 'onedrive' ? 'Folders' : 'Excel Files' + } + + const getSearchPlaceholder = () => { + return serviceId === 'onedrive' ? 'Search OneDrive folders...' : 'Search Excel files...' + } + + const getEmptyStateText = () => { + if (serviceId === 'onedrive') { + return { + title: 'No folders found.', + description: 'No folders were found in your OneDrive.' + } + } + return { + title: 'No Excel files found.', + description: 'No .xlsx files were found in your OneDrive.' + } + } + return ( <>
@@ -463,7 +502,7 @@ export function MicrosoftFileSelector({ )} - + {isLoading || isLoadingFiles ? ( @@ -480,9 +519,9 @@ export function MicrosoftFileSelector({
) : availableFiles.length === 0 ? (
-

No Excel files found.

+

{getEmptyStateText().title}

- No .xlsx files were found in your OneDrive. + {getEmptyStateText().description}

) : null} @@ -510,11 +549,11 @@ export function MicrosoftFileSelector({ )} - {/* Available Excel files - only show if we have credentials and files */} + {/* Available files - only show if we have credentials and files */} {credentials.length > 0 && selectedCredentialId && availableFiles.length > 0 && (
- Excel Files + {getFileTypeTitleCase()}
{availableFiles.map((file) => ( + + +
+ void} + /> +
+
+ {!credential && ( + +

Please select Microsoft credentials first

+
+ )} +
+ + ) + } + // Handle Microsoft Teams selector if (isMicrosoftTeams) { // Get credential using the same pattern as other tools diff --git a/apps/sim/blocks/blocks/onedrive.ts b/apps/sim/blocks/blocks/onedrive.ts index e69de29bb2d..a6b2e272583 100644 --- a/apps/sim/blocks/blocks/onedrive.ts +++ b/apps/sim/blocks/blocks/onedrive.ts @@ -0,0 +1,245 @@ +import { MicrosoftOneDriveIcon } from '@/components/icons' +import type { BlockConfig } from '@/blocks/types' +import type { OneDriveResponse } from '@/tools/onedrive/types' + +export const OneDriveBlock: BlockConfig = { + type: 'onedrive', + name: 'OneDrive', + description: 'Create, upload, and list files', + longDescription: + 'Integrate OneDrive functionality to manage files and folders. Upload new files, get content from existing files, create new folders, and list contents of folders using OAuth authentication. Supports file operations with custom MIME types and folder organization.', + docsLink: 'https://docs.sim.ai/tools/onedrive', + category: 'tools', + bgColor: '#E0E0E0', + icon: MicrosoftOneDriveIcon, + subBlocks: [ + // Operation selector + { + id: 'operation', + title: 'Operation', + type: 'dropdown', + layout: 'full', + options: [ + { label: 'Create Folder', id: 'create_folder' }, + { label: 'Upload File', id: 'upload' }, + // { label: 'Get File Content', id: 'get_content' }, + { label: 'List Files', id: 'list' }, + ], + }, + // Google Drive Credentials + { + id: 'credential', + title: 'Microsoft Account', + type: 'oauth-input', + layout: 'full', + provider: 'onedrive', + serviceId: 'onedrive', + requiredScopes: ['openid', 'profile', 'email','Files.Read', 'Files.ReadWrite', 'offline_access'], + placeholder: 'Select Microsoft account', + }, + // Upload Fields + { + id: 'fileName', + title: 'File Name', + type: 'short-input', + layout: 'full', + placeholder: 'Name of the file', + condition: { field: 'operation', value: 'upload' }, + }, + { + id: 'content', + title: 'Content', + type: 'long-input', + layout: 'full', + placeholder: 'Content to upload to the file', + condition: { field: 'operation', value: 'upload' }, + }, + + { + id: 'folderSelector', + title: 'Select Parent Folder', + type: 'file-selector', + layout: 'full', + provider: 'microsoft', + serviceId: 'onedrive', + requiredScopes: ['openid', 'profile', 'email', 'Files.Read', 'Files.ReadWrite', 'offline_access'], + mimeType: 'application/vnd.microsoft.graph.folder', + placeholder: 'Select a parent folder', + mode: 'basic', + condition: { field: 'operation', value: 'upload' }, + }, + { + id: 'manualFolderId', + title: 'Parent Folder ID', + type: 'short-input', + layout: 'full', + placeholder: 'Enter parent folder ID (leave empty for root folder)', + mode: 'advanced', + condition: { field: 'operation', value: 'upload' }, + }, + // Get Content Fields + // { + // id: 'fileId', + // title: 'Select File', + // type: 'file-selector', + // layout: 'full', + // provider: 'google-drive', + // serviceId: 'google-drive', + // requiredScopes: [], + // placeholder: 'Select a file', + // condition: { field: 'operation', value: 'get_content' }, + // }, + // // Manual File ID input (shown only when no file is selected) + // { + // id: 'fileId', + // title: 'Or Enter File ID Manually', + // type: 'short-input', + // layout: 'full', + // placeholder: 'ID of the file to get content from', + // condition: { + // field: 'operation', + // value: 'get_content', + // and: { + // field: 'fileId', + // value: '', + // }, + // }, + // }, + // Export format for Google Workspace files + // { + // id: 'mimeType', + // title: 'Export Format', + // type: 'dropdown', + // layout: 'full', + // options: [ + // { label: 'Plain Text', id: 'text/plain' }, + // { label: 'HTML', id: 'text/html' }, + // ], + // placeholder: 'Optional: Choose export format for Google Workspace files', + // condition: { field: 'operation', value: 'get_content' }, + // }, + // Create Folder Fields + { + id: 'fileName', + title: 'Folder Name', + type: 'short-input', + layout: 'full', + placeholder: 'Name for the new folder', + condition: { field: 'operation', value: 'create_folder' }, + }, + { + id: 'folderSelector', + title: 'Select Parent Folder', + type: 'file-selector', + layout: 'full', + provider: 'microsoft', + serviceId: 'onedrive', + requiredScopes: ['openid', 'profile', 'email', 'Files.Read', 'Files.ReadWrite', 'offline_access'], + mimeType: 'application/vnd.microsoft.graph.folder', + placeholder: 'Select a parent folder', + mode: 'basic', + condition: { field: 'operation', value: 'create_folder' }, + }, + // Manual Folder ID input (advanced mode) + { + id: 'manualFolderId', + title: 'Parent Folder ID', + type: 'short-input', + layout: 'full', + placeholder: 'Enter parent folder ID (leave empty for root folder)', + mode: 'advanced', + condition: { field: 'operation', value: 'create_folder' }, + }, + // List Fields - Folder Selector (basic mode) + { + id: 'folderSelector', + title: 'Select Folder', + type: 'file-selector', + layout: 'full', + provider: 'microsoft', + serviceId: 'onedrive', + requiredScopes: ['openid', 'profile', 'email', 'Files.Read', 'Files.ReadWrite', 'offline_access'], + mimeType: 'application/vnd.microsoft.graph.folder', + placeholder: 'Select a folder to list files from', + mode: 'basic', + condition: { field: 'operation', value: 'list' }, + }, + // Manual Folder ID input (advanced mode) + { + id: 'manualFolderId', + title: 'Folder ID', + type: 'short-input', + layout: 'full', + placeholder: 'Enter folder ID (leave empty for root folder)', + mode: 'advanced', + condition: { field: 'operation', value: 'list' }, + }, + { + id: 'query', + title: 'Search Query', + type: 'short-input', + layout: 'full', + placeholder: 'Search for specific files (e.g., name contains "report")', + condition: { field: 'operation', value: 'list' }, + }, + { + id: 'pageSize', + title: 'Results Per Page', + type: 'short-input', + layout: 'full', + placeholder: 'Number of results (default: 100, max: 1000)', + condition: { field: 'operation', value: 'list' }, + }, + ], + tools: { + access: ['onedrive_upload', 'onedrive_create_folder', 'onedrive_list'], + config: { + tool: (params) => { + switch (params.operation) { + case 'upload': + return 'onedrive_upload' + // case 'get_content': + // return 'google_drive_get_content' + case 'create_folder': + return 'onedrive_create_folder' + case 'list': + return 'onedrive_list' + default: + throw new Error(`Invalid OneDrive operation: ${params.operation}`) + } + }, + params: (params) => { + const { credential, folderSelector, manualFolderId, mimeType, ...rest } = params + + // Use folderSelector if provided, otherwise use manualFolderId + const effectiveFolderId = (folderSelector || manualFolderId || '').trim() + + return { + accessToken: credential, + folderId: effectiveFolderId, + pageSize: rest.pageSize ? Number.parseInt(rest.pageSize as string, 10) : undefined, + mimeType: mimeType, + ...rest, + } + }, + }, + }, + inputs: { + operation: { type: 'string', required: true }, + credential: { type: 'string', required: true }, + // Upload and Create Folder operation inputs + fileName: { type: 'string', required: false }, + content: { type: 'string', required: false }, + // Get Content operation inputs + // fileId: { type: 'string', required: false }, + // List operation inputs + folderSelector: { type: 'string', required: false }, + manualFolderId: { type: 'string', required: false }, + query: { type: 'string', required: false }, + pageSize: { type: 'number', required: false }, + }, + outputs: { + file: 'json', + files: 'json', + }, +} diff --git a/apps/sim/blocks/registry.ts b/apps/sim/blocks/registry.ts index a307e84fedc..655cfcdff7c 100644 --- a/apps/sim/blocks/registry.ts +++ b/apps/sim/blocks/registry.ts @@ -41,6 +41,7 @@ import { MistralParseBlock } from '@/blocks/blocks/mistral_parse' import { NotionBlock } from '@/blocks/blocks/notion' import { OpenAIBlock } from '@/blocks/blocks/openai' import { OutlookBlock } from '@/blocks/blocks/outlook' +import { OneDriveBlock } from '@/blocks/blocks/onedrive' import { PerplexityBlock } from '@/blocks/blocks/perplexity' import { PineconeBlock } from '@/blocks/blocks/pinecone' import { QdrantBlock } from '@/blocks/blocks/qdrant' @@ -110,6 +111,7 @@ export const registry: Record = { notion: NotionBlock, openai: OpenAIBlock, outlook: OutlookBlock, + onedrive: OneDriveBlock, perplexity: PerplexityBlock, pinecone: PineconeBlock, qdrant: QdrantBlock, diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx index c743b572263..b559dd04365 100644 --- a/apps/sim/components/icons.tsx +++ b/apps/sim/components/icons.tsx @@ -3187,7 +3187,7 @@ export function MicrosoftOneDriveIcon(props: SVGProps) { diff --git a/apps/sim/lib/auth.ts b/apps/sim/lib/auth.ts index 14760e71686..67bdf36a201 100644 --- a/apps/sim/lib/auth.ts +++ b/apps/sim/lib/auth.ts @@ -467,6 +467,22 @@ export const auth = betterAuth({ redirectURI: `${env.NEXT_PUBLIC_APP_URL}/api/auth/oauth2/callback/outlook`, }, + { + providerId: 'onedrive', + clientId: env.MICROSOFT_CLIENT_ID as string, + clientSecret: env.MICROSOFT_CLIENT_SECRET as string, + authorizationUrl: 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize', + tokenUrl: 'https://login.microsoftonline.com/common/oauth2/v2.0/token', + userInfoUrl: 'https://graph.microsoft.com/v1.0/me', + scopes: ['openid', 'profile', 'email', 'Files.Read', 'Files.ReadWrite', 'offline_access'], + responseType: 'code', + accessType: 'offline', + authentication: 'basic', + prompt: 'consent', + pkce: true, + redirectURI: `${env.NEXT_PUBLIC_APP_URL}/api/auth/oauth2/callback/onedrive`, + }, + { providerId: 'wealthbox', clientId: env.WEALTHBOX_CLIENT_ID as string, diff --git a/apps/sim/lib/oauth/oauth.ts b/apps/sim/lib/oauth/oauth.ts index 8b538c7e874..49b4b5e38ca 100644 --- a/apps/sim/lib/oauth/oauth.ts +++ b/apps/sim/lib/oauth/oauth.ts @@ -15,6 +15,7 @@ import { MicrosoftExcelIcon, MicrosoftIcon, MicrosoftTeamsIcon, + MicrosoftOneDriveIcon, NotionIcon, OutlookIcon, RedditIcon, @@ -67,7 +68,7 @@ export type OAuthService = | 'slack' | 'reddit' | 'wealthbox' - + | 'onedrive' export interface OAuthProviderConfig { id: OAuthProvider name: string @@ -201,6 +202,15 @@ export const OAUTH_PROVIDERS: Record = { 'offline_access', ], }, + 'onedrive': { + id: 'onedrive', + name: 'OneDrive', + description: 'Connect to OneDrive and manage files.', + providerId: 'onedrive', + icon: (props) => MicrosoftOneDriveIcon(props), + baseProviderIcon: (props) => MicrosoftIcon(props), + scopes: ['openid', 'profile', 'email', 'Files.Read', 'Files.ReadWrite', 'offline_access'], + }, }, defaultService: 'microsoft', }, @@ -543,6 +553,12 @@ export function parseProvider(provider: OAuthProvider): ProviderConfig { featureType: 'outlook', } } + else if (provider === 'onedrive') { + return { + baseProvider: 'microsoft', + featureType: 'onedrive', + } + } // Handle compound providers (e.g., 'google-email' -> { baseProvider: 'google', featureType: 'email' }) const [base, feature] = provider.split('-') diff --git a/apps/sim/tools/onedrive/create_folder.ts b/apps/sim/tools/onedrive/create_folder.ts index e69de29bb2d..b106b5f8be4 100644 --- a/apps/sim/tools/onedrive/create_folder.ts +++ b/apps/sim/tools/onedrive/create_folder.ts @@ -0,0 +1,88 @@ +import type { OneDriveToolParams, OneDriveUploadResponse } from '@/tools/onedrive/types' +import type { ToolConfig } from '@/tools/types' + +export const createFolderTool: ToolConfig = { + id: 'onedrive_create_folder', + name: 'Create Folder in OneDrive', + description: 'Create a new folder in OneDrive', + version: '1.0', + oauth: { + required: true, + provider: 'onedrive', + additionalScopes: [], + }, + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the OneDrive API', + }, + fileName: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Name of the folder to create', + }, + folderSelector: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Select the parent folder to create the folder in', + }, + folderId: { + type: 'string', + required: false, + visibility: 'hidden', + description: 'ID of the parent folder (internal use)', + }, + }, + request: { + url: (params) => { + // Use specific parent folder URL if parentId is provided + const parentFolderId = params.folderSelector || params.folderId + if (parentFolderId) { + return `https://graph.microsoft.com/v1.0/me/drive/items/${parentFolderId}/children` + } + return 'https://graph.microsoft.com/v1.0/me/drive/root/children' + }, + method: 'POST', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + body: (params) => { + return { + name: params.fileName, + folder: {}, // Required facet for folder creation in Microsoft Graph API + '@microsoft.graph.conflictBehavior': 'rename', // Handle name conflicts + } + }, + }, + transformResponse: async (response: Response) => { + if (!response.ok) { + const data = await response.json().catch(() => ({})) + throw new Error(data.error?.message || 'Failed to create folder in OneDrive') + } + const data = await response.json() + + return { + success: true, + output: { + file: { + id: data.id, + name: data.name, + mimeType: 'application/vnd.microsoft.graph.folder', + webViewLink: data.webUrl, + size: data.size, + createdTime: data.createdDateTime, + modifiedTime: data.lastModifiedDateTime, + parentReference: data.parentReference, + }, + }, + } + }, + transformError: (error) => { + return error.message || 'An error occurred while creating folder in OneDrive' + }, +} diff --git a/apps/sim/tools/onedrive/index.ts b/apps/sim/tools/onedrive/index.ts index e69de29bb2d..30298d9d783 100644 --- a/apps/sim/tools/onedrive/index.ts +++ b/apps/sim/tools/onedrive/index.ts @@ -0,0 +1,7 @@ +import { createFolderTool } from '@/tools/onedrive/create_folder' +import { listTool } from '@/tools/onedrive/list' +import { uploadTool } from '@/tools/onedrive/upload' + +export const onedriveCreateFolderTool = createFolderTool +export const onedriveListTool = listTool +export const onedriveUploadTool = uploadTool diff --git a/apps/sim/tools/onedrive/list.ts b/apps/sim/tools/onedrive/list.ts index e69de29bb2d..ddb3fafb564 100644 --- a/apps/sim/tools/onedrive/list.ts +++ b/apps/sim/tools/onedrive/list.ts @@ -0,0 +1,117 @@ +import type { OneDriveListResponse, OneDriveToolParams } from '@/tools/onedrive/types' +import type { ToolConfig } from '@/tools/types' + +export const listTool: ToolConfig = { + id: 'onedrive_list', + name: 'List OneDrive Files', + description: 'List files and folders in OneDrive', + version: '1.0', + oauth: { + required: true, + provider: 'onedrive', + additionalScopes: ['openid', 'profile', 'email', 'Files.Read', 'Files.ReadWrite', 'offline_access'], + }, + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the OneDrive API', + }, + folderSelector: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Select the folder to list files from', + }, + folderId: { + type: 'string', + required: false, + visibility: 'hidden', + description: 'The ID of the folder to list files from (internal use)', + }, + query: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'A query to filter the files', + }, + pageSize: { + type: 'number', + required: false, + visibility: 'user-only', + description: 'The number of files to return', + }, + pageToken: { + type: 'string', + required: false, + visibility: 'hidden', + description: 'The page token to use for pagination', + }, + }, + request: { + url: (params) => { + // Use specific folder if provided, otherwise use root + const folderId = params.folderId || params.folderSelector + const baseUrl = folderId + ? `https://graph.microsoft.com/v1.0/me/drive/items/${folderId}/children` + : 'https://graph.microsoft.com/v1.0/me/drive/root/children' + + const url = new URL(baseUrl) + + // Use Microsoft Graph $select parameter + url.searchParams.append( + '$select', + 'id,name,file,folder,webUrl,size,createdDateTime,lastModifiedDateTime,parentReference' + ) + + // Add name filter if query provided + if (params.query) { + url.searchParams.append('$filter', `startswith(name,'${params.query}')`) + } + + // Add pagination + if (params.pageSize) { + url.searchParams.append('$top', params.pageSize.toString()) + } + + if (params.pageToken) { + url.searchParams.append('$skip', params.pageToken) + } + + return url.toString() + }, + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + }), + }, + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error?.message || 'Failed to list OneDrive files') + } + + return { + success: true, + output: { + files: data.value.map((item: any) => ({ + id: item.id, + name: item.name, + mimeType: item.file?.mimeType || (item.folder ? 'application/folder' : 'unknown'), + webViewLink: item.webUrl, + webContentLink: item['@microsoft.graph.downloadUrl'], + size: item.size?.toString() || '0', + createdTime: item.createdDateTime, + modifiedTime: item.lastModifiedDateTime, + parents: item.parentReference ? [item.parentReference.id] : [], + })), + nextPageToken: data['@odata.nextLink'] ? 'has_more' : undefined, + }, + } + }, + transformError: (error) => { + return error.message || 'An error occurred while listing OneDrive files' + }, +} diff --git a/apps/sim/tools/onedrive/types.ts b/apps/sim/tools/onedrive/types.ts index e69de29bb2d..d0401ad97e8 100644 --- a/apps/sim/tools/onedrive/types.ts +++ b/apps/sim/tools/onedrive/types.ts @@ -0,0 +1,52 @@ +import type { ToolResponse } from '@/tools/types' + +export interface OneDriveFile { + id: string + name: string + mimeType: string + webViewLink?: string + webContentLink?: string + size?: string + createdTime?: string + modifiedTime?: string + parents?: string[] +} + +export interface OneDriveListResponse extends ToolResponse { + output: { + files: OneDriveFile[] + nextPageToken?: string + } +} + +export interface OneDriveUploadResponse extends ToolResponse { + output: { + file: OneDriveFile + } +} + +export interface OneDriveGetContentResponse extends ToolResponse { + output: { + content: string + metadata: OneDriveFile + } +} + +export interface OneDriveToolParams { + accessToken: string + folderId?: string + folderSelector?: string + fileId?: string + fileName?: string + content?: string + mimeType?: string + query?: string + pageSize?: number + pageToken?: string + exportMimeType?: string +} + +export type OneDriveResponse = + | OneDriveUploadResponse + | OneDriveGetContentResponse + | OneDriveListResponse diff --git a/apps/sim/tools/onedrive/upload.ts b/apps/sim/tools/onedrive/upload.ts index e69de29bb2d..f64eb82d591 100644 --- a/apps/sim/tools/onedrive/upload.ts +++ b/apps/sim/tools/onedrive/upload.ts @@ -0,0 +1,125 @@ +import { createLogger } from '@/lib/logs/console/logger' +import type { OneDriveToolParams, OneDriveUploadResponse } from '@/tools/onedrive/types' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('OneDriveUploadTool') + +export const uploadTool: ToolConfig = { + id: 'onedrive_upload', + name: 'Upload to OneDrive', + description: 'Upload a file to OneDrive', + version: '1.0', + oauth: { + required: true, + provider: 'onedrive', + additionalScopes: ['openid', 'profile', 'email', 'Files.Read', 'Files.ReadWrite', 'offline_access'], + }, + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the OneDrive API', + }, + fileName: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The name of the file to upload', + }, + content: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The content of the file to upload', + }, + folderSelector: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Select the folder to upload the file to', + }, + folderId: { + type: 'string', + required: false, + visibility: 'hidden', + description: 'The ID of the folder to upload the file to (internal use)', + }, + }, + request: { + url: (params) => { + let fileName = params.fileName || 'untitled' + + // Always create .txt files for text content + if (!fileName.endsWith('.txt')) { + // Remove any existing extensions and add .txt + fileName = fileName.replace(/\.[^.]*$/, '') + '.txt' + } + + // Build the proper URL based on parent folder + const parentFolderId = params.folderSelector || params.folderId + if (parentFolderId && parentFolderId.trim() !== '') { + return `https://graph.microsoft.com/v1.0/me/drive/items/${parentFolderId}:/${fileName}:/content` + } + // Default to root folder + return `https://graph.microsoft.com/v1.0/me/drive/root:/${fileName}:/content` + }, + method: 'PUT', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'text/plain', + }), + body: (params) => (params.content || '') as unknown as Record, + }, + transformResponse: async (response: Response, params?: OneDriveToolParams) => { + try { + if (!response.ok) { + const errorData = await response.json().catch(() => ({})) + logger.error('Failed to upload file to OneDrive', { + status: response.status, + statusText: response.statusText, + errorData, + }) + throw new Error(errorData.error?.message || 'Failed to upload file to OneDrive') + } + + // Microsoft Graph API returns the file metadata directly + const fileData = await response.json() + + logger.info('Successfully uploaded file to OneDrive', { + fileId: fileData.id, + fileName: fileData.name, + }) + + return { + success: true, + output: { + file: { + id: fileData.id, + name: fileData.name, + mimeType: fileData.file?.mimeType || params?.mimeType || 'text/plain', + webViewLink: fileData.webUrl, + webContentLink: fileData['@microsoft.graph.downloadUrl'], + size: fileData.size, + createdTime: fileData.createdDateTime, + modifiedTime: fileData.lastModifiedDateTime, + parentReference: fileData.parentReference, + }, + }, + } + } catch (error: any) { + logger.error('Error in upload transformation', { + error: error.message, + stack: error.stack, + }) + throw error + } + }, + transformError: (error) => { + logger.error('Upload error', { + error: error.message, + stack: error.stack, + }) + return error.message || 'An error occurred while uploading to OneDrive' + }, +} diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index fe580703043..02449bf32e4 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -79,6 +79,7 @@ import { microsoftExcelTableAddTool, microsoftExcelWriteTool, } from '@/tools/microsoft_excel' +import { onedriveCreateFolderTool, onedriveUploadTool, onedriveListTool } from '@/tools/onedrive' import { microsoftTeamsReadChannelTool, microsoftTeamsReadChatTool, @@ -265,6 +266,9 @@ export const tools: Record = { outlook_draft: outlookDraftTool, linear_read_issues: linearReadIssuesTool, linear_create_issue: linearCreateIssueTool, + onedrive_create_folder: onedriveCreateFolderTool, + onedrive_upload: onedriveUploadTool, + onedrive_list: onedriveListTool, microsoft_excel_read: microsoftExcelReadTool, microsoft_excel_write: microsoftExcelWriteTool, microsoft_excel_table_add: microsoftExcelTableAddTool, From ecc230970d83135a21a59c4e8256385daa4e34e1 Mon Sep 17 00:00:00 2001 From: Adam Gough Date: Tue, 29 Jul 2025 23:15:34 -0700 Subject: [PATCH 03/23] added refresh --- apps/sim/lib/oauth/oauth.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/apps/sim/lib/oauth/oauth.ts b/apps/sim/lib/oauth/oauth.ts index 49b4b5e38ca..4a8629ceb12 100644 --- a/apps/sim/lib/oauth/oauth.ts +++ b/apps/sim/lib/oauth/oauth.ts @@ -728,6 +728,18 @@ function getProviderAuthConfig(provider: string): ProviderAuthConfig { useBasicAuth: false, } } + case 'onedrive': { + const { clientId, clientSecret } = getCredentials( + env.MICROSOFT_CLIENT_ID, + env.MICROSOFT_CLIENT_SECRET + ) + return { + tokenEndpoint: 'https://login.microsoftonline.com/common/oauth2/v2.0/token', + clientId, + clientSecret, + useBasicAuth: false, + } + } case 'linear': { const { clientId, clientSecret } = getCredentials( env.LINEAR_CLIENT_ID, From 2ba03e229108a4dab7018092e534d3c3336c1b85 Mon Sep 17 00:00:00 2001 From: Adam Gough Date: Thu, 31 Jul 2025 17:52:20 -0700 Subject: [PATCH 04/23] added sharepoint with create page --- .../app/api/tools/sharepoint/site/route.ts | 105 ++++++++++++ .../app/api/tools/sharepoint/sites/route.ts | 84 +++++++++ .../components/microsoft-file-selector.tsx | 44 ++++- .../file-selector/file-selector-input.tsx | 73 ++++++++ apps/sim/blocks/blocks/onedrive.ts | 44 +---- apps/sim/blocks/blocks/sharepoint.ts | 162 ++++++++++++++++++ apps/sim/blocks/registry.ts | 2 + apps/sim/components/icons.tsx | 21 +++ apps/sim/lib/auth.ts | 16 ++ apps/sim/lib/oauth/oauth.ts | 30 ++++ apps/sim/tools/registry.ts | 4 + apps/sim/tools/sharepoint/create_page.ts | 152 ++++++++++++++++ apps/sim/tools/sharepoint/index.ts | 7 + apps/sim/tools/sharepoint/read_page.ts | 156 +++++++++++++++++ apps/sim/tools/sharepoint/read_site.ts | 121 +++++++++++++ apps/sim/tools/sharepoint/types.ts | 83 +++++++++ 16 files changed, 1053 insertions(+), 51 deletions(-) create mode 100644 apps/sim/app/api/tools/sharepoint/site/route.ts create mode 100644 apps/sim/app/api/tools/sharepoint/sites/route.ts create mode 100644 apps/sim/blocks/blocks/sharepoint.ts create mode 100644 apps/sim/tools/sharepoint/create_page.ts create mode 100644 apps/sim/tools/sharepoint/index.ts create mode 100644 apps/sim/tools/sharepoint/read_page.ts create mode 100644 apps/sim/tools/sharepoint/read_site.ts create mode 100644 apps/sim/tools/sharepoint/types.ts diff --git a/apps/sim/app/api/tools/sharepoint/site/route.ts b/apps/sim/app/api/tools/sharepoint/site/route.ts new file mode 100644 index 00000000000..2a6ff702776 --- /dev/null +++ b/apps/sim/app/api/tools/sharepoint/site/route.ts @@ -0,0 +1,105 @@ +import { NextRequest, NextResponse } from 'next/server' +import crypto from 'node:crypto' +import { eq } from 'drizzle-orm' +import { getSession } from '@/lib/auth' +import { db } from '@/db' +import { account } from '@/db/schema' +import { createLogger } from '@/lib/logs/console/logger' +import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('SharePointSiteAPI') + +/** + * Get a single SharePoint site from Microsoft Graph API + */ +export async function GET(request: NextRequest) { + const requestId = crypto.randomUUID().slice(0, 8) + + try { + const session = await getSession() + if (!session?.user?.id) { + return NextResponse.json({ error: 'User not authenticated' }, { status: 401 }) + } + + const { searchParams } = new URL(request.url) + const credentialId = searchParams.get('credentialId') + const fileId = searchParams.get('fileId') // This will be the site ID + + if (!credentialId || !fileId) { + return NextResponse.json({ error: 'Credential ID and Site ID are required' }, { status: 400 }) + } + + const credentials = await db.select().from(account).where(eq(account.id, credentialId)).limit(1) + if (!credentials.length) { + return NextResponse.json({ error: 'Credential not found' }, { status: 404 }) + } + + const credential = credentials[0] + if (credential.userId !== session.user.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 403 }) + } + + const accessToken = await refreshAccessTokenIfNeeded(credentialId, session.user.id, requestId) + if (!accessToken) { + return NextResponse.json({ error: 'Failed to obtain valid access token' }, { status: 401 }) + } + + // Handle different ways to access SharePoint sites: + // 1. Site ID: sites/{site-id} + // 2. Root site: sites/root + // 3. Hostname: sites/{hostname} + // 4. Server-relative URL: sites/{hostname}:/{server-relative-path} + // 5. Group team site: groups/{group-id}/sites/root + + let endpoint: string + if (fileId === 'root') { + endpoint = 'sites/root' + } else if (fileId.includes(':')) { + // Server-relative URL format + endpoint = `sites/${fileId}` + } else if (fileId.includes('groups/')) { + // Group team site format + endpoint = fileId + } else { + // Standard site ID or hostname + endpoint = `sites/${fileId}` + } + + const response = await fetch( + `https://graph.microsoft.com/v1.0/${endpoint}?$select=id,name,displayName,webUrl,createdDateTime,lastModifiedDateTime`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + } + ) + + if (!response.ok) { + const errorData = await response.json().catch(() => ({ error: { message: 'Unknown error' } })) + return NextResponse.json( + { error: errorData.error?.message || 'Failed to fetch site from SharePoint' }, + { status: response.status } + ) + } + + const site = await response.json() + + // Transform the response to match expected format + const transformedSite = { + id: site.id, + name: site.displayName || site.name, + mimeType: 'application/vnd.microsoft.graph.site', + webViewLink: site.webUrl, + createdTime: site.createdDateTime, + modifiedTime: site.lastModifiedDateTime, + } + + logger.info(`[${requestId}] Successfully fetched SharePoint site: ${transformedSite.name}`) + return NextResponse.json({ file: transformedSite }, { status: 200 }) + } catch (error) { + logger.error(`[${requestId}] Error fetching site from SharePoint`, error) + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + } +} \ No newline at end of file diff --git a/apps/sim/app/api/tools/sharepoint/sites/route.ts b/apps/sim/app/api/tools/sharepoint/sites/route.ts new file mode 100644 index 00000000000..a72871aebd1 --- /dev/null +++ b/apps/sim/app/api/tools/sharepoint/sites/route.ts @@ -0,0 +1,84 @@ +import { NextRequest, NextResponse } from 'next/server' +import crypto from 'node:crypto' +import { eq } from 'drizzle-orm' +import { getSession } from '@/lib/auth' +import { db } from '@/db' +import { account } from '@/db/schema' +import { createLogger } from '@/lib/logs/console/logger' +import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('SharePointSitesAPI') + +/** + * Get SharePoint sites from Microsoft Graph API + */ +export async function GET(request: NextRequest) { + const requestId = crypto.randomUUID().slice(0, 8) + + try { + const session = await getSession() + if (!session?.user?.id) { + return NextResponse.json({ error: 'User not authenticated' }, { status: 401 }) + } + + const { searchParams } = new URL(request.url) + const credentialId = searchParams.get('credentialId') + const query = searchParams.get('query') || '' + + if (!credentialId) { + return NextResponse.json({ error: 'Credential ID is required' }, { status: 400 }) + } + + const credentials = await db.select().from(account).where(eq(account.id, credentialId)).limit(1) + if (!credentials.length) { + return NextResponse.json({ error: 'Credential not found' }, { status: 404 }) + } + + const credential = credentials[0] + if (credential.userId !== session.user.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 403 }) + } + + const accessToken = await refreshAccessTokenIfNeeded(credentialId, session.user.id, requestId) + if (!accessToken) { + return NextResponse.json({ error: 'Failed to obtain valid access token' }, { status: 401 }) + } + + // Build URL for SharePoint sites + // Use search=* to get all sites the user has access to, or search for specific query + const searchQuery = query || '*' + let url = `https://graph.microsoft.com/v1.0/sites?search=${encodeURIComponent(searchQuery)}&$select=id,name,displayName,webUrl,createdDateTime,lastModifiedDateTime&$top=50` + + const response = await fetch(url, { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => ({ error: { message: 'Unknown error' } })) + return NextResponse.json( + { error: errorData.error?.message || 'Failed to fetch sites from SharePoint' }, + { status: response.status } + ) + } + + const data = await response.json() + const sites = (data.value || []).map((site: any) => ({ + id: site.id, + name: site.displayName || site.name, + mimeType: 'application/vnd.microsoft.graph.site', + webViewLink: site.webUrl, + createdTime: site.createdDateTime, + modifiedTime: site.lastModifiedDateTime, + })) + + logger.info(`[${requestId}] Successfully fetched ${sites.length} SharePoint sites`) + return NextResponse.json({ files: sites }, { status: 200 }) + } catch (error) { + logger.error(`[${requestId}] Error fetching sites from SharePoint`, error) + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + } +} \ No newline at end of file diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/microsoft-file-selector.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/microsoft-file-selector.tsx index 4c47ae7009a..e632bea2c7a 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/microsoft-file-selector.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/microsoft-file-selector.tsx @@ -144,9 +144,14 @@ export function MicrosoftFileSelector({ } // Route to correct endpoint based on service - const endpoint = serviceId === 'onedrive' - ? `/api/tools/onedrive/folders?${queryParams.toString()}` - : `/api/auth/oauth/microsoft/files?${queryParams.toString()}` + let endpoint: string + if (serviceId === 'onedrive') { + endpoint = `/api/tools/onedrive/folders?${queryParams.toString()}` + } else if (serviceId === 'sharepoint') { + endpoint = `/api/tools/sharepoint/sites?${queryParams.toString()}` + } else { + endpoint = `/api/auth/oauth/microsoft/files?${queryParams.toString()}` + } const response = await fetch(endpoint) @@ -181,9 +186,14 @@ export function MicrosoftFileSelector({ }) // Route to correct endpoint based on service - const endpoint = serviceId === 'onedrive' - ? `/api/tools/onedrive/folder?${queryParams.toString()}` - : `/api/auth/oauth/microsoft/file?${queryParams.toString()}` + let endpoint: string + if (serviceId === 'onedrive') { + endpoint = `/api/tools/onedrive/folder?${queryParams.toString()}` + } else if (serviceId === 'sharepoint') { + endpoint = `/api/tools/sharepoint/site?${queryParams.toString()}` + } else { + endpoint = `/api/auth/oauth/microsoft/file?${queryParams.toString()}` + } const response = await fetch(endpoint) @@ -342,6 +352,14 @@ export function MicrosoftFileSelector({ } } + // Handle SharePoint specifically by checking serviceId + if (baseProvider === 'microsoft' && serviceId === 'sharepoint') { + const sharepointService = baseProviderConfig.services['sharepoint'] + if (sharepointService) { + return sharepointService.icon({ className: 'h-4 w-4' }) + } + } + // For compound providers, find the specific service if (providerName.includes('-')) { for (const service of Object.values(baseProviderConfig.services)) { @@ -416,11 +434,15 @@ export function MicrosoftFileSelector({ } const getFileTypeTitleCase = () => { - return serviceId === 'onedrive' ? 'Folders' : 'Excel Files' + if (serviceId === 'onedrive') return 'Folders' + if (serviceId === 'sharepoint') return 'Sites' + return 'Excel Files' } const getSearchPlaceholder = () => { - return serviceId === 'onedrive' ? 'Search OneDrive folders...' : 'Search Excel files...' + if (serviceId === 'onedrive') return 'Search OneDrive folders...' + if (serviceId === 'sharepoint') return 'Search SharePoint sites...' + return 'Search Excel files...' } const getEmptyStateText = () => { @@ -430,6 +452,12 @@ export function MicrosoftFileSelector({ description: 'No folders were found in your OneDrive.' } } + if (serviceId === 'sharepoint') { + return { + title: 'No sites found.', + description: 'No SharePoint sites were found.' + } + } return { title: 'No Excel files found.', description: 'No .xlsx files were found in your OneDrive.' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/file-selector-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/file-selector-input.tsx index 5a8d83d6e72..9c919530b6c 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/file-selector-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/file-selector-input.tsx @@ -68,9 +68,11 @@ export function FileSelectorInput({ const isDiscord = provider === 'discord' const isMicrosoftTeams = provider === 'microsoft-teams' const isMicrosoftExcel = provider === 'microsoft-excel' + const isMicrosoftWord = provider === 'microsoft-word' const isMicrosoftOneDrive = provider === 'microsoft' && subBlock.serviceId === 'onedrive' const isGoogleCalendar = subBlock.provider === 'google-calendar' const isWealthbox = provider === 'wealthbox' + const isMicrosoftSharePoint = provider === 'microsoft' && subBlock.serviceId === 'sharepoint' // For Confluence and Jira, we need the domain and credentials const domain = isConfluence || isJira ? (getValue(blockId, 'domain') as string) || '' : '' // For Discord, we need the bot token and server ID @@ -95,6 +97,8 @@ export function FileSelectorInput({ setSelectedCalendarId(value) } else if (isWealthbox) { setSelectedWealthboxItemId(value) + } else if (isMicrosoftSharePoint) { + setSelectedFileId(value) } else { setSelectedFileId(value) } @@ -112,6 +116,8 @@ export function FileSelectorInput({ setSelectedCalendarId(value) } else if (isWealthbox) { setSelectedWealthboxItemId(value) + } else if (isMicrosoftSharePoint) { + setSelectedFileId(value) } else { setSelectedFileId(value) } @@ -126,6 +132,7 @@ export function FileSelectorInput({ isMicrosoftTeams, isGoogleCalendar, isWealthbox, + isMicrosoftSharePoint, isPreview, previewValue, ]) @@ -326,6 +333,39 @@ export function FileSelectorInput({ ) } + // Handle Microsoft Word selector + if (isMicrosoftWord) { + // Get credential using the same pattern as other tools + const credential = (getValue(blockId, 'credential') as string) || '' + + return ( + + + +
+ void} + /> +
+ + {!credential && ( + +

Please select Microsoft Word credentials first

+
+ )} + + + ) + } + // Handle Microsoft OneDrive selector if (isMicrosoftOneDrive) { const credential = (getValue(blockId, 'credential') as string) || '' @@ -358,6 +398,38 @@ export function FileSelectorInput({ ) } + // Handle Microsoft SharePoint selector + if (isMicrosoftSharePoint) { + const credential = (getValue(blockId, 'credential') as string) || '' + + return ( + + + +
+ void} + /> +
+
+ {!credential && ( + +

Please select SharePoint credentials first

+
+ )} +
+
+ ) + } + // Handle Microsoft Teams selector if (isMicrosoftTeams) { // Get credential using the same pattern as other tools @@ -480,3 +552,4 @@ export function FileSelectorInput({ /> ) } + diff --git a/apps/sim/blocks/blocks/onedrive.ts b/apps/sim/blocks/blocks/onedrive.ts index a6b2e272583..5db1e834622 100644 --- a/apps/sim/blocks/blocks/onedrive.ts +++ b/apps/sim/blocks/blocks/onedrive.ts @@ -7,7 +7,7 @@ export const OneDriveBlock: BlockConfig = { name: 'OneDrive', description: 'Create, upload, and list files', longDescription: - 'Integrate OneDrive functionality to manage files and folders. Upload new files, get content from existing files, create new folders, and list contents of folders using OAuth authentication. Supports file operations with custom MIME types and folder organization.', + 'Integrate OneDrive functionality to manage files and folders. Upload new files, create new folders, and list contents of folders using OAuth authentication. Supports file operations with custom MIME types and folder organization.', docsLink: 'https://docs.sim.ai/tools/onedrive', category: 'tools', bgColor: '#E0E0E0', @@ -77,48 +77,6 @@ export const OneDriveBlock: BlockConfig = { mode: 'advanced', condition: { field: 'operation', value: 'upload' }, }, - // Get Content Fields - // { - // id: 'fileId', - // title: 'Select File', - // type: 'file-selector', - // layout: 'full', - // provider: 'google-drive', - // serviceId: 'google-drive', - // requiredScopes: [], - // placeholder: 'Select a file', - // condition: { field: 'operation', value: 'get_content' }, - // }, - // // Manual File ID input (shown only when no file is selected) - // { - // id: 'fileId', - // title: 'Or Enter File ID Manually', - // type: 'short-input', - // layout: 'full', - // placeholder: 'ID of the file to get content from', - // condition: { - // field: 'operation', - // value: 'get_content', - // and: { - // field: 'fileId', - // value: '', - // }, - // }, - // }, - // Export format for Google Workspace files - // { - // id: 'mimeType', - // title: 'Export Format', - // type: 'dropdown', - // layout: 'full', - // options: [ - // { label: 'Plain Text', id: 'text/plain' }, - // { label: 'HTML', id: 'text/html' }, - // ], - // placeholder: 'Optional: Choose export format for Google Workspace files', - // condition: { field: 'operation', value: 'get_content' }, - // }, - // Create Folder Fields { id: 'fileName', title: 'Folder Name', diff --git a/apps/sim/blocks/blocks/sharepoint.ts b/apps/sim/blocks/blocks/sharepoint.ts new file mode 100644 index 00000000000..063cf55359e --- /dev/null +++ b/apps/sim/blocks/blocks/sharepoint.ts @@ -0,0 +1,162 @@ +import { MicrosoftSharepointIcon } from '@/components/icons' +import type { BlockConfig } from '@/blocks/types' +import type { SharepointResponse } from '@/tools/sharepoint/types' + +export const SharepointBlock: BlockConfig = { + type: 'sharepoint', + name: 'Sharepoint', + description: 'Read and create pages', + longDescription: + 'Integrate Sharepoint functionality to manage pages. Read and create pages, and list sites using OAuth authentication. Supports page operations with custom MIME types and folder organization.', + docsLink: 'https://docs.sim.ai/tools/sharepoint', + category: 'tools', + bgColor: '#E0E0E0', + icon: MicrosoftSharepointIcon, + subBlocks: [ + // Operation selector + { + id: 'operation', + title: 'Operation', + type: 'dropdown', + layout: 'full', + options: [ + { label: 'Create Page', id: 'create_page' }, + { label: 'Read Page', id: 'read_page' }, + { label: 'List Sites', id: 'list_sites' }, + ], + }, + // Google Drive Credentials + { + id: 'credential', + title: 'Microsoft Account', + type: 'oauth-input', + layout: 'full', + provider: 'sharepoint', + serviceId: 'sharepoint', + requiredScopes: ['openid', 'profile', 'email','Files.Read', 'Files.ReadWrite', 'offline_access'], + placeholder: 'Select Microsoft account', + }, + + { + id: 'siteSelector', + title: 'Select Site', + type: 'file-selector', + layout: 'full', + provider: 'microsoft', + serviceId: 'sharepoint', + requiredScopes: ['openid', 'profile', 'email', 'Files.Read', 'Files.ReadWrite', 'offline_access'], + mimeType: 'application/vnd.microsoft.graph.folder', + placeholder: 'Select a site', + mode: 'basic', + condition: { field: 'operation', value: ['create_page', 'read_page'] }, + }, + + { + id: 'pageName', + title: 'Page Name', + type: 'short-input', + layout: 'full', + placeholder: 'Name for the new page', + condition: { field: 'operation', value: ['create_page', 'read_page'] }, + }, + + { + id: 'pageContent', + title: 'Page Content', + type: 'long-input', + layout: 'full', + placeholder: 'Content of the page', + condition: { field: 'operation', value: 'create_page' }, + }, + + { + id: 'manualSiteId', + title: 'Site ID', + type: 'short-input', + layout: 'full', + placeholder: 'Enter site ID (leave empty for root site)', + mode: 'advanced', + condition: { field: 'operation', value: 'upload' }, + }, + // Manual Folder ID input (advanced mode) + { + id: 'manualSiteId', + title: 'Site ID', + type: 'short-input', + layout: 'full', + placeholder: 'Enter site ID (leave empty for root site)', + mode: 'advanced', + condition: { field: 'operation', value: 'create_page' }, + }, + // List Fields - Site Selector (basic mode) + // Manual Site ID input (advanced mode) + { + id: 'manualSiteId', + title: 'Site ID', + type: 'short-input', + layout: 'full', + placeholder: 'Enter site ID (leave empty for root site)', + mode: 'advanced', + condition: { field: 'operation', value: 'list_sites' }, + }, + { + id: 'query', + title: 'Search Query', + type: 'short-input', + layout: 'full', + placeholder: 'Search for specific pages (e.g., name contains "report")', + condition: { field: 'operation', value: 'list_pages' }, + }, + ], + tools: { + access: ['sharepoint_create_page', 'sharepoint_read_page', 'sharepoint_list_sites'], + config: { + tool: (params) => { + switch (params.operation) { + case 'create_page': + return 'sharepoint_create_page' + case 'read_page': + return 'sharepoint_read_page' + case 'list_sites': + return 'sharepoint_list_sites' + default: + throw new Error(`Invalid Sharepoint operation: ${params.operation}`) + } + }, + params: (params) => { + const { credential, siteSelector, manualSiteId, mimeType, ...rest } = params + + // Use siteSelector if provided, otherwise use manualSiteId + const effectiveSiteId = (siteSelector || manualSiteId || '').trim() + + return { + accessToken: credential, + siteId: effectiveSiteId, + pageSize: rest.pageSize ? Number.parseInt(rest.pageSize as string, 10) : undefined, + mimeType: mimeType, + ...rest, + } + }, + }, + }, + inputs: { + operation: { type: 'string', required: true }, + credential: { type: 'string', required: true }, + // Create Page operation inputs + pageName: { type: 'string', required: false }, + pageContent: { type: 'string', required: false }, + pageTitle: { type: 'string', required: false }, + // Get Content operation inputs + // fileId: { type: 'string', required: false }, + // List operation inputs + siteSelector: { type: 'string', required: false }, + manualSiteId: { type: 'string', required: false }, + query: { type: 'string', required: false }, + pageSize: { type: 'number', required: false }, + }, + outputs: { + page: 'json', + content: 'json', + sites: 'json', + }, +} diff --git a/apps/sim/blocks/registry.ts b/apps/sim/blocks/registry.ts index 655cfcdff7c..418391bbbfa 100644 --- a/apps/sim/blocks/registry.ts +++ b/apps/sim/blocks/registry.ts @@ -38,6 +38,7 @@ import { MemoryBlock } from '@/blocks/blocks/memory' import { MicrosoftExcelBlock } from '@/blocks/blocks/microsoft_excel' import { MicrosoftTeamsBlock } from '@/blocks/blocks/microsoft_teams' import { MistralParseBlock } from '@/blocks/blocks/mistral_parse' +import { SharepointBlock } from '@/blocks/blocks/sharepoint' import { NotionBlock } from '@/blocks/blocks/notion' import { OpenAIBlock } from '@/blocks/blocks/openai' import { OutlookBlock } from '@/blocks/blocks/outlook' @@ -107,6 +108,7 @@ export const registry: Record = { mem0: Mem0Block, microsoft_excel: MicrosoftExcelBlock, microsoft_teams: MicrosoftTeamsBlock, + sharepoint: SharepointBlock, mistral_parse: MistralParseBlock, notion: NotionBlock, openai: OpenAIBlock, diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx index b559dd04365..16ae2393ef0 100644 --- a/apps/sim/components/icons.tsx +++ b/apps/sim/components/icons.tsx @@ -3198,3 +3198,24 @@ export function MicrosoftOneDriveIcon(props: SVGProps) { ) } + +export function MicrosoftSharepointIcon(props: SVGProps) { + return ( + + + + + + + + + + + + ) +} \ No newline at end of file diff --git a/apps/sim/lib/auth.ts b/apps/sim/lib/auth.ts index 67bdf36a201..048f6712c3e 100644 --- a/apps/sim/lib/auth.ts +++ b/apps/sim/lib/auth.ts @@ -483,6 +483,22 @@ export const auth = betterAuth({ redirectURI: `${env.NEXT_PUBLIC_APP_URL}/api/auth/oauth2/callback/onedrive`, }, + { + providerId: 'sharepoint', + clientId: env.MICROSOFT_CLIENT_ID as string, + clientSecret: env.MICROSOFT_CLIENT_SECRET as string, + authorizationUrl: 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize', + tokenUrl: 'https://login.microsoftonline.com/common/oauth2/v2.0/token', + userInfoUrl: 'https://graph.microsoft.com/v1.0/me', + scopes: ['openid', 'profile', 'email', 'Sites.Read.All', 'Sites.ReadWrite.All', 'offline_access'], + responseType: 'code', + accessType: 'offline', + authentication: 'basic', + prompt: 'consent', + pkce: true, + redirectURI: `${env.NEXT_PUBLIC_APP_URL}/api/auth/oauth2/callback/sharepoint`, + }, + { providerId: 'wealthbox', clientId: env.WEALTHBOX_CLIENT_ID as string, diff --git a/apps/sim/lib/oauth/oauth.ts b/apps/sim/lib/oauth/oauth.ts index 4a8629ceb12..1b7def5c053 100644 --- a/apps/sim/lib/oauth/oauth.ts +++ b/apps/sim/lib/oauth/oauth.ts @@ -14,6 +14,7 @@ import { LinearIcon, MicrosoftExcelIcon, MicrosoftIcon, + MicrosoftSharepointIcon, MicrosoftTeamsIcon, MicrosoftOneDriveIcon, NotionIcon, @@ -63,6 +64,7 @@ export type OAuthService = | 'discord' | 'microsoft-excel' | 'microsoft-teams' + | 'sharepoint' | 'outlook' | 'linear' | 'slack' @@ -211,6 +213,15 @@ export const OAUTH_PROVIDERS: Record = { baseProviderIcon: (props) => MicrosoftIcon(props), scopes: ['openid', 'profile', 'email', 'Files.Read', 'Files.ReadWrite', 'offline_access'], }, + 'sharepoint': { + id: 'sharepoint', + name: 'SharePoint', + description: 'Connect to SharePoint and manage sites.', + providerId: 'sharepoint', + icon: (props) => MicrosoftSharepointIcon(props), + baseProviderIcon: (props) => MicrosoftIcon(props), + scopes: ['openid', 'profile', 'email', 'Sites.Read.All', 'Sites.ReadWrite.All', 'offline_access'], + }, }, defaultService: 'microsoft', }, @@ -482,6 +493,8 @@ export function getServiceIdFromScopes(provider: OAuthProvider, scopes: string[] return 'microsoft-teams' } else if (provider === 'outlook') { return 'outlook' + } else if (provider === 'sharepoint') { + return 'sharepoint' } else if (provider === 'github') { return 'github' } else if (provider === 'supabase') { @@ -558,6 +571,11 @@ export function parseProvider(provider: OAuthProvider): ProviderConfig { baseProvider: 'microsoft', featureType: 'onedrive', } + } else if (provider === 'sharepoint') { + return { + baseProvider: 'microsoft', + featureType: 'sharepoint', + } } // Handle compound providers (e.g., 'google-email' -> { baseProvider: 'google', featureType: 'email' }) @@ -740,6 +758,18 @@ function getProviderAuthConfig(provider: string): ProviderAuthConfig { useBasicAuth: false, } } + case 'sharepoint': { + const { clientId, clientSecret } = getCredentials( + env.MICROSOFT_CLIENT_ID, + env.MICROSOFT_CLIENT_SECRET + ) + return { + tokenEndpoint: 'https://login.microsoftonline.com/common/oauth2/v2.0/token', + clientId, + clientSecret, + useBasicAuth: false, + } + } case 'linear': { const { clientId, clientSecret } = getCredentials( env.LINEAR_CLIENT_ID, diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 02449bf32e4..75cd12e67f4 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -96,6 +96,7 @@ import { notionSearchTool, notionWriteTool, } from '@/tools/notion' +import { sharepointCreatePageTool, sharepointReadPageTool, sharepointListSitesTool } from '@/tools/sharepoint' import { imageTool, embeddingsTool as openAIEmbeddings } from '@/tools/openai' import { outlookDraftTool, outlookReadTool, outlookSendTool } from '@/tools/outlook' import { perplexityChatTool } from '@/tools/perplexity' @@ -297,4 +298,7 @@ export const tools: Record = { hunter_email_verifier: hunterEmailVerifierTool, hunter_companies_find: hunterCompaniesFindTool, hunter_email_count: hunterEmailCountTool, + sharepoint_create_page: sharepointCreatePageTool, + sharepoint_read_page: sharepointReadPageTool, + sharepoint_list_sites: sharepointListSitesTool, } diff --git a/apps/sim/tools/sharepoint/create_page.ts b/apps/sim/tools/sharepoint/create_page.ts new file mode 100644 index 00000000000..770c7d0b268 --- /dev/null +++ b/apps/sim/tools/sharepoint/create_page.ts @@ -0,0 +1,152 @@ +import type { SharepointCreatePageResponse, SharepointToolParams } from '@/tools/sharepoint/types' +import type { ToolConfig } from '@/tools/types' +import { createLogger } from '@/lib/logs/console/logger' + +const logger = createLogger('SharePointCreatePage') + +export const createPageTool: ToolConfig = { + id: 'sharepoint_create_page', + name: 'Create SharePoint Page', + description: 'Create a new page in a SharePoint site', + version: '1.0', + oauth: { + required: true, + provider: 'sharepoint', + additionalScopes: ['openid', 'profile', 'email', 'Sites.ReadWrite.All', 'offline_access'], + }, + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the SharePoint API', + }, + siteId: { + type: 'string', + required: false, + visibility: 'hidden', + description: 'The ID of the SharePoint site (internal use)', + }, + siteSelector: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Select the SharePoint site', + }, + pageName: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The name of the page to create', + }, + pageTitle: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'The title of the page (defaults to page name if not provided)', + }, + pageContent: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'The content of the page', + }, + }, + request: { + url: (params) => { + // Use specific site if provided, otherwise use root site + const siteId = params.siteSelector || params.siteId || 'root' + return `https://graph.microsoft.com/v1.0/sites/${siteId}/pages` + }, + method: 'POST', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + Accept: 'application/json', + }), + body: (params) => { + if (!params.pageName) { + throw new Error('Page name is required') + } + + const pageTitle = params.pageTitle || params.pageName + + // Basic page structure required by Microsoft Graph API + const pageData: any = { + '@odata.type': '#microsoft.graph.sitePage', + name: params.pageName, + title: pageTitle, + publishingState: { + level: 'draft' + }, + pageLayout: 'article' + } + + // Add content if provided using the simple innerHtml approach from the documentation + if (params.pageContent) { + pageData.canvasLayout = { + horizontalSections: [ + { + layout: "oneColumn", + id: "1", + emphasis: "none", + columns: [ + { + id: "1", + width: 12, + webparts: [ + { + id: "6f9230af-2a98-4952-b205-9ede4f9ef548", + innerHtml: `

${params.pageContent.replace(/"/g, '"').replace(/'/g, ''')}

` + } + ] + } + ] + } + ] + } + } + + return pageData + }, + }, + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + logger.error('SharePoint page creation failed', { + status: response.status, + statusText: response.statusText, + error: data.error, + data + }) + throw new Error(data.error?.message || `Failed to create SharePoint page: ${response.status} ${response.statusText}`) + } + + logger.info('SharePoint page created successfully', { + pageId: data.id, + pageName: data.name, + pageTitle: data.title + }) + + return { + success: true, + output: { + page: { + id: data.id, + name: data.name, + title: data.title || data.name, + webUrl: data.webUrl, + pageLayout: data.pageLayout, + promotionKind: data.promotionKind, + createdDateTime: data.createdDateTime, + lastModifiedDateTime: data.lastModifiedDateTime, + contentType: data.contentType, + }, + }, + } + }, + transformError: (error) => { + return error.message || 'An error occurred while creating the SharePoint page' + }, +} diff --git a/apps/sim/tools/sharepoint/index.ts b/apps/sim/tools/sharepoint/index.ts new file mode 100644 index 00000000000..58f432dd0e7 --- /dev/null +++ b/apps/sim/tools/sharepoint/index.ts @@ -0,0 +1,7 @@ +import { createPageTool } from '@/tools/sharepoint/create_page' +import { listSitesTool } from '@/tools/sharepoint/read_site' +import { readPageTool } from '@/tools/sharepoint/read_page' + +export const sharepointCreatePageTool = createPageTool +export const sharepointListSitesTool = listSitesTool +export const sharepointReadPageTool = readPageTool \ No newline at end of file diff --git a/apps/sim/tools/sharepoint/read_page.ts b/apps/sim/tools/sharepoint/read_page.ts new file mode 100644 index 00000000000..590a3c3d5d8 --- /dev/null +++ b/apps/sim/tools/sharepoint/read_page.ts @@ -0,0 +1,156 @@ +import type { SharepointReadPageResponse, SharepointToolParams } from '@/tools/sharepoint/types' +import type { ToolConfig } from '@/tools/types' + +export const readPageTool: ToolConfig = { + id: 'sharepoint_read_page', + name: 'Read SharePoint Page', + description: 'Read a specific page from a SharePoint site', + version: '1.0', + oauth: { + required: true, + provider: 'sharepoint', + additionalScopes: ['openid', 'profile', 'email', 'Sites.Read.All', 'offline_access'], + }, + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the SharePoint API', + }, + siteSelector: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Select the SharePoint site', + }, + siteId: { + type: 'string', + required: false, + visibility: 'hidden', + description: 'The ID of the SharePoint site (internal use)', + }, + pageId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the page to read', + }, + pageName: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'The name of the page to read (alternative to pageId)', + }, + }, + request: { + url: (params) => { + // Use specific site if provided, otherwise use root site + const siteId = params.siteId || params.siteSelector || 'root' + + let baseUrl: string + if (params.pageId) { + // Read specific page by ID + baseUrl = `https://graph.microsoft.com/v1.0/sites/${siteId}/pages/${params.pageId}` + } else if (params.pageName) { + // Search for page by name - we'll need to list pages and filter + baseUrl = `https://graph.microsoft.com/v1.0/sites/${siteId}/pages/microsoft.graph.sitePage` + } else { + throw new Error('Either pageId or pageName must be provided') + } + + const url = new URL(baseUrl) + + // Use Microsoft Graph $select parameter to get page details + url.searchParams.append( + '$select', + 'id,name,title,webUrl,pageLayout,promotionKind,createdDateTime,lastModifiedDateTime,contentType' + ) + + // If searching by name, add filter + if (params.pageName && !params.pageId) { + url.searchParams.append('$filter', `name eq '${params.pageName}'`) + url.searchParams.append('$top', '1') + } + + // Expand content if we're getting a specific page + if (params.pageId) { + url.searchParams.append('$expand', 'canvasLayout') + } + + return url.toString() + }, + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + Accept: 'application/json', + }), + }, + transformResponse: async (response: Response, params) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error?.message || 'Failed to read SharePoint page') + } + + let pageData: any + let contentData: any = { content: '' } + + if (params?.pageId) { + // Direct page access + pageData = data + contentData = { + content: data.canvasLayout ? JSON.stringify(data.canvasLayout, null, 2) : '', + canvasLayout: data.canvasLayout, + } + } else { + // Search result - take first match + if (!data.value || data.value.length === 0) { + throw new Error(`Page with name '${params?.pageName}' not found`) + } + pageData = data.value[0] + + // For search results, we need to make another call to get the content + if (pageData.id) { + const siteId = params?.siteId || params?.siteSelector || 'root' + const contentUrl = `https://graph.microsoft.com/v1.0/sites/${siteId}/pages/${pageData.id}?$expand=canvasLayout` + + const contentResponse = await fetch(contentUrl, { + headers: { + Authorization: `Bearer ${params?.accessToken}`, + Accept: 'application/json', + }, + }) + + if (contentResponse.ok) { + const contentResult = await contentResponse.json() + contentData = { + content: contentResult.canvasLayout ? JSON.stringify(contentResult.canvasLayout, null, 2) : '', + canvasLayout: contentResult.canvasLayout, + } + } + } + } + + return { + success: true, + output: { + page: { + id: pageData.id, + name: pageData.name, + title: pageData.title || pageData.name, + webUrl: pageData.webUrl, + pageLayout: pageData.pageLayout, + promotionKind: pageData.promotionKind, + createdDateTime: pageData.createdDateTime, + lastModifiedDateTime: pageData.lastModifiedDateTime, + contentType: pageData.contentType, + }, + content: contentData, + }, + } + }, + transformError: (error) => { + return error.message || 'An error occurred while reading the SharePoint page' + }, +} diff --git a/apps/sim/tools/sharepoint/read_site.ts b/apps/sim/tools/sharepoint/read_site.ts new file mode 100644 index 00000000000..121c1045148 --- /dev/null +++ b/apps/sim/tools/sharepoint/read_site.ts @@ -0,0 +1,121 @@ +import type { SharepointToolParams } from '@/tools/sharepoint/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface SharepointReadSiteResponse extends ToolResponse { + output: { + site: { + id: string + name: string + displayName: string + webUrl: string + description?: string + createdDateTime?: string + lastModifiedDateTime?: string + isPersonalSite?: boolean + root?: { + serverRelativeUrl: string + } + siteCollection?: { + hostname: string + } + } + } +} + +export const listSitesTool: ToolConfig = { + id: 'sharepoint_list_sites', + name: 'List SharePoint Sites', + description: 'List details of all SharePoint sites', + version: '1.0', + oauth: { + required: true, + provider: 'sharepoint', + additionalScopes: ['openid', 'profile', 'email', 'Sites.Read.All', 'offline_access'], + }, + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the SharePoint API', + }, + siteSelector: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Select the SharePoint site', + }, + siteId: { + type: 'string', + required: false, + visibility: 'hidden', + description: 'The ID of the SharePoint site (internal use)', + }, + groupId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'The group ID for accessing a group team site', + }, + }, + request: { + url: (params) => { + let baseUrl: string + + if (params.groupId) { + // Access group team site + baseUrl = `https://graph.microsoft.com/v1.0/groups/${params.groupId}/sites/root` + } else if (params.siteId || params.siteSelector) { + // Access specific site by ID + const siteId = params.siteId || params.siteSelector + baseUrl = `https://graph.microsoft.com/v1.0/sites/${siteId}` + } else { + // Default to root site + baseUrl = 'https://graph.microsoft.com/v1.0/sites/root' + } + + const url = new URL(baseUrl) + + // Use Microsoft Graph $select parameter to get site details + url.searchParams.append( + '$select', + 'id,name,displayName,webUrl,description,createdDateTime,lastModifiedDateTime,isPersonalSite,root,siteCollection' + ) + + return url.toString() + }, + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + Accept: 'application/json', + }), + }, + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error?.message || 'Failed to read SharePoint site') + } + + return { + success: true, + output: { + site: { + id: data.id, + name: data.name, + displayName: data.displayName, + webUrl: data.webUrl, + description: data.description, + createdDateTime: data.createdDateTime, + lastModifiedDateTime: data.lastModifiedDateTime, + isPersonalSite: data.isPersonalSite, + root: data.root, + siteCollection: data.siteCollection, + }, + }, + } + }, + transformError: (error) => { + return error.message || 'An error occurred while reading the SharePoint site' + }, +} diff --git a/apps/sim/tools/sharepoint/types.ts b/apps/sim/tools/sharepoint/types.ts new file mode 100644 index 00000000000..70925834faf --- /dev/null +++ b/apps/sim/tools/sharepoint/types.ts @@ -0,0 +1,83 @@ +import type { ToolResponse } from '@/tools/types' + +export interface SharepointSite { + id: string + name: string + displayName: string + webUrl: string + description?: string + createdDateTime?: string + lastModifiedDateTime?: string +} + +export interface SharepointPage { + id: string + name: string + title: string + webUrl: string + pageLayout?: string + promotionKind?: string + createdDateTime?: string + lastModifiedDateTime?: string + contentType?: { + id: string + name: string + } +} + +export interface SharepointPageContent { + content: string + canvasLayout?: { + horizontalSections: Array<{ + layout: string + id: string + emphasis: string + webparts: Array<{ + id: string + innerHtml: string + }> + }> + } +} + +export interface SharepointListSitesResponse extends ToolResponse { + output: { + sites: SharepointSite[] + nextPageToken?: string + } +} + +export interface SharepointCreatePageResponse extends ToolResponse { + output: { + page: SharepointPage + } +} + +export interface SharepointReadPageResponse extends ToolResponse { + output: { + page: SharepointPage + content: SharepointPageContent + } +} + +export interface SharepointToolParams { + accessToken: string + siteId?: string + siteSelector?: string + pageId?: string + pageName?: string + pageContent?: string + pageTitle?: string + publishingState?: string + query?: string + pageSize?: number + pageToken?: string + hostname?: string + serverRelativePath?: string + groupId?: string +} + +export type SharepointResponse = + | SharepointListSitesResponse + | SharepointCreatePageResponse + | SharepointReadPageResponse From af6df47ae365fb285b528c6d35f7fcf0d7584580 Mon Sep 17 00:00:00 2001 From: Adam Gough Date: Thu, 31 Jul 2025 18:31:52 -0700 Subject: [PATCH 05/23] finished sharepoint and onedrive --- apps/sim/blocks/blocks/sharepoint.ts | 38 ++--- apps/sim/tools/sharepoint/create_page.ts | 2 - apps/sim/tools/sharepoint/read_page.ts | 185 +++++++++++++++++++++-- apps/sim/tools/sharepoint/read_site.ts | 69 ++++++--- apps/sim/tools/sharepoint/types.ts | 36 ++++- 5 files changed, 264 insertions(+), 66 deletions(-) diff --git a/apps/sim/blocks/blocks/sharepoint.ts b/apps/sim/blocks/blocks/sharepoint.ts index 063cf55359e..793b900a5f4 100644 --- a/apps/sim/blocks/blocks/sharepoint.ts +++ b/apps/sim/blocks/blocks/sharepoint.ts @@ -48,7 +48,7 @@ export const SharepointBlock: BlockConfig = { mimeType: 'application/vnd.microsoft.graph.folder', placeholder: 'Select a site', mode: 'basic', - condition: { field: 'operation', value: ['create_page', 'read_page'] }, + condition: { field: 'operation', value: ['create_page', 'read_page', 'list_sites'] }, }, { @@ -60,6 +60,16 @@ export const SharepointBlock: BlockConfig = { condition: { field: 'operation', value: ['create_page', 'read_page'] }, }, + { + id: 'pageId', + title: 'Page ID', + type: 'short-input', + layout: 'full', + placeholder: 'Page ID (alternative to page name)', + condition: { field: 'operation', value: 'read_page' }, + mode: 'advanced', + }, + { id: 'pageContent', title: 'Page Content', @@ -78,27 +88,7 @@ export const SharepointBlock: BlockConfig = { mode: 'advanced', condition: { field: 'operation', value: 'upload' }, }, - // Manual Folder ID input (advanced mode) - { - id: 'manualSiteId', - title: 'Site ID', - type: 'short-input', - layout: 'full', - placeholder: 'Enter site ID (leave empty for root site)', - mode: 'advanced', - condition: { field: 'operation', value: 'create_page' }, - }, - // List Fields - Site Selector (basic mode) - // Manual Site ID input (advanced mode) - { - id: 'manualSiteId', - title: 'Site ID', - type: 'short-input', - layout: 'full', - placeholder: 'Enter site ID (leave empty for root site)', - mode: 'advanced', - condition: { field: 'operation', value: 'list_sites' }, - }, + { id: 'query', title: 'Search Query', @@ -146,8 +136,8 @@ export const SharepointBlock: BlockConfig = { pageName: { type: 'string', required: false }, pageContent: { type: 'string', required: false }, pageTitle: { type: 'string', required: false }, - // Get Content operation inputs - // fileId: { type: 'string', required: false }, + // Read Page operation inputs + pageId: { type: 'string', required: false }, // List operation inputs siteSelector: { type: 'string', required: false }, manualSiteId: { type: 'string', required: false }, diff --git a/apps/sim/tools/sharepoint/create_page.ts b/apps/sim/tools/sharepoint/create_page.ts index 770c7d0b268..4fad53677b4 100644 --- a/apps/sim/tools/sharepoint/create_page.ts +++ b/apps/sim/tools/sharepoint/create_page.ts @@ -138,10 +138,8 @@ export const createPageTool: ToolConfig]*>/g, '').trim() + if (text) { + textParts.push(text) + logger.info('Extracted text', { text }) + } + } + } + } + } + } else if (section.webparts) { + for (const webpart of section.webparts) { + if (webpart.innerHtml) { + const text = webpart.innerHtml.replace(/<[^>]*>/g, '').trim() + if (text) textParts.push(text) + } + } + } + } + + const finalContent = textParts.join('\n\n') + logger.info('Final extracted content', { + textPartsCount: textParts.length, + finalContentLength: finalContent.length, + finalContent + }) + + return finalContent +} + +// Remove OData metadata from objects +function cleanODataMetadata(obj: any): any { + if (!obj || typeof obj !== 'object') return obj + + if (Array.isArray(obj)) { + return obj.map(item => cleanODataMetadata(item)) + } + + const cleaned: any = {} + for (const [key, value] of Object.entries(obj)) { + // Skip OData metadata keys + if (key.includes('@odata')) continue + + cleaned[key] = cleanODataMetadata(value) + } + + return cleaned +} export const readPageTool: ToolConfig = { id: 'sharepoint_read_page', @@ -32,7 +118,7 @@ export const readPageTool: ToolConfig { + // Validate that at least pageId or pageName is provided + if (!params.pageId && !params.pageName) { + throw new Error('Either pageId or pageName must be provided') + } + // Use specific site if provided, otherwise use root site const siteId = params.siteId || params.siteSelector || 'root' @@ -53,8 +144,8 @@ export const readPageTool: ToolConfig ({ @@ -90,9 +197,23 @@ export const readPageTool: ToolConfig ({ id: p.id, name: p.name, title: p.title })), + selectedPage: data.value[0].name + }) + pageData = data.value[0] // For search results, we need to make another call to get the content if (pageData.id) { const siteId = params?.siteId || params?.siteSelector || 'root' - const contentUrl = `https://graph.microsoft.com/v1.0/sites/${siteId}/pages/${pageData.id}?$expand=canvasLayout` + const contentUrl = `https://graph.microsoft.com/v1.0/sites/${siteId}/pages/${pageData.id}/microsoft.graph.sitePage?$expand=canvasLayout` + + logger.info('Making second API call to get page content', { + pageId: pageData.id, + contentUrl, + siteId + }) const contentResponse = await fetch(contentUrl, { headers: { @@ -122,12 +261,30 @@ export const readPageTool: ToolConfig } } @@ -70,8 +79,8 @@ export const listSitesTool: ToolConfig { + transformResponse: async (response: Response, params) => { const data = await response.json() if (!response.ok) { - throw new Error(data.error?.message || 'Failed to read SharePoint site') + throw new Error(data.error?.message || 'Failed to read SharePoint site(s)') } - return { - success: true, - output: { - site: { - id: data.id, - name: data.name, - displayName: data.displayName, - webUrl: data.webUrl, - description: data.description, - createdDateTime: data.createdDateTime, - lastModifiedDateTime: data.lastModifiedDateTime, - isPersonalSite: data.isPersonalSite, - root: data.root, - siteCollection: data.siteCollection, + // Check if this is a search result (multiple sites) or single site + if (data.value && Array.isArray(data.value)) { + // Multiple sites from search + return { + success: true, + output: { + sites: data.value.map((site: any) => ({ + id: site.id, + name: site.name, + displayName: site.displayName, + webUrl: site.webUrl, + description: site.description, + createdDateTime: site.createdDateTime, + lastModifiedDateTime: site.lastModifiedDateTime, + })) }, - }, + } + } else { + // Single site response + return { + success: true, + output: { + site: { + id: data.id, + name: data.name, + displayName: data.displayName, + webUrl: data.webUrl, + description: data.description, + createdDateTime: data.createdDateTime, + lastModifiedDateTime: data.lastModifiedDateTime, + isPersonalSite: data.isPersonalSite, + root: data.root, + siteCollection: data.siteCollection, + }, + }, + } } }, transformError: (error) => { diff --git a/apps/sim/tools/sharepoint/types.ts b/apps/sim/tools/sharepoint/types.ts index 70925834faf..910369f3955 100644 --- a/apps/sim/tools/sharepoint/types.ts +++ b/apps/sim/tools/sharepoint/types.ts @@ -16,13 +16,8 @@ export interface SharepointPage { title: string webUrl: string pageLayout?: string - promotionKind?: string createdDateTime?: string lastModifiedDateTime?: string - contentType?: { - id: string - name: string - } } export interface SharepointPageContent { @@ -60,6 +55,36 @@ export interface SharepointReadPageResponse extends ToolResponse { } } +export interface SharepointReadSiteResponse extends ToolResponse { + output: { + site?: { + id: string + name: string + displayName: string + webUrl: string + description?: string + createdDateTime?: string + lastModifiedDateTime?: string + isPersonalSite?: boolean + root?: { + serverRelativeUrl: string + } + siteCollection?: { + hostname: string + } + } + sites?: Array<{ + id: string + name: string + displayName: string + webUrl: string + description?: string + createdDateTime?: string + lastModifiedDateTime?: string + }> + } +} + export interface SharepointToolParams { accessToken: string siteId?: string @@ -81,3 +106,4 @@ export type SharepointResponse = | SharepointListSitesResponse | SharepointCreatePageResponse | SharepointReadPageResponse + | SharepointReadSiteResponse From 005c04f399f9cfa8c5551ee1ee019f7db4c28c17 Mon Sep 17 00:00:00 2001 From: Adam Gough Date: Thu, 31 Jul 2025 21:45:51 -0700 Subject: [PATCH 06/23] planner working --- apps/sim/blocks/blocks/microsoft_planner.ts | 242 ++++++++++++++++++ apps/sim/blocks/registry.ts | 2 + apps/sim/components/icons.tsx | 45 ++++ apps/sim/lib/auth.ts | 15 ++ apps/sim/lib/oauth/oauth.ts | 13 + .../tools/microsoft_planner/create_task.ts | 212 +++++++++++++++ apps/sim/tools/microsoft_planner/index.ts | 5 + apps/sim/tools/microsoft_planner/read_task.ts | 140 ++++++++++ apps/sim/tools/microsoft_planner/types.ts | 77 ++++++ apps/sim/tools/registry.ts | 6 + 10 files changed, 757 insertions(+) create mode 100644 apps/sim/blocks/blocks/microsoft_planner.ts create mode 100644 apps/sim/tools/microsoft_planner/create_task.ts create mode 100644 apps/sim/tools/microsoft_planner/index.ts create mode 100644 apps/sim/tools/microsoft_planner/read_task.ts create mode 100644 apps/sim/tools/microsoft_planner/types.ts diff --git a/apps/sim/blocks/blocks/microsoft_planner.ts b/apps/sim/blocks/blocks/microsoft_planner.ts new file mode 100644 index 00000000000..fdc06f19287 --- /dev/null +++ b/apps/sim/blocks/blocks/microsoft_planner.ts @@ -0,0 +1,242 @@ +import { MicrosoftPlannerIcon } from '@/components/icons' +import type { BlockConfig } from '@/blocks/types' +import type { MicrosoftPlannerResponse } from '@/tools/microsoft_planner/types' + +export const MicrosoftPlannerBlock: BlockConfig = { + type: 'microsoft_planner', + name: 'Microsoft Planner', + description: 'Read and create tasks in Microsoft Planner', + longDescription: + 'Integrate Microsoft Planner functionality to manage tasks. Read all user tasks, tasks from specific plans, individual tasks, or create new tasks with various properties like title, description, due date, and assignees using OAuth authentication.', + docsLink: 'https://docs.sim.ai/tools/microsoft_planner', + category: 'tools', + bgColor: '#E0E0E0', + icon: MicrosoftPlannerIcon, + subBlocks: [ + { + id: 'operation', + title: 'Operation', + type: 'dropdown', + layout: 'full', + options: [ + { label: 'Read Task', id: 'read_task' }, + { label: 'Create Task', id: 'create_task' }, + ], + }, + { + id: 'credential', + title: 'Microsoft Account', + type: 'oauth-input', + layout: 'full', + provider: 'microsoft-planner', + serviceId: 'microsoft-planner', + requiredScopes: ['openid', 'profile', 'email', 'Group.ReadWrite.All','Group.Read.All', 'Tasks.ReadWrite', 'offline_access'], + placeholder: 'Select Microsoft account', + }, + { + id: 'planId', + title: 'Plan ID', + type: 'short-input', + layout: 'full', + placeholder: 'Enter the plan ID', + condition: { field: 'operation', value: ['create_task'] }, + }, + { + id: 'taskId', + title: 'Task ID', + type: 'short-input', + layout: 'full', + placeholder: 'Enter the task ID', + condition: { field: 'operation', value: ['read_task'] }, + }, + { + id: 'title', + title: 'Task Title', + type: 'short-input', + layout: 'full', + placeholder: 'Enter the task title', + condition: { field: 'operation', value: ['create_task'] }, + }, + { + id: 'description', + title: 'Description', + type: 'long-input', + layout: 'full', + placeholder: 'Enter task description (optional)', + condition: { field: 'operation', value: ['create_task'] }, + }, + { + id: 'dueDateTime', + title: 'Due Date', + type: 'short-input', + layout: 'full', + placeholder: 'Enter due date in ISO 8601 format (e.g., 2024-12-31T23:59:59Z)', + condition: { field: 'operation', value: ['create_task'] }, + }, + { + id: 'assigneeUserId', + title: 'Assignee User ID', + type: 'short-input', + layout: 'full', + placeholder: 'Enter the user ID to assign this task to (optional)', + condition: { field: 'operation', value: ['create_task'] }, + }, + { + id: 'bucketId', + title: 'Bucket ID', + type: 'short-input', + layout: 'full', + placeholder: 'Enter the bucket ID to organize the task (optional)', + condition: { field: 'operation', value: ['create_task'] }, + }, + { + id: 'priority', + title: 'Priority', + type: 'dropdown', + layout: 'full', + options: [ + { label: 'Urgent (0)', id: '0' }, + { label: 'Important (1)', id: '1' }, + { label: 'Medium (2)', id: '2' }, + { label: 'Low (3)', id: '3' }, + { label: 'Later (4)', id: '4' }, + { label: 'Lowest (5)', id: '5' }, + ], + condition: { field: 'operation', value: ['create_task'] }, + }, + { + id: 'percentComplete', + title: 'Completion %', + type: 'short-input', + layout: 'full', + placeholder: 'Enter completion percentage (0-100)', + condition: { field: 'operation', value: ['create_task'] }, + }, + ], + tools: { + access: ['microsoft_planner_read_task', 'microsoft_planner_create_task'], + config: { + tool: (params) => { + switch (params.operation) { + case 'read_task': + return 'microsoft_planner_read_task' + case 'create_task': + return 'microsoft_planner_create_task' + default: + throw new Error(`Invalid Microsoft Planner operation: ${params.operation}`) + } + }, + params: (params) => { + const { + credential, + operation, + planId, + taskId, + title, + description, + dueDateTime, + assigneeUserId, + bucketId, + priority, + percentComplete, + ...rest + } = params + + const baseParams = { + ...rest, + credential, + } + + // For read operations + if (operation === 'read_task') { + // No additional parameters needed - will get all user tasks + return baseParams + } + + if (operation === 'read_task') { + if (!planId?.trim()) { + throw new Error('Plan ID is required to read tasks from a specific plan.') + } + return { + ...baseParams, + planId: planId.trim(), + } + } + + if (operation === 'read_task') { + if (!taskId?.trim()) { + throw new Error('Task ID is required to read a specific task.') + } + return { + ...baseParams, + taskId: taskId.trim(), + } + } + + // For create operation + if (operation === 'create_task') { + if (!planId?.trim()) { + throw new Error('Plan ID is required to create a task.') + } + if (!title?.trim()) { + throw new Error('Task title is required to create a task.') + } + + const createParams: any = { + ...baseParams, + planId: planId.trim(), + title: title.trim(), + } + + if (description?.trim()) { + createParams.description = description.trim() + } + + if (dueDateTime?.trim()) { + createParams.dueDateTime = dueDateTime.trim() + } + + if (assigneeUserId?.trim()) { + createParams.assigneeUserId = assigneeUserId.trim() + } + + if (bucketId?.trim()) { + createParams.bucketId = bucketId.trim() + } + + if (priority !== undefined && priority !== '') { + createParams.priority = parseInt(priority as string, 10) + } + + if (percentComplete?.trim()) { + const percent = parseInt(percentComplete.trim(), 10) + if (percent >= 0 && percent <= 100) { + createParams.percentComplete = percent + } + } + + return createParams + } + + return baseParams + }, + }, + }, + inputs: { + operation: { type: 'string', required: true }, + credential: { type: 'string', required: true }, + planId: { type: 'string', required: false }, + taskId: { type: 'string', required: false }, + title: { type: 'string', required: false }, + description: { type: 'string', required: false }, + dueDateTime: { type: 'string', required: false }, + assigneeUserId: { type: 'string', required: false }, + bucketId: { type: 'string', required: false }, + priority: { type: 'string', required: false }, + percentComplete: { type: 'string', required: false }, + }, + outputs: { + task: 'json', + metadata: 'json', + }, +} diff --git a/apps/sim/blocks/registry.ts b/apps/sim/blocks/registry.ts index 418391bbbfa..914d6682a72 100644 --- a/apps/sim/blocks/registry.ts +++ b/apps/sim/blocks/registry.ts @@ -36,6 +36,7 @@ import { LinkupBlock } from '@/blocks/blocks/linkup' import { Mem0Block } from '@/blocks/blocks/mem0' import { MemoryBlock } from '@/blocks/blocks/memory' import { MicrosoftExcelBlock } from '@/blocks/blocks/microsoft_excel' +import { MicrosoftPlannerBlock } from '@/blocks/blocks/microsoft_planner' import { MicrosoftTeamsBlock } from '@/blocks/blocks/microsoft_teams' import { MistralParseBlock } from '@/blocks/blocks/mistral_parse' import { SharepointBlock } from '@/blocks/blocks/sharepoint' @@ -107,6 +108,7 @@ export const registry: Record = { linkup: LinkupBlock, mem0: Mem0Block, microsoft_excel: MicrosoftExcelBlock, + microsoft_planner: MicrosoftPlannerBlock, microsoft_teams: MicrosoftTeamsBlock, sharepoint: SharepointBlock, mistral_parse: MistralParseBlock, diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx index 16ae2393ef0..8b1bb97ea7b 100644 --- a/apps/sim/components/icons.tsx +++ b/apps/sim/components/icons.tsx @@ -3218,4 +3218,49 @@ export function MicrosoftSharepointIcon(props: SVGProps) { ) +} + +export function MicrosoftPlannerIcon(props: SVGProps) { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ) } \ No newline at end of file diff --git a/apps/sim/lib/auth.ts b/apps/sim/lib/auth.ts index 048f6712c3e..42980f609c3 100644 --- a/apps/sim/lib/auth.ts +++ b/apps/sim/lib/auth.ts @@ -441,6 +441,21 @@ export const auth = betterAuth({ pkce: true, redirectURI: `${env.NEXT_PUBLIC_APP_URL}/api/auth/oauth2/callback/microsoft-excel`, }, + { + providerId: 'microsoft-planner', + clientId: env.MICROSOFT_CLIENT_ID as string, + clientSecret: env.MICROSOFT_CLIENT_SECRET as string, + authorizationUrl: 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize', + tokenUrl: 'https://login.microsoftonline.com/common/oauth2/v2.0/token', + userInfoUrl: 'https://graph.microsoft.com/v1.0/me', + scopes: ['openid', 'profile', 'email', 'Group.ReadWrite.All', 'Group.Read.All', 'Tasks.ReadWrite', 'offline_access'], + responseType: 'code', + accessType: 'offline', + authentication: 'basic', + prompt: 'consent', + pkce: true, + redirectURI: `${env.NEXT_PUBLIC_APP_URL}/api/auth/oauth2/callback/microsoft-planner`, + }, { providerId: 'outlook', diff --git a/apps/sim/lib/oauth/oauth.ts b/apps/sim/lib/oauth/oauth.ts index 1b7def5c053..dca5cb9bca3 100644 --- a/apps/sim/lib/oauth/oauth.ts +++ b/apps/sim/lib/oauth/oauth.ts @@ -24,6 +24,7 @@ import { SupabaseIcon, WealthboxIcon, xIcon, + MicrosoftPlannerIcon, } from '@/components/icons' import { env } from '@/lib/env' import { createLogger } from '@/lib/logs/console/logger' @@ -64,6 +65,7 @@ export type OAuthService = | 'discord' | 'microsoft-excel' | 'microsoft-teams' + | 'microsoft-planner' | 'sharepoint' | 'outlook' | 'linear' @@ -162,6 +164,15 @@ export const OAUTH_PROVIDERS: Record = { baseProviderIcon: (props) => MicrosoftIcon(props), scopes: ['openid', 'profile', 'email', 'Files.Read', 'Files.ReadWrite', 'offline_access'], }, + 'microsoft-planner': { + id: 'microsoft-planner', + name: 'Microsoft Planner', + description: 'Connect to Microsoft Planner and manage tasks.', + providerId: 'microsoft-planner', + icon: (props) => MicrosoftPlannerIcon(props), + baseProviderIcon: (props) => MicrosoftIcon(props), + scopes: ['openid', 'profile', 'email', 'Group.ReadWrite.All','Group.Read.All', 'Tasks.ReadWrite', 'offline_access'], + }, 'microsoft-teams': { id: 'microsoft-teams', name: 'Microsoft Teams', @@ -495,6 +506,8 @@ export function getServiceIdFromScopes(provider: OAuthProvider, scopes: string[] return 'outlook' } else if (provider === 'sharepoint') { return 'sharepoint' + } else if (provider === 'microsoft-planner') { + return 'microsoft-planner' } else if (provider === 'github') { return 'github' } else if (provider === 'supabase') { diff --git a/apps/sim/tools/microsoft_planner/create_task.ts b/apps/sim/tools/microsoft_planner/create_task.ts new file mode 100644 index 00000000000..e9c648ff040 --- /dev/null +++ b/apps/sim/tools/microsoft_planner/create_task.ts @@ -0,0 +1,212 @@ +import { createLogger } from '@/lib/logs/console/logger' +import type { + MicrosoftPlannerCreateResponse, + MicrosoftPlannerToolParams, +} from '@/tools/microsoft_planner/types' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('MicrosoftPlannerCreateTask') + +export const createTaskTool: ToolConfig = { + id: 'microsoft_planner_create_task', + name: 'Create Microsoft Planner Task', + description: 'Create a new task in Microsoft Planner', + version: '1.0', + oauth: { + required: true, + provider: 'microsoft-planner', + additionalScopes: [], + }, + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Microsoft Planner API', + }, + planId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the plan where the task will be created', + }, + title: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The title of the task', + }, + description: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'The description of the task', + }, + dueDateTime: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'The due date and time for the task (ISO 8601 format)', + }, + assigneeUserId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'The user ID to assign the task to', + }, + bucketId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'The bucket ID to place the task in', + }, + priority: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'The priority of the task (0-10, where 0 is highest priority)', + }, + percentComplete: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'The completion percentage of the task (0-100)', + }, + }, + request: { + url: () => 'https://graph.microsoft.com/v1.0/planner/tasks', + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Access token is required') + } + + return { + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + } + }, + body: (params) => { + if (!params.planId) { + throw new Error('Plan ID is required') + } + if (!params.title) { + throw new Error('Task title is required') + } + + const body: any = { + planId: params.planId, + title: params.title, + } + + if (params.bucketId) { + body.bucketId = params.bucketId + } + + if (params.dueDateTime) { + body.dueDateTime = params.dueDateTime + } + + if (params.priority !== undefined) { + body.priority = params.priority + } + + if (params.percentComplete !== undefined) { + body.percentComplete = params.percentComplete + } + + if (params.assigneeUserId) { + body.assignments = { + [params.assigneeUserId]: { + '@odata.type': 'microsoft.graph.plannerAssignment', + orderHint: ' !', + }, + } + } + + logger.info('Creating task with body:', body) + return body + }, + }, + transformResponse: async (response: Response, params) => { + if (!response.ok) { + const errorJson = await response.json().catch(() => ({ error: response.statusText })) + const errorText = + errorJson.error && typeof errorJson.error === 'object' + ? errorJson.error.message || JSON.stringify(errorJson.error) + : errorJson.error || response.statusText + throw new Error(`Failed to create Microsoft Planner task: ${errorText}`) + } + + const task = await response.json() + logger.info('Created task:', task) + + // If description was provided, update the task details + if (params?.description && task.id) { + try { + const detailsUrl = `https://graph.microsoft.com/v1.0/planner/tasks/${task.id}/details` + const detailsResponse = await fetch(detailsUrl, { + method: 'PATCH', + headers: { + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + 'If-Match': '*', + }, + body: JSON.stringify({ + description: params.description, + }), + }) + + if (detailsResponse.ok) { + const details = await detailsResponse.json() + task.details = details + } + } catch (error) { + logger.warn('Failed to update task description:', error) + } + } + + const result: MicrosoftPlannerCreateResponse = { + success: true, + output: { + task, + metadata: { + planId: task.planId, + taskId: task.id, + taskUrl: `https://graph.microsoft.com/v1.0/planner/tasks/${task.id}`, + }, + }, + } + + return result + }, + transformError: (error) => { + if (error instanceof Error) { + return error.message + } + + if (typeof error === 'object' && error !== null) { + if (error.error) { + if (typeof error.error === 'string') { + return error.error + } + if (typeof error.error === 'object' && error.error.message) { + return error.error.message + } + return JSON.stringify(error.error) + } + + if (error.message) { + return error.message + } + + try { + return `Microsoft Planner API error: ${JSON.stringify(error)}` + } catch (_e) { + return 'Microsoft Planner API error: Unable to parse error details' + } + } + + return 'An error occurred while creating the Microsoft Planner task' + }, +} diff --git a/apps/sim/tools/microsoft_planner/index.ts b/apps/sim/tools/microsoft_planner/index.ts new file mode 100644 index 00000000000..145034f0941 --- /dev/null +++ b/apps/sim/tools/microsoft_planner/index.ts @@ -0,0 +1,5 @@ +import { createTaskTool } from '@/tools/microsoft_planner/create_task' +import { readTaskTool } from '@/tools/microsoft_planner/read_task' + +export const microsoftPlannerCreateTaskTool = createTaskTool +export const microsoftPlannerReadTaskTool = readTaskTool diff --git a/apps/sim/tools/microsoft_planner/read_task.ts b/apps/sim/tools/microsoft_planner/read_task.ts new file mode 100644 index 00000000000..51e4f2bb788 --- /dev/null +++ b/apps/sim/tools/microsoft_planner/read_task.ts @@ -0,0 +1,140 @@ +import { createLogger } from '@/lib/logs/console/logger' +import type { + MicrosoftPlannerReadResponse, + MicrosoftPlannerToolParams, +} from '@/tools/microsoft_planner/types' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('MicrosoftPlannerReadTask') + +export const readTaskTool: ToolConfig = { + id: 'microsoft_planner_read_task', + name: 'Read Microsoft Planner Tasks', + description: 'Read tasks from Microsoft Planner - get all user tasks, all tasks in a plan, or a specific task', + version: '1.0', + oauth: { + required: true, + provider: 'microsoft-planner', + additionalScopes: [], + }, + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Microsoft Planner API', + }, + planId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'The ID of the plan to get tasks from (if not provided, gets all user tasks)', + }, + taskId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'The ID of a specific task to retrieve', + }, + }, + request: { + url: (params) => { + const finalUrl = params.taskId + ? `https://graph.microsoft.com/v1.0/planner/tasks/${params.taskId}` + : 'https://graph.microsoft.com/v1.0/me/planner/tasks' + + logger.info('Microsoft Planner URL:', finalUrl) + return finalUrl + }, + method: 'GET', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Access token is required') + } + + logger.info('Access token present:', !!params.accessToken) + return { + Authorization: `Bearer ${params.accessToken}`, + } + }, + }, + transformResponse: async (response: Response, params) => { + logger.info('Raw response URL:', response.url) + logger.info('Raw response status:', response.status) + + if (!response.ok) { + const errorJson = await response.json().catch(() => ({ error: response.statusText })) + const errorText = + errorJson.error && typeof errorJson.error === 'object' + ? errorJson.error.message || JSON.stringify(errorJson.error) + : errorJson.error || response.statusText + throw new Error(`Failed to read Microsoft Planner tasks: ${errorText}`) + } + + const data = await response.json() + logger.info('Raw response data:', data) + logger.info('Retrieved task data:', data) + + let result: MicrosoftPlannerReadResponse + + // If we got a specific task + if (params?.taskId) { + result = { + success: true, + output: { + task: data, + metadata: { + taskId: data.id, + planId: data.planId, + taskUrl: `https://graph.microsoft.com/v1.0/planner/tasks/${data.id}`, + }, + }, + } + } else { + // If we got multiple tasks (either from plan or user tasks) + const tasks = data.value || [] + result = { + success: true, + output: { + tasks, + metadata: { + planId: params?.planId, + userId: params?.planId ? undefined : 'me', + planUrl: params?.planId ? `https://graph.microsoft.com/v1.0/planner/plans/${params.planId}` : undefined, + }, + }, + } + } + + return result + }, + transformError: (error) => { + if (error instanceof Error) { + return error.message + } + + if (typeof error === 'object' && error !== null) { + if (error.error) { + if (typeof error.error === 'string') { + return error.error + } + if (typeof error.error === 'object' && error.error.message) { + return error.error.message + } + return JSON.stringify(error.error) + } + + if (error.message) { + return error.message + } + + try { + return `Microsoft Planner API error: ${JSON.stringify(error)}` + } catch (_e) { + return 'Microsoft Planner API error: Unable to parse error details' + } + } + + return 'An error occurred while reading Microsoft Planner tasks' + }, +} diff --git a/apps/sim/tools/microsoft_planner/types.ts b/apps/sim/tools/microsoft_planner/types.ts new file mode 100644 index 00000000000..c8d9afdc6c4 --- /dev/null +++ b/apps/sim/tools/microsoft_planner/types.ts @@ -0,0 +1,77 @@ +import type { ToolResponse } from '@/tools/types' + +export interface PlannerTask { + id?: string + planId: string + title: string + orderHint?: string + assigneePriority?: string + percentComplete?: number + startDateTime?: string + createdDateTime?: string + dueDateTime?: string + hasDescription?: boolean + previewType?: string + completedDateTime?: string + completedBy?: any + referenceCount?: number + checklistItemCount?: number + activeChecklistItemCount?: number + conversationThreadId?: string + priority?: number + assignments?: Record + bucketId?: string + details?: { + description?: string + references?: Record + checklist?: Record + } +} + +export interface PlannerPlan { + id: string + title: string + owner?: string + createdDateTime?: string + container?: any +} + +export interface MicrosoftPlannerMetadata { + planId?: string + taskId?: string + userId?: string + planUrl?: string + taskUrl?: string +} + +export interface MicrosoftPlannerReadResponse extends ToolResponse { + output: { + tasks?: PlannerTask[] + task?: PlannerTask + plan?: PlannerPlan + metadata: MicrosoftPlannerMetadata + } +} + +export interface MicrosoftPlannerCreateResponse extends ToolResponse { + output: { + task: PlannerTask + metadata: MicrosoftPlannerMetadata + } +} + +export interface MicrosoftPlannerToolParams { + accessToken: string + planId?: string + taskId?: string + title?: string + description?: string + dueDateTime?: string + assigneeUserId?: string + bucketId?: string + priority?: number + percentComplete?: number +} + +export type MicrosoftPlannerResponse = MicrosoftPlannerReadResponse | MicrosoftPlannerCreateResponse + diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 75cd12e67f4..0376e7af416 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -79,6 +79,10 @@ import { microsoftExcelTableAddTool, microsoftExcelWriteTool, } from '@/tools/microsoft_excel' +import { + microsoftPlannerCreateTaskTool, + microsoftPlannerReadTaskTool, +} from '@/tools/microsoft_planner' import { onedriveCreateFolderTool, onedriveUploadTool, onedriveListTool } from '@/tools/onedrive' import { microsoftTeamsReadChannelTool, @@ -273,6 +277,8 @@ export const tools: Record = { microsoft_excel_read: microsoftExcelReadTool, microsoft_excel_write: microsoftExcelWriteTool, microsoft_excel_table_add: microsoftExcelTableAddTool, + microsoft_planner_create_task: microsoftPlannerCreateTaskTool, + microsoft_planner_read_task: microsoftPlannerReadTaskTool, google_calendar_create: googleCalendarCreateTool, google_calendar_get: googleCalendarGetTool, google_calendar_list: googleCalendarListTool, From 5f0305dc2e6ebcefaf5ccef531db034c10e1f983 Mon Sep 17 00:00:00 2001 From: Adam Gough Date: Thu, 31 Jul 2025 22:11:09 -0700 Subject: [PATCH 07/23] fixed create task tool --- .../app/api/tools/onedrive/folder/route.ts | 8 +- .../app/api/tools/onedrive/folders/route.ts | 8 +- .../app/api/tools/sharepoint/site/route.ts | 10 +- .../app/api/tools/sharepoint/sites/route.ts | 10 +- .../components/microsoft-file-selector.tsx | 12 +- .../file-selector/file-selector-input.tsx | 1 - apps/sim/blocks/blocks/microsoft_planner.ts | 14 +- apps/sim/blocks/blocks/onedrive.ts | 38 +++- apps/sim/blocks/blocks/sharepoint.ts | 28 ++- apps/sim/blocks/registry.ts | 4 +- apps/sim/components/icons.tsx | 194 +++++++++++++----- apps/sim/lib/auth.ts | 19 +- apps/sim/lib/oauth/oauth.ts | 32 ++- .../tools/microsoft_planner/create_task.ts | 18 +- apps/sim/tools/microsoft_planner/read_task.ts | 13 +- apps/sim/tools/microsoft_planner/types.ts | 1 - apps/sim/tools/onedrive/list.ts | 19 +- apps/sim/tools/onedrive/upload.ts | 15 +- apps/sim/tools/registry.ts | 8 +- apps/sim/tools/sharepoint/create_page.ts | 41 ++-- apps/sim/tools/sharepoint/index.ts | 4 +- apps/sim/tools/sharepoint/read_page.ts | 84 ++++---- apps/sim/tools/sharepoint/read_site.ts | 43 ++-- 23 files changed, 411 insertions(+), 213 deletions(-) diff --git a/apps/sim/app/api/tools/onedrive/folder/route.ts b/apps/sim/app/api/tools/onedrive/folder/route.ts index 17f5f6856ac..d7a518c6fed 100644 --- a/apps/sim/app/api/tools/onedrive/folder/route.ts +++ b/apps/sim/app/api/tools/onedrive/folder/route.ts @@ -1,11 +1,11 @@ -import { NextRequest, NextResponse } from 'next/server' import crypto from 'node:crypto' import { eq } from 'drizzle-orm' +import { type NextRequest, NextResponse } from 'next/server' import { getSession } from '@/lib/auth' -import { db } from '@/db' -import { account } from '@/db/schema' import { createLogger } from '@/lib/logs/console/logger' import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils' +import { db } from '@/db' +import { account } from '@/db/schema' export const dynamic = 'force-dynamic' @@ -80,4 +80,4 @@ export async function GET(request: NextRequest) { logger.error(`[${requestId}] Error fetching folder from OneDrive`, error) return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) } -} \ No newline at end of file +} diff --git a/apps/sim/app/api/tools/onedrive/folders/route.ts b/apps/sim/app/api/tools/onedrive/folders/route.ts index 406ed7d9952..71e98d7b007 100644 --- a/apps/sim/app/api/tools/onedrive/folders/route.ts +++ b/apps/sim/app/api/tools/onedrive/folders/route.ts @@ -1,11 +1,11 @@ -import { NextRequest, NextResponse } from 'next/server' import crypto from 'node:crypto' import { eq } from 'drizzle-orm' +import { type NextRequest, NextResponse } from 'next/server' import { getSession } from '@/lib/auth' -import { db } from '@/db' -import { account } from '@/db/schema' import { createLogger } from '@/lib/logs/console/logger' import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils' +import { db } from '@/db' +import { account } from '@/db/schema' export const dynamic = 'force-dynamic' @@ -48,7 +48,7 @@ export async function GET(request: NextRequest) { // Build URL for OneDrive folders let url = `https://graph.microsoft.com/v1.0/me/drive/root/children?$filter=folder ne null&$select=id,name,folder,webUrl,createdDateTime,lastModifiedDateTime&$top=50` - + if (query) { url += `&$search="${encodeURIComponent(query)}"` } diff --git a/apps/sim/app/api/tools/sharepoint/site/route.ts b/apps/sim/app/api/tools/sharepoint/site/route.ts index 2a6ff702776..c52c9f8cea5 100644 --- a/apps/sim/app/api/tools/sharepoint/site/route.ts +++ b/apps/sim/app/api/tools/sharepoint/site/route.ts @@ -1,11 +1,11 @@ -import { NextRequest, NextResponse } from 'next/server' import crypto from 'node:crypto' import { eq } from 'drizzle-orm' +import { type NextRequest, NextResponse } from 'next/server' import { getSession } from '@/lib/auth' -import { db } from '@/db' -import { account } from '@/db/schema' import { createLogger } from '@/lib/logs/console/logger' import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils' +import { db } from '@/db' +import { account } from '@/db/schema' export const dynamic = 'force-dynamic' @@ -52,7 +52,7 @@ export async function GET(request: NextRequest) { // 3. Hostname: sites/{hostname} // 4. Server-relative URL: sites/{hostname}:/{server-relative-path} // 5. Group team site: groups/{group-id}/sites/root - + let endpoint: string if (fileId === 'root') { endpoint = 'sites/root' @@ -102,4 +102,4 @@ export async function GET(request: NextRequest) { logger.error(`[${requestId}] Error fetching site from SharePoint`, error) return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) } -} \ No newline at end of file +} diff --git a/apps/sim/app/api/tools/sharepoint/sites/route.ts b/apps/sim/app/api/tools/sharepoint/sites/route.ts index a72871aebd1..439c4137de5 100644 --- a/apps/sim/app/api/tools/sharepoint/sites/route.ts +++ b/apps/sim/app/api/tools/sharepoint/sites/route.ts @@ -1,11 +1,11 @@ -import { NextRequest, NextResponse } from 'next/server' import crypto from 'node:crypto' import { eq } from 'drizzle-orm' +import { type NextRequest, NextResponse } from 'next/server' import { getSession } from '@/lib/auth' -import { db } from '@/db' -import { account } from '@/db/schema' import { createLogger } from '@/lib/logs/console/logger' import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils' +import { db } from '@/db' +import { account } from '@/db/schema' export const dynamic = 'force-dynamic' @@ -49,7 +49,7 @@ export async function GET(request: NextRequest) { // Build URL for SharePoint sites // Use search=* to get all sites the user has access to, or search for specific query const searchQuery = query || '*' - let url = `https://graph.microsoft.com/v1.0/sites?search=${encodeURIComponent(searchQuery)}&$select=id,name,displayName,webUrl,createdDateTime,lastModifiedDateTime&$top=50` + const url = `https://graph.microsoft.com/v1.0/sites?search=${encodeURIComponent(searchQuery)}&$select=id,name,displayName,webUrl,createdDateTime,lastModifiedDateTime&$top=50` const response = await fetch(url, { headers: { @@ -81,4 +81,4 @@ export async function GET(request: NextRequest) { logger.error(`[${requestId}] Error fetching sites from SharePoint`, error) return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) } -} \ No newline at end of file +} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/microsoft-file-selector.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/microsoft-file-selector.tsx index e632bea2c7a..ce71f7160c0 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/microsoft-file-selector.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/microsoft-file-selector.tsx @@ -346,15 +346,15 @@ export function MicrosoftFileSelector({ // Handle OneDrive specifically by checking serviceId if (baseProvider === 'microsoft' && serviceId === 'onedrive') { - const onedriveService = baseProviderConfig.services['onedrive'] + const onedriveService = baseProviderConfig.services.onedrive if (onedriveService) { return onedriveService.icon({ className: 'h-4 w-4' }) } } - // Handle SharePoint specifically by checking serviceId + // Handle SharePoint specifically by checking serviceId if (baseProvider === 'microsoft' && serviceId === 'sharepoint') { - const sharepointService = baseProviderConfig.services['sharepoint'] + const sharepointService = baseProviderConfig.services.sharepoint if (sharepointService) { return sharepointService.icon({ className: 'h-4 w-4' }) } @@ -449,18 +449,18 @@ export function MicrosoftFileSelector({ if (serviceId === 'onedrive') { return { title: 'No folders found.', - description: 'No folders were found in your OneDrive.' + description: 'No folders were found in your OneDrive.', } } if (serviceId === 'sharepoint') { return { title: 'No sites found.', - description: 'No SharePoint sites were found.' + description: 'No SharePoint sites were found.', } } return { title: 'No Excel files found.', - description: 'No .xlsx files were found in your OneDrive.' + description: 'No .xlsx files were found in your OneDrive.', } } diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/file-selector-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/file-selector-input.tsx index 9c919530b6c..033085789b9 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/file-selector-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/file-selector-input.tsx @@ -552,4 +552,3 @@ export function FileSelectorInput({ /> ) } - diff --git a/apps/sim/blocks/blocks/microsoft_planner.ts b/apps/sim/blocks/blocks/microsoft_planner.ts index fdc06f19287..e49a86f473b 100644 --- a/apps/sim/blocks/blocks/microsoft_planner.ts +++ b/apps/sim/blocks/blocks/microsoft_planner.ts @@ -30,7 +30,15 @@ export const MicrosoftPlannerBlock: BlockConfig = { layout: 'full', provider: 'microsoft-planner', serviceId: 'microsoft-planner', - requiredScopes: ['openid', 'profile', 'email', 'Group.ReadWrite.All','Group.Read.All', 'Tasks.ReadWrite', 'offline_access'], + requiredScopes: [ + 'openid', + 'profile', + 'email', + 'Group.ReadWrite.All', + 'Group.Read.All', + 'Tasks.ReadWrite', + 'offline_access', + ], placeholder: 'Select Microsoft account', }, { @@ -205,11 +213,11 @@ export const MicrosoftPlannerBlock: BlockConfig = { } if (priority !== undefined && priority !== '') { - createParams.priority = parseInt(priority as string, 10) + createParams.priority = Number.parseInt(priority as string, 10) } if (percentComplete?.trim()) { - const percent = parseInt(percentComplete.trim(), 10) + const percent = Number.parseInt(percentComplete.trim(), 10) if (percent >= 0 && percent <= 100) { createParams.percentComplete = percent } diff --git a/apps/sim/blocks/blocks/onedrive.ts b/apps/sim/blocks/blocks/onedrive.ts index 5db1e834622..df359441fca 100644 --- a/apps/sim/blocks/blocks/onedrive.ts +++ b/apps/sim/blocks/blocks/onedrive.ts @@ -34,7 +34,14 @@ export const OneDriveBlock: BlockConfig = { layout: 'full', provider: 'onedrive', serviceId: 'onedrive', - requiredScopes: ['openid', 'profile', 'email','Files.Read', 'Files.ReadWrite', 'offline_access'], + requiredScopes: [ + 'openid', + 'profile', + 'email', + 'Files.Read', + 'Files.ReadWrite', + 'offline_access', + ], placeholder: 'Select Microsoft account', }, // Upload Fields @@ -54,7 +61,7 @@ export const OneDriveBlock: BlockConfig = { placeholder: 'Content to upload to the file', condition: { field: 'operation', value: 'upload' }, }, - + { id: 'folderSelector', title: 'Select Parent Folder', @@ -62,7 +69,14 @@ export const OneDriveBlock: BlockConfig = { layout: 'full', provider: 'microsoft', serviceId: 'onedrive', - requiredScopes: ['openid', 'profile', 'email', 'Files.Read', 'Files.ReadWrite', 'offline_access'], + requiredScopes: [ + 'openid', + 'profile', + 'email', + 'Files.Read', + 'Files.ReadWrite', + 'offline_access', + ], mimeType: 'application/vnd.microsoft.graph.folder', placeholder: 'Select a parent folder', mode: 'basic', @@ -92,7 +106,14 @@ export const OneDriveBlock: BlockConfig = { layout: 'full', provider: 'microsoft', serviceId: 'onedrive', - requiredScopes: ['openid', 'profile', 'email', 'Files.Read', 'Files.ReadWrite', 'offline_access'], + requiredScopes: [ + 'openid', + 'profile', + 'email', + 'Files.Read', + 'Files.ReadWrite', + 'offline_access', + ], mimeType: 'application/vnd.microsoft.graph.folder', placeholder: 'Select a parent folder', mode: 'basic', @@ -116,7 +137,14 @@ export const OneDriveBlock: BlockConfig = { layout: 'full', provider: 'microsoft', serviceId: 'onedrive', - requiredScopes: ['openid', 'profile', 'email', 'Files.Read', 'Files.ReadWrite', 'offline_access'], + requiredScopes: [ + 'openid', + 'profile', + 'email', + 'Files.Read', + 'Files.ReadWrite', + 'offline_access', + ], mimeType: 'application/vnd.microsoft.graph.folder', placeholder: 'Select a folder to list files from', mode: 'basic', diff --git a/apps/sim/blocks/blocks/sharepoint.ts b/apps/sim/blocks/blocks/sharepoint.ts index 793b900a5f4..ef7ea443ba5 100644 --- a/apps/sim/blocks/blocks/sharepoint.ts +++ b/apps/sim/blocks/blocks/sharepoint.ts @@ -33,31 +33,45 @@ export const SharepointBlock: BlockConfig = { layout: 'full', provider: 'sharepoint', serviceId: 'sharepoint', - requiredScopes: ['openid', 'profile', 'email','Files.Read', 'Files.ReadWrite', 'offline_access'], + requiredScopes: [ + 'openid', + 'profile', + 'email', + 'Files.Read', + 'Files.ReadWrite', + 'offline_access', + ], placeholder: 'Select Microsoft account', }, - - { + + { id: 'siteSelector', title: 'Select Site', type: 'file-selector', layout: 'full', provider: 'microsoft', serviceId: 'sharepoint', - requiredScopes: ['openid', 'profile', 'email', 'Files.Read', 'Files.ReadWrite', 'offline_access'], + requiredScopes: [ + 'openid', + 'profile', + 'email', + 'Files.Read', + 'Files.ReadWrite', + 'offline_access', + ], mimeType: 'application/vnd.microsoft.graph.folder', placeholder: 'Select a site', mode: 'basic', condition: { field: 'operation', value: ['create_page', 'read_page', 'list_sites'] }, }, - { + { id: 'pageName', title: 'Page Name', type: 'short-input', layout: 'full', placeholder: 'Name for the new page', - condition: { field: 'operation', value: ['create_page', 'read_page'] }, + condition: { field: 'operation', value: ['create_page', 'read_page'] }, }, { @@ -96,7 +110,7 @@ export const SharepointBlock: BlockConfig = { layout: 'full', placeholder: 'Search for specific pages (e.g., name contains "report")', condition: { field: 'operation', value: 'list_pages' }, - }, + }, ], tools: { access: ['sharepoint_create_page', 'sharepoint_read_page', 'sharepoint_list_sites'], diff --git a/apps/sim/blocks/registry.ts b/apps/sim/blocks/registry.ts index 914d6682a72..c9ada0f7c05 100644 --- a/apps/sim/blocks/registry.ts +++ b/apps/sim/blocks/registry.ts @@ -39,11 +39,10 @@ import { MicrosoftExcelBlock } from '@/blocks/blocks/microsoft_excel' import { MicrosoftPlannerBlock } from '@/blocks/blocks/microsoft_planner' import { MicrosoftTeamsBlock } from '@/blocks/blocks/microsoft_teams' import { MistralParseBlock } from '@/blocks/blocks/mistral_parse' -import { SharepointBlock } from '@/blocks/blocks/sharepoint' import { NotionBlock } from '@/blocks/blocks/notion' +import { OneDriveBlock } from '@/blocks/blocks/onedrive' import { OpenAIBlock } from '@/blocks/blocks/openai' import { OutlookBlock } from '@/blocks/blocks/outlook' -import { OneDriveBlock } from '@/blocks/blocks/onedrive' import { PerplexityBlock } from '@/blocks/blocks/perplexity' import { PineconeBlock } from '@/blocks/blocks/pinecone' import { QdrantBlock } from '@/blocks/blocks/qdrant' @@ -53,6 +52,7 @@ import { RouterBlock } from '@/blocks/blocks/router' import { S3Block } from '@/blocks/blocks/s3' import { ScheduleBlock } from '@/blocks/blocks/schedule' import { SerperBlock } from '@/blocks/blocks/serper' +import { SharepointBlock } from '@/blocks/blocks/sharepoint' import { SlackBlock } from '@/blocks/blocks/slack' import { StagehandBlock } from '@/blocks/blocks/stagehand' import { StagehandAgentBlock } from '@/blocks/blocks/stagehand_agent' diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx index 8b1bb97ea7b..13471d59488 100644 --- a/apps/sim/components/icons.tsx +++ b/apps/sim/components/icons.tsx @@ -3184,16 +3184,24 @@ export function HunterIOIcon(props: SVGProps) { export function MicrosoftOneDriveIcon(props: SVGProps) { return ( - + - - - - + + + + ) @@ -3201,66 +3209,138 @@ export function MicrosoftOneDriveIcon(props: SVGProps) { export function MicrosoftSharepointIcon(props: SVGProps) { return ( - - - - - - - - - - - + + + + + + + + + + + ) } export function MicrosoftPlannerIcon(props: SVGProps) { return ( - + - - - + + + - - - + + + - - - + + + - - - - + + + + - - - + + + - - - + + + - - - - - - + + + + + + ) -} \ No newline at end of file +} diff --git a/apps/sim/lib/auth.ts b/apps/sim/lib/auth.ts index 42980f609c3..4d1b0127e58 100644 --- a/apps/sim/lib/auth.ts +++ b/apps/sim/lib/auth.ts @@ -448,7 +448,15 @@ export const auth = betterAuth({ authorizationUrl: 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize', tokenUrl: 'https://login.microsoftonline.com/common/oauth2/v2.0/token', userInfoUrl: 'https://graph.microsoft.com/v1.0/me', - scopes: ['openid', 'profile', 'email', 'Group.ReadWrite.All', 'Group.Read.All', 'Tasks.ReadWrite', 'offline_access'], + scopes: [ + 'openid', + 'profile', + 'email', + 'Group.ReadWrite.All', + 'Group.Read.All', + 'Tasks.ReadWrite', + 'offline_access', + ], responseType: 'code', accessType: 'offline', authentication: 'basic', @@ -505,7 +513,14 @@ export const auth = betterAuth({ authorizationUrl: 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize', tokenUrl: 'https://login.microsoftonline.com/common/oauth2/v2.0/token', userInfoUrl: 'https://graph.microsoft.com/v1.0/me', - scopes: ['openid', 'profile', 'email', 'Sites.Read.All', 'Sites.ReadWrite.All', 'offline_access'], + scopes: [ + 'openid', + 'profile', + 'email', + 'Sites.Read.All', + 'Sites.ReadWrite.All', + 'offline_access', + ], responseType: 'code', accessType: 'offline', authentication: 'basic', diff --git a/apps/sim/lib/oauth/oauth.ts b/apps/sim/lib/oauth/oauth.ts index dca5cb9bca3..264a048de3e 100644 --- a/apps/sim/lib/oauth/oauth.ts +++ b/apps/sim/lib/oauth/oauth.ts @@ -14,9 +14,10 @@ import { LinearIcon, MicrosoftExcelIcon, MicrosoftIcon, + MicrosoftOneDriveIcon, + MicrosoftPlannerIcon, MicrosoftSharepointIcon, MicrosoftTeamsIcon, - MicrosoftOneDriveIcon, NotionIcon, OutlookIcon, RedditIcon, @@ -24,7 +25,6 @@ import { SupabaseIcon, WealthboxIcon, xIcon, - MicrosoftPlannerIcon, } from '@/components/icons' import { env } from '@/lib/env' import { createLogger } from '@/lib/logs/console/logger' @@ -171,7 +171,15 @@ export const OAUTH_PROVIDERS: Record = { providerId: 'microsoft-planner', icon: (props) => MicrosoftPlannerIcon(props), baseProviderIcon: (props) => MicrosoftIcon(props), - scopes: ['openid', 'profile', 'email', 'Group.ReadWrite.All','Group.Read.All', 'Tasks.ReadWrite', 'offline_access'], + scopes: [ + 'openid', + 'profile', + 'email', + 'Group.ReadWrite.All', + 'Group.Read.All', + 'Tasks.ReadWrite', + 'offline_access', + ], }, 'microsoft-teams': { id: 'microsoft-teams', @@ -215,7 +223,7 @@ export const OAUTH_PROVIDERS: Record = { 'offline_access', ], }, - 'onedrive': { + onedrive: { id: 'onedrive', name: 'OneDrive', description: 'Connect to OneDrive and manage files.', @@ -224,14 +232,21 @@ export const OAUTH_PROVIDERS: Record = { baseProviderIcon: (props) => MicrosoftIcon(props), scopes: ['openid', 'profile', 'email', 'Files.Read', 'Files.ReadWrite', 'offline_access'], }, - 'sharepoint': { + sharepoint: { id: 'sharepoint', name: 'SharePoint', description: 'Connect to SharePoint and manage sites.', providerId: 'sharepoint', icon: (props) => MicrosoftSharepointIcon(props), baseProviderIcon: (props) => MicrosoftIcon(props), - scopes: ['openid', 'profile', 'email', 'Sites.Read.All', 'Sites.ReadWrite.All', 'offline_access'], + scopes: [ + 'openid', + 'profile', + 'email', + 'Sites.Read.All', + 'Sites.ReadWrite.All', + 'offline_access', + ], }, }, defaultService: 'microsoft', @@ -579,12 +594,13 @@ export function parseProvider(provider: OAuthProvider): ProviderConfig { featureType: 'outlook', } } - else if (provider === 'onedrive') { + if (provider === 'onedrive') { return { baseProvider: 'microsoft', featureType: 'onedrive', } - } else if (provider === 'sharepoint') { + } + if (provider === 'sharepoint') { return { baseProvider: 'microsoft', featureType: 'sharepoint', diff --git a/apps/sim/tools/microsoft_planner/create_task.ts b/apps/sim/tools/microsoft_planner/create_task.ts index e9c648ff040..5fdcba1713e 100644 --- a/apps/sim/tools/microsoft_planner/create_task.ts +++ b/apps/sim/tools/microsoft_planner/create_task.ts @@ -7,7 +7,10 @@ import type { ToolConfig } from '@/tools/types' const logger = createLogger('MicrosoftPlannerCreateTask') -export const createTaskTool: ToolConfig = { +export const createTaskTool: ToolConfig< + MicrosoftPlannerToolParams, + MicrosoftPlannerCreateResponse +> = { id: 'microsoft_planner_create_task', name: 'Create Microsoft Planner Task', description: 'Create a new task in Microsoft Planner', @@ -145,12 +148,23 @@ export const createTaskTool: ToolConfig = { id: 'microsoft_planner_read_task', name: 'Read Microsoft Planner Tasks', - description: 'Read tasks from Microsoft Planner - get all user tasks, all tasks in a plan, or a specific task', + description: + 'Read tasks from Microsoft Planner - get all user tasks, all tasks in a plan, or a specific task', version: '1.0', oauth: { required: true, @@ -39,10 +40,10 @@ export const readTaskTool: ToolConfig { - const finalUrl = params.taskId + const finalUrl = params.taskId ? `https://graph.microsoft.com/v1.0/planner/tasks/${params.taskId}` : 'https://graph.microsoft.com/v1.0/me/planner/tasks' - + logger.info('Microsoft Planner URL:', finalUrl) return finalUrl }, @@ -61,7 +62,7 @@ export const readTaskTool: ToolConfig { logger.info('Raw response URL:', response.url) logger.info('Raw response status:', response.status) - + if (!response.ok) { const errorJson = await response.json().catch(() => ({ error: response.statusText })) const errorText = @@ -100,7 +101,9 @@ export const readTaskTool: ToolConfig = { oauth: { required: true, provider: 'onedrive', - additionalScopes: ['openid', 'profile', 'email', 'Files.Read', 'Files.ReadWrite', 'offline_access'], + additionalScopes: [ + 'openid', + 'profile', + 'email', + 'Files.Read', + 'Files.ReadWrite', + 'offline_access', + ], }, params: { accessToken: { @@ -53,12 +60,12 @@ export const listTool: ToolConfig = { url: (params) => { // Use specific folder if provided, otherwise use root const folderId = params.folderId || params.folderSelector - const baseUrl = folderId + const baseUrl = folderId ? `https://graph.microsoft.com/v1.0/me/drive/items/${folderId}/children` : 'https://graph.microsoft.com/v1.0/me/drive/root/children' - + const url = new URL(baseUrl) - + // Use Microsoft Graph $select parameter url.searchParams.append( '$select', @@ -69,12 +76,12 @@ export const listTool: ToolConfig = { if (params.query) { url.searchParams.append('$filter', `startswith(name,'${params.query}')`) } - + // Add pagination if (params.pageSize) { url.searchParams.append('$top', params.pageSize.toString()) } - + if (params.pageToken) { url.searchParams.append('$skip', params.pageToken) } diff --git a/apps/sim/tools/onedrive/upload.ts b/apps/sim/tools/onedrive/upload.ts index f64eb82d591..5783351bb53 100644 --- a/apps/sim/tools/onedrive/upload.ts +++ b/apps/sim/tools/onedrive/upload.ts @@ -12,7 +12,14 @@ export const uploadTool: ToolConfig oauth: { required: true, provider: 'onedrive', - additionalScopes: ['openid', 'profile', 'email', 'Files.Read', 'Files.ReadWrite', 'offline_access'], + additionalScopes: [ + 'openid', + 'profile', + 'email', + 'Files.Read', + 'Files.ReadWrite', + 'offline_access', + ], }, params: { accessToken: { @@ -49,11 +56,11 @@ export const uploadTool: ToolConfig request: { url: (params) => { let fileName = params.fileName || 'untitled' - + // Always create .txt files for text content if (!fileName.endsWith('.txt')) { // Remove any existing extensions and add .txt - fileName = fileName.replace(/\.[^.]*$/, '') + '.txt' + fileName = `${fileName.replace(/\.[^.]*$/, '')}.txt` } // Build the proper URL based on parent folder @@ -61,7 +68,7 @@ export const uploadTool: ToolConfig if (parentFolderId && parentFolderId.trim() !== '') { return `https://graph.microsoft.com/v1.0/me/drive/items/${parentFolderId}:/${fileName}:/content` } - // Default to root folder + // Default to root folder return `https://graph.microsoft.com/v1.0/me/drive/root:/${fileName}:/content` }, method: 'PUT', diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 0376e7af416..ad43566b11e 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -83,7 +83,6 @@ import { microsoftPlannerCreateTaskTool, microsoftPlannerReadTaskTool, } from '@/tools/microsoft_planner' -import { onedriveCreateFolderTool, onedriveUploadTool, onedriveListTool } from '@/tools/onedrive' import { microsoftTeamsReadChannelTool, microsoftTeamsReadChatTool, @@ -100,7 +99,7 @@ import { notionSearchTool, notionWriteTool, } from '@/tools/notion' -import { sharepointCreatePageTool, sharepointReadPageTool, sharepointListSitesTool } from '@/tools/sharepoint' +import { onedriveCreateFolderTool, onedriveListTool, onedriveUploadTool } from '@/tools/onedrive' import { imageTool, embeddingsTool as openAIEmbeddings } from '@/tools/openai' import { outlookDraftTool, outlookReadTool, outlookSendTool } from '@/tools/outlook' import { perplexityChatTool } from '@/tools/perplexity' @@ -115,6 +114,11 @@ import { qdrantFetchTool, qdrantSearchTool, qdrantUpsertTool } from '@/tools/qdr import { redditGetCommentsTool, redditGetPostsTool, redditHotPostsTool } from '@/tools/reddit' import { s3GetObjectTool } from '@/tools/s3' import { searchTool as serperSearch } from '@/tools/serper' +import { + sharepointCreatePageTool, + sharepointListSitesTool, + sharepointReadPageTool, +} from '@/tools/sharepoint' import { slackCanvasTool, slackMessageReaderTool, slackMessageTool } from '@/tools/slack' import { stagehandAgentTool, stagehandExtractTool } from '@/tools/stagehand' import { diff --git a/apps/sim/tools/sharepoint/create_page.ts b/apps/sim/tools/sharepoint/create_page.ts index 4fad53677b4..f139815199f 100644 --- a/apps/sim/tools/sharepoint/create_page.ts +++ b/apps/sim/tools/sharepoint/create_page.ts @@ -1,6 +1,6 @@ +import { createLogger } from '@/lib/logs/console/logger' import type { SharepointCreatePageResponse, SharepointToolParams } from '@/tools/sharepoint/types' import type { ToolConfig } from '@/tools/types' -import { createLogger } from '@/lib/logs/console/logger' const logger = createLogger('SharePointCreatePage') @@ -70,16 +70,16 @@ export const createPageTool: ToolConfig${params.pageContent.replace(/"/g, '"').replace(/'/g, ''')}

` - } - ] - } - ] - } - ] + id: '6f9230af-2a98-4952-b205-9ede4f9ef548', + innerHtml: `

${params.pageContent.replace(/"/g, '"').replace(/'/g, ''')}

`, + }, + ], + }, + ], + }, + ], } } @@ -118,15 +118,18 @@ export const createPageTool: ToolConfig]*>/g, '').trim() @@ -57,33 +57,33 @@ function extractTextFromCanvasLayout(canvasLayout: any): string { } } } - + const finalContent = textParts.join('\n\n') logger.info('Final extracted content', { textPartsCount: textParts.length, finalContentLength: finalContent.length, - finalContent + finalContent, }) - + return finalContent } // Remove OData metadata from objects function cleanODataMetadata(obj: any): any { if (!obj || typeof obj !== 'object') return obj - + if (Array.isArray(obj)) { - return obj.map(item => cleanODataMetadata(item)) + return obj.map((item) => cleanODataMetadata(item)) } - + const cleaned: any = {} for (const [key, value] of Object.entries(obj)) { // Skip OData metadata keys if (key.includes('@odata')) continue - + cleaned[key] = cleanODataMetadata(value) } - + return cleaned } @@ -138,7 +138,7 @@ export const readPageTool: ToolConfig ({ id: p.id, name: p.name, title: p.title })), - selectedPage: data.value[0].name + selectedPage: data.value[0].name, }) - + pageData = data.value[0] - + // For search results, we need to make another call to get the content if (pageData.id) { const siteId = params?.siteId || params?.siteSelector || 'root' const contentUrl = `https://graph.microsoft.com/v1.0/sites/${siteId}/pages/${pageData.id}/microsoft.graph.sitePage?$expand=canvasLayout` - + logger.info('Making second API call to get page content', { pageId: pageData.id, contentUrl, - siteId + siteId, }) - + const contentResponse = await fetch(contentUrl, { headers: { Authorization: `Bearer ${params?.accessToken}`, Accept: 'application/json', }, }) - + logger.info('Content API response', { status: contentResponse.status, statusText: contentResponse.statusText, - ok: contentResponse.ok + ok: contentResponse.ok, }) - + if (contentResponse.ok) { const contentResult = await contentResponse.json() logger.info('Content API result', { hasCanvasLayout: !!contentResult.canvasLayout, - contentResultKeys: Object.keys(contentResult) + contentResultKeys: Object.keys(contentResult), }) - + contentData = { content: extractTextFromCanvasLayout(contentResult.canvasLayout), canvasLayout: cleanODataMetadata(contentResult.canvasLayout), @@ -283,7 +285,7 @@ export const readPageTool: ToolConfig { let baseUrl: string - + if (params.groupId) { // Access group team site baseUrl = `https://graph.microsoft.com/v1.0/groups/${params.groupId}/sites/root` @@ -82,9 +82,9 @@ export const listSitesTool: ToolConfig { From 6a82b0bbb80cffc5698896fc02088f32eedd7f11 Mon Sep 17 00:00:00 2001 From: Adam Gough Date: Thu, 31 Jul 2025 23:46:49 -0700 Subject: [PATCH 08/23] made read task better --- .../tools/microsoft_planner/tasks/route.ts | 109 ++++++++++++++ .../components/microsoft-file-selector.tsx | 141 ++++++++++++++++-- .../file-selector/file-selector-input.tsx | 39 +++++ apps/sim/blocks/blocks/microsoft_planner.ts | 45 +----- .../tools/microsoft_planner/create_task.ts | 20 --- apps/sim/tools/microsoft_planner/read_task.ts | 66 ++++---- 6 files changed, 316 insertions(+), 104 deletions(-) create mode 100644 apps/sim/app/api/tools/microsoft_planner/tasks/route.ts diff --git a/apps/sim/app/api/tools/microsoft_planner/tasks/route.ts b/apps/sim/app/api/tools/microsoft_planner/tasks/route.ts new file mode 100644 index 00000000000..8d6ce970b47 --- /dev/null +++ b/apps/sim/app/api/tools/microsoft_planner/tasks/route.ts @@ -0,0 +1,109 @@ +import { randomUUID } from 'crypto' +import { eq } from 'drizzle-orm' +import { type NextRequest, NextResponse } from 'next/server' +import { getSession } from '@/lib/auth' +import { createLogger } from '@/lib/logs/console/logger' +import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils' +import { db } from '@/db' +import { account } from '@/db/schema' + +const logger = createLogger('MicrosoftPlannerTasksAPI') + +export async function GET(request: NextRequest) { + const requestId = randomUUID().slice(0, 8) + + try { + const session = await getSession() + + if (!session?.user?.id) { + logger.warn(`[${requestId}] Unauthenticated request rejected`) + return NextResponse.json({ error: 'User not authenticated' }, { status: 401 }) + } + + const { searchParams } = new URL(request.url) + const credentialId = searchParams.get('credentialId') + const planId = searchParams.get('planId') + + if (!credentialId) { + logger.error(`[${requestId}] Missing credentialId parameter`) + return NextResponse.json({ error: 'Credential ID is required' }, { status: 400 }) + } + + if (!planId) { + logger.error(`[${requestId}] Missing planId parameter`) + return NextResponse.json({ error: 'Plan ID is required' }, { status: 400 }) + } + + // Get the credential from the database + const credentials = await db.select().from(account).where(eq(account.id, credentialId)).limit(1) + + if (!credentials.length) { + logger.warn(`[${requestId}] Credential not found`, { credentialId }) + return NextResponse.json({ error: 'Credential not found' }, { status: 404 }) + } + + const credential = credentials[0] + + // Check if the credential belongs to the user + if (credential.userId !== session.user.id) { + logger.warn(`[${requestId}] Unauthorized credential access attempt`, { + credentialUserId: credential.userId, + requestUserId: session.user.id, + }) + return NextResponse.json({ error: 'Unauthorized' }, { status: 403 }) + } + + // Refresh access token if needed + const accessToken = await refreshAccessTokenIfNeeded(credentialId, session.user.id, requestId) + + if (!accessToken) { + logger.error(`[${requestId}] Failed to obtain valid access token`) + return NextResponse.json({ error: 'Failed to obtain valid access token' }, { status: 401 }) + } + + // Fetch tasks directly from Microsoft Graph API + const response = await fetch(`https://graph.microsoft.com/v1.0/planner/plans/${planId}/tasks`, { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }) + + if (!response.ok) { + const errorText = await response.text() + logger.error(`[${requestId}] Microsoft Graph API error:`, errorText) + return NextResponse.json( + { error: 'Failed to fetch tasks from Microsoft Graph' }, + { status: response.status } + ) + } + + const data = await response.json() + const tasks = data.value || [] + + // Filter tasks to only include useful fields (matching our read_task tool) + const filteredTasks = tasks.map((task: any) => ({ + id: task.id, + title: task.title, + planId: task.planId, + bucketId: task.bucketId, + percentComplete: task.percentComplete, + priority: task.priority, + dueDateTime: task.dueDateTime, + createdDateTime: task.createdDateTime, + completedDateTime: task.completedDateTime, + hasDescription: task.hasDescription, + assignments: task.assignments ? Object.keys(task.assignments) : [], + })) + + return NextResponse.json({ + tasks: filteredTasks, + metadata: { + planId, + planUrl: `https://graph.microsoft.com/v1.0/planner/plans/${planId}`, + }, + }) + } catch (error) { + logger.error(`[${requestId}] Error fetching Microsoft Planner tasks:`, error) + return NextResponse.json({ error: 'Failed to fetch tasks' }, { status: 500 }) + } +} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/microsoft-file-selector.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/microsoft-file-selector.tsx index ce71f7160c0..f397d28f3ce 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/microsoft-file-selector.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/microsoft-file-selector.tsx @@ -50,6 +50,7 @@ interface MicrosoftFileSelectorProps { serviceId?: string showPreview?: boolean onFileInfoChange?: (fileInfo: MicrosoftFileInfo | null) => void + planId?: string } export function MicrosoftFileSelector({ @@ -62,6 +63,7 @@ export function MicrosoftFileSelector({ serviceId, showPreview = true, onFileInfoChange, + planId, }: MicrosoftFileSelectorProps) { const [open, setOpen] = useState(false) const [credentials, setCredentials] = useState([]) @@ -77,6 +79,11 @@ export function MicrosoftFileSelector({ const [credentialsLoaded, setCredentialsLoaded] = useState(false) const initialFetchRef = useRef(false) + // Handle Microsoft Planner task selection + const [plannerTasks, setPlannerTasks] = useState([]) + const [isLoadingTasks, setIsLoadingTasks] = useState(false) + const [selectedTask, setSelectedTask] = useState(null) + // Determine the appropriate service ID based on provider and scopes const getServiceId = (): string => { if (serviceId) return serviceId @@ -227,6 +234,74 @@ export function MicrosoftFileSelector({ [selectedCredentialId, onFileInfoChange, serviceId] ) + // Fetch Microsoft Planner tasks when planId and credentials are available + const fetchPlannerTasks = useCallback(async () => { + if (!selectedCredentialId || !planId || serviceId !== 'microsoft-planner') { + logger.info('Skipping task fetch - missing requirements:', { + selectedCredentialId: !!selectedCredentialId, + planId: !!planId, + serviceId, + }) + return + } + + logger.info('Fetching Planner tasks with:', { + credentialId: selectedCredentialId, + planId, + serviceId, + }) + + setIsLoadingTasks(true) + try { + const queryParams = new URLSearchParams({ + credentialId: selectedCredentialId, + planId: planId, + }) + + const url = `/api/tools/microsoft_planner/tasks?${queryParams.toString()}` + logger.info('Calling API endpoint:', url) + + const response = await fetch(url) + + if (response.ok) { + const data = await response.json() + logger.info('Received task data:', data) + const tasks = data.tasks || [] + + // Transform tasks to match file info format for consistency + const transformedTasks = tasks.map((task: any) => ({ + id: task.id, + name: task.title, + mimeType: 'planner/task', + webViewLink: `https://tasks.office.com/planner/task/${task.id}`, + modifiedTime: task.createdDateTime, + createdTime: task.createdDateTime, + planId: task.planId, + bucketId: task.bucketId, + percentComplete: task.percentComplete, + priority: task.priority, + dueDateTime: task.dueDateTime, + })) + + logger.info('Transformed tasks:', transformedTasks) + setPlannerTasks(transformedTasks) + } else { + const errorText = await response.text() + logger.error('API response not ok:', { + status: response.status, + statusText: response.statusText, + errorText, + }) + setPlannerTasks([]) + } + } catch (error) { + logger.error('Network/fetch error:', error) + setPlannerTasks([]) + } finally { + setIsLoadingTasks(false) + } + }, [selectedCredentialId, planId, serviceId]) + // Fetch credentials on initial mount useEffect(() => { if (!initialFetchRef.current) { @@ -253,6 +328,24 @@ export function MicrosoftFileSelector({ } }, [searchQuery, selectedCredentialId, fetchAvailableFiles]) + // Fetch planner tasks when credentials and planId change + useEffect(() => { + if (serviceId === 'microsoft-planner' && selectedCredentialId && planId) { + fetchPlannerTasks() + } + }, [selectedCredentialId, planId, serviceId, fetchPlannerTasks]) + + // Handle task selection for planner + const handleTaskSelect = (task: any) => { + setSelectedFileId(task.id) + setSelectedFile(task) + setSelectedTask(task) + onChange(task.id, task) + onFileInfoChange?.(task) + setOpen(false) + setSearchQuery('') + } + // Keep internal selectedFileId in sync with the value prop useEffect(() => { if (value !== selectedFileId) { @@ -419,6 +512,9 @@ export function MicrosoftFileSelector({ if (file.mimeType === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') { return } + if (file.mimeType === 'planner/task') { + return getProviderIcon(provider) + } // if (file.mimeType === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document') { // return // } @@ -436,12 +532,14 @@ export function MicrosoftFileSelector({ const getFileTypeTitleCase = () => { if (serviceId === 'onedrive') return 'Folders' if (serviceId === 'sharepoint') return 'Sites' + if (serviceId === 'microsoft-planner') return 'Tasks' return 'Excel Files' } const getSearchPlaceholder = () => { if (serviceId === 'onedrive') return 'Search OneDrive folders...' if (serviceId === 'sharepoint') return 'Search SharePoint sites...' + if (serviceId === 'microsoft-planner') return 'Search tasks...' return 'Search Excel files...' } @@ -458,12 +556,24 @@ export function MicrosoftFileSelector({ description: 'No SharePoint sites were found.', } } + if (serviceId === 'microsoft-planner') { + return { + title: 'No tasks found.', + description: 'No tasks were found in this plan.', + } + } return { title: 'No Excel files found.', description: 'No .xlsx files were found in your OneDrive.', } } + // Filter tasks based on search query for planner + const filteredTasks = + serviceId === 'microsoft-planner' + ? plannerTasks.filter((task) => task.name.toLowerCase().includes(searchQuery.toLowerCase())) + : availableFiles + return ( <>
@@ -472,7 +582,7 @@ export function MicrosoftFileSelector({ onOpenChange={(isOpen) => { setOpen(isOpen) if (!isOpen) { - setSearchQuery('') // Clear search when popover closes + setSearchQuery('') } }} > @@ -482,7 +592,7 @@ export function MicrosoftFileSelector({ role='combobox' aria-expanded={open} className='h-10 w-full min-w-0 justify-between' - disabled={disabled} + disabled={disabled || (serviceId === 'microsoft-planner' && !planId)} >
{selectedFile ? ( @@ -533,7 +643,7 @@ export function MicrosoftFileSelector({ - {isLoading || isLoadingFiles ? ( + {isLoading || isLoadingFiles || isLoadingTasks ? (
Loading... @@ -545,7 +655,14 @@ export function MicrosoftFileSelector({ Connect a {getProviderName(provider)} account to continue.

- ) : availableFiles.length === 0 ? ( + ) : serviceId === 'microsoft-planner' && !planId ? ( +
+

Plan ID required.

+

+ Please enter a Plan ID first to see tasks. +

+
+ ) : filteredTasks.length === 0 ? (

{getEmptyStateText().title}

@@ -577,17 +694,21 @@ export function MicrosoftFileSelector({ )} - {/* Available files - only show if we have credentials and files */} - {credentials.length > 0 && selectedCredentialId && availableFiles.length > 0 && ( + {/* Available files/tasks - only show if we have credentials and items */} + {credentials.length > 0 && selectedCredentialId && filteredTasks.length > 0 && (

{getFileTypeTitleCase()}
- {availableFiles.map((file) => ( + {filteredTasks.map((file) => ( handleFileSelect(file)} + onSelect={() => + serviceId === 'microsoft-planner' + ? handleTaskSelect(file) + : handleFileSelect(file) + } >
{getFileIcon(file, 'sm')} @@ -656,7 +777,9 @@ export function MicrosoftFileSelector({ className='flex items-center gap-1 text-primary text-xs hover:underline' onClick={(e) => e.stopPropagation()} > - Open in OneDrive + + {serviceId === 'microsoft-planner' ? 'Open in Planner' : 'Open in OneDrive'} + ) : ( diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/file-selector-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/file-selector-input.tsx index 033085789b9..ab88099adf7 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/file-selector-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/file-selector-input.tsx @@ -73,6 +73,7 @@ export function FileSelectorInput({ const isGoogleCalendar = subBlock.provider === 'google-calendar' const isWealthbox = provider === 'wealthbox' const isMicrosoftSharePoint = provider === 'microsoft' && subBlock.serviceId === 'sharepoint' + const isMicrosoftPlanner = provider === 'microsoft-planner' // For Confluence and Jira, we need the domain and credentials const domain = isConfluence || isJira ? (getValue(blockId, 'domain') as string) || '' : '' // For Discord, we need the bot token and server ID @@ -430,6 +431,44 @@ export function FileSelectorInput({ ) } + // Handle Microsoft Planner task selector + if (isMicrosoftPlanner) { + const credential = (getValue(blockId, 'credential') as string) || '' + const planId = (getValue(blockId, 'planId') as string) || '' + + return ( + + + +
+ void} + planId={planId} + /> +
+
+ {!credential ? ( + +

Please select Microsoft Planner credentials first

+
+ ) : !planId ? ( + +

Please enter a Plan ID first

+
+ ) : null} +
+
+ ) + } + // Handle Microsoft Teams selector if (isMicrosoftTeams) { // Get credential using the same pattern as other tools diff --git a/apps/sim/blocks/blocks/microsoft_planner.ts b/apps/sim/blocks/blocks/microsoft_planner.ts index e49a86f473b..5822a24ec34 100644 --- a/apps/sim/blocks/blocks/microsoft_planner.ts +++ b/apps/sim/blocks/blocks/microsoft_planner.ts @@ -47,14 +47,15 @@ export const MicrosoftPlannerBlock: BlockConfig = { type: 'short-input', layout: 'full', placeholder: 'Enter the plan ID', - condition: { field: 'operation', value: ['create_task'] }, + condition: { field: 'operation', value: ['create_task', 'read_task'] }, }, { id: 'taskId', title: 'Task ID', - type: 'short-input', + type: 'file-selector', layout: 'full', - placeholder: 'Enter the task ID', + placeholder: 'Select a task', + provider: 'microsoft-planner', condition: { field: 'operation', value: ['read_task'] }, }, { @@ -97,29 +98,6 @@ export const MicrosoftPlannerBlock: BlockConfig = { placeholder: 'Enter the bucket ID to organize the task (optional)', condition: { field: 'operation', value: ['create_task'] }, }, - { - id: 'priority', - title: 'Priority', - type: 'dropdown', - layout: 'full', - options: [ - { label: 'Urgent (0)', id: '0' }, - { label: 'Important (1)', id: '1' }, - { label: 'Medium (2)', id: '2' }, - { label: 'Low (3)', id: '3' }, - { label: 'Later (4)', id: '4' }, - { label: 'Lowest (5)', id: '5' }, - ], - condition: { field: 'operation', value: ['create_task'] }, - }, - { - id: 'percentComplete', - title: 'Completion %', - type: 'short-input', - layout: 'full', - placeholder: 'Enter completion percentage (0-100)', - condition: { field: 'operation', value: ['create_task'] }, - }, ], tools: { access: ['microsoft_planner_read_task', 'microsoft_planner_create_task'], @@ -145,8 +123,6 @@ export const MicrosoftPlannerBlock: BlockConfig = { dueDateTime, assigneeUserId, bucketId, - priority, - percentComplete, ...rest } = params @@ -212,17 +188,6 @@ export const MicrosoftPlannerBlock: BlockConfig = { createParams.bucketId = bucketId.trim() } - if (priority !== undefined && priority !== '') { - createParams.priority = Number.parseInt(priority as string, 10) - } - - if (percentComplete?.trim()) { - const percent = Number.parseInt(percentComplete.trim(), 10) - if (percent >= 0 && percent <= 100) { - createParams.percentComplete = percent - } - } - return createParams } @@ -240,8 +205,6 @@ export const MicrosoftPlannerBlock: BlockConfig = { dueDateTime: { type: 'string', required: false }, assigneeUserId: { type: 'string', required: false }, bucketId: { type: 'string', required: false }, - priority: { type: 'string', required: false }, - percentComplete: { type: 'string', required: false }, }, outputs: { task: 'json', diff --git a/apps/sim/tools/microsoft_planner/create_task.ts b/apps/sim/tools/microsoft_planner/create_task.ts index 5fdcba1713e..df6880d3170 100644 --- a/apps/sim/tools/microsoft_planner/create_task.ts +++ b/apps/sim/tools/microsoft_planner/create_task.ts @@ -63,18 +63,6 @@ export const createTaskTool: ToolConfig< visibility: 'user-or-llm', description: 'The bucket ID to place the task in', }, - priority: { - type: 'number', - required: false, - visibility: 'user-or-llm', - description: 'The priority of the task (0-10, where 0 is highest priority)', - }, - percentComplete: { - type: 'number', - required: false, - visibility: 'user-or-llm', - description: 'The completion percentage of the task (0-100)', - }, }, request: { url: () => 'https://graph.microsoft.com/v1.0/planner/tasks', @@ -110,14 +98,6 @@ export const createTaskTool: ToolConfig< body.dueDateTime = params.dueDateTime } - if (params.priority !== undefined) { - body.priority = params.priority - } - - if (params.percentComplete !== undefined) { - body.percentComplete = params.percentComplete - } - if (params.assigneeUserId) { body.assignments = { [params.assigneeUserId]: { diff --git a/apps/sim/tools/microsoft_planner/read_task.ts b/apps/sim/tools/microsoft_planner/read_task.ts index 60dd58786b7..ece8e084d81 100644 --- a/apps/sim/tools/microsoft_planner/read_task.ts +++ b/apps/sim/tools/microsoft_planner/read_task.ts @@ -11,7 +11,7 @@ export const readTaskTool: ToolConfig { - const finalUrl = params.taskId - ? `https://graph.microsoft.com/v1.0/planner/tasks/${params.taskId}` + const finalUrl = params.planId + ? `https://graph.microsoft.com/v1.0/planner/plans/${params.planId}/tasks` : 'https://graph.microsoft.com/v1.0/me/planner/tasks' logger.info('Microsoft Planner URL:', finalUrl) @@ -74,39 +74,37 @@ export const readTaskTool: ToolConfig ({ + id: task.id, + title: task.title, + planId: task.planId, + bucketId: task.bucketId, + percentComplete: task.percentComplete, + priority: task.priority, + dueDateTime: task.dueDateTime, + createdDateTime: task.createdDateTime, + completedDateTime: task.completedDateTime, + hasDescription: task.hasDescription, + assignments: task.assignments ? Object.keys(task.assignments) : [], + })) + + const result: MicrosoftPlannerReadResponse = { + success: true, + output: { + tasks, + metadata: { + planId: params?.planId || '', + userId: params?.planId ? undefined : 'me', + planUrl: params?.planId + ? `https://graph.microsoft.com/v1.0/planner/plans/${params.planId}` + : undefined, }, - } + }, } return result From e8042f1767772d81a0d2926e6d7a209add821435 Mon Sep 17 00:00:00 2001 From: Adam Gough Date: Fri, 1 Aug 2025 10:32:30 -0700 Subject: [PATCH 09/23] cleaned up read task --- .../components/microsoft-file-selector.tsx | 1 + apps/sim/blocks/blocks/microsoft_planner.ts | 31 +++++++------------ apps/sim/tools/microsoft_planner/read_task.ts | 17 ++++++++-- 3 files changed, 26 insertions(+), 23 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/microsoft-file-selector.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/microsoft-file-selector.tsx index f397d28f3ce..a171a0e01a1 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/microsoft-file-selector.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/microsoft-file-selector.tsx @@ -400,6 +400,7 @@ export function MicrosoftFileSelector({ selectedFile, isLoadingSelectedFile, fetchFileById, + serviceId ]) // Handle selecting a file from the available files diff --git a/apps/sim/blocks/blocks/microsoft_planner.ts b/apps/sim/blocks/blocks/microsoft_planner.ts index 5822a24ec34..cde92c68ab9 100644 --- a/apps/sim/blocks/blocks/microsoft_planner.ts +++ b/apps/sim/blocks/blocks/microsoft_planner.ts @@ -133,28 +133,19 @@ export const MicrosoftPlannerBlock: BlockConfig = { // For read operations if (operation === 'read_task') { - // No additional parameters needed - will get all user tasks - return baseParams - } - - if (operation === 'read_task') { - if (!planId?.trim()) { - throw new Error('Plan ID is required to read tasks from a specific plan.') - } - return { - ...baseParams, - planId: planId.trim(), - } - } - - if (operation === 'read_task') { - if (!taskId?.trim()) { - throw new Error('Task ID is required to read a specific task.') + const readParams: any = { ...baseParams } + + // If taskId is provided, add it (highest priority - get specific task) + if (taskId?.trim()) { + readParams.taskId = taskId.trim() } - return { - ...baseParams, - taskId: taskId.trim(), + // If no taskId but planId is provided, add planId (get tasks from plan) + else if (planId?.trim()) { + readParams.planId = planId.trim() } + // If neither, get all user tasks (baseParams only) + + return readParams } // For create operation diff --git a/apps/sim/tools/microsoft_planner/read_task.ts b/apps/sim/tools/microsoft_planner/read_task.ts index ece8e084d81..f80b9745f07 100644 --- a/apps/sim/tools/microsoft_planner/read_task.ts +++ b/apps/sim/tools/microsoft_planner/read_task.ts @@ -40,9 +40,20 @@ export const readTaskTool: ToolConfig { - const finalUrl = params.planId - ? `https://graph.microsoft.com/v1.0/planner/plans/${params.planId}/tasks` - : 'https://graph.microsoft.com/v1.0/me/planner/tasks' + let finalUrl: string + + // If taskId is provided, get specific task + if (params.taskId) { + finalUrl = `https://graph.microsoft.com/v1.0/planner/tasks/${params.taskId}` + } + // Else if planId is provided, get tasks from plan + else if (params.planId) { + finalUrl = `https://graph.microsoft.com/v1.0/planner/plans/${params.planId}/tasks` + } + // Else get all user tasks + else { + finalUrl = 'https://graph.microsoft.com/v1.0/me/planner/tasks' + } logger.info('Microsoft Planner URL:', finalUrl) return finalUrl From fb823c14912060ed233f21ebed22a69d9927993e Mon Sep 17 00:00:00 2001 From: Adam Gough Date: Fri, 1 Aug 2025 10:37:39 -0700 Subject: [PATCH 10/23] bun run lint --- .../file-selector/components/microsoft-file-selector.tsx | 2 +- apps/sim/blocks/blocks/microsoft_planner.ts | 6 +++--- apps/sim/tools/microsoft_planner/read_task.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/microsoft-file-selector.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/microsoft-file-selector.tsx index a171a0e01a1..780c99d4e5d 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/microsoft-file-selector.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/microsoft-file-selector.tsx @@ -400,7 +400,7 @@ export function MicrosoftFileSelector({ selectedFile, isLoadingSelectedFile, fetchFileById, - serviceId + serviceId, ]) // Handle selecting a file from the available files diff --git a/apps/sim/blocks/blocks/microsoft_planner.ts b/apps/sim/blocks/blocks/microsoft_planner.ts index cde92c68ab9..8e424aefe1e 100644 --- a/apps/sim/blocks/blocks/microsoft_planner.ts +++ b/apps/sim/blocks/blocks/microsoft_planner.ts @@ -134,17 +134,17 @@ export const MicrosoftPlannerBlock: BlockConfig = { // For read operations if (operation === 'read_task') { const readParams: any = { ...baseParams } - + // If taskId is provided, add it (highest priority - get specific task) if (taskId?.trim()) { readParams.taskId = taskId.trim() } - // If no taskId but planId is provided, add planId (get tasks from plan) + // If no taskId but planId is provided, add planId (get tasks from plan) else if (planId?.trim()) { readParams.planId = planId.trim() } // If neither, get all user tasks (baseParams only) - + return readParams } diff --git a/apps/sim/tools/microsoft_planner/read_task.ts b/apps/sim/tools/microsoft_planner/read_task.ts index f80b9745f07..240ee4baf2f 100644 --- a/apps/sim/tools/microsoft_planner/read_task.ts +++ b/apps/sim/tools/microsoft_planner/read_task.ts @@ -41,7 +41,7 @@ export const readTaskTool: ToolConfig { let finalUrl: string - + // If taskId is provided, get specific task if (params.taskId) { finalUrl = `https://graph.microsoft.com/v1.0/planner/tasks/${params.taskId}` From 34a2eea58c947e15fbe6093ed4c16fb3682d21cf Mon Sep 17 00:00:00 2001 From: Adam Gough Date: Fri, 1 Aug 2025 10:41:54 -0700 Subject: [PATCH 11/23] cleaned up #840 --- apps/sim/blocks/blocks/sharepoint.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/apps/sim/blocks/blocks/sharepoint.ts b/apps/sim/blocks/blocks/sharepoint.ts index ef7ea443ba5..d3c63c14e7d 100644 --- a/apps/sim/blocks/blocks/sharepoint.ts +++ b/apps/sim/blocks/blocks/sharepoint.ts @@ -159,8 +159,6 @@ export const SharepointBlock: BlockConfig = { pageSize: { type: 'number', required: false }, }, outputs: { - page: 'json', - content: 'json', sites: 'json', }, } From 14c3dd92fdbee9ab5bda0e8e369cd7fd5dca00c6 Mon Sep 17 00:00:00 2001 From: Adam Gough Date: Fri, 1 Aug 2025 13:26:38 -0700 Subject: [PATCH 12/23] greptile changes and clean up --- .../tools/microsoft_planner/tasks/route.ts | 3 +- .../app/api/tools/onedrive/folders/route.ts | 19 ++- .../app/api/tools/sharepoint/site/route.ts | 18 +-- .../app/api/tools/sharepoint/sites/route.ts | 3 +- .../components/microsoft-file-selector.tsx | 114 ++++++++++++------ apps/sim/blocks/blocks/microsoft_planner.ts | 35 ++++-- apps/sim/blocks/blocks/onedrive.ts | 60 +++------ apps/sim/blocks/blocks/sharepoint.ts | 31 ++--- apps/sim/blocks/registry.ts | 2 +- apps/sim/lib/oauth/oauth.ts | 2 + .../tools/microsoft_planner/create_task.ts | 6 +- apps/sim/tools/microsoft_planner/read_task.ts | 4 +- apps/sim/tools/microsoft_planner/types.ts | 51 +++++++- apps/sim/tools/onedrive/create_folder.ts | 2 +- apps/sim/tools/onedrive/list.ts | 36 +++++- apps/sim/tools/sharepoint/create_page.ts | 4 +- apps/sim/tools/sharepoint/read_site.ts | 4 +- 17 files changed, 250 insertions(+), 144 deletions(-) diff --git a/apps/sim/app/api/tools/microsoft_planner/tasks/route.ts b/apps/sim/app/api/tools/microsoft_planner/tasks/route.ts index 8d6ce970b47..f25802e8c89 100644 --- a/apps/sim/app/api/tools/microsoft_planner/tasks/route.ts +++ b/apps/sim/app/api/tools/microsoft_planner/tasks/route.ts @@ -6,6 +6,7 @@ import { createLogger } from '@/lib/logs/console/logger' import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils' import { db } from '@/db' import { account } from '@/db/schema' +import type { PlannerTask } from '@/tools/microsoft_planner/types' const logger = createLogger('MicrosoftPlannerTasksAPI') @@ -81,7 +82,7 @@ export async function GET(request: NextRequest) { const tasks = data.value || [] // Filter tasks to only include useful fields (matching our read_task tool) - const filteredTasks = tasks.map((task: any) => ({ + const filteredTasks = tasks.map((task: PlannerTask) => ({ id: task.id, title: task.title, planId: task.planId, diff --git a/apps/sim/app/api/tools/onedrive/folders/route.ts b/apps/sim/app/api/tools/onedrive/folders/route.ts index 71e98d7b007..1a00d2cb6be 100644 --- a/apps/sim/app/api/tools/onedrive/folders/route.ts +++ b/apps/sim/app/api/tools/onedrive/folders/route.ts @@ -11,6 +11,21 @@ export const dynamic = 'force-dynamic' const logger = createLogger('OneDriveFoldersAPI') +interface MicrosoftGraphDriveItem { + id: string + name: string + folder?: { + childCount: number + } + file?: { + mimeType: string + } + webUrl: string + createdDateTime: string + lastModifiedDateTime: string + size?: number +} + /** * Get folders from Microsoft OneDrive */ @@ -69,8 +84,8 @@ export async function GET(request: NextRequest) { const data = await response.json() const folders = (data.value || []) - .filter((item: any) => item.folder) // Only folders - .map((folder: any) => ({ + .filter((item: MicrosoftGraphDriveItem) => item.folder) // Only folders + .map((folder: MicrosoftGraphDriveItem) => ({ id: folder.id, name: folder.name, mimeType: 'application/vnd.microsoft.graph.folder', diff --git a/apps/sim/app/api/tools/sharepoint/site/route.ts b/apps/sim/app/api/tools/sharepoint/site/route.ts index c52c9f8cea5..d4432da88c6 100644 --- a/apps/sim/app/api/tools/sharepoint/site/route.ts +++ b/apps/sim/app/api/tools/sharepoint/site/route.ts @@ -25,9 +25,9 @@ export async function GET(request: NextRequest) { const { searchParams } = new URL(request.url) const credentialId = searchParams.get('credentialId') - const fileId = searchParams.get('fileId') // This will be the site ID + const siteId = searchParams.get('siteId') - if (!credentialId || !fileId) { + if (!credentialId || !siteId) { return NextResponse.json({ error: 'Credential ID and Site ID are required' }, { status: 400 }) } @@ -54,17 +54,17 @@ export async function GET(request: NextRequest) { // 5. Group team site: groups/{group-id}/sites/root let endpoint: string - if (fileId === 'root') { + if (siteId === 'root') { endpoint = 'sites/root' - } else if (fileId.includes(':')) { + } else if (siteId.includes(':')) { // Server-relative URL format - endpoint = `sites/${fileId}` - } else if (fileId.includes('groups/')) { + endpoint = `sites/${siteId}` + } else if (siteId.includes('groups/')) { // Group team site format - endpoint = fileId + endpoint = siteId } else { // Standard site ID or hostname - endpoint = `sites/${fileId}` + endpoint = `sites/${siteId}` } const response = await fetch( @@ -97,7 +97,7 @@ export async function GET(request: NextRequest) { } logger.info(`[${requestId}] Successfully fetched SharePoint site: ${transformedSite.name}`) - return NextResponse.json({ file: transformedSite }, { status: 200 }) + return NextResponse.json({ site: transformedSite }, { status: 200 }) } catch (error) { logger.error(`[${requestId}] Error fetching site from SharePoint`, error) return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) diff --git a/apps/sim/app/api/tools/sharepoint/sites/route.ts b/apps/sim/app/api/tools/sharepoint/sites/route.ts index 439c4137de5..e2571e3758a 100644 --- a/apps/sim/app/api/tools/sharepoint/sites/route.ts +++ b/apps/sim/app/api/tools/sharepoint/sites/route.ts @@ -6,6 +6,7 @@ import { createLogger } from '@/lib/logs/console/logger' import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils' import { db } from '@/db' import { account } from '@/db/schema' +import type { SharepointSite } from '@/tools/sharepoint/types' export const dynamic = 'force-dynamic' @@ -66,7 +67,7 @@ export async function GET(request: NextRequest) { } const data = await response.json() - const sites = (data.value || []).map((site: any) => ({ + const sites = (data.value || []).map((site: SharepointSite) => ({ id: site.id, name: site.displayName || site.name, mimeType: 'application/vnd.microsoft.graph.site', diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/microsoft-file-selector.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/microsoft-file-selector.tsx index 780c99d4e5d..ca1c68c0bcd 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/microsoft-file-selector.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/microsoft-file-selector.tsx @@ -24,6 +24,7 @@ import { parseProvider, } from '@/lib/oauth' import { OAuthRequiredModal } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/credential-selector/components/oauth-required-modal' +import { PlannerTask } from '@/tools/microsoft_planner/types' const logger = createLogger('MicrosoftFileSelector') @@ -40,6 +41,9 @@ export interface MicrosoftFileInfo { owners?: { displayName: string; emailAddress: string }[] } +// Union type for items that can be displayed in the file selector +type SelectableItem = MicrosoftFileInfo | PlannerTask + interface MicrosoftFileSelectorProps { value: string onChange: (value: string, fileInfo?: MicrosoftFileInfo) => void @@ -80,9 +84,9 @@ export function MicrosoftFileSelector({ const initialFetchRef = useRef(false) // Handle Microsoft Planner task selection - const [plannerTasks, setPlannerTasks] = useState([]) + const [plannerTasks, setPlannerTasks] = useState([]) const [isLoadingTasks, setIsLoadingTasks] = useState(false) - const [selectedTask, setSelectedTask] = useState(null) + const [selectedTask, setSelectedTask] = useState(null) // Determine the appropriate service ID based on provider and scopes const getServiceId = (): string => { @@ -197,7 +201,12 @@ export function MicrosoftFileSelector({ if (serviceId === 'onedrive') { endpoint = `/api/tools/onedrive/folder?${queryParams.toString()}` } else if (serviceId === 'sharepoint') { - endpoint = `/api/tools/sharepoint/site?${queryParams.toString()}` + // Change from fileId to siteId for SharePoint + const sharepointParams = new URLSearchParams({ + credentialId: selectedCredentialId, + siteId: fileId, // Use siteId instead of fileId + }) + endpoint = `/api/tools/sharepoint/site?${sharepointParams.toString()}` } else { endpoint = `/api/auth/oauth/microsoft/file?${queryParams.toString()}` } @@ -269,7 +278,7 @@ export function MicrosoftFileSelector({ const tasks = data.tasks || [] // Transform tasks to match file info format for consistency - const transformedTasks = tasks.map((task: any) => ({ + const transformedTasks = tasks.map((task: PlannerTask) => ({ id: task.id, name: task.title, mimeType: 'planner/task', @@ -336,12 +345,23 @@ export function MicrosoftFileSelector({ }, [selectedCredentialId, planId, serviceId, fetchPlannerTasks]) // Handle task selection for planner - const handleTaskSelect = (task: any) => { - setSelectedFileId(task.id) - setSelectedFile(task) + const handleTaskSelect = (task: PlannerTask) => { + const taskId = task.id || '' + // Convert PlannerTask to MicrosoftFileInfo format for compatibility + const taskAsFileInfo: MicrosoftFileInfo = { + id: taskId, + name: task.title, + mimeType: 'planner/task', + webViewLink: `https://tasks.office.com/planner/task/${taskId}`, + createdTime: task.createdDateTime, + modifiedTime: task.createdDateTime, + } + + setSelectedFileId(taskId) + setSelectedFile(taskAsFileInfo) setSelectedTask(task) - onChange(task.id, task) - onFileInfoChange?.(task) + onChange(taskId, taskAsFileInfo) + onFileInfoChange?.(taskAsFileInfo) setOpen(false) setSearchQuery('') } @@ -389,7 +409,10 @@ export function MicrosoftFileSelector({ selectedCredentialId && credentialsLoaded && !selectedFile && - !isLoadingSelectedFile + !isLoadingSelectedFile && + serviceId !== 'microsoft-planner' && + serviceId !== 'sharepoint' && + serviceId !== 'onedrive' ) { fetchFileById(value) } @@ -570,9 +593,13 @@ export function MicrosoftFileSelector({ } // Filter tasks based on search query for planner - const filteredTasks = + const filteredTasks: SelectableItem[] = serviceId === 'microsoft-planner' - ? plannerTasks.filter((task) => task.name.toLowerCase().includes(searchQuery.toLowerCase())) + ? plannerTasks.filter((task) => { + const title = task.title || '' + const query = searchQuery || '' + return title.toLowerCase().includes(query.toLowerCase()) + }) : availableFiles return ( @@ -701,30 +728,47 @@ export function MicrosoftFileSelector({
{getFileTypeTitleCase()}
- {filteredTasks.map((file) => ( - - serviceId === 'microsoft-planner' - ? handleTaskSelect(file) - : handleFileSelect(file) - } - > -
- {getFileIcon(file, 'sm')} -
- {file.name} - {file.modifiedTime && ( -
- Modified {new Date(file.modifiedTime).toLocaleDateString()} -
- )} + {filteredTasks.map((item) => { + const isPlanner = serviceId === 'microsoft-planner' + const isPlannerTask = isPlanner && 'title' in item + const plannerTask = item as PlannerTask + const fileInfo = item as MicrosoftFileInfo + + const displayName = isPlannerTask ? plannerTask.title : fileInfo.name + const dateField = isPlannerTask + ? plannerTask.createdDateTime + : fileInfo.createdTime + + return ( + + isPlannerTask + ? handleTaskSelect(plannerTask) + : handleFileSelect(fileInfo) + } + > +
+ {getFileIcon(isPlannerTask ? { + ...fileInfo, + id: plannerTask.id || '', + name: plannerTask.title, + mimeType: 'planner/task' + } : fileInfo, 'sm')} +
+ {displayName} + {dateField && ( +
+ Modified {new Date(dateField).toLocaleDateString()} +
+ )} +
-
- {file.id === selectedFileId && } - - ))} + {item.id === selectedFileId && } + + ) + })} )} diff --git a/apps/sim/blocks/blocks/microsoft_planner.ts b/apps/sim/blocks/blocks/microsoft_planner.ts index 8e424aefe1e..c231d9f4b42 100644 --- a/apps/sim/blocks/blocks/microsoft_planner.ts +++ b/apps/sim/blocks/blocks/microsoft_planner.ts @@ -2,6 +2,19 @@ import { MicrosoftPlannerIcon } from '@/components/icons' import type { BlockConfig } from '@/blocks/types' import type { MicrosoftPlannerResponse } from '@/tools/microsoft_planner/types' +interface MicrosoftPlannerBlockParams { + credential: string + accessToken?: string + planId?: string + taskId?: string + title?: string + description?: string + dueDateTime?: string + assigneeUserId?: string + bucketId?: string + [key: string]: string | number | boolean | undefined +} + export const MicrosoftPlannerBlock: BlockConfig = { type: 'microsoft_planner', name: 'Microsoft Planner', @@ -133,7 +146,7 @@ export const MicrosoftPlannerBlock: BlockConfig = { // For read operations if (operation === 'read_task') { - const readParams: any = { ...baseParams } + const readParams: MicrosoftPlannerBlockParams = { ...baseParams } // If taskId is provided, add it (highest priority - get specific task) if (taskId?.trim()) { @@ -157,7 +170,7 @@ export const MicrosoftPlannerBlock: BlockConfig = { throw new Error('Task title is required to create a task.') } - const createParams: any = { + const createParams: MicrosoftPlannerBlockParams = { ...baseParams, planId: planId.trim(), title: title.trim(), @@ -187,15 +200,15 @@ export const MicrosoftPlannerBlock: BlockConfig = { }, }, inputs: { - operation: { type: 'string', required: true }, - credential: { type: 'string', required: true }, - planId: { type: 'string', required: false }, - taskId: { type: 'string', required: false }, - title: { type: 'string', required: false }, - description: { type: 'string', required: false }, - dueDateTime: { type: 'string', required: false }, - assigneeUserId: { type: 'string', required: false }, - bucketId: { type: 'string', required: false }, + operation: { type: 'string', description: 'Operation to perform' }, + credential: { type: 'string', description: 'Microsoft account credential' }, + planId: { type: 'string', description: 'Plan ID' }, + taskId: { type: 'string', description: 'Task ID' }, + title: { type: 'string', description: 'Task title' }, + description: { type: 'string', description: 'Task description' }, + dueDateTime: { type: 'string', description: 'Due date' }, + assigneeUserId: { type: 'string', description: 'Assignee user ID' }, + bucketId: { type: 'string', description: 'Bucket ID' }, }, outputs: { task: 'json', diff --git a/apps/sim/blocks/blocks/onedrive.ts b/apps/sim/blocks/blocks/onedrive.ts index df359441fca..45aa910530c 100644 --- a/apps/sim/blocks/blocks/onedrive.ts +++ b/apps/sim/blocks/blocks/onedrive.ts @@ -26,7 +26,7 @@ export const OneDriveBlock: BlockConfig = { { label: 'List Files', id: 'list' }, ], }, - // Google Drive Credentials + // One Drive Credentials { id: 'credential', title: 'Microsoft Account', @@ -34,14 +34,7 @@ export const OneDriveBlock: BlockConfig = { layout: 'full', provider: 'onedrive', serviceId: 'onedrive', - requiredScopes: [ - 'openid', - 'profile', - 'email', - 'Files.Read', - 'Files.ReadWrite', - 'offline_access', - ], + requiredScopes: ['openid', 'profile', 'email','Files.Read', 'Files.ReadWrite', 'offline_access'], placeholder: 'Select Microsoft account', }, // Upload Fields @@ -61,7 +54,7 @@ export const OneDriveBlock: BlockConfig = { placeholder: 'Content to upload to the file', condition: { field: 'operation', value: 'upload' }, }, - + { id: 'folderSelector', title: 'Select Parent Folder', @@ -69,14 +62,7 @@ export const OneDriveBlock: BlockConfig = { layout: 'full', provider: 'microsoft', serviceId: 'onedrive', - requiredScopes: [ - 'openid', - 'profile', - 'email', - 'Files.Read', - 'Files.ReadWrite', - 'offline_access', - ], + requiredScopes: ['openid', 'profile', 'email', 'Files.Read', 'Files.ReadWrite', 'offline_access'], mimeType: 'application/vnd.microsoft.graph.folder', placeholder: 'Select a parent folder', mode: 'basic', @@ -92,7 +78,7 @@ export const OneDriveBlock: BlockConfig = { condition: { field: 'operation', value: 'upload' }, }, { - id: 'fileName', + id: 'folderName', title: 'Folder Name', type: 'short-input', layout: 'full', @@ -106,14 +92,7 @@ export const OneDriveBlock: BlockConfig = { layout: 'full', provider: 'microsoft', serviceId: 'onedrive', - requiredScopes: [ - 'openid', - 'profile', - 'email', - 'Files.Read', - 'Files.ReadWrite', - 'offline_access', - ], + requiredScopes: ['openid', 'profile', 'email', 'Files.Read', 'Files.ReadWrite', 'offline_access'], mimeType: 'application/vnd.microsoft.graph.folder', placeholder: 'Select a parent folder', mode: 'basic', @@ -137,14 +116,7 @@ export const OneDriveBlock: BlockConfig = { layout: 'full', provider: 'microsoft', serviceId: 'onedrive', - requiredScopes: [ - 'openid', - 'profile', - 'email', - 'Files.Read', - 'Files.ReadWrite', - 'offline_access', - ], + requiredScopes: ['openid', 'profile', 'email', 'Files.Read', 'Files.ReadWrite', 'offline_access'], mimeType: 'application/vnd.microsoft.graph.folder', placeholder: 'Select a folder to list files from', mode: 'basic', @@ -184,8 +156,6 @@ export const OneDriveBlock: BlockConfig = { switch (params.operation) { case 'upload': return 'onedrive_upload' - // case 'get_content': - // return 'google_drive_get_content' case 'create_folder': return 'onedrive_create_folder' case 'list': @@ -211,18 +181,18 @@ export const OneDriveBlock: BlockConfig = { }, }, inputs: { - operation: { type: 'string', required: true }, - credential: { type: 'string', required: true }, + operation: { type: 'string', description: 'Operation to perform' }, + credential: { type: 'string', description: 'Microsoft account credential' }, // Upload and Create Folder operation inputs - fileName: { type: 'string', required: false }, - content: { type: 'string', required: false }, + fileName: { type: 'string', description: 'File name' }, + content: { type: 'string', description: 'File content' }, // Get Content operation inputs // fileId: { type: 'string', required: false }, // List operation inputs - folderSelector: { type: 'string', required: false }, - manualFolderId: { type: 'string', required: false }, - query: { type: 'string', required: false }, - pageSize: { type: 'number', required: false }, + folderSelector: { type: 'string', description: 'Folder selector' }, + manualFolderId: { type: 'string', description: 'Manual folder ID' }, + query: { type: 'string', description: 'Search query' }, + pageSize: { type: 'number', description: 'Results per page' }, }, outputs: { file: 'json', diff --git a/apps/sim/blocks/blocks/sharepoint.ts b/apps/sim/blocks/blocks/sharepoint.ts index d3c63c14e7d..fce6d760496 100644 --- a/apps/sim/blocks/blocks/sharepoint.ts +++ b/apps/sim/blocks/blocks/sharepoint.ts @@ -25,7 +25,7 @@ export const SharepointBlock: BlockConfig = { { label: 'List Sites', id: 'list_sites' }, ], }, - // Google Drive Credentials + // Sharepoint Credentials { id: 'credential', title: 'Microsoft Account', @@ -100,17 +100,9 @@ export const SharepointBlock: BlockConfig = { layout: 'full', placeholder: 'Enter site ID (leave empty for root site)', mode: 'advanced', - condition: { field: 'operation', value: 'upload' }, + condition: { field: 'operation', value: 'create_page' }, }, - { - id: 'query', - title: 'Search Query', - type: 'short-input', - layout: 'full', - placeholder: 'Search for specific pages (e.g., name contains "report")', - condition: { field: 'operation', value: 'list_pages' }, - }, ], tools: { access: ['sharepoint_create_page', 'sharepoint_read_page', 'sharepoint_list_sites'], @@ -144,19 +136,18 @@ export const SharepointBlock: BlockConfig = { }, }, inputs: { - operation: { type: 'string', required: true }, - credential: { type: 'string', required: true }, + operation: { type: 'string', description: 'Operation to perform' }, + credential: { type: 'string', description: 'Microsoft account credential' }, // Create Page operation inputs - pageName: { type: 'string', required: false }, - pageContent: { type: 'string', required: false }, - pageTitle: { type: 'string', required: false }, + pageName: { type: 'string', description: 'Page name' }, + pageContent: { type: 'string', description: 'Page content' }, + pageTitle: { type: 'string', description: 'Page title' }, // Read Page operation inputs - pageId: { type: 'string', required: false }, + pageId: { type: 'string', description: 'Page ID' }, // List operation inputs - siteSelector: { type: 'string', required: false }, - manualSiteId: { type: 'string', required: false }, - query: { type: 'string', required: false }, - pageSize: { type: 'number', required: false }, + siteSelector: { type: 'string', description: 'Site selector' }, + manualSiteId: { type: 'string', description: 'Manual site ID' }, + pageSize: { type: 'number', description: 'Results per page' }, }, outputs: { sites: 'json', diff --git a/apps/sim/blocks/registry.ts b/apps/sim/blocks/registry.ts index c9ada0f7c05..9cde501eb06 100644 --- a/apps/sim/blocks/registry.ts +++ b/apps/sim/blocks/registry.ts @@ -110,7 +110,6 @@ export const registry: Record = { microsoft_excel: MicrosoftExcelBlock, microsoft_planner: MicrosoftPlannerBlock, microsoft_teams: MicrosoftTeamsBlock, - sharepoint: SharepointBlock, mistral_parse: MistralParseBlock, notion: NotionBlock, openai: OpenAIBlock, @@ -126,6 +125,7 @@ export const registry: Record = { schedule: ScheduleBlock, s3: S3Block, serper: SerperBlock, + sharepoint: SharepointBlock, stagehand: StagehandBlock, stagehand_agent: StagehandAgentBlock, slack: SlackBlock, diff --git a/apps/sim/lib/oauth/oauth.ts b/apps/sim/lib/oauth/oauth.ts index 264a048de3e..03010ea2788 100644 --- a/apps/sim/lib/oauth/oauth.ts +++ b/apps/sim/lib/oauth/oauth.ts @@ -523,6 +523,8 @@ export function getServiceIdFromScopes(provider: OAuthProvider, scopes: string[] return 'sharepoint' } else if (provider === 'microsoft-planner') { return 'microsoft-planner' + } else if (provider === 'onedrive') { + return 'onedrive' } else if (provider === 'github') { return 'github' } else if (provider === 'supabase') { diff --git a/apps/sim/tools/microsoft_planner/create_task.ts b/apps/sim/tools/microsoft_planner/create_task.ts index df6880d3170..8a73cc03f8f 100644 --- a/apps/sim/tools/microsoft_planner/create_task.ts +++ b/apps/sim/tools/microsoft_planner/create_task.ts @@ -2,6 +2,7 @@ import { createLogger } from '@/lib/logs/console/logger' import type { MicrosoftPlannerCreateResponse, MicrosoftPlannerToolParams, + PlannerTask, } from '@/tools/microsoft_planner/types' import type { ToolConfig } from '@/tools/types' @@ -85,7 +86,7 @@ export const createTaskTool: ToolConfig< throw new Error('Task title is required') } - const body: any = { + const body: PlannerTask = { planId: params.planId, title: params.title, } @@ -130,12 +131,11 @@ export const createTaskTool: ToolConfig< const detailsUrl = `https://graph.microsoft.com/v1.0/planner/tasks/${task.id}/details` // Get task details to get the ETag const getDetailsResponse = await fetch( - `https://graph.microsoft.com/v1.0/planner/tasks/${task.id}/details`, + detailsUrl, { headers: { Authorization: `Bearer ${params.accessToken}` }, } ) - const detailsData = await getDetailsResponse.json() const etag = getDetailsResponse.headers.get('ETag') // Then update with correct ETag diff --git a/apps/sim/tools/microsoft_planner/read_task.ts b/apps/sim/tools/microsoft_planner/read_task.ts index 240ee4baf2f..fa6a23b670a 100644 --- a/apps/sim/tools/microsoft_planner/read_task.ts +++ b/apps/sim/tools/microsoft_planner/read_task.ts @@ -86,8 +86,8 @@ export const readTaskTool: ToolConfig ({ diff --git a/apps/sim/tools/microsoft_planner/types.ts b/apps/sim/tools/microsoft_planner/types.ts index ab5ce1db97f..01aea1632f1 100644 --- a/apps/sim/tools/microsoft_planner/types.ts +++ b/apps/sim/tools/microsoft_planner/types.ts @@ -1,5 +1,46 @@ import type { ToolResponse } from '@/tools/types' +export interface PlannerIdentitySet { + user?: { + displayName?: string + id?: string + } + application?: { + displayName?: string + id?: string + } +} + +export interface PlannerAssignment { + '@odata.type': string + assignedDateTime?: string + orderHint?: string + assignedBy?: PlannerIdentitySet +} + +export interface PlannerReference { + alias?: string + lastModifiedBy?: PlannerIdentitySet + lastModifiedDateTime?: string + previewPriority?: string + type?: string +} + +export interface PlannerChecklistItem { + '@odata.type': string + isChecked?: boolean + title?: string + orderHint?: string + lastModifiedBy?: PlannerIdentitySet + lastModifiedDateTime?: string +} + +export interface PlannerContainer { + containerId?: string + type?: string + url?: string +} + export interface PlannerTask { id?: string planId: string @@ -13,18 +54,18 @@ export interface PlannerTask { hasDescription?: boolean previewType?: string completedDateTime?: string - completedBy?: any + completedBy?: PlannerIdentitySet referenceCount?: number checklistItemCount?: number activeChecklistItemCount?: number conversationThreadId?: string priority?: number - assignments?: Record + assignments?: Record bucketId?: string details?: { description?: string - references?: Record - checklist?: Record + references?: Record + checklist?: Record } } @@ -33,7 +74,7 @@ export interface PlannerPlan { title: string owner?: string createdDateTime?: string - container?: any + container?: PlannerContainer } export interface MicrosoftPlannerMetadata { diff --git a/apps/sim/tools/onedrive/create_folder.ts b/apps/sim/tools/onedrive/create_folder.ts index b106b5f8be4..8b8938dcdbe 100644 --- a/apps/sim/tools/onedrive/create_folder.ts +++ b/apps/sim/tools/onedrive/create_folder.ts @@ -18,7 +18,7 @@ export const createFolderTool: ToolConfig = { id: 'onedrive_list', name: 'List OneDrive Files', @@ -82,11 +103,17 @@ export const listTool: ToolConfig = { url.searchParams.append('$top', params.pageSize.toString()) } + // Remove the $skip logic entirely. Instead, use the full nextLink URL if provided + let finalUrl: string if (params.pageToken) { - url.searchParams.append('$skip', params.pageToken) + // pageToken should contain the full @odata.nextLink URL + finalUrl = params.pageToken + } else { + // Construct the initial request URL + finalUrl = url.toString() } - return url.toString() + return finalUrl }, method: 'GET', headers: (params) => ({ @@ -103,7 +130,7 @@ export const listTool: ToolConfig = { return { success: true, output: { - files: data.value.map((item: any) => ({ + files: data.value.map((item: MicrosoftGraphDriveItem) => ({ id: item.id, name: item.name, mimeType: item.file?.mimeType || (item.folder ? 'application/folder' : 'unknown'), @@ -114,7 +141,8 @@ export const listTool: ToolConfig = { modifiedTime: item.lastModifiedDateTime, parents: item.parentReference ? [item.parentReference.id] : [], })), - nextPageToken: data['@odata.nextLink'] ? 'has_more' : undefined, + // Use the actual @odata.nextLink URL as the continuation token + nextPageToken: data['@odata.nextLink'] || undefined, }, } }, diff --git a/apps/sim/tools/sharepoint/create_page.ts b/apps/sim/tools/sharepoint/create_page.ts index f139815199f..cda061b114c 100644 --- a/apps/sim/tools/sharepoint/create_page.ts +++ b/apps/sim/tools/sharepoint/create_page.ts @@ -1,5 +1,5 @@ import { createLogger } from '@/lib/logs/console/logger' -import type { SharepointCreatePageResponse, SharepointToolParams } from '@/tools/sharepoint/types' +import type { SharepointCreatePageResponse, SharepointPage, SharepointToolParams } from '@/tools/sharepoint/types' import type { ToolConfig } from '@/tools/types' const logger = createLogger('SharePointCreatePage') @@ -72,7 +72,7 @@ export const createPageTool: ToolConfig ({ + sites: data.value.map((site: SharepointSite) => ({ id: site.id, name: site.name, displayName: site.displayName, From f32003de05ebe3725b1722a7f9f388690a016fd9 Mon Sep 17 00:00:00 2001 From: Adam Gough Date: Fri, 1 Aug 2025 13:26:56 -0700 Subject: [PATCH 13/23] bun run lint --- .../components/microsoft-file-selector.tsx | 31 ++++++++------- apps/sim/blocks/blocks/onedrive.ts | 38 ++++++++++++++++--- apps/sim/blocks/blocks/sharepoint.ts | 1 - .../tools/microsoft_planner/create_task.ts | 9 ++--- apps/sim/tools/microsoft_planner/read_task.ts | 2 +- apps/sim/tools/sharepoint/create_page.ts | 6 ++- 6 files changed, 60 insertions(+), 27 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/microsoft-file-selector.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/microsoft-file-selector.tsx index ca1c68c0bcd..35c956a48c7 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/microsoft-file-selector.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/microsoft-file-selector.tsx @@ -24,7 +24,7 @@ import { parseProvider, } from '@/lib/oauth' import { OAuthRequiredModal } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/credential-selector/components/oauth-required-modal' -import { PlannerTask } from '@/tools/microsoft_planner/types' +import type { PlannerTask } from '@/tools/microsoft_planner/types' const logger = createLogger('MicrosoftFileSelector') @@ -204,7 +204,7 @@ export function MicrosoftFileSelector({ // Change from fileId to siteId for SharePoint const sharepointParams = new URLSearchParams({ credentialId: selectedCredentialId, - siteId: fileId, // Use siteId instead of fileId + siteId: fileId, // Use siteId instead of fileId }) endpoint = `/api/tools/sharepoint/site?${sharepointParams.toString()}` } else { @@ -356,7 +356,7 @@ export function MicrosoftFileSelector({ createdTime: task.createdDateTime, modifiedTime: task.createdDateTime, } - + setSelectedFileId(taskId) setSelectedFile(taskAsFileInfo) setSelectedTask(task) @@ -733,12 +733,12 @@ export function MicrosoftFileSelector({ const isPlannerTask = isPlanner && 'title' in item const plannerTask = item as PlannerTask const fileInfo = item as MicrosoftFileInfo - + const displayName = isPlannerTask ? plannerTask.title : fileInfo.name - const dateField = isPlannerTask - ? plannerTask.createdDateTime + const dateField = isPlannerTask + ? plannerTask.createdDateTime : fileInfo.createdTime - + return (
- {getFileIcon(isPlannerTask ? { - ...fileInfo, - id: plannerTask.id || '', - name: plannerTask.title, - mimeType: 'planner/task' - } : fileInfo, 'sm')} + {getFileIcon( + isPlannerTask + ? { + ...fileInfo, + id: plannerTask.id || '', + name: plannerTask.title, + mimeType: 'planner/task', + } + : fileInfo, + 'sm' + )}
{displayName} {dateField && ( diff --git a/apps/sim/blocks/blocks/onedrive.ts b/apps/sim/blocks/blocks/onedrive.ts index 45aa910530c..a1bbff332e6 100644 --- a/apps/sim/blocks/blocks/onedrive.ts +++ b/apps/sim/blocks/blocks/onedrive.ts @@ -34,7 +34,14 @@ export const OneDriveBlock: BlockConfig = { layout: 'full', provider: 'onedrive', serviceId: 'onedrive', - requiredScopes: ['openid', 'profile', 'email','Files.Read', 'Files.ReadWrite', 'offline_access'], + requiredScopes: [ + 'openid', + 'profile', + 'email', + 'Files.Read', + 'Files.ReadWrite', + 'offline_access', + ], placeholder: 'Select Microsoft account', }, // Upload Fields @@ -54,7 +61,7 @@ export const OneDriveBlock: BlockConfig = { placeholder: 'Content to upload to the file', condition: { field: 'operation', value: 'upload' }, }, - + { id: 'folderSelector', title: 'Select Parent Folder', @@ -62,7 +69,14 @@ export const OneDriveBlock: BlockConfig = { layout: 'full', provider: 'microsoft', serviceId: 'onedrive', - requiredScopes: ['openid', 'profile', 'email', 'Files.Read', 'Files.ReadWrite', 'offline_access'], + requiredScopes: [ + 'openid', + 'profile', + 'email', + 'Files.Read', + 'Files.ReadWrite', + 'offline_access', + ], mimeType: 'application/vnd.microsoft.graph.folder', placeholder: 'Select a parent folder', mode: 'basic', @@ -92,7 +106,14 @@ export const OneDriveBlock: BlockConfig = { layout: 'full', provider: 'microsoft', serviceId: 'onedrive', - requiredScopes: ['openid', 'profile', 'email', 'Files.Read', 'Files.ReadWrite', 'offline_access'], + requiredScopes: [ + 'openid', + 'profile', + 'email', + 'Files.Read', + 'Files.ReadWrite', + 'offline_access', + ], mimeType: 'application/vnd.microsoft.graph.folder', placeholder: 'Select a parent folder', mode: 'basic', @@ -116,7 +137,14 @@ export const OneDriveBlock: BlockConfig = { layout: 'full', provider: 'microsoft', serviceId: 'onedrive', - requiredScopes: ['openid', 'profile', 'email', 'Files.Read', 'Files.ReadWrite', 'offline_access'], + requiredScopes: [ + 'openid', + 'profile', + 'email', + 'Files.Read', + 'Files.ReadWrite', + 'offline_access', + ], mimeType: 'application/vnd.microsoft.graph.folder', placeholder: 'Select a folder to list files from', mode: 'basic', diff --git a/apps/sim/blocks/blocks/sharepoint.ts b/apps/sim/blocks/blocks/sharepoint.ts index fce6d760496..1eb46a4b716 100644 --- a/apps/sim/blocks/blocks/sharepoint.ts +++ b/apps/sim/blocks/blocks/sharepoint.ts @@ -102,7 +102,6 @@ export const SharepointBlock: BlockConfig = { mode: 'advanced', condition: { field: 'operation', value: 'create_page' }, }, - ], tools: { access: ['sharepoint_create_page', 'sharepoint_read_page', 'sharepoint_list_sites'], diff --git a/apps/sim/tools/microsoft_planner/create_task.ts b/apps/sim/tools/microsoft_planner/create_task.ts index 8a73cc03f8f..03ce779f1f4 100644 --- a/apps/sim/tools/microsoft_planner/create_task.ts +++ b/apps/sim/tools/microsoft_planner/create_task.ts @@ -130,12 +130,9 @@ export const createTaskTool: ToolConfig< try { const detailsUrl = `https://graph.microsoft.com/v1.0/planner/tasks/${task.id}/details` // Get task details to get the ETag - const getDetailsResponse = await fetch( - detailsUrl, - { - headers: { Authorization: `Bearer ${params.accessToken}` }, - } - ) + const getDetailsResponse = await fetch(detailsUrl, { + headers: { Authorization: `Bearer ${params.accessToken}` }, + }) const etag = getDetailsResponse.headers.get('ETag') // Then update with correct ETag diff --git a/apps/sim/tools/microsoft_planner/read_task.ts b/apps/sim/tools/microsoft_planner/read_task.ts index fa6a23b670a..eff8bb33a87 100644 --- a/apps/sim/tools/microsoft_planner/read_task.ts +++ b/apps/sim/tools/microsoft_planner/read_task.ts @@ -87,7 +87,7 @@ export const readTaskTool: ToolConfig ({ diff --git a/apps/sim/tools/sharepoint/create_page.ts b/apps/sim/tools/sharepoint/create_page.ts index cda061b114c..ae71375dc40 100644 --- a/apps/sim/tools/sharepoint/create_page.ts +++ b/apps/sim/tools/sharepoint/create_page.ts @@ -1,5 +1,9 @@ import { createLogger } from '@/lib/logs/console/logger' -import type { SharepointCreatePageResponse, SharepointPage, SharepointToolParams } from '@/tools/sharepoint/types' +import type { + SharepointCreatePageResponse, + SharepointPage, + SharepointToolParams, +} from '@/tools/sharepoint/types' import type { ToolConfig } from '@/tools/types' const logger = createLogger('SharePointCreatePage') From 81264cb2390dd886a85cafefc73d1ab7664df255 Mon Sep 17 00:00:00 2001 From: Adam Gough Date: Fri, 1 Aug 2025 14:12:51 -0700 Subject: [PATCH 14/23] fix #840 --- apps/sim/blocks/blocks/sharepoint.ts | 2 +- apps/sim/tools/sharepoint/read_page.ts | 283 ++++++++++++++++++------- apps/sim/tools/sharepoint/types.ts | 41 +++- 3 files changed, 247 insertions(+), 79 deletions(-) diff --git a/apps/sim/blocks/blocks/sharepoint.ts b/apps/sim/blocks/blocks/sharepoint.ts index 1eb46a4b716..57f85f5bf24 100644 --- a/apps/sim/blocks/blocks/sharepoint.ts +++ b/apps/sim/blocks/blocks/sharepoint.ts @@ -70,7 +70,7 @@ export const SharepointBlock: BlockConfig = { title: 'Page Name', type: 'short-input', layout: 'full', - placeholder: 'Name for the new page', + placeholder: 'Name of the page', condition: { field: 'operation', value: ['create_page', 'read_page'] }, }, diff --git a/apps/sim/tools/sharepoint/read_page.ts b/apps/sim/tools/sharepoint/read_page.ts index 25bf56ef839..d2b4cc4c1e3 100644 --- a/apps/sim/tools/sharepoint/read_page.ts +++ b/apps/sim/tools/sharepoint/read_page.ts @@ -1,11 +1,59 @@ import { createLogger } from '@/lib/logs/console/logger' -import type { SharepointReadPageResponse, SharepointToolParams } from '@/tools/sharepoint/types' +import type { + SharepointPageContent, + SharepointReadPageResponse, + SharepointToolParams, +} from '@/tools/sharepoint/types' import type { ToolConfig } from '@/tools/types' const logger = createLogger('SharePointReadPage') +// Types for API responses +interface GraphApiResponse { + id?: string + name?: string + title?: string + webUrl?: string + pageLayout?: string + createdDateTime?: string + lastModifiedDateTime?: string + canvasLayout?: CanvasLayout + value?: GraphApiPageItem[] + error?: { + message: string + } +} + +interface GraphApiPageItem { + id: string + name: string + title?: string + webUrl?: string + pageLayout?: string + createdDateTime?: string + lastModifiedDateTime?: string +} + +interface CanvasLayout { + horizontalSections?: Array<{ + layout?: string + id?: string + emphasis?: string + columns?: Array<{ + webparts?: Array<{ + id?: string + innerHtml?: string + }> + }> + webparts?: Array<{ + id?: string + innerHtml?: string + }> + }> +} + // Extract readable text from SharePoint canvas layout -function extractTextFromCanvasLayout(canvasLayout: any): string { +function extractTextFromCanvasLayout(canvasLayout: CanvasLayout | null | undefined): string { logger.info('Extracting text from canvas layout', { hasCanvasLayout: !!canvasLayout, hasHorizontalSections: !!canvasLayout?.horizontalSections, @@ -69,22 +117,22 @@ function extractTextFromCanvasLayout(canvasLayout: any): string { } // Remove OData metadata from objects -function cleanODataMetadata(obj: any): any { +function cleanODataMetadata(obj: T): T { if (!obj || typeof obj !== 'object') return obj if (Array.isArray(obj)) { - return obj.map((item) => cleanODataMetadata(item)) + return obj.map((item) => cleanODataMetadata(item)) as T } - const cleaned: any = {} - for (const [key, value] of Object.entries(obj)) { + const cleaned: Record = {} + for (const [key, value] of Object.entries(obj as Record)) { // Skip OData metadata keys if (key.includes('@odata')) continue cleaned[key] = cleanODataMetadata(value) } - return cleaned + return cleaned as T } export const readPageTool: ToolConfig = { @@ -128,14 +176,16 @@ export const readPageTool: ToolConfig { - // Validate that at least pageId or pageName is provided - if (!params.pageId && !params.pageName) { - throw new Error('Either pageId or pageName must be provided') - } - // Use specific site if provided, otherwise use root site const siteId = params.siteId || params.siteSelector || 'root' @@ -143,11 +193,9 @@ export const readPageTool: ToolConfig { - const data = await response.json() + const data: GraphApiResponse = await response.json() if (!response.ok) { logger.error('SharePoint API error', { @@ -214,48 +266,112 @@ export const readPageTool: ToolConfig ({ id: p.id, name: p.name, title: p.title })), - selectedPage: data.value[0].name, + siteId: params?.siteId || params?.siteSelector || 'root', + totalResults: data.value?.length || 0, }) + const errorMessage = params?.pageName + ? `Page with name '${params?.pageName}' not found. Make sure the page exists and you have access to it. Note: SharePoint page names typically include the .aspx extension.` + : 'No pages found on this SharePoint site.' + throw new Error(errorMessage) + } - pageData = data.value[0] + logger.info('Found pages', { + searchName: params?.pageName, + foundPages: data.value.map((p: any) => ({ id: p.id, name: p.name, title: p.title })), + totalCount: data.value.length, + }) + + if (params?.pageName) { + // Search by name - return single page (first match) + const pageData = data.value[0] + const siteId = params?.siteId || params?.siteSelector || 'root' + const contentUrl = `https://graph.microsoft.com/v1.0/sites/${siteId}/pages/${pageData.id}/microsoft.graph.sitePage?$expand=canvasLayout` - // For search results, we need to make another call to get the content - if (pageData.id) { - const siteId = params?.siteId || params?.siteSelector || 'root' - const contentUrl = `https://graph.microsoft.com/v1.0/sites/${siteId}/pages/${pageData.id}/microsoft.graph.sitePage?$expand=canvasLayout` + logger.info('Making API call to get page content for searched page', { + pageId: pageData.id, + contentUrl, + siteId, + }) + + const contentResponse = await fetch(contentUrl, { + headers: { + Authorization: `Bearer ${params?.accessToken}`, + Accept: 'application/json', + }, + }) - logger.info('Making second API call to get page content', { - pageId: pageData.id, - contentUrl, - siteId, + let contentData: SharepointPageContent = { content: '' } + if (contentResponse.ok) { + const contentResult = await contentResponse.json() + contentData = { + content: extractTextFromCanvasLayout(contentResult.canvasLayout), + canvasLayout: cleanODataMetadata(contentResult.canvasLayout), + } + } else { + logger.error('Failed to fetch page content', { + status: contentResponse.status, + statusText: contentResponse.statusText, }) + } + return { + success: true, + output: { + page: { + id: pageData.id, + name: pageData.name, + title: pageData.title || pageData.name, + webUrl: pageData.webUrl, + pageLayout: pageData.pageLayout, + createdDateTime: pageData.createdDateTime, + lastModifiedDateTime: pageData.lastModifiedDateTime, + }, + content: contentData, + }, + } + } + // List all pages - return multiple pages with content + const siteId = params?.siteId || params?.siteSelector || 'root' + const pagesWithContent = [] + + logger.info('Fetching content for all pages', { + totalPages: data.value.length, + siteId, + }) + + // Fetch content for each page + for (const pageInfo of data.value) { + const contentUrl = `https://graph.microsoft.com/v1.0/sites/${siteId}/pages/${pageInfo.id}/microsoft.graph.sitePage?$expand=canvasLayout` + + try { const contentResponse = await fetch(contentUrl, { headers: { Authorization: `Bearer ${params?.accessToken}`, @@ -263,47 +379,68 @@ export const readPageTool: ToolConfig p.content.content !== 'Failed to fetch content' + ).length, + }) + return { success: true, output: { - page: { - id: pageData.id, - name: pageData.name, - title: pageData.title || pageData.name, - webUrl: pageData.webUrl, - pageLayout: pageData.pageLayout, - createdDateTime: pageData.createdDateTime, - lastModifiedDateTime: pageData.lastModifiedDateTime, - }, - content: contentData, + pages: pagesWithContent, + totalPages: pagesWithContent.length, }, } }, diff --git a/apps/sim/tools/sharepoint/types.ts b/apps/sim/tools/sharepoint/types.ts index 910369f3955..6ef14e1c511 100644 --- a/apps/sim/tools/sharepoint/types.ts +++ b/apps/sim/tools/sharepoint/types.ts @@ -11,13 +11,36 @@ export interface SharepointSite { } export interface SharepointPage { - id: string + '@odata.type'?: string + id?: string name: string title: string - webUrl: string + webUrl?: string pageLayout?: string createdDateTime?: string lastModifiedDateTime?: string + publishingState?: { + level: string + } + canvasLayout?: { + horizontalSections: Array<{ + layout: string + id: string + emphasis: string + columns?: Array<{ + id: string + width: number + webparts: Array<{ + id: string + innerHtml: string + }> + }> + webparts?: Array<{ + id: string + innerHtml: string + }> + }> + } } export interface SharepointPageContent { @@ -32,7 +55,7 @@ export interface SharepointPageContent { innerHtml: string }> }> - } + } | null } export interface SharepointListSitesResponse extends ToolResponse { @@ -48,10 +71,17 @@ export interface SharepointCreatePageResponse extends ToolResponse { } } +export interface SharepointPageWithContent { + page: SharepointPage + content: SharepointPageContent +} + export interface SharepointReadPageResponse extends ToolResponse { output: { - page: SharepointPage - content: SharepointPageContent + page?: SharepointPage + pages?: SharepointPageWithContent[] + content?: SharepointPageContent + totalPages?: number } } @@ -100,6 +130,7 @@ export interface SharepointToolParams { hostname?: string serverRelativePath?: string groupId?: string + maxPages?: number } export type SharepointResponse = From 4356f07348565e7dd16707dbcffe9d38f8a03ac2 Mon Sep 17 00:00:00 2001 From: Adam Gough Date: Fri, 1 Aug 2025 16:47:09 -0700 Subject: [PATCH 15/23] added docs #840 --- apps/docs/content/docs/tools/meta.json | 5 +- .../content/docs/tools/microsoft_planner.mdx | 164 ++++++++++++++++++ apps/docs/content/docs/tools/onedrive.mdx | 110 ++++++++++++ apps/docs/content/docs/tools/sharepoint.mdx | 115 ++++++++++++ 4 files changed, 393 insertions(+), 1 deletion(-) create mode 100644 apps/docs/content/docs/tools/microsoft_planner.mdx create mode 100644 apps/docs/content/docs/tools/onedrive.mdx create mode 100644 apps/docs/content/docs/tools/sharepoint.mdx diff --git a/apps/docs/content/docs/tools/meta.json b/apps/docs/content/docs/tools/meta.json index db0fc6c0753..3347e27cb1d 100644 --- a/apps/docs/content/docs/tools/meta.json +++ b/apps/docs/content/docs/tools/meta.json @@ -29,9 +29,11 @@ "mem0", "memory", "microsoft_excel", + "microsoft_planner", "microsoft_teams", "mistral_parse", "notion", + "onedrive", "openai", "outlook", "perplexity", @@ -41,6 +43,7 @@ "s3", "schedule", "serper", + "sharepoint", "slack", "stagehand", "stagehand_agent", @@ -59,4 +62,4 @@ "x", "youtube" ] -} +} \ No newline at end of file diff --git a/apps/docs/content/docs/tools/microsoft_planner.mdx b/apps/docs/content/docs/tools/microsoft_planner.mdx new file mode 100644 index 00000000000..059978b3f9b --- /dev/null +++ b/apps/docs/content/docs/tools/microsoft_planner.mdx @@ -0,0 +1,164 @@ +--- +title: Microsoft Planner +description: Read and create tasks in Microsoft Planner +--- + +import { BlockInfoCard } from "@/components/ui/block-info-card" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + `} +/> + +## Usage Instructions + +Integrate Microsoft Planner functionality to manage tasks. Read all user tasks, tasks from specific plans, individual tasks, or create new tasks with various properties like title, description, due date, and assignees using OAuth authentication. + + + +## Tools + +### `microsoft_planner_read_task` + +Read tasks from Microsoft Planner - get all user tasks or all tasks from a specific plan + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `accessToken` | string | Yes | The access token for the Microsoft Planner API | +| `planId` | string | No | The ID of the plan to get tasks from \(if not provided, gets all user tasks\) | +| `taskId` | string | No | The ID of the task to get | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `task` | json | task output from the block | +| `metadata` | json | metadata output from the block | + +### `microsoft_planner_create_task` + +Create a new task in Microsoft Planner + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `accessToken` | string | Yes | The access token for the Microsoft Planner API | +| `planId` | string | Yes | The ID of the plan where the task will be created | +| `title` | string | Yes | The title of the task | +| `description` | string | No | The description of the task | +| `dueDateTime` | string | No | The due date and time for the task \(ISO 8601 format\) | +| `assigneeUserId` | string | No | The user ID to assign the task to | +| `bucketId` | string | No | The bucket ID to place the task in | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `task` | json | task output from the block | +| `metadata` | json | metadata output from the block | + + + +## Notes + +- Category: `tools` +- Type: `microsoft_planner` diff --git a/apps/docs/content/docs/tools/onedrive.mdx b/apps/docs/content/docs/tools/onedrive.mdx new file mode 100644 index 00000000000..5bc7181b8ef --- /dev/null +++ b/apps/docs/content/docs/tools/onedrive.mdx @@ -0,0 +1,110 @@ +--- +title: OneDrive +description: Create, upload, and list files +--- + +import { BlockInfoCard } from "@/components/ui/block-info-card" + + + + + + + + + `} +/> + +## Usage Instructions + +Integrate OneDrive functionality to manage files and folders. Upload new files, create new folders, and list contents of folders using OAuth authentication. Supports file operations with custom MIME types and folder organization. + + + +## Tools + +### `onedrive_upload` + +Upload a file to OneDrive + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `accessToken` | string | Yes | The access token for the OneDrive API | +| `fileName` | string | Yes | The name of the file to upload | +| `content` | string | Yes | The content of the file to upload | +| `folderSelector` | string | No | Select the folder to upload the file to | +| `folderId` | string | No | The ID of the folder to upload the file to \(internal use\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `file` | json | file output from the block | +| `files` | json | files output from the block | + +### `onedrive_create_folder` + +Create a new folder in OneDrive + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `accessToken` | string | Yes | The access token for the OneDrive API | +| `folderName` | string | Yes | Name of the folder to create | +| `folderSelector` | string | No | Select the parent folder to create the folder in | +| `folderId` | string | No | ID of the parent folder \(internal use\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `file` | json | file output from the block | +| `files` | json | files output from the block | + +### `onedrive_list` + +List files and folders in OneDrive + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `accessToken` | string | Yes | The access token for the OneDrive API | +| `folderSelector` | string | No | Select the folder to list files from | +| `folderId` | string | No | The ID of the folder to list files from \(internal use\) | +| `query` | string | No | A query to filter the files | +| `pageSize` | number | No | The number of files to return | +| `pageToken` | string | No | The page token to use for pagination | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `file` | json | file output from the block | +| `files` | json | files output from the block | + + + +## Notes + +- Category: `tools` +- Type: `onedrive` diff --git a/apps/docs/content/docs/tools/sharepoint.mdx b/apps/docs/content/docs/tools/sharepoint.mdx new file mode 100644 index 00000000000..c4deada3d59 --- /dev/null +++ b/apps/docs/content/docs/tools/sharepoint.mdx @@ -0,0 +1,115 @@ +--- +title: Sharepoint +description: Read and create pages +--- + +import { BlockInfoCard } from "@/components/ui/block-info-card" + + + + + + + + + + + + `} +/> + +## Usage Instructions + +Integrate Sharepoint functionality to manage pages. Read and create pages, and list sites using OAuth authentication. Supports page operations with custom MIME types and folder organization. + + + +## Tools + +### `sharepoint_create_page` + +Create a new page in a SharePoint site + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `accessToken` | string | Yes | The access token for the SharePoint API | +| `siteId` | string | No | The ID of the SharePoint site \(internal use\) | +| `siteSelector` | string | No | Select the SharePoint site | +| `pageName` | string | Yes | The name of the page to create | +| `pageTitle` | string | No | The title of the page \(defaults to page name if not provided\) | +| `pageContent` | string | No | The content of the page | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `sites` | json | sites output from the block | + +### `sharepoint_read_page` + +Read a specific page from a SharePoint site + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `accessToken` | string | Yes | The access token for the SharePoint API | +| `siteSelector` | string | No | Select the SharePoint site | +| `siteId` | string | No | The ID of the SharePoint site \(internal use\) | +| `pageId` | string | No | The ID of the page to read | +| `pageName` | string | No | The name of the page to read \(alternative to pageId\) | +| `maxPages` | number | No | Maximum number of pages to return when listing all pages \(default: 10, max: 50\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `sites` | json | sites output from the block | + +### `sharepoint_list_sites` + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `sites` | json | sites output from the block | + + + +## Notes + +- Category: `tools` +- Type: `sharepoint` From a819ed607aa82b0c3b9a93d81653b891c11bf49f Mon Sep 17 00:00:00 2001 From: Adam Gough Date: Fri, 1 Aug 2025 16:49:26 -0700 Subject: [PATCH 16/23] bun run lint #840 --- apps/docs/content/docs/tools/meta.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/docs/content/docs/tools/meta.json b/apps/docs/content/docs/tools/meta.json index 3347e27cb1d..b4ba8f9277f 100644 --- a/apps/docs/content/docs/tools/meta.json +++ b/apps/docs/content/docs/tools/meta.json @@ -62,4 +62,4 @@ "x", "youtube" ] -} \ No newline at end of file +} From 2b6a1ce4d423136d306eed70e5b287844ed11ea3 Mon Sep 17 00:00:00 2001 From: Adam Gough Date: Fri, 1 Aug 2025 18:35:16 -0700 Subject: [PATCH 17/23] removed unnecessary logic #840 --- .../tools/microsoft_planner/create_task.ts | 12 +- apps/sim/tools/microsoft_planner/read_task.ts | 4 +- apps/sim/tools/onedrive/get_content.ts | 0 apps/sim/tools/onedrive/index.ts | 2 - apps/sim/tools/onedrive/list.ts | 2 +- apps/sim/tools/onedrive/upload.ts | 132 ------------------ apps/sim/tools/registry.ts | 3 +- apps/sim/tools/sharepoint/create_page.ts | 6 +- apps/sim/tools/sharepoint/read_page.ts | 6 +- apps/sim/tools/sharepoint/read_site.ts | 8 +- 10 files changed, 17 insertions(+), 158 deletions(-) delete mode 100644 apps/sim/tools/onedrive/get_content.ts delete mode 100644 apps/sim/tools/onedrive/upload.ts diff --git a/apps/sim/tools/microsoft_planner/create_task.ts b/apps/sim/tools/microsoft_planner/create_task.ts index 03ce779f1f4..30401ac1957 100644 --- a/apps/sim/tools/microsoft_planner/create_task.ts +++ b/apps/sim/tools/microsoft_planner/create_task.ts @@ -31,37 +31,37 @@ export const createTaskTool: ToolConfig< planId: { type: 'string', required: true, - visibility: 'user-or-llm', + visibility: 'user-only', description: 'The ID of the plan where the task will be created', }, title: { type: 'string', required: true, - visibility: 'user-or-llm', + visibility: 'user-only', description: 'The title of the task', }, description: { type: 'string', required: false, - visibility: 'user-or-llm', + visibility: 'user-only', description: 'The description of the task', }, dueDateTime: { type: 'string', required: false, - visibility: 'user-or-llm', + visibility: 'user-only', description: 'The due date and time for the task (ISO 8601 format)', }, assigneeUserId: { type: 'string', required: false, - visibility: 'user-or-llm', + visibility: 'user-only', description: 'The user ID to assign the task to', }, bucketId: { type: 'string', required: false, - visibility: 'user-or-llm', + visibility: 'user-only', description: 'The bucket ID to place the task in', }, }, diff --git a/apps/sim/tools/microsoft_planner/read_task.ts b/apps/sim/tools/microsoft_planner/read_task.ts index eff8bb33a87..4a2a4ce4f1f 100644 --- a/apps/sim/tools/microsoft_planner/read_task.ts +++ b/apps/sim/tools/microsoft_planner/read_task.ts @@ -28,13 +28,13 @@ export const readTaskTool: ToolConfig = { query: { type: 'string', required: false, - visibility: 'user-or-llm', + visibility: 'user-only', description: 'A query to filter the files', }, pageSize: { diff --git a/apps/sim/tools/onedrive/upload.ts b/apps/sim/tools/onedrive/upload.ts deleted file mode 100644 index 5783351bb53..00000000000 --- a/apps/sim/tools/onedrive/upload.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { createLogger } from '@/lib/logs/console/logger' -import type { OneDriveToolParams, OneDriveUploadResponse } from '@/tools/onedrive/types' -import type { ToolConfig } from '@/tools/types' - -const logger = createLogger('OneDriveUploadTool') - -export const uploadTool: ToolConfig = { - id: 'onedrive_upload', - name: 'Upload to OneDrive', - description: 'Upload a file to OneDrive', - version: '1.0', - oauth: { - required: true, - provider: 'onedrive', - additionalScopes: [ - 'openid', - 'profile', - 'email', - 'Files.Read', - 'Files.ReadWrite', - 'offline_access', - ], - }, - params: { - accessToken: { - type: 'string', - required: true, - visibility: 'hidden', - description: 'The access token for the OneDrive API', - }, - fileName: { - type: 'string', - required: true, - visibility: 'user-or-llm', - description: 'The name of the file to upload', - }, - content: { - type: 'string', - required: true, - visibility: 'user-or-llm', - description: 'The content of the file to upload', - }, - folderSelector: { - type: 'string', - required: false, - visibility: 'user-only', - description: 'Select the folder to upload the file to', - }, - folderId: { - type: 'string', - required: false, - visibility: 'hidden', - description: 'The ID of the folder to upload the file to (internal use)', - }, - }, - request: { - url: (params) => { - let fileName = params.fileName || 'untitled' - - // Always create .txt files for text content - if (!fileName.endsWith('.txt')) { - // Remove any existing extensions and add .txt - fileName = `${fileName.replace(/\.[^.]*$/, '')}.txt` - } - - // Build the proper URL based on parent folder - const parentFolderId = params.folderSelector || params.folderId - if (parentFolderId && parentFolderId.trim() !== '') { - return `https://graph.microsoft.com/v1.0/me/drive/items/${parentFolderId}:/${fileName}:/content` - } - // Default to root folder - return `https://graph.microsoft.com/v1.0/me/drive/root:/${fileName}:/content` - }, - method: 'PUT', - headers: (params) => ({ - Authorization: `Bearer ${params.accessToken}`, - 'Content-Type': 'text/plain', - }), - body: (params) => (params.content || '') as unknown as Record, - }, - transformResponse: async (response: Response, params?: OneDriveToolParams) => { - try { - if (!response.ok) { - const errorData = await response.json().catch(() => ({})) - logger.error('Failed to upload file to OneDrive', { - status: response.status, - statusText: response.statusText, - errorData, - }) - throw new Error(errorData.error?.message || 'Failed to upload file to OneDrive') - } - - // Microsoft Graph API returns the file metadata directly - const fileData = await response.json() - - logger.info('Successfully uploaded file to OneDrive', { - fileId: fileData.id, - fileName: fileData.name, - }) - - return { - success: true, - output: { - file: { - id: fileData.id, - name: fileData.name, - mimeType: fileData.file?.mimeType || params?.mimeType || 'text/plain', - webViewLink: fileData.webUrl, - webContentLink: fileData['@microsoft.graph.downloadUrl'], - size: fileData.size, - createdTime: fileData.createdDateTime, - modifiedTime: fileData.lastModifiedDateTime, - parentReference: fileData.parentReference, - }, - }, - } - } catch (error: any) { - logger.error('Error in upload transformation', { - error: error.message, - stack: error.stack, - }) - throw error - } - }, - transformError: (error) => { - logger.error('Upload error', { - error: error.message, - stack: error.stack, - }) - return error.message || 'An error occurred while uploading to OneDrive' - }, -} diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index ad43566b11e..fced0945d22 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -99,7 +99,7 @@ import { notionSearchTool, notionWriteTool, } from '@/tools/notion' -import { onedriveCreateFolderTool, onedriveListTool, onedriveUploadTool } from '@/tools/onedrive' +import { onedriveCreateFolderTool, onedriveListTool } from '@/tools/onedrive' import { imageTool, embeddingsTool as openAIEmbeddings } from '@/tools/openai' import { outlookDraftTool, outlookReadTool, outlookSendTool } from '@/tools/outlook' import { perplexityChatTool } from '@/tools/perplexity' @@ -276,7 +276,6 @@ export const tools: Record = { linear_read_issues: linearReadIssuesTool, linear_create_issue: linearCreateIssueTool, onedrive_create_folder: onedriveCreateFolderTool, - onedrive_upload: onedriveUploadTool, onedrive_list: onedriveListTool, microsoft_excel_read: microsoftExcelReadTool, microsoft_excel_write: microsoftExcelWriteTool, diff --git a/apps/sim/tools/sharepoint/create_page.ts b/apps/sim/tools/sharepoint/create_page.ts index ae71375dc40..235ecac6cde 100644 --- a/apps/sim/tools/sharepoint/create_page.ts +++ b/apps/sim/tools/sharepoint/create_page.ts @@ -40,19 +40,19 @@ export const createPageTool: ToolConfig Date: Sat, 2 Aug 2025 14:07:23 -0700 Subject: [PATCH 18/23] removed page token #840 --- apps/sim/tools/onedrive/list.ts | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/apps/sim/tools/onedrive/list.ts b/apps/sim/tools/onedrive/list.ts index dc36a72dfca..089d5ace326 100644 --- a/apps/sim/tools/onedrive/list.ts +++ b/apps/sim/tools/onedrive/list.ts @@ -61,7 +61,7 @@ export const listTool: ToolConfig = { query: { type: 'string', required: false, - visibility: 'user-only', + visibility: 'user-or-llm', description: 'A query to filter the files', }, pageSize: { @@ -70,12 +70,6 @@ export const listTool: ToolConfig = { visibility: 'user-only', description: 'The number of files to return', }, - pageToken: { - type: 'string', - required: false, - visibility: 'hidden', - description: 'The page token to use for pagination', - }, }, request: { url: (params) => { @@ -104,16 +98,7 @@ export const listTool: ToolConfig = { } // Remove the $skip logic entirely. Instead, use the full nextLink URL if provided - let finalUrl: string - if (params.pageToken) { - // pageToken should contain the full @odata.nextLink URL - finalUrl = params.pageToken - } else { - // Construct the initial request URL - finalUrl = url.toString() - } - - return finalUrl + return url.toString() }, method: 'GET', headers: (params) => ({ From 4ee7252025fadd994b067fa0291dc0fa225b0b14 Mon Sep 17 00:00:00 2001 From: Adam Gough Date: Sat, 2 Aug 2025 15:39:54 -0700 Subject: [PATCH 19/23] fixed docs and descriptions, added advanced mode #840 --- .../content/docs/tools/microsoft_planner.mdx | 14 ++++++++++ apps/docs/content/docs/tools/onedrive.mdx | 26 +++++++++++++------ apps/docs/content/docs/tools/sharepoint.mdx | 15 +++++++++++ apps/sim/blocks/blocks/microsoft_planner.ts | 13 ++++++++++ apps/sim/blocks/blocks/onedrive.ts | 5 ++-- apps/sim/blocks/blocks/sharepoint.ts | 2 +- 6 files changed, 63 insertions(+), 12 deletions(-) diff --git a/apps/docs/content/docs/tools/microsoft_planner.mdx b/apps/docs/content/docs/tools/microsoft_planner.mdx index 059978b3f9b..09cc412fc26 100644 --- a/apps/docs/content/docs/tools/microsoft_planner.mdx +++ b/apps/docs/content/docs/tools/microsoft_planner.mdx @@ -106,6 +106,20 @@ import { BlockInfoCard } from "@/components/ui/block-info-card" `} /> +{/* MANUAL-CONTENT-START:intro */} +[Microsoft Planner](https://www.microsoft.com/en-us/microsoft-365/planner) is a task management tool that helps teams organize work visually using boards, tasks, and buckets. Integrated with Microsoft 365, it offers a simple, intuitive way to manage team projects, assign responsibilities, and track progress. + +With Microsoft Planner, you can: + +- **Create and manage tasks**: Add new tasks with due dates, priorities, and assigned users +- **Organize with buckets**: Group tasks by phase, status, or category to reflect your team’s workflow +- **Visualize project status**: Use boards, charts, and filters to monitor workload and track progress +- **Stay integrated with Microsoft 365**: Seamlessly connect tasks with Teams, Outlook, and other Microsoft tools + +In Sim, the Microsoft Planner integration allows your agents to programmatically create, read, and manage tasks as part of their workflows. Agents can generate new tasks based on incoming requests, retrieve task details to drive decisions, and track status across projects — all without human intervention. Whether you're building workflows for client onboarding, internal project tracking, or follow-up task generation, integrating Microsoft Planner with Sim gives your agents a structured way to coordinate work, automate task creation, and keep teams aligned. +{/* MANUAL-CONTENT-END */} + + ## Usage Instructions Integrate Microsoft Planner functionality to manage tasks. Read all user tasks, tasks from specific plans, individual tasks, or create new tasks with various properties like title, description, due date, and assignees using OAuth authentication. diff --git a/apps/docs/content/docs/tools/onedrive.mdx b/apps/docs/content/docs/tools/onedrive.mdx index 5bc7181b8ef..99c8913ad5f 100644 --- a/apps/docs/content/docs/tools/onedrive.mdx +++ b/apps/docs/content/docs/tools/onedrive.mdx @@ -31,6 +31,24 @@ import { BlockInfoCard } from "@/components/ui/block-info-card" `} /> +{/* MANUAL-CONTENT-START:intro */} +[OneDrive](https://onedrive.live.com) is Microsoft’s cloud storage and file synchronization service that allows users to securely store, access, and share files across devices. Integrated deeply into the Microsoft 365 ecosystem, OneDrive supports seamless collaboration, version control, and real-time access to content across teams and organizations. + +Learn how to integrate the OneDrive tool in Sim to automatically pull, manage, and organize your cloud files within your workflows. This tutorial walks you through connecting OneDrive, setting up file access, and using stored content to power automation. Ideal for syncing essential documents and media with your agents in real time. + +With OneDrive, you can: + +- **Store files securely in the cloud**: Upload and access documents, images, and other files from any device +- **Organize your content**: Create structured folders and manage file versions with ease +- **Collaborate in real time**: Share files, edit them simultaneously with others, and track changes +- **Access across devices**: Use OneDrive from desktop, mobile, and web platforms +- **Integrate with Microsoft 365**: Work seamlessly with Word, Excel, PowerPoint, and Teams +- **Control permissions**: Share files and folders with custom access settings and expiration controls + +In Sim, the OneDrive integration enables your agents to directly interact with your cloud storage. Agents can upload new files to specific folders, retrieve and read existing files, and list folder contents to dynamically organize and access information. This integration allows your agents to incorporate file operations into intelligent workflows — automating document intake, content analysis, and structured storage management. By connecting Sim with OneDrive, you empower your agents to manage and use cloud documents programmatically, eliminating manual steps and enhancing automation with secure, real-time file access. +{/* MANUAL-CONTENT-END */} + + ## Usage Instructions Integrate OneDrive functionality to manage files and folders. Upload new files, create new folders, and list contents of folders using OAuth authentication. Supports file operations with custom MIME types and folder organization. @@ -41,17 +59,10 @@ Integrate OneDrive functionality to manage files and folders. Upload new files, ### `onedrive_upload` -Upload a file to OneDrive - #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `accessToken` | string | Yes | The access token for the OneDrive API | -| `fileName` | string | Yes | The name of the file to upload | -| `content` | string | Yes | The content of the file to upload | -| `folderSelector` | string | No | Select the folder to upload the file to | -| `folderId` | string | No | The ID of the folder to upload the file to \(internal use\) | #### Output @@ -93,7 +104,6 @@ List files and folders in OneDrive | `folderId` | string | No | The ID of the folder to list files from \(internal use\) | | `query` | string | No | A query to filter the files | | `pageSize` | number | No | The number of files to return | -| `pageToken` | string | No | The page token to use for pagination | #### Output diff --git a/apps/docs/content/docs/tools/sharepoint.mdx b/apps/docs/content/docs/tools/sharepoint.mdx index c4deada3d59..2e1be850ec3 100644 --- a/apps/docs/content/docs/tools/sharepoint.mdx +++ b/apps/docs/content/docs/tools/sharepoint.mdx @@ -44,6 +44,21 @@ import { BlockInfoCard } from "@/components/ui/block-info-card" `} /> +{/* MANUAL-CONTENT-START:intro */} +[SharePoint](https://www.microsoft.com/en-us/microsoft-365/sharepoint/collaboration) is a collaborative platform from Microsoft that enables users to build and manage internal websites, share documents, and organize team resources. It provides a powerful, flexible solution for creating digital workspaces and streamlining content management across organizations. + +With SharePoint, you can: + +- **Create team and communication sites**: Set up pages and portals to support collaboration, announcements, and content distribution +- **Organize and share content**: Store documents, manage files, and enable version control with secure sharing capabilities +- **Customize pages**: Add text parts to tailor each site to your team's needs +- **Improve discoverability**: Use metadata, search, and navigation tools to help users quickly find what they need +- **Collaborate securely**: Control access with robust permission settings and Microsoft 365 integration + +In Sim, the SharePoint integration empowers your agents to create and access SharePoint sites and pages as part of their workflows. This enables automated document management, knowledge sharing, and workspace creation without manual effort. Agents can generate new project pages, upload or retrieve files, and organize resources dynamically, based on workflow inputs. By connecting Sim with SharePoint, you bring structured collaboration and content management into your automation flows — giving your agents the ability to coordinate team activities, surface key information, and maintain a single source of truth across your organization. +{/* MANUAL-CONTENT-END */} + + ## Usage Instructions Integrate Sharepoint functionality to manage pages. Read and create pages, and list sites using OAuth authentication. Supports page operations with custom MIME types and folder organization. diff --git a/apps/sim/blocks/blocks/microsoft_planner.ts b/apps/sim/blocks/blocks/microsoft_planner.ts index c231d9f4b42..c2b4cbf570c 100644 --- a/apps/sim/blocks/blocks/microsoft_planner.ts +++ b/apps/sim/blocks/blocks/microsoft_planner.ts @@ -70,7 +70,20 @@ export const MicrosoftPlannerBlock: BlockConfig = { placeholder: 'Select a task', provider: 'microsoft-planner', condition: { field: 'operation', value: ['read_task'] }, + mode: 'basic', }, + + // Advanced mode + { + id: 'taskId', + title: 'Manual Task ID', + type: 'short-input', + layout: 'full', + placeholder: 'Enter the task ID', + condition: { field: 'operation', value: ['read_task'] }, + mode: 'advanced', + }, + { id: 'title', title: 'Task Title', diff --git a/apps/sim/blocks/blocks/onedrive.ts b/apps/sim/blocks/blocks/onedrive.ts index a1bbff332e6..9850223e0c3 100644 --- a/apps/sim/blocks/blocks/onedrive.ts +++ b/apps/sim/blocks/blocks/onedrive.ts @@ -22,7 +22,6 @@ export const OneDriveBlock: BlockConfig = { options: [ { label: 'Create Folder', id: 'create_folder' }, { label: 'Upload File', id: 'upload' }, - // { label: 'Get File Content', id: 'get_content' }, { label: 'List Files', id: 'list' }, ], }, @@ -223,7 +222,7 @@ export const OneDriveBlock: BlockConfig = { pageSize: { type: 'number', description: 'Results per page' }, }, outputs: { - file: 'json', - files: 'json', + file: { type: 'any', description: 'File metadata' }, + files: { type: 'any', description: 'Files metadata' }, }, } diff --git a/apps/sim/blocks/blocks/sharepoint.ts b/apps/sim/blocks/blocks/sharepoint.ts index 57f85f5bf24..4d1ba523b24 100644 --- a/apps/sim/blocks/blocks/sharepoint.ts +++ b/apps/sim/blocks/blocks/sharepoint.ts @@ -149,6 +149,6 @@ export const SharepointBlock: BlockConfig = { pageSize: { type: 'number', description: 'Results per page' }, }, outputs: { - sites: 'json', + sites: { type: 'any', description: 'Sites metadata' }, }, } From 8140a5ab4d231a2ae0e111343d67c36460d73c28 Mon Sep 17 00:00:00 2001 From: waleedlatif1 Date: Tue, 5 Aug 2025 19:18:28 -0700 Subject: [PATCH 20/23] remove unused types, cleaned up a lot, fixed docs --- .../content/docs/tools/microsoft_planner.mdx | 8 +- apps/docs/content/docs/tools/onedrive.mdx | 12 +- apps/docs/content/docs/tools/sharepoint.mdx | 11 +- .../[documentId]/chunks/[chunkId]/route.ts | 13 +- .../app/api/knowledge/[id]/documents/route.ts | 12 +- .../app/api/tools/onedrive/folder/route.ts | 4 +- .../app/api/tools/onedrive/folders/route.ts | 19 +-- .../app/api/tools/sharepoint/site/route.ts | 4 +- .../app/api/tools/sharepoint/sites/route.ts | 4 +- apps/sim/blocks/blocks/microsoft_planner.ts | 12 +- apps/sim/blocks/blocks/onedrive.ts | 11 +- apps/sim/blocks/blocks/sharepoint.ts | 6 +- apps/sim/tools/microsoft_planner/read_task.ts | 3 - apps/sim/tools/onedrive/list.ts | 27 +--- apps/sim/tools/onedrive/types.ts | 33 +++-- apps/sim/tools/sharepoint/index.ts | 2 +- .../{read_site.ts => list_sites.ts} | 38 +----- apps/sim/tools/sharepoint/read_page.ts | 129 +----------------- apps/sim/tools/sharepoint/types.ts | 73 ++++++++++ apps/sim/tools/sharepoint/utils.ts | 87 ++++++++++++ 20 files changed, 258 insertions(+), 250 deletions(-) rename apps/sim/tools/sharepoint/{read_site.ts => list_sites.ts} (81%) create mode 100644 apps/sim/tools/sharepoint/utils.ts diff --git a/apps/docs/content/docs/tools/microsoft_planner.mdx b/apps/docs/content/docs/tools/microsoft_planner.mdx index 09cc412fc26..5ae82298f35 100644 --- a/apps/docs/content/docs/tools/microsoft_planner.mdx +++ b/apps/docs/content/docs/tools/microsoft_planner.mdx @@ -144,8 +144,8 @@ Read tasks from Microsoft Planner - get all user tasks or all tasks from a speci | Parameter | Type | Description | | --------- | ---- | ----------- | -| `task` | json | task output from the block | -| `metadata` | json | metadata output from the block | +| `task` | json | The Microsoft Planner task object, including details such as id, title, description, status, due date, and assignees. | +| `metadata` | json | Additional metadata about the operation, such as timestamps, request status, or other relevant information. | ### `microsoft_planner_create_task` @@ -167,8 +167,8 @@ Create a new task in Microsoft Planner | Parameter | Type | Description | | --------- | ---- | ----------- | -| `task` | json | task output from the block | -| `metadata` | json | metadata output from the block | +| `task` | json | The Microsoft Planner task object, including details such as id, title, description, status, due date, and assignees. | +| `metadata` | json | Additional metadata about the operation, such as timestamps, request status, or other relevant information. | diff --git a/apps/docs/content/docs/tools/onedrive.mdx b/apps/docs/content/docs/tools/onedrive.mdx index 99c8913ad5f..24e58c2d2eb 100644 --- a/apps/docs/content/docs/tools/onedrive.mdx +++ b/apps/docs/content/docs/tools/onedrive.mdx @@ -68,8 +68,8 @@ Integrate OneDrive functionality to manage files and folders. Upload new files, | Parameter | Type | Description | | --------- | ---- | ----------- | -| `file` | json | file output from the block | -| `files` | json | files output from the block | +| `file` | json | The OneDrive file object, including details such as id, name, size, and more. | +| `files` | json | An array of OneDrive file objects, each containing details such as id, name, size, and more. | ### `onedrive_create_folder` @@ -88,8 +88,8 @@ Create a new folder in OneDrive | Parameter | Type | Description | | --------- | ---- | ----------- | -| `file` | json | file output from the block | -| `files` | json | files output from the block | +| `file` | json | The OneDrive file object, including details such as id, name, size, and more. | +| `files` | json | An array of OneDrive file objects, each containing details such as id, name, size, and more. | ### `onedrive_list` @@ -109,8 +109,8 @@ List files and folders in OneDrive | Parameter | Type | Description | | --------- | ---- | ----------- | -| `file` | json | file output from the block | -| `files` | json | files output from the block | +| `file` | json | The OneDrive file object, including details such as id, name, size, and more. | +| `files` | json | An array of OneDrive file objects, each containing details such as id, name, size, and more. | diff --git a/apps/docs/content/docs/tools/sharepoint.mdx b/apps/docs/content/docs/tools/sharepoint.mdx index 2e1be850ec3..3c44e35104e 100644 --- a/apps/docs/content/docs/tools/sharepoint.mdx +++ b/apps/docs/content/docs/tools/sharepoint.mdx @@ -86,7 +86,7 @@ Create a new page in a SharePoint site | Parameter | Type | Description | | --------- | ---- | ----------- | -| `sites` | json | sites output from the block | +| `sites` | json | An array of SharePoint site objects, each containing details such as id, name, and more. | ### `sharepoint_read_page` @@ -107,20 +107,25 @@ Read a specific page from a SharePoint site | Parameter | Type | Description | | --------- | ---- | ----------- | -| `sites` | json | sites output from the block | +| `sites` | json | An array of SharePoint site objects, each containing details such as id, name, and more. | ### `sharepoint_list_sites` +List details of all SharePoint sites + #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | +| `accessToken` | string | Yes | The access token for the SharePoint API | +| `siteSelector` | string | No | Select the SharePoint site | +| `groupId` | string | No | The group ID for accessing a group team site | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `sites` | json | sites output from the block | +| `sites` | json | An array of SharePoint site objects, each containing details such as id, name, and more. | diff --git a/apps/sim/app/api/knowledge/[id]/documents/[documentId]/chunks/[chunkId]/route.ts b/apps/sim/app/api/knowledge/[id]/documents/[documentId]/chunks/[chunkId]/route.ts index f453790ebe0..b2eeb803522 100644 --- a/apps/sim/app/api/knowledge/[id]/documents/[documentId]/chunks/[chunkId]/route.ts +++ b/apps/sim/app/api/knowledge/[id]/documents/[documentId]/chunks/[chunkId]/route.ts @@ -1,4 +1,4 @@ -import crypto from 'node:crypto' +import { createHash, randomUUID } from 'crypto' import { eq, sql } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' @@ -22,7 +22,7 @@ export async function GET( req: NextRequest, { params }: { params: Promise<{ id: string; documentId: string; chunkId: string }> } ) { - const requestId = crypto.randomUUID().slice(0, 8) + const requestId = randomUUID().slice(0, 8) const { id: knowledgeBaseId, documentId, chunkId } = await params try { @@ -70,7 +70,7 @@ export async function PUT( req: NextRequest, { params }: { params: Promise<{ id: string; documentId: string; chunkId: string }> } ) { - const requestId = crypto.randomUUID().slice(0, 8) + const requestId = randomUUID().slice(0, 8) const { id: knowledgeBaseId, documentId, chunkId } = await params try { @@ -119,10 +119,7 @@ export async function PUT( updateData.contentLength = validatedData.content.length // Update token count estimation (rough approximation: 4 chars per token) updateData.tokenCount = Math.ceil(validatedData.content.length / 4) - updateData.chunkHash = crypto - .createHash('sha256') - .update(validatedData.content) - .digest('hex') + updateData.chunkHash = createHash('sha256').update(validatedData.content).digest('hex') } if (validatedData.enabled !== undefined) updateData.enabled = validatedData.enabled @@ -166,7 +163,7 @@ export async function DELETE( req: NextRequest, { params }: { params: Promise<{ id: string; documentId: string; chunkId: string }> } ) { - const requestId = crypto.randomUUID().slice(0, 8) + const requestId = randomUUID().slice(0, 8) const { id: knowledgeBaseId, documentId, chunkId } = await params try { diff --git a/apps/sim/app/api/knowledge/[id]/documents/route.ts b/apps/sim/app/api/knowledge/[id]/documents/route.ts index b7492151f15..c3b14ac4a79 100644 --- a/apps/sim/app/api/knowledge/[id]/documents/route.ts +++ b/apps/sim/app/api/knowledge/[id]/documents/route.ts @@ -1,4 +1,4 @@ -import crypto from 'node:crypto' +import { randomUUID } from 'crypto' import { and, desc, eq, inArray, isNull, sql } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' @@ -114,7 +114,7 @@ async function processDocumentTags( // Create new tag definition if we have a slot if (targetSlot) { const newDefinition = { - id: crypto.randomUUID(), + id: randomUUID(), knowledgeBaseId, tagSlot: targetSlot as any, displayName: tagName, @@ -312,7 +312,7 @@ const BulkUpdateDocumentsSchema = z.object({ }) export async function GET(req: NextRequest, { params }: { params: Promise<{ id: string }> }) { - const requestId = crypto.randomUUID().slice(0, 8) + const requestId = randomUUID().slice(0, 8) const { id: knowledgeBaseId } = await params try { @@ -423,7 +423,7 @@ export async function GET(req: NextRequest, { params }: { params: Promise<{ id: } export async function POST(req: NextRequest, { params }: { params: Promise<{ id: string }> }) { - const requestId = crypto.randomUUID().slice(0, 8) + const requestId = randomUUID().slice(0, 8) const { id: knowledgeBaseId } = await params try { @@ -470,7 +470,7 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id: const createdDocuments = await db.transaction(async (tx) => { const documentPromises = validatedData.documents.map(async (docData) => { - const documentId = crypto.randomUUID() + const documentId = randomUUID() const now = new Date() // Process documentTagsData if provided (for knowledge base block) @@ -578,7 +578,7 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id: try { const validatedData = CreateDocumentSchema.parse(body) - const documentId = crypto.randomUUID() + const documentId = randomUUID() const now = new Date() // Process structured tag data if provided diff --git a/apps/sim/app/api/tools/onedrive/folder/route.ts b/apps/sim/app/api/tools/onedrive/folder/route.ts index d7a518c6fed..d29ad7e57bd 100644 --- a/apps/sim/app/api/tools/onedrive/folder/route.ts +++ b/apps/sim/app/api/tools/onedrive/folder/route.ts @@ -1,4 +1,4 @@ -import crypto from 'node:crypto' +import { randomUUID } from 'crypto' import { eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { getSession } from '@/lib/auth' @@ -15,7 +15,7 @@ const logger = createLogger('OneDriveFolderAPI') * Get a single folder from Microsoft OneDrive */ export async function GET(request: NextRequest) { - const requestId = crypto.randomUUID().slice(0, 8) + const requestId = randomUUID().slice(0, 8) try { const session = await getSession() diff --git a/apps/sim/app/api/tools/onedrive/folders/route.ts b/apps/sim/app/api/tools/onedrive/folders/route.ts index 1a00d2cb6be..4194addfbab 100644 --- a/apps/sim/app/api/tools/onedrive/folders/route.ts +++ b/apps/sim/app/api/tools/onedrive/folders/route.ts @@ -1,4 +1,4 @@ -import crypto from 'node:crypto' +import { randomUUID } from 'crypto' import { eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { getSession } from '@/lib/auth' @@ -11,26 +11,13 @@ export const dynamic = 'force-dynamic' const logger = createLogger('OneDriveFoldersAPI') -interface MicrosoftGraphDriveItem { - id: string - name: string - folder?: { - childCount: number - } - file?: { - mimeType: string - } - webUrl: string - createdDateTime: string - lastModifiedDateTime: string - size?: number -} +import type { MicrosoftGraphDriveItem } from '@/tools/onedrive/types' /** * Get folders from Microsoft OneDrive */ export async function GET(request: NextRequest) { - const requestId = crypto.randomUUID().slice(0, 8) + const requestId = randomUUID().slice(0, 8) try { const session = await getSession() diff --git a/apps/sim/app/api/tools/sharepoint/site/route.ts b/apps/sim/app/api/tools/sharepoint/site/route.ts index d4432da88c6..225bd748e7a 100644 --- a/apps/sim/app/api/tools/sharepoint/site/route.ts +++ b/apps/sim/app/api/tools/sharepoint/site/route.ts @@ -1,4 +1,4 @@ -import crypto from 'node:crypto' +import { randomUUID } from 'crypto' import { eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { getSession } from '@/lib/auth' @@ -15,7 +15,7 @@ const logger = createLogger('SharePointSiteAPI') * Get a single SharePoint site from Microsoft Graph API */ export async function GET(request: NextRequest) { - const requestId = crypto.randomUUID().slice(0, 8) + const requestId = randomUUID().slice(0, 8) try { const session = await getSession() diff --git a/apps/sim/app/api/tools/sharepoint/sites/route.ts b/apps/sim/app/api/tools/sharepoint/sites/route.ts index e2571e3758a..93bc5bd0942 100644 --- a/apps/sim/app/api/tools/sharepoint/sites/route.ts +++ b/apps/sim/app/api/tools/sharepoint/sites/route.ts @@ -1,4 +1,4 @@ -import crypto from 'node:crypto' +import { randomUUID } from 'crypto' import { eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { getSession } from '@/lib/auth' @@ -16,7 +16,7 @@ const logger = createLogger('SharePointSitesAPI') * Get SharePoint sites from Microsoft Graph API */ export async function GET(request: NextRequest) { - const requestId = crypto.randomUUID().slice(0, 8) + const requestId = randomUUID().slice(0, 8) try { const session = await getSession() diff --git a/apps/sim/blocks/blocks/microsoft_planner.ts b/apps/sim/blocks/blocks/microsoft_planner.ts index c2b4cbf570c..ce020652a7b 100644 --- a/apps/sim/blocks/blocks/microsoft_planner.ts +++ b/apps/sim/blocks/blocks/microsoft_planner.ts @@ -224,7 +224,15 @@ export const MicrosoftPlannerBlock: BlockConfig = { bucketId: { type: 'string', description: 'Bucket ID' }, }, outputs: { - task: 'json', - metadata: 'json', + task: { + type: 'json', + description: + 'The Microsoft Planner task object, including details such as id, title, description, status, due date, and assignees.', + }, + metadata: { + type: 'json', + description: + 'Additional metadata about the operation, such as timestamps, request status, or other relevant information.', + }, }, } diff --git a/apps/sim/blocks/blocks/onedrive.ts b/apps/sim/blocks/blocks/onedrive.ts index 9850223e0c3..dd99159da1d 100644 --- a/apps/sim/blocks/blocks/onedrive.ts +++ b/apps/sim/blocks/blocks/onedrive.ts @@ -222,7 +222,14 @@ export const OneDriveBlock: BlockConfig = { pageSize: { type: 'number', description: 'Results per page' }, }, outputs: { - file: { type: 'any', description: 'File metadata' }, - files: { type: 'any', description: 'Files metadata' }, + file: { + type: 'json', + description: 'The OneDrive file object, including details such as id, name, size, and more.', + }, + files: { + type: 'json', + description: + 'An array of OneDrive file objects, each containing details such as id, name, size, and more.', + }, }, } diff --git a/apps/sim/blocks/blocks/sharepoint.ts b/apps/sim/blocks/blocks/sharepoint.ts index 4d1ba523b24..ddceb96c621 100644 --- a/apps/sim/blocks/blocks/sharepoint.ts +++ b/apps/sim/blocks/blocks/sharepoint.ts @@ -149,6 +149,10 @@ export const SharepointBlock: BlockConfig = { pageSize: { type: 'number', description: 'Results per page' }, }, outputs: { - sites: { type: 'any', description: 'Sites metadata' }, + sites: { + type: 'json', + description: + 'An array of SharePoint site objects, each containing details such as id, name, and more.', + }, }, } diff --git a/apps/sim/tools/microsoft_planner/read_task.ts b/apps/sim/tools/microsoft_planner/read_task.ts index 4a2a4ce4f1f..891f7f7a20d 100644 --- a/apps/sim/tools/microsoft_planner/read_task.ts +++ b/apps/sim/tools/microsoft_planner/read_task.ts @@ -71,9 +71,6 @@ export const readTaskTool: ToolConfig { - logger.info('Raw response URL:', response.url) - logger.info('Raw response status:', response.status) - if (!response.ok) { const errorJson = await response.json().catch(() => ({ error: response.statusText })) const errorText = diff --git a/apps/sim/tools/onedrive/list.ts b/apps/sim/tools/onedrive/list.ts index 089d5ace326..f8c8315b51d 100644 --- a/apps/sim/tools/onedrive/list.ts +++ b/apps/sim/tools/onedrive/list.ts @@ -1,27 +1,10 @@ -import type { OneDriveListResponse, OneDriveToolParams } from '@/tools/onedrive/types' +import type { + MicrosoftGraphDriveItem, + OneDriveListResponse, + OneDriveToolParams, +} from '@/tools/onedrive/types' import type { ToolConfig } from '@/tools/types' -interface MicrosoftGraphDriveItem { - id: string - name: string - file?: { - mimeType: string - } - folder?: { - childCount: number - } - webUrl: string - createdDateTime: string - lastModifiedDateTime: string - size?: number - '@microsoft.graph.downloadUrl'?: string - parentReference?: { - id: string - driveId: string - path: string - } -} - export const listTool: ToolConfig = { id: 'onedrive_list', name: 'List OneDrive Files', diff --git a/apps/sim/tools/onedrive/types.ts b/apps/sim/tools/onedrive/types.ts index d0401ad97e8..34494253605 100644 --- a/apps/sim/tools/onedrive/types.ts +++ b/apps/sim/tools/onedrive/types.ts @@ -1,5 +1,26 @@ import type { ToolResponse } from '@/tools/types' +export interface MicrosoftGraphDriveItem { + id: string + name: string + file?: { + mimeType: string + } + folder?: { + childCount: number + } + webUrl: string + createdDateTime: string + lastModifiedDateTime: string + size?: number + '@microsoft.graph.downloadUrl'?: string + parentReference?: { + id: string + driveId: string + path: string + } +} + export interface OneDriveFile { id: string name: string @@ -25,13 +46,6 @@ export interface OneDriveUploadResponse extends ToolResponse { } } -export interface OneDriveGetContentResponse extends ToolResponse { - output: { - content: string - metadata: OneDriveFile - } -} - export interface OneDriveToolParams { accessToken: string folderId?: string @@ -46,7 +60,4 @@ export interface OneDriveToolParams { exportMimeType?: string } -export type OneDriveResponse = - | OneDriveUploadResponse - | OneDriveGetContentResponse - | OneDriveListResponse +export type OneDriveResponse = OneDriveUploadResponse | OneDriveListResponse diff --git a/apps/sim/tools/sharepoint/index.ts b/apps/sim/tools/sharepoint/index.ts index d05ca327899..702d29aec99 100644 --- a/apps/sim/tools/sharepoint/index.ts +++ b/apps/sim/tools/sharepoint/index.ts @@ -1,6 +1,6 @@ import { createPageTool } from '@/tools/sharepoint/create_page' +import { listSitesTool } from '@/tools/sharepoint/list_sites' import { readPageTool } from '@/tools/sharepoint/read_page' -import { listSitesTool } from '@/tools/sharepoint/read_site' export const sharepointCreatePageTool = createPageTool export const sharepointListSitesTool = listSitesTool diff --git a/apps/sim/tools/sharepoint/read_site.ts b/apps/sim/tools/sharepoint/list_sites.ts similarity index 81% rename from apps/sim/tools/sharepoint/read_site.ts rename to apps/sim/tools/sharepoint/list_sites.ts index ce0bdadc834..d7876a47bea 100644 --- a/apps/sim/tools/sharepoint/read_site.ts +++ b/apps/sim/tools/sharepoint/list_sites.ts @@ -1,35 +1,9 @@ -import type { SharepointSite, SharepointToolParams } from '@/tools/sharepoint/types' -import type { ToolConfig, ToolResponse } from '@/tools/types' - -export interface SharepointReadSiteResponse extends ToolResponse { - output: { - site?: { - id: string - name: string - displayName: string - webUrl: string - description?: string - createdDateTime?: string - lastModifiedDateTime?: string - isPersonalSite?: boolean - root?: { - serverRelativeUrl: string - } - siteCollection?: { - hostname: string - } - } - sites?: Array<{ - id: string - name: string - displayName: string - webUrl: string - description?: string - createdDateTime?: string - lastModifiedDateTime?: string - }> - } -} +import type { + SharepointReadSiteResponse, + SharepointSite, + SharepointToolParams, +} from '@/tools/sharepoint/types' +import type { ToolConfig } from '@/tools/types' export const listSitesTool: ToolConfig = { id: 'sharepoint_list_sites', diff --git a/apps/sim/tools/sharepoint/read_page.ts b/apps/sim/tools/sharepoint/read_page.ts index 19fb557b2cf..36a0685993e 100644 --- a/apps/sim/tools/sharepoint/read_page.ts +++ b/apps/sim/tools/sharepoint/read_page.ts @@ -1,140 +1,15 @@ import { createLogger } from '@/lib/logs/console/logger' import type { + GraphApiResponse, SharepointPageContent, SharepointReadPageResponse, SharepointToolParams, } from '@/tools/sharepoint/types' +import { cleanODataMetadata, extractTextFromCanvasLayout } from '@/tools/sharepoint/utils' import type { ToolConfig } from '@/tools/types' const logger = createLogger('SharePointReadPage') -// Types for API responses -interface GraphApiResponse { - id?: string - name?: string - title?: string - webUrl?: string - pageLayout?: string - createdDateTime?: string - lastModifiedDateTime?: string - canvasLayout?: CanvasLayout - value?: GraphApiPageItem[] - error?: { - message: string - } -} - -interface GraphApiPageItem { - id: string - name: string - title?: string - webUrl?: string - pageLayout?: string - createdDateTime?: string - lastModifiedDateTime?: string -} - -interface CanvasLayout { - horizontalSections?: Array<{ - layout?: string - id?: string - emphasis?: string - columns?: Array<{ - webparts?: Array<{ - id?: string - innerHtml?: string - }> - }> - webparts?: Array<{ - id?: string - innerHtml?: string - }> - }> -} - -// Extract readable text from SharePoint canvas layout -function extractTextFromCanvasLayout(canvasLayout: CanvasLayout | null | undefined): string { - logger.info('Extracting text from canvas layout', { - hasCanvasLayout: !!canvasLayout, - hasHorizontalSections: !!canvasLayout?.horizontalSections, - sectionsCount: canvasLayout?.horizontalSections?.length || 0, - }) - - if (!canvasLayout?.horizontalSections) { - logger.info('No canvas layout or horizontal sections found') - return '' - } - - const textParts: string[] = [] - - for (const section of canvasLayout.horizontalSections) { - logger.info('Processing section', { - sectionId: section.id, - hasColumns: !!section.columns, - hasWebparts: !!section.webparts, - columnsCount: section.columns?.length || 0, - }) - - if (section.columns) { - for (const column of section.columns) { - if (column.webparts) { - for (const webpart of column.webparts) { - logger.info('Processing webpart', { - webpartId: webpart.id, - hasInnerHtml: !!webpart.innerHtml, - innerHtml: webpart.innerHtml, - }) - - if (webpart.innerHtml) { - // Extract text from HTML, removing tags - const text = webpart.innerHtml.replace(/<[^>]*>/g, '').trim() - if (text) { - textParts.push(text) - logger.info('Extracted text', { text }) - } - } - } - } - } - } else if (section.webparts) { - for (const webpart of section.webparts) { - if (webpart.innerHtml) { - const text = webpart.innerHtml.replace(/<[^>]*>/g, '').trim() - if (text) textParts.push(text) - } - } - } - } - - const finalContent = textParts.join('\n\n') - logger.info('Final extracted content', { - textPartsCount: textParts.length, - finalContentLength: finalContent.length, - finalContent, - }) - - return finalContent -} - -// Remove OData metadata from objects -function cleanODataMetadata(obj: T): T { - if (!obj || typeof obj !== 'object') return obj - - if (Array.isArray(obj)) { - return obj.map((item) => cleanODataMetadata(item)) as T - } - - const cleaned: Record = {} - for (const [key, value] of Object.entries(obj as Record)) { - // Skip OData metadata keys - if (key.includes('@odata')) continue - - cleaned[key] = cleanODataMetadata(value) - } - - return cleaned as T -} - export const readPageTool: ToolConfig = { id: 'sharepoint_read_page', name: 'Read SharePoint Page', diff --git a/apps/sim/tools/sharepoint/types.ts b/apps/sim/tools/sharepoint/types.ts index 6ef14e1c511..6ecddf4ff4e 100644 --- a/apps/sim/tools/sharepoint/types.ts +++ b/apps/sim/tools/sharepoint/types.ts @@ -133,6 +133,79 @@ export interface SharepointToolParams { maxPages?: number } +export interface GraphApiResponse { + id?: string + name?: string + title?: string + webUrl?: string + pageLayout?: string + createdDateTime?: string + lastModifiedDateTime?: string + canvasLayout?: CanvasLayout + value?: GraphApiPageItem[] + error?: { + message: string + } +} + +export interface GraphApiPageItem { + id: string + name: string + title?: string + webUrl?: string + pageLayout?: string + createdDateTime?: string + lastModifiedDateTime?: string +} + +export interface CanvasLayout { + horizontalSections?: Array<{ + layout?: string + id?: string + emphasis?: string + columns?: Array<{ + webparts?: Array<{ + id?: string + innerHtml?: string + }> + }> + webparts?: Array<{ + id?: string + innerHtml?: string + }> + }> +} + +export interface SharepointReadSiteResponse extends ToolResponse { + output: { + site?: { + id: string + name: string + displayName: string + webUrl: string + description?: string + createdDateTime?: string + lastModifiedDateTime?: string + isPersonalSite?: boolean + root?: { + serverRelativeUrl: string + } + siteCollection?: { + hostname: string + } + } + sites?: Array<{ + id: string + name: string + displayName: string + webUrl: string + description?: string + createdDateTime?: string + lastModifiedDateTime?: string + }> + } +} + export type SharepointResponse = | SharepointListSitesResponse | SharepointCreatePageResponse diff --git a/apps/sim/tools/sharepoint/utils.ts b/apps/sim/tools/sharepoint/utils.ts new file mode 100644 index 00000000000..7ed3169f22d --- /dev/null +++ b/apps/sim/tools/sharepoint/utils.ts @@ -0,0 +1,87 @@ +import { createLogger } from '@/lib/logs/console/logger' +import type { CanvasLayout } from '@/tools/sharepoint/types' + +const logger = createLogger('SharepointUtils') + +// Extract readable text from SharePoint canvas layout +export function extractTextFromCanvasLayout(canvasLayout: CanvasLayout | null | undefined): string { + logger.info('Extracting text from canvas layout', { + hasCanvasLayout: !!canvasLayout, + hasHorizontalSections: !!canvasLayout?.horizontalSections, + sectionsCount: canvasLayout?.horizontalSections?.length || 0, + }) + + if (!canvasLayout?.horizontalSections) { + logger.info('No canvas layout or horizontal sections found') + return '' + } + + const textParts: string[] = [] + + for (const section of canvasLayout.horizontalSections) { + logger.info('Processing section', { + sectionId: section.id, + hasColumns: !!section.columns, + hasWebparts: !!section.webparts, + columnsCount: section.columns?.length || 0, + }) + + if (section.columns) { + for (const column of section.columns) { + if (column.webparts) { + for (const webpart of column.webparts) { + logger.info('Processing webpart', { + webpartId: webpart.id, + hasInnerHtml: !!webpart.innerHtml, + innerHtml: webpart.innerHtml, + }) + + if (webpart.innerHtml) { + // Extract text from HTML, removing tags + const text = webpart.innerHtml.replace(/<[^>]*>/g, '').trim() + if (text) { + textParts.push(text) + logger.info('Extracted text', { text }) + } + } + } + } + } + } else if (section.webparts) { + for (const webpart of section.webparts) { + if (webpart.innerHtml) { + const text = webpart.innerHtml.replace(/<[^>]*>/g, '').trim() + if (text) textParts.push(text) + } + } + } + } + + const finalContent = textParts.join('\n\n') + logger.info('Final extracted content', { + textPartsCount: textParts.length, + finalContentLength: finalContent.length, + finalContent, + }) + + return finalContent +} + +// Remove OData metadata from objects +export function cleanODataMetadata(obj: T): T { + if (!obj || typeof obj !== 'object') return obj + + if (Array.isArray(obj)) { + return obj.map((item) => cleanODataMetadata(item)) as T + } + + const cleaned: Record = {} + for (const [key, value] of Object.entries(obj as Record)) { + // Skip OData metadata keys + if (key.includes('@odata')) continue + + cleaned[key] = cleanODataMetadata(value) + } + + return cleaned as T +} From fc4f66ac6988abe15d4e4b835aaeb3402dc5618f Mon Sep 17 00:00:00 2001 From: Adam Gough Date: Tue, 5 Aug 2025 20:25:12 -0700 Subject: [PATCH 21/23] readded file upload and changed docs --- apps/docs/content/docs/tools/meta.json | 2 +- apps/docs/content/docs/tools/onedrive.mdx | 7 + .../components/microsoft-file-selector.tsx | 9 +- apps/sim/tools/onedrive/create_folder.ts | 2 +- apps/sim/tools/onedrive/index.ts | 2 + apps/sim/tools/onedrive/upload.ts | 125 ++++++++++++++++++ apps/sim/tools/registry.ts | 3 +- 7 files changed, 143 insertions(+), 7 deletions(-) create mode 100644 apps/sim/tools/onedrive/upload.ts diff --git a/apps/docs/content/docs/tools/meta.json b/apps/docs/content/docs/tools/meta.json index b4ba8f9277f..3347e27cb1d 100644 --- a/apps/docs/content/docs/tools/meta.json +++ b/apps/docs/content/docs/tools/meta.json @@ -62,4 +62,4 @@ "x", "youtube" ] -} +} \ No newline at end of file diff --git a/apps/docs/content/docs/tools/onedrive.mdx b/apps/docs/content/docs/tools/onedrive.mdx index 24e58c2d2eb..7a389f238ef 100644 --- a/apps/docs/content/docs/tools/onedrive.mdx +++ b/apps/docs/content/docs/tools/onedrive.mdx @@ -59,10 +59,17 @@ Integrate OneDrive functionality to manage files and folders. Upload new files, ### `onedrive_upload` +Upload a file to OneDrive + #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | +| `accessToken` | string | Yes | The access token for the OneDrive API | +| `fileName` | string | Yes | The name of the file to upload | +| `content` | string | Yes | The content of the file to upload | +| `folderSelector` | string | No | Select the folder to upload the file to | +| `folderId` | string | No | The ID of the folder to upload the file to \(internal use\) | #### Output diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/microsoft-file-selector.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/microsoft-file-selector.tsx index 35c956a48c7..e3cc39291be 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/microsoft-file-selector.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/microsoft-file-selector.tsx @@ -827,9 +827,10 @@ export function MicrosoftFileSelector({ className='flex items-center gap-1 text-primary text-xs hover:underline' onClick={(e) => e.stopPropagation()} > - - {serviceId === 'microsoft-planner' ? 'Open in Planner' : 'Open in OneDrive'} - + + {serviceId === 'microsoft-planner' ? 'Open in Planner' : + serviceId === 'sharepoint' ? 'Open in SharePoint' : 'Open in OneDrive'} + ) : ( @@ -840,7 +841,7 @@ export function MicrosoftFileSelector({ className='flex items-center gap-1 text-primary text-xs hover:underline' onClick={(e) => e.stopPropagation()} > - Open in OneDrive + {serviceId === 'sharepoint' ? 'Open in SharePoint' : 'Open in OneDrive'} )} diff --git a/apps/sim/tools/onedrive/create_folder.ts b/apps/sim/tools/onedrive/create_folder.ts index 8b8938dcdbe..31d10c5410d 100644 --- a/apps/sim/tools/onedrive/create_folder.ts +++ b/apps/sim/tools/onedrive/create_folder.ts @@ -53,7 +53,7 @@ export const createFolderTool: ToolConfig { return { - name: params.fileName, + name: params.folderName, folder: {}, // Required facet for folder creation in Microsoft Graph API '@microsoft.graph.conflictBehavior': 'rename', // Handle name conflicts } diff --git a/apps/sim/tools/onedrive/index.ts b/apps/sim/tools/onedrive/index.ts index 00aaa35cdab..30298d9d783 100644 --- a/apps/sim/tools/onedrive/index.ts +++ b/apps/sim/tools/onedrive/index.ts @@ -1,5 +1,7 @@ import { createFolderTool } from '@/tools/onedrive/create_folder' import { listTool } from '@/tools/onedrive/list' +import { uploadTool } from '@/tools/onedrive/upload' export const onedriveCreateFolderTool = createFolderTool export const onedriveListTool = listTool +export const onedriveUploadTool = uploadTool diff --git a/apps/sim/tools/onedrive/upload.ts b/apps/sim/tools/onedrive/upload.ts new file mode 100644 index 00000000000..01761c9ddc7 --- /dev/null +++ b/apps/sim/tools/onedrive/upload.ts @@ -0,0 +1,125 @@ +import { createLogger } from '@/lib/logs/console/logger' +import type { OneDriveToolParams, OneDriveUploadResponse } from '@/tools/onedrive/types' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('OneDriveUploadTool') + +export const uploadTool: ToolConfig = { + id: 'onedrive_upload', + name: 'Upload to OneDrive', + description: 'Upload a file to OneDrive', + version: '1.0', + oauth: { + required: true, + provider: 'onedrive', + additionalScopes: ['openid', 'profile', 'email', 'Files.Read', 'Files.ReadWrite', 'offline_access'], + }, + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the OneDrive API', + }, + fileName: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The name of the file to upload', + }, + content: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The content of the file to upload', + }, + folderSelector: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Select the folder to upload the file to', + }, + folderId: { + type: 'string', + required: false, + visibility: 'hidden', + description: 'The ID of the folder to upload the file to (internal use)', + }, + }, + request: { + url: (params) => { + let fileName = params.fileName || 'untitled' + + // Always create .txt files for text content + if (!fileName.endsWith('.txt')) { + // Remove any existing extensions and add .txt + fileName = fileName.replace(/\.[^.]*$/, '') + '.txt' + } + + // Build the proper URL based on parent folder + const parentFolderId = params.folderSelector || params.folderId + if (parentFolderId && parentFolderId.trim() !== '') { + return `https://graph.microsoft.com/v1.0/me/drive/items/${parentFolderId}:/${fileName}:/content` + } + // Default to root folder + return `https://graph.microsoft.com/v1.0/me/drive/root:/${fileName}:/content` + }, + method: 'PUT', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'text/plain', + }), + body: (params) => (params.content || '') as unknown as Record, + }, + transformResponse: async (response: Response, params?: OneDriveToolParams) => { + try { + if (!response.ok) { + const errorData = await response.json().catch(() => ({})) + logger.error('Failed to upload file to OneDrive', { + status: response.status, + statusText: response.statusText, + errorData, + }) + throw new Error(errorData.error?.message || 'Failed to upload file to OneDrive') + } + + // Microsoft Graph API returns the file metadata directly + const fileData = await response.json() + + logger.info('Successfully uploaded file to OneDrive', { + fileId: fileData.id, + fileName: fileData.name, + }) + + return { + success: true, + output: { + file: { + id: fileData.id, + name: fileData.name, + mimeType: fileData.file?.mimeType || params?.mimeType || 'text/plain', + webViewLink: fileData.webUrl, + webContentLink: fileData['@microsoft.graph.downloadUrl'], + size: fileData.size, + createdTime: fileData.createdDateTime, + modifiedTime: fileData.lastModifiedDateTime, + parentReference: fileData.parentReference, + }, + }, + } + } catch (error: any) { + logger.error('Error in upload transformation', { + error: error.message, + stack: error.stack, + }) + throw error + } + }, + transformError: (error) => { + logger.error('Upload error', { + error: error.message, + stack: error.stack, + }) + return error.message || 'An error occurred while uploading to OneDrive' + }, +} \ No newline at end of file diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index fced0945d22..84f33c7ecd9 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -99,7 +99,7 @@ import { notionSearchTool, notionWriteTool, } from '@/tools/notion' -import { onedriveCreateFolderTool, onedriveListTool } from '@/tools/onedrive' +import { onedriveCreateFolderTool, onedriveListTool, onedriveUploadTool } from '@/tools/onedrive' import { imageTool, embeddingsTool as openAIEmbeddings } from '@/tools/openai' import { outlookDraftTool, outlookReadTool, outlookSendTool } from '@/tools/outlook' import { perplexityChatTool } from '@/tools/perplexity' @@ -277,6 +277,7 @@ export const tools: Record = { linear_create_issue: linearCreateIssueTool, onedrive_create_folder: onedriveCreateFolderTool, onedrive_list: onedriveListTool, + onedrive_upload: onedriveUploadTool, microsoft_excel_read: microsoftExcelReadTool, microsoft_excel_write: microsoftExcelWriteTool, microsoft_excel_table_add: microsoftExcelTableAddTool, From 95275a0a3ffe6091f40f07cb29ae5b8ccd125269 Mon Sep 17 00:00:00 2001 From: Adam Gough Date: Tue, 5 Aug 2025 20:25:40 -0700 Subject: [PATCH 22/23] bun run lint --- apps/docs/content/docs/tools/meta.json | 2 +- .../components/microsoft-file-selector.tsx | 15 ++++++++++----- apps/sim/tools/onedrive/upload.ts | 15 +++++++++++---- 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/apps/docs/content/docs/tools/meta.json b/apps/docs/content/docs/tools/meta.json index 3347e27cb1d..b4ba8f9277f 100644 --- a/apps/docs/content/docs/tools/meta.json +++ b/apps/docs/content/docs/tools/meta.json @@ -62,4 +62,4 @@ "x", "youtube" ] -} \ No newline at end of file +} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/microsoft-file-selector.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/microsoft-file-selector.tsx index e3cc39291be..d11c3e547fa 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/microsoft-file-selector.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/microsoft-file-selector.tsx @@ -827,10 +827,13 @@ export function MicrosoftFileSelector({ className='flex items-center gap-1 text-primary text-xs hover:underline' onClick={(e) => e.stopPropagation()} > - - {serviceId === 'microsoft-planner' ? 'Open in Planner' : - serviceId === 'sharepoint' ? 'Open in SharePoint' : 'Open in OneDrive'} - + + {serviceId === 'microsoft-planner' + ? 'Open in Planner' + : serviceId === 'sharepoint' + ? 'Open in SharePoint' + : 'Open in OneDrive'} + ) : ( @@ -841,7 +844,9 @@ export function MicrosoftFileSelector({ className='flex items-center gap-1 text-primary text-xs hover:underline' onClick={(e) => e.stopPropagation()} > - {serviceId === 'sharepoint' ? 'Open in SharePoint' : 'Open in OneDrive'} + + {serviceId === 'sharepoint' ? 'Open in SharePoint' : 'Open in OneDrive'} + )} diff --git a/apps/sim/tools/onedrive/upload.ts b/apps/sim/tools/onedrive/upload.ts index 01761c9ddc7..5783351bb53 100644 --- a/apps/sim/tools/onedrive/upload.ts +++ b/apps/sim/tools/onedrive/upload.ts @@ -12,7 +12,14 @@ export const uploadTool: ToolConfig oauth: { required: true, provider: 'onedrive', - additionalScopes: ['openid', 'profile', 'email', 'Files.Read', 'Files.ReadWrite', 'offline_access'], + additionalScopes: [ + 'openid', + 'profile', + 'email', + 'Files.Read', + 'Files.ReadWrite', + 'offline_access', + ], }, params: { accessToken: { @@ -53,7 +60,7 @@ export const uploadTool: ToolConfig // Always create .txt files for text content if (!fileName.endsWith('.txt')) { // Remove any existing extensions and add .txt - fileName = fileName.replace(/\.[^.]*$/, '') + '.txt' + fileName = `${fileName.replace(/\.[^.]*$/, '')}.txt` } // Build the proper URL based on parent folder @@ -61,7 +68,7 @@ export const uploadTool: ToolConfig if (parentFolderId && parentFolderId.trim() !== '') { return `https://graph.microsoft.com/v1.0/me/drive/items/${parentFolderId}:/${fileName}:/content` } - // Default to root folder + // Default to root folder return `https://graph.microsoft.com/v1.0/me/drive/root:/${fileName}:/content` }, method: 'PUT', @@ -122,4 +129,4 @@ export const uploadTool: ToolConfig }) return error.message || 'An error occurred while uploading to OneDrive' }, -} \ No newline at end of file +} From ccd0e9a918d8b1eea0ee8e12508a67014476f904 Mon Sep 17 00:00:00 2001 From: Adam Gough Date: Tue, 5 Aug 2025 20:30:30 -0700 Subject: [PATCH 23/23] added folder name --- apps/sim/tools/onedrive/types.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/sim/tools/onedrive/types.ts b/apps/sim/tools/onedrive/types.ts index 34494253605..20056ebdfd7 100644 --- a/apps/sim/tools/onedrive/types.ts +++ b/apps/sim/tools/onedrive/types.ts @@ -50,6 +50,7 @@ export interface OneDriveToolParams { accessToken: string folderId?: string folderSelector?: string + folderName?: string fileId?: string fileName?: string content?: string