[IMP] website_sale: add alternative product image displays

This commit adds a new display type for a product page, namely the grid
mode.
The web designer will now be able to choose between carousel (previous
mode) and grid.
The web designer may also choose how much space the images take on the
page between 0%, 50% 66% and 100%.

TaskId-2833507

Part-of: odoo/odoo#92447
This commit is contained in:
William Braeckman
2022-07-14 16:41:52 +02:00
parent cf9de209e3
commit 54c6d36cfb
13 changed files with 268 additions and 44 deletions
+1
View File
@@ -31,6 +31,7 @@
'views/website_sale_visitor_views.xml',
'views/base_unit_view.xml',
'views/product_product_add.xml',
'views/website_views.xml',
],
'demo': [
'data/demo.xml',
+15 -6
View File
@@ -1207,12 +1207,21 @@ class WebsiteSale(http.Controller):
raise NotFound()
current_website = request.env['website'].get_current_website()
if "ppg" in options:
current_website.shop_ppg = options["ppg"] or 1
if "ppr" in options:
current_website.shop_ppr = options["ppr"]
if "default_sort" in options:
current_website.shop_default_sort = options["default_sort"]
# Restrict options we can write to.
writable_fields = {
'shop_ppg', 'shop_ppr', 'shop_default_sort',
'product_page_image_layout', 'product_page_image_width',
'product_page_grid_columns', 'product_page_image_spacing'
}
# Default ppg to 1.
if 'ppg' in options and not options['ppg']:
options['ppg'] = 1
if 'product_page_grid_columns' in options:
options['product_page_grid_columns'] = int(options['product_page_grid_columns'])
write_vals = {k: v for k, v in options.items() if k in writable_fields}
if write_vals:
current_website.write(write_vals)
def order_lines_2_google_api(self, order_lines):
""" Transforms a list of order lines into a dict for google analytics """
+7 -5
View File
@@ -18,11 +18,13 @@ class WebsiteSaleVariantController(VariantController):
if request.website.google_analytics_key:
combination['product_tracking_info'] = request.env['product.template'].get_google_analytics_data(combination)
carousel_view = request.env['ir.ui.view']._render_template('website_sale.shop_product_carousel', values={
'product': request.env['product.template'].browse(combination['product_template_id']),
'product_variant': request.env['product.product'].browse(combination['product_id']),
})
combination['carousel'] = carousel_view
if request.website.product_page_image_width != 'none':
carousel_view = request.env['ir.ui.view']._render_template('website_sale.shop_product_images', values={
'product': request.env['product.template'].browse(combination['product_template_id']),
'product_variant': request.env['product.product'].browse(combination['product_id']),
'website': request.env['website'].get_current_website(),
})
combination['carousel'] = carousel_view
return combination
@http.route(auth="public")
+48
View File
@@ -80,6 +80,27 @@ class Website(models.Model):
],
default='optional')
product_page_image_layout = fields.Selection([
('carousel', 'Carousel'),
('grid', 'Grid'),
], default='carousel', required=True,
)
product_page_grid_columns = fields.Integer(default=2)
product_page_image_width = fields.Selection([
('none', 'Hidden'),
('50_pc', '50 %'),
('66_pc', '66 %'),
('100_pc', '100 %'),
], default='50_pc', required=True,
)
product_page_image_spacing = fields.Selection([
('none', 'None'),
('small', 'Small'),
('medium', 'Medium'),
('big', 'Big'),
], default='small', required=True,
)
@api.depends('all_pricelist_ids')
def _compute_pricelist_ids(self):
for website in self:
@@ -462,6 +483,33 @@ class Website(models.Model):
result.append(self.env['product.template']._search_get_detail(self, order, options))
return result
def _get_product_page_proportions(self):
"""
Returns the number of columns (css) that both the images and the product details should take.
"""
self.ensure_one()
return {
'none': (0, 12),
'50_pc': (6, 6),
'66_pc': (8, 4),
'100_pc': (12, 12),
}.get(self.product_page_image_width)
def _get_product_page_grid_image_classes(self):
spacing_map = {
'none': 'p-0',
'small': 'p-2',
'medium': 'p-3',
'big': 'p-4',
}
columns_map = {
1: 'col-12',
2: 'col-6',
3: 'col-4',
}
return spacing_map.get(self.product_page_image_spacing) + ' ' +\
columns_map.get(self.product_page_grid_columns)
class WebsiteSaleExtraField(models.Model):
_name = 'website.sale.extra.field'
@@ -0,0 +1,3 @@
<svg width="24" height="8" viewBox="0 0 24 8" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M23.3594 0V8H0.359375V0H23.3594ZM18.3594 1V3.5H15.3594V4.5H18.3594V7L21.3594 4L18.3594 1ZM5.35938 1L2.35938 4L5.35938 7V4.5H8.35938V3.5H5.35938V1Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 313 B

