重构消息通知弹窗
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 SelectOrVariableInput from '@/components/SelectOrVariableInput';
|
||||
import type { FormField as FormFieldType } from '@/components/CodeMirrorVariableInput/types';
|
||||
import { KeyValueEditor } from './KeyValueEditor';
|
||||
|
||||
interface NodeConfigModalProps {
|
||||
visible: boolean;
|
||||
@ -221,7 +222,7 @@ const NodeConfigModal: React.FC<NodeConfigModalProps> = ({
|
||||
// ✅ 初始化表单数据
|
||||
useEffect(() => {
|
||||
if (visible && node && nodeDefinition) {
|
||||
const nodeData = node.data || {};
|
||||
const nodeData = (node.data || {}) as FlowNodeData;
|
||||
|
||||
// 设置固定节点信息(从 configs 或 nodeDefinition 获取)
|
||||
setNodeName(nodeData.configs?.nodeName || nodeDefinition.nodeName);
|
||||
@ -410,6 +411,27 @@ const NodeConfigModal: React.FC<NodeConfigModalProps> = ({
|
||||
|
||||
// ✅ 渲染字段控件
|
||||
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']) {
|
||||
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 的响应式更新)
|
||||
const renderFormFields = (
|
||||
|
||||
@ -4,6 +4,7 @@ import * as definitionService from '../../Definition/List/service';
|
||||
import type { FlowNode, FlowEdge } from '../types';
|
||||
import { NodeType, isConfigurableNode } from '../nodes/types';
|
||||
import { getGatewayErrors, getGatewayWarnings } from '../utils/gatewayValidation';
|
||||
import { convertToUUID } from '@/utils/workflow/variableConversion';
|
||||
|
||||
interface WorkflowSaveData {
|
||||
nodes: FlowNode[];
|
||||
@ -95,17 +96,33 @@ export const useWorkflowSave = () => {
|
||||
const isFromParallelGateway = sourceNode?.data?.nodeType === 'GATEWAY_NODE'
|
||||
&& 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 {
|
||||
id: edge.id,
|
||||
from: edge.source, // 后端使用from字段
|
||||
to: edge.target, // 后端使用to字段
|
||||
name: edge.data?.label || "", // 边的名称
|
||||
name: finalName, // ⚠️ 边的名称(已转换)
|
||||
sourceHandle: edge.sourceHandle, // 保存源连接点
|
||||
targetHandle: edge.targetHandle, // 保存目标连接点
|
||||
config: {
|
||||
type: "sequence", // 固定为sequence类型
|
||||
// ⚠️ 并行网关的边线不传递条件配置(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
|
||||
|
||||
@ -10,6 +10,7 @@ import { EndEventNodeDefinition } from './EndEventNode';
|
||||
import { JenkinsBuildNodeDefinition } from './JenkinsBuildNode';
|
||||
import { NotificationNodeDefinition } from './NotificationNode';
|
||||
import { ApprovalNodeDefinition } from './ApprovalNode';
|
||||
import { HttpRequestNodeDefinition } from './HttpRequestNode';
|
||||
import { GatewayNodeDefinition } from './GatewayNode';
|
||||
import type { WorkflowNodeDefinition } from './types';
|
||||
|
||||
@ -22,6 +23,7 @@ export const NODE_DEFINITIONS: WorkflowNodeDefinition[] = [
|
||||
JenkinsBuildNodeDefinition,
|
||||
NotificationNodeDefinition,
|
||||
ApprovalNodeDefinition,
|
||||
HttpRequestNodeDefinition,
|
||||
GatewayNodeDefinition,
|
||||
];
|
||||
|
||||
@ -35,6 +37,7 @@ export const nodeTypes = {
|
||||
JENKINS_BUILD: BaseNode,
|
||||
NOTIFICATION: BaseNode,
|
||||
APPROVAL: BaseNode,
|
||||
HTTP_REQUEST: BaseNode,
|
||||
GATEWAY_NODE: BaseNode,
|
||||
};
|
||||
|
||||
@ -51,6 +54,7 @@ export {
|
||||
JenkinsBuildNodeDefinition,
|
||||
NotificationNodeDefinition,
|
||||
ApprovalNodeDefinition,
|
||||
HttpRequestNodeDefinition,
|
||||
GatewayNodeDefinition,
|
||||
};
|
||||
|
||||
|
||||
@ -17,6 +17,7 @@ export enum NodeType {
|
||||
JENKINS_BUILD = 'JENKINS_BUILD',
|
||||
NOTIFICATION = 'NOTIFICATION',
|
||||
APPROVAL = 'APPROVAL',
|
||||
HTTP_REQUEST = 'HTTP_REQUEST',
|
||||
GATEWAY_NODE = 'GATEWAY_NODE',
|
||||
SUB_PROCESS = 'SUB_PROCESS',
|
||||
CALL_ACTIVITY = 'CALL_ACTIVITY'
|
||||
@ -33,6 +34,7 @@ export const NODE_CATEGORY_MAP: Record<NodeType, NodeCategory> = {
|
||||
[NodeType.JENKINS_BUILD]: NodeCategory.TASK,
|
||||
[NodeType.NOTIFICATION]: NodeCategory.TASK,
|
||||
[NodeType.APPROVAL]: NodeCategory.TASK,
|
||||
[NodeType.HTTP_REQUEST]: NodeCategory.TASK,
|
||||
[NodeType.GATEWAY_NODE]: NodeCategory.GATEWAY,
|
||||
[NodeType.SUB_PROCESS]: NodeCategory.CONTAINER,
|
||||
[NodeType.CALL_ACTIVITY]: NodeCategory.CONTAINER,
|
||||
|
||||
@ -96,9 +96,9 @@ class MaintenanceDetector {
|
||||
|
||||
this.recoveryCheckTimer = setInterval(async () => {
|
||||
try {
|
||||
// 尝试请求一个轻量级接口
|
||||
// 尝试请求 Spring Boot Actuator 健康检查端点(无需认证)
|
||||
// 使用原生fetch避免触发axios拦截器
|
||||
const response = await fetch('/api/health', {
|
||||
const response = await fetch('/actuator/health', {
|
||||
method: 'GET',
|
||||
cache: 'no-cache',
|
||||
signal: AbortSignal.timeout(3000)
|
||||
@ -147,7 +147,7 @@ class MaintenanceDetector {
|
||||
*/
|
||||
async checkRecovery(): Promise<boolean> {
|
||||
try {
|
||||
const response = await fetch('/api/health', {
|
||||
const response = await fetch('/actuator/health', {
|
||||
method: 'GET',
|
||||
cache: 'no-cache',
|
||||
signal: AbortSignal.timeout(3000)
|
||||
|
||||
@ -1,12 +1,15 @@
|
||||
import type { FlowNode } from '@/pages/Workflow/Design/types';
|
||||
|
||||
/**
|
||||
* 变量表达式的正则模式
|
||||
* 匹配 ${xxx.yyy} 格式
|
||||
* xxx: 节点名称或UUID
|
||||
* yyy: 字段名
|
||||
* 正则1:匹配 ${...} 块
|
||||
*/
|
||||
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格式
|
||||
@ -28,26 +31,39 @@ export const convertToUUID = (
|
||||
return displayText;
|
||||
}
|
||||
|
||||
return displayText.replace(VARIABLE_PATTERN, (match, nodeNameOrId, fieldName) => {
|
||||
// 特殊处理:表单字段
|
||||
if (nodeNameOrId === '启动表单' || nodeNameOrId === 'form') {
|
||||
return `\${form.${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);
|
||||
|
||||
// 尝试通过名称查找节点
|
||||
const nodeByName = allNodes.find(n => {
|
||||
const nodeName = n.data?.label || n.data?.nodeDefinition?.nodeName;
|
||||
return nodeName === nodeNameOrId;
|
||||
// 阶段2:在块内容中查找并替换所有 节点名.字段 引用
|
||||
const transformedContent = blockContent.replace(NODE_REF_PATTERN, (refMatch: string, nodeNameOrId: string, fieldPath: string) => {
|
||||
console.log('🔍 找到节点引用:', { refMatch, nodeNameOrId, fieldPath });
|
||||
|
||||
// 特殊处理:表单字段
|
||||
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) {
|
||||
// 找到了,替换为UUID
|
||||
return `\${${nodeByName.id}.${fieldName}}`;
|
||||
}
|
||||
|
||||
// 可能已经是UUID格式,或者节点已被删除
|
||||
// 保持原样
|
||||
return match;
|
||||
return `\${${transformedContent}}`;
|
||||
});
|
||||
};
|
||||
|
||||
@ -71,24 +87,29 @@ export const convertToDisplayName = (
|
||||
return uuidText;
|
||||
}
|
||||
|
||||
return uuidText.replace(VARIABLE_PATTERN, (match, nodeIdOrName, fieldName) => {
|
||||
// 特殊处理:表单字段
|
||||
if (nodeIdOrName === 'form' || nodeIdOrName === '启动表单') {
|
||||
return `\${启动表单.${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 === '启动表单' || 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查找节点
|
||||
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;
|
||||
return `\${${transformedContent}}`;
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user