[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:
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user