global variable & interactive node dnd (#3764)

This commit is contained in:
heheer
2025-02-12 12:27:36 +08:00
committed by GitHub
parent 58f715e878
commit c42deab63b
5 changed files with 407 additions and 162 deletions

View File

@@ -1,23 +1,35 @@
import { Box } from '@chakra-ui/react';
import React, { useState } from 'react';
import { Box, Tbody } from '@chakra-ui/react';
import React, { ReactNode, useState } from 'react';
import {
DragDropContext,
DroppableProps,
Droppable,
DraggableChildrenFn,
DragStart,
DropResult
DropResult,
DroppableProvided,
DroppableStateSnapshot
} from 'react-beautiful-dnd';
export * from 'react-beautiful-dnd';
type Props<T = any> = {
onDragEndCb: (result: T[]) => void;
renderClone?: DraggableChildrenFn;
children: DroppableProps['children'];
children:
| ((provided: DroppableProvided, snapshot: DroppableStateSnapshot) => ReactNode)
| ReactNode;
dataList: T[];
isTable?: boolean;
zoom?: number;
};
function DndDrag<T>({ children, renderClone, onDragEndCb, dataList }: Props<T>) {
function DndDrag<T>({
children,
renderClone,
onDragEndCb,
dataList,
isTable = false,
zoom = 1
}: Props<T>) {
const [draggingItemHeight, setDraggingItemHeight] = useState(0);
const onDragStart = (start: DragStart) => {
@@ -45,10 +57,15 @@ function DndDrag<T>({ children, renderClone, onDragEndCb, dataList }: Props<T>)
<DragDropContext onDragStart={onDragStart} onDragEnd={onDragEnd}>
<Droppable droppableId="droppable" renderClone={renderClone}>
{(provided, snapshot) => {
return (
return isTable ? (
<Tbody {...provided.droppableProps} ref={provided.innerRef}>
{typeof children !== 'function' && children}
{snapshot.isDraggingOver && <Box height={`${draggingItemHeight / zoom}px`} />}
</Tbody>
) : (
<Box {...provided.droppableProps} ref={provided.innerRef}>
{children(provided, snapshot)}
{snapshot.isDraggingOver && <Box height={`${draggingItemHeight}px`} />}
{typeof children === 'function' && children(provided, snapshot)}
{snapshot.isDraggingOver && <Box height={`${draggingItemHeight / zoom}px`} />}
</Box>
);
}}

View File

@@ -1,11 +1,10 @@
import React, { useCallback, useMemo, useState } from 'react';
import React, { useCallback, useMemo } from 'react';
import {
Box,
Button,
Flex,
Table,
Thead,
Tbody,
Tr,
Th,
Td,
@@ -20,9 +19,8 @@ import {
} from '@fastgpt/global/core/workflow/constants';
import type { VariableItemType } from '@fastgpt/global/core/app/type.d';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useForm } from 'react-hook-form';
import { useForm, UseFormReset } from 'react-hook-form';
import { customAlphabet } from 'nanoid';
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 6);
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useTranslation } from 'next-i18next';
import { useToast } from '@fastgpt/web/hooks/useToast';
@@ -32,6 +30,14 @@ import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import InputTypeConfig from '@/pageComponents/app/detail/WorkflowComponents/Flow/nodes/NodePluginIO/InputTypeConfig';
import MyIconButton from '@fastgpt/web/components/common/Icon/button';
import { useReactFlow, useViewport } from 'reactflow';
import DndDrag, {
Draggable,
DraggableProvided,
DraggableStateSnapshot
} from '@fastgpt/web/components/common/DndDrag';
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 6);
export const defaultVariable: VariableItemType = {
id: nanoid(),
@@ -61,6 +67,7 @@ const VariableEdit = ({
}) => {
const { t } = useTranslation();
const { toast } = useToast();
const { zoom } = useViewport();
const form = useForm<VariableItemType>();
const { setValue, reset, watch, getValues } = form;
@@ -166,7 +173,7 @@ const VariableEdit = ({
);
return (
<Box>
<Box className="nodrag">
{/* Row box */}
<Flex alignItems={'center'}>
<MyIcon name={'core/app/simpleMode/variable'} w={'20px'} />
@@ -200,48 +207,40 @@ const VariableEdit = ({
<Th>{t('common:common.Operation')}</Th>
</Tr>
</Thead>
<Tbody>
{formatVariables.map((item, index) => (
<Tr key={item.id}>
<Td fontWeight={'medium'}>
<Flex alignItems={'center'}>
<MyIcon name={item.icon as any} w={'16px'} color={'myGray.400'} mr={2} />
{item.key}
</Flex>
</Td>
<Td>
<Flex alignItems={'center'}>
{item.required ? (
<MyIcon name={'check'} w={'16px'} color={'myGray.900'} mr={2} />
) : (
''
)}
</Flex>
</Td>
<Td>
<Flex>
<MyIconButton
icon={'common/settingLight'}
onClick={() => {
const formattedItem = {
...item,
list: item.enums || []
};
reset(formattedItem);
<DndDrag<VariableItemType>
onDragEndCb={(list) => {
onChange(list);
}}
dataList={formatVariables}
renderClone={(provided, snapshot, rubric) => (
<TableItem
provided={provided}
snapshot={snapshot}
item={formatVariables[rubric.source.index]}
reset={reset}
onChange={onChange}
variables={variables}
/>
<MyIconButton
icon={'delete'}
hoverColor={'red.500'}
onClick={() =>
onChange(variables.filter((variable) => variable.id !== item.id))
}
)}
isTable
zoom={zoom}
>
{formatVariables.map((item, index) => (
<Draggable key={item.id} draggableId={item.id} index={index}>
{(provided, snapshot) => (
<TableItem
provided={provided}
snapshot={snapshot}
item={item}
reset={reset}
onChange={onChange}
variables={variables}
key={item.id}
/>
</Flex>
</Td>
</Tr>
)}
</Draggable>
))}
</Tbody>
</DndDrag>
</Table>
</TableContainer>
)}
@@ -339,4 +338,65 @@ const VariableEdit = ({
);
};
const TableItem = ({
provided,
snapshot,
item,
reset,
onChange,
variables
}: {
provided: DraggableProvided;
snapshot: DraggableStateSnapshot;
item: VariableItemType & {
icon?: string;
};
reset: UseFormReset<VariableItemType>;
onChange: (data: VariableItemType[]) => void;
variables: VariableItemType[];
}) => {
return (
<Tr
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
style={{
...provided.draggableProps.style,
opacity: snapshot.isDragging ? 0.8 : 1
}}
>
<Td fontWeight={'medium'}>
<Flex alignItems={'center'}>
<MyIcon name={item.icon as any} w={'16px'} color={'myGray.400'} mr={1} />
{item.key}
</Flex>
</Td>
<Td>
<Flex alignItems={'center'}>
{item.required ? <MyIcon name={'check'} w={'16px'} color={'myGray.900'} mr={2} /> : ''}
</Flex>
</Td>
<Td>
<Flex>
<MyIconButton
icon={'common/settingLight'}
onClick={() => {
const formattedItem = {
...item,
list: item.enums || []
};
reset(formattedItem);
}}
/>
<MyIconButton
icon={'delete'}
hoverColor={'red.500'}
onClick={() => onChange(variables.filter((variable) => variable.id !== item.id))}
/>
</Flex>
</Td>
</Tr>
);
};
export default React.memo(VariableEdit);

View File

@@ -1,6 +1,6 @@
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node';
import React, { useMemo, useState } from 'react';
import { NodeProps } from 'reactflow';
import { NodeProps, useViewport } from 'reactflow';
import NodeCard from '../render/NodeCard';
import Container from '../../components/Container';
import RenderInput from '../render/RenderInput';
@@ -38,11 +38,17 @@ import InputFormEditModal, { defaultFormInput } from './InputFormEditModal';
import RenderOutput from '../render/RenderOutput';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
import MyIconButton from '@fastgpt/web/components/common/Icon/button';
import DndDrag, {
Draggable,
DraggableProvided,
DraggableStateSnapshot
} from '@fastgpt/web/components/common/DndDrag';
const NodeFormInput = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
const { nodeId, inputs, outputs } = data;
const { t } = useTranslation();
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const { zoom } = useViewport();
const [editField, setEditField] = useState<UserInputFormItemType>();
@@ -159,53 +165,79 @@ const NodeFormInput = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
<Th>{t('user:operations')}</Th>
</Tr>
</Thead>
<Tbody>
<DndDrag<UserInputFormItemType>
onDragEndCb={(list) => {
const sortedOutputs = [
outputs[0],
...outputs.slice(1).sort((a, b) => {
const aIndex = list.findIndex((item) => item.key === a.key);
const bIndex = list.findIndex((item) => item.key === b.key);
return aIndex - bIndex;
})
];
onChangeNode({
nodeId,
type: 'updateInput',
key,
value: {
...props,
key,
value: list
}
});
onChangeNode({
nodeId,
type: 'attr',
key: 'outputs',
value: sortedOutputs
});
}}
dataList={inputs}
renderClone={(provided, snapshot, rubric) => {
const item = inputs[rubric.source.index];
const icon = FlowNodeInputMap[item.type as FlowNodeInputTypeEnum]?.icon;
return (
<TableItem
provided={provided}
snapshot={snapshot}
item={item}
icon={icon}
setEditField={setEditField}
onDelete={onDelete}
/>
);
}}
isTable
zoom={zoom}
>
{inputs.map((item, index) => {
const icon = FlowNodeInputMap[item.type as FlowNodeInputTypeEnum]?.icon;
return (
<Tr key={index}>
<Td>
<Flex alignItems={'center'} fontSize={'mini'} fontWeight={'medium'}>
{!!icon && (
<MyIcon name={icon as any} w={'14px'} mr={1} color={'myGray.400'} />
)}
{item.label}
</Flex>
</Td>
<Td>{item.description || '-'}</Td>
<Td>
{item.required ? (
<Flex alignItems={'center'}>
<MyIcon name={'check'} w={'16px'} color={'myGray.900'} mr={2} />
</Flex>
) : (
'-'
)}
</Td>
<Td>
<Flex>
<MyIconButton
icon={'common/settingLight'}
onClick={() => setEditField(item)}
<Draggable key={item.key} draggableId={item.key} index={index}>
{(provided, snapshot) => (
<TableItem
provided={provided}
snapshot={snapshot}
key={item.key}
item={item}
icon={icon}
setEditField={setEditField}
onDelete={onDelete}
/>
<MyIconButton
icon={'delete'}
hoverColor={'red.500'}
onClick={() => onDelete(item.key)}
/>
</Flex>
</Td>
</Tr>
)}
</Draggable>
);
})}
</Tbody>
</DndDrag>
</Table>
</TableContainer>
</Box>
);
}
}),
[t, editField, onChangeNode, nodeId, outputs]
[t, editField, zoom, onChangeNode, nodeId, outputs]
);
return (
@@ -223,3 +255,54 @@ const NodeFormInput = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
};
export default React.memo(NodeFormInput);
const TableItem = ({
provided,
snapshot,
item,
icon,
setEditField,
onDelete
}: {
provided: DraggableProvided;
snapshot: DraggableStateSnapshot;
item: UserInputFormItemType;
icon: string;
setEditField: (item: UserInputFormItemType) => void;
onDelete: (valueKey: string) => void;
}) => {
return (
<Tr
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
style={{
...provided.draggableProps.style,
opacity: snapshot.isDragging ? 0.8 : 1
}}
>
<Td>
<Flex alignItems={'center'} fontSize={'mini'} fontWeight={'medium'} whiteSpace={'nowrap'}>
{!!icon && <MyIcon name={icon as any} w={'14px'} mr={1} color={'myGray.400'} />}
{item.label}
</Flex>
</Td>
<Td>{item.description || '-'}</Td>
<Td>
{item.required ? (
<Flex alignItems={'center'}>
<MyIcon name={'check'} w={'16px'} color={'myGray.900'} mr={2} />
</Flex>
) : (
'-'
)}
</Td>
<Td>
<Flex>
<MyIconButton icon={'common/settingLight'} onClick={() => setEditField(item)} />
<MyIconButton icon={'delete'} hoverColor={'red.500'} onClick={() => onDelete(item.key)} />
</Flex>
</Td>
</Tr>
);
};

View File

@@ -3,7 +3,7 @@ import NodeCard from '../render/NodeCard';
import { useTranslation } from 'next-i18next';
import { Box, Button, Flex } from '@chakra-ui/react';
import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import { NodeProps, Position } from 'reactflow';
import { NodeProps, Position, useViewport } from 'reactflow';
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node';
import { IfElseListItemType } from '@fastgpt/global/core/workflow/template/system/ifElse/type';
import { useContextSelector } from 'use-context-selector';
@@ -18,6 +18,7 @@ import { IfElseResultEnum } from '@fastgpt/global/core/workflow/template/system/
const NodeIfElse = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
const { t } = useTranslation();
const { nodeId, inputs = [] } = data;
const { zoom } = useViewport();
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const elseHandleId = getHandleId(nodeId, 'source', IfElseResultEnum.ELSE);
@@ -63,6 +64,7 @@ const NodeIfElse = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
nodeId={nodeId}
/>
)}
zoom={zoom}
>
{(provided) => (
<Box {...provided.droppableProps} ref={provided.innerRef}>

View File

@@ -1,5 +1,5 @@
import React, { useMemo } from 'react';
import { NodeProps, Position } from 'reactflow';
import { NodeProps, Position, useViewport } from 'reactflow';
import { Box, Button, HStack, Input } from '@chakra-ui/react';
import NodeCard from './render/NodeCard';
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node.d';
@@ -18,24 +18,142 @@ import { WorkflowContext } from '../../context';
import { UserSelectOptionItemType } from '@fastgpt/global/core/workflow/template/system/interactive/type';
import IOTitle from '../components/IOTitle';
import RenderOutput from './render/RenderOutput';
import DndDrag, {
Draggable,
DraggableProvided,
DraggableStateSnapshot
} from '@fastgpt/web/components/common/DndDrag';
const NodeUserSelect = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
const { t } = useTranslation();
const { nodeId, inputs, outputs } = data;
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const { zoom } = useViewport();
const CustomComponent = useMemo(
() => ({
[NodeInputKeyEnum.userSelectOptions]: ({
key: optionKey,
value = [],
...props
}: FlowNodeInputItemType) => {
[NodeInputKeyEnum.userSelectOptions]: (v: FlowNodeInputItemType) => {
const { key: optionKey, value, ...props } = v;
const options = value as UserSelectOptionItemType[];
return (
<Box>
<DndDrag<UserSelectOptionItemType>
onDragEndCb={(list) => {
onChangeNode({
nodeId,
type: 'updateInput',
key: optionKey,
value: {
...props,
key: optionKey,
value: list
}
});
}}
dataList={options}
renderClone={(provided, snapshot, rubric) => (
<OptionItem
provided={provided}
snapshot={snapshot}
item={options[rubric.source.index]}
nodeId={nodeId}
itemValue={v}
index={rubric.source.index}
/>
)}
zoom={zoom}
>
{(provided) => (
<Box ref={provided.innerRef} {...provided.droppableProps}>
{options.map((item, i) => (
<Box key={item.key} mb={4}>
<Draggable key={item.key} index={i} draggableId={item.key}>
{(provided, snapshot) => (
<OptionItem
provided={provided}
snapshot={snapshot}
item={item}
nodeId={nodeId}
itemValue={v}
index={i}
key={item.key}
/>
)}
</Draggable>
))}
</Box>
)}
</DndDrag>
<Button
fontSize={'sm'}
leftIcon={<MyIcon name={'common/addLight'} w={4} />}
onClick={() => {
onChangeNode({
nodeId,
type: 'updateInput',
key: optionKey,
value: {
...props,
key: optionKey,
value: options.concat({ value: '', key: getNanoid() })
}
});
}}
>
{t('common:core.module.Add_option')}
</Button>
</Box>
);
}
}),
[nodeId, onChangeNode, t, zoom]
);
return (
<NodeCard minW={'400px'} selected={selected} {...data}>
<Container>
<RenderInput nodeId={nodeId} flowInputList={inputs} CustomComponent={CustomComponent} />
</Container>
<Container>
<IOTitle text={t('common:common.Output')} />
<RenderOutput nodeId={nodeId} flowOutputList={outputs} />
</Container>
</NodeCard>
);
};
export default React.memo(NodeUserSelect);
const OptionItem = ({
provided,
snapshot,
item,
nodeId,
itemValue,
index
}: {
provided: DraggableProvided;
snapshot: DraggableStateSnapshot;
item: UserSelectOptionItemType;
nodeId: string;
itemValue: FlowNodeInputItemType;
index: number;
}) => {
const { t } = useTranslation();
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const { key: optionKey, value, ...props } = itemValue;
const options = value as UserSelectOptionItemType[];
return (
<Box
mb={4}
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
style={{
...provided.draggableProps.style,
opacity: snapshot.isDragging ? 0.8 : 1
}}
>
<HStack spacing={1}>
<MyTooltip label={t('common:common.Delete')}>
<MyIcon
@@ -60,7 +178,7 @@ const NodeUserSelect = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
/>
</MyTooltip>
<Box color={'myGray.600'} fontWeight={'medium'} fontSize={'sm'}>
{t('common:option') + (i + 1)}
{t('common:option') + (index + 1)}
</Box>
</HStack>
<Box position={'relative'} mt={1}>
@@ -89,50 +207,15 @@ const NodeUserSelect = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
});
}}
/>
{!snapshot.isDragging && (
<SourceHandle
nodeId={nodeId}
handleId={getHandleId(nodeId, 'source', item.key)}
position={Position.Right}
translate={[34, 0]}
/>
)}
</Box>
</Box>
))}
<Button
fontSize={'sm'}
leftIcon={<MyIcon name={'common/addLight'} w={4} />}
onClick={() => {
onChangeNode({
nodeId,
type: 'updateInput',
key: optionKey,
value: {
...props,
key: optionKey,
value: options.concat({ value: '', key: getNanoid() })
}
});
}}
>
{t('common:core.module.Add_option')}
</Button>
</Box>
);
}
}),
[nodeId, onChangeNode, t]
);
return (
<NodeCard minW={'400px'} selected={selected} {...data}>
<Container>
<RenderInput nodeId={nodeId} flowInputList={inputs} CustomComponent={CustomComponent} />
</Container>
<Container>
<IOTitle text={t('common:common.Output')} />
<RenderOutput nodeId={nodeId} flowOutputList={outputs} />
</Container>
</NodeCard>
);
};
export default React.memo(NodeUserSelect);