Skip to content

Commit

Permalink
[IMP] website, web_editor: allow saving of snippets
Browse files Browse the repository at this point in the history
Now the user can save snippets to use them later.

task-2120409
  • Loading branch information
fja-odoo committed Nov 18, 2019
1 parent a7d029a commit eae47b7
Show file tree
Hide file tree
Showing 8 changed files with 359 additions and 130 deletions.
5 changes: 3 additions & 2 deletions addons/web_editor/models/ir_qweb.py
Original file line number Diff line number Diff line change
Expand Up @@ -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>'))]

Expand Down
89 changes: 77 additions & 12 deletions addons/web_editor/static/src/js/editor/snippets.editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -722,6 +722,7 @@ var SnippetsMenu = Widget.extend({
events: {
'click we-select': '_onOptionTogglerClick',
'click .o_install_btn': '_onInstallBtnClick',
'click #snippet_custom .o_delete_btn': '_onDeleteBtnClick',
},
custom_events: {
'activate_insertion_zones': '_onActivateInsertionZones',
Expand All @@ -738,6 +739,7 @@ var SnippetsMenu = Widget.extend({
'hide_overlay': '_onHideOverlay',
'block_preview_overlays': '_onBlockPreviewOverlays',
'unblock_preview_overlays': '_onUnblockPreviewOverlays',
'reload_snippet_template': '_onReloadSnippetTemplate',
},

/**
Expand Down Expand Up @@ -783,13 +785,7 @@ var SnippetsMenu = Widget.extend({

// Fetch snippet templates and compute it

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);
}));
defs.push(this._loadSnippetsTemplates());

// Prepare snippets editor environment
this.$snippetEditorArea = $('<div/>', {
Expand Down Expand Up @@ -912,8 +908,8 @@ var SnippetsMenu = Widget.extend({
/**
* Load snippets.
*/
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;
}
Expand Down Expand Up @@ -1170,6 +1166,26 @@ var SnippetsMenu = Widget.extend({
});
});
},
/**
* @private
* @param {bool} invalidateCache
* @param {JQuery} $selectedSnippet Can be empty than no snippet will be selected
*/
_loadSnippetsTemplates: function (invalidateCache, $selectedSnippet) {
this._destroyEditors();
return this.loadSnippets(invalidateCache).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);
if ($selectedSnippet) {
return this._activateSnippet($selectedSnippet);
} else {
return Promise.resolve();
}
});
},
/**
* @private
*/
Expand Down Expand Up @@ -1379,9 +1395,10 @@ 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) {
if ($sbody.length && !isCustomSnippet) {
var snippetClasses = $sbody.attr('class').match(/s_[^ ]+/g);
if (snippetClasses && snippetClasses.length) {
snippetClasses = '.' + snippetClasses.join('.');
Expand All @@ -1399,9 +1416,18 @@ var SnippetsMenu = Widget.extend({
'<div class="oe_snippet_thumbnail_img" style="background-image: url(%s);"/>' +
'<span class="oe_snippet_thumbnail_title">%s</span>' +
'</div>',
$snippet.find('[data-oe-thumbnail]').data('oeThumbnail'),
$snippet.data('oeThumbnail'),
name
));
if (isCustomSnippet) {
$thumbnail.prepend($(_.str.sprintf(
'<div data-snippet-id="%s" class="o_delete_btn">' +
'<i class="fa fa-trash"/>' +
'</div>',
$snippet.data('oeSnippetId')
)));
$thumbnail.prepend($('<div class="o_image_ribbon"/>'));
}
$snippet.prepend($thumbnail);

// Create the install button (t-install feature) if necessary
Expand Down Expand Up @@ -1554,7 +1580,12 @@ var SnippetsMenu = Widget.extend({

$snippets.draggable({
greedy: true,
helper: 'clone',
helper: function () {
const dragSnip = this.cloneNode(true);
dragSnip.querySelector('.o_delete_btn').remove();
dragSnip.querySelector('.o_image_ribbon').remove();
return dragSnip;
},
appendTo: this.$body,
cursor: 'move',
handle: '.oe_snippet_thumbnail',
Expand Down Expand Up @@ -1842,6 +1873,40 @@ var SnippetsMenu = Widget.extend({
}],
}).open();
},
/**
* @private
*/
_onDeleteBtnClick: function (ev) {
const self = this;
const $snippet = $(ev.target).closest('.oe_snippet');
new Dialog(this, {
size: 'medium',
title: _.str.sprintf(_t("Delete the snippet : %s"), $snippet.attr('name')),
buttons: [{
text: _t("Yes"),
classes: 'btn-primary',
click: async function () {
await self._rpc({
route: '/snippet/delete',
params: {
view_id: parseInt(ev.currentTarget.dataset.snippetId),
}
});
await self._loadSnippetsTemplates(true);
this.close();
},
}, {
text: _t("No"),
close: true,
}],
}).open();
},
/**
* @private
*/
_onReloadSnippetTemplate: function (ev) {
this._loadSnippetsTemplates(true, ev.data.$snippet).then(ev.data.onSuccess);
},
/**
* @private
*/
Expand Down
30 changes: 30 additions & 0 deletions addons/web_editor/static/src/scss/wysiwyg_snippets.scss
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,36 @@ body.editor_enable.editor_has_snippets {
}
}
}
#snippet_custom .oe_snippet .oe_snippet_thumbnail{
.o_delete_btn {
position: absolute;
right: 0;
z-index: 1;
width: 25px;
height: 25px;
background-color: rgba(255, 0, 0, 0.75);
display: none;
justify-content: center;
align-items: center;

&:hover {
cursor: pointer;
}
}
&:hover {
.o_delete_btn {
display: flex;
}
}
.o_image_ribbon {
position: absolute;
z-index: 1;
width: 0px;
height: 0px;
border-right: 20px solid transparent;
border-top: 20px solid $o-enterprise-color;
}
}
}

