[FIX] mail: set thread cache as loaded is message_fetch failed

task-2377440

closes odoo/odoo#61469

Signed-off-by: Alexandre Kühn (aku) <aku@odoo.com>
This commit is contained in:
Xavier Dubuc
2021-01-27 11:38:28 +00:00
parent 9777a2b4d6
commit 6169ae7e87
7 changed files with 357 additions and 30 deletions
@@ -490,6 +490,20 @@ class MessageList extends Component {
this._loadMore();
}
/**
* @private
*/
_onClickRetryLoadMoreMessages() {
if (!this.threadView) {
return;
}
if (!this.threadView.threadCache) {
return;
}
this.threadView.threadCache.update({ hasLoadingFailed: false });
this._loadMore();
}
/**
* @private
* @param {ScrollEvent} ev
@@ -37,7 +37,17 @@
</div>
</t>
<!-- LOADING (if order asc)-->
<t t-if="props.order === 'asc' and orderedMessages.length > 0">
<t t-if="threadView.threadCache.hasLoadingFailed">
<div class="o_ThreadView_alertLoadingFailed alert alert-info">
<div>
An error occured while fetching messages.
</div>
<button class="o_ThreadView_alertLoadingFailedRetryButton btn btn-link" t-on-click="_onClickRetryLoadMoreMessages">
Click here to retry
</button>
</div>
</t>
<t t-elif="props.order === 'asc' and orderedMessages.length > 0">
<t t-call="mail.MessageList.loadMore"/>
</t>
<!-- MESSAGES -->
@@ -75,7 +85,7 @@
</t>
</t>
<!-- LOADING (if order desc)-->
<t t-if="props.order === 'desc' and orderedMessages.length > 0">
<t t-if="!threadView.threadCache.hasLoadingFailed and props.order === 'desc' and orderedMessages.length > 0">
<t t-call="mail.MessageList.loadMore"/>
</t>
</t>
@@ -130,6 +130,7 @@ class ThreadView extends Component {
isDeviceMobile: this.env.messaging.device.isMobile,
thread,
threadCacheIsLoaded: threadCache && threadCache.isLoaded,
threadCacheHasLoadingFailed: threadCache && threadCache.hasLoadingFailed,
threadIsTemporary: thread && thread.isTemporary,
threadMassMailing: thread && thread.mass_mailing,
threadModel: thread && thread.model,
@@ -138,6 +139,23 @@ class ThreadView extends Component {
};
}
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* @private
*/
_onClickRetryLoadMessages() {
if (!this.threadView) {
return;
}
if (!this.threadView.threadCache) {
return;
}
this.threadView.threadCache.update({ hasLoadingFailed: false });
}
}
Object.assign(ThreadView, {
@@ -13,6 +13,11 @@
flex: 0 0 auto;
}
.o_ThreadView_alertLoadingFailed {
display: flex;
align-items: center;
}
.o_ThreadView_loading {
display: flex;
align-self: center;
@@ -20,6 +25,14 @@
align-items: center;
}
.o_ThreadView_loadingFailed {
display: flex;
flex: 1 1 auto;
align-items: center;
justify-content: center;
flex-direction: column;
}
.o_ThreadView_messageList {
flex: 1 1 auto;
}
@@ -22,6 +22,16 @@
t-ref="messageList"
/>
</t>
<t t-elif="threadView.threadCache.hasLoadingFailed">
<div class="o_ThreadView_loadingFailed">
<div class="o_ThreadView_loadingFailedText">
An error occured while fetching messages.
</div>
<button class="o_ThreadView_loadingFailedRetryButton btn btn-link" t-on-click="_onClickRetryLoadMessages">
Click here to retry
</button>
</div>
</t>
<t t-elif="props.hasComposer">
<div class="o-autogrow"/>
</t>
@@ -1816,6 +1816,240 @@ QUnit.test('composer should be focused automatically after clicking on the send
);
});
QUnit.test('failure on loading messages should display error', async function (assert) {
assert.expect(1);
this.data['mail.channel'].records.push({
channel_type: 'channel',
id: 20,
is_pinned: true,
name: "General",
});
await this.start({
async mockRPC(route, args) {
if (args.method === 'message_fetch') {
throw new Error();
}
return this._super(...arguments);
},
});
const thread = this.env.models['mail.thread'].findFromIdentifyingData({
id: 20,
model: 'mail.channel'
});
const threadViewer = this.env.models['mail.thread_viewer'].create({
hasThreadView: true,
thread: [['link', thread]],
});
await this.createThreadViewComponent(threadViewer.threadView);
assert.containsOnce(
document.body,
'.o_ThreadView_loadingFailed',
"should show loading error message"
);
});
QUnit.test('failure on loading messages should prompt retry button', async function (assert) {
assert.expect(1);
this.data['mail.channel'].records.push({
channel_type: 'channel',
id: 20,
is_pinned: true,
name: "General",
});
await this.start({
async mockRPC(route, args) {
if (args.method === 'message_fetch') {
throw new Error();
}
return this._super(...arguments);
},
});
const thread = this.env.models['mail.thread'].findFromIdentifyingData({
id: 20,
model: 'mail.channel'
});
const threadViewer = this.env.models['mail.thread_viewer'].create({
hasThreadView: true,
thread: [['link', thread]],
});
await this.createThreadViewComponent(threadViewer.threadView);
assert.containsOnce(
document.body,
'.o_ThreadView_loadingFailedRetryButton',
"should show a button to allow user to retry loading"
);
});
QUnit.test('failure on loading more messages should not alter message list display', async function (assert) {
assert.expect(1);
// first call needs to be successful as it is the initial loading of messages
// second call comes from load more and needs to fail in order to show the error alert
// any later call should work so that retry button and load more clicks would now work
let messageFetchShouldFail = false;
this.data['mail.channel'].records.push({
channel_type: 'channel',
id: 20,
is_pinned: true,
name: "General",
});
this.data['mail.message'].records.push(...[...Array(60).keys()].map(id => {
return {
body: 'coucou',
channel_ids: [20],
id,
};
}));
await this.start({
async mockRPC(route, args) {
if (args.method === 'message_fetch') {
if (messageFetchShouldFail) {
throw new Error();
}
}
return this._super(...arguments);
},
});
const thread = this.env.models['mail.thread'].findFromIdentifyingData({
id: 20,
model: 'mail.channel'
});
const threadViewer = this.env.models['mail.thread_viewer'].create({
hasThreadView: true,
thread: [['link', thread]],
});
await this.createThreadViewComponent(threadViewer.threadView, { hasComposer: true });
messageFetchShouldFail = true;
await afterNextRender(() => document.querySelector('.o_MessageList_loadMore').click());
assert.containsN(
document.body,
'.o_Message',
30,
"should still show 30 messages as load more has failed"
);
});
QUnit.test('failure on loading more messages should display error and prompt retry button', async function (assert) {
assert.expect(3);
// first call needs to be successful as it is the initial loading of messages
// second call comes from load more and needs to fail in order to show the error alert
// any later call should work so that retry button and load more clicks would now work
let messageFetchShouldFail = false;
this.data['mail.channel'].records.push({
channel_type: 'channel',
id: 20,
is_pinned: true,
name: "General",
});
this.data['mail.message'].records.push(...[...Array(60).keys()].map(id => {
return {
body: 'coucou',
channel_ids: [20],
id,
};
}));
await this.start({
async mockRPC(route, args) {
if (args.method === 'message_fetch') {
if (messageFetchShouldFail) {
throw new Error();
}
}
return this._super(...arguments);
},
});
const thread = this.env.models['mail.thread'].findFromIdentifyingData({
id: 20,
model: 'mail.channel'
});
const threadViewer = this.env.models['mail.thread_viewer'].create({
hasThreadView: true,
thread: [['link', thread]],
});
await this.createThreadViewComponent(threadViewer.threadView, { hasComposer: true });
messageFetchShouldFail = true;
await afterNextRender(() => document.querySelector('.o_MessageList_loadMore').click());
assert.containsOnce(
document.body,
'.o_ThreadView_alertLoadingFailed',
"should show loading error message"
);
assert.containsOnce(
document.body,
'.o_ThreadView_alertLoadingFailedRetryButton',
"should show loading error message button"
);
assert.containsNone(
document.body,
'.o_MessageList_loadMore',
"should not show load more buttton"
);
});
QUnit.test('Retry loading more messages on failed load more messages should load more messages', async function (assert) {
assert.expect(0);
// first call needs to be successful as it is the initial loading of messages
// second call comes from load more and needs to fail in order to show the error alert
// any later call should work so that retry button and load more clicks would now work
let messageFetchShouldFail = false;
this.data['mail.channel'].records.push({
channel_type: 'channel',
id: 20,
is_pinned: true,
name: "General",
});
this.data['mail.message'].records = [...Array(90).keys()].map(id => {
return {
body: 'coucou',
channel_ids: [20],
id,
};
});
await this.start({
async mockRPC(route, args) {
if (args.method === 'message_fetch') {
if (messageFetchShouldFail) {
throw new Error();
}
}
return this._super(...arguments);
},
});
const thread = this.env.models['mail.thread'].findFromIdentifyingData({
id: 20,
model: 'mail.channel'
});
const threadViewer = this.env.models['mail.thread_viewer'].create({
hasThreadView: true,
thread: [['link', thread]],
});
await this.createThreadViewComponent(threadViewer.threadView, { hasComposer: true });
messageFetchShouldFail = true;
await afterNextRender(() => document.querySelector('.o_MessageList_loadMore').click());
messageFetchShouldFail = false;
await this.afterEvent({
eventName: 'o-thread-view-hint-processed',
func: () => document.querySelector('.o_ThreadView_alertLoadingFailedRetryButton').click(),
message: "should wait until channel 20 loaded more messages after clicked on load more",
predicate: ({ hint, threadViewer }) => {
return (
hint.type === 'more-messages-loaded' &&
threadViewer.thread.model === 'mail.channel' &&
threadViewer.thread.id === 20
);
},
});
});
});
});
});
@@ -12,9 +12,6 @@ function factory(dependencies) {
// Public
//----------------------------------------------------------------------
/**
* @returns {mail.message[]|undefined}
*/
async loadMoreMessages() {
if (this.isAllHistoryLoaded || this.isLoading) {
return;
@@ -26,18 +23,26 @@ function factory(dependencies) {
this.update({ isLoadingMore: true });
const messageIds = this.fetchedMessages.map(message => message.id);
const limit = 30;
const fetchedMessages = await this.async(() => this._loadMessages({
extraDomain: [['id', '<', Math.min(...messageIds)]],
limit,
}));
let fetchedMessages;
let success;
try {
fetchedMessages = await this.async(() => this._loadMessages({
extraDomain: [['id', '<', Math.min(...messageIds)]],
limit,
}));
success = true;
} catch (e) {
success = false;
}
if (success) {
if (fetchedMessages.length < limit) {
this.update({ isAllHistoryLoaded: true });
}
for (const threadView of this.threadViews) {
threadView.addComponentHint('more-messages-loaded', { fetchedMessages });
}
}
this.update({ isLoadingMore: false });
if (fetchedMessages.length < limit) {
this.update({ isAllHistoryLoaded: true });
}
for (const threadView of this.threadViews) {
threadView.addComponentHint('more-messages-loaded', { fetchedMessages });
}
return fetchedMessages;
}
/**
@@ -56,6 +61,9 @@ function factory(dependencies) {
extraDomain: [['id', '>', Math.max(...messageIds)]],
limit: false,
});
if (!fetchedMessages) {
return;
}
for (const threadView of this.threadViews) {
threadView.addComponentHint('new-messages-loaded', { fetchedMessages });
}
@@ -193,6 +201,9 @@ function factory(dependencies) {
// happens during destroy or compute executed in wrong order
return false;
}
if (this.hasLoadingFailed) {
return false;
}
const wasCacheRefreshRequested = this.isCacheRefreshRequested;
// mark hint as processed
if (this.isCacheRefreshRequested) {
@@ -271,6 +282,7 @@ function factory(dependencies) {
* @param {Array[]} [param0.extraDomain]
* @param {integer} [param0.limit=30]
* @returns {mail.message[]}
* @throws {Error} when failed to load messages
*/
async _loadMessages({ extraDomain, limit = 30 } = {}) {
this.update({ isLoading: true });
@@ -284,16 +296,26 @@ function factory(dependencies) {
const moderated_channel_ids = this.thread.moderation
? [this.thread.id]
: undefined;
const messages = await this.async(() =>
this.env.models['mail.message'].performRpcMessageFetch(
domain,
limit,
moderated_channel_ids,
context,
)
);
let messages;
try {
messages = await this.async(() =>
this.env.models['mail.message'].performRpcMessageFetch(
domain,
limit,
moderated_channel_ids,
context,
)
);
} catch(e) {
this.update({
hasLoadingFailed: true,
isLoading: false,
});
throw e;
}
this.update({
fetchedMessages: [['link', messages]],
hasLoadingFailed: false,
isLoaded: true,
isLoading: false,
});
@@ -351,15 +373,14 @@ function factory(dependencies) {
*
* @private
*/
_onHasToLoadMessagesChanged() {
async _onHasToLoadMessagesChanged() {
if (!this.hasToLoadMessages) {
return;
}
this._loadMessages().then(fetchedMessages => {
for (const threadView of this.threadViews) {
threadView.addComponentHint('messages-loaded', { fetchedMessages });
}
});
const fetchedMessages = await this.async(() => this._loadMessages());
for (const threadView of this.threadViews) {
threadView.addComponentHint('messages-loaded', { fetchedMessages });
}
}
/**
@@ -414,6 +435,12 @@ function factory(dependencies) {
compute: '_computeFetchedMessages',
dependencies: ['threadMessages'],
}),
/**
* Determines whether the last message fetch failed.
*/
hasLoadingFailed: attr({
default: false,
}),
/**
* Determines whether `this` should load initial messages. This field is
* computed and should be considered read-only.
@@ -422,6 +449,7 @@ function factory(dependencies) {
hasToLoadMessages: attr({
compute: '_computeHasToLoadMessages',
dependencies: [
'hasLoadingFailed',
'isCacheRefreshRequested',
'isLoaded',
'isLoading',