From 3edf1e2f59b8bb1abef9d37ca2ea2c2bbd0907f4 Mon Sep 17 00:00:00 2001 From: yessenia Date: Sun, 28 Sep 2025 11:45:12 +0800 Subject: [PATCH] feat: add checkbox list --- .../components/base/checkbox-list/index.tsx | 171 ++++++++++++++++++ web/app/components/base/checkbox/index.tsx | 2 +- .../base/form/components/base/base-field.tsx | 24 ++- .../base/form/hooks/use-get-form-values.ts | 4 +- web/app/components/base/form/types.ts | 1 + .../subscription-list/create/common-modal.tsx | 94 +++++----- .../subscription-list/create/index.tsx | 13 -- web/i18n/en-US/common.ts | 3 + web/i18n/zh-Hans/common.ts | 3 + 9 files changed, 249 insertions(+), 66 deletions(-) create mode 100644 web/app/components/base/checkbox-list/index.tsx diff --git a/web/app/components/base/checkbox-list/index.tsx b/web/app/components/base/checkbox-list/index.tsx new file mode 100644 index 0000000000..efb3d101da --- /dev/null +++ b/web/app/components/base/checkbox-list/index.tsx @@ -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 = ({ + 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 ( +
+ {label && ( +
+ {label} +
+ )} + {description && ( +
+ {description} +
+ )} + +
+ {(showSelectAll || title) && ( +
+ {showSelectAll && ( + + )} +
+ {title && ( + + {title} + + )} + {showCount && selectedCount > 0 && ( + + {t('common.operation.selectCount', { count: selectedCount })} + + )} +
+
+ )} + +
+ {!options.length ? ( +
+ {t('common.noData')} +
+ ) : ( + options.map((option) => { + const selected = value.includes(option.value) + + return ( +
{ + if (!option.disabled && !disabled) + handleToggleOption(option.value) + }} + > + { + if (!option.disabled && !disabled) + handleToggleOption(option.value) + }} + disabled={option.disabled || disabled} + /> +
+ {option.label} +
+
+ ) + }) + )} +
+
+
+ ) +} + +export default CheckboxList diff --git a/web/app/components/base/checkbox/index.tsx b/web/app/components/base/checkbox/index.tsx index 2411d98966..9495292ea6 100644 --- a/web/app/components/base/checkbox/index.tsx +++ b/web/app/components/base/checkbox/index.tsx @@ -30,7 +30,7 @@ const Checkbox = ({
{ 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 = ({ * ) } + {tooltip && ( + {typeof tooltip === 'string' ? tooltip : renderI18nObject(tooltip as Record)}
} + triggerClassName='ml-0.5 w-4 h-4' + /> + )}
{ @@ -170,7 +181,7 @@ const BaseField = ({ ) } { - formItemType === FormTypeEnum.select && ( + formItemType === FormTypeEnum.select && !multiple && ( handleChange(v)} @@ -184,6 +195,17 @@ const BaseField = ({ /> ) } + { + formItemType === FormTypeEnum.select && multiple && ( + field.handleChange(v)} + options={memorizedOptions} + maxHeight='200px' + /> + ) + } { formItemType === FormTypeEnum.dynamicSelect && ( 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, } } diff --git a/web/app/components/base/form/types.ts b/web/app/components/base/form/types.ts index d0e1788580..5eed355449 100644 --- a/web/app/components/base/form/types.ts +++ b/web/app/components/base/form/types.ts @@ -50,6 +50,7 @@ export type FormSchema = { name: string label: string | ReactNode | TypeWithI18N | Record required: boolean + multiple?: boolean default?: any tooltip?: string | TypeWithI18N | Record show_on?: FormShowOnObject[] 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 52bd686a0e..be37effd6a 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 @@ -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(createType === SupportedCreationMethods.APIKEY ? ApiKeyStep.Verify : ApiKeyStep.Configuration) - const [subscriptionName, setSubscriptionName] = useState('') const [subscriptionBuilder, setSubscriptionBuilder] = useState(builder) const [verificationError, setVerificationError] = useState('') @@ -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(null) const propertiesFormRef = React.useRef(null) const parametersSchema = detail?.declaration.trigger?.subscription_schema?.parameters_schema || [] // apikey and oauth const parametersFormRef = React.useRef(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' />
)} @@ -241,34 +232,39 @@ export const CommonCreateModal = ({ onClose, createType, builder }: Props) => { )} {currentStep === ApiKeyStep.Configuration &&
-
- - setSubscriptionName(e.target.value)} - placeholder={t('pluginTrigger.modal.form.subscriptionName.placeholder')} - /> -
- -
- -
- - -
-
- {t('pluginTrigger.modal.form.callbackUrl.description')} -
-
+ + // ) : undefined, + }, + ]} + ref={subscriptionFormRef} + labelClassName='system-sm-medium mb-2 flex items-center gap-1 text-text-primary' + formClassName='space-y-4 mb-4' + /> + {/*
+ {t('pluginTrigger.modal.form.callbackUrl.description')} +
*/} {createType !== SupportedCreationMethods.MANUAL && parametersSchema.length > 0 && ( ({ @@ -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' />
)} @@ -311,11 +309,9 @@ export const CommonCreateModal = ({ onClose, createType, builder }: Props) => { Awaiting request from {detail?.declaration.name}... - } - } ) 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 c9137662b3..88dadb8300 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 @@ -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) diff --git a/web/i18n/en-US/common.ts b/web/i18n/en-US/common.ts index 9c86c55005..f33a58cd87 100644 --- a/web/i18n/en-US/common.ts +++ b/web/i18n/en-US/common.ts @@ -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)', }, diff --git a/web/i18n/zh-Hans/common.ts b/web/i18n/zh-Hans/common.ts index 65e963ef6b..e035871303 100644 --- a/web/i18n/zh-Hans/common.ts +++ b/web/i18n/zh-Hans/common.ts @@ -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: '(可选)', },