Преглед изворни кода

feat: Panel support fullscreen (#1005)

July пре 1 месец
родитељ
комит
e98d9234b9

+ 0 - 6
packages/plugins/panel-manager-plugin/src/components/panel-layer/css.ts

@@ -20,8 +20,6 @@ export const globalCSS = `
 
   }
   .gedit-flow-panel-layer-wrap-floating {
-    column-gap: 4px;
-    padding: 4px;
     pointer-events: none;
   }
 
@@ -33,16 +31,12 @@ export const globalCSS = `
     display: flex;
     flex-direction: column;
   }
-  .gedit-flow-panel-layer-wrap-floating .gedit-flow-panel-left-area {
-    row-gap: 4px;
-  }
   .gedit-flow-panel-right-area {
     height: 100%;
     flex-grow: 1;
     flex-shrink: 0;
     min-width: 0;
     display: flex;
-    column-gap: 4px;
     max-width: 100%;
   }
 

+ 27 - 10
packages/plugins/panel-manager-plugin/src/components/panel-layer/panel.tsx

@@ -5,26 +5,27 @@
 
 import { useEffect, startTransition, useState, useRef } from 'react';
 
-import { useStoreWithEqualityFn } from 'zustand/traditional';
-import { shallow } from 'zustand/shallow';
-import clsx from 'clsx';
+import { clsx } from 'clsx';
 
 import { Area } from '../../types';
 import { PanelEntity } from '../../services/panel-factory';
 import { usePanelManager } from '../../hooks/use-panel-manager';
