[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:
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user