Support OAuth Integration for Plugin Tools (#22550)

Co-authored-by: zxhlyh <jasonapring2015@outlook.com>
Co-authored-by: Yeuoly <admin@srmxy.cn>
This commit is contained in:
Maries
2025-07-17 17:18:44 +08:00
committed by GitHub
parent 965e952336
commit a4ef900916
89 changed files with 5516 additions and 875 deletions

View File

@@ -0,0 +1,177 @@
import {
isValidElement,
memo,
useMemo,
} from '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'
export type BaseFieldProps = {
fieldClassName?: string
labelClassName?: string
inputContainerClassName?: string
inputClassName?: string
formSchema: FormSchema
field: AnyFieldApi
disabled?: boolean
}
const BaseField = ({
fieldClassName,
labelClassName,
inputContainerClassName,
inputClassName,
formSchema,
field,
disabled,
}: BaseFieldProps) => {
const renderI18nObject = useRenderI18nObject()
const {
label,
required,
placeholder,
options,
labelClassName: formLabelClassName,
show_on = [],
} = formSchema
const memorizedLabel = useMemo(() => {
if (isValidElement(label))
return label
if (typeof label === 'string')
return label
if (typeof label === 'object' && label !== null)
return renderI18nObject(label as Record<string, string>)
}, [label, renderI18nObject])
const memorizedPlaceholder = useMemo(() => {
if (typeof placeholder === 'string')
return placeholder
if (typeof placeholder === 'object' && placeholder !== null)
return renderI18nObject(placeholder as Record<string, string>)
}, [placeholder, renderI18nObject])
const memorizedOptions = useMemo(() => {
return options?.map((option) => {
return {
label: typeof option.label === 'string' ? option.label : renderI18nObject(option.label),
value: option.value,
}
}) || []
}, [options, renderI18nObject])
const value = useStore(field.form.store, s => s.values[field.name])
const values = useStore(field.form.store, (s) => {
return show_on.reduce((acc, condition) => {
acc[condition.variable] = s.values[condition.variable]
return acc
}, {} as Record<string, any>)
})
const show = useMemo(() => {
return show_on.every((condition) => {
const conditionValue = values[condition.variable]
return conditionValue === condition.value
})
}, [values, show_on])
if (!show)
return null
return (
<div className={cn(fieldClassName)}>
<div className={cn(labelClassName, formLabelClassName)}>
{memorizedLabel}
{
required && !isValidElement(label) && (
<span className='ml-1 text-text-destructive-secondary'>*</span>
)
}
</div>
<div className={cn(inputContainerClassName)}>
{
formSchema.type === FormTypeEnum.textInput && (
<Input
id={field.name}
name={field.name}
className={cn(inputClassName)}
value={value || ''}
onChange={e => field.handleChange(e.target.value)}
onBlur={field.handleBlur}
disabled={disabled}
placeholder={memorizedPlaceholder}
/>
)
}
{
formSchema.type === FormTypeEnum.secretInput && (
<Input
id={field.name}
name={field.name}
type='password'
className={cn(inputClassName)}
value={value || ''}
onChange={e => field.handleChange(e.target.value)}
onBlur={field.handleBlur}
disabled={disabled}
placeholder={memorizedPlaceholder}
/>
)
}
{
formSchema.type === FormTypeEnum.textNumber && (
<Input
id={field.name}
name={field.name}
type='number'
className={cn(inputClassName)}
value={value || ''}
onChange={e => field.handleChange(e.target.value)}
onBlur={field.handleBlur}
disabled={disabled}
placeholder={memorizedPlaceholder}
/>
)
}
{
formSchema.type === FormTypeEnum.select && (
<PureSelect
value={value}
onChange={v => field.handleChange(v)}
disabled={disabled}
placeholder={memorizedPlaceholder}
options={memorizedOptions}
triggerPopupSameWidth
/>
)
}
{
formSchema.type === FormTypeEnum.radio && (
<div className='flex items-center space-x-2'>
{
memorizedOptions.map(option => (
<div
key={option.value}
className={cn(
'system-sm-regular hover:bg-components-option-card-option-hover-bg hover:border-components-option-card-option-hover-border flex h-8 grow cursor-pointer items-center justify-center rounded-lg border border-components-option-card-option-border bg-components-option-card-option-bg p-2 text-text-secondary',
value === option.value && 'border-components-option-card-option-selected-border bg-components-option-card-option-selected-bg text-text-primary shadow-xs',
)}
onClick={() => field.handleChange(option.value)}
>
{option.label}
</div>
))
}
</div>
)
}
</div>
</div>
)
}
export default memo(BaseField)

View File

@@ -0,0 +1,115 @@
import {
memo,
useCallback,
useImperativeHandle,
} from 'react'
import type {
AnyFieldApi,
AnyFormApi,
} from '@tanstack/react-form'
import { useForm } from '@tanstack/react-form'
import type {
FormRef,
FormSchema,
} from '@/app/components/base/form/types'
import {
BaseField,
} from '.'
import type {
BaseFieldProps,
} from '.'
import cn from '@/utils/classnames'
import {
useGetFormValues,
useGetValidators,
} from '@/app/components/base/form/hooks'
export type BaseFormProps = {
formSchemas?: FormSchema[]
defaultValues?: Record<string, any>
formClassName?: string
ref?: FormRef
disabled?: boolean
formFromProps?: AnyFormApi
} & Pick<BaseFieldProps, 'fieldClassName' | 'labelClassName' | 'inputContainerClassName' | 'inputClassName'>
const BaseForm = ({
formSchemas = [],
defaultValues,
formClassName,
fieldClassName,
labelClassName,
inputContainerClassName,
inputClassName,
ref,
disabled,
formFromProps,
}: BaseFormProps) => {
const formFromHook = useForm({
defaultValues,
})
const form: any = formFromProps || formFromHook
const { getFormValues } = useGetFormValues(form, formSchemas)
const { getValidators } = useGetValidators()
useImperativeHandle(ref, () => {
return {
getForm() {
return form
},
getFormValues: (option) => {
return getFormValues(option)
},
}
}, [form, getFormValues])
const renderField = useCallback((field: AnyFieldApi) => {
const formSchema = formSchemas?.find(schema => schema.name === field.name)
if (formSchema) {
return (
<BaseField
field={field}
formSchema={formSchema}
fieldClassName={fieldClassName}
labelClassName={labelClassName}
inputContainerClassName={inputContainerClassName}
inputClassName={inputClassName}
disabled={disabled}
/>
)
}
return null
}, [formSchemas, fieldClassName, labelClassName, inputContainerClassName, inputClassName, disabled])
const renderFieldWrapper = useCallback((formSchema: FormSchema) => {
const validators = getValidators(formSchema)
const {
name,
} = formSchema
return (
<form.Field
key={name}
name={name}
validators={validators}
>
{renderField}
</form.Field>
)
}, [renderField, form, getValidators])
if (!formSchemas?.length)
return null
return (
<form
className={cn(formClassName)}
>
{formSchemas.map(renderFieldWrapper)}
</form>
)
}
export default memo(BaseForm)

View File

@@ -0,0 +1,2 @@
export { default as BaseForm, type BaseFormProps } from './base-form'
export { default as BaseField, type BaseFieldProps } from './base-field'

View File

@@ -0,0 +1,23 @@
import { memo } from 'react'
import { BaseForm } from '../../components/base'
import type { BaseFormProps } from '../../components/base'
const AuthForm = ({
formSchemas = [],
defaultValues,
ref,
formFromProps,
}: BaseFormProps) => {
return (
<BaseForm
ref={ref}
formSchemas={formSchemas}
defaultValues={defaultValues}
formClassName='space-y-4'
labelClassName='h-6 flex items-center mb-1 system-sm-medium text-text-secondary'
formFromProps={formFromProps}
/>
)
}
export default memo(AuthForm)

View File

@@ -0,0 +1,3 @@
export * from './use-check-validated'
export * from './use-get-form-values'
export * from './use-get-validators'

View File

@@ -0,0 +1,48 @@
import { useCallback } from 'react'
import type { AnyFormApi } from '@tanstack/react-form'
import { useToastContext } from '@/app/components/base/toast'
import type { FormSchema } from '@/app/components/base/form/types'
export const useCheckValidated = (form: AnyFormApi, FormSchemas: FormSchema[]) => {
const { notify } = useToastContext()
const checkValidated = useCallback(() => {
const allError = form?.getAllErrors()
const values = form.state.values
if (allError) {
const fields = allError.fields
const errorArray = Object.keys(fields).reduce((acc: string[], key: string) => {
const currentSchema = FormSchemas.find(schema => schema.name === key)
const { show_on = [] } = currentSchema || {}
const showOnValues = show_on.reduce((acc, condition) => {
acc[condition.variable] = values[condition.variable]
return acc
}, {} as Record<string, any>)
const show = show_on?.every((condition) => {
const conditionValue = showOnValues[condition.variable]
return conditionValue === condition.value
})
const errors: any[] = show ? fields[key].errors : []
return [...acc, ...errors]
}, [] as string[])
if (errorArray.length) {
notify({
type: 'error',
message: errorArray[0],
})
return false
}
return true
}
return true
}, [form, notify, FormSchemas])
return {
checkValidated,
}
}

View File

@@ -0,0 +1,44 @@
import { useCallback } from 'react'
import type { AnyFormApi } from '@tanstack/react-form'
import { useCheckValidated } from './use-check-validated'
import type {
FormSchema,
GetValuesOptions,
} from '../types'
import { getTransformedValuesWhenSecretInputPristine } from '../utils'
export const useGetFormValues = (form: AnyFormApi, formSchemas: FormSchema[]) => {
const { checkValidated } = useCheckValidated(form, formSchemas)
const getFormValues = useCallback((
{
needCheckValidatedValues,
needTransformWhenSecretFieldIsPristine,
}: GetValuesOptions,
) => {
const values = form?.store.state.values || {}
if (!needCheckValidatedValues) {
return {
values,
isCheckValidated: false,
}
}
if (checkValidated()) {
return {
values: needTransformWhenSecretFieldIsPristine ? getTransformedValuesWhenSecretInputPristine(formSchemas, form) : values,
isCheckValidated: true,
}
}
else {
return {
values: {},
isCheckValidated: false,
}
}
}, [form, checkValidated, formSchemas])
return {
getFormValues,
}
}

View File

@@ -0,0 +1,36 @@
import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import type { FormSchema } from '../types'
export const useGetValidators = () => {
const { t } = useTranslation()
const getValidators = useCallback((formSchema: FormSchema) => {
const {
name,
validators,
required,
} = formSchema
let mergedValidators = validators
if (required && !validators) {
mergedValidators = {
onMount: ({ value }: any) => {
if (!value)
return t('common.errorMsg.fieldRequired', { field: name })
},
onChange: ({ value }: any) => {
if (!value)
return t('common.errorMsg.fieldRequired', { field: name })
},
onBlur: ({ value }: any) => {
if (!value)
return t('common.errorMsg.fieldRequired', { field: name })
},
}
}
return mergedValidators
}, [t])
return {
getValidators,
}
}

View File

@@ -0,0 +1,76 @@
import type {
ForwardedRef,
ReactNode,
} from 'react'
import type {
AnyFormApi,
FieldValidators,
} from '@tanstack/react-form'
export type TypeWithI18N<T = string> = {
en_US: T
zh_Hans: T
[key: string]: T
}
export type FormShowOnObject = {
variable: string
value: string
}
export enum FormTypeEnum {
textInput = 'text-input',
textNumber = 'number-input',
secretInput = 'secret-input',
select = 'select',
radio = 'radio',
boolean = 'boolean',
files = 'files',
file = 'file',
modelSelector = 'model-selector',
toolSelector = 'tool-selector',
multiToolSelector = 'array[tools]',
appSelector = 'app-selector',
dynamicSelect = 'dynamic-select',
}
export type FormOption = {
label: TypeWithI18N | string
value: string
show_on?: FormShowOnObject[]
icon?: string
}
export type AnyValidators = FieldValidators<any, any, any, any, any, any, any, any, any, any>
export type FormSchema = {
type: FormTypeEnum
name: string
label: string | ReactNode | TypeWithI18N
required: boolean
default?: any
tooltip?: string | TypeWithI18N
show_on?: FormShowOnObject[]
url?: string
scope?: string
help?: string | TypeWithI18N
placeholder?: string | TypeWithI18N
options?: FormOption[]
labelClassName?: string
validators?: AnyValidators
}
export type FormValues = Record<string, any>
export type GetValuesOptions = {
needTransformWhenSecretFieldIsPristine?: boolean
needCheckValidatedValues?: boolean
}
export type FormRefObject = {
getForm: () => AnyFormApi
getFormValues: (obj: GetValuesOptions) => {
values: Record<string, any>
isCheckValidated: boolean
}
}
export type FormRef = ForwardedRef<FormRefObject>

View File

@@ -0,0 +1 @@
export * from './secret-input'

View File

@@ -0,0 +1,29 @@
import type { AnyFormApi } from '@tanstack/react-form'
import type { FormSchema } from '@/app/components/base/form/types'
import { FormTypeEnum } from '@/app/components/base/form/types'
export const transformFormSchemasSecretInput = (isPristineSecretInputNames: string[], values: Record<string, any>) => {
const transformedValues: Record<string, any> = { ...values }
isPristineSecretInputNames.forEach((name) => {
if (transformedValues[name])
transformedValues[name] = '[__HIDDEN__]'
})
return transformedValues
}
export const getTransformedValuesWhenSecretInputPristine = (formSchemas: FormSchema[], form: AnyFormApi) => {
const values = form?.store.state.values || {}
const isPristineSecretInputNames: string[] = []
for (let i = 0; i < formSchemas.length; i++) {
const schema = formSchemas[i]
if (schema.type === FormTypeEnum.secretInput) {
const fieldMeta = form?.getFieldMeta(schema.name)
if (fieldMeta?.isPristine)
isPristineSecretInputNames.push(schema.name)
}
}
return transformFormSchemasSecretInput(isPristineSecretInputNames, values)
}