One of the best UX features available for users, admins, and developers is Odoo's Command Palette (triggered by using Ctrl+K). In the Odoo 19 framework, there is an API provided to users for customizing the command palette via registries.
In this blog post, let us understand the internal workings of the Odoo 19 Command Palette service. We will learn how to register global actions, namespace searching with @ or # triggers, making dynamic RPC calls based on user inputs, and rendering custom results using OWL.
1. Architectural Blueprint: How the Command Service Works
The Odoo command palette relies on the package @web/core/registry. Two different types of categories are used for categorizing the registry handlers in the command palette:
- command_setup: This is used for setting up the command namespaces. The namespace becomes active depending upon the key prefix in the keyboard (? or /).
- command_provider: This is used for declaring the provider which contains the function provide(env, options). Odoo calls this function when the user enters anything in the palette.
2. Step 1: Registering a Global Quick-Action
We will start by creating an action globally without having to use any namespace prefix. In this case, an action named “System: Reset Local Cache” which would reset the browser’s cache and reload the page.
Create a new file at the below-mentioned location for your custom module:
/** @odoo-module **/
import { registry } from "@web/core/registry";
import { _t } from "@web/core/l10n/translation";
registry.category("command_provider").add("clear_cache_provider", {
async provide(env, options) {
return [
{
name: _t("System: Reset Local Cache"),
category: "tools",
action: () => {
localStorage.clear();
sessionStorage.clear();
window.location.reload();
},
},
];
},
});
Each time the user uses the command palette, the suggestion shall be shown in the “Tools” menu, or the user can find it by searching “cache”.
3. Step 2: Creating a Custom Namespace (e.g., # to Search Open Tasks)
Sometimes, there is a requirement to filter items with a prefix dynamically. This can be done by creating a custom namespace “#” through which developers can search project tasks by name.
1. Register the Namespace Setup
We will be using the command_setup registry for setting up the prefix, placeholder, and the debounce timeout in order to not flood our database with RPCs.
// static/src/js/task_command_provider.js
/** @odoo-module **/
import { registry } from "@web/core/registry";
import { _t } from "@web/core/l10n/translation";
// 1. Setup the '#' namespace
registry.category("command_setup").add("#", {
name: _t("Search Tasks"),
placeholder: _t("Search tasks by name..."),
debounceDelay: 300, // wait 300ms after typing before triggering search
});
2. Implementation of Command Provider Using RPC Search
The next step is to create a provider in command_provider with the ‘#’ namespace. The searching will be done using the ORM service of Odoo.
// Register the provider associated with '#'
registry.category("command_provider").add("task_provider", {
namespace: "#",
async provide(env, options) {
// Whatever the user entered in after the '#' trigger is in the 'options.searchValue'
const searchVal = options.searchValue || "";
const orm = env.services.orm;
const actionService = env.services.action;
try {
// Retrieve up to 5 tasks that match the search criteria
const tasks = await orm.searchRead(
"project.task",
[["name", "ilike", searchVal]],
["id", "name", "project_id"]
);
// Convert records from the Database to commands for the command palette
return tasks.map(task => ({
name: `[${task.project_id[1]}] ${task.name}`,
category: "tasks",
action: () => {
// Opens the form view of task
actionService.doAction({
type: "ir.actions.act_window",
res_model: "project.task",
res_id: task.id,
views: [[false, "form"]],
target: "current",
});
},
}));
} catch (error) {
console.error("Failed to fetch tasks for command palette:", error);
return [];
}
},
});
4. Step 3: Creation of a Custom Rendering Component
Default Odoo provides commands in text form. However, if we want to display components such as icons for priority, colored, user images, and statuses, this could be done via a custom OWL Component.
1. The OWL Template (static/src/xml/task_command.xml):
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="my_module.TaskCommandItem" xml:space="preserve">
<div class="d-flex align-items-center justify-content-between w-100 px-3 py-2">
<div class="d-flex flex-column">
<span class="fw-bold text-dark"><t t-esc="props.name"/></span>
<span class="text-muted small"><t t-esc="props.project_name"/></span>
</div>
<span class="badge bg-primary text-white rounded-pill"><t t-esc="props.stage"/></span>
</div>
</t>
</templates>
2. The OWL Component Definition & Registration:
import { Component } from "@odoo/owl";
export class TaskCommandItem extends Component {
static template = "my_module.TaskCommandItem";
}
// Modify the 'provide' mapping from Step 2 to return the component and props:
return tasks.map(task => ({
Component: TaskCommandItem, // Our custom rendering component
props: {
name: task.name,
project_name: task.project_id[1],
stage: task.stage_id[1] || "New",
},
action: () => {
// navigation logic can be entered here...
},
}));3. Adding Assets to __manifest__.py
For your custom commands to function properly, their scripts must be added into the assets bundle on the backend side:
{
'name': 'Custom Command Palette',
'version': '19.0.1.0.0',
'depends': ['web', 'project'],
'assets': {
'web.assets_backend': [
'my_module/static/src/js/global_command_provider.js',
'my_module/static/src/js/task_command_provider.js',
'my_module/static/src/xml/task_command.xml',
],
},
}Command Provider Best Practices
- RPC Debouncing: It is recommended to include a debounce Delay in your namespace declaration to protect yourself against multiple hits on the database for every keystroke.
- Return Smaller Array of Results: Do not return a large array of results. Try to limit your number of results to a maximum of 5-10 results.
- Use Try/Catch in Error Handling: Since you are going to be working with the database, make sure all errors are caught using try/catch in order to avoid a typing lock in the command palette.
However, the Command Palette in Odoo 19 is not just another search dialog. It represents an interface that allows navigating through records, performing different actions, and optimizing day-to-day processes within the platform. Using command_setup and command_provider, it is possible to add custom commands, perform namespace searches, and fetch dynamic results from the database. Moreover, with the help of OWL components, the command palette allows displaying rich, interactive search results, which may be needed by a user before he opens a particular record.
Whatever you want to add from administrative functions to document searches to commonly performed tasks, the command palette is a convenient, yet highly extensible feature that does not require any changes in Odoo's core modules. Debouncing of RPC calls, a limited number of search results, and exception handling will ensure high performance and scalability of custom command providers. Therefore, the command palette becomes not only an additional function but also a productivity feature of Odoo.
To read more about Overview of OWL Component Structure In Odoo 19, refer to our blog Overview of OWL Component Structure In Odoo 19.