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/controllers/api/v1/widget/campaigns_controller.rb b/app/controllers/api/v1/widget/campaigns_controller.rb index cb9b96a38..10a73aa62 100644 --- a/app/controllers/api/v1/widget/campaigns_controller.rb +++ b/app/controllers/api/v1/widget/campaigns_controller.rb @@ -2,6 +2,10 @@ class Api::V1::Widget::CampaignsController < Api::V1::Widget::BaseController skip_before_action :set_contact def index - @campaigns = @web_widget.inbox.campaigns.where(enabled: true) + @campaigns = @web_widget + .inbox + .campaigns + .where(enabled: true, account_id: @web_widget.inbox.account_id) + .includes(:sender) end end diff --git a/app/controllers/api/v1/widget/inbox_members_controller.rb b/app/controllers/api/v1/widget/inbox_members_controller.rb index c4bc377ea..22934388b 100644 --- a/app/controllers/api/v1/widget/inbox_members_controller.rb +++ b/app/controllers/api/v1/widget/inbox_members_controller.rb @@ -2,6 +2,6 @@ class Api::V1::Widget::InboxMembersController < Api::V1::Widget::BaseController skip_before_action :set_contact def index - @inbox_members = @web_widget.inbox.inbox_members.includes(:user) + @inbox_members = @web_widget.inbox.inbox_members.includes(user: { avatar_attachment: :blob }) end end diff --git a/app/controllers/public/api/v1/portals/articles_controller.rb b/app/controllers/public/api/v1/portals/articles_controller.rb index f0fcf403d..d07dbcb9d 100644 --- a/app/controllers/public/api/v1/portals/articles_controller.rb +++ b/app/controllers/public/api/v1/portals/articles_controller.rb @@ -6,17 +6,25 @@ class Public::Api::V1::Portals::ArticlesController < Public::Api::V1::Portals::B layout 'portal' def index - @articles = @portal.articles.published + @articles = @portal.articles.published.includes(:category, :author) @articles_count = @articles.count search_articles order_by_sort_param - @articles = @articles.page(list_params[:page]) if list_params[:page].present? + limit_results end def show; end private + def limit_results + return if list_params[:per_page].blank? + + per_page = [list_params[:per_page].to_i, 100].min + per_page = 25 if per_page < 1 + @articles = @articles.page(list_params[:page]).per(per_page) + end + def search_articles @articles = @articles.search(list_params) if list_params.present? end @@ -45,7 +53,7 @@ class Public::Api::V1::Portals::ArticlesController < Public::Api::V1::Portals::B end def list_params - params.permit(:query, :locale, :sort, :status, :page) + params.permit(:query, :locale, :sort, :status, :page, :per_page) end def permitted_params diff --git a/app/controllers/twilio/callback_controller.rb b/app/controllers/twilio/callback_controller.rb index 7723e5dd2..ff5db952d 100644 --- a/app/controllers/twilio/callback_controller.rb +++ b/app/controllers/twilio/callback_controller.rb @@ -1,6 +1,6 @@ class Twilio::CallbackController < ApplicationController def create - ::Twilio::IncomingMessageService.new(params: permitted_params).perform + Webhooks::TwilioEventsJob.perform_later(permitted_params.to_unsafe_hash) head :no_content end diff --git a/app/controllers/twilio/delivery_status_controller.rb b/app/controllers/twilio/delivery_status_controller.rb index cc7afb0fc..1c756a1c2 100644 --- a/app/controllers/twilio/delivery_status_controller.rb +++ b/app/controllers/twilio/delivery_status_controller.rb @@ -1,6 +1,6 @@ class Twilio::DeliveryStatusController < ApplicationController def create - ::Twilio::DeliveryStatusService.new(params: permitted_params).perform + Webhooks::TwilioDeliveryStatusJob.perform_later(permitted_params.to_unsafe_hash) head :no_content end 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/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/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" } } 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/conversation/customAttributes/CustomAttributes.vue b/app/javascript/dashboard/routes/dashboard/conversation/customAttributes/CustomAttributes.vue index 01fbe9b71..4f0cf6340 100644 --- a/app/javascript/dashboard/routes/dashboard/conversation/customAttributes/CustomAttributes.vue +++ b/app/javascript/dashboard/routes/dashboard/conversation/customAttributes/CustomAttributes.vue @@ -271,7 +271,7 @@ const evenClass = [ ghost-class="ghost" handle=".drag-handle" item-key="key" - class="last:rounded-b-lg overflow-hidden" + class="last:rounded-b-lg" :class="evenClass" @start="dragging = true" @end="onDragEnd" 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 +);