[IMP] website, *: allow to add a language from theme tab + options

*: web_editor, portal

This adds a button to add language in theme tab. When clicked, opens
the "add a language" wizard.
If the website has only one language, the options to enable language
in header & footer are hidden.
When an user add a language, the default behavior is Header: dropdown
& Footer: inline.
Also provides two more rendering options : Language Code and Flag +
Language Code.

task-3457554

closes odoo/odoo#119650

Related: odoo/design-themes#651
Related: odoo/upgrade#5214
Signed-off-by: Quentin Smetz (qsm) <qsm@odoo.com>
Co-authored-by: qsm-odoo <qsm@odoo.com>
This commit is contained in:
Serge Bayet (seba)
2023-10-25 11:37:12 +00:00
committed by qsm-odoo
co-authored by qsm-odoo
parent fb63a6abbb
commit dc81acfb2d
11 changed files with 174 additions and 79 deletions
+4 -1
View File
@@ -71,6 +71,7 @@
<template id="language_selector" name="Language Selector">
<t t-nocache="The query strings can change for the same page and the same rendering."
t-nocache-no_text="no_text"
t-nocache-codes="codes"
t-nocache-_div_classes="_div_classes"
t-nocache-_btn_class="_btn_class"
t-nocache-_txt_class="_txt_class"
@@ -82,13 +83,15 @@
<span t-if="not no_text"
t-attf-class="align-middle #{_txt_class}"
t-esc="active_lang[2].split('/').pop()"/>
<span t-elif="codes" class="align-middle" t-esc="active_lang[1].split('_').pop(0).upper()"/>
</button>
<div t-attf-class="dropdown-menu #{_dropdown_menu_class}" role="menu">
<t t-foreach="languages" t-as="lg">
<a class="dropdown-item" t-att-href="url_for(request.httprequest.path + '?' + keep_query(), lang_code=lg[0])"
t-attf-class="dropdown-item js_change_lang #{active_lang == lg and 'active'}"
t-att-data-url_code="lg[1]" t-att-title="lg[2].split('/').pop()">
<span t-if="not no_text" t-out="lg[2].split('/').pop()" t-attf-class="#{_txt_class}"/>
<span t-if="not no_text" t-esc="lg[2].split('/').pop()" t-attf-class="#{_txt_class}"/>
<span t-elif="codes" t-esc="lg[1].split('_').pop(0).upper()" t-attf-class="align-middle #{_txt_class}"/>
</a>
</t>
</div>
@@ -2109,7 +2109,7 @@ var SnippetsMenu = Widget.extend({
this.el.ownerDocument.body.classList.remove('editor_has_snippets');
// Dispose BS tooltips.
this.tooltips.dispose();
options.clearM2oRpcCache();
options.clearServiceCache();
},
//--------------------------------------------------------------------------
@@ -45,7 +45,38 @@ import { jsonrpc } from "@web/core/network/rpc_service";
const preserveCursor = OdooEditorLib.preserveCursor;
const { DateTime } = luxon;
const resetOuids = OdooEditorLib.resetOuids;
let _serviceCache = {
orm: {},
rpc: {},
};
const clearServiceCache = () => {
_serviceCache = {
orm: {},
rpc: {},
};
};
/**
* Caches rpc/orm service
* @param {Function} service
* @param {...any} args
* @returns
*/
function serviceCached(service) {
const cache = _serviceCache;
return Object.assign(Object.create(service), {
call() {
const serviceName = Object.prototype.hasOwnProperty.call(service, "call")
? "orm"
: "rpc";
const cacheId = JSON.stringify(arguments);
if (!cache[serviceName][cacheId]) {
cache[serviceName][cacheId] =
serviceName == "rpc" ? service(...arguments) : service.call(...arguments);
}
return cache[serviceName][cacheId];
},
});
}
/**
* @param {HTMLElement} el
* @param {string} [title]
@@ -2596,10 +2627,6 @@ const SelectPagerUserValueWidget = SelectUserValueWidget.extend({
}
});
let m2oRpcCache = {};
const clearM2oRpcCache = () => {
m2oRpcCache = {};
};
const Many2oneUserValueWidget = SelectUserValueWidget.extend({
className: (SelectUserValueWidget.prototype.className || '') + ' o_we_many2one',
events: Object.assign({}, SelectUserValueWidget.prototype.events, {
@@ -2621,7 +2648,6 @@ const Many2oneUserValueWidget = SelectUserValueWidget.extend({
init(parent, title, options, $target) {
this.afterSearch = [];
this.displayNameCache = {};
this._rpcCache = m2oRpcCache;
const {dataAttributes} = options;
Object.assign(options, {
limit: '5',
@@ -2645,7 +2671,7 @@ const Many2oneUserValueWidget = SelectUserValueWidget.extend({
options.nullText = $target[0].dataset.nullText ||
JSON.parse($target[0].dataset.oeContactOptions || '{}')['null_text'];
this.orm = this._withCache(this.bindService("orm"));
this.orm = serviceCached(this.bindService("orm"));
return this._super(...arguments);
},
@@ -2773,23 +2799,6 @@ const Many2oneUserValueWidget = SelectUserValueWidget.extend({
// Private
//--------------------------------------------------------------------------
/**
* Caches the rpc.
*
* @override
*/
_withCache(orm) {
const cache = this._rpcCache;
return Object.assign(Object.create(orm), {
call() {
const cacheId = JSON.stringify(arguments);
if (!cache[cacheId]) {
cache[cacheId] = orm.call(...arguments);
}
return cache[cacheId];
},
});
},
/**
* Searches the database for corresponding records and updates the dropdown
*
@@ -9209,6 +9218,6 @@ export default {
// Other names for convenience
Class: SnippetOptionWidget,
registry: registry,
clearM2oRpcCache,
serviceCached,
clearServiceCache,
};
@@ -988,7 +988,7 @@ export class WysiwygAdapterComponent extends Wysiwyg {
} else if (event.data.action) {
callback = () => {
this.leaveEditMode({
onLeave: () => this.action.doAction(event.data.action),
onLeave: () => this.action.doAction(event.data.action, event.data.options || {}),
forceLeave: true,
reloadIframe: false,
});
@@ -1406,6 +1406,39 @@ options.registry.OptionsTab = options.Class.extend({
action: 'website.theme_install_kanban_action',
});
},
/**
* @see this.selectClass for parameters
*/
async addLanguage(previewMode, widgetValue, params) {
// Retrieve the website id to check by default the website checkbox in
// the dialog box 'action_view_base_language_install'
const websiteId = this.options.context.website_id;
const save = await new Promise((resolve) => {
Dialog.confirm(
this,
_t("Adding a language requires to leave the editor. This will save all your changes, are you sure you want to proceed?"),
{
confirm_callback: () => resolve(true),
cancel_callback: () => resolve(false),
}
);
});
if (!save) {
return;
}
this.trigger_up("request_save", {
reload: false,
action: "base.action_view_base_language_install",
options: {
additionalContext: {
params: {
website_id: websiteId,
url_return: "[lang]",
}
},
}
});
},
/**
* @see this.selectClass for parameters
*/
@@ -2191,7 +2224,13 @@ options.registry.collapse = options.Class.extend({
options.registry.WebsiteLevelColor = options.Class.extend({
specialCheckAndReloadMethodsNames: options.Class.prototype.specialCheckAndReloadMethodsNames
.concat(['customizeWebsiteLayer2Color']),
/**
* @constructor
*/
init() {
this._super(...arguments);
this._rpc = options.serviceCached(this.bindService("rpc"));
},
/**
* @see this.selectClass for parameters
*/
@@ -2234,6 +2273,24 @@ options.registry.WebsiteLevelColor = options.Class.extend({
}
return this._super(...arguments);
},
/**
* @override
*/
async _computeWidgetVisibility(widgetName, params) {
const _super = this._super.bind(this);
if (
[
"footer_language_selector_label_opt",
"footer_language_selector_opt",
].includes(widgetName)
) {
this._languages = await this._rpc.call("/website/get_languages");
if (this._languages.length === 1) {
return false;
}
}
return _super(...arguments);
},
});
options.registry.HeaderLayout = options.registry.WebsiteLevelColor.extend({
@@ -2259,6 +2316,36 @@ options.registry.HeaderLayout = options.registry.WebsiteLevelColor.extend({
}
});
options.registry.HeaderElements = options.Class.extend({
/**
* @constructor
*/
init() {
this._super(...arguments);
this._rpc = options.serviceCached(this.bindService("rpc"));
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* @override
*/
async _computeWidgetVisibility(widgetName, params) {
const _super = this._super.bind(this);
switch (widgetName) {
case "header_language_selector_opt":
this._languages = await this._rpc.call("/website/get_languages");
if (this._languages.length === 1) {
return false;
}
break;
}
return _super(...arguments);
},
});
options.registry.HeaderNavbar = options.Class.extend({
/**
* Particular case: we want the option to be associated on the header navbar
@@ -1452,6 +1452,7 @@ header {
margin-right: 0.2em;
border-radius: $border-radius-pill;
object-fit: cover;
box-shadow: rgba(0, 0, 0, 0.429) 1px 1px 1px;
}
span.list-inline-item.o_add_language:last-child {
display: none !important; // Hide the separator if it is the last list item
@@ -27,7 +27,7 @@ export class WebsiteSwitcherSystray extends Component {
const path = pathname + search + hash;
window.location.href = `${encodeURI(website.domain)}/web#action=website.website_preview&path=${encodeURIComponent(path)}&website_id=${encodeURIComponent(website.id)}`;
} else {
this.websiteService.goToWebsite({ websiteId: website.id });
this.websiteService.goToWebsite({ websiteId: website.id, path: "", lang: "default" });
if (!website.domain) {
const closeFn = this.notificationService.add(
_t(
@@ -4,16 +4,18 @@ import wTourUtils from "@website/js/tours/tour_utils";
wTourUtils.registerWebsitePreviewTour('automatic_editor_on_new_website', {
test: true,
edition: true,
url: '/',
},
() => [
{
content: "Select the language dropdown",
trigger: 'iframe .js_language_selector .dropdown-toggle'
},
wTourUtils.goToTheme(),
{
content: "click on Add a language",
trigger: 'iframe a.o_add_language',
trigger: "we-button[data-add-language]"
},
{
content: "confirm leave editor",
trigger: ".modal-dialog button.btn-primary"
},
{
content: "type Parseltongue",
@@ -37,7 +39,6 @@ wTourUtils.registerWebsitePreviewTour('automatic_editor_on_new_website', {
{
content: "Select parseltongue",
trigger: 'iframe a.js_change_lang[data-url_code=pa_GB]',
extra_trigger: 'iframe a.js_change_lang .o_lang_flag',
},
{
content: "Check that we're on parseltongue and then go to settings",
+8 -5
View File
@@ -6,13 +6,16 @@ import { whenReady } from "@odoo/owl";
wTourUtils.registerWebsitePreviewTour('rte_translator', {
test: true,
url: '/',
edition: true,
wait_for: whenReady(),
}, () => [{
content: "click language dropdown",
trigger: 'iframe .js_language_selector .dropdown-toggle',
}, {
}, () => [
wTourUtils.goToTheme(),
{
content: "click on Add a language",
trigger: 'iframe a.o_add_language',
trigger: "we-button[data-add-language]"
}, {
content: "confirm leave editor",
trigger: ".modal-dialog button.btn-primary"
}, {
content: "type Parseltongue",
trigger: 'div[name="lang_ids"] .o_input_dropdown input',
+12 -3
View File
@@ -987,7 +987,8 @@
data-page-option-name="header_text_color"/>
</div>
<div data-selector="#wrapwrap > header"
<div data-js="HeaderElements"
data-selector="#wrapwrap > header"
data-no-check="true"
groups="website.group_website_designer">
<we-row string="Elements" class="o_we_full_row align-items-start">
@@ -997,6 +998,7 @@
data-reset-view-arch="true"
data-reload="/"/>
<we-button title="Show/hide language selector" class="fa fa-flag d-flex justify-content-center flex-grow-1"
data-name="header_language_selector_opt"
data-customize-website-views="website.header_language_selector"
data-reset-view-arch="true"
data-reload="/"/>
@@ -1107,6 +1109,8 @@
<we-button data-customize-website-views="">Text</we-button>
<we-button data-customize-website-views="website.header_language_selector_flag, website.header_language_selector_no_text">Flag</we-button>
<we-button data-customize-website-views="website.header_language_selector_flag">Flag and Text</we-button>
<we-button data-customize-website-views="website.header_language_selector_code, website.header_language_selector_no_text">Code</we-button>
<we-button data-customize-website-views="website.header_language_selector_flag, website.header_language_selector_code, website.header_language_selector_no_text">Flag and Code</we-button>
</we-select>
</div>
@@ -1259,16 +1263,18 @@
data-null-value="'NULL'"
data-with-combinations="customizeWebsiteColor"
data-with-gradients="true"/>
<we-select string="Language Selector" data-reload="/">
<we-select data-name="footer_language_selector_opt" string="Language Selector" data-reload="/">
<we-button data-name="language_selector_none_opt"
data-customize-website-views="">None</we-button>
<we-button data-customize-website-views="portal.footer_language_selector">Dropdown</we-button>
<we-button data-customize-website-views="portal.footer_language_selector, website.footer_language_selector_inline">Inline</we-button>
</we-select>
<we-select string="Label" class="o_we_sublevel_1" data-dependencies="!language_selector_none_opt" data-reload="/">
<we-select data-name="footer_language_selector_label_opt" string="Label" class="o_we_sublevel_1" data-dependencies="!language_selector_none_opt" data-reload="/">
<we-button data-customize-website-views="">Text</we-button>
<we-button data-customize-website-views="website.footer_language_selector_flag, website.footer_language_selector_no_text">Flag</we-button>
<we-button data-customize-website-views="website.footer_language_selector_flag">Flag and Text</we-button>
<we-button data-customize-website-views="website.footer_language_selector_code, website.footer_language_selector_no_text">Code</we-button>
<we-button data-customize-website-views="website.footer_language_selector_flag, website.footer_language_selector_code, website.footer_language_selector_no_text">Flag and Code</we-button>
</we-select>
</div>
@@ -1690,6 +1696,9 @@
<we-row string="Theme">
<we-button data-switch-theme="" data-no-preview="true" class="o_we_bg_brand_primary">Switch Theme</we-button>
</we-row>
<we-row string="Language">
<we-button data-add-language="" data-no-preview="true" class="o_we_bg_brand_primary">Add a Language</we-button>
</we-row>
<we-select string="Page Layout" data-variable="layout">
<we-button data-customize-website-variable="'full'" data-name="layout_full_opt">Full</we-button>
<we-button data-customize-website-variable="'boxed'">Boxed</we-button>
+16 -34
View File
@@ -2078,17 +2078,6 @@
<img t-attf-class="o_lang_flag #{_flag_class}" t-attf-src="#{flag_image_src}?height=25"/>
</template>
<template id="language_selector_add_language">
<!-- When clicked from the iframe preview in backend, the click will be
prevented to directly call the action in the top window -->
<a t-attf-class="o_add_language d-none #{dropdown and 'd-sm-block dropdown-item' or 'd-sm-inline-block list-inline-item'} small"
groups="website.group_website_restricted_editor"
t-attf-href="/web#action=base.action_view_base_language_install&amp;website_id=#{website.id if website else ''}&amp;url_return=#{quote_plus(url_for('', '[lang]') + '?' + keep_query())}">
<i class="fa fa-plus-circle"/>
<span>Add a language...</span>
</a>
</template>
<template id="language_selector" inherit_id="portal.language_selector">
<xpath expr="//t[@t-nocache]" position="attributes">
<attribute name="t-nocache-flags">flags</attribute>
@@ -2101,18 +2090,6 @@
</t>
</xpath>
<!-- Always show the language selector on editable websites -->
<xpath expr="//t[@t-set='language_selector_visible']" position="after">
<t t-set="language_selector_visible" t-value="language_selector_visible or (website and (editable or translatable))"/>
</xpath>
<!-- Add the button to add a language -->
<xpath expr="//t[@t-foreach='languages']" position="after">
<t t-call="website.language_selector_add_language">
<t t-set="dropdown" t-value="True"/>
</t>
</xpath>
<!-- Add the 'flags' possibility -->
<xpath expr="//button[contains(@t-attf-class, 'dropdown-toggle')]/span" position="before">
<t t-if="flags" t-call="website.lang_flag">
@@ -2144,18 +2121,11 @@
<span class="list-inline-item">|</span>
</t>
</xpath>
<xpath expr="//t[@t-call='website.language_selector_add_language']" position="replace">
<span class="o_add_language list-inline-item"
groups="website.group_website_restricted_editor">|</span>
<t t-call="website.language_selector_add_language">
<t t-set="dropdown" t-value="False"/>
</t>
</xpath>
</template>
<template id="placeholder_header_language_selector" name="Placeholder Header Language Selector"/>
<template id="header_language_selector" inherit_id="website.placeholder_header_language_selector" name="Header Language Selector" active="False">
<template id="header_language_selector" inherit_id="website.placeholder_header_language_selector" name="Header Language Selector" active="True">
<xpath expr="." position="inside">
<li data-name="Language Selector" t-attf-class="o_header_language_selector #{_item_class}">
<t id="header_language_selector_call" t-call="portal.language_selector">
@@ -2171,30 +2141,42 @@
</xpath>
</template>
<template id="header_language_selector_flag" name="Header Language Selector Flag" inherit_id="website.header_language_selector" active="True">
<template id="header_language_selector_flag" name="Header Language Selector Flag" inherit_id="website.header_language_selector" active="False">
<xpath expr="//t[@id='header_language_selector_call']" position="before">
<t t-set="flags" t-value="True"/>
</xpath>
</template>
<template id="header_language_selector_code" name="Header Language Selector Code" inherit_id="website.header_language_selector" active="False">
<xpath expr="//t[@id='header_language_selector_call']" position="before">
<t t-set="codes" t-value="True"/>
</xpath>
</template>
<template id="header_language_selector_no_text" name="Header Language Selector No Text" inherit_id="website.header_language_selector" active="False">
<xpath expr="//t[@id='header_language_selector_call']" position="before">
<t t-set="no_text" t-value="True"/>
</xpath>
</template>
<template id="footer_language_selector_inline" name="Footer Language Selector Inline" inherit_id="portal.footer_language_selector" active="False">
<template id="footer_language_selector_inline" name="Footer Language Selector Inline" inherit_id="portal.footer_language_selector" active="True">
<xpath expr="//t[@id='language_selector_call']" position="replace">
<t id="language_selector_call" t-call="website.language_selector_inline"/>
</xpath>
</template>
<template id="footer_language_selector_flag" name="Footer Language Selector Flag" inherit_id="portal.footer_language_selector" active="True">
<template id="footer_language_selector_flag" name="Footer Language Selector Flag" inherit_id="portal.footer_language_selector" active="False">
<xpath expr="//t[@id='language_selector_call']" position="before">
<t t-set="flags" t-value="True"/>
</xpath>
</template>
<template id="footer_language_selector_code" name="Footer Language Selector Code" inherit_id="portal.footer_language_selector" active="False">
<xpath expr="//t[@id='language_selector_call']" position="before">
<t t-set="codes" t-value="True"/>
</xpath>
</template>
<template id="footer_language_selector_no_text" name="Footer Language Selector No Text" inherit_id="portal.footer_language_selector" active="False">
<xpath expr="//t[@id='language_selector_call']" position="before">
<t t-set="no_text" t-value="True"/>