Integrate Flowgram visual workflow editor: update data models and add dependencies

Co-authored-by: whyour <22700758+whyour@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot] 2025-11-09 10:27:36 +00:00
parent 3b95ea64d3
commit fffc1e4fd1
6 changed files with 219 additions and 9 deletions

View File

@ -30,13 +30,14 @@ export default (app: Router) => {
body: Joi.object({ body: Joi.object({
name: Joi.string().required(), name: Joi.string().required(),
description: Joi.string().optional().allow(''), description: Joi.string().optional().allow(''),
workflowGraph: Joi.object().optional(),
triggerType: Joi.string() triggerType: Joi.string()
.valid('variable', 'webhook', 'task_status', 'time', 'system_event') .valid('variable', 'webhook', 'task_status', 'time', 'system_event')
.required(), .optional(),
triggerConfig: Joi.object().optional(), triggerConfig: Joi.object().optional(),
conditionLogic: Joi.string().valid('AND', 'OR').default('AND'), conditionLogic: Joi.string().valid('AND', 'OR').default('AND'),
conditions: Joi.array().optional().default([]), conditions: Joi.array().optional().default([]),
actions: Joi.array().required(), actions: Joi.array().optional(),
retryStrategy: Joi.object({ retryStrategy: Joi.object({
maxRetries: Joi.number().min(0).max(10), maxRetries: Joi.number().min(0).max(10),
retryDelay: Joi.number().min(1), retryDelay: Joi.number().min(1),
@ -67,6 +68,7 @@ export default (app: Router) => {
id: Joi.number().required(), id: Joi.number().required(),
name: Joi.string().optional(), name: Joi.string().optional(),
description: Joi.string().optional().allow(''), description: Joi.string().optional().allow(''),
workflowGraph: Joi.object().optional(),
triggerType: Joi.string() triggerType: Joi.string()
.valid('variable', 'webhook', 'task_status', 'time', 'system_event') .valid('variable', 'webhook', 'task_status', 'time', 'system_event')
.optional(), .optional(),

View File

@ -6,11 +6,12 @@ export class Scenario {
name: string; name: string;
description?: string; description?: string;
isEnabled?: 1 | 0; isEnabled?: 1 | 0;
triggerType?: string; // 'variable' | 'webhook' | 'task_status' | 'time' | 'system_event' workflowGraph?: any; // Flowgram workflow graph structure
triggerConfig?: any; // JSON configuration for the trigger triggerType?: string; // Deprecated: kept for backward compatibility
conditionLogic?: 'AND' | 'OR'; triggerConfig?: any; // Deprecated: kept for backward compatibility
conditions?: any[]; // Array of condition objects conditionLogic?: 'AND' | 'OR'; // Deprecated: kept for backward compatibility
actions?: any[]; // Array of actions to execute conditions?: any[]; // Deprecated: kept for backward compatibility
actions?: any[]; // Deprecated: kept for backward compatibility
retryStrategy?: { retryStrategy?: {
maxRetries: number; maxRetries: number;
retryDelay: number; // in seconds retryDelay: number; // in seconds
@ -33,6 +34,7 @@ export class Scenario {
this.name = options.name; this.name = options.name;
this.description = options.description; this.description = options.description;
this.isEnabled = options.isEnabled ?? 1; this.isEnabled = options.isEnabled ?? 1;
this.workflowGraph = options.workflowGraph || null;
this.triggerType = options.triggerType; this.triggerType = options.triggerType;
this.triggerConfig = options.triggerConfig; this.triggerConfig = options.triggerConfig;
this.conditionLogic = options.conditionLogic || 'AND'; this.conditionLogic = options.conditionLogic || 'AND';
@ -64,9 +66,13 @@ export const ScenarioModel = sequelize.define<ScenarioInstance>('Scenario', {
type: DataTypes.NUMBER, type: DataTypes.NUMBER,
defaultValue: 1, defaultValue: 1,
}, },
workflowGraph: {
type: DataTypes.JSON,
allowNull: true,
},
triggerType: { triggerType: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: false, allowNull: true,
}, },
triggerConfig: { triggerConfig: {
type: DataTypes.JSON, type: DataTypes.JSON,

View File

@ -60,6 +60,9 @@ export default async () => {
try { try {
await sequelize.query('alter table Crontabs add column task_after TEXT'); await sequelize.query('alter table Crontabs add column task_after TEXT');
} catch (error) {} } catch (error) {}
try {
await sequelize.query('alter table Scenarios add column workflowGraph JSON');
} catch (error) {}
Logger.info('✌️ DB loaded'); Logger.info('✌️ DB loaded');
} catch (error) { } catch (error) {

View File

@ -100,6 +100,9 @@
"helmet": "^8.1.0" "helmet": "^8.1.0"
}, },
"devDependencies": { "devDependencies": {
"@flowgram.ai/free-layout-editor": "^1.0.2",
"@flowgram.ai/core": "^1.0.2",
"@flowgram.ai/reactive": "^1.0.2",
"moment": "2.30.1", "moment": "2.30.1",
"@ant-design/icons": "^5.0.1", "@ant-design/icons": "^5.0.1",
"@ant-design/pro-layout": "6.38.22", "@ant-design/pro-layout": "6.38.22",

View File

@ -0,0 +1,196 @@
import React, { useState, useEffect, useRef } from 'react';
import { Modal, Form, Input, message } from 'antd';
import { request } from '@/utils/http';
import intl from 'react-intl-universal';
import { FreeLayoutEditor } from '@flowgram.ai/free-layout-editor';
import '@flowgram.ai/free-layout-editor/dist/index.css';
const { TextArea } = Input;
interface ScenarioModalProps {
visible: boolean;
scenario: any;
onCancel: () => void;
onSuccess: () => void;
}
const ScenarioModal: React.FC<ScenarioModalProps> = ({
visible,
scenario,
onCancel,
onSuccess,
}) => {
const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
const editorRef = useRef<any>(null);
const [workflowGraph, setWorkflowGraph] = useState<any>(null);
useEffect(() => {
if (visible) {
if (scenario) {
form.setFieldsValue({
name: scenario.name,
description: scenario.description || '',
});
setWorkflowGraph(scenario.workflowGraph || getInitialWorkflow());
} else {
form.resetFields();
setWorkflowGraph(getInitialWorkflow());
}
}
}, [visible, scenario, form]);
const getInitialWorkflow = () => {
return {
nodes: [
{
id: 'trigger-1',
type: 'trigger',
position: { x: 100, y: 100 },
data: {
label: intl.get('触发器'),
triggerType: 'time',
config: {},
},
},
],
edges: [],
};
};
const handleSubmit = async () => {
try {
const values = await form.validateFields();
setLoading(true);
// Get workflow data from editor
const currentWorkflow = editorRef.current?.getData();
const endpoint = scenario ? '/api/scenarios' : '/api/scenarios';
const method = scenario ? 'put' : 'post';
const payload = {
...values,
workflowGraph: currentWorkflow || workflowGraph,
...(scenario ? { id: scenario.id } : {}),
};
const { code } = await request[method](endpoint, payload);
if (code === 200) {
message.success(
scenario ? intl.get('更新成功') : intl.get('创建成功'),
);
onSuccess();
}
} catch (error) {
console.error('Failed to save scenario:', error);
} finally {
setLoading(false);
}
};
const handleWorkflowChange = (newWorkflow: any) => {
setWorkflowGraph(newWorkflow);
};
return (
<Modal
title={scenario ? intl.get('编辑场景') : intl.get('新建场景')}
open={visible}
onCancel={onCancel}
onOk={handleSubmit}
confirmLoading={loading}
width={1200}
destroyOnClose
style={{ top: 20 }}
>
<Form form={form} layout="vertical">
<Form.Item
name="name"
label={intl.get('名称')}
rules={[{ required: true }]}
>
<Input />
</Form.Item>
<Form.Item name="description" label={intl.get('描述')}>
<TextArea rows={2} />
</Form.Item>
<Form.Item label={intl.get('工作流设计')}>
<div
style={{
border: '1px solid #d9d9d9',
borderRadius: 4,
height: 500,
overflow: 'hidden',
}}
>
<FreeLayoutEditor
ref={editorRef}
data={workflowGraph}
onChange={handleWorkflowChange}
nodeTypes={{
trigger: {
render: (node: any) => (
<div
style={{
padding: 10,
background: '#1890ff',
color: 'white',
borderRadius: 4,
minWidth: 150,
}}
>
<div>{node.data.label}</div>
<div style={{ fontSize: 12, marginTop: 4 }}>
{node.data.triggerType}
</div>
</div>
),
},
condition: {
render: (node: any) => (
<div
style={{
padding: 10,
background: '#52c41a',
color: 'white',
borderRadius: 4,
minWidth: 150,
}}
>
<div>{node.data.label}</div>
<div style={{ fontSize: 12, marginTop: 4 }}>
{node.data.operator}
</div>
</div>
),
},
action: {
render: (node: any) => (
<div
style={{
padding: 10,
background: '#fa8c16',
color: 'white',
borderRadius: 4,
minWidth: 150,
}}
>
<div>{node.data.label}</div>
<div style={{ fontSize: 12, marginTop: 4 }}>
{node.data.actionType}
</div>
</div>
),
},
}}
/>
</div>
</Form.Item>
</Form>
</Modal>
);
};
export default ScenarioModal;

View File

@ -23,7 +23,7 @@ import {
} from '@ant-design/icons'; } from '@ant-design/icons';
import { request } from '@/utils/http'; import { request } from '@/utils/http';
import intl from 'react-intl-universal'; import intl from 'react-intl-universal';
import ScenarioModal from './modal'; import ScenarioModal from './flowgramModal';
import ScenarioLogModal from './logModal'; import ScenarioLogModal from './logModal';
import dayjs from 'dayjs'; import dayjs from 'dayjs';