mirror of
https://github.com/lingble/chatwoot.git
synced 2025-11-02 20:18:08 +00:00
feat: Rewrite agentMixin to a helper (#9940)
# Pull Request Template ## Description This PR will replace the usage of `agentMixin`with the utility helpers functions. **Files updated** 1. dashboard/components/widgets/conversation/contextMenu/Index.vue 2. dashboard/components/widgets/conversation/ConversationHeader.vue **(Not used)** 3. dashboard/routes/dashboard/commands/commandbar.vue 4. dashboard/routes/dashboard/conversation/ConversationAction.vue 5. dashboard/routes/dashboard/conversation/ConversationParticipant.vue Fixes https://linear.app/chatwoot/issue/CW-3442/rewrite-agentmixin-mixin-to-a-composable ## Type of change - [x] New feature (non-breaking change which adds functionality) ## How Has This Been Tested? **Test cases** 1. See agent list sorting based on availability, if agents are on the same status, then sorted by name. 2. Test actions like assigning/unassigning agent from conversation sidebar, CMD bar, Context menu. 3. Test actions like adding/removing participants from conversation sidebar. 4. See agent list is generated properly, none value. ## 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
This commit is contained in:
@@ -2,7 +2,6 @@
|
||||
import { ref } from 'vue';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { useKeyboardEvents } from 'dashboard/composables/useKeyboardEvents';
|
||||
import agentMixin from '../../../mixins/agentMixin.js';
|
||||
import BackButton from '../BackButton.vue';
|
||||
import inboxMixin from 'shared/mixins/inboxMixin';
|
||||
import InboxName from '../InboxName.vue';
|
||||
@@ -24,7 +23,7 @@ export default {
|
||||
SLACardLabel,
|
||||
Linear,
|
||||
},
|
||||
mixins: [inboxMixin, agentMixin],
|
||||
mixins: [inboxMixin],
|
||||
props: {
|
||||
chat: {
|
||||
type: Object,
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import {
|
||||
getSortedAgentsByAvailability,
|
||||
getAgentsByUpdatedPresence,
|
||||
} from 'dashboard/helper/agentHelper.js';
|
||||
import MenuItem from './menuItem.vue';
|
||||
import MenuItemWithSubmenu from './menuItemWithSubmenu.vue';
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
import agentMixin from 'dashboard/mixins/agentMixin';
|
||||
import { mapGetters } from 'vuex';
|
||||
import AgentLoadingPlaceholder from './agentLoadingPlaceholder.vue';
|
||||
export default {
|
||||
components: {
|
||||
@@ -11,7 +14,6 @@ export default {
|
||||
MenuItemWithSubmenu,
|
||||
AgentLoadingPlaceholder,
|
||||
},
|
||||
mixins: [agentMixin],
|
||||
props: {
|
||||
chatId: {
|
||||
type: Number,
|
||||
@@ -112,13 +114,19 @@ export default {
|
||||
labels: 'labels/getLabels',
|
||||
teams: 'teams/getTeams',
|
||||
assignableAgentsUiFlags: 'inboxAssignableAgents/getUIFlags',
|
||||
currentUser: 'getCurrentUser',
|
||||
currentAccountId: 'getCurrentAccountId',
|
||||
}),
|
||||
filteredAgentOnAvailability() {
|
||||
const agents = this.$store.getters[
|
||||
'inboxAssignableAgents/getAssignableAgents'
|
||||
](this.inboxId);
|
||||
const agentsByUpdatedPresence = this.getAgentsByUpdatedPresence(agents);
|
||||
const filteredAgents = this.sortedAgentsByAvailability(
|
||||
const agentsByUpdatedPresence = getAgentsByUpdatedPresence(
|
||||
agents,
|
||||
this.currentUser,
|
||||
this.currentAccountId
|
||||
);
|
||||
const filteredAgents = getSortedAgentsByAvailability(
|
||||
agentsByUpdatedPresence
|
||||
);
|
||||
return filteredAgents;
|
||||
|
||||
63
app/javascript/dashboard/composables/spec/fixtures/agentFixtures.js
vendored
Normal file
63
app/javascript/dashboard/composables/spec/fixtures/agentFixtures.js
vendored
Normal file
@@ -0,0 +1,63 @@
|
||||
import { allAgentsData } from 'dashboard/helper/specs/fixtures/agentFixtures';
|
||||
|
||||
export { allAgentsData };
|
||||
export const formattedAgentsData = [
|
||||
{
|
||||
account_id: 0,
|
||||
confirmed: true,
|
||||
email: 'None',
|
||||
id: 0,
|
||||
name: 'None',
|
||||
role: 'agent',
|
||||
},
|
||||
{
|
||||
account_id: 1,
|
||||
availability_status: 'online',
|
||||
available_name: 'Abraham',
|
||||
confirmed: true,
|
||||
email: 'abraham@chatwoot.com',
|
||||
id: 5,
|
||||
name: 'Abraham Keta',
|
||||
role: 'agent',
|
||||
},
|
||||
{
|
||||
account_id: 1,
|
||||
availability_status: 'online',
|
||||
available_name: 'John K',
|
||||
confirmed: true,
|
||||
email: 'john@chatwoot.com',
|
||||
id: 1,
|
||||
name: 'John Kennady',
|
||||
role: 'administrator',
|
||||
},
|
||||
{
|
||||
account_id: 1,
|
||||
availability_status: 'busy',
|
||||
available_name: 'Honey',
|
||||
confirmed: true,
|
||||
email: 'bee@chatwoot.com',
|
||||
id: 4,
|
||||
name: 'Honey Bee',
|
||||
role: 'agent',
|
||||
},
|
||||
{
|
||||
account_id: 1,
|
||||
availability_status: 'busy',
|
||||
available_name: 'Samuel K',
|
||||
confirmed: true,
|
||||
email: 'samuel@chatwoot.com',
|
||||
id: 2,
|
||||
name: 'Samuel Keta',
|
||||
role: 'agent',
|
||||
},
|
||||
{
|
||||
account_id: 1,
|
||||
availability_status: 'offline',
|
||||
available_name: 'James K',
|
||||
confirmed: true,
|
||||
email: 'james@chatwoot.com',
|
||||
id: 3,
|
||||
name: 'James Koti',
|
||||
role: 'agent',
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,91 @@
|
||||
import { ref } from 'vue';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { useAgentsList } from '../useAgentsList';
|
||||
import { useMapGetter } from 'dashboard/composables/store';
|
||||
import { allAgentsData, formattedAgentsData } from './fixtures/agentFixtures';
|
||||
import * as agentHelper from 'dashboard/helper/agentHelper';
|
||||
|
||||
vi.mock('dashboard/composables/store');
|
||||
vi.mock('dashboard/helper/agentHelper');
|
||||
|
||||
const mockUseMapGetter = (overrides = {}) => {
|
||||
const defaultGetters = {
|
||||
getCurrentUser: ref(allAgentsData[0]),
|
||||
getSelectedChat: ref({ inbox_id: 1, meta: { assignee: true } }),
|
||||
getCurrentAccountId: ref(1),
|
||||
'inboxAssignableAgents/getAssignableAgents': ref(() => allAgentsData),
|
||||
};
|
||||
|
||||
const mergedGetters = { ...defaultGetters, ...overrides };
|
||||
|
||||
useMapGetter.mockImplementation(getter => mergedGetters[getter]);
|
||||
};
|
||||
|
||||
describe('useAgentsList', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
agentHelper.getAgentsByUpdatedPresence.mockImplementation(agents => agents);
|
||||
agentHelper.getSortedAgentsByAvailability.mockReturnValue(
|
||||
formattedAgentsData.slice(1)
|
||||
);
|
||||
agentHelper.getCombinedAgents.mockImplementation(
|
||||
(agents, includeNone, isAgentSelected) => {
|
||||
if (includeNone && isAgentSelected) {
|
||||
return [agentHelper.createNoneAgent, ...agents];
|
||||
}
|
||||
return agents;
|
||||
}
|
||||
);
|
||||
|
||||
mockUseMapGetter();
|
||||
});
|
||||
|
||||
it('returns agentsList and assignableAgents', () => {
|
||||
const { agentsList, assignableAgents } = useAgentsList();
|
||||
|
||||
expect(assignableAgents.value).toEqual(allAgentsData);
|
||||
expect(agentsList.value).toEqual([
|
||||
agentHelper.createNoneAgent,
|
||||
...formattedAgentsData.slice(1),
|
||||
]);
|
||||
});
|
||||
|
||||
it('includes None agent when includeNoneAgent is true', () => {
|
||||
const { agentsList } = useAgentsList(true);
|
||||
|
||||
expect(agentsList.value[0]).toEqual(agentHelper.createNoneAgent);
|
||||
expect(agentsList.value.length).toBe(formattedAgentsData.length);
|
||||
});
|
||||
|
||||
it('excludes None agent when includeNoneAgent is false', () => {
|
||||
const { agentsList } = useAgentsList(false);
|
||||
|
||||
expect(agentsList.value[0]).not.toEqual(agentHelper.createNoneAgent);
|
||||
expect(agentsList.value.length).toBe(formattedAgentsData.length - 1);
|
||||
});
|
||||
|
||||
it('handles empty assignable agents', () => {
|
||||
mockUseMapGetter({
|
||||
'inboxAssignableAgents/getAssignableAgents': ref(() => []),
|
||||
});
|
||||
agentHelper.getSortedAgentsByAvailability.mockReturnValue([]);
|
||||
|
||||
const { agentsList, assignableAgents } = useAgentsList();
|
||||
|
||||
expect(assignableAgents.value).toEqual([]);
|
||||
expect(agentsList.value).toEqual([agentHelper.createNoneAgent]);
|
||||
});
|
||||
|
||||
it('handles missing inbox_id', () => {
|
||||
mockUseMapGetter({
|
||||
getSelectedChat: ref({ meta: { assignee: true } }),
|
||||
'inboxAssignableAgents/getAssignableAgents': ref(() => []),
|
||||
});
|
||||
agentHelper.getSortedAgentsByAvailability.mockReturnValue([]);
|
||||
|
||||
const { agentsList, assignableAgents } = useAgentsList();
|
||||
|
||||
expect(assignableAgents.value).toEqual([]);
|
||||
expect(agentsList.value).toEqual([agentHelper.createNoneAgent]);
|
||||
});
|
||||
});
|
||||
57
app/javascript/dashboard/composables/useAgentsList.js
Normal file
57
app/javascript/dashboard/composables/useAgentsList.js
Normal file
@@ -0,0 +1,57 @@
|
||||
import { computed } from 'vue';
|
||||
import { useMapGetter } from 'dashboard/composables/store';
|
||||
import {
|
||||
getAgentsByUpdatedPresence,
|
||||
getSortedAgentsByAvailability,
|
||||
getCombinedAgents,
|
||||
} from 'dashboard/helper/agentHelper';
|
||||
|
||||
/**
|
||||
* A composable function that provides a list of agents for assignment.
|
||||
*
|
||||
* @param {boolean} [includeNoneAgent=true] - Whether to include a 'None' agent option.
|
||||
* @returns {Object} An object containing the agents list and assignable agents.
|
||||
*/
|
||||
export function useAgentsList(includeNoneAgent = true) {
|
||||
const currentUser = useMapGetter('getCurrentUser');
|
||||
const currentChat = useMapGetter('getSelectedChat');
|
||||
const currentAccountId = useMapGetter('getCurrentAccountId');
|
||||
const assignable = useMapGetter('inboxAssignableAgents/getAssignableAgents');
|
||||
|
||||
const inboxId = computed(() => currentChat.value?.inbox_id);
|
||||
const isAgentSelected = computed(() => currentChat.value?.meta?.assignee);
|
||||
|
||||
/**
|
||||
* @type {import('vue').ComputedRef<Array>}
|
||||
*/
|
||||
const assignableAgents = computed(() => {
|
||||
return inboxId.value ? assignable.value(inboxId.value) : [];
|
||||
});
|
||||
|
||||
/**
|
||||
* @type {import('vue').ComputedRef<Array>}
|
||||
*/
|
||||
const agentsList = computed(() => {
|
||||
const agents = assignableAgents.value || [];
|
||||
const agentsByUpdatedPresence = getAgentsByUpdatedPresence(
|
||||
agents,
|
||||
currentUser.value,
|
||||
currentAccountId.value
|
||||
);
|
||||
|
||||
const filteredAgentsByAvailability = getSortedAgentsByAvailability(
|
||||
agentsByUpdatedPresence
|
||||
);
|
||||
|
||||
return getCombinedAgents(
|
||||
filteredAgentsByAvailability,
|
||||
includeNoneAgent,
|
||||
isAgentSelected.value
|
||||
);
|
||||
});
|
||||
|
||||
return {
|
||||
agentsList,
|
||||
assignableAgents,
|
||||
};
|
||||
}
|
||||
83
app/javascript/dashboard/helper/agentHelper.js
Normal file
83
app/javascript/dashboard/helper/agentHelper.js
Normal file
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* Default agent object representing 'None'
|
||||
* @type {Object}
|
||||
*/
|
||||
export const createNoneAgent = {
|
||||
confirmed: true,
|
||||
name: 'None',
|
||||
id: 0,
|
||||
role: 'agent',
|
||||
account_id: 0,
|
||||
email: 'None',
|
||||
};
|
||||
|
||||
/**
|
||||
* Filters and sorts agents by availability status
|
||||
* @param {Array} agents - List of agents
|
||||
* @param {string} availability - Availability status to filter by
|
||||
* @returns {Array} Filtered and sorted list of agents
|
||||
*/
|
||||
export const getAgentsByAvailability = (agents, availability) => {
|
||||
return agents
|
||||
.filter(agent => agent.availability_status === availability)
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
};
|
||||
|
||||
/**
|
||||
* Sorts agents by availability status: online, busy, then offline
|
||||
* @param {Array} agents - List of agents
|
||||
* @returns {Array} Sorted list of agents
|
||||
*/
|
||||
export const getSortedAgentsByAvailability = agents => {
|
||||
const onlineAgents = getAgentsByAvailability(agents, 'online');
|
||||
const busyAgents = getAgentsByAvailability(agents, 'busy');
|
||||
const offlineAgents = getAgentsByAvailability(agents, 'offline');
|
||||
const filteredAgents = [...onlineAgents, ...busyAgents, ...offlineAgents];
|
||||
return filteredAgents;
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the availability status of the current user based on the current account
|
||||
* @param {Array} agents - List of agents
|
||||
* @param {Object} currentUser - Current user object
|
||||
* @param {number} currentAccountId - ID of the current account
|
||||
* @returns {Array} Updated list of agents with dynamic presence
|
||||
*/
|
||||
// Here we are updating the availability status of the current user dynamically
|
||||
// based on the current account availability status
|
||||
export const getAgentsByUpdatedPresence = (
|
||||
agents,
|
||||
currentUser,
|
||||
currentAccountId
|
||||
) => {
|
||||
const agentsWithDynamicPresenceUpdate = agents.map(item =>
|
||||
item.id === currentUser.id
|
||||
? {
|
||||
...item,
|
||||
availability_status: currentUser.accounts.find(
|
||||
account => account.id === currentAccountId
|
||||
).availability_status,
|
||||
}
|
||||
: item
|
||||
);
|
||||
return agentsWithDynamicPresenceUpdate;
|
||||
};
|
||||
|
||||
/**
|
||||
* Combines the filtered agents with the 'None' agent option if applicable.
|
||||
*
|
||||
* @param {Array} filteredAgentsByAvailability - The list of agents sorted by availability.
|
||||
* @param {boolean} includeNoneAgent - Whether to include the 'None' agent option.
|
||||
* @param {boolean} isAgentSelected - Whether an agent is currently selected.
|
||||
* @returns {Array} The combined list of agents, potentially including the 'None' agent.
|
||||
*/
|
||||
export const getCombinedAgents = (
|
||||
filteredAgentsByAvailability,
|
||||
includeNoneAgent,
|
||||
isAgentSelected
|
||||
) => {
|
||||
return [
|
||||
...(includeNoneAgent && isAgentSelected ? [createNoneAgent] : []),
|
||||
...filteredAgentsByAvailability,
|
||||
];
|
||||
};
|
||||
131
app/javascript/dashboard/helper/specs/agentHelper.spec.js
Normal file
131
app/javascript/dashboard/helper/specs/agentHelper.spec.js
Normal file
@@ -0,0 +1,131 @@
|
||||
import {
|
||||
getAgentsByAvailability,
|
||||
getSortedAgentsByAvailability,
|
||||
getAgentsByUpdatedPresence,
|
||||
getCombinedAgents,
|
||||
createNoneAgent,
|
||||
} from '../agentHelper';
|
||||
import {
|
||||
allAgentsData,
|
||||
onlineAgentsData,
|
||||
busyAgentsData,
|
||||
offlineAgentsData,
|
||||
sortedByAvailability,
|
||||
formattedAgentsByPresenceOnline,
|
||||
formattedAgentsByPresenceOffline,
|
||||
} from 'dashboard/helper/specs/fixtures/agentFixtures';
|
||||
|
||||
describe('agentHelper', () => {
|
||||
describe('getAgentsByAvailability', () => {
|
||||
it('returns agents by availability', () => {
|
||||
expect(getAgentsByAvailability(allAgentsData, 'online')).toEqual(
|
||||
onlineAgentsData
|
||||
);
|
||||
expect(getAgentsByAvailability(allAgentsData, 'busy')).toEqual(
|
||||
busyAgentsData
|
||||
);
|
||||
expect(getAgentsByAvailability(allAgentsData, 'offline')).toEqual(
|
||||
offlineAgentsData
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSortedAgentsByAvailability', () => {
|
||||
it('returns sorted agents by availability', () => {
|
||||
expect(getSortedAgentsByAvailability(allAgentsData)).toEqual(
|
||||
sortedByAvailability
|
||||
);
|
||||
});
|
||||
|
||||
it('returns an empty array when given an empty input', () => {
|
||||
expect(getSortedAgentsByAvailability([])).toEqual([]);
|
||||
});
|
||||
|
||||
it('maintains the order of agents with the same availability status', () => {
|
||||
const result = getSortedAgentsByAvailability(allAgentsData);
|
||||
expect(result[2].name).toBe('Honey Bee');
|
||||
expect(result[3].name).toBe('Samuel Keta');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAgentsByUpdatedPresence', () => {
|
||||
it('returns agents with updated presence', () => {
|
||||
const currentUser = {
|
||||
id: 1,
|
||||
accounts: [{ id: 1, availability_status: 'offline' }],
|
||||
};
|
||||
const currentAccountId = 1;
|
||||
|
||||
expect(
|
||||
getAgentsByUpdatedPresence(
|
||||
formattedAgentsByPresenceOnline,
|
||||
currentUser,
|
||||
currentAccountId
|
||||
)
|
||||
).toEqual(formattedAgentsByPresenceOffline);
|
||||
});
|
||||
|
||||
it('does not modify other agents presence', () => {
|
||||
const currentUser = {
|
||||
id: 2,
|
||||
accounts: [{ id: 1, availability_status: 'offline' }],
|
||||
};
|
||||
const currentAccountId = 1;
|
||||
|
||||
expect(
|
||||
getAgentsByUpdatedPresence(
|
||||
formattedAgentsByPresenceOnline,
|
||||
currentUser,
|
||||
currentAccountId
|
||||
)
|
||||
).toEqual(formattedAgentsByPresenceOnline);
|
||||
});
|
||||
|
||||
it('handles empty agent list', () => {
|
||||
const currentUser = {
|
||||
id: 1,
|
||||
accounts: [{ id: 1, availability_status: 'offline' }],
|
||||
};
|
||||
const currentAccountId = 1;
|
||||
|
||||
expect(
|
||||
getAgentsByUpdatedPresence([], currentUser, currentAccountId)
|
||||
).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getCombinedAgents', () => {
|
||||
it('includes None agent when includeNoneAgent is true and isAgentSelected is true', () => {
|
||||
const result = getCombinedAgents(sortedByAvailability, true, true);
|
||||
expect(result).toEqual([createNoneAgent, ...sortedByAvailability]);
|
||||
expect(result.length).toBe(sortedByAvailability.length + 1);
|
||||
expect(result[0]).toEqual(createNoneAgent);
|
||||
});
|
||||
|
||||
it('excludes None agent when includeNoneAgent is false', () => {
|
||||
const result = getCombinedAgents(sortedByAvailability, false, true);
|
||||
expect(result).toEqual(sortedByAvailability);
|
||||
expect(result.length).toBe(sortedByAvailability.length);
|
||||
expect(result[0]).not.toEqual(createNoneAgent);
|
||||
});
|
||||
|
||||
it('excludes None agent when isAgentSelected is false', () => {
|
||||
const result = getCombinedAgents(sortedByAvailability, true, false);
|
||||
expect(result).toEqual(sortedByAvailability);
|
||||
expect(result.length).toBe(sortedByAvailability.length);
|
||||
expect(result[0]).not.toEqual(createNoneAgent);
|
||||
});
|
||||
|
||||
it('returns only filtered agents when both includeNoneAgent and isAgentSelected are false', () => {
|
||||
const result = getCombinedAgents(sortedByAvailability, false, false);
|
||||
expect(result).toEqual(sortedByAvailability);
|
||||
expect(result.length).toBe(sortedByAvailability.length);
|
||||
});
|
||||
|
||||
it('handles empty filteredAgentsByAvailability array', () => {
|
||||
const result = getCombinedAgents([], true, true);
|
||||
expect(result).toEqual([createNoneAgent]);
|
||||
expect(result.length).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
184
app/javascript/dashboard/helper/specs/fixtures/agentFixtures.js
vendored
Normal file
184
app/javascript/dashboard/helper/specs/fixtures/agentFixtures.js
vendored
Normal file
@@ -0,0 +1,184 @@
|
||||
export const allAgentsData = [
|
||||
{
|
||||
account_id: 1,
|
||||
availability_status: 'online',
|
||||
available_name: 'John K',
|
||||
confirmed: true,
|
||||
email: 'john@chatwoot.com',
|
||||
id: 1,
|
||||
name: 'John Kennady',
|
||||
role: 'administrator',
|
||||
},
|
||||
{
|
||||
account_id: 1,
|
||||
availability_status: 'busy',
|
||||
available_name: 'Samuel K',
|
||||
confirmed: true,
|
||||
email: 'samuel@chatwoot.com',
|
||||
id: 2,
|
||||
name: 'Samuel Keta',
|
||||
role: 'agent',
|
||||
},
|
||||
{
|
||||
account_id: 1,
|
||||
availability_status: 'offline',
|
||||
available_name: 'James K',
|
||||
confirmed: true,
|
||||
email: 'james@chatwoot.com',
|
||||
id: 3,
|
||||
name: 'James Koti',
|
||||
role: 'agent',
|
||||
},
|
||||
{
|
||||
account_id: 1,
|
||||
availability_status: 'busy',
|
||||
available_name: 'Honey',
|
||||
confirmed: true,
|
||||
email: 'bee@chatwoot.com',
|
||||
id: 4,
|
||||
name: 'Honey Bee',
|
||||
role: 'agent',
|
||||
},
|
||||
{
|
||||
account_id: 1,
|
||||
availability_status: 'online',
|
||||
available_name: 'Abraham',
|
||||
confirmed: true,
|
||||
email: 'abraham@chatwoot.com',
|
||||
id: 5,
|
||||
name: 'Abraham Keta',
|
||||
role: 'agent',
|
||||
},
|
||||
];
|
||||
export const onlineAgentsData = [
|
||||
{
|
||||
account_id: 1,
|
||||
availability_status: 'online',
|
||||
available_name: 'Abraham',
|
||||
confirmed: true,
|
||||
email: 'abraham@chatwoot.com',
|
||||
id: 5,
|
||||
name: 'Abraham Keta',
|
||||
role: 'agent',
|
||||
},
|
||||
{
|
||||
account_id: 1,
|
||||
availability_status: 'online',
|
||||
available_name: 'John K',
|
||||
confirmed: true,
|
||||
email: 'john@chatwoot.com',
|
||||
id: 1,
|
||||
name: 'John Kennady',
|
||||
role: 'administrator',
|
||||
},
|
||||
];
|
||||
export const busyAgentsData = [
|
||||
{
|
||||
account_id: 1,
|
||||
availability_status: 'busy',
|
||||
available_name: 'Honey',
|
||||
confirmed: true,
|
||||
email: 'bee@chatwoot.com',
|
||||
id: 4,
|
||||
name: 'Honey Bee',
|
||||
role: 'agent',
|
||||
},
|
||||
{
|
||||
account_id: 1,
|
||||
availability_status: 'busy',
|
||||
available_name: 'Samuel K',
|
||||
confirmed: true,
|
||||
email: 'samuel@chatwoot.com',
|
||||
id: 2,
|
||||
name: 'Samuel Keta',
|
||||
role: 'agent',
|
||||
},
|
||||
];
|
||||
export const offlineAgentsData = [
|
||||
{
|
||||
account_id: 1,
|
||||
availability_status: 'offline',
|
||||
available_name: 'James K',
|
||||
confirmed: true,
|
||||
email: 'james@chatwoot.com',
|
||||
id: 3,
|
||||
name: 'James Koti',
|
||||
role: 'agent',
|
||||
},
|
||||
];
|
||||
export const sortedByAvailability = [
|
||||
{
|
||||
account_id: 1,
|
||||
availability_status: 'online',
|
||||
available_name: 'Abraham',
|
||||
confirmed: true,
|
||||
email: 'abraham@chatwoot.com',
|
||||
id: 5,
|
||||
name: 'Abraham Keta',
|
||||
role: 'agent',
|
||||
},
|
||||
{
|
||||
account_id: 1,
|
||||
availability_status: 'online',
|
||||
available_name: 'John K',
|
||||
confirmed: true,
|
||||
email: 'john@chatwoot.com',
|
||||
id: 1,
|
||||
name: 'John Kennady',
|
||||
role: 'administrator',
|
||||
},
|
||||
{
|
||||
account_id: 1,
|
||||
availability_status: 'busy',
|
||||
available_name: 'Honey',
|
||||
confirmed: true,
|
||||
email: 'bee@chatwoot.com',
|
||||
id: 4,
|
||||
name: 'Honey Bee',
|
||||
role: 'agent',
|
||||
},
|
||||
{
|
||||
account_id: 1,
|
||||
availability_status: 'busy',
|
||||
available_name: 'Samuel K',
|
||||
confirmed: true,
|
||||
email: 'samuel@chatwoot.com',
|
||||
id: 2,
|
||||
name: 'Samuel Keta',
|
||||
role: 'agent',
|
||||
},
|
||||
{
|
||||
account_id: 1,
|
||||
availability_status: 'offline',
|
||||
available_name: 'James K',
|
||||
confirmed: true,
|
||||
email: 'james@chatwoot.com',
|
||||
id: 3,
|
||||
name: 'James Koti',
|
||||
role: 'agent',
|
||||
},
|
||||
];
|
||||
export const formattedAgentsByPresenceOnline = [
|
||||
{
|
||||
account_id: 1,
|
||||
availability_status: 'online',
|
||||
available_name: 'Abraham',
|
||||
confirmed: true,
|
||||
email: 'abr@chatwoot.com',
|
||||
id: 1,
|
||||
name: 'Abraham Keta',
|
||||
role: 'agent',
|
||||
},
|
||||
];
|
||||
export const formattedAgentsByPresenceOffline = [
|
||||
{
|
||||
account_id: 1,
|
||||
availability_status: 'offline',
|
||||
available_name: 'Abraham',
|
||||
confirmed: true,
|
||||
email: 'abr@chatwoot.com',
|
||||
id: 1,
|
||||
name: 'Abraham Keta',
|
||||
role: 'agent',
|
||||
},
|
||||
];
|
||||
@@ -1,69 +0,0 @@
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
export default {
|
||||
computed: {
|
||||
...mapGetters({
|
||||
currentUser: 'getCurrentUser',
|
||||
currentAccountId: 'getCurrentAccountId',
|
||||
}),
|
||||
assignableAgents() {
|
||||
return this.$store.getters['inboxAssignableAgents/getAssignableAgents'](
|
||||
this.inboxId
|
||||
);
|
||||
},
|
||||
isAgentSelected() {
|
||||
return this.currentChat?.meta?.assignee;
|
||||
},
|
||||
createNoneAgent() {
|
||||
return {
|
||||
confirmed: true,
|
||||
name: 'None',
|
||||
id: 0,
|
||||
role: 'agent',
|
||||
account_id: 0,
|
||||
email: 'None',
|
||||
};
|
||||
},
|
||||
agentsList() {
|
||||
const agents = this.assignableAgents || [];
|
||||
const agentsByUpdatedPresence = this.getAgentsByUpdatedPresence(agents);
|
||||
const none = this.createNoneAgent;
|
||||
const filteredAgentsByAvailability = this.sortedAgentsByAvailability(
|
||||
agentsByUpdatedPresence
|
||||
);
|
||||
const filteredAgents = [
|
||||
...(this.isAgentSelected ? [none] : []),
|
||||
...filteredAgentsByAvailability,
|
||||
];
|
||||
return filteredAgents;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
getAgentsByAvailability(agents, availability) {
|
||||
return agents
|
||||
.filter(agent => agent.availability_status === availability)
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
},
|
||||
sortedAgentsByAvailability(agents) {
|
||||
const onlineAgents = this.getAgentsByAvailability(agents, 'online');
|
||||
const busyAgents = this.getAgentsByAvailability(agents, 'busy');
|
||||
const offlineAgents = this.getAgentsByAvailability(agents, 'offline');
|
||||
const filteredAgents = [...onlineAgents, ...busyAgents, ...offlineAgents];
|
||||
return filteredAgents;
|
||||
},
|
||||
getAgentsByUpdatedPresence(agents) {
|
||||
// Here we are updating the availability status of the current user dynamically (live) based on the current account availability status
|
||||
const agentsWithDynamicPresenceUpdate = agents.map(item =>
|
||||
item.id === this.currentUser.id
|
||||
? {
|
||||
...item,
|
||||
availability_status: this.currentUser.accounts.find(
|
||||
account => account.id === this.currentAccountId
|
||||
).availability_status,
|
||||
}
|
||||
: item
|
||||
);
|
||||
return agentsWithDynamicPresenceUpdate;
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -1,246 +0,0 @@
|
||||
export default {
|
||||
allAgents: [
|
||||
{
|
||||
account_id: 1,
|
||||
availability_status: 'online',
|
||||
available_name: 'John K',
|
||||
confirmed: true,
|
||||
email: 'john@chatwoot.com',
|
||||
id: 1,
|
||||
name: 'John Kennady',
|
||||
role: 'administrator',
|
||||
},
|
||||
{
|
||||
account_id: 1,
|
||||
availability_status: 'busy',
|
||||
available_name: 'Samuel K',
|
||||
confirmed: true,
|
||||
email: 'samuel@chatwoot.com',
|
||||
id: 2,
|
||||
name: 'Samuel Keta',
|
||||
role: 'agent',
|
||||
},
|
||||
{
|
||||
account_id: 1,
|
||||
availability_status: 'offline',
|
||||
available_name: 'James K',
|
||||
confirmed: true,
|
||||
email: 'james@chatwoot.com',
|
||||
id: 3,
|
||||
name: 'James Koti',
|
||||
role: 'agent',
|
||||
},
|
||||
{
|
||||
account_id: 1,
|
||||
availability_status: 'busy',
|
||||
available_name: 'Honey',
|
||||
confirmed: true,
|
||||
email: 'bee@chatwoot.com',
|
||||
id: 4,
|
||||
name: 'Honey Bee',
|
||||
role: 'agent',
|
||||
},
|
||||
{
|
||||
account_id: 1,
|
||||
availability_status: 'online',
|
||||
available_name: 'Abraham',
|
||||
confirmed: true,
|
||||
email: 'abraham@chatwoot.com',
|
||||
id: 5,
|
||||
name: 'Abraham Keta',
|
||||
role: 'agent',
|
||||
},
|
||||
],
|
||||
formattedAgents: [
|
||||
{
|
||||
account_id: 0,
|
||||
confirmed: true,
|
||||
email: 'None',
|
||||
id: 0,
|
||||
name: 'None',
|
||||
role: 'agent',
|
||||
},
|
||||
{
|
||||
account_id: 1,
|
||||
availability_status: 'online',
|
||||
available_name: 'Abraham',
|
||||
confirmed: true,
|
||||
email: 'abraham@chatwoot.com',
|
||||
id: 5,
|
||||
name: 'Abraham Keta',
|
||||
role: 'agent',
|
||||
},
|
||||
{
|
||||
account_id: 1,
|
||||
availability_status: 'online',
|
||||
available_name: 'John K',
|
||||
confirmed: true,
|
||||
email: 'john@chatwoot.com',
|
||||
id: 1,
|
||||
name: 'John Kennady',
|
||||
role: 'administrator',
|
||||
},
|
||||
{
|
||||
account_id: 1,
|
||||
availability_status: 'busy',
|
||||
available_name: 'Honey',
|
||||
confirmed: true,
|
||||
email: 'bee@chatwoot.com',
|
||||
id: 4,
|
||||
name: 'Honey Bee',
|
||||
role: 'agent',
|
||||
},
|
||||
{
|
||||
account_id: 1,
|
||||
availability_status: 'busy',
|
||||
available_name: 'Samuel K',
|
||||
confirmed: true,
|
||||
email: 'samuel@chatwoot.com',
|
||||
id: 2,
|
||||
name: 'Samuel Keta',
|
||||
role: 'agent',
|
||||
},
|
||||
{
|
||||
account_id: 1,
|
||||
availability_status: 'offline',
|
||||
available_name: 'James K',
|
||||
confirmed: true,
|
||||
email: 'james@chatwoot.com',
|
||||
id: 3,
|
||||
name: 'James Koti',
|
||||
role: 'agent',
|
||||
},
|
||||
],
|
||||
onlineAgents: [
|
||||
{
|
||||
account_id: 1,
|
||||
availability_status: 'online',
|
||||
available_name: 'Abraham',
|
||||
confirmed: true,
|
||||
email: 'abraham@chatwoot.com',
|
||||
id: 5,
|
||||
name: 'Abraham Keta',
|
||||
role: 'agent',
|
||||
},
|
||||
{
|
||||
account_id: 1,
|
||||
availability_status: 'online',
|
||||
available_name: 'John K',
|
||||
confirmed: true,
|
||||
email: 'john@chatwoot.com',
|
||||
id: 1,
|
||||
name: 'John Kennady',
|
||||
role: 'administrator',
|
||||
},
|
||||
],
|
||||
busyAgents: [
|
||||
{
|
||||
account_id: 1,
|
||||
availability_status: 'busy',
|
||||
available_name: 'Honey',
|
||||
confirmed: true,
|
||||
email: 'bee@chatwoot.com',
|
||||
id: 4,
|
||||
name: 'Honey Bee',
|
||||
role: 'agent',
|
||||
},
|
||||
{
|
||||
account_id: 1,
|
||||
availability_status: 'busy',
|
||||
available_name: 'Samuel K',
|
||||
confirmed: true,
|
||||
email: 'samuel@chatwoot.com',
|
||||
id: 2,
|
||||
name: 'Samuel Keta',
|
||||
role: 'agent',
|
||||
},
|
||||
],
|
||||
offlineAgents: [
|
||||
{
|
||||
account_id: 1,
|
||||
availability_status: 'offline',
|
||||
available_name: 'James K',
|
||||
confirmed: true,
|
||||
email: 'james@chatwoot.com',
|
||||
id: 3,
|
||||
name: 'James Koti',
|
||||
role: 'agent',
|
||||
},
|
||||
],
|
||||
sortedByAvailability: [
|
||||
{
|
||||
account_id: 1,
|
||||
availability_status: 'online',
|
||||
available_name: 'Abraham',
|
||||
confirmed: true,
|
||||
email: 'abraham@chatwoot.com',
|
||||
id: 5,
|
||||
name: 'Abraham Keta',
|
||||
role: 'agent',
|
||||
},
|
||||
{
|
||||
account_id: 1,
|
||||
availability_status: 'online',
|
||||
available_name: 'John K',
|
||||
confirmed: true,
|
||||
email: 'john@chatwoot.com',
|
||||
id: 1,
|
||||
name: 'John Kennady',
|
||||
role: 'administrator',
|
||||
},
|
||||
{
|
||||
account_id: 1,
|
||||
availability_status: 'busy',
|
||||
available_name: 'Honey',
|
||||
confirmed: true,
|
||||
email: 'bee@chatwoot.com',
|
||||
id: 4,
|
||||
name: 'Honey Bee',
|
||||
role: 'agent',
|
||||
},
|
||||
{
|
||||
account_id: 1,
|
||||
availability_status: 'busy',
|
||||
available_name: 'Samuel K',
|
||||
confirmed: true,
|
||||
email: 'samuel@chatwoot.com',
|
||||
id: 2,
|
||||
name: 'Samuel Keta',
|
||||
role: 'agent',
|
||||
},
|
||||
{
|
||||
account_id: 1,
|
||||
availability_status: 'offline',
|
||||
available_name: 'James K',
|
||||
confirmed: true,
|
||||
email: 'james@chatwoot.com',
|
||||
id: 3,
|
||||
name: 'James Koti',
|
||||
role: 'agent',
|
||||
},
|
||||
],
|
||||
formattedAgentsByPresenceOnline: [
|
||||
{
|
||||
account_id: 1,
|
||||
availability_status: 'online',
|
||||
available_name: 'Abraham',
|
||||
confirmed: true,
|
||||
email: 'abr@chatwoot.com',
|
||||
id: 1,
|
||||
name: 'Abraham Keta',
|
||||
role: 'agent',
|
||||
},
|
||||
],
|
||||
formattedAgentsByPresenceOffline: [
|
||||
{
|
||||
account_id: 1,
|
||||
availability_status: 'offline',
|
||||
available_name: 'Abraham',
|
||||
confirmed: true,
|
||||
email: 'abr@chatwoot.com',
|
||||
id: 1,
|
||||
name: 'Abraham Keta',
|
||||
role: 'agent',
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -1,140 +0,0 @@
|
||||
import { shallowMount, createLocalVue } from '@vue/test-utils';
|
||||
import agentMixin from '../agentMixin';
|
||||
import agentFixtures from './agentFixtures';
|
||||
import Vuex from 'vuex';
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(Vuex);
|
||||
|
||||
describe('agentMixin', () => {
|
||||
let getters;
|
||||
let store;
|
||||
beforeEach(() => {
|
||||
getters = {
|
||||
getCurrentUser: () => ({
|
||||
id: 1,
|
||||
accounts: [
|
||||
{
|
||||
id: 1,
|
||||
availability_status: 'online',
|
||||
auto_offline: false,
|
||||
},
|
||||
],
|
||||
}),
|
||||
getCurrentAccountId: () => 1,
|
||||
};
|
||||
store = new Vuex.Store({ getters });
|
||||
});
|
||||
|
||||
it('return agents by availability', () => {
|
||||
const Component = {
|
||||
render() {},
|
||||
title: 'TestComponent',
|
||||
mixins: [agentMixin],
|
||||
data() {
|
||||
return {
|
||||
inboxId: 1,
|
||||
currentChat: { meta: { assignee: { name: 'John' } } },
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
assignableAgents() {
|
||||
return agentFixtures.allAgents;
|
||||
},
|
||||
},
|
||||
};
|
||||
const wrapper = shallowMount(Component, { store, localVue });
|
||||
expect(
|
||||
wrapper.vm.getAgentsByAvailability(agentFixtures.allAgents, 'online')
|
||||
).toEqual(agentFixtures.onlineAgents);
|
||||
expect(
|
||||
wrapper.vm.getAgentsByAvailability(agentFixtures.allAgents, 'busy')
|
||||
).toEqual(agentFixtures.busyAgents);
|
||||
expect(
|
||||
wrapper.vm.getAgentsByAvailability(agentFixtures.allAgents, 'offline')
|
||||
).toEqual(agentFixtures.offlineAgents);
|
||||
});
|
||||
|
||||
it('return sorted agents by availability', () => {
|
||||
const Component = {
|
||||
render() {},
|
||||
title: 'TestComponent',
|
||||
mixins: [agentMixin],
|
||||
data() {
|
||||
return {
|
||||
inboxId: 1,
|
||||
currentChat: { meta: { assignee: { name: 'John' } } },
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
assignableAgents() {
|
||||
return agentFixtures.allAgents;
|
||||
},
|
||||
},
|
||||
};
|
||||
const wrapper = shallowMount(Component, { store, localVue });
|
||||
expect(
|
||||
wrapper.vm.sortedAgentsByAvailability(agentFixtures.allAgents)
|
||||
).toEqual(agentFixtures.sortedByAvailability);
|
||||
});
|
||||
|
||||
it('return formatted agents', () => {
|
||||
const Component = {
|
||||
render() {},
|
||||
title: 'TestComponent',
|
||||
mixins: [agentMixin],
|
||||
data() {
|
||||
return {
|
||||
inboxId: 1,
|
||||
currentChat: { meta: { assignee: { name: 'John' } } },
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
assignableAgents() {
|
||||
return agentFixtures.allAgents;
|
||||
},
|
||||
},
|
||||
};
|
||||
const wrapper = shallowMount(Component, { store, localVue });
|
||||
expect(wrapper.vm.agentsList).toEqual(agentFixtures.formattedAgents);
|
||||
});
|
||||
|
||||
it('return formatted agents by presence', () => {
|
||||
const Component = {
|
||||
render() {},
|
||||
title: 'TestComponent',
|
||||
mixins: [agentMixin],
|
||||
data() {
|
||||
return {
|
||||
inboxId: 1,
|
||||
currentChat: { meta: { assignee: { name: 'John' } } },
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
currentUser() {
|
||||
return {
|
||||
id: 1,
|
||||
accounts: [
|
||||
{
|
||||
id: 1,
|
||||
availability_status: 'offline',
|
||||
auto_offline: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
currentAccountId() {
|
||||
return 1;
|
||||
},
|
||||
assignableAgents() {
|
||||
return agentFixtures.allAgents;
|
||||
},
|
||||
},
|
||||
};
|
||||
const wrapper = shallowMount(Component, { store, localVue });
|
||||
expect(
|
||||
wrapper.vm.getAgentsByUpdatedPresence(
|
||||
agentFixtures.formattedAgentsByPresenceOnline
|
||||
)
|
||||
).toEqual(agentFixtures.formattedAgentsByPresenceOffline);
|
||||
});
|
||||
});
|
||||
@@ -1,18 +1,17 @@
|
||||
<script>
|
||||
import '@chatwoot/ninja-keys';
|
||||
import { useConversationLabels } from 'dashboard/composables/useConversationLabels';
|
||||
import { useAgentsList } from 'dashboard/composables/useAgentsList';
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
import conversationHotKeysMixin from './conversationHotKeys';
|
||||
import bulkActionsHotKeysMixin from './bulkActionsHotKeys';
|
||||
import inboxHotKeysMixin from './inboxHotKeys';
|
||||
import goToCommandHotKeys from './goToCommandHotKeys';
|
||||
import appearanceHotKeys from './appearanceHotKeys';
|
||||
import agentMixin from 'dashboard/mixins/agentMixin';
|
||||
import { GENERAL_EVENTS } from '../../../helper/AnalyticsHelper/events';
|
||||
|
||||
export default {
|
||||
mixins: [
|
||||
agentMixin,
|
||||
conversationHotKeysMixin,
|
||||
bulkActionsHotKeysMixin,
|
||||
inboxHotKeysMixin,
|
||||
@@ -28,7 +27,11 @@ export default {
|
||||
removeLabelFromConversation,
|
||||
} = useConversationLabels();
|
||||
|
||||
const { agentsList, assignableAgents } = useAgentsList();
|
||||
|
||||
return {
|
||||
agentsList,
|
||||
assignableAgents,
|
||||
activeLabels,
|
||||
inactiveLabels,
|
||||
addLabelToConversation,
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { useAgentsList } from 'dashboard/composables/useAgentsList';
|
||||
import ContactDetailsItem from './ContactDetailsItem.vue';
|
||||
import MultiselectDropdown from 'shared/components/ui/MultiselectDropdown.vue';
|
||||
import ConversationLabels from './labels/LabelBox.vue';
|
||||
import agentMixin from 'dashboard/mixins/agentMixin';
|
||||
import { CONVERSATION_PRIORITY } from '../../../../shared/constants/messages';
|
||||
import { CONVERSATION_EVENTS } from '../../../helper/AnalyticsHelper/events';
|
||||
|
||||
@@ -15,19 +15,17 @@ export default {
|
||||
MultiselectDropdown,
|
||||
ConversationLabels,
|
||||
},
|
||||
mixins: [agentMixin],
|
||||
props: {
|
||||
conversationId: {
|
||||
type: [Number, String],
|
||||
required: true,
|
||||
},
|
||||
// inboxId prop is used in /mixins/agentMixin,
|
||||
// remove this props when refactoring to composable if not needed
|
||||
// eslint-disable-next-line vue/no-unused-properties
|
||||
inboxId: {
|
||||
type: Number,
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const { agentsList } = useAgentsList();
|
||||
return {
|
||||
agentsList,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
import Spinner from 'shared/components/Spinner.vue';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { mapGetters } from 'vuex';
|
||||
import agentMixin from 'dashboard/mixins/agentMixin';
|
||||
import { useAgentsList } from 'dashboard/composables/useAgentsList';
|
||||
|
||||
import ThumbnailGroup from 'dashboard/components/widgets/ThumbnailGroup.vue';
|
||||
import MultiselectDropdownItems from 'shared/components/ui/MultiselectDropdownItems.vue';
|
||||
|
||||
@@ -12,19 +13,17 @@ export default {
|
||||
ThumbnailGroup,
|
||||
MultiselectDropdownItems,
|
||||
},
|
||||
mixins: [agentMixin],
|
||||
props: {
|
||||
conversationId: {
|
||||
type: [Number, String],
|
||||
required: true,
|
||||
},
|
||||
// inboxId prop is used in /mixins/agentMixin,
|
||||
// remove this props when refactoring to composable if not needed
|
||||
// eslint-disable-next-line vue/no-unused-properties
|
||||
inboxId: {
|
||||
type: Number,
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const { agentsList } = useAgentsList(false);
|
||||
return {
|
||||
agentsList,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user