重构消息通知弹窗

This commit is contained in:
dengqichen 2025-11-20 13:52:03 +08:00
parent e409101fbf
commit 85bebb7fc3
7 changed files with 200 additions and 46 deletions

View File

@ -0,0 +1,88 @@
import React from 'react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Plus, Trash2 } from 'lucide-react';
import { useFieldArray, Control, Controller } from 'react-hook-form';
/**
*
* x-component: "KeyValueEditor" 使
* HTTP
*/
interface KeyValueEditorProps {
control: Control<any>;
name: string;
keyPlaceholder?: string;
valuePlaceholder?: string;
disabled?: boolean;
}
export const KeyValueEditor: React.FC<KeyValueEditorProps> = ({
control,
name,
keyPlaceholder = 'Key',
valuePlaceholder = 'Value',
disabled = false,
}) => {
const { fields, append, remove } = useFieldArray({
control,
name,
});
return (
<div className="space-y-2">
{fields.map((field, index) => (
<div key={field.id} className="flex items-center gap-2">
<div className="flex-1 grid grid-cols-2 gap-2">
<Controller
control={control}
name={`${name}.${index}.key`}
defaultValue={(field as any).key || ''}
render={({ field: controllerField }) => (
<Input
{...controllerField}
placeholder={keyPlaceholder}
disabled={disabled}
/>
)}
/>
<Controller
control={control}
name={`${name}.${index}.value`}
defaultValue={(field as any).value || ''}
render={({ field: controllerField }) => (
<Input
{...controllerField}
placeholder={valuePlaceholder}
disabled={disabled}
/>
)}
/>
</div>
<Button
type="button"
variant="ghost"
size="icon"
className="h-9 w-9 text-destructive hover:text-destructive hover:bg-destructive/10"
onClick={() => remove(index)}
disabled={disabled}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
))}
<Button
type="button"
variant="outline"
size="sm"
className="w-full"
onClick={() => append({ key: '', value: '' })}
disabled={disabled}
>
<Plus className="h-4 w-4 mr-2" />
</Button>
</div>
);
};

View File

