Skip to content

Add "Category Listing (search engine)" source to the Products content type - #911

Open
lbajsarowicz wants to merge 7 commits into
magento:developfrom
lbajsarowicz:feature/products-category-listing
Open

lbajsarowicz wants to merge 7 commits into
magento:developfrom
lbajsarowicz:feature/products-category-listing

Conversation

@lbajsarowicz

Copy link
Copy Markdown
Contributor

Description (*)

The Products content type builds its collection through Magento\CatalogWidget\Block\Product\ProductsList: a MySQL product collection with rule conditions attached by the SQL condition builder. The category page builds its collection through Magento\Catalog\Model\Layer::getProductCollection(), whose collection provider and filter are replaced by CatalogSearch (and by third-party search modules such as ElasticSuite) with a search-engine-backed collection. The two paths return different product sets and orders: the category default sort is ignored, position sorting differs, and virtual categories, merchandising and search-engine boosts never apply. Merchants who place "products of category X" on a landing page get a list that does not match category X.

This adds a fourth Select Products By option to the Products content type: Category Listing (search engine). Existing options, templates, appearances and stored content are untouched.

How it works:

  • Admin: new condition_option value category_listing with a single-category picker. The widget directive mass converters emit {{widget type="Magento\PageBuilder\Block\Catalog\Product\CategoryListing" ... category_id="N" ...}} with no conditions_encoded, and read it back. The stage preview renders for this option, and the "N products" badge is counted through the same collection as the storefront.
  • Storefront: Block\Catalog\Product\CategoryListing extends ProductsList and delegates to Model\Catalog\CategoryListing\CollectionBuilder, which creates a dedicated Magento\Catalog\Model\Layer\Category, sets the category, takes getProductCollection() and calls addCategoryFilter($category). Every di override that the category page relies on applies. Sorting replicates ListProduct::prepareSortableFieldsByCategory() and Toolbar::setCollection(): the category default_sort_by, then catalog/frontend/default_sort_by, then the first available order; position through addAttributeToSort(). A Page Builder sort_order, when chosen, wins.
  • The builder dispatches pagebuilder_products_category_listing_prepare (category, layer, store_id, block) before touching the layer so a search module can seed request-scoped state (ElasticSuite's search context, see below).
  • The block is declared in etc/widget.xml because the widget directive filter renders only declared widgets. The Insert Widget form gets a category chooser (id_path); the Page Builder directive passes category_id.
  • Two plugins inherited from ProductsList are disabled for the new block: Page Builder's own sorting plugin (the block owns sorting) and ConfigurableProduct's widget plugin, which calls getAllIds() on the collection early and merges parent configurables of hidden children. Both break parity with the category page.
  • Cache key and identities include the category id and sort order.

Verified locally on OpenSearch (2.4-develop, developer mode) against a category with a disabled, an out-of-stock and a not-visible product plus custom positions: grid and carousel render the same ids in the same order as the category page for default (position) and name sorts; today's Catalog Products List widget differs on both.

Verified with ElasticSuite 2.11.x installed (modules copied into app/code): regular category, two virtual categories (rule-based, one spanning the whole catalog) and two widgets for different categories on one page all match their category pages exactly. The addCategoryFilter() call is what makes virtual categories resolve; ElasticSuite's collection provider ignores the category otherwise. ElasticSuite's request-scoped search context still latches the store root on a CMS page (its afterGetCurrentCategory plugin runs before the layer stores the category), which leaves category-scoped optimizers unapplied; product lists are unaffected. A three-line observer on the new event sets the context and fixes that; it belongs in ElasticSuite or a glue module, not here.

Tests: unit tests for the block, the collection builder and the totals branch; an integration test asserting the block's loaded items equal a ListProduct + Toolbar collection for the same category (default and name sort, missing category, cache key); Jasmine cases for both directive converters; an MFTF test creating a category with three products, selecting the new option and asserting the order on the stage and the storefront.

Fixed Issues (if relevant)

None filed. This is a feature request from agency practice: the Products content type cannot reproduce a category page listing.

Manual testing scenarios (*)

  1. Create a category with 6 simple products: one disabled, one out of stock, one "Not Visible Individually", and give the visible ones positions 5, 10, 20. Set the category default sort to Position. Reindex.
  2. Admin > Content > Pages > new page > Page Builder. Drag Products onto the stage. In the edit panel choose Select Products By: Category Listing (search engine), pick the category, set Number of Products to 8. The stage badge shows "3 products" and the preview shows them in position order. Save.
  3. Open the CMS page and the category page on the storefront. Both list the same three products in the same order (position 5, 10, 20). Repeat with the Carousel appearance.
  4. In the edit panel set Sort By to "Name: A - Z". Save. The CMS page order equals the category page with ?product_list_order=name.
  5. Change the category default sort to Product Name and clear the Page Builder Sort By. The CMS page follows the category's default sort.
  6. Pick a category that does not exist for the store view (or disable the category). The content type renders the empty state, no exceptions in var/log.
  7. Content > Widgets > Add Widget > type "Catalog Category Listing": choose a category with the chooser, save, assign to a page. Same products as the category page.
  8. Existing pages using "Category", "SKU" or "Condition" render exactly as before.
  9. Optional: with ElasticSuite, create a virtual category with a rule and point the content type at it. The CMS page lists the same products as the virtual category page.

Questions or comments

PHPCS reports pre-existing line-length warnings in etc/di.xml, pagebuilder_products_form.xml and Test/Mftf/Data/ProductsData.xml on develop; the new files are clean. The Jasmine and MFTF tests have not been executed locally (no runner in this repository); the unit and integration tests were run in a local 2.4-develop environment with OpenSearch.

Add a fourth "Select Products By" option, "Category Listing (search
engine)", with a single-category picker. The widget directive mass
converters emit a directive for
Magento\PageBuilder\Block\Catalog\Product\CategoryListing carrying the
category id instead of encoded conditions, and read it back into the
form. The stage preview renders for this option when a category is
selected.
…type

