Преглед на файлове

feat(variable): global variable + variable panel plugin in demo (#435)

* feat: global variable panel

* feat(variable): on any change to on list or any var change

* feat: fix layout add variable panel
Yiwei Mao преди 6 месеца
родител
ревизия
7c6c7ab7a2
променени са 33 файла, в които са добавени 465 реда и са изтрити 20 реда
  1. 1 0
      apps/demo-fixed-layout/package.json
  2. 2 1
      apps/demo-fixed-layout/rsbuild.config.ts
  3. BIN
      apps/demo-fixed-layout/src/assets/icon-variable.png
  4. 7 1
      apps/demo-fixed-layout/src/hooks/use-editor-props.ts
  5. 1 0
      apps/demo-fixed-layout/src/plugins/index.ts
  6. 8 0
      apps/demo-fixed-layout/src/plugins/variable-panel-plugin/components/full-variable-list.tsx
  7. 40 0
      apps/demo-fixed-layout/src/plugins/variable-panel-plugin/components/global-variable-editor.tsx
  8. 40 0
      apps/demo-fixed-layout/src/plugins/variable-panel-plugin/components/index.module.less
  9. 40 0
      apps/demo-fixed-layout/src/plugins/variable-panel-plugin/components/variable-panel.tsx
  10. 1 0
      apps/demo-fixed-layout/src/plugins/variable-panel-plugin/index.ts
  11. 22 0
      apps/demo-fixed-layout/src/plugins/variable-panel-plugin/variable-panel-layer.tsx
  12. 35 0
      apps/demo-fixed-layout/src/plugins/variable-panel-plugin/variable-panel-plugin.ts
  13. 1 0
      apps/demo-fixed-layout/src/type.d.ts
  14. BIN
      apps/demo-free-layout/src/assets/icon-variable.png
  15. 11 1
      apps/demo-free-layout/src/hooks/use-editor-props.tsx
  16. 1 0
      apps/demo-free-layout/src/plugins/index.ts
  17. 8 0
      apps/demo-free-layout/src/plugins/variable-panel-plugin/components/full-variable-list.tsx
  18. 40 0
      apps/demo-free-layout/src/plugins/variable-panel-plugin/components/global-variable-editor.tsx
  19. 39 0
      apps/demo-free-layout/src/plugins/variable-panel-plugin/components/index.module.less
  20. 40 0
      apps/demo-free-layout/src/plugins/variable-panel-plugin/components/variable-panel.tsx
  21. 1 0
      apps/demo-free-layout/src/plugins/variable-panel-plugin/index.ts
  22. 22 0
      apps/demo-free-layout/src/plugins/variable-panel-plugin/variable-panel-layer.tsx
  23. 35 0
      apps/demo-free-layout/src/plugins/variable-panel-plugin/variable-panel-plugin.ts
  24. 1 0
      apps/demo-free-layout/src/type.d.ts
  25. 3 0
      common/config/rush/pnpm-lock.yaml
  26. 2 0
      packages/materials/form-materials/src/components/variable-selector/index.tsx
  27. 3 3
      packages/materials/form-materials/src/components/variable-selector/use-variable-tree.tsx
  28. 1 1
      packages/materials/form-materials/src/utils/json-schema/index.ts
  29. 1 1
      packages/variable-engine/variable-core/src/react/hooks/useAvailableVariables.ts
  30. 16 4
      packages/variable-engine/variable-core/src/scope/datas/scope-available-data.ts
  31. 20 0
      packages/variable-engine/variable-core/src/scope/datas/scope-output-data.ts
  32. 2 1
      packages/variable-engine/variable-core/src/scope/types.ts
  33. 21 7
      packages/variable-engine/variable-core/src/scope/variable-table.ts

+ 1 - 0
apps/demo-fixed-layout/package.json

@@ -46,6 +46,7 @@
     "@flowgram.ai/eslint-config": "workspace:*",
     "@rsbuild/core": "^1.2.16",
     "@rsbuild/plugin-react": "^1.1.1",
+    "@rsbuild/plugin-less": "^1.1.1",
     "@types/lodash-es": "^4.17.12",
     "@types/node": "^18",
     "@types/react": "^18",

+ 2 - 1
apps/demo-fixed-layout/rsbuild.config.ts

@@ -1,8 +1,9 @@
 import { pluginReact } from '@rsbuild/plugin-react';
+import { pluginLess } from '@rsbuild/plugin-less';
 import { defineConfig } from '@rsbuild/core';
 
 export default defineConfig({
-  plugins: [pluginReact()],
+  plugins: [pluginReact(), pluginLess()],
   source: {
     entry: {
       index: './src/app.tsx',

BIN
apps/demo-fixed-layout/src/assets/icon-variable.png


+ 7 - 1
apps/demo-fixed-layout/src/hooks/use-editor-props.ts

@@ -17,7 +17,7 @@ import { type FlowNodeRegistry } from '../typings';
 import { shortcutGetter } from '../shortcuts';
 import { CustomService } from '../services';
 import { GroupBoxHeader, GroupNode } from '../plugins/group-plugin';
-import { createClipboardPlugin } from '../plugins';
+import { createClipboardPlugin, createVariablePanelPlugin } from '../plugins';
 import { SelectorBoxPopover } from '../components/selector-box-popover';
 import NodeAdder from '../components/node-adder';
 import BranchAdder from '../components/branch-adder';
@@ -260,6 +260,12 @@ export function useEditorProps(
          * 剪切板插件
          */
         createClipboardPlugin(),
+
+        /**
+         * Variable panel plugin
+         * 变量面板插件
+         */
+        createVariablePanelPlugin({}),
       ],
     }),
     []

+ 1 - 0
apps/demo-fixed-layout/src/plugins/index.ts

@@ -1 +1,2 @@
 export { createClipboardPlugin } from './clipboard-plugin/create-clipboard-plugin';
+export { createVariablePanelPlugin } from './variable-panel-plugin';

+ 8 - 0
apps/demo-fixed-layout/src/plugins/variable-panel-plugin/components/full-variable-list.tsx

@@ -0,0 +1,8 @@
+import { useVariableTree } from '@flowgram.ai/form-materials';
+import { Tree } from '@douyinfe/semi-ui';
+
+export function FullVariableList() {
+  const treeData = useVariableTree({});
+
+  return <Tree treeData={treeData} />;
+}

+ 40 - 0
apps/demo-fixed-layout/src/plugins/variable-panel-plugin/components/global-variable-editor.tsx

@@ -0,0 +1,40 @@
+import { useEffect } from 'react';
+
+import { JsonSchemaEditor, JsonSchemaUtils } from '@flowgram.ai/form-materials';
+import {
+  BaseVariableField,
+  GlobalScope,
+  useRefresh,
+  useService,
+} from '@flowgram.ai/fixed-layout-editor';
+
+export function GlobalVariableEditor() {
+  const globalScope = useService(GlobalScope);
+
+  const refresh = useRefresh();
+
+  const globalVar = globalScope.getVar() as BaseVariableField;
+
+  useEffect(() => {
+    const disposable = globalScope.output.onVariableListChange(() => {
+      refresh();
+    });
+
+    return () => {
+      disposable.dispose();
+    };
+  }, []);
+
+  if (!globalVar) {
+    return;
+  }
+
+  const value = globalVar.type ? JsonSchemaUtils.astToSchema(globalVar.type) : { type: 'object' };
+
+  return (
+    <JsonSchemaEditor
+      value={value}
+      onChange={(_schema) => globalVar.updateType(JsonSchemaUtils.schemaToAST(_schema))}
+    />
+  );
+}

+ 40 - 0
apps/demo-fixed-layout/src/plugins/variable-panel-plugin/components/index.module.less

@@ -0,0 +1,40 @@
+.panel-wrapper {
+  position: relative;
+  z-index: 9999;
+}
+
+.variable-panel-button {
+  position: absolute;
+  top: 0;
+  right: 0;
+  border-radius: 50%;
+  width: 50px;
+  height: 50px;
+  z-index: 1;
+
+  &.close {
+    width: 30px;
+    height: 30px;
+    top: 10px;
+    right: 10px;
+  }
+}
+
+.panel-container {
+  width: 500px;
+  border-radius: 5px;
+  background-color: #fff;
+  overflow: hidden;
+  box-shadow: 4px 4px 4px rgba(0, 0, 0, 0.1);
+  z-index: 30;
+
+  :global(.semi-tabs-bar) {
+    padding-left: 20px;
+  }
+
+  :global(.semi-tabs-content) {
+    padding: 20px;
+    height: 500px;
+    overflow: auto;
+  }
+}

+ 40 - 0
apps/demo-fixed-layout/src/plugins/variable-panel-plugin/components/variable-panel.tsx

@@ -0,0 +1,40 @@
+import { useState } from 'react';
+
+import { Button, Collapsible, Tabs, Tooltip } from '@douyinfe/semi-ui';
+import { IconMinus } from '@douyinfe/semi-icons';
+
+import iconVariable from '../../../assets/icon-variable.png';
+import { GlobalVariableEditor } from './global-variable-editor';
+import { FullVariableList } from './full-variable-list';
+
+import styles from './index.module.less';
+
+export function VariablePanel() {
+  const [isOpen, setOpen] = useState<boolean>(false);
+
+  return (
+    <div className={styles['panel-wrapper']}>
+      <Tooltip content="Toggle Variable Panel">
+        <Button
+          className={`${styles['variable-panel-button']} ${isOpen ? styles.close : ''}`}
+          theme={isOpen ? 'borderless' : 'light'}
+          onClick={() => setOpen((_open) => !_open)}
+        >
+          {isOpen ? <IconMinus /> : <img src={iconVariable} width={20} height={20} />}
+        </Button>
+      </Tooltip>
+      <Collapsible isOpen={isOpen}>
+        <div className={styles['panel-container']}>
+          <Tabs>
+            <Tabs.TabPane itemKey="variables" tab="Variable List">
+              <FullVariableList />
+            </Tabs.TabPane>
+            <Tabs.TabPane itemKey="global" tab="Global Editor">
+              <GlobalVariableEditor />
+            </Tabs.TabPane>
+          </Tabs>
+        </div>
+      </Collapsible>
+    </div>
+  );
+}

+ 1 - 0
apps/demo-fixed-layout/src/plugins/variable-panel-plugin/index.ts

@@ -0,0 +1 @@
+export { createVariablePanelPlugin } from './variable-panel-plugin';

+ 22 - 0
apps/demo-fixed-layout/src/plugins/variable-panel-plugin/variable-panel-layer.tsx

@@ -0,0 +1,22 @@
+import { domUtils, injectable, Layer } from '@flowgram.ai/fixed-layout-editor';
+
+import { VariablePanel } from './components/variable-panel';
+
+@injectable()
+export class VariablePanelLayer extends Layer {
+  onReady(): void {
+    // Fix variable panel in the right of canvas
+    this.config.onDataChange(() => {
+      const { scrollX, scrollY } = this.config.config;
+      domUtils.setStyle(this.node, {
+        position: 'absolute',
+        right: 25 - scrollX,
+        top: scrollY + 25,
+      });
+    });
+  }
+
+  render(): JSX.Element {
+    return <VariablePanel />;
+  }
+}

+ 35 - 0
apps/demo-fixed-layout/src/plugins/variable-panel-plugin/variable-panel-plugin.ts

@@ -0,0 +1,35 @@
+import { JsonSchemaUtils } from '@flowgram.ai/form-materials';
+import { ASTFactory, definePluginCreator, GlobalScope } from '@flowgram.ai/fixed-layout-editor';
+
+import iconVariable from '../../assets/icon-variable.png';
+import { VariablePanelLayer } from './variable-panel-layer';
+
+const fetchMockVariableFromRemote = async () => {
+  await new Promise((resolve) => setTimeout(resolve, 1000));
+  return {
+    type: 'object',
+    properties: {
+      userId: { type: 'string' },
+    },
+  };
+};
+
+export const createVariablePanelPlugin = definePluginCreator({
+  onInit(ctx) {
+    ctx.playground.registerLayer(VariablePanelLayer);
+
+    // Fetch Global Variable
+    fetchMockVariableFromRemote().then((v) => {
+      ctx.get(GlobalScope).setVar(
+        ASTFactory.createVariableDeclaration({
+          key: 'global',
+          meta: {
+            title: 'Global',
+            icon: iconVariable,
+          },
+          type: JsonSchemaUtils.schemaToAST(v),
+        })
+      );
+    });
+  },
+});

+ 1 - 0
apps/demo-fixed-layout/src/type.d.ts

@@ -1,3 +1,4 @@
 declare module '*.svg'
 declare module '*.png'
 declare module '*.jpg'
+declare module '*.module.less'

BIN
apps/demo-free-layout/src/assets/icon-variable.png


+ 11 - 1
apps/demo-free-layout/src/hooks/use-editor-props.tsx

@@ -15,7 +15,11 @@ import { FlowNodeRegistry, FlowDocumentJSON } from '../typings';
 import { shortcuts } from '../shortcuts';
 import { CustomService } from '../services';
 import { WorkflowRuntimeService } from '../plugins/runtime-plugin/runtime-service';
-import { createRuntimePlugin, createContextMenuPlugin } from '../plugins';
+import {
+  createRuntimePlugin,
+  createContextMenuPlugin,
+  createVariablePanelPlugin,
+} from '../plugins';
 import { defaultFormMeta } from '../nodes/default-form-meta';
 import { WorkflowNodeType } from '../nodes';
 import { SelectorBoxPopover } from '../components/selector-box-popover';
@@ -294,6 +298,12 @@ export function useEditorProps(
           //   protocol: 'http',
           // },
         }),
+
+        /**
+         * Variable panel plugin
+         * 变量面板插件
+         */
+        createVariablePanelPlugin({}),
       ],
     }),
     []

