[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:
FrancoisGe
2022-01-10 13:52:34 +00:00
parent acab8ea899
commit 52a1d997d2
5 changed files with 87 additions and 30 deletions
@@ -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, {