[REF] mail: NavigableList Ref

closes odoo/odoo#116037

Signed-off-by: Alexandre Kühn (aku) <aku@odoo.com>
This commit is contained in:
Zelong Lin
2023-03-22 01:45:24 +01:00
committed by Alexandre Kühn
parent 73a0817688
commit 3a5eb4567f
12 changed files with 247 additions and 344 deletions
+63 -61
View File
@@ -217,77 +217,79 @@ export class Composer extends Component {
}
get hasSuggestions() {
return this.suggestion?.state.items.length > 0;
return Boolean(this.suggestion?.state.items);
}
get navigableListProps() {
return {
const props = {
anchorRef: this.ref.el,
position: "top",
placeholder: _t("Loading"),
onSelect: (ev, option) => {
this.suggestion.insert(option);
markEventHandled(ev, "composer.selectSuggestion");
},
sources: this.suggestion.state.items.map((mainOrExtraSuggestions) => {
switch (mainOrExtraSuggestions.type) {
case "Partner":
return {
placeholder: "Loading",
optionTemplate: "mail.Composer.suggestionPartner",
options: mainOrExtraSuggestions.suggestions.map((suggestion) => {
return {
label: suggestion.name,
partner: suggestion,
classList:
"o-composer-suggestion o-composer-suggestion-partner",
};
}),
};
case "Thread":
return {
placeholder: "Loading",
optionTemplate: "mail.Composer.suggestionThread",
options: mainOrExtraSuggestions.suggestions.map((suggestion) => {
return {
label: suggestion.displayName,
thread: suggestion,
classList: "o-composer-suggestion o-composer-suggestion-thread",
};
}),
};
case "ChannelCommand":
return {
placeholder: "Loading",
optionTemplate: "mail.Composer.suggestionChannelCommand",
options: mainOrExtraSuggestions.suggestions.map((suggestion) => {
return {
label: suggestion.name,
help: suggestion.help,
classList:
"o-composer-suggestion o-composer-suggestion-channel-command",
};
}),
};
case "CannedResponse":
return {
placeholder: "Loading",
optionTemplate: "mail.Composer.suggestionCannedResponse",
options: mainOrExtraSuggestions.suggestions.map((suggestion) => {
return {
name: suggestion.name,
label: suggestion.substitution,
classList:
"o-composer-suggestion o-composer-suggestion-canned-response",
};
}),
};
default:
return {
options: [],
};
}
}),
options: [],
};
if (!this.hasSuggestions) {
return props;
}
const suggestions = Array(
...this.suggestion.state.items.mainSuggestions,
...this.suggestion.state.items.extraSuggestions
);
switch (this.suggestion.state.items.type) {
case "Partner":
return {
...props,
optionTemplate: "mail.Composer.suggestionPartner",
options: suggestions.map((suggestion) => {
return {
label: suggestion.name,
partner: suggestion,
classList: "o-mail-Composer-suggestion",
};
}),
};
case "Thread":
return {
...props,
optionTemplate: "mail.Composer.suggestionThread",
options: suggestions.map((suggestion) => {
return {
label: suggestion.displayName,
thread: suggestion,
classList: "o-mail-Composer-suggestion",
};
}),
};
case "ChannelCommand":
return {
...props,
optionTemplate: "mail.Composer.suggestionChannelCommand",
options: suggestions.map((suggestion) => {
return {
label: suggestion.name,
help: suggestion.help,
classList: "o-mail-Composer-suggestion",
};
}),
};
case "CannedResponse":
return {
...props,
optionTemplate: "mail.Composer.suggestionCannedResponse",
options: suggestions.map((suggestion) => {
return {
name: suggestion.name,
label: suggestion.substitution,
classList: "o-mail-Composer-suggestion",
};
}),
};
default:
return props;
}
}
/**
+3 -3
View File
@@ -10,7 +10,7 @@
t-att-class="{
'px-3 py-4 o-extended': extended,
'p-3': normal,
'o-hasSelfAvatar px-3': !this.env.inChatWindow and thread,
'o-hasSelfAvatar px-3': !env.inChatWindow and thread,
}">
<div class="o-mail-Composer-sidebarMain flex-shrink-0" t-if="!compact">
<img class="o-mail-Composer-avatar mt-1 rounded-circle" t-att-src="threadService.avatarUrl(store.self, props.composer.thread)" alt="Avatar of user"/>
@@ -82,7 +82,7 @@
t-att-disabled="isSendButtonDisabled"
aria-label="Send"
>
<t t-if="this.store.isSmall"><i class="fa fa-paper-plane-o"/></t>
<t t-if="store.isSmall"><i class="fa fa-paper-plane-o"/></t>
<t t-else="">Send</t>
</button>
</div>
@@ -99,7 +99,7 @@
</div>
<span t-if="props.composer.message" class="text-muted">escape to <a href="#" t-on-click="props.onDiscardCallback">cancel</a>, enter to <a href="#" t-on-click="editMessage">save</a></span>
</div>
<NavigableList t-if="this.suggestion" class="'o-composer-suggestion-list'" t-props="navigableListProps"/>
<NavigableList t-if="suggestion" class="'o-mail-Composer-suggestionList'" t-props="navigableListProps"/>
</t>
<t t-name="mail.Composer.suggestionPartner" owl="1">
+72 -148
View File
@@ -20,33 +20,22 @@ export class NavigableList extends Component {
static template = "mail.NavigableList";
static props = {
anchorRef: {},
position: { type: String, optional: true },
onSelect: { type: Function },
sources: {
type: Array,
element: {
type: Object,
shape: {
placeholder: { type: String, optional: true },
optionTemplate: { type: String, optional: true },
options: [Array, Promise],
},
},
},
class: { type: String, optional: true },
onSelect: { type: Function },
options: { type: [Array, Promise] },
optionTemplate: { type: String, optional: true },
placeholder: { type: String, optional: true },
position: { type: String, optional: true },
};
static defaultProps = { position: "bottom" };
setup() {
this.nextOptionId = 0;
this.nextSourceId = 0;
this.rootRef = useRef("root");
this.sources = [];
this.state = useState({
activeSourceOption: null,
navigationRev: 0,
activeOption: null,
isLoading: false,
open: false,
optionsRev: 0,
options: [],
});
this.hotkey = useService("hotkey");
this.hotkeysToRemove = [];
@@ -78,88 +67,49 @@ export class NavigableList extends Component {
});
}
get isOpened() {
return this.state.open;
}
get hasOptions() {
for (const source of this.sources) {
if (source.isLoading || source.options.length) {
return true;
}
}
return false;
get show() {
return Boolean(this.state.open && (this.state.isLoading || this.state.options.length));
}
open() {
this.loadSources();
this.load().then(() => {
this.state.open = true;
this.navigate("first");
});
}
close() {
this.state.open = false;
this.state.activeSourceOption = null;
this.state.activeOption = null;
}
loadSources() {
this.sources = [];
const proms = [];
for (const pSource of this.props.sources) {
if (pSource.options.length === 0) {
continue;
}
const source = this.makeSource(pSource);
this.sources.push(source);
const options = pSource.options;
if (options instanceof Promise) {
source.isLoading = true;
const prom = options.then((options) => {
source.options = options.map((option) => this.makeOption(option));
source.isLoading = false;
if (source.options.length === 0) {
this.sources = this.sources.filter((s) => s !== source);
}
this.state.optionsRev++;
});
proms.push(prom);
} else {
source.options = options.map((option) => this.makeOption(option));
}
}
Promise.all(proms).then(() => {
this.navigate(0);
});
if (this.sources.length === 0) {
return;
}
this.state.open = true;
}
makeOption(option) {
return Object.assign(Object.create(option), {
id: ++this.nextOptionId,
});
}
makeSource(source) {
return {
id: ++this.nextSourceId,
options: [],
isLoading: false,
placeholder: source.placeholder,
optionTemplate: source.optionTemplate,
async load() {
this.state.options = [];
const makeOption = (opt) => {
return Object.assign(Object.create(opt), {
id: this.state.options.length,
});
};
if (this.props.options instanceof Promise) {
this.state.isLoading = true;
return this.props.options.then((opts) => {
opts.forEach((opt) => this.state.options.push(makeOption(opt)));
this.state.isLoading = false;
});
}
if (this.props.options instanceof Array) {
if (this.props.options.length === 0) {
return;
}
this.props.options.forEach((opt) => this.state.options.push(makeOption(opt)));
}
}
isActiveSourceOption([sourceIndex, optionIndex]) {
return (
this.state.activeSourceOption &&
this.state.activeSourceOption[0] === sourceIndex &&
this.state.activeSourceOption[1] === optionIndex
);
isActiveOption(option) {
return this.state.activeOption?.id === option.id;
}
selectOption(ev, indices, params = {}) {
const option = this.sources[indices[0]].options[indices[1]];
selectOption(ev, option, params = {}) {
if (option.unselectable) {
this.close();
return;
@@ -171,85 +121,67 @@ export class NavigableList extends Component {
}
navigate(direction) {
let step = Math.sign(direction);
if (!step) {
this.state.activeSourceOption = null;
step = 1;
} else {
this.state.navigationRev++;
}
if (this.state.activeSourceOption) {
let [sourceIndex, optionIndex] = this.state.activeSourceOption;
let source = this.sources[sourceIndex];
optionIndex += step;
if (0 > optionIndex || optionIndex >= source.options.length) {
sourceIndex += step;
source = this.sources[sourceIndex];
while (source && source.isLoading) {
sourceIndex += step;
source = this.sources[sourceIndex];
const activeOptionId = this.state.activeOption ? this.state.activeOption.id : -1;
let targetId = undefined;
switch (direction) {
case "first":
targetId = 0;
break;
case "last":
targetId = this.state.options.length - 1;
break;
case "previous":
targetId = activeOptionId - 1;
if (targetId < 0) {
this.navigate("last");
return;
}
if (source) {
optionIndex = step < 0 ? source.options.length - 1 : 0;
break;
case "next":
targetId = activeOptionId + 1;
if (targetId > this.state.options.length - 1) {
this.navigate("first");
return;
}
}
if (source) {
this.state.activeSourceOption = [sourceIndex, optionIndex];
} else {
this.state.activeSourceOption =
direction > 0
? [0, 0]
: [
this.sources.length - 1,
this.sources[this.sources.length - 1].options.length - 1,
];
}
} else {
let sourceIndex = step < 0 ? this.sources.length - 1 : 0;
let source = this.sources[sourceIndex];
while (source && source.isLoading) {
sourceIndex += step;
source = this.sources[sourceIndex];
}
if (source) {
const optionIndex = step < 0 ? source.options.length - 1 : 0;
this.state.activeSourceOption = [sourceIndex, optionIndex];
}
break;
default:
return;
}
this.state.activeOption = this.state.options.find((o) => o.id === targetId);
}
onKeydown(ev) {
const hotkey = getActiveHotkey(ev);
switch (hotkey) {
case "enter":
if (!this.isOpened || !this.state.activeSourceOption) {
if (!this.show || !this.state.activeOption) {
return;
}
markEventHandled(ev, "NavigableList.select");
this.selectOption(ev, this.state.activeSourceOption);
this.selectOption(ev, this.state.activeOption);
break;
case "escape":
if (!this.isOpened) {
if (!this.show) {
return;
}
markEventHandled(ev, "NavigableList.close");
this.close();
break;
case "tab":
this.navigate(+1);
if (!this.isOpened) {
this.navigate("next");
if (!this.show) {
this.open();
}
break;
case "arrowup":
this.navigate(-1);
if (!this.isOpened) {
this.navigate("previous");
if (!this.show) {
this.open();
}
break;
case "arrowdown":
this.navigate(+1);
if (!this.isOpened) {
this.navigate("next");
if (!this.show) {
this.open();
}
break;
@@ -259,15 +191,7 @@ export class NavigableList extends Component {
ev.preventDefault();
}
onOptionMouseEnter(indices) {
this.state.activeSourceOption = indices;
}
onOptionMouseLeave() {
this.state.activeSourceOption = null;
}
onOptionClick(ev, indices) {
this.selectOption(ev, indices);
onOptionMouseEnter(option) {
this.state.activeOption = option;
}
}
@@ -3,29 +3,26 @@
<t t-name="mail.NavigableList" owl="1">
<div class="o-mail-NavigableList bg-white m-0 p-0" t-ref="root" t-att-class="props.class">
<div t-if="isOpened and hasOptions" class="o-open border" t-on-mousedown.prevent="">
<t t-foreach="sources" t-as="source" t-key="source.id">
<div t-if="source.isLoading" class="o-mail-NavigableList-item">
<a href="#" class="">
<i class="fa fa-spin fa-circle-o-notch"/>
<t t-esc="source.placeholder"/>
<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="">
<i class="fa fa-spin fa-circle-o-notch"/>
<t t-esc="props.placeholder"/>
</a>
</div>
<t t-else="">
<div
t-foreach="state.options" t-as="option" t-key="option.id"
class="o-mail-NavigableList-item"
t-att-class="option.classList"
t-on-mouseenter="() => this.onOptionMouseEnter(option)"
t-on-click="(ev) => this.selectOption(ev, option)"
>
<a href="#" class="d-flex align-items-center w-100 py-2 px-4" t-att-class="{ 'o-mail-NavigableList-active bg-300': isActiveOption(option) }">
<t t-if="props.optionTemplate" t-call="{{ props.optionTemplate }}"/>
<t t-else="" t-esc="option.label"/>
</a>
</div>
<t t-else="">
<div
t-foreach="source.options" t-as="option" t-key="option.id"
class="o-mail-NavigableList-item"
t-att-class="option.classList"
t-on-mouseenter="() => this.onOptionMouseEnter([source_index, option_index])"
t-on-mouseleave="() => this.onOptionMouseLeave([source_index, option_index])"
t-on-click="(ev) => this.onOptionClick(ev, [source_index, option_index])"
>
<a href="#" class="d-flex align-items-center w-100 py-2 px-4" t-att-class="{ 'o-mail-NavigableList-active bg-300': isActiveSourceOption([source_index, option_index]) }">
<t t-if="source.optionTemplate" t-call="{{ source.optionTemplate }}"/>
<t t-else="" t-esc="option.label"/>
</a>
</div>
</t>
</t>
</div>
</div>
@@ -17,7 +17,7 @@ export function useSuggestion() {
position: undefined,
term: undefined,
});
self.state.items.length = 0;
self.state.items = undefined;
},
detect() {
const selectionEnd = comp.props.composer.selection.end;
@@ -113,7 +113,7 @@ export function useSuggestion() {
},
state: useState({
count: 0,
items: [],
items: undefined,
}),
update() {
if (!self.search.delimiter || !comp.props.composer.thread) {
@@ -127,16 +127,16 @@ export function useSuggestion() {
if (!suggestions) {
return;
}
const [main, extra = { suggestions: [] }] = suggestions;
const { type, mainSuggestions, extraSuggestions = [] } = suggestions;
// arbitrary limit to avoid displaying too many elements at once
// ideally a load more mechanism should be introduced
const limit = 8;
main.suggestions.length = Math.min(main.suggestions.length, limit);
extra.suggestions.length = Math.min(
extra.suggestions.length,
limit - main.suggestions.length
mainSuggestions.length = Math.min(mainSuggestions.length, limit);
extraSuggestions.length = Math.min(
extraSuggestions.length,
limit - mainSuggestions.length
);
self.state.items = [main, extra];
self.state.items = { type, mainSuggestions, extraSuggestions };
},
};
useEffect(
@@ -99,16 +99,11 @@ class SuggestionService {
case "/":
return this.searchChannelCommand(cleanedSearchTerm, thread, sort);
}
return [
{
type: undefined,
suggestions: [],
},
{
type: undefined,
suggestions: [],
},
];
return {
type: undefined,
mainSuggestions: [],
extraSuggestions: [],
};
}
searchChannelCommand(cleanedSearchTerm, thread, sort) {
@@ -164,12 +159,10 @@ class SuggestionService {
}
return c1.id - c2.id;
};
return [
{
type: "ChannelCommand",
suggestions: sort ? commands.sort(sortFunc) : commands,
},
];
return {
type: "ChannelCommand",
mainSuggestions: sort ? commands.sort(sortFunc) : commands,
};
}
searchPartnerSuggestions(cleanedSearchTerm, thread, sort) {
@@ -285,16 +278,11 @@ class SuggestionService {
}
return p1.id - p2.id;
};
return [
{
type: "Partner",
suggestions: sort ? mainSuggestionList.sort(sortFunc) : mainSuggestionList,
},
{
type: "Partner",
suggestions: sort ? extraSuggestionList.sort(sortFunc) : extraSuggestionList,
},
];
return {
type: "Partner",
mainSuggestions: sort ? mainSuggestionList.sort(sortFunc) : mainSuggestionList,
extraSuggestions: sort ? extraSuggestionList.sort(sortFunc) : extraSuggestionList,
};
}
searchCannedResponseSuggestions(cleanedSearchTerm, sort) {
@@ -332,12 +320,10 @@ class SuggestionService {
}
return c1.id - c2.id;
};
return [
{
type: "CannedResponse",
suggestions: sort ? cannedResponses.sort(sortFunc) : cannedResponses,
},
];
return {
type: "CannedResponse",
mainSuggestions: sort ? cannedResponses.sort(sortFunc) : cannedResponses,
};
}
searchChannelSuggestions(cleanedSearchTerm, thread, sort) {
@@ -400,12 +386,10 @@ class SuggestionService {
}
return c1.id - c2.id;
};
return [
{
type: "Thread",
suggestions: sort ? suggestionList.sort(sortFunc) : suggestionList,
},
];
return {
type: "Thread",
mainSuggestions: sort ? suggestionList.sort(sortFunc) : suggestionList,
};
}
}
@@ -179,16 +179,12 @@ export class ChannelSelector extends Component {
anchorRef: this.rootRef.el,
position: "bottom",
onSelect: (ev, option) => this.onSelect(option),
sources: [
{
placeholder: "Loading",
optionTemplate:
this.props.category.id === "channels"
? "mail.ChannelSelector.channel"
: "mail.ChannelSelector.chat",
options: this.fetchSuggestions(this.state.value),
},
],
placeholder: _t("Loading"),
optionTemplate:
this.props.category.id === "channels"
? "mail.ChannelSelector.channel"
: "mail.ChannelSelector.chat",
options: this.fetchSuggestions(this.state.value),
};
}
}
@@ -364,7 +364,7 @@ QUnit.test(
});
await openDiscuss(channelId);
await insertText(".o-mail-Composer-input", "/");
await click(".o-composer-suggestion");
await click(".o-mail-Composer-suggestion");
await insertText(".o-mail-Composer-input", " is user?");
assert.verifySteps([], "No rpc done");
}
@@ -378,10 +378,10 @@ QUnit.test("add an emoji after a command", async (assert) => {
});
const { openDiscuss } = await start();
await openDiscuss(channelId);
assert.containsNone($, ".o-composer-suggestion-list .o-open");
assert.containsNone($, ".o-mail-Composer-suggestionList .o-open");
assert.strictEqual($(".o-mail-Composer-input").val(), "");
await insertText(".o-mail-Composer-input", "/");
await click(".o-composer-suggestion");
await click(".o-mail-Composer-suggestion");
assert.strictEqual(
$(".o-mail-Composer-input").val().replace(/\s/, " "),
"/who ",
@@ -404,11 +404,11 @@ QUnit.test("add an emoji after a canned response", async (assert) => {
});
const { openDiscuss } = await start();
await openDiscuss(channelId);
assert.containsNone($, ".o-composer-suggestion");
assert.containsNone($, ".o-mail-Composer-suggestion");
assert.strictEqual($(".o-mail-Composer-input").val(), "");
await insertText(".o-mail-Composer-input", ":");
assert.containsOnce($, ".o-composer-suggestion");
await click(".o-composer-suggestion");
assert.containsOnce($, ".o-mail-Composer-suggestion");
await click(".o-mail-Composer-suggestion");
assert.strictEqual(
$(".o-mail-Composer-input").val().replace(/\s/, " "),
"Hello! How are you? ",
@@ -438,12 +438,12 @@ QUnit.test("add an emoji after a partner mention", async (assert) => {
});
const { openDiscuss } = await start();
await openDiscuss(channelId);
assert.containsNone($, ".o-composer-suggestion");
assert.containsNone($, ".o-mail-Composer-suggestion");
assert.strictEqual($(".o-mail-Composer-input").val(), "");
await insertText(".o-mail-Composer-input", "@");
await insertText(".o-mail-Composer-input", "T");
await insertText(".o-mail-Composer-input", "e");
await click(".o-composer-suggestion");
await click(".o-mail-Composer-suggestion");
assert.strictEqual($(".o-mail-Composer-input").val().replace(/\s/, " "), "@TestPartner ");
await click("button[aria-label='Emojis']");
@@ -459,7 +459,7 @@ QUnit.test("mention a channel after some text", async (assert) => {
});
const { openDiscuss } = await start();
await openDiscuss(channelId);
assert.containsNone($, ".o-composer-suggestion");
assert.containsNone($, ".o-mail-Composer-suggestion");
assert.strictEqual($(".o-mail-Composer-input").val(), "");
await insertText(".o-mail-Composer-input", "bluhbluh ");
assert.strictEqual(
@@ -468,8 +468,8 @@ QUnit.test("mention a channel after some text", async (assert) => {
"text content of composer should have content"
);
await insertText(".o-mail-Composer-input", "#");
assert.containsOnce($, ".o-composer-suggestion");
await click(".o-composer-suggestion");
assert.containsOnce($, ".o-mail-Composer-suggestion");
await click(".o-mail-Composer-suggestion");
assert.strictEqual(
$(".o-mail-Composer-input").val().replace(/\s/, " "),
"bluhbluh #General ",
@@ -485,11 +485,11 @@ QUnit.test("add an emoji after a channel mention", async (assert) => {
});
const { openDiscuss } = await start();
await openDiscuss(channelId);
assert.containsNone($, ".o-composer-suggestion");
assert.containsNone($, ".o-mail-Composer-suggestion");
assert.strictEqual($(".o-mail-Composer-input").val(), "");
await insertText(".o-mail-Composer-input", "#");
assert.containsOnce($, ".o-composer-suggestion");
await click(".o-composer-suggestion");
assert.containsOnce($, ".o-mail-Composer-suggestion");
await click(".o-mail-Composer-suggestion");
assert.strictEqual(
$(".o-mail-Composer-input").val().replace(/\s/, " "),
"#General ",
@@ -112,9 +112,9 @@ QUnit.test("reply: discard on pressing escape", async (assert) => {
// Escape on suggestion prompt does not stop replying
await insertText(".o-mail-Composer-input", "@");
assert.containsOnce($, ".o-composer-suggestion-list .o-open");
assert.containsOnce($, ".o-mail-Composer-suggestionList .o-open");
await afterNextRender(() => triggerHotkey("Escape"));
assert.containsNone($, ".o-composer-suggestion-list .o-open");
assert.containsNone($, ".o-mail-Composer-suggestionList .o-open");
assert.containsOnce($, ".o-mail-Composer");
click(".o-mail-Composer-input").catch(() => {});
@@ -1170,7 +1170,7 @@ QUnit.test("Chat with partner should be opened after clicking on their mention",
await insertText(".o-mail-Composer-input", "@");
await insertText(".o-mail-Composer-input", "T");
await insertText(".o-mail-Composer-input", "e");
await click(".o-composer-suggestion:contains(Test Partner)");
await click(".o-mail-Composer-suggestion:contains(Test Partner)");
await click(".o-mail-Composer-send");
await click(".o_mail_redirect");
assert.containsOnce($, ".o-mail-ChatWindow-content");
@@ -1216,7 +1216,7 @@ QUnit.test("Channel should be opened after clicking on its mention", async (asse
await openFormView("res.partner", partnerId);
await click("button:contains(Send message)");
await insertText(".o-mail-Composer-input", "#");
await click(".o-composer-suggestion:contains(my-channel)");
await click(".o-mail-Composer-suggestion:contains(my-channel)");
await click(".o-mail-Composer-send");
await click(".o_channel_redirect");
assert.containsOnce($, ".o-mail-ChatWindow-content");
@@ -41,10 +41,10 @@ QUnit.test('display partner mention suggestions on typing "@"', async (assert) =
});
const { openDiscuss } = await start();
await openDiscuss(channelId);
assert.containsNone($, ".o-composer-suggestion");
assert.containsNone($, ".o-mail-Composer-suggestion");
await insertText(".o-mail-Composer-input", "@");
assert.containsN($, ".o-composer-suggestion", 3);
assert.containsN($, ".o-mail-Composer-suggestion", 3);
});
QUnit.test(
@@ -70,13 +70,13 @@ QUnit.test(
});
const { openDiscuss } = await start();
await openDiscuss(channelId);
assert.containsNone($, ".o-composer-suggestion");
assert.containsNone($, ".o-mail-Composer-suggestion");
await insertText(".o-mail-Composer-input", "first message");
triggerHotkey("Enter");
await nextTick();
await insertText(".o-mail-Composer-input", "@");
assert.containsN($, ".o-composer-suggestion", 3);
assert.containsN($, ".o-mail-Composer-suggestion", 3);
}
);
@@ -85,9 +85,9 @@ QUnit.test('display partner mention suggestions on typing "@" in chatter', async
const { openFormView } = await start();
await openFormView("res.partner", pyEnv.currentPartnerId);
await click("button:contains(Send message)");
assert.containsNone($, ".o-composer-suggestion");
assert.containsNone($, ".o-mail-Composer-suggestion");
await insertText(".o-mail-Composer-input", "@");
assert.containsOnce($, ".o-composer-suggestion:contains(Mitchell Admin)");
assert.containsOnce($, ".o-mail-Composer-suggestion:contains(Mitchell Admin)");
});
QUnit.test("show other channel member in @ mention", async (assert) => {
@@ -106,7 +106,7 @@ QUnit.test("show other channel member in @ mention", async (assert) => {
const { openDiscuss } = await start();
await openDiscuss(channelId);
await insertText(".o-mail-Composer-input", "@");
assert.containsOnce($, ".o-composer-suggestion:contains(TestPartner)");
assert.containsOnce($, ".o-mail-Composer-suggestion:contains(TestPartner)");
});
QUnit.test("select @ mention insert mention text in composer", async (assert) => {
@@ -125,7 +125,7 @@ QUnit.test("select @ mention insert mention text in composer", async (assert) =>
const { openDiscuss } = await start();
await openDiscuss(channelId);
await insertText(".o-mail-Composer-input", "@");
await click(".o-composer-suggestion:contains(TestPartner)");
await click(".o-mail-Composer-suggestion:contains(TestPartner)");
assert.strictEqual($(".o-mail-Composer-input").val().trim(), "@TestPartner");
});
@@ -137,9 +137,9 @@ QUnit.test('display command suggestions on typing "/"', async (assert) => {
});
const { openDiscuss } = await start();
await openDiscuss(channelId);
assert.containsNone($, ".o-composer-suggestion-list .o-open");
assert.containsNone($, ".o-mail-Composer-suggestionList .o-open");
await insertText(".o-mail-Composer-input", "/");
assert.containsOnce($, ".o-composer-suggestion-list .o-open");
assert.containsOnce($, ".o-mail-Composer-suggestionList .o-open");
});
QUnit.test("use a command for a specific channel type", async (assert) => {
@@ -147,10 +147,10 @@ QUnit.test("use a command for a specific channel type", async (assert) => {
const channelId = pyEnv["mail.channel"].create({ channel_type: "chat" });
const { openDiscuss } = await start();
await openDiscuss(channelId);
assert.containsNone($, ".o-composer-suggestion-list .o-open");
assert.containsNone($, ".o-mail-Composer-suggestionList .o-open");
assert.strictEqual($(".o-mail-Composer-input").val(), "");
await insertText(".o-mail-Composer-input", "/");
await click(".o-composer-suggestion");
await click(".o-mail-Composer-suggestion");
assert.strictEqual(
$(".o-mail-Composer-input").val().replace(/\s/, " "),
"/who ",
@@ -168,12 +168,12 @@ QUnit.test(
});
const { openDiscuss } = await start();
await openDiscuss(channelId);
assert.containsNone($, ".o-composer-suggestion-list .o-open");
assert.containsNone($, ".o-mail-Composer-suggestionList .o-open");
assert.strictEqual($(".o-mail-Composer-input").val(), "");
await insertText(".o-mail-Composer-input", "bluhbluh ");
assert.strictEqual($(".o-mail-Composer-input").val(), "bluhbluh ");
await insertText(".o-mail-Composer-input", "/");
assert.containsNone($, ".o-composer-suggestion-list .o-open");
assert.containsNone($, ".o-mail-Composer-suggestionList .o-open");
}
);
@@ -186,9 +186,9 @@ QUnit.test('display canned response suggestions on typing ":"', async (assert) =
});
const { openDiscuss } = await start();
await openDiscuss(channelId);
assert.containsNone($, ".o-composer-suggestion-list .o-open");
assert.containsNone($, ".o-mail-Composer-suggestionList .o-open");
await insertText(".o-mail-Composer-input", ":");
assert.containsOnce($, ".o-composer-suggestion-list .o-open");
assert.containsOnce($, ".o-mail-Composer-suggestionList .o-open");
});
QUnit.test("use a canned response", async (assert) => {
@@ -200,11 +200,11 @@ QUnit.test("use a canned response", async (assert) => {
});
const { openDiscuss } = await start();
await openDiscuss(channelId);
assert.containsNone($, ".o-composer-suggestion-list .o-open");
assert.containsNone($, ".o-mail-Composer-suggestionList .o-open");
assert.strictEqual($(".o-mail-Composer-input").val(), "");
await insertText(".o-mail-Composer-input", ":");
assert.containsOnce($, ".o-composer-suggestion");
await click(".o-composer-suggestion");
assert.containsOnce($, ".o-mail-Composer-suggestion");
await click(".o-mail-Composer-suggestion");
assert.strictEqual(
$(".o-mail-Composer-input").val().replace(/\s/, " "),
"Hello! How are you? ",
@@ -221,13 +221,13 @@ QUnit.test("use a canned response some text", async (assert) => {
});
const { openDiscuss } = await start();
await openDiscuss(channelId);
assert.containsNone($, ".o-composer-suggestion");
assert.containsNone($, ".o-mail-Composer-suggestion");
assert.strictEqual($(".o-mail-Composer-input").val(), "");
await insertText(".o-mail-Composer-input", "bluhbluh ");
assert.strictEqual($(".o-mail-Composer-input").val(), "bluhbluh ");
await insertText(".o-mail-Composer-input", ":");
assert.containsOnce($, ".o-composer-suggestion");
await click(".o-composer-suggestion");
assert.containsOnce($, ".o-mail-Composer-suggestion");
await click(".o-mail-Composer-suggestion");
assert.strictEqual(
$(".o-mail-Composer-input").val().replace(/\s/, " "),
"bluhbluh Hello! How are you? ",
@@ -243,9 +243,9 @@ QUnit.test('display channel mention suggestions on typing "#"', async (assert) =
});
const { openDiscuss } = await start();
await openDiscuss(channelId);
assert.containsNone($, ".o-composer-suggestion-list .o-open");
assert.containsNone($, ".o-mail-Composer-suggestionList .o-open");
await insertText(".o-mail-Composer-input", "#");
assert.containsOnce($, ".o-composer-suggestion-list .o-open");
assert.containsOnce($, ".o-mail-Composer-suggestionList .o-open");
});
QUnit.test("mention a channel", async (assert) => {
@@ -256,11 +256,11 @@ QUnit.test("mention a channel", async (assert) => {
});
const { openDiscuss } = await start();
await openDiscuss(channelId);
assert.containsNone($, ".o-composer-suggestion-list .o-open");
assert.containsNone($, ".o-mail-Composer-suggestionList .o-open");
assert.strictEqual($(".o-mail-Composer-input").val(), "");
await insertText(".o-mail-Composer-input", "#");
assert.containsOnce($, ".o-composer-suggestion");
await click(".o-composer-suggestion");
assert.containsOnce($, ".o-mail-Composer-suggestion");
await click(".o-mail-Composer-suggestion");
assert.strictEqual(
$(".o-mail-Composer-input").val().replace(/\s/, " "),
"#General ",
+10 -10
View File
@@ -210,7 +210,7 @@ QUnit.test("mention a channel with space in the name", async (assert) => {
const { openDiscuss } = await start();
await openDiscuss(channelId);
await insertText(".o-mail-Composer-input", "#");
await click(".o-composer-suggestion");
await click(".o-mail-Composer-suggestion");
await click(".o-mail-Composer-send");
assert.containsOnce($(".o-mail-Message-body"), ".o_channel_redirect");
assert.strictEqual($(".o_channel_redirect").text(), "#General good boy");
@@ -222,7 +222,7 @@ QUnit.test('mention a channel with "&" in the name', async (assert) => {
const { openDiscuss } = await start();
await openDiscuss(channelId);
await insertText(".o-mail-Composer-input", "#");
await click(".o-composer-suggestion");
await click(".o-mail-Composer-suggestion");
await click(".o-mail-Composer-send");
assert.containsOnce($(".o-mail-Message-body"), ".o_channel_redirect");
assert.strictEqual($(".o_channel_redirect").text(), "#General & good");
@@ -545,7 +545,7 @@ QUnit.test("Mention a partner with special character (e.g. apostrophe ')", async
await openDiscuss(channelId);
await insertText(".o-mail-Composer-input", "@");
await insertText(".o-mail-Composer-input", "Pyn");
await click(".o-composer-suggestion");
await click(".o-mail-Composer-suggestion");
await click(".o-mail-Composer-send");
assert.containsOnce(
$(".o-mail-Message-body"),
@@ -577,10 +577,10 @@ QUnit.test("mention 2 different partners that have the same name", async (assert
await openDiscuss(channelId);
await insertText(".o-mail-Composer-input", "@");
await insertText(".o-mail-Composer-input", "Te");
await click(".o-composer-suggestion:eq(0)");
await click(".o-mail-Composer-suggestion:eq(0)");
await insertText(".o-mail-Composer-input", "@");
await insertText(".o-mail-Composer-input", "Te");
await click(".o-composer-suggestion:eq(1)");
await click(".o-mail-Composer-suggestion:eq(1)");
await click(".o-mail-Composer-send");
assert.containsOnce($, ".o-mail-Message-body");
assert.containsOnce(
@@ -600,7 +600,7 @@ QUnit.test("mention a channel on a second line when the first line contains #",
await openDiscuss(channelId);
await insertText(".o-mail-Composer-input", "#blabla\n");
await insertText(".o-mail-Composer-input", "#");
await click(".o-composer-suggestion");
await click(".o-mail-Composer-suggestion");
await click(".o-mail-Composer-send");
assert.containsOnce($(".o-mail-Message-body"), ".o_channel_redirect");
assert.strictEqual($(".o_channel_redirect").text(), "#General good");
@@ -614,7 +614,7 @@ QUnit.test(
const { openDiscuss } = await start();
await openDiscuss(channelId);
await insertText(".o-mail-Composer-input", "#");
await click(".o-composer-suggestion");
await click(".o-mail-Composer-suggestion");
const text = $(".o-mail-Composer-input").val();
$(".o-mail-Composer-input").val(text.slice(0, -1));
await insertText(".o-mail-Composer-input", ", test");
@@ -641,10 +641,10 @@ QUnit.test("mention 2 different channels that have the same name", async (assert
await openDiscuss(channelId_1);
await insertText(".o-mail-Composer-input", "#");
await insertText(".o-mail-Composer-input", "m");
await click(".o-composer-suggestion:eq(0)");
await click(".o-mail-Composer-suggestion:eq(0)");
await insertText(".o-mail-Composer-input", "#");
await insertText(".o-mail-Composer-input", "m");
await click(".o-composer-suggestion:eq(1)");
await click(".o-mail-Composer-suggestion:eq(1)");
await click(".o-mail-Composer-send");
assert.containsOnce($, ".o-mail-Message-body");
assert.containsOnce(
@@ -677,7 +677,7 @@ QUnit.test(
await insertText(".o-mail-Composer-input", "email@odoo.com\n");
await insertText(".o-mail-Composer-input", "@");
await insertText(".o-mail-Composer-input", "Te");
await click(".o-composer-suggestion");
await click(".o-mail-Composer-suggestion");
await click(".o-mail-Composer-send");
assert.containsOnce(
$(".o-mail-Message-body"),