AddSubModelButton

用于在指定的 FlowModel 中添加子模型(subModel)。支持异步加载、分组、子菜单、自定义模型继承规则等多种配置方式。

Props

interface AddSubModelButtonProps {
  model: FlowModel;
  subModelKey: string;
  subModelType?: 'object' | 'array';
  items?: SubModelItemsType;
  subModelBaseClass?: string | ModelConstructor;
  subModelBaseClasses?: Array<string | ModelConstructor>;
  afterSubModelInit?: (subModel: FlowModel) => Promise<void>;
  afterSubModelAdd?: (subModel: FlowModel) => Promise<void>;
  afterSubModelRemove?: (subModel: FlowModel) => Promise<void>;
  children?: React.ReactNode;
  keepDropdownOpen?: boolean;
}
参数类型说明
modelFlowModel必填。要添加子模型的目标模型。
subModelKeystring必填。子模型在 model.subModels 中的键名。
subModelType'object' | 'array'子模型的数据结构类型,默认为 'array'
itemsSubModelItem[] | (ctx) => SubModelItem[] | Promise<...>菜单项定义,支持静态或异步生成。
subModelBaseClassstring | ModelConstructor指定一个基类,列出继承该类的所有模型作为菜单项。
subModelBaseClasses(string | ModelConstructor)[]指定多个基类,自动按组列出继承模型。
afterSubModelInit(subModel) => Promise<void>子模型初始化后回调。
afterSubModelAdd(subModel) => Promise<void>子模型添加后回调。
afterSubModelRemove(subModel) => Promise<void>子模型移除后回调。
childrenReact.ReactNode按钮内容,可自定义为文字或图标。
keepDropdownOpenboolean添加后是否保持下拉菜单展开。默认自动关闭。

SubModelItem 类型定义

interface SubModelItem {
  key?: string;
  label?: string;
  type?: 'group' | 'divider';
  disabled?: boolean;
  hide?: boolean | ((ctx: FlowModelContext) => boolean | Promise<boolean>);
  icon?: React.ReactNode;
  children?: SubModelItemsType;
  useModel?: string;
  createModelOptions?: {
    props?: Record<string, any>;
    stepParams?: Record<string, any>;
  };
  toggleable?: boolean | ((model: FlowModel) => boolean);
}
字段类型说明
keystring唯一标识。
labelstring显示文本。
type'group' | 'divider'分组或分隔符。省略时为普通项或子菜单。
disabledboolean是否禁用当前项。
hideboolean | (ctx) => boolean | Promise<boolean>动态隐藏(返回 true 表示隐藏)。
iconReact.ReactNode图标内容。
childrenSubModelItemsType子菜单项,用于嵌套分组或子菜单。
useModelstring指定使用的 Model 类型(注册名)。
createModelOptionsobject初始化模型时的参数。
toggleableboolean | (model: FlowModel) => boolean开关形态,已添加则移除,未添加则添加(只允许一个)。

示例

使用 <AddSubModelButton /> 添加 subModels

import { Application, Plugin, BlockModel } from '@nocobase/client-v2';
import { AddSubModelButton, FlowModel, FlowModelRenderer } from '@nocobase/flow-engine';
import { Button, Space } from 'antd';

class HelloBlockModel extends FlowModel {
  render() {
    return (
      <Space direction="vertical" style={{ width: '100%' }}>
        {this.mapSubModels('items', (item) => {
          return <FlowModelRenderer key={item.uid} model={item} showFlowSettings={{ showBorder: true }} />;
        })}
        <AddSubModelButton
          model={this}
          subModelKey="items"
          items={[
            {
              key: 'sub1',
              label: 'Sub1 Block',
              useModel: 'Sub1BlockModel',
            },
            {
              key: 'sub2',
              label: 'Sub2 Block',
              children: [
                {
                  key: 'sub2-1',
                  label: 'Sub2-1 Block',
                  useModel: 'Sub2BlockModel',
                },
                {
                  key: 'sub2-2',
                  label: 'Sub2-2 Block',
                  useModel: 'Sub2BlockModel',
                },
              ],
            },
          ]}
        >
          <Button>Add block</Button>
        </AddSubModelButton>
      </Space>
    );
  }
}

class Sub1BlockModel extends BlockModel {
  renderComponent() {
    return (
      <div>
        <h2>Sub1 Block</h2>
        <p>This is a sub block rendered by Sub1BlockModel.</p>
      </div>
    );
  }
}

class Sub2BlockModel extends BlockModel {
  renderComponent() {
    return (
      <div>
        <h2>Sub2 Block</h2>
        <p>This is a sub block rendered by Sub2BlockModel.</p>
      </div>
    );
  }
}

