Skip to content
Permalink
Browse files

[IMP] web_editor, *: allow saving of snippets

* website, mass_mailing

Now the user can save snippets to use them on other pages.

task-2120409

Co-authored-by: qsm-odoo <qsm@odoo.com>
  • Loading branch information
fja-odoo and qsm-odoo committed Nov 15, 2019
1 parent c63ffe9 commit 5ddb10281d44b18f3e80a5bd1ece3ff9f76530d2
@@ -5,9 +5,9 @@
<xpath expr="//div[@id='snippets_menu']" position="inside">
<button type="button" disabled="disabled">Select a template</button>
</xpath>
<xpath expr="//div[@id='o_scroll']" position="replace">
<t t-set="company_id" t-value="res_company"/>
<div id="o_scroll">
<xpath expr="//t[@id='default_snippets']" position="replace">
<t id="default_snippets">
<t t-set="company_id" t-value="res_company"/>
<div id="email_designer_themes">
<div data-name="basic"
data-nowrap="1"
@@ -70,7 +70,7 @@
<t t-snippet="mass_mailing.s_mail_block_footer_social_left" t-thumbnail="/mass_mailing/static/src/img/blocks/block_footer_social_left.png"/>
</div>
</div>
</div>
</t>
</xpath>
<xpath expr="//div[@id='snippet_options']" position="inside">
<t t-call="mass_mailing.snippet_options"/>
@@ -797,12 +797,21 @@
data-exclude=".o_mail_no_options"
data-drop-near=".col_mv, td, th"/>

<t t-set="mailing_content_selector">.note-editable > div:not(.o_layout), .note-editable .oe_structure > div, .oe_snippet_body</t>
<div data-js="content"
data-selector=".note-editable > div:not(.o_layout), .note-editable .oe_structure > div, .oe_snippet_body"
t-att-data-selector="mailing_content_selector"
data-exclude=".o_mail_no_options"
data-drop-near="[data-oe-field='body_html']:not(:has(.o_layout)) > *, .oe_structure > *"
data-drop-in="[data-oe-field='body_html']:not(:has(.o_layout)), .oe_structure"/>

<div data-js="SnippetSave"
t-att-data-selector="mailing_content_selector">
<we-button class="fa fa-fw fa-save"
title="Save the block to use it elsewhere on the website"
data-save-snippet=""
data-no-preview="true"/>
</div>

