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>
411 lines
14 KiB
JavaScript
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,
|
|
};
|
|
}
|