class PluginHelloModel extends Plugin {
  async load() {
    await this.flowEngine.flowSettings.forceEnable();
    this.flowEngine.registerModels({ HelloBlockModel, Sub1BlockModel, Sub2BlockModel });
    const model = this.flowEngine.createModel({
      uid: 'my-model',
      use: 'HelloBlockModel',
    });
    this.router.add('root', {
      path: '/',
      element: <FlowModelRenderer model={model} />,
    });
  }
}

const app = new Application({
  router: { type: 'memory', initialEntries: ['/'] },
  plugins: [PluginHelloModel],
});

export default app.getRootComponent();
  • 使用 <AddSubModelButton /> 添加 subModels,按钮必须放到某个 FlowModel 里才能使用;
  • 使用 model.mapSubModels() 遍历 subModels,mapSubModels 方法会解决缺失、排序等问题;
  • 使用 <FlowModelRenderer /> 渲染 subModels。

不同形态的 AddSubModelButton

import { PlusOutlined } from '@ant-design/icons';
import { Application, Plugin, BlockModel } from '@nocobase/client-v2';
import { AddSubModelButton, FlowModel, FlowModelRenderer } from '@nocobase/flow-engine';
import { Button, Card, Space } from 'antd';
import type { ReactNode } from 'react';

function AddBlock(props: { model: FlowModel; children?: ReactNode }) {
  const { model, children } = props;

  return (
    <AddSubModelButton
      model={model}
      subModelKey="items"
      items={[
        {
          key: 'sub1',
          label: 'Sub1 Block',
          createModelOptions: {
            use: 'Sub1BlockModel',
          },
        },
      ]}
    >
      {children}
    </AddSubModelButton>
  );
}

class HelloBlockModel extends FlowModel {
  render() {
    return (
      <Card>
        <Space direction="vertical" style={{ width: '100%' }}>
          {this.mapSubModels('items', (item) => {
            return <FlowModelRenderer key={item.uid} model={item} showFlowSettings={{ showBorder: true }} />;
          })}
          <Space>
            <AddBlock model={this}>
              <Button>Add block</Button>
            </AddBlock>
            <AddBlock model={this}>
              <a>
                <PlusOutlined /> Add block
              </a>
            </AddBlock>
          </Space>
        </Space>
      </Card>
    );
  }
}

class Sub1BlockModel extends BlockModel {
  renderComponent() {
    return (
      <div>
        <h2>Sub1 Block</h2>
        <p>This is a sub block rendered by Sub1BlockModel.</p>
      </div>
    );
  }
}

class PluginHelloModel extends Plugin {
  async load() {
    this.flowEngine.flowSettings.forceEnable();
    this.flowEngine.registerModels({ HelloBlockModel, Sub1BlockModel });
    const model = this.flowEngine.createModel({
      uid: 'my-model',
      use: 'HelloBlockModel',
    });
    this.router.add('root', {
      path: '/',
      element: (
        <FlowModelRenderer
          model={model}
          showFlowSettings={{ showBorder: true }}
          hideRemoveInSettings
          extraToolbarItems={[
            {
              key: 'add-block',
              component: () => {
                return (
                  <AddBlock model={model}>
                    <PlusOutlined />
                  </AddBlock>
                );
              },
              sort: 1,
            },
          ]}
        />
      ),
    });
  }
}

const app = new Application({
  router: { type: 'memory', initialEntries: ['/'] },
  plugins: [PluginHelloModel],
});

export default app.getRootComponent();
  • 可以使用按钮组件 <Button>Add block</Button>,可以随处放置;
  • 也可以使用图标 <PlusOutlined />
  • 也可以放到右上角 Flow Settings 的位置。

支持开关形态

import { Application, Plugin, BlockModel } from '@nocobase/client-v2';
import { AddSubModelButton, FlowModel, FlowModelRenderer } from '@nocobase/flow-engine';
import { Button, Space } from 'antd';

