diff --git a/addons/web/static/src/core/commands/command_palette.js b/addons/web/static/src/core/commands/command_palette.js index 72218535caf..55d5ef7f0a8 100644 --- a/addons/web/static/src/core/commands/command_palette.js +++ b/addons/web/static/src/core/commands/command_palette.js @@ -81,7 +81,6 @@ export class CommandPalette extends Component { this.keepLast = new KeepLast(); this.DefaultCommandItem = DefaultCommandItem; this.activeElement = useService("ui").activeElement; - this.defaultDebounceSearch = debounce.apply(this, [this.search, 0]); this.inputRef = useAutofocus(); useHotkey("Enter", () => this.executeSelectedCommand(), { bypassEditableProtection: true }); @@ -98,6 +97,7 @@ export class CommandPalette extends Component { * @type {{ commands: CommandItem[], * emptyMessage: string, * FooterComponent: Component, + * namespace: string, * placeholder: string, * searchValue: string, * selectedCommand: CommandItem }} @@ -131,7 +131,6 @@ export class CommandPalette extends Component { */ async setCommandPaletteConfig(config) { this.configByNamespace = config.configByNamespace || {}; - this.debounceSearchByNamespace = {}; this.state.FooterComponent = config.FooterComponent; this.state.placeholder = config.placeholder || DEFAULT_PLACEHOLDER.toString(); @@ -144,8 +143,10 @@ export class CommandPalette extends Component { this.providersByNamespace[namespace] = [provider]; } } - this.namespaces = Object.keys(this.providersByNamespace); - await this.search(config.searchValue || ""); + + const { namespace, searchValue } = this.processSearchValue(config.searchValue || ""); + this.switchNamespace(namespace); + await this.search(searchValue); } /** @@ -194,7 +195,6 @@ export class CommandPalette extends Component { this.state.emptyMessage = ( namespaceConfig.emptyMessage || DEFAULT_EMPTY_MESSAGE ).toString(); - this.clearSearchValue = options.searchValue; } selectCommand(index) { @@ -260,10 +260,9 @@ export class CommandPalette extends Component { } } - async search(value) { - this.state.searchValue = value; - const { namespace, searchValue } = this.processSearchValue(value); - await this.setCommands(namespace, { + async search(searchValue) { + this.state.searchValue = searchValue; + await this.setCommands(this.state.namespace, { searchValue, activeElement: this.activeElement, }); @@ -273,19 +272,12 @@ export class CommandPalette extends Component { } debounceSearch(value) { - const { namespace } = this.processSearchValue(value); - const namespaceConfig = this.configByNamespace[namespace] || {}; - if (this.namespace !== namespace) { - if (this.lastDebounceSearch) { - this.lastDebounceSearch.cancel(); - } - this.lastDebounceSearch = debounce( - (value) => this.search(value), - namespaceConfig.debounceDelay || 0 - ); - this.namespace = namespace; + const { namespace, searchValue } = this.processSearchValue(value); + if (namespace !== "default" && this.state.namespace !== namespace) { + this.switchNamespace(namespace); } - this.searchValuePromise = this.lastDebounceSearch(value).catch(() => { + this.inputRef.el.value = searchValue; + this.searchValuePromise = this.lastDebounceSearch(searchValue).catch(() => { this.searchValuePromise = null; }); } @@ -294,19 +286,34 @@ export class CommandPalette extends Component { this.debounceSearch(ev.target.value); } + onKeyDown(ev) { + if (ev.key.toLowerCase() === "backspace" && !ev.target.value.length && !ev.repeat) { + this.switchNamespace("default"); + this.searchValuePromise = this.lastDebounceSearch("").catch(() => { + this.searchValuePromise = null; + }); + } + } + + switchNamespace(namespace) { + if (this.lastDebounceSearch) { + this.lastDebounceSearch.cancel(); + } + const namespaceConfig = this.configByNamespace[namespace] || {}; + this.lastDebounceSearch = debounce( + (value) => this.search(value), + namespaceConfig.debounceDelay || 0 + ); + this.state.namespace = namespace; + } + processSearchValue(searchValue) { let namespace = "default"; - if (searchValue.length && this.namespaces.includes(searchValue[0])) { + if (searchValue.length && this.providersByNamespace[searchValue[0]]) { namespace = searchValue[0]; searchValue = searchValue.slice(1); } return { namespace, searchValue }; } - - switchNamespace(namespace) { - const searchValue = `${namespace}${this.clearSearchValue}`; - this.state.searchValue = searchValue; - this.debounceSearch(searchValue); - } } CommandPalette.template = "web.CommandPalette"; diff --git a/addons/web/static/src/core/commands/command_palette.scss b/addons/web/static/src/core/commands/command_palette.scss index e9e5990b6a9..99cedb1b5d2 100644 --- a/addons/web/static/src/core/commands/command_palette.scss +++ b/addons/web/static/src/core/commands/command_palette.scss @@ -25,6 +25,12 @@ color:#C7C7C7; } + .o_namespace { + line-height: 1.8em; + min-width: 1em; + text-align: center; + } + input { border: none; padding: 0; diff --git a/addons/web/static/src/core/commands/command_palette.xml b/addons/web/static/src/core/commands/command_palette.xml index 5f7724af3a3..5effa89e8e2 100644 --- a/addons/web/static/src/core/commands/command_palette.xml +++ b/addons/web/static/src/core/commands/command_palette.xml @@ -4,7 +4,8 @@
@@ -16,7 +17,7 @@
- + @@ -37,7 +38,7 @@
diff --git a/addons/web/static/tests/core/commands/command_palette_tests.js b/addons/web/static/tests/core/commands/command_palette_tests.js index 8f3d0836744..c1cc76b596c 100644 --- a/addons/web/static/tests/core/commands/command_palette_tests.js +++ b/addons/web/static/tests/core/commands/command_palette_tests.js @@ -17,7 +17,7 @@ import { patchWithCleanup, triggerHotkey, } from "../../helpers/utils"; -import { editSearchBar } from "./command_service_tests"; +import { backspaceSearchBar, editSearchBar } from "./command_service_tests"; const { Component, xml } = owl; @@ -177,7 +177,7 @@ QUnit.test("custom debounce delay", async (assert) => { await editSearchBar("com"); await editSearchBar("@"); await editSearchBar("#"); - await editSearchBar(""); + await backspaceSearchBar(); assert.verifySteps(["0", "200", "100", "0"]); }); @@ -233,20 +233,27 @@ QUnit.test("concurrency with custom debounce delay", async (assert) => { await nextTick(); assert.containsOnce(target, ".o_command_palette"); assert.containsNone(target, ".o_command"); + assert.containsNone(target, ".o_command_palette .o_namespace"); + await editSearchBar("@"); await nextTick(); + assert.strictEqual(target.querySelector(".o_command_palette .o_namespace").innerText, "@"); assert.deepEqual( [...target.querySelectorAll(".o_command")].map((el) => el.textContent), [] ); + await editSearchBar("#"); await nextTick(); + assert.strictEqual(target.querySelector(".o_command_palette .o_namespace").innerText, "#"); assert.deepEqual( [...target.querySelectorAll(".o_command")].map((el) => el.textContent), ["Command#"] ); + def.resolve(); await nextTick(); + assert.strictEqual(target.querySelector(".o_command_palette .o_namespace").innerText, "#"); assert.deepEqual( [...target.querySelectorAll(".o_command")].map((el) => el.textContent), ["Command#"] @@ -374,6 +381,7 @@ QUnit.test("multi namespace with provider", async (assert) => { }); await nextTick(); assert.containsOnce(target, ".o_command_palette"); + assert.containsNone(target, ".o_command_palette .o_namespace"); assert.containsN(target, ".o_command", 2); assert.deepEqual( [...target.querySelectorAll(".o_command")].map((el) => el.textContent), @@ -381,6 +389,7 @@ QUnit.test("multi namespace with provider", async (assert) => { ); await editSearchBar("@"); + assert.strictEqual(target.querySelector(".o_command_palette .o_namespace").innerText, "@"); assert.containsN(target, ".o_command", 2); assert.deepEqual( [...target.querySelectorAll(".o_command")].map((el) => el.textContent), @@ -628,7 +637,7 @@ QUnit.test("open the command palette with a namespace already in the searchbar", }); await nextTick(); assert.containsOnce(target, ".o_command_palette"); - assert.strictEqual(target.querySelector(".o_command_palette_search input").value, "@"); + assert.strictEqual(target.querySelector(".o_command_palette .o_namespace").innerText, "@"); assert.containsN(target, ".o_command", 2); assert.deepEqual( [...target.querySelectorAll(".o_command")].map((el) => el.textContent), @@ -636,6 +645,101 @@ QUnit.test("open the command palette with a namespace already in the searchbar", ); }); +QUnit.test("open the command palette with a searchValue with a namespace", async (assert) => { + await mount(TestComponent, target, { env }); + const action = () => {}; + const providers = [ + { + provide: () => [ + { + name: "Command1", + action, + }, + { + name: "Command2", + action, + }, + ], + }, + { + namespace: "@", + provide: () => [ + { + name: "Command3", + action, + }, + { + name: "Command4", + action, + }, + ], + }, + ]; + const config = { + searchValue: "@Test", + providers, + }; + env.services.dialog.add(CommandPaletteDialog, { + config, + }); + await nextTick(); + assert.containsOnce(target, ".o_command_palette"); + assert.strictEqual(target.querySelector(".o_command_palette .o_namespace").innerText, "@"); + assert.strictEqual(target.querySelector(".o_command_palette_search input").value, "Test"); + assert.deepEqual( + [...target.querySelectorAll(".o_command")].map((el) => el.textContent), + ["Command3", "Command4"] + ); +}); + +QUnit.test("open the command palette with a searchValue without namespace", async (assert) => { + await mount(TestComponent, target, { env }); + const action = () => {}; + const providers = [ + { + provide: () => [ + { + name: "Command1", + action, + }, + { + name: "Command2", + action, + }, + ], + }, + { + namespace: "@", + provide: () => [ + { + name: "Command3", + action, + }, + { + name: "Command4", + action, + }, + ], + }, + ]; + const config = { + searchValue: "Command1", + providers, + }; + env.services.dialog.add(CommandPaletteDialog, { + config, + }); + await nextTick(); + assert.containsOnce(target, ".o_command_palette"); + assert.containsNone(target, ".o_command_palette .o_namespace"); + assert.strictEqual(target.querySelector(".o_command_palette_search input").value, "Command1"); + assert.containsN(target, ".o_command", 1); + assert.deepEqual( + [...target.querySelectorAll(".o_command")].map((el) => el.textContent), + ["Command1"] + ); +}); + QUnit.test("multi provider with categories", async (assert) => { await mount(TestComponent, target, { env }); const action = () => {}; @@ -1340,3 +1444,49 @@ QUnit.test("bold the searchValue on the commands with special char", async (asse ["&"] ); }); + +QUnit.test("remove namespace with backspace", async (assert) => { + await mount(TestComponent, target, { env }); + const provide = () => []; + const providers = [ + { + provide, + }, + { + namespace: "@", + provide, + }, + ]; + const config = { + providers, + }; + env.services.dialog.add(CommandPaletteDialog, { + config, + }); + await nextTick(); + await editSearchBar("@"); + assert.strictEqual(target.querySelector(".o_command_palette .o_namespace").innerText, "@"); + + // remove namespace "@" because the input is empty + await backspaceSearchBar(); + assert.containsNone(target, ".o_command_palette .o_namespace"); + assert.strictEqual(target.querySelector(".o_command_palette_search input").value, ""); + + await editSearchBar("@NotEmpty"); + assert.strictEqual(target.querySelector(".o_command_palette .o_namespace").innerText, "@"); + assert.strictEqual(target.querySelector(".o_command_palette_search input").value, "NotEmpty"); + + // Do not remove the namespace "@" because the input is not empty + await backspaceSearchBar(); + assert.strictEqual(target.querySelector(".o_command_palette .o_namespace").innerText, "@"); + + await editSearchBar("@"); + assert.strictEqual(target.querySelector(".o_command_palette .o_namespace").innerText, "@"); + + // Does not remove the namespace if the backspace is repeatedly applied. + // You don't want to remove the namespace by pressing the "backspace" key + const searchBar = document.querySelector(".o_command_palette_search input"); + searchBar.dispatchEvent(new KeyboardEvent("keydown", { key: "backspace", repeat: true })); + await nextTick(); + assert.strictEqual(target.querySelector(".o_command_palette .o_namespace").innerText, "@"); +}); diff --git a/addons/web/static/tests/core/commands/command_service_tests.js b/addons/web/static/tests/core/commands/command_service_tests.js index 8cebd1a1ce3..374779e59eb 100644 --- a/addons/web/static/tests/core/commands/command_service_tests.js +++ b/addons/web/static/tests/core/commands/command_service_tests.js @@ -32,7 +32,11 @@ const commandProviderRegistry = registry.category("command_provider"); export async function editSearchBar(searchValue) { const searchBar = document.querySelector(".o_command_palette_search input"); await testUtils.fields.editInput(searchBar, searchValue); - searchBar.dispatchEvent(new Event("keydown")); +} + +export async function backspaceSearchBar() { + const searchBar = document.querySelector(".o_command_palette_search input"); + searchBar.dispatchEvent(new KeyboardEvent("keydown", { key: "backspace" })); await nextTick(); } @@ -535,7 +539,11 @@ QUnit.test("namespaces display in the footer are still clickable", async (assert ); await click(target.querySelectorAll(".o_command_palette_footer .o_namespace")[0]); - assert.strictEqual(target.querySelector(".o_command_palette_search input").value, "@"); + assert.strictEqual( + target.querySelector(".o_command_palette_search .o_namespace").textContent, + "@" + ); + assert.strictEqual(target.querySelector(".o_command_palette_search input").value, ""); assert.deepEqual( [...target.querySelectorAll(".o_command")].map((el) => el.textContent), ["Command@"] @@ -543,14 +551,22 @@ QUnit.test("namespaces display in the footer are still clickable", async (assert await editSearchBar("Com"); await click(target.querySelectorAll(".o_command_palette_footer .o_namespace")[1]); - assert.strictEqual(target.querySelector(".o_command_palette_search input").value, "#Com"); + assert.strictEqual( + target.querySelector(".o_command_palette_search .o_namespace").textContent, + "#" + ); + assert.strictEqual(target.querySelector(".o_command_palette_search input").value, "Com"); assert.deepEqual( [...target.querySelectorAll(".o_command")].map((el) => el.textContent), ["Command#"] ); await click(target.querySelectorAll(".o_command_palette_footer .o_namespace")[0]); - assert.strictEqual(target.querySelector(".o_command_palette_search input").value, "@Com"); + assert.strictEqual( + target.querySelector(".o_command_palette_search .o_namespace").textContent, + "@" + ); + assert.strictEqual(target.querySelector(".o_command_palette_search input").value, "Com"); assert.deepEqual( [...target.querySelectorAll(".o_command")].map((el) => el.textContent), ["Command@"] @@ -653,8 +669,8 @@ QUnit.test("can switch between command providers", async (assert) => { "all other commands are present" ); - // Clear search input to recover the default provider - await editSearchBar(""); + // Press backspace to recover the default provider + await backspaceSearchBar(); assert.deepEqual( [...target.querySelectorAll(".o_command")].map((el) => el.textContent),