global variable & interactive node dnd (#3764)
This commit is contained in:
@@ -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)}
|
||||
/>
|
||||
<MyIconButton
|
||||
icon={'delete'}
|
||||
hoverColor={'red.500'}
|
||||
onClick={() => onDelete(item.key)}
|
||||
/>
|
||||
</Flex>
|
||||
</Td>
|
||||
</Tr>
|
||||
<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}
|
||||
/>
|
||||
)}
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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,86 +18,72 @@ 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>
|
||||
{options.map((item, i) => (
|
||||
<Box key={item.key} mb={4}>
|
||||
<HStack spacing={1}>
|
||||
<MyTooltip label={t('common:common.Delete')}>
|
||||
<MyIcon
|
||||
mt={0.5}
|
||||
name={'minus'}
|
||||
w={'0.8rem'}
|
||||
cursor={'pointer'}
|
||||
color={'myGray.600'}
|
||||
_hover={{ color: 'red.600' }}
|
||||
onClick={() => {
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
type: 'updateInput',
|
||||
key: optionKey,
|
||||
value: {
|
||||
...props,
|
||||
key: optionKey,
|
||||
value: options.filter((input) => input.key !== item.key)
|
||||
}
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</MyTooltip>
|
||||
<Box color={'myGray.600'} fontWeight={'medium'} fontSize={'sm'}>
|
||||
{t('common:option') + (i + 1)}
|
||||
</Box>
|
||||
</HStack>
|
||||
<Box position={'relative'} mt={1}>
|
||||
<Input
|
||||
defaultValue={item.value}
|
||||
bg={'white'}
|
||||
fontSize={'sm'}
|
||||
onChange={(e) => {
|
||||
const newVal = options.map((val) =>
|
||||
val.key === item.key
|
||||
? {
|
||||
...val,
|
||||
value: e.target.value
|
||||
}
|
||||
: val
|
||||
);
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
type: 'updateInput',
|
||||
key: optionKey,
|
||||
value: {
|
||||
...props,
|
||||
key: optionKey,
|
||||
value: newVal
|
||||
}
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<SourceHandle
|
||||
nodeId={nodeId}
|
||||
handleId={getHandleId(nodeId, 'source', item.key)}
|
||||
position={Position.Right}
|
||||
translate={[34, 0]}
|
||||
/>
|
||||
<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) => (
|
||||
<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>
|
||||
</Box>
|
||||
))}
|
||||
)}
|
||||
</DndDrag>
|
||||
<Button
|
||||
fontSize={'sm'}
|
||||
leftIcon={<MyIcon name={'common/addLight'} w={4} />}
|
||||
@@ -120,7 +106,7 @@ const NodeUserSelect = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
|
||||
);
|
||||
}
|
||||
}),
|
||||
[nodeId, onChangeNode, t]
|
||||
[nodeId, onChangeNode, t, zoom]
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -136,3 +122,100 @@ const NodeUserSelect = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
|
||||
);
|
||||
};
|
||||
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
|
||||
mt={0.5}
|
||||
name={'minus'}
|
||||
w={'0.8rem'}
|
||||
cursor={'pointer'}
|
||||
color={'myGray.600'}
|
||||
_hover={{ color: 'red.600' }}
|
||||
onClick={() => {
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
type: 'updateInput',
|
||||
key: optionKey,
|
||||
value: {
|
||||
...props,
|
||||
key: optionKey,
|
||||
value: options.filter((input) => input.key !== item.key)
|
||||
}
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</MyTooltip>
|
||||
<Box color={'myGray.600'} fontWeight={'medium'} fontSize={'sm'}>
|
||||
{t('common:option') + (index + 1)}
|
||||
</Box>
|
||||
</HStack>
|
||||
<Box position={'relative'} mt={1}>
|
||||
<Input
|
||||
defaultValue={item.value}
|
||||
bg={'white'}
|
||||
fontSize={'sm'}
|
||||
onChange={(e) => {
|
||||
const newVal = options.map((val) =>
|
||||
val.key === item.key
|
||||
? {
|
||||
...val,
|
||||
value: e.target.value
|
||||
}
|
||||
: val
|
||||
);
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
type: 'updateInput',
|
||||
key: optionKey,
|
||||
value: {
|
||||
...props,
|
||||
key: optionKey,
|
||||
value: newVal
|
||||
}
|
||||
});
|
||||
}}
|
||||
/>
|
||||
{!snapshot.isDragging && (
|
||||
<SourceHandle
|
||||
nodeId={nodeId}
|
||||
handleId={getHandleId(nodeId, 'source', item.key)}
|
||||
position={Position.Right}
|
||||
translate={[34, 0]}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user