[IMP] mail: ChannelSelector improvements

1. Display no result when having no matched results.
2. Hidden when clicking outside of the field.
3. Be visible when clicking inside the field.
4. Add related tests

task-3208033

closes odoo/odoo#116214

Signed-off-by: Alexandre Kühn (aku) <aku@odoo.com>
This commit is contained in:
zel-odoo
2023-03-30 15:09:20 +02:00
committed by Alexandre Kühn
parent 641766bd20
commit fa006f2bbe
10 changed files with 67 additions and 16 deletions
@@ -100,6 +100,7 @@ export class Composer extends Component {
},
});
this.suggestion = this.store.user ? useSuggestion() : undefined;
this.markEventHandled = markEventHandled;
if (this.props.dropzoneRef && this.allowUpload) {
useDropzone(
this.props.dropzoneRef,
@@ -41,6 +41,7 @@
t-on-input="onInput"
t-on-focusin="onFocusin"
t-on-focusout="() => this.props.composer.isFocused = false"
t-on-click="(ev) => markEventHandled(ev, 'composer.onClickTextarea')"
t-on-paste="onPaste"
t-model="props.composer.textInputContent"
t-att-placeholder="placeholder"
@@ -13,7 +13,8 @@ import {
useRef,
useState,
} from "@odoo/owl";
import { markEventHandled } from "../utils/misc";
import { markEventHandled, isEventHandled } from "../utils/misc";
import { onExternalClick } from "@mail/utils/hooks";
export class NavigableList extends Component {
static components = { ImStatus };
@@ -41,6 +42,17 @@ export class NavigableList extends Component {
this.hotkeysToRemove = [];
useExternalListener(window, "keydown", this.onKeydown, true);
onExternalClick("root", async (ev) => {
// Let event be handled by bubbling handlers first.
await new Promise(setTimeout);
if (
isEventHandled(ev, "composer.onClickTextarea") ||
isEventHandled(ev, "channelSelector.onClickInput")
) {
return;
}
this.close();
});
// position and size
usePosition(() => this.props.anchorRef, {
popper: "root",
@@ -72,6 +84,9 @@ export class NavigableList extends Component {
}
open() {
if (this.state.isLoading) {
return;
}
this.load().then(() => {
this.state.open = true;
this.navigate("first");
@@ -5,7 +5,7 @@
<div class="o-mail-NavigableList bg-white m-0 p-0" t-ref="root" t-att-class="props.class">
<div t-if="show" class="o-open border" t-on-mousedown.prevent="">
<div t-if="state.isLoading" class="o-mail-NavigableList-item">
<a href="#" class="">
<a href="#" class="d-flex align-items-center w-100 py-2 px-4">
<i class="fa fa-spin fa-circle-o-notch"/>
<t t-esc="props.placeholder"/>
</a>
@@ -7,7 +7,7 @@ import { useService } from "@web/core/utils/hooks";
import { getActiveHotkey } from "@web/core/hotkeys/hotkey_service";
import { Component, onMounted, useRef, useState } from "@odoo/owl";
import { cleanTerm } from "@mail/utils/format";
import { createLocalId, isEventHandled } from "@mail/utils/misc";
import { createLocalId, isEventHandled, markEventHandled } from "@mail/utils/misc";
import { _t } from "@web/core/l10n/translation";
export class ChannelSelector extends Component {
@@ -30,10 +30,11 @@ export class ChannelSelector extends Component {
if (this.props.autofocus) {
onMounted(() => this.inputRef.el.focus());
}
this.markEventHandled = markEventHandled;
}
async fetchSuggestions(term) {
const cleanedTerm = cleanTerm(term);
async fetchSuggestions() {
const cleanedTerm = cleanTerm(this.state.value);
if (cleanedTerm) {
if (this.props.category.id === "channels") {
const domain = [
@@ -79,6 +80,13 @@ export class ChannelSelector extends Component {
partner: this.store.self,
});
}
if (suggestions.length === 0) {
suggestions.push({
classList: "o-mail-ChannelSelector-suggestion",
label: _t("No results found"),
unselectable: true,
});
}
return suggestions;
}
}
@@ -184,7 +192,7 @@ export class ChannelSelector extends Component {
this.props.category.id === "channels"
? "mail.ChannelSelector.channel"
: "mail.ChannelSelector.chat",
options: this.fetchSuggestions(this.state.value),
options: this.fetchSuggestions(),
};
}
}
@@ -9,6 +9,7 @@
t-att-placeholder="inputPlaceholder"
t-model="state.value"
t-ref="input"
t-on-click="(ev) => markEventHandled(ev, 'channelSelector.onClickInput')"
t-on-keydown="onKeydownInput"
type="text"
/>
@@ -34,7 +34,7 @@
<ChannelSelector category="category" autofocus="true"/>
</div>
</t>
<button t-if="!env.inDiscussApp or (env.inDiscussApp and store.discuss.activeTab === 'chat' and !state.addingChat)" t-att-class="env.inDiscussApp ? 'w-100 p-2 btn btn-secondary border-bottom bg-light' : 'btn btn-link o-mail-MessagingMenu-new'" t-on-click.stop="onClickNewMessage">
<button t-if="!env.inDiscussApp or (env.inDiscussApp and store.discuss.activeTab === 'chat' and !state.addingChat)" t-att-class="env.inDiscussApp ? 'w-100 p-2 btn btn-secondary border-bottom bg-light' : 'btn btn-link'" t-on-click.stop="onClickNewMessage">
<t t-if="env.inDiscussApp">Start a conversation</t>
<t t-else="">New Message</t>
</button>
@@ -20,7 +20,7 @@ QUnit.module("chat window: new message");
QUnit.test("basic rendering", async (assert) => {
await start();
await click(".o_menu_systray i[aria-label='Messages']");
await click(".o-mail-MessagingMenu-new");
await click("button:contains(New Message)");
assert.containsOnce($, ".o-mail-ChatWindow");
assert.containsOnce($, ".o-mail-ChatWindow-header");
assert.containsOnce($, ".o-mail-ChatWindow-header .o-mail-ChatWindow-name");
@@ -40,7 +40,7 @@ QUnit.test("basic rendering", async (assert) => {
QUnit.test("focused on open [REQUIRE FOCUS]", async (assert) => {
await start();
await click(".o_menu_systray i[aria-label='Messages']");
await click(".o-mail-MessagingMenu-new");
await click("button:contains(New Message)");
assert.strictEqual(
document.activeElement,
$(".o-mail-ChatWindow .o-mail-ChannelSelector input")[0]
@@ -50,7 +50,7 @@ QUnit.test("focused on open [REQUIRE FOCUS]", async (assert) => {
QUnit.test("close", async (assert) => {
await start();
await click(".o_menu_systray i[aria-label='Messages']");
await click(".o-mail-MessagingMenu-new");
await click("button:contains(New Message)");
await click(".o-mail-ChatWindow-header .o-mail-ChatWindow-command[title='Close chat window']");
assert.containsNone($, ".o-mail-ChatWindow");
});
@@ -58,7 +58,7 @@ QUnit.test("close", async (assert) => {
QUnit.test("fold", async (assert) => {
await start();
await click(".o_menu_systray i[aria-label='Messages']");
await click(".o-mail-MessagingMenu-new");
await click("button:contains(New Message)");
assert.containsOnce($, ".o-mail-ChatWindow-content");
assert.containsOnce($, ".o-mail-ChannelSelector");
@@ -111,7 +111,7 @@ QUnit.test(
// open "new message" chat window
await click(".o_menu_systray i[aria-label='Messages']");
await click(".o-mail-MessagingMenu-new");
await click("button:contains(New Message)");
assert.containsOnce($, ".o-mail-ChatWindow-header:contains(New message)");
assert.containsN($, ".o-mail-ChatWindow", 2);
assert.containsOnce($, ".o-mail-ChatWindow .o-mail-ChannelSelector");
@@ -170,7 +170,7 @@ QUnit.test(
});
await start();
await click(".o_menu_systray i[aria-label='Messages']");
await click(".o-mail-MessagingMenu-new");
await click("button:contains(New Message)");
await insertText(".o-mail-ChannelSelector", "131");
await click(".o-mail-ChannelSelector-suggestion a");
assert.containsNone($, ".o-mail-ChatWindow-header:contains(New message)");
@@ -192,7 +192,7 @@ QUnit.test("new message autocomplete should automatically select first result",
});
// open "new message" chat window
await click(".o_menu_systray i[aria-label='Messages']");
await click(".o-mail-MessagingMenu-new");
await click("button:contains(New Message)");
// search for a user in "new message" autocomplete
await afterNextRender(async () => {
await insertText(".o-mail-ChannelSelector", "131");
@@ -223,6 +223,31 @@ QUnit.test("can create a group chat conversation", async (assert) => {
assert.containsNone($, ".o-mail-Message");
});
QUnit.test("chat search should display no result when no matches found", async (assert) => {
const { openDiscuss } = await start();
await openDiscuss();
await click(".o-mail-DiscussSidebar i[title='Start a conversation']");
await insertText(".o-mail-ChannelSelector", "Rainbow Panda");
assert.containsOnce($, ".o-mail-ChannelSelector-suggestion:contains(No results found)");
});
QUnit.test(
"chat search should not be visible when clicking outside of the field",
async (assert) => {
const pyEnv = await startServer();
const partnerId = pyEnv["res.partner"].create({ name: "Panda" });
pyEnv["res.users"].create({ partner_id: partnerId });
const { openDiscuss } = await start();
await openDiscuss();
assert.containsNone($, ".o-mail-DiscussCategoryItem");
await click(".o-mail-DiscussSidebar i[title='Start a conversation']");
await insertText(".o-mail-ChannelSelector", "Panda");
assert.containsOnce($, ".o-mail-ChannelSelector-suggestion");
await click(".o-mail-DiscussSidebar");
assert.containsNone($, ".o-mail-ChannelSelector-suggestion");
}
);
QUnit.test("Message following a notification should not be squashed", async (assert) => {
const pyEnv = await startServer();
const channelId = pyEnv["mail.channel"].create({
@@ -127,7 +127,7 @@ QUnit.test("respond to notification prompt (granted)", async (assert) => {
QUnit.test("Is closed after clicking on new message", async (assert) => {
await start();
await click(".o_menu_systray i[aria-label='Messages']");
await click(".o-mail-MessagingMenu-new");
await click("button:contains(New Message)");
assert.containsNone($, ".o-mail-MessagingMenu");
});
@@ -582,7 +582,7 @@ QUnit.test("basic rendering", async (assert) => {
assert.hasClass($('.o-mail-MessagingMenu button:contains("All")'), "fw-bolder");
assert.doesNotHaveClass($('.o-mail-MessagingMenu button:contains("Chats")'), "fw-bolder");
assert.doesNotHaveClass($('.o-mail-MessagingMenu button:contains("Channels")'), "fw-bolder");
assert.containsOnce($, ".o-mail-MessagingMenu-new");
assert.containsOnce($, "button:contains(New Message)");
assert.containsOnce($, '.o-mail-MessagingMenu:contains("No conversation yet...")');
await click(".o_menu_systray .dropdown-toggle:has(i[aria-label='Messages'])");
assert.doesNotHaveClass(