mirror of
https://github.com/langgenius/dify.git
synced 2026-05-06 02:18:08 +08:00
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:
@ -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>
|
||||
)
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
|
||||
@ -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>
|
||||
|
||||
Reference in New Issue
Block a user