diff options
| -rw-r--r-- | addons/website/static/src/scss/website.scss | 37 | ||||
| -rw-r--r-- | addons/website_event_track/static/src/css/website_event_track.css | 32 | ||||
| -rw-r--r-- | addons/website_event_track/views/event_track_templates.xml | 12 | ||||
| -rw-r--r-- | addons/website_sale/controllers/main.py | 24 | ||||
| -rw-r--r-- | addons/website_sale/data/data.xml | 16 | ||||
| -rw-r--r-- | addons/website_sale/models/product.py | 14 | ||||
| -rw-r--r-- | addons/website_sale/security/ir.model.access.csv | 3 | ||||
| -rw-r--r-- | addons/website_sale/static/src/js/website_sale.editor.js | 408 | ||||
| -rw-r--r-- | addons/website_sale/static/src/scss/website_sale.editor.scss | 6 | ||||
| -rw-r--r-- | addons/website_sale/static/src/scss/website_sale.scss | 27 | ||||
| -rw-r--r-- | addons/website_sale/static/src/xml/website_sale_utils.xml | 9 | ||||
| -rw-r--r-- | addons/website_sale/views/product_views.xml | 2 | ||||
| -rw-r--r-- | addons/website_sale/views/snippets.xml | 26 | ||||
| -rw-r--r-- | addons/website_sale/views/templates.xml | 11 |
14 files changed, 501 insertions, 126 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 @@ <div class="row"> <div t-attf-class="col-sm-6 col-lg-#{(len(event.sponsor_ids) > 6) and 2 or (12 // len(event.sponsor_ids))} text-center mb16 oe_sponsor" t-foreach="event.sponsor_ids" t-as="sponsor"> <t t-if="sponsor.url"> - <a t-att-href="sponsor.url" style="position: relative; display: inline-block;" class="bg-white"> + <a t-att-href="sponsor.url" class="bg-white o_wevent_sponsor"> <span t-field="sponsor.image_128" t-options='{"widget": "image", "class": "shadow"}'/> - <div class="ribbon-wrapper"> - <div t-field="sponsor.sponsor_type_id" t-attf-class="ribbon ribbon_#{sponsor.sponsor_type_id.name}"/> - </div> + <span t-field="sponsor.sponsor_type_id" t-attf-class="o_ribbon o_ribbon_right ribbon_#{sponsor.sponsor_type_id.name}"/> </a> </t> <t t-if="not sponsor.url"> - <span style="position: relative; display: inline-block;" class="bg-white"> + <span class="bg-white o_wevent_sponsor"> <span t-field="sponsor.image_128" t-options='{"widget": "image", "class": "shadow"}'/> - <div class="ribbon-wrapper"> - <div t-field="sponsor.sponsor_type_id" t-attf-class="ribbon ribbon_#{sponsor.sponsor_type_id.name}"/> - </div> + <span t-field="sponsor.sponsor_type_id" t-attf-class="o_ribbon o_ribbon_right ribbon_#{sponsor.sponsor_type_id.name}"/> </span> </t> </div> 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 @@ <field name="state">open</field> </record> - <record id="website_sale.image_promo" model="product.style"> - <field name="name">Sale Ribbon</field> - <field name="html_class">oe_ribbon_promo</field> + <record id="website_sale.sale_ribbon" model="product.ribbon"> + <field name="html">Sale</field> + <field name="html_class">bg-success o_ribbon_left</field> + </record> + + <record id="website_sale.sold_out_ribbon" model="product.ribbon"> + <field name="html">Sold out</field> + <field name="html_class">bg-danger o_ribbon_left</field> + </record> + + <record id="website_sale.new_ribbon" model="product.ribbon"> + <field name="html">New!</field> + <field name="html_class">bg-primary o_ribbon_left</field> </record> <record id="sales_team.salesteam_website_sales" model="crm.team"> 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,6 +377,8 @@ 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; }, //-------------------------------------------------------------------------- @@ -184,16 +386,113 @@ options.registry.WebsiteSaleProductsItem = options.Class.extend({ //-------------------------------------------------------------------------- /** + * @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,28 +539,75 @@ 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()); + 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'; } - }).then(() => { - const menuEl = uiFragment.querySelector('[name="style"]'); - for (const checkbox of checkboxes) { - menuEl.appendChild(checkbox.el); + 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); }, //-------------------------------------------------------------------------- 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 @@ </t> </div> +<!-- Products Search Bar autocomplete item --> +<we-button t-name="website_sale.ribbonSelectItem" t-att-data-set-ribbon="ribbon.id"> + <t t-raw="ribbon.html"/> + <span t-attf-class="fa fa-#{isTag ? 'tag' : 'bookmark'} ml-auto"></span> + <span t-attf-class="fa fa-arrow-#{isLeft ? 'left' : 'right'} ml-1"></span> + <span t-attf-class="o_wsale_color_preview #{colorClasses} ml-1" t-attf-style="background-color: #{ribbon.bg_color}"></span> + <span t-attf-class="o_wsale_color_preview #{colorClasses} ml-1" t-attf-style="background-color: #{textColor} !important;"></span> +</we-button> + </templates> 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 @@ <field name="public_categ_ids" widget="many2many_tags" string="Categories"/> <field name="alternative_product_ids" widget="many2many_tags" domain="[('id', '!=', active_id), '|', ('company_id', '=', company_id), ('company_id', '=', False)]"/> <field name="accessory_product_ids" widget="many2many_tags"/> - <field name="website_style_ids" widget="many2many_tags" groups="base.group_no_one"/> + <field name="website_ribbon_id" groups="base.group_no_one"/> </group> </group> <group name="product_template_images" string="Extra Product Media"> 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 @@ </table> </div> - <div name="style" data-no-preview="true"> - <!-- Placeholder to be filled in JS --> + <we-row data-name="ribbon_options"> + <we-select string="Ribbon" data-no-preview="true" class="o_wsale_ribbon_select"> + <we-button data-set-ribbon="" data-name="no_ribbon_opt">None</we-button> + <!-- Ribbons are filled in JS --> + </we-select> + <we-button data-edit-ribbon="" class="fa fa-edit" data-no-preview="true" data-dependencies="!no_ribbon_opt"/> + <we-button data-create-ribbon="" class="fa fa-plus text-success" data-no-preview="true"/> + </we-row> + <div class="d-none" data-name="ribbon_customize_opt"> + <we-row string="Ribbon"> + <we-input data-set-ribbon-html="" data-apply-to=".o_wsale_ribbon_dummy" data-no-preview="true"/> + <we-button class="fa fa-check" data-save-ribbon="" title="Validate" data-no-preview="true"/> + <we-button class="fa fa-trash" data-delete-ribbon="" title="Delete" data-no-preview="true"/> + </we-row> + <we-colorpicker string="Background Color" title="" data-select-style="" data-apply-to=".o_wsale_ribbon_dummy" data-css-property="background-color" data-color-prefix="bg-" data-no-preview="true"/> + <we-colorpicker string="Text Color" title="" data-select-style="" data-apply-to=".o_wsale_ribbon_dummy" data-css-property="color" data-no-preview="true"/> + <we-select string="Mode"> + <we-button data-set-ribbon-mode="ribbon">Slanted</we-button> + <we-button data-set-ribbon-mode="tag">Tag</we-button> + </we-select> + <we-select string="Position"> + <we-button data-set-ribbon-position="left">Left</we-button> + <we-button data-set-ribbon-position="right">Right</we-button> + </we-select> </div> <div name="reordering" data-no-preview="true"> 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"> <div class="card-body p-1 oe_product_image"> <input type="hidden" name="csrf_token" t-att-value="request.csrf_token()" /> - <div class="ribbon-wrapper"> - <a href="#" role="button" class="ribbon btn btn-danger">Sale</a> - </div> <a t-att-href="product_href" class="d-block h-100" itemprop="url"> <span t-field="product.image_1920" t-options="{'widget': 'image', 'preview_image': 'image_1024' if product_image_big else 'image_256'}" @@ -156,6 +153,10 @@ </div> <div class="o_wsale_product_btn"/> </div> + <t t-set="bg_color" t-value="td_product['ribbon']['bg_color'] or ''"/> + <t t-set="text_color" t-value="td_product['ribbon']['text_color']"/> + <t t-set="bg_class" t-value="td_product['ribbon']['html_class']"/> + <span t-attf-class="o_ribbon #{bg_class}" t-attf-style="#{text_color and ('color: %s; ' % text_color)}#{bg_color and 'background-color:' + bg_color}" t-raw="td_product['ribbon']['html'] or ''"/> </form> </template> @@ -232,9 +233,11 @@ <t t-foreach="tr_product" t-as="td_product"> <t t-if="td_product"> <t t-set="product" t-value="td_product['product']" /> + <!-- We use t-attf-class here to allow easier customization --> <td t-att-colspan="td_product['x'] != 1 and td_product['x']" t-att-rowspan="td_product['y'] != 1 and td_product['y']" - t-attf-class="oe_product #{td_product['class']}"> + t-attf-class="oe_product" + t-att-data-ribbon-id="td_product['ribbon'].id"> <div t-attf-class="o_wsale_product_grid_wrapper o_wsale_product_grid_wrapper_#{td_product['x']}_#{td_product['y']}"> <t t-call="website_sale.products_item"> <t t-set="product_image_big" t-value="td_product['x'] + td_product['y'] > 2"/> |
