mirror of https://github.com/langgenius/dify.git
feat: add checkbox list
This commit is contained in:
parent
4d49db0ff9
commit
3edf1e2f59
|
|
@ -0,0 +1,171 @@
|
|||
'use client'
|
||||
import Badge from '@/app/components/base/badge'
|
||||
import Checkbox from '@/app/components/base/checkbox'
|
||||
import cn from '@/utils/classnames'
|
||||
import type { FC } from 'react'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export type CheckboxListOption = {
|
||||
label: string
|
||||
value: string
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
export type CheckboxListProps = {
|
||||
title?: string
|
||||
label?: string
|
||||
description?: string
|
||||
options: CheckboxListOption[]
|
||||
value?: string[]
|
||||
onChange?: (value: string[]) => void
|
||||
disabled?: boolean
|
||||
containerClassName?: string
|
||||
showSelectAll?: boolean
|
||||
showCount?: boolean
|
||||
maxHeight?: string | number
|
||||
}
|
||||
|
||||
const CheckboxList: FC<CheckboxListProps> = ({
|
||||
title = '',
|
||||
label,
|
||||
description,
|
||||
options,
|
||||
value = [],
|
||||
onChange,
|
||||
disabled = false,
|
||||
containerClassName,
|
||||
showSelectAll = true,
|
||||
showCount = true,
|
||||
maxHeight,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const selectedCount = value.length
|
||||
|
||||
const isAllSelected = useMemo(() => {
|
||||
const selectableOptions = options.filter(option => !option.disabled)
|
||||
return selectableOptions.length > 0 && selectableOptions.every(option => value.includes(option.value))
|
||||
}, [options, value])
|
||||
|
||||
const isIndeterminate = useMemo(() => {
|
||||
const selectableOptions = options.filter(option => !option.disabled)
|
||||
const selectedCount = selectableOptions.filter(option => value.includes(option.value)).length
|
||||
return selectedCount > 0 && selectedCount < selectableOptions.length
|
||||
}, [options, value])
|
||||
|
||||
const handleSelectAll = useCallback(() => {
|
||||
if (disabled)
|
||||
return
|
||||
|
||||
if (isAllSelected) {
|
||||
// Deselect all
|
||||
onChange?.([])
|
||||
}
|
||||
else {
|
||||
// Select all non-disabled options
|
||||
const allValues = options
|
||||
.filter(option => !option.disabled)
|
||||
.map(option => option.value)
|
||||
onChange?.(allValues)
|
||||
}
|
||||
}, [isAllSelected, options, onChange, disabled])
|
||||
|
||||
const handleToggleOption = useCallback((optionValue: string) => {
|
||||
if (disabled)
|
||||
return
|
||||
|
||||
const newValue = value.includes(optionValue)
|
||||
? value.filter(v => v !== optionValue)
|
||||
: [...value, optionValue]
|
||||
onChange?.(newValue)
|
||||
}, [value, onChange, disabled])
|
||||
|
||||
return (
|
||||
<div className={cn('flex flex-col gap-1', containerClassName)}>
|
||||
{label && (
|
||||
<div className='system-sm-medium text-text-secondary'>
|
||||
{label}
|
||||
</div>
|
||||
)}
|
||||
{description && (
|
||||
<div className='body-xs-regular text-text-tertiary'>
|
||||
{description}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className='rounded-lg border border-components-panel-border bg-components-panel-bg'>
|
||||
{(showSelectAll || title) && (
|
||||
<div className='relative flex items-center gap-2 border-b border-divider-subtle px-3 py-2'>
|
||||
{showSelectAll && (
|
||||
<Checkbox
|
||||
checked={isAllSelected}
|
||||
indeterminate={isIndeterminate}
|
||||
onCheck={handleSelectAll}
|
||||
disabled={disabled}
|
||||
/>
|
||||
)}
|
||||
<div className='flex flex-1 items-center gap-1'>
|
||||
{title && (
|
||||
<span className='system-xs-semibold-uppercase leading-5 text-text-secondary'>
|
||||
{title}
|
||||
</span>
|
||||
)}
|
||||
{showCount && selectedCount > 0 && (
|
||||
<Badge uppercase>
|
||||
{t('common.operation.selectCount', { count: selectedCount })}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
className='p-1'
|
||||
style={maxHeight ? { maxHeight, overflowY: 'auto' } : {}}
|
||||
>
|
||||
{!options.length ? (
|
||||
<div className='px-3 py-6 text-center text-sm text-text-tertiary'>
|
||||
{t('common.noData')}
|
||||
</div>
|
||||
) : (
|
||||
options.map((option) => {
|
||||
const selected = value.includes(option.value)
|
||||
|
||||
return (
|
||||
<div
|
||||
key={option.value}
|
||||
className={cn(
|
||||
'flex cursor-pointer items-center gap-2 rounded-md px-2 py-1.5 transition-colors hover:bg-state-base-hover',
|
||||
option.disabled && 'cursor-not-allowed opacity-50',
|
||||
)}
|
||||
onClick={() => {
|
||||
if (!option.disabled && !disabled)
|
||||
handleToggleOption(option.value)
|
||||
}}
|
||||
>
|
||||
<Checkbox
|
||||
checked={selected}
|
||||
onCheck={() => {
|
||||
if (!option.disabled && !disabled)
|
||||
handleToggleOption(option.value)
|
||||
}}
|
||||
disabled={option.disabled || disabled}
|
||||
/>
|
||||
<div
|
||||
className='system-sm-medium flex-1 truncate text-text-secondary'
|
||||
title={option.label}
|
||||
>
|
||||
{option.label}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CheckboxList
|
||||
|
|
@ -30,7 +30,7 @@ const Checkbox = ({
|
|||
<div
|
||||
id={id}
|
||||
className={cn(
|
||||
'flex h-4 w-4 cursor-pointer items-center justify-center rounded-[4px] shadow-xs shadow-shadow-shadow-3',
|
||||
'flex h-4 w-4 shrink-0 cursor-pointer items-center justify-center rounded-[4px] shadow-xs shadow-shadow-shadow-3',
|
||||
checkClassName,
|
||||
disabled && disabledClassName,
|
||||
className,
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import CheckboxList from '@/app/components/base/checkbox-list'
|
||||
import type { FormSchema } from '@/app/components/base/form/types'
|
||||
import { FormTypeEnum } from '@/app/components/base/form/types'
|
||||
import Input from '@/app/components/base/input'
|
||||
|
|
@ -5,6 +6,7 @@ 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 Tooltip from '@/app/components/base/tooltip'
|
||||
import { useRenderI18nObject } from '@/hooks/use-i18n'
|
||||
import { useTriggerPluginDynamicOptions } from '@/service/use-triggers'
|
||||
import cn from '@/utils/classnames'
|
||||
|
|
@ -52,6 +54,7 @@ const BaseField = ({
|
|||
}: BaseFieldProps) => {
|
||||
const renderI18nObject = useRenderI18nObject()
|
||||
const {
|
||||
name,
|
||||
label,
|
||||
required,
|
||||
placeholder,
|
||||
|
|
@ -60,6 +63,8 @@ const BaseField = ({
|
|||
disabled: formSchemaDisabled,
|
||||
type: formItemType,
|
||||
dynamicSelectParams,
|
||||
multiple = false,
|
||||
tooltip,
|
||||
} = formSchema
|
||||
const disabled = propsDisabled || formSchemaDisabled
|
||||
|
||||
|
|
@ -150,6 +155,12 @@ const BaseField = ({
|
|||
<span className='ml-1 text-text-destructive-secondary'>*</span>
|
||||
)
|
||||
}
|
||||
{tooltip && (
|
||||
<Tooltip
|
||||
popupContent={<div className='w-[200px]'>{typeof tooltip === 'string' ? tooltip : renderI18nObject(tooltip as Record<string, string>)}</div>}
|
||||
triggerClassName='ml-0.5 w-4 h-4'
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className={cn(inputContainerClassName)}>
|
||||
{
|
||||
|
|
@ -170,7 +181,7 @@ const BaseField = ({
|
|||
)
|
||||
}
|
||||
{
|
||||
formItemType === FormTypeEnum.select && (
|
||||
formItemType === FormTypeEnum.select && !multiple && (
|
||||
<PureSelect
|
||||
value={value}
|
||||
onChange={v => handleChange(v)}
|
||||
|
|
@ -184,6 +195,17 @@ const BaseField = ({
|
|||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
formItemType === FormTypeEnum.select && multiple && (
|
||||
<CheckboxList
|
||||
title={name}
|
||||
value={value}
|
||||
onChange={v => field.handleChange(v)}
|
||||
options={memorizedOptions}
|
||||
maxHeight='200px'
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
formItemType === FormTypeEnum.dynamicSelect && (
|
||||
<PortalSelect
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ export const useGetFormValues = (form: AnyFormApi, formSchemas: FormSchema[]) =>
|
|||
|
||||
const getFormValues = useCallback((
|
||||
{
|
||||
needCheckValidatedValues,
|
||||
needCheckValidatedValues = true,
|
||||
needTransformWhenSecretFieldIsPristine,
|
||||
}: GetValuesOptions,
|
||||
) => {
|
||||
|
|
@ -20,7 +20,7 @@ export const useGetFormValues = (form: AnyFormApi, formSchemas: FormSchema[]) =>
|
|||
if (!needCheckValidatedValues) {
|
||||
return {
|
||||
values,
|
||||
isCheckValidated: false,
|
||||
isCheckValidated: true,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ export type FormSchema = {
|
|||
name: string
|
||||
label: string | ReactNode | TypeWithI18N | Record<Locale, string>
|
||||
required: boolean
|
||||
multiple?: boolean
|
||||
default?: any
|
||||
tooltip?: string | TypeWithI18N | Record<Locale, string>
|
||||
show_on?: FormShowOnObject[]
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
'use client'
|
||||
import { CopyFeedbackNew } from '@/app/components/base/copy-feedback'
|
||||
// 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'
|
||||
import { SupportedCreationMethods } from '@/app/components/plugins/types'
|
||||
|
|
@ -66,7 +65,6 @@ export const CommonCreateModal = ({ onClose, createType, builder }: Props) => {
|
|||
|
||||
const [currentStep, setCurrentStep] = useState<ApiKeyStep>(createType === SupportedCreationMethods.APIKEY ? ApiKeyStep.Verify : ApiKeyStep.Configuration)
|
||||
|
||||
const [subscriptionName, setSubscriptionName] = useState('')
|
||||
const [subscriptionBuilder, setSubscriptionBuilder] = useState<TriggerSubscriptionBuilder | undefined>(builder)
|
||||
const [verificationError, setVerificationError] = useState<string>('')
|
||||
|
||||
|
|
@ -76,6 +74,7 @@ export const CommonCreateModal = ({ onClose, createType, builder }: Props) => {
|
|||
|
||||
const providerName = `${detail?.plugin_id}/${detail?.declaration.name}`
|
||||
const propertiesSchema = detail?.declaration.trigger.subscription_schema.properties_schema || [] // manual
|
||||
const subscriptionFormRef = React.useRef<FormRefObject>(null)
|
||||
const propertiesFormRef = React.useRef<FormRefObject>(null)
|
||||
const parametersSchema = detail?.declaration.trigger?.subscription_schema?.parameters_schema || [] // apikey and oauth
|
||||
const parametersFormRef = React.useRef<FormRefObject>(null)
|
||||
|
|
@ -151,32 +150,23 @@ export const CommonCreateModal = ({ onClose, createType, builder }: Props) => {
|
|||
}
|
||||
|
||||
const handleCreate = () => {
|
||||
if (!subscriptionName.trim()) {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: t('pluginTrigger.modal.form.subscriptionName.required'),
|
||||
})
|
||||
const parameterForm = parametersFormRef.current?.getFormValues({}) || { values: {}, isCheckValidated: false }
|
||||
const subscriptionForm = subscriptionFormRef.current?.getFormValues({})
|
||||
// console.log('parameterForm', parameterForm)
|
||||
|
||||
if (!subscriptionForm?.isCheckValidated || !parameterForm?.isCheckValidated)
|
||||
return
|
||||
}
|
||||
|
||||
if (!subscriptionBuilder)
|
||||
return
|
||||
|
||||
const parameterForm = parametersFormRef.current?.getFormValues({}) || { values: {}, isCheckValidated: false }
|
||||
// console.log('formValues', formValues)
|
||||
// if (!formValues.isCheckValidated) {
|
||||
// Toast.notify({
|
||||
// type: 'error',
|
||||
// message: t('pluginTrigger.modal.form.properties.required'),
|
||||
// })
|
||||
// return
|
||||
// }
|
||||
const subscriptionNameValue = subscriptionForm.values.subscription_name as string
|
||||
|
||||
buildSubscription(
|
||||
{
|
||||
provider: providerName,
|
||||
subscriptionBuilderId: subscriptionBuilder.id,
|
||||
name: subscriptionName,
|
||||
name: subscriptionNameValue,
|
||||
parameters: { ...parameterForm.values, events: ['*'] },
|
||||
// properties: formValues.values,
|
||||
},
|
||||
|
|
@ -228,6 +218,7 @@ export const CommonCreateModal = ({ onClose, createType, builder }: Props) => {
|
|||
ref={credentialsFormRef}
|
||||
labelClassName='system-sm-medium mb-2 block text-text-primary'
|
||||
preventDefaultSubmit={true}
|
||||
formClassName='space-y-4'
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -241,34 +232,39 @@ export const CommonCreateModal = ({ onClose, createType, builder }: Props) => {
|
|||
</>
|
||||
)}
|
||||
{currentStep === ApiKeyStep.Configuration && <div className='max-h-[70vh] overflow-y-auto'>
|
||||
<div className='mb-6'>
|
||||
<label className='system-sm-medium mb-2 block text-text-primary'>
|
||||
{t('pluginTrigger.modal.form.subscriptionName.label')}
|
||||
</label>
|
||||
<Input
|
||||
value={subscriptionName}
|
||||
onChange={e => setSubscriptionName(e.target.value)}
|
||||
placeholder={t('pluginTrigger.modal.form.subscriptionName.placeholder')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='mb-6'>
|
||||
<label className='system-sm-medium mb-2 block text-text-primary'>
|
||||
{t('pluginTrigger.modal.form.callbackUrl.label')}
|
||||
</label>
|
||||
<div className='relative'>
|
||||
<Input
|
||||
value={subscriptionBuilder?.endpoint}
|
||||
readOnly
|
||||
className='pr-12'
|
||||
placeholder={t('pluginTrigger.modal.form.callbackUrl.placeholder')}
|
||||
/>
|
||||
<CopyFeedbackNew className='absolute right-1 top-1/2 h-4 w-4 -translate-y-1/2 text-text-tertiary' content={subscriptionBuilder?.endpoint || ''} />
|
||||
</div>
|
||||
<div className='system-xs-regular mt-1 text-text-tertiary'>
|
||||
{t('pluginTrigger.modal.form.callbackUrl.description')}
|
||||
</div>
|
||||
</div>
|
||||
<BaseForm
|
||||
formSchemas={[
|
||||
{
|
||||
name: 'subscription_name',
|
||||
label: t('pluginTrigger.modal.form.subscriptionName.label'),
|
||||
placeholder: t('pluginTrigger.modal.form.subscriptionName.placeholder'),
|
||||
type: FormTypeEnum.textInput,
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'callback_url',
|
||||
label: t('pluginTrigger.modal.form.callbackUrl.label'),
|
||||
placeholder: t('pluginTrigger.modal.form.callbackUrl.placeholder'),
|
||||
type: FormTypeEnum.textInput,
|
||||
required: false,
|
||||
default: subscriptionBuilder?.endpoint || '',
|
||||
disabled: true,
|
||||
tooltip: t('pluginTrigger.modal.form.callbackUrl.tooltip'),
|
||||
// extra: subscriptionBuilder?.endpoint ? (
|
||||
// <CopyFeedbackNew
|
||||
// className='absolute right-1 top-1/2 h-4 w-4 -translate-y-1/2 text-text-tertiary'
|
||||
// content={subscriptionBuilder?.endpoint || ''}
|
||||
// />
|
||||
// ) : undefined,
|
||||
},
|
||||
]}
|
||||
ref={subscriptionFormRef}
|
||||
labelClassName='system-sm-medium mb-2 flex items-center gap-1 text-text-primary'
|
||||
formClassName='space-y-4 mb-4'
|
||||
/>
|
||||
{/* <div className='system-xs-regular mb-6 mt-[-1rem] text-text-tertiary'>
|
||||
{t('pluginTrigger.modal.form.callbackUrl.description')}
|
||||
</div> */}
|
||||
{createType !== SupportedCreationMethods.MANUAL && parametersSchema.length > 0 && (
|
||||
<BaseForm
|
||||
formSchemas={parametersSchema.map(schema => ({
|
||||
|
|
@ -283,6 +279,7 @@ export const CommonCreateModal = ({ onClose, createType, builder }: Props) => {
|
|||
}))}
|
||||
ref={parametersFormRef}
|
||||
labelClassName='system-sm-medium mb-2 block text-text-primary'
|
||||
formClassName='space-y-4'
|
||||
/>
|
||||
)}
|
||||
{createType === SupportedCreationMethods.MANUAL && <>
|
||||
|
|
@ -292,6 +289,7 @@ export const CommonCreateModal = ({ onClose, createType, builder }: Props) => {
|
|||
formSchemas={propertiesSchema}
|
||||
ref={propertiesFormRef}
|
||||
labelClassName='system-sm-medium mb-2 block text-text-primary'
|
||||
formClassName='space-y-4'
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -311,11 +309,9 @@ export const CommonCreateModal = ({ onClose, createType, builder }: Props) => {
|
|||
Awaiting request from {detail?.declaration.name}...
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<LogViewer logs={logData?.logs || []} />
|
||||
</div>
|
||||
</>}
|
||||
|
||||
</div>}
|
||||
</Modal>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -30,19 +30,6 @@ type Props = {
|
|||
|
||||
export const DEFAULT_METHOD = 'default'
|
||||
|
||||
/**
|
||||
* 区分创建订阅的授权方式有几种
|
||||
* 1. 只有一种授权方式
|
||||
* - 按钮直接显示授权方式,点击按钮展示创建订阅弹窗
|
||||
* 2. 有多种授权方式
|
||||
* - 下拉框显示授权方式,点击按钮展示下拉框,点击选项展示创建订阅弹窗
|
||||
* 有订阅与无订阅时,按钮形态不同
|
||||
* oauth 的授权类型:
|
||||
* - 是否配置 client_id 和 client_secret
|
||||
* - 未配置则点击按钮去配置
|
||||
* - 已配置则点击按钮去创建
|
||||
* - 固定展示设置按钮
|
||||
*/
|
||||
export const CreateSubscriptionButton = ({ buttonType = CreateButtonType.FULL_BUTTON }: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const [selectedCreateInfo, setSelectedCreateInfo] = useState<{ type: SupportedCreationMethods, builder?: TriggerSubscriptionBuilder } | null>(null)
|
||||
|
|
|
|||
|
|
@ -68,6 +68,7 @@ const translation = {
|
|||
more: 'More',
|
||||
selectAll: 'Select All',
|
||||
deSelectAll: 'Deselect All',
|
||||
selectCount: '{{count}} Selected',
|
||||
},
|
||||
errorMsg: {
|
||||
fieldRequired: '{{field}} is required',
|
||||
|
|
@ -76,7 +77,9 @@ const translation = {
|
|||
placeholder: {
|
||||
input: 'Please enter',
|
||||
select: 'Please select',
|
||||
search: 'Search...',
|
||||
},
|
||||
noData: 'No data',
|
||||
label: {
|
||||
optional: '(optional)',
|
||||
},
|
||||
|
|
|
|||
|
|
@ -68,6 +68,7 @@ const translation = {
|
|||
selectAll: '全选',
|
||||
deSelectAll: '取消全选',
|
||||
now: '现在',
|
||||
selectCount: '已选择 {{count}} 项',
|
||||
},
|
||||
errorMsg: {
|
||||
fieldRequired: '{{field}} 为必填项',
|
||||
|
|
@ -76,7 +77,9 @@ const translation = {
|
|||
placeholder: {
|
||||
input: '请输入',
|
||||
select: '请选择',
|
||||
search: '搜索...',
|
||||
},
|
||||
noData: '暂无数据',
|
||||
label: {
|
||||
optional: '(可选)',
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in New Issue