orangecat/module-product-lists

Saved product lists (order templates) for logged-in B2B customers in Magento 2

Maintainers

Package info

github.com/olivertar/m2_productlists

Type:magento2-module

pkg:composer/orangecat/module-product-lists

Statistics

Installs: 20

Dependents: 1

Suggesters: 0

Stars: 0

Open Issues: 0

0.0.2 2026-06-07 18:28 UTC

This package is auto-updated.

Last update: 2026-06-07 18:54:49 UTC


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

  1. Overview
  2. Theme Compatibility
  3. Requirements
  4. Installation
  5. What Gets Installed
  6. Configuration
  7. Store Admin Guide
  8. Buyer Guide
  9. Developer Guide
  10. REST API
  11. Frontend Routes Reference
  12. 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_item tables)

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 ProductListsProviderInterface via di.xml preference to change how lists are loaded (e.g., filter by company, add sharing across company users).
  • Extend item behavior: Override ProductListItemInterface preference 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\ProductList into any layout handle with Helper\Data::getListParams($product) to generate the required data-post attribute.

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_list rows, which in turn cascades to all product_list_item rows for those lists.
  • Deleting a product does not cascade to product_list_item (no FK enforced on product_id). Orphaned items pointing to deleted products are skipped gracefully at render and cart-add time.
  • The buy_request JSON 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 a LocalizedException.
  • No unique constraint exists on (list_id, product_id) — the application layer deduplicates by matching super_attribute values and incrementing qty instead of inserting.