mirror of https://github.com/langgenius/dify.git
feat: add subscription in node
This commit is contained in:
parent
48597ef193
commit
973b937ba5
|
|
@ -0,0 +1,273 @@
|
|||
'use client'
|
||||
import type { ErrorInfo, ReactNode } from 'react'
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { RiAlertLine, RiBugLine } from '@remixicon/react'
|
||||
import Button from '@/app/components/base/button'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type ErrorBoundaryState = {
|
||||
hasError: boolean
|
||||
error: Error | null
|
||||
errorInfo: ErrorInfo | null
|
||||
errorCount: number
|
||||
}
|
||||
|
||||
type ErrorBoundaryProps = {
|
||||
children: ReactNode
|
||||
fallback?: ReactNode | ((error: Error, reset: () => void) => ReactNode)
|
||||
onError?: (error: Error, errorInfo: ErrorInfo) => void
|
||||
onReset?: () => void
|
||||
showDetails?: boolean
|
||||
className?: string
|
||||
resetKeys?: Array<string | number>
|
||||
resetOnPropsChange?: boolean
|
||||
isolate?: boolean
|
||||
enableRecovery?: boolean
|
||||
customTitle?: string
|
||||
customMessage?: string
|
||||
}
|
||||
|
||||
// Internal class component for error catching
|
||||
class ErrorBoundaryInner extends React.Component<
|
||||
ErrorBoundaryProps & {
|
||||
resetErrorBoundary: () => void
|
||||
onResetKeysChange: (prevResetKeys?: Array<string | number>) => void
|
||||
},
|
||||
ErrorBoundaryState
|
||||
> {
|
||||
constructor(props: any) {
|
||||
super(props)
|
||||
this.state = {
|
||||
hasError: false,
|
||||
error: null,
|
||||
errorInfo: null,
|
||||
errorCount: 0,
|
||||
}
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(error: Error): Partial<ErrorBoundaryState> {
|
||||
return {
|
||||
hasError: true,
|
||||
error,
|
||||
}
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.error('ErrorBoundary caught an error:', error)
|
||||
console.error('Error Info:', errorInfo)
|
||||
}
|
||||
|
||||
this.setState(prevState => ({
|
||||
errorInfo,
|
||||
errorCount: prevState.errorCount + 1,
|
||||
}))
|
||||
|
||||
if (this.props.onError)
|
||||
this.props.onError(error, errorInfo)
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: any) {
|
||||
const { resetKeys, resetOnPropsChange } = this.props
|
||||
const { hasError } = this.state
|
||||
|
||||
if (hasError && prevProps.resetKeys !== resetKeys) {
|
||||
if (resetKeys?.some((key, idx) => key !== prevProps.resetKeys?.[idx]))
|
||||
this.props.resetErrorBoundary()
|
||||
}
|
||||
|
||||
if (hasError && resetOnPropsChange && prevProps.children !== this.props.children)
|
||||
this.props.resetErrorBoundary()
|
||||
|
||||
if (prevProps.resetKeys !== resetKeys)
|
||||
this.props.onResetKeysChange(prevProps.resetKeys)
|
||||
}
|
||||
|
||||
render() {
|
||||
const { hasError, error, errorInfo, errorCount } = this.state
|
||||
const {
|
||||
fallback,
|
||||
children,
|
||||
showDetails = false,
|
||||
className,
|
||||
isolate = true,
|
||||
enableRecovery = true,
|
||||
customTitle,
|
||||
customMessage,
|
||||
resetErrorBoundary,
|
||||
} = this.props
|
||||
|
||||
if (hasError && error) {
|
||||
if (fallback) {
|
||||
if (typeof fallback === 'function')
|
||||
return fallback(error, resetErrorBoundary)
|
||||
|
||||
return fallback
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'border-state-critical-border bg-state-critical-hover-alt flex flex-col items-center justify-center rounded-lg border p-8',
|
||||
isolate && 'min-h-[200px]',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className='mb-4 flex items-center gap-2'>
|
||||
<RiAlertLine className='text-state-critical-solid h-8 w-8' />
|
||||
<h2 className='text-xl font-semibold text-text-primary'>
|
||||
{customTitle || 'Something went wrong'}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<p className='mb-6 text-center text-text-secondary'>
|
||||
{customMessage || 'An unexpected error occurred while rendering this component.'}
|
||||
</p>
|
||||
|
||||
{showDetails && errorInfo && (
|
||||
<details className='mb-6 w-full max-w-2xl'>
|
||||
<summary className='mb-2 cursor-pointer text-sm font-medium text-text-tertiary hover:text-text-secondary'>
|
||||
<span className='inline-flex items-center gap-1'>
|
||||
<RiBugLine className='h-4 w-4' />
|
||||
Error Details (Development Only)
|
||||
</span>
|
||||
</summary>
|
||||
<div className='rounded-lg bg-gray-100 p-4'>
|
||||
<div className='mb-2'>
|
||||
<span className='font-mono text-xs font-semibold text-gray-600'>Error:</span>
|
||||
<pre className='mt-1 overflow-auto whitespace-pre-wrap font-mono text-xs text-gray-800'>
|
||||
{error.toString()}
|
||||
</pre>
|
||||
</div>
|
||||
{errorInfo && (
|
||||
<div>
|
||||
<span className='font-mono text-xs font-semibold text-gray-600'>Component Stack:</span>
|
||||
<pre className='mt-1 max-h-40 overflow-auto whitespace-pre-wrap font-mono text-xs text-gray-700'>
|
||||
{errorInfo.componentStack}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
{errorCount > 1 && (
|
||||
<div className='mt-2 text-xs text-gray-600'>
|
||||
This error has occurred {errorCount} times
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</details>
|
||||
)}
|
||||
|
||||
{enableRecovery && (
|
||||
<div className='flex gap-3'>
|
||||
<Button
|
||||
variant='primary'
|
||||
size='small'
|
||||
onClick={resetErrorBoundary}
|
||||
>
|
||||
Try Again
|
||||
</Button>
|
||||
<Button
|
||||
variant='secondary'
|
||||
size='small'
|
||||
onClick={() => window.location.reload()}
|
||||
>
|
||||
Reload Page
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return children
|
||||
}
|
||||
}
|
||||
|
||||
// Main functional component wrapper
|
||||
const ErrorBoundary: React.FC<ErrorBoundaryProps> = (props) => {
|
||||
const [errorBoundaryKey, setErrorBoundaryKey] = useState(0)
|
||||
const resetKeysRef = useRef(props.resetKeys)
|
||||
const prevResetKeysRef = useRef<Array<string | number> | undefined>(undefined)
|
||||
|
||||
const resetErrorBoundary = useCallback(() => {
|
||||
setErrorBoundaryKey(prev => prev + 1)
|
||||
props.onReset?.()
|
||||
}, [props])
|
||||
|
||||
const onResetKeysChange = useCallback((prevResetKeys?: Array<string | number>) => {
|
||||
prevResetKeysRef.current = prevResetKeys
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (prevResetKeysRef.current !== props.resetKeys)
|
||||
resetKeysRef.current = props.resetKeys
|
||||
}, [props.resetKeys])
|
||||
|
||||
return (
|
||||
<ErrorBoundaryInner
|
||||
{...props}
|
||||
key={errorBoundaryKey}
|
||||
resetErrorBoundary={resetErrorBoundary}
|
||||
onResetKeysChange={onResetKeysChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
// Hook for imperative error handling
|
||||
export function useErrorHandler() {
|
||||
const [error, setError] = useState<Error | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (error)
|
||||
throw error
|
||||
}, [error])
|
||||
|
||||
return setError
|
||||
}
|
||||
|
||||
// Hook for catching async errors
|
||||
export function useAsyncError() {
|
||||
const [, setError] = useState()
|
||||
|
||||
return useCallback(
|
||||
(error: Error) => {
|
||||
setError(() => {
|
||||
throw error
|
||||
})
|
||||
},
|
||||
[setError],
|
||||
)
|
||||
}
|
||||
|
||||
// HOC for wrapping components with error boundary
|
||||
export function withErrorBoundary<P extends object>(
|
||||
Component: React.ComponentType<P>,
|
||||
errorBoundaryProps?: Omit<ErrorBoundaryProps, 'children'>,
|
||||
): React.ComponentType<P> {
|
||||
const WrappedComponent = (props: P) => (
|
||||
<ErrorBoundary {...errorBoundaryProps}>
|
||||
<Component {...props} />
|
||||
</ErrorBoundary>
|
||||
)
|
||||
|
||||
WrappedComponent.displayName = `withErrorBoundary(${Component.displayName || Component.name || 'Component'})`
|
||||
|
||||
return WrappedComponent
|
||||
}
|
||||
|
||||
// Simple error fallback component
|
||||
export const ErrorFallback: React.FC<{
|
||||
error: Error
|
||||
resetErrorBoundary: () => void
|
||||
}> = ({ error, resetErrorBoundary }) => {
|
||||
return (
|
||||
<div className='flex min-h-[200px] flex-col items-center justify-center rounded-lg border border-red-200 bg-red-50 p-8'>
|
||||
<h2 className='mb-2 text-lg font-semibold text-red-800'>Oops! Something went wrong</h2>
|
||||
<p className='mb-4 text-center text-red-600'>{error.message}</p>
|
||||
<Button onClick={resetErrorBoundary} size='small'>
|
||||
Try again
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ErrorBoundary
|
||||
|
|
@ -1,18 +1,18 @@
|
|||
'use client'
|
||||
import React, { useEffect } from 'react'
|
||||
import type { FC } from 'react'
|
||||
import DetailHeader from './detail-header'
|
||||
import EndpointList from './endpoint-list'
|
||||
import ActionList from './action-list'
|
||||
import DatasourceActionList from './datasource-action-list'
|
||||
import ModelList from './model-list'
|
||||
import AgentStrategyList from './agent-strategy-list'
|
||||
import { SubscriptionList } from './subscription-list'
|
||||
import { TriggerEventsList } from './trigger-events-list'
|
||||
import Drawer from '@/app/components/base/drawer'
|
||||
import { type PluginDetail, PluginType } from '@/app/components/plugins/types'
|
||||
import cn from '@/utils/classnames'
|
||||
import { usePluginStore } from './store'
|
||||
import type { FC } from 'react'
|
||||
import { useEffect } from 'react'
|
||||
import ActionList from './action-list'
|
||||
import AgentStrategyList from './agent-strategy-list'
|
||||
import DatasourceActionList from './datasource-action-list'
|
||||
import DetailHeader from './detail-header'
|
||||
import EndpointList from './endpoint-list'
|
||||
import ModelList from './model-list'
|
||||
import { SubscriptionList } from './subscription-list'
|
||||
import { usePluginStore } from './subscription-list/store'
|
||||
import { TriggerEventsList } from './trigger-events-list'
|
||||
|
||||
type Props = {
|
||||
detail?: PluginDetail
|
||||
|
|
@ -33,8 +33,13 @@ const PluginDetailPanel: FC<Props> = ({
|
|||
const { setDetail } = usePluginStore()
|
||||
|
||||
useEffect(() => {
|
||||
if (detail)
|
||||
setDetail(detail)
|
||||
if (detail) {
|
||||
setDetail({
|
||||
plugin_id: detail.plugin_id,
|
||||
provider: `${detail.plugin_id}/${detail.declaration.name}`,
|
||||
declaration: detail.declaration,
|
||||
})
|
||||
}
|
||||
}, [detail])
|
||||
|
||||
if (!detail)
|
||||
|
|
|
|||
|
|
@ -1,22 +0,0 @@
|
|||
import { create } from 'zustand'
|
||||
import type { PluginDetail } from '../types'
|
||||
|
||||
type Shape = {
|
||||
detail: PluginDetail | undefined
|
||||
setDetail: (detail: PluginDetail) => void
|
||||
}
|
||||
|
||||
export const usePluginStore = create<Shape>(set => ({
|
||||
detail: undefined,
|
||||
setDetail: (detail: PluginDetail) => set({ detail }),
|
||||
}))
|
||||
|
||||
type ShapeSubscription = {
|
||||
refresh?: () => void
|
||||
setRefresh: (refresh: () => void) => void
|
||||
}
|
||||
|
||||
export const usePluginSubscriptionStore = create<ShapeSubscription>(set => ({
|
||||
refresh: undefined,
|
||||
setRefresh: (refresh: () => void) => set({ refresh }),
|
||||
}))
|
||||
|
|
@ -17,8 +17,8 @@ import {
|
|||
import { RiLoader2Line } from '@remixicon/react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { usePluginStore, usePluginSubscriptionStore } from '../../store'
|
||||
import LogViewer from '../log-viewer'
|
||||
import { usePluginStore, usePluginSubscriptionStore } from '../store'
|
||||
|
||||
type Props = {
|
||||
onClose: () => void
|
||||
|
|
@ -72,7 +72,6 @@ export const CommonCreateModal = ({ onClose, createType, builder }: Props) => {
|
|||
const { mutate: createBuilder /* isPending: isCreatingBuilder */ } = useCreateTriggerSubscriptionBuilder()
|
||||
const { mutate: buildSubscription, isPending: isBuilding } = useBuildTriggerSubscription()
|
||||
|
||||
const providerName = `${detail?.plugin_id}/${detail?.declaration.name}`
|
||||
const propertiesSchema = detail?.declaration.trigger.subscription_schema.properties_schema || [] // manual
|
||||
const subscriptionFormRef = React.useRef<FormRefObject>(null)
|
||||
const propertiesFormRef = React.useRef<FormRefObject>(null)
|
||||
|
|
@ -82,7 +81,7 @@ export const CommonCreateModal = ({ onClose, createType, builder }: Props) => {
|
|||
const credentialsFormRef = React.useRef<FormRefObject>(null)
|
||||
|
||||
const { data: logData } = useTriggerSubscriptionBuilderLogs(
|
||||
providerName,
|
||||
detail?.provider || '',
|
||||
subscriptionBuilder?.id || '',
|
||||
{
|
||||
enabled: createType === SupportedCreationMethods.MANUAL && !!subscriptionBuilder?.id,
|
||||
|
|
@ -94,7 +93,7 @@ export const CommonCreateModal = ({ onClose, createType, builder }: Props) => {
|
|||
if (!subscriptionBuilder) {
|
||||
createBuilder(
|
||||
{
|
||||
provider: providerName,
|
||||
provider: detail?.provider || '',
|
||||
credential_type: CREDENTIAL_TYPE_MAP[createType],
|
||||
},
|
||||
{
|
||||
|
|
@ -112,7 +111,7 @@ export const CommonCreateModal = ({ onClose, createType, builder }: Props) => {
|
|||
},
|
||||
)
|
||||
}
|
||||
}, [createBuilder, providerName, subscriptionBuilder, t])
|
||||
}, [createBuilder, detail?.provider, subscriptionBuilder, t])
|
||||
|
||||
const handleVerify = () => {
|
||||
const credentialsFormValues = credentialsFormRef.current?.getFormValues({}) || { values: {}, isCheckValidated: false }
|
||||
|
|
@ -130,7 +129,7 @@ export const CommonCreateModal = ({ onClose, createType, builder }: Props) => {
|
|||
|
||||
verifyCredentials(
|
||||
{
|
||||
provider: providerName,
|
||||
provider: detail?.provider || '',
|
||||
subscriptionBuilderId: subscriptionBuilder?.id || '',
|
||||
credentials,
|
||||
},
|
||||
|
|
@ -164,7 +163,7 @@ export const CommonCreateModal = ({ onClose, createType, builder }: Props) => {
|
|||
|
||||
buildSubscription(
|
||||
{
|
||||
provider: providerName,
|
||||
provider: detail?.provider || '',
|
||||
subscriptionBuilderId: subscriptionBuilder.id,
|
||||
name: subscriptionNameValue,
|
||||
parameters: { ...parameterForm.values, events: ['*'] },
|
||||
|
|
@ -267,11 +266,11 @@ export const CommonCreateModal = ({ onClose, createType, builder }: Props) => {
|
|||
</div> */}
|
||||
{createType !== SupportedCreationMethods.MANUAL && parametersSchema.length > 0 && (
|
||||
<BaseForm
|
||||
formSchemas={parametersSchema.map(schema => ({
|
||||
formSchemas={parametersSchema.map((schema: { type: FormTypeEnum; name: any }) => ({
|
||||
...schema,
|
||||
dynamicSelectParams: schema.type === FormTypeEnum.dynamicSelect ? {
|
||||
plugin_id: detail?.plugin_id || '',
|
||||
provider: providerName,
|
||||
provider: detail?.provider || '',
|
||||
action: 'provider',
|
||||
parameter: schema.name,
|
||||
credential_id: subscriptionBuilder?.id || '',
|
||||
|
|
@ -306,7 +305,7 @@ export const CommonCreateModal = ({ onClose, createType, builder }: Props) => {
|
|||
<RiLoader2Line className='h-full w-full animate-spin' />
|
||||
</div>
|
||||
<div className='system-xs-regular text-text-tertiary'>
|
||||
Awaiting request from {detail?.declaration.name}...
|
||||
Awaiting request from {detail?.plugin_id}...
|
||||
</div>
|
||||
</div>
|
||||
<LogViewer logs={logData?.logs || []} />
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import { useBoolean } from 'ahooks'
|
|||
import { useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { SupportedCreationMethods } from '../../../types'
|
||||
import { usePluginStore } from '../../store'
|
||||
import { usePluginStore } from '../store'
|
||||
import { CommonCreateModal } from './common-modal'
|
||||
import { OAuthClientSettingsModal } from './oauth-client'
|
||||
|
||||
|
|
@ -35,11 +35,10 @@ export const CreateSubscriptionButton = ({ buttonType = CreateButtonType.FULL_BU
|
|||
const [selectedCreateInfo, setSelectedCreateInfo] = useState<{ type: SupportedCreationMethods, builder?: TriggerSubscriptionBuilder } | null>(null)
|
||||
|
||||
const detail = usePluginStore(state => state.detail)
|
||||
const provider = `${detail?.plugin_id}/${detail?.declaration.name}`
|
||||
|
||||
const { data: providerInfo } = useTriggerProviderInfo(provider, !!detail?.plugin_id && !!detail?.declaration.name)
|
||||
const { data: providerInfo } = useTriggerProviderInfo(detail?.provider || '')
|
||||
const supportedMethods = providerInfo?.supported_creation_methods || []
|
||||
const { data: oauthConfig } = useTriggerOAuthConfig(provider, supportedMethods.includes(SupportedCreationMethods.OAUTH))
|
||||
const { data: oauthConfig } = useTriggerOAuthConfig(detail?.provider || '', supportedMethods.includes(SupportedCreationMethods.OAUTH))
|
||||
const { mutate: initiateOAuth } = useInitiateTriggerOAuth()
|
||||
|
||||
const methodType = supportedMethods.length === 1 ? supportedMethods[0] : DEFAULT_METHOD
|
||||
|
|
@ -94,7 +93,7 @@ export const CreateSubscriptionButton = ({ buttonType = CreateButtonType.FULL_BU
|
|||
const onChooseCreateType = (type: SupportedCreationMethods) => {
|
||||
if (type === SupportedCreationMethods.OAUTH) {
|
||||
if (oauthConfig?.configured) {
|
||||
initiateOAuth(provider, {
|
||||
initiateOAuth(detail?.provider || '', {
|
||||
onSuccess: (response) => {
|
||||
openOAuthPopup(response.authorization_url, (callbackData) => {
|
||||
if (callbackData) {
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ import {
|
|||
} from '@remixicon/react'
|
||||
import React, { useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { usePluginStore } from '../../store'
|
||||
import { usePluginStore } from '../store'
|
||||
|
||||
type Props = {
|
||||
oauthConfig?: TriggerOAuthConfig
|
||||
|
|
|
|||
|
|
@ -0,0 +1,46 @@
|
|||
import Confirm from '@/app/components/base/confirm'
|
||||
import { usePluginSubscriptionStore } from './store'
|
||||
import { useDeleteTriggerSubscription } from '@/service/use-triggers'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
|
||||
type Props = {
|
||||
onClose: (deleted: boolean) => void
|
||||
isShow: boolean
|
||||
currentId: string
|
||||
currentName: string
|
||||
}
|
||||
|
||||
export const DeleteConfirm = (props: Props) => {
|
||||
const { onClose, isShow, currentId, currentName } = props
|
||||
const { refresh } = usePluginSubscriptionStore()
|
||||
const { mutate: deleteSubscription, isPending: isDeleting } = useDeleteTriggerSubscription()
|
||||
const { t } = useTranslation()
|
||||
|
||||
const onConfirm = () => {
|
||||
deleteSubscription(currentId, {
|
||||
onSuccess: () => {
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: t('pluginTrigger.subscription.list.item.actions.deleteConfirm.title'),
|
||||
})
|
||||
refresh?.()
|
||||
onClose(true)
|
||||
},
|
||||
onError: (error: any) => {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: error?.message || 'Failed to delete subscription',
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
return <Confirm
|
||||
title={t('pluginTrigger.subscription.list.item.actions.deleteConfirm.title')}
|
||||
content={t('pluginTrigger.subscription.list.item.actions.deleteConfirm.content', { name: currentName })}
|
||||
isShow={isShow}
|
||||
isLoading={isDeleting}
|
||||
onConfirm={onConfirm}
|
||||
onCancel={() => onClose(false)}
|
||||
/>
|
||||
}
|
||||
|
|
@ -1,64 +1,50 @@
|
|||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { useTriggerSubscriptions } from '@/service/use-triggers'
|
||||
import cn from '@/utils/classnames'
|
||||
import { useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { usePluginStore, usePluginSubscriptionStore } from '../store'
|
||||
import { CreateButtonType, CreateSubscriptionButton } from './create'
|
||||
import SubscriptionCard from './subscription-card'
|
||||
import { withErrorBoundary } from '@/app/components/base/error-boundary'
|
||||
import { SubscriptionListView } from './list-view'
|
||||
import { SubscriptionSelectorView } from './selector-view'
|
||||
import { useSubscriptionList } from './use-subscription-list'
|
||||
|
||||
export const SubscriptionList = () => {
|
||||
const { t } = useTranslation()
|
||||
const detail = usePluginStore(state => state.detail)
|
||||
export enum SubscriptionListMode {
|
||||
PANEL = 'panel',
|
||||
SELECTOR = 'selector',
|
||||
}
|
||||
|
||||
const showTopBorder = detail?.declaration.tool || detail?.declaration.endpoint
|
||||
const provider = `${detail?.plugin_id}/${detail?.declaration.name}`
|
||||
type SubscriptionListProps = {
|
||||
mode?: SubscriptionListMode
|
||||
selectedId?: string
|
||||
onSelect?: ({ id, name }: { id: string, name: string }) => void
|
||||
}
|
||||
|
||||
const { data: subscriptions, isLoading, refetch } = useTriggerSubscriptions(provider, !!detail?.plugin_id && !!detail?.declaration.name)
|
||||
export { SubscriptionSelectorEntry } from './selector-entry'
|
||||
|
||||
const { setRefresh } = usePluginSubscriptionStore()
|
||||
export const SubscriptionList = withErrorBoundary(({
|
||||
mode = SubscriptionListMode.PANEL,
|
||||
selectedId,
|
||||
onSelect,
|
||||
}: SubscriptionListProps) => {
|
||||
const { subscriptions, isLoading, hasSubscriptions } = useSubscriptionList()
|
||||
|
||||
useEffect(() => {
|
||||
if (refetch)
|
||||
setRefresh(refetch)
|
||||
}, [refetch])
|
||||
// console.log('detail', detail)
|
||||
|
||||
if (isLoading) {
|
||||
if (mode === SubscriptionListMode.SELECTOR) {
|
||||
return (
|
||||
<div className={cn('border-divider-subtle px-4 py-2', showTopBorder && 'border-t')}>
|
||||
<div className='flex items-center justify-center py-8'>
|
||||
<div className='text-text-tertiary'>{t('common.dataLoading')}</div>
|
||||
</div>
|
||||
</div>
|
||||
<SubscriptionSelectorView
|
||||
subscriptions={subscriptions}
|
||||
isLoading={isLoading}
|
||||
hasSubscriptions={hasSubscriptions}
|
||||
selectedId={selectedId}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const hasSubscriptions = subscriptions && subscriptions.length > 0
|
||||
// const showTopBorder = !!(detail?.declaration?.tool || detail?.declaration?.endpoint)
|
||||
|
||||
return (
|
||||
<div className={cn('border-divider-subtle px-4 py-2', showTopBorder && 'border-t')}>
|
||||
<div className='relative mb-3 flex items-center justify-between'>
|
||||
{
|
||||
hasSubscriptions
|
||||
&& <div className='flex shrink-0 items-center gap-1'>
|
||||
<span className='system-sm-semibold-uppercase text-text-secondary'>
|
||||
{t('pluginTrigger.subscription.listNum', { num: subscriptions?.length || 0 })}
|
||||
</span>
|
||||
<Tooltip popupContent={t('pluginTrigger.subscription.list.tip')} />
|
||||
</div>
|
||||
}
|
||||
<CreateSubscriptionButton buttonType={hasSubscriptions ? CreateButtonType.ICON_BUTTON : CreateButtonType.FULL_BUTTON} />
|
||||
</div>
|
||||
|
||||
{hasSubscriptions
|
||||
&& <div className='flex flex-col gap-1'>
|
||||
{subscriptions?.map(subscription => (
|
||||
<SubscriptionCard
|
||||
key={subscription.id}
|
||||
data={subscription}
|
||||
/>
|
||||
))}
|
||||
</div>}
|
||||
</div>
|
||||
<SubscriptionListView
|
||||
subscriptions={subscriptions}
|
||||
isLoading={isLoading}
|
||||
// showTopBorder={showTopBorder}
|
||||
hasSubscriptions={hasSubscriptions}
|
||||
/>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -0,0 +1,63 @@
|
|||
'use client'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import cn from '@/utils/classnames'
|
||||
import { CreateButtonType, CreateSubscriptionButton } from './create'
|
||||
import SubscriptionCard from './subscription-card'
|
||||
import type { TriggerSubscription } from '@/app/components/workflow/block-selector/types'
|
||||
|
||||
type SubscriptionListViewProps = {
|
||||
subscriptions?: TriggerSubscription[]
|
||||
isLoading: boolean
|
||||
showTopBorder?: boolean
|
||||
hasSubscriptions: boolean
|
||||
}
|
||||
|
||||
export const SubscriptionListView: React.FC<SubscriptionListViewProps> = ({
|
||||
subscriptions,
|
||||
isLoading,
|
||||
showTopBorder = false,
|
||||
hasSubscriptions,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className={cn('border-divider-subtle px-4 py-2', showTopBorder && 'border-t')}>
|
||||
<div className='flex items-center justify-center py-8'>
|
||||
<div className='text-text-tertiary'>{t('common.dataLoading')}</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn('border-divider-subtle px-4 py-2', showTopBorder && 'border-t')}>
|
||||
<div className='relative mb-3 flex items-center justify-between'>
|
||||
{hasSubscriptions && (
|
||||
<div className='flex shrink-0 items-center gap-1'>
|
||||
<span className='system-sm-semibold-uppercase text-text-secondary'>
|
||||
{t('pluginTrigger.subscription.listNum', { num: subscriptions?.length || 0 })}
|
||||
</span>
|
||||
<Tooltip popupContent={t('pluginTrigger.subscription.list.tip')} />
|
||||
</div>
|
||||
)}
|
||||
<CreateSubscriptionButton
|
||||
buttonType={hasSubscriptions ? CreateButtonType.ICON_BUTTON : CreateButtonType.FULL_BUTTON}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{hasSubscriptions && (
|
||||
<div className='flex flex-col gap-1'>
|
||||
{subscriptions?.map(subscription => (
|
||||
<SubscriptionCard
|
||||
key={subscription.id}
|
||||
data={subscription}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
'use client'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import Indicator from '@/app/components/header/indicator'
|
||||
import { SubscriptionList, SubscriptionListMode } from '@/app/components/plugins/plugin-detail-panel/subscription-list'
|
||||
import cn from '@/utils/classnames'
|
||||
import { RiArrowDownSLine } from '@remixicon/react'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useSubscriptionList } from './use-subscription-list'
|
||||
|
||||
type SubscriptionTriggerButtonProps = {
|
||||
selectedId?: string
|
||||
onClick?: () => void
|
||||
isOpen?: boolean
|
||||
className?: string
|
||||
}
|
||||
|
||||
const SubscriptionTriggerButton: React.FC<SubscriptionTriggerButtonProps> = ({
|
||||
selectedId,
|
||||
onClick,
|
||||
isOpen = false,
|
||||
className,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { subscriptions } = useSubscriptionList()
|
||||
|
||||
const statusConfig = useMemo(() => {
|
||||
if (!selectedId) {
|
||||
if (isOpen) {
|
||||
return {
|
||||
label: t('workflow.nodes.triggerPlugin.selectSubscription'),
|
||||
color: 'yellow' as const,
|
||||
}
|
||||
}
|
||||
return {
|
||||
label: 'No subscription selected',
|
||||
color: 'red' as const,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
label: subscriptions?.find(sub => sub.id === selectedId)?.name || '--',
|
||||
color: 'green' as const,
|
||||
}
|
||||
}, [selectedId, subscriptions, t, isOpen])
|
||||
|
||||
return (
|
||||
<button
|
||||
className={cn(
|
||||
'flex h-8 items-center gap-1 rounded-lg px-2 transition-colors',
|
||||
'hover:bg-state-base-hover-alt',
|
||||
isOpen && 'bg-state-base-hover-alt',
|
||||
className,
|
||||
)}
|
||||
onClick={onClick}
|
||||
>
|
||||
<Indicator
|
||||
className='shrink-0'
|
||||
color={statusConfig.color}
|
||||
/>
|
||||
<span className={cn('system-xs-medium truncate text-components-button-ghost-text', statusConfig.color === 'red' && 'text-components-button-destructive-secondary-text')}>
|
||||
{statusConfig.label}
|
||||
</span>
|
||||
<RiArrowDownSLine
|
||||
className={cn(
|
||||
'ml-auto h-4 w-4 shrink-0 text-text-quaternary transition-transform',
|
||||
isOpen && 'rotate-180',
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
export const SubscriptionSelectorEntry = ({ selectedId, onSelect }: {
|
||||
selectedId?: string,
|
||||
onSelect: ({ id, name }: { id: string, name: string }) => void
|
||||
}) => {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
|
||||
return <PortalToFollowElem
|
||||
placement='bottom-start'
|
||||
offset={4}
|
||||
open={isOpen}
|
||||
onOpenChange={setIsOpen}
|
||||
>
|
||||
<PortalToFollowElemTrigger asChild>
|
||||
<div>
|
||||
<SubscriptionTriggerButton
|
||||
selectedId={selectedId}
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
isOpen={isOpen}
|
||||
/>
|
||||
</div>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className='z-[11]'>
|
||||
<div className='rounded-xl border border-components-panel-border bg-components-panel-bg shadow-lg'>
|
||||
<SubscriptionList
|
||||
mode={SubscriptionListMode.SELECTOR}
|
||||
selectedId={selectedId}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
}
|
||||
|
|
@ -0,0 +1,112 @@
|
|||
'use client'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import Indicator from '@/app/components/header/indicator'
|
||||
import type { TriggerSubscription } from '@/app/components/workflow/block-selector/types'
|
||||
import cn from '@/utils/classnames'
|
||||
import { RiCheckLine, RiDeleteBinLine } from '@remixicon/react'
|
||||
import React, { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { CreateButtonType, CreateSubscriptionButton } from './create'
|
||||
import { DeleteConfirm } from './delete-confirm'
|
||||
|
||||
type SubscriptionSelectorProps = {
|
||||
subscriptions?: TriggerSubscription[]
|
||||
isLoading: boolean
|
||||
hasSubscriptions: boolean
|
||||
selectedId?: string
|
||||
onSelect?: ({ id, name }: { id: string, name: string }) => void
|
||||
}
|
||||
|
||||
export const SubscriptionSelectorView: React.FC<SubscriptionSelectorProps> = ({
|
||||
subscriptions,
|
||||
isLoading,
|
||||
hasSubscriptions,
|
||||
selectedId,
|
||||
onSelect,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [deletedSubscription, setDeletedSubscription] = useState<TriggerSubscription | null>(null)
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className='flex items-center justify-center py-8'>
|
||||
<div className='text-text-tertiary'>{t('common.dataLoading')}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='w-[320px] p-1'>
|
||||
{hasSubscriptions && <div className='ml-7 mr-1.5 mt-0.5 flex items-center justify-between'>
|
||||
<div className='flex shrink-0 items-center gap-1'>
|
||||
<span className='system-sm-semibold-uppercase text-text-secondary'>
|
||||
{t('pluginTrigger.subscription.listNum', { num: subscriptions?.length || 0 })}
|
||||
</span>
|
||||
<Tooltip popupContent={t('pluginTrigger.subscription.list.tip')} />
|
||||
</div>
|
||||
<CreateSubscriptionButton
|
||||
buttonType={CreateButtonType.ICON_BUTTON}
|
||||
/>
|
||||
</div>}
|
||||
<div className='max-h-[320px] overflow-y-auto'>
|
||||
{hasSubscriptions ? (
|
||||
<>
|
||||
{subscriptions?.map(subscription => (
|
||||
<button
|
||||
key={subscription.id}
|
||||
className={cn(
|
||||
'group flex w-full items-center justify-between rounded-lg p-1 text-left transition-colors',
|
||||
'hover:bg-state-base-hover has-[.subscription-delete-btn:hover]:!bg-state-destructive-hover',
|
||||
selectedId === subscription.id && 'bg-state-base-hover',
|
||||
)}
|
||||
onClick={() => onSelect?.(subscription)}
|
||||
>
|
||||
<div className='flex items-center'>
|
||||
{selectedId === subscription.id && (
|
||||
<RiCheckLine className='mr-2 h-4 w-4 shrink-0 text-text-accent' />
|
||||
)}
|
||||
<Indicator
|
||||
color={subscription.properties?.active !== false ? 'green' : 'red'}
|
||||
className={cn('mr-1.5', selectedId !== subscription.id && 'ml-6')}
|
||||
/>
|
||||
<span className='system-md-regular leading-6 text-text-secondary'>
|
||||
{subscription.name}
|
||||
</span>
|
||||
</div>
|
||||
<ActionButton onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
setDeletedSubscription(subscription)
|
||||
}} className='subscription-delete-btn hidden shrink-0 text-text-tertiary hover:bg-state-destructive-hover hover:text-text-destructive group-hover:flex'>
|
||||
<RiDeleteBinLine className='size-4' />
|
||||
</ActionButton>
|
||||
</button>
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
// todo: refactor this
|
||||
<div className='p-2 text-center'>
|
||||
<div className='mb-2 text-sm text-text-tertiary'>
|
||||
{t('pluginTrigger.subscription.empty.description')}
|
||||
</div>
|
||||
<CreateSubscriptionButton
|
||||
buttonType={CreateButtonType.FULL_BUTTON}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{deletedSubscription && (
|
||||
<DeleteConfirm
|
||||
onClose={(deleted) => {
|
||||
if (deleted)
|
||||
onSelect?.({ id: '', name: '' })
|
||||
setDeletedSubscription(null)
|
||||
}}
|
||||
isShow={!!deletedSubscription}
|
||||
currentId={deletedSubscription.id}
|
||||
currentName={deletedSubscription.name}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
import { create } from 'zustand'
|
||||
|
||||
export type SubscriptionListDetail = {
|
||||
plugin_id: string
|
||||
// name: string
|
||||
provider: string
|
||||
declaration: {
|
||||
tool?: any
|
||||
endpoint?: any
|
||||
trigger?: any
|
||||
name?: string
|
||||
meta?: {
|
||||
version?: string
|
||||
}
|
||||
}
|
||||
version?: string
|
||||
}
|
||||
|
||||
type Shape = {
|
||||
detail: SubscriptionListDetail | undefined
|
||||
setDetail: (detail: SubscriptionListDetail) => void
|
||||
}
|
||||
|
||||
export const usePluginStore = create<Shape>(set => ({
|
||||
detail: undefined,
|
||||
setDetail: (detail: SubscriptionListDetail) => set({ detail }),
|
||||
}))
|
||||
|
||||
type ShapeSubscription = {
|
||||
refresh?: () => void
|
||||
setRefresh: (refresh: () => void) => void
|
||||
}
|
||||
|
||||
export const usePluginSubscriptionStore = create<ShapeSubscription>(set => ({
|
||||
refresh: undefined,
|
||||
setRefresh: (refresh: () => void) => set({ refresh }),
|
||||
}))
|
||||
|
|
@ -1,10 +1,7 @@
|
|||
'use client'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import Confirm from '@/app/components/base/confirm'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import Indicator from '@/app/components/header/indicator'
|
||||
import type { TriggerSubscription } from '@/app/components/workflow/block-selector/types'
|
||||
import { useDeleteTriggerSubscription } from '@/service/use-triggers'
|
||||
import cn from '@/utils/classnames'
|
||||
import {
|
||||
RiDeleteBinLine,
|
||||
|
|
@ -12,7 +9,7 @@ import {
|
|||
} from '@remixicon/react'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { usePluginSubscriptionStore } from '../store'
|
||||
import { DeleteConfirm } from './delete-confirm'
|
||||
|
||||
type Props = {
|
||||
data: TriggerSubscription
|
||||
|
|
@ -24,28 +21,6 @@ const SubscriptionCard = ({ data }: Props) => {
|
|||
setTrue: showDeleteModal,
|
||||
setFalse: hideDeleteModal,
|
||||
}] = useBoolean(false)
|
||||
const { refresh } = usePluginSubscriptionStore()
|
||||
|
||||
const { mutate: deleteSubscription, isPending: isDeleting } = useDeleteTriggerSubscription()
|
||||
|
||||
const handleDelete = () => {
|
||||
deleteSubscription(data.id, {
|
||||
onSuccess: () => {
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: t('pluginTrigger.subscription.list.item.actions.deleteConfirm.title'),
|
||||
})
|
||||
refresh?.()
|
||||
hideDeleteModal()
|
||||
},
|
||||
onError: (error: any) => {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: error?.message || 'Failed to delete subscription',
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const isActive = data.properties?.active !== false
|
||||
|
||||
|
|
@ -96,13 +71,11 @@ const SubscriptionCard = ({ data }: Props) => {
|
|||
</div>
|
||||
|
||||
{isShowDeleteModal && (
|
||||
<Confirm
|
||||
title={t('pluginTrigger.subscription.list.item.actions.deleteConfirm.title')}
|
||||
content={t('pluginTrigger.subscription.list.item.actions.deleteConfirm.content', { name: data.name })}
|
||||
<DeleteConfirm
|
||||
onClose={hideDeleteModal}
|
||||
isShow={isShowDeleteModal}
|
||||
onConfirm={handleDelete}
|
||||
onCancel={hideDeleteModal}
|
||||
isLoading={isDeleting}
|
||||
currentId={data.id}
|
||||
currentName={data.name}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -1,237 +0,0 @@
|
|||
'use client'
|
||||
import React, { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
RiCloseLine,
|
||||
RiEditLine,
|
||||
RiKeyLine,
|
||||
RiUserLine,
|
||||
} from '@remixicon/react'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Input from '@/app/components/base/input'
|
||||
import type { PluginDetail } from '@/app/components/plugins/types'
|
||||
|
||||
type Props = {
|
||||
pluginDetail: PluginDetail
|
||||
onCancel: () => void
|
||||
onSaved: (data: any) => void
|
||||
}
|
||||
|
||||
type CreateMode = 'api-key' | 'oauth' | 'manual'
|
||||
|
||||
const SubscriptionModal = ({ pluginDetail, onCancel, onSaved }: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const [selectedMode, setSelectedMode] = useState<CreateMode | null>(null)
|
||||
const [subscriptionName, setSubscriptionName] = useState('')
|
||||
const [apiKey, setApiKey] = useState('')
|
||||
const [webhookUrl, setWebhookUrl] = useState('')
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
const handleModeSelect = (mode: CreateMode) => {
|
||||
setSelectedMode(mode)
|
||||
}
|
||||
|
||||
const handleBack = () => {
|
||||
setSelectedMode(null)
|
||||
}
|
||||
|
||||
const handleCreate = async () => {
|
||||
if (!selectedMode || !subscriptionName.trim()) return
|
||||
|
||||
setIsLoading(true)
|
||||
try {
|
||||
const subscriptionData = {
|
||||
name: subscriptionName,
|
||||
mode: selectedMode,
|
||||
plugin_id: pluginDetail.plugin_id,
|
||||
...(selectedMode === 'api-key' && { api_key: apiKey }),
|
||||
...(selectedMode === 'manual' && { webhook_url: webhookUrl }),
|
||||
}
|
||||
|
||||
onSaved(subscriptionData)
|
||||
}
|
||||
finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const canCreate = subscriptionName.trim() && (
|
||||
selectedMode === 'oauth'
|
||||
|| (selectedMode === 'api-key' && apiKey.trim())
|
||||
|| (selectedMode === 'manual' && webhookUrl.trim())
|
||||
)
|
||||
|
||||
if (!selectedMode) {
|
||||
return (
|
||||
<Modal
|
||||
isShow
|
||||
onClose={onCancel}
|
||||
className='!max-w-[520px] !p-0'
|
||||
>
|
||||
<div className='flex items-center justify-between p-6 pb-4'>
|
||||
<h3 className='text-lg font-semibold text-text-primary'>
|
||||
{t('plugin.detailPanel.createSubscription')}
|
||||
</h3>
|
||||
<Button variant='ghost' size='small' onClick={onCancel}>
|
||||
<RiCloseLine className='h-4 w-4' />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className='px-6 pb-2'>
|
||||
<p className='system-sm-regular mb-4 text-text-secondary'>
|
||||
{t('plugin.detailPanel.createSubscriptionDesc')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className='px-6 pb-6'>
|
||||
<div className='space-y-3'>
|
||||
<button
|
||||
onClick={() => handleModeSelect('api-key')}
|
||||
className='flex w-full items-center gap-3 rounded-lg border border-components-panel-border p-4 text-left transition-colors hover:bg-background-default-hover'
|
||||
>
|
||||
<div className='flex h-10 w-10 items-center justify-center rounded-lg bg-background-default-subtle'>
|
||||
<RiKeyLine className='h-5 w-5 text-text-warning' />
|
||||
</div>
|
||||
<div className='flex-1'>
|
||||
<div className='system-sm-semibold text-text-primary'>
|
||||
{t('plugin.detailPanel.createViaApiKey')}
|
||||
</div>
|
||||
<div className='system-xs-regular text-text-tertiary'>
|
||||
{t('plugin.detailPanel.createViaApiKeyDesc')}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => handleModeSelect('oauth')}
|
||||
className='flex w-full items-center gap-3 rounded-lg border border-components-panel-border p-4 text-left transition-colors hover:bg-background-default-hover'
|
||||
>
|
||||
<div className='flex h-10 w-10 items-center justify-center rounded-lg bg-background-default-subtle'>
|
||||
<RiUserLine className='h-5 w-5 text-text-accent' />
|
||||
</div>
|
||||
<div className='flex-1'>
|
||||
<div className='system-sm-semibold text-text-primary'>
|
||||
{t('plugin.detailPanel.createViaOAuth')}
|
||||
</div>
|
||||
<div className='system-xs-regular text-text-tertiary'>
|
||||
{t('plugin.detailPanel.createViaOAuthDesc')}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => handleModeSelect('manual')}
|
||||
className='flex w-full items-center gap-3 rounded-lg border border-components-panel-border p-4 text-left transition-colors hover:bg-background-default-hover'
|
||||
>
|
||||
<div className='flex h-10 w-10 items-center justify-center rounded-lg bg-background-default-subtle'>
|
||||
<RiEditLine className='h-5 w-5 text-text-secondary' />
|
||||
</div>
|
||||
<div className='flex-1'>
|
||||
<div className='system-sm-semibold text-text-primary'>
|
||||
{t('plugin.detailPanel.createManual')}
|
||||
</div>
|
||||
<div className='system-xs-regular text-text-tertiary'>
|
||||
{t('plugin.detailPanel.createManualDesc')}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isShow
|
||||
onClose={onCancel}
|
||||
className='!max-w-[520px] !p-0'
|
||||
>
|
||||
<div className='flex items-center justify-between p-6 pb-4'>
|
||||
<div className='flex items-center gap-3'>
|
||||
<Button variant='ghost' size='small' onClick={handleBack}>
|
||||
←
|
||||
</Button>
|
||||
<h3 className='text-lg font-semibold text-text-primary'>
|
||||
{selectedMode === 'api-key' && t('plugin.detailPanel.createViaApiKey')}
|
||||
{selectedMode === 'oauth' && t('plugin.detailPanel.createViaOAuth')}
|
||||
{selectedMode === 'manual' && t('plugin.detailPanel.createManual')}
|
||||
</h3>
|
||||
</div>
|
||||
<Button variant='ghost' size='small' onClick={onCancel}>
|
||||
<RiCloseLine className='h-4 w-4' />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className='px-6 pb-6'>
|
||||
<div className='space-y-4'>
|
||||
<div>
|
||||
<label className='system-sm-medium mb-2 block text-text-primary'>
|
||||
{t('plugin.detailPanel.subscriptionName')}
|
||||
</label>
|
||||
<Input
|
||||
value={subscriptionName}
|
||||
onChange={e => setSubscriptionName(e.target.value)}
|
||||
placeholder={t('plugin.detailPanel.subscriptionNamePlaceholder')}
|
||||
className='w-full'
|
||||
/>
|
||||
</div>
|
||||
|
||||
{selectedMode === 'api-key' && (
|
||||
<div>
|
||||
<label className='system-sm-medium mb-2 block text-text-primary'>
|
||||
{t('plugin.detailPanel.apiKey')}
|
||||
</label>
|
||||
<Input
|
||||
type='password'
|
||||
value={apiKey}
|
||||
onChange={e => setApiKey(e.target.value)}
|
||||
placeholder={t('plugin.detailPanel.apiKeyPlaceholder')}
|
||||
className='w-full'
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedMode === 'oauth' && (
|
||||
<div className='rounded-lg bg-background-section p-4'>
|
||||
<p className='system-sm-regular text-text-secondary'>
|
||||
{t('plugin.detailPanel.oauthCreateNote')}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedMode === 'manual' && (
|
||||
<div>
|
||||
<label className='system-sm-medium mb-2 block text-text-primary'>
|
||||
{t('plugin.detailPanel.webhookUrl')}
|
||||
</label>
|
||||
<Input
|
||||
value={webhookUrl}
|
||||
onChange={e => setWebhookUrl(e.target.value)}
|
||||
placeholder={t('plugin.detailPanel.webhookUrlPlaceholder')}
|
||||
className='w-full'
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className='mt-6 flex justify-end gap-2'>
|
||||
<Button variant='secondary' onClick={onCancel}>
|
||||
{t('common.operation.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
variant='primary'
|
||||
onClick={handleCreate}
|
||||
disabled={!canCreate}
|
||||
loading={isLoading}
|
||||
>
|
||||
{t('common.operation.create')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default SubscriptionModal
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
import { useEffect } from 'react'
|
||||
import { useTriggerSubscriptions } from '@/service/use-triggers'
|
||||
import { usePluginStore, usePluginSubscriptionStore } from './store'
|
||||
|
||||
export const useSubscriptionList = () => {
|
||||
const detail = usePluginStore(state => state.detail)
|
||||
const { setRefresh } = usePluginSubscriptionStore()
|
||||
|
||||
const { data: subscriptions, isLoading, refetch } = useTriggerSubscriptions(detail?.provider || '', !!detail?.provider)
|
||||
|
||||
useEffect(() => {
|
||||
if (refetch)
|
||||
setRefresh(refetch)
|
||||
}, [refetch, setRefresh])
|
||||
|
||||
return {
|
||||
detail,
|
||||
subscriptions,
|
||||
isLoading,
|
||||
refetch,
|
||||
hasSubscriptions: !!(subscriptions && subscriptions.length > 0),
|
||||
}
|
||||
}
|
||||
|
|
@ -1,15 +1,16 @@
|
|||
import React, { useContext, useMemo } from 'react'
|
||||
import React, { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContextSelector } from 'use-context-selector'
|
||||
import I18n from '@/context/i18n'
|
||||
import { getLanguage } from '@/i18n-config/language'
|
||||
import ToolItem from '@/app/components/tools/provider/tool-item'
|
||||
import { usePluginStore } from './store'
|
||||
import { useTriggerProviderInfo } from '@/service/use-triggers'
|
||||
import type { Tool, ToolParameter } from '@/app/components/tools/types'
|
||||
import { CollectionType } from '@/app/components/tools/types'
|
||||
import type { ToolWithProvider } from '@/app/components/workflow/types'
|
||||
import type { TypeWithI18N } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import type { Trigger } from '@/app/components/plugins/types'
|
||||
import { usePluginStore } from './subscription-list/store'
|
||||
|
||||
type TriggerOption = {
|
||||
value: string
|
||||
|
|
@ -79,7 +80,7 @@ const toTool = (trigger: Trigger, fallbackAuthor: string): Tool => {
|
|||
|
||||
export const TriggerEventsList = () => {
|
||||
const { t } = useTranslation()
|
||||
const { locale } = useContext(I18n)
|
||||
const locale = useContextSelector(I18n, state => state.locale)
|
||||
const language = getLanguage(locale)
|
||||
const detail = usePluginStore(state => state.detail)
|
||||
const triggers = detail?.declaration.trigger?.triggers || []
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ import { useStore as useAppStore } from '@/app/components/app/store'
|
|||
import { useStore } from '@/app/components/workflow/store'
|
||||
import Tab, { TabType } from './tab'
|
||||
import { useAllTriggerPlugins } from '@/service/use-triggers'
|
||||
import AuthMethodSelector from '@/app/components/workflow/nodes/trigger-plugin/components/auth-method-selector'
|
||||
// import AuthMethodSelector from '@/app/components/workflow/nodes/trigger-plugin/components/auth-method-selector'
|
||||
import LastRun from './last-run'
|
||||
import useLastRun from './last-run/use-last-run'
|
||||
import BeforeRunForm from '../before-run-form'
|
||||
|
|
@ -275,20 +275,20 @@ const BasePanel: FC<BasePanelProps> = ({
|
|||
return triggerProviders.find(p => p.plugin_id === data.provider_id && p.name === data.provider_name)
|
||||
}, [data.type, data.provider_id, data.provider_name, triggerProviders])
|
||||
|
||||
const supportedAuthMethods = useMemo(() => {
|
||||
if (!currentTriggerProvider) return []
|
||||
const methods = []
|
||||
if (currentTriggerProvider.oauth_client_schema && currentTriggerProvider.oauth_client_schema.length > 0)
|
||||
methods.push('oauth')
|
||||
if (currentTriggerProvider.credentials_schema && currentTriggerProvider.credentials_schema.length > 0)
|
||||
methods.push('api_key')
|
||||
return methods
|
||||
}, [currentTriggerProvider])
|
||||
// const supportedAuthMethods = useMemo(() => {
|
||||
// if (!currentTriggerProvider) return []
|
||||
// const methods = []
|
||||
// if (currentTriggerProvider.oauth_client_schema && currentTriggerProvider.oauth_client_schema.length > 0)
|
||||
// methods.push('oauth')
|
||||
// if (currentTriggerProvider.credentials_schema && currentTriggerProvider.credentials_schema.length > 0)
|
||||
// methods.push('api_key')
|
||||
// return methods
|
||||
// }, [currentTriggerProvider])
|
||||
|
||||
// Simplified: Always show auth selector for trigger plugins
|
||||
const shouldShowTriggerAuthSelector = useMemo(() => {
|
||||
return data.type === BlockEnum.TriggerPlugin && currentTriggerProvider && supportedAuthMethods.length > 0
|
||||
}, [data.type, currentTriggerProvider, supportedAuthMethods.length])
|
||||
// const shouldShowTriggerAuthSelector = useMemo(() => {
|
||||
// return data.type === BlockEnum.TriggerPlugin && currentTriggerProvider && supportedAuthMethods.length > 0
|
||||
// }, [data.type, currentTriggerProvider, supportedAuthMethods.length])
|
||||
|
||||
// Simplified: Always show tab for trigger plugins
|
||||
const shouldShowTriggerTab = useMemo(() => {
|
||||
|
|
@ -536,14 +536,14 @@ const BasePanel: FC<BasePanelProps> = ({
|
|||
</PluginAuthInDataSourceNode>
|
||||
)
|
||||
}
|
||||
{
|
||||
{/* {
|
||||
shouldShowTriggerAuthSelector && (
|
||||
<AuthMethodSelector
|
||||
provider={currentTriggerProvider!}
|
||||
supportedMethods={supportedAuthMethods}
|
||||
/>
|
||||
)
|
||||
}
|
||||
} */}
|
||||
{
|
||||
shouldShowTriggerTab && (
|
||||
<div className='flex items-center justify-between pl-4 pr-3'>
|
||||
|
|
|
|||
|
|
@ -1,122 +1,24 @@
|
|||
import type { FC } from 'react'
|
||||
import { memo, useCallback, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { AuthorizedInNode } from '@/app/components/plugins/plugin-auth'
|
||||
import { AuthCategory } from '@/app/components/plugins/plugin-auth'
|
||||
import { AuthCategory, AuthorizedInNode } from '@/app/components/plugins/plugin-auth'
|
||||
import { SubscriptionMenu } from '@/app/components/workflow/nodes/trigger-plugin/components/subscription-menu'
|
||||
import { useStore } from '@/app/components/workflow/store'
|
||||
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 {
|
||||
useDeleteTriggerSubscription,
|
||||
useInitiateTriggerOAuth,
|
||||
useInvalidateTriggerSubscriptions,
|
||||
useTriggerSubscriptions,
|
||||
} from '@/service/use-triggers'
|
||||
import { useToastContext } from '@/app/components/base/toast'
|
||||
import { openOAuthPopup } from '@/hooks/use-oauth'
|
||||
import type { FC } from 'react'
|
||||
import { memo, useMemo } from 'react'
|
||||
|
||||
type NodeAuthProps = {
|
||||
data: Node['data']
|
||||
onAuthorizationChange: (credential_id: string) => void
|
||||
onSubscriptionChange?: (subscription_id: string) => void
|
||||
onSubscriptionChange?: (id: string, name: string) => void
|
||||
}
|
||||
|
||||
const NodeAuth: FC<NodeAuthProps> = ({ data, onAuthorizationChange, onSubscriptionChange }) => {
|
||||
const { t } = useTranslation()
|
||||
const buildInTools = useStore(s => s.buildInTools)
|
||||
const { notify } = useToastContext()
|
||||
|
||||
// Construct the correct provider path for trigger plugins
|
||||
// Format should be: plugin_id/provider_name (e.g., "langgenius/github_trigger/github_trigger")
|
||||
const provider = useMemo(() => {
|
||||
if (data.type === BlockEnum.TriggerPlugin) {
|
||||
// If we have both plugin_id and provider_name, construct the full path
|
||||
if (data.provider_name)
|
||||
return data.provider_name
|
||||
}
|
||||
return data.provider_id || ''
|
||||
}, [data.type, data.provider_id, data.provider_name])
|
||||
|
||||
// Always call hooks at the top level
|
||||
const { data: subscriptions = [] } = useTriggerSubscriptions(
|
||||
provider,
|
||||
data.type === BlockEnum.TriggerPlugin && !!provider,
|
||||
)
|
||||
const deleteSubscription = useDeleteTriggerSubscription()
|
||||
const initiateTriggerOAuth = useInitiateTriggerOAuth()
|
||||
const invalidateSubscriptions = useInvalidateTriggerSubscriptions()
|
||||
|
||||
const currCollection = useMemo(() => {
|
||||
return buildInTools.find(item => canFindTool(item.id, data.provider_id))
|
||||
}, [buildInTools, data.provider_id])
|
||||
|
||||
// Get selected subscription ID from node data
|
||||
const selectedSubscriptionId = data.subscription_id
|
||||
|
||||
const handleConfigure = useCallback(async () => {
|
||||
if (!provider) return
|
||||
|
||||
try {
|
||||
const response = await initiateTriggerOAuth.mutateAsync(provider)
|
||||
if (response.authorization_url) {
|
||||
openOAuthPopup(response.authorization_url, (callbackData) => {
|
||||
invalidateSubscriptions(provider)
|
||||
|
||||
if (callbackData?.success === false) {
|
||||
notify({
|
||||
type: 'error',
|
||||
message: callbackData.errorDescription || callbackData.error || t('workflow.nodes.triggerPlugin.authenticationFailed'),
|
||||
})
|
||||
}
|
||||
else if (callbackData?.subscriptionId) {
|
||||
notify({
|
||||
type: 'success',
|
||||
message: t('workflow.nodes.triggerPlugin.authenticationSuccess'),
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
catch (error: any) {
|
||||
notify({
|
||||
type: 'error',
|
||||
message: `Failed to configure authentication: ${error.message}`,
|
||||
})
|
||||
}
|
||||
}, [provider, initiateTriggerOAuth, invalidateSubscriptions, notify])
|
||||
|
||||
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) {
|
||||
return (
|
||||
<AuthorizedInNode
|
||||
|
|
@ -130,20 +32,17 @@ const NodeAuth: FC<NodeAuthProps> = ({ data, onAuthorizationChange, onSubscripti
|
|||
)
|
||||
}
|
||||
|
||||
// Trigger Plugin authentication
|
||||
if (data.type === BlockEnum.TriggerPlugin) {
|
||||
return (
|
||||
<AuthenticationMenu
|
||||
subscriptions={subscriptions}
|
||||
selectedSubscriptionId={selectedSubscriptionId}
|
||||
onSubscriptionSelect={handleSubscriptionSelect}
|
||||
onConfigure={handleConfigure}
|
||||
onRemove={handleRemove}
|
||||
<SubscriptionMenu
|
||||
// @ts-expect-error TODO: fix this
|
||||
payload={data}
|
||||
selectedSubscriptionId={data.subscription_id}
|
||||
onSubscriptionSelect={({ id, name }) => onSubscriptionChange?.(id, name)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
// No authentication needed
|
||||
return null
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,209 +0,0 @@
|
|||
'use client'
|
||||
|
||||
import type { FC } from 'react'
|
||||
import { memo, useCallback, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
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'
|
||||
|
||||
export type AuthSubscription = {
|
||||
id: string
|
||||
name: string
|
||||
status: AuthenticationStatus
|
||||
credentials?: Record<string, any>
|
||||
}
|
||||
|
||||
type AuthenticationMenuProps = {
|
||||
subscriptions: TriggerSubscription[]
|
||||
selectedSubscriptionId?: string
|
||||
onSubscriptionSelect: (subscriptionId: string) => void
|
||||
onConfigure: () => void
|
||||
onRemove: (subscriptionId: string) => void
|
||||
className?: string
|
||||
}
|
||||
|
||||
const AuthenticationMenu: FC<AuthenticationMenuProps> = ({
|
||||
subscriptions,
|
||||
selectedSubscriptionId,
|
||||
onSubscriptionSelect,
|
||||
onConfigure,
|
||||
onRemove,
|
||||
className,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
|
||||
const selectedSubscription = useMemo(() => {
|
||||
return subscriptions.find(sub => sub.id === selectedSubscriptionId)
|
||||
}, [subscriptions, selectedSubscriptionId])
|
||||
|
||||
const getStatusConfig = useCallback(() => {
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
// 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,
|
||||
}
|
||||
}
|
||||
else {
|
||||
return {
|
||||
label: t('workflow.nodes.triggerPlugin.notAuthorized'),
|
||||
color: 'red' as const,
|
||||
}
|
||||
}
|
||||
}, [selectedSubscription, subscriptions.length, t])
|
||||
|
||||
const statusConfig = getStatusConfig()
|
||||
|
||||
const handleConfigure = useCallback(() => {
|
||||
onConfigure()
|
||||
setIsOpen(false)
|
||||
}, [onConfigure])
|
||||
|
||||
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
|
||||
size='small'
|
||||
variant='ghost'
|
||||
className={cn(
|
||||
'h-6 px-1.5 py-1',
|
||||
'hover:bg-components-button-ghost-bg-hover',
|
||||
isOpen && 'bg-components-button-ghost-bg-hover',
|
||||
)}
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
>
|
||||
<Indicator
|
||||
className='mr-1.5'
|
||||
color={statusConfig.color}
|
||||
/>
|
||||
<span className="text-xs font-medium text-components-button-ghost-text">
|
||||
{statusConfig.label}
|
||||
</span>
|
||||
<RiArrowDownSLine
|
||||
className='ml-1 h-3.5 w-3.5 text-components-button-ghost-text'
|
||||
/>
|
||||
</Button>
|
||||
|
||||
{isOpen && (
|
||||
<>
|
||||
{/* Backdrop */}
|
||||
<div
|
||||
className="fixed inset-0 z-20"
|
||||
onClick={() => setIsOpen(false)}
|
||||
/>
|
||||
|
||||
{/* Dropdown Menu */}
|
||||
<div className={cn(
|
||||
'absolute right-0 z-30 mt-1',
|
||||
'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(
|
||||
'flex w-full items-center gap-2 px-3 py-2 text-left text-sm',
|
||||
'text-text-secondary hover:bg-state-base-hover',
|
||||
)}
|
||||
onClick={handleConfigure}
|
||||
>
|
||||
<RiAddLine className="h-4 w-4" />
|
||||
{t('workflow.nodes.triggerPlugin.addSubscription')}
|
||||
</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>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(AuthenticationMenu)
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
'use client'
|
||||
|
||||
import { SubscriptionSelectorEntry } from '@/app/components/plugins/plugin-detail-panel/subscription-list/selector-entry'
|
||||
import { usePluginStore } from '@/app/components/plugins/plugin-detail-panel/subscription-list/store'
|
||||
import { memo, useEffect } from 'react'
|
||||
import type { PluginTriggerNodeType } from '../types'
|
||||
import useConfig from '../use-config'
|
||||
|
||||
export const SubscriptionMenu = memo(({ payload, selectedSubscriptionId, onSubscriptionSelect }: {
|
||||
payload: PluginTriggerNodeType,
|
||||
selectedSubscriptionId?: string,
|
||||
onSubscriptionSelect: ({ id, name }: { id: string, name: string }) => void
|
||||
}) => {
|
||||
// @ts-expect-error TODO: fix this
|
||||
const { currentProvider } = useConfig(payload.id as string, payload)
|
||||
const { setDetail } = usePluginStore()
|
||||
|
||||
useEffect(() => {
|
||||
if (currentProvider) {
|
||||
setDetail({
|
||||
plugin_id: currentProvider.plugin_id || '',
|
||||
provider: currentProvider.name,
|
||||
declaration: {
|
||||
tool: undefined,
|
||||
endpoint: undefined,
|
||||
trigger: {
|
||||
subscription_schema: currentProvider.subscription_schema,
|
||||
credentials_schema: currentProvider.credentials_schema,
|
||||
oauth_schema: {
|
||||
client_schema: currentProvider.oauth_client_schema,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
}, [currentProvider, setDetail])
|
||||
|
||||
return (
|
||||
<SubscriptionSelectorEntry
|
||||
selectedId={selectedSubscriptionId}
|
||||
onSelect={onSubscriptionSelect}
|
||||
/>
|
||||
)
|
||||
})
|
||||
Loading…
Reference in New Issue