+ 1 - 0
apps/demo-free-layout/src/plugins/index.ts

@@ -1,2 +1,3 @@
 export { createContextMenuPlugin } from './context-menu-plugin';
 export { createRuntimePlugin } from './runtime-plugin';
+export { createVariablePanelPlugin } from './variable-panel-plugin';

+ 8 - 0
apps/demo-free-layout/src/plugins/variable-panel-plugin/components/full-variable-list.tsx

@@ -0,0 +1,8 @@
+import { useVariableTree } from '@flowgram.ai/form-materials';
+import { Tree } from '@douyinfe/semi-ui';
+
+export function FullVariableList() {
+  const treeData = useVariableTree({});
+
+  return <Tree treeData={treeData} />;
+}

+ 40 - 0
apps/demo-free-layout/src/plugins/variable-panel-plugin/components/global-variable-editor.tsx

@@ -0,0 +1,40 @@
+import { useEffect } from 'react';
+
+import {
+  BaseVariableField,
+  GlobalScope,
+  useRefresh,
+  useService,
+} from '@flowgram.ai/free-layout-editor';
+import { JsonSchemaEditor, JsonSchemaUtils } from '@flowgram.ai/form-materials';
+
+export function GlobalVariableEditor() {
+  const globalScope = useService(GlobalScope);
+
+  const refresh = useRefresh();
+
+  const globalVar = globalScope.getVar() as BaseVariableField;
+
+  useEffect(() => {
+    const disposable = globalScope.output.onVariableListChange(() => {
+      refresh();
+    });
+
+    return () => {
+      disposable.dispose();
+    };
+  }, []);
+
+  if (!globalVar) {
+    return;
+  }
+
+  const value = globalVar.type ? JsonSchemaUtils.astToSchema(globalVar.type) : { type: 'object' };
+
+  return (
+    <JsonSchemaEditor
+      value={value}
+      onChange={(_schema) => globalVar.updateType(JsonSchemaUtils.schemaToAST(_schema))}
+    />
+  );
+}

