Files
odoo_source/addons/web/static/tests/webclient/helpers.js
T
Damien Bouvy 264ec1c4dd [IMP] web: support explicit mobile view mode on window actions
Up until this commit, which view was displayed by default on mobile was
somewhat of a lottery.

Initially, only kanban views were considered as 'more adapted to mobile
devices' and used as a main fallback to display records who had a kanban
view in the window action definition.

The the concept of 'mobile-friendly view' was extended to map and grid
so that these views could take precedence over the kanban view in
specific circumstances (indsutry_fsm and timesheet_grid, both of which
are enterprise edition apps) - basically they were marked as
mobile-friendly not because they *are*, but because it was the only way
to override the kanban override.

But this comes with its lot of problems and limitations, namely that the
mobile friendly view will be found based on the order of view modes for
a window action, so if you have a window action with the view modes
`kanban,map,tree,form`, you will never be able to have e.g. the kanban
view shown on desktop by default and the map view on mobile.

This commit introduces the notion of a 'default mobile view' on window
actions so that one may decide on an *arbitrary* view mode to use on
mobile for an action - without impacting the ordering of views on other
models, etc.

Task-3460374

closes odoo/odoo#133608

Related: odoo/enterprise#46562
Signed-off-by: Aaron Bohy (aab) <aab@odoo.com>
2023-09-22 15:06:00 +00:00

411 lines
14 KiB
JavaScript

/** @odoo-module **/
import { dialogService } from "@web/core/dialog/dialog_service";
import { notificationService } from "@web/core/notifications/notification_service";
import { ormService } from "@web/core/orm_service";
import { popoverService } from "@web/core/popover/popover_service";
import { registry } from "@web/core/registry";
import { legacyServiceProvider } from "@web/legacy/legacy_service_provider";
import { viewService } from "@web/views/view_service";
import { actionService } from "@web/webclient/actions/action_service";
import { effectService } from "@web/core/effects/effect_service";
import { hotkeyService } from "@web/core/hotkeys/hotkey_service";
import { menuService } from "@web/webclient/menus/menu_service";
import { WebClient } from "@web/webclient/webclient";
import { registerCleanup } from "../helpers/cleanup";
import { makeTestEnv } from "../helpers/mock_env";
import {
fakeTitleService,
fakeCompanyService,
makeFakeLocalizationService,
makeFakeRouterService,
makeFakeHTTPService,
makeFakeUserService,
} from "../helpers/mock_services";
import { getFixture, mount, nextTick } from "../helpers/utils";
import { uiService } from "@web/core/ui/ui_service";
import { commandService } from "@web/core/commands/command_service";
import { CustomFavoriteItem } from "@web/search/custom_favorite_item/custom_favorite_item";
import { overlayService } from "@web/core/overlay/overlay_service";
import { Component, xml } from "@odoo/owl";
import { fieldService } from "@web/core/field_service";
import { nameService } from "@web/core/name_service";
import { datetimePickerService } from "@web/core/datetime/datetimepicker_service";
const actionRegistry = registry.category("actions");
const serviceRegistry = registry.category("services");
const favoriteMenuRegistry = registry.category("favoriteMenu");
/**
* Builds the required registries for tests using a WebClient.
* We use a default version of each required registry item.
* If the registry already contains one of those items,
* the existing one is kept (it means it has been added in the test
* directly, e.g. to have a custom version of the item).
*/
export function setupWebClientRegistries() {
const favoriveMenuItems = {
"custom-favorite-item": {
value: { Component: CustomFavoriteItem, groupNumber: 3 },
options: { sequence: 0 },
},
};
for (const [key, { value, options }] of Object.entries(favoriveMenuItems)) {
if (!favoriteMenuRegistry.contains(key)) {
favoriteMenuRegistry.add(key, value, options);
}
}
const services = {
action: () => actionService,
command: () => commandService,
dialog: () => dialogService,
effect: () => effectService,
field: () => fieldService,
hotkey: () => hotkeyService,
http: () => makeFakeHTTPService(),
legacy_service_provider: () => legacyServiceProvider,
localization: () => makeFakeLocalizationService(),
menu: () => menuService,
name: () => nameService,
notification: () => notificationService,
orm: () => ormService,
overlay: () => overlayService,
popover: () => popoverService,
router: () => makeFakeRouterService(),
title: () => fakeTitleService,
ui: () => uiService,
user: () => makeFakeUserService(),
view: () => viewService,
company: () => fakeCompanyService,
datetime_picker: () => datetimePickerService,
};
for (const serviceName in services) {
if (!serviceRegistry.contains(serviceName)) {
serviceRegistry.add(serviceName, services[serviceName]());
}
}
}
/**
* This method create a web client instance properly configured.
*
* Note that the returned web client will be automatically cleaned up after the
* end of the test.
*
* @param {*} params
*/
export async function createWebClient(params) {
setupWebClientRegistries();
params.serverData = params.serverData || {};
const mockRPC = params.mockRPC || undefined;
const env = await makeTestEnv({
serverData: params.serverData,
mockRPC,
});
const WebClientClass = params.WebClientClass || WebClient;
const target = params && params.target ? params.target : getFixture();
const wc = await mount(WebClientClass, target, { env });
odoo.__WOWL_DEBUG__ = { root: wc };
target.classList.add("o_web_client"); // necessary for the stylesheet
registerCleanup(() => {
target.classList.remove("o_web_client");
});
// Wait for visual changes caused by a potential loadState
await nextTick();
return wc;
}
export function doAction(env, ...args) {
if (env instanceof Component) {
env = env.env;
}
return env.services.action.doAction(...args);
}
export async function loadState(env, state) {
if (env instanceof Component) {
env = env.env;
}
env.bus.trigger("test:hashchange", state);
// wait the asynchronous hashchange
// (the event hashchange must be triggered in a nonBlocking stack)
await nextTick();
// wait for BlankComponent
await nextTick();
// wait for the regular rendering
await nextTick();
}
export function getActionManagerServerData() {
// additional basic client action
class TestClientAction extends Component {}
TestClientAction.template = xml`
<div class="test_client_action">
ClientAction_<t t-esc="props.action.params?.description"/>
</div>`;
actionRegistry.add("__test__client__action__", TestClientAction);
const menus = {
root: { id: "root", children: [0, 1, 2], name: "root", appID: "root" },
// id:0 is a hack to not load anything at webClient mount
0: { id: 0, children: [], name: "UglyHack", appID: 0, xmlid: "menu_0" },
1: { id: 1, children: [], name: "App1", appID: 1, actionID: 1001, xmlid: "menu_1" },
2: { id: 2, children: [], name: "App2", appID: 2, actionID: 1002, xmlid: "menu_2" },
};
const actionsArray = [
{
id: 1,
xml_id: "action_1",
name: "Partners Action 1",
res_model: "partner",
type: "ir.actions.act_window",
views: [[1, "kanban"]],
},
{
id: 2,
xml_id: "action_2",
type: "ir.actions.server",
},
{
id: 3,
xml_id: "action_3",
name: "Partners",
res_model: "partner",
mobile_view_mode: "kanban",
type: "ir.actions.act_window",
views: [
[false, "list"],
[1, "kanban"],
[false, "form"],
],
},
{
id: 4,
xml_id: "action_4",
name: "Partners Action 4",
res_model: "partner",
type: "ir.actions.act_window",
views: [
[1, "kanban"],
[2, "list"],
[false, "form"],
],
},
{
id: 5,
xml_id: "action_5",
name: "Create a Partner",
res_model: "partner",
target: "new",
type: "ir.actions.act_window",
views: [[false, "form"]],
},
{
id: 6,
xml_id: "action_6",
name: "Partner",
res_id: 2,
res_model: "partner",
target: "inline",
type: "ir.actions.act_window",
views: [[false, "form"]],
},
{
id: 7,
xml_id: "action_7",
name: "Some Report",
report_name: "some_report",
report_type: "qweb-pdf",
type: "ir.actions.report",
},
{
id: 8,
xml_id: "action_8",
name: "Favorite Ponies",
res_model: "pony",
type: "ir.actions.act_window",
views: [
[false, "list"],
[false, "form"],
],
},
{
id: 9,
xml_id: "action_9",
name: "A Client Action",
tag: "ClientAction",
type: "ir.actions.client",
},
{
id: 10,
type: "ir.actions.act_window_close",
},
{
id: 11,
xml_id: "action_11",
name: "Another Report",
report_name: "another_report",
report_type: "qweb-pdf",
type: "ir.actions.report",
close_on_report_download: true,
},
{
id: 12,
xml_id: "action_12",
name: "Some HTML Report",
report_name: "some_report",
report_type: "qweb-html",
type: "ir.actions.report",
},
{
id: 24,
name: "Partner",
res_id: 2,
res_model: "partner",
type: "ir.actions.act_window",
views: [[666, "form"]],
},
{
id: 25,
name: "Create a Partner",
res_model: "partner",
target: "new",
type: "ir.actions.act_window",
views: [[3, "form"]],
},
{
id: 26,
xml_id: "action_26",
name: "Partner",
res_model: "partner",
target: "new",
type: "ir.actions.act_window",
views: [[false, "list"]],
},
{
id: 1001,
tag: "__test__client__action__",
target: "main",
type: "ir.actions.client",
params: { description: "Id 1" },
},
{
id: 1002,
tag: "__test__client__action__",
target: "main",
type: "ir.actions.client",
params: { description: "Id 2" },
},
{
xmlId: "wowl.client_action",
id: 1099,
tag: "__test__client__action__",
target: "main",
type: "ir.actions.client",
params: { description: "xmlId" },
},
];
const actions = {};
actionsArray.forEach((act) => {
actions[act.xmlId || act.id] = act;
});
const archs = {
// kanban views
"partner,1,kanban":
'<kanban><templates><t t-name="kanban-box">' +
'<div class="oe_kanban_global_click"><field name="foo"/></div>' +
"</t></templates></kanban>",
// list views
"partner,false,list": '<tree><field name="foo"/></tree>',
"partner,2,list": '<tree limit="3"><field name="foo"/></tree>',
"pony,false,list": '<tree><field name="name"/></tree>',
// form views
"partner,false,form":
"<form>" +
"<header>" +
'<button name="object" string="Call method" type="object"/>' +
'<button name="4" string="Execute action" type="action"/>' +
"</header>" +
"<group>" +
'<field name="display_name"/>' +
'<field name="foo"/>' +
"</group>" +
"</form>",
"partner,3,form": `
<form>
<footer>
<button class="btn-primary" string="Save" special="save"/>
</footer>
</form>`,
"partner,666,form": `<form>
<header></header>
<sheet>
<div class="oe_button_box" name="button_box">
<button class="oe_stat_button" type="action" name="1" icon="fa-star" context="{'default_partner': active_id}">
<field string="Partners" name="o2m" widget="statinfo"/>
</button>
</div>
<field name="display_name"/>
</sheet>
</form>`,
"pony,false,form": "<form>" + '<field name="name"/>' + "</form>",
// search views
"partner,false,search": '<search><field name="foo" string="Foo"/></search>',
"partner,4,search":
"<search>" +
'<filter name="bar" help="Bar" domain="[(\'bar\', \'=\', 1)]"/>' +
"</search>",
"pony,false,search": "<search></search>",
};
const models = {
partner: {
fields: {
id: { string: "Id", type: "integer" },
foo: { string: "Foo", type: "char" },
bar: { string: "Bar", type: "many2one", relation: "partner" },
o2m: {
string: "One2Many",
type: "one2many",
relation: "partner",
relation_field: "bar",
},
m2o: { string: "Many2one", type: "many2one", relation: "partner" },
},
records: [
{ id: 1, display_name: "First record", foo: "yop", bar: 2, o2m: [2, 3], m2o: 3 },
{
id: 2,
display_name: "Second record",
foo: "blip",
bar: 1,
o2m: [1, 4, 5],
m2o: 3,
},
{ id: 3, display_name: "Third record", foo: "gnap", bar: 1, o2m: [], m2o: 1 },
{ id: 4, display_name: "Fourth record", foo: "plop", bar: 2, o2m: [], m2o: 1 },
{ id: 5, display_name: "Fifth record", foo: "zoup", bar: 2, o2m: [], m2o: 1 },
],
},
pony: {
fields: {
id: { string: "Id", type: "integer" },
name: { string: "Name", type: "char" },
},
records: [
{ id: 4, name: "Twilight Sparkle" },
{ id: 6, name: "Applejack" },
{ id: 9, name: "Fluttershy" },
],
},
};
return {
models,
views: archs,
actions,
menus,
};
}