19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-09 09:32:12 +01:00
parent 79f83631d5
commit 73afc09215
6267 changed files with 1534193 additions and 1130106 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Before After
Before After

View file

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="70" height="70" viewBox="0 0 70 70"><defs><path id="a" d="M4 0h61c4 0 5 1 5 5v60c0 4-1 5-5 5H4c-3 0-4-1-4-5V5c0-4 1-5 4-5z"/><linearGradient id="c" x1="100%" x2="0%" y1="0%" y2="100%"><stop offset="0%" stop-color="#DA956B"/><stop offset="100%" stop-color="#CC7039"/></linearGradient><path id="d" d="M23 27.281L27 43l1 4h19c1 0 1 2 0 2H26l-6-24h-2v1c0 .667-.333 1-1 1s-1-.333-1-1v-2c.066-.667.4-1 1-1h4c.517 0 .85.333 1 1l1 3.281zM45.5 55a2.5 2.5 0 1 1 0-5 2.5 2.5 0 0 1 0 5zm-19 0a2.5 2.5 0 1 1 0-5 2.5 2.5 0 0 1 0 5zM50 37.299v2.449c0 .11-.038.206-.115.287a.363.363 0 0 1-.272.121H32.977v2.45c0 .11-.038.206-.114.287a.363.363 0 0 1-.272.121.442.442 0 0 1-.29-.128l-3.857-4.082a.393.393 0 0 1-.11-.28.41.41 0 0 1 .11-.294l3.868-4.083a.366.366 0 0 1 .279-.114c.104 0 .195.04.272.12.076.082.114.177.114.288v2.45h16.636c.105 0 .196.04.272.12.077.081.115.177.115.288zm0-7.199a.41.41 0 0 1-.109.294l-3.869 4.082a.366.366 0 0 1-.278.115.363.363 0 0 1-.272-.121.403.403 0 0 1-.115-.287v-2.45H28.722a.363.363 0 0 1-.272-.12.403.403 0 0 1-.115-.288v-2.45c0-.11.038-.206.115-.286a.363.363 0 0 1 .272-.122h16.635v-2.449a.41.41 0 0 1 .11-.293.366.366 0 0 1 .277-.115c.097 0 .194.042.29.127l3.857 4.07A.41.41 0 0 1 50 30.1z"/><path id="e" d="M23 25.281L27 41l1 3h19c1 0 1 3 0 3H26l-6-24h-2v1c0 .667-.333 1-1 1s-1-.333-1-1v-2c.066-.667.4-1 1-1h4c.517 0 .85.333 1 1l1 3.281zM45.5 53a2.5 2.5 0 1 1 0-5 2.5 2.5 0 0 1 0 5zm-19 0a2.5 2.5 0 1 1 0-5 2.5 2.5 0 0 1 0 5zM50 34.689v2.45c0 .11-.038.205-.115.286a.363.363 0 0 1-.272.121H32.977v2.45c0 .11-.038.206-.114.287a.363.363 0 0 1-.272.121.442.442 0 0 1-.29-.127l-3.857-4.083a.393.393 0 0 1-.11-.28.41.41 0 0 1 .11-.294l3.868-4.082a.366.366 0 0 1 .279-.115c.104 0 .195.04.272.121.076.08.114.176.114.287v2.45h16.636c.105 0 .196.04.272.12.077.082.115.177.115.288zm0-7.198a.41.41 0 0 1-.109.293l-3.869 4.083a.366.366 0 0 1-.278.114.363.363 0 0 1-.272-.12.403.403 0 0 1-.115-.288v-2.45H28.722a.363.363 0 0 1-.272-.12.403.403 0 0 1-.115-.288v-2.449c0-.11.038-.206.115-.287a.363.363 0 0 1 .272-.121h16.635v-2.45a.41.41 0 0 1 .11-.293.366.366 0 0 1 .277-.115c.097 0 .194.043.29.128l3.857 4.07a.41.41 0 0 1 .109.293z"/></defs><g fill="none" fill-rule="evenodd"><mask id="b" fill="#fff"><use xlink:href="#a"/></mask><g mask="url(#b)"><path fill="url(#c)" d="M0 0H70V70H0z"/><path fill="#FFF" fill-opacity=".383" d="M4 1h61c2.667 0 4.333.667 5 2V0H0v3c.667-1.333 2-2 4-2z"/><path fill="#393939" d="M32.073 69H4c-2 0-4-1-4-4V38.16l16.339-16.914L21 21l3.218 10.05 4.433-4.928h14.216l2.738-2.955 4.233 4.435-5.877 6.823c3.956-.137 5.915-.137 5.877 0-.013.046-.013 1.01 0 2.894L43.961 45H47l.709.999-3.337 3.855 2.886 2.366L32.073 69z" opacity=".324"/><path fill="#000" fill-opacity=".383" d="M4 69h61c2.667 0 4.333-1 5-3v4H0v-4c.667 2 2 3 4 3z"/><use fill="#000" fill-rule="nonzero" opacity=".3" xlink:href="#d"/><use fill="#FFF" fill-rule="nonzero" xlink:href="#e"/></g></g></svg>
<svg width="50" height="50" viewBox="0 0 50 50" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M15.724 6.397C16.377 4.94 17.852 4 19.481 4h11.037c1.63 0 3.104.94 3.757 2.397L37.236 13H41.9c2.46 0 4.367 2.099 4.07 4.481l-3.106 25C42.613 44.49 40.866 46 38.793 46H11.207c-2.074 0-3.82-1.51-4.07-3.519l-3.107-25C3.734 15.1 5.64 13 8.1 13h4.663l2.961-6.603ZM32.917 13H17.082c0-.56.123-1.134.39-1.691l.956-2C19.102 7.9 20.551 7 22.144 7h5.711c1.593 0 3.042.9 3.716 2.308l.957 2c.266.558.39 1.132.39 1.692Z" fill="#F78613"/><path fill-rule="evenodd" clip-rule="evenodd" d="M8.514 45.016a3.963 3.963 0 0 1-1.377-2.535l-3.107-25C3.734 15.1 5.64 13 8.1 13h4.663l2.961-6.603C16.377 4.94 17.852 4 19.481 4h11.037c1.63 0 3.104.94 3.757 2.397l2.59 5.777C35.5 28.256 23.848 41.405 8.515 45.016ZM17.082 13h15.835c0-.56-.123-1.134-.39-1.691l-.956-2C30.897 7.9 29.448 7 27.855 7h-5.711c-1.593 0-3.042.9-3.716 2.308l-.956 2a3.904 3.904 0 0 0-.39 1.692Z" fill="#FBB945"/><path d="m14.691 24.973 11.976-6.914L25.4 22.78l7.942 2.128a4 4 0 0 1 2.829 4.899l-.23.858-21.25-5.694Zm20.619 8.055-11.976 6.914 1.265-4.722-7.942-2.128a4 4 0 0 1-2.828-4.9l.23-.858 21.25 5.694Z" fill="#fff"/></svg>

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View file

