mirror of
https://gitee.com/ByteDance/flowgram.ai.git
synced 2025-07-07 17:43:29 +08:00
feat(free-demo): support create nodes group (#185)
* feat(demo): create group tool * feat(demo): create group shortcut * refactor(core): split group service, controller, utils files * feat(history): free history add group operations * feat(demo): group node render * feat(demo): group node registry * refactor(stack): remove layer computing config * feat(stack): line stackIndex cannot be recalculated by default * feat(demo): group title & color palette acess form * feat(demo): ungroup button & shortcut * feat(demo): create group & ungroup operation register to free history service * refactor(group): group shortcuts move to group-plugin * refactor(group): group node render move to group-plugin * fix(group): undo/redo of create node or ungroup not work * perf(history): free history remove async operation * feat(group): trigger select box inside group * fix(group): container inside group * fix(group): auto layout should not be affected by group node * feat(container): support multi-layer nested containers * fix(group): group css variables overwrite each other * fix(container): node move in or out group shouldn't clear lines * feat(demo): node should follow mouse after move out container button clicked * feat(container): disable group move to non-group container node * fix(container): cross-level node moving causing coord offset * feat(demo): comment node support more button * fix(demo): comment in container fromJSON * feat(container): node into container show move out tips * feat(group): node into group show move out tips * feat(group): delete group when blocks is empty * refactor(group): createFreeGroupPlugin move to container-plugin * refactor(demo): replace disablePorts with defaultPorts * fix(demo): react warning * refactor(group): group plugin built-in GroupNodeRegistry * refactor(group): create free-group-plugin * fix(ci): lock & ts-check & test errors
This commit is contained in:
parent
8c0f007127
commit
19ff04abc7
@ -36,6 +36,7 @@
|
|||||||
"@flowgram.ai/free-node-panel-plugin": "workspace:*",
|
"@flowgram.ai/free-node-panel-plugin": "workspace:*",
|
||||||
"@flowgram.ai/minimap-plugin": "workspace:*",
|
"@flowgram.ai/minimap-plugin": "workspace:*",
|
||||||
"@flowgram.ai/free-container-plugin": "workspace:*",
|
"@flowgram.ai/free-container-plugin": "workspace:*",
|
||||||
|
"@flowgram.ai/free-group-plugin": "workspace:*",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"nanoid": "^4.0.2",
|
"nanoid": "^4.0.2",
|
||||||
"react": "^18",
|
"react": "^18",
|
||||||
|
|||||||
@ -26,6 +26,8 @@ export const NodeWrapper: React.FC<NodeWrapperProps> = (props) => {
|
|||||||
const form = nodeRender.form;
|
const form = nodeRender.form;
|
||||||
const ctx = useClientContext();
|
const ctx = useClientContext();
|
||||||
|
|
||||||
|
const portsRender = ports.map((p) => <WorkflowPortRender key={p.id} entity={p} />);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<NodeWrapperStyle
|
<NodeWrapperStyle
|
||||||
@ -57,9 +59,7 @@ export const NodeWrapper: React.FC<NodeWrapperProps> = (props) => {
|
|||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</NodeWrapperStyle>
|
</NodeWrapperStyle>
|
||||||
{ports.map((p) => (
|
{portsRender}
|
||||||
<WorkflowPortRender key={p.id} entity={p} />
|
|
||||||
))}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -14,13 +14,13 @@ export const CommentContainer: FC<ICommentContainer> = (props) => {
|
|||||||
scrollbarWidth: 'thin',
|
scrollbarWidth: 'thin',
|
||||||
scrollbarColor: 'rgb(159 159 158 / 65%) transparent',
|
scrollbarColor: 'rgb(159 159 158 / 65%) transparent',
|
||||||
// 针对 WebKit 浏览器(如 Chrome、Safari)的样式
|
// 针对 WebKit 浏览器(如 Chrome、Safari)的样式
|
||||||
'&::-webkit-scrollbar': {
|
'&:WebkitScrollbar': {
|
||||||
width: '4px',
|
width: '4px',
|
||||||
},
|
},
|
||||||
'&::-webkit-scrollbar-track': {
|
'&::WebkitScrollbarTrack': {
|
||||||
background: 'transparent',
|
background: 'transparent',
|
||||||
},
|
},
|
||||||
'&::-webkit-scrollbar-thumb': {
|
'&::WebkitScrollbarThumb': {
|
||||||
backgroundColor: 'rgb(159 159 158 / 65%)',
|
backgroundColor: 'rgb(159 159 158 / 65%)',
|
||||||
borderRadius: '20px',
|
borderRadius: '20px',
|
||||||
border: '2px solid transparent',
|
border: '2px solid transparent',
|
||||||
|
|||||||
@ -68,3 +68,37 @@
|
|||||||
resize: none;
|
resize: none;
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.workflow-comment-more-button {
|
||||||
|
position: absolute;
|
||||||
|
right: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-comment-more-button > .semi-button {
|
||||||
|
color: rgba(255, 255, 255, 0);
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-comment-more-button > .semi-button:hover {
|
||||||
|
color: #ffa100;
|
||||||
|
background: #fbf2d2cc;
|
||||||
|
backdrop-filter: blur(1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-comment-more-button-focused > .semi-button:hover {
|
||||||
|
color: #ff811a;
|
||||||
|
background: #ffe3cecc;
|
||||||
|
backdrop-filter: blur(1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-comment-more-button > .semi-button:active {
|
||||||
|
color: #f2b600;
|
||||||
|
background: #ede5c7cc;
|
||||||
|
backdrop-filter: blur(1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-comment-more-button-focused > .semi-button:active {
|
||||||
|
color: #ff811a;
|
||||||
|
background: #eed5c1cc;
|
||||||
|
backdrop-filter: blur(1px);
|
||||||
|
}
|
||||||
|
|||||||
@ -0,0 +1,21 @@
|
|||||||
|
import { FC } from 'react';
|
||||||
|
|
||||||
|
import { WorkflowNodeEntity } from '@flowgram.ai/free-layout-editor';
|
||||||
|
|
||||||
|
import { NodeMenu } from '../../node-menu';
|
||||||
|
|
||||||
|
interface IMoreButton {
|
||||||
|
node: WorkflowNodeEntity;
|
||||||
|
focused: boolean;
|
||||||
|
deleteNode: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MoreButton: FC<IMoreButton> = ({ node, focused, deleteNode }) => (
|
||||||
|
<div
|
||||||
|
className={`workflow-comment-more-button ${
|
||||||
|
focused ? 'workflow-comment-more-button-focused' : ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<NodeMenu node={node} deleteNode={deleteNode} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
@ -14,6 +14,7 @@ import { useOverflow } from '../hooks/use-overflow';
|
|||||||
import { useModel } from '../hooks/use-model';
|
import { useModel } from '../hooks/use-model';
|
||||||
import { useSize } from '../hooks';
|
import { useSize } from '../hooks';
|
||||||
import { CommentEditorFormField } from '../constant';
|
import { CommentEditorFormField } from '../constant';
|
||||||
|
import { MoreButton } from './more-button';
|
||||||
import { CommentEditor } from './editor';
|
import { CommentEditor } from './editor';
|
||||||
import { ContentDragArea } from './content-drag-area';
|
import { ContentDragArea } from './content-drag-area';
|
||||||
import { CommentContainer } from './container';
|
import { CommentContainer } from './container';
|
||||||
@ -26,7 +27,7 @@ export const CommentRender: FC<{
|
|||||||
const { node } = props;
|
const { node } = props;
|
||||||
const model = useModel();
|
const model = useModel();
|
||||||
|
|
||||||
const { selected: focused, selectNode, nodeRef } = useNodeRender();
|
const { selected: focused, selectNode, nodeRef, deleteNode } = useNodeRender();
|
||||||
|
|
||||||
const formModel = node.getData(FlowNodeFormData).getFormModel<FormModelV2>();
|
const formModel = node.getData(FlowNodeFormData).getFormModel<FormModelV2>();
|
||||||
const formControl = formModel?.formControl;
|
const formControl = formModel?.formControl;
|
||||||
@ -65,6 +66,8 @@ export const CommentRender: FC<{
|
|||||||
<BlankArea model={model} />
|
<BlankArea model={model} />
|
||||||
{/* 内容拖拽区域(点击后隐藏) */}
|
{/* 内容拖拽区域(点击后隐藏) */}
|
||||||
<ContentDragArea model={model} focused={focused} overflow={overflow} />
|
<ContentDragArea model={model} focused={focused} overflow={overflow} />
|
||||||
|
{/* 更多按钮 */}
|
||||||
|
<MoreButton node={node} focused={focused} deleteNode={deleteNode} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Field>
|
</Field>
|
||||||
|
|||||||
100
apps/demo-free-layout/src/components/group/color.ts
Normal file
100
apps/demo-free-layout/src/components/group/color.ts
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
type GroupColor = {
|
||||||
|
'50': string;
|
||||||
|
'300': string;
|
||||||
|
'400': string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const defaultColor = 'Blue';
|
||||||
|
|
||||||
|
export const groupColors: Record<string, GroupColor> = {
|
||||||
|
Red: {
|
||||||
|
'50': '#fef2f2',
|
||||||
|
'300': '#fca5a5',
|
||||||
|
'400': '#f87171',
|
||||||
|
},
|
||||||
|
Orange: {
|
||||||
|
'50': '#fff7ed',
|
||||||
|
'300': '#fdba74',
|
||||||
|
'400': '#fb923c',
|
||||||
|
},
|
||||||
|
Amber: {
|
||||||
|
'50': '#fffbeb',
|
||||||
|
'300': '#fcd34d',
|
||||||
|
'400': '#fbbf24',
|
||||||
|
},
|
||||||
|
Yellow: {
|
||||||
|
'50': '#fef9c3',
|
||||||
|
'300': '#fde047',
|
||||||
|
'400': '#facc15',
|
||||||
|
},
|
||||||
|
Lime: {
|
||||||
|
'50': '#f7fee7',
|
||||||
|
'300': '#bef264',
|
||||||
|
'400': '#a3e635',
|
||||||
|
},
|
||||||
|
Green: {
|
||||||
|
'50': '#f0fdf4',
|
||||||
|
'300': '#86efac',
|
||||||
|
'400': '#4ade80',
|
||||||
|
},
|
||||||
|
Emerald: {
|
||||||
|
'50': '#ecfdf5',
|
||||||
|
'300': '#6ee7b7',
|
||||||
|
'400': '#34d399',
|
||||||
|
},
|
||||||
|
Teal: {
|
||||||
|
'50': '#f0fdfa',
|
||||||
|
'300': '#5eead4',
|
||||||
|
'400': '#2dd4bf',
|
||||||
|
},
|
||||||
|
Cyan: {
|
||||||
|
'50': '#ecfeff',
|
||||||
|
'300': '#67e8f9',
|
||||||
|
'400': '#22d3ee',
|
||||||
|
},
|
||||||
|
Sky: {
|
||||||
|
'50': '#ecfeff',
|
||||||
|
'300': '#7dd3fc',
|
||||||
|
'400': '#38bdf8',
|
||||||
|
},
|
||||||
|
Blue: {
|
||||||
|
'50': '#eff6ff',
|
||||||
|
'300': '#93c5fd',
|
||||||
|
'400': '#60a5fa',
|
||||||
|
},
|
||||||
|
Indigo: {
|
||||||
|
'50': '#eef2ff',
|
||||||
|
'300': '#a5b4fc',
|
||||||
|
'400': '#818cf8',
|
||||||
|
},
|
||||||
|
Violet: {
|
||||||
|
'50': '#f5f3ff',
|
||||||
|
'300': '#c4b5fd',
|
||||||
|
'400': '#a78bfa',
|
||||||
|
},
|
||||||
|
Purple: {
|
||||||
|
'50': '#faf5ff',
|
||||||
|
'300': '#d8b4fe',
|
||||||
|
'400': '#c084fc',
|
||||||
|
},
|
||||||
|
Fuchsia: {
|
||||||
|
'50': '#fdf4ff',
|
||||||
|
'300': '#f0abfc',
|
||||||
|
'400': '#e879f9',
|
||||||
|
},
|
||||||
|
Pink: {
|
||||||
|
'50': '#fdf2f8',
|
||||||
|
'300': '#f9a8d4',
|
||||||
|
'400': '#f472b6',
|
||||||
|
},
|
||||||
|
Rose: {
|
||||||
|
'50': '#fff1f2',
|
||||||
|
'300': '#fda4af',
|
||||||
|
'400': '#fb7185',
|
||||||
|
},
|
||||||
|
Gray: {
|
||||||
|
'50': '#f9fafb',
|
||||||
|
'300': '#d1d5db',
|
||||||
|
'400': '#9ca3af',
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -0,0 +1,49 @@
|
|||||||
|
import { CSSProperties, FC, useEffect } from 'react';
|
||||||
|
|
||||||
|
import { useWatch, WorkflowNodeEntity } from '@flowgram.ai/free-layout-editor';
|
||||||
|
|
||||||
|
import { GroupField } from '../constant';
|
||||||
|
import { defaultColor, groupColors } from '../color';
|
||||||
|
|
||||||
|
interface GroupBackgroundProps {
|
||||||
|
node: WorkflowNodeEntity;
|
||||||
|
style?: CSSProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GroupBackground: FC<GroupBackgroundProps> = ({ node, style }) => {
|
||||||
|
const colorName = useWatch<string>(GroupField.Color) ?? defaultColor;
|
||||||
|
const color = groupColors[colorName];
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const styleElement = document.createElement('style');
|
||||||
|
|
||||||
|
// 使用独特的选择器
|
||||||
|
const styleContent = `
|
||||||
|
.workflow-group-render[data-group-id="${node.id}"] .workflow-group-background {
|
||||||
|
border: 1px solid ${color['300']};
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-group-render.selected[data-group-id="${node.id}"] .workflow-group-background {
|
||||||
|
border: 1px solid ${color['400']};
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
styleElement.textContent = styleContent;
|
||||||
|
document.head.appendChild(styleElement);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
styleElement.remove();
|
||||||
|
};
|
||||||
|
}, [color]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="workflow-group-background"
|
||||||
|
data-flow-editor-selectable="true"
|
||||||
|
style={{
|
||||||
|
...style,
|
||||||
|
backgroundColor: `${color['300']}29`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,45 @@
|
|||||||
|
import { FC } from 'react';
|
||||||
|
|
||||||
|
import { Field } from '@flowgram.ai/free-layout-editor';
|
||||||
|
import { Popover, Tooltip } from '@douyinfe/semi-ui';
|
||||||
|
|
||||||
|
import { GroupField } from '../constant';
|
||||||
|
import { defaultColor, groupColors } from '../color';
|
||||||
|
|
||||||
|
export const GroupColor: FC = () => (
|
||||||
|
<Field<string> name={GroupField.Color}>
|
||||||
|
{({ field }) => {
|
||||||
|
const colorName = field.value ?? defaultColor;
|
||||||
|
return (
|
||||||
|
<Popover
|
||||||
|
position="top"
|
||||||
|
mouseLeaveDelay={300}
|
||||||
|
content={
|
||||||
|
<div className="workflow-group-color-palette">
|
||||||
|
{Object.entries(groupColors).map(([name, color]) => (
|
||||||
|
<Tooltip content={name} key={name} mouseEnterDelay={300}>
|
||||||
|
<span
|
||||||
|
className="workflow-group-color-item"
|
||||||
|
key={name}
|
||||||
|
style={{
|
||||||
|
backgroundColor: color['300'],
|
||||||
|
borderColor: name === colorName ? color['400'] : '#fff',
|
||||||
|
}}
|
||||||
|
onClick={() => field.onChange(name)}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="workflow-group-color"
|
||||||
|
style={{
|
||||||
|
backgroundColor: groupColors[colorName]['300'],
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Field>
|
||||||
|
);
|
||||||
@ -0,0 +1,40 @@
|
|||||||
|
import type { FC, ReactNode, MouseEvent, CSSProperties } from 'react';
|
||||||
|
|
||||||
|
import { useWatch } from '@flowgram.ai/free-layout-editor';
|
||||||
|
|
||||||
|
import { GroupField } from '../constant';
|
||||||
|
import { defaultColor, groupColors } from '../color';
|
||||||
|
|
||||||
|
interface GroupHeaderProps {
|
||||||
|
onMouseDown: (e: MouseEvent) => void;
|
||||||
|
onFocus: () => void;
|
||||||
|
onBlur: () => void;
|
||||||
|
children: ReactNode;
|
||||||
|
style?: CSSProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GroupHeader: FC<GroupHeaderProps> = ({
|
||||||
|
onMouseDown,
|
||||||
|
onFocus,
|
||||||
|
onBlur,
|
||||||
|
children,
|
||||||
|
style,
|
||||||
|
}) => {
|
||||||
|
const colorName = useWatch<string>(GroupField.Color) ?? defaultColor;
|
||||||
|
const color = groupColors[colorName];
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="workflow-group-header"
|
||||||
|
onMouseDown={onMouseDown}
|
||||||
|
onFocus={onFocus}
|
||||||
|
onBlur={onBlur}
|
||||||
|
style={{
|
||||||
|
...style,
|
||||||
|
backgroundColor: color['50'],
|
||||||
|
borderColor: color['300'],
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,47 @@
|
|||||||
|
import { FC } from 'react';
|
||||||
|
|
||||||
|
interface IconGroupProps {
|
||||||
|
size?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const IconGroup: FC<IconGroupProps> = ({ size }) => (
|
||||||
|
<svg
|
||||||
|
width="10"
|
||||||
|
height="10"
|
||||||
|
viewBox="0 0 10 10"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
style={{
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
id="group"
|
||||||
|
fill="currentColor"
|
||||||
|
fillRule="evenodd"
|
||||||
|
stroke="none"
|
||||||
|
d="M 0.009766 10 L 0.009766 9.990234 L 0 9.990234 L 0 7.5 L 1 7.5 L 1 9 L 2.5 9 L 2.5 10 L 0.009766 10 Z M 3.710938 10 L 3.710938 9 L 6.199219 9 L 6.199219 10 L 3.710938 10 Z M 7.5 10 L 7.5 9 L 9 9 L 9 7.5 L 10 7.5 L 10 9.990234 L 9.990234 9.990234 L 9.990234 10 L 7.5 10 Z M 0 6.289063 L 0 3.800781 L 1 3.800781 L 1 6.289063 L 0 6.289063 Z M 9 6.289063 L 9 3.800781 L 10 3.800781 L 10 6.289063 L 9 6.289063 Z M 0 2.5 L 0 0.009766 L 0.009766 0.009766 L 0.009766 0 L 2.5 0 L 2.5 1 L 1 1 L 1 2.5 L 0 2.5 Z M 9 2.5 L 9 1 L 7.5 1 L 7.5 0 L 9.990234 0 L 9.990234 0.009766 L 10 0.009766 L 10 2.5 L 9 2.5 Z M 3.710938 1 L 3.710938 0 L 6.199219 0 L 6.199219 1 L 3.710938 1 Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const IconUngroup: FC<IconGroupProps> = ({ size }) => (
|
||||||
|
<svg
|
||||||
|
width="10"
|
||||||
|
height="10"
|
||||||
|
viewBox="0 0 10 10"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
style={{
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
id="ungroup"
|
||||||
|
fill="currentColor"
|
||||||
|
fillRule="evenodd"
|
||||||
|
stroke="none"
|
||||||
|
d="M 9.654297 10.345703 L 8.808594 9.5 L 7.175781 9.5 L 7.175781 8.609375 L 7.917969 8.609375 L 1.390625 2.082031 L 1.390625 2.824219 L 0.5 2.824219 L 0.5 1.191406 L -0.345703 0.345703 L 0.283203 -0.283203 L 1.166016 0.599609 L 2.724609 0.599609 L 2.724609 1.490234 L 2.056641 1.490234 L 8.509766 7.943359 L 8.509766 7.275391 L 9.400391 7.275391 L 9.400391 8.833984 L 10.283203 9.716797 L 9.654297 10.345703 Z M 0.509766 9.5 L 0.509766 9.490234 L 0.5 9.490234 L 0.5 7.275391 L 1.390625 7.275391 L 1.390625 8.609375 L 2.724609 8.609375 L 2.724609 9.5 L 0.509766 9.5 Z M 3.802734 9.5 L 3.802734 8.609375 L 6.017578 8.609375 L 6.017578 9.5 L 3.802734 9.5 Z M 0.5 6.197266 L 0.5 3.982422 L 1.390625 3.982422 L 1.390625 6.197266 L 0.5 6.197266 Z M 8.509766 6.197266 L 8.509766 3.982422 L 9.400391 3.982422 L 9.400391 6.197266 L 8.509766 6.197266 Z M 8.509766 2.824219 L 8.509766 1.490234 L 7.175781 1.490234 L 7.175781 0.599609 L 9.390625 0.599609 L 9.390625 0.609375 L 9.400391 0.609375 L 9.400391 2.824219 L 8.509766 2.824219 Z M 3.802734 1.490234 L 3.802734 0.599609 L 6.017578 0.599609 L 6.017578 1.490234 L 3.802734 1.490234 Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
@ -0,0 +1,2 @@
|
|||||||
|
export { GroupNodeRender } from './node-render';
|
||||||
|
export { IconGroup } from './icon-group';
|
||||||
@ -0,0 +1,66 @@
|
|||||||
|
import {
|
||||||
|
FlowNodeFormData,
|
||||||
|
Form,
|
||||||
|
FormModelV2,
|
||||||
|
useNodeRender,
|
||||||
|
} from '@flowgram.ai/free-layout-editor';
|
||||||
|
import { useNodeSize } from '@flowgram.ai/free-container-plugin';
|
||||||
|
|
||||||
|
import { HEADER_HEIGHT, HEADER_PADDING } from '../constant';
|
||||||
|
import { UngroupButton } from './ungroup';
|
||||||
|
import { GroupTools } from './tools';
|
||||||
|
import { GroupTips } from './tips';
|
||||||
|
import { GroupHeader } from './header';
|
||||||
|
import { GroupBackground } from './background';
|
||||||
|
|
||||||
|
export const GroupNodeRender = () => {
|
||||||
|
const { node, selected, selectNode, nodeRef, startDrag, onFocus, onBlur } = useNodeRender();
|
||||||
|
const nodeSize = useNodeSize();
|
||||||
|
const formModel = node.getData(FlowNodeFormData).getFormModel<FormModelV2>();
|
||||||
|
const formControl = formModel?.formControl;
|
||||||
|
|
||||||
|
const { height, width } = nodeSize ?? {};
|
||||||
|
const nodeHeight = height ?? 0;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`workflow-group-render ${selected ? 'selected' : ''}`}
|
||||||
|
ref={nodeRef}
|
||||||
|
data-group-id={node.id}
|
||||||
|
data-node-selected={String(selected)}
|
||||||
|
onMouseDown={selectNode}
|
||||||
|
onClick={(e) => {
|
||||||
|
selectNode(e);
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Form control={formControl}>
|
||||||
|
<>
|
||||||
|
<GroupHeader
|
||||||
|
onMouseDown={(e) => {
|
||||||
|
startDrag(e);
|
||||||
|
}}
|
||||||
|
onFocus={onFocus}
|
||||||
|
onBlur={onBlur}
|
||||||
|
style={{
|
||||||
|
height: HEADER_HEIGHT,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<GroupTools />
|
||||||
|
</GroupHeader>
|
||||||
|
<GroupTips />
|
||||||
|
<UngroupButton node={node} />
|
||||||
|
<GroupBackground
|
||||||
|
node={node}
|
||||||
|
style={{
|
||||||
|
top: HEADER_HEIGHT + HEADER_PADDING,
|
||||||
|
height: nodeHeight - HEADER_HEIGHT - HEADER_PADDING,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,33 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/naming-convention -- no need */
|
||||||
|
|
||||||
|
const STORAGE_KEY = 'workflow-move-into-group-tip-visible';
|
||||||
|
const STORAGE_VALUE = 'false';
|
||||||
|
|
||||||
|
export class TipsGlobalStore {
|
||||||
|
private static _instance?: TipsGlobalStore;
|
||||||
|
|
||||||
|
public static get instance(): TipsGlobalStore {
|
||||||
|
if (!this._instance) {
|
||||||
|
this._instance = new TipsGlobalStore();
|
||||||
|
}
|
||||||
|
return this._instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
private closed = false;
|
||||||
|
|
||||||
|
public isClosed(): boolean {
|
||||||
|
return this.isCloseForever() || this.closed;
|
||||||
|
}
|
||||||
|
|
||||||
|
public close(): void {
|
||||||
|
this.closed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public isCloseForever(): boolean {
|
||||||
|
return localStorage.getItem(STORAGE_KEY) === STORAGE_VALUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public closeForever(): void {
|
||||||
|
localStorage.setItem(STORAGE_KEY, STORAGE_VALUE);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
export const IconClose = () => (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none" viewBox="0 0 16 16">
|
||||||
|
<path
|
||||||
|
fill="#060709"
|
||||||
|
fillOpacity="0.5"
|
||||||
|
d="M12.13 12.128a.5.5 0 0 0 .001-.706L8.71 8l3.422-3.423a.5.5 0 0 0-.001-.705.5.5 0 0 0-.706-.002L8.002 7.293 4.579 3.87a.5.5 0 0 0-.705.002.5.5 0 0 0-.002.705L7.295 8l-3.423 3.422a.5.5 0 0 0 .002.706c.195.195.51.197.705.001l3.423-3.422 3.422 3.422c.196.196.51.194.706-.001"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
@ -0,0 +1,36 @@
|
|||||||
|
import { useControlTips } from './use-control';
|
||||||
|
import { GroupTipsStyle } from './style';
|
||||||
|
import { isMacOS } from './is-mac-os';
|
||||||
|
import { IconClose } from './icon-close';
|
||||||
|
|
||||||
|
export const GroupTips = () => {
|
||||||
|
const { visible, close, closeForever } = useControlTips();
|
||||||
|
|
||||||
|
if (!visible) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<GroupTipsStyle className={'workflow-group-tips'}>
|
||||||
|
<div className="container">
|
||||||
|
<div className="content">
|
||||||
|
<p className="text">{`Hold ${isMacOS ? 'Cmd ⌘' : 'Ctrl'} to drag node out`}</p>
|
||||||
|
<div
|
||||||
|
className="space"
|
||||||
|
style={{
|
||||||
|
width: 0,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="actions">
|
||||||
|
<p className="close-forever" onClick={closeForever}>
|
||||||
|
Never Remind
|
||||||
|
</p>
|
||||||
|
<div className="close" onClick={close}>
|
||||||
|
<IconClose />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</GroupTipsStyle>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export const isMacOS = /(Macintosh|MacIntel|MacPPC|Mac68K|iPad)/.test(navigator.userAgent);
|
||||||
@ -0,0 +1,74 @@
|
|||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
export const GroupTipsStyle = styled.div`
|
||||||
|
position: absolute;
|
||||||
|
top: 35px;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
height: 28px;
|
||||||
|
white-space: nowrap;
|
||||||
|
pointer-events: auto;
|
||||||
|
|
||||||
|
.container {
|
||||||
|
display: inline-flex;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
background-color: rgb(255 255 255);
|
||||||
|
border-radius: 8px 8px 0 0;
|
||||||
|
|
||||||
|
.content {
|
||||||
|
overflow: hidden;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
|
||||||
|
width: fit-content;
|
||||||
|
height: 100%;
|
||||||
|
padding: 0 12px;
|
||||||
|
|
||||||
|
.text {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 400;
|
||||||
|
font-style: normal;
|
||||||
|
line-height: 20px;
|
||||||
|
color: rgba(15, 21, 40, 82%);
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.space {
|
||||||
|
width: 128px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
height: 28px;
|
||||||
|
padding: 0 12px;
|
||||||
|
|
||||||
|
.close-forever {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
padding: 0 3px;
|
||||||
|
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 400;
|
||||||
|
font-style: normal;
|
||||||
|
line-height: 12px;
|
||||||
|
color: rgba(32, 41, 69, 62%);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close {
|
||||||
|
display: flex;
|
||||||
|
cursor: pointer;
|
||||||
|
height: 100%;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
@ -0,0 +1,66 @@
|
|||||||
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import { useCurrentEntity, useService } from '@flowgram.ai/free-layout-editor';
|
||||||
|
import {
|
||||||
|
NodeIntoContainerService,
|
||||||
|
NodeIntoContainerType,
|
||||||
|
} from '@flowgram.ai/free-container-plugin';
|
||||||
|
|
||||||
|
import { TipsGlobalStore } from './global-store';
|
||||||
|
|
||||||
|
export const useControlTips = () => {
|
||||||
|
const node = useCurrentEntity();
|
||||||
|
const [visible, setVisible] = useState(false);
|
||||||
|
const globalStore = TipsGlobalStore.instance;
|
||||||
|
|
||||||
|
const nodeIntoContainerService = useService<NodeIntoContainerService>(NodeIntoContainerService);
|
||||||
|
|
||||||
|
const show = useCallback(() => {
|
||||||
|
if (globalStore.isClosed()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setVisible(true);
|
||||||
|
}, [globalStore]);
|
||||||
|
|
||||||
|
const close = useCallback(() => {
|
||||||
|
globalStore.close();
|
||||||
|
setVisible(false);
|
||||||
|
}, [globalStore]);
|
||||||
|
|
||||||
|
const closeForever = useCallback(() => {
|
||||||
|
globalStore.closeForever();
|
||||||
|
close();
|
||||||
|
}, [close, globalStore]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// 监听移入
|
||||||
|
const inDisposer = nodeIntoContainerService.on((e) => {
|
||||||
|
if (e.type !== NodeIntoContainerType.In) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (e.targetContainer === node) {
|
||||||
|
show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// 监听移出事件
|
||||||
|
const outDisposer = nodeIntoContainerService.on((e) => {
|
||||||
|
if (e.type !== NodeIntoContainerType.Out) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (e.sourceContainer === node && !node.blocks.length) {
|
||||||
|
setVisible(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
inDisposer.dispose();
|
||||||
|
outDisposer.dispose();
|
||||||
|
};
|
||||||
|
}, [nodeIntoContainerService, node, show, close, visible]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
visible,
|
||||||
|
close,
|
||||||
|
closeForever,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -0,0 +1,33 @@
|
|||||||
|
import { FC, useState } from 'react';
|
||||||
|
|
||||||
|
import { Field } from '@flowgram.ai/free-layout-editor';
|
||||||
|
import { Input } from '@douyinfe/semi-ui';
|
||||||
|
|
||||||
|
import { GroupField } from '../constant';
|
||||||
|
|
||||||
|
export const GroupTitle: FC = () => {
|
||||||
|
const [inputting, setInputting] = useState(false);
|
||||||
|
return (
|
||||||
|
<Field<string> name={GroupField.Title}>
|
||||||
|
{({ field }) =>
|
||||||
|
inputting ? (
|
||||||
|
<Input
|
||||||
|
autoFocus
|
||||||
|
className="workflow-group-title-input"
|
||||||
|
size="small"
|
||||||
|
value={field.value}
|
||||||
|
onChange={field.onChange}
|
||||||
|
onMouseDown={(e) => e.stopPropagation()}
|
||||||
|
onBlur={() => setInputting(false)}
|
||||||
|
draggable={false}
|
||||||
|
onEnterPress={() => setInputting(false)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<p className="workflow-group-title" onDoubleClick={() => setInputting(true)}>
|
||||||
|
{field.value ?? 'Group'}
|
||||||
|
</p>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</Field>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
import { FC } from 'react';
|
||||||
|
|
||||||
|
import { IconHandle } from '@douyinfe/semi-icons';
|
||||||
|
|
||||||
|
import { GroupTitle } from './title';
|
||||||
|
import { GroupColor } from './color';
|
||||||
|
|
||||||
|
export const GroupTools: FC = () => (
|
||||||
|
<div className="workflow-group-tools">
|
||||||
|
<IconHandle className="workflow-group-tools-drag" />
|
||||||
|
<GroupTitle />
|
||||||
|
<GroupColor />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
@ -0,0 +1,31 @@
|
|||||||
|
import { CSSProperties, FC } from 'react';
|
||||||
|
|
||||||
|
import { CommandRegistry, useService, WorkflowNodeEntity } from '@flowgram.ai/free-layout-editor';
|
||||||
|
import { WorkflowGroupCommand } from '@flowgram.ai/free-group-plugin';
|
||||||
|
import { Button, Tooltip } from '@douyinfe/semi-ui';
|
||||||
|
|
||||||
|
import { IconUngroup } from './icon-group';
|
||||||
|
|
||||||
|
interface UngroupButtonProps {
|
||||||
|
node: WorkflowNodeEntity;
|
||||||
|
style?: CSSProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const UngroupButton: FC<UngroupButtonProps> = ({ node, style }) => {
|
||||||
|
const commandRegistry = useService(CommandRegistry);
|
||||||
|
return (
|
||||||
|
<Tooltip content="Ungroup">
|
||||||
|
<div className="workflow-group-ungroup" style={style}>
|
||||||
|
<Button
|
||||||
|
icon={<IconUngroup size={14} />}
|
||||||
|
style={{ height: 30, width: 30 }}
|
||||||
|
theme="borderless"
|
||||||
|
type="tertiary"
|
||||||
|
onClick={() => {
|
||||||
|
commandRegistry.executeCommand(WorkflowGroupCommand.Ungroup, node);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
};
|
||||||
7
apps/demo-free-layout/src/components/group/constant.ts
Normal file
7
apps/demo-free-layout/src/components/group/constant.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export const HEADER_HEIGHT = 30;
|
||||||
|
export const HEADER_PADDING = 5;
|
||||||
|
|
||||||
|
export enum GroupField {
|
||||||
|
Title = 'title',
|
||||||
|
Color = 'color',
|
||||||
|
}
|
||||||
109
apps/demo-free-layout/src/components/group/index.css
Normal file
109
apps/demo-free-layout/src/components/group/index.css
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
.workflow-group-render {
|
||||||
|
border-radius: 8px;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-group-header {
|
||||||
|
height: 30px;
|
||||||
|
width: fit-content;
|
||||||
|
background-color: #fefce8;
|
||||||
|
border: 1px solid #facc15;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding-right: 8px;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-group-ungroup {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 30px;
|
||||||
|
width: 30px;
|
||||||
|
position: absolute;
|
||||||
|
top: 35px;
|
||||||
|
right: 0;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-group-ungroup .semi-button {
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-group-ungroup:hover .semi-button {
|
||||||
|
color: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-group-background {
|
||||||
|
position: absolute;
|
||||||
|
pointer-events: none;
|
||||||
|
top: 0;
|
||||||
|
background-color: #fddf4729;
|
||||||
|
border: 1px solid #fde047;
|
||||||
|
border-radius: 8px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-group-render.selected .workflow-group-background {
|
||||||
|
border: 1px solid #facc15;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-group-tools {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
height: 100%;
|
||||||
|
cursor: move;
|
||||||
|
color: oklch(44.6% 0.043 257.281);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.workflow-group-title {
|
||||||
|
margin: 0;
|
||||||
|
max-width: 242px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-group-tools-drag {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding-left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-group-color {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: #fde047;
|
||||||
|
margin-left: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-group-title-input {
|
||||||
|
width: 242px;
|
||||||
|
border: none;
|
||||||
|
color: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-group-color-palette {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(6, 24px);
|
||||||
|
gap: 12px;
|
||||||
|
margin: 8px;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-group-color-item {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: #fde047;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 3px solid;
|
||||||
|
}
|
||||||
4
apps/demo-free-layout/src/components/group/index.ts
Normal file
4
apps/demo-free-layout/src/components/group/index.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import './index.css';
|
||||||
|
|
||||||
|
export { GroupNodeRender } from './components';
|
||||||
|
export { IconGroup } from './components';
|
||||||
@ -2,3 +2,4 @@ export * from './base-node';
|
|||||||
export * from './line-add-button';
|
export * from './line-add-button';
|
||||||
export * from './node-panel';
|
export * from './node-panel';
|
||||||
export * from './comment';
|
export * from './comment';
|
||||||
|
export * from './group';
|
||||||
|
|||||||
113
apps/demo-free-layout/src/components/node-menu/index.tsx
Normal file
113
apps/demo-free-layout/src/components/node-menu/index.tsx
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
import { FC, useCallback, useState, type MouseEvent } from 'react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
delay,
|
||||||
|
useClientContext,
|
||||||
|
useService,
|
||||||
|
WorkflowDragService,
|
||||||
|
WorkflowNodeEntity,
|
||||||
|
WorkflowSelectService,
|
||||||
|
} from '@flowgram.ai/free-layout-editor';
|
||||||
|
import { NodeIntoContainerService } from '@flowgram.ai/free-container-plugin';
|
||||||
|
import { IconButton, Dropdown } from '@douyinfe/semi-ui';
|
||||||
|
import { IconMore } from '@douyinfe/semi-icons';
|
||||||
|
|
||||||
|
import { FlowNodeRegistry } from '../../typings';
|
||||||
|
import { PasteShortcut } from '../../shortcuts/paste';
|
||||||
|
import { CopyShortcut } from '../../shortcuts/copy';
|
||||||
|
|
||||||
|
interface NodeMenuProps {
|
||||||
|
node: WorkflowNodeEntity;
|
||||||
|
deleteNode: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NodeMenu: FC<NodeMenuProps> = ({ node, deleteNode }) => {
|
||||||
|
const [visible, setVisible] = useState(true);
|
||||||
|
const clientContext = useClientContext();
|
||||||
|
const registry = node.getNodeRegistry<FlowNodeRegistry>();
|
||||||
|
const nodeIntoContainerService = useService(NodeIntoContainerService);
|
||||||
|
const selectService = useService(WorkflowSelectService);
|
||||||
|
const dragService = useService(WorkflowDragService);
|
||||||
|
const canMoveOut = nodeIntoContainerService.canMoveOutContainer(node);
|
||||||
|
|
||||||
|
const rerenderMenu = useCallback(() => {
|
||||||
|
// force destroy component - 强制销毁组件触发重新渲染
|
||||||
|
setVisible(false);
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
setVisible(true);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleMoveOut = useCallback(
|
||||||
|
async (e: MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const sourceParent = node.parent;
|
||||||
|
// move out of container - 移出容器
|
||||||
|
nodeIntoContainerService.moveOutContainer({ node });
|
||||||
|
// clear invalid lines - 清除非法线条
|
||||||
|
await nodeIntoContainerService.clearInvalidLines({
|
||||||
|
dragNode: node,
|
||||||
|
sourceParent,
|
||||||
|
});
|
||||||
|
rerenderMenu();
|
||||||
|
await delay(16);
|
||||||
|
// select node - 选中节点
|
||||||
|
selectService.selectNode(node);
|
||||||
|
// start drag node - 开始拖拽
|
||||||
|
dragService.startDragSelectedNodes(e);
|
||||||
|
},
|
||||||
|
[nodeIntoContainerService, node, rerenderMenu]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleCopy = useCallback(
|
||||||
|
(e: React.MouseEvent) => {
|
||||||
|
const copyShortcut = new CopyShortcut(clientContext);
|
||||||
|
const pasteShortcut = new PasteShortcut(clientContext);
|
||||||
|
const data = copyShortcut.toClipboardData([node]);
|
||||||
|
pasteShortcut.apply(data);
|
||||||
|
e.stopPropagation(); // Disable clicking prevents the sidebar from opening
|
||||||
|
},
|
||||||
|
[clientContext, node]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleDelete = useCallback(
|
||||||
|
(e: React.MouseEvent) => {
|
||||||
|
deleteNode();
|
||||||
|
e.stopPropagation(); // Disable clicking prevents the sidebar from opening
|
||||||
|
},
|
||||||
|
[clientContext, node]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!visible) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dropdown
|
||||||
|
trigger="hover"
|
||||||
|
position="bottomRight"
|
||||||
|
render={
|
||||||
|
<Dropdown.Menu>
|
||||||
|
{canMoveOut && <Dropdown.Item onClick={handleMoveOut}>Move out</Dropdown.Item>}
|
||||||
|
<Dropdown.Item onClick={handleCopy} disabled={registry.meta!.copyDisable === true}>
|
||||||
|
Create Copy
|
||||||
|
</Dropdown.Item>
|
||||||
|
<Dropdown.Item
|
||||||
|
onClick={handleDelete}
|
||||||
|
disabled={!!(registry.canDelete?.(clientContext, node) || registry.meta!.deleteDisable)}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</Dropdown.Item>
|
||||||
|
</Dropdown.Menu>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<IconButton
|
||||||
|
color="secondary"
|
||||||
|
size="small"
|
||||||
|
theme="borderless"
|
||||||
|
icon={<IconMore />}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
/>
|
||||||
|
</Dropdown>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -1,9 +1,11 @@
|
|||||||
import { FunctionComponent } from 'react';
|
import { FunctionComponent } from 'react';
|
||||||
|
|
||||||
import { SelectorBoxPopoverProps } from '@flowgram.ai/free-layout-editor';
|
import { SelectorBoxPopoverProps } from '@flowgram.ai/free-layout-editor';
|
||||||
|
import { WorkflowGroupCommand } from '@flowgram.ai/free-group-plugin';
|
||||||
import { Button, ButtonGroup, Tooltip } from '@douyinfe/semi-ui';
|
import { Button, ButtonGroup, Tooltip } from '@douyinfe/semi-ui';
|
||||||
import { IconCopy, IconDeleteStroked, IconExpand, IconShrink } from '@douyinfe/semi-icons';
|
import { IconCopy, IconDeleteStroked, IconExpand, IconShrink } from '@douyinfe/semi-icons';
|
||||||
|
|
||||||
|
import { IconGroup } from '../group';
|
||||||
import { FlowCommandId } from '../../shortcuts/constants';
|
import { FlowCommandId } from '../../shortcuts/constants';
|
||||||
|
|
||||||
const BUTTON_HEIGHT = 24;
|
const BUTTON_HEIGHT = 24;
|
||||||
@ -54,6 +56,18 @@ export const SelectorBoxPopover: FunctionComponent<SelectorBoxPopoverProps> = ({
|
|||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
|
<Tooltip content={'Create Group'}>
|
||||||
|
<Button
|
||||||
|
icon={<IconGroup size={14} />}
|
||||||
|
style={{ height: BUTTON_HEIGHT }}
|
||||||
|
type="primary"
|
||||||
|
theme="solid"
|
||||||
|
onClick={() => {
|
||||||
|
commandRegistry.executeCommand(WorkflowGroupCommand.Group);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip content={'Copy'}>
|
<Tooltip content={'Copy'}>
|
||||||
<Button
|
<Button
|
||||||
icon={<IconCopy />}
|
icon={<IconCopy />}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useContext, useEffect } from 'react';
|
import { useCallback, useContext, useEffect, useMemo } from 'react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
PlaygroundEntityContext,
|
PlaygroundEntityContext,
|
||||||
@ -7,6 +7,7 @@ import {
|
|||||||
} from '@flowgram.ai/free-layout-editor';
|
} from '@flowgram.ai/free-layout-editor';
|
||||||
import { SideSheet } from '@douyinfe/semi-ui';
|
import { SideSheet } from '@douyinfe/semi-ui';
|
||||||
|
|
||||||
|
import { FlowNodeMeta } from '../../typings';
|
||||||
import { SidebarContext, IsSidebarContext, NodeRenderContext } from '../../context';
|
import { SidebarContext, IsSidebarContext, NodeRenderContext } from '../../context';
|
||||||
|
|
||||||
export const SidebarRenderer = () => {
|
export const SidebarRenderer = () => {
|
||||||
@ -53,6 +54,14 @@ export const SidebarRenderer = () => {
|
|||||||
return () => {};
|
return () => {};
|
||||||
}, [nodeRender]);
|
}, [nodeRender]);
|
||||||
|
|
||||||
|
const visible = useMemo(() => {
|
||||||
|
if (!nodeRender) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const { disableSideBar = false } = nodeRender.node.getNodeMeta<FlowNodeMeta>();
|
||||||
|
return !disableSideBar;
|
||||||
|
}, [nodeRender]);
|
||||||
|
|
||||||
if (playground.config.readonly) {
|
if (playground.config.readonly) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -68,7 +77,7 @@ export const SidebarRenderer = () => {
|
|||||||
) : null;
|
) : null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SideSheet mask={false} visible={!!nodeRender} onCancel={handleClose}>
|
<SideSheet mask={false} visible={visible} onCancel={handleClose}>
|
||||||
<IsSidebarContext.Provider value={true}>{content}</IsSidebarContext.Provider>
|
<IsSidebarContext.Provider value={true}>{content}</IsSidebarContext.Provider>
|
||||||
</SideSheet>
|
</SideSheet>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,99 +1,17 @@
|
|||||||
import { useCallback, useState, type MouseEvent } from 'react';
|
import { Field, FieldRenderProps } from '@flowgram.ai/free-layout-editor';
|
||||||
|
import { Typography, Button } from '@douyinfe/semi-ui';
|
||||||
import {
|
import { IconSmallTriangleDown, IconSmallTriangleLeft } from '@douyinfe/semi-icons';
|
||||||
Field,
|
|
||||||
FieldRenderProps,
|
|
||||||
useClientContext,
|
|
||||||
useService,
|
|
||||||
} from '@flowgram.ai/free-layout-editor';
|
|
||||||
import { NodeIntoContainerService } from '@flowgram.ai/free-container-plugin';
|
|
||||||
import { IconButton, Dropdown, Typography, Button } from '@douyinfe/semi-ui';
|
|
||||||
import { IconMore, IconSmallTriangleDown, IconSmallTriangleLeft } from '@douyinfe/semi-icons';
|
|
||||||
|
|
||||||
import { Feedback } from '../feedback';
|
import { Feedback } from '../feedback';
|
||||||
import { FlowNodeRegistry } from '../../typings';
|
|
||||||
import { PasteShortcut } from '../../shortcuts/paste';
|
|
||||||
import { CopyShortcut } from '../../shortcuts/copy';
|
|
||||||
import { useIsSidebar, useNodeRenderContext } from '../../hooks';
|
import { useIsSidebar, useNodeRenderContext } from '../../hooks';
|
||||||
|
import { NodeMenu } from '../../components/node-menu';
|
||||||
import { getIcon } from './utils';
|
import { getIcon } from './utils';
|
||||||
import { Header, Operators, Title } from './styles';
|
import { Header, Operators, Title } from './styles';
|
||||||
|
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
|
|
||||||
function DropdownContent() {
|
|
||||||
const [key, setKey] = useState(0);
|
|
||||||
const { node, deleteNode } = useNodeRenderContext();
|
|
||||||
const clientContext = useClientContext();
|
|
||||||
const registry = node.getNodeRegistry<FlowNodeRegistry>();
|
|
||||||
const nodeIntoContainerService = useService<NodeIntoContainerService>(NodeIntoContainerService);
|
|
||||||
const canMoveOut = nodeIntoContainerService.canMoveOutContainer(node);
|
|
||||||
|
|
||||||
const rerenderMenu = useCallback(() => {
|
|
||||||
setKey((prevKey) => prevKey + 1);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleMoveOut = useCallback(
|
|
||||||
(e: MouseEvent) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
nodeIntoContainerService.moveOutContainer({ node });
|
|
||||||
nodeIntoContainerService.removeNodeLines(node);
|
|
||||||
requestAnimationFrame(rerenderMenu);
|
|
||||||
},
|
|
||||||
[nodeIntoContainerService, node, rerenderMenu]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleCopy = useCallback(
|
|
||||||
(e: React.MouseEvent) => {
|
|
||||||
const copyShortcut = new CopyShortcut(clientContext);
|
|
||||||
const pasteShortcut = new PasteShortcut(clientContext);
|
|
||||||
const data = copyShortcut.toClipboardData([node]);
|
|
||||||
pasteShortcut.apply(data);
|
|
||||||
e.stopPropagation(); // Disable clicking prevents the sidebar from opening
|
|
||||||
},
|
|
||||||
[clientContext, node]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleDelete = useCallback(
|
|
||||||
(e: React.MouseEvent) => {
|
|
||||||
deleteNode();
|
|
||||||
e.stopPropagation(); // Disable clicking prevents the sidebar from opening
|
|
||||||
},
|
|
||||||
[clientContext, node]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dropdown
|
|
||||||
trigger="hover"
|
|
||||||
position="bottomRight"
|
|
||||||
onVisibleChange={rerenderMenu}
|
|
||||||
render={
|
|
||||||
<Dropdown.Menu key={key}>
|
|
||||||
{canMoveOut && <Dropdown.Item onClick={handleMoveOut}>Move out</Dropdown.Item>}
|
|
||||||
<Dropdown.Item onClick={handleCopy} disabled={registry.meta!.copyDisable === true}>
|
|
||||||
Create Copy
|
|
||||||
</Dropdown.Item>
|
|
||||||
<Dropdown.Item
|
|
||||||
onClick={handleDelete}
|
|
||||||
disabled={!!(registry.canDelete?.(clientContext, node) || registry.meta!.deleteDisable)}
|
|
||||||
>
|
|
||||||
Delete
|
|
||||||
</Dropdown.Item>
|
|
||||||
</Dropdown.Menu>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<IconButton
|
|
||||||
color="secondary"
|
|
||||||
size="small"
|
|
||||||
theme="borderless"
|
|
||||||
icon={<IconMore />}
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
/>
|
|
||||||
</Dropdown>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function FormHeader() {
|
export function FormHeader() {
|
||||||
const { node, expanded, toggleExpand, readonly } = useNodeRenderContext();
|
const { node, expanded, toggleExpand, readonly, deleteNode } = useNodeRenderContext();
|
||||||
const isSidebar = useIsSidebar();
|
const isSidebar = useIsSidebar();
|
||||||
const handleExpand = (e: React.MouseEvent) => {
|
const handleExpand = (e: React.MouseEvent) => {
|
||||||
toggleExpand();
|
toggleExpand();
|
||||||
@ -124,7 +42,7 @@ export function FormHeader() {
|
|||||||
)}
|
)}
|
||||||
{readonly ? undefined : (
|
{readonly ? undefined : (
|
||||||
<Operators>
|
<Operators>
|
||||||
<DropdownContent />
|
<NodeMenu node={node} deleteNode={deleteNode} />
|
||||||
</Operators>
|
</Operators>
|
||||||
)}
|
)}
|
||||||
</Header>
|
</Header>
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import { createFreeSnapPlugin } from '@flowgram.ai/free-snap-plugin';
|
|||||||
import { createFreeNodePanelPlugin } from '@flowgram.ai/free-node-panel-plugin';
|
import { createFreeNodePanelPlugin } from '@flowgram.ai/free-node-panel-plugin';
|
||||||
import { createFreeLinesPlugin } from '@flowgram.ai/free-lines-plugin';
|
import { createFreeLinesPlugin } from '@flowgram.ai/free-lines-plugin';
|
||||||
import { FreeLayoutProps } from '@flowgram.ai/free-layout-editor';
|
import { FreeLayoutProps } from '@flowgram.ai/free-layout-editor';
|
||||||
|
import { createFreeGroupPlugin } from '@flowgram.ai/free-group-plugin';
|
||||||
import { createContainerNodePlugin } from '@flowgram.ai/free-container-plugin';
|
import { createContainerNodePlugin } from '@flowgram.ai/free-container-plugin';
|
||||||
|
|
||||||
import { onDragLineEnd } from '../utils';
|
import { onDragLineEnd } from '../utils';
|
||||||
@ -17,7 +18,7 @@ import { createSyncVariablePlugin } from '../plugins';
|
|||||||
import { defaultFormMeta } from '../nodes/default-form-meta';
|
import { defaultFormMeta } from '../nodes/default-form-meta';
|
||||||
import { WorkflowNodeType } from '../nodes';
|
import { WorkflowNodeType } from '../nodes';
|
||||||
import { SelectorBoxPopover } from '../components/selector-box-popover';
|
import { SelectorBoxPopover } from '../components/selector-box-popover';
|
||||||
import { BaseNode, CommentRender, LineAddButton, NodePanel } from '../components';
|
import { BaseNode, CommentRender, GroupNodeRender, LineAddButton, NodePanel } from '../components';
|
||||||
|
|
||||||
export function useEditorProps(
|
export function useEditorProps(
|
||||||
initialData: FlowDocumentJSON,
|
initialData: FlowDocumentJSON,
|
||||||
@ -225,6 +226,9 @@ export function useEditorProps(
|
|||||||
* 这个用于 loop 节点子画布的渲染
|
* 这个用于 loop 节点子画布的渲染
|
||||||
*/
|
*/
|
||||||
createContainerNodePlugin({}),
|
createContainerNodePlugin({}),
|
||||||
|
createFreeGroupPlugin({
|
||||||
|
groupNodeRender: GroupNodeRender,
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
[]
|
[]
|
||||||
|
|||||||
@ -74,51 +74,6 @@ export const initialData: FlowDocumentJSON = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: 'llm_0',
|
|
||||||
type: 'llm',
|
|
||||||
meta: {
|
|
||||||
position: {
|
|
||||||
x: 1430,
|
|
||||||
y: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
title: 'LLM_0',
|
|
||||||
inputsValues: {
|
|
||||||
modelType: 'gpt-3.5-turbo',
|
|
||||||
temperature: 0.5,
|
|
||||||
systemPrompt: 'You are an AI assistant.',
|
|
||||||
prompt: '',
|
|
||||||
},
|
|
||||||
inputs: {
|
|
||||||
type: 'object',
|
|
||||||
required: ['modelType', 'temperature', 'prompt'],
|
|
||||||
properties: {
|
|
||||||
modelType: {
|
|
||||||
type: 'string',
|
|
||||||
},
|
|
||||||
temperature: {
|
|
||||||
type: 'number',
|
|
||||||
},
|
|
||||||
systemPrompt: {
|
|
||||||
type: 'string',
|
|
||||||
},
|
|
||||||
prompt: {
|
|
||||||
type: 'string',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
outputs: {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
result: {
|
|
||||||
type: 'string',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: 'end_0',
|
id: 'end_0',
|
||||||
type: 'end',
|
type: 'end',
|
||||||
@ -267,7 +222,7 @@ export const initialData: FlowDocumentJSON = {
|
|||||||
meta: {
|
meta: {
|
||||||
position: {
|
position: {
|
||||||
x: 640,
|
x: 640,
|
||||||
y: 465.5,
|
y: 478,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
@ -278,6 +233,122 @@ export const initialData: FlowDocumentJSON = {
|
|||||||
note: 'hi ~\n\nthis is a comment node\n\n- flowgram.ai',
|
note: 'hi ~\n\nthis is a comment node\n\n- flowgram.ai',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'group_V-_st',
|
||||||
|
type: 'group',
|
||||||
|
meta: {
|
||||||
|
position: {
|
||||||
|
x: -1.5112031149433278,
|
||||||
|
y: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
title: 'LLM_Group',
|
||||||
|
color: 'Violet',
|
||||||
|
},
|
||||||
|
blocks: [
|
||||||
|
{
|
||||||
|
id: 'llm_0',
|
||||||
|
type: 'llm',
|
||||||
|
meta: {
|
||||||
|
position: {
|
||||||
|
x: 1660.1942854301792,
|
||||||
|
y: 1.8635936030104148,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
title: 'LLM_0',
|
||||||
|
inputsValues: {
|
||||||
|
modelType: 'gpt-3.5-turbo',
|
||||||
|
temperature: 0.5,
|
||||||
|
systemPrompt: 'You are an AI assistant.',
|
||||||
|
prompt: '',
|
||||||
|
},
|
||||||
|
inputs: {
|
||||||
|
type: 'object',
|
||||||
|
required: ['modelType', 'temperature', 'prompt'],
|
||||||
|
properties: {
|
||||||
|
modelType: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
temperature: {
|
||||||
|
type: 'number',
|
||||||
|
},
|
||||||
|
systemPrompt: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
prompt: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
outputs: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
result: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'llm_l_TcE',
|
||||||
|
type: 'llm',
|
||||||
|
meta: {
|
||||||
|
position: {
|
||||||
|
x: 1202.8281207997074,
|
||||||
|
y: 1.8635936030104148,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
title: 'LLM_1',
|
||||||
|
inputsValues: {},
|
||||||
|
inputs: {
|
||||||
|
type: 'object',
|
||||||
|
required: ['modelType', 'temperature', 'prompt'],
|
||||||
|
properties: {
|
||||||
|
modelType: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
temperature: {
|
||||||
|
type: 'number',
|
||||||
|
},
|
||||||
|
systemPrompt: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
prompt: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
outputs: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
result: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
edges: [
|
||||||
|
{
|
||||||
|
sourceNodeID: 'llm_l_TcE',
|
||||||
|
targetNodeID: 'llm_0',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sourceNodeID: 'llm_0',
|
||||||
|
targetNodeID: 'end_0',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sourceNodeID: 'condition_0',
|
||||||
|
targetNodeID: 'llm_l_TcE',
|
||||||
|
sourcePortID: 'if_0',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
edges: [
|
edges: [
|
||||||
{
|
{
|
||||||
@ -286,7 +357,7 @@ export const initialData: FlowDocumentJSON = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
sourceNodeID: 'condition_0',
|
sourceNodeID: 'condition_0',
|
||||||
targetNodeID: 'llm_0',
|
targetNodeID: 'llm_l_TcE',
|
||||||
sourcePortID: 'if_0',
|
sourcePortID: 'if_0',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@ -4,9 +4,8 @@ import { FlowNodeRegistry } from '../../typings';
|
|||||||
export const CommentNodeRegistry: FlowNodeRegistry = {
|
export const CommentNodeRegistry: FlowNodeRegistry = {
|
||||||
type: WorkflowNodeType.Comment,
|
type: WorkflowNodeType.Comment,
|
||||||
meta: {
|
meta: {
|
||||||
isStart: true,
|
disableSideBar: true,
|
||||||
isNodeEnd: true,
|
defaultPorts: [],
|
||||||
disableSideSheet: true,
|
|
||||||
renderKey: WorkflowNodeType.Comment,
|
renderKey: WorkflowNodeType.Comment,
|
||||||
size: {
|
size: {
|
||||||
width: 240,
|
width: 240,
|
||||||
|
|||||||
@ -38,7 +38,7 @@ export const LoopNodeRegistry: FlowNodeRegistry = {
|
|||||||
* 子画布 padding 设置
|
* 子画布 padding 设置
|
||||||
*/
|
*/
|
||||||
padding: () => ({
|
padding: () => ({
|
||||||
top: 150,
|
top: 125,
|
||||||
bottom: 100,
|
bottom: 100,
|
||||||
left: 100,
|
left: 100,
|
||||||
right: 100,
|
right: 100,
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import {
|
|||||||
FreeLayoutPluginContext,
|
FreeLayoutPluginContext,
|
||||||
FlowNodeEntity,
|
FlowNodeEntity,
|
||||||
type WorkflowEdgeJSON,
|
type WorkflowEdgeJSON,
|
||||||
|
WorkflowNodeMeta,
|
||||||
} from '@flowgram.ai/free-layout-editor';
|
} from '@flowgram.ai/free-layout-editor';
|
||||||
|
|
||||||
import { type JsonSchema } from './json-schema';
|
import { type JsonSchema } from './json-schema';
|
||||||
@ -45,11 +46,20 @@ export interface FlowNodeJSON extends FlowNodeJSONDefault {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* You can customize your own node meta
|
||||||
|
* 你可以自定义节点的meta
|
||||||
|
*/
|
||||||
|
export interface FlowNodeMeta extends WorkflowNodeMeta {
|
||||||
|
disableSideBar?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* You can customize your own node registry
|
* You can customize your own node registry
|
||||||
* 你可以自定义节点的注册器
|
* 你可以自定义节点的注册器
|
||||||
*/
|
*/
|
||||||
export interface FlowNodeRegistry extends FlowNodeRegistryDefault {
|
export interface FlowNodeRegistry extends FlowNodeRegistryDefault {
|
||||||
|
meta: FlowNodeMeta;
|
||||||
info?: {
|
info?: {
|
||||||
icon: string;
|
icon: string;
|
||||||
description: string;
|
description: string;
|
||||||
|
|||||||
138
common/config/rush/pnpm-lock.yaml
generated
138
common/config/rush/pnpm-lock.yaml
generated
@ -199,6 +199,9 @@ importers:
|
|||||||
'@flowgram.ai/free-container-plugin':
|
'@flowgram.ai/free-container-plugin':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../packages/plugins/free-container-plugin
|
version: link:../../packages/plugins/free-container-plugin
|
||||||
|
'@flowgram.ai/free-group-plugin':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../../packages/plugins/free-group-plugin
|
||||||
'@flowgram.ai/free-layout-editor':
|
'@flowgram.ai/free-layout-editor':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../packages/client/free-layout-editor
|
version: link:../../packages/client/free-layout-editor
|
||||||
@ -2215,6 +2218,94 @@ importers:
|
|||||||
'@flowgram.ai/renderer':
|
'@flowgram.ai/renderer':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../canvas-engine/renderer
|
version: link:../../canvas-engine/renderer
|
||||||
|
'@flowgram.ai/shortcuts-plugin':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../shortcuts-plugin
|
||||||
|
'@flowgram.ai/utils':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../../common/utils
|
||||||
|
inversify:
|
||||||
|
specifier: ^6.0.1
|
||||||
|
version: 6.2.0(reflect-metadata@0.2.2)
|
||||||
|
lodash:
|
||||||
|
specifier: ^4.17.21
|
||||||
|
version: 4.17.21
|
||||||
|
reflect-metadata:
|
||||||
|
specifier: ~0.2.2
|
||||||
|
version: 0.2.2
|
||||||
|
devDependencies:
|
||||||
|
'@flowgram.ai/eslint-config':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../../../config/eslint-config
|
||||||
|
'@flowgram.ai/ts-config':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../../../config/ts-config
|
||||||
|
'@types/bezier-js':
|
||||||
|
specifier: 4.1.3
|
||||||
|
version: 4.1.3
|
||||||
|
'@types/lodash':
|
||||||
|
specifier: ^4.14.137
|
||||||
|
version: 4.17.13
|
||||||
|
'@types/react':
|
||||||
|
specifier: ^18
|
||||||
|
version: 18.3.16
|
||||||
|
'@types/react-dom':
|
||||||
|
specifier: ^18
|
||||||
|
version: 18.3.5(@types/react@18.3.16)
|
||||||
|
'@types/styled-components':
|
||||||
|
specifier: ^5
|
||||||
|
version: 5.1.34
|
||||||
|
'@vitest/coverage-v8':
|
||||||
|
specifier: ^0.32.0
|
||||||
|
version: 0.32.4(vitest@0.34.6)
|
||||||
|
eslint:
|
||||||
|
specifier: ^8.54.0
|
||||||
|
version: 8.57.1
|
||||||
|
react:
|
||||||
|
specifier: ^18
|
||||||
|
version: 18.3.1
|
||||||
|
react-dom:
|
||||||
|
specifier: ^18
|
||||||
|
version: 18.3.1(react@18.3.1)
|
||||||
|
styled-components:
|
||||||
|
specifier: ^5
|
||||||
|
version: 5.3.11(@babel/core@7.26.10)(react-dom@18.3.1)(react-is@18.3.1)(react@18.3.1)
|
||||||
|
tsup:
|
||||||
|
specifier: ^8.0.1
|
||||||
|
version: 8.3.5(typescript@5.0.4)
|
||||||
|
typescript:
|
||||||
|
specifier: ^5.0.4
|
||||||
|
version: 5.0.4
|
||||||
|
vitest:
|
||||||
|
specifier: ^0.34.6
|
||||||
|
version: 0.34.6(jsdom@22.1.0)
|
||||||
|
|
||||||
|
../../packages/plugins/free-group-plugin:
|
||||||
|
dependencies:
|
||||||
|
'@flowgram.ai/core':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../../canvas-engine/core
|
||||||
|
'@flowgram.ai/document':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../../canvas-engine/document
|
||||||
|
'@flowgram.ai/free-container-plugin':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../free-container-plugin
|
||||||
|
'@flowgram.ai/free-history-plugin':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../free-history-plugin
|
||||||
|
'@flowgram.ai/free-layout-core':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../../canvas-engine/free-layout-core
|
||||||
|
'@flowgram.ai/free-lines-plugin':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../free-lines-plugin
|
||||||
|
'@flowgram.ai/renderer':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../../canvas-engine/renderer
|
||||||
|
'@flowgram.ai/shortcuts-plugin':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../shortcuts-plugin
|
||||||
'@flowgram.ai/utils':
|
'@flowgram.ai/utils':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../common/utils
|
version: link:../../common/utils
|
||||||
@ -3544,7 +3635,7 @@ packages:
|
|||||||
'@babel/helpers': 7.27.0
|
'@babel/helpers': 7.27.0
|
||||||
'@babel/parser': 7.27.0
|
'@babel/parser': 7.27.0
|
||||||
'@babel/template': 7.27.0
|
'@babel/template': 7.27.0
|
||||||
'@babel/traverse': 7.27.0
|
'@babel/traverse': 7.27.0(supports-color@5.5.0)
|
||||||
'@babel/types': 7.27.0
|
'@babel/types': 7.27.0
|
||||||
convert-source-map: 2.0.0
|
convert-source-map: 2.0.0
|
||||||
debug: 4.4.0(supports-color@5.5.0)
|
debug: 4.4.0(supports-color@5.5.0)
|
||||||
@ -3704,6 +3795,15 @@ packages:
|
|||||||
- supports-color
|
- supports-color
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@babel/helper-module-imports@7.25.9:
|
||||||
|
resolution: {integrity: sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==}
|
||||||
|
engines: {node: '>=6.9.0'}
|
||||||
|
dependencies:
|
||||||
|
'@babel/traverse': 7.27.0(supports-color@5.5.0)
|
||||||
|
'@babel/types': 7.27.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
/@babel/helper-module-imports@7.25.9(supports-color@5.5.0):
|
/@babel/helper-module-imports@7.25.9(supports-color@5.5.0):
|
||||||
resolution: {integrity: sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==}
|
resolution: {integrity: sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
@ -3720,7 +3820,7 @@ packages:
|
|||||||
'@babel/core': ^7.0.0
|
'@babel/core': ^7.0.0
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.26.0
|
'@babel/core': 7.26.0
|
||||||
'@babel/helper-module-imports': 7.25.9(supports-color@5.5.0)
|
'@babel/helper-module-imports': 7.25.9
|
||||||
'@babel/helper-validator-identifier': 7.25.9
|
'@babel/helper-validator-identifier': 7.25.9
|
||||||
'@babel/traverse': 7.26.4(supports-color@5.5.0)
|
'@babel/traverse': 7.26.4(supports-color@5.5.0)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
@ -3734,7 +3834,7 @@ packages:
|
|||||||
'@babel/core': ^7.0.0
|
'@babel/core': ^7.0.0
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.26.10
|
'@babel/core': 7.26.10
|
||||||
'@babel/helper-module-imports': 7.25.9(supports-color@5.5.0)
|
'@babel/helper-module-imports': 7.25.9
|
||||||
'@babel/helper-validator-identifier': 7.25.9
|
'@babel/helper-validator-identifier': 7.25.9
|
||||||
'@babel/traverse': 7.26.4(supports-color@5.5.0)
|
'@babel/traverse': 7.26.4(supports-color@5.5.0)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
@ -4241,7 +4341,7 @@ packages:
|
|||||||
'@babel/core': ^7.0.0-0
|
'@babel/core': ^7.0.0-0
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.26.0
|
'@babel/core': 7.26.0
|
||||||
'@babel/helper-module-imports': 7.25.9(supports-color@5.5.0)
|
'@babel/helper-module-imports': 7.25.9
|
||||||
'@babel/helper-plugin-utils': 7.25.9
|
'@babel/helper-plugin-utils': 7.25.9
|
||||||
'@babel/helper-remap-async-to-generator': 7.25.9(@babel/core@7.26.0)
|
'@babel/helper-remap-async-to-generator': 7.25.9(@babel/core@7.26.0)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
@ -4535,7 +4635,7 @@ packages:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.26.0
|
'@babel/core': 7.26.0
|
||||||
'@babel/helper-annotate-as-pure': 7.25.9
|
'@babel/helper-annotate-as-pure': 7.25.9
|
||||||
'@babel/helper-module-imports': 7.25.9(supports-color@5.5.0)
|
'@babel/helper-module-imports': 7.25.9
|
||||||
'@babel/helper-plugin-utils': 7.25.9
|
'@babel/helper-plugin-utils': 7.25.9
|
||||||
'@babel/plugin-syntax-jsx': 7.25.9(@babel/core@7.26.0)
|
'@babel/plugin-syntax-jsx': 7.25.9(@babel/core@7.26.0)
|
||||||
'@babel/types': 7.26.3
|
'@babel/types': 7.26.3
|
||||||
@ -4800,7 +4900,7 @@ packages:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
/@babel/traverse@7.27.0:
|
/@babel/traverse@7.27.0(supports-color@5.5.0):
|
||||||
resolution: {integrity: sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA==}
|
resolution: {integrity: sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -9961,7 +10061,7 @@ packages:
|
|||||||
/estree-util-attach-comments@3.0.0:
|
/estree-util-attach-comments@3.0.0:
|
||||||
resolution: {integrity: sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw==}
|
resolution: {integrity: sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw==}
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/estree': 1.0.6
|
'@types/estree': 1.0.7
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/estree-util-build-jsx@2.2.2:
|
/estree-util-build-jsx@2.2.2:
|
||||||
@ -9990,7 +10090,7 @@ packages:
|
|||||||
/estree-util-scope@1.0.0:
|
/estree-util-scope@1.0.0:
|
||||||
resolution: {integrity: sha512-2CAASclonf+JFWBNJPndcOpA8EMJwa0Q8LUFJEKqXLW6+qBvbFZuF5gItbQOs/umBUkjviCSDCbBwU2cXbmrhQ==}
|
resolution: {integrity: sha512-2CAASclonf+JFWBNJPndcOpA8EMJwa0Q8LUFJEKqXLW6+qBvbFZuF5gItbQOs/umBUkjviCSDCbBwU2cXbmrhQ==}
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/estree': 1.0.6
|
'@types/estree': 1.0.7
|
||||||
devlop: 1.1.0
|
devlop: 1.1.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
@ -10664,7 +10764,7 @@ packages:
|
|||||||
/hast-util-to-estree@3.1.1:
|
/hast-util-to-estree@3.1.1:
|
||||||
resolution: {integrity: sha512-IWtwwmPskfSmma9RpzCappDUitC8t5jhAynHhc1m2+5trOgsrp7txscUSavc5Ic8PATyAjfrCK1wgtxh2cICVQ==}
|
resolution: {integrity: sha512-IWtwwmPskfSmma9RpzCappDUitC8t5jhAynHhc1m2+5trOgsrp7txscUSavc5Ic8PATyAjfrCK1wgtxh2cICVQ==}
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/estree': 1.0.6
|
'@types/estree': 1.0.7
|
||||||
'@types/estree-jsx': 1.0.5
|
'@types/estree-jsx': 1.0.5
|
||||||
'@types/hast': 3.0.4
|
'@types/hast': 3.0.4
|
||||||
comma-separated-tokens: 2.0.3
|
comma-separated-tokens: 2.0.3
|
||||||
@ -10687,7 +10787,7 @@ packages:
|
|||||||
/hast-util-to-jsx-runtime@2.3.2:
|
/hast-util-to-jsx-runtime@2.3.2:
|
||||||
resolution: {integrity: sha512-1ngXYb+V9UT5h+PxNRa1O1FYguZK/XL+gkeqvp7EdHlB9oHUG0eYRo/vY5inBdcqo3RkPMC58/H94HvkbfGdyg==}
|
resolution: {integrity: sha512-1ngXYb+V9UT5h+PxNRa1O1FYguZK/XL+gkeqvp7EdHlB9oHUG0eYRo/vY5inBdcqo3RkPMC58/H94HvkbfGdyg==}
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/estree': 1.0.6
|
'@types/estree': 1.0.7
|
||||||
'@types/hast': 3.0.4
|
'@types/hast': 3.0.4
|
||||||
'@types/unist': 3.0.3
|
'@types/unist': 3.0.3
|
||||||
comma-separated-tokens: 2.0.3
|
comma-separated-tokens: 2.0.3
|
||||||
@ -12364,7 +12464,7 @@ packages:
|
|||||||
/micromark-extension-mdx-expression@3.0.0:
|
/micromark-extension-mdx-expression@3.0.0:
|
||||||
resolution: {integrity: sha512-sI0nwhUDz97xyzqJAbHQhp5TfaxEvZZZ2JDqUo+7NvyIYG6BZ5CPPqj2ogUoPJlmXHBnyZUzISg9+oUmU6tUjQ==}
|
resolution: {integrity: sha512-sI0nwhUDz97xyzqJAbHQhp5TfaxEvZZZ2JDqUo+7NvyIYG6BZ5CPPqj2ogUoPJlmXHBnyZUzISg9+oUmU6tUjQ==}
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/estree': 1.0.6
|
'@types/estree': 1.0.7
|
||||||
devlop: 1.1.0
|
devlop: 1.1.0
|
||||||
micromark-factory-mdx-expression: 2.0.2
|
micromark-factory-mdx-expression: 2.0.2
|
||||||
micromark-factory-space: 2.0.1
|
micromark-factory-space: 2.0.1
|
||||||
@ -12392,7 +12492,7 @@ packages:
|
|||||||
resolution: {integrity: sha512-vNuFb9czP8QCtAQcEJn0UJQJZA8Dk6DXKBqx+bg/w0WGuSxDxNr7hErW89tHUY31dUW4NqEOWwmEUNhjTFmHkg==}
|
resolution: {integrity: sha512-vNuFb9czP8QCtAQcEJn0UJQJZA8Dk6DXKBqx+bg/w0WGuSxDxNr7hErW89tHUY31dUW4NqEOWwmEUNhjTFmHkg==}
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/acorn': 4.0.6
|
'@types/acorn': 4.0.6
|
||||||
'@types/estree': 1.0.6
|
'@types/estree': 1.0.7
|
||||||
devlop: 1.1.0
|
devlop: 1.1.0
|
||||||
estree-util-is-identifier-name: 3.0.0
|
estree-util-is-identifier-name: 3.0.0
|
||||||
micromark-factory-mdx-expression: 2.0.2
|
micromark-factory-mdx-expression: 2.0.2
|
||||||
@ -12431,7 +12531,7 @@ packages:
|
|||||||
/micromark-extension-mdxjs-esm@3.0.0:
|
/micromark-extension-mdxjs-esm@3.0.0:
|
||||||
resolution: {integrity: sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A==}
|
resolution: {integrity: sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A==}
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/estree': 1.0.6
|
'@types/estree': 1.0.7
|
||||||
devlop: 1.1.0
|
devlop: 1.1.0
|
||||||
micromark-core-commonmark: 2.0.2
|
micromark-core-commonmark: 2.0.2
|
||||||
micromark-util-character: 2.1.1
|
micromark-util-character: 2.1.1
|
||||||
@ -12514,7 +12614,7 @@ packages:
|
|||||||
/micromark-factory-mdx-expression@2.0.2:
|
/micromark-factory-mdx-expression@2.0.2:
|
||||||
resolution: {integrity: sha512-5E5I2pFzJyg2CtemqAbcyCktpHXuJbABnsb32wX2U8IQKhhVFBqkcZR5LRm1WVoFqa4kTueZK4abep7wdo9nrw==}
|
resolution: {integrity: sha512-5E5I2pFzJyg2CtemqAbcyCktpHXuJbABnsb32wX2U8IQKhhVFBqkcZR5LRm1WVoFqa4kTueZK4abep7wdo9nrw==}
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/estree': 1.0.6
|
'@types/estree': 1.0.7
|
||||||
devlop: 1.1.0
|
devlop: 1.1.0
|
||||||
micromark-factory-space: 2.0.1
|
micromark-factory-space: 2.0.1
|
||||||
micromark-util-character: 2.1.1
|
micromark-util-character: 2.1.1
|
||||||
@ -12675,7 +12775,7 @@ packages:
|
|||||||
resolution: {integrity: sha512-Fk+xmBrOv9QZnEDguL9OI9/NQQp6Hz4FuQ4YmCb/5V7+9eAh1s6AYSvL20kHkD67YIg7EpE54TiSlcsf3vyZgA==}
|
resolution: {integrity: sha512-Fk+xmBrOv9QZnEDguL9OI9/NQQp6Hz4FuQ4YmCb/5V7+9eAh1s6AYSvL20kHkD67YIg7EpE54TiSlcsf3vyZgA==}
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/acorn': 4.0.6
|
'@types/acorn': 4.0.6
|
||||||
'@types/estree': 1.0.6
|
'@types/estree': 1.0.7
|
||||||
'@types/unist': 3.0.3
|
'@types/unist': 3.0.3
|
||||||
devlop: 1.1.0
|
devlop: 1.1.0
|
||||||
estree-util-visit: 2.0.0
|
estree-util-visit: 2.0.0
|
||||||
@ -13723,7 +13823,7 @@ packages:
|
|||||||
/recma-build-jsx@1.0.0:
|
/recma-build-jsx@1.0.0:
|
||||||
resolution: {integrity: sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew==}
|
resolution: {integrity: sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew==}
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/estree': 1.0.6
|
'@types/estree': 1.0.7
|
||||||
estree-util-build-jsx: 3.0.1
|
estree-util-build-jsx: 3.0.1
|
||||||
vfile: 6.0.3
|
vfile: 6.0.3
|
||||||
dev: false
|
dev: false
|
||||||
@ -13743,7 +13843,7 @@ packages:
|
|||||||
/recma-parse@1.0.0:
|
/recma-parse@1.0.0:
|
||||||
resolution: {integrity: sha512-OYLsIGBB5Y5wjnSnQW6t3Xg7q3fQ7FWbw/vcXtORTnyaSFscOtABg+7Pnz6YZ6c27fG1/aN8CjfwoUEUIdwqWQ==}
|
resolution: {integrity: sha512-OYLsIGBB5Y5wjnSnQW6t3Xg7q3fQ7FWbw/vcXtORTnyaSFscOtABg+7Pnz6YZ6c27fG1/aN8CjfwoUEUIdwqWQ==}
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/estree': 1.0.6
|
'@types/estree': 1.0.7
|
||||||
esast-util-from-js: 2.0.1
|
esast-util-from-js: 2.0.1
|
||||||
unified: 11.0.5
|
unified: 11.0.5
|
||||||
vfile: 6.0.3
|
vfile: 6.0.3
|
||||||
@ -13752,7 +13852,7 @@ packages:
|
|||||||
/recma-stringify@1.0.0:
|
/recma-stringify@1.0.0:
|
||||||
resolution: {integrity: sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g==}
|
resolution: {integrity: sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g==}
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/estree': 1.0.6
|
'@types/estree': 1.0.7
|
||||||
estree-util-to-js: 2.0.0
|
estree-util-to-js: 2.0.0
|
||||||
unified: 11.0.5
|
unified: 11.0.5
|
||||||
vfile: 6.0.3
|
vfile: 6.0.3
|
||||||
@ -13854,7 +13954,7 @@ packages:
|
|||||||
/rehype-recma@1.0.0:
|
/rehype-recma@1.0.0:
|
||||||
resolution: {integrity: sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw==}
|
resolution: {integrity: sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw==}
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/estree': 1.0.6
|
'@types/estree': 1.0.7
|
||||||
'@types/hast': 3.0.4
|
'@types/hast': 3.0.4
|
||||||
hast-util-to-estree: 3.1.1
|
hast-util-to-estree: 3.1.1
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
|
|||||||
@ -9,6 +9,7 @@
|
|||||||
"douyinfe",
|
"douyinfe",
|
||||||
"flowgram",
|
"flowgram",
|
||||||
"flowgram.ai",
|
"flowgram.ai",
|
||||||
|
"Hoverable",
|
||||||
"openbracket",
|
"openbracket",
|
||||||
"rsbuild",
|
"rsbuild",
|
||||||
"rspack",
|
"rspack",
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
import { inject, injectable } from 'inversify';
|
import { inject, injectable } from 'inversify';
|
||||||
import { EntityManager } from '@flowgram.ai/core';
|
|
||||||
import { Emitter } from '@flowgram.ai/utils';
|
import { Emitter } from '@flowgram.ai/utils';
|
||||||
|
import { EntityManager } from '@flowgram.ai/core';
|
||||||
|
|
||||||
|
import { FlowGroupUtils } from './flow-group-service/flow-group-utils';
|
||||||
import { FlowNodeBaseType, FlowOperationBaseService, LABEL_SIDE_TYPE } from '../typings';
|
import { FlowNodeBaseType, FlowOperationBaseService, LABEL_SIDE_TYPE } from '../typings';
|
||||||
import { FlowGroupController } from '../services';
|
|
||||||
import { FlowDocument } from '../flow-document';
|
import { FlowDocument } from '../flow-document';
|
||||||
import { FlowNodeEntity, FlowRendererStateEntity } from '../entities';
|
import { FlowNodeEntity, FlowRendererStateEntity } from '../entities';
|
||||||
import { FlowNodeRenderData } from '../datas';
|
import { FlowNodeRenderData } from '../datas';
|
||||||
@ -126,9 +126,11 @@ export class FlowDragService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 分组节点不能嵌套
|
// 分组节点不能嵌套
|
||||||
const hasGroupNode = this.dragNodes.some(node => node.flowNodeType === FlowNodeBaseType.GROUP);
|
const hasGroupNode = this.dragNodes.some(
|
||||||
|
(node) => node.flowNodeType === FlowNodeBaseType.GROUP
|
||||||
|
);
|
||||||
if (hasGroupNode) {
|
if (hasGroupNode) {
|
||||||
const group = FlowGroupController.getNodeRecursionGroupController(node);
|
const group = FlowGroupUtils.getNodeRecursionGroupController(node);
|
||||||
if (group) {
|
if (group) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,305 +0,0 @@
|
|||||||
import { nanoid } from 'nanoid';
|
|
||||||
import { inject, injectable } from 'inversify';
|
|
||||||
import { Rectangle } from '@flowgram.ai/utils';
|
|
||||||
import { EntityManager } from '@flowgram.ai/core';
|
|
||||||
|
|
||||||
import { FlowNodeBaseType, FlowOperationBaseService, OperationType } from '../typings';
|
|
||||||
import { FlowNodeEntity } from '../entities';
|
|
||||||
import { FlowNodeRenderData, FlowNodeTransformData } from '../datas';
|
|
||||||
|
|
||||||
@injectable()
|
|
||||||
/** 分组服务 */
|
|
||||||
export class FlowGroupService {
|
|
||||||
@inject(EntityManager) public readonly entityManager: EntityManager;
|
|
||||||
|
|
||||||
@inject(FlowOperationBaseService)
|
|
||||||
public readonly operationService: FlowOperationBaseService;
|
|
||||||
|
|
||||||
/** 创建分组节点 */
|
|
||||||
public createGroup(nodes: FlowNodeEntity[]): FlowNodeEntity | undefined {
|
|
||||||
if (!nodes || !Array.isArray(nodes) || nodes.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!FlowGroupController.validate(nodes)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const sortedNodes: FlowNodeEntity[] = nodes.sort((a, b) => a.index - b.index);
|
|
||||||
const fromNode = sortedNodes[0];
|
|
||||||
const groupId = `group_${nanoid(5)}`;
|
|
||||||
this.operationService.apply({
|
|
||||||
type: OperationType.createGroup,
|
|
||||||
value: {
|
|
||||||
targetId: fromNode.id,
|
|
||||||
groupId,
|
|
||||||
nodeIds: nodes.map((node) => node.id),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const groupNode = this.entityManager.getEntityById<FlowNodeEntity>(groupId);
|
|
||||||
if (!groupNode) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const group = this.groupController(groupNode);
|
|
||||||
if (!group) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
group.expand();
|
|
||||||
return groupNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 删除分组 */
|
|
||||||
public deleteGroup(groupNode: FlowNodeEntity): void {
|
|
||||||
const json = groupNode.toJSON();
|
|
||||||
if (!groupNode.pre || !json) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.operationService.apply({
|
|
||||||
type: OperationType.deleteNodes,
|
|
||||||
value: {
|
|
||||||
fromId: groupNode.pre.id,
|
|
||||||
nodes: [json],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 取消分组 */
|
|
||||||
public ungroup(groupNode: FlowNodeEntity): void {
|
|
||||||
const group = this.groupController(groupNode);
|
|
||||||
if (!group) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const nodes = group.nodes;
|
|
||||||
if (!groupNode.pre) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
group.collapse();
|
|
||||||
this.operationService.apply({
|
|
||||||
type: OperationType.ungroup,
|
|
||||||
value: {
|
|
||||||
groupId: groupNode.id,
|
|
||||||
targetId: groupNode.pre.id,
|
|
||||||
nodeIds: nodes.map((node) => node.id),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 返回所有分组节点 */
|
|
||||||
public getAllGroups(): FlowGroupController[] {
|
|
||||||
const allNodes = this.entityManager.getEntities<FlowNodeEntity>(FlowNodeEntity);
|
|
||||||
const groupNodes = allNodes.filter((node) => node.flowNodeType === FlowNodeBaseType.GROUP);
|
|
||||||
return groupNodes
|
|
||||||
.map((node) => this.groupController(node))
|
|
||||||
.filter(Boolean) as FlowGroupController[];
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 获取分组控制器*/
|
|
||||||
public groupController(group: FlowNodeEntity): FlowGroupController | undefined {
|
|
||||||
return FlowGroupController.create(group);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static validate(nodes: FlowNodeEntity[]): boolean {
|
|
||||||
return FlowGroupController.validate(nodes);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 分组控制器 */
|
|
||||||
export class FlowGroupController {
|
|
||||||
private constructor(public readonly groupNode: FlowNodeEntity) {}
|
|
||||||
|
|
||||||
public get nodes(): FlowNodeEntity[] {
|
|
||||||
return this.groupNode.collapsedChildren || [];
|
|
||||||
}
|
|
||||||
|
|
||||||
public get collapsed(): boolean {
|
|
||||||
const groupTransformData = this.groupNode.getData<FlowNodeTransformData>(FlowNodeTransformData);
|
|
||||||
return groupTransformData.collapsed;
|
|
||||||
}
|
|
||||||
|
|
||||||
public collapse(): void {
|
|
||||||
this.collapsed = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public expand(): void {
|
|
||||||
this.collapsed = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 获取分组外围的最大边框 */
|
|
||||||
public get bounds(): Rectangle {
|
|
||||||
const groupNodeBounds =
|
|
||||||
this.groupNode.getData<FlowNodeTransformData>(FlowNodeTransformData).bounds;
|
|
||||||
return groupNodeBounds;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 是否是开始节点 */
|
|
||||||
public isStartNode(node?: FlowNodeEntity): boolean {
|
|
||||||
if (!node) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const nodes = this.nodes;
|
|
||||||
if (!nodes[0]) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return node.id === nodes[0].id;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 是否是结束节点 */
|
|
||||||
public isEndNode(node?: FlowNodeEntity): boolean {
|
|
||||||
if (!node) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const nodes = this.nodes;
|
|
||||||
if (!nodes[nodes.length - 1]) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return node.id === nodes[nodes.length - 1].id;
|
|
||||||
}
|
|
||||||
|
|
||||||
public set note(note: string) {
|
|
||||||
this.groupNode.getNodeMeta().note = note;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get note(): string {
|
|
||||||
return this.groupNode.getNodeMeta().note || '';
|
|
||||||
}
|
|
||||||
|
|
||||||
public set noteHeight(height: number) {
|
|
||||||
this.groupNode.getNodeMeta().noteHeight = height;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get noteHeight(): number {
|
|
||||||
return this.groupNode.getNodeMeta().noteHeight || 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get positionConfig(): Record<string, number> {
|
|
||||||
return this.groupNode.getNodeMeta().positionConfig;
|
|
||||||
}
|
|
||||||
|
|
||||||
private set collapsed(collapsed: boolean) {
|
|
||||||
const groupTransformData = this.groupNode.getData<FlowNodeTransformData>(FlowNodeTransformData);
|
|
||||||
groupTransformData.collapsed = collapsed;
|
|
||||||
groupTransformData.localDirty = true;
|
|
||||||
if (groupTransformData.parent) groupTransformData.parent.localDirty = true;
|
|
||||||
if (groupTransformData.parent?.firstChild)
|
|
||||||
groupTransformData.parent.firstChild.localDirty = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public set hovered(hovered: boolean) {
|
|
||||||
const groupRenderData = this.groupNode.getData<FlowNodeRenderData>(FlowNodeRenderData);
|
|
||||||
if (hovered) {
|
|
||||||
groupRenderData.toggleMouseEnter();
|
|
||||||
} else {
|
|
||||||
groupRenderData.toggleMouseLeave();
|
|
||||||
}
|
|
||||||
if (groupRenderData.hovered === hovered) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
groupRenderData.hovered = hovered;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get hovered(): boolean {
|
|
||||||
const groupRenderData = this.groupNode.getData<FlowNodeRenderData>(FlowNodeRenderData);
|
|
||||||
return groupRenderData.hovered;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static create(groupNode?: FlowNodeEntity): FlowGroupController | undefined {
|
|
||||||
if (!groupNode) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!FlowGroupController.isGroupNode(groupNode)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
return new FlowGroupController(groupNode);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 判断节点能否组成分组 */
|
|
||||||
public static validate(nodes: FlowNodeEntity[]): boolean {
|
|
||||||
if (!nodes || !Array.isArray(nodes) || nodes.length === 0) {
|
|
||||||
// 参数不合法
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 判断是否有分组节点
|
|
||||||
const isGroupRelatedNode = nodes.some((node) => FlowGroupController.isGroupNode(node));
|
|
||||||
if (isGroupRelatedNode) return false;
|
|
||||||
|
|
||||||
// 判断是否有节点已经处于分组中
|
|
||||||
const hasGroup = nodes.some((node) => node && this.isNodeInGroup(node));
|
|
||||||
if (hasGroup) return false;
|
|
||||||
|
|
||||||
// 判断是否来自同一个父亲
|
|
||||||
const parent = nodes[0].parent;
|
|
||||||
const isSameParent = nodes.every((node) => node.parent === parent);
|
|
||||||
if (!isSameParent) return false;
|
|
||||||
|
|
||||||
// 判断节点索引是否连续
|
|
||||||
const indexes = nodes.map((node) => node.index).sort((a, b) => a - b);
|
|
||||||
const isIndexContinuous = indexes.every((index, i, arr) => {
|
|
||||||
if (i === 0) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return index === arr[i - 1] + 1;
|
|
||||||
});
|
|
||||||
if (!isIndexContinuous) return false;
|
|
||||||
|
|
||||||
// 判断节点父亲是否已经在分组中
|
|
||||||
const parents = this.findNodeParents(nodes[0]);
|
|
||||||
const parentsInGroup = parents.some((parent) => this.isNodeInGroup(parent));
|
|
||||||
if (parentsInGroup) return false;
|
|
||||||
|
|
||||||
// 参数正确
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 获取节点分组控制 */
|
|
||||||
public static getNodeGroupController(node?: FlowNodeEntity): FlowGroupController | undefined {
|
|
||||||
if (!node) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!this.isNodeInGroup(node)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const groupNode = node?.parent;
|
|
||||||
return FlowGroupController.create(groupNode);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 向上递归查找分组递归控制 */
|
|
||||||
public static getNodeRecursionGroupController(
|
|
||||||
node?: FlowNodeEntity
|
|
||||||
): FlowGroupController | undefined {
|
|
||||||
if (!node) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const group = this.getNodeGroupController(node);
|
|
||||||
if (group) {
|
|
||||||
return group;
|
|
||||||
}
|
|
||||||
if (node.parent) {
|
|
||||||
return this.getNodeRecursionGroupController(node.parent);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 是否分组节点 */
|
|
||||||
public static isGroupNode(group: FlowNodeEntity): boolean {
|
|
||||||
return group.flowNodeType === FlowNodeBaseType.GROUP;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 找到节点所有上级 */
|
|
||||||
private static findNodeParents(node: FlowNodeEntity): FlowNodeEntity[] {
|
|
||||||
const parents = [];
|
|
||||||
let parent = node.parent;
|
|
||||||
while (parent) {
|
|
||||||
parents.push(parent);
|
|
||||||
parent = parent.parent;
|
|
||||||
}
|
|
||||||
return parents;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 节点是否处于分组中 */
|
|
||||||
private static isNodeInGroup(node: FlowNodeEntity): boolean {
|
|
||||||
// 处于分组中
|
|
||||||
if (node?.parent?.flowNodeType === FlowNodeBaseType.GROUP) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -0,0 +1,115 @@
|
|||||||
|
import { Rectangle } from '@flowgram.ai/utils';
|
||||||
|
|
||||||
|
import { FlowNodeEntity } from '../../entities';
|
||||||
|
import { FlowNodeRenderData, FlowNodeTransformData } from '../../datas';
|
||||||
|
import { FlowGroupUtils } from './flow-group-utils';
|
||||||
|
|
||||||
|
/** 分组控制器 */
|
||||||
|
export class FlowGroupController {
|
||||||
|
private constructor(public readonly groupNode: FlowNodeEntity) {}
|
||||||
|
|
||||||
|
public get nodes(): FlowNodeEntity[] {
|
||||||
|
return this.groupNode.collapsedChildren || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public get collapsed(): boolean {
|
||||||
|
const groupTransformData = this.groupNode.getData<FlowNodeTransformData>(FlowNodeTransformData);
|
||||||
|
return groupTransformData.collapsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
public collapse(): void {
|
||||||
|
this.collapsed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public expand(): void {
|
||||||
|
this.collapsed = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取分组外围的最大边框 */
|
||||||
|
public get bounds(): Rectangle {
|
||||||
|
const groupNodeBounds =
|
||||||
|
this.groupNode.getData<FlowNodeTransformData>(FlowNodeTransformData).bounds;
|
||||||
|
return groupNodeBounds;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 是否是开始节点 */
|
||||||
|
public isStartNode(node?: FlowNodeEntity): boolean {
|
||||||
|
if (!node) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const nodes = this.nodes;
|
||||||
|
if (!nodes[0]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return node.id === nodes[0].id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 是否是结束节点 */
|
||||||
|
public isEndNode(node?: FlowNodeEntity): boolean {
|
||||||
|
if (!node) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const nodes = this.nodes;
|
||||||
|
if (!nodes[nodes.length - 1]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return node.id === nodes[nodes.length - 1].id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public set note(note: string) {
|
||||||
|
this.groupNode.getNodeMeta().note = note;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get note(): string {
|
||||||
|
return this.groupNode.getNodeMeta().note || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public set noteHeight(height: number) {
|
||||||
|
this.groupNode.getNodeMeta().noteHeight = height;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get noteHeight(): number {
|
||||||
|
return this.groupNode.getNodeMeta().noteHeight || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get positionConfig(): Record<string, number> {
|
||||||
|
return this.groupNode.getNodeMeta().positionConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
private set collapsed(collapsed: boolean) {
|
||||||
|
const groupTransformData = this.groupNode.getData<FlowNodeTransformData>(FlowNodeTransformData);
|
||||||
|
groupTransformData.collapsed = collapsed;
|
||||||
|
groupTransformData.localDirty = true;
|
||||||
|
if (groupTransformData.parent) groupTransformData.parent.localDirty = true;
|
||||||
|
if (groupTransformData.parent?.firstChild)
|
||||||
|
groupTransformData.parent.firstChild.localDirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public set hovered(hovered: boolean) {
|
||||||
|
const groupRenderData = this.groupNode.getData<FlowNodeRenderData>(FlowNodeRenderData);
|
||||||
|
if (hovered) {
|
||||||
|
groupRenderData.toggleMouseEnter();
|
||||||
|
} else {
|
||||||
|
groupRenderData.toggleMouseLeave();
|
||||||
|
}
|
||||||
|
if (groupRenderData.hovered === hovered) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
groupRenderData.hovered = hovered;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get hovered(): boolean {
|
||||||
|
const groupRenderData = this.groupNode.getData<FlowNodeRenderData>(FlowNodeRenderData);
|
||||||
|
return groupRenderData.hovered;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static create(groupNode?: FlowNodeEntity): FlowGroupController | undefined {
|
||||||
|
if (!groupNode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!FlowGroupUtils.isGroupNode(groupNode)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return new FlowGroupController(groupNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,102 @@
|
|||||||
|
import { nanoid } from 'nanoid';
|
||||||
|
import { inject, injectable } from 'inversify';
|
||||||
|
import { EntityManager } from '@flowgram.ai/core';
|
||||||
|
|
||||||
|
import { FlowNodeBaseType, FlowOperationBaseService, OperationType } from '../../typings';
|
||||||
|
import { FlowNodeEntity } from '../../entities';
|
||||||
|
import { FlowGroupUtils } from './flow-group-utils';
|
||||||
|
import { FlowGroupController } from './flow-group-controller';
|
||||||
|
|
||||||
|
@injectable()
|
||||||
|
/** 分组服务 */
|
||||||
|
export class FlowGroupService {
|
||||||
|
@inject(EntityManager) public readonly entityManager: EntityManager;
|
||||||
|
|
||||||
|
@inject(FlowOperationBaseService)
|
||||||
|
public readonly operationService: FlowOperationBaseService;
|
||||||
|
|
||||||
|
/** 创建分组节点 */
|
||||||
|
public createGroup(nodes: FlowNodeEntity[]): FlowNodeEntity | undefined {
|
||||||
|
if (!nodes || !Array.isArray(nodes) || nodes.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!FlowGroupUtils.validate(nodes)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const sortedNodes: FlowNodeEntity[] = nodes.sort((a, b) => a.index - b.index);
|
||||||
|
const fromNode = sortedNodes[0];
|
||||||
|
const groupId = `group_${nanoid(5)}`;
|
||||||
|
this.operationService.apply({
|
||||||
|
type: OperationType.createGroup,
|
||||||
|
value: {
|
||||||
|
targetId: fromNode.id,
|
||||||
|
groupId,
|
||||||
|
nodeIds: nodes.map((node) => node.id),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const groupNode = this.entityManager.getEntityById<FlowNodeEntity>(groupId);
|
||||||
|
if (!groupNode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const group = this.groupController(groupNode);
|
||||||
|
if (!group) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
group.expand();
|
||||||
|
return groupNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除分组 */
|
||||||
|
public deleteGroup(groupNode: FlowNodeEntity): void {
|
||||||
|
const json = groupNode.toJSON();
|
||||||
|
if (!groupNode.pre || !json) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.operationService.apply({
|
||||||
|
type: OperationType.deleteNodes,
|
||||||
|
value: {
|
||||||
|
fromId: groupNode.pre.id,
|
||||||
|
nodes: [json],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 取消分组 */
|
||||||
|
public ungroup(groupNode: FlowNodeEntity): void {
|
||||||
|
const group = this.groupController(groupNode);
|
||||||
|
if (!group) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const nodes = group.nodes;
|
||||||
|
if (!groupNode.pre) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
group.collapse();
|
||||||
|
this.operationService.apply({
|
||||||
|
type: OperationType.ungroup,
|
||||||
|
value: {
|
||||||
|
groupId: groupNode.id,
|
||||||
|
targetId: groupNode.pre.id,
|
||||||
|
nodeIds: nodes.map((node) => node.id),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 返回所有分组节点 */
|
||||||
|
public getAllGroups(): FlowGroupController[] {
|
||||||
|
const allNodes = this.entityManager.getEntities<FlowNodeEntity>(FlowNodeEntity);
|
||||||
|
const groupNodes = allNodes.filter((node) => node.flowNodeType === FlowNodeBaseType.GROUP);
|
||||||
|
return groupNodes
|
||||||
|
.map((node) => this.groupController(node))
|
||||||
|
.filter(Boolean) as FlowGroupController[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取分组控制器*/
|
||||||
|
public groupController(group: FlowNodeEntity): FlowGroupController | undefined {
|
||||||
|
return FlowGroupController.create(group);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static validate(nodes: FlowNodeEntity[]): boolean {
|
||||||
|
return FlowGroupUtils.validate(nodes);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,99 @@
|
|||||||
|
import { FlowNodeBaseType } from '../../typings';
|
||||||
|
import { FlowNodeEntity } from '../../entities';
|
||||||
|
import { FlowGroupController } from './flow-group-controller';
|
||||||
|
|
||||||
|
export namespace FlowGroupUtils {
|
||||||
|
/** 找到节点所有上级 */
|
||||||
|
const findNodeParents = (node: FlowNodeEntity): FlowNodeEntity[] => {
|
||||||
|
const parents = [];
|
||||||
|
let parent = node.parent;
|
||||||
|
while (parent) {
|
||||||
|
parents.push(parent);
|
||||||
|
parent = parent.parent;
|
||||||
|
}
|
||||||
|
return parents;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 节点是否处于分组中 */
|
||||||
|
const isNodeInGroup = (node: FlowNodeEntity): boolean => {
|
||||||
|
// 处于分组中
|
||||||
|
if (node?.parent?.flowNodeType === FlowNodeBaseType.GROUP) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 判断节点能否组成分组 */
|
||||||
|
export const validate = (nodes: FlowNodeEntity[]): boolean => {
|
||||||
|
if (!nodes || !Array.isArray(nodes) || nodes.length === 0) {
|
||||||
|
// 参数不合法
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判断是否有分组节点
|
||||||
|
const isGroupRelatedNode = nodes.some((node) => isGroupNode(node));
|
||||||
|
if (isGroupRelatedNode) return false;
|
||||||
|
|
||||||
|
// 判断是否有节点已经处于分组中
|
||||||
|
const hasGroup = nodes.some((node) => node && isNodeInGroup(node));
|
||||||
|
if (hasGroup) return false;
|
||||||
|
|
||||||
|
// 判断是否来自同一个父亲
|
||||||
|
const parent = nodes[0].parent;
|
||||||
|
const isSameParent = nodes.every((node) => node.parent === parent);
|
||||||
|
if (!isSameParent) return false;
|
||||||
|
|
||||||
|
// 判断节点索引是否连续
|
||||||
|
const indexes = nodes.map((node) => node.index).sort((a, b) => a - b);
|
||||||
|
const isIndexContinuous = indexes.every((index, i, arr) => {
|
||||||
|
if (i === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return index === arr[i - 1] + 1;
|
||||||
|
});
|
||||||
|
if (!isIndexContinuous) return false;
|
||||||
|
|
||||||
|
// 判断节点父亲是否已经在分组中
|
||||||
|
const parents = findNodeParents(nodes[0]);
|
||||||
|
const parentsInGroup = parents.some((parent) => isNodeInGroup(parent));
|
||||||
|
if (parentsInGroup) return false;
|
||||||
|
|
||||||
|
// 参数正确
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 获取节点分组控制 */
|
||||||
|
export const getNodeGroupController = (
|
||||||
|
node?: FlowNodeEntity
|
||||||
|
): FlowGroupController | undefined => {
|
||||||
|
if (!node) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!isNodeInGroup(node)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const groupNode = node?.parent;
|
||||||
|
return FlowGroupController.create(groupNode);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 向上递归查找分组递归控制 */
|
||||||
|
export const getNodeRecursionGroupController = (
|
||||||
|
node?: FlowNodeEntity
|
||||||
|
): FlowGroupController | undefined => {
|
||||||
|
if (!node) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const group = getNodeGroupController(node);
|
||||||
|
if (group) {
|
||||||
|
return group;
|
||||||
|
}
|
||||||
|
if (node.parent) {
|
||||||
|
return getNodeRecursionGroupController(node.parent);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 是否分组节点 */
|
||||||
|
export const isGroupNode = (group: FlowNodeEntity): boolean =>
|
||||||
|
group.flowNodeType === FlowNodeBaseType.GROUP;
|
||||||
|
}
|
||||||
@ -0,0 +1,2 @@
|
|||||||
|
export { FlowGroupController } from './flow-group-controller';
|
||||||
|
export { FlowGroupService } from './flow-group-service';
|
||||||
@ -1,4 +1,11 @@
|
|||||||
import { inject, injectable } from 'inversify';
|
import { inject, injectable } from 'inversify';
|
||||||
|
import {
|
||||||
|
type IPoint,
|
||||||
|
PaddingSchema,
|
||||||
|
Rectangle,
|
||||||
|
type ScrollSchema,
|
||||||
|
SizeSchema,
|
||||||
|
} from '@flowgram.ai/utils';
|
||||||
import {
|
import {
|
||||||
type FlowDocument,
|
type FlowDocument,
|
||||||
type FlowLayout,
|
type FlowLayout,
|
||||||
@ -7,13 +14,6 @@ import {
|
|||||||
FlowNodeTransformData,
|
FlowNodeTransformData,
|
||||||
} from '@flowgram.ai/document';
|
} from '@flowgram.ai/document';
|
||||||
import { PlaygroundConfigEntity, TransformData } from '@flowgram.ai/core';
|
import { PlaygroundConfigEntity, TransformData } from '@flowgram.ai/core';
|
||||||
import {
|
|
||||||
type IPoint,
|
|
||||||
PaddingSchema,
|
|
||||||
Rectangle,
|
|
||||||
type ScrollSchema,
|
|
||||||
SizeSchema,
|
|
||||||
} from '@flowgram.ai/utils';
|
|
||||||
|
|
||||||
export const FREE_LAYOUT_KEY = 'free-layout';
|
export const FREE_LAYOUT_KEY = 'free-layout';
|
||||||
/**
|
/**
|
||||||
@ -64,6 +64,22 @@ export class FreeLayout implements FlowLayout {
|
|||||||
parentTransform.transform.fireChange();
|
parentTransform.transform.fireChange();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新所有受影响的上下游节点
|
||||||
|
*/
|
||||||
|
updateAffectedTransform(node: FlowNodeEntity): void {
|
||||||
|
const transformData = node.transform;
|
||||||
|
if (!transformData.localDirty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const allParents = this.getAllParents(node);
|
||||||
|
const allBlocks = this.getAllBlocks(node).reverse();
|
||||||
|
const affectedNodes = [...allBlocks, ...allParents];
|
||||||
|
affectedNodes.forEach((node) => {
|
||||||
|
this.fireChange(node);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取节点的 padding 数据
|
* 获取节点的 padding 数据
|
||||||
* @param node
|
* @param node
|
||||||
@ -83,7 +99,7 @@ export class FreeLayout implements FlowLayout {
|
|||||||
*/
|
*/
|
||||||
getInitScroll(contentSize: SizeSchema): ScrollSchema {
|
getInitScroll(contentSize: SizeSchema): ScrollSchema {
|
||||||
const bounds = Rectangle.enlarge(
|
const bounds = Rectangle.enlarge(
|
||||||
this.document.getAllNodes().map(node => node.getData<TransformData>(TransformData).bounds),
|
this.document.getAllNodes().map((node) => node.getData<TransformData>(TransformData).bounds)
|
||||||
).pad(30, 30); // 留出 30 像素的边界
|
).pad(30, 30); // 留出 30 像素的边界
|
||||||
const viewport = this.playgroundConfig.getViewport(false);
|
const viewport = this.playgroundConfig.getViewport(false);
|
||||||
const zoom = SizeSchema.fixSize(bounds, viewport);
|
const zoom = SizeSchema.fixSize(bounds, viewport);
|
||||||
@ -113,4 +129,33 @@ export class FreeLayout implements FlowLayout {
|
|||||||
getDefaultNodeOrigin(): IPoint {
|
getDefaultNodeOrigin(): IPoint {
|
||||||
return { x: 0.5, y: 0 };
|
return { x: 0.5, y: 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getAllParents(node: FlowNodeEntity): FlowNodeEntity[] {
|
||||||
|
const parents: FlowNodeEntity[] = [];
|
||||||
|
let current = node.parent;
|
||||||
|
|
||||||
|
while (current) {
|
||||||
|
parents.push(current);
|
||||||
|
current = current.parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
return parents;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getAllBlocks(node: FlowNodeEntity): FlowNodeEntity[] {
|
||||||
|
return node.blocks.reduce<FlowNodeEntity[]>(
|
||||||
|
(acc, child) => [...acc, ...this.getAllBlocks(child)],
|
||||||
|
[node]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private fireChange(node?: FlowNodeEntity): void {
|
||||||
|
const transformData = node?.transform;
|
||||||
|
if (!node || !transformData?.localDirty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
node.clearMemoGlobal();
|
||||||
|
node.clearMemoLocal();
|
||||||
|
transformData.transform.fireChange();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -176,17 +176,10 @@ export class WorkflowDragService {
|
|||||||
x: nodeStartPosition.x + offset.x,
|
x: nodeStartPosition.x + offset.x,
|
||||||
y: nodeStartPosition.y + offset.y,
|
y: nodeStartPosition.y + offset.y,
|
||||||
};
|
};
|
||||||
if (node.collapsedChildren?.length > 0) {
|
|
||||||
// 嵌套情况下需将子节点 transform 设为 dirty
|
|
||||||
node.collapsedChildren.forEach((childNode) => {
|
|
||||||
const childNodeTransformData =
|
|
||||||
childNode.getData<FlowNodeTransformData>(FlowNodeTransformData);
|
|
||||||
childNodeTransformData.fireChange();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
transform.update({
|
transform.update({
|
||||||
position: newPosition,
|
position: newPosition,
|
||||||
});
|
});
|
||||||
|
this.document.layout.updateAffectedTransform(node);
|
||||||
positions.push(newPosition);
|
positions.push(newPosition);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { inject, injectable } from 'inversify';
|
import { inject, injectable } from 'inversify';
|
||||||
import { EntityManager } from '@flowgram.ai/core';
|
|
||||||
import { Emitter, type IPoint } from '@flowgram.ai/utils';
|
import { Emitter, type IPoint } from '@flowgram.ai/utils';
|
||||||
|
import { EntityManager } from '@flowgram.ai/core';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
type WorkflowLineEntity,
|
type WorkflowLineEntity,
|
||||||
@ -11,7 +11,10 @@ import {
|
|||||||
/**
|
/**
|
||||||
* 可 Hover 的节点 类型
|
* 可 Hover 的节点 类型
|
||||||
*/
|
*/
|
||||||
export type WorkfloEntityHoverable = WorkflowNodeEntity | WorkflowLineEntity | WorkflowPortEntity;
|
export type WorkflowEntityHoverable = WorkflowNodeEntity | WorkflowLineEntity | WorkflowPortEntity;
|
||||||
|
|
||||||
|
/** @deprecated */
|
||||||
|
export type WorkfloEntityHoverable = WorkflowEntityHoverable;
|
||||||
/**
|
/**
|
||||||
* hover 状态管理
|
* hover 状态管理
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -1,6 +1,9 @@
|
|||||||
import { FlowNodeTransformData, type FlowNodeEntity } from '@flowgram.ai/document';
|
|
||||||
import { TransformData, startTween } from '@flowgram.ai/core';
|
|
||||||
import { type IPoint } from '@flowgram.ai/utils';
|
import { type IPoint } from '@flowgram.ai/utils';
|
||||||
|
import { FlowNodeTransformData } from '@flowgram.ai/document';
|
||||||
|
import { TransformData, startTween } from '@flowgram.ai/core';
|
||||||
|
|
||||||
|
import { type WorkflowDocument } from '../workflow-document';
|
||||||
|
import { type WorkflowNodeEntity } from '../entities';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Coze 中节点坐标,以卡片顶部中间为原点。
|
* Coze 中节点坐标,以卡片顶部中间为原点。
|
||||||
@ -9,12 +12,12 @@ import { type IPoint } from '@flowgram.ai/utils';
|
|||||||
* bounds 的原点坐标为左上角。
|
* bounds 的原点坐标为左上角。
|
||||||
*/
|
*/
|
||||||
export const layoutToPositions = async (
|
export const layoutToPositions = async (
|
||||||
nodes: FlowNodeEntity[],
|
nodes: WorkflowNodeEntity[],
|
||||||
nodePositionMap: Record<string, IPoint>,
|
nodePositionMap: Record<string, IPoint>
|
||||||
): Promise<Record<string, IPoint>> => {
|
): Promise<Record<string, IPoint>> => {
|
||||||
// 缓存上次位置,用来还原位置
|
// 缓存上次位置,用来还原位置
|
||||||
const newNodePositionMap: Record<string, IPoint> = {};
|
const newNodePositionMap: Record<string, IPoint> = {};
|
||||||
nodes.forEach(node => {
|
nodes.forEach((node) => {
|
||||||
const transform = node.getData(TransformData);
|
const transform = node.getData(TransformData);
|
||||||
const nodeTransform = node.getData(FlowNodeTransformData);
|
const nodeTransform = node.getData(FlowNodeTransformData);
|
||||||
|
|
||||||
@ -24,13 +27,13 @@ export const layoutToPositions = async (
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
return new Promise(resolve => {
|
return new Promise((resolve) => {
|
||||||
startTween({
|
startTween({
|
||||||
from: { d: 0 },
|
from: { d: 0 },
|
||||||
to: { d: 100 },
|
to: { d: 100 },
|
||||||
duration: 300,
|
duration: 300,
|
||||||
onUpdate: v => {
|
onUpdate: (v) => {
|
||||||
nodes.forEach(node => {
|
nodes.forEach((node) => {
|
||||||
const transform = node.getData(TransformData);
|
const transform = node.getData(TransformData);
|
||||||
const deltaX = ((nodePositionMap[node.id].x - transform.position.x) * v.d) / 100;
|
const deltaX = ((nodePositionMap[node.id].x - transform.position.x) * v.d) / 100;
|
||||||
const deltaY =
|
const deltaY =
|
||||||
@ -38,21 +41,15 @@ export const layoutToPositions = async (
|
|||||||
v.d) /
|
v.d) /
|
||||||
100;
|
100;
|
||||||
|
|
||||||
if (node.collapsedChildren?.length > 0) {
|
|
||||||
// 嵌套情况下需将子节点 transform 设为 dirty
|
|
||||||
node.collapsedChildren.forEach(childNode => {
|
|
||||||
const childNodeTransformData =
|
|
||||||
childNode.getData<FlowNodeTransformData>(FlowNodeTransformData);
|
|
||||||
childNodeTransformData.fireChange();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
transform.update({
|
transform.update({
|
||||||
position: {
|
position: {
|
||||||
x: transform.position.x + deltaX,
|
x: transform.position.x + deltaX,
|
||||||
y: transform.position.y + deltaY,
|
y: transform.position.y + deltaY,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const document = node.document as WorkflowDocument;
|
||||||
|
document.layout.updateAffectedTransform(node);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onComplete: () => {
|
onComplete: () => {
|
||||||
|
|||||||
@ -280,6 +280,14 @@ export class WorkflowDocument extends FlowDocument {
|
|||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get layout(): FreeLayout {
|
||||||
|
const layout = this.layouts.find((layout) => layout.name == this.currentLayoutKey);
|
||||||
|
if (!layout) {
|
||||||
|
throw new Error(`Unknown flow layout: ${this.currentLayoutKey}`);
|
||||||
|
}
|
||||||
|
return layout as FreeLayout;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取默认的 x y 坐标, 默认为当前画布可视区域中心
|
* 获取默认的 x y 坐标, 默认为当前画布可视区域中心
|
||||||
* @param type
|
* @param type
|
||||||
|
|||||||
@ -222,6 +222,9 @@ export function createFreeLayoutPreset(
|
|||||||
// 2. 如存在自定义配置,以配置为准
|
// 2. 如存在自定义配置,以配置为准
|
||||||
const element = e.target as Element;
|
const element = e.target as Element;
|
||||||
if (element) {
|
if (element) {
|
||||||
|
if (element.classList.contains('gedit-flow-background-layer')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (element.closest('[data-flow-editor-selectable="true"]')) {
|
if (element.closest('[data-flow-editor-selectable="true"]')) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { FlowNodeTransformData } from '@flowgram.ai/document';
|
import { WorkflowDocument } from '@flowgram.ai/free-layout-core';
|
||||||
import { PositionSchema, startTween, TransformData } from '@flowgram.ai/core';
|
import { PositionSchema, startTween, TransformData } from '@flowgram.ai/core';
|
||||||
|
|
||||||
import { LayoutNode } from './type';
|
import { LayoutNode } from './type';
|
||||||
@ -35,20 +35,13 @@ export class LayoutPosition {
|
|||||||
const deltaX = ((position.x - transform.position.x) * step) / 100;
|
const deltaX = ((position.x - transform.position.x) * step) / 100;
|
||||||
const deltaY = ((position.y - transform.bounds.height / 2 - transform.position.y) * step) / 100;
|
const deltaY = ((position.y - transform.bounds.height / 2 - transform.position.y) * step) / 100;
|
||||||
|
|
||||||
if (layoutNode.hasChildren) {
|
|
||||||
// 嵌套情况下需将子节点 transform 设为 dirty
|
|
||||||
layoutNode.entity.collapsedChildren.forEach((childNode) => {
|
|
||||||
const childNodeTransformData =
|
|
||||||
childNode.getData<FlowNodeTransformData>(FlowNodeTransformData);
|
|
||||||
childNodeTransformData.fireChange();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
transform.update({
|
transform.update({
|
||||||
position: {
|
position: {
|
||||||
x: transform.position.x + deltaX,
|
x: transform.position.x + deltaX,
|
||||||
y: transform.position.y + deltaY,
|
y: transform.position.y + deltaY,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
const document = layoutNode.entity.document as WorkflowDocument;
|
||||||
|
document.layout.updateAffectedTransform(layoutNode.entity);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import {
|
|||||||
WorkflowNodeEntity,
|
WorkflowNodeEntity,
|
||||||
WorkflowNodeLinesData,
|
WorkflowNodeLinesData,
|
||||||
} from '@flowgram.ai/free-layout-core';
|
} from '@flowgram.ai/free-layout-core';
|
||||||
|
import { FlowNodeBaseType } from '@flowgram.ai/document';
|
||||||
|
|
||||||
import { Layout, type LayoutOptions } from './layout';
|
import { Layout, type LayoutOptions } from './layout';
|
||||||
|
|
||||||
@ -17,13 +18,13 @@ export class AutoLayoutService {
|
|||||||
|
|
||||||
private async layoutNode(node: WorkflowNodeEntity, options: LayoutOptions): Promise<void> {
|
private async layoutNode(node: WorkflowNodeEntity, options: LayoutOptions): Promise<void> {
|
||||||
// 获取子节点
|
// 获取子节点
|
||||||
const nodes = node.collapsedChildren;
|
const nodes = this.getAvailableBlocks(node);
|
||||||
if (!nodes || !Array.isArray(nodes) || !nodes.length) {
|
if (!nodes || !Array.isArray(nodes) || !nodes.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取子线条
|
// 获取子线条
|
||||||
const edges = node.collapsedChildren
|
const edges = node.blocks
|
||||||
.map((child) => {
|
.map((child) => {
|
||||||
const childLinesData = child.getData<WorkflowNodeLinesData>(WorkflowNodeLinesData);
|
const childLinesData = child.getData<WorkflowNodeLinesData>(WorkflowNodeLinesData);
|
||||||
return childLinesData.outputLines.filter(Boolean);
|
return childLinesData.outputLines.filter(Boolean);
|
||||||
@ -38,4 +39,18 @@ export class AutoLayoutService {
|
|||||||
layout.layout();
|
layout.layout();
|
||||||
await layout.position();
|
await layout.position();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getAvailableBlocks(node: WorkflowNodeEntity): WorkflowNodeEntity[] {
|
||||||
|
const commonNodes = node.blocks.filter((n) => !this.shouldFlatNode(n));
|
||||||
|
const flatNodes = node.blocks
|
||||||
|
.filter((n) => this.shouldFlatNode(n))
|
||||||
|
.map((flatNode) => flatNode.blocks)
|
||||||
|
.flat();
|
||||||
|
return [...commonNodes, ...flatNodes];
|
||||||
|
}
|
||||||
|
|
||||||
|
private shouldFlatNode(node: WorkflowNodeEntity): boolean {
|
||||||
|
// Group 节点不参与自动布局
|
||||||
|
return node.flowNodeType === FlowNodeBaseType.GROUP;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -30,6 +30,7 @@
|
|||||||
"@flowgram.ai/free-lines-plugin": "workspace:*",
|
"@flowgram.ai/free-lines-plugin": "workspace:*",
|
||||||
"@flowgram.ai/core": "workspace:*",
|
"@flowgram.ai/core": "workspace:*",
|
||||||
"@flowgram.ai/document": "workspace:*",
|
"@flowgram.ai/document": "workspace:*",
|
||||||
|
"@flowgram.ai/shortcuts-plugin": "workspace:*",
|
||||||
"@flowgram.ai/free-layout-core": "workspace:*",
|
"@flowgram.ai/free-layout-core": "workspace:*",
|
||||||
"@flowgram.ai/renderer": "workspace:*",
|
"@flowgram.ai/renderer": "workspace:*",
|
||||||
"@flowgram.ai/utils": "workspace:*",
|
"@flowgram.ai/utils": "workspace:*",
|
||||||
|
|||||||
@ -1,4 +1,2 @@
|
|||||||
export * from './node-into-container';
|
export * from './node-into-container';
|
||||||
export * from './sub-canvas';
|
export * from './sub-canvas';
|
||||||
export { createContainerNodePlugin } from './plugin';
|
|
||||||
export type { WorkflowContainerPluginOptions } from './type';
|
|
||||||
|
|||||||
@ -1,3 +1,8 @@
|
|||||||
export { NodeIntoContainerType } from './constant';
|
export { NodeIntoContainerType } from './constant';
|
||||||
export { NodeIntoContainerService } from './service';
|
export { NodeIntoContainerService } from './service';
|
||||||
export { NodeIntoContainerState, NodeIntoContainerEvent } from './type';
|
export {
|
||||||
|
NodeIntoContainerState,
|
||||||
|
NodeIntoContainerEvent,
|
||||||
|
WorkflowContainerPluginOptions,
|
||||||
|
} from './type';
|
||||||
|
export { createContainerNodePlugin } from './plugin';
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { definePluginCreator } from '@flowgram.ai/core';
|
import { definePluginCreator } from '@flowgram.ai/core';
|
||||||
|
|
||||||
import type { WorkflowContainerPluginOptions } from './type';
|
import type { WorkflowContainerPluginOptions } from './type';
|
||||||
import { NodeIntoContainerService } from './node-into-container';
|
import { NodeIntoContainerService } from '.';
|
||||||
|
|
||||||
export const createContainerNodePlugin = definePluginCreator<WorkflowContainerPluginOptions>({
|
export const createContainerNodePlugin = definePluginCreator<WorkflowContainerPluginOptions>({
|
||||||
onBind: ({ bind }) => {
|
onBind: ({ bind }) => {
|
||||||
@ -7,6 +7,7 @@ import {
|
|||||||
type Disposable,
|
type Disposable,
|
||||||
DisposableCollection,
|
DisposableCollection,
|
||||||
Emitter,
|
Emitter,
|
||||||
|
type IPoint,
|
||||||
} from '@flowgram.ai/utils';
|
} from '@flowgram.ai/utils';
|
||||||
import {
|
import {
|
||||||
type NodesDragEvent,
|
type NodesDragEvent,
|
||||||
@ -19,7 +20,7 @@ import {
|
|||||||
WorkflowSelectService,
|
WorkflowSelectService,
|
||||||
} from '@flowgram.ai/free-layout-core';
|
} from '@flowgram.ai/free-layout-core';
|
||||||
import { HistoryService } from '@flowgram.ai/free-history-plugin';
|
import { HistoryService } from '@flowgram.ai/free-history-plugin';
|
||||||
import { FlowNodeTransformData, FlowNodeRenderData } from '@flowgram.ai/document';
|
import { FlowNodeTransformData, FlowNodeRenderData, FlowNodeBaseType } from '@flowgram.ai/document';
|
||||||
import { PlaygroundConfigEntity, TransformData } from '@flowgram.ai/core';
|
import { PlaygroundConfigEntity, TransformData } from '@flowgram.ai/core';
|
||||||
|
|
||||||
import type { NodeIntoContainerEvent, NodeIntoContainerState } from './type';
|
import type { NodeIntoContainerEvent, NodeIntoContainerState } from './type';
|
||||||
@ -68,24 +69,12 @@ export class NodeIntoContainerService {
|
|||||||
this.toDispose.dispose();
|
this.toDispose.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 移除节点连线 */
|
|
||||||
public async removeNodeLines(node: WorkflowNodeEntity): Promise<void> {
|
|
||||||
const lines = this.linesManager.getAllLines();
|
|
||||||
lines.forEach((line) => {
|
|
||||||
if (line.from.id !== node.id && line.to?.id !== node.id) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
line.dispose();
|
|
||||||
});
|
|
||||||
await this.nextFrame();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 将节点移出容器 */
|
/** 将节点移出容器 */
|
||||||
public async moveOutContainer(params: { node: WorkflowNodeEntity }): Promise<void> {
|
public async moveOutContainer(params: { node: WorkflowNodeEntity }): Promise<void> {
|
||||||
const { node } = params;
|
const { node } = params;
|
||||||
const parentNode = node.parent;
|
const parentNode = node.parent;
|
||||||
const containerNode = parentNode?.parent;
|
const containerNode = parentNode?.parent;
|
||||||
const nodeJSON = await this.document.toNodeJSON(node);
|
const nodeJSON = this.document.toNodeJSON(node);
|
||||||
if (
|
if (
|
||||||
!parentNode ||
|
!parentNode ||
|
||||||
!containerNode ||
|
!containerNode ||
|
||||||
@ -130,6 +119,29 @@ export class NodeIntoContainerService {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 移除节点所有非法连线 */
|
||||||
|
public async clearInvalidLines(params: {
|
||||||
|
dragNode?: WorkflowNodeEntity;
|
||||||
|
sourceParent?: WorkflowNodeEntity;
|
||||||
|
}): Promise<void> {
|
||||||
|
const { dragNode, sourceParent } = params;
|
||||||
|
if (!dragNode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (dragNode.parent === sourceParent) {
|
||||||
|
// 容器节点未改变
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
dragNode.parent?.flowNodeType === FlowNodeBaseType.GROUP ||
|
||||||
|
sourceParent?.flowNodeType === FlowNodeBaseType.GROUP
|
||||||
|
) {
|
||||||
|
// 移入移出 group 节点无需删除节点
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await this.removeNodeLines(dragNode);
|
||||||
|
}
|
||||||
|
|
||||||
/** 初始化状态 */
|
/** 初始化状态 */
|
||||||
private initState(): void {
|
private initState(): void {
|
||||||
this.state = {
|
this.state = {
|
||||||
@ -175,7 +187,10 @@ export class NodeIntoContainerService {
|
|||||||
throttledDraggingNode.cancel();
|
throttledDraggingNode.cancel();
|
||||||
draggingNode(event); // 直接触发一次计算,防止延迟
|
draggingNode(event); // 直接触发一次计算,防止延迟
|
||||||
await this.dropNodeToContainer(); // 放置节点
|
await this.dropNodeToContainer(); // 放置节点
|
||||||
await this.clearInvalidLines(); // 清除非法线条
|
await this.clearInvalidLines({
|
||||||
|
dragNode: this.state.dragNode,
|
||||||
|
sourceParent: this.state.sourceParent,
|
||||||
|
}); // 清除非法线条
|
||||||
this.setDropNode(undefined);
|
this.setDropNode(undefined);
|
||||||
this.initState(); // 重置状态
|
this.initState(); // 重置状态
|
||||||
this.historyService.endTransaction(); // 结束合并历史记录
|
this.historyService.endTransaction(); // 结束合并历史记录
|
||||||
@ -201,18 +216,11 @@ export class NodeIntoContainerService {
|
|||||||
this.dragService.startDragSelectedNodes(event.triggerEvent);
|
this.dragService.startDragSelectedNodes(event.triggerEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 移除节点所有非法连线 */
|
/** 移除节点连线 */
|
||||||
private async clearInvalidLines(): Promise<void> {
|
private async removeNodeLines(node: WorkflowNodeEntity): Promise<void> {
|
||||||
const { dragNode, sourceParent } = this.state;
|
|
||||||
if (!dragNode) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (dragNode.parent === sourceParent) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const lines = this.linesManager.getAllLines();
|
const lines = this.linesManager.getAllLines();
|
||||||
lines.forEach((line) => {
|
lines.forEach((line) => {
|
||||||
if (line.from.id !== dragNode.id && line.to?.id !== dragNode.id) {
|
if (line.from.id !== node.id && line.to?.id !== node.id) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
line.dispose();
|
line.dispose();
|
||||||
@ -277,15 +285,21 @@ export class NodeIntoContainerService {
|
|||||||
/** 获取容器节点transforms */
|
/** 获取容器节点transforms */
|
||||||
private getContainerTransforms(): FlowNodeTransformData[] {
|
private getContainerTransforms(): FlowNodeTransformData[] {
|
||||||
return this.document
|
return this.document
|
||||||
.getRenderDatas(FlowNodeTransformData, false)
|
.getAllNodes()
|
||||||
.filter((transform) => {
|
.filter((node) => {
|
||||||
const { entity } = transform;
|
if (node.originParent) {
|
||||||
if (entity.originParent) {
|
return node.getNodeMeta().selectable && node.originParent.getNodeMeta().selectable;
|
||||||
return entity.getNodeMeta().selectable && entity.originParent.getNodeMeta().selectable;
|
|
||||||
}
|
}
|
||||||
return entity.getNodeMeta().selectable;
|
return node.getNodeMeta().selectable;
|
||||||
})
|
})
|
||||||
.filter((transform) => this.isContainer(transform.entity));
|
.filter((node) => this.isContainer(node))
|
||||||
|
.sort((a, b) => {
|
||||||
|
const aIndex = a.renderData.stackIndex;
|
||||||
|
const bIndex = b.renderData.stackIndex;
|
||||||
|
// 确保层级高的节点在前面
|
||||||
|
return bIndex - aIndex;
|
||||||
|
})
|
||||||
|
.map((node) => node.transform);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 放置节点到容器 */
|
/** 放置节点到容器 */
|
||||||
@ -302,27 +316,66 @@ export class NodeIntoContainerService {
|
|||||||
|
|
||||||
/** 拖拽节点 */
|
/** 拖拽节点 */
|
||||||
private draggingNode(nodeDragEvent: NodesDragEvent): void {
|
private draggingNode(nodeDragEvent: NodesDragEvent): void {
|
||||||
const { dragNode, isDraggingNode, transforms } = this.state;
|
const { dragNode, isDraggingNode, transforms = [] } = this.state;
|
||||||
if (!isDraggingNode || !dragNode || this.isContainer(dragNode) || !transforms?.length) {
|
if (!isDraggingNode || !dragNode || !transforms?.length) {
|
||||||
return this.setDropNode(undefined);
|
return this.setDropNode(undefined);
|
||||||
}
|
}
|
||||||
const mousePos = this.playgroundConfig.getPosFromMouseEvent(nodeDragEvent.dragEvent);
|
const mousePos = this.playgroundConfig.getPosFromMouseEvent(nodeDragEvent.dragEvent);
|
||||||
|
const availableTransforms = transforms.filter(
|
||||||
|
(transform) => transform.entity.id !== dragNode.id
|
||||||
|
);
|
||||||
const collisionTransform = this.getCollisionTransform({
|
const collisionTransform = this.getCollisionTransform({
|
||||||
targetPoint: mousePos,
|
targetPoint: mousePos,
|
||||||
transforms: this.state.transforms ?? [],
|
transforms: availableTransforms,
|
||||||
});
|
});
|
||||||
const dropNode = collisionTransform?.entity;
|
const dropNode = collisionTransform?.entity;
|
||||||
if (!dropNode || dragNode.parent?.id === dropNode.id) {
|
const canDrop = this.canDropToContainer({
|
||||||
|
dragNode,
|
||||||
|
dropNode,
|
||||||
|
});
|
||||||
|
if (!canDrop) {
|
||||||
return this.setDropNode(undefined);
|
return this.setDropNode(undefined);
|
||||||
}
|
}
|
||||||
|
return this.setDropNode(dropNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 判断能否将节点拖入容器 */
|
||||||
|
protected canDropToContainer(params: {
|
||||||
|
dragNode: WorkflowNodeEntity;
|
||||||
|
dropNode?: WorkflowNodeEntity;
|
||||||
|
}): boolean {
|
||||||
|
const { dragNode, dropNode } = params;
|
||||||
|
const isDropContainer = dropNode?.getNodeMeta<WorkflowNodeMeta>().isContainer;
|
||||||
|
if (!dropNode || !isDropContainer || this.isParent(dragNode, dropNode)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
dragNode.flowNodeType === FlowNodeBaseType.GROUP &&
|
||||||
|
dropNode.flowNodeType !== FlowNodeBaseType.GROUP
|
||||||
|
) {
|
||||||
|
// 禁止将 group 节点拖入非 group 节点(由于目前不支持多节点拖入容器,无法计算有效线条,因此进行屏蔽)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
const canDrop = this.dragService.canDropToNode({
|
const canDrop = this.dragService.canDropToNode({
|
||||||
dragNodeType: dragNode.flowNodeType,
|
dragNodeType: dragNode.flowNodeType,
|
||||||
dropNode,
|
dropNode,
|
||||||
});
|
});
|
||||||
if (!canDrop.allowDrop) {
|
if (!canDrop.allowDrop) {
|
||||||
return this.setDropNode(undefined);
|
return false;
|
||||||
}
|
}
|
||||||
return this.setDropNode(canDrop.dropNode);
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 判断一个节点是否为另一个节点的父节点(向上查找直到根节点) */
|
||||||
|
private isParent(node: WorkflowNodeEntity, parent: WorkflowNodeEntity): boolean {
|
||||||
|
let current = node.parent;
|
||||||
|
while (current) {
|
||||||
|
if (current.id === parent.id) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
current = current.parent;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 将节点移入容器 */
|
/** 将节点移入容器 */
|
||||||
@ -332,20 +385,12 @@ export class NodeIntoContainerService {
|
|||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
const { node, containerNode } = params;
|
const { node, containerNode } = params;
|
||||||
const parentNode = node.parent;
|
const parentNode = node.parent;
|
||||||
const nodeJSON = await this.document.toNodeJSON(node);
|
|
||||||
|
|
||||||
this.operationService.moveNode(node, {
|
this.operationService.moveNode(node, {
|
||||||
parent: containerNode,
|
parent: containerNode,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.operationService.updateNodePosition(
|
this.operationService.updateNodePosition(node, this.adjustSubNodePosition(node, containerNode));
|
||||||
node,
|
|
||||||
this.dragService.adjustSubNodePosition(
|
|
||||||
nodeJSON.type as string,
|
|
||||||
containerNode,
|
|
||||||
nodeJSON.meta!.position
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
await this.nextFrame();
|
await this.nextFrame();
|
||||||
|
|
||||||
@ -357,6 +402,38 @@ export class NodeIntoContainerService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 如果存在容器节点,且传入鼠标坐标,需要用容器的坐标减去传入的鼠标坐标
|
||||||
|
*/
|
||||||
|
private adjustSubNodePosition(
|
||||||
|
targetNode: WorkflowNodeEntity,
|
||||||
|
containerNode: WorkflowNodeEntity
|
||||||
|
): IPoint {
|
||||||
|
if (containerNode.flowNodeType === FlowNodeBaseType.ROOT) {
|
||||||
|
return targetNode.transform.position;
|
||||||
|
}
|
||||||
|
const nodeWorldTransform = targetNode.transform.transform.worldTransform;
|
||||||
|
const containerWorldTransform = containerNode.transform.transform.worldTransform;
|
||||||
|
const nodePosition = {
|
||||||
|
x: nodeWorldTransform.tx,
|
||||||
|
y: nodeWorldTransform.ty,
|
||||||
|
};
|
||||||
|
const isParentEmpty = !containerNode.children || containerNode.children.length === 0;
|
||||||
|
const containerPadding = this.document.layout.getPadding(containerNode);
|
||||||
|
if (isParentEmpty) {
|
||||||
|
// 确保空容器节点不偏移
|
||||||
|
return {
|
||||||
|
x: 0,
|
||||||
|
y: containerPadding.top,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
x: nodePosition.x - containerWorldTransform.tx,
|
||||||
|
y: nodePosition.y - containerWorldTransform.ty,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private isContainer(node?: WorkflowNodeEntity): boolean {
|
private isContainer(node?: WorkflowNodeEntity): boolean {
|
||||||
return node?.getNodeMeta<WorkflowNodeMeta>().isContainer ?? false;
|
return node?.getNodeMeta<WorkflowNodeMeta>().isContainer ?? false;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,3 +18,7 @@ export interface NodeIntoContainerEvent {
|
|||||||
sourceContainer?: WorkflowNodeEntity;
|
sourceContainer?: WorkflowNodeEntity;
|
||||||
targetContainer: WorkflowNodeEntity;
|
targetContainer: WorkflowNodeEntity;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface WorkflowContainerPluginOptions {
|
||||||
|
disableNodeIntoContainer?: boolean;
|
||||||
|
}
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
export { SubCanvasBackground } from './background';
|
export { SubCanvasBackground } from './background';
|
||||||
export { SubCanvasBorder } from './border';
|
export { SubCanvasBorder } from './border';
|
||||||
export { SubCanvasRender } from './render';
|
export { SubCanvasRender } from './render';
|
||||||
|
export { SubCanvasTips } from './tips';
|
||||||
|
|||||||
@ -1,11 +1,12 @@
|
|||||||
import React, { CSSProperties, useLayoutEffect, type FC } from 'react';
|
import React, { CSSProperties, type FC } from 'react';
|
||||||
|
|
||||||
import { useCurrentEntity } from '@flowgram.ai/free-layout-core';
|
import { useCurrentEntity } from '@flowgram.ai/free-layout-core';
|
||||||
|
|
||||||
import { SubCanvasRenderStyle } from './style';
|
import { SubCanvasRenderStyle } from './style';
|
||||||
|
import { SubCanvasTips } from '../tips';
|
||||||
import { SubCanvasBorder } from '../border';
|
import { SubCanvasBorder } from '../border';
|
||||||
import { SubCanvasBackground } from '../background';
|
import { SubCanvasBackground } from '../background';
|
||||||
import { useNodeSize } from '../../hooks';
|
import { useNodeSize, useSyncNodeRenderSize } from '../../hooks';
|
||||||
|
|
||||||
interface ISubCanvasBorder {
|
interface ISubCanvasBorder {
|
||||||
className?: string;
|
className?: string;
|
||||||
@ -15,14 +16,10 @@ interface ISubCanvasBorder {
|
|||||||
export const SubCanvasRender: FC<ISubCanvasBorder> = ({ className, style }) => {
|
export const SubCanvasRender: FC<ISubCanvasBorder> = ({ className, style }) => {
|
||||||
const node = useCurrentEntity();
|
const node = useCurrentEntity();
|
||||||
const nodeSize = useNodeSize();
|
const nodeSize = useNodeSize();
|
||||||
const { height, width } = nodeSize ?? {};
|
|
||||||
const nodeHeight = nodeSize?.height ?? 0;
|
const nodeHeight = nodeSize?.height ?? 0;
|
||||||
const { padding } = node.transform;
|
const { padding } = node.transform;
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useSyncNodeRenderSize(nodeSize);
|
||||||
node.renderData.node.style.width = width + 'px';
|
|
||||||
node.renderData.node.style.height = height + 'px';
|
|
||||||
}, [height, width]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SubCanvasRenderStyle
|
<SubCanvasRenderStyle
|
||||||
@ -38,6 +35,7 @@ export const SubCanvasRender: FC<ISubCanvasBorder> = ({ className, style }) => {
|
|||||||
>
|
>
|
||||||
<SubCanvasBorder>
|
<SubCanvasBorder>
|
||||||
<SubCanvasBackground />
|
<SubCanvasBackground />
|
||||||
|
<SubCanvasTips />
|
||||||
</SubCanvasBorder>
|
</SubCanvasBorder>
|
||||||
</SubCanvasRenderStyle>
|
</SubCanvasRenderStyle>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -0,0 +1,33 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/naming-convention -- no need */
|
||||||
|
|
||||||
|
const STORAGE_KEY = 'workflow-move-into-sub-canvas-tip-visible';
|
||||||
|
const STORAGE_VALUE = 'false';
|
||||||
|
|
||||||
|
export class TipsGlobalStore {
|
||||||
|
private static _instance?: TipsGlobalStore;
|
||||||
|
|
||||||
|
public static get instance(): TipsGlobalStore {
|
||||||
|
if (!this._instance) {
|
||||||
|
this._instance = new TipsGlobalStore();
|
||||||
|
}
|
||||||
|
return this._instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
private closed = false;
|
||||||
|
|
||||||
|
public isClosed(): boolean {
|
||||||
|
return this.isCloseForever() || this.closed;
|
||||||
|
}
|
||||||
|
|
||||||
|
public close(): void {
|
||||||
|
this.closed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public isCloseForever(): boolean {
|
||||||
|
return localStorage.getItem(STORAGE_KEY) === STORAGE_VALUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public closeForever(): void {
|
||||||
|
localStorage.setItem(STORAGE_KEY, STORAGE_VALUE);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export const IconClose = () => (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none" viewBox="0 0 16 16">
|
||||||
|
<path
|
||||||
|
fill="#060709"
|
||||||
|
fillOpacity="0.5"
|
||||||
|
d="M12.13 12.128a.5.5 0 0 0 .001-.706L8.71 8l3.422-3.423a.5.5 0 0 0-.001-.705.5.5 0 0 0-.706-.002L8.002 7.293 4.579 3.87a.5.5 0 0 0-.705.002.5.5 0 0 0-.002.705L7.295 8l-3.423 3.422a.5.5 0 0 0 .002.706c.195.195.51.197.705.001l3.423-3.422 3.422 3.422c.196.196.51.194.706-.001"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
@ -0,0 +1,37 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { useControlTips } from './use-control';
|
||||||
|
import { SubCanvasTipsStyle } from './style';
|
||||||
|
import { isMacOS } from './is-mac-os';
|
||||||
|
import { IconClose } from './icon-close';
|
||||||
|
|
||||||
|
export const SubCanvasTips = () => {
|
||||||
|
const { visible, close, closeForever } = useControlTips();
|
||||||
|
|
||||||
|
if (!visible) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<SubCanvasTipsStyle className={'sub-canvas-tips'}>
|
||||||
|
<div className="container">
|
||||||
|
<div className="content">
|
||||||
|
<p className="text">{`Hold ${isMacOS ? 'Cmd ⌘' : 'Ctrl'} to drag node out`}</p>
|
||||||
|
<div
|
||||||
|
className="space"
|
||||||
|
style={{
|
||||||
|
width: 0,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="actions">
|
||||||
|
<p className="close-forever" onClick={closeForever}>
|
||||||
|
Never Remind
|
||||||
|
</p>
|
||||||
|
<div className="close" onClick={close}>
|
||||||
|
<IconClose />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</SubCanvasTipsStyle>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export const isMacOS = /(Macintosh|MacIntel|MacPPC|Mac68K|iPad)/.test(navigator.userAgent);
|
||||||
@ -0,0 +1,70 @@
|
|||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
export const SubCanvasTipsStyle = styled.div`
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
height: 28px;
|
||||||
|
|
||||||
|
.container {
|
||||||
|
height: 100%;
|
||||||
|
background-color: #e4e6f5;
|
||||||
|
border-radius: 4px 4px 0 0;
|
||||||
|
|
||||||
|
.content {
|
||||||
|
overflow: hidden;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
.text {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 400;
|
||||||
|
font-style: normal;
|
||||||
|
line-height: 20px;
|
||||||
|
color: rgba(15, 21, 40, 82%);
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.space {
|
||||||
|
width: 128px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
height: 28px;
|
||||||
|
padding: 0 16px;
|
||||||
|
|
||||||
|
.close-forever {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
padding: 0 3px;
|
||||||
|
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 400;
|
||||||
|
font-style: normal;
|
||||||
|
line-height: 12px;
|
||||||
|
color: rgba(32, 41, 69, 62%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.close {
|
||||||
|
display: flex;
|
||||||
|
cursor: pointer;
|
||||||
|
height: 100%;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
@ -0,0 +1,64 @@
|
|||||||
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import { useCurrentEntity } from '@flowgram.ai/free-layout-core';
|
||||||
|
import { useService } from '@flowgram.ai/core';
|
||||||
|
|
||||||
|
import { NodeIntoContainerService, NodeIntoContainerType } from '../../../node-into-container';
|
||||||
|
import { TipsGlobalStore } from './global-store';
|
||||||
|
|
||||||
|
export const useControlTips = () => {
|
||||||
|
const node = useCurrentEntity();
|
||||||
|
const [visible, setVisible] = useState(false);
|
||||||
|
const globalStore = TipsGlobalStore.instance;
|
||||||
|
|
||||||
|
const nodeIntoContainerService = useService<NodeIntoContainerService>(NodeIntoContainerService);
|
||||||
|
|
||||||
|
const show = useCallback(() => {
|
||||||
|
if (globalStore.isClosed()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setVisible(true);
|
||||||
|
}, [globalStore]);
|
||||||
|
|
||||||
|
const close = useCallback(() => {
|
||||||
|
globalStore.close();
|
||||||
|
setVisible(false);
|
||||||
|
}, [globalStore]);
|
||||||
|
|
||||||
|
const closeForever = useCallback(() => {
|
||||||
|
globalStore.closeForever();
|
||||||
|
close();
|
||||||
|
}, [close, globalStore]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// 监听移入
|
||||||
|
const inDisposer = nodeIntoContainerService.on((e) => {
|
||||||
|
if (e.type !== NodeIntoContainerType.In) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (e.targetContainer === node) {
|
||||||
|
show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// 监听移出事件
|
||||||
|
const outDisposer = nodeIntoContainerService.on((e) => {
|
||||||
|
if (e.type !== NodeIntoContainerType.Out) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (e.sourceContainer === node && !node.blocks.length) {
|
||||||
|
setVisible(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
inDisposer.dispose();
|
||||||
|
outDisposer.dispose();
|
||||||
|
};
|
||||||
|
}, [nodeIntoContainerService, node, show, close, visible]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
visible,
|
||||||
|
close,
|
||||||
|
closeForever,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -1 +1,2 @@
|
|||||||
export { useNodeSize } from './use-node-size';
|
export { NodeSize, useNodeSize } from './use-node-size';
|
||||||
|
export { useSyncNodeRenderSize } from './use-sync-node-render-size';
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import {
|
|||||||
} from '@flowgram.ai/free-layout-core';
|
} from '@flowgram.ai/free-layout-core';
|
||||||
import { FlowNodeTransformData } from '@flowgram.ai/document';
|
import { FlowNodeTransformData } from '@flowgram.ai/document';
|
||||||
|
|
||||||
interface NodeSize {
|
export interface NodeSize {
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,17 @@
|
|||||||
|
import { useLayoutEffect } from 'react';
|
||||||
|
|
||||||
|
import { useCurrentEntity } from '@flowgram.ai/free-layout-core';
|
||||||
|
|
||||||
|
import { NodeSize } from './use-node-size';
|
||||||
|
|
||||||
|
export const useSyncNodeRenderSize = (nodeSize?: NodeSize) => {
|
||||||
|
const node = useCurrentEntity();
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (!nodeSize) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
node.renderData.node.style.width = nodeSize.width + 'px';
|
||||||
|
node.renderData.node.style.height = nodeSize.height + 'px';
|
||||||
|
}, [nodeSize?.width, nodeSize?.height]);
|
||||||
|
};
|
||||||
@ -1,6 +0,0 @@
|
|||||||
import type { ReactNode } from 'react';
|
|
||||||
|
|
||||||
export interface WorkflowContainerPluginOptions {
|
|
||||||
disableNodeIntoContainer?: boolean;
|
|
||||||
renderContent?: ReactNode;
|
|
||||||
}
|
|
||||||
6
packages/plugins/free-group-plugin/.eslintrc.js
Normal file
6
packages/plugins/free-group-plugin/.eslintrc.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
const { defineConfig } = require('@flowgram.ai/eslint-config');
|
||||||
|
|
||||||
|
module.exports = defineConfig({
|
||||||
|
preset: 'web',
|
||||||
|
packageRoot: __dirname,
|
||||||
|
});
|
||||||
68
packages/plugins/free-group-plugin/package.json
Normal file
68
packages/plugins/free-group-plugin/package.json
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
{
|
||||||
|
"name": "@flowgram.ai/free-group-plugin",
|
||||||
|
"version": "0.1.8",
|
||||||
|
"homepage": "https://flowgram.ai/",
|
||||||
|
"repository": "https://github.com/bytedance/flowgram.ai",
|
||||||
|
"license": "MIT",
|
||||||
|
"exports": {
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"import": "./dist/esm/index.js",
|
||||||
|
"require": "./dist/index.js"
|
||||||
|
},
|
||||||
|
"main": "./dist/index.js",
|
||||||
|
"module": "./dist/esm/index.js",
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"files": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"build": "npm run build:fast -- --dts-resolve",
|
||||||
|
"build:fast": "tsup src/index.ts --format cjs,esm --sourcemap --legacy-output",
|
||||||
|
"build:watch": "npm run build:fast -- --dts-resolve",
|
||||||
|
"clean": "rimraf dist",
|
||||||
|
"test": "exit 0",
|
||||||
|
"test:cov": "exit 0",
|
||||||
|
"ts-check": "tsc --noEmit",
|
||||||
|
"watch": "npm run build:fast -- --dts-resolve --watch --ignore-watch dist"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@flowgram.ai/free-history-plugin": "workspace:*",
|
||||||
|
"@flowgram.ai/free-container-plugin": "workspace:*",
|
||||||
|
"@flowgram.ai/free-lines-plugin": "workspace:*",
|
||||||
|
"@flowgram.ai/core": "workspace:*",
|
||||||
|
"@flowgram.ai/document": "workspace:*",
|
||||||
|
"@flowgram.ai/shortcuts-plugin": "workspace:*",
|
||||||
|
"@flowgram.ai/free-layout-core": "workspace:*",
|
||||||
|
"@flowgram.ai/renderer": "workspace:*",
|
||||||
|
"@flowgram.ai/utils": "workspace:*",
|
||||||
|
"inversify": "^6.0.1",
|
||||||
|
"reflect-metadata": "~0.2.2",
|
||||||
|
"lodash": "^4.17.21"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@flowgram.ai/eslint-config": "workspace:*",
|
||||||
|
"@flowgram.ai/ts-config": "workspace:*",
|
||||||
|
"@types/bezier-js": "4.1.3",
|
||||||
|
"@types/lodash": "^4.14.137",
|
||||||
|
"@types/react": "^18",
|
||||||
|
"@types/react-dom": "^18",
|
||||||
|
"@types/styled-components": "^5",
|
||||||
|
"@vitest/coverage-v8": "^0.32.0",
|
||||||
|
"eslint": "^8.54.0",
|
||||||
|
"react": "^18",
|
||||||
|
"react-dom": "^18",
|
||||||
|
"styled-components": "^5",
|
||||||
|
"tsup": "^8.0.1",
|
||||||
|
"typescript": "^5.0.4",
|
||||||
|
"vitest": "^0.34.6"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=17",
|
||||||
|
"react-dom": ">=17",
|
||||||
|
"styled-components": ">=4"
|
||||||
|
},
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public",
|
||||||
|
"registry": "https://registry.npmjs.org/"
|
||||||
|
}
|
||||||
|
}
|
||||||
4
packages/plugins/free-group-plugin/src/constant.ts
Normal file
4
packages/plugins/free-group-plugin/src/constant.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export enum WorkflowGroupCommand {
|
||||||
|
Group = 'group',
|
||||||
|
Ungroup = 'ungroup',
|
||||||
|
}
|
||||||
@ -0,0 +1,46 @@
|
|||||||
|
import { ShortcutsRegistry } from '@flowgram.ai/shortcuts-plugin';
|
||||||
|
import { FlowRendererRegistry } from '@flowgram.ai/renderer';
|
||||||
|
import { WorkflowDocument } from '@flowgram.ai/free-layout-core';
|
||||||
|
import { FlowGroupService, FlowNodeBaseType } from '@flowgram.ai/document';
|
||||||
|
import { definePluginCreator, PluginContext } from '@flowgram.ai/core';
|
||||||
|
|
||||||
|
import { WorkflowGroupService } from './workflow-group-service';
|
||||||
|
import { WorkflowGroupPluginOptions } from './type';
|
||||||
|
import { GroupShortcut, UngroupShortcut } from './shortcuts';
|
||||||
|
import { GroupNodeRegistry } from './group-node';
|
||||||
|
|
||||||
|
export const createFreeGroupPlugin = definePluginCreator<WorkflowGroupPluginOptions, PluginContext>(
|
||||||
|
{
|
||||||
|
onBind({ bind, rebind }) {
|
||||||
|
bind(WorkflowGroupService).toSelf().inSingletonScope();
|
||||||
|
rebind(FlowGroupService).toService(WorkflowGroupService);
|
||||||
|
},
|
||||||
|
onInit(
|
||||||
|
ctx,
|
||||||
|
{ groupNodeRender, disableGroupShortcuts = false, disableGroupNodeRegister = false }
|
||||||
|
) {
|
||||||
|
// register node render
|
||||||
|
if (groupNodeRender) {
|
||||||
|
const renderRegistry = ctx.get<FlowRendererRegistry>(FlowRendererRegistry);
|
||||||
|
renderRegistry.registerReactComponent(FlowNodeBaseType.GROUP, groupNodeRender);
|
||||||
|
}
|
||||||
|
// register shortcuts
|
||||||
|
if (!disableGroupShortcuts) {
|
||||||
|
const shortcutsRegistry = ctx.get(ShortcutsRegistry);
|
||||||
|
shortcutsRegistry.addHandlers(new GroupShortcut(ctx), new UngroupShortcut(ctx));
|
||||||
|
}
|
||||||
|
if (!disableGroupNodeRegister) {
|
||||||
|
const document = ctx.get(WorkflowDocument);
|
||||||
|
document.registerFlowNodes(GroupNodeRegistry);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onReady(ctx) {
|
||||||
|
const groupService = ctx.get(WorkflowGroupService);
|
||||||
|
groupService.ready();
|
||||||
|
},
|
||||||
|
onDispose(ctx) {
|
||||||
|
const groupService = ctx.get(WorkflowGroupService);
|
||||||
|
groupService.dispose();
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
37
packages/plugins/free-group-plugin/src/group-node.tsx
Normal file
37
packages/plugins/free-group-plugin/src/group-node.tsx
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { PositionSchema } from '@flowgram.ai/utils';
|
||||||
|
import { WorkflowNodeEntity } from '@flowgram.ai/free-layout-core';
|
||||||
|
import { FlowNodeRegistry, FlowNodeBaseType, FlowNodeTransformData } from '@flowgram.ai/document';
|
||||||
|
|
||||||
|
export const GroupNodeRegistry: FlowNodeRegistry = {
|
||||||
|
type: FlowNodeBaseType.GROUP,
|
||||||
|
meta: {
|
||||||
|
renderKey: FlowNodeBaseType.GROUP,
|
||||||
|
defaultPorts: [],
|
||||||
|
isContainer: true,
|
||||||
|
disableSideBar: true,
|
||||||
|
size: {
|
||||||
|
width: 560,
|
||||||
|
height: 400,
|
||||||
|
},
|
||||||
|
padding: () => ({
|
||||||
|
top: 80,
|
||||||
|
bottom: 40,
|
||||||
|
left: 65,
|
||||||
|
right: 65,
|
||||||
|
}),
|
||||||
|
selectable(node: WorkflowNodeEntity, mousePos?: PositionSchema): boolean {
|
||||||
|
if (!mousePos) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const transform = node.getData<FlowNodeTransformData>(FlowNodeTransformData);
|
||||||
|
return !transform.bounds.contains(mousePos.x, mousePos.y);
|
||||||
|
},
|
||||||
|
expandable: false,
|
||||||
|
},
|
||||||
|
formMeta: {
|
||||||
|
render: () => <></>,
|
||||||
|
},
|
||||||
|
onCreate() {
|
||||||
|
// NOTICE: 这个函数是为了避免触发固定布局 flowDocument.addBlocksAsChildren
|
||||||
|
},
|
||||||
|
};
|
||||||
3
packages/plugins/free-group-plugin/src/index.ts
Normal file
3
packages/plugins/free-group-plugin/src/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export { createFreeGroupPlugin } from './create-free-group-plugin';
|
||||||
|
export { WorkflowGroupService } from './workflow-group-service';
|
||||||
|
export { WorkflowGroupCommand } from './constant';
|
||||||
31
packages/plugins/free-group-plugin/src/shortcuts/group.ts
Normal file
31
packages/plugins/free-group-plugin/src/shortcuts/group.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { ShortcutsHandler } from '@flowgram.ai/shortcuts-plugin';
|
||||||
|
import { WorkflowSelectService } from '@flowgram.ai/free-layout-core';
|
||||||
|
import { PluginContext } from '@flowgram.ai/core';
|
||||||
|
|
||||||
|
import { WorkflowGroupService } from '../workflow-group-service';
|
||||||
|
import { WorkflowGroupCommand } from '../constant';
|
||||||
|
|
||||||
|
export class GroupShortcut implements ShortcutsHandler {
|
||||||
|
public commandId = WorkflowGroupCommand.Group;
|
||||||
|
|
||||||
|
public commandDetail: ShortcutsHandler['commandDetail'] = {
|
||||||
|
label: 'Group',
|
||||||
|
};
|
||||||
|
|
||||||
|
public shortcuts = ['meta g', 'ctrl g'];
|
||||||
|
|
||||||
|
private selectService: WorkflowSelectService;
|
||||||
|
|
||||||
|
private groupService: WorkflowGroupService;
|
||||||
|
|
||||||
|
constructor(context: PluginContext) {
|
||||||
|
this.selectService = context.get(WorkflowSelectService);
|
||||||
|
this.groupService = context.get(WorkflowGroupService);
|
||||||
|
this.execute = this.execute.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async execute(): Promise<void> {
|
||||||
|
this.groupService.createGroup(this.selectService.selectedNodes);
|
||||||
|
this.selectService.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,2 @@
|
|||||||
|
export { GroupShortcut } from './group';
|
||||||
|
export { UngroupShortcut } from './ungroup';
|
||||||
36
packages/plugins/free-group-plugin/src/shortcuts/ungroup.ts
Normal file
36
packages/plugins/free-group-plugin/src/shortcuts/ungroup.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { ShortcutsHandler } from '@flowgram.ai/shortcuts-plugin';
|
||||||
|
import { WorkflowSelectService, WorkflowNodeEntity } from '@flowgram.ai/free-layout-core';
|
||||||
|
import { FlowNodeBaseType } from '@flowgram.ai/document';
|
||||||
|
import { PluginContext } from '@flowgram.ai/core';
|
||||||
|
|
||||||
|
import { WorkflowGroupService } from '../workflow-group-service';
|
||||||
|
import { WorkflowGroupCommand } from '../constant';
|
||||||
|
|
||||||
|
export class UngroupShortcut implements ShortcutsHandler {
|
||||||
|
public commandId = WorkflowGroupCommand.Ungroup;
|
||||||
|
|
||||||
|
public commandDetail: ShortcutsHandler['commandDetail'] = {
|
||||||
|
label: 'Ungroup',
|
||||||
|
};
|
||||||
|
|
||||||
|
public shortcuts = ['meta shift g', 'ctrl shift g'];
|
||||||
|
|
||||||
|
private selectService: WorkflowSelectService;
|
||||||
|
|
||||||
|
private groupService: WorkflowGroupService;
|
||||||
|
|
||||||
|
constructor(context: PluginContext) {
|
||||||
|
this.selectService = context.get(WorkflowSelectService);
|
||||||
|
this.groupService = context.get(WorkflowGroupService);
|
||||||
|
this.execute = this.execute.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async execute(_groupNode?: WorkflowNodeEntity): Promise<void> {
|
||||||
|
const groupNode = _groupNode || this.selectService.activatedNode;
|
||||||
|
if (!groupNode || groupNode.flowNodeType !== FlowNodeBaseType.GROUP) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.groupService.ungroup(groupNode);
|
||||||
|
this.selectService.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
7
packages/plugins/free-group-plugin/src/type.ts
Normal file
7
packages/plugins/free-group-plugin/src/type.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { FC } from 'react';
|
||||||
|
|
||||||
|
export interface WorkflowGroupPluginOptions {
|
||||||
|
groupNodeRender: FC;
|
||||||
|
disableGroupShortcuts?: boolean;
|
||||||
|
disableGroupNodeRegister?: boolean;
|
||||||
|
}
|
||||||
57
packages/plugins/free-group-plugin/src/utils.ts
Normal file
57
packages/plugins/free-group-plugin/src/utils.ts
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import { WorkflowNodeEntity } from '@flowgram.ai/free-layout-core';
|
||||||
|
import { FlowNodeBaseType } from '@flowgram.ai/document';
|
||||||
|
|
||||||
|
export namespace WorkflowGroupUtils {
|
||||||
|
/** 找到节点所有上级 */
|
||||||
|
// const findNodeParents = (node: WorkflowNodeEntity): WorkflowNodeEntity[] => {
|
||||||
|
// const parents = [];
|
||||||
|
// let parent = node.parent;
|
||||||
|
// while (parent) {
|
||||||
|
// parents.push(parent);
|
||||||
|
// parent = parent.parent;
|
||||||
|
// }
|
||||||
|
// return parents;
|
||||||
|
// };
|
||||||
|
|
||||||
|
/** 节点是否处于分组中 */
|
||||||
|
const isNodeInGroup = (node: WorkflowNodeEntity): boolean => {
|
||||||
|
// 处于分组中
|
||||||
|
if (node?.parent?.flowNodeType === FlowNodeBaseType.GROUP) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 是否分组节点 */
|
||||||
|
const isGroupNode = (group: WorkflowNodeEntity): boolean =>
|
||||||
|
group.flowNodeType === FlowNodeBaseType.GROUP;
|
||||||
|
|
||||||
|
/** 判断节点能否组成分组 */
|
||||||
|
export const validate = (nodes: WorkflowNodeEntity[]): boolean => {
|
||||||
|
if (!nodes || !Array.isArray(nodes) || nodes.length === 0) {
|
||||||
|
// 参数不合法
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判断是否有分组节点
|
||||||
|
const isGroupRelatedNode = nodes.some((node) => isGroupNode(node));
|
||||||
|
if (isGroupRelatedNode) return false;
|
||||||
|
|
||||||
|
// 判断是否有节点已经处于分组中
|
||||||
|
const hasGroup = nodes.some((node) => node && isNodeInGroup(node));
|
||||||
|
if (hasGroup) return false;
|
||||||
|
|
||||||
|
// 判断是否来自同一个父亲
|
||||||
|
const parent = nodes[0].parent;
|
||||||
|
const isSameParent = nodes.every((node) => node.parent === parent);
|
||||||
|
if (!isSameParent) return false;
|
||||||
|
|
||||||
|
// 判断节点父亲是否已经在分组中
|
||||||
|
// const parents = findNodeParents(nodes[0]);
|
||||||
|
// const parentsInGroup = parents.some((parent) => isNodeInGroup(parent));
|
||||||
|
// if (parentsInGroup) return false;
|
||||||
|
|
||||||
|
// 参数正确
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
}
|
||||||
116
packages/plugins/free-group-plugin/src/workflow-group-service.ts
Normal file
116
packages/plugins/free-group-plugin/src/workflow-group-service.ts
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
import { injectable, inject } from 'inversify';
|
||||||
|
import { DisposableCollection, Disposable } from '@flowgram.ai/utils';
|
||||||
|
import {
|
||||||
|
WorkflowDocument,
|
||||||
|
WorkflowOperationBaseService,
|
||||||
|
WorkflowNodeEntity,
|
||||||
|
nanoid,
|
||||||
|
WorkflowNodeJSON,
|
||||||
|
} from '@flowgram.ai/free-layout-core';
|
||||||
|
import { HistoryService } from '@flowgram.ai/free-history-plugin';
|
||||||
|
import {
|
||||||
|
NodeIntoContainerService,
|
||||||
|
NodeIntoContainerType,
|
||||||
|
} from '@flowgram.ai/free-container-plugin';
|
||||||
|
import { FlowGroupService, FlowNodeBaseType } from '@flowgram.ai/document';
|
||||||
|
import { TransformData } from '@flowgram.ai/core';
|
||||||
|
|
||||||
|
import { WorkflowGroupUtils } from './utils';
|
||||||
|
|
||||||
|
@injectable()
|
||||||
|
/** 分组服务 */
|
||||||
|
export class WorkflowGroupService extends FlowGroupService {
|
||||||
|
@inject(WorkflowDocument) private document: WorkflowDocument;
|
||||||
|
|
||||||
|
@inject(WorkflowOperationBaseService) freeOperationService: WorkflowOperationBaseService;
|
||||||
|
|
||||||
|
@inject(HistoryService) private historyService: HistoryService;
|
||||||
|
|
||||||
|
@inject(NodeIntoContainerService) private nodeIntoContainerService: NodeIntoContainerService;
|
||||||
|
|
||||||
|
private toDispose = new DisposableCollection();
|
||||||
|
|
||||||
|
public ready(): void {
|
||||||
|
this.toDispose.push(this.listenContainer());
|
||||||
|
}
|
||||||
|
|
||||||
|
public dispose(): void {
|
||||||
|
this.toDispose.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 创建分组节点 */
|
||||||
|
public createGroup(nodes: WorkflowNodeEntity[]): WorkflowNodeEntity | undefined {
|
||||||
|
if (!WorkflowGroupUtils.validate(nodes)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const parent = nodes[0].parent ?? this.document.root;
|
||||||
|
const groupId = `group_${nanoid(5)}`;
|
||||||
|
const groupJSON: WorkflowNodeJSON = {
|
||||||
|
type: FlowNodeBaseType.GROUP,
|
||||||
|
id: groupId,
|
||||||
|
meta: {
|
||||||
|
position: {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data: {},
|
||||||
|
};
|
||||||
|
this.historyService.startTransaction();
|
||||||
|
this.document.createWorkflowNodeByType(
|
||||||
|
FlowNodeBaseType.GROUP,
|
||||||
|
{
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
},
|
||||||
|
groupJSON,
|
||||||
|
parent.id
|
||||||
|
);
|
||||||
|
nodes.forEach((node) => {
|
||||||
|
this.freeOperationService.moveNode(node, {
|
||||||
|
parent: groupId,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
this.historyService.endTransaction();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 取消分组 */
|
||||||
|
public ungroup(groupNode: WorkflowNodeEntity): void {
|
||||||
|
const groupBlocks = groupNode.blocks.slice();
|
||||||
|
if (!groupNode.parent) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const groupPosition = groupNode.transform.position;
|
||||||
|
|
||||||
|
this.historyService.startTransaction();
|
||||||
|
groupBlocks.forEach((node) => {
|
||||||
|
this.freeOperationService.moveNode(node, {
|
||||||
|
parent: groupNode.parent?.id,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
groupNode.dispose();
|
||||||
|
groupBlocks.forEach((node) => {
|
||||||
|
const transform = node.getData(TransformData);
|
||||||
|
const position = {
|
||||||
|
x: transform.position.x + groupPosition.x,
|
||||||
|
y: transform.position.y + groupPosition.y,
|
||||||
|
};
|
||||||
|
this.freeOperationService.updateNodePosition(node, position);
|
||||||
|
});
|
||||||
|
this.historyService.endTransaction();
|
||||||
|
}
|
||||||
|
|
||||||
|
private listenContainer(): Disposable {
|
||||||
|
return this.nodeIntoContainerService.on((e) => {
|
||||||
|
if (
|
||||||
|
e.type !== NodeIntoContainerType.Out ||
|
||||||
|
e.sourceContainer?.flowNodeType !== FlowNodeBaseType.GROUP
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (e.sourceContainer?.blocks.length === 0) {
|
||||||
|
e.sourceContainer.dispose();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
7
packages/plugins/free-group-plugin/tsconfig.json
Normal file
7
packages/plugins/free-group-plugin/tsconfig.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"extends": "@flowgram.ai/ts-config/tsconfig.flow.path.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
},
|
||||||
|
"include": ["./src"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
26
packages/plugins/free-group-plugin/vitest.config.ts
Normal file
26
packages/plugins/free-group-plugin/vitest.config.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
const path = require('path');
|
||||||
|
import { defineConfig } from 'vitest/config';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
build: {
|
||||||
|
commonjsOptions: {
|
||||||
|
transformMixedEsModules: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
test: {
|
||||||
|
globals: true,
|
||||||
|
mockReset: false,
|
||||||
|
environment: 'jsdom',
|
||||||
|
setupFiles: [path.resolve(__dirname, './vitest.setup.ts')],
|
||||||
|
include: ['**/?(*.){test,spec}.?(c|m)[jt]s?(x)'],
|
||||||
|
exclude: [
|
||||||
|
'**/__mocks__**',
|
||||||
|
'**/node_modules/**',
|
||||||
|
'**/dist/**',
|
||||||
|
'**/lib/**', // lib 编译结果忽略掉
|
||||||
|
'**/cypress/**',
|
||||||
|
'**/.{idea,git,cache,output,temp}/**',
|
||||||
|
'**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build}.config.*',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
1
packages/plugins/free-group-plugin/vitest.setup.ts
Normal file
1
packages/plugins/free-group-plugin/vitest.setup.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
import 'reflect-metadata';
|
||||||
@ -90,8 +90,8 @@ export class FreeHistoryManager {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
: Disposable.NULL,
|
: Disposable.NULL,
|
||||||
document.onContentChange(async (event) => {
|
document.onContentChange((event) => {
|
||||||
await this._changeContentHandler.handle(event, ctx);
|
this._changeContentHandler.handle(event, ctx);
|
||||||
}),
|
}),
|
||||||
document.onReload((_event) => {
|
document.onReload((_event) => {
|
||||||
historyService.clear();
|
historyService.clear();
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
/* eslint-disable @typescript-eslint/naming-convention */
|
/* eslint-disable @typescript-eslint/naming-convention */
|
||||||
import { injectable, inject } from 'inversify';
|
import { injectable, inject } from 'inversify';
|
||||||
import { type PluginContext } from '@flowgram.ai/core';
|
import { HistoryService, Operation } from '@flowgram.ai/history';
|
||||||
import { type WorkflowContentChangeEvent } from '@flowgram.ai/free-layout-core';
|
import { type WorkflowContentChangeEvent } from '@flowgram.ai/free-layout-core';
|
||||||
import { HistoryService } from '@flowgram.ai/history';
|
import { type PluginContext } from '@flowgram.ai/core';
|
||||||
|
|
||||||
import { type IHandler } from '../types';
|
import { type IHandler } from '../types';
|
||||||
import changes from '../changes';
|
import changes from '../changes';
|
||||||
@ -12,16 +12,16 @@ export class ChangeContentHandler implements IHandler<WorkflowContentChangeEvent
|
|||||||
@inject(HistoryService)
|
@inject(HistoryService)
|
||||||
private _historyService: HistoryService;
|
private _historyService: HistoryService;
|
||||||
|
|
||||||
async handle(event: WorkflowContentChangeEvent, ctx: PluginContext) {
|
handle(event: WorkflowContentChangeEvent, ctx: PluginContext) {
|
||||||
if (!this._historyService.undoRedoService.canPush()) {
|
if (!this._historyService.undoRedoService.canPush()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const change = changes.find(c => c.type === event.type);
|
const change = changes.find((c) => c.type === event.type);
|
||||||
if (!change) {
|
if (!change) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const operation = await change.toOperation(event, ctx);
|
const operation: Operation | undefined = change.toOperation(event, ctx);
|
||||||
if (!operation) {
|
if (!operation) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import { type OperationMeta } from '@flowgram.ai/history';
|
import { type OperationMeta } from '@flowgram.ai/history';
|
||||||
import { WorkflowDocument } from '@flowgram.ai/free-layout-core';
|
import { WorkflowDocument } from '@flowgram.ai/free-layout-core';
|
||||||
import { FlowNodeTransformData } from '@flowgram.ai/document';
|
|
||||||
import { type PluginContext, TransformData } from '@flowgram.ai/core';
|
import { type PluginContext, TransformData } from '@flowgram.ai/core';
|
||||||
|
|
||||||
import { type DragNodeOperationValue, FreeOperationType } from '../types';
|
import { type DragNodeOperationValue, FreeOperationType } from '../types';
|
||||||
@ -33,14 +32,7 @@ export const dragNodesOperationMeta: OperationMeta<DragNodeOperationValue, Plugi
|
|||||||
y: point.y,
|
y: point.y,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
// 嵌套情况下需将子节点 transform 设为 dirty
|
document.layout.updateAffectedTransform(node);
|
||||||
if (node.collapsedChildren?.length > 0) {
|
|
||||||
node.collapsedChildren.forEach((childNode) => {
|
|
||||||
const childNodeTransformData =
|
|
||||||
childNode.getData<FlowNodeTransformData>(FlowNodeTransformData);
|
|
||||||
childNodeTransformData.fireChange();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -84,10 +84,7 @@ export interface ResetLayoutOperationValue {
|
|||||||
|
|
||||||
export interface ContentChangeTypeToOperation<T extends Operation> {
|
export interface ContentChangeTypeToOperation<T extends Operation> {
|
||||||
type: WorkflowContentChangeType;
|
type: WorkflowContentChangeType;
|
||||||
toOperation: (
|
toOperation: (event: WorkflowContentChangeEvent, ctx: PluginContext) => T | undefined;
|
||||||
event: WorkflowContentChangeEvent,
|
|
||||||
ctx: PluginContext
|
|
||||||
) => T | undefined | Promise<T | undefined>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EntityDataType {
|
export interface EntityDataType {
|
||||||
|
|||||||
@ -398,17 +398,10 @@ export class WorkflowSnapService {
|
|||||||
x: transform.position.x + offset.x,
|
x: transform.position.x + offset.x,
|
||||||
y: transform.position.y + offset.y,
|
y: transform.position.y + offset.y,
|
||||||
};
|
};
|
||||||
if (node.collapsedChildren?.length > 0) {
|
|
||||||
// 嵌套情况下需将子节点 transform 设为 dirty
|
|
||||||
node.collapsedChildren.forEach((childNode) => {
|
|
||||||
const childNodeTransformData =
|
|
||||||
childNode.getData<FlowNodeTransformData>(FlowNodeTransformData);
|
|
||||||
childNodeTransformData.fireChange();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
transform.update({
|
transform.update({
|
||||||
position: positionWithOffset,
|
position: positionWithOffset,
|
||||||
});
|
});
|
||||||
|
this.document.layout.updateAffectedTransform(node);
|
||||||
}
|
}
|
||||||
|
|
||||||
private calcAlignOffset(params: {
|
private calcAlignOffset(params: {
|
||||||
|
|||||||
@ -16,7 +16,6 @@ import {
|
|||||||
} from '@flowgram.ai/core';
|
} from '@flowgram.ai/core';
|
||||||
|
|
||||||
import { StackingContextManager } from '../src/manager';
|
import { StackingContextManager } from '../src/manager';
|
||||||
import { StackingComputeMode } from '../src/constant';
|
|
||||||
import { createWorkflowContainer, workflowJSON } from './utils.mock';
|
import { createWorkflowContainer, workflowJSON } from './utils.mock';
|
||||||
import { IStackingContextManager } from './type.mock';
|
import { IStackingContextManager } from './type.mock';
|
||||||
|
|
||||||
@ -58,13 +57,8 @@ describe('StackingContextManager public methods', () => {
|
|||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
);
|
);
|
||||||
expect(stackingContextManager.mode).toEqual(StackingComputeMode.Stacking);
|
|
||||||
expect(stackingContextManager.disposers).toHaveLength(4);
|
expect(stackingContextManager.disposers).toHaveLength(4);
|
||||||
});
|
});
|
||||||
it('should init with mode', () => {
|
|
||||||
stackingContextManager.init(StackingComputeMode.Stacking);
|
|
||||||
expect(stackingContextManager.mode).toEqual(StackingComputeMode.Stacking);
|
|
||||||
});
|
|
||||||
it('should execute ready', () => {
|
it('should execute ready', () => {
|
||||||
stackingContextManager.compute = vi.fn();
|
stackingContextManager.compute = vi.fn();
|
||||||
stackingContextManager.ready();
|
stackingContextManager.ready();
|
||||||
@ -206,18 +200,7 @@ describe('StackingContextManager private methods', () => {
|
|||||||
expect(compute).toBeCalledTimes(2);
|
expect(compute).toBeCalledTimes(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should trigger compute in layers mode', async () => {
|
it('should trigger compute', async () => {
|
||||||
stackingContextManager.init(StackingComputeMode.Layers);
|
|
||||||
stackingContextManager.ready();
|
|
||||||
await delay(200);
|
|
||||||
const node = document.getNode('loop_0')!;
|
|
||||||
const nodeRenderData = node.getData<FlowNodeRenderData>(FlowNodeRenderData);
|
|
||||||
const element = nodeRenderData.node;
|
|
||||||
expect(element.style.zIndex).toBe('9');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should trigger compute in stacking mode', async () => {
|
|
||||||
stackingContextManager.init(StackingComputeMode.Stacking);
|
|
||||||
stackingContextManager.ready();
|
stackingContextManager.ready();
|
||||||
await delay(200);
|
await delay(200);
|
||||||
const node = document.getNode('loop_0')!;
|
const node = document.getNode('loop_0')!;
|
||||||
|
|||||||
@ -34,10 +34,3 @@ export const StackingConfig = {
|
|||||||
/** 最大 index */
|
/** 最大 index */
|
||||||
maxIndex,
|
maxIndex,
|
||||||
};
|
};
|
||||||
|
|
||||||
export enum StackingComputeMode {
|
|
||||||
/** 层叠计算模式 */
|
|
||||||
Stacking = 'stacking',
|
|
||||||
/** 层级计算模式 */
|
|
||||||
Layers = 'layers',
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,17 +1,14 @@
|
|||||||
import { definePluginCreator } from '@flowgram.ai/core';
|
import { definePluginCreator } from '@flowgram.ai/core';
|
||||||
|
|
||||||
import { StackingContextManager } from './manager';
|
import { StackingContextManager } from './manager';
|
||||||
import { StackingComputeMode } from './constant';
|
|
||||||
|
|
||||||
export const createFreeStackPlugin = definePluginCreator<{
|
export const createFreeStackPlugin = definePluginCreator({
|
||||||
mode?: StackingComputeMode;
|
|
||||||
}>({
|
|
||||||
onBind({ bind }) {
|
onBind({ bind }) {
|
||||||
bind(StackingContextManager).toSelf().inSingletonScope();
|
bind(StackingContextManager).toSelf().inSingletonScope();
|
||||||
},
|
},
|
||||||
onInit(ctx, opts) {
|
onInit(ctx) {
|
||||||
const stackingContextManager = ctx.get<StackingContextManager>(StackingContextManager);
|
const stackingContextManager = ctx.get<StackingContextManager>(StackingContextManager);
|
||||||
stackingContextManager.init(opts?.mode);
|
stackingContextManager.init();
|
||||||
},
|
},
|
||||||
onReady(ctx) {
|
onReady(ctx) {
|
||||||
const stackingContextManager = ctx.get<StackingContextManager>(StackingContextManager);
|
const stackingContextManager = ctx.get<StackingContextManager>(StackingContextManager);
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { FlowNodeRenderData } from '@flowgram.ai/document';
|
|
||||||
import type { WorkflowNodeEntity } from '@flowgram.ai/free-layout-core';
|
import type { WorkflowNodeEntity } from '@flowgram.ai/free-layout-core';
|
||||||
import type { WorkflowLineEntity } from '@flowgram.ai/free-layout-core';
|
import type { WorkflowLineEntity } from '@flowgram.ai/free-layout-core';
|
||||||
|
import { FlowNodeRenderData } from '@flowgram.ai/document';
|
||||||
|
|
||||||
import type { StackingContext } from './type';
|
import type { StackingContext } from './type';
|
||||||
import { StackingBaseIndex, StackingConfig, StackingType } from './constant';
|
import { StackingBaseIndex, StackingConfig, StackingType } from './constant';
|
||||||
@ -27,7 +27,7 @@ namespace NodeComputing {
|
|||||||
export const nodeStackingLevel = (
|
export const nodeStackingLevel = (
|
||||||
node: WorkflowNodeEntity,
|
node: WorkflowNodeEntity,
|
||||||
context: StackingContext,
|
context: StackingContext,
|
||||||
disableTopLevel = false,
|
disableTopLevel = false
|
||||||
): number => {
|
): number => {
|
||||||
// TODO 后续支持多层级时这个计算逻辑应该去掉,level信息应该直接由 FlowNodeEntity 缓存给出
|
// TODO 后续支持多层级时这个计算逻辑应该去掉,level信息应该直接由 FlowNodeEntity 缓存给出
|
||||||
// 多层时这里的计算会有 O(logN) 时间复杂度,并且在多层级联同计算时会有BUG,本次需求不处理这种情况
|
// 多层时这里的计算会有 O(logN) 时间复杂度,并且在多层级联同计算时会有BUG,本次需求不处理这种情况
|
||||||
@ -116,10 +116,10 @@ export const layersComputing = (params: {
|
|||||||
context: StackingContext;
|
context: StackingContext;
|
||||||
}) => {
|
}) => {
|
||||||
const { nodes, lines, context } = params;
|
const { nodes, lines, context } = params;
|
||||||
nodes.forEach(node => {
|
nodes.forEach((node) => {
|
||||||
NodeComputing.compute(node, context);
|
NodeComputing.compute(node, context);
|
||||||
});
|
});
|
||||||
lines.forEach(line => {
|
lines.forEach((line) => {
|
||||||
LineComputing.compute(line, context);
|
LineComputing.compute(line, context);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@ -14,8 +14,7 @@ import { EntityManager, PipelineRegistry, PipelineRenderer } from '@flowgram.ai/
|
|||||||
|
|
||||||
import type { StackingContext } from './type';
|
import type { StackingContext } from './type';
|
||||||
import { StackingComputing } from './stacking-computing';
|
import { StackingComputing } from './stacking-computing';
|
||||||
import { layersComputing } from './layers-computing';
|
import { StackingConfig } from './constant';
|
||||||
import { StackingComputeMode, StackingConfig } from './constant';
|
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class StackingContextManager {
|
export class StackingContextManager {
|
||||||
@ -41,12 +40,9 @@ export class StackingContextManager {
|
|||||||
|
|
||||||
private disposers: Disposable[] = [];
|
private disposers: Disposable[] = [];
|
||||||
|
|
||||||
private mode: StackingComputeMode = StackingComputeMode.Stacking;
|
|
||||||
|
|
||||||
constructor() {}
|
constructor() {}
|
||||||
|
|
||||||
public init(mode?: StackingComputeMode): void {
|
public init(): void {
|
||||||
if (mode) this.mode = mode;
|
|
||||||
this.pipelineRenderer.node.appendChild(this.node);
|
this.pipelineRenderer.node.appendChild(this.node);
|
||||||
this.mountListener();
|
this.mountListener();
|
||||||
}
|
}
|
||||||
@ -66,18 +62,6 @@ export class StackingContextManager {
|
|||||||
private compute = debounce(this._compute, 10);
|
private compute = debounce(this._compute, 10);
|
||||||
|
|
||||||
private _compute(): void {
|
private _compute(): void {
|
||||||
if (this.mode === StackingComputeMode.Stacking) {
|
|
||||||
return this.stackingCompute();
|
|
||||||
} else {
|
|
||||||
return layersComputing({
|
|
||||||
nodes: this.nodes,
|
|
||||||
lines: this.lines,
|
|
||||||
context: this.context,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private stackingCompute(): void {
|
|
||||||
const context = this.context;
|
const context = this.context;
|
||||||
const stackingComputing = new StackingComputing();
|
const stackingComputing = new StackingComputing();
|
||||||
const { nodeLevel, lineLevel } = stackingComputing.compute({
|
const { nodeLevel, lineLevel } = stackingComputing.compute({
|
||||||
@ -90,7 +74,7 @@ export class StackingContextManager {
|
|||||||
const nodeRenderData = node.getData<FlowNodeRenderData>(FlowNodeRenderData);
|
const nodeRenderData = node.getData<FlowNodeRenderData>(FlowNodeRenderData);
|
||||||
const element = nodeRenderData.node;
|
const element = nodeRenderData.node;
|
||||||
element.style.position = 'absolute';
|
element.style.position = 'absolute';
|
||||||
if (!level) {
|
if (level === undefined) {
|
||||||
element.style.zIndex = 'auto';
|
element.style.zIndex = 'auto';
|
||||||
nodeRenderData.stackIndex = 0;
|
nodeRenderData.stackIndex = 0;
|
||||||
return;
|
return;
|
||||||
@ -103,7 +87,7 @@ export class StackingContextManager {
|
|||||||
const level = lineLevel.get(line.id);
|
const level = lineLevel.get(line.id);
|
||||||
const element = line.node;
|
const element = line.node;
|
||||||
element.style.position = 'absolute';
|
element.style.position = 'absolute';
|
||||||
if (!level) {
|
if (level === undefined) {
|
||||||
element.style.zIndex = 'auto';
|
element.style.zIndex = 'auto';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,9 @@
|
|||||||
|
import {
|
||||||
|
WorkflowLineEntity,
|
||||||
|
WorkflowNodeEntity,
|
||||||
|
WorkflowNodeLinesData,
|
||||||
|
} from '@flowgram.ai/free-layout-core';
|
||||||
import { FlowNodeBaseType } from '@flowgram.ai/document';
|
import { FlowNodeBaseType } from '@flowgram.ai/document';
|
||||||
import { WorkflowNodeEntity, WorkflowNodeLinesData } from '@flowgram.ai/free-layout-core';
|
|
||||||
|
|
||||||
import type { StackingContext } from './type';
|
import type { StackingContext } from './type';
|
||||||
|
|
||||||
@ -38,7 +42,7 @@ export class StackingComputing {
|
|||||||
this.nodeIndexes = this.computeNodeIndexesMap(nodes);
|
this.nodeIndexes = this.computeNodeIndexesMap(nodes);
|
||||||
this.topLevel = this.computeTopLevel(nodes);
|
this.topLevel = this.computeTopLevel(nodes);
|
||||||
this.maxLevel = this.topLevel * 2;
|
this.maxLevel = this.topLevel * 2;
|
||||||
this.layerHandler(root.collapsedChildren);
|
this.layerHandler(root.blocks);
|
||||||
return {
|
return {
|
||||||
nodeLevel: this.nodeLevel,
|
nodeLevel: this.nodeLevel,
|
||||||
lineLevel: this.lineLevel,
|
lineLevel: this.lineLevel,
|
||||||
@ -65,9 +69,9 @@ export class StackingComputing {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private computeTopLevel(nodes: WorkflowNodeEntity[]): number {
|
private computeTopLevel(nodes: WorkflowNodeEntity[]): number {
|
||||||
const nodesWithoutRoot = nodes.filter(node => node.id !== FlowNodeBaseType.ROOT);
|
const nodesWithoutRoot = nodes.filter((node) => node.id !== FlowNodeBaseType.ROOT);
|
||||||
const nodeHasChildren = nodesWithoutRoot.reduce((count, node) => {
|
const nodeHasChildren = nodesWithoutRoot.reduce((count, node) => {
|
||||||
if (node.collapsedChildren.length > 0) {
|
if (node.blocks.length > 0) {
|
||||||
return count + 1;
|
return count + 1;
|
||||||
} else {
|
} else {
|
||||||
return count;
|
return count;
|
||||||
@ -77,28 +81,12 @@ export class StackingComputing {
|
|||||||
return nodesWithoutRoot.length + nodeHasChildren + 1;
|
return nodesWithoutRoot.length + nodeHasChildren + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
private layerHandler(nodes: WorkflowNodeEntity[], pinTop: boolean = false): void {
|
private layerHandler(layerNodes: WorkflowNodeEntity[], pinTop: boolean = false): void {
|
||||||
const sortedNodes = nodes.sort((a, b) => {
|
const nodes = this.sortNodes(layerNodes);
|
||||||
const aIndex = this.nodeIndexes.get(a.id);
|
const lines = this.getNodesAllLines(nodes);
|
||||||
const bIndex = this.nodeIndexes.get(b.id);
|
|
||||||
if (aIndex === undefined || bIndex === undefined) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
return aIndex - bIndex;
|
|
||||||
});
|
|
||||||
|
|
||||||
const lines = nodes
|
|
||||||
.map(node => {
|
|
||||||
const linesData = node.getData<WorkflowNodeLinesData>(WorkflowNodeLinesData);
|
|
||||||
const outputLines = linesData.outputLines.filter(Boolean);
|
|
||||||
const inputLines = linesData.inputLines.filter(Boolean);
|
|
||||||
// 前后线条会有重复,下面 Map 会通过线条 ID 过滤掉
|
|
||||||
return [...outputLines, ...inputLines];
|
|
||||||
})
|
|
||||||
.flat();
|
|
||||||
|
|
||||||
// 线条统一设为当前层级最低
|
// 线条统一设为当前层级最低
|
||||||
lines.forEach(line => {
|
lines.forEach((line) => {
|
||||||
if (
|
if (
|
||||||
line.isDrawing || // 正在绘制
|
line.isDrawing || // 正在绘制
|
||||||
this.context.hoveredEntityID === line.id || // hover
|
this.context.hoveredEntityID === line.id || // hover
|
||||||
@ -111,7 +99,7 @@ export class StackingComputing {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.levelIncrease();
|
this.levelIncrease();
|
||||||
sortedNodes.forEach(node => {
|
nodes.forEach((node) => {
|
||||||
const selected = this.context.selectedIDs.includes(node.id);
|
const selected = this.context.selectedIDs.includes(node.id);
|
||||||
if (selected) {
|
if (selected) {
|
||||||
// 节点置顶条件:选中
|
// 节点置顶条件:选中
|
||||||
@ -121,13 +109,47 @@ export class StackingComputing {
|
|||||||
}
|
}
|
||||||
// 节点层级逐层增高
|
// 节点层级逐层增高
|
||||||
this.levelIncrease();
|
this.levelIncrease();
|
||||||
if (node.collapsedChildren.length > 0) {
|
if (node.blocks.length > 0) {
|
||||||
// 子节点层级需低于后续兄弟节点,因此需要先进行计算
|
// 子节点层级需低于后续兄弟节点,因此需要先进行计算
|
||||||
this.layerHandler(node.collapsedChildren, pinTop || selected);
|
this.layerHandler(node.blocks, pinTop || selected);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private sortNodes(nodes: WorkflowNodeEntity[]): WorkflowNodeEntity[] {
|
||||||
|
return nodes.sort((a, b) => {
|
||||||
|
const aIndex = this.nodeIndexes.get(a.id);
|
||||||
|
const bIndex = this.nodeIndexes.get(b.id);
|
||||||
|
if (aIndex === undefined || bIndex === undefined) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return aIndex - bIndex;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private getNodesAllLines(nodes: WorkflowNodeEntity[]): WorkflowLineEntity[] {
|
||||||
|
const lines = nodes
|
||||||
|
.map((node) => {
|
||||||
|
const linesData = node.getData<WorkflowNodeLinesData>(WorkflowNodeLinesData);
|
||||||
|
const outputLines = linesData.outputLines.filter(Boolean);
|
||||||
|
const inputLines = linesData.inputLines.filter(Boolean);
|
||||||
|
return [...outputLines, ...inputLines];
|
||||||
|
})
|
||||||
|
.flat();
|
||||||
|
|
||||||
|
// 过滤出未计算层级的线条,以及高度优先(需要覆盖计算)的线条
|
||||||
|
const filteredLines = lines.filter(
|
||||||
|
(line) => this.lineLevel.get(line.id) === undefined || this.isHigherFirstLine(line)
|
||||||
|
);
|
||||||
|
|
||||||
|
return filteredLines;
|
||||||
|
}
|
||||||
|
|
||||||
|
private isHigherFirstLine(line: WorkflowLineEntity): boolean {
|
||||||
|
// 父子相连的线条,需要作为高度优先的线条,避免线条不可见
|
||||||
|
return line.to?.parent === line.from || line.from?.parent === line.to;
|
||||||
|
}
|
||||||
|
|
||||||
private getLevel(pinTop: boolean): number {
|
private getLevel(pinTop: boolean): number {
|
||||||
if (pinTop) {
|
if (pinTop) {
|
||||||
return this.topLevel + this.currentLevel;
|
return this.topLevel + this.currentLevel;
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
|
import type { WorkflowEntityHoverable } from '@flowgram.ai/free-layout-core';
|
||||||
import type { Entity } from '@flowgram.ai/core';
|
import type { Entity } from '@flowgram.ai/core';
|
||||||
import type { WorkfloEntityHoverable } from '@flowgram.ai/free-layout-core';
|
|
||||||
|
|
||||||
export type StackingContext = {
|
export type StackingContext = {
|
||||||
hoveredEntity?: WorkfloEntityHoverable;
|
hoveredEntity?: WorkflowEntityHoverable;
|
||||||
hoveredEntityID?: string;
|
hoveredEntityID?: string;
|
||||||
selectedEntities: Entity[];
|
selectedEntities: Entity[];
|
||||||
selectedIDs: string[];
|
selectedIDs: string[];
|
||||||
|
|||||||
12
rush.json
12
rush.json
@ -167,14 +167,12 @@
|
|||||||
// "[^@]+@users\\.noreply\\.github\\.com",
|
// "[^@]+@users\\.noreply\\.github\\.com",
|
||||||
// "rush-bot@example\\.org"
|
// "rush-bot@example\\.org"
|
||||||
// ],
|
// ],
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When Rush reports that the address is malformed, the notice can include an example
|
* When Rush reports that the address is malformed, the notice can include an example
|
||||||
* of a recommended email. Make sure it conforms to one of the allowedEmailRegExps
|
* of a recommended email. Make sure it conforms to one of the allowedEmailRegExps
|
||||||
* expressions.
|
* expressions.
|
||||||
*/
|
*/
|
||||||
// "sampleEmail": "example@users.noreply.github.com",
|
// "sampleEmail": "example@users.noreply.github.com",
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The commit message to use when committing changes during 'rush publish'.
|
* The commit message to use when committing changes during 'rush publish'.
|
||||||
*
|
*
|
||||||
@ -183,7 +181,6 @@
|
|||||||
* in the commit message, and then customize Rush's message to contain that string.
|
* in the commit message, and then customize Rush's message to contain that string.
|
||||||
*/
|
*/
|
||||||
// "versionBumpCommitMessage": "Bump versions [skip ci]",
|
// "versionBumpCommitMessage": "Bump versions [skip ci]",
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The commit message to use when committing changes during 'rush version'.
|
* The commit message to use when committing changes during 'rush version'.
|
||||||
*
|
*
|
||||||
@ -192,7 +189,6 @@
|
|||||||
* in the commit message, and then customize Rush's message to contain that string.
|
* in the commit message, and then customize Rush's message to contain that string.
|
||||||
*/
|
*/
|
||||||
// "changeLogUpdateCommitMessage": "Update changelogs [skip ci]",
|
// "changeLogUpdateCommitMessage": "Update changelogs [skip ci]",
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The commit message to use when committing changefiles during 'rush change --commit'
|
* The commit message to use when committing changefiles during 'rush change --commit'
|
||||||
*
|
*
|
||||||
@ -218,13 +214,11 @@
|
|||||||
* to retrieve the latest activity for the remote main branch.
|
* to retrieve the latest activity for the remote main branch.
|
||||||
*/
|
*/
|
||||||
// "url": "https://github.com/microsoft/rush-example",
|
// "url": "https://github.com/microsoft/rush-example",
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The default branch name. This tells "rush change" which remote branch to compare against.
|
* The default branch name. This tells "rush change" which remote branch to compare against.
|
||||||
* The default value is "main"
|
* The default value is "main"
|
||||||
*/
|
*/
|
||||||
// "defaultBranch": "main",
|
// "defaultBranch": "main",
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The default remote. This tells "rush change" which remote to compare against if the remote URL is
|
* The default remote. This tells "rush change" which remote to compare against if the remote URL is
|
||||||
* not set or if a remote matching the provided remote URL is not found.
|
* not set or if a remote matching the provided remote URL is not found.
|
||||||
@ -605,6 +599,12 @@
|
|||||||
"versionPolicyName": "publishPolicy",
|
"versionPolicyName": "publishPolicy",
|
||||||
"tags": ["level-1", "team-flow"]
|
"tags": ["level-1", "team-flow"]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"packageName": "@flowgram.ai/free-group-plugin",
|
||||||
|
"projectFolder": "packages/plugins/free-group-plugin",
|
||||||
|
"versionPolicyName": "publishPolicy",
|
||||||
|
"tags": ["level-1", "team-flow"]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"packageName": "@flowgram.ai/group-plugin",
|
"packageName": "@flowgram.ai/group-plugin",
|
||||||
"projectFolder": "packages/plugins/group-plugin",
|
"projectFolder": "packages/plugins/group-plugin",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user