+import { usePanelStore } from '../../hooks/use-panel';
 import { PanelContext } from '../../contexts';
 
 const PanelItem: React.FC<{ panel: PanelEntity }> = ({ panel }) => {
   const panelManager = usePanelManager();
   const ref = useRef<HTMLDivElement>(null);
-  const resize =
-    panel.factory.resize !== undefined ? panel.factory.resize : panelManager.config.autoResize;
 
   const isHorizontal = ['right', 'docked-right'].includes(panel.area);
 
-  const size = useStoreWithEqualityFn(panel.store, (s) => s.size, shallow);
+  const { size, fullscreen } = usePanelStore((s) => ({ size: s.size, fullscreen: s.fullscreen }));
 
-  const sizeStyle = isHorizontal ? { width: size } : { height: size };
+  const [layerSize, setLayerSize] = useState(size);
+
+  const currentSize = fullscreen ? layerSize : size;
+
+  const sizeStyle = isHorizontal ? { width: currentSize } : { height: currentSize };
   const handleResize = (next: number) => {
     let nextSize = next;
     if (typeof panel.factory.maxSize === 'number' && nextSize > panel.factory.maxSize) {
@@ -37,12 +38,28 @@ const PanelItem: React.FC<{ panel: PanelEntity }> = ({ panel }) => {
 
   useEffect(() => {
     /** The set size may be illegal and needs to be updated according to the real element rendered for the first time. */
-    if (ref.current) {
+    if (ref.current && !fullscreen) {
       const { width, height } = ref.current.getBoundingClientRect();
       const realSize = isHorizontal ? width : height;
       panel.store.setState({ size: realSize });
     }
-  }, []);
+  }, [fullscreen]);
+
+  useEffect(() => {
+    if (!fullscreen) {
+      return;
+    }
+    const layer = panel.layer;
+    if (!layer) {
+      return;
+    }
+    const observer = new ResizeObserver(([entry]) => {
+      const { width, height } = entry.contentRect;
+      setLayerSize(isHorizontal ? width : height);
+    });
+    observer.observe(layer);
+    return () => observer.disconnect();
+  }, [fullscreen]);
 
   return (
     <div
@@ -54,7 +71,7 @@ const PanelItem: React.FC<{ panel: PanelEntity }> = ({ panel }) => {
       ref={ref}
       style={{ ...panel.factory.style, ...panel.config.style, ...sizeStyle }}
     >
-      {resize &&
+      {panel.resizable &&
         panelManager.config.resizeBarRender({
           size,
           direction: isHorizontal ? 'vertical' : 'horizontal',

+ 9 - 0
packages/plugins/panel-manager-plugin/src/hooks/use-panel.ts

@@ -5,6 +5,15 @@
 
 import { useContext } from 'react';
 
+import { useStoreWithEqualityFn } from 'zustand/traditional';
+import { shallow } from 'zustand/shallow';
+
+import { PanelEntityState } from '../services/panel-factory';
 import { PanelContext } from '../contexts';
 
 export const usePanel = () => useContext(PanelContext);
+
+export const usePanelStore = <T>(selector: (s: PanelEntityState) => T) => {
+  const panel = usePanel();
+  return useStoreWithEqualityFn(panel.store, selector, shallow);
+};

+ 47 - 5
packages/plugins/panel-manager-plugin/src/services/panel-factory.ts

@@ -9,6 +9,8 @@ import { inject, injectable } from 'inversify';
 
 import type { PanelFactory, PanelEntityConfig, Area } from '../types';
 import { PanelRestore } from './panel-restore';
+import { PanelManagerConfig } from './panel-config';
+import { merge } from '../utils';
 
 export const PanelEntityFactory = Symbol('PanelEntityFactory');
 export type PanelEntityFactory = (options: {
@@ -25,8 +27,9 @@ export type PanelEntityConfigConstant = PanelEntityConfig<any> & {
 
 const PANEL_SIZE_DEFAULT = 400;
 
-interface PanelEntityState {
+export interface PanelEntityState {
   size: number;
+  fullscreen: boolean;
 }
 
 @injectable()
@@ -38,6 +41,8 @@ export class PanelEntity {
 
   @inject(PanelEntityConfigConstant) public config: PanelEntityConfigConstant;
 
+  @inject(PanelManagerConfig) readonly globalConfig: PanelManagerConfig;
+
   private initialized = false;
 
   /** 实例唯一标识 */
@@ -52,6 +57,10 @@ export class PanelEntity {
     return this.config.area;
   }
 
+  get mode() {
+    return this.config.area.startsWith('docked') ? 'docked' : 'floating';
+  }
+
   get key() {
     return this.factory.key;
   }
@@ -63,18 +72,51 @@ export class PanelEntity {
     return this.node;
   }
 
+  get fullscreen() {
+    return this.store.getState().fullscreen;
+  }
+
+  set fullscreen(next: boolean) {
+    this.store.setState({ fullscreen: next });
+  }
+
+  get resizable() {
+    if (this.fullscreen) {
+      return false;
+    }
+    return this.factory.resize !== undefined ? this.factory.resize : this.globalConfig.autoResize;
+  }
+
+  get layer() {
+    return document.querySelector(
+      this.mode ? '.gedit-flow-panel-layer-wrap-docked' : '.gedit-flow-panel-layer-wrap-floating'
+    );
+  }
+
   init() {
     if (this.initialized) {
       return;
     }
     this.initialized = true;
     const cache = this.restore.restore<PanelEntityState>(this.key);
-    this.store = createStore<PanelEntityState>(() => ({
-      size: this.config.defaultSize || this.factory.defaultSize || PANEL_SIZE_DEFAULT,
-      ...(cache ?? {}),
-    }));
+
+    const initialState = merge<PanelEntityState>(
+      {
+        size: this.config.defaultSize,
+        fullscreen: this.config.fullscreen,
+      },
+      cache ? cache : {},
+      {
+        size: this.factory.defaultSize || PANEL_SIZE_DEFAULT,
+        fullscreen: this.factory.fullscreen || false,
+      }
+    );
+
+    this.store = createStore<PanelEntityState>(() => initialState);
   }
 
+  mergeState() {}
+
   dispose() {
     this.restore.store(this.key, this.store.getState());
   }

+ 0 - 2
packages/plugins/panel-manager-plugin/src/services/panel-manager.ts

@@ -56,7 +56,6 @@ export class PanelManager {
     this.panels.set(panel.id, panel);
     this.trim(area);
     this.onPanelsChangeEvent.fire();
-    console.log('jxj', this.panels);
   }
 
   /** close panel */
@@ -70,7 +69,6 @@ export class PanelManager {
   private trim(area: Area) {
     const panels = this.getPanels(area);
     const areaConfig = this.getAreaConfig(area);
-    console.log('jxj', areaConfig.max, panels.length);
     while (panels.length > areaConfig.max) {
       const removed = panels.shift();
       if (removed) {

+ 2 - 0
packages/plugins/panel-manager-plugin/src/types.ts

@@ -13,6 +13,7 @@ export interface PanelConfig {
 export interface PanelFactory<T extends any> {
   key: string;
   defaultSize: number;
+  fullscreen?: boolean;
   maxSize?: number;
   minSize?: number;
   style?: React.CSSProperties;
@@ -24,6 +25,7 @@ export interface PanelFactory<T extends any> {
 
 export interface PanelEntityConfig<T extends any = any> {
   defaultSize?: number;
+  fullscreen?: boolean;
   style?: React.CSSProperties;
   props?: T;
 }

+ 22 - 0
packages/plugins/panel-manager-plugin/src/utils.ts

@@ -0,0 +1,22 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+export const merge = <T>(...objs: Partial<T>[]) => {
+  const result: any = {};
+
+  for (const obj of objs) {
+    if (!obj || typeof obj !== 'object') continue;
+
+    for (const key of Object.keys(obj)) {
+      const value = (obj as any)[key];
+
+      if (result[key] === undefined) {
+        result[key] = value;
+      }
+    }
+  }
+
+  return result as T;
+};

+ 14 - 2
packages/plugins/test-run-plugin/src/services/pipeline/pipeline.ts

@@ -16,7 +16,7 @@ export interface TestRunPipelineEntityOptions {
 }
 
 interface TestRunPipelineEntityState<T = any> {
-  status: 'idle' | 'preparing' | 'executing' | 'canceled' | 'finished';
+  status: 'idle' | 'preparing' | 'executing' | 'canceled' | 'finished' | 'disposed';
   data?: T;
   result?: any;
   getData: () => T;
@@ -43,6 +43,8 @@ export class TestRunPipelineEntity extends StoreService<TestRunPipelineEntitySta
 
   id = nanoid();
 
+  plugins: TestRunPipelinePlugin[] = [];
+
   prepare = new Tap<TestRunPipelineEntityCtx>();
 
   private execute?: (ctx: TestRunPipelineEntityCtx) => Promise<void> | void;
@@ -81,6 +83,7 @@ export class TestRunPipelineEntity extends StoreService<TestRunPipelineEntitySta
     for (const PluginClass of plugins) {
       const plugin = this.container.resolve<TestRunPipelinePlugin>(PluginClass);
       plugin.apply(this);
+      this.plugins.push(plugin);
     }
   }
 
@@ -139,5 +142,14 @@ export class TestRunPipelineEntity extends StoreService<TestRunPipelineEntitySta
     this.status = 'canceled';
   }
 
-  dispose() {}
+  dispose() {
+    this.status = 'disposed';
+    this.plugins.forEach((p) => {
+      if (p.dispose) {
+        p.dispose();
+      }
+    });
+    this.onProgressEmitter.dispose();
+    this.onFinishedEmitter.dispose();
+  }
 }

+ 2 - 0
packages/plugins/test-run-plugin/src/services/pipeline/plugin.ts

@@ -8,4 +8,6 @@ import type { TestRunPipelineEntity } from './pipeline';
 export interface TestRunPipelinePlugin {
   name: string;
   apply(pipeline: TestRunPipelineEntity): void;
+
+  dispose?: () => void;
 }

+ 8 - 0
packages/plugins/test-run-plugin/src/services/test-run.ts

@@ -73,6 +73,14 @@ export class TestRunService {
     return pipeline;
   }
 
+  disposePipeline(id: string) {
+    const pipeline = this.pipelineEntities.get(id);
+    if (pipeline) {
+      this.pipelineEntities.delete(id);
+      pipeline.dispose();
+    }
+  }
+
   connectPipeline(pipeline: TestRunPipelineEntity) {
     if (this.pipelineBindings.get(pipeline.id)) {
       return;