[IMP] web: highlight searchValue in CommandItem

With this commit, when you perform a search in the command palette,
the part of the commandItem's name that matches the searchValue will
be highlighted.

closes odoo/odoo#82407

Task-id: 2741837
Signed-off-by: Aaron Bohy (aab) <aab@odoo.com>
This commit is contained in:
FrancoisGe
2022-01-21 09:40:50 +00:00
parent f052624c97
commit 6daa9188cb
8 changed files with 152 additions and 33 deletions
@@ -8,7 +8,7 @@ const { Component, xml } = owl;
class DialogCommand extends Component {}
DialogCommand.template = xml`
<div class="o_command_default">
<span t-esc="props.name"/>
<t t-slot="name"/>
<span t-if="props.email" t-esc="props.email"/>
</div>
`;
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="web.DefaultCommandItem" owl="1">
<div class="o_command_default">
<t t-slot="name"/>
</div>
</t>
<t t-name="web.HotkeyCommandItem" owl="1">
<div class="o_command_hotkey">
<t t-slot="name"/>
<span>
<t t-foreach="getKeysToPress(props)" t-as="key">
<kbd t-esc="key" />
<span t-if="!key_last"> + </span>
</t>
</span>
</div>
</t>
</templates>
@@ -1,12 +1,13 @@
/** @odoo-module **/
import { useAutofocus, useService } from "@web/core/utils/hooks";
import { useHotkey } from "@web/core/hotkeys/hotkey_hook";
import { _lt } from "@web/core/l10n/translation";
import { KeepLast } from "@web/core/utils/concurrency";
import { useAutofocus, useService } from "@web/core/utils/hooks";
import { scrollTo } from "@web/core/utils/scrolling";
import { fuzzyLookup } from "@web/core/utils/search";
import { debounce } from "@web/core/utils/timing";
import { _lt } from "@web/core/l10n/translation";
import { escapeRegExp } from "../utils/strings";
const { Component, onWillStart, useRef, useState } = owl;
@@ -59,6 +60,11 @@ function commandsWithinCategory(categoryName, categories) {
};
}
function splitCommandName(name, searchValue) {
const splitName = name.split(new RegExp(`(${escapeRegExp(searchValue)})`, "ig"));
return searchValue.length && splitName.length > 1 ? splitName : [name];
}
export class DefaultCommandItem extends Component {}
DefaultCommandItem.template = "web.DefaultCommandItem";
@@ -175,11 +181,13 @@ export class CommandPalette extends Component {
this.state.commands = commands.map((command) => ({
...command,
keyId: this.keyId++,
splitName: splitCommandName(command.name, options.searchValue),
}));
this.selectCommand(this.state.commands.length ? 0 : -1);
this.mouseSelectionActive = false;
this.state.emptyMessage =
this.emptyMessageByNamespace[namespace] || DEFAULT_EMPTY_MESSAGE.toString();
this.clearSearchValue = options.searchValue;
}
selectCommand(index) {
@@ -91,6 +91,12 @@
}
}
.o_command_name {
> b {
color: $o-brand-primary;
}
}
.o_app_icon {
position: relative;
border-radius: 4%;
@@ -17,7 +17,16 @@
<t t-foreach="category.commands" t-as="command">
<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="onCommandClicked(commandIndex)" t-on-mouseenter="onCommandMouseEnter(commandIndex)" t-key="command.keyId">
<t t-component="command.Component || DefaultCommandItem" name="command.name" t-props="command.props" t-on-close="props.closeMe()" t-on-execute-command="executeCommand(command)"/>
<t t-component="command.Component || DefaultCommandItem" name="command.name" searchValue="clearSearchValue" t-props="command.props" t-on-close="props.closeMe()" t-on-execute-command="executeCommand(command)">
<t t-set-slot="name">
<span class="o_command_name">
<t t-foreach="command.splitName" t-as="name" t-key="name_index">
<b t-if="name_index % 2" t-esc="name"/>
<t t-else="" t-esc="name"/>
</t>
</span>
</t>
</t>
</div>
</t>
</div>
@@ -35,32 +44,4 @@
</div>
</t>
<t t-name="web.DefaultCommandItem" owl="1">
<div class="o_command_default">
<span t-esc="props.name" />
</div>
</t>
<t t-name="web.HotkeyCommandItem" owl="1">
<div class="o_command_hotkey">
<span t-esc="props.name" />
<span>
<t t-foreach="getKeysToPress(props)" t-as="key">
<kbd t-esc="key" />
<span t-if="!key_last"> + </span>
</t>
</span>
</div>
</t>
<t t-name="web.AppIconCommand" owl="1">
<div class="o_command_default">
<span t-esc="props.name"/>
<img t-if="props.webIconData" class="o_app_icon" t-attf-src="{{props.webIconData}}"/>
<div t-else="" class="o_app_icon" t-attf-style="background-color:{{props.webIcon.backgroundColor}}" >
<i t-att-class="props.webIcon.iconClass" t-attf-style="color:{{props.webIcon.color}}"></i>
</div>
</div>
</t>
</templates>
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="web.AppIconCommand" owl="1">
<div class="o_command_default">
<t t-slot="name"/>
<img t-if="props.webIconData" class="o_app_icon" t-attf-src="{{props.webIconData}}"/>
<div t-else="" class="o_app_icon" t-attf-style="background-color:{{props.webIcon.backgroundColor}}" >
<i t-att-class="props.webIcon.iconClass" t-attf-style="color:{{props.webIcon.color}}"></i>
</div>
</div>
</t>
</templates>
@@ -2,8 +2,8 @@
import { _lt } from "@web/core/l10n/translation";
import { registry } from "@web/core/registry";
import { computeAppsAndMenuItems } from "@web/webclient/menus/menu_helpers";
import { fuzzyLookup } from "@web/core/utils/search";
import { computeAppsAndMenuItems } from "@web/webclient/menus/menu_helpers";
const { Component } = owl;
@@ -1120,3 +1120,91 @@ QUnit.test("navigate in the command palette with an empty list", async (assert)
assert.containsNone(target, ".o_command");
assert.containsOnce(target, ".o_command_palette_listbox_empty");
});
QUnit.test("bold the searchValue on the commands", async (assert) => {
testComponent = await mount(TestComponent, { env, target });
const action = () => {};
const providers = [
{
namespace: "@",
provide: () => [
{
name: "Test",
action,
},
{
name: "test hello",
action,
},
{
name: "hello test",
action,
},
{
name: "hello Test hello",
action,
},
{
name: "TeSt hello Test hello TEST",
action,
},
],
},
];
const config = {
searchValue: "@",
providers,
};
env.services.dialog.add(CommandPaletteDialog, {
config,
});
await nextTick();
assert.containsOnce(target, ".o_command_palette");
assert.containsN(target, ".o_command", 5);
assert.deepEqual(
[...target.querySelectorAll(".o_command b")].map((el) => el.textContent),
[]
);
await editSearchBar("@test");
assert.containsN(target, ".o_command", 5);
assert.deepEqual(
[...target.querySelectorAll(".o_command")].map((command) => {
return [...command.querySelectorAll(".o_command_name b")].map((el) => el.textContent);
}),
[["Test"], ["test"], ["test"], ["Test"], ["TeSt", "Test", "TEST"]]
);
});
QUnit.test("bold the searchValue on the commands with special char", async (assert) => {
testComponent = await mount(TestComponent, { env, target });
const action = () => {};
const providers = [
{
provide: () => [
{
name: "Test&",
action,
},
],
},
];
const config = {
searchValue: "&",
providers,
};
env.services.dialog.add(CommandPaletteDialog, {
config,
});
await nextTick();
assert.containsOnce(target, ".o_command_palette");
assert.containsN(target, ".o_command", 1);
assert.deepEqual(
[...target.querySelectorAll(".o_command")].map((el) => el.textContent),
["Test&"]
);
assert.deepEqual(
[...target.querySelectorAll(".o_command b")].map((el) => el.textContent),
["&"]
);
});