fix: clear conversation list loading state when filter request fails (#15320)
## Description
When `POST /api/v1/accounts/:id/conversations/filter` fails (for example
a 500 or a server-side timeout), the conversation list spinner never
clears because the Vuex action's catch block was empty. The user is
stuck on an infinite loader with no way to retry.
This change:
- Clears the list loading state (`CLEAR_LIST_LOADING_STATUS`) and
rethrows in `fetchFilteredConversations`, matching how sibling actions
in the module handle errors. Both applied filters and saved filters
(custom views) go through this action.
- Catches the rejection in `ChatList.vue` and shows an alert ("Couldn't
load conversations. Please try again.") so the user can retry.
- Guards the reconnect flow in `ReconnectService` so a failed filtered
fetch on websocket reconnect does not abort cache revalidation.
- Fixes the existing spec for the success path, which passed no
`dispatch` and was silently exercising the error path.
No client-side request timeout was added since the dashboard API layer
has no per-request timeout convention; the fix is scoped to error
handling.
Fixes https://linear.app/chatwoot/issue/CW-7831
## Type of change
- [x] Bug fix (non-breaking change which fixes an issue)
## How Has This Been Tested?
- Added a spec asserting the loading state clears and the error is
rethrown when the filter request rejects: `pnpm test
app/javascript/dashboard/store/modules/specs/conversations/actions.spec.js`
(49 passed).
- `pnpm test
app/javascript/dashboard/helper/specs/ReconnectService.spec.js` (24
passed).
## Checklist:
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
This commit is contained in:
@@ -401,7 +401,10 @@ function fetchFilteredConversations(payload) {
|
||||
queryData: filterQueryGenerator(payload),
|
||||
page,
|
||||
})
|
||||
.then(emitConversationLoaded);
|
||||
.catch(() => useAlert(t('CHAT_LIST.FETCH_ERROR')))
|
||||
// emit even on failure so a deep-linked conversation still loads via
|
||||
// fetchConversationIfUnavailable
|
||||
.finally(emitConversationLoaded);
|
||||
|
||||
showAdvancedFilters.value = false;
|
||||
}
|
||||
@@ -414,7 +417,8 @@ function fetchSavedFilteredConversations(payload) {
|
||||
queryData: payload,
|
||||
page,
|
||||
})
|
||||
.then(emitConversationLoaded);
|
||||
.catch(() => useAlert(t('CHAT_LIST.FETCH_ERROR')))
|
||||
.finally(emitConversationLoaded);
|
||||
}
|
||||
|
||||
function onApplyFilter(payload) {
|
||||
|
||||
@@ -63,10 +63,14 @@ class ReconnectService {
|
||||
};
|
||||
|
||||
fetchFilteredOrSavedConversations = async queryData => {
|
||||
await this.store.dispatch('fetchFilteredConversations', {
|
||||
queryData,
|
||||
page: 1,
|
||||
});
|
||||
try {
|
||||
await this.store.dispatch('fetchFilteredConversations', {
|
||||
queryData,
|
||||
page: 1,
|
||||
});
|
||||
} catch (error) {
|
||||
// Ignore error, reconnect flow should continue
|
||||
}
|
||||
};
|
||||
|
||||
fetchConversationsOnReconnect = async () => {
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
"404": "There are no active conversations in this group."
|
||||
},
|
||||
"FAILED_TO_SEND": "Failed to send",
|
||||
"FETCH_ERROR": "Couldn't load conversations. Please try again.",
|
||||
"TAB_HEADING": "Conversations",
|
||||
"MENTION_HEADING": "Mentions",
|
||||
"UNATTENDED_HEADING": "Unattended",
|
||||
|
||||
@@ -72,7 +72,8 @@ const actions = {
|
||||
'appliedFilters'
|
||||
);
|
||||
} catch (error) {
|
||||
// Handle error
|
||||
commit(types.CLEAR_LIST_LOADING_STATUS);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -437,11 +437,30 @@ describe('#actions', () => {
|
||||
axios.post.mockResolvedValue({
|
||||
data: dataReceived,
|
||||
});
|
||||
await actions.fetchFilteredConversations({ commit }, dataToSend);
|
||||
expect(commit).toHaveBeenCalledTimes(2);
|
||||
await actions.fetchFilteredConversations(
|
||||
{ commit, dispatch },
|
||||
dataToSend
|
||||
);
|
||||
expect(commit).toHaveBeenCalledTimes(4);
|
||||
expect(commit.mock.calls).toEqual([
|
||||
['SET_LIST_LOADING_STATUS'],
|
||||
['SET_ALL_CONVERSATION', dataReceived.payload],
|
||||
['CLEAR_LIST_LOADING_STATUS'],
|
||||
[
|
||||
`contacts/${types.SET_CONTACTS}`,
|
||||
dataReceived.payload.map(chat => chat.meta.sender),
|
||||
],
|
||||
]);
|
||||
});
|
||||
|
||||
it('clears the loading state and rethrows if the request fails', async () => {
|
||||
axios.post.mockRejectedValue(new Error('Request failed'));
|
||||
await expect(
|
||||
actions.fetchFilteredConversations({ commit }, dataToSend)
|
||||
).rejects.toThrow('Request failed');
|
||||
expect(commit.mock.calls).toEqual([
|
||||
['SET_LIST_LOADING_STATUS'],
|
||||
['CLEAR_LIST_LOADING_STATUS'],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user