diff --git a/app/javascript/dashboard/api/assignmentPolicies.js b/app/javascript/dashboard/api/assignmentPolicies.js new file mode 100644 index 000000000..e6baca97a --- /dev/null +++ b/app/javascript/dashboard/api/assignmentPolicies.js @@ -0,0 +1,36 @@ +/* global axios */ + +import ApiClient from './ApiClient'; + +class AssignmentPolicies extends ApiClient { + constructor() { + super('assignment_policies', { accountScoped: true }); + } + + getInboxes(policyId) { + return axios.get(`${this.url}/${policyId}/inboxes`); + } + + setInboxPolicy(inboxId, policyId) { + return axios.post( + `/api/v1/accounts/${this.accountIdFromRoute}/inboxes/${inboxId}/assignment_policy`, + { + assignment_policy_id: policyId, + } + ); + } + + getInboxPolicy(inboxId) { + return axios.get( + `/api/v1/accounts/${this.accountIdFromRoute}/inboxes/${inboxId}/assignment_policy` + ); + } + + removeInboxPolicy(inboxId) { + return axios.delete( + `/api/v1/accounts/${this.accountIdFromRoute}/inboxes/${inboxId}/assignment_policy` + ); + } +} + +export default new AssignmentPolicies(); diff --git a/app/javascript/dashboard/api/specs/assignmentPolicies.spec.js b/app/javascript/dashboard/api/specs/assignmentPolicies.spec.js new file mode 100644 index 000000000..8d0aea7d0 --- /dev/null +++ b/app/javascript/dashboard/api/specs/assignmentPolicies.spec.js @@ -0,0 +1,70 @@ +import assignmentPolicies from '../assignmentPolicies'; +import ApiClient from '../ApiClient'; + +describe('#AssignmentPoliciesAPI', () => { + it('creates correct instance', () => { + expect(assignmentPolicies).toBeInstanceOf(ApiClient); + expect(assignmentPolicies).toHaveProperty('get'); + expect(assignmentPolicies).toHaveProperty('show'); + expect(assignmentPolicies).toHaveProperty('create'); + expect(assignmentPolicies).toHaveProperty('update'); + expect(assignmentPolicies).toHaveProperty('delete'); + expect(assignmentPolicies).toHaveProperty('getInboxes'); + expect(assignmentPolicies).toHaveProperty('setInboxPolicy'); + expect(assignmentPolicies).toHaveProperty('getInboxPolicy'); + expect(assignmentPolicies).toHaveProperty('removeInboxPolicy'); + }); + + describe('API calls', () => { + const originalAxios = window.axios; + const axiosMock = { + get: vi.fn(() => Promise.resolve()), + post: vi.fn(() => Promise.resolve()), + delete: vi.fn(() => Promise.resolve()), + }; + + beforeEach(() => { + window.axios = axiosMock; + // Mock accountIdFromRoute + Object.defineProperty(assignmentPolicies, 'accountIdFromRoute', { + get: () => '1', + configurable: true, + }); + }); + + afterEach(() => { + window.axios = originalAxios; + }); + + it('#getInboxes', () => { + assignmentPolicies.getInboxes(123); + expect(axiosMock.get).toHaveBeenCalledWith( + '/api/v1/accounts/1/assignment_policies/123/inboxes' + ); + }); + + it('#setInboxPolicy', () => { + assignmentPolicies.setInboxPolicy(456, 123); + expect(axiosMock.post).toHaveBeenCalledWith( + '/api/v1/accounts/1/inboxes/456/assignment_policy', + { + assignment_policy_id: 123, + } + ); + }); + + it('#getInboxPolicy', () => { + assignmentPolicies.getInboxPolicy(456); + expect(axiosMock.get).toHaveBeenCalledWith( + '/api/v1/accounts/1/inboxes/456/assignment_policy' + ); + }); + + it('#removeInboxPolicy', () => { + assignmentPolicies.removeInboxPolicy(456); + expect(axiosMock.delete).toHaveBeenCalledWith( + '/api/v1/accounts/1/inboxes/456/assignment_policy' + ); + }); + }); +}); diff --git a/app/javascript/dashboard/components-next/AssignmentPolicy/AssignmentPolicyCard/AssignmentPolicyCard.story.vue b/app/javascript/dashboard/components-next/AssignmentPolicy/AssignmentPolicyCard/AssignmentPolicyCard.story.vue new file mode 100644 index 000000000..cd6f1d49b --- /dev/null +++ b/app/javascript/dashboard/components-next/AssignmentPolicy/AssignmentPolicyCard/AssignmentPolicyCard.story.vue @@ -0,0 +1,104 @@ + + + diff --git a/app/javascript/dashboard/components-next/AssignmentPolicy/AssignmentPolicyCard/AssignmentPolicyCard.vue b/app/javascript/dashboard/components-next/AssignmentPolicy/AssignmentPolicyCard/AssignmentPolicyCard.vue new file mode 100644 index 000000000..fe9965777 --- /dev/null +++ b/app/javascript/dashboard/components-next/AssignmentPolicy/AssignmentPolicyCard/AssignmentPolicyCard.vue @@ -0,0 +1,133 @@ + + + diff --git a/app/javascript/dashboard/components-next/AssignmentPolicy/components/CardPopover.vue b/app/javascript/dashboard/components-next/AssignmentPolicy/components/CardPopover.vue new file mode 100644 index 000000000..013e6d5fe --- /dev/null +++ b/app/javascript/dashboard/components-next/AssignmentPolicy/components/CardPopover.vue @@ -0,0 +1,103 @@ + + + diff --git a/app/javascript/dashboard/components-next/AssignmentPolicy/components/story/CardPopover.story.vue b/app/javascript/dashboard/components-next/AssignmentPolicy/components/story/CardPopover.story.vue new file mode 100644 index 000000000..d010c16f9 --- /dev/null +++ b/app/javascript/dashboard/components-next/AssignmentPolicy/components/story/CardPopover.story.vue @@ -0,0 +1,45 @@ + + + diff --git a/app/javascript/dashboard/helper/commons.js b/app/javascript/dashboard/helper/commons.js index b12d1aa3d..3be7538bb 100644 --- a/app/javascript/dashboard/helper/commons.js +++ b/app/javascript/dashboard/helper/commons.js @@ -96,3 +96,18 @@ export const sanitizeVariableSearchKey = (searchKey = '') => { .replace(/,/g, '') // remove commas .trim(); }; + +/** + * Convert underscore-separated string to title case. + * Eg. "round_robin" => "Round Robin" + * @param {string} str + * @returns {string} + */ +export const formatToTitleCase = str => { + return ( + str + ?.replace(/_/g, ' ') + .replace(/\b\w/g, l => l.toUpperCase()) + .trim() || '' + ); +}; diff --git a/app/javascript/dashboard/helper/specs/commons.spec.js b/app/javascript/dashboard/helper/specs/commons.spec.js index 466cdcf45..d892d3a94 100644 --- a/app/javascript/dashboard/helper/specs/commons.spec.js +++ b/app/javascript/dashboard/helper/specs/commons.spec.js @@ -5,6 +5,7 @@ import { convertToCategorySlug, convertToPortalSlug, sanitizeVariableSearchKey, + formatToTitleCase, } from '../commons'; describe('#getTypingUsersText', () => { @@ -142,3 +143,51 @@ describe('sanitizeVariableSearchKey', () => { expect(sanitizeVariableSearchKey()).toBe(''); }); }); + +describe('formatToTitleCase', () => { + it('converts underscore-separated string to title case', () => { + expect(formatToTitleCase('round_robin')).toBe('Round Robin'); + }); + + it('converts single word to title case', () => { + expect(formatToTitleCase('priority')).toBe('Priority'); + }); + + it('converts multiple underscores to title case', () => { + expect(formatToTitleCase('auto_assignment_policy')).toBe( + 'Auto Assignment Policy' + ); + }); + + it('handles already capitalized words', () => { + expect(formatToTitleCase('HIGH_PRIORITY')).toBe('HIGH PRIORITY'); + }); + + it('handles mixed case with underscores', () => { + expect(formatToTitleCase('first_Name_last')).toBe('First Name Last'); + }); + + it('handles empty string', () => { + expect(formatToTitleCase('')).toBe(''); + }); + + it('handles null input', () => { + expect(formatToTitleCase(null)).toBe(''); + }); + + it('handles undefined input', () => { + expect(formatToTitleCase(undefined)).toBe(''); + }); + + it('handles string without underscores', () => { + expect(formatToTitleCase('hello')).toBe('Hello'); + }); + + it('handles string with numbers', () => { + expect(formatToTitleCase('priority_1_high')).toBe('Priority 1 High'); + }); + + it('handles leading and trailing underscores', () => { + expect(formatToTitleCase('_leading_trailing_')).toBe('Leading Trailing'); + }); +}); diff --git a/app/javascript/dashboard/i18n/locale/en/settings.json b/app/javascript/dashboard/i18n/locale/en/settings.json index 2e24bace4..c0367ea9f 100644 --- a/app/javascript/dashboard/i18n/locale/en/settings.json +++ b/app/javascript/dashboard/i18n/locale/en/settings.json @@ -451,6 +451,31 @@ "Add agents to a policy - one policy per agent" ] } + }, + "AGENT_ASSIGNMENT_POLICY": { + "INDEX": { + "HEADER": { + "TITLE": "Assignment policy", + "CREATE_POLICY": "New policy" + }, + "CARD": { + "ORDER": "Order", + "PRIORITY": "Priority", + "ACTIVE": "Active", + "INACTIVE": "Inactive", + "POPOVER": "Added inboxes", + "EDIT": "Edit" + }, + "NO_RECORDS_FOUND": "No assignment policies found" + }, + "DELETE_POLICY": { + "TITLE": "Delete policy", + "DESCRIPTION": "Are you sure you want to delete this policy? This action cannot be undone.", + "CONFIRM_BUTTON_LABEL": "Delete", + "CANCEL_BUTTON_LABEL": "Cancel", + "SUCCESS_MESSAGE": "Assignment policy deleted successfully", + "ERROR_MESSAGE": "Failed to delete assignment policy" + } } } } diff --git a/app/javascript/dashboard/routes/dashboard/settings/SettingsLayout.vue b/app/javascript/dashboard/routes/dashboard/settings/SettingsLayout.vue index 44bad28c1..9e34cb384 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/SettingsLayout.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/SettingsLayout.vue @@ -23,7 +23,7 @@ defineProps({
-
+
@@ -37,6 +37,6 @@ defineProps({ -
+
diff --git a/app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/Index.vue b/app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/Index.vue index 6b0f88033..b41d9990a 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/Index.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/Index.vue @@ -11,7 +11,7 @@ const { t } = useI18n(); const agentAssignments = computed(() => [ { - key: 'assignment_policy', + key: 'agent_assignment_policy_index', title: t('ASSIGNMENT_POLICY.INDEX.ASSIGNMENT_POLICY.TITLE'), description: t('ASSIGNMENT_POLICY.INDEX.ASSIGNMENT_POLICY.DESCRIPTION'), features: [ diff --git a/app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/assignmentPolicy.routes.js b/app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/assignmentPolicy.routes.js index 2d62674b0..6b4c35da3 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/assignmentPolicy.routes.js +++ b/app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/assignmentPolicy.routes.js @@ -2,6 +2,7 @@ import { FEATURE_FLAGS } from '../../../../featureFlags'; import { frontendURL } from '../../../../helper/URLHelper'; import SettingsWrapper from '../SettingsWrapper.vue'; import AssignmentPolicyIndex from './Index.vue'; +import AgentAssignmentIndex from './pages/AgentAssignmentIndexPage.vue'; export default { routes: [ @@ -24,6 +25,15 @@ export default { permissions: ['administrator'], }, }, + { + path: 'assignment', + name: 'agent_assignment_policy_index', + component: AgentAssignmentIndex, + meta: { + featureFlag: FEATURE_FLAGS.ASSIGNMENT_V2, + permissions: ['administrator'], + }, + }, ], }, ], diff --git a/app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/pages/AgentAssignmentIndexPage.vue b/app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/pages/AgentAssignmentIndexPage.vue new file mode 100644 index 000000000..e931d6bbd --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/pages/AgentAssignmentIndexPage.vue @@ -0,0 +1,118 @@ + + + diff --git a/app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/pages/components/ConfirmDeletePolicyDialog.vue b/app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/pages/components/ConfirmDeletePolicyDialog.vue new file mode 100644 index 000000000..8cf13bcd5 --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/pages/components/ConfirmDeletePolicyDialog.vue @@ -0,0 +1,50 @@ + + + diff --git a/app/javascript/dashboard/store/index.js b/app/javascript/dashboard/store/index.js index 5a020dda6..291031ffd 100755 --- a/app/javascript/dashboard/store/index.js +++ b/app/javascript/dashboard/store/index.js @@ -3,6 +3,7 @@ import { createStore } from 'vuex'; import accounts from './modules/accounts'; import agentBots from './modules/agentBots'; import agents from './modules/agents'; +import assignmentPolicies from './modules/assignmentPolicies'; import articles from './modules/helpCenterArticles'; import attributes from './modules/attributes'; import auditlogs from './modules/auditlogs'; @@ -63,6 +64,7 @@ export default createStore({ accounts, agentBots, agents, + assignmentPolicies, articles, attributes, auditlogs, diff --git a/app/javascript/dashboard/store/modules/assignmentPolicies.js b/app/javascript/dashboard/store/modules/assignmentPolicies.js new file mode 100644 index 000000000..80c903c4e --- /dev/null +++ b/app/javascript/dashboard/store/modules/assignmentPolicies.js @@ -0,0 +1,156 @@ +import * as MutationHelpers from 'shared/helpers/vuex/mutationHelpers'; +import types from '../mutation-types'; +import AssignmentPoliciesAPI from '../../api/assignmentPolicies'; +import { throwErrorMessage } from '../utils/api'; +import camelcaseKeys from 'camelcase-keys'; + +export const state = { + records: [], + uiFlags: { + isFetching: false, + isFetchingItem: false, + isCreating: false, + isUpdating: false, + isDeleting: false, + }, + inboxUiFlags: { + isFetching: false, + }, +}; + +export const getters = { + getAssignmentPolicies(_state) { + return _state.records; + }, + getUIFlags(_state) { + return _state.uiFlags; + }, + getInboxUiFlags(_state) { + return _state.inboxUiFlags; + }, + getAssignmentPolicyById: _state => id => { + return _state.records.find(record => record.id === Number(id)) || {}; + }, +}; + +export const actions = { + get: async function get({ commit }) { + commit(types.SET_ASSIGNMENT_POLICIES_UI_FLAG, { isFetching: true }); + try { + const response = await AssignmentPoliciesAPI.get(); + commit(types.SET_ASSIGNMENT_POLICIES, camelcaseKeys(response.data)); + } catch (error) { + throwErrorMessage(error); + } finally { + commit(types.SET_ASSIGNMENT_POLICIES_UI_FLAG, { isFetching: false }); + } + }, + + show: async function show({ commit }, policyId) { + commit(types.SET_ASSIGNMENT_POLICIES_UI_FLAG, { isFetchingItem: true }); + try { + const response = await AssignmentPoliciesAPI.show(policyId); + const policy = camelcaseKeys(response.data); + commit(types.EDIT_ASSIGNMENT_POLICY, policy); + } catch (error) { + throwErrorMessage(error); + } finally { + commit(types.SET_ASSIGNMENT_POLICIES_UI_FLAG, { isFetchingItem: false }); + } + }, + + create: async function create({ commit }, policyObj) { + commit(types.SET_ASSIGNMENT_POLICIES_UI_FLAG, { isCreating: true }); + try { + const response = await AssignmentPoliciesAPI.create(policyObj); + commit(types.ADD_ASSIGNMENT_POLICY, camelcaseKeys(response.data)); + return response.data; + } catch (error) { + throwErrorMessage(error); + throw error; + } finally { + commit(types.SET_ASSIGNMENT_POLICIES_UI_FLAG, { isCreating: false }); + } + }, + + update: async function update({ commit }, { id, ...policyParams }) { + commit(types.SET_ASSIGNMENT_POLICIES_UI_FLAG, { isUpdating: true }); + try { + const response = await AssignmentPoliciesAPI.update(id, policyParams); + commit(types.EDIT_ASSIGNMENT_POLICY, camelcaseKeys(response.data)); + return response.data; + } catch (error) { + throwErrorMessage(error); + throw error; + } finally { + commit(types.SET_ASSIGNMENT_POLICIES_UI_FLAG, { isUpdating: false }); + } + }, + + delete: async function deletePolicy({ commit }, policyId) { + commit(types.SET_ASSIGNMENT_POLICIES_UI_FLAG, { isDeleting: true }); + try { + await AssignmentPoliciesAPI.delete(policyId); + commit(types.DELETE_ASSIGNMENT_POLICY, policyId); + } catch (error) { + throwErrorMessage(error); + throw error; + } finally { + commit(types.SET_ASSIGNMENT_POLICIES_UI_FLAG, { isDeleting: false }); + } + }, + + getInboxes: async function getInboxes({ commit }, policyId) { + commit(types.SET_ASSIGNMENT_POLICIES_INBOXES_UI_FLAG, { isFetching: true }); + try { + const response = await AssignmentPoliciesAPI.getInboxes(policyId); + commit(types.SET_ASSIGNMENT_POLICIES_INBOXES, { + policyId, + inboxes: camelcaseKeys(response.data.inboxes), + }); + } catch (error) { + throwErrorMessage(error); + throw error; + } finally { + commit(types.SET_ASSIGNMENT_POLICIES_INBOXES_UI_FLAG, { + isFetching: false, + }); + } + }, +}; + +export const mutations = { + [types.SET_ASSIGNMENT_POLICIES_UI_FLAG](_state, data) { + _state.uiFlags = { + ..._state.uiFlags, + ...data, + }; + }, + + [types.SET_ASSIGNMENT_POLICIES]: MutationHelpers.set, + [types.ADD_ASSIGNMENT_POLICY]: MutationHelpers.create, + [types.EDIT_ASSIGNMENT_POLICY]: MutationHelpers.update, + [types.DELETE_ASSIGNMENT_POLICY]: MutationHelpers.destroy, + + [types.SET_ASSIGNMENT_POLICIES_INBOXES_UI_FLAG](_state, data) { + _state.inboxUiFlags = { + ..._state.inboxUiFlags, + ...data, + }; + }, + + [types.SET_ASSIGNMENT_POLICIES_INBOXES](_state, { policyId, inboxes }) { + const policy = _state.records.find(p => p.id === policyId); + if (policy) { + policy.inboxes = inboxes; + } + }, +}; + +export default { + namespaced: true, + state, + getters, + actions, + mutations, +}; diff --git a/app/javascript/dashboard/store/modules/specs/assignmentPolicies/actions.spec.js b/app/javascript/dashboard/store/modules/specs/assignmentPolicies/actions.spec.js new file mode 100644 index 000000000..5358144e8 --- /dev/null +++ b/app/javascript/dashboard/store/modules/specs/assignmentPolicies/actions.spec.js @@ -0,0 +1,214 @@ +import axios from 'axios'; +import { actions } from '../../assignmentPolicies'; +import types from '../../../mutation-types'; +import assignmentPoliciesList, { camelCaseFixtures } from './fixtures'; +import camelcaseKeys from 'camelcase-keys'; + +const commit = vi.fn(); + +global.axios = axios; +vi.mock('axios'); +vi.mock('camelcase-keys'); +vi.mock('../../../utils/api'); + +describe('#actions', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + describe('#get', () => { + it('sends correct actions if API is success', async () => { + axios.get.mockResolvedValue({ data: assignmentPoliciesList }); + camelcaseKeys.mockReturnValue(camelCaseFixtures); + + await actions.get({ commit }); + + expect(camelcaseKeys).toHaveBeenCalledWith(assignmentPoliciesList); + expect(commit.mock.calls).toEqual([ + [types.SET_ASSIGNMENT_POLICIES_UI_FLAG, { isFetching: true }], + [types.SET_ASSIGNMENT_POLICIES, camelCaseFixtures], + [types.SET_ASSIGNMENT_POLICIES_UI_FLAG, { isFetching: false }], + ]); + }); + + it('sends correct actions if API is error', async () => { + axios.get.mockRejectedValue({ message: 'Incorrect header' }); + + await actions.get({ commit }); + + expect(commit.mock.calls).toEqual([ + [types.SET_ASSIGNMENT_POLICIES_UI_FLAG, { isFetching: true }], + [types.SET_ASSIGNMENT_POLICIES_UI_FLAG, { isFetching: false }], + ]); + }); + }); + + describe('#show', () => { + it('sends correct actions if API is success', async () => { + const policyData = assignmentPoliciesList[0]; + const camelCasedPolicy = camelCaseFixtures[0]; + + axios.get.mockResolvedValue({ data: policyData }); + camelcaseKeys.mockReturnValue(camelCasedPolicy); + + await actions.show({ commit }, 1); + + expect(camelcaseKeys).toHaveBeenCalledWith(policyData); + expect(commit.mock.calls).toEqual([ + [types.SET_ASSIGNMENT_POLICIES_UI_FLAG, { isFetchingItem: true }], + [types.EDIT_ASSIGNMENT_POLICY, camelCasedPolicy], + [types.SET_ASSIGNMENT_POLICIES_UI_FLAG, { isFetchingItem: false }], + ]); + }); + + it('sends correct actions if API is error', async () => { + axios.get.mockRejectedValue({ message: 'Not found' }); + + await actions.show({ commit }, 1); + + expect(commit.mock.calls).toEqual([ + [types.SET_ASSIGNMENT_POLICIES_UI_FLAG, { isFetchingItem: true }], + [types.SET_ASSIGNMENT_POLICIES_UI_FLAG, { isFetchingItem: false }], + ]); + }); + }); + + describe('#create', () => { + it('sends correct actions if API is success', async () => { + const newPolicy = assignmentPoliciesList[0]; + const camelCasedData = camelCaseFixtures[0]; + + axios.post.mockResolvedValue({ data: newPolicy }); + camelcaseKeys.mockReturnValue(camelCasedData); + + const result = await actions.create({ commit }, newPolicy); + + expect(camelcaseKeys).toHaveBeenCalledWith(newPolicy); + expect(commit.mock.calls).toEqual([ + [types.SET_ASSIGNMENT_POLICIES_UI_FLAG, { isCreating: true }], + [types.ADD_ASSIGNMENT_POLICY, camelCasedData], + [types.SET_ASSIGNMENT_POLICIES_UI_FLAG, { isCreating: false }], + ]); + expect(result).toEqual(newPolicy); + }); + + it('sends correct actions if API is error', async () => { + axios.post.mockRejectedValue(new Error('Validation error')); + + await expect(actions.create({ commit }, {})).rejects.toThrow(Error); + + expect(commit.mock.calls).toEqual([ + [types.SET_ASSIGNMENT_POLICIES_UI_FLAG, { isCreating: true }], + [types.SET_ASSIGNMENT_POLICIES_UI_FLAG, { isCreating: false }], + ]); + }); + }); + + describe('#update', () => { + it('sends correct actions if API is success', async () => { + const updateParams = { id: 1, name: 'Updated Policy' }; + const responseData = { + ...assignmentPoliciesList[0], + name: 'Updated Policy', + }; + const camelCasedData = { + ...camelCaseFixtures[0], + name: 'Updated Policy', + }; + + axios.patch.mockResolvedValue({ data: responseData }); + camelcaseKeys.mockReturnValue(camelCasedData); + + const result = await actions.update({ commit }, updateParams); + + expect(camelcaseKeys).toHaveBeenCalledWith(responseData); + expect(commit.mock.calls).toEqual([ + [types.SET_ASSIGNMENT_POLICIES_UI_FLAG, { isUpdating: true }], + [types.EDIT_ASSIGNMENT_POLICY, camelCasedData], + [types.SET_ASSIGNMENT_POLICIES_UI_FLAG, { isUpdating: false }], + ]); + expect(result).toEqual(responseData); + }); + + it('sends correct actions if API is error', async () => { + axios.patch.mockRejectedValue(new Error('Validation error')); + + await expect( + actions.update({ commit }, { id: 1, name: 'Test' }) + ).rejects.toThrow(Error); + + expect(commit.mock.calls).toEqual([ + [types.SET_ASSIGNMENT_POLICIES_UI_FLAG, { isUpdating: true }], + [types.SET_ASSIGNMENT_POLICIES_UI_FLAG, { isUpdating: false }], + ]); + }); + }); + + describe('#delete', () => { + it('sends correct actions if API is success', async () => { + const policyId = 1; + axios.delete.mockResolvedValue({}); + + await actions.delete({ commit }, policyId); + + expect(commit.mock.calls).toEqual([ + [types.SET_ASSIGNMENT_POLICIES_UI_FLAG, { isDeleting: true }], + [types.DELETE_ASSIGNMENT_POLICY, policyId], + [types.SET_ASSIGNMENT_POLICIES_UI_FLAG, { isDeleting: false }], + ]); + }); + + it('sends correct actions if API is error', async () => { + axios.delete.mockRejectedValue(new Error('Not found')); + + await expect(actions.delete({ commit }, 1)).rejects.toThrow(Error); + + expect(commit.mock.calls).toEqual([ + [types.SET_ASSIGNMENT_POLICIES_UI_FLAG, { isDeleting: true }], + [types.SET_ASSIGNMENT_POLICIES_UI_FLAG, { isDeleting: false }], + ]); + }); + }); + + describe('#getInboxes', () => { + it('sends correct actions if API is success', async () => { + const policyId = 1; + const inboxData = { + inboxes: [ + { id: 1, name: 'Support' }, + { id: 2, name: 'Sales' }, + ], + }; + const camelCasedInboxes = [ + { id: 1, name: 'Support' }, + { id: 2, name: 'Sales' }, + ]; + + axios.get.mockResolvedValue({ data: inboxData }); + camelcaseKeys.mockReturnValue(camelCasedInboxes); + + await actions.getInboxes({ commit }, policyId); + + expect(camelcaseKeys).toHaveBeenCalledWith(inboxData.inboxes); + expect(commit.mock.calls).toEqual([ + [types.SET_ASSIGNMENT_POLICIES_INBOXES_UI_FLAG, { isFetching: true }], + [ + types.SET_ASSIGNMENT_POLICIES_INBOXES, + { policyId, inboxes: camelCasedInboxes }, + ], + [types.SET_ASSIGNMENT_POLICIES_INBOXES_UI_FLAG, { isFetching: false }], + ]); + }); + + it('sends correct actions if API fails', async () => { + axios.get.mockRejectedValue(new Error('API Error')); + + await expect(actions.getInboxes({ commit }, 1)).rejects.toThrow(Error); + + expect(commit.mock.calls).toEqual([ + [types.SET_ASSIGNMENT_POLICIES_INBOXES_UI_FLAG, { isFetching: true }], + [types.SET_ASSIGNMENT_POLICIES_INBOXES_UI_FLAG, { isFetching: false }], + ]); + }); + }); +}); diff --git a/app/javascript/dashboard/store/modules/specs/assignmentPolicies/fixtures.js b/app/javascript/dashboard/store/modules/specs/assignmentPolicies/fixtures.js new file mode 100644 index 000000000..1b5ed25af --- /dev/null +++ b/app/javascript/dashboard/store/modules/specs/assignmentPolicies/fixtures.js @@ -0,0 +1,57 @@ +export default [ + { + id: 1, + name: 'Round Robin Policy', + description: 'Distributes conversations evenly among agents', + assignment_order: 'round_robin', + conversation_priority: 'earliest_created', + fair_distribution_limit: 100, + fair_distribution_window: 3600, + enabled: true, + assigned_inbox_count: 3, + created_at: 1704110400, + updated_at: 1704110400, + }, + { + id: 2, + name: 'Balanced Policy', + description: 'Assigns conversations based on agent capacity', + assignment_order: 'balanced', + conversation_priority: 'longest_waiting', + fair_distribution_limit: 50, + fair_distribution_window: 1800, + enabled: false, + assigned_inbox_count: 1, + created_at: 1704114000, + updated_at: 1704114000, + }, +]; + +export const camelCaseFixtures = [ + { + id: 1, + name: 'Round Robin Policy', + description: 'Distributes conversations evenly among agents', + assignmentOrder: 'round_robin', + conversationPriority: 'earliest_created', + fairDistributionLimit: 100, + fairDistributionWindow: 3600, + enabled: true, + assignedInboxCount: 3, + createdAt: 1704110400, + updatedAt: 1704110400, + }, + { + id: 2, + name: 'Balanced Policy', + description: 'Assigns conversations based on agent capacity', + assignmentOrder: 'balanced', + conversationPriority: 'longest_waiting', + fairDistributionLimit: 50, + fairDistributionWindow: 1800, + enabled: false, + assignedInboxCount: 1, + createdAt: 1704114000, + updatedAt: 1704114000, + }, +]; diff --git a/app/javascript/dashboard/store/modules/specs/assignmentPolicies/getters.spec.js b/app/javascript/dashboard/store/modules/specs/assignmentPolicies/getters.spec.js new file mode 100644 index 000000000..7e0e2041c --- /dev/null +++ b/app/javascript/dashboard/store/modules/specs/assignmentPolicies/getters.spec.js @@ -0,0 +1,49 @@ +import { getters } from '../../assignmentPolicies'; +import assignmentPoliciesList from './fixtures'; + +describe('#getters', () => { + it('getAssignmentPolicies', () => { + const state = { records: assignmentPoliciesList }; + expect(getters.getAssignmentPolicies(state)).toEqual( + assignmentPoliciesList + ); + }); + + it('getUIFlags', () => { + const state = { + uiFlags: { + isFetching: true, + isFetchingItem: false, + isCreating: false, + isUpdating: false, + isDeleting: false, + }, + }; + expect(getters.getUIFlags(state)).toEqual({ + isFetching: true, + isFetchingItem: false, + isCreating: false, + isUpdating: false, + isDeleting: false, + }); + }); + + it('getInboxUiFlags', () => { + const state = { + inboxUiFlags: { + isFetching: false, + }, + }; + expect(getters.getInboxUiFlags(state)).toEqual({ + isFetching: false, + }); + }); + + it('getAssignmentPolicyById', () => { + const state = { records: assignmentPoliciesList }; + expect(getters.getAssignmentPolicyById(state)(1)).toEqual( + assignmentPoliciesList[0] + ); + expect(getters.getAssignmentPolicyById(state)(3)).toEqual({}); + }); +}); diff --git a/app/javascript/dashboard/store/modules/specs/assignmentPolicies/mutations.spec.js b/app/javascript/dashboard/store/modules/specs/assignmentPolicies/mutations.spec.js new file mode 100644 index 000000000..58d5527ca --- /dev/null +++ b/app/javascript/dashboard/store/modules/specs/assignmentPolicies/mutations.spec.js @@ -0,0 +1,267 @@ +import { mutations } from '../../assignmentPolicies'; +import types from '../../../mutation-types'; +import assignmentPoliciesList from './fixtures'; + +describe('#mutations', () => { + describe('#SET_ASSIGNMENT_POLICIES_UI_FLAG', () => { + it('sets single ui flag', () => { + const state = { + uiFlags: { + isFetching: false, + isCreating: false, + }, + }; + + mutations[types.SET_ASSIGNMENT_POLICIES_UI_FLAG](state, { + isFetching: true, + }); + + expect(state.uiFlags).toEqual({ + isFetching: true, + isCreating: false, + }); + }); + + it('sets multiple ui flags', () => { + const state = { + uiFlags: { + isFetching: false, + isCreating: false, + isUpdating: false, + }, + }; + + mutations[types.SET_ASSIGNMENT_POLICIES_UI_FLAG](state, { + isFetching: true, + isCreating: true, + }); + + expect(state.uiFlags).toEqual({ + isFetching: true, + isCreating: true, + isUpdating: false, + }); + }); + }); + + describe('#SET_ASSIGNMENT_POLICIES', () => { + it('sets assignment policies records', () => { + const state = { records: [] }; + + mutations[types.SET_ASSIGNMENT_POLICIES](state, assignmentPoliciesList); + + expect(state.records).toEqual(assignmentPoliciesList); + }); + + it('replaces existing records', () => { + const state = { records: [{ id: 999, name: 'Old Policy' }] }; + + mutations[types.SET_ASSIGNMENT_POLICIES](state, assignmentPoliciesList); + + expect(state.records).toEqual(assignmentPoliciesList); + }); + }); + + describe('#ADD_ASSIGNMENT_POLICY', () => { + it('adds new policy to empty records', () => { + const state = { records: [] }; + + mutations[types.ADD_ASSIGNMENT_POLICY](state, assignmentPoliciesList[0]); + + expect(state.records).toEqual([assignmentPoliciesList[0]]); + }); + + it('adds new policy to existing records', () => { + const state = { records: [assignmentPoliciesList[0]] }; + + mutations[types.ADD_ASSIGNMENT_POLICY](state, assignmentPoliciesList[1]); + + expect(state.records).toEqual([ + assignmentPoliciesList[0], + assignmentPoliciesList[1], + ]); + }); + }); + + describe('#EDIT_ASSIGNMENT_POLICY', () => { + it('updates existing policy by id', () => { + const state = { + records: [ + { ...assignmentPoliciesList[0] }, + { ...assignmentPoliciesList[1] }, + ], + }; + + const updatedPolicy = { + ...assignmentPoliciesList[0], + name: 'Updated Policy Name', + description: 'Updated Description', + }; + + mutations[types.EDIT_ASSIGNMENT_POLICY](state, updatedPolicy); + + expect(state.records[0]).toEqual(updatedPolicy); + expect(state.records[1]).toEqual(assignmentPoliciesList[1]); + }); + + it('updates policy with camelCase properties', () => { + const camelCasePolicy = { + id: 1, + name: 'Camel Case Policy', + assignmentOrder: 'round_robin', + conversationPriority: 'earliest_created', + }; + + const state = { + records: [camelCasePolicy], + }; + + const updatedPolicy = { + ...camelCasePolicy, + name: 'Updated Camel Case', + assignmentOrder: 'balanced', + }; + + mutations[types.EDIT_ASSIGNMENT_POLICY](state, updatedPolicy); + + expect(state.records[0]).toEqual(updatedPolicy); + }); + + it('does nothing if policy id not found', () => { + const state = { + records: [assignmentPoliciesList[0]], + }; + + const nonExistentPolicy = { + id: 999, + name: 'Non-existent', + }; + + const originalRecords = [...state.records]; + mutations[types.EDIT_ASSIGNMENT_POLICY](state, nonExistentPolicy); + + expect(state.records).toEqual(originalRecords); + }); + }); + + describe('#DELETE_ASSIGNMENT_POLICY', () => { + it('deletes policy by id', () => { + const state = { + records: [assignmentPoliciesList[0], assignmentPoliciesList[1]], + }; + + mutations[types.DELETE_ASSIGNMENT_POLICY](state, 1); + + expect(state.records).toEqual([assignmentPoliciesList[1]]); + }); + + it('does nothing if id not found', () => { + const state = { + records: [assignmentPoliciesList[0]], + }; + + mutations[types.DELETE_ASSIGNMENT_POLICY](state, 999); + + expect(state.records).toEqual([assignmentPoliciesList[0]]); + }); + + it('handles empty records', () => { + const state = { records: [] }; + + mutations[types.DELETE_ASSIGNMENT_POLICY](state, 1); + + expect(state.records).toEqual([]); + }); + }); + + describe('#SET_ASSIGNMENT_POLICIES_INBOXES_UI_FLAG', () => { + it('sets inbox ui flags', () => { + const state = { + inboxUiFlags: { + isFetching: false, + }, + }; + + mutations[types.SET_ASSIGNMENT_POLICIES_INBOXES_UI_FLAG](state, { + isFetching: true, + }); + + expect(state.inboxUiFlags).toEqual({ + isFetching: true, + }); + }); + + it('merges with existing flags', () => { + const state = { + inboxUiFlags: { + isFetching: false, + isLoading: true, + }, + }; + + mutations[types.SET_ASSIGNMENT_POLICIES_INBOXES_UI_FLAG](state, { + isFetching: true, + }); + + expect(state.inboxUiFlags).toEqual({ + isFetching: true, + isLoading: true, + }); + }); + }); + + describe('#SET_ASSIGNMENT_POLICIES_INBOXES', () => { + it('sets inboxes for existing policy', () => { + const mockInboxes = [ + { id: 1, name: 'Support Inbox' }, + { id: 2, name: 'Sales Inbox' }, + ]; + + const state = { + records: [ + { id: 1, name: 'Policy 1', inboxes: [] }, + { id: 2, name: 'Policy 2', inboxes: [] }, + ], + }; + + mutations[types.SET_ASSIGNMENT_POLICIES_INBOXES](state, { + policyId: 1, + inboxes: mockInboxes, + }); + + expect(state.records[0].inboxes).toEqual(mockInboxes); + expect(state.records[1].inboxes).toEqual([]); + }); + + it('replaces existing inboxes', () => { + const oldInboxes = [{ id: 99, name: 'Old Inbox' }]; + const newInboxes = [{ id: 1, name: 'New Inbox' }]; + + const state = { + records: [{ id: 1, name: 'Policy 1', inboxes: oldInboxes }], + }; + + mutations[types.SET_ASSIGNMENT_POLICIES_INBOXES](state, { + policyId: 1, + inboxes: newInboxes, + }); + + expect(state.records[0].inboxes).toEqual(newInboxes); + }); + + it('does nothing if policy not found', () => { + const state = { + records: [{ id: 1, name: 'Policy 1', inboxes: [] }], + }; + + const originalState = JSON.parse(JSON.stringify(state)); + + mutations[types.SET_ASSIGNMENT_POLICIES_INBOXES](state, { + policyId: 999, + inboxes: [{ id: 1, name: 'Test' }], + }); + + expect(state).toEqual(originalState); + }); + }); +}); diff --git a/app/javascript/dashboard/store/mutation-types.js b/app/javascript/dashboard/store/mutation-types.js index a63fec2d1..a6fbefa17 100644 --- a/app/javascript/dashboard/store/mutation-types.js +++ b/app/javascript/dashboard/store/mutation-types.js @@ -348,4 +348,14 @@ export default { SET_TEAM_CONVERSATION_METRIC: 'SET_TEAM_CONVERSATION_METRIC', TOGGLE_TEAM_CONVERSATION_METRIC_LOADING: 'TOGGLE_TEAM_CONVERSATION_METRIC_LOADING', + + // Assignment Policies + SET_ASSIGNMENT_POLICIES_UI_FLAG: 'SET_ASSIGNMENT_POLICIES_UI_FLAG', + SET_ASSIGNMENT_POLICIES: 'SET_ASSIGNMENT_POLICIES', + ADD_ASSIGNMENT_POLICY: 'ADD_ASSIGNMENT_POLICY', + EDIT_ASSIGNMENT_POLICY: 'EDIT_ASSIGNMENT_POLICY', + DELETE_ASSIGNMENT_POLICY: 'DELETE_ASSIGNMENT_POLICY', + SET_ASSIGNMENT_POLICIES_INBOXES: 'SET_ASSIGNMENT_POLICIES_INBOXES', + SET_ASSIGNMENT_POLICIES_INBOXES_UI_FLAG: + 'SET_ASSIGNMENT_POLICIES_INBOXES_UI_FLAG', }; diff --git a/app/views/api/v1/accounts/assignment_policies/_assignment_policy.json.jbuilder b/app/views/api/v1/accounts/assignment_policies/_assignment_policy.json.jbuilder index b48307a94..cf09a2949 100644 --- a/app/views/api/v1/accounts/assignment_policies/_assignment_policy.json.jbuilder +++ b/app/views/api/v1/accounts/assignment_policies/_assignment_policy.json.jbuilder @@ -6,5 +6,6 @@ json.conversation_priority assignment_policy.conversation_priority json.fair_distribution_limit assignment_policy.fair_distribution_limit json.fair_distribution_window assignment_policy.fair_distribution_window json.enabled assignment_policy.enabled +json.assigned_inbox_count assignment_policy.inboxes.count json.created_at assignment_policy.created_at.to_i json.updated_at assignment_policy.updated_at.to_i