+ 39 - 0
apps/demo-free-layout/src/plugins/variable-panel-plugin/components/index.module.less

@@ -0,0 +1,39 @@
+.panel-wrapper {
+  position: relative;
+}
+
+.variable-panel-button {
+  position: absolute;
+  top: 0;
+  right: 0;
+  border-radius: 50%;
+  width: 50px;
+  height: 50px;
+  z-index: 1;
+
+  &.close {
+    width: 30px;
+    height: 30px;
+    top: 10px;
+    right: 10px;
+  }
+}
+
+.panel-container {
+  width: 500px;
+  border-radius: 5px;
+  background-color: #fff;
+  overflow: hidden;
+  box-shadow: 4px 4px 4px rgba(0, 0, 0, 0.1);
+  z-index: 30;
+
+  :global(.semi-tabs-bar) {
+    padding-left: 20px;
+  }
+
+  :global(.semi-tabs-content) {
+    padding: 20px;
+    height: 500px;
+    overflow: auto;
+  }
+}

+ 40 - 0
apps/demo-free-layout/src/plugins/variable-panel-plugin/components/variable-panel.tsx

@@ -0,0 +1,40 @@
+import { useState } from 'react';
+
+import { Button, Collapsible, Tabs, Tooltip } from '@douyinfe/semi-ui';
+import { IconMinus } from '@douyinfe/semi-icons';
+
+import iconVariable from '../../../assets/icon-variable.png';
+import { GlobalVariableEditor } from './global-variable-editor';
+import { FullVariableList } from './full-variable-list';
+
+import styles from './index.module.less';
+
+export function VariablePanel() {
+  const [isOpen, setOpen] = useState<boolean>(false);
+
+  return (
+    <div className={styles['panel-wrapper']}>
+      <Tooltip content="Toggle Variable Panel">
+        <Button
+          className={`${styles['variable-panel-button']} ${isOpen ? styles.close : ''}`}
+          theme={isOpen ? 'borderless' : 'light'}
+          onClick={() => setOpen((_open) => !_open)}
+        >
+          {isOpen ? <IconMinus /> : <img src={iconVariable} width={20} height={20} />}
+        </Button>
+      </Tooltip>
+      <Collapsible isOpen={isOpen}>
+        <div className={styles['panel-container']}>
+          <Tabs>
+            <Tabs.TabPane itemKey="variables" tab="Variable List">
+              <FullVariableList />
+            </Tabs.TabPane>
+            <Tabs.TabPane itemKey="global" tab="Global Editor">
+              <GlobalVariableEditor />
+            </Tabs.TabPane>
+          </Tabs>
+        </div>
+      </Collapsible>
+    </div>
+  );
+}

