Skip to content
  1. Extras
  2. MiniShop3
  3. Manager interface
  4. Category

Category page

Editing a product category in the MiniShop3 admin panel.

Overview

The category edit page (msCategory) extends standard MODX functionality with product management features:

  • Product table with drag-and-drop sorting
  • Filtering and search
  • Bulk operations (publish, delete)
  • Configurable columns and actions
  • Category options management

Tab structure

Products

Main category tab — product table with:

FeatureDescription
Drag-and-dropSort products by dragging
FiltersSearch, published status, custom filters
Bulk operationsPublish, unpublish, delete
Configurable columnsVia Utilities → Grid columns
ActionsView, edit, delete, duplicate

Document

Standard MODX tab with resource fields:

FieldDescription
pagetitleCategory name
longtitleExtended title
descriptionMeta description
introtextShort description
contentFull description
aliasURL alias
parentParent category

Settings

Resource system settings:

  • Template
  • Publication (date, status)
  • Indexing and caching
  • Category options — configure product options in this category (see below)

Category options

From v1.10.0-beta1

The tab is fully Vue (CategoryOptionsTab component). The legacy ExtJS grid and Processors/Category/Option/* processors were removed.

Grid of options linked to this category, with:

  • Drag-and-drop sorting — order (position) is saved in one POST on drop (/api/mgr/categories/{id}/options/sort)
  • Inline editing on double-click:
    • Default value — used as the default in the product form
    • Name (for category) — per-category override of the global caption (empty = use global)
  • "Global" column (read-only) — shows caption from msOption for comparison with the override
  • Bulk actions (checkbox selection): Activate / Deactivate / Make required / Remove required / Delete
  • "Add option" button — dialog to pick an existing option with fields: default value, active, required, caption/description override
  • "Copy options from category" button — copies all links from another category (skips duplicates). After copy, msCategoryOption::afterSave automatically applies options to all products in the current category.

Resource groups

Access control for the category.

Product table

Technology

The product table is built with Vue 3 + PrimeVue for modern UX:

  • Virtualization for large lists
  • Dynamic data loading
  • Reactive filters
  • Smooth drag-and-drop animation

Column configuration

Table columns are configured in Utilities → Grid columns (grid category-products).

Deprecated setting

The system setting ms3_category_grid_fields was removed in 1.7.0. Use Grid columns instead.

Inline editing

Double-click a cell to edit in place:

Editor typeFields
textpagetitle, longtitle, article, made_in
numberprice, old_price, weight
booleanpublished, new, popular, favorite

Configure in Utilities → Grid columns, grid category-products: enable editable, set editor_type (text, number, select, combo). Step-by-step: Grid columns cookbook.

See also: Utilities: Grid columns.

Table column configuration

Via interface

  1. Open Utilities → Grid columns
  2. Select the category-products grid
  3. Configure visibility, order, column width
  4. Save

Via API and utilities

In 1.13.x there is no PHP file core/components/minishop3/custom/grids/category-products.php. Columns live in ms3_grid_fields and are configured via:

  • Utilities → Grid columns (grid_key=category-products)
  • Manager API /api/mgr/grid-config/category-products (see Grid columns cookbook)

Example: add a column via API:

http
POST /api/mgr/grid-config/category-products/field
json
{
  "field_name": "stock",
  "label": "Stock",
  "type": "model",
  "visible": true,
  "sortable": true,
  "editable": true,
  "editor_type": "number",
  "config": {}
}

Inline edit uses editable, editor_type (text, number, select, combo), and optional editor_options. Cell write permission: msproduct_save (PUT /api/mgr/categories/{id}/products/{productId}/data).

Column types

TypeDescriptionExample
modelModel fieldTitle, SKU
pricePrice with displayConfig1,234.56
weightWeight with displayConfig0.5 kg
booleanYes/NoPublished
imageThumbnailProduct photo
templateHTML templateProduct link
actionsAction buttonsEdit, delete
relationRelated tableStatus name
badgeColored tagStatus with color
optionProduct optionoption.key
computedPHP column classcomputed.className

Full reference: Grid columns.

Relation and badge

Relation loads a JOIN. For badge in the orders grid, hidden relation columns supply text and HEX; the visible column is type badge with fields at the top level of config (not under computed):

json
{
  "type": "badge",
  "source_field": "status_name",
  "color_field": "status_color"
}

For type computed, config must include computed.className (class implements ComputedFieldInterface).

In category-products, relation aggregation is not supported.

Status colors

In msOrderStatus, color is often HEX without #. The UI prepends # when rendering a badge.

Adding actions to the column

Action configuration

Actions are configured in a column with type actions:

php
[
    'name' => 'actions',
    'label' => 'Actions',
    'visible' => true,
    'type' => 'actions',
    'width' => '180px',
    'actions' => [
        [
            'name' => 'view',
            'handler' => 'view',
            'icon' => 'pi-eye',
            'label' => 'view',
        ],
        [
            'name' => 'edit',
            'handler' => 'edit',
            'icon' => 'pi-pencil',
            'label' => 'edit',
        ],
        [
            'name' => 'publish',
            'handler' => 'publish',
            'icon' => 'pi-check',
            'iconOff' => 'pi-times',
            'label' => 'publish',
            'labelOff' => 'unpublish',
            'toggleField' => 'published',
        ],
        [
            'name' => 'duplicate',
            'handler' => 'duplicate',
            'icon' => 'pi-copy',
            'label' => 'duplicate',
        ],
        [
            'name' => 'delete',
            'handler' => 'delete',
            'icon' => 'pi-trash',
            'label' => 'delete',
            'severity' => 'danger',
            'confirm' => true,
            'confirmMessage' => 'product_delete_confirm_message',
        ],
    ],
]

Action parameters

ParameterTypeDescription
namestringUnique identifier
handlerstringHandler name (view, edit, delete, publish, duplicate)
iconstringPrimeIcons icon (pi-*)
iconOffstringIcon for off state (toggle)
labelstringLexicon key for tooltip
labelOffstringLexicon key for off state
severitystringButton style (danger, success, warning)
confirmboolRequire confirmation
confirmMessagestringLexicon key for confirmation message
toggleFieldstringField for toggle actions
visiblefunctionVisibility condition

Custom actions via JavaScript

Register via MS3ActionRegistry. The second argument is context, not gridId:

javascript
document.addEventListener('DOMContentLoaded', () => {
  if (!window.MS3ActionRegistry) return

  MS3ActionRegistry.register('addToFavorites', async (data, context) => {
    const response = await fetch('/assets/components/mycomponent/api.php', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        action: 'addToFavorites',
        product_id: data.id
      })
    })

    const result = await response.json()

    if (result.success) {
      return { success: true, refresh: true }
    }

    return { success: false, message: result.message }
  })
})

Add the action to the actions column config via Utilities → Grid columns or PUT grid-config:

json
{
  "name": "addToFavorites",
  "handler": "addToFavorites",
  "icon": "pi-heart",
  "label": "Add to favorites"
}

Hooks for standard actions

javascript
MS3ActionRegistry.registerBeforeHook('delete', async (data, context) => {
  if (data.orders_count > 0) {
    return false
  }
  return true
})

MS3ActionRegistry.registerAfterHook('publish', async (data, result, context) => {
  console.log(`Product ${data.id} published:`, result)
})

Adding a filter

Filter structure

Filters are configured via config files:

  • Default: core/components/minishop3/config/filters/category-products.php
  • Custom: core/components/minishop3/custom/filters/category-products.php

The custom file overrides the default and is not overwritten on component update.

Filter configuration

php
// core/components/minishop3/custom/filters/category-products.php

return [
    // Text search
    'query' => [
        'type' => 'text',
        'label' => 'search',
        'placeholder' => 'search_by_title_article',
        'width' => '250px',
        'position' => 10,
    ],

    // Published filter
    'published' => [
        'type' => 'select',
        'label' => 'published',
        'placeholder' => 'all',
        'source' => [
            'type' => 'static',
            'options' => [
                ['label' => 'ms3_yes', 'value' => 1],
                ['label' => 'ms3_no', 'value' => 0],
            ],
        ],
        'width' => '120px',
        'position' => 20,
    ],

    // Vendor filter
    'vendor_id' => [
        'type' => 'select',
        'label' => 'vendor',
        'placeholder' => 'all',
        'source' => [
            'type' => 'model',
            'class' => 'MiniShop3\\Model\\msVendor',
            'valueField' => 'id',
            'labelField' => 'name',
            'where' => ['active' => 1],
            'sort' => ['name' => 'ASC'],
        ],
        'width' => '180px',
        'position' => 30,
    ],

    // "New" flag filter
    'new' => [
        'type' => 'select',
        'label' => 'new',
        'placeholder' => 'all',
        'source' => [
            'type' => 'static',
            'options' => [
                ['label' => 'ms3_yes', 'value' => 1],
                ['label' => 'ms3_no', 'value' => 0],
            ],
        ],
        'width' => '100px',
        'position' => 40,
    ],

    // Hidden filter (not shown by default)
    'deleted' => [
        'type' => 'select',
        'label' => 'deleted',
        'visible' => false,
        'position' => 100,
    ],
];

Filter parameters

ParameterTypeDescription
typestringFilter type: text, select, datepicker, daterange
labelstringLexicon key for label
placeholderstringLexicon key for placeholder
widthstringCSS width (150px, 20%)
positionintDisplay order (lower = left)
visibleboolShow filter (default true)
sourcearrayData source config for select

Source types for select

Static options

php
'source' => [
    'type' => 'static',
    'options' => [
        ['label' => 'Yes', 'value' => 1],
        ['label' => 'No', 'value' => 0],
    ],
]

From xPDO model

php
'source' => [
    'type' => 'model',
    'class' => 'MiniShop3\\Model\\msVendor',
    'valueField' => 'id',
    'labelField' => 'name',
    'where' => ['active' => 1],
    'sort' => ['name' => 'ASC'],
    'limit' => 500,
]

Server-side filter handling

Filters are applied automatically in CategoryProductsController:

php
// core/components/minishop3/src/Controllers/Api/Manager/CategoryProductsController.php

// Boolean filters for msProduct fields
$productBooleanFields = ['published', 'deleted', 'hidemenu'];
foreach ($productBooleanFields as $field) {
    if (isset($params[$field]) && $params[$field] !== '') {
        $c->where(["msProduct.{$field}" => (int)$params[$field]]);
    }
}

// Boolean filters for msProductData fields
$dataBooleanFields = ['new', 'popular', 'favorite'];
foreach ($dataBooleanFields as $field) {
    if (isset($params[$field]) && $params[$field] !== '') {
        $c->where(["Data.{$field}" => (int)$params[$field]]);
    }
}

// Numeric filters
$dataNumericFields = ['price', 'vendor_id'];
foreach ($dataNumericFields as $field) {
    if (isset($params[$field]) && $params[$field] !== '') {
        $c->where(["Data.{$field}" => $params[$field]]);
    }
}

Adding a custom filter

Step 1: add filter to config

php
// core/components/minishop3/custom/filters/category-products.php

return [
    // ... existing filters ...

    'price_range' => [
        'type' => 'select',
        'label' => 'price_range',
        'placeholder' => 'all',
        'source' => [
            'type' => 'static',
            'options' => [
                ['label' => 'Up to 1000', 'value' => '0-1000'],
                ['label' => '1000-5000', 'value' => '1000-5000'],
                ['label' => '5000-10000', 'value' => '5000-10000'],
                ['label' => 'Over 10000', 'value' => '10000+'],
            ],
        ],
        'width' => '150px',
        'position' => 25,
    ],
];

Step 2: handle filter on server

Create a plugin to handle the custom filter:

php
<?php
// Plugin: CustomCategoryFilters
// Events: msOnBeforeCategoryProductsQuery

if ($modx->event->name !== 'msOnBeforeCategoryProductsQuery') return;

$params = $modx->event->params['params'] ?? [];
$query = $modx->event->params['query'];

// Price range filter
if (!empty($params['price_range'])) {
    $range = $params['price_range'];

    if ($range === '10000+') {
        $query->where(['Data.price:>=' => 10000]);
    } else {
        [$min, $max] = explode('-', $range);
        $query->where([
            'Data.price:>=' => (int)$min,
            'Data.price:<=' => (int)$max,
        ]);
    }
}

Bulk operations

Available operations

OperationDescription
PublishPublish selected products
UnpublishUnpublish selected products
DeleteMark as deleted
RestoreRestore deleted products

Bulk operations API

POST /api/mgr/categories/{id}/products/multiple

Parameters:

json
{
  "method": "publish",
  "ids": [1, 2, 3]
}

Available methods: publish, unpublish, delete, undelete, show, hide

Drag-and-drop sorting

When it works

Drag-and-drop sorting is available when:

  1. Sorting by menuindex
  2. "Show nested products" mode is off
  3. No active filters

Sort API

POST /api/mgr/categories/{id}/products/sort

Parameters:

json
{
  "items": [
    {"id": 5, "menuindex": 0},
    {"id": 3, "menuindex": 1},
    {"id": 8, "menuindex": 2}
  ]
}

System settings

SettingDescriptionDefault
ms3_category_show_nested_productsShow nested productsfalse
ms3_category_show_optionsShow category optionstrue
ms3_category_remember_tabsRemember active tabtrue
ms3_category_remember_gridRemember table statetrue

Events

msOnManagerCustomCssJs

Adding CSS/JS to the category page:

php
<?php
// Plugin: MyCategoryExtension
// Events: msOnManagerCustomCssJs

if ($modx->event->name !== 'msOnManagerCustomCssJs') return;

$page = $modx->event->params['page'] ?? '';

if ($page === 'category_update' || $page === 'category_create') {
    $modx->regClientCSS('/assets/components/mycomponent/css/category.css');
    $modx->regClientStartupScript('/assets/components/mycomponent/js/category.js');
}

API Endpoints

Category products

GET /api/mgr/categories/{id}/products

Parameters:

ParameterDescription
startOffset (pagination)
limitNumber of records
sortSort field
dirDirection (ASC/DESC)
querySearch query
nestedShow nested (0/1)
publishedPublished filter
*Any other filters

Filter configuration

GET /api/mgr/categories/{id}/products/filters

Response:

json
{
  "success": true,
  "object": {
    "filters": {
      "query": {
        "type": "text",
        "label": "search",
        "position": 10
      },
      "published": {
        "type": "select",
        "label": "published",
        "options": [
          {"label": "Yes", "value": 1},
          {"label": "No", "value": 0}
        ],
        "position": 20
      }
    }
  }
}

Inline product data editing

PUT /api/mgr/categories/{id}/products/{productId}/data

JSON body — msProductData fields (price, article, etc.) from the category grid without opening the product card. Controller: CategoryProductsController::updateProductData().