[MERGE] *: rewrite the webclient in OWL (phase 1)
This commit is the first phase of the conversion of the web/ JS codebase to the owl framework. The impact of this commit is two-fold. First, it rewrites the framework part of web with a new system of services and registries. Services allow to execute code (e.g. do rpcs, setup things) before launching the application. They can also expose an API to be used by other parts of the application (e.g. a `notification` service would expose a function to display notifications). Services are often a good extension point for external modules that want to execute code at webclient startup. Registries offer another way to extend the application. They provide well designed extension points to add elements/behaviors from the outside (for instance, to add a systray item, an error handler...). Second, this commit initiates the conversion of the webclient to owl with a top-down approach, around those notions of services and registries. The root of the web application is now an owl application. Among others, the WebClient, ActionManager, Navbar, UserMenu, DebugManager, Dialogs, services (e.g. notification, ajax...) have been converted to the new framework/architecture. Legacy views and client actions are still supported (and used). They will be converted in the next months, and at some point, the support will be dropped. The code in other addons (e.g. including the legacy WebClient, ActionManager...) has been adapted to fit with the new implementation. This commit also does a general design improvement pass. Task 2344639 Enterprise PR https://github.com/odoo/enterprise/pull/13444 closes odoo/odoo#58287 Signed-off-by: Aaron Bohy (aab) <aab@odoo.com> 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> Co-authored-by: stefanorigano (SRI) <sri@odoo.com>
This commit is contained in:
co-authored by
Aaron Bohy
Bruno Boi
Géry Debongnie
Samuel Degueldre
Mathieu Duckerts-Antoine
Simon Genin
Francois
Michael Mattiello
Julien Mougenot
Lucas Perais
Jorge Pinna Puissant
stefanorigano
commit
29731b404f
@@ -30,6 +30,12 @@ setup/win32/static/less/
|
||||
setup/win32/static/wkhtmltopdf/
|
||||
setup/win32/static/postgresql*.exe
|
||||
|
||||
# js tooling
|
||||
node_modules
|
||||
package-lock.json
|
||||
package.json
|
||||
.husky
|
||||
|
||||
# various virtualenv
|
||||
/bin/
|
||||
/build/
|
||||
|
||||
@@ -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/**/*',
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
}
|
||||
|
||||
.fa-check {
|
||||
color: theme-color('success');
|
||||
@extend .text-success;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -219,7 +219,7 @@ tour.register('totp_admin_disables', {
|
||||
}
|
||||
}, {
|
||||
content: "Open Actions menu",
|
||||
trigger: 'button.o_dropdown_toggler_btn:contains("Action")'
|
||||
trigger: 'button.o_dropdown_toggler:contains("Action")'
|
||||
}, {
|
||||
content: "Select totp remover",
|
||||
trigger: 'a.dropdown-item:contains(Disable TOTP on users)'
|
||||
|
||||
@@ -22,6 +22,9 @@ trigger an automatic reminder email.
|
||||
'views/base_automation_view.xml',
|
||||
],
|
||||
'assets': {
|
||||
'web.assets_qweb': [
|
||||
'base_automation/static/src/xml/*.xml',
|
||||
],
|
||||
'web.assets_backend': [
|
||||
'base_automation/static/src/js/**/*',
|
||||
],
|
||||
|
||||
@@ -1,78 +1,60 @@
|
||||
odoo.define('base_automation.BaseAutomatioErrorDialog', function (require) {
|
||||
"use strict";
|
||||
/** @odoo-module */
|
||||
|
||||
const CrashManager = require('web.CrashManager');
|
||||
const ErrorDialog = CrashManager.ErrorDialog;
|
||||
const ErrorDialogRegistry = require('web.ErrorDialogRegistry');
|
||||
const session = require('web.session');
|
||||
import { ErrorDialog } from "@web/core/errors/error_dialogs";
|
||||
import session from "web.session";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { useService } from "@web/core/service_hook";
|
||||
|
||||
const BaseAutomationErrorDialog = ErrorDialog.extend({
|
||||
xmlDependencies: (ErrorDialog.prototype.xmlDependencies || []).concat(
|
||||
['/base_automation/static/src/xml/base_automation_error_dialog.xml']
|
||||
),
|
||||
template: 'CrashManager.BaseAutomationError',
|
||||
events: {
|
||||
'click .o_disable_action_button': '_onDisableAction',
|
||||
'click .o_edit_action_button': '_onEditAction',
|
||||
},
|
||||
/**
|
||||
* Assign the `base_automation` object based on the error data,
|
||||
* which is then used by the `CrashManager.BaseAutomationError` template
|
||||
* and the events defined above.
|
||||
* @override
|
||||
* @param {Object} error
|
||||
* @param {string} error.data.context.base_automation.id the ID of the failing automated action
|
||||
* @param {string} error.data.context.base_automation.name the name of the failing automated action
|
||||
*/
|
||||
init: function (parent, options, error) {
|
||||
this._super.apply(this, arguments);
|
||||
this.base_automation = error.data.context.base_automation;
|
||||
this.is_admin = session.is_admin;
|
||||
},
|
||||
export class BaseAutomationErrorDialog extends ErrorDialog {
|
||||
setup() {
|
||||
super.setup(...arguments);
|
||||
const { id, name } = this.props.data.context.base_automation;
|
||||
this.actionId = id;
|
||||
this.actionName = name;
|
||||
this.isUserAdmin = useService("user").isAdmin;
|
||||
this.actionService = useService("action");
|
||||
this.orm = useService("orm");
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* This method is called when the user clicks on the 'Disable action' button
|
||||
* displayed when a crash occurs in the evaluation of an automated action.
|
||||
* Then, we write `active` to `False` on the automated action to disable it.
|
||||
*
|
||||
* @private
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
_onDisableAction: function (ev) {
|
||||
ev.preventDefault();
|
||||
this._rpc({
|
||||
model: 'base.automation',
|
||||
method: 'write',
|
||||
args: [[this.base_automation.id], {
|
||||
active: false,
|
||||
}],
|
||||
}).then(this.destroy.bind(this));
|
||||
},
|
||||
/**
|
||||
* This method is called when the user clicks on the 'Edit action' button
|
||||
* displayed when a crash occurs in the evaluation of an automated action.
|
||||
* Then, we redirect the user to the automated action form.
|
||||
*
|
||||
* @private
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
_onEditAction: function (ev) {
|
||||
ev.preventDefault();
|
||||
this.do_action({
|
||||
name: 'Automated Actions',
|
||||
res_model: 'base.automation',
|
||||
res_id: this.base_automation.id,
|
||||
views: [[false, 'form']],
|
||||
type: 'ir.actions.act_window',
|
||||
view_mode: 'form',
|
||||
});
|
||||
},
|
||||
});
|
||||
/**
|
||||
* This method is called when the user clicks on the 'Disable action' button
|
||||
* displayed when a crash occurs in the evaluation of an automated action.
|
||||
* Then, we write `active` to `False` on the automated action to disable it.
|
||||
*
|
||||
* @private
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
async disableAction(ev) {
|
||||
await this.orm.write("base.automation", [this.actionId], {
|
||||
active: false,
|
||||
});
|
||||
this.close();
|
||||
}
|
||||
/**
|
||||
* This method is called when the user clicks on the 'Edit action' button
|
||||
* displayed when a crash occurs in the evaluation of an automated action.
|
||||
* Then, we redirect the user to the automated action form.
|
||||
*
|
||||
* @private
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
editAction(ev) {
|
||||
this.actionService.doAction({
|
||||
name: "Automated Actions",
|
||||
res_model: "base.automation",
|
||||
res_id: this.actionId,
|
||||
views: [[false, "form"]],
|
||||
type: "ir.actions.act_window",
|
||||
view_mode: "form",
|
||||
});
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
|
||||
ErrorDialogRegistry.add('base_automation', BaseAutomationErrorDialog);
|
||||
BaseAutomationErrorDialog.bodyTemplate = "base_automation.ErrorDialogBody";
|
||||
|
||||
});
|
||||
registry.category("error_dialogs").add("base_automation", BaseAutomationErrorDialog);
|
||||
|
||||
@@ -1,36 +1,34 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<templates>
|
||||
<t t-name="CrashManager.BaseAutomationError" t-extend="CrashManager.error">
|
||||
<t t-jquery=".alert.alert-warning" t-operation="append">
|
||||
<t t-if="widget.base_automation.id">
|
||||
<p>
|
||||
The error occurred during the execution of the automated action
|
||||
"<t t-esc="widget.base_automation.name"/>"
|
||||
(ID: <t t-esc="widget.base_automation.id"/>).
|
||||
<br/>
|
||||
</p>
|
||||
<p t-if="!widget.is_admin">
|
||||
You can ask an administrator to disable or correct this automated action.
|
||||
</p>
|
||||
<p t-if="widget.is_admin">
|
||||
You can disable this automated action or edit it to solve the issue.<br/>
|
||||
Disabling this automated action will enable you to continue your workflow
|
||||
but any data created after this could potentially be corrupted,
|
||||
as you are effectively disabling a customization that may set
|
||||
important and/or required fields.
|
||||
</p>
|
||||
</t>
|
||||
</t>
|
||||
<t t-jquery=".alert.alert-warning button" t-operation="after">
|
||||
<t t-if="widget.base_automation.id && widget.is_admin">
|
||||
<button class="btn btn-secondary mt4 o_disable_action_button">
|
||||
<t t-name="base_automation.ErrorDialogBody" t-inherit="web.ErrorDialogBody" t-inherit-mode="primary" owl="1">
|
||||
<xpath expr="//div[@role='alert']" position="inside">
|
||||
<p>
|
||||
The error occurred during the execution of the automated action
|
||||
"<t t-esc="actionName"/>"
|
||||
(ID: <t t-esc="actionId"/>).
|
||||
<br/>
|
||||
</p>
|
||||
<p t-if="isUserAdmin">
|
||||
You can disable this automated action or edit it to solve the issue.<br/>
|
||||
Disabling this automated action will enable you to continue your workflow
|
||||
but any data created after this could potentially be corrupted,
|
||||
as you are effectively disabling a customization that may set
|
||||
important and/or required fields.
|
||||
</p>
|
||||
<p t-else="">
|
||||
You can ask an administrator to disable or correct this automated action.
|
||||
</p>
|
||||
</xpath>
|
||||
<xpath expr="//div[@role='alert']//button" position="after">
|
||||
<t t-if="isUserAdmin">
|
||||
<button class="btn btn-secondary mt4 o_disable_action_button" t-on-click.prevent="disableAction">
|
||||
<i class="fa fa-ban mr8"/>Disable Action
|
||||
</button>
|
||||
<button class="btn btn-secondary mt4 o_edit_action_button">
|
||||
<button class="btn btn-secondary mt4 o_edit_action_button" t-on-click.prevent="editAction">
|
||||
<i class="fa fa-edit mr8"/>Edit action
|
||||
</button>
|
||||
</t>
|
||||
</t>
|
||||
</xpath>
|
||||
</t>
|
||||
</templates>
|
||||
|
||||
@@ -1,75 +1,130 @@
|
||||
odoo.define('base_automation.BaseAutomatioErrorDialogTests', function (require) {
|
||||
'use strict';
|
||||
/** @odoo-modules */
|
||||
|
||||
const CrashManager = require('web.CrashManager').CrashManager;
|
||||
const session = require('web.session');
|
||||
const makeTestEnvironment = require("web.test_env");
|
||||
import { registry } from "@web/core/registry";
|
||||
import { errorService } from "@web/core/errors/error_service";
|
||||
import { dialogService } from "@web/core/dialog/dialog_service";
|
||||
import { notificationService } from "@web/core/notifications/notification_service";
|
||||
import { uiService } from "@web/core/ui/ui_service";
|
||||
import { hotkeyService } from "@web/core/hotkeys/hotkey_service";
|
||||
import { registerCleanup } from "@web/../tests/helpers/cleanup";
|
||||
import { makeTestEnv } from "@web/../tests/helpers/mock_env";
|
||||
import { makeFakeRPCService, makeFakeLocalizationService } from "@web/../tests/helpers/mock_services";
|
||||
import { RPCError } from "@web/core/network/rpc_service";
|
||||
|
||||
QUnit.module('base_automation', {}, function () {
|
||||
import { BaseAutomationErrorDialog } from "../src/js/base_automation_error_dialog";
|
||||
import { patchWithCleanup } from "@web/../tests/helpers/utils";
|
||||
import { DialogContainer } from "@web/core/dialog/dialog_container";
|
||||
import { getFixture } from "@web/../tests/helpers/utils";
|
||||
import { nextTick } from "@web/../tests/helpers/utils";
|
||||
|
||||
QUnit.module('Error Dialog');
|
||||
const serviceRegistry = registry.category("services");
|
||||
|
||||
QUnit.test('Error due to an automated action', async function (assert) {
|
||||
assert.expect(4);
|
||||
QUnit.module("base_automation", {}, function () {
|
||||
|
||||
let baseAutomationName = 'Test base automation error dialog';
|
||||
let error = {
|
||||
type: 'Odoo Client Error',
|
||||
message: 'Message',
|
||||
data: {
|
||||
debug: 'Traceback',
|
||||
context: {
|
||||
exception_class: 'base_automation',
|
||||
base_automation: {
|
||||
id: 1,
|
||||
name: baseAutomationName,
|
||||
},
|
||||
},
|
||||
},
|
||||
let unhandledRejectionCb;
|
||||
QUnit.module("Error Dialog", {
|
||||
async beforeEach() {
|
||||
serviceRegistry.add("dialog", dialogService);
|
||||
serviceRegistry.add("ui", uiService);
|
||||
serviceRegistry.add("error", errorService);
|
||||
serviceRegistry.add("hotkey", hotkeyService);
|
||||
serviceRegistry.add("localization", makeFakeLocalizationService());
|
||||
serviceRegistry.add("action", { start: () => {} });
|
||||
serviceRegistry.add("orm", { start: () => {} });
|
||||
serviceRegistry.add("user", { start: () => ({ isAdmin: true }) });
|
||||
registry.category("error_dialogs").add("base_automation", BaseAutomationErrorDialog);
|
||||
// Both of these are unused but required for the error service to call error handlers
|
||||
serviceRegistry.add("notification", notificationService);
|
||||
serviceRegistry.add("rpc", makeFakeRPCService());
|
||||
const windowAddEventListener = window.addEventListener;
|
||||
window.addEventListener = (type, cb) => {
|
||||
if (type === "unhandledrejection") {
|
||||
unhandledRejectionCb = cb;
|
||||
}
|
||||
};
|
||||
// Force the user session to be admin, to display the disable and edit action buttons,
|
||||
// then reset back to the origin value after the test.
|
||||
let isAdmin = session.is_admin;
|
||||
session.is_admin = true;
|
||||
registerCleanup(() => {
|
||||
window.addEventListener = windowAddEventListener;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
let crashManager = new CrashManager();
|
||||
crashManager.env = makeTestEnvironment();
|
||||
let dialog = crashManager.show_error(error);
|
||||
QUnit.test("Error due to an automated action", async function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
await dialog._opened;
|
||||
|
||||
assert.containsOnce(document.body, '.modal .o_clipboard_button');
|
||||
assert.containsOnce(document.body, '.modal .o_disable_action_button');
|
||||
assert.containsOnce(document.body, '.modal .o_edit_action_button');
|
||||
assert.ok(dialog.$el.text().indexOf(baseAutomationName) !== -1);
|
||||
|
||||
session.is_admin = isAdmin;
|
||||
crashManager.destroy();
|
||||
const error = new RPCError();
|
||||
const errorContext = {
|
||||
exception_class: "base_automation",
|
||||
base_automation: {
|
||||
id: 1,
|
||||
name: "Test base automation error dialog",
|
||||
},
|
||||
};
|
||||
Object.assign(error, {
|
||||
subType: "Odoo Client Error",
|
||||
message: "Message",
|
||||
data: {
|
||||
debug: "Traceback",
|
||||
context: errorContext,
|
||||
},
|
||||
exceptionName: errorContext.exception_class,
|
||||
});
|
||||
|
||||
QUnit.test('Error not due to an automated action', async function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
let error = {
|
||||
type: 'Odoo Client Error',
|
||||
message: 'Message',
|
||||
data: {
|
||||
debug: 'Traceback',
|
||||
},
|
||||
};
|
||||
let crashManager = new CrashManager();
|
||||
crashManager.env = makeTestEnvironment();
|
||||
let dialog = crashManager.show_error(error);
|
||||
|
||||
await dialog._opened;
|
||||
|
||||
assert.containsOnce(document.body, '.modal .o_clipboard_button');
|
||||
assert.containsNone(document.body, '.modal .o_disable_action_button');
|
||||
assert.containsNone(document.body, '.modal .o_edit_action_button');
|
||||
|
||||
crashManager.destroy();
|
||||
patchWithCleanup(BaseAutomationErrorDialog.prototype, {
|
||||
setup() {
|
||||
assert.equal(this.props.data.context, errorContext, "Received the correct error context");
|
||||
this._super();
|
||||
},
|
||||
});
|
||||
|
||||
const env = await makeTestEnv();
|
||||
const { Component: Container, props } = registry.category("main_components").get("DialogContainer");
|
||||
const dialogContainer = await owl.mount(Container, { target: getFixture(), env, props });
|
||||
|
||||
const errorEvent = new PromiseRejectionEvent("error", { reason: {
|
||||
message: error,
|
||||
legacy: true,
|
||||
event: $.Event(),
|
||||
}, promise: null });
|
||||
await unhandledRejectionCb(errorEvent);
|
||||
await nextTick();
|
||||
assert.containsOnce(dialogContainer, '.modal .fa-clipboard');
|
||||
assert.containsOnce(dialogContainer, '.modal .o_disable_action_button');
|
||||
assert.containsOnce(dialogContainer, '.modal .o_edit_action_button');
|
||||
});
|
||||
|
||||
QUnit.test("Error not due to an automated action", async function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
const error = new RPCError();
|
||||
Object.assign(error, {
|
||||
subType: "Odoo Client Error",
|
||||
message: "Message",
|
||||
data: {
|
||||
debug: "Traceback",
|
||||
},
|
||||
});
|
||||
|
||||
patchWithCleanup(DialogContainer.prototype, {
|
||||
mounted() {
|
||||
this._super();
|
||||
this.el.classList.add("o_dialog_container");
|
||||
}
|
||||
});
|
||||
|
||||
const env = await makeTestEnv();
|
||||
const { Component: Container, props } = registry.category("main_components").get("DialogContainer");
|
||||
const dialogContainer = await owl.mount(Container, { target: getFixture(), env, props });
|
||||
|
||||
const errorEvent = new PromiseRejectionEvent("error", { reason: {
|
||||
message: error,
|
||||
legacy: true,
|
||||
event: $.Event(),
|
||||
}, promise: null });
|
||||
await unhandledRejectionCb(errorEvent);
|
||||
await nextTick();
|
||||
assert.containsOnce(dialogContainer, '.modal .fa-clipboard');
|
||||
assert.containsNone(dialogContainer, '.modal .o_disable_action_button');
|
||||
assert.containsNone(dialogContainer, '.modal .o_edit_action_button');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -22,7 +22,6 @@ Allows users to create custom dashboard.
|
||||
'assets': {
|
||||
'web.assets_backend': [
|
||||
'board/static/src/scss/dashboard.scss',
|
||||
'board/static/src/js/action_manager_board_action.js',
|
||||
'board/static/src/js/board_view.js',
|
||||
'board/static/src/js/add_to_board_menu.js',
|
||||
],
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
odoo.define('board.ActionManager', function (require) {
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* The purpose of this file is to patch the ActionManager to properly generate
|
||||
* the flags for the 'ir.actions.act_window' of model 'board.board'.
|
||||
*/
|
||||
|
||||
var ActionManager = require('web.ActionManager');
|
||||
|
||||
ActionManager.include({
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @override
|
||||
* @private
|
||||
*/
|
||||
_executeWindowAction: function (action) {
|
||||
if (action.res_model === 'board.board' && action.view_mode === 'form') {
|
||||
action.target = 'inline';
|
||||
_.extend(action.flags, {
|
||||
hasActionMenus: false,
|
||||
hasSearchView: false,
|
||||
headless: true,
|
||||
});
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
|
||||
});
|
||||
@@ -446,6 +446,19 @@ var BoardView = FormView.extend({
|
||||
this._super.apply(this, arguments);
|
||||
this.controllerParams.customViewID = viewInfo.custom_view_id;
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
_extractParamsFromAction(action) {
|
||||
action.target = "inline";
|
||||
action.flags = action.flags || {};
|
||||
Object.assign(action.flags, {
|
||||
hasActionMenus: false,
|
||||
hasSearchView: false,
|
||||
headless: true,
|
||||
});
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
|
||||
return BoardView;
|
||||
|
||||
@@ -9,11 +9,12 @@ var ListRenderer = require('web.ListRenderer');
|
||||
var pyUtils = require('web.py_utils');
|
||||
|
||||
const cpHelpers = testUtils.controlPanel;
|
||||
var createActionManager = testUtils.createActionManager;
|
||||
const { createWebClient, doAction } = require("@web/../tests/webclient/helpers");
|
||||
var createView = testUtils.createView;
|
||||
|
||||
const patchDate = testUtils.mock.patchDate;
|
||||
|
||||
let serverData;
|
||||
QUnit.module('Dashboard', {
|
||||
beforeEach: function () {
|
||||
this.data = {
|
||||
@@ -48,6 +49,8 @@ QUnit.module('Dashboard', {
|
||||
}],
|
||||
},
|
||||
};
|
||||
|
||||
serverData = { models: this.data };
|
||||
},
|
||||
});
|
||||
|
||||
@@ -82,7 +85,7 @@ QUnit.test('dashboard basic rendering', async function (assert) {
|
||||
"with a dashboard, the renderer should have the proper css class");
|
||||
assert.containsOnce(form, '.o_dashboard .o_view_nocontent',
|
||||
"should have a no content helper");
|
||||
assert.strictEqual(form.$('.o_control_panel .breadcrumb-item').text(), "My Dashboard",
|
||||
assert.strictEqual(form.getTitle(), "My Dashboard",
|
||||
"should have the correct title");
|
||||
form.destroy();
|
||||
});
|
||||
@@ -696,143 +699,156 @@ QUnit.test('dashboard intercepts custom events triggered by sub controllers', as
|
||||
board.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('save actions to dashboard', async function (assert) {
|
||||
QUnit.test("save actions to dashboard", async function (assert) {
|
||||
assert.expect(6);
|
||||
|
||||
testUtils.patch(ListController, {
|
||||
getOwnedQueryParams: function () {
|
||||
var result = this._super.apply(this, arguments);
|
||||
result.context = {
|
||||
'fire': 'on the bayou',
|
||||
fire: "on the bayou",
|
||||
};
|
||||
return result;
|
||||
}
|
||||
});
|
||||
|
||||
this.data['partner'].fields.foo.sortable = true;
|
||||
|
||||
var actionManager = await createActionManager({
|
||||
data: this.data,
|
||||
archs: {
|
||||
'partner,false,list': '<list><field name="foo"/></list>',
|
||||
'partner,false,search': '<search></search>',
|
||||
},
|
||||
mockRPC: function (route, args) {
|
||||
if (route === '/board/add_to_dashboard') {
|
||||
assert.deepEqual(args.context_to_save.group_by, ['foo'],
|
||||
'The group_by should have been saved');
|
||||
assert.deepEqual(args.context_to_save.orderedBy,
|
||||
[{
|
||||
name: 'foo',
|
||||
});
|
||||
|
||||
serverData.models.partner.fields.foo.sortable = true;
|
||||
|
||||
serverData.views = {
|
||||
"partner,false,list": '<list><field name="foo"/></list>',
|
||||
"partner,false,search": "<search></search>",
|
||||
};
|
||||
|
||||
const mockRPC = (route, args) => {
|
||||
if (route === "/board/add_to_dashboard") {
|
||||
assert.deepEqual(
|
||||
args.context_to_save.group_by,
|
||||
["foo"],
|
||||
"The group_by should have been saved"
|
||||
);
|
||||
assert.deepEqual(
|
||||
args.context_to_save.orderedBy,
|
||||
[
|
||||
{
|
||||
name: "foo",
|
||||
asc: true,
|
||||
}],
|
||||
'The orderedBy should have been saved');
|
||||
assert.strictEqual(args.context_to_save.fire, 'on the bayou',
|
||||
'The context of a controller should be passed and flattened');
|
||||
assert.strictEqual(args.action_id, 1,
|
||||
"should save the correct action");
|
||||
assert.strictEqual(args.view_mode, 'list',
|
||||
"should save the correct view type");
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
],
|
||||
"The orderedBy should have been saved"
|
||||
);
|
||||
assert.strictEqual(
|
||||
args.context_to_save.fire,
|
||||
"on the bayou",
|
||||
"The context of a controller should be passed and flattened"
|
||||
);
|
||||
assert.strictEqual(args.action_id, 1, "should save the correct action");
|
||||
assert.strictEqual(args.view_mode, "list", "should save the correct view type");
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
await actionManager.doAction({
|
||||
const webClient = await createWebClient({ serverData, mockRPC });
|
||||
|
||||
await doAction(webClient, {
|
||||
id: 1,
|
||||
res_model: 'partner',
|
||||
type: 'ir.actions.act_window',
|
||||
views: [[false, 'list']],
|
||||
res_model: "partner",
|
||||
type: "ir.actions.act_window",
|
||||
views: [[false, "list"]],
|
||||
});
|
||||
|
||||
assert.containsOnce(actionManager, '.o_list_view',
|
||||
"should display the list view");
|
||||
assert.containsOnce(webClient, ".o_list_view", "should display the list view");
|
||||
|
||||
// Sort the list
|
||||
await testUtils.dom.click($('.o_column_sortable'));
|
||||
await testUtils.dom.click($(".o_column_sortable"));
|
||||
|
||||
// Group It
|
||||
await cpHelpers.toggleGroupByMenu(actionManager);
|
||||
await cpHelpers.toggleAddCustomGroup(actionManager);
|
||||
await cpHelpers.applyGroup(actionManager);
|
||||
await cpHelpers.toggleGroupByMenu(webClient);
|
||||
await cpHelpers.toggleAddCustomGroup(webClient);
|
||||
await cpHelpers.applyGroup(webClient);
|
||||
|
||||
// add this action to dashboard
|
||||
await cpHelpers.toggleFavoriteMenu(actionManager);
|
||||
await cpHelpers.toggleFavoriteMenu(webClient);
|
||||
|
||||
await testUtils.dom.click($('.o_add_to_board > button'));
|
||||
await testUtils.fields.editInput($('.o_add_to_board input'), 'a name');
|
||||
await testUtils.dom.click($('.o_add_to_board div button'));
|
||||
await testUtils.dom.click($(".o_add_to_board > button"));
|
||||
await testUtils.fields.editInput($(".o_add_to_board input"), "a name");
|
||||
await testUtils.dom.click($(".o_add_to_board div button"));
|
||||
|
||||
testUtils.unpatch(ListController);
|
||||
|
||||
actionManager.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('save two searches to dashboard', async function (assert) {
|
||||
QUnit.test("save two searches to dashboard", async function (assert) {
|
||||
// the second search saved should not be influenced by the first
|
||||
assert.expect(2);
|
||||
|
||||
var actionManager = await createActionManager({
|
||||
data: this.data,
|
||||
archs: {
|
||||
'partner,false,list': '<list><field name="foo"/></list>',
|
||||
'partner,false,search': '<search></search>',
|
||||
},
|
||||
mockRPC: function (route, args) {
|
||||
if (route === '/board/add_to_dashboard') {
|
||||
if (filter_count === 0) {
|
||||
assert.deepEqual(args.domain, [["display_name", "ilike", "a"]],
|
||||
"the correct domain should be sent");
|
||||
}
|
||||
if (filter_count === 1) {
|
||||
assert.deepEqual(args.domain, [["display_name", "ilike", "b"]],
|
||||
"the correct domain should be sent");
|
||||
}
|
||||
serverData.views = {
|
||||
"partner,false,list": '<list><field name="foo"/></list>',
|
||||
"partner,false,search": "<search></search>",
|
||||
};
|
||||
|
||||
filter_count += 1;
|
||||
return Promise.resolve(true);
|
||||
const mockRPC = (route, args) => {
|
||||
if (route === "/board/add_to_dashboard") {
|
||||
if (filter_count === 0) {
|
||||
assert.deepEqual(
|
||||
args.domain,
|
||||
[["display_name", "ilike", "a"]],
|
||||
"the correct domain should be sent"
|
||||
);
|
||||
}
|
||||
if (filter_count === 1) {
|
||||
assert.deepEqual(
|
||||
args.domain,
|
||||
[["display_name", "ilike", "b"]],
|
||||
"the correct domain should be sent"
|
||||
);
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
|
||||
await actionManager.doAction({
|
||||
filter_count += 1;
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
};
|
||||
|
||||
const webClient = await createWebClient({ serverData, mockRPC });
|
||||
|
||||
await doAction(webClient, {
|
||||
id: 1,
|
||||
res_model: 'partner',
|
||||
type: 'ir.actions.act_window',
|
||||
views: [[false, 'list']],
|
||||
res_model: "partner",
|
||||
type: "ir.actions.act_window",
|
||||
views: [[false, "list"]],
|
||||
});
|
||||
|
||||
var filter_count = 0;
|
||||
// Add a first filter
|
||||
await cpHelpers.toggleFilterMenu(actionManager);
|
||||
await cpHelpers.toggleAddCustomFilter(actionManager);
|
||||
await testUtils.fields.editInput(actionManager.el.querySelector('.o_generator_menu_value .o_input'), 'a');
|
||||
await cpHelpers.applyFilter(actionManager);
|
||||
await cpHelpers.toggleFilterMenu(webClient);
|
||||
await cpHelpers.toggleAddCustomFilter(webClient);
|
||||
await testUtils.fields.editInput(
|
||||
webClient.el.querySelector(".o_generator_menu_value .o_input"),
|
||||
"a"
|
||||
);
|
||||
await cpHelpers.applyFilter(webClient);
|
||||
|
||||
// Add it to dashboard
|
||||
await cpHelpers.toggleFavoriteMenu(actionManager);
|
||||
await testUtils.dom.click($('.o_add_to_board > button'));
|
||||
await testUtils.dom.click($('.o_add_to_board div button'));
|
||||
await cpHelpers.toggleFavoriteMenu(webClient);
|
||||
await testUtils.dom.click($(".o_add_to_board > button"));
|
||||
await testUtils.dom.click($(".o_add_to_board div button"));
|
||||
|
||||
// Remove it
|
||||
await testUtils.dom.click(actionManager.el.querySelector('.o_facet_remove'));
|
||||
await testUtils.dom.click(webClient.el.querySelector(".o_facet_remove"));
|
||||
|
||||
// Add the second filter
|
||||
await cpHelpers.toggleFilterMenu(actionManager);
|
||||
await cpHelpers.toggleAddCustomFilter(actionManager);
|
||||
await testUtils.fields.editInput(actionManager.el.querySelector('.o_generator_menu_value .o_input'), "b");
|
||||
await cpHelpers.applyFilter(actionManager);
|
||||
await cpHelpers.toggleFilterMenu(webClient);
|
||||
await cpHelpers.toggleAddCustomFilter(webClient);
|
||||
await testUtils.fields.editInput(
|
||||
webClient.el.querySelector(".o_generator_menu_value .o_input"),
|
||||
"b"
|
||||
);
|
||||
await cpHelpers.applyFilter(webClient);
|
||||
// Add it to dashboard
|
||||
await cpHelpers.toggleFavoriteMenu(actionManager);
|
||||
await testUtils.dom.click(actionManager.el.querySelector('.o_add_to_board > button'));
|
||||
await testUtils.dom.click(actionManager.el.querySelector('.o_add_to_board div button'));
|
||||
|
||||
actionManager.destroy();
|
||||
await cpHelpers.toggleFavoriteMenu(webClient);
|
||||
await testUtils.dom.click(webClient.el.querySelector(".o_add_to_board > button"));
|
||||
await testUtils.dom.click(webClient.el.querySelector(".o_add_to_board div button"));
|
||||
});
|
||||
|
||||
QUnit.test('save a action domain to dashboard', async function (assert) {
|
||||
QUnit.test("save a action domain to dashboard", async function (assert) {
|
||||
// View domains are to be added to the dashboard domain
|
||||
assert.expect(1);
|
||||
|
||||
@@ -840,47 +856,43 @@ QUnit.test('save a action domain to dashboard', async function (assert) {
|
||||
var filter_domain = ["display_name", "ilike", "b"];
|
||||
|
||||
// The filter domain already contains the view domain, but is always added by dashboard..,
|
||||
var expected_domain = ['&', view_domain, '&', view_domain, filter_domain];
|
||||
var expected_domain = ["&", view_domain, "&", view_domain, filter_domain];
|
||||
|
||||
var actionManager = await createActionManager({
|
||||
data: this.data,
|
||||
archs: {
|
||||
'partner,false,list': '<list><field name="foo"/></list>',
|
||||
'partner,false,search': '<search></search>',
|
||||
},
|
||||
mockRPC: function (route, args) {
|
||||
if (route === '/board/add_to_dashboard') {
|
||||
assert.deepEqual(args.domain, expected_domain,
|
||||
"the correct domain should be sent");
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
serverData.views = {
|
||||
"partner,false,list": '<list><field name="foo"/></list>',
|
||||
"partner,false,search": "<search></search>",
|
||||
};
|
||||
|
||||
await actionManager.doAction({
|
||||
const mockRPC = (route, args) => {
|
||||
if (route === "/board/add_to_dashboard") {
|
||||
assert.deepEqual(args.domain, expected_domain, "the correct domain should be sent");
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
};
|
||||
|
||||
const webClient = await createWebClient({ serverData, mockRPC });
|
||||
|
||||
await doAction(webClient, {
|
||||
id: 1,
|
||||
res_model: 'partner',
|
||||
type: 'ir.actions.act_window',
|
||||
views: [[false, 'list']],
|
||||
res_model: "partner",
|
||||
type: "ir.actions.act_window",
|
||||
views: [[false, "list"]],
|
||||
domain: [view_domain],
|
||||
});
|
||||
|
||||
// Add a filter
|
||||
await cpHelpers.toggleFilterMenu(actionManager);
|
||||
await cpHelpers.toggleAddCustomFilter(actionManager);
|
||||
await cpHelpers.toggleFilterMenu(webClient);
|
||||
await cpHelpers.toggleAddCustomFilter(webClient);
|
||||
await testUtils.fields.editInput(
|
||||
actionManager.el.querySelector('.o_generator_menu_value .o_input'),
|
||||
webClient.el.querySelector(".o_generator_menu_value .o_input"),
|
||||
"b"
|
||||
);
|
||||
await cpHelpers.applyFilter(actionManager);
|
||||
await cpHelpers.applyFilter(webClient);
|
||||
// Add it to dashboard
|
||||
await cpHelpers.toggleFavoriteMenu(actionManager);
|
||||
await testUtils.dom.click(actionManager.el.querySelector('.o_add_to_board > button'));
|
||||
await cpHelpers.toggleFavoriteMenu(webClient);
|
||||
await testUtils.dom.click(webClient.el.querySelector(".o_add_to_board > button"));
|
||||
// add
|
||||
await testUtils.dom.click(actionManager.el.querySelector('.o_add_to_board div button'));
|
||||
|
||||
actionManager.destroy();
|
||||
await testUtils.dom.click(webClient.el.querySelector(".o_add_to_board div button"));
|
||||
});
|
||||
|
||||
QUnit.test("Views should be loaded in the user's language", async function (assert) {
|
||||
@@ -1041,62 +1053,71 @@ QUnit.test('click on a cell of pivot view inside dashboard', async function (ass
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('correctly save the time ranges of a reporting view in comparison mode', async function (assert) {
|
||||
assert.expect(1);
|
||||
QUnit.test(
|
||||
"correctly save the time ranges of a reporting view in comparison mode",
|
||||
async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
const unpatchDate = patchDate(2020, 6, 1, 11, 0, 0);
|
||||
const unpatchDate = patchDate(2020, 6, 1, 11, 0, 0);
|
||||
|
||||
this.data.partner.fields.date = { string: 'Date', type: 'date', sortable: true };
|
||||
serverData.models.partner.fields.date = {
|
||||
string: "Date",
|
||||
type: "date",
|
||||
sortable: true,
|
||||
};
|
||||
|
||||
const actionManager = await createActionManager({
|
||||
data: this.data,
|
||||
archs: {
|
||||
'partner,false,pivot': '<pivot><field name="foo"/></pivot>',
|
||||
'partner,false,search': '<search><filter name="Date" date="date"/></search>',
|
||||
},
|
||||
mockRPC: function (route, args) {
|
||||
if (route === '/board/add_to_dashboard') {
|
||||
serverData.views = {
|
||||
"partner,false,pivot": '<pivot><field name="foo"/></pivot>',
|
||||
"partner,false,search": '<search><filter name="Date" date="date"/></search>',
|
||||
};
|
||||
|
||||
const mockRPC = (route, args) => {
|
||||
if (route === "/board/add_to_dashboard") {
|
||||
assert.deepEqual(args.context_to_save.comparison, {
|
||||
comparisonId: "previous_period",
|
||||
fieldName: "date",
|
||||
fieldDescription: "Date",
|
||||
rangeDescription: "July 2020",
|
||||
range: ["&",["date", ">=", "2020-07-01"], ["date", "<=", "2020-07-31"]],
|
||||
comparisonRange: ["&", ["date", ">=", "2020-06-01"], ["date", "<=", "2020-06-30"]],
|
||||
range: ["&", ["date", ">=", "2020-07-01"], ["date", "<=", "2020-07-31"]],
|
||||
comparisonRange: [
|
||||
"&",
|
||||
["date", ">=", "2020-06-01"],
|
||||
["date", "<=", "2020-06-30"],
|
||||
],
|
||||
comparisonRangeDescription: "June 2020",
|
||||
});
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
await actionManager.doAction({
|
||||
id: 1,
|
||||
res_model: 'partner',
|
||||
type: 'ir.actions.act_window',
|
||||
views: [[false, 'pivot']],
|
||||
});
|
||||
const webClient = await createWebClient({ serverData, mockRPC });
|
||||
|
||||
// filter on July 2020
|
||||
await cpHelpers.toggleFilterMenu(actionManager);
|
||||
await cpHelpers.toggleMenuItem(actionManager, 'Date');
|
||||
await cpHelpers.toggleMenuItemOption(actionManager, 'Date', 'July');
|
||||
await doAction(webClient, {
|
||||
id: 1,
|
||||
res_model: "partner",
|
||||
type: "ir.actions.act_window",
|
||||
views: [[false, "pivot"]],
|
||||
});
|
||||
|
||||
// compare July 2020 to June 2020
|
||||
await cpHelpers.toggleComparisonMenu(actionManager);
|
||||
await cpHelpers.toggleMenuItem(actionManager, 0);
|
||||
// filter on July 2020
|
||||
await cpHelpers.toggleFilterMenu(webClient);
|
||||
await cpHelpers.toggleMenuItem(webClient, "Date");
|
||||
await cpHelpers.toggleMenuItemOption(webClient, "Date", "July");
|
||||
|
||||
// add the view to the dashboard
|
||||
await cpHelpers.toggleFavoriteMenu(actionManager);
|
||||
// compare July 2020 to June 2020
|
||||
await cpHelpers.toggleComparisonMenu(webClient);
|
||||
await cpHelpers.toggleMenuItem(webClient, 0);
|
||||
|
||||
await testUtils.dom.click($('.o_add_to_board > button'));
|
||||
await testUtils.fields.editInput($('.o_add_to_board input'), 'a name');
|
||||
await testUtils.dom.click($('.o_add_to_board div button'));
|
||||
// add the view to the dashboard
|
||||
await cpHelpers.toggleFavoriteMenu(webClient);
|
||||
|
||||
unpatchDate();
|
||||
actionManager.destroy();
|
||||
});
|
||||
await testUtils.dom.click($(".o_add_to_board > button"));
|
||||
await testUtils.fields.editInput($(".o_add_to_board input"), "a name");
|
||||
await testUtils.dom.click($(".o_add_to_board div button"));
|
||||
|
||||
unpatchDate();
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test('correctly display the time range descriptions of a reporting view in comparison mode', async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
@@ -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,93 @@
|
||||
/** @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 isNotificationDisplayed = false;
|
||||
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 (!isNotificationDisplayed) {
|
||||
// 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(() => {
|
||||
notification.add(
|
||||
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: () => {
|
||||
isNotificationDisplayed = false;
|
||||
},
|
||||
}
|
||||
);
|
||||
isNotificationDisplayed = true;
|
||||
}, 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,76 @@
|
||||
/** @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 } from "@web/../tests/webclient/helpers";
|
||||
import { assetsWatchdogService } from "@bus/js/services/assets_watchdog_service";
|
||||
import { click, nextTick, patchWithCleanup } from "@web/../tests/helpers/utils";
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
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("Bus Assets WatchDog", (hooks) => {
|
||||
let legacyServicesRegistry;
|
||||
hooks.beforeEach((assert) => {
|
||||
legacyServicesRegistry = new legacyRegistry();
|
||||
legacyServicesRegistry.add("bus_service", BusService);
|
||||
legacyServicesRegistry.add("local_storage", LocalStorageService);
|
||||
|
||||
serviceRegistry.add("assetsWatchdog", assetsWatchdogService);
|
||||
|
||||
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({
|
||||
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 displayedNotifications = new Set();
|
||||
|
||||
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 (displayedNotifications.has(key)) {
|
||||
return;
|
||||
}
|
||||
calendarNotifTimeouts[key] = browser.setTimeout(function () {
|
||||
const notificationRemove = notification.add(notif.message, {
|
||||
title: notif.title,
|
||||
type: "warning",
|
||||
sticky: true,
|
||||
onClose: () => {
|
||||
displayedNotifications.delete(key);
|
||||
},
|
||||
buttons: [
|
||||
{
|
||||
name: env._t("OK"),
|
||||
primary: true,
|
||||
onClick: async () => {
|
||||
await rpc("/calendar/notify_ack");
|
||||
notificationRemove();
|
||||
},
|
||||
},
|
||||
{
|
||||
name: env._t("Details"),
|
||||
onClick: async () => {
|
||||
await action.doAction(
|
||||
"calendar.action_calendar_event_notify",
|
||||
{
|
||||
resId: notif.event_id,
|
||||
}
|
||||
);
|
||||
notificationRemove();
|
||||
},
|
||||
},
|
||||
{
|
||||
name: env._t("Snooze"),
|
||||
onClick: () => {
|
||||
notificationRemove();
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
displayedNotifications.add(key);
|
||||
}, 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);
|
||||
@@ -53,10 +53,10 @@
|
||||
}
|
||||
|
||||
.o_accepted {
|
||||
color: theme-color('success');
|
||||
@extend .text-success;
|
||||
}
|
||||
.o_declined {
|
||||
color: theme-color('danger');
|
||||
@extend .text-danger;
|
||||
}
|
||||
}
|
||||
.o_meeting_filter {
|
||||
|
||||
@@ -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,219 @@
|
||||
/** @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 } from "@web/../tests/webclient/helpers";
|
||||
import { calendarNotificationService } from "@calendar/js/services/calendar_notification_service";
|
||||
import { click, nextTick, patchWithCleanup } from "@web/../tests/helpers/utils";
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
const LocalStorageService = AbstractStorageService.extend({
|
||||
storage: new RamStorage(),
|
||||
});
|
||||
const serviceRegistry = registry.category("services");
|
||||
|
||||
QUnit.module("Calendar Notification", (hooks) => {
|
||||
let legacyServicesRegistry;
|
||||
hooks.beforeEach(() => {
|
||||
legacyServicesRegistry = new legacyRegistry();
|
||||
legacyServicesRegistry.add("bus_service", BusService);
|
||||
legacyServicesRegistry.add("local_storage", LocalStorageService);
|
||||
|
||||
serviceRegistry.add("calendarNotification", calendarNotificationService);
|
||||
|
||||
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({
|
||||
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({
|
||||
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({
|
||||
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");
|
||||
}
|
||||
);
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
<templates>
|
||||
<t t-name="GoogleDriveMenu" owl="1">
|
||||
<li>
|
||||
<ul class="o_embed_menu">
|
||||
<ul class="o_embed_menu p-0">
|
||||
<li t-foreach="props.items" t-as="gdriveItem" t-key="gdriveItem.id"
|
||||
role="menuitem"
|
||||
class="o_menu_item oe_share_gdoc_item dropdown-item"
|
||||
|
||||
@@ -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/**/*',
|
||||
|
||||
@@ -36,7 +36,7 @@ tour.register('hr_expense_tour' , {
|
||||
content: _t('<p>Select expenses to submit them to your manager</p>'),
|
||||
position: 'bottom'
|
||||
}, {
|
||||
trigger: '.o_dropdown_toggler_btn',
|
||||
trigger: '.o_dropdown_toggler',
|
||||
extra_trigger: ".o_expense_tree",
|
||||
content: _t('<p>Click on <b> Action Create Report </b> to submit selected expenses to your manager</p>'),
|
||||
position: 'right',
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
.o_timeoff_calendar .o_content {
|
||||
.o_timeoff_container {
|
||||
height: 6rem;
|
||||
box-shadow: inset 0 -1px 0 $border-color;
|
||||
|
||||
+ .o_calendar_container {
|
||||
height: calc(100% - 6rem);
|
||||
@@ -12,7 +13,7 @@
|
||||
}
|
||||
|
||||
.o_timeoff_card {
|
||||
border-right: #adb5bd solid 2px;
|
||||
border-right: $border-color solid 2px;
|
||||
text-align: center;
|
||||
margin-top: 4px;
|
||||
div {
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
|
||||
<t t-name="hr_trash_button">
|
||||
<td class="o_list_record_remove pr-3">
|
||||
<button name="delete" arial-label="Delete row" class="btn btn-secondary">
|
||||
<button name="delete" arial-label="Delete row" class="btn btn-link text-danger">
|
||||
<i class="fa fa-trash"/>
|
||||
</button>
|
||||
</td>
|
||||
|
||||
@@ -20,9 +20,6 @@ This module adds certification to resumé for employees.
|
||||
],
|
||||
'auto_install': True,
|
||||
'assets': {
|
||||
'web.assets_backend': [
|
||||
'hr_skills_survey/static/src/scss/**/*',
|
||||
],
|
||||
'web.assets_qweb': [
|
||||
'hr_skills_survey/static/src/xml/**/*',
|
||||
],
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
.o_resume_line {
|
||||
.o_resume_line_icon.icon_trophy {
|
||||
color: theme-color('warning');
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@
|
||||
<t t-if="data.description" t-esc="data.description"/>
|
||||
</div>
|
||||
<div class="o_resume_line_icon icon_trophy col-lg-1">
|
||||
<i class="fa fa-trophy fa-2x"></i>
|
||||
<i class="fa fa-trophy fa-2x text-warning"></i>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
odoo.define('hr_timesheet.timesheet_uom', function (require) {
|
||||
'use strict';
|
||||
|
||||
const { registry } = require("@web/core/registry");
|
||||
const basicFields = require('web.basic_fields');
|
||||
const fieldUtils = require('web.field_utils');
|
||||
|
||||
@@ -11,7 +12,6 @@ const fieldRegistry = require('web.field_registry');
|
||||
require('web._field_registry');
|
||||
|
||||
const session = require('web.session');
|
||||
const AbstractWebClient = require('web.AbstractWebClient');
|
||||
|
||||
const TimesheetUOMMultiCompanyMixin = {
|
||||
init: function(parent, name, record, options) {
|
||||
@@ -90,9 +90,25 @@ const FieldTimesheetTime = basicFields.FieldFloatTime.extend(TimesheetUOMMultiCo
|
||||
}
|
||||
});
|
||||
|
||||
AbstractWebClient.include({
|
||||
init: function () {
|
||||
this._super(...arguments);
|
||||
const timesheetUomService = {
|
||||
dependencies: ["legacy_session"],
|
||||
start() {
|
||||
const timesheetUomInfo = {
|
||||
widget: null,
|
||||
factor: 1,
|
||||
};
|
||||
if (session.user_context &&
|
||||
session.user_context.allowed_company_ids &&
|
||||
session.user_context.allowed_company_ids.length) {
|
||||
const currentCompanyId = session.user_context.allowed_company_ids[0];
|
||||
const currentCompany = session.user_companies.allowed_companies[currentCompanyId];
|
||||
const currentCompanyTimesheetUOMId = currentCompany.timesheet_uom_id || false;
|
||||
timesheetUomInfo.factor = currentCompany.timesheet_uom_factor || 1;
|
||||
if (currentCompanyTimesheetUOMId) {
|
||||
timesheetUomInfo.widget = session.uom_ids[currentCompanyTimesheetUOMId].timesheet_widget;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Binding depending on Company Preference
|
||||
*
|
||||
@@ -100,10 +116,8 @@ AbstractWebClient.include({
|
||||
* Simply match the 'timesheet_uom' widget key with the correct
|
||||
* implementation (float_time, float_toggle, ...). The default
|
||||
* value will be 'float_factor'.
|
||||
**/
|
||||
const widgetName = this.currentCompanyTimesheetUOM &&
|
||||
this.currentCompanyTimesheetUOM.timesheet_widget ||
|
||||
'float_factor';
|
||||
**/
|
||||
const widgetName = timesheetUomInfo.widget || 'float_factor';
|
||||
|
||||
let FieldTimesheetUom = null;
|
||||
|
||||
@@ -113,9 +127,9 @@ AbstractWebClient.include({
|
||||
FieldTimesheetUom = FieldTimesheetTime;
|
||||
} else {
|
||||
FieldTimesheetUom = (
|
||||
fieldRegistry.get(widgetName) &&
|
||||
fieldRegistry.get(widgetName).extend({ })
|
||||
) || FieldTimesheetFactor;
|
||||
fieldRegistry.get(widgetName) &&
|
||||
fieldRegistry.get(widgetName).extend({ })
|
||||
) || FieldTimesheetFactor;
|
||||
}
|
||||
fieldRegistry.add('timesheet_uom', FieldTimesheetUom);
|
||||
|
||||
@@ -133,7 +147,7 @@ AbstractWebClient.include({
|
||||
// bind the formatter and parser method, and tweak the options
|
||||
const _tweak_options = (options) => {
|
||||
if (!_.contains(options, 'factor')) {
|
||||
options.factor = this.currentCompanyTimesheetUOMFactor;
|
||||
options.factor = timesheetUomInfo.factor;
|
||||
}
|
||||
return options;
|
||||
};
|
||||
@@ -161,13 +175,16 @@ AbstractWebClient.include({
|
||||
const parser = fieldUtils.parse[FieldTimesheetUom.prototype.formatType];
|
||||
return parser(value, field, options);
|
||||
};
|
||||
return timesheetUomInfo;
|
||||
},
|
||||
});
|
||||
};
|
||||
registry.category("services").add("timesheet_uom", timesheetUomService);
|
||||
|
||||
return {
|
||||
FieldTimesheetFactor,
|
||||
FieldTimesheetTime,
|
||||
FieldTimesheetToggle,
|
||||
timesheetUomService,
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
@@ -19,7 +19,7 @@ QUnit.module('Timesheet UOM Widgets', function (hooks) {
|
||||
sessionUOMIdsBackup = session.uom_ids || false;
|
||||
sessionUIDBackup = session.uid || false;
|
||||
});
|
||||
hooks.after(function (assert) {
|
||||
hooks.after(async function (assert) {
|
||||
// Restores the session
|
||||
const sessionToApply = Object.assign(
|
||||
{ },
|
||||
@@ -35,7 +35,7 @@ QUnit.module('Timesheet UOM Widgets', function (hooks) {
|
||||
sessionUIDBackup && {
|
||||
uid: sessionUIDBackup,
|
||||
} || { });
|
||||
env.triggerAbstractWebClientInit(sessionToApply, true);
|
||||
await env.patchSessionAndStartServices(sessionToApply, true);
|
||||
});
|
||||
QUnit.module('GraphView', function (hooks) {
|
||||
QUnit.test('the timesheet_graph view data are multiplied by a factor that is company related', async function (assert) {
|
||||
|
||||
@@ -2,14 +2,16 @@ odoo.define("hr_timesheet.timesheet_uom_tests_env", function (require) {
|
||||
"use strict";
|
||||
|
||||
const session = require('web.session');
|
||||
const AbstractWebClient = require('web.AbstractWebClient');
|
||||
const { createView } = require("web.test_utils");
|
||||
const ListView = require('web.ListView');
|
||||
const { timesheetUomService } = require('hr_timesheet.timesheet_uom');
|
||||
const { makeTestEnv } = require("@web/../tests/helpers/mock_env");
|
||||
const { registry } = require("@web/core/registry");
|
||||
|
||||
/**
|
||||
* Sets the timesheet related widgets testing environment up.
|
||||
*/
|
||||
const SetupTimesheetUOMWidgetsTestEnvironment = function () {
|
||||
function SetupTimesheetUOMWidgetsTestEnvironment () {
|
||||
this.allowedCompanies = {
|
||||
1: {
|
||||
id: 1,
|
||||
@@ -138,16 +140,18 @@ const SetupTimesheetUOMWidgetsTestEnvironment = function () {
|
||||
],
|
||||
},
|
||||
};
|
||||
this.triggerAbstractWebClientInit = function (sessionToApply, doNotUseEnvSession = false) {
|
||||
this.patchSessionAndStartServices = async function (sessionToApply, doNotUseEnvSession = false) {
|
||||
/*
|
||||
Adds the timesheet_uom to the fieldRegistry by setting the session and
|
||||
instantiating AbstractWebClient as the process of adding the right widget
|
||||
is included in the init function of AbstractWebClient.
|
||||
starting the timesheet_uom service which registers the widget in the registry.
|
||||
*/
|
||||
session.user_companies = Object.assign(
|
||||
{ },
|
||||
!doNotUseEnvSession && this.session.user_companies || { },
|
||||
sessionToApply && sessionToApply.user_companies);
|
||||
if (Object.keys(session.user_companies).length === 0) {
|
||||
delete session.user_companies;
|
||||
}
|
||||
session.user_context = Object.assign(
|
||||
{ },
|
||||
!doNotUseEnvSession && this.session.user_context || { },
|
||||
@@ -162,15 +166,16 @@ const SetupTimesheetUOMWidgetsTestEnvironment = function () {
|
||||
if (sessionToApply && 'uid' in sessionToApply) {
|
||||
session.uid = sessionToApply.uid;
|
||||
}
|
||||
if (!this.abstractWebClient) {
|
||||
this.abstractWebClient = new AbstractWebClient();
|
||||
} else {
|
||||
this.abstractWebClient.init();
|
||||
const serviceRegistry = registry.category("services");
|
||||
if (!serviceRegistry.contains("timesheet_uom")) {
|
||||
// Remove dependency on legacy_session since we're patching the session directly
|
||||
serviceRegistry.add("timesheet_uom", Object.assign({}, timesheetUomService, { dependencies: [] }));
|
||||
}
|
||||
await makeTestEnv(); // Start services
|
||||
};
|
||||
this.createView = async function(options) {
|
||||
this.createView = async function (options) {
|
||||
const sessionToApply = options && options.session || { };
|
||||
this.triggerAbstractWebClientInit(sessionToApply);
|
||||
await this.patchSessionAndStartServices(sessionToApply);
|
||||
return await createView(Object.assign(
|
||||
{
|
||||
View: ListView,
|
||||
|
||||
@@ -12,7 +12,7 @@ QUnit.module('Timesheet UOM Widgets', function (hooks) {
|
||||
let sessionUserContextBackup;
|
||||
let sessionUOMIdsBackup;
|
||||
let sessionUIDBackup;
|
||||
hooks.before(function (assert) {
|
||||
hooks.beforeEach(async function (assert) {
|
||||
env = new SetupTimesheetUOMWidgetsTestEnvironment();
|
||||
// Backups session parts that this testing module will alter in order to restore it at the end.
|
||||
sessionUserCompaniesBackup = session.user_companies || false;
|
||||
@@ -20,7 +20,7 @@ QUnit.module('Timesheet UOM Widgets', function (hooks) {
|
||||
sessionUOMIdsBackup = session.uom_ids || false;
|
||||
sessionUIDBackup = session.uid || false;
|
||||
});
|
||||
hooks.after(function (assert) {
|
||||
hooks.afterEach(async function (assert) {
|
||||
// Restores the session
|
||||
const sessionToApply = Object.assign(
|
||||
{ },
|
||||
@@ -36,13 +36,13 @@ QUnit.module('Timesheet UOM Widgets', function (hooks) {
|
||||
sessionUIDBackup && {
|
||||
uid: sessionUIDBackup,
|
||||
} || { });
|
||||
env.triggerAbstractWebClientInit(sessionToApply, true);
|
||||
await env.patchSessionAndStartServices(sessionToApply, true);
|
||||
});
|
||||
QUnit.module('timesheet_uom', function (hooks) {
|
||||
QUnit.module('fieldRegistry', function (hooks) {
|
||||
let FieldTimesheetTimeBackup;
|
||||
let FieldTimesheetToggleBackup;
|
||||
hooks.before(function (assert) {
|
||||
hooks.beforeEach(function (assert) {
|
||||
// Backups the FieldTimesheetTime widget as it will be altered in this testing module
|
||||
// in order to to ease testing.
|
||||
FieldTimesheetTimeBackup = TimesheetUOM.FieldTimesheetTime;
|
||||
@@ -60,11 +60,11 @@ QUnit.module('Timesheet UOM Widgets', function (hooks) {
|
||||
},
|
||||
});
|
||||
});
|
||||
hooks.after(function (hooks) {
|
||||
hooks.afterEach(async function (hooks) {
|
||||
// Restores the widgets and trigger reload in FieldRegistry.
|
||||
TimesheetUOM.FieldTimesheetTime = FieldTimesheetTimeBackup;
|
||||
TimesheetUOM.FieldTimesheetToggle = FieldTimesheetToggleBackup;
|
||||
env.triggerAbstractWebClientInit({ }, true);
|
||||
await env.patchSessionAndStartServices({ }, true);
|
||||
});
|
||||
QUnit.test('the timesheet_uom widget added to the fieldRegistry is company related', async function (assert) {
|
||||
assert.expect(2);
|
||||
@@ -108,14 +108,14 @@ QUnit.module('Timesheet UOM Widgets', function (hooks) {
|
||||
QUnit.test('the timesheet_uom_factor usage in formatters and parsers is company related', async function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
env.triggerAbstractWebClientInit();
|
||||
await env.patchSessionAndStartServices();
|
||||
assert.strictEqual(fieldUtils.format.timesheet_uom(1), '01:00', 'The format is taking the timesheet_uom_factor into account');
|
||||
assert.strictEqual(fieldUtils.parse.timesheet_uom('01:00'), 1, 'The parsing is taking the timesheet_uom_factor into account');
|
||||
|
||||
const sessionToApply = {
|
||||
user_context: env.singleCompanyDayUOMUser,
|
||||
};
|
||||
env.triggerAbstractWebClientInit(sessionToApply);
|
||||
await env.patchSessionAndStartServices(sessionToApply);
|
||||
assert.strictEqual(fieldUtils.format.timesheet_uom(8), '1.00', 'The format is taking the timesheet_uom_factor into account');
|
||||
assert.strictEqual(fieldUtils.parse.timesheet_uom('1.00'), 8, 'The parsing is taking the timesheet_uom_factor into account');
|
||||
});
|
||||
@@ -125,12 +125,12 @@ QUnit.module('Timesheet UOM Widgets', function (hooks) {
|
||||
let sessionToApply = {
|
||||
user_context: env.multiCompanyHourUOMUser,
|
||||
};
|
||||
env.triggerAbstractWebClientInit(sessionToApply);
|
||||
await env.patchSessionAndStartServices(sessionToApply);
|
||||
assert.strictEqual(fieldUtils.format.timesheet_uom(1), '01:00', 'The format is taking the timesheet_uom_factor into account');
|
||||
assert.strictEqual(fieldUtils.parse.timesheet_uom('01:00'), 1, 'The parsing is taking the timesheet_uom_factor into account');
|
||||
|
||||
sessionToApply.user_context = env.singleCompanyDayUOMUser;
|
||||
env.triggerAbstractWebClientInit(sessionToApply);
|
||||
await env.patchSessionAndStartServices(sessionToApply);
|
||||
assert.strictEqual(fieldUtils.format.timesheet_uom(8), '1.00', 'The format is taking the timesheet_uom_factor into account');
|
||||
assert.strictEqual(fieldUtils.parse.timesheet_uom('1.00'), 8, 'The parsing is taking the timesheet_uom_factor into account');
|
||||
});
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
odoo.define('iap.CrashManager', function (require) {
|
||||
"use strict";
|
||||
|
||||
var ajax = require('web.ajax');
|
||||
var core = require('web.core');
|
||||
var CrashManager = require('web.CrashManager').CrashManager;
|
||||
var Dialog = require('web.Dialog');
|
||||
|
||||
var _t = core._t;
|
||||
var QWeb = core.qweb;
|
||||
|
||||
CrashManager.include({
|
||||
/**
|
||||
* Change button string depending if it's Enterprise and trial available
|
||||
* (Only change the text message in the button, doesn't change IAP side validation)
|
||||
*
|
||||
* @param {boolean} isTrial
|
||||
* @returns {string}
|
||||
* @private
|
||||
*/
|
||||
_getButtonMessage: function (isTrial){
|
||||
var isEnterprise = _.last(odoo.session_info.server_version_info) === 'e';
|
||||
return isTrial && isEnterprise ? _t('Start a Trial at Odoo') : _t('Buy credits');
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
rpc_error: function (error) {
|
||||
var self = this;
|
||||
if (error.data.name === "odoo.addons.iap.tools.iap_tools.InsufficientCreditError") {
|
||||
var error_data = JSON.parse(error.data.message);
|
||||
ajax.jsonRpc('/web/dataset/call_kw', 'call', {
|
||||
model: 'iap.account',
|
||||
method: 'get_credits_url',
|
||||
args: [],
|
||||
kwargs: {
|
||||
base_url: error_data.base_url,
|
||||
service_name: error_data.service_name,
|
||||
credit: error_data.credit,
|
||||
trial: error_data.trial
|
||||
}
|
||||
}).then(function (url) {
|
||||
var content = $(QWeb.render('iap.redirect_to_odoo_credit', {
|
||||
data: error_data,
|
||||
}));
|
||||
if (error_data.body) {
|
||||
content.css('padding', 0);
|
||||
}
|
||||
new Dialog(this, {
|
||||
size: 'large',
|
||||
title: error_data.title || _t("Insufficient Balance"),
|
||||
$content: content,
|
||||
buttons: [{
|
||||
text: self._getButtonMessage(error_data.trial),
|
||||
classes : "btn-primary",
|
||||
click: function () {
|
||||
window.open(url, '_blank');
|
||||
},
|
||||
close:true,
|
||||
}, {
|
||||
text: _t("Cancel"),
|
||||
close: true,
|
||||
}],
|
||||
}).open();
|
||||
});
|
||||
} else {
|
||||
this._super.apply(this, arguments);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
});
|
||||
@@ -1,26 +0,0 @@
|
||||
odoo.define('iap.redirect_odoo_credit_widget', function(require) {
|
||||
"use strict";
|
||||
|
||||
var AbstractAction = require('web.AbstractAction');
|
||||
var core = require('web.core');
|
||||
|
||||
|
||||
var IapOdooCreditRedirect = AbstractAction.extend({
|
||||
template: 'iap.redirect_to_odoo_credit',
|
||||
events : {
|
||||
"click .redirect_confirm" : "odoo_redirect",
|
||||
},
|
||||
init: function (parent, action) {
|
||||
this._super(parent, action);
|
||||
this.url = action.params.url;
|
||||
},
|
||||
|
||||
odoo_redirect: function () {
|
||||
window.open(this.url, '_blank');
|
||||
this.do_action({type: 'ir.actions.act_window_close'});
|
||||
// framework.redirect(this.url);
|
||||
},
|
||||
|
||||
});
|
||||
core.action_registry.add('iap_odoo_credit_redirect', IapOdooCreditRedirect);
|
||||
});
|
||||
@@ -0,0 +1,54 @@
|
||||
/** @odoo-module */
|
||||
import { Dialog } from "@web/core/dialog/dialog";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { useService } from "@web/core/service_hook";
|
||||
|
||||
class InsufficientCreditDialog extends Dialog {
|
||||
setup() {
|
||||
super.setup(...arguments);
|
||||
this.orm = useService("orm");
|
||||
}
|
||||
|
||||
async willStart() {
|
||||
const { errorData } = this.props;
|
||||
this.url = await this.orm.call("iap.account", "get_credits_url", [], {
|
||||
base_url: errorData.base_url,
|
||||
service_name: errorData.service_name,
|
||||
credit: errorData.credit,
|
||||
trial: errorData.trial,
|
||||
});
|
||||
this.style = errorData.body ? "padding:0;" : "";
|
||||
const { _t } = this.env;
|
||||
const { isEnterprise } = odoo.info;
|
||||
if (errorData.trial && isEnterprise) {
|
||||
this.buttonMessage = _t("Start a Trial at Odoo");
|
||||
} else {
|
||||
this.buttonMessage = _t("Buy credits");
|
||||
}
|
||||
}
|
||||
|
||||
buyCredits() {
|
||||
window.open(this.url, "_blank");
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
InsufficientCreditDialog.bodyTemplate = "iap.redirect_to_odoo_credit";
|
||||
InsufficientCreditDialog.footerTemplate = "iap.InsufficientCreditFooter";
|
||||
|
||||
function insufficientCreditHandler(env, error, originalError) {
|
||||
if (!originalError) {
|
||||
return false;
|
||||
}
|
||||
const { data } = originalError;
|
||||
if (data && data.name === "odoo.addons.iap.tools.iap_tools.InsufficientCreditError") {
|
||||
env.services.dialog.add(InsufficientCreditDialog, {
|
||||
errorData: JSON.parse(data.message),
|
||||
});
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
registry
|
||||
.category("error_handlers")
|
||||
.add("insufficientCreditHandler", insufficientCreditHandler, { sequence: 0 });
|
||||
@@ -2,20 +2,25 @@
|
||||
<template id="template" xml:space="preserve">
|
||||
|
||||
<!-- LAYOUT TEMPLATES -->
|
||||
<div t-name="iap.redirect_to_odoo_credit">
|
||||
<t t-if="data.body">
|
||||
<div t-raw="data.body"/>
|
||||
<div t-name="iap.redirect_to_odoo_credit" t-att-style="style" owl="1">
|
||||
<t t-if="props.errorData.body">
|
||||
<div t-raw="props.errorData.body"/>
|
||||
</t>
|
||||
<t t-if="!data.body">
|
||||
<t t-if="data.message">
|
||||
<span t-esc="data.message"/>
|
||||
<t t-if="!props.errorData.body">
|
||||
<t t-if="props.errorData.message">
|
||||
<span t-esc="props.errorData.message"/>
|
||||
</t>
|
||||
<t t-if="!data.message">
|
||||
<t t-if="!props.errorData.message">
|
||||
<span>Insufficient credit to perform this service.</span>
|
||||
</t>
|
||||
</t>
|
||||
</div>
|
||||
|
||||
<div t-name="iap.InsufficientCreditFooter" owl="1">
|
||||
<button class="btn btn-primary" t-on-click="buyCredits" t-esc="buttonMessage"/>
|
||||
<button class="btn" t-on-click="close">Cancel</button>
|
||||
</div>
|
||||
|
||||
<t t-extend="DashboardMain">
|
||||
<t t-jquery=".o_web_settings_apps" t-operation="after">
|
||||
<div class="o_web_settings_iap"></div>
|
||||
|
||||
@@ -93,42 +93,42 @@ Help your customers with this chat, and analyse their feedback.
|
||||
'web/static/lib/qweb/qweb2.js',
|
||||
# Odoo JS Framework
|
||||
'web/static/lib/owl/owl.js',
|
||||
'web/static/src/js/promise_extension.js',
|
||||
'web/static/src/legacy/js/promise_extension.js',
|
||||
'web/static/src/js/boot.js',
|
||||
'web/static/src/js/libs/download.js',
|
||||
'web/static/src/js/libs/content-disposition.js',
|
||||
'web/static/src/js/libs/pdfjs.js',
|
||||
'web/static/src/js/services/config.js',
|
||||
'web/static/src/js/core/abstract_service.js',
|
||||
'web/static/src/js/core/class.js',
|
||||
'web/static/src/js/core/collections.js',
|
||||
'web/static/src/js/core/translation.js',
|
||||
'web/static/src/js/core/ajax.js',
|
||||
'web/static/src/legacy/js/libs/download.js',
|
||||
'web/static/src/legacy/js/libs/content-disposition.js',
|
||||
'web/static/src/legacy/js/libs/pdfjs.js',
|
||||
'web/static/src/legacy/js/services/config.js',
|
||||
'web/static/src/legacy/js/core/abstract_service.js',
|
||||
'web/static/src/legacy/js/core/class.js',
|
||||
'web/static/src/legacy/js/core/collections.js',
|
||||
'web/static/src/legacy/js/core/translation.js',
|
||||
'web/static/src/legacy/js/core/ajax.js',
|
||||
'im_livechat/static/src/js/ajax_external.js',
|
||||
'web/static/src/js/core/time.js',
|
||||
'web/static/src/js/core/mixins.js',
|
||||
'web/static/src/js/core/service_mixins.js',
|
||||
'web/static/src/js/core/rpc.js',
|
||||
'web/static/src/js/core/widget.js',
|
||||
'web/static/src/js/core/registry.js',
|
||||
'web/static/src/js/core/session.js',
|
||||
'web/static/src/js/core/concurrency.js',
|
||||
'web/static/src/js/core/utils.js',
|
||||
'web/static/src/js/core/dom.js',
|
||||
'web/static/src/js/core/qweb.js',
|
||||
'web/static/src/js/core/bus.js',
|
||||
'web/static/src/js/services/core.js',
|
||||
'web/static/src/js/core/local_storage.js',
|
||||
'web/static/src/js/core/ram_storage.js',
|
||||
'web/static/src/js/core/abstract_storage_service.js',
|
||||
'web/static/src/js/common_env.js',
|
||||
'web/static/src/js/public/lazyloader.js',
|
||||
'web/static/src/js/public/public_env.js',
|
||||
'web/static/src/js/public/public_root.js',
|
||||
'web/static/src/js/public/public_root_instance.js',
|
||||
'web/static/src/js/public/public_widget.js',
|
||||
'web/static/src/js/services/ajax_service.js',
|
||||
'web/static/src/js/services/local_storage_service.js',
|
||||
'web/static/src/legacy/js/core/time.js',
|
||||
'web/static/src/legacy/js/core/mixins.js',
|
||||
'web/static/src/legacy/js/core/service_mixins.js',
|
||||
'web/static/src/legacy/js/core/rpc.js',
|
||||
'web/static/src/legacy/js/core/widget.js',
|
||||
'web/static/src/legacy/js/core/registry.js',
|
||||
'web/static/src/legacy/js/core/session.js',
|
||||
'web/static/src/legacy/js/core/concurrency.js',
|
||||
'web/static/src/legacy/js/core/utils.js',
|
||||
'web/static/src/legacy/js/core/dom.js',
|
||||
'web/static/src/legacy/js/core/qweb.js',
|
||||
'web/static/src/legacy/js/core/bus.js',
|
||||
'web/static/src/legacy/js/services/core.js',
|
||||
'web/static/src/legacy/js/core/local_storage.js',
|
||||
'web/static/src/legacy/js/core/ram_storage.js',
|
||||
'web/static/src/legacy/js/core/abstract_storage_service.js',
|
||||
'web/static/src/legacy/js/common_env.js',
|
||||
'web/static/src/legacy/js/public/lazyloader.js',
|
||||
'web/static/src/legacy/js/public/public_env.js',
|
||||
'web/static/src/legacy/js/public/public_root.js',
|
||||
'web/static/src/legacy/js/public/public_root_instance.js',
|
||||
'web/static/src/legacy/js/public/public_widget.js',
|
||||
'web/static/src/legacy/js/services/ajax_service.js',
|
||||
'web/static/src/legacy/js/services/local_storage_service.js',
|
||||
# Bus, Mail, Livechat
|
||||
'bus/static/src/js/longpolling_bus.js',
|
||||
'bus/static/src/js/crosstab_bus.js',
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -80,9 +80,9 @@
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
$o-mail-activity-default-color: gray('300') !default;
|
||||
$o-mail-activity-overdue-color: darken(theme-color('danger'), 10%) !default;
|
||||
$o-mail-activity-planned-color: darken(theme-color('success'), 10%) !default;
|
||||
$o-mail-activity-today-color: darken(theme-color('warning'), 10%) !default;
|
||||
$o-mail-activity-overdue-color: o-text-color('danger') !default;
|
||||
$o-mail-activity-planned-color: o-text-color('success') !default;
|
||||
$o-mail-activity-today-color: o-text-color('warning') !default;
|
||||
|
||||
|
||||
.o_Activity_deadlineDateText {
|
||||
|
||||
@@ -11,9 +11,9 @@
|
||||
</t>
|
||||
<div class="o_Activity_iconContainer"
|
||||
t-att-class="{
|
||||
'bg-success-full': activity.state === 'planned',
|
||||
'bg-warning-full': activity.state === 'today',
|
||||
'bg-danger-full': activity.state === 'overdue',
|
||||
'bg-success': activity.state === 'planned',
|
||||
'bg-warning': activity.state === 'today',
|
||||
'bg-danger': activity.state === 'overdue',
|
||||
}"
|
||||
>
|
||||
<i class="o_Activity_icon fa" t-attf-class="{{ activity.icon }}"/>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -18,10 +18,10 @@
|
||||
color: gray('600');
|
||||
|
||||
&.o-following {
|
||||
color: $green;
|
||||
color: o-text-color('success');
|
||||
}
|
||||
|
||||
&.o-unfollow {
|
||||
color: $orange;
|
||||
color: o-text-color('warning')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -366,7 +366,7 @@
|
||||
font-style: italic;
|
||||
|
||||
&.o-author {
|
||||
color: theme-color('danger');
|
||||
color: o-text-color('danger');
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
@@ -376,7 +376,7 @@
|
||||
cursor: pointer;
|
||||
|
||||
&.o-error {
|
||||
color: $red;
|
||||
color: o-text-color('danger');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -50,13 +50,13 @@
|
||||
@include o-position-absolute($bottom: 0, $right: 0);
|
||||
|
||||
&.planned {
|
||||
color: theme-color('success');
|
||||
@extend .text-success;
|
||||
}
|
||||
&.overdue {
|
||||
color: theme-color('danger');
|
||||
@extend .text-danger;
|
||||
}
|
||||
&.today {
|
||||
color: theme-color('warning');
|
||||
@extend .text-warning;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -116,7 +116,7 @@
|
||||
padding:5px;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
color: theme-color('success');
|
||||
color: o-text-color('success');
|
||||
}
|
||||
}
|
||||
.o_kanban_counter {
|
||||
|
||||
@@ -12,9 +12,6 @@
|
||||
&.o_user_online {
|
||||
color: $o-enterprise-primary-color;
|
||||
}
|
||||
&.o_user_idle {
|
||||
color: theme-color('warning');
|
||||
}
|
||||
&.fa-stack {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
|
||||
@@ -130,13 +130,13 @@
|
||||
}
|
||||
|
||||
.o_activity_color_planned {
|
||||
color: darken(theme-color('success'), 10%);
|
||||
@extend .text-success;
|
||||
}
|
||||
.o_activity_color_overdue {
|
||||
color: darken(theme-color('danger'), 10%);
|
||||
@extend .text-danger;
|
||||
}
|
||||
.o_activity_color_today {
|
||||
color: darken(theme-color('warning'), 10%);
|
||||
@extend .text-warning;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,12 +18,16 @@ 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, getActionManagerServerData } from "@web/../tests/webclient/helpers";
|
||||
|
||||
import { ComponentAdapter } from "web.OwlCompatibility";
|
||||
import LegacyMockServer from "web.MockServer";
|
||||
import LegacyRegistry from "web.Registry";
|
||||
|
||||
const {
|
||||
addMockEnvironment,
|
||||
@@ -384,8 +388,8 @@ async function createRootComponent(self, Component, { props = {}, target }) {
|
||||
* @param {Object} [param0.env={}]
|
||||
* @param {function} [param0.mockFetch]
|
||||
* @param {function} [param0.mockRPC]
|
||||
* @param {boolean} [param0.hasActionManager=false] if set, use
|
||||
* createActionManager.
|
||||
* @param {boolean} [param0.hasWebClient=false] if set, use
|
||||
* createWebClient
|
||||
* @param {boolean} [param0.hasChatWindow=false] if set, mount chat window
|
||||
* service.
|
||||
* @param {boolean} [param0.hasDiscuss=false] if set, mount discuss app.
|
||||
@@ -441,7 +445,7 @@ async function start(param0 = {}) {
|
||||
};
|
||||
const {
|
||||
env: providedEnv,
|
||||
hasActionManager = false,
|
||||
hasWebClient = false,
|
||||
hasChatWindow = false,
|
||||
hasDialog = false,
|
||||
hasDiscuss = false,
|
||||
@@ -456,7 +460,7 @@ async function start(param0 = {}) {
|
||||
throw Error(`Unknown parameter value ${waitUntilMessagingCondition} for 'waitUntilMessaging'.`);
|
||||
}
|
||||
delete param0.env;
|
||||
delete param0.hasActionManager;
|
||||
delete param0.hasWebClient;
|
||||
delete param0.hasChatWindow;
|
||||
delete param0.hasDiscuss;
|
||||
delete param0.hasMessagingMenu;
|
||||
@@ -545,8 +549,47 @@ async function start(param0 = {}) {
|
||||
}
|
||||
}
|
||||
});
|
||||
} else if (hasActionManager) {
|
||||
widget = await createActionManager(kwargs);
|
||||
} else if (hasWebClient) {
|
||||
let serverData;
|
||||
if (!kwargs.serverData) {
|
||||
serverData = getActionManagerServerData();
|
||||
} else {
|
||||
serverData = kwargs.serverData;
|
||||
delete kwargs.serverData;
|
||||
}
|
||||
|
||||
if (kwargs.actions) {
|
||||
const actions = {};
|
||||
kwargs.actions.forEach((act) => {
|
||||
actions[act.xml_id || act.id] = act;
|
||||
});
|
||||
Object.assign(serverData.actions, actions);
|
||||
delete kwargs.actions;
|
||||
}
|
||||
|
||||
Object.assign(serverData.views, kwargs.archs);
|
||||
delete kwargs.archs;
|
||||
|
||||
Object.assign(serverData.models, kwargs.data);
|
||||
delete kwargs.data;
|
||||
|
||||
const mockRPC = kwargs.mockRPC;
|
||||
delete kwargs.mockRPC;
|
||||
|
||||
if (kwargs.services) {
|
||||
const serviceRegistry = kwargs.serviceRegistry = new LegacyRegistry();
|
||||
for (const sname in kwargs.services) {
|
||||
serviceRegistry.add(sname, kwargs.services[sname]);
|
||||
}
|
||||
delete kwargs.services;
|
||||
}
|
||||
|
||||
const legacyParams = kwargs;
|
||||
legacyParams.withLegacyMockServer = true;
|
||||
legacyParams.env = env;
|
||||
|
||||
widget = await createWebClient({ serverData, mockRPC, legacyParams });
|
||||
|
||||
legacyPatch(widget, {
|
||||
destroy() {
|
||||
destroyCallbacks.forEach(callback => callback({ widget }));
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<i class="o_mail_user_status o_user_online fa fa-circle" title="Online" role="img" aria-label="User is online"/>
|
||||
</t>
|
||||
<t t-if="status == 'away'">
|
||||
<i class="fa fa-circle o_mail_user_status o_user_idle" title="Idle" role="img" aria-label="User is idle"/>
|
||||
<i class="fa fa-circle o_mail_user_status o_user_idle text-warning" title="Idle" role="img" aria-label="User is idle"/>
|
||||
</t>
|
||||
<t t-if="status == 'offline'">
|
||||
<i class="o_mail_user_status fa fa-circle-o" title="Offline" role="img" aria-label="User is offline"/>
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<div class="o_thread_message_sidebar">
|
||||
<div class="o_avatar_stack">
|
||||
<img t-attf-src="/web/image#{activity.user_id[0] >= 0 ? ('/res.users/' + activity.user_id[0] + '/avatar_128') : ''}" class="o_thread_message_avatar rounded-circle mb8" t-att-title="activity.user_id[1]" t-att-alt="activity.user_id[1]"/>
|
||||
<i t-att-class="'o_avatar_icon fa ' + activity.icon + ' bg-' + (activity.state == 'planned'? 'success' : (activity.state == 'today'? 'warning' : 'danger')) + '-full'"
|
||||
<i t-att-class="'o_avatar_icon fa ' + activity.icon + ' bg-' + (activity.state == 'planned'? 'success' : (activity.state == 'today'? 'warning' : 'danger'))"
|
||||
t-att-title="activity.activity_type_id[1]"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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
|
||||
-->
|
||||
|
||||
@@ -3,7 +3,11 @@
|
||||
import ActivityView from '@mail/js/views/activity/activity_view';
|
||||
import testUtils from 'web.test_utils';
|
||||
|
||||
var createActionManager = testUtils.createActionManager;
|
||||
import { legacyExtraNextTick, patchWithCleanup } from "@web/../tests/helpers/utils";
|
||||
import { createWebClient, doAction } from "@web/../tests/webclient/helpers";
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
let serverData;
|
||||
|
||||
var createView = testUtils.createView;
|
||||
|
||||
@@ -108,6 +112,7 @@ QUnit.module('activity view', {
|
||||
],
|
||||
},
|
||||
};
|
||||
serverData = { models: this.data };
|
||||
}
|
||||
});
|
||||
|
||||
@@ -350,49 +355,62 @@ 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: [{
|
||||
serverData.actions = {
|
||||
1: {
|
||||
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>',
|
||||
name: "Task Action",
|
||||
res_model: "task",
|
||||
type: "ir.actions.act_window",
|
||||
views: [[false, "activity"]],
|
||||
},
|
||||
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);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
await actionManager.doAction(1);
|
||||
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"
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
patchWithCleanup(odoo.session_info.user_context, { lang: "zz_ZZ" });
|
||||
|
||||
const webClient = await createWebClient({ serverData, 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 +476,56 @@ 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: [{
|
||||
serverData.actions = {
|
||||
1: {
|
||||
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>`
|
||||
name: "Task Action",
|
||||
res_model: "task",
|
||||
type: "ir.actions.act_window",
|
||||
views: [[false, "activity"]],
|
||||
},
|
||||
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);
|
||||
};
|
||||
|
||||
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");
|
||||
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({ serverData, 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,65 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import testUtils from 'web.test_utils';
|
||||
import { manageMessages } from "@mail/js/tools/debug_manager";
|
||||
import { click, legacyExtraNextTick, patchWithCleanup } from "@web/../tests/helpers/utils";
|
||||
import { createWebClient, doAction, getActionManagerServerData } from "@web/../tests/webclient/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);
|
||||
patchWithCleanup(odoo, { debug: "1" });
|
||||
const serverData = getActionManagerServerData();
|
||||
|
||||
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();
|
||||
// Add fake "mail.message" model and arch
|
||||
serverData.models["mail.message"] = {
|
||||
fields: { name: { string: "Name", type: "char" } },
|
||||
records: [],
|
||||
};
|
||||
Object.assign(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({ serverData, 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"
|
||||
);
|
||||
});
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
('include', 'web._assets_helpers'),
|
||||
'web/static/lib/bootstrap/scss/_variables.scss',
|
||||
'mass_mailing/static/src/scss/mass_mailing.ui.scss',
|
||||
'web/static/src/scss/webclient.scss',
|
||||
'web/static/src/legacy/scss/webclient.scss',
|
||||
],
|
||||
'web.assets_common': [
|
||||
'mass_mailing/static/src/js/tours/**/*',
|
||||
|
||||
@@ -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){
|
||||
|
||||
@@ -172,7 +172,7 @@
|
||||
<label for="product_uom_id" string="" class="oe_inline"/>
|
||||
<field name="product_uom_category_id" invisible="1"/>
|
||||
<field name="product_uom_id" options="{'no_open': True, 'no_create': True}" force_save="1" groups="uom.group_uom" attrs="{'readonly': [('state', '!=', 'draft')]}"/>
|
||||
<span class='text-bf'>To Produce</span>
|
||||
<span class='font-weight-bold'>To Produce</span>
|
||||
<button type="object" name="action_product_forecast_report" icon="fa-area-chart" attrs="{'invisible': [('forecasted_issue', '=', True)]}"/>
|
||||
<button type="object" name="action_product_forecast_report" icon="fa-area-chart" attrs="{'invisible': [('forecasted_issue', '=', False)]}" class="text-danger"/>
|
||||
</div>
|
||||
|
||||
@@ -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", "company"],
|
||||
|
||||
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, company }) {
|
||||
if (odoo.session_info.iap_company_enrich) {
|
||||
const currentCompanyId = company.currentCompany.id;
|
||||
orm.silent.call("res.company", "iap_enrich_auto", [currentCompanyId], {});
|
||||
}
|
||||
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
registry
|
||||
.category("services")
|
||||
.add("partner_autocomplete.companyAutocomplete", companyAutocompleteService);
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
'point_of_sale/static/src/css/pos.css',
|
||||
'point_of_sale/static/src/css/keyboard.css',
|
||||
'point_of_sale/static/src/css/pos_receipts.css',
|
||||
'web/static/src/scss/fontawesome_overridden.scss',
|
||||
'web/static/src/legacy/scss/fontawesome_overridden.scss',
|
||||
'point_of_sale/static/lib/html2canvas.js',
|
||||
'point_of_sale/static/lib/backbone/backbone.js',
|
||||
'point_of_sale/static/lib/waitfont.js',
|
||||
@@ -164,6 +164,42 @@
|
||||
],
|
||||
'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/**/*',
|
||||
],
|
||||
'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/**/*',
|
||||
|
||||
@@ -5,7 +5,6 @@ odoo.define('point_of_sale.Chrome', function(require) {
|
||||
const { debounce } = owl.utils;
|
||||
const { loadCSS } = require('web.ajax');
|
||||
const { useListener } = require('web.custom_hooks');
|
||||
const { CrashManager } = require('web.CrashManager');
|
||||
const { BarcodeEvents } = require('barcodes.BarcodeEvents');
|
||||
const PosComponent = require('point_of_sale.PosComponent');
|
||||
const NumberBuffer = require('point_of_sale.NumberBuffer');
|
||||
@@ -13,6 +12,7 @@ odoo.define('point_of_sale.Chrome', function(require) {
|
||||
const Registries = require('point_of_sale.Registries');
|
||||
const IndependentToOrderScreen = require('point_of_sale.IndependentToOrderScreen');
|
||||
const contexts = require('point_of_sale.PosContext');
|
||||
const { registry } = require("@web/core/registry");
|
||||
|
||||
// This is kind of a trick.
|
||||
// We get a reference to the whole exports so that
|
||||
@@ -20,6 +20,15 @@ odoo.define('point_of_sale.Chrome', function(require) {
|
||||
// we instantiate the extended one.
|
||||
const models = require('point_of_sale.models');
|
||||
|
||||
let chromeInstance;
|
||||
function posErrorHandler() {
|
||||
if (chromeInstance) {
|
||||
return chromeInstance.handleError(...arguments);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
registry.category("error_handlers").add("posErrorHandler", posErrorHandler, { sequence: 0 });
|
||||
|
||||
/**
|
||||
* Chrome is the root component of the PoS App.
|
||||
*/
|
||||
@@ -186,6 +195,17 @@ odoo.define('point_of_sale.Chrome', function(require) {
|
||||
});
|
||||
}
|
||||
}
|
||||
handleError(env, error, originalError) {
|
||||
if (this.env.pos && originalError && originalError.message) {
|
||||
const { type, message, data } = originalError.message;
|
||||
this.showPopup('ErrorTracebackPopup', {
|
||||
title: type,
|
||||
body: message + '\n' + data.debug + '\n',
|
||||
});
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// EVENT HANDLERS //
|
||||
|
||||
@@ -393,27 +413,7 @@ odoo.define('point_of_sale.Chrome', function(require) {
|
||||
}
|
||||
|
||||
this._disableBackspaceBack();
|
||||
this._replaceCrashmanager();
|
||||
}
|
||||
// replaces the error handling of the existing crashmanager which
|
||||
// uses jquery dialog to display the error, to use the pos popup
|
||||
// instead
|
||||
_replaceCrashmanager() {
|
||||
var self = this;
|
||||
CrashManager.include({
|
||||
show_error: function (error) {
|
||||
if (self.env.pos) {
|
||||
// self == this component
|
||||
self.showPopup('ErrorTracebackPopup', {
|
||||
title: error.type,
|
||||
body: error.message + '\n' + error.data.debug + '\n',
|
||||
});
|
||||
} else {
|
||||
// this == CrashManager instance
|
||||
this._super(error);
|
||||
}
|
||||
},
|
||||
});
|
||||
chromeInstance = this;
|
||||
}
|
||||
// prevent backspace from performing a 'back' navigation
|
||||
_disableBackspaceBack() {
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
/** @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");
|
||||
|
||||
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="C" t-key="C[0]">
|
||||
<t t-component="C[1].Component" t-props="C[1].props"/>
|
||||
</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];
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
odoo.define('web.web_client', function (require) {
|
||||
// this module is required by the test
|
||||
const { bus } = require('web.core');
|
||||
const WebClient = require('web.AbstractWebClient');
|
||||
|
||||
// listen to unhandled rejected promises, and when the rejection is not due
|
||||
// to a crash, prevent the browser from displaying an 'unhandledrejection'
|
||||
// error in the console, which would make tests crash on each Promise.reject()
|
||||
// something similar is done by the CrashManagerService, but by default, it
|
||||
// isn't deployed in tests
|
||||
bus.on('crash_manager_unhandledrejection', this, function (ev) {
|
||||
if (!ev.reason || !(ev.reason instanceof Error)) {
|
||||
ev.stopPropagation();
|
||||
ev.stopImmediatePropagation();
|
||||
ev.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
owl.config.mode = "dev";
|
||||
|
||||
const webClient = new WebClient();
|
||||
return webClient;
|
||||
});
|
||||
@@ -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,11 @@
|
||||
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, doAction } = require('@web/../tests/webclient/helpers');
|
||||
|
||||
let serverData;
|
||||
|
||||
QUnit.module('Product Pricelist', {
|
||||
beforeEach: function () {
|
||||
@@ -32,6 +32,7 @@ QUnit.module('Product Pricelist', {
|
||||
}]
|
||||
}
|
||||
};
|
||||
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({ serverData, 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();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
.o_form_project_tasks.o_form_editable .oe_title .o_task_name {
|
||||
/*
|
||||
* should be (coming from addons/web/static/src/scss/form_view.scss):
|
||||
* should be (coming from addons/web/static/src/legacy/scss/form_view.scss):
|
||||
* max-width: map-get($container-max-widths, md) - (2 * $o-horizontal-padding);
|
||||
*/
|
||||
max-width: 688px;
|
||||
@@ -22,17 +22,17 @@
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.o_kanban_project_tasks .badge {
|
||||
.o_kanban_project_tasks .oe_kanban_align.badge {
|
||||
background: inherit;
|
||||
color: inherit;
|
||||
border: 1px solid var(--success);
|
||||
}
|
||||
|
||||
.o_kanban_project_tasks .badge-warning {
|
||||
.o_kanban_project_tasks .oe_kanban_align.badge-warning {
|
||||
border-color: var(--warning);
|
||||
}
|
||||
|
||||
.o_kanban_project_tasks .badge-danger {
|
||||
.o_kanban_project_tasks .oe_kanban_align.badge-danger {
|
||||
border-color: var(--danger);
|
||||
}
|
||||
|
||||
|
||||
@@ -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")',
|
||||
|
||||
@@ -90,8 +90,8 @@
|
||||
'post_init_hook': '_assign_default_mail_template_picking_id',
|
||||
'assets': {
|
||||
'web.report_assets_common': [
|
||||
'web/static/src/scss/views.scss',
|
||||
'web/static/src/scss/graph_view.scss',
|
||||
'web/static/src/legacy/scss/views.scss',
|
||||
'web/static/src/legacy/scss/graph_view.scss',
|
||||
'stock/static/src/scss/report_stock_forecasted.scss',
|
||||
'stock/static/src/scss/report_stock_rule.scss',
|
||||
],
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<t t-name="warehouseFilter">
|
||||
<div id="warehouse_filter" class="btn-group o_dropdown o_stock_report_warehouse_filter"
|
||||
t-if="displayWarehouseFilter">
|
||||
<button type="button" class="o_dropdown_toggler_btn btn btn-secondary dropdown-toggle"
|
||||
<button type="button" class="o_dropdown_toggler btn btn-secondary dropdown-toggle"
|
||||
data-toggle="dropdown">
|
||||
<span class="fa fa-home"/> Warehouse: <t t-esc="active_warehouse['name']"/>
|
||||
</button>
|
||||
|
||||
@@ -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, doAction } = require('@web/../tests/webclient/helpers');
|
||||
|
||||
/**
|
||||
* Helper function to instantiate a stock report action.
|
||||
@@ -91,23 +93,8 @@ odoo.define('stock.stock_traceability_report_backend_tests', function (require)
|
||||
this.__superMounted(...arguments);
|
||||
},
|
||||
});
|
||||
|
||||
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: {
|
||||
const serverData = {
|
||||
models: {
|
||||
partner: {
|
||||
fields: {
|
||||
display_name: { string: "Displayed name", type: "char" },
|
||||
@@ -117,23 +104,39 @@ odoo.define('stock.stock_traceability_report_backend_tests', function (require)
|
||||
],
|
||||
},
|
||||
},
|
||||
views: {
|
||||
'partner,false,form': '<form><field name="display_name"/></form>',
|
||||
'partner,false,search': '<search></search>',
|
||||
},
|
||||
actions: {
|
||||
42: {
|
||||
id: 42,
|
||||
name: "Stock report",
|
||||
tag: 'stock_report_generic',
|
||||
type: 'ir.actions.client',
|
||||
context: {},
|
||||
params: {},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const webClient = await createWebClient({
|
||||
serverData,
|
||||
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',
|
||||
|
||||
@@ -58,7 +58,7 @@ sent mails with personal token for the invitation of the survey.
|
||||
'assets': {
|
||||
'survey.survey_assets': [
|
||||
'web/static/lib/Chart/Chart.js',
|
||||
'web/static/src/js/fields/field_utils.js',
|
||||
'web/static/src/legacy/js/fields/field_utils.js',
|
||||
'survey/static/src/js/survey_quick_access.js',
|
||||
'survey/static/src/js/survey_timer.js',
|
||||
'survey/static/src/js/survey_breadcrumb.js',
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
module.exports = function(grunt) {
|
||||
|
||||
grunt.initConfig({
|
||||
jshint: {
|
||||
src: ['static/src/**/*.js', 'static/tests/**/*.js'],
|
||||
options: {
|
||||
sub: true, //[] instead of .
|
||||
evil: true, //eval
|
||||
laxbreak: true, //unsafe line breaks
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
grunt.loadNpmTasks('grunt-contrib-jshint');
|
||||
|
||||
grunt.registerTask('test', []);
|
||||
|
||||
grunt.registerTask('default', ['jshint']);
|
||||
|
||||
};
|
||||
+262
-296
@@ -38,31 +38,29 @@ This module provides the core of the Odoo Web Client.
|
||||
# > web_editor.assets_wysiwyg = assets needed by components defined in the "web_editor" module.
|
||||
|
||||
'web.assets_qweb': [
|
||||
'web/static/src/xml/base.xml',
|
||||
'web/static/src/xml/chart.xml',
|
||||
'web/static/src/xml/fields.xml',
|
||||
'web/static/src/xml/file_upload_progress_bar.xml',
|
||||
'web/static/src/xml/file_upload_progress_card.xml',
|
||||
'web/static/src/xml/kanban.xml',
|
||||
'web/static/src/xml/menu.xml',
|
||||
'web/static/src/xml/notification.xml',
|
||||
'web/static/src/xml/pivot.xml',
|
||||
'web/static/src/xml/rainbow_man.xml',
|
||||
'web/static/src/xml/report.xml',
|
||||
'web/static/src/xml/search_panel.xml',
|
||||
'web/static/src/xml/web_calendar.xml',
|
||||
'web/static/src/xml/graph.xml',
|
||||
'web/static/src/xml/week_days.xml',
|
||||
'web/static/src/**/*.xml',
|
||||
('remove', 'web/static/src/legacy/**/*.xml'),
|
||||
'web/static/src/legacy/xml/base.xml',
|
||||
'web/static/src/legacy/xml/chart.xml',
|
||||
'web/static/src/legacy/xml/fields.xml',
|
||||
'web/static/src/legacy/xml/file_upload_progress_bar.xml',
|
||||
'web/static/src/legacy/xml/file_upload_progress_card.xml',
|
||||
'web/static/src/legacy/xml/kanban.xml',
|
||||
'web/static/src/legacy/xml/pivot.xml',
|
||||
'web/static/src/legacy/xml/report.xml',
|
||||
'web/static/src/legacy/xml/search_panel.xml',
|
||||
'web/static/src/legacy/xml/web_calendar.xml',
|
||||
'web/static/src/legacy/xml/graph.xml',
|
||||
'web/static/src/legacy/xml/week_days.xml',
|
||||
],
|
||||
'web.assets_common_minimal': [
|
||||
'web/static/lib/es6-promise/es6-promise-polyfill.js',
|
||||
'web/static/src/js/promise_extension.js',
|
||||
'web/static/src/js/boot.js',
|
||||
'web/static/src/legacy/js/promise_extension.js',
|
||||
'web/static/src/boot.js',
|
||||
],
|
||||
'web.assets_common': [
|
||||
('include', 'web._assets_helpers'),
|
||||
|
||||
'web/static/src/scss/debug_profiling.scss',
|
||||
'web/static/lib/bootstrap/scss/_variables.scss',
|
||||
|
||||
('include', 'web._assets_common_styles'),
|
||||
@@ -73,8 +71,8 @@ This module provides the core of the Odoo Web Client.
|
||||
('include', 'web.assets_common'),
|
||||
# Remove assets_common_minimal
|
||||
('remove', 'web/static/lib/es6-promise/es6-promise-polyfill.js'),
|
||||
('remove', 'web/static/src/js/promise_extension.js'),
|
||||
('remove', 'web/static/src/js/boot.js'),
|
||||
('remove', 'web/static/src/legacy/js/promise_extension.js'),
|
||||
('remove', 'web/static/src/boot.js'),
|
||||
],
|
||||
'web.assets_backend': [
|
||||
('include', 'web._assets_helpers'),
|
||||
@@ -85,138 +83,110 @@ This module provides the core of the Odoo Web Client.
|
||||
('include', 'web._assets_bootstrap'),
|
||||
|
||||
'base/static/src/css/modules.css',
|
||||
|
||||
'web/static/src/scss/webclient_extra.scss',
|
||||
'web/static/src/scss/webclient_layout.scss',
|
||||
|
||||
'web/static/src/scss/webclient.scss',
|
||||
'web/static/src/scss/domain_selector.scss',
|
||||
'web/static/src/scss/model_field_selector.scss',
|
||||
'web/static/src/scss/progress_bar.scss',
|
||||
'web/static/src/scss/dropdown.scss',
|
||||
'web/static/src/scss/tooltip.scss',
|
||||
'web/static/src/scss/switch_company_menu.scss',
|
||||
'web/static/src/scss/debug_manager.scss',
|
||||
'web/static/src/scss/ace.scss',
|
||||
'web/static/src/scss/control_panel.scss',
|
||||
'web/static/src/scss/fields.scss',
|
||||
'web/static/src/scss/file_upload.scss',
|
||||
'web/static/src/scss/views.scss',
|
||||
'web/static/src/scss/pivot_view.scss',
|
||||
'web/static/src/scss/graph_view.scss',
|
||||
'web/static/src/scss/form_view.scss',
|
||||
'web/static/src/scss/list_view.scss',
|
||||
'web/static/src/scss/kanban_dashboard.scss',
|
||||
'web/static/src/scss/kanban_examples_dialog.scss',
|
||||
'web/static/src/scss/kanban_column_progressbar.scss',
|
||||
'web/static/src/scss/kanban_view.scss',
|
||||
'web/static/src/scss/web_calendar.scss',
|
||||
'web/static/src/scss/search_view.scss',
|
||||
'web/static/src/scss/search_panel.scss',
|
||||
'web/static/src/scss/dropdown_menu.scss',
|
||||
'web/static/src/scss/data_export.scss',
|
||||
'base/static/src/scss/onboarding.scss',
|
||||
'web/static/src/scss/attachment_preview.scss',
|
||||
'web/static/src/scss/notification.scss',
|
||||
'web/static/src/scss/base_document_layout.scss',
|
||||
'web/static/src/scss/special_fields.scss',
|
||||
'web/static/src/scss/ribbon.scss',
|
||||
'web/static/src/scss/base_settings.scss',
|
||||
'web/static/src/scss/report_backend.scss',
|
||||
'web/static/src/scss/dropdown_extra.scss',
|
||||
'web/static/src/scss/fields_extra.scss',
|
||||
'web/static/src/scss/form_view_extra.scss',
|
||||
'web/static/src/scss/list_view_extra.scss',
|
||||
'web/static/src/scss/search_view_extra.scss',
|
||||
|
||||
'base/static/src/js/res_config_settings.js',
|
||||
|
||||
'web/static/src/core/utils/transitions.scss',
|
||||
'web/static/src/core/**/*',
|
||||
'web/static/src/fields/**/*',
|
||||
'web/static/src/views/**/*',
|
||||
'web/static/src/webclient/**/*',
|
||||
('remove', 'web/static/src/webclient/clickbot/clickbot.js'), # lazy loaded
|
||||
'web/static/src/env.js',
|
||||
|
||||
'web/static/lib/jquery.scrollTo/jquery.scrollTo.js',
|
||||
'web/static/lib/fuzzy-master/fuzzy.js',
|
||||
'web/static/lib/luxon/luxon.js',
|
||||
'web/static/lib/py.js/lib/py.js',
|
||||
'web/static/lib/py.js/lib/py_extras.js',
|
||||
'web/static/lib/jquery.ba-bbq/jquery.ba-bbq.js',
|
||||
|
||||
'web/static/src/js/_deprecated/*',
|
||||
'web/static/src/js/chrome/abstract_action.js',
|
||||
'web/static/src/js/chrome/abstract_web_client.js',
|
||||
'web/static/src/js/chrome/action_manager_act_window.js',
|
||||
'web/static/src/js/chrome/action_manager_report.js',
|
||||
'web/static/src/js/chrome/action_manager.js',
|
||||
'web/static/src/js/chrome/action_mixin.js',
|
||||
'web/static/src/js/chrome/apps_menu.js',
|
||||
'web/static/src/js/chrome/loading.js',
|
||||
'web/static/src/js/chrome/menu.js',
|
||||
'web/static/src/js/chrome/root_widget.js',
|
||||
'web/static/src/js/chrome/systray_menu.js',
|
||||
'web/static/src/js/chrome/user_menu.js',
|
||||
'web/static/src/js/chrome/web_client.js',
|
||||
'web/static/src/js/components/*',
|
||||
'web/static/src/js/control_panel/*',
|
||||
'web/static/src/js/core/domain.js',
|
||||
'web/static/src/js/core/mvc.js',
|
||||
'web/static/src/js/core/py_utils.js',
|
||||
'web/static/src/js/core/context.js',
|
||||
'web/static/src/js/core/data_comparison_utils.js',
|
||||
'web/static/src/js/core/math_utils.js',
|
||||
'web/static/src/js/core/misc.js',
|
||||
'web/static/src/js/fields/*',
|
||||
'web/static/src/js/report/utils.js',
|
||||
'web/static/src/js/report/client_action.js',
|
||||
'web/static/src/js/services/crash_manager_service.js',
|
||||
'web/static/src/js/services/data_manager.js',
|
||||
'web/static/src/js/services/report_service.js',
|
||||
'web/static/src/js/services/session.js',
|
||||
'web/static/src/js/tools/test_menus_loader.js',
|
||||
'web/static/src/js/tools/debug_manager_backend.js',
|
||||
'web/static/src/js/tools/tools.js',
|
||||
'web/static/src/js/views/abstract_view.js',
|
||||
'web/static/src/js/views/abstract_renderer.js',
|
||||
'web/static/src/js/views/abstract_renderer_owl.js',
|
||||
'web/static/src/js/views/abstract_model.js',
|
||||
'web/static/src/js/views/abstract_controller.js',
|
||||
'web/static/src/js/views/renderer_wrapper.js',
|
||||
'web/static/src/js/views/search_panel_model_extension.js',
|
||||
'web/static/src/js/views/search_panel.js',
|
||||
'web/static/src/js/views/action_model.js',
|
||||
'web/static/src/js/views/field_manager_mixin.js',
|
||||
'web/static/src/js/views/file_upload_mixin.js',
|
||||
'web/static/src/js/views/file_upload_progress_bar.js',
|
||||
'web/static/src/js/views/file_upload_progress_card.js',
|
||||
'web/static/src/js/views/sample_server.js',
|
||||
'web/static/src/js/views/select_create_controllers_registry.js',
|
||||
'web/static/src/js/views/signature_dialog.js',
|
||||
'web/static/src/js/views/standalone_field_manager_mixin.js',
|
||||
'web/static/src/js/views/view_registry.js',
|
||||
'web/static/src/js/views/view_dialogs.js',
|
||||
'web/static/src/js/views/view_utils.js',
|
||||
'web/static/src/js/views/basic/*',
|
||||
'web/static/src/js/views/calendar/*',
|
||||
'web/static/src/js/views/form/*',
|
||||
'web/static/src/js/views/graph/*',
|
||||
'web/static/src/js/views/kanban/*',
|
||||
'web/static/src/js/views/list/*',
|
||||
'web/static/src/js/views/pivot/*',
|
||||
'web/static/src/js/views/qweb/*',
|
||||
'web/static/src/js/widgets/change_password.js',
|
||||
'web/static/src/js/widgets/data_export.js',
|
||||
'web/static/src/js/widgets/date_picker.js',
|
||||
'web/static/src/js/widgets/domain_selector_dialog.js',
|
||||
'web/static/src/js/widgets/domain_selector.js',
|
||||
'web/static/src/js/widgets/iframe_widget.js',
|
||||
'web/static/src/js/widgets/model_field_selector.js',
|
||||
'web/static/src/js/widgets/switch_company_menu.js',
|
||||
'web/static/src/js/widgets/pie_chart.js',
|
||||
'web/static/src/js/widgets/ribbon.js',
|
||||
'web/static/src/js/widgets/week_days.js',
|
||||
'web/static/src/js/widgets/signature.js',
|
||||
'web/static/src/js/widgets/attach_document.js',
|
||||
'web/static/src/js/apps.js',
|
||||
'web/static/src/js/env.js',
|
||||
'web/static/src/js/model.js',
|
||||
'web/static/src/js/owl_compatibility.js',
|
||||
'web/static/src/legacy/scss/domain_selector.scss',
|
||||
'web/static/src/legacy/scss/model_field_selector.scss',
|
||||
'web/static/src/legacy/scss/progress_bar.scss',
|
||||
'web/static/src/legacy/scss/dropdown.scss',
|
||||
'web/static/src/legacy/scss/tooltip.scss',
|
||||
'web/static/src/legacy/scss/switch_company_menu.scss',
|
||||
'web/static/src/legacy/scss/ace.scss',
|
||||
'web/static/src/legacy/scss/control_panel.scss',
|
||||
'web/static/src/legacy/scss/fields.scss',
|
||||
'web/static/src/legacy/scss/file_upload.scss',
|
||||
'web/static/src/legacy/scss/views.scss',
|
||||
'web/static/src/legacy/scss/pivot_view.scss',
|
||||
'web/static/src/legacy/scss/graph_view.scss',
|
||||
'web/static/src/legacy/scss/form_view.scss',
|
||||
'web/static/src/legacy/scss/list_view.scss',
|
||||
'web/static/src/legacy/scss/kanban_dashboard.scss',
|
||||
'web/static/src/legacy/scss/kanban_examples_dialog.scss',
|
||||
'web/static/src/legacy/scss/kanban_column_progressbar.scss',
|
||||
'web/static/src/legacy/scss/kanban_view.scss',
|
||||
'web/static/src/legacy/scss/web_calendar.scss',
|
||||
'web/static/src/legacy/scss/search_view.scss',
|
||||
'web/static/src/legacy/scss/search_panel.scss',
|
||||
'web/static/src/legacy/scss/data_export.scss',
|
||||
'base/static/src/scss/onboarding.scss',
|
||||
'web/static/src/legacy/scss/attachment_preview.scss',
|
||||
'web/static/src/legacy/scss/base_document_layout.scss',
|
||||
'web/static/src/legacy/scss/special_fields.scss',
|
||||
'web/static/src/legacy/scss/ribbon.scss',
|
||||
'web/static/src/legacy/scss/base_settings.scss',
|
||||
'web/static/src/legacy/scss/report_backend.scss',
|
||||
'web/static/src/legacy/scss/dropdown_extra.scss',
|
||||
'web/static/src/legacy/scss/fields_extra.scss',
|
||||
'web/static/src/legacy/scss/form_view_extra.scss',
|
||||
'web/static/src/legacy/scss/list_view_extra.scss',
|
||||
'web/static/src/legacy/scss/search_view_extra.scss',
|
||||
|
||||
'web/static/src/legacy/action_adapters.js',
|
||||
'web/static/src/legacy/debug_manager.js',
|
||||
'web/static/src/legacy/legacy_service_provider.js',
|
||||
'web/static/src/legacy/legacy_client_actions.js',
|
||||
'web/static/src/legacy/legacy_dialog.js',
|
||||
'web/static/src/legacy/legacy_views.js',
|
||||
'web/static/src/legacy/legacy_promise_error_handler.js',
|
||||
'web/static/src/legacy/legacy_rpc_error_handler.js',
|
||||
'web/static/src/legacy/root_widget.js',
|
||||
'web/static/src/legacy/systray_menu.js',
|
||||
'web/static/src/legacy/systray_menu_item.js',
|
||||
'web/static/src/legacy/utils.js',
|
||||
'web/static/src/legacy/web_client.js',
|
||||
'web/static/src/legacy/js/_deprecated/*',
|
||||
'web/static/src/legacy/js/chrome/*',
|
||||
'web/static/src/legacy/js/components/*',
|
||||
'web/static/src/legacy/js/control_panel/*',
|
||||
'web/static/src/legacy/js/core/domain.js',
|
||||
'web/static/src/legacy/js/core/mvc.js',
|
||||
'web/static/src/legacy/js/core/py_utils.js',
|
||||
'web/static/src/legacy/js/core/context.js',
|
||||
'web/static/src/legacy/js/core/data_comparison_utils.js',
|
||||
'web/static/src/legacy/js/core/math_utils.js',
|
||||
'web/static/src/legacy/js/core/misc.js',
|
||||
'web/static/src/legacy/js/fields/*',
|
||||
'web/static/src/legacy/js/report/utils.js',
|
||||
'web/static/src/legacy/js/report/client_action.js',
|
||||
'web/static/src/legacy/js/services/data_manager.js',
|
||||
'web/static/src/legacy/js/services/report_service.js',
|
||||
'web/static/src/legacy/js/services/session.js',
|
||||
'web/static/src/legacy/js/tools/tools.js',
|
||||
'web/static/src/legacy/js/views/**/*',
|
||||
'web/static/src/legacy/js/widgets/change_password.js',
|
||||
'web/static/src/legacy/js/widgets/data_export.js',
|
||||
'web/static/src/legacy/js/widgets/date_picker.js',
|
||||
'web/static/src/legacy/js/widgets/domain_selector_dialog.js',
|
||||
'web/static/src/legacy/js/widgets/domain_selector.js',
|
||||
'web/static/src/legacy/js/widgets/iframe_widget.js',
|
||||
'web/static/src/legacy/js/widgets/model_field_selector.js',
|
||||
'web/static/src/legacy/js/widgets/pie_chart.js',
|
||||
'web/static/src/legacy/js/widgets/ribbon.js',
|
||||
'web/static/src/legacy/js/widgets/week_days.js',
|
||||
'web/static/src/legacy/js/widgets/signature.js',
|
||||
'web/static/src/legacy/js/widgets/attach_document.js',
|
||||
'web/static/src/legacy/js/apps.js',
|
||||
'web/static/src/legacy/js/env.js',
|
||||
'web/static/src/legacy/js/model.js',
|
||||
'web/static/src/legacy/js/owl_compatibility.js',
|
||||
],
|
||||
'web.assets_frontend_minimal': [
|
||||
'web/static/src/js/public/lazyloader.js',
|
||||
'web/static/src/legacy/js/public/lazyloader.js',
|
||||
],
|
||||
'web.assets_frontend': [
|
||||
('include', 'web._assets_helpers'),
|
||||
@@ -226,28 +196,29 @@ This module provides the core of the Odoo Web Client.
|
||||
|
||||
('include', 'web._assets_bootstrap'),
|
||||
|
||||
'web/static/src/scss/base_frontend.scss',
|
||||
'web/static/src/scss/lazyloader.scss',
|
||||
'web/static/src/scss/navbar_mobile.scss',
|
||||
'web/static/src/scss/notification.scss',
|
||||
'web/static/src/legacy/scss/base_frontend.scss',
|
||||
'web/static/src/legacy/scss/lazyloader.scss',
|
||||
'web/static/src/legacy/scss/navbar_mobile.scss',
|
||||
|
||||
('include', 'web.assets_frontend_minimal'),
|
||||
|
||||
'web/static/src/js/services/session.js',
|
||||
'web/static/src/js/public/public_env.js',
|
||||
'web/static/src/js/public/public_crash_manager.js',
|
||||
'web/static/src/js/public/public_notification.js',
|
||||
'web/static/src/js/public/public_root.js',
|
||||
'web/static/src/js/public/public_root_instance.js',
|
||||
'web/static/src/js/public/public_widget.js',
|
||||
'web/static/src/legacy/js/services/session.js',
|
||||
'web/static/src/legacy/js/public/public_env.js',
|
||||
'web/static/src/legacy/js/public/public_root.js',
|
||||
'web/static/src/legacy/js/public/public_root_instance.js',
|
||||
'web/static/src/legacy/js/public/public_widget.js',
|
||||
|
||||
('include', 'web.frontend_legacy'),
|
||||
],
|
||||
'web.assets_frontend_lazy': [
|
||||
('include', 'web.assets_frontend'),
|
||||
# Remove assets_frontend_minimal
|
||||
('remove', 'web/static/src/js/public/lazyloader.js')
|
||||
('remove', 'web/static/src/legacy/js/public/lazyloader.js')
|
||||
],
|
||||
'web.assets_backend_prod_only': [
|
||||
'web/static/src/js/main.js',
|
||||
'web/static/src/main.js',
|
||||
'web/static/src/start.js',
|
||||
'web/static/src/legacy/legacy_setup.js',
|
||||
],
|
||||
# Optional Bundle for PDFJS lib
|
||||
# Since PDFJS is quite huge (80000≈ lines), please only load it when it is necessary.
|
||||
@@ -260,7 +231,7 @@ This module provides the core of the Odoo Web Client.
|
||||
'web.report_assets_common': [
|
||||
('include', 'web._assets_helpers'),
|
||||
|
||||
'web/static/src/scss/bootstrap_overridden_report.scss',
|
||||
'web/static/src/legacy/scss/bootstrap_overridden_report.scss',
|
||||
'web/static/lib/bootstrap/scss/_variables.scss',
|
||||
|
||||
('include', 'web._assets_bootstrap'),
|
||||
@@ -268,18 +239,18 @@ This module provides the core of the Odoo Web Client.
|
||||
'base/static/src/css/description.css',
|
||||
'web/static/lib/fontawesome/css/font-awesome.css',
|
||||
'web/static/fonts/fonts.scss',
|
||||
'web/static/src/scss/report.scss',
|
||||
'web/static/src/scss/layout_standard.scss',
|
||||
'web/static/src/scss/layout_background.scss',
|
||||
'web/static/src/scss/layout_boxed.scss',
|
||||
'web/static/src/scss/layout_clean.scss',
|
||||
'/web/static/src/scss/asset_styles_company_report.scss',
|
||||
'web/static/src/js/services/session.js',
|
||||
'web/static/src/js/public/public_root.js',
|
||||
'web/static/src/js/public/public_root_instance.js',
|
||||
'web/static/src/js/public/public_widget.js',
|
||||
'web/static/src/js/report/utils.js',
|
||||
'web/static/src/js/report/report.js',
|
||||
'web/static/src/legacy/scss/report.scss',
|
||||
'web/static/src/legacy/scss/layout_standard.scss',
|
||||
'web/static/src/legacy/scss/layout_background.scss',
|
||||
'web/static/src/legacy/scss/layout_boxed.scss',
|
||||
'web/static/src/legacy/scss/layout_clean.scss',
|
||||
'/web/static/src/legacy/scss/asset_styles_company_report.scss',
|
||||
'web/static/src/legacy/js/services/session.js',
|
||||
'web/static/src/legacy/js/public/public_root.js',
|
||||
'web/static/src/legacy/js/public/public_root_instance.js',
|
||||
'web/static/src/legacy/js/public/public_widget.js',
|
||||
'web/static/src/legacy/js/report/utils.js',
|
||||
'web/static/src/legacy/js/report/report.js',
|
||||
],
|
||||
'web.report_assets_pdf': [
|
||||
'web/static/src/css/reset.min.css',
|
||||
@@ -299,62 +270,62 @@ This module provides the core of the Odoo Web Client.
|
||||
# > web._assets_helpers = define assets needed in most main bundles
|
||||
|
||||
'web._assets_primary_variables': [
|
||||
'web/static/src/scss/primary_variables.scss',
|
||||
'web/static/src/legacy/scss/primary_variables.scss',
|
||||
],
|
||||
'web._assets_secondary_variables': [
|
||||
'web/static/src/scss/secondary_variables.scss',
|
||||
'web/static/src/legacy/scss/secondary_variables.scss',
|
||||
],
|
||||
'web._assets_helpers': [
|
||||
'web/static/lib/bootstrap/scss/_functions.scss',
|
||||
'web/static/lib/bootstrap/scss/_mixins.scss',
|
||||
'web/static/src/scss/bs_mixins_overrides.scss',
|
||||
'web/static/src/scss/utils.scss',
|
||||
'web/static/src/legacy/scss/bs_mixins_overrides.scss',
|
||||
'web/static/src/legacy/scss/utils.scss',
|
||||
|
||||
('include', 'web._assets_primary_variables'),
|
||||
('include', 'web._assets_secondary_variables'),
|
||||
],
|
||||
'web._assets_bootstrap': [
|
||||
'web/static/src/scss/import_bootstrap.scss',
|
||||
'web/static/src/scss/bootstrap_review.scss',
|
||||
'web/static/src/legacy/scss/import_bootstrap.scss',
|
||||
'web/static/src/legacy/scss/bootstrap_review.scss',
|
||||
],
|
||||
'web._assets_backend_helpers': [
|
||||
'web/static/src/scss/bootstrap_overridden.scss',
|
||||
'web/static/src/legacy/scss/bootstrap_overridden.scss',
|
||||
'web/static/src/legacy/scss/bs_mixins_overrides_backend.scss',
|
||||
],
|
||||
'web._assets_frontend_helpers': [
|
||||
'web/static/src/scss/bootstrap_overridden_frontend.scss',
|
||||
'web/static/src/legacy/scss/bootstrap_overridden_frontend.scss',
|
||||
],
|
||||
'web._assets_common_styles': [
|
||||
'web/static/lib/jquery.ui/jquery-ui.css',
|
||||
'web/static/lib/fontawesome/css/font-awesome.css',
|
||||
'web/static/lib/select2/select2.css',
|
||||
'web/static/lib/select2-bootstrap-css/select2-bootstrap.css',
|
||||
'web/static/lib/tempusdominus/tempusdominus.scss',
|
||||
'web/static/fonts/fonts.scss',
|
||||
'web/static/src/scss/ui.scss',
|
||||
'web/static/src/scss/ui_extra.scss',
|
||||
'web/static/src/scss/navbar.scss',
|
||||
'web/static/src/scss/mimetypes.scss',
|
||||
'web/static/src/scss/modal.scss',
|
||||
'web/static/src/scss/animation.scss',
|
||||
'web/static/src/scss/rainbow.scss',
|
||||
'web/static/src/scss/datepicker.scss',
|
||||
'web/static/src/scss/daterangepicker.scss',
|
||||
'web/static/src/scss/banner.scss',
|
||||
'web/static/src/scss/colorpicker.scss',
|
||||
'web/static/src/scss/popover.scss',
|
||||
'web/static/src/scss/translation_dialog.scss',
|
||||
'web/static/src/scss/keyboard.scss',
|
||||
'web/static/src/scss/name_and_signature.scss',
|
||||
'web/static/src/scss/web.zoomodoo.scss',
|
||||
'web/static/src/scss/color_picker.scss',
|
||||
'web/static/src/scss/fontawesome_overridden.scss',
|
||||
'web/static/src/legacy/scss/ui.scss',
|
||||
'web/static/src/legacy/scss/ui_extra.scss',
|
||||
'web/static/src/legacy/scss/navbar.scss',
|
||||
'web/static/src/legacy/scss/mimetypes.scss',
|
||||
'web/static/src/legacy/scss/modal.scss',
|
||||
'web/static/src/legacy/scss/animation.scss',
|
||||
'web/static/src/legacy/scss/datepicker.scss',
|
||||
'web/static/src/legacy/scss/daterangepicker.scss',
|
||||
'web/static/src/legacy/scss/banner.scss',
|
||||
'web/static/src/legacy/scss/colorpicker.scss',
|
||||
'web/static/src/legacy/scss/popover.scss',
|
||||
'web/static/src/legacy/scss/translation_dialog.scss',
|
||||
'web/static/src/legacy/scss/keyboard.scss',
|
||||
'web/static/src/legacy/scss/name_and_signature.scss',
|
||||
'web/static/src/legacy/scss/web.zoomodoo.scss',
|
||||
'web/static/src/legacy/scss/color_picker.scss',
|
||||
'web/static/src/legacy/scss/fontawesome_overridden.scss',
|
||||
'web/static/lib/tempusdominus/tempusdominus.scss',
|
||||
],
|
||||
'web._assets_common_scripts': [
|
||||
'web/static/lib/underscore/underscore.js',
|
||||
'web/static/lib/underscore.string/lib/underscore.string.js',
|
||||
'web/static/lib/moment/moment.js',
|
||||
'web/static/lib/owl/owl.js',
|
||||
'web/static/src/js/component_extension.js',
|
||||
'web/static/src/legacy/js/component_extension.js',
|
||||
'web/static/lib/jquery/jquery.js',
|
||||
'web/static/lib/jquery.ui/jquery-ui.js',
|
||||
'web/static/lib/jquery/jquery.browser.js',
|
||||
@@ -383,60 +354,59 @@ This module provides the core of the Odoo Web Client.
|
||||
'web/static/lib/clipboard/clipboard.js',
|
||||
'web/static/lib/jSignature/jSignatureCustom.js',
|
||||
'web/static/lib/qweb/qweb2.js',
|
||||
'web/static/src/js/libs/autocomplete.js',
|
||||
'web/static/src/js/libs/bootstrap.js',
|
||||
'web/static/src/js/libs/content-disposition.js',
|
||||
'web/static/src/js/libs/download.js',
|
||||
'web/static/src/js/libs/fullcalendar.js',
|
||||
'web/static/src/js/libs/jquery.js',
|
||||
'web/static/src/js/libs/underscore.js',
|
||||
'web/static/src/js/libs/pdfjs.js',
|
||||
'web/static/src/js/libs/popper.js',
|
||||
'web/static/src/js/libs/zoomodoo.js',
|
||||
'web/static/src/js/libs/jSignatureCustom.js',
|
||||
'web/static/src/js/chrome/keyboard_navigation_mixin.js',
|
||||
'web/static/src/js/core/abstract_service.js',
|
||||
'web/static/src/js/core/abstract_storage_service.js',
|
||||
'web/static/src/js/core/ajax.js',
|
||||
'web/static/src/js/core/browser_detection.js',
|
||||
'web/static/src/js/core/bus.js',
|
||||
'web/static/src/js/core/custom_hooks.js',
|
||||
'web/static/src/js/core/class.js',
|
||||
'web/static/src/js/core/collections.js',
|
||||
'web/static/src/js/core/concurrency.js',
|
||||
'web/static/src/js/core/dialog.js',
|
||||
'web/static/src/js/core/owl_dialog.js',
|
||||
'web/static/src/js/core/popover.js',
|
||||
'web/static/src/js/core/dom.js',
|
||||
'web/static/src/js/core/local_storage.js',
|
||||
'web/static/src/js/core/mixins.js',
|
||||
'web/static/src/js/core/qweb.js',
|
||||
'web/static/src/js/core/ram_storage.js',
|
||||
'web/static/src/js/core/registry.js',
|
||||
'web/static/src/js/core/rpc.js',
|
||||
'web/static/src/js/core/service_mixins.js',
|
||||
'web/static/src/js/core/session.js',
|
||||
'web/static/src/js/core/session_storage.js',
|
||||
'web/static/src/js/core/time.js',
|
||||
'web/static/src/js/core/translation.js',
|
||||
'web/static/src/js/core/utils.js',
|
||||
'web/static/src/js/core/widget.js',
|
||||
'web/static/src/js/services/ajax_service.js',
|
||||
'web/static/src/js/services/config.js',
|
||||
'web/static/src/js/services/core.js',
|
||||
'web/static/src/js/services/local_storage_service.js',
|
||||
'web/static/src/js/services/notification_service.js',
|
||||
'web/static/src/js/services/crash_manager.js',
|
||||
'web/static/src/js/core/error_utils.js',
|
||||
'web/static/src/js/services/session_storage_service.js',
|
||||
'web/static/src/js/tools/debug_manager.js',
|
||||
'web/static/src/js/common_env.js',
|
||||
'web/static/src/js/widgets/name_and_signature.js',
|
||||
'web/static/src/js/widgets/notification.js',
|
||||
'web/static/src/js/widgets/rainbow_man.js',
|
||||
'web/static/src/js/core/smooth_scroll_on_drag.js',
|
||||
'web/static/src/js/widgets/colorpicker.js',
|
||||
'web/static/src/js/widgets/translation_dialog.js',
|
||||
'web/static/src/legacy/js/libs/autocomplete.js',
|
||||
'web/static/src/legacy/js/libs/bootstrap.js',
|
||||
'web/static/src/legacy/js/libs/content-disposition.js',
|
||||
'web/static/src/legacy/js/libs/download.js',
|
||||
'web/static/src/legacy/js/libs/fullcalendar.js',
|
||||
'web/static/src/legacy/js/libs/jquery.js',
|
||||
'web/static/src/legacy/js/libs/underscore.js',
|
||||
'web/static/src/legacy/js/libs/pdfjs.js',
|
||||
'web/static/src/legacy/js/libs/popper.js',
|
||||
'web/static/src/legacy/js/libs/zoomodoo.js',
|
||||
'web/static/src/legacy/js/libs/jSignatureCustom.js',
|
||||
'web/static/src/legacy/js/core/abstract_service.js',
|
||||
'web/static/src/legacy/js/core/abstract_storage_service.js',
|
||||
'web/static/src/legacy/js/core/ajax.js',
|
||||
'web/static/src/legacy/js/core/browser_detection.js',
|
||||
'web/static/src/legacy/js/core/bus.js',
|
||||
'web/static/src/legacy/js/core/custom_hooks.js',
|
||||
'web/static/src/legacy/js/core/class.js',
|
||||
'web/static/src/legacy/js/core/collections.js',
|
||||
'web/static/src/legacy/js/core/concurrency.js',
|
||||
'web/static/src/legacy/js/core/dialog.js',
|
||||
'web/static/src/legacy/js/core/owl_dialog.js',
|
||||
'web/static/src/legacy/js/core/popover.js',
|
||||
'web/static/src/legacy/js/core/dom.js',
|
||||
'web/static/src/legacy/js/core/local_storage.js',
|
||||
'web/static/src/legacy/js/core/mixins.js',
|
||||
'web/static/src/legacy/js/core/qweb.js',
|
||||
'web/static/src/legacy/js/core/ram_storage.js',
|
||||
'web/static/src/legacy/js/core/registry.js',
|
||||
'web/static/src/legacy/js/core/rpc.js',
|
||||
'web/static/src/legacy/js/core/service_mixins.js',
|
||||
'web/static/src/legacy/js/core/session.js',
|
||||
'web/static/src/legacy/js/core/session_storage.js',
|
||||
'web/static/src/legacy/js/core/time.js',
|
||||
'web/static/src/legacy/js/core/translation.js',
|
||||
'web/static/src/legacy/js/core/utils.js',
|
||||
'web/static/src/legacy/js/core/widget.js',
|
||||
'web/static/src/legacy/js/services/ajax_service.js',
|
||||
'web/static/src/legacy/js/services/config.js',
|
||||
'web/static/src/legacy/js/services/core.js',
|
||||
'web/static/src/legacy/js/services/local_storage_service.js',
|
||||
'web/static/src/legacy/js/core/error_utils.js',
|
||||
'web/static/src/legacy/js/services/session_storage_service.js',
|
||||
'web/static/src/legacy/js/common_env.js',
|
||||
'web/static/src/legacy/js/widgets/name_and_signature.js',
|
||||
'web/static/src/legacy/js/core/smooth_scroll_on_drag.js',
|
||||
'web/static/src/legacy/js/widgets/colorpicker.js',
|
||||
'web/static/src/legacy/js/widgets/translation_dialog.js',
|
||||
],
|
||||
|
||||
# Used during the transition of the web architecture
|
||||
'web.frontend_legacy': [
|
||||
'web/static/src/legacy/frontend/**/*',
|
||||
],
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
@@ -446,13 +416,15 @@ This module provides the core of the Odoo Web Client.
|
||||
'web.assets_tests': [
|
||||
# No tours are defined in web, but the bundle "assets_tests" is
|
||||
# first called in web.
|
||||
'web/static/tests/helpers/test_utils_file.js'
|
||||
'web/static/tests/legacy/helpers/test_utils_file.js'
|
||||
],
|
||||
'web.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/helpers/**/*',
|
||||
'web/static/tests/legacy/helpers/**/*',
|
||||
('remove', 'web/static/tests/legacy/helpers/test_utils_tests.js'),
|
||||
'web/static/tests/legacy/legacy_setup.js',
|
||||
|
||||
'web/static/lib/fullcalendar/core/main.css',
|
||||
'web/static/lib/fullcalendar/daygrid/main.css',
|
||||
@@ -473,56 +445,50 @@ This module provides the core of the Odoo Web Client.
|
||||
'web/static/lib/nearest/jquery.nearest.js',
|
||||
'web/static/lib/daterangepicker/daterangepicker.js',
|
||||
'web/static/lib/stacktracejs/stacktrace.js',
|
||||
'web/static/tests/main_tests.js',
|
||||
|
||||
# 'web/static/tests/legacy/main_tests.js',
|
||||
'web/static/tests/helpers/**/*.js',
|
||||
'web/static/tests/webclient/**/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'),
|
||||
],
|
||||
'web.qunit_suite_tests': [
|
||||
'base/static/tests/base_settings_tests.js',
|
||||
'web/static/tests/chrome/**/*.js',
|
||||
'web/static/tests/components/action_menus_tests.js',
|
||||
'web/static/tests/components/custom_checkbox_tests.js',
|
||||
'web/static/tests/components/custom_file_input_tests.js',
|
||||
'web/static/tests/components/datepicker_tests.js',
|
||||
'web/static/tests/components/dropdown_menu_tests.js',
|
||||
'web/static/tests/components/pager_tests.js',
|
||||
'web/static/tests/control_panel/**/*.js',
|
||||
'web/static/tests/env_tests.js',
|
||||
'web/static/tests/core/**/*.js',
|
||||
'web/static/tests/fields/relational_fields/**/*.js',
|
||||
'web/static/tests/fields/basic_fields_tests.js',
|
||||
'web/static/tests/fields/field_utils_tests.js',
|
||||
'web/static/tests/fields/relational_fields_tests.js',
|
||||
'web/static/tests/fields/signature_tests.js',
|
||||
'web/static/tests/fields/special_fields_tests.js',
|
||||
'web/static/tests/fields/upgrade_fields_tests.js',
|
||||
'web/static/tests/services/**/*.js',
|
||||
'web/static/tests/tools/**/*.js',
|
||||
'web/static/tests/views/abstract_controller_tests.js',
|
||||
'web/static/tests/views/abstract_model_tests.js',
|
||||
'web/static/tests/views/abstract_view_banner_tests.js',
|
||||
'web/static/tests/views/abstract_view_tests.js',
|
||||
'web/static/tests/views/basic_model_tests.js',
|
||||
'web/static/tests/views/calendar_tests.js',
|
||||
'web/static/tests/views/form_tests.js',
|
||||
'web/static/tests/views/graph_tests.js',
|
||||
'web/static/tests/views/kanban_model_tests.js',
|
||||
'web/static/tests/views/kanban_tests.js',
|
||||
'web/static/tests/views/list_tests.js',
|
||||
'web/static/tests/views/pivot_tests.js',
|
||||
'web/static/tests/views/qweb_tests.js',
|
||||
'web/static/tests/views/sample_server_tests.js',
|
||||
'web/static/tests/views/search_panel_tests.js',
|
||||
'web/static/tests/views/view_dialogs_tests.js',
|
||||
'web/static/tests/widgets/**/*.js',
|
||||
'web/static/tests/report/**/*.js',
|
||||
'web/static/tests/component_extension_tests.js',
|
||||
'web/static/tests/mockserver_tests.js',
|
||||
'web/static/tests/owl_compatibility_tests.js',
|
||||
'web/static/tests/qweb_tests.js',
|
||||
'web/static/tests/fields/**/*.js',
|
||||
'web/static/tests/views/**/*.js',
|
||||
'web/static/tests/webclient/**/*.js',
|
||||
('remove', 'web/static/tests/webclient/**/helpers.js'),
|
||||
'web/static/tests/legacy/**/*.js',
|
||||
('remove', 'web/static/tests/legacy/**/*_mobile_tests.js'),
|
||||
('remove', 'web/static/tests/legacy/**/*_benchmarks.js'),
|
||||
('remove', 'web/static/tests/legacy/helpers/**/*.js'),
|
||||
('remove', 'web/static/tests/legacy/legacy_setup.js'),
|
||||
|
||||
('include', 'web.frontend_legacy_tests'),
|
||||
],
|
||||
'web.qunit_mobile_suite_tests': [
|
||||
'web/static/lib/jquery.touchSwipe/jquery.touchSwipe.js',
|
||||
'web/static/tests/fields/basic_fields_mobile_tests.js',
|
||||
'web/static/tests/fields/relational_fields_mobile_tests.js',
|
||||
'web/static/tests/components/dropdown_menu_mobile_tests.js',
|
||||
|
||||
'web/static/tests/legacy/fields/basic_fields_mobile_tests.js',
|
||||
'web/static/tests/legacy/fields/relational_fields_mobile_tests.js',
|
||||
'web/static/tests/legacy/components/dropdown_menu_mobile_tests.js',
|
||||
],
|
||||
|
||||
# Used during the transition of the web architecture
|
||||
'web.frontend_legacy_tests': [
|
||||
'web/static/tests/legacy/frontend/*.js',
|
||||
],
|
||||
},
|
||||
'bootstrap': True, # load translations for login screen
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"name": "web",
|
||||
"version": "0.0.0",
|
||||
"ignore": [
|
||||
"**/.*",
|
||||
"node_modules",
|
||||
"bower_components",
|
||||
"test",
|
||||
"tests"
|
||||
],
|
||||
"dependencies": {
|
||||
"jquery": "1.8.3",
|
||||
"underscore": "1.3.1",
|
||||
"underscore.string": "2.2.1",
|
||||
"qweb": "git@github.com:OpenERP/qweb.git#~1.0.0"
|
||||
}
|
||||
}
|
||||
@@ -845,7 +845,7 @@ class Home(http.Controller):
|
||||
:param unique: this parameters is not used, but mandatory: it is used by the HTTP stack to make a unique request
|
||||
:return: the menus (including the images in Base64)
|
||||
"""
|
||||
menus = request.env["ir.ui.menu"].load_menus(request.session.debug)
|
||||
menus = request.env["ir.ui.menu"].load_web_menus(request.session.debug)
|
||||
body = json.dumps(menus, default=ustr)
|
||||
response = request.make_response(body, [
|
||||
# this method must specify a content-type application/json instead of using the default text/html set because
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
<field name="mimetype">text/scss</field>
|
||||
<field name="name">res.company.scss</field>
|
||||
<field name="type">binary</field>
|
||||
<field name="url">/web/static/src/scss/asset_styles_company_report.scss</field>
|
||||
<field name="url">/web/static/src/legacy/scss/asset_styles_company_report.scss</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
|
||||
@@ -3,5 +3,6 @@
|
||||
|
||||
from . import ir_qweb
|
||||
from . import ir_http
|
||||
from . import ir_ui_menu
|
||||
from . import models
|
||||
from . import base_document_layout
|
||||
|
||||
@@ -40,6 +40,7 @@ class Http(models.AbstractModel):
|
||||
"db": request.session.db,
|
||||
"server_version": version_info.get('server_version'),
|
||||
"server_version_info": version_info.get('server_version_info'),
|
||||
"support_url": "https://www.odoo.com/buy",
|
||||
"name": user.name,
|
||||
"username": user.login,
|
||||
"partner_display_name": user.partner_id.display_name,
|
||||
@@ -51,6 +52,7 @@ class Http(models.AbstractModel):
|
||||
'profile_collectors': request.session.profile_collectors,
|
||||
'profile_params': request.session.profile_params,
|
||||
"max_file_upload_size": max_file_upload_size,
|
||||
"home_action_id": user.action_id.id,
|
||||
}
|
||||
if self.env.user.has_group('base.group_user'):
|
||||
# the following is only useful in the context of a webclient bootstrapping
|
||||
@@ -63,7 +65,9 @@ class Http(models.AbstractModel):
|
||||
qweb_checksum = HomeStaticTemplateHelpers.get_qweb_templates_checksum(debug=request.session.debug, bundle="web.assets_qweb")
|
||||
lang = user_context.get("lang")
|
||||
translation_hash = request.env['ir.translation'].get_web_translations_hash(mods, lang)
|
||||
menu_json_utf8 = json.dumps(request.env['ir.ui.menu'].load_menus(request.session.debug), default=ustr, sort_keys=True).encode()
|
||||
menus = request.env['ir.ui.menu'].load_menus(request.session.debug)
|
||||
ordered_menus = {str(k): v for k, v in menus.items()}
|
||||
menu_json_utf8 = json.dumps(ordered_menus, default=ustr, sort_keys=True).encode()
|
||||
cache_hashes = {
|
||||
"load_menus": hashlib.sha512(menu_json_utf8).hexdigest()[:64], # sha512/256
|
||||
"qweb": qweb_checksum,
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models
|
||||
|
||||
|
||||
class IrUiMenu(models.Model):
|
||||
_inherit = "ir.ui.menu"
|
||||
|
||||
def load_web_menus(self, debug):
|
||||
""" Loads all menu items (all applications and their sub-menus) and
|
||||
processes them to be used by the webclient. Mainly, it associates with
|
||||
each application (top level menu) the action of its first child menu
|
||||
that is associated with an action (recursively), i.e. with the action
|
||||
to execute when the opening the app.
|
||||
|
||||
:return: the menus (including the images in Base64)
|
||||
"""
|
||||
menus = self.load_menus(debug)
|
||||
|
||||
web_menus = {}
|
||||
for menu in menus.values():
|
||||
if not menu['id']:
|
||||
# special root menu case
|
||||
web_menus['root'] = {
|
||||
"id": 'root',
|
||||
"name": menu['name'],
|
||||
"children": menu['children'],
|
||||
"appID": False,
|
||||
"xmlid": "",
|
||||
"actionID": False,
|
||||
"actionModel": False,
|
||||
"webIcon": None,
|
||||
"webIconData": None,
|
||||
}
|
||||
else:
|
||||
action = menu['action']
|
||||
|
||||
if menu['id'] == menu['app_id']:
|
||||
# if it's an app take action of first (sub)child having one defined
|
||||
child = menu
|
||||
while child and not action:
|
||||
action = child['action']
|
||||
child = menus[child['children'][0]] if child['children'] else False
|
||||
|
||||
action_model, action_id = action.split(',') if action else (False, False)
|
||||
action_id = int(action_id) if action_id else False
|
||||
|
||||
web_menus[menu['id']] = {
|
||||
"id": menu['id'],
|
||||
"name": menu['name'],
|
||||
"children": menu['children'],
|
||||
"appID": menu['app_id'],
|
||||
"xmlid": menu['xmlid'],
|
||||
"actionID": action_id,
|
||||
"actionModel": action_model,
|
||||
"webIcon": menu['web_icon'],
|
||||
"webIconData": menu['web_icon_data'],
|
||||
}
|
||||
|
||||
return web_menus
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"devDependencies": {
|
||||
"grunt": "*",
|
||||
"grunt-contrib-jshint": "*",
|
||||
"grunt-contrib-sass": "~0.4.1",
|
||||
"grunt-contrib-watch": "~0.5.1"
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -14,6 +14,8 @@
|
||||
|
||||
global$1 = global$1 && global$1.hasOwnProperty('default') ? global$1['default'] : global$1;
|
||||
|
||||
const debug = odoo.debug;
|
||||
|
||||
var window$1 = global$1.window;
|
||||
var self$1 = global$1.self;
|
||||
var console = global$1.console;
|
||||
@@ -5338,7 +5340,7 @@
|
||||
|
||||
// Odoo Customisation!!!
|
||||
// Crappy hack to display traceback with sourcemaps if debug=assets
|
||||
if (lastError && QUnit.annotateTraceback && odoo && odoo.debug && odoo.debug.includes("assets")) {
|
||||
if (lastError && QUnit.annotateTraceback && debug && debug.includes("assets")) {
|
||||
const pre = assertLi.querySelector("pre");
|
||||
|
||||
QUnit.annotateTraceback(lastError).then(traceback => {
|
||||
|
||||
@@ -35,20 +35,21 @@
|
||||
|
||||
var services = Object.create({});
|
||||
|
||||
var commentRegExp = /(\/\*([\s\S]*?)\*\/|([^:]|^)\/\/(.*)$)/mg;
|
||||
var commentRegExp = /(\/\*([\s\S]*?)\*\/|([^:]|^)\/\/(.*)$)/gm;
|
||||
var cjsRequireRegExp = /[^.]\s*require\s*\(\s*["']([^'"\s]+)["']\s*\)/g;
|
||||
|
||||
if (!window.odoo) {
|
||||
window.odoo = {};
|
||||
}
|
||||
var odoo = window.odoo;
|
||||
var debug = odoo.debug;
|
||||
|
||||
var didLogInfoResolve;
|
||||
var didLogInfoPromise = new Promise(function (resolve) {
|
||||
didLogInfoResolve = resolve;
|
||||
});
|
||||
|
||||
odoo.testing = typeof QUnit === 'object';
|
||||
odoo.testing = typeof QUnit === "object";
|
||||
odoo.remainingJobs = jobs;
|
||||
odoo.__DEBUG__ = {
|
||||
didLogInfo: didLogInfoPromise,
|
||||
@@ -67,18 +68,23 @@
|
||||
return deps;
|
||||
},
|
||||
getDependents: function (name) {
|
||||
return jobDeps.filter(function (dep) {
|
||||
return dep.from === name;
|
||||
}).map(function (dep) {
|
||||
return dep.to;
|
||||
});
|
||||
return jobDeps
|
||||
.filter(function (dep) {
|
||||
return dep.from === name;
|
||||
})
|
||||
.map(function (dep) {
|
||||
return dep.to;
|
||||
});
|
||||
},
|
||||
getWaitedJobs: function () {
|
||||
return jobs.map(function (job) {
|
||||
return job.name;
|
||||
}).filter(function (item, index, self) { // uniq
|
||||
return self.indexOf(item) === index;
|
||||
});
|
||||
return jobs
|
||||
.map(function (job) {
|
||||
return job.name;
|
||||
})
|
||||
.filter(function (item, index, self) {
|
||||
// uniq
|
||||
return self.indexOf(item) === index;
|
||||
});
|
||||
},
|
||||
getMissingJobs: function () {
|
||||
var self = this;
|
||||
@@ -91,13 +97,16 @@
|
||||
}
|
||||
});
|
||||
});
|
||||
return missing.filter(function (item, index, self) {
|
||||
return self.indexOf(item) === index;
|
||||
}).filter(function (item) {
|
||||
return waited.indexOf(item) < 0;
|
||||
}).filter(function (job) {
|
||||
return !job.error;
|
||||
});
|
||||
return missing
|
||||
.filter(function (item, index, self) {
|
||||
return self.indexOf(item) === index;
|
||||
})
|
||||
.filter(function (item) {
|
||||
return waited.indexOf(item) < 0;
|
||||
})
|
||||
.filter(function (job) {
|
||||
return !job.error;
|
||||
});
|
||||
},
|
||||
getFailedJobs: function () {
|
||||
return jobs.filter(function (job) {
|
||||
@@ -109,27 +118,28 @@
|
||||
};
|
||||
odoo.define = function () {
|
||||
var args = Array.prototype.slice.call(arguments);
|
||||
var name = typeof args[0] === 'string' ? args.shift() : ('__odoo_job' + (jobUID++));
|
||||
var name = typeof args[0] === "string" ? args.shift() : "__odoo_job" + jobUID++;
|
||||
var factory = args[args.length - 1];
|
||||
var deps;
|
||||
if (args[0] instanceof Array) {
|
||||
deps = args[0];
|
||||
} else {
|
||||
deps = [];
|
||||
factory.toString()
|
||||
.replace(commentRegExp, '')
|
||||
factory
|
||||
.toString()
|
||||
.replace(commentRegExp, "")
|
||||
.replace(cjsRequireRegExp, function (match, dep) {
|
||||
deps.push(dep);
|
||||
});
|
||||
}
|
||||
|
||||
if (!(deps instanceof Array)) {
|
||||
throw new Error('Dependencies should be defined by an array', deps);
|
||||
throw new Error("Dependencies should be defined by an array", deps);
|
||||
}
|
||||
if (typeof factory !== 'function') {
|
||||
throw new Error('Factory should be defined by a function', factory);
|
||||
if (typeof factory !== "function") {
|
||||
throw new Error("Factory should be defined by a function", factory);
|
||||
}
|
||||
if (typeof name !== 'string') {
|
||||
if (typeof name !== "string") {
|
||||
throw new Error("Invalid name definition (should be a string", name);
|
||||
}
|
||||
if (name in factories) {
|
||||
@@ -146,7 +156,7 @@
|
||||
});
|
||||
|
||||
deps.forEach(function (dep) {
|
||||
jobDeps.push({from: dep, to: name});
|
||||
jobDeps.push({ from: dep, to: name });
|
||||
});
|
||||
|
||||
this.processJobs(jobs, services);
|
||||
@@ -154,6 +164,7 @@
|
||||
odoo.log = function () {
|
||||
var missing = [];
|
||||
var failed = [];
|
||||
var cycle = null;
|
||||
|
||||
if (jobs.length) {
|
||||
var debugJobs = {};
|
||||
@@ -166,7 +177,7 @@
|
||||
debugJobs[jobs[k].name] = job = {
|
||||
dependencies: jobs[k].deps,
|
||||
dependents: odoo.__DEBUG__.getDependents(jobs[k].name),
|
||||
name: jobs[k].name
|
||||
name: jobs[k].name,
|
||||
};
|
||||
if (jobs[k].error) {
|
||||
job.error = jobs[k].error;
|
||||
@@ -207,42 +218,58 @@
|
||||
var unloaded = Object.keys(debugJobs) // Object.values is not supported
|
||||
.map(function (key) {
|
||||
return debugJobs[key];
|
||||
}).filter(function (job) {
|
||||
})
|
||||
.filter(function (job) {
|
||||
return job.missing;
|
||||
});
|
||||
|
||||
if (odoo.debug || failed.length || unloaded.length) {
|
||||
var log = window.console[!failed.length || !unloaded.length ? 'info' : 'error'].bind(window.console);
|
||||
log((failed.length ? 'error' : (unloaded.length ? 'warning' : 'info')) + ': Some modules could not be started');
|
||||
if (debug || failed.length || unloaded.length) {
|
||||
var log = window.console[
|
||||
!failed.length || !unloaded.length ? "info" : "error"
|
||||
].bind(window.console);
|
||||
log(
|
||||
(failed.length ? "error" : unloaded.length ? "warning" : "info") +
|
||||
": Some modules could not be started"
|
||||
);
|
||||
if (missing.length) {
|
||||
log('Missing dependencies: ', missing);
|
||||
log("Missing dependencies: ", missing);
|
||||
}
|
||||
if (failed.length) {
|
||||
log('Failed modules: ', failed.map(function (fail) {
|
||||
return fail.name;
|
||||
}));
|
||||
log(
|
||||
"Failed modules: ",
|
||||
failed.map(function (fail) {
|
||||
return fail.name;
|
||||
})
|
||||
);
|
||||
}
|
||||
if (rejected.length) {
|
||||
log('Rejected modules: ', rejected);
|
||||
log("Rejected modules: ", rejected);
|
||||
}
|
||||
if (rejectedLinked.length) {
|
||||
log('Rejected linked modules: ', rejectedLinked);
|
||||
log("Rejected linked modules: ", rejectedLinked);
|
||||
}
|
||||
if (unloaded.length) {
|
||||
log('Non loaded modules: ', unloaded.map(function (unload) {
|
||||
return unload.name;
|
||||
}));
|
||||
cycle = findCycle(unloaded);
|
||||
if (cycle) {
|
||||
console.error("Cyclic dependencies: " + cycle);
|
||||
}
|
||||
log(
|
||||
"Non loaded modules: ",
|
||||
unloaded.map(function (unload) {
|
||||
return unload.name;
|
||||
})
|
||||
);
|
||||
}
|
||||
if (odoo.debug && Object.keys(debugJobs).length) {
|
||||
log('Debug: ', debugJobs);
|
||||
if (debug && Object.keys(debugJobs).length) {
|
||||
log("Debug: ", debugJobs);
|
||||
}
|
||||
}
|
||||
}
|
||||
odoo.__DEBUG__.jsModules = {
|
||||
missing: missing,
|
||||
failed: failed.map(function (fail) {
|
||||
return fail.name;
|
||||
}),
|
||||
failed: failed.map((mod) => mod.name),
|
||||
unloaded: unloaded ? unloaded.map((mod) => mod.name) : [],
|
||||
cycle,
|
||||
};
|
||||
didLogInfoResolve();
|
||||
};
|
||||
@@ -263,39 +290,47 @@
|
||||
console.error(`Error while loading ${job.name}: ${e.message}`);
|
||||
}
|
||||
if (!job.error) {
|
||||
Promise.resolve(jobExec).then(
|
||||
function (data) {
|
||||
Promise.resolve(jobExec)
|
||||
.then(function (data) {
|
||||
services[job.name] = data;
|
||||
resolve();
|
||||
odoo.processJobs(jobs, services);
|
||||
}).guardedCatch(function (e) {
|
||||
})
|
||||
.guardedCatch(function (e) {
|
||||
job.rejected = e || true;
|
||||
jobs.push(job);
|
||||
resolve();
|
||||
}
|
||||
);
|
||||
});
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
jobPromises.push(def);
|
||||
}
|
||||
|
||||
function isReady(job) {
|
||||
return !job.error && !job.rejected && job.factory.deps.every(function (name) {
|
||||
return name in services;
|
||||
});
|
||||
return (
|
||||
!job.error &&
|
||||
!job.rejected &&
|
||||
job.factory.deps.every(function (name) {
|
||||
return name in services;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function makeRequire(job) {
|
||||
var deps = {};
|
||||
Object.keys(services).filter(function (item) {
|
||||
return job.deps.indexOf(item) >= 0;
|
||||
}).forEach(function (key) {
|
||||
deps[key] = services[key];
|
||||
});
|
||||
Object.keys(services)
|
||||
.filter(function (item) {
|
||||
return job.deps.indexOf(item) >= 0;
|
||||
})
|
||||
.forEach(function (key) {
|
||||
deps[key] = services[key];
|
||||
});
|
||||
|
||||
return function require(name) {
|
||||
if (!(name in deps)) {
|
||||
console.error('Undefined dependency: ', name);
|
||||
console.error("Undefined dependency: ", name);
|
||||
}
|
||||
return deps[name];
|
||||
};
|
||||
@@ -319,7 +354,7 @@
|
||||
};
|
||||
|
||||
// Automatically log errors detected when loading modules
|
||||
window.addEventListener('load', function logWhenLoaded() {
|
||||
window.addEventListener("load", function logWhenLoaded() {
|
||||
setTimeout(function () {
|
||||
var len = jobPromises.length;
|
||||
Promise.all(jobPromises).then(function () {
|
||||
@@ -329,6 +364,47 @@
|
||||
logWhenLoaded();
|
||||
}
|
||||
});
|
||||
}, 9999);
|
||||
}, 5000);
|
||||
});
|
||||
|
||||
/**
|
||||
* Visit the list of jobs, and return the first found cycle, if any
|
||||
*
|
||||
* @param {any[]} jobs
|
||||
* @returns {null | string} either a string describing a cycle, or null
|
||||
*/
|
||||
function findCycle(jobs) {
|
||||
// build dependency graph
|
||||
const dependencyGraph = new Map();
|
||||
for (let job of jobs) {
|
||||
dependencyGraph.set(job.name, job.dependencies);
|
||||
}
|
||||
|
||||
// helpers
|
||||
function visitJobs(jobs, visited = new Set()) {
|
||||
for (let job of jobs) {
|
||||
const result = visitJob(job, visited);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function visitJob(job, visited) {
|
||||
if (visited.has(job)) {
|
||||
const jobs = Array.from(visited).concat([job]);
|
||||
const index = jobs.indexOf(job);
|
||||
return jobs
|
||||
.slice(index)
|
||||
.map((j) => `"${j}"`)
|
||||
.join(" => ");
|
||||
}
|
||||
const deps = dependencyGraph.get(job);
|
||||
return deps ? visitJobs(deps, new Set(visited).add(job)) : null;
|
||||
}
|
||||
|
||||
// visit each root to find cycles
|
||||
return visitJobs(jobs.map((j) => j.name));
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,252 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
const { useEnv, onWillStart } = owl.hooks;
|
||||
import { memoize } from "./utils/functions";
|
||||
import { browser } from "./browser/browser";
|
||||
|
||||
class AssetsLoadingError extends Error {}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Types
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* An object describing a bundle to load
|
||||
* @typedef {Object} Bundle
|
||||
* @property {boolean} [templates] whether to load the qweb templates
|
||||
* @property {boolean} [js] whether to load the js from the bundle. Currently
|
||||
* not implemented.
|
||||
* @property {boolean} [css] whether to load the css from the bundle. Currently
|
||||
* not implemented.
|
||||
*/
|
||||
|
||||
/**
|
||||
* An object describing a bundle to load. The keys are bundle names.
|
||||
* @typedef {Object<string, Bundle>} Bundles
|
||||
*/
|
||||
|
||||
/**
|
||||
* An object describing a loaded bundle
|
||||
* @typedef {Object} LoadedBundle
|
||||
* @property {XMLDocument} templates an XML document containing the owl
|
||||
* templates defined in that bundle
|
||||
*/
|
||||
|
||||
/**
|
||||
* An object describing a loaded bundle. The keys are bundle names.
|
||||
* @typedef {Object<string, LoadedBundle>} LoadedBundles
|
||||
*/
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Loads the given url inside a script tag.
|
||||
*
|
||||
* @param {string} url the url of the script
|
||||
* @returns {Promise} resolved when the script has been loaded
|
||||
*/
|
||||
const loadJS = memoize(function loadJS(url) {
|
||||
if (document.querySelector(`script[src="${url}"]`)) {
|
||||
// Already in the DOM and wasn't loaded through this function
|
||||
// Unfortunately there is no way to check whether a script has loaded
|
||||
// or not (which may not be the case for async/defer scripts)
|
||||
// so we assume it is.
|
||||
return Promise.resolve();
|
||||
}
|
||||
const scriptEl = document.createElement("script");
|
||||
scriptEl.type = "text/javascript";
|
||||
scriptEl.src = url;
|
||||
document.head.appendChild(scriptEl);
|
||||
return new Promise((resolve, reject) => {
|
||||
scriptEl.addEventListener("load", resolve);
|
||||
scriptEl.addEventListener("error", () => {
|
||||
reject(new AssetsLoadingError(`The loading of ${url} failed`));
|
||||
});
|
||||
});
|
||||
});
|
||||
/**
|
||||
* Loads the given url as a stylesheet.
|
||||
*
|
||||
* @param {string} url the url of the stylesheet
|
||||
* @returns {Promise} resolved when the stylesheet has been loaded
|
||||
*/
|
||||
const loadCSS = memoize(function loadCSS(url) {
|
||||
if (document.querySelector(`link[href="${url}"]`)) {
|
||||
// Already in the DOM and wasn't loaded through this function
|
||||
// Unfortunately there is no way to check whether a link has loaded
|
||||
// or not (which may not be the case for async/defer stylesheets)
|
||||
// so we assume it is.
|
||||
return Promise.resolve();
|
||||
}
|
||||
const linkEl = document.createElement("link");
|
||||
linkEl.type = "text/css";
|
||||
linkEl.rel = "stylesheet";
|
||||
linkEl.href = url;
|
||||
document.head.appendChild(linkEl);
|
||||
return new Promise(function (resolve, reject) {
|
||||
linkEl.addEventListener("load", resolve);
|
||||
linkEl.addEventListener("error", () => {
|
||||
reject(new AssetsLoadingError(`The loading of ${url} failed`));
|
||||
});
|
||||
});
|
||||
});
|
||||
/**
|
||||
* Loads the qweb templates from a given bundle name.
|
||||
*
|
||||
* @param {string} name the name of the bundle as declared in the manifest.
|
||||
* @returns {Promise<XMLDocument>} A Promise of an XML document containing the
|
||||
* owl templates.
|
||||
*/
|
||||
const loadBundleTemplates = memoize(async function loadBundleTemplates(name) {
|
||||
// TODO: quid of the "unique" in the URL? We can"t have one cache_hash
|
||||
// for each and every bundle I"m guessing.
|
||||
const bundleURL = new URL(`/web/webclient/qweb/${Date.now()}`, window.location.origin);
|
||||
bundleURL.searchParams.set("bundle", name);
|
||||
const templates = await (await browser.fetch(bundleURL.href)).text();
|
||||
return processTemplates(templates);
|
||||
});
|
||||
|
||||
const bundlesCache = {};
|
||||
/**
|
||||
* Loads a bundle.
|
||||
*
|
||||
* @param {string} name the name of the bundle to load
|
||||
* @param {Bundle} options parts of the bundle to load (see Bundle typedef)
|
||||
* @returns {Promise<LoadedBundle>}
|
||||
*/
|
||||
async function loadBundle(name, options) {
|
||||
const { templates } = options;
|
||||
if (!bundlesCache[name]) {
|
||||
bundlesCache[name] = { name };
|
||||
}
|
||||
if (templates && !bundlesCache[name].templates) {
|
||||
bundlesCache[name].templates = loadBundleTemplates(name);
|
||||
}
|
||||
// TODO: if ("js/css") {...} to support lazy loading js/css from bundles
|
||||
|
||||
// Wait only for the requested keys
|
||||
const entries = await Promise.all(
|
||||
Object.keys(options).map(async (key) => [key, await bundlesCache[name][key]])
|
||||
);
|
||||
return Object.fromEntries(entries);
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Exports
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Process the qweb templates to obtain only the owl templates. This function
|
||||
* does NOT register the templates into Owl.
|
||||
*
|
||||
* @param {string} templates An xml string describing templates
|
||||
* @returns {XMLDocument} An xml document containing only the owl templates
|
||||
*/
|
||||
export function processTemplates(templates) {
|
||||
const doc = new DOMParser().parseFromString(templates, "text/xml");
|
||||
// as we currently have two qweb engines (owl and legacy), owl templates are
|
||||
// flagged with attribute `owl="1"`. The following lines removes the "owl"
|
||||
// attribute from the templates, so that it doesn't appear in the DOM. We
|
||||
// also remove the non-owl templates, as those shouldn't be loaded in the
|
||||
// owl environment's QWeb, and will be loaded separately.
|
||||
for (const template of [...doc.querySelector("templates").children]) {
|
||||
if (template.hasAttribute("owl")) {
|
||||
template.removeAttribute("owl");
|
||||
} else {
|
||||
template.remove();
|
||||
}
|
||||
}
|
||||
return doc;
|
||||
}
|
||||
/**
|
||||
* Renders a public asset template and loads the libraries defined inside of it.
|
||||
* Only loads js and css, template declarations will be ignored. Only loads
|
||||
* scripts and styles that are defined in script src and link href, ignores
|
||||
* inline scripts and styles.
|
||||
*
|
||||
* @deprecated
|
||||
* @param {string} xmlid The xmlid of the template that defines the public asset
|
||||
* @param {ORM} orm An ORM object capable of calling methods on models
|
||||
* @returns {Promise} Resolved when the contents of the asset is loaded
|
||||
*/
|
||||
export const loadPublicAsset = memoize(async function loadPublicAsset(xmlid, orm) {
|
||||
const xml = await orm.call("ir.ui.view", "render_public_asset", [xmlid]);
|
||||
const doc = new DOMParser().parseFromString(`<xml>${xml}</xml>`, "text/xml");
|
||||
return loadAssets({
|
||||
cssLibs: [...doc.querySelectorAll("link[href]")].map((node) => node.getAttribute("href")),
|
||||
jsLibs: [...doc.querySelectorAll("script[src]")].map((node) => node.getAttribute("src")),
|
||||
});
|
||||
});
|
||||
/**
|
||||
* Loads the given assets. Currently, when passing bundles, only the templates
|
||||
* key is supported, as loading a bundle's JS/CSS asynchronously requires some
|
||||
* groundwork.
|
||||
*
|
||||
* @param {Object} assets
|
||||
* @param {Bundles} [assets.bundles] an object describing the bundles to load.
|
||||
* The keys are bundle names, and the values describe whether to load the
|
||||
* templates, js and/or css from that bundle.
|
||||
* @param {string[]} [assets.jsLibs] urls of the javascript libraries to load
|
||||
* @param {string[]} [assets.cssLibs] urls of the css libraries to load
|
||||
* @returns {Promise<{[bundles: LoadedBundle[]]}>} An object describing the loaded
|
||||
* assets. The js and css libs are loaded globally and will be loaded when
|
||||
* this promise is resolved, the owl xml templates will be loaded in the
|
||||
* `templates` key of each LoadedBundle object.
|
||||
*/
|
||||
export async function loadAssets(assets) {
|
||||
const proms = [];
|
||||
const loadedAssets = {};
|
||||
if ("bundles" in assets) {
|
||||
const bundles = Object.entries(assets.bundles);
|
||||
const bundlesProm = Promise.all(
|
||||
bundles.map(async ([name, options]) => [name, await loadBundle(name, options)])
|
||||
).then((loadedBundles) => {
|
||||
loadedAssets.bundles = Object.fromEntries(loadedBundles);
|
||||
});
|
||||
proms.push(bundlesProm);
|
||||
}
|
||||
if ("jsLibs" in assets) {
|
||||
proms.push(Promise.all(assets.jsLibs.map(loadJS)));
|
||||
}
|
||||
if ("cssLibs" in assets) {
|
||||
proms.push(Promise.all(assets.cssLibs.map(loadCSS)));
|
||||
}
|
||||
await Promise.all(proms);
|
||||
return loadedAssets;
|
||||
}
|
||||
/**
|
||||
* Loads the given assets, and adds the loaded owl templates into the current
|
||||
* environment's qweb instance.
|
||||
*
|
||||
* @param {Object} assets
|
||||
* @param {Bundles} [assets.bundles] the bundles to load. See above typedef for
|
||||
* details.
|
||||
* @param {string[]} [assets.jsLibs] urls of the javascript libraries to load
|
||||
* @param {string[]} [assets.cssLibs] urls of the css libraries to load
|
||||
*/
|
||||
export function useAssets(assets) {
|
||||
const env = useEnv();
|
||||
onWillStart(async () => {
|
||||
const loadedAssets = await loadAssets(assets);
|
||||
// TODO: { js, css } = loadedAssets when we support lazy loading js/css from bundles
|
||||
const { bundles } = loadedAssets;
|
||||
if (bundles) {
|
||||
const templateDocs = Object.values(bundles).map(({ templates }) => templates);
|
||||
// Some templates might already be defined by another bundle, but need
|
||||
// to be included in the current bundle for primary inherits to work.
|
||||
// We don't want to add those again. We also have to add them to qweb
|
||||
// one bundle at a time in case multiple bundles were specified that
|
||||
// include the same template (e.g. for inheritance)
|
||||
for (const xmlDoc of templateDocs) {
|
||||
for (const template of [...xmlDoc.querySelector("templates").children]) {
|
||||
if (template.getAttribute("t-name") in env.qweb.templates) {
|
||||
template.remove();
|
||||
}
|
||||
}
|
||||
env.qweb.addTemplates(xmlDoc);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { useEffect } from "./effect_hook";
|
||||
|
||||
const { useComponent } = owl.hooks;
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Hook functions
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Focus a given selector as soon as it appears in the DOM and if it was not
|
||||
* displayed before. If the selected target is an input|textarea, set the selection
|
||||
* at the end.
|
||||
*
|
||||
* @param {Object} [params]
|
||||
* @param {string} [params.selector='autofocus'] default: select the first element
|
||||
* with an `autofocus` attribute.
|
||||
* @returns {Function} function that forces the focus on the next update if visible.
|
||||
*/
|
||||
export function useAutofocus(params = {}) {
|
||||
const comp = useComponent();
|
||||
// Prevent autofocus in mobile
|
||||
if (comp.env.isSmall) {
|
||||
return () => {};
|
||||
}
|
||||
const selector = params.selector || "[autofocus]";
|
||||
let forceFocusCount = 0;
|
||||
useEffect(
|
||||
function autofocus(target) {
|
||||
if (target) {
|
||||
target.focus();
|
||||
if (["INPUT", "TEXTAREA"].includes(target.tagName)) {
|
||||
const inputEl = target;
|
||||
inputEl.selectionStart = inputEl.selectionEnd = inputEl.value.length;
|
||||
}
|
||||
}
|
||||
},
|
||||
() => [comp.el.querySelector(selector), forceFocusCount]
|
||||
);
|
||||
|
||||
return function focusOnUpdate() {
|
||||
forceFocusCount++; // force the effect to rerun on next patch
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
/**
|
||||
* Browser
|
||||
*
|
||||
* This file exports an object containing common browser API. It may not look
|
||||
* incredibly useful, but it is very convenient when one needs to test code using
|
||||
* these methods. With this indirection, it is possible to patch the browser
|
||||
* object for a test.
|
||||
*/
|
||||
|
||||
let sessionStorage = window.sessionStorage;
|
||||
let localStorage = owl.browser.localStorage;
|
||||
try {
|
||||
// Safari crashes in Private Browsing
|
||||
localStorage.setItem("__localStorage__", "true");
|
||||
localStorage.removeItem("__localStorage__");
|
||||
} catch (e) {
|
||||
localStorage = makeRAMLocalStorage();
|
||||
sessionStorage = makeRAMLocalStorage();
|
||||
}
|
||||
|
||||
export const browser = Object.assign({}, owl.browser, {
|
||||
addEventListener: window.addEventListener.bind(window),
|
||||
removeEventListener: window.removeEventListener.bind(window),
|
||||
console: window.console,
|
||||
location: window.location,
|
||||
history: window.history,
|
||||
navigator: navigator,
|
||||
open: window.open.bind(window),
|
||||
XMLHttpRequest: window.XMLHttpRequest,
|
||||
localStorage,
|
||||
sessionStorage,
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// memory localStorage
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @returns {typeof window["localStorage"]}
|
||||
*/
|
||||
export function makeRAMLocalStorage() {
|
||||
let store = {};
|
||||
return {
|
||||
setItem(key, value) {
|
||||
store[key] = value;
|
||||
},
|
||||
getItem(key) {
|
||||
return store[key];
|
||||
},
|
||||
clear() {
|
||||
store = {};
|
||||
},
|
||||
removeItem(key) {
|
||||
delete store[key];
|
||||
},
|
||||
get length() {
|
||||
return Object.keys(store).length;
|
||||
},
|
||||
key() {
|
||||
return "";
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "../registry";
|
||||
|
||||
/**
|
||||
* Service to make use of document.cookie
|
||||
* https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies
|
||||
* As recommended, storage should not be done by the cookie
|
||||
* but with localStorage/sessionStorage
|
||||
*/
|
||||
|
||||
const COOKIE_TTL = 24 * 60 * 60 * 365;
|
||||
|
||||
function parseCookieString(str) {
|
||||
const cookie = {};
|
||||
const parts = str.split("; ");
|
||||
for (let part of parts) {
|
||||
const [key, value] = part.split("=");
|
||||
cookie[key] = value || "";
|
||||
}
|
||||
return cookie;
|
||||
}
|
||||
|
||||
function cookieToString(key, value, ttl = COOKIE_TTL) {
|
||||
let fullCookie = [];
|
||||
if (value !== undefined) {
|
||||
fullCookie.push(`${key}=${value}`);
|
||||
}
|
||||
fullCookie = fullCookie.concat(["path=/", `max-age=${ttl}`]);
|
||||
return fullCookie.join(";");
|
||||
}
|
||||
|
||||
function makeCookieService() {
|
||||
function getCurrent() {
|
||||
return parseCookieString(document.cookie);
|
||||
}
|
||||
let cookie = getCurrent();
|
||||
function setCookie(key, value, ttl) {
|
||||
document.cookie = cookieToString(key, value, ttl);
|
||||
cookie = getCurrent();
|
||||
}
|
||||
return {
|
||||
get current() {
|
||||
return cookie;
|
||||
},
|
||||
setCookie,
|
||||
deleteCookie(key) {
|
||||
setCookie(key, "kill", 0);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const cookieService = {
|
||||
start() {
|
||||
return makeCookieService();
|
||||
},
|
||||
};
|
||||
|
||||
registry.category("services").add("cookie", cookieService);
|
||||
@@ -0,0 +1,40 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { browser } from "./browser";
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Feature detection
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* true if the browser is based on Chromium (Google Chrome, Opera, Edge)
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isBrowserChrome() {
|
||||
return browser.navigator.userAgent.includes("Chrome");
|
||||
}
|
||||
|
||||
export function isMacOS() {
|
||||
return Boolean(browser.navigator.platform.match(/Mac/i));
|
||||
}
|
||||
|
||||
export function isMobileOS() {
|
||||
return Boolean(
|
||||
browser.navigator.userAgent.match(/Android/i) ||
|
||||
browser.navigator.userAgent.match(/webOS/i) ||
|
||||
browser.navigator.userAgent.match(/iPhone/i) ||
|
||||
browser.navigator.userAgent.match(/iPad/i) ||
|
||||
browser.navigator.userAgent.match(/iPod/i) ||
|
||||
browser.navigator.userAgent.match(/BlackBerry/i) ||
|
||||
browser.navigator.userAgent.match(/Windows Phone/i)
|
||||
);
|
||||
}
|
||||
|
||||
export function isIosApp() {
|
||||
return navigator.userAgent.match(/OdooMobile \(iOS\)/i);
|
||||
}
|
||||
|
||||
export function hasTouch() {
|
||||
return "ontouchstart" in window || "onmsgesturechange" in window;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user