[FIX] mail: traceback when switch nav list
When a nav list is open, if there is a new props provided, a error could be thrown. Reproduce: 1. write "@aUser #ge" in the composer 2. click on the @aUser => traceback The problem is because when NavigableList is open, the `props.optionTemplate` will change before `state.options`, leading to a mismatch between the template and the record. This commit solves the issue by removing syncing props.options and state.options altogether. This felt needed to enrich options with id, but this is unnecessary because the forged id was simply the index of option in the list. closes odoo/odoo#144260 X-original-commit: b41f25bc7b41799bd34e27e8e1013d7a85320bcc Signed-off-by: Alexandre Kühn (aku) <aku@odoo.com>
This commit is contained in:
@@ -28,9 +28,8 @@ export class NavigableList extends Component {
|
||||
setup() {
|
||||
this.rootRef = useRef("root");
|
||||
this.state = useState({
|
||||
activeOption: null,
|
||||
activeIndex: null,
|
||||
open: false,
|
||||
options: [],
|
||||
});
|
||||
this.hotkey = useService("hotkey");
|
||||
this.hotkeysToRemove = [];
|
||||
@@ -58,33 +57,22 @@ export class NavigableList extends Component {
|
||||
}
|
||||
|
||||
get show() {
|
||||
return Boolean(this.state.open && (this.props.isLoading || this.state.options.length));
|
||||
return Boolean(this.state.open && (this.props.isLoading || this.props.options.length));
|
||||
}
|
||||
|
||||
open() {
|
||||
this.load();
|
||||
this.state.open = true;
|
||||
this.state.activeIndex = null;
|
||||
this.navigate("first");
|
||||
}
|
||||
|
||||
close() {
|
||||
this.state.open = false;
|
||||
this.state.activeOption = null;
|
||||
this.state.activeIndex = null;
|
||||
}
|
||||
|
||||
load() {
|
||||
this.state.options = [];
|
||||
this.state.options = this.props.options.map((option, index) => ({
|
||||
...option,
|
||||
id: index,
|
||||
}));
|
||||
}
|
||||
|
||||
isActiveOption(option) {
|
||||
return this.state.activeOption?.id === option.id;
|
||||
}
|
||||
|
||||
selectOption(ev, option, params = {}) {
|
||||
selectOption(ev, index, params = {}) {
|
||||
const option = this.props.options[index];
|
||||
if (option.unselectable) {
|
||||
this.close();
|
||||
return;
|
||||
@@ -96,14 +84,14 @@ export class NavigableList extends Component {
|
||||
}
|
||||
|
||||
navigate(direction) {
|
||||
const activeOptionId = this.state.activeOption ? this.state.activeOption.id : -1;
|
||||
const activeOptionId = this.state.activeIndex !== null ? this.state.activeIndex : 0;
|
||||
let targetId = undefined;
|
||||
switch (direction) {
|
||||
case "first":
|
||||
targetId = 0;
|
||||
break;
|
||||
case "last":
|
||||
targetId = this.state.options.length - 1;
|
||||
targetId = this.props.options.length - 1;
|
||||
break;
|
||||
case "previous":
|
||||
targetId = activeOptionId - 1;
|
||||
@@ -114,7 +102,7 @@ export class NavigableList extends Component {
|
||||
break;
|
||||
case "next":
|
||||
targetId = activeOptionId + 1;
|
||||
if (targetId > this.state.options.length - 1) {
|
||||
if (targetId > this.props.options.length - 1) {
|
||||
this.navigate("first");
|
||||
return;
|
||||
}
|
||||
@@ -122,7 +110,7 @@ export class NavigableList extends Component {
|
||||
default:
|
||||
return;
|
||||
}
|
||||
this.state.activeOption = this.state.options.find((o) => o.id === targetId);
|
||||
this.state.activeIndex = targetId;
|
||||
}
|
||||
|
||||
onKeydown(ev) {
|
||||
@@ -132,11 +120,11 @@ export class NavigableList extends Component {
|
||||
const hotkey = getActiveHotkey(ev);
|
||||
switch (hotkey) {
|
||||
case "enter":
|
||||
if (!this.show || !this.state.activeOption) {
|
||||
if (!this.show || this.state.activeIndex === null) {
|
||||
return;
|
||||
}
|
||||
markEventHandled(ev, "NavigableList.select");
|
||||
this.selectOption(ev, this.state.activeOption);
|
||||
this.selectOption(ev, this.state.activeIndex);
|
||||
break;
|
||||
case "escape":
|
||||
markEventHandled(ev, "NavigableList.close");
|
||||
@@ -157,7 +145,7 @@ export class NavigableList extends Component {
|
||||
ev.preventDefault();
|
||||
}
|
||||
|
||||
onOptionMouseEnter(option) {
|
||||
this.state.activeOption = option;
|
||||
onOptionMouseEnter(index) {
|
||||
this.state.activeIndex = index;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,13 +12,13 @@
|
||||
</div>
|
||||
<t t-else="">
|
||||
<div
|
||||
t-foreach="state.options" t-as="option" t-key="option.id"
|
||||
t-foreach="props.options" t-as="option" t-key="option_index"
|
||||
class="o-mail-NavigableList-item"
|
||||
t-att-class="option.classList"
|
||||
t-on-mouseenter="() => this.onOptionMouseEnter(option)"
|
||||
t-on-click="(ev) => this.selectOption(ev, option)"
|
||||
t-on-mouseenter="() => this.onOptionMouseEnter(option_index)"
|
||||
t-on-click="(ev) => this.selectOption(ev, 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': isActiveOption(option) }">
|
||||
<a href="#" class="d-flex align-items-center w-100 py-2 px-4" t-att-class="{ 'o-mail-NavigableList-active bg-300': state.activeIndex === option_index }">
|
||||
<t t-if="props.optionTemplate" t-call="{{ props.optionTemplate }}"/>
|
||||
<t t-else="" t-esc="option.label"/>
|
||||
</a>
|
||||
|
||||
@@ -388,6 +388,35 @@ QUnit.test("pending mentions are kept when toggling composer", async () => {
|
||||
await contains(".o-mail-Message-body a.o_mail_redirect", { text: "@Mitchell Admin" });
|
||||
});
|
||||
|
||||
QUnit.test("composer suggestion should match with input selection", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({
|
||||
email: "testpartner@odoo.com",
|
||||
name: "Luigi",
|
||||
});
|
||||
const channelId = pyEnv["discuss.channel"].create({
|
||||
name: "Mario Party",
|
||||
channel_member_ids: [
|
||||
Command.create({ partner_id: pyEnv.currentPartnerId }),
|
||||
Command.create({ partner_id: partnerId }),
|
||||
],
|
||||
});
|
||||
const { openDiscuss } = await start();
|
||||
openDiscuss(channelId);
|
||||
await contains(".o-mail-Composer-input", { value: "" });
|
||||
await insertText(".o-mail-Composer-input", "#");
|
||||
await contains(".o-mail-Composer-suggestion", { text: "#Mario Party" });
|
||||
await click(".o-mail-Composer-suggestion");
|
||||
await contains(".o-mail-Composer-input", { value: "#Mario Party " });
|
||||
await insertText(".o-mail-Composer-input", "@");
|
||||
await contains(".o-mail-Composer-suggestion", { text: "Luigi" });
|
||||
$(".o-mail-Composer-input")[0].setSelectionRange(3, 3);
|
||||
await contains(".o-mail-Composer-suggestion", { text: "#Mario Party" });
|
||||
const textarea = $(".o-mail-Composer-input")[0];
|
||||
textarea.setSelectionRange(textarea.value.length, textarea.value.length);
|
||||
await contains(".o-mail-Composer-suggestion", { text: "Luigi" });
|
||||
});
|
||||
|
||||
QUnit.test('do not post message on channel with "SHIFT-Enter" keyboard shortcut', async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "general" });
|
||||
|
||||
Reference in New Issue
Block a user