class HelloBlockModel extends FlowModel {
  render() {
    return (
      <Space direction="vertical" style={{ width: '100%' }}>
        {this.mapSubModels('items', (item) => {
          return <FlowModelRenderer key={item.uid} model={item} showFlowSettings={{ showBorder: true }} />;
        })}
        <AddSubModelButton
          model={this}
          subModelKey="items"
          items={[
            {
              key: 'sub1',
              label: 'Sub1 Block',
              toggleable: true,
              useModel: 'Sub1BlockModel',
            },
            {
              key: 'sub2',
              label: 'Sub2 Block',
              children: [
                {
                  key: 'sub2-1',
                  label: 'Sub2-1 Block',
                  toggleable: Sub2BlockModel.customToggleable('foo'),
                  useModel: 'Sub2BlockModel',
                  createModelOptions: {
                    props: {
                      name: 'foo',
                    },
                  },
                },
                {
                  key: 'sub2-2',
                  label: 'Sub2-2 Block',
                  toggleable: Sub2BlockModel.customToggleable('bar'),
                  useModel: 'Sub2BlockModel',
                  createModelOptions: {
                    props: {
                      name: 'bar',
                    },
                  },
                },
              ],
            },
          ]}
        >
          <Button>Add block</Button>
        </AddSubModelButton>
      </Space>
    );
  }
}

class Sub1BlockModel extends BlockModel {
  renderComponent() {
    return (
      <div>
        <h2>Sub1 Block</h2>
        <p>This is a sub block rendered by Sub1BlockModel.</p>
      </div>
    );
  }
}

class Sub2BlockModel extends BlockModel {
  static customToggleable(name: string) {
    return (model: Sub2BlockModel) => {
      return model.props.name === name;
    };
  }

  renderComponent() {
    return (
      <div>
        <h2>Sub2 Block - {this.props.name}</h2>
        <p>This is a sub block rendered by Sub2BlockModel.</p>
      </div>
    );
  }
}

class PluginHelloModel extends Plugin {
  async load() {
    this.flowEngine.flowSettings.forceEnable();
    this.flowEngine.registerModels({ HelloBlockModel, Sub1BlockModel, Sub2BlockModel });
    const model = this.flowEngine.createModel({
      uid: 'my-model',
      use: 'HelloBlockModel',
    });
    this.router.add('root', {
      path: '/',
      element: <FlowModelRenderer model={model} />,
    });
  }
}

const app = new Application({
  router: { type: 'memory', initialEntries: ['/'] },
  plugins: [PluginHelloModel],
});

export default app.getRootComponent();
  • 简单场景 toggleable: true 即可,默认根据类名查找,同一个类的实例只允许出现一次;
  • 自定义查找规则:toggleable: (model: FlowModel) => boolean

异步 items

import { Application, Plugin, BlockModel } from '@nocobase/client-v2';
import { AddSubModelButton, FlowModel, FlowModelRenderer } from '@nocobase/flow-engine';
import { Button, Space } from 'antd';

const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));

class HelloBlockModel extends FlowModel {
  render() {
    return (
      <Space direction="vertical" style={{ width: '100%' }}>
        {this.mapSubModels('items', (item) => {
          return <FlowModelRenderer key={item.uid} model={item} showFlowSettings={{ showBorder: true }} />;
        })}
        <AddSubModelButton
          model={this}
          subModelKey="items"
          items={async (ctx) => {
            return await ctx.getTestModels();
          }}
        >
          <Button>Add block</Button>
        </AddSubModelButton>
      </Space>
    );
  }
}

class Sub1BlockModel extends BlockModel {
  renderComponent() {
    return (
      <div>
        <h2>Sub1 Block</h2>
        <p>This is a sub block rendered by Sub1BlockModel.</p>
      </div>
    );
  }
}

class Sub2BlockModel extends BlockModel {
  renderComponent() {
    return (
      <div>
        <h2>Sub2 Block</h2>
        <p>This is a sub block rendered by Sub2BlockModel.</p>
      </div>
    );
  }
}

class PluginHelloModel extends Plugin {
  async load() {
    this.flowEngine.flowSettings.forceEnable();
    this.flowEngine.context.defineMethod('getTestModels', async () => {
      await sleep(1000);
      return [
        {
          key: 'sub1',
          label: 'Sub1 Block',
          createModelOptions: {
            use: 'Sub1BlockModel',
          },
        },
        {
          key: 'sub2',
          label: 'Sub2 Block',
          children: async () => {
            await sleep(1000);
            return [
              {
                key: 'sub2-1',
                label: 'Sub2-1 Block',
                createModelOptions: {
                  use: 'Sub2BlockModel',
                },
              },
              {
                key: 'sub2-2',
                label: 'Sub2-2 Block',
                createModelOptions: {
                  use: 'Sub2BlockModel',
                },
              },
            ];
          },
        },
        {
          key: 'async-group',
          label: 'Async Group',
          type: 'group' as const,
          children: async () => {
            await sleep(800);
            return [
              {
                key: 'g-sub1',
                label: 'G-Sub1 Block',
                createModelOptions: { use: 'Sub1BlockModel' },
              },
              {
                key: 'g-nested-group',
                label: 'Nested Group',
                type: 'group' as const,
                children: async () => {
                  await sleep(500);
                  return [
                    {
                      key: 'g-sub2-1',
                      label: 'G-Sub2-1 Block',
                      createModelOptions: { use: 'Sub2BlockModel' },
                    },
                    {
                      key: 'g-sub2-2',
                      label: 'G-Sub2-2 Block',
                      createModelOptions: { use: 'Sub2BlockModel' },
                    },
                  ];
                },
              },
            ];
          },
        },
      ];
    });
    this.flowEngine.registerModels({ HelloBlockModel, Sub1BlockModel, Sub2BlockModel });
    const model = this.flowEngine.createModel({
      uid: 'my-model',
      use: 'HelloBlockModel',
    });
    this.router.add('root', {
      path: '/',
      element: <FlowModelRenderer model={model} />,
    });
  }
}

