diff --git a/addons/test_website/static/tests/tours/custom_snippets.js b/addons/test_website/static/tests/tours/custom_snippets.js
index 7fc066a1648..eb6a22f29b8 100644
--- a/addons/test_website/static/tests/tours/custom_snippets.js
+++ b/addons/test_website/static/tests/tours/custom_snippets.js
@@ -10,12 +10,13 @@ var tour = require('web_tour.tour');
* -> drag a banner into page content
* -> customize banner (set text)
* -> save banner as custom snippet
- * -> confirm name (TODO: remove when implicit default name feature is implemented)
- * -> go to blocks
+ * -> confirm save
* -> ensure custom snippet is available
* -> drag custom snippet
* -> ensure block appears as banner
* -> ensure block appears as custom banner
+ * -> rename custom banner
+ * -> verify rename took effect
* -> delete custom snippet
* -> confirm delete
* -> ensure it was deleted
@@ -46,26 +47,35 @@ tour.register('test_custom_snippet', {
content: "save custom snippet",
trigger: ".snippet-option-SnippetSave we-button",
},
- {
- content: "confirm save name",
- trigger: ".modal-dialog button span:contains('Save')",
- },
{
content: "confirm reload",
trigger: ".modal-dialog button span:contains('Save and Reload')",
},
- {
- content: "go to blocks",
- trigger: ".o_we_add_snippet_btn",
- },
{
content: "ensure custom snippet appeared",
trigger: "#oe_snippets .oe_snippet[name='Custom Banner']",
- run: function() {}, // check
+ run: function () {
+ $("#oe_snippets .oe_snippet[name='Custom Banner'] .o_rename_btn").attr("style", "display: block;");
+ // hover is needed for rename button to appear
+ },
+ },
+ {
+ content: "rename custom snippet",
+ trigger: ".oe_snippet[name='Custom Banner'] we-button.o_rename_btn",
+ extra_trigger: ".oe_snippet[name='Custom Banner'] .oe_snippet_thumbnail:not(.o_we_already_dragging)",
+ },
+ {
+ content: "set name",
+ trigger: ".oe_snippet[name='Custom Banner'] input",
+ run: "text Bruce Banner",
+ },
+ {
+ content: "confirm rename",
+ trigger: ".oe_snippet[name='Custom Banner'] we-button.o_we_confirm_btn",
},
{
content: "drop custom snippet",
- trigger: ".oe_snippet[name='Custom Banner'] .oe_snippet_thumbnail:not(.o_we_already_dragging)",
+ trigger: ".oe_snippet[name='Bruce Banner'] .oe_snippet_thumbnail:not(.o_we_already_dragging)",
extra_trigger: "body.editor_enable.editor_has_snippets",
moveTrigger: ".oe_drop_zone",
run: "drag_and_drop #wrap",
@@ -73,17 +83,20 @@ tour.register('test_custom_snippet', {
{
content: "ensure banner section exists",
trigger: "#wrap section[data-name='Banner']",
- run: function() {}, // check
+ run: function () {}, // check
},
{
content: "ensure custom banner section exists",
- trigger: "#wrap section[data-name='Custom Banner']",
- run: function() {}, // check
+ trigger: "#wrap section[data-name='Bruce Banner']",
+ run: function () {
+ $("#oe_snippets .oe_snippet[name='Bruce Banner'] .o_delete_btn").attr("style", "display: block;");
+ // hover is needed for delete button to appear
+ },
},
{
content: "delete custom snippet",
- trigger: ".oe_snippet[name='Custom Banner'] we-button.o_delete_btn",
- extra_trigger: ".oe_snippet[name='Custom Banner'] .oe_snippet_thumbnail:not(.o_we_already_dragging)",
+ trigger: ".oe_snippet[name='Bruce Banner'] we-button.o_delete_btn",
+ extra_trigger: ".oe_snippet[name='Bruce Banner'] .oe_snippet_thumbnail:not(.o_we_already_dragging)",
},
{
content: "confirm delete",
@@ -92,8 +105,8 @@ tour.register('test_custom_snippet', {
{
content: "ensure custom snippet disappeared",
trigger: "#oe_snippets",
- extra_trigger: "#oe_snippets:not(:has(.oe_snippet[name='Custom Banner']))",
- run: function() {}, // check
+ extra_trigger: "#oe_snippets:not(:has(.oe_snippet[name='Bruce Banner']))",
+ run: function () {}, // check
},
]);
diff --git a/addons/web_editor/models/ir_ui_view.py b/addons/web_editor/models/ir_ui_view.py
index 3d7a4a734a4..13b369cb50b 100644
--- a/addons/web_editor/models/ir_ui_view.py
+++ b/addons/web_editor/models/ir_ui_view.py
@@ -8,6 +8,7 @@ from lxml import etree, html
from odoo.exceptions import AccessError
from odoo import api, models
+from odoo.osv import expression
_logger = logging.getLogger(__name__)
@@ -298,6 +299,14 @@ class IrUiView(models.Model):
def _snippet_save_view_values_hook(self):
return {}
+ def _find_available_name(self, name, used_names):
+ attempt = 1
+ candidate_name = name
+ while candidate_name in used_names:
+ attempt += 1
+ candidate_name = f"{name} ({attempt})"
+ return candidate_name
+
@api.model
def save_snippet(self, name, arch, template_key, snippet_key, thumbnail_url):
"""
@@ -317,6 +326,14 @@ class IrUiView(models.Model):
snippet_key = '%s_%s' % (snippet_key, uuid.uuid4().hex)
full_snippet_key = '%s.%s' % (app_name, snippet_key)
+ # find available name
+ current_website = self.env['website'].browse(self._context.get('website_id'))
+ website_domain = current_website.website_domain()
+ used_names = self.search(expression.AND([
+ [('name', '=like', '%s%%' % name)], website_domain
+ ])).mapped('name')
+ name = self._find_available_name(name, used_names)
+
# 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 = {
@@ -348,6 +365,16 @@ class IrUiView(models.Model):
snippet_addition_view_values.update(self._snippet_save_view_values_hook())
self.create(snippet_addition_view_values)
+ @api.model
+ def rename_snippet(self, name, view_id, template_key):
+ snippet_view = self.browse(view_id)
+ key = snippet_view.key.split('.')[1]
+ custom_key = self._get_snippet_addition_view_key(template_key, key)
+ snippet_addition_view = self.search([('key', '=', custom_key)])
+ if snippet_addition_view:
+ snippet_addition_view.name = name + ' Block'
+ snippet_view.name = name
+
@api.model
def delete_snippet(self, view_id, template_key):
snippet_view = self.browse(view_id)
diff --git a/addons/web_editor/static/src/js/editor/snippets.editor.js b/addons/web_editor/static/src/js/editor/snippets.editor.js
index 8abc8eec695..d608fa88546 100644
--- a/addons/web_editor/static/src/js/editor/snippets.editor.js
+++ b/addons/web_editor/static/src/js/editor/snippets.editor.js
@@ -894,6 +894,7 @@ var SnippetsMenu = Widget.extend({
'click .o_install_btn': '_onInstallBtnClick',
'click .o_we_add_snippet_btn': '_onBlocksTabClick',
'click .o_we_invisible_entry': '_onInvisibleEntryClick',
+ 'click #snippet_custom .o_rename_btn': '_onRenameBtnClick',
'click #snippet_custom .o_delete_btn': '_onDeleteBtnClick',
'mousedown': '_onMouseDown',
'input .o_snippet_search_filter_input': '_onSnippetSearchInput',
@@ -1760,8 +1761,13 @@ var SnippetsMenu = Widget.extend({
}));
}
- // Create the delete button for custom snippets
+ // Create the rename and delete button for custom snippets
if (isCustomSnippet) {
+ const btnRenameEl = document.createElement('we-button');
+ btnRenameEl.dataset.snippetId = $snippet.data('oeSnippetId');
+ btnRenameEl.classList.add('o_rename_btn', 'fa', 'fa-pencil', 'btn', 'o_we_hover_success');
+ btnRenameEl.title = _.str.sprintf(_t("Rename %s"), name);
+ $snippet.append(btnRenameEl);
const btnEl = document.createElement('we-button');
btnEl.dataset.snippetId = $snippet.data('oeSnippetId');
btnEl.classList.add('o_delete_btn', 'fa', 'fa-trash', 'btn', 'o_we_hover_danger');
@@ -2438,6 +2444,50 @@ var SnippetsMenu = Widget.extend({
}],
}).open();
},
+ /**
+ * @private
+ */
+ _onRenameBtnClick: function (ev) {
+ const $snippet = $(ev.target).closest('.oe_snippet');
+ const snippetName = $snippet.attr('name');
+ const confirmText = _t('Confirm');
+ const cancelText = _t('Cancel');
+ const $input = $(`
+
Hello World!
', base.inherit_children_ids.read_combined(['arch'])['arch'], ) + + def test_find_available_name(self): + View = self.env['ir.ui.view'] + used_names = ['Unrelated name'] + initial_name = "Test name" + name = View._find_available_name(initial_name, used_names) + self.assertEqual(initial_name, name) + used_names.append(name) + name = View._find_available_name(initial_name, used_names) + self.assertEqual('Test name (2)', name) + used_names.append(name) + name = View._find_available_name(initial_name, used_names) + self.assertEqual('Test name (3)', name)