feat: outbound voice call essentials (#12782)

- Enables outbound voice calls in voice channel . We are only caring
about wiring the logic to trigger outgoing calls to the call button
introduced in previous PRs. We will connect it to call component in
subsequent PRs

ref: #11602 

## Screens

<img width="2304" height="1202" alt="image"
src="https://github.com/user-attachments/assets/b91543a8-8d4e-4229-bd80-9727b42c7b0f"
/>

<img width="2304" height="1200" alt="image"
src="https://github.com/user-attachments/assets/1a1dad2a-8cb2-4aa2-9702-c062416556a7"
/>

---------

Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: iamsivin <iamsivin@gmail.com>
Co-authored-by: Vishnu Narayanan <vishnu@chatwoot.com>
This commit is contained in:
Sojan Jose
2025-11-24 17:47:00 -08:00
committed by GitHub
parent 6a712b7592
commit 48627da0f9
39 changed files with 1485 additions and 344 deletions

View File

@@ -302,4 +302,22 @@ export const actions = {
clearContactFilters({ commit }) {
commit(types.CLEAR_CONTACT_FILTERS);
},
initiateCall: async ({ commit }, { contactId, inboxId }) => {
commit(types.SET_CONTACT_UI_FLAG, { isInitiatingCall: true });
try {
const response = await ContactAPI.initiateCall(contactId, inboxId);
commit(types.SET_CONTACT_UI_FLAG, { isInitiatingCall: false });
return response.data;
} catch (error) {
commit(types.SET_CONTACT_UI_FLAG, { isInitiatingCall: false });
if (error.response?.data?.message) {
throw new ExceptionWithMessage(error.response.data.message);
} else if (error.response?.data?.error) {
throw new ExceptionWithMessage(error.response.data.error);
} else {
throw new Error(error);
}
}
},
};

View File

@@ -17,6 +17,7 @@ const state = {
isDeleting: false,
isExporting: false,
isImporting: false,
isInitiatingCall: false,
},
sortOrder: [],
appliedFilters: [],

View File

@@ -359,4 +359,83 @@ describe('#actions', () => {
).rejects.toThrow(Error);
});
});
describe('#initiateCall', () => {
const contactId = 123;
const inboxId = 456;
it('sends correct mutations if API is success', async () => {
const mockResponse = {
data: {
conversation_id: 789,
status: 'initiated',
},
};
axios.post.mockResolvedValue(mockResponse);
const result = await actions.initiateCall(
{ commit },
{ contactId, inboxId }
);
expect(commit.mock.calls).toEqual([
[types.SET_CONTACT_UI_FLAG, { isInitiatingCall: true }],
[types.SET_CONTACT_UI_FLAG, { isInitiatingCall: false }],
]);
expect(result).toEqual(mockResponse.data);
});
it('sends correct actions if API returns error with message', async () => {
const errorMessage = 'Failed to initiate call';
axios.post.mockRejectedValue({
response: {
data: {
message: errorMessage,
},
},
});
await expect(
actions.initiateCall({ commit }, { contactId, inboxId })
).rejects.toThrow(ExceptionWithMessage);
expect(commit.mock.calls).toEqual([
[types.SET_CONTACT_UI_FLAG, { isInitiatingCall: true }],
[types.SET_CONTACT_UI_FLAG, { isInitiatingCall: false }],
]);
});
it('sends correct actions if API returns error with error field', async () => {
const errorMessage = 'Call initiation error';
axios.post.mockRejectedValue({
response: {
data: {
error: errorMessage,
},
},
});
await expect(
actions.initiateCall({ commit }, { contactId, inboxId })
).rejects.toThrow(ExceptionWithMessage);
expect(commit.mock.calls).toEqual([
[types.SET_CONTACT_UI_FLAG, { isInitiatingCall: true }],
[types.SET_CONTACT_UI_FLAG, { isInitiatingCall: false }],
]);
});
it('sends correct actions if API returns generic error', async () => {
axios.post.mockRejectedValue({ message: 'Network error' });
await expect(
actions.initiateCall({ commit }, { contactId, inboxId })
).rejects.toThrow(Error);
expect(commit.mock.calls).toEqual([
[types.SET_CONTACT_UI_FLAG, { isInitiatingCall: true }],
[types.SET_CONTACT_UI_FLAG, { isInitiatingCall: false }],
]);
});
});
});