[IMP] website, website_sale: add dynamic snippets
* Implement new snippets that allow the user to choose a filter
and a template.
Three snippets have been created:
- Dynamic Snippet: Displays the data in a grid format
- Dynamic Carousel: Displays the date in a carousel
- Dynamic Products: Let the user pick a product category and
displays the products in a carousel
task-2276740
PR #53175
Signed-off-by: Jérémy Kersten (jke) <jke@openerp.com>
This commit is contained in:
@@ -75,6 +75,8 @@
|
||||
'views/snippets/s_mega_menu_multi_menus.xml',
|
||||
'views/snippets/s_mega_menu_menu_image_menu.xml',
|
||||
'views/snippets/s_google_map.xml',
|
||||
'views/snippets/s_dynamic_snippet.xml',
|
||||
'views/snippets/s_dynamic_snippet_carousel.xml',
|
||||
'views/website_views.xml',
|
||||
'views/website_visitor_views.xml',
|
||||
'views/res_config_settings_views.xml',
|
||||
|
||||
@@ -269,6 +269,21 @@ class Website(Home):
|
||||
]
|
||||
}
|
||||
|
||||
@http.route('/website/snippet/filters', type='json', auth='public', website=True)
|
||||
def get_dynamic_filter(self, filter_id, template_key, limit=None, search_domain=None):
|
||||
dynamic_filter = request.env['website.snippet.filter'].sudo().search(
|
||||
[('id', '=', filter_id)] + request.website.website_domain()
|
||||
)
|
||||
return dynamic_filter and dynamic_filter.render(template_key, limit, search_domain) or ''
|
||||
|
||||
@http.route('/website/snippet/filter_templates', type='json', auth='public', website=True)
|
||||
def get_dynamic_snippet_templates(self, filter_id=False):
|
||||
# todo: if filter_id.model -> filter template
|
||||
templates = request.env['ir.ui.view'].sudo().search_read(
|
||||
[['key', 'ilike', '.dynamic_filter_template_'], ['type', '=', 'qweb']], ['key', 'name']
|
||||
)
|
||||
return templates
|
||||
|
||||
# ------------------------------------------------------
|
||||
# Edit
|
||||
# ------------------------------------------------------
|
||||
|
||||
@@ -698,6 +698,70 @@
|
||||
<field name="type">url</field>
|
||||
<field name="url">/website/static/src/img/snippets_demo/s_popup.jpg</field>
|
||||
</record>
|
||||
|
||||
<!-- Filter for Dynamic Snippet -->
|
||||
<record id="dynamic_snippet_country_filter" model="ir.filters">
|
||||
<field name="name">Countries</field>
|
||||
<field name="model_id">res.country</field>
|
||||
<field name="user_id" eval="False" />
|
||||
</record>
|
||||
<!-- Template for Dynamic Snippet -->
|
||||
<template id="dynamic_filter_template_image_title_footer" name="Image Title Footer Card">
|
||||
<t t-foreach="records" t-as="record">
|
||||
<t t-set="fields" t-value="list(record['fields'].values())"/>
|
||||
<t t-set="fieldsImg" t-value="list(record['image_fields'].values())"/>
|
||||
<div class="card h-100" t-att-data-url="record['fields']['call_to_action_url']">
|
||||
<img t-if="len(fieldsImg) > 0" class="card-img-top p-3" loading="lazy" t-att-src="fieldsImg[0]"/>
|
||||
<div class="card-body">
|
||||
<h5 t-if="len(fields) > 0" class="card-title">
|
||||
<t t-raw="fields[0]"/>
|
||||
</h5>
|
||||
<div t-if="len(fields) > 1" class="card-text">
|
||||
<t t-raw="fields[1]"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer d-flex align-items-center">
|
||||
<div t-if="len(fields) > 2" class="card-text">
|
||||
<t t-raw="fields[2]"/>
|
||||
</div>
|
||||
<a t-if="call_to_action" class="btn btn-primary ml-auto" t-att-href="record['fields']['call_to_action_url']">
|
||||
<i class="fa fa-fw fa-eye"/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
<template id="dynamic_filter_template_header_image_footer_card" name="Header Image Footer Card">
|
||||
<t t-foreach="records" t-as="record">
|
||||
<t t-set="fields" t-value="list(record['fields'].values())"/>
|
||||
<t t-set="fieldsImg" t-value="list(record['image_fields'].values())"/>
|
||||
<div class="card h-100" t-att-data-url="record['fields']['call_to_action_url']">
|
||||
<h5 t-if="len(fields) > 0" class="card-header">
|
||||
<t t-raw="fields[0]"/>
|
||||
</h5>
|
||||
<div class="card-body">
|
||||
<img t-if="len(fieldsImg) > 0" class="card-img-top p-3" loading="lazy" t-att-src="fieldsImg[0]"/>
|
||||
<div t-if="len(fields) > 1" class="card-text">
|
||||
<t t-raw="fields[1]"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer d-flex align-items-center">
|
||||
<div t-if="len(fields) > 2" class="card-text">
|
||||
<t t-raw="fields[2]"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
<!-- Dynamic Snippet Data Source -->
|
||||
<record id="dynamic_snippet_data_source_country" model="website.snippet.filter">
|
||||
<field name="field_names">name,code,image,phone_code</field>
|
||||
<field name="filter_id" ref="website.dynamic_snippet_country_filter"/>
|
||||
<field name="limit" eval="16"/>
|
||||
<field name="name">Countries</field>
|
||||
<field name="website_id" ref="website.default_website"/>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
<data>
|
||||
<record id="group_multi_website" model="res.groups">
|
||||
|
||||
@@ -23,3 +23,4 @@ from . import res_config_settings
|
||||
from . import res_lang
|
||||
from . import theme_models
|
||||
from . import website_visitor
|
||||
from . import website_snippet_filter
|
||||
|
||||
@@ -4,6 +4,7 @@ from werkzeug import urls
|
||||
|
||||
from odoo import api, fields, models
|
||||
from odoo.http import request
|
||||
from odoo.tools.json import scriptsafe as json_scriptsafe
|
||||
|
||||
|
||||
class ServerAction(models.Model):
|
||||
@@ -48,6 +49,7 @@ class ServerAction(models.Model):
|
||||
eval_context = super(ServerAction, self)._get_eval_context(action)
|
||||
if action.state == 'code':
|
||||
eval_context['request'] = request
|
||||
eval_context['json'] = json_scriptsafe
|
||||
return eval_context
|
||||
|
||||
@api.model
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from ast import literal_eval
|
||||
from collections import OrderedDict
|
||||
from odoo import models, fields, api, _
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.osv import expression
|
||||
|
||||
|
||||
class WebsiteSnippetFilter(models.Model):
|
||||
_name = 'website.snippet.filter'
|
||||
_inherit = ['website.published.multi.mixin']
|
||||
_description = 'Website Snippet Filter'
|
||||
_order = 'name ASC'
|
||||
|
||||
name = fields.Char(required=True)
|
||||
action_server_id = fields.Many2one('ir.actions.server', 'Server Action', ondelete='cascade')
|
||||
field_names = fields.Char(help="A list of comma-separated field names", required=True)
|
||||
filter_id = fields.Many2one('ir.filters', 'Filter', ondelete='cascade')
|
||||
limit = fields.Integer(help='The limit is the maximum number of records retrieved', required=True)
|
||||
website_id = fields.Many2one('website', string='Website', ondelete='cascade', required=True)
|
||||
|
||||
@api.constrains('action_server_id', 'filter_id')
|
||||
def _check_data_source_is_provided(self):
|
||||
for record in self:
|
||||
if bool(record.action_server_id) == bool(record.filter_id):
|
||||
raise ValidationError(_("Either action_server_id or filter_id must be provided."))
|
||||
|
||||
@api.constrains('limit')
|
||||
def _check_limit(self):
|
||||
"""Limit must be between 1 and 16."""
|
||||
for record in self:
|
||||
if not 0 < record.limit <= 16:
|
||||
raise ValidationError(_("The limit must be between 1 and 16."))
|
||||
|
||||
@api.constrains('field_names')
|
||||
def _check_field_names(self):
|
||||
for record in self:
|
||||
for field_name in record.field_names.split(","):
|
||||
if not field_name.strip():
|
||||
raise ValidationError(_("Empty field name in %r") % (record.field_names))
|
||||
|
||||
def render(self, template_key, limit, search_domain=[]):
|
||||
"""Renders the website dynamic snippet items"""
|
||||
self.ensure_one()
|
||||
assert '.dynamic_filter_template_' in template_key, _("You can only use template prefixed by dynamic_filter_template_ ")
|
||||
|
||||
if self.env['website'].get_current_website() != self.website_id:
|
||||
return ''
|
||||
|
||||
records = self._prepare_values(limit, search_domain)
|
||||
return self.env['ir.ui.view'].sudo().with_context(inherit_branding=False)._render_template(template_key, dict(records=records))
|
||||
|
||||
def _prepare_values(self, limit=None, search_domain=[]):
|
||||
"""Gets the data and returns it the right format for render."""
|
||||
self.ensure_one()
|
||||
limit = limit and min(limit, self.limit) or self.limit
|
||||
if self.filter_id:
|
||||
filter_sudo = self.filter_id.sudo()
|
||||
domain = filter_sudo._get_eval_domain()
|
||||
if 'is_published' in self.env[filter_sudo.model_id]:
|
||||
domain = expression.AND([domain, [('is_published', '=', True)]])
|
||||
if search_domain:
|
||||
domain = expression.AND([domain, search_domain]),
|
||||
|
||||
records = self.env[filter_sudo.model_id].search(
|
||||
domain,
|
||||
order=','.join(literal_eval(filter_sudo.sort)) or None,
|
||||
limit=limit
|
||||
)
|
||||
elif self.action_server_id:
|
||||
records = self.action_server_id.with_context(
|
||||
dynamic_filter=self,
|
||||
limit=limit,
|
||||
search_domain=search_domain,
|
||||
).sudo().run()
|
||||
|
||||
return self._filter_records_to_dict_values(records)
|
||||
|
||||
def _filter_records_to_dict_values(self, records):
|
||||
"""Extract the fields from the data source and put them into a dictionary of values
|
||||
|
||||
[{
|
||||
'fields':
|
||||
OrderedDict([
|
||||
('name', 'Afghanistan'),
|
||||
('code', 'AF'),
|
||||
]),
|
||||
'image_fields':
|
||||
OrderedDict([
|
||||
('image', '/web/image/res.country/3/image?unique=5d9b44e')
|
||||
]),
|
||||
}, ... , ...]
|
||||
|
||||
"""
|
||||
|
||||
self.ensure_one()
|
||||
values = []
|
||||
model = self.env[self.filter_id.model_id] if self.filter_id else self.env[self.action_server_id.model_id.model]
|
||||
for record in records:
|
||||
data = {
|
||||
'fields': OrderedDict({}),
|
||||
'image_fields': OrderedDict({}),
|
||||
}
|
||||
|
||||
for field_name in self.field_names.split(","):
|
||||
field = model._fields.get(field_name)
|
||||
data_type = 'image_fields' if field.type == 'binary' else 'fields'
|
||||
if data_type == 'image_fields' and self.filter_id:
|
||||
data[data_type][field_name] = self.env['website'].image_url(record, field_name)
|
||||
else:
|
||||
data[data_type][field_name] = record[field_name]
|
||||
|
||||
data['fields']['call_to_action_url'] = 'website_url' in record and record['website_url']
|
||||
values.append(data)
|
||||
return values
|
||||
@@ -21,3 +21,5 @@ access_theme_ir_attachment,access_theme_ir_attachment,model_theme_ir_attachment,
|
||||
access_theme_website_menu,access_theme_website_menu,model_theme_website_menu,base.group_system,1,1,1,1
|
||||
access_theme_website_page,access_theme_website_page,model_theme_website_page,base.group_system,1,1,1,1
|
||||
access_website_robots,access.website.robots,model_website_robots,website.group_website_designer,1,1,1,0
|
||||
access_website_dynamic_filter_public,access_website_dynamic_filter,model_website_snippet_filter,,0,0,0,0
|
||||
access_website_dynamic_filter_system,access_website_dynamic_filter,model_website_snippet_filter,base.group_system,1,1,1,1
|
||||
|
||||
|
@@ -0,0 +1,245 @@
|
||||
odoo.define('website.s_dynamic_snippet', function (require) {
|
||||
'use strict';
|
||||
|
||||
const core = require('web.core');
|
||||
const config = require('web.config');
|
||||
const publicWidget = require('web.public.widget');
|
||||
|
||||
const DynamicSnippet = publicWidget.Widget.extend({
|
||||
selector: '.s_dynamic_snippet',
|
||||
xmlDependencies: ['/website/static/src/snippets/s_dynamic_snippet/000.xml'],
|
||||
disabledInEditableMode: false,
|
||||
|
||||
/**
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
init: function () {
|
||||
this._super.apply(this, arguments);
|
||||
/**
|
||||
* The dynamic filter data source data formatted with the chosen template.
|
||||
* Can be accessed when overriding the _render_content() function in order to generate
|
||||
* a new renderedContent from the original data.
|
||||
*
|
||||
* @type {*|jQuery.fn.init|jQuery|HTMLElement}
|
||||
*/
|
||||
this.$data = $();
|
||||
this.renderedContent = '';
|
||||
this.isDesplayedAsMobile = config.device.isMobile;
|
||||
this.uniqueId = _.uniqueId('s_dynamic_snippet_');
|
||||
this.template_key = 'website.s_dynamic_snippet.grid';
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
willStart: function () {
|
||||
return this._super.apply(this, arguments).then(
|
||||
() => Promise.all([
|
||||
this._fetchData(),
|
||||
this._manageWarningMessageVisibility()
|
||||
])
|
||||
);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
start: function () {
|
||||
return this._super.apply(this, arguments)
|
||||
.then(() => {
|
||||
this._setupCallToAction(true);
|
||||
this._setupSizeChangedManagement(true);
|
||||
this._render();
|
||||
this._toggleVisibility(true);
|
||||
});
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
destroy: function () {
|
||||
this._toggleVisibility(false);
|
||||
this._setupSizeChangedManagement(false);
|
||||
this._setupCallToAction(false);
|
||||
this._clearContent();
|
||||
this._super.apply(this, arguments);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_clearContent: function() {
|
||||
const $dynamicSnippetTemplate = this.$el.find('.dynamic_snippet_template');
|
||||
if ($dynamicSnippetTemplate) {
|
||||
$dynamicSnippetTemplate.html('');
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Method to be overridden in child components if additional configuration elements
|
||||
* are required in order to fetch data.
|
||||
* @private
|
||||
*/
|
||||
_isConfigComplete: function() {
|
||||
return this.$el.get(0).dataset.filterId !== undefined && this.$el.get(0).dataset.templateKey !== undefined;
|
||||
},
|
||||
/**
|
||||
* Method to be overridden in child components in order to provide a search
|
||||
* domain if needed.
|
||||
* @private
|
||||
*/
|
||||
_getSearchDomain: function () {
|
||||
return [];
|
||||
},
|
||||
/**
|
||||
* Fetches the data.
|
||||
* @private
|
||||
*/
|
||||
_fetchData: function () {
|
||||
if (this._isConfigComplete()) {
|
||||
return this._rpc(
|
||||
{
|
||||
'route': '/website/snippet/filters',
|
||||
'params': {
|
||||
'filter_id': parseInt(this.$el.get(0).dataset.filterId),
|
||||
'template_key': this.$el.get(0).dataset.templateKey,
|
||||
'limit': parseInt(this.$el.get(0).dataset.numberOfRecords),
|
||||
'search_domain': this._getSearchDomain()
|
||||
},
|
||||
})
|
||||
.then(
|
||||
(data) => {
|
||||
this.$data = $(data).filter((index, node) => node.nodeType === 1);
|
||||
}
|
||||
);
|
||||
} else {
|
||||
return new Promise((resolve) => {
|
||||
this.$data = $();
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_manageWarningMessageVisibility: async function () {
|
||||
this.$el.find('.missing_option_warning').toggleClass(
|
||||
'd-none',
|
||||
this._isConfigComplete()
|
||||
);
|
||||
},
|
||||
/**
|
||||
* Method to be overridden in child components in order to prepare content
|
||||
* before rendering.
|
||||
* @private
|
||||
*/
|
||||
_prepareContent: function () {
|
||||
if (this.$target[0].dataset.numberOfElements && this.$target[0].dataset.numberOfElementsSmallDevices) {
|
||||
this.renderedContent = core.qweb.render(
|
||||
this.template_key,
|
||||
this._getQWebRenderOptions());
|
||||
} else {
|
||||
this.renderedContent = '';
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Method to be overridden in child components in order to prepare QWeb
|
||||
* options.
|
||||
* @private
|
||||
*/
|
||||
_getQWebRenderOptions: function () {
|
||||
return {
|
||||
chunkSize: parseInt(
|
||||
config.device.isMobile
|
||||
? this.$target[0].dataset.numberOfElementsSmallDevices
|
||||
: this.$target[0].dataset.numberOfElements
|
||||
),
|
||||
data: this.$data.map((index, data) => data.outerHTML),
|
||||
uniqueId: this.uniqueId
|
||||
};
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_render: function () {
|
||||
if (this.$data.length > 0) {
|
||||
this._prepareContent();
|
||||
} else {
|
||||
this.renderedContent = '';
|
||||
}
|
||||
this._renderContent();
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_renderContent: function () {
|
||||
this.$el.find('.dynamic_snippet_template').html(this.renderedContent);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {Boolean} enable
|
||||
* @private
|
||||
*/
|
||||
_setupCallToAction: function (enable) {
|
||||
if (enable === true) {
|
||||
this.$el.on('click.s_dynamic_snippet', '*[data-url]', function (ev) {
|
||||
if (this.editableMode !== true) {
|
||||
window.location = $(ev.currentTarget).attr('data-url');
|
||||
}
|
||||
}.bind(this));
|
||||
} else {
|
||||
this.$el.off('.s_dynamic_snippet');
|
||||
}
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {Boolean} enable
|
||||
* @private
|
||||
*/
|
||||
_setupSizeChangedManagement: function (enable) {
|
||||
if (enable === true) {
|
||||
config.device.bus.on('size_changed', this, this._onSizeChanged);
|
||||
} else {
|
||||
config.device.bus.off('size_changed', this, this._onSizeChanged);
|
||||
}
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param visible
|
||||
* @private
|
||||
*/
|
||||
_toggleVisibility: function (visible) {
|
||||
this.$el.toggleClass('d-none', !visible);
|
||||
},
|
||||
|
||||
//------------------------------------- -------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Called when the size has reached a new bootstrap breakpoint.
|
||||
*
|
||||
* @private
|
||||
* @param {number} size as Integer @see web.config.device.SIZES
|
||||
*/
|
||||
_onSizeChanged: function (size) {
|
||||
if (this.isDesplayedAsMobile !== config.device.isMobile) {
|
||||
this.isDesplayedAsMobile = config.device.isMobile;
|
||||
this._render();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
publicWidget.registry.dynamic_snippet = DynamicSnippet;
|
||||
|
||||
return DynamicSnippet;
|
||||
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
body:not(.editor_enable) {
|
||||
.s_dynamic {
|
||||
*[data-url] {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.s_dynamic {
|
||||
.card-img-top {
|
||||
height: 12rem;
|
||||
}
|
||||
img {
|
||||
object-fit: scale-down;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="website.s_dynamic_snippet.grid">
|
||||
<!-- Content -->
|
||||
<t t-set="colClass" t-value="'col-' + (12 / chunkSize).toString()"/>
|
||||
<t t-set="rowIndexGenerator" t-value="Array.from(Array(Math.ceil(data.length/chunkSize)).keys())"/>
|
||||
<t t-set="colIndexGenerator" t-value="Array.from(Array(chunkSize).keys())"/>
|
||||
<t t-foreach="rowIndexGenerator" t-as="rowIndex">
|
||||
<div class="row my-4">
|
||||
<t t-foreach="colIndexGenerator" t-as="colIndex">
|
||||
<t t-if="(rowIndex * chunkSize + colIndex) < data.length">
|
||||
<div t-attf-class="#{colClass}">
|
||||
<t t-raw="data[rowIndex * chunkSize + colIndex]"/>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
</templates>
|
||||
@@ -0,0 +1,142 @@
|
||||
odoo.define('website.s_dynamic_snippet_options', function (require) {
|
||||
'use strict';
|
||||
|
||||
const options = require('web_editor.snippets.options');
|
||||
|
||||
const dynamicSnippetOptions = options.Class.extend({
|
||||
|
||||
/**
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
init: function () {
|
||||
this._super.apply(this, arguments);
|
||||
this.dynamicFilters = {};
|
||||
this.dynamicFilterTemplates = {};
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
onBuilt: function () {
|
||||
this._setOptionsDefaultValues();
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Options
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
*
|
||||
* @see this.selectClass for parameters
|
||||
*/
|
||||
selectDataAttribute: function (previewMode, widgetValue, params) {
|
||||
this._super.apply(this, arguments);
|
||||
if (params.attributeName === 'filterId' && previewMode === false) {
|
||||
this.$target.get(0).dataset.numberOfRecords = this.dynamicFilters[parseInt(widgetValue)].limit;
|
||||
}
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Fetches dynamic filters.
|
||||
* @private
|
||||
* @returns {Promise}
|
||||
*/
|
||||
_fetchDynamicFilters: function () {
|
||||
return this._rpc({
|
||||
model: 'website.snippet.filter',
|
||||
method: 'search_read',
|
||||
kwargs: {
|
||||
domain: [],
|
||||
fields: ['id', 'name', 'limit'],
|
||||
}
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Fetch dynamic filters templates.
|
||||
* @private
|
||||
* @returns {Promise}
|
||||
*/
|
||||
_fetchDynamicFilterTemplates: function () {
|
||||
return this._rpc({route: '/website/snippet/filter_templates'});
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @override
|
||||
* @private
|
||||
*/
|
||||
_renderCustomXML: function (uiFragment) {
|
||||
return Promise.all([this._renderDynamicFiltersSelector(uiFragment), this._renderDynamicFilterTemplatesSelector(uiFragment)]);
|
||||
},
|
||||
/**
|
||||
* Renders the dynamic filter option selector content into the provided uiFragment.
|
||||
* @param {HTMLElement} uiFragment
|
||||
* @private
|
||||
*/
|
||||
_renderDynamicFiltersSelector: async function (uiFragment) {
|
||||
const dynamicFilters = await this._fetchDynamicFilters();
|
||||
for (let index in dynamicFilters) {
|
||||
this.dynamicFilters[dynamicFilters[index].id] = dynamicFilters[index];
|
||||
}
|
||||
const filtersSelectorEl = uiFragment.querySelector('[data-name="filter_opt"]');
|
||||
return this._renderSelectUserValueWidgetButtons(filtersSelectorEl, this.dynamicFilters);
|
||||
},
|
||||
/**
|
||||
* Renders we-buttons into a SelectUserValueWidget element according to provided data.
|
||||
* @param {HTMLElement} selectUserValueWidgetElement the SelectUserValueWidget buttons
|
||||
* have to be created into.
|
||||
* @param {JSON} data
|
||||
* @private
|
||||
*/
|
||||
_renderSelectUserValueWidgetButtons: async function (selectUserValueWidgetElement, data) {
|
||||
for (let id in data) {
|
||||
const button = document.createElement('we-button');
|
||||
button.dataset.selectDataAttribute = id;
|
||||
button.innerHTML = data[id].name;
|
||||
selectUserValueWidgetElement.appendChild(button);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Renders the template option selector content into the provided uiFragment.
|
||||
* @param {HTMLElement} uiFragment
|
||||
* @private
|
||||
*/
|
||||
_renderDynamicFilterTemplatesSelector: async function (uiFragment) {
|
||||
const dynamicFilterTemplates = await this._fetchDynamicFilterTemplates();
|
||||
for (let index in dynamicFilterTemplates) {
|
||||
this.dynamicFilterTemplates[dynamicFilterTemplates[index].key] = dynamicFilterTemplates[index];
|
||||
}
|
||||
const templatesSelectorEl = uiFragment.querySelector('[data-name="template_opt"]');
|
||||
return this._renderSelectUserValueWidgetButtons(templatesSelectorEl, this.dynamicFilterTemplates);
|
||||
},
|
||||
/**
|
||||
* Sets default options values.
|
||||
* Method to be overridden in child components in order to set additional
|
||||
* options default values.
|
||||
* @private
|
||||
*/
|
||||
_setOptionsDefaultValues: function () {
|
||||
this._setOptionValue('numberOfElements', 4);
|
||||
this._setOptionValue('numberOfElementsSmallDevices', 1);
|
||||
},
|
||||
/**
|
||||
* Sets the option value.
|
||||
* @param optionName
|
||||
* @param value
|
||||
* @private
|
||||
*/
|
||||
_setOptionValue: function (optionName, value) {
|
||||
if (this.$target.get(0).dataset[optionName] === undefined) {
|
||||
this.$target.get(0).dataset[optionName] = value;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
options.registry.dynamic_snippet = dynamicSnippetOptions;
|
||||
|
||||
return dynamicSnippetOptions;
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
odoo.define('website.s_dynamic_snippet_carousel', function (require) {
|
||||
'use strict';
|
||||
|
||||
const config = require('web.config');
|
||||
const core = require('web.core');
|
||||
const publicWidget = require('web.public.widget');
|
||||
const DynamicSnippet = require('website.s_dynamic_snippet');
|
||||
|
||||
const DynamicSnippetCarousel = DynamicSnippet.extend({
|
||||
selector: '.s_dynamic_snippet_carousel',
|
||||
xmlDependencies: (DynamicSnippet.prototype.xmlDependencies || []).concat(
|
||||
['/website/static/src/snippets/s_dynamic_snippet_carousel/000.xml']
|
||||
),
|
||||
|
||||
/**
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
init: function () {
|
||||
this._super.apply(this, arguments);
|
||||
this.template_key = 'website.s_dynamic_snippet.carousel';
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Method to be overridden in child components in order to prepare QWeb
|
||||
* options
|
||||
* @private
|
||||
*/
|
||||
_getQWebRenderParams: function () {
|
||||
return Object.assign(
|
||||
this._super.apply(this, arguments),
|
||||
{
|
||||
interval : parseInt(this.$target[0].dataset.carouselInterval),
|
||||
},
|
||||
);
|
||||
},
|
||||
|
||||
});
|
||||
publicWidget.registry.dynamic_snippet_carousel = DynamicSnippetCarousel;
|
||||
|
||||
return DynamicSnippetCarousel;
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
.s_dynamic {
|
||||
.carousel-control-prev, .carousel-control-next {
|
||||
position: absolute;
|
||||
text-shadow: 0px 0px 3px #343A40;
|
||||
width: 4rem;
|
||||
}
|
||||
.carousel-control-prev{
|
||||
left: -4rem
|
||||
}
|
||||
.carousel-control-next{
|
||||
right: -4rem
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="website.s_dynamic_snippet.carousel">
|
||||
<div t-att-id="uniqueId" class="carousel slide" t-att-data-interval="interval">
|
||||
<!-- Content -->
|
||||
<div class="carousel-inner row w-100 mx-auto" role="listbox">
|
||||
<t t-set="colClass" t-value="'col-' + (12 / chunkSize).toString()"/>
|
||||
<t t-set="slideIndexGenerator" t-value="Array.from(Array(Math.ceil(data.length/chunkSize)).keys())"/>
|
||||
<t t-set="itemIndexGenerator" t-value="Array.from(Array(chunkSize).keys())"/>
|
||||
<t t-foreach="slideIndexGenerator" t-as="slideIndex">
|
||||
<div t-attf-class="carousel-item #{slideIndex_first ? 'active' : ''}">
|
||||
<div class="row">
|
||||
<t t-foreach="itemIndexGenerator" t-as="itemIndex">
|
||||
<t t-if="(slideIndex * chunkSize + itemIndex) < data.length">
|
||||
<div t-attf-class="#{colClass}">
|
||||
<t t-raw="data[slideIndex * chunkSize + itemIndex]"/>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
<!-- Controls -->
|
||||
<a t-attf-href="##{uniqueId}" class="carousel-control-prev" data-slide="prev" role="button" aria-label="Previous" title="Previous">
|
||||
<span class="fa fa-chevron-left fa-2x text-white"/>
|
||||
<span class="sr-only">Previous</span>
|
||||
</a>
|
||||
<a t-attf-href="##{uniqueId}" class="carousel-control-next" data-slide="next" role="button" aria-label="Next" title="Next">
|
||||
<span class="fa fa-chevron-right fa-2x text-white"/>
|
||||
<span class="sr-only">Next</span>
|
||||
</a>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
@@ -0,0 +1,28 @@
|
||||
odoo.define('website.s_dynamic_snippet_carousel_options', function (require) {
|
||||
'use strict';
|
||||
|
||||
const options = require('web_editor.snippets.options');
|
||||
const s_dynamic_snippet_options = require('website.s_dynamic_snippet_options');
|
||||
|
||||
const dynamicSnippetCarouselOptions = s_dynamic_snippet_options.extend({
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Options
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
*
|
||||
* @override
|
||||
* @private
|
||||
*/
|
||||
_setOptionsDefaultValues: function () {
|
||||
this._super.apply(this, arguments);
|
||||
this._setOptionValue('carouselInterval', '5000');
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
options.registry.dynamic_snippet_carousel = dynamicSnippetCarouselOptions;
|
||||
|
||||
return dynamicSnippetCarouselOptions;
|
||||
});
|
||||
@@ -139,6 +139,8 @@
|
||||
<script type="text/javascript" src="/website/static/src/snippets/s_timeline/options.js"/>
|
||||
<script type="text/javascript" src="/website/static/src/snippets/s_media_list/options.js"/>
|
||||
<script type="text/javascript" src="/website/static/src/snippets/s_google_map/options.js"/>
|
||||
<script type="text/javascript" src="/website/static/src/snippets/s_dynamic_snippet/options.js"/>
|
||||
<script type="text/javascript" src="/website/static/src/snippets/s_dynamic_snippet_carousel/options.js"/>
|
||||
|
||||
<script type="text/javascript" src="/website/static/src/js/editor/wysiwyg_multizone.js"/>
|
||||
<script type="text/javascript" src="/website/static/src/js/editor/wysiwyg_multizone_translate.js"/>
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<template id="website.s_dynamic_snippet_template">
|
||||
<section t-attf-class="#{snippet_name} s_dynamic d-none pt24 pb24">
|
||||
<div class="container o_not_editable">
|
||||
<div class="css_non_editable_mode_hidden">
|
||||
<div class="missing_option_warning alert alert-info rounded-0 fade show d-print-none">
|
||||
Your Dynamic Snippet will be displayed here... This message is displayed because you did not provided both a filter and a template to use.<br/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dynamic_snippet_template"/>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
<template id="s_dynamic_snippet" name="Dynamic Snippet">
|
||||
<t t-call="website.s_dynamic_snippet_template">
|
||||
<t t-set="snippet_name" t-value="'s_dynamic_snippet'"/>
|
||||
</t>
|
||||
</template>
|
||||
<template id="website.s_dynamic_snippet_options_template">
|
||||
<div t-attf-data-js="#{snippet_name}" t-attf-data-selector="#{snippet_selector}" data-no-preview="true">
|
||||
<we-select string="Filter" data-name="filter_opt" data-attribute-name="filterId">
|
||||
</we-select>
|
||||
<we-select string="Template" data-name="template_opt" data-attribute-name="templateKey" data-no-preview="true">
|
||||
</we-select>
|
||||
<we-select string="Fetched elements" data-name="number_of_records_opt" data-attribute-name="numberOfRecords" data-no-preview="true">
|
||||
<we-button data-select-data-attribute="1">1</we-button>
|
||||
<we-button data-select-data-attribute="2">2</we-button>
|
||||
<we-button data-select-data-attribute="3">3</we-button>
|
||||
<we-button data-select-data-attribute="4">4</we-button>
|
||||
<we-button data-select-data-attribute="6">6</we-button>
|
||||
<we-button data-select-data-attribute="7">7</we-button>
|
||||
<we-button data-select-data-attribute="8">8</we-button>
|
||||
<we-button data-select-data-attribute="9">9</we-button>
|
||||
<we-button data-select-data-attribute="10">10</we-button>
|
||||
<we-button data-select-data-attribute="11">11</we-button>
|
||||
<we-button data-select-data-attribute="12">12</we-button>
|
||||
<we-button data-select-data-attribute="13">13</we-button>
|
||||
<we-button data-select-data-attribute="14">14</we-button>
|
||||
<we-button data-select-data-attribute="15">15</we-button>
|
||||
<we-button data-select-data-attribute="16">16</we-button>
|
||||
</we-select>
|
||||
<we-title class="mt-2"><t t-esc="grouping_message"/></we-title>
|
||||
<we-select string="⌙ Normal devices" data-name="number_of_elements_opt" data-attribute-name="numberOfElements" data-no-preview="true"><!--   -->
|
||||
<we-button data-select-data-attribute="1">1</we-button>
|
||||
<we-button data-select-data-attribute="2">2</we-button>
|
||||
<we-button data-select-data-attribute="3">3</we-button>
|
||||
<we-button data-select-data-attribute="4">4</we-button>
|
||||
<we-button data-select-data-attribute="6">6</we-button>
|
||||
</we-select>
|
||||
<we-select string="⌙ Small devices" data-name="number_of_elements_small_devices_opt" data-attribute-name="numberOfElementsSmallDevices" data-no-preview="true"><!--   -->
|
||||
<we-button data-select-data-attribute="1">1</we-button>
|
||||
<we-button data-select-data-attribute="2">2</we-button>
|
||||
<we-button data-select-data-attribute="3">3</we-button>
|
||||
</we-select>
|
||||
<t t-raw="0"/>
|
||||
</div>
|
||||
</template>
|
||||
<template id="s_dynamic_snippet_options" inherit_id="website.snippet_options">
|
||||
<xpath expr="." position="inside">
|
||||
<t t-call="website.s_dynamic_snippet_options_template">
|
||||
<t t-set="snippet_name" t-value="'dynamic_snippet'"/>
|
||||
<t t-set="snippet_selector" t-value="'.s_dynamic_snippet'"/>
|
||||
<t t-set="grouping_message">Items per row</t>
|
||||
</t>
|
||||
</xpath>
|
||||
</template>
|
||||
<template id="assets_snippet_s_dynamic_snippet_css_000" inherit_id="website.assets_frontend">
|
||||
<xpath expr="//link[last()]" position="after">
|
||||
<link rel="stylesheet" type="text/scss" href="/website/static/src/snippets/s_dynamic_snippet/000.scss"/>
|
||||
</xpath>
|
||||
</template>
|
||||
<template id="assets_snippet_s_dynamic_snippet_js_000" inherit_id="website.assets_frontend">
|
||||
<xpath expr="//script[last()]" position="after">
|
||||
<script type="text/javascript" src="/website/static/src/snippets/s_dynamic_snippet/000.js"/>
|
||||
</xpath>
|
||||
</template>
|
||||
</odoo>
|
||||
@@ -0,0 +1,37 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<template id="s_dynamic_snippet_carousel" name="Dynamic Carousel">
|
||||
<t t-call="website.s_dynamic_snippet_template">
|
||||
<t t-set="snippet_name" t-value="'s_dynamic_snippet_carousel'"/>
|
||||
</t>
|
||||
</template>
|
||||
<template id="dynamic_snippet_carousel_options_template">
|
||||
<t t-call="website.s_dynamic_snippet_options_template">
|
||||
<t t-set="snippet_name" t-value="snippet_name"/>
|
||||
<t t-set="snippet_selector" t-value="snippet_selector"/>
|
||||
<t t-set="grouping_message">Items per slide</t>
|
||||
<we-input string="Speed"
|
||||
data-select-data-attribute="1s" data-name="speed_opt" data-attribute-name="carouselInterval" data-no-preview="true"
|
||||
data-unit="s" data-save-unit="ms" data-step="0.1"/>
|
||||
<t t-raw="0"/>
|
||||
</t>
|
||||
</template>
|
||||
<template id="s_dynamic_snippet_carousel_options" inherit_id="website.snippet_options">
|
||||
<xpath expr="." position="inside">
|
||||
<t t-call="website.dynamic_snippet_carousel_options_template">
|
||||
<t t-set="snippet_name" t-value="'dynamic_snippet_carousel'"/>
|
||||
<t t-set="snippet_selector" t-value="'.s_dynamic_snippet_carousel'"/>
|
||||
</t>
|
||||
</xpath>
|
||||
</template>
|
||||
<template id="assets_snippet_s_dynamic_snippet_carousel_css_000" inherit_id="website.assets_snippet_s_dynamic_snippet_css_000">
|
||||
<xpath expr="//link[last()]" position="after">
|
||||
<link rel="stylesheet" type="text/scss" href="/website/static/src/snippets/s_dynamic_snippet_carousel/000.scss"/>
|
||||
</xpath>
|
||||
</template>
|
||||
<template id="assets_snippet_s_dynamic_snippet_carousel_js_000" inherit_id="website.assets_snippet_s_dynamic_snippet_js_000">
|
||||
<xpath expr="//script[last()]" position="after">
|
||||
<script type="text/javascript" src="/website/static/src/snippets/s_dynamic_snippet_carousel/000.js"/>
|
||||
</xpath>
|
||||
</template>
|
||||
</odoo>
|
||||
@@ -110,6 +110,9 @@
|
||||
<div class="o_panel_body">
|
||||
<t id="form_form_hook"/>
|
||||
<t t-snippet="website.s_google_map" t-thumbnail="/website/static/src/img/snippets_thumbs/s_google_map.svg"/>
|
||||
<t t-snippet="website.s_dynamic_snippet" t-thumbnail="/website/static/src/img/snippets_thumbs/s_dynamic_snippet.svg"/>
|
||||
<t t-snippet="website.s_dynamic_snippet_carousel" t-thumbnail="/website/static/src/img/snippets_thumbs/s_dynamic_carousel.svg"/>
|
||||
<t id="sale_products_hook"/>
|
||||
<t id="sale_recently_viewed_product_hook"/>
|
||||
<t id="sale_product_search_section_hook"/>
|
||||
<t id="blog_posts_hook"/>
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
<field name="default" eval="True"/>
|
||||
<field name="description">Published Post</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
|
||||
<data>
|
||||
|
||||
@@ -74,6 +74,7 @@
|
||||
<field name="is_published" eval="True"/>
|
||||
<field name="published_date" eval="(datetime.now()-relativedelta(days=2)).strftime('%Y-%m-%d %H:%M:%S')"/>
|
||||
<field name="tag_ids" eval="[(6, 0, [ref('blog_tag_2')])]"/>
|
||||
<field name="visits">246</field>
|
||||
<field name="cover_properties">{"background-image": "url('/website_blog/static/src/img/cover_2.jpg')", "resize_class": "o_record_has_cover o_half_screen_height", "opacity": "0.4"}</field>
|
||||
<field name="content"><![CDATA[
|
||||
<section class="container">
|
||||
@@ -120,6 +121,7 @@ People who live on the island have never even seen this remarkable scenery unles
|
||||
<field name="blog_id" ref="blog_blog_1"/>
|
||||
<field name="author_id" ref="base.partner_demo"/>
|
||||
<field name="tag_ids" eval="[(6, 0, [ref('blog_tag_1')])]"/>
|
||||
<field name="visits">467</field>
|
||||
<field name="is_published" eval="True"/>
|
||||
<field name="published_date" eval="(datetime.now()-relativedelta(days=1)).strftime('%Y-%m-%d %H:%M:%S')"/>
|
||||
<field name="cover_properties">{"background-image": "url('/website_blog/static/src/img/cover_3.jpg')", "resize_class": "o_record_has_cover o_half_screen_height", "opacity": "0.4"}</field>
|
||||
@@ -172,6 +174,7 @@ People who live on the island have never even seen this remarkable scenery unles
|
||||
<field name="blog_id" ref="blog_blog_2"/>
|
||||
<field name="author_id" ref="base.partner_admin"/>
|
||||
<field name="tag_ids" eval="[(6, 0, [ref('blog_tag_5')])]"/>
|
||||
<field name="visits">453</field>
|
||||
<field name="is_published" eval="True"/>
|
||||
<field name="published_date" eval="(datetime.now()-relativedelta(days=6)).strftime('%Y-%m-%d %H:%M:%S')"/>
|
||||
<field name="cover_properties">{"background-image": "url('/website_blog/static/src/img/cover_4.jpg')", "resize_class": "o_record_has_cover o_half_screen_height", "text_align_class": "text-center", "opacity": "0.2"}</field>
|
||||
|
||||
@@ -20,7 +20,8 @@
|
||||
'views/sale_order_views.xml',
|
||||
'views/crm_team_views.xml',
|
||||
'views/templates.xml',
|
||||
'views/snippets.xml',
|
||||
'views/snippets/snippets.xml',
|
||||
'views/snippets/s_dynamic_snippet_products.xml',
|
||||
'views/snippets/s_products_searchbar.xml',
|
||||
'views/res_config_settings_views.xml',
|
||||
'views/digest_views.xml',
|
||||
|
||||
@@ -45,6 +45,55 @@
|
||||
<field name="selectable" eval="True" />
|
||||
<field name="website_id" eval="False"/>
|
||||
</record>
|
||||
|
||||
<!-- Action Server for Dynamic Filter -->
|
||||
<record id="dynamic_snippet_products_action" model="ir.actions.server">
|
||||
<field name="name">Products Dynamic Snippet</field>
|
||||
<field name="model_id" ref="model_product_product"/>
|
||||
<field name="state">code</field>
|
||||
<field name="code">
|
||||
ProductProduct = model.env['product.product']
|
||||
FieldMonetary = model.env['ir.qweb.field.monetary']
|
||||
|
||||
website = request.website.get_current_website()
|
||||
dynamic_filter = model.env.context.get('dynamic_filter')
|
||||
limit = model.env.context.get('limit')
|
||||
search_domain = model.env.context.get('search_domain')
|
||||
|
||||
domain = [('website_published', '=', True)] + website.website_domain() + (search_domain or [])
|
||||
products = ProductProduct.search(domain, limit=limit)
|
||||
_ = products.mapped('name')
|
||||
|
||||
monetary_options = {
|
||||
'display_currency': request.website.get_current_pricelist().currency_id,
|
||||
}
|
||||
|
||||
max_nb_chars = 100
|
||||
res_products = []
|
||||
for product in products:
|
||||
res_product = product._read_format(['id', 'name', 'website_url', 'description_sale'])[0]
|
||||
res_product.update(product._get_combination_info_variant())
|
||||
|
||||
if res_product['description_sale'] and len(res_product['description_sale']) > max_nb_chars:
|
||||
res_product['description_sale'] = "%s ..." % res_product['description_sale'][:max_nb_chars]
|
||||
res_product['list_price'] = FieldMonetary.value_to_html(res_product['price'], monetary_options)
|
||||
for field_name in dynamic_filter.field_names.split(","):
|
||||
field = ProductProduct._fields.get(field_name)
|
||||
if field and field.type == 'binary':
|
||||
res_product[field_name] = website.image_url(product, field_name)
|
||||
res_products.append(res_product)
|
||||
|
||||
response = res_products
|
||||
</field>
|
||||
</record>
|
||||
<!-- Dynamic Filter -->
|
||||
<record id="dynamic_filter_demo_products" model="website.snippet.filter">
|
||||
<field name="action_server_id" ref="website_sale.dynamic_snippet_products_action"/>
|
||||
<field name="field_names">display_name,description_sale,image_512,list_price</field>
|
||||
<field name="limit" eval="16"/>
|
||||
<field name="name">Products</field>
|
||||
<field name="website_id" ref="website.default_website"/>
|
||||
</record>
|
||||
</data>
|
||||
<data>
|
||||
<function model="ir.model.fields" name="formbuilder_whitelist">
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
odoo.define('website_sale.s_dynamic_snippet_products', function (require) {
|
||||
'use strict';
|
||||
|
||||
const config = require('web.config');
|
||||
const core = require('web.core');
|
||||
const publicWidget = require('web.public.widget');
|
||||
const DynamicSnippetCarousel = require('website.s_dynamic_snippet_carousel');
|
||||
|
||||
const DynamicSnippetProducts = DynamicSnippetCarousel.extend({
|
||||
selector: '.s_dynamic_snippet_products',
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Method to be overridden in child components if additional configuration elements
|
||||
* are required in order to fetch data.
|
||||
* @override
|
||||
* @private
|
||||
*/
|
||||
_isConfigComplete: function() {
|
||||
return this._super.apply(this, arguments) && this.$el.get(0).dataset.productCategoryId !== undefined;
|
||||
},
|
||||
/**
|
||||
* Method to be overridden in child components in order to provide a search
|
||||
* domain if needed.
|
||||
* @override
|
||||
* @private
|
||||
*/
|
||||
_getSearchDomain: function () {
|
||||
const searchDomain = this._super.apply(this, arguments);
|
||||
searchDomain.push(['public_categ_ids', 'child_of', parseInt(this.$el.get(0).dataset.productCategoryId)]);
|
||||
return searchDomain;
|
||||
},
|
||||
|
||||
});
|
||||
publicWidget.registry.dynamic_snippet_products = DynamicSnippetProducts;
|
||||
|
||||
return DynamicSnippetProducts;
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="website_sale.s_dynamic_snippet.products" inherit_id="website.s_dynamic_snippet.carousel">
|
||||
<xpath expr="//div" position="attributes">
|
||||
<attribute name="data-filter-id" add=""/>
|
||||
</xpath>
|
||||
</t>
|
||||
</templates>
|
||||
@@ -0,0 +1,93 @@
|
||||
odoo.define('website_sale.s_dynamic_snippet_products_options', function (require) {
|
||||
'use strict';
|
||||
|
||||
const options = require('web_editor.snippets.options');
|
||||
const s_dynamic_snippet_carousel_options = require('website.s_dynamic_snippet_carousel_options');
|
||||
|
||||
const dynamicSnippetProductsOptions = s_dynamic_snippet_carousel_options.extend({
|
||||
|
||||
/**
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
init: function () {
|
||||
this._super.apply(this, arguments);
|
||||
this.productCategories = {};
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
onBuilt: function () {
|
||||
this._super.apply(this, arguments);
|
||||
this._rpc({
|
||||
model: 'ir.model.data',
|
||||
method: 'search_read',
|
||||
kwargs: {
|
||||
domain: [['module', '=', 'website_sale'], ['model', '=', 'website.snippet.filter']],
|
||||
fields: ['id', 'res_id'],
|
||||
}
|
||||
}).then((data) => {
|
||||
this.$target.get(0).dataset.filterId = data[0].res_id;
|
||||
this.$target.get(0).dataset.numberOfRecords = this.dynamicFilters[data[0].res_id].limit;
|
||||
});
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
*
|
||||
* @override
|
||||
* @private
|
||||
*/
|
||||
_computeWidgetVisibility: function (widgetName, params) {
|
||||
if (widgetName === 'filter_opt') {
|
||||
return false;
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
/**
|
||||
* Fetches product categories.
|
||||
* @private
|
||||
* @returns {Promise}
|
||||
*/
|
||||
_fetchProductCategories: function () {
|
||||
return this._rpc({
|
||||
model: 'product.public.category',
|
||||
method: 'search_read',
|
||||
kwargs: {
|
||||
domain: [],
|
||||
fields: ['id', 'name'],
|
||||
}
|
||||
});
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @override
|
||||
* @private
|
||||
*/
|
||||
_renderCustomXML: function (uiFragment) {
|
||||
return Promise.all([this._super.apply(this, arguments), this._renderProductCategorySelector(uiFragment)]);
|
||||
},
|
||||
/**
|
||||
* Renders the product categories option selector content into the provided uiFragment.
|
||||
* @private
|
||||
* @param {HTMLElement} uiFragment
|
||||
*/
|
||||
_renderProductCategorySelector: async function (uiFragment) {
|
||||
const productCategories = await this._fetchProductCategories();
|
||||
for (let index in productCategories) {
|
||||
this.productCategories[productCategories[index].id] = productCategories[index];
|
||||
}
|
||||
const productCategoriesSelectorEl = uiFragment.querySelector('[data-name="product_category_opt"]');
|
||||
return this._renderSelectUserValueWidgetButtons(productCategoriesSelectorEl, this.productCategories);
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
options.registry.dynamic_snippet_products = dynamicSnippetProductsOptions;
|
||||
|
||||
return dynamicSnippetProductsOptions;
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<template id="s_dynamic_snippet_products" name="Dynamic Products">
|
||||
<t t-call="website.s_dynamic_snippet_template">
|
||||
<t t-set="snippet_name" t-value="'s_dynamic_snippet_products'"/>
|
||||
</t>
|
||||
</template>
|
||||
<template id="s_dynamic_snippet_products_options" inherit_id="website.snippet_options">
|
||||
<xpath expr="." position="inside">
|
||||
<t t-call="website.dynamic_snippet_carousel_options_template">
|
||||
<t t-set="snippet_name" t-value="'dynamic_snippet_products'"/>
|
||||
<t t-set="snippet_selector" t-value="'.s_dynamic_snippet_products'"/>
|
||||
<we-select string="Product Category" data-name="product_category_opt" data-attribute-name="productCategoryId" data-no-preview="true">
|
||||
</we-select>
|
||||
</t>
|
||||
</xpath>
|
||||
</template>
|
||||
<template id="assets_snippet_s_dynamic_snippet_products_js_000" inherit_id="website.assets_snippet_s_dynamic_snippet_carousel_js_000">
|
||||
<xpath expr="//script[last()]" position="after">
|
||||
<script type="text/javascript" src="/website_sale/static/src/snippets/s_dynamic_snippet_products/000.js"/>
|
||||
</xpath>
|
||||
</template>
|
||||
</odoo>
|
||||
+4
-1
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<template id="s_products_recently_viewed" name="Products">
|
||||
<template id="s_products_recently_viewed" name="Viewed Products">
|
||||
<section class="s_wsale_products_recently_viewed d-none pt24 pb24" style="min-height: 400px;">
|
||||
<div class="container">
|
||||
<div class="alert alert-info alert-dismissible rounded-0 fade show d-print-none css_non_editable_mode_hidden o_not_editable">
|
||||
@@ -17,6 +17,9 @@
|
||||
</template>
|
||||
|
||||
<template id="snippets" inherit_id="website.snippets" name="e-commerce snippets">
|
||||
<xpath expr="//t[@id='sale_products_hook']" position="replace">
|
||||
<t t-snippet="website_sale.s_dynamic_snippet_products" t-thumbnail="/website_sale/static/src/img/snippets_thumbs/s_products_recently_viewed.svg"/>
|
||||
</xpath>
|
||||
<xpath expr="//t[@id='sale_recently_viewed_product_hook']" position="replace">
|
||||
<t t-snippet="website_sale.s_products_recently_viewed" t-thumbnail="/website_sale/static/src/img/snippets_thumbs/s_products_recently_viewed.svg"/>
|
||||
</xpath>
|
||||
@@ -39,6 +39,7 @@
|
||||
<template id="assets_wysiwyg" inherit_id="website.assets_wysiwyg" name="Shop Wysiwyg Editor">
|
||||
<xpath expr="//link[last()]" position="after">
|
||||
<link rel="stylesheet" type="text/scss" href="/website_sale/static/src/scss/website_sale.editor.scss"/>
|
||||
<script type="text/javascript" src="/website_sale/static/src/snippets/s_dynamic_snippet_products/options.js"/>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user