feat: introduce trigger functionality (#27644)

Signed-off-by: lyzno1 <yuanyouhuilyz@gmail.com>
Co-authored-by: Stream <Stream_2@qq.com>
Co-authored-by: lyzno1 <92089059+lyzno1@users.noreply.github.com>
Co-authored-by: zhsama <torvalds@linux.do>
Co-authored-by: Harry <xh001x@hotmail.com>
Co-authored-by: lyzno1 <yuanyouhuilyz@gmail.com>
Co-authored-by: yessenia <yessenia.contact@gmail.com>
Co-authored-by: hjlarry <hjlarry@163.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: WTW0313 <twwu@dify.ai>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Yeuoly
2025-11-12 17:59:37 +08:00
committed by GitHub
parent ca7794305b
commit b76e17b25d
785 changed files with 41186 additions and 3725 deletions

View File

@@ -58,6 +58,7 @@ const CustomSelect = <T extends Option>({
onOpenChange,
placement,
offset,
triggerPopupSameWidth = true,
} = containerProps || {}
const {
className: triggerClassName,
@@ -85,6 +86,7 @@ const CustomSelect = <T extends Option>({
offset={offset || 4}
open={mergedOpen}
onOpenChange={handleOpenChange}
triggerPopupSameWidth={triggerPopupSameWidth}
>
<PortalToFollowElemTrigger
onClick={() => handleOpenChange(!mergedOpen)}

View File

@@ -26,6 +26,9 @@ const defaultItems = [
export type Item = {
value: number | string
name: string
isGroup?: boolean
disabled?: boolean
extra?: React.ReactNode
} & Record<string, any>
export type ISelectProps = {
@@ -70,14 +73,13 @@ const Select: FC<ISelectProps> = ({
const [open, setOpen] = useState(false)
const [selectedItem, setSelectedItem] = useState<Item | null>(null)
// Ensure selectedItem is properly set when defaultValue or items change
useEffect(() => {
let defaultSelect = null
const existed = items.find((item: Item) => item.value === defaultValue)
if (existed)
defaultSelect = existed
// Handle cases where defaultValue might be undefined, null, or empty string
defaultSelect = (defaultValue && items.find((item: Item) => item.value === defaultValue)) || null
setSelectedItem(defaultSelect)
}, [defaultValue])
}, [defaultValue, items])
const filteredItems: Item[]
= query === ''
@@ -193,14 +195,18 @@ const SimpleSelect: FC<ISelectProps> = ({
const [selectedItem, setSelectedItem] = useState<Item | null>(null)
// Enhanced: Preserve user selection, only reset when necessary
useEffect(() => {
let defaultSelect = null
const existed = items.find((item: Item) => item.value === defaultValue)
if (existed)
defaultSelect = existed
// Only reset if no current selection or current selection is invalid
const isCurrentSelectionValid = selectedItem && items.some(item => item.value === selectedItem.value)
setSelectedItem(defaultSelect)
}, [defaultValue])
if (!isCurrentSelectionValid) {
let defaultSelect = null
// Handle cases where defaultValue might be undefined, null, or empty string
defaultSelect = items.find((item: Item) => item.value === defaultValue) ?? null
setSelectedItem(defaultSelect)
}
}, [defaultValue, items, selectedItem])
const listboxRef = useRef<HTMLDivElement>(null)
@@ -255,38 +261,47 @@ const SimpleSelect: FC<ISelectProps> = ({
{(!disabled) && (
<ListboxOptions className={classNames('absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur px-1 py-1 text-base shadow-lg backdrop-blur-sm focus:outline-none sm:text-sm', optionWrapClassName)}>
{items.map((item: Item) => (
<ListboxOption
key={item.value}
className={
classNames(
'relative cursor-pointer select-none rounded-lg py-2 pl-3 pr-9 text-text-secondary hover:bg-state-base-hover',
optionClassName,
)
}
value={item}
disabled={disabled}
>
{({ /* active, */ selected }) => (
<>
{renderOption
? renderOption({ item, selected })
: (<>
<span className={classNames('block', selected && 'font-normal')}>{item.name}</span>
{selected && !hideChecked && (
<span
className={classNames(
'absolute inset-y-0 right-0 flex items-center pr-4 text-text-accent',
)}
>
<RiCheckLine className="h-4 w-4" aria-hidden="true" />
</span>
)}
</>)}
</>
)}
</ListboxOption>
))}
{items.map((item: Item) =>
item.isGroup ? (
<div
key={item.value}
className="select-none px-3 py-1.5 text-xs font-medium uppercase tracking-wide text-text-tertiary"
>
{item.name}
</div>
) : (
<ListboxOption
key={item.value}
className={
classNames(
'relative cursor-pointer select-none rounded-lg py-2 pl-3 pr-9 text-text-secondary hover:bg-state-base-hover',
optionClassName,
)
}
value={item}
disabled={item.disabled || disabled}
>
{({ /* active, */ selected }) => (
<>
{renderOption
? renderOption({ item, selected })
: (<>
<span className={classNames('block', selected && 'font-normal')}>{item.name}</span>
{selected && !hideChecked && (
<span
className={classNames(
'absolute inset-y-0 right-0 flex items-center pr-2 text-text-accent',
)}
>
<RiCheckLine className="h-4 w-4" aria-hidden="true" />
</span>
)}
</>)}
</>
)}
</ListboxOption>
),
)}
</ListboxOptions>
)}
</div>
@@ -334,6 +349,7 @@ const PortalSelect: FC<PortalSelectProps> = ({
onOpenChange={setOpen}
placement='bottom-start'
offset={4}
triggerPopupSameWidth={true}
>
<PortalToFollowElemTrigger onClick={() => !readonly && setOpen(v => !v)} className='w-full'>
{renderTrigger
@@ -361,7 +377,7 @@ const PortalSelect: FC<PortalSelectProps> = ({
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className={`z-20 ${popupClassName}`}>
<div
className={classNames('max-h-60 overflow-auto rounded-md border-[0.5px] border-components-panel-border bg-components-panel-bg px-1 py-1 text-base shadow-lg focus:outline-none sm:text-sm', popupInnerClassName)}
className={classNames('max-h-60 overflow-auto rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg px-1 py-1 text-base shadow-lg focus:outline-none sm:text-sm', popupInnerClassName)}
>
{items.map((item: Item) => (
<div
@@ -388,6 +404,7 @@ const PortalSelect: FC<PortalSelectProps> = ({
{!hideChecked && item.value === value && (
<RiCheckLine className='h-4 w-4 shrink-0 text-text-accent' />
)}
{item.extra}
</div>
))}
</div>

View File

@@ -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 (
<PortalToFollowElem
@@ -122,13 +144,16 @@ const PureSelect = ({
)}>
<div
className={cn(
'rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-1 shadow-lg',
'max-h-80 overflow-auto rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-1 shadow-lg',
popupClassName,
)}
>
{
popupTitle && (
<div className='system-xs-medium-uppercase flex h-[22px] items-center px-3 text-text-tertiary'>
<div className={cn(
'system-xs-medium-uppercase flex h-[22px] items-center px-3 text-text-tertiary',
popupTitleClassName,
)}>
{popupTitle}
</div>
)
@@ -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}
</div>
{
value === option.value && <RiCheckLine className='h-4 w-4 shrink-0 text-text-accent' />
(
multiple
? (value ?? []).includes(option.value)
: value === option.value
) && <RiCheckLine className='h-4 w-4 shrink-0 text-text-accent' />
}
</div>
))