feat(trigger): implement plugin trigger synchronization and subscription management in workflow

- Added a new event handler for syncing plugin trigger relationships when a draft workflow is synced, ensuring that the database reflects the current state of plugin triggers.
- Introduced subscription management features in the frontend, allowing users to select, add, and remove subscriptions for trigger plugins.
- Updated various components to support subscription handling, including the addition of new UI elements for subscription selection and removal.
- Enhanced internationalization support by adding new translation keys related to subscription management.

These changes improve the overall functionality and user experience of trigger plugins within workflows.
This commit is contained in:
Harry
2025-09-15 15:49:07 +08:00
parent dcf3ee6982
commit 6857bb4406
11 changed files with 352 additions and 82 deletions

View File

@ -280,6 +280,15 @@ const BasePanel: FC<BasePanelProps> = ({
})
}, [handleNodeDataUpdateWithSyncDraft, id])
const handleSubscriptionChange = useCallback((subscription_id: string) => {
handleNodeDataUpdateWithSyncDraft({
id,
data: {
subscription_id,
},
})
}, [handleNodeDataUpdateWithSyncDraft, id])
if(logParams.showSpecialResultPanel) {
return (
<div className={cn(
@ -428,6 +437,7 @@ const BasePanel: FC<BasePanelProps> = ({
<NodeAuth
data={data}
onAuthorizationChange={handleAuthorizationItemClick}
onSubscriptionChange={handleSubscriptionChange}
/>
</div>
</PluginAuth>
@ -451,6 +461,7 @@ const BasePanel: FC<BasePanelProps> = ({
<NodeAuth
data={data}
onAuthorizationChange={handleAuthorizationItemClick}
onSubscriptionChange={handleSubscriptionChange}
/>
</div>
)

View File

@ -7,7 +7,6 @@ import { BlockEnum, type Node } from '@/app/components/workflow/types'
import { canFindTool } from '@/utils'
import { useStore } from '@/app/components/workflow/store'
import AuthenticationMenu from '@/app/components/workflow/nodes/trigger-plugin/components/authentication-menu'
import type { AuthSubscription } from '@/app/components/workflow/nodes/trigger-plugin/components/authentication-menu'
import {
useDeleteTriggerSubscription,
useInitiateTriggerOAuth,
@ -20,9 +19,10 @@ import { openOAuthPopup } from '@/hooks/use-oauth'
type NodeAuthProps = {
data: Node['data']
onAuthorizationChange: (credential_id: string) => void
onSubscriptionChange?: (subscription_id: string) => void
}
const NodeAuth: FC<NodeAuthProps> = ({ data, onAuthorizationChange }) => {
const NodeAuth: FC<NodeAuthProps> = ({ data, onAuthorizationChange, onSubscriptionChange }) => {
const { t } = useTranslation()
const buildInTools = useStore(s => s.buildInTools)
const { notify } = useToastContext()
@ -51,39 +51,8 @@ const NodeAuth: FC<NodeAuthProps> = ({ data, onAuthorizationChange }) => {
return buildInTools.find(item => canFindTool(item.id, data.provider_id))
}, [buildInTools, data.provider_id])
// Convert TriggerSubscription to AuthSubscription format
const authSubscription: AuthSubscription = useMemo(() => {
if (data.type !== BlockEnum.TriggerPlugin) {
return {
id: '',
name: '',
status: 'not_configured',
credentials: {},
}
}
const subscription = subscriptions[0] // Use first subscription if available
if (!subscription) {
return {
id: '',
name: '',
status: 'not_configured',
credentials: {},
}
}
const status = subscription.credential_type === 'unauthorized'
? 'not_configured'
: 'authorized'
return {
id: subscription.id,
name: subscription.name,
status,
credentials: subscription.credentials,
}
}, [data.type, subscriptions])
// Get selected subscription ID from node data
const selectedSubscriptionId = data.subscription_id
const handleConfigure = useCallback(async () => {
if (!provider) return
@ -117,10 +86,35 @@ const NodeAuth: FC<NodeAuthProps> = ({ data, onAuthorizationChange }) => {
}
}, [provider, initiateTriggerOAuth, invalidateSubscriptions, notify])
const handleRemove = useCallback(() => {
if (authSubscription.id)
deleteSubscription.mutate(authSubscription.id)
}, [authSubscription.id, deleteSubscription])
const handleRemove = useCallback(async (subscriptionId: string) => {
if (!subscriptionId) return
try {
await deleteSubscription.mutateAsync(subscriptionId)
// Clear subscription_id from node data
if (onSubscriptionChange)
onSubscriptionChange('')
// Refresh subscriptions list
invalidateSubscriptions(provider)
notify({
type: 'success',
message: t('workflow.nodes.triggerPlugin.subscriptionRemoved'),
})
}
catch (error: any) {
notify({
type: 'error',
message: `Failed to remove subscription: ${error.message}`,
})
}
}, [deleteSubscription, invalidateSubscriptions, notify, onSubscriptionChange, provider, t])
const handleSubscriptionSelect = useCallback((subscriptionId: string) => {
if (onSubscriptionChange)
onSubscriptionChange(subscriptionId)
}, [onSubscriptionChange])
// Tool authentication
if (data.type === BlockEnum.Tool && currCollection?.allow_delete) {
@ -140,7 +134,9 @@ const NodeAuth: FC<NodeAuthProps> = ({ data, onAuthorizationChange }) => {
if (data.type === BlockEnum.TriggerPlugin) {
return (
<AuthenticationMenu
subscription={authSubscription}
subscriptions={subscriptions}
selectedSubscriptionId={selectedSubscriptionId}
onSubscriptionSelect={handleSubscriptionSelect}
onConfigure={handleConfigure}
onRemove={handleRemove}
/>

View File

@ -1,12 +1,13 @@
'use client'
import type { FC } from 'react'
import { memo, useCallback, useState } from 'react'
import { memo, useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { RiArrowDownSLine } from '@remixicon/react'
import { RiAddLine, RiArrowDownSLine, RiCheckLine } from '@remixicon/react'
import Button from '@/app/components/base/button'
import Indicator from '@/app/components/header/indicator'
import cn from '@/utils/classnames'
import type { TriggerSubscription } from '@/app/components/workflow/block-selector/types'
export type AuthenticationStatus = 'authorized' | 'not_configured' | 'error'
@ -18,14 +19,18 @@ export type AuthSubscription = {
}
type AuthenticationMenuProps = {
subscription?: AuthSubscription
subscriptions: TriggerSubscription[]
selectedSubscriptionId?: string
onSubscriptionSelect: (subscriptionId: string) => void
onConfigure: () => void
onRemove: () => void
onRemove: (subscriptionId: string) => void
className?: string
}
const AuthenticationMenu: FC<AuthenticationMenuProps> = ({
subscription,
subscriptions,
selectedSubscriptionId,
onSubscriptionSelect,
onConfigure,
onRemove,
className,
@ -33,32 +38,40 @@ const AuthenticationMenu: FC<AuthenticationMenuProps> = ({
const { t } = useTranslation()
const [isOpen, setIsOpen] = useState(false)
const selectedSubscription = useMemo(() => {
return subscriptions.find(sub => sub.id === selectedSubscriptionId)
}, [subscriptions, selectedSubscriptionId])
const getStatusConfig = useCallback(() => {
if (!subscription) {
if (!selectedSubscription) {
if (subscriptions.length > 0) {
return {
label: t('workflow.nodes.triggerPlugin.selectSubscription'),
color: 'yellow' as const,
}
}
return {
label: t('workflow.nodes.triggerPlugin.notConfigured'),
color: 'red' as const,
}
}
switch (subscription.status) {
case 'authorized':
return {
label: t('workflow.nodes.triggerPlugin.authorized'),
color: 'green' as const,
}
case 'error':
return {
label: t('workflow.nodes.triggerPlugin.error'),
color: 'red' as const,
}
default:
return {
label: t('workflow.nodes.triggerPlugin.notConfigured'),
color: 'red' as const,
}
// Check if subscription is authorized based on credential_type
const isAuthorized = selectedSubscription.credential_type !== 'unauthorized'
if (isAuthorized) {
return {
label: selectedSubscription.name || t('workflow.nodes.triggerPlugin.authorized'),
color: 'green' as const,
}
}
}, [subscription, t])
else {
return {
label: t('workflow.nodes.triggerPlugin.notAuthorized'),
color: 'red' as const,
}
}
}, [selectedSubscription, subscriptions.length, t])
const statusConfig = getStatusConfig()
@ -67,11 +80,16 @@ const AuthenticationMenu: FC<AuthenticationMenuProps> = ({
setIsOpen(false)
}, [onConfigure])
const handleRemove = useCallback(() => {
onRemove()
const handleRemove = useCallback((subscriptionId: string) => {
onRemove(subscriptionId)
setIsOpen(false)
}, [onRemove])
const handleSelectSubscription = useCallback((subscriptionId: string) => {
onSubscriptionSelect(subscriptionId)
setIsOpen(false)
}, [onSubscriptionSelect])
return (
<div className={cn('relative', className)}>
<Button
@ -107,31 +125,78 @@ const AuthenticationMenu: FC<AuthenticationMenuProps> = ({
{/* Dropdown Menu */}
<div className={cn(
'absolute right-0 z-30 mt-1',
'w-[136px] rounded-xl border-[0.5px] border-components-panel-border',
'w-[240px] rounded-xl border-[0.5px] border-components-panel-border',
'bg-components-panel-bg-blur shadow-lg backdrop-blur-sm',
)}>
<div className="py-1">
{/* Subscription list */}
{subscriptions.length > 0 && (
<>
<div className="px-3 py-1.5 text-xs font-medium text-text-tertiary">
{t('workflow.nodes.triggerPlugin.availableSubscriptions')}
</div>
<div className="max-h-[200px] overflow-y-auto">
{subscriptions.map((subscription) => {
const isSelected = subscription.id === selectedSubscriptionId
const isAuthorized = subscription.credential_type !== 'unauthorized'
return (
<button
key={subscription.id}
className={cn(
'flex w-full items-center justify-between px-3 py-2 text-left text-sm',
'hover:bg-state-base-hover',
isSelected && 'bg-state-base-hover',
)}
onClick={() => handleSelectSubscription(subscription.id)}
>
<div className="flex items-center gap-2">
<Indicator
color={isAuthorized ? 'green' : 'red'}
/>
<span className={cn(
'text-text-secondary',
isSelected && 'font-medium text-text-primary',
)}>
{subscription.name}
</span>
</div>
{isSelected && (
<RiCheckLine className="h-4 w-4 text-text-accent" />
)}
</button>
)
})}
</div>
<div className="my-1 h-[0.5px] bg-divider-subtle" />
</>
)}
{/* Add new subscription */}
<button
className={cn(
'block w-full px-4 py-2 text-left text-sm',
'flex w-full items-center gap-2 px-3 py-2 text-left text-sm',
'text-text-secondary hover:bg-state-base-hover',
'mx-1 rounded-lg',
)}
onClick={handleConfigure}
>
{t('workflow.nodes.triggerPlugin.configuration')}
<RiAddLine className="h-4 w-4" />
{t('workflow.nodes.triggerPlugin.addSubscription')}
</button>
{subscription && subscription.status === 'authorized' && (
<button
className={cn(
'block w-full px-4 py-2 text-left text-sm',
'text-text-destructive hover:bg-state-destructive-hover',
'mx-1 rounded-lg',
)}
onClick={handleRemove}
>
{t('workflow.nodes.triggerPlugin.remove')}
</button>
{/* Remove subscription */}
{selectedSubscription && (
<>
<div className="my-1 h-[0.5px] bg-divider-subtle" />
<button
className={cn(
'block w-full px-3 py-2 text-left text-sm',
'text-text-destructive hover:bg-state-destructive-hover',
)}
onClick={() => handleRemove(selectedSubscription.id)}
>
{t('workflow.nodes.triggerPlugin.removeSubscription')}
</button>
</>
)}
</div>
</div>