feat: manually mirror opencoze's code from bytedance
Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { LINE_OFFSET } from '../../../constants/lines';
|
||||
|
||||
export default function ArrowRenderer({
|
||||
id,
|
||||
pos,
|
||||
strokeWidth,
|
||||
}: {
|
||||
id: string;
|
||||
strokeWidth: number;
|
||||
pos: {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
}) {
|
||||
return (
|
||||
<path
|
||||
d={`M ${pos.x - LINE_OFFSET},${pos.y - LINE_OFFSET} L ${pos.x},${
|
||||
pos.y
|
||||
} L ${pos.x - LINE_OFFSET},${pos.y + LINE_OFFSET}`}
|
||||
strokeLinecap="round"
|
||||
stroke={`url(#${id})`}
|
||||
fill="none"
|
||||
strokeWidth={strokeWidth}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
type WorkflowLineEntity,
|
||||
POINT_RADIUS,
|
||||
WorkflowLineRenderData,
|
||||
} from '@flowgram-adapter/free-layout-editor';
|
||||
import { type IPoint } from '@flowgram-adapter/common';
|
||||
|
||||
import WithPopover from '../popover/with-popover';
|
||||
import styles from '../index.module.less';
|
||||
import ArrowRenderer from '../arrow';
|
||||
import { STROKE_WIDTH_SLECTED, STROKE_WIDTH } from '../../../constants/points';
|
||||
|
||||
// import { AddPoint } from '@/components/add-point';
|
||||
|
||||
const PADDING = 12;
|
||||
|
||||
export interface BezierLineProps {
|
||||
fromColor?: string;
|
||||
toColor?: string;
|
||||
color?: string; // 高亮颜色,优先级最高
|
||||
selected?: boolean;
|
||||
showControlPoints?: boolean;
|
||||
line: WorkflowLineEntity;
|
||||
version: string; // 用于控制 memo 刷新
|
||||
}
|
||||
|
||||
export const BezierLineRender = React.memo(
|
||||
WithPopover((props: BezierLineProps) => {
|
||||
const { line, color, fromColor, toColor, selected } = props;
|
||||
const renderData = line.getData(WorkflowLineRenderData);
|
||||
const { bounds: bbox } = renderData;
|
||||
const { position } = line;
|
||||
// 相对位置
|
||||
const toRelative = (p: IPoint) => ({
|
||||
x: p.x - bbox.x + PADDING,
|
||||
y: p.y - bbox.y + PADDING,
|
||||
});
|
||||
const fromPos = toRelative(position.from);
|
||||
const toPos = toRelative(position.to);
|
||||
// 真正连接线需要到的点的位置
|
||||
const arrowToPos = {
|
||||
x: toPos.x - POINT_RADIUS,
|
||||
y: toPos.y,
|
||||
};
|
||||
const linearStartColor = fromPos.x < arrowToPos.x ? fromColor : toColor;
|
||||
const linerarEndColor = fromPos.x < arrowToPos.x ? toColor : fromColor;
|
||||
|
||||
const strokeWidth = selected ? STROKE_WIDTH_SLECTED : STROKE_WIDTH;
|
||||
const path = (
|
||||
<path
|
||||
d={renderData.path}
|
||||
fill="none"
|
||||
stroke={`url(#${line.id})`}
|
||||
strokeWidth={strokeWidth}
|
||||
className={line.processing ? styles.processingLine : ''}
|
||||
/>
|
||||
);
|
||||
|
||||
// const cls = clx('gedit-mindmap-line', {
|
||||
// hovered,
|
||||
// drawing,
|
||||
// processing: props.processing
|
||||
// });
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className="gedit-flow-activity-edge"
|
||||
style={{
|
||||
left: bbox.x - PADDING,
|
||||
top: bbox.y - PADDING,
|
||||
position: 'absolute',
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
width={bbox.width + PADDING * 2}
|
||||
height={bbox.height + PADDING * 2}
|
||||
>
|
||||
<defs>
|
||||
<linearGradient
|
||||
x1="0%"
|
||||
y1="100%"
|
||||
x2="100%"
|
||||
y2="100%"
|
||||
id={line.id}
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor={color || linearStartColor} offset="0%" />
|
||||
<stop stopColor={color || linerarEndColor} offset="100%" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<g>
|
||||
{path}
|
||||
<ArrowRenderer
|
||||
id={line.id}
|
||||
pos={arrowToPos}
|
||||
strokeWidth={strokeWidth}
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}),
|
||||
);
|
||||
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
POINT_RADIUS,
|
||||
WorkflowLineRenderData,
|
||||
} from '@flowgram-adapter/free-layout-editor';
|
||||
|
||||
import WithPopover from '../popover/with-popover';
|
||||
import styles from '../index.module.less';
|
||||
import { type BezierLineProps } from '../bezier-line';
|
||||
import ArrowRenderer from '../arrow';
|
||||
import { STROKE_WIDTH, STROKE_WIDTH_SLECTED } from '../../../constants/points';
|
||||
|
||||
/**
|
||||
* 折叠线
|
||||
*/
|
||||
export const FoldLineRender = React.memo(
|
||||
WithPopover((props: BezierLineProps) => {
|
||||
const { selected, color, line } = props;
|
||||
const { to } = line.position;
|
||||
const strokeWidth = selected ? STROKE_WIDTH_SLECTED : STROKE_WIDTH;
|
||||
// 真正连接线需要到的点的位置
|
||||
const arrowToPos = {
|
||||
x: to.x - POINT_RADIUS,
|
||||
y: to.y,
|
||||
};
|
||||
const renderData = line.getData(WorkflowLineRenderData);
|
||||
// const bounds = line.bezier.foldBounds
|
||||
// const points = line.bezier.foldPoints
|
||||
//
|
||||
// const debug = (
|
||||
// <>
|
||||
// <div
|
||||
// style={{
|
||||
// left: bounds.left,
|
||||
// top: bounds.top,
|
||||
// width: bounds.width,
|
||||
// height: bounds.height,
|
||||
// position: 'absolute',
|
||||
// background: 'red',
|
||||
// zIndex: 1000,
|
||||
// opacity: 0.3,
|
||||
// }}
|
||||
// />
|
||||
// {points.map((p, i) => (
|
||||
// <div
|
||||
// key={i}
|
||||
// style={{
|
||||
// left: p.x,
|
||||
// top: p.y,
|
||||
// width: 10,
|
||||
// height: 10,
|
||||
// marginTop: -5,
|
||||
// marginBottom: -5,
|
||||
// position: 'absolute',
|
||||
// background: 'blue',
|
||||
// zIndex: 1000,
|
||||
// }}
|
||||
// />
|
||||
// ))}
|
||||
// </>
|
||||
// )
|
||||
return (
|
||||
<div
|
||||
className="gedit-flow-activity-edge"
|
||||
style={{ position: 'absolute' }}
|
||||
>
|
||||
<svg overflow="visible">
|
||||
<defs>
|
||||
<linearGradient
|
||||
x1="0%"
|
||||
y1="100%"
|
||||
x2="100%"
|
||||
y2="100%"
|
||||
id={line.id}
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor={color} offset="0%" />
|
||||
<stop stopColor={color} offset="100%" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<g>
|
||||
<path
|
||||
d={renderData.path}
|
||||
fill="none"
|
||||
strokeLinecap="round"
|
||||
stroke={color}
|
||||
strokeWidth={strokeWidth}
|
||||
className={line.processing ? styles.processingLine : ''}
|
||||
/>
|
||||
<ArrowRenderer
|
||||
id={line.id}
|
||||
pos={arrowToPos}
|
||||
strokeWidth={strokeWidth}
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
}),
|
||||
);
|
||||
@@ -0,0 +1,10 @@
|
||||
@keyframes dashdraw {
|
||||
from {
|
||||
stroke-dashoffset: 10;
|
||||
}
|
||||
}
|
||||
|
||||
.processing-line {
|
||||
stroke-dasharray: 5;
|
||||
animation: dashdraw 0.5s linear infinite;
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { FlowRendererRegistry } from '@flowgram-adapter/free-layout-editor';
|
||||
import {
|
||||
useService,
|
||||
WorkflowHoverService,
|
||||
} from '@flowgram-adapter/free-layout-editor';
|
||||
|
||||
const LINE_POPOVER = 'line-popover';
|
||||
|
||||
export default function WithPopover(Component) {
|
||||
return function WrappedComponent(props) {
|
||||
const hoverService = useService<WorkflowHoverService>(WorkflowHoverService);
|
||||
|
||||
const renderRegistry =
|
||||
useService<FlowRendererRegistry>(FlowRendererRegistry);
|
||||
|
||||
const Popover =
|
||||
renderRegistry.tryToGetRendererComponent(LINE_POPOVER)?.renderer;
|
||||
|
||||
const { line } = props;
|
||||
const isHovered = hoverService.isHovered(line._id);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Component {...props} />
|
||||
{Popover ? <Popover line={line} isHovered={isHovered} /> : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
.selector-bounds-background {
|
||||
cursor: move;
|
||||
display: none;
|
||||
// background: rgba(255, 255, 255, 0);
|
||||
}
|
||||
.selector-bounds-forground {
|
||||
cursor: move;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
outline: 1px solid var(--g-playground-selectBox-outline);
|
||||
// 在节点的上边
|
||||
z-index: 33;
|
||||
background-color: var(--g-playground-selectBox-background);
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
// import { domUtils } from '@flowgram-adapter/common';
|
||||
import { type FlowSelectorBoundsLayerOptions } from '@flowgram-adapter/free-layout-editor';
|
||||
import { SelectionService, useService } from '@flowgram-adapter/free-layout-editor';
|
||||
|
||||
import { getSelectionBounds } from '../../utils/selection-utils';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
/**
|
||||
* 选择框
|
||||
* @param props
|
||||
* @constructor
|
||||
*/
|
||||
export const SelectorBounds: React.FC<
|
||||
FlowSelectorBoundsLayerOptions
|
||||
> = props => {
|
||||
const selectService = useService<SelectionService>(SelectionService);
|
||||
const bounds = getSelectionBounds(selectService, true);
|
||||
if (bounds.width === 0 || bounds.height === 0) {
|
||||
// domUtils.setStyle(domNode, {
|
||||
// display: 'none',
|
||||
// });
|
||||
return <></>;
|
||||
}
|
||||
const style = {
|
||||
display: 'block',
|
||||
left: bounds.left,
|
||||
top: bounds.top,
|
||||
width: bounds.width,
|
||||
height: bounds.height,
|
||||
};
|
||||
// domUtils.setStyle(domNode, style);
|
||||
return <div className={styles.selectorBoundsForground} style={style} />;
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
// demo 环境自绘 cross-hair,正式环境使用 IconAdd
|
||||
export default function CrossHair(): JSX.Element {
|
||||
return (
|
||||
<div className={styles.symbol}>
|
||||
<div className={styles.crossHair} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
/* stylelint-disable selector-class-pattern */
|
||||
// 背景的白色圆圈
|
||||
.workflow-point {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-top: -10px;
|
||||
margin-left: -10px;
|
||||
// 非 hover 状态下的样式
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
|
||||
& > .symbol {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.bg-circle {
|
||||
position: absolute;
|
||||
transform: scale(0.5);
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
|
||||
background-color: #fff;
|
||||
border-radius: 50%;
|
||||
|
||||
transition: all 0.2s linear 0s;
|
||||
}
|
||||
|
||||
.bg {
|
||||
position: relative;
|
||||
transform: scale(0.4, 0.4);
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
background: #9197f1;
|
||||
border-radius: 50%;
|
||||
|
||||
transition: all 0.2s linear 0s;
|
||||
|
||||
&.hasError {
|
||||
background: red;
|
||||
}
|
||||
|
||||
.symbol {
|
||||
pointer-events: none;
|
||||
|
||||
position: absolute;
|
||||
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
|
||||
color: #fff;
|
||||
|
||||
opacity: 0;
|
||||
|
||||
transition: opacity 0.2s linear 0s;
|
||||
|
||||
& > svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.focus-circle {
|
||||
position: absolute;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
|
||||
opacity: 0;
|
||||
background: #9197f1;
|
||||
border-radius: 50%;
|
||||
|
||||
transition: opacity 0.2s linear 0s;
|
||||
}
|
||||
}
|
||||
|
||||
&.linked .bg:not(.hasError) {
|
||||
background: #4d53e8;
|
||||
}
|
||||
|
||||
&.hovered .bg:not(.hasError) {
|
||||
cursor: crosshair;
|
||||
transform: scale(1, 1);
|
||||
background: #4d53e8;
|
||||
border: none;
|
||||
|
||||
& > .symbol {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover .bg:is(.hasError) {
|
||||
transform: scale(1, 1);
|
||||
background: red;
|
||||
border: none;
|
||||
|
||||
& > .symbol {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cross-hair {
|
||||
position: relative;
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
|
||||
&::after,
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
background: #fff;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
&::after {
|
||||
left: 4px;
|
||||
width: 2px;
|
||||
height: 6px;
|
||||
box-shadow: 0 4px #fff;
|
||||
}
|
||||
|
||||
&::before {
|
||||
top: 4px;
|
||||
width: 6px;
|
||||
height: 2px;
|
||||
box-shadow: 4px 0 #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.warning {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
transform: translateY(-8px);
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import ReactDOM from 'react-dom';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { Tooltip } from '@coze-arch/coze-design';
|
||||
import { useService } from '@flowgram-adapter/free-layout-editor';
|
||||
import {
|
||||
usePlaygroundReadonlyState,
|
||||
WorkflowDragService,
|
||||
WorkflowHoverService,
|
||||
WorkflowLinesManager,
|
||||
type WorkflowPortEntity,
|
||||
} from '@flowgram-adapter/free-layout-editor';
|
||||
|
||||
import { PORT_BG_CLASS_NAME } from '../../constants/points';
|
||||
import { Warning } from './warning';
|
||||
import CrossHair from './cross-hair';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
export interface WorkflowPortRenderProps {
|
||||
entity: WorkflowPortEntity;
|
||||
onClick?: (event: React.MouseEvent, port: WorkflowPortEntity) => void;
|
||||
}
|
||||
|
||||
export const WorkflowPortRender: React.FC<WorkflowPortRenderProps> =
|
||||
React.memo<WorkflowPortRenderProps>(props => {
|
||||
const dragService = useService<WorkflowDragService>(WorkflowDragService);
|
||||
const hoverService = useService<WorkflowHoverService>(WorkflowHoverService);
|
||||
const linesManager = useService<WorkflowLinesManager>(WorkflowLinesManager);
|
||||
const { entity, onClick } = props;
|
||||
const { portType, portID, relativePosition, disabled, errorMessage } =
|
||||
entity as WorkflowPortEntity & {
|
||||
errorMessage?: string;
|
||||
};
|
||||
|
||||
const [targetElement, setTargetElement] = useState(entity.targetElement);
|
||||
const [posX, updatePosX] = useState(relativePosition.x);
|
||||
const [posY, updatePosY] = useState(relativePosition.y);
|
||||
const [hovered, setHovered] = useState(false);
|
||||
const [linked, setLinked] = useState(Boolean(entity?.lines?.length));
|
||||
const [hasError, setHasError] = useState(props.entity.hasError);
|
||||
const readonly = usePlaygroundReadonlyState();
|
||||
|
||||
const onMouseDown = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
const isMouseCenterButton = e.button === 1;
|
||||
if (portType === 'input' || disabled || isMouseCenterButton) {
|
||||
return;
|
||||
}
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
dragService.startDrawingLine(entity, e);
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- custom
|
||||
[dragService, portType, portID],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// useEffect 时序问题可能导致 port.hasError 非最新,需重新触发一次 validate
|
||||
props.entity.validate();
|
||||
setHasError(props.entity.hasError);
|
||||
const dispose = props.entity.onEntityChange(() => {
|
||||
// 如果有挂载的节点,不需要更新位置信息
|
||||
if (entity.targetElement) {
|
||||
if (entity.targetElement !== targetElement) {
|
||||
setTargetElement(entity.targetElement);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const newPos = props.entity.relativePosition;
|
||||
// 加上 round 避免点位抖动
|
||||
updatePosX(Math.round(newPos.x));
|
||||
updatePosY(Math.round(newPos.y));
|
||||
});
|
||||
const dispose2 = hoverService.onHoveredChange(id => {
|
||||
setHovered(hoverService.isHovered(entity.id));
|
||||
});
|
||||
const dispose3 = props.entity.onErrorChanged(() => {
|
||||
setHasError(props.entity.hasError);
|
||||
});
|
||||
const dispose4 = linesManager.onAvailableLinesChange(() => {
|
||||
setTimeout(() => {
|
||||
setLinked(Boolean(entity?.lines?.length));
|
||||
}, 0);
|
||||
});
|
||||
return () => {
|
||||
dispose.dispose();
|
||||
dispose2.dispose();
|
||||
dispose3.dispose();
|
||||
dispose4.dispose();
|
||||
};
|
||||
}, [props.entity, hoverService, entity, targetElement, linesManager]);
|
||||
|
||||
// 监听变化
|
||||
const className = classNames(styles.workflowPoint, {
|
||||
[styles.hovered]:
|
||||
!readonly && hovered && !disabled && portType !== 'input',
|
||||
// 有线条链接的时候深蓝色小圆点
|
||||
[styles.linked]: linked,
|
||||
});
|
||||
|
||||
const icon = useMemo(() => {
|
||||
const iconComp = (
|
||||
<div
|
||||
className={classNames({
|
||||
[styles.bg]: true,
|
||||
[PORT_BG_CLASS_NAME]: true,
|
||||
'workflow-point-bg': true,
|
||||
[styles.hasError]: hasError,
|
||||
})}
|
||||
>
|
||||
{hasError ? <Warning /> : <CrossHair />}
|
||||
</div>
|
||||
);
|
||||
if (hasError && errorMessage) {
|
||||
return (
|
||||
<Tooltip
|
||||
className={styles.tooltip}
|
||||
content={errorMessage}
|
||||
trigger="hover"
|
||||
position="top"
|
||||
>
|
||||
{iconComp}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
return iconComp;
|
||||
}, [hasError, errorMessage]);
|
||||
|
||||
const content = (
|
||||
<div
|
||||
className={className}
|
||||
style={targetElement ? undefined : { left: posX, top: posY }}
|
||||
onClick={e => onClick?.(e, entity)}
|
||||
onMouseDown={onMouseDown}
|
||||
data-port-entity-id={entity.id}
|
||||
data-testid="bot-edit-multi-agent-flow-node-add-button"
|
||||
>
|
||||
<div
|
||||
className={classNames(styles.bgCircle, 'workflow-bg-circle')}
|
||||
></div>
|
||||
{icon}
|
||||
<div className={styles['focus-circle']} />
|
||||
</div>
|
||||
);
|
||||
if (targetElement) {
|
||||
return ReactDOM.createPortal(content, targetElement);
|
||||
}
|
||||
return content;
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
export const Warning = () => (
|
||||
<div className={classNames(styles.symbol, styles.warning)}>
|
||||
<svg
|
||||
style={{ width: 10, height: 10 }}
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
id="path1"
|
||||
fill="#ffffff"
|
||||
stroke="none"
|
||||
d="M 12 0 C 10.674479 0 9.6 1.074528 9.6 2.4 L 9.6 14.4 C 9.6 15.725521 10.674479 16.799999 12 16.799999 C 13.325521 16.799999 14.4 15.725521 14.4 14.4 L 14.4 2.4 C 14.4 1.074528 13.325521 0 12 0 Z"
|
||||
/>
|
||||
<path
|
||||
id="path2"
|
||||
fill="#ffffff"
|
||||
stroke="none"
|
||||
d="M 12 19.200001 C 10.674479 19.200001 9.6 20.274479 9.6 21.6 C 9.6 22.925518 10.674479 24 12 24 C 13.325521 24 14.4 22.925518 14.4 21.6 C 14.4 20.274479 13.325521 19.200001 12 19.200001 Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
18
frontend/packages/workflow/render/src/constants/lines.ts
Normal file
18
frontend/packages/workflow/render/src/constants/lines.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// 箭头宽度
|
||||
export const LINE_OFFSET = 6;
|
||||
23
frontend/packages/workflow/render/src/constants/points.ts
Normal file
23
frontend/packages/workflow/render/src/constants/points.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// 连接点半径
|
||||
|
||||
export const STROKE_WIDTH_SLECTED = 3;
|
||||
|
||||
export const STROKE_WIDTH = 2;
|
||||
|
||||
export const PORT_BG_CLASS_NAME = 'workflow-port-bg';
|
||||
17
frontend/packages/workflow/render/src/global.d.ts
vendored
Normal file
17
frontend/packages/workflow/render/src/global.d.ts
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/// <reference types='@coze-arch/bot-typings' />
|
||||
224
frontend/packages/workflow/render/src/index.module.less
Normal file
224
frontend/packages/workflow/render/src/index.module.less
Normal file
@@ -0,0 +1,224 @@
|
||||
/* stylelint-disable block-no-empty */
|
||||
/* stylelint-disable plugin/disallow-first-level-global */
|
||||
.playground-load {}
|
||||
|
||||
:root {
|
||||
--g-selection-background: #4D53E8;
|
||||
--g-editor-background: #f2f3f5;
|
||||
--g-playground-select: var(--g-selection-background);
|
||||
--g-playground-hover: var(--g-selection-background);
|
||||
--g-playground-line: var(--g-selection-background);
|
||||
--g-playground-blur: #999;
|
||||
--g-playground-selectBox-outline: var(--g-selection-background);
|
||||
--g-playground-selectBox-background: rgba(141, 144, 231, 10%);
|
||||
--g-playground-select-hover-background: rgba(77, 83, 232, 10%);
|
||||
--g-playground-select-control-size: 12px;
|
||||
}
|
||||
|
||||
:global {
|
||||
.gedit-playground {
|
||||
user-select: none;
|
||||
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
background-color: var(--g-editor-background);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.gedit-playground-scroll-right {
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
right: 2px;
|
||||
|
||||
width: 7px;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.gedit-playground-scroll-bottom {
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
bottom: 2px;
|
||||
|
||||
width: 100vw;
|
||||
height: 7px;
|
||||
}
|
||||
|
||||
.gedit-playground-scroll-right-block {
|
||||
position: absolute;
|
||||
opacity: 0.3;
|
||||
border-radius: 3.5px;
|
||||
}
|
||||
|
||||
.gedit-playground-scroll-right-block:hover {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.gedit-playground-scroll-bottom-block {
|
||||
position: absolute;
|
||||
opacity: 0.3;
|
||||
border-radius: 3.5px;
|
||||
}
|
||||
|
||||
.gedit-playground-scroll-bottom-block:hover {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.gedit-playground-scroll-hidden {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.gedit-playground * {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.gedit-playground-loading {
|
||||
position: absolute;
|
||||
z-index: 100;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
color: white;
|
||||
text-align: center;
|
||||
|
||||
opacity: 0.8;
|
||||
|
||||
transition: opacity 0.8s;
|
||||
}
|
||||
|
||||
.gedit-hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.gedit-playground-pipeline {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
overflow: visible;
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.gedit-playground-pipeline::before {
|
||||
content: '';
|
||||
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
width: 1px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.gedit-playground-layer {
|
||||
position: absolute;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.gedit-selector-box {
|
||||
position: absolute;
|
||||
z-index: 33;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
width: 0;
|
||||
height: 0;
|
||||
|
||||
background-color: var(--g-playground-selectBox-background);
|
||||
outline: 1px solid var(--g-playground-selectBox-outline);
|
||||
}
|
||||
|
||||
.gedit-selector-box-block {
|
||||
position: absolute;
|
||||
z-index: 9999;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
display: none;
|
||||
|
||||
width: 0;
|
||||
height: 0;
|
||||
|
||||
background-color: rgba(0, 0, 0, 0%);
|
||||
}
|
||||
|
||||
.gedit-selector-bounds-background {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
width: 0;
|
||||
height: 0;
|
||||
|
||||
background-color: #f0f4ff;
|
||||
outline: 1px solid var(--g-playground-selectBox-outline);
|
||||
}
|
||||
|
||||
.gedit-selector-bounds-foreground {
|
||||
position: absolute;
|
||||
z-index: 33;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
width: 0;
|
||||
height: 0;
|
||||
|
||||
background: rgba(255, 255, 255, 0%);
|
||||
}
|
||||
|
||||
.gedit-flow-activity-node {
|
||||
position: absolute;
|
||||
|
||||
&:hover {
|
||||
:global {
|
||||
.workflow-point-bg {
|
||||
transform: scale(0.7, 0.7);
|
||||
background: #4d53e8;
|
||||
}
|
||||
|
||||
.workflow-bg-circle {
|
||||
transform: scale(1, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:active {
|
||||
:global {
|
||||
.workflow-point-bg {
|
||||
transform: scale(0.7, 0.7);
|
||||
background: #4d53e8;
|
||||
}
|
||||
|
||||
.workflow-bg-circle {
|
||||
transform: scale(1, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.gedit-grid-svg {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
left: 20px;
|
||||
|
||||
display: block;
|
||||
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
27
frontend/packages/workflow/render/src/index.ts
Normal file
27
frontend/packages/workflow/render/src/index.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import 'reflect-metadata';
|
||||
export * from './workflow-render-provider';
|
||||
export * from './workflow-render-contribution';
|
||||
export * from './components/workflow-port-render';
|
||||
export * from './workflow-shorcuts-contribution';
|
||||
export {
|
||||
FlowRendererKey,
|
||||
FlowRendererRegistry,
|
||||
FlowRendererContribution,
|
||||
} from '@flowgram-adapter/free-layout-editor';
|
||||
export * from './constants/lines';
|
||||
161
frontend/packages/workflow/render/src/layer/background-layer.tsx
Normal file
161
frontend/packages/workflow/render/src/layer/background-layer.tsx
Normal file
@@ -0,0 +1,161 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { nanoid } from 'nanoid';
|
||||
import {
|
||||
Layer,
|
||||
observeEntity,
|
||||
PlaygroundConfigEntity,
|
||||
SCALE_WIDTH,
|
||||
} from '@flowgram-adapter/free-layout-editor';
|
||||
import { domUtils } from '@flowgram-adapter/common';
|
||||
|
||||
interface BackgroundScaleUnit {
|
||||
realSize: number;
|
||||
renderSize: number;
|
||||
zoom: number;
|
||||
}
|
||||
|
||||
const PATTERN_ID = 'grid-dot-pattern';
|
||||
const RENDER_SIZE = 20;
|
||||
const DOT_SIZE = 1;
|
||||
|
||||
/**
|
||||
* dot 网格背景
|
||||
*/
|
||||
export class BackgroundLayer extends Layer {
|
||||
static type = 'WorkflowBackgroundLayer';
|
||||
@observeEntity(PlaygroundConfigEntity)
|
||||
protected playgroundConfigEntity: PlaygroundConfigEntity;
|
||||
protected patternId = `${PATTERN_ID}${nanoid()}`;
|
||||
|
||||
node = domUtils.createDivWithClass('gedit-flow-background-layer');
|
||||
grid: HTMLElement = document.createElement('div');
|
||||
|
||||
/**
|
||||
* 当前缩放比
|
||||
*/
|
||||
get zoom(): number {
|
||||
return this.config.finalScale;
|
||||
}
|
||||
|
||||
onReady() {
|
||||
const { firstChild } = this.pipelineNode;
|
||||
// 背景插入到最下边
|
||||
this.pipelineNode.insertBefore(this.node, firstChild);
|
||||
// 初始化设置最大 200% 最小 10% 缩放
|
||||
this.playgroundConfigEntity.updateConfig({
|
||||
minZoom: 0.1,
|
||||
maxZoom: 2,
|
||||
});
|
||||
// 确保点的位置在线条的下方
|
||||
this.grid.style.zIndex = '-1';
|
||||
this.grid.style.position = 'relative';
|
||||
this.node.appendChild(this.grid);
|
||||
this.grid.className = 'gedit-grid-svg';
|
||||
}
|
||||
|
||||
/**
|
||||
* 最小单元格大小
|
||||
*/
|
||||
getScaleUnit(): BackgroundScaleUnit {
|
||||
const { zoom } = this;
|
||||
|
||||
return {
|
||||
realSize: RENDER_SIZE, // 一个单元格代表的真实大小
|
||||
renderSize: Math.round(RENDER_SIZE * zoom * 100) / 100, // 一个单元格渲染的大小值
|
||||
zoom, // 缩放比
|
||||
};
|
||||
}
|
||||
/**
|
||||
* 绘制
|
||||
*/
|
||||
autorun(): void {
|
||||
const playgroundConfig = this.playgroundConfigEntity.config;
|
||||
const scaleUnit = this.getScaleUnit();
|
||||
const mod = scaleUnit.renderSize * 10;
|
||||
const viewBoxWidth = playgroundConfig.width + mod * 2;
|
||||
const viewBoxHeight = playgroundConfig.height + mod * 2;
|
||||
const { scrollX } = playgroundConfig;
|
||||
const { scrollY } = playgroundConfig;
|
||||
const scrollXDelta = this.getScrollDelta(scrollX, mod);
|
||||
const scrollYDelta = this.getScrollDelta(scrollY, mod);
|
||||
domUtils.setStyle(this.node, {
|
||||
left: scrollX - SCALE_WIDTH,
|
||||
top: scrollY - SCALE_WIDTH,
|
||||
});
|
||||
this.drawGrid(scaleUnit);
|
||||
// 设置网格
|
||||
this.setSVGStyle(this.grid, {
|
||||
width: viewBoxWidth,
|
||||
height: viewBoxHeight,
|
||||
left: SCALE_WIDTH - scrollXDelta - mod,
|
||||
top: SCALE_WIDTH - scrollYDelta - mod,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制网格
|
||||
*/
|
||||
protected drawGrid(unit: BackgroundScaleUnit): void {
|
||||
const minor = unit.renderSize;
|
||||
if (!this.grid) {
|
||||
return;
|
||||
}
|
||||
const patternSize = DOT_SIZE * this.zoom;
|
||||
const newContent = `
|
||||
<svg width="100%" height="100%">
|
||||
<pattern id="${this.patternId}" width="${minor}" height="${minor}" patternUnits="userSpaceOnUse">
|
||||
<circle
|
||||
cx="${patternSize}"
|
||||
cy="${patternSize}"
|
||||
r="${patternSize}"
|
||||
stroke="#eceeef"
|
||||
fill-opacity="0.5"
|
||||
/>
|
||||
</pattern>
|
||||
<rect width="100%" height="100%" fill="url(#${this.patternId})"/>
|
||||
</svg>`;
|
||||
this.grid.innerHTML = newContent;
|
||||
}
|
||||
|
||||
protected setSVGStyle(
|
||||
svgElement: HTMLElement | undefined,
|
||||
style: { width: number; height: number; left: number; top: number },
|
||||
): void {
|
||||
if (!svgElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
svgElement.style.width = `${style.width}px`;
|
||||
svgElement.style.height = `${style.height}px`;
|
||||
svgElement.style.left = `${style.left}px`;
|
||||
svgElement.style.top = `${style.top}px`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取相对滚动距离
|
||||
* @param realScroll
|
||||
* @param mod
|
||||
*/
|
||||
protected getScrollDelta(realScroll: number, mod: number): number {
|
||||
// 正向滚动不用补差
|
||||
if (realScroll >= 0) {
|
||||
return realScroll % mod;
|
||||
}
|
||||
return mod - (Math.abs(realScroll) % mod);
|
||||
}
|
||||
}
|
||||
309
frontend/packages/workflow/render/src/layer/hover-layer.tsx
Normal file
309
frontend/packages/workflow/render/src/layer/hover-layer.tsx
Normal file
@@ -0,0 +1,309 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/* eslint-disable complexity */
|
||||
import { inject, injectable } from 'inversify';
|
||||
import { SelectorBoxConfigEntity } from '@flowgram-adapter/free-layout-editor';
|
||||
import { FlowNodeTransformData } from '@flowgram-adapter/free-layout-editor';
|
||||
import {
|
||||
EditorState,
|
||||
EditorStateConfigEntity,
|
||||
Layer,
|
||||
PlaygroundConfigEntity,
|
||||
observeEntities,
|
||||
observeEntity,
|
||||
observeEntityDatas,
|
||||
type LayerOptions,
|
||||
} from '@flowgram-adapter/free-layout-editor';
|
||||
import {
|
||||
WorkflowDocument,
|
||||
WorkflowDragService,
|
||||
WorkflowHoverService,
|
||||
WorkflowLineEntity,
|
||||
WorkflowLinesManager,
|
||||
WorkflowNodeEntity,
|
||||
WorkflowPortEntity,
|
||||
WorkflowSelectService,
|
||||
} from '@flowgram-adapter/free-layout-editor';
|
||||
import { type IPoint } from '@flowgram-adapter/common';
|
||||
|
||||
import { getSelectionBounds } from '../utils/selection-utils';
|
||||
import { PORT_BG_CLASS_NAME } from '../constants/points';
|
||||
|
||||
export interface HoverLayerOptions extends LayerOptions {
|
||||
canHovered?: (e: MouseEvent, service: WorkflowHoverService) => boolean;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
export namespace HoverLayerOptions {
|
||||
export const DEFAULT: HoverLayerOptions = {
|
||||
canHovered: () => true,
|
||||
};
|
||||
}
|
||||
|
||||
const LINE_CLASS_NAME = '.gedit-flow-activity-line';
|
||||
const NODE_CLASS_NAME = '.gedit-flow-activity-node';
|
||||
|
||||
@injectable()
|
||||
export class HoverLayer extends Layer<HoverLayerOptions> {
|
||||
static type = 'HoverLayer';
|
||||
@inject(WorkflowDocument) document: WorkflowDocument;
|
||||
@inject(WorkflowSelectService) selectionService: WorkflowSelectService;
|
||||
@inject(WorkflowDragService) dragService: WorkflowDragService;
|
||||
@inject(WorkflowHoverService) hoverService: WorkflowHoverService;
|
||||
@inject(WorkflowLinesManager)
|
||||
linesManager: WorkflowLinesManager;
|
||||
@observeEntity(EditorStateConfigEntity)
|
||||
protected editorStateConfig: EditorStateConfigEntity;
|
||||
@observeEntity(SelectorBoxConfigEntity)
|
||||
protected selectorBoxConfigEntity: SelectorBoxConfigEntity;
|
||||
@inject(PlaygroundConfigEntity) configEntity: PlaygroundConfigEntity;
|
||||
/**
|
||||
* 监听节点 transform
|
||||
*/
|
||||
@observeEntityDatas(WorkflowNodeEntity, FlowNodeTransformData)
|
||||
protected readonly nodeTransforms: FlowNodeTransformData[];
|
||||
/**
|
||||
* 按选中排序
|
||||
* @private
|
||||
*/
|
||||
protected nodeTransformsWithSort: FlowNodeTransformData[] = [];
|
||||
autorun(): void {
|
||||
const { activatedNode } = this.selectionService;
|
||||
this.nodeTransformsWithSort = this.nodeTransforms
|
||||
.filter(n => n.entity.id !== 'root')
|
||||
.reverse() // 后创建的排在前面
|
||||
.sort(n1 => (n1.entity === activatedNode ? -1 : 0));
|
||||
}
|
||||
/**
|
||||
* 监听线条
|
||||
*/
|
||||
@observeEntities(WorkflowLineEntity)
|
||||
protected readonly lines: WorkflowLineEntity[];
|
||||
/**
|
||||
* 是否正在调整线条
|
||||
* @protected
|
||||
*/
|
||||
get isDrawing(): boolean {
|
||||
return this.linesManager.isDrawing;
|
||||
}
|
||||
onReady(): void {
|
||||
this.options = {
|
||||
...HoverLayerOptions.DEFAULT,
|
||||
...this.options,
|
||||
};
|
||||
this.toDispose.pushAll([
|
||||
// 监听画布鼠标移动事件
|
||||
this.listenPlaygroundEvent('mousemove', (e: MouseEvent) => {
|
||||
this.hoverService.hoveredPos = this.config.getPosFromMouseEvent(e);
|
||||
if (!this.isEnabled()) {
|
||||
return;
|
||||
}
|
||||
// @ts-expect-error -- linter-disable-autofix
|
||||
if (!this.options.canHovered(e, this.hoverService)) {
|
||||
return;
|
||||
}
|
||||
const mousePos = this.config.getPosFromMouseEvent(e);
|
||||
// 更新 hover 状态
|
||||
this.updateHoveredState(mousePos, e?.target as HTMLElement);
|
||||
}),
|
||||
this.selectionService.onSelectionChanged(() => this.autorun()),
|
||||
// 控制选中逻辑
|
||||
this.listenPlaygroundEvent(
|
||||
'mousedown',
|
||||
(e: MouseEvent): boolean | undefined => {
|
||||
if (!this.isEnabled() || this.isDrawing) {
|
||||
return undefined;
|
||||
}
|
||||
const { hoveredNode } = this.hoverService;
|
||||
// 重置线条
|
||||
if (hoveredNode && hoveredNode instanceof WorkflowLineEntity) {
|
||||
this.dragService.resetLine(hoveredNode, e);
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
hoveredNode &&
|
||||
hoveredNode instanceof WorkflowPortEntity &&
|
||||
hoveredNode.portType !== 'input' &&
|
||||
!hoveredNode.disabled &&
|
||||
e.button !== 1
|
||||
) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.selectionService.clear();
|
||||
this.dragService.startDrawingLine(hoveredNode, e);
|
||||
return true;
|
||||
}
|
||||
const mousePos = this.config.getPosFromMouseEvent(e);
|
||||
const selectionBounds = getSelectionBounds(
|
||||
this.selectionService,
|
||||
// 这里只考虑多选模式,单选模式已经下沉到 use-node-render 中
|
||||
true,
|
||||
);
|
||||
if (
|
||||
selectionBounds.width > 0 &&
|
||||
selectionBounds.contains(mousePos.x, mousePos.y)
|
||||
) {
|
||||
/**
|
||||
* 拖拽选择框
|
||||
*/
|
||||
this.dragService.startDragSelectedNodes(e).then(dragSuccess => {
|
||||
if (!dragSuccess) {
|
||||
// 拖拽没有成功触发了点击
|
||||
if (hoveredNode && hoveredNode instanceof WorkflowNodeEntity) {
|
||||
if (e.metaKey || e.shiftKey || e.ctrlKey) {
|
||||
this.selectionService.toggleSelect(hoveredNode);
|
||||
} else {
|
||||
this.selectionService.selectNode(hoveredNode);
|
||||
}
|
||||
} else {
|
||||
this.selectionService.clear();
|
||||
}
|
||||
}
|
||||
});
|
||||
// 这里会组织触发 selector box
|
||||
return true;
|
||||
} else {
|
||||
if (!hoveredNode) {
|
||||
this.selectionService.clear();
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新 hoverd
|
||||
* @param mousePos
|
||||
*/
|
||||
updateHoveredState(mousePos: IPoint, target?: HTMLElement): void {
|
||||
const nodeTransforms = this.nodeTransformsWithSort;
|
||||
// // 判断连接点是否 hover
|
||||
const portHovered = this.linesManager.getPortFromMousePos(mousePos);
|
||||
|
||||
const lineDomNodes = this.playgroundNode.querySelectorAll(LINE_CLASS_NAME);
|
||||
const checkTargetFromLine = [...lineDomNodes].some(lineDom =>
|
||||
lineDom.contains(target as HTMLElement),
|
||||
);
|
||||
// 默认 只有 output 点位可以 hover
|
||||
if (portHovered) {
|
||||
// 输出点可以直接选中
|
||||
if (portHovered.portType === 'output') {
|
||||
this.updateHoveredKey(portHovered.id);
|
||||
} else if (
|
||||
checkTargetFromLine ||
|
||||
target?.className?.includes?.(PORT_BG_CLASS_NAME)
|
||||
) {
|
||||
// 输入点采用获取最接近的线条
|
||||
const lineHovered =
|
||||
this.linesManager.getCloseInLineFromMousePos(mousePos);
|
||||
if (lineHovered) {
|
||||
this.updateHoveredKey(lineHovered.id);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Drawing 情况,不能选中节点和线条
|
||||
if (this.isDrawing) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nodeHovered = nodeTransforms.find((trans: FlowNodeTransformData) =>
|
||||
trans.bounds.contains(mousePos.x, mousePos.y),
|
||||
)?.entity as WorkflowNodeEntity;
|
||||
|
||||
// 判断当前鼠标位置所在元素是否在节点内部
|
||||
const nodeDomNodes = this.playgroundNode.querySelectorAll(NODE_CLASS_NAME);
|
||||
const checkTargetFromNode = [...nodeDomNodes].some(nodeDom =>
|
||||
nodeDom.contains(target as HTMLElement),
|
||||
);
|
||||
|
||||
if (nodeHovered || checkTargetFromNode) {
|
||||
if (nodeHovered?.id) {
|
||||
this.updateHoveredKey(nodeHovered.id);
|
||||
}
|
||||
}
|
||||
|
||||
const nodeInContainer = !!(
|
||||
nodeHovered?.parent && nodeHovered.parent.flowNodeType !== 'root'
|
||||
);
|
||||
|
||||
// 获取最接近的线条
|
||||
// 线条会相交需要获取最接近点位的线条,不能删除的线条不能被选中
|
||||
const lineHovered = checkTargetFromLine
|
||||
? this.linesManager.getCloseInLineFromMousePos(mousePos)
|
||||
: undefined;
|
||||
const lineInContainer = !!lineHovered?.inContainer;
|
||||
|
||||
// 判断容器内节点是否 hover
|
||||
if (nodeHovered && nodeInContainer) {
|
||||
this.updateHoveredKey(nodeHovered.id);
|
||||
return;
|
||||
}
|
||||
// 判断容器内线条是否 hover
|
||||
if (lineHovered && lineInContainer) {
|
||||
this.updateHoveredKey(lineHovered.id);
|
||||
return;
|
||||
}
|
||||
|
||||
// 判断节点是否 hover
|
||||
if (nodeHovered) {
|
||||
this.updateHoveredKey(nodeHovered.id);
|
||||
return;
|
||||
}
|
||||
// 判断线条是否 hover
|
||||
if (lineHovered) {
|
||||
this.hoverService.updateHoveredKey(lineHovered.id);
|
||||
return;
|
||||
}
|
||||
|
||||
// 上述逻辑都未命中 则清空 hoverd
|
||||
this.hoverService.clearHovered();
|
||||
|
||||
const currentState = this.editorStateConfig.getCurrentState();
|
||||
const isMouseFriendly =
|
||||
currentState === EditorState.STATE_MOUSE_FRIENDLY_SELECT;
|
||||
|
||||
// 鼠标优先,并且不是按住 shift 键,更新为小手
|
||||
if (isMouseFriendly && !this.editorStateConfig.isPressingShift) {
|
||||
this.configEntity.updateCursor('grab');
|
||||
}
|
||||
}
|
||||
|
||||
updateHoveredKey(key: string): void {
|
||||
// 鼠标优先交互模式,如果是 hover,需要将鼠标的小手去掉,还原鼠标原有样式
|
||||
this.configEntity.updateCursor('default');
|
||||
this.hoverService.updateHoveredKey(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否能够 hover
|
||||
* @returns 是否能 hover
|
||||
*/
|
||||
isEnabled(): boolean {
|
||||
const currentState = this.editorStateConfig.getCurrentState();
|
||||
// 选择框情况禁止 hover
|
||||
return (
|
||||
(currentState === EditorState.STATE_SELECT ||
|
||||
currentState === EditorState.STATE_MOUSE_FRIENDLY_SELECT) &&
|
||||
!this.selectorBoxConfigEntity.isStart &&
|
||||
!this.dragService.isDragging
|
||||
);
|
||||
}
|
||||
}
|
||||
20
frontend/packages/workflow/render/src/layer/index.ts
Normal file
20
frontend/packages/workflow/render/src/layer/index.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export * from './lines-layer';
|
||||
export * from './background-layer';
|
||||
export * from './hover-layer';
|
||||
export * from './shortcuts-layer';
|
||||
233
frontend/packages/workflow/render/src/layer/lines-layer.tsx
Normal file
233
frontend/packages/workflow/render/src/layer/lines-layer.tsx
Normal file
@@ -0,0 +1,233 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import ReactDOM from 'react-dom';
|
||||
import React from 'react';
|
||||
|
||||
import { inject, injectable } from 'inversify';
|
||||
import {
|
||||
Layer,
|
||||
observeEntities,
|
||||
observeEntityDatas,
|
||||
TransformData,
|
||||
} from '@flowgram-adapter/free-layout-editor';
|
||||
import {
|
||||
LineColors,
|
||||
LineType,
|
||||
WorkflowDocument,
|
||||
WorkflowHoverService,
|
||||
WorkflowLineEntity,
|
||||
WorkflowLineRenderData,
|
||||
WorkflowNodeEntity,
|
||||
WorkflowPortEntity,
|
||||
WorkflowSelectService,
|
||||
} from '@flowgram-adapter/free-layout-editor';
|
||||
import { domUtils } from '@flowgram-adapter/common';
|
||||
|
||||
import { FoldLineRender } from '../components/lines/fold-line';
|
||||
import { BezierLineRender } from '../components/lines/bezier-line';
|
||||
|
||||
const errorActiveColor = '#FF5DC8';
|
||||
|
||||
@injectable()
|
||||
export class LinesLayer extends Layer {
|
||||
static type = 'WorkflowLinesLayer';
|
||||
@inject(WorkflowHoverService) hoverService: WorkflowHoverService;
|
||||
@inject(WorkflowSelectService) selectService: WorkflowSelectService;
|
||||
// @observeEntity(FlowDocumentTransformerEntity)
|
||||
// readonly documentTransformer: FlowDocumentTransformerEntity
|
||||
|
||||
@observeEntities(WorkflowLineEntity) readonly lines: WorkflowLineEntity[];
|
||||
@observeEntities(WorkflowPortEntity) readonly ports: WorkflowPortEntity[];
|
||||
@observeEntityDatas(WorkflowNodeEntity, TransformData)
|
||||
readonly trans: TransformData[];
|
||||
@inject(WorkflowDocument) protected workflowDocument: WorkflowDocument;
|
||||
|
||||
private _frontLineEntities: WorkflowLineEntity[] = [];
|
||||
|
||||
private _backLineEntities: WorkflowLineEntity[] = [];
|
||||
private _version = 0;
|
||||
/**
|
||||
* 节点下边的线条
|
||||
*/
|
||||
protected backLines = domUtils.createDivWithClass(
|
||||
'gedit-playground-layer gedit-flow-lines-layer back',
|
||||
);
|
||||
/**
|
||||
* 节点前面的线条
|
||||
*/
|
||||
protected frontLines = domUtils.createDivWithClass(
|
||||
'gedit-playground-layer gedit-flow-lines-layer front',
|
||||
);
|
||||
|
||||
onZoom(scale: number): void {
|
||||
this.backLines.style.transform = `scale(${scale})`;
|
||||
this.frontLines.style.transform = `scale(${scale})`;
|
||||
}
|
||||
|
||||
// 用来绕过 memo
|
||||
private bumpVersion() {
|
||||
this._version = this._version + 1;
|
||||
if (this._version === Number.MAX_SAFE_INTEGER) {
|
||||
this._version = 0;
|
||||
}
|
||||
}
|
||||
|
||||
onReady() {
|
||||
this.pipelineNode.appendChild(this.backLines);
|
||||
this.pipelineNode.appendChild(this.frontLines);
|
||||
this.frontLines.style.zIndex = '20';
|
||||
this.toDispose.pushAll([
|
||||
this.selectService.onSelectionChanged(() => this.render()),
|
||||
this.hoverService.onHoveredChange(() => this.render()),
|
||||
this.workflowDocument.linesManager.onForceUpdate(() => {
|
||||
this.bumpVersion();
|
||||
this.render();
|
||||
}),
|
||||
]);
|
||||
}
|
||||
getLineColor(line: WorkflowLineEntity): string {
|
||||
// 隐藏的优先级比 hasError 高
|
||||
if (line.isHidden) {
|
||||
return line.highlightColor;
|
||||
}
|
||||
if (line.hasError) {
|
||||
if (
|
||||
(this.selectService.isSelected(line.id) ||
|
||||
this.hoverService.isHovered(line.id)) &&
|
||||
!this.config.readonly
|
||||
) {
|
||||
return errorActiveColor;
|
||||
}
|
||||
return LineColors.ERROR;
|
||||
}
|
||||
if (line.highlightColor) {
|
||||
return line.highlightColor;
|
||||
}
|
||||
if (line.drawingTo) {
|
||||
return LineColors.DRAWING;
|
||||
}
|
||||
|
||||
if (
|
||||
(this.selectService.isSelected(line.id) ||
|
||||
this.hoverService.isHovered(line.id)) &&
|
||||
!this.config.readonly
|
||||
) {
|
||||
return LineColors.HOVER;
|
||||
}
|
||||
return LineColors.DEFUALT;
|
||||
}
|
||||
renderLines(lines: WorkflowLineEntity[]) {
|
||||
const { lineType } = this.workflowDocument.linesManager;
|
||||
|
||||
// const isViewportVisible = this.config.isViewportVisible.bind(this.config);
|
||||
return (
|
||||
<>
|
||||
{lines
|
||||
.map(line => {
|
||||
const color = this.getLineColor(line);
|
||||
const selected = this.config.readonly
|
||||
? false
|
||||
: this.selectService.isSelected(line.id);
|
||||
const renderData = line.getData(WorkflowLineRenderData);
|
||||
const version = `${this._version}:${line.version}:${renderData.renderVersion}`;
|
||||
// 正在绘制中的线条使用贝塞尔曲线
|
||||
if (lineType === LineType.LINE_CHART) {
|
||||
return (
|
||||
<FoldLineRender
|
||||
key={line.id}
|
||||
color={color}
|
||||
selected={selected}
|
||||
line={line}
|
||||
version={version}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<BezierLineRender
|
||||
key={line.id}
|
||||
color={color}
|
||||
selected={selected}
|
||||
line={line}
|
||||
version={version}
|
||||
/>
|
||||
);
|
||||
})
|
||||
.filter(l => l)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
protected isFrontLine(line: WorkflowLineEntity): boolean {
|
||||
if (
|
||||
this.hoverService.isHovered(line.id) ||
|
||||
this.selectService.isSelected(line.id) ||
|
||||
line.isDrawing
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
// const { activatedNode } = this.selectService
|
||||
// // 将选中的节点的连接线放置到前面
|
||||
// if (activatedNode) {
|
||||
// const { inputLines, outputLines } = activatedNode.getData<WorkflowNodeLinesData>(WorkflowNodeLinesData)!
|
||||
// if (inputLines.includes(line) || outputLines.includes(line)) {
|
||||
// return true
|
||||
// }
|
||||
// }
|
||||
return false;
|
||||
}
|
||||
renderBackLines(): React.ReactNode {
|
||||
return ReactDOM.createPortal(
|
||||
this.renderLines(this._backLineEntities),
|
||||
this.backLines,
|
||||
);
|
||||
}
|
||||
renderFrontLines(): React.ReactNode {
|
||||
return ReactDOM.createPortal(
|
||||
this.renderLines(this._frontLineEntities),
|
||||
this.frontLines,
|
||||
);
|
||||
}
|
||||
|
||||
// onViewportChange = throttle(() => {
|
||||
// this.render();
|
||||
// }, 100);
|
||||
/**
|
||||
* 对线条进行分组
|
||||
*/
|
||||
groupLines(): void {
|
||||
this._backLineEntities = [];
|
||||
this._frontLineEntities = [];
|
||||
this.lines.forEach(line => {
|
||||
if (this.isFrontLine(line)) {
|
||||
this._frontLineEntities.push(line);
|
||||
} else {
|
||||
this._backLineEntities.push(line);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
render(): JSX.Element {
|
||||
// const isViewportVisible = this.config.isViewportVisible.bind(this.config);
|
||||
// 对线条进行分组
|
||||
this.groupLines();
|
||||
return (
|
||||
<>
|
||||
{this.renderBackLines()}
|
||||
{this.renderFrontLines()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
117
frontend/packages/workflow/render/src/layer/shortcuts-layer.tsx
Normal file
117
frontend/packages/workflow/render/src/layer/shortcuts-layer.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { inject, injectable } from 'inversify';
|
||||
import { Layer, SelectionService } from '@flowgram-adapter/free-layout-editor';
|
||||
import {
|
||||
WorkflowCommands,
|
||||
WorkflowDocument,
|
||||
WorkflowHoverService,
|
||||
WorkflowLineEntity,
|
||||
WorkflowLinesManager,
|
||||
WorkflowNodeEntity,
|
||||
type WorkflowNodeMeta,
|
||||
} from '@flowgram-adapter/free-layout-editor';
|
||||
|
||||
import { WorkflowShortcutsRegistry } from '../workflow-shorcuts-contribution';
|
||||
import { isShortcutsMatch } from '../utils/shortcuts-utils';
|
||||
|
||||
@injectable()
|
||||
export class ShortcutsLayer extends Layer<object> {
|
||||
static type = 'ShortcutsLayer';
|
||||
@inject(WorkflowShortcutsRegistry) shortcuts: WorkflowShortcutsRegistry;
|
||||
@inject(SelectionService) selection: SelectionService;
|
||||
@inject(WorkflowHoverService) hoverService: WorkflowHoverService;
|
||||
@inject(WorkflowDocument) document: WorkflowDocument;
|
||||
@inject(WorkflowLinesManager) linesManager: WorkflowLinesManager;
|
||||
onReady(): void {
|
||||
this.shortcuts.addHandlersIfNotFound(
|
||||
/**
|
||||
* 删除
|
||||
*/
|
||||
{
|
||||
commandId: WorkflowCommands.DELETE_NODES,
|
||||
shortcuts: ['backspace', 'delete'],
|
||||
isEnabled: () =>
|
||||
this.selection.selection.length > 0 &&
|
||||
!this.config.disabled &&
|
||||
!this.config.readonly,
|
||||
execute: () => {
|
||||
this.selection.selection.forEach(entity => {
|
||||
if (entity instanceof WorkflowNodeEntity) {
|
||||
if (!this.document.canRemove(entity)) {
|
||||
return;
|
||||
}
|
||||
const nodeMeta = entity.getNodeMeta<WorkflowNodeMeta>();
|
||||
const subCanvas = nodeMeta.subCanvas?.(entity);
|
||||
if (subCanvas?.isCanvas) {
|
||||
subCanvas.parentNode.dispose();
|
||||
return;
|
||||
}
|
||||
} else if (
|
||||
entity instanceof WorkflowLineEntity &&
|
||||
!this.linesManager.canRemove(entity)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
entity.dispose();
|
||||
});
|
||||
this.selection.selection = this.selection.selection.filter(
|
||||
s => !s.disposed,
|
||||
);
|
||||
},
|
||||
},
|
||||
/**
|
||||
* 放大
|
||||
*/
|
||||
{
|
||||
commandId: WorkflowCommands.ZOOM_IN,
|
||||
shortcuts: ['meta =', 'ctrl ='],
|
||||
execute: () => {
|
||||
this.config.zoomin();
|
||||
},
|
||||
},
|
||||
/**
|
||||
* 缩小
|
||||
*/
|
||||
{
|
||||
commandId: WorkflowCommands.ZOOM_OUT,
|
||||
shortcuts: ['meta -', 'ctrl -'],
|
||||
execute: () => {
|
||||
this.config.zoomout();
|
||||
},
|
||||
},
|
||||
);
|
||||
this.toDispose.pushAll([
|
||||
// 监听画布鼠标移动事件
|
||||
this.listenPlaygroundEvent('keydown', (e: KeyboardEvent) => {
|
||||
if (!this.isFocused || e.target !== this.playgroundNode) {
|
||||
return;
|
||||
}
|
||||
this.shortcuts.shortcutsHandlers.some(shortcutsHandler => {
|
||||
if (
|
||||
isShortcutsMatch(e, shortcutsHandler.shortcuts) &&
|
||||
(!shortcutsHandler.isEnabled || shortcutsHandler.isEnabled(e))
|
||||
) {
|
||||
shortcutsHandler.execute(e);
|
||||
e.preventDefault();
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { FlowNodeTransformData } from '@flowgram-adapter/free-layout-editor';
|
||||
import { type SelectionService } from '@flowgram-adapter/free-layout-editor';
|
||||
import {
|
||||
WorkflowNodeEntity,
|
||||
type WorkflowSelectService,
|
||||
} from '@flowgram-adapter/free-layout-editor';
|
||||
import { Rectangle } from '@flowgram-adapter/common';
|
||||
|
||||
const BOUNDS_PADDING = 2;
|
||||
export function getSelectionBounds(
|
||||
selectionService: SelectionService | WorkflowSelectService,
|
||||
ignoreOneSelect?: boolean, // 忽略单选
|
||||
): Rectangle {
|
||||
const selectedNodes = selectionService.selection.filter(
|
||||
node => node instanceof WorkflowNodeEntity,
|
||||
);
|
||||
|
||||
// 选中单个的时候不显示
|
||||
return selectedNodes.length > (ignoreOneSelect ? 1 : 0)
|
||||
? Rectangle.enlarge(
|
||||
selectedNodes.map(n => n.getData(FlowNodeTransformData)!.bounds),
|
||||
).pad(BOUNDS_PADDING)
|
||||
: Rectangle.EMPTY;
|
||||
}
|
||||
209
frontend/packages/workflow/render/src/utils/shortcuts-utils.ts
Normal file
209
frontend/packages/workflow/render/src/utils/shortcuts-utils.ts
Normal file
@@ -0,0 +1,209 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const isAppleDevice = /(mac|iphone|ipod|ipad)/i.test(
|
||||
typeof navigator !== 'undefined' ? navigator?.platform : '',
|
||||
);
|
||||
// 键盘事件 keyCode 别名
|
||||
const aliasKeyCodeMap = {
|
||||
'0': 48,
|
||||
'1': 49,
|
||||
'2': 50,
|
||||
'3': 51,
|
||||
'4': 52,
|
||||
'5': 53,
|
||||
'6': 54,
|
||||
'7': 55,
|
||||
'8': 56,
|
||||
'9': 57,
|
||||
backspace: 8,
|
||||
tab: 9,
|
||||
enter: 13,
|
||||
shift: 16,
|
||||
ctrl: 17,
|
||||
alt: 18,
|
||||
pausebreak: 19,
|
||||
capslock: 20,
|
||||
esc: 27,
|
||||
space: 32,
|
||||
pageup: 33,
|
||||
pagedown: 34,
|
||||
end: 35,
|
||||
home: 36,
|
||||
leftarrow: 37,
|
||||
uparrow: 38,
|
||||
rightarrow: 39,
|
||||
downarrow: 40,
|
||||
insert: 45,
|
||||
delete: 46,
|
||||
a: 65,
|
||||
b: 66,
|
||||
c: 67,
|
||||
d: 68,
|
||||
e: 69,
|
||||
f: 70,
|
||||
g: 71,
|
||||
h: 72,
|
||||
i: 73,
|
||||
j: 74,
|
||||
k: 75,
|
||||
l: 76,
|
||||
m: 77,
|
||||
n: 78,
|
||||
o: 79,
|
||||
p: 80,
|
||||
q: 81,
|
||||
r: 82,
|
||||
s: 83,
|
||||
t: 84,
|
||||
u: 85,
|
||||
v: 86,
|
||||
w: 87,
|
||||
x: 88,
|
||||
y: 89,
|
||||
z: 90,
|
||||
leftwindowkey: 91,
|
||||
rightwindowkey: 92,
|
||||
meta: isAppleDevice ? [91, 93] : [91, 92],
|
||||
selectkey: 93,
|
||||
numpad0: 96,
|
||||
numpad1: 97,
|
||||
numpad2: 98,
|
||||
numpad3: 99,
|
||||
numpad4: 100,
|
||||
numpad5: 101,
|
||||
numpad6: 102,
|
||||
numpad7: 103,
|
||||
numpad8: 104,
|
||||
numpad9: 105,
|
||||
multiply: 106,
|
||||
add: 107,
|
||||
subtract: 109,
|
||||
decimalpoint: 110,
|
||||
divide: 111,
|
||||
f1: 112,
|
||||
f2: 113,
|
||||
f3: 114,
|
||||
f4: 115,
|
||||
f5: 116,
|
||||
f6: 117,
|
||||
f7: 118,
|
||||
f8: 119,
|
||||
f9: 120,
|
||||
f10: 121,
|
||||
f11: 122,
|
||||
f12: 123,
|
||||
numlock: 144,
|
||||
scrolllock: 145,
|
||||
semicolon: 186,
|
||||
equalsign: 187,
|
||||
'=': 187,
|
||||
comma: 188,
|
||||
dash: 189,
|
||||
'-': 189,
|
||||
period: 190,
|
||||
forwardslash: 191,
|
||||
graveaccent: 192,
|
||||
openbracket: 219,
|
||||
backslash: 220,
|
||||
closebracket: 221,
|
||||
singlequote: 222,
|
||||
};
|
||||
|
||||
const modifierKey = {
|
||||
ctrl: (event: KeyboardEvent) => event.ctrlKey,
|
||||
shift: (event: KeyboardEvent) => event.shiftKey,
|
||||
alt: (event: KeyboardEvent) => event.altKey,
|
||||
meta: (event: KeyboardEvent) => {
|
||||
if (event.type === 'keyup') {
|
||||
return aliasKeyCodeMap.meta.includes(event.keyCode);
|
||||
}
|
||||
return event.metaKey;
|
||||
},
|
||||
};
|
||||
|
||||
// 根据 event 计算激活键数量
|
||||
function countKeyByEvent(event: KeyboardEvent): number {
|
||||
const countOfModifier = Object.keys(modifierKey).reduce((total, key) => {
|
||||
if (modifierKey[key](event)) {
|
||||
return total + 1;
|
||||
}
|
||||
|
||||
return total;
|
||||
}, 0);
|
||||
|
||||
// 16 17 18 91 92 是修饰键的 keyCode,如果 keyCode 是修饰键,那么激活数量就是修饰键的数量,如果不是,那么就需要 +1
|
||||
return [16, 17, 18, 91, 92].includes(event.keyCode)
|
||||
? countOfModifier
|
||||
: countOfModifier + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param event
|
||||
* @param keyString 'ctrl.s' 'meta.s'
|
||||
* @param exactMatch
|
||||
*/
|
||||
function isKeyStringMatch(
|
||||
event: KeyboardEvent,
|
||||
keyString: string,
|
||||
exactMatch = true,
|
||||
): boolean {
|
||||
// 浏览器自动补全 input 的时候,会触发 keyDown、keyUp 事件,但此时 event.key 等为空
|
||||
if (!event.key || !keyString) {
|
||||
return false;
|
||||
}
|
||||
// 字符串依次判断是否有组合键
|
||||
const genArr = keyString.split(/\s+/);
|
||||
let genLen = 0;
|
||||
|
||||
for (const key of genArr) {
|
||||
// 组合键
|
||||
const genModifier = modifierKey[key];
|
||||
// keyCode 别名
|
||||
const aliasKeyCode: number | number[] = aliasKeyCodeMap[key.toLowerCase()];
|
||||
|
||||
if (
|
||||
(genModifier && genModifier(event)) ||
|
||||
(aliasKeyCode && aliasKeyCode === event.keyCode)
|
||||
) {
|
||||
genLen++;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 需要判断触发的键位和监听的键位完全一致,判断方法就是触发的键位里有且等于监听的键位
|
||||
* genLen === genArr.length 能判断出来触发的键位里有监听的键位
|
||||
* countKeyByEvent(event) === genArr.length 判断出来触发的键位数量里有且等于监听的键位数量
|
||||
* 主要用来防止按组合键其子集也会触发的情况,例如监听 ctrl+a 会触发监听 ctrl 和 a 两个键的事件。
|
||||
*/
|
||||
if (exactMatch) {
|
||||
return genLen === genArr.length && countKeyByEvent(event) === genArr.length;
|
||||
}
|
||||
return genLen === genArr.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* 匹配指定的快捷键
|
||||
* @param event
|
||||
* @param shortcuts
|
||||
*/
|
||||
export function isShortcutsMatch(
|
||||
event: KeyboardEvent,
|
||||
shortcuts: string[],
|
||||
): boolean {
|
||||
return shortcuts.some(keyString => isKeyStringMatch(event, keyString));
|
||||
}
|
||||
52
frontend/packages/workflow/render/src/workflow-loader.tsx
Normal file
52
frontend/packages/workflow/render/src/workflow-loader.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useLayoutEffect, useMemo, useEffect } from 'react';
|
||||
|
||||
import { reportTti } from '@coze-arch/report-tti/custom-perf-metric';
|
||||
import { FlowRendererRegistry } from '@flowgram-adapter/free-layout-editor';
|
||||
import { LoggerEvent, LoggerService, useService } from '@flowgram-adapter/free-layout-editor';
|
||||
import { WorkflowDocument } from '@flowgram-adapter/free-layout-editor';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
export const WorkflowLoader: React.FC = () => {
|
||||
const doc = useService<WorkflowDocument>(WorkflowDocument);
|
||||
const renderRegistry = useService<FlowRendererRegistry>(FlowRendererRegistry);
|
||||
const loggerService = useService<LoggerService>(LoggerService);
|
||||
useMemo(() => renderRegistry.init(), [renderRegistry]);
|
||||
useLayoutEffect(() => {
|
||||
// 加载数据
|
||||
doc.load();
|
||||
// 销毁数据
|
||||
return () => doc.dispose();
|
||||
}, [doc]);
|
||||
|
||||
useEffect(() => {
|
||||
const disposable = loggerService.onLogger(({ event }) => {
|
||||
if (event === LoggerEvent.CANVAS_TTI) {
|
||||
// 上报到 coze
|
||||
reportTti();
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
disposable?.dispose();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return <div className={styles.playgroundLoad} />;
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ContainerModule } from 'inversify';
|
||||
import { FlowRendererContribution } from '@flowgram-adapter/free-layout-editor';
|
||||
import { PlaygroundContribution } from '@flowgram-adapter/free-layout-editor';
|
||||
import { bindContributions } from '@flowgram-adapter/common';
|
||||
|
||||
import { WorkflowShortcutsRegistry } from './workflow-shorcuts-contribution';
|
||||
import { WorkflowRenderContribution } from './workflow-render-contribution';
|
||||
|
||||
export const WorkflowRenderContainerModule = new ContainerModule(bind => {
|
||||
bindContributions(bind, WorkflowRenderContribution, [
|
||||
PlaygroundContribution,
|
||||
FlowRendererContribution,
|
||||
]);
|
||||
bind(WorkflowShortcutsRegistry).toSelf().inSingletonScope();
|
||||
});
|
||||
@@ -0,0 +1,149 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { inject, injectable } from 'inversify';
|
||||
import { Gesture } from '@use-gesture/vanilla';
|
||||
import {
|
||||
FlowDebugLayer,
|
||||
FlowNodesContentLayer,
|
||||
FlowNodesTransformLayer,
|
||||
FlowScrollBarLayer,
|
||||
FlowSelectorBoundsLayer,
|
||||
FlowSelectorBoxLayer,
|
||||
type FlowRendererContribution,
|
||||
type FlowRendererRegistry,
|
||||
} from '@flowgram-adapter/free-layout-editor';
|
||||
import { StackingContextManager } from '@flowgram-adapter/free-layout-editor';
|
||||
import {
|
||||
WorkflowBezierLineContribution,
|
||||
WorkflowFoldLineContribution,
|
||||
WorkflowLinesLayer,
|
||||
} from '@flowgram-adapter/free-layout-editor';
|
||||
import {
|
||||
PlaygroundLayer,
|
||||
type PlaygroundContribution,
|
||||
} from '@flowgram-adapter/free-layout-editor';
|
||||
import {
|
||||
WorkflowHoverService,
|
||||
WorkflowLinesManager,
|
||||
} from '@flowgram-adapter/free-layout-editor';
|
||||
|
||||
import { BackgroundLayer, HoverLayer, ShortcutsLayer } from './layer';
|
||||
import { SelectorBounds } from './components/selector-bounds';
|
||||
|
||||
@injectable()
|
||||
export class WorkflowRenderContribution
|
||||
implements FlowRendererContribution, PlaygroundContribution
|
||||
{
|
||||
@inject(WorkflowHoverService) protected hoverService: WorkflowHoverService;
|
||||
@inject(StackingContextManager)
|
||||
protected stackingContext: StackingContextManager;
|
||||
@inject(WorkflowLinesManager)
|
||||
protected linesManager: WorkflowLinesManager;
|
||||
|
||||
registerRenderer(registry: FlowRendererRegistry): void {
|
||||
// 画布基础层,提供缩放、手势等能力
|
||||
registry.registerLayer(PlaygroundLayer, {
|
||||
hoverService: this.hoverService,
|
||||
});
|
||||
registry.registerLayers(
|
||||
FlowNodesContentLayer,
|
||||
// FlowScrollLimitLayer, // 控制滚动范围
|
||||
FlowScrollBarLayer, // 滚动条
|
||||
HoverLayer, // 控制hover
|
||||
ShortcutsLayer, // 快捷键配置
|
||||
);
|
||||
// 线条
|
||||
registry.registerLayer(WorkflowLinesLayer, {
|
||||
renderElement: () => this.stackingContext.node,
|
||||
});
|
||||
// 节点位置
|
||||
registry.registerLayer(FlowNodesTransformLayer, {
|
||||
renderElement: () => this.stackingContext.node,
|
||||
});
|
||||
registry.registerLayer<FlowSelectorBoundsLayer>(FlowSelectorBoundsLayer, {
|
||||
disableBackground: true,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
CustomBoundsRenderer: SelectorBounds as any,
|
||||
});
|
||||
registry.registerLayer<FlowSelectorBoxLayer>(FlowSelectorBoxLayer, {
|
||||
canSelect: (event, entity) => {
|
||||
// 需满足以下条件:
|
||||
// 1. 非左键不能触发框选
|
||||
if (event.button !== 0) {
|
||||
return false;
|
||||
}
|
||||
const element = event.target as Element;
|
||||
// 2. 没有元素不能触发框选
|
||||
if (!element) {
|
||||
return false;
|
||||
}
|
||||
// 3. 如存在自定义配置,以配置为准
|
||||
if (element) {
|
||||
if (element.closest('[data-flow-editor-selectable="true"]')) {
|
||||
return true;
|
||||
}
|
||||
if (element.closest('[data-flow-editor-selectable="false"]')) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// 4. hover 到节点或者线条不能触发框选
|
||||
if (this.hoverService.isSomeHovered()) {
|
||||
return false;
|
||||
}
|
||||
// 5. 未处于画布内不能触发框选
|
||||
if (
|
||||
!element.classList.contains('gedit-playground-layer') &&
|
||||
!element.classList.contains('gedit-flow-background-layer') &&
|
||||
// 连线的空白区域
|
||||
!element.closest('.gedit-flow-activity-edge')
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
});
|
||||
// 调试画布
|
||||
if (location.search.match('playground_debug')) {
|
||||
registry.registerLayers(FlowDebugLayer);
|
||||
}
|
||||
// 背景最后插入,因为里边会调整位置
|
||||
registry.registerLayer(BackgroundLayer);
|
||||
}
|
||||
|
||||
/**
|
||||
* 这个用于阻止 document.body 的手势缩放
|
||||
* @private
|
||||
*/
|
||||
private _gestureForStopDefault = new Gesture(document.body, {
|
||||
onPinch: () => {
|
||||
// Do nothing
|
||||
},
|
||||
});
|
||||
onReady(): void {
|
||||
if (document.documentElement) {
|
||||
document.documentElement.style.overscrollBehavior = 'none';
|
||||
}
|
||||
document.body.style.overscrollBehavior = 'none';
|
||||
this.linesManager
|
||||
.registerContribution(WorkflowBezierLineContribution)
|
||||
.registerContribution(WorkflowFoldLineContribution);
|
||||
}
|
||||
|
||||
onDispose() {
|
||||
this._gestureForStopDefault.destroy();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
|
||||
import { type interfaces } from 'inversify';
|
||||
import { FlowRendererContainerModule } from '@flowgram-adapter/free-layout-editor';
|
||||
import { createNodeCorePlugin } from '@flowgram-adapter/free-layout-editor';
|
||||
import { createFreeStackPlugin } from '@flowgram-adapter/free-layout-editor';
|
||||
import { createFreeAutoLayoutPlugin } from '@flowgram-adapter/free-layout-editor';
|
||||
import { FlowDocumentContainerModule } from '@flowgram-adapter/free-layout-editor';
|
||||
import {
|
||||
PlaygroundReactProvider,
|
||||
type Plugin,
|
||||
} from '@flowgram-adapter/free-layout-editor';
|
||||
import { WorkflowDocumentContainerModule } from '@flowgram-adapter/free-layout-editor';
|
||||
|
||||
import { WorkflowRenderContainerModule } from './workflow-render-container-module';
|
||||
import { WorkflowLoader } from './workflow-loader';
|
||||
|
||||
export interface WorkflowRenderProviderProps {
|
||||
children: React.ReactElement;
|
||||
containerModules?: interfaces.ContainerModule[];
|
||||
preset?: () => Plugin[];
|
||||
parentContainer?: interfaces.Container;
|
||||
}
|
||||
|
||||
/**
|
||||
* 画布引擎渲染
|
||||
*/
|
||||
export const WorkflowRenderProvider = (props: WorkflowRenderProviderProps) => {
|
||||
const modules = useMemo(
|
||||
() => [
|
||||
FlowDocumentContainerModule, // 默认文档
|
||||
FlowRendererContainerModule, // 默认渲染
|
||||
// FlowActivitiesContainerModule, // 这是固定画布的 module,目前不需要依赖
|
||||
WorkflowDocumentContainerModule, // 扩展文档
|
||||
WorkflowRenderContainerModule, // 扩展渲染
|
||||
...(props.containerModules || []),
|
||||
],
|
||||
[],
|
||||
);
|
||||
|
||||
const preset = useCallback(
|
||||
() => [
|
||||
createFreeAutoLayoutPlugin({}),
|
||||
createFreeStackPlugin({}), // 渲染层级管理
|
||||
createNodeCorePlugin({}),
|
||||
...(props.preset?.() || []),
|
||||
],
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<PlaygroundReactProvider
|
||||
containerModules={modules}
|
||||
plugins={preset}
|
||||
parentContainer={props.parentContainer}
|
||||
>
|
||||
<WorkflowLoader />
|
||||
{props.children}
|
||||
</PlaygroundReactProvider>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
inject,
|
||||
injectable,
|
||||
multiInject,
|
||||
optional,
|
||||
postConstruct,
|
||||
} from 'inversify';
|
||||
import { CommandRegistry } from '@flowgram-adapter/free-layout-editor';
|
||||
|
||||
interface ShorcutsHandler {
|
||||
commandId: string;
|
||||
shortcuts: string[];
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
isEnabled?: (...args: any[]) => boolean;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
execute: (...args: any[]) => void;
|
||||
}
|
||||
|
||||
export const WorkflowShortcutsContribution = Symbol(
|
||||
'WorkflowShortcutsContribution',
|
||||
);
|
||||
|
||||
export interface WorkflowShortcutsContribution {
|
||||
registerShortcuts: (registry: WorkflowShortcutsRegistry) => void;
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class WorkflowShortcutsRegistry {
|
||||
@multiInject(WorkflowShortcutsContribution)
|
||||
@optional()
|
||||
protected contribs: WorkflowShortcutsContribution[];
|
||||
@inject(CommandRegistry) protected commandRegistry: CommandRegistry;
|
||||
readonly shortcutsHandlers: ShorcutsHandler[] = [];
|
||||
addHandlers(...handlers: ShorcutsHandler[]): void {
|
||||
// 注册 command
|
||||
handlers.forEach(handler => {
|
||||
this.commandRegistry.registerCommand(
|
||||
{ id: handler.commandId },
|
||||
{ execute: handler.execute, isEnabled: handler.isEnabled },
|
||||
);
|
||||
});
|
||||
this.shortcutsHandlers.push(...handlers);
|
||||
}
|
||||
addHandlersIfNotFound(...handlers: ShorcutsHandler[]): void {
|
||||
handlers.forEach(handler => {
|
||||
if (!this.has(handler.commandId)) {
|
||||
this.addHandlers(handler);
|
||||
}
|
||||
});
|
||||
}
|
||||
has(commandId: string): boolean {
|
||||
return this.shortcutsHandlers.some(
|
||||
handler => handler.commandId === commandId,
|
||||
);
|
||||
}
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
this.contribs?.forEach(contrib => contrib.registerShortcuts(this));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user