+ 1 - 0
apps/demo-free-layout/src/plugins/variable-panel-plugin/index.ts

@@ -0,0 +1 @@
+export { createVariablePanelPlugin } from './variable-panel-plugin';

+ 22 - 0
apps/demo-free-layout/src/plugins/variable-panel-plugin/variable-panel-layer.tsx

@@ -0,0 +1,22 @@
+import { domUtils, injectable, Layer } from '@flowgram.ai/free-layout-editor';
+
+import { VariablePanel } from './components/variable-panel';
+
+@injectable()
+export class VariablePanelLayer extends Layer {
+  onReady(): void {
+    // Fix variable panel in the right of canvas
+    this.config.onDataChange(() => {
+      const { scrollX, scrollY } = this.config.config;
+      domUtils.setStyle(this.node, {
+        position: 'absolute',
+        right: 25 - scrollX,
+        top: scrollY + 25,
+      });
+    });
+  }
+
+  render(): JSX.Element {
+    return <VariablePanel />;
+  }
+}

+ 35 - 0
apps/demo-free-layout/src/plugins/variable-panel-plugin/variable-panel-plugin.ts

@@ -0,0 +1,35 @@
+import { ASTFactory, definePluginCreator, GlobalScope } from '@flowgram.ai/free-layout-editor';
+import { JsonSchemaUtils } from '@flowgram.ai/form-materials';
+
+import iconVariable from '../../assets/icon-variable.png';
+import { VariablePanelLayer } from './variable-panel-layer';
+
+const fetchMockVariableFromRemote = async () => {
+  await new Promise((resolve) => setTimeout(resolve, 1000));
+  return {
+    type: 'object',
+    properties: {
+      userId: { type: 'string' },
+    },
+  };
+};
+
+export const createVariablePanelPlugin = definePluginCreator({
+  onInit(ctx) {
+    ctx.playground.registerLayer(VariablePanelLayer);
+
+    // Fetch Global Variable
+    fetchMockVariableFromRemote().then((v) => {
+      ctx.get(GlobalScope).setVar(
+        ASTFactory.createVariableDeclaration({
+          key: 'global',
+          meta: {
+            title: 'Global',
+            icon: iconVariable,
+          },
+          type: JsonSchemaUtils.schemaToAST(v),
+        })
+      );
+    });
+  },
+});

