chore: use useAbortableRequest for FAQ list fetches
This commit is contained in:
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+17
-14
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user