orangecat / module-product-lists
Saved product lists (order templates) for logged-in B2B customers in Magento 2
Package info
github.com/olivertar/m2_productlists
Type:magento2-module
pkg:composer/orangecat/module-product-lists
Requires
- php: >=8.1
- magento/framework: *
- orangecat/core: *
README
Saved product lists (order templates) for logged-in B2B customers — create, manage, and reorder from named lists.
Module: Orangecat_ProductLists
Version: 1.0.0
License: OSL-3.0
Author: Oliverio Gombert olivertar@gmail.com
Table of Contents
- Overview
- Theme Compatibility
- Requirements
- Installation
- What Gets Installed
- Configuration
- Store Admin Guide
- Buyer Guide
- Developer Guide
- REST API
- Frontend Routes Reference
- DevOps & Integrator Notes
Overview
Orangecat_ProductLists lets logged-in customers maintain multiple named product lists — essentially saved order templates. Each list holds products with their configured options (color, size, etc.) and quantities, which can be added to cart in one click.
The module handles:
- Creating, renaming, and deleting multiple product lists per customer
- Adding products (simple and configurable) to a list from the PLP, PDP, related products, and upsell blocks
- Updating item quantities within a list
- Adding all or selected items from a list to the shopping cart
- Exporting a list to CSV (SKU, price, qty, name, options, URL)
- Paginated "My Lists" dashboard and per-list detail page in the customer account
- AJAX-based list selector modal when multiple lists exist
- Optional AJAX mode for silent add-to-list without page reload
Position in the Orangecat B2B Dependency Chain
Orangecat_Core (via composer: orangecat/core)
└── Orangecat_ProductLists ← this module (standalone — no Company dependency)
Orangecat_ProductLists is independent of Orangecat_Company. It works for any logged-in customer, whether they belong to a company or not.
The buy_request Field
Each product_list_item row stores a buy_request JSON blob — the same data structure Magento uses for cart line items. This preserves the full configurable option selection (e.g. {"super_attribute":{"93":52,"142":167},"qty":2}). ProductListItem::getBuyRequestObject() deserializes this blob into a DataObject ready to pass to Cart::addProduct(), overriding the stored qty with the item's dedicated qty column.
Theme Compatibility
| Theme | Status | Notes |
|---|---|---|
| Luma | Supported | Standard .phtml templates. add-to-list button injected into PDP (product.info.addto), PLP (category.product.addto), related, and upsell blocks. List selector is a jQuery modal (addto.phtml + productlists_addto.js). Less CSS from web/css/source/_module.less. |
| Breeze Evolution | Supported | Same templates and JS as Luma — Breeze bundles them via breeze_default.xml. Additional Less from web/css/breeze/_default.less. |
| Hyvä | Supported | Alpine.js templates (-hyva.phtml). hyva_* layout handles active. hyva_default.xml removes the Luma modal block. PLP uses a dedicated Hyvä modal component (list-modal-hyva.phtml) registered as a JS block dependency. CSS from web/css/hyva/module.css and view/frontend/tailwind/tailwind-source.css. |
Hyvä templates follow the view/frontend/templates/ convention (not view/hyva/).
Requirements
| Dependency | Version / Notes |
|---|---|
| Magento 2.4.x | Tested on 2.4.8-p5 |
| PHP | >= 8.1 |
orangecat/core |
Composer dependency |
Magento_Customer |
Customer session |
Magento_Catalog |
Product repository |
Magento_Checkout |
Cart model |
Magento_ConfigurableProduct |
Configurable option resolution |
Installation
Via Git Submodule (recommended for this project)
# From repo root
git submodule add git@github.com:olivertar/m2_productlists.git app/code/Orangecat/ProductLists
git submodule update --init --recursive
Enable the Module
Run inside the PHP container (reward shell):
bin/magento module:enable Orangecat_ProductLists bin/magento setup:upgrade bin/magento setup:di:compile bin/magento setup:static-content:deploy -f bin/magento cache:flush
What Gets Installed
Database Tables
product_list
| Column | Type | Notes |
|---|---|---|
list_id |
int unsigned |
Primary key, auto-increment |
customer_id |
int unsigned |
FK → customer_entity.entity_id (CASCADE DELETE) |
list_name |
varchar(255) |
Default: "Main" |
description |
text |
Optional list description, nullable |
created_at |
timestamp |
Set on insert, not updated |
Index on customer_id. Deleting a customer cascades to all their lists.
product_list_item
| Column | Type | Notes |
|---|---|---|
item_id |
int unsigned |
Primary key, auto-increment |
list_id |
int unsigned |
FK → product_list.list_id (CASCADE DELETE) |
product_id |
int unsigned |
Catalog product entity ID |
qty |
decimal(12,4) |
Default: 1 |
buy_request |
text |
JSON blob with configurable options and qty |
added_at |
timestamp |
Set on insert, not updated |
Indexes on list_id and product_id. Deleting a list cascades to all its items.
EAV Attributes
None.
Data Patches
None. No default records, roles, or CMS pages are created.
Configuration
Path: Stores > Configuration > Orangecat > Product List
General Settings
| Label | Config path | Default | Description |
|---|---|---|---|
| Enable Product List | productlists/general/enable_productlists |
No | Master switch. When disabled, the "My Lists" nav link is hidden and all frontend routes become inaccessible. |
| Enable Ajax Product List | productlists/general/enable_ajaxwishlist |
No | When enabled, clicking "Add to List" silently posts via AJAX without page reload. |
productlists/general/enable_productlists
productlists/general/enable_ajaxwishlist
Both settings are configurable at Default, Website, and Store view scope.
Store Admin Guide
There is no Admin UI for managing individual product lists or customer list items. Lists are owned and managed entirely by customers via the frontend.
Admin access is limited to:
- Enabling/disabling the feature:
Stores > Configuration > Orangecat > Product List - Viewing data directly: via database (
product_list,product_list_itemtables)
To grant a role access to the configuration section, assign the ACL resource Orangecat_ProductLists::config (visible under Stores > Settings > Configuration).
Buyer Guide
Accessing My Lists
After logging in, navigate to Account Dashboard > My Lists or go directly to:
https://your-store.test/product_lists/index/index
The "My Lists" link appears in the customer account sidebar when Enable Product List is set to Yes.
Creating a List
From the My Lists page, click Add List. Enter a name and optional description in the modal dialog, then save.
Adding Products to a List
From a product listing page (PLP) or product detail page (PDP):
Click the Add to List button. If multiple lists exist, a modal appears to select the target list or create a new one inline. For configurable products, the required options (size, color, etc.) must be selected first — an error is shown if they are not.
If only one list exists and AJAX mode is enabled, the product is added silently.
Duplicate handling: Adding the same product with the same option selection to a list increments the existing item's quantity rather than creating a duplicate row.
Viewing a List
Click any list name from My Lists to open its detail page:
https://your-store.test/product_lists/index/view?list_id=N
The detail page shows product thumbnails, names, configured options, unit prices, and a quantity field per item. A search box filters items by product name within the list.
Updating Quantities
Edit the quantity field for any item on the list detail page and click Update to save all changes at once.
Removing Items
Click Remove next to any item to delete it from the list immediately.
Adding to Cart
From the list detail page:
- Add All to Cart — adds every item to the cart, preserving configured options and quantities. Products remain in the list.
- Add Selected to Cart — check the items to add, optionally override qty, then click Add Selected to Cart.
After a successful add, the browser redirects to the cart.
Exporting a List
From the list detail page, click Export to download a CSV file with columns: SKU, Price, Qty, Name, Options, URL.
Deleting a List
From My Lists, click Delete next to a list. A confirmation dialog is shown. Deleting a list permanently removes it and all its items.
Developer Guide
Module Structure
Orangecat/ProductLists/
├── Api/
│ ├── ConfigInterface.php isEnabled(), isAjaxEnabled()
│ ├── ProductListsProviderInterface.php getLists(), getListCollection(), getItemsForList()
│ └── Data/
│ ├── ProductListInterface.php List data contract
│ └── ProductListItemInterface.php Item data contract (incl. getBuyRequestObject)
├── Block/
│ ├── ListAddto.php Modal/AJAX config block for Luma/Breeze
│ ├── ProductListsIndex.php My Lists dashboard (paginated)
│ ├── ProductListsView.php List detail (items, search, pagination)
│ └── Catalog/Product/
│ ├── ProductList/Item/AddTo/ProductList.php PLP add-to button block
│ └── View/AddTo/ProductList.php PDP add-to button block
├── Controller/
│ ├── Index/
│ │ ├── Index.php GET — My Lists dashboard
│ │ ├── View.php GET — List detail (validates ownership)
│ │ └── Export.php GET — CSV download (validates ownership)
│ ├── Item/
│ │ ├── Add.php POST — Add product to list (AJAX or redirect)
│ │ ├── Remove.php POST — Remove item from list (AJAX or redirect)
│ │ ├── Update.php POST — Bulk update item quantities
│ │ ├── AddAll.php POST — Add all list items to cart
│ │ └── AddSelected.php POST — Add selected items to cart
│ └── Productlists/
│ ├── Get.php AJAX GET — Return customer's lists as JSON
│ ├── Update.php AJAX POST — Create or rename a list
│ └── Delete.php AJAX POST — Delete a list
├── Helper/Data.php getItemProduct(), getItemImageProduct(), getConfiguredOptions(), getListParams()
├── Model/
│ ├── Config.php
│ ├── ProductList.php
│ ├── ProductListItem.php getBuyRequestObject() merges JSON + qty column
│ ├── ProductListsProvider.php Session-aware; getLists() result is cached per request
│ └── ResourceModel/ Standard AbstractResource + Collection pairs
├── Plugin/WishlistAddAction.php around Magento\ProductLists\Controller\Index\Add
└── view/frontend/
├── layout/ Luma, Hyvä (hyva_*), and Breeze (breeze_*) handles
├── templates/ Luma/Breeze .phtml + Hyvä -hyva.phtml variants
└── web/
├── js/productlists.js My Lists CRUD UI (jQuery modal, delete confirm)
├── js/productlists_addto.js Add-to-List button behavior (modal, AJAX, configurable validation)
├── css/source/_module.less Luma Less
├── css/breeze/_default.less Breeze Less
└── css/hyva/module.css Hyvä CSS
Key Interfaces
ConfigInterface
isEnabled(int|null $storeId = null): bool isAjaxEnabled(int|null $storeId = null): bool
ProductListsProviderInterface
getLists(): array // All lists for logged-in customer (cached) getListCollection(): ?ListCollection // Raw paginatable collection getItemsForList(int $listId): ItemCollection
ProductListInterface
getListId(): int|null getCustomerId(): int|null setCustomerId(int $customerId): $this getListName(): string|null setListName(string $name): $this getDescription(): string|null setDescription(string|null $description): $this getCreatedAt(): string|null
ProductListItemInterface
getItemId(): int|null getListId(): int|null setListId(int $listId): $this getProductId(): int|null setProductId(int $productId): $this getQty(): float|null setQty(float $qty): $this getBuyRequest(): string|null // Raw JSON setBuyRequest(string|null $buyRequest): $this getBuyRequestObject(): DataObject // Deserialized; qty column takes precedence over JSON qty getAddedAt(): string|null
Observers
This module registers no observers. There is no etc/events.xml.
Plugins
| Class | Target | Hook | Purpose |
|---|---|---|---|
Plugin\WishlistAddAction |
Magento\ProductLists\Controller\Index\Add |
around |
Validates configurable options are selected before add; returns AJAX-friendly JSON when ajax=1. Note: the target class name Magento\ProductLists is non-standard — verify it resolves correctly in your Magento installation. |
JS Components
Frontend
| File | Purpose |
|---|---|
js/productlists.js |
My Lists dashboard — creates/edits lists via AJAX modal, deletes with confirmation dialog, DOM-only row removal on delete |
js/productlists_addto.js |
Add-to-List button behavior — list selector modal, new-list creation inline, configurable option validation (select dropdowns and swatch options), AJAX add mode |
Both components use RequireJS AMD format and work on Luma and Breeze. Hyvä uses Alpine.js templates instead.
Admin JS
None.
Email Templates
This module sends no transactional emails. There are no email template files.
ACL Resources
| Resource ID | Title | Location |
|---|---|---|
Orangecat_ProductLists::config |
Multi Wishlist | Stores > Settings > Configuration |
Adding Custom Logic
- Extend the provider: Implement or decorate
ProductListsProviderInterfaceviadi.xmlpreference to change how lists are loaded (e.g., filter by company, add sharing across company users). - Extend item behavior: Override
ProductListItemInterfacepreference to add custom buy-request transformations before items are added to cart. - Add new add-to-list placements: Inject a block extending
Orangecat\ProductLists\Block\Catalog\Product\ProductList\Item\AddTo\ProductListinto any layout handle withHelper\Data::getListParams($product)to generate the requireddata-postattribute.
REST API
This module exposes no REST API endpoints. There is no etc/webapi.xml.
Frontend Routes Reference
Route ID: product_lists (front name: product_lists)
| Route | Controller | Method | Access |
|---|---|---|---|
/product_lists/index/index |
Controller\Index\Index |
GET | Logged-in customers only |
/product_lists/index/view?list_id=N |
Controller\Index\View |
GET | Logged-in owner of the list |
/product_lists/index/export?list_id=N |
Controller\Index\Export |
GET | Logged-in owner of the list |
/product_lists/item/add |
Controller\Item\Add |
POST | Logged-in customers only |
/product_lists/item/remove |
Controller\Item\Remove |
POST | Logged-in owner of the item |
/product_lists/item/update |
Controller\Item\Update |
POST | Logged-in owner of the list |
/product_lists/item/addall |
Controller\Item\AddAll |
POST | Logged-in owner of the list |
/product_lists/item/addselected |
Controller\Item\AddSelected |
POST | Logged-in owner of the list |
/product_lists/productlists/get |
Controller\Productlists\Get |
GET (AJAX only) | Logged-in customers only |
/product_lists/productlists/update |
Controller\Productlists\Update |
GET/POST (AJAX only) | Logged-in customers only |
/product_lists/productlists/delete |
Controller\Productlists\Delete |
POST (AJAX only) | Logged-in owner of the list |
All ownership checks are enforced by filtering collections on both customer_id and the requested entity ID — no separate authorization layer is needed.
DevOps & Integrator Notes
Deployment Checklist
# After deploying or updating this module: bin/magento module:enable Orangecat_ProductLists bin/magento setup:upgrade # creates product_list and product_list_item tables bin/magento setup:di:compile # regenerates interceptors for the Plugin bin/magento setup:static-content:deploy -f bin/magento cache:flush # Enable the feature in config: bin/magento config:set productlists/general/enable_productlists 1 bin/magento cache:flush
Integration Token Scope
This module has no REST API. No integration token permissions are required to consume it. Direct database access to product_list / product_list_item requires no ACL resource.
For Admin panel config access, grant Orangecat_ProductLists::config.
Disabling Without Uninstalling
bin/magento module:disable Orangecat_ProductLists bin/magento setup:upgrade bin/magento cache:flush
Disabling hides the "My Lists" nav link (ifconfig guard on the layout block) and stops all frontend routes from resolving, but leaves the database tables and data intact.
Data Integrity Notes
- Deleting a customer triggers a cascade delete of all their
product_listrows, which in turn cascades to allproduct_list_itemrows for those lists. - Deleting a product does not cascade to
product_list_item(no FK enforced onproduct_id). Orphaned items pointing to deleted products are skipped gracefully at render and cart-add time. - The
buy_requestJSON blob may become stale if a configurable product's attribute options change after an item was saved. The cart-add action will reject an invalid option combination with aLocalizedException. - No unique constraint exists on
(list_id, product_id)— the application layer deduplicates by matchingsuper_attributevalues and incrementing qty instead of inserting.