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 ( - <> -
item.url ? ( - - {t('tools.howToGet')} - - - ) : null} - /> -
- - -
- - ) - - case 'params': - return ( - - ) - - case 'complete': - return ( -
-
- - - -
-

- {t('workflow.nodes.triggerPlugin.configurationCompleteMessage')} -

- -
- ) - - default: - return null - } - } - - return ( - - {renderContent()} - - } - isShowMask={true} - clickOutsideNotOpen={false} - /> - ) -} - -export default React.memo(ApiKeyConfigModal) diff --git a/web/app/components/workflow/nodes/trigger-plugin/components/auth-method-selector.tsx b/web/app/components/workflow/nodes/trigger-plugin/components/auth-method-selector.tsx deleted file mode 100644 index 12cf853d92..0000000000 --- a/web/app/components/workflow/nodes/trigger-plugin/components/auth-method-selector.tsx +++ /dev/null @@ -1,158 +0,0 @@ -'use client' -import type { FC } from 'react' -import { useCallback, useState } from 'react' -import { useTranslation } from 'react-i18next' -import { RiEqualizer2Line } from '@remixicon/react' -import Button from '@/app/components/base/button' -import type { TriggerWithProvider } from '@/app/components/workflow/block-selector/types' -import { - useInitiateTriggerOAuth, - useInvalidateTriggerSubscriptions, - useTriggerOAuthConfig, -} from '@/service/use-triggers' -import { useToastContext } from '@/app/components/base/toast' -import { openOAuthPopup } from '@/hooks/use-oauth' -import ApiKeyConfigModal from './api-key-config-modal' -import OAuthClientConfigModal from './oauth-client-config-modal' - -type AuthMethodSelectorProps = { - provider: TriggerWithProvider - supportedMethods: string[] -} - -const AuthMethodSelector: FC = ({ - provider, - supportedMethods, -}) => { - const { t } = useTranslation() - const { notify } = useToastContext() - const [showApiKeyModal, setShowApiKeyModal] = useState(false) - const [showOAuthClientModal, setShowOAuthClientModal] = useState(false) - const initiateTriggerOAuth = useInitiateTriggerOAuth() - const invalidateSubscriptions = useInvalidateTriggerSubscriptions() - - const { data: oauthConfig } = useTriggerOAuthConfig(provider.name, supportedMethods.includes('oauth')) - - const handleOAuthAuth = useCallback(async () => { - // Check if OAuth client is configured - if (!oauthConfig?.custom_configured || !oauthConfig?.custom_enabled) { - // Need to configure OAuth client first - setShowOAuthClientModal(true) - return - } - - try { - const response = await initiateTriggerOAuth.mutateAsync(provider.name) - if (response.authorization_url) { - openOAuthPopup(response.authorization_url, (callbackData) => { - invalidateSubscriptions(provider.name) - - 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: t('workflow.nodes.triggerPlugin.oauthConfigFailed', { error: error.message }), - }) - } - }, [provider.name, initiateTriggerOAuth, invalidateSubscriptions, notify, oauthConfig]) - - const handleApiKeyAuth = useCallback(() => { - setShowApiKeyModal(true) - }, []) - - if (!supportedMethods.includes('oauth') && !supportedMethods.includes('api_key')) - return null - - return ( -
-
- {/* OAuth Button Group */} - {supportedMethods.includes('oauth') && ( -
- -
- -
- )} - - {/* Divider with OR */} - {supportedMethods.includes('oauth') && supportedMethods.includes('api_key') && ( -
-
- {t('workflow.nodes.triggerPlugin.or')} -
-
- )} - - {/* API Key Button */} - {supportedMethods.includes('api_key') && ( -
- -
- )} -
- - {/* API Key Configuration Modal */} - {showApiKeyModal && ( - setShowApiKeyModal(false)} - onSuccess={() => { - setShowApiKeyModal(false) - invalidateSubscriptions(provider.name) - }} - /> - )} - - {/* OAuth Client Configuration Modal */} - {showOAuthClientModal && ( - setShowOAuthClientModal(false)} - onSuccess={() => { - setShowOAuthClientModal(false) - // After OAuth client configuration, proceed with OAuth auth - handleOAuthAuth() - }} - /> - )} -
- ) -} - -export default AuthMethodSelector diff --git a/web/app/components/workflow/nodes/trigger-plugin/components/oauth-client-config-modal.tsx b/web/app/components/workflow/nodes/trigger-plugin/components/oauth-client-config-modal.tsx deleted file mode 100644 index 15657dbb5b..0000000000 --- a/web/app/components/workflow/nodes/trigger-plugin/components/oauth-client-config-modal.tsx +++ /dev/null @@ -1,192 +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 { TriggerOAuthClientParams, 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 { - useConfigureTriggerOAuth, - useInvalidateTriggerOAuthConfig, - useTriggerOAuthConfig, -} from '@/service/use-triggers' -import { useToastContext } from '@/app/components/base/toast' -import { findMissingRequiredField, sanitizeFormValues } from '../utils/form-helpers' - -// Type-safe conversion function for dynamic OAuth client parameters -const convertToOAuthClientParams = (credentials: Record): TriggerOAuthClientParams => { - // Use utility function for consistent data sanitization - const sanitizedCredentials = sanitizeFormValues(credentials) - - // Create base params with required fields - const baseParams: TriggerOAuthClientParams = { - client_id: sanitizedCredentials.client_id || '', - client_secret: sanitizedCredentials.client_secret || '', - } - - // Add optional fields if they exist - if (sanitizedCredentials.authorization_url) - baseParams.authorization_url = sanitizedCredentials.authorization_url - if (sanitizedCredentials.token_url) - baseParams.token_url = sanitizedCredentials.token_url - if (sanitizedCredentials.scope) - baseParams.scope = sanitizedCredentials.scope - - return baseParams -} - -type OAuthClientConfigModalProps = { - provider: TriggerWithProvider - onCancel: () => void - onSuccess: () => void -} - -const OAuthClientConfigModal: FC = ({ - provider, - onCancel, - onSuccess, -}) => { - const { t } = useTranslation() - const { notify } = useToastContext() - const language = useLanguage() - const [credentialSchema, setCredentialSchema] = useState([]) - const [tempCredential, setTempCredential] = useState>({}) - const [isLoading, setIsLoading] = useState(false) - - const { data: oauthConfig, isLoading: isLoadingConfig } = useTriggerOAuthConfig(provider.name) - const configureTriggerOAuth = useConfigureTriggerOAuth() - const invalidateOAuthConfig = useInvalidateTriggerOAuthConfig() - - useEffect(() => { - if (provider.oauth_client_schema) { - const schemas = toolCredentialToFormSchemas(provider.oauth_client_schema as any) - setCredentialSchema(schemas) - - // Load existing configuration if available, ensure no null values - const existingParams = oauthConfig?.params || {} - const defaultCredentials = addDefaultValue(existingParams, schemas) - - // Use utility function for consistent data sanitization - setTempCredential(sanitizeFormValues(defaultCredentials)) - } - }, [provider.oauth_client_schema, oauthConfig]) - - const handleSave = async () => { - // Validate required fields using utility function - const requiredFields = credentialSchema - .filter(field => field.required) - .map(field => ({ - name: field.name, - label: field.label[language] || field.label.en_US, - })) - - const missingField = findMissingRequiredField(tempCredential, requiredFields) - if (missingField) { - Toast.notify({ - type: 'error', - message: t('common.errorMsg.fieldRequired', { - field: missingField.label, - }), - }) - return - } - - setIsLoading(true) - - try { - await configureTriggerOAuth.mutateAsync({ - provider: provider.name, - client_params: convertToOAuthClientParams(tempCredential), - enabled: true, - }) - - // Invalidate cache - invalidateOAuthConfig(provider.name) - - notify({ - type: 'success', - message: t('workflow.nodes.triggerPlugin.oauthClientSaved'), - }) - onSuccess() - } - catch (error: any) { - notify({ - type: 'error', - message: t('workflow.nodes.triggerPlugin.configurationFailed', { error: error.message }), - }) - } - finally { - setIsLoading(false) - } - } - - return ( - - {isLoadingConfig || credentialSchema.length === 0 ? ( - - ) : ( - <> - { - // Use utility function for consistent data sanitization - setTempCredential(sanitizeFormValues(value)) - }} - formSchemas={credentialSchema} - isEditMode={true} - showOnVariableMap={{}} - validating={false} - inputClassName='!bg-components-input-bg-normal' - fieldMoreInfo={item => item.url ? ( - - {t('tools.howToGet')} - - - ) : null} - /> -
- - -
- - )} -
- } - isShowMask={true} - clickOutsideNotOpen={false} - /> - ) -} - -export default React.memo(OAuthClientConfigModal) diff --git a/web/app/components/workflow/nodes/trigger-plugin/components/parameters-form.tsx b/web/app/components/workflow/nodes/trigger-plugin/components/parameters-form.tsx deleted file mode 100644 index 11bbea2431..0000000000 --- a/web/app/components/workflow/nodes/trigger-plugin/components/parameters-form.tsx +++ /dev/null @@ -1,171 +0,0 @@ -'use client' -import type { FC } from 'react' -import { useMemo } from 'react' -import { useTranslation } from 'react-i18next' -import type { TriggerWithProvider } from '@/app/components/workflow/block-selector/types' -import type { Event } from '@/app/components/tools/types' -import { toolCredentialToFormSchemas, toolParametersToFormSchemas } from '@/app/components/tools/utils/to-form-schema' -import TriggerForm from './trigger-form' -import Button from '@/app/components/base/button' -import Input from '@/app/components/base/input' - -type ParametersFormProps = { - provider: TriggerWithProvider - trigger?: Event - builderId: string - parametersValue: Record - propertiesValue: Record - subscriptionName: string - onParametersChange: (value: Record) => void - onPropertiesChange: (value: Record) => void - onSubscriptionNameChange: (value: string) => void - onSubmit: () => void - onCancel: () => void - isLoading?: boolean - readOnly?: boolean -} - -const ParametersForm: FC = ({ - provider, - trigger, - builderId, - parametersValue, - propertiesValue, - subscriptionName, - onParametersChange, - onPropertiesChange, - onSubscriptionNameChange, - onSubmit, - onCancel, - isLoading = false, - readOnly = false, -}) => { - const { t } = useTranslation() - - // Use the first trigger if no specific trigger is provided - // This is needed for dynamic options API which requires a trigger action - const currentTrigger = trigger || provider.triggers?.[0] - - const parametersSchema = useMemo(() => { - if (!provider.subscription_schema?.parameters_schema) return [] - return toolParametersToFormSchemas(provider.subscription_schema.parameters_schema as any) - }, [provider.subscription_schema?.parameters_schema]) - - const propertiesSchema = useMemo(() => { - if (!provider.subscription_schema?.properties_schema) return [] - return toolCredentialToFormSchemas(provider.subscription_schema.properties_schema as any) - }, [provider.subscription_schema?.properties_schema]) - - const hasParameters = parametersSchema.length > 0 - const hasProperties = propertiesSchema.length > 0 - - if (!hasParameters && !hasProperties) { - return ( -
-

- {t('workflow.nodes.triggerPlugin.noConfigurationRequired')} -

-
- - -
-
- ) - } - - return ( -
- {/* Subscription Name Section */} -
-
-

- {t('workflow.nodes.triggerPlugin.subscriptionName')} -

-

- {t('workflow.nodes.triggerPlugin.subscriptionNameDescription')} -

-
- onSubscriptionNameChange(e.target.value)} - placeholder={t('workflow.nodes.triggerPlugin.subscriptionNamePlaceholder')} - readOnly={readOnly} - /> -
- - {/* Parameters Section */} - {hasParameters && ( -
-
-

- {t('workflow.nodes.triggerPlugin.parameters')} -

-

- {t('workflow.nodes.triggerPlugin.parametersDescription')} -

-
- -
- )} - - {/* Properties Section */} - {hasProperties && ( -
-
-

- {t('workflow.nodes.triggerPlugin.properties')} -

-

- {t('workflow.nodes.triggerPlugin.propertiesDescription')} -

-
- -
- )} - - {/* Action Buttons */} -
- - -
-
- ) -} - -export default ParametersForm