The Products content type builds its collection through
CatalogWidget\Block\Product\ProductsList: a MySQL collection with rule
conditions. The category page builds its collection through
Catalog\Model\Layer::getProductCollection(), whose provider and filter
are replaced by CatalogSearch. The two return different product sets and
orders, so "products of category X" on a landing page does not match
category X.

CategoryListing builds its collection through a dedicated
Catalog\Model\Layer\Category instance, so every di override
(CatalogSearch, third-party search modules) applies, and replicates the
sorting the category page applies through ListProduct and Toolbar: the
category default_sort_by, falling back to catalog/frontend/default_sort_by,
with position handled via addAttributeToSort(). A chosen Page Builder
sort_order still wins.

The collection building lives in a CollectionBuilder shared by the block
and by ProductTotals, so the stage badge cannot drift from the storefront.
The block dispatches pagebuilder_products_category_listing_prepare before
it touches the layer, so a search module can seed its request-scoped
context.

The block is declared in widget.xml because the widget directive filter
renders only declared widgets; the widget takes a category chooser
(id_path) while the Page Builder directive passes category_id.

Two plugins inherited from ProductsList are disabled for the block: Page
Builder's own sorting plugin (the block owns sorting) and the
ConfigurableProduct widget plugin, which loads the collection early and
merges parent products, both of which break parity with the category
page.
Extend the product-totals admin component so the "Category Listing
(search engine)" option refreshes the stage badge: it skips the
rule-condition encoder, posts conditionOption and categoryId directly,
and tracks its own previous-value signature instead of
conditions_encoded, so switching categories still refreshes the count.

Add an MFTF test that creates a category with three products, selects
the new option, and asserts the same product order on the stage and on
the storefront.
@lbajsarowicz

Copy link
Copy Markdown
Contributor Author

@magento run all tests

The old header form trips the Magento2Framework copyright sniff, and
lines over 120 characters fail the static code style test once the
files are part of a change set. Formatting only, no configuration or
behaviour change.
@lbajsarowicz

Copy link
Copy Markdown
Contributor Author

@magento run all tests

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant