feat: introduce voice call dashboard (#14954)
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
import camelcaseKeys from 'camelcase-keys';
|
||||
import CallsAPI from 'dashboard/api/calls';
|
||||
import { throwErrorMessage } from 'dashboard/store/utils/api';
|
||||
import { defineStore } from 'pinia';
|
||||
|
||||
export const useCallHistoryStore = defineStore('callHistory', {
|
||||
state: () => ({
|
||||
records: [],
|
||||
meta: { count: 0, currentPage: 1, totalPages: 0 },
|
||||
uiFlags: { isFetching: false },
|
||||
fetchRequestToken: 0,
|
||||
}),
|
||||
|
||||
actions: {
|
||||
async fetchCalls(params = {}) {
|
||||
this.uiFlags.isFetching = true;
|
||||
this.fetchRequestToken += 1;
|
||||
const requestToken = this.fetchRequestToken;
|
||||
try {
|
||||
const { data } = await CallsAPI.get(params);
|
||||
// A newer fetch (filter/page change) superseded this one; drop the result.
|
||||
if (this.fetchRequestToken !== requestToken) return this.records;
|
||||
this.records = camelcaseKeys(data.payload, { deep: true });
|
||||
this.meta = camelcaseKeys(data.meta);
|
||||
return this.records;
|
||||
} catch (error) {
|
||||
// Don't surface errors from a fetch that a newer request already replaced.
|
||||
if (this.fetchRequestToken !== requestToken) return this.records;
|
||||
// Drop the previous results so stale rows aren't shown under the new view.
|
||||
this.records = [];
|
||||
this.meta = { count: 0, currentPage: 1, totalPages: 0 };
|
||||
return throwErrorMessage(error);
|
||||
} finally {
|
||||
if (this.fetchRequestToken === requestToken) {
|
||||
this.uiFlags.isFetching = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,117 @@
|
||||
import { setActivePinia, createPinia } from 'pinia';
|
||||
import CallsAPI from 'dashboard/api/calls';
|
||||
import { throwErrorMessage } from 'dashboard/store/utils/api';
|
||||
import { useCallHistoryStore } from '../callHistory';
|
||||
|
||||
vi.mock('dashboard/api/calls', () => ({
|
||||
default: {
|
||||
get: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('dashboard/store/utils/api', () => ({
|
||||
throwErrorMessage: vi.fn(error => error),
|
||||
}));
|
||||
|
||||
const createDeferred = () => {
|
||||
let resolve;
|
||||
const promise = new Promise(res => {
|
||||
resolve = res;
|
||||
});
|
||||
|
||||
return { promise, resolve };
|
||||
};
|
||||
|
||||
const buildResponse = (payload, meta) => ({ data: { payload, meta } });
|
||||
|
||||
describe('callHistory store', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia());
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('fetches calls and stores camelized records and meta', async () => {
|
||||
CallsAPI.get.mockResolvedValue(
|
||||
buildResponse(
|
||||
[{ id: 1, recording_url: 'rec.mp3', contact: { phone_number: '+1' } }],
|
||||
{ count: 44, current_page: 1, total_pages: 2 }
|
||||
)
|
||||
);
|
||||
const store = useCallHistoryStore();
|
||||
|
||||
await store.fetchCalls({ page: 1, status: 'no-answer' });
|
||||
|
||||
expect(CallsAPI.get).toHaveBeenCalledWith({ page: 1, status: 'no-answer' });
|
||||
expect(store.records).toEqual([
|
||||
{ id: 1, recordingUrl: 'rec.mp3', contact: { phoneNumber: '+1' } },
|
||||
]);
|
||||
expect(store.meta).toEqual({ count: 44, currentPage: 1, totalPages: 2 });
|
||||
expect(store.uiFlags.isFetching).toBe(false);
|
||||
});
|
||||
|
||||
it('drops a superseded response that resolves after the latest one', async () => {
|
||||
const firstRequest = createDeferred();
|
||||
const secondRequest = createDeferred();
|
||||
CallsAPI.get
|
||||
.mockImplementationOnce(() => firstRequest.promise)
|
||||
.mockImplementationOnce(() => secondRequest.promise);
|
||||
const store = useCallHistoryStore();
|
||||
|
||||
const staleFetch = store.fetchCalls({ page: 1 });
|
||||
const currentFetch = store.fetchCalls({ page: 2 });
|
||||
|
||||
secondRequest.resolve(
|
||||
buildResponse([{ id: 2 }], { count: 1, current_page: 2, total_pages: 2 })
|
||||
);
|
||||
await currentFetch;
|
||||
|
||||
firstRequest.resolve(
|
||||
buildResponse([{ id: 1 }], { count: 99, current_page: 1, total_pages: 9 })
|
||||
);
|
||||
await staleFetch;
|
||||
|
||||
expect(store.records).toEqual([{ id: 2 }]);
|
||||
expect(store.meta.count).toBe(1);
|
||||
expect(store.uiFlags.isFetching).toBe(false);
|
||||
});
|
||||
|
||||
it('keeps fetching state when a superseded response resolves first', async () => {
|
||||
const firstRequest = createDeferred();
|
||||
const secondRequest = createDeferred();
|
||||
CallsAPI.get
|
||||
.mockImplementationOnce(() => firstRequest.promise)
|
||||
.mockImplementationOnce(() => secondRequest.promise);
|
||||
const store = useCallHistoryStore();
|
||||
|
||||
const staleFetch = store.fetchCalls({ page: 1 });
|
||||
const currentFetch = store.fetchCalls({ page: 2 });
|
||||
|
||||
firstRequest.resolve(
|
||||
buildResponse([{ id: 1 }], { count: 99, current_page: 1, total_pages: 9 })
|
||||
);
|
||||
await staleFetch;
|
||||
|
||||
expect(store.records).toEqual([]);
|
||||
expect(store.uiFlags.isFetching).toBe(true);
|
||||
|
||||
secondRequest.resolve(
|
||||
buildResponse([{ id: 2 }], { count: 1, current_page: 2, total_pages: 2 })
|
||||
);
|
||||
await currentFetch;
|
||||
|
||||
expect(store.records).toEqual([{ id: 2 }]);
|
||||
expect(store.uiFlags.isFetching).toBe(false);
|
||||
});
|
||||
|
||||
it('surfaces the error and resets fetching state on failure', async () => {
|
||||
const error = new Error('Request failed');
|
||||
CallsAPI.get.mockRejectedValue(error);
|
||||
const store = useCallHistoryStore();
|
||||
|
||||
await store.fetchCalls();
|
||||
|
||||
expect(throwErrorMessage).toHaveBeenCalledWith(error);
|
||||
expect(store.records).toEqual([]);
|
||||
expect(store.uiFlags.isFetching).toBe(false);
|
||||
});
|
||||
});
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import { setActivePinia, createPinia } from 'pinia';
|
||||
import CompanyAPI from 'dashboard/api/companies';
|
||||
import { useCompaniesStore } from './companies';
|
||||
import { useCompaniesStore } from '../companies';
|
||||
|
||||
vi.mock('dashboard/api/companies', () => ({
|
||||
default: {
|
||||
Reference in New Issue
Block a user