Files
MemoryBear/web/src/views/Workflow/components/Properties/VariableSelect.tsx
2026-03-25 17:13:54 +08:00

157 lines
4.5 KiB
TypeScript

/*
* @Author: ZhaoYing
* @Date: 2026-02-03 15:40:13
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-25 16:54:44
*/
import { type FC } from 'react'
import clsx from 'clsx';
import { Select, type SelectProps, Flex, Space } from 'antd'
import type { Suggestion } from '../Editor/plugin/AutocompletePlugin'
type LabelRender = SelectProps['labelRender'];
/**
* Props for VariableSelect component
*/
interface VariableSelectProps extends SelectProps {
/** Available variable options */
options: Suggestion[];
/** Current selected value */
value?: string;
/** Whether to show clear button */
allowClear?: boolean;
/** Filter out boolean type variables */
filterBooleanType?: boolean;
/** Size of the select component */
size?: 'small' | 'middle' | 'large'
}
/**
* VariableSelect component
* Custom select component for workflow variables with grouped options and custom rendering
* @param props - Component props
*/
const VariableSelect: FC<VariableSelectProps> = ({
placeholder,
options,
value,
allowClear = true,
onChange,
size = 'middle',
filterBooleanType = false,
mode,
...resetPorps
}) => {
/**
* Handle value change and pass selected option to parent
* @param value - Selected value
*/
const handleChange: SelectProps['onChange'] = (value: string) => {
const filterItem = options.find(option => `{{${option.value}}}` === value)
onChange?.(value, filterItem);
}
/**
* Custom label renderer for selected value
* Displays node icon, name and variable label
* @param props - Label render props
*/
const labelRender: LabelRender = (props) => {
const { value } = props
const filterOption = filteredOptions.find(vo => `{{${vo.value}}}` === value)
if (filterOption) {
return (
<span
className={clsx("rb:max-w-full rb:wrap-break-word rb:line-clamp-1 rb:rounded-md rb:bg-white rb:text-[12px] rb:inline-flex rb:items-center rb:px-1.5 rb:cursor-pointer", {
'rb:leading-5.5!': size !== 'small',
'rb:leading-4! rb:text-[10px]!': size === 'small',
'rb-border': mode !== "multiple"
})}
contentEditable={false}
>
{filterOption.nodeData?.icon && filterOption.nodeData?.name && (
<>
<img
src={filterOption.nodeData.icon}
style={{ width: '12px', height: '12px', marginRight: '4px' }}
alt=""
/>
{filterOption.nodeData.name}
<span className="rb:text-[#DFE4ED] rb:mx-0.5">/</span>
</>
)}
<span className="rb:text-[#171719]">{filterOption.label}</span>
</span>
)
}
return null
}
// Filter options based on boolean type if needed
const filteredOptions = filterBooleanType
? options.filter(option => option.dataType !== 'boolean')
: options;
/**
* Group suggestions by node ID
*/
const groupedSuggestions = filteredOptions.reduce((groups: Record<string, any[]>, suggestion) => {
const { nodeData } = suggestion
const nodeId = nodeData.id as string;
if (!groups[nodeId]) {
groups[nodeId] = [];
}
groups[nodeId].push(suggestion);
return groups;
}, {});
/**
* Format grouped options for Select component
*/
const groupedOptions = Object.entries(groupedSuggestions).map(([_nodeId, suggestions]) => ({
label: <Flex align="center" gap={4}>
{suggestions[0].nodeData.icon && <img
src={suggestions[0].nodeData.icon}
className="rb:size-3"
alt=""
/>}
{suggestions[0].nodeData.name}
</Flex>,
options: suggestions.map(s => ({
label: <Flex align="center" justify="space-between" gap={4}>
<Space size={8}>
<span className="rb:text-[#155EEF]">{`{x}`}</span>
{s.label}
</Space>
<span className="rb:text-[#5B6167]">{s.dataType}</span>
</Flex>,
value: `{{${s.value}}}`
}))
}));
return (
<Select
{...resetPorps}
mode={mode}
size={size}
placeholder={placeholder}
value={value}
style={{ width: '100%' }}
options={groupedOptions}
labelRender={labelRender}
onChange={handleChange}
showSearch
allowClear={allowClear}
optionFilterProp="value"
filterOption={(input, option) => {
if (input === '/') return true;
const value = 'value' in option! ? option.value as string : '';
return value.toLowerCase().includes(input.toLowerCase());
}}
/>
)
}
export default VariableSelect