- 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>
762 lines
28 KiB
Markdown
762 lines
28 KiB
Markdown
# 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
|