@flowgram.ai/panel-manager-plugin 1.0.2 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,13 +6,26 @@
6
6
  import { PluginContext } from '@flowgram.ai/core';
7
7
 
8
8
  import type { PanelFactory, PanelConfig } from '../types';
9
+ import { ResizeBar } from '../components/resize-bar';
10
+ import type { PanelLayerProps } from '../components/panel-layer';
9
11
 
10
12
  export interface PanelManagerConfig {
11
13
  factories: PanelFactory<any>[];
12
14
  right: PanelConfig;
13
15
  bottom: PanelConfig;
14
- getPopupContainer: (ctx: PluginContext) => HTMLElement; // default playground.node.parentElement
16
+ dockedRight: PanelConfig;
17
+ dockedBottom: PanelConfig;
18
+ /** Resizable, and multi-panel options mutually exclusive */
15
19
  autoResize: boolean;
20
+ layerProps: PanelLayerProps;
21
+ resizeBarRender: ({
22
+ size,
23
+ }: {
24
+ size: number;
25
+ direction?: 'vertical' | 'horizontal';
26
+ onResize: (size: number) => void;
27
+ }) => React.ReactNode;
28
+ getPopupContainer: (ctx: PluginContext) => HTMLElement; // default playground.node.parentElement
16
29
  }
17
30
 
18
31
  export const PanelManagerConfig = Symbol('PanelManagerConfig');
@@ -25,9 +38,17 @@ export const defineConfig = (config: Partial<PanelManagerConfig>) => {
25
38
  bottom: {
26
39
  max: 1,
27
40
  },
41
+ dockedRight: {
42
+ max: 1,
43
+ },
44
+ dockedBottom: {
45
+ max: 1,
46
+ },
28
47
  factories: [],
29
- getPopupContainer: (ctx: PluginContext) => ctx.playground.node.parentNode as HTMLElement,
30
48
  autoResize: true,
49
+ layerProps: {},
50
+ resizeBarRender: ResizeBar,
51
+ getPopupContainer: (ctx: PluginContext) => ctx.playground.node.parentNode as HTMLElement,
31
52
  };
