Files
prestashop-entity-selector/ENTITY-SELECTOR.md
myprestarocks 55e3135903 feat: unified preview eye icon component, enhanced search & preview
- Unify filter group and filter value preview icons into shared
  .filter-chip-wrapper + .chip-preview-btn component pattern
- Remove old .toggle-count.clickable inline eye icon approach
- Add dropdown-level event handler for preview buttons (dropdown
  appended to body, needs separate delegation)
- Enhanced EntitySearchEngine with improved product condition
  resolution and preview data
- Add EntityPreviewHandler for richer preview popovers
- Various SCSS improvements for chips, groups, and list-preview

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 21:34:37 +00:00

28 KiB
Raw Blame History

PrestaShop Entity Selector — Architecture & Developer Guide

Overview

The Entity Selector is a Composer package (myprestarocks/prestashop-entity-selector) providing a universal entity targeting widget for PrestaShop admin controllers. It supports group-based selection with include/exclude logic, live preview, and 18 entity types with 130+ selection methods.

Package: vendor/myprestarocks/prestashop-entity-selector/ Namespace: MyPrestaRocks\EntitySelector


Architecture

File Structure

src/
├── EntitySelector.php                    # Main trait (orchestrator, AJAX routing, asset loading)
├── ScheduleConditions.php                # Scheduling trait (datetime, weekly, holidays)
├── HolidayProviderInterface.php          # Holiday provider contract
└── EntitySelector/
    ├── EntitySelectorRenderer.php        # HTML rendering, layouts, block/group UI
    ├── EntitySearchEngine.php            # AJAX search, count, getByIds per entity type
    ├── ProductConditionResolver.php      # Product ID resolution for all 40+ methods
    ├── EntityQueryHandler.php            # Non-product entity ID resolution
    └── EntityPreviewHandler.php          # Preview data generation for all types

assets/
├── css/admin/
│   ├── entity-selector.css               # Compiled from SCSS
│   └── mpr-modal.css
└── js/admin/
    ├── entity-selector.js                 # Main JS (chips, search, serialization, UI)
    ├── entity-selector.min.js
    ├── entity-list-preview.js
    └── schedule-conditions.js

sources/
├── scss/                                  # SCSS source files
└── js/admin/entity-selector/              # JS source (compiled via gulp)
    ├── _core.js
    ├── _events.js
    ├── _dropdown.js
    ├── _search.js
    ├── _groups.js
    ├── _chips.js
    ├── _methods.js
    ├── _preview.js
    ├── _filters.js
    └── _utils.js

Component Roles

Component Role
EntitySelector (trait) Orchestrator. Mixes into admin controllers. Routes AJAX, initializes assets, lazy-loads helpers.
EntitySelectorRenderer Generates all HTML. Handles 3 layout modes, block tabs, group UI, value pickers.
EntitySearchEngine Handles searchTargetEntities AJAX. Dispatches to searchTarget{EntityType}() methods. Returns {id, name, reference, subtitle, active, image}.
ProductConditionResolver Resolves product selection methods to product IDs. 40+ methods from by_category to by_isbn_pattern. Also handles combination attribute resolution.
EntityQueryHandler Resolves selection methods for non-product entities (categories, manufacturers, customers, etc.).
EntityPreviewHandler Generates rich preview data (name, image, reference, attributes) for displaying matched items.
entity-selector.js Client-side: chips, search dropdowns, serialization, value pickers, keyboard shortcuts, pagination.

Data Flow

Controller (uses EntitySelector trait)
    │
    ├── setMedia() → initEntitySelector() → loads CSS + JS
    │
    ├── renderForm() → renderEntitySelectorHtml($config, $savedData)
    │   └── EntitySelectorRenderer → HTML with blocks, groups, pickers
    │
    ├── ajaxProcess*() → handleEntitySelectorAjax()
    │   ├── searchTargetEntities → EntitySearchEngine::search()
    │   ├── getTargetEntitiesByIds → EntitySearchEngine::getByIds()
    │   ├── countConditionMatches → ProductConditionResolver::getIdsByMethod()
    │   ├── previewConditionItems → EntityPreviewHandler::getProductPreviewData()
    │   ├── previewGroupItems → resolve + apply modifiers + preview
    │   └── ... (20+ AJAX actions)
    │
    └── postProcess() → parse JSON from hidden input → resolve IDs → save

