[REF] mail: NavigableList Ref
closes odoo/odoo#116037 Signed-off-by: Alexandre Kühn (aku) <aku@odoo.com>
This commit is contained in:
committed by
Alexandre Kühn
parent
73a0817688
commit
3a5eb4567f
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 ",
|
||||
|
||||
@@ -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"),
|
||||
|
||||
Reference in New Issue
Block a user