@ -0,0 +1,169 @@
import { Interaction } from '@web/public/interaction';
import { registry } from '@web/core/registry';
import comparisonUtils from '@website_sale_comparison/js/website_sale_comparison_utils';
import { redirect } from '@web/core/utils/urls';
export class ComparisonPage extends Interaction {
static selector = '#o_comparelist_table';
dynamicSelectors = {
...this.dynamicSelectors,
_miniSticky: () => document.querySelector('#miniStickyComparison'),
_mainScroll: () => document.querySelector('.table-comparator')?.closest('.overflow-x-auto'),
_miniScroll: () => document.querySelector('#miniStickyComparison .overflow-x-auto'),
_backButton: () => document.querySelector('button[name="comparison_back_button"]'),
_clearAllButton: () => document.querySelector('button[name="comparison_clear_all_button"]'),
};
dynamicContent = {
'button[name="comparison_add_to_cart"]': { 't-on-click': this.addToCart },
'.o_comparelist_remove': { 't-on-click': this.removeProduct },
_backButton: { 't-on-click': () => redirect('/shop') },
_clearAllButton: { 't-on-click': this.clearAllProducts },
};
// TODO the sticky logic could probably make use of the WebsiteSaleStickyObject
// interaction. We'd simply need to remove the offset that comes with the interaction
// and handle the fact that the sticky element is hidden and appears when the user scrolls.
setup() {
this.position = 0;
}
start() {
this._adaptToHeaderChange();
this.registerCleanup(this.services.website_menus.registerCallback(this._adaptToHeaderChange.bind(this)));
this._initMiniStickyComparison();
}
/**
* Adapt the position of elements when the header changes.
*
* @private
*/
_adaptToHeaderChange() {
let position = 0;
// Calculate total height of fixed elements at top
for (const el of this.el.ownerDocument.querySelectorAll(".o_top_fixed_element")) {
position += el.offsetHeight;
}
if (this.position !== position) {
this.position = position;
this.updateContent();
// Update mini sticky position if it exists
const miniStickyEl = this.dynamicSelectors._miniSticky();
if (miniStickyEl) {
miniStickyEl.style.top = `${position}px`;
}
}
}
/**
* Clear all products from the comparison.
*/
clearAllProducts() {
comparisonUtils.clearComparisonProducts(this.bus);
redirect('/shop');
}
/**
* Initialize the mini sticky comparison overview.
*
* @private
*/
_initMiniStickyComparison() {
const miniStickyEl = this.dynamicSelectors._miniSticky();
const productImagesEl = this.el.querySelector('ul:first-of-type');
if (!miniStickyEl || !productImagesEl) return;
// Set initial position
miniStickyEl.style.top = `${this.position}px`;
// Get scroll containers
const mainScrollEl = this.dynamicSelectors._mainScroll();
const miniScrollEl = this.dynamicSelectors._miniScroll();
// Handle vertical scroll (show/hide mini sticky)
const handleVerticalScroll = () => {
const rect = productImagesEl.getBoundingClientRect();
const shouldShow = rect.bottom < this.position + 20;
miniStickyEl.classList.toggle('show', shouldShow);
miniStickyEl.classList.toggle('d-none', !shouldShow);
// Sync horizontal position when showing
if (shouldShow && mainScrollEl && miniScrollEl) {
miniScrollEl.scrollLeft = mainScrollEl.scrollLeft;
}
};
// Handle horizontal scroll sync
const syncScroll = (source, target) => {
if (!source._syncing) {
target._syncing = true;
target.scrollLeft = source.scrollLeft;
requestAnimationFrame(() => target._syncing = false);
}
};
// Bind events
window.addEventListener('scroll', handleVerticalScroll, { passive: true });
if (mainScrollEl && miniScrollEl) {
mainScrollEl.addEventListener('scroll', () => syncScroll(mainScrollEl, miniScrollEl), { passive: true });
miniScrollEl.addEventListener('scroll', () => syncScroll(miniScrollEl, mainScrollEl), { passive: true });
}
// Cleanup
this.registerCleanup(() => {
window.removeEventListener('scroll', handleVerticalScroll);
});
// Initial check
handleVerticalScroll();
}
/**
* Add a product to the cart from the comparison page.
*
* @param {Event} ev
*/
addToCart(ev) {
const button = ev.currentTarget;
const productId = parseInt(button.dataset.productProductId);
const productTemplateId = parseInt(button.dataset.productTemplateId);
const showQuantity = Boolean(button.dataset.showQuantity);
this.services['cart'].add({
productTemplateId: productTemplateId,
productId: productId,
}, {
showQuantity: showQuantity,
});
}
/**
* Remove a product from the comparison.
*
* @param {Event} ev
*/
removeProduct(ev) {
const productId = parseInt(ev.currentTarget.dataset.productProductId);
comparisonUtils.removeComparisonProduct(productId, null); // No bus needed on comparison page
const productIds = comparisonUtils.getComparisonProductIds();
if (productIds.length === 0) {
redirect('/shop');
} else {
const comparisonUrl = `/shop/compare?products=${encodeURIComponent(productIds.join(','))}`;
redirect(comparisonUrl);
}
}
}
registry
.category('public.interactions')
.add('website_sale_comparison.comparison_page', ComparisonPage);

View file

