docs/docs/en/plugin-development/client/examples/custom-action.md
In NocoBase, actions are buttons within blocks that trigger business logic -- such as "Create", "Edit", "Delete", etc. This example demonstrates how to build custom action buttons using ActionModel and control the scenarios where buttons appear using ActionSceneEnum.
:::tip Prerequisites
It's recommended to familiarize yourself with the following content for a smoother development experience:
load() lifecycletExpr() deferred translation usage:::
We'll build three custom action buttons, each corresponding to a different action scenario:
collection) -- Appears in the action bar at the top of the block, next to the "Create" buttonrecord) -- Appears in the action column of each table row, next to "Edit" and "Delete"both) -- Appears in both scenariosFull source code is available at @nocobase-example/plugin-simple-action. If you want to run it locally:
yarn pm enable @nocobase-example/plugin-simple-action
Let's build this plugin step by step from scratch.
Run the following in the repository root:
yarn pm create @my-project/plugin-simple-action
For detailed instructions, see Writing Your First Plugin.
Each action needs to declare the scenario it appears in via the static scene property:
| Scenario | Value | Description |
|---|---|---|
| collection | ActionSceneEnum.collection | Acts on the data table, e.g., the "Create" button |
| record | ActionSceneEnum.record | Acts on a single record, e.g., "Edit" and "Delete" buttons |
| both | ActionSceneEnum.both | Available in both scenarios |
Create src/client-v2/models/SimpleCollectionActionModel.tsx:
// src/client-v2/models/SimpleCollectionActionModel.tsx
import { ActionModel, ActionSceneEnum } from '@nocobase/client-v2';
import { ButtonProps } from 'antd';
import { tExpr } from '../locale';
export class SimpleCollectionActionModel extends ActionModel {
static scene = ActionSceneEnum.collection;
defaultProps: ButtonProps = {
children: tExpr('Simple collection action'),
};
}
SimpleCollectionActionModel.define({
label: tExpr('Simple collection action'),
});
// Listen for click events via registerFlow, and provide user feedback with ctx.message
SimpleCollectionActionModel.registerFlow({
key: 'clickFlow',
title: tExpr('Simple collection action'),
on: 'click',
steps: {
showMessage: {
async handler(ctx) {
ctx.message.success(ctx.t('Collection action clicked'));
},
},
},
});
Create src/client-v2/models/SimpleRecordActionModel.tsx:
// src/client-v2/models/SimpleRecordActionModel.tsx
import { ActionModel, ActionSceneEnum } from '@nocobase/client-v2';
import { ButtonProps } from 'antd';
import { tExpr } from '../locale';
export class SimpleRecordActionModel extends ActionModel {
static scene = ActionSceneEnum.record;
defaultProps: ButtonProps = {
children: tExpr('Simple record action'),
};
}
SimpleRecordActionModel.define({
label: tExpr('Simple record action'),
});
// Record-level actions can access the current row's data and index via ctx.model.context
SimpleRecordActionModel.registerFlow({
key: 'clickFlow',
title: tExpr('Simple record action'),
on: 'click',
steps: {
showMessage: {
async handler(ctx) {
const index = ctx.model.context.recordIndex;
const record = ctx.model.context.record;
const id = record?.id;
ctx.message.info(ctx.t('Record action clicked, record ID: {{id}}, row index: {{index}}', { id, index }));
},
},
},
});
Create src/client-v2/models/SimpleBothActionModel.tsx:
// src/client-v2/models/SimpleBothActionModel.tsx
import { ActionModel, ActionSceneEnum } from '@nocobase/client-v2';
import { ButtonProps } from 'antd';
import { tExpr } from '../locale';
export class SimpleBothActionModel extends ActionModel {
static scene = ActionSceneEnum.both;
defaultProps: ButtonProps = {
children: tExpr('Simple both action'),
};
}
SimpleBothActionModel.define({
label: tExpr('Simple both action'),
});
SimpleBothActionModel.registerFlow({
key: 'clickFlow',
title: tExpr('Simple both action'),
on: 'click',
steps: {
showMessage: {
async handler(ctx) {
ctx.message.info(ctx.t('Both action clicked'));
},
},
},
});
The structure of all three is identical -- the only differences are the static scene value and button text. Each button listens for click events via registerFlow({ on: 'click' }) and uses ctx.message to display a notification so users can see the button is working.
Edit the translation files under the plugin's src/locale/:
// src/locale/zh-CN.json
{
"Simple collection action": "简单数据表操作",
"Simple record action": "简单记录操作",
"Simple both action": "简单通用操作",
"Collection action clicked": "数据表操作被点击了",
"Record action clicked, record ID: {{id}}, row index: {{index}}": "记录操作被点击了,记录 ID:{{id}},行索引:{{index}}",
"Both action clicked": "通用操作被点击了"
}
// src/locale/en-US.json
{
"Simple collection action": "Simple collection action",
"Simple record action": "Simple record action",
"Simple both action": "Simple both action",
"Collection action clicked": "Collection action clicked",
"Record action clicked, record ID: {{id}}, row index: {{index}}": "Record action clicked, record ID: {{id}}, row index: {{index}}",
"Both action clicked": "Both action clicked"
}
:::warning Note
Adding language files for the first time requires restarting the application to take effect.
:::
For more about translation file conventions and tExpr() usage, see i18n Internationalization.
Edit src/client-v2/plugin.tsx to register with lazy-loading via registerModelLoaders:
// src/client-v2/plugin.tsx
import { Plugin } from '@nocobase/client-v2';
export class PluginSimpleActionClient extends Plugin {
async load() {
this.flowEngine.registerModelLoaders({
SimpleCollectionActionModel: {
loader: () => import('./models/SimpleCollectionActionModel'),
},
SimpleRecordActionModel: {
loader: () => import('./models/SimpleRecordActionModel'),
},
SimpleBothActionModel: {
loader: () => import('./models/SimpleBothActionModel'),
},
});
}
}
export default PluginSimpleActionClient;
yarn pm enable @my-project/plugin-simple-action
Once enabled, you can add these custom action buttons from the "Configure Actions" menu in table blocks.
Capabilities used in this example:
| Capability | Usage | Documentation |
|---|---|---|
| Action Button | ActionModel + static scene | FlowEngine -> Action Extension |
| Action Scenario | ActionSceneEnum.collection / record / both / all | FlowEngine -> Action Extension |
| Menu Registration | define({ label }) | FlowEngine Overview |
| Model Registration | this.flowEngine.registerModelLoaders() | Plugin |
| Deferred Translation | tExpr() | i18n Internationalization |