[REF] *: adapt code to new owl webclient
This commit adapts the community codebase to the rewriting of the /web application in owl. Co-authored-by: Aaron Bohy <aab@odoo.com> Co-authored-by: Bruno Boi <boi@odoo.com> Co-authored-by: Géry Debongnie <ged@odoo.com> Co-authored-by: Samuel Degueldre <sad@odoo.com> Co-authored-by: Mathieu Duckerts-Antoine <dam@odoo.com> Co-authored-by: Simon Genin (ges) <ges@odoo.com> Co-authored-by: Francois (fge) <fge@odoo.com> Co-authored-by: Michael Mattiello (mcm) <mcm@odoo.com> Co-authored-by: Julien Mougenot <jum@odoo.com> Co-authored-by: Lucas Perais (lpe) <lpe@odoo.com> Co-authored-by: Jorge Pinna Puissant <jpp@odoo.com>
This commit is contained in:
co-authored by
Bruno Boi
Géry Debongnie
Samuel Degueldre
Mathieu Duckerts-Antoine
Simon Genin
Francois
Michael Mattiello
Julien Mougenot
Lucas Perais
Jorge Pinna Puissant
parent
2587a16835
commit
14bffd983e
@@ -115,8 +115,8 @@ You could use this simplified accounting in case you work with an (external) acc
|
||||
'account/static/tests/tours/**/*',
|
||||
],
|
||||
'web.qunit_suite_tests': [
|
||||
('after', 'web/static/tests/views/kanban_tests.js', 'account/static/tests/account_payment_field_tests.js'),
|
||||
('after', 'web/static/tests/views/kanban_tests.js', 'account/static/tests/section_and_note_tests.js'),
|
||||
('after', 'web/static/tests/legacy/views/kanban_tests.js', 'account/static/tests/account_payment_field_tests.js'),
|
||||
('after', 'web/static/tests/legacy/views/kanban_tests.js', 'account/static/tests/section_and_note_tests.js'),
|
||||
],
|
||||
'web.assets_qweb': [
|
||||
'account/static/src/xml/**/*',
|
||||
|
||||
@@ -6,8 +6,6 @@ var config = require('web.config');
|
||||
var core = require('web.core');
|
||||
var session = require('web.session');
|
||||
var time = require('web.time');
|
||||
var AbstractWebClient = require('web.AbstractWebClient');
|
||||
var Loading = require('web.Loading');
|
||||
|
||||
var QWeb = core.qweb;
|
||||
var _t = core._t;
|
||||
@@ -800,9 +798,6 @@ var DataImport = AbstractAction.extend({
|
||||
{tracking_disable: tracking_disable}
|
||||
);
|
||||
var self = this;
|
||||
this.trigger_up('with_client', {callback: function () {
|
||||
this.loading.ignore_events = true;
|
||||
}});
|
||||
$.blockUI({message: QWeb.render('Throbber')});
|
||||
$(document.body).addClass('o_ui_blocked');
|
||||
var opts = this.import_options();
|
||||
@@ -851,9 +846,6 @@ var DataImport = AbstractAction.extend({
|
||||
}).finally(function () {
|
||||
$(document.body).removeClass('o_ui_blocked');
|
||||
$.unblockUI();
|
||||
self.trigger_up('with_client', {callback: function () {
|
||||
delete this.loading.ignore_events;
|
||||
}});
|
||||
});
|
||||
}, /**
|
||||
*
|
||||
@@ -877,6 +869,8 @@ var DataImport = AbstractAction.extend({
|
||||
method: 'execute_import',
|
||||
args: args.concat([opts]),
|
||||
kwargs: kwargs
|
||||
}, {
|
||||
shadow: true,
|
||||
}).then(function (results) {
|
||||
_.each(results.messages, offset_by(opts.skip));
|
||||
if (!kwargs.dryrun && !results.ids) {
|
||||
@@ -1172,18 +1166,6 @@ StateMachine.create({
|
||||
],
|
||||
});
|
||||
|
||||
Loading.include({
|
||||
on_rpc_event: function () {
|
||||
if (this.ignore_events) {
|
||||
return
|
||||
}
|
||||
this._super.apply(this, arguments);
|
||||
}
|
||||
});
|
||||
AbstractWebClient.prototype.custom_events['with_client'] = function (ev) {
|
||||
ev.data.callback.call(this);
|
||||
};
|
||||
|
||||
function offset_by(by) {
|
||||
return function offset_message(msg) {
|
||||
if (msg.rows) {
|
||||
|
||||
@@ -19,10 +19,7 @@
|
||||
'bus/static/src/js/services/bus_service.js',
|
||||
],
|
||||
'web.qunit_suite_tests': [
|
||||
'bus/static/tests/bus_tests.js',
|
||||
],
|
||||
'web.assets_tests': [
|
||||
'bus/static/tests/bus_tests_tour.js',
|
||||
'bus/static/tests/*.js',
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
export const assetsWatchdogService = {
|
||||
dependencies: ["notification"],
|
||||
|
||||
start(env, { notification }) {
|
||||
const assets = {};
|
||||
let assetsChangedNotificationId = null;
|
||||
let bundleNotifTimerID = null;
|
||||
|
||||
env.bus.on("WEB_CLIENT_READY", null, async () => {
|
||||
const legacyEnv = owl.Component.env;
|
||||
|
||||
document.querySelectorAll("*[data-asset-bundle]").forEach((el) => {
|
||||
assets[el.getAttribute("data-asset-bundle")] = el.getAttribute(
|
||||
"data-asset-version"
|
||||
);
|
||||
});
|
||||
|
||||
legacyEnv.services.bus_service.onNotification(this, onNotification);
|
||||
legacyEnv.services.bus_service.addChannel("bundle_changed");
|
||||
});
|
||||
|
||||
/**
|
||||
* Displays one notification on user's screen when assets have changed
|
||||
*/
|
||||
function displayBundleChangedNotification() {
|
||||
if (!assetsChangedNotificationId) {
|
||||
// Wrap the notification inside a delay.
|
||||
// The server may be overwhelmed with recomputing assets
|
||||
// We wait until things settle down
|
||||
browser.clearTimeout(bundleNotifTimerID);
|
||||
bundleNotifTimerID = browser.setTimeout(() => {
|
||||
assetsChangedNotificationId = notification.create(
|
||||
env._t("The page appears to be out of date."),
|
||||
{
|
||||
title: env._t("Refresh"),
|
||||
type: "warning",
|
||||
sticky: true,
|
||||
buttons: [
|
||||
{
|
||||
name: env._t("Refresh"),
|
||||
primary: true,
|
||||
onClick: () => {
|
||||
browser.location.reload();
|
||||
},
|
||||
},
|
||||
],
|
||||
onClose: () => {
|
||||
assetsChangedNotificationId = null;
|
||||
},
|
||||
}
|
||||
);
|
||||
}, getBundleNotificationDelay());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes a random delay to avoid hammering the server
|
||||
* when bundles change with all the users reloading
|
||||
* at the same time
|
||||
*
|
||||
* @return {number} delay in milliseconds
|
||||
*/
|
||||
function getBundleNotificationDelay() {
|
||||
return 10000 + Math.floor(Math.random() * 50) * 1000;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reacts to bus's notification
|
||||
*
|
||||
* @param {Array} notifications: list of received notifications
|
||||
*/
|
||||
function onNotification(notifications) {
|
||||
for (const notif of notifications) {
|
||||
if (notif[0][1] === "bundle_changed") {
|
||||
const bundleXmlId = notif[1][0];
|
||||
const bundleVersion = notif[1][1];
|
||||
if (bundleXmlId in assets && bundleVersion !== assets[bundleXmlId]) {
|
||||
displayBundleChangedNotification();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
registry.category("services").add("assetsWatchdog", assetsWatchdogService);
|
||||
@@ -1,109 +0,0 @@
|
||||
odoo.define('bus.WebClient', function (require) {
|
||||
"use strict";
|
||||
|
||||
const core = require('web.core');
|
||||
const WebClient = require('web.WebClient');
|
||||
|
||||
const _t = core._t;
|
||||
|
||||
WebClient.include({
|
||||
|
||||
//----------------------------------------------------------------------
|
||||
// Public
|
||||
//----------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Detects the presence of assets in DOM's HEAD
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
async start() {
|
||||
this._assetsChangedNotificationId = null;
|
||||
this._assets = {};
|
||||
await this._super(...arguments);
|
||||
},
|
||||
/**
|
||||
* Assigns handler to bus notification
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
show_application() {
|
||||
const shown = this._super(...arguments);
|
||||
document.querySelectorAll('*[data-asset-bundle]').forEach(el => {
|
||||
this._assets[el.getAttribute('data-asset-bundle')] = el.getAttribute('data-asset-version');
|
||||
});
|
||||
this.call('bus_service', 'onNotification', this, this._onNotification);
|
||||
this.call('bus_service', 'addChannel', 'bundle_changed');
|
||||
return shown;
|
||||
},
|
||||
|
||||
//----------------------------------------------------------------------
|
||||
// Private
|
||||
//----------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Displays one notification on user's screen when assets have changed
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_displayBundleChangedNotification() {
|
||||
if (!this._assetsChangedNotificationId) {
|
||||
// Wrap the notification inside a delay.
|
||||
// The server may be overwhelmed with recomputing assets
|
||||
// We wait until things settle down
|
||||
clearTimeout(this._bundleNotifTimerID);
|
||||
this._bundleNotifTimerID = setTimeout(() => {
|
||||
this._assetsChangedNotificationId = this.call('notification', 'notify', {
|
||||
title: _t('Refresh'),
|
||||
message: _t('The page appears to be out of date.'),
|
||||
sticky: true,
|
||||
onClose: () => {
|
||||
this._assetsChangedNotificationId = null;
|
||||
},
|
||||
buttons: [{
|
||||
text: _t('Refresh'),
|
||||
primary: true,
|
||||
click: () => {
|
||||
window.location.reload(true);
|
||||
}
|
||||
}],
|
||||
});
|
||||
}, this._getBundleNotificationDelay());
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Computes a random delay to avoid hammering the server
|
||||
* when bundles change with all the users reloading
|
||||
* at the same time
|
||||
*
|
||||
* @private
|
||||
* @return {number} delay in milliseconds
|
||||
*/
|
||||
_getBundleNotificationDelay() {
|
||||
return 10000 + Math.floor(Math.random()*50) * 1000;
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Reacts to bus's notification
|
||||
*
|
||||
* @private
|
||||
* @param {Array} notifications: list of received notifications
|
||||
*/
|
||||
_onNotification(notifications) {
|
||||
for (const notif of notifications) {
|
||||
if (notif[0][1] === 'bundle_changed') {
|
||||
const bundleXmlId = notif[1][0];
|
||||
const bundleVersion = notif[1][1];
|
||||
if (bundleXmlId in this._assets && bundleVersion !== this._assets[bundleXmlId]) {
|
||||
this._displayBundleChangedNotification();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,86 @@
|
||||
/** @odoo-module */
|
||||
|
||||
import * as legacyRegistry from "web.Registry";
|
||||
import * as BusService from "bus.BusService";
|
||||
import * as RamStorage from "web.RamStorage";
|
||||
import * as AbstractStorageService from "web.AbstractStorageService";
|
||||
|
||||
import {
|
||||
createWebClient,
|
||||
getActionManagerTestConfig,
|
||||
} from "@web/../tests/webclient/actions/helpers";
|
||||
import { assetsWatchdogService } from "@bus/js/services/assets_watchdog_service";
|
||||
import { click, nextTick, patchWithCleanup } from "@web/../tests/helpers/utils";
|
||||
import { NotificationContainer } from "@web/core/notifications/notification_container";
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { clearRegistryWithCleanup } from "@web/../tests/helpers/mock_env";
|
||||
|
||||
const LocalStorageService = AbstractStorageService.extend({
|
||||
storage: new RamStorage(),
|
||||
});
|
||||
const mainComponentRegistry = registry.category("main_components");
|
||||
const serviceRegistry = registry.category("services");
|
||||
|
||||
QUnit.module("Bus Assets WatchDog", (hooks) => {
|
||||
let legacyServicesRegistry;
|
||||
let testConfig;
|
||||
hooks.beforeEach((assert) => {
|
||||
legacyServicesRegistry = new legacyRegistry();
|
||||
legacyServicesRegistry.add("bus_service", BusService);
|
||||
legacyServicesRegistry.add("local_storage", LocalStorageService);
|
||||
|
||||
testConfig = getActionManagerTestConfig();
|
||||
serviceRegistry.add("assetsWatchdog", assetsWatchdogService);
|
||||
|
||||
clearRegistryWithCleanup(mainComponentRegistry);
|
||||
mainComponentRegistry.add("NotificationContainer", NotificationContainer);
|
||||
patchWithCleanup(browser, {
|
||||
setTimeout: (fn) => fn(),
|
||||
clearTimeout: () => {},
|
||||
location: {
|
||||
reload: () => assert.step("reloadPage"),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test("can listen on bus and displays notifications in DOM", async (assert) => {
|
||||
assert.expect(5);
|
||||
|
||||
let pollNumber = 0;
|
||||
const mockRPC = (route, args) => {
|
||||
if (route === "/longpolling/poll") {
|
||||
if (pollNumber > 0) {
|
||||
return new Promise(() => {}); // let it hang to avoid further calls
|
||||
}
|
||||
pollNumber++;
|
||||
assert.deepEqual(args.channels, ["bundle_changed"]);
|
||||
return Promise.resolve([
|
||||
{
|
||||
id: "prout",
|
||||
channel: ["db_name", "bundle_changed"],
|
||||
message: ["web.assets_backend", "newHash"],
|
||||
},
|
||||
]);
|
||||
}
|
||||
};
|
||||
|
||||
const webClient = await createWebClient({
|
||||
testConfig,
|
||||
legacyParams: { serviceRegistry: legacyServicesRegistry },
|
||||
mockRPC,
|
||||
});
|
||||
|
||||
await nextTick();
|
||||
|
||||
assert.containsOnce(webClient.el, ".o_notification_body");
|
||||
assert.strictEqual(
|
||||
webClient.el.querySelector(".o_notification_body .o_notification_content").textContent,
|
||||
"The page appears to be out of date."
|
||||
);
|
||||
|
||||
// reload by clicking on the reload button
|
||||
await click(webClient.el, ".o_notification_buttons .btn-primary");
|
||||
assert.verifySteps(["reloadPage"]);
|
||||
});
|
||||
});
|
||||
@@ -1,25 +0,0 @@
|
||||
odoo.define("bus.tour", function (require) {
|
||||
"use strict";
|
||||
|
||||
const tour = require("web_tour.tour");
|
||||
|
||||
tour.register("bundle_changed_notification", {
|
||||
test: true,
|
||||
url: '/web',
|
||||
}, [{
|
||||
trigger: '.o_web_client',
|
||||
run() {
|
||||
const webClient = odoo.__DEBUG__.services['web.web_client'];
|
||||
const _delayFn = webClient._getBundleNotificationDelay;
|
||||
webClient._getBundleNotificationDelay = () => 0;
|
||||
this.call('bus_service', 'trigger',
|
||||
'notification',
|
||||
[[['db_name', 'bundle_changed'], ['web.assets_backend', 'hash']]]
|
||||
);
|
||||
webClient._getBundleNotificationDelay = _delayFn;
|
||||
}
|
||||
}, {
|
||||
trigger: '.o_notification_title:contains(Refresh)',
|
||||
}]
|
||||
);
|
||||
});
|
||||
@@ -36,7 +36,8 @@ class BusWebTests(odoo.tests.HttpCase):
|
||||
|
||||
self.patch(type(self.env['bus.bus']), 'sendone', patched_sendone)
|
||||
|
||||
self.start_tour('/web', "bundle_changed_notification", login='admin', timeout=180)
|
||||
self.authenticate('admin', 'admin')
|
||||
self.url_open('/web')
|
||||
|
||||
# One sendone for each asset bundle and for each CSS / JS
|
||||
self.assertEqual(
|
||||
|
||||
@@ -47,6 +47,7 @@ If you need to manage your meetings, you should install the CRM module.
|
||||
'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',
|
||||
],
|
||||
'web.qunit_suite_tests': [
|
||||
'calendar/static/tests/**/*',
|
||||
|
||||
@@ -3,116 +3,10 @@ odoo.define('base_calendar.base_calendar', function (require) {
|
||||
|
||||
var BasicModel = require('web.BasicModel');
|
||||
var fieldRegistry = require('web.field_registry');
|
||||
var Notification = require('web.Notification');
|
||||
var relationalFields = require('web.relational_fields');
|
||||
var session = require('web.session');
|
||||
var WebClient = require('web.WebClient');
|
||||
|
||||
const FieldMany2ManyTagsAvatar = relationalFields.FieldMany2ManyTagsAvatar;
|
||||
|
||||
var CalendarNotification = Notification.extend({
|
||||
template: "CalendarNotification",
|
||||
xmlDependencies: (Notification.prototype.xmlDependencies || [])
|
||||
.concat(['/calendar/static/src/xml/notification_calendar.xml']),
|
||||
|
||||
init: function(parent, params) {
|
||||
this._super(parent, params);
|
||||
this.eid = params.eventID;
|
||||
this.sticky = true;
|
||||
|
||||
this.events = _.extend(this.events || {}, {
|
||||
'click .link2event': function() {
|
||||
var self = this;
|
||||
|
||||
this._rpc({
|
||||
route: '/web/action/load',
|
||||
params: {
|
||||
action_id: 'calendar.action_calendar_event_notify',
|
||||
},
|
||||
})
|
||||
.then(function(r) {
|
||||
r.res_id = self.eid;
|
||||
return self.do_action(r);
|
||||
});
|
||||
},
|
||||
|
||||
'click .link2recall': function() {
|
||||
this.close();
|
||||
},
|
||||
|
||||
'click .link2showed': function() {
|
||||
this._rpc({route: '/calendar/notify_ack'})
|
||||
.then(this.close.bind(this, false), this.close.bind(this, false));
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
WebClient.include({
|
||||
display_calendar_notif: function(notifications) {
|
||||
var self = this;
|
||||
var last_notif_timer = 0;
|
||||
|
||||
// Clear previously set timeouts and destroy currently displayed calendar notifications
|
||||
clearTimeout(this.get_next_calendar_notif_timeout);
|
||||
_.each(this.calendar_notif_timeouts, clearTimeout);
|
||||
this.calendar_notif_timeouts = {};
|
||||
|
||||
// For each notification, set a timeout to display it
|
||||
_.each(notifications, function(notif) {
|
||||
var key = notif.event_id + ',' + notif.alarm_id;
|
||||
if (key in self.calendar_notif) {
|
||||
return;
|
||||
}
|
||||
self.calendar_notif_timeouts[key] = setTimeout(function () {
|
||||
var notificationID = self.call('notification', 'notify', {
|
||||
Notification: CalendarNotification,
|
||||
title: notif.title,
|
||||
message: notif.message,
|
||||
eventID: notif.event_id,
|
||||
onClose: function () {
|
||||
delete self.calendar_notif[key];
|
||||
},
|
||||
});
|
||||
self.calendar_notif[key] = notificationID;
|
||||
}, notif.timer * 1000);
|
||||
last_notif_timer = Math.max(last_notif_timer, notif.timer);
|
||||
});
|
||||
|
||||
// Set a timeout to get the next notifications when the last one has been displayed
|
||||
if (last_notif_timer > 0) {
|
||||
this.get_next_calendar_notif_timeout = setTimeout(this.get_next_calendar_notif.bind(this), last_notif_timer * 1000);
|
||||
}
|
||||
},
|
||||
get_next_calendar_notif: function() {
|
||||
session.rpc("/calendar/notify", {}, {shadow: true})
|
||||
.then(this.display_calendar_notif.bind(this))
|
||||
.guardedCatch(function(reason) { //
|
||||
var err = reason.message;
|
||||
var ev = reason.event;
|
||||
if(err.code === -32098) {
|
||||
// Prevent the CrashManager to display an error
|
||||
// in case of an xhr error not due to a server error
|
||||
ev.preventDefault();
|
||||
}
|
||||
});
|
||||
},
|
||||
show_application: function() {
|
||||
// An event is triggered on the bus each time a calendar event with alarm
|
||||
// in which the current user is involved is created, edited or deleted
|
||||
this.calendar_notif_timeouts = {};
|
||||
this.calendar_notif = {};
|
||||
this.call('bus_service', 'onNotification', this, function (notifications) {
|
||||
_.each(notifications, (function (notification) {
|
||||
if (notification[0][1] === 'calendar.alarm') {
|
||||
this.display_calendar_notif(notification[1]);
|
||||
}
|
||||
}).bind(this));
|
||||
});
|
||||
return this._super.apply(this, arguments).then(this.get_next_calendar_notif.bind(this));
|
||||
},
|
||||
});
|
||||
|
||||
BasicModel.include({
|
||||
/**
|
||||
* @private
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { ConnectionLostError } from "@web/core/network/rpc_service";
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
export const calendarNotificationService = {
|
||||
dependencies: ["action", "notification", "rpc"],
|
||||
|
||||
start(env, { action, notification, rpc }) {
|
||||
let calendarNotifTimeouts = {};
|
||||
let nextCalendarNotifTimeout = null;
|
||||
const calendarNotif = {};
|
||||
|
||||
env.bus.on("WEB_CLIENT_READY", null, async () => {
|
||||
const legacyEnv = owl.Component.env;
|
||||
legacyEnv.services.bus_service.onNotification(this, (notifications) => {
|
||||
for (const notif of notifications) {
|
||||
if (notif[0][1] === "calendar.alarm") {
|
||||
displayCalendarNotification(notif[1]);
|
||||
}
|
||||
}
|
||||
});
|
||||
legacyEnv.services.bus_service.startPolling();
|
||||
});
|
||||
|
||||
/**
|
||||
* Displays the Calendar notification on user's screen
|
||||
*/
|
||||
function displayCalendarNotification(notifications) {
|
||||
let lastNotifTimer = 0;
|
||||
|
||||
// Clear previously set timeouts and destroy currently displayed calendar notifications
|
||||
browser.clearTimeout(nextCalendarNotifTimeout);
|
||||
Object.values(calendarNotifTimeouts).forEach((notif) => browser.clearTimeout(notif));
|
||||
calendarNotifTimeouts = {};
|
||||
|
||||
// For each notification, set a timeout to display it
|
||||
notifications.forEach(function (notif) {
|
||||
const key = notif.event_id + "," + notif.alarm_id;
|
||||
if (key in calendarNotif) {
|
||||
return;
|
||||
}
|
||||
calendarNotifTimeouts[key] = browser.setTimeout(function () {
|
||||
const notificationID = notification.create(notif.message, {
|
||||
title: notif.title,
|
||||
type: "warning",
|
||||
sticky: true,
|
||||
onClose: () => {
|
||||
delete calendarNotif[key];
|
||||
},
|
||||
buttons: [
|
||||
{
|
||||
name: env._t("OK"),
|
||||
primary: true,
|
||||
onClick: async () => {
|
||||
await rpc("/calendar/notify_ack");
|
||||
notification.close(calendarNotif[key]);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: env._t("Details"),
|
||||
onClick: async () => {
|
||||
await action.doAction(
|
||||
"calendar.action_calendar_event_notify",
|
||||
{
|
||||
resId: notif.event_id,
|
||||
}
|
||||
);
|
||||
notification.close(calendarNotif[key]);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: env._t("Snooze"),
|
||||
onClick: () => {
|
||||
notification.close(calendarNotif[key]);
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
calendarNotif[key] = notificationID;
|
||||
}, notif.timer * 1000);
|
||||
lastNotifTimer = Math.max(lastNotifTimer, notif.timer);
|
||||
});
|
||||
|
||||
// Set a timeout to get the next notifications when the last one has been displayed
|
||||
if (lastNotifTimer > 0) {
|
||||
nextCalendarNotifTimeout = browser.setTimeout(
|
||||
getNextCalendarNotif,
|
||||
lastNotifTimer * 1000
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function getNextCalendarNotif() {
|
||||
try {
|
||||
const result = await rpc("/calendar/notify", {}, { silent: true });
|
||||
displayCalendarNotification(result);
|
||||
} catch (error) {
|
||||
if (!(error instanceof ConnectionLostError)) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
registry.category("services").add("calendarNotification", calendarNotificationService);
|
||||
@@ -1,19 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<template>
|
||||
|
||||
<t t-name="CalendarNotification" t-extend="Notification">
|
||||
<t t-jquery=".o_notification_title > t" t-operation="replace">
|
||||
<span t-attf-class="link2event eid_{{widget.eid}}">
|
||||
<t t-esc="widget.title"/>
|
||||
</span>
|
||||
</t>
|
||||
<t t-jquery=".o_notification_content" t-operation="after">
|
||||
<div class="mt-2">
|
||||
<button type="button" class="btn btn-primary link2showed oe_highlight oe_form oe_button"><span>OK</span></button>
|
||||
<button type="button" class="btn btn-link link2event">Details</button>
|
||||
<button type="button" class="btn btn-link link2recall">Snooze</button>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
</template>
|
||||
@@ -0,0 +1,233 @@
|
||||
/** @odoo-module */
|
||||
|
||||
import * as legacyRegistry from "web.Registry";
|
||||
import * as BusService from "bus.BusService";
|
||||
import * as RamStorage from "web.RamStorage";
|
||||
import * as AbstractStorageService from "web.AbstractStorageService";
|
||||
|
||||
import {
|
||||
createWebClient,
|
||||
getActionManagerTestConfig,
|
||||
} from "@web/../tests/webclient/actions/helpers";
|
||||
import { calendarNotificationService } from "@calendar/js/services/calendar_notification_service";
|
||||
import { click, nextTick, patchWithCleanup } from "@web/../tests/helpers/utils";
|
||||
import { NotificationContainer } from "@web/core/notifications/notification_container";
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { clearRegistryWithCleanup } from "@web/../tests/helpers/mock_env";
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
const LocalStorageService = AbstractStorageService.extend({
|
||||
storage: new RamStorage(),
|
||||
});
|
||||
const mainComponentRegistry = registry.category("main_components");
|
||||
const serviceRegistry = registry.category("services");
|
||||
|
||||
QUnit.module("Calendar Notification", (hooks) => {
|
||||
let legacyServicesRegistry;
|
||||
let testConfig;
|
||||
hooks.beforeEach(() => {
|
||||
legacyServicesRegistry = new legacyRegistry();
|
||||
legacyServicesRegistry.add("bus_service", BusService);
|
||||
legacyServicesRegistry.add("local_storage", LocalStorageService);
|
||||
|
||||
testConfig = getActionManagerTestConfig();
|
||||
serviceRegistry.add("calendarNotification", calendarNotificationService);
|
||||
|
||||
clearRegistryWithCleanup(mainComponentRegistry);
|
||||
mainComponentRegistry.add("NotificationContainer", NotificationContainer);
|
||||
|
||||
patchWithCleanup(browser, {
|
||||
setTimeout: (fn) => fn(),
|
||||
clearTimeout: () => {},
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"can listen on bus and display notifications in DOM and click OK",
|
||||
async (assert) => {
|
||||
assert.expect(5);
|
||||
|
||||
let pollNumber = 0;
|
||||
const mockRPC = (route, args) => {
|
||||
if (route === "/longpolling/poll") {
|
||||
if (pollNumber > 0) {
|
||||
return new Promise(() => {}); // let it hang to avoid further calls
|
||||
}
|
||||
pollNumber++;
|
||||
return Promise.resolve([
|
||||
{
|
||||
id: "prout",
|
||||
channel: ["db_name", "calendar.alarm", 7],
|
||||
message: [
|
||||
{
|
||||
alarm_id: 1,
|
||||
event_id: 2,
|
||||
title: "Meeting",
|
||||
message: "Very old meeting message",
|
||||
timer: 20 * 60,
|
||||
notify_at: "1978-04-14 12:45:00",
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
}
|
||||
if (route === "/calendar/notify") {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
if (route === "/calendar/notify_ack") {
|
||||
assert.step("notifyAck");
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
};
|
||||
|
||||
const webClient = await createWebClient({
|
||||
testConfig,
|
||||
legacyParams: { serviceRegistry: legacyServicesRegistry },
|
||||
mockRPC,
|
||||
});
|
||||
|
||||
await nextTick();
|
||||
|
||||
assert.containsOnce(webClient.el, ".o_notification_body");
|
||||
assert.strictEqual(
|
||||
webClient.el.querySelector(".o_notification_body .o_notification_content")
|
||||
.textContent,
|
||||
"Very old meeting message"
|
||||
);
|
||||
|
||||
await click(webClient.el.querySelector(".o_notification_buttons .btn"));
|
||||
assert.verifySteps(["notifyAck"]);
|
||||
assert.containsNone(webClient.el, ".o_notification");
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test(
|
||||
"can listen on bus and display notifications in DOM and click Detail",
|
||||
async (assert) => {
|
||||
assert.expect(5);
|
||||
|
||||
let pollNumber = 0;
|
||||
const mockRPC = (route, args) => {
|
||||
if (route === "/longpolling/poll") {
|
||||
if (pollNumber > 0) {
|
||||
return new Promise(() => {}); // let it hang to avoid further calls
|
||||
}
|
||||
pollNumber++;
|
||||
return Promise.resolve([
|
||||
{
|
||||
id: "prout",
|
||||
channel: ["db_name", "calendar.alarm", 7],
|
||||
message: [
|
||||
{
|
||||
alarm_id: 1,
|
||||
event_id: 2,
|
||||
title: "Meeting",
|
||||
message: "Very old meeting message",
|
||||
timer: 20 * 60,
|
||||
notify_at: "1978-04-14 12:45:00",
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
}
|
||||
if (route === "/calendar/notify") {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
};
|
||||
|
||||
const fakeActionService = {
|
||||
name: "action",
|
||||
start() {
|
||||
return {
|
||||
doAction(actionId) {
|
||||
assert.step(actionId);
|
||||
return Promise.resolve(true);
|
||||
},
|
||||
loadState(state, options) {
|
||||
return Promise.resolve(true);
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
serviceRegistry.add("action", fakeActionService, { force: true });
|
||||
|
||||
const webClient = await createWebClient({
|
||||
testConfig,
|
||||
legacyParams: { serviceRegistry: legacyServicesRegistry },
|
||||
mockRPC,
|
||||
});
|
||||
|
||||
await nextTick();
|
||||
|
||||
assert.containsOnce(webClient.el, ".o_notification_body");
|
||||
assert.strictEqual(
|
||||
webClient.el.querySelector(".o_notification_body .o_notification_content")
|
||||
.textContent,
|
||||
"Very old meeting message"
|
||||
);
|
||||
|
||||
await click(webClient.el.querySelectorAll(".o_notification_buttons .btn")[1]);
|
||||
assert.verifySteps(["calendar.action_calendar_event_notify"]);
|
||||
assert.containsNone(webClient.el, ".o_notification");
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test(
|
||||
"can listen on bus and display notifications in DOM and click Snooze",
|
||||
async (assert) => {
|
||||
assert.expect(4);
|
||||
|
||||
let pollNumber = 0;
|
||||
const mockRPC = (route, args) => {
|
||||
if (route === "/longpolling/poll") {
|
||||
if (pollNumber > 0) {
|
||||
return new Promise(() => {}); // let it hang to avoid further calls
|
||||
}
|
||||
pollNumber++;
|
||||
return Promise.resolve([
|
||||
{
|
||||
id: "prout",
|
||||
channel: ["db_name", "calendar.alarm", 7],
|
||||
message: [
|
||||
{
|
||||
alarm_id: 1,
|
||||
event_id: 2,
|
||||
title: "Meeting",
|
||||
message: "Very old meeting message",
|
||||
timer: 20 * 60,
|
||||
notify_at: "1978-04-14 12:45:00",
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
}
|
||||
if (route === "/calendar/notify") {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
if (route === "/calendar/notify_ack") {
|
||||
assert.step("notifyAck");
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
};
|
||||
|
||||
const webClient = await createWebClient({
|
||||
testConfig,
|
||||
legacyParams: { serviceRegistry: legacyServicesRegistry },
|
||||
mockRPC,
|
||||
});
|
||||
|
||||
await nextTick();
|
||||
|
||||
assert.containsOnce(webClient.el, ".o_notification_body");
|
||||
assert.strictEqual(
|
||||
webClient.el.querySelector(".o_notification_body .o_notification_content")
|
||||
.textContent,
|
||||
"Very old meeting message"
|
||||
);
|
||||
|
||||
await click(webClient.el.querySelectorAll(".o_notification_buttons .btn")[2]);
|
||||
assert.verifySteps([], "should only close the notification withtout calling a rpc");
|
||||
assert.containsNone(webClient.el, ".o_notification");
|
||||
}
|
||||
);
|
||||
});
|
||||
@@ -42,7 +42,7 @@ actions(Check in/Check out) performed by them.
|
||||
'hr_attendance/static/src/scss/hr_attendance.scss',
|
||||
],
|
||||
'web.qunit_suite_tests': [
|
||||
('after', 'web/static/tests/views/kanban_tests.js', 'hr_attendance/static/tests/hr_attendance_tests.js'),
|
||||
('after', 'web/static/tests/legacy/views/kanban_tests.js', 'hr_attendance/static/tests/hr_attendance_tests.js'),
|
||||
],
|
||||
'web.assets_qweb': [
|
||||
'hr_attendance/static/src/xml/**/*',
|
||||
|
||||
@@ -3530,7 +3530,7 @@ odoo.define('im_livechat.legacy.mail.DocumentViewer', function (require) {
|
||||
|
||||
var core = require('web.core');
|
||||
var Widget = require('web.Widget');
|
||||
var { hidePDFJSButtons } = require('@web/js/libs/pdfjs');
|
||||
var { hidePDFJSButtons } = require('@web/legacy/js/libs/pdfjs');
|
||||
|
||||
var QWeb = core.qweb;
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import useShouldUpdateBasedOnProps from '@mail/component_hooks/use_should_update
|
||||
import useStore from '@mail/component_hooks/use_store/use_store';
|
||||
import { link } from '@mail/model/model_field_command';
|
||||
|
||||
import { hidePDFJSButtons } from '@web/js/libs/pdfjs';
|
||||
import { hidePDFJSButtons } from '@web/legacy/js/libs/pdfjs';
|
||||
|
||||
const { Component, QWeb } = owl;
|
||||
const { useRef } = owl.hooks;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import core from 'web.core';
|
||||
import Widget from 'web.Widget';
|
||||
import { hidePDFJSButtons } from '@web/js/libs/pdfjs';
|
||||
import { hidePDFJSButtons } from '@web/legacy/js/libs/pdfjs';
|
||||
|
||||
var QWeb = core.qweb;
|
||||
|
||||
|
||||
@@ -1,30 +1,37 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import core from 'web.core';
|
||||
import DebugManager from 'web.DebugManager.Backend';
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
var _t = core._t;
|
||||
/**
|
||||
* adds a new method available for the debug manager, called by the "Manage Messages" button.
|
||||
*
|
||||
*/
|
||||
DebugManager.include({
|
||||
getMailMessages: function () {
|
||||
var selectedIDs = this._controller.getSelectedIds();
|
||||
if (!selectedIDs.length) {
|
||||
console.warn(_t("No message available"));
|
||||
return;
|
||||
}
|
||||
this.do_action({
|
||||
res_model: 'mail.message',
|
||||
name: _t('Manage Messages'),
|
||||
views: [[false, 'list'], [false, 'form']],
|
||||
type: 'ir.actions.act_window',
|
||||
domain: [['res_id', '=', selectedIDs[0]], ['model', '=', this._controller.modelName]],
|
||||
context: {
|
||||
default_res_model: this._controller.modelName,
|
||||
default_res_id: selectedIDs[0],
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
export function manageMessages({ action, component, env }) {
|
||||
const selectedIds = component.widget.getSelectedIds();
|
||||
if (!selectedIds.length) {
|
||||
return null; // No record
|
||||
}
|
||||
const description = env._t("Manage Messages");
|
||||
return {
|
||||
type: "item",
|
||||
description,
|
||||
callback: () => {
|
||||
env.services.action.doAction({
|
||||
res_model: "mail.message",
|
||||
name: description,
|
||||
views: [
|
||||
[false, "list"],
|
||||
[false, "form"],
|
||||
],
|
||||
type: "ir.actions.act_window",
|
||||
domain: [
|
||||
["res_id", "=", selectedIds[0]],
|
||||
["model", "=", action.res_model],
|
||||
],
|
||||
context: {
|
||||
default_res_model: action.res_model,
|
||||
default_res_id: selectedIds[0],
|
||||
},
|
||||
});
|
||||
},
|
||||
sequence: 325,
|
||||
};
|
||||
}
|
||||
|
||||
registry.category("debug").category("form").add("mail.manageMessages", manageMessages);
|
||||
|
||||
@@ -191,7 +191,7 @@ function factory(dependencies) {
|
||||
options: {
|
||||
active_id: this.threadToActiveId(this),
|
||||
clear_breadcrumbs: false,
|
||||
on_reverse_breadcrumb: () => this.close(),
|
||||
on_reverse_breadcrumb: () => this.close(), // this is useless, close is called by destroy anyway
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -18,12 +18,17 @@ import AbstractStorageService from 'web.AbstractStorageService';
|
||||
import NotificationService from 'web.NotificationService';
|
||||
import RamStorage from 'web.RamStorage';
|
||||
import {
|
||||
createActionManager,
|
||||
createView,
|
||||
makeTestPromise,
|
||||
mock,
|
||||
} from 'web.test_utils';
|
||||
import Widget from 'web.Widget';
|
||||
import {
|
||||
createWebClient,
|
||||
getActionManagerTestConfig,
|
||||
} from "@web/../tests/webclient/actions/helpers";
|
||||
import { ComponentAdapter } from "web.OwlCompatibility";
|
||||
import LegacyMockServer from "web.MockServer";
|
||||
|
||||
const {
|
||||
addMockEnvironment,
|
||||
@@ -546,7 +551,36 @@ async function start(param0 = {}) {
|
||||
}
|
||||
});
|
||||
} else if (hasActionManager) {
|
||||
widget = await createActionManager(kwargs);
|
||||
let testConfig;
|
||||
if (!kwargs.testConfig) {
|
||||
testConfig = getActionManagerTestConfig();
|
||||
} else {
|
||||
testConfig = kwargs.testConfig;
|
||||
delete kwargs.testConfig;
|
||||
}
|
||||
|
||||
if (kwargs.actions) {
|
||||
const actions = {};
|
||||
kwargs.actions.forEach((act) => {
|
||||
actions[act.xml_id || act.id] = act;
|
||||
});
|
||||
Object.assign(testConfig.serverData.actions, actions);
|
||||
delete kwargs.actions;
|
||||
}
|
||||
|
||||
Object.assign(testConfig.serverData.views, kwargs.archs);
|
||||
delete kwargs.archs;
|
||||
|
||||
Object.assign(testConfig.serverData.models, kwargs.data);
|
||||
delete kwargs.data;
|
||||
|
||||
const mockRPC = kwargs.mockRPC;
|
||||
delete kwargs.mockRPC;
|
||||
|
||||
const legacyParams = kwargs;
|
||||
legacyParams.withLegacyMockServer = true;
|
||||
const widget = await createWebClient({ testConfig, mockRPC, legacyParams });
|
||||
|
||||
legacyPatch(widget, {
|
||||
destroy() {
|
||||
destroyCallbacks.forEach(callback => callback({ widget }));
|
||||
|
||||
@@ -1,14 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<!--
|
||||
extends the debug mode menu to allow access to the attachment list view of the current record.
|
||||
-->
|
||||
<t t-extend="WebClient.DebugManager.View">
|
||||
<t t-jquery="a[data-action='get_metadata']" t-operation="after">
|
||||
<a role="menuitem" href="#" data-action="getMailMessages" class="dropdown-item">Manage Messages</a>
|
||||
</t>
|
||||
</t>
|
||||
<!--
|
||||
@param {mail.DocumentViewer} widget
|
||||
-->
|
||||
|
||||
@@ -2,8 +2,14 @@
|
||||
|
||||
import ActivityView from '@mail/js/views/activity/activity_view';
|
||||
import testUtils from 'web.test_utils';
|
||||
import { createWebClient } from "@web/../tests/webclient/actions/helpers";
|
||||
|
||||
var createActionManager = testUtils.createActionManager;
|
||||
import { legacyExtraNextTick } from "@web/../tests/helpers/utils";
|
||||
import { doAction, getActionManagerTestConfig } from "@web/../tests/webclient/actions/helpers";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { makeFakeUserService } from "@web/../tests/helpers/mock_services";
|
||||
|
||||
let testConfig;
|
||||
|
||||
var createView = testUtils.createView;
|
||||
|
||||
@@ -108,6 +114,8 @@ QUnit.module('activity view', {
|
||||
],
|
||||
},
|
||||
};
|
||||
testConfig = getActionManagerTestConfig();
|
||||
Object.assign(testConfig.serverData.models, this.data);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -350,49 +358,68 @@ QUnit.test('activity view: activity widget', async function (assert) {
|
||||
|
||||
activity.destroy();
|
||||
});
|
||||
QUnit.test('activity view: no group_by_menu and no comparison_menu', async function (assert) {
|
||||
|
||||
QUnit.test("activity view: no group_by_menu and no comparison_menu", async function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
var actionManager = await createActionManager({
|
||||
actions: [{
|
||||
id: 1,
|
||||
name: 'Task Action',
|
||||
res_model: 'task',
|
||||
type: 'ir.actions.act_window',
|
||||
views: [[false, 'activity']],
|
||||
}],
|
||||
archs: {
|
||||
'task,false,activity': '<activity string="Task">' +
|
||||
'<templates>' +
|
||||
'<div t-name="activity-box">' +
|
||||
'<field name="foo"/>' +
|
||||
'</div>' +
|
||||
'</templates>' +
|
||||
'</activity>',
|
||||
'task,false,search': '<search></search>',
|
||||
},
|
||||
data: this.data,
|
||||
session: {
|
||||
user_context: {lang: 'zz_ZZ'},
|
||||
},
|
||||
mockRPC: function(route, args) {
|
||||
if (args.method === 'get_activity_data') {
|
||||
assert.deepEqual(args.kwargs.context, {lang: 'zz_ZZ'},
|
||||
'The context should have been passed');
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
testConfig.serverData.actions[1] = {
|
||||
id: 1,
|
||||
name: "Task Action",
|
||||
res_model: "task",
|
||||
type: "ir.actions.act_window",
|
||||
views: [[false, "activity"]],
|
||||
};
|
||||
|
||||
await actionManager.doAction(1);
|
||||
testConfig.serverData.views = {
|
||||
"task,false,activity":
|
||||
'<activity string="Task">' +
|
||||
"<templates>" +
|
||||
'<div t-name="activity-box">' +
|
||||
'<field name="foo"/>' +
|
||||
"</div>" +
|
||||
"</templates>" +
|
||||
"</activity>",
|
||||
"task,false,search": "<search></search>",
|
||||
};
|
||||
|
||||
assert.containsN(actionManager, '.o_search_options .o_dropdown button:visible', 2,
|
||||
"only two elements should be available in view search");
|
||||
assert.isVisible(actionManager.$('.o_search_options .o_dropdown.o_filter_menu > button'),
|
||||
"filter should be available in view search");
|
||||
assert.isVisible(actionManager.$('.o_search_options .o_dropdown.o_favorite_menu > button'),
|
||||
"favorites should be available in view search");
|
||||
actionManager.destroy();
|
||||
const mockRPC = (route, args) => {
|
||||
if (args.method === "get_activity_data") {
|
||||
assert.strictEqual(
|
||||
args.kwargs.context.lang,
|
||||
"zz_ZZ",
|
||||
"The context should have been passed"
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
registry.category("services").add(
|
||||
"user",
|
||||
makeFakeUserService({
|
||||
session_info: {
|
||||
user_context: { lang: "zz_ZZ" },
|
||||
},
|
||||
}),
|
||||
{ force: true }
|
||||
);
|
||||
|
||||
const webClient = await createWebClient({ testConfig, mockRPC , legacyParams: {withLegacyMockServer: true}});
|
||||
|
||||
await doAction(webClient, 1);
|
||||
|
||||
assert.containsN(
|
||||
webClient,
|
||||
".o_search_options .o_dropdown button:visible",
|
||||
2,
|
||||
"only two elements should be available in view search"
|
||||
);
|
||||
assert.isVisible(
|
||||
$(webClient.el).find(".o_search_options .o_dropdown.o_filter_menu > button"),
|
||||
"filter should be available in view search"
|
||||
);
|
||||
assert.isVisible(
|
||||
$(webClient.el).find(".o_search_options .o_dropdown.o_favorite_menu > button"),
|
||||
"favorites should be available in view search"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test('activity view: search more to schedule an activity for a record of a respecting model', async function (assert) {
|
||||
@@ -458,63 +485,54 @@ QUnit.test('activity view: search more to schedule an activity for a record of a
|
||||
activity.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('Activity view: discard an activity creation dialog', async function (assert) {
|
||||
QUnit.test("Activity view: discard an activity creation dialog", async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
var actionManager = await createActionManager({
|
||||
actions: [{
|
||||
id: 1,
|
||||
name: 'Task Action',
|
||||
res_model: 'task',
|
||||
type: 'ir.actions.act_window',
|
||||
views: [[false, 'activity']],
|
||||
}],
|
||||
archs: {
|
||||
'task,false,activity': `
|
||||
<activity string="Task">
|
||||
<templates>
|
||||
<div t-name="activity-box">
|
||||
<field name="foo"/>
|
||||
</div>
|
||||
</templates>
|
||||
</activity>`,
|
||||
'task,false,search': '<search></search>',
|
||||
'mail.activity,false,form': `
|
||||
<form>
|
||||
<field name="display_name"/>
|
||||
<footer>
|
||||
<button string="Discard" class="btn-secondary" special="cancel"/>
|
||||
</footer>
|
||||
</form>`
|
||||
},
|
||||
data: this.data,
|
||||
intercepts: {
|
||||
do_action(ev) {
|
||||
actionManager.doAction(ev.data.action, ev.data.options);
|
||||
}
|
||||
},
|
||||
async mockRPC(route, args) {
|
||||
if (args.method === 'check_access_rights') {
|
||||
return true;
|
||||
}
|
||||
return this._super(...arguments);
|
||||
},
|
||||
});
|
||||
await actionManager.doAction(1);
|
||||
testConfig.serverData.actions[1] = {
|
||||
id: 1,
|
||||
name: "Task Action",
|
||||
res_model: "task",
|
||||
type: "ir.actions.act_window",
|
||||
views: [[false, "activity"]],
|
||||
};
|
||||
|
||||
await testUtils.dom.click(actionManager.$('.o_activity_view .o_data_row .o_activity_empty_cell')[0]);
|
||||
assert.containsOnce(
|
||||
$,
|
||||
'.modal.o_technical_modal.show',
|
||||
"Activity Modal should be opened");
|
||||
testConfig.serverData.views = {
|
||||
"task,false,activity": `
|
||||
<activity string="Task">
|
||||
<templates>
|
||||
<div t-name="activity-box">
|
||||
<field name="foo"/>
|
||||
</div>
|
||||
</templates>
|
||||
</activity>`,
|
||||
"task,false,search": "<search></search>",
|
||||
"mail.activity,false,form": `
|
||||
<form>
|
||||
<field name="display_name"/>
|
||||
<footer>
|
||||
<button string="Discard" class="btn-secondary" special="cancel"/>
|
||||
</footer>
|
||||
</form>`,
|
||||
};
|
||||
|
||||
await testUtils.dom.click($('.modal.o_technical_modal.show button[special="cancel"]'));
|
||||
assert.containsNone(
|
||||
$,
|
||||
'.modal.o_technical_modal.show',
|
||||
"Activity Modal should be closed");
|
||||
const mockRPC = (route, args) => {
|
||||
if (args.method === "check_access_rights") {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
actionManager.destroy();
|
||||
const webClient = await createWebClient({ testConfig, mockRPC, legacyParams: {withLegacyMockServer: true} });
|
||||
await doAction(webClient, 1);
|
||||
|
||||
await testUtils.dom.click(
|
||||
$(webClient.el).find(".o_activity_view .o_data_row .o_activity_empty_cell")[0]
|
||||
);
|
||||
await legacyExtraNextTick();
|
||||
assert.containsOnce($, ".modal.o_technical_modal", "Activity Modal should be opened");
|
||||
|
||||
await testUtils.dom.click($('.modal.o_technical_modal button[special="cancel"]'));
|
||||
await legacyExtraNextTick();
|
||||
assert.containsNone($, ".modal.o_technical_modal", "Activity Modal should be closed");
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -1,62 +1,70 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import testUtils from 'web.test_utils';
|
||||
import { manageMessages } from "@mail/js/tools/debug_manager";
|
||||
import { click, legacyExtraNextTick } from "@web/../tests/helpers/utils";
|
||||
import {
|
||||
createWebClient,
|
||||
doAction,
|
||||
getActionManagerTestConfig,
|
||||
} from "@web/../tests/webclient/actions/helpers";
|
||||
import { debugService } from "@web/core/debug/debug_service";
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
var createDebugManager = testUtils.createDebugManager;
|
||||
QUnit.module("DebugMenu");
|
||||
|
||||
QUnit.module('Mail DebugManager', {}, function () {
|
||||
QUnit.test("Manage Messages", async function (assert) {
|
||||
assert.expect(6);
|
||||
|
||||
QUnit.test("Manage Messages", async function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
var debugManager = await createDebugManager({
|
||||
intercepts: {
|
||||
do_action: function (event) {
|
||||
assert.deepEqual(event.data.action, {
|
||||
context: {
|
||||
default_res_model: "testModel",
|
||||
default_res_id: 5,
|
||||
},
|
||||
res_model: 'mail.message',
|
||||
name: "Manage Messages",
|
||||
views: [[false, 'list'], [false, 'form']],
|
||||
type: 'ir.actions.act_window',
|
||||
domain: [['res_id', '=', 5], ['model', '=', 'testModel']],
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await debugManager.appendTo($('#qunit-fixture'));
|
||||
|
||||
// Simulate update debug manager from web client
|
||||
var action = {
|
||||
views: [{
|
||||
displayName: "Form",
|
||||
fieldsView: {
|
||||
view_id: 1,
|
||||
},
|
||||
type: "form",
|
||||
}],
|
||||
};
|
||||
var view = {
|
||||
viewType: "form",
|
||||
getSelectedIds: function () {
|
||||
return [5];
|
||||
},
|
||||
modelName: 'testModel',
|
||||
};
|
||||
await testUtils.nextTick();
|
||||
await debugManager.update('action', action, view);
|
||||
|
||||
var $messageMenu = debugManager.$('a[data-action=getMailMessages]');
|
||||
assert.strictEqual($messageMenu.length, 1, "should have Manage Message menu item");
|
||||
assert.strictEqual($messageMenu.text().trim(), "Manage Messages",
|
||||
"should have correct menu item text");
|
||||
|
||||
await testUtils.dom.click(debugManager.$('> a')); // open dropdown
|
||||
await testUtils.dom.click($messageMenu);
|
||||
|
||||
debugManager.destroy();
|
||||
const testConfig = Object.assign(getActionManagerTestConfig(), {
|
||||
debug: "1",
|
||||
});
|
||||
|
||||
// Add fake "mail.message" model and arch
|
||||
testConfig.serverData.models["mail.message"] = {
|
||||
fields: { name: { string: "Name", type: "char" } },
|
||||
records: [],
|
||||
};
|
||||
Object.assign(testConfig.serverData.views, {
|
||||
"mail.message,false,list": `<tree/>`,
|
||||
"mail.message,false,form": `<form/>`,
|
||||
"mail.message,false,search": `<search/>`,
|
||||
});
|
||||
|
||||
// Activate debug service with "Manage Message" item
|
||||
registry.category("services").add("debug", debugService);
|
||||
registry.category("debug").category("form").add("manageMessages", manageMessages);
|
||||
|
||||
async function mockRPC(route, args) {
|
||||
if (args.method === "check_access_rights") {
|
||||
return true;
|
||||
}
|
||||
if (route === "/web/dataset/search_read" && args.model === "mail.message") {
|
||||
assert.strictEqual(args.context.default_res_id, 5);
|
||||
assert.strictEqual(args.context.default_res_model, "partner");
|
||||
assert.deepEqual(args.domain, ["&", ["res_id", "=", 5], ["model", "=", "partner"]]);
|
||||
}
|
||||
}
|
||||
|
||||
const wc = await createWebClient({ testConfig, mockRPC });
|
||||
await doAction(wc, 3, { viewType: "form", resId: 5 });
|
||||
await legacyExtraNextTick();
|
||||
await click(wc.el, ".o_debug_manager .o_dropdown_toggler");
|
||||
|
||||
const dropdownItems = wc.el.querySelectorAll(
|
||||
".o_debug_manager .o_dropdown_menu .o_dropdown_item"
|
||||
);
|
||||
assert.strictEqual(dropdownItems.length, 1);
|
||||
assert.strictEqual(
|
||||
dropdownItems[0].innerText.trim(),
|
||||
"Manage Messages",
|
||||
"should have correct menu item text"
|
||||
);
|
||||
|
||||
await click(dropdownItems[0]);
|
||||
await legacyExtraNextTick();
|
||||
|
||||
assert.strictEqual(
|
||||
wc.el.querySelector(".breadcrumb-item.active").innerText.trim(),
|
||||
"Manage Messages"
|
||||
);
|
||||
});
|
||||
|
||||
@@ -14,7 +14,7 @@ odoo.define('mass_mailing.unsubscribe', function (require) {
|
||||
var token = (location.search.split('token' + '=')[1] || '').split('&')[0];
|
||||
|
||||
if (!$('.o_unsubscribe_form').length) {
|
||||
return Promise.reject("DOM doesn't contain '.o_unsubscribe_form'");
|
||||
return;
|
||||
}
|
||||
session.load_translations().then(function () {
|
||||
if (email != '' && email != undefined){
|
||||
|
||||
@@ -1,26 +1,18 @@
|
||||
odoo.define('web.company_autocomplete', function (require) {
|
||||
"use strict";
|
||||
/** @odoo-module **/
|
||||
|
||||
const AbstractWebClient = require('web.AbstractWebClient');
|
||||
const session = require('web.session');
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
return AbstractWebClient.include({
|
||||
export const companyAutocompleteService = {
|
||||
dependencies: ["orm", "user"],
|
||||
|
||||
start: function () {
|
||||
if (session.iap_company_enrich) {
|
||||
const current_company_id = session.user_companies.current_company;
|
||||
this._rpc({
|
||||
model: 'res.company',
|
||||
method: 'iap_enrich_auto',
|
||||
args: [current_company_id],
|
||||
}, {
|
||||
shadow: true,
|
||||
});
|
||||
start(env, { orm, user }) {
|
||||
if (odoo.session_info.iap_company_enrich) {
|
||||
const currentCompanyId = user.current_company.id;
|
||||
orm.silent.call("res.company", "iap_enrich_auto", [currentCompanyId], {});
|
||||
}
|
||||
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
registry
|
||||
.category("services")
|
||||
.add("partner_autocomplete.companyAutocomplete", companyAutocompleteService);
|
||||
|
||||
@@ -164,6 +164,43 @@
|
||||
],
|
||||
'point_of_sale.pos_assets_backend': [
|
||||
('include', 'web.assets_backend'),
|
||||
('remove', 'web/static/src/webclient/menu_service.js'),
|
||||
('remove', 'web/static/src/core/errors/error_handlers.js'),
|
||||
('remove', 'web/static/src/legacy/legacy_rpc_error_handler.js'),
|
||||
],
|
||||
'point_of_sale.tests_assets': [
|
||||
'web/static/lib/daterangepicker/daterangepicker.css',
|
||||
'web/static/lib/qunit/qunit-2.9.1.css',
|
||||
'web/static/lib/qunit/qunit-2.9.1.js',
|
||||
'web/static/tests/legacy/helpers/**/*',
|
||||
('remove', 'web/static/tests/legacy/helpers/test_utils_tests.js'),
|
||||
|
||||
'web/static/tests/legacy/legacy_setup.js',
|
||||
|
||||
'web/static/tests/helpers/**/*.js',
|
||||
'web/static/tests/qunit.js',
|
||||
'web/static/tests/main.js',
|
||||
'web/static/tests/setup.js',
|
||||
|
||||
# These 2 lines below are taken from web.assets_frontend
|
||||
# They're required for the web.frontend_legacy to work properly
|
||||
# It is expected to add other lines coming from the web.assets_frontend
|
||||
# if we need to add more and more legacy stuff that would require other scss or js.
|
||||
('include', 'web._assets_helpers'),
|
||||
'web/static/lib/bootstrap/scss/_variables.scss',
|
||||
|
||||
('include', 'web.frontend_legacy'),
|
||||
],
|
||||
'point_of_sale.qunit_suite_tests': [
|
||||
'web/static/tests/legacy/component_extension_tests.js',
|
||||
'point_of_sale/static/tests/unit/**/*',
|
||||
('remove', 'point_of_sale/static/tests/unit/helpers/test_main.js')
|
||||
],
|
||||
'point_of_sale.assets_backend_prod_only': [
|
||||
'point_of_sale/static/src/js/chrome_adapter.js',
|
||||
'point_of_sale/static/src/js/main.js',
|
||||
'web/static/src/start.js',
|
||||
'web/static/src/legacy/legacy_setup.js',
|
||||
],
|
||||
'web.assets_qweb': [
|
||||
'point_of_sale/static/src/xml/**/*',
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
/** @odoo-module */
|
||||
|
||||
import { useService } from "@web/core/service_hook";
|
||||
|
||||
import Chrome from "point_of_sale.Chrome";
|
||||
import Registries from "point_of_sale.Registries";
|
||||
import { configureGui } from "point_of_sale.Gui";
|
||||
import { useBus } from "@web/core/bus_hook";
|
||||
import PosComponent from "point_of_sale.PosComponent";
|
||||
import PopupControllerMixin from "point_of_sale.PopupControllerMixin";
|
||||
import { useErrorHandlers } from "point_of_sale.custom_hooks";
|
||||
import { ConnectionLostError } from "@web/core/network/rpc_service";
|
||||
|
||||
function setupResponsivePlugin(env) {
|
||||
const isMobile = () => window.innerWidth <= 768;
|
||||
env.isMobile = isMobile();
|
||||
const updateEnv = owl.utils.debounce(() => {
|
||||
if (env.isMobile !== isMobile()) {
|
||||
env.isMobile = !env.isMobile;
|
||||
env.qweb.forceUpdate();
|
||||
}
|
||||
}, 15);
|
||||
window.addEventListener("resize", updateEnv);
|
||||
}
|
||||
|
||||
export class ChromeAdapter extends PopupControllerMixin(PosComponent) {
|
||||
setup() {
|
||||
this.PosChrome = Registries.Component.get(Chrome);
|
||||
this.legacyActionManager = useService("legacy_action_manager");
|
||||
useErrorHandlers();
|
||||
useBus(this.env.bus, "ERROR_DISPATCHED", (error) => {
|
||||
if (error instanceof ConnectionLostError) {
|
||||
error = { code: -9999 }; // just a negative number
|
||||
}
|
||||
this._handlePushOrderError(error);
|
||||
});
|
||||
|
||||
this.env = owl.Component.env;
|
||||
useBus(this.env.qweb, "update", () => this.render());
|
||||
const chrome = owl.hooks.useRef("chrome");
|
||||
owl.hooks.onMounted(async () => {
|
||||
await chrome.comp.start();
|
||||
configureGui({ component: chrome.comp });
|
||||
setupResponsivePlugin(this.env);
|
||||
});
|
||||
}
|
||||
}
|
||||
ChromeAdapter.template = owl.tags.xml`<PosChrome t-ref="chrome" webClient="legacyActionManager"/>`;
|
||||
@@ -1,20 +1,21 @@
|
||||
odoo.define('point_of_sale.DebugManager.Backend', function(require) {
|
||||
'use strict';
|
||||
/** @odoo-module */
|
||||
|
||||
const { _t } = require('web.core');
|
||||
const DebugManager = require('web.DebugManager.Backend');
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
DebugManager.include({
|
||||
/**
|
||||
* Runs the JS (desktop) tests
|
||||
*/
|
||||
perform_pos_js_tests() {
|
||||
this.do_action({
|
||||
name: _t('JS Tests'),
|
||||
target: 'new',
|
||||
type: 'ir.actions.act_url',
|
||||
url: '/pos/ui/tests?mod=*',
|
||||
function runPoSJSTests({ env }) {
|
||||
return {
|
||||
type: "item",
|
||||
description: env._t("Run Point of Sale JS Tests"),
|
||||
callback: () => {
|
||||
env.services.action.doAction({
|
||||
name: env._t("JS Tests"),
|
||||
target: "new",
|
||||
type: "ir.actions.act_url",
|
||||
url: "/pos/ui/tests?mod=*",
|
||||
});
|
||||
},
|
||||
});
|
||||
});
|
||||
sequence: 35,
|
||||
};
|
||||
}
|
||||
|
||||
registry.category("debug").add("point_of_sale.runPoSJSTests", runPoSJSTests);
|
||||
|
||||
@@ -1,49 +1,34 @@
|
||||
odoo.define('web.web_client', function (require) {
|
||||
'use strict';
|
||||
/** @odoo-module */
|
||||
|
||||
const AbstractService = require('web.AbstractService');
|
||||
const env = require('web.env');
|
||||
const WebClient = require('web.AbstractWebClient');
|
||||
const Chrome = require('point_of_sale.Chrome');
|
||||
const Registries = require('point_of_sale.Registries');
|
||||
const { configureGui } = require('point_of_sale.Gui');
|
||||
import { startWebClient } from "@web/start";
|
||||
|
||||
owl.config.mode = env.isDebug() ? 'dev' : 'prod';
|
||||
owl.Component.env = env;
|
||||
import { ChromeAdapter } from "@point_of_sale/js/chrome_adapter";
|
||||
import Registries from "point_of_sale.Registries";
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
// For consistency's sake, we should trigger"WEB_CLIENT_READY" on the bus when PosApp is mounted
|
||||
// But we can't since mail and some other poll react on that cue, and we don't want those services started
|
||||
class PosApp extends owl.Component {
|
||||
setup() {
|
||||
this.Components = registry.category("main_components").getEntries();
|
||||
}
|
||||
}
|
||||
PosApp.template = owl.tags.xml`
|
||||
<body>
|
||||
<ChromeAdapter />
|
||||
<div>
|
||||
<t t-foreach="Components" t-as="Component" t-key="Component[0]">
|
||||
<t t-component="Component[1]"/>
|
||||
</t>
|
||||
</div>
|
||||
</body>
|
||||
`;
|
||||
PosApp.components = { ChromeAdapter };
|
||||
|
||||
function startPosApp() {
|
||||
Registries.Component.add(owl.misc.Portal);
|
||||
Registries.Component.freeze();
|
||||
startWebClient(PosApp);
|
||||
}
|
||||
|
||||
function setupResponsivePlugin(env) {
|
||||
const isMobile = () => window.innerWidth <= 768;
|
||||
env.isMobile = isMobile();
|
||||
const updateEnv = owl.utils.debounce(() => {
|
||||
if (env.isMobile !== isMobile()) {
|
||||
env.isMobile = !env.isMobile;
|
||||
env.qweb.forceUpdate();
|
||||
}
|
||||
}, 15);
|
||||
window.addEventListener("resize", updateEnv);
|
||||
}
|
||||
|
||||
setupResponsivePlugin(owl.Component.env);
|
||||
|
||||
async function startPosApp(webClient) {
|
||||
Registries.Component.freeze();
|
||||
await env.session.is_bound;
|
||||
env.qweb.addTemplates(env.session.owlTemplates);
|
||||
env.bus = new owl.core.EventBus();
|
||||
await owl.utils.whenReady();
|
||||
await webClient.setElement(document.body);
|
||||
await webClient.start();
|
||||
webClient.isStarted = true;
|
||||
const chrome = new (Registries.Component.get(Chrome))(null, { webClient });
|
||||
await chrome.mount(document.querySelector('.o_action_manager'));
|
||||
await chrome.start();
|
||||
configureGui({ component: chrome });
|
||||
}
|
||||
|
||||
AbstractService.prototype.deployServices(env);
|
||||
const webClient = new WebClient();
|
||||
startPosApp(webClient);
|
||||
return webClient;
|
||||
});
|
||||
startPosApp();
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<t t-extend="WebClient.DebugManager.Backend">
|
||||
<t t-jquery="a[data-action='perform_click_everywhere_test']" t-operation="after">
|
||||
<a role="menuitem" href="#" data-action="perform_pos_js_tests" class="dropdown-item">Run Point of Sale JS Tests</a>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -50,7 +50,7 @@ odoo.define('point_of_sale.tour.pricelist', function (require) {
|
||||
var steps = [{ // Leave category displayed by default
|
||||
content: 'waiting for loading to finish',
|
||||
extra_trigger: 'body .pos:not(:has(.loader))', // Pos has finished loading
|
||||
trigger: 'body:not(.oe_wait)', // WebClient has finished Loading
|
||||
trigger: '.o_loading_indicator:not(.o_loading)', // WebClient has finished Loading
|
||||
run: function () {
|
||||
var product_wall_shelf = posmodel.db.search_product_in_category(0, 'Wall Shelf Unit')[0];
|
||||
var product_small_shelf = posmodel.db.search_product_in_category(0, 'Small Shelf')[0];
|
||||
|
||||
@@ -32,11 +32,12 @@
|
||||
'login_number': login_number,
|
||||
'debug': debug,
|
||||
})"/>;
|
||||
odoo.loadTemplatesPromise = fetch(`/web/webclient/qweb/${odoo.session_info.cache_hashes.qweb}?bundle=web.assets_qweb`).then(doc => doc.text());
|
||||
</script>
|
||||
|
||||
<t t-call="point_of_sale.assets_common"/>
|
||||
<t t-call="web.conditional_assets_tests"/>
|
||||
<script type="text/javascript" src="/point_of_sale/static/src/js/main.js"></script>
|
||||
<t t-call-assets="point_of_sale.assets_backend_prod_only" />
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
|
||||
@@ -3,12 +3,8 @@
|
||||
|
||||
<template id="point_of_sale.qunit_suite_assets">
|
||||
<t t-call-assets="point_of_sale.pos_assets_backend" t-js="false"/>
|
||||
|
||||
<link type="text/css" rel="stylesheet" href="/web/static/lib/qunit/qunit-2.9.1.css"/>
|
||||
<script type="text/javascript" src="/web/static/lib/qunit/qunit-2.9.1.js"></script>
|
||||
<script type="text/javascript" src="/web/static/tests/helpers/qunit_config.js"></script>
|
||||
<script type="text/javascript" src="/web/static/tests/helpers/qunit_asserts.js"></script>
|
||||
|
||||
<t t-call-assets="point_of_sale.tests_assets" t-js="false"/>
|
||||
<t t-call-assets="point_of_sale.tests_assets" t-css="false"/>
|
||||
<style>
|
||||
body {
|
||||
position: relative;
|
||||
@@ -25,36 +21,6 @@
|
||||
display: inherit;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script type="text/javascript" src="/web/static/tests/helpers/test_utils_create.js"></script>
|
||||
<script type="text/javascript" src="/web/static/tests/helpers/test_utils_control_panel.js"></script>
|
||||
<script type="text/javascript" src="/web/static/tests/helpers/test_utils_dom.js"></script>
|
||||
<script type="text/javascript" src="/web/static/tests/helpers/test_utils_fields.js"></script>
|
||||
<script type="text/javascript" src="/web/static/tests/helpers/test_utils_file.js"></script>
|
||||
<script type="text/javascript" src="/web/static/tests/helpers/test_utils_form.js"></script>
|
||||
<script type="text/javascript" src="/web/static/tests/helpers/test_utils_graph.js"></script>
|
||||
<script type="text/javascript" src="/web/static/tests/helpers/test_utils_kanban.js"></script>
|
||||
<script type="text/javascript" src="/web/static/tests/helpers/test_utils_mock.js"></script>
|
||||
<script type="text/javascript" src="/web/static/tests/helpers/test_utils_modal.js"></script>
|
||||
<script type="text/javascript" src="/web/static/tests/helpers/test_utils_pivot.js"></script>
|
||||
<script type="text/javascript" src="/web/static/tests/helpers/test_utils.js"></script>
|
||||
<script type="text/javascript" src="/web/static/tests/helpers/mock_server.js"></script>
|
||||
<script type="text/javascript" src="/web/static/tests/helpers/test_env.js"></script>
|
||||
|
||||
<script type="text/javascript" src="/web/static/tests/component_extension_tests.js"></script>
|
||||
<script type="text/javascript" src="/web/static/tests/boot_tests.js"></script>
|
||||
|
||||
<script type="text/javascript" src="/point_of_sale/static/tests/unit/helpers/test_env.js"></script>
|
||||
<script type="text/javascript" src="/point_of_sale/static/tests/unit/helpers/test_main.js"></script>
|
||||
</template>
|
||||
|
||||
<template id="point_of_sale.qunit_suite_tests">
|
||||
<script type="text/javascript" src="/point_of_sale/static/tests/unit/test_ComponentRegistry.js"></script>
|
||||
<script type="text/javascript" src="/point_of_sale/static/tests/unit/test_NumberBuffer.js"></script>
|
||||
<script type="text/javascript" src="/point_of_sale/static/tests/unit/test_ChromeWidgets.js"></script>
|
||||
<script type="text/javascript" src="/point_of_sale/static/tests/unit/test_ProductScreen.js"></script>
|
||||
<script type="text/javascript" src="/point_of_sale/static/tests/unit/test_PaymentScreen.js"></script>
|
||||
<script type="text/javascript" src="/point_of_sale/static/tests/unit/test_popups.js"></script>
|
||||
</template>
|
||||
|
||||
<template id="point_of_sale.qunit_suite">
|
||||
@@ -73,7 +39,7 @@
|
||||
|
||||
<t t-call="point_of_sale.assets_common" />
|
||||
<t t-call="point_of_sale.qunit_suite_assets" />
|
||||
<t t-call="point_of_sale.qunit_suite_tests" />
|
||||
<t t-call-assets="point_of_sale.qunit_suite_tests" />
|
||||
</t>
|
||||
|
||||
<div id="qunit"/>
|
||||
|
||||
@@ -23,7 +23,6 @@ This module adds several features to the Point of Sale that are specific to rest
|
||||
'views/pos_order_views.xml',
|
||||
'views/pos_restaurant_views.xml',
|
||||
'views/pos_config_views.xml',
|
||||
'views/pos_restaurant_templates.xml',
|
||||
],
|
||||
'demo': [
|
||||
'data/pos_restaurant_demo.xml',
|
||||
@@ -69,6 +68,9 @@ This module adds several features to the Point of Sale that are specific to rest
|
||||
'web.assets_tests': [
|
||||
'pos_restaurant/static/tests/tours/**/*',
|
||||
],
|
||||
'point_of_sale.qunit_suite_tests': [
|
||||
'pos_restaurant/static/tests/unit/**/*',
|
||||
],
|
||||
'web.assets_qweb': [
|
||||
'pos_restaurant/static/src/xml/**/*',
|
||||
],
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<template id="pos_restaurant.qunit_suite_tests" inherit_id="point_of_sale.qunit_suite_tests">
|
||||
<xpath expr="." position="inside">
|
||||
<script type="text/javascript" src="/pos_restaurant/static/tests/unit/test_FloorScreen.js"></script>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
</odoo>
|
||||
@@ -1,11 +1,10 @@
|
||||
odoo.define('product.pricelist.report.tests', function (require) {
|
||||
"use strict";
|
||||
const core = require('web.core');
|
||||
const GeneratePriceList = require('product.generate_pricelist').GeneratePriceList;
|
||||
const NotificationService = require('web.NotificationService');
|
||||
const testUtils = require('web.test_utils');
|
||||
const createActionManager = testUtils.createActionManager;
|
||||
const testUtilsMock = require('web.test_utils_mock');
|
||||
|
||||
const { createWebClient, getActionManagerTestConfig, doAction } = require('@web/../tests/webclient/actions/helpers');
|
||||
let testConfig;
|
||||
|
||||
QUnit.module('Product Pricelist', {
|
||||
beforeEach: function () {
|
||||
@@ -32,6 +31,8 @@ QUnit.module('Product Pricelist', {
|
||||
}]
|
||||
}
|
||||
};
|
||||
testConfig = getActionManagerTestConfig();
|
||||
Object.assign(testConfig.serverData, {models: this.data});
|
||||
},
|
||||
}, function () {
|
||||
QUnit.test('Pricelist Client Action', async function (assert) {
|
||||
@@ -50,21 +51,14 @@ QUnit.module('Product Pricelist', {
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
const mockRPC = (route, args) => {
|
||||
if (route === '/web/dataset/call_kw/report.product.report_pricelist/get_html') {
|
||||
return Promise.resolve("");
|
||||
}
|
||||
};
|
||||
|
||||
const actionManager = await createActionManager({
|
||||
data: this.data,
|
||||
mockRPC: function(route, args) {
|
||||
if (route === '/web/dataset/call_kw/report.product.report_pricelist/get_html') {
|
||||
return Promise.resolve("");
|
||||
}
|
||||
return this._super(route, args);
|
||||
},
|
||||
services: {
|
||||
notification: NotificationService,
|
||||
},
|
||||
});
|
||||
|
||||
await actionManager.doAction({
|
||||
const webClient = await createWebClient({ testConfig, mockRPC });
|
||||
await doAction(webClient, {
|
||||
id: 1,
|
||||
name: 'Generate Pricelist',
|
||||
tag: 'generate_pricelist',
|
||||
@@ -78,7 +72,7 @@ QUnit.module('Product Pricelist', {
|
||||
});
|
||||
|
||||
// checking default pricelist
|
||||
assert.strictEqual(actionManager.$('.o_field_many2one input').val(), "Public Pricelist",
|
||||
assert.strictEqual($(webClient.el).find('.o_field_many2one input').val(), "Public Pricelist",
|
||||
"should have default pricelist");
|
||||
|
||||
// changing pricelist
|
||||
@@ -86,28 +80,28 @@ QUnit.module('Product Pricelist', {
|
||||
await testUtils.fields.many2one.clickItem("pricelist_id", "Test");
|
||||
|
||||
// check wherther pricelist value has been updated or not. along with that check default quantities should be there.
|
||||
assert.strictEqual(actionManager.$('.o_field_many2one input').val(), "Test",
|
||||
assert.strictEqual($(webClient.el).find('.o_field_many2one input').val(), "Test",
|
||||
"After pricelist change, the pricelist_id field should be updated");
|
||||
assert.strictEqual(actionManager.$('.o_badges > .badge').length, 3,
|
||||
assert.strictEqual($(webClient.el).find('.o_badges > .badge').length, 3,
|
||||
"There should be 3 default Quantities");
|
||||
|
||||
// existing quantity can not be added.
|
||||
await testUtils.dom.click(actionManager.$('.o_add_qty'));
|
||||
await testUtils.dom.click($(webClient.el).find('.o_add_qty'));
|
||||
let notificationElement = document.body.querySelector('.o_notification_manager .o_notification.bg-info');
|
||||
assert.strictEqual(notificationElement.querySelector('.o_notification_content').textContent,
|
||||
"Quantity already present (1).", "Existing Quantity can not be added");
|
||||
|
||||
// adding few more quantities to check.
|
||||
actionManager.$('.o_product_qty').val(2);
|
||||
$(webClient.el).find('.o_product_qty').val(2);
|
||||
Qty.push(2);
|
||||
await testUtils.dom.click(actionManager.$('.o_add_qty'));
|
||||
actionManager.$('.o_product_qty').val(3);
|
||||
await testUtils.dom.click($(webClient.el).find('.o_add_qty'));
|
||||
$(webClient.el).find('.o_product_qty').val(3);
|
||||
Qty.push(3);
|
||||
await testUtils.dom.click(actionManager.$('.o_add_qty'));
|
||||
await testUtils.dom.click($(webClient.el).find('.o_add_qty'));
|
||||
|
||||
// should not be added more then 5 quantities.
|
||||
actionManager.$('.o_product_qty').val(4);
|
||||
await testUtils.dom.click(actionManager.$('.o_add_qty'));
|
||||
$(webClient.el).find('.o_product_qty').val(4);
|
||||
await testUtils.dom.click($(webClient.el).find('.o_add_qty'));
|
||||
|
||||
notificationElement = document.body.querySelector('.o_notification_manager .o_notification.bg-warning');
|
||||
assert.strictEqual(notificationElement.querySelector('.o_notification_content').textContent,
|
||||
@@ -116,15 +110,15 @@ QUnit.module('Product Pricelist', {
|
||||
|
||||
// removing all the quantities should work
|
||||
Qty.pop(10);
|
||||
await testUtils.dom.click(actionManager.$('.o_badges .badge:contains("10") .o_remove_qty'));
|
||||
await testUtils.dom.click($(webClient.el).find('.o_badges .badge:contains("10") .o_remove_qty'));
|
||||
Qty.pop(5);
|
||||
await testUtils.dom.click(actionManager.$('.o_badges .badge:contains("5") .o_remove_qty'));
|
||||
await testUtils.dom.click($(webClient.el).find('.o_badges .badge:contains("5") .o_remove_qty'));
|
||||
Qty.pop(3);
|
||||
await testUtils.dom.click(actionManager.$('.o_badges .badge:contains("3") .o_remove_qty'));
|
||||
await testUtils.dom.click($(webClient.el).find('.o_badges .badge:contains("3") .o_remove_qty'));
|
||||
Qty.pop(2);
|
||||
await testUtils.dom.click(actionManager.$('.o_badges .badge:contains("2") .o_remove_qty'));
|
||||
await testUtils.dom.click($(webClient.el).find('.o_badges .badge:contains("2") .o_remove_qty'));
|
||||
Qty.pop(1);
|
||||
await testUtils.dom.click(actionManager.$('.o_badges .badge:contains("1") .o_remove_qty'));
|
||||
await testUtils.dom.click($(webClient.el).find('.o_badges .badge:contains("1") .o_remove_qty'));
|
||||
|
||||
assert.verifySteps([
|
||||
'field_changed',
|
||||
@@ -138,7 +132,6 @@ QUnit.module('Product Pricelist', {
|
||||
]);
|
||||
|
||||
testUtils.mock.unpatch(GeneratePriceList);
|
||||
actionManager.destroy();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ tour.register('sale_timesheet_tour', {
|
||||
actions.text('Service Product (Prepaid Hours)', this.$anchor.find('input'));
|
||||
},
|
||||
}, {
|
||||
trigger: 'ul.ui-menu.ui-widget.ui-autocomplete > li:first-child > a',
|
||||
trigger: 'ul.ui-menu.ui-widget.ui-autocomplete > li:first-child > a:contains(Service Product (Prepaid Hours))',
|
||||
content: 'Select the prepaid service product in the autocomplete dropdown',
|
||||
run: 'click',
|
||||
}, {
|
||||
@@ -155,11 +155,11 @@ tour.register('sale_timesheet_tour', {
|
||||
console.error('The quantity delivered on this Sales Order Item should be equal to 1.00 hour. qtyDelivered = ' + qtyDelivered);
|
||||
},
|
||||
}, {
|
||||
trigger: 'a[data-menu-xmlid="project.menu_project_config"]',
|
||||
trigger: 'button[data-menu-xmlid="project.menu_project_config"]',
|
||||
content: 'Click on the Configuration menu.',
|
||||
run: 'click',
|
||||
}, {
|
||||
trigger: 'a[role="menuitem"][data-menu-xmlid="project.menu_projects_config"]',
|
||||
trigger: 'li[data-menu-xmlid="project.menu_projects_config"]',
|
||||
content: 'Select Configuration > Projects.',
|
||||
run: 'click',
|
||||
}, {
|
||||
@@ -241,7 +241,7 @@ tour.register('sale_timesheet_tour', {
|
||||
trigger: '.o_form_button_save',
|
||||
content: 'Save Project',
|
||||
}, {
|
||||
trigger: 'a[role="menuitem"][data-menu-xmlid="project.menu_projects"]',
|
||||
trigger: 'li[data-menu-xmlid="project.menu_projects"]',
|
||||
content: 'Select Projects'
|
||||
}, {
|
||||
trigger: 'div.o_kanban_primary_left :contains("Project for Freeman")',
|
||||
|
||||
@@ -7,7 +7,9 @@ odoo.define('stock.stock_traceability_report_backend_tests', function (require)
|
||||
const testUtils = require('web.test_utils');
|
||||
const { patch, unpatch } = require('web.utils');
|
||||
|
||||
const { createActionManager, dom: domUtils } = testUtils;
|
||||
const { dom: domUtils } = testUtils;
|
||||
const { legacyExtraNextTick } = require("@web/../tests/helpers/utils");
|
||||
const { createWebClient, getActionManagerTestConfig, doAction } = require('@web/../tests/webclient/actions/helpers');
|
||||
|
||||
/**
|
||||
* Helper function to instantiate a stock report action.
|
||||
@@ -91,49 +93,48 @@ odoo.define('stock.stock_traceability_report_backend_tests', function (require)
|
||||
this.__superMounted(...arguments);
|
||||
},
|
||||
});
|
||||
const models = {
|
||||
partner: {
|
||||
fields: {
|
||||
display_name: { string: "Displayed name", type: "char" },
|
||||
},
|
||||
records: [
|
||||
{id: 1, display_name: "Genda Swami"},
|
||||
],
|
||||
},
|
||||
};
|
||||
let testConfig = getActionManagerTestConfig();
|
||||
Object.assign(testConfig.serverData, { models });
|
||||
|
||||
const actionManager = await createActionManager({
|
||||
actions: [
|
||||
{
|
||||
id: 42,
|
||||
name: "Stock report",
|
||||
tag: 'stock_report_generic',
|
||||
type: 'ir.actions.client',
|
||||
context: {},
|
||||
params: {},
|
||||
},
|
||||
],
|
||||
archs: {
|
||||
'partner,false,form': '<form><field name="display_name"/></form>',
|
||||
'partner,false,search': '<search></search>',
|
||||
},
|
||||
data: {
|
||||
partner: {
|
||||
fields: {
|
||||
display_name: { string: "Displayed name", type: "char" },
|
||||
},
|
||||
records: [
|
||||
{id: 1, display_name: "Genda Swami"},
|
||||
],
|
||||
},
|
||||
},
|
||||
testConfig.serverData.views = {
|
||||
'partner,false,form': '<form><field name="display_name"/></form>',
|
||||
'partner,false,search': '<search></search>',
|
||||
};
|
||||
testConfig.serverData.actions[42] = {
|
||||
id: 42,
|
||||
name: "Stock report",
|
||||
tag: 'stock_report_generic',
|
||||
type: 'ir.actions.client',
|
||||
context: {},
|
||||
params: {},
|
||||
};
|
||||
const webClient = await createWebClient({
|
||||
testConfig,
|
||||
mockRPC: function (route) {
|
||||
if (route === '/web/dataset/call_kw/stock.traceability.report/get_html') {
|
||||
return Promise.resolve({
|
||||
html: '<a class="o_stock_reports_web_action" href="#" data-active-id="1" data-res-model="partner">Go to form view</a>',
|
||||
});
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
intercepts: {
|
||||
do_action: ev => actionManager.doAction(ev.data.action, ev.data.options),
|
||||
},
|
||||
});
|
||||
|
||||
await actionManager.doAction(42);
|
||||
await domUtils.click(actionManager.$('.o_stock_reports_web_action'));
|
||||
await domUtils.click(actionManager.$('.breadcrumb-item:first'));
|
||||
actionManager.destroy();
|
||||
await doAction(webClient, 42);
|
||||
await domUtils.click($(webClient.el).find('.o_stock_reports_web_action'));
|
||||
await legacyExtraNextTick();
|
||||
await domUtils.click($(webClient.el).find('.breadcrumb-item:first'));
|
||||
await legacyExtraNextTick();
|
||||
webClient.destroy();
|
||||
|
||||
assert.verifySteps([
|
||||
'mounted 1',
|
||||
|
||||
@@ -18,7 +18,7 @@ Odoo Web tours.
|
||||
],
|
||||
'assets': {
|
||||
'web.assets_qweb': [
|
||||
'web_tour/static/src/xml/debug_manager.xml',
|
||||
'web_tour/static/src/debug/tour_dialog_component.xml',
|
||||
],
|
||||
'web.assets_common': [
|
||||
'web_tour/static/src/scss/**/*',
|
||||
@@ -30,7 +30,9 @@ Odoo Web tours.
|
||||
'web_tour/static/src/js/tour_utils.js',
|
||||
],
|
||||
'web.assets_backend': [
|
||||
'web_tour/static/src/js/debug_manager.js',
|
||||
'web_tour/static/src/debug/debug_manager.js',
|
||||
'web_tour/static/src/debug/tour_dialog_component.js',
|
||||
'web_tour/static/src/services/*.js',
|
||||
],
|
||||
'web.assets_frontend': [
|
||||
'web_tour/static/src/js/public/**/*',
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { registry } from "@web/core/registry";
|
||||
import ToursDialog from "@web_tour/debug/tour_dialog_component";
|
||||
import utils from "web_tour.utils";
|
||||
|
||||
function debugDisableTourItem(env) {
|
||||
if (!env.services.user.isSystem) {
|
||||
return {};
|
||||
}
|
||||
const activeTours = env.services.tour.getActiveTours();
|
||||
if (activeTours.length === 0) {
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
type: "item",
|
||||
description: env._t("Disable Tours"),
|
||||
callback: async () => {
|
||||
await env.services.orm.call("web_tour.tour", "consume", [activeTours]);
|
||||
for (const tourName of activeTours) {
|
||||
browser.localStorage.removeItem(utils.get_debugging_key(tourName));
|
||||
}
|
||||
browser.location.reload();
|
||||
},
|
||||
sequence: 33,
|
||||
};
|
||||
}
|
||||
|
||||
function debugStartTourItem(env) {
|
||||
if (!env.services.user.isSystem) {
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
type: "item",
|
||||
description: env._t("Start Tour"),
|
||||
callback: async () => {
|
||||
env.services.dialog.open(ToursDialog);
|
||||
},
|
||||
sequence: 32,
|
||||
};
|
||||
}
|
||||
|
||||
registry
|
||||
.category("debug")
|
||||
.add("web_tour.start_tour", debugStartTourItem)
|
||||
.add("web_tour.disable_tour", debugDisableTourItem);
|
||||
@@ -0,0 +1,43 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { useService } from "@web/core/service_hook";
|
||||
|
||||
export default class ToursDialog extends owl.Component {
|
||||
setup() {
|
||||
this.tourService = useService("tour");
|
||||
this.onboardingTours = this.tourService.getOnboardingTours();
|
||||
this.testingTours = this.tourService.getTestingTours();
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Getters
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
get dialogTitle() {
|
||||
return this.env._t("Tours");
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Resets the given tour to its initial step, in onboarding mode.
|
||||
*
|
||||
* @private
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
_onStartTour(ev) {
|
||||
this.tourService.reset(ev.target.dataset.name);
|
||||
}
|
||||
/**
|
||||
* Starts the given tour in test mode.
|
||||
*
|
||||
* @private
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
_onTestTour(ev) {
|
||||
this.tourService.run(ev.target.dataset.name);
|
||||
}
|
||||
}
|
||||
ToursDialog.template = "web_tour.ToursDialog";
|
||||
@@ -0,0 +1,61 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<t t-name="web_tour.ToursDialog" owl="1">
|
||||
<Dialog title="dialogTitle">
|
||||
<div>
|
||||
<t t-call="web_tour.ToursDialog.Table">
|
||||
<t t-set="caption">Onboarding tours</t>
|
||||
<t t-set="tours" t-value="onboardingTours"/>
|
||||
</t>
|
||||
<t t-if="testingTours.length" t-call="web_tour.ToursDialog.Table">
|
||||
<t t-set="caption">Testing tours</t>
|
||||
<t t-set="tours" t-value="testingTours"/>
|
||||
</t>
|
||||
</div>
|
||||
</Dialog>
|
||||
</t>
|
||||
|
||||
<t t-name="web_tour.ToursDialog.Table" owl="1">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-striped">
|
||||
<caption style="caption-side: top; font-size: 14px">
|
||||
<t t-esc="caption"/>
|
||||
</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Sequence</th>
|
||||
<th width="50%">Name</th>
|
||||
<th width="50%">Path</th>
|
||||
<th>Start</th>
|
||||
<th>Test</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr t-foreach="tours" t-as="tour">
|
||||
<td><t t-esc="tour.sequence"/></td>
|
||||
<td><t t-esc="tour.name"/></td>
|
||||
<td><t t-esc="tour.url"/></td>
|
||||
<td>
|
||||
<button type="button"
|
||||
class="btn btn-primary fa fa-play o_start_tour"
|
||||
t-on-click.prevent="_onStartTour"
|
||||
t-att-data-name="tour.name"
|
||||
aria-label="Start tour"
|
||||
title="Start tour"/>
|
||||
</td>
|
||||
<td>
|
||||
<button type="button"
|
||||
class="btn btn-primary fa fa-cogs o_test_tour"
|
||||
t-on-click.prevent="_onTestTour"
|
||||
t-att-data-name="tour.name"
|
||||
aria-label="Test tour"
|
||||
title="Test tour"/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -1,81 +0,0 @@
|
||||
odoo.define('web_tour.DebugManager.Backend', function (require) {
|
||||
"use strict";
|
||||
|
||||
var core = require("web.core");
|
||||
var DebugManager = require('web.DebugManager.Backend');
|
||||
var Dialog = require("web.Dialog");
|
||||
var local_storage = require('web.local_storage');
|
||||
|
||||
var tour = require('web_tour.tour');
|
||||
var utils = require('web_tour.utils');
|
||||
|
||||
var get_debugging_key = utils.get_debugging_key;
|
||||
|
||||
function get_active_tours () {
|
||||
return _.difference(_.keys(tour.tours), tour.consumed_tours);
|
||||
}
|
||||
|
||||
DebugManager.include({
|
||||
start: function () {
|
||||
this.consume_tours_enabled = get_active_tours().length > 0;
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
consume_tours: function () {
|
||||
var active_tours = get_active_tours();
|
||||
if (active_tours.length > 0) { // tours might have been consumed meanwhile
|
||||
this._rpc({
|
||||
model: 'web_tour.tour',
|
||||
method: 'consume',
|
||||
args: [active_tours],
|
||||
})
|
||||
.then(function () {
|
||||
for (const tourName of active_tours) {
|
||||
local_storage.removeItem(get_debugging_key(tourName));
|
||||
}
|
||||
window.location.reload();
|
||||
});
|
||||
}
|
||||
},
|
||||
start_tour: async function () {
|
||||
const tours = Object.values(tour.tours).sort((t1, t2) => {
|
||||
return (t1.sequence - t2.sequence) || (t1.name < t2.name ? -1 : 1);
|
||||
});
|
||||
const dialog = new Dialog(this, {
|
||||
title: 'Tours',
|
||||
$content: core.qweb.render('web_tour.ToursDialog', {
|
||||
onboardingTours: tours.filter(t => !t.test),
|
||||
testingTours: tours.filter(t => t.test),
|
||||
}),
|
||||
});
|
||||
await dialog.open().opened();
|
||||
dialog.$('.o_start_tour').on('click', this._onStartTour.bind(this));
|
||||
dialog.$('.o_test_tour').on('click', this._onTestTour.bind(this));
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Resets the given tour to its initial step, in onboarding mode.
|
||||
*
|
||||
* @private
|
||||
* @param {MouseEvent}
|
||||
*/
|
||||
_onStartTour(ev) {
|
||||
ev.preventDefault();
|
||||
tour.reset($(ev.target).data('name'));
|
||||
},
|
||||
/**
|
||||
* Starts the given tour in test mode.
|
||||
*
|
||||
* @private
|
||||
* @param {MouseEvent}
|
||||
*/
|
||||
_onTestTour(ev) {
|
||||
ev.preventDefault();
|
||||
tour.run($(ev.target).data('name'));
|
||||
},
|
||||
});
|
||||
|
||||
});
|
||||
@@ -7,7 +7,6 @@ var local_storage = require('web.local_storage');
|
||||
var mixins = require('web.mixins');
|
||||
var utils = require('web_tour.utils');
|
||||
var TourStepUtils = require('web_tour.TourStepUtils');
|
||||
var RainbowMan = require('web.RainbowMan');
|
||||
var RunningTourActionHelper = require('web_tour.RunningTourActionHelper');
|
||||
var ServicesMixin = require('web.ServicesMixin');
|
||||
var session = require('web.session');
|
||||
@@ -433,7 +432,7 @@ return core.Class.extend(mixins.EventDispatcherMixin, ServicesMixin, {
|
||||
message = _t('<strong><b>Good job!</b> You went through all steps of this tour.</strong>');
|
||||
}
|
||||
const fadeout = this.tours[tour_name].fadeout;
|
||||
new RainbowMan({message, fadeout}).appendTo(this.$body);
|
||||
core.bus.trigger('show-effect', {type: "rainbowman", message, fadeout});
|
||||
}
|
||||
this.tours[tour_name].current_step = 0;
|
||||
local_storage.removeItem(get_step_key(tour_name));
|
||||
|
||||
@@ -29,7 +29,7 @@ return core.Class.extend({
|
||||
showAppsMenuItem() {
|
||||
return {
|
||||
edition: 'community',
|
||||
trigger: '.o_menu_apps a',
|
||||
trigger: '.o_navbar_apps_menu button',
|
||||
auto: true,
|
||||
position: 'bottom',
|
||||
};
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import tourManager from "web_tour.tour";
|
||||
|
||||
export const tourService = {
|
||||
start() {
|
||||
/**
|
||||
* @private
|
||||
* @returns {Object} All the tours as a map
|
||||
*/
|
||||
function _getAllTourMap() {
|
||||
return tourManager.tours;
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @returns {Object} All the active tours as a map
|
||||
*/
|
||||
function _getActiveTourMap() {
|
||||
return Object.fromEntries(
|
||||
Object.entries(_getAllTourMap()).filter(
|
||||
([key, value]) => !tourManager.consumed_tours.includes(key)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @returns {Array} Takes an Object (map) of tours and returns all the values
|
||||
*/
|
||||
function _fromTourMapToArray(tourMap) {
|
||||
return Object.values(tourMap).sort((t1, t2) => {
|
||||
return t1.sequence - t2.sequence || (t1.name < t2.name ? -1 : 1);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Array} All the tours
|
||||
*/
|
||||
function getAllTours() {
|
||||
return _fromTourMapToArray(_getAllTourMap());
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Array} All the active tours
|
||||
*/
|
||||
function getActiveTours() {
|
||||
return _fromTourMapToArray(_getActiveTourMap());
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Array} The onboarding tours
|
||||
*/
|
||||
function getOnboardingTours() {
|
||||
return getAllTours().filter((t) => !t.test);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Array} The testing tours
|
||||
*/
|
||||
function getTestingTours() {
|
||||
return getAllTours().filter((t) => t.test);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} tourName
|
||||
* Run a tour
|
||||
*/
|
||||
function run(tourName) {
|
||||
return tourManager.run(tourName);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} tourName
|
||||
* Reset a tour
|
||||
*/
|
||||
function reset(tourName) {
|
||||
return tourManager.reset(tourName);
|
||||
}
|
||||
|
||||
return {
|
||||
getAllTours,
|
||||
getActiveTours,
|
||||
getOnboardingTours,
|
||||
getTestingTours,
|
||||
run,
|
||||
reset,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
registry.category("services").add("tour", tourService);
|
||||
@@ -1,54 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<t t-extend="WebClient.DebugManager.Backend">
|
||||
<t t-jquery="a[data-action='select_view']" t-operation="after">
|
||||
<t t-if="manager._is_admin">
|
||||
<a t-if="manager.consume_tours_enabled" href="#" class="dropdown-item" data-action="consume_tours">Disable Tours</a>
|
||||
<a href="#" class="dropdown-item" data-action="start_tour">Start Tour</a>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
<t t-name="web_tour.ToursDialog">
|
||||
<div>
|
||||
<t t-call="web_tour.ToursDialog.Table">
|
||||
<t t-set="caption" t-value="'Onboarding tours'"/>
|
||||
<t t-set="tours" t-value="onboardingTours"/>
|
||||
</t>
|
||||
<t t-if="testingTours.length" t-call="web_tour.ToursDialog.Table">
|
||||
<t t-set="caption" t-value="'Testing tours'"/>
|
||||
<t t-set="tours" t-value="testingTours"/>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="web_tour.ToursDialog.Table">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-striped">
|
||||
<caption style="caption-side: top; font-size: 14px">
|
||||
<t t-esc="caption"/>
|
||||
</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Sequence</th>
|
||||
<th width="50%">Name</th>
|
||||
<th width="50%">Path</th>
|
||||
<th>Start</th>
|
||||
<th>Test</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr t-foreach="tours" t-as="tour">
|
||||
<td><t t-esc="tour.sequence"/></td>
|
||||
<td><t t-esc="tour.name"/></td>
|
||||
<td><t t-esc="tour.url"/></td>
|
||||
<td><button type="button" class="btn btn-primary fa fa-play o_start_tour" t-att-data-name="tour.name" aria-label="Start tour" title="Start tour"/></td>
|
||||
<td><button type="button" class="btn btn-primary fa fa-cogs o_test_tour" t-att-data-name="tour.name" aria-label="Test tour" title="Test tour"/></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -185,7 +185,7 @@
|
||||
'website.assets_editor': [
|
||||
('include', 'web._assets_helpers'),
|
||||
'web/static/lib/bootstrap/scss/_variables.scss',
|
||||
'web/static/src/scss/ace.scss',
|
||||
'web/static/src/legacy/scss/ace.scss',
|
||||
'website/static/src/scss/website.editor.ui.scss',
|
||||
'website/static/src/scss/website.theme_install.scss',
|
||||
'website/static/src/js/menu/content.js',
|
||||
|
||||
@@ -1,22 +1,31 @@
|
||||
odoo.define("website.tour.backend_dashboard", function (require) {
|
||||
"use strict";
|
||||
"use strict";
|
||||
|
||||
var tour = require("web_tour.tour");
|
||||
var tour = require("web_tour.tour");
|
||||
|
||||
tour.register("backend_dashboard", {
|
||||
test: true,
|
||||
url: "/web",
|
||||
}, [tour.stepUtils.showAppsMenuItem(),
|
||||
tour.register(
|
||||
"backend_dashboard",
|
||||
{
|
||||
trigger: 'a[data-menu-xmlid="website.menu_website_configuration"]',
|
||||
}, {
|
||||
trigger: '.dropdown-toggle[data-menu-xmlid="website.menu_dashboard"]',
|
||||
}, {
|
||||
trigger: '.dropdown-item[data-menu-xmlid="website.menu_website_google_analytics"]',
|
||||
}, {
|
||||
// Visits section should always be present even when empty / not hooked to anything
|
||||
trigger: 'h2:contains("Visits")',
|
||||
content: "Check if dashboard loads",
|
||||
run: function () {}
|
||||
}]);
|
||||
test: true,
|
||||
url: "/web",
|
||||
},
|
||||
[
|
||||
tour.stepUtils.showAppsMenuItem(),
|
||||
{
|
||||
trigger: '.o_app[data-menu-xmlid="website.menu_website_configuration"]',
|
||||
},
|
||||
{
|
||||
trigger: 'button[data-menu-xmlid="website.menu_dashboard"]',
|
||||
},
|
||||
{
|
||||
trigger: 'li[data-menu-xmlid="website.menu_website_google_analytics"]',
|
||||
},
|
||||
{
|
||||
// Visits section should always be present even when empty / not hooked to anything
|
||||
trigger: 'h2:contains("Visits")',
|
||||
content: "Check if dashboard loads",
|
||||
run: function () {},
|
||||
},
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
@@ -7,7 +7,7 @@ const options = require('web_editor.snippets.options');
|
||||
require('website.editor.snippets.options');
|
||||
|
||||
if (!$('.website_blog').length) {
|
||||
return Promise.reject("DOM doesn't contain '.website_blog'");
|
||||
return;
|
||||
}
|
||||
|
||||
const NEW_TAG_PREFIX = 'new-blog-tag-';
|
||||
|
||||
@@ -6,9 +6,15 @@ var view_registry = require('web.view_registry');
|
||||
|
||||
var createView = testUtils.createView;
|
||||
var BaseSettingsView = view_registry.get('base_settings');
|
||||
var createActionManager = testUtils.createActionManager;
|
||||
|
||||
const { legacyExtraNextTick } = require("@web/../tests/helpers/utils");
|
||||
const {
|
||||
createWebClient,
|
||||
doAction,
|
||||
getActionManagerTestConfig,
|
||||
} = require("@web/../tests/webclient/actions/helpers");
|
||||
|
||||
let testConfig;
|
||||
QUnit.module('base_settings_tests', {
|
||||
beforeEach: function () {
|
||||
this.data = {
|
||||
@@ -23,6 +29,9 @@ QUnit.module('base_settings_tests', {
|
||||
fields: {}
|
||||
}
|
||||
};
|
||||
testConfig = getActionManagerTestConfig();
|
||||
const models = Object.assign(testConfig.serverData.models, this.data);
|
||||
Object.assign(testConfig.serverData, { models });
|
||||
}
|
||||
}, function () {
|
||||
|
||||
@@ -91,160 +100,189 @@ QUnit.module('base_settings_tests', {
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('settings views does not read existing id when coming back in breadcrumbs', async function (assert) {
|
||||
assert.expect(8);
|
||||
QUnit.test(
|
||||
"settings views does not read existing id when coming back in breadcrumbs",
|
||||
async function (assert) {
|
||||
assert.expect(8);
|
||||
|
||||
var actions = [{
|
||||
id: 1,
|
||||
name: 'Settings view',
|
||||
res_model: 'res.config.settings',
|
||||
type: 'ir.actions.act_window',
|
||||
views: [[1, 'form']],
|
||||
}, {
|
||||
id: 4,
|
||||
name: 'Other action',
|
||||
res_model: 'task',
|
||||
type: 'ir.actions.act_window',
|
||||
views: [[2, 'list']],
|
||||
}];
|
||||
var archs = {
|
||||
'res.config.settings,1,form': '<form string="Settings" js_class="base_settings">' +
|
||||
'<div class="app_settings_block" string="CRM" data-key="crm">' +
|
||||
'<button name="4" string="Execute action" type="action"/>' +
|
||||
'</div>' +
|
||||
'</form>',
|
||||
'task,2,list': '<tree><field name="display_name"/></tree>',
|
||||
'res.config.settings,false,search': '<search></search>',
|
||||
'task,false,search': '<search></search>',
|
||||
};
|
||||
testConfig.serverData.actions = {
|
||||
1: {
|
||||
id: 1,
|
||||
name: "Settings view",
|
||||
res_model: "res.config.settings",
|
||||
type: "ir.actions.act_window",
|
||||
views: [[1, "form"]],
|
||||
},
|
||||
4: {
|
||||
id: 4,
|
||||
name: "Other action",
|
||||
res_model: "task",
|
||||
type: "ir.actions.act_window",
|
||||
views: [[2, "list"]],
|
||||
},
|
||||
};
|
||||
|
||||
var actionManager = await createActionManager({
|
||||
actions: actions,
|
||||
archs: archs,
|
||||
data: this.data,
|
||||
mockRPC: function (route, args) {
|
||||
testConfig.serverData.views = {
|
||||
"res.config.settings,1,form":
|
||||
`<form string="Settings" js_class="base_settings">
|
||||
<div class="app_settings_block" string="CRM" data-key="crm">
|
||||
<button name="4" string="Execute action" type="action"/>
|
||||
</div>
|
||||
</form>`,
|
||||
"task,2,list": '<tree><field name="display_name"/></tree>',
|
||||
"res.config.settings,false,search": "<search></search>",
|
||||
"task,false,search": "<search></search>",
|
||||
};
|
||||
|
||||
const mockRPC = (route, args) => {
|
||||
if (args.method) {
|
||||
assert.step(args.method);
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
await actionManager.doAction(1);
|
||||
await testUtils.nextTick();
|
||||
await testUtils.dom.click(actionManager.$('button[name="4"]'));
|
||||
await testUtils.dom.click($('.o_control_panel .breadcrumb-item a'));
|
||||
assert.hasClass(actionManager.$('.o_form_view'), 'o_form_editable');
|
||||
assert.verifySteps([
|
||||
'load_views', // initial setting action
|
||||
'onchange', // this is a setting view => create new record
|
||||
'create', // when we click on action button => save
|
||||
'read', // with save, we have a reload... (not necessary actually)
|
||||
'load_views', // for other action in breadcrumb,
|
||||
// with a searchread (not shown here since it is a route)
|
||||
'onchange', // when we come back, we want to restart from scratch
|
||||
]);
|
||||
const webClient = await createWebClient({ testConfig, mockRPC });
|
||||
|
||||
actionManager.destroy();
|
||||
});
|
||||
await doAction(webClient, 1);
|
||||
await testUtils.dom.click($(webClient.el).find('button[name="4"]'));
|
||||
await legacyExtraNextTick();
|
||||
await testUtils.dom.click($(".o_control_panel .breadcrumb-item a"));
|
||||
await legacyExtraNextTick();
|
||||
assert.hasClass($(webClient.el).find(".o_form_view"), "o_form_editable");
|
||||
assert.verifySteps([
|
||||
"load_views", // initial setting action
|
||||
"onchange", // this is a setting view => create new record
|
||||
"create", // when we click on action button => save
|
||||
"read", // with save, we have a reload... (not necessary actually)
|
||||
"load_views", // for other action in breadcrumb,
|
||||
// with a searchread (not shown here since it is a route)
|
||||
"onchange", // when we come back, we want to restart from scratch
|
||||
]);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test('clicking on any button in setting should show discard warning if setting form is dirty', async function (assert) {
|
||||
assert.expect(11);
|
||||
QUnit.test(
|
||||
"clicking on any button in setting should show discard warning if setting form is dirty",
|
||||
async function (assert) {
|
||||
assert.expect(11);
|
||||
|
||||
var actions = [{
|
||||
id: 1,
|
||||
name: 'Settings view',
|
||||
res_model: 'res.config.settings',
|
||||
type: 'ir.actions.act_window',
|
||||
views: [[1, 'form']],
|
||||
}, {
|
||||
id: 4,
|
||||
name: 'Other action',
|
||||
res_model: 'task',
|
||||
type: 'ir.actions.act_window',
|
||||
views: [[2, 'list']],
|
||||
}];
|
||||
var archs = {
|
||||
'res.config.settings,1,form': '<form string="Settings" js_class="base_settings">' +
|
||||
'<header>' +
|
||||
'<button string="Save" type="object" name="execute" class="oe_highlight" />' +
|
||||
'<button string="Cancel" type="object" name="cancel" class="oe_link" />' +
|
||||
'</header>' +
|
||||
'<div class="app_settings_block" string="CRM" data-key="crm">' +
|
||||
'<div class="row mt16 o_settings_container">'+
|
||||
'<div class="col-12 col-lg-6 o_setting_box">'+
|
||||
'<div class="o_setting_left_pane">' +
|
||||
'<field name="foo"/>'+
|
||||
'</div>'+
|
||||
'<div class="o_setting_right_pane">'+
|
||||
'<span class="o_form_label">Foo</span>'+
|
||||
'<div class="text-muted">'+
|
||||
'this is foo'+
|
||||
'</div>'+
|
||||
'</div>' +
|
||||
'</div>'+
|
||||
'</div>'+
|
||||
'<button name="4" string="Execute action" type="action"/>' +
|
||||
'</div>' +
|
||||
'</form>',
|
||||
'task,2,list': '<tree><field name="display_name"/></tree>',
|
||||
'res.config.settings,false,search': '<search></search>',
|
||||
'task,false,search': '<search></search>',
|
||||
};
|
||||
testConfig.serverData.actions = {
|
||||
1: {
|
||||
id: 1,
|
||||
name: "Settings view",
|
||||
res_model: "res.config.settings",
|
||||
type: "ir.actions.act_window",
|
||||
views: [[1, "form"]],
|
||||
},
|
||||
4: {
|
||||
id: 4,
|
||||
name: "Other action",
|
||||
res_model: "task",
|
||||
type: "ir.actions.act_window",
|
||||
views: [[2, "list"]],
|
||||
},
|
||||
};
|
||||
|
||||
var actionManager = await createActionManager({
|
||||
actions: actions,
|
||||
archs: archs,
|
||||
data: this.data,
|
||||
mockRPC: function (route, args) {
|
||||
if (route === '/web/dataset/call_button') {
|
||||
testConfig.serverData.views = {
|
||||
"res.config.settings,1,form":
|
||||
`<form string="Settings" js_class="base_settings">
|
||||
<header>
|
||||
<button string="Save" type="object" name="execute" class="oe_highlight" />
|
||||
<button string="Cancel" type="object" name="cancel" class="oe_link" />
|
||||
</header>
|
||||
<div class="app_settings_block" string="CRM" data-key="crm">
|
||||
<div class="row mt16 o_settings_container">
|
||||
<div class="col-12 col-lg-6 o_setting_box">
|
||||
<div class="o_setting_left_pane">
|
||||
<field name="foo"/>
|
||||
</div>
|
||||
<div class="o_setting_right_pane">
|
||||
<span class="o_form_label">Foo</span>
|
||||
<div class="text-muted">
|
||||
this is foo
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button name="4" string="Execute action" type="action"/>
|
||||
</div>
|
||||
</form>`,
|
||||
"task,2,list": '<tree><field name="display_name"/></tree>',
|
||||
"res.config.settings,false,search": "<search></search>",
|
||||
"task,false,search": "<search></search>",
|
||||
};
|
||||
|
||||
const mockRPC = (route, args) => {
|
||||
if (route === "/web/dataset/call_button") {
|
||||
if (args.method === "execute") {
|
||||
assert.ok("execute method called");
|
||||
return Promise.resolve(true);
|
||||
return true;
|
||||
}
|
||||
if (args.method === "cancel") {
|
||||
assert.ok("cancel method called");
|
||||
return Promise.resolve(true);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
await actionManager.doAction(1);
|
||||
assert.containsNone(actionManager, '.o_field_boolean input:checked',
|
||||
"checkbox should not be checked");
|
||||
const webClient = await createWebClient({ testConfig, mockRPC });
|
||||
|
||||
await testUtils.dom.click(actionManager.$("input[type='checkbox']"));
|
||||
assert.containsOnce(actionManager, '.o_field_boolean input:checked',
|
||||
"checkbox should be checked");
|
||||
await doAction(webClient, 1);
|
||||
assert.containsNone(
|
||||
webClient,
|
||||
".o_field_boolean input:checked",
|
||||
"checkbox should not be checked"
|
||||
);
|
||||
|
||||
await testUtils.dom.click(actionManager.$('button[name="4"]'));
|
||||
assert.containsOnce(document.body, '.modal', "should open a warning dialog");
|
||||
await testUtils.dom.click($(webClient.el).find("input[type='checkbox']"));
|
||||
assert.containsOnce(
|
||||
webClient,
|
||||
".o_field_boolean input:checked",
|
||||
"checkbox should be checked"
|
||||
);
|
||||
|
||||
await testUtils.dom.click($('.modal button:contains(Discard)'));
|
||||
assert.containsOnce(actionManager, '.o_list_view', "should be open list view");
|
||||
await testUtils.dom.click($(webClient.el).find('button[name="4"]'));
|
||||
await legacyExtraNextTick();
|
||||
assert.containsOnce(document.body, ".modal", "should open a warning dialog");
|
||||
|
||||
await testUtils.dom.click($('.o_control_panel .breadcrumb-item a'));
|
||||
assert.containsNone(actionManager, '.o_field_boolean input:checked',
|
||||
"checkbox should not be checked");
|
||||
await testUtils.dom.click($(".modal button:contains(Discard)"));
|
||||
await legacyExtraNextTick();
|
||||
assert.containsOnce(webClient, ".o_list_view", "should be open list view");
|
||||
|
||||
await testUtils.dom.click(actionManager.$("input[type='checkbox']"));
|
||||
await testUtils.dom.click(actionManager.$('button[name="4"]'));
|
||||
assert.containsOnce(document.body, '.modal', "should open a warning dialog");
|
||||
await testUtils.dom.click($(".o_control_panel .breadcrumb-item a"));
|
||||
await legacyExtraNextTick();
|
||||
assert.containsNone(
|
||||
webClient,
|
||||
".o_field_boolean input:checked",
|
||||
"checkbox should not be checked"
|
||||
);
|
||||
|
||||
await testUtils.dom.click($('.modal button:contains(Stay Here)'));
|
||||
assert.containsOnce(actionManager, '.o_form_view' ,"should be remain on form view");
|
||||
await testUtils.dom.click($(webClient.el).find("input[type='checkbox']"));
|
||||
await testUtils.dom.click($(webClient.el).find('button[name="4"]'));
|
||||
await legacyExtraNextTick();
|
||||
assert.containsOnce(document.body, ".modal", "should open a warning dialog");
|
||||
|
||||
await testUtils.dom.click(actionManager.$("button[name='execute']"));
|
||||
assert.containsNone(document.body, '.modal', "should not open a warning dialog");
|
||||
await testUtils.dom.click($(".modal button:contains(Stay Here)"));
|
||||
await legacyExtraNextTick();
|
||||
assert.containsOnce(webClient, ".o_form_view", "should be remain on form view");
|
||||
|
||||
await testUtils.dom.click(actionManager.$("input[type='checkbox']"));
|
||||
await testUtils.dom.click(actionManager.$("button[name='cancel']"));
|
||||
assert.containsNone(document.body, '.modal', "should not open a warning dialog");
|
||||
await testUtils.dom.click($(webClient.el).find("button[name='execute']"));
|
||||
await legacyExtraNextTick();
|
||||
assert.containsNone(
|
||||
document.body,
|
||||
".modal",
|
||||
"should not open a warning dialog"
|
||||
);
|
||||
|
||||
actionManager.destroy();
|
||||
});
|
||||
await testUtils.dom.click($(webClient.el).find("input[type='checkbox']"));
|
||||
await testUtils.dom.click($(webClient.el).find("button[name='cancel']"));
|
||||
await legacyExtraNextTick();
|
||||
assert.containsNone(
|
||||
document.body,
|
||||
".modal",
|
||||
"should not open a warning dialog"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test('settings view does not display other settings after reload', async function (assert) {
|
||||
assert.expect(2);
|
||||
@@ -365,72 +403,83 @@ QUnit.module('base_settings_tests', {
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('execute action from settings view with several actions in the breadcrumb', async function (assert) {
|
||||
// This commit fixes a race condition, that's why we artificially slow down a read rpc
|
||||
assert.expect(4);
|
||||
QUnit.test(
|
||||
"execute action from settings view with several actions in the breadcrumb",
|
||||
async function (assert) {
|
||||
// This commit fixes a race condition, that's why we artificially slow down a read rpc
|
||||
assert.expect(4);
|
||||
|
||||
const actions = [{
|
||||
id: 1,
|
||||
name: 'First action',
|
||||
res_model: 'task',
|
||||
type: 'ir.actions.act_window',
|
||||
views: [[1, 'list']],
|
||||
}, {
|
||||
id: 2,
|
||||
name: 'Settings view',
|
||||
res_model: 'res.config.settings',
|
||||
type: 'ir.actions.act_window',
|
||||
views: [[2, 'form']],
|
||||
}, {
|
||||
id: 3,
|
||||
name: 'Other action',
|
||||
res_model: 'task',
|
||||
type: 'ir.actions.act_window',
|
||||
views: [[3, 'list']],
|
||||
}];
|
||||
const archs = {
|
||||
'task,1,list': '<tree><field name="display_name"/></tree>',
|
||||
'res.config.settings,2,form': `
|
||||
<form string="Settings" js_class="base_settings">
|
||||
<div class="app_settings_block" string="CRM" data-key="crm">
|
||||
<button name="3" string="Execute action" type="action"/>
|
||||
</div>
|
||||
</form>`,
|
||||
'task,3,list': '<tree><field name="display_name"/></tree>',
|
||||
'res.config.settings,false,search': '<search></search>',
|
||||
'task,false,search': '<search></search>',
|
||||
};
|
||||
testConfig.serverData.actions = {
|
||||
1: {
|
||||
id: 1,
|
||||
name: "First action",
|
||||
res_model: "task",
|
||||
type: "ir.actions.act_window",
|
||||
views: [[1, "list"]],
|
||||
},
|
||||
2: {
|
||||
id: 2,
|
||||
name: "Settings view",
|
||||
res_model: "res.config.settings",
|
||||
type: "ir.actions.act_window",
|
||||
views: [[2, "form"]],
|
||||
},
|
||||
3: {
|
||||
id: 3,
|
||||
name: "Other action",
|
||||
res_model: "task",
|
||||
type: "ir.actions.act_window",
|
||||
views: [[3, "list"]],
|
||||
},
|
||||
};
|
||||
|
||||
let loadViewsDef;
|
||||
const actionManager = await createActionManager({
|
||||
actions: actions,
|
||||
archs: archs,
|
||||
data: this.data,
|
||||
async mockRPC(route, args) {
|
||||
const _super = this._super.bind(this);
|
||||
if (args.method === 'read') {
|
||||
await Promise.resolve(loadViewsDef); // slow down reload of settings view
|
||||
testConfig.serverData.views = {
|
||||
"task,1,list": '<tree><field name="display_name"/></tree>',
|
||||
"res.config.settings,2,form": `
|
||||
<form string="Settings" js_class="base_settings">
|
||||
<div class="app_settings_block" string="CRM" data-key="crm">
|
||||
<button name="3" string="Execute action" type="action"/>
|
||||
</div>
|
||||
</form>`,
|
||||
"task,3,list": '<tree><field name="display_name"/></tree>',
|
||||
"res.config.settings,false,search": "<search></search>",
|
||||
"task,false,search": "<search></search>",
|
||||
};
|
||||
|
||||
let loadViewsDef;
|
||||
const mockRPC = async (route, args) => {
|
||||
if (args.method === "read") {
|
||||
await loadViewsDef; // slow down reload of settings view
|
||||
}
|
||||
return _super(route, args);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
await actionManager.doAction(1);
|
||||
assert.strictEqual(actionManager.$('.breadcrumb').text(), 'First action');
|
||||
const webClient = await createWebClient({ testConfig, mockRPC });
|
||||
await doAction(webClient, 1);
|
||||
assert.strictEqual($(webClient.el).find(".breadcrumb").text(), "First action");
|
||||
|
||||
await actionManager.doAction(2);
|
||||
assert.strictEqual(actionManager.$('.breadcrumb').text(), 'First actionNew');
|
||||
await doAction(webClient, 2);
|
||||
assert.strictEqual(
|
||||
$(webClient.el).find(".breadcrumb").text(),
|
||||
"First actionNew"
|
||||
);
|
||||
|
||||
loadViewsDef = testUtils.makeTestPromise();
|
||||
await testUtils.dom.click(actionManager.$('button[name="3"]'));
|
||||
assert.strictEqual(actionManager.$('.breadcrumb').text(), 'First actionNew');
|
||||
loadViewsDef = testUtils.makeTestPromise();
|
||||
await testUtils.dom.click($(webClient.el).find('button[name="3"]'));
|
||||
await legacyExtraNextTick();
|
||||
assert.strictEqual(
|
||||
$(webClient.el).find(".breadcrumb").text(),
|
||||
"First actionNew"
|
||||
);
|
||||
|
||||
loadViewsDef.resolve();
|
||||
await testUtils.nextTick();
|
||||
assert.strictEqual(actionManager.$('.breadcrumb').text(), 'First actionNewOther action');
|
||||
|
||||
actionManager.destroy();
|
||||
});
|
||||
loadViewsDef.resolve();
|
||||
await testUtils.nextTick();
|
||||
await legacyExtraNextTick();
|
||||
assert.strictEqual(
|
||||
$(webClient.el).find(".breadcrumb").text(),
|
||||
"First actionNewOther action"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test('settings can contain one2many fields', async function (assert) {
|
||||
assert.expect(2);
|
||||
@@ -477,75 +526,79 @@ QUnit.module('base_settings_tests', {
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('call "call_button/execute" when clicking on a button in dirty settings', async function (assert) {
|
||||
assert.expect(7);
|
||||
QUnit.test(
|
||||
'call "call_button/execute" when clicking on a button in dirty settings',
|
||||
async function (assert) {
|
||||
assert.expect(7);
|
||||
|
||||
const actions = [{
|
||||
id: 1,
|
||||
name: 'Settings view',
|
||||
res_model: 'res.config.settings',
|
||||
type: 'ir.actions.act_window',
|
||||
views: [[1, 'form']],
|
||||
}];
|
||||
const archs = {
|
||||
'res.config.settings,1,form': `
|
||||
<form string="Settings" js_class="base_settings">
|
||||
<div class="app_settings_block" string="CRM" data-key="crm">
|
||||
<div class="row mt16 o_settings_container">
|
||||
<div class="col-12 col-lg-6 o_setting_box">
|
||||
<div class="o_setting_left_pane">
|
||||
<field name="foo"/>
|
||||
</div>
|
||||
<div class="o_setting_right_pane">
|
||||
<span class="o_form_label">Foo</span>
|
||||
<div class="text-muted">
|
||||
this is foo
|
||||
testConfig.serverData.actions[1] = {
|
||||
id: 1,
|
||||
name: "Settings view",
|
||||
res_model: "res.config.settings",
|
||||
type: "ir.actions.act_window",
|
||||
views: [[1, "form"]],
|
||||
};
|
||||
|
||||
testConfig.serverData.views = Object.assign(testConfig.serverData.views, {
|
||||
"res.config.settings,1,form": `
|
||||
<form string="Settings" js_class="base_settings">
|
||||
<div class="app_settings_block" string="CRM" data-key="crm">
|
||||
<div class="row mt16 o_settings_container">
|
||||
<div class="col-12 col-lg-6 o_setting_box">
|
||||
<div class="o_setting_left_pane">
|
||||
<field name="foo"/>
|
||||
</div>
|
||||
<div class="o_setting_right_pane">
|
||||
<span class="o_form_label">Foo</span>
|
||||
<div class="text-muted">
|
||||
this is foo
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button name="4" string="Execute action" type="action"/>
|
||||
</div>
|
||||
<button name="4" string="Execute action" type="action"/>
|
||||
</div>
|
||||
</form>
|
||||
</form>
|
||||
`,
|
||||
'res.config.settings,false,search': '<search></search>',
|
||||
};
|
||||
"res.config.settings,false,search": "<search></search>",
|
||||
});
|
||||
|
||||
const actionManager = await createActionManager({
|
||||
actions: actions,
|
||||
archs: archs,
|
||||
data: this.data,
|
||||
mockRPC(route, args) {
|
||||
if (route === '/web/dataset/call_button' && args.method === 'execute') {
|
||||
assert.step('execute');
|
||||
return Promise.resolve(true);
|
||||
} else if (args.method === 'create') {
|
||||
assert.step('create');
|
||||
const mockRPC = (route, args) => {
|
||||
if (route === "/web/dataset/call_button" && args.method === "execute") {
|
||||
assert.step("execute");
|
||||
return true;
|
||||
} else if (args.method === "create") {
|
||||
assert.step("create");
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
await actionManager.doAction(1);
|
||||
assert.containsNone(actionManager, '.o_field_boolean input:checked',
|
||||
'checkbox should not be checked');
|
||||
const webClient = await createWebClient({ testConfig, mockRPC });
|
||||
|
||||
await testUtils.dom.click(actionManager.$('input[type="checkbox"]'));
|
||||
assert.containsOnce(actionManager, '.o_field_boolean input:checked',
|
||||
'checkbox should be checked');
|
||||
await doAction(webClient, 1);
|
||||
assert.containsNone(
|
||||
webClient,
|
||||
".o_field_boolean input:checked",
|
||||
"checkbox should not be checked"
|
||||
);
|
||||
|
||||
await testUtils.dom.click(actionManager.$('button[name="4"]'));
|
||||
assert.containsOnce(document.body, '.modal', 'should open a warning dialog');
|
||||
await testUtils.dom.click($(webClient.el).find('input[type="checkbox"]'));
|
||||
assert.containsOnce(
|
||||
webClient,
|
||||
".o_field_boolean input:checked",
|
||||
"checkbox should be checked"
|
||||
);
|
||||
|
||||
await testUtils.dom.click($('.modal-footer .btn-primary'));
|
||||
assert.verifySteps([
|
||||
'create', // saveRecord from modal
|
||||
'execute', // execute_action
|
||||
'create' // saveRecord from FormController._onButtonClicked
|
||||
]);
|
||||
await testUtils.dom.click($(webClient.el).find('button[name="4"]'));
|
||||
assert.containsOnce(document.body, ".modal", "should open a warning dialog");
|
||||
|
||||
actionManager.destroy();
|
||||
});
|
||||
await testUtils.dom.click($(".modal-footer .btn-primary"));
|
||||
assert.verifySteps([
|
||||
"create", // saveRecord from modal
|
||||
"execute", // execute_action
|
||||
"create", // saveRecord from FormController._onButtonClicked
|
||||
]);
|
||||
}
|
||||
);
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
@@ -27,6 +27,7 @@ GLOBAL = ','.join([
|
||||
'Popover',
|
||||
'StackTrace',
|
||||
'QUnit',
|
||||
'luxon',
|
||||
'moment',
|
||||
'py',
|
||||
'ClipboardJS',
|
||||
|
||||
@@ -31,6 +31,8 @@ class TestConflictMarkers(lint_case.LintCase):
|
||||
|
||||
for p in paths:
|
||||
for dp, _, file_names in os.walk(p):
|
||||
if 'node_modules' in dp:
|
||||
continue
|
||||
for fn in file_names:
|
||||
if fn.endswith(EXTENSIONS):
|
||||
self.check_file(os.path.join(dp, fn))
|
||||
|
||||
@@ -12,12 +12,12 @@ tour.register('main_flow_tour', {
|
||||
tour.stepUtils.openBuggerMenu("li.breadcrumb-item.active:contains('Quotations')"),
|
||||
{
|
||||
// Add Stockable product
|
||||
trigger: ".o_menu_sections a:contains('Products')",
|
||||
trigger: ".o_menu_sections .o_dropdown_toggler span:contains('Products')",
|
||||
extra_trigger: '.o_main_navbar',
|
||||
content: _t("Let's create products."),
|
||||
position: "bottom",
|
||||
}, {
|
||||
trigger: ".o_menu_sections a:has(span:contains('Products'))",
|
||||
trigger: ".o_menu_sections .o_dropdown_item span:contains('Products')",
|
||||
content: _t("Let's create products."),
|
||||
position: "bottom"
|
||||
}, {
|
||||
@@ -654,29 +654,29 @@ tour.stepUtils.autoExpandMoreButtons('.o_form_readonly'),
|
||||
tour.stepUtils.openBuggerMenu("li.breadcrumb-item.active:contains('Inventory Overview')"),
|
||||
{
|
||||
mobile: false,
|
||||
trigger: ".o_menu_sections a[data-menu-xmlid='stock.menu_stock_config_settings']",
|
||||
trigger: ".o_menu_sections button[data-menu-xmlid='stock.menu_stock_config_settings']",
|
||||
extra_trigger: '.o_main_navbar',
|
||||
content: _t("Go to Configuration"),
|
||||
position: "bottom"
|
||||
}, {
|
||||
mobile: false,
|
||||
trigger: ".o_menu_sections a[data-menu-xmlid='stock.menu_reordering_rules_config']",
|
||||
trigger: ".o_menu_sections li[data-menu-xmlid='stock.menu_reordering_rules_config']",
|
||||
content: _t("Reordering Rules"),
|
||||
position: "bottom"
|
||||
}, {
|
||||
mobile: true,
|
||||
trigger: ".o_menu_sections a:contains('Configuration')",
|
||||
trigger: ".o_menu_sections button:contains('Configuration')",
|
||||
extra_trigger: '.o_main_navbar',
|
||||
content: _t("Go to Configuration"),
|
||||
position: "bottom"
|
||||
}, {
|
||||
mobile: true,
|
||||
trigger: ".o_menu_sections a[data-menu-xmlid='stock.menu_product_in_config_stock']",
|
||||
trigger: ".o_menu_sections button[data-menu-xmlid='stock.menu_product_in_config_stock']",
|
||||
content: _t("Click on Product Section"),
|
||||
position: "bottom"
|
||||
}, {
|
||||
mobile: true,
|
||||
trigger: ".o_menu_sections a:contains('Reordering Rules')",
|
||||
trigger: ".o_menu_sections li:contains('Reordering Rules')",
|
||||
content: _t("Click on Product Section"),
|
||||
position: "bottom"
|
||||
}, {
|
||||
@@ -730,11 +730,11 @@ tour.stepUtils.openBuggerMenu("li.breadcrumb-item.active:contains('Inventory Ove
|
||||
tour.stepUtils.openBuggerMenu("li.breadcrumb-item.active:contains('OP/')"),
|
||||
{
|
||||
// Run the schedulers
|
||||
trigger: ".o_menu_sections a:contains('Operations')",
|
||||
trigger: ".o_menu_sections button:contains('Operations')",
|
||||
content: _t("Go to Run Schedulers"),
|
||||
position: "bottom"
|
||||
}, {
|
||||
trigger: ".o_menu_sections a[data-menu-xmlid='stock.menu_procurement_compute']",
|
||||
trigger: ".o_menu_sections li[data-menu-xmlid='stock.menu_procurement_compute']",
|
||||
content: _t("Click on schedulers"),
|
||||
position: "bottom"
|
||||
}, {
|
||||
@@ -778,7 +778,7 @@ tour.stepUtils.openBuggerMenu("li.breadcrumb-item.active:contains('OP/')"),
|
||||
{
|
||||
trigger: '.o_form_button_edit',
|
||||
content: _t('Edit the vendor bill'),
|
||||
extra_trigger: 'body.o_web_client:not(.oe_wait)',
|
||||
extra_trigger: ".o_form_label .o_field_widget:contains('Vendor Bill')",
|
||||
}, {
|
||||
trigger:".o_field_widget[name=invoice_date] input",
|
||||
content: _t('Set the invoice date'),
|
||||
@@ -800,12 +800,12 @@ tour.stepUtils.openBuggerMenu("li.breadcrumb-item.active:contains('OP/')"),
|
||||
tour.stepUtils.openBuggerMenu("li.breadcrumb-item.active:contains('Manufacturing Orders'), li.breadcrumb-item.active:contains('Work Centers Overview')"),
|
||||
{
|
||||
mobile: false,
|
||||
trigger: ".o_menu_sections a[data-menu-xmlid='mrp.menu_mrp_manufacturing']",
|
||||
trigger: ".o_menu_sections button[data-menu-xmlid='mrp.menu_mrp_manufacturing']",
|
||||
content: _t('Click on Operations menuitem'),
|
||||
position: 'bottom',
|
||||
}, {
|
||||
mobile: false,
|
||||
trigger: ".o_menu_sections a[data-menu-xmlid='mrp.menu_mrp_production_action']",
|
||||
trigger: ".o_menu_sections li[data-menu-xmlid='mrp.menu_mrp_production_action']",
|
||||
content: _t('Open manufacturing orders'),
|
||||
position: 'bottom',
|
||||
}, {
|
||||
@@ -815,13 +815,13 @@ tour.stepUtils.openBuggerMenu("li.breadcrumb-item.active:contains('Manufacturing
|
||||
position: 'bottom',
|
||||
}, {
|
||||
mobile: true,
|
||||
trigger: ".o_menu_sections a:contains('Operations')",
|
||||
trigger: ".o_menu_sections button:contains('Operations')",
|
||||
extra_trigger: '.o_main_navbar',
|
||||
content: _t("Let's go to manufacturing order."),
|
||||
position: "bottom",
|
||||
}, {
|
||||
mobile: true,
|
||||
trigger: ".o_menu_sections a:has(span:contains('Manufacturing Orders'))",
|
||||
trigger: ".o_menu_sections li:contains('Manufacturing Orders')",
|
||||
content: _t("Let's go to manufacturing order."),
|
||||
position: "bottom"
|
||||
}, {
|
||||
@@ -833,9 +833,8 @@ tour.stepUtils.openBuggerMenu("li.breadcrumb-item.active:contains('Manufacturing
|
||||
},
|
||||
...tour.stepUtils.statusbarButtonsSteps('Check availability', _t("Check availability")),
|
||||
{
|
||||
trigger: '.o_form_button_edit',
|
||||
trigger: ".o_form_button_edit:not(:disabled)",
|
||||
content: _t('Edit the production order'),
|
||||
extra_trigger: 'body.o_web_client:not(.oe_wait)',
|
||||
}, {
|
||||
trigger: "input[name=qty_producing]",
|
||||
position: 'left',
|
||||
@@ -852,11 +851,11 @@ tour.stepUtils.openBuggerMenu("li.breadcrumb-item.active:contains('Manufacturing
|
||||
...tour.stepUtils.goToAppSteps('sale.sale_menu_root', _t('Organize your sales activities with the <b>Sales app</b>.')),
|
||||
tour.stepUtils.openBuggerMenu("li.breadcrumb-item.active:contains('Quotations')"),
|
||||
{
|
||||
trigger: ".o_menu_sections a[data-menu-xmlid='sale.sale_order_menu']",
|
||||
trigger: ".o_menu_sections button[data-menu-xmlid='sale.sale_order_menu']",
|
||||
content: _t("Go to Sales menu"),
|
||||
position: "bottom"
|
||||
}, {
|
||||
trigger: ".o_menu_sections a[data-menu-xmlid='sale.menu_sale_order']",
|
||||
trigger: ".o_menu_sections li[data-menu-xmlid='sale.menu_sale_order']",
|
||||
content: _t("Go to the sales orders"),
|
||||
position: "bottom"
|
||||
}, {
|
||||
|
||||
@@ -336,11 +336,11 @@ odoo.define('web.test.x2many', function (require) {
|
||||
|
||||
{
|
||||
content: "switch to the second form view to test one2many with editable list (toggle menu dropdown)",
|
||||
trigger: 'nav .o_menu_sections .dropdown-toggle:containsExact(Discussions), li.o_extra_menu_items a i.fa-plus',
|
||||
trigger: 'button[data-menu-xmlid="test_new_api.menu_main"], li.o_extra_menu_items a i.fa-plus',
|
||||
extra_trigger: '.tab-pane:eq(0) .o_field_widget tbody .o_data_row:eq(2)',
|
||||
}, {
|
||||
content: "switch to the second form view to test one2many with editable list (open submenu)",
|
||||
trigger: 'nav .o_menu_sections .dropdown-item:contains(Discussions 2)',
|
||||
trigger: 'li[data-menu-xmlid="test_new_api.menu_discussions_2"]',
|
||||
}, {
|
||||
content: "select previous created record",
|
||||
trigger: 'td:contains(test_trigger):last',
|
||||
@@ -453,11 +453,11 @@ odoo.define('web.test.x2many', function (require) {
|
||||
|
||||
{
|
||||
content: "switch to the third form view to test onchange changing one2many (toggle menu dropdown)",
|
||||
trigger: 'nav .o_menu_sections .dropdown-toggle:containsExact(Discussions), li.o_extra_menu_items a i.fa-plus',
|
||||
trigger: 'button[data-menu-xmlid="test_new_api.menu_main"], li.o_extra_menu_items a i.fa-plus',
|
||||
extra_trigger: '.tab-pane:eq(0) .o_field_widget tbody .o_data_row:eq(2)',
|
||||
}, {
|
||||
content: "switch to the thied form view to test onchange changing one2many (open submenu)",
|
||||
trigger: 'nav .o_menu_sections .dropdown-item:contains(Discussions 3)',
|
||||
trigger: 'li[data-menu-xmlid="test_new_api.menu_discussions_3"]',
|
||||
}, {
|
||||
content: "wait web client",
|
||||
trigger: '.breadcrumb:contains(Discussions 3)',
|
||||
|
||||
Reference in New Issue
Block a user