chore: use useAbortableRequest for FAQ list fetches

This commit is contained in:
iamsivin
2026-07-23 17:13:26 +05:30
parent 8d068dbac6
commit bbd7a46618
5 changed files with 52 additions and 76 deletions
@@ -6,7 +6,7 @@ class CaptainFaqSuggestions extends ApiClient {
super('captain/faq_suggestions', { accountScoped: true });
}
get({ page = 1, search, assistantId, status = 'open' } = {}) {
get({ page = 1, search, assistantId, status = 'open', signal } = {}) {
return axios.get(this.url, {
params: {
page,
@@ -14,6 +14,7 @@ class CaptainFaqSuggestions extends ApiClient {
assistant_id: assistantId,
status,
},
signal,
});
}
@@ -6,7 +6,7 @@ class CaptainResponses extends ApiClient {
super('captain/assistant_responses', { accountScoped: true });
}
get({ page = 1, search, assistantId, documentId } = {}) {
get({ page = 1, search, assistantId, documentId, signal } = {}) {
return axios.get(this.url, {
params: {
page,
@@ -14,6 +14,7 @@ class CaptainResponses extends ApiClient {
assistant_id: assistantId,
document_id: documentId,
},
signal,
});
}
}
@@ -54,10 +54,12 @@ vi.mock('vue-router', async importOriginal => {
const deferred = () => {
let resolve;
const promise = new Promise(resolvePromise => {
let reject;
const promise = new Promise((resolvePromise, rejectPromise) => {
resolve = resolvePromise;
reject = rejectPromise;
});
return { promise, resolve };
return { promise, resolve, reject };
};
const PageLayoutStub = {
@@ -91,12 +93,20 @@ describe('FaqSuggestions', () => {
});
});
it('keeps the latest assistant results when requests finish in the wrong order', async () => {
it('keeps the latest assistant results when an older request is superseded', async () => {
const firstRequest = deferred();
const secondRequest = deferred();
mocks.apiGet
.mockReturnValueOnce(firstRequest.promise)
.mockReturnValueOnce(secondRequest.promise);
const queued = [firstRequest, secondRequest];
mocks.apiGet.mockImplementation(({ signal }) => {
const request = queued.shift();
signal.addEventListener('abort', () => {
const error = new Error('canceled');
error.name = 'CanceledError';
request.reject(error);
});
return request.promise;
});
const wrapper = shallowMount(FaqSuggestions, {
global: {
@@ -109,6 +119,7 @@ describe('FaqSuggestions', () => {
});
await flushPromises();
// Switching assistants aborts the first request before it resolves.
mocks.route.params.assistantId = 2;
await flushPromises();
@@ -120,14 +131,6 @@ describe('FaqSuggestions', () => {
});
await flushPromises();
firstRequest.resolve({
data: {
payload: [{ id: 1, question: 'Previous assistant' }],
meta: { page: 1, total_count: 1 },
},
});
await flushPromises();
expect(wrapper.text()).toContain('Current assistant');
expect(wrapper.text()).not.toContain('Previous assistant');
@@ -5,6 +5,7 @@ import { useRoute, useRouter } from 'vue-router';
import { debounce } from '@chatwoot/utils';
import { useAlert } from 'dashboard/composables';
import { useMapGetter, useStore } from 'dashboard/composables/store';
import { useAbortableRequest } from 'dashboard/composables/useAbortableRequest';
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
import CaptainFaqSuggestionsAPI from 'dashboard/api/captain/faqSuggestions';
@@ -51,47 +52,32 @@ const updateURL = (page, search) => {
router.replace({ query });
};
let suggestionsRequestId = 0;
let fetchingListRequestId = null;
const isCurrentSuggestionRequest = (requestId, assistantId) =>
requestId === suggestionsRequestId &&
assistantId === selectedAssistantId.value;
const { run: runListRequest, abort: abortListRequest } = useAbortableRequest();
const fetchSuggestions = async (page = 1) => {
suggestionsRequestId += 1;
const requestId = suggestionsRequestId;
const assistantId = selectedAssistantId.value;
updateURL(page, searchQuery.value);
fetchingListRequestId = requestId;
store.dispatch('captainFaqSuggestions/setFetchingList', true);
try {
const response = await CaptainFaqSuggestionsAPI.get({
page,
search: searchQuery.value,
assistantId,
});
const response = await runListRequest(signal =>
CaptainFaqSuggestionsAPI.get({
page,
search: searchQuery.value,
assistantId: selectedAssistantId.value,
signal,
})
);
if (!isCurrentSuggestionRequest(requestId, assistantId)) return [];
if (!response) return;
const { payload, meta } = response.data;
store.dispatch('captainFaqSuggestions/setRecords', {
records: payload,
meta,
records: response.data.payload,
meta: response.data.meta,
});
return payload;
store.dispatch('captainFaqSuggestions/setFetchingList', false);
} catch (error) {
if (isCurrentSuggestionRequest(requestId, assistantId)) {
useAlert(error?.message || t('CAPTAIN.FAQ_SUGGESTIONS.ERRORS.LOAD'));
}
return [];
} finally {
if (fetchingListRequestId === requestId) {
fetchingListRequestId = null;
store.dispatch('captainFaqSuggestions/setFetchingList', false);
}
useAlert(error?.message || t('CAPTAIN.FAQ_SUGGESTIONS.ERRORS.LOAD'));
store.dispatch('captainFaqSuggestions/setFetchingList', false);
}
};
@@ -150,7 +136,7 @@ const handleResolved = () => {
const debouncedSearch = debounce(() => fetchSuggestions(1), 500);
const handleSearchInput = () => {
suggestionsRequestId += 1;
abortListRequest();
debouncedSearch();
};
@@ -179,7 +165,6 @@ watch(
);
onUnmounted(() => {
suggestionsRequestId += 1;
store.dispatch('captainFaqSuggestions/setFetchingList', false);
});
</script>
@@ -2,6 +2,7 @@
import { computed, onUnmounted, ref, nextTick, watch } from 'vue';
import { useMapGetter, useStore } from 'dashboard/composables/store';
import { useAlert } from 'dashboard/composables';
import { useAbortableRequest } from 'dashboard/composables/useAbortableRequest';
import { useI18n } from 'vue-i18n';
import { useRouter, useRoute } from 'vue-router';
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
@@ -97,20 +98,13 @@ const updateURLWithFilters = (page, search) => {
router.replace({ query });
};
let responsesRequestId = 0;
let fetchingListRequestId = null;
const isCurrentResponseRequest = (requestId, assistantId) =>
requestId === responsesRequestId && assistantId === selectedAssistantId.value;
const { run: runListRequest, abort: abortListRequest } = useAbortableRequest();
const fetchResponses = async (page = 1) => {
responsesRequestId += 1;
const requestId = responsesRequestId;
const assistantId = selectedAssistantId.value;
const filterParams = { page };
if (assistantId) {
filterParams.assistantId = assistantId;
if (selectedAssistantId.value) {
filterParams.assistantId = selectedAssistantId.value;
}
if (searchQuery.value) {
filterParams.search = searchQuery.value;
@@ -119,30 +113,23 @@ const fetchResponses = async (page = 1) => {
// Update URL with current filters
updateURLWithFilters(page, searchQuery.value);
fetchingListRequestId = requestId;
store.dispatch('captainResponses/setFetchingList', true);
try {
const response = await CaptainResponseAPI.get(filterParams);
const response = await runListRequest(signal =>
CaptainResponseAPI.get({ ...filterParams, signal })
);
if (!isCurrentResponseRequest(requestId, assistantId)) return [];
if (!response) return;
const { payload, meta } = response.data;
store.dispatch('captainResponses/setRecords', {
records: payload,
meta,
records: response.data.payload,
meta: response.data.meta,
});
return payload;
store.dispatch('captainResponses/setFetchingList', false);
} catch (error) {
if (isCurrentResponseRequest(requestId, assistantId)) {
useAlert(error?.message || t('CAPTAIN.RESPONSES.ERRORS.LOAD'));
}
return [];
} finally {
if (fetchingListRequestId === requestId) {
fetchingListRequestId = null;
store.dispatch('captainResponses/setFetchingList', false);
}
useAlert(error?.message || t('CAPTAIN.RESPONSES.ERRORS.LOAD'));
store.dispatch('captainResponses/setFetchingList', false);
}
};
@@ -217,7 +204,7 @@ const debouncedSearch = debounce(async () => {
}, 500);
const handleSearchInput = () => {
responsesRequestId += 1;
abortListRequest();
debouncedSearch();
};
@@ -256,7 +243,6 @@ watch(
);
onUnmounted(() => {
responsesRequestId += 1;
store.dispatch('captainResponses/setFetchingList', false);
});
</script>