const app = new Application({
  router: { type: 'memory', initialEntries: ['/'] },
  plugins: [PluginHelloModel],
});

export default app.getRootComponent();

可以从上下文获取动态 items,例如:

  • 可以是远程 ctx.api.request()
  • 也可以从 ctx.dataSourceManager 提供的 API 里获取必要的数据;
  • 也可以是自定义的上下文属性或方法;
  • itemschildren 都支持 async 调用。

动态隐藏菜单项(hide)

import { Application, Plugin, BlockModel } from '@nocobase/client-v2';
import { AddSubModelButton, FlowModel, FlowModelRenderer } from '@nocobase/flow-engine';
import { Button, Space, Switch, Typography } from 'antd';
import { useState } from 'react';

const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));

class ContainerModel extends FlowModel {
  render() {
    return <View model={this} />;
  }
}

function View({ model }: { readonly model: FlowModel }) {
  const [showAdvanced, setShowAdvanced] = useState(false);

  return (
    <Space direction="vertical" style={{ width: '100%' }}>
      <Space>
        <Typography.Text>showAdvanced</Typography.Text>
        <Switch checked={showAdvanced} onChange={setShowAdvanced} />
      </Space>

      {model.mapSubModels('items', (item) => {
        return <FlowModelRenderer key={item.uid} model={item} showFlowSettings={{ showBorder: true }} />;
      })}

      <AddSubModelButton
        model={model}
        subModelKey="items"
        items={[
          {
            key: 'basic',
            label: 'Basic Block',
            createModelOptions: { use: 'BasicBlockModel' },
          },
          {
            key: 'advanced-group',
            label: 'Advanced (hide)',
            type: 'group' as const,
            children: [
              {
                key: 'advanced-async',
                label: 'Advanced Block (async hide)',
                hide: async () => {
                  await sleep(200);
                  return !showAdvanced;
                },
                createModelOptions: { use: 'AdvancedBlockModel' },
              },
              {
                key: 'advanced-alias',
                label: 'Advanced Block (sync hide)',
                hide: () => !showAdvanced,
                createModelOptions: { use: 'AdvancedBlockModel' },
              },
            ],
          },
        ]}
      >
        <Button>Add block</Button>
      </AddSubModelButton>
    </Space>
  );
}

class BasicBlockModel extends BlockModel {
  renderComponent() {
    return (
      <div>
        <h3>Basic Block</h3>
        <div style={{ color: '#666' }}>我是一个普通的 subModel。</div>
      </div>
    );
  }
}

class AdvancedBlockModel extends BlockModel {
  renderComponent() {
    return (
      <div>
        <h3>Advanced Block</h3>
        <div style={{ color: '#666' }}>仅当 showAdvanced=true 时才会出现在 AddSubModelButton 菜单里。</div>
      </div>
    );
  }
}

class PluginDemo extends Plugin {
  async load() {
    this.flowEngine.flowSettings.forceEnable();
    this.flowEngine.registerModels({ ContainerModel, BasicBlockModel, AdvancedBlockModel });

    const model = this.flowEngine.createModel({
      uid: 'container',
      use: 'ContainerModel',
    });

    this.router.add('root', {
      path: '/',
      element: <FlowModelRenderer model={model} showFlowSettings={{ showBorder: true, showBackground: true }} />,
    });
  }
}

const app = new Application({
  router: { type: 'memory', initialEntries: ['/'] },
  plugins: [PluginDemo],
});

export default app.getRootComponent();
  • hide 支持 boolean 或函数(支持 async);返回 true 表示隐藏;
  • 会递归作用于 group 与 children。

使用分组、子菜单和分隔符