Usage

Basic Integration

use MyPrestaRocks\EntitySelector\EntitySelector;

class AdminMyController extends ModuleAdminController
{
    use EntitySelector;

    public function setMedia($isNewTheme = false)
    {
        parent::setMedia($isNewTheme);
        $this->initEntitySelector();   // loads CSS + JS
    }

    // AJAX handler — route entity selector requests
    public function ajaxProcessEntitySelector()
    {
        $this->handleEntitySelectorAjax();
    }

    // Render the widget
    public function renderForm()
    {
        $html = $this->getEntitySelectorRenderer()->renderEntitySelectorHtml(
            $config,     // widget configuration
            $savedData   // previously saved selection data
        );
        // embed $html in your form
    }

    // Required: translate strings for the widget
    protected function transEntitySelector($string)
    {
        return $this->module->l($string, 'EntitySelector');
    }
}

Configuration Options

$config = [
    // Identity
    'id' => 'my-selector',              // unique DOM ID
    'name' => 'entity_selector',         // form field name
    'name_prefix' => 'target_',          // prefix for generated field names

    // Labels
    'title' => 'Target Selection',
    'subtitle' => 'Define which items to target',

    // Which built-in entity blocks to show
    'show_products' => true,
    'show_categories' => true,
    'show_manufacturers' => true,
    'show_suppliers' => true,
    'show_cms' => true,
    'show_cms_categories' => true,

    // Mode
    'mode' => 'multi',                   // 'multi' = multiple groups, 'single' = one group only
    'combination_mode' => 'products',    // 'products', 'combinations', or 'toggle' (for by_combination method)
    'empty_means_all' => false,          // if true, empty selection = all items
    'required' => false,                 // validation: must have at least one selection
    'required_message' => '',            // custom validation message

    // UI
    'show_modifiers' => true,            // show limit/sort per group
    'show_all_toggle' => false,          // show "select all" toggle

    // Layout: 'standalone' (full widget), 'form-group' (PS form-group), 'form-content' (content only)
    'layout' => 'standalone',

    // Custom entity blocks (see Custom Entity Types section)
    'customBlocks' => [],

    // Override default block definitions
    'blocks' => [],
];

Saved Data Format

{
    "products": {
        "groups": [
            {
                "name": "Group 1",
                "include": {
                    "method": "by_category",
                    "values": [3, 5, 8]
                },
                "excludes": [
                    {
                        "method": "specific",
                        "values": [42, 99]
                    }
                ],
                "modifiers": {
                    "limit": 10,
                    "sort_by": "sales",
                    "sort_dir": "DESC"
                }
            }
        ]
    },
    "categories": {
        "groups": [
            {
                "include": { "method": "all", "values": [] },
                "excludes": []
            }
        ]
    }
}

Group Logic

  • Within a group: Include MINUS Exclude (set difference)
  • Between groups: OR (union)
  • Between blocks: AND (intersection)

Resolving IDs from Saved Data

// Get product IDs from saved selection
$data = json_decode(Tools::getValue('entity_selector'), true);
$groups = $data['products']['groups'] ?? [];

$resolver = $this->getProductConditionResolver();
$productIds = [];

foreach ($groups as $group) {
    // Resolve include
    $includeIds = $resolver->getIdsByMethod($group['include']['method'], $group['include']['values']);

    // Resolve excludes
    foreach ($group['excludes'] ?? [] as $exclude) {
        $excludeIds = $resolver->getIdsByMethod($exclude['method'], $exclude['values']);
        $includeIds = array_diff($includeIds, $excludeIds);
    }

    // Apply modifiers
    if (!empty($group['modifiers'])) {
        $includeIds = $resolver->applyModifiers($includeIds, $group['modifiers']);
    }

    $productIds = array_merge($productIds, $includeIds);
}

$productIds = array_unique($productIds);

Entity Types & Selection Methods

Products (40+ methods)

By Entity:

