#AddSubModelButton
Используется для добавления дочерней модели (subModel) в указанный FlowModel. Поддерживает множество вариантов настройки: асинхронную загрузку, группы, подменю, правила наследования моделей и другие.
#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;
}| Параметр | Тип | Описание |
|---|---|---|
model | FlowModel | Обязательный. Целевая модель, в которую добавляется дочерняя модель. |
subModelKey | string | Обязательный. Имя ключа дочерней модели в model.subModels. |
subModelType | 'object' | 'array' | Тип структуры данных дочерней модели, по умолчанию 'array'. |
items | SubModelItem[] | (ctx) => SubModelItem[] | Promise<...> | Определение пунктов меню, поддерживает статическую или асинхронную генерацию. |
subModelBaseClass | string | ModelConstructor | Указывает базовый класс — все наследующие его модели будут перечислены как пункты меню. |
subModelBaseClasses | (string | ModelConstructor)[] | Указывает несколько базовых классов — наследующие модели автоматически группируются. |
afterSubModelInit | (subModel) => Promise<void> | Колбэк, вызываемый после инициализации дочерней модели. |
afterSubModelAdd | (subModel) => Promise<void> | Колбэк, вызываемый после добавления дочерней модели. |
afterSubModelRemove | (subModel) => Promise<void> | Колбэк, вызываемый после удаления дочерней модели. |
children | React.ReactNode | Содержимое кнопки — можно настроить как те кст или иконку. |
keepDropdownOpen | boolean | Оставлять ли раскрытым выпадающее меню после добавления. По умолчанию закрывается автоматически. |
#Описание типа 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);
}| Поле | Тип | Описание |
|---|---|---|
key | string | Уникальный идентификатор. |
label | string | Отображаемый текст. |
type | 'group' | 'divider' | Группа или разделитель. Если опущено — обычный пункт или подменю. |
disabled | boolean | Отключён ли текущий пункт. |
hide | boolean | (ctx) => boolean | Promise<boolean> | Динамическое скрытие (возврат true означает скрыть). |
icon | React.ReactNode | Содержимое иконки. |
children | SubModelItemsType | Пункты подменю — для вложенных групп или подменю. |
useModel | string | Указывает используемый тип Model (зарегистрированное имя). |
createModelOptions | object | Параметры инициализации модели. |
toggleable | boolean | (model: FlowModel) => boolean | Режим переключателя: если уже добавлена — удалить, иначе — добавить (допускается только один экземпляр). |
#Примеры
#Добавление subModels через <AddSubModelButton />
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 — этот метод решает проблемы пропусков, сортировки и т. д.; - Используйте
<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(); - Из API, предоставляемого
ctx.dataSourceManager; - Из пользовательских свойств или методов контекста;
- Как
items, так иchildrenподдерживают асинхронные вызовы.
#Динамическое скрытие пунктов меню (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();
- Все FlowModel, наследующие
subModelBaseClass, будут перечислены; - Метаданные можно задать через
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();
- Все FlowModel, наследующие
subModelBaseClasses, будут перечислены; - Они автоматически группируются по
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();
#Пользовательские group children через 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();
#Включение поиска в подменю
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 на одном уровне; поиск работает только на текущем уровне.