import { Application, Plugin, BlockModel } from '@nocobase/client-v2';
import { AddSubModelButton, FlowModel, FlowModelRenderer } from '@nocobase/flow-engine';
import { Button, Space } from 'antd';

class HelloBlockModel extends FlowModel {
  render() {
    return (
      <Space direction="vertical" style={{ width: '100%' }}>
        {this.mapSubModels('items', (item) => {
          return <FlowModelRenderer key={item.uid} model={item} showFlowSettings={{ showBorder: true }} />;
        })}
        <AddSubModelButton
          model={this}
          subModelKey="items"
          items={[
            {
              key: 'group1',
              label: 'Group1',
              type: 'group',
              children: [
                {
                  key: 'group1-sub1',
                  label: 'Block1',
                  createModelOptions: {
                    use: 'Sub1BlockModel',
                  },
                },
              ],
            },
            {
              type: 'divider',
            },
            {
              key: 'submenu1',
              label: 'Sub Menu',
              children: [
                {
                  key: 'submenu1-sub1',
                  label: 'Block1',
                  createModelOptions: {
                    use: 'Sub1BlockModel',
                  },
                },
              ],
            },
            {
              type: 'divider',
            },
            {
              key: 'sub1',
              label: 'Block1',
              createModelOptions: {
                use: 'Sub1BlockModel',
              },
            },
          ]}
        >
          <Button>Add block</Button>
        </AddSubModelButton>
      </Space>
    );
  }
}

class Sub1BlockModel extends BlockModel {
  renderComponent() {
    return (
      <div>
        <h2>Sub1 Block</h2>
        <p>This is a sub block rendered by Sub1BlockModel.</p>
      </div>
    );
  }
}

class PluginHelloModel extends Plugin {
  async load() {
    this.flowEngine.flowSettings.forceEnable();
    this.flowEngine.registerModels({ HelloBlockModel, Sub1BlockModel });
    const model = this.flowEngine.createModel({
      uid: 'my-model',
      use: 'HelloBlockModel',
    });
    this.router.add('root', {
      path: '/',
      element: <FlowModelRenderer model={model} />,
    });
  }
}

const app = new Application({
  router: { type: 'memory', initialEntries: ['/'] },
  plugins: [PluginHelloModel],
});

export default app.getRootComponent();
  • type: divider 时为分隔符;
  • type: group 并有 children 时为菜单分组;
  • children,但是没有 type 时为子菜单。

通过继承类的方式自动生成 items

import { Application, Plugin, BlockModel } from '@nocobase/client-v2';
import { AddSubModelButton, FlowModel, FlowModelRenderer } from '@nocobase/flow-engine';
import { Button, Space } from 'antd';

class HelloBlockModel extends FlowModel {
  get title() {
    return 'HelloBlockModel';
  }

  render() {
    return (
      <Space direction="vertical" style={{ width: '100%' }}>
        {this.mapSubModels('items', (item) => {
          return <FlowModelRenderer key={item.uid} model={item} showFlowSettings={{ showBorder: true }} />;
        })}
        <AddSubModelButton model={this} subModelKey="items" subModelBaseClass="BlockModel">
          <Button>Add block</Button>
        </AddSubModelButton>
      </Space>
    );
  }
}

class Sub1BlockModel extends BlockModel {
  renderComponent() {
    return (
      <div>
        <h2>Sub1 Block</h2>
        <p>This is a sub block rendered by Sub1BlockModel.</p>
      </div>
    );
  }
}

class Sub2BlockModel extends BlockModel {
  renderComponent() {
    return (
      <div>
        <h2>Sub2 Block</h2>
        <p>This is a sub block rendered by Sub2BlockModel.</p>
      </div>
    );
  }
}

class Sub3BlockModel extends BlockModel {
  renderComponent() {
    return (
      <div>
        <h2>Sub3 Block</h2>
        <p>This is a sub block rendered by Sub3BlockModel.</p>
      </div>
    );
  }
}

class Sub4BlockModel extends BlockModel {
  renderComponent() {
    return (
      <div>
        <h2>Sub4 Block</h2>
        <p>This is a sub block rendered by Sub4BlockModel.</p>
      </div>
    );
  }
}

Sub2BlockModel.define({
  label: 'Sub2 Block',
  hide: (ctx) => ctx.model.title !== 'HelloBlockModel',
});

Sub3BlockModel.define({
  hide: (ctx) => ctx.model.title === 'HelloBlockModel',
});

Sub4BlockModel.define({
  label: 'Sub4 Block',
  hide: true,
});