> .o_we_customize_panel {
Expand Down
54 changes: 54 additions & 0 deletions addons/website/controllers/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@
import logging
import pytz
import requests
import uuid
import werkzeug.utils
import werkzeug.wrappers

from itertools import islice
from lxml import etree, html
from xml.etree import ElementTree as ET

import odoo
Expand Down Expand Up @@ -345,6 +347,58 @@ def google_console_search(self, key, **kwargs):

return request.make_response("google-site-verification: %s" % request.website.google_search_console)

# ------------------------------------------------------
# Snippets
# ------------------------------------------------------
custom_section_xml_id = 'website.custom_snippets'

@http.route(['/snippet/save'], type='json', auth="public", website=True)
def snippet_save(self, name, arch, original=None):
View = request.env['ir.ui.view']
original_id = 's_custom_snippet'
for path in odoo.addons.website.__path__:
if original and os.path.isfile(path + '/static/src/img/snippets_thumbs/%s.png' % (original,)):
original_id = original
break
thumbnail_url = '/website/static/src/img/snippets_thumbs/%s.png' % (original_id,)
key = '%s_custom_%s' % (original_id, uuid.uuid4().hex)
snippet_key = 'website.%s' % (key,)

# html to xml to add / at the end of self closing tags like br, input, img, ...
xml_arch = etree.tostring(html.fromstring(arch))
View.create({
'name': name,
'key': snippet_key,
'type': 'qweb',
'arch': xml_arch,
})
custom_section = View.search([('key', '=', self.custom_section_xml_id)])
View.create({
'name': name + ' Block',
'key': '%s_%s' % (self.custom_section_xml_id, 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>
""" % (self.custom_section_xml_id, snippet_key, thumbnail_url),
})

@http.route(['/snippet/delete'], type='json', auth="public", website=True)
def snippet_delete(self, view_id):
View = request.env['ir.ui.view']
snippet = View.browse(view_id)
# See snippet_save where we create the key for the related view
custom_key = '%s_%s' % (self.custom_section_xml_id, snippet.key.replace('website.', ''))
View.search([('key', '=', custom_key)]).unlink()
snippet.unlink()

# ------------------------------------------------------
# Themes
# ------------------------------------------------------
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
54 changes: 54 additions & 0 deletions addons/website/static/src/js/editor/snippets.options.js
Original file line number Diff line number Diff line change
Expand Up @@ -1599,6 +1599,60 @@ options.registry.topMenuColor = options.registry.colorpicker.extend({
},
});

/**
* Handle the save of a snippet as a template that can be reused later
*/
options.registry.saveTemplate = options.Class.extend({
xmlDependencies: ['/website/static/src/xml/website.editor.xml'],

/**
* @override
*/
isTopOption: function () {
return true;
},
/**
*
*/
saveTemplate() {
const self = this;
// parent is null to prevent the dialog from being destroyed when reloading the snippets.
new Dialog(null, {
title: _t("Create new Snippet Template"),
$content: $(qweb.render('website.dialog.saveSnippet', {
currentSnippet: self.$target[0].dataset.name + ' Custom',
})),
buttons: [{
text: _t("Save"),
classes: 'btn-primary',
click: function () {
var snippetName = this.el.querySelector('.o_input_snippet_name').value;
self._rpc({
route: '/snippet/save',
params: {
name: snippetName,
arch: self.$target.prop('outerHTML'),
original: Array.from(self.$target[0].classList).filter(x => {
return /\bs_./g.test(x);
})[0],
}
}).then(() => {
self.trigger_up('reload_snippet_template', {
$snippet: self.$target,
onSuccess: () => {
this.close();
},
});
});
},
}, {
text: _t("Discard"),
close: true,
}],
}).open();
},
});

/**
* Handles the edition of snippet's anchor name.
*/
Expand Down
9 changes: 9 additions & 0 deletions addons/website/static/src/xml/website.editor.xml
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,15 @@
</div>
</t>
</t>
<!-- Save Snippet Name option dialog -->
<div t-name="website.dialog.saveSnippet">
<div class="form-group row">
<label class="col-form-label col-md-3" for="snippetName">Choose a snippet name</label>
<div class="col-md-9">
<input type="text" class="form-control o_input_snippet_name" id="snippetName" t-attf-value="#{currentSnippet}" placeholder="Snippet name"/>
</div>
</div>
</div>
<!-- Anchor Name option dialog -->
<div t-name="website.dialog.anchorName">
<div class="form-group row">
Expand Down
Loading

0 comments on commit eae47b7

Please sign in to comment.