Compare commits

...

1 Commits

Author SHA1 Message Date
waleed
09cea81ae3 feat(trigger): for all triggers, remove save and delete buttons and autosave config 2025-12-16 17:35:09 -08:00
87 changed files with 1489 additions and 1599 deletions

View File

@@ -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>
)}
</>
)
}

View File

@@ -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>
)
}

View File

@@ -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>
)
}

View File

@@ -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(

View File

@@ -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

View File

@@ -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,

View File

@@ -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) => {

View File

@@ -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>
)}

View File

@@ -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

View 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,
}
}

View File

@@ -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,
}
}

View File

@@ -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 {

View File

@@ -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,
}
}

View File

@@ -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: {

View File

@@ -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(),

View File

@@ -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(),

View File

@@ -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(),

View File

@@ -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: {

View File

@@ -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: {},

View File

@@ -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: {

View File

@@ -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: {

View File

@@ -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: {

View File

@@ -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: {

View File

@@ -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: {

View File

@@ -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: {

View File

@@ -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: {

View File

@@ -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: {

View File

@@ -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: {

View File

@@ -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: {

View File

@@ -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: {

View File

@@ -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: {

View File

@@ -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: {

View File

@@ -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: {

View File

@@ -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',

View File

@@ -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',

View File

@@ -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',

View File

@@ -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',

View File

@@ -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',

View File

@@ -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',

View File

@@ -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',

View File

@@ -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',

View File

@@ -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',

View File

@@ -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',

View File

@@ -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',

View File

@@ -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',

View File

@@ -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',

View File

@@ -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',

View File

@@ -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',

View File

@@ -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',

View File

@@ -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',

View File

@@ -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',

View File

@@ -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.",
]

View File

@@ -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(),

View File

@@ -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(),

View File

@@ -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(),

View File

@@ -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(),

View File

@@ -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: {

View File

@@ -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(),

View File

@@ -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(),

View File

@@ -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(),

View File

@@ -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(),

View File

@@ -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(),

View File

@@ -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(),

View File

@@ -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(),

View File

@@ -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(),

View File

@@ -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(),

View File

@@ -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(),

View File

@@ -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(),

View File

@@ -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(),

View File

@@ -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(),

View File

@@ -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(),

View File

@@ -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(),

View File

@@ -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: {

View File

@@ -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: {

View File

@@ -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: {

View File

@@ -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: {

View File

@@ -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: {

View File

@@ -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: {

View File

@@ -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: {

View File

@@ -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: {

View File

@@ -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: {

View File

@@ -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: {

View File

@@ -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: {

View File

@@ -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: {

View File

@@ -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: {

View File

@@ -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: {

View File

@@ -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: {