AddSubModelButton

Digunakan untuk menambahkan sub-model (subModel) ke FlowModel yang ditentukan. Mendukung berbagai cara konfigurasi seperti loading asynchronous, group, sub-menu, aturan inheritance model kustom, dan sebagainya.

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;
}
ParameterTipeDeskripsi
modelFlowModelWajib. Model target yang akan ditambahi sub-model.
subModelKeystringWajib. Nama key untuk sub-model di model.subModels.
subModelType'object' | 'array'Tipe struktur data sub-model, default 'array'.
itemsSubModelItem[] | (ctx) => SubModelItem[] | Promise<...>Definisi item menu, mendukung pembuatan statis atau asynchronous.
subModelBaseClassstring | ModelConstructorMenentukan satu kelas dasar, semua model yang mewarisi kelas ini akan ditampilkan sebagai item menu.
subModelBaseClasses(string | ModelConstructor)[]Menentukan beberapa kelas dasar, secara otomatis menampilkan model turunan dalam grup.
afterSubModelInit(subModel) => Promise<void>Callback setelah sub-model diinisialisasi.
afterSubModelAdd(subModel) => Promise<void>Callback setelah sub-model ditambahkan.
afterSubModelRemove(subModel) => Promise<void>Callback setelah sub-model dihapus.
childrenReact.ReactNodeKonten tombol, dapat dikustomisasi sebagai teks atau ikon.
keepDropdownOpenbooleanApakah dropdown tetap terbuka setelah ditambahkan. Default tertutup secara otomatis.

Definisi Tipe 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);
}
FieldTipeDeskripsi
keystringIdentifier unik.
labelstringTeks tampilan.
type'group' | 'divider'Group atau divider. Jika dihilangkan, ini adalah item biasa atau sub-menu.
disabledbooleanApakah item ini dinonaktifkan.
hideboolean | (ctx) => boolean | Promise<boolean>Sembunyikan secara dinamis (return true untuk menyembunyikan).
iconReact.ReactNodeKonten ikon.
childrenSubModelItemsTypeItem sub-menu, digunakan untuk nesting group atau sub-menu.
useModelstringMenentukan tipe Model yang digunakan (nama terdaftar).
createModelOptionsobjectParameter saat menginisialisasi model.
toggleableboolean | (model: FlowModel) => booleanBentuk toggle, hapus jika sudah ditambahkan, tambahkan jika belum (hanya satu yang diperbolehkan).

Contoh

Menggunakan <AddSubModelButton /> untuk Menambahkan 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();
  • Menggunakan <AddSubModelButton /> untuk menambahkan subModels, tombol harus ditempatkan di dalam suatu FlowModel agar dapat digunakan;
  • Menggunakan model.mapSubModels() untuk iterasi subModels, metode mapSubModels akan menyelesaikan masalah missing, sorting, dan sebagainya;
  • Menggunakan <FlowModelRenderer /> untuk merender subModels.

Bentuk AddSubModelButton yang Berbeda

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();
  • Anda dapat menggunakan komponen tombol <Button>Add block</Button>, dapat ditempatkan di mana saja;
  • Atau menggunakan ikon <PlusOutlined />;
  • Atau menempatkannya di posisi Flow Settings di pojok kanan atas.

Mendukung Bentuk Toggle

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();
  • Untuk skenario sederhana cukup menggunakan toggleable: true, secara default mencari berdasarkan nama kelas, instance dari kelas yang sama hanya diperbolehkan muncul satu kali;
  • Aturan pencarian kustom: toggleable: (model: FlowModel) => boolean.

Items Asynchronous

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

Anda dapat memperoleh items dinamis dari context, misalnya:

  • Dapat berupa remote ctx.api.request();
  • Atau dapat memperoleh data yang diperlukan dari API yang disediakan oleh ctx.dataSourceManager;
  • Atau dapat berupa properti atau metode context kustom;
  • items dan children keduanya mendukung pemanggilan async.

Menyembunyikan Item Menu Secara Dinamis (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' }}>Saya adalah subModel biasa.</div>
      </div>
    );
  }
}

class AdvancedBlockModel extends BlockModel {
  renderComponent() {
    return (
      <div>
        <h3>Advanced Block</h3>
        <div style={{ color: '#666' }}>Hanya muncul di menu AddSubModelButton ketika 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 mendukung boolean atau function (mendukung async); return true untuk menyembunyikan;
  • Akan berlaku secara recursive untuk group dan children.

Menggunakan Group, Sub-menu, dan Divider

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 adalah divider;
  • type: group dengan children adalah grup menu;
  • Memiliki children tetapi tidak memiliki type adalah sub-menu.

Menghasilkan Items Secara Otomatis Melalui Kelas Turunan

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();
  • Semua FlowModel yang mewarisi subModelBaseClass akan ditampilkan;
  • Metadata terkait dapat didefinisikan melalui Model.define();
  • Item dengan hide: true akan otomatis disembunyikan.

Mengimplementasikan Group Melalui Kelas Turunan

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();
  • Semua FlowModel yang mewarisi subModelBaseClasses akan ditampilkan;
  • Otomatis di-grup berdasarkan subModelBaseClasses dan di-deduplikasi.

Mengimplementasikan Menu Dua Tingkat Melalui Kelas Turunan + 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>Tambah Sub-model</Button>
        </AddSubModelButton>
      </Space>
    );
  }
}

class GroupBase extends FlowModel {}

GroupBase.define({
  label: 'Group (Flat)',
  sort: 200,
  children: () => [{ key: 'group-leaf', label: 'Item Dalam Group', createModelOptions: { use: 'Leaf' } }],
});

class SubmenuBase extends FlowModel {}

SubmenuBase.define({
  label: 'Sub-menu',
  menuType: 'submenu',
  sort: 110,
  children: () => [{ key: 'submenu-leaf', label: 'Item Sub-menu', 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();
  • Tentukan bentuk tampilan untuk kelas dasar melalui Model.define({ menuType: 'submenu' });
  • Muncul sebagai item tingkat pertama, di-expand menjadi sub-menu tingkat dua; dapat digabungkan dengan grup lain dan disorting berdasarkan meta.sort.
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();

Children Group Kustom Melalui 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();

Mengaktifkan Pencarian dalam Sub-menu

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();
  • Item menu apa pun yang mengandung children, asalkan diatur searchable: true, akan menampilkan kotak pencarian pada level tersebut;
  • Mendukung struktur campuran group dan non-group pada level yang sama, pencarian hanya berlaku untuk level saat ini.