class PluginHelloModel extends Plugin {
  async load() {
    this.flowEngine.flowSettings.forceEnable();
    this.flowEngine.registerModels({
      BlockModel,
      HelloBlockModel,
      Sub1BlockModel,
      Sub2BlockModel,
      Sub3BlockModel,
    });
    const model = this.flowEngine.createModel({
      uid: 'my-model',
      use: 'HelloBlockModel',
    });
    this.router.add('root', {
      path: '/',
      element: <FlowModelRenderer model={model} />,
    });
  }
}

const app = new Application({
  router: { type: 'memory', initialEntries: ['/'] },
  plugins: [PluginHelloModel],
});

export default app.getRootComponent();
  • 所有继承 subModelBaseClass 的 FlowModel 都会罗列出来;
  • 通过 Model.define() 可以定义相关元数据;
  • 使用 hide: true 标记的会自动隐藏。

通过继承类的方式实现分组

import { Application, Plugin, BlockModel } from '@nocobase/client-v2';
import { AddSubModelButton, FlowModel, FlowModelRenderer } from '@nocobase/flow-engine';
import { Button, Space } from 'antd';

class HelloBlockModel extends FlowModel {
  render() {
    return (
      <Space direction="vertical" style={{ width: '100%' }}>
        {this.mapSubModels('items', (item) => {
          return <FlowModelRenderer key={item.uid} model={item} showFlowSettings={{ showBorder: true }} />;
        })}
        <AddSubModelButton
          model={this}
          subModelKey="items"
          subModelBaseClasses={['BaseBlockModel', 'BlockModel']}
        >
          <Button>Add block</Button>
        </AddSubModelButton>
      </Space>
    );
  }
}

class Sub1BlockModel extends BlockModel {
  renderComponent() {
    return (
      <div>
        <h2>Sub1 Block</h2>
        <p>This is a sub block rendered by Sub1BlockModel.</p>
      </div>
    );
  }
}

class BaseBlockModel extends BlockModel {}

BaseBlockModel.define({
  label: 'Group1',
});

class Sub2BlockModel extends BaseBlockModel {
  renderComponent() {
    return (
      <div>
        <h2>Sub2 Block</h2>
        <p>This is a sub block rendered by Sub2BlockModel.</p>
      </div>
    );
  }
}

class Sub3BlockModel extends BaseBlockModel {
  renderComponent() {
    return (
      <div>
        <h2>Sub3 Block</h2>
        <p>This is a sub block rendered by Sub2BlockModel.</p>
      </div>
    );
  }
}

Sub2BlockModel.define({
  label: 'Sub2 Block',
});

class PluginHelloModel extends Plugin {
  async load() {
    this.flowEngine.flowSettings.forceEnable();
    this.flowEngine.registerModels({
      BlockModel,
      HelloBlockModel,
      BaseBlockModel,
      Sub1BlockModel,
      Sub2BlockModel,
      Sub3BlockModel,
    });
    const model = this.flowEngine.createModel({
      uid: 'my-model',
      use: 'HelloBlockModel',
    });
    this.router.add('root', {
      path: '/',
      element: <FlowModelRenderer model={model} />,
    });
  }
}

const app = new Application({
  router: { type: 'memory', initialEntries: ['/'] },
  plugins: [PluginHelloModel],
});

export default app.getRootComponent();
  • 所有继承 subModelBaseClasses 的 FlowModel 都会罗列出来;
  • 自动按 subModelBaseClasses 分组并去重。

通过继承类 + menuType=submenu 实现二级菜单

import { Application, Plugin } from '@nocobase/client-v2';
import { AddSubModelButton, FlowEngineProvider, FlowModel, FlowModelRenderer } from '@nocobase/flow-engine';
import { Button, Space } from 'antd';

class DemoRootModel extends FlowModel {
  render() {
    return (
      <Space direction="vertical" style={{ width: '100%' }}>
        {this.mapSubModels('items', (item) => (
          <FlowModelRenderer key={item.uid} model={item} showFlowSettings={{ showBorder: true }} />
        ))}
        <AddSubModelButton model={this} subModelKey="items" subModelBaseClasses={[GroupBase, SubmenuBase]}>
          <Button>添加子模型</Button>
        </AddSubModelButton>
      </Space>
    );
  }
}

class GroupBase extends FlowModel {}

GroupBase.define({
  label: '分组(平铺)',
  sort: 200,
  children: () => [{ key: 'group-leaf', label: '组内子项', createModelOptions: { use: 'Leaf' } }],
});

class SubmenuBase extends FlowModel {}

SubmenuBase.define({
  label: '二级菜单',
  menuType: 'submenu',
  sort: 110,
  children: () => [{ key: 'submenu-leaf', label: '子菜单子项', createModelOptions: { use: 'Leaf' } }],
});

