feat: Agent capacity policy Create/Edit pages (#12424)

# Pull Request Template

## Description

Fixes
https://linear.app/chatwoot/issue/CW-5573/feat-createedit-agent-capacity-policy-page

## Type of change

- [x] New feature (non-breaking change which adds functionality)

## How Has This Been Tested?

### Loom video

https://www.loom.com/share/8de9e3c5d8824cd998d242636540dd18?sid=1314536f-c8d6-41fd-8139-cae9bf94f942

### Screenshots

**Light mode**
<img width="1666" height="1225" alt="image"
src="https://github.com/user-attachments/assets/7e6d83a4-ce02-47a7-91f6-87745f8f5549"
/>
<img width="1666" height="1225" alt="image"
src="https://github.com/user-attachments/assets/7dd1f840-2e25-4365-aa1d-ed9dac13385a"
/>

**Dark mode**
<img width="1666" height="1225" alt="image"
src="https://github.com/user-attachments/assets/0c787095-7146-4fb3-a61a-e2232973bcba"
/>
<img width="1666" height="1225" alt="image"
src="https://github.com/user-attachments/assets/481c21fd-03b5-4c1f-b59e-7f8c8017f9ce"
/>


## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] 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

---------

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
This commit is contained in:
Sivin Varghese
2025-09-12 18:42:55 +05:30
committed by GitHub
parent 699731d351
commit ca579bd62a
21 changed files with 1965 additions and 33 deletions

View File

@@ -1,7 +1,12 @@
import axios from 'axios';
import { actions } from '../../agentCapacityPolicies';
import types from '../../../mutation-types';
import agentCapacityPoliciesList, { camelCaseFixtures } from './fixtures';
import agentCapacityPoliciesList, {
camelCaseFixtures,
mockUsers,
mockInboxLimits,
camelCaseMockInboxLimits,
} from './fixtures';
import camelcaseKeys from 'camelcase-keys';
import snakecaseKeys from 'snakecase-keys';
@@ -25,7 +30,9 @@ describe('#actions', () => {
await actions.get({ commit });
expect(camelcaseKeys).toHaveBeenCalledWith(agentCapacityPoliciesList);
expect(camelcaseKeys).toHaveBeenCalledWith(agentCapacityPoliciesList, {
deep: true,
});
expect(commit.mock.calls).toEqual([
[types.SET_AGENT_CAPACITY_POLICIES_UI_FLAG, { isFetching: true }],
[types.SET_AGENT_CAPACITY_POLICIES, camelCaseFixtures],
@@ -55,7 +62,9 @@ describe('#actions', () => {
await actions.show({ commit }, 1);
expect(camelcaseKeys).toHaveBeenCalledWith(policyData);
expect(camelcaseKeys).toHaveBeenCalledWith(policyData, {
deep: true,
});
expect(commit.mock.calls).toEqual([
[types.SET_AGENT_CAPACITY_POLICIES_UI_FLAG, { isFetchingItem: true }],
[types.SET_AGENT_CAPACITY_POLICY, camelCasedPolicy],
@@ -88,7 +97,9 @@ describe('#actions', () => {
const result = await actions.create({ commit }, newPolicy);
expect(snakecaseKeys).toHaveBeenCalledWith(newPolicy);
expect(camelcaseKeys).toHaveBeenCalledWith(newPolicy);
expect(camelcaseKeys).toHaveBeenCalledWith(newPolicy, {
deep: true,
});
expect(commit.mock.calls).toEqual([
[types.SET_AGENT_CAPACITY_POLICIES_UI_FLAG, { isCreating: true }],
[types.ADD_AGENT_CAPACITY_POLICY, camelCasedData],
@@ -129,7 +140,9 @@ describe('#actions', () => {
const result = await actions.update({ commit }, updateParams);
expect(snakecaseKeys).toHaveBeenCalledWith({ name: 'Updated Policy' });
expect(camelcaseKeys).toHaveBeenCalledWith(responseData);
expect(camelcaseKeys).toHaveBeenCalledWith(responseData, {
deep: true,
});
expect(commit.mock.calls).toEqual([
[types.SET_AGENT_CAPACITY_POLICIES_UI_FLAG, { isUpdating: true }],
[types.EDIT_AGENT_CAPACITY_POLICY, camelCasedData],
@@ -224,4 +237,172 @@ describe('#actions', () => {
]);
});
});
describe('#addUser', () => {
it('sends correct actions if API is success', async () => {
const policyId = 1;
const userData = { user_id: 3, capacity: 12 };
const responseData = mockUsers[2];
const camelCasedUser = mockUsers[2];
axios.post.mockResolvedValue({ data: responseData });
camelcaseKeys.mockReturnValue(camelCasedUser);
const result = await actions.addUser({ commit }, { policyId, userData });
expect(camelcaseKeys).toHaveBeenCalledWith(responseData);
expect(commit.mock.calls).toEqual([
[
types.ADD_AGENT_CAPACITY_POLICIES_USERS,
{ policyId, user: camelCasedUser },
],
]);
expect(result).toEqual(responseData);
});
it('sends correct actions if API is error', async () => {
axios.post.mockRejectedValue(new Error('Validation error'));
await expect(
actions.addUser({ commit }, { policyId: 1, userData: {} })
).rejects.toThrow(Error);
expect(commit).not.toHaveBeenCalled();
});
});
describe('#removeUser', () => {
it('sends correct actions if API is success', async () => {
const policyId = 1;
const userId = 2;
axios.delete.mockResolvedValue({});
await actions.removeUser({ commit }, { policyId, userId });
expect(commit.mock.calls).toEqual([
[types.SET_AGENT_CAPACITY_POLICIES_USERS_UI_FLAG, { isDeleting: true }],
[types.DELETE_AGENT_CAPACITY_POLICIES_USERS, { policyId, userId }],
[
types.SET_AGENT_CAPACITY_POLICIES_USERS_UI_FLAG,
{ isDeleting: false },
],
]);
});
it('sends correct actions if API is error', async () => {
axios.delete.mockRejectedValue(new Error('Not found'));
await expect(
actions.removeUser({ commit }, { policyId: 1, userId: 2 })
).rejects.toThrow(Error);
expect(commit.mock.calls).toEqual([
[types.SET_AGENT_CAPACITY_POLICIES_USERS_UI_FLAG, { isDeleting: true }],
[
types.SET_AGENT_CAPACITY_POLICIES_USERS_UI_FLAG,
{ isDeleting: false },
],
]);
});
});
describe('#createInboxLimit', () => {
it('sends correct actions if API is success', async () => {
const policyId = 1;
const limitData = { inbox_id: 3, conversation_limit: 20 };
const responseData = mockInboxLimits[2];
const camelCasedData = camelCaseMockInboxLimits[2];
axios.post.mockResolvedValue({ data: responseData });
camelcaseKeys.mockReturnValue(camelCasedData);
const result = await actions.createInboxLimit(
{ commit },
{ policyId, limitData }
);
expect(camelcaseKeys).toHaveBeenCalledWith(responseData);
expect(commit.mock.calls).toEqual([
[types.SET_AGENT_CAPACITY_POLICIES_INBOXES, camelCasedData],
]);
expect(result).toEqual(responseData);
});
it('sends correct actions if API is error', async () => {
axios.post.mockRejectedValue(new Error('Validation error'));
await expect(
actions.createInboxLimit({ commit }, { policyId: 1, limitData: {} })
).rejects.toThrow(Error);
expect(commit).not.toHaveBeenCalled();
});
});
describe('#updateInboxLimit', () => {
it('sends correct actions if API is success', async () => {
const policyId = 1;
const limitId = 1;
const limitData = { conversation_limit: 25 };
const responseData = {
...mockInboxLimits[0],
conversation_limit: 25,
};
const camelCasedData = {
...camelCaseMockInboxLimits[0],
conversationLimit: 25,
};
axios.put.mockResolvedValue({ data: responseData });
camelcaseKeys.mockReturnValue(camelCasedData);
const result = await actions.updateInboxLimit(
{ commit },
{ policyId, limitId, limitData }
);
expect(camelcaseKeys).toHaveBeenCalledWith(responseData);
expect(commit.mock.calls).toEqual([
[types.EDIT_AGENT_CAPACITY_POLICIES_INBOXES, camelCasedData],
]);
expect(result).toEqual(responseData);
});
it('sends correct actions if API is error', async () => {
axios.put.mockRejectedValue(new Error('Validation error'));
await expect(
actions.updateInboxLimit(
{ commit },
{ policyId: 1, limitId: 1, limitData: {} }
)
).rejects.toThrow(Error);
expect(commit).not.toHaveBeenCalled();
});
});
describe('#deleteInboxLimit', () => {
it('sends correct actions if API is success', async () => {
const policyId = 1;
const limitId = 1;
axios.delete.mockResolvedValue({});
await actions.deleteInboxLimit({ commit }, { policyId, limitId });
expect(commit.mock.calls).toEqual([
[types.DELETE_AGENT_CAPACITY_POLICIES_INBOXES, { policyId, limitId }],
]);
});
it('sends correct actions if API is error', async () => {
axios.delete.mockRejectedValue(new Error('Not found'));
await expect(
actions.deleteInboxLimit({ commit }, { policyId: 1, limitId: 1 })
).rejects.toThrow(Error);
expect(commit).not.toHaveBeenCalled();
});
});
});

View File

@@ -10,6 +10,20 @@ export default [
created_at: '2024-01-01T10:00:00.000Z',
updated_at: '2024-01-01T10:00:00.000Z',
users: [],
inbox_capacity_limits: [
{
id: 1,
inbox_id: 1,
conversation_limit: 15,
agent_capacity_policy_id: 1,
},
{
id: 2,
inbox_id: 2,
conversation_limit: 8,
agent_capacity_policy_id: 1,
},
],
},
{
id: 2,
@@ -21,7 +35,21 @@ export default [
assigned_agent_count: 5,
created_at: '2024-01-01T11:00:00.000Z',
updated_at: '2024-01-01T11:00:00.000Z',
users: [],
users: [
{
id: 1,
name: 'Agent Smith',
email: 'agent.smith@example.com',
capacity: 25,
},
{
id: 2,
name: 'Agent Johnson',
email: 'agent.johnson@example.com',
capacity: 18,
},
],
inbox_capacity_limits: [],
},
{
id: 3,
@@ -34,6 +62,7 @@ export default [
created_at: '2024-01-01T12:00:00.000Z',
updated_at: '2024-01-01T12:00:00.000Z',
users: [],
inbox_capacity_limits: [],
},
];
@@ -49,6 +78,20 @@ export const camelCaseFixtures = [
createdAt: '2024-01-01T10:00:00.000Z',
updatedAt: '2024-01-01T10:00:00.000Z',
users: [],
inboxCapacityLimits: [
{
id: 1,
inboxId: 1,
conversationLimit: 15,
agentCapacityPolicyId: 1,
},
{
id: 2,
inboxId: 2,
conversationLimit: 8,
agentCapacityPolicyId: 1,
},
],
},
{
id: 2,
@@ -60,7 +103,21 @@ export const camelCaseFixtures = [
assignedAgentCount: 5,
createdAt: '2024-01-01T11:00:00.000Z',
updatedAt: '2024-01-01T11:00:00.000Z',
users: [],
users: [
{
id: 1,
name: 'Agent Smith',
email: 'agent.smith@example.com',
capacity: 25,
},
{
id: 2,
name: 'Agent Johnson',
email: 'agent.johnson@example.com',
capacity: 18,
},
],
inboxCapacityLimits: [],
},
{
id: 3,
@@ -73,5 +130,70 @@ export const camelCaseFixtures = [
createdAt: '2024-01-01T12:00:00.000Z',
updatedAt: '2024-01-01T12:00:00.000Z',
users: [],
inboxCapacityLimits: [],
},
];
// Additional test data for user and inbox limit operations
export const mockUsers = [
{
id: 1,
name: 'Agent Smith',
email: 'agent.smith@example.com',
capacity: 25,
},
{
id: 2,
name: 'Agent Johnson',
email: 'agent.johnson@example.com',
capacity: 18,
},
{
id: 3,
name: 'Agent Brown',
email: 'agent.brown@example.com',
capacity: 12,
},
];
export const mockInboxLimits = [
{
id: 1,
inbox_id: 1,
conversation_limit: 15,
agent_capacity_policy_id: 1,
},
{
id: 2,
inbox_id: 2,
conversation_limit: 8,
agent_capacity_policy_id: 1,
},
{
id: 3,
inbox_id: 3,
conversation_limit: 20,
agent_capacity_policy_id: 2,
},
];
export const camelCaseMockInboxLimits = [
{
id: 1,
inboxId: 1,
conversationLimit: 15,
agentCapacityPolicyId: 1,
},
{
id: 2,
inboxId: 2,
conversationLimit: 8,
agentCapacityPolicyId: 1,
},
{
id: 3,
inboxId: 3,
conversationLimit: 20,
agentCapacityPolicyId: 2,
},
];

View File

@@ -1,6 +1,6 @@
import { mutations } from '../../agentCapacityPolicies';
import types from '../../../mutation-types';
import agentCapacityPoliciesList from './fixtures';
import agentCapacityPoliciesList, { mockUsers } from './fixtures';
describe('#mutations', () => {
describe('#SET_AGENT_CAPACITY_POLICIES_UI_FLAG', () => {
@@ -248,7 +248,7 @@ describe('#mutations', () => {
describe('#SET_AGENT_CAPACITY_POLICIES_USERS', () => {
it('sets users for existing policy', () => {
const mockUsers = [
const testUsers = [
{ id: 1, name: 'Agent 1', email: 'agent1@example.com', capacity: 15 },
{ id: 2, name: 'Agent 2', email: 'agent2@example.com', capacity: 20 },
];
@@ -262,10 +262,10 @@ describe('#mutations', () => {
mutations[types.SET_AGENT_CAPACITY_POLICIES_USERS](state, {
policyId: 1,
users: mockUsers,
users: testUsers,
});
expect(state.records[0].users).toEqual(mockUsers);
expect(state.records[0].users).toEqual(testUsers);
expect(state.records[1].users).toEqual([]);
});
@@ -300,4 +300,320 @@ describe('#mutations', () => {
expect(state).toEqual(originalState);
});
});
describe('#ADD_AGENT_CAPACITY_POLICIES_USERS', () => {
it('adds user to existing policy', () => {
const state = {
records: [
{ id: 1, name: 'Policy 1', users: [] },
{ id: 2, name: 'Policy 2', users: [] },
],
};
mutations[types.ADD_AGENT_CAPACITY_POLICIES_USERS](state, {
policyId: 1,
user: mockUsers[0],
});
expect(state.records[0].users).toEqual([mockUsers[0]]);
expect(state.records[1].users).toEqual([]);
});
it('adds user to policy with existing users', () => {
const state = {
records: [{ id: 1, name: 'Policy 1', users: [mockUsers[0]] }],
};
mutations[types.ADD_AGENT_CAPACITY_POLICIES_USERS](state, {
policyId: 1,
user: mockUsers[1],
});
expect(state.records[0].users).toEqual([mockUsers[0], mockUsers[1]]);
});
it('initializes users array if undefined', () => {
const state = {
records: [{ id: 1, name: 'Policy 1' }],
};
mutations[types.ADD_AGENT_CAPACITY_POLICIES_USERS](state, {
policyId: 1,
user: mockUsers[0],
});
expect(state.records[0].users).toEqual([mockUsers[0]]);
});
it('updates assigned agent count', () => {
const state = {
records: [{ id: 1, name: 'Policy 1', users: [] }],
};
mutations[types.ADD_AGENT_CAPACITY_POLICIES_USERS](state, {
policyId: 1,
user: mockUsers[0],
});
expect(state.records[0].assignedAgentCount).toEqual(1);
});
});
describe('#DELETE_AGENT_CAPACITY_POLICIES_USERS', () => {
it('removes user from policy', () => {
const state = {
records: [
{
id: 1,
name: 'Policy 1',
users: [mockUsers[0], mockUsers[1], mockUsers[2]],
},
],
};
mutations[types.DELETE_AGENT_CAPACITY_POLICIES_USERS](state, {
policyId: 1,
userId: 2,
});
expect(state.records[0].users).toEqual([mockUsers[0], mockUsers[2]]);
});
it('handles removing non-existent user', () => {
const state = {
records: [
{
id: 1,
name: 'Policy 1',
users: [mockUsers[0]],
},
],
};
mutations[types.DELETE_AGENT_CAPACITY_POLICIES_USERS](state, {
policyId: 1,
userId: 999,
});
expect(state.records[0].users).toEqual([mockUsers[0]]);
});
it('updates assigned agent count', () => {
const state = {
records: [{ id: 1, name: 'Policy 1', users: [mockUsers[0]] }],
};
mutations[types.DELETE_AGENT_CAPACITY_POLICIES_USERS](state, {
policyId: 1,
userId: 1,
});
expect(state.records[0].assignedAgentCount).toEqual(0);
});
});
describe('#SET_AGENT_CAPACITY_POLICIES_INBOXES', () => {
it('adds inbox limit to policy', () => {
const state = {
records: [
{
id: 1,
name: 'Policy 1',
inboxCapacityLimits: [],
},
],
};
const inboxLimitData = {
id: 1,
inboxId: 1,
conversationLimit: 15,
agentCapacityPolicyId: 1,
};
mutations[types.SET_AGENT_CAPACITY_POLICIES_INBOXES](
state,
inboxLimitData
);
expect(state.records[0].inboxCapacityLimits).toEqual([
{
id: 1,
inboxId: 1,
conversationLimit: 15,
},
]);
});
it('does nothing if policy not found', () => {
const state = {
records: [{ id: 1, name: 'Policy 1', inboxCapacityLimits: [] }],
};
const originalState = JSON.parse(JSON.stringify(state));
mutations[types.SET_AGENT_CAPACITY_POLICIES_INBOXES](state, {
id: 1,
inboxId: 1,
conversationLimit: 15,
agentCapacityPolicyId: 999,
});
expect(state).toEqual(originalState);
});
});
describe('#EDIT_AGENT_CAPACITY_POLICIES_INBOXES', () => {
it('updates existing inbox limit', () => {
const state = {
records: [
{
id: 1,
name: 'Policy 1',
inboxCapacityLimits: [
{
id: 1,
inboxId: 1,
conversationLimit: 15,
},
{
id: 2,
inboxId: 2,
conversationLimit: 8,
},
],
},
],
};
mutations[types.EDIT_AGENT_CAPACITY_POLICIES_INBOXES](state, {
id: 1,
inboxId: 1,
conversationLimit: 25,
agentCapacityPolicyId: 1,
});
expect(state.records[0].inboxCapacityLimits[0]).toEqual({
id: 1,
inboxId: 1,
conversationLimit: 25,
});
expect(state.records[0].inboxCapacityLimits[1]).toEqual({
id: 2,
inboxId: 2,
conversationLimit: 8,
});
});
it('does nothing if limit not found', () => {
const state = {
records: [
{
id: 1,
name: 'Policy 1',
inboxCapacityLimits: [
{
id: 1,
inboxId: 1,
conversationLimit: 15,
},
],
},
],
};
const originalLimits = [...state.records[0].inboxCapacityLimits];
mutations[types.EDIT_AGENT_CAPACITY_POLICIES_INBOXES](state, {
id: 999,
inboxId: 1,
conversationLimit: 25,
agentCapacityPolicyId: 1,
});
expect(state.records[0].inboxCapacityLimits).toEqual(originalLimits);
});
it('does nothing if policy not found', () => {
const state = {
records: [{ id: 1, name: 'Policy 1', inboxCapacityLimits: [] }],
};
const originalState = JSON.parse(JSON.stringify(state));
mutations[types.EDIT_AGENT_CAPACITY_POLICIES_INBOXES](state, {
id: 1,
inboxId: 1,
conversationLimit: 25,
agentCapacityPolicyId: 999,
});
expect(state).toEqual(originalState);
});
});
describe('#DELETE_AGENT_CAPACITY_POLICIES_INBOXES', () => {
it('removes inbox limit from policy', () => {
const state = {
records: [
{
id: 1,
name: 'Policy 1',
inboxCapacityLimits: [
{
id: 1,
inboxId: 1,
conversationLimit: 15,
},
{
id: 2,
inboxId: 2,
conversationLimit: 8,
},
],
},
],
};
mutations[types.DELETE_AGENT_CAPACITY_POLICIES_INBOXES](state, {
policyId: 1,
limitId: 1,
});
expect(state.records[0].inboxCapacityLimits).toEqual([
{
id: 2,
inboxId: 2,
conversationLimit: 8,
},
]);
});
it('handles removing non-existent limit', () => {
const state = {
records: [
{
id: 1,
name: 'Policy 1',
inboxCapacityLimits: [
{
id: 1,
inboxId: 1,
conversationLimit: 15,
},
],
},
],
};
const originalLimits = [...state.records[0].inboxCapacityLimits];
mutations[types.DELETE_AGENT_CAPACITY_POLICIES_INBOXES](state, {
policyId: 1,
limitId: 999,
});
expect(state.records[0].inboxCapacityLimits).toEqual(originalLimits);
});
});
});