fix(triggers-persistence): triggers persistence, deletion, updating configs + state management simplifcation (#1783)

* fix(triggers): configuration persistences issues

* required fields validation staleness issue
This commit is contained in:
Vikhyath Mondreti
2025-10-31 18:06:31 -07:00
committed by GitHub
parent f6a5c5c829
commit 44271cd101
3 changed files with 140 additions and 161 deletions

View File

@@ -421,7 +421,8 @@ export async function POST(request: NextRequest) {
const success = await configureGmailPolling(savedWebhook, requestId)
if (!success) {
logger.error(`[${requestId}] Failed to configure Gmail polling`)
logger.error(`[${requestId}] Failed to configure Gmail polling, rolling back webhook`)
await db.delete(webhook).where(eq(webhook.id, savedWebhook.id))
return NextResponse.json(
{
error: 'Failed to configure Gmail polling',
@@ -433,7 +434,11 @@ export async function POST(request: NextRequest) {
logger.info(`[${requestId}] Successfully configured Gmail polling`)
} catch (err) {
logger.error(`[${requestId}] Error setting up Gmail webhook configuration`, err)
logger.error(
`[${requestId}] Error setting up Gmail webhook configuration, rolling back webhook`,
err
)
await db.delete(webhook).where(eq(webhook.id, savedWebhook.id))
return NextResponse.json(
{
error: 'Failed to configure Gmail webhook',
@@ -455,7 +460,8 @@ export async function POST(request: NextRequest) {
const success = await configureOutlookPolling(savedWebhook, requestId)
if (!success) {
logger.error(`[${requestId}] Failed to configure Outlook polling`)
logger.error(`[${requestId}] Failed to configure Outlook polling, rolling back webhook`)
await db.delete(webhook).where(eq(webhook.id, savedWebhook.id))
return NextResponse.json(
{
error: 'Failed to configure Outlook polling',
@@ -467,7 +473,11 @@ export async function POST(request: NextRequest) {
logger.info(`[${requestId}] Successfully configured Outlook polling`)
} catch (err) {
logger.error(`[${requestId}] Error setting up Outlook webhook configuration`, err)
logger.error(
`[${requestId}] Error setting up Outlook webhook configuration, rolling back webhook`,
err
)
await db.delete(webhook).where(eq(webhook.id, savedWebhook.id))
return NextResponse.json(
{
error: 'Failed to configure Outlook webhook',

View File

@@ -15,6 +15,7 @@ import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { createLogger } from '@/lib/logs/console/logger'
import { cn } from '@/lib/utils'
import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow'
import { useTriggerConfigAggregation } from '@/hooks/use-trigger-config-aggregation'
import { useWebhookManagement } from '@/hooks/use-webhook-management'
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
@@ -60,6 +61,8 @@ export function TriggerSave({
return triggerId
}, [blockId, triggerId])
const { collaborativeSetSubblockValue } = useCollaborativeWorkflow()
const { webhookId, saveConfig, deleteConfig, isLoading } = useWebhookManagement({
blockId,
triggerId: effectiveTriggerId,
@@ -119,39 +122,35 @@ export function TriggerSave({
.map((sb) => sb.id)
}, [triggerDef])
const otherRequiredValues = useMemo(() => {
if (!triggerDef) return {}
const values: Record<string, any> = {}
requiredSubBlockIds
.filter((id) => id !== 'triggerCredentials')
.forEach((subBlockId) => {
const value = useSubBlockStore.getState().getValue(blockId, subBlockId)
if (value !== null && value !== undefined && value !== '') {
values[subBlockId] = value
}
})
return values
}, [blockId, triggerDef, requiredSubBlockIds])
const requiredSubBlockValues = useMemo(() => {
return {
triggerCredentials,
...otherRequiredValues,
}
}, [triggerCredentials, otherRequiredValues])
const subscribedSubBlockValues = useSubBlockStore(
useCallback(
(state) => {
if (!triggerDef) return {}
const values: Record<string, any> = {}
requiredSubBlockIds.forEach((subBlockId) => {
const value = state.getValue(blockId, subBlockId)
if (value !== null && value !== undefined && value !== '') {
values[subBlockId] = value
}
})
return values
},
[blockId, triggerDef, requiredSubBlockIds]
)
)
const previousValuesRef = useRef<Record<string, any>>({})
const validationTimeoutRef = useRef<NodeJS.Timeout | null>(null)
useEffect(() => {
if (saveStatus !== 'error' || !triggerDef) {
previousValuesRef.current = requiredSubBlockValues
previousValuesRef.current = subscribedSubBlockValues
return
}
const hasChanges = Object.keys(requiredSubBlockValues).some(
const hasChanges = Object.keys(subscribedSubBlockValues).some(
(key) =>
previousValuesRef.current[key] !== (requiredSubBlockValues as Record<string, any>)[key]
previousValuesRef.current[key] !== (subscribedSubBlockValues as Record<string, any>)[key]
)
if (!hasChanges) {
@@ -169,9 +168,7 @@ export function TriggerSave({
useSubBlockStore.getState().setValue(blockId, 'triggerConfig', aggregatedConfig)
}
const configToValidate =
aggregatedConfig ?? useSubBlockStore.getState().getValue(blockId, 'triggerConfig')
const validation = validateRequiredFields(configToValidate)
const validation = validateRequiredFields(aggregatedConfig)
if (validation.valid) {
setErrorMessage(null)
@@ -181,21 +178,15 @@ export function TriggerSave({
triggerId: effectiveTriggerId,
})
} else {
const newErrorMessage = `Missing required fields: ${validation.missingFields.join(', ')}`
setErrorMessage((prev) => {
if (prev !== newErrorMessage) {
logger.debug('Error message updated', {
blockId,
triggerId: effectiveTriggerId,
missingFields: validation.missingFields,
})
return newErrorMessage
}
return prev
setErrorMessage(`Missing required fields: ${validation.missingFields.join(', ')}`)
logger.debug('Error message updated', {
blockId,
triggerId: effectiveTriggerId,
missingFields: validation.missingFields,
})
}
previousValuesRef.current = requiredSubBlockValues
previousValuesRef.current = subscribedSubBlockValues
}, 300)
return () => {
@@ -207,7 +198,7 @@ export function TriggerSave({
blockId,
effectiveTriggerId,
triggerDef,
requiredSubBlockValues,
subscribedSubBlockValues,
saveStatus,
validateRequiredFields,
])
@@ -230,8 +221,7 @@ export function TriggerSave({
})
}
const configToValidate = aggregatedConfig ?? triggerConfig
const validation = validateRequiredFields(configToValidate)
const validation = validateRequiredFields(aggregatedConfig)
if (!validation.valid) {
setErrorMessage(`Missing required fields: ${validation.missingFields.join(', ')}`)
setSaveStatus('error')
@@ -239,25 +229,32 @@ export function TriggerSave({
}
const success = await saveConfig()
if (success) {
setSaveStatus('saved')
setErrorMessage(null)
setTimeout(() => {
setSaveStatus('idle')
}, 2000)
logger.info('Trigger configuration saved successfully', {
blockId,
triggerId: effectiveTriggerId,
hasWebhookId: !!webhookId,
})
} else {
setSaveStatus('error')
setErrorMessage('Failed to save trigger configuration. Please try again.')
logger.error('Failed to save trigger configuration')
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.')
@@ -317,6 +314,10 @@ export function TriggerSave({
setTestUrl(null)
setTestUrlExpiresAt(null)
collaborativeSetSubblockValue(blockId, 'triggerPath', '')
collaborativeSetSubblockValue(blockId, 'webhookId', null)
collaborativeSetSubblockValue(blockId, 'triggerConfig', null)
logger.info('Trigger configuration deleted successfully', {
blockId,
triggerId: effectiveTriggerId,

View File

@@ -26,6 +26,59 @@ interface WebhookManagementState {
deleteConfig: () => Promise<boolean>
}
/**
* Resolves the effective triggerId from various sources in order of priority
*/
function resolveEffectiveTriggerId(
blockId: string,
triggerId: string | undefined,
webhook?: { providerConfig?: { triggerId?: string } }
): string | undefined {
if (triggerId && isTriggerValid(triggerId)) {
return triggerId
}
const selectedTriggerId = useSubBlockStore.getState().getValue(blockId, 'selectedTriggerId')
if (typeof selectedTriggerId === 'string' && isTriggerValid(selectedTriggerId)) {
return selectedTriggerId
}
const storedTriggerId = useSubBlockStore.getState().getValue(blockId, 'triggerId')
if (typeof storedTriggerId === 'string' && isTriggerValid(storedTriggerId)) {
return storedTriggerId
}
if (webhook?.providerConfig?.triggerId && typeof webhook.providerConfig.triggerId === 'string') {
return webhook.providerConfig.triggerId
}
const workflowState = useWorkflowStore.getState()
const block = workflowState.blocks?.[blockId]
if (block) {
const blockConfig = getBlock(block.type)
if (blockConfig) {
if (blockConfig.category === 'triggers') {
return block.type
}
if (block.triggerMode && blockConfig.triggers?.enabled) {
const selectedTriggerIdValue = block.subBlocks?.selectedTriggerId?.value
const triggerIdValue = block.subBlocks?.triggerId?.value
return (
(typeof selectedTriggerIdValue === 'string' && isTriggerValid(selectedTriggerIdValue)
? selectedTriggerIdValue
: undefined) ||
(typeof triggerIdValue === 'string' && isTriggerValid(triggerIdValue)
? triggerIdValue
: undefined) ||
blockConfig.triggers?.available?.[0]
)
}
}
}
return undefined
}
/**
* Hook to manage webhook lifecycle for trigger blocks
* Handles:
@@ -82,37 +135,17 @@ export function useWebhookManagement({
const alreadyChecked = store.checkedWebhooks.has(blockId)
const currentWebhookId = store.getValue(blockId, 'webhookId')
if (currentlyLoading) {
if (currentlyLoading || (alreadyChecked && currentWebhookId)) {
return
}
if (alreadyChecked && currentWebhookId) {
return
}
if (alreadyChecked && !currentWebhookId) {
useSubBlockStore.setState((state) => {
const newSet = new Set(state.checkedWebhooks)
newSet.delete(blockId)
return { checkedWebhooks: newSet }
})
}
let isMounted = true
const loadWebhookOrGenerateUrl = async () => {
const currentStore = useSubBlockStore.getState()
if (currentStore.loadingWebhooks.has(blockId)) {
return
}
useSubBlockStore.setState((state) => ({
loadingWebhooks: new Set([...state.loadingWebhooks, blockId]),
}))
try {
const response = await fetch(`/api/webhooks?workflowId=${workflowId}&blockId=${blockId}`)
const stillMounted = isMounted
if (response.ok) {
const data = await response.json()
@@ -125,73 +158,25 @@ export function useWebhookManagement({
blockId,
webhookId: webhook.id,
hasProviderConfig: !!webhook.providerConfig,
wasMounted: stillMounted,
})
if (webhook.path) {
const currentPath = useSubBlockStore.getState().getValue(blockId, 'triggerPath')
if (webhook.path !== currentPath) {
useSubBlockStore.getState().setValue(blockId, 'triggerPath', webhook.path)
}
useSubBlockStore.getState().setValue(blockId, 'triggerPath', webhook.path)
}
if (webhook.providerConfig) {
let effectiveTriggerId: string | undefined = triggerId
if (!effectiveTriggerId) {
const storedTriggerId = useSubBlockStore.getState().getValue(blockId, 'triggerId')
effectiveTriggerId =
(typeof storedTriggerId === 'string' ? storedTriggerId : undefined) || undefined
}
if (!effectiveTriggerId && webhook.providerConfig.triggerId) {
effectiveTriggerId =
typeof webhook.providerConfig.triggerId === 'string'
? webhook.providerConfig.triggerId
: undefined
}
if (!effectiveTriggerId) {
const workflowState = useWorkflowStore.getState()
const block = workflowState.blocks?.[blockId]
if (block) {
const blockConfig = getBlock(block.type)
if (blockConfig) {
if (blockConfig.category === 'triggers') {
effectiveTriggerId = block.type
} else if (block.triggerMode && blockConfig.triggers?.enabled) {
const selectedTriggerIdValue = block.subBlocks?.selectedTriggerId?.value
const triggerIdValue = block.subBlocks?.triggerId?.value
effectiveTriggerId =
(typeof selectedTriggerIdValue === 'string' &&
isTriggerValid(selectedTriggerIdValue)
? selectedTriggerIdValue
: undefined) ||
(typeof triggerIdValue === 'string' && isTriggerValid(triggerIdValue)
? triggerIdValue
: undefined) ||
blockConfig.triggers?.available?.[0]
}
}
}
}
const effectiveTriggerId = resolveEffectiveTriggerId(blockId, triggerId, webhook)
const currentConfig = useSubBlockStore.getState().getValue(blockId, 'triggerConfig')
if (JSON.stringify(webhook.providerConfig) !== JSON.stringify(currentConfig)) {
useSubBlockStore
.getState()
.setValue(blockId, 'triggerConfig', webhook.providerConfig)
useSubBlockStore.getState().setValue(blockId, 'triggerConfig', webhook.providerConfig)
if (effectiveTriggerId) {
populateTriggerFieldsFromConfig(
blockId,
webhook.providerConfig,
effectiveTriggerId
)
} else {
logger.warn('Cannot migrate - triggerId not available', {
blockId,
propTriggerId: triggerId,
providerConfigTriggerId: webhook.providerConfig.triggerId,
})
}
if (effectiveTriggerId) {
populateTriggerFieldsFromConfig(blockId, webhook.providerConfig, effectiveTriggerId)
} else {
logger.warn('Cannot migrate - triggerId not available', {
blockId,
propTriggerId: triggerId,
providerConfigTriggerId: webhook.providerConfig.triggerId,
})
}
}
} else {
@@ -221,10 +206,6 @@ export function useWebhookManagement({
}
loadWebhookOrGenerateUrl()
return () => {
isMounted = false
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isPreview, triggerId, workflowId, blockId])
@@ -233,20 +214,7 @@ export function useWebhookManagement({
return false
}
let effectiveTriggerId: string | undefined = triggerId
if (!effectiveTriggerId) {
const selectedTriggerId = useSubBlockStore.getState().getValue(blockId, 'selectedTriggerId')
if (typeof selectedTriggerId === 'string' && isTriggerValid(selectedTriggerId)) {
effectiveTriggerId = selectedTriggerId
}
}
if (!effectiveTriggerId) {
const storedTriggerId = useSubBlockStore.getState().getValue(blockId, 'triggerId')
effectiveTriggerId =
typeof storedTriggerId === 'string' && isTriggerValid(storedTriggerId)
? storedTriggerId
: triggerId
}
const effectiveTriggerId = resolveEffectiveTriggerId(blockId, triggerId)
try {
setIsSaving(true)