This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
const { defineFlatConfig } = require('@flowgram.ai/eslint-config');
|
||||
|
||||
module.exports = defineFlatConfig({
|
||||
preset: 'web',
|
||||
packageRoot: __dirname,
|
||||
});
|
||||
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"name": "@flowgram.ai/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/core": "workspace:*",
|
||||
"@flowgram.ai/document": "workspace:*",
|
||||
"@flowgram.ai/renderer": "workspace:*",
|
||||
"@flowgram.ai/utils": "workspace:*",
|
||||
"inversify": "^6.0.1",
|
||||
"reflect-metadata": "~0.2.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@flowgram.ai/eslint-config": "workspace:*",
|
||||
"@flowgram.ai/ts-config": "workspace:*",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"@vitest/coverage-v8": "^3.2.4",
|
||||
"eslint": "^9.0.0",
|
||||
"tsup": "^8.0.1",
|
||||
"typescript": "^5.8.3",
|
||||
"vitest": "^3.2.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8",
|
||||
"react-dom": ">=16.8"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://registry.npmjs.org/"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
/* eslint-disable react/prop-types */
|
||||
import { useEffect, type CSSProperties } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import type { Rectangle } from '@flowgram.ai/utils';
|
||||
import { FlowGroupController } from '@flowgram.ai/document';
|
||||
|
||||
import { IGroupBox } from '../type';
|
||||
import { useHover } from './hooks';
|
||||
|
||||
export const GroupBox: IGroupBox = (props) => {
|
||||
const { groupNode } = props;
|
||||
const groupController = FlowGroupController.create(groupNode)!;
|
||||
const bounds: Rectangle = groupController.bounds;
|
||||
const { hover, ref } = useHover();
|
||||
|
||||
const positionStyle: CSSProperties = {
|
||||
position: 'absolute',
|
||||
left: bounds.left,
|
||||
top: bounds.top,
|
||||
width: bounds.width,
|
||||
height: bounds.height,
|
||||
};
|
||||
|
||||
const defaultBackgroundStyle: CSSProperties = {
|
||||
borderRadius: 10,
|
||||
zIndex: -1,
|
||||
outline: `${hover ? 2 : 1}px solid rgb(97, 69, 211)`,
|
||||
backgroundColor: 'rgb(236 233 247)',
|
||||
};
|
||||
|
||||
const backgroundStyle = props.backgroundStyle
|
||||
? props.backgroundStyle(groupController)
|
||||
: defaultBackgroundStyle;
|
||||
|
||||
useEffect(() => {
|
||||
groupController.hovered = hover;
|
||||
}, [hover]);
|
||||
|
||||
if (!groupController || groupController.collapsed) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="gedit-group-box" data-group-id={groupNode.id}>
|
||||
<div
|
||||
className="gedit-group-background"
|
||||
ref={ref}
|
||||
style={{
|
||||
...positionStyle,
|
||||
...backgroundStyle,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
/* eslint-disable react/prop-types */
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { FlowGroupController, FlowNodeEntity, FlowNodeRenderData } from '@flowgram.ai/document';
|
||||
import { FlowDocument } from '@flowgram.ai/document';
|
||||
import { useEntityFromContext, useService } from '@flowgram.ai/core';
|
||||
import { delay, Rectangle } from '@flowgram.ai/utils';
|
||||
|
||||
import { IGroupRender } from '../type';
|
||||
|
||||
function useCurrentDomNode(): HTMLDivElement {
|
||||
const entity = useEntityFromContext<FlowNodeEntity>();
|
||||
const renderData = entity.getData<FlowNodeRenderData>(FlowNodeRenderData);
|
||||
return renderData.node;
|
||||
}
|
||||
|
||||
export const GroupRender: IGroupRender = props => {
|
||||
const { groupNode, GroupNode, GroupBoxHeader } = props;
|
||||
const container = useCurrentDomNode();
|
||||
const document = useService<FlowDocument>(FlowDocument);
|
||||
const groupController = FlowGroupController.create(groupNode);
|
||||
|
||||
const [key, setKey] = useState(0);
|
||||
const [rendering, setRendering] = useState(true);
|
||||
const [collapsedCache, setCollapsedCache] = useState(groupController?.collapsed ?? false);
|
||||
|
||||
const rerender = useCallback(async () => {
|
||||
setRendering(true);
|
||||
setKey(key + 1);
|
||||
// 边框bounds计算会有延迟
|
||||
await delay(50);
|
||||
setKey(key + 1);
|
||||
setRendering(false);
|
||||
}, [key]);
|
||||
|
||||
// 监听 collapsed 变化触发重渲染
|
||||
useEffect(() => {
|
||||
const disposer = document.renderTree.onTreeChange(() => {
|
||||
if (groupController?.collapsed !== collapsedCache) {
|
||||
setCollapsedCache(groupController?.collapsed ?? false);
|
||||
rerender();
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
disposer.dispose();
|
||||
};
|
||||
}, [key]);
|
||||
|
||||
// 首次渲染时如果分组是展开状态,此时边框bounds计算会有延迟,需要强制重新渲染
|
||||
useEffect(() => {
|
||||
if (!groupController || groupController.collapsed) {
|
||||
return;
|
||||
}
|
||||
rerender();
|
||||
}, []);
|
||||
|
||||
if (!groupController) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
const groupNodeRender = (
|
||||
<GroupNode key={key} groupNode={groupNode} groupController={groupController} />
|
||||
);
|
||||
const groupBoxHeader = (
|
||||
<GroupBoxHeader key={key} groupController={groupController} groupNode={groupNode} />
|
||||
);
|
||||
|
||||
if (groupController.collapsed) {
|
||||
const positionStyle: Partial<CSSStyleDeclaration> = {
|
||||
display: 'block',
|
||||
zIndex: '0',
|
||||
width: 'auto',
|
||||
height: 'auto',
|
||||
};
|
||||
Object.assign(container.style, positionStyle);
|
||||
return groupNodeRender;
|
||||
} else if (!rendering) {
|
||||
const bounds: Rectangle = groupController.bounds;
|
||||
const positionStyle: Partial<CSSStyleDeclaration> = {
|
||||
width: `${bounds.width}px`,
|
||||
};
|
||||
Object.assign(container.style, positionStyle);
|
||||
return groupBoxHeader;
|
||||
} else {
|
||||
return <></>;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
export const useHover = () => {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const [hover, setHover] = useState(false);
|
||||
|
||||
const checkMouseOver = (event: MouseEvent) => {
|
||||
if (!ref.current) {
|
||||
return;
|
||||
}
|
||||
const { left, top, right, bottom } = ref.current.getBoundingClientRect();
|
||||
const isOver =
|
||||
event.clientX >= left &&
|
||||
event.clientX <= right &&
|
||||
event.clientY >= top &&
|
||||
event.clientY <= bottom;
|
||||
|
||||
setHover(isOver);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('mousemove', checkMouseOver);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('mousemove', checkMouseOver);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return {
|
||||
hover,
|
||||
ref,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
export { GroupRender } from './group-render';
|
||||
export { GroupBox } from './group-box';
|
||||
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
export enum GroupRenderer {
|
||||
GroupRender = 'group_render',
|
||||
GroupBox = 'group_box',
|
||||
}
|
||||
|
||||
export const PositionConfig = {
|
||||
paddingWithNote: 50, // note 留白大小
|
||||
padding: 10, // 无 label 的 padding
|
||||
paddingWithAddLabel: 20, // 有 label 的padding,如要放添加按钮
|
||||
headerHeight: 20, // 基础头部高度
|
||||
};
|
||||
|
||||
export enum GroupPluginRegister {
|
||||
GroupNode = 'registerGroupNode',
|
||||
Render = 'registerRender',
|
||||
Layer = 'registerLayer',
|
||||
CleanGroups = 'registerCleanGroups',
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { definePluginCreator, PluginContext } from '@flowgram.ai/core';
|
||||
|
||||
import { CreateGroupPluginOptions } from './type';
|
||||
import { groupRegisters } from './registers';
|
||||
import { GroupPluginRegister } from './constant';
|
||||
|
||||
/**
|
||||
* 分组插件
|
||||
*/
|
||||
export const createGroupPlugin = definePluginCreator<CreateGroupPluginOptions>({
|
||||
onInit: (ctx: PluginContext, opts: CreateGroupPluginOptions) => {
|
||||
const { registers: registerConfs = {} } = opts;
|
||||
Object.entries(groupRegisters).forEach(([key, register]) => {
|
||||
const registerName = key as GroupPluginRegister;
|
||||
const registerConf = registerConfs[registerName];
|
||||
if (registerConf === false) {
|
||||
return;
|
||||
}
|
||||
if (typeof registerConf === 'function') {
|
||||
registerConf(ctx, opts);
|
||||
return;
|
||||
}
|
||||
register(ctx, opts);
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,167 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { IPoint, PaddingSchema, Point } from '@flowgram.ai/utils';
|
||||
import {
|
||||
FlowGroupController,
|
||||
FlowNodeBaseType,
|
||||
FlowNodeRegistry,
|
||||
FlowNodeTransformData,
|
||||
FlowTransitionLabelEnum,
|
||||
type FlowTransitionLine,
|
||||
FlowTransitionLineEnum,
|
||||
} from '@flowgram.ai/document';
|
||||
|
||||
import { GroupRenderer, PositionConfig } from './constant';
|
||||
|
||||
export const GroupRegister: FlowNodeRegistry = {
|
||||
type: FlowNodeBaseType.GROUP,
|
||||
meta: {
|
||||
exportJSON: true,
|
||||
renderKey: GroupRenderer.GroupRender,
|
||||
positionConfig: PositionConfig,
|
||||
padding: (transform: FlowNodeTransformData): PaddingSchema => {
|
||||
const groupController = FlowGroupController.create(transform.entity);
|
||||
if (!groupController || groupController.collapsed || groupController.nodes.length === 0) {
|
||||
return {
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
};
|
||||
}
|
||||
if (transform.entity.isVertical) {
|
||||
return {
|
||||
top: PositionConfig.paddingWithNote,
|
||||
bottom: PositionConfig.paddingWithAddLabel,
|
||||
left: PositionConfig.padding,
|
||||
right: PositionConfig.padding,
|
||||
};
|
||||
}
|
||||
return {
|
||||
top: PositionConfig.paddingWithNote,
|
||||
bottom: PositionConfig.padding,
|
||||
left: PositionConfig.padding,
|
||||
right: PositionConfig.paddingWithAddLabel,
|
||||
};
|
||||
},
|
||||
},
|
||||
formMeta: {
|
||||
render: () => React.createElement('div'),
|
||||
},
|
||||
getLines(transition) {
|
||||
const { transform } = transition;
|
||||
const lines: FlowTransitionLine[] = [];
|
||||
if (transform.firstChild) {
|
||||
lines.push({
|
||||
type: FlowTransitionLineEnum.STRAIGHT_LINE,
|
||||
from: transform.inputPoint,
|
||||
to: transform.firstChild.inputPoint,
|
||||
});
|
||||
}
|
||||
if (transform.next) {
|
||||
lines.push({
|
||||
type: FlowTransitionLineEnum.STRAIGHT_LINE,
|
||||
from: transform.outputPoint,
|
||||
to: transform.next.inputPoint,
|
||||
});
|
||||
} else {
|
||||
lines.push({
|
||||
type: FlowTransitionLineEnum.STRAIGHT_LINE,
|
||||
from: transform.outputPoint,
|
||||
to: transform.parent!.outputPoint,
|
||||
});
|
||||
}
|
||||
return lines;
|
||||
},
|
||||
getDelta(transform: FlowNodeTransformData): IPoint | undefined {
|
||||
const groupController = FlowGroupController.create(transform.entity);
|
||||
if (!groupController || groupController.collapsed) {
|
||||
return;
|
||||
}
|
||||
if (transform.entity.isVertical) {
|
||||
return {
|
||||
x: 0,
|
||||
y: PositionConfig.paddingWithNote,
|
||||
};
|
||||
}
|
||||
return {
|
||||
x: PositionConfig.padding,
|
||||
y: 0,
|
||||
};
|
||||
},
|
||||
getInputPoint(transform: FlowNodeTransformData): IPoint {
|
||||
const child = transform.firstChild;
|
||||
if (!child) return transform.defaultInputPoint;
|
||||
if (transform.entity.isVertical) {
|
||||
return {
|
||||
x: child.inputPoint.x,
|
||||
y: transform.bounds.topCenter.y,
|
||||
};
|
||||
}
|
||||
return {
|
||||
x: transform.bounds.leftCenter.x,
|
||||
y: child.inputPoint.y,
|
||||
};
|
||||
},
|
||||
getOutputPoint(transform: FlowNodeTransformData): IPoint {
|
||||
const child = transform.lastChild;
|
||||
if (!child) return transform.defaultOutputPoint;
|
||||
if (transform.entity.isVertical) {
|
||||
return {
|
||||
x: child.outputPoint.x,
|
||||
y: child.outputPoint.y + PositionConfig.paddingWithAddLabel / 2,
|
||||
};
|
||||
}
|
||||
return {
|
||||
x: child.outputPoint.x + PositionConfig.paddingWithAddLabel / 2,
|
||||
y: child.outputPoint.y,
|
||||
};
|
||||
},
|
||||
getLabels(transition) {
|
||||
const { transform } = transition;
|
||||
if (transform.next) {
|
||||
if (transform.entity.isVertical) {
|
||||
return [
|
||||
{
|
||||
offset: Point.getMiddlePoint(
|
||||
Point.move(transform.outputPoint, {
|
||||
x: 0,
|
||||
y: PositionConfig.paddingWithAddLabel / 2,
|
||||
}),
|
||||
transform.next.inputPoint
|
||||
),
|
||||
type: FlowTransitionLabelEnum.ADDER_LABEL,
|
||||
},
|
||||
];
|
||||
}
|
||||
return [
|
||||
{
|
||||
offset: Point.getMiddlePoint(
|
||||
Point.move(transform.outputPoint, { x: PositionConfig.paddingWithAddLabel / 2, y: 0 }),
|
||||
transform.next.inputPoint
|
||||
),
|
||||
type: FlowTransitionLabelEnum.ADDER_LABEL,
|
||||
},
|
||||
];
|
||||
}
|
||||
return [
|
||||
{
|
||||
offset: transform.parent!.outputPoint,
|
||||
type: FlowTransitionLabelEnum.ADDER_LABEL,
|
||||
},
|
||||
];
|
||||
},
|
||||
getOriginDeltaY(transform): number {
|
||||
const { children } = transform;
|
||||
if (children.length === 0) {
|
||||
return -transform.size.height * transform.origin.y;
|
||||
}
|
||||
// 这里要加上 y 轴的偏移
|
||||
return -transform.size.height * transform.origin.y - PositionConfig.paddingWithNote;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { inject, injectable } from 'inversify';
|
||||
import { FlowRendererRegistry } from '@flowgram.ai/renderer';
|
||||
import {
|
||||
FlowDocument,
|
||||
FlowDocumentTransformerEntity,
|
||||
FlowGroupController,
|
||||
FlowGroupService,
|
||||
FlowNodeEntity,
|
||||
FlowNodeRenderData,
|
||||
FlowNodeTransformData,
|
||||
} from '@flowgram.ai/document';
|
||||
import { Layer, observeEntity, observeEntityDatas } from '@flowgram.ai/core';
|
||||
import { domUtils } from '@flowgram.ai/utils';
|
||||
|
||||
import { GroupsLayerOptions, IGroupBox } from './type';
|
||||
import { GroupRenderer } from './constant';
|
||||
import { GroupBox } from './components';
|
||||
|
||||
@injectable()
|
||||
export class GroupsLayer extends Layer<GroupsLayerOptions> {
|
||||
public readonly node: HTMLElement;
|
||||
|
||||
@inject(FlowDocument) protected document: FlowDocument;
|
||||
|
||||
@inject(FlowRendererRegistry)
|
||||
protected readonly rendererRegistry: FlowRendererRegistry;
|
||||
|
||||
@inject(FlowGroupService)
|
||||
protected readonly groupService: FlowGroupService;
|
||||
|
||||
@observeEntity(FlowDocumentTransformerEntity)
|
||||
readonly documentTransformer: FlowDocumentTransformerEntity;
|
||||
|
||||
@observeEntityDatas(FlowNodeEntity, FlowNodeRenderData)
|
||||
renderStates: FlowNodeRenderData[];
|
||||
|
||||
@observeEntityDatas(FlowNodeEntity, FlowNodeTransformData)
|
||||
transforms: FlowNodeTransformData[];
|
||||
|
||||
private readonly className = 'gedit-groups-layer';
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.node = domUtils.createDivWithClass(this.className);
|
||||
this.node.style.zIndex = '0';
|
||||
}
|
||||
|
||||
/** 缩放 */
|
||||
public onZoom(scale: number): void {
|
||||
this.node!.style.transform = `scale(${scale})`;
|
||||
}
|
||||
|
||||
public render(): JSX.Element {
|
||||
if (this.documentTransformer.loading) return <></>;
|
||||
this.documentTransformer.refresh();
|
||||
|
||||
return <>{this.renderGroups()}</>;
|
||||
}
|
||||
|
||||
/** 渲染分组 */
|
||||
protected renderGroups(): JSX.Element {
|
||||
const Box = this.renderer || GroupBox;
|
||||
return (
|
||||
<>
|
||||
{this.groups.map(group => (
|
||||
<Box
|
||||
key={group.groupNode.id}
|
||||
groupNode={group.groupNode}
|
||||
backgroundStyle={this.options.groupBoxStyle}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
/** 所有分组 */
|
||||
protected get groups(): FlowGroupController[] {
|
||||
return this.groupService.getAllGroups();
|
||||
}
|
||||
|
||||
protected get renderer(): IGroupBox {
|
||||
return this.rendererRegistry.tryToGetRendererComponent(GroupRenderer.GroupBox)
|
||||
?.renderer as IGroupBox;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
export { FlowGroupController } from '@flowgram.ai/document';
|
||||
export * from './groups-layer';
|
||||
export * from './create-group-plugin';
|
||||
export * from './type';
|
||||
export * from './constant';
|
||||
export * from './group-node-register';
|
||||
export * from './components';
|
||||
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { IGroupPluginRegister } from '../type';
|
||||
import { GroupPluginRegister } from '../constant';
|
||||
import { registerRender } from './register-render';
|
||||
import { registerLayer } from './register-layer';
|
||||
import { registerGroupNode } from './register-group-node';
|
||||
import { registerCleanGroups } from './register-clean-groups';
|
||||
|
||||
export const groupRegisters: Record<GroupPluginRegister, IGroupPluginRegister> = {
|
||||
[GroupPluginRegister.GroupNode]: registerGroupNode,
|
||||
[GroupPluginRegister.Render]: registerRender,
|
||||
[GroupPluginRegister.Layer]: registerLayer,
|
||||
[GroupPluginRegister.CleanGroups]: registerCleanGroups,
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { FlowDocument } from '@flowgram.ai/document';
|
||||
import { FlowGroupService } from '@flowgram.ai/document';
|
||||
|
||||
import { IGroupPluginRegister } from '../type';
|
||||
|
||||
/** 注册清理分组逻辑 */
|
||||
export const registerCleanGroups: IGroupPluginRegister = (ctx, opts) => {
|
||||
const groupService = ctx.get<FlowGroupService>(FlowGroupService);
|
||||
const document = ctx.get<FlowDocument>(FlowDocument);
|
||||
|
||||
const clearInvalidGroups = () => {
|
||||
groupService.getAllGroups().forEach(group => {
|
||||
if (group?.nodes.length !== 0) {
|
||||
return;
|
||||
}
|
||||
if (!group.groupNode.pre) {
|
||||
return;
|
||||
}
|
||||
groupService.deleteGroup(group.groupNode);
|
||||
});
|
||||
};
|
||||
|
||||
document.originTree.onTreeChange(() => {
|
||||
setTimeout(() => {
|
||||
clearInvalidGroups();
|
||||
}, 0);
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { FlowDocument } from '@flowgram.ai/document';
|
||||
|
||||
import { IGroupPluginRegister } from '../type';
|
||||
import { GroupRegister } from '../group-node-register';
|
||||
|
||||
/** 注册分组节点 */
|
||||
export const registerGroupNode: IGroupPluginRegister = ctx => {
|
||||
const document = ctx.get<FlowDocument>(FlowDocument);
|
||||
document.registerFlowNodes(GroupRegister);
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { IGroupPluginRegister } from '../type';
|
||||
import { GroupsLayer } from '../groups-layer';
|
||||
|
||||
/** 注册背景层 */
|
||||
export const registerLayer: IGroupPluginRegister = (ctx, opts) => {
|
||||
ctx.playground.registerLayer(GroupsLayer, opts);
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import React, { FC } from 'react';
|
||||
|
||||
import { FlowRendererRegistry } from '@flowgram.ai/renderer';
|
||||
import { FlowNodeEntity } from '@flowgram.ai/document';
|
||||
|
||||
import { IGroupPluginRegister } from '../type';
|
||||
import { GroupRenderer } from '../constant';
|
||||
import { GroupRender } from '../components';
|
||||
|
||||
/** 注册渲染组件 */
|
||||
export const registerRender: IGroupPluginRegister = (ctx, opts) => {
|
||||
const rendererRegistry = ctx.get<FlowRendererRegistry>(FlowRendererRegistry);
|
||||
const renderer: FC<{ node: FlowNodeEntity }> = props => (
|
||||
<GroupRender
|
||||
groupNode={props.node}
|
||||
GroupNode={opts.components!.GroupNode}
|
||||
GroupBoxHeader={opts.components!.GroupBoxHeader}
|
||||
/>
|
||||
);
|
||||
rendererRegistry.registerReactComponent(GroupRenderer.GroupRender, renderer);
|
||||
};
|
||||
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import type { CSSProperties, FC } from 'react';
|
||||
|
||||
import type { FlowNodeEntity } from '@flowgram.ai/document';
|
||||
import type { FlowGroupController } from '@flowgram.ai/document';
|
||||
import type { LayerOptions, PluginContext } from '@flowgram.ai/core';
|
||||
|
||||
import type { GroupPluginRegister } from './constant';
|
||||
|
||||
export type IGroupBox = FC<{
|
||||
groupNode: FlowNodeEntity;
|
||||
backgroundStyle?: (groupController: FlowGroupController) => CSSProperties;
|
||||
}>;
|
||||
|
||||
export type IGroupRender = FC<{
|
||||
groupNode: FlowNodeEntity;
|
||||
GroupNode: IGroupNode;
|
||||
GroupBoxHeader: IGroupBoxHeader;
|
||||
}>;
|
||||
|
||||
export type IGroupNode = FC<{
|
||||
groupNode: FlowNodeEntity;
|
||||
groupController: FlowGroupController;
|
||||
}>;
|
||||
|
||||
export type IGroupBoxHeader = FC<{
|
||||
groupNode: FlowNodeEntity;
|
||||
groupController: FlowGroupController;
|
||||
}>;
|
||||
|
||||
export interface GroupsLayerOptions extends LayerOptions {
|
||||
groupBoxStyle?: (groupController: FlowGroupController) => CSSProperties;
|
||||
}
|
||||
|
||||
export type IGroupPluginRegister = (ctx: PluginContext, opts: CreateGroupPluginOptions) => void;
|
||||
|
||||
export type CreateGroupPluginOptions = GroupsLayerOptions & {
|
||||
components?: {
|
||||
GroupNode: IGroupNode;
|
||||
GroupBoxHeader: IGroupBoxHeader;
|
||||
};
|
||||
registers?: {
|
||||
[key in GroupPluginRegister]?: IGroupPluginRegister | boolean;
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "@flowgram.ai/ts-config/tsconfig.flow.path.json",
|
||||
"compilerOptions": {
|
||||
"jsx": "react",
|
||||
},
|
||||
"include": ["./src"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
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.*',
|
||||
],
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import 'reflect-metadata';
|
||||
Reference in New Issue
Block a user