feat: oauth config opt & add dynamic options

This commit is contained in:
yessenia 2025-09-18 15:45:09 +08:00
parent 0edf06329f
commit eae65e55ce
8 changed files with 129 additions and 61 deletions

View File

@ -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 && (
<PortalSelect
value={value}
onSelect={(item: any) => field.handleChange(item.value)}
readonly={disabled || isDynamicOptionsLoading}
placeholder={
isDynamicOptionsLoading
? 'Loading options...'
: memorizedPlaceholder || 'Select an option'
}
items={dynamicOptions}
popupClassName="z-[9999]"
/>
)
}
{
formItemType === FormTypeEnum.radio && (
<div className={cn(

View File

@ -62,6 +62,13 @@ export type FormSchema = {
validators?: AnyValidators
showRadioUI?: boolean
disabled?: boolean
dynamicSelectParams?: {
plugin_id: string
provider: string
action: string
parameter: string
credential_id: string
}
}
export type FormValues = Record<string, any>

View File

@ -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, TriggerCredentialTypeEnum> = {
[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) => {
</div>
{createType !== SupportedCreationMethods.MANUAL && parametersSchema.length > 0 && (
<BaseForm
formSchemas={parametersSchema}
formSchemas={parametersSchema.map(schema => ({
...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'
/>

View File

@ -202,6 +202,7 @@ export const CreateSubscriptionButton = ({ buttonType = CreateButtonType.FULL_BU
<OAuthClientSettingsModal
oauthConfig={oauthConfig}
onClose={hideClientSettingsModal}
showOAuthCreateModal={() => setSelectedCreateType(SupportedCreationMethods.OAUTH)}
/>
)}
</>

View File

@ -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<TriggerSubscriptionBuilder | undefined>()
const [authorizationStatus, setAuthorizationStatus] = useState<AuthorizationStatusEnum>()
@ -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 (
<Modal
title={t('pluginTrigger.modal.oauth.title')}
confirmButtonText={t('plugin.auth.saveAndAuth')}
confirmButtonText={authorizationStatus === AuthorizationStatusEnum.Pending ? t('pluginTrigger.modal.common.authorizing')
: authorizationStatus === AuthorizationStatusEnum.Success ? t('pluginTrigger.modal.oauth.authorization.waitingJump') : t('plugin.auth.saveAndAuth')}
cancelButtonText={t('plugin.auth.saveOnly')}
extraButtonText={t('common.operation.cancel')}
showExtraButton
extraButtonVariant='secondary'
onExtraButtonClick={onClose}
onClose={onClose}
onCancel={handleSaveOnly}
onConfirm={handleSaveAuthorize}
onCancel={() => handleSave(false)}
onConfirm={() => handleSave(true)}
footerSlot={
oauthConfig?.custom_enabled && oauthConfig?.params && (
<div className='grow'>

View File

@ -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',

View File

@ -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',

View File

@ -166,7 +166,7 @@ export const useVerifyTriggerSubscriptionBuilder = () => {
credentials?: Record<string, any>
}) => {
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<string, any>
}, 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,
})
}