feat(flows): 新增流程编辑器基础功能与相关组件
feat(backend): 添加流程模型与服务支持 feat(frontend): 实现流程编辑器UI与交互 feat(assets): 添加流程节点图标资源 feat(plugins): 实现上下文菜单和运行时插件 feat(components): 新增基础节点和侧边栏组件 feat(routes): 添加流程相关路由配置 feat(models): 创建流程和运行日志数据模型 feat(services): 实现流程服务层逻辑 feat(migration): 添加流程相关数据库迁移 feat(config): 更新前端配置支持流程编辑器 feat(utils): 增强axios错误处理和工具函数
This commit is contained in:
73
frontend/src/flows/form-components/form-header/index.tsx
Normal file
73
frontend/src/flows/form-components/form-header/index.tsx
Normal file
@ -0,0 +1,73 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { useContext, useState, useEffect } from 'react';
|
||||
|
||||
import { useClientContext, CommandService } from '@flowgram.ai/free-layout-editor';
|
||||
import { Button } from '@douyinfe/semi-ui';
|
||||
import { IconClose, IconSmallTriangleDown, IconSmallTriangleLeft } from '@douyinfe/semi-icons';
|
||||
|
||||
import { toggleLoopExpanded } from '../../utils';
|
||||
import { FlowCommandId } from '../../shortcuts';
|
||||
import { useIsSidebar, useNodeRenderContext } from '../../hooks';
|
||||
import { SidebarContext } from '../../context';
|
||||
import { NodeMenu } from '../../components/node-menu';
|
||||
import { getIcon } from './utils';
|
||||
import { TitleInput } from './title-input';
|
||||
import { Header, Operators } from './styles';
|
||||
|
||||
export function FormHeader() {
|
||||
const { node, expanded, toggleExpand, readonly } = useNodeRenderContext();
|
||||
const [titleEdit, updateTitleEdit] = useState<boolean>(false);
|
||||
const ctx = useClientContext();
|
||||
const { setNodeId } = useContext(SidebarContext);
|
||||
const isSidebar = useIsSidebar();
|
||||
const handleExpand = (e: React.MouseEvent) => {
|
||||
toggleExpand();
|
||||
e.stopPropagation(); // Disable clicking prevents the sidebar from opening
|
||||
};
|
||||
const handleDelete = () => {
|
||||
ctx.get<CommandService>(CommandService).executeCommand(FlowCommandId.DELETE, [node]);
|
||||
};
|
||||
const handleClose = () => {
|
||||
setNodeId(undefined);
|
||||
};
|
||||
useEffect(() => {
|
||||
// 折叠 loop 子节点
|
||||
if (node.flowNodeType === 'loop') {
|
||||
toggleLoopExpanded(node, expanded);
|
||||
}
|
||||
}, [expanded]);
|
||||
|
||||
return (
|
||||
<Header>
|
||||
{getIcon(node)}
|
||||
<TitleInput readonly={readonly} updateTitleEdit={updateTitleEdit} titleEdit={titleEdit} />
|
||||
{node.renderData.expandable && !isSidebar && (
|
||||
<Button
|
||||
type="primary"
|
||||
icon={expanded ? <IconSmallTriangleDown /> : <IconSmallTriangleLeft />}
|
||||
size="small"
|
||||
theme="borderless"
|
||||
onClick={handleExpand}
|
||||
/>
|
||||
)}
|
||||
{readonly ? undefined : (
|
||||
<Operators>
|
||||
<NodeMenu node={node} deleteNode={handleDelete} updateTitleEdit={updateTitleEdit} />
|
||||
</Operators>
|
||||
)}
|
||||
{isSidebar && (
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<IconClose />}
|
||||
size="small"
|
||||
theme="borderless"
|
||||
onClick={handleClose}
|
||||
/>
|
||||
)}
|
||||
</Header>
|
||||
);
|
||||
}
|
||||
41
frontend/src/flows/form-components/form-header/styles.tsx
Normal file
41
frontend/src/flows/form-components/form-header/styles.tsx
Normal file
@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const Header = styled.div`
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
column-gap: 8px;
|
||||
border-radius: 8px 8px 0 0;
|
||||
cursor: move;
|
||||
|
||||
background: linear-gradient(#f2f2ff 0%, rgb(251, 251, 251) 100%);
|
||||
overflow: hidden;
|
||||
|
||||
padding: 8px;
|
||||
`;
|
||||
|
||||
export const Title = styled.div`
|
||||
font-size: 20px;
|
||||
flex: 1;
|
||||
width: 0;
|
||||
`;
|
||||
|
||||
export const Icon = styled.img`
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
scale: 0.8;
|
||||
border-radius: 4px;
|
||||
`;
|
||||
|
||||
export const Operators = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 4px;
|
||||
`;
|
||||
@ -0,0 +1,50 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { useRef, useEffect } from 'react';
|
||||
|
||||
import { Field, FieldRenderProps } from '@flowgram.ai/free-layout-editor';
|
||||
import { Typography, Input } from '@douyinfe/semi-ui';
|
||||
|
||||
import { Title } from './styles';
|
||||
import { Feedback } from '../feedback';
|
||||
const { Text } = Typography;
|
||||
|
||||
export function TitleInput(props: {
|
||||
readonly: boolean;
|
||||
titleEdit: boolean;
|
||||
updateTitleEdit: (setEdit: boolean) => void;
|
||||
}): JSX.Element {
|
||||
const { readonly, titleEdit, updateTitleEdit } = props;
|
||||
const ref = useRef<any>();
|
||||
const titleEditing = titleEdit && !readonly;
|
||||
useEffect(() => {
|
||||
if (titleEditing) {
|
||||
ref.current?.focus();
|
||||
}
|
||||
}, [titleEditing]);
|
||||
|
||||
return (
|
||||
<Title>
|
||||
<Field name="title">
|
||||
{({ field: { value, onChange }, fieldState }: FieldRenderProps<string>) => (
|
||||
<div style={{ height: 24 }}>
|
||||
{titleEditing ? (
|
||||
<Input
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
ref={ref}
|
||||
onBlur={() => updateTitleEdit(false)}
|
||||
/>
|
||||
) : (
|
||||
<Text ellipsis={{ showTooltip: true }}>{value}</Text>
|
||||
)}
|
||||
<Feedback errors={fieldState?.errors} />
|
||||
</div>
|
||||
)}
|
||||
</Field>
|
||||
</Title>
|
||||
);
|
||||
}
|
||||
15
frontend/src/flows/form-components/form-header/utils.tsx
Normal file
15
frontend/src/flows/form-components/form-header/utils.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { type FlowNodeEntity } from '@flowgram.ai/free-layout-editor';
|
||||
|
||||
import { FlowNodeRegistry } from '../../typings';
|
||||
import { Icon } from './styles';
|
||||
|
||||
export const getIcon = (node: FlowNodeEntity) => {
|
||||
const icon = node.getNodeRegistry<FlowNodeRegistry>().info?.icon;
|
||||
if (!icon) return null;
|
||||
return <Icon src={icon} />;
|
||||
};
|
||||
Reference in New Issue
Block a user