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>
This commit is contained in:
2026-02-20 21:33:14 +00:00
parent a7fbfa372c
commit 55e3135903
23 changed files with 2683 additions and 266 deletions

761
ENTITY-SELECTOR.md Normal file
View File

@@ -0,0 +1,761 @@
# 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
```php
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
```php
$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
```json
{
"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
```php
// 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
```json
{
"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)
```php
// 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
```php
$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:
```php
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
```php
[
'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.
```php
// 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:
```php
'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:
```javascript
// 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
```javascript
// 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)
```php
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
```bash
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.
```php
$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