mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-12 16:38:15 -05:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
09cea81ae3 |
@@ -6,22 +6,7 @@ import {
|
||||
workflowDeploymentVersion,
|
||||
workflowExecutionLogs,
|
||||
} from '@sim/db/schema'
|
||||
import {
|
||||
and,
|
||||
desc,
|
||||
eq,
|
||||
gt,
|
||||
gte,
|
||||
inArray,
|
||||
isNotNull,
|
||||
isNull,
|
||||
lt,
|
||||
lte,
|
||||
ne,
|
||||
or,
|
||||
type SQL,
|
||||
sql,
|
||||
} from 'drizzle-orm'
|
||||
import { and, desc, eq, gte, inArray, isNotNull, isNull, lte, or, type SQL, sql } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { getSession } from '@/lib/auth'
|
||||
@@ -37,19 +22,14 @@ const QueryParamsSchema = z.object({
|
||||
limit: z.coerce.number().optional().default(100),
|
||||
offset: z.coerce.number().optional().default(0),
|
||||
level: z.string().optional(),
|
||||
workflowIds: z.string().optional(),
|
||||
folderIds: z.string().optional(),
|
||||
triggers: z.string().optional(),
|
||||
workflowIds: z.string().optional(), // Comma-separated list of workflow IDs
|
||||
folderIds: z.string().optional(), // Comma-separated list of folder IDs
|
||||
triggers: z.string().optional(), // Comma-separated list of trigger types
|
||||
startDate: z.string().optional(),
|
||||
endDate: z.string().optional(),
|
||||
search: z.string().optional(),
|
||||
workflowName: z.string().optional(),
|
||||
folderName: z.string().optional(),
|
||||
executionId: z.string().optional(),
|
||||
costOperator: z.enum(['=', '>', '<', '>=', '<=', '!=']).optional(),
|
||||
costValue: z.coerce.number().optional(),
|
||||
durationOperator: z.enum(['=', '>', '<', '>=', '<=', '!=']).optional(),
|
||||
durationValue: z.coerce.number().optional(),
|
||||
workspaceId: z.string(),
|
||||
})
|
||||
|
||||
@@ -69,6 +49,7 @@ export async function GET(request: NextRequest) {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const params = QueryParamsSchema.parse(Object.fromEntries(searchParams.entries()))
|
||||
|
||||
// Conditionally select columns based on detail level to optimize performance
|
||||
const selectColumns =
|
||||
params.details === 'full'
|
||||
? {
|
||||
@@ -82,9 +63,9 @@ export async function GET(request: NextRequest) {
|
||||
startedAt: workflowExecutionLogs.startedAt,
|
||||
endedAt: workflowExecutionLogs.endedAt,
|
||||
totalDurationMs: workflowExecutionLogs.totalDurationMs,
|
||||
executionData: workflowExecutionLogs.executionData,
|
||||
executionData: workflowExecutionLogs.executionData, // Large field - only in full mode
|
||||
cost: workflowExecutionLogs.cost,
|
||||
files: workflowExecutionLogs.files,
|
||||
files: workflowExecutionLogs.files, // Large field - only in full mode
|
||||
createdAt: workflowExecutionLogs.createdAt,
|
||||
workflowName: workflow.name,
|
||||
workflowDescription: workflow.description,
|
||||
@@ -101,6 +82,7 @@ export async function GET(request: NextRequest) {
|
||||
deploymentVersionName: workflowDeploymentVersion.name,
|
||||
}
|
||||
: {
|
||||
// Basic mode - exclude large fields for better performance
|
||||
id: workflowExecutionLogs.id,
|
||||
workflowId: workflowExecutionLogs.workflowId,
|
||||
executionId: workflowExecutionLogs.executionId,
|
||||
@@ -111,9 +93,9 @@ export async function GET(request: NextRequest) {
|
||||
startedAt: workflowExecutionLogs.startedAt,
|
||||
endedAt: workflowExecutionLogs.endedAt,
|
||||
totalDurationMs: workflowExecutionLogs.totalDurationMs,
|
||||
executionData: sql<null>`NULL`,
|
||||
executionData: sql<null>`NULL`, // Exclude large execution data in basic mode
|
||||
cost: workflowExecutionLogs.cost,
|
||||
files: sql<null>`NULL`,
|
||||
files: sql<null>`NULL`, // Exclude files in basic mode
|
||||
createdAt: workflowExecutionLogs.createdAt,
|
||||
workflowName: workflow.name,
|
||||
workflowDescription: workflow.description,
|
||||
@@ -127,7 +109,7 @@ export async function GET(request: NextRequest) {
|
||||
pausedTotalPauseCount: pausedExecutions.totalPauseCount,
|
||||
pausedResumedCount: pausedExecutions.resumedCount,
|
||||
deploymentVersion: workflowDeploymentVersion.version,
|
||||
deploymentVersionName: sql<null>`NULL`,
|
||||
deploymentVersionName: sql<null>`NULL`, // Only needed in full mode for details panel
|
||||
}
|
||||
|
||||
const baseQuery = db
|
||||
@@ -157,28 +139,34 @@ export async function GET(request: NextRequest) {
|
||||
)
|
||||
)
|
||||
|
||||
// Build additional conditions for the query
|
||||
let conditions: SQL | undefined
|
||||
|
||||
// Filter by level with support for derived statuses (running, pending)
|
||||
if (params.level && params.level !== 'all') {
|
||||
const levels = params.level.split(',').filter(Boolean)
|
||||
const levelConditions: SQL[] = []
|
||||
|
||||
for (const level of levels) {
|
||||
if (level === 'error') {
|
||||
// Direct database field
|
||||
levelConditions.push(eq(workflowExecutionLogs.level, 'error'))
|
||||
} else if (level === 'info') {
|
||||
// Completed info logs only (not running, not pending)
|
||||
const condition = and(
|
||||
eq(workflowExecutionLogs.level, 'info'),
|
||||
isNotNull(workflowExecutionLogs.endedAt)
|
||||
)
|
||||
if (condition) levelConditions.push(condition)
|
||||
} else if (level === 'running') {
|
||||
// Running logs: info level with no endedAt
|
||||
const condition = and(
|
||||
eq(workflowExecutionLogs.level, 'info'),
|
||||
isNull(workflowExecutionLogs.endedAt)
|
||||
)
|
||||
if (condition) levelConditions.push(condition)
|
||||
} else if (level === 'pending') {
|
||||
// Pending logs: info level with pause status indicators
|
||||
const condition = and(
|
||||
eq(workflowExecutionLogs.level, 'info'),
|
||||
or(
|
||||
@@ -201,6 +189,7 @@ export async function GET(request: NextRequest) {
|
||||
}
|
||||
}
|
||||
|
||||
// Filter by specific workflow IDs
|
||||
if (params.workflowIds) {
|
||||
const workflowIds = params.workflowIds.split(',').filter(Boolean)
|
||||
if (workflowIds.length > 0) {
|
||||
@@ -208,6 +197,7 @@ export async function GET(request: NextRequest) {
|
||||
}
|
||||
}
|
||||
|
||||
// Filter by folder IDs
|
||||
if (params.folderIds) {
|
||||
const folderIds = params.folderIds.split(',').filter(Boolean)
|
||||
if (folderIds.length > 0) {
|
||||
@@ -215,6 +205,7 @@ export async function GET(request: NextRequest) {
|
||||
}
|
||||
}
|
||||
|
||||
// Filter by triggers
|
||||
if (params.triggers) {
|
||||
const triggers = params.triggers.split(',').filter(Boolean)
|
||||
if (triggers.length > 0 && !triggers.includes('all')) {
|
||||
@@ -222,6 +213,7 @@ export async function GET(request: NextRequest) {
|
||||
}
|
||||
}
|
||||
|
||||
// Filter by date range
|
||||
if (params.startDate) {
|
||||
conditions = and(
|
||||
conditions,
|
||||
@@ -232,79 +224,33 @@ export async function GET(request: NextRequest) {
|
||||
conditions = and(conditions, lte(workflowExecutionLogs.startedAt, new Date(params.endDate)))
|
||||
}
|
||||
|
||||
// Filter by search query
|
||||
if (params.search) {
|
||||
const searchTerm = `%${params.search}%`
|
||||
// With message removed, restrict search to executionId only
|
||||
conditions = and(conditions, sql`${workflowExecutionLogs.executionId} ILIKE ${searchTerm}`)
|
||||
}
|
||||
|
||||
// Filter by workflow name (from advanced search input)
|
||||
if (params.workflowName) {
|
||||
const nameTerm = `%${params.workflowName}%`
|
||||
conditions = and(conditions, sql`${workflow.name} ILIKE ${nameTerm}`)
|
||||
}
|
||||
|
||||
// Filter by folder name (best-effort text match when present on workflows)
|
||||
if (params.folderName) {
|
||||
const folderTerm = `%${params.folderName}%`
|
||||
conditions = and(conditions, sql`${workflow.name} ILIKE ${folderTerm}`)
|
||||
}
|
||||
|
||||
if (params.executionId) {
|
||||
conditions = and(conditions, eq(workflowExecutionLogs.executionId, params.executionId))
|
||||
}
|
||||
|
||||
if (params.costOperator && params.costValue !== undefined) {
|
||||
const costField = sql`(${workflowExecutionLogs.cost}->>'total')::numeric`
|
||||
switch (params.costOperator) {
|
||||
case '=':
|
||||
conditions = and(conditions, sql`${costField} = ${params.costValue}`)
|
||||
break
|
||||
case '>':
|
||||
conditions = and(conditions, sql`${costField} > ${params.costValue}`)
|
||||
break
|
||||
case '<':
|
||||
conditions = and(conditions, sql`${costField} < ${params.costValue}`)
|
||||
break
|
||||
case '>=':
|
||||
conditions = and(conditions, sql`${costField} >= ${params.costValue}`)
|
||||
break
|
||||
case '<=':
|
||||
conditions = and(conditions, sql`${costField} <= ${params.costValue}`)
|
||||
break
|
||||
case '!=':
|
||||
conditions = and(conditions, sql`${costField} != ${params.costValue}`)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (params.durationOperator && params.durationValue !== undefined) {
|
||||
const durationField = workflowExecutionLogs.totalDurationMs
|
||||
switch (params.durationOperator) {
|
||||
case '=':
|
||||
conditions = and(conditions, eq(durationField, params.durationValue))
|
||||
break
|
||||
case '>':
|
||||
conditions = and(conditions, gt(durationField, params.durationValue))
|
||||
break
|
||||
case '<':
|
||||
conditions = and(conditions, lt(durationField, params.durationValue))
|
||||
break
|
||||
case '>=':
|
||||
conditions = and(conditions, gte(durationField, params.durationValue))
|
||||
break
|
||||
case '<=':
|
||||
conditions = and(conditions, lte(durationField, params.durationValue))
|
||||
break
|
||||
case '!=':
|
||||
conditions = and(conditions, ne(durationField, params.durationValue))
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Execute the query using the optimized join
|
||||
const logs = await baseQuery
|
||||
.where(conditions)
|
||||
.orderBy(desc(workflowExecutionLogs.startedAt))
|
||||
.limit(params.limit)
|
||||
.offset(params.offset)
|
||||
|
||||
// Get total count for pagination using the same join structure
|
||||
const countQuery = db
|
||||
.select({ count: sql<number>`count(*)` })
|
||||
.from(workflowExecutionLogs)
|
||||
@@ -333,10 +279,13 @@ export async function GET(request: NextRequest) {
|
||||
|
||||
const count = countResult[0]?.count || 0
|
||||
|
||||
// Block executions are now extracted from trace spans instead of separate table
|
||||
const blockExecutionsByExecution: Record<string, any[]> = {}
|
||||
|
||||
// Create clean trace spans from block executions
|
||||
const createTraceSpans = (blockExecutions: any[]) => {
|
||||
return blockExecutions.map((block, index) => {
|
||||
// For error blocks, include error information in the output
|
||||
let output = block.outputData
|
||||
if (block.status === 'error' && block.errorMessage) {
|
||||
output = {
|
||||
@@ -365,6 +314,7 @@ export async function GET(request: NextRequest) {
|
||||
})
|
||||
}
|
||||
|
||||
// Extract cost information from block executions
|
||||
const extractCostSummary = (blockExecutions: any[]) => {
|
||||
let totalCost = 0
|
||||
let totalInputCost = 0
|
||||
@@ -383,6 +333,7 @@ export async function GET(request: NextRequest) {
|
||||
totalPromptTokens += block.cost.tokens?.prompt || 0
|
||||
totalCompletionTokens += block.cost.tokens?.completion || 0
|
||||
|
||||
// Track per-model costs
|
||||
if (block.cost.model) {
|
||||
if (!models.has(block.cost.model)) {
|
||||
models.set(block.cost.model, {
|
||||
@@ -412,29 +363,34 @@ export async function GET(request: NextRequest) {
|
||||
prompt: totalPromptTokens,
|
||||
completion: totalCompletionTokens,
|
||||
},
|
||||
models: Object.fromEntries(models),
|
||||
models: Object.fromEntries(models), // Convert Map to object for JSON serialization
|
||||
}
|
||||
}
|
||||
|
||||
// Transform to clean log format with workflow data included
|
||||
const enhancedLogs = logs.map((log) => {
|
||||
const blockExecutions = blockExecutionsByExecution[log.executionId] || []
|
||||
|
||||
// Only process trace spans and detailed cost in full mode
|
||||
let traceSpans = []
|
||||
let finalOutput: any
|
||||
let costSummary = (log.cost as any) || { total: 0 }
|
||||
|
||||
if (params.details === 'full' && log.executionData) {
|
||||
// Use stored trace spans if available, otherwise create from block executions
|
||||
const storedTraceSpans = (log.executionData as any)?.traceSpans
|
||||
traceSpans =
|
||||
storedTraceSpans && Array.isArray(storedTraceSpans) && storedTraceSpans.length > 0
|
||||
? storedTraceSpans
|
||||
: createTraceSpans(blockExecutions)
|
||||
|
||||
// Prefer stored cost JSON; otherwise synthesize from blocks
|
||||
costSummary =
|
||||
log.cost && Object.keys(log.cost as any).length > 0
|
||||
? (log.cost as any)
|
||||
: extractCostSummary(blockExecutions)
|
||||
|
||||
// Include finalOutput if present on executionData
|
||||
try {
|
||||
const fo = (log.executionData as any)?.finalOutput
|
||||
if (fo !== undefined) finalOutput = fo
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { Search, X } from 'lucide-react'
|
||||
import { Badge, Popover, PopoverAnchor, PopoverContent } from '@/components/emcn'
|
||||
import { useParams } from 'next/navigation'
|
||||
import { Button, Popover, PopoverAnchor, PopoverContent } from '@/components/emcn'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
import { getTriggerOptions } from '@/lib/logs/get-trigger-options'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { getIntegrationMetadata } from '@/lib/logs/get-trigger-options'
|
||||
import { type ParsedFilter, parseQuery } from '@/lib/logs/query-parser'
|
||||
import {
|
||||
type FolderData,
|
||||
@@ -16,15 +18,7 @@ import { useSearchState } from '@/app/workspace/[workspaceId]/logs/hooks/use-sea
|
||||
import { useFolderStore } from '@/stores/folders/store'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
|
||||
function truncateFilterValue(field: string, value: string): string {
|
||||
if ((field === 'executionId' || field === 'workflowId') && value.length > 12) {
|
||||
return `...${value.slice(-6)}`
|
||||
}
|
||||
if (value.length > 20) {
|
||||
return `${value.slice(0, 17)}...`
|
||||
}
|
||||
return value
|
||||
}
|
||||
const logger = createLogger('AutocompleteSearch')
|
||||
|
||||
interface AutocompleteSearchProps {
|
||||
value: string
|
||||
@@ -41,8 +35,11 @@ export function AutocompleteSearch({
|
||||
className,
|
||||
onOpenChange,
|
||||
}: AutocompleteSearchProps) {
|
||||
const params = useParams()
|
||||
const workspaceId = params.workspaceId as string
|
||||
const workflows = useWorkflowRegistry((state) => state.workflows)
|
||||
const folders = useFolderStore((state) => state.folders)
|
||||
const [triggersData, setTriggersData] = useState<TriggerData[]>([])
|
||||
|
||||
const workflowsData = useMemo<WorkflowData[]>(() => {
|
||||
return Object.values(workflows).map((w) => ({
|
||||
@@ -59,13 +56,32 @@ export function AutocompleteSearch({
|
||||
}))
|
||||
}, [folders])
|
||||
|
||||
const triggersData = useMemo<TriggerData[]>(() => {
|
||||
return getTriggerOptions().map((t) => ({
|
||||
value: t.value,
|
||||
label: t.label,
|
||||
color: t.color,
|
||||
}))
|
||||
}, [])
|
||||
useEffect(() => {
|
||||
if (!workspaceId) return
|
||||
|
||||
const fetchTriggers = async () => {
|
||||
try {
|
||||
const response = await fetch(`/api/logs/triggers?workspaceId=${workspaceId}`)
|
||||
if (!response.ok) return
|
||||
|
||||
const data = await response.json()
|
||||
const triggers: TriggerData[] = data.triggers.map((trigger: string) => {
|
||||
const metadata = getIntegrationMetadata(trigger)
|
||||
return {
|
||||
value: trigger,
|
||||
label: metadata.label,
|
||||
color: metadata.color,
|
||||
}
|
||||
})
|
||||
|
||||
setTriggersData(triggers)
|
||||
} catch (error) {
|
||||
logger.error('Failed to fetch triggers:', error)
|
||||
}
|
||||
}
|
||||
|
||||
fetchTriggers()
|
||||
}, [workspaceId])
|
||||
|
||||
const suggestionEngine = useMemo(() => {
|
||||
return new SearchSuggestions(workflowsData, foldersData, triggersData)
|
||||
@@ -87,6 +103,7 @@ export function AutocompleteSearch({
|
||||
suggestions,
|
||||
sections,
|
||||
highlightedIndex,
|
||||
highlightedBadgeIndex,
|
||||
inputRef,
|
||||
dropdownRef,
|
||||
handleInputChange,
|
||||
@@ -105,6 +122,7 @@ export function AutocompleteSearch({
|
||||
|
||||
const lastExternalValue = useRef(value)
|
||||
useEffect(() => {
|
||||
// Only re-initialize if value changed externally (not from user typing)
|
||||
if (value !== lastExternalValue.current) {
|
||||
lastExternalValue.current = value
|
||||
const parsed = parseQuery(value)
|
||||
@@ -112,6 +130,7 @@ export function AutocompleteSearch({
|
||||
}
|
||||
}, [value, initializeFromQuery])
|
||||
|
||||
// Initial sync on mount
|
||||
useEffect(() => {
|
||||
if (value) {
|
||||
const parsed = parseQuery(value)
|
||||
@@ -170,49 +189,40 @@ export function AutocompleteSearch({
|
||||
<div className='flex flex-1 items-center gap-[6px] overflow-x-auto pr-[6px] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden'>
|
||||
{/* Applied Filter Badges */}
|
||||
{appliedFilters.map((filter, index) => (
|
||||
<Badge
|
||||
<Button
|
||||
key={`${filter.field}-${filter.value}-${index}`}
|
||||
variant='outline'
|
||||
role='button'
|
||||
tabIndex={0}
|
||||
className='h-6 shrink-0 cursor-pointer whitespace-nowrap rounded-md px-2 text-[11px]'
|
||||
onClick={() => removeBadge(index)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault()
|
||||
removeBadge(index)
|
||||
}
|
||||
className={cn(
|
||||
'h-6 flex-shrink-0 gap-1 rounded-[6px] px-2 text-[11px]',
|
||||
highlightedBadgeIndex === index && 'border'
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
removeBadge(index)
|
||||
}}
|
||||
>
|
||||
<span className='text-[var(--text-muted)]'>{filter.field}:</span>
|
||||
<span className='text-[var(--text-primary)]'>
|
||||
{filter.operator !== '=' && filter.operator}
|
||||
{truncateFilterValue(filter.field, filter.originalValue)}
|
||||
{filter.originalValue}
|
||||
</span>
|
||||
<X className='h-3 w-3 shrink-0' />
|
||||
</Badge>
|
||||
<X className='h-3 w-3' />
|
||||
</Button>
|
||||
))}
|
||||
|
||||
{/* Text Search Badge (if present) */}
|
||||
{hasTextSearch && (
|
||||
<Badge
|
||||
<Button
|
||||
variant='outline'
|
||||
role='button'
|
||||
tabIndex={0}
|
||||
className='h-6 shrink-0 cursor-pointer whitespace-nowrap rounded-md px-2 text-[11px]'
|
||||
onClick={() => handleFiltersChange(appliedFilters, '')}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault()
|
||||
handleFiltersChange(appliedFilters, '')
|
||||
}
|
||||
className='h-6 flex-shrink-0 gap-1 rounded-[6px] px-2 text-[11px]'
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
handleFiltersChange(appliedFilters, '')
|
||||
}}
|
||||
>
|
||||
<span className='max-w-[150px] truncate text-[var(--text-primary)]'>
|
||||
"{textSearch}"
|
||||
</span>
|
||||
<X className='h-3 w-3 shrink-0' />
|
||||
</Badge>
|
||||
<span className='text-[var(--text-primary)]'>"{textSearch}"</span>
|
||||
<X className='h-3 w-3' />
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Input - only current typing */}
|
||||
@@ -251,8 +261,9 @@ export function AutocompleteSearch({
|
||||
sideOffset={4}
|
||||
onOpenAutoFocus={(e) => e.preventDefault()}
|
||||
>
|
||||
<div className='max-h-96 overflow-y-auto px-1'>
|
||||
<div className='max-h-96 overflow-y-auto'>
|
||||
{sections.length > 0 ? (
|
||||
// Multi-section layout
|
||||
<div className='py-1'>
|
||||
{/* Show all results (no header) */}
|
||||
{suggestions[0]?.category === 'show-all' && (
|
||||
@@ -260,9 +271,9 @@ export function AutocompleteSearch({
|
||||
key={suggestions[0].id}
|
||||
data-index={0}
|
||||
className={cn(
|
||||
'w-full rounded-[6px] px-3 py-2 text-left transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-[var(--border-focus)]',
|
||||
'hover:bg-[var(--surface-9)]',
|
||||
highlightedIndex === 0 && 'bg-[var(--surface-9)]'
|
||||
'w-full px-3 py-1.5 text-left transition-colors focus:outline-none',
|
||||
'hover:bg-[var(--surface-9)] dark:hover:bg-[var(--surface-9)]',
|
||||
highlightedIndex === 0 && 'bg-[var(--surface-9)] dark:bg-[var(--surface-9)]'
|
||||
)}
|
||||
onMouseEnter={() => setHighlightedIndex(0)}
|
||||
onMouseDown={(e) => {
|
||||
@@ -276,7 +287,7 @@ export function AutocompleteSearch({
|
||||
|
||||
{sections.map((section) => (
|
||||
<div key={section.title}>
|
||||
<div className='px-3 py-1.5 font-medium text-[12px] text-[var(--text-tertiary)] uppercase tracking-wide'>
|
||||
<div className='border-[var(--divider)] border-t px-3 py-1.5 font-medium text-[11px] text-[var(--text-tertiary)] uppercase tracking-wide'>
|
||||
{section.title}
|
||||
</div>
|
||||
{section.suggestions.map((suggestion) => {
|
||||
@@ -290,9 +301,9 @@ export function AutocompleteSearch({
|
||||
key={suggestion.id}
|
||||
data-index={index}
|
||||
className={cn(
|
||||
'w-full rounded-[6px] px-3 py-2 text-left transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-[var(--border-focus)]',
|
||||
'hover:bg-[var(--surface-9)]',
|
||||
isHighlighted && 'bg-[var(--surface-9)]'
|
||||
'w-full px-3 py-1.5 text-left transition-colors focus:outline-none',
|
||||
'hover:bg-[var(--surface-9)] dark:hover:bg-[var(--surface-9)]',
|
||||
isHighlighted && 'bg-[var(--surface-9)] dark:bg-[var(--surface-9)]'
|
||||
)}
|
||||
onMouseEnter={() => setHighlightedIndex(index)}
|
||||
onMouseDown={(e) => {
|
||||
@@ -301,11 +312,19 @@ export function AutocompleteSearch({
|
||||
}}
|
||||
>
|
||||
<div className='flex items-center justify-between gap-3'>
|
||||
<div className='min-w-0 flex-1 truncate text-[13px]'>
|
||||
{suggestion.label}
|
||||
<div className='flex min-w-0 flex-1 items-center gap-2'>
|
||||
{suggestion.category === 'trigger' && suggestion.color && (
|
||||
<div
|
||||
className='h-2 w-2 flex-shrink-0 rounded-full'
|
||||
style={{ backgroundColor: suggestion.color }}
|
||||
/>
|
||||
)}
|
||||
<div className='min-w-0 flex-1 truncate text-[13px]'>
|
||||
{suggestion.label}
|
||||
</div>
|
||||
</div>
|
||||
{suggestion.value !== suggestion.label && (
|
||||
<div className='shrink-0 font-mono text-[11px] text-[var(--text-muted)]'>
|
||||
<div className='flex-shrink-0 font-mono text-[11px] text-[var(--text-muted)]'>
|
||||
{suggestion.category === 'workflow' ||
|
||||
suggestion.category === 'folder'
|
||||
? `${suggestion.category}:`
|
||||
@@ -323,7 +342,7 @@ export function AutocompleteSearch({
|
||||
// Single section layout
|
||||
<div className='py-1'>
|
||||
{suggestionType === 'filters' && (
|
||||
<div className='px-3 py-1.5 font-medium text-[12px] text-[var(--text-tertiary)] uppercase tracking-wide'>
|
||||
<div className='border-[var(--divider)] border-b px-3 py-1.5 font-medium text-[11px] text-[var(--text-tertiary)] uppercase tracking-wide'>
|
||||
SUGGESTED FILTERS
|
||||
</div>
|
||||
)}
|
||||
@@ -333,9 +352,10 @@ export function AutocompleteSearch({
|
||||
key={suggestion.id}
|
||||
data-index={index}
|
||||
className={cn(
|
||||
'w-full rounded-[6px] px-3 py-2 text-left transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-[var(--border-focus)]',
|
||||
'hover:bg-[var(--surface-9)]',
|
||||
index === highlightedIndex && 'bg-[var(--surface-9)]'
|
||||
'w-full px-3 py-1.5 text-left transition-colors focus:outline-none',
|
||||
'hover:bg-[var(--surface-9)] dark:hover:bg-[var(--surface-9)]',
|
||||
index === highlightedIndex &&
|
||||
'bg-[var(--surface-9)] dark:bg-[var(--surface-9)]'
|
||||
)}
|
||||
onMouseEnter={() => setHighlightedIndex(index)}
|
||||
onMouseDown={(e) => {
|
||||
@@ -344,9 +364,17 @@ export function AutocompleteSearch({
|
||||
}}
|
||||
>
|
||||
<div className='flex items-center justify-between gap-3'>
|
||||
<div className='min-w-0 flex-1 text-[13px]'>{suggestion.label}</div>
|
||||
<div className='flex min-w-0 flex-1 items-center gap-2'>
|
||||
{suggestion.category === 'trigger' && suggestion.color && (
|
||||
<div
|
||||
className='h-2 w-2 flex-shrink-0 rounded-full'
|
||||
style={{ backgroundColor: suggestion.color }}
|
||||
/>
|
||||
)}
|
||||
<div className='min-w-0 flex-1 text-[13px]'>{suggestion.label}</div>
|
||||
</div>
|
||||
{suggestion.description && (
|
||||
<div className='shrink-0 text-[11px] text-[var(--text-muted)]'>
|
||||
<div className='flex-shrink-0 text-[11px] text-[var(--text-muted)]'>
|
||||
{suggestion.value}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -21,15 +21,21 @@ export function useSearchState({
|
||||
const [currentInput, setCurrentInput] = useState('')
|
||||
const [textSearch, setTextSearch] = useState('')
|
||||
|
||||
// Dropdown state
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [suggestions, setSuggestions] = useState<Suggestion[]>([])
|
||||
const [sections, setSections] = useState<SuggestionSection[]>([])
|
||||
const [highlightedIndex, setHighlightedIndex] = useState(-1)
|
||||
|
||||
// Badge interaction
|
||||
const [highlightedBadgeIndex, setHighlightedBadgeIndex] = useState<number | null>(null)
|
||||
|
||||
// Refs
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
const dropdownRef = useRef<HTMLDivElement>(null)
|
||||
const debounceRef = useRef<NodeJS.Timeout | null>(null)
|
||||
|
||||
// Update suggestions when input changes
|
||||
const updateSuggestions = useCallback(
|
||||
(input: string) => {
|
||||
const suggestionGroup = getSuggestions(input)
|
||||
@@ -49,10 +55,13 @@ export function useSearchState({
|
||||
[getSuggestions]
|
||||
)
|
||||
|
||||
// Handle input changes
|
||||
const handleInputChange = useCallback(
|
||||
(value: string) => {
|
||||
setCurrentInput(value)
|
||||
setHighlightedBadgeIndex(null) // Clear badge highlight on any input
|
||||
|
||||
// Debounce suggestion updates
|
||||
if (debounceRef.current) {
|
||||
clearTimeout(debounceRef.current)
|
||||
}
|
||||
@@ -64,9 +73,11 @@ export function useSearchState({
|
||||
[updateSuggestions, debounceMs]
|
||||
)
|
||||
|
||||
// Handle suggestion selection
|
||||
const handleSuggestionSelect = useCallback(
|
||||
(suggestion: Suggestion) => {
|
||||
if (suggestion.category === 'show-all') {
|
||||
// Treat as text search
|
||||
setTextSearch(suggestion.value)
|
||||
setCurrentInput('')
|
||||
setIsOpen(false)
|
||||
@@ -74,12 +85,15 @@ export function useSearchState({
|
||||
return
|
||||
}
|
||||
|
||||
// Check if this is a filter-key suggestion (ends with ':')
|
||||
if (suggestion.category === 'filters' && suggestion.value.endsWith(':')) {
|
||||
// Set input to the filter key and keep dropdown open for values
|
||||
setCurrentInput(suggestion.value)
|
||||
updateSuggestions(suggestion.value)
|
||||
return
|
||||
}
|
||||
|
||||
// For filter values, workflows, folders - add as a filter
|
||||
const newFilter: ParsedFilter = {
|
||||
field: suggestion.value.split(':')[0] as any,
|
||||
operator: '=',
|
||||
@@ -96,12 +110,15 @@ export function useSearchState({
|
||||
setCurrentInput('')
|
||||
setTextSearch('')
|
||||
|
||||
// Notify parent
|
||||
onFiltersChange(updatedFilters, '')
|
||||
|
||||
// Focus back on input and reopen dropdown with empty suggestions
|
||||
if (inputRef.current) {
|
||||
inputRef.current.focus()
|
||||
}
|
||||
|
||||
// Show filter keys dropdown again after selection
|
||||
setTimeout(() => {
|
||||
updateSuggestions('')
|
||||
}, 50)
|
||||
@@ -109,10 +126,12 @@ export function useSearchState({
|
||||
[appliedFilters, onFiltersChange, updateSuggestions]
|
||||
)
|
||||
|
||||
// Remove a badge
|
||||
const removeBadge = useCallback(
|
||||
(index: number) => {
|
||||
const updatedFilters = appliedFilters.filter((_, i) => i !== index)
|
||||
setAppliedFilters(updatedFilters)
|
||||
setHighlightedBadgeIndex(null)
|
||||
onFiltersChange(updatedFilters, textSearch)
|
||||
|
||||
if (inputRef.current) {
|
||||
@@ -122,22 +141,39 @@ export function useSearchState({
|
||||
[appliedFilters, textSearch, onFiltersChange]
|
||||
)
|
||||
|
||||
// Handle keyboard navigation
|
||||
const handleKeyDown = useCallback(
|
||||
(event: React.KeyboardEvent) => {
|
||||
// Backspace on empty input - badge deletion
|
||||
if (event.key === 'Backspace' && currentInput === '') {
|
||||
if (appliedFilters.length > 0) {
|
||||
event.preventDefault()
|
||||
removeBadge(appliedFilters.length - 1)
|
||||
event.preventDefault()
|
||||
|
||||
if (highlightedBadgeIndex !== null) {
|
||||
// Delete highlighted badge
|
||||
removeBadge(highlightedBadgeIndex)
|
||||
} else if (appliedFilters.length > 0) {
|
||||
// Highlight last badge
|
||||
setHighlightedBadgeIndex(appliedFilters.length - 1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Clear badge highlight on any other key when not in dropdown navigation
|
||||
if (
|
||||
highlightedBadgeIndex !== null &&
|
||||
!['ArrowDown', 'ArrowUp', 'Enter'].includes(event.key)
|
||||
) {
|
||||
setHighlightedBadgeIndex(null)
|
||||
}
|
||||
|
||||
// Enter key
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault()
|
||||
|
||||
if (isOpen && highlightedIndex >= 0 && suggestions[highlightedIndex]) {
|
||||
handleSuggestionSelect(suggestions[highlightedIndex])
|
||||
} else if (currentInput.trim()) {
|
||||
// Submit current input as text search
|
||||
setTextSearch(currentInput.trim())
|
||||
setCurrentInput('')
|
||||
setIsOpen(false)
|
||||
@@ -146,6 +182,7 @@ export function useSearchState({
|
||||
return
|
||||
}
|
||||
|
||||
// Dropdown navigation
|
||||
if (!isOpen) return
|
||||
|
||||
switch (event.key) {
|
||||
@@ -179,6 +216,7 @@ export function useSearchState({
|
||||
},
|
||||
[
|
||||
currentInput,
|
||||
highlightedBadgeIndex,
|
||||
appliedFilters,
|
||||
isOpen,
|
||||
highlightedIndex,
|
||||
@@ -189,10 +227,12 @@ export function useSearchState({
|
||||
]
|
||||
)
|
||||
|
||||
// Handle focus
|
||||
const handleFocus = useCallback(() => {
|
||||
updateSuggestions(currentInput)
|
||||
}, [currentInput, updateSuggestions])
|
||||
|
||||
// Handle blur
|
||||
const handleBlur = useCallback(() => {
|
||||
setTimeout(() => {
|
||||
setIsOpen(false)
|
||||
@@ -200,6 +240,7 @@ export function useSearchState({
|
||||
}, 150)
|
||||
}, [])
|
||||
|
||||
// Clear all filters
|
||||
const clearAll = useCallback(() => {
|
||||
setAppliedFilters([])
|
||||
setCurrentInput('')
|
||||
@@ -212,6 +253,7 @@ export function useSearchState({
|
||||
}
|
||||
}, [onFiltersChange])
|
||||
|
||||
// Initialize from external value (URL params, etc.)
|
||||
const initializeFromQuery = useCallback((query: string, filters: ParsedFilter[]) => {
|
||||
setAppliedFilters(filters)
|
||||
setTextSearch(query)
|
||||
@@ -219,6 +261,7 @@ export function useSearchState({
|
||||
}, [])
|
||||
|
||||
return {
|
||||
// State
|
||||
appliedFilters,
|
||||
currentInput,
|
||||
textSearch,
|
||||
@@ -226,10 +269,13 @@ export function useSearchState({
|
||||
suggestions,
|
||||
sections,
|
||||
highlightedIndex,
|
||||
highlightedBadgeIndex,
|
||||
|
||||
// Refs
|
||||
inputRef,
|
||||
dropdownRef,
|
||||
|
||||
// Handlers
|
||||
handleInputChange,
|
||||
handleSuggestionSelect,
|
||||
handleKeyDown,
|
||||
@@ -239,6 +285,7 @@ export function useSearchState({
|
||||
clearAll,
|
||||
initializeFromQuery,
|
||||
|
||||
// Setters for external control
|
||||
setHighlightedIndex,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import { Button } from '@/components/emcn'
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||
import type { SaveStatus } from '@/hooks/use-auto-save'
|
||||
|
||||
interface SaveStatusIndicatorProps {
|
||||
/** Current save status */
|
||||
status: SaveStatus
|
||||
/** Error message to display */
|
||||
errorMessage: string | null
|
||||
/** Text to show while saving (e.g., "Saving schedule...") */
|
||||
savingText?: string
|
||||
/** Text to show while loading (e.g., "Loading schedule...") */
|
||||
loadingText?: string
|
||||
/** Whether to show loading indicator */
|
||||
isLoading?: boolean
|
||||
/** Callback when retry button is clicked */
|
||||
onRetry?: () => void
|
||||
/** Whether retry is disabled (e.g., during saving) */
|
||||
retryDisabled?: boolean
|
||||
/** Number of retry attempts made */
|
||||
retryCount?: number
|
||||
/** Maximum retry attempts allowed */
|
||||
maxRetries?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared component for displaying save status indicators.
|
||||
* Shows saving spinner, error alerts with retry, and loading indicators.
|
||||
*/
|
||||
export function SaveStatusIndicator({
|
||||
status,
|
||||
errorMessage,
|
||||
savingText = 'Saving...',
|
||||
loadingText = 'Loading...',
|
||||
isLoading = false,
|
||||
onRetry,
|
||||
retryDisabled = false,
|
||||
retryCount = 0,
|
||||
maxRetries = 3,
|
||||
}: SaveStatusIndicatorProps) {
|
||||
const maxRetriesReached = retryCount >= maxRetries
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Saving indicator */}
|
||||
{status === 'saving' && (
|
||||
<div className='flex items-center gap-2 text-muted-foreground text-sm'>
|
||||
<div className='h-4 w-4 animate-spin rounded-full border-[1.5px] border-current border-t-transparent' />
|
||||
{savingText}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Error message with retry */}
|
||||
{errorMessage && (
|
||||
<Alert variant='destructive'>
|
||||
<AlertDescription className='flex items-center justify-between'>
|
||||
<span>
|
||||
{errorMessage}
|
||||
{maxRetriesReached && (
|
||||
<span className='ml-1 text-xs opacity-75'>(Max retries reached)</span>
|
||||
)}
|
||||
</span>
|
||||
{onRetry && (
|
||||
<Button
|
||||
variant='ghost'
|
||||
onClick={onRetry}
|
||||
disabled={retryDisabled || status === 'saving'}
|
||||
className='ml-2 h-6 px-2 text-xs'
|
||||
>
|
||||
Retry
|
||||
</Button>
|
||||
)}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* Loading indicator */}
|
||||
{isLoading && status !== 'saving' && (
|
||||
<div className='flex items-center gap-2 text-muted-foreground text-sm'>
|
||||
<div className='h-4 w-4 animate-spin rounded-full border-[1.5px] border-current border-t-transparent' />
|
||||
{loadingText}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,11 +1,9 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useParams } from 'next/navigation'
|
||||
import { Button, Modal, ModalBody, ModalContent, ModalFooter, ModalHeader } from '@/components/emcn'
|
||||
import { Trash } from '@/components/emcn/icons/trash'
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { parseCronToHumanReadable } from '@/lib/workflows/schedules/utils'
|
||||
import { SaveStatusIndicator } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/save-status-indicator/save-status-indicator'
|
||||
import { useAutoSave } from '@/hooks/use-auto-save'
|
||||
import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow'
|
||||
import { useScheduleManagement } from '@/hooks/use-schedule-management'
|
||||
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
|
||||
@@ -18,16 +16,9 @@ interface ScheduleSaveProps {
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
type SaveStatus = 'idle' | 'saving' | 'saved' | 'error'
|
||||
|
||||
export function ScheduleSave({ blockId, isPreview = false, disabled = false }: ScheduleSaveProps) {
|
||||
const params = useParams()
|
||||
const workflowId = params.workflowId as string
|
||||
const [saveStatus, setSaveStatus] = useState<SaveStatus>('idle')
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null)
|
||||
const [deleteStatus, setDeleteStatus] = useState<'idle' | 'deleting'>('idle')
|
||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false)
|
||||
const [scheduleStatus, setScheduleStatus] = useState<'active' | 'disabled' | null>(null)
|
||||
const [nextRunAt, setNextRunAt] = useState<Date | null>(null)
|
||||
const [lastRanAt, setLastRanAt] = useState<Date | null>(null)
|
||||
const [failedCount, setFailedCount] = useState<number>(0)
|
||||
@@ -36,7 +27,7 @@ export function ScheduleSave({ blockId, isPreview = false, disabled = false }: S
|
||||
|
||||
const { collaborativeSetSubblockValue } = useCollaborativeWorkflow()
|
||||
|
||||
const { scheduleId, saveConfig, deleteConfig, isSaving } = useScheduleManagement({
|
||||
const { scheduleId, saveConfig, isSaving } = useScheduleManagement({
|
||||
blockId,
|
||||
isPreview,
|
||||
})
|
||||
@@ -56,13 +47,8 @@ export function ScheduleSave({ blockId, isPreview = false, disabled = false }: S
|
||||
)
|
||||
const scheduleTimezone = useSubBlockStore((state) => state.getValue(blockId, 'timezone'))
|
||||
|
||||
const validateRequiredFields = useCallback((): { valid: boolean; missingFields: string[] } => {
|
||||
const missingFields: string[] = []
|
||||
|
||||
if (!scheduleType) {
|
||||
missingFields.push('Frequency')
|
||||
return { valid: false, missingFields }
|
||||
}
|
||||
const validateRequiredFields = useCallback((): boolean => {
|
||||
if (!scheduleType) return false
|
||||
|
||||
switch (scheduleType) {
|
||||
case 'minutes': {
|
||||
@@ -73,7 +59,7 @@ export function ScheduleSave({ blockId, isPreview = false, disabled = false }: S
|
||||
minutesNum < 1 ||
|
||||
minutesNum > 1440
|
||||
) {
|
||||
missingFields.push('Minutes Interval (must be 1-1440)')
|
||||
return false
|
||||
}
|
||||
break
|
||||
}
|
||||
@@ -87,48 +73,39 @@ export function ScheduleSave({ blockId, isPreview = false, disabled = false }: S
|
||||
hourlyNum < 0 ||
|
||||
hourlyNum > 59
|
||||
) {
|
||||
missingFields.push('Minute (must be 0-59)')
|
||||
return false
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'daily':
|
||||
if (!scheduleDailyTime) {
|
||||
missingFields.push('Time')
|
||||
}
|
||||
if (!scheduleDailyTime) return false
|
||||
break
|
||||
case 'weekly':
|
||||
if (!scheduleWeeklyDay) {
|
||||
missingFields.push('Day of Week')
|
||||
}
|
||||
if (!scheduleWeeklyTime) {
|
||||
missingFields.push('Time')
|
||||
}
|
||||
if (!scheduleWeeklyDay || !scheduleWeeklyTime) return false
|
||||
break
|
||||
case 'monthly': {
|
||||
const monthlyNum = Number(scheduleMonthlyDay)
|
||||
if (!scheduleMonthlyDay || Number.isNaN(monthlyNum) || monthlyNum < 1 || monthlyNum > 31) {
|
||||
missingFields.push('Day of Month (must be 1-31)')
|
||||
}
|
||||
if (!scheduleMonthlyTime) {
|
||||
missingFields.push('Time')
|
||||
if (
|
||||
!scheduleMonthlyDay ||
|
||||
Number.isNaN(monthlyNum) ||
|
||||
monthlyNum < 1 ||
|
||||
monthlyNum > 31 ||
|
||||
!scheduleMonthlyTime
|
||||
) {
|
||||
return false
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'custom':
|
||||
if (!scheduleCronExpression) {
|
||||
missingFields.push('Cron Expression')
|
||||
}
|
||||
if (!scheduleCronExpression) return false
|
||||
break
|
||||
}
|
||||
|
||||
if (!scheduleTimezone && scheduleType !== 'minutes' && scheduleType !== 'hourly') {
|
||||
missingFields.push('Timezone')
|
||||
return false
|
||||
}
|
||||
|
||||
return {
|
||||
valid: missingFields.length === 0,
|
||||
missingFields,
|
||||
}
|
||||
return true
|
||||
}, [
|
||||
scheduleType,
|
||||
scheduleMinutesInterval,
|
||||
@@ -160,7 +137,7 @@ export function ScheduleSave({ blockId, isPreview = false, disabled = false }: S
|
||||
const subscribedSubBlockValues = useSubBlockStore(
|
||||
useCallback(
|
||||
(state) => {
|
||||
const values: Record<string, any> = {}
|
||||
const values: Record<string, unknown> = {}
|
||||
requiredSubBlockIds.forEach((subBlockId) => {
|
||||
const value = state.getValue(blockId, subBlockId)
|
||||
if (value !== null && value !== undefined && value !== '') {
|
||||
@@ -173,52 +150,57 @@ export function ScheduleSave({ blockId, isPreview = false, disabled = false }: S
|
||||
)
|
||||
)
|
||||
|
||||
const previousValuesRef = useRef<Record<string, any>>({})
|
||||
const validationTimeoutRef = useRef<NodeJS.Timeout | null>(null)
|
||||
const configFingerprint = useMemo(() => {
|
||||
return JSON.stringify(subscribedSubBlockValues)
|
||||
}, [subscribedSubBlockValues])
|
||||
|
||||
const handleSaveSuccess = useCallback(
|
||||
async (result: { success: boolean; nextRunAt?: string; cronExpression?: string }) => {
|
||||
const scheduleIdValue = useSubBlockStore.getState().getValue(blockId, 'scheduleId')
|
||||
collaborativeSetSubblockValue(blockId, 'scheduleId', scheduleIdValue)
|
||||
|
||||
if (result.nextRunAt) {
|
||||
setNextRunAt(new Date(result.nextRunAt))
|
||||
}
|
||||
|
||||
await fetchScheduleStatus()
|
||||
|
||||
if (result.cronExpression) {
|
||||
setSavedCronExpression(result.cronExpression)
|
||||
}
|
||||
},
|
||||
[blockId, collaborativeSetSubblockValue]
|
||||
)
|
||||
|
||||
const {
|
||||
saveStatus,
|
||||
errorMessage,
|
||||
retryCount,
|
||||
maxRetries,
|
||||
triggerSave,
|
||||
onConfigChange,
|
||||
markInitialLoadComplete,
|
||||
} = useAutoSave({
|
||||
disabled: isPreview || disabled,
|
||||
isExternallySaving: isSaving,
|
||||
validate: validateRequiredFields,
|
||||
onSave: saveConfig,
|
||||
onSaveSuccess: handleSaveSuccess,
|
||||
loggerName: 'ScheduleSave',
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (saveStatus !== 'error') {
|
||||
previousValuesRef.current = subscribedSubBlockValues
|
||||
return
|
||||
onConfigChange(configFingerprint)
|
||||
}, [configFingerprint, onConfigChange])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoadingStatus && scheduleId) {
|
||||
return markInitialLoadComplete(configFingerprint)
|
||||
}
|
||||
|
||||
const hasChanges = Object.keys(subscribedSubBlockValues).some(
|
||||
(key) =>
|
||||
previousValuesRef.current[key] !== (subscribedSubBlockValues as Record<string, any>)[key]
|
||||
)
|
||||
|
||||
if (!hasChanges) {
|
||||
return
|
||||
if (!scheduleId && !isLoadingStatus) {
|
||||
return markInitialLoadComplete(configFingerprint)
|
||||
}
|
||||
|
||||
if (validationTimeoutRef.current) {
|
||||
clearTimeout(validationTimeoutRef.current)
|
||||
}
|
||||
|
||||
validationTimeoutRef.current = setTimeout(() => {
|
||||
const validation = validateRequiredFields()
|
||||
|
||||
if (validation.valid) {
|
||||
setErrorMessage(null)
|
||||
setSaveStatus('idle')
|
||||
logger.debug('Error cleared after validation passed', { blockId })
|
||||
} else {
|
||||
setErrorMessage(`Missing required fields: ${validation.missingFields.join(', ')}`)
|
||||
logger.debug('Error message updated', {
|
||||
blockId,
|
||||
missingFields: validation.missingFields,
|
||||
})
|
||||
}
|
||||
|
||||
previousValuesRef.current = subscribedSubBlockValues
|
||||
}, 300)
|
||||
|
||||
return () => {
|
||||
if (validationTimeoutRef.current) {
|
||||
clearTimeout(validationTimeoutRef.current)
|
||||
}
|
||||
}
|
||||
}, [blockId, subscribedSubBlockValues, saveStatus, validateRequiredFields])
|
||||
}, [isLoadingStatus, scheduleId, configFingerprint, markInitialLoadComplete])
|
||||
|
||||
const fetchScheduleStatus = useCallback(async () => {
|
||||
if (!scheduleId || isPreview) return
|
||||
@@ -231,7 +213,6 @@ export function ScheduleSave({ blockId, isPreview = false, disabled = false }: S
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
if (data.schedule) {
|
||||
setScheduleStatus(data.schedule.status)
|
||||
setNextRunAt(data.schedule.nextRunAt ? new Date(data.schedule.nextRunAt) : null)
|
||||
setLastRanAt(data.schedule.lastRanAt ? new Date(data.schedule.lastRanAt) : null)
|
||||
setFailedCount(data.schedule.failedCount || 0)
|
||||
@@ -251,249 +232,82 @@ export function ScheduleSave({ blockId, isPreview = false, disabled = false }: S
|
||||
}
|
||||
}, [scheduleId, isPreview, fetchScheduleStatus])
|
||||
|
||||
const handleSave = async () => {
|
||||
if (isPreview || disabled) return
|
||||
|
||||
setSaveStatus('saving')
|
||||
setErrorMessage(null)
|
||||
|
||||
try {
|
||||
const validation = validateRequiredFields()
|
||||
if (!validation.valid) {
|
||||
setErrorMessage(`Missing required fields: ${validation.missingFields.join(', ')}`)
|
||||
setSaveStatus('error')
|
||||
return
|
||||
}
|
||||
|
||||
const result = await saveConfig()
|
||||
if (!result.success) {
|
||||
throw new Error('Save config returned false')
|
||||
}
|
||||
|
||||
setSaveStatus('saved')
|
||||
setErrorMessage(null)
|
||||
|
||||
const scheduleIdValue = useSubBlockStore.getState().getValue(blockId, 'scheduleId')
|
||||
collaborativeSetSubblockValue(blockId, 'scheduleId', scheduleIdValue)
|
||||
|
||||
if (result.nextRunAt) {
|
||||
setNextRunAt(new Date(result.nextRunAt))
|
||||
setScheduleStatus('active')
|
||||
}
|
||||
|
||||
// Fetch additional status info, then apply cron from save result to prevent stale data
|
||||
await fetchScheduleStatus()
|
||||
|
||||
if (result.cronExpression) {
|
||||
setSavedCronExpression(result.cronExpression)
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
setSaveStatus('idle')
|
||||
}, 2000)
|
||||
|
||||
logger.info('Schedule configuration saved successfully', {
|
||||
blockId,
|
||||
hasScheduleId: !!scheduleId,
|
||||
})
|
||||
} catch (error: any) {
|
||||
setSaveStatus('error')
|
||||
setErrorMessage(error.message || 'An error occurred while saving.')
|
||||
logger.error('Error saving schedule config', { error })
|
||||
}
|
||||
if (isPreview) {
|
||||
return null
|
||||
}
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (isPreview || disabled) return
|
||||
const hasScheduleInfo = scheduleId || isLoadingStatus || saveStatus === 'saving' || errorMessage
|
||||
|
||||
setShowDeleteDialog(false)
|
||||
setDeleteStatus('deleting')
|
||||
|
||||
try {
|
||||
const success = await deleteConfig()
|
||||
if (!success) {
|
||||
throw new Error('Failed to delete schedule')
|
||||
}
|
||||
|
||||
setScheduleStatus(null)
|
||||
setNextRunAt(null)
|
||||
setLastRanAt(null)
|
||||
setFailedCount(0)
|
||||
|
||||
collaborativeSetSubblockValue(blockId, 'scheduleId', null)
|
||||
|
||||
logger.info('Schedule deleted successfully', { blockId })
|
||||
} catch (error: any) {
|
||||
setErrorMessage(error.message || 'An error occurred while deleting.')
|
||||
logger.error('Error deleting schedule', { error })
|
||||
} finally {
|
||||
setDeleteStatus('idle')
|
||||
}
|
||||
}
|
||||
|
||||
const handleDeleteConfirm = () => {
|
||||
handleDelete()
|
||||
}
|
||||
|
||||
const handleToggleStatus = async () => {
|
||||
if (!scheduleId || isPreview || disabled) return
|
||||
|
||||
try {
|
||||
const action = scheduleStatus === 'active' ? 'disable' : 'reactivate'
|
||||
const response = await fetch(`/api/schedules/${scheduleId}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ action }),
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
await fetchScheduleStatus()
|
||||
logger.info(`Schedule ${action}d successfully`, { scheduleId })
|
||||
} else {
|
||||
throw new Error(`Failed to ${action} schedule`)
|
||||
}
|
||||
} catch (error: any) {
|
||||
setErrorMessage(
|
||||
error.message ||
|
||||
`An error occurred while ${scheduleStatus === 'active' ? 'disabling' : 'reactivating'} the schedule.`
|
||||
)
|
||||
logger.error('Error toggling schedule status', { error })
|
||||
}
|
||||
if (!hasScheduleInfo) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='mt-2'>
|
||||
<div className='flex gap-2'>
|
||||
<Button
|
||||
variant='default'
|
||||
onClick={handleSave}
|
||||
disabled={disabled || isPreview || isSaving || saveStatus === 'saving' || isLoadingStatus}
|
||||
className={cn(
|
||||
'h-9 flex-1 rounded-[8px] transition-all duration-200',
|
||||
saveStatus === 'saved' && 'bg-green-600 hover:bg-green-700',
|
||||
saveStatus === 'error' && 'bg-red-600 hover:bg-red-700'
|
||||
)}
|
||||
>
|
||||
{saveStatus === 'saving' && (
|
||||
<>
|
||||
<div className='mr-2 h-4 w-4 animate-spin rounded-full border-[1.5px] border-current border-t-transparent' />
|
||||
Saving...
|
||||
</>
|
||||
)}
|
||||
{saveStatus === 'saved' && 'Saved'}
|
||||
{saveStatus === 'idle' && (scheduleId ? 'Update Schedule' : 'Save Schedule')}
|
||||
{saveStatus === 'error' && 'Error'}
|
||||
</Button>
|
||||
<div className='space-y-1 pb-4'>
|
||||
<SaveStatusIndicator
|
||||
status={saveStatus}
|
||||
errorMessage={errorMessage}
|
||||
savingText='Saving schedule...'
|
||||
loadingText='Loading schedule...'
|
||||
isLoading={isLoadingStatus}
|
||||
onRetry={triggerSave}
|
||||
retryDisabled={isSaving}
|
||||
retryCount={retryCount}
|
||||
maxRetries={maxRetries}
|
||||
/>
|
||||
|
||||
{scheduleId && (
|
||||
<Button
|
||||
variant='default'
|
||||
onClick={() => setShowDeleteDialog(true)}
|
||||
disabled={disabled || isPreview || deleteStatus === 'deleting' || isSaving}
|
||||
className='h-9 rounded-[8px] px-3'
|
||||
>
|
||||
{deleteStatus === 'deleting' ? (
|
||||
<div className='h-4 w-4 animate-spin rounded-full border-[1.5px] border-current border-t-transparent' />
|
||||
) : (
|
||||
<Trash className='h-[14px] w-[14px]' />
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{errorMessage && (
|
||||
<Alert variant='destructive' className='mt-2'>
|
||||
<AlertDescription>{errorMessage}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{scheduleId && (scheduleStatus || isLoadingStatus || nextRunAt) && (
|
||||
<div className='mt-2 space-y-1'>
|
||||
{isLoadingStatus ? (
|
||||
<div className='flex items-center gap-2 text-muted-foreground text-sm'>
|
||||
<div className='h-4 w-4 animate-spin rounded-full border-[1.5px] border-current border-t-transparent' />
|
||||
Loading schedule status...
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{failedCount > 0 && (
|
||||
<div className='flex items-center gap-2'>
|
||||
<span className='text-destructive text-sm'>
|
||||
⚠️ {failedCount} failed run{failedCount !== 1 ? 's' : ''}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{savedCronExpression && (
|
||||
<p className='text-muted-foreground text-sm'>
|
||||
Runs{' '}
|
||||
{parseCronToHumanReadable(
|
||||
savedCronExpression,
|
||||
scheduleTimezone || 'UTC'
|
||||
).toLowerCase()}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{nextRunAt && (
|
||||
<p className='text-sm'>
|
||||
<span className='font-medium'>Next run:</span>{' '}
|
||||
{nextRunAt.toLocaleString('en-US', {
|
||||
timeZone: scheduleTimezone || 'UTC',
|
||||
year: 'numeric',
|
||||
month: 'numeric',
|
||||
day: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: '2-digit',
|
||||
hour12: true,
|
||||
})}{' '}
|
||||
{scheduleTimezone || 'UTC'}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{lastRanAt && (
|
||||
<p className='text-muted-foreground text-sm'>
|
||||
<span className='font-medium'>Last ran:</span>{' '}
|
||||
{lastRanAt.toLocaleString('en-US', {
|
||||
timeZone: scheduleTimezone || 'UTC',
|
||||
year: 'numeric',
|
||||
month: 'numeric',
|
||||
day: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: '2-digit',
|
||||
hour12: true,
|
||||
})}{' '}
|
||||
{scheduleTimezone || 'UTC'}
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Modal open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
|
||||
<ModalContent size='sm'>
|
||||
<ModalHeader>Delete Schedule</ModalHeader>
|
||||
<ModalBody>
|
||||
<p className='text-[12px] text-[var(--text-tertiary)]'>
|
||||
Are you sure you want to delete this schedule configuration? This will stop the
|
||||
workflow from running automatically.{' '}
|
||||
<span className='text-[var(--text-error)]'>This action cannot be undone.</span>
|
||||
{/* Schedule status info */}
|
||||
{scheduleId && !isLoadingStatus && saveStatus !== 'saving' && (
|
||||
<>
|
||||
{failedCount > 0 && (
|
||||
<p className='text-destructive text-sm'>
|
||||
{failedCount} failed run{failedCount !== 1 ? 's' : ''}
|
||||
</p>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button variant='active' onClick={() => setShowDeleteDialog(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant='primary'
|
||||
onClick={handleDeleteConfirm}
|
||||
className='!bg-[var(--text-error)] !text-white hover:!bg-[var(--text-error)]/90'
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
)}
|
||||
|
||||
{savedCronExpression && (
|
||||
<p className='text-muted-foreground text-sm'>
|
||||
Runs{' '}
|
||||
{parseCronToHumanReadable(
|
||||
savedCronExpression,
|
||||
scheduleTimezone || 'UTC'
|
||||
).toLowerCase()}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{nextRunAt && (
|
||||
<p className='text-sm'>
|
||||
<span className='font-medium'>Next run:</span>{' '}
|
||||
{nextRunAt.toLocaleString('en-US', {
|
||||
timeZone: scheduleTimezone || 'UTC',
|
||||
year: 'numeric',
|
||||
month: 'numeric',
|
||||
day: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: '2-digit',
|
||||
hour12: true,
|
||||
})}{' '}
|
||||
{scheduleTimezone || 'UTC'}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{lastRanAt && (
|
||||
<p className='text-muted-foreground text-sm'>
|
||||
<span className='font-medium'>Last ran:</span>{' '}
|
||||
{lastRanAt.toLocaleString('en-US', {
|
||||
timeZone: scheduleTimezone || 'UTC',
|
||||
year: 'numeric',
|
||||
month: 'numeric',
|
||||
day: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: '2-digit',
|
||||
hour12: true,
|
||||
})}{' '}
|
||||
{scheduleTimezone || 'UTC'}
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,23 +1,15 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import {
|
||||
Button,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
} from '@/components/emcn/components'
|
||||
import { Trash } from '@/components/emcn/icons/trash'
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { Button } from '@/components/emcn/components'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { SaveStatusIndicator } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/save-status-indicator/save-status-indicator'
|
||||
import { ShortInput } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/short-input/short-input'
|
||||
import { useAutoSave } from '@/hooks/use-auto-save'
|
||||
import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow'
|
||||
import { useTriggerConfigAggregation } from '@/hooks/use-trigger-config-aggregation'
|
||||
import { getTriggerConfigAggregation } from '@/hooks/use-trigger-config-aggregation'
|
||||
import { useWebhookManagement } from '@/hooks/use-webhook-management'
|
||||
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
|
||||
import { getTrigger, isTriggerValid } from '@/triggers'
|
||||
import { SYSTEM_SUBBLOCK_IDS } from '@/triggers/constants'
|
||||
import { ShortInput } from '../short-input/short-input'
|
||||
|
||||
const logger = createLogger('TriggerSave')
|
||||
|
||||
@@ -29,8 +21,6 @@ interface TriggerSaveProps {
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
type SaveStatus = 'idle' | 'saving' | 'saved' | 'error'
|
||||
|
||||
export function TriggerSave({
|
||||
blockId,
|
||||
subBlockId,
|
||||
@@ -38,11 +28,8 @@ export function TriggerSave({
|
||||
isPreview = false,
|
||||
disabled = false,
|
||||
}: TriggerSaveProps) {
|
||||
const [saveStatus, setSaveStatus] = useState<SaveStatus>('idle')
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null)
|
||||
const [deleteStatus, setDeleteStatus] = useState<'idle' | 'deleting'>('idle')
|
||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false)
|
||||
const [isGeneratingTestUrl, setIsGeneratingTestUrl] = useState(false)
|
||||
const [testUrlError, setTestUrlError] = useState<string | null>(null)
|
||||
|
||||
const storedTestUrl = useSubBlockStore((state) => state.getValue(blockId, 'testUrl'))
|
||||
const storedTestUrlExpiresAt = useSubBlockStore((state) =>
|
||||
@@ -70,13 +57,12 @@ export function TriggerSave({
|
||||
|
||||
const { collaborativeSetSubblockValue } = useCollaborativeWorkflow()
|
||||
|
||||
const { webhookId, saveConfig, deleteConfig, isLoading } = useWebhookManagement({
|
||||
const { webhookId, saveConfig, isLoading } = useWebhookManagement({
|
||||
blockId,
|
||||
triggerId: effectiveTriggerId,
|
||||
isPreview,
|
||||
})
|
||||
|
||||
const triggerConfig = useSubBlockStore((state) => state.getValue(blockId, 'triggerConfig'))
|
||||
const triggerCredentials = useSubBlockStore((state) =>
|
||||
state.getValue(blockId, 'triggerCredentials')
|
||||
)
|
||||
@@ -87,40 +73,26 @@ export function TriggerSave({
|
||||
const hasWebhookUrlDisplay =
|
||||
triggerDef?.subBlocks.some((sb) => sb.id === 'webhookUrlDisplay') ?? false
|
||||
|
||||
const validateRequiredFields = useCallback(
|
||||
(
|
||||
configToCheck: Record<string, any> | null | undefined
|
||||
): { valid: boolean; missingFields: string[] } => {
|
||||
if (!triggerDef) {
|
||||
return { valid: true, missingFields: [] }
|
||||
const validateRequiredFields = useCallback((): boolean => {
|
||||
if (!triggerDef) return true
|
||||
|
||||
const aggregatedConfig = getTriggerConfigAggregation(blockId, effectiveTriggerId)
|
||||
|
||||
const requiredSubBlocks = triggerDef.subBlocks.filter(
|
||||
(sb) => sb.required && sb.mode === 'trigger' && !SYSTEM_SUBBLOCK_IDS.includes(sb.id)
|
||||
)
|
||||
|
||||
for (const subBlock of requiredSubBlocks) {
|
||||
if (subBlock.id === 'triggerCredentials') {
|
||||
if (!triggerCredentials) return false
|
||||
} else {
|
||||
const value = aggregatedConfig?.[subBlock.id]
|
||||
if (value === undefined || value === null || value === '') return false
|
||||
}
|
||||
}
|
||||
|
||||
const missingFields: string[] = []
|
||||
|
||||
triggerDef.subBlocks
|
||||
.filter(
|
||||
(sb) => sb.required && sb.mode === 'trigger' && !SYSTEM_SUBBLOCK_IDS.includes(sb.id)
|
||||
)
|
||||
.forEach((subBlock) => {
|
||||
if (subBlock.id === 'triggerCredentials') {
|
||||
if (!triggerCredentials) {
|
||||
missingFields.push(subBlock.title || 'Credentials')
|
||||
}
|
||||
} else {
|
||||
const value = configToCheck?.[subBlock.id]
|
||||
if (value === undefined || value === null || value === '') {
|
||||
missingFields.push(subBlock.title || subBlock.id)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
valid: missingFields.length === 0,
|
||||
missingFields,
|
||||
}
|
||||
},
|
||||
[triggerDef, triggerCredentials]
|
||||
)
|
||||
return true
|
||||
}, [triggerDef, triggerCredentials, blockId, effectiveTriggerId])
|
||||
|
||||
const requiredSubBlockIds = useMemo(() => {
|
||||
if (!triggerDef) return []
|
||||
@@ -133,11 +105,11 @@ export function TriggerSave({
|
||||
useCallback(
|
||||
(state) => {
|
||||
if (!triggerDef) return {}
|
||||
const values: Record<string, any> = {}
|
||||
requiredSubBlockIds.forEach((subBlockId) => {
|
||||
const value = state.getValue(blockId, subBlockId)
|
||||
const values: Record<string, unknown> = {}
|
||||
requiredSubBlockIds.forEach((id) => {
|
||||
const value = state.getValue(blockId, id)
|
||||
if (value !== null && value !== undefined && value !== '') {
|
||||
values[subBlockId] = value
|
||||
values[id] = value
|
||||
}
|
||||
})
|
||||
return values
|
||||
@@ -146,69 +118,9 @@ export function TriggerSave({
|
||||
)
|
||||
)
|
||||
|
||||
const previousValuesRef = useRef<Record<string, any>>({})
|
||||
const validationTimeoutRef = useRef<NodeJS.Timeout | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (saveStatus !== 'error' || !triggerDef) {
|
||||
previousValuesRef.current = subscribedSubBlockValues
|
||||
return
|
||||
}
|
||||
|
||||
const hasChanges = Object.keys(subscribedSubBlockValues).some(
|
||||
(key) =>
|
||||
previousValuesRef.current[key] !== (subscribedSubBlockValues as Record<string, any>)[key]
|
||||
)
|
||||
|
||||
if (!hasChanges) {
|
||||
return
|
||||
}
|
||||
|
||||
if (validationTimeoutRef.current) {
|
||||
clearTimeout(validationTimeoutRef.current)
|
||||
}
|
||||
|
||||
validationTimeoutRef.current = setTimeout(() => {
|
||||
const aggregatedConfig = useTriggerConfigAggregation(blockId, effectiveTriggerId)
|
||||
|
||||
if (aggregatedConfig) {
|
||||
useSubBlockStore.getState().setValue(blockId, 'triggerConfig', aggregatedConfig)
|
||||
}
|
||||
|
||||
const validation = validateRequiredFields(aggregatedConfig)
|
||||
|
||||
if (validation.valid) {
|
||||
setErrorMessage(null)
|
||||
setSaveStatus('idle')
|
||||
logger.debug('Error cleared after validation passed', {
|
||||
blockId,
|
||||
triggerId: effectiveTriggerId,
|
||||
})
|
||||
} else {
|
||||
setErrorMessage(`Missing required fields: ${validation.missingFields.join(', ')}`)
|
||||
logger.debug('Error message updated', {
|
||||
blockId,
|
||||
triggerId: effectiveTriggerId,
|
||||
missingFields: validation.missingFields,
|
||||
})
|
||||
}
|
||||
|
||||
previousValuesRef.current = subscribedSubBlockValues
|
||||
}, 300)
|
||||
|
||||
return () => {
|
||||
if (validationTimeoutRef.current) {
|
||||
clearTimeout(validationTimeoutRef.current)
|
||||
}
|
||||
}
|
||||
}, [
|
||||
blockId,
|
||||
effectiveTriggerId,
|
||||
triggerDef,
|
||||
subscribedSubBlockValues,
|
||||
saveStatus,
|
||||
validateRequiredFields,
|
||||
])
|
||||
const configFingerprint = useMemo(() => {
|
||||
return JSON.stringify({ ...subscribedSubBlockValues, triggerCredentials })
|
||||
}, [subscribedSubBlockValues, triggerCredentials])
|
||||
|
||||
useEffect(() => {
|
||||
if (isTestUrlExpired && storedTestUrl) {
|
||||
@@ -217,69 +129,63 @@ export function TriggerSave({
|
||||
}
|
||||
}, [blockId, isTestUrlExpired, storedTestUrl])
|
||||
|
||||
const handleSave = async () => {
|
||||
if (isPreview || disabled) return
|
||||
const handleSave = useCallback(async () => {
|
||||
const aggregatedConfig = getTriggerConfigAggregation(blockId, effectiveTriggerId)
|
||||
|
||||
setSaveStatus('saving')
|
||||
setErrorMessage(null)
|
||||
|
||||
try {
|
||||
const aggregatedConfig = useTriggerConfigAggregation(blockId, effectiveTriggerId)
|
||||
|
||||
if (aggregatedConfig) {
|
||||
useSubBlockStore.getState().setValue(blockId, 'triggerConfig', aggregatedConfig)
|
||||
logger.debug('Stored aggregated trigger config', {
|
||||
blockId,
|
||||
triggerId: effectiveTriggerId,
|
||||
aggregatedConfig,
|
||||
})
|
||||
}
|
||||
|
||||
const validation = validateRequiredFields(aggregatedConfig)
|
||||
if (!validation.valid) {
|
||||
setErrorMessage(`Missing required fields: ${validation.missingFields.join(', ')}`)
|
||||
setSaveStatus('error')
|
||||
return
|
||||
}
|
||||
|
||||
const success = await saveConfig()
|
||||
if (!success) {
|
||||
throw new Error('Save config returned false')
|
||||
}
|
||||
|
||||
setSaveStatus('saved')
|
||||
setErrorMessage(null)
|
||||
|
||||
const savedWebhookId = useSubBlockStore.getState().getValue(blockId, 'webhookId')
|
||||
const savedTriggerPath = useSubBlockStore.getState().getValue(blockId, 'triggerPath')
|
||||
const savedTriggerId = useSubBlockStore.getState().getValue(blockId, 'triggerId')
|
||||
const savedTriggerConfig = useSubBlockStore.getState().getValue(blockId, 'triggerConfig')
|
||||
|
||||
collaborativeSetSubblockValue(blockId, 'webhookId', savedWebhookId)
|
||||
collaborativeSetSubblockValue(blockId, 'triggerPath', savedTriggerPath)
|
||||
collaborativeSetSubblockValue(blockId, 'triggerId', savedTriggerId)
|
||||
collaborativeSetSubblockValue(blockId, 'triggerConfig', savedTriggerConfig)
|
||||
|
||||
setTimeout(() => {
|
||||
setSaveStatus('idle')
|
||||
}, 2000)
|
||||
|
||||
logger.info('Trigger configuration saved successfully', {
|
||||
blockId,
|
||||
triggerId: effectiveTriggerId,
|
||||
hasWebhookId: !!webhookId,
|
||||
})
|
||||
} catch (error: any) {
|
||||
setSaveStatus('error')
|
||||
setErrorMessage(error.message || 'An error occurred while saving.')
|
||||
logger.error('Error saving trigger configuration', { error })
|
||||
if (aggregatedConfig) {
|
||||
useSubBlockStore.getState().setValue(blockId, 'triggerConfig', aggregatedConfig)
|
||||
}
|
||||
}
|
||||
|
||||
return saveConfig()
|
||||
}, [blockId, effectiveTriggerId, saveConfig])
|
||||
|
||||
const handleSaveSuccess = useCallback(() => {
|
||||
const savedWebhookId = useSubBlockStore.getState().getValue(blockId, 'webhookId')
|
||||
const savedTriggerPath = useSubBlockStore.getState().getValue(blockId, 'triggerPath')
|
||||
const savedTriggerId = useSubBlockStore.getState().getValue(blockId, 'triggerId')
|
||||
const savedTriggerConfig = useSubBlockStore.getState().getValue(blockId, 'triggerConfig')
|
||||
|
||||
collaborativeSetSubblockValue(blockId, 'webhookId', savedWebhookId)
|
||||
collaborativeSetSubblockValue(blockId, 'triggerPath', savedTriggerPath)
|
||||
collaborativeSetSubblockValue(blockId, 'triggerId', savedTriggerId)
|
||||
collaborativeSetSubblockValue(blockId, 'triggerConfig', savedTriggerConfig)
|
||||
}, [blockId, collaborativeSetSubblockValue])
|
||||
|
||||
const {
|
||||
saveStatus,
|
||||
errorMessage,
|
||||
retryCount,
|
||||
maxRetries,
|
||||
triggerSave,
|
||||
onConfigChange,
|
||||
markInitialLoadComplete,
|
||||
} = useAutoSave({
|
||||
disabled: isPreview || disabled || !triggerDef,
|
||||
isExternallySaving: isLoading,
|
||||
validate: validateRequiredFields,
|
||||
onSave: handleSave,
|
||||
onSaveSuccess: handleSaveSuccess,
|
||||
loggerName: 'TriggerSave',
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
onConfigChange(configFingerprint)
|
||||
}, [configFingerprint, onConfigChange])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoading && webhookId) {
|
||||
return markInitialLoadComplete(configFingerprint)
|
||||
}
|
||||
if (!webhookId && !isLoading) {
|
||||
return markInitialLoadComplete(configFingerprint)
|
||||
}
|
||||
}, [isLoading, webhookId, configFingerprint, markInitialLoadComplete])
|
||||
|
||||
const generateTestUrl = async () => {
|
||||
if (!webhookId) return
|
||||
try {
|
||||
setIsGeneratingTestUrl(true)
|
||||
setTestUrlError(null)
|
||||
const res = await fetch(`/api/webhooks/${webhookId}/test-url`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
@@ -296,7 +202,7 @@ export function TriggerSave({
|
||||
collaborativeSetSubblockValue(blockId, 'testUrlExpiresAt', json.expiresAt)
|
||||
} catch (e) {
|
||||
logger.error('Failed to generate test webhook URL', { error: e })
|
||||
setErrorMessage(
|
||||
setTestUrlError(
|
||||
e instanceof Error ? e.message : 'Failed to generate test URL. Please try again.'
|
||||
)
|
||||
} finally {
|
||||
@@ -304,114 +210,49 @@ export function TriggerSave({
|
||||
}
|
||||
}
|
||||
|
||||
const handleDeleteClick = () => {
|
||||
if (isPreview || disabled || !webhookId) return
|
||||
setShowDeleteDialog(true)
|
||||
}
|
||||
|
||||
const handleDeleteConfirm = async () => {
|
||||
setShowDeleteDialog(false)
|
||||
setDeleteStatus('deleting')
|
||||
setErrorMessage(null)
|
||||
|
||||
try {
|
||||
const success = await deleteConfig()
|
||||
|
||||
if (success) {
|
||||
setDeleteStatus('idle')
|
||||
setSaveStatus('idle')
|
||||
setErrorMessage(null)
|
||||
|
||||
useSubBlockStore.getState().setValue(blockId, 'testUrl', null)
|
||||
useSubBlockStore.getState().setValue(blockId, 'testUrlExpiresAt', null)
|
||||
|
||||
collaborativeSetSubblockValue(blockId, 'triggerPath', '')
|
||||
collaborativeSetSubblockValue(blockId, 'webhookId', null)
|
||||
collaborativeSetSubblockValue(blockId, 'triggerConfig', null)
|
||||
collaborativeSetSubblockValue(blockId, 'testUrl', null)
|
||||
collaborativeSetSubblockValue(blockId, 'testUrlExpiresAt', null)
|
||||
|
||||
logger.info('Trigger configuration deleted successfully', {
|
||||
blockId,
|
||||
triggerId: effectiveTriggerId,
|
||||
})
|
||||
} else {
|
||||
setDeleteStatus('idle')
|
||||
setErrorMessage('Failed to delete trigger configuration.')
|
||||
logger.error('Failed to delete trigger configuration')
|
||||
}
|
||||
} catch (error: any) {
|
||||
setDeleteStatus('idle')
|
||||
setErrorMessage(error.message || 'An error occurred while deleting.')
|
||||
logger.error('Error deleting trigger configuration', { error })
|
||||
}
|
||||
}
|
||||
|
||||
if (isPreview) {
|
||||
return null
|
||||
}
|
||||
|
||||
const isProcessing = saveStatus === 'saving' || deleteStatus === 'deleting' || isLoading
|
||||
const isProcessing = saveStatus === 'saving' || isLoading
|
||||
const displayError = errorMessage || testUrlError
|
||||
|
||||
const hasStatusIndicator = isLoading || saveStatus === 'saving' || displayError
|
||||
const hasTestUrlSection =
|
||||
webhookId && hasWebhookUrlDisplay && !isLoading && saveStatus !== 'saving'
|
||||
|
||||
if (!hasStatusIndicator && !hasTestUrlSection) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div id={`${blockId}-${subBlockId}`}>
|
||||
<div className='flex gap-2'>
|
||||
<Button
|
||||
variant='default'
|
||||
onClick={handleSave}
|
||||
disabled={disabled || isProcessing}
|
||||
className={cn(
|
||||
'h-[32px] flex-1 rounded-[8px] px-[12px] transition-all duration-200',
|
||||
saveStatus === 'saved' && 'bg-green-600 hover:bg-green-700',
|
||||
saveStatus === 'error' && 'bg-red-600 hover:bg-red-700'
|
||||
)}
|
||||
>
|
||||
{saveStatus === 'saving' && (
|
||||
<>
|
||||
<div className='mr-2 h-4 w-4 animate-spin rounded-full border-[1.5px] border-current border-t-transparent' />
|
||||
Saving...
|
||||
</>
|
||||
)}
|
||||
{saveStatus === 'saved' && 'Saved'}
|
||||
{saveStatus === 'error' && 'Error'}
|
||||
{saveStatus === 'idle' && (webhookId ? 'Update Configuration' : 'Save Configuration')}
|
||||
</Button>
|
||||
<div id={`${blockId}-${subBlockId}`} className='space-y-2 pb-4'>
|
||||
<SaveStatusIndicator
|
||||
status={saveStatus}
|
||||
errorMessage={displayError}
|
||||
savingText='Saving trigger...'
|
||||
loadingText='Loading trigger...'
|
||||
isLoading={isLoading}
|
||||
onRetry={testUrlError ? () => setTestUrlError(null) : triggerSave}
|
||||
retryDisabled={isProcessing}
|
||||
retryCount={retryCount}
|
||||
maxRetries={maxRetries}
|
||||
/>
|
||||
|
||||
{webhookId && (
|
||||
<Button
|
||||
variant='default'
|
||||
onClick={handleDeleteClick}
|
||||
disabled={disabled || isProcessing}
|
||||
className='h-[32px] rounded-[8px] px-[12px]'
|
||||
>
|
||||
{deleteStatus === 'deleting' ? (
|
||||
<div className='h-4 w-4 animate-spin rounded-full border-[1.5px] border-current border-t-transparent' />
|
||||
) : (
|
||||
<Trash className='h-[14px] w-[14px]' />
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{errorMessage && (
|
||||
<Alert variant='destructive' className='mt-2'>
|
||||
<AlertDescription>{errorMessage}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{webhookId && hasWebhookUrlDisplay && (
|
||||
<div className='mt-2 space-y-1'>
|
||||
{/* Test webhook URL section */}
|
||||
{webhookId && hasWebhookUrlDisplay && !isLoading && saveStatus !== 'saving' && (
|
||||
<div className='space-y-1'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<span className='font-medium text-sm'>Test Webhook URL</span>
|
||||
<Button
|
||||
variant='outline'
|
||||
variant='ghost'
|
||||
onClick={generateTestUrl}
|
||||
disabled={isGeneratingTestUrl || isProcessing}
|
||||
className='h-[32px] rounded-[8px] px-[12px]'
|
||||
className='h-6 px-2 py-1 text-[11px]'
|
||||
>
|
||||
{isGeneratingTestUrl ? (
|
||||
<>
|
||||
<div className='mr-2 h-3 w-3 animate-spin rounded-full border-[1.5px] border-current border-t-transparent' />
|
||||
<div className='mr-1.5 h-2.5 w-2.5 animate-spin rounded-full border-[1.5px] border-current border-t-transparent' />
|
||||
Generating…
|
||||
</>
|
||||
) : testUrl ? (
|
||||
@@ -450,31 +291,6 @@ export function TriggerSave({
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Modal open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
|
||||
<ModalContent size='sm'>
|
||||
<ModalHeader>Delete Trigger</ModalHeader>
|
||||
<ModalBody>
|
||||
<p className='text-[12px] text-[var(--text-tertiary)]'>
|
||||
Are you sure you want to delete this trigger configuration? This will remove the
|
||||
webhook and stop all incoming triggers.{' '}
|
||||
<span className='text-[var(--text-error)]'>This action cannot be undone.</span>
|
||||
</p>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button variant='active' onClick={() => setShowDeleteDialog(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant='primary'
|
||||
onClick={handleDeleteConfirm}
|
||||
className='!bg-[var(--text-error)] !text-white hover:!bg-[var(--text-error)]/90'
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -293,7 +293,6 @@ function SubBlockComponent({
|
||||
setIsValidJson(isValid)
|
||||
}
|
||||
|
||||
// Check if wand is enabled for this sub-block
|
||||
const isWandEnabled = config.wandConfig?.enabled ?? false
|
||||
|
||||
/**
|
||||
@@ -816,6 +815,13 @@ function SubBlockComponent({
|
||||
}
|
||||
}
|
||||
|
||||
// Render without wrapper for components that may return null
|
||||
const noWrapper =
|
||||
config.noWrapper || config.type === 'trigger-save' || config.type === 'schedule-save'
|
||||
if (noWrapper) {
|
||||
return renderInput()
|
||||
}
|
||||
|
||||
return (
|
||||
<div onMouseDown={handleMouseDown} className='flex flex-col gap-[10px]'>
|
||||
{renderLabel(
|
||||
|
||||
@@ -336,6 +336,26 @@ export function Editor() {
|
||||
subBlockState
|
||||
)
|
||||
|
||||
const isNoWrapper =
|
||||
subBlock.noWrapper ||
|
||||
subBlock.type === 'trigger-save' ||
|
||||
subBlock.type === 'schedule-save'
|
||||
|
||||
if (isNoWrapper) {
|
||||
return (
|
||||
<SubBlock
|
||||
key={stableKey}
|
||||
blockId={currentBlockId}
|
||||
config={subBlock}
|
||||
isPreview={false}
|
||||
subBlockValues={subBlockState}
|
||||
disabled={!userPermissions.canEdit}
|
||||
fieldDiffStatus={undefined}
|
||||
allowExpandInPreview={false}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={stableKey}>
|
||||
<SubBlock
|
||||
|
||||
@@ -26,7 +26,7 @@ const SUBFLOW_CONFIG = {
|
||||
},
|
||||
typeKey: 'loopType' as const,
|
||||
storeKey: 'loops' as const,
|
||||
maxIterations: 1000,
|
||||
maxIterations: 100,
|
||||
configKeys: {
|
||||
iterations: 'iterations' as const,
|
||||
items: 'forEachItems' as const,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { parseCronToHumanReadable } from '@/lib/workflows/schedules/utils'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
|
||||
import type { ScheduleInfo } from '../types'
|
||||
|
||||
const logger = createLogger('useScheduleInfo')
|
||||
@@ -32,9 +34,20 @@ export function useScheduleInfo(
|
||||
blockType: string,
|
||||
workflowId: string
|
||||
): UseScheduleInfoReturn {
|
||||
const activeWorkflowId = useWorkflowRegistry((state) => state.activeWorkflowId)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [scheduleInfo, setScheduleInfo] = useState<ScheduleInfo | null>(null)
|
||||
|
||||
const scheduleIdFromStore = useSubBlockStore(
|
||||
useCallback(
|
||||
(state) => {
|
||||
if (!activeWorkflowId) return null
|
||||
return state.workflowValues[activeWorkflowId]?.[blockId]?.scheduleId as string | null
|
||||
},
|
||||
[activeWorkflowId, blockId]
|
||||
)
|
||||
)
|
||||
|
||||
const fetchScheduleInfo = useCallback(
|
||||
async (wfId: string) => {
|
||||
if (!wfId) return
|
||||
@@ -143,22 +156,10 @@ export function useScheduleInfo(
|
||||
setIsLoading(false)
|
||||
}
|
||||
|
||||
const handleScheduleUpdate = (event: CustomEvent) => {
|
||||
if (event.detail?.workflowId === workflowId && event.detail?.blockId === blockId) {
|
||||
logger.debug('Schedule update event received, refetching schedule info')
|
||||
if (blockType === 'schedule') {
|
||||
fetchScheduleInfo(workflowId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('schedule-updated', handleScheduleUpdate as EventListener)
|
||||
|
||||
return () => {
|
||||
setIsLoading(false)
|
||||
window.removeEventListener('schedule-updated', handleScheduleUpdate as EventListener)
|
||||
}
|
||||
}, [blockType, workflowId, blockId, fetchScheduleInfo])
|
||||
}, [blockType, workflowId, blockId, scheduleIdFromStore, fetchScheduleInfo])
|
||||
|
||||
return {
|
||||
scheduleInfo,
|
||||
|
||||
@@ -44,7 +44,11 @@ export function useWebhookInfo(blockId: string, workflowId: string): UseWebhookI
|
||||
useCallback(
|
||||
(state) => {
|
||||
const blockValues = state.workflowValues[activeWorkflowId || '']?.[blockId]
|
||||
return !!(blockValues?.webhookProvider && blockValues?.webhookPath)
|
||||
// Check for webhookId (set by trigger auto-save) or webhookProvider+webhookPath (legacy)
|
||||
return !!(
|
||||
blockValues?.webhookId ||
|
||||
(blockValues?.webhookProvider && blockValues?.webhookPath)
|
||||
)
|
||||
},
|
||||
[activeWorkflowId, blockId]
|
||||
)
|
||||
@@ -72,6 +76,16 @@ export function useWebhookInfo(blockId: string, workflowId: string): UseWebhookI
|
||||
)
|
||||
)
|
||||
|
||||
const webhookIdFromStore = useSubBlockStore(
|
||||
useCallback(
|
||||
(state) => {
|
||||
if (!activeWorkflowId) return null
|
||||
return state.workflowValues[activeWorkflowId]?.[blockId]?.webhookId as string | null
|
||||
},
|
||||
[activeWorkflowId, blockId]
|
||||
)
|
||||
)
|
||||
|
||||
const fetchWebhookStatus = useCallback(async () => {
|
||||
if (!workflowId || !blockId || !isWebhookConfigured) {
|
||||
setWebhookStatus({ isDisabled: false, webhookId: undefined })
|
||||
@@ -114,7 +128,7 @@ export function useWebhookInfo(blockId: string, workflowId: string): UseWebhookI
|
||||
|
||||
useEffect(() => {
|
||||
fetchWebhookStatus()
|
||||
}, [fetchWebhookStatus])
|
||||
}, [fetchWebhookStatus, webhookIdFromStore])
|
||||
|
||||
const reactivateWebhook = useCallback(
|
||||
async (webhookId: string) => {
|
||||
|
||||
@@ -550,9 +550,6 @@ export const WorkflowBlock = memo(function WorkflowBlock({
|
||||
|
||||
const currentStoreBlock = currentWorkflow.getBlockById(id)
|
||||
|
||||
const isStarterBlock = type === 'starter'
|
||||
const isWebhookTriggerBlock = type === 'webhook' || type === 'generic_webhook'
|
||||
|
||||
/**
|
||||
* Subscribe to this block's subblock values to track changes for conditional rendering
|
||||
* of subblocks based on their conditions.
|
||||
@@ -808,7 +805,7 @@ export const WorkflowBlock = memo(function WorkflowBlock({
|
||||
updateNodeInternals(id)
|
||||
}, [horizontalHandles, id, updateNodeInternals])
|
||||
|
||||
const showWebhookIndicator = (isStarterBlock || isWebhookTriggerBlock) && isWebhookConfigured
|
||||
const showWebhookIndicator = displayTriggerMode && isWebhookConfigured
|
||||
const shouldShowScheduleBadge =
|
||||
type === 'schedule' && !isLoadingScheduleInfo && scheduleInfo !== null
|
||||
const userPermissions = useUserPermissionsContext()
|
||||
@@ -909,28 +906,30 @@ export const WorkflowBlock = memo(function WorkflowBlock({
|
||||
)}
|
||||
{!isEnabled && <Badge>disabled</Badge>}
|
||||
|
||||
{type === 'schedule' && shouldShowScheduleBadge && scheduleInfo?.isDisabled && (
|
||||
{type === 'schedule' && shouldShowScheduleBadge && (
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger asChild>
|
||||
<Badge
|
||||
variant='outline'
|
||||
className='cursor-pointer'
|
||||
className={scheduleInfo?.isDisabled ? 'cursor-pointer' : ''}
|
||||
style={{
|
||||
borderColor: 'var(--warning)',
|
||||
color: 'var(--warning)',
|
||||
borderColor: scheduleInfo?.isDisabled ? 'var(--warning)' : 'var(--success)',
|
||||
color: scheduleInfo?.isDisabled ? 'var(--warning)' : 'var(--success)',
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
if (scheduleInfo?.id) {
|
||||
if (scheduleInfo?.isDisabled && scheduleInfo?.id) {
|
||||
reactivateSchedule(scheduleInfo.id)
|
||||
}
|
||||
}}
|
||||
>
|
||||
disabled
|
||||
{scheduleInfo?.isDisabled ? 'disabled' : 'active'}
|
||||
</Badge>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<span className='text-sm'>Click to reactivate</span>
|
||||
{scheduleInfo?.isDisabled
|
||||
? 'Click to reactivate'
|
||||
: scheduleInfo?.scheduleTiming || 'Schedule is active'}
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
)}
|
||||
@@ -940,47 +939,27 @@ export const WorkflowBlock = memo(function WorkflowBlock({
|
||||
<Tooltip.Trigger asChild>
|
||||
<Badge
|
||||
variant='outline'
|
||||
className='bg-[var(--brand-tertiary)] text-[var(--brand-tertiary)]'
|
||||
>
|
||||
<div className='relative flex items-center justify-center'>
|
||||
<div className='197, 94, 0.2)] absolute h-3 w-3 rounded-full bg-[rgba(34,' />
|
||||
<div className='relative h-2 w-2 rounded-full bg-[var(--brand-tertiary)]' />
|
||||
</div>
|
||||
Webhook
|
||||
</Badge>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content side='top' className='max-w-[300px] p-4'>
|
||||
{webhookProvider && webhookPath ? (
|
||||
<>
|
||||
<p className='text-sm'>{getProviderName(webhookProvider)} Webhook</p>
|
||||
<p className='mt-1 text-muted-foreground text-xs'>Path: {webhookPath}</p>
|
||||
</>
|
||||
) : (
|
||||
<p className='text-muted-foreground text-sm'>
|
||||
This workflow is triggered by a webhook.
|
||||
</p>
|
||||
)}
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
)}
|
||||
|
||||
{isWebhookConfigured && isWebhookDisabled && webhookId && (
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger asChild>
|
||||
<Badge
|
||||
variant='outline'
|
||||
className='cursor-pointer'
|
||||
style={{ borderColor: 'var(--warning)', color: 'var(--warning)' }}
|
||||
className={isWebhookDisabled && webhookId ? 'cursor-pointer' : ''}
|
||||
style={{
|
||||
borderColor: isWebhookDisabled ? 'var(--warning)' : 'var(--success)',
|
||||
color: isWebhookDisabled ? 'var(--warning)' : 'var(--success)',
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
reactivateWebhook(webhookId)
|
||||
if (isWebhookDisabled && webhookId) {
|
||||
reactivateWebhook(webhookId)
|
||||
}
|
||||
}}
|
||||
>
|
||||
disabled
|
||||
{isWebhookDisabled ? 'disabled' : 'active'}
|
||||
</Badge>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<span className='text-sm'>Click to reactivate</span>
|
||||
{isWebhookDisabled
|
||||
? 'Click to reactivate'
|
||||
: webhookProvider
|
||||
? `${getProviderName(webhookProvider)} Webhook`
|
||||
: 'Trigger is active'}
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
)}
|
||||
|
||||
@@ -655,7 +655,6 @@ export function useWorkflowExecution() {
|
||||
setExecutor,
|
||||
setPendingBlocks,
|
||||
setActiveBlocks,
|
||||
workflows,
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@@ -215,6 +215,7 @@ export interface SubBlockConfig {
|
||||
connectionDroppable?: boolean
|
||||
hidden?: boolean
|
||||
hideFromPreview?: boolean // Hide this subblock from the workflow block preview
|
||||
noWrapper?: boolean // Render the input directly without wrapper div
|
||||
requiresFeature?: string // Environment variable name that must be truthy for this subblock to be visible
|
||||
description?: string
|
||||
value?: (params: Record<string, any>) => string
|
||||
|
||||
236
apps/sim/hooks/use-auto-save.ts
Normal file
236
apps/sim/hooks/use-auto-save.ts
Normal file
@@ -0,0 +1,236 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
|
||||
const logger = createLogger('useAutoSave')
|
||||
|
||||
/** Auto-save debounce delay in milliseconds */
|
||||
const AUTO_SAVE_DEBOUNCE_MS = 1500
|
||||
|
||||
/** Delay before enabling auto-save after initial load */
|
||||
const INITIAL_LOAD_DELAY_MS = 500
|
||||
|
||||
/** Default maximum retry attempts */
|
||||
const DEFAULT_MAX_RETRIES = 3
|
||||
|
||||
/** Delay before resetting save status to idle after successful save */
|
||||
const SAVED_STATUS_DISPLAY_MS = 2000
|
||||
|
||||
export type SaveStatus = 'idle' | 'saving' | 'saved' | 'error'
|
||||
|
||||
export interface SaveConfigResult {
|
||||
success: boolean
|
||||
}
|
||||
|
||||
interface UseAutoSaveOptions<T extends SaveConfigResult = SaveConfigResult> {
|
||||
/** Whether auto-save is disabled (e.g., in preview mode) */
|
||||
disabled?: boolean
|
||||
/** Whether a save operation is already in progress externally */
|
||||
isExternallySaving?: boolean
|
||||
/** Maximum retry attempts (default: 3) */
|
||||
maxRetries?: number
|
||||
/** Validate config before saving, return true if valid */
|
||||
validate: () => boolean
|
||||
/** Perform the save operation */
|
||||
onSave: () => Promise<T>
|
||||
/** Optional callback after successful save */
|
||||
onSaveSuccess?: (result: T) => void
|
||||
/** Optional callback after failed save */
|
||||
onSaveError?: (error: Error) => void
|
||||
/** Logger name for debugging */
|
||||
loggerName?: string
|
||||
}
|
||||
|
||||
interface UseAutoSaveReturn {
|
||||
/** Current save status */
|
||||
saveStatus: SaveStatus
|
||||
/** Error message if save failed */
|
||||
errorMessage: string | null
|
||||
/** Current retry count */
|
||||
retryCount: number
|
||||
/** Maximum retries allowed */
|
||||
maxRetries: number
|
||||
/** Whether max retries has been reached */
|
||||
maxRetriesReached: boolean
|
||||
/** Trigger an immediate save attempt (for retry button) */
|
||||
triggerSave: () => Promise<void>
|
||||
/** Call this when config changes to trigger debounced save */
|
||||
onConfigChange: (configFingerprint: string) => void
|
||||
/** Call this when initial load completes to enable auto-save */
|
||||
markInitialLoadComplete: (currentFingerprint: string) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared hook for auto-saving configuration with debouncing, retry limits, and status management.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* const { saveStatus, errorMessage, triggerSave, onConfigChange, markInitialLoadComplete } = useAutoSave({
|
||||
* disabled: isPreview,
|
||||
* isExternallySaving: isSaving,
|
||||
* validate: () => validateRequiredFields(),
|
||||
* onSave: async () => saveConfig(),
|
||||
* onSaveSuccess: (result) => { ... },
|
||||
* })
|
||||
*
|
||||
* // When config fingerprint changes
|
||||
* useEffect(() => {
|
||||
* onConfigChange(configFingerprint)
|
||||
* }, [configFingerprint, onConfigChange])
|
||||
*
|
||||
* // When initial data loads
|
||||
* useEffect(() => {
|
||||
* if (!isLoading && dataId) {
|
||||
* markInitialLoadComplete(configFingerprint)
|
||||
* }
|
||||
* }, [isLoading, dataId, configFingerprint, markInitialLoadComplete])
|
||||
* ```
|
||||
*/
|
||||
export function useAutoSave<T extends SaveConfigResult = SaveConfigResult>({
|
||||
disabled = false,
|
||||
isExternallySaving = false,
|
||||
maxRetries = DEFAULT_MAX_RETRIES,
|
||||
validate,
|
||||
onSave,
|
||||
onSaveSuccess,
|
||||
onSaveError,
|
||||
loggerName,
|
||||
}: UseAutoSaveOptions<T>): UseAutoSaveReturn {
|
||||
const [saveStatus, setSaveStatus] = useState<SaveStatus>('idle')
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null)
|
||||
const [retryCount, setRetryCount] = useState(0)
|
||||
|
||||
const autoSaveTimeoutRef = useRef<NodeJS.Timeout | null>(null)
|
||||
const lastSavedConfigRef = useRef<string | null>(null)
|
||||
const isInitialLoadRef = useRef(true)
|
||||
const currentFingerprintRef = useRef<string | null>(null)
|
||||
|
||||
// Clear any pending timeout on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (autoSaveTimeoutRef.current) {
|
||||
clearTimeout(autoSaveTimeoutRef.current)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
const performSave = useCallback(async () => {
|
||||
if (disabled || isExternallySaving) return
|
||||
|
||||
// Final validation check before saving
|
||||
if (!validate()) {
|
||||
setSaveStatus('idle')
|
||||
return
|
||||
}
|
||||
|
||||
setSaveStatus('saving')
|
||||
setErrorMessage(null)
|
||||
|
||||
try {
|
||||
const result = await onSave()
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error('Save operation returned unsuccessful result')
|
||||
}
|
||||
|
||||
// Update last saved config to current
|
||||
lastSavedConfigRef.current = currentFingerprintRef.current
|
||||
setSaveStatus('saved')
|
||||
setErrorMessage(null)
|
||||
setRetryCount(0) // Reset retry count on success
|
||||
|
||||
if (onSaveSuccess) {
|
||||
onSaveSuccess(result)
|
||||
}
|
||||
|
||||
// Reset to idle after display duration
|
||||
setTimeout(() => {
|
||||
setSaveStatus('idle')
|
||||
}, SAVED_STATUS_DISPLAY_MS)
|
||||
|
||||
if (loggerName) {
|
||||
logger.info(`${loggerName}: Auto-save completed successfully`)
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
setSaveStatus('error')
|
||||
const message = error instanceof Error ? error.message : 'An error occurred while saving.'
|
||||
setErrorMessage(message)
|
||||
setRetryCount((prev) => prev + 1)
|
||||
|
||||
if (onSaveError && error instanceof Error) {
|
||||
onSaveError(error)
|
||||
}
|
||||
|
||||
if (loggerName) {
|
||||
logger.error(`${loggerName}: Auto-save failed`, { error })
|
||||
}
|
||||
}
|
||||
}, [disabled, isExternallySaving, validate, onSave, onSaveSuccess, onSaveError, loggerName])
|
||||
|
||||
const onConfigChange = useCallback(
|
||||
(configFingerprint: string) => {
|
||||
currentFingerprintRef.current = configFingerprint
|
||||
|
||||
if (disabled) return
|
||||
|
||||
// Clear any existing timeout
|
||||
if (autoSaveTimeoutRef.current) {
|
||||
clearTimeout(autoSaveTimeoutRef.current)
|
||||
}
|
||||
|
||||
// Skip if initial load hasn't completed
|
||||
if (isInitialLoadRef.current) return
|
||||
|
||||
// Skip if already saving
|
||||
if (saveStatus === 'saving' || isExternallySaving) return
|
||||
|
||||
// Clear error if validation now passes
|
||||
if (saveStatus === 'error' && validate()) {
|
||||
setErrorMessage(null)
|
||||
setSaveStatus('idle')
|
||||
setRetryCount(0) // Reset retry count when config changes
|
||||
}
|
||||
|
||||
// Skip if config hasn't changed
|
||||
if (configFingerprint === lastSavedConfigRef.current) return
|
||||
|
||||
// Skip if validation fails
|
||||
if (!validate()) return
|
||||
|
||||
// Schedule debounced save
|
||||
autoSaveTimeoutRef.current = setTimeout(() => {
|
||||
if (loggerName) {
|
||||
logger.debug(`${loggerName}: Triggering debounced auto-save`)
|
||||
}
|
||||
performSave()
|
||||
}, AUTO_SAVE_DEBOUNCE_MS)
|
||||
},
|
||||
[disabled, saveStatus, isExternallySaving, validate, performSave, loggerName]
|
||||
)
|
||||
|
||||
const markInitialLoadComplete = useCallback((currentFingerprint: string) => {
|
||||
// Delay before enabling auto-save to prevent immediate trigger
|
||||
const timer = setTimeout(() => {
|
||||
isInitialLoadRef.current = false
|
||||
lastSavedConfigRef.current = currentFingerprint
|
||||
currentFingerprintRef.current = currentFingerprint
|
||||
}, INITIAL_LOAD_DELAY_MS)
|
||||
|
||||
return () => clearTimeout(timer)
|
||||
}, [])
|
||||
|
||||
const triggerSave = useCallback(async () => {
|
||||
// Allow retry even if max retries reached (manual trigger)
|
||||
await performSave()
|
||||
}, [performSave])
|
||||
|
||||
return {
|
||||
saveStatus,
|
||||
errorMessage,
|
||||
retryCount,
|
||||
maxRetries,
|
||||
maxRetriesReached: retryCount >= maxRetries,
|
||||
triggerSave,
|
||||
onConfigChange,
|
||||
markInitialLoadComplete,
|
||||
}
|
||||
}
|
||||
@@ -1540,7 +1540,7 @@ export function useCollaborativeWorkflow() {
|
||||
const config = {
|
||||
id: nodeId,
|
||||
nodes: childNodes,
|
||||
iterations: Math.max(1, Math.min(1000, count)), // Clamp between 1-1000 for loops
|
||||
iterations: Math.max(1, Math.min(100, count)), // Clamp between 1-100 for loops
|
||||
loopType: currentLoopType,
|
||||
forEachItems: currentCollection,
|
||||
}
|
||||
|
||||
@@ -23,15 +23,10 @@ interface ScheduleManagementState {
|
||||
isLoading: boolean
|
||||
isSaving: boolean
|
||||
saveConfig: () => Promise<SaveConfigResult>
|
||||
deleteConfig: () => Promise<boolean>
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to manage schedule lifecycle for schedule blocks
|
||||
* Handles:
|
||||
* - Loading existing schedules from the API
|
||||
* - Saving schedule configurations
|
||||
* - Deleting schedule configurations
|
||||
*/
|
||||
export function useScheduleManagement({
|
||||
blockId,
|
||||
@@ -45,8 +40,6 @@ export function useScheduleManagement({
|
||||
)
|
||||
|
||||
const isLoading = useSubBlockStore((state) => state.loadingSchedules.has(blockId))
|
||||
const isChecked = useSubBlockStore((state) => state.checkedSchedules.has(blockId))
|
||||
|
||||
const [isSaving, setIsSaving] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
@@ -183,49 +176,10 @@ export function useScheduleManagement({
|
||||
}
|
||||
}
|
||||
|
||||
const deleteConfig = async (): Promise<boolean> => {
|
||||
if (isPreview || !scheduleId) {
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
setIsSaving(true)
|
||||
|
||||
const response = await fetch(`/api/schedules/${scheduleId}`, {
|
||||
method: 'DELETE',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
workspaceId: params.workspaceId as string,
|
||||
}),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
logger.error('Failed to delete schedule')
|
||||
return false
|
||||
}
|
||||
|
||||
useSubBlockStore.getState().setValue(blockId, 'scheduleId', null)
|
||||
useSubBlockStore.setState((state) => {
|
||||
const newSet = new Set(state.checkedSchedules)
|
||||
newSet.delete(blockId)
|
||||
return { checkedSchedules: newSet }
|
||||
})
|
||||
|
||||
logger.info('Schedule deleted successfully')
|
||||
return true
|
||||
} catch (error) {
|
||||
logger.error('Error deleting schedule:', error)
|
||||
return false
|
||||
} finally {
|
||||
setIsSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
scheduleId,
|
||||
isLoading,
|
||||
isSaving,
|
||||
saveConfig,
|
||||
deleteConfig,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useSubBlockStore } from '@/stores/workflows/subblock/store'
|
||||
import { getTrigger, isTriggerValid } from '@/triggers'
|
||||
import { SYSTEM_SUBBLOCK_IDS } from '@/triggers/constants'
|
||||
|
||||
const logger = createLogger('useTriggerConfigAggregation')
|
||||
const logger = createLogger('getTriggerConfigAggregation')
|
||||
|
||||
/**
|
||||
* Maps old trigger config field names to new subblock IDs for backward compatibility.
|
||||
@@ -34,7 +34,7 @@ function mapOldFieldNameToNewSubBlockId(oldFieldName: string): string {
|
||||
* @returns The aggregated config object, or null if no valid config
|
||||
*/
|
||||
|
||||
export function useTriggerConfigAggregation(
|
||||
export function getTriggerConfigAggregation(
|
||||
blockId: string,
|
||||
triggerId: string | undefined
|
||||
): Record<string, any> | null {
|
||||
|
||||
@@ -16,14 +16,18 @@ interface UseWebhookManagementProps {
|
||||
isPreview?: boolean
|
||||
}
|
||||
|
||||
interface SaveConfigResult {
|
||||
success: boolean
|
||||
webhookId?: string
|
||||
}
|
||||
|
||||
interface WebhookManagementState {
|
||||
webhookUrl: string
|
||||
webhookPath: string
|
||||
webhookId: string | null
|
||||
isLoading: boolean
|
||||
isSaving: boolean
|
||||
saveConfig: () => Promise<boolean>
|
||||
deleteConfig: () => Promise<boolean>
|
||||
saveConfig: () => Promise<SaveConfigResult>
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -81,10 +85,6 @@ function resolveEffectiveTriggerId(
|
||||
|
||||
/**
|
||||
* Hook to manage webhook lifecycle for trigger blocks
|
||||
* Handles:
|
||||
* - Pre-generating webhook URLs based on blockId (without creating webhook)
|
||||
* - Loading existing webhooks from the API
|
||||
* - Saving and deleting webhook configurations
|
||||
*/
|
||||
export function useWebhookManagement({
|
||||
blockId,
|
||||
@@ -103,7 +103,6 @@ export function useWebhookManagement({
|
||||
useCallback((state) => state.getValue(blockId, 'triggerPath') as string | null, [blockId])
|
||||
)
|
||||
const isLoading = useSubBlockStore((state) => state.loadingWebhooks.has(blockId))
|
||||
const isChecked = useSubBlockStore((state) => state.checkedWebhooks.has(blockId))
|
||||
|
||||
const webhookUrl = useMemo(() => {
|
||||
if (!webhookPath) {
|
||||
@@ -211,9 +210,9 @@ export function useWebhookManagement({
|
||||
const createWebhook = async (
|
||||
effectiveTriggerId: string | undefined,
|
||||
selectedCredentialId: string | null
|
||||
): Promise<boolean> => {
|
||||
): Promise<SaveConfigResult> => {
|
||||
if (!triggerDef || !effectiveTriggerId) {
|
||||
return false
|
||||
return { success: false }
|
||||
}
|
||||
|
||||
const triggerConfig = useSubBlockStore.getState().getValue(blockId, 'triggerConfig')
|
||||
@@ -266,14 +265,14 @@ export function useWebhookManagement({
|
||||
blockId,
|
||||
})
|
||||
|
||||
return true
|
||||
return { success: true, webhookId: savedWebhookId }
|
||||
}
|
||||
|
||||
const updateWebhook = async (
|
||||
webhookIdToUpdate: string,
|
||||
effectiveTriggerId: string | undefined,
|
||||
selectedCredentialId: string | null
|
||||
): Promise<boolean> => {
|
||||
): Promise<SaveConfigResult> => {
|
||||
const triggerConfig = useSubBlockStore.getState().getValue(blockId, 'triggerConfig')
|
||||
|
||||
const response = await fetch(`/api/webhooks/${webhookIdToUpdate}`, {
|
||||
@@ -310,12 +309,12 @@ export function useWebhookManagement({
|
||||
}
|
||||
|
||||
logger.info('Trigger config saved successfully', { blockId, webhookId: webhookIdToUpdate })
|
||||
return true
|
||||
return { success: true, webhookId: webhookIdToUpdate }
|
||||
}
|
||||
|
||||
const saveConfig = async (): Promise<boolean> => {
|
||||
const saveConfig = async (): Promise<SaveConfigResult> => {
|
||||
if (isPreview || !triggerDef) {
|
||||
return false
|
||||
return { success: false }
|
||||
}
|
||||
|
||||
const effectiveTriggerId = resolveEffectiveTriggerId(blockId, triggerId)
|
||||
@@ -339,41 +338,6 @@ export function useWebhookManagement({
|
||||
}
|
||||
}
|
||||
|
||||
const deleteConfig = async (): Promise<boolean> => {
|
||||
if (isPreview || !webhookId) {
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
setIsSaving(true)
|
||||
|
||||
const response = await fetch(`/api/webhooks/${webhookId}`, {
|
||||
method: 'DELETE',
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
logger.error('Failed to delete webhook')
|
||||
return false
|
||||
}
|
||||
|
||||
useSubBlockStore.getState().setValue(blockId, 'triggerPath', '')
|
||||
useSubBlockStore.getState().setValue(blockId, 'webhookId', null)
|
||||
useSubBlockStore.setState((state) => {
|
||||
const newSet = new Set(state.checkedWebhooks)
|
||||
newSet.delete(blockId)
|
||||
return { checkedWebhooks: newSet }
|
||||
})
|
||||
|
||||
logger.info('Webhook deleted successfully')
|
||||
return true
|
||||
} catch (error) {
|
||||
logger.error('Error deleting webhook:', error)
|
||||
return false
|
||||
} finally {
|
||||
setIsSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
webhookUrl,
|
||||
webhookPath: webhookPath || blockId,
|
||||
@@ -381,6 +345,5 @@ export function useWebhookManagement({
|
||||
isLoading,
|
||||
isSaving,
|
||||
saveConfig,
|
||||
deleteConfig,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,8 +23,6 @@ const FILTER_FIELDS = {
|
||||
workflow: 'string',
|
||||
trigger: 'string',
|
||||
execution: 'string',
|
||||
executionId: 'string',
|
||||
workflowId: 'string',
|
||||
id: 'string',
|
||||
cost: 'number',
|
||||
duration: 'number',
|
||||
@@ -217,13 +215,11 @@ export function queryToApiParams(parsedQuery: ParsedQuery): Record<string, strin
|
||||
break
|
||||
|
||||
case 'cost':
|
||||
params.costOperator = filter.operator
|
||||
params.costValue = String(filter.value)
|
||||
params[`cost_${filter.operator}_${filter.value}`] = 'true'
|
||||
break
|
||||
|
||||
case 'duration':
|
||||
params.durationOperator = filter.operator
|
||||
params.durationValue = String(filter.value)
|
||||
params[`duration_${filter.operator}_${filter.value}`] = 'true'
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +38,8 @@ export const FILTER_DEFINITIONS: FilterDefinition[] = [
|
||||
{ value: 'info', label: 'Info', description: 'Info logs only' },
|
||||
],
|
||||
},
|
||||
// Note: Trigger options are now dynamically populated from active logs
|
||||
// Core types are included by default, integration triggers are added from actual log data
|
||||
{
|
||||
key: 'cost',
|
||||
label: 'Cost',
|
||||
@@ -80,6 +82,14 @@ export const FILTER_DEFINITIONS: FilterDefinition[] = [
|
||||
},
|
||||
]
|
||||
|
||||
const CORE_TRIGGERS: TriggerData[] = [
|
||||
{ value: 'api', label: 'API', color: '#3b82f6' },
|
||||
{ value: 'manual', label: 'Manual', color: '#6b7280' },
|
||||
{ value: 'webhook', label: 'Webhook', color: '#f97316' },
|
||||
{ value: 'chat', label: 'Chat', color: '#8b5cf6' },
|
||||
{ value: 'schedule', label: 'Schedule', color: '#10b981' },
|
||||
]
|
||||
|
||||
export class SearchSuggestions {
|
||||
private workflowsData: WorkflowData[]
|
||||
private foldersData: FolderData[]
|
||||
@@ -106,10 +116,10 @@ export class SearchSuggestions {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all triggers from registry data
|
||||
* Get all triggers (core + integrations)
|
||||
*/
|
||||
private getAllTriggers(): TriggerData[] {
|
||||
return this.triggersData
|
||||
return [...CORE_TRIGGERS, ...this.triggersData]
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -118,20 +128,24 @@ export class SearchSuggestions {
|
||||
getSuggestions(input: string): SuggestionGroup | null {
|
||||
const trimmed = input.trim()
|
||||
|
||||
// Empty input → show all filter keys
|
||||
if (!trimmed) {
|
||||
return this.getFilterKeysList()
|
||||
}
|
||||
|
||||
// Input ends with ':' → show values for that key
|
||||
if (trimmed.endsWith(':')) {
|
||||
const key = trimmed.slice(0, -1)
|
||||
return this.getFilterValues(key)
|
||||
}
|
||||
|
||||
// Input contains ':' → filter value context
|
||||
if (trimmed.includes(':')) {
|
||||
const [key, partial] = trimmed.split(':')
|
||||
return this.getFilterValues(key, partial)
|
||||
}
|
||||
|
||||
// Plain text → multi-section results
|
||||
return this.getMultiSectionResults(trimmed)
|
||||
}
|
||||
|
||||
@@ -141,6 +155,7 @@ export class SearchSuggestions {
|
||||
private getFilterKeysList(): SuggestionGroup {
|
||||
const suggestions: Suggestion[] = []
|
||||
|
||||
// Add all filter keys
|
||||
for (const filter of FILTER_DEFINITIONS) {
|
||||
suggestions.push({
|
||||
id: `filter-key-${filter.key}`,
|
||||
@@ -151,6 +166,7 @@ export class SearchSuggestions {
|
||||
})
|
||||
}
|
||||
|
||||
// Add trigger key (always available - core types + integrations)
|
||||
suggestions.push({
|
||||
id: 'filter-key-trigger',
|
||||
value: 'trigger:',
|
||||
@@ -159,6 +175,7 @@ export class SearchSuggestions {
|
||||
category: 'filters',
|
||||
})
|
||||
|
||||
// Add workflow and folder keys
|
||||
if (this.workflowsData.length > 0) {
|
||||
suggestions.push({
|
||||
id: 'filter-key-workflow',
|
||||
@@ -232,10 +249,12 @@ export class SearchSuggestions {
|
||||
: null
|
||||
}
|
||||
|
||||
// Trigger filter values (core + integrations)
|
||||
if (key === 'trigger') {
|
||||
const allTriggers = this.getAllTriggers()
|
||||
const suggestions = allTriggers
|
||||
.filter((t) => !partial || t.label.toLowerCase().includes(partial.toLowerCase()))
|
||||
.slice(0, 15) // Show more since we have core + integrations
|
||||
.map((t) => ({
|
||||
id: `filter-value-trigger-${t.value}`,
|
||||
value: `trigger:${t.value}`,
|
||||
@@ -254,9 +273,11 @@ export class SearchSuggestions {
|
||||
: null
|
||||
}
|
||||
|
||||
// Workflow filter values
|
||||
if (key === 'workflow') {
|
||||
const suggestions = this.workflowsData
|
||||
.filter((w) => !partial || w.name.toLowerCase().includes(partial.toLowerCase()))
|
||||
.slice(0, 8)
|
||||
.map((w) => ({
|
||||
id: `filter-value-workflow-${w.id}`,
|
||||
value: `workflow:"${w.name}"`,
|
||||
@@ -274,9 +295,11 @@ export class SearchSuggestions {
|
||||
: null
|
||||
}
|
||||
|
||||
// Folder filter values
|
||||
if (key === 'folder') {
|
||||
const suggestions = this.foldersData
|
||||
.filter((f) => !partial || f.name.toLowerCase().includes(partial.toLowerCase()))
|
||||
.slice(0, 8)
|
||||
.map((f) => ({
|
||||
id: `filter-value-folder-${f.id}`,
|
||||
value: `folder:"${f.name}"`,
|
||||
@@ -303,6 +326,7 @@ export class SearchSuggestions {
|
||||
const sections: Array<{ title: string; suggestions: Suggestion[] }> = []
|
||||
const allSuggestions: Suggestion[] = []
|
||||
|
||||
// Show all results option
|
||||
const showAllSuggestion: Suggestion = {
|
||||
id: 'show-all',
|
||||
value: query,
|
||||
@@ -311,6 +335,7 @@ export class SearchSuggestions {
|
||||
}
|
||||
allSuggestions.push(showAllSuggestion)
|
||||
|
||||
// Match filter values (e.g., "info" → "Status: Info")
|
||||
const matchingFilterValues = this.getMatchingFilterValues(query)
|
||||
if (matchingFilterValues.length > 0) {
|
||||
sections.push({
|
||||
@@ -320,6 +345,7 @@ export class SearchSuggestions {
|
||||
allSuggestions.push(...matchingFilterValues)
|
||||
}
|
||||
|
||||
// Match triggers
|
||||
const matchingTriggers = this.getMatchingTriggers(query)
|
||||
if (matchingTriggers.length > 0) {
|
||||
sections.push({
|
||||
@@ -329,6 +355,7 @@ export class SearchSuggestions {
|
||||
allSuggestions.push(...matchingTriggers)
|
||||
}
|
||||
|
||||
// Match workflows
|
||||
const matchingWorkflows = this.getMatchingWorkflows(query)
|
||||
if (matchingWorkflows.length > 0) {
|
||||
sections.push({
|
||||
@@ -338,6 +365,7 @@ export class SearchSuggestions {
|
||||
allSuggestions.push(...matchingWorkflows)
|
||||
}
|
||||
|
||||
// Match folders
|
||||
const matchingFolders = this.getMatchingFolders(query)
|
||||
if (matchingFolders.length > 0) {
|
||||
sections.push({
|
||||
@@ -347,6 +375,7 @@ export class SearchSuggestions {
|
||||
allSuggestions.push(...matchingFolders)
|
||||
}
|
||||
|
||||
// Add filter keys if no specific matches
|
||||
if (
|
||||
matchingFilterValues.length === 0 &&
|
||||
matchingTriggers.length === 0 &&
|
||||
|
||||
@@ -18,70 +18,11 @@ import { executeTool } from '@/tools'
|
||||
|
||||
const logger = createLogger('AzureOpenAIProvider')
|
||||
|
||||
/**
|
||||
* Determines if the API version uses the Responses API (2025+) or Chat Completions API
|
||||
*/
|
||||
function useResponsesApi(apiVersion: string): boolean {
|
||||
// 2025-* versions use the Responses API
|
||||
// 2024-* and earlier versions use the Chat Completions API
|
||||
return apiVersion.startsWith('2025-')
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to convert an Azure OpenAI Responses API stream to a standard ReadableStream
|
||||
* and collect completion metrics
|
||||
*/
|
||||
function createReadableStreamFromResponsesApiStream(
|
||||
responsesStream: any,
|
||||
onComplete?: (content: string, usage?: any) => void
|
||||
): ReadableStream {
|
||||
let fullContent = ''
|
||||
let usageData: any = null
|
||||
|
||||
return new ReadableStream({
|
||||
async start(controller) {
|
||||
try {
|
||||
for await (const event of responsesStream) {
|
||||
if (event.usage) {
|
||||
usageData = event.usage
|
||||
}
|
||||
|
||||
if (event.type === 'response.output_text.delta') {
|
||||
const content = event.delta || ''
|
||||
if (content) {
|
||||
fullContent += content
|
||||
controller.enqueue(new TextEncoder().encode(content))
|
||||
}
|
||||
} else if (event.type === 'response.content_part.delta') {
|
||||
const content = event.delta?.text || ''
|
||||
if (content) {
|
||||
fullContent += content
|
||||
controller.enqueue(new TextEncoder().encode(content))
|
||||
}
|
||||
} else if (event.type === 'response.completed' || event.type === 'response.done') {
|
||||
if (event.response?.usage) {
|
||||
usageData = event.response.usage
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (onComplete) {
|
||||
onComplete(fullContent, usageData)
|
||||
}
|
||||
|
||||
controller.close()
|
||||
} catch (error) {
|
||||
controller.error(error)
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to convert an Azure OpenAI stream to a standard ReadableStream
|
||||
* and collect completion metrics
|
||||
*/
|
||||
function createReadableStreamFromChatCompletionsStream(
|
||||
function createReadableStreamFromAzureOpenAIStream(
|
||||
azureOpenAIStream: any,
|
||||
onComplete?: (content: string, usage?: any) => void
|
||||
): ReadableStream {
|
||||
@@ -92,6 +33,7 @@ function createReadableStreamFromChatCompletionsStream(
|
||||
async start(controller) {
|
||||
try {
|
||||
for await (const chunk of azureOpenAIStream) {
|
||||
// Check for usage data in the final chunk
|
||||
if (chunk.usage) {
|
||||
usageData = chunk.usage
|
||||
}
|
||||
@@ -103,6 +45,7 @@ function createReadableStreamFromChatCompletionsStream(
|
||||
}
|
||||
}
|
||||
|
||||
// Once stream is complete, call the completion callback with the final content and usage
|
||||
if (onComplete) {
|
||||
onComplete(fullContent, usageData)
|
||||
}
|
||||
@@ -115,430 +58,6 @@ function createReadableStreamFromChatCompletionsStream(
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a request using the Responses API (for 2025+ API versions)
|
||||
*/
|
||||
async function executeWithResponsesApi(
|
||||
azureOpenAI: AzureOpenAI,
|
||||
request: ProviderRequest,
|
||||
deploymentName: string,
|
||||
providerStartTime: number,
|
||||
providerStartTimeISO: string
|
||||
): Promise<ProviderResponse | StreamingExecution> {
|
||||
const inputMessages: any[] = []
|
||||
|
||||
if (request.context) {
|
||||
inputMessages.push({
|
||||
role: 'user',
|
||||
content: request.context,
|
||||
})
|
||||
}
|
||||
|
||||
if (request.messages) {
|
||||
inputMessages.push(...request.messages)
|
||||
}
|
||||
|
||||
const tools = request.tools?.length
|
||||
? request.tools.map((tool) => ({
|
||||
type: 'function' as const,
|
||||
function: {
|
||||
name: tool.id,
|
||||
description: tool.description,
|
||||
parameters: tool.parameters,
|
||||
},
|
||||
}))
|
||||
: undefined
|
||||
|
||||
const payload: any = {
|
||||
model: deploymentName,
|
||||
input: inputMessages.length > 0 ? inputMessages : request.systemPrompt || '',
|
||||
}
|
||||
|
||||
if (request.systemPrompt) {
|
||||
payload.instructions = request.systemPrompt
|
||||
}
|
||||
|
||||
if (request.temperature !== undefined) payload.temperature = request.temperature
|
||||
if (request.maxTokens !== undefined) payload.max_output_tokens = request.maxTokens
|
||||
|
||||
if (request.reasoningEffort !== undefined) {
|
||||
payload.reasoning = { effort: request.reasoningEffort }
|
||||
}
|
||||
|
||||
if (request.responseFormat) {
|
||||
payload.text = {
|
||||
format: {
|
||||
type: 'json_schema',
|
||||
json_schema: {
|
||||
name: request.responseFormat.name || 'response_schema',
|
||||
schema: request.responseFormat.schema || request.responseFormat,
|
||||
strict: request.responseFormat.strict !== false,
|
||||
},
|
||||
},
|
||||
}
|
||||
logger.info('Added JSON schema text format to Responses API request')
|
||||
}
|
||||
|
||||
if (tools?.length) {
|
||||
payload.tools = tools
|
||||
|
||||
const forcedTools = request.tools?.filter((t) => t.usageControl === 'force') || []
|
||||
if (forcedTools.length > 0) {
|
||||
if (forcedTools.length === 1) {
|
||||
payload.tool_choice = {
|
||||
type: 'function',
|
||||
function: { name: forcedTools[0].id },
|
||||
}
|
||||
} else {
|
||||
payload.tool_choice = 'required'
|
||||
}
|
||||
} else {
|
||||
payload.tool_choice = 'auto'
|
||||
}
|
||||
|
||||
logger.info('Responses API request configuration:', {
|
||||
toolCount: tools.length,
|
||||
model: deploymentName,
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
if (request.stream && (!tools || tools.length === 0)) {
|
||||
logger.info('Using streaming response for Responses API request')
|
||||
|
||||
const streamResponse = await (azureOpenAI as any).responses.create({
|
||||
...payload,
|
||||
stream: true,
|
||||
})
|
||||
|
||||
const tokenUsage = {
|
||||
prompt: 0,
|
||||
completion: 0,
|
||||
total: 0,
|
||||
}
|
||||
|
||||
const streamingResult = {
|
||||
stream: createReadableStreamFromResponsesApiStream(streamResponse, (content, usage) => {
|
||||
streamingResult.execution.output.content = content
|
||||
|
||||
const streamEndTime = Date.now()
|
||||
const streamEndTimeISO = new Date(streamEndTime).toISOString()
|
||||
|
||||
if (streamingResult.execution.output.providerTiming) {
|
||||
streamingResult.execution.output.providerTiming.endTime = streamEndTimeISO
|
||||
streamingResult.execution.output.providerTiming.duration =
|
||||
streamEndTime - providerStartTime
|
||||
|
||||
if (streamingResult.execution.output.providerTiming.timeSegments?.[0]) {
|
||||
streamingResult.execution.output.providerTiming.timeSegments[0].endTime =
|
||||
streamEndTime
|
||||
streamingResult.execution.output.providerTiming.timeSegments[0].duration =
|
||||
streamEndTime - providerStartTime
|
||||
}
|
||||
}
|
||||
|
||||
if (usage) {
|
||||
streamingResult.execution.output.tokens = {
|
||||
prompt: usage.input_tokens || usage.prompt_tokens || 0,
|
||||
completion: usage.output_tokens || usage.completion_tokens || 0,
|
||||
total:
|
||||
(usage.input_tokens || usage.prompt_tokens || 0) +
|
||||
(usage.output_tokens || usage.completion_tokens || 0),
|
||||
}
|
||||
}
|
||||
}),
|
||||
execution: {
|
||||
success: true,
|
||||
output: {
|
||||
content: '',
|
||||
model: request.model,
|
||||
tokens: tokenUsage,
|
||||
toolCalls: undefined,
|
||||
providerTiming: {
|
||||
startTime: providerStartTimeISO,
|
||||
endTime: new Date().toISOString(),
|
||||
duration: Date.now() - providerStartTime,
|
||||
timeSegments: [
|
||||
{
|
||||
type: 'model',
|
||||
name: 'Streaming response',
|
||||
startTime: providerStartTime,
|
||||
endTime: Date.now(),
|
||||
duration: Date.now() - providerStartTime,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
logs: [],
|
||||
metadata: {
|
||||
startTime: providerStartTimeISO,
|
||||
endTime: new Date().toISOString(),
|
||||
duration: Date.now() - providerStartTime,
|
||||
},
|
||||
},
|
||||
} as StreamingExecution
|
||||
|
||||
return streamingResult
|
||||
}
|
||||
|
||||
const initialCallTime = Date.now()
|
||||
let currentResponse = await (azureOpenAI as any).responses.create(payload)
|
||||
const firstResponseTime = Date.now() - initialCallTime
|
||||
|
||||
let content = currentResponse.output_text || ''
|
||||
|
||||
const tokens = {
|
||||
prompt: currentResponse.usage?.input_tokens || 0,
|
||||
completion: currentResponse.usage?.output_tokens || 0,
|
||||
total:
|
||||
(currentResponse.usage?.input_tokens || 0) + (currentResponse.usage?.output_tokens || 0),
|
||||
}
|
||||
|
||||
const toolCalls: any[] = []
|
||||
const toolResults: any[] = []
|
||||
let iterationCount = 0
|
||||
const MAX_ITERATIONS = 10
|
||||
|
||||
let modelTime = firstResponseTime
|
||||
let toolsTime = 0
|
||||
|
||||
const timeSegments: TimeSegment[] = [
|
||||
{
|
||||
type: 'model',
|
||||
name: 'Initial response',
|
||||
startTime: initialCallTime,
|
||||
endTime: initialCallTime + firstResponseTime,
|
||||
duration: firstResponseTime,
|
||||
},
|
||||
]
|
||||
|
||||
while (iterationCount < MAX_ITERATIONS) {
|
||||
const toolCallsInResponse =
|
||||
currentResponse.output?.filter((item: any) => item.type === 'function_call') || []
|
||||
|
||||
if (toolCallsInResponse.length === 0) {
|
||||
break
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`Processing ${toolCallsInResponse.length} tool calls (iteration ${iterationCount + 1}/${MAX_ITERATIONS})`
|
||||
)
|
||||
|
||||
const toolsStartTime = Date.now()
|
||||
|
||||
for (const toolCall of toolCallsInResponse) {
|
||||
try {
|
||||
const toolName = toolCall.name
|
||||
const toolArgs =
|
||||
typeof toolCall.arguments === 'string'
|
||||
? JSON.parse(toolCall.arguments)
|
||||
: toolCall.arguments
|
||||
|
||||
const tool = request.tools?.find((t) => t.id === toolName)
|
||||
if (!tool) continue
|
||||
|
||||
const toolCallStartTime = Date.now()
|
||||
const { toolParams, executionParams } = prepareToolExecution(tool, toolArgs, request)
|
||||
|
||||
const result = await executeTool(toolName, executionParams, true)
|
||||
const toolCallEndTime = Date.now()
|
||||
const toolCallDuration = toolCallEndTime - toolCallStartTime
|
||||
|
||||
timeSegments.push({
|
||||
type: 'tool',
|
||||
name: toolName,
|
||||
startTime: toolCallStartTime,
|
||||
endTime: toolCallEndTime,
|
||||
duration: toolCallDuration,
|
||||
})
|
||||
|
||||
let resultContent: any
|
||||
if (result.success) {
|
||||
toolResults.push(result.output)
|
||||
resultContent = result.output
|
||||
} else {
|
||||
resultContent = {
|
||||
error: true,
|
||||
message: result.error || 'Tool execution failed',
|
||||
tool: toolName,
|
||||
}
|
||||
}
|
||||
|
||||
toolCalls.push({
|
||||
name: toolName,
|
||||
arguments: toolParams,
|
||||
startTime: new Date(toolCallStartTime).toISOString(),
|
||||
endTime: new Date(toolCallEndTime).toISOString(),
|
||||
duration: toolCallDuration,
|
||||
result: resultContent,
|
||||
success: result.success,
|
||||
})
|
||||
|
||||
// Add function call output to input for next request
|
||||
inputMessages.push({
|
||||
type: 'function_call_output',
|
||||
call_id: toolCall.call_id || toolCall.id,
|
||||
output: JSON.stringify(resultContent),
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Error processing tool call:', {
|
||||
error,
|
||||
toolName: toolCall?.name,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const thisToolsTime = Date.now() - toolsStartTime
|
||||
toolsTime += thisToolsTime
|
||||
|
||||
// Make the next request
|
||||
const nextModelStartTime = Date.now()
|
||||
const nextPayload = {
|
||||
...payload,
|
||||
input: inputMessages,
|
||||
tool_choice: 'auto',
|
||||
}
|
||||
|
||||
currentResponse = await (azureOpenAI as any).responses.create(nextPayload)
|
||||
|
||||
const nextModelEndTime = Date.now()
|
||||
const thisModelTime = nextModelEndTime - nextModelStartTime
|
||||
|
||||
timeSegments.push({
|
||||
type: 'model',
|
||||
name: `Model response (iteration ${iterationCount + 1})`,
|
||||
startTime: nextModelStartTime,
|
||||
endTime: nextModelEndTime,
|
||||
duration: thisModelTime,
|
||||
})
|
||||
|
||||
modelTime += thisModelTime
|
||||
|
||||
// Update content
|
||||
if (currentResponse.output_text) {
|
||||
content = currentResponse.output_text
|
||||
}
|
||||
|
||||
// Update token counts
|
||||
if (currentResponse.usage) {
|
||||
tokens.prompt += currentResponse.usage.input_tokens || 0
|
||||
tokens.completion += currentResponse.usage.output_tokens || 0
|
||||
tokens.total = tokens.prompt + tokens.completion
|
||||
}
|
||||
|
||||
iterationCount++
|
||||
}
|
||||
|
||||
// Handle streaming for final response after tool processing
|
||||
if (request.stream) {
|
||||
logger.info('Using streaming for final response after tool processing (Responses API)')
|
||||
|
||||
const streamingPayload = {
|
||||
...payload,
|
||||
input: inputMessages,
|
||||
tool_choice: 'auto',
|
||||
stream: true,
|
||||
}
|
||||
|
||||
const streamResponse = await (azureOpenAI as any).responses.create(streamingPayload)
|
||||
|
||||
const streamingResult = {
|
||||
stream: createReadableStreamFromResponsesApiStream(streamResponse, (content, usage) => {
|
||||
streamingResult.execution.output.content = content
|
||||
|
||||
if (usage) {
|
||||
streamingResult.execution.output.tokens = {
|
||||
prompt: usage.input_tokens || tokens.prompt,
|
||||
completion: usage.output_tokens || tokens.completion,
|
||||
total:
|
||||
(usage.input_tokens || tokens.prompt) + (usage.output_tokens || tokens.completion),
|
||||
}
|
||||
}
|
||||
}),
|
||||
execution: {
|
||||
success: true,
|
||||
output: {
|
||||
content: '',
|
||||
model: request.model,
|
||||
tokens: {
|
||||
prompt: tokens.prompt,
|
||||
completion: tokens.completion,
|
||||
total: tokens.total,
|
||||
},
|
||||
toolCalls:
|
||||
toolCalls.length > 0
|
||||
? {
|
||||
list: toolCalls,
|
||||
count: toolCalls.length,
|
||||
}
|
||||
: undefined,
|
||||
providerTiming: {
|
||||
startTime: providerStartTimeISO,
|
||||
endTime: new Date().toISOString(),
|
||||
duration: Date.now() - providerStartTime,
|
||||
modelTime: modelTime,
|
||||
toolsTime: toolsTime,
|
||||
firstResponseTime: firstResponseTime,
|
||||
iterations: iterationCount + 1,
|
||||
timeSegments: timeSegments,
|
||||
},
|
||||
},
|
||||
logs: [],
|
||||
metadata: {
|
||||
startTime: providerStartTimeISO,
|
||||
endTime: new Date().toISOString(),
|
||||
duration: Date.now() - providerStartTime,
|
||||
},
|
||||
},
|
||||
} as StreamingExecution
|
||||
|
||||
return streamingResult
|
||||
}
|
||||
|
||||
// Calculate overall timing
|
||||
const providerEndTime = Date.now()
|
||||
const providerEndTimeISO = new Date(providerEndTime).toISOString()
|
||||
const totalDuration = providerEndTime - providerStartTime
|
||||
|
||||
return {
|
||||
content,
|
||||
model: request.model,
|
||||
tokens,
|
||||
toolCalls: toolCalls.length > 0 ? toolCalls : undefined,
|
||||
toolResults: toolResults.length > 0 ? toolResults : undefined,
|
||||
timing: {
|
||||
startTime: providerStartTimeISO,
|
||||
endTime: providerEndTimeISO,
|
||||
duration: totalDuration,
|
||||
modelTime: modelTime,
|
||||
toolsTime: toolsTime,
|
||||
firstResponseTime: firstResponseTime,
|
||||
iterations: iterationCount + 1,
|
||||
timeSegments: timeSegments,
|
||||
},
|
||||
}
|
||||
} catch (error) {
|
||||
const providerEndTime = Date.now()
|
||||
const providerEndTimeISO = new Date(providerEndTime).toISOString()
|
||||
const totalDuration = providerEndTime - providerStartTime
|
||||
|
||||
logger.error('Error in Responses API request:', {
|
||||
error,
|
||||
duration: totalDuration,
|
||||
})
|
||||
|
||||
const enhancedError = new Error(error instanceof Error ? error.message : String(error))
|
||||
// @ts-ignore - Adding timing property to the error
|
||||
enhancedError.timing = {
|
||||
startTime: providerStartTimeISO,
|
||||
endTime: providerEndTimeISO,
|
||||
duration: totalDuration,
|
||||
}
|
||||
|
||||
throw enhancedError
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Azure OpenAI provider configuration
|
||||
*/
|
||||
@@ -566,7 +85,8 @@ export const azureOpenAIProvider: ProviderConfig = {
|
||||
// Extract Azure-specific configuration from request or environment
|
||||
// Priority: request parameters > environment variables
|
||||
const azureEndpoint = request.azureEndpoint || env.AZURE_OPENAI_ENDPOINT
|
||||
const azureApiVersion = request.azureApiVersion || env.AZURE_OPENAI_API_VERSION || '2024-10-21'
|
||||
const azureApiVersion =
|
||||
request.azureApiVersion || env.AZURE_OPENAI_API_VERSION || '2024-07-01-preview'
|
||||
|
||||
if (!azureEndpoint) {
|
||||
throw new Error(
|
||||
@@ -581,34 +101,6 @@ export const azureOpenAIProvider: ProviderConfig = {
|
||||
endpoint: azureEndpoint,
|
||||
})
|
||||
|
||||
// Build deployment name - use deployment name instead of model name
|
||||
const deploymentName = (request.model || 'azure/gpt-4o').replace('azure/', '')
|
||||
|
||||
// Start execution timer for the entire provider execution
|
||||
const providerStartTime = Date.now()
|
||||
const providerStartTimeISO = new Date(providerStartTime).toISOString()
|
||||
|
||||
// Check if we should use the Responses API (2025+ versions)
|
||||
if (useResponsesApi(azureApiVersion)) {
|
||||
logger.info('Using Responses API for Azure OpenAI request', {
|
||||
apiVersion: azureApiVersion,
|
||||
model: deploymentName,
|
||||
})
|
||||
return executeWithResponsesApi(
|
||||
azureOpenAI,
|
||||
request,
|
||||
deploymentName,
|
||||
providerStartTime,
|
||||
providerStartTimeISO
|
||||
)
|
||||
}
|
||||
|
||||
// Continue with Chat Completions API for 2024 and earlier versions
|
||||
logger.info('Using Chat Completions API for Azure OpenAI request', {
|
||||
apiVersion: azureApiVersion,
|
||||
model: deploymentName,
|
||||
})
|
||||
|
||||
// Start with an empty array for all messages
|
||||
const allMessages = []
|
||||
|
||||
@@ -645,7 +137,8 @@ export const azureOpenAIProvider: ProviderConfig = {
|
||||
}))
|
||||
: undefined
|
||||
|
||||
// Build the request payload
|
||||
// Build the request payload - use deployment name instead of model name
|
||||
const deploymentName = (request.model || 'azure/gpt-4o').replace('azure/', '')
|
||||
const payload: any = {
|
||||
model: deploymentName, // Azure OpenAI uses deployment name
|
||||
messages: allMessages,
|
||||
@@ -702,16 +195,23 @@ export const azureOpenAIProvider: ProviderConfig = {
|
||||
}
|
||||
}
|
||||
|
||||
// Start execution timer for the entire provider execution
|
||||
const providerStartTime = Date.now()
|
||||
const providerStartTimeISO = new Date(providerStartTime).toISOString()
|
||||
|
||||
try {
|
||||
// Check if we can stream directly (no tools required)
|
||||
if (request.stream && (!tools || tools.length === 0)) {
|
||||
logger.info('Using streaming response for Azure OpenAI request')
|
||||
|
||||
// Create a streaming request with token usage tracking
|
||||
const streamResponse = await azureOpenAI.chat.completions.create({
|
||||
...payload,
|
||||
stream: true,
|
||||
stream_options: { include_usage: true },
|
||||
})
|
||||
|
||||
// Start collecting token usage from the stream
|
||||
const tokenUsage = {
|
||||
prompt: 0,
|
||||
completion: 0,
|
||||
@@ -720,44 +220,47 @@ export const azureOpenAIProvider: ProviderConfig = {
|
||||
|
||||
let _streamContent = ''
|
||||
|
||||
// Create a StreamingExecution response with a callback to update content and tokens
|
||||
const streamingResult = {
|
||||
stream: createReadableStreamFromChatCompletionsStream(
|
||||
streamResponse,
|
||||
(content, usage) => {
|
||||
_streamContent = content
|
||||
streamingResult.execution.output.content = content
|
||||
stream: createReadableStreamFromAzureOpenAIStream(streamResponse, (content, usage) => {
|
||||
// Update the execution data with the final content and token usage
|
||||
_streamContent = content
|
||||
streamingResult.execution.output.content = content
|
||||
|
||||
const streamEndTime = Date.now()
|
||||
const streamEndTimeISO = new Date(streamEndTime).toISOString()
|
||||
// Update the timing information with the actual completion time
|
||||
const streamEndTime = Date.now()
|
||||
const streamEndTimeISO = new Date(streamEndTime).toISOString()
|
||||
|
||||
if (streamingResult.execution.output.providerTiming) {
|
||||
streamingResult.execution.output.providerTiming.endTime = streamEndTimeISO
|
||||
streamingResult.execution.output.providerTiming.duration =
|
||||
if (streamingResult.execution.output.providerTiming) {
|
||||
streamingResult.execution.output.providerTiming.endTime = streamEndTimeISO
|
||||
streamingResult.execution.output.providerTiming.duration =
|
||||
streamEndTime - providerStartTime
|
||||
|
||||
// Update the time segment as well
|
||||
if (streamingResult.execution.output.providerTiming.timeSegments?.[0]) {
|
||||
streamingResult.execution.output.providerTiming.timeSegments[0].endTime =
|
||||
streamEndTime
|
||||
streamingResult.execution.output.providerTiming.timeSegments[0].duration =
|
||||
streamEndTime - providerStartTime
|
||||
|
||||
if (streamingResult.execution.output.providerTiming.timeSegments?.[0]) {
|
||||
streamingResult.execution.output.providerTiming.timeSegments[0].endTime =
|
||||
streamEndTime
|
||||
streamingResult.execution.output.providerTiming.timeSegments[0].duration =
|
||||
streamEndTime - providerStartTime
|
||||
}
|
||||
}
|
||||
|
||||
if (usage) {
|
||||
const newTokens = {
|
||||
prompt: usage.prompt_tokens || tokenUsage.prompt,
|
||||
completion: usage.completion_tokens || tokenUsage.completion,
|
||||
total: usage.total_tokens || tokenUsage.total,
|
||||
}
|
||||
|
||||
streamingResult.execution.output.tokens = newTokens
|
||||
}
|
||||
}
|
||||
),
|
||||
|
||||
// Update token usage if available from the stream
|
||||
if (usage) {
|
||||
const newTokens = {
|
||||
prompt: usage.prompt_tokens || tokenUsage.prompt,
|
||||
completion: usage.completion_tokens || tokenUsage.completion,
|
||||
total: usage.total_tokens || tokenUsage.total,
|
||||
}
|
||||
|
||||
streamingResult.execution.output.tokens = newTokens
|
||||
}
|
||||
// We don't need to estimate tokens here as logger.ts will handle that
|
||||
}),
|
||||
execution: {
|
||||
success: true,
|
||||
output: {
|
||||
content: '',
|
||||
content: '', // Will be filled by the stream completion callback
|
||||
model: request.model,
|
||||
tokens: tokenUsage,
|
||||
toolCalls: undefined,
|
||||
@@ -775,8 +278,9 @@ export const azureOpenAIProvider: ProviderConfig = {
|
||||
},
|
||||
],
|
||||
},
|
||||
// Cost will be calculated in logger
|
||||
},
|
||||
logs: [],
|
||||
logs: [], // No block logs for direct streaming
|
||||
metadata: {
|
||||
startTime: providerStartTimeISO,
|
||||
endTime: new Date().toISOString(),
|
||||
@@ -785,16 +289,21 @@ export const azureOpenAIProvider: ProviderConfig = {
|
||||
},
|
||||
} as StreamingExecution
|
||||
|
||||
// Return the streaming execution object with explicit casting
|
||||
return streamingResult as StreamingExecution
|
||||
}
|
||||
|
||||
// Make the initial API request
|
||||
const initialCallTime = Date.now()
|
||||
|
||||
// Track the original tool_choice for forced tool tracking
|
||||
const originalToolChoice = payload.tool_choice
|
||||
|
||||
// Track forced tools and their usage
|
||||
const forcedTools = preparedTools?.forcedTools || []
|
||||
let usedForcedTools: string[] = []
|
||||
|
||||
// Helper function to check for forced tool usage in responses
|
||||
const checkForForcedToolUsage = (
|
||||
response: any,
|
||||
toolChoice: string | { type: string; function?: { name: string }; name?: string; any?: any }
|
||||
@@ -818,6 +327,7 @@ export const azureOpenAIProvider: ProviderConfig = {
|
||||
const firstResponseTime = Date.now() - initialCallTime
|
||||
|
||||
let content = currentResponse.choices[0]?.message?.content || ''
|
||||
// Collect token information but don't calculate costs - that will be done in logger.ts
|
||||
const tokens = {
|
||||
prompt: currentResponse.usage?.prompt_tokens || 0,
|
||||
completion: currentResponse.usage?.completion_tokens || 0,
|
||||
@@ -827,13 +337,16 @@ export const azureOpenAIProvider: ProviderConfig = {
|
||||
const toolResults = []
|
||||
const currentMessages = [...allMessages]
|
||||
let iterationCount = 0
|
||||
const MAX_ITERATIONS = 10
|
||||
const MAX_ITERATIONS = 10 // Prevent infinite loops
|
||||
|
||||
// Track time spent in model vs tools
|
||||
let modelTime = firstResponseTime
|
||||
let toolsTime = 0
|
||||
|
||||
// Track if a forced tool has been used
|
||||
let hasUsedForcedTool = false
|
||||
|
||||
// Track each model and tool call segment with timestamps
|
||||
const timeSegments: TimeSegment[] = [
|
||||
{
|
||||
type: 'model',
|
||||
@@ -844,9 +357,11 @@ export const azureOpenAIProvider: ProviderConfig = {
|
||||
},
|
||||
]
|
||||
|
||||
// Check if a forced tool was used in the first response
|
||||
checkForForcedToolUsage(currentResponse, originalToolChoice)
|
||||
|
||||
while (iterationCount < MAX_ITERATIONS) {
|
||||
// Check for tool calls
|
||||
const toolCallsInResponse = currentResponse.choices[0]?.message?.tool_calls
|
||||
if (!toolCallsInResponse || toolCallsInResponse.length === 0) {
|
||||
break
|
||||
@@ -856,16 +371,20 @@ export const azureOpenAIProvider: ProviderConfig = {
|
||||
`Processing ${toolCallsInResponse.length} tool calls (iteration ${iterationCount + 1}/${MAX_ITERATIONS})`
|
||||
)
|
||||
|
||||
// Track time for tool calls in this batch
|
||||
const toolsStartTime = Date.now()
|
||||
|
||||
// Process each tool call
|
||||
for (const toolCall of toolCallsInResponse) {
|
||||
try {
|
||||
const toolName = toolCall.function.name
|
||||
const toolArgs = JSON.parse(toolCall.function.arguments)
|
||||
|
||||
// Get the tool from the tools registry
|
||||
const tool = request.tools?.find((t) => t.id === toolName)
|
||||
if (!tool) continue
|
||||
|
||||
// Execute the tool
|
||||
const toolCallStartTime = Date.now()
|
||||
|
||||
const { toolParams, executionParams } = prepareToolExecution(tool, toolArgs, request)
|
||||
@@ -874,6 +393,7 @@ export const azureOpenAIProvider: ProviderConfig = {
|
||||
const toolCallEndTime = Date.now()
|
||||
const toolCallDuration = toolCallEndTime - toolCallStartTime
|
||||
|
||||
// Add to time segments for both success and failure
|
||||
timeSegments.push({
|
||||
type: 'tool',
|
||||
name: toolName,
|
||||
@@ -882,11 +402,13 @@ export const azureOpenAIProvider: ProviderConfig = {
|
||||
duration: toolCallDuration,
|
||||
})
|
||||
|
||||
// Prepare result content for the LLM
|
||||
let resultContent: any
|
||||
if (result.success) {
|
||||
toolResults.push(result.output)
|
||||
resultContent = result.output
|
||||
} else {
|
||||
// Include error information so LLM can respond appropriately
|
||||
resultContent = {
|
||||
error: true,
|
||||
message: result.error || 'Tool execution failed',
|
||||
@@ -904,6 +426,7 @@ export const azureOpenAIProvider: ProviderConfig = {
|
||||
success: result.success,
|
||||
})
|
||||
|
||||
// Add the tool call and result to messages (both success and failure)
|
||||
currentMessages.push({
|
||||
role: 'assistant',
|
||||
content: null,
|
||||
@@ -932,38 +455,48 @@ export const azureOpenAIProvider: ProviderConfig = {
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate tool call time for this iteration
|
||||
const thisToolsTime = Date.now() - toolsStartTime
|
||||
toolsTime += thisToolsTime
|
||||
|
||||
// Make the next request with updated messages
|
||||
const nextPayload = {
|
||||
...payload,
|
||||
messages: currentMessages,
|
||||
}
|
||||
|
||||
// Update tool_choice based on which forced tools have been used
|
||||
if (typeof originalToolChoice === 'object' && hasUsedForcedTool && forcedTools.length > 0) {
|
||||
// If we have remaining forced tools, get the next one to force
|
||||
const remainingTools = forcedTools.filter((tool) => !usedForcedTools.includes(tool))
|
||||
|
||||
if (remainingTools.length > 0) {
|
||||
// Force the next tool
|
||||
nextPayload.tool_choice = {
|
||||
type: 'function',
|
||||
function: { name: remainingTools[0] },
|
||||
}
|
||||
logger.info(`Forcing next tool: ${remainingTools[0]}`)
|
||||
} else {
|
||||
// All forced tools have been used, switch to auto
|
||||
nextPayload.tool_choice = 'auto'
|
||||
logger.info('All forced tools have been used, switching to auto tool_choice')
|
||||
}
|
||||
}
|
||||
|
||||
// Time the next model call
|
||||
const nextModelStartTime = Date.now()
|
||||
|
||||
// Make the next request
|
||||
currentResponse = await azureOpenAI.chat.completions.create(nextPayload)
|
||||
|
||||
// Check if any forced tools were used in this response
|
||||
checkForForcedToolUsage(currentResponse, nextPayload.tool_choice)
|
||||
|
||||
const nextModelEndTime = Date.now()
|
||||
const thisModelTime = nextModelEndTime - nextModelStartTime
|
||||
|
||||
// Add to time segments
|
||||
timeSegments.push({
|
||||
type: 'model',
|
||||
name: `Model response (iteration ${iterationCount + 1})`,
|
||||
@@ -972,12 +505,15 @@ export const azureOpenAIProvider: ProviderConfig = {
|
||||
duration: thisModelTime,
|
||||
})
|
||||
|
||||
// Add to model time
|
||||
modelTime += thisModelTime
|
||||
|
||||
// Update content if we have a text response
|
||||
if (currentResponse.choices[0]?.message?.content) {
|
||||
content = currentResponse.choices[0].message.content
|
||||
}
|
||||
|
||||
// Update token counts
|
||||
if (currentResponse.usage) {
|
||||
tokens.prompt += currentResponse.usage.prompt_tokens || 0
|
||||
tokens.completion += currentResponse.usage.completion_tokens || 0
|
||||
@@ -987,43 +523,46 @@ export const azureOpenAIProvider: ProviderConfig = {
|
||||
iterationCount++
|
||||
}
|
||||
|
||||
// After all tool processing complete, if streaming was requested, use streaming for the final response
|
||||
if (request.stream) {
|
||||
logger.info('Using streaming for final response after tool processing')
|
||||
|
||||
// When streaming after tool calls with forced tools, make sure tool_choice is set to 'auto'
|
||||
// This prevents Azure OpenAI API from trying to force tool usage again in the final streaming response
|
||||
const streamingPayload = {
|
||||
...payload,
|
||||
messages: currentMessages,
|
||||
tool_choice: 'auto',
|
||||
tool_choice: 'auto', // Always use 'auto' for the streaming response after tool calls
|
||||
stream: true,
|
||||
stream_options: { include_usage: true },
|
||||
}
|
||||
|
||||
const streamResponse = await azureOpenAI.chat.completions.create(streamingPayload)
|
||||
|
||||
// Create the StreamingExecution object with all collected data
|
||||
let _streamContent = ''
|
||||
|
||||
const streamingResult = {
|
||||
stream: createReadableStreamFromChatCompletionsStream(
|
||||
streamResponse,
|
||||
(content, usage) => {
|
||||
_streamContent = content
|
||||
streamingResult.execution.output.content = content
|
||||
stream: createReadableStreamFromAzureOpenAIStream(streamResponse, (content, usage) => {
|
||||
// Update the execution data with the final content and token usage
|
||||
_streamContent = content
|
||||
streamingResult.execution.output.content = content
|
||||
|
||||
if (usage) {
|
||||
const newTokens = {
|
||||
prompt: usage.prompt_tokens || tokens.prompt,
|
||||
completion: usage.completion_tokens || tokens.completion,
|
||||
total: usage.total_tokens || tokens.total,
|
||||
}
|
||||
|
||||
streamingResult.execution.output.tokens = newTokens
|
||||
// Update token usage if available from the stream
|
||||
if (usage) {
|
||||
const newTokens = {
|
||||
prompt: usage.prompt_tokens || tokens.prompt,
|
||||
completion: usage.completion_tokens || tokens.completion,
|
||||
total: usage.total_tokens || tokens.total,
|
||||
}
|
||||
|
||||
streamingResult.execution.output.tokens = newTokens
|
||||
}
|
||||
),
|
||||
}),
|
||||
execution: {
|
||||
success: true,
|
||||
output: {
|
||||
content: '',
|
||||
content: '', // Will be filled by the callback
|
||||
model: request.model,
|
||||
tokens: {
|
||||
prompt: tokens.prompt,
|
||||
@@ -1058,9 +597,11 @@ export const azureOpenAIProvider: ProviderConfig = {
|
||||
},
|
||||
} as StreamingExecution
|
||||
|
||||
// Return the streaming execution object with explicit casting
|
||||
return streamingResult as StreamingExecution
|
||||
}
|
||||
|
||||
// Calculate overall timing
|
||||
const providerEndTime = Date.now()
|
||||
const providerEndTimeISO = new Date(providerEndTime).toISOString()
|
||||
const totalDuration = providerEndTime - providerStartTime
|
||||
@@ -1081,8 +622,10 @@ export const azureOpenAIProvider: ProviderConfig = {
|
||||
iterations: iterationCount + 1,
|
||||
timeSegments: timeSegments,
|
||||
},
|
||||
// We're not calculating cost here as it will be handled in logger.ts
|
||||
}
|
||||
} catch (error) {
|
||||
// Include timing information even for errors
|
||||
const providerEndTime = Date.now()
|
||||
const providerEndTimeISO = new Date(providerEndTime).toISOString()
|
||||
const totalDuration = providerEndTime - providerStartTime
|
||||
@@ -1092,6 +635,7 @@ export const azureOpenAIProvider: ProviderConfig = {
|
||||
duration: totalDuration,
|
||||
})
|
||||
|
||||
// Create a new error with timing information
|
||||
const enhancedError = new Error(error instanceof Error ? error.message : String(error))
|
||||
// @ts-ignore - Adding timing property to the error
|
||||
enhancedError.timing = {
|
||||
|
||||
@@ -22,9 +22,10 @@ describe('workflow store', () => {
|
||||
})
|
||||
|
||||
describe('loop management', () => {
|
||||
it.concurrent('should regenerate loops when updateLoopCount is called', () => {
|
||||
it('should regenerate loops when updateLoopCount is called', () => {
|
||||
const { addBlock, updateLoopCount } = useWorkflowStore.getState()
|
||||
|
||||
// Add a loop block
|
||||
addBlock(
|
||||
'loop1',
|
||||
'loop',
|
||||
@@ -37,19 +38,23 @@ describe('workflow store', () => {
|
||||
}
|
||||
)
|
||||
|
||||
// Update loop count
|
||||
updateLoopCount('loop1', 10)
|
||||
|
||||
const state = useWorkflowStore.getState()
|
||||
|
||||
// Check that block data was updated
|
||||
expect(state.blocks.loop1?.data?.count).toBe(10)
|
||||
|
||||
// Check that loops were regenerated
|
||||
expect(state.loops.loop1).toBeDefined()
|
||||
expect(state.loops.loop1.iterations).toBe(10)
|
||||
})
|
||||
|
||||
it.concurrent('should regenerate loops when updateLoopType is called', () => {
|
||||
it('should regenerate loops when updateLoopType is called', () => {
|
||||
const { addBlock, updateLoopType } = useWorkflowStore.getState()
|
||||
|
||||
// Add a loop block
|
||||
addBlock(
|
||||
'loop1',
|
||||
'loop',
|
||||
@@ -62,20 +67,24 @@ describe('workflow store', () => {
|
||||
}
|
||||
)
|
||||
|
||||
// Update loop type
|
||||
updateLoopType('loop1', 'forEach')
|
||||
|
||||
const state = useWorkflowStore.getState()
|
||||
|
||||
// Check that block data was updated
|
||||
expect(state.blocks.loop1?.data?.loopType).toBe('forEach')
|
||||
|
||||
// Check that loops were regenerated with forEach items
|
||||
expect(state.loops.loop1).toBeDefined()
|
||||
expect(state.loops.loop1.loopType).toBe('forEach')
|
||||
expect(state.loops.loop1.forEachItems).toBe('["a", "b", "c"]')
|
||||
expect(state.loops.loop1.forEachItems).toEqual(['a', 'b', 'c'])
|
||||
})
|
||||
|
||||
it.concurrent('should regenerate loops when updateLoopCollection is called', () => {
|
||||
it('should regenerate loops when updateLoopCollection is called', () => {
|
||||
const { addBlock, updateLoopCollection } = useWorkflowStore.getState()
|
||||
|
||||
// Add a forEach loop block
|
||||
addBlock(
|
||||
'loop1',
|
||||
'loop',
|
||||
@@ -87,19 +96,23 @@ describe('workflow store', () => {
|
||||
}
|
||||
)
|
||||
|
||||
// Update loop collection
|
||||
updateLoopCollection('loop1', '["item1", "item2", "item3"]')
|
||||
|
||||
const state = useWorkflowStore.getState()
|
||||
|
||||
// Check that block data was updated
|
||||
expect(state.blocks.loop1?.data?.collection).toBe('["item1", "item2", "item3"]')
|
||||
|
||||
// Check that loops were regenerated with new items
|
||||
expect(state.loops.loop1).toBeDefined()
|
||||
expect(state.loops.loop1.forEachItems).toBe('["item1", "item2", "item3"]')
|
||||
expect(state.loops.loop1.forEachItems).toEqual(['item1', 'item2', 'item3'])
|
||||
})
|
||||
|
||||
it.concurrent('should clamp loop count between 1 and 1000', () => {
|
||||
it('should clamp loop count between 1 and 100', () => {
|
||||
const { addBlock, updateLoopCount } = useWorkflowStore.getState()
|
||||
|
||||
// Add a loop block
|
||||
addBlock(
|
||||
'loop1',
|
||||
'loop',
|
||||
@@ -112,10 +125,12 @@ describe('workflow store', () => {
|
||||
}
|
||||
)
|
||||
|
||||
updateLoopCount('loop1', 1500)
|
||||
// Try to set count above max
|
||||
updateLoopCount('loop1', 150)
|
||||
let state = useWorkflowStore.getState()
|
||||
expect(state.blocks.loop1?.data?.count).toBe(1000)
|
||||
expect(state.blocks.loop1?.data?.count).toBe(100)
|
||||
|
||||
// Try to set count below min
|
||||
updateLoopCount('loop1', 0)
|
||||
state = useWorkflowStore.getState()
|
||||
expect(state.blocks.loop1?.data?.count).toBe(1)
|
||||
@@ -123,9 +138,10 @@ describe('workflow store', () => {
|
||||
})
|
||||
|
||||
describe('parallel management', () => {
|
||||
it.concurrent('should regenerate parallels when updateParallelCount is called', () => {
|
||||
it('should regenerate parallels when updateParallelCount is called', () => {
|
||||
const { addBlock, updateParallelCount } = useWorkflowStore.getState()
|
||||
|
||||
// Add a parallel block
|
||||
addBlock(
|
||||
'parallel1',
|
||||
'parallel',
|
||||
@@ -137,19 +153,23 @@ describe('workflow store', () => {
|
||||
}
|
||||
)
|
||||
|
||||
// Update parallel count
|
||||
updateParallelCount('parallel1', 5)
|
||||
|
||||
const state = useWorkflowStore.getState()
|
||||
|
||||
// Check that block data was updated
|
||||
expect(state.blocks.parallel1?.data?.count).toBe(5)
|
||||
|
||||
// Check that parallels were regenerated
|
||||
expect(state.parallels.parallel1).toBeDefined()
|
||||
expect(state.parallels.parallel1.distribution).toBe('')
|
||||
})
|
||||
|
||||
it.concurrent('should regenerate parallels when updateParallelCollection is called', () => {
|
||||
it('should regenerate parallels when updateParallelCollection is called', () => {
|
||||
const { addBlock, updateParallelCollection } = useWorkflowStore.getState()
|
||||
|
||||
// Add a parallel block
|
||||
addBlock(
|
||||
'parallel1',
|
||||
'parallel',
|
||||
@@ -162,22 +182,27 @@ describe('workflow store', () => {
|
||||
}
|
||||
)
|
||||
|
||||
// Update parallel collection
|
||||
updateParallelCollection('parallel1', '["item1", "item2", "item3"]')
|
||||
|
||||
const state = useWorkflowStore.getState()
|
||||
|
||||
// Check that block data was updated
|
||||
expect(state.blocks.parallel1?.data?.collection).toBe('["item1", "item2", "item3"]')
|
||||
|
||||
// Check that parallels were regenerated
|
||||
expect(state.parallels.parallel1).toBeDefined()
|
||||
expect(state.parallels.parallel1.distribution).toBe('["item1", "item2", "item3"]')
|
||||
|
||||
// Verify that the parallel count matches the collection size
|
||||
const parsedDistribution = JSON.parse(state.parallels.parallel1.distribution as string)
|
||||
expect(parsedDistribution).toHaveLength(3)
|
||||
})
|
||||
|
||||
it.concurrent('should clamp parallel count between 1 and 20', () => {
|
||||
it('should clamp parallel count between 1 and 20', () => {
|
||||
const { addBlock, updateParallelCount } = useWorkflowStore.getState()
|
||||
|
||||
// Add a parallel block
|
||||
addBlock(
|
||||
'parallel1',
|
||||
'parallel',
|
||||
@@ -189,18 +214,21 @@ describe('workflow store', () => {
|
||||
}
|
||||
)
|
||||
|
||||
// Try to set count above max
|
||||
updateParallelCount('parallel1', 100)
|
||||
let state = useWorkflowStore.getState()
|
||||
expect(state.blocks.parallel1?.data?.count).toBe(20)
|
||||
|
||||
// Try to set count below min
|
||||
updateParallelCount('parallel1', 0)
|
||||
state = useWorkflowStore.getState()
|
||||
expect(state.blocks.parallel1?.data?.count).toBe(1)
|
||||
})
|
||||
|
||||
it.concurrent('should regenerate parallels when updateParallelType is called', () => {
|
||||
it('should regenerate parallels when updateParallelType is called', () => {
|
||||
const { addBlock, updateParallelType } = useWorkflowStore.getState()
|
||||
|
||||
// Add a parallel block with default collection type
|
||||
addBlock(
|
||||
'parallel1',
|
||||
'parallel',
|
||||
@@ -213,40 +241,50 @@ describe('workflow store', () => {
|
||||
}
|
||||
)
|
||||
|
||||
// Update parallel type to count
|
||||
updateParallelType('parallel1', 'count')
|
||||
|
||||
const state = useWorkflowStore.getState()
|
||||
|
||||
// Check that block data was updated
|
||||
expect(state.blocks.parallel1?.data?.parallelType).toBe('count')
|
||||
|
||||
// Check that parallels were regenerated with new type
|
||||
expect(state.parallels.parallel1).toBeDefined()
|
||||
expect(state.parallels.parallel1.parallelType).toBe('count')
|
||||
})
|
||||
})
|
||||
|
||||
describe('mode switching', () => {
|
||||
it.concurrent('should toggle advanced mode on a block', () => {
|
||||
it('should toggle advanced mode on a block', () => {
|
||||
const { addBlock, toggleBlockAdvancedMode } = useWorkflowStore.getState()
|
||||
|
||||
// Add an agent block
|
||||
addBlock('agent1', 'agent', 'Test Agent', { x: 0, y: 0 })
|
||||
|
||||
// Initially should be in basic mode (advancedMode: false)
|
||||
let state = useWorkflowStore.getState()
|
||||
expect(state.blocks.agent1?.advancedMode).toBe(false)
|
||||
|
||||
// Toggle to advanced mode
|
||||
toggleBlockAdvancedMode('agent1')
|
||||
state = useWorkflowStore.getState()
|
||||
expect(state.blocks.agent1?.advancedMode).toBe(true)
|
||||
|
||||
// Toggle back to basic mode
|
||||
toggleBlockAdvancedMode('agent1')
|
||||
state = useWorkflowStore.getState()
|
||||
expect(state.blocks.agent1?.advancedMode).toBe(false)
|
||||
})
|
||||
|
||||
it.concurrent('should preserve systemPrompt and userPrompt when switching modes', () => {
|
||||
it('should preserve systemPrompt and userPrompt when switching modes', () => {
|
||||
const { addBlock, toggleBlockAdvancedMode } = useWorkflowStore.getState()
|
||||
const { setState: setSubBlockState } = useSubBlockStore
|
||||
// Set up a mock active workflow
|
||||
useWorkflowRegistry.setState({ activeWorkflowId: 'test-workflow' })
|
||||
// Add an agent block
|
||||
addBlock('agent1', 'agent', 'Test Agent', { x: 0, y: 0 })
|
||||
// Set initial values in basic mode
|
||||
setSubBlockState({
|
||||
workflowValues: {
|
||||
'test-workflow': {
|
||||
@@ -257,7 +295,9 @@ describe('workflow store', () => {
|
||||
},
|
||||
},
|
||||
})
|
||||
// Toggle to advanced mode
|
||||
toggleBlockAdvancedMode('agent1')
|
||||
// Check that prompts are preserved in advanced mode
|
||||
let subBlockState = useSubBlockStore.getState()
|
||||
expect(subBlockState.workflowValues['test-workflow'].agent1.systemPrompt).toBe(
|
||||
'You are a helpful assistant'
|
||||
@@ -265,7 +305,9 @@ describe('workflow store', () => {
|
||||
expect(subBlockState.workflowValues['test-workflow'].agent1.userPrompt).toBe(
|
||||
'Hello, how are you?'
|
||||
)
|
||||
// Toggle back to basic mode
|
||||
toggleBlockAdvancedMode('agent1')
|
||||
// Check that prompts are still preserved
|
||||
subBlockState = useSubBlockStore.getState()
|
||||
expect(subBlockState.workflowValues['test-workflow'].agent1.systemPrompt).toBe(
|
||||
'You are a helpful assistant'
|
||||
@@ -275,16 +317,20 @@ describe('workflow store', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it.concurrent('should preserve memories when switching from advanced to basic mode', () => {
|
||||
it('should preserve memories when switching from advanced to basic mode', () => {
|
||||
const { addBlock, toggleBlockAdvancedMode } = useWorkflowStore.getState()
|
||||
const { setState: setSubBlockState } = useSubBlockStore
|
||||
|
||||
// Set up a mock active workflow
|
||||
useWorkflowRegistry.setState({ activeWorkflowId: 'test-workflow' })
|
||||
|
||||
// Add an agent block in advanced mode
|
||||
addBlock('agent1', 'agent', 'Test Agent', { x: 0, y: 0 })
|
||||
|
||||
// First toggle to advanced mode
|
||||
toggleBlockAdvancedMode('agent1')
|
||||
|
||||
// Set values including memories
|
||||
setSubBlockState({
|
||||
workflowValues: {
|
||||
'test-workflow': {
|
||||
@@ -300,8 +346,10 @@ describe('workflow store', () => {
|
||||
},
|
||||
})
|
||||
|
||||
// Toggle back to basic mode
|
||||
toggleBlockAdvancedMode('agent1')
|
||||
|
||||
// Check that prompts and memories are all preserved
|
||||
const subBlockState = useSubBlockStore.getState()
|
||||
expect(subBlockState.workflowValues['test-workflow'].agent1.systemPrompt).toBe(
|
||||
'You are a helpful assistant'
|
||||
@@ -315,50 +363,52 @@ describe('workflow store', () => {
|
||||
])
|
||||
})
|
||||
|
||||
it.concurrent('should handle mode switching when no subblock values exist', () => {
|
||||
it('should handle mode switching when no subblock values exist', () => {
|
||||
const { addBlock, toggleBlockAdvancedMode } = useWorkflowStore.getState()
|
||||
|
||||
// Set up a mock active workflow
|
||||
useWorkflowRegistry.setState({ activeWorkflowId: 'test-workflow' })
|
||||
|
||||
// Add an agent block
|
||||
addBlock('agent1', 'agent', 'Test Agent', { x: 0, y: 0 })
|
||||
|
||||
// Toggle modes without any subblock values set
|
||||
expect(useWorkflowStore.getState().blocks.agent1?.advancedMode).toBe(false)
|
||||
expect(() => toggleBlockAdvancedMode('agent1')).not.toThrow()
|
||||
|
||||
// Verify the mode changed
|
||||
const state = useWorkflowStore.getState()
|
||||
expect(state.blocks.agent1?.advancedMode).toBe(true)
|
||||
})
|
||||
|
||||
it.concurrent('should not throw when toggling non-existent block', () => {
|
||||
it('should not throw when toggling non-existent block', () => {
|
||||
const { toggleBlockAdvancedMode } = useWorkflowStore.getState()
|
||||
|
||||
// Try to toggle a block that doesn't exist
|
||||
expect(() => toggleBlockAdvancedMode('non-existent')).not.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe('addBlock with blockProperties', () => {
|
||||
it.concurrent(
|
||||
'should create a block with default properties when no blockProperties provided',
|
||||
() => {
|
||||
const { addBlock } = useWorkflowStore.getState()
|
||||
it('should create a block with default properties when no blockProperties provided', () => {
|
||||
const { addBlock } = useWorkflowStore.getState()
|
||||
|
||||
addBlock('agent1', 'agent', 'Test Agent', { x: 100, y: 200 })
|
||||
addBlock('agent1', 'agent', 'Test Agent', { x: 100, y: 200 })
|
||||
|
||||
const state = useWorkflowStore.getState()
|
||||
const block = state.blocks.agent1
|
||||
const state = useWorkflowStore.getState()
|
||||
const block = state.blocks.agent1
|
||||
|
||||
expect(block).toBeDefined()
|
||||
expect(block.id).toBe('agent1')
|
||||
expect(block.type).toBe('agent')
|
||||
expect(block.name).toBe('Test Agent')
|
||||
expect(block.position).toEqual({ x: 100, y: 200 })
|
||||
expect(block.enabled).toBe(true)
|
||||
expect(block.horizontalHandles).toBe(true)
|
||||
expect(block.height).toBe(0)
|
||||
}
|
||||
)
|
||||
expect(block).toBeDefined()
|
||||
expect(block.id).toBe('agent1')
|
||||
expect(block.type).toBe('agent')
|
||||
expect(block.name).toBe('Test Agent')
|
||||
expect(block.position).toEqual({ x: 100, y: 200 })
|
||||
expect(block.enabled).toBe(true)
|
||||
expect(block.horizontalHandles).toBe(true)
|
||||
expect(block.height).toBe(0)
|
||||
})
|
||||
|
||||
it.concurrent('should create a block with custom blockProperties for regular blocks', () => {
|
||||
it('should create a block with custom blockProperties for regular blocks', () => {
|
||||
const { addBlock } = useWorkflowStore.getState()
|
||||
|
||||
addBlock(
|
||||
@@ -474,8 +524,10 @@ describe('workflow store', () => {
|
||||
it('should handle blockProperties with parent relationships', () => {
|
||||
const { addBlock } = useWorkflowStore.getState()
|
||||
|
||||
// First add a parent loop block
|
||||
addBlock('loop1', 'loop', 'Parent Loop', { x: 0, y: 0 })
|
||||
|
||||
// Then add a child block with custom properties
|
||||
addBlock(
|
||||
'agent1',
|
||||
'agent',
|
||||
@@ -519,7 +571,7 @@ describe('workflow store', () => {
|
||||
addBlock('block3', 'trigger', 'Start', { x: 200, y: 0 })
|
||||
})
|
||||
|
||||
it.concurrent('should have test blocks set up correctly', () => {
|
||||
it('should have test blocks set up correctly', () => {
|
||||
const state = useWorkflowStore.getState()
|
||||
|
||||
expect(state.blocks.block1).toBeDefined()
|
||||
@@ -530,7 +582,7 @@ describe('workflow store', () => {
|
||||
expect(state.blocks.block3.name).toBe('Start')
|
||||
})
|
||||
|
||||
it.concurrent('should successfully rename a block when no conflicts exist', () => {
|
||||
it('should successfully rename a block when no conflicts exist', () => {
|
||||
const { updateBlockName } = useWorkflowStore.getState()
|
||||
|
||||
const result = updateBlockName('block1', 'Data Processor')
|
||||
@@ -541,21 +593,18 @@ describe('workflow store', () => {
|
||||
expect(state.blocks.block1.name).toBe('Data Processor')
|
||||
})
|
||||
|
||||
it.concurrent(
|
||||
'should allow renaming a block to a different case/spacing of its current name',
|
||||
() => {
|
||||
const { updateBlockName } = useWorkflowStore.getState()
|
||||
it('should allow renaming a block to a different case/spacing of its current name', () => {
|
||||
const { updateBlockName } = useWorkflowStore.getState()
|
||||
|
||||
const result = updateBlockName('block1', 'column ad')
|
||||
const result = updateBlockName('block1', 'column ad')
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.success).toBe(true)
|
||||
|
||||
const state = useWorkflowStore.getState()
|
||||
expect(state.blocks.block1.name).toBe('column ad')
|
||||
}
|
||||
)
|
||||
const state = useWorkflowStore.getState()
|
||||
expect(state.blocks.block1.name).toBe('column ad')
|
||||
})
|
||||
|
||||
it.concurrent('should prevent renaming when another block has the same normalized name', () => {
|
||||
it('should prevent renaming when another block has the same normalized name', () => {
|
||||
const { updateBlockName } = useWorkflowStore.getState()
|
||||
|
||||
const result = updateBlockName('block2', 'Column AD')
|
||||
@@ -566,35 +615,29 @@ describe('workflow store', () => {
|
||||
expect(state.blocks.block2.name).toBe('Employee Length')
|
||||
})
|
||||
|
||||
it.concurrent(
|
||||
'should prevent renaming when another block has a name that normalizes to the same value',
|
||||
() => {
|
||||
const { updateBlockName } = useWorkflowStore.getState()
|
||||
it('should prevent renaming when another block has a name that normalizes to the same value', () => {
|
||||
const { updateBlockName } = useWorkflowStore.getState()
|
||||
|
||||
const result = updateBlockName('block2', 'columnad')
|
||||
const result = updateBlockName('block2', 'columnad')
|
||||
|
||||
expect(result.success).toBe(false)
|
||||
expect(result.success).toBe(false)
|
||||
|
||||
const state = useWorkflowStore.getState()
|
||||
expect(state.blocks.block2.name).toBe('Employee Length')
|
||||
}
|
||||
)
|
||||
const state = useWorkflowStore.getState()
|
||||
expect(state.blocks.block2.name).toBe('Employee Length')
|
||||
})
|
||||
|
||||
it.concurrent(
|
||||
'should prevent renaming when another block has a similar name with different spacing',
|
||||
() => {
|
||||
const { updateBlockName } = useWorkflowStore.getState()
|
||||
it('should prevent renaming when another block has a similar name with different spacing', () => {
|
||||
const { updateBlockName } = useWorkflowStore.getState()
|
||||
|
||||
const result = updateBlockName('block3', 'employee length')
|
||||
const result = updateBlockName('block3', 'employee length')
|
||||
|
||||
expect(result.success).toBe(false)
|
||||
expect(result.success).toBe(false)
|
||||
|
||||
const state = useWorkflowStore.getState()
|
||||
expect(state.blocks.block3.name).toBe('Start')
|
||||
}
|
||||
)
|
||||
const state = useWorkflowStore.getState()
|
||||
expect(state.blocks.block3.name).toBe('Start')
|
||||
})
|
||||
|
||||
it.concurrent('should handle edge cases with empty or whitespace-only names', () => {
|
||||
it('should handle edge cases with empty or whitespace-only names', () => {
|
||||
const { updateBlockName } = useWorkflowStore.getState()
|
||||
|
||||
const result1 = updateBlockName('block1', '')
|
||||
@@ -608,7 +651,7 @@ describe('workflow store', () => {
|
||||
expect(state.blocks.block2.name).toBe(' ')
|
||||
})
|
||||
|
||||
it.concurrent('should return false when trying to rename a non-existent block', () => {
|
||||
it('should return false when trying to rename a non-existent block', () => {
|
||||
const { updateBlockName } = useWorkflowStore.getState()
|
||||
|
||||
const result = updateBlockName('nonexistent', 'New Name')
|
||||
|
||||
@@ -850,7 +850,7 @@ export const useWorkflowStore = create<WorkflowStore>()(
|
||||
...block,
|
||||
data: {
|
||||
...block.data,
|
||||
count: Math.max(1, Math.min(1000, count)), // Clamp between 1-1000
|
||||
count: Math.max(1, Math.min(100, count)), // Clamp between 1-100
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { BlockState } from '@/stores/workflows/workflow/types'
|
||||
import { convertLoopBlockToLoop } from '@/stores/workflows/workflow/utils'
|
||||
|
||||
describe('convertLoopBlockToLoop', () => {
|
||||
it.concurrent('should keep JSON array string as-is for forEach loops', () => {
|
||||
it.concurrent('should parse JSON array string for forEach loops', () => {
|
||||
const blocks: Record<string, BlockState> = {
|
||||
loop1: {
|
||||
id: 'loop1',
|
||||
@@ -25,11 +25,11 @@ describe('convertLoopBlockToLoop', () => {
|
||||
|
||||
expect(result).toBeDefined()
|
||||
expect(result?.loopType).toBe('forEach')
|
||||
expect(result?.forEachItems).toBe('["item1", "item2", "item3"]')
|
||||
expect(result?.forEachItems).toEqual(['item1', 'item2', 'item3'])
|
||||
expect(result?.iterations).toBe(10)
|
||||
})
|
||||
|
||||
it.concurrent('should keep JSON object string as-is for forEach loops', () => {
|
||||
it.concurrent('should parse JSON object string for forEach loops', () => {
|
||||
const blocks: Record<string, BlockState> = {
|
||||
loop1: {
|
||||
id: 'loop1',
|
||||
@@ -51,7 +51,7 @@ describe('convertLoopBlockToLoop', () => {
|
||||
|
||||
expect(result).toBeDefined()
|
||||
expect(result?.loopType).toBe('forEach')
|
||||
expect(result?.forEachItems).toBe('{"key1": "value1", "key2": "value2"}')
|
||||
expect(result?.forEachItems).toEqual({ key1: 'value1', key2: 'value2' })
|
||||
})
|
||||
|
||||
it.concurrent('should keep string as-is if not valid JSON', () => {
|
||||
@@ -125,6 +125,7 @@ describe('convertLoopBlockToLoop', () => {
|
||||
expect(result).toBeDefined()
|
||||
expect(result?.loopType).toBe('for')
|
||||
expect(result?.iterations).toBe(5)
|
||||
expect(result?.forEachItems).toBe('["should", "not", "matter"]')
|
||||
// For 'for' loops, the collection is still parsed in case it's later changed to forEach
|
||||
expect(result?.forEachItems).toEqual(['should', 'not', 'matter'])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -25,8 +25,28 @@ export function convertLoopBlockToLoop(
|
||||
loopType,
|
||||
}
|
||||
|
||||
loop.forEachItems = loopBlock.data?.collection || ''
|
||||
// Load ALL fields regardless of current loop type
|
||||
// This allows switching between loop types without losing data
|
||||
|
||||
// For for/forEach loops, read from collection (block data) and map to forEachItems (loops store)
|
||||
let forEachItems: any = loopBlock.data?.collection || ''
|
||||
if (typeof forEachItems === 'string' && forEachItems.trim()) {
|
||||
const trimmed = forEachItems.trim()
|
||||
// Try to parse if it looks like JSON
|
||||
if (trimmed.startsWith('[') || trimmed.startsWith('{')) {
|
||||
try {
|
||||
forEachItems = JSON.parse(trimmed)
|
||||
} catch {
|
||||
// Keep as string if parsing fails - will be evaluated at runtime
|
||||
}
|
||||
}
|
||||
}
|
||||
loop.forEachItems = forEachItems
|
||||
|
||||
// For while loops, use whileCondition
|
||||
loop.whileCondition = loopBlock.data?.whileCondition || ''
|
||||
|
||||
// For do-while loops, use doWhileCondition
|
||||
loop.doWhileCondition = loopBlock.data?.doWhileCondition || ''
|
||||
|
||||
return loop
|
||||
@@ -46,13 +66,16 @@ export function convertParallelBlockToParallel(
|
||||
const parallelBlock = blocks[parallelBlockId]
|
||||
if (!parallelBlock || parallelBlock.type !== 'parallel') return undefined
|
||||
|
||||
// Get the parallel type from block data, defaulting to 'count' for consistency
|
||||
const parallelType = parallelBlock.data?.parallelType || 'count'
|
||||
|
||||
// Validate parallelType against allowed values
|
||||
const validParallelTypes = ['collection', 'count'] as const
|
||||
const validatedParallelType = validParallelTypes.includes(parallelType as any)
|
||||
? parallelType
|
||||
: 'collection'
|
||||
|
||||
// Only set distribution if it's a collection-based parallel
|
||||
const distribution =
|
||||
validatedParallelType === 'collection' ? parallelBlock.data?.collection || '' : ''
|
||||
|
||||
@@ -116,6 +139,7 @@ export function findAllDescendantNodes(
|
||||
export function generateLoopBlocks(blocks: Record<string, BlockState>): Record<string, Loop> {
|
||||
const loops: Record<string, Loop> = {}
|
||||
|
||||
// Find all loop nodes
|
||||
Object.entries(blocks)
|
||||
.filter(([_, block]) => block.type === 'loop')
|
||||
.forEach(([id, block]) => {
|
||||
@@ -139,6 +163,7 @@ export function generateParallelBlocks(
|
||||
): Record<string, Parallel> {
|
||||
const parallels: Record<string, Parallel> = {}
|
||||
|
||||
// Find all parallel nodes
|
||||
Object.entries(blocks)
|
||||
.filter(([_, block]) => block.type === 'parallel')
|
||||
.forEach(([id, block]) => {
|
||||
|
||||
@@ -47,6 +47,14 @@ export const airtableWebhookTrigger: TriggerConfig = {
|
||||
defaultValue: false,
|
||||
mode: 'trigger',
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'airtable_webhook',
|
||||
},
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
@@ -67,14 +75,6 @@ export const airtableWebhookTrigger: TriggerConfig = {
|
||||
.join(''),
|
||||
mode: 'trigger',
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'airtable_webhook',
|
||||
},
|
||||
],
|
||||
|
||||
outputs: {
|
||||
|
||||
@@ -38,6 +38,18 @@ export const calendlyInviteeCanceledTrigger: TriggerConfig = {
|
||||
value: 'calendly_invitee_canceled',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'calendly_invitee_canceled',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'calendly_invitee_canceled',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
@@ -47,7 +59,7 @@ export const calendlyInviteeCanceledTrigger: TriggerConfig = {
|
||||
'<strong>Note:</strong> This trigger requires a paid Calendly subscription (Professional, Teams, or Enterprise plan).',
|
||||
'Get your Personal Access Token from <strong>Settings > Integrations > API & Webhooks</strong> in your Calendly account.',
|
||||
'Use the "Get Current User" operation in a Calendly block to retrieve your Organization URI.',
|
||||
'The webhook will be automatically created in Calendly when you save this trigger.',
|
||||
'The webhook will be automatically created in Calendly once you complete the configuration above.',
|
||||
'This webhook triggers when an invitee cancels an event. The payload includes cancellation details and reason.',
|
||||
]
|
||||
.map(
|
||||
@@ -61,18 +73,6 @@ export const calendlyInviteeCanceledTrigger: TriggerConfig = {
|
||||
value: 'calendly_invitee_canceled',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'calendly_invitee_canceled',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'calendly_invitee_canceled',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
outputs: buildInviteeOutputs(),
|
||||
|
||||
@@ -47,6 +47,18 @@ export const calendlyInviteeCreatedTrigger: TriggerConfig = {
|
||||
value: 'calendly_invitee_created',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'calendly_invitee_created',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'calendly_invitee_created',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
@@ -56,7 +68,7 @@ export const calendlyInviteeCreatedTrigger: TriggerConfig = {
|
||||
'<strong>Note:</strong> This trigger requires a paid Calendly subscription (Professional, Teams, or Enterprise plan).',
|
||||
'Get your Personal Access Token from <strong>Settings > Integrations > API & Webhooks</strong> in your Calendly account.',
|
||||
'Use the "Get Current User" operation in a Calendly block to retrieve your Organization URI.',
|
||||
'The webhook will be automatically created in Calendly when you save this trigger.',
|
||||
'The webhook will be automatically created in Calendly once you complete the configuration above.',
|
||||
'This webhook triggers when an invitee schedules a new event. Rescheduling triggers both cancellation and creation events.',
|
||||
]
|
||||
.map(
|
||||
@@ -70,18 +82,6 @@ export const calendlyInviteeCreatedTrigger: TriggerConfig = {
|
||||
value: 'calendly_invitee_created',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'calendly_invitee_created',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'calendly_invitee_created',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
outputs: buildInviteeOutputs(),
|
||||
|
||||
@@ -38,6 +38,18 @@ export const calendlyRoutingFormSubmittedTrigger: TriggerConfig = {
|
||||
value: 'calendly_routing_form_submitted',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'calendly_routing_form_submitted',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'calendly_routing_form_submitted',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
@@ -47,7 +59,7 @@ export const calendlyRoutingFormSubmittedTrigger: TriggerConfig = {
|
||||
'<strong>Note:</strong> This trigger requires a paid Calendly subscription (Professional, Teams, or Enterprise plan).',
|
||||
'Get your Personal Access Token from <strong>Settings > Integrations > API & Webhooks</strong> in your Calendly account.',
|
||||
'Use the "Get Current User" operation in a Calendly block to retrieve your Organization URI.',
|
||||
'The webhook will be automatically created in Calendly when you save this trigger.',
|
||||
'The webhook will be automatically created in Calendly once you complete the configuration above.',
|
||||
'This webhook triggers when someone submits a routing form, regardless of whether they book an event.',
|
||||
]
|
||||
.map(
|
||||
@@ -61,18 +73,6 @@ export const calendlyRoutingFormSubmittedTrigger: TriggerConfig = {
|
||||
value: 'calendly_routing_form_submitted',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'calendly_routing_form_submitted',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'calendly_routing_form_submitted',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
outputs: buildRoutingFormOutputs(),
|
||||
|
||||
@@ -37,6 +37,18 @@ export const calendlyWebhookTrigger: TriggerConfig = {
|
||||
value: 'calendly_webhook',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'calendly_webhook',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'calendly_webhook',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
@@ -46,7 +58,7 @@ export const calendlyWebhookTrigger: TriggerConfig = {
|
||||
'<strong>Note:</strong> This trigger requires a paid Calendly subscription (Professional, Teams, or Enterprise plan).',
|
||||
'Get your Personal Access Token from <strong>Settings > Integrations > API & Webhooks</strong> in your Calendly account.',
|
||||
'Use the "Get Current User" operation in a Calendly block to retrieve your Organization URI.',
|
||||
'The webhook will be automatically created in Calendly when you save this trigger.',
|
||||
'The webhook will be automatically created in Calendly once you complete the configuration above.',
|
||||
'This webhook subscribes to all Calendly events (invitee created, invitee canceled, and routing form submitted). Use the <code>event</code> field in the payload to determine the event type.',
|
||||
]
|
||||
.map(
|
||||
@@ -60,18 +72,6 @@ export const calendlyWebhookTrigger: TriggerConfig = {
|
||||
value: 'calendly_webhook',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'calendly_webhook',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'calendly_webhook',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
outputs: {
|
||||
|
||||
@@ -56,6 +56,14 @@ export const genericWebhookTrigger: TriggerConfig = {
|
||||
'Define the expected JSON input schema for this webhook (optional). Use type "files" for file uploads.',
|
||||
mode: 'trigger',
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'generic_webhook',
|
||||
},
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
@@ -76,14 +84,6 @@ export const genericWebhookTrigger: TriggerConfig = {
|
||||
.join(''),
|
||||
mode: 'trigger',
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'generic_webhook',
|
||||
},
|
||||
],
|
||||
|
||||
outputs: {},
|
||||
|
||||
@@ -75,6 +75,18 @@ export const githubIssueClosedTrigger: TriggerConfig = {
|
||||
value: 'github_issue_closed',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'github_issue_closed',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'github_issue_closed',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
@@ -101,18 +113,6 @@ export const githubIssueClosedTrigger: TriggerConfig = {
|
||||
value: 'github_issue_closed',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'github_issue_closed',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'github_issue_closed',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
outputs: {
|
||||
|
||||
@@ -75,6 +75,18 @@ export const githubIssueCommentTrigger: TriggerConfig = {
|
||||
value: 'github_issue_comment',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'github_issue_comment',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'github_issue_comment',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
@@ -102,18 +114,6 @@ export const githubIssueCommentTrigger: TriggerConfig = {
|
||||
value: 'github_issue_comment',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'github_issue_comment',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'github_issue_comment',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
outputs: {
|
||||
|
||||
@@ -96,6 +96,18 @@ export const githubIssueOpenedTrigger: TriggerConfig = {
|
||||
value: 'github_issue_opened',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'github_issue_opened',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'github_issue_opened',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
@@ -122,18 +134,6 @@ export const githubIssueOpenedTrigger: TriggerConfig = {
|
||||
value: 'github_issue_opened',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'github_issue_opened',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'github_issue_opened',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
outputs: {
|
||||
|
||||
@@ -76,6 +76,18 @@ export const githubPRClosedTrigger: TriggerConfig = {
|
||||
value: 'github_pr_closed',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'github_pr_closed',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'github_pr_closed',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
@@ -102,18 +114,6 @@ export const githubPRClosedTrigger: TriggerConfig = {
|
||||
value: 'github_pr_closed',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'github_pr_closed',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'github_pr_closed',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
outputs: {
|
||||
|
||||
@@ -75,6 +75,18 @@ export const githubPRCommentTrigger: TriggerConfig = {
|
||||
value: 'github_pr_comment',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'github_pr_comment',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'github_pr_comment',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
@@ -102,18 +114,6 @@ export const githubPRCommentTrigger: TriggerConfig = {
|
||||
value: 'github_pr_comment',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'github_pr_comment',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'github_pr_comment',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
outputs: {
|
||||
|
||||
@@ -75,6 +75,18 @@ export const githubPRMergedTrigger: TriggerConfig = {
|
||||
value: 'github_pr_merged',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'github_pr_merged',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'github_pr_merged',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
@@ -101,18 +113,6 @@ export const githubPRMergedTrigger: TriggerConfig = {
|
||||
value: 'github_pr_merged',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'github_pr_merged',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'github_pr_merged',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
outputs: {
|
||||
|
||||
@@ -75,6 +75,18 @@ export const githubPROpenedTrigger: TriggerConfig = {
|
||||
value: 'github_pr_opened',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'github_pr_opened',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'github_pr_opened',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
@@ -101,18 +113,6 @@ export const githubPROpenedTrigger: TriggerConfig = {
|
||||
value: 'github_pr_opened',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'github_pr_opened',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'github_pr_opened',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
outputs: {
|
||||
|
||||
@@ -76,6 +76,18 @@ export const githubPRReviewedTrigger: TriggerConfig = {
|
||||
value: 'github_pr_reviewed',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'github_pr_reviewed',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'github_pr_reviewed',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
@@ -102,18 +114,6 @@ export const githubPRReviewedTrigger: TriggerConfig = {
|
||||
value: 'github_pr_reviewed',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'github_pr_reviewed',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'github_pr_reviewed',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
outputs: {
|
||||
|
||||
@@ -75,6 +75,18 @@ export const githubPushTrigger: TriggerConfig = {
|
||||
value: 'github_push',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'github_push',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'github_push',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
@@ -101,18 +113,6 @@ export const githubPushTrigger: TriggerConfig = {
|
||||
value: 'github_push',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'github_push',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'github_push',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
outputs: {
|
||||
|
||||
@@ -75,6 +75,18 @@ export const githubReleasePublishedTrigger: TriggerConfig = {
|
||||
value: 'github_release_published',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'github_release_published',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'github_release_published',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
@@ -101,18 +113,6 @@ export const githubReleasePublishedTrigger: TriggerConfig = {
|
||||
value: 'github_release_published',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'github_release_published',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'github_release_published',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
outputs: {
|
||||
|
||||
@@ -72,6 +72,18 @@ export const githubWebhookTrigger: TriggerConfig = {
|
||||
value: 'github_webhook',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'github_webhook',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'github_webhook',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
@@ -98,18 +110,6 @@ export const githubWebhookTrigger: TriggerConfig = {
|
||||
value: 'github_webhook',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'github_webhook',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'github_webhook',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
outputs: {
|
||||
|
||||
@@ -76,6 +76,18 @@ export const githubWorkflowRunTrigger: TriggerConfig = {
|
||||
value: 'github_workflow_run',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'github_workflow_run',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'github_workflow_run',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
@@ -102,18 +114,6 @@ export const githubWorkflowRunTrigger: TriggerConfig = {
|
||||
value: 'github_workflow_run',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'github_workflow_run',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'github_workflow_run',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
outputs: {
|
||||
|
||||
@@ -104,6 +104,14 @@ export const gmailPollingTrigger: TriggerConfig = {
|
||||
required: false,
|
||||
mode: 'trigger',
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'gmail_poller',
|
||||
},
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
@@ -121,14 +129,6 @@ export const gmailPollingTrigger: TriggerConfig = {
|
||||
.join(''),
|
||||
mode: 'trigger',
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'gmail_poller',
|
||||
},
|
||||
],
|
||||
|
||||
outputs: {
|
||||
|
||||
@@ -59,6 +59,14 @@ export const googleFormsWebhookTrigger: TriggerConfig = {
|
||||
defaultValue: true,
|
||||
mode: 'trigger',
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'google_forms_webhook',
|
||||
},
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
@@ -86,12 +94,12 @@ export const googleFormsWebhookTrigger: TriggerConfig = {
|
||||
const script = `function onFormSubmit(e) {
|
||||
const WEBHOOK_URL = "{{WEBHOOK_URL}}";
|
||||
const SHARED_SECRET = "{{SHARED_SECRET}}";
|
||||
|
||||
|
||||
try {
|
||||
const form = FormApp.getActiveForm();
|
||||
const formResponse = e.response;
|
||||
const itemResponses = formResponse.getItemResponses();
|
||||
|
||||
|
||||
// Build answers object
|
||||
const answers = {};
|
||||
for (var i = 0; i < itemResponses.length; i++) {
|
||||
@@ -100,7 +108,7 @@ export const googleFormsWebhookTrigger: TriggerConfig = {
|
||||
const answer = itemResponse.getResponse();
|
||||
answers[question] = answer;
|
||||
}
|
||||
|
||||
|
||||
// Build payload
|
||||
const payload = {
|
||||
provider: "google_forms",
|
||||
@@ -110,7 +118,7 @@ export const googleFormsWebhookTrigger: TriggerConfig = {
|
||||
lastSubmittedTime: formResponse.getTimestamp().toISOString(),
|
||||
answers: answers
|
||||
};
|
||||
|
||||
|
||||
// Send to webhook
|
||||
const options = {
|
||||
method: "post",
|
||||
@@ -121,9 +129,9 @@ export const googleFormsWebhookTrigger: TriggerConfig = {
|
||||
payload: JSON.stringify(payload),
|
||||
muteHttpExceptions: true
|
||||
};
|
||||
|
||||
|
||||
const response = UrlFetchApp.fetch(WEBHOOK_URL, options);
|
||||
|
||||
|
||||
if (response.getResponseCode() !== 200) {
|
||||
Logger.log("Webhook failed: " + response.getContentText());
|
||||
} else {
|
||||
@@ -145,14 +153,6 @@ export const googleFormsWebhookTrigger: TriggerConfig = {
|
||||
description: 'Copy this code and paste it into your Google Forms Apps Script editor',
|
||||
mode: 'trigger',
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'google_forms_webhook',
|
||||
},
|
||||
],
|
||||
|
||||
outputs: {
|
||||
|
||||
@@ -93,6 +93,17 @@ export const hubspotCompanyCreatedTrigger: TriggerConfig = {
|
||||
value: 'hubspot_company_created',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
mode: 'trigger',
|
||||
triggerId: 'hubspot_company_created',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'hubspot_company_created',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
@@ -156,17 +167,6 @@ export const hubspotCompanyCreatedTrigger: TriggerConfig = {
|
||||
value: 'hubspot_company_created',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
mode: 'trigger',
|
||||
triggerId: 'hubspot_company_created',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'hubspot_company_created',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'samplePayload',
|
||||
title: 'Event Payload Example',
|
||||
|
||||
@@ -93,6 +93,17 @@ export const hubspotCompanyDeletedTrigger: TriggerConfig = {
|
||||
value: 'hubspot_company_deleted',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
mode: 'trigger',
|
||||
triggerId: 'hubspot_company_deleted',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'hubspot_company_deleted',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
@@ -156,17 +167,6 @@ export const hubspotCompanyDeletedTrigger: TriggerConfig = {
|
||||
value: 'hubspot_company_deleted',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
mode: 'trigger',
|
||||
triggerId: 'hubspot_company_deleted',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'hubspot_company_deleted',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'samplePayload',
|
||||
title: 'Event Payload Example',
|
||||
|
||||
@@ -107,6 +107,17 @@ export const hubspotCompanyPropertyChangedTrigger: TriggerConfig = {
|
||||
value: 'hubspot_company_property_changed',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
mode: 'trigger',
|
||||
triggerId: 'hubspot_company_property_changed',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'hubspot_company_property_changed',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
@@ -170,17 +181,6 @@ export const hubspotCompanyPropertyChangedTrigger: TriggerConfig = {
|
||||
value: 'hubspot_company_property_changed',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
mode: 'trigger',
|
||||
triggerId: 'hubspot_company_property_changed',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'hubspot_company_property_changed',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'samplePayload',
|
||||
title: 'Event Payload Example',
|
||||
|
||||
@@ -93,6 +93,17 @@ export const hubspotContactCreatedTrigger: TriggerConfig = {
|
||||
value: 'hubspot_contact_created',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
mode: 'trigger',
|
||||
triggerId: 'hubspot_contact_created',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'hubspot_contact_created',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
@@ -156,17 +167,6 @@ export const hubspotContactCreatedTrigger: TriggerConfig = {
|
||||
value: 'hubspot_contact_created',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
mode: 'trigger',
|
||||
triggerId: 'hubspot_contact_created',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'hubspot_contact_created',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'samplePayload',
|
||||
title: 'Event Payload Example',
|
||||
|
||||
@@ -93,6 +93,17 @@ export const hubspotContactDeletedTrigger: TriggerConfig = {
|
||||
value: 'hubspot_contact_deleted',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
mode: 'trigger',
|
||||
triggerId: 'hubspot_contact_deleted',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'hubspot_contact_deleted',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
@@ -156,17 +167,6 @@ export const hubspotContactDeletedTrigger: TriggerConfig = {
|
||||
value: 'hubspot_contact_deleted',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
mode: 'trigger',
|
||||
triggerId: 'hubspot_contact_deleted',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'hubspot_contact_deleted',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'samplePayload',
|
||||
title: 'Event Payload Example',
|
||||
|
||||
@@ -94,6 +94,17 @@ export const hubspotContactPrivacyDeletedTrigger: TriggerConfig = {
|
||||
value: 'hubspot_contact_privacy_deleted',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
mode: 'trigger',
|
||||
triggerId: 'hubspot_contact_privacy_deleted',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'hubspot_contact_privacy_deleted',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
@@ -157,17 +168,6 @@ export const hubspotContactPrivacyDeletedTrigger: TriggerConfig = {
|
||||
value: 'hubspot_contact_privacy_deleted',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
mode: 'trigger',
|
||||
triggerId: 'hubspot_contact_privacy_deleted',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'hubspot_contact_privacy_deleted',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'samplePayload',
|
||||
title: 'Event Payload Example',
|
||||
|
||||
@@ -107,6 +107,17 @@ export const hubspotContactPropertyChangedTrigger: TriggerConfig = {
|
||||
value: 'hubspot_contact_property_changed',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
mode: 'trigger',
|
||||
triggerId: 'hubspot_contact_property_changed',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'hubspot_contact_property_changed',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
@@ -170,17 +181,6 @@ export const hubspotContactPropertyChangedTrigger: TriggerConfig = {
|
||||
value: 'hubspot_contact_property_changed',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
mode: 'trigger',
|
||||
triggerId: 'hubspot_contact_property_changed',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'hubspot_contact_property_changed',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'samplePayload',
|
||||
title: 'Event Payload Example',
|
||||
|
||||
@@ -93,6 +93,17 @@ export const hubspotConversationCreationTrigger: TriggerConfig = {
|
||||
value: 'hubspot_conversation_creation',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
mode: 'trigger',
|
||||
triggerId: 'hubspot_conversation_creation',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'hubspot_conversation_creation',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
@@ -156,17 +167,6 @@ export const hubspotConversationCreationTrigger: TriggerConfig = {
|
||||
value: 'hubspot_conversation_creation',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
mode: 'trigger',
|
||||
triggerId: 'hubspot_conversation_creation',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'hubspot_conversation_creation',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'samplePayload',
|
||||
title: 'Event Payload Example',
|
||||
|
||||
@@ -93,6 +93,17 @@ export const hubspotConversationDeletionTrigger: TriggerConfig = {
|
||||
value: 'hubspot_conversation_deletion',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
mode: 'trigger',
|
||||
triggerId: 'hubspot_conversation_deletion',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'hubspot_conversation_deletion',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
@@ -156,17 +167,6 @@ export const hubspotConversationDeletionTrigger: TriggerConfig = {
|
||||
value: 'hubspot_conversation_deletion',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
mode: 'trigger',
|
||||
triggerId: 'hubspot_conversation_deletion',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'hubspot_conversation_deletion',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'samplePayload',
|
||||
title: 'Event Payload Example',
|
||||
|
||||
@@ -93,6 +93,17 @@ export const hubspotConversationNewMessageTrigger: TriggerConfig = {
|
||||
value: 'hubspot_conversation_new_message',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
mode: 'trigger',
|
||||
triggerId: 'hubspot_conversation_new_message',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'hubspot_conversation_new_message',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
@@ -156,17 +167,6 @@ export const hubspotConversationNewMessageTrigger: TriggerConfig = {
|
||||
value: 'hubspot_conversation_new_message',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
mode: 'trigger',
|
||||
triggerId: 'hubspot_conversation_new_message',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'hubspot_conversation_new_message',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'samplePayload',
|
||||
title: 'Event Payload Example',
|
||||
|
||||
@@ -94,6 +94,17 @@ export const hubspotConversationPrivacyDeletionTrigger: TriggerConfig = {
|
||||
value: 'hubspot_conversation_privacy_deletion',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
mode: 'trigger',
|
||||
triggerId: 'hubspot_conversation_privacy_deletion',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'hubspot_conversation_privacy_deletion',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
@@ -157,17 +168,6 @@ export const hubspotConversationPrivacyDeletionTrigger: TriggerConfig = {
|
||||
value: 'hubspot_conversation_privacy_deletion',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
mode: 'trigger',
|
||||
triggerId: 'hubspot_conversation_privacy_deletion',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'hubspot_conversation_privacy_deletion',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'samplePayload',
|
||||
title: 'Event Payload Example',
|
||||
|
||||
@@ -107,6 +107,17 @@ export const hubspotConversationPropertyChangedTrigger: TriggerConfig = {
|
||||
value: 'hubspot_conversation_property_changed',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
mode: 'trigger',
|
||||
triggerId: 'hubspot_conversation_property_changed',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'hubspot_conversation_property_changed',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
@@ -170,17 +181,6 @@ export const hubspotConversationPropertyChangedTrigger: TriggerConfig = {
|
||||
value: 'hubspot_conversation_property_changed',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
mode: 'trigger',
|
||||
triggerId: 'hubspot_conversation_property_changed',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'hubspot_conversation_property_changed',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'samplePayload',
|
||||
title: 'Event Payload Example',
|
||||
|
||||
@@ -93,6 +93,17 @@ export const hubspotDealCreatedTrigger: TriggerConfig = {
|
||||
value: 'hubspot_deal_created',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
mode: 'trigger',
|
||||
triggerId: 'hubspot_deal_created',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'hubspot_deal_created',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
@@ -156,17 +167,6 @@ export const hubspotDealCreatedTrigger: TriggerConfig = {
|
||||
value: 'hubspot_deal_created',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
mode: 'trigger',
|
||||
triggerId: 'hubspot_deal_created',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'hubspot_deal_created',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'samplePayload',
|
||||
title: 'Event Payload Example',
|
||||
|
||||
@@ -93,6 +93,17 @@ export const hubspotDealDeletedTrigger: TriggerConfig = {
|
||||
value: 'hubspot_deal_deleted',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
mode: 'trigger',
|
||||
triggerId: 'hubspot_deal_deleted',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'hubspot_deal_deleted',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
@@ -156,17 +167,6 @@ export const hubspotDealDeletedTrigger: TriggerConfig = {
|
||||
value: 'hubspot_deal_deleted',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
mode: 'trigger',
|
||||
triggerId: 'hubspot_deal_deleted',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'hubspot_deal_deleted',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'samplePayload',
|
||||
title: 'Event Payload Example',
|
||||
|
||||
@@ -107,6 +107,17 @@ export const hubspotDealPropertyChangedTrigger: TriggerConfig = {
|
||||
value: 'hubspot_deal_property_changed',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
mode: 'trigger',
|
||||
triggerId: 'hubspot_deal_property_changed',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'hubspot_deal_property_changed',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
@@ -170,17 +181,6 @@ export const hubspotDealPropertyChangedTrigger: TriggerConfig = {
|
||||
value: 'hubspot_deal_property_changed',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
mode: 'trigger',
|
||||
triggerId: 'hubspot_deal_property_changed',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'hubspot_deal_property_changed',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'samplePayload',
|
||||
title: 'Event Payload Example',
|
||||
|
||||
@@ -93,6 +93,17 @@ export const hubspotTicketCreatedTrigger: TriggerConfig = {
|
||||
value: 'hubspot_ticket_created',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
mode: 'trigger',
|
||||
triggerId: 'hubspot_ticket_created',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'hubspot_ticket_created',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
@@ -156,17 +167,6 @@ export const hubspotTicketCreatedTrigger: TriggerConfig = {
|
||||
value: 'hubspot_ticket_created',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
mode: 'trigger',
|
||||
triggerId: 'hubspot_ticket_created',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'hubspot_ticket_created',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'samplePayload',
|
||||
title: 'Event Payload Example',
|
||||
|
||||
@@ -93,6 +93,17 @@ export const hubspotTicketDeletedTrigger: TriggerConfig = {
|
||||
value: 'hubspot_ticket_deleted',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
mode: 'trigger',
|
||||
triggerId: 'hubspot_ticket_deleted',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'hubspot_ticket_deleted',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
@@ -156,17 +167,6 @@ export const hubspotTicketDeletedTrigger: TriggerConfig = {
|
||||
value: 'hubspot_ticket_deleted',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
mode: 'trigger',
|
||||
triggerId: 'hubspot_ticket_deleted',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'hubspot_ticket_deleted',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'samplePayload',
|
||||
title: 'Event Payload Example',
|
||||
|
||||
@@ -107,6 +107,17 @@ export const hubspotTicketPropertyChangedTrigger: TriggerConfig = {
|
||||
value: 'hubspot_ticket_property_changed',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
mode: 'trigger',
|
||||
triggerId: 'hubspot_ticket_property_changed',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'hubspot_ticket_property_changed',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
@@ -170,17 +181,6 @@ export const hubspotTicketPropertyChangedTrigger: TriggerConfig = {
|
||||
value: 'hubspot_ticket_property_changed',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
mode: 'trigger',
|
||||
triggerId: 'hubspot_ticket_property_changed',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'hubspot_ticket_property_changed',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'samplePayload',
|
||||
title: 'Event Payload Example',
|
||||
|
||||
@@ -82,7 +82,7 @@ export function hubspotSetupInstructions(eventType: string, additionalNotes?: st
|
||||
'<strong>Step 3: Configure OAuth Settings</strong><br/>After creating your app via CLI, configure it to add the OAuth Redirect URL: <code>https://www.sim.ai/api/auth/oauth2/callback/hubspot</code>. Then retrieve your <strong>Client ID</strong> and <strong>Client Secret</strong> from your app configuration and enter them in the fields above.',
|
||||
"<strong>Step 4: Get App ID and Developer API Key</strong><br/>In your HubSpot developer account, find your <strong>App ID</strong> (shown below your app name) and your <strong>Developer API Key</strong> (in app settings). You'll need both for the next steps.",
|
||||
'<strong>Step 5: Set Required Scopes</strong><br/>Configure your app to include the required OAuth scope: <code>crm.objects.contacts.read</code>',
|
||||
'<strong>Step 6: Save Configuration in Sim</strong><br/>Click the <strong>"Save Configuration"</strong> button below. This will generate your unique webhook URL.',
|
||||
'<strong>Step 6: Save Configuration in Sim</strong><br/>Your unique webhook URL will be generated automatically once you complete the configuration above.',
|
||||
'<strong>Step 7: Configure Webhook in HubSpot via API</strong><br/>After saving above, copy the <strong>Webhook URL</strong> and run the two curl commands below (replace <code>{YOUR_APP_ID}</code>, <code>{YOUR_DEVELOPER_API_KEY}</code>, and <code>{YOUR_WEBHOOK_URL_FROM_ABOVE}</code> with your actual values).',
|
||||
"<strong>Step 8: Test Your Webhook</strong><br/>Create or modify a contact in HubSpot to trigger the webhook. Check your workflow execution logs in Sim to verify it's working.",
|
||||
]
|
||||
|
||||
@@ -56,18 +56,6 @@ export const jiraIssueCommentedTrigger: TriggerConfig = {
|
||||
value: 'jira_issue_commented',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
hideFromPreview: true,
|
||||
type: 'text',
|
||||
defaultValue: jiraSetupInstructions('comment_created'),
|
||||
mode: 'trigger',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'jira_issue_commented',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
@@ -80,6 +68,18 @@ export const jiraIssueCommentedTrigger: TriggerConfig = {
|
||||
value: 'jira_issue_commented',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
hideFromPreview: true,
|
||||
type: 'text',
|
||||
defaultValue: jiraSetupInstructions('comment_created'),
|
||||
mode: 'trigger',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'jira_issue_commented',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
outputs: buildCommentOutputs(),
|
||||
|
||||
@@ -65,18 +65,6 @@ export const jiraIssueCreatedTrigger: TriggerConfig = {
|
||||
value: 'jira_issue_created',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
hideFromPreview: true,
|
||||
type: 'text',
|
||||
defaultValue: jiraSetupInstructions('jira:issue_created'),
|
||||
mode: 'trigger',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'jira_issue_created',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
@@ -89,6 +77,18 @@ export const jiraIssueCreatedTrigger: TriggerConfig = {
|
||||
value: 'jira_issue_created',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
hideFromPreview: true,
|
||||
type: 'text',
|
||||
defaultValue: jiraSetupInstructions('jira:issue_created'),
|
||||
mode: 'trigger',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'jira_issue_created',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
outputs: buildIssueOutputs(),
|
||||
|
||||
@@ -56,18 +56,6 @@ export const jiraIssueDeletedTrigger: TriggerConfig = {
|
||||
value: 'jira_issue_deleted',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
hideFromPreview: true,
|
||||
type: 'text',
|
||||
defaultValue: jiraSetupInstructions('jira:issue_deleted'),
|
||||
mode: 'trigger',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'jira_issue_deleted',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
@@ -80,6 +68,18 @@ export const jiraIssueDeletedTrigger: TriggerConfig = {
|
||||
value: 'jira_issue_deleted',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
hideFromPreview: true,
|
||||
type: 'text',
|
||||
defaultValue: jiraSetupInstructions('jira:issue_deleted'),
|
||||
mode: 'trigger',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'jira_issue_deleted',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
outputs: buildIssueOutputs(),
|
||||
|
||||
@@ -70,18 +70,6 @@ export const jiraIssueUpdatedTrigger: TriggerConfig = {
|
||||
value: 'jira_issue_updated',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
hideFromPreview: true,
|
||||
type: 'text',
|
||||
defaultValue: jiraSetupInstructions('jira:issue_updated'),
|
||||
mode: 'trigger',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'jira_issue_updated',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
@@ -94,6 +82,18 @@ export const jiraIssueUpdatedTrigger: TriggerConfig = {
|
||||
value: 'jira_issue_updated',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
hideFromPreview: true,
|
||||
type: 'text',
|
||||
defaultValue: jiraSetupInstructions('jira:issue_updated'),
|
||||
mode: 'trigger',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'jira_issue_updated',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
outputs: buildIssueUpdatedOutputs(),
|
||||
|
||||
@@ -43,18 +43,6 @@ export const jiraWebhookTrigger: TriggerConfig = {
|
||||
value: 'jira_webhook',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
hideFromPreview: true,
|
||||
type: 'text',
|
||||
defaultValue: jiraSetupInstructions('All Events'),
|
||||
mode: 'trigger',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'jira_webhook',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
@@ -67,6 +55,18 @@ export const jiraWebhookTrigger: TriggerConfig = {
|
||||
value: 'jira_webhook',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
hideFromPreview: true,
|
||||
type: 'text',
|
||||
defaultValue: jiraSetupInstructions('All Events'),
|
||||
mode: 'trigger',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'jira_webhook',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
outputs: {
|
||||
|
||||
@@ -56,18 +56,6 @@ export const jiraWorklogCreatedTrigger: TriggerConfig = {
|
||||
value: 'jira_worklog_created',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
hideFromPreview: true,
|
||||
type: 'text',
|
||||
defaultValue: jiraSetupInstructions('worklog_created'),
|
||||
mode: 'trigger',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'jira_worklog_created',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
@@ -80,6 +68,18 @@ export const jiraWorklogCreatedTrigger: TriggerConfig = {
|
||||
value: 'jira_worklog_created',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
hideFromPreview: true,
|
||||
type: 'text',
|
||||
defaultValue: jiraSetupInstructions('worklog_created'),
|
||||
mode: 'trigger',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'jira_worklog_created',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
outputs: buildWorklogOutputs(),
|
||||
|
||||
@@ -39,18 +39,6 @@ export const linearCommentCreatedTrigger: TriggerConfig = {
|
||||
value: 'linear_comment_created',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
hideFromPreview: true,
|
||||
type: 'text',
|
||||
defaultValue: linearSetupInstructions('Comment (create)'),
|
||||
mode: 'trigger',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'linear_comment_created',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
@@ -63,6 +51,18 @@ export const linearCommentCreatedTrigger: TriggerConfig = {
|
||||
value: 'linear_comment_created',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
hideFromPreview: true,
|
||||
type: 'text',
|
||||
defaultValue: linearSetupInstructions('Comment (create)'),
|
||||
mode: 'trigger',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'linear_comment_created',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
outputs: buildCommentOutputs(),
|
||||
|
||||
@@ -39,18 +39,6 @@ export const linearCommentUpdatedTrigger: TriggerConfig = {
|
||||
value: 'linear_comment_updated',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
hideFromPreview: true,
|
||||
type: 'text',
|
||||
defaultValue: linearSetupInstructions('Comment (update)'),
|
||||
mode: 'trigger',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'linear_comment_updated',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
@@ -63,6 +51,18 @@ export const linearCommentUpdatedTrigger: TriggerConfig = {
|
||||
value: 'linear_comment_updated',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
hideFromPreview: true,
|
||||
type: 'text',
|
||||
defaultValue: linearSetupInstructions('Comment (update)'),
|
||||
mode: 'trigger',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'linear_comment_updated',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
outputs: buildCommentOutputs(),
|
||||
|
||||
@@ -39,18 +39,6 @@ export const linearCustomerRequestCreatedTrigger: TriggerConfig = {
|
||||
value: 'linear_customer_request_created',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
hideFromPreview: true,
|
||||
type: 'text',
|
||||
defaultValue: linearSetupInstructions('Customer Requests'),
|
||||
mode: 'trigger',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'linear_customer_request_created',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
@@ -63,6 +51,18 @@ export const linearCustomerRequestCreatedTrigger: TriggerConfig = {
|
||||
value: 'linear_customer_request_created',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
hideFromPreview: true,
|
||||
type: 'text',
|
||||
defaultValue: linearSetupInstructions('Customer Requests'),
|
||||
mode: 'trigger',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'linear_customer_request_created',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
outputs: buildCustomerRequestOutputs(),
|
||||
|
||||
@@ -39,18 +39,6 @@ export const linearCustomerRequestUpdatedTrigger: TriggerConfig = {
|
||||
value: 'linear_customer_request_updated',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
hideFromPreview: true,
|
||||
type: 'text',
|
||||
defaultValue: linearSetupInstructions('CustomerNeed (update)'),
|
||||
mode: 'trigger',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'linear_customer_request_updated',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
@@ -63,6 +51,18 @@ export const linearCustomerRequestUpdatedTrigger: TriggerConfig = {
|
||||
value: 'linear_customer_request_updated',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
hideFromPreview: true,
|
||||
type: 'text',
|
||||
defaultValue: linearSetupInstructions('CustomerNeed (update)'),
|
||||
mode: 'trigger',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'linear_customer_request_updated',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
outputs: buildCustomerRequestOutputs(),
|
||||
|
||||
@@ -39,18 +39,6 @@ export const linearCycleCreatedTrigger: TriggerConfig = {
|
||||
value: 'linear_cycle_created',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
hideFromPreview: true,
|
||||
type: 'text',
|
||||
defaultValue: linearSetupInstructions('Cycle (create)'),
|
||||
mode: 'trigger',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'linear_cycle_created',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
@@ -63,6 +51,18 @@ export const linearCycleCreatedTrigger: TriggerConfig = {
|
||||
value: 'linear_cycle_created',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
hideFromPreview: true,
|
||||
type: 'text',
|
||||
defaultValue: linearSetupInstructions('Cycle (create)'),
|
||||
mode: 'trigger',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'linear_cycle_created',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
outputs: buildCycleOutputs(),
|
||||
|
||||
@@ -39,18 +39,6 @@ export const linearCycleUpdatedTrigger: TriggerConfig = {
|
||||
value: 'linear_cycle_updated',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
hideFromPreview: true,
|
||||
type: 'text',
|
||||
defaultValue: linearSetupInstructions('Cycle (update)'),
|
||||
mode: 'trigger',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'linear_cycle_updated',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
@@ -63,6 +51,18 @@ export const linearCycleUpdatedTrigger: TriggerConfig = {
|
||||
value: 'linear_cycle_updated',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
hideFromPreview: true,
|
||||
type: 'text',
|
||||
defaultValue: linearSetupInstructions('Cycle (update)'),
|
||||
mode: 'trigger',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'linear_cycle_updated',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
outputs: buildCycleOutputs(),
|
||||
|
||||
@@ -52,18 +52,6 @@ export const linearIssueCreatedTrigger: TriggerConfig = {
|
||||
value: 'linear_issue_created',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
hideFromPreview: true,
|
||||
type: 'text',
|
||||
defaultValue: linearSetupInstructions('Issue (create)'),
|
||||
mode: 'trigger',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'linear_issue_created',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
@@ -76,6 +64,18 @@ export const linearIssueCreatedTrigger: TriggerConfig = {
|
||||
value: 'linear_issue_created',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
hideFromPreview: true,
|
||||
type: 'text',
|
||||
defaultValue: linearSetupInstructions('Issue (create)'),
|
||||
mode: 'trigger',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'linear_issue_created',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
outputs: buildIssueOutputs(),
|
||||
|
||||
@@ -39,18 +39,6 @@ export const linearIssueRemovedTrigger: TriggerConfig = {
|
||||
value: 'linear_issue_removed',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
hideFromPreview: true,
|
||||
type: 'text',
|
||||
defaultValue: linearSetupInstructions('Issue (remove)'),
|
||||
mode: 'trigger',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'linear_issue_removed',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
@@ -63,6 +51,18 @@ export const linearIssueRemovedTrigger: TriggerConfig = {
|
||||
value: 'linear_issue_removed',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
hideFromPreview: true,
|
||||
type: 'text',
|
||||
defaultValue: linearSetupInstructions('Issue (remove)'),
|
||||
mode: 'trigger',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'linear_issue_removed',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
outputs: buildIssueOutputs(),
|
||||
|
||||
@@ -39,18 +39,6 @@ export const linearIssueUpdatedTrigger: TriggerConfig = {
|
||||
value: 'linear_issue_updated',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
hideFromPreview: true,
|
||||
type: 'text',
|
||||
defaultValue: linearSetupInstructions('Issue (update)'),
|
||||
mode: 'trigger',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'linear_issue_updated',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
@@ -63,6 +51,18 @@ export const linearIssueUpdatedTrigger: TriggerConfig = {
|
||||
value: 'linear_issue_updated',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
hideFromPreview: true,
|
||||
type: 'text',
|
||||
defaultValue: linearSetupInstructions('Issue (update)'),
|
||||
mode: 'trigger',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'linear_issue_updated',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
outputs: buildIssueOutputs(),
|
||||
|
||||
@@ -39,18 +39,6 @@ export const linearLabelCreatedTrigger: TriggerConfig = {
|
||||
value: 'linear_label_created',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
hideFromPreview: true,
|
||||
type: 'text',
|
||||
defaultValue: linearSetupInstructions('IssueLabel (create)'),
|
||||
mode: 'trigger',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'linear_label_created',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
@@ -63,6 +51,18 @@ export const linearLabelCreatedTrigger: TriggerConfig = {
|
||||
value: 'linear_label_created',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
hideFromPreview: true,
|
||||
type: 'text',
|
||||
defaultValue: linearSetupInstructions('IssueLabel (create)'),
|
||||
mode: 'trigger',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'linear_label_created',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
outputs: buildLabelOutputs(),
|
||||
|
||||
@@ -39,18 +39,6 @@ export const linearLabelUpdatedTrigger: TriggerConfig = {
|
||||
value: 'linear_label_updated',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
hideFromPreview: true,
|
||||
type: 'text',
|
||||
defaultValue: linearSetupInstructions('IssueLabel (update)'),
|
||||
mode: 'trigger',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'linear_label_updated',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
@@ -63,6 +51,18 @@ export const linearLabelUpdatedTrigger: TriggerConfig = {
|
||||
value: 'linear_label_updated',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
hideFromPreview: true,
|
||||
type: 'text',
|
||||
defaultValue: linearSetupInstructions('IssueLabel (update)'),
|
||||
mode: 'trigger',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'linear_label_updated',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
outputs: buildLabelOutputs(),
|
||||
|
||||
@@ -39,18 +39,6 @@ export const linearProjectCreatedTrigger: TriggerConfig = {
|
||||
value: 'linear_project_created',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
hideFromPreview: true,
|
||||
type: 'text',
|
||||
defaultValue: linearSetupInstructions('Project (create)'),
|
||||
mode: 'trigger',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'linear_project_created',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
@@ -63,6 +51,18 @@ export const linearProjectCreatedTrigger: TriggerConfig = {
|
||||
value: 'linear_project_created',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
hideFromPreview: true,
|
||||
type: 'text',
|
||||
defaultValue: linearSetupInstructions('Project (create)'),
|
||||
mode: 'trigger',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'linear_project_created',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
outputs: buildProjectOutputs(),
|
||||
|
||||
@@ -39,18 +39,6 @@ export const linearProjectUpdateCreatedTrigger: TriggerConfig = {
|
||||
value: 'linear_project_update_created',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
hideFromPreview: true,
|
||||
type: 'text',
|
||||
defaultValue: linearSetupInstructions('ProjectUpdate (create)'),
|
||||
mode: 'trigger',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'linear_project_update_created',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
@@ -63,6 +51,18 @@ export const linearProjectUpdateCreatedTrigger: TriggerConfig = {
|
||||
value: 'linear_project_update_created',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
hideFromPreview: true,
|
||||
type: 'text',
|
||||
defaultValue: linearSetupInstructions('ProjectUpdate (create)'),
|
||||
mode: 'trigger',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'linear_project_update_created',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
outputs: buildProjectUpdateOutputs(),
|
||||
|
||||
@@ -39,18 +39,6 @@ export const linearProjectUpdatedTrigger: TriggerConfig = {
|
||||
value: 'linear_project_updated',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
hideFromPreview: true,
|
||||
type: 'text',
|
||||
defaultValue: linearSetupInstructions('Project (update)'),
|
||||
mode: 'trigger',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'linear_project_updated',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
@@ -63,6 +51,18 @@ export const linearProjectUpdatedTrigger: TriggerConfig = {
|
||||
value: 'linear_project_updated',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
hideFromPreview: true,
|
||||
type: 'text',
|
||||
defaultValue: linearSetupInstructions('Project (update)'),
|
||||
mode: 'trigger',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'linear_project_updated',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
outputs: buildProjectOutputs(),
|
||||
|
||||
@@ -39,6 +39,18 @@ export const linearWebhookTrigger: TriggerConfig = {
|
||||
value: 'linear_webhook',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'linear_webhook',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'linear_webhook',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
@@ -54,18 +66,6 @@ export const linearWebhookTrigger: TriggerConfig = {
|
||||
value: 'linear_webhook',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'linear_webhook',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'linear_webhook',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
outputs: {
|
||||
|
||||
@@ -72,6 +72,18 @@ export const microsoftTeamsChatSubscriptionTrigger: TriggerConfig = {
|
||||
value: 'microsoftteams_chat_subscription',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'microsoftteams_chat_subscription',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'microsoftteams_chat_subscription',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
@@ -93,18 +105,6 @@ export const microsoftTeamsChatSubscriptionTrigger: TriggerConfig = {
|
||||
value: 'microsoftteams_chat_subscription',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'microsoftteams_chat_subscription',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'microsoftteams_chat_subscription',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
outputs: {
|
||||
|
||||
@@ -51,6 +51,18 @@ export const microsoftTeamsWebhookTrigger: TriggerConfig = {
|
||||
value: 'microsoftteams_webhook',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'microsoftteams_webhook',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'microsoftteams_webhook',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
@@ -76,18 +88,6 @@ export const microsoftTeamsWebhookTrigger: TriggerConfig = {
|
||||
value: 'microsoftteams_webhook',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'microsoftteams_webhook',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'microsoftteams_webhook',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
outputs: {
|
||||
|
||||
@@ -93,6 +93,14 @@ export const outlookPollingTrigger: TriggerConfig = {
|
||||
required: false,
|
||||
mode: 'trigger',
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'outlook_poller',
|
||||
},
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
@@ -110,14 +118,6 @@ export const outlookPollingTrigger: TriggerConfig = {
|
||||
.join(''),
|
||||
mode: 'trigger',
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'outlook_poller',
|
||||
},
|
||||
],
|
||||
|
||||
outputs: {
|
||||
|
||||
@@ -19,6 +19,14 @@ export const rssPollingTrigger: TriggerConfig = {
|
||||
required: true,
|
||||
mode: 'trigger',
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'rss_poller',
|
||||
},
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
@@ -36,14 +44,6 @@ export const rssPollingTrigger: TriggerConfig = {
|
||||
.join(''),
|
||||
mode: 'trigger',
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'rss_poller',
|
||||
},
|
||||
],
|
||||
|
||||
outputs: {
|
||||
|
||||
@@ -30,6 +30,14 @@ export const slackWebhookTrigger: TriggerConfig = {
|
||||
required: true,
|
||||
mode: 'trigger',
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'slack_webhook',
|
||||
},
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
@@ -41,7 +49,7 @@ export const slackWebhookTrigger: TriggerConfig = {
|
||||
'Go to "OAuth & Permissions" and add bot token scopes:<br><ul class="mt-1 ml-5 list-disc"><li><code>app_mentions:read</code> - For viewing messages that tag your bot with an @</li><li><code>chat:write</code> - To send messages to channels your bot is a part of</li></ul>',
|
||||
'Go to "Event Subscriptions":<br><ul class="mt-1 ml-5 list-disc"><li>Enable events</li><li>Under "Subscribe to Bot Events", add <code>app_mention</code> to listen to messages that mention your bot</li><li>Paste the Webhook URL above into the "Request URL" field</li></ul>',
|
||||
'Go to "Install App" in the left sidebar and install the app into your desired Slack workspace and channel.',
|
||||
'Save changes in both Slack and here.',
|
||||
'Save your changes in Slack. Your trigger will configure automatically once you complete the fields above.',
|
||||
]
|
||||
.map(
|
||||
(instruction, index) =>
|
||||
@@ -51,14 +59,6 @@ export const slackWebhookTrigger: TriggerConfig = {
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'slack_webhook',
|
||||
},
|
||||
],
|
||||
|
||||
outputs: {
|
||||
|
||||
@@ -165,6 +165,14 @@ export const stripeWebhookTrigger: TriggerConfig = {
|
||||
password: true,
|
||||
mode: 'trigger',
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'stripe_webhook',
|
||||
},
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
@@ -178,7 +186,7 @@ export const stripeWebhookTrigger: TriggerConfig = {
|
||||
'Click "Create Destination" to save',
|
||||
'After creating the endpoint, click "Reveal" next to "Signing secret" and copy it',
|
||||
'Paste the signing secret into the <strong>Webhook Signing Secret</strong> field above',
|
||||
'Click "Save" to activate your webhook trigger',
|
||||
'Your webhook trigger will activate automatically once you complete the configuration above',
|
||||
]
|
||||
.map(
|
||||
(instruction, index) =>
|
||||
@@ -187,14 +195,6 @@ export const stripeWebhookTrigger: TriggerConfig = {
|
||||
.join(''),
|
||||
mode: 'trigger',
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'stripe_webhook',
|
||||
},
|
||||
],
|
||||
|
||||
outputs: {
|
||||
|
||||
@@ -30,6 +30,14 @@ export const telegramWebhookTrigger: TriggerConfig = {
|
||||
required: true,
|
||||
mode: 'trigger',
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'telegram_webhook',
|
||||
},
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
@@ -38,7 +46,7 @@ export const telegramWebhookTrigger: TriggerConfig = {
|
||||
defaultValue: [
|
||||
'Message "/newbot" to <a href="https://t.me/BotFather" target="_blank" rel="noopener noreferrer" class="text-muted-foreground underline transition-colors hover:text-muted-foreground/80">@BotFather</a> in Telegram to create a bot and copy its token.',
|
||||
'Enter your Bot Token above.',
|
||||
'Save settings and any message sent to your bot will trigger the workflow.',
|
||||
'Once configured, any message sent to your bot will trigger the workflow.',
|
||||
]
|
||||
.map(
|
||||
(instruction, index) =>
|
||||
@@ -47,14 +55,6 @@ export const telegramWebhookTrigger: TriggerConfig = {
|
||||
.join(''),
|
||||
mode: 'trigger',
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'telegram_webhook',
|
||||
},
|
||||
],
|
||||
|
||||
outputs: {
|
||||
|
||||
@@ -49,6 +49,14 @@ export const twilioVoiceWebhookTrigger: TriggerConfig = {
|
||||
required: false,
|
||||
mode: 'trigger',
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'twilio_voice_webhook',
|
||||
},
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
@@ -69,14 +77,6 @@ export const twilioVoiceWebhookTrigger: TriggerConfig = {
|
||||
.join('\n\n'),
|
||||
mode: 'trigger',
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'twilio_voice_webhook',
|
||||
},
|
||||
],
|
||||
|
||||
outputs: {
|
||||
|
||||
@@ -61,6 +61,14 @@ export const typeformWebhookTrigger: TriggerConfig = {
|
||||
defaultValue: false,
|
||||
mode: 'trigger',
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'typeform_webhook',
|
||||
},
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
@@ -71,7 +79,7 @@ export const typeformWebhookTrigger: TriggerConfig = {
|
||||
'Find your Form ID in the URL when editing your form (e.g., <code>https://admin.typeform.com/form/ABC123/create</code> → Form ID is <code>ABC123</code>)',
|
||||
'Fill in the form above with your Form ID and Personal Access Token',
|
||||
'Optionally add a Webhook Secret for enhanced security - Sim will verify all incoming webhooks match this secret',
|
||||
'Click "Save" below - Sim will automatically register the webhook with Typeform',
|
||||
'Sim will automatically register the webhook with Typeform when you complete the configuration above',
|
||||
'<strong>Note:</strong> Requires a Typeform PRO or PRO+ account to use webhooks',
|
||||
]
|
||||
.map(
|
||||
@@ -81,14 +89,6 @@ export const typeformWebhookTrigger: TriggerConfig = {
|
||||
.join(''),
|
||||
mode: 'trigger',
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'typeform_webhook',
|
||||
},
|
||||
],
|
||||
|
||||
outputs: {
|
||||
|
||||
@@ -53,6 +53,18 @@ export const webflowCollectionItemChangedTrigger: TriggerConfig = {
|
||||
value: 'webflow_collection_item_changed',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'webflow_collection_item_changed',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'webflow_collection_item_changed',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
@@ -77,18 +89,6 @@ export const webflowCollectionItemChangedTrigger: TriggerConfig = {
|
||||
value: 'webflow_collection_item_changed',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'webflow_collection_item_changed',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'webflow_collection_item_changed',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
outputs: {
|
||||
|
||||
@@ -66,6 +66,18 @@ export const webflowCollectionItemCreatedTrigger: TriggerConfig = {
|
||||
value: 'webflow_collection_item_created',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'webflow_collection_item_created',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'webflow_collection_item_created',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
@@ -90,18 +102,6 @@ export const webflowCollectionItemCreatedTrigger: TriggerConfig = {
|
||||
value: 'webflow_collection_item_created',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'webflow_collection_item_created',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'webflow_collection_item_created',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
outputs: {
|
||||
|
||||
@@ -53,6 +53,18 @@ export const webflowCollectionItemDeletedTrigger: TriggerConfig = {
|
||||
value: 'webflow_collection_item_deleted',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'webflow_collection_item_deleted',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'webflow_collection_item_deleted',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
@@ -78,18 +90,6 @@ export const webflowCollectionItemDeletedTrigger: TriggerConfig = {
|
||||
value: 'webflow_collection_item_deleted',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'webflow_collection_item_deleted',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'webflow_collection_item_deleted',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
outputs: {
|
||||
|
||||
@@ -40,6 +40,14 @@ export const webflowFormSubmissionTrigger: TriggerConfig = {
|
||||
required: false,
|
||||
mode: 'trigger',
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'webflow_form_submission',
|
||||
},
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
@@ -61,14 +69,6 @@ export const webflowFormSubmissionTrigger: TriggerConfig = {
|
||||
.join(''),
|
||||
mode: 'trigger',
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'webflow_form_submission',
|
||||
},
|
||||
],
|
||||
|
||||
outputs: {
|
||||
|
||||
@@ -31,6 +31,14 @@ export const whatsappWebhookTrigger: TriggerConfig = {
|
||||
required: true,
|
||||
mode: 'trigger',
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'whatsapp_webhook',
|
||||
},
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
@@ -53,14 +61,6 @@ export const whatsappWebhookTrigger: TriggerConfig = {
|
||||
.join(''),
|
||||
mode: 'trigger',
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'whatsapp_webhook',
|
||||
},
|
||||
],
|
||||
|
||||
outputs: {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user