[IMP] web: display empty screen when switching app/menu
The goal is to provide a better feedback to the user, especially in mobile mode. With this commit, when switching menus, we clear the screen and display an empty control panel (only for act_window actions though) so we have some initial content that get filled as soon as the view/action is ready. Task 2900847 closes odoo/odoo#124068 Related: odoo/enterprise#43038 Signed-off-by: Aaron Bohy (aab) <aab@odoo.com>
This commit is contained in:
@@ -8,7 +8,7 @@
|
||||
<div class="o_control_panel_main_buttons d-flex gap-1 d-empty-none d-print-none" t-ref="mainButtons" t-on-keydown="onMainButtonsKeydown">
|
||||
<div class="btn-group d-xl-none o_control_panel_collapsed_create">
|
||||
<t t-slot="control-panel-create-button"/>
|
||||
<button type="button" class="btn btn-primary dropdown-toggle dropdown-toggle-split o_control_panel_collapsed_create" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<button t-att-class="{invisible: display.disableDropdown}" type="button" class="btn btn-primary dropdown-toggle dropdown-toggle-split o_control_panel_collapsed_create" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<span class="visually-hidden">Toggle Dropdown</span>
|
||||
</button>
|
||||
<ul class="dropdown-menu o_control_panel_collapsed_create">
|
||||
|
||||
@@ -5,7 +5,7 @@ import { makeContext } from "@web/core/context";
|
||||
import { useDebugCategory } from "@web/core/debug/debug_context";
|
||||
import { evaluateExpr } from "@web/core/py_js/py";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { KeepLast } from "@web/core/utils/concurrency";
|
||||
import { Deferred, KeepLast } from "@web/core/utils/concurrency";
|
||||
import { useBus, useService } from "@web/core/utils/hooks";
|
||||
import { sprintf } from "@web/core/utils/strings";
|
||||
import { cleanDomFromBootstrap } from "@web/legacy/utils";
|
||||
@@ -14,6 +14,7 @@ import { ActionDialog } from "./action_dialog";
|
||||
import { CallbackRecorder } from "./action_hook";
|
||||
import { ReportAction } from "./reports/report_action";
|
||||
import { UPDATE_METHODS } from "@web/core/orm_service";
|
||||
import { ControlPanel } from "@web/search/control_panel/control_panel";
|
||||
|
||||
import {
|
||||
Component,
|
||||
@@ -27,6 +28,22 @@ import {
|
||||
} from "@odoo/owl";
|
||||
import { downloadReport, getReportUrl } from "./reports/utils";
|
||||
|
||||
class BlankComponent extends Component {
|
||||
static props = ["onMounted", "withControlPanel", "*"];
|
||||
static template = xml`
|
||||
<ControlPanel display="{disableDropdown: true}" t-if="props.withControlPanel and !env.isSmall">
|
||||
<t t-set-slot="layout-buttons">
|
||||
<button class="btn btn-primary invisible"> empty </button>
|
||||
</t>
|
||||
</ControlPanel>`;
|
||||
static components = { ControlPanel };
|
||||
|
||||
setup() {
|
||||
useChildSubEnv({ config: { breadcrumbs: [], noBreadcrumbs: true } });
|
||||
onMounted(() => this.props.onMounted());
|
||||
}
|
||||
}
|
||||
|
||||
const actionHandlersRegistry = registry.category("action_handlers");
|
||||
const actionRegistry = registry.category("actions");
|
||||
const viewRegistry = registry.category("views");
|
||||
@@ -809,6 +826,21 @@ function makeActionManager(env) {
|
||||
|
||||
const closingProm = _executeCloseAction();
|
||||
|
||||
if (options.clearBreadcrumbs && !options.noEmptyTransition) {
|
||||
const def = new Deferred();
|
||||
env.bus.trigger("ACTION_MANAGER:UPDATE", {
|
||||
id: ++id,
|
||||
Component: BlankComponent,
|
||||
componentProps: {
|
||||
onMounted: () => def.resolve(),
|
||||
withControlPanel: action.type === "ir.actions.act_window",
|
||||
},
|
||||
});
|
||||
await def;
|
||||
}
|
||||
if (options.onActionReady) {
|
||||
options.onActionReady(action);
|
||||
}
|
||||
controller.__info__ = {
|
||||
id: ++id,
|
||||
Component: ControllerComponent,
|
||||
@@ -924,6 +956,8 @@ function makeActionManager(env) {
|
||||
clearBreadcrumbs: options.clearBreadcrumbs,
|
||||
onClose: options.onClose,
|
||||
stackPosition: options.stackPosition,
|
||||
onActionReady: options.onActionReady,
|
||||
noEmptyTransition: options.noEmptyTransition,
|
||||
};
|
||||
|
||||
if (lazyView) {
|
||||
@@ -985,6 +1019,8 @@ function makeActionManager(env) {
|
||||
clearBreadcrumbs: options.clearBreadcrumbs,
|
||||
stackPosition: options.stackPosition,
|
||||
onClose: options.onClose,
|
||||
onActionReady: options.onActionReady,
|
||||
noEmptyTransition: options.noEmptyTransition,
|
||||
});
|
||||
} else {
|
||||
const next = await clientAction(env, action);
|
||||
|
||||
@@ -53,8 +53,12 @@ function makeMenus(env, menusData, fetchLoadMenus) {
|
||||
if (!menu.actionID) {
|
||||
return;
|
||||
}
|
||||
await env.services.action.doAction(menu.actionID, { clearBreadcrumbs: true });
|
||||
this.setCurrentMenu(menu);
|
||||
await env.services.action.doAction(menu.actionID, {
|
||||
clearBreadcrumbs: true,
|
||||
onActionReady: () => {
|
||||
this.setCurrentMenu(menu);
|
||||
},
|
||||
});
|
||||
},
|
||||
setCurrentMenu(menu) {
|
||||
menu = typeof menu === "number" ? this.getMenu(menu) : menu;
|
||||
|
||||
@@ -111,6 +111,8 @@ QUnit.test("opens an app", async (assert) => {
|
||||
triggerHotkey("enter");
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
// empty screen for now, wait for actual action to show up
|
||||
await nextTick();
|
||||
assert.strictEqual(target.querySelector(".o_menu_brand").textContent, "Contact");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".test_client_action").textContent,
|
||||
@@ -131,6 +133,8 @@ QUnit.test("opens a menu items", async (assert) => {
|
||||
click(target, "#o_command_2");
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
// empty screen for now, wait for actual action to show up
|
||||
await nextTick();
|
||||
assert.strictEqual(target.querySelector(".o_menu_brand").textContent, "Sales");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".test_client_action").textContent,
|
||||
|
||||
@@ -1777,6 +1777,8 @@ QUnit.module("Views", (hooks) => {
|
||||
const webClient = await createWebClient({ serverData });
|
||||
|
||||
await doAction(webClient, 1);
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_main_navbar .o_menu_brand").textContent,
|
||||
"App0"
|
||||
|
||||
@@ -516,7 +516,7 @@ QUnit.module("ActionManager", (hooks) => {
|
||||
await doAction(webClient, 3);
|
||||
assert.containsOnce(target, ".o_list_view");
|
||||
def = makeDeferred();
|
||||
doAction(webClient, 4, { clearBreadcrumbs: true });
|
||||
doAction(webClient, 4);
|
||||
await nextTick();
|
||||
assert.containsOnce(target, ".o_list_view", "should still contain the list view");
|
||||
await switchView(target, "kanban");
|
||||
@@ -549,7 +549,7 @@ QUnit.module("ActionManager", (hooks) => {
|
||||
const webClient = await createWebClient({ serverData, mockRPC });
|
||||
await doAction(webClient, 3);
|
||||
assert.containsOnce(target, ".o_list_view");
|
||||
doAction(webClient, 4, { clearBreadcrumbs: true });
|
||||
doAction(webClient, 4);
|
||||
await nextTick();
|
||||
await switchView(target, "kanban");
|
||||
def.resolve();
|
||||
|
||||
@@ -124,6 +124,7 @@ QUnit.module("ActionManager", (hooks) => {
|
||||
|
||||
await createWebClient({ serverData });
|
||||
await testUtils.nextTick(); // wait for the navbar to be updated
|
||||
await testUtils.nextTick(); // wait for the action to be displayed
|
||||
|
||||
assert.containsOnce(target, ".test_client_action");
|
||||
assert.strictEqual(target.querySelector(".o_menu_brand").innerText, "App1");
|
||||
@@ -198,6 +199,7 @@ QUnit.module("ActionManager", (hooks) => {
|
||||
action: "HelloWorldTest",
|
||||
});
|
||||
await testUtils.nextTick();
|
||||
await testUtils.nextTick();
|
||||
assert.strictEqual(
|
||||
$(target).find(".o_client_action_test").text(),
|
||||
"Hello World",
|
||||
@@ -238,6 +240,7 @@ QUnit.module("ActionManager", (hooks) => {
|
||||
model: "partner",
|
||||
});
|
||||
await testUtils.nextTick();
|
||||
await testUtils.nextTick();
|
||||
assert.containsOnce(target, ".o_form_view");
|
||||
assert.deepEqual(getBreadCrumbTexts(target), ["Second record"]);
|
||||
assert.verifySteps(["/web/webclient/load_menus", "get_views", "read"]);
|
||||
@@ -687,6 +690,7 @@ QUnit.module("ActionManager", (hooks) => {
|
||||
1: { id: 1, children: [], name: "App1", appID: 1, actionID: 1 },
|
||||
};
|
||||
const webClient = await createWebClient({ serverData });
|
||||
await nextTick();
|
||||
assert.containsOnce(target, ".o_kanban_view"); // action 1 (default app)
|
||||
assert.deepEqual(getBreadCrumbTexts(target), ["Partners Action 1"]);
|
||||
await loadState(webClient, { action: 3 });
|
||||
@@ -964,6 +968,8 @@ QUnit.module("ActionManager", (hooks) => {
|
||||
browser.location.hash = "#action=myAction";
|
||||
|
||||
const webClient = await createWebClient({ serverData });
|
||||
assert.verifySteps([]);
|
||||
await nextTick();
|
||||
assert.verifySteps(["myAction mounted"]);
|
||||
assert.containsOnce(target, ".not-here");
|
||||
|
||||
@@ -971,6 +977,9 @@ QUnit.module("ActionManager", (hooks) => {
|
||||
await hashchangeDef;
|
||||
await nextTick();
|
||||
assert.containsNone(target, ".not-here");
|
||||
assert.containsNone(target, ".test_client_action");
|
||||
await nextTick();
|
||||
assert.containsNone(target, ".not-here");
|
||||
assert.containsOnce(target, ".test_client_action");
|
||||
|
||||
assert.deepEqual(webClient.env.services.router.current.hash, {
|
||||
@@ -980,7 +989,7 @@ QUnit.module("ActionManager", (hooks) => {
|
||||
});
|
||||
|
||||
QUnit.test("concurrent hashchange during action mounting -- 2", async (assert) => {
|
||||
assert.expect(5);
|
||||
assert.expect(6);
|
||||
|
||||
const baseURL = new URL(browser.location.href).toString();
|
||||
|
||||
@@ -999,8 +1008,11 @@ QUnit.module("ActionManager", (hooks) => {
|
||||
|
||||
browser.location.hash = "#action=myAction";
|
||||
const webClient = await createWebClient({ serverData });
|
||||
assert.verifySteps([]);
|
||||
await nextTick();
|
||||
assert.verifySteps(["myAction mounted"]);
|
||||
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
assert.containsNone(target, ".not-here");
|
||||
assert.containsOnce(target, ".test_client_action");
|
||||
|
||||
@@ -6,6 +6,7 @@ import { session } from "@web/session";
|
||||
import AbstractAction from "web.AbstractAction";
|
||||
import core from "web.core";
|
||||
import { makeTestEnv } from "../../helpers/mock_env";
|
||||
import testUtils from "web.test_utils";
|
||||
import {
|
||||
click,
|
||||
getFixture,
|
||||
@@ -22,6 +23,7 @@ import {
|
||||
} from "./../helpers";
|
||||
import { listView } from "@web/views/list/list_view";
|
||||
import { companyService } from "@web/webclient/company_service";
|
||||
import { onWillStart } from "@odoo/owl";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
@@ -435,6 +437,7 @@ QUnit.module("ActionManager", (hooks) => {
|
||||
|
||||
// Create the web client. It should execute the stored action.
|
||||
const webClient = await createWebClient({ serverData });
|
||||
await nextTick();
|
||||
|
||||
// Check the current action context
|
||||
assert.deepEqual(webClient.env.services.action.currentController.action.context, {
|
||||
@@ -447,4 +450,44 @@ QUnit.module("ActionManager", (hooks) => {
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test(
|
||||
"action is removed while waiting for another action with selectMenu",
|
||||
async (assert) => {
|
||||
let def;
|
||||
const actionRegistry = registry.category("actions");
|
||||
const ClientAction = actionRegistry.get("__test__client__action__");
|
||||
|
||||
patchWithCleanup(ClientAction.prototype, {
|
||||
setup() {
|
||||
this._super();
|
||||
onWillStart(() => {
|
||||
return def;
|
||||
});
|
||||
},
|
||||
});
|
||||
const webClient = await createWebClient({ serverData });
|
||||
// starting point: a kanban view
|
||||
await doAction(webClient, 4);
|
||||
const root = document.querySelector(".o_web_client");
|
||||
assert.containsOnce(root, ".o_kanban_view");
|
||||
|
||||
// select one new app in navbar menu
|
||||
def = testUtils.makeTestPromise();
|
||||
const menus = webClient.env.services.menu.getApps();
|
||||
webClient.env.services.menu.selectMenu(menus[1], { resetScreen: true });
|
||||
await nextTick();
|
||||
|
||||
// check that the action manager is empty, even though client action is loading
|
||||
assert.strictEqual(root.querySelector(".o_action_manager").textContent, "");
|
||||
|
||||
// resolve onwillstart so client action is ready
|
||||
def.resolve();
|
||||
await nextTick();
|
||||
assert.strictEqual(
|
||||
root.querySelector(".o_action_manager").textContent,
|
||||
" ClientAction_Id 1"
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
@@ -556,6 +556,7 @@ QUnit.module("ActionManager", (hooks) => {
|
||||
'<form><button name="24" type="action" class="oe_stat_button"/></form>';
|
||||
await createWebClient({ serverData });
|
||||
await nextTick(); // wait for the load state (default app)
|
||||
await nextTick(); // wait for the action to be mounted
|
||||
assert.containsOnce(target, "nav .o_menu_brand");
|
||||
assert.strictEqual(target.querySelector("nav .o_menu_brand").innerText, "MAIN APP");
|
||||
await click(target.querySelector("button[name='24']"));
|
||||
|
||||
Reference in New Issue
Block a user