@ -0,0 +1,142 @@
import { EventBus } from '@odoo/owl';
import { Interaction } from '@web/public/interaction';
import { registry } from '@web/core/registry';
import { _t } from '@web/core/l10n/translation';
import { rpc } from '@web/core/network/rpc';
import { redirect } from '@web/core/utils/urls';
import wSaleUtils from '@website_sale/js/website_sale_utils';
import comparisonUtils from '@website_sale_comparison/js/website_sale_comparison_utils';
import {
ProductComparisonBottomBar
} from '@website_sale_comparison/js/product_comparison_bottom_bar/product_comparison_bottom_bar';
export class ProductComparison extends Interaction {
static selector = '.js_sale:not(.o_wsale_comparison_page)';
dynamicContent = {
'.o_add_compare, .o_add_compare_dyn': { 't-on-click': this.addProduct },
'input.product_id': { 't-on-change': this.onChangeVariant },
'.o_comparelist_remove': { 't-on-click': this.removeProduct },
};
setup() {
this.bus = new EventBus();
// Mount the ProductComparisonBottomBar on pages with comparison functionality
this.mountComponent(
this.el,
ProductComparisonBottomBar,
{
bus: this.bus,
},
);
}
/**
* Add a product to the comparison.
*
* @param {Event} ev
*/
async addProduct(ev) {
if (this._checkMaxComparisonProducts()) return;
const el = ev.currentTarget;
let productId = parseInt(el.dataset.productProductId);
const form = wSaleUtils.getClosestProductForm(el);
if (!productId) {
productId = await this.waitFor(rpc('/sale/create_product_variant', {
product_template_id: parseInt(el.dataset.productTemplateId),
product_template_attribute_value_ids: wSaleUtils.getSelectedAttributeValues(form),
}));
}
if (!productId || this._checkProductAlreadyInComparison(productId)) {
comparisonUtils.updateDisabled(el, true);
return;
}
comparisonUtils.addComparisonProduct(productId, this.bus);
comparisonUtils.updateDisabled(el, true);
}
/**
* Enable/disable the "add to comparison" button based on the selected variant.
*
* @param {Event} ev
*/
onChangeVariant(ev) {
const input = ev.target;
const productId = input.value;
const button = input.closest('.js_product')?.querySelector('[data-action="o_comparelist"]');
if (button) {
const isDisabled = comparisonUtils.getComparisonProductIds().includes(
parseInt(productId)
);
comparisonUtils.updateDisabled(button, isDisabled);
button.dataset.productProductId = productId;
}
}
/**
* Remove a product from the comparison.
*
* @param {Event} ev
*/
removeProduct(ev) {
const productId = parseInt(ev.currentTarget.dataset.productProductId);
comparisonUtils.removeComparisonProduct(productId, this.bus);
const productIds = comparisonUtils.getComparisonProductIds();
const comparisonUrl = `/shop/compare?products=${encodeURIComponent(productIds.join(','))}`;
redirect(productIds.length ? comparisonUrl : '/shop');
}
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* Check whether the maximum number of products in the comparison has been reached, and if so,
* show a warning.
*
* @return {boolean} Whether the maximum number of products in the comparison has been reached.
*/
_checkMaxComparisonProducts() {
if (
comparisonUtils.getComparisonProductIds().length
>= comparisonUtils.MAX_COMPARISON_PRODUCTS
) {
this.services.notification.add(
_t("You can compare up to 4 products at a time."),
{
type: 'warning',
sticky: false,
title: _t("Too many products to compare"),
},
);
return true;
}
return false;
}
/**
* Check whether the product is already in the comparison, and if so, show a warning.
*
* @param productId The ID of the product to check.
* @return {boolean} Whether the product is already in the comparison.
*/
_checkProductAlreadyInComparison(productId) {
if (comparisonUtils.getComparisonProductIds().includes(productId)) {
this.services.notification.add(
_t("This product has already been added to the comparison."),
{ type: 'warning', sticky: false },
);
return true;
}
return false;
}
}
registry
.category('public.interactions')
.add('website_sale_comparison.product_comparison', ProductComparison);

View file

@ -0,0 +1,67 @@
import { Component, onWillStart, useState, useSubEnv } from '@odoo/owl';
import { rpc } from '@web/core/network/rpc';
import { useBus } from '@web/core/utils/hooks';
import comparisonUtils from '@website_sale_comparison/js/website_sale_comparison_utils';
import { ProductRow } from '../product_row/product_row';
export class ProductComparisonBottomBar extends Component {
static template = 'website_sale_comparison.ProductComparisonBottomBar';
static components = { ProductRow };
static props = {
bus: Object,
};
setup() {
super.setup();
this.state = useState({ products: new Map() });
useBus(this.props.bus, comparisonUtils.COMPARISON_EVENT, (_) => this._loadProducts());
useSubEnv({bus: this.props.bus});
onWillStart(this._loadProducts);
}
/**
* Load the products to compare from the server.
*
* This method also removes any products that are no longer available from the comparison.
*/
async _loadProducts() {
const productIds = comparisonUtils.getComparisonProductIds();
if (!productIds.length) {
this.state.products.clear();
return;
}
const productData = await rpc('/shop/compare/get_product_data', {
product_ids: productIds,
});
this.state.products.clear();
productData.forEach((product) => this.state.products.set(product.id, product));
}
/**
* Get the URL of the comparison page with the selected products.
*
* @return {string} The URL of the comparison page.
*/
get comparisonUrl() {
const productIds = Array.from(this.state.products.keys());
return `/shop/compare?products=${encodeURIComponent(productIds.join(','))}`;
}
/**
* Get the count of products being compared.
* @return {number} The number of products.
*/
get productCount() {
return this.state.products.size;
}
/**
* Clear all products from comparison.
*/
clearAllProducts() {
comparisonUtils.clearComparisonProducts(this.env.bus);
}
}

View file

@ -0,0 +1,76 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="website_sale_comparison.ProductComparisonBottomBar">
<div class="o_not_editable position-absolute top-0 start-0 bottom-0 end-0 d-flex align-items-end justify-content-end p-0 pe-none">
<div
t-if="state.products.size"
class="o_wsale_comparison_bottom_bar sticky-bottom flex-shrink-0 w-100 bg-body pe-auto"
name="comparison_bottom_bar"
>
<div class="container">
<div class="collapse d-xl-none" id="comparisonCollapse">
<ul class="list-group list-group-flush">
<li
t-foreach="state.products.values()"
t-as="product"
t-key="product.id"
class="list-group-item d-flex align-items-start justify-content-between gap-2 px-0 py-3"
>
<ProductRow t-props="product"/>
</li>
</ul>
</div>
<div class="d-flex align-items-center gap-3 py-3">
<button
class="btn btn-light d-xl-none me-auto"
type="button"
data-bs-toggle="collapse"
data-bs-target="#comparisonCollapse"
aria-expanded="false"
aria-controls="comparisonCollapse"
aria-label="Show products added to comparison"
>
<i class="oi oi-chevron-up" role="presentation"/>
</button>
<ul class="list-unstyled d-none d-xl-flex flex-grow-1 flex-shrink-1 gap-3 overflow-hidden me-auto mb-0">
<li
t-foreach="state.products.values()"
t-as="product"
t-key="product.id"
class="flex-basis-25 overflow-hidden"
t-att-class="{'border-end pe-3': !product_last}"
>
<ProductRow t-props="product"/>
</li>
</ul>
<div class="d-flex gap-2 justify-content-end flex-grow-1 flex-xl-grow-0 flex-shrink-0">
<t t-call="website_sale_comparison.ProductComparisonBottomBar.Actions"/>
</div>
</div>
</div>
</div>
</div>
</t>
<t t-name="website_sale_comparison.ProductComparisonBottomBar.Actions">
<a
t-att-href="comparisonUrl"
t-att-disabled="productCount lt 2"
class="btn btn-primary flex-grow-1 flex-md-grow-0"
t-att-class="{ 'disabled': productCount lt 2 }"
role="button"
title="Compare products"
>
Compare
<span t-if="productCount" t-out="productCount" class="badge ms-2"/>
</a>
<button
t-on-click="clearAllProducts"
class="btn btn-light d-none d-sm-block"
aria-label="Remove all products from comparison"
>
Remove All
</button>
</t>
</templates>