class Leaf extends FlowModel {
  render() {
    return (
      <div
        style={{
          padding: 12,
          border: '1px dashed #d9d9d9',
          background: '#fafafa',
          borderRadius: 6,
        }}
      >
        <div style={{ fontWeight: 600, marginBottom: 4 }}>Leaf Block</div>
        <div style={{ color: '#555' }}>UID: {this.uid.slice(0, 6)}</div>
      </div>
    );
  }
}

class PluginSubmenuDemo extends Plugin {
  async load() {
    this.flowEngine.flowSettings.forceEnable();
    this.flowEngine.registerModels({ DemoRootModel, GroupBase, SubmenuBase, Leaf });
    const model = this.flowEngine.createModel({ uid: 'demo-root', use: 'DemoRootModel' });

    this.router.add('root', {
      path: '/',
      element: (
        <FlowEngineProvider engine={this.flowEngine}>
          <FlowModelRenderer model={model} />
        </FlowEngineProvider>
      ),
    });
  }
}

const app = new Application({
  router: { type: 'memory', initialEntries: ['/'] },
  plugins: [PluginSubmenuDemo],
});

export default app.getRootComponent();
  • 给基类通过 Model.define({ menuType: 'submenu' }) 指定展示形态;
  • 作为一级项出现,展开为二级菜单;可与其它分组按 meta.sort 混排排序。

通过 Model.defineChildren() 的方式自定义子菜单

import { Application, Plugin, BlockModel } from '@nocobase/client-v2';
import { AddSubModelButton, FlowModel, FlowModelContext, FlowModelRenderer } from '@nocobase/flow-engine';
import { Button, Space } from 'antd';

class HelloBlockModel extends FlowModel {
  render() {
    return (
      <Space direction="vertical" style={{ width: '100%' }}>
        {this.mapSubModels('items', (item) => {
          return <FlowModelRenderer key={item.uid} model={item} showFlowSettings={{ showBorder: true }} />;
        })}
        <AddSubModelButton model={this} subModelKey="items" subModelBaseClasses={['BlockModel']}>
          <Button>Add block</Button>
        </AddSubModelButton>
      </Space>
    );
  }
}

class BaseCollectionModel extends BlockModel {
  static defineChildren(ctx: FlowModelContext) {
    const ds = ctx.dataSourceManager.getDataSource('main');
    return ds.getCollections().map((collection) => {
      return {
        key: collection.name,
        label: collection.title,
        createModelOptions: {
          use: this.name,
          props: {
            collectionName: collection.name,
          },
        },
      };
    });
  }
}

BaseCollectionModel.define({
  hide: true,
});

class Hello1CollectionModel extends BaseCollectionModel {
  renderComponent() {
    return (
      <div>
        <h2>Hello1CollectionModel - {this.props.collectionName}</h2>
        <p>This is a sub model rendered by Hello1CollectionModel.</p>
      </div>
    );
  }
}

class Hello2CollectionModel extends BaseCollectionModel {
  renderComponent() {
    return (
      <div>
        <h2>Hello2CollectionModel - {this.props.collectionName}</h2>
        <p>This is a sub model rendered by Hello2CollectionModel.</p>
      </div>
    );
  }
}

Hello2CollectionModel.define({
  children: false,
});

class PluginHelloModel extends Plugin {
  async load() {
    this.flowEngine.flowSettings.forceEnable();
    this.flowEngine.registerModels({
      BlockModel,
      HelloBlockModel,
      BaseCollectionModel,
      Hello1CollectionModel,
      Hello2CollectionModel,
    });

    const mainDataSource = this.flowEngine.context.dataSourceManager.getDataSource('main');
    mainDataSource.addCollection({
      name: 'collection1',
      title: 'Collection 1',
    });
    mainDataSource.addCollection({
      name: 'collection2',
      title: 'Collection 2',
    });

    const model = this.flowEngine.createModel({
      uid: 'my-model',
      use: 'HelloBlockModel',
    });
    this.router.add('root', {
      path: '/',
      element: <FlowModelRenderer model={model} />,
    });
  }
}

const app = new Application({
  router: { type: 'memory', initialEntries: ['/'] },
  plugins: [PluginHelloModel],
});

export default app.getRootComponent();

通过 Model.defineChildren() 的方式自定义 group children

import { Application, Plugin, BlockModel } from '@nocobase/client-v2';
import { AddSubModelButton, FlowModel, FlowModelContext, FlowModelRenderer } from '@nocobase/flow-engine';
import { Button, Form, Input, Space } from 'antd';

