mirror of
https://github.com/labring/FastGPT.git
synced 2025-10-19 18:14:38 +00:00
feat: workflow input node add selectMulti; MultipleSelect component (#4527)
* feat: workflow input node add selectMulti; MultipleSelect component add disabled state (#4440) * perf: input form support multiple select --------- Co-authored-by: mmagi <magizhang@qq.com>
This commit is contained in:
@@ -7,6 +7,7 @@ export enum FlowNodeInputTypeEnum { // render ui
|
|||||||
numberInput = 'numberInput',
|
numberInput = 'numberInput',
|
||||||
switch = 'switch', // true/false
|
switch = 'switch', // true/false
|
||||||
select = 'select',
|
select = 'select',
|
||||||
|
multipleSelect = 'multipleSelect',
|
||||||
|
|
||||||
// editor
|
// editor
|
||||||
JSONEditor = 'JSONEditor',
|
JSONEditor = 'JSONEditor',
|
||||||
@@ -46,6 +47,9 @@ export const FlowNodeInputMap: Record<
|
|||||||
[FlowNodeInputTypeEnum.select]: {
|
[FlowNodeInputTypeEnum.select]: {
|
||||||
icon: 'core/workflow/inputType/option'
|
icon: 'core/workflow/inputType/option'
|
||||||
},
|
},
|
||||||
|
[FlowNodeInputTypeEnum.multipleSelect]: {
|
||||||
|
icon: 'core/workflow/inputType/option'
|
||||||
|
},
|
||||||
[FlowNodeInputTypeEnum.switch]: {
|
[FlowNodeInputTypeEnum.switch]: {
|
||||||
icon: 'core/workflow/inputType/switch'
|
icon: 'core/workflow/inputType/switch'
|
||||||
},
|
},
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
|
Button,
|
||||||
ButtonProps,
|
ButtonProps,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
Flex,
|
Flex,
|
||||||
@@ -26,13 +27,14 @@ export type SelectProps<T = any> = {
|
|||||||
}[];
|
}[];
|
||||||
value: T[];
|
value: T[];
|
||||||
isSelectAll: boolean;
|
isSelectAll: boolean;
|
||||||
setIsSelectAll: React.Dispatch<React.SetStateAction<boolean>>;
|
setIsSelectAll?: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
maxH?: number;
|
maxH?: number;
|
||||||
itemWrap?: boolean;
|
itemWrap?: boolean;
|
||||||
onSelect: (val: T[]) => void;
|
onSelect: (val: T[]) => void;
|
||||||
closeable?: boolean;
|
closeable?: boolean;
|
||||||
|
isDisabled?: boolean;
|
||||||
ScrollData?: ReturnType<typeof useScrollPagination>['ScrollData'];
|
ScrollData?: ReturnType<typeof useScrollPagination>['ScrollData'];
|
||||||
} & Omit<ButtonProps, 'onSelect'>;
|
} & Omit<ButtonProps, 'onSelect'>;
|
||||||
|
|
||||||
@@ -47,6 +49,7 @@ const MultipleSelect = <T = any,>({
|
|||||||
ScrollData,
|
ScrollData,
|
||||||
isSelectAll,
|
isSelectAll,
|
||||||
setIsSelectAll,
|
setIsSelectAll,
|
||||||
|
isDisabled = false,
|
||||||
...props
|
...props
|
||||||
}: SelectProps<T>) => {
|
}: SelectProps<T>) => {
|
||||||
const ref = useRef<HTMLButtonElement>(null);
|
const ref = useRef<HTMLButtonElement>(null);
|
||||||
@@ -70,7 +73,7 @@ const MultipleSelect = <T = any,>({
|
|||||||
// 全选状态下,value 实际上上空。
|
// 全选状态下,value 实际上上空。
|
||||||
if (isSelectAll) {
|
if (isSelectAll) {
|
||||||
onSelect(list.map((item) => item.value).filter((i) => i !== val));
|
onSelect(list.map((item) => item.value).filter((i) => i !== val));
|
||||||
setIsSelectAll(false);
|
setIsSelectAll?.(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,7 +90,7 @@ const MultipleSelect = <T = any,>({
|
|||||||
const hasSelected = isSelectAll || value.length > 0;
|
const hasSelected = isSelectAll || value.length > 0;
|
||||||
onSelect(hasSelected ? [] : list.map((item) => item.value));
|
onSelect(hasSelected ? [] : list.map((item) => item.value));
|
||||||
|
|
||||||
setIsSelectAll((state) => !state);
|
setIsSelectAll?.((state) => !state);
|
||||||
}, [value, list, setIsSelectAll, onSelect]);
|
}, [value, list, setIsSelectAll, onSelect]);
|
||||||
|
|
||||||
const ListRender = useMemo(() => {
|
const ListRender = useMemo(() => {
|
||||||
@@ -126,11 +129,11 @@ const MultipleSelect = <T = any,>({
|
|||||||
}, [value, list, isSelectAll]);
|
}, [value, list, isSelectAll]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box h={'100%'} w={'100%'}>
|
||||||
<Menu
|
<Menu
|
||||||
autoSelect={false}
|
autoSelect={false}
|
||||||
isOpen={isOpen}
|
isOpen={isOpen && !isDisabled}
|
||||||
onOpen={onOpen}
|
onOpen={isDisabled ? undefined : onOpen}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
strategy={'fixed'}
|
strategy={'fixed'}
|
||||||
matchWidth
|
matchWidth
|
||||||
@@ -138,21 +141,23 @@ const MultipleSelect = <T = any,>({
|
|||||||
>
|
>
|
||||||
<MenuButton
|
<MenuButton
|
||||||
as={Flex}
|
as={Flex}
|
||||||
|
h={'100%'}
|
||||||
alignItems={'center'}
|
alignItems={'center'}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
px={3}
|
px={3}
|
||||||
borderRadius={'md'}
|
borderRadius={'md'}
|
||||||
border={'base'}
|
border={'base'}
|
||||||
userSelect={'none'}
|
userSelect={'none'}
|
||||||
cursor={'pointer'}
|
cursor={isDisabled ? 'not-allowed' : 'pointer'}
|
||||||
_active={{
|
_active={{
|
||||||
transform: 'none'
|
transform: 'none'
|
||||||
}}
|
}}
|
||||||
_hover={{
|
_hover={{
|
||||||
borderColor: 'primary.300'
|
borderColor: isDisabled ? 'myGray.200' : 'primary.300'
|
||||||
}}
|
}}
|
||||||
|
opacity={isDisabled ? 0.6 : 1}
|
||||||
{...props}
|
{...props}
|
||||||
{...(isOpen
|
{...(isOpen && !isDisabled
|
||||||
? {
|
? {
|
||||||
boxShadow: '0px 0px 4px #A8DBFF',
|
boxShadow: '0px 0px 4px #A8DBFF',
|
||||||
borderColor: 'primary.500',
|
borderColor: 'primary.500',
|
||||||
|
@@ -77,6 +77,7 @@
|
|||||||
"ifelse.Input value": "Input Value",
|
"ifelse.Input value": "Input Value",
|
||||||
"ifelse.Select value": "Select Value",
|
"ifelse.Select value": "Select Value",
|
||||||
"input_description": "Field Description",
|
"input_description": "Field Description",
|
||||||
|
"input_type_multiple_select": "Multiple selection boxes",
|
||||||
"input_variable_list": "Type / to invoke variable list",
|
"input_variable_list": "Type / to invoke variable list",
|
||||||
"intro_assigned_reply": "This module can directly reply with a specified content. Commonly used for guidance or prompts. Non-string content will be converted to string for output.",
|
"intro_assigned_reply": "This module can directly reply with a specified content. Commonly used for guidance or prompts. Non-string content will be converted to string for output.",
|
||||||
"intro_custom_feedback": "When this module is triggered, a feedback will be added to the current conversation record. It can be used to automatically record conversation effects, etc.",
|
"intro_custom_feedback": "When this module is triggered, a feedback will be added to the current conversation record. It can be used to automatically record conversation effects, etc.",
|
||||||
@@ -152,6 +153,7 @@
|
|||||||
"response.read files": "Read Files",
|
"response.read files": "Read Files",
|
||||||
"select_an_application": "Select an Application",
|
"select_an_application": "Select an Application",
|
||||||
"select_another_application_to_call": "You can choose another application to call",
|
"select_another_application_to_call": "You can choose another application to call",
|
||||||
|
"select_default_option": "Select the default value",
|
||||||
"special_array_format": "Special array format, returns an empty array when the search result is empty.",
|
"special_array_format": "Special array format, returns an empty array when the search result is empty.",
|
||||||
"start_with": "Starts With",
|
"start_with": "Starts With",
|
||||||
"support_code_language": "Support import list: pandas,numpy",
|
"support_code_language": "Support import list: pandas,numpy",
|
||||||
|
@@ -77,6 +77,7 @@
|
|||||||
"ifelse.Input value": "输入值",
|
"ifelse.Input value": "输入值",
|
||||||
"ifelse.Select value": "选择值",
|
"ifelse.Select value": "选择值",
|
||||||
"input_description": "字段描述",
|
"input_description": "字段描述",
|
||||||
|
"input_type_multiple_select": "多选框",
|
||||||
"input_variable_list": "可输入 / 唤起变量列表",
|
"input_variable_list": "可输入 / 唤起变量列表",
|
||||||
"intro_assigned_reply": "该模块可以直接回复一段指定的内容。常用于引导、提示。非字符串内容传入时,会转成字符串进行输出。",
|
"intro_assigned_reply": "该模块可以直接回复一段指定的内容。常用于引导、提示。非字符串内容传入时,会转成字符串进行输出。",
|
||||||
"intro_custom_feedback": "该模块被触发时,会给当前的对话记录增加一条反馈。可用于自动记录对话效果等。",
|
"intro_custom_feedback": "该模块被触发时,会给当前的对话记录增加一条反馈。可用于自动记录对话效果等。",
|
||||||
@@ -152,6 +153,7 @@
|
|||||||
"response.read files": "解析的文档",
|
"response.read files": "解析的文档",
|
||||||
"select_an_application": "选择一个应用",
|
"select_an_application": "选择一个应用",
|
||||||
"select_another_application_to_call": "可以选择一个其他应用进行调用",
|
"select_another_application_to_call": "可以选择一个其他应用进行调用",
|
||||||
|
"select_default_option": "选择默认值",
|
||||||
"special_array_format": "特殊数组格式,搜索结果为空时,返回空数组。",
|
"special_array_format": "特殊数组格式,搜索结果为空时,返回空数组。",
|
||||||
"start_with": "开始为",
|
"start_with": "开始为",
|
||||||
"support_code_language": "支持import列表:pandas,numpy",
|
"support_code_language": "支持import列表:pandas,numpy",
|
||||||
|
@@ -77,6 +77,7 @@
|
|||||||
"ifelse.Input value": "輸入值",
|
"ifelse.Input value": "輸入值",
|
||||||
"ifelse.Select value": "選擇值",
|
"ifelse.Select value": "選擇值",
|
||||||
"input_description": "欄位描述",
|
"input_description": "欄位描述",
|
||||||
|
"input_type_multiple_select": "多選框",
|
||||||
"input_variable_list": "輸入 / 叫出變數清單",
|
"input_variable_list": "輸入 / 叫出變數清單",
|
||||||
"intro_assigned_reply": "這個模組可以直接回覆指定的內容。常用於引導、提示。非字串內容傳入時,會轉換成字串輸出。",
|
"intro_assigned_reply": "這個模組可以直接回覆指定的內容。常用於引導、提示。非字串內容傳入時,會轉換成字串輸出。",
|
||||||
"intro_custom_feedback": "當這個模組被觸發時,會在目前的對話紀錄中新增一條回饋。可以用於自動記錄對話效果等等。",
|
"intro_custom_feedback": "當這個模組被觸發時,會在目前的對話紀錄中新增一條回饋。可以用於自動記錄對話效果等等。",
|
||||||
@@ -152,6 +153,7 @@
|
|||||||
"response.read files": "解析的檔案",
|
"response.read files": "解析的檔案",
|
||||||
"select_an_application": "選擇一個應用程式",
|
"select_an_application": "選擇一個應用程式",
|
||||||
"select_another_application_to_call": "可以選擇另一個應用程式來呼叫",
|
"select_another_application_to_call": "可以選擇另一個應用程式來呼叫",
|
||||||
|
"select_default_option": "選擇默認值",
|
||||||
"special_array_format": "特殊陣列格式,搜尋結果為空時,回傳空陣列。",
|
"special_array_format": "特殊陣列格式,搜尋結果為空時,回傳空陣列。",
|
||||||
"start_with": "開頭為",
|
"start_with": "開頭為",
|
||||||
"support_code_language": "支援 import 列表:pandas,numpy",
|
"support_code_language": "支援 import 列表:pandas,numpy",
|
||||||
|
@@ -14,6 +14,7 @@ import {
|
|||||||
UserSelectInteractive,
|
UserSelectInteractive,
|
||||||
UserSelectOptionItemType
|
UserSelectOptionItemType
|
||||||
} from '@fastgpt/global/core/workflow/template/system/interactive/type';
|
} from '@fastgpt/global/core/workflow/template/system/interactive/type';
|
||||||
|
import MultipleSelect from '@fastgpt/web/components/common/MySelect/MultipleSelect';
|
||||||
|
|
||||||
const DescriptionBox = React.memo(function DescriptionBox({
|
const DescriptionBox = React.memo(function DescriptionBox({
|
||||||
description
|
description
|
||||||
@@ -173,6 +174,30 @@ export const FormInputComponent = React.memo(function FormInputComponent({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
case FlowNodeInputTypeEnum.multipleSelect:
|
||||||
|
return (
|
||||||
|
<Controller
|
||||||
|
key={label}
|
||||||
|
control={control}
|
||||||
|
name={label}
|
||||||
|
rules={{ required: required }}
|
||||||
|
render={({ field: { ref, value } }) => {
|
||||||
|
if (!list) return <></>;
|
||||||
|
return (
|
||||||
|
<MultipleSelect<string>
|
||||||
|
width={'100%'}
|
||||||
|
bg={'white'}
|
||||||
|
py={2}
|
||||||
|
list={list}
|
||||||
|
value={value}
|
||||||
|
isDisabled={submitted}
|
||||||
|
onSelect={(e) => setValue(label, e)}
|
||||||
|
isSelectAll={value.length === list.length}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@@ -64,6 +64,12 @@ const InputFormEditModal = ({
|
|||||||
label: t('common:core.workflow.inputType.select'),
|
label: t('common:core.workflow.inputType.select'),
|
||||||
value: FlowNodeInputTypeEnum.select,
|
value: FlowNodeInputTypeEnum.select,
|
||||||
defaultValueType: WorkflowIOValueTypeEnum.string
|
defaultValueType: WorkflowIOValueTypeEnum.string
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: 'core/workflow/inputType/option',
|
||||||
|
label: t('workflow:input_type_multiple_select'),
|
||||||
|
value: FlowNodeInputTypeEnum.multipleSelect,
|
||||||
|
defaultValueType: WorkflowIOValueTypeEnum.arrayString
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@@ -144,6 +144,7 @@ const InputTypeConfig = ({
|
|||||||
FlowNodeInputTypeEnum.numberInput,
|
FlowNodeInputTypeEnum.numberInput,
|
||||||
FlowNodeInputTypeEnum.switch,
|
FlowNodeInputTypeEnum.switch,
|
||||||
FlowNodeInputTypeEnum.select,
|
FlowNodeInputTypeEnum.select,
|
||||||
|
FlowNodeInputTypeEnum.multipleSelect,
|
||||||
VariableInputEnum.custom
|
VariableInputEnum.custom
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -157,7 +158,8 @@ const InputTypeConfig = ({
|
|||||||
FlowNodeInputTypeEnum.input,
|
FlowNodeInputTypeEnum.input,
|
||||||
FlowNodeInputTypeEnum.numberInput,
|
FlowNodeInputTypeEnum.numberInput,
|
||||||
FlowNodeInputTypeEnum.switch,
|
FlowNodeInputTypeEnum.switch,
|
||||||
FlowNodeInputTypeEnum.select
|
FlowNodeInputTypeEnum.select,
|
||||||
|
FlowNodeInputTypeEnum.multipleSelect
|
||||||
];
|
];
|
||||||
return type === 'plugin' && list.includes(inputType as FlowNodeInputTypeEnum);
|
return type === 'plugin' && list.includes(inputType as FlowNodeInputTypeEnum);
|
||||||
}, [inputType, type]);
|
}, [inputType, type]);
|
||||||
@@ -363,6 +365,27 @@ const InputTypeConfig = ({
|
|||||||
w={'200px'}
|
w={'200px'}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{inputType === FlowNodeInputTypeEnum.multipleSelect && (
|
||||||
|
<MultipleSelect<string>
|
||||||
|
flex={'1 0 0'}
|
||||||
|
itemWrap={true}
|
||||||
|
bg={'myGray.50'}
|
||||||
|
list={listValue
|
||||||
|
.filter((item: any) => item.label !== '')
|
||||||
|
.map((item: any) => ({
|
||||||
|
label: item.label,
|
||||||
|
value: item.value
|
||||||
|
}))}
|
||||||
|
placeholder={t('workflow:select_default_option')}
|
||||||
|
value={defaultValue || []}
|
||||||
|
onSelect={(val) => setValue('defaultValue', val)}
|
||||||
|
isSelectAll={
|
||||||
|
defaultValue &&
|
||||||
|
defaultValue.length ===
|
||||||
|
listValue.filter((item: any) => item.label !== '').length
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
)}
|
)}
|
||||||
@@ -390,7 +413,8 @@ const InputTypeConfig = ({
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{inputType === FlowNodeInputTypeEnum.select && (
|
{(inputType === FlowNodeInputTypeEnum.select ||
|
||||||
|
inputType == FlowNodeInputTypeEnum.multipleSelect) && (
|
||||||
<>
|
<>
|
||||||
<DndDrag<{ id: string; value: string }>
|
<DndDrag<{ id: string; value: string }>
|
||||||
onDragEndCb={(list) => {
|
onDragEndCb={(list) => {
|
||||||
|
@@ -25,6 +25,9 @@ const RenderList: Record<
|
|||||||
[FlowNodeInputTypeEnum.select]: {
|
[FlowNodeInputTypeEnum.select]: {
|
||||||
Component: dynamic(() => import('./templates/Select'))
|
Component: dynamic(() => import('./templates/Select'))
|
||||||
},
|
},
|
||||||
|
[FlowNodeInputTypeEnum.multipleSelect]: {
|
||||||
|
Component: dynamic(() => import('./templates/SelectMulti'))
|
||||||
|
},
|
||||||
[FlowNodeInputTypeEnum.numberInput]: {
|
[FlowNodeInputTypeEnum.numberInput]: {
|
||||||
Component: dynamic(() => import('./templates/NumberInput'))
|
Component: dynamic(() => import('./templates/NumberInput'))
|
||||||
},
|
},
|
||||||
|
@@ -0,0 +1,43 @@
|
|||||||
|
import MultipleSelect from '@fastgpt/web/components/common/MySelect/MultipleSelect';
|
||||||
|
import { RenderInputProps } from '../type';
|
||||||
|
import { WorkflowContext } from '@/pageComponents/app/detail/WorkflowComponents/context';
|
||||||
|
import { useContextSelector } from 'use-context-selector';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const SelectMultiRender = ({ item, nodeId }: RenderInputProps) => {
|
||||||
|
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
|
||||||
|
return (
|
||||||
|
<MultipleSelect<string>
|
||||||
|
width={'100%'}
|
||||||
|
value={item.value}
|
||||||
|
list={item.list || []}
|
||||||
|
onSelect={(e) =>
|
||||||
|
onChangeNode({
|
||||||
|
nodeId,
|
||||||
|
type: 'updateInput',
|
||||||
|
key: item.key,
|
||||||
|
value: {
|
||||||
|
...item,
|
||||||
|
value: e
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
isSelectAll={item.value.length === item?.list?.length}
|
||||||
|
setIsSelectAll={(all) => {
|
||||||
|
if (all) {
|
||||||
|
onChangeNode({
|
||||||
|
nodeId,
|
||||||
|
type: 'updateInput',
|
||||||
|
key: item.key,
|
||||||
|
value: {
|
||||||
|
...item,
|
||||||
|
value: item?.list?.map((item) => item.value)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(SelectMultiRender);
|
Reference in New Issue
Block a user