diff --git a/back/api/scenario.ts b/back/api/scenario.ts index d14c2e69..5c30f34f 100644 --- a/back/api/scenario.ts +++ b/back/api/scenario.ts @@ -30,13 +30,14 @@ export default (app: Router) => { body: Joi.object({ name: Joi.string().required(), description: Joi.string().optional().allow(''), + workflowGraph: Joi.object().optional(), triggerType: Joi.string() .valid('variable', 'webhook', 'task_status', 'time', 'system_event') - .required(), + .optional(), triggerConfig: Joi.object().optional(), conditionLogic: Joi.string().valid('AND', 'OR').default('AND'), conditions: Joi.array().optional().default([]), - actions: Joi.array().required(), + actions: Joi.array().optional(), retryStrategy: Joi.object({ maxRetries: Joi.number().min(0).max(10), retryDelay: Joi.number().min(1), @@ -67,6 +68,7 @@ export default (app: Router) => { id: Joi.number().required(), name: Joi.string().optional(), description: Joi.string().optional().allow(''), + workflowGraph: Joi.object().optional(), triggerType: Joi.string() .valid('variable', 'webhook', 'task_status', 'time', 'system_event') .optional(), diff --git a/back/data/scenario.ts b/back/data/scenario.ts index 1dead96a..b8099508 100644 --- a/back/data/scenario.ts +++ b/back/data/scenario.ts @@ -6,11 +6,12 @@ export class Scenario { name: string; description?: string; isEnabled?: 1 | 0; - triggerType?: string; // 'variable' | 'webhook' | 'task_status' | 'time' | 'system_event' - triggerConfig?: any; // JSON configuration for the trigger - conditionLogic?: 'AND' | 'OR'; - conditions?: any[]; // Array of condition objects - actions?: any[]; // Array of actions to execute + workflowGraph?: any; // Flowgram workflow graph structure + triggerType?: string; // Deprecated: kept for backward compatibility + triggerConfig?: any; // Deprecated: kept for backward compatibility + conditionLogic?: 'AND' | 'OR'; // Deprecated: kept for backward compatibility + conditions?: any[]; // Deprecated: kept for backward compatibility + actions?: any[]; // Deprecated: kept for backward compatibility retryStrategy?: { maxRetries: number; retryDelay: number; // in seconds @@ -33,6 +34,7 @@ export class Scenario { this.name = options.name; this.description = options.description; this.isEnabled = options.isEnabled ?? 1; + this.workflowGraph = options.workflowGraph || null; this.triggerType = options.triggerType; this.triggerConfig = options.triggerConfig; this.conditionLogic = options.conditionLogic || 'AND'; @@ -64,9 +66,13 @@ export const ScenarioModel = sequelize.define('Scenario', { type: DataTypes.NUMBER, defaultValue: 1, }, + workflowGraph: { + type: DataTypes.JSON, + allowNull: true, + }, triggerType: { type: DataTypes.STRING, - allowNull: false, + allowNull: true, }, triggerConfig: { type: DataTypes.JSON, diff --git a/back/loaders/db.ts b/back/loaders/db.ts index 1026f4dd..980a28a3 100644 --- a/back/loaders/db.ts +++ b/back/loaders/db.ts @@ -60,6 +60,9 @@ export default async () => { try { await sequelize.query('alter table Crontabs add column task_after TEXT'); } catch (error) {} + try { + await sequelize.query('alter table Scenarios add column workflowGraph JSON'); + } catch (error) {} Logger.info('✌️ DB loaded'); } catch (error) { diff --git a/package.json b/package.json index 8c2dbc9e..58c54148 100644 --- a/package.json +++ b/package.json @@ -100,6 +100,9 @@ "helmet": "^8.1.0" }, "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", "@ant-design/icons": "^5.0.1", "@ant-design/pro-layout": "6.38.22", diff --git a/src/pages/scenario/flowgramModal.tsx b/src/pages/scenario/flowgramModal.tsx new file mode 100644 index 00000000..219e39b7 --- /dev/null +++ b/src/pages/scenario/flowgramModal.tsx @@ -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 = ({ + visible, + scenario, + onCancel, + onSuccess, +}) => { + const [form] = Form.useForm(); + const [loading, setLoading] = useState(false); + const editorRef = useRef(null); + const [workflowGraph, setWorkflowGraph] = useState(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 ( + +
+ + + + + +