class HelloBlockModel extends FlowModel {
  render() {
    return (
      <Space direction="vertical" style={{ width: '100%' }}>
        {this.mapSubModels('items', (item) => {
          return (
            <FlowModelRenderer
              key={item.uid}
              model={item}
              showFlowSettings={{ showBorder: false, showBackground: true }}
            />
          );
        })}
        <AddSubModelButton model={this} subModelKey="items" subModelBaseClasses={['BaseFieldModel']}>
          <Button>Configure fields</Button>
        </AddSubModelButton>
      </Space>
    );
  }
}

class BaseFieldModel extends BlockModel {
  static defineChildren(ctx: FlowModelContext) {
    const collection = ctx.dataSourceManager.getCollection('main', 'tests');
    return collection.getFields().map((field) => {
      return {
        key: field.name,
        label: field.title,
        createModelOptions: {
          use: 'FieldModel',
          props: {
            fieldName: field.name,
          },
        },
      };
    });
  }
}

BaseFieldModel.define({
  label: 'Fields',
});

class FieldModel extends FlowModel {
  render() {
    return (
      <div>
        <Form.Item layout="vertical" label={this.props.fieldName}>
          <Input />
        </Form.Item>
      </div>
    );
  }
}

class PluginHelloModel extends Plugin {
  async load() {
    this.flowEngine.flowSettings.forceEnable();
    const mainDataSource = this.flowEngine.context.dataSourceManager.getDataSource('main');
    mainDataSource.addCollection({
      name: 'tests',
      title: 'Tests',
      fields: [
        {
          name: 'name',
          type: 'string',
          title: 'Name',
          interface: 'input',
        },
        {
          name: 'title',
          type: 'string',
          title: 'Title',
          interface: 'input',
        },
      ],
    });
    this.flowEngine.registerModels({
      HelloBlockModel,
      BaseFieldModel,
      FieldModel,
    });
    const model = this.flowEngine.createModel({
      uid: 'my-model',
      use: 'HelloBlockModel',
    });
    this.router.add('root', {
      path: '/',
      element: <FlowModelRenderer model={model} />,
    });
  }
}

const app = new Application({
  router: { type: 'memory', initialEntries: ['/'] },
  plugins: [PluginHelloModel],
});

export default app.getRootComponent();

在子菜单中启用搜索

import { Application, Plugin, BlockModel } from '@nocobase/client-v2';
import { AddSubModelButton, FlowModel, FlowModelRenderer } from '@nocobase/flow-engine';
import { Button, Space } from 'antd';

class DemoContainerModel extends FlowModel {
  render() {
    return (
      <Space direction="vertical" style={{ width: '100%' }}>
        {this.mapSubModels('items', (item) => (
          <FlowModelRenderer key={item.uid} model={item} showFlowSettings={{ showBorder: true }} />
        ))}

        <AddSubModelButton
          model={this}
          subModelKey="items"
          items={[
            {
              key: 'submenu',
              label: 'Pick a block',
              searchable: true,
              searchPlaceholder: 'Search blocks',
              children: [
                { key: 'a', label: 'Block 1', useModel: 'BlockAModel' },
                { key: 'b', label: 'Block 2', useModel: 'BlockBModel' },
                {
                  key: 'g',
                  type: 'group',
                  label: 'Group X',
                  children: [{ key: 'x', label: 'Xray', useModel: 'BlockBModel' }],
                },
              ],
            },
          ]}
        >
          <Button>Add block (submenu search)</Button>
        </AddSubModelButton>
      </Space>
    );
  }
}

class BlockAModel extends BlockModel {
  renderComponent() {
    return (
      <div>
        <h3>Block 1</h3>
      </div>
    );
  }
}

class BlockBModel extends BlockModel {
  renderComponent() {
    return (
      <div>
        <h3>Block 2</h3>
      </div>
    );
  }
}

class DemoPlugin extends Plugin {
  async load() {
    this.flowEngine.flowSettings.forceEnable();
    this.flowEngine.registerModels({ DemoContainerModel, BlockAModel, BlockBModel });
    const model = this.flowEngine.createModel({ uid: 'demo', use: 'DemoContainerModel' });
    this.router.add('root', { path: '/', element: <FlowModelRenderer model={model} /> });
  }
}

const app = new Application({
  router: { type: 'memory', initialEntries: ['/'] },
  plugins: [DemoPlugin],
});

export default app.getRootComponent();
  • 任何包含 children 的菜单项只要设置 searchable: true,即会在该层级显示搜索框;
  • 支持同级存在 group 与非 group 的混合结构,搜索仅作用于当前层级。