From 2853e7d1cf12e918727eae19b9f39695eb30f4b2 Mon Sep 17 00:00:00 2001 From: FrancoisGe Date: Fri, 18 Mar 2022 13:41:38 +0000 Subject: [PATCH] [FIX] web: CP separate namespace and search value The purpose of this commit is to allow the removal of the search value without removing the namespace. Use case before this commit: 1. I perform a search in the homeMenu; 2. The command palette opens with "/" + my search; 3. I realise that my search is wrong and I would like to delete its contents; 4. I press control+a to select the content of my search; 5. I press the backspace key; 6. The "/" and my search is deleted; 7. I perform a new search; 8. I realize that I forgot to add "/" at the beginning. This commit will remove the namespace from the input which will allow the user to delete the search without adding the namespace. If the user wants to remove the namespace, they will have to press the backspace key with an empty input. If the user wishes to change the namespace, they will have to start their search with that namespace. This will be removed from the input and replace the old namespace. closes odoo/odoo#86926 Task-id: 2768527 Signed-off-by: Aaron Bohy (aab) --- .../src/core/commands/command_palette.js | 63 +++---- .../src/core/commands/command_palette.scss | 6 + .../src/core/commands/command_palette.xml | 7 +- .../core/commands/command_palette_tests.js | 156 +++++++++++++++++- .../core/commands/command_service_tests.js | 28 +++- 5 files changed, 220 insertions(+), 40 deletions(-) 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),