yyd-flow-designer 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +55 -4
- package/src/adapters/hooks.ts +58 -0
- package/src/adapters/http.ts +49 -0
- package/src/adapters/router.tsx +23 -0
- package/src/adapters/utils.ts +15 -0
- package/src/api/flowDefine.ts +30 -0
- package/src/api/flowDraft.ts +13 -0
- package/src/designer/flow/.DS_Store +0 -0
- package/src/designer/flow/Edges/ConnectionLine.tsx +44 -0
- package/src/designer/flow/Edges/CustomEdge.tsx +112 -0
- package/src/designer/flow/Edges/DefaultEdge.tsx +97 -0
- package/src/designer/flow/Edges/defaultEdgeOptions.ts +13 -0
- package/src/designer/flow/Edges/index.tsx +5 -0
- package/src/designer/flow/FlowStage.tsx +366 -0
- package/src/designer/flow/FlowToolbar.tsx +457 -0
- package/src/designer/flow/Nodes/EndEvent.tsx +51 -0
- package/src/designer/flow/Nodes/ExclusiveGateway.tsx +65 -0
- package/src/designer/flow/Nodes/ParallelGateway.tsx +68 -0
- package/src/designer/flow/Nodes/ServiceTask.tsx +37 -0
- package/src/designer/flow/Nodes/StartEvent.tsx +36 -0
- package/src/designer/flow/Nodes/UserTask.tsx +41 -0
- package/src/designer/flow/Nodes/index.tsx +15 -0
- package/src/designer/flow/actions/.DS_Store +0 -0
- package/src/designer/flow/actions/AddNode.tsx +354 -0
- package/src/designer/flow/actions/Property.tsx +200 -0
- package/src/designer/flow/actions/properties/BpmnProperties.tsx +186 -0
- package/src/designer/flow/actions/properties/components/AssigneeRulesEditor.tsx +464 -0
- package/src/designer/flow/actions/properties/components/DivInput.tsx +533 -0
- package/src/designer/flow/actions/properties/components/GatewayBranchesEditor.tsx +343 -0
- package/src/designer/flow/actions/properties/components/ParamIOEditors.tsx +611 -0
- package/src/designer/flow/actions/properties/components/TimeoutSettingEditor.tsx +166 -0
- package/src/designer/flow/actions/properties/index.tsx +17 -0
- package/src/designer/flow/components/BpmnNodeWrapper.tsx +158 -0
- package/src/designer/flow/components/ExecuteTestFlowPreview.tsx +115 -0
- package/src/designer/flow/components/ExecuteTestModal.tsx +354 -0
- package/src/designer/flow/components/FlowHotKeys.tsx +139 -0
- package/src/designer/flow/components/ShortcutsModal.tsx +202 -0
- package/src/designer/flow/components/VersionManager.tsx +214 -0
- package/src/designer/flow/config.tsx +80 -0
- package/src/designer/flow/constants.ts +15 -0
- package/src/designer/flow/icons/index.tsx +18 -0
- package/src/designer/flow/index.tsx +16 -0
- package/src/designer/flow/types.ts +22 -0
- package/src/designer/flow/utils/nodeIdentity.ts +191 -0
- package/src/designer/flow/utils/trailOverlay.ts +108 -0
- package/src/designer/flow/utils/validateFlowGraph.ts +185 -0
- package/src/designer/index.tsx +420 -0
- package/src/designer/service/index.ts +21 -0
- package/src/designer/style.less +815 -0
- package/src/index.tsx +43 -0
- package/src/shared/flow/Handles/HoverPlusHandle.tsx +41 -0
- package/src/shared/flow/PopoverContext.tsx +154 -0
- package/src/shared/flow/components/FlowPopover.tsx +23 -0
- package/src/shared/flow/components/FlowToolTip.tsx +11 -0
- package/src/shared/flow/components/XyflowFixedBackground.tsx +55 -0
- package/src/shared/flow/hooks/useAutoSizeParent.ts +55 -0
- package/src/shared/flow/stores/UndoRedoStore.ts +134 -0
- package/src/shared/flow/stores/useFlowHistoryStore.ts +3 -0
- package/src/shared/flow/stores/useFlowStore.ts +15 -0
- package/src/shared/flow/utils.ts +324 -0
- package/README.md +0 -3
package/package.json
CHANGED
|
@@ -1,6 +1,57 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "yyd-flow-designer",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "YYD Flow designer (process orchestration UI)",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"react",
|
|
8
|
+
"flow",
|
|
9
|
+
"workflow",
|
|
10
|
+
"designer",
|
|
11
|
+
"xyflow",
|
|
12
|
+
"yyd-flow-designer"
|
|
13
|
+
],
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"type": "module",
|
|
16
|
+
"main": "./src/index.tsx",
|
|
17
|
+
"module": "./src/index.tsx",
|
|
18
|
+
"types": "./src/index.tsx",
|
|
19
|
+
"exports": {
|
|
20
|
+
".": "./src/index.tsx",
|
|
21
|
+
"./style.less": "./src/designer/style.less"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"src"
|
|
25
|
+
],
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public"
|
|
28
|
+
},
|
|
29
|
+
"scripts": {
|
|
30
|
+
"build": "echo 'library ships TypeScript source for Vite/Webpack hosts'",
|
|
31
|
+
"prepublishOnly": "node -e \"require('fs').accessSync('src/index.tsx')\""
|
|
32
|
+
},
|
|
33
|
+
"peerDependencies": {
|
|
34
|
+
"@ant-design/icons": ">=5",
|
|
35
|
+
"@heroicons/react": ">=2",
|
|
36
|
+
"@xyflow/react": ">=12",
|
|
37
|
+
"ahooks": ">=3",
|
|
38
|
+
"antd": ">=5",
|
|
39
|
+
"lodash": ">=4",
|
|
40
|
+
"react": ">=18",
|
|
41
|
+
"react-dom": ">=18",
|
|
42
|
+
"react-hotkeys-hook": ">=4",
|
|
43
|
+
"zustand": ">=4"
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"clsx": "^2.1.1",
|
|
47
|
+
"dagre": "^0.8.5",
|
|
48
|
+
"tailwind-merge": "^2.5.4"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@types/dagre": "^0.7.52",
|
|
52
|
+
"@types/lodash": "^4.17.13",
|
|
53
|
+
"@types/react": "^18.3.12",
|
|
54
|
+
"@types/react-dom": "^18.3.1",
|
|
55
|
+
"typescript": "^5.6.3"
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
type ServiceFn = (params?: any) => Promise<any>;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 兼容 lowcode useService:期望后端返回 { Code, Content, Message }
|
|
7
|
+
*/
|
|
8
|
+
export function useService(fn: ServiceFn) {
|
|
9
|
+
const [data, setData] = useState<any>(null);
|
|
10
|
+
const [loading, setLoading] = useState(false);
|
|
11
|
+
const [error, setError] = useState<any>(null);
|
|
12
|
+
const fnRef = useRef(fn);
|
|
13
|
+
fnRef.current = fn;
|
|
14
|
+
|
|
15
|
+
const run = useCallback(async (params?: any) => {
|
|
16
|
+
setLoading(true);
|
|
17
|
+
setError(null);
|
|
18
|
+
try {
|
|
19
|
+
const res = await fnRef.current(params);
|
|
20
|
+
setData(res);
|
|
21
|
+
return res;
|
|
22
|
+
} catch (e) {
|
|
23
|
+
setError(e);
|
|
24
|
+
throw e;
|
|
25
|
+
} finally {
|
|
26
|
+
setLoading(false);
|
|
27
|
+
}
|
|
28
|
+
}, []);
|
|
29
|
+
|
|
30
|
+
return { data, run, loading, error };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** 取 Content / Entity */
|
|
34
|
+
export function useServiceContent(fn: ServiceFn) {
|
|
35
|
+
const [data, setData] = useState<any>(null);
|
|
36
|
+
const [loading, setLoading] = useState(false);
|
|
37
|
+
const fnRef = useRef(fn);
|
|
38
|
+
fnRef.current = fn;
|
|
39
|
+
|
|
40
|
+
const run = useCallback(async (params?: any) => {
|
|
41
|
+
setLoading(true);
|
|
42
|
+
try {
|
|
43
|
+
const res = await fnRef.current(params);
|
|
44
|
+
const content = res?.Content ?? res?.content ?? res;
|
|
45
|
+
const entity = content?.entity ?? content?.Entity ?? content;
|
|
46
|
+
setData(entity);
|
|
47
|
+
return entity;
|
|
48
|
+
} finally {
|
|
49
|
+
setLoading(false);
|
|
50
|
+
}
|
|
51
|
+
}, []);
|
|
52
|
+
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
// no auto
|
|
55
|
+
}, []);
|
|
56
|
+
|
|
57
|
+
return { data, run, loading };
|
|
58
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/** HTTP — 替代 @/share_lib/utils/HttpRequest;由宿主注入 baseURL / token */
|
|
2
|
+
let baseURL = '';
|
|
3
|
+
let getHeaders: () => Record<string, string> = () => ({});
|
|
4
|
+
|
|
5
|
+
export function configureHttp(options: { baseURL?: string; getHeaders?: () => Record<string, string> }) {
|
|
6
|
+
if (options.baseURL != null) baseURL = options.baseURL.replace(/\/$/, '');
|
|
7
|
+
if (options.getHeaders) getHeaders = options.getHeaders;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async function request<T = any>(method: string, url: string, data?: any): Promise<T> {
|
|
11
|
+
const full = url.startsWith('http') ? url : `${baseURL}${url.startsWith('/') ? '' : '/'}${url}`;
|
|
12
|
+
let res: Response;
|
|
13
|
+
try {
|
|
14
|
+
res = await fetch(full, {
|
|
15
|
+
method,
|
|
16
|
+
headers: {
|
|
17
|
+
'Content-Type': 'application/json',
|
|
18
|
+
...getHeaders(),
|
|
19
|
+
},
|
|
20
|
+
body: data != null && method !== 'GET' ? JSON.stringify(data) : undefined,
|
|
21
|
+
});
|
|
22
|
+
} catch (e: any) {
|
|
23
|
+
const raw = e?.message || String(e);
|
|
24
|
+
throw new Error(
|
|
25
|
+
raw === 'Failed to fetch'
|
|
26
|
+
? `无法连接接口 ${full}。请确认前端 dev(5173 代理)和后端 8088 都在运行`
|
|
27
|
+
: raw,
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
const json = await res.json().catch(() => ({}));
|
|
31
|
+
if (!res.ok) {
|
|
32
|
+
throw new Error(json?.Message || json?.message || res.statusText);
|
|
33
|
+
}
|
|
34
|
+
return json as T;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const httpGet = <T = any>(url: string, params?: Record<string, any>) => {
|
|
38
|
+
let u = url;
|
|
39
|
+
if (params && Object.keys(params).length) {
|
|
40
|
+
const q = new URLSearchParams();
|
|
41
|
+
Object.entries(params).forEach(([k, v]) => {
|
|
42
|
+
if (v != null) q.set(k, String(v));
|
|
43
|
+
});
|
|
44
|
+
u += (u.includes('?') ? '&' : '?') + q.toString();
|
|
45
|
+
}
|
|
46
|
+
return request<T>('GET', u);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export const httpPost = <T = any>(url: string, data?: any) => request<T>('POST', url, data);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** 替代 umi / react-router useParams — 由 BpmnDesigner 注入 */
|
|
2
|
+
import { createContext, useContext } from 'react';
|
|
3
|
+
|
|
4
|
+
export type RouteParams = {
|
|
5
|
+
processId?: string;
|
|
6
|
+
designMode?: string;
|
|
7
|
+
appId?: string;
|
|
8
|
+
projectId?: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const RouteCtx = createContext<RouteParams>({});
|
|
12
|
+
|
|
13
|
+
export const RouteParamsProvider = RouteCtx.Provider;
|
|
14
|
+
|
|
15
|
+
export function useParams<T extends Record<string, string | undefined> = RouteParams>(): T {
|
|
16
|
+
return useContext(RouteCtx) as T;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const history = {
|
|
20
|
+
push: (url: string) => {
|
|
21
|
+
if (typeof window !== 'undefined') window.location.href = url;
|
|
22
|
+
},
|
|
23
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Toast / message helpers — 替代 @/share_lib/utils */
|
|
2
|
+
import { message } from 'antd';
|
|
3
|
+
|
|
4
|
+
export const successMessage = (msg: string) => message.success(msg);
|
|
5
|
+
export const errorMessage = (msg: string) => message.error(msg);
|
|
6
|
+
|
|
7
|
+
export function getAllQueryString(): Record<string, string> {
|
|
8
|
+
if (typeof window === 'undefined') return {};
|
|
9
|
+
const params = new URLSearchParams(window.location.search);
|
|
10
|
+
const out: Record<string, string> = {};
|
|
11
|
+
params.forEach((v, k) => {
|
|
12
|
+
out[k] = v;
|
|
13
|
+
});
|
|
14
|
+
return out;
|
|
15
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { httpGet, httpPost } from '../adapters/http';
|
|
2
|
+
|
|
3
|
+
/** 与 lowcode 前端一致的路径前缀;boot 可配 context 或反代 */
|
|
4
|
+
const P = '/web';
|
|
5
|
+
|
|
6
|
+
async function getEntity(params: any) {
|
|
7
|
+
return httpGet(`${P}/flowDefine/get/` + params.processId);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async function getList(params: any) {
|
|
11
|
+
return httpPost(`${P}/flowDefine/getList`, params);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function add(params: any) {
|
|
15
|
+
return httpPost(`${P}/flowDefine/add`, params);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function update(params: any) {
|
|
19
|
+
return httpPost(`${P}/flowDefine/update`, params);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function del(params: any) {
|
|
23
|
+
return httpPost(`${P}/flowDefine/deleteByIds`, params);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function checkProcessCodeExists(params: any) {
|
|
27
|
+
return httpGet(`${P}/flowDefine/checkProcessCodeExists`, params);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export default { getEntity, getList, add, update, del, checkProcessCodeExists };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { httpGet, httpPost } from '../adapters/http';
|
|
2
|
+
|
|
3
|
+
const P = '/web';
|
|
4
|
+
|
|
5
|
+
export const FlowDraftSaveReq = (params: any) => httpPost(`${P}/flowDraft/save`, params);
|
|
6
|
+
export const FlowDraftGetReq = (params: any) =>
|
|
7
|
+
httpGet(`${P}/flowDraft/getByProcessId/` + params?.processId);
|
|
8
|
+
export const FlowVersionHistoryAddReq = (params: any) =>
|
|
9
|
+
httpPost(`${P}/flowVersionHistory/add`, params);
|
|
10
|
+
export const FlowVersionHistoryGetListReq = (params: any) =>
|
|
11
|
+
httpPost(`${P}/flowVersionHistory/getList`, params);
|
|
12
|
+
export const FlowVersionHistoryRestoreReq = (params: any) =>
|
|
13
|
+
httpPost(`${P}/flowVersionHistory/restore`, params);
|
|
Binary file
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { ConnectionLineComponentProps } from "@xyflow/react";
|
|
2
|
+
import { getSmoothStepPath, Position } from "@xyflow/react";
|
|
3
|
+
|
|
4
|
+
const ConnectionLine = ({
|
|
5
|
+
fromX,
|
|
6
|
+
fromY,
|
|
7
|
+
toX,
|
|
8
|
+
toY,
|
|
9
|
+
fromPosition = Position.Right,
|
|
10
|
+
toPosition = Position.Left,
|
|
11
|
+
}: ConnectionLineComponentProps) => {
|
|
12
|
+
const [edgePath] = getSmoothStepPath({
|
|
13
|
+
sourceX: fromX,
|
|
14
|
+
sourceY: fromY,
|
|
15
|
+
sourcePosition: fromPosition,
|
|
16
|
+
targetX: toX,
|
|
17
|
+
targetY: toY,
|
|
18
|
+
targetPosition: toPosition,
|
|
19
|
+
borderRadius: 14,
|
|
20
|
+
offset: 18,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<g>
|
|
25
|
+
<path
|
|
26
|
+
fill="none"
|
|
27
|
+
stroke={"#009688"}
|
|
28
|
+
strokeWidth={2}
|
|
29
|
+
className="animated"
|
|
30
|
+
d={edgePath}
|
|
31
|
+
/>
|
|
32
|
+
<circle
|
|
33
|
+
cx={toX}
|
|
34
|
+
cy={toY}
|
|
35
|
+
fill="#fff"
|
|
36
|
+
r={2}
|
|
37
|
+
stroke={"#009688"}
|
|
38
|
+
strokeWidth={10}
|
|
39
|
+
/>
|
|
40
|
+
</g>
|
|
41
|
+
);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export default ConnectionLine;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
BaseEdge,
|
|
5
|
+
getBezierPath,
|
|
6
|
+
type EdgeProps,
|
|
7
|
+
} from '@xyflow/react';
|
|
8
|
+
import { useMemo } from 'react';
|
|
9
|
+
|
|
10
|
+
export default function CustomEdge({
|
|
11
|
+
id,
|
|
12
|
+
sourceX,
|
|
13
|
+
sourceY,
|
|
14
|
+
targetX,
|
|
15
|
+
targetY,
|
|
16
|
+
sourcePosition,
|
|
17
|
+
targetPosition,
|
|
18
|
+
style = {},
|
|
19
|
+
selected,
|
|
20
|
+
markerEnd,
|
|
21
|
+
}: EdgeProps) {
|
|
22
|
+
const [edgePath] = getBezierPath({
|
|
23
|
+
sourceX,
|
|
24
|
+
sourceY,
|
|
25
|
+
sourcePosition,
|
|
26
|
+
targetX,
|
|
27
|
+
targetY,
|
|
28
|
+
targetPosition,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// 计算路径长度用于动画
|
|
32
|
+
const pathLength = useMemo(() => {
|
|
33
|
+
if (typeof document !== 'undefined') {
|
|
34
|
+
const pathElement = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
35
|
+
pathElement.setAttribute('d', edgePath);
|
|
36
|
+
const length = pathElement.getTotalLength();
|
|
37
|
+
return length || 1000; // 如果计算失败,使用默认值
|
|
38
|
+
}
|
|
39
|
+
return 1000;
|
|
40
|
+
}, [edgePath]);
|
|
41
|
+
|
|
42
|
+
// 使用 #4b7bec 作为默认颜色
|
|
43
|
+
const strokeColor = "#4b7bec";
|
|
44
|
+
|
|
45
|
+
// 获取 strokeWidth 的实际数值,用于计算动画路径的宽度
|
|
46
|
+
const strokeWidthValue = useMemo(() => {
|
|
47
|
+
if (typeof document !== 'undefined') {
|
|
48
|
+
const root = document.documentElement;
|
|
49
|
+
const computedValue = getComputedStyle(root)
|
|
50
|
+
.getPropertyValue('--xy-edge-stroke-width-default')
|
|
51
|
+
.trim();
|
|
52
|
+
const numValue = parseFloat(computedValue);
|
|
53
|
+
// 如果无法获取 CSS 变量值,使用默认值 2
|
|
54
|
+
return isNaN(numValue) ? 1 : numValue;
|
|
55
|
+
}
|
|
56
|
+
return 2; // SSR 时的默认值
|
|
57
|
+
}, []);
|
|
58
|
+
|
|
59
|
+
// 虚线参数:8px 实线,4px 空白
|
|
60
|
+
const dashArray = "8,4";
|
|
61
|
+
// 电流光点长度约为路径的 20%,让效果更明显
|
|
62
|
+
const dashLength = pathLength * 0.2;
|
|
63
|
+
|
|
64
|
+
// 处理 markerEnd:如果是字符串直接使用,如果是对象则转换为字符串 URL
|
|
65
|
+
// xyflow 会自动生成 marker,我们只需要使用传递进来的 markerEnd(字符串格式)
|
|
66
|
+
const markerEndUrl = useMemo(() => {
|
|
67
|
+
if (!markerEnd) {
|
|
68
|
+
// 如果没有传递 markerEnd,使用 BaseEdge 的默认行为
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
// markerEnd 应该是字符串 URL(如 "url(#react-flow__arrowclosed-...)")
|
|
72
|
+
return typeof markerEnd === 'string' ? markerEnd : undefined;
|
|
73
|
+
}, [markerEnd]);
|
|
74
|
+
|
|
75
|
+
// 合并样式,添加虚线
|
|
76
|
+
const edgeStyle = {
|
|
77
|
+
...style,
|
|
78
|
+
strokeDasharray: dashArray,
|
|
79
|
+
opacity: 0.4,
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<>
|
|
84
|
+
{/* 使用 BaseEdge 渲染基础虚线路径,它会自动处理 xyflow 默认箭头 */}
|
|
85
|
+
<BaseEdge
|
|
86
|
+
id={id}
|
|
87
|
+
path={edgePath}
|
|
88
|
+
style={edgeStyle}
|
|
89
|
+
markerEnd={markerEndUrl}
|
|
90
|
+
/>
|
|
91
|
+
{/* 电流动画路径 - 仅在选中时显示动画效果 */}
|
|
92
|
+
{selected && (
|
|
93
|
+
<path
|
|
94
|
+
fill="none"
|
|
95
|
+
stroke={strokeColor}
|
|
96
|
+
strokeWidth={strokeWidthValue * 1.8}
|
|
97
|
+
strokeDasharray={`${dashLength},${pathLength + dashLength}`}
|
|
98
|
+
d={edgePath}
|
|
99
|
+
opacity="1"
|
|
100
|
+
markerEnd={markerEndUrl}
|
|
101
|
+
>
|
|
102
|
+
<animate
|
|
103
|
+
attributeName="stroke-dashoffset"
|
|
104
|
+
values={`0;-${pathLength + dashLength}`}
|
|
105
|
+
dur="2s"
|
|
106
|
+
repeatCount="indefinite"
|
|
107
|
+
/>
|
|
108
|
+
</path>
|
|
109
|
+
)}
|
|
110
|
+
</>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { BaseEdge, getBezierPath, type EdgeProps } from '@xyflow/react';
|
|
4
|
+
import { useMemo } from 'react';
|
|
5
|
+
|
|
6
|
+
const EDGE_STROKE = '#94a3b8';
|
|
7
|
+
const EDGE_STROKE_SELECTED = '#6366f1';
|
|
8
|
+
const BACK_RAIL_GAP = 28;
|
|
9
|
+
const BACK_RAIL_STEP = 16;
|
|
10
|
+
|
|
11
|
+
const round = (n: number) => Math.round(n * 10) / 10;
|
|
12
|
+
|
|
13
|
+
const isBackEdge = (sourceX: number, targetX: number) => sourceX > targetX + 16;
|
|
14
|
+
|
|
15
|
+
const getBackArcPath = (sx: number, sy: number, tx: number, ty: number, railY: number) => {
|
|
16
|
+
const x1 = round(sx);
|
|
17
|
+
const y1 = round(sy);
|
|
18
|
+
const x2 = round(tx);
|
|
19
|
+
const y2 = round(ty);
|
|
20
|
+
const yRail = round(railY);
|
|
21
|
+
const span = Math.max(80, x1 - x2);
|
|
22
|
+
const c1x = round(x1 - span * 0.08);
|
|
23
|
+
const c2x = round(x2 + span * 0.22);
|
|
24
|
+
return `M ${x1} ${y1} C ${c1x} ${yRail}, ${c2x} ${yRail}, ${x2} ${y2}`;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export default function DefaultEdge({
|
|
28
|
+
id,
|
|
29
|
+
sourceX,
|
|
30
|
+
sourceY,
|
|
31
|
+
targetX,
|
|
32
|
+
targetY,
|
|
33
|
+
sourcePosition,
|
|
34
|
+
targetPosition,
|
|
35
|
+
style = {},
|
|
36
|
+
selected,
|
|
37
|
+
data,
|
|
38
|
+
}: EdgeProps) {
|
|
39
|
+
const back = isBackEdge(sourceX, targetX);
|
|
40
|
+
const lane = back ? (id.charCodeAt(Math.max(0, id.length - 1)) % 4) : 0;
|
|
41
|
+
const railY = Math.min(sourceY, targetY) - BACK_RAIL_GAP - lane * BACK_RAIL_STEP;
|
|
42
|
+
|
|
43
|
+
const edgePath = useMemo(() => {
|
|
44
|
+
if (back) {
|
|
45
|
+
return getBackArcPath(sourceX, sourceY, targetX, targetY, railY);
|
|
46
|
+
}
|
|
47
|
+
const [path] = getBezierPath({
|
|
48
|
+
sourceX,
|
|
49
|
+
sourceY,
|
|
50
|
+
sourcePosition,
|
|
51
|
+
targetX,
|
|
52
|
+
targetY,
|
|
53
|
+
targetPosition,
|
|
54
|
+
});
|
|
55
|
+
return path;
|
|
56
|
+
}, [back, sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition, railY]);
|
|
57
|
+
|
|
58
|
+
const trailOn = Boolean((data as any)?.trailOn);
|
|
59
|
+
const stroke = trailOn ? '#22c55e' : selected ? EDGE_STROKE_SELECTED : EDGE_STROKE;
|
|
60
|
+
const markerId = `bpmn-edge-arrow-${id}-${trailOn ? 'trail' : selected ? 'selected' : 'default'}`;
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<>
|
|
64
|
+
<defs>
|
|
65
|
+
<marker
|
|
66
|
+
id={markerId}
|
|
67
|
+
viewBox="0 0 14 14"
|
|
68
|
+
markerWidth="12.5"
|
|
69
|
+
markerHeight="12.5"
|
|
70
|
+
refX="11"
|
|
71
|
+
refY="7"
|
|
72
|
+
orient="auto"
|
|
73
|
+
markerUnits="strokeWidth"
|
|
74
|
+
>
|
|
75
|
+
<path
|
|
76
|
+
d="M1.8 2.8 L10.5 7 L1.8 11.2 Q3.4 7 1.8 2.8 Z"
|
|
77
|
+
fill={stroke}
|
|
78
|
+
stroke={stroke}
|
|
79
|
+
strokeWidth="0.6"
|
|
80
|
+
strokeLinejoin="round"
|
|
81
|
+
strokeLinecap="round"
|
|
82
|
+
/>
|
|
83
|
+
</marker>
|
|
84
|
+
</defs>
|
|
85
|
+
<BaseEdge
|
|
86
|
+
id={id}
|
|
87
|
+
path={edgePath}
|
|
88
|
+
style={{
|
|
89
|
+
...style,
|
|
90
|
+
stroke,
|
|
91
|
+
strokeWidth: trailOn || selected ? 2.5 : 2,
|
|
92
|
+
}}
|
|
93
|
+
markerEnd={`url(#${markerId})`}
|
|
94
|
+
/>
|
|
95
|
+
</>
|
|
96
|
+
);
|
|
97
|
+
}
|