diff --git a/app/javascript/dashboard/api/inbox/conversation.js b/app/javascript/dashboard/api/inbox/conversation.js index f94fca452..08820aac9 100644 --- a/app/javascript/dashboard/api/inbox/conversation.js +++ b/app/javascript/dashboard/api/inbox/conversation.js @@ -62,9 +62,10 @@ class ConversationApi extends ApiClient { }); } - assignAgent({ conversationId, agentId }) { + assignAgent({ conversationId, agentId, assigneeType }) { return axios.post(`${this.url}/${conversationId}/assignments`, { assignee_id: agentId, + assignee_type: assigneeType, }); } diff --git a/app/javascript/dashboard/api/specs/inbox/conversation.spec.js b/app/javascript/dashboard/api/specs/inbox/conversation.spec.js index de0d7a7d0..ea0ef3e75 100644 --- a/app/javascript/dashboard/api/specs/inbox/conversation.spec.js +++ b/app/javascript/dashboard/api/specs/inbox/conversation.spec.js @@ -90,11 +90,16 @@ describe('#ConversationAPI', () => { }); it('#assignAgent', () => { - conversationAPI.assignAgent({ conversationId: 12, agentId: 34 }); + conversationAPI.assignAgent({ + conversationId: 12, + agentId: 34, + assigneeType: 'AgentBot', + }); expect(axiosMock.post).toHaveBeenCalledWith( `/api/v1/conversations/12/assignments`, { assignee_id: 34, + assignee_type: 'AgentBot', } ); }); diff --git a/app/javascript/dashboard/composables/spec/useAgentsList.spec.js b/app/javascript/dashboard/composables/spec/useAgentsList.spec.js index 7f6d8e757..0f00fbcfc 100644 --- a/app/javascript/dashboard/composables/spec/useAgentsList.spec.js +++ b/app/javascript/dashboard/composables/spec/useAgentsList.spec.js @@ -31,6 +31,7 @@ const mockUseMapGetter = (overrides = {}) => { getSelectedChat: ref({ inbox_id: 1, meta: { assignee: true } }), getCurrentAccountId: ref(1), 'inboxAssignableAgents/getAssignableAgents': ref(() => allAgentsData), + 'agentBots/getBots': ref([]), }; const mergedGetters = { ...defaultGetters, ...overrides }; @@ -75,6 +76,23 @@ describe('useAgentsList', () => { expect(agentsList.value.length).toBe(formattedAgentsData.slice(1).length); }); + it('includes agent bots when includeAgentBots is true', () => { + mockUseMapGetter({ + 'agentBots/getBots': ref([{ id: 1, name: 'Bot', thumbnail: '' }]), + }); + + const { agentsList } = useAgentsList(true, true); + + expect(agentsList.value).toContainEqual( + expect.objectContaining({ + id: 1, + name: 'Bot', + assignee_type: 'AgentBot', + icon: 'i-lucide-bot', + }) + ); + }); + it('handles empty assignable agents', () => { mockUseMapGetter({ 'inboxAssignableAgents/getAssignableAgents': ref(() => []), diff --git a/app/javascript/dashboard/composables/useAgentsList.js b/app/javascript/dashboard/composables/useAgentsList.js index 47e843be6..f64cf12df 100644 --- a/app/javascript/dashboard/composables/useAgentsList.js +++ b/app/javascript/dashboard/composables/useAgentsList.js @@ -10,14 +10,19 @@ import { * A composable function that provides a list of agents for assignment. * * @param {boolean} [includeNoneAgent=true] - Whether to include a 'None' agent option. + * @param {boolean} [includeAgentBots=false] - Whether to include agent bots as assignment options. * @returns {Object} An object containing the agents list and assignable agents. */ -export function useAgentsList(includeNoneAgent = true) { +export function useAgentsList( + includeNoneAgent = true, + includeAgentBots = false +) { const { t } = useI18n(); const currentUser = useMapGetter('getCurrentUser'); const currentChat = useMapGetter('getSelectedChat'); const currentAccountId = useMapGetter('getCurrentAccountId'); const assignable = useMapGetter('inboxAssignableAgents/getAssignableAgents'); + const agentBots = useMapGetter('agentBots/getBots'); const inboxId = computed(() => currentChat.value?.inbox_id); const isAgentSelected = computed(() => currentChat.value?.meta?.assignee); @@ -47,6 +52,13 @@ export function useAgentsList(includeNoneAgent = true) { */ const agentsList = computed(() => { const agents = assignableAgents.value || []; + const bots = includeAgentBots + ? agentBots.value.map(bot => ({ + ...bot, + assignee_type: 'AgentBot', + icon: 'i-lucide-bot', + })) + : []; const agentsByUpdatedPresence = getAgentsByUpdatedPresence( agents, currentUser.value, @@ -60,6 +72,7 @@ export function useAgentsList(includeNoneAgent = true) { return [ ...(includeNoneAgent && isAgentSelected.value ? [createNoneAgent()] : []), ...filteredAgentsByAvailability, + ...bots, ]; }); diff --git a/app/javascript/dashboard/routes/dashboard/conversation/ConversationAction.vue b/app/javascript/dashboard/routes/dashboard/conversation/ConversationAction.vue index 8543968fe..c1732d2a9 100644 --- a/app/javascript/dashboard/routes/dashboard/conversation/ConversationAction.vue +++ b/app/javascript/dashboard/routes/dashboard/conversation/ConversationAction.vue @@ -25,7 +25,7 @@ export default { }, }, setup() { - const { agentsList } = useAgentsList(); + const { agentsList } = useAgentsList(true, true); return { agentsList, }; @@ -81,18 +81,27 @@ export default { }, assignedAgent: { get() { - return this.currentChat.meta.assignee; + const assignee = this.currentChat.meta.assignee; + if (!assignee) return assignee; + + return { + ...assignee, + assignee_type: this.currentChat.meta.assignee_type || 'User', + }; }, set(agent) { const agentId = agent ? agent.id : null; + const assigneeType = agent ? agent.assignee_type || 'User' : null; this.$store.dispatch('setCurrentChatAssignee', { conversationId: this.currentChat.id, assignee: agent, + assigneeType, }); this.$store .dispatch('assignAgent', { conversationId: this.currentChat.id, agentId, + assigneeType, }) .then(() => { useAlert(this.$t('CONVERSATION.CHANGE_AGENT')); @@ -158,6 +167,9 @@ export default { return false; }, }, + mounted() { + this.$store.dispatch('agentBots/get'); + }, methods: { onSelfAssign() { const { @@ -174,6 +186,7 @@ export default { account_id, availability_status, available_name, + assignee_type: 'User', email, id, name, @@ -183,7 +196,14 @@ export default { this.assignedAgent = selfAssign; }, onClickAssignAgent(selectedItem) { - if (this.assignedAgent && this.assignedAgent.id === selectedItem.id) { + const currentAssigneeType = this.assignedAgent?.assignee_type || 'User'; + const selectedAssigneeType = selectedItem.assignee_type || 'User'; + + if ( + this.assignedAgent && + this.assignedAgent.id === selectedItem.id && + currentAssigneeType === selectedAssigneeType + ) { this.assignedAgent = null; } else { this.assignedAgent = selectedItem; diff --git a/app/javascript/dashboard/store/modules/conversations/actions.js b/app/javascript/dashboard/store/modules/conversations/actions.js index 72ab8fa5e..f8fdecc36 100644 --- a/app/javascript/dashboard/store/modules/conversations/actions.js +++ b/app/javascript/dashboard/store/modules/conversations/actions.js @@ -208,23 +208,31 @@ const actions = { } }, - assignAgent: async ({ dispatch }, { conversationId, agentId }) => { + assignAgent: async ( + { dispatch }, + { conversationId, agentId, assigneeType } + ) => { try { const response = await ConversationApi.assignAgent({ conversationId, agentId, + assigneeType, }); dispatch('setCurrentChatAssignee', { conversationId, assignee: response.data, + assigneeType, }); } catch (error) { // Handle error } }, - setCurrentChatAssignee({ commit }, { conversationId, assignee }) { - commit(types.ASSIGN_AGENT, { conversationId, assignee }); + setCurrentChatAssignee( + { commit }, + { conversationId, assignee, assigneeType } + ) { + commit(types.ASSIGN_AGENT, { conversationId, assignee, assigneeType }); }, assignTeam: async ({ dispatch }, { conversationId, teamId }) => { diff --git a/app/javascript/dashboard/store/modules/conversations/index.js b/app/javascript/dashboard/store/modules/conversations/index.js index 8a13940c0..4f539e22d 100644 --- a/app/javascript/dashboard/store/modules/conversations/index.js +++ b/app/javascript/dashboard/store/modules/conversations/index.js @@ -108,10 +108,11 @@ export const mutations = { } }, - [types.ASSIGN_AGENT](_state, { conversationId, assignee }) { + [types.ASSIGN_AGENT](_state, { conversationId, assignee, assigneeType }) { const chat = getConversationById(_state)(conversationId); if (chat) { chat.meta.assignee = assignee; + chat.meta.assignee_type = assigneeType; } }, diff --git a/app/javascript/dashboard/store/modules/specs/conversations/actions.spec.js b/app/javascript/dashboard/store/modules/specs/conversations/actions.spec.js index fa052ec1b..5014b63ca 100644 --- a/app/javascript/dashboard/store/modules/specs/conversations/actions.spec.js +++ b/app/javascript/dashboard/store/modules/specs/conversations/actions.spec.js @@ -357,11 +357,12 @@ describe('#actions', () => { }); await actions.assignAgent( { dispatch }, - { conversationId: 1, agentId: 1 } + { conversationId: 1, agentId: 1, assigneeType: 'AgentBot' } ); expect(dispatch).toHaveBeenCalledWith('setCurrentChatAssignee', { conversationId: 1, assignee: { id: 1, name: 'User' }, + assigneeType: 'AgentBot', }); }); }); @@ -371,6 +372,7 @@ describe('#actions', () => { const payload = { conversationId: 1, assignee: { id: 1, name: 'User' }, + assigneeType: 'AgentBot', }; await actions.setCurrentChatAssignee({ commit }, payload); expect(commit).toHaveBeenCalledTimes(1); diff --git a/app/javascript/dashboard/store/modules/specs/conversations/mutations.spec.js b/app/javascript/dashboard/store/modules/specs/conversations/mutations.spec.js index fc1c61b35..a97bdad53 100644 --- a/app/javascript/dashboard/store/modules/specs/conversations/mutations.spec.js +++ b/app/javascript/dashboard/store/modules/specs/conversations/mutations.spec.js @@ -712,8 +712,10 @@ describe('#mutations', () => { mutations[types.ASSIGN_AGENT](state, { conversationId: 1, assignee, + assigneeType: 'AgentBot', }); expect(state.allConversations[0].meta.assignee).toEqual(assignee); + expect(state.allConversations[0].meta.assignee_type).toEqual('AgentBot'); expect(state.allConversations[1].meta.assignee).toBeUndefined(); }); }); diff --git a/app/javascript/shared/components/ui/MultiselectDropdownItems.vue b/app/javascript/shared/components/ui/MultiselectDropdownItems.vue index e3d072dd8..e4e59b50b 100644 --- a/app/javascript/shared/components/ui/MultiselectDropdownItems.vue +++ b/app/javascript/shared/components/ui/MultiselectDropdownItems.vue @@ -67,7 +67,13 @@ export default { this.$refs.searchbar.focus(); }, isActive(option) { - return this.selectedItems.some(item => item && option.id === item.id); + return this.selectedItems.some(item => { + if (!item || option.id !== item.id) return false; + + return ( + (option.assignee_type || 'User') === (item.assignee_type || 'User') + ); + }); }, }, }; @@ -88,7 +94,10 @@ export default {
- +