diff --git a/sim/app/api/marketplace/[id]/view/route.ts b/sim/app/api/marketplace/[id]/view/route.ts index edfc385174..d376a81e58 100644 --- a/sim/app/api/marketplace/[id]/view/route.ts +++ b/sim/app/api/marketplace/[id]/view/route.ts @@ -10,7 +10,7 @@ const logger = createLogger('MarketplaceViewAPI') /** * POST handler for incrementing the view count when a workflow card is clicked * This endpoint is called from the WorkflowCard component's onClick handler - * + * * The ID parameter is the marketplace entry ID, not the workflow ID */ export async function POST(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { @@ -37,8 +37,8 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{ // Increment the view count for this workflow await db .update(schema.marketplace) - .set({ - views: sql`${schema.marketplace.views} + 1` + .set({ + views: sql`${schema.marketplace.views} + 1`, }) .where(eq(schema.marketplace.id, id)) @@ -54,4 +54,4 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{ ) return createErrorResponse('Failed to track view', 500) } -} \ No newline at end of file +} diff --git a/sim/app/api/marketplace/publish/route.ts b/sim/app/api/marketplace/publish/route.ts index ff5a4e6b2e..f13accfa48 100644 --- a/sim/app/api/marketplace/publish/route.ts +++ b/sim/app/api/marketplace/publish/route.ts @@ -1,11 +1,11 @@ import { NextRequest, NextResponse } from 'next/server' -import { z } from 'zod' +import { eq } from 'drizzle-orm' import { v4 as uuidv4 } from 'uuid' +import { z } from 'zod' import { getSession } from '@/lib/auth' import { createLogger } from '@/lib/logs/console-logger' import { db } from '@/db' import { marketplace, user, workflow } from '@/db/schema' -import { eq } from 'drizzle-orm' // Create a logger for this module const logger = createLogger('MarketplacePublishAPI') @@ -40,7 +40,8 @@ export async function POST(request: NextRequest) { try { // Parse request body const body = await request.json() - const { workflowId, name, description, category, authorName, workflowState } = PublishRequestSchema.parse(body) + const { workflowId, name, description, category, authorName, workflowState } = + PublishRequestSchema.parse(body) // Check if the workflow belongs to the user const userWorkflow = await db @@ -50,7 +51,9 @@ export async function POST(request: NextRequest) { .limit(1) if (!userWorkflow.length || userWorkflow[0].id !== workflowId) { - logger.warn(`[${requestId}] User ${userId} attempted to publish workflow they don't own: ${workflowId}`) + logger.warn( + `[${requestId}] User ${userId} attempted to publish workflow they don't own: ${workflowId}` + ) return NextResponse.json({ error: 'Workflow not found' }, { status: 404 }) } @@ -148,4 +151,4 @@ export async function POST(request: NextRequest) { logger.error(`[${requestId}] Marketplace publish error`, error) return NextResponse.json({ error: error.message }, { status: 500 }) } -} \ No newline at end of file +} diff --git a/sim/app/api/marketplace/workflows/route.ts b/sim/app/api/marketplace/workflows/route.ts index 479fef806f..4aa2c9c9f1 100644 --- a/sim/app/api/marketplace/workflows/route.ts +++ b/sim/app/api/marketplace/workflows/route.ts @@ -2,9 +2,9 @@ import { NextRequest, NextResponse } from 'next/server' import { desc, eq, sql } from 'drizzle-orm' import { createLogger } from '@/lib/logs/console-logger' import { createErrorResponse, createSuccessResponse } from '@/app/api/workflows/utils' +import { CATEGORIES } from '@/app/w/marketplace/constants/categories' import { db } from '@/db' import * as schema from '@/db/schema' -import { CATEGORIES } from '@/app/w/marketplace/constants/categories' const logger = createLogger('MarketplaceWorkflowsAPI') @@ -13,14 +13,14 @@ export const revalidate = 60 /** * Consolidated API endpoint for marketplace workflows - * + * * Supports: * - Getting featured/popular/recent workflows * - Getting workflows by category * - Getting workflow state * - Getting workflow details * - Incrementing view counts - * + * * Query parameters: * - section: 'popular', 'recent', 'byCategory', or specific category name * - limit: Maximum number of items to return per section (default: 6) @@ -30,7 +30,7 @@ export const revalidate = 60 */ export async function GET(request: NextRequest) { const requestId = crypto.randomUUID().slice(0, 8) - + try { // Parse query parameters const url = new URL(request.url) @@ -41,11 +41,11 @@ export async function GET(request: NextRequest) { const includeState = url.searchParams.get('includeState') === 'true' const workflowId = url.searchParams.get('workflowId') const marketplaceId = url.searchParams.get('marketplaceId') - + // Handle single workflow request first (by workflow ID) if (workflowId) { - let marketplaceEntry; - + let marketplaceEntry + if (includeState) { // Query with state included marketplaceEntry = await db @@ -66,7 +66,7 @@ export async function GET(request: NextRequest) { .from(schema.marketplace) .where(eq(schema.marketplace.workflowId, workflowId)) .limit(1) - .then((rows) => rows[0]); + .then((rows) => rows[0]) } else { // Query without state marketplaceEntry = await db @@ -86,7 +86,7 @@ export async function GET(request: NextRequest) { .from(schema.marketplace) .where(eq(schema.marketplace.workflowId, workflowId)) .limit(1) - .then((rows) => rows[0]); + .then((rows) => rows[0]) } if (!marketplaceEntry) { @@ -95,22 +95,23 @@ export async function GET(request: NextRequest) { } // Transform response if state was requested - const responseData = includeState && 'state' in marketplaceEntry - ? { - ...marketplaceEntry, - workflowState: marketplaceEntry.state, - state: undefined, - } - : marketplaceEntry; + const responseData = + includeState && 'state' in marketplaceEntry + ? { + ...marketplaceEntry, + workflowState: marketplaceEntry.state, + state: undefined, + } + : marketplaceEntry logger.info(`[${requestId}] Retrieved marketplace data for workflow: ${workflowId}`) return createSuccessResponse(responseData) } - + // Handle single marketplace entry request (by marketplace ID) if (marketplaceId) { - let marketplaceEntry; - + let marketplaceEntry + if (includeState) { // Query with state included marketplaceEntry = await db @@ -131,7 +132,7 @@ export async function GET(request: NextRequest) { .from(schema.marketplace) .where(eq(schema.marketplace.id, marketplaceId)) .limit(1) - .then((rows) => rows[0]); + .then((rows) => rows[0]) } else { // Query without state marketplaceEntry = await db @@ -151,7 +152,7 @@ export async function GET(request: NextRequest) { .from(schema.marketplace) .where(eq(schema.marketplace.id, marketplaceId)) .limit(1) - .then((rows) => rows[0]); + .then((rows) => rows[0]) } if (!marketplaceEntry) { @@ -160,18 +161,19 @@ export async function GET(request: NextRequest) { } // Transform response if state was requested - const responseData = includeState && 'state' in marketplaceEntry - ? { - ...marketplaceEntry, - workflowState: marketplaceEntry.state, - state: undefined, - } - : marketplaceEntry; + const responseData = + includeState && 'state' in marketplaceEntry + ? { + ...marketplaceEntry, + workflowState: marketplaceEntry.state, + state: undefined, + } + : marketplaceEntry logger.info(`[${requestId}] Retrieved marketplace entry: ${marketplaceId}`) return createSuccessResponse(responseData) } - + // Handle featured/collection requests const result: { popular: any[] @@ -180,7 +182,7 @@ export async function GET(request: NextRequest) { } = { popular: [], recent: [], - byCategory: {} + byCategory: {}, } // Define common fields to select @@ -198,13 +200,13 @@ export async function GET(request: NextRequest) { } // Add state if requested - const selectFields = includeState + const selectFields = includeState ? { ...baseFields, state: schema.marketplace.state } - : baseFields; + : baseFields // Determine which sections to fetch const sections = sectionParam ? sectionParam.split(',') : ['popular', 'recent', 'byCategory'] - + // Get popular items if requested if (sections.includes('popular')) { result.popular = await db @@ -213,7 +215,7 @@ export async function GET(request: NextRequest) { .orderBy(desc(schema.marketplace.stars), desc(schema.marketplace.views)) .limit(limit) } - + // Get recent items if requested if (sections.includes('recent')) { result.recent = await db @@ -222,33 +224,37 @@ export async function GET(request: NextRequest) { .orderBy(desc(schema.marketplace.createdAt)) .limit(limit) } - + // Get categories if requested - if (sections.includes('byCategory') || categoryParam || sections.some(s => CATEGORIES.some(c => c.value === s))) { + if ( + sections.includes('byCategory') || + categoryParam || + sections.some((s) => CATEGORIES.some((c) => c.value === s)) + ) { // Identify all requested categories - const requestedCategories = new Set(); - + const requestedCategories = new Set() + // Add explicitly requested category if (categoryParam) { - requestedCategories.add(categoryParam); + requestedCategories.add(categoryParam) } - + // Add categories from sections parameter - sections.forEach(section => { - if (CATEGORIES.some(c => c.value === section)) { - requestedCategories.add(section); + sections.forEach((section) => { + if (CATEGORIES.some((c) => c.value === section)) { + requestedCategories.add(section) } - }); - + }) + // Include byCategory section contents if requested if (sections.includes('byCategory')) { - CATEGORIES.forEach(c => requestedCategories.add(c.value)); + CATEGORIES.forEach((c) => requestedCategories.add(c.value)) } - + // Log what we're fetching - const categoriesToFetch = Array.from(requestedCategories); - logger.info(`[${requestId}] Fetching specific categories: ${categoriesToFetch.join(', ')}`); - + const categoriesToFetch = Array.from(requestedCategories) + logger.info(`[${requestId}] Fetching specific categories: ${categoriesToFetch.join(', ')}`) + // Process each requested category await Promise.all( categoriesToFetch.map(async (categoryValue) => { @@ -257,40 +263,44 @@ export async function GET(request: NextRequest) { .from(schema.marketplace) .where(eq(schema.marketplace.category, categoryValue)) .orderBy(desc(schema.marketplace.stars), desc(schema.marketplace.views)) - .limit(limit); - + .limit(limit) + // Always add the category to the result, even if empty - result.byCategory[categoryValue] = categoryItems; - logger.info(`[${requestId}] Category ${categoryValue}: found ${categoryItems.length} items`); + result.byCategory[categoryValue] = categoryItems + logger.info( + `[${requestId}] Category ${categoryValue}: found ${categoryItems.length} items` + ) }) - ); + ) } // Transform the data if state was included to match the expected format if (includeState) { const transformSection = (section: any[]) => { - return section.map(item => - 'state' in item ? { - ...item, - workflowState: item.state, - state: undefined - } : item - ); - }; + return section.map((item) => + 'state' in item + ? { + ...item, + workflowState: item.state, + state: undefined, + } + : item + ) + } if (result.popular.length > 0) { - result.popular = transformSection(result.popular); + result.popular = transformSection(result.popular) } - + if (result.recent.length > 0) { - result.recent = transformSection(result.recent); + result.recent = transformSection(result.recent) } - - Object.keys(result.byCategory).forEach(category => { + + Object.keys(result.byCategory).forEach((category) => { if (result.byCategory[category].length > 0) { - result.byCategory[category] = transformSection(result.byCategory[category]); + result.byCategory[category] = transformSection(result.byCategory[category]) } - }); + }) } logger.info(`[${requestId}] Fetched marketplace items${includeState ? ' with state' : ''}`) @@ -303,17 +313,17 @@ export async function GET(request: NextRequest) { /** * POST handler for incrementing view counts - * + * * Request body: * - id: Marketplace entry ID to increment view count for */ export async function POST(request: NextRequest) { const requestId = crypto.randomUUID().slice(0, 8) - + try { const body = await request.json() const { id } = body - + if (!id) { return createErrorResponse('Marketplace ID is required', 400) } @@ -336,8 +346,8 @@ export async function POST(request: NextRequest) { // Increment the view count await db .update(schema.marketplace) - .set({ - views: sql`${schema.marketplace.views} + 1` + .set({ + views: sql`${schema.marketplace.views} + 1`, }) .where(eq(schema.marketplace.id, id)) @@ -350,4 +360,4 @@ export async function POST(request: NextRequest) { logger.error(`[${requestId}] Error incrementing view count`, error) return createErrorResponse(`Failed to track view: ${error.message}`, 500) } -} \ No newline at end of file +} diff --git a/sim/app/api/user/api-keys/[id]/route.ts b/sim/app/api/user/api-keys/[id]/route.ts index 189675cc15..69fb95d622 100644 --- a/sim/app/api/user/api-keys/[id]/route.ts +++ b/sim/app/api/user/api-keys/[id]/route.ts @@ -2,8 +2,8 @@ import { NextRequest, NextResponse } from 'next/server' import { and, eq } from 'drizzle-orm' import { getSession } from '@/lib/auth' import { createLogger } from '@/lib/logs/console-logger' -import { apiKey } from '@/db/schema' import { db } from '@/db' +import { apiKey } from '@/db/schema' const logger = createLogger('ApiKeyRoute') diff --git a/sim/app/api/webhooks/test/route.ts b/sim/app/api/webhooks/test/route.ts index 8e71485267..1fa78e7867 100644 --- a/sim/app/api/webhooks/test/route.ts +++ b/sim/app/api/webhooks/test/route.ts @@ -226,7 +226,7 @@ export async function GET(request: NextRequest) { case 'slack': { const signingSecret = providerConfig.signingSecret - + if (!signingSecret) { logger.warn(`[${requestId}] Slack webhook missing signing secret: ${webhookId}`) return NextResponse.json( @@ -234,7 +234,7 @@ export async function GET(request: NextRequest) { { status: 400 } ) } - + logger.info(`[${requestId}] Slack webhook test successful: ${webhookId}`) return NextResponse.json({ success: true, @@ -254,23 +254,23 @@ export async function GET(request: NextRequest) { curlCommand: [ `curl -X POST "${webhookUrl}"`, `-H "Content-Type: application/json"`, - `-H "X-Slack-Request-Timestamp: $(date +%s)"`, + `-H "X-Slack-Request-Timestamp: $(date +%s)"`, `-H "X-Slack-Signature: v0=$(date +%s)"`, - `-d '{"type":"event_callback","event":{"type":"message","channel":"C0123456789","user":"U0123456789","text":"Hello from Slack!","ts":"1234567890.123456"},"team_id":"T0123456789"}'` + `-d '{"type":"event_callback","event":{"type":"message","channel":"C0123456789","user":"U0123456789","text":"Hello from Slack!","ts":"1234567890.123456"},"team_id":"T0123456789"}'`, ].join(' \\\n'), samplePayload: { - "type": "event_callback", - "token": "XXYYZZ", - "team_id": "T123ABC", - "event": { - "type": "message", - "user": "U123ABC", - "text": "Hello from Slack!", - "ts": "1234567890.1234" + type: 'event_callback', + token: 'XXYYZZ', + team_id: 'T123ABC', + event: { + type: 'message', + user: 'U123ABC', + text: 'Hello from Slack!', + ts: '1234567890.1234', }, - "event_id": "Ev123ABC" - } - } + event_id: 'Ev123ABC', + }, + }, }) } default: { diff --git a/sim/app/api/webhooks/trigger/[path]/route.ts b/sim/app/api/webhooks/trigger/[path]/route.ts index 3f5a0bf8d1..4c05e09bb1 100644 --- a/sim/app/api/webhooks/trigger/[path]/route.ts +++ b/sim/app/api/webhooks/trigger/[path]/route.ts @@ -12,7 +12,7 @@ import { db } from '@/db' import { environment, userStats, webhook, workflow } from '@/db/schema' import { Executor } from '@/executor' import { Serializer } from '@/serializer' -import {validateSlackSignature} from '../../utils' +import { validateSlackSignature } from '../../utils' const logger = createLogger('WebhookTriggerAPI') @@ -118,12 +118,12 @@ export async function POST( try { const path = (await params).path - + // Clone the request to get both the raw body for Slack signature verification // and the parsed JSON body for processing const requestClone = request.clone() rawBody = await requestClone.text() - + // Parse the request body const body = JSON.parse(rawBody || '{}') logger.info(`[${requestId}] Webhook POST request received for path: ${path}`) @@ -168,16 +168,16 @@ export async function POST( // Validate Slack signature if this is a Slack webhook const providerConfig = (foundWebhook.providerConfig as Record) || {} const signingSecret = providerConfig.signingSecret - + if (signingSecret) { const slackSignature = request.headers.get('x-slack-signature') const slackTimestamp = request.headers.get('x-slack-request-timestamp') - + if (!slackSignature || !slackTimestamp || !rawBody) { logger.warn(`[${requestId}] Missing Slack signature headers`, { hasSignature: !!slackSignature, hasTimestamp: !!slackTimestamp, - hasBody: !!rawBody + hasBody: !!rawBody, }) return NextResponse.json({ error: 'Invalid Slack request' }, { status: 400 }) } @@ -189,14 +189,14 @@ export async function POST( slackTimestamp, rawBody ) - + if (!isValid) { logger.warn(`[${requestId}] Invalid Slack signature`) return NextResponse.json({ error: 'Invalid signature' }, { status: 401 }) } - + logger.info(`[${requestId}] Slack signature validated successfully`) - + // Handle Slack URL verification challenge during POST if (body.type === 'url_verification' && body.challenge) { logger.info(`[${requestId}] Responding to Slack URL verification challenge`) @@ -216,13 +216,19 @@ export async function POST( if (messageId) { await markMessageAsProcessed(messageId) } - + // Mark this request as processed to prevent duplicates await markMessageAsProcessed(requestHash, 60 * 60 * 24) - - // Process the webhook for Slack - return await processWebhook(foundWebhook, foundWorkflow, body, request, executionId, requestId) + // Process the webhook for Slack + return await processWebhook( + foundWebhook, + foundWorkflow, + body, + request, + executionId, + requestId + ) } else if (foundWebhook.provider === 'whatsapp') { // Extract WhatsApp specific data const data = body?.entry?.[0]?.changes?.[0]?.value diff --git a/sim/app/api/webhooks/utils.ts b/sim/app/api/webhooks/utils.ts index 61dc6d570e..d0d418429f 100644 --- a/sim/app/api/webhooks/utils.ts +++ b/sim/app/api/webhooks/utils.ts @@ -8,63 +8,59 @@ */ export async function validateSlackSignature( - signingSecret: string, - signature: string, - timestamp: string, - body: string - ): Promise { - try { - // Basic validation first - if (!signingSecret || !signature || !timestamp || !body) { - return false - } - - // Check if the timestamp is too old (> 5 minutes) - const currentTime = Math.floor(Date.now() / 1000) - if (Math.abs(currentTime - parseInt(timestamp)) > 300) { - return false - } - - // Compute the signature - const encoder = new TextEncoder() - const baseString = `v0:${timestamp}:${body}` - - // Create the HMAC with the signing secret - const key = await crypto.subtle.importKey( - 'raw', - encoder.encode(signingSecret), - { name: 'HMAC', hash: 'SHA-256' }, - false, - ['sign'] - ) - - const signatureBytes = await crypto.subtle.sign( - 'HMAC', - key, - encoder.encode(baseString) - ) - - // Convert the signature to hex - const signatureHex = Array.from(new Uint8Array(signatureBytes)) - .map(b => b.toString(16).padStart(2, '0')) - .join('') - - // Prepare the expected signature format - const computedSignature = `v0=${signatureHex}` - - // Constant-time comparison to prevent timing attacks - if (computedSignature.length !== signature.length) { - return false - } - - let result = 0 - for (let i = 0; i < computedSignature.length; i++) { - result |= computedSignature.charCodeAt(i) ^ signature.charCodeAt(i) - } - - return result === 0 - } catch (error) { - console.error('Error validating Slack signature:', error) + signingSecret: string, + signature: string, + timestamp: string, + body: string +): Promise { + try { + // Basic validation first + if (!signingSecret || !signature || !timestamp || !body) { return false } - } \ No newline at end of file + + // Check if the timestamp is too old (> 5 minutes) + const currentTime = Math.floor(Date.now() / 1000) + if (Math.abs(currentTime - parseInt(timestamp)) > 300) { + return false + } + + // Compute the signature + const encoder = new TextEncoder() + const baseString = `v0:${timestamp}:${body}` + + // Create the HMAC with the signing secret + const key = await crypto.subtle.importKey( + 'raw', + encoder.encode(signingSecret), + { name: 'HMAC', hash: 'SHA-256' }, + false, + ['sign'] + ) + + const signatureBytes = await crypto.subtle.sign('HMAC', key, encoder.encode(baseString)) + + // Convert the signature to hex + const signatureHex = Array.from(new Uint8Array(signatureBytes)) + .map((b) => b.toString(16).padStart(2, '0')) + .join('') + + // Prepare the expected signature format + const computedSignature = `v0=${signatureHex}` + + // Constant-time comparison to prevent timing attacks + if (computedSignature.length !== signature.length) { + return false + } + + let result = 0 + for (let i = 0; i < computedSignature.length; i++) { + result |= computedSignature.charCodeAt(i) ^ signature.charCodeAt(i) + } + + return result === 0 + } catch (error) { + console.error('Error validating Slack signature:', error) + return false + } +} diff --git a/sim/app/api/workflows/public/[id]/route.ts b/sim/app/api/workflows/public/[id]/route.ts index 642ad9b3ad..a8a4c2e651 100644 --- a/sim/app/api/workflows/public/[id]/route.ts +++ b/sim/app/api/workflows/public/[id]/route.ts @@ -24,7 +24,7 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{ state: schema.marketplace.state, name: schema.marketplace.name, description: schema.marketplace.description, - authorId: schema.marketplace.authorId, + authorId: schema.marketplace.authorId, authorName: schema.marketplace.authorName, }) .from(schema.marketplace) @@ -65,4 +65,4 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{ logger.error(`[${requestId}] Error getting public workflow: ${(await params).id}`, error) return createErrorResponse('Failed to get public workflow', 500) } -} \ No newline at end of file +} diff --git a/sim/app/globals.css b/sim/app/globals.css index a3c80b587b..adf8585d94 100644 --- a/sim/app/globals.css +++ b/sim/app/globals.css @@ -33,7 +33,7 @@ --ring: 222.2 84% 4.9%; --radius: 0.5rem; - + /* Scrollbar Custom Properties */ --scrollbar-track: 0 0% 85%; --scrollbar-thumb: 0 0% 65%; @@ -69,7 +69,7 @@ --border: 217.2 32.6% 17.5%; --input: 217.2 32.6% 17.5%; --ring: 212.7 26.8% 83.9%; - + /* Dark Mode Scrollbar Custom Properties */ --scrollbar-track: 217.2 32.6% 17.5%; --scrollbar-thumb: 217.2 32.6% 30%; @@ -87,27 +87,27 @@ @apply bg-background text-foreground; overscroll-behavior-x: none; } - + /* Global Scrollbar Styling */ ::-webkit-scrollbar { width: var(--scrollbar-size); height: var(--scrollbar-size); } - + ::-webkit-scrollbar-track { background-color: hsl(var(--scrollbar-track)); border-radius: var(--radius); } - + ::-webkit-scrollbar-thumb { background-color: hsl(var(--scrollbar-thumb)); border-radius: var(--radius); } - + ::-webkit-scrollbar-thumb:hover { background-color: hsl(var(--scrollbar-thumb-hover)); } - + /* For Firefox */ * { scrollbar-width: thin; @@ -138,13 +138,13 @@ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); transition-duration: 300ms; } - + /* Custom Scrollbar Utility Classes */ .scrollbar-none { -ms-overflow-style: none; scrollbar-width: none; } - + .scrollbar-none::-webkit-scrollbar { display: none; } diff --git a/sim/app/w/[id]/components/workflow-block/components/sub-block/components/webhook/components/providers/slack-config.tsx b/sim/app/w/[id]/components/workflow-block/components/sub-block/components/webhook/components/providers/slack-config.tsx index 155c5742ae..ea5921a23f 100644 --- a/sim/app/w/[id]/components/workflow-block/components/sub-block/components/webhook/components/providers/slack-config.tsx +++ b/sim/app/w/[id]/components/workflow-block/components/sub-block/components/webhook/components/providers/slack-config.tsx @@ -13,18 +13,17 @@ interface SlackConfigProps { test?: any } | null copied: string | null - copyToClipboard: (text: string, type: string) => void + copyToClipboard: (text: string, type: string) => void testWebhook: () => Promise - } export function SlackConfig({ - signingSecret, - setSigningSecret, - isLoadingToken, - testResult, - copied, - copyToClipboard, + signingSecret, + setSigningSecret, + isLoadingToken, + testResult, + copied, + copyToClipboard, }: SlackConfigProps) { return (
@@ -57,7 +56,17 @@ export function SlackConfig({

Setup Instructions

    -
  1. Go to your Slack Apps page
  2. +
  3. + Go to your{' '} + + Slack Apps page + +
  4. Create a new app or select an existing one
  5. Navigate to "Event Subscriptions" in the left sidebar
  6. Enable events and add the Webhook URL above as the Request URL
  7. diff --git a/sim/app/w/[id]/components/workflow-block/components/sub-block/components/webhook/components/ui/webhook-footer.tsx b/sim/app/w/[id]/components/workflow-block/components/sub-block/components/webhook/components/ui/webhook-footer.tsx index e4b38aaa02..dd56ad2b0a 100644 --- a/sim/app/w/[id]/components/workflow-block/components/sub-block/components/webhook/components/ui/webhook-footer.tsx +++ b/sim/app/w/[id]/components/workflow-block/components/sub-block/components/webhook/components/ui/webhook-footer.tsx @@ -28,7 +28,11 @@ export function WebhookDialogFooter({ onClose, }: WebhookDialogFooterProps) { const showTestButton = - webhookId && (webhookProvider === 'whatsapp' || webhookProvider === 'generic' || webhookProvider === "slack") && onTest + webhookId && + (webhookProvider === 'whatsapp' || + webhookProvider === 'generic' || + webhookProvider === 'slack') && + onTest return ( diff --git a/sim/app/w/[id]/components/workflow-block/components/sub-block/components/webhook/components/webhook-modal.tsx b/sim/app/w/[id]/components/workflow-block/components/sub-block/components/webhook/components/webhook-modal.tsx index 2f9ccb2c14..47b68722ef 100644 --- a/sim/app/w/[id]/components/workflow-block/components/sub-block/components/webhook/components/webhook-modal.tsx +++ b/sim/app/w/[id]/components/workflow-block/components/sub-block/components/webhook/components/webhook-modal.tsx @@ -5,6 +5,7 @@ import { ProviderConfig, WEBHOOK_PROVIDERS } from '../webhook-config' import { DiscordConfig } from './providers/discord-config' import { GenericConfig } from './providers/generic-config' import { GithubConfig } from './providers/github-config' +import { SlackConfig } from './providers/slack-config' import { StripeConfig } from './providers/stripe-config' import { WhatsAppConfig } from './providers/whatsapp-config' import { DeleteConfirmDialog } from './ui/confirmation' @@ -12,7 +13,6 @@ import { UnsavedChangesDialog } from './ui/confirmation' import { WebhookDialogFooter } from './ui/webhook-footer' import { WebhookDialogHeader } from './ui/webhook-header' import { WebhookUrlField } from './ui/webhook-url' -import { SlackConfig } from './providers/slack-config' const logger = createLogger('WebhookModal') @@ -213,8 +213,7 @@ export function WebhookModal({ secretHeaderName !== originalValues.secretHeaderName || requireAuth !== originalValues.requireAuth || allowedIps !== originalValues.allowedIps)) || - (webhookProvider === 'slack' && - slackSigningSecret !== originalValues.slackSigningSecret) + (webhookProvider === 'slack' && slackSigningSecret !== originalValues.slackSigningSecret) setHasUnsavedChanges(hasChanges) }, [ @@ -448,18 +447,18 @@ export function WebhookModal({ copyToClipboard={copyToClipboard} /> ) - case 'slack': - return ( - - ) + case 'slack': + return ( + + ) case 'generic': default: return ( diff --git a/sim/app/w/[id]/components/workflow-block/components/sub-block/components/webhook/webhook-config.tsx b/sim/app/w/[id]/components/workflow-block/components/sub-block/components/webhook/webhook-config.tsx index 34e3861bda..1caaa18347 100644 --- a/sim/app/w/[id]/components/workflow-block/components/sub-block/components/webhook/webhook-config.tsx +++ b/sim/app/w/[id]/components/workflow-block/components/sub-block/components/webhook/webhook-config.tsx @@ -1,7 +1,7 @@ import { useEffect, useState } from 'react' import { useParams } from 'next/navigation' import { CheckCircle2, ExternalLink } from 'lucide-react' -import { DiscordIcon, GithubIcon, StripeIcon, WhatsAppIcon, SlackIcon } from '@/components/icons' +import { DiscordIcon, GithubIcon, SlackIcon, StripeIcon, WhatsAppIcon } from '@/components/icons' import { Button } from '@/components/ui/button' import { createLogger } from '@/lib/logs/console-logger' import { useSubBlockValue } from '../../hooks/use-sub-block-value' @@ -156,7 +156,7 @@ export const WEBHOOK_PROVIDERS: { [key: string]: WebhookProvider } = { icon: (props) => , configFields: { signingSecret: { - type: 'string', + type: 'string', label: 'Signing Secret', placeholder: 'Enter your Slack app signing secret', description: 'The signing secret from your Slack app to validate request authenticity.', diff --git a/sim/app/w/[id]/hooks/use-block-connections.ts b/sim/app/w/[id]/hooks/use-block-connections.ts index df1b950b0e..dfe8a86761 100644 --- a/sim/app/w/[id]/hooks/use-block-connections.ts +++ b/sim/app/w/[id]/hooks/use-block-connections.ts @@ -67,7 +67,7 @@ function findAllPathNodes(edges: any[], targetNodeId: string): string[] { const visited = new Set() const queue: [string, number][] = [[targetNodeId, 0]] // [nodeId, distance] const pathNodes = new Set() - + // Build a reverse adjacency list for faster traversal const reverseAdjList: Record = {} for (const edge of edges) { @@ -76,11 +76,11 @@ function findAllPathNodes(edges: any[], targetNodeId: string): string[] { } reverseAdjList[edge.target].push(edge.source) } - + // BFS to find all ancestors and their shortest distance from target while (queue.length > 0) { const [currentNodeId, distance] = queue.shift()! - + if (visited.has(currentNodeId)) { // If we've seen this node before, update its distance if this path is shorter const currentDistance = nodeDistances.get(currentNodeId) || Infinity @@ -89,24 +89,24 @@ function findAllPathNodes(edges: any[], targetNodeId: string): string[] { } continue } - + visited.add(currentNodeId) nodeDistances.set(currentNodeId, distance) - + // Don't add the target node itself to the results if (currentNodeId !== targetNodeId) { pathNodes.add(currentNodeId) } - + // Get all incoming edges from the reverse adjacency list const incomingNodeIds = reverseAdjList[currentNodeId] || [] - + // Add all source nodes to the queue with incremented distance for (const sourceId of incomingNodeIds) { queue.push([sourceId, distance + 1]) } } - + return Array.from(pathNodes) } @@ -121,46 +121,46 @@ export function useBlockConnections(blockId: string) { // Find all blocks along paths leading to this block const allPathNodeIds = findAllPathNodes(edges, blockId) - + // Map each path node to a ConnectedBlock structure - const allPathConnections = allPathNodeIds.map(sourceId => { - const sourceBlock = blocks[sourceId] - if (!sourceBlock) return null - - // Get the response format from the subblock store - const responseFormatValue = useSubBlockStore - .getState() - .getValue(sourceId, 'responseFormat') + const allPathConnections = allPathNodeIds + .map((sourceId) => { + const sourceBlock = blocks[sourceId] + if (!sourceBlock) return null - let responseFormat + // Get the response format from the subblock store + const responseFormatValue = useSubBlockStore.getState().getValue(sourceId, 'responseFormat') - try { - responseFormat = - typeof responseFormatValue === 'string' && responseFormatValue - ? JSON.parse(responseFormatValue) - : responseFormatValue // Handle case where it's already an object - } catch (e) { - logger.error('Failed to parse response format:', { e }) - responseFormat = undefined - } + let responseFormat - // Get the default output type from the block's outputs - const defaultOutputs: Field[] = Object.entries(sourceBlock.outputs || {}).map(([key]) => ({ - name: key, - type: 'string', - })) + try { + responseFormat = + typeof responseFormatValue === 'string' && responseFormatValue + ? JSON.parse(responseFormatValue) + : responseFormatValue // Handle case where it's already an object + } catch (e) { + logger.error('Failed to parse response format:', { e }) + responseFormat = undefined + } - // Extract fields from the response format using our helper function - const outputFields = responseFormat ? extractFieldsFromSchema(responseFormat) : defaultOutputs + // Get the default output type from the block's outputs + const defaultOutputs: Field[] = Object.entries(sourceBlock.outputs || {}).map(([key]) => ({ + name: key, + type: 'string', + })) - return { - id: sourceBlock.id, - type: sourceBlock.type, - outputType: outputFields.map((field: Field) => field.name), - name: sourceBlock.name, - responseFormat, - } - }).filter(Boolean) as ConnectedBlock[] + // Extract fields from the response format using our helper function + const outputFields = responseFormat ? extractFieldsFromSchema(responseFormat) : defaultOutputs + + return { + id: sourceBlock.id, + type: sourceBlock.type, + outputType: outputFields.map((field: Field) => field.name), + name: sourceBlock.name, + responseFormat, + } + }) + .filter(Boolean) as ConnectedBlock[] // Keep the original incoming connections for compatibility const directIncomingConnections = edges diff --git a/sim/app/w/[id]/hooks/use-workflow-execution.ts b/sim/app/w/[id]/hooks/use-workflow-execution.ts index f15c6c8376..02651ddb03 100644 --- a/sim/app/w/[id]/hooks/use-workflow-execution.ts +++ b/sim/app/w/[id]/hooks/use-workflow-execution.ts @@ -2,16 +2,16 @@ import { useCallback, useState } from 'react' import { v4 as uuidv4 } from 'uuid' import { createLogger } from '@/lib/logs/console-logger' import { buildTraceSpans } from '@/lib/logs/trace-spans' -import { useConsoleStore } from '@/stores/panel/console/store' import { useExecutionStore } from '@/stores/execution/store' import { useNotificationStore } from '@/stores/notifications/store' +import { useConsoleStore } from '@/stores/panel/console/store' +import { usePanelStore } from '@/stores/panel/store' +import { useVariablesStore } from '@/stores/panel/variables/store' import { useEnvironmentStore } from '@/stores/settings/environment/store' import { useGeneralStore } from '@/stores/settings/general/store' -import { usePanelStore } from '@/stores/panel/store' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' import { mergeSubblockState } from '@/stores/workflows/utils' import { useWorkflowStore } from '@/stores/workflows/workflow/store' -import { useVariablesStore } from '@/stores/panel/variables/store' import { Executor } from '@/executor' import { ExecutionResult } from '@/executor/types' import { Serializer } from '@/serializer' @@ -86,7 +86,7 @@ export function useWorkflowExecution() { if (!isPanelOpen) { togglePanel() } - + // Set active tab to console setActiveTab('console') @@ -136,7 +136,12 @@ export function useWorkflowExecution() { const workflow = new Serializer().serializeWorkflow(mergedStates, edges, loops) // Create executor and store in global state - const newExecutor = new Executor(workflow, currentBlockStates, envVarValues, workflowVariables) + const newExecutor = new Executor( + workflow, + currentBlockStates, + envVarValues, + workflowVariables + ) setExecutor(newExecutor) // Execute workflow diff --git a/sim/app/w/marketplace/constants/categories.tsx b/sim/app/w/marketplace/constants/categories.tsx index 181e8d37ef..6279af69ae 100644 --- a/sim/app/w/marketplace/constants/categories.tsx +++ b/sim/app/w/marketplace/constants/categories.tsx @@ -1,14 +1,14 @@ -import { - Atom, - BotMessageSquare, - Brain, - Code, - Database, - LineChart, - MailIcon, - Store -} from 'lucide-react' import { ReactNode } from 'react' +import { + Atom, + BotMessageSquare, + Brain, + Code, + Database, + LineChart, + MailIcon, + Store, +} from 'lucide-react' export interface Category { value: string @@ -18,59 +18,59 @@ export interface Category { } export const CATEGORIES: Category[] = [ - { - value: 'data', - label: 'Data Analysis', + { + value: 'data', + label: 'Data Analysis', icon: , - color: '#0ea5e9' // sky-500 + color: '#0ea5e9', // sky-500 }, - { - value: 'marketing', - label: 'Marketing', + { + value: 'marketing', + label: 'Marketing', icon: , - color: '#f43f5e' // rose-500 + color: '#f43f5e', // rose-500 }, - { - value: 'sales', - label: 'Sales', + { + value: 'sales', + label: 'Sales', icon: , - color: '#10b981' // emerald-500 + color: '#10b981', // emerald-500 }, { value: 'customer_service', label: 'Customer Service', icon: , - color: '#8b5cf6' // violet-500 + color: '#8b5cf6', // violet-500 }, - { - value: 'research', - label: 'Research', + { + value: 'research', + label: 'Research', icon: , - color: '#f59e0b' // amber-500 + color: '#f59e0b', // amber-500 }, - { - value: 'finance', - label: 'Finance', + { + value: 'finance', + label: 'Finance', icon: , - color: '#14b8a6' // teal-500 + color: '#14b8a6', // teal-500 }, - { - value: 'programming', - label: 'Programming', + { + value: 'programming', + label: 'Programming', icon: , - color: '#6366f1' // indigo-500 + color: '#6366f1', // indigo-500 }, - { - value: 'other', - label: 'Other', + { + value: 'other', + label: 'Other', icon: , - color: '#7F2FFF' // Brand purple + color: '#7F2FFF', // Brand purple }, ] // Helper functions to get category information export const getCategoryByValue = (value: string): Category => { - return CATEGORIES.find(cat => cat.value === value) || CATEGORIES[CATEGORIES.length - 1] + return CATEGORIES.find((cat) => cat.value === value) || CATEGORIES[CATEGORIES.length - 1] } export const getCategoryLabel = (value: string): string => { @@ -83,4 +83,4 @@ export const getCategoryIcon = (value: string): ReactNode => { export const getCategoryColor = (value: string): string => { return getCategoryByValue(value).color -} \ No newline at end of file +} diff --git a/sim/components/icons.tsx b/sim/components/icons.tsx index 4efba58251..1310ddf670 100644 --- a/sim/components/icons.tsx +++ b/sim/components/icons.tsx @@ -1716,7 +1716,6 @@ export function EyeIcon(props: SVGProps) { ) } - export function ConfluenceIcon(props: SVGProps) { return ( ) { aria-hidden="true" xmlns="http://www.w3.org/2000/svg" > - - ); -} \ No newline at end of file + ) +} diff --git a/sim/components/ui/command.tsx b/sim/components/ui/command.tsx index c73faee67d..bd415fc07c 100644 --- a/sim/components/ui/command.tsx +++ b/sim/components/ui/command.tsx @@ -42,6 +42,12 @@ import { cn } from '@/lib/utils' // This file is not typed correctly from shadcn, so we're disabling the type checker // @ts-nocheck +// This file is not typed correctly from shadcn, so we're disabling the type checker +// @ts-nocheck + +// This file is not typed correctly from shadcn, so we're disabling the type checker +// @ts-nocheck + const Command = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef & { diff --git a/sim/components/ui/form.tsx b/sim/components/ui/form.tsx index 4f02ae6a48..76f6b4b509 100644 --- a/sim/components/ui/form.tsx +++ b/sim/components/ui/form.tsx @@ -1,36 +1,33 @@ -"use client" +'use client' -import * as React from "react" -import * as LabelPrimitive from "@radix-ui/react-label" -import { Slot } from "@radix-ui/react-slot" +import * as React from 'react' +import * as LabelPrimitive from '@radix-ui/react-label' +import { Slot } from '@radix-ui/react-slot' import { Controller, - FormProvider, - useFormContext, type ControllerProps, type FieldPath, type FieldValues, -} from "react-hook-form" - -import { cn } from "@/lib/utils" -import { Label } from "@/components/ui/label" + FormProvider, + useFormContext, +} from 'react-hook-form' +import { Label } from '@/components/ui/label' +import { cn } from '@/lib/utils' const Form = FormProvider type FormFieldContextValue< TFieldValues extends FieldValues = FieldValues, - TName extends FieldPath = FieldPath + TName extends FieldPath = FieldPath, > = { name: TName } -const FormFieldContext = React.createContext( - {} as FormFieldContextValue -) +const FormFieldContext = React.createContext({} as FormFieldContextValue) const FormField = < TFieldValues extends FieldValues = FieldValues, - TName extends FieldPath = FieldPath + TName extends FieldPath = FieldPath, >({ ...props }: ControllerProps) => { @@ -49,7 +46,7 @@ const useFormField = () => { const fieldState = getFieldState(fieldContext.name, formState) if (!fieldContext) { - throw new Error("useFormField should be used within ") + throw new Error('useFormField should be used within ') } const { id } = itemContext @@ -68,23 +65,20 @@ type FormItemContextValue = { id: string } -const FormItemContext = React.createContext( - {} as FormItemContextValue +const FormItemContext = React.createContext({} as FormItemContextValue) + +const FormItem = React.forwardRef>( + ({ className, ...props }, ref) => { + const id = React.useId() + + return ( + +
    + + ) + } ) - -const FormItem = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => { - const id = React.useId() - - return ( - -
    - - ) -}) -FormItem.displayName = "FormItem" +FormItem.displayName = 'FormItem' const FormLabel = React.forwardRef< React.ElementRef, @@ -95,13 +89,13 @@ const FormLabel = React.forwardRef< return (