Skip to content

Shopping list#2969

Draft
adriendupuis wants to merge 73 commits into5.0from
shopping-list
Draft

Shopping list#2969
adriendupuis wants to merge 73 commits into5.0from
shopping-list

Conversation

@adriendupuis
Copy link
Contributor

@adriendupuis adriendupuis commented Dec 3, 2025

Question Answer
JIRA Ticket IBX-5671
Versions 5.0
Edition Commerce

Document the Commerce's Shopping List LTS Update feature for developers.

Previews:

Related PRs:

  • REST API Ref in-code doc:
    • ibexa/shopping-list#25
    • ibexa/cart#158
  • "Add to shopping list" widget depends on this rework: ibexa/shopping-list#48

Not started:

  • JS API & something about visual customization

Checklist

  • Text renders correctly
  • Text has been checked with vale
  • Description metadata is up to date
  • Redirects cover removed/moved pages
  • Code samples are working
  • PHP code samples have been fixed with PHP CS fixer
  • Added link to this PR in relevant JIRA ticket or code PR

@github-actions
Copy link

github-actions bot commented Dec 3, 2025

Preview of modified files

Preview of modified Markdown:

Preview of addition to PHP API Reference:

month_change: true
---

# Shopping list APIs
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is also \Ibexa\Contracts\Cart\CartShoppingListTransferServiceInterface that could be mentioned here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a simple example.

import { ShoppingListsList } from '@ibexa-shopping-list/src/bundle/Resources/public/js/component/shopping.lists.list';

