[MERGE] *: deprecate and replace non-owl t-raw
non-owl JS-side of #68072: * deprecates `t-raw` * adds a `t-out` which `t-esc` aliases and which "does the right thing" * introduces a `Markup` function / object which is considered markup-safe by t-out (thus left unescaped) - unlike the python version, most of the override hooks (e.g. concatenation, formatting, ...) are not available in JS, we might eventually want to override some of the action methods (e.g. replace) but so far the needs seemed pretty limited - and javascript pretty strongly differentiates between a string (primitive) and a String (object), the latter being what Markup gets - so various constructs which can return a Markup in Python can't really afford to in JS: I tried with QWeb and e.g. jquery *really* does not deal well with non-primitive strings, as a result `_.escape` is shimmed to understand `Markup` objects but will not return markup objects when escaping strings - `sprintf` (the one from web.utils) was updated for Markup-awareness for convenience though - and `Markup` can be used as a template tag, in which case it will automatically escape the substitutions - removed `messageIsHtml` from the non-owl notifications, replaced by the message being a markup object - updated Dialog to work the same way (it did not even have a flag), removed explicit escaping from most of the callsites (only found one where we actually leveraged dialog titles being markup) - modified the kanban view so HTML non-raw values automatically get wrapped in Markup Also moved some formatting from the server to the client, either removing the need to inject markup entirely or making the use of `Markup` much cleaner than just "mark whatever the server returned as safe". There are a few things I'm not entirely sure about e.g. whether even using markup is necessary for `formatMonetary`, doesn't the "unicode" NBSP work fine in HTML? Though we may need to keep `forceString` to ensure something like ascii-compatibility. closes odoo/odoo#70004 Related: odoo/enterprise#18005 Signed-off-by: Xavier Morel (xmo) <xmo@odoo.com>
This commit is contained in:
@@ -2,6 +2,7 @@ odoo.define('account.tour', function(require) {
|
||||
"use strict";
|
||||
|
||||
var core = require('web.core');
|
||||
const {Markup} = require('web.utils');
|
||||
var tour = require('web_tour.tour');
|
||||
|
||||
var _t = core._t;
|
||||
@@ -36,7 +37,7 @@ tour.register('account_tour', {
|
||||
}, {
|
||||
trigger: "div[name=partner_id] input",
|
||||
extra_trigger: "[name=move_type][raw-value=out_invoice]",
|
||||
content: _t("Write a company name to <b>create one</b> or <b>see suggestions</b>."),
|
||||
content: Markup(_t("Write a company name to <b>create one</b> or <b>see suggestions</b>.")),
|
||||
position: "bottom",
|
||||
}, {
|
||||
trigger: ".o_m2o_dropdown_option a:contains('Create')",
|
||||
@@ -46,7 +47,7 @@ tour.register('account_tour', {
|
||||
}, {
|
||||
trigger: ".modal-content button.btn-primary",
|
||||
extra_trigger: "[name=move_type][raw-value=out_invoice]",
|
||||
content: _t("Once everything is set, you are good to continue. You will be able to edit this later in the <b>Customers</b> menu."),
|
||||
content: Markup(_t("Once everything is set, you are good to continue. You will be able to edit this later in the <b>Customers</b> menu.")),
|
||||
auto: true,
|
||||
}, {
|
||||
trigger: "div[name=invoice_line_ids] .o_field_x2many_list_row_add a:not([data-context])",
|
||||
@@ -74,7 +75,7 @@ tour.register('account_tour', {
|
||||
}, {
|
||||
trigger: "input[name=email]",
|
||||
extra_trigger: "[name=move_type][raw-value=out_invoice]",
|
||||
content: _t("Write here <b>your own email address</b> to test the flow."),
|
||||
content: Markup(_t("Write here <b>your own email address</b> to test the flow.")),
|
||||
run: 'text customer@example.com',
|
||||
auto: true,
|
||||
}, {
|
||||
|
||||
@@ -118,7 +118,7 @@
|
||||
<t t-name="kanban-box">
|
||||
<div t-attf-class="oe_kanban_global_click">
|
||||
<div><strong class="o_kanban_record_title"><t t-esc="record.name.value"/></strong></div>
|
||||
<div t-if="!widget.isHtmlEmpty(record.note.raw_value)"><t t-raw="record.note.raw_value"/></div>
|
||||
<div t-if="!widget.isHtmlEmpty(record.note.raw_value)"><t t-out="record.note.value"/></div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
|
||||
@@ -45,7 +45,6 @@ If you need to manage your meetings, you should install the CRM module.
|
||||
'calendar/static/src/js/calendar_controller.js',
|
||||
'calendar/static/src/js/calendar_model.js',
|
||||
'calendar/static/src/js/calendar_view.js',
|
||||
'calendar/static/src/js/mail_activity.js',
|
||||
'calendar/static/src/js/systray_activity_menu.js',
|
||||
'calendar/static/src/js/services/calendar_notification_service.js',
|
||||
],
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
odoo.define('calendar.Activity', function (require) {
|
||||
"use strict";
|
||||
|
||||
var Activity = require('@mail/js/activity')[Symbol.for("default")];
|
||||
var Dialog = require('web.Dialog');
|
||||
var core = require('web.core');
|
||||
var _t = core._t;
|
||||
|
||||
Activity.include({
|
||||
|
||||
/**
|
||||
* Override behavior to redirect to calendar event instead of activity
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
_onEditActivity: function (event, options) {
|
||||
var self = this;
|
||||
var activity_id = $(event.currentTarget).data('activity-id');
|
||||
var activity = _.find(this.activities, function (act) { return act.id === activity_id; });
|
||||
if (activity && activity.activity_category === 'meeting' && activity.calendar_event_id) {
|
||||
return self._super(event, _.extend({
|
||||
res_model: 'calendar.event',
|
||||
res_id: activity.calendar_event_id[0],
|
||||
}));
|
||||
}
|
||||
return self._super(event, options);
|
||||
},
|
||||
|
||||
/**
|
||||
* Override behavior to warn that the calendar event is about to be removed as well
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
_onUnlinkActivity: function (event, options) {
|
||||
event.preventDefault();
|
||||
var self = this;
|
||||
var activity_id = $(event.currentTarget).data('activity-id');
|
||||
var activity = _.find(this.activities, function (act) { return act.id === activity_id; });
|
||||
if (activity && activity.activity_category === 'meeting' && activity.calendar_event_id) {
|
||||
Dialog.confirm(
|
||||
self,
|
||||
_t("The activity is linked to a meeting. Deleting it will remove the meeting as well. Do you want to proceed ?"), {
|
||||
confirm_callback: function () {
|
||||
return self._rpc({
|
||||
model: 'mail.activity',
|
||||
method: 'unlink_w_meeting',
|
||||
args: [[activity_id]],
|
||||
})
|
||||
.then(self._reload.bind(self, {activity: true}));
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
else {
|
||||
return self._super(event, options);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
});
|
||||
@@ -2,6 +2,7 @@ odoo.define('crm.tour', function(require) {
|
||||
"use strict";
|
||||
|
||||
var core = require('web.core');
|
||||
const {Markup} = require('web.utils');
|
||||
var tour = require('web_tour.tour');
|
||||
|
||||
var _t = core._t;
|
||||
@@ -12,22 +13,22 @@ tour.register('crm_tour', {
|
||||
sequence: 10,
|
||||
}, [tour.stepUtils.showAppsMenuItem(), {
|
||||
trigger: '.o_app[data-menu-xmlid="crm.crm_menu_root"]',
|
||||
content: _t('Ready to boost your sales? Let\'s have a look at your <b>Pipeline</b>.'),
|
||||
content: Markup(_t('Ready to boost your sales? Let\'s have a look at your <b>Pipeline</b>.')),
|
||||
position: 'bottom',
|
||||
edition: 'community',
|
||||
}, {
|
||||
trigger: '.o_app[data-menu-xmlid="crm.crm_menu_root"]',
|
||||
content: _t('Ready to boost your sales? Let\'s have a look at your <b>Pipeline</b>.'),
|
||||
content: Markup(_t('Ready to boost your sales? Let\'s have a look at your <b>Pipeline</b>.')),
|
||||
position: 'bottom',
|
||||
edition: 'enterprise',
|
||||
}, {
|
||||
trigger: '.o-kanban-button-new',
|
||||
extra_trigger: '.o_opportunity_kanban',
|
||||
content: _t("<b>Create your first opportunity.</b>"),
|
||||
content: Markup(_t("<b>Create your first opportunity.</b>")),
|
||||
position: 'bottom',
|
||||
}, {
|
||||
trigger: ".o_kanban_quick_create .o_field_widget[name='partner_id']",
|
||||
content: _t('<b>Write a few letters</b> to look for a company, or create a new one.'),
|
||||
content: Markup(_t('<b>Write a few letters</b> to look for a company, or create a new one.')),
|
||||
position: "top",
|
||||
run: function (actions) {
|
||||
actions.text("Brandon Freeman", this.$anchor.find("input"));
|
||||
@@ -38,28 +39,28 @@ tour.register('crm_tour', {
|
||||
in_modal: false,
|
||||
}, {
|
||||
trigger: ".o_kanban_quick_create .o_kanban_add",
|
||||
content: _t("Now, <b>add your Opportunity</b> to your Pipeline."),
|
||||
content: Markup(_t("Now, <b>add your Opportunity</b> to your Pipeline.")),
|
||||
position: "bottom",
|
||||
}, {
|
||||
trigger: ".o_opportunity_kanban .o_kanban_group:first-child .o_kanban_record:last-child .oe_kanban_content",
|
||||
extra_trigger: ".o_opportunity_kanban",
|
||||
content: _t("<b>Drag & drop opportunities</b> between columns as you progress in your sales cycle."),
|
||||
content: Markup(_t("<b>Drag & drop opportunities</b> between columns as you progress in your sales cycle.")),
|
||||
position: "right",
|
||||
run: "drag_and_drop .o_opportunity_kanban .o_kanban_group:eq(2) ",
|
||||
}, {
|
||||
trigger: ".o_kanban_record:not(.o_updating) .o_activity_color_default",
|
||||
extra_trigger: ".o_opportunity_kanban",
|
||||
content: _t("Looks like nothing is planned. :(<br><br><i>Tip : Schedule activities to keep track of everything you have to do!</i>"),
|
||||
content: Markup(_t("Looks like nothing is planned. :(<br><br><i>Tip : Schedule activities to keep track of everything you have to do!</i>")),
|
||||
position: "bottom",
|
||||
}, {
|
||||
trigger: ".o_schedule_activity",
|
||||
extra_trigger: ".o_opportunity_kanban",
|
||||
content: _t("Let's <b>Schedule an Activity.</b>"),
|
||||
content: Markup(_t("Let's <b>Schedule an Activity.</b>")),
|
||||
position: "bottom",
|
||||
width: 200,
|
||||
}, {
|
||||
trigger: '.modal-footer button[name="action_close_dialog"]',
|
||||
content: _t("All set. Let’s <b>Schedule</b> it."),
|
||||
content: Markup(_t("All set. Let’s <b>Schedule</b> it.")),
|
||||
position: "top", // dot NOT move to bottom, it would cause a resize flicker, see task-2476595
|
||||
run: function (actions) {
|
||||
actions.auto('.modal-footer button[special=cancel]');
|
||||
@@ -67,7 +68,7 @@ tour.register('crm_tour', {
|
||||
}, {
|
||||
id: "drag_opportunity_to_won_step",
|
||||
trigger: ".o_opportunity_kanban .o_kanban_record:last-child",
|
||||
content: _t("Drag your opportunity to <b>Won</b> when you get the deal. Congrats !"),
|
||||
content: Markup(_t("Drag your opportunity to <b>Won</b> when you get the deal. Congrats !")),
|
||||
position: "bottom",
|
||||
run: "drag_and_drop .o_opportunity_kanban .o_kanban_group:eq(3) ",
|
||||
}, {
|
||||
|
||||
@@ -2,6 +2,7 @@ odoo.define('crm_iap_lead.generate_leads_steps', function (require) {
|
||||
"use strict";
|
||||
|
||||
var tour = require('web_tour.tour');
|
||||
const {Markup} = require('web.utils');
|
||||
var core = require('web.core');
|
||||
|
||||
require('crm.tour');
|
||||
@@ -20,7 +21,7 @@ tour.tours.crm_tour.steps.splice(DragOppToWonStepIndex + 1, 0, {
|
||||
*
|
||||
*/
|
||||
trigger: ".o_button_generate_leads",
|
||||
content: _t("Looking for more opportunities ?<br>Try the <b>Lead Generation</b> tool."),
|
||||
content: Markup(_t("Looking for more opportunities ?<br>Try the <b>Lead Generation</b> tool.")),
|
||||
position: "bottom",
|
||||
run: function (actions) {
|
||||
actions.auto('.o_button_generate_leads');
|
||||
@@ -38,4 +39,4 @@ tour.tours.crm_tour.steps.splice(DragOppToWonStepIndex + 1, 0, {
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
@@ -18,8 +18,8 @@ return EventAdditionalTourSteps;
|
||||
odoo.define('event.event_tour', function (require) {
|
||||
"use strict";
|
||||
|
||||
var core = require('web.core');
|
||||
var _t = core._t;
|
||||
const {_t} = require('web.core');
|
||||
const {Markup} = require('web.utils');
|
||||
|
||||
var tour = require('web_tour.tour');
|
||||
var EventAdditionalTourSteps = require('event.event_steps');
|
||||
@@ -30,33 +30,33 @@ tour.register('event_tour', {
|
||||
sequence: 210,
|
||||
}, [tour.stepUtils.showAppsMenuItem(), {
|
||||
trigger: '.o_app[data-menu-xmlid="event.event_main_menu"]',
|
||||
content: _t("Ready to <b>organize events</b> in a few minutes? Let's get started!"),
|
||||
content: Markup(_t("Ready to <b>organize events</b> in a few minutes? Let's get started!")),
|
||||
position: 'bottom',
|
||||
edition: 'enterprise',
|
||||
}, {
|
||||
trigger: '.o_app[data-menu-xmlid="event.event_main_menu"]',
|
||||
content: _t("Ready to <b>organize events</b> in a few minutes? Let's get started!"),
|
||||
content: Markup(_t("Ready to <b>organize events</b> in a few minutes? Let's get started!")),
|
||||
edition: 'community',
|
||||
}, {
|
||||
trigger: '.o-kanban-button-new',
|
||||
extra_trigger: '.o_event_kanban_view',
|
||||
content: _t("Let's create your first <b>event</b>."),
|
||||
content: Markup(_t("Let's create your first <b>event</b>.")),
|
||||
position: 'bottom',
|
||||
width: 175,
|
||||
}, {
|
||||
trigger: '.o_event_form_view input[name="name"]',
|
||||
content: _t("This is the <b>name</b> your guests will see when registering."),
|
||||
content: Markup(_t("This is the <b>name</b> your guests will see when registering.")),
|
||||
run: 'text Odoo Experience 2020',
|
||||
}, {
|
||||
trigger: '.o_event_form_view input[name="date_end"]',
|
||||
content: _t("When will your event take place? <b>Select</b> the start and end dates <b>and click Apply</b>."),
|
||||
content: Markup(_t("When will your event take place? <b>Select</b> the start and end dates <b>and click Apply</b>.")),
|
||||
run: function () {
|
||||
$('input[name="date_begin"]').val('09/30/2020 08:00:00').change();
|
||||
$('input[name="date_end"]').val('10/02/2020 23:00:00').change();
|
||||
},
|
||||
}, {
|
||||
trigger: '.o_event_form_view div[name="event_ticket_ids"] .o_field_x2many_list_row_add a',
|
||||
content: _t("Ticket types allow you to distinguish your attendees. Let's <b>create</b> a new one."),
|
||||
content: Markup(_t("Ticket types allow you to distinguish your attendees. Let's <b>create</b> a new one.")),
|
||||
}, ...new EventAdditionalTourSteps()._get_website_event_steps(), {
|
||||
trigger: '.o_event_form_view div[name="stage_id"]',
|
||||
extra_trigger: 'div.o_form_buttons_view:not(.o_hidden)',
|
||||
@@ -65,7 +65,7 @@ tour.register('event_tour', {
|
||||
}, {
|
||||
trigger: 'ol.breadcrumb li.breadcrumb-item:first',
|
||||
extra_trigger: '.o_event_form_view div[name="stage_id"]',
|
||||
content: _t("Use the <b>breadcrumbs</b> to go back to your kanban overview."),
|
||||
content: Markup(_t("Use the <b>breadcrumbs</b> to go back to your kanban overview.")),
|
||||
position: 'bottom',
|
||||
run: 'click',
|
||||
}].filter(Boolean));
|
||||
|
||||
@@ -145,7 +145,7 @@
|
||||
<strong><t t-esc="record.stat_this_month.raw_value"/></strong> this month
|
||||
</div>
|
||||
<div t-if="!widget.isHtmlEmpty(record.description.value)" class="o_kanban_badge_description">
|
||||
<em t-raw="record.description.raw_value"/>
|
||||
<em t-out="record.description.value"/>
|
||||
<div>
|
||||
<t t-foreach="record.unique_owner_ids.raw_value.slice(0,11)" t-as="owner">
|
||||
<img class="oe_kanban_avatar o_image_24_cover" t-att-src="kanban_image('res.users', 'avatar_128', owner)" t-att-data-member_id="owner" alt="Owner"/>
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
odoo.define('hr_expense.tour', function(require) {
|
||||
"use strict";
|
||||
|
||||
var core = require('web.core');
|
||||
const {_t} = require('web.core');
|
||||
const {Markup} = require('web.utils');
|
||||
var tour = require('web_tour.tour');
|
||||
|
||||
var _t = core._t;
|
||||
|
||||
tour.register('hr_expense_tour' , {
|
||||
url: "/web"
|
||||
}, [tour.stepUtils.showAppsMenuItem(), {
|
||||
@@ -20,7 +19,7 @@ tour.register('hr_expense_tour' , {
|
||||
edition: 'enterprise'
|
||||
}, {
|
||||
trigger: '.o_form_button_save',
|
||||
content: _t("<p>Once your <b> Expense </b> is ready, you can save it.</p>"),
|
||||
content: Markup(_t("<p>Once your <b> Expense </b> is ready, you can save it.</p>")),
|
||||
position: 'bottom',
|
||||
}, {
|
||||
trigger: '.o_attach_document',
|
||||
@@ -29,28 +28,28 @@ tour.register('hr_expense_tour' , {
|
||||
}, {
|
||||
trigger: '.o_expense_submit',
|
||||
extra_triggger: ".o_expense_form",
|
||||
content: _t('<p>Click on <b> Create Report </b> to create the report.</p>'),
|
||||
content: Markup(_t('<p>Click on <b> Create Report </b> to create the report.</p>')),
|
||||
position: 'right',
|
||||
}, {
|
||||
trigger: '.o_expense_tree input[type=checkbox]',
|
||||
content: _t('<p>Select expenses to submit them to your manager</p>'),
|
||||
content: Markup(_t('<p>Select expenses to submit them to your manager</p>')),
|
||||
position: 'bottom'
|
||||
}, {
|
||||
trigger: '.o_dropdown_toggler',
|
||||
extra_trigger: ".o_expense_tree",
|
||||
content: _t('<p>Click on <b> Action Create Report </b> to submit selected expenses to your manager</p>'),
|
||||
content: Markup(_t('<p>Click on <b> Action Create Report </b> to submit selected expenses to your manager</p>')),
|
||||
position: 'right',
|
||||
}, {
|
||||
trigger: '.o_expense_sheet_submit',
|
||||
content: _t('Once your <b>Expense report</b> is ready, you can submit it to your manager and wait for the approval from your manager.'),
|
||||
content: Markup(_t('Once your <b>Expense report</b> is ready, you can submit it to your manager and wait for the approval from your manager.')),
|
||||
position: 'bottom',
|
||||
}, {
|
||||
trigger: '.o_expense_sheet_approve',
|
||||
content: _t("<p>Approve the report here.</p><p>Tip: if you refuse, don’t forget to give the reason thanks to the hereunder message tool</p>"),
|
||||
content: Markup(_t("<p>Approve the report here.</p><p>Tip: if you refuse, don’t forget to give the reason thanks to the hereunder message tool</p>")),
|
||||
position: 'bottom',
|
||||
}, {
|
||||
trigger: '.o_expense_sheet_post',
|
||||
content: _t("<p>The accountant receive approved expense reports.</p><p>He can post journal entries in one click if taxes and accounts are right.</p>"),
|
||||
content: Markup(_t("<p>The accountant receive approved expense reports.</p><p>He can post journal entries in one click if taxes and accounts are right.</p>")),
|
||||
position: 'bottom',
|
||||
}, {
|
||||
trigger: '.o_expense_sheet_pay',
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
odoo.define('hr_recruitment.tour', function(require) {
|
||||
"use strict";
|
||||
|
||||
var core = require('web.core');
|
||||
const {_t} = require('web.core');
|
||||
const {Markup} = require('web.utils');
|
||||
var tour = require('web_tour.tour');
|
||||
|
||||
var _t = core._t;
|
||||
|
||||
tour.register('hr_recruitment_tour',{
|
||||
url: "/web",
|
||||
rainbowManMessage: _t("<div>Great job! You hired a new colleague!</div><div>Try the Website app to publish job offers online.</div>"),
|
||||
@@ -13,12 +12,12 @@ tour.register('hr_recruitment_tour',{
|
||||
sequence: 230,
|
||||
}, [tour.stepUtils.showAppsMenuItem(), {
|
||||
trigger: '.o_app[data-menu-xmlid="hr_recruitment.menu_hr_recruitment_root"]',
|
||||
content: _t("Let's have a look at how to <b>improve</b> your <b>hiring process</b>."),
|
||||
content: Markup(_t("Let's have a look at how to <b>improve</b> your <b>hiring process</b>.")),
|
||||
position: 'right',
|
||||
edition: 'community'
|
||||
}, {
|
||||
trigger: '.o_app[data-menu-xmlid="hr_recruitment.menu_hr_recruitment_root"]',
|
||||
content: _t("Let's have a look at how to <b>improve</b> your <b>hiring process</b>."),
|
||||
content: Markup(_t("Let's have a look at how to <b>improve</b> your <b>hiring process</b>.")),
|
||||
position: 'bottom',
|
||||
edition: 'enterprise'
|
||||
}, {
|
||||
@@ -62,24 +61,24 @@ tour.register('hr_recruitment_tour',{
|
||||
}, {
|
||||
trigger: ".oe_kanban_action_button",
|
||||
extra_trigger: '.o_hr_recruitment_kanban',
|
||||
content: _t("<b>Did you apply by sending an email?</b> Check incoming applications."),
|
||||
content: Markup(_t("<b>Did you apply by sending an email?</b> Check incoming applications.")),
|
||||
position: "bottom"
|
||||
}, {
|
||||
trigger: ".oe_kanban_card",
|
||||
extra_trigger: '.o_kanban_applicant',
|
||||
content: _t("<b>Drag this card</b>, to qualify him for a first interview."),
|
||||
content: Markup(_t("<b>Drag this card</b>, to qualify him for a first interview.")),
|
||||
position: "bottom",
|
||||
run: "drag_and_drop .o_kanban_group:eq(1) ",
|
||||
}, {
|
||||
trigger: ".oe_kanban_card",
|
||||
extra_trigger: '.o_kanban_applicant',
|
||||
content: _t("<b>Click to view</b> the application."),
|
||||
content: Markup(_t("<b>Click to view</b> the application.")),
|
||||
position: "bottom",
|
||||
width: 195
|
||||
}, {
|
||||
trigger: ".o_Chatter .o_ChatterTopbar_buttonSendMessage",
|
||||
extra_trigger: '.o_applicant_form',
|
||||
content: _t("<div><b>Try to send an email</b> to the applicant.</div><div><i>Tips: All emails sent or received are saved in the history here</i>"),
|
||||
content: Markup(_t("<div><b>Try to send an email</b> to the applicant.</div><div><i>Tips: All emails sent or received are saved in the history here</i>")),
|
||||
position: "bottom"
|
||||
}, {
|
||||
trigger: ".o_Chatter .o_Composer_buttonSend",
|
||||
|
||||
@@ -71,31 +71,25 @@ var LivechatButton = Widget.extend({
|
||||
this._messages = [];
|
||||
this._serverURL = serverURL;
|
||||
},
|
||||
willStart: function () {
|
||||
var self = this;
|
||||
async willStart() {
|
||||
var cookie = utils.get_cookie('im_livechat_session');
|
||||
var ready;
|
||||
if (!cookie) {
|
||||
ready = session.rpc('/im_livechat/init', { channel_id: this.options.channel_id })
|
||||
.then(function (result) {
|
||||
if (!result.available_for_me) {
|
||||
return Promise.reject();
|
||||
}
|
||||
self._rule = result.rule;
|
||||
});
|
||||
} else {
|
||||
if (cookie) {
|
||||
var channel = JSON.parse(cookie);
|
||||
ready = session.rpc('/mail/chat_history', { uuid: channel.uuid, limit: 100 })
|
||||
.then(function (history) {
|
||||
self._history = history;
|
||||
});
|
||||
this._history = await session.rpc('/mail/chat_history', {uuid: channel.uuid, limit: 100});
|
||||
this._history.reverse().forEach(message => { message.body = utils.Markup(message.body); });
|
||||
} else {
|
||||
const result = await session.rpc('/im_livechat/init', {channel_id: this.options.channel_id});
|
||||
if (!result.available_for_me) {
|
||||
return Promise.reject();
|
||||
}
|
||||
this._rule = result.rule;
|
||||
}
|
||||
return ready.then(this._loadQWebTemplate.bind(this));
|
||||
return this._loadQWebTemplate();
|
||||
},
|
||||
start: function () {
|
||||
this.$el.text(this.options.button_text);
|
||||
if (this._history) {
|
||||
_.each(this._history.reverse(), this._addMessage.bind(this));
|
||||
this._history.forEach(m => this._addMessage(m));
|
||||
this._openChat();
|
||||
} else if (!config.device.isMobile && this._rule.action === 'auto_popup') {
|
||||
var autoPopupCookie = utils.get_cookie('im_livechat_auto_popup');
|
||||
@@ -204,6 +198,7 @@ var LivechatButton = Widget.extend({
|
||||
if (this._messages.some(message => message.getID() === notificationData.id)) {
|
||||
return;
|
||||
}
|
||||
notificationData.body = utils.Markup(notificationData.body);
|
||||
this._addMessage(notificationData);
|
||||
if (this._chatWindow.isFolded() || !this._chatWindow.isAtBottom()) {
|
||||
this._livechat.incrementUnreadCounter();
|
||||
|
||||
@@ -400,7 +400,7 @@
|
||||
</span>
|
||||
</p>
|
||||
<div class="o_thread_message_content">
|
||||
<t t-raw="message.getBody()"/>
|
||||
<t t-out="message.getBody()"/>
|
||||
<t t-if="message.hasTrackingValues()">
|
||||
<t t-if="message.hasSubtypeDescription()">
|
||||
<p><t t-esc="message.getSubtypeDescription()"/></p>
|
||||
|
||||
@@ -7,6 +7,7 @@ odoo.define('lunch.LunchControllerCommon', function (require) {
|
||||
|
||||
var session = require('web.session');
|
||||
var core = require('web.core');
|
||||
const {Markup} = require('web.utils');
|
||||
var LunchWidget = require('lunch.LunchWidget');
|
||||
var LunchPaymentDialog = require('lunch.LunchPaymentDialog');
|
||||
|
||||
@@ -59,14 +60,17 @@ var LunchControllerCommon = {
|
||||
},
|
||||
});
|
||||
},
|
||||
_fetchWidgetData: async function () {
|
||||
this.widgetData = await this._rpc({
|
||||
async _fetchWidgetData() {
|
||||
const widgetData = await this._rpc({
|
||||
route: '/lunch/infos',
|
||||
params: {
|
||||
user_id: this.searchModel.get('userId'),
|
||||
context: this.context,
|
||||
},
|
||||
});
|
||||
widgetData.wallet = parseFloat(widgetData.wallet).toFixed(2);
|
||||
(widgetData.alerts || []).forEach(alert => { alert.message = Markup(alert.message); });
|
||||
this.widgetData = widgetData;
|
||||
},
|
||||
/**
|
||||
* Renders and appends the lunch banner widget.
|
||||
@@ -74,12 +78,10 @@ var LunchControllerCommon = {
|
||||
* @private
|
||||
*/
|
||||
_renderLunchWidget: function () {
|
||||
var self = this;
|
||||
var oldWidget = this.widget;
|
||||
this.widgetData.wallet = parseFloat(this.widgetData.wallet).toFixed(2);
|
||||
this.widget = new LunchWidget(this, _.extend(this.widgetData, {edit: this.editMode}));
|
||||
return this.widget.appendTo(document.createDocumentFragment()).then(function () {
|
||||
self.$('.o_lunch_content').prepend(self.widget.$el);
|
||||
this.widget = new LunchWidget(this, Object.assign(this.widgetData, {edit: this.editMode}));
|
||||
return this.widget.appendTo(document.createDocumentFragment()).then(() => {
|
||||
this.$('.o_lunch_content').prepend(this.widget.$el);
|
||||
if (oldWidget) {
|
||||
oldWidget.destroy();
|
||||
}
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates>
|
||||
<div t-name="LunchPreviousOrdersWidgetNoOrder" class="col-lg-12">
|
||||
<h3>This is the first time you order a meal</h3>
|
||||
<p class="text-muted">Select a product and put your order comments on the note.</p>
|
||||
<p class="text-muted">Your favorite meals will be created based on your last orders.</p>
|
||||
<p class="text-muted">Don't forget the alerts displayed in the reddish area</p>
|
||||
</div>
|
||||
<div t-name="LunchPreviousOrdersWidgetList" class="row">
|
||||
<div t-foreach="categories" t-as="supplier" class="col-lg-4">
|
||||
<h3><t t-esc="supplier"/></h3>
|
||||
<div t-foreach='categories[supplier]' t-as='order' class="o_lunch_vignette">
|
||||
<button type="button" class="float-right o_add_button oe_edit_only oe_link" t-att-data-id="order.line_id">
|
||||
<span class="fa fa-plus-square"></span>
|
||||
<span>Add</span>
|
||||
</button>
|
||||
<div>
|
||||
<t t-esc="order.product_name"/>
|
||||
<span class="badge badge-pill float-right">
|
||||
<span class="o_lunch_price" t-raw="formatValue(order)"/>
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-muted">
|
||||
<t t-if="order.note != false" t-esc="order.note"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</templates>
|
||||
@@ -3,7 +3,7 @@
|
||||
<span t-name="LunchWidget">
|
||||
<t t-foreach="widget.alerts" t-as="alert">
|
||||
<div class="alert alert-warning mb-0" role="alert">
|
||||
<t t-raw="alert.message"/> <!-- alert.message is coming from a fields.Html so it should be safe -->
|
||||
<t t-out="alert.message"/>
|
||||
</div>
|
||||
</t>
|
||||
<div class="o_lunch_banner container-fluid">
|
||||
|
||||
@@ -73,11 +73,12 @@ QUnit.module('LunchListView', {
|
||||
position: "after"
|
||||
},
|
||||
user_location: [2, "Office 2"],
|
||||
alerts: [{id: 42, message: '<b>Warning! Neurotoxin pressure has reached dangerously unlethal levels.</b>'}]
|
||||
};
|
||||
},
|
||||
}, function () {
|
||||
QUnit.test('basic rendering', async function (assert) {
|
||||
assert.expect(6);
|
||||
assert.expect(9);
|
||||
|
||||
const list = await createLunchView({
|
||||
View: LunchListView,
|
||||
@@ -108,6 +109,11 @@ QUnit.module('LunchListView', {
|
||||
assert.containsOnce(list, '.o_lunch_content > span > .o_lunch_banner',
|
||||
"should have a 'lunch' banner");
|
||||
|
||||
const $alertMessage = list.$('.alert > *');
|
||||
assert.equal($alertMessage.length, 1);
|
||||
assert.equal($alertMessage.prop('tagName'), 'B');
|
||||
assert.equal($alertMessage.text(), "Warning! Neurotoxin pressure has reached dangerously unlethal levels.")
|
||||
|
||||
list.destroy();
|
||||
});
|
||||
|
||||
|
||||
@@ -121,7 +121,7 @@
|
||||
</div>
|
||||
<div class="o_kanban_record_bottom">
|
||||
<ul>
|
||||
<li t-raw="record.description.raw_value" class="text-muted"/>
|
||||
<li t-out="record.description.value" class="text-muted"/>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -165,7 +165,7 @@
|
||||
</div>
|
||||
<div class="o_kanban_record_bottom">
|
||||
<ul>
|
||||
<li t-raw="record.description.raw_value" class="text-muted"/>
|
||||
<li t-out="record.description.value" class="text-muted"/>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -539,109 +539,6 @@ var BasicActivity = AbstractField.extend({
|
||||
},
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Activities Widget for Form views ('mail_activity' widget)
|
||||
// -----------------------------------------------------------------------------
|
||||
// FIXME seems to still be needed in some cases like systray
|
||||
var Activity = BasicActivity.extend({
|
||||
className: 'o_mail_activity',
|
||||
events: _.extend({}, BasicActivity.prototype.events, {
|
||||
'click a': '_onClickRedirect',
|
||||
}),
|
||||
specialData: '_fetchSpecialActivity',
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
init: function () {
|
||||
this._super.apply(this, arguments);
|
||||
this._activities = this.record.specialData[this.name];
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
destroy: function () {
|
||||
this._unbindOnUploadAction();
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
|
||||
//------------------------------------------------------------
|
||||
// Private
|
||||
//------------------------------------------------------------
|
||||
/**
|
||||
* @private
|
||||
* @param {Object} fieldsToReload
|
||||
*/
|
||||
_reload: function (fieldsToReload) {
|
||||
this.trigger_up('reload_mail_fields', fieldsToReload);
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
* @private
|
||||
*/
|
||||
_render: function () {
|
||||
_.each(this._activities, function (activity) {
|
||||
var note = mailUtils.parseAndTransform(activity.note || '', mailUtils.inline);
|
||||
var is_blank = (/^\s*$/).test(note);
|
||||
if (!is_blank) {
|
||||
activity.note = mailUtils.parseAndTransform(activity.note, mailUtils.addLink);
|
||||
} else {
|
||||
activity.note = '';
|
||||
}
|
||||
});
|
||||
var activities = setFileUploadID(setDelayLabel(this._activities));
|
||||
if (activities.length) {
|
||||
var nbActivities = _.countBy(activities, 'state');
|
||||
this.$el.html(QWeb.render('mail.activity_items', {
|
||||
uid: session.uid,
|
||||
activities: activities,
|
||||
nbPlannedActivities: nbActivities.planned,
|
||||
nbTodayActivities: nbActivities.today,
|
||||
nbOverdueActivities: nbActivities.overdue,
|
||||
dateFormat: time.getLangDateFormat(),
|
||||
datetimeFormat: time.getLangDatetimeFormat(),
|
||||
session: session,
|
||||
widget: this,
|
||||
}));
|
||||
this._bindOnUploadAction(this._activities);
|
||||
} else {
|
||||
this._unbindOnUploadAction(this._activities);
|
||||
this.$el.empty();
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
* @private
|
||||
* @param {Object} record
|
||||
*/
|
||||
_reset: function (record) {
|
||||
this._super.apply(this, arguments);
|
||||
this._activities = this.record.specialData[this.name];
|
||||
// the mail widgets being persistent, one need to update the res_id on reset
|
||||
this.res_id = record.res_id;
|
||||
},
|
||||
|
||||
//------------------------------------------------------------
|
||||
// Handlers
|
||||
//------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
_onClickRedirect: function (ev) {
|
||||
var id = $(ev.currentTarget).data('oe-id');
|
||||
if (id) {
|
||||
ev.preventDefault();
|
||||
var model = $(ev.currentTarget).data('oe-model');
|
||||
this.trigger_up('redirect', {
|
||||
res_id: id,
|
||||
res_model: model,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Activities Widget for Kanban views ('kanban_activity' widget)
|
||||
// -----------------------------------------------------------------------------
|
||||
@@ -861,5 +758,3 @@ field_registry
|
||||
.add('kanban_activity', KanbanActivity)
|
||||
.add('list_activity', ListActivity)
|
||||
.add('activity_exception', ActivityException);
|
||||
|
||||
export default Activity;
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import core from 'web.core';
|
||||
import {_t} from 'web.core';
|
||||
import {Markup} from 'web.utils';
|
||||
import tour from 'web_tour.tour';
|
||||
|
||||
var _t = core._t;
|
||||
|
||||
tour.register('mail_tour', {
|
||||
url: "/web#action=mail.widgets.discuss",
|
||||
sequence: 80,
|
||||
}, [{
|
||||
trigger: '.o_DiscussSidebar_groupChannel .o_DiscussSidebar_groupHeaderItemAdd',
|
||||
content: _t("<p>Channels make it easy to organize information across different topics and groups.</p> <p>Try to <b>create your first channel</b> (e.g. sales, marketing, product XYZ, after work party, etc).</p>"),
|
||||
content: Markup(_t("<p>Channels make it easy to organize information across different topics and groups.</p> <p>Try to <b>create your first channel</b> (e.g. sales, marketing, product XYZ, after work party, etc).</p>")),
|
||||
position: 'bottom',
|
||||
}, {
|
||||
trigger: '.o_DiscussSidebar_itemNewInput',
|
||||
content: _t("<p>Create a channel here.</p>"),
|
||||
content: Markup(_t("<p>Create a channel here.</p>")),
|
||||
position: 'bottom',
|
||||
auto: true,
|
||||
run: function (actions) {
|
||||
@@ -23,14 +22,14 @@ tour.register('mail_tour', {
|
||||
},
|
||||
}, {
|
||||
trigger: ".o_DiscussSidebar_newChannelAutocompleteSuggestions",
|
||||
content: _t("<p>Create a public or private channel.</p>"),
|
||||
content: Markup(_t("<p>Create a public or private channel.</p>")),
|
||||
position: 'right',
|
||||
run() {
|
||||
this.$consumeEventAnchors.find('li:first').click();
|
||||
},
|
||||
}, {
|
||||
trigger: '.o_Discuss_thread .o_ComposerTextInput_textarea',
|
||||
content: _t("<p><b>Write a message</b> to the members of the channel here.</p> <p>You can notify someone with <i>'@'</i> or link another channel with <i>'#'</i>. Start your message with <i>'/'</i> to get the list of possible commands.</p>"),
|
||||
content: Markup(_t("<p><b>Write a message</b> to the members of the channel here.</p> <p>You can notify someone with <i>'@'</i> or link another channel with <i>'#'</i>. Start your message with <i>'/'</i> to get the list of possible commands.</p>")),
|
||||
position: "top",
|
||||
width: 350,
|
||||
run: function (actions) {
|
||||
@@ -43,7 +42,7 @@ tour.register('mail_tour', {
|
||||
position: "top",
|
||||
}, {
|
||||
trigger: '.o_Discuss_thread .o_Message_commandStar',
|
||||
content: _t("Messages can be <b>starred</b> to remind you to check back later."),
|
||||
content: Markup(_t("Messages can be <b>starred</b> to remind you to check back later.")),
|
||||
position: "bottom",
|
||||
}, {
|
||||
trigger: '.o_DiscussSidebarItem.o-starred-box',
|
||||
@@ -51,6 +50,6 @@ tour.register('mail_tour', {
|
||||
position: "bottom",
|
||||
}, {
|
||||
trigger: '.o_DiscussSidebar_groupChat .o_DiscussSidebar_groupHeaderItemAdd',
|
||||
content: _t("<p><b>Chat with coworkers</b> in real-time using direct messages.</p><p><i>You might need to invite users from the Settings app first.</i></p>"),
|
||||
content: Markup(_t("<p><b>Chat with coworkers</b> in real-time using direct messages.</p><p><i>You might need to invite users from the Settings app first.</i></p>")),
|
||||
position: 'bottom',
|
||||
}]);
|
||||
|
||||
+1
-1
@@ -30,7 +30,7 @@ const PartnerInviteDialog = Dialog.extend({
|
||||
this.channelId = channel.id;
|
||||
this.env = env;
|
||||
this._super(parent, {
|
||||
title: _.str.sprintf(this.env._t("Invite people to #%s"), owl.utils.escape(channel.displayName)),
|
||||
title: _.str.sprintf(this.env._t("Invite people to #%s"), channel.displayName),
|
||||
size: 'medium',
|
||||
buttons: [{
|
||||
text: this.env._t("Invite"),
|
||||
|
||||
@@ -1,113 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="mail.activity_items">
|
||||
<div class="o_thread_date_separator o_border_dashed" data-toggle="collapse" data-target="#o_chatter_planned_activities">
|
||||
<a role="button" class="o_thread_date btn">
|
||||
<i class="fa fa-fw fa-caret-down"/>
|
||||
Planned activities
|
||||
<small class="o_chatter_planned_activities_summary ml8">
|
||||
<span class="badge rounded-circle badge-danger"><t t-esc="nbOverdueActivities"/></span>
|
||||
<span class="badge rounded-circle badge-warning"><t t-esc="nbTodayActivities"/></span>
|
||||
<span class="badge rounded-circle badge-success"><t t-esc="nbPlannedActivities"/></span>
|
||||
</small>
|
||||
</a>
|
||||
</div>
|
||||
<div id="o_chatter_planned_activities" class="collapse show">
|
||||
<t t-foreach="activities" t-as="activity">
|
||||
<div class="o_thread_message" style="margin-bottom: 10px">
|
||||
<div class="o_thread_message_sidebar">
|
||||
<div class="o_avatar_stack">
|
||||
<img t-attf-src="/web/image#{activity.user_id[0] >= 0 ? ('/res.users/' + activity.user_id[0] + '/avatar_128') : ''}" class="o_thread_message_avatar rounded-circle mb8" t-att-title="activity.user_id[1]" t-att-alt="activity.user_id[1]"/>
|
||||
<i t-att-class="'o_avatar_icon fa ' + activity.icon + ' bg-' + (activity.state == 'planned'? 'success' : (activity.state == 'today'? 'warning' : 'danger'))"
|
||||
t-att-title="activity.activity_type_id[1]"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="o_thread_message_core">
|
||||
<div class="o_mail_info text-muted">
|
||||
<strong><span t-attf-class="o_activity_date o_activity_color_#{activity.state}"><t t-esc="activity.label_delay" /></span></strong>:
|
||||
<strong t-if="activity.summary" class="o_activity_summary"> “<t t-esc="activity.summary"/>”</strong>
|
||||
<strong t-if="!activity.summary"> <t t-esc="activity.activity_type_id[1]" /></strong>
|
||||
<em> for </em>
|
||||
<t t-esc="activity.user_id[1]" />
|
||||
<a class="btn btn-link btn-info text-muted collapsed o_activity_info ml4" role="button" data-toggle="collapse" t-attf-data-target="#o_chatter_activity_info_#{activity.id}">
|
||||
<i class="fa fa-info-circle" role="img" aria-label="Info" title="Info"></i>
|
||||
</a>
|
||||
<div class="o_thread_message_collapse collapse" t-attf-id="o_chatter_activity_info_#{activity.id}">
|
||||
<dl class="dl-horizontal">
|
||||
<dt>Activity type</dt>
|
||||
<dd class="mb8">
|
||||
<t t-esc="activity.activity_type_id[1]"/>
|
||||
</dd>
|
||||
<dt>Created on</dt>
|
||||
<dd class="mb8">
|
||||
<t t-esc="activity.create_date.format(datetimeFormat)"/>
|
||||
by
|
||||
<img t-attf-src="/web/image#{activity.create_uid[0] >= 0 ? ('/res.users/' + activity.create_uid[0] + '/avatar_128') : ''}"
|
||||
height="18" width="18"
|
||||
class="o_object_fit_cover rounded-circle mr4"
|
||||
t-att-title="activity.create_uid[1]"
|
||||
t-att-alt="activity.create_uid[1]"/>
|
||||
<b><t t-esc="activity.create_uid[1]"/></b>
|
||||
</dd>
|
||||
<dt>Assigned to</dt>
|
||||
<dd class="mb8">
|
||||
<img t-attf-src="/web/image#{activity.user_id[0] >= 0 ? ('/res.users/' + activity.user_id[0] + '/avatar_128') : ''}" height="18" width="18" class="o_object_fit_cover rounded-circle mr4" t-att-title="activity.user_id[1]" t-att-alt="activity.user_id[1]"/>
|
||||
<b><t t-esc="activity.user_id[1]"/></b>
|
||||
<em>, due on </em><span t-attf-class="o_activity_color_#{activity.state}"><t t-esc="activity.date_deadline.format(dateFormat)"/></span>
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
<div t-if="activity.note" t-attf-class="o_thread_message_#{activity.activity_decoration ? activity.activity_decoration : 'note'} #{activity.activity_decoration ? 'alert alert-' + activity.activity_decoration : ''}">
|
||||
<t t-raw="activity.note"/>
|
||||
</div>
|
||||
<t t-if="activity.mail_template_ids && activity.mail_template_ids.length > 0">
|
||||
<div class="mt16" t-att-data-activity-id="activity.id" t-att-data-previous-activity-type-id="activity.activity_type_id[0]">
|
||||
<t t-foreach="activity.mail_template_ids" t-as="mail_template">
|
||||
<div>
|
||||
<i class="fa fa-envelope-o" aria-label="Mail" title="Mail" role="img"></i>
|
||||
<span t-esc="mail_template.name"/>:
|
||||
<span class="o_activity_template_preview" t-att-data-template-id="mail_template.id">Preview</span>
|
||||
<span class="text-muted">or</span>
|
||||
<span class="o_activity_template_send" t-att-data-template-id="mail_template.id">Send Now</span>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
<div class="o_thread_message_tools btn-group">
|
||||
<t t-call="mail.activity_thread_message_tools"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
<t t-name="mail.activity_thread_message_tools">
|
||||
<div t-if="activity.can_write" class="o_thread_message_tools btn-group">
|
||||
<span t-if="activity.activity_category !== 'upload_file'" class="o_mark_as_done" data-toggle="popover" t-att-data-activity-id="activity.id" t-att-data-chaining-type-activity="activity.chaining_type" t-att-data-previous-activity-type-id="activity.activity_type_id[0]">
|
||||
<a role="button" href="#" class="btn btn-link btn-success text-muted o_activity_link mr8">
|
||||
<i class="fa fa-check"/> Mark Done </a>
|
||||
</span>
|
||||
<span t-if="activity.activity_category === 'upload_file'" class="o_mark_as_done_upload_file" t-att-data-activity-id="activity.id" t-att-data-chaining-type-activity="activity.chaining_type" t-att-data-previous-activity-type-id="activity.activity_type_id[0]" t-att-data-fileupload-id="activity.fileuploadID">
|
||||
<a role="button" href="#" class="btn btn-link btn-success text-muted o_activity_link mr8">
|
||||
<i class="fa fa-upload"/> Upload Document </a>
|
||||
</span>
|
||||
<span t-if="activity.activity_category === 'upload_file'" class="d-none">
|
||||
<t t-call="HiddenInputFile">
|
||||
<t t-set="fileupload_id" t-value="activity.fileuploadID"/>
|
||||
<t t-set="fileupload_action" t-translation="off">/web/binary/upload_attachment</t>
|
||||
<input type="hidden" name="model" t-att-value="activity.res_model"/>
|
||||
<input type="hidden" name="id" t-att-value="activity.res_id"/>
|
||||
</t>
|
||||
</span>
|
||||
<a role="button" href="#" class="btn btn-link btn-secondary text-muted o_edit_activity o_activity_link" t-att-data-activity-id="activity.id">
|
||||
<i class="fa fa-pencil"/> Edit
|
||||
</a>
|
||||
<a role="button" href="#" class="btn btn-link btn-danger text-muted o_unlink_activity o_activity_link" t-att-data-activity-id="activity.id">
|
||||
<i class="fa fa-times"/> Cancel
|
||||
</a>
|
||||
</div>
|
||||
</t>
|
||||
<t t-name="mail.activity_feedback_form">
|
||||
<div>
|
||||
<textarea class="form-control" rows="3" id="activity_feedback" placeholder="Write Feedback"/>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<div t-name="mail.legacy.Composer.emojis" class="o_mail_emoji_container">
|
||||
<t t-foreach="emojis" t-as="emoji">
|
||||
<span t-att-data-emoji="emoji.sources[0]" class="o_mail_emoji" t-att-title="emoji.description" t-att-aria-label="emoji.description">
|
||||
<t t-raw="emoji.unicode"/>
|
||||
<t t-out="emoji.unicode"/>
|
||||
</span>
|
||||
</t>
|
||||
</div>
|
||||
|
||||
@@ -457,10 +457,10 @@
|
||||
<div class="o_kanban_record_bottom">
|
||||
<div class="oe_kanban_bottom_left">
|
||||
<div class="badge badge-danger" t-if="!selection_mode and record.maintenance_open_count.raw_value" >
|
||||
<t t-raw="record.maintenance_open_count.raw_value"/> Request
|
||||
<t t-out="record.maintenance_open_count.raw_value"/> Request
|
||||
</div>
|
||||
<div class="badge badge-secondary" t-if="!selection_mode and record.next_action_date.raw_value" >
|
||||
<t t-raw="moment(record.next_action_date.raw_value).format('MMMM Do')"/>
|
||||
<t t-out="moment(record.next_action_date.raw_value).format('MMMM Do')"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="oe_kanban_bottom_right">
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
odoo.define('mass_mailing.mass_mailing_tour', function (require) {
|
||||
"use strict";
|
||||
|
||||
var core = require('web.core');
|
||||
var _t = core._t;
|
||||
const {_t} = require('web.core');
|
||||
const {Markup} = require('web.utils');
|
||||
var tour = require('web_tour.tour');
|
||||
var now = moment();
|
||||
|
||||
@@ -22,11 +22,11 @@ odoo.define('mass_mailing.mass_mailing_tour', function (require) {
|
||||
edition: 'community',
|
||||
}, {
|
||||
trigger: '.o-kanban-button-new',
|
||||
content: _t("Start by creating your first <b>Mailing</b>."),
|
||||
content: Markup(_t("Start by creating your first <b>Mailing</b>.")),
|
||||
position: 'bottom',
|
||||
}, {
|
||||
trigger: 'input[name="subject"]',
|
||||
content: _t('Pick the <b>email subject</b>.'),
|
||||
content: Markup(_t('Pick the <b>email subject</b>.')),
|
||||
position: 'right',
|
||||
run: 'text ' + now.format("MMMM") + " Newsletter",
|
||||
}, {
|
||||
@@ -39,13 +39,13 @@ odoo.define('mass_mailing.mass_mailing_tour', function (require) {
|
||||
auto: true,
|
||||
}, {
|
||||
trigger: 'div[name="body_arch"] iframe #newsletter',
|
||||
content: _t('Choose this <b>theme</b>.'),
|
||||
content: Markup(_t('Choose this <b>theme</b>.')),
|
||||
position: 'left',
|
||||
edition: 'enterprise',
|
||||
run: 'click',
|
||||
}, {
|
||||
trigger: 'div[name="body_arch"] iframe #default',
|
||||
content: _t('Choose this <b>theme</b>.'),
|
||||
content: Markup(_t('Choose this <b>theme</b>.')),
|
||||
position: 'right',
|
||||
edition: 'community',
|
||||
run: 'click',
|
||||
@@ -80,7 +80,7 @@ odoo.define('mass_mailing.mass_mailing_tour', function (require) {
|
||||
run: "click",
|
||||
}, {
|
||||
trigger: '.o_back_button',
|
||||
content: _t("By using the <b>Breadcrumb</b>, you can navigate back to the overview."),
|
||||
content: Markup(_t("By using the <b>Breadcrumb</b>, you can navigate back to the overview.")),
|
||||
position: 'bottom',
|
||||
run: 'click',
|
||||
}]
|
||||
|
||||
@@ -84,7 +84,7 @@
|
||||
<a name="%(action_view_mass_mailings_from_campaign)d" type="action"
|
||||
t-attf-class="oe_mailings #{record.mailing_mail_ids.raw_value.length === 0 ? 'text-muted' : ''}"
|
||||
groups="mass_mailing.group_mass_mailing_campaign">
|
||||
<t t-raw="record.mailing_mail_ids.raw_value.length"/> Mailings
|
||||
<t t-out="record.mailing_mail_ids.raw_value.length"/> Mailings
|
||||
</a>
|
||||
</xpath>
|
||||
</field>
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
<xpath expr="//ul[@id='o_utm_actions']">
|
||||
<a name="action_redirect_to_mailing_sms" type="object"
|
||||
t-attf-class="oe_mailings #{record.mailing_sms_count.raw_value === 0 ? 'text-muted' : ''}">
|
||||
<t t-raw="record.mailing_sms_count.raw_value"/> SMS
|
||||
<t t-out="record.mailing_sms_count.raw_value"/> SMS
|
||||
</a>
|
||||
</xpath>
|
||||
</field>
|
||||
|
||||
@@ -124,11 +124,11 @@ class StockRule(models.Model):
|
||||
manufacture_delay = product.produce_delay
|
||||
delay += manufacture_delay
|
||||
if not bypass_delay_description:
|
||||
delay_description += '<tr><td>%s</td><td class="text-right">+ %d %s</td></tr>' % (_('Manufacturing Lead Time'), manufacture_delay, _('day(s)'))
|
||||
delay_description.append((_('Manufacturing Lead Time'), _('+ %d day(s)', manufacture_delay)))
|
||||
security_delay = manufacture_rule.picking_type_id.company_id.manufacturing_lead
|
||||
delay += security_delay
|
||||
if not bypass_delay_description:
|
||||
delay_description += '<tr><td>%s</td><td class="text-right">+ %d %s</td></tr>' % (_('Manufacture Security Lead Time'), security_delay, _('day(s)'))
|
||||
delay_description.append((_('Manufacture Security Lead Time'), _('+ %d day(s)', security_delay)))
|
||||
return delay, delay_description
|
||||
|
||||
def _push_prepare_move_copy_values(self, move_to_copy, new_date):
|
||||
|
||||
@@ -75,7 +75,7 @@ odoo.define('payment.payment_form_mixin', require => {
|
||||
const $checkedRadios = this.$('input[name="o_payment_radio"]:checked');
|
||||
if ($checkedRadios.length !== 1) { // Cannot find selected payment option, show dialog
|
||||
return new Dialog(null, {
|
||||
title: _.str.sprintf(_t("Error: %s"), _.str.escapeHTML(title)),
|
||||
title: _.str.sprintf(_t("Error: %s"), title),
|
||||
size: 'medium',
|
||||
$content: `<p>${_.str.escapeHTML(description) || ''}</p>`,
|
||||
buttons: [{text: _t("Ok"), close: true}]
|
||||
|
||||
@@ -2,8 +2,8 @@ odoo.define('payment.post_processing', function (require) {
|
||||
'use strict';
|
||||
|
||||
var publicWidget = require('web.public.widget');
|
||||
var ajax = require('web.ajax');
|
||||
var core = require('web.core');
|
||||
const {Markup} = require('web.utils');
|
||||
|
||||
var _t = core._t;
|
||||
|
||||
@@ -92,6 +92,9 @@ odoo.define('payment.post_processing', function (require) {
|
||||
display_values_list.forEach(function (display_values) {
|
||||
var key = 'tx_' + display_values.state;
|
||||
if(key in render_values) {
|
||||
if (display_values["display_message"]) {
|
||||
display_values.display_message = Markup(display_values.display_message)
|
||||
}
|
||||
render_values[key].push(display_values);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
<t t-if="tx['display_message']">
|
||||
<!-- display_message is the content of the HTML field associated
|
||||
with the current transaction state, set on the acquirer. -->
|
||||
<t t-raw="tx['display_message']"/>
|
||||
<t t-out="tx['display_message']"/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
Your payment is in pending state.<br/>
|
||||
@@ -75,7 +75,7 @@
|
||||
<t t-if="tx['display_message']">
|
||||
<!-- display_message is the content of the HTML field associated
|
||||
with the current transaction state, set on the acquirer. -->
|
||||
<t t-raw="tx['display_message']"/>
|
||||
<t t-out="tx['display_message']"/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
Your payment has been received but need to be confirmed manually.<br/>
|
||||
@@ -100,7 +100,7 @@
|
||||
<t t-if="tx['display_message']">
|
||||
<!-- display_message is the content of the HTML field associated
|
||||
with the current transaction state, set on the acquirer. -->
|
||||
<t t-raw="tx['display_message']"/>
|
||||
<t t-out="tx['display_message']"/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
We are waiting for the payment acquirer to confirm the payment.
|
||||
|
||||
@@ -132,7 +132,7 @@
|
||||
<img type="open" t-att-src="kanban_image('payment.acquirer', 'image_128', record.id.raw_value)" alt="Acquirer"/>
|
||||
</div>
|
||||
<h3 class="mt4"><t t-esc="record.name.value"/></h3>
|
||||
<t t-if="record.description.raw_value" t-raw="record.description.raw_value"/>
|
||||
<t t-if="record.description.raw_value" t-out="record.description.value"/>
|
||||
</div>
|
||||
<div class="o_payment_acquirer_bottom">
|
||||
<t t-if="installed">
|
||||
|
||||
@@ -5,6 +5,7 @@ import logging
|
||||
import pprint
|
||||
import re
|
||||
|
||||
import markupsafe
|
||||
import werkzeug
|
||||
|
||||
from odoo import _, http
|
||||
@@ -88,7 +89,7 @@ class OgoneController(http.Controller):
|
||||
# The content of the HTML is not sanitized to preserve the redirection in the script.
|
||||
# After redirection, the customer comes back to the directlink return URL and we proceed
|
||||
# with the (now authenticated) payment request.
|
||||
redirect_html = ustr(base64.b64decode(redirect_html_element.text))
|
||||
redirect_html = markupsafe.Markup(ustr(base64.b64decode(redirect_html_element.text)))
|
||||
return request.render(
|
||||
'payment_ogone.directlink_feedback', {'redirect_html': redirect_html}
|
||||
)
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
<template id="directlink_feedback" name="Payment processing page">
|
||||
<t t-call="web.layout">
|
||||
<t t-call-assets="web.assets_common"/>
|
||||
<t t-raw="redirect_html"/>
|
||||
<t t-out="redirect_html"/>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,30 +1,29 @@
|
||||
odoo.define('point_of_sale.tour', function(require) {
|
||||
"use strict";
|
||||
|
||||
var core = require('web.core');
|
||||
const {_t} = require('web.core');
|
||||
const {Markup} = require('web.utils');
|
||||
var tour = require('web_tour.tour');
|
||||
|
||||
var _t = core._t;
|
||||
|
||||
tour.register('point_of_sale_tour', {
|
||||
url: "/web",
|
||||
rainbowMan: false,
|
||||
sequence: 45,
|
||||
}, [tour.stepUtils.showAppsMenuItem(), {
|
||||
trigger: '.o_app[data-menu-xmlid="point_of_sale.menu_point_root"]',
|
||||
content: _t("Ready to launch your <b>point of sale</b>?"),
|
||||
content: Markup(_t("Ready to launch your <b>point of sale</b>?")),
|
||||
width: 215,
|
||||
position: 'right',
|
||||
edition: 'community'
|
||||
}, {
|
||||
trigger: '.o_app[data-menu-xmlid="point_of_sale.menu_point_root"]',
|
||||
content: _t("Ready to launch your <b>point of sale</b>?"),
|
||||
content: Markup(_t("Ready to launch your <b>point of sale</b>?")),
|
||||
width: 215,
|
||||
position: 'bottom',
|
||||
edition: 'enterprise'
|
||||
}, {
|
||||
trigger: ".o_pos_kanban button.oe_kanban_action_button",
|
||||
content: _t("<p>Ready to have a look at the <b>POS Interface</b>? Let's start our first session.</p>"),
|
||||
content: Markup(_t("<p>Ready to have a look at the <b>POS Interface</b>? Let's start our first session.</p>")),
|
||||
position: "bottom"
|
||||
}]);
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ const dom = require('web.dom');
|
||||
var publicWidget = require('web.public.widget');
|
||||
var time = require('web.time');
|
||||
var portalComposer = require('portal.composer');
|
||||
const {Markup} = require('web.utils');
|
||||
|
||||
var qweb = core.qweb;
|
||||
var _t = core._t;
|
||||
@@ -118,13 +119,14 @@ var PortalChatter = publicWidget.Widget.extend({
|
||||
/**
|
||||
* Update the messages format
|
||||
*
|
||||
* @param {Array<Object>}
|
||||
* @param {Array<Object>} messages
|
||||
* @returns {Array}
|
||||
*/
|
||||
preprocessMessages: function (messages) {
|
||||
preprocessMessages(messages) {
|
||||
_.each(messages, function (m) {
|
||||
m['author_avatar_url'] = _.str.sprintf('/web/image/%s/%s/author_avatar/50x50', 'mail.message', m.id);
|
||||
m['published_date_str'] = _.str.sprintf(_t('Published on %s'), moment(time.str_to_datetime(m.date)).format('MMMM Do YYYY, h:mm:ss a'));
|
||||
m['body'] = Markup(m.body);
|
||||
});
|
||||
return messages;
|
||||
},
|
||||
|
||||
@@ -88,7 +88,7 @@
|
||||
<h5 class='mb-1'><t t-esc="message.author_id[1]"/></h5>
|
||||
<p class="o_portal_chatter_puslished_date"><t t-esc="message.published_date_str"/></p>
|
||||
</div>
|
||||
<t t-raw="message.body"/>
|
||||
<t t-out="message.body"/>
|
||||
|
||||
<div class="o_portal_chatter_attachments">
|
||||
<t t-call="portal.Chatter.Attachments">
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
</t>
|
||||
|
||||
<t t-extend="portal.chatter_messages">
|
||||
<t t-jquery="t[t-raw='message.body']" t-operation="before">
|
||||
<t t-jquery="t[t-out='message.body']" t-operation="before">
|
||||
<t t-if="message['rating_value']">
|
||||
<t t-call="portal_rating.rating_stars_static">
|
||||
<t t-set="val" t-value="message.rating_value"/>
|
||||
@@ -84,7 +84,7 @@
|
||||
</div>
|
||||
<p>Published on <t t-esc="rating.publisher_datetime"/></p>
|
||||
</div>
|
||||
<t t-esc="rating.publisher_comment"/>
|
||||
<t t-out="rating.publisher_comment"/>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
|
||||
<s:Body>
|
||||
<epos-print xmlns="http://www.epson-pos.com/schemas/2011/03/epos-print">
|
||||
<t t-raw="0"/>
|
||||
<t t-out="0"/>
|
||||
</epos-print>
|
||||
</s:Body>
|
||||
</s:Envelope>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
import itertools
|
||||
|
||||
from odoo import models
|
||||
from odoo import models, fields
|
||||
|
||||
|
||||
class ProductTemplate(models.Model):
|
||||
@@ -79,14 +79,7 @@ class ProductTemplateAttributeValue(models.Model):
|
||||
} # The " " is to avoid having 'Not available' if the template has only one attribute line.
|
||||
extra_price = sum(self.mapped('price_extra')) if display_extra else 0
|
||||
if extra_price:
|
||||
sign = '+ ' if extra_price > 0 else '- '
|
||||
header_cell.update({
|
||||
"price": sign + self.env['ir.qweb.field.monetary'].value_to_html(
|
||||
extra_price, {
|
||||
'from_currency': fro_currency,
|
||||
'display_currency': to_currency,
|
||||
'company_id': company.id,
|
||||
}
|
||||
)
|
||||
})
|
||||
header_cell['currency_id'] = to_currency.id
|
||||
header_cell['price'] = fro_currency._convert(
|
||||
extra_price, to_currency, company.id, fields.Date.today())
|
||||
return header_cell
|
||||
|
||||
@@ -4,7 +4,9 @@ var Dialog = require('web.Dialog');
|
||||
var core = require('web.core');
|
||||
var _t = core._t;
|
||||
var qweb = core.qweb;
|
||||
const {Markup} = require('web.utils');
|
||||
var fieldRegistry = require('web.field_registry');
|
||||
var {format} = require('web.field_utils');
|
||||
require('account.section_and_note_backend');
|
||||
|
||||
var SectionAndNoteFieldOne2Many = fieldRegistry.get('section_and_note_one2many');
|
||||
@@ -98,6 +100,12 @@ SectionAndNoteFieldOne2Many.include({
|
||||
'product_matrix.matrix', {
|
||||
header: infos.header,
|
||||
rows: infos.matrix,
|
||||
format({price, currency_id}) {
|
||||
if (!price) { return ""; }
|
||||
const sign = price < 0 ? '-' : '+';
|
||||
const formatted = format.monetary(Math.abs(price), null, {currency_id});
|
||||
return Markup`${sign} ${formatted}`;
|
||||
}
|
||||
}
|
||||
)),
|
||||
buttons: [
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<th t-attf-class="o_matrix_title_header {{column_header_first?'text-left':'text-center'}}">
|
||||
<span t-esc="column_header.name"/>
|
||||
<t t-call="product_matrix.extra_price">
|
||||
<t t-set="price" t-value="column_header.price"/>
|
||||
<t t-set="cell" t-value="column_header"/>
|
||||
</t>
|
||||
</th>
|
||||
</t>
|
||||
@@ -16,12 +16,10 @@
|
||||
<tbody>
|
||||
<tr t-foreach="rows" t-as="row">
|
||||
<t t-foreach="row" t-as="cell">
|
||||
<td t-if="cell.name" class="text-left">
|
||||
<th t-if="cell.name" class="text-left">
|
||||
<strong t-esc="cell.name"/>
|
||||
<t t-call="product_matrix.extra_price">
|
||||
<t t-set="price" t-value="cell.price"/>
|
||||
</t>
|
||||
</td>
|
||||
<t t-call="product_matrix.extra_price"/>
|
||||
</th>
|
||||
<td t-else="">
|
||||
<div t-if="cell.is_possible_combination" class="input-group">
|
||||
<input type="number"
|
||||
@@ -36,7 +34,7 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<span t-name="product_matrix.extra_price" t-if="price" class="badge badge-pill badge-secondary">
|
||||
<span t-name="product_matrix.extra_price" t-if="cell.price" class="badge badge-pill badge-secondary">
|
||||
<!--
|
||||
price_extra is displayed as catalog price instead of
|
||||
price after pricelist because it is impossible to
|
||||
@@ -47,7 +45,7 @@
|
||||
accurate to display it.
|
||||
-->
|
||||
<span class="variant_price_extra" style="white-space: nowrap;">
|
||||
<t t-raw="price"/>
|
||||
<t t-out="format(cell)"/>
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
@@ -44,3 +44,8 @@ class TestMatrixCommon(common.HttpCase):
|
||||
'value_ids': [(6, 0, attribute.value_ids.ids)]
|
||||
}) for attribute in product_attributes],
|
||||
})
|
||||
def get_ptav(pav_name):
|
||||
return self.env['product.template.attribute.value']\
|
||||
.search([('product_attribute_value_id.name', '=', pav_name)])
|
||||
get_ptav('PAV12').price_extra = 50
|
||||
get_ptav('PAV31').price_extra = -25
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { _lt } from 'web.core';
|
||||
import {Markup} from 'web.utils';
|
||||
import kanbanExamplesRegistry from 'web.kanban_examples_registry';
|
||||
|
||||
const greenBullet = '<span class="o_status o_status_green"></span>';
|
||||
const redBullet = '<span class="o_status o_status_red"></span>';
|
||||
const star = '<a style="color: gold;" class="fa fa-star"/>';
|
||||
const clock = '<a class="fa fa-clock-o" />';
|
||||
const greenBullet = Markup`<span class="o_status o_status_green"></span>`;
|
||||
const redBullet = Markup`<span class="o_status o_status_red"></span>`;
|
||||
const star = Markup`<a style="color: gold;" class="fa fa-star"/>`;
|
||||
const clock = Markup`<a class="fa fa-clock-o" />`;
|
||||
|
||||
const descriptionActivities = escFormat(_lt('%s Use the %s icon to organize your daily activities.'), '<br/>', clock);
|
||||
const description = escFormat(_lt('Prioritize Tasks by using the %s icon.' +
|
||||
@@ -15,21 +16,23 @@ const description = escFormat(_lt('Prioritize Tasks by using the %s icon.' +
|
||||
'%s'), star, '<br/>', greenBullet, '<br/>', redBullet, descriptionActivities);
|
||||
|
||||
/**
|
||||
* Helper function to escape a text before formatting it.
|
||||
* Helper function to escape the format string, but not the values formatted in,
|
||||
* rougly equivalent to `_.str.sprintf(_.escape(fmt), ...)`.
|
||||
*
|
||||
* First argument is the string to format and the other arguments are the values
|
||||
* to inject into the string.
|
||||
* Performs the escaping lazily (at point of use) as it's designed to be used
|
||||
* with `_lt` as the source of the format string.
|
||||
*
|
||||
* Sort of 'lazy escaping' as it is used alongside _lt.
|
||||
*
|
||||
* @returns {string} the formatted and escaped string
|
||||
* @param fmt format string, to escape
|
||||
* @param args objects to format into `fmt`, unescaped
|
||||
* @returns {Object} a stringifiable object returning the escaped then formatted string
|
||||
*/
|
||||
function escFormat() {
|
||||
const args = arguments;
|
||||
function escFormat(fmt, ...args) {
|
||||
return {
|
||||
toString: function () {
|
||||
args[0] = _.escape(args[0]);
|
||||
return _.str.sprintf.apply(_.str, args);
|
||||
[_.escapeMethod]() {
|
||||
return this;
|
||||
},
|
||||
toString() {
|
||||
return _.str.sprintf(_.escape(fmt), ...args);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,46 +1,45 @@
|
||||
odoo.define('project.tour', function(require) {
|
||||
"use strict";
|
||||
|
||||
var core = require('web.core');
|
||||
const {_t} = require('web.core');
|
||||
const {Markup} = require('web.utils');
|
||||
var tour = require('web_tour.tour');
|
||||
|
||||
var _t = core._t;
|
||||
|
||||
tour.register('project_tour', {
|
||||
sequence: 110,
|
||||
url: "/web",
|
||||
rainbowManMessage: "Congratulations, you are now a master of project management.",
|
||||
}, [tour.stepUtils.showAppsMenuItem(), {
|
||||
trigger: '.o_app[data-menu-xmlid="project.menu_main_pm"]',
|
||||
content: _t('Want a better way to <b>manage your projects</b>? <i>It starts here.</i>'),
|
||||
content: Markup(_t('Want a better way to <b>manage your projects</b>? <i>It starts here.</i>')),
|
||||
position: 'right',
|
||||
edition: 'community',
|
||||
}, {
|
||||
trigger: '.o_app[data-menu-xmlid="project.menu_main_pm"]',
|
||||
content: _t('Want a better way to <b>manage your projects</b>? <i>It starts here.</i>'),
|
||||
content: Markup(_t('Want a better way to <b>manage your projects</b>? <i>It starts here.</i>')),
|
||||
position: 'bottom',
|
||||
edition: 'enterprise',
|
||||
}, {
|
||||
trigger: '.o-kanban-button-new',
|
||||
extra_trigger: '.o_project_kanban',
|
||||
content: _t('Let\'s create your first <b>project</b>.'),
|
||||
content: Markup(_t('Let\'s create your first <b>project</b>.')),
|
||||
position: 'bottom',
|
||||
width: 200,
|
||||
}, {
|
||||
trigger: 'input.o_project_name',
|
||||
content: _t('Choose a <b>name</b> for your project. <i>It can be anything you want: the name of a customer,\
|
||||
of a product, of a team, of a construction site, etc.</i>'),
|
||||
content: Markup(_t('Choose a <b>name</b> for your project. <i>It can be anything you want: the name of a customer,\
|
||||
of a product, of a team, of a construction site, etc.</i>')),
|
||||
position: 'right',
|
||||
}, {
|
||||
trigger: '.o_open_tasks',
|
||||
content: _t('Let\'s create your first <b>project</b>.'),
|
||||
content: Markup(_t('Let\'s create your first <b>project</b>.')),
|
||||
position: 'top',
|
||||
run: function (actions) {
|
||||
actions.auto('.modal:visible .btn.btn-primary');
|
||||
},
|
||||
}, {
|
||||
trigger: ".o_kanban_project_tasks .o_column_quick_create .input-group",
|
||||
content: _t("Add columns to organize your tasks into <b>stages</b> <i>e.g. New - In Progress - Done</i>."),
|
||||
content: Markup(_t("Add columns to organize your tasks into <b>stages</b> <i>e.g. New - In Progress - Done</i>.")),
|
||||
position: 'right',
|
||||
run: function (actions) {
|
||||
actions.text("Test", this.$anchor.find("input"));
|
||||
@@ -51,7 +50,7 @@ tour.register('project_tour', {
|
||||
}, {
|
||||
trigger: ".o_kanban_project_tasks .o_column_quick_create .input-group",
|
||||
extra_trigger: '.o_kanban_group',
|
||||
content: _t("Add columns to organize your tasks into <b>stages</b> <i>e.g. New - In Progress - Done</i>."),
|
||||
content: Markup(_t("Add columns to organize your tasks into <b>stages</b> <i>e.g. New - In Progress - Done</i>.")),
|
||||
position: 'right',
|
||||
run: function (actions) {
|
||||
actions.text("Test", this.$anchor.find("input"));
|
||||
@@ -62,13 +61,13 @@ tour.register('project_tour', {
|
||||
}, {
|
||||
trigger: '.o-kanban-button-new',
|
||||
extra_trigger: '.o_kanban_group:eq(1)',
|
||||
content: _t("Let's create your first <b>task</b>."),
|
||||
content: Markup(_t("Let's create your first <b>task</b>.")),
|
||||
position: 'bottom',
|
||||
width: 200,
|
||||
}, {
|
||||
trigger: '.o_kanban_quick_create input.o_field_char[name=name]',
|
||||
extra_trigger: '.o_kanban_project_tasks',
|
||||
content: _t('Choose a task <b>name</b> <i>(e.g. Website Design, Purchase Goods...)</i>'),
|
||||
content: Markup(_t('Choose a task <b>name</b> <i>(e.g. Website Design, Purchase Goods...)</i>')),
|
||||
position: 'right',
|
||||
}, {
|
||||
trigger: '.o_kanban_quick_create .o_kanban_add',
|
||||
@@ -78,7 +77,7 @@ tour.register('project_tour', {
|
||||
}, {
|
||||
trigger: ".o_kanban_record .oe_kanban_content",
|
||||
extra_trigger: '.o_kanban_project_tasks',
|
||||
content: _t("<b>Drag & drop</b> the card to change your task from stage."),
|
||||
content: Markup(_t("<b>Drag & drop</b> the card to change your task from stage.")),
|
||||
position: "bottom",
|
||||
run: "drag_and_drop .o_kanban_group:eq(1) ",
|
||||
}, {
|
||||
@@ -88,20 +87,20 @@ tour.register('project_tour', {
|
||||
position: "bottom",
|
||||
}, {
|
||||
trigger: ".o_ChatterTopbar_buttonSendMessage",
|
||||
content: _t("Use the chatter to <b>send emails</b> and communicate efficiently with your customers. \
|
||||
Add new people to the followers' list to make them aware of the main changes about this task."),
|
||||
content: Markup(_t("Use the chatter to <b>send emails</b> and communicate efficiently with your customers. \
|
||||
Add new people to the followers' list to make them aware of the main changes about this task.")),
|
||||
width: 350,
|
||||
position: "bottom",
|
||||
}, {
|
||||
trigger: ".o_ChatterTopbar_buttonLogNote",
|
||||
content: _t("<b>Log notes</b> for internal communications <i>(the people following this task won't be notified \
|
||||
content: Markup(_t("<b>Log notes</b> for internal communications <i>(the people following this task won't be notified \
|
||||
of the note you are logging unless you specifically tag them)</i>. Use @ <b>mentions</b> to ping a colleague \
|
||||
or # <b>mentions</b> to reach an entire team."),
|
||||
or # <b>mentions</b> to reach an entire team.")),
|
||||
width: 350,
|
||||
position: "bottom"
|
||||
}, {
|
||||
trigger: ".o_ChatterTopbar_buttonScheduleActivity",
|
||||
content: _t("Use <b>activities</b> to organize your daily work."),
|
||||
content: Markup(_t("Use <b>activities</b> to organize your daily work.")),
|
||||
}, {
|
||||
trigger: ".modal-dialog .btn-primary",
|
||||
content: "Schedule your activity once it is ready.",
|
||||
@@ -110,7 +109,7 @@ tour.register('project_tour', {
|
||||
}, {
|
||||
trigger: ".breadcrumb-item:not(.active):last",
|
||||
extra_trigger: '.o_form_project_tasks.o_form_readonly',
|
||||
content: _t("Let's go back to the <b>kanban view</b> to have an overview of your next tasks."),
|
||||
content: Markup(_t("Let's go back to the <b>kanban view</b> to have an overview of your next tasks.")),
|
||||
position: "right",
|
||||
run: 'click',
|
||||
}]);
|
||||
|
||||
@@ -156,13 +156,13 @@ class StockRule(models.Model):
|
||||
buy_rule.ensure_one()
|
||||
supplier_delay = seller[0].delay
|
||||
if supplier_delay and not bypass_delay_description:
|
||||
delay_description += '<tr><td>%s</td><td class="text-right">+ %d %s</td></tr>' % (_('Vendor Lead Time'), supplier_delay, _('day(s)'))
|
||||
delay_description.append((_('Vendor Lead Time'), _('+ %d day(s)', supplier_delay)))
|
||||
security_delay = buy_rule.picking_type_id.company_id.po_lead
|
||||
if not bypass_delay_description:
|
||||
delay_description += '<tr><td>%s</td><td class="text-right">+ %d %s</td></tr>' % (_('Purchase Security Lead Time'), security_delay, _('day(s)'))
|
||||
delay_description.append((_('Purchase Security Lead Time'), _('+ %d day(s)', security_delay)))
|
||||
days_to_purchase = buy_rule.company_id.days_to_purchase
|
||||
if not bypass_delay_description:
|
||||
delay_description += '<tr><td>%s</td><td class="text-right">+ %d %s</td></tr>' % (_('Days to Purchase'), days_to_purchase, _('day(s)'))
|
||||
delay_description.append((_('Days to Purchase'), _('+ %d day(s)', days_to_purchase)))
|
||||
return delay + supplier_delay + security_delay + days_to_purchase, delay_description
|
||||
|
||||
@api.model
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
odoo.define('sale.tour', function(require) {
|
||||
"use strict";
|
||||
|
||||
var core = require('web.core');
|
||||
const {_t} = require('web.core');
|
||||
const {Markup} = require('web.utils');
|
||||
var tour = require('web_tour.tour');
|
||||
|
||||
var _t = core._t;
|
||||
|
||||
tour.register("sale_tour", {
|
||||
url: "/web",
|
||||
rainbowMan: false,
|
||||
@@ -108,11 +107,11 @@ tour.register("sale_quote_tour", {
|
||||
}, {
|
||||
trigger: ".o_field_widget[name='price_unit'] ",
|
||||
extra_trigger: ".o_sale_order",
|
||||
content: _t("<b>Set a price</b>."),
|
||||
content: Markup(_t("<b>Set a price</b>.")),
|
||||
position: "right",
|
||||
run: "text 10.0"
|
||||
},
|
||||
...tour.stepUtils.statusbarButtonsSteps("Send by Email", _t("<b>Send the quote</b> to yourself and check what the customer will receive."), ".o_statusbar_buttons button[name='action_quotation_send']"),
|
||||
...tour.stepUtils.statusbarButtonsSteps("Send by Email", Markup(_t("<b>Send the quote</b> to yourself and check what the customer will receive.")), ".o_statusbar_buttons button[name='action_quotation_send']"),
|
||||
{
|
||||
trigger: ".modal-footer button.btn-primary",
|
||||
auto: true,
|
||||
|
||||
@@ -2,6 +2,20 @@ odoo.define('sale_product_matrix.sale_matrix_tour', function (require) {
|
||||
"use strict";
|
||||
|
||||
var tour = require('web_tour.tour');
|
||||
let EXPECTED = [
|
||||
"Matrix", "PAV11", "PAV12 + $ 50.00",
|
||||
]
|
||||
for (let no of ['PAV41', 'PAV42']) {
|
||||
for (let dyn of ['PAV31', 'PAV32']) {
|
||||
for (let al of ['PAV21', 'PAV22']) {
|
||||
let row_label = [al, dyn, no].join(' • ');
|
||||
if (dyn === 'PAV31') {
|
||||
row_label += ' - $ 25.00';
|
||||
}
|
||||
EXPECTED.push(row_label, "", "");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tour.register('sale_matrix_tour', {
|
||||
url: "/web",
|
||||
@@ -45,6 +59,18 @@ tour.register('sale_matrix_tour', {
|
||||
}, {
|
||||
trigger: '.o_product_variant_matrix',
|
||||
run: function () {
|
||||
// whitespace normalization: removes newlines around text from markup
|
||||
// content, then collapse & convert internal whitespace to regular
|
||||
// spaces.
|
||||
const texts = $('.o_matrix_input_table').find('th, td')
|
||||
.map((_, el) => el.innerText.trim().replace(/\s+/g, ' '))
|
||||
.get();
|
||||
|
||||
for (let i=0; i<EXPECTED.length; ++i) {
|
||||
if (EXPECTED[i] !== texts[i]) {
|
||||
throw new Error(`${EXPECTED[i]} != ${texts[i]}`)
|
||||
}
|
||||
}
|
||||
// set all qties to 3
|
||||
$('.o_matrix_input').val(3);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
odoo.define('sale_timesheet.tour', function (require) {
|
||||
"use strict";
|
||||
|
||||
const {Markup} = require('web.utils');
|
||||
const tour = require('web_tour.tour');
|
||||
|
||||
tour.register('sale_timesheet_tour', {
|
||||
@@ -28,7 +29,7 @@ tour.register('sale_timesheet_tour', {
|
||||
run: 'click',
|
||||
}, {
|
||||
trigger: '.o_field_widget[name="product_id"], .o_field_widget[name="product_template_id"]',
|
||||
content: 'Select a prepaid service product <i>(e.g. Service Product (Prepaid Hours))</i>',
|
||||
content: Markup('Select a prepaid service product <i>(e.g. Service Product (Prepaid Hours))</i>'),
|
||||
run: function (actions) {
|
||||
actions.text('Service Product (Prepaid Hours)', this.$anchor.find('input'));
|
||||
},
|
||||
@@ -91,7 +92,7 @@ tour.register('sale_timesheet_tour', {
|
||||
run: 'click',
|
||||
}, {
|
||||
trigger: 'div[name="partner_id"]',
|
||||
content: 'Select the customer of your Sales Order <i>(e.g. Brandon Freeman)</i>. Since we have a Sales Order for this customer with a prepaid service product which the remaining hours to deliver is greater than 0, the Sales Order Item in the task should be contain the Sales Order Item containing this prepaid service product.',
|
||||
content: Markup('Select the customer of your Sales Order <i>(e.g. Brandon Freeman)</i>. Since we have a Sales Order for this customer with a prepaid service product which the remaining hours to deliver is greater than 0, the Sales Order Item in the task should be contain the Sales Order Item containing this prepaid service product.'),
|
||||
run: function (actions) {
|
||||
actions.text('Brandon Freeman', this.$anchor.find('input'));
|
||||
},
|
||||
@@ -186,7 +187,7 @@ tour.register('sale_timesheet_tour', {
|
||||
},
|
||||
}, {
|
||||
trigger: 'div[name="partner_id"]',
|
||||
content: 'Add the customer for this project to select an SO and SOL for this customer <i>(e.g. Brandon Freeman)</i>.',
|
||||
content: Markup('Add the customer for this project to select an SO and SOL for this customer <i>(e.g. Brandon Freeman)</i>.'),
|
||||
run: function (actions) {
|
||||
actions.text('Azure Interior, Brandon Freeman', this.$anchor.find('input'));
|
||||
},
|
||||
|
||||
@@ -327,13 +327,17 @@ class StockRule(models.Model):
|
||||
:param product: the product of the procurement
|
||||
:type product: :class:`~odoo.addons.product.models.product.ProductProduct`
|
||||
:return: the cumulative delay and cumulative delay's description
|
||||
:rtype: tuple
|
||||
:rtype: tuple[int, list[str, str]]
|
||||
"""
|
||||
delay = sum(self.filtered(lambda r: r.action in ['pull', 'pull_push']).mapped('delay'))
|
||||
if self.env.context.get('bypass_delay_description'):
|
||||
delay_description = ""
|
||||
delay_description = []
|
||||
else:
|
||||
delay_description = ''.join(['<tr><td>%s %s</td><td class="text-right">+ %d %s</td></tr>' % (_('Delay on'), html_escape(rule.name), rule.delay, _('day(s)')) for rule in self if rule.action in ['pull', 'pull_push'] and rule.delay])
|
||||
delay_description = [
|
||||
(_('Delay on %s', rule.name), _('+ %d day(s)', rule.delay))
|
||||
for rule in self
|
||||
if rule.action in ['pull', 'pull_push'] and rule.delay
|
||||
]
|
||||
return delay, delay_description
|
||||
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
The forecasted stock on the <b><t t-esc="lead_days_date"/></b>
|
||||
is <t t-if="qty_to_order <= 0"><b><t t-esc="qty_forecast"/> <t t-esc="product_uom_name"/></b></t><t t-else="">
|
||||
below the inventory <b>minimum of <t t-esc="product_min_qty"/> <t t-esc="product_uom_name"/>
|
||||
: <t t-esc="qty_to_order"/> <t t-esc="product_uom_name"/> should be replenished</b> to reach the maximum of
|
||||
: <t t-esc="qty_to_order"/> <t t-esc="product_uom_name"/> should be replenished</b> to reach the maximum of
|
||||
<t t-esc="product_max_qty"/> <t t-esc="product_uom_name"/>.</t>
|
||||
</p>
|
||||
<table t-if="lead_days_description" class="table table-borderless table-sm">
|
||||
@@ -17,16 +17,19 @@
|
||||
Today
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<t t-esc="today"/>
|
||||
<t t-out="today"/>
|
||||
</td>
|
||||
</tr>
|
||||
<t t-raw="lead_days_description"/>
|
||||
<tr t-foreach="lead_days_description" t-as="descr">
|
||||
<td><t t-out="descr[0]"/></td>
|
||||
<td class="text-right"><t t-out="descr[1]"/></td>
|
||||
</tr>
|
||||
<tr class="table-info">
|
||||
<td>
|
||||
Forecasted Date
|
||||
</td>
|
||||
<td class="text-right text-nowrap">
|
||||
= <t t-esc="lead_days_date"/>
|
||||
= <t t-out="lead_days_date"/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
@@ -28,7 +28,7 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
var QWeb2 = {
|
||||
expressions_cache: { },
|
||||
RESERVED_WORDS: 'true,false,NaN,null,undefined,debugger,console,window,in,instanceof,new,function,return,this,typeof,eval,void,Math,RegExp,Array,Object,Date'.split(','),
|
||||
ACTIONS_PRECEDENCE: 'foreach,if,elif,else,call,set,tag,esc,raw,js,debug,log'.split(','),
|
||||
ACTIONS_PRECEDENCE: 'foreach,if,elif,else,call,set,tag,out,esc,raw,js,debug,log'.split(','),
|
||||
WORD_REPLACEMENT: {
|
||||
'and': '&&',
|
||||
'or': '||',
|
||||
@@ -65,15 +65,14 @@ var QWeb2 = {
|
||||
js_escape: function(s, noquotes) {
|
||||
return (noquotes ? '' : "'") + s.replace(/\r?\n/g, "\\n").replace(/'/g, "\\'") + (noquotes ? '' : "'");
|
||||
},
|
||||
html_escape: function(s, attribute) {
|
||||
html_escape: function(s) {
|
||||
if (s == null) {
|
||||
return '';
|
||||
}
|
||||
s = String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
if (attribute) {
|
||||
s = s.replace(/"/g, '"');
|
||||
}
|
||||
return s;
|
||||
return _.escape(s);
|
||||
},
|
||||
markup(s) {
|
||||
return new _Markup(s);
|
||||
},
|
||||
gen_attribute: function(o) {
|
||||
if (o !== null && o !== undefined) {
|
||||
@@ -94,7 +93,10 @@ var QWeb2 = {
|
||||
return '';
|
||||
},
|
||||
format_attribute: function(name, value) {
|
||||
return ' ' + name + '="' + this.html_escape(value, true) + '"';
|
||||
// we want to ensure `value` is *not* a `Markup`, because markup-safe
|
||||
// strings are not necessarily attributes-safe.
|
||||
const attrvalue = value == null ? '' : this.html_escape(String(value));
|
||||
return ` ${name}="${attrvalue}"`;
|
||||
},
|
||||
extend: function(dst, src, exclude) {
|
||||
for (var p in src) {
|
||||
@@ -149,9 +151,9 @@ var QWeb2 = {
|
||||
var new_dict = this.extend({}, old_dict);
|
||||
new_dict['__caller__'] = old_dict['__template__'];
|
||||
if (callback) {
|
||||
new_dict[0] = callback(context, new_dict);
|
||||
new_dict[0] = this.markup(callback(context, new_dict));
|
||||
}
|
||||
return context.engine._render(template, new_dict);
|
||||
return this.markup(context.engine._render(template, new_dict));
|
||||
},
|
||||
foreach: function(context, enu, as, old_dict, callback) {
|
||||
if (enu != null) {
|
||||
@@ -689,7 +691,7 @@ QWeb2.Element = (function() {
|
||||
s && r.push(_this.engine.tools.js_escape(s));
|
||||
}
|
||||
|
||||
var re = /(?:#{(.+?)}|{{(.+?)}})/g, start = 0, r = [], m;
|
||||
var re = /#{(.+?)}|{{(.+?)}}/g, start = 0, r = [], m;
|
||||
while (m = re.exec(s)) {
|
||||
// extract literal string between previous and current match
|
||||
append_literal(s.slice(start, re.lastIndex - m[0].length));
|
||||
@@ -800,7 +802,7 @@ QWeb2.Element = (function() {
|
||||
this.bottom("}));");
|
||||
this.indent();
|
||||
this.top("var r = [];");
|
||||
return this.bottom("return r.join('');");
|
||||
return this.bottom("return context.engine.tools.markup(r.join(''));");
|
||||
}
|
||||
},
|
||||
compile_action_set : function(value) {
|
||||
@@ -822,11 +824,14 @@ QWeb2.Element = (function() {
|
||||
this.bottom("})(dict);");
|
||||
this.indent();
|
||||
this.top("var r = [];");
|
||||
this.bottom("return r.join('');");
|
||||
this.bottom("return context.engine.tools.markup(r.join(''));");
|
||||
}
|
||||
}
|
||||
},
|
||||
compile_action_esc : function(value) {
|
||||
return this.compile_action_out(value);
|
||||
},
|
||||
compile_action_out(value) {
|
||||
this.top("var t = " + this.format_str(value) + ";");
|
||||
this.top("if (t != null) r.push(context.engine.tools.html_escape(t));");
|
||||
this.top("else {");
|
||||
@@ -834,6 +839,14 @@ QWeb2.Element = (function() {
|
||||
this.indent();
|
||||
},
|
||||
compile_action_raw : function(value) {
|
||||
let e = this.node;
|
||||
while (e.parentElement && !e.getAttribute('t-name')) {
|
||||
e = e.parentElement;
|
||||
}
|
||||
console.warn(
|
||||
"Found deprecated directive '@t-raw=\"%s\"' in "
|
||||
+ "template '%s'. Replace by @t-out, and explicitely wrap content in "
|
||||
+ "utils.Markup if necessary.", value, e.getAttribute('t-name'));
|
||||
this.top("var t = " + this.format_str(value) + ";");
|
||||
this.top("if (t != null) r.push(t);");
|
||||
this.top("else {");
|
||||
|
||||
@@ -4,6 +4,7 @@ odoo.define('web.ajax', function (require) {
|
||||
var config = require('web.config');
|
||||
var concurrency = require('web.concurrency');
|
||||
var core = require('web.core');
|
||||
const {Markup} = require('web.utils');
|
||||
var time = require('web.time');
|
||||
var download = require('web.download');
|
||||
var contentdisposition = require('web.contentdisposition');
|
||||
@@ -460,13 +461,13 @@ var loadAsset = (function () {
|
||||
return $(this).attr('href');
|
||||
}).get(),
|
||||
cssContents: $xml.filter('style').map(function () {
|
||||
return $(this).html();
|
||||
return Markup($(this).html());
|
||||
}).get(),
|
||||
jsLibs: $xml.filter('script[src]').map(function () {
|
||||
return $(this).attr('src');
|
||||
}).get(),
|
||||
jsContents: $xml.filter('script:not([src])').map(function () {
|
||||
return $(this).html();
|
||||
return Markup($(this).html());
|
||||
}).get(),
|
||||
};
|
||||
}).guardedCatch(reason => {
|
||||
|
||||
@@ -240,9 +240,81 @@ class UnknownPatchError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
const __escape = _.escape;
|
||||
_.escapeMethod = Symbol('html')
|
||||
_.escape = function escape(s) {
|
||||
return s[_.escapeMethod] ? s[_.escapeMethod]() : __escape(s);
|
||||
}
|
||||
|
||||
// notable issues:
|
||||
// * objects can't be negative in JS, so !!"" -> false but
|
||||
// !!(new String) -> true, likewise markup
|
||||
// TODO (?)
|
||||
// * Markup.join / Markup#join => escapes items and returns a Markup
|
||||
// * Markup#replace => automatically escapes the replacements (difficult impl)
|
||||
class _Markup extends String {
|
||||
[_.escapeMethod]() {
|
||||
return this;
|
||||
}
|
||||
}
|
||||
// exposed for qweb2.js
|
||||
window._Markup = _Markup;
|
||||
|
||||
/**
|
||||
* Returns a markup object, which acts like a String but is considered safe by
|
||||
* `_.escape`, and will therefore be injected as-is (without additional
|
||||
* escaping) in templates. Can be used to inject dynamic HTML in templates
|
||||
* (where the template itself can't), see first example.
|
||||
*
|
||||
* Can also be used as a *template tag*, in which case the literal content
|
||||
* won't be escaped but the substitutions which are not already markup objects
|
||||
* will be.
|
||||
*
|
||||
* ## WARNINGS:
|
||||
* * A markup object is a `String` (boxed) but not a `string` (primitive), they
|
||||
* typecheck differently which can be relevant.
|
||||
* * To strip out the "markupness", just call `String(markup)`.
|
||||
* * Most string operations (e.g. concatenation, `String#replace`, ...) will
|
||||
* also strip out markupness
|
||||
* * If the input is empty, returns a regular string (that way boolean tests
|
||||
* work as expected).
|
||||
*
|
||||
* @returns a markup object
|
||||
*
|
||||
* @example regular function
|
||||
* let h;
|
||||
* if (someTest) {
|
||||
* h = Markup(_t("This is a <strong>success</strong>"));
|
||||
* } else {
|
||||
* h = Markup(_t("Things did <strong>not</strong> work out"));
|
||||
* }
|
||||
* qweb.render("some_template", { message: h });
|
||||
*
|
||||
* @example template tag
|
||||
* const escaped = "<some> text";
|
||||
* const asis = Markup`some <b>text</b>`;
|
||||
* const h = Markup`Regular strings get ${escaped} but markup is injected ${asis}`;
|
||||
*/
|
||||
function Markup(v, ...exprs) {
|
||||
if (!(v instanceof Array)) {
|
||||
return v ? new _Markup(v) : '';
|
||||
}
|
||||
const elements = [];
|
||||
let i = 0;
|
||||
for(; i < exprs.length; ++i) {
|
||||
elements.push(v[i], _.escape(exprs[i]));
|
||||
}
|
||||
elements.push(v[i]);
|
||||
|
||||
const s = elements.join('');
|
||||
if (!s) { return '' }
|
||||
return new _Markup(s);
|
||||
}
|
||||
|
||||
var utils = {
|
||||
AlreadyDefinedPatchError,
|
||||
UnknownPatchError,
|
||||
Markup,
|
||||
|
||||
/**
|
||||
* Throws an error if the given condition is not true
|
||||
@@ -818,25 +890,40 @@ var utils = {
|
||||
},
|
||||
/**
|
||||
* Returns a string formatted using given values.
|
||||
*
|
||||
* If the value is an object, its keys will replace `%(key)s` expressions.
|
||||
* If the values are a set of strings, they will replace `%s` expressions.
|
||||
* If no value is given, the string will not be formatted.
|
||||
*
|
||||
* @param {string} string
|
||||
* @param {(Object|...string)} values
|
||||
* `Markup`-aware: if a `Markup` object is provided as format string,
|
||||
* automatically escapes injected values and returns a new `Markup`
|
||||
* object.
|
||||
*
|
||||
* @param {string|Markup} string format string
|
||||
* @param values values injected into the format string, can be either a
|
||||
* sequence of positional parameters (for positional
|
||||
* placeholders) or a single Object acting a as map (for named
|
||||
* placeholders).
|
||||
* @returns {string|Markup}
|
||||
*/
|
||||
sprintf: function (string, ...values) {
|
||||
sprintf(string, ...values) {
|
||||
let finalizer, mapper;
|
||||
finalizer = mapper = a => a;
|
||||
if (string instanceof _Markup) {
|
||||
string = String(string);
|
||||
finalizer = Markup;
|
||||
mapper = _.escape;
|
||||
}
|
||||
if (values.length === 1 && typeof values[0] === 'object') {
|
||||
const valuesDict = values[0];
|
||||
for (const value in valuesDict) {
|
||||
string = string.replace(`%(${value})s`, valuesDict[value]);
|
||||
}
|
||||
string = string.replace(
|
||||
/%\((\w+)\)s/g,
|
||||
(_, group) => mapper(valuesDict[group]));
|
||||
} else {
|
||||
for (const value of values) {
|
||||
string = string.replace(/%s/, value);
|
||||
}
|
||||
let i = 0;
|
||||
string = string.replace(/%s/g, () => mapper(values[i++]));
|
||||
}
|
||||
return string;
|
||||
return finalizer(string);
|
||||
},
|
||||
/**
|
||||
* Sort an array in place, keeping the initial order for identical values.
|
||||
|
||||
@@ -332,7 +332,7 @@ function formatX2Many(value) {
|
||||
* @param {boolean} [options.forceString=false]
|
||||
* if false, returns a string encoding the html formatted value (with
|
||||
* whitespace encoded as ' ')
|
||||
* @returns {string}
|
||||
* @returns {string | _Markup}
|
||||
*/
|
||||
function formatMonetary(value, field, options) {
|
||||
if (value === false) {
|
||||
@@ -361,12 +361,13 @@ function formatMonetary(value, field, options) {
|
||||
if (!currency || options.noSymbol) {
|
||||
return formatted_value;
|
||||
}
|
||||
const ws = options.forceString ? ' ' : ' ';
|
||||
if (currency.position === "after") {
|
||||
return formatted_value + ws + currency.symbol;
|
||||
} else {
|
||||
return currency.symbol + ws + formatted_value;
|
||||
const val = currency.position === 'after'
|
||||
? [formatted_value, currency.symbol]
|
||||
: [currency.symbol, formatted_value];
|
||||
if (options.forceString) {
|
||||
return val.join(' ');
|
||||
}
|
||||
return utils.Markup(val.map((v) => _.escape(v)).join(' '));
|
||||
}
|
||||
/**
|
||||
* Returns a string representing the given value (multiplied by 100)
|
||||
|
||||
@@ -410,7 +410,7 @@ var FieldMany2One = AbstractField.extend({
|
||||
domain: this.record.getDomain({fieldName: this.name}),
|
||||
context: _.extend({}, this.record.getContext(this.recordParams), context || {}),
|
||||
dynamicFilters: dynamicFilters || [],
|
||||
title: (view === 'search' ? _t("Search: ") : _t("Create: ")) + this.string,
|
||||
title: _.str.sprintf((view === 'search' ? _t("Search: %s") : _t("Create: %s")), this.string),
|
||||
initial_ids: ids,
|
||||
initial_view: view,
|
||||
disable_multiple_selection: true,
|
||||
|
||||
@@ -411,7 +411,7 @@ var CalendarController = AbstractController.extend({
|
||||
res_model: self.modelName,
|
||||
res_id: id || null,
|
||||
context: event.context || self.context,
|
||||
title: _t("Open: ") + _.escape(event.data.title),
|
||||
title: _.str.sprintf(_t("Open: %s"), event.data.title),
|
||||
on_saved: function () {
|
||||
if (event.data.on_save) {
|
||||
event.data.on_save();
|
||||
|
||||
@@ -14,10 +14,7 @@ odoo.define('web.kanban_examples_registry', function (require) {
|
||||
* following keys:
|
||||
* - name (string)
|
||||
* - columns (Array[string])
|
||||
* - description (string, optional) BE CAREFUL [*]
|
||||
*
|
||||
* [*] The description is added with a t-raw so the translated texts must be
|
||||
* properly escaped.
|
||||
* - description (string, optional)
|
||||
*/
|
||||
|
||||
var Registry = require('web.Registry');
|
||||
|
||||
@@ -610,31 +610,29 @@ var KanbanRecord = Widget.extend(WidgetAdapterMixin, {
|
||||
* @returns {Object} transformed record data
|
||||
*/
|
||||
_transformRecord: function (recordData) {
|
||||
var self = this;
|
||||
var new_record = {};
|
||||
_.each(this.state.getFieldNames(), function (name) {
|
||||
var value = recordData[name];
|
||||
var r = _.clone(self.fields[name] || {});
|
||||
const new_record = Object.create(null);
|
||||
for (const name of this.state.getFieldNames()) {
|
||||
const value = recordData[name];
|
||||
const r = new_record[name] = _.clone(this.fields[name] || {});
|
||||
const formatter = r.type ? field_utils.format[r.type] : _.identity;
|
||||
|
||||
if ((r.type === 'date' || r.type === 'datetime') && value) {
|
||||
r.raw_value = value.toDate();
|
||||
} else if (r.type === 'one2many' || r.type === 'many2many') {
|
||||
r.raw_value = value;
|
||||
r.value = formatter(value, this.fields[name], recordData, this.state);
|
||||
switch (r.type) {
|
||||
case 'date': case 'datetime':
|
||||
r.raw_value = value && value.toDate();
|
||||
break;
|
||||
case 'one2many': case 'many2many':
|
||||
r.raw_value = value.count ? value.res_ids : [];
|
||||
} else if (r.type === 'many2one') {
|
||||
break;
|
||||
case 'many2one':
|
||||
r.raw_value = value && value.res_id || false;
|
||||
} else {
|
||||
r.raw_value = value;
|
||||
break;
|
||||
case 'html': // or should this be in `format` instead of _.identity?
|
||||
r.value = utils.Markup(value);
|
||||
break;
|
||||
}
|
||||
|
||||
if (r.type) {
|
||||
var formatter = field_utils.format[r.type];
|
||||
r.value = formatter(value, self.fields[name], recordData, self.state);
|
||||
} else {
|
||||
r.value = value;
|
||||
}
|
||||
|
||||
new_record[name] = r;
|
||||
});
|
||||
}
|
||||
return new_record;
|
||||
},
|
||||
/**
|
||||
|
||||
@@ -42,7 +42,7 @@ odoo.define('web.TranslationDialog', function (require) {
|
||||
|
||||
this._super(parent, _.extend({
|
||||
size: 'large',
|
||||
title: _t('Translate: ') + `${this.fieldName}`,
|
||||
title: _.str.sprintf(_t('Translate: %s'), this.fieldName),
|
||||
buttons: [
|
||||
{ text: _t('Save'), classes: 'btn-primary', close: true, click: this._onSave.bind(this) },
|
||||
{ text: _t('Discard'), close: true },
|
||||
|
||||
@@ -1041,7 +1041,7 @@
|
||||
<t t-foreach="states" t-as="rec">
|
||||
<a role="menuitem" href="#" class="dropdown-item pl-4" t-att-data-value="rec.name">
|
||||
<span t-att-class="'o_status rounded-circle d-inline-block align-middle ' + (rec.state_class && rec.state_class || '')"/>
|
||||
<t t-raw="rec.state_name"/>
|
||||
<t t-out="rec.state_name"/>
|
||||
</a>
|
||||
</t>
|
||||
</t>
|
||||
@@ -1321,11 +1321,11 @@
|
||||
</t>
|
||||
<t t-if="hasDropdown">
|
||||
<a role="button" href="#" class="dropdown-toggle o-no-caret" data-toggle="dropdown" aria-expanded="false">
|
||||
<span t-raw="_badge_text"/>
|
||||
<span t-out="_badge_text"/>
|
||||
</a>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<span t-raw="_badge_text"/>
|
||||
<span t-out="_badge_text"/>
|
||||
</t>
|
||||
<a t-if="!readonly" href="#" class="fa fa-times o_delete" title="Delete" aria-label="Delete"/>
|
||||
</div>
|
||||
@@ -1475,7 +1475,7 @@
|
||||
<input type="hidden" name="callback" t-att-value="fileupload_id"/>
|
||||
<input type="file" class="o_input_file" name="ufile"
|
||||
t-att="{'multiple': multi_upload ? 'multiple' : null, 'accept': widget.accepted_file_extensions || '*'}"/>
|
||||
<t t-raw="0"/>
|
||||
<t t-out="0"/>
|
||||
</form>
|
||||
<iframe t-att-id="fileupload_id" t-att-name="fileupload_id" style="display: none"/>
|
||||
</div>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<header t-if="renderHeader" class="modal-header">
|
||||
<h4 class="modal-title"><t t-raw="title"/><span class="o_subtitle text-muted small" t-esc="subtitle"/></h4>
|
||||
<h4 class="modal-title"><t t-out="title"/><span class="o_subtitle text-muted small" t-out="subtitle"/></h4>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close" tabindex="-1">×</button>
|
||||
</header>
|
||||
<main class="modal-body"/>
|
||||
|
||||
@@ -156,7 +156,7 @@
|
||||
<t t-foreach="examples" t-as="example">
|
||||
<div role="tabpanel" t-attf-id="example_#{example_index}" t-attf-class="tab-pane #{example_index == 0 and 'active in' or ''}">
|
||||
<div t-if="example.description" class="o_kanban_examples_description">
|
||||
<t t-raw="example.description"/> <!-- description is supposed to be properly escaped -->
|
||||
<t t-out="example.description"/>
|
||||
</div>
|
||||
<div class="o_kanban_examples">
|
||||
<t t-foreach="example.columns" t-as="column">
|
||||
@@ -177,10 +177,8 @@
|
||||
<div class="o_kanban_examples_ghost">
|
||||
<div class="o_ghost_content"/>
|
||||
<div class="o_ghost_content o_ghost_tag"/>
|
||||
<t t-if="example.bullets">
|
||||
<t t-if="_.random(0,5) > 3">
|
||||
<t t-raw="example.bullets[_.random(0, example.bullets.length-1)]"/>
|
||||
</t>
|
||||
<t t-if="example.bullets and _.random(0, 5) > 3">
|
||||
<t t-out="_.sample(example.bullets)"/>
|
||||
</t>
|
||||
<img class="float-right o_ghost_avatar" src="/base/static/img/avatar.png" alt="Avatar"/>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
odoo.define('web.qweb_tests', function (require) {
|
||||
"use strict";
|
||||
|
||||
const {Markup} = require('web.utils');
|
||||
|
||||
var qwebPath = '/web/static/lib/qweb/';
|
||||
const {hushConsole} = require('@web/../tests/helpers/utils');
|
||||
|
||||
@@ -9,99 +11,182 @@ function trim(s) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the template file, and executes all the test template in a
|
||||
* qunit module $title
|
||||
* Promise-based wrapper for QWeb2.Engine#add_template
|
||||
*
|
||||
* The base version is callbacks-based which is a bit shit, and it also has
|
||||
* variable asynchronicity: it'll be async if passed a URL but not if passed
|
||||
* either a Document or an XML string.
|
||||
*
|
||||
* Either way this converts the `(error, doc)` callback to a `Promise<Document>`
|
||||
* as in it literally returns the parsed DOM Document.
|
||||
*
|
||||
* @param qweb the qweb instance to load the template into
|
||||
* @param {String|Document} template qweb template (Document, template file, or template URL)
|
||||
*/
|
||||
function loadTest(assert, template, context) {
|
||||
var done = assert.async();
|
||||
assert.expect(1);
|
||||
|
||||
var qweb = new window.QWeb2.Engine();
|
||||
var prom = new Promise(function (resolve, reject) {
|
||||
qweb.add_template(qwebPath + template, function (error, doc) {
|
||||
if (error) {
|
||||
return reject(error);
|
||||
}
|
||||
resolve({
|
||||
qweb: qweb,
|
||||
doc: doc
|
||||
});
|
||||
});
|
||||
function add_template(qweb, template) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
qweb.add_template(template, (error, doc) => error ? reject(error) : resolve(doc));
|
||||
});
|
||||
|
||||
prom.then(function (r) {
|
||||
var qweb = r.qweb;
|
||||
var doc = r.doc;
|
||||
assert.expect(doc.querySelectorAll('result').length);
|
||||
|
||||
var templates = qweb.templates;
|
||||
for (var template in templates) {
|
||||
try {
|
||||
if (!templates.hasOwnProperty(template)) {
|
||||
continue;
|
||||
}
|
||||
// ignore templates whose name starts with _, they're
|
||||
// helpers/internal
|
||||
if (/^_/.test(template)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var params = doc.querySelector('params#' + template);
|
||||
var args = params ? JSON.parse(params.textContent) : (context ? _.clone(context) : {});
|
||||
|
||||
var results = doc.querySelector('result#' + template);
|
||||
|
||||
assert.equal(
|
||||
trim(qweb.render(template, args)),
|
||||
trim(results.textContent.replace(new RegExp(String.fromCharCode(13), 'g'), '')),
|
||||
template);
|
||||
} catch (error) {
|
||||
assert.notOk(error.stack || error, 'Rendering error');
|
||||
}
|
||||
}
|
||||
done();
|
||||
});
|
||||
return prom;
|
||||
}
|
||||
|
||||
QUnit.module('QWeb', {
|
||||
before() {
|
||||
this.oldConsole = window.console;
|
||||
window.console = hushConsole;
|
||||
/**
|
||||
* Loads the template file, and executes all the test template in a
|
||||
* qunit module $title
|
||||
*
|
||||
* @param assert QUnit assertion module
|
||||
* @param {String|Document} templateFile template container to load
|
||||
* @param {Object} [context] additional rendering context
|
||||
*/
|
||||
async function loadTest(assert, templateFile, context) {
|
||||
const qweb = new window.QWeb2.Engine();
|
||||
const doc = await add_template(qweb, qwebPath + templateFile);
|
||||
|
||||
assert.expect(doc.querySelectorAll('result').length);
|
||||
|
||||
const templates = qweb.templates;
|
||||
for (const template in templates) {
|
||||
if (!templates.hasOwnProperty(template)) {
|
||||
continue;
|
||||
}
|
||||
// ignore templates whose name starts with _, they're
|
||||
// helpers/internal
|
||||
if (/^_/.test(template)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const results = doc.querySelector(`result#${template}`).textContent.replace(/\r/g, '');
|
||||
const params = doc.querySelector(`params#${template}`) || {textContent: 'null'};
|
||||
const args = {...JSON.parse(params.textContent), ...context};
|
||||
|
||||
try {
|
||||
assert.equal(trim(qweb.render(template, args)), trim(results), template);
|
||||
} catch (error) {
|
||||
assert.notOk(error.stack || error, `Rendering error for ${template} (in ${templateFile} with context ${JSON.stringify(args)}).`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const table = [
|
||||
{name: 'Output', file: 'qweb-test-output.xml'},
|
||||
{name: 'Context-setting', file: 'qweb-test-set.xml'},
|
||||
{name: 'Conditionals', file: 'qweb-test-conditionals.xml'},
|
||||
{name: 'Attributes manipulation', file: 'qweb-test-attributes.xml'},
|
||||
{name: 'Templates calling (to the faraway pages)', file: 'qweb-test-call.xml', context: {True: true}},
|
||||
{name: 'Foreach', file: 'qweb-test-foreach.xml'},
|
||||
{
|
||||
name: 'Global', file: 'qweb-test-global.xml',
|
||||
// test uses python syntax
|
||||
context: {bool: (v) => !!v ? 'True' : 'False'},
|
||||
fixture: {
|
||||
before() {
|
||||
this.WORD_REPLACEMENT = window.QWeb2.WORD_REPLACEMENT;
|
||||
window.QWeb2.WORD_REPLACEMENT = _.extend(
|
||||
{not: '!', None: 'undefined'},
|
||||
this.WORD_REPLACEMENT
|
||||
)
|
||||
},
|
||||
after() {
|
||||
window.QWeb2.WORD_REPLACEMENT = this.WORD_REPLACEMENT;
|
||||
}
|
||||
}
|
||||
},
|
||||
after() {window.console = this.oldConsole;}
|
||||
}, function () {
|
||||
QUnit.test('Output', function (assert) {
|
||||
loadTest(assert, 'qweb-test-output.xml');
|
||||
});
|
||||
QUnit.test('Context-setting', function (assert) {
|
||||
loadTest(assert, 'qweb-test-set.xml');
|
||||
});
|
||||
QUnit.test('Conditionals', function (assert) {
|
||||
loadTest(assert, 'qweb-test-conditionals.xml');
|
||||
});
|
||||
QUnit.test('Attributes manipulation', function (assert) {
|
||||
loadTest(assert, 'qweb-test-attributes.xml');
|
||||
});
|
||||
QUnit.test('Template calling (to the faraway pages)', function (assert) {
|
||||
loadTest(assert, 'qweb-test-call.xml', {True: true});
|
||||
});
|
||||
QUnit.test('Foreach', function (assert) {
|
||||
loadTest(assert, 'qweb-test-foreach.xml');
|
||||
});
|
||||
QUnit.test('Global', function (assert) {
|
||||
// test use python syntax
|
||||
var WORD_REPLACEMENT = window.QWeb2.WORD_REPLACEMENT;
|
||||
window.QWeb2.WORD_REPLACEMENT = _.extend({not: '!', None: 'undefined'}, WORD_REPLACEMENT);
|
||||
loadTest(assert, 'qweb-test-global.xml', {bool: function (v) { return !!v ? 'True' : 'False';}})
|
||||
.then(function () {
|
||||
window.QWeb2.WORD_REPLACEMENT = WORD_REPLACEMENT;
|
||||
}, function () {
|
||||
window.QWeb2.WORD_REPLACEMENT = WORD_REPLACEMENT;
|
||||
});
|
||||
});
|
||||
QUnit.test('Template inheritance', function (assert) {
|
||||
loadTest(assert, 'qweb-test-extend.xml');
|
||||
{name: 'Template Inheritance', file: 'qweb-test-extend.xml'},
|
||||
];
|
||||
QUnit.module('QWeb', {
|
||||
beforeEach() {
|
||||
this.oldConsole = window.console;
|
||||
window.console = hushConsole;
|
||||
},
|
||||
afterEach() {window.console = this.oldConsole;}
|
||||
}, () => {
|
||||
for(const {name, file, context, fixture} of table) {
|
||||
QUnit.test(name, async assert => {
|
||||
// fake expect to avoid qunit being a pain in the ass, loadTest will
|
||||
// update it
|
||||
assert.expect(1);
|
||||
if (fixture && 'before' in fixture) { await fixture.before(); }
|
||||
try {
|
||||
await loadTest(assert, file, context);
|
||||
} finally {
|
||||
if (fixture && 'after' in fixture) { await fixture.after(); }
|
||||
}
|
||||
});
|
||||
}
|
||||
QUnit.test('escape', assert => {
|
||||
// not strictly about qweb...
|
||||
assert.expect(8);
|
||||
assert.equal(_.escape('a'), 'a');
|
||||
assert.equal(_.escape('<a>'), '<a>');
|
||||
assert.equal(_.escape({[_.escapeMethod]() { return 'a'; }}), 'a');
|
||||
assert.equal(_.escape({[_.escapeMethod]() { return '<a>'; }}), '<a>');
|
||||
assert.equal(_.escape(Markup('a')), 'a');
|
||||
assert.equal(_.escape(Markup('<a>')), '<a>');
|
||||
assert.equal(_.escape(Markup`a`), 'a');
|
||||
assert.equal(_.escape(Markup`<a>`), '<a>');
|
||||
});
|
||||
QUnit.module('t-out', {}, () => {
|
||||
QUnit.test("basics", async assert => {
|
||||
assert.expect(5);
|
||||
const qweb = new QWeb2.Engine;
|
||||
await add_template(qweb, `<templates>
|
||||
<t t-name="t-out"><p><t t-out="value"/></p></t>
|
||||
</templates>`);
|
||||
|
||||
assert.equal(
|
||||
qweb.render('t-out', {value: '<i>test</i>'}),
|
||||
'<p><i>test</i></p>',
|
||||
"regular t-out should just escape the contents"
|
||||
);
|
||||
assert.equal(
|
||||
qweb.render('t-out', {value: Markup('<i>test</i>')}),
|
||||
'<p><i>test</i></p>',
|
||||
"Markup contents should not be escaped"
|
||||
);
|
||||
assert.equal(
|
||||
qweb.render('t-out', {value: Markup`<i>test ${1}</i>`}),
|
||||
'<p><i>test 1</i></p>',
|
||||
"markup template string should not be escaped"
|
||||
);
|
||||
const teststr = '<i>test</i>';
|
||||
assert.equal(
|
||||
qweb.render('t-out', {value: Markup`<b>${teststr}</b>`}),
|
||||
'<p><b><i>test</i></b></p>',
|
||||
"the markup template should not be escaped but what it uses should be"
|
||||
);
|
||||
const testMarkup = Markup(teststr);
|
||||
assert.equal(
|
||||
qweb.render('t-out', {value: Markup`<b>${testMarkup}</b>`}),
|
||||
'<p><b><i>test</i></b></p>',
|
||||
"markupception"
|
||||
);
|
||||
});
|
||||
QUnit.test("Set", async assert => {
|
||||
assert.expect(4);
|
||||
const qweb = new QWeb2.Engine;
|
||||
await add_template(qweb, `<templates>
|
||||
<t t-name="litval">
|
||||
<t t-set="x" t-value="'<a/>'"/>
|
||||
<x><t t-out="x"/></x>
|
||||
</t>
|
||||
<t t-name="body">
|
||||
<t t-set="x"><a/></t>
|
||||
<x><t t-out="x"/></x>
|
||||
</t>
|
||||
<t t-name="value">
|
||||
<t t-set="x" t-value="val"/>
|
||||
<x><t t-out="x"/></x>
|
||||
</t>
|
||||
<t t-name="bodyout">
|
||||
<t t-set="x"><t t-out="val"/></t>
|
||||
<x><t t-out="x"/></x>
|
||||
</t>
|
||||
</templates>`);
|
||||
|
||||
assert.equal(trim(qweb.render('litval')), '<x><a/></x>');
|
||||
assert.equal(trim(qweb.render('body')), '<x><a></a></x>');
|
||||
assert.equal(trim(qweb.render('value', {val: '<a/>'})), '<x><a/></x>');
|
||||
assert.equal(trim(qweb.render('bodyout', {val: '<a/>'})), '<x><a/></x>');
|
||||
});
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,6 +13,7 @@ var testUtils = require('web.test_utils');
|
||||
var Widget = require('web.Widget');
|
||||
var widgetRegistry = require('web.widget_registry');
|
||||
const widgetRegistryOwl = require('web.widgetRegistry');
|
||||
const {Markup} = require('web.utils');
|
||||
|
||||
var makeTestPromise = testUtils.makeTestPromise;
|
||||
var nextTick = testUtils.nextTick;
|
||||
@@ -3941,10 +3942,11 @@ QUnit.module('Views', {
|
||||
examples:[{
|
||||
name: "A first example",
|
||||
columns: ["Column 1", "Column 2", "Column 3"],
|
||||
description: "Some description",
|
||||
description: "A <b>weak</b> description.",
|
||||
}, {
|
||||
name: "A second example",
|
||||
columns: ["Col 1", "Col 2"],
|
||||
description: Markup`A <b>fantastic</b> description.`
|
||||
}],
|
||||
});
|
||||
|
||||
@@ -3982,23 +3984,27 @@ QUnit.module('Views', {
|
||||
assert.strictEqual($('.modal .o_kanban_examples_dialog_content .tab-pane').length, 2,
|
||||
"should have two examples");
|
||||
|
||||
var $firstPane = $('.modal .o_kanban_examples_dialog_content .tab-pane:first');
|
||||
const $panes = $('.modal .o_kanban_examples_dialog_content .tab-pane');
|
||||
var $firstPane = $panes.eq(0);
|
||||
assert.strictEqual($firstPane.find('.o_kanban_examples_group').length, 3,
|
||||
"there should be 3 stages in the first example");
|
||||
assert.strictEqual($firstPane.find('h6').text(), 'Column 1Column 2Column 3',
|
||||
"column titles should be correct");
|
||||
assert.strictEqual($firstPane.find('.o_kanban_examples_description').text().trim(),
|
||||
"Some description", "the correct description should be displayed");
|
||||
assert.strictEqual($firstPane.find('.o_kanban_examples_description').html().trim(),
|
||||
"A <b>weak</b> description.",
|
||||
"An escaped description should be displayed");
|
||||
|
||||
var $secondPane = $('.modal .o_kanban_examples_dialog_content .tab-pane:nth(1)');
|
||||
var $secondPane = $panes.eq(1);
|
||||
assert.strictEqual($secondPane.find('.o_kanban_examples_group').length, 2,
|
||||
"there should be 2 stages in the second example");
|
||||
assert.strictEqual($secondPane.find('h6').text(), 'Col 1Col 2',
|
||||
"column titles should be correct");
|
||||
assert.strictEqual($secondPane.find('.o_kanban_examples_description').text().trim(),
|
||||
"", "there should be no description for the second example");
|
||||
assert.strictEqual($secondPane.find('.o_kanban_examples_description').html().trim(),
|
||||
"A <b>fantastic</b> description.",
|
||||
"A formatted description should be displayed.");
|
||||
|
||||
kanban.destroy();
|
||||
delete kanbanExamplesRegistry.map['test'];
|
||||
});
|
||||
|
||||
QUnit.test("quick create column's apply button's display text", async function (assert) {
|
||||
@@ -4010,7 +4016,6 @@ QUnit.module('Views', {
|
||||
examples:[{
|
||||
name: "A first example",
|
||||
columns: ["Column 1", "Column 2", "Column 3"],
|
||||
description: "Some description",
|
||||
}, {
|
||||
name: "A second example",
|
||||
columns: ["Col 1", "Col 2"],
|
||||
@@ -4052,7 +4057,6 @@ QUnit.module('Views', {
|
||||
examples:[{
|
||||
name: "A first example",
|
||||
columns: ["Column 1", "Column 2", "Column 3"],
|
||||
description: "Some description",
|
||||
}, {
|
||||
name: "A second example",
|
||||
columns: ["Col 1", "Col 2"],
|
||||
@@ -7782,7 +7786,7 @@ QUnit.module('Views', {
|
||||
<div class="oe_kanban_global_click">
|
||||
<field name="display_name"/>
|
||||
<div class="test" t-if="!widget.isHtmlEmpty(record.description.raw_value)">
|
||||
<t t-raw="record.description.raw_value"/>
|
||||
<t t-out="record.description.value"/>
|
||||
</div>
|
||||
</div>
|
||||
</t></templates>
|
||||
|
||||
@@ -13,13 +13,13 @@
|
||||
<link type="text/css" rel="stylesheet" t-att-href="cssLib"/>
|
||||
</t>
|
||||
<t t-foreach="asset.cssContents || []" t-as="cssContent">
|
||||
<style type="text/css" t-raw="cssContent"/>
|
||||
<style type="text/css" t-out="cssContent"/>
|
||||
</t>
|
||||
<t t-foreach="asset.jsLibs || []" t-as="jsLib">
|
||||
<script type="text/javascript" t-att-src="jsLib"/>
|
||||
</t>
|
||||
<t t-foreach="asset.jsContents || []" t-as="jsContent" t-if="jsContent.indexOf('inline asset') !== -1">
|
||||
<script type="text/javascript" t-raw="jsContent"/>
|
||||
<script type="text/javascript" t-out="jsContent"/>
|
||||
</t>
|
||||
</t>
|
||||
<script type="text/javascript">
|
||||
@@ -125,7 +125,7 @@
|
||||
<t t-name="wysiwyg.widgets.files.submenu">
|
||||
<div class="form-inline align-items-center py-4">
|
||||
<t t-call="wysiwyg.widgets.media.search"/>
|
||||
<t t-raw="0"/>
|
||||
<t t-out="0"/>
|
||||
|
||||
<input type="file" class="d-none o_file_input" name="upload" t-att-accept="widget.options.accept" t-att-multiple="widget.options.multiImages && 'multiple'"/>
|
||||
|
||||
@@ -133,7 +133,7 @@
|
||||
<input type="text" class="form-control o_we_url_input o_we_horizontal_collapse o_we_transition_ease" name="url" t-att-placeholder="urlPlaceholder"/>
|
||||
<div class="input-group-append align-items-center">
|
||||
<button type="button" class="btn btn-secondary o_upload_media_url_button">
|
||||
<t t-esc="addText"/>
|
||||
<t t-out="addText"/>
|
||||
</button>
|
||||
<div class="ml-2">
|
||||
<span class="o_we_url_success text-success d-none fa fa-lg fa-check" title="The URL seems valid."/>
|
||||
@@ -145,7 +145,7 @@
|
||||
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-primary o_upload_media_button">
|
||||
<t t-esc="uploadText"/>
|
||||
<t t-out="uploadText"/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -11,6 +11,7 @@ var RunningTourActionHelper = require('web_tour.RunningTourActionHelper');
|
||||
var ServicesMixin = require('web.ServicesMixin');
|
||||
var session = require('web.session');
|
||||
var Tip = require('web_tour.Tip');
|
||||
const {Markup} = require('web.utils');
|
||||
|
||||
var _t = core._t;
|
||||
|
||||
@@ -92,7 +93,7 @@ return core.Class.extend(mixins.EventDispatcherMixin, ServicesMixin, {
|
||||
wait_for: options.wait_for || Promise.resolve(),
|
||||
};
|
||||
if (options.skip_enabled) {
|
||||
tour.skip_link = '<p><span class="o_skip_tour">' + _t('Skip tour') + '</span></p>';
|
||||
tour.skip_link = Markup`<p><span class="o_skip_tour">${_t('Skip tour')}</span></p>`;
|
||||
tour.skip_handler = function (tip) {
|
||||
this._deactivate_tip(tip);
|
||||
this._consume_tour(name);
|
||||
@@ -353,7 +354,7 @@ return core.Class.extend(mixins.EventDispatcherMixin, ServicesMixin, {
|
||||
var tip_info = tip;
|
||||
if (tour.skip_link) {
|
||||
tip_info = _.extend(_.omit(tip_info, 'content'), {
|
||||
content: tip.content + tour.skip_link,
|
||||
content: Markup`${tip.content}${tour.skip_link}`,
|
||||
event_handlers: [{
|
||||
event: 'click',
|
||||
selector: '.o_skip_tour',
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
odoo.define('web_tour.TourStepUtils', function (require) {
|
||||
'use strict';
|
||||
|
||||
const core = require('web.core');
|
||||
const _t = core._t;
|
||||
const {_t, Class} = require('web.core');
|
||||
const {Markup} = require('web.utils');
|
||||
|
||||
return core.Class.extend({
|
||||
return Class.extend({
|
||||
_getHelpMessage: (functionName, ...args) => `Generated by function tour utils ${functionName}(${args.join(', ')})`,
|
||||
|
||||
addDebugHelp: helpMessage => step => {
|
||||
@@ -39,7 +39,7 @@ return core.Class.extend({
|
||||
return {
|
||||
edition: 'enterprise',
|
||||
trigger: '.o_main_navbar .o_menu_toggle',
|
||||
content: _t('Click on the <i>Home icon</i> to navigate across apps.'),
|
||||
content: Markup(_t('Click on the <i>Home icon</i> to navigate across apps.')),
|
||||
position: 'bottom',
|
||||
};
|
||||
},
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<div t-name="Tip" t-attf-class="o_tooltip #{widget.info.position} #{widget.is_anchor_fixed_position ? 'o_tooltip_fixed' : ''}">
|
||||
<div class="o_tooltip_overlay"/>
|
||||
<div class="o_tooltip_content">
|
||||
<t t-raw="widget.info.content"/>
|
||||
<t t-out="widget.info.content"/>
|
||||
</div>
|
||||
</div>
|
||||
</templates>
|
||||
|
||||
@@ -308,7 +308,7 @@ class Website(Home):
|
||||
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, with_sample) or ''
|
||||
return dynamic_filter and dynamic_filter._render(template_key, limit, search_domain, with_sample) or ''
|
||||
|
||||
@http.route('/website/snippet/options_filters', type='json', auth='user', website=True)
|
||||
def get_dynamic_snippet_filters(self, model_name=None, search_domain=None):
|
||||
|
||||
@@ -55,7 +55,7 @@ class WebsiteSnippetFilter(models.Model):
|
||||
if not field_name.strip():
|
||||
raise ValidationError(_("Empty field name in %r") % (record.field_names))
|
||||
|
||||
def render(self, template_key, limit, search_domain=None, with_sample=False):
|
||||
def _render(self, template_key, limit, search_domain=None, with_sample=False):
|
||||
"""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_ ")
|
||||
@@ -76,8 +76,8 @@ class WebsiteSnippetFilter(models.Model):
|
||||
content = View._render_template(template_key, dict(
|
||||
records=records,
|
||||
is_sample=is_sample,
|
||||
)).decode('utf-8')
|
||||
return [ET.tostring(el, encoding='utf-8') for el in ET.fromstring('<root>%s</root>' % content).getchildren()]
|
||||
))
|
||||
return [ET.tostring(el, encoding='unicode') for el in ET.fromstring(b'<root>%s</root>' % content).getchildren()]
|
||||
|
||||
def _prepare_values(self, limit=None, search_domain=None):
|
||||
"""Gets the data and returns it the right format for render."""
|
||||
|
||||
@@ -7,6 +7,7 @@ import time from 'web.time';
|
||||
import weWidgets from 'wysiwyg.widgets';
|
||||
import websiteNavbarData from 'website.navbar';
|
||||
import Widget from 'web.Widget';
|
||||
import { Markup } from 'web.utils';
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
@@ -1054,35 +1055,24 @@ var PageManagement = Widget.extend({
|
||||
* It will affect redirect after page deletion: reload or '/'
|
||||
*/
|
||||
// TODO: This function should be integrated in a widget in the future
|
||||
function _deletePage(pageId, fromPageManagement) {
|
||||
var self = this;
|
||||
new Promise(function (resolve, reject) {
|
||||
// Search the page dependencies
|
||||
self._getPageDependencies(pageId)
|
||||
.then(function (dependencies) {
|
||||
// Inform the user about those dependencies and ask him confirmation
|
||||
return new Promise(function (confirmResolve, confirmReject) {
|
||||
Dialog.safeConfirm(self, "", {
|
||||
title: _t("Delete Page"),
|
||||
$content: $(qweb.render('website.delete_page', {dependencies: dependencies})),
|
||||
confirm_callback: confirmResolve,
|
||||
cancel_callback: resolve,
|
||||
});
|
||||
});
|
||||
}).then(function () {
|
||||
// Delete the page if the user confirmed
|
||||
return self._rpc({
|
||||
model: 'website.page',
|
||||
method: 'unlink',
|
||||
args: [pageId],
|
||||
});
|
||||
}).then(function () {
|
||||
async function _deletePage(pageId, fromPageManagement) {
|
||||
const dependencies = await this._getPageDependencies(pageId);
|
||||
for (const locs of Object.values(dependencies)) {
|
||||
for (const loc of locs) {
|
||||
loc.text = Markup(loc.text);
|
||||
}
|
||||
}
|
||||
Dialog.safeConfirm(this, "", {
|
||||
title: _t("Delete Page"),
|
||||
$content: $(qweb.render('website.delete_page', {dependencies: dependencies})),
|
||||
async confirm_callback() {
|
||||
await this._rpc({model: 'website.page', method: 'unlink', args: [pageId]});
|
||||
if (fromPageManagement) {
|
||||
window.location.reload(true);
|
||||
} else {
|
||||
window.location.href = '/';
|
||||
}
|
||||
}, reject);
|
||||
}
|
||||
});
|
||||
}
|
||||
/**
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
odoo.define('website.mobile', function (require) {
|
||||
'use strict';
|
||||
|
||||
var core = require('web.core');
|
||||
const {_t} = require('web.core');
|
||||
const {Markup} = require('web.utils');
|
||||
var Dialog = require('web.Dialog');
|
||||
var websiteNavbarData = require('website.navbar');
|
||||
|
||||
const { registry } = require("@web/core/registry");
|
||||
|
||||
var _t = core._t;
|
||||
|
||||
var MobilePreviewDialog = Dialog.extend({
|
||||
/**
|
||||
* Tweaks the modal so that it appears as a phone and modifies the iframe
|
||||
@@ -56,7 +55,7 @@ var MobileMenu = websiteNavbarData.WebsiteNavbarActionWidget.extend({
|
||||
return this.mobilePreview.close();
|
||||
}
|
||||
this.mobilePreview = new MobilePreviewDialog(this, {
|
||||
title: _t('Mobile preview') + ' <span class="fa fa-refresh"/>',
|
||||
title: Markup(_.escape(_t('Mobile preview')) + ' <span class="fa fa-refresh"/>'),
|
||||
}).open();
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
odoo.define("website.tour_utils", function (require) {
|
||||
"use strict";
|
||||
|
||||
const core = require("web.core");
|
||||
const _t = core._t;
|
||||
|
||||
const {_t} = require("web.core");
|
||||
const {Markup} = require('web.utils');
|
||||
var tour = require("web_tour.tour");
|
||||
|
||||
function addMedia(position = "right") {
|
||||
return {
|
||||
trigger: `.modal-content footer .btn-primary`,
|
||||
content: _t("<b>Add</b> the selected image."),
|
||||
content: Markup(_t("<b>Add</b> the selected image.")),
|
||||
position: position,
|
||||
run: "click",
|
||||
};
|
||||
@@ -18,7 +17,7 @@ function addMedia(position = "right") {
|
||||
function changeBackground(snippet, position = "bottom") {
|
||||
return {
|
||||
trigger: ".o_we_customize_panel .o_we_bg_success",
|
||||
content: _t("<b>Customize</b> any block through this menu. Try to change the background image of this block."),
|
||||
content: Markup(_t("<b>Customize</b> any block through this menu. Try to change the background image of this block.")),
|
||||
position: position,
|
||||
run: "click",
|
||||
};
|
||||
@@ -27,7 +26,7 @@ function changeBackground(snippet, position = "bottom") {
|
||||
function changeBackgroundColor(position = "bottom") {
|
||||
return {
|
||||
trigger: ".o_we_customize_panel .o_we_color_preview",
|
||||
content: _t("<b>Customize</b> any block through this menu. Try to change the background color of this block."),
|
||||
content: Markup(_t("<b>Customize</b> any block through this menu. Try to change the background color of this block.")),
|
||||
position: position,
|
||||
run: "click",
|
||||
};
|
||||
@@ -37,7 +36,7 @@ function selectColorPalette(position = "left") {
|
||||
return {
|
||||
trigger: ".o_we_customize_panel .o_we_so_color_palette we-selection-items",
|
||||
alt_trigger: ".o_we_customize_panel .o_we_color_preview",
|
||||
content: _t(`<b>Select</b> a Color Palette.`),
|
||||
content: Markup(_t(`<b>Select</b> a Color Palette.`)),
|
||||
position: position,
|
||||
run: 'click',
|
||||
location: position === 'left' ? '#oe_snippets' : undefined,
|
||||
@@ -47,7 +46,7 @@ function selectColorPalette(position = "left") {
|
||||
function changeColumnSize(position = "right") {
|
||||
return {
|
||||
trigger: `.oe_overlay.ui-draggable.o_we_overlay_sticky.oe_active .o_handle.e`,
|
||||
content: _t("<b>Slide</b> this button to change the column size."),
|
||||
content: Markup(_t("<b>Slide</b> this button to change the column size.")),
|
||||
position: position,
|
||||
};
|
||||
}
|
||||
@@ -56,7 +55,7 @@ function changeIcon(snippet, index = 0, position = "bottom") {
|
||||
return {
|
||||
trigger: `#wrapwrap .${snippet.id} i:eq(${index})`,
|
||||
extra_trigger: "body.editor_enable",
|
||||
content: _t("<b>Double click on an icon</b> to change it with one of your choice."),
|
||||
content: Markup(_t("<b>Double click on an icon</b> to change it with one of your choice.")),
|
||||
position: position,
|
||||
run: "dblclick",
|
||||
};
|
||||
@@ -66,7 +65,7 @@ function changeImage(snippet, position = "bottom") {
|
||||
return {
|
||||
trigger: snippet.id ? `#wrapwrap .${snippet.id} img` : snippet,
|
||||
extra_trigger: "body.editor_enable",
|
||||
content: _t("<b>Double click on an image</b> to change it with one of your choice."),
|
||||
content: Markup(_t("<b>Double click on an image</b> to change it with one of your choice.")),
|
||||
position: position,
|
||||
run: "dblclick",
|
||||
};
|
||||
@@ -79,7 +78,7 @@ function changeOption(optionName, weName = '', optionTooltipLabel = '', position
|
||||
const option_block = `we-customizeblock-option[class='snippet-option-${optionName}']`
|
||||
return {
|
||||
trigger: `${option_block} ${weName}, ${option_block} [title='${weName}']`,
|
||||
content: _.str.sprintf(_t("<b>Click</b> on this option to change the %s of the block."), optionTooltipLabel),
|
||||
content: Markup(_.str.sprintf(_t("<b>Click</b> on this option to change the %s of the block."), optionTooltipLabel)),
|
||||
position: position,
|
||||
run: "click",
|
||||
};
|
||||
@@ -89,7 +88,7 @@ function selectNested(trigger, optionName, alt_trigger = null, optionTooltipLabe
|
||||
const option_block = `we-customizeblock-option[class='snippet-option-${optionName}']`;
|
||||
return {
|
||||
trigger: trigger,
|
||||
content: _.str.sprintf(_t("<b>Select</b> a %s."), optionTooltipLabel),
|
||||
content: Markup(_.str.sprintf(_t("<b>Select</b> a %s."), optionTooltipLabel)),
|
||||
alt_trigger: alt_trigger == null ? undefined : `${option_block} ${alt_trigger}`,
|
||||
position: position,
|
||||
run: 'click',
|
||||
@@ -106,7 +105,7 @@ function changePaddingSize(direction) {
|
||||
}
|
||||
return {
|
||||
trigger: `.oe_overlay.ui-draggable.o_we_overlay_sticky.oe_active .o_handle.${paddingDirection}`,
|
||||
content: _.str.sprintf(_t("<b>Slide</b> this button to change the %s padding"), direction),
|
||||
content: Markup(_.str.sprintf(_t("<b>Slide</b> this button to change the %s padding"), direction)),
|
||||
position: position,
|
||||
};
|
||||
}
|
||||
@@ -118,7 +117,7 @@ function changePaddingSize(direction) {
|
||||
function clickOnEdit(position = "bottom") {
|
||||
return {
|
||||
trigger: "a[data-action=edit]",
|
||||
content: _t("<b>Click Edit</b> to start designing your homepage."),
|
||||
content: Markup(_t("<b>Click Edit</b> to start designing your homepage.")),
|
||||
extra_trigger: ".homepage",
|
||||
position: position,
|
||||
};
|
||||
@@ -133,7 +132,7 @@ function clickOnSnippet(snippet, position = "bottom") {
|
||||
return {
|
||||
trigger: snippet.id ? `#wrapwrap .${snippet.id}` : snippet,
|
||||
extra_trigger: "body.editor_enable",
|
||||
content: _t("<b>Click on a snippet</b> to access its options menu."),
|
||||
content: Markup(_t("<b>Click on a snippet</b> to access its options menu.")),
|
||||
position: position,
|
||||
run: "click",
|
||||
};
|
||||
@@ -143,7 +142,7 @@ function clickOnSave(position = "bottom") {
|
||||
return [{
|
||||
trigger: "button[data-action=save]",
|
||||
in_modal: false,
|
||||
content: _t("Good job! It's time to <b>Save</b> your work."),
|
||||
content: Markup(_t("Good job! It's time to <b>Save</b> your work.")),
|
||||
position: position,
|
||||
}, {
|
||||
trigger: 'body:not(.editor_enable)',
|
||||
@@ -162,7 +161,7 @@ function clickOnText(snippet, element, position = "bottom") {
|
||||
return {
|
||||
trigger: snippet.id ? `#wrapwrap .${snippet.id} ${element}` : snippet,
|
||||
extra_trigger: "body.editor_enable",
|
||||
content: _t("<b>Click on a text</b> to start editing it."),
|
||||
content: Markup(_t("<b>Click on a text</b> to start editing it.")),
|
||||
position: position,
|
||||
run: "text",
|
||||
consumeEvent: "input",
|
||||
@@ -179,7 +178,7 @@ function dragNDrop(snippet, position = "bottom") {
|
||||
trigger: `#oe_snippets .oe_snippet[name="${snippet.name}"] .oe_snippet_thumbnail:not(.o_we_already_dragging)`,
|
||||
extra_trigger: "body.editor_enable.editor_has_snippets",
|
||||
moveTrigger: '.oe_drop_zone',
|
||||
content: _.str.sprintf(_t("Drag the <b>%s</b> building block and drop it at the bottom of the page."), snippet.name),
|
||||
content: Markup(_.str.sprintf(_t("Drag the <b>%s</b> building block and drop it at the bottom of the page."), snippet.name)),
|
||||
position: position,
|
||||
// Normally no main snippet can be dropped in the default footer but
|
||||
// targeting it allows to force "dropping at the end of the page".
|
||||
@@ -208,7 +207,7 @@ function goToTheme(position = "bottom") {
|
||||
function selectHeader(position = "bottom") {
|
||||
return {
|
||||
trigger: `header#top`,
|
||||
content: _t(`<b>Click</b> on this header to configure it.`),
|
||||
content: Markup(_t(`<b>Click</b> on this header to configure it.`)),
|
||||
position: position,
|
||||
run: "click",
|
||||
};
|
||||
@@ -217,7 +216,7 @@ function selectHeader(position = "bottom") {
|
||||
function selectSnippetColumn(snippet, index = 0, position = "bottom") {
|
||||
return {
|
||||
trigger: `#wrapwrap .${snippet.id} .row div[class*="col-lg-"]:eq(${index})`,
|
||||
content: _t("<b>Click</b> on this column to access its options."),
|
||||
content: Markup(_t("<b>Click</b> on this column to access its options.")),
|
||||
position: position,
|
||||
run: "click",
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@ odoo.define('website.s_dynamic_snippet', function (require) {
|
||||
const core = require('web.core');
|
||||
const config = require('web.config');
|
||||
const publicWidget = require('web.public.widget');
|
||||
const {Markup} = require('web.utils');
|
||||
|
||||
const DynamicSnippet = publicWidget.Widget.extend({
|
||||
selector: '.s_dynamic_snippet',
|
||||
@@ -110,29 +111,22 @@ const DynamicSnippet = publicWidget.Widget.extend({
|
||||
* Fetches the data.
|
||||
* @private
|
||||
*/
|
||||
_fetchData: function () {
|
||||
async _fetchData() {
|
||||
if (this._isConfigComplete()) {
|
||||
return this._rpc(
|
||||
{
|
||||
'route': '/website/snippet/filters',
|
||||
'params': Object.assign({
|
||||
'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(),
|
||||
'with_sample': this.editableMode,
|
||||
}, this._getRpcParameters()),
|
||||
})
|
||||
.then(
|
||||
(data) => {
|
||||
this.data = data;
|
||||
}
|
||||
);
|
||||
} else {
|
||||
return new Promise((resolve) => {
|
||||
this.data = [];
|
||||
resolve();
|
||||
const nodeData = this.el.dataset;
|
||||
const filterFragments = await this._rpc({
|
||||
'route': '/website/snippet/filters',
|
||||
'params': Object.assign({
|
||||
'filter_id': parseInt(nodeData.filterId),
|
||||
'template_key': nodeData.templateKey,
|
||||
'limit': parseInt(nodeData.numberOfRecords),
|
||||
'search_domain': this._getSearchDomain(),
|
||||
'with_sample': this.editableMode,
|
||||
}, this._getRpcParameters()),
|
||||
});
|
||||
this.data = filterFragments.map(Markup);
|
||||
} else {
|
||||
this.data = [];
|
||||
}
|
||||
},
|
||||
/**
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<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]"/>
|
||||
<t t-out="data[rowIndex * chunkSize + colIndex]"/>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<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]"/>
|
||||
<t t-out="data[slideIndex * chunkSize + itemIndex]"/>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
@@ -44,10 +44,10 @@
|
||||
<ul>
|
||||
<li t-foreach="type_value" t-as="error">
|
||||
<a t-if="!_.contains(['', '#', false], error.link)" t-att-href="error.link">
|
||||
<t t-raw="error.text"/>
|
||||
<t t-out="error.text"/>
|
||||
</a>
|
||||
<t t-else="">
|
||||
<t t-raw="error.text"/>
|
||||
<t t-out="error.text"/>
|
||||
</t>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
<t t-call="website.form_label_content"/>
|
||||
</label>
|
||||
<div class="col-sm">
|
||||
<t t-raw="0"/>
|
||||
<t t-out="0"/>
|
||||
<t t-call="website.form_field_description"/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -49,7 +49,7 @@
|
||||
<label t-attf-class="s_website_form_label #{field.formatInfo.labelPosition == 'none' and 'd-none' or ''}" t-attf-style="width: #{field.formatInfo.labelWidth or '200px'}" t-att-for="field.id">
|
||||
<t t-call="website.form_label_content"/>
|
||||
</label>
|
||||
<t t-raw="0"/>
|
||||
<t t-out="0"/>
|
||||
<t t-call="website.form_field_description"/>
|
||||
</t>
|
||||
</div>
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
odoo.define("website_blog.tour", function (require) {
|
||||
"use strict";
|
||||
|
||||
var core = require("web.core");
|
||||
const {_t} = require("web.core");
|
||||
const {Markup} = require('web.utils');
|
||||
var tour = require("web_tour.tour");
|
||||
|
||||
var _t = core._t;
|
||||
|
||||
tour.register("blog", {
|
||||
url: "/",
|
||||
}, [{
|
||||
@@ -30,7 +29,7 @@ odoo.define("website_blog.tour", function (require) {
|
||||
}, {
|
||||
trigger: "we-button[data-background]:nth(1)",
|
||||
extra_trigger: "#wrap div[data-oe-expression=\"blog_post.name\"]:not(:containsExact(\"\"))",
|
||||
content: _t("Set a blog post <b>cover</b>."),
|
||||
content: Markup(_t("Set a blog post <b>cover</b>.")),
|
||||
position: "right",
|
||||
}, {
|
||||
trigger: ".o_select_media_dialog .o_we_search",
|
||||
@@ -43,7 +42,7 @@ odoo.define("website_blog.tour", function (require) {
|
||||
position: "top",
|
||||
}, {
|
||||
trigger: "#o_wblog_post_content",
|
||||
content: _t("<b>Write your story here.</b> Use the top toolbar to style your text: add an image or table, set bold or italic, etc. Drag and drop building blocks for more graphical blogs."),
|
||||
content: Markup(_t("<b>Write your story here.</b> Use the top toolbar to style your text: add an image or table, set bold or italic, etc. Drag and drop building blocks for more graphical blogs.")),
|
||||
position: "top",
|
||||
run: function (actions) {
|
||||
actions.auto();
|
||||
@@ -52,12 +51,12 @@ odoo.define("website_blog.tour", function (require) {
|
||||
}, {
|
||||
trigger: "button[data-action=save]",
|
||||
extra_trigger: "#o_wblog_post_content .o_wblog_post_content_field p:first:not(:containsExact(" + _t("Start writing here...") + "))",
|
||||
content: _t("<b>Click on Save</b> to record your changes."),
|
||||
content: Markup(_t("<b>Click on Save</b> to record your changes.")),
|
||||
position: "bottom",
|
||||
}, {
|
||||
trigger: "a[data-action=show-mobile-preview]",
|
||||
extra_trigger: "body:not(.editor_enable)",
|
||||
content: _t("Use this icon to preview your blog post on <b>mobile devices</b>."),
|
||||
content: Markup(_t("Use this icon to preview your blog post on <b>mobile devices</b>.")),
|
||||
position: "bottom",
|
||||
}, {
|
||||
trigger: "button[data-dismiss=modal]",
|
||||
@@ -68,11 +67,11 @@ odoo.define("website_blog.tour", function (require) {
|
||||
trigger: ".js_publish_management .js_publish_btn",
|
||||
extra_trigger: "body:not(.editor_enable)",
|
||||
position: "bottom",
|
||||
content: _t("<b>Publish your blog post</b> to make it visible to your visitors."),
|
||||
content: Markup(_t("<b>Publish your blog post</b> to make it visible to your visitors.")),
|
||||
}, {
|
||||
trigger: "#customize-menu > a",
|
||||
extra_trigger: ".js_publish_management .js_publish_btn .css_unpublish:visible",
|
||||
content: _t("<b>That's it, your blog post is published!</b> Discover more features through the <i>Customize</i> menu."),
|
||||
content: Markup(_t("<b>That's it, your blog post is published!</b> Discover more features through the <i>Customize</i> menu.")),
|
||||
position: "bottom",
|
||||
width: 500,
|
||||
}]);
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<div class="oe_structure" id="oe_structure_website_crm_partner_assign_layout_1"/>
|
||||
<div class="container mt16">
|
||||
<div class="row">
|
||||
<t t-raw="ref_content" />
|
||||
<t t-out="0" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="oe_structure" id="oe_structure_website_crm_partner_assign_layout_2"/>
|
||||
@@ -19,95 +19,93 @@
|
||||
|
||||
<template id="index" name="Find Resellers">
|
||||
<t t-call="website_crm_partner_assign.layout">
|
||||
<t t-set="ref_content">
|
||||
<div class="col-lg-12">
|
||||
<h1 class="text-center">
|
||||
Looking For a Local Store?
|
||||
</h1><h2 class="text-center text-muted">
|
||||
Contact a reseller
|
||||
</h2>
|
||||
<div class="col-lg-12">
|
||||
<h1 class="text-center">
|
||||
Looking For a Local Store?
|
||||
</h1><h2 class="text-center text-muted">
|
||||
Contact a reseller
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-3 mb32" id="partner_left">
|
||||
|
||||
<ul id="reseller_grades" class="nav nav-pills flex-column mt16">
|
||||
<li class="nav-header nav-item"><h3>Filter by Level</h3></li>
|
||||
<t t-foreach="grades" t-as="grade">
|
||||
<li class="nav-item">
|
||||
<a t-attf-href="/partners#{ grade['grade_id'][0] and '/grade/%s' % grade['grade_id'][0] or '' }#{ current_country and '/country/%s' % slug(current_country) or '' }#{ '?' + (search_path or '') + '&' + keep_query('country_all') }"
|
||||
t-attf-class="nav-link#{grade['active'] and ' active' or ''}">
|
||||
<span class="badge badge-pill float-right" t-esc="grade['grade_id_count'] or ''"/>
|
||||
<t t-esc="grade['grade_id'][1]"/>
|
||||
</a>
|
||||
</li>
|
||||
</t>
|
||||
</ul>
|
||||
|
||||
<ul id="reseller_countries" class="nav nav-pills flex-column mt16">
|
||||
<li class="nav-header nav-item"><h3>Filter by Country</h3></li>
|
||||
<t t-foreach="countries" t-as="country">
|
||||
<li t-if="country['country_id']" class="nav-item">
|
||||
<a t-attf-href="/partners#{ current_grade and '/grade/%s' % slug(current_grade) or ''}#{country['country_id'][0] and '/country/%s' % country['country_id'][0] or '' }#{ '?' + (search_path or '') + (country['country_id'][0] == 0 and '&country_all=True' or '')}"
|
||||
t-attf-class="nav-link#{country['active'] and ' active' or ''}">
|
||||
<span class="badge badge-pill float-right" t-esc="country['country_id_count'] or ''"/>
|
||||
<t t-esc="country['country_id'][1]"/>
|
||||
</a>
|
||||
</li>
|
||||
</t>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="col-lg-8 offset-lg-1" id="ref_content">
|
||||
<div class="d-flex p-2">
|
||||
<t t-call="website.pager"/>
|
||||
<form action="" method="get" class="form-inline ml-auto">
|
||||
<div class="form-group">
|
||||
<input t-if="country_all" type="hidden" name="country_all" value="True" />
|
||||
<input type="text" name="search" class="search-query form-control" placeholder="Search" t-att-value="searches.get('search', '')"/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-3 mb32" id="partner_left">
|
||||
|
||||
<ul id="reseller_grades" class="nav nav-pills flex-column mt16">
|
||||
<li class="nav-header nav-item"><h3>Filter by Level</h3></li>
|
||||
<t t-foreach="grades" t-as="grade">
|
||||
<li class="nav-item">
|
||||
<a t-attf-href="/partners#{ grade['grade_id'][0] and '/grade/%s' % grade['grade_id'][0] or '' }#{ current_country and '/country/%s' % slug(current_country) or '' }#{ '?' + (search_path or '') + '&' + keep_query('country_all') }"
|
||||
t-attf-class="nav-link#{grade['active'] and ' active' or ''}">
|
||||
<span class="badge badge-pill float-right" t-esc="grade['grade_id_count'] or ''"/>
|
||||
<t t-esc="grade['grade_id'][1]"/>
|
||||
<div>
|
||||
<p t-if="not partners">No result found</p>
|
||||
<t t-foreach="partners" t-as="partner">
|
||||
<t t-if="last_grade != partner.grade_id.id">
|
||||
<h3 class="text-center mt-4">
|
||||
<span t-field="partner.grade_id"/> Partners
|
||||
<t t-call="website.publish_management">
|
||||
<t t-set="object" t-value="partner.grade_id"/>
|
||||
<t t-set="publish_edit" t-value="True"/>
|
||||
</t>
|
||||
</h3>
|
||||
<t t-set="last_grade" t-value="partner.grade_id.id"/>
|
||||
</t>
|
||||
<div class="media mt-3">
|
||||
<a t-attf-href="/partners/#{slug(partner)}?#{current_grade and 'grade_id=%s&' % current_grade.id}#{current_country and 'country_id=%s' % current_country.id}"
|
||||
t-field="partner.avatar_128"
|
||||
class="mr-3 text-center o_width_128"
|
||||
t-options='{"widget": "image", "qweb_img_responsive": False, "class": "o_image_128_max"}'
|
||||
></a>
|
||||
<div class="media-body o_partner_body" style="min-height: 64px;">
|
||||
<a t-attf-href="/partners/#{slug(partner)}?#{current_grade and 'grade_id=%s&' % current_grade.id}#{current_country and 'country_id=%s' % current_country.id}">
|
||||
<span t-field="partner.display_name"/>
|
||||
</a>
|
||||
</li>
|
||||
</t>
|
||||
</ul>
|
||||
|
||||
<ul id="reseller_countries" class="nav nav-pills flex-column mt16">
|
||||
<li class="nav-header nav-item"><h3>Filter by Country</h3></li>
|
||||
<t t-foreach="countries" t-as="country">
|
||||
<li t-if="country['country_id']" class="nav-item">
|
||||
<a t-attf-href="/partners#{ current_grade and '/grade/%s' % slug(current_grade) or ''}#{country['country_id'][0] and '/country/%s' % country['country_id'][0] or '' }#{ '?' + (search_path or '') + (country['country_id'][0] == 0 and '&country_all=True' or '')}"
|
||||
t-attf-class="nav-link#{country['active'] and ' active' or ''}">
|
||||
<span class="badge badge-pill float-right" t-esc="country['country_id_count'] or ''"/>
|
||||
<t t-esc="country['country_id'][1]"/>
|
||||
</a>
|
||||
</li>
|
||||
</t>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="col-lg-8 offset-lg-1" id="ref_content">
|
||||
<div class="d-flex p-2">
|
||||
<t t-call="website.pager"/>
|
||||
<form action="" method="get" class="form-inline ml-auto">
|
||||
<div class="form-group">
|
||||
<input t-if="country_all" type="hidden" name="country_all" value="True" />
|
||||
<input type="text" name="search" class="search-query form-control" placeholder="Search" t-att-value="searches.get('search', '')"/>
|
||||
<div t-field="partner.website_short_description"/>
|
||||
<t t-if="any(p.website_published for p in partner.implemented_partner_ids)">
|
||||
<small><a t-attf-href="/partners/#{slug(partner)}#right_column">
|
||||
<t t-esc="partner.implemented_count"/> reference(s)
|
||||
</a></small>
|
||||
</t>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div>
|
||||
<p t-if="not partners">No result found</p>
|
||||
<t t-foreach="partners" t-as="partner">
|
||||
<t t-if="last_grade != partner.grade_id.id">
|
||||
<h3 class="text-center mt-4">
|
||||
<span t-field="partner.grade_id"/> Partners
|
||||
<t t-call="website.publish_management">
|
||||
<t t-set="object" t-value="partner.grade_id"/>
|
||||
<t t-set="publish_edit" t-value="True"/>
|
||||
</t>
|
||||
</h3>
|
||||
<t t-set="last_grade" t-value="partner.grade_id.id"/>
|
||||
</t>
|
||||
<div class="media mt-3">
|
||||
<a t-attf-href="/partners/#{slug(partner)}?#{current_grade and 'grade_id=%s&' % current_grade.id}#{current_country and 'country_id=%s' % current_country.id}"
|
||||
t-field="partner.avatar_128"
|
||||
class="mr-3 text-center o_width_128"
|
||||
t-options='{"widget": "image", "qweb_img_responsive": False, "class": "o_image_128_max"}'
|
||||
></a>
|
||||
<div class="media-body o_partner_body" style="min-height: 64px;">
|
||||
<a t-attf-href="/partners/#{slug(partner)}?#{current_grade and 'grade_id=%s&' % current_grade.id}#{current_country and 'country_id=%s' % current_country.id}">
|
||||
<span t-field="partner.display_name"/>
|
||||
</a>
|
||||
<div t-field="partner.website_short_description"/>
|
||||
<t t-if="any(p.website_published for p in partner.implemented_partner_ids)">
|
||||
<small><a t-attf-href="/partners/#{slug(partner)}#right_column">
|
||||
<t t-esc="partner.implemented_count"/> reference(s)
|
||||
</a></small>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
<div class='navbar'>
|
||||
<t t-call="website.pager">
|
||||
<t t-set="classname" t-valuef="float-left"/>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
<div class='navbar'>
|
||||
<t t-call="website.pager">
|
||||
<t t-set="classname" t-valuef="float-left"/>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
@@ -139,17 +137,15 @@
|
||||
|
||||
<template id="partner" name="Partner Detail">
|
||||
<t t-call="website_crm_partner_assign.layout">
|
||||
<t t-set="ref_content">
|
||||
<div class="col-lg-5">
|
||||
<ol t-if="not edit_page" class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a t-attf-href="/partners#{current_grade and '/grade/%s' % slug(current_grade)}#{current_country and '/country/%s' % slug(current_country)}">Our Partners</a></li>
|
||||
<li class="breadcrumb-item active"><span t-field="partner.display_name"/></li>
|
||||
</ol>
|
||||
</div>
|
||||
<t t-call="website_partner.partner_detail">
|
||||
<t t-set="right_column">
|
||||
<div id="right_column" class="mb16"><t t-call="website_crm_partner_assign.references_block"/></div>
|
||||
</t>
|
||||
<div class="col-lg-5">
|
||||
<ol t-if="not edit_page" class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a t-attf-href="/partners#{current_grade and '/grade/%s' % slug(current_grade)}#{current_country and '/country/%s' % slug(current_country)}">Our Partners</a></li>
|
||||
<li class="breadcrumb-item active"><span t-field="partner.display_name"/></li>
|
||||
</ol>
|
||||
</div>
|
||||
<t t-call="website_partner.partner_detail">
|
||||
<t t-set="right_column">
|
||||
<div id="right_column" class="mb16"><t t-call="website_crm_partner_assign.references_block"/></div>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
odoo.define('website_event.event_steps', function (require) {
|
||||
"use strict";
|
||||
|
||||
var core = require('web.core');
|
||||
var _t = core._t;
|
||||
const {_t} = require('web.core');
|
||||
const {Markup} = require('web.utils');
|
||||
|
||||
var EventAdditionalTourSteps = require('event.event_steps');
|
||||
|
||||
@@ -16,28 +16,28 @@ EventAdditionalTourSteps.include({
|
||||
this._super.apply(this, arguments);
|
||||
return [{
|
||||
trigger: '.o_event_form_view button[name="is_published"]',
|
||||
content: _t("Use this <b>shortcut</b> to easily access your event web page."),
|
||||
content: Markup(_t("Use this <b>shortcut</b> to easily access your event web page.")),
|
||||
position: 'bottom',
|
||||
}, {
|
||||
trigger: 'li#edit-page-menu a',
|
||||
extra_trigger: '.o_wevent_event',
|
||||
content: _t("With the Edit button, you can <b>customize</b> the web page visitors will see when registering."),
|
||||
content: Markup(_t("With the Edit button, you can <b>customize</b> the web page visitors will see when registering.")),
|
||||
position: 'bottom',
|
||||
}, {
|
||||
trigger: 'div[name="Image - Text"] .oe_snippet_thumbnail',
|
||||
extra_trigger: '.o_wevent_event',
|
||||
content: _t("<b>Drag and Drop</b> this snippet below the event title."),
|
||||
content: Markup(_t("<b>Drag and Drop</b> this snippet below the event title.")),
|
||||
position: 'bottom',
|
||||
run: 'drag_and_drop #o_wevent_event_main_col',
|
||||
}, {
|
||||
trigger: 'button[data-action="save"]',
|
||||
extra_trigger: '.o_wevent_event',
|
||||
content: _t("Don't forget to click <b>save</b> when you're done."),
|
||||
content: Markup(_t("Don't forget to click <b>save</b> when you're done.")),
|
||||
position: 'bottom',
|
||||
}, {
|
||||
trigger: 'label.js_publish_btn',
|
||||
extra_trigger: '.o_wevent_event',
|
||||
content: _t("Looking great! Let's now <b>publish</b> this page so that it becomes <b>visible</b> on your website!"),
|
||||
content: Markup(_t("Looking great! Let's now <b>publish</b> this page so that it becomes <b>visible</b> on your website!")),
|
||||
position: 'bottom',
|
||||
}, {
|
||||
trigger: 'a.css_edit_dynamic',
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
odoo.define("website_event.tour", function (require) {
|
||||
"use strict";
|
||||
|
||||
var core = require("web.core");
|
||||
const {_t} = require("web.core");
|
||||
const {Markup} = require('web.utils');
|
||||
var tour = require("web_tour.tour");
|
||||
|
||||
var _t = core._t;
|
||||
|
||||
tour.register("website_event_tour", {
|
||||
test: true,
|
||||
url: "/",
|
||||
@@ -19,12 +18,12 @@ odoo.define("website_event.tour", function (require) {
|
||||
position: "bottom",
|
||||
}, {
|
||||
trigger: '.modal-dialog #editor_new_event input[type=text]',
|
||||
content: _t("Create a name for your new event and click <em>\"Continue\"</em>. e.g: Technical Training"),
|
||||
content: Markup(_t("Create a name for your new event and click <em>\"Continue\"</em>. e.g: Technical Training")),
|
||||
position: "left",
|
||||
}, {
|
||||
trigger: '.modal-footer button.btn-primary.btn-continue',
|
||||
extra_trigger: '#editor_new_event input[type=text][value!=""]',
|
||||
content: _t("Click <em>Continue</em> to create the event."),
|
||||
content: Markup(_t("Click <em>Continue</em> to create the event.")),
|
||||
position: "right",
|
||||
}, {
|
||||
trigger: "#snippet_structure .oe_snippet:eq(2) .oe_snippet_thumbnail",
|
||||
|
||||
@@ -3,6 +3,7 @@ odoo.define('website_event_exhibitor.event_exhibitor_connect', function (require
|
||||
|
||||
var Dialog = require('web.Dialog');
|
||||
var publicWidget = require('web.public.widget');
|
||||
const {Markup} = require('web.utils');
|
||||
|
||||
var ExhibitorConnectClosedDialog = Dialog.extend({
|
||||
events: _.extend({}, Dialog.prototype.events, {
|
||||
@@ -44,18 +45,13 @@ var ExhibitorConnectClosedDialog = Dialog.extend({
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @returns {Promise<*>} promise after fetching sponsor data, given its
|
||||
* sponsorId. Necessary to render template content;
|
||||
*/
|
||||
_fetchSponsor: function () {
|
||||
let self = this;
|
||||
let rpcPromise = this._rpc({
|
||||
route: `/event_sponsor/${this.sponsorId}/read`,
|
||||
}).then(function (readData) {
|
||||
self.sponsorData = readData;
|
||||
return Promise.resolve();
|
||||
async _fetchSponsor() {
|
||||
const sponsorData = await this._rpc({
|
||||
route: `/event_sponsor/${this.sponsorId}/read`
|
||||
});
|
||||
return rpcPromise;
|
||||
sponsorData.website_description = Markup(sponsorData.website_description);
|
||||
this.sponsorData = sponsorData;
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -67,9 +67,7 @@
|
||||
t-att-alt="widget.sponsorData.country_name"/>
|
||||
</div>
|
||||
</div>
|
||||
<span class="col-12" t-if="widget.sponsorData.website_description" t-raw="widget.sponsorData.website_description"/>
|
||||
</div>
|
||||
<div>
|
||||
<div class="col-12" t-if="widget.sponsorData.website_description" t-out="widget.sponsorData.website_description"/>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
odoo.define("website_sale.tour_shop", function (require) {
|
||||
"use strict";
|
||||
|
||||
var core = require("web.core");
|
||||
var _t = core._t;
|
||||
const {_t} = require("web.core");
|
||||
const {Markup} = require('web.utils');
|
||||
|
||||
// return the steps, used for backend and frontend
|
||||
|
||||
@@ -13,7 +13,7 @@ odoo.define("website_sale.tour_shop", function (require) {
|
||||
position: "bottom",
|
||||
}, {
|
||||
trigger: "a[data-action=new_product]",
|
||||
content: _t("Select <b>New Product</b> to create it and manage its properties to boost your sales."),
|
||||
content: Markup(_t("Select <b>New Product</b> to create it and manage its properties to boost your sales.")),
|
||||
position: "bottom",
|
||||
}, {
|
||||
trigger: ".modal-dialog #editor_new_product input[type=text]",
|
||||
@@ -21,7 +21,7 @@ odoo.define("website_sale.tour_shop", function (require) {
|
||||
position: "right",
|
||||
}, {
|
||||
trigger: ".modal-footer button.btn-primary.btn-continue",
|
||||
content: _t("Click on <em>Continue</em> to create the product."),
|
||||
content: Markup(_t("Click on <em>Continue</em> to create the product.")),
|
||||
position: "right",
|
||||
}, {
|
||||
trigger: ".product_price .oe_currency_value:visible",
|
||||
@@ -55,7 +55,7 @@ odoo.define("website_sale.tour_shop", function (require) {
|
||||
run: "drag_and_drop",
|
||||
}, {
|
||||
trigger: "button[data-action=save]",
|
||||
content: _t("Once you click on <b>Save</b>, your product is updated."),
|
||||
content: Markup(_t("Once you click on <b>Save</b>, your product is updated.")),
|
||||
position: "bottom",
|
||||
}, {
|
||||
trigger: ".js_publish_management .js_publish_btn .css_publish",
|
||||
|
||||
@@ -56,6 +56,7 @@ var options = require('web_editor.snippets.options');
|
||||
var publicWidget = require('web.public.widget');
|
||||
const Wysiwyg = require('web_editor.wysiwyg');
|
||||
const {qweb, _t} = require('web.core');
|
||||
const {Markup} = require('web.utils');
|
||||
|
||||
Wysiwyg.include({
|
||||
custom_events: Object.assign(Wysiwyg.prototype.custom_events, {
|
||||
@@ -79,7 +80,10 @@ Wysiwyg.include({
|
||||
fields: ['id', 'html', 'bg_color', 'text_color', 'html_class'],
|
||||
});
|
||||
}
|
||||
this.ribbons = Object.fromEntries(ribbons.map(ribbon => [ribbon.id, ribbon]));
|
||||
this.ribbons = Object.fromEntries(ribbons.map(ribbon => {
|
||||
ribbon.html = Markup(ribbon.html);
|
||||
return [ribbon.id, ribbon];
|
||||
}));
|
||||
this.originalRibbons = Object.assign({}, this.ribbons);
|
||||
this.productTemplatesRibbons = [];
|
||||
this.deletedRibbonClasses = '';
|
||||
|
||||
@@ -4,6 +4,7 @@ odoo.define('website_sale.video_field_preview', function (require) {
|
||||
|
||||
var AbstractField = require('web.AbstractField');
|
||||
var core = require('web.core');
|
||||
const {Markup} = require('web.utils');
|
||||
var fieldRegistry = require('web.field_registry');
|
||||
|
||||
var QWeb = core.qweb;
|
||||
@@ -16,7 +17,7 @@ var FieldVideoPreview = AbstractField.extend({
|
||||
|
||||
_render: function () {
|
||||
this.$el.html(QWeb.render('productVideo', {
|
||||
embedCode: this.value,
|
||||
embedCode: Markup(this.value),
|
||||
}));
|
||||
},
|
||||
});
|
||||
|
||||
@@ -5,6 +5,7 @@ const concurrency = require('web.concurrency');
|
||||
const publicWidget = require('web.public.widget');
|
||||
|
||||
const { qweb } = require('web.core');
|
||||
const { Markup } = require('web.utils');
|
||||
|
||||
/**
|
||||
* @todo maybe the custom autocomplete logic could be extract to be reusable
|
||||
@@ -56,8 +57,8 @@ publicWidget.registry.productsSearchBar = publicWidget.Widget.extend({
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_fetch: function () {
|
||||
return this._rpc({
|
||||
async _fetch() {
|
||||
const res = await this._rpc({
|
||||
route: '/shop/products/autocomplete',
|
||||
params: {
|
||||
'term': this.$input.val(),
|
||||
@@ -70,6 +71,10 @@ publicWidget.registry.productsSearchBar = publicWidget.Widget.extend({
|
||||
},
|
||||
},
|
||||
});
|
||||
if (this.displayDescription) {
|
||||
res.products.forEach(p => {p.description_sale = Markup(p.description_sale);});
|
||||
}
|
||||
return res;
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
<t t-name="productVideo">
|
||||
<div class="embed-responsive embed-responsive-16by9 mt-2" t-if="embedCode">
|
||||
<t t-raw="embedCode"/>
|
||||
<t t-out="embedCode"/>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
|
||||
@@ -16,18 +16,18 @@
|
||||
</t>
|
||||
<div class="media-body px-3">
|
||||
<t t-set="description" t-value="widget.displayDescription and product['description_sale']"/>
|
||||
<h6 t-attf-class="font-weight-bold #{description ? '' : 'mb-0'}" t-esc="product['name']"/>
|
||||
<p t-if="description" class="mb-0" t-raw="description"/>
|
||||
<h6 t-attf-class="font-weight-bold #{description ? '' : 'mb-0'}" t-out="product['name']"/>
|
||||
<p t-if="description" class="mb-0" t-out="description"/>
|
||||
</div>
|
||||
<div t-if="widget.displayPrice" class="flex-shrink-0">
|
||||
<t t-if="product['has_discounted_price']">
|
||||
<span class="text-danger text-nowrap" style="text-decoration: line-through;">
|
||||
<t t-raw="product['list_price']"/>
|
||||
<t t-out="product['list_price']"/>
|
||||
</span>
|
||||
<br/>
|
||||
</t>
|
||||
<b class="text-nowrap">
|
||||
<t t-raw="product['price']"/>
|
||||
<t t-out="product['price']"/>
|
||||
</b>
|
||||
</div>
|
||||
</div>
|
||||
@@ -39,7 +39,7 @@
|
||||
|
||||
<!-- Products Search Bar autocomplete item -->
|
||||
<we-button t-name="website_sale.ribbonSelectItem" t-att-data-set-ribbon="ribbon.id">
|
||||
<t t-raw="ribbon.html"/>
|
||||
<t t-out="ribbon.html"/>
|
||||
<span t-attf-class="fa fa-#{isTag ? 'tag' : 'bookmark'} ml-auto"></span>
|
||||
<span t-attf-class="fa fa-arrow-#{isLeft ? 'left' : 'right'} ml-1"></span>
|
||||
<span t-attf-class="o_wsale_color_preview #{colorClasses} ml-1" t-attf-style="background-color: #{ribbon.bg_color}"></span>
|
||||
|
||||
@@ -5,6 +5,7 @@ odoo.define('website_slides.quiz', function (require) {
|
||||
var Dialog = require('web.Dialog');
|
||||
var core = require('web.core');
|
||||
var session = require('web.session');
|
||||
const {Markup} = require('web.utils');
|
||||
|
||||
var CourseJoinWidget = require('website_slides.course.join.widget').courseJoinWidget;
|
||||
var QuestionFormWidget = require('website_slides.quiz.question.form');
|
||||
@@ -342,35 +343,39 @@ odoo.define('website_slides.quiz', function (require) {
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_submitQuiz: function () {
|
||||
var self = this;
|
||||
|
||||
return this._rpc({
|
||||
async _submitQuiz() {
|
||||
const data = await this._rpc({
|
||||
route: '/slides/slide/quiz/submit',
|
||||
params: {
|
||||
slide_id: self.slide.id,
|
||||
slide_id: this.slide.id,
|
||||
answer_ids: this._getQuizAnswers(),
|
||||
}
|
||||
}).then(function (data) {
|
||||
if (data.error) {
|
||||
self._alertShow(data.error);
|
||||
} else {
|
||||
self.quiz = _.extend(self.quiz, data);
|
||||
if (data.completed) {
|
||||
self._disableAnswers();
|
||||
new SlideQuizFinishModal(self, {
|
||||
quiz: self.quiz,
|
||||
hasNext: self.slide.hasNext,
|
||||
userId: self.userId
|
||||
}).open();
|
||||
self.slide.completed = true;
|
||||
self.trigger_up('slide_completed', {slide: self.slide, completion: data.channel_completion});
|
||||
}
|
||||
self._hideEditOptions();
|
||||
self._renderAnswersHighlightingAndComments();
|
||||
self._renderValidationInfo();
|
||||
}
|
||||
});
|
||||
if (data.error) {
|
||||
this._alertShow(data.error);
|
||||
return;
|
||||
}
|
||||
Object.assign(this.quiz, data);
|
||||
const {rankProgress, completed, channel_completion: completion} = this.quiz;
|
||||
// two of the rankProgress properties are HTML messages, mark if set
|
||||
if ('description' in rankProgress) {
|
||||
rankProgress['description'] = Markup(rankProgress['description'] || '');
|
||||
rankProgress['previous_rank']['motivational'] =
|
||||
Markup(rankProgress['previous_rank']['motivational'] || '');
|
||||
}
|
||||
if (completed) {
|
||||
this._disableAnswers();
|
||||
new SlideQuizFinishModal(this, {
|
||||
quiz: this.quiz,
|
||||
hasNext: this.slide.hasNext,
|
||||
userId: this.userId
|
||||
}).open();
|
||||
this.slide.completed = true;
|
||||
this.trigger_up('slide_completed', {slide: this.slide, completion});
|
||||
}
|
||||
this._hideEditOptions();
|
||||
this._renderAnswersHighlightingAndComments();
|
||||
this._renderValidationInfo();
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,116 +1,115 @@
|
||||
odoo.define('website_slides.slides_tour', function (require) {
|
||||
"use strict";
|
||||
|
||||
var core = require('web.core');
|
||||
var _t = core._t;
|
||||
|
||||
const {_t} = require('web.core');
|
||||
const {Markup} = require('web.utils');
|
||||
var tour = require('web_tour.tour');
|
||||
|
||||
tour.register('slides_tour', {
|
||||
url: '/slides',
|
||||
}, [{
|
||||
trigger: '#new-content-menu > a',
|
||||
content: _t("Welcome on your course's home page. It's still empty for now. Click on \"<b>New</b>\" to write your first course."),
|
||||
content: Markup(_t("Welcome on your course's home page. It's still empty for now. Click on \"<b>New</b>\" to write your first course.")),
|
||||
position: 'bottom',
|
||||
}, {
|
||||
trigger: 'a[data-action="new_slide_channel"]',
|
||||
content: _t("Select <b>Course</b> to create it and manage it."),
|
||||
content: Markup(_t("Select <b>Course</b> to create it and manage it.")),
|
||||
position: 'bottom',
|
||||
width: 210,
|
||||
}, {
|
||||
trigger: 'input[name="name"]',
|
||||
content: _t("Give your course an engaging <b>Title</b>."),
|
||||
content: Markup(_t("Give your course an engaging <b>Title</b>.")),
|
||||
position: 'bottom',
|
||||
width: 280,
|
||||
run: 'text My New Course',
|
||||
}, {
|
||||
trigger: 'textarea[name="description"]',
|
||||
content: _t("Give your course a helpful <b>Description</b>."),
|
||||
content: Markup(_t("Give your course a helpful <b>Description</b>.")),
|
||||
position: 'bottom',
|
||||
width: 300,
|
||||
run: 'text This course is for advanced users.',
|
||||
}, {
|
||||
trigger: 'button.btn-primary',
|
||||
content: _t("Click on the <b>Create</b> button to create your first course."),
|
||||
content: Markup(_t("Click on the <b>Create</b> button to create your first course.")),
|
||||
}, {
|
||||
trigger: '.o_wslides_js_slide_section_add',
|
||||
content: _t("Congratulations, your course has been created, but there isn't any content yet. First, let's add a <b>Section</b> to give your course a structure."),
|
||||
content: Markup(_t("Congratulations, your course has been created, but there isn't any content yet. First, let's add a <b>Section</b> to give your course a structure.")),
|
||||
position: 'bottom',
|
||||
}, {
|
||||
trigger: 'input[name="name"]',
|
||||
content: _t("A good course has structure and a table of content. Your first section will be the <b>Introduction</b>."),
|
||||
content: Markup(_t("A good course has structure and a table of content. Your first section will be the <b>Introduction</b>.")),
|
||||
position: 'bottom',
|
||||
}, {
|
||||
trigger: 'button.btn-primary',
|
||||
content: _t("Click on <b>Save</b> to apply changes."),
|
||||
content: Markup(_t("Click on <b>Save</b> to apply changes.")),
|
||||
position: 'bottom',
|
||||
width: 260,
|
||||
}, {
|
||||
trigger: 'a.btn-primary.o_wslides_js_slide_upload',
|
||||
content: _t("Your first section is created, now it's time to add lessons to your course. Click on <b>Add Content</b> to upload a document, create a web page or link a video."),
|
||||
content: Markup(_t("Your first section is created, now it's time to add lessons to your course. Click on <b>Add Content</b> to upload a document, create a web page or link a video.")),
|
||||
position: 'bottom',
|
||||
}, {
|
||||
trigger: 'a[data-slide-type="presentation"]',
|
||||
content: _t("First, let's add a <b>Presentation</b>. It can be a .pdf or an image."),
|
||||
content: Markup(_t("First, let's add a <b>Presentation</b>. It can be a .pdf or an image.")),
|
||||
position: 'bottom',
|
||||
}, {
|
||||
trigger: 'input#upload',
|
||||
content: _t("Choose a <b>File</b> on your computer."),
|
||||
content: Markup(_t("Choose a <b>File</b> on your computer.")),
|
||||
}, {
|
||||
trigger: 'input#name',
|
||||
content: _t("The <b>Title</b> of your lesson is autocompleted but you can change it if you want.</br>A <b>Preview</b> of your file is available on the right side of the screen."),
|
||||
content: Markup(_t("The <b>Title</b> of your lesson is autocompleted but you can change it if you want.</br>A <b>Preview</b> of your file is available on the right side of the screen.")),
|
||||
}, {
|
||||
trigger: 'input#duration',
|
||||
content: _t("The <b>Duration</b> of the lesson is based on the number of pages of your document. You can change this number if your attendees will need more time to assimilate the content."),
|
||||
content: Markup(_t("The <b>Duration</b> of the lesson is based on the number of pages of your document. You can change this number if your attendees will need more time to assimilate the content.")),
|
||||
}, {
|
||||
trigger: 'button.o_w_slide_upload_published',
|
||||
content: _t("<b>Save & Publish</b> your lesson to make it available to your attendees."),
|
||||
content: Markup(_t("<b>Save & Publish</b> your lesson to make it available to your attendees.")),
|
||||
position: 'bottom',
|
||||
width: 285,
|
||||
}, {
|
||||
trigger: 'span.badge-info:contains("New")',
|
||||
content: _t("Congratulations! Your first lesson is available. Let's see the options available here. The tag \"<b>New</b>\" indicates that this lesson was created less than 7 days ago."),
|
||||
content: Markup(_t("Congratulations! Your first lesson is available. Let's see the options available here. The tag \"<b>New</b>\" indicates that this lesson was created less than 7 days ago.")),
|
||||
position: 'bottom',
|
||||
}, {
|
||||
trigger: 'a[name="o_wslides_list_slide_add_quizz"]',
|
||||
extra_trigger: '.o_wslides_slides_list_slide:hover',
|
||||
content: _t("If you want to be sure that attendees have understood and memorized the content, you can add a Quiz on the lesson. Click on <b>Add Quiz</b>."),
|
||||
content: Markup(_t("If you want to be sure that attendees have understood and memorized the content, you can add a Quiz on the lesson. Click on <b>Add Quiz</b>.")),
|
||||
}, {
|
||||
trigger: 'input[name="question-name"]',
|
||||
content: _t("Enter your <b>Question</b>. Be clear and concise."),
|
||||
content: Markup(_t("Enter your <b>Question</b>. Be clear and concise.")),
|
||||
position: 'left',
|
||||
width: 330,
|
||||
}, {
|
||||
trigger: 'input.o_wslides_js_quiz_answer_value',
|
||||
content: _t("Enter at least two possible <b>Answers</b>."),
|
||||
content: Markup(_t("Enter at least two possible <b>Answers</b>.")),
|
||||
position: 'left',
|
||||
width: 290,
|
||||
}, {
|
||||
trigger: 'a.o_wslides_js_quiz_is_correct',
|
||||
content: _t("Mark the correct answer by checking the <b>correct</b> mark."),
|
||||
content: Markup(_t("Mark the correct answer by checking the <b>correct</b> mark.")),
|
||||
position: 'right',
|
||||
width: 230,
|
||||
}, {
|
||||
trigger: 'i.o_wslides_js_quiz_comment_answer:last',
|
||||
content: _t("You can add <b>comments</b> on answers. This will be visible with the results if the user select this answer."),
|
||||
content: Markup(_t("You can add <b>comments</b> on answers. This will be visible with the results if the user select this answer.")),
|
||||
position: 'right',
|
||||
|
||||
}, {
|
||||
trigger: 'a.o_wslides_js_quiz_validate_question',
|
||||
content: _t("<b>Save</b> your question."),
|
||||
content: Markup(_t("<b>Save</b> your question.")),
|
||||
position: 'left',
|
||||
width: 170,
|
||||
}, {
|
||||
trigger: 'li.breadcrumb-item:nth-child(2)',
|
||||
content: _t("Click on your <b>Course</b> to go back to the table of content."),
|
||||
content: Markup(_t("Click on your <b>Course</b> to go back to the table of content.")),
|
||||
position: 'top',
|
||||
}, {
|
||||
trigger: 'label.js_publish_btn',
|
||||
content: _t("Once you're done, don't forget to <b>Publish</b> your course."),
|
||||
content: Markup(_t("Once you're done, don't forget to <b>Publish</b> your course.")),
|
||||
position: 'bottom',
|
||||
}, {
|
||||
trigger: 'a.o_wslides_js_slides_list_slide_link',
|
||||
content: _t("Congratulations, you've created your first course.<br/>Click on the title of this content to see it in fullscreen mode."),
|
||||
content: Markup(_t("Congratulations, you've created your first course.<br/>Click on the title of this content to see it in fullscreen mode.")),
|
||||
position: 'bottom',
|
||||
}]);
|
||||
|
||||
|
||||
@@ -105,6 +105,9 @@ tour.register('course_member', {
|
||||
trigger: '.o_wslides_js_lesson_quiz_question:last .list-group a:first'
|
||||
}, {
|
||||
trigger: '.o_wslides_js_lesson_quiz_submit'
|
||||
}, {
|
||||
// check that we have a properly motivational message to motivate us!
|
||||
trigger: '.o_wslides_quiz_modal_rank_motivational > div > div:contains("Reach the next rank and gain a very nice mug !")'
|
||||
}, {
|
||||
trigger: 'a:contains("End course")'
|
||||
},
|
||||
|
||||
@@ -144,8 +144,12 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="pb-3 o_wslides_quiz_modal_rank_motivational">
|
||||
<t t-set="showLastRankDescription" t-value="widget.quiz.rankProgress.last_rank && !widget.quiz.rankProgress.level_up" />
|
||||
<t t-raw="showLastRankDescription ? widget.quiz.rankProgress.description : widget.quiz.rankProgress.previous_rank.motivational" />
|
||||
<t t-if="widget.quiz.rankProgress.last_rank and !widget.quiz.rankProgress.level_up">
|
||||
<t t-out="widget.quiz.rankProgress.description"/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<t t-out="widget.quiz.rankProgress.previous_rank.motivational"/>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
<div class="o_wslides_quiz_modal_dismiss align-self-end d-none">
|
||||
|
||||
@@ -102,7 +102,8 @@ class TestUi(TestUICommon):
|
||||
user_demo = self.user_demo
|
||||
user_demo.flush()
|
||||
user_demo.write({
|
||||
'groups_id': [(5, 0), (4, self.env.ref('base.group_user').id)]
|
||||
'karma': 1,
|
||||
'groups_id': [(6, 0, self.env.ref('base.group_user').ids)]
|
||||
})
|
||||
|
||||
self.browser_js(
|
||||
@@ -115,7 +116,8 @@ class TestUi(TestUICommon):
|
||||
user_demo = self.user_demo
|
||||
user_demo.flush()
|
||||
user_demo.write({
|
||||
'groups_id': [(5, 0), (4, self.env.ref('base.group_user').id), (4, self.env.ref('website_slides.group_website_slides_officer').id)]
|
||||
'karma': 1,
|
||||
'groups_id': [(6, 0, (self.env.ref('base.group_user') | self.env.ref('website_slides.group_website_slides_officer')).ids)]
|
||||
})
|
||||
|
||||
self.browser_js(
|
||||
@@ -127,6 +129,7 @@ class TestUi(TestUICommon):
|
||||
def test_course_member_portal(self):
|
||||
user_portal = self.user_portal
|
||||
user_portal.flush()
|
||||
user_portal.karma = 1
|
||||
|
||||
self.browser_js(
|
||||
'/slides',
|
||||
|
||||
@@ -2,6 +2,7 @@ odoo.define('website_twitter.animation', function (require) {
|
||||
'use strict';
|
||||
|
||||
var core = require('web.core');
|
||||
const {Markup} = require('web.utils');
|
||||
var publicWidget = require('web.public.widget');
|
||||
|
||||
var qweb = core.qweb;
|
||||
@@ -47,7 +48,7 @@ publicWidget.registry.twitter = publicWidget.Widget.extend({
|
||||
}
|
||||
|
||||
// Parse tweet text
|
||||
tweet.text = tweet.text
|
||||
tweet.text = Markup(_.escape(tweet.text)
|
||||
.replace(
|
||||
/[A-Za-z]+:\/\/[A-Za-z0-9-_]+\.[A-Za-z0-9-_:%&~\?\/.=]+/g,
|
||||
function (url) {
|
||||
@@ -65,18 +66,12 @@ publicWidget.registry.twitter = publicWidget.Widget.extend({
|
||||
function (hashtag) {
|
||||
return _makeLink('http://twitter.com/search?q='+hashtag.replace('#',''), hashtag);
|
||||
}
|
||||
);
|
||||
));
|
||||
|
||||
return qweb.render('website.Twitter.Tweet', {tweet: tweet});
|
||||
|
||||
function _makeLink(url, text) {
|
||||
var c = $('<a/>', {
|
||||
text: text,
|
||||
href: url,
|
||||
target: '_blank',
|
||||
rel: 'noreferrer noopener',
|
||||
});
|
||||
return c.prop('outerHTML');
|
||||
return Markup`<a href="${url}" target="_blank" rel="noreferrer noopener">${text}</a>`;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<a class="date" target="_blank" t-attf-href="http://twitter.com/#{tweet.user.screen_name}/status/#{tweet.id_str}"><t t-esc="tweet.created_at"/></a>
|
||||
</div>
|
||||
<div class="bottom">
|
||||
<p><t t-raw="tweet.text"/></p>
|
||||
<p><t t-out="tweet.text"/></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,13 +2,14 @@ odoo.define('test_main_flows.tour', function (require) {
|
||||
"use strict";
|
||||
|
||||
const {_t} = require('web.core');
|
||||
const {Markup} = require('web.utils');
|
||||
const tour = require('web_tour.tour');
|
||||
|
||||
tour.register('main_flow_tour', {
|
||||
test: true,
|
||||
url: "/web",
|
||||
}, [
|
||||
...tour.stepUtils.goToAppSteps('sale.sale_menu_root', _t('Organize your sales activities with the <b>Sales app</b>.')),
|
||||
...tour.stepUtils.goToAppSteps('sale.sale_menu_root', Markup(_t('Organize your sales activities with the <b>Sales app</b>.'))),
|
||||
tour.stepUtils.openBuggerMenu("li.breadcrumb-item.active:contains('Quotations')"),
|
||||
{
|
||||
// Add Stockable product
|
||||
@@ -335,11 +336,11 @@ tour.stepUtils.autoExpandMoreButtons('.o_form_readonly'),
|
||||
}, {
|
||||
mobile: false,
|
||||
trigger: ".breadcrumb-item:first",
|
||||
content: _t("Use the breadcrumbs to <b>go back to products</b>."),
|
||||
content: Markup(_t("Use the breadcrumbs to <b>go back to products</b>.")),
|
||||
position: "bottom"
|
||||
},
|
||||
...tour.stepUtils.goBackBreadcrumbsMobile(
|
||||
_t("Use the breadcrumbs to <b>go back to products</b>."),
|
||||
Markup(_t("Use the breadcrumbs to <b>go back to products</b>.")),
|
||||
undefined,
|
||||
".breadcrumb-item.active:contains('Bill of Materials')",
|
||||
".breadcrumb-item.active:contains('the_flow.product')"
|
||||
@@ -432,15 +433,15 @@ tour.stepUtils.autoExpandMoreButtons('.o_form_readonly'),
|
||||
content: _t('Go back to the home menu'),
|
||||
position: 'bottom',
|
||||
},
|
||||
...tour.stepUtils.goToAppSteps('crm.crm_menu_root', _t('Organize your sales activities with the <b>CRM app</b>.')),
|
||||
...tour.stepUtils.goToAppSteps('crm.crm_menu_root', Markup(_t('Organize your sales activities with the <b>CRM app</b>.'))),
|
||||
{
|
||||
trigger: ".o-kanban-button-new",
|
||||
extra_trigger: '.o_opportunity_kanban',
|
||||
content: _t("Click here to <b>create your first opportunity</b> and add it to your pipeline."),
|
||||
content: Markup(_t("Click here to <b>create your first opportunity</b> and add it to your pipeline.")),
|
||||
position: "bottom"
|
||||
}, {
|
||||
trigger: ".o_kanban_quick_create input[name='name']",
|
||||
content: _t("<b>Choose a name</b> for your opportunity."),
|
||||
content: Markup(_t("<b>Choose a name</b> for your opportunity.")),
|
||||
position: "right",
|
||||
run: "text the_flow.opportunity",
|
||||
}, {
|
||||
@@ -480,12 +481,12 @@ tour.stepUtils.autoExpandMoreButtons('.o_form_readonly'),
|
||||
}, {
|
||||
trigger: ".o_kanban_quick_create .o_kanban_add",
|
||||
extra_trigger: ".o_kanban_quick_create .o_field_widget[name=partner_id] .o_external_button", // Wait name_create
|
||||
content: _t("Click here to <b>add your opportunity</b>."),
|
||||
content: Markup(_t("Click here to <b>add your opportunity</b>.")),
|
||||
position: "right",
|
||||
}, {
|
||||
mobile: false,
|
||||
trigger: ".o_kanban_group:first .o_kanban_record:has(span:contains('the_flow.opportunity'))",
|
||||
content: _t("<b>Drag & drop opportunities</b> between columns as you progress in your sales cycle."),
|
||||
content: Markup(_t("<b>Drag & drop opportunities</b> between columns as you progress in your sales cycle.")),
|
||||
position: "right",
|
||||
run: "drag_and_drop .o_opportunity_kanban .o_kanban_group:eq(2) ",
|
||||
}, {
|
||||
@@ -510,7 +511,7 @@ tour.stepUtils.autoExpandMoreButtons('.o_form_readonly'),
|
||||
position: "bottom",
|
||||
},
|
||||
// Create a quotation
|
||||
...tour.stepUtils.statusbarButtonsSteps('New Quotation', _t('<p><b>Create a quotation</p>')),
|
||||
...tour.stepUtils.statusbarButtonsSteps('New Quotation', Markup(_t('<p><b>Create a quotation</p>'))),
|
||||
{
|
||||
mobile: false,
|
||||
trigger: ".o_field_widget[name=order_line] .o_field_x2many_list_row_add > a",
|
||||
@@ -637,11 +638,11 @@ tour.stepUtils.autoExpandMoreButtons('.o_form_readonly'),
|
||||
content: _t("Try to send it to email"),
|
||||
position: "bottom",
|
||||
},
|
||||
...tour.stepUtils.statusbarButtonsSteps('Confirm', _t("<p>Confirm this quotation</p>")),
|
||||
...tour.stepUtils.statusbarButtonsSteps('Confirm', Markup(_t("<p>Confirm this quotation</p>"))),
|
||||
{
|
||||
trigger: ".o_form_button_save",
|
||||
extra_trigger: ".o_statusbar_status .btn-primary:contains('Sales Order')",
|
||||
content: _t("<p>Save this quotation</p>"),
|
||||
content: Markup(_t("<p>Save this quotation</p>")),
|
||||
position: "bottom"
|
||||
// Reordering rule
|
||||
}, {
|
||||
@@ -719,12 +720,12 @@ tour.stepUtils.openBuggerMenu("li.breadcrumb-item.active:contains('Inventory Ove
|
||||
}, {
|
||||
mobile: false,
|
||||
trigger: ".o_list_button_save",
|
||||
content: _t("<p>Save this reordering rule</p>"),
|
||||
content: Markup(_t("<p>Save this reordering rule</p>")),
|
||||
position: "bottom"
|
||||
}, {
|
||||
mobile: true,
|
||||
trigger: ".o_form_button_save",
|
||||
content: _t("<p>Save this reordering rule</p>"),
|
||||
content: Markup(_t("<p>Save this reordering rule</p>")),
|
||||
position: "bottom"
|
||||
},
|
||||
tour.stepUtils.openBuggerMenu("li.breadcrumb-item.active:contains('OP/')"),
|
||||
@@ -848,7 +849,7 @@ tour.stepUtils.openBuggerMenu("li.breadcrumb-item.active:contains('Manufacturing
|
||||
content: _t('Go back to the home menu'),
|
||||
position: 'bottom',
|
||||
},
|
||||
...tour.stepUtils.goToAppSteps('sale.sale_menu_root', _t('Organize your sales activities with the <b>Sales app</b>.')),
|
||||
...tour.stepUtils.goToAppSteps('sale.sale_menu_root', Markup(_t('Organize your sales activities with the <b>Sales app</b>.'))),
|
||||
tour.stepUtils.openBuggerMenu("li.breadcrumb-item.active:contains('Quotations')"),
|
||||
{
|
||||
trigger: ".o_menu_sections button[data-menu-xmlid='sale.sale_order_menu']",
|
||||
@@ -1031,12 +1032,12 @@ tour.stepUtils.mobileModifier(tour.stepUtils.autoExpandMoreButtons('.o_control_p
|
||||
{
|
||||
edition: "enterprise",
|
||||
trigger: "button.o_reconcile, button.o_validate",
|
||||
content: _t('<p><b>Click on Reconcile</p>'),
|
||||
content: Markup(_t('<p><b>Click on Reconcile</p>')),
|
||||
position: "right",
|
||||
}, {
|
||||
edition: "enterprise",
|
||||
trigger: ".button_back_to_statement",
|
||||
content: _t('<p><b>Close this statement.</p>'),
|
||||
content: Markup(_t('<p><b>Close this statement.</p>')),
|
||||
position: "bottom",
|
||||
}]);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user