mirror of
https://gitee.com/ByteDance/flowgram.ai.git
synced 2025-07-07 17:43:29 +08:00
feat(free-demo): support comment node (#165)
* feat(demo): comment node render component * feat(demo): comment node register to editor * feat(demo): comment node editor component * feat(demo): comment node editor placeholder * feat(demo): toolbar create comment node * fix(demo): scrolling issue when comment loses focus
This commit is contained in:
parent
0bd90c2fce
commit
88f7ccae37
19
apps/demo-free-layout/src/assets/icon-comment.tsx
Normal file
19
apps/demo-free-layout/src/assets/icon-comment.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
import { CSSProperties, FC } from 'react';
|
||||
|
||||
interface IconCommentProps {
|
||||
style?: CSSProperties;
|
||||
}
|
||||
|
||||
export const IconComment: FC<IconCommentProps> = ({ style }) => (
|
||||
<svg
|
||||
width="1em"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
style={style}
|
||||
>
|
||||
<path d="M6.5 9C5.94772 9 5.5 9.44772 5.5 10V11C5.5 11.5523 5.94772 12 6.5 12H7.5C8.05228 12 8.5 11.5523 8.5 11V10C8.5 9.44772 8.05228 9 7.5 9H6.5zM11.5 9C10.9477 9 10.5 9.44772 10.5 10V11C10.5 11.5523 10.9477 12 11.5 12H12.5C13.0523 12 13.5 11.5523 13.5 11V10C13.5 9.44772 13.0523 9 12.5 9H11.5zM15.5 10C15.5 9.44772 15.9477 9 16.5 9H17.5C18.0523 9 18.5 9.44772 18.5 10V11C18.5 11.5523 18.0523 12 17.5 12H16.5C15.9477 12 15.5 11.5523 15.5 11V10z"></path>
|
||||
<path d="M23 4C23 2.9 22.1 2 21 2H3C1.9 2 1 2.9 1 4V17.0111C1 18.0211 1.9 19.0111 3 19.0111H7.7586L10.4774 22C10.9822 22.5017 11.3166 22.6311 12 22.7009C12.414 22.707 13.0502 22.5093 13.5 22L16.2414 19.0111H21C22.1 19.0111 23 18.1111 23 17.0111V4ZM3 4H21V17.0111H15.5L12 20.6714L8.5 17.0111H3V4Z"></path>
|
||||
</svg>
|
||||
);
|
||||
@ -0,0 +1,43 @@
|
||||
import type { FC } from 'react';
|
||||
|
||||
import { useNodeRender, usePlayground } from '@flowgram.ai/free-layout-editor';
|
||||
|
||||
import type { CommentEditorModel } from '../model';
|
||||
import { DragArea } from './drag-area';
|
||||
|
||||
interface IBlankArea {
|
||||
model: CommentEditorModel;
|
||||
}
|
||||
|
||||
export const BlankArea: FC<IBlankArea> = (props) => {
|
||||
const { model } = props;
|
||||
const playground = usePlayground();
|
||||
const { selectNode } = useNodeRender();
|
||||
|
||||
return (
|
||||
<div
|
||||
className="workflow-comment-blank-area h-full w-full"
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
model.setFocus(false);
|
||||
selectNode(e);
|
||||
playground.node.focus(); // 防止节点无法被删除
|
||||
}}
|
||||
onClick={(e) => {
|
||||
model.setFocus(true);
|
||||
model.selectEnd();
|
||||
}}
|
||||
>
|
||||
<DragArea
|
||||
style={{
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
}}
|
||||
model={model}
|
||||
stopEvent={false}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,115 @@
|
||||
import { type FC } from 'react';
|
||||
|
||||
import type { CommentEditorModel } from '../model';
|
||||
import { ResizeArea } from './resize-area';
|
||||
import { DragArea } from './drag-area';
|
||||
|
||||
interface IBorderArea {
|
||||
model: CommentEditorModel;
|
||||
overflow: boolean;
|
||||
onResize?: () => {
|
||||
resizing: (delta: { top: number; right: number; bottom: number; left: number }) => void;
|
||||
resizeEnd: () => void;
|
||||
};
|
||||
}
|
||||
|
||||
export const BorderArea: FC<IBorderArea> = (props) => {
|
||||
const { model, overflow, onResize } = props;
|
||||
|
||||
return (
|
||||
<div style={{ zIndex: 999 }}>
|
||||
{/* 左边 */}
|
||||
<DragArea
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: -10,
|
||||
top: 10,
|
||||
width: 20,
|
||||
height: 'calc(100% - 20px)',
|
||||
}}
|
||||
model={model}
|
||||
/>
|
||||
{/* 右边 */}
|
||||
<DragArea
|
||||
style={{
|
||||
position: 'absolute',
|
||||
right: -10,
|
||||
top: 10,
|
||||
height: 'calc(100% - 20px)',
|
||||
width: overflow ? 10 : 20, // 防止遮挡滚动条
|
||||
}}
|
||||
model={model}
|
||||
/>
|
||||
{/* 上边 */}
|
||||
<DragArea
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: -10,
|
||||
left: 10,
|
||||
width: 'calc(100% - 20px)',
|
||||
height: 20,
|
||||
}}
|
||||
model={model}
|
||||
/>
|
||||
{/* 下边 */}
|
||||
<DragArea
|
||||
style={{
|
||||
position: 'absolute',
|
||||
bottom: -10,
|
||||
left: 10,
|
||||
width: 'calc(100% - 20px)',
|
||||
height: 20,
|
||||
}}
|
||||
model={model}
|
||||
/>
|
||||
{/** 左上角 */}
|
||||
<ResizeArea
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: 0,
|
||||
cursor: 'nwse-resize',
|
||||
}}
|
||||
model={model}
|
||||
getDelta={({ x, y }) => ({ top: y, right: 0, bottom: 0, left: x })}
|
||||
onResize={onResize}
|
||||
/>
|
||||
{/** 右上角 */}
|
||||
<ResizeArea
|
||||
style={{
|
||||
position: 'absolute',
|
||||
right: 0,
|
||||
top: 0,
|
||||
cursor: 'nesw-resize',
|
||||
}}
|
||||
model={model}
|
||||
getDelta={({ x, y }) => ({ top: y, right: x, bottom: 0, left: 0 })}
|
||||
onResize={onResize}
|
||||
/>
|
||||
{/** 右下角 */}
|
||||
<ResizeArea
|
||||
style={{
|
||||
position: 'absolute',
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
cursor: 'nwse-resize',
|
||||
}}
|
||||
model={model}
|
||||
getDelta={({ x, y }) => ({ top: 0, right: x, bottom: y, left: 0 })}
|
||||
onResize={onResize}
|
||||
/>
|
||||
{/** 左下角 */}
|
||||
<ResizeArea
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
cursor: 'nesw-resize',
|
||||
}}
|
||||
model={model}
|
||||
getDelta={({ x, y }) => ({ top: 0, right: 0, bottom: y, left: x })}
|
||||
onResize={onResize}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,47 @@
|
||||
import type { ReactNode, FC, CSSProperties } from 'react';
|
||||
|
||||
interface ICommentContainer {
|
||||
focused: boolean;
|
||||
overflow: boolean;
|
||||
children?: ReactNode;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
export const CommentContainer: FC<ICommentContainer> = (props) => {
|
||||
const { focused, overflow, children, style } = props;
|
||||
|
||||
const scrollbarStyle = {
|
||||
// 滚动条样式
|
||||
scrollbarWidth: 'thin',
|
||||
scrollbarColor: 'rgb(159 159 158 / 65%) transparent',
|
||||
// 针对 WebKit 浏览器(如 Chrome、Safari)的样式
|
||||
'&::-webkit-scrollbar': {
|
||||
width: '4px',
|
||||
},
|
||||
'&::-webkit-scrollbar-track': {
|
||||
background: 'transparent',
|
||||
},
|
||||
'&::-webkit-scrollbar-thumb': {
|
||||
backgroundColor: 'rgb(159 159 158 / 65%)',
|
||||
borderRadius: '20px',
|
||||
border: '2px solid transparent',
|
||||
},
|
||||
} as unknown as CSSProperties;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="workflow-comment-container"
|
||||
data-flow-editor-selectable="false"
|
||||
style={{
|
||||
// tailwind 不支持 outline 的样式,所以这里需要使用 style 来设置
|
||||
outline: focused ? '1px solid #FF811A' : '1px solid #F2B600',
|
||||
backgroundColor: focused ? '#FFF3EA' : '#FFFBED',
|
||||
// paddingRight: overflow ? 0 : undefined,
|
||||
...scrollbarStyle,
|
||||
...style,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,89 @@
|
||||
import { type FC, useState, useEffect, type WheelEventHandler } from 'react';
|
||||
|
||||
import { useNodeRender, usePlayground } from '@flowgram.ai/free-layout-editor';
|
||||
|
||||
import type { CommentEditorModel } from '../model';
|
||||
import { DragArea } from './drag-area';
|
||||
|
||||
interface IContentDragArea {
|
||||
model: CommentEditorModel;
|
||||
focused: boolean;
|
||||
overflow: boolean;
|
||||
}
|
||||
|
||||
export const ContentDragArea: FC<IContentDragArea> = (props) => {
|
||||
const { model, focused, overflow } = props;
|
||||
const playground = usePlayground();
|
||||
const { selectNode } = useNodeRender();
|
||||
|
||||
const [active, setActive] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// 当编辑器失去焦点时,取消激活状态
|
||||
if (!focused) {
|
||||
setActive(false);
|
||||
}
|
||||
}, [focused]);
|
||||
|
||||
const handleWheel: WheelEventHandler<HTMLDivElement> = (e) => {
|
||||
const editorElement = model.element;
|
||||
if (active || !overflow || !editorElement) {
|
||||
return;
|
||||
}
|
||||
e.stopPropagation();
|
||||
const maxScroll = editorElement.scrollHeight - editorElement.clientHeight;
|
||||
const newScrollTop = Math.min(Math.max(editorElement.scrollTop + e.deltaY, 0), maxScroll);
|
||||
editorElement.scroll(0, newScrollTop);
|
||||
};
|
||||
|
||||
const handleMouseDown = (mouseDownEvent: React.MouseEvent) => {
|
||||
if (active) {
|
||||
return;
|
||||
}
|
||||
mouseDownEvent.preventDefault();
|
||||
mouseDownEvent.stopPropagation();
|
||||
model.setFocus(false);
|
||||
selectNode(mouseDownEvent);
|
||||
playground.node.focus(); // 防止节点无法被删除
|
||||
|
||||
const startX = mouseDownEvent.clientX;
|
||||
const startY = mouseDownEvent.clientY;
|
||||
|
||||
const handleMouseUp = (mouseMoveEvent: MouseEvent) => {
|
||||
const deltaX = mouseMoveEvent.clientX - startX;
|
||||
const deltaY = mouseMoveEvent.clientY - startY;
|
||||
// 判断是拖拽还是点击
|
||||
const delta = 5;
|
||||
if (Math.abs(deltaX) < delta && Math.abs(deltaY) < delta) {
|
||||
// 点击后隐藏
|
||||
setActive(true);
|
||||
}
|
||||
document.removeEventListener('mouseup', handleMouseUp);
|
||||
document.removeEventListener('click', handleMouseUp);
|
||||
};
|
||||
|
||||
document.addEventListener('mouseup', handleMouseUp);
|
||||
document.addEventListener('click', handleMouseUp);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="workflow-comment-content-drag-area"
|
||||
onMouseDown={handleMouseDown}
|
||||
onWheel={handleWheel}
|
||||
style={{
|
||||
display: active ? 'none' : undefined,
|
||||
}}
|
||||
>
|
||||
<DragArea
|
||||
style={{
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
}}
|
||||
model={model}
|
||||
stopEvent={false}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,40 @@
|
||||
import { CSSProperties, type FC } from 'react';
|
||||
|
||||
import { useNodeRender, usePlayground } from '@flowgram.ai/free-layout-editor';
|
||||
|
||||
import { type CommentEditorModel } from '../model';
|
||||
|
||||
interface IDragArea {
|
||||
model: CommentEditorModel;
|
||||
stopEvent?: boolean;
|
||||
style?: CSSProperties;
|
||||
}
|
||||
|
||||
export const DragArea: FC<IDragArea> = (props) => {
|
||||
const { model, stopEvent = true, style } = props;
|
||||
|
||||
const playground = usePlayground();
|
||||
|
||||
const { startDrag: onStartDrag, onFocus, onBlur, selectNode } = useNodeRender();
|
||||
|
||||
return (
|
||||
<div
|
||||
className="workflow-comment-drag-area"
|
||||
data-flow-editor-selectable="false"
|
||||
draggable={true}
|
||||
style={style}
|
||||
onMouseDown={(e) => {
|
||||
if (stopEvent) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
model.setFocus(false);
|
||||
onStartDrag(e);
|
||||
selectNode(e);
|
||||
playground.node.focus(); // 防止节点无法被删除
|
||||
}}
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,71 @@
|
||||
import { type FC, type CSSProperties, useEffect, useRef, useState, useMemo } from 'react';
|
||||
|
||||
import { usePlayground } from '@flowgram.ai/free-layout-editor';
|
||||
|
||||
import { CommentEditorModel } from '../model';
|
||||
import { CommentEditorEvent } from '../constant';
|
||||
|
||||
interface ICommentEditor {
|
||||
model: CommentEditorModel;
|
||||
style?: CSSProperties;
|
||||
value?: string;
|
||||
onChange?: (value: string) => void;
|
||||
}
|
||||
|
||||
export const CommentEditor: FC<ICommentEditor> = (props) => {
|
||||
const { model, style, onChange } = props;
|
||||
const playground = usePlayground();
|
||||
const editorRef = useRef<HTMLTextAreaElement | null>(null);
|
||||
const [value, setValue] = useState(model.value);
|
||||
const [focused, setFocus] = useState(false);
|
||||
|
||||
const placeholder: string | undefined = useMemo(() => {
|
||||
if (value || focused) {
|
||||
return;
|
||||
}
|
||||
return 'Enter a comment...';
|
||||
}, [value, focused]);
|
||||
|
||||
// 同步编辑器内部值变化
|
||||
useEffect(() => {
|
||||
const disposer = model.on((params) => {
|
||||
if (params.type !== CommentEditorEvent.Change) {
|
||||
return;
|
||||
}
|
||||
onChange?.(model.value);
|
||||
});
|
||||
return () => disposer.dispose();
|
||||
}, [model, onChange]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!editorRef.current) {
|
||||
return;
|
||||
}
|
||||
model.element = editorRef.current;
|
||||
}, [editorRef]);
|
||||
|
||||
return (
|
||||
<div className="workflow-comment-editor">
|
||||
<p className="workflow-comment-editor-placeholder">{placeholder}</p>
|
||||
<textarea
|
||||
className="workflow-comment-editor-textarea"
|
||||
ref={editorRef}
|
||||
style={style}
|
||||
readOnly={playground.config.readonly}
|
||||
onChange={(e) => {
|
||||
const { value } = e.target;
|
||||
model.setValue(value);
|
||||
setValue(value);
|
||||
}}
|
||||
onFocus={() => {
|
||||
model.setFocus(true);
|
||||
setFocus(true);
|
||||
}}
|
||||
onBlur={() => {
|
||||
model.setFocus(false);
|
||||
setFocus(false);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,70 @@
|
||||
.workflow-comment {
|
||||
width: auto;
|
||||
height: auto;
|
||||
min-width: 120px;
|
||||
min-height: 80px;
|
||||
}
|
||||
|
||||
.workflow-comment-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 8px;
|
||||
outline: 1px solid;
|
||||
padding: 6px 2px 6px 10px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.workflow-comment-drag-area {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
.workflow-comment-content-drag-area {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: calc(100% - 22px);
|
||||
}
|
||||
|
||||
.workflow-comment-resize-area {
|
||||
position: absolute;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
.workflow-comment-editor {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.workflow-comment-editor-placeholder {
|
||||
margin: 0;
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
color: rgba(55, 67, 106, 0.38);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.workflow-comment-editor-textarea {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
appearance: none;
|
||||
border: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
background: none;
|
||||
color: inherit;
|
||||
font-family: inherit;
|
||||
font-size: 16px;
|
||||
resize: none;
|
||||
outline: none;
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
import './index.css';
|
||||
|
||||
export { CommentRender } from './render';
|
||||
@ -0,0 +1,78 @@
|
||||
import { FC } from 'react';
|
||||
|
||||
import {
|
||||
Field,
|
||||
FieldRenderProps,
|
||||
FlowNodeFormData,
|
||||
Form,
|
||||
FormModelV2,
|
||||
useNodeRender,
|
||||
WorkflowNodeEntity,
|
||||
} from '@flowgram.ai/free-layout-editor';
|
||||
|
||||
import { useOverflow } from '../hooks/use-overflow';
|
||||
import { useModel } from '../hooks/use-model';
|
||||
import { useSize } from '../hooks';
|
||||
import { CommentEditorFormField } from '../constant';
|
||||
import { CommentEditor } from './editor';
|
||||
import { ContentDragArea } from './content-drag-area';
|
||||
import { CommentContainer } from './container';
|
||||
import { BorderArea } from './border-area';
|
||||
import { BlankArea } from './blank-area';
|
||||
|
||||
export const CommentRender: FC<{
|
||||
node: WorkflowNodeEntity;
|
||||
}> = (props) => {
|
||||
const { node } = props;
|
||||
const model = useModel();
|
||||
|
||||
const { selected: focused, selectNode, nodeRef } = useNodeRender();
|
||||
|
||||
const formModel = node.getData(FlowNodeFormData).getFormModel<FormModelV2>();
|
||||
const formControl = formModel?.formControl;
|
||||
|
||||
const { width, height, onResize } = useSize();
|
||||
const { overflow, updateOverflow } = useOverflow({ model, height });
|
||||
|
||||
return (
|
||||
<div
|
||||
className="workflow-comment"
|
||||
style={{
|
||||
width,
|
||||
height,
|
||||
}}
|
||||
ref={nodeRef}
|
||||
data-node-selected={String(focused)}
|
||||
onMouseEnter={updateOverflow}
|
||||
onMouseDown={(e) => {
|
||||
setTimeout(() => {
|
||||
// 防止 selectNode 拦截事件,导致 slate 编辑器无法聚焦
|
||||
selectNode(e);
|
||||
// eslint-disable-next-line @typescript-eslint/no-magic-numbers -- delay
|
||||
}, 20);
|
||||
}}
|
||||
>
|
||||
<Form control={formControl}>
|
||||
<>
|
||||
{/* 背景 */}
|
||||
<CommentContainer focused={focused} overflow={overflow} style={{ height }}>
|
||||
<Field name={CommentEditorFormField.Note}>
|
||||
{({ field }: FieldRenderProps<string>) => (
|
||||
<>
|
||||
{/** 编辑器 */}
|
||||
<CommentEditor model={model} value={field.value} onChange={field.onChange} />
|
||||
{/* 空白区域 */}
|
||||
<BlankArea model={model} />
|
||||
{/* 内容拖拽区域(点击后隐藏) */}
|
||||
<ContentDragArea model={model} focused={focused} overflow={overflow} />
|
||||
</>
|
||||
)}
|
||||
</Field>
|
||||
</CommentContainer>
|
||||
{/* 边框 */}
|
||||
<BorderArea model={model} overflow={overflow} onResize={onResize} />
|
||||
</>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,73 @@
|
||||
import { CSSProperties, type FC } from 'react';
|
||||
|
||||
import { useNodeRender, usePlayground } from '@flowgram.ai/free-layout-editor';
|
||||
|
||||
import type { CommentEditorModel } from '../model';
|
||||
|
||||
interface IResizeArea {
|
||||
model: CommentEditorModel;
|
||||
onResize?: () => {
|
||||
resizing: (delta: { top: number; right: number; bottom: number; left: number }) => void;
|
||||
resizeEnd: () => void;
|
||||
};
|
||||
getDelta?: (delta: { x: number; y: number }) => {
|
||||
top: number;
|
||||
right: number;
|
||||
bottom: number;
|
||||
left: number;
|
||||
};
|
||||
style?: CSSProperties;
|
||||
}
|
||||
|
||||
export const ResizeArea: FC<IResizeArea> = (props) => {
|
||||
const { model, onResize, getDelta, style } = props;
|
||||
|
||||
const playground = usePlayground();
|
||||
|
||||
const { selectNode } = useNodeRender();
|
||||
|
||||
const handleMouseDown = (mouseDownEvent: React.MouseEvent) => {
|
||||
mouseDownEvent.preventDefault();
|
||||
mouseDownEvent.stopPropagation();
|
||||
if (!onResize) {
|
||||
return;
|
||||
}
|
||||
const { resizing, resizeEnd } = onResize();
|
||||
model.setFocus(false);
|
||||
selectNode(mouseDownEvent);
|
||||
playground.node.focus(); // 防止节点无法被删除
|
||||
|
||||
const startX = mouseDownEvent.clientX;
|
||||
const startY = mouseDownEvent.clientY;
|
||||
|
||||
const handleMouseMove = (mouseMoveEvent: MouseEvent) => {
|
||||
const deltaX = mouseMoveEvent.clientX - startX;
|
||||
const deltaY = mouseMoveEvent.clientY - startY;
|
||||
const delta = getDelta?.({ x: deltaX, y: deltaY });
|
||||
if (!delta || !resizing) {
|
||||
return;
|
||||
}
|
||||
resizing(delta);
|
||||
};
|
||||
|
||||
const handleMouseUp = () => {
|
||||
resizeEnd();
|
||||
document.removeEventListener('mousemove', handleMouseMove);
|
||||
document.removeEventListener('mouseup', handleMouseUp);
|
||||
document.removeEventListener('click', handleMouseUp);
|
||||
};
|
||||
|
||||
document.addEventListener('mousemove', handleMouseMove);
|
||||
document.addEventListener('mouseup', handleMouseUp);
|
||||
document.addEventListener('click', handleMouseUp);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="workflow-comment-resize-area"
|
||||
style={style}
|
||||
data-flow-editor-selectable="false"
|
||||
onMouseDown={handleMouseDown}
|
||||
/>
|
||||
);
|
||||
};
|
||||
20
apps/demo-free-layout/src/components/comment/constant.ts
Normal file
20
apps/demo-free-layout/src/components/comment/constant.ts
Normal file
@ -0,0 +1,20 @@
|
||||
/* eslint-disable @typescript-eslint/naming-convention -- enum */
|
||||
|
||||
export enum CommentEditorFormField {
|
||||
Size = 'size',
|
||||
Note = 'note',
|
||||
}
|
||||
|
||||
/** 编辑器事件 */
|
||||
export enum CommentEditorEvent {
|
||||
/** 内容变更事件 */
|
||||
Change = 'change',
|
||||
/** 多选事件 */
|
||||
MultiSelect = 'multiSelect',
|
||||
/** 单选事件 */
|
||||
Select = 'select',
|
||||
/** 失焦事件 */
|
||||
Blur = 'blur',
|
||||
}
|
||||
|
||||
export const CommentEditorDefaultValue = '';
|
||||
@ -0,0 +1 @@
|
||||
export { useSize } from './use-size';
|
||||
@ -0,0 +1,50 @@
|
||||
import { useEffect, useMemo } from 'react';
|
||||
|
||||
import {
|
||||
FlowNodeFormData,
|
||||
FormModelV2,
|
||||
useEntityFromContext,
|
||||
useNodeRender,
|
||||
WorkflowNodeEntity,
|
||||
} from '@flowgram.ai/free-layout-editor';
|
||||
|
||||
import { CommentEditorModel } from '../model';
|
||||
import { CommentEditorFormField } from '../constant';
|
||||
|
||||
export const useModel = () => {
|
||||
const node = useEntityFromContext<WorkflowNodeEntity>();
|
||||
const { selected: focused } = useNodeRender();
|
||||
|
||||
const formModel = node.getData(FlowNodeFormData).getFormModel<FormModelV2>();
|
||||
|
||||
const model = useMemo(() => new CommentEditorModel(), []);
|
||||
|
||||
// 同步失焦状态
|
||||
useEffect(() => {
|
||||
if (focused) {
|
||||
return;
|
||||
}
|
||||
model.setFocus(focused);
|
||||
}, [focused, model]);
|
||||
|
||||
// 同步表单值初始化
|
||||
useEffect(() => {
|
||||
const value = formModel.getValueIn<string>(CommentEditorFormField.Note);
|
||||
model.setValue(value); // 设置初始值
|
||||
model.selectEnd(); // 设置初始化光标位置
|
||||
}, [formModel, model]);
|
||||
|
||||
// 同步表单外部值变化:undo/redo/协同
|
||||
useEffect(() => {
|
||||
const disposer = formModel.onFormValuesChange(({ name }) => {
|
||||
if (name !== CommentEditorFormField.Note) {
|
||||
return;
|
||||
}
|
||||
const value = formModel.getValueIn<string>(CommentEditorFormField.Note);
|
||||
model.setValue(value);
|
||||
});
|
||||
return () => disposer.dispose();
|
||||
}, [formModel, model]);
|
||||
|
||||
return model;
|
||||
};
|
||||
@ -0,0 +1,45 @@
|
||||
import { useCallback, useState, useEffect } from 'react';
|
||||
|
||||
import { usePlayground } from '@flowgram.ai/free-layout-editor';
|
||||
|
||||
import { CommentEditorModel } from '../model';
|
||||
import { CommentEditorEvent } from '../constant';
|
||||
|
||||
export const useOverflow = (params: { model: CommentEditorModel; height: number }) => {
|
||||
const { model, height } = params;
|
||||
const playground = usePlayground();
|
||||
|
||||
const [overflow, setOverflow] = useState(false);
|
||||
|
||||
const isOverflow = useCallback((): boolean => {
|
||||
if (!model.element) {
|
||||
return false;
|
||||
}
|
||||
return model.element.scrollHeight > model.element.clientHeight;
|
||||
}, [model, height, playground]);
|
||||
|
||||
// 更新 overflow
|
||||
const updateOverflow = useCallback(() => {
|
||||
setOverflow(isOverflow());
|
||||
}, [isOverflow]);
|
||||
|
||||
// 监听高度变化
|
||||
useEffect(() => {
|
||||
updateOverflow();
|
||||
}, [height, updateOverflow]);
|
||||
|
||||
// 监听 change 事件
|
||||
useEffect(() => {
|
||||
const changeDisposer = model.on((params) => {
|
||||
if (params.type !== CommentEditorEvent.Change) {
|
||||
return;
|
||||
}
|
||||
updateOverflow();
|
||||
});
|
||||
return () => {
|
||||
changeDisposer.dispose();
|
||||
};
|
||||
}, [model, updateOverflow]);
|
||||
|
||||
return { overflow, updateOverflow };
|
||||
};
|
||||
163
apps/demo-free-layout/src/components/comment/hooks/use-size.ts
Normal file
163
apps/demo-free-layout/src/components/comment/hooks/use-size.ts
Normal file
@ -0,0 +1,163 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import {
|
||||
FlowNodeFormData,
|
||||
FormModelV2,
|
||||
FreeOperationType,
|
||||
HistoryService,
|
||||
TransformData,
|
||||
useCurrentEntity,
|
||||
usePlayground,
|
||||
useService,
|
||||
} from '@flowgram.ai/free-layout-editor';
|
||||
|
||||
import { CommentEditorFormField } from '../constant';
|
||||
|
||||
export const useSize = () => {
|
||||
const node = useCurrentEntity();
|
||||
const nodeMeta = node.getNodeMeta();
|
||||
const playground = usePlayground();
|
||||
const historyService = useService(HistoryService);
|
||||
const { size = { width: 240, height: 150 } } = nodeMeta;
|
||||
const transform = node.getData(TransformData);
|
||||
const formModel = node.getData(FlowNodeFormData).getFormModel<FormModelV2>();
|
||||
const formSize = formModel.getValueIn<{ width: number; height: number }>(
|
||||
CommentEditorFormField.Size
|
||||
);
|
||||
|
||||
const [width, setWidth] = useState(formSize?.width ?? size.width);
|
||||
const [height, setHeight] = useState(formSize?.height ?? size.height);
|
||||
|
||||
// 初始化表单值
|
||||
useEffect(() => {
|
||||
const initSize = formModel.getValueIn<{ width: number; height: number }>(
|
||||
CommentEditorFormField.Size
|
||||
);
|
||||
if (!initSize) {
|
||||
formModel.setValueIn(CommentEditorFormField.Size, {
|
||||
width,
|
||||
height,
|
||||
});
|
||||
}
|
||||
}, [formModel, width, height]);
|
||||
|
||||
// 同步表单外部值变化:初始化/undo/redo/协同
|
||||
useEffect(() => {
|
||||
const disposer = formModel.onFormValuesChange(({ name }) => {
|
||||
if (name !== CommentEditorFormField.Size) {
|
||||
return;
|
||||
}
|
||||
const newSize = formModel.getValueIn<{ width: number; height: number }>(
|
||||
CommentEditorFormField.Size
|
||||
);
|
||||
if (!newSize) {
|
||||
return;
|
||||
}
|
||||
setWidth(newSize.width);
|
||||
setHeight(newSize.height);
|
||||
});
|
||||
return () => disposer.dispose();
|
||||
}, [formModel]);
|
||||
|
||||
const onResize = useCallback(() => {
|
||||
const resizeState = {
|
||||
width,
|
||||
height,
|
||||
originalWidth: width,
|
||||
originalHeight: height,
|
||||
positionX: transform.position.x,
|
||||
positionY: transform.position.y,
|
||||
offsetX: 0,
|
||||
offsetY: 0,
|
||||
};
|
||||
const resizing = (delta: { top: number; right: number; bottom: number; left: number }) => {
|
||||
if (!resizeState) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { zoom } = playground.config;
|
||||
|
||||
const top = delta.top / zoom;
|
||||
const right = delta.right / zoom;
|
||||
const bottom = delta.bottom / zoom;
|
||||
const left = delta.left / zoom;
|
||||
|
||||
const minWidth = 120;
|
||||
const minHeight = 80;
|
||||
|
||||
const newWidth = Math.max(minWidth, resizeState.originalWidth + right - left);
|
||||
const newHeight = Math.max(minHeight, resizeState.originalHeight + bottom - top);
|
||||
|
||||
// 如果宽度或高度小于最小值,则不更新偏移量
|
||||
const newOffsetX =
|
||||
(left > 0 || right < 0) && newWidth <= minWidth
|
||||
? resizeState.offsetX
|
||||
: left / 2 + right / 2;
|
||||
const newOffsetY =
|
||||
(top > 0 || bottom < 0) && newHeight <= minHeight ? resizeState.offsetY : top;
|
||||
|
||||
const newPositionX = resizeState.positionX + newOffsetX;
|
||||
const newPositionY = resizeState.positionY + newOffsetY;
|
||||
|
||||
resizeState.width = newWidth;
|
||||
resizeState.height = newHeight;
|
||||
resizeState.offsetX = newOffsetX;
|
||||
resizeState.offsetY = newOffsetY;
|
||||
|
||||
// 更新状态
|
||||
setWidth(newWidth);
|
||||
setHeight(newHeight);
|
||||
|
||||
// 更新偏移量
|
||||
transform.update({
|
||||
position: {
|
||||
x: newPositionX,
|
||||
y: newPositionY,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const resizeEnd = () => {
|
||||
historyService.transact(() => {
|
||||
historyService.pushOperation(
|
||||
{
|
||||
type: FreeOperationType.dragNodes,
|
||||
value: {
|
||||
ids: [node.id],
|
||||
value: [
|
||||
{
|
||||
x: resizeState.positionX + resizeState.offsetX,
|
||||
y: resizeState.positionY + resizeState.offsetY,
|
||||
},
|
||||
],
|
||||
oldValue: [
|
||||
{
|
||||
x: resizeState.positionX,
|
||||
y: resizeState.positionY,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
noApply: true,
|
||||
}
|
||||
);
|
||||
formModel.setValueIn(CommentEditorFormField.Size, {
|
||||
width: resizeState.width,
|
||||
height: resizeState.height,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
resizing,
|
||||
resizeEnd,
|
||||
};
|
||||
}, [node, width, height, transform, playground, formModel, historyService]);
|
||||
|
||||
return {
|
||||
width,
|
||||
height,
|
||||
onResize,
|
||||
};
|
||||
};
|
||||
1
apps/demo-free-layout/src/components/comment/index.ts
Normal file
1
apps/demo-free-layout/src/components/comment/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { CommentRender } from './components';
|
||||
83
apps/demo-free-layout/src/components/comment/model.ts
Normal file
83
apps/demo-free-layout/src/components/comment/model.ts
Normal file
@ -0,0 +1,83 @@
|
||||
import { Emitter } from '@flowgram.ai/free-layout-editor';
|
||||
|
||||
import { CommentEditorEventParams } from './type';
|
||||
import { CommentEditorDefaultValue, CommentEditorEvent } from './constant';
|
||||
|
||||
export class CommentEditorModel {
|
||||
private innerValue: string = CommentEditorDefaultValue;
|
||||
|
||||
private emitter: Emitter<CommentEditorEventParams> = new Emitter();
|
||||
|
||||
private editor: HTMLTextAreaElement | null = null;
|
||||
|
||||
/** 注册事件 */
|
||||
public on = this.emitter.event;
|
||||
|
||||
/** 获取当前值 */
|
||||
public get value(): string {
|
||||
return this.innerValue;
|
||||
}
|
||||
|
||||
/** 外部设置模型值 */
|
||||
public setValue(value: string = CommentEditorDefaultValue): void {
|
||||
if (value === this.innerValue) {
|
||||
return;
|
||||
}
|
||||
this.innerValue = value;
|
||||
this.emitter.fire({
|
||||
type: CommentEditorEvent.Change,
|
||||
value: this.innerValue,
|
||||
});
|
||||
}
|
||||
|
||||
public set element(el: HTMLTextAreaElement) {
|
||||
if (Boolean(this.editor)) {
|
||||
return;
|
||||
}
|
||||
this.editor = el;
|
||||
}
|
||||
|
||||
/** 获取编辑器 DOM 节点 */
|
||||
public get element(): HTMLTextAreaElement | null {
|
||||
return this.editor;
|
||||
}
|
||||
|
||||
/** 编辑器聚焦/失焦 */
|
||||
public setFocus(focused: boolean): void {
|
||||
if (focused && !this.focused) {
|
||||
this.editor?.focus();
|
||||
} else if (!focused && this.focused) {
|
||||
this.editor?.blur();
|
||||
this.deselect();
|
||||
this.emitter.fire({
|
||||
type: CommentEditorEvent.Blur,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** 选择末尾 */
|
||||
public selectEnd(): void {
|
||||
if (!this.editor) {
|
||||
return;
|
||||
}
|
||||
// 获取文本长度
|
||||
const length = this.editor.value.length;
|
||||
// 将选择范围设置为文本末尾(开始位置和结束位置都是文本长度)
|
||||
this.editor.setSelectionRange(length, length);
|
||||
}
|
||||
|
||||
/** 获取聚焦状态 */
|
||||
public get focused(): boolean {
|
||||
return document.activeElement === this.editor;
|
||||
}
|
||||
|
||||
/** 取消选择文本 */
|
||||
private deselect(): void {
|
||||
const selection: Selection | null = window.getSelection();
|
||||
|
||||
// 清除所有选择区域
|
||||
if (selection) {
|
||||
selection.removeAllRanges();
|
||||
}
|
||||
}
|
||||
}
|
||||
24
apps/demo-free-layout/src/components/comment/type.ts
Normal file
24
apps/demo-free-layout/src/components/comment/type.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import type { CommentEditorEvent } from './constant';
|
||||
|
||||
interface CommentEditorChangeEvent {
|
||||
type: CommentEditorEvent.Change;
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface CommentEditorMultiSelectEvent {
|
||||
type: CommentEditorEvent.MultiSelect;
|
||||
}
|
||||
|
||||
interface CommentEditorSelectEvent {
|
||||
type: CommentEditorEvent.Select;
|
||||
}
|
||||
|
||||
interface CommentEditorBlurEvent {
|
||||
type: CommentEditorEvent.Blur;
|
||||
}
|
||||
|
||||
export type CommentEditorEventParams =
|
||||
| CommentEditorChangeEvent
|
||||
| CommentEditorMultiSelectEvent
|
||||
| CommentEditorSelectEvent
|
||||
| CommentEditorBlurEvent;
|
||||
@ -1,3 +1,4 @@
|
||||
export * from './base-node';
|
||||
export * from './line-add-button';
|
||||
export * from './node-panel';
|
||||
export * from './comment';
|
||||
|
||||
@ -5,7 +5,7 @@ import { NodePanelRenderProps } from '@flowgram.ai/free-node-panel-plugin';
|
||||
import { useClientContext } from '@flowgram.ai/free-layout-editor';
|
||||
|
||||
import { FlowNodeRegistry } from '../../typings';
|
||||
import { nodeRegistries } from '../../nodes';
|
||||
import { visibleNodeRegistries } from '../../nodes';
|
||||
|
||||
const NodeWrap = styled.div`
|
||||
width: 100%;
|
||||
@ -71,11 +71,13 @@ export const NodeList: FC<NodeListProps> = (props) => {
|
||||
};
|
||||
return (
|
||||
<NodesWrap style={{ width: 80 * 2 + 20 }}>
|
||||
{nodeRegistries.map((registry) => (
|
||||
{visibleNodeRegistries.map((registry) => (
|
||||
<Node
|
||||
key={registry.type}
|
||||
disabled={!(registry.canAdd?.(context) ?? true)}
|
||||
icon={<img style={{ width: 10, height: 10, borderRadius: 4 }} src={registry.info.icon} />}
|
||||
icon={
|
||||
<img style={{ width: 10, height: 10, borderRadius: 4 }} src={registry.info?.icon} />
|
||||
}
|
||||
label={registry.type as string}
|
||||
onClick={(e) => handleClick(e, registry)}
|
||||
/>
|
||||
|
||||
76
apps/demo-free-layout/src/components/tools/comment.tsx
Normal file
76
apps/demo-free-layout/src/components/tools/comment.tsx
Normal file
@ -0,0 +1,76 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
|
||||
import {
|
||||
delay,
|
||||
usePlayground,
|
||||
useService,
|
||||
WorkflowDocument,
|
||||
WorkflowDragService,
|
||||
WorkflowSelectService,
|
||||
} from '@flowgram.ai/free-layout-editor';
|
||||
import { IconButton, Tooltip } from '@douyinfe/semi-ui';
|
||||
|
||||
import { WorkflowNodeType } from '../../nodes';
|
||||
import { IconComment } from '../../assets/icon-comment';
|
||||
|
||||
export const Comment = () => {
|
||||
const playground = usePlayground();
|
||||
const document = useService(WorkflowDocument);
|
||||
const selectService = useService(WorkflowSelectService);
|
||||
const dragService = useService(WorkflowDragService);
|
||||
|
||||
const [tooltipVisible, setTooltipVisible] = useState(false);
|
||||
|
||||
const calcNodePosition = useCallback(
|
||||
(mouseEvent: React.MouseEvent<HTMLButtonElement>) => {
|
||||
const mousePosition = playground.config.getPosFromMouseEvent(mouseEvent);
|
||||
return {
|
||||
x: mousePosition.x,
|
||||
y: mousePosition.y - 75,
|
||||
};
|
||||
},
|
||||
[playground]
|
||||
);
|
||||
|
||||
const createComment = useCallback(
|
||||
async (mouseEvent: React.MouseEvent<HTMLButtonElement>) => {
|
||||
setTooltipVisible(false);
|
||||
const canvasPosition = calcNodePosition(mouseEvent);
|
||||
// 创建节点
|
||||
const node = document.createWorkflowNodeByType(WorkflowNodeType.Comment, canvasPosition);
|
||||
// 等待节点渲染
|
||||
await delay(16);
|
||||
// 选中节点
|
||||
selectService.selectNode(node);
|
||||
// 开始拖拽
|
||||
dragService.startDragSelectedNodes(mouseEvent);
|
||||
},
|
||||
[selectService, calcNodePosition, document, dragService]
|
||||
);
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
trigger="custom"
|
||||
visible={tooltipVisible}
|
||||
onVisibleChange={setTooltipVisible}
|
||||
content="Comment"
|
||||
>
|
||||
<IconButton
|
||||
disabled={playground.config.readonly}
|
||||
icon={
|
||||
<IconComment
|
||||
style={{
|
||||
width: 16,
|
||||
height: 16,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
type="tertiary"
|
||||
theme="borderless"
|
||||
onClick={createComment}
|
||||
onMouseEnter={() => setTooltipVisible(true)}
|
||||
onMouseLeave={() => setTooltipVisible(false)}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
@ -15,6 +15,7 @@ import { MinimapSwitch } from './minimap-switch';
|
||||
import { Minimap } from './minimap';
|
||||
import { Interactive } from './interactive';
|
||||
import { FitView } from './fit-view';
|
||||
import { Comment } from './comment';
|
||||
import { AutoLayout } from './auto-layout';
|
||||
|
||||
export const DemoTools = () => {
|
||||
@ -47,6 +48,7 @@ export const DemoTools = () => {
|
||||
<MinimapSwitch minimapVisible={minimapVisible} setMinimapVisible={setMinimapVisible} />
|
||||
<Minimap visible={minimapVisible} />
|
||||
<Readonly />
|
||||
<Comment />
|
||||
<Tooltip content="Undo">
|
||||
<IconButton
|
||||
type="tertiary"
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { usePlayground } from '@flowgram.ai/free-layout-editor';
|
||||
import { IconButton } from '@douyinfe/semi-ui';
|
||||
import { IconButton, Tooltip } from '@douyinfe/semi-ui';
|
||||
import { IconUnlock, IconLock } from '@douyinfe/semi-icons';
|
||||
|
||||
export const Readonly = () => {
|
||||
@ -10,18 +10,22 @@ export const Readonly = () => {
|
||||
playground.config.readonly = !playground.config.readonly;
|
||||
}, [playground]);
|
||||
return playground.config.readonly ? (
|
||||
<Tooltip content="Editable">
|
||||
<IconButton
|
||||
theme="borderless"
|
||||
type="tertiary"
|
||||
icon={<IconLock size="default" />}
|
||||
onClick={toggleReadonly}
|
||||
/>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Tooltip content="Readonly">
|
||||
<IconButton
|
||||
theme="borderless"
|
||||
type="tertiary"
|
||||
icon={<IconUnlock size="default" />}
|
||||
onClick={toggleReadonly}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
@ -15,8 +15,9 @@ import { shortcuts } from '../shortcuts';
|
||||
import { CustomService } from '../services';
|
||||
import { createSyncVariablePlugin } from '../plugins';
|
||||
import { defaultFormMeta } from '../nodes/default-form-meta';
|
||||
import { WorkflowNodeType } from '../nodes';
|
||||
import { SelectorBoxPopover } from '../components/selector-box-popover';
|
||||
import { BaseNode, LineAddButton, NodePanel } from '../components';
|
||||
import { BaseNode, CommentRender, LineAddButton, NodePanel } from '../components';
|
||||
|
||||
export function useEditorProps(
|
||||
initialData: FlowDocumentJSON,
|
||||
@ -104,6 +105,9 @@ export function useEditorProps(
|
||||
* Render Node
|
||||
*/
|
||||
renderDefaultNode: BaseNode,
|
||||
renderNodes: {
|
||||
[WorkflowNodeType.Comment]: CommentRender,
|
||||
},
|
||||
},
|
||||
/**
|
||||
* Node engine enable, you can configure formMeta in the FlowNodeRegistry
|
||||
|
||||
21
apps/demo-free-layout/src/nodes/comment/index.tsx
Normal file
21
apps/demo-free-layout/src/nodes/comment/index.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import { WorkflowNodeType } from '../constants';
|
||||
import { FlowNodeRegistry } from '../../typings';
|
||||
|
||||
export const CommentNodeRegistry: FlowNodeRegistry = {
|
||||
type: WorkflowNodeType.Comment,
|
||||
meta: {
|
||||
isStart: true,
|
||||
isNodeEnd: true,
|
||||
disableSideSheet: true,
|
||||
renderKey: WorkflowNodeType.Comment,
|
||||
size: {
|
||||
width: 240,
|
||||
height: 150,
|
||||
},
|
||||
},
|
||||
formMeta: {
|
||||
render: () => <></>,
|
||||
},
|
||||
getInputPoints: () => [], // Comment 节点没有输入
|
||||
getOutputPoints: () => [], // Comment 节点没有输出
|
||||
};
|
||||
@ -4,4 +4,5 @@ export enum WorkflowNodeType {
|
||||
LLM = 'llm',
|
||||
Condition = 'condition',
|
||||
Loop = 'loop',
|
||||
Comment = 'comment',
|
||||
}
|
||||
|
||||
@ -3,7 +3,9 @@ import { StartNodeRegistry } from './start';
|
||||
import { LoopNodeRegistry } from './loop';
|
||||
import { LLMNodeRegistry } from './llm';
|
||||
import { EndNodeRegistry } from './end';
|
||||
import { WorkflowNodeType } from './constants';
|
||||
import { ConditionNodeRegistry } from './condition';
|
||||
import { CommentNodeRegistry } from './comment';
|
||||
export { WorkflowNodeType } from './constants';
|
||||
|
||||
export const nodeRegistries: FlowNodeRegistry[] = [
|
||||
@ -12,4 +14,9 @@ export const nodeRegistries: FlowNodeRegistry[] = [
|
||||
EndNodeRegistry,
|
||||
LLMNodeRegistry,
|
||||
LoopNodeRegistry,
|
||||
CommentNodeRegistry,
|
||||
];
|
||||
|
||||
export const visibleNodeRegistries = nodeRegistries.filter(
|
||||
(r) => r.type !== WorkflowNodeType.Comment
|
||||
);
|
||||
|
||||
@ -46,7 +46,7 @@ export interface FlowNodeJSON extends FlowNodeJSONDefault {
|
||||
* 你可以自定义节点的注册器
|
||||
*/
|
||||
export interface FlowNodeRegistry extends FlowNodeRegistryDefault {
|
||||
info: {
|
||||
info?: {
|
||||
icon: string;
|
||||
description: string;
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user