@ -24,6 +24,7 @@ import { convertObjectToUUID, convertObjectToDisplayName } from '@/utils/workflo
import CodeMirrorVariableInput from '@/components/CodeMirrorVariableInput'; import CodeMirrorVariableInput from '@/components/CodeMirrorVariableInput';
import SelectOrVariableInput from '@/components/SelectOrVariableInput'; import SelectOrVariableInput from '@/components/SelectOrVariableInput';
import type { FormField as FormFieldType } from '@/components/CodeMirrorVariableInput/types'; import type { FormField as FormFieldType } from '@/components/CodeMirrorVariableInput/types';
import { KeyValueEditor } from './KeyValueEditor';
interface NodeConfigModalProps { interface NodeConfigModalProps {
visible: boolean; visible: boolean;
@ -221,7 +222,7 @@ const NodeConfigModal: React.FC<NodeConfigModalProps> = ({
// ✅ 初始化表单数据 // ✅ 初始化表单数据
useEffect(() => { useEffect(() => {
if (visible && node && nodeDefinition) { if (visible && node && nodeDefinition) {
const nodeData = node.data || {}; const nodeData = (node.data || {}) as FlowNodeData;
// 设置固定节点信息(从 configs 或 nodeDefinition 获取) // 设置固定节点信息(从 configs 或 nodeDefinition 获取)
setNodeName(nodeData.configs?.nodeName || nodeDefinition.nodeName); setNodeName(nodeData.configs?.nodeName || nodeDefinition.nodeName);
@ -410,6 +411,27 @@ const NodeConfigModal: React.FC<NodeConfigModalProps> = ({
// ✅ 渲染字段控件 // ✅ 渲染字段控件
const renderFieldControl = useCallback((prop: any, field: any, key: string) => { const renderFieldControl = useCallback((prop: any, field: any, key: string) => {
// ✅ 优先检查自定义组件x-component
if (prop['x-component']) {
switch (prop['x-component']) {
case 'KeyValueEditor':
return (
<KeyValueEditor
control={inputForm.control}
name={key}
keyPlaceholder={prop.items?.properties?.key?.title || 'Key'}
valuePlaceholder={prop.items?.properties?.value?.title || 'Value'}
disabled={loading}
{...(prop['x-component-props'] || {})}
/>
);
// 未来可以添加更多自定义组件
default:
console.warn(`Unknown x-component: ${prop['x-component']}`);
break;
}
}
// 动态数据源 // 动态数据源
if (prop['x-dataSource']) { if (prop['x-dataSource']) {
const options = dataSourceCache[prop['x-dataSource']] || []; const options = dataSourceCache[prop['x-dataSource']] || [];
@ -607,7 +629,7 @@ const NodeConfigModal: React.FC<NodeConfigModalProps> = ({
/> />
); );
} }
}, [dataSourceCache, loadingDataSources, loading, node, renderSelect]); }, [dataSourceCache, loadingDataSources, loading, node, renderSelect, inputForm.control]);
// ✅ 渲染表单字段(不使用 useCallback 以支持 watch 的响应式更新) // ✅ 渲染表单字段(不使用 useCallback 以支持 watch 的响应式更新)
const renderFormFields = ( const renderFormFields = (

View File

@ -4,6 +4,7 @@ import * as definitionService from '../../Definition/List/service';
import type { FlowNode, FlowEdge } from '../types'; import type { FlowNode, FlowEdge } from '../types';
import { NodeType, isConfigurableNode } from '../nodes/types'; import { NodeType, isConfigurableNode } from '../nodes/types';
import { getGatewayErrors, getGatewayWarnings } from '../utils/gatewayValidation'; import { getGatewayErrors, getGatewayWarnings } from '../utils/gatewayValidation';
import { convertToUUID } from '@/utils/workflow/variableConversion';
interface WorkflowSaveData { interface WorkflowSaveData {
nodes: FlowNode[]; nodes: FlowNode[];
@ -95,17 +96,33 @@ export const useWorkflowSave = () => {
const isFromParallelGateway = sourceNode?.data?.nodeType === 'GATEWAY_NODE' const isFromParallelGateway = sourceNode?.data?.nodeType === 'GATEWAY_NODE'
&& sourceNode?.data?.inputMapping?.gatewayType === 'parallelGateway'; && sourceNode?.data?.inputMapping?.gatewayType === 'parallelGateway';
// ⚠️⚠️⚠️ 关键:兜底转换边的表达式(可能从旧数据加载,从未编辑过)
const originalCondition = edge.data?.condition;
let finalCondition = originalCondition;
if (originalCondition && originalCondition.type === 'EXPRESSION' && originalCondition.expression) {
// 转换条件表达式中的节点名称为 UUID
finalCondition = {
...originalCondition,
expression: convertToUUID(originalCondition.expression, data.nodes)
};
}
// 转换边的 name通常就是 condition.expression
const originalName = edge.data?.label || "";
const finalName = originalName ? convertToUUID(originalName, data.nodes) : "";
return { return {
id: edge.id, id: edge.id,
from: edge.source, // 后端使用from字段 from: edge.source, // 后端使用from字段
to: edge.target, // 后端使用to字段 to: edge.target, // 后端使用to字段
name: edge.data?.label || "", // 边的名称 name: finalName, // ⚠️ 边的名称(已转换)
sourceHandle: edge.sourceHandle, // 保存源连接点 sourceHandle: edge.sourceHandle, // 保存源连接点
targetHandle: edge.targetHandle, // 保存目标连接点 targetHandle: edge.targetHandle, // 保存目标连接点
config: { config: {
type: "sequence", // 固定为sequence类型 type: "sequence", // 固定为sequence类型
// ⚠️ 并行网关的边线不传递条件配置BPMN 规范不允许) // ⚠️ 并行网关的边线不传递条件配置BPMN 规范不允许)
condition: isFromParallelGateway ? undefined : edge.data?.condition condition: isFromParallelGateway ? undefined : finalCondition
}, },
// 优先保存多个拐点;若无则回退到单个控制点;否则为空 // 优先保存多个拐点;若无则回退到单个控制点;否则为空
vertices: Array.isArray((edge as any)?.data?.vertices) && (edge as any).data.vertices.length > 0 vertices: Array.isArray((edge as any)?.data?.vertices) && (edge as any).data.vertices.length > 0

View File

@ -10,6 +10,7 @@ import { EndEventNodeDefinition } from './EndEventNode';
import { JenkinsBuildNodeDefinition } from './JenkinsBuildNode'; import { JenkinsBuildNodeDefinition } from './JenkinsBuildNode';
import { NotificationNodeDefinition } from './NotificationNode'; import { NotificationNodeDefinition } from './NotificationNode';
import { ApprovalNodeDefinition } from './ApprovalNode'; import { ApprovalNodeDefinition } from './ApprovalNode';
import { HttpRequestNodeDefinition } from './HttpRequestNode';
import { GatewayNodeDefinition } from './GatewayNode'; import { GatewayNodeDefinition } from './GatewayNode';
import type { WorkflowNodeDefinition } from './types'; import type { WorkflowNodeDefinition } from './types';
@ -22,6 +23,7 @@ export const NODE_DEFINITIONS: WorkflowNodeDefinition[] = [
JenkinsBuildNodeDefinition, JenkinsBuildNodeDefinition,
NotificationNodeDefinition, NotificationNodeDefinition,
ApprovalNodeDefinition, ApprovalNodeDefinition,
HttpRequestNodeDefinition,
GatewayNodeDefinition, GatewayNodeDefinition,
]; ];
@ -35,6 +37,7 @@ export const nodeTypes = {
JENKINS_BUILD: BaseNode, JENKINS_BUILD: BaseNode,
NOTIFICATION: BaseNode, NOTIFICATION: BaseNode,
APPROVAL: BaseNode, APPROVAL: BaseNode,
HTTP_REQUEST: BaseNode,
GATEWAY_NODE: BaseNode, GATEWAY_NODE: BaseNode,
}; };
@ -51,6 +54,7 @@ export {
JenkinsBuildNodeDefinition, JenkinsBuildNodeDefinition,
NotificationNodeDefinition, NotificationNodeDefinition,
ApprovalNodeDefinition, ApprovalNodeDefinition,
HttpRequestNodeDefinition,
GatewayNodeDefinition, GatewayNodeDefinition,
}; };

View File

@ -17,6 +17,7 @@ export enum NodeType {
JENKINS_BUILD = 'JENKINS_BUILD', JENKINS_BUILD = 'JENKINS_BUILD',
NOTIFICATION = 'NOTIFICATION', NOTIFICATION = 'NOTIFICATION',
APPROVAL = 'APPROVAL', APPROVAL = 'APPROVAL',
HTTP_REQUEST = 'HTTP_REQUEST',
GATEWAY_NODE = 'GATEWAY_NODE', GATEWAY_NODE = 'GATEWAY_NODE',
SUB_PROCESS = 'SUB_PROCESS', SUB_PROCESS = 'SUB_PROCESS',
CALL_ACTIVITY = 'CALL_ACTIVITY' CALL_ACTIVITY = 'CALL_ACTIVITY'
@ -33,6 +34,7 @@ export const NODE_CATEGORY_MAP: Record<NodeType, NodeCategory> = {
[NodeType.JENKINS_BUILD]: NodeCategory.TASK, [NodeType.JENKINS_BUILD]: NodeCategory.TASK,
[NodeType.NOTIFICATION]: NodeCategory.TASK, [NodeType.NOTIFICATION]: NodeCategory.TASK,
[NodeType.APPROVAL]: NodeCategory.TASK, [NodeType.APPROVAL]: NodeCategory.TASK,
[NodeType.HTTP_REQUEST]: NodeCategory.TASK,
[NodeType.GATEWAY_NODE]: NodeCategory.GATEWAY, [NodeType.GATEWAY_NODE]: NodeCategory.GATEWAY,
[NodeType.SUB_PROCESS]: NodeCategory.CONTAINER, [NodeType.SUB_PROCESS]: NodeCategory.CONTAINER,
[NodeType.CALL_ACTIVITY]: NodeCategory.CONTAINER, [NodeType.CALL_ACTIVITY]: NodeCategory.CONTAINER,

View File

@ -96,9 +96,9 @@ class MaintenanceDetector {
this.recoveryCheckTimer = setInterval(async () => { this.recoveryCheckTimer = setInterval(async () => {
try { try {
// 尝试请求一个轻量级接口 // 尝试请求 Spring Boot Actuator 健康检查端点(无需认证)
// 使用原生fetch避免触发axios拦截器 // 使用原生fetch避免触发axios拦截器
const response = await fetch('/api/health', { const response = await fetch('/actuator/health', {
method: 'GET', method: 'GET',
cache: 'no-cache', cache: 'no-cache',
signal: AbortSignal.timeout(3000) signal: AbortSignal.timeout(3000)
@ -147,7 +147,7 @@ class MaintenanceDetector {
*/ */
async checkRecovery(): Promise<boolean> { async checkRecovery(): Promise<boolean> {
try { try {
const response = await fetch('/api/health', { const response = await fetch('/actuator/health', {
method: 'GET', method: 'GET',
cache: 'no-cache', cache: 'no-cache',
signal: AbortSignal.timeout(3000) signal: AbortSignal.timeout(3000)

View File

@ -1,12 +1,15 @@
import type { FlowNode } from '@/pages/Workflow/Design/types'; import type { FlowNode } from '@/pages/Workflow/Design/types';
/** /**
* * 1 ${...}
* ${xxx.yyy}
* xxx: 节点名称或UUID
* yyy: 字段名
*/ */
const VARIABLE_PATTERN = /\$\{([^.}]+)\.([^}]+)\}/g; const BLOCK_PATTERN = /\$\{([^}]+)\}/g;
/**
* 2 .
* UUID格式节点ID
*/
const NODE_REF_PATTERN = /([a-zA-Z0-9_\u4e00-\u9fa5\-]+)\.([a-zA-Z0-9_.]+)/g;
/** /**
* UUID格式 * UUID格式
@ -28,26 +31,39 @@ export const convertToUUID = (
return displayText; return displayText;
} }
return displayText.replace(VARIABLE_PATTERN, (match, nodeNameOrId, fieldName) => { console.log('🔍 convertToUUID - 输入:', displayText);
// 特殊处理:表单字段 console.log('🔍 可用节点:', allNodes.map(n => ({ id: n.id, label: n.data?.label })));
if (nodeNameOrId === '启动表单' || nodeNameOrId === 'form') {
return `\${form.${fieldName}}`; // 阶段1处理 ${...} 块
} return displayText.replace(BLOCK_PATTERN, (blockMatch, blockContent) => {
console.log('🔍 找到 ${} 块:', blockContent);
// 尝试通过名称查找节点 // 阶段2在块内容中查找并替换所有 节点名.字段 引用
const nodeByName = allNodes.find(n => { const transformedContent = blockContent.replace(NODE_REF_PATTERN, (refMatch: string, nodeNameOrId: string, fieldPath: string) => {
const nodeName = n.data?.label || n.data?.nodeDefinition?.nodeName; console.log('🔍 找到节点引用:', { refMatch, nodeNameOrId, fieldPath });
return nodeName === nodeNameOrId;
// 特殊处理:表单字段
if (nodeNameOrId === '启动表单' || nodeNameOrId === 'form' || nodeNameOrId === 'notification') {
return `${nodeNameOrId}.${fieldPath}`;
}
// 尝试通过名称查找节点
const nodeByName = allNodes.find(n => {
const nodeName = n.data?.label || n.data?.nodeDefinition?.nodeName;
return nodeName === nodeNameOrId;
});
if (nodeByName) {
console.log('✅ 找到节点转换为UUID:', nodeByName.id);
return `${nodeByName.id}.${fieldPath}`;
}
// 可能已经是UUID格式保持原样
console.log('⚠️ 未找到节点可能已是UUID:', nodeNameOrId);
return refMatch;
}); });
if (nodeByName) { return `\${${transformedContent}}`;
// 找到了替换为UUID
return `\${${nodeByName.id}.${fieldName}}`;
}
// 可能已经是UUID格式或者节点已被删除
// 保持原样
return match;
}); });
}; };
@ -71,24 +87,29 @@ export const convertToDisplayName = (
return uuidText; return uuidText;
} }
return uuidText.replace(VARIABLE_PATTERN, (match, nodeIdOrName, fieldName) => { // 阶段1处理 ${...} 块
// 特殊处理:表单字段 return uuidText.replace(BLOCK_PATTERN, (blockMatch, blockContent) => {
if (nodeIdOrName === 'form' || nodeIdOrName === '启动表单') { // 阶段2在块内容中查找并替换所有 UUID.字段 引用为 节点名.字段
return `\${.${fieldName}}`; const transformedContent = blockContent.replace(NODE_REF_PATTERN, (refMatch: string, nodeIdOrName: string, fieldPath: string) => {
} // 特殊处理:表单字段
if (nodeIdOrName === 'form' || nodeIdOrName === '启动表单' || nodeIdOrName === 'notification') {
return `${nodeIdOrName}.${fieldPath}`;
}
// 尝试通过UUID查找节点
const nodeById = allNodes.find(n => n.id === nodeIdOrName);
if (nodeById) {
// 找到了,替换为显示名称
const nodeName = nodeById.data?.label || nodeById.data?.nodeDefinition?.nodeName || nodeIdOrName;
return `${nodeName}.${fieldPath}`;
}
// 可能已经是显示名称格式,保持原样
return refMatch;
});
// 尝试通过UUID查找节点 return `\${${transformedContent}}`;
const nodeById = allNodes.find(n => n.id === nodeIdOrName);
if (nodeById) {
// 找到了,替换为显示名称
const nodeName = nodeById.data?.label || nodeById.data?.nodeDefinition?.nodeName || nodeIdOrName;
return `\${${nodeName}.${fieldName}}`;
}
// 可能已经是显示名称格式,或者节点已被删除
// 保持原样
return match;
}); });
}; };