mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-28 03:00:29 -04:00
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:
committed by
GitHub
parent
f6a5c5c829
commit
44271cd101
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user