@@ -0,0 +1,4 @@
<svg width="23" height="8" viewBox="0 0 23 8" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M21 0H12V8H21V0Z" fill="#CDCDCD"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M2 0V8H11V0H2Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 225 B

@@ -0,0 +1,4 @@
<svg width="24" height="8" viewBox="0 0 24 8" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M21.3594 0H15.3594V8H21.3594V0Z" fill="#CDCDCD"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.35938 0V8H13.3594V0H2.35938Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 257 B

@@ -0,0 +1,3 @@
<svg width="24" height="8" viewBox="0 0 24 8" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2 0V8H21V0H2ZM14.36 5.56C14.45 5.65 14.5 5.75 14.5 5.87C14.5 5.99 14.45 6.1 14.36 6.18L13.69 6.8C13.6 6.89 13.49 6.93 13.35 6.93C13.21 6.93 13.11 6.89 13.01 6.8L11.56 5.46L10.11 6.8C10.02 6.89 9.91 6.93 9.77 6.93C9.63 6.93 9.53 6.89 9.43 6.8L8.76 6.18C8.67 6.09 8.62 5.99 8.62 5.87C8.62 5.75 8.67 5.64 8.76 5.56L10.21 4.22L8.76 2.88C8.67 2.79 8.62 2.69 8.62 2.57C8.62 2.45 8.67 2.34 8.76 2.26L9.43 1.64C9.52 1.55 9.63 1.51 9.77 1.51C9.91 1.51 10.01 1.55 10.11 1.64L11.56 2.98L13.01 1.64C13.1 1.55 13.21 1.51 13.35 1.51C13.49 1.51 13.59 1.55 13.69 1.64L14.36 2.26C14.45 2.35 14.5 2.45 14.5 2.57C14.5 2.69 14.45 2.8 14.36 2.88L12.91 4.22L14.36 5.56Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 775 B

@@ -234,7 +234,7 @@ options.registry.WebsiteSaleGridLayout = options.Class.extend({
return this._rpc({
route: '/shop/config/website',
params: {
'ppg': ppg,
'shop_ppg': ppg,
},
});
},
@@ -246,7 +246,7 @@ options.registry.WebsiteSaleGridLayout = options.Class.extend({
this._rpc({
route: '/shop/config/website',
params: {
'ppr': this.ppr,
'shop_ppr': this.ppr,
},
});
},
@@ -258,7 +258,7 @@ options.registry.WebsiteSaleGridLayout = options.Class.extend({
this._rpc({
route: '/shop/config/website',
params: {
'default_sort': this.default_sort,
'shop_default_sort': this.default_sort,
},
});
},
@@ -643,6 +643,96 @@ options.registry.WebsiteSaleProductsItem = options.Class.extend({
}
});
options.registry.WebsiteSaleProductPage = options.Class.extend({
_updateWebsiteConfig(params) {
this._rpc({
route: '/shop/config/website',
params,
}).then(() => this.trigger_up('request_save', {reload: true, optionSelector: this.data.selector}));
},
/**
* @override
*/
setImageWidth(previewMode, widgetValue, params) {
this._updateWebsiteConfig({
product_page_image_width: widgetValue,
});
},
/**
* @override
*/
setImageLayout(previewMode, widgetValue, params) {
this._updateWebsiteConfig({
product_page_image_layout: widgetValue,
});
},
/**
* @override
*/
async _computeWidgetState(methodName, params) {
switch (methodName) {
case 'setImageWidth':
return this.$target.data('image_width');
case 'setImageLayout':
return this.$target.data('image_layout');
}
return this._super(...arguments);
}
});
options.registry.WebsiteSaleProductPageGrid = options.Class.extend({
/**
* @override
*/
setSpacing(previewMode, widgetValue, params) {
const spacing = {
0: 'none',
1: 'small',
2: 'medium',
3: 'big',
}[widgetValue];
this._rpc({
route: '/shop/config/website',
params: {
'product_page_image_spacing': spacing,
},
}).then(() => this.trigger_up('request_save', {reload: true, optionSelector: this.data.selector}));
this.$target.data('image_spacing', spacing);
},
setColumns(previewMode, widgetValue, params) {
this._rpc({
route: '/shop/config/website',
params: {
'product_page_grid_columns': widgetValue,
},
}).then(() => this.trigger_up('request_save', {reload: true, optionSelector: this.data.selector}));
this.$target.data('grid_columns', widgetValue);
},
/**
* @override
*/
async _computeWidgetState(methodName, params) {
switch (methodName) {
case 'setSpacing':
return {
'none': 0,
'small': 1,
'medium': 2,
'big': 3,
}[this.$target.data('image_spacing')];
case 'setColumns':
return this.$target.data('grid_columns');
}
return this._super(methodName, params);
}
});
options.registry.WebsiteSaleProductAttribute = options.Class.extend({
/**
* @override
@@ -36,7 +36,10 @@ const cartHandlerMixin = {
}).then(async data => {
sessionStorage.setItem('website_sale_cart_quantity', data.cart_quantity);
if (data.cart_quantity && (data.cart_quantity !== parseInt($(".my_cart_quantity").text()))) {
await animateClone($('header .o_wsale_my_cart').first(), this.$itemImgContainer, 25, 40);
// No animation if the product's page images are hidden
if ($('div[data-image_width]').data('image_width') !== 'none') {
await animateClone($('header .o_wsale_my_cart').first(), this.$itemImgContainer, 25, 40);
}
updateCartNavBar(data);
}
});
@@ -185,6 +185,18 @@
data-reload="/"/>
</div>
<!-- Product image -->
<div data-js="WebsiteSaleProductPage" data-selector="#product_detail_main" data-no-check="true">
<we-button-group string="Images Width" data-no-preview="true" data-reload="/">
<we-button data-set-image-width="none" data-img="/website_sale/static/src/img/snippet_options/image-width-none.svg" title="None"/>
<we-button data-set-image-width="50_pc" data-img="/website_sale/static/src/img/snippet_options/image-width-50.svg" title="50 percent"/>
<we-button data-set-image-width="66_pc" data-img="/website_sale/static/src/img/snippet_options/image-width-66.svg" title="66 percent"/>
<we-button data-set-image-width="100_pc" data-img="/website_sale/static/src/img/snippet_options/image-width-100.svg" title="100 percent"/>
</we-button-group>
<we-select string="Layout" data-no-preview="true" data-reload="/">
<we-button data-set-image-layout="carousel">Carousel</we-button>
<we-button data-set-image-layout="grid">Grid</we-button>
</we-select>
</div>
<div data-selector="#product_detail #o-carousel-product" data-no-check="true">
<we-select string="Image Zoom" data-no-preview="true" data-reload="/">
<we-button data-customize-website-views="">None</we-button>
@@ -196,6 +208,14 @@
<we-button class="fa fa-fw fa-long-arrow-down" title="Bottom" data-customize-website-views="website_sale.carousel_product_indicators_bottom"/>
</we-button-group>
</div>
<div data-js="WebsiteSaleProductPageGrid" data-selector="#product_detail #o-grid-product" data-no-check="true">
<we-range string="Image Spacing" data-no-preview="true" data-reload="/" data-max="3" data-step="1" data-set-spacing=""/>
<we-select string="Columns" data-no-preview="true" data-reload="/">
<we-button data-set-columns="1">1</we-button>
<we-button data-set-columns="2">2</we-button>
<we-button data-set-columns="3">3</we-button>
</we-select>
</div>
<div data-js="WebsiteSaleProductAttribute" data-selector="#product_detail .o_wsale_product_attribute" data-no-check="true">
<we-select string="Display Type" data-no-preview="true">
<we-button data-set-display-type="radio">Radio</we-button>
+46 -29
View File
@@ -624,11 +624,14 @@
</div>
</div>
</div>
<div class="row">
<div class="col-lg-6 mt-lg-4">
<t t-call="website_sale.shop_product_carousel"/>
<div class="row" id="product_detail_main" data-name="Product Page"
t-att-data-image_width="website.product_page_image_width"
t-att-data-image_layout="website.product_page_image_layout">
<t t-set="image_cols" t-value="website._get_product_page_proportions()"/>
<div t-attf-class="col-lg-#{image_cols[0]} mt-lg-4 o_wsale_product_images" t-if="website.product_page_image_width != 'none'">
<t t-call="website_sale.shop_product_images"/>
</div>
<div class="col-lg-6 mt-md-4" id="product_details">
<div t-attf-class="col-lg-#{image_cols[1]} mt-md-4" id="product_details">
<h1 itemprop="name" t-field="product.name">Product Name</h1>
<span itemprop="url" style="display:none;" t-esc="product.website_url"/>
<t t-if="is_view_active('website_sale.product_comment')">
@@ -1975,27 +1978,38 @@
</xpath>
</template>
<template id="website_sale.shop_product_carousel" name="Shop Product Carousel">
<!-- Product page images -->
<template id="website_sale.shop_product_images" name="Shop Product Images">
<t t-set="product_images" t-value="product_variant._get_images() if product_variant else product._get_images()"/>
<t t-set="product_carousel_block_name">Product Image</t>
<t t-call="website_sale.shop_product_#{website.product_page_image_layout}"/>
</template>
<template id="website_sale.shop_product_image">
<div t-if="product_image._name == 'product.image' and product_image.embed_code" t-att-class="image_classes + 'ratio ratio-16x9'">
<t t-out="product_image.embed_code"/>
</div>
<div t-else="" t-field="product_image.image_1920" t-att-class="image_classes + ' oe_unmovable'" t-options='{"widget": "image", "preview_image": "image_1024", "class": "oe_unmovable product_detail_img mh-100", "alt-field": "name", "zoom": product_image.can_image_1024_be_zoomed and "image_1920", "itemprop": "image"}'/>
</template>
<!-- Product page images: Carousel -->
<template id="website_sale.shop_product_carousel" name="Shop Product Carousel">
<t t-set="product_carousel_block_name">Product Carousel</t>
<div id="o-carousel-product" class="carousel slide position-sticky mb-3 overflow-hidden" data-bs-ride="carousel" data-bs-interval="0" t-att-data-name="product_carousel_block_name">
<div class="o_carousel_product_outer carousel-outer position-relative flex-grow-1">
<div class="carousel-inner h-100">
<t t-set="image_classes" t-value="'d-flex align-items-center justify-content-center h-100'"/>
<t t-foreach="product_images" t-as="product_image">
<div t-attf-class="carousel-item h-100#{' active' if product_image_first else ''}">
<div t-if="product_image._name == 'product.image' and product_image.embed_code" class="d-flex align-items-center justify-content-center h-100 ratio ratio-16x9">
<t t-out="product_image.embed_code"/>
</div>
<div t-else="" t-field="product_image.image_1920" class="d-flex align-items-center justify-content-center h-100" t-options='{"widget": "image", "preview_image": "image_1024", "class": "product_detail_img mh-100", "alt-field": "name", "zoom": product_image.can_image_1024_be_zoomed and "image_1920", "itemprop": "image"}'/>
<t t-call="website_sale.shop_product_image"/>
</div>
</t>
</div>
<t t-if="len(product_images) > 1">
<a class="carousel-control-prev" href="#o-carousel-product" role="button" data-bs-slide="prev">
<span class="fa fa-chevron-left fa-2x" role="img" aria-label="Previous" title="Previous"/>
<span class="fa fa-chevron-left fa-2x oe_unmovable" role="img" aria-label="Previous" title="Previous"/>
</a>
<a class="carousel-control-next" href="#o-carousel-product" role="button" data-bs-slide="next">
<span class="fa fa-chevron-right fa-2x" role="img" aria-label="Next" title="Next"/>
<span class="fa fa-chevron-right fa-2x oe_unmovable" role="img" aria-label="Next" title="Next"/>
</a>
</t>
</div>
@@ -2035,23 +2049,26 @@
</div>
</template>
<record id="view_website_sale_website_form" model="ir.ui.view">
<field name="name">website_sale.website.form</field>
<field name="model">website</field>
<field name="inherit_id" ref="website.view_website_form"/>
<field name="arch" type="xml">
<xpath expr="//notebook" position="inside">
<page string="Product Page Extra Fields" groups="base.group_no_one">
<field name="shop_extra_field_ids" context="{'default_website_id': active_id}">
<tree editable="bottom">
<field name="sequence" widget="handle"/>
<field name="field_id"/>
</tree>
</field>
</page>
</xpath>
</field>
</record>
<!-- Product page images: Grid -->
<template id="website_sale.shop_product_grid" name="Shop Product Grid">
<div id="o-grid-product" class="o_wsale_product_page_grid mb-3" data-name="Product Grid" t-att-data-image_spacing="website.product_page_image_spacing" t-att-data-grid_columns="website.product_page_grid_columns">
<div class="container">
<!-- One row for every two images -->
<t t-set="image_classes" t-value="'w-100'"/>
<t t-set="col_classes" t-value="website._get_product_page_grid_image_classes()"/>
<t t-foreach="range(ceil(len(product_images) / website.product_page_grid_columns))" t-as="row_idx">
<div class="row m-0">
<t t-foreach="range(row_idx * website.product_page_grid_columns, (row_idx + 1) * website.product_page_grid_columns)" t-as="image_idx">
<t t-set="product_image" t-value="image_idx &lt; len(product_images) and product_images[image_idx] or False"/>
<div t-if="product_image" t-att-class="col_classes">
<t t-call="website_sale.shop_product_image"/>
</div>
</t>
</div>
</t>
</div>
</div>
</template>
<template id="ecom_show_extra_fields" inherit_id="website_sale.product" active="True" name="Show Extra Fields">
<xpath expr="//div[@id='product_details']" position="inside">
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<record id="view_website_sale_website_form" model="ir.ui.view">
<field name="name">website_sale.website.form</field>
<field name="model">website</field>
<field name="inherit_id" ref="website.view_website_form"/>
<field name="arch" type="xml">
<xpath expr="//notebook" position="inside">
<page string="Product Page Extra Fields" groups="base.group_no_one">
<field name="shop_extra_field_ids" context="{'default_website_id': active_id}">
<tree editable="bottom">
<field name="sequence" widget="handle"/>
<field name="field_id"/>
</tree>
</field>
</page>
</xpath>
</field>
</record>
</odoo>