(function (global: Window, doc: Document) {
const shoppingList = new ShoppingList();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably we can mention that there should be exactly one instance of ShoppingList, which is accessible on window.ibexaShoppingList after init.
And that it is a wrapper for service functions shopping-list/src/bundle/Resources/public/js/service/shopping.list.ts.
After new shopping list data was loaded with it, it dispatches the ibexa-shopping-list:shopping-lists-data-changed event:

document.body.dispatchEvent(
    new CustomEvent<ShoppingListChangedDetail>('ibexa-shopping-list:shopping-lists-data-changed', {
        detail: { shoppingList: this },
    }),
);

@github-actions
Copy link

code_samples/ change report

Before (on target branch)After (in current PR)

code_samples/shopping_list/add_to_shopping_list/assets/js/add-to-shopping-list.ts


code_samples/shopping_list/add_to_shopping_list/assets/js/add-to-shopping-list.ts

docs/commerce/shopping_list/shopping_list_design.md@26:``` ts
docs/commerce/shopping_list/shopping_list_design.md@27:[[= include_file('code_samples/shopping_list/add_to_shopping_list/assets/js/add-to-shopping-list.ts') =]]
docs/commerce/shopping_list/shopping_list_design.md@28:```

001⫶// Shopping list service
002⫶import ShoppingList from '@ibexa-shopping-list/src/bundle/Resources/public/js/component/shopping.list';
003⫶// The Add to shopping list interaction
004⫶import { AddToShoppingList } from '@ibexa-shopping-list/src/bundle/Resources/public/js/component/add.to.shopping.list';
005⫶// List of all user's shopping lists
006⫶import { ShoppingListsList } from '@ibexa-shopping-list/src/bundle/Resources/public/js/component/shopping.lists.list';
007⫶
008⫶(function (global: Window, doc: Document) {
009⫶ const shoppingList = new ShoppingList();
010⫶ shoppingList.init(); // Fetch user's shopping lists
011⫶
012⫶ const addToShoppingListsNodes = doc.querySelectorAll<HTMLDivElement>('.ibexa-sl-add-to-shopping-list');
013⫶ addToShoppingListsNodes.forEach((addToShoppingListNode) => {
014⫶ const addToShoppingList = new AddToShoppingList({ node: addToShoppingListNode, ListClass: ShoppingListsList });
015⫶
016⫶ addToShoppingList.init();
017⫶ });
018⫶})(window, window.document);


code_samples/shopping_list/add_to_shopping_list/config/packages/views.yaml


code_samples/shopping_list/add_to_shopping_list/config/packages/views.yaml

docs/commerce/shopping_list/shopping_list_design.md@63:``` yaml hl_lines="7 8"
docs/commerce/shopping_list/shopping_list_design.md@64:[[= include_file('code_samples/shopping_list/add_to_shopping_list/config/packages/views.yaml') =]]
docs/commerce/shopping_list/shopping_list_design.md@65:```

001⫶ibexa:
002⫶ system:
003⫶ default:
004⫶ content_view:
005⫶ full:
006⫶ product:
007❇️ controller: 'App\Controller\ProductViewController::viewAction'
008❇️ template: '@ibexadesign/full/product.html.twig'
009⫶ match:
010⫶ '@Ibexa\Contracts\ProductCatalog\ViewMatcher\ProductBased\IsProduct': true


code_samples/shopping_list/add_to_shopping_list/src/Controller/ProductViewController.php


code_samples/shopping_list/add_to_shopping_list/src/Controller/ProductViewController.php

docs/commerce/shopping_list/shopping_list_design.md@58:``` php hl_lines="24-30"
docs/commerce/shopping_list/shopping_list_design.md@59:[[= include_file('code_samples/shopping_list/add_to_shopping_list/src/Controller/ProductViewController.php') =]]
docs/commerce/shopping_list/shopping_list_design.md@60:```

001⫶<?php declare(strict_types=1);
002⫶
003⫶namespace App\Controller;
004⫶
005⫶use Ibexa\Contracts\Core\Repository\Iterator\BatchIterator;
006⫶use Ibexa\Contracts\ProductCatalog\Iterator\BatchIteratorAdapter\ProductVariantFetchAdapter;
007⫶use Ibexa\Contracts\ProductCatalog\Local\LocalProductServiceInterface;
008⫶use Ibexa\Contracts\ProductCatalog\Values\Product\ProductVariantQuery;
009⫶use Ibexa\Core\MVC\Symfony\View\ContentView;
010⫶use Ibexa\Core\MVC\Symfony\View\View;
011⫶use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
012⫶use Symfony\Component\HttpFoundation\Request;
013⫶
014⫶class ProductViewController extends AbstractController
015⫶{
016⫶ public function __construct(private LocalProductServiceInterface $productService)
017⫶ {
018⫶ }
019⫶
020⫶ public function viewAction(Request $request, ContentView $view): View
021⫶ {
022⫶ $product = $this->productService->getProductFromContent($view->getContent());
023⫶ if ($product->isBaseProduct()) {
024❇️ $view->addParameters([
025❇️ 'variants' => new BatchIterator(new ProductVariantFetchAdapter(
026❇️ $this->productService,
027❇️ $product,
028❇️ new ProductVariantQuery(),
029❇️ )),
030❇️ ]);
031⫶ }
032⫶
033⫶ return $view;
034⫶ }
035⫶}


code_samples/shopping_list/add_to_shopping_list/templates/themes/standard/full/product.html.twig


code_samples/shopping_list/add_to_shopping_list/templates/themes/standard/full/product.html.twig

docs/commerce/shopping_list/shopping_list_design.md@68:``` twig hl_lines="7 8 16-18 31-33 44"
docs/commerce/shopping_list/shopping_list_design.md@69:[[= include_file('code_samples/shopping_list/add_to_shopping_list/templates/themes/standard/full/product.html.twig') =]]
docs/commerce/shopping_list/shopping_list_design.md@70:```

001⫶{% extends '@ibexadesign/pagelayout.html.twig' %}
002⫶
003⫶{% set product = content|ibexa_get_product %}
004⫶
005⫶{% block meta %}
006⫶ {% set token = csrf_token ?? csrf_token(ibexa_get_rest_csrf_token_intention()) %}
007❇️ <meta name="CSRF-Token" content="{{ token }}"/>
008❇️ <meta name="SiteAccess" content="{{ app.request.get('siteaccess').name }}"/>
009⫶{% endblock %}
010⫶
011⫶{% block content %}
012⫶ {{ ibexa_content_name(content) }}
013⫶ {{ product.code }}
014⫶ {% if not product.isBaseProduct() and can_view_shopping_list and can_edit_shopping_list %}
015⫶ {% set component %}
016❇️ {% include '@ibexadesign/shopping_list/component/add_to_shopping_list/add_to_shopping_list.html.twig' with {
017❇️ product_code: product.code,
018❇️ } %}
019⫶ {% endset %}
020⫶ {{ _self.add_to_shopping_list(product, component) }}
021⫶ {% endif %}
022⫶
023⫶ {% if product.isBaseProduct() %}
024⫶ <ul>
025⫶ {% for variant in variants %}
026⫶ <li>
027⫶ {{ variant.name }}
028⫶ {{ variant.code }}
029⫶ {% if can_view_shopping_list and can_edit_shopping_list %}
030⫶ {% set component %}
031❇️ {% include '@ibexadesign/shopping_list/component/add_to_shopping_list/add_to_shopping_list.html.twig' with {
032❇️ product_code: variant.code,
033❇️ } %}
034⫶ {% endset %}
035⫶ {{ _self.add_to_shopping_list(variant, component) }}
036⫶ {% endif %}
037⫶ </li>
038⫶ {% endfor %}
039⫶ </ul>
040⫶ {% endif %}
041⫶{% endblock %}
042⫶
043⫶{% block javascripts %}
044❇️ {{ encore_entry_script_tags('add-to-shopping-list-js') }}
045⫶{% endblock %}
046⫶
047⫶{% macro add_to_shopping_list(product, component) %}
048⫶ {% set widget_id = 'add-to-shopping-list-' ~ product.code|slug %}
049⫶ <button
050⫶ onclick="(function(){let e=document.getElementById('{{ widget_id }}'); e.style.display=('none'===window.getComputedStyle(e).display)?'block':'none';})()">
051⫶ Add to shopping list
052⫶ </button>
053⫶ <div id="{{ widget_id }}" style="display: none;">
054⫶ {{ component }}
055⫶ </div>
056⫶{% endmacro %}


code_samples/shopping_list/add_to_shopping_list/webpack.config.js


code_samples/shopping_list/add_to_shopping_list/webpack.config.js

docs/commerce/shopping_list/shopping_list_design.md@31:``` js hl_lines="3-11"
docs/commerce/shopping_list/shopping_list_design.md@32:[[= include_file('code_samples/shopping_list/add_to_shopping_list/webpack.config.js', 43) =]]
docs/commerce/shopping_list/shopping_list_design.md@33:```




code_samples/shopping_list/install/src/Migrations/Ibexa/migrations/shopping_list_user.yaml


code_samples/shopping_list/install/src/Migrations/Ibexa/migrations/shopping_list_user.yaml

docs/commerce/shopping_list/install_shopping_list.md@85:``` yaml
docs/commerce/shopping_list/install_shopping_list.md@86:[[= include_file('code_samples/shopping_list/install/src/Migrations/Ibexa/migrations/shopping_list_user.yaml', 4, 29) =]]
docs/commerce/shopping_list/install_shopping_list.md@87:```

001⫶- type: role
002⫶ mode: create
003⫶ metadata:
004⫶ identifier: Shopping List User
005⫶ policies:
006⫶ - module: shopping_list
007⫶ function: create
008⫶ limitations:
009⫶ - identifier: ShoppingListOwner
010⫶ values: [self]
011⫶ - module: shopping_list
012⫶ function: view
013⫶ limitations:
014⫶ - identifier: ShoppingListOwner
015⫶ values: [self]
016⫶ - module: shopping_list
017⫶ function: edit
018⫶ limitations:
019⫶ - identifier: ShoppingListOwner
020⫶ values: [self]
021⫶ - module: shopping_list
022⫶ function: delete
023⫶ limitations:
024⫶ - identifier: ShoppingListOwner
025⫶ values: [self]


code_samples/shopping_list/php_api/src/Command/ShoppingListMoveCommand.php


code_samples/shopping_list/php_api/src/Command/ShoppingListMoveCommand.php

docs/commerce/shopping_list/shopping_list_api.md@87:```php
docs/commerce/shopping_list/shopping_list_api.md@88:[[= include_file('code_samples/shopping_list/php_api/src/Command/ShoppingListMoveCommand.php', 42, 56) =]]
docs/commerce/shopping_list/shopping_list_api.md@89:```

001⫶ $entriesToMove = [];
002⫶ $entriesToRemove = [];
003⫶ foreach ($movedProductCodes as $productCode) {
004⫶ if ($sourceList->getEntries()->hasEntryWithProductCode($productCode)) {
005⫶ if ($targetList->getEntries()->hasEntryWithProductCode($productCode)) {
006⫶ $entriesToRemove[] = $sourceList->getEntries()->getEntryWithProductCode($productCode);
007⫶ } else {
008⫶ $entriesToMove[] = $sourceList->getEntries()->getEntryWithProductCode($productCode);
009⫶ }
010⫶ }
011⫶ }
012⫶ $this->shoppingListService->moveEntries($targetList, $entriesToMove);
013⫶ $targetList = $this->shoppingListService->getShoppingList($targetList->getIdentifier()); // Refresh local object from persistence
014⫶ $sourceList = $this->shoppingListService->removeEntries($sourceList, $entriesToRemove); // Refresh local object from persistence even if $entriesToRemove is empty


code_samples/shopping_list/php_api/src/Controller/CartShoppingListTransferController.php


code_samples/shopping_list/php_api/src/Controller/CartShoppingListTransferController.php

docs/commerce/shopping_list/shopping_list_api.md@100:```php
docs/commerce/shopping_list/shopping_list_api.md@101:[[= include_file('code_samples/shopping_list/php_api/src/Controller/CartShoppingListTransferController.php', 69, 90) =]]
docs/commerce/shopping_list/shopping_list_api.md@102:```

001⫶ $this->cartService->emptyCart($cart);
002⫶ $list = $this->shoppingListService->clearShoppingList($list);
003⫶
004⫶ $list = $this->shoppingListService->addEntries($list, [new ShoppingListEntryAddStruct($productCode)]);
005⫶
006⫶ $cart = $this->cartShoppingListTransferService->addSelectedEntriesToCart($list, [$list->getEntries()->getEntryWithProductCode($productCode)->getIdentifier()], $cart);
007⫶
008⫶ dump(
009⫶ $list->getEntries()->hasEntryWithProductCode($productCode), // true as the entry is copied and not moved
010⫶ $cart->getEntries()->hasEntryForProduct($this->productService->getProduct($productCode)) // true
011⫶ );
012⫶
013⫶ $list = $this->shoppingListService->clearShoppingList($list); // Empty the list to avoid duplicate and test the move from cart
014⫶
015⫶ $list = $this->cartShoppingListTransferService->moveCartToShoppingList($cart, $list);
016⫶ $cart = $this->cartService->getCart($cart->getIdentifier()); // Refresh local object from persistence
017⫶
018⫶ dump(
019⫶ $list->getEntries()->hasEntryWithProductCode($productCode), // true as, after the clear, the entry is moved from cart
020⫶ $cart->getEntries()->hasEntryForProduct($this->productService->getProduct($productCode)) // false as the entry was moved
021⫶ );

Download colorized diff

@sonarqubecloud
Copy link

Quality Gate Failed Quality Gate failed

Failed conditions
132 Security Hotspots
52.9% Duplication on New Code (required ≤ 3%)
E Reliability Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants