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

762 lines
28 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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