+ 1 - 0
apps/demo-free-layout/src/type.d.ts

@@ -1,3 +1,4 @@
 declare module '*.svg'
 declare module '*.png'
 declare module '*.jpg'
+declare module '*.module.less'

+ 3 - 0
common/config/rush/pnpm-lock.yaml

@@ -102,6 +102,9 @@ importers:
       '@rsbuild/core':
         specifier: ^1.2.16
         version: 1.2.19
+      '@rsbuild/plugin-less':
+        specifier: ^1.1.1
+        version: 1.1.1(@rsbuild/core@1.2.19)
       '@rsbuild/plugin-react':
         specifier: ^1.1.1
         version: 1.1.1(@rsbuild/core@1.2.19)

+ 2 - 0
packages/materials/form-materials/src/components/variable-selector/index.tsx

@@ -25,6 +25,8 @@ interface PropTypes {
 
 export type VariableSelectorProps = PropTypes;
 
+export { useVariableTree };
+
 export const VariableSelector = ({
   value,
   config = {},

+ 3 - 3
packages/materials/form-materials/src/components/variable-selector/use-variable-tree.tsx

@@ -1,6 +1,6 @@
 import React, { useCallback } from 'react';
 
-import { useScopeAvailable, ASTMatch, BaseVariableField } from '@flowgram.ai/editor';
+import { ASTMatch, BaseVariableField, useAvailableVariables } from '@flowgram.ai/editor';
 import { TreeNodeData } from '@douyinfe/semi-ui/lib/es/tree';
 import { Icon } from '@douyinfe/semi-ui';
 
@@ -16,7 +16,7 @@ export function useVariableTree(params: {
 }): TreeNodeData[] {
   const { includeSchema, excludeSchema } = params;
 
-  const available = useScopeAvailable();
+  const variables = useAvailableVariables();
 
   const getVariableTypeIcon = useCallback((variable: VariableField) => {
     if (variable.meta?.icon) {
@@ -92,7 +92,7 @@ export function useVariableTree(params: {
     };
   };
 
-  return [...available.variables.slice(0).reverse()]
+  return [...variables.slice(0).reverse()]
     .map((_variable) => renderVariable(_variable as VariableField))
     .filter(Boolean) as TreeNodeData[];
 }

+ 1 - 1
packages/materials/form-materials/src/utils/json-schema/index.ts

@@ -109,7 +109,7 @@ export namespace JsonSchemaUtils {
         type: 'object',
         properties: drilldown
           ? Object.fromEntries(
-              Object.entries(typeAST.properties).map(([key, value]) => [key, astToSchema(value)!])
+              typeAST.properties.map((property) => [property.key, astToSchema(property.type)!])
             )
           : {},
       };

+ 1 - 1
packages/variable-engine/variable-core/src/react/hooks/useAvailableVariables.ts

@@ -18,7 +18,7 @@ export function useAvailableVariables(): VariableDeclaration[] {
   useEffect(() => {
     // 没有作用域时,监听全局变量表
     if (!scope) {
-      const disposable = variableEngine.globalVariableTable.onAnyChange(() => {
+      const disposable = variableEngine.globalVariableTable.onListOrAnyVarChange(() => {
         refresh();
       });
 

+ 16 - 4
packages/variable-engine/variable-core/src/scope/datas/scope-available-data.ts

@@ -70,8 +70,8 @@ export class ScopeAvailableData {
   );
 
   /**
-   * 监听任意变量变化
-   * @param observer 监听器,变量变化时会吐出值
+   * listen to any variable update in list
+   * @param observer
    * @returns
    */
   onAnyVariableChange(observer: (changedVariable: VariableDeclaration) => void) {
@@ -79,7 +79,7 @@ export class ScopeAvailableData {
   }
 
   /**
-   * 监听变量列表变化
+   * listen to variable list change
    * @param observer
    * @returns
    */
@@ -87,22 +87,34 @@ export class ScopeAvailableData {
     return subsToDisposable(this.variables$.subscribe(observer));
   }
 
+  /**
+   * @deprecated
+   */
   protected onDataChangeEmitter = new Emitter<VariableDeclaration[]>();
 
+  protected onListOrAnyVarChangeEmitter = new Emitter<VariableDeclaration[]>();
+
   /**
-   * 监听变量列表变化 + 任意子变量变化
+   * @deprecated use available.onListOrAnyVarChange instead
    */
   public onDataChange = this.onDataChangeEmitter.event;
 
+  /**
+   * listen to variable list change + any variable drilldown change
+   */
+  public onListOrAnyVarChange = this.onListOrAnyVarChangeEmitter.event;
+
   constructor(public readonly scope: Scope) {
     this.scope.toDispose.pushAll([
       this.onVariableListChange((_variables) => {
         this._variables = _variables;
         this.memo.clear();
         this.onDataChangeEmitter.fire(this._variables);
+        this.onListOrAnyVarChangeEmitter.fire(this._variables);
       }),
       this.onAnyVariableChange(() => {
         this.onDataChangeEmitter.fire(this._variables);
+        this.onListOrAnyVarChangeEmitter.fire(this._variables);
       }),
       Disposable.create(() => {
         this.refresh$.complete();

+ 20 - 0
packages/variable-engine/variable-core/src/scope/datas/scope-output-data.ts

@@ -24,14 +24,34 @@ export class ScopeOutputData {
     return this.scope.variableEngine.globalVariableTable;
   }
 
+  /**
+   * @deprecated use onListOrAnyVarChange instead
+   */
   get onDataChange() {
     return this.variableTable.onDataChange.bind(this.variableTable);
   }
 
+  /**
+   * listen to variable list change
+   */
+  get onVariableListChange() {
+    return this.variableTable.onVariableListChange.bind(this.variableTable);
+  }
+
+  /**
+   * listen to any variable update in list
+   */
   get onAnyVariableChange() {
     return this.variableTable.onAnyVariableChange.bind(this.variableTable);
   }
 
+  /**
+   * listen to variable list change + any variable update in list
+   */
+  get onListOrAnyVarChange() {
+    return this.variableTable.onListOrAnyVarChange.bind(this.variableTable);
+  }
+
   protected _hasChanges = false;
 
   constructor(public readonly scope: Scope) {

+ 2 - 1
packages/variable-engine/variable-core/src/scope/types.ts

@@ -25,6 +25,7 @@ export interface IVariableTable extends Disposable {
   // addVariableToTable(variable: VariableDeclaration): void;
   // removeVariableFromTable(key: string): void;
   dispose(): void;
+  onVariableListChange(observer: (variables: VariableDeclaration[]) => void): Disposable;
   onAnyVariableChange(observer: (changedVariable: VariableDeclaration) => void): Disposable;
-  onAnyChange(observer: () => void): Disposable;
+  onListOrAnyVarChange(observer: () => void): Disposable;
 }

+ 21 - 7
packages/variable-engine/variable-core/src/scope/variable-table.ts

@@ -29,8 +29,8 @@ export class VariableTable implements IVariableTable {
   );
 
   /**
-   * 监听任意变量变化
-   * @param observer 监听器,变量变化时会吐出值
+   * listen to any variable update in list
+   * @param observer
    * @returns
    */
   onAnyVariableChange(observer: (changedVariable: VariableDeclaration) => void) {
@@ -38,15 +38,27 @@ export class VariableTable implements IVariableTable {
   }
 
   /**
-   * 列表或者任意变量变化
+   * listen to variable list change
+   * @param observer
+   * @returns
+   */
+  onVariableListChange(observer: (variables: VariableDeclaration[]) => void) {
+    return subsToDisposable(this.variables$.subscribe(observer));
+  }
+
+  /**
+   * listen to variable list change + any variable update in list
    * @param observer
    */
-  onAnyChange(observer: () => void) {
+  onListOrAnyVarChange(observer: () => void) {
     const disposables = new DisposableCollection();
-    disposables.pushAll([this.onDataChange(observer), this.onAnyVariableChange(observer)]);
+    disposables.pushAll([this.onVariableListChange(observer), this.onAnyVariableChange(observer)]);
     return disposables;
   }
 
+  /**
+   * @deprecated use onListOrAnyVarChange instead
+   */
   public onDataChange = this.onDataChangeEmitter.event;
 
   protected _version: number = 0;
@@ -54,6 +66,7 @@ export class VariableTable implements IVariableTable {
   fireChange() {
     this._version++;
     this.onDataChangeEmitter.fire();
+    this.variables$.next(this.variables);
     this.parentTable?.fireChange();
   }
 
@@ -108,7 +121,6 @@ export class VariableTable implements IVariableTable {
     if (this.parentTable) {
       (this.parentTable as VariableTable).addVariableToTable(variable);
     }
-    this.variables$.next(this.variables);
   }
 
   /**
@@ -120,13 +132,15 @@ export class VariableTable implements IVariableTable {
     if (this.parentTable) {
       (this.parentTable as VariableTable).removeVariableFromTable(key);
     }
-    this.variables$.next(this.variables);
   }
 
   dispose(): void {
     this.variableKeys.forEach((_key) =>
       (this.parentTable as VariableTable)?.removeVariableFromTable(_key)
     );
+    this.parentTable?.fireChange();
+    this.variables$.complete();
+    this.variables$.unsubscribe();
     this.onDataChangeEmitter.dispose();
   }
 }