[IMP] web: menu providers command in command palette
Adds the possibility to search an app/menu from the command palette . It allows for fuzzy-searching apps and their submenus as shortcuts to directly enter the right app and submenu and execute the corresponding action. The comamnd is initiated by typing "/" in the command palette. closes odoo/odoo#75396 Task-id: 2622952 Related: odoo/enterprise#20447 Signed-off-by: Lucas Perais (lpe) <lpe@odoo.com>
This commit is contained in:
committed by
Lucas Perais (lpe)
parent
7ddcebfcda
commit
aa797792ca
@@ -88,6 +88,11 @@
|
||||
padding: 0 0.65rem;
|
||||
}
|
||||
}
|
||||
|
||||
.o_app_icon {
|
||||
border-radius: 4%;
|
||||
height: 1.8em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -53,4 +53,11 @@
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="web.AppIconCommand" owl="1">
|
||||
<div class="o_command_default">
|
||||
<span t-esc="props.name"/>
|
||||
<img class="o_app_icon" t-attf-src="{{props.webIconData}}"/>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { _lt } from "@web/core/l10n/translation";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { computeAppsAndMenuItems } from "@web/webclient/menu_service";
|
||||
import { fuzzyLookup } from "@web/core/utils/search";
|
||||
|
||||
const { Component } = owl;
|
||||
|
||||
class AppIconCommand extends Component {}
|
||||
AppIconCommand.template = "web.AppIconCommand";
|
||||
|
||||
const commandCategoryRegistry = registry.category("command_categories");
|
||||
commandCategoryRegistry.add("apps", { namespace: "/" }, { sequence: 10 });
|
||||
commandCategoryRegistry.add("menu_items", { namespace: "/" }, { sequence: 20 });
|
||||
|
||||
const commandEmptyMessageRegistry = registry.category("command_empty_list");
|
||||
commandEmptyMessageRegistry.add("/", _lt("No menu found"));
|
||||
|
||||
const commandProviderRegistry = registry.category("command_provider");
|
||||
commandProviderRegistry.add("menu", {
|
||||
namespace: "/",
|
||||
async provide(env, options) {
|
||||
const result = [];
|
||||
const menuService = env.services.menu;
|
||||
let { apps, menuItems } = computeAppsAndMenuItems(menuService.getMenuAsTree("root"));
|
||||
if (options.searchValue !== "") {
|
||||
apps = fuzzyLookup(options.searchValue, apps, (menu) => menu.label);
|
||||
|
||||
fuzzyLookup(options.searchValue, menuItems, (menu) =>
|
||||
(menu.parents + " / " + menu.label).split("/").reverse().join("/")
|
||||
).forEach((menu) => {
|
||||
result.push({
|
||||
action() {
|
||||
menuService.selectMenu(menu);
|
||||
},
|
||||
category: "menu_items",
|
||||
name: menu.parents + " / " + menu.label,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
apps.forEach((menu) => {
|
||||
const prefix = "data:image/png;base64,";
|
||||
const webIconData = menu.webIconData.startsWith(prefix)
|
||||
? menu.webIconData
|
||||
: prefix + menu.webIconData.replace(/\s/g, "");
|
||||
result.push({
|
||||
Component: AppIconCommand,
|
||||
action() {
|
||||
menuService.selectMenu(menu);
|
||||
},
|
||||
category: "apps",
|
||||
name: menu.label,
|
||||
props: {
|
||||
webIconData: webIconData,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
return result;
|
||||
},
|
||||
});
|
||||
@@ -6,6 +6,68 @@ import { session } from "@web/session";
|
||||
|
||||
const loadMenusUrl = `/web/webclient/load_menus`;
|
||||
|
||||
/**
|
||||
* Traverses the given menu tree, executes the given callback for each node with
|
||||
* the node itself and the list of its ancestors as arguments.
|
||||
*
|
||||
* @param {Object} tree tree of menus as exported by the menus service
|
||||
* @param {Function} cb
|
||||
* @param {[Object]} [parents] the ancestors of the tree root, if any
|
||||
*/
|
||||
function traverseMenuTree(tree, cb, parents = []) {
|
||||
cb(tree, parents);
|
||||
tree.childrenTree.forEach((c) => traverseMenuTree(c, cb, parents.concat([tree])));
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the "apps" and "menuItems" from a given menu tree.
|
||||
*
|
||||
* @param {Object} menuTree tree of menus as exported by the menus service
|
||||
* @returns {Object} with keys "apps" and "menuItems" (HomeMenu props)
|
||||
*/
|
||||
export function computeAppsAndMenuItems(menuTree) {
|
||||
const apps = [];
|
||||
const menuItems = [];
|
||||
traverseMenuTree(menuTree, (menuItem, parents) => {
|
||||
if (!menuItem.id || !menuItem.actionID) {
|
||||
return;
|
||||
}
|
||||
const isApp = menuItem.id === menuItem.appID;
|
||||
const item = {
|
||||
parents: parents
|
||||
.slice(1)
|
||||
.map((p) => p.name)
|
||||
.join(" / "),
|
||||
label: menuItem.name,
|
||||
id: menuItem.id,
|
||||
xmlid: menuItem.xmlid,
|
||||
actionID: menuItem.actionID,
|
||||
appID: menuItem.appID,
|
||||
};
|
||||
if (isApp) {
|
||||
if (menuItem.webIconData) {
|
||||
item.webIconData = menuItem.webIconData;
|
||||
} else {
|
||||
const [iconClass, color, backgroundColor] = (menuItem.webIcon || "").split(",");
|
||||
if (backgroundColor !== undefined) {
|
||||
// Could split in three parts?
|
||||
item.webIcon = { iconClass, color, backgroundColor };
|
||||
} else {
|
||||
item.webIconData = "/web_enterprise/static/img/default_icon_app.png";
|
||||
}
|
||||
}
|
||||
} else {
|
||||
item.menuID = parents[1].id;
|
||||
}
|
||||
if (isApp) {
|
||||
apps.push(item);
|
||||
} else {
|
||||
menuItems.push(item);
|
||||
}
|
||||
});
|
||||
return { apps, menuItems };
|
||||
}
|
||||
|
||||
function makeFetchLoadMenus() {
|
||||
const cacheHashes = session.cache_hashes;
|
||||
let loadMenusHash = cacheHashes.load_menus || new Date().getTime().toString();
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { createWebClient, getActionManagerServerData } from "@web/../tests/webclient/helpers";
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { click, nextTick, patchWithCleanup, triggerHotkey } from "../../helpers/utils";
|
||||
import { editSearchBar } from "./command_service_tests";
|
||||
|
||||
let serverData;
|
||||
QUnit.module("Menu Command Provider", {
|
||||
async beforeEach() {
|
||||
patchWithCleanup(browser, {
|
||||
clearTimeout: () => {},
|
||||
setTimeout: (later, wait) => {
|
||||
later();
|
||||
},
|
||||
});
|
||||
const commandCategoryRegistry = registry.category("command_categories");
|
||||
commandCategoryRegistry.add("apps", { namespace: "/" }, { sequence: 10 });
|
||||
commandCategoryRegistry.add("menu_items", { namespace: "/" }, { sequence: 20 });
|
||||
serverData = getActionManagerServerData();
|
||||
serverData.menus = {
|
||||
root: { id: "root", children: [0, 1, 2], name: "root", appID: "root" },
|
||||
0: { id: 0, children: [], name: "UglyHack", appID: 0, xmlid: "menu_0" },
|
||||
1: { id: 1, children: [], name: "Contact", appID: 1, actionID: 1001, xmlid: "menu_1" },
|
||||
2: {
|
||||
id: 2,
|
||||
children: [3, 4],
|
||||
name: "Sales",
|
||||
appID: 2,
|
||||
actionID: 1002,
|
||||
xmlid: "menu_2",
|
||||
},
|
||||
3: {
|
||||
id: 3,
|
||||
children: [],
|
||||
name: "Info",
|
||||
appID: 2,
|
||||
actionID: 1003,
|
||||
xmlid: "menu_3",
|
||||
},
|
||||
4: {
|
||||
id: 4,
|
||||
children: [],
|
||||
name: "Report",
|
||||
appID: 2,
|
||||
actionID: 1004,
|
||||
xmlid: "menu_4",
|
||||
},
|
||||
};
|
||||
serverData.actions[1003] = {
|
||||
id: 1003,
|
||||
tag: "__test__client__action__",
|
||||
target: "main",
|
||||
type: "ir.actions.client",
|
||||
params: { description: "Info" },
|
||||
};
|
||||
serverData.actions[1004] = {
|
||||
id: 1004,
|
||||
tag: "__test__client__action__",
|
||||
target: "main",
|
||||
type: "ir.actions.client",
|
||||
params: { description: "Report" },
|
||||
};
|
||||
},
|
||||
afterEach() {},
|
||||
});
|
||||
|
||||
QUnit.test("displays only apps if the search value is '/'", async (assert) => {
|
||||
const webClient = await createWebClient({ serverData });
|
||||
assert.containsNone(webClient, ".o_menu_brand");
|
||||
|
||||
triggerHotkey("control+k");
|
||||
await nextTick();
|
||||
await editSearchBar("/");
|
||||
assert.containsOnce(webClient, ".o_command_palette");
|
||||
assert.containsOnce(webClient, ".o_command_category");
|
||||
assert.containsN(webClient, ".o_command", 2);
|
||||
assert.deepEqual(
|
||||
[...webClient.el.querySelectorAll(".o_command")].map((el) => el.textContent),
|
||||
["Contact", "Sales"]
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("displays apps and menu items if the search value is not only '/'", async (assert) => {
|
||||
const webClient = await createWebClient({ serverData });
|
||||
|
||||
triggerHotkey("control+k");
|
||||
await nextTick();
|
||||
await editSearchBar("/sal");
|
||||
assert.containsOnce(webClient, ".o_command_palette");
|
||||
assert.containsN(webClient, ".o_command", 3);
|
||||
assert.deepEqual(
|
||||
[...webClient.el.querySelectorAll(".o_command")].map((el) => el.textContent),
|
||||
["Sales", "Sales / Info", "Sales / Report"]
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("opens an app", async (assert) => {
|
||||
const webClient = await createWebClient({ serverData });
|
||||
assert.containsNone(webClient, ".o_menu_brand");
|
||||
|
||||
triggerHotkey("control+k");
|
||||
await nextTick();
|
||||
await editSearchBar("/");
|
||||
assert.containsOnce(webClient, ".o_command_palette");
|
||||
|
||||
triggerHotkey("enter");
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
assert.strictEqual(webClient.el.querySelector(".o_menu_brand").textContent, "Contact");
|
||||
assert.strictEqual(
|
||||
webClient.el.querySelector(".test_client_action").textContent,
|
||||
" ClientAction_Id 1"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("opens a menu items", async (assert) => {
|
||||
const webClient = await createWebClient({ serverData });
|
||||
assert.containsNone(webClient, ".o_menu_brand");
|
||||
|
||||
triggerHotkey("control+k");
|
||||
await nextTick();
|
||||
await editSearchBar("/sal");
|
||||
assert.containsOnce(webClient, ".o_command_palette");
|
||||
assert.containsN(webClient, ".o_command_category", 2);
|
||||
|
||||
click(webClient.el, "#o_command_2");
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
assert.strictEqual(webClient.el.querySelector(".o_menu_brand").textContent, "Sales");
|
||||
assert.strictEqual(
|
||||
webClient.el.querySelector(".test_client_action").textContent,
|
||||
" ClientAction_Report"
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user