From a8001ccabc47f8945315bd781954806113b78e8f Mon Sep 17 00:00:00 2001 From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Date: Fri, 14 Mar 2025 05:14:46 +0530 Subject: [PATCH 1/9] fix: Translate "None" option in automation select (#11076) # Pull Request Template ## Description This PR includes a translation update for the "None" option in the automation select for both agents and teams ## Type of change - [x] Bug fix (non-breaking change which fixes an issue) ## 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 --- .../composables/useAutomationValues.js | 15 ++++++- .../dashboard/helper/automationHelper.js | 15 ++----- .../helper/specs/automationHelper.spec.js | 40 +++++++++++++++++++ .../dashboard/i18n/locale/en/automation.json | 3 +- 4 files changed, 60 insertions(+), 13 deletions(-) diff --git a/app/javascript/dashboard/composables/useAutomationValues.js b/app/javascript/dashboard/composables/useAutomationValues.js index 88568fee5..4df46be8f 100644 --- a/app/javascript/dashboard/composables/useAutomationValues.js +++ b/app/javascript/dashboard/composables/useAutomationValues.js @@ -16,7 +16,6 @@ import { export default function useAutomationValues() { const getters = useStoreGetters(); const { t } = useI18n(); - const agents = useMapGetter('agents/getAgents'); const campaigns = useMapGetter('campaigns/getAllCampaigns'); const contacts = useMapGetter('contacts/getContacts'); @@ -61,6 +60,19 @@ export default function useAutomationValues() { ]; }); + /** + * Adds a translated "None" option to the beginning of a list + * @param {Array} list - The list to add "None" to + * @returns {Array} A new array with "None" option at the beginning + */ + const addNoneToList = list => [ + { + id: 'nil', + name: t('AUTOMATION.NONE_OPTION') || 'None', + }, + ...(list || []), + ]; + /** * Gets the condition dropdown values for a given type. * @param {string} type - The type of condition. @@ -95,6 +107,7 @@ export default function useAutomationValues() { slaPolicies: slaPolicies.value, languages, type, + addNoneToListFn: addNoneToList, }); }; diff --git a/app/javascript/dashboard/helper/automationHelper.js b/app/javascript/dashboard/helper/automationHelper.js index 6ff941822..07186c122 100644 --- a/app/javascript/dashboard/helper/automationHelper.js +++ b/app/javascript/dashboard/helper/automationHelper.js @@ -87,6 +87,7 @@ export const generateCustomAttributeTypes = (customAttributes, type) => { }; export const generateConditionOptions = (options, key = 'id') => { + if (!options || !Array.isArray(options)) return []; return options.map(i => { return { id: i[key], @@ -95,25 +96,17 @@ export const generateConditionOptions = (options, key = 'id') => { }); }; -// Add the "None" option to the agent list -export const addNoneToList = agents => [ - { - id: 'nil', - name: 'None', - }, - ...(agents || []), -]; - export const getActionOptions = ({ agents, teams, labels, slaPolicies, type, + addNoneToListFn, }) => { const actionsMap = { - assign_agent: addNoneToList(agents), - assign_team: addNoneToList(teams), + assign_agent: addNoneToListFn ? addNoneToListFn(agents) : agents, + assign_team: addNoneToListFn ? addNoneToListFn(teams) : teams, send_email_to_team: teams, add_label: generateConditionOptions(labels, 'title'), remove_label: generateConditionOptions(labels, 'title'), diff --git a/app/javascript/dashboard/helper/specs/automationHelper.spec.js b/app/javascript/dashboard/helper/specs/automationHelper.spec.js index b55da1bad..10088963a 100644 --- a/app/javascript/dashboard/helper/specs/automationHelper.spec.js +++ b/app/javascript/dashboard/helper/specs/automationHelper.spec.js @@ -118,6 +118,46 @@ describe('getActionOptions', () => { expectedOptions ); }); + + it('adds None option when addNoneToListFn is provided', () => { + const mockAddNoneToListFn = list => [ + { id: 'nil', name: 'None' }, + ...(list || []), + ]; + + const agents = [ + { id: 1, name: 'Agent 1' }, + { id: 2, name: 'Agent 2' }, + ]; + + const expectedOptions = [ + { id: 'nil', name: 'None' }, + { id: 1, name: 'Agent 1' }, + { id: 2, name: 'Agent 2' }, + ]; + + expect( + helpers.getActionOptions({ + agents, + type: 'assign_agent', + addNoneToListFn: mockAddNoneToListFn, + }) + ).toEqual(expectedOptions); + }); + + it('does not add None option when addNoneToListFn is not provided', () => { + const agents = [ + { id: 1, name: 'Agent 1' }, + { id: 2, name: 'Agent 2' }, + ]; + + expect( + helpers.getActionOptions({ + agents, + type: 'assign_agent', + }) + ).toEqual(agents); + }); }); describe('getConditionOptions', () => { diff --git a/app/javascript/dashboard/i18n/locale/en/automation.json b/app/javascript/dashboard/i18n/locale/en/automation.json index 483965f53..bb4946416 100644 --- a/app/javascript/dashboard/i18n/locale/en/automation.json +++ b/app/javascript/dashboard/i18n/locale/en/automation.json @@ -125,6 +125,7 @@ "ACTION_PARAMETERS_REQUIRED": "Action parameters are required", "ATLEAST_ONE_CONDITION_REQUIRED": "At least one condition is required", "ATLEAST_ONE_ACTION_REQUIRED": "At least one action is required" - } + }, + "NONE_OPTION": "None" } } From 325dc4a741f28d2b0cbc478a97a736bca14a032e Mon Sep 17 00:00:00 2001 From: Pranav Date: Thu, 13 Mar 2025 17:46:48 -0700 Subject: [PATCH 2/9] fix: Move contact events to account stream rather than individual user stream (#11082) --- app/channels/room_channel.rb | 17 ++++++++++------- app/listeners/action_cable_listener.rb | 20 ++++++++------------ spec/channels/room_channel_spec.rb | 9 +++++++++ spec/listeners/action_cable_listener_spec.rb | 4 +--- 4 files changed, 28 insertions(+), 22 deletions(-) diff --git a/app/channels/room_channel.rb b/app/channels/room_channel.rb index 0680f1458..629078229 100644 --- a/app/channels/room_channel.rb +++ b/app/channels/room_channel.rb @@ -2,10 +2,9 @@ class RoomChannel < ApplicationCable::Channel def subscribed # TODO: should we only do ensure stream if current account is present? # for now going ahead with guard clauses in update_subscription and broadcast_presence - - ensure_stream current_user current_account + ensure_stream update_subscription broadcast_presence end @@ -22,12 +21,12 @@ class RoomChannel < ApplicationCable::Channel data = { account_id: @current_account.id, users: ::OnlineStatusTracker.get_available_users(@current_account.id) } data[:contacts] = ::OnlineStatusTracker.get_available_contacts(@current_account.id) if @current_user.is_a? User - ActionCable.server.broadcast(@pubsub_token, { event: 'presence.update', data: data }) + ActionCable.server.broadcast(pubsub_token, { event: 'presence.update', data: data }) end def ensure_stream - @pubsub_token = params[:pubsub_token] - stream_from @pubsub_token + stream_from pubsub_token + stream_from "account_#{@current_account.id}" if @current_account.present? && @current_user.is_a?(User) end def update_subscription @@ -36,11 +35,15 @@ class RoomChannel < ApplicationCable::Channel ::OnlineStatusTracker.update_presence(@current_account.id, @current_user.class.name, @current_user.id) end + def pubsub_token + @pubsub_token ||= params[:pubsub_token] + end + def current_user @current_user ||= if params[:user_id].blank? - ContactInbox.find_by!(pubsub_token: @pubsub_token).contact + ContactInbox.find_by!(pubsub_token: pubsub_token).contact else - User.find_by!(pubsub_token: @pubsub_token, id: params[:user_id]) + User.find_by!(pubsub_token: pubsub_token, id: params[:user_id]) end end diff --git a/app/listeners/action_cable_listener.rb b/app/listeners/action_cable_listener.rb index 99a8fe2af..37dc939e7 100644 --- a/app/listeners/action_cable_listener.rb +++ b/app/listeners/action_cable_listener.rb @@ -137,30 +137,22 @@ class ActionCableListener < BaseListener def contact_created(event) contact, account = extract_contact_and_account(event) - tokens = user_tokens(account, account.agents) - - broadcast(account, tokens, CONTACT_CREATED, contact.push_event_data) + broadcast(account, [account_token(account)], CONTACT_CREATED, contact.push_event_data) end def contact_updated(event) contact, account = extract_contact_and_account(event) - tokens = user_tokens(account, account.agents) - - broadcast(account, tokens, CONTACT_UPDATED, contact.push_event_data) + broadcast(account, [account_token(account)], CONTACT_UPDATED, contact.push_event_data) end def contact_merged(event) contact, account = extract_contact_and_account(event) - tokens = event.data[:tokens] - - broadcast(account, tokens, CONTACT_MERGED, contact.push_event_data) + broadcast(account, [account_token(account)], CONTACT_MERGED, contact.push_event_data) end def contact_deleted(event) contact, account = extract_contact_and_account(event) - tokens = user_tokens(account, account.agents) - - broadcast(account, tokens, CONTACT_DELETED, contact.push_event_data) + broadcast(account, [account_token(account)], CONTACT_DELETED, contact.push_event_data) end def conversation_mentioned(event) @@ -172,6 +164,10 @@ class ActionCableListener < BaseListener private + def account_token(account) + "account_#{account.id}" + end + def typing_event_listener_tokens(account, conversation, user) current_user_token = user.is_a?(Contact) ? conversation.contact_inbox.pubsub_token : user.pubsub_token (user_tokens(account, conversation.inbox.members) + [conversation.contact_inbox.pubsub_token]) - [current_user_token] diff --git a/spec/channels/room_channel_spec.rb b/spec/channels/room_channel_spec.rb index 362a3d405..b39725f04 100644 --- a/spec/channels/room_channel_spec.rb +++ b/spec/channels/room_channel_spec.rb @@ -2,6 +2,8 @@ require 'rails_helper' RSpec.describe RoomChannel do let!(:contact_inbox) { create(:contact_inbox) } + let!(:account) { create(:account) } + let!(:user) { create(:user, account: account) } before do stub_connection @@ -12,4 +14,11 @@ RSpec.describe RoomChannel do expect(subscription).to be_confirmed expect(subscription).to have_stream_for(contact_inbox.pubsub_token) end + + it 'subscribes to a stream when pubsub_token is provided for user' do + subscribe(user_id: user.id, pubsub_token: user.pubsub_token, account_id: account.id) + expect(subscription).to be_confirmed + expect(subscription).to have_stream_for(user.pubsub_token) + expect(subscription).to have_stream_for("account_#{account.id}") + end end diff --git a/spec/listeners/action_cable_listener_spec.rb b/spec/listeners/action_cable_listener_spec.rb index b353f330d..55b74116c 100644 --- a/spec/listeners/action_cable_listener_spec.rb +++ b/spec/listeners/action_cable_listener_spec.rb @@ -121,9 +121,7 @@ describe ActionCableListener do it 'sends message to account admins, inbox agents' do expect(ActionCableBroadcastJob).to receive(:perform_later).with( - a_collection_containing_exactly( - agent.pubsub_token, admin.pubsub_token - ), + ["account_#{account.id}"], 'contact.deleted', contact.push_event_data.merge(account_id: account.id) ) From ed970ee190802f9411c8123b7f1e9efbe5b8a4fe Mon Sep 17 00:00:00 2001 From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Date: Fri, 14 Mar 2025 14:28:14 +0530 Subject: [PATCH 3/9] chore: Update settings to match the new design (#11084) --- .../dashboard/i18n/locale/en/agentBots.json | 4 +- .../i18n/locale/en/integrationApps.json | 2 +- .../routes/dashboard/settings/Wrapper.vue | 9 + .../dashboard/settings/agentBots/Index.vue | 170 +++++++++--------- .../settings/agentBots/agentBot.routes.js | 21 ++- .../agentBots/components/AgentBotRow.vue | 103 +++++------ .../agentBots/components/AgentBotType.vue | 51 +++--- .../integrations/IntegrationHooks.vue | 15 +- .../integrations/MultipleIntegrationHooks.vue | 58 ++++-- .../integrations/integrations.routes.js | 8 + 10 files changed, 224 insertions(+), 217 deletions(-) diff --git a/app/javascript/dashboard/i18n/locale/en/agentBots.json b/app/javascript/dashboard/i18n/locale/en/agentBots.json index fb744b4a9..41b8fcb1b 100644 --- a/app/javascript/dashboard/i18n/locale/en/agentBots.json +++ b/app/javascript/dashboard/i18n/locale/en/agentBots.json @@ -2,8 +2,8 @@ "AGENT_BOTS": { "HEADER": "Bots", "LOADING_EDITOR": "Loading editor...", - "HEADER_BTN_TXT": "Add bot configuration", - "SIDEBAR_TXT": "

Agent Bots

Agent Bots are like the most fabulous members of your team. They can handle the small stuff, so you can focus on the stuff that matters. Give them a try.

You can manage your bots from this page or create new ones using the 'Add bot configuraton' button.

Open the Agent bots handbook in another tab for a helping hand.

", + "DESCRIPTION": "Agent Bots are like the most fabulous members of your team. They can handle the small stuff, so you can focus on the stuff that matters. Give them a try.You can manage your bots from this page or create new ones using the 'Configure new bot' button.", + "LEARN_MORE": "Learn about agent bots", "CSML_BOT_EDITOR": { "NAME": { "LABEL": "Bot name", diff --git a/app/javascript/dashboard/i18n/locale/en/integrationApps.json b/app/javascript/dashboard/i18n/locale/en/integrationApps.json index 9404e12c2..b91b434f7 100644 --- a/app/javascript/dashboard/i18n/locale/en/integrationApps.json +++ b/app/javascript/dashboard/i18n/locale/en/integrationApps.json @@ -56,7 +56,7 @@ "BUTTON_TEXT": "Disconnect" }, "SIDEBAR_DESCRIPTION": { - "DIALOGFLOW": "Dialogflow is a natural language understanding platform that makes it easy to design and integrate a conversational user interface into your mobile app, web application, device, bot, interactive voice response system, and so on.

Dialogflow integration with {installationName} allows you to configure a Dialogflow bot with your inboxes which lets the bot handle the queries initially and hand them over to an agent when needed. Dialogflow can be used to qualifying the leads, reduce the workload of agents by providing frequently asked questions etc.

To add Dialogflow, you need to create a Service Account in your Google project console and share the credentials. Please refer to the Dialogflow docs for more information." + "DIALOGFLOW": "Dialogflow is a natural language processing platform for building conversational interfaces. Integrating it with {installationName} lets bots handle queries first and transfer them to agents when needed. It helps qualify leads and reduce agent workload by answering FAQs. To add Dialogflow, create a Service Account in Google Console and share the credentials. Refer to the docs for details" } } } diff --git a/app/javascript/dashboard/routes/dashboard/settings/Wrapper.vue b/app/javascript/dashboard/routes/dashboard/settings/Wrapper.vue index f5ba2b4eb..59cf178ba 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/Wrapper.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/Wrapper.vue @@ -19,12 +19,21 @@ const { t } = useI18n(); const showNewButton = computed( () => props.newButtonRoutes.length && !props.showBackButton ); + +const showSettingsHeader = computed( + () => + props.headerTitle || + props.icon || + props.showBackButton || + showNewButton.value +);