重构消息通知弹窗
This commit is contained in:
parent
e409101fbf
commit
85bebb7fc3
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -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 = (
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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,10 +31,20 @@ 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 })));
|
||||||
|
|
||||||
|
// 阶段1:处理 ${...} 块
|
||||||
|
return displayText.replace(BLOCK_PATTERN, (blockMatch, blockContent) => {
|
||||||
|
console.log('🔍 找到 ${} 块:', blockContent);
|
||||||
|
|
||||||
|
// 阶段2:在块内容中查找并替换所有 节点名.字段 引用
|
||||||
|
const transformedContent = blockContent.replace(NODE_REF_PATTERN, (refMatch: string, nodeNameOrId: string, fieldPath: string) => {
|
||||||
|
console.log('🔍 找到节点引用:', { refMatch, nodeNameOrId, fieldPath });
|
||||||
|
|
||||||
// 特殊处理:表单字段
|
// 特殊处理:表单字段
|
||||||
if (nodeNameOrId === '启动表单' || nodeNameOrId === 'form') {
|
if (nodeNameOrId === '启动表单' || nodeNameOrId === 'form' || nodeNameOrId === 'notification') {
|
||||||
return `\${form.${fieldName}}`;
|
return `${nodeNameOrId}.${fieldPath}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 尝试通过名称查找节点
|
// 尝试通过名称查找节点
|
||||||
@ -41,13 +54,16 @@ export const convertToUUID = (
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (nodeByName) {
|
if (nodeByName) {
|
||||||
// 找到了,替换为UUID
|
console.log('✅ 找到节点,转换为UUID:', nodeByName.id);
|
||||||
return `\${${nodeByName.id}.${fieldName}}`;
|
return `${nodeByName.id}.${fieldPath}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 可能已经是UUID格式,或者节点已被删除
|
// 可能已经是UUID格式,保持原样
|
||||||
// 保持原样
|
console.log('⚠️ 未找到节点(可能已是UUID):', nodeNameOrId);
|
||||||
return match;
|
return refMatch;
|
||||||
|
});
|
||||||
|
|
||||||
|
return `\${${transformedContent}}`;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -71,10 +87,13 @@ export const convertToDisplayName = (
|
|||||||
return uuidText;
|
return uuidText;
|
||||||
}
|
}
|
||||||
|
|
||||||
return uuidText.replace(VARIABLE_PATTERN, (match, nodeIdOrName, fieldName) => {
|
// 阶段1:处理 ${...} 块
|
||||||
|
return uuidText.replace(BLOCK_PATTERN, (blockMatch, blockContent) => {
|
||||||
|
// 阶段2:在块内容中查找并替换所有 UUID.字段 引用为 节点名.字段
|
||||||
|
const transformedContent = blockContent.replace(NODE_REF_PATTERN, (refMatch: string, nodeIdOrName: string, fieldPath: string) => {
|
||||||
// 特殊处理:表单字段
|
// 特殊处理:表单字段
|
||||||
if (nodeIdOrName === 'form' || nodeIdOrName === '启动表单') {
|
if (nodeIdOrName === 'form' || nodeIdOrName === '启动表单' || nodeIdOrName === 'notification') {
|
||||||
return `\${启动表单.${fieldName}}`;
|
return `${nodeIdOrName}.${fieldPath}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 尝试通过UUID查找节点
|
// 尝试通过UUID查找节点
|
||||||
@ -83,12 +102,14 @@ export const convertToDisplayName = (
|
|||||||
if (nodeById) {
|
if (nodeById) {
|
||||||
// 找到了,替换为显示名称
|
// 找到了,替换为显示名称
|
||||||
const nodeName = nodeById.data?.label || nodeById.data?.nodeDefinition?.nodeName || nodeIdOrName;
|
const nodeName = nodeById.data?.label || nodeById.data?.nodeDefinition?.nodeName || nodeIdOrName;
|
||||||
return `\${${nodeName}.${fieldName}}`;
|
return `${nodeName}.${fieldPath}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 可能已经是显示名称格式,或者节点已被删除
|
// 可能已经是显示名称格式,保持原样
|
||||||
// 保持原样
|
return refMatch;
|
||||||
return match;
|
});
|
||||||
|
|
||||||
|
return `\${${transformedContent}}`;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user