diff --git a/web/app/components/base/form/components/base/base-field.tsx b/web/app/components/base/form/components/base/base-field.tsx index 1be94f1317..552dbfbc37 100644 --- a/web/app/components/base/form/components/base/base-field.tsx +++ b/web/app/components/base/form/components/base/base-field.tsx @@ -1,19 +1,21 @@ +import type { FormSchema } from '@/app/components/base/form/types' +import { FormTypeEnum } from '@/app/components/base/form/types' +import Input from '@/app/components/base/input' +import Radio from '@/app/components/base/radio' +import RadioE from '@/app/components/base/radio/ui' +import { PortalSelect } from '@/app/components/base/select' +import PureSelect from '@/app/components/base/select/pure' +import { useRenderI18nObject } from '@/hooks/use-i18n' +import { useTriggerPluginDynamicOptions } from '@/service/use-triggers' +import cn from '@/utils/classnames' +import { RiExternalLinkLine } from '@remixicon/react' +import type { AnyFieldApi } from '@tanstack/react-form' +import { useStore } from '@tanstack/react-form' import { isValidElement, memo, useMemo, } from 'react' -import { RiExternalLinkLine } from '@remixicon/react' -import type { AnyFieldApi } from '@tanstack/react-form' -import { useStore } from '@tanstack/react-form' -import cn from '@/utils/classnames' -import Input from '@/app/components/base/input' -import PureSelect from '@/app/components/base/select/pure' -import type { FormSchema } from '@/app/components/base/form/types' -import { FormTypeEnum } from '@/app/components/base/form/types' -import { useRenderI18nObject } from '@/hooks/use-i18n' -import Radio from '@/app/components/base/radio' -import RadioE from '@/app/components/base/radio/ui' const getInputType = (type: FormTypeEnum) => { switch (type) { @@ -56,6 +58,7 @@ const BaseField = ({ disabled: formSchemaDisabled, showRadioUI, type: formItemType, + dynamicSelectParams, } = formSchema const disabled = propsDisabled || formSchemaDisabled @@ -115,6 +118,26 @@ const BaseField = ({ const value = useStore(field.form.store, s => s.values[field.name]) + const { data: dynamicOptionsData, isLoading: isDynamicOptionsLoading } = useTriggerPluginDynamicOptions( + dynamicSelectParams || { + plugin_id: '', + provider: '', + action: '', + parameter: '', + credential_id: '', + }, + formItemType === FormTypeEnum.dynamicSelect, + ) + + const dynamicOptions = useMemo(() => { + if (!dynamicOptionsData?.options) + return [] + return dynamicOptionsData.options.map(option => ({ + name: typeof option.label === 'string' ? option.label : renderI18nObject(option.label), + value: option.value, + })) + }, [dynamicOptionsData, renderI18nObject]) + const show = useMemo(() => { return show_on.every((condition) => { return watchedValues[condition.variable] === condition.value @@ -168,6 +191,22 @@ const BaseField = ({ /> ) } + { + formItemType === FormTypeEnum.dynamicSelect && ( + field.handleChange(item.value)} + readonly={disabled || isDynamicOptionsLoading} + placeholder={ + isDynamicOptionsLoading + ? 'Loading options...' + : memorizedPlaceholder || 'Select an option' + } + items={dynamicOptions} + popupClassName="z-[9999]" + /> + ) + } { formItemType === FormTypeEnum.radio && (
diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/create/common-modal.tsx b/web/app/components/plugins/plugin-detail-panel/subscription-list/create/common-modal.tsx index cea576bb94..13ac0e7d63 100644 --- a/web/app/components/plugins/plugin-detail-panel/subscription-list/create/common-modal.tsx +++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/create/common-modal.tsx @@ -2,6 +2,7 @@ import { CopyFeedbackNew } from '@/app/components/base/copy-feedback' import { BaseForm } from '@/app/components/base/form/components/base' import type { FormRefObject } from '@/app/components/base/form/types' +import { FormTypeEnum } from '@/app/components/base/form/types' import Input from '@/app/components/base/input' import Modal from '@/app/components/base/modal/modal' import Toast from '@/app/components/base/toast' @@ -25,6 +26,12 @@ type Props = { createType: SupportedCreationMethods } +const CREDENTIAL_TYPE_MAP: Record = { + [SupportedCreationMethods.APIKEY]: TriggerCredentialTypeEnum.ApiKey, + [SupportedCreationMethods.OAUTH]: TriggerCredentialTypeEnum.Oauth2, + [SupportedCreationMethods.MANUAL]: TriggerCredentialTypeEnum.Unauthorized, +} + enum ApiKeyStep { Verify = 'verify', Configuration = 'configuration', @@ -87,7 +94,7 @@ export const CommonCreateModal = ({ onClose, createType }: Props) => { createBuilder( { provider: providerName, - credential_type: TriggerCredentialTypeEnum.Unauthorized, + credential_type: CREDENTIAL_TYPE_MAP[createType], }, { onSuccess: (response) => { @@ -262,7 +269,16 @@ export const CommonCreateModal = ({ onClose, createType }: Props) => {
{createType !== SupportedCreationMethods.MANUAL && parametersSchema.length > 0 && ( ({ + ...schema, + dynamicSelectParams: schema.type === FormTypeEnum.dynamicSelect ? { + plugin_id: detail?.plugin_id || '', + provider: providerName, + action: 'provider', + parameter: schema.name, + credential_id: subscriptionBuilder?.id || '', + } : undefined, + }))} ref={parametersFormRef} labelClassName='system-sm-medium mb-2 block text-text-primary' /> diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/create/index.tsx b/web/app/components/plugins/plugin-detail-panel/subscription-list/create/index.tsx index a97181b570..f8fe63fb75 100644 --- a/web/app/components/plugins/plugin-detail-panel/subscription-list/create/index.tsx +++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/create/index.tsx @@ -202,6 +202,7 @@ export const CreateSubscriptionButton = ({ buttonType = CreateButtonType.FULL_BU setSelectedCreateType(SupportedCreationMethods.OAUTH)} /> )} diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/create/oauth-client.tsx b/web/app/components/plugins/plugin-detail-panel/subscription-list/create/oauth-client.tsx index 4befe9f4d1..1a92bd6547 100644 --- a/web/app/components/plugins/plugin-detail-panel/subscription-list/create/oauth-client.tsx +++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/create/oauth-client.tsx @@ -6,6 +6,7 @@ import Modal from '@/app/components/base/modal/modal' import Toast from '@/app/components/base/toast' import type { TriggerOAuthClientParams, TriggerOAuthConfig, TriggerSubscriptionBuilder } from '@/app/components/workflow/block-selector/types' import OptionCard from '@/app/components/workflow/nodes/_base/components/option-card' +import { openOAuthPopup } from '@/hooks/use-oauth' import { useConfigureTriggerOAuth, useDeleteTriggerOAuth, @@ -23,6 +24,7 @@ import { usePluginStore } from '../../store' type Props = { oauthConfig?: TriggerOAuthConfig onClose: () => void + showOAuthCreateModal: () => void } enum AuthorizationStatusEnum { @@ -36,10 +38,9 @@ enum ClientTypeEnum { Custom = 'custom', } -export const OAuthClientSettingsModal = ({ oauthConfig, onClose }: Props) => { +export const OAuthClientSettingsModal = ({ oauthConfig, onClose, showOAuthCreateModal }: Props) => { const { t } = useTranslation() const detail = usePluginStore(state => state.detail) - const [authorizationUrl, setAuthorizationUrl] = useState('') const [subscriptionBuilder, setSubscriptionBuilder] = useState() const [authorizationStatus, setAuthorizationStatus] = useState() @@ -55,22 +56,30 @@ export const OAuthClientSettingsModal = ({ oauthConfig, onClose }: Props) => { const { mutate: configureOAuth } = useConfigureTriggerOAuth() const { mutate: deleteOAuth } = useDeleteTriggerOAuth() - useEffect(() => { - if (providerName && oauthConfig?.params.client_id && oauthConfig?.params.client_secret) { - initiateOAuth(providerName, { - onSuccess: (response) => { - setAuthorizationUrl(response.authorization_url) - setSubscriptionBuilder(response.subscription_builder) - }, - onError: (error: any) => { - Toast.notify({ - type: 'error', - message: error?.message || t('pluginTrigger.modal.errors.authFailed'), - }) - }, - }) - } - }, [initiateOAuth, providerName, t, oauthConfig]) + const handleAuthorization = () => { + setAuthorizationStatus(AuthorizationStatusEnum.Pending) + initiateOAuth(providerName, { + onSuccess: (response) => { + setSubscriptionBuilder(response.subscription_builder) + openOAuthPopup(response.authorization_url, (callbackData) => { + if (callbackData) { + Toast.notify({ + type: 'success', + message: t('pluginTrigger.modal.oauth.authorization.authSuccess'), + }) + onClose() + showOAuthCreateModal() + } + }) + }, + onError: (error: any) => { + Toast.notify({ + type: 'error', + message: error?.message || t('pluginTrigger.modal.errors.authFailed'), + }) + }, + }) + } useEffect(() => { if (providerName && subscriptionBuilder && authorizationStatus === AuthorizationStatusEnum.Pending) { @@ -81,14 +90,11 @@ export const OAuthClientSettingsModal = ({ oauthConfig, onClose }: Props) => { subscriptionBuilderId: subscriptionBuilder.id, }, { - onSuccess: () => { - setAuthorizationStatus(AuthorizationStatusEnum.Success) - // setCurrentStep(OAuthStepEnum.Configuration) - Toast.notify({ - type: 'success', - message: t('pluginTrigger.modal.oauth.authorization.authSuccess'), - }) - clearInterval(pollInterval) + onSuccess: (response) => { + if (response.verified) { + setAuthorizationStatus(AuthorizationStatusEnum.Success) + clearInterval(pollInterval) + } }, onError: () => { // Continue polling - auth might still be in progress @@ -119,7 +125,7 @@ export const OAuthClientSettingsModal = ({ oauthConfig, onClose }: Props) => { }) } - const handleSaveOnly = () => { + const handleSave = (needAuth: boolean) => { const clientParams = clientFormRef.current?.getFormValues({})?.values || {} if (clientParams.client_id === oauthConfig?.params.client_id) clientParams.client_id = '[__HIDDEN__]' @@ -133,7 +139,11 @@ export const OAuthClientSettingsModal = ({ oauthConfig, onClose }: Props) => { enabled: clientType === ClientTypeEnum.Custom, }, { onSuccess: () => { - onClose() + if (needAuth) + handleAuthorization() + else + onClose() + Toast.notify({ type: 'success', message: t('pluginTrigger.modal.oauth.configuration.success'), @@ -148,27 +158,19 @@ export const OAuthClientSettingsModal = ({ oauthConfig, onClose }: Props) => { }) } - const handleSaveAuthorize = () => { - handleSaveOnly() - if (authorizationUrl) { - setAuthorizationStatus(AuthorizationStatusEnum.Pending) - // Open authorization URL in new window - window.open(authorizationUrl, '_blank', 'width=500,height=600') - } - } - return ( handleSave(false)} + onConfirm={() => handleSave(true)} footerSlot={ oauthConfig?.custom_enabled && oauthConfig?.params && (
diff --git a/web/i18n/en-US/plugin-trigger.ts b/web/i18n/en-US/plugin-trigger.ts index 7e45b85615..0130cd85fb 100644 --- a/web/i18n/en-US/plugin-trigger.ts +++ b/web/i18n/en-US/plugin-trigger.ts @@ -103,6 +103,7 @@ const translation = { waitingAuth: 'Waiting for authorization...', authSuccess: 'Authorization successful', authFailed: 'Authorization failed', + waitingJump: 'Authorized, waiting for jump', }, configuration: { title: 'Configure Subscription', diff --git a/web/i18n/zh-Hans/plugin-trigger.ts b/web/i18n/zh-Hans/plugin-trigger.ts index 197d1c965c..e2718a8900 100644 --- a/web/i18n/zh-Hans/plugin-trigger.ts +++ b/web/i18n/zh-Hans/plugin-trigger.ts @@ -96,13 +96,14 @@ const translation = { title: '通过OAuth创建', authorization: { title: 'OAuth授权', - description: '授权Dify访问您的账户', - redirectUrl: '重定向URL', - redirectUrlHelp: '在您的OAuth应用配置中使用此URL', - authorizeButton: '使用{{provider}}授权', + description: '授权 Dify 访问您的账户', + redirectUrl: '重定向 URL', + redirectUrlHelp: '在您的 OAuth 应用配置中使用此 URL', + authorizeButton: '使用 {{provider}} 授权', waitingAuth: '等待授权中...', authSuccess: '授权成功', authFailed: '授权失败', + waitingJump: '已授权,待跳转', }, configuration: { title: '配置订阅', @@ -112,7 +113,7 @@ const translation = { }, manual: { title: '手动设置', - description: '手动配置您的Webhook订阅', + description: '手动配置您的 Webhook 订阅', instruction: { title: '设置说明', step1: '1. 复制下方的回调URL', diff --git a/web/service/use-triggers.ts b/web/service/use-triggers.ts index bf23d80875..1097fdcef1 100644 --- a/web/service/use-triggers.ts +++ b/web/service/use-triggers.ts @@ -166,7 +166,7 @@ export const useVerifyTriggerSubscriptionBuilder = () => { credentials?: Record }) => { const { provider, subscriptionBuilderId, ...body } = payload - return post( + return post<{ verified: boolean }>( `/workspaces/current/trigger-provider/${provider}/subscriptions/builder/verify/${subscriptionBuilderId}`, { body }, ) @@ -276,10 +276,11 @@ export const useTriggerPluginDynamicOptions = (payload: { provider: string action: string parameter: string + credential_id: string extra?: Record }, enabled = true) => { return useQuery<{ options: Array<{ value: string; label: any }> }>({ - queryKey: [NAME_SPACE, 'dynamic-options', payload.plugin_id, payload.provider, payload.action, payload.parameter, payload.extra], + queryKey: [NAME_SPACE, 'dynamic-options', payload.plugin_id, payload.provider, payload.action, payload.parameter, payload.credential_id, payload.extra], queryFn: () => get<{ options: Array<{ value: string; label: any }> }>( '/workspaces/current/plugin/parameters/dynamic-options', { @@ -289,7 +290,7 @@ export const useTriggerPluginDynamicOptions = (payload: { }, }, ), - enabled: enabled && !!payload.plugin_id && !!payload.provider && !!payload.action && !!payload.parameter, + enabled: enabled && !!payload.plugin_id && !!payload.provider && !!payload.action && !!payload.parameter && !!payload.credential_id, }) }