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