AddSubModelButton

Dùng để thêm subModel (sub-model) vào FlowModel đã chỉ định. Hỗ trợ nhiều cách cấu hình như tải bất đồng bộ, nhóm, submenu, quy tắc kế thừa model tùy chỉnh, v.v.

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;
}
Tham sốKiểuMô tả
modelFlowModelBắt buộc. Model đích để thêm subModel.
subModelKeystringBắt buộc. Tên key của subModel trong model.subModels.
subModelType'object' | 'array'Loại cấu trúc dữ liệu của subModel, mặc định là 'array'.
itemsSubModelItem[] | (ctx) => SubModelItem[] | Promise<...>Định nghĩa mục menu, hỗ trợ sinh tĩnh hoặc bất đồng bộ.
subModelBaseClassstring | ModelConstructorChỉ định một lớp cơ sở, liệt kê tất cả model kế thừa lớp này làm mục menu.
subModelBaseClasses(string | ModelConstructor)[]Chỉ định nhiều lớp cơ sở, tự động liệt kê model kế thừa theo nhóm.
afterSubModelInit(subModel) => Promise<void>Callback sau khi subModel khởi tạo.
afterSubModelAdd(subModel) => Promise<void>Callback sau khi subModel được thêm.
afterSubModelRemove(subModel) => Promise<void>Callback sau khi subModel bị xóa.
childrenReact.ReactNodeNội dung nút, có thể tùy chỉnh thành chữ hoặc biểu tượng.
keepDropdownOpenbooleanCó giữ menu thả xuống mở sau khi thêm hay không. Mặc định tự động đóng.

Định nghĩa kiểu 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);
}
TrườngKiểuMô tả
keystringĐịnh danh duy nhất.
labelstringVăn bản hiển thị.
type'group' | 'divider'Nhóm hoặc dấu phân cách. Khi bỏ qua là mục thông thường hoặc submenu.
disabledbooleanCó vô hiệu hóa mục hiện tại.
hideboolean | (ctx) => boolean | Promise<boolean>Ẩn động (trả về true biểu thị ẩn).
iconReact.ReactNodeNội dung biểu tượng.
childrenSubModelItemsTypeMục submenu, dùng để lồng nhóm hoặc submenu.
useModelstringChỉ định loại Model được dùng (tên đăng ký).
createModelOptionsobjectTham số khi khởi tạo model.
toggleableboolean | (model: FlowModel) => booleanDạng switch, đã thêm thì xóa, chưa thêm thì thêm (chỉ cho phép một).

Ví dụ

Dùng <AddSubModelButton /> để thêm 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();
  • Dùng <AddSubModelButton /> để thêm subModels, nút phải đặt trong một FlowModel nào đó mới có thể dùng;
  • Dùng model.mapSubModels() để duyệt subModels, phương thức mapSubModels sẽ giải quyết các vấn đề thiếu, sắp xếp, v.v.;
  • Dùng <FlowModelRenderer /> để render subModels.

Các dạng khác nhau của 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();
  • Có thể dùng component nút <Button>Add block</Button>, có thể đặt ở bất cứ đâu;
  • Cũng có thể dùng biểu tượng <PlusOutlined />;
  • Cũng có thể đặt ở vị trí Flow Settings ở góc trên bên phải.

Hỗ trợ dạng switch

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();
  • Tình huống đơn giản toggleable: true là được, mặc định tìm theo tên class, instance của cùng một class chỉ cho phép xuất hiện một lần;
  • Quy tắc tìm tùy chỉnh: toggleable: (model: FlowModel) => boolean.

items bất đồng bộ

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();

Có thể lấy items động từ context, ví dụ:

  • Có thể là remote ctx.api.request();
  • Cũng có thể lấy dữ liệu cần thiết từ API mà ctx.dataSourceManager cung cấp;
  • Cũng có thể là thuộc tính hoặc phương thức context tùy chỉnh;
  • Cả itemschildren đều hỗ trợ gọi async.

Ẩn mục menu động (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' }}>Tôi là một subModel thông thường.</div>
      </div>
    );
  }
}

class AdvancedBlockModel extends BlockModel {
  renderComponent() {
    return (
      <div>
        <h3>Advanced Block</h3>
        <div style={{ color: '#666' }}>Chỉ xuất hiện trong menu AddSubModelButton khi showAdvanced=true.</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 hỗ trợ boolean hoặc hàm (hỗ trợ async); trả về true biểu thị ẩn;
  • Sẽ tác động đệ quy lên group và children.

Sử dụng nhóm, submenu và dấu phân cách

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 là dấu phân cách;
  • type: group và có children là nhóm menu;
  • children, nhưng không có type là submenu.

Tự động sinh items qua cách kế thừa lớp

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();
  • Tất cả FlowModel kế thừa subModelBaseClass sẽ được liệt kê;
  • Có thể định nghĩa metadata liên quan thông qua Model.define();
  • Các mục được đánh dấu hide: true sẽ tự động ẩn.

Triển khai nhóm qua cách kế thừa lớp

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();
  • Tất cả FlowModel kế thừa subModelBaseClasses sẽ được liệt kê;
  • Tự động nhóm theo subModelBaseClasses và loại bỏ trùng lặp.

Triển khai menu cấp 2 qua kế thừa lớp + 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>Thêm subModel</Button>
        </AddSubModelButton>
      </Space>
    );
  }
}

class GroupBase extends FlowModel {}

GroupBase.define({
  label: 'Nhóm (phẳng)',
  sort: 200,
  children: () => [{ key: 'group-leaf', label: 'Mục con trong nhóm', createModelOptions: { use: 'Leaf' } }],
});

class SubmenuBase extends FlowModel {}

SubmenuBase.define({
  label: 'Menu cấp 2',
  menuType: 'submenu',
  sort: 110,
  children: () => [{ key: 'submenu-leaf', label: 'Mục con submenu', 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();
  • Chỉ định dạng hiển thị cho lớp cơ sở thông qua Model.define({ menuType: 'submenu' });
  • Xuất hiện làm mục cấp 1, mở rộng thành menu cấp 2; có thể trộn sắp xếp với các nhóm khác theo meta.sort.

Tùy chỉnh submenu thông qua 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();

Tùy chỉnh group children thông qua Model.defineChildren()

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();

Bật tìm kiếm trong submenu

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();
  • Bất kỳ mục menu nào chứa children chỉ cần đặt searchable: true, sẽ hiển thị ô tìm kiếm ở cấp đó;
  • Hỗ trợ cấu trúc hỗn hợp giữa group và non-group ở cùng cấp, tìm kiếm chỉ tác động lên cấp hiện tại.