diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/index.tsx b/web/app/components/plugins/plugin-detail-panel/subscription-list/index.tsx
index 045ece3b96..8acb8f40df 100644
--- a/web/app/components/plugins/plugin-detail-panel/subscription-list/index.tsx
+++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/index.tsx
@@ -9,10 +9,15 @@ export enum SubscriptionListMode {
SELECTOR = 'selector',
}
+export type SimpleSubscription = {
+ id: string,
+ name: string
+}
+
type SubscriptionListProps = {
mode?: SubscriptionListMode
selectedId?: string
- onSelect?: ({ id, name }: { id: string, name: string }) => void
+ onSelect?: (v: SimpleSubscription, callback?: () => void) => void
}
export { SubscriptionSelectorEntry } from './selector-entry'
@@ -22,7 +27,7 @@ export const SubscriptionList = withErrorBoundary(({
selectedId,
onSelect,
}: SubscriptionListProps) => {
- const { subscriptions, isLoading } = useSubscriptionList()
+ const { isLoading, refetch } = useSubscriptionList()
if (isLoading) {
return (
@@ -34,16 +39,13 @@ export const SubscriptionList = withErrorBoundary(({
if (mode === SubscriptionListMode.SELECTOR) {
return (
{
+ onSelect?.(v, refetch)
+ }}
/>
)
}
- return (
-
- )
+ return
})
diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/list-view.tsx b/web/app/components/plugins/plugin-detail-panel/subscription-list/list-view.tsx
index 9828dcae57..d172f73283 100644
--- a/web/app/components/plugins/plugin-detail-panel/subscription-list/list-view.tsx
+++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/list-view.tsx
@@ -1,22 +1,21 @@
'use client'
import Tooltip from '@/app/components/base/tooltip'
-import type { TriggerSubscription } from '@/app/components/workflow/block-selector/types'
import cn from '@/utils/classnames'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { CreateButtonType, CreateSubscriptionButton } from './create'
import SubscriptionCard from './subscription-card'
+import { useSubscriptionList } from './use-subscription-list'
type SubscriptionListViewProps = {
- subscriptions?: TriggerSubscription[]
showTopBorder?: boolean
}
export const SubscriptionListView: React.FC = ({
- subscriptions,
showTopBorder = false,
}) => {
const { t } = useTranslation()
+ const { subscriptions } = useSubscriptionList()
const subscriptionCount = subscriptions?.length || 0
diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/selector-entry.tsx b/web/app/components/plugins/plugin-detail-panel/subscription-list/selector-entry.tsx
index 97d6e57e94..443c418eb4 100644
--- a/web/app/components/plugins/plugin-detail-panel/subscription-list/selector-entry.tsx
+++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/selector-entry.tsx
@@ -4,6 +4,7 @@ import {
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
+import type { SimpleSubscription } from '@/app/components/plugins/plugin-detail-panel/subscription-list'
import { SubscriptionList, SubscriptionListMode } from '@/app/components/plugins/plugin-detail-panel/subscription-list'
import cn from '@/utils/classnames'
import { RiArrowDownSLine, RiWebhookLine } from '@remixicon/react'
@@ -74,7 +75,7 @@ const SubscriptionTriggerButton: React.FC = ({
export const SubscriptionSelectorEntry = ({ selectedId, onSelect }: {
selectedId?: string,
- onSelect: ({ id, name }: { id: string, name: string }) => void
+ onSelect: (v: SimpleSubscription, callback?: () => void) => void
}) => {
const [isOpen, setIsOpen] = useState(false)
diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/selector-view.tsx b/web/app/components/plugins/plugin-detail-panel/subscription-list/selector-view.tsx
index 17c39b41e0..f789f9575e 100644
--- a/web/app/components/plugins/plugin-detail-panel/subscription-list/selector-view.tsx
+++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/selector-view.tsx
@@ -8,19 +8,19 @@ import React, { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { CreateButtonType, CreateSubscriptionButton } from './create'
import { DeleteConfirm } from './delete-confirm'
+import { useSubscriptionList } from './use-subscription-list'
type SubscriptionSelectorProps = {
- subscriptions?: TriggerSubscription[]
selectedId?: string
onSelect?: ({ id, name }: { id: string, name: string }) => void
}
export const SubscriptionSelectorView: React.FC = ({
- subscriptions,
selectedId,
onSelect,
}) => {
const { t } = useTranslation()
+ const { subscriptions } = useSubscriptionList()
const [deletedSubscription, setDeletedSubscription] = useState(null)
const subscriptionCount = subscriptions?.length || 0
@@ -39,47 +39,33 @@ export const SubscriptionSelectorView: React.FC = ({
/>
}
- {subscriptionCount > 0 ? (
- <>
- {subscriptions?.map(subscription => (
-
- ))}
- >
- ) : (
- // todo: refactor this
-
-
- {t('pluginTrigger.subscription.empty.description')}
+ {subscriptions?.map(subscription => (
+
- )}
+
{
+ 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'>
+
+
+
+ ))}
{deletedSubscription && (
{
setNodes(newNodes)
}, [store])
- const handleNodeDataUpdateWithSyncDraft = useCallback((payload: NodeDataUpdatePayload) => {
+ const handleNodeDataUpdateWithSyncDraft = useCallback((
+ payload: NodeDataUpdatePayload,
+ options?: {
+ sync?: boolean
+ notRefreshWhenSyncError?: boolean
+ callback?: SyncCallback
+ },
+ ) => {
if (getNodesReadOnly())
return
handleNodeDataUpdate(payload)
- handleSyncWorkflowDraft()
+ handleSyncWorkflowDraft(options?.sync, options?.notRefreshWhenSyncError, options?.callback)
}, [handleSyncWorkflowDraft, handleNodeDataUpdate, getNodesReadOnly])
return {
diff --git a/web/app/components/workflow/hooks/use-nodes-sync-draft.ts b/web/app/components/workflow/hooks/use-nodes-sync-draft.ts
index e6cc3a97e3..a3cf8dbe79 100644
--- a/web/app/components/workflow/hooks/use-nodes-sync-draft.ts
+++ b/web/app/components/workflow/hooks/use-nodes-sync-draft.ts
@@ -7,6 +7,12 @@ import {
} from './use-workflow'
import { useHooksStore } from '@/app/components/workflow/hooks-store'
+export type SyncCallback = {
+ onSuccess?: () => void
+ onError?: () => void
+ onSettled?: () => void
+}
+
export const useNodesSyncDraft = () => {
const { getNodesReadOnly } = useNodesReadOnly()
const debouncedSyncWorkflowDraft = useStore(s => s.debouncedSyncWorkflowDraft)
@@ -16,11 +22,7 @@ export const useNodesSyncDraft = () => {
const handleSyncWorkflowDraft = useCallback((
sync?: boolean,
notRefreshWhenSyncError?: boolean,
- callback?: {
- onSuccess?: () => void
- onError?: () => void
- onSettled?: () => void
- },
+ callback?: SyncCallback,
) => {
if (getNodesReadOnly())
return
diff --git a/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx b/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx
index 61add8c1a9..8f28d128c8 100644
--- a/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx
+++ b/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx
@@ -1,3 +1,31 @@
+import { useStore as useAppStore } from '@/app/components/app/store'
+import Tooltip from '@/app/components/base/tooltip'
+import BlockIcon from '@/app/components/workflow/block-icon'
+import {
+ WorkflowHistoryEvent,
+ useAvailableBlocks,
+ useNodeDataUpdate,
+ useNodesInteractions,
+ useNodesMetaData,
+ useNodesReadOnly,
+ useToolIcon,
+ useWorkflowHistory,
+} from '@/app/components/workflow/hooks'
+import Split from '@/app/components/workflow/nodes/_base/components/split'
+import { useStore } from '@/app/components/workflow/store'
+import { BlockEnum, type Node, NodeRunningStatus } from '@/app/components/workflow/types'
+import {
+ canRunBySingle,
+ hasErrorHandleNode,
+ hasRetryNode,
+ isSupportCustomRunForm,
+} from '@/app/components/workflow/utils'
+import { useAllTriggerPlugins } from '@/service/use-triggers'
+import cn from '@/utils/classnames'
+import {
+ RiCloseLine,
+ RiPlayLargeLine,
+} from '@remixicon/react'
import type {
FC,
ReactNode,
@@ -11,71 +39,44 @@ import React, {
useRef,
useState,
} from 'react'
-import {
- RiCloseLine,
- RiPlayLargeLine,
-} from '@remixicon/react'
-import { useShallow } from 'zustand/react/shallow'
import { useTranslation } from 'react-i18next'
+import { useShallow } from 'zustand/react/shallow'
+import { useResizePanel } from '../../hooks/use-resize-panel'
+import ErrorHandleOnPanel from '../error-handle/error-handle-on-panel'
+import HelpLink from '../help-link'
import NextStep from '../next-step'
import PanelOperator from '../panel-operator'
-import HelpLink from '../help-link'
+import RetryOnPanel from '../retry/retry-on-panel'
import {
DescriptionInput,
TitleInput,
} from '../title-description-input'
-import ErrorHandleOnPanel from '../error-handle/error-handle-on-panel'
-import RetryOnPanel from '../retry/retry-on-panel'
-import { useResizePanel } from '../../hooks/use-resize-panel'
-import cn from '@/utils/classnames'
-import BlockIcon from '@/app/components/workflow/block-icon'
-import Split from '@/app/components/workflow/nodes/_base/components/split'
-import {
- WorkflowHistoryEvent,
- useAvailableBlocks,
- useNodeDataUpdate,
- useNodesInteractions,
- useNodesMetaData,
- useNodesReadOnly,
- useToolIcon,
- useWorkflowHistory,
-} from '@/app/components/workflow/hooks'
-import {
- canRunBySingle,
- hasErrorHandleNode,
- hasRetryNode,
- isSupportCustomRunForm,
-} from '@/app/components/workflow/utils'
-import Tooltip from '@/app/components/base/tooltip'
-import { BlockEnum, type Node, NodeRunningStatus } from '@/app/components/workflow/types'
-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 LastRun from './last-run'
-import useLastRun from './last-run/use-last-run'
-import BeforeRunForm from '../before-run-form'
-import { debounce } from 'lodash-es'
-import { useLogs } from '@/app/components/workflow/run/hooks'
-import PanelWrap from '../before-run-form/panel-wrap'
-import SpecialResultPanel from '@/app/components/workflow/run/special-result-panel'
import { Stop } from '@/app/components/base/icons/src/vender/line/mediaAndDevices'
-import { useHooksStore } from '@/app/components/workflow/hooks-store'
-import { FlowType } from '@/types/common'
import {
+ AuthCategory,
AuthorizedInDataSourceNode,
AuthorizedInNode,
PluginAuth,
PluginAuthInDataSourceNode,
} from '@/app/components/plugins/plugin-auth'
-import { AuthCategory } from '@/app/components/plugins/plugin-auth'
-import { canFindTool } from '@/utils'
+import type { SimpleSubscription } from '@/app/components/plugins/plugin-detail-panel/subscription-list'
+import { useHooksStore } from '@/app/components/workflow/hooks-store'
+import useInspectVarsCrud from '@/app/components/workflow/hooks/use-inspect-vars-crud'
+import DataSourceBeforeRunForm from '@/app/components/workflow/nodes/data-source/before-run-form'
import type { CustomRunFormProps } from '@/app/components/workflow/nodes/data-source/types'
import { DataSourceClassification } from '@/app/components/workflow/nodes/data-source/types'
+import { useLogs } from '@/app/components/workflow/run/hooks'
+import SpecialResultPanel from '@/app/components/workflow/run/special-result-panel'
import { useModalContext } from '@/context/modal-context'
-import DataSourceBeforeRunForm from '@/app/components/workflow/nodes/data-source/before-run-form'
-import useInspectVarsCrud from '@/app/components/workflow/hooks/use-inspect-vars-crud'
+import { FlowType } from '@/types/common'
+import { canFindTool } from '@/utils'
+import { debounce } from 'lodash-es'
+import BeforeRunForm from '../before-run-form'
+import PanelWrap from '../before-run-form/panel-wrap'
+import LastRun from './last-run'
+import useLastRun from './last-run/use-last-run'
import { TriggerSubscription } from './trigger-subscription'
const getCustomRunForm = (params: CustomRunFormProps): React.JSX.Element => {
@@ -311,13 +312,14 @@ const BasePanel: FC = ({
appendNodeInspectVars,
} = useInspectVarsCrud()
- const handleSubscriptionChange = useCallback((subscription_id: string) => {
- handleNodeDataUpdateWithSyncDraft({
- id,
- data: {
- subscription_id,
+ const handleSubscriptionChange = useCallback((v: SimpleSubscription, callback?: () => void) => {
+ handleNodeDataUpdateWithSyncDraft(
+ { id, data: { subscription_id: v.id } },
+ {
+ sync: true,
+ callback: { onSettled: callback },
},
- })
+ )
}, [handleNodeDataUpdateWithSyncDraft, id])
if (logParams.showSpecialResultPanel) {
@@ -497,11 +499,6 @@ const BasePanel: FC = ({
onAuthorizationItemClick={handleAuthorizationItemClick}
credentialId={data.credential_id}
/>
- {/* */}
)
diff --git a/web/app/components/workflow/nodes/_base/components/workflow-panel/trigger-subscription.tsx b/web/app/components/workflow/nodes/_base/components/workflow-panel/trigger-subscription.tsx
index 460e60caa7..7b8800d787 100644
--- a/web/app/components/workflow/nodes/_base/components/workflow-panel/trigger-subscription.tsx
+++ b/web/app/components/workflow/nodes/_base/components/workflow-panel/trigger-subscription.tsx
@@ -1,4 +1,5 @@
import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks'
+import type { SimpleSubscription } from '@/app/components/plugins/plugin-detail-panel/subscription-list'
import { CreateButtonType, CreateSubscriptionButton } from '@/app/components/plugins/plugin-detail-panel/subscription-list/create'
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'
@@ -11,7 +12,7 @@ import { useEffect } from 'react'
type NodeAuthProps = {
data: Node['data']
- onSubscriptionChange?: (id: string, name: string) => void
+ onSubscriptionChange: (v: SimpleSubscription, callback?: () => void) => void
children: React.ReactNode
}
@@ -46,7 +47,7 @@ export const TriggerSubscription: FC = ({ data, onSubscriptionCha
{children}
{subscriptionCount > 0 && onSubscriptionChange?.(id, name)}
+ onSelect={onSubscriptionChange}
/>}
}
diff --git a/web/app/components/workflow/nodes/trigger-plugin/components/__tests__/api-key-config-modal.test.tsx b/web/app/components/workflow/nodes/trigger-plugin/components/__tests__/api-key-config-modal.test.tsx
deleted file mode 100644
index 2c7c0c506d..0000000000
--- a/web/app/components/workflow/nodes/trigger-plugin/components/__tests__/api-key-config-modal.test.tsx
+++ /dev/null
@@ -1,185 +0,0 @@
-import React from 'react'
-import { fireEvent, render, screen, waitFor } from '@testing-library/react'
-import { useTranslation } from 'react-i18next'
-
-jest.mock('react-i18next')
-jest.mock('@/service/use-triggers', () => ({
- useCreateTriggerSubscriptionBuilder: () => ({ mutateAsync: jest.fn().mockResolvedValue({ subscription_builder: { id: 'test-id' } }) }),
- useUpdateTriggerSubscriptionBuilder: () => ({ mutateAsync: jest.fn() }),
- useVerifyTriggerSubscriptionBuilder: () => ({ mutateAsync: jest.fn() }),
- useBuildTriggerSubscription: () => ({ mutateAsync: jest.fn() }),
- useInvalidateTriggerSubscriptions: () => jest.fn(),
-}))
-jest.mock('@/app/components/base/toast', () => ({
- useToastContext: () => ({ notify: jest.fn() }),
-}))
-jest.mock('@/app/components/tools/utils/to-form-schema', () => ({
- toolCredentialToFormSchemas: jest.fn().mockReturnValue([
- {
- name: 'api_key',
- label: { en_US: 'API Key' },
- required: true,
- },
- ]),
- addDefaultValue: jest.fn().mockReturnValue({ api_key: '' }),
-}))
-jest.mock('@/app/components/header/account-setting/model-provider-page/hooks', () => ({
- useLanguage: () => 'en_US',
-}))
-jest.mock('@/app/components/header/account-setting/model-provider-page/model-modal/Form', () => {
- return function MockForm({ value, onChange, formSchemas }: any) {
- return (
-
- {formSchemas.map((schema: any, index: number) => (
-
-
- onChange({ ...value, [schema.name]: e.target.value })}
- />
-
- ))}
-
- )
- }
-})
-
-import ApiKeyConfigModal from '../api-key-config-modal'
-
-const mockUseTranslation = useTranslation as jest.MockedFunction
-
-const mockTranslation = {
- t: (key: string, params?: any) => {
- const translations: Record = {
- 'workflow.nodes.triggerPlugin.configureApiKey': 'Configure API Key',
- 'workflow.nodes.triggerPlugin.apiKeyDescription': 'Configure API key credentials for authentication',
- 'workflow.nodes.triggerPlugin.apiKeyConfigured': 'API key configured successfully',
- 'workflow.nodes.triggerPlugin.configurationFailed': 'Configuration failed',
- 'common.operation.cancel': 'Cancel',
- 'common.operation.save': 'Save',
- 'common.errorMsg.fieldRequired': `${params?.field} is required`,
- }
- return translations[key] || key
- },
-}
-
-const mockProvider = {
- plugin_id: 'test-plugin',
- name: 'test-provider',
- author: 'test',
- label: { en_US: 'Test Provider' },
- description: { en_US: 'Test Description' },
- icon: 'test-icon.svg',
- icon_dark: null,
- tags: ['test'],
- plugin_unique_identifier: 'test:1.0.0',
- credentials_schema: [
- {
- type: 'secret-input' as const,
- name: 'api_key',
- required: true,
- label: { en_US: 'API Key' },
- scope: null,
- default: null,
- options: null,
- help: null,
- url: null,
- placeholder: null,
- },
- ],
- oauth_client_schema: [],
- subscription_schema: {
- parameters_schema: [],
- properties_schema: [],
- },
- triggers: [],
-}
-
-beforeEach(() => {
- mockUseTranslation.mockReturnValue(mockTranslation as any)
- jest.clearAllMocks()
-})
-
-describe('ApiKeyConfigModal', () => {
- const mockProps = {
- provider: mockProvider,
- onCancel: jest.fn(),
- onSuccess: jest.fn(),
- }
-
- describe('Rendering', () => {
- it('should render modal with correct title and description', () => {
- render()
-
- expect(screen.getByText('Configure API Key')).toBeInTheDocument()
- expect(screen.getByText('Configure API key credentials for authentication')).toBeInTheDocument()
- })
-
- it('should render form when credential schema is loaded', async () => {
- render()
-
- await waitFor(() => {
- expect(screen.getByTestId('mock-form')).toBeInTheDocument()
- })
- })
-
- it('should render form fields with correct labels', async () => {
- render()
-
- await waitFor(() => {
- expect(screen.getByLabelText('API Key')).toBeInTheDocument()
- })
- })
-
- it('should render cancel and save buttons', async () => {
- render()
-
- await waitFor(() => {
- expect(screen.getByRole('button', { name: 'Cancel' })).toBeInTheDocument()
- expect(screen.getByRole('button', { name: 'Save' })).toBeInTheDocument()
- })
- })
- })
-
- describe('Form Interaction', () => {
- it('should update form values on input change', async () => {
- render()
-
- await waitFor(() => {
- const apiKeyInput = screen.getByTestId('input-api_key')
- fireEvent.change(apiKeyInput, { target: { value: 'test-api-key' } })
- expect(apiKeyInput).toHaveValue('test-api-key')
- })
- })
-
- it('should call onCancel when cancel button is clicked', async () => {
- render()
-
- await waitFor(() => {
- fireEvent.click(screen.getByRole('button', { name: 'Cancel' }))
- expect(mockProps.onCancel).toHaveBeenCalledTimes(1)
- })
- })
- })
-
- describe('Save Process', () => {
- it('should proceed with save when required fields are filled', async () => {
- render()
-
- await waitFor(() => {
- const apiKeyInput = screen.getByTestId('input-api_key')
- fireEvent.change(apiKeyInput, { target: { value: 'valid-api-key' } })
- })
-
- await waitFor(() => {
- fireEvent.click(screen.getByRole('button', { name: 'Save' }))
- })
-
- await waitFor(() => {
- expect(mockProps.onSuccess).toHaveBeenCalledTimes(1)
- })
- })
- })
-})
diff --git a/web/app/components/workflow/nodes/trigger-plugin/components/__tests__/auth-method-selector.test.tsx b/web/app/components/workflow/nodes/trigger-plugin/components/__tests__/auth-method-selector.test.tsx
deleted file mode 100644
index fea1adba2d..0000000000
--- a/web/app/components/workflow/nodes/trigger-plugin/components/__tests__/auth-method-selector.test.tsx
+++ /dev/null
@@ -1,194 +0,0 @@
-import React from 'react'
-import { fireEvent, render, screen } from '@testing-library/react'
-import { useTranslation } from 'react-i18next'
-
-jest.mock('react-i18next')
-jest.mock('@/service/use-triggers', () => ({
- useInitiateTriggerOAuth: () => ({ mutateAsync: jest.fn() }),
- useInvalidateTriggerSubscriptions: () => jest.fn(),
- useTriggerOAuthConfig: () => ({ data: null }),
-}))
-jest.mock('@/hooks/use-oauth', () => ({
- openOAuthPopup: jest.fn(),
-}))
-jest.mock('@/app/components/base/toast', () => ({
- useToastContext: () => ({ notify: jest.fn() }),
-}))
-jest.mock('../api-key-config-modal', () => {
- return function MockApiKeyConfigModal({ onCancel }: any) {
- return (
-
-
-
- )
- }
-})
-jest.mock('../oauth-client-config-modal', () => {
- return function MockOAuthClientConfigModal({ onCancel }: any) {
- return (
-
-
-
- )
- }
-})
-
-import AuthMethodSelector from '../auth-method-selector'
-
-const mockUseTranslation = useTranslation as jest.MockedFunction
-
-const mockTranslation = {
- t: (key: string) => {
- const translations: Record = {
- 'workflow.nodes.triggerPlugin.or': 'OR',
- 'workflow.nodes.triggerPlugin.useOAuth': 'Use OAuth',
- 'workflow.nodes.triggerPlugin.useApiKey': 'Use API Key',
- }
- return translations[key] || key
- },
-}
-
-const mockProvider = {
- plugin_id: 'test-plugin',
- name: 'test-provider',
- author: 'test',
- label: { en_US: 'Test Provider', zh_Hans: '测试提供者' },
- description: { en_US: 'Test Description', zh_Hans: '测试描述' },
- icon: 'test-icon.svg',
- icon_dark: null,
- tags: ['test'],
- plugin_unique_identifier: 'test:1.0.0',
- credentials_schema: [
- {
- type: 'secret-input' as const,
- name: 'api_key',
- required: true,
- label: { en_US: 'API Key', zh_Hans: 'API密钥' },
- scope: null,
- default: null,
- options: null,
- help: null,
- url: null,
- placeholder: null,
- },
- ],
- oauth_client_schema: [
- {
- type: 'secret-input' as const,
- name: 'client_id',
- required: true,
- label: { en_US: 'Client ID', zh_Hans: '客户端ID' },
- scope: null,
- default: null,
- options: null,
- help: null,
- url: null,
- placeholder: null,
- },
- ],
- subscription_schema: {
- parameters_schema: [],
- properties_schema: [],
- },
- triggers: [],
-}
-
-beforeEach(() => {
- mockUseTranslation.mockReturnValue(mockTranslation as any)
-})
-
-describe('AuthMethodSelector', () => {
- describe('Rendering', () => {
- it('should not render when no supported methods are available', () => {
- const { container } = render(
- ,
- )
-
- expect(container.firstChild).toBeNull()
- })
-
- it('should render OAuth button when oauth is supported', () => {
- render(
- ,
- )
-
- expect(screen.getByRole('button', { name: 'Use OAuth' })).toBeInTheDocument()
- })
-
- it('should render API Key button when api_key is supported', () => {
- render(
- ,
- )
-
- expect(screen.getByRole('button', { name: 'Use API Key' })).toBeInTheDocument()
- })
-
- it('should render both buttons and OR divider when both methods are supported', () => {
- render(
- ,
- )
-
- expect(screen.getByRole('button', { name: 'Use OAuth' })).toBeInTheDocument()
- expect(screen.getByRole('button', { name: 'Use API Key' })).toBeInTheDocument()
- expect(screen.getByText('OR')).toBeInTheDocument()
- })
- })
-
- describe('Modal Interactions', () => {
- it('should open API Key modal when API Key button is clicked', () => {
- render(
- ,
- )
-
- fireEvent.click(screen.getByRole('button', { name: 'Use API Key' }))
- expect(screen.getByTestId('api-key-modal')).toBeInTheDocument()
- })
-
- it('should close API Key modal when cancel is clicked', () => {
- render(
- ,
- )
-
- fireEvent.click(screen.getByRole('button', { name: 'Use API Key' }))
- expect(screen.getByTestId('api-key-modal')).toBeInTheDocument()
-
- fireEvent.click(screen.getByText('Cancel'))
- expect(screen.queryByTestId('api-key-modal')).not.toBeInTheDocument()
- })
-
- it('should open OAuth client config modal when OAuth settings button is clicked', () => {
- render(
- ,
- )
-
- const settingsButtons = screen.getAllByRole('button')
- const settingsButton = settingsButtons.find(button =>
- button.querySelector('svg') && !button.textContent?.includes('Use OAuth'),
- )
-
- fireEvent.click(settingsButton!)
- expect(screen.getByTestId('oauth-client-modal')).toBeInTheDocument()
- })
- })
-})
diff --git a/web/app/components/workflow/nodes/trigger-plugin/components/__tests__/oauth-client-config-modal.test.tsx b/web/app/components/workflow/nodes/trigger-plugin/components/__tests__/oauth-client-config-modal.test.tsx
deleted file mode 100644
index 60b7bb168e..0000000000
--- a/web/app/components/workflow/nodes/trigger-plugin/components/__tests__/oauth-client-config-modal.test.tsx
+++ /dev/null
@@ -1,204 +0,0 @@
-import React from 'react'
-import { fireEvent, render, screen, waitFor } from '@testing-library/react'
-import { useTranslation } from 'react-i18next'
-
-jest.mock('react-i18next')
-jest.mock('@/service/use-triggers', () => ({
- useConfigureTriggerOAuth: () => ({ mutateAsync: jest.fn() }),
- useInvalidateTriggerOAuthConfig: () => jest.fn(),
- useTriggerOAuthConfig: () => ({ data: null, isLoading: false }),
-}))
-jest.mock('@/app/components/base/toast', () => ({
- useToastContext: () => ({ notify: jest.fn() }),
-}))
-jest.mock('@/app/components/tools/utils/to-form-schema', () => ({
- toolCredentialToFormSchemas: jest.fn().mockReturnValue([
- {
- name: 'client_id',
- label: { en_US: 'Client ID' },
- required: true,
- },
- {
- name: 'client_secret',
- label: { en_US: 'Client Secret' },
- required: true,
- },
- ]),
- addDefaultValue: jest.fn().mockReturnValue({ client_id: '', client_secret: '' }),
-}))
-jest.mock('@/app/components/header/account-setting/model-provider-page/hooks', () => ({
- useLanguage: () => 'en_US',
-}))
-jest.mock('@/app/components/header/account-setting/model-provider-page/model-modal/Form', () => {
- return function MockForm({ value, onChange, formSchemas }: any) {
- return (
-
- {formSchemas.map((schema: any, index: number) => (
-
-
- onChange({ ...value, [schema.name]: e.target.value })}
- />
-
- ))}
-
- )
- }
-})
-
-import OAuthClientConfigModal from '../oauth-client-config-modal'
-
-const mockUseTranslation = useTranslation as jest.MockedFunction
-
-const mockTranslation = {
- t: (key: string, params?: any) => {
- const translations: Record = {
- 'workflow.nodes.triggerPlugin.configureOAuthClient': 'Configure OAuth Client',
- 'workflow.nodes.triggerPlugin.oauthClientDescription': 'Configure OAuth client credentials to enable authentication',
- 'workflow.nodes.triggerPlugin.oauthClientSaved': 'OAuth client configuration saved successfully',
- 'workflow.nodes.triggerPlugin.configurationFailed': 'Configuration failed',
- 'common.operation.cancel': 'Cancel',
- 'common.operation.save': 'Save',
- 'common.errorMsg.fieldRequired': `${params?.field} is required`,
- }
- return translations[key] || key
- },
-}
-
-const mockProvider = {
- plugin_id: 'test-plugin',
- name: 'test-provider',
- author: 'test',
- label: { en_US: 'Test Provider' },
- description: { en_US: 'Test Description' },
- icon: 'test-icon.svg',
- icon_dark: null,
- tags: ['test'],
- plugin_unique_identifier: 'test:1.0.0',
- credentials_schema: [],
- oauth_client_schema: [
- {
- type: 'secret-input' as const,
- name: 'client_id',
- required: true,
- label: { en_US: 'Client ID' },
- scope: null,
- default: null,
- options: null,
- help: null,
- url: null,
- placeholder: null,
- },
- {
- type: 'secret-input' as const,
- name: 'client_secret',
- required: true,
- label: { en_US: 'Client Secret' },
- scope: null,
- default: null,
- options: null,
- help: null,
- url: null,
- placeholder: null,
- },
- ],
- subscription_schema: {
- parameters_schema: [],
- properties_schema: [],
- },
- triggers: [],
-}
-
-beforeEach(() => {
- mockUseTranslation.mockReturnValue(mockTranslation as any)
- jest.clearAllMocks()
-})
-
-describe('OAuthClientConfigModal', () => {
- const mockProps = {
- provider: mockProvider,
- onCancel: jest.fn(),
- onSuccess: jest.fn(),
- }
-
- describe('Rendering', () => {
- it('should render modal with correct title and description', () => {
- render()
-
- expect(screen.getByText('Configure OAuth Client')).toBeInTheDocument()
- expect(screen.getByText('Configure OAuth client credentials to enable authentication')).toBeInTheDocument()
- })
-
- it('should render form when schema is loaded', async () => {
- render()
-
- await waitFor(() => {
- expect(screen.getByTestId('mock-form')).toBeInTheDocument()
- })
- })
-
- it('should render form fields with correct labels', async () => {
- render()
-
- await waitFor(() => {
- expect(screen.getByLabelText('Client ID')).toBeInTheDocument()
- expect(screen.getByLabelText('Client Secret')).toBeInTheDocument()
- })
- })
-
- it('should render cancel and save buttons', async () => {
- render()
-
- await waitFor(() => {
- expect(screen.getByRole('button', { name: 'Cancel' })).toBeInTheDocument()
- expect(screen.getByRole('button', { name: 'Save' })).toBeInTheDocument()
- })
- })
- })
-
- describe('Form Interaction', () => {
- it('should update form values on input change', async () => {
- render()
-
- await waitFor(() => {
- const clientIdInput = screen.getByTestId('input-client_id')
- fireEvent.change(clientIdInput, { target: { value: 'test-client-id' } })
- expect(clientIdInput).toHaveValue('test-client-id')
- })
- })
-
- it('should call onCancel when cancel button is clicked', async () => {
- render()
-
- await waitFor(() => {
- fireEvent.click(screen.getByRole('button', { name: 'Cancel' }))
- expect(mockProps.onCancel).toHaveBeenCalledTimes(1)
- })
- })
- })
-
- describe('Save Process', () => {
- it('should proceed with save when required fields are filled', async () => {
- render()
-
- await waitFor(() => {
- const clientIdInput = screen.getByTestId('input-client_id')
- const clientSecretInput = screen.getByTestId('input-client_secret')
-
- fireEvent.change(clientIdInput, { target: { value: 'valid-client-id' } })
- fireEvent.change(clientSecretInput, { target: { value: 'valid-client-secret' } })
- })
-
- await waitFor(() => {
- fireEvent.click(screen.getByRole('button', { name: 'Save' }))
- })
-
- await waitFor(() => {
- expect(mockProps.onSuccess).toHaveBeenCalledTimes(1)
- })
- })
- })
-})
diff --git a/web/app/components/workflow/nodes/trigger-plugin/components/api-key-config-modal.tsx b/web/app/components/workflow/nodes/trigger-plugin/components/api-key-config-modal.tsx
deleted file mode 100644
index a38e72a7fe..0000000000
--- a/web/app/components/workflow/nodes/trigger-plugin/components/api-key-config-modal.tsx
+++ /dev/null
@@ -1,268 +0,0 @@
-'use client'
-import type { FC } from 'react'
-import React, { useEffect, useState } from 'react'
-import { useTranslation } from 'react-i18next'
-import { addDefaultValue, toolCredentialToFormSchemas } from '@/app/components/tools/utils/to-form-schema'
-import type { TriggerWithProvider } from '@/app/components/workflow/block-selector/types'
-import Drawer from '@/app/components/base/drawer-plus'
-import Button from '@/app/components/base/button'
-import Toast from '@/app/components/base/toast'
-import Loading from '@/app/components/base/loading'
-import Form from '@/app/components/header/account-setting/model-provider-page/model-modal/Form'
-import { LinkExternal02 } from '@/app/components/base/icons/src/vender/line/general'
-import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks'
-import { useInvalidateTriggerSubscriptions } from '@/service/use-triggers'
-import { useToastContext } from '@/app/components/base/toast'
-import { findMissingRequiredField, sanitizeFormValues } from '../utils/form-helpers'
-import { useTriggerAuthFlow } from '../hooks/use-trigger-auth-flow'
-import ParametersForm from './parameters-form'
-
-type ApiKeyConfigModalProps = {
- provider: TriggerWithProvider
- onCancel: () => void
- onSuccess: () => void
-}
-
-const ApiKeyConfigModal: FC = ({
- provider,
- onCancel,
- onSuccess,
-}) => {
- const { t } = useTranslation()
- const { notify } = useToastContext()
- const language = useLanguage()
- const invalidateSubscriptions = useInvalidateTriggerSubscriptions()
-
- const [credentialSchema, setCredentialSchema] = useState([])
- const [credentials, setCredentials] = useState>({})
- const [parameters, setParameters] = useState>({})
- const [properties, setProperties] = useState>({})
- const [subscriptionName, setSubscriptionName] = useState('')
-
- const {
- step,
- builderId,
- isLoading,
- startAuth,
- verifyAuth,
- completeConfig,
- reset,
- } = useTriggerAuthFlow(provider)
-
- useEffect(() => {
- if (provider.credentials_schema) {
- const schemas = toolCredentialToFormSchemas(provider.credentials_schema as any)
- setCredentialSchema(schemas)
- const defaultCredentials = addDefaultValue({}, schemas)
- setCredentials(sanitizeFormValues(defaultCredentials))
- }
- }, [provider.credentials_schema])
-
- useEffect(() => {
- startAuth().catch((err) => {
- notify({
- type: 'error',
- message: t('workflow.nodes.triggerPlugin.failedToStart', { error: err.message }),
- })
- })
-
- return () => {
- reset()
- }
- }, []) // Remove dependencies to run only once on mount
-
- const handleCredentialsSubmit = async () => {
- const requiredFields = credentialSchema
- .filter(field => field.required)
- .map(field => ({
- name: field.name,
- label: field.label[language] || field.label.en_US,
- }))
-
- const missingField = findMissingRequiredField(credentials, requiredFields)
- if (missingField) {
- Toast.notify({
- type: 'error',
- message: t('common.errorMsg.fieldRequired', {
- field: missingField.label,
- }),
- })
- return
- }
-
- try {
- await verifyAuth(credentials)
- notify({
- type: 'success',
- message: t('workflow.nodes.triggerPlugin.credentialsVerified'),
- })
- }
- catch (err: any) {
- notify({
- type: 'error',
- message: t('workflow.nodes.triggerPlugin.credentialVerificationFailed', {
- error: err.message,
- }),
- })
- }
- }
-
- const handleFinalSubmit = async () => {
- if (!subscriptionName.trim()) {
- notify({
- type: 'error',
- message: t('workflow.nodes.triggerPlugin.subscriptionNameRequired'),
- })
- return
- }
-
- try {
- await completeConfig(parameters, properties, subscriptionName)
-
- invalidateSubscriptions(provider.name)
- notify({
- type: 'success',
- message: t('workflow.nodes.triggerPlugin.configurationComplete'),
- })
- onSuccess()
- }
- catch (err: any) {
- notify({
- type: 'error',
- message: t('workflow.nodes.triggerPlugin.configurationFailed', { error: err.message }),
- })
- }
- }
-
- const getTitle = () => {
- switch (step) {
- case 'auth':
- return t('workflow.nodes.triggerPlugin.configureApiKey')
- case 'params':
- return t('workflow.nodes.triggerPlugin.configureParameters')
- case 'complete':
- return t('workflow.nodes.triggerPlugin.configurationComplete')
- default:
- return t('workflow.nodes.triggerPlugin.configureApiKey')
- }
- }
-
- const getDescription = () => {
- switch (step) {
- case 'auth':
- return t('workflow.nodes.triggerPlugin.apiKeyDescription')
- case 'params':
- return t('workflow.nodes.triggerPlugin.parametersDescription')
- case 'complete':
- return t('workflow.nodes.triggerPlugin.configurationCompleteDescription')
- default:
- return ''
- }
- }
-
- const renderContent = () => {
- if (credentialSchema.length === 0 && step === 'auth')
- return
-
- switch (step) {
- case 'auth':
- return (
- <>
-