32
53
  return {
33
54
  ...defaultConfig,
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ import { createStore, StoreApi } from 'zustand/vanilla';
7
+ import { nanoid } from 'nanoid';
8
+ import { inject, injectable } from 'inversify';
9
+
10
+ import type { PanelFactory, PanelEntityConfig, Area } from '../types';
11
+ import { PanelRestore } from './panel-restore';
12
+
13
+ export const PanelEntityFactory = Symbol('PanelEntityFactory');
14
+ export type PanelEntityFactory = (options: {
15
+ factory: PanelEntityFactoryConstant;
16
+ config: PanelEntityConfigConstant;
17
+ }) => PanelEntity;
18
+
19
+ export const PanelEntityFactoryConstant = Symbol('PanelEntityFactoryConstant');
20
+ export type PanelEntityFactoryConstant = PanelFactory<any>;
21
+ export const PanelEntityConfigConstant = Symbol('PanelEntityConfigConstant');
22
+ export type PanelEntityConfigConstant = PanelEntityConfig<any> & {
23
+ area: Area;
24
+ };
25
+
26
+ const PANEL_SIZE_DEFAULT = 400;
27
+
28
+ interface PanelEntityState {
29
+ size: number;
30
+ }
31
+
32
+ @injectable()
33
+ export class PanelEntity {
34
+ @inject(PanelRestore) restore: PanelRestore;
35
+
36
+ /** 面板工厂 */
37
+ @inject(PanelEntityFactoryConstant) public factory: PanelEntityFactoryConstant;
38
+
39
+ @inject(PanelEntityConfigConstant) public config: PanelEntityConfigConstant;
40
+
41
+ private initialized = false;
42
+
43
+ /** 实例唯一标识 */
44
+ id: string = nanoid();
45
+
46
+ /** 渲染缓存 */
47
+ node: React.ReactNode = null;
48
+
49
+ store: StoreApi<PanelEntityState>;
50
+
51
+ get area() {
52
+ return this.config.area;
53
+ }
54
+
55
+ get key() {
56
+ return this.factory.key;
57
+ }
58
+
59
+ get renderer() {
60
+ if (!this.node) {
61
+ this.node = this.factory.render(this.config.props);
62
+ }
63
+ return this.node;
64
+ }
65
+
66
+ init() {
67
+ if (this.initialized) {
68
+ return;
69
+ }
70
+ this.initialized = true;
71
+ const cache = this.restore.restore<PanelEntityState>(this.key);
72
+ this.store = createStore<PanelEntityState>(() => ({
73
+ size: this.config.defaultSize || this.factory.defaultSize || PANEL_SIZE_DEFAULT,
74
+ ...(cache ?? {}),
75
+ }));
76
+ }
77
+
78
+ dispose() {
79
+ this.restore.store(this.key, this.store.getState());
80
+ }
81
+ }
@@ -45,7 +45,8 @@ export class PanelLayer extends Layer {
45
45
 
46
46
  render(): JSX.Element {
47
47
  if (!this.layout) {
48
- this.layout = createElement(PanelLayerComp);
48
+ const { children, ...layoutProps } = this.panelConfig.layerProps;
49
+ this.layout = createElement(PanelLayerComp, layoutProps, children);
49
50
  }
50
51
  return ReactDOM.createPortal(this.layout, this.panelRoot);
51
52
  }
@@ -4,51 +4,114 @@
4
4
  */
5
5
 
6
6
  import { injectable, inject } from 'inversify';
7
+ import { Emitter } from '@flowgram.ai/utils';
7
8
 
8
9
  import { PanelManagerConfig } from './panel-config';
9
- import type { Area, PanelFactory } from '../types';
10
- import { FloatPanel } from './float-panel';
10
+ import type { Area, PanelEntityConfig, PanelFactory } from '../types';
11
+ import { PanelEntity, PanelEntityFactory } from './panel-factory';
11
12
 
12
13
  @injectable()
13
14
  export class PanelManager {
14
15
  @inject(PanelManagerConfig) readonly config: PanelManagerConfig;
15
16
 
17
+ @inject(PanelEntityFactory) readonly createPanel: PanelEntityFactory;
18
+
16
19
  readonly panelRegistry = new Map<string, PanelFactory<any>>();
17
20
 
18
- right: FloatPanel;
21
+ private panels = new Map<string, PanelEntity>();
22
+
23
+ private onPanelsChangeEvent = new Emitter<void>();
19
24
 
20
- bottom: FloatPanel;
25
+ public onPanelsChange = this.onPanelsChangeEvent.event;
21
26
 
22
27
  init() {
23
28
  this.config.factories.forEach((factory) => this.register(factory));
24
- this.right = new FloatPanel(this.config.right);
25
- this.bottom = new FloatPanel(this.config.bottom);
26
29
  }
27
30
 
31
+ /** registry panel factory */
28
32
  register<T extends any>(factory: PanelFactory<T>) {
29
33
  this.panelRegistry.set(factory.key, factory);
30
34
  }
31
35
 
32
- open(key: string, area: Area = 'right', options?: any) {
36
+ /** open panel */
37
+ public open(key: string, area: Area = 'right', options?: PanelEntityConfig) {
33
38
  const factory = this.panelRegistry.get(key);
34
39
  if (!factory) {
35
40
  return;
36
41
  }
37
- const panel = this.getPanel(area);
38
- panel.open(factory, options);
42
+
43
+ const sameKeyPanels = this.getPanels(area).filter((p) => p.key === key);
44
+ if (!factory.allowDuplicates && sameKeyPanels.length) {
45
+ sameKeyPanels.forEach((p) => this.remove(p.id));
46
+ }
47
+
48
+ const panel = this.createPanel({
49
+ factory,
50
+ config: {
51
+ area,
52
+ ...options,
53
+ },
54
+ });
55
+
56
+ this.panels.set(panel.id, panel);
57
+ this.trim(area);
58
+ this.onPanelsChangeEvent.fire();
59
+ console.log('jxj', this.panels);
39
60
  }
40
61
 
41
- close(key?: string) {
42
- this.right.close(key);
43
- this.bottom.close(key);
62
+ /** close panel */
63
+ public close(key?: string) {
64
+ const panels = this.getPanels();
65
+ const closedPanels = key ? panels.filter((p) => p.key === key) : panels;
66
+ closedPanels.forEach((p) => this.remove(p.id));
67
+ this.onPanelsChangeEvent.fire();
44
68
  }
45
69
 
46
- getPanel(area: Area) {
47
- return area === 'right' ? this.right : this.bottom;
70
+ private trim(area: Area) {
71
+ const panels = this.getPanels(area);
72
+ const areaConfig = this.getAreaConfig(area);
73
+ console.log('jxj', areaConfig.max, panels.length);
74
+ while (panels.length > areaConfig.max) {
75
+ const removed = panels.shift();
76
+ if (removed) {
77
+ this.remove(removed.id);
78
+ }
79
+ }
80
+ }
81
+
82
+ private remove(id: string) {
83
+ const panel = this.panels.get(id);
84
+ if (panel) {
85
+ panel.dispose();
86
+ this.panels.delete(id);
87
+ }
88
+ }
89
+
90
+ getPanels(area?: Area) {
91
+ const panels: PanelEntity[] = [];
92
+ this.panels.forEach((panel) => {
93
+ if (!area || panel.area === area) {
94
+ panels.push(panel);
95
+ }
96
+ });
97
+ return panels;
98
+ }
99
+
100
+ getAreaConfig(area: Area) {
101
+ switch (area) {
102
+ case 'docked-bottom':
103
+ return this.config.dockedBottom;
104
+ case 'docked-right':
105
+ return this.config.dockedRight;
106
+ case 'bottom':
107
+ return this.config.bottom;
108
+ case 'right':
109
+ default:
110
+ return this.config.right;
111
+ }
48
112
  }
49
113
 
50
114
  dispose() {
51
- this.right.dispose();
52
- this.bottom.dispose();
115
+ this.onPanelsChangeEvent.dispose();
53
116
  }
54
117
  }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ import { injectable } from 'inversify';
7
+
8
+ export const PanelRestore = Symbol('PanelRestore');
9
+ export interface PanelRestore {
10
+ store: (k: string, v: any) => void;
11
+ restore: <T>(k: string) => T | undefined;
12
+ }
13
+
14
+ @injectable()
15
+ export class PanelRestoreImpl implements PanelRestore {
16
+ map = new Map<string, any>();
17
+
18
+ store(k: string, v: any) {
19
+ this.map.set(k, v);
20
+ }
21
+
22
+ restore<T>(k: string): T | undefined {
23
+ return this.map.get(k) as T;
24
+ }
25
+ }
package/src/types.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  * SPDX-License-Identifier: MIT
4
4
  */
5
5
 
6
- export type Area = 'right' | 'bottom';
6
+ export type Area = 'right' | 'bottom' | 'docked-right' | 'docked-bottom';
7
7
 
8
8
  export interface PanelConfig {
9
9
  /** max panel */
@@ -13,6 +13,17 @@ export interface PanelConfig {
13
13
  export interface PanelFactory<T extends any> {
14
14
  key: string;
15
15
  defaultSize: number;
16
+ maxSize?: number;
17
+ minSize?: number;
16
18
  style?: React.CSSProperties;
19
+ /** Allows multiple panels with the same key to be rendered simultaneously */
20
+ allowDuplicates?: boolean;
21
+ resize?: boolean;
17
22
  render: (props: T) => React.ReactNode;
18
23
  }
24
+
25
+ export interface PanelEntityConfig<T extends any = any> {
26
+ defaultSize?: number;
27
+ style?: React.CSSProperties;
28
+ props?: T;
29
+ }
@@ -1,54 +0,0 @@
1
- /**
2
- * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
- * SPDX-License-Identifier: MIT
4
- */
5
-
6
- import { useEffect, useRef, startTransition, useState, useCallback } from 'react';
7
-
8
- import { Area } from '../../types';
9
- import { usePanelManager } from '../../hooks/use-panel-manager';
10
- import { floatPanelWrap } from './css';
11
- import { ResizeBar } from '../resize-bar';
12
-
13
- export const FloatPanel: React.FC<{ area: Area }> = ({ area }) => {
14
- const [, setVersion] = useState(0);
15
- const panelManager = usePanelManager();
16
- const panel = useRef(panelManager.getPanel(area));
17
- const render = () =>
18
- panel.current.elements.map((i) => (
19
- <div className="float-panel-wrap" key={i.key} style={{ ...floatPanelWrap, ...i.style }}>
20
- {i.el}
21
- </div>
22
- ));
23
- const node = useRef(render());
24
-
25
- useEffect(() => {
26
- const dispose = panel.current.onUpdate(() => {
27
- startTransition(() => {
28
- node.current = render();
29
- setVersion((v) => v + 1);
30
- });
31
- });
32
- return () => dispose.dispose();
33
- }, [panel]);
34
- const onResize = useCallback((newSize: number) => panel.current!.updateSize(newSize), []);
35
- const size = panel.current!.currentSize;
36
- const sizeStyle =
37
- area === 'right' ? { width: size, height: '100%' } : { height: size, width: '100%' };
38
-
39
- return (
40
- <div
41
- className="gedit-flow-panel"
42
- style={{
43
- position: 'relative',
44
- display: panel.current.visible ? 'block' : 'none',
45
- ...sizeStyle,
46
- }}
47
- >
48
- {panelManager.config.autoResize && panel.current.elements.length > 0 && (
49
- <ResizeBar size={size} isVertical={area === 'right'} onResize={onResize} />
50
- )}
51
- {node.current}
52
- </div>
53
- );
54
- };
@@ -1,75 +0,0 @@
1
- /**
2
- * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
- * SPDX-License-Identifier: MIT
4
- */
5
-
6
- import { Emitter } from '@flowgram.ai/utils';
7
-
8
- import type { PanelFactory, PanelConfig } from '../types';
9
-
10
- export interface PanelElement {
11
- key: string;
12
- style?: React.CSSProperties;
13
- el: React.ReactNode;
14
- }
15
-
16
- const PANEL_SIZE_DEFAULT = 400;
17
-
18
- export class FloatPanel {
19
- elements: PanelElement[] = [];
20
-
21
- private onUpdateEmitter = new Emitter<void>();
22
-
23
- sizeMap = new Map<string, number>();
24
-
25
- onUpdate = this.onUpdateEmitter.event;
26
-
27
- currentFactoryKey = '';
28
-
29
- updateSize(newSize: number) {
30
- this.sizeMap.set(this.currentFactoryKey, newSize);
31
- this.onUpdateEmitter.fire();
32
- }
33
-
34
- get currentSize(): number {
35
- return this.sizeMap.get(this.currentFactoryKey) || PANEL_SIZE_DEFAULT;
36
- }
37
-
38
- constructor(private config: PanelConfig) {}
39
-
40
- open(factory: PanelFactory<any>, options: any) {
41
- const el = factory.render(options?.props);
42
- const idx = this.elements.findIndex((e) => e.key === factory.key);
43
- this.currentFactoryKey = factory.key;
44
- if (!this.sizeMap.has(factory.key)) {
45
- this.sizeMap.set(factory.key, factory.defaultSize || PANEL_SIZE_DEFAULT);
46
- }
47
- if (idx >= 0) {
48
- this.elements[idx] = { el, key: factory.key, style: factory.style };
49
- } else {
50
- this.elements.push({ el, key: factory.key, style: factory.style });
51
- if (this.elements.length > this.config.max) {
52
- this.elements.shift();
53
- }
54
- }
55
- this.onUpdateEmitter.fire();
56
- }
57
-
58
- get visible() {
59
- return this.elements.length > 0;
60
- }
61
-
62
- close(key?: string) {
63
- if (!key) {
64
- this.elements = [];
65
- } else {
66
- this.elements = this.elements.filter((e) => e.key !== key);
67
- }
68
- this.onUpdateEmitter.fire();
69
- }
70
-
71
- dispose() {
72
- this.elements = [];
73
- this.onUpdateEmitter.dispose();
74
- }
75
- }