Buttons
Buttons are the primary way to wire actions into the admin UI — both in list rows ($content_button) and in the header ($action_button, $header_buttons). This page covers every button type with its JSON structure and copy-paste examples.
Common fields
Every button is a JSON object with the following fields:
| Field | Required | Purpose |
|---|---|---|
button_id | yes | Unique ID within the plugin — referenced by the permission system ({pluginId}_{buttonId}_view/edit) |
type | yes | See the list below — controls behavior and rendering |
style | no | default, primary, success, info, danger — Bootstrap color class |
icon | no | Font Awesome icon (without the fa- prefix, e.g. pencil, minus, plus, download) |
title | no | Visible button label |
support_text | no | Help text (e.g. in the delete dialog) |
submit_text | no | Label of the submit button in the modal |
forStatus | no | Visible only when parseInt(item.status) === parseInt(btn.forStatus) |
multilanguage | no | "1" enables language tabs in the edit modal (requires base_id/language_short on the DB table) |
editable_rows | for edit/insert | Array of field definitions for the modal |
buttons | for nested buttons | Nested sub-buttons (e.g. in show_module_list) |
Button types
edit — edit modal
Opens an edit modal with the fields defined in editable_rows. The record is loaded on open via /api/backend/item?table=…&id=…&language=….
{
"button_id": "2",
"type": "edit",
"style": "default",
"icon": "pencil",
"title": "Edit",
"submit_text": "Save",
"multilanguage": "1",
"editable_rows": [
{"placeholder": "Title", "column": "title", "type": "text"},
{"placeholder": "Body", "column": "body", "type": "ckeditor"},
{"placeholder": "Image", "column": "image", "type": "image"}
]
}On submit, a PATCH /api/backend/item goes out with the form payload. With multilanguage: "1", one record is stored per language (base_id/language_short pattern).
insert — create-new modal
Identical to edit, but without preloading a record. Lives inside the $action_button property (header action):
{
"button_id": "3",
"type": "insert",
"style": "primary",
"icon": "plus",
"title": "New entry",
"submit_text": "Save",
"editable_rows": [
{"placeholder": "Title", "column": "title", "type": "text"},
{"placeholder": "Category", "column": "category", "type": "select",
"select_value": {"table": "news_category", "value": "id", "title": "category"}}
]
}Submit triggers POST /api/backend/item.
delete — delete dialog
Shows a confirmation dialog and triggers DELETE /api/backend/item on OK.
{
"button_id": "1",
"type": "delete",
"style": "danger",
"icon": "minus",
"title": "Delete",
"submit_text": "Delete",
"support_text": "Really delete this entry?"
}delete needs neither a column payload nor editable_rows — the row ID comes from the table and the core API deletes directly.
script — run a PHP action script
Runs a PHP script from the plugin's script/ folder without registering a custom API. The script receives id, url_rewrite, and optionally a data payload.
{
"button_id": "4",
"type": "script",
"style": "info",
"icon": "download",
"title": "Export",
"script": "export/runExport.php",
"submit_text": "Run"
}The script lives at _public/extensions/core/backend/{plugin}/script/export/runExport.php. The script value is a path relative to script/ — with or without subdirectories. If the script sits directly under script/runExport.php, "script": "runExport.php" is enough. The backend/actions/script endpoint loads the file dynamically and returns the response as JSON.
Script path validation
The path is filtered through a regex whitelist (/^[a-zA-Z0-9_\/\-]+\.php$/) and str_contains($script, '..'). Paths with dots (..) or invalid characters are rejected — never use user input directly as a script path.
show_module_list — sub-list with a custom row component
Opens a modal with a nested list, rendered through a Vue component (per row). Useful for 1:n relations — e.g. "edit article → article images" or "edit course → episodes".
{
"button_id": "5",
"type": "show_module_list",
"style": "default",
"icon": "list",
"title": "Episodes",
"values": {
"table": "elearning_episodes",
"template": "episodes.vue"
},
"buttons": [
{"button_id": "6", "type": "edit", "icon": "pencil", "title": "Edit episode",
"editable_rows": [...]},
{"button_id": "7", "type": "delete", "icon": "minus", "title": "Delete episode"}
]
}The row component episodes.vue lives at layout/episodes.vue in the plugin folder and renders a <tr> per row. It receives item, config, urlRewrite, and buttons as props.
Details on row components: Plugin Anatomy › Directory structure.
show_checklist — checkbox group
Opens a modal with a checklist that maps to a group table. Typical use case: user rights (user_groups.rights) or feature flags per entity.
{
"button_id": "8",
"type": "show_checklist",
"style": "info",
"icon": "check-square",
"title": "Assign rights",
"values": {
"table": "user_groups",
"column": "rights"
}
}Submit calls /api/backend/actions/checklist, which updates the column value (a JSON array) atomically.
show_custom_content — custom UI area
Loads an arbitrary PHP class and calls getContent(). The return value is used as the data source for a Vue template. For complex detail views (e.g. statistics, report dashboards) that don't fit a standard edit modal.
{
"button_id": "9",
"type": "show_custom_content",
"style": "default",
"icon": "chart-bar",
"title": "Statistics",
"values": {
"class": "StatsController",
"template": "stats.vue"
}
}Details in Plugin Anatomy › getContent().
link — external/internal link
No modal, no API call — just an <a href="…">:
{
"button_id": "10",
"type": "link",
"style": "default",
"icon": "external-link",
"title": "Documentation",
"url": "https://docs.example.com"
}Style colors
style | Typical use |
|---|---|
default | Standard actions (edit) |
primary | Main action (new, save) |
success | Positive confirmation (activate, publish) |
info | Secondary actions (export, statistics) |
danger | Destructive actions (delete) |
Fields in editable_rows
Every entry in the editable_rows array is an object with:
| Field | Required | Purpose |
|---|---|---|
column | yes | DB column name |
placeholder | no | Label above the input |
type | yes | Field type — see the table below |
select_value | for select | {table, value, title} for dropdown options |
select_options | for static select | Array: [{value, title}] |
data-aifeature | no | Comma-list of enabled AI features (e.g. "seo_title,translate") |
Field types
type | UI | Example |
|---|---|---|
text | Single-line input | Title |
textarea | Multi-line text | Short description |
ckeditor | Rich-text editor | Article body |
codemirror | Code editor (LESS/CSS/JS, GitHub theme, LESS syntax validation) | Custom CSS |
select | Dropdown | Category picker |
image | Media Manager trigger | Featured image |
checkbox | Toggle | Published yes/no |
number | Numeric input | Price |
date, time, datetime | Native inputs | Publish date |
password | Masked input | API keys (stored encrypted) |
color | Color picker | Badge color |
Per-button permissions
For every button, admins can set view and edit per group. Permission keys follow the scheme {pluginId}_{buttonId}_view and {pluginId}_{buttonId}_edit — generated automatically, no manual maintenance.
24_2_view # Plugin 24, button 2 (edit) visible
24_2_edit # Plugin 24, button 2 executable
24_1_view # Button 1 (delete) visible
24_1_edit # Button 1 executableAdmin users (rights === "0") bypass all checks. Plugin permissions are assigned under /admin/user-management.
forStatus — conditional visibility
Show a button only when the record has a specific status:
{
"button_id": "11",
"type": "script",
"icon": "check",
"title": "Publish",
"forStatus": 0,
"script": "publish.php"
}The button is visible only when parseInt(item.status) === 0. Useful for workflow statuses (draft → approved → archived).
Sentinel value -1: setting forStatus: -1 skips the check entirely — the button is always visible. That's the conventional way to disable a pre-existing forStatus entry without removing it. The real check in NsTable.vue looks like this:
if (btn.forStatus !== undefined && btn.forStatus !== -1) {
if (parseInt(item.status) !== parseInt(btn.forStatus)) return false
}Nested buttons
show_module_list and show_custom_content buttons can contain their own buttons array, rendered inside the sub-modal:
{
"button_id": "5",
"type": "show_module_list",
"title": "Episodes",
"values": {"table": "elearning_episodes", "template": "episodes.vue"},
"buttons": [
{"button_id": "6", "type": "edit", "editable_rows": [...]},
{"button_id": "7", "type": "delete"},
{"button_id": "8", "type": "script", "script": "reorder.php", "title": "Reorder"}
]
}button_ids must be unique within the plugin — including for nested buttons. Otherwise the permission keys collide. (The fact that the snippets above reuse the same IDs is a doc simplification — in a real plugin, IDs are not reused.)
Common issues
JSON escaping inside PHP heredoc
$content_button is a PHP string containing JSON. The easiest path is single quotes: '[{"button_id":"1",...}]'. With double quotes, every " needs a \ escape — error-prone. For deeply nested escapes, use a heredoc with <<<JSON.
button_id as a string, not a number
All button_id values are strings ("2", not 2). The permission system uses string keys — 2 vs. "2" leads to non-matching permissions.
editable_rows with an invalid DB column
A field with "column": "nonexistent_column" throws a SQL error on submit. The core API does not pre-validate columns.
forStatus compares via parseInt
forStatus: 0 matches both item.status === 0 and item.status === "0" (parseInt). But forStatus: "draft" matches nothing useful — parseInt("draft") === NaN. Use numeric statuses only.
Submit response handling for script
PHP scripts must call json_encode() and exit themselves so the core action returns cleanly. The response is passed through 1:1, no automatic wrapping in {success:true, ...}.
See also
- Content Constructs — where buttons plug in
- Plugin Anatomy ›
getContent()— custom-content buttons - Migrations — DB tables for button actions
- API Endpoints — a custom API instead of a
scriptbutton when you need more control - Widgets › widget_menu field types — same field types for widget forms