[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) <aab@odoo.com>
This commit is contained in:
FrancoisGe
2022-03-23 18:05:08 +01:00
parent ea0d4faa79
commit 2853e7d1cf
5 changed files with 220 additions and 40 deletions
@@ -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";
@@ -25,6 +25,12 @@
color:#C7C7C7;
}
.o_namespace {
line-height: 1.8em;
min-width: 1em;
text-align: center;
}
input {
border: none;
padding: 0;
@@ -4,7 +4,8 @@
<t t-name="web.CommandPalette" owl="1">
<div>
<div class="o_command_palette_search">
<input type="text" t-model="state.searchValue" t-on-input="onSearchInput" t-ref="autofocus" t-att-placeholder="state.placeholder"/>
<span t-if="state.namespace !== 'default'" class="o_namespace" t-esc="state.namespace"/>
<input type="text" t-att-value="state.searchValue" t-ref="autofocus" t-att-placeholder="state.placeholder" t-on-input="onSearchInput" t-on-keydown="onKeyDown"/>
<i t-att-title="state.placeholder" role="img" t-att-aria-label="state.placeholder" class="fa fa-search"></i>
</div>
@@ -16,7 +17,7 @@
<t t-foreach="category.commands" t-as="command" t-key="command.keyId">
<t t-set="commandIndex" t-value="state.commands.indexOf(command)" />
<div t-attf-id="o_command_{{commandIndex}}" class="o_command" t-att-class="{ focused: state.selectedCommand === command }" t-on-click="() => this.onCommandClicked(commandIndex)" t-on-mouseenter="() => this.onCommandMouseEnter(commandIndex)" t-on-close="() => this.props.closeMe()">
<t t-component="command.Component || DefaultCommandItem" name="command.name" searchValue="clearSearchValue" t-props="command.props" executeCommand="() => this.executeCommand(command)">
<t t-component="command.Component || DefaultCommandItem" name="command.name" searchValue="state.searchValue" t-props="command.props" executeCommand="() => this.executeCommand(command)">
<t t-set-slot="name">
<span class="o_command_name">
<t t-foreach="command.splitName" t-as="name" t-key="name_index">
@@ -37,7 +38,7 @@
</div>
<div t-if="state.FooterComponent" class="o_command_palette_footer">
<t t-component="state.FooterComponent" switchNamespace="(namespace) => this.switchNamespace(namespace)"/>
<t t-component="state.FooterComponent" switchNamespace="(namespace) => this.debounceSearch(namespace.concat(this.state.searchValue))"/>
</div>
</div>
@@ -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, "@");
});
@@ -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),