<div data-js="sizing_y"
data-selector=".note-editable > div:not(.o_layout), .note-editable .oe_structure > div, td, th"
data-exclude=".o_mail_no_resize, .o_mail_no_options"/>
@@ -50,9 +50,10 @@ def _compile_directive_snippet(self, el, options):
view_id = View.get_view_id(el.attrib.get('t-call'))
name = View.browse(view_id).name
thumbnail = el.attrib.pop('t-thumbnail', "oe-thumbnail")
div = u'<div name="%s" data-oe-type="snippet" data-oe-thumbnail="%s">' % (
div = u'<div name="%s" data-oe-type="snippet" data-oe-thumbnail="%s" data-oe-snippet-id="%s">' % (
escape(pycompat.to_text(name)),
escape(pycompat.to_text(thumbnail))
escape(pycompat.to_text(thumbnail)),
escape(pycompat.to_text(view_id)),
)
return [self._append(ast.Str(div))] + self._compile_node(el, options) + [self._append(ast.Str(u'</div>'))]

@@ -3,10 +3,11 @@

import copy
import logging
import uuid
from lxml import etree, html

from odoo.exceptions import AccessError
from odoo import api, fields, models
from odoo import api, models

_logger = logging.getLogger(__name__)

@@ -270,3 +271,89 @@ def get_related_views(self, key, bundles=False):
View = self.with_context(active_test=False, lang=None)
views = View._views_get(key, bundles=bundles)
return views.filtered(lambda v: not v.groups_id or len(user_groups.intersection(v.groups_id)))

# --------------------------------------------------------------------------
# Snippet saving
# --------------------------------------------------------------------------

@api.model
def _get_default_snippet_thumbnail(self, snippet_class=None):
return '/web_editor/static/src/img/snippets_thumbs/s_custom_snippet.png'

@api.model
def _get_snippet_addition_view_key(self, template_key, key):
return '%s.%s' % (template_key, key)

@api.model
def _snippet_save_view_values_hook(self, app_name):
return {}

@api.model
def save_snippet(self, name, arch, template_key, snippet_class=None, thumbnail_url=None):
"""
Save a new snippet arch so that it appears with the given name when
using the given snippets template.
Params:
name (str): the name of the snippet to save
arch (str): the html structure of the snippet to save
template_key (str):
the key of the view regrouping all snippets in which the
snippet to save is meant to appear
snippet_class (str, default=None):
a className which is supposed to uniquely-identify the snippet
from which the snippet to save originates
thumbnail_url (str, default=None):
the url of the thumbnail to use when displaying the snippet to
save (default one: see '_get_default_snippet_thumbnail')
"""
if not thumbnail_url:
thumbnail_url = self._get_default_snippet_thumbnail(snippet_class)

app_name = template_key.split('.')[0]
snippet_class = snippet_class or 's_custom_snippet'
key = '%s_%s' % (snippet_class, uuid.uuid4().hex)
snippet_key = '%s.%s' % (app_name, key)

# html to xml to add '/' at the end of self closing tags like br, ...
xml_arch = etree.tostring(html.fromstring(arch))
new_snippet_view_values = {
'name': name,
'key': snippet_key,
'type': 'qweb',
'arch': xml_arch,
}
new_snippet_view_values.update(self._snippet_save_view_values_hook(app_name))
self.create(new_snippet_view_values)

custom_section = self.search([('key', '=', template_key)])
snippet_addition_view_values = {
'name': name + ' Block',
'key': self._get_snippet_addition_view_key(template_key, key),
'inherit_id': custom_section.id,
'type': 'qweb',
'arch': """
<data inherit_id="%s">
<xpath expr="//div[@id='snippet_custom']" position="attributes">
<attribute name="class" remove="d-none" separator=" "/>
</xpath>
<xpath expr="//div[@id='snippet_custom_body']" position="inside">
<t t-snippet="%s" t-thumbnail="%s"/>
</xpath>
</data>
""" % (template_key, snippet_key, thumbnail_url),
}
snippet_addition_view_values.update(self._snippet_save_view_values_hook(app_name))
self.create(snippet_addition_view_values)

@api.model
def delete_snippet(self, view_id, template_key):
snippet = self.browse(view_id)
key = snippet.key.split('.')[1]
custom_key = self._get_snippet_addition_view_key(template_key, key)
self.search([('key', '=', custom_key)]).unlink()
snippet.unlink()
Binary file not shown.
@@ -780,6 +780,7 @@ var SnippetsMenu = Widget.extend({
events: {
'click .o_install_btn': '_onInstallBtnClick',
'click .o_we_invisible_entry': '_onInvisibleEntryClick',
'click #snippet_custom .o_delete_btn': '_onDeleteBtnClick',
},
custom_events: {
'activate_insertion_zones': '_onActivateInsertionZones',
@@ -801,6 +802,7 @@ var SnippetsMenu = Widget.extend({
'block_preview_overlays': '_onBlockPreviewOverlays',
'unblock_preview_overlays': '_onUnblockPreviewOverlays',
'user_value_widget_opening': '_onUserValueWidgetOpening',
'reload_snippet_template': '_onReloadSnippetTemplate',
},

/**
@@ -857,26 +859,22 @@ var SnippetsMenu = Widget.extend({
this.window = this.ownerDocument.defaultView;
this.$window = $(this.window);

// Fetch snippet templates and compute it
this.customizePanel = document.createElement('div');
this.customizePanel.classList.add('o_we_customize_panel', 'd-none');

defs.push(this.loadSnippets().then(html => {
return this._computeSnippetTemplates(html);
}).then(() => {
this.customizePanel = document.createElement('div');
this.customizePanel.classList.add('o_we_customize_panel', 'd-none');
this.$el.append(this.customizePanel);

this.invisibleDOMPanelEl = document.createElement('div');
this.invisibleDOMPanelEl.classList.add('o_we_invisible_el_panel');
this.invisibleDOMPanelEl.appendChild(
$('<div/>', {
text: _t('Invisible Elements'),
class: 'o_panel_header',
}).prepend(
$('<i/>', {class: 'fa fa-eye-slash'})
)[0]
);
this.$el.append(this.invisibleDOMPanelEl);
this.invisibleDOMPanelEl = document.createElement('div');
this.invisibleDOMPanelEl.classList.add('o_we_invisible_el_panel');
this.invisibleDOMPanelEl.appendChild(
$('<div/>', {
text: _t('Invisible Elements'),
class: 'o_panel_header',
}).prepend(
$('<i/>', {class: 'fa fa-eye-slash'})
)[0]
);

// Fetch snippet templates and compute it
defs.push(this._loadSnippetsTemplates().then(() => {
return this._updateInvisibleDOM();
}));

@@ -998,9 +996,10 @@ var SnippetsMenu = Widget.extend({
},
/**
* Load snippets.
* @param {boolean} invalidateCache
*/
loadSnippets: function () {
if (this.cacheSnippetTemplate[this.options.snippets]) {
loadSnippets: function (invalidateCache) {
if (!invalidateCache && this.cacheSnippetTemplate[this.options.snippets]) {
this._defLoadSnippets = this.cacheSnippetTemplate[this.options.snippets];
return this._defLoadSnippets;
}
@@ -1285,6 +1284,17 @@ var SnippetsMenu = Widget.extend({
});
});
},
/**
* @private
* @param {boolean} invalidateCache
*/
_loadSnippetsTemplates: async function (invalidateCache) {
return this._mutex.exec(async () => {
await this._destroyEditors();
const html = await this.loadSnippets(invalidateCache);
await this._computeSnippetTemplates(html);
});
},
/**
* @private
*/
@@ -1502,6 +1512,7 @@ var SnippetsMenu = Widget.extend({
var $snippet = $(this);
var name = $snippet.attr('name');
var $sbody = $snippet.children(':not(.oe_snippet_thumbnail)').addClass('oe_snippet_body');
const isCustomSnippet = !!$snippet.parents('#snippet_custom').length;

// Associate in-page snippets to their name
if ($sbody.length) {
@@ -1525,6 +1536,13 @@ var SnippetsMenu = Widget.extend({
$snippet.find('[data-oe-thumbnail]').data('oeThumbnail'),
name
));
if (isCustomSnippet) {
const btn = document.createElement('we-button');
btn.dataset.snippetId = $snippet.data('oeSnippetId');
btn.classList.add('o_delete_btn', 'fa', 'fa-trash');
$thumbnail.prepend(btn);
$thumbnail.prepend($('<div class="o_image_ribbon"/>'));
}
$snippet.prepend($thumbnail);

// Create the install button (t-install feature) if necessary
@@ -1563,6 +1581,8 @@ var SnippetsMenu = Widget.extend({

// Add the computed template and make elements draggable
this.$el.html($html);
this.$el.append(this.customizePanel);
this.$el.append(this.invisibleDOMPanelEl);
this._makeSnippetDraggable(this.$snippets);
this._disableUndroppableSnippets();

@@ -1677,7 +1697,11 @@ var SnippetsMenu = Widget.extend({

$snippets.draggable({
greedy: true,
helper: 'clone',
helper: function () {
const dragSnip = this.cloneNode(true);
dragSnip.querySelectorAll('.o_delete_btn, .o_image_ribbon').forEach(el => el.remove());
return dragSnip;
},
appendTo: this.$body,
cursor: 'move',
handle: '.oe_snippet_thumbnail',
@@ -1971,6 +1995,43 @@ var SnippetsMenu = Widget.extend({
.toggleClass('fa-eye-slash', !isVisible);
return this._activateSnippet(isVisible ? $snippet : false);
},
/**
* @private
*/
_onDeleteBtnClick: function (ev) {
const $snippet = $(ev.target).closest('.oe_snippet');
new Dialog(this, {
size: 'medium',
title: _t('Confirmation'),
$content: $('<div><p>' + _t(`Are you sure you want to delete the snippet: ${$snippet.attr('name')} ?`) + '</p></div>'),
buttons: [{
text: _t("Yes"),
close: true,
classes: 'btn-primary',
click: async () => {
await this._rpc({
model: 'ir.ui.view',
method: 'delete_snippet',
kwargs: {
'view_id': parseInt(ev.currentTarget.dataset.snippetId),
'template_key': this.options.snippets,
},
});
await this._loadSnippetsTemplates(true);
},
}, {
text: _t("No"),
close: true,
}],
}).open();
},
/**
* @private
*/
_onReloadSnippetTemplate: async function (ev) {
await this._activateSnippet(false);
await this._loadSnippetsTemplates(true);
},
/**
* @private
*/

0 comments on commit 5ddb102

Please sign in to comment.
You can’t perform that action at this time.