From bf7b18d442c572d1b3f4a853238ef572e47135ec Mon Sep 17 00:00:00 2001 From: yessenia Date: Tue, 28 Oct 2025 14:42:55 +0800 Subject: [PATCH] feat(trigger): dynamic options opt --- .../base/form/components/base/base-field.tsx | 22 +++--- web/app/components/base/select/pure.tsx | 75 ++++++++++++++----- web/i18n/en-US/common.ts | 6 ++ web/i18n/zh-Hans/common.ts | 6 ++ web/service/use-triggers.ts | 2 + 5 files changed, 82 insertions(+), 29 deletions(-) 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 be74d701ba..db57059b82 100644 --- a/web/app/components/base/form/components/base/base-field.tsx +++ b/web/app/components/base/form/components/base/base-field.tsx @@ -4,7 +4,6 @@ import { FormItemValidateStatusEnum, FormTypeEnum } from '@/app/components/base/ 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 Tooltip from '@/app/components/base/tooltip' import { useRenderI18nObject } from '@/hooks/use-i18n' @@ -161,7 +160,7 @@ const BaseField = ({ const value = useStore(field.form.store, s => s.values[field.name]) - const { data: dynamicOptionsData, isLoading: isDynamicOptionsLoading } = useTriggerPluginDynamicOptions( + const { data: dynamicOptionsData, isLoading: isDynamicOptionsLoading, error: dynamicOptionsError } = useTriggerPluginDynamicOptions( dynamicSelectParams || { plugin_id: '', provider: '', @@ -176,7 +175,7 @@ const BaseField = ({ if (!dynamicOptionsData?.options) return [] return dynamicOptionsData.options.map(option => ({ - name: getTranslatedContent({ content: option.label, render: renderI18nObject }), + label: getTranslatedContent({ content: option.label, render: renderI18nObject }), value: option.value, })) }, [dynamicOptionsData, renderI18nObject]) @@ -250,17 +249,20 @@ const BaseField = ({ } { formItemType === FormTypeEnum.dynamicSelect && ( - field.handleChange(item.value)} - readonly={disabled || isDynamicOptionsLoading} + onChange={field.handleChange} + disabled={disabled || isDynamicOptionsLoading} placeholder={ isDynamicOptionsLoading - ? 'Loading options...' - : translatedPlaceholder || 'Select an option' + ? t('common.dynamicSelect.loading') + : translatedPlaceholder } - items={dynamicOptions} - popupClassName="z-[9999]" + {...(dynamicOptionsError ? { popupProps: { title: t('common.dynamicSelect.error'), titleClassName: 'text-text-destructive-secondary' } } + : (!dynamicOptions.length ? { popupProps: { title: t('common.dynamicSelect.noData') } } : {}))} + triggerPopupSameWidth + multiple={multiple} /> ) } diff --git a/web/app/components/base/select/pure.tsx b/web/app/components/base/select/pure.tsx index cede31d2ba..3de8245025 100644 --- a/web/app/components/base/select/pure.tsx +++ b/web/app/components/base/select/pure.tsx @@ -1,5 +1,6 @@ import { useCallback, + useMemo, useState, } from 'react' import { useTranslation } from 'react-i18next' @@ -22,10 +23,8 @@ export type Option = { value: string } -export type PureSelectProps = { +type SharedPureSelectProps = { options: Option[] - value?: string - onChange?: (value: string) => void containerProps?: PortalToFollowElemOptions & { open?: boolean onOpenChange?: (open: boolean) => void @@ -38,22 +37,39 @@ export type PureSelectProps = { className?: string itemClassName?: string title?: string + titleClassName?: string }, placeholder?: string disabled?: boolean triggerPopupSameWidth?: boolean } -const PureSelect = ({ - options, - value, - onChange, - containerProps, - triggerProps, - popupProps, - placeholder, - disabled, - triggerPopupSameWidth, -}: PureSelectProps) => { + +type SingleSelectProps = { + multiple?: false + value?: string + onChange?: (value: string) => void +} + +type MultiSelectProps = { + multiple: true + value?: string[] + onChange?: (value: string[]) => void +} + +export type PureSelectProps = SharedPureSelectProps & (SingleSelectProps | MultiSelectProps) +const PureSelect = (props: PureSelectProps) => { + const { + options, + containerProps, + triggerProps, + popupProps, + placeholder, + disabled, + triggerPopupSameWidth, + multiple, + value, + onChange, + } = props const { t } = useTranslation() const { open, @@ -69,6 +85,7 @@ const PureSelect = ({ className: popupClassName, itemClassName: popupItemClassName, title: popupTitle, + titleClassName: popupTitleClassName, } = popupProps || {} const [localOpen, setLocalOpen] = useState(false) @@ -79,8 +96,13 @@ const PureSelect = ({ setLocalOpen(openValue) }, [onOpenChange]) - const selectedOption = options.find(option => option.value === value) - const triggerText = selectedOption?.label || placeholder || t('common.placeholder.select') + const triggerText = useMemo(() => { + const placeholderText = placeholder || t('common.placeholder.select') + if (multiple) + return value?.length ? t('common.dynamicSelect.selected', { count: value.length }) : placeholderText + + return options.find(option => option.value === value)?.label || placeholderText + }, [multiple, value, options, placeholder]) return (
{ popupTitle && ( -
+
{popupTitle}
) @@ -144,6 +169,14 @@ const PureSelect = ({ title={option.label} onClick={() => { if (disabled) return + if (multiple) { + const currentValues = value ?? [] + const nextValues = currentValues.includes(option.value) + ? currentValues.filter(valueItem => valueItem !== option.value) + : [...currentValues, option.value] + onChange?.(nextValues) + return + } onChange?.(option.value) handleOpenChange(false) }} @@ -152,7 +185,11 @@ const PureSelect = ({ {option.label}
{ - value === option.value && + ( + multiple + ? (value ?? []).includes(option.value) + : value === option.value + ) && }
)) diff --git a/web/i18n/en-US/common.ts b/web/i18n/en-US/common.ts index d650779af5..08539d6ecf 100644 --- a/web/i18n/en-US/common.ts +++ b/web/i18n/en-US/common.ts @@ -776,6 +776,12 @@ const translation = { supportedFormats: 'Supports PNG, JPG, JPEG, WEBP and GIF', }, you: 'You', + dynamicSelect: { + error: 'Loading options failed', + noData: 'No options available', + loading: 'Loading options...', + selected: '{{count}} selected', + }, } export default translation diff --git a/web/i18n/zh-Hans/common.ts b/web/i18n/zh-Hans/common.ts index 9f9f4bfbdd..1f807b7f49 100644 --- a/web/i18n/zh-Hans/common.ts +++ b/web/i18n/zh-Hans/common.ts @@ -776,6 +776,12 @@ const translation = { title: '提供反馈', placeholder: '请描述发生了什么问题或我们可以如何改进...', }, + dynamicSelect: { + error: '加载选项失败', + noData: '没有可用的选项', + loading: '加载选项...', + selected: '已选择 {{count}} 项', + }, } export default translation diff --git a/web/service/use-triggers.ts b/web/service/use-triggers.ts index afd478e196..43defceae3 100644 --- a/web/service/use-triggers.ts +++ b/web/service/use-triggers.ts @@ -299,8 +299,10 @@ export const useTriggerPluginDynamicOptions = (payload: { provider_type: 'trigger', // Add required provider_type parameter }, }, + { silent: true }, ), enabled: enabled && !!payload.plugin_id && !!payload.provider && !!payload.action && !!payload.parameter && !!payload.credential_id, + retry: 0, }) }