View file

@ -0,0 +1,43 @@
import { Component } from '@odoo/owl';
import { formatCurrency } from '@web/core/currency';
import comparisonUtils from '@website_sale_comparison/js/website_sale_comparison_utils';
export class ProductRow extends Component {
static template = 'website_sale_comparison.ProductRow';
static props = {
id: Number,
display_name: String,
website_url: String,
image_url: String,
price: Number,
strikethrough_price: { type: Number, optional: true },
prevent_zero_price_sale: Boolean,
currency_id: Number,
};
/**
* Remove the product from the comparison.
*/
removeProduct() {
comparisonUtils.removeComparisonProduct(this.props.id, this.env.bus);
comparisonUtils.enableDisabledProducts([this.props.id], false);
}
/**
* Get the price, formatted using the provided currency.
*
* @return {string} The formatted price.
*/
get formattedPrice() {
return formatCurrency(this.props.price, this.props.currency_id);
}
/**
* Get the strikethrough price, formatted using the provided currency.
*
* @return {string} The formatted strikethrough price.
*/
get formattedStrikethroughPrice() {
return formatCurrency(this.props.strikethrough_price, this.props.currency_id);
}
}

View file

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="website_sale_comparison.ProductRow">
<div
class="d-flex align-items-center gap-2 flex-grow-1 p-1 rounded text-truncate"
name="product_comparison_bottom_bar_row"
>
<a
t-att-href="props.website_url"
class="d-flex gap-2 flex-grow-1 text-truncate text-decoration-none"
>
<img
t-att-src="props.image_url"
class="o_wsale_comparison_bottom_bar_image border rounded"
alt="Product Image"
/>
<h6 class="flex-grow-1 mb-0 text-truncate">
<span t-out="props.display_name" class="d-block mb-0 small text-truncate"/>
<small t-if="!props.prevent_zero_price_sale" class="mb-0 text-body">
<span t-out="formattedPrice" class="me-1 text-nowrap"/>
<del
t-if="props.strikethrough_price"
t-out="formattedStrikethroughPrice"
class="text-nowrap text-muted"
/>
</small>
</h6>
</a>
<button
t-on-click="removeProduct"
class="btn btn-sm btn-light ms-auto rounded-circle p-2 lh-1"
aria-label="Remove product from comparison"
>
<i class="oi oi-close"/>
</button>
</div>
</t>
</templates>

View file

@ -1,16 +0,0 @@
/** @odoo-module **/
import { WebsiteSale } from 'website_sale.website_sale';
WebsiteSale.include({
/**
* Toggles the add to cart button depending on the possibility of the
* current combination.
*
* @override
*/
_toggleDisable: function ($parent, isCombinationPossible) {
this._super(...arguments);
$parent.find('a.a-submit').toggleClass('disabled', !isCombinationPossible);
},
});

View file

