From c058f19bfa33b510addfed314aa27d7e5c7fdb18 Mon Sep 17 00:00:00 2001 From: Samuel Degueldre Date: Wed, 11 Mar 2020 07:36:03 +0000 Subject: [PATCH] [IMP] website_sale: let users create and edit custom product ribbons Previously, there was only one static "Sale" ribbon. Users might want to have many types of ribbons, for different levels of sales, or to indicate that a product is sold out. This commit lets the user create custom ribbons and use them on prodcuts on the shop page. Additionally, setting a ribbon on a product used to require a reload, this is no longer the case. All ribbon modifications are stored in a cache and the RPCs to put them into effect are only done on save. task-1981170 Part of odoo/odoo#47574 Related: odoo/upgrade#1079 Related: odoo/design-themes#219 Signed-off-by: Quentin Smetz (qsm) --- addons/website/static/src/scss/website.scss | 37 ++ .../static/src/css/website_event_track.css | 32 +- .../views/event_track_templates.xml | 12 +- addons/website_sale/controllers/main.py | 24 +- addons/website_sale/data/data.xml | 16 +- addons/website_sale/models/product.py | 14 +- .../website_sale/security/ir.model.access.csv | 3 +- .../static/src/js/website_sale.editor.js | 410 ++++++++++++++++-- .../static/src/scss/website_sale.editor.scss | 6 + .../static/src/scss/website_sale.scss | 27 -- .../static/src/xml/website_sale_utils.xml | 9 + addons/website_sale/views/product_views.xml | 2 +- addons/website_sale/views/snippets.xml | 26 +- addons/website_sale/views/templates.xml | 11 +- 14 files changed, 502 insertions(+), 127 deletions(-) diff --git a/addons/website/static/src/scss/website.scss b/addons/website/static/src/scss/website.scss index 520102c1f7c..321f7792112 100644 --- a/addons/website/static/src/scss/website.scss +++ b/addons/website/static/src/scss/website.scss @@ -959,3 +959,40 @@ header { background-color: rgba(theme-color('primary'), 0.8); } } + +$ribbon-padding: 100px; +.o_ribbon { + margin: 0; + font-size: 1rem; + font-weight: bold; + white-space: nowrap; + text-align: center; + pointer-events: none; +} + +.o_ribbon_right { + @include o-position-absolute($top: 0, $right: 0); + padding: 0.5rem $ribbon-padding; + // 0.708 is 1 - cos(45deg) + // Transforms are applied right-to-left + // Cannot use matrix because of the use of % values. + transform: translateX(calc(-0.708 * (100% - #{2 * $ribbon-padding}))) rotate(45deg) translateX(calc(100% - #{$ribbon-padding})); + transform-origin: top right; +} + +.o_ribbon_left { + @include o-position-absolute($top: 0, $left: 0); + padding: 0.5rem $ribbon-padding; + transform: translateX(calc(0.708 * (100% - #{2 * $ribbon-padding}) - 100%)) rotate(-45deg) translateX($ribbon-padding); + transform-origin: top right; +} + +.o_tag_right { + @include o-position-absolute($top: 0, $right: 0); + padding: 0.25rem 1rem; +} + +.o_tag_left { + @include o-position-absolute($top: 0, $left: 0); + padding: 0.25rem 1rem; +} diff --git a/addons/website_event_track/static/src/css/website_event_track.css b/addons/website_event_track/static/src/css/website_event_track.css index 865dc812980..0940ceea082 100644 --- a/addons/website_event_track/static/src/css/website_event_track.css +++ b/addons/website_event_track/static/src/css/website_event_track.css @@ -1,32 +1,20 @@ -.o_wevent_event .ribbon-wrapper { - width: 60px; - height: 60px; - z-index: 5; +.o_wevent_event .o_wevent_sponsor { + position: relative; + display: inline-block; overflow: hidden; - position: absolute; - top: 0; - right: 0; } -.o_wevent_event .ribbon { - font: bold 13px Sans-Serif; +.o_wevent_event .o_ribbon { + font: 12px Sans-Serif; color: #404040; - text-align: center; - -webkit-transform: rotate(45deg); - -moz-transform: rotate(45deg); - -ms-transform: rotate(45deg); - -o-transform: rotate(45deg); - position: relative; - padding: 5px 0; - left: -4px; - top: 8px; - width: 90px; + padding-top: 2px; + padding-bottom: 2px; -webkit-box-shadow: 0px 0px 3px rgba(0, 0, 0, 0.3); -moz-box-shadow: 0px 0px 3px rgba(0, 0, 0, 0.3); box-shadow: 0px 0px 3px rgba(0, 0, 0, 0.3); } -.o_wevent_event .ribbon.ribbon_Gold { +.o_wevent_event .ribbon_Gold { background-color: #FDE21B; background-image: -webkit-gradient(linear, left top, left bottom, from(#E9CE0C), to(#FDE21B)); background-image: -webkit-linear-gradient(top, #E9CE0C, #FDE21B); @@ -35,7 +23,7 @@ background-image: -o-linear-gradient(top, #E9CE0C, #FDE21B); } -.o_wevent_event .ribbon.ribbon_Silver { +.o_wevent_event .ribbon_Silver { background-color: #CCCCCC; background-image: -webkit-gradient(linear, left top, left bottom, from(#BBBBBB), to(#CCCCCC)); background-image: -webkit-linear-gradient(top, #BBBBBB, #CCCCCC); @@ -44,7 +32,7 @@ background-image: -o-linear-gradient(top, #BBBBBB, #CCCCCC); } -.o_wevent_event .ribbon.ribbon_Bronze { +.o_wevent_event .ribbon_Bronze { background-color: #DB9141; background-image: -webkit-gradient(linear, left top, left bottom, from(#C2792A), to(#DB9141)); background-image: -webkit-linear-gradient(top, #C2792A, #DB9141); diff --git a/addons/website_event_track/views/event_track_templates.xml b/addons/website_event_track/views/event_track_templates.xml index c9003a9e3bf..020b25c1923 100644 --- a/addons/website_event_track/views/event_track_templates.xml +++ b/addons/website_event_track/views/event_track_templates.xml @@ -18,21 +18,17 @@
- + -
-
-
+
- + -
-
-
+
diff --git a/addons/website_sale/controllers/main.py b/addons/website_sale/controllers/main.py index f3901529aa7..6aa56b87e99 100644 --- a/addons/website_sale/controllers/main.py +++ b/addons/website_sale/controllers/main.py @@ -71,7 +71,7 @@ class TableCompute(object): self.table[(pos // ppr) + y2][(pos % ppr) + x2] = False self.table[pos // ppr][pos % ppr] = { 'product': p, 'x': x, 'y': y, - 'class': " ".join(x.html_class for x in p.website_style_ids if x.html_class) + 'ribbon': p.website_ribbon_id, } if index <= ppg: maxy = max(maxy, y + (pos // ppr)) @@ -1054,28 +1054,6 @@ class WebsiteSale(http.Controller): }) return "%s?enable_editor=1" % product.product_tmpl_id.website_url - @http.route(['/shop/change_styles'], type='json', auth='user') - def change_styles(self, id, style_id): - product = request.env['product.template'].browse(id) - - remove = [] - active = False - style_id = int(style_id) - for style in product.website_style_ids: - if style.id == style_id: - remove.append(style.id) - active = True - break - - style = request.env['product.style'].browse(style_id) - - if remove: - product.write({'website_style_ids': [(3, rid) for rid in remove]}) - if not active: - product.write({'website_style_ids': [(4, style.id)]}) - - return not active - @http.route(['/shop/change_sequence'], type='json', auth='user') def change_sequence(self, id, sequence): product_tmpl = request.env['product.template'].browse(id) diff --git a/addons/website_sale/data/data.xml b/addons/website_sale/data/data.xml index e110dd493e6..c2fddedad54 100644 --- a/addons/website_sale/data/data.xml +++ b/addons/website_sale/data/data.xml @@ -18,9 +18,19 @@ open - - Sale Ribbon - oe_ribbon_promo + + Sale + bg-success o_ribbon_left + + + + Sold out + bg-danger o_ribbon_left + + + + New! + bg-primary o_ribbon_left diff --git a/addons/website_sale/models/product.py b/addons/website_sale/models/product.py index effe4749833..57398dc3130 100644 --- a/addons/website_sale/models/product.py +++ b/addons/website_sale/models/product.py @@ -9,12 +9,14 @@ from odoo.tools.translate import html_translate from odoo.osv import expression -class ProductStyle(models.Model): - _name = "product.style" - _description = 'Product Style' +class ProductRibbon(models.Model): + _name = "product.ribbon" + _description = 'Product ribbon' - name = fields.Char(string='Style Name', required=True) - html_class = fields.Char(string='HTML Classes') + html = fields.Char(string='Ribbon html', required=True) + bg_color = fields.Char(string='Ribbon background color', required=False) + text_color = fields.Char(string='Ribbon text color', required=False) + html_class = fields.Char(string='Ribbon class', required=True, default='') class ProductPricelist(models.Model): @@ -186,7 +188,7 @@ class ProductTemplate(models.Model): help='Accessories show up when the customer reviews the cart before payment (cross-sell strategy).') website_size_x = fields.Integer('Size X', default=1) website_size_y = fields.Integer('Size Y', default=1) - website_style_ids = fields.Many2many('product.style', string='Styles') + website_ribbon_id = fields.Many2one('product.ribbon', string='Ribbon') website_sequence = fields.Integer('Website Sequence', help="Determine the display order in the Website E-commerce", default=lambda self: self._default_website_sequence(), copy=False) public_categ_ids = fields.Many2many( diff --git a/addons/website_sale/security/ir.model.access.csv b/addons/website_sale/security/ir.model.access.csv index 0e3f19844c8..479a8838622 100644 --- a/addons/website_sale/security/ir.model.access.csv +++ b/addons/website_sale/security/ir.model.access.csv @@ -6,7 +6,8 @@ access_product_category_pos_manager,product.public.category manager,model_produc access_product_public_category_public,product.category.public,model_product_public_category,,1,0,0,0 access_product_pricelist_public,product.pricelist.public,product.model_product_pricelist,,1,0,0,0 access_product_pricelist_item_public,product.pricelist.item.public,product.model_product_pricelist_item,,1,0,0,0 -access_product_style,product.style.public,website_sale.model_product_style,,1,0,0,0 +access_product_ribbon_public,product.ribbon.public,website_sale.model_product_ribbon,,1,0,0,0 +access_product_ribbon_sale_manager,product.ribbon.sale_manager,website_sale.model_product_ribbon,sales_team.group_sale_manager,1,1,1,1 access_product_attribute_public,product.attribute public,product.model_product_attribute,,1,0,0,0 access_product_attribute_value_public,product.attribute value public,product.model_product_attribute_value,,1,0,0,0 access_product_product_attribute,product.template.attribute value public,product.model_product_template_attribute_value,,1,0,0,0 diff --git a/addons/website_sale/static/src/js/website_sale.editor.js b/addons/website_sale/static/src/js/website_sale.editor.js index e9188bbad7d..4b1a7946ec9 100644 --- a/addons/website_sale/static/src/js/website_sale.editor.js +++ b/addons/website_sale/static/src/js/website_sale.editor.js @@ -54,6 +54,197 @@ odoo.define('website_sale.editor', function (require) { var options = require('web_editor.snippets.options'); var publicWidget = require('web.public.widget'); +const {Class: EditorMenuBar} = require('web_editor.editor'); +const {qweb} = require('web.core'); + +EditorMenuBar.include({ + custom_events: Object.assign(EditorMenuBar.prototype.custom_events, { + get_ribbons: '_onGetRibbons', + get_ribbon_classes: '_onGetRibbonClasses', + delete_ribbon: '_onDeleteRibbon', + set_ribbon: '_onSetRibbon', + set_product_ribbon: '_onSetProductRibbon', + }), + + /** + * @override + */ + async willStart() { + const _super = this._super.bind(this); + let ribbons = []; + if (this._isProductListPage()) { + ribbons = await this._rpc({ + model: 'product.ribbon', + method: 'search_read', + fields: ['id', 'html', 'bg_color', 'text_color', 'html_class'], + }); + } + this.ribbons = Object.fromEntries(ribbons.map(ribbon => [ribbon.id, ribbon])); + this.originalRibbons = Object.assign({}, this.ribbons); + this.productTemplatesRibbons = []; + this.deletedRibbonClasses = ''; + return _super(...arguments); + }, + /** + * @override + */ + async save() { + const _super = this._super.bind(this); + await this._saveRibbons(); + return _super(...arguments); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Saves the ribbons in the database. + * + * @private + */ + async _saveRibbons() { + if (!this._isProductListPage()) { + return; + } + const originalIds = Object.keys(this.originalRibbons).map(id => parseInt(id)); + const currentIds = Object.keys(this.ribbons).map(id => parseInt(id)); + + const ribbons = Object.values(this.ribbons); + const created = ribbons.filter(ribbon => !originalIds.includes(ribbon.id)); + const deletedIds = originalIds.filter(id => !currentIds.includes(id)); + const modified = ribbons.filter(ribbon => { + if (created.includes(ribbon)) { + return false; + } + const original = this.originalRibbons[ribbon.id]; + return Object.entries(ribbon).some(([key, value]) => value !== original[key]); + }); + + const proms = []; + let createdRibbonIds; + if (created.length > 0) { + proms.push(this._rpc({ + method: 'create', + model: 'product.ribbon', + args: [created.map(ribbon => { + ribbon = Object.assign({}, ribbon); + delete ribbon.id; + return ribbon; + })], + }).then(ids => createdRibbonIds = ids)); + } + + modified.forEach(ribbon => proms.push(this._rpc({ + method: 'write', + model: 'product.ribbon', + args: [[ribbon.id], ribbon], + }))); + + if (deletedIds.length > 0) { + proms.push(this._rpc({ + method: 'unlink', + model: 'product.ribbon', + args: [deletedIds], + })); + } + + await Promise.all(proms); + const localToServer = Object.assign( + this.ribbons, + Object.fromEntries(created.map((ribbon, index) => [ribbon.id, {id: createdRibbonIds[index]}])), + {'false': {id: false}}, + ); + + // Building the final template to ribbon-id map + const finalTemplateRibbons = this.productTemplatesRibbons.reduce((acc, {templateId, ribbonId}) => { + acc[templateId] = ribbonId; + return acc; + }, {}); + // Inverting the relationship so that we have all templates that have the same ribbon to reduce RPCs + const ribbonTemplates = Object.entries(finalTemplateRibbons).reduce((acc, [templateId, ribbonId]) => { + if (!acc[ribbonId]) { + acc[ribbonId] = []; + } + acc[ribbonId].push(parseInt(templateId)); + return acc; + }, {}); + const setProductTemplateRibbons = Object.entries(ribbonTemplates) + // If the ribbonId that the template had no longer exists, remove the ribbon (id = false) + .map(([ribbonId, templateIds]) => { + const id = currentIds.includes(parseInt(ribbonId)) ? ribbonId : false; + return [id, templateIds]; + }).map(([ribbonId, templateIds]) => this._rpc({ + method: 'write', + model: 'product.template', + args: [templateIds, {'website_ribbon_id': localToServer[ribbonId].id}], + })); + return Promise.all(setProductTemplateRibbons); + }, + /** + * Checks whether the current page is the product list. + * + * @private + */ + _isProductListPage() { + return $('#products_grid').length !== 0; + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * Returns a copy of this.ribbons through a callback. + * + * @private + */ + _onGetRibbons(ev) { + ev.data.callback(Object.assign({}, this.ribbons)); + }, + /** + * Returns all ribbon classes, current and deleted, so they can be removed. + * + * @private + */ + _onGetRibbonClasses(ev) { + const classes = Object.values(this.ribbons).reduce((classes, ribbon) => { + return classes + ` ${ribbon.html_class}`; + }, '') + this.deletedRibbonClasses; + ev.data.callback(classes); + }, + /** + * Deletes a ribbon. + * + * @private + */ + _onDeleteRibbon(ev) { + this.deletedRibbonClasses += ` ${this.ribbons[ev.data.id].html_class}`; + delete this.ribbons[ev.data.id]; + }, + /** + * Sets a ribbon; + * + * @private + */ + _onSetRibbon(ev) { + const {ribbon} = ev.data; + const previousRibbon = this.ribbons[ribbon.id]; + if (previousRibbon) { + this.deletedRibbonClasses += ` ${previousRibbon.html_class}`; + } + this.ribbons[ribbon.id] = ribbon; + }, + /** + * Sets which ribbon is used by a product template. + * + * @private + */ + _onSetProductRibbon(ev) { + const {templateId, ribbonId} = ev.data; + this.productTemplatesRibbons.push({templateId, ribbonId}); + }, +}); publicWidget.registry.websiteSaleCurrency = publicWidget.Widget.extend({ selector: '.oe_website_sale', @@ -154,6 +345,7 @@ options.registry.WebsiteSaleGridLayout = options.Class.extend({ }); options.registry.WebsiteSaleProductsItem = options.Class.extend({ + xmlDependencies: (options.Class.prototype.xmlDependencies || []).concat(['/website_sale/static/src/xml/website_sale_utils.xml']), events: _.extend({}, options.Class.prototype.events || {}, { 'mouseenter .o_wsale_soptions_menu_sizes table': '_onTableMouseEnter', 'mouseleave .o_wsale_soptions_menu_sizes table': '_onTableMouseLeave', @@ -164,10 +356,18 @@ options.registry.WebsiteSaleProductsItem = options.Class.extend({ /** * @override */ - willStart: function () { + willStart: async function () { + const _super = this._super.bind(this); this.ppr = this.$target.closest('[data-ppr]').data('ppr'); this.productTemplateID = parseInt(this.$target.find('[data-oe-model="product.template"]').data('oe-id')); - + this.ribbons = await new Promise(resolve => this.trigger_up('get_ribbons', {callback: resolve})); + return _super(...arguments); + }, + /** + * @override + */ + start: function () { + this._resetRibbonDummy(); return this._super(...arguments); }, /** @@ -177,23 +377,122 @@ options.registry.WebsiteSaleProductsItem = options.Class.extend({ var listLayoutEnabled = this.$target.closest('#products_grid').hasClass('o_wsale_layout_list'); this.$el.find('.o_wsale_soptions_menu_sizes') .toggleClass('d-none', listLayoutEnabled); + // Ribbons may have been edited or deleted in another products' option, need to make sure they're up to date + this.rerender = true; }, //-------------------------------------------------------------------------- // Options //-------------------------------------------------------------------------- + /** + * @override + */ + selectStyle(previewMode, widgetValue, params) { + const proms = [this._super(...arguments)]; + if (params.cssProperty === 'background-color' && params.colorNames.includes(widgetValue)) { + // Reset text-color when choosing a background-color class, so it uses the automatic text-color of the class. + proms.push(this.selectStyle(previewMode, '', {applyTo: '.o_wsale_ribbon_dummy', cssProperty: 'color'})); + } + return Promise.all(proms); + }, /** * @see this.selectClass for params */ - style: function (previewMode, widgetValue, params) { - this._rpc({ - route: '/shop/change_styles', - params: { - 'id': this.productTemplateID, - 'style_id': params.possibleValues[params.possibleValues.length - 1], - }, + async setRibbon(previewMode, widgetValue, params) { + this.$target[0].dataset.ribbonId = widgetValue; + this.trigger_up('set_product_ribbon', { + templateId: this.productTemplateID, + ribbonId: widgetValue || false, }); + const ribbon = this.ribbons[widgetValue] || {html: '', bg_color: '', text_color: '', html_class: ''}; + const $ribbons = $(`[data-ribbon-id="${widgetValue}"] .o_ribbon:not(.o_wsale_ribbon_dummy)`); + $ribbons.html(ribbon.html); + let htmlClasses; + this.trigger_up('get_ribbon_classes', {callback: classes => htmlClasses = classes}); + $ribbons.removeClass(htmlClasses); + + $ribbons.addClass(ribbon.html_class || ''); + $ribbons.css('color', ribbon.text_color); + $ribbons.css('background-color', ribbon.bg_color || ''); + + if (!this.ribbons[widgetValue]) { + $(`[data-ribbon-id="${widgetValue}"]`).each((index, product) => delete product.dataset.ribbonId); + } + this._resetRibbonDummy(); + }, + /** + * @see this.selectClass for params + */ + editRibbon(previewMode, widgetValue, params) { + this.saveMethod = 'modify'; + this._toggleEditingUI(true); + }, + /** + * @see this.selectClass for params + */ + createRibbon(previewMode, widgetValue, params) { + this.saveMethod = 'create'; + this.$ribbon.html('Ribbon text'); + this.$ribbon.addClass('bg-primary o_ribbon_left'); + this._toggleEditingUI(true); + this.isCreating = true; + }, + /** + * @see this.selectClass for params + */ + async deleteRibbon(previewMode, widgetValue, params) { + if (this.isCreating) { + // Ribbon doesn't exist yet, simply discard. + this.isCreating = false; + this._resetRibbonDummy(); + return this._toggleEditingUI(false); + } + const {ribbonId} = this.$target[0].dataset; + this.trigger_up('delete_ribbon', {id: ribbonId}); + this.ribbons = await new Promise(resolve => this.trigger_up('get_ribbons', {callback: resolve})); + this.rerender = true; + await this.setRibbon(false, ribbonId); + }, + /** + * @see this.selectClass for params + */ + async saveRibbon(previewMode, widgetValue, params) { + const text = this.$ribbon.html().trim(); + if (!text) { + return; + } + const ribbon = { + 'html': text, + 'bg_color': this.$ribbon[0].style.backgroundColor, + 'text_color': this.$ribbon[0].style.color, + 'html_class': this.$ribbon.attr('class').split(' ') + .filter(c => !['d-none', 'o_wsale_ribbon_dummy', 'o_ribbon'].includes(c)) + .join(' '), + }; + ribbon.id = this.saveMethod === 'modify' ? parseInt(this.$target[0].dataset.ribbonId) : Date.now(); + this.trigger_up('set_ribbon', {ribbon: ribbon}); + this.ribbons = await new Promise(resolve => this.trigger_up('get_ribbons', {callback: resolve})); + this.rerender = true; + await this.setRibbon(false, ribbon.id); + }, + /** + * @see this.selectClass for params + */ + setRibbonHtml(previewMode, widgetValue, params) { + this.$ribbon.html(widgetValue); + }, + /** + * @see this.selectClass for params + */ + setRibbonMode(previewMode, widgetValue, params) { + this.$ribbon[0].className = this.$ribbon[0].className.replace(/o_(ribbon|tag)_(left|right)/, `o_${widgetValue}_$2`); + }, + /** + * @see this.selectClass for params + */ + setRibbonPosition(previewMode, widgetValue, params) { + this.$ribbon[0].className = this.$ribbon[0].className.replace(/o_(ribbon|tag)_(left|right)/, `o_$1_${widgetValue}`); }, /** * @see this.selectClass for params @@ -227,6 +526,10 @@ options.registry.WebsiteSaleProductsItem = options.Class.extend({ // Adapt size array preview to fit ppr $size.find('tr td:nth-child(n + ' + parseInt(this.ppr + 1) + ')').hide(); + if (this.rerender) { + this.rerender = false; + return this._rerenderXML(); + } }, //-------------------------------------------------------------------------- @@ -236,29 +539,76 @@ options.registry.WebsiteSaleProductsItem = options.Class.extend({ /** * @override */ - _renderCustomWidgets: async function (uiFragment) { - const checkboxes = []; - return this._rpc({ - model: 'product.style', - method: 'search_read', - }).then(async data => { - for (var k in data) { - const checkboxWidget = this._registerUserValueWidget('we-checkbox', this, data[k]['name'], { - dataAttributes: { - 'style': data[k]['id'], - 'selectClass': data[k]['html_class'], - }, - }); - checkboxes.push(checkboxWidget); - await checkboxWidget.appendTo(document.createDocumentFragment()); - } - }).then(() => { - const menuEl = uiFragment.querySelector('[name="style"]'); - for (const checkbox of checkboxes) { - menuEl.appendChild(checkbox.el); - } + async _renderCustomXML(uiFragment) { + const $select = $(uiFragment.querySelector('.o_wsale_ribbon_select')); + this.ribbons = await new Promise(resolve => this.trigger_up('get_ribbons', {callback: resolve})); + if (!this.$ribbon) { + this._resetRibbonDummy(); + } + const classes = this.$ribbon[0].className; + this.$ribbon[0].className = ''; + const defaultTextColor = window.getComputedStyle(this.$ribbon[0]).color; + this.$ribbon[0].className = classes; + Object.values(this.ribbons).forEach(ribbon => { + const colorClasses = ribbon.html_class + .split(' ') + .filter(className => !/^o_(ribbon|tag)_(left|right)$/.test(className)) + .join(' '); + $select.append(qweb.render('website_sale.ribbonSelectItem', { + ribbon, + colorClasses, + isTag: /o_tag_(left|right)/.test(ribbon.html_class), + isLeft: /o_(tag|ribbon)_left/.test(ribbon.html_class), + textColor: ribbon.text_color || colorClasses ? 'currentColor' : defaultTextColor, + })); }); }, + /** + * @override + */ + async _computeWidgetState(methodName, params) { + const classList = this.$ribbon[0].classList; + switch (methodName) { + case 'setRibbon': + return this.$target.attr('data-ribbon-id') || ''; + case 'setRibbonHtml': + return this.$ribbon.html(); + case 'setRibbonMode': { + if (classList.contains('o_ribbon_left') || classList.contains('o_ribbon_right')) { + return 'ribbon'; + } + return 'tag'; + } + case 'setRibbonPosition': { + if (classList.contains('o_tag_left') || classList.contains('o_ribbon_left')) { + return 'left'; + } + return 'right'; + } + } + return this._super(methodName, params); + }, + /** + * Toggles the UI mode between select and create/edit mode. + * + * @private + * @param {Boolean} state true to activate editing UI, false to deactivate. + */ + _toggleEditingUI(state) { + this.$el.find('[data-name="ribbon_options"]').toggleClass('d-none', state); + this.$el.find('[data-name="ribbon_customize_opt"]').toggleClass('d-none', !state); + }, + /** + * Creates a copy of current ribbon to manipulate for edition/creation. + * + * @private + */ + _resetRibbonDummy() { + if (this.$ribbon) { + this.$ribbon.remove(); + } + this.$ribbon = this.$('.o_ribbon').clone().addClass('d-none o_wsale_ribbon_dummy').appendTo(this.$target); + }, //-------------------------------------------------------------------------- // Handlers diff --git a/addons/website_sale/static/src/scss/website_sale.editor.scss b/addons/website_sale/static/src/scss/website_sale.editor.scss index 729bde03af2..658ed4f5e42 100644 --- a/addons/website_sale/static/src/scss/website_sale.editor.scss +++ b/addons/website_sale/static/src/scss/website_sale.editor.scss @@ -26,6 +26,12 @@ } } +.o_wsale_color_preview { + width: 1em; + height: 1em; + border: 1px solid white; +} + .oe_drop_zone + .s_wsale_products_recently_viewed { display: block !important; } diff --git a/addons/website_sale/static/src/scss/website_sale.scss b/addons/website_sale/static/src/scss/website_sale.scss index cb9b44b8ed4..42b2006a1e0 100644 --- a/addons/website_sale/static/src/scss/website_sale.scss +++ b/addons/website_sale/static/src/scss/website_sale.scss @@ -132,28 +132,6 @@ $o-wsale-products-layout-grid-gutter-width: min($grid-gutter-width / 2, $o-wsale transform: scale(1); } } - - // Ribbon for promotions on products - .ribbon-wrapper { - display: none; - width: 85px; - height: 88px; - z-index: 5; - overflow: hidden; - @include o-position-absolute($left: 0, $top: 0); - } - .ribbon { - font: bold 15px Sans-Serif; - color: white; - text-align: center; - transform: rotate(-45deg); - position: relative; - padding: 7px 0; - left: -31px; - top: 14px; - width: 120px; - cursor: default; - } } // Options relative to where the product card is put @@ -167,11 +145,6 @@ $o-wsale-products-layout-grid-gutter-width: min($grid-gutter-width / 2, $o-wsale @include o-position-absolute(auto, 0, 0, 0); // The wrapper is always relatively positioned } } - - // Enable product promo ribbon - &.oe_ribbon_promo .ribbon-wrapper { - display: block; - } } #products_grid { diff --git a/addons/website_sale/static/src/xml/website_sale_utils.xml b/addons/website_sale/static/src/xml/website_sale_utils.xml index f1cf6a40c6a..8a470613325 100644 --- a/addons/website_sale/static/src/xml/website_sale_utils.xml +++ b/addons/website_sale/static/src/xml/website_sale_utils.xml @@ -37,4 +37,13 @@
+ + + + + + + + + diff --git a/addons/website_sale/views/product_views.xml b/addons/website_sale/views/product_views.xml index 2a7e1c81d94..f8387d9710b 100644 --- a/addons/website_sale/views/product_views.xml +++ b/addons/website_sale/views/product_views.xml @@ -94,7 +94,7 @@ - + diff --git a/addons/website_sale/views/snippets.xml b/addons/website_sale/views/snippets.xml index 41810ec0ff2..3a3dec8dd97 100644 --- a/addons/website_sale/views/snippets.xml +++ b/addons/website_sale/views/snippets.xml @@ -62,8 +62,30 @@
-
- + + + None + + + + + +
+ + + + + + + + + Slanted + Tag + + + Left + Right +
diff --git a/addons/website_sale/views/templates.xml b/addons/website_sale/views/templates.xml index 9a0cba9ea4f..e814f8570b8 100644 --- a/addons/website_sale/views/templates.xml +++ b/addons/website_sale/views/templates.xml @@ -132,9 +132,6 @@ itemscope="itemscope" itemtype="http://schema.org/Product">