[IMP] web: custom command palette
This commit adds the possibility to open the custom main command palette.
So it is possible to replace some parts of the default command palette
config.
For example, we will customize the placeholder:
```
this.command_service.openMainPalette({
placeholder: "My custom placeholder",
})
```
This commit also modify the api of CommandPaletteConfig, we replace
namespace by searchValue. Now it is possible to open a command palette
with a value in its input.
For example:
```
this.command_service.openPalette({
searchValue: "@test",
})
```
closes odoo/odoo#81814
Signed-off-by: Géry Debongnie <ged@odoo.com>
This commit is contained in:
@@ -9,7 +9,7 @@ import { debounce } from "@web/core/utils/timing";
|
||||
import { _lt } from "@web/core/l10n/translation";
|
||||
|
||||
const { Component, hooks } = owl;
|
||||
const { useState } = hooks;
|
||||
const { onWillStart, useState } = hooks;
|
||||
|
||||
const DEFAULT_PLACEHOLDER = _lt("Search...");
|
||||
const DEFAULT_EMPTY_MESSAGE = _lt("No results found");
|
||||
@@ -36,11 +36,11 @@ const FUZZY_NAMESPACES = ["default"];
|
||||
/**
|
||||
* @typedef {{
|
||||
* categoriesByNamespace?: {[namespace]: string[]};
|
||||
* namespace?: string;
|
||||
* emptyMessageByNamespace?: {[namespace]: string};
|
||||
* footerTemplate?: string;
|
||||
* placeholder?: string;
|
||||
* providers: Provider[];
|
||||
* searchValue?: string;
|
||||
* }} CommandPaletteConfig
|
||||
*/
|
||||
|
||||
@@ -69,13 +69,12 @@ export class CommandPalette extends Component {
|
||||
this.keepLast = new KeepLast();
|
||||
this.DefaultCommandItem = DefaultCommandItem;
|
||||
this.activeElement = useService("ui").activeElement;
|
||||
const onDebouncedSearchInput = debounce.apply(this, [this.onSearchInput, 200]);
|
||||
this.onDebouncedSearchInput = (...args) => {
|
||||
this.inputPromise = onDebouncedSearchInput.apply(this, args).catch(() => {
|
||||
this.inputPromise = null;
|
||||
const applyDebounceSearchValue = debounce.apply(this, [this.applySearchValue, 200]);
|
||||
this.applyDebounceSearchValue = (...args) => {
|
||||
this.searchValuePromise = applyDebounceSearchValue.apply(this, args).catch(() => {
|
||||
this.searchValuePromise = null;
|
||||
});
|
||||
};
|
||||
|
||||
useAutofocus();
|
||||
|
||||
useHotkey("Enter", () => this.executeSelectedCommand());
|
||||
@@ -94,7 +93,7 @@ export class CommandPalette extends Component {
|
||||
commands: [],
|
||||
});
|
||||
|
||||
this.setCommandPaletteConfig(this.props.config);
|
||||
onWillStart(() => this.setCommandPaletteConfig(this.props.config));
|
||||
}
|
||||
|
||||
get commandsByCategory() {
|
||||
@@ -117,7 +116,7 @@ export class CommandPalette extends Component {
|
||||
* Apply the new config to the command pallet
|
||||
* @param {CommandPaletteConfig} config
|
||||
*/
|
||||
setCommandPaletteConfig(config) {
|
||||
async setCommandPaletteConfig(config) {
|
||||
const result = { default: [] };
|
||||
for (const provider of config.providers) {
|
||||
const namespace = provider.namespace || "default";
|
||||
@@ -135,12 +134,8 @@ export class CommandPalette extends Component {
|
||||
|
||||
this.state.placeholder = config.placeholder || DEFAULT_PLACEHOLDER.toString();
|
||||
|
||||
const namespace = config.namespace || "default";
|
||||
this.setCommands(namespace, {
|
||||
activeElement: this.activeElement,
|
||||
searchValue: "",
|
||||
});
|
||||
this.state.searchValue = namespace === "default" ? "" : namespace;
|
||||
const searchValue = config.searchValue || "";
|
||||
await this.applySearchValue(searchValue);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -236,7 +231,7 @@ export class CommandPalette extends Component {
|
||||
}
|
||||
|
||||
async executeSelectedCommand() {
|
||||
await this.inputPromise;
|
||||
await this.searchValuePromise;
|
||||
if (this.state.selectedCommand) {
|
||||
this.executeCommand(this.state.selectedCommand);
|
||||
}
|
||||
@@ -250,9 +245,8 @@ export class CommandPalette extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
async onSearchInput(ev) {
|
||||
this.state.searchValue = ev.target.value;
|
||||
let searchValue = this.state.searchValue;
|
||||
async applySearchValue(searchValue) {
|
||||
this.state.searchValue = searchValue;
|
||||
let namespace = "default";
|
||||
if (searchValue.length && searchValue[0] in this.providersByNamespace) {
|
||||
namespace = searchValue[0];
|
||||
@@ -263,5 +257,9 @@ export class CommandPalette extends Component {
|
||||
activeElement: this.activeElement,
|
||||
});
|
||||
}
|
||||
|
||||
onSearchInput(ev) {
|
||||
this.applyDebounceSearchValue(ev.target.value);
|
||||
}
|
||||
}
|
||||
CommandPalette.template = "web.CommandPalette";
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
.o_command_palette {
|
||||
margin-top: 10vh;
|
||||
|
||||
&.modal-content {
|
||||
border: none;
|
||||
border-radius: 3px !important;
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<t t-name="web.CommandPalette" owl="1">
|
||||
<div>
|
||||
<div class="o_command_palette_search">
|
||||
<input type="text" t-model="state.searchValue" t-on-input="onDebouncedSearchInput" autofocus="" t-att-placeholder="state.placeholder"/>
|
||||
<input type="text" t-model="state.searchValue" t-on-input="onSearchInput" autofocus="" t-att-placeholder="state.placeholder"/>
|
||||
<i t-att-title="state.placeholder" role="img" t-att-aria-label="state.placeholder" class="fa fa-search"></i>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -54,7 +54,11 @@ export const commandService = {
|
||||
global: true,
|
||||
});
|
||||
|
||||
function openMainPalette() {
|
||||
/**
|
||||
* @param {CommandPaletteConfig} config command palette config merged with default config
|
||||
* @returns the actual command palette config if the command palette is already open
|
||||
*/
|
||||
function openMainPalette(config = {}) {
|
||||
const categoriesByNamespace = {};
|
||||
commandCategoryRegistry.getEntries().forEach(([category, el]) => {
|
||||
const namespace = el.namespace ? el.namespace : "default";
|
||||
@@ -69,14 +73,16 @@ export const commandService = {
|
||||
commandEmptyMessageRegistry.getEntries().forEach(([key, message]) => {
|
||||
emptyMessageByNamespace[key] = message.toString();
|
||||
});
|
||||
|
||||
const config = {
|
||||
categoriesByNamespace,
|
||||
emptyMessageByNamespace,
|
||||
footerTemplate,
|
||||
placeholder: env._t("Search for a command..."),
|
||||
providers: commandProviderRegistry.getAll(),
|
||||
};
|
||||
config = Object.assign(
|
||||
{
|
||||
categoriesByNamespace,
|
||||
emptyMessageByNamespace,
|
||||
footerTemplate,
|
||||
placeholder: env._t("Search for a command..."),
|
||||
providers: commandProviderRegistry.getAll(),
|
||||
},
|
||||
config
|
||||
);
|
||||
return openPalette(config);
|
||||
}
|
||||
|
||||
@@ -176,6 +182,7 @@ export const commandService = {
|
||||
(command) => command.activeElement === activeElement || command.global
|
||||
);
|
||||
},
|
||||
openMainPalette,
|
||||
openPalette,
|
||||
};
|
||||
},
|
||||
|
||||
@@ -416,6 +416,56 @@ QUnit.test("check the concurrency during a research", async (assert) => {
|
||||
assert.verifySteps(["b"]);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"open the command palette with a searchValue already in the searchbar",
|
||||
async (assert) => {
|
||||
testComponent = await mount(TestComponent, { env, target });
|
||||
const action = () => {};
|
||||
const providers = [
|
||||
{
|
||||
provide: () => [
|
||||
{
|
||||
name: "Command1",
|
||||
action,
|
||||
},
|
||||
{
|
||||
name: "Command2",
|
||||
action,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
namespace: "@",
|
||||
provide: () => [
|
||||
{
|
||||
name: "Command3",
|
||||
action,
|
||||
},
|
||||
{
|
||||
name: "Command4",
|
||||
action,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
const config = {
|
||||
searchValue: "C1",
|
||||
providers,
|
||||
};
|
||||
env.services.dialog.add(CommandPaletteDialog, {
|
||||
config,
|
||||
});
|
||||
await nextTick();
|
||||
assert.containsOnce(target, ".o_command_palette");
|
||||
assert.strictEqual(target.querySelector(".o_command_palette_search input").value, "C1");
|
||||
assert.containsN(target, ".o_command", 1);
|
||||
assert.deepEqual(
|
||||
[...target.querySelectorAll(".o_command")].map((el) => el.textContent),
|
||||
["Command1"]
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("open the command palette with a namespace already in the searchbar", async (assert) => {
|
||||
testComponent = await mount(TestComponent, { env, target });
|
||||
const action = () => {};
|
||||
@@ -447,7 +497,7 @@ QUnit.test("open the command palette with a namespace already in the searchbar",
|
||||
},
|
||||
];
|
||||
const config = {
|
||||
namespace: "@",
|
||||
searchValue: "@",
|
||||
providers,
|
||||
};
|
||||
env.services.dialog.add(CommandPaletteDialog, {
|
||||
|
||||
Reference in New Issue
Block a user