@ -1,337 +0,0 @@
odoo.define('website_sale_comparison.comparison', function (require) {
'use strict';
var concurrency = require('web.concurrency');
var core = require('web.core');
var publicWidget = require('web.public.widget');
const {getCookie, setCookie} = require('web.utils.cookies');
var VariantMixin = require('sale.VariantMixin');
var website_sale_utils = require('website_sale.utils');
const cartHandlerMixin = website_sale_utils.cartHandlerMixin;
var qweb = core.qweb;
var _t = core._t;
// VariantMixin events are overridden on purpose here
// to avoid registering them more than once since they are already registered
// in website_sale.js
var ProductComparison = publicWidget.Widget.extend(VariantMixin, {
template: 'product_comparison_template',
events: {
'click .o_product_panel_header': '_onClickPanelHeader',
},
/**
* @constructor
*/
init: function () {
this._super.apply(this, arguments);
this.product_data = {};
this.comparelist_product_ids = JSON.parse(getCookie('comparelist_product_ids') || '[]');
this.product_compare_limit = 4;
this.guard = new concurrency.Mutex();
},
/**
* @override
*/
start: function () {
var self = this;
self._loadProducts(this.comparelist_product_ids).then(function () {
self._updateContent('hide');
});
self._updateComparelistView();
$('#comparelist .o_product_panel_header').popover({
trigger: 'manual',
animation: true,
html: true,
title: function () {
return _t("Compare Products");
},
container: '.o_product_feature_panel',
placement: 'top',
template: qweb.render('popover'),
content: function () {
return $('#comparelist .o_product_panel_content').html();
}
});
// We trigger a resize to launch the event that checks if this element hides
// a button when the page is loaded.
$(window).trigger('resize');
$(document.body).on('click.product_comparaison_widget', '.comparator-popover .o_comparelist_products .o_remove', function (ev) {
ev.preventDefault();
self._removeFromComparelist(ev);
});
$(document.body).on('click.product_comparaison_widget', '.o_comparelist_remove', function (ev) {
self._removeFromComparelist(ev);
self.guard.exec(function() {
const newLink = '/shop/compare?products=' + encodeURIComponent(self.comparelist_product_ids);
window.location.href = _.isEmpty(self.comparelist_product_ids) ? '/shop' : newLink;
});
});
return this._super.apply(this, arguments);
},
/**
* @override
*/
destroy: function () {
this._super.apply(this, arguments);
$(document.body).off('.product_comparaison_widget');
},
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* @param {jQuery} $elem
*/
handleCompareAddition: function ($elem) {
var self = this;
if (this.comparelist_product_ids.length < this.product_compare_limit) {
var productId = $elem.data('product-product-id');
if ($elem.hasClass('o_add_compare_dyn')) {
productId = $elem.parent().find('.product_id').val();
if (!productId) { // case List View Variants
productId = $elem.parent().find('input:checked').first().val();
}
}
let $form = $elem.closest('form');
$form = $form.length ? $form : $('#product_details > form');
this.selectOrCreateProduct(
$form,
productId,
$form.find('.product_template_id').val(),
false
).then(function (productId) {
productId = parseInt(productId, 10) || parseInt($elem.data('product-product-id'), 10);
if (!productId) {
return;
}
self._addNewProducts(productId).then(function () {
website_sale_utils.animateClone(
$('#comparelist .o_product_panel_header'),
$elem.closest('form'),
-50,
10
);
});
});
} else {
this.$('.o_comparelist_limit_warning').show();
$('#comparelist .o_product_panel_header').popover('show');
}
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* @private
*/
_loadProducts: function (product_ids) {
var self = this;
return this._rpc({
route: '/shop/get_product_data',
params: {
product_ids: product_ids,
cookies: JSON.parse(getCookie('comparelist_product_ids') || '[]'),
},
}).then(function (data) {
self.comparelist_product_ids = JSON.parse(data.cookies);
delete data.cookies;
_.each(data, function (product) {
self.product_data[product.product.id] = product;
});
if (product_ids.length > Object.keys(data).length) {
/* If some products have been archived
they are not displayed but the count & cookie
need to be updated.
*/
self._updateCookie();
}
});
},
/**
* @private
*/
_togglePanel: function () {
$('#comparelist .o_product_panel_header').popover('toggle');
},
/**
* @private
*/
_addNewProducts: function (product_id) {
return this.guard.exec(this._addNewProductsImpl.bind(this, product_id));
},
_addNewProductsImpl: function (product_id) {
var self = this;
$('.o_product_feature_panel').addClass('d-md-block');
if (!_.contains(self.comparelist_product_ids, product_id)) {
self.comparelist_product_ids.push(product_id);
if (_.has(self.product_data, product_id)){
self._updateContent();
} else {
return self._loadProducts([product_id]).then(function () {
self._updateContent();
self._updateCookie();
});
}
}
self._updateCookie();
},
/**
* @private
*/
_updateContent: function (force) {
var self = this;
this.$('.o_comparelist_products .o_product_row').remove();
_.each(this.comparelist_product_ids, function (res) {
if (self.product_data.hasOwnProperty(res)) {
// It is possible that we do not have the required product_data for all IDs in
// comparelist_product_ids
var $template = self.product_data[res].render;
self.$('.o_comparelist_products').append($template);
}
});
if (force !== 'hide' && (this.comparelist_product_ids.length > 1 || force === 'show')) {
$('#comparelist .o_product_panel_header').popover('show');
}
else {
$('#comparelist .o_product_panel_header').popover('hide');
}
},
/**
* @private
*/
_removeFromComparelist: function (e) {
this.guard.exec(this._removeFromComparelistImpl.bind(this, e));
},
_removeFromComparelistImpl: function (e) {
var target = $(e.target.closest('.o_comparelist_remove, .o_remove'));
this.comparelist_product_ids = _.without(this.comparelist_product_ids, target.data('product_product_id'));
target.parents('.o_product_row').remove();
this._updateCookie();
$('.o_comparelist_limit_warning').hide();
this._updateContent('show');
},
/**
* @private
*/
_updateCookie: function () {
setCookie('comparelist_product_ids', JSON.stringify(this.comparelist_product_ids), 24 * 60 * 60 * 365, 'required');
this._updateComparelistView();
},
/**
* @private
*/
_updateComparelistView: function () {
this.$('.o_product_circle').text(this.comparelist_product_ids.length);
this.$('.o_comparelist_button').removeClass('d-md-block');
if (_.isEmpty(this.comparelist_product_ids)) {
$('.o_product_feature_panel').removeClass('d-md-block');
} else {
$('.o_product_feature_panel').addClass('d-md-block');
this.$('.o_comparelist_products').addClass('d-md-block');
if (this.comparelist_product_ids.length >=2) {
this.$('.o_comparelist_button').addClass('d-md-block');
this.$('.o_comparelist_button a').attr('href',
'/shop/compare?products=' + encodeURIComponent(this.comparelist_product_ids));
}
}
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* @private
*/
_onClickPanelHeader: function () {
this._togglePanel();
},
});
publicWidget.registry.ProductComparison = publicWidget.Widget.extend(cartHandlerMixin, {
selector: '.js_sale',
events: {
'click .o_add_compare, .o_add_compare_dyn': '_onClickAddCompare',
'click #o_comparelist_table tr': '_onClickComparelistTr',
'submit .o_add_cart_form_compare': '_onFormSubmit',
},
/**
* @override
*/
start: function () {
var def = this._super.apply(this, arguments);
this.productComparison = new ProductComparison(this);
this.getRedirectOption();
return Promise.all([def, this.productComparison.appendTo(this.$el)]);
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* @private
* @param {Event} ev
*/
_onClickAddCompare: function (ev) {
this.productComparison.handleCompareAddition($(ev.currentTarget));
},
/**
* @private
* @param {Event} ev
*/
_onClickComparelistTr: function (ev) {
var $target = $(ev.currentTarget);
$($target.data('target')).children().slideToggle(100);
$target.find('.fa-chevron-circle-down, .fa-chevron-circle-right').toggleClass('fa-chevron-circle-down fa-chevron-circle-right');
},
/**
* @private
* @param {Event} ev
*/
_onFormSubmit(ev) {
ev.preventDefault();
const $form = $(ev.currentTarget);
const cellIndex = $(ev.currentTarget).closest('td')[0].cellIndex;
this.getCartHandlerOptions(ev);
// Override product image container for animation.
this.$itemImgContainer = this.$('#o_comparelist_table tr').first().find('td').eq(cellIndex);
const $inputProduct = $form.find('input[type="hidden"][name="product_id"]').first();
const productId = parseInt($inputProduct.val());
if (productId) {
const productTrackingInfo = $inputProduct.data('product-tracking-info');
if (productTrackingInfo) {
productTrackingInfo.quantity = 1;
$inputProduct.trigger('add_to_cart_event', [productTrackingInfo]);
}
return this.addToCart(this._getAddToCartParams(productId, $form));
}
},
/**
* Get the addToCart Params
*
* @param {number} productId
* @param {JQuery} $form
* @override
*/
_getAddToCartParams(productId, $form) {
return {
product_id: productId,
add_qty: 1,
};
}
});
return ProductComparison;
});

View file

@ -0,0 +1,110 @@
import { cookie } from '@web/core/browser/cookie';
const COMPARISON_PRODUCT_IDS_COOKIE_NAME = 'comparison_product_ids';
const MAX_COMPARISON_PRODUCTS = 4;
const COMPARISON_EVENT = 'comparison_products_changed'
/**
* Get the IDs of the products to compare from the cookie.
*
* @return {Array<number>} The IDs of the products to compare.
*/
function getComparisonProductIds() {
return JSON.parse(cookie.get(COMPARISON_PRODUCT_IDS_COOKIE_NAME) || '[]');
}
/**
* Set the IDs of the products to compare in the cookie.
*
* @param {ArrayLike<number>} productIds The IDs of the products to compare.
* @param {EventBus} bus
*/
function setComparisonProductIds(productIds, bus) {
cookie.set(COMPARISON_PRODUCT_IDS_COOKIE_NAME, JSON.stringify(Array.from(productIds)));
notifyComparisonListeners(bus);
}
/**
* Add the specified product to the comparison.
*
* @param {number} productId
* @param {EventBus} bus
*/
function addComparisonProduct(productId, bus) {
const productIds = new Set(getComparisonProductIds());
productIds.add(productId);
setComparisonProductIds(productIds, bus);
}
/**
* Remove the specified product from the comparison.
*
* @param {number} productId
* @param {EventBus} bus
*/
function removeComparisonProduct(productId, bus) {
const productIds = new Set(getComparisonProductIds());
productIds.delete(productId);
setComparisonProductIds(productIds, bus);
}
/**
* Clear all products in comparison list
*
* @param {EventBus} bus
*/
function clearComparisonProducts(bus) {
const productIds = getComparisonProductIds();
cookie.delete(COMPARISON_PRODUCT_IDS_COOKIE_NAME);
notifyComparisonListeners(bus);
enableDisabledProducts(productIds);
}
/**
* Notify comparison listeners using an event bus that the values of productshave changed
*
* @param {EventBus} bus
*/
function notifyComparisonListeners(bus) {
if (bus) {
bus.dispatchEvent(new CustomEvent(COMPARISON_EVENT, { bubbles: true }));
}
}
/**
* Update the disabled/enabled state of an element.
*
* @param {Element} el The element to disable/enable.
* @param {boolean} isDisabled Whether the element should be disabled.
*/
function updateDisabled(el, isDisabled) {
el.disabled = isDisabled;
el.classList.toggle('disabled', isDisabled);
}
/**
* After removing products from comparison, update the disabled button
*/
function enableDisabledProducts(productIds) {
for (const productId of productIds) {
const productCompareButton = document.querySelector(
`.o_add_compare[data-product-product-id="${productId}"]`
);
if (productCompareButton) {
updateDisabled(productCompareButton, false);
}
}
}
export default {
MAX_COMPARISON_PRODUCTS: MAX_COMPARISON_PRODUCTS,
COMPARISON_EVENT: COMPARISON_EVENT,
getComparisonProductIds: getComparisonProductIds,
setComparisonProductIds: setComparisonProductIds,
addComparisonProduct: addComparisonProduct,
removeComparisonProduct: removeComparisonProduct,
clearComparisonProducts: clearComparisonProducts,
notifyComparisonListeners: notifyComparisonListeners,
updateDisabled: updateDisabled,
enableDisabledProducts: enableDisabledProducts,
};

View file

@ -0,0 +1,28 @@
// ======= Options
// By default, hide `o_add_compare` buttons when inside a product
// entry. Show them when `o_wsale_products_opt_has_comparison` is applied.
:where(.oe_product_cart) {
.o_add_compare, .o_add_to_compare {
display: var(--o-wsale-comparison-btn-display, none);
order: var(--o-wsale-comparison-btn-order);
}
.o_add_compare_placeholder, .o_add_to_compare_placeholder {
display: var(--o-wsale-comparison-btn-placeholder-display, none);
order: var(--o-wsale-comparison-btn-order);
}
}
:where(.o_wsale_products_opt_has_comparison) {
:where(.oe_product_cart) {
--o-wsale-comparison-btn-display: inline-flex;
}
&:where(.o_wsale_products_opt_actions_onhover):where(.o_wsale_products_opt_layout_catalog) :where(.oe_product_cart) {
--o-wsale-comparison-btn-display: flex;
}
}

View file

@ -1,57 +1,91 @@
.o_product_feature_panel {
position: fixed;
bottom: 0;
left: 50%;
transform: translateX(-50%);
z-index:10;
border: 2px solid;
.oe_website_sale {
.o_add_compare_dyn.btn-lg {
aspect-ratio: 1/1;
height: calc(#{$btn-padding-y-lg} * 2 + #{$btn-font-size-lg} * #{$btn-line-height} + #{$btn-border-width} * 2);
}
}
.o_product_panel {
position: relative;
.o_product_panel_header {
margin: 0 10px 0 10px;
cursor: pointer;
.o_product_icon {
margin-right: 5px;
}
.o_product_text {
text-transform: uppercase;
vertical-align: middle;
font-size: 16px;
}
.o_product_circle {
vertical-align: 6px;
padding: 0 3px;
line-height: 14px;
}
}
.o_product_panel_content {
display: none !important;
:where(main:has(.oe_website_sale.container-fluid)) {
.o_wsale_comparison_bottom_bar > .container {
max-width: unset;
}
ul {
@media screen and (min-width: 1920px) {
max-width: 75%;
}
}
}
.oe_website_sale {
.product_summary > *{
display: block;
margin: 15px 0 15px 0;
}
.table-comparator {
.o_product_comparison_collpase {
margin-right: 8px;
.o_wsale_comparison_page {
--o-website-sale-comparison-table-column-padding-x: #{map-get($spacers, 2)};
> .overflow-x-auto {
margin: 0 calc(var(--o-website-sale-comparison-table-column-padding-x) * -1);
@include media-breakpoint-down(lg) {
margin: 0 calc(var(--gutter-x, 30px) / -2);
}
}
div.css_not_available .o_add_compare_dyn {
display: none;
.o_wsale_compare_table_column {
--o-website-sale-comparison-table-column-width: #{map-get($container-max-widths, 'sm') * .5};
@include media-breakpoint-up(xl) {
--o-website-sale-comparison-table-column-width: 25%;
}
width: var(--o-website-sale-comparison-table-column-width);
padding: 0 var(--o-website-sale-comparison-table-column-padding-x);
img {
@extend %o-wsale-shop-thumb;
}
}
.o_comparelist_remove {
@include o-position-absolute($top: 0, $right: 0.5rem);
#o_comparelist_table {
@include media-breakpoint-down(xl) {
min-width: max-content;
}
@include media-breakpoint-down(lg) {
padding: 0 calc(var(--o-website-sale-comparison-table-column-padding-x));
}
}
.o_ws_compare_image {
vertical-align: middle;
// Mini sticky comparison overview
.o_wsale_comparison_mini_sticky {
transform: translateY(-100%);
transition: transform .4s ease-in-out;
box-shadow: 0px 3px 3px rgba($black, .1);
z-index: $zindex-fixed - 1;
&.show {
transform: translateY(0);
}
.o_mini_product_img_container {
width: 2.5rem;
}
.o_wsale_comparison_scroll_container {
-ms-overflow-style: none;
scrollbar-width: none;
margin: 0 calc(var(--o-website-sale-comparison-table-column-padding-x) * -1);
&::-webkit-scrollbar {
display: none;
}
}
&::after {
@include o-position-absolute(-50vh, 0, 0, 0);
content: "";
background-color: inherit;
height: 50vh;
width: 100%;
}
}
}
@ -60,6 +94,53 @@
border-top: 1px solid map-get($grays, '400');
.o_add_compare_dyn {
font-size: 1.1rem;
@include font-size(1.1rem);
}
}
.o_wsale_comparison_bottom_bar {
z-index: $zindex-modal;
box-shadow: 0px -12px 32px rgba($black, .175);
.btn[data-bs-toggle="collapse"] {
.oi-chevron-up {
transition: transform 0.2s ease-in-out;
}
&[aria-expanded="true"] .oi-chevron-up {
transform: rotate(180deg);
}
}
.o_wsale_comparison_offcanvas_image {
width: 5rem;
}
a[title="Compare products"] .badge {
--badge-color: var(--btn-hover-bg);
--badge-bg: var(--btn-color);
}
div[name="product_comparison_bottom_bar_row"]:where(:has(button[aria-label="Remove product from comparison"]:hover)) {
background: $light;
}
.o_wsale_comparison_bottom_bar_image {
width: 2.5rem;
height: fit-content;
@extend %o-wsale-shop-thumb;
}
}
// Style the <main> element when comparison bar is present
:where(main:has(.o_wsale_comparison_bottom_bar)) {
--o-wsale-comparison-bottom-bar-height: 80px;
position: relative;
padding-bottom: var(--o-wsale-comparison-bottom-bar-height);
// Offset floating bar when comparison bar is visible
#o_wsale_floating_bar {
bottom: calc(var(--_container-gap) + var(--o-wsale-comparison-bottom-bar-height)) !important;
}
}

View file

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="website_sale_comparison.ProductPageOption" t-inherit="website_sale.ProductPageOption" t-inherit-mode="extension">
<BuilderRow id="'o_we_actions_opt'" position="inside">
<BuilderButton title.translate="Compare"
action="'websiteConfig'"
actionParam="{views:['website_sale_comparison.product_add_to_compare']}"
>
<i class="fa fa-fw fa-exchange"/>
Compare
</BuilderButton>
</BuilderRow>
<BuilderRow id="'o_we_actions_opt'" position="after">
<BuilderRow label.translate="Specification">
<BuilderSelect action="'websiteConfig'">
<BuilderSelectItem actionParam="{views:[]}">None</BuilderSelectItem>
<BuilderSelectItem actionParam="{views:['website_sale_comparison.product_attributes_body']}">Bottom of Page</BuilderSelectItem>
<BuilderSelectItem actionParam="{views:['website_sale_comparison.accordion_specs_item']}">In accordion</BuilderSelectItem>
</BuilderSelect>
</BuilderRow>
</BuilderRow>
</t>
</templates>

View file

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t
t-name="website_sale_comparison.ProductsDesignPanel"
t-inherit="website_sale.ProductsDesignPanel"
t-inherit-mode="extension"
>
<BuilderButton id="'wsale_products_opt_add_to_cart'" position="after">
<BuilderButton
t-if="props.recordName === 'shop_opt_products_design_classes'"
title.translate="Compare"
icon="'fa-exchange'"
classAction="'o_wsale_products_opt_has_comparison'"
/>
<span/><!-- Spacer -->
</BuilderButton>
</t>
</templates>

View file

@ -1,35 +0,0 @@
<templates id="compare_products" xml:space="preserve">
<t t-name="product_comparison_template">
<div class="o_product_feature_panel d-none css_editable_mode_hidden o_bottom_fixed_element bg-white rounded-top border-primary border-bottom-0 px-3 py-2">
<span class="o_product_panel" id="comparelist">
<span class="o_product_panel_header text-center">
<span class="o_product_icon"><i class="fa fa-exchange" role="img" aria-label="Product" title="Product"></i></span>
<span class="o_product_text">Compare</span>
<span class="o_product_circle o_animate_blink badge text-bg-primary">0</span>
</span>
<span class="o_product_panel_content">
<div class="o_comparelist_products">
<div class="o_comparelist_limit_warning" style="display:none">
<div class="o_shortlog alert alert-warning" role="alert">
<span><i class="fa fa-warning text-danger" role="img" aria-label="Warning" title="Warning"></i> You can compare max 4 products.</span>
</div>
</div>
</div>
<div class="o_comparelist_button" style='display:none'>
<a role="button" class="btn btn-primary d-block" href="#"><i class="fa fa-exchange me-2"/>Compare</a>
</div>
</span>
</span>
</div>
</t>
<t t-name="popover">
<div style="width:600px;" class="popover comparator-popover" role="tooltip">
<div class="arrow"/>
<h3 class="popover-header"/>
<div class="popover-body"/>
</div>
</t>
</templates>

View file

@ -1,169 +1,154 @@
odoo.define('website_sale_comparison.tour_comparison', function (require) {
'use strict';
import { registry } from "@web/core/registry";
import { clickOnElement } from '@website/js/tours/tour_utils';
import * as tourUtils from "@website_sale/js/tours/tour_utils";
var tour = require('web_tour.tour');
const tourUtils = require('website_sale.tour_utils');
tour.register('product_comparison', {
test: true,
registry.category("web_tour.tours").add('product_comparison', {
url: "/shop",
}, [
steps: () => [
// test from shop page
{
content: "add first product 'Color T-Shirt' in a comparison list",
trigger: '.oe_product_cart:contains("Color T-Shirt") .o_add_compare',
trigger: '.oe_product_cart:contains("Color T-Shirt")',
run: "hover && click .oe_product_cart:contains(Color T-Shirt) .o_add_compare",
},
{
content: "check compare button contains one product",
trigger: '.o_product_circle:contains(1)',
run: function () {},
},
{
content: "check popover is closed when only one product",
trigger: 'body:not(:has(.comparator-popover))',
run: function () {},
trigger: '.o_wsale_comparison_bottom_bar .badge:contains(1)',
},
{
content: "add second product 'Color Pants' in a comparison list",
trigger: '.oe_product_cart:contains("Color Pants") .o_add_compare',
trigger: '.oe_product_cart:contains("Color Pants")',
run: "hover && click .oe_product_cart:contains(Color Pants) .o_add_compare",
},
{
content: "check popover is now open and compare button contains two products",
extra_trigger: '.comparator-popover',
trigger: ' .o_product_circle:contains(2)',
run: function () {},
content: "check that the compare button contains two products",
trigger: '.o_wsale_comparison_bottom_bar .badge:contains(2)',
},
{
content: "check products name are correct in the comparelist",
extra_trigger: '.o_product_row:contains("Color T-Shirt")',
trigger: '.o_product_row:contains("Color Pants")',
run: function () {},
trigger: '[name="product_comparison_bottom_bar_row"]:contains("Color T-Shirt")',
},
{
content: "check products name are correct in the comparelist",
trigger: '[name="product_comparison_bottom_bar_row"]:contains("Color Pants")',
},
{
content: "remove product",
trigger: '[name="product_comparison_bottom_bar_row"]:contains("Color T-Shirt") button:has(i.oi-close)',
run: "click",
},
{
content: "wait for 'Color T-Shirt' to be removed from the popover",
trigger: '[name="product_comparison_bottom_bar_row"]:not(:contains("Color T-Shirt"))',
},
{
content: "re-add 'Color T-Shirt' in comparison list",
trigger: '.oe_product_cart:contains("Color T-Shirt")',
run: "hover && click .oe_product_cart:contains(Color T-Shirt) .o_add_compare",
},
// test form product page
{
content: "go to product page of Color Shoes (with variants)",
trigger: '.oe_product_cart a:contains("Color Shoes")',
run: "click",
expectUnloadPage: true,
},
{
content: "check compare button is still there and contains 2 products",
extra_trigger: '#product_details',
trigger: '.o_product_circle:contains(2)',
run: function () {},
},
{
content: "check popover is closed after changing page",
trigger: 'body:not(:has(.comparator-popover))',
run: function () {},
trigger: '.o_wsale_comparison_bottom_bar .badge:contains(2)',
},
{
content: "add first variant to comparelist",
trigger: '.o_add_compare_dyn',
run: "click",
},
{
content: "check the comparelist is now open and contains 3rd product with correct variant",
extra_trigger: '.comparator-popover',
trigger: '.o_product_row:contains("Color Shoes (Red)")',
run: function () {},
trigger: '[name="product_comparison_bottom_bar_row"]:contains("Color Shoes (Red)")',
},
{
content: "select 2nd variant(Pink Color)",
trigger: '.variant_attribute[data-attribute_name="Color"] input[data-value_name="Pink"]',
trigger: '.variant_attribute[data-attribute-name="Color"] input[data-value-name="Pink"]:not(:visible)',
run: function (actions) {
$('img[class*="product_detail_img"]').attr('data-image-to-change', 1);
document.querySelector('img[class*="product_detail_img"]').setAttribute('data-image-to-change', 1);
actions.click();
},
},
{
trigger: 'img[class*="product_detail_img"]:not([data-image-to-change])',
},
{
content: "click on compare button to add in comparison list when variant changed",
extra_trigger: 'img[class*="product_detail_img"]:not([data-image-to-change])',
trigger: '.o_add_compare_dyn',
run: "click",
},
{
content: "comparelist contains 4th product with correct variant",
extra_trigger: '.o_product_circle:contains(4)',
trigger: '.o_product_row:contains("Color Shoes (Red)")',
run: function () {},
trigger: '[name="product_comparison_bottom_bar_row"]:contains("Color Shoes (Red)")',
},
{
content: "check limit is not reached",
trigger: ':not(.o_comparelist_limit_warning)',
run: function () {},
trigger: ':not(.o_notification:contains("You can compare up to 4 products at a time."))',
},
{
content: "select 3rd variant(Blue)",
trigger: '.variant_attribute[data-attribute_name="Color"] input[data-value_name="Blue"]',
content: "select 3nd variant(Custom)",
trigger: '.variant_attribute[data-attribute-name="Color"] input[data-value-name="Blue"]:not(:visible)',
run: "click",
},
{
trigger: 'body:not(:has(.carousel-indicators))', // there is 1 image on the custom variant
},
{
content: "click on compare button to add in comparison list when variant changed",
extra_trigger: 'body:not(:has(.carousel-indicators))', // there is 1 image on the custom variant
trigger: '.o_add_compare_dyn',
run: "click",
},
{
content: "check limit is reached",
trigger: '.o_comparelist_limit_warning',
run: function () {},
trigger: '.o_notification:contains("You can compare up to 4 products at a time.")',
},
{
content: "click on compare button",
trigger: '.o_comparelist_button a',
trigger: 'a:contains("Compare")',
run: "click",
expectUnloadPage: true,
},
// test on compare page
{
content: "check 1st product contains correct variant",
trigger: '.o_product_comparison_table:contains("Color Pants (Red)")',
run: function () {},
trigger: '.product_summary a:contains("Color Pants (Red)")',
},
{
content: "check 2nd product contains correct variant",
trigger: '.o_product_comparison_table:contains("Color Shoes (Pink)")',
run: function () {},
trigger: '.product_summary a:contains("Color Shoes (Pink)")',
},
{
content: "check 3rd product is correctly added",
trigger: '.o_product_comparison_table:contains("Color Shoes (Red)")',
run: function () {},
trigger: '.product_summary a:contains("Color Shoes (Red)")',
},
{
content: "check 4th product is correctly added",
trigger: '.o_product_comparison_table:contains("Color T-Shirt")',
run: function () {},
trigger: '.product_summary a:contains("Color T-Shirt")',
},
{
content: "remove Color Shoes (Pink) from compare table",
trigger: '#o_comparelist_table .o_comparelist_remove:eq(2)',
run: "click",
expectUnloadPage: true,
},
{
content: "check color shoes pink variant is removed",
content: "check color shoes with pink variant is removed",
trigger: '#o_comparelist_table:not(:contains("Color Shoes (Pink)"))',
run: function () {},
},
{
content: "open compare menu",
extra_trigger: 'body:has(.o_product_row:contains("Color T-Shirt") .o_remove)',
trigger: '.o_product_panel_header',
},
{
content: "remove product",
trigger: '.o_product_row:contains("Color T-Shirt") .o_remove',
},
{
content: "click on compare button to reload",
trigger: '.o_comparelist_button a',
},
{
content: "check product 'Color T-Shirt' is removed",
trigger: '#o_comparelist_table:not(:contains("Color T-Shirt"))',
run: function () {},
},
{
content: "add product 'Color Pants' to cart",
trigger: '.product_summary:contains("Color Pants") .a-submit:contains("Add to Cart")',
trigger: '.product_summary:contains("Color Pants") button:contains("Add to Cart")',
run: "click",
},
clickOnElement('Add to cart', 'button[name="website_sale_product_configurator_continue_button"]'),
tourUtils.goToCart(),
{
content: "check product correctly added to cart",
trigger: '#cart_products:contains("Color Pants") .js_quantity[value="1"]',
run: function () {},
},
]);
});
]});