chore: import upstream snapshot with attribution
CI / build (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:09:51 +08:00
commit 0232b4e2bb
3528 changed files with 291404 additions and 0 deletions
@@ -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);
};
+49
View File
@@ -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';