Method Value Type Returns
all none All active products
specific entity_search (products) Selected product IDs
by_category entity_search (categories) Products in categories
by_manufacturer entity_search (manufacturers) Products by manufacturer
by_supplier entity_search (suppliers) Products by supplier
by_tag entity_search (tags) Products by tag
by_attribute entity_search (attributes) Products with attribute(s)
by_feature entity_search (features) Products with feature(s)
by_combination combination_attributes Products/combinations by attribute groups

By Property:

Method Value Type Options
by_condition multi_select_tiles new, used, refurbished
by_visibility multi_select_tiles both, catalog, search, none
by_active_status multi_select_tiles active, inactive
by_stock_status multi_select_tiles in_stock, out_of_stock, low_stock
by_on_sale boolean Has on-sale flag
by_has_specific_price boolean Has active specific price
by_is_virtual boolean Is virtual product
by_is_pack boolean Is pack product
by_has_combinations boolean Has combinations/variants
by_available_for_order boolean Available for order
by_online_only boolean Online only
by_has_related boolean Has accessories/related
by_has_customization boolean Has customization fields
by_has_attachments boolean Has attachments
by_has_additional_shipping boolean Has additional shipping cost
by_out_of_stock_behavior multi_select_tiles deny, allow, default
by_delivery_time multi_select_tiles none, default, specific
by_carrier_restriction multi_select_tiles restricted, all

By Text Pattern:

Method Value Type
by_name_pattern pattern
by_reference_pattern pattern
by_description_pattern pattern
by_long_description_pattern pattern
by_ean13_pattern pattern
by_upc_pattern pattern
by_isbn_pattern pattern
by_mpn_pattern pattern
by_meta_title_pattern pattern
by_meta_description_pattern pattern

By Range:

Method Value Type
by_id_range numeric_range
by_price_range numeric_range
by_weight_range numeric_range
by_quantity_range numeric_range
by_position_range numeric_range
by_date_added date_range
by_date_updated date_range

Other Entities (summary)

Entity Key Methods
Categories all, specific, by_name_pattern, by_product_count, by_depth_level, by_active_status
Manufacturers all, specific, by_name_pattern, by_product_count, by_active_status
Suppliers all, specific, by_name_pattern, by_product_count, by_active_status
CMS Pages all, specific, by_cms_category, by_name_pattern, by_active_status, by_indexable
CMS Categories all, specific, by_name_pattern, by_active_status, by_page_count
Employees all, specific, by_profile, by_name_pattern, by_active_status
Customers all, specific, by_group, by_name_pattern, by_email_pattern, by_company, by_order_count, by_turnover, by_active_status, by_newsletter, by_guest
Customer Groups all, specific, by_name_pattern, by_price_display
Carriers all, specific, by_name_pattern, by_active_status, by_shipping_handling, by_free_shipping, by_zone, by_customer_group, by_price_range, by_weight_range
Zones all, specific, by_name_pattern, by_active_status
Countries all, specific, by_zone, by_name_pattern, by_active_status, by_contains_states, by_need_zip_code, by_zip_format, by_need_identification
Currencies all, specific, by_name_pattern, by_active_status
Languages all, specific, by_name_pattern, by_active_status, by_rtl
Shops all, specific, by_name_pattern, by_active_status
Profiles all, specific, by_name_pattern
Order States all, specific, by_name_pattern, by_paid, by_shipped, by_delivery
Taxes all, specific, by_name_pattern, by_rate_range, by_active_status

Value Types

Type UI Component Input Format Example
none No input [] "All products"
entity_search Search dropdown + chips [id1, id2, ...] Specific products, By category
pattern Text input + case toggle [{"pattern": "blue*", "caseSensitive": false}] Name pattern
multi_select_tiles Toggle tile buttons ["new", "used"] Condition
select Single dropdown "value" Active status
numeric_range Min/Max inputs {"min": 10, "max": 50} Price range
multi_numeric_range Multiple ranges [{"min": 0, "max": 10}, ...] Price bands
date_range Date pickers {"from": "2024-01-01", "to": "2024-12-31"} Date added
boolean Flag (no input) [true] Has combinations
combination_attributes Attribute group picker {"mode": "products", "attributes": {"1": [3,4]}} By combination

Combination Attributes (by_combination)

The by_combination selection method allows targeting products by their attribute combinations. This is a selection method within the products block, not a separate entity type.

How It Works

  1. User selects "Combination" from the method dropdown
  2. UI loads all attribute groups via AJAX (getAttributeGroups)
  3. User clicks a group → lazy-loads values via AJAX (getAttributeValues)
  4. User selects attribute values (tile-based UI with select-all/clear/search)
  5. Logic: OR within a group (Red OR Blue), AND between groups (Color AND Size)

Combination Modes (combination_mode config)

Mode Behavior Returns
'products' (default) Attribute selection filters products. Returns product IDs. id_product[]
'combinations' Returns exact combination IDs (product_attribute). id_product_attribute[]
'toggle' Shows radio toggle letting user choose per-selection. Either type

Data Format

{
    "method": "by_combination",
    "values": {
        "mode": "products",
        "attributes": {
            "1": [3, 4],
            "2": [7, 8, 9]
        }
    }
}

Where "1" is id_attribute_group and [3, 4] are id_attribute values within that group.

Backend Resolution (ProductConditionResolver)

// ProductConditionResolver::getProductIdsByCombinationAttributes($values)
// mode = 'products': SELECT DISTINCT pa.id_product FROM ...
// mode = 'combinations': SELECT DISTINCT pa.id_product_attribute FROM ...
// Uses correlated subqueries for AND-logic between groups, OR within groups

Custom Entity Blocks

Custom entity blocks allow modules to add their own entity types (e.g., materials, suppliers, warehouses) to the entity selector widget.

Registration via customBlocks config

$config['customBlocks'] = [
    'mpr_materials' => [
        'label' => 'Materials',
        'entity_label' => 'material',
        'entity_label_plural' => 'materials',
        'icon' => 'icon-cubes',
        'search_entity' => 'mpr_materials',    // entity type key for search routing
        'selection_methods' => [
            'all' => [
                'label' => 'All materials',
                'icon' => 'icon-asterisk',
                'value_type' => 'none',
                'group' => '',
            ],
            'specific' => [
                'label' => 'Specific materials',
                'icon' => 'icon-cube',
                'value_type' => 'entity_search',
                'search_entity' => 'mpr_materials',
                'group' => 'by_entity',
            ],
        ],
    ],
];

Search Handler Implementation

The EntitySearchEngine routes searches via searchTarget{CamelCase}(). For custom entities, you need to either:

Option A: Subclass EntitySearchEngine and add your search methods, then override the getter in your controller.

Option B: Override ajaxSearchTargetEntities() in your controller to intercept custom entity types before delegating to the engine:

protected function ajaxSearchTargetEntities()
{
    $entityType = Tools::getValue('entity_type', '');

    // Handle custom entity types
    if ($entityType === 'mpr_materials') {
        $query = trim(Tools::getValue('q', ''));
        $limit = (int) Tools::getValue('limit', 20);
        $offset = (int) Tools::getValue('offset', 0);

        // Your search logic
        $results = $this->searchMaterials($query, $limit, $offset);
        $total = $this->countMaterials($query);

        $this->ajaxDie(json_encode([
            'success' => true,
            'results' => $results,   // [{id, name, subtitle?, active?, image?}, ...]
            'total' => $total,
            'offset' => $offset,
            'limit' => $limit,
        ]));
        return;
    }

    // Fall back to built-in handler
    parent::ajaxSearchTargetEntities();
}

Similarly override ajaxGetTargetEntitiesByIds() and ajaxGetTargetEntitiesByIdsBulk() for chip restoration on form load.

Search Result Format

[
    'id' => (int) 42,
    'name' => 'Display Name',           // required
    'subtitle' => 'Reference or info',  // optional, shown below name
    'active' => true,                   // optional, dim if false
    'image' => 'https://...',           // optional, thumbnail URL
    'reference' => 'REF-001',           // optional, shown as badge
]

Layout Modes

standalone (default)

Full widget with header, collapse toggle, tabs, groups. Best for dedicated pages or modals.

form-group

Wraps widget in PrestaShop's standard form-group structure (col-lg-3 label + col-lg-9 content). Best for embedding in existing forms.

form-content

Just the content area (no wrapper). For when you have an existing form-group and need only the inner widget. Collapsed by default.

// Standalone
$config['layout'] = 'standalone';

// Inside a form
$config['layout'] = 'form-group';

// Content only (collapsed by default)
$config['layout'] = 'form-content';
$config['collapsed'] = false;  // override default collapse

AJAX Actions

All AJAX actions route through handleEntitySelectorAjax() with trait=EntitySelector.

Action Purpose Key Params
searchTargetEntities Search entities by type entity_type, q, limit, offset, filters
getTargetEntitiesByIds Get entity by ID (chip restore) entity_type, ids
getTargetEntitiesByIdsBulk Bulk get entities by IDs entities (JSON object)
countConditionMatches Count matching items for a method block_type, method, values
countConditionMatchesBulk Bulk count multiple conditions conditions (JSON)
previewConditionItems Preview items for a condition block_type, method, values, limit
previewGroupItems Preview group with modifiers block_type, group_data, limit
countGroupItems Count items in a group block_type, group_data
previewEntitySelector Preview full entity selection conditions, block_type, limit
previewEntitySelectorBulk Bulk preview all entity types conditions (JSON)
getAttributeGroups List attribute groups
getAttributeValues Get values for an attribute group group_id
getFeatureGroups List feature groups
getCategoryTree Category tree hierarchy
countPatternMatches Count pattern matches pattern, field, entity_type
previewPatternMatches Preview pattern matches pattern, entity_type, limit

Group Modifiers

Available per group to limit and sort results:

'modifiers' => [
    'limit' => 5,                  // max items from this group
    'sort_by' => 'sales',          // sort field
    'sort_dir' => 'DESC',          // sort direction
]

Sort fields for products: name, price, date_add, position, quantity, reference, id, sales


JavaScript API

Configuration Object

The JS receives config from PHP via data attributes:

// Config keys available in JS
this.config = {
    id: 'selector-id',
    ajaxUrl: '...',
    trans: { /* all translation strings */ },
    methodHelp: { /* help content per method */ },
    combinationMode: 'products',     // 'products', 'combinations', 'toggle'
    emptyMeansAll: false,
};

Events

// Selection changed
$(document).on('entitySelector:change', function(e, data) { });

// Block tab switched
$(document).on('entitySelector:tabChange', function(e, blockType) { });

// Group added/removed
$(document).on('entitySelector:groupAdd', function(e, groupData) { });
$(document).on('entitySelector:groupRemove', function(e, groupIndex) { });

Keyboard Shortcuts

Key Action
Ctrl+A Select all in dropdown
Ctrl+D Deselect all
Esc Close dropdown
Enter Confirm selection

Schedule Conditions (separate trait)

use MyPrestaRocks\EntitySelector\ScheduleConditions;

class AdminMyController extends ModuleAdminController
{
    use EntitySelector;
    use ScheduleConditions;

    public function setMedia($isNewTheme = false)
    {
        parent::setMedia($isNewTheme);
        $this->initEntitySelector();
        $this->initScheduleConditions();
    }
}

Configuration: datetime range, weekly schedule (per-day hour ranges), holiday exclusions by country.


Building from Source

npm install
npm run build      # compile SCSS → CSS, concatenate JS
npm run watch       # watch mode

Planned Feature: Product Selection Level

Status: Planned — Not Yet Implemented

Problem

Currently, the entity selector's product search always returns base products. When a user searches for "Hummingbird sweater", they get one result regardless of how many combinations (Size: S/M/L/XL × Color: White/Black) the product has.

For many use cases, users need to target specific combinations rather than entire products:

  • Stock management: Set stock per combination (Size S, Color White)
  • Discounts: Apply discount to specific variants
  • Product links: Link specific combinations to materials

Solution: product_selection_level Config

A new config option that changes how the products block fundamentally works — not just the by_combination selection method, but how all product-related operations (search, display, selection, preview) behave.

$config['product_selection_level'] = 'product';       // default — current behavior
$config['product_selection_level'] = 'combination';   // combination mode
$config['product_selection_level'] = 'both';           // combined mode

Three Modes

Mode 1: product (Default — Current Behavior)

Search results show base products. Returns id_product. The product "Hummingbird sweater" appears as one item regardless of combinations.

Use case: Product links, category assignments, anywhere you target an entire product.

Search result:

[x] Hummingbird sweater          REF-001

Returns: id_product = 1

Mode 2: combination

For products with combinations, each combination appears as a separate selectable item. The base product itself is NOT shown. Products without combinations (simple products) still appear as-is.

Use case: Stock management, per-variant pricing, anything that targets individual variants.

Search result for "Hummingbird sweater" (4 combinations):

[x] Hummingbird sweater — Size: S, Color: White     REF-001
[x] Hummingbird sweater — Size: S, Color: Black     REF-001
[x] Hummingbird sweater — Size: M, Color: White     REF-001
[x] Hummingbird sweater — Size: M, Color: Black     REF-001

Search result for "Brown bear notebook" (simple product, no combinations):

[x] Brown bear notebook          REF-002

Returns: id_product_attribute for combination products, id_product for simple products (needs encoding, e.g., negative ID or composite key).

Mode 3: both (Combined)

Both the full product AND each individual combination appear as selectable items. Users choose what they need — the entire product or specific variants.

Use case: Discounts (apply to whole product OR specific combinations), flexible targeting.

Search result for "Hummingbird sweater":

[x] Hummingbird sweater          REF-001          [entire product]
    [x] — Size: S, Color: White
    [x] — Size: S, Color: Black
    [x] — Size: M, Color: White
    [x] — Size: M, Color: Black

Returns: Mix of id_product (for whole-product selections) and id_product_attribute (for specific combinations).

Implementation Areas

This feature requires changes across the full stack:

1. EntitySearchEngine — searchTargetProducts()

  • When product_selection_level = 'combination': query ps_product_attribute joined with ps_product_attribute_combination + attribute names
  • Return combination-level results: {id: pa.id_product_attribute, name: "Product — Attr1: Val1, Attr2: Val2", ...}
  • Simple products (no combinations) still returned with id_product

2. EntitySelectorRenderer — Config Propagation

  • Pass product_selection_level to JS config
  • Adapt chip display for combination items (show attribute string)

3. EntityPreviewHandler — Combination Previews

  • getCombinationPreviewData() already exists — extend for search-based previews
  • Show attribute values, per-combination stock/price if relevant

4. entity-selector.js — UI Adaptation

  • Chip display: show attribute string for combination items
  • Search result rendering: indent combinations under base product (mode 3)
  • Data serialization: encode combination IDs distinctly from product IDs

5. ProductConditionResolver — All Selection Methods

  • getIdsByMethod() must respect product_selection_level to return the right ID type
  • For by_category in combination mode: return all id_product_attribute for products in that category
  • For specific in combination mode: directly store id_product_attribute values

6. ID Encoding Strategy

Need a way to distinguish product IDs from combination IDs in the same value space:

  • Option A: Encode as strings: "p:1" (product), "c:42" (combination)
  • Option B: Encode as negative/positive: positive = id_product_attribute, negative = -id_product for simple products
  • Option C: Always store tuples: {id_product, id_product_attribute} — 0 means "entire product"

Relationship to by_combination / combination_mode

by_combination is a selection method (one of 40+ ways to filter products). It uses attribute group tiles to filter products by their combination attributes, controlled by combination_mode.

product_selection_level is a block-level config that changes how the entire products block works — how search results appear, what IDs are stored in chips, what previews show. It affects ALL selection methods, not just by_combination.

They are complementary:

  • product_selection_level = 'product' + by_combination = "Find products that have Red + XL combination" → returns id_product
  • product_selection_level = 'combination' + specific = "Search and pick individual combinations" → returns id_product_attribute
  • product_selection_level = 'combination' + by_category = "All combinations of products in Hoodies category" → returns id_product_attribute[]

Requirements

  • PrestaShop 1.7.x, 8.x, or 9.x
  • PHP 7.1+
  • Node.js 16+ (for building from source)

License

Proprietary — MyPrestaRocks