From ca9bedfba1ef864f5609111ab285c3069b106d61 Mon Sep 17 00:00:00 2001 From: Sojan Jose Date: Fri, 26 Jun 2026 02:07:41 -0700 Subject: [PATCH] feat: expose assignable conversation owners --- .../api/v1/accounts/inboxes_controller.rb | 5 ++++ app/javascript/dashboard/api/inboxes.js | 4 ++++ .../dashboard/api/specs/inboxes.spec.js | 8 +++++++ .../composables/spec/useAgentsList.spec.js | 13 ++++++++-- .../dashboard/composables/useAgentsList.js | 24 ++++++++++++------- .../conversation/ConversationAction.vue | 5 +++- .../store/modules/inboxAssignableAgents.js | 19 +++++++++++++++ .../inboxAssignableMembers/actions.spec.js | 22 +++++++++++++++++ .../inboxAssignableMembers/mutations.spec.js | 12 ++++++++++ app/policies/inbox_policy.rb | 4 ++++ .../inboxes/assignable_owners.json.jbuilder | 15 ++++++++++++ config/routes.rb | 1 + .../v1/accounts/inboxes_controller_spec.rb | 22 +++++++++++++++++ 13 files changed, 142 insertions(+), 12 deletions(-) create mode 100644 app/views/api/v1/accounts/inboxes/assignable_owners.json.jbuilder diff --git a/app/controllers/api/v1/accounts/inboxes_controller.rb b/app/controllers/api/v1/accounts/inboxes_controller.rb index 757af9b62..0c7756d2f 100644 --- a/app/controllers/api/v1/accounts/inboxes_controller.rb +++ b/app/controllers/api/v1/accounts/inboxes_controller.rb @@ -21,6 +21,11 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController @assignable_agents = @inbox.assignable_agents end + def assignable_owners + @assignable_agents = @inbox.assignable_agents.select(&:confirmed?) + @agent_bots = AgentBot.accessible_to(Current.account) + end + def campaigns @campaigns = @inbox.campaigns end diff --git a/app/javascript/dashboard/api/inboxes.js b/app/javascript/dashboard/api/inboxes.js index 3c1d17fc8..0251e76ea 100644 --- a/app/javascript/dashboard/api/inboxes.js +++ b/app/javascript/dashboard/api/inboxes.js @@ -15,6 +15,10 @@ class Inboxes extends CacheEnabledApiClient { return axios.get(`${this.url}/${inboxId}/campaigns`); } + getAssignableOwners(inboxId) { + return axios.get(`${this.url}/${inboxId}/assignable_owners`); + } + deleteInboxAvatar(inboxId) { return axios.delete(`${this.url}/${inboxId}/avatar`); } diff --git a/app/javascript/dashboard/api/specs/inboxes.spec.js b/app/javascript/dashboard/api/specs/inboxes.spec.js index 64ba44aea..260efb907 100644 --- a/app/javascript/dashboard/api/specs/inboxes.spec.js +++ b/app/javascript/dashboard/api/specs/inboxes.spec.js @@ -9,6 +9,7 @@ describe('#InboxesAPI', () => { expect(inboxesAPI).toHaveProperty('create'); expect(inboxesAPI).toHaveProperty('update'); expect(inboxesAPI).toHaveProperty('delete'); + expect(inboxesAPI).toHaveProperty('getAssignableOwners'); expect(inboxesAPI).toHaveProperty('getCampaigns'); expect(inboxesAPI).toHaveProperty('getAgentBot'); expect(inboxesAPI).toHaveProperty('setAgentBot'); @@ -37,6 +38,13 @@ describe('#InboxesAPI', () => { expect(axiosMock.get).toHaveBeenCalledWith('/api/v1/inboxes/2/campaigns'); }); + it('#getAssignableOwners', () => { + inboxesAPI.getAssignableOwners(2); + expect(axiosMock.get).toHaveBeenCalledWith( + '/api/v1/inboxes/2/assignable_owners' + ); + }); + it('#deleteInboxAvatar', () => { inboxesAPI.deleteInboxAvatar(2); expect(axiosMock.delete).toHaveBeenCalledWith('/api/v1/inboxes/2/avatar'); diff --git a/app/javascript/dashboard/composables/spec/useAgentsList.spec.js b/app/javascript/dashboard/composables/spec/useAgentsList.spec.js index 0f00fbcfc..5e6c718e2 100644 --- a/app/javascript/dashboard/composables/spec/useAgentsList.spec.js +++ b/app/javascript/dashboard/composables/spec/useAgentsList.spec.js @@ -31,7 +31,7 @@ const mockUseMapGetter = (overrides = {}) => { getSelectedChat: ref({ inbox_id: 1, meta: { assignee: true } }), getCurrentAccountId: ref(1), 'inboxAssignableAgents/getAssignableAgents': ref(() => allAgentsData), - 'agentBots/getBots': ref([]), + 'inboxAssignableAgents/getAssignableOwners': ref(() => []), }; const mergedGetters = { ...defaultGetters, ...overrides }; @@ -78,7 +78,16 @@ describe('useAgentsList', () => { it('includes agent bots when includeAgentBots is true', () => { mockUseMapGetter({ - 'agentBots/getBots': ref([{ id: 1, name: 'Bot', thumbnail: '' }]), + 'inboxAssignableAgents/getAssignableOwners': ref(() => [ + { id: 2, name: 'Agent', assignee_type: 'User' }, + { + id: 1, + name: 'Bot', + thumbnail: '', + assignee_type: 'AgentBot', + icon: 'i-lucide-bot', + }, + ]), }); const { agentsList } = useAgentsList(true, true); diff --git a/app/javascript/dashboard/composables/useAgentsList.js b/app/javascript/dashboard/composables/useAgentsList.js index f64cf12df..56dfe44c4 100644 --- a/app/javascript/dashboard/composables/useAgentsList.js +++ b/app/javascript/dashboard/composables/useAgentsList.js @@ -22,7 +22,9 @@ export function useAgentsList( const currentChat = useMapGetter('getSelectedChat'); const currentAccountId = useMapGetter('getCurrentAccountId'); const assignable = useMapGetter('inboxAssignableAgents/getAssignableAgents'); - const agentBots = useMapGetter('agentBots/getBots'); + const assignableOwners = useMapGetter( + 'inboxAssignableAgents/getAssignableOwners' + ); const inboxId = computed(() => currentChat.value?.inbox_id); const isAgentSelected = computed(() => currentChat.value?.meta?.assignee); @@ -47,18 +49,22 @@ export function useAgentsList( return inboxId.value ? assignable.value(inboxId.value) : []; }); + const owners = computed(() => { + return includeAgentBots && inboxId.value + ? assignableOwners.value(inboxId.value) + : []; + }); + /** * @type {import('vue').ComputedRef} */ const agentsList = computed(() => { - const agents = assignableAgents.value || []; - const bots = includeAgentBots - ? agentBots.value.map(bot => ({ - ...bot, - assignee_type: 'AgentBot', - icon: 'i-lucide-bot', - })) - : []; + const agents = includeAgentBots + ? owners.value.filter(owner => owner.assignee_type === 'User') + : assignableAgents.value || []; + const bots = owners.value.filter( + owner => owner.assignee_type === 'AgentBot' + ); const agentsByUpdatedPresence = getAgentsByUpdatedPresence( agents, currentUser.value, diff --git a/app/javascript/dashboard/routes/dashboard/conversation/ConversationAction.vue b/app/javascript/dashboard/routes/dashboard/conversation/ConversationAction.vue index c1732d2a9..d7f4ce60d 100644 --- a/app/javascript/dashboard/routes/dashboard/conversation/ConversationAction.vue +++ b/app/javascript/dashboard/routes/dashboard/conversation/ConversationAction.vue @@ -168,7 +168,10 @@ export default { }, }, mounted() { - this.$store.dispatch('agentBots/get'); + this.$store.dispatch( + 'inboxAssignableAgents/fetchAssignableOwners', + this.currentChat.inbox_id + ); }, methods: { onSelfAssign() { diff --git a/app/javascript/dashboard/store/modules/inboxAssignableAgents.js b/app/javascript/dashboard/store/modules/inboxAssignableAgents.js index 1b129e3ef..e7f78e683 100644 --- a/app/javascript/dashboard/store/modules/inboxAssignableAgents.js +++ b/app/javascript/dashboard/store/modules/inboxAssignableAgents.js @@ -1,7 +1,9 @@ import AssignableAgentsAPI from '../../api/assignableAgents'; +import InboxesAPI from '../../api/inboxes'; const state = { records: {}, + ownerRecords: {}, uiFlags: { isFetching: false, }, @@ -10,6 +12,7 @@ const state = { export const types = { SET_INBOX_ASSIGNABLE_AGENTS_UI_FLAG: 'SET_INBOX_ASSIGNABLE_AGENTS_UI_FLAG', SET_INBOX_ASSIGNABLE_AGENTS: 'SET_INBOX_ASSIGNABLE_AGENTS', + SET_INBOX_ASSIGNABLE_OWNERS: 'SET_INBOX_ASSIGNABLE_OWNERS', }; export const getters = { @@ -18,6 +21,7 @@ export const getters = { const verifiedAgents = allAgents.filter(record => record.confirmed); return verifiedAgents; }, + getAssignableOwners: $state => inboxId => $state.ownerRecords[inboxId] || [], getUIFlags($state) { return $state.uiFlags; }, @@ -40,6 +44,15 @@ export const actions = { commit(types.SET_INBOX_ASSIGNABLE_AGENTS_UI_FLAG, { isFetching: false }); } }, + async fetchAssignableOwners({ commit }, inboxId) { + const { + data: { payload }, + } = await InboxesAPI.getAssignableOwners(inboxId); + commit(types.SET_INBOX_ASSIGNABLE_OWNERS, { + inboxId, + owners: payload, + }); + }, }; export const mutations = { @@ -55,6 +68,12 @@ export const mutations = { [inboxId]: members, }; }, + [types.SET_INBOX_ASSIGNABLE_OWNERS]: ($state, { inboxId, owners }) => { + $state.ownerRecords = { + ...$state.ownerRecords, + [inboxId]: owners, + }; + }, }; export default { diff --git a/app/javascript/dashboard/store/modules/specs/inboxAssignableMembers/actions.spec.js b/app/javascript/dashboard/store/modules/specs/inboxAssignableMembers/actions.spec.js index eac8e7d08..e29433291 100644 --- a/app/javascript/dashboard/store/modules/specs/inboxAssignableMembers/actions.spec.js +++ b/app/javascript/dashboard/store/modules/specs/inboxAssignableMembers/actions.spec.js @@ -1,12 +1,18 @@ import axios from 'axios'; +import InboxesAPI from 'dashboard/api/inboxes'; import { actions, types } from '../../inboxAssignableAgents'; import agentsData from './fixtures'; const commit = vi.fn(); global.axios = axios; vi.mock('axios'); +vi.mock('dashboard/api/inboxes'); describe('#actions', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + describe('#fetch', () => { it('sends correct actions if API is success', async () => { axios.get.mockResolvedValue({ @@ -33,4 +39,20 @@ describe('#actions', () => { ]); }); }); + + describe('#fetchAssignableOwners', () => { + it('stores assignable owners for the inbox', async () => { + InboxesAPI.getAssignableOwners.mockResolvedValue({ + data: { payload: agentsData }, + }); + + await actions.fetchAssignableOwners({ commit }, 1); + + expect(InboxesAPI.getAssignableOwners).toHaveBeenCalledWith(1); + expect(commit).toHaveBeenCalledWith(types.SET_INBOX_ASSIGNABLE_OWNERS, { + inboxId: 1, + owners: agentsData, + }); + }); + }); }); diff --git a/app/javascript/dashboard/store/modules/specs/inboxAssignableMembers/mutations.spec.js b/app/javascript/dashboard/store/modules/specs/inboxAssignableMembers/mutations.spec.js index da0bb2a17..af563e9d5 100644 --- a/app/javascript/dashboard/store/modules/specs/inboxAssignableMembers/mutations.spec.js +++ b/app/javascript/dashboard/store/modules/specs/inboxAssignableMembers/mutations.spec.js @@ -13,4 +13,16 @@ describe('#mutations', () => { expect(state.records).toEqual({ 1: agentsData }); }); }); + + describe('#SET_INBOX_ASSIGNABLE_OWNERS', () => { + it('adds inbox owners to ownerRecords', () => { + const state = { ownerRecords: {} }; + mutations[types.SET_INBOX_ASSIGNABLE_OWNERS](state, { + owners: [...agentsData], + inboxId: 1, + }); + + expect(state.ownerRecords).toEqual({ 1: agentsData }); + }); + }); }); diff --git a/app/policies/inbox_policy.rb b/app/policies/inbox_policy.rb index 9c74c35e7..e0d55cd0a 100644 --- a/app/policies/inbox_policy.rb +++ b/app/policies/inbox_policy.rb @@ -30,6 +30,10 @@ class InboxPolicy < ApplicationPolicy true end + def assignable_owners? + true + end + def agent_bot? true end diff --git a/app/views/api/v1/accounts/inboxes/assignable_owners.json.jbuilder b/app/views/api/v1/accounts/inboxes/assignable_owners.json.jbuilder new file mode 100644 index 000000000..018597125 --- /dev/null +++ b/app/views/api/v1/accounts/inboxes/assignable_owners.json.jbuilder @@ -0,0 +1,15 @@ +owners = @assignable_agents.map { |agent| { type: 'User', resource: agent } } +owners += @agent_bots.map { |agent_bot| { type: 'AgentBot', resource: agent_bot } } + +json.payload do + json.array! owners do |owner| + if owner[:type] == 'User' + json.partial! 'api/v1/models/agent', formats: [:json], resource: owner[:resource] + json.assignee_type 'User' + else + json.partial! 'api/v1/models/agent_bot_slim', formats: [:json], resource: owner[:resource] + json.assignee_type 'AgentBot' + json.icon 'i-lucide-bot' + end + end +end diff --git a/config/routes.rb b/config/routes.rb index 64e1df440..939290d45 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -254,6 +254,7 @@ Rails.application.routes.draw do resources :custom_filters, only: [:index, :show, :create, :update, :destroy] resources :inboxes, only: [:index, :show, :create, :update, :destroy] do get :assignable_agents, on: :member + get :assignable_owners, on: :member get :campaigns, on: :member get :agent_bot, on: :member post :set_agent_bot, on: :member diff --git a/spec/controllers/api/v1/accounts/inboxes_controller_spec.rb b/spec/controllers/api/v1/accounts/inboxes_controller_spec.rb index 0fd6ad7bf..aac418e7a 100644 --- a/spec/controllers/api/v1/accounts/inboxes_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/inboxes_controller_spec.rb @@ -251,6 +251,28 @@ RSpec.describe 'Inboxes API', type: :request do end end + describe 'GET /api/v1/accounts/{account.id}/inboxes/{inbox.id}/assignable_owners' do + let(:inbox) { create(:inbox, account: account) } + let!(:account_bot) { create(:agent_bot, account: account, name: 'Account bot') } + let!(:global_bot) { create(:agent_bot, account: nil, name: 'Global bot') } + + before do + create(:inbox_member, user: agent, inbox: inbox) + end + + it 'returns assignable agents and accessible agent bots' do + get "/api/v1/accounts/#{account.id}/inboxes/#{inbox.id}/assignable_owners", + headers: admin.create_new_auth_token, + as: :json + + expect(response).to have_http_status(:success) + + response_data = response.parsed_body['payload'] + expect(response_data.pluck('assignee_type')).to include('User', 'AgentBot') + expect(response_data.pluck('name')).to include(agent.name, admin.name, account_bot.name, global_bot.name) + end + end + describe 'GET /api/v1/accounts/{account.id}/inboxes/{inbox.id}/campaigns' do let(:inbox) { create(:inbox, account: account) }