fix(constants): modify consts to match new api routes, run format

This commit is contained in:
Waleed Latif
2025-03-26 16:09:21 -07:00
parent fe2b64050a
commit c47d704160
35 changed files with 700 additions and 846 deletions
+4 -4
View File
@@ -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)
}
}
}
+8 -5
View File
@@ -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 })
}
}
}
+86 -76
View File
@@ -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<string>();
const requestedCategories = new Set<string>()
// 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)
}
}
}
+1 -1
View File
@@ -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')
+15 -15
View File
@@ -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: {
+19 -13
View File
@@ -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<string, any>) || {}
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
+54 -58
View File
@@ -8,63 +8,59 @@
*/
export async function validateSlackSignature(
signingSecret: string,
signature: string,
timestamp: string,
body: string
): Promise<boolean> {
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<boolean> {
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)
return false
}
}
+2 -2
View File
@@ -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)
}
}
}
+9 -9
View File
@@ -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;
}
@@ -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<void>
}
export function SlackConfig({
signingSecret,
setSigningSecret,
isLoadingToken,
testResult,
copied,
copyToClipboard,
signingSecret,
setSigningSecret,
isLoadingToken,
testResult,
copied,
copyToClipboard,
}: SlackConfigProps) {
return (
<div className="space-y-4">
@@ -57,7 +56,17 @@ export function SlackConfig({
<div className="bg-gray-50 dark:bg-gray-800 p-3 rounded-md mt-3 border border-gray-200 dark:border-gray-700">
<h4 className="font-medium">Setup Instructions</h4>
<ol className="list-decimal list-inside space-y-1 text-sm">
<li>Go to your <a href="https://api.slack.com/apps" target="_blank" rel="noopener noreferrer" className="text-blue-500 hover:underline">Slack Apps page</a></li>
<li>
Go to your{' '}
<a
href="https://api.slack.com/apps"
target="_blank"
rel="noopener noreferrer"
className="text-blue-500 hover:underline"
>
Slack Apps page
</a>
</li>
<li>Create a new app or select an existing one</li>
<li>Navigate to &quot;Event Subscriptions&quot; in the left sidebar</li>
<li>Enable events and add the Webhook URL above as the Request URL</li>
@@ -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 (
<DialogFooter className="flex justify-between sticky bottom-0 py-3 bg-background border-t z-10 mt-auto w-full">
@@ -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 (
<SlackConfig
signingSecret={slackSigningSecret}
setSigningSecret={setSlackSigningSecret}
isLoadingToken={isLoadingToken}
testResult={testResult}
copied={copied}
copyToClipboard={copyToClipboard}
testWebhook={testWebhook}
/>
)
case 'slack':
return (
<SlackConfig
signingSecret={slackSigningSecret}
setSigningSecret={setSlackSigningSecret}
isLoadingToken={isLoadingToken}
testResult={testResult}
copied={copied}
copyToClipboard={copyToClipboard}
testWebhook={testWebhook}
/>
)
case 'generic':
default:
return (
@@ -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) => <SlackIcon {...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.',
+42 -42
View File
@@ -67,7 +67,7 @@ function findAllPathNodes(edges: any[], targetNodeId: string): string[] {
const visited = new Set<string>()
const queue: [string, number][] = [[targetNodeId, 0]] // [nodeId, distance]
const pathNodes = new Set<string>()
// Build a reverse adjacency list for faster traversal
const reverseAdjList: Record<string, string[]> = {}
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
+10 -5
View File
@@ -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
+41 -41
View File
@@ -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: <Database className="h-4 w-4 mr-2" />,
color: '#0ea5e9' // sky-500
color: '#0ea5e9', // sky-500
},
{
value: 'marketing',
label: 'Marketing',
{
value: 'marketing',
label: 'Marketing',
icon: <MailIcon className="h-4 w-4 mr-2" />,
color: '#f43f5e' // rose-500
color: '#f43f5e', // rose-500
},
{
value: 'sales',
label: 'Sales',
{
value: 'sales',
label: 'Sales',
icon: <Store className="h-4 w-4 mr-2" />,
color: '#10b981' // emerald-500
color: '#10b981', // emerald-500
},
{
value: 'customer_service',
label: 'Customer Service',
icon: <BotMessageSquare className="h-4 w-4 mr-2" />,
color: '#8b5cf6' // violet-500
color: '#8b5cf6', // violet-500
},
{
value: 'research',
label: 'Research',
{
value: 'research',
label: 'Research',
icon: <Atom className="h-4 w-4 mr-2" />,
color: '#f59e0b' // amber-500
color: '#f59e0b', // amber-500
},
{
value: 'finance',
label: 'Finance',
{
value: 'finance',
label: 'Finance',
icon: <LineChart className="h-4 w-4 mr-2" />,
color: '#14b8a6' // teal-500
color: '#14b8a6', // teal-500
},
{
value: 'programming',
label: 'Programming',
{
value: 'programming',
label: 'Programming',
icon: <Code className="h-4 w-4 mr-2" />,
color: '#6366f1' // indigo-500
color: '#6366f1', // indigo-500
},
{
value: 'other',
label: 'Other',
{
value: 'other',
label: 'Other',
icon: <Brain className="h-4 w-4 mr-2" />,
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
}
}
+5 -6
View File
@@ -1716,7 +1716,6 @@ export function EyeIcon(props: SVGProps<SVGSVGElement>) {
)
}
export function ConfluenceIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
@@ -1729,10 +1728,10 @@ export function ConfluenceIcon(props: SVGProps<SVGSVGElement>) {
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill="#1868DB"
d="M20.602 20.234c-6.584-3.183-8.507-3.66-11.281-3.66-3.255 0-6.03 1.355-8.507 5.16l-.407.622c-.333.513-.407.696-.407.915s.111.403.518.659l4.18 2.598c.221.146.406.22.591.22.222 0 .37-.11.592-.44l.666-1.024c1.035-1.574 1.96-2.086 3.144-2.086 1.035 0 2.256.293 3.772 1.025l4.365 2.049c.444.22.925.11 1.146-.403l2.072-4.537c.222-.512.074-.842-.444-1.098M1.406 12.22c6.583 3.184 8.507 3.66 11.28 3.66 3.256 0 6.03-1.354 8.508-5.16l.407-.622c.332-.512.406-.695.406-.915s-.11-.402-.518-.658L17.31 5.927c-.222-.147-.407-.22-.592-.22-.222 0-.37.11-.592.44l-.665 1.024c-1.036 1.573-1.96 2.086-3.144 2.086-1.036 0-2.257-.293-3.773-1.025L4.18 6.183c-.444-.22-.925-.11-1.147.402L.962 11.123c-.222.512-.074.841.444 1.098"
<path
fill="#1868DB"
d="M20.602 20.234c-6.584-3.183-8.507-3.66-11.281-3.66-3.255 0-6.03 1.355-8.507 5.16l-.407.622c-.333.513-.407.696-.407.915s.111.403.518.659l4.18 2.598c.221.146.406.22.591.22.222 0 .37-.11.592-.44l.666-1.024c1.035-1.574 1.96-2.086 3.144-2.086 1.035 0 2.256.293 3.772 1.025l4.365 2.049c.444.22.925.11 1.146-.403l2.072-4.537c.222-.512.074-.842-.444-1.098M1.406 12.22c6.583 3.184 8.507 3.66 11.28 3.66 3.256 0 6.03-1.354 8.508-5.16l.407-.622c.332-.512.406-.695.406-.915s-.11-.402-.518-.658L17.31 5.927c-.222-.147-.407-.22-.592-.22-.222 0-.37.11-.592.44l-.665 1.024c-1.036 1.573-1.96 2.086-3.144 2.086-1.036 0-2.257-.293-3.773-1.025L4.18 6.183c-.444-.22-.925-.11-1.147.402L.962 11.123c-.222.512-.074.841.444 1.098"
/>
</svg>
);
}
)
}
+6
View File
@@ -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<typeof CommandPrimitive>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive> & {
+35 -45
View File
@@ -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<TFieldValues> = FieldPath<TFieldValues>
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
> = {
name: TName
}
const FormFieldContext = React.createContext<FormFieldContextValue>(
{} as FormFieldContextValue
)
const FormFieldContext = React.createContext<FormFieldContextValue>({} as FormFieldContextValue)
const FormField = <
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
>({
...props
}: ControllerProps<TFieldValues, TName>) => {
@@ -49,7 +46,7 @@ const useFormField = () => {
const fieldState = getFieldState(fieldContext.name, formState)
if (!fieldContext) {
throw new Error("useFormField should be used within <FormField>")
throw new Error('useFormField should be used within <FormField>')
}
const { id } = itemContext
@@ -68,23 +65,20 @@ type FormItemContextValue = {
id: string
}
const FormItemContext = React.createContext<FormItemContextValue>(
{} as FormItemContextValue
const FormItemContext = React.createContext<FormItemContextValue>({} as FormItemContextValue)
const FormItem = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => {
const id = React.useId()
return (
<FormItemContext.Provider value={{ id }}>
<div ref={ref} className={cn('space-y-2', className)} {...props} />
</FormItemContext.Provider>
)
}
)
const FormItem = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
const id = React.useId()
return (
<FormItemContext.Provider value={{ id }}>
<div ref={ref} className={cn("space-y-2", className)} {...props} />
</FormItemContext.Provider>
)
})
FormItem.displayName = "FormItem"
FormItem.displayName = 'FormItem'
const FormLabel = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
@@ -95,13 +89,13 @@ const FormLabel = React.forwardRef<
return (
<Label
ref={ref}
className={cn(error && "text-destructive", className)}
className={cn(error && 'text-destructive', className)}
htmlFor={formItemId}
{...props}
/>
)
})
FormLabel.displayName = "FormLabel"
FormLabel.displayName = 'FormLabel'
const FormControl = React.forwardRef<
React.ElementRef<typeof Slot>,
@@ -113,17 +107,13 @@ const FormControl = React.forwardRef<
<Slot
ref={ref}
id={formItemId}
aria-describedby={
!error
? `${formDescriptionId}`
: `${formDescriptionId} ${formMessageId}`
}
aria-describedby={!error ? `${formDescriptionId}` : `${formDescriptionId} ${formMessageId}`}
aria-invalid={!!error}
{...props}
/>
)
})
FormControl.displayName = "FormControl"
FormControl.displayName = 'FormControl'
const FormDescription = React.forwardRef<
HTMLParagraphElement,
@@ -135,19 +125,19 @@ const FormDescription = React.forwardRef<
<p
ref={ref}
id={formDescriptionId}
className={cn("text-sm text-muted-foreground", className)}
className={cn('text-sm text-muted-foreground', className)}
{...props}
/>
)
})
FormDescription.displayName = "FormDescription"
FormDescription.displayName = 'FormDescription'
const FormMessage = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, children, ...props }, ref) => {
const { error, formMessageId } = useFormField()
const body = error ? String(error?.message ?? "") : children
const body = error ? String(error?.message ?? '') : children
if (!body) {
return null
@@ -157,14 +147,14 @@ const FormMessage = React.forwardRef<
<p
ref={ref}
id={formMessageId}
className={cn("text-sm font-medium text-destructive", className)}
className={cn('text-sm font-medium text-destructive', className)}
{...props}
>
{body}
</p>
)
})
FormMessage.displayName = "FormMessage"
FormMessage.displayName = 'FormMessage'
export {
useFormField,
+34 -100
View File
@@ -93,12 +93,8 @@
"name": "account_user_id_user_id_fk",
"tableFrom": "account",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -170,12 +166,8 @@
"name": "api_key_user_id_user_id_fk",
"tableFrom": "api_key",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -185,9 +177,7 @@
"api_key_key_unique": {
"name": "api_key_key_unique",
"nullsNotDistinct": false,
"columns": [
"key"
]
"columns": ["key"]
}
},
"policies": {},
@@ -230,12 +220,8 @@
"name": "environment_user_id_user_id_fk",
"tableFrom": "environment",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -245,9 +231,7 @@
"environment_user_id_unique": {
"name": "environment_user_id_unique",
"nullsNotDistinct": false,
"columns": [
"user_id"
]
"columns": ["user_id"]
}
},
"policies": {},
@@ -341,12 +325,8 @@
"name": "marketplace_workflow_id_workflow_id_fk",
"tableFrom": "marketplace",
"tableTo": "workflow",
"columnsFrom": [
"workflow_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["workflow_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
@@ -354,12 +334,8 @@
"name": "marketplace_author_id_user_id_fk",
"tableFrom": "marketplace",
"tableTo": "user",
"columnsFrom": [
"author_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["author_id"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
}
@@ -428,12 +404,8 @@
"name": "marketplace_star_marketplace_id_marketplace_id_fk",
"tableFrom": "marketplace_star",
"tableTo": "marketplace",
"columnsFrom": [
"marketplace_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["marketplace_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
@@ -441,12 +413,8 @@
"name": "marketplace_star_user_id_user_id_fk",
"tableFrom": "marketplace_star",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
}
@@ -516,12 +484,8 @@
"name": "session_user_id_user_id_fk",
"tableFrom": "session",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -531,9 +495,7 @@
"session_token_unique": {
"name": "session_token_unique",
"nullsNotDistinct": false,
"columns": [
"token"
]
"columns": ["token"]
}
},
"policies": {},
@@ -576,12 +538,8 @@
"name": "settings_user_id_user_id_fk",
"tableFrom": "settings",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -591,9 +549,7 @@
"settings_user_id_unique": {
"name": "settings_user_id_unique",
"nullsNotDistinct": false,
"columns": [
"user_id"
]
"columns": ["user_id"]
}
},
"policies": {},
@@ -654,9 +610,7 @@
"user_email_unique": {
"name": "user_email_unique",
"nullsNotDistinct": false,
"columns": [
"email"
]
"columns": ["email"]
}
},
"policies": {},
@@ -757,9 +711,7 @@
"waitlist_email_unique": {
"name": "waitlist_email_unique",
"nullsNotDistinct": false,
"columns": [
"email"
]
"columns": ["email"]
}
},
"policies": {},
@@ -844,12 +796,8 @@
"name": "webhook_workflow_id_workflow_id_fk",
"tableFrom": "webhook",
"tableTo": "workflow",
"columnsFrom": [
"workflow_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["workflow_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -952,12 +900,8 @@
"name": "workflow_user_id_user_id_fk",
"tableFrom": "workflow",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -1034,12 +978,8 @@
"name": "workflow_logs_workflow_id_workflow_id_fk",
"tableFrom": "workflow_logs",
"tableTo": "workflow",
"columnsFrom": [
"workflow_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["workflow_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -1111,12 +1051,8 @@
"name": "workflow_schedule_workflow_id_workflow_id_fk",
"tableFrom": "workflow_schedule",
"tableTo": "workflow",
"columnsFrom": [
"workflow_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["workflow_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -1126,9 +1062,7 @@
"workflow_schedule_workflow_id_unique": {
"name": "workflow_schedule_workflow_id_unique",
"nullsNotDistinct": false,
"columns": [
"workflow_id"
]
"columns": ["workflow_id"]
}
},
"policies": {},
@@ -1147,4 +1081,4 @@
"schemas": {},
"tables": {}
}
}
}
+34 -100
View File
@@ -93,12 +93,8 @@
"name": "account_user_id_user_id_fk",
"tableFrom": "account",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -170,12 +166,8 @@
"name": "api_key_user_id_user_id_fk",
"tableFrom": "api_key",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -185,9 +177,7 @@
"api_key_key_unique": {
"name": "api_key_key_unique",
"nullsNotDistinct": false,
"columns": [
"key"
]
"columns": ["key"]
}
},
"policies": {},
@@ -230,12 +220,8 @@
"name": "environment_user_id_user_id_fk",
"tableFrom": "environment",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -245,9 +231,7 @@
"environment_user_id_unique": {
"name": "environment_user_id_unique",
"nullsNotDistinct": false,
"columns": [
"user_id"
]
"columns": ["user_id"]
}
},
"policies": {},
@@ -341,12 +325,8 @@
"name": "marketplace_workflow_id_workflow_id_fk",
"tableFrom": "marketplace",
"tableTo": "workflow",
"columnsFrom": [
"workflow_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["workflow_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
@@ -354,12 +334,8 @@
"name": "marketplace_author_id_user_id_fk",
"tableFrom": "marketplace",
"tableTo": "user",
"columnsFrom": [
"author_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["author_id"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
}
@@ -428,12 +404,8 @@
"name": "marketplace_star_marketplace_id_marketplace_id_fk",
"tableFrom": "marketplace_star",
"tableTo": "marketplace",
"columnsFrom": [
"marketplace_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["marketplace_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
@@ -441,12 +413,8 @@
"name": "marketplace_star_user_id_user_id_fk",
"tableFrom": "marketplace_star",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
}
@@ -516,12 +484,8 @@
"name": "session_user_id_user_id_fk",
"tableFrom": "session",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -531,9 +495,7 @@
"session_token_unique": {
"name": "session_token_unique",
"nullsNotDistinct": false,
"columns": [
"token"
]
"columns": ["token"]
}
},
"policies": {},
@@ -576,12 +538,8 @@
"name": "settings_user_id_user_id_fk",
"tableFrom": "settings",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -591,9 +549,7 @@
"settings_user_id_unique": {
"name": "settings_user_id_unique",
"nullsNotDistinct": false,
"columns": [
"user_id"
]
"columns": ["user_id"]
}
},
"policies": {},
@@ -654,9 +610,7 @@
"user_email_unique": {
"name": "user_email_unique",
"nullsNotDistinct": false,
"columns": [
"email"
]
"columns": ["email"]
}
},
"policies": {},
@@ -757,9 +711,7 @@
"waitlist_email_unique": {
"name": "waitlist_email_unique",
"nullsNotDistinct": false,
"columns": [
"email"
]
"columns": ["email"]
}
},
"policies": {},
@@ -844,12 +796,8 @@
"name": "webhook_workflow_id_workflow_id_fk",
"tableFrom": "webhook",
"tableTo": "workflow",
"columnsFrom": [
"workflow_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["workflow_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -959,12 +907,8 @@
"name": "workflow_user_id_user_id_fk",
"tableFrom": "workflow",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -1041,12 +985,8 @@
"name": "workflow_logs_workflow_id_workflow_id_fk",
"tableFrom": "workflow_logs",
"tableTo": "workflow",
"columnsFrom": [
"workflow_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["workflow_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -1118,12 +1058,8 @@
"name": "workflow_schedule_workflow_id_workflow_id_fk",
"tableFrom": "workflow_schedule",
"tableTo": "workflow",
"columnsFrom": [
"workflow_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["workflow_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -1133,9 +1069,7 @@
"workflow_schedule_workflow_id_unique": {
"name": "workflow_schedule_workflow_id_unique",
"nullsNotDistinct": false,
"columns": [
"workflow_id"
]
"columns": ["workflow_id"]
}
},
"policies": {},
@@ -1154,4 +1088,4 @@
"schemas": {},
"tables": {}
}
}
}
+37 -109
View File
@@ -93,12 +93,8 @@
"name": "account_user_id_user_id_fk",
"tableFrom": "account",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -170,12 +166,8 @@
"name": "api_key_user_id_user_id_fk",
"tableFrom": "api_key",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -185,9 +177,7 @@
"api_key_key_unique": {
"name": "api_key_key_unique",
"nullsNotDistinct": false,
"columns": [
"key"
]
"columns": ["key"]
}
},
"policies": {},
@@ -230,12 +220,8 @@
"name": "environment_user_id_user_id_fk",
"tableFrom": "environment",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -245,9 +231,7 @@
"environment_user_id_unique": {
"name": "environment_user_id_unique",
"nullsNotDistinct": false,
"columns": [
"user_id"
]
"columns": ["user_id"]
}
},
"policies": {},
@@ -341,12 +325,8 @@
"name": "marketplace_workflow_id_workflow_id_fk",
"tableFrom": "marketplace",
"tableTo": "workflow",
"columnsFrom": [
"workflow_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["workflow_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
@@ -354,12 +334,8 @@
"name": "marketplace_author_id_user_id_fk",
"tableFrom": "marketplace",
"tableTo": "user",
"columnsFrom": [
"author_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["author_id"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
}
@@ -428,12 +404,8 @@
"name": "marketplace_star_marketplace_id_marketplace_id_fk",
"tableFrom": "marketplace_star",
"tableTo": "marketplace",
"columnsFrom": [
"marketplace_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["marketplace_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
@@ -441,12 +413,8 @@
"name": "marketplace_star_user_id_user_id_fk",
"tableFrom": "marketplace_star",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
}
@@ -516,12 +484,8 @@
"name": "session_user_id_user_id_fk",
"tableFrom": "session",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -531,9 +495,7 @@
"session_token_unique": {
"name": "session_token_unique",
"nullsNotDistinct": false,
"columns": [
"token"
]
"columns": ["token"]
}
},
"policies": {},
@@ -576,12 +538,8 @@
"name": "settings_user_id_user_id_fk",
"tableFrom": "settings",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -591,9 +549,7 @@
"settings_user_id_unique": {
"name": "settings_user_id_unique",
"nullsNotDistinct": false,
"columns": [
"user_id"
]
"columns": ["user_id"]
}
},
"policies": {},
@@ -654,9 +610,7 @@
"user_email_unique": {
"name": "user_email_unique",
"nullsNotDistinct": false,
"columns": [
"email"
]
"columns": ["email"]
}
},
"policies": {},
@@ -735,12 +689,8 @@
"name": "user_stats_user_id_user_id_fk",
"tableFrom": "user_stats",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -750,9 +700,7 @@
"user_stats_user_id_unique": {
"name": "user_stats_user_id_unique",
"nullsNotDistinct": false,
"columns": [
"user_id"
]
"columns": ["user_id"]
}
},
"policies": {},
@@ -853,9 +801,7 @@
"waitlist_email_unique": {
"name": "waitlist_email_unique",
"nullsNotDistinct": false,
"columns": [
"email"
]
"columns": ["email"]
}
},
"policies": {},
@@ -940,12 +886,8 @@
"name": "webhook_workflow_id_workflow_id_fk",
"tableFrom": "webhook",
"tableTo": "workflow",
"columnsFrom": [
"workflow_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["workflow_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -1075,12 +1017,8 @@
"name": "workflow_user_id_user_id_fk",
"tableFrom": "workflow",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -1157,12 +1095,8 @@
"name": "workflow_logs_workflow_id_workflow_id_fk",
"tableFrom": "workflow_logs",
"tableTo": "workflow",
"columnsFrom": [
"workflow_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["workflow_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -1234,12 +1168,8 @@
"name": "workflow_schedule_workflow_id_workflow_id_fk",
"tableFrom": "workflow_schedule",
"tableTo": "workflow",
"columnsFrom": [
"workflow_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["workflow_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -1249,9 +1179,7 @@
"workflow_schedule_workflow_id_unique": {
"name": "workflow_schedule_workflow_id_unique",
"nullsNotDistinct": false,
"columns": [
"workflow_id"
]
"columns": ["workflow_id"]
}
},
"policies": {},
@@ -1270,4 +1198,4 @@
"schemas": {},
"tables": {}
}
}
}
+14 -10
View File
@@ -1,6 +1,6 @@
import { createLogger } from '@/lib/logs/console-logger'
import { useConsoleStore } from '@/stores/panel/console/store'
import { useExecutionStore } from '@/stores/execution/store'
import { useConsoleStore } from '@/stores/panel/console/store'
import { useGeneralStore } from '@/stores/settings/general/store'
import { BlockOutput } from '@/blocks/types'
import { SerializedBlock, SerializedWorkflow } from '@/serializer/types'
@@ -394,8 +394,9 @@ export class Executor {
const sourceExecuted = executedBlocks.has(conn.source)
const sourceBlock = this.workflow.blocks.find((b) => b.id === conn.source)
const sourceBlockState = context.blockStates.get(conn.source)
const hasSourceError = sourceBlockState?.output?.error !== undefined ||
sourceBlockState?.output?.response?.error !== undefined
const hasSourceError =
sourceBlockState?.output?.error !== undefined ||
sourceBlockState?.output?.response?.error !== undefined
// For condition blocks, check if this is the selected path
if (conn.sourceHandle?.startsWith('condition-')) {
@@ -605,7 +606,10 @@ export class Executor {
const hasErrorPath = this.activateErrorPath(blockId, context)
// Console.error the error for visibility
logger.error(`Error executing block ${block.metadata?.name || blockId}:`, this.sanitizeError(error))
logger.error(
`Error executing block ${block.metadata?.name || blockId}:`,
this.sanitizeError(error)
)
// If there are error paths to follow, return error output instead of throwing
if (hasErrorPath) {
@@ -635,7 +639,7 @@ export class Executor {
/**
* Activates error paths from a block that had an error.
* Checks for connections from the block's "error" handle and adds them to the active execution path.
*
*
* @param blockId - ID of the block that had an error
* @param context - Current execution context
* @returns Whether there was an error path to follow
@@ -682,7 +686,7 @@ export class Executor {
status: output.status || 500,
},
error: output.error,
};
}
}
if (output && typeof output === 'object' && 'response' in output) {
@@ -690,10 +694,10 @@ export class Executor {
if (output.response && output.response.error) {
return {
...output,
error: output.response.error
};
error: output.response.error,
}
}
return output as NormalizedBlockOutput;
return output as NormalizedBlockOutput
}
const blockType = block.metadata?.id
@@ -923,4 +927,4 @@ export class Executor {
return { message: `Unexpected error type: ${typeof error}` }
}
}
}
+5 -3
View File
@@ -98,8 +98,10 @@ export class PathTracker {
} else {
// For regular blocks, activate all outgoing connections based on success or error status
const blockState = context.blockStates.get(blockId)
const hasError = blockState?.output?.error !== undefined || blockState?.output?.response?.error !== undefined
const hasError =
blockState?.output?.error !== undefined ||
blockState?.output?.response?.error !== undefined
// Get all outgoing connections
const outgoingConnections = this.workflow.connections.filter(
(conn) => conn.source === blockId
@@ -111,7 +113,7 @@ export class PathTracker {
if (hasError) {
context.activeExecutionPath.add(conn.target)
}
}
}
// For regular (source) connections, only activate them if there's no error
else if (conn.sourceHandle === 'source' || !conn.sourceHandle) {
if (!hasError) {
+11 -13
View File
@@ -62,7 +62,7 @@ export class InputResolver {
if (typeof value === 'string') {
// First check for variable references
let resolvedValue = this.resolveVariableReferences(value)
// Then resolve block references
resolvedValue = this.resolveBlockReferences(resolvedValue, context, block)
@@ -102,7 +102,7 @@ export class InputResolver {
/**
* Resolves workflow variable references in a string (<variable.name>).
*
*
* @param value - String containing variable references
* @returns String with resolved variable references
*/
@@ -114,23 +114,21 @@ export class InputResolver {
for (const match of variableMatches) {
const variableName = match.slice('<variable.'.length, -1)
// Find the variable by normalized name (without spaces)
const foundVariable = Object.entries(this.workflowVariables).find(
([_, variable]) => {
const normalizedName = (variable.name || '').replace(/\s+/g, '')
return normalizedName === variableName
}
)
const foundVariable = Object.entries(this.workflowVariables).find(([_, variable]) => {
const normalizedName = (variable.name || '').replace(/\s+/g, '')
return normalizedName === variableName
})
if (foundVariable) {
const [_, variable] = foundVariable
// Format the value appropriately
const formattedValue =
const formattedValue =
typeof variable.value === 'object'
? JSON.stringify(variable.value)
: String(variable.value)
resolvedValue = resolvedValue.replace(match, formattedValue)
}
}
@@ -163,7 +161,7 @@ export class InputResolver {
if (match.startsWith('<variable.')) {
continue
}
const path = match.slice(1, -1)
const [blockRef, ...pathParts] = path.split('.')
@@ -408,7 +406,7 @@ export class InputResolver {
if (typeof value === 'string') {
// First resolve variable references
const resolvedVars = this.resolveVariableReferences(value)
// Then resolve block references
const resolvedReferences = this.resolveBlockReferences(resolvedVars, context, currentBlock)
+2 -2
View File
@@ -5,11 +5,11 @@ export const STORAGE_KEYS = {
}
export const API_ENDPOINTS = {
WORKFLOW: '/api/workflows/sync',
SYNC: '/api/workflows/sync',
ENVIRONMENT: '/api/environment',
SCHEDULE: '/api/schedules/schedule',
SETTINGS: '/api/settings',
WORKFLOW_VARIABLES: '/api/workflows/[id]/variables',
WORKFLOWS: '/api/workflows',
}
export const SYNC_INTERVALS = {
+2 -2
View File
@@ -1,12 +1,12 @@
import { useEffect } from 'react'
import { createLogger } from '@/lib/logs/console-logger'
import { useChatStore } from './chat/store'
import { useConsoleStore } from './panel/console/store'
import { useCustomToolsStore } from './custom-tools/store'
import { useExecutionStore } from './execution/store'
import { useNotificationStore } from './notifications/store'
import { useEnvironmentStore } from './settings/environment/store'
import { useConsoleStore } from './panel/console/store'
import { useVariablesStore } from './panel/variables/store'
import { useEnvironmentStore } from './settings/environment/store'
import { getSyncManagers, initializeSyncManagers, resetSyncManagers } from './sync-registry'
import {
loadRegistry,
+4 -4
View File
@@ -62,13 +62,13 @@ export const useNotificationStore = create<NotificationStore>()(
showNotification: (id) =>
set((state) => {
// Find the notification first to ensure it exists
const notification = state.notifications.find(n => n.id === id)
const notification = state.notifications.find((n) => n.id === id)
if (!notification) return { notifications: state.notifications }
// Bring the notification to the top and make it visible
const filteredNotifications = state.notifications.filter(n => n.id !== id)
const filteredNotifications = state.notifications.filter((n) => n.id !== id)
const updatedNotification = { ...notification, isVisible: true, read: false }
// Put the notification at the top so it's easily visible in dropdowns
const newNotifications = [updatedNotification, ...filteredNotifications]
persistNotifications(newNotifications)
+1 -1
View File
@@ -22,4 +22,4 @@ export const usePanelStore = create<PanelStore>()(
}
)
)
)
)
+1 -1
View File
@@ -5,4 +5,4 @@ export interface PanelStore {
activeTab: PanelTab
togglePanel: () => void
setActiveTab: (tab: PanelTab) => void
}
}
+144 -117
View File
@@ -1,10 +1,10 @@
import { create } from 'zustand'
import { createLogger } from '@/lib/logs/console-logger'
import { devtools, persist } from 'zustand/middleware'
import { Variable, VariablesStore } from './types'
import { createLogger } from '@/lib/logs/console-logger'
import { API_ENDPOINTS } from '@/stores/constants'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
import { Variable, VariablesStore } from './types'
const logger = createLogger('Variables Store')
const SAVE_DEBOUNCE_DELAY = 500 // 500ms debounce delay
@@ -25,61 +25,64 @@ export const useVariablesStore = create<VariablesStore>()(
addVariable: (variable) => {
const id = crypto.randomUUID()
// Get variables for this workflow
const workflowVariables = get().getVariablesByWorkflowId(variable.workflowId)
// Auto-generate variable name if not provided or it's a default pattern name
if (!variable.name || /^variable\d+$/.test(variable.name)) {
// Find the highest existing Variable N number
const existingNumbers = workflowVariables
.map(v => {
.map((v) => {
const match = v.name.match(/^variable(\d+)$/)
return match ? parseInt(match[1]) : 0
})
.filter(n => !isNaN(n))
.filter((n) => !isNaN(n))
// Set new number to max + 1, or 1 if none exist
const nextNumber = existingNumbers.length > 0
? Math.max(...existingNumbers) + 1
: 1
const nextNumber = existingNumbers.length > 0 ? Math.max(...existingNumbers) + 1 : 1
variable.name = `variable${nextNumber}`
}
// Ensure name uniqueness within the workflow
let uniqueName = variable.name
let nameIndex = 1
// Check if name already exists in this workflow
while (workflowVariables.some(v => v.name === uniqueName)) {
while (workflowVariables.some((v) => v.name === uniqueName)) {
uniqueName = `${variable.name} (${nameIndex})`
nameIndex++
}
// Handle initial value
let variableValue = variable.value
// Auto-add quotes for string values if they aren't already quoted
if (variable.type === 'string' &&
typeof variableValue === 'string' &&
variableValue.trim() !== '') {
if (
variable.type === 'string' &&
typeof variableValue === 'string' &&
variableValue.trim() !== ''
) {
// Only add quotes if not already properly quoted
const trimmedValue = variableValue.trim()
// Check if entire string is already properly quoted
const isAlreadyQuoted = (
(trimmedValue.startsWith('"') && trimmedValue.endsWith('"') && trimmedValue.length >= 2) ||
(trimmedValue.startsWith("'") && trimmedValue.endsWith("'") && trimmedValue.length >= 2)
)
const isAlreadyQuoted =
(trimmedValue.startsWith('"') &&
trimmedValue.endsWith('"') &&
trimmedValue.length >= 2) ||
(trimmedValue.startsWith("'") &&
trimmedValue.endsWith("'") &&
trimmedValue.length >= 2)
if (!isAlreadyQuoted) {
// Escape any existing quotes in the content
const escapedValue = variableValue.replace(/"/g, '\\"')
variableValue = `"${escapedValue}"`
}
}
set((state) => ({
variables: {
...state.variables,
@@ -92,88 +95,94 @@ export const useVariablesStore = create<VariablesStore>()(
},
},
}))
// Auto-save to DB
get().saveVariables(variable.workflowId)
return id
},
updateVariable: (id, update) => {
set((state) => {
if (!state.variables[id]) return state
// If name is being updated, ensure it's unique
if (update.name) {
const oldVariable = state.variables[id]
const oldVariableName = oldVariable.name
const workflowId = oldVariable.workflowId
const workflowVariables = Object.values(state.variables).filter(
v => v.workflowId === workflowId && v.id !== id
(v) => v.workflowId === workflowId && v.id !== id
)
let uniqueName = update.name
let nameIndex = 1
// Check if name already exists in this workflow
while (workflowVariables.some(v => v.name === uniqueName)) {
while (workflowVariables.some((v) => v.name === uniqueName)) {
uniqueName = `${update.name} (${nameIndex})`
nameIndex++
}
// If name has changed, update references in subblocks
if (uniqueName !== oldVariableName) {
// Update references in subblock store
const subBlockStore = useSubBlockStore.getState()
const activeWorkflowId = useWorkflowRegistry.getState().activeWorkflowId
if (activeWorkflowId) {
// Get the workflow values for the active workflow
const workflowValues = subBlockStore.workflowValues[activeWorkflowId] || {}
const updatedWorkflowValues = { ...workflowValues }
// Loop through blocks
Object.entries(workflowValues).forEach(([blockId, blockValues]) => {
// Loop through subblocks and update references
Object.entries(blockValues as Record<string, any>).forEach(([subBlockId, value]) => {
const oldVarName = oldVariableName.replace(/\s+/g, '').toLowerCase()
const newVarName = uniqueName.replace(/\s+/g, '').toLowerCase()
const regex = new RegExp(`<variable\.${oldVarName}>`, 'gi')
// Use a recursive function to handle all object types
updatedWorkflowValues[blockId][subBlockId] = updateReferences(
value,
regex,
`<variable.${newVarName}>`
)
// Helper function to recursively update references in any data structure
function updateReferences(value: any, regex: RegExp, replacement: string): any {
// Handle string values
if (typeof value === 'string') {
return regex.test(value) ? value.replace(regex, replacement) : value
}
// Handle arrays
if (Array.isArray(value)) {
return value.map((item) => updateReferences(item, regex, replacement))
}
// Handle objects
if (value !== null && typeof value === 'object') {
const result = { ...value }
for (const key in result) {
result[key] = updateReferences(result[key], regex, replacement)
Object.entries(blockValues as Record<string, any>).forEach(
([subBlockId, value]) => {
const oldVarName = oldVariableName.replace(/\s+/g, '').toLowerCase()
const newVarName = uniqueName.replace(/\s+/g, '').toLowerCase()
const regex = new RegExp(`<variable\.${oldVarName}>`, 'gi')
// Use a recursive function to handle all object types
updatedWorkflowValues[blockId][subBlockId] = updateReferences(
value,
regex,
`<variable.${newVarName}>`
)
// Helper function to recursively update references in any data structure
function updateReferences(
value: any,
regex: RegExp,
replacement: string
): any {
// Handle string values
if (typeof value === 'string') {
return regex.test(value) ? value.replace(regex, replacement) : value
}
return result
// Handle arrays
if (Array.isArray(value)) {
return value.map((item) => updateReferences(item, regex, replacement))
}
// Handle objects
if (value !== null && typeof value === 'object') {
const result = { ...value }
for (const key in result) {
result[key] = updateReferences(result[key], regex, replacement)
}
return result
}
// Return unchanged for other types
return value
}
// Return unchanged for other types
return value
}
})
)
})
// Update the subblock store with the new values
useSubBlockStore.setState({
workflowValues: {
@@ -183,25 +192,30 @@ export const useVariablesStore = create<VariablesStore>()(
})
}
}
// Update with unique name
update = { ...update, name: uniqueName }
}
// Auto-add quotes for string values if they aren't already quoted
if (update.value !== undefined &&
state.variables[id].type === 'string' &&
typeof update.value === 'string' &&
update.value.trim() !== '') {
if (
update.value !== undefined &&
state.variables[id].type === 'string' &&
typeof update.value === 'string' &&
update.value.trim() !== ''
) {
// Only add quotes if not already properly quoted
const trimmedValue = update.value.trim()
// Check if entire string is already properly quoted
const isAlreadyQuoted = (
(trimmedValue.startsWith('"') && trimmedValue.endsWith('"') && trimmedValue.length >= 2) ||
(trimmedValue.startsWith("'") && trimmedValue.endsWith("'") && trimmedValue.length >= 2)
)
const isAlreadyQuoted =
(trimmedValue.startsWith('"') &&
trimmedValue.endsWith('"') &&
trimmedValue.length >= 2) ||
(trimmedValue.startsWith("'") &&
trimmedValue.endsWith("'") &&
trimmedValue.length >= 2)
if (!isAlreadyQuoted) {
// Escape any existing quotes in the content
const escapedValue = update.value.replace(/"/g, '\\"')
@@ -219,18 +233,18 @@ export const useVariablesStore = create<VariablesStore>()(
// Debounced auto-save to DB
const workflowId = state.variables[id].workflowId
// Clear existing timer for this workflow if it exists
if (saveTimers.has(workflowId)) {
clearTimeout(saveTimers.get(workflowId))
}
// Set new debounced save timer
const timer = setTimeout(() => {
get().saveVariables(workflowId)
saveTimers.delete(workflowId)
}, SAVE_DEBOUNCE_DELAY)
saveTimers.set(workflowId, timer)
return { variables: updated }
@@ -257,15 +271,15 @@ export const useVariablesStore = create<VariablesStore>()(
const variable = state.variables[id]
const newId = crypto.randomUUID()
// Ensure the duplicated name is unique
const workflowVariables = get().getVariablesByWorkflowId(variable.workflowId)
let baseName = `${variable.name} (copy)`
let uniqueName = baseName
let nameIndex = 1
// Check if name already exists in this workflow
while (workflowVariables.some(v => v.name === uniqueName)) {
while (workflowVariables.some((v) => v.name === uniqueName)) {
uniqueName = `${baseName} (${nameIndex})`
nameIndex++
}
@@ -292,27 +306,30 @@ export const useVariablesStore = create<VariablesStore>()(
loadVariables: async (workflowId) => {
// Skip if already loaded to prevent redundant API calls
if (loadedWorkflows.has(workflowId)) return
try {
set({ isLoading: true, error: null })
const response = await fetch(`${API_ENDPOINTS.WORKFLOW_VARIABLES}/${workflowId}`)
const response = await fetch(`${API_ENDPOINTS.WORKFLOWS}/${workflowId}/variables`)
// Handle 404 workflow not found gracefully
if (response.status === 404) {
logger.info(`No variables found for workflow ${workflowId}, initializing empty set`)
set((state) => {
// Keep variables from other workflows
const otherVariables = Object.values(state.variables).reduce((acc, variable) => {
if (variable.workflowId !== workflowId) {
acc[variable.id] = variable
}
return acc
}, {} as Record<string, Variable>)
const otherVariables = Object.values(state.variables).reduce(
(acc, variable) => {
if (variable.workflowId !== workflowId) {
acc[variable.id] = variable
}
return acc
},
{} as Record<string, Variable>
)
// Mark this workflow as loaded to prevent further attempts
loadedWorkflows.add(workflowId)
return {
variables: otherVariables,
isLoading: false,
@@ -330,16 +347,19 @@ export const useVariablesStore = create<VariablesStore>()(
if (data && typeof data === 'object') {
set((state) => {
// Merge with existing variables from other workflows
const otherVariables = Object.values(state.variables).reduce((acc, variable) => {
if (variable.workflowId !== workflowId) {
acc[variable.id] = variable
}
return acc
}, {} as Record<string, Variable>)
const otherVariables = Object.values(state.variables).reduce(
(acc, variable) => {
if (variable.workflowId !== workflowId) {
acc[variable.id] = variable
}
return acc
},
{} as Record<string, Variable>
)
// Mark this workflow as loaded
loadedWorkflows.add(workflowId)
return {
variables: { ...otherVariables, ...data },
isLoading: false,
@@ -348,16 +368,19 @@ export const useVariablesStore = create<VariablesStore>()(
} else {
set((state) => {
// Keep variables from other workflows
const otherVariables = Object.values(state.variables).reduce((acc, variable) => {
if (variable.workflowId !== workflowId) {
acc[variable.id] = variable
}
return acc
}, {} as Record<string, Variable>)
const otherVariables = Object.values(state.variables).reduce(
(acc, variable) => {
if (variable.workflowId !== workflowId) {
acc[variable.id] = variable
}
return acc
},
{} as Record<string, Variable>
)
// Mark this workflow as loaded
loadedWorkflows.add(workflowId)
return {
variables: otherVariables,
isLoading: false,
@@ -381,7 +404,7 @@ export const useVariablesStore = create<VariablesStore>()(
logger.info(`Skipping variable save for non-existent workflow: ${workflowId}`)
return
}
set({ isLoading: true, error: null })
// Get only variables for this workflow
@@ -390,7 +413,7 @@ export const useVariablesStore = create<VariablesStore>()(
)
// Send to DB
const response = await fetch(`${API_ENDPOINTS.WORKFLOW_VARIABLES}/${workflowId}`, {
const response = await fetch(`${API_ENDPOINTS.WORKFLOWS}/${workflowId}/variables`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@@ -402,7 +425,9 @@ export const useVariablesStore = create<VariablesStore>()(
// Handle 404 workflow not found gracefully
if (response.status === 404) {
logger.info(`Cannot save variables - workflow ${workflowId} not found in database yet`)
logger.info(
`Cannot save variables - workflow ${workflowId} not found in database yet`
)
// Reset loading state but don't treat as error
set({ isLoading: false })
return
@@ -428,9 +453,11 @@ export const useVariablesStore = create<VariablesStore>()(
},
getVariablesByWorkflowId: (workflowId) => {
return Object.values(get().variables).filter((variable) => variable.workflowId === workflowId)
return Object.values(get().variables).filter(
(variable) => variable.workflowId === workflowId
)
},
// Reset the loaded workflow tracking
resetLoaded: () => {
loadedWorkflows.clear()
+6 -6
View File
@@ -23,28 +23,28 @@ export interface VariablesStore {
* If a variable with the same name exists, it will be suffixed with a number
*/
addVariable: (variable: Omit<Variable, 'id'>) => string
/**
* Updates a variable, ensuring name remains unique within the workflow
* If an updated name conflicts with existing ones, a numbered suffix is added
*/
updateVariable: (id: string, update: Partial<Omit<Variable, 'id' | 'workflowId'>>) => void
deleteVariable: (id: string) => void
/**
* Duplicates a variable with a "(copy)" suffix, ensuring name uniqueness
*/
duplicateVariable: (id: string) => string
loadVariables: (workflowId: string) => Promise<void>
saveVariables: (workflowId: string) => Promise<void>
/**
* Returns all variables for a specific workflow
*/
getVariablesByWorkflowId: (workflowId: string) => Variable[]
/**
* Resets tracking of loaded workflows
*/
+6 -6
View File
@@ -13,17 +13,17 @@ export function getWorkflowWithValues(workflowId: string) {
const { workflows } = useWorkflowRegistry.getState()
const activeWorkflowId = useWorkflowRegistry.getState().activeWorkflowId
const currentState = useWorkflowStore.getState()
if (!workflows[workflowId]) {
logger.warn(`Workflow ${workflowId} not found`)
return null
}
const metadata = workflows[workflowId]
// Load the specific state for this workflow
let workflowState: WorkflowState
if (workflowId === activeWorkflowId) {
// For the active workflow, use the current state from the store
workflowState = {
@@ -43,10 +43,10 @@ export function getWorkflowWithValues(workflowId: string) {
}
workflowState = savedState
}
// Merge the subblock values for this specific workflow
const mergedBlocks = mergeSubblockState(workflowState.blocks, workflowId)
return {
id: workflowId,
name: metadata.name,
+7 -7
View File
@@ -40,12 +40,10 @@ export function getNextWorkflowColor(existingWorkflows: Record<string, WorkflowM
// Sort workflows by lastModified date (newest first)
const sortedWorkflows = [...workflowArray].sort((a, b) => {
const dateA = a.lastModified instanceof Date
? a.lastModified.getTime()
: new Date(a.lastModified).getTime()
const dateB = b.lastModified instanceof Date
? b.lastModified.getTime()
: new Date(b.lastModified).getTime()
const dateA =
a.lastModified instanceof Date ? a.lastModified.getTime() : new Date(a.lastModified).getTime()
const dateB =
b.lastModified instanceof Date ? b.lastModified.getTime() : new Date(b.lastModified).getTime()
return dateB - dateA
})
@@ -53,7 +51,9 @@ export function getNextWorkflowColor(existingWorkflows: Record<string, WorkflowM
const newestWorkflow = sortedWorkflows[0]
// Find the index of the newest workflow's color, defaulting to -1 if undefined
const currentColorIndex = newestWorkflow?.color ? WORKFLOW_COLORS.indexOf(newestWorkflow.color) : -1
const currentColorIndex = newestWorkflow?.color
? WORKFLOW_COLORS.indexOf(newestWorkflow.color)
: -1
// Get next color index, wrapping around to 0 if we reach the end
const nextColorIndex = (currentColorIndex + 1) % WORKFLOW_COLORS.length
+16 -16
View File
@@ -13,8 +13,8 @@ import { BlockState } from './workflow/types'
const logger = createLogger('Workflows Sync')
// Add debounce utility
let syncDebounceTimer: NodeJS.Timeout | null = null;
const DEBOUNCE_DELAY = 500; // 500ms delay
let syncDebounceTimer: NodeJS.Timeout | null = null
const DEBOUNCE_DELAY = 500 // 500ms delay
// Flag to prevent immediate sync back to DB after loading from DB
let isLoadingFromDB = false
@@ -54,7 +54,7 @@ export async function fetchWorkflowsFromDB(): Promise<void> {
loadingFromDBStartTime = Date.now()
// Call the API endpoint to get workflows from DB
const response = await fetch(API_ENDPOINTS.WORKFLOW, {
const response = await fetch(API_ENDPOINTS.SYNC, {
method: 'GET',
})
@@ -200,13 +200,13 @@ function activeDBSyncNeeded(): boolean {
// Only sync if we have detected a change that needs to be persisted
const lastSynced = localStorage.getItem('last_db_sync_timestamp')
const currentTime = Date.now()
if (!lastSynced) {
// First sync - record it and return true
localStorage.setItem('last_db_sync_timestamp', currentTime.toString())
return true
}
// Add additional checks here if needed for specific workflow changes
// For now, we'll simply avoid the automatic sync after load
return false
@@ -214,7 +214,7 @@ function activeDBSyncNeeded(): boolean {
// Create the basic sync configuration
const workflowSyncConfig = {
endpoint: API_ENDPOINTS.WORKFLOW,
endpoint: API_ENDPOINTS.SYNC,
preparePayload: () => {
if (typeof window === 'undefined') return {}
@@ -252,10 +252,10 @@ const workflowSyncConfig = {
onSyncSuccess: async () => {
logger.info('Workflows synced to DB successfully')
},
};
}
// Create the sync manager
const baseWorkflowSync = createSingletonSyncManager('workflow-sync', () => workflowSyncConfig);
const baseWorkflowSync = createSingletonSyncManager('workflow-sync', () => workflowSyncConfig)
// Create a debounced version of the sync manager
export const workflowSync = {
@@ -263,18 +263,18 @@ export const workflowSync = {
sync: () => {
// Clear any existing timeout
if (syncDebounceTimer) {
clearTimeout(syncDebounceTimer);
clearTimeout(syncDebounceTimer)
}
// Set new timeout
syncDebounceTimer = setTimeout(() => {
// Perform the sync
baseWorkflowSync.sync();
baseWorkflowSync.sync()
// Update the last sync timestamp
if (typeof window !== 'undefined') {
localStorage.setItem('last_db_sync_timestamp', Date.now().toString());
localStorage.setItem('last_db_sync_timestamp', Date.now().toString())
}
}, DEBOUNCE_DELAY);
}
};
}, DEBOUNCE_DELAY)
},
}