Skip to content

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:

FieldRequiredPurpose
button_idyesUnique ID within the plugin — referenced by the permission system ({pluginId}_{buttonId}_view/edit)
typeyesSee the list below — controls behavior and rendering
stylenodefault, primary, success, info, danger — Bootstrap color class
iconnoFont Awesome icon (without the fa- prefix, e.g. pencil, minus, plus, download)
titlenoVisible button label
support_textnoHelp text (e.g. in the delete dialog)
submit_textnoLabel of the submit button in the modal
forStatusnoVisible only when parseInt(item.status) === parseInt(btn.forStatus)
multilanguageno"1" enables language tabs in the edit modal (requires base_id/language_short on the DB table)
editable_rowsfor edit/insertArray of field definitions for the modal
buttonsfor nested buttonsNested 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=….

json
{
  "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):

json
{
  "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.

json
{
  "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.

json
{
  "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".

json
{
  "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.

json
{
  "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.

json
{
  "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().

No modal, no API call — just an <a href="…">:

json
{
  "button_id": "10",
  "type": "link",
  "style": "default",
  "icon": "external-link",
  "title": "Documentation",
  "url": "https://docs.example.com"
}

Style colors

styleTypical use
defaultStandard actions (edit)
primaryMain action (new, save)
successPositive confirmation (activate, publish)
infoSecondary actions (export, statistics)
dangerDestructive actions (delete)

Fields in editable_rows

Every entry in the editable_rows array is an object with:

FieldRequiredPurpose
columnyesDB column name
placeholdernoLabel above the input
typeyesField type — see the table below
select_valuefor select{table, value, title} for dropdown options
select_optionsfor static selectArray: [{value, title}]
data-aifeaturenoComma-list of enabled AI features (e.g. "seo_title,translate")

Field types

typeUIExample
textSingle-line inputTitle
textareaMulti-line textShort description
ckeditorRich-text editorArticle body
codemirrorCode editor (LESS/CSS/JS, GitHub theme, LESS syntax validation)Custom CSS
selectDropdownCategory picker
imageMedia Manager triggerFeatured image
checkboxTogglePublished yes/no
numberNumeric inputPrice
date, time, datetimeNative inputsPublish date
passwordMasked inputAPI keys (stored encrypted)
colorColor pickerBadge 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 executable

Admin 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:

json
{
  "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:

js
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:

json
{
  "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