diff --git a/Gemfile.lock b/Gemfile.lock index 7258f9672..9cbfa801d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -172,6 +172,8 @@ GEM bundler (>= 1.2.0, < 3) thor (~> 1.0) byebug (11.1.3) + childprocess (5.1.0) + logger (~> 1.5) climate_control (1.2.0) coderay (1.1.3) commonmarker (0.23.10) @@ -435,10 +437,12 @@ GEM json (>= 1.8) rexml language_server-protocol (3.17.0.5) - launchy (2.5.2) + launchy (3.1.1) addressable (~> 2.8) - letter_opener (1.8.1) - launchy (>= 2.2, < 3) + childprocess (~> 5.0) + logger (~> 1.6) + letter_opener (1.10.0) + launchy (>= 2.2, < 4) line-bot-api (1.28.0) lint_roller (1.1.0) liquid (5.4.0) @@ -565,7 +569,7 @@ GEM method_source (~> 1.0) pry-rails (0.3.9) pry (>= 0.10.4) - public_suffix (6.0.0) + public_suffix (6.0.2) puma (6.4.3) nio4r (~> 2.0) pundit (2.3.0) diff --git a/app/controllers/api/v1/accounts/inboxes_controller.rb b/app/controllers/api/v1/accounts/inboxes_controller.rb index abae69c1c..99b05cb3c 100644 --- a/app/controllers/api/v1/accounts/inboxes_controller.rb +++ b/app/controllers/api/v1/accounts/inboxes_controller.rb @@ -81,11 +81,15 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController end def create_channel - return unless %w[web_widget api email line telegram whatsapp sms voice].include?(permitted_params[:channel][:type]) + return unless allowed_channel_types.include?(permitted_params[:channel][:type]) account_channels_method.create!(permitted_params(channel_type_from_params::EDITABLE_ATTRS)[:channel].except(:type)) end + def allowed_channel_types + %w[web_widget api email line telegram whatsapp sms] + end + def update_inbox_working_hours @inbox.update_working_hours(params.permit(working_hours: Inbox::OFFISABLE_ATTRS)[:working_hours]) if params[:working_hours] end diff --git a/app/controllers/public/api/v1/portals/articles_controller.rb b/app/controllers/public/api/v1/portals/articles_controller.rb index 32a147d34..02023559b 100644 --- a/app/controllers/public/api/v1/portals/articles_controller.rb +++ b/app/controllers/public/api/v1/portals/articles_controller.rb @@ -7,7 +7,11 @@ class Public::Api::V1::Portals::ArticlesController < Public::Api::V1::Portals::B def index @articles = @portal.articles.published.includes(:category, :author) + + @articles = @articles.where(locale: permitted_params[:locale]) if permitted_params[:locale].present? + @articles_count = @articles.count + search_articles order_by_sort_param limit_results diff --git a/app/javascript/dashboard/components-next/captain/assistant/ResponseCard.vue b/app/javascript/dashboard/components-next/captain/assistant/ResponseCard.vue index f00354105..7879411c8 100644 --- a/app/javascript/dashboard/components-next/captain/assistant/ResponseCard.vue +++ b/app/javascript/dashboard/components-next/captain/assistant/ResponseCard.vue @@ -123,7 +123,7 @@ const handleDocumentableClick = () => { @mouseenter="emit('hover', true)" @mouseleave="emit('hover', false)" > -
+
diff --git a/app/javascript/dashboard/components-next/icon/provider.js b/app/javascript/dashboard/components-next/icon/provider.js index 9c0a24925..36dd6216e 100644 --- a/app/javascript/dashboard/components-next/icon/provider.js +++ b/app/javascript/dashboard/components-next/icon/provider.js @@ -13,6 +13,7 @@ export function useChannelIcon(inbox) { 'Channel::WebWidget': 'i-ri-global-fill', 'Channel::Whatsapp': 'i-ri-whatsapp-fill', 'Channel::Instagram': 'i-ri-instagram-fill', + 'Channel::Voice': 'i-ri-phone-fill', }; const providerIconMap = { diff --git a/app/javascript/dashboard/components-next/icon/specs/provider.spec.js b/app/javascript/dashboard/components-next/icon/specs/provider.spec.js index df30d7138..5860e30ea 100644 --- a/app/javascript/dashboard/components-next/icon/specs/provider.spec.js +++ b/app/javascript/dashboard/components-next/icon/specs/provider.spec.js @@ -19,6 +19,12 @@ describe('useChannelIcon', () => { expect(icon).toBe('i-ri-whatsapp-fill'); }); + it('returns correct icon for Voice channel', () => { + const inbox = { channel_type: 'Channel::Voice' }; + const { value: icon } = useChannelIcon(inbox); + expect(icon).toBe('i-ri-phone-fill'); + }); + describe('Email channel', () => { it('returns mail icon for generic email channel', () => { const inbox = { channel_type: 'Channel::Email' }; diff --git a/app/javascript/dashboard/components-next/input/Input.vue b/app/javascript/dashboard/components-next/input/Input.vue index ea6eb0417..ed6d7a20b 100644 --- a/app/javascript/dashboard/components-next/input/Input.vue +++ b/app/javascript/dashboard/components-next/input/Input.vue @@ -1,51 +1,21 @@ diff --git a/app/javascript/dashboard/components/widgets/ChannelItem.vue b/app/javascript/dashboard/components/widgets/ChannelItem.vue index d9dc62bf5..85699942c 100644 --- a/app/javascript/dashboard/components/widgets/ChannelItem.vue +++ b/app/javascript/dashboard/components/widgets/ChannelItem.vue @@ -40,6 +40,11 @@ export default { this.enabledFeatures.channel_instagram && this.hasInstagramConfigured ); } + + if (key === 'voice') { + return this.enabledFeatures.channel_voice; + } + return [ 'website', 'twilio', @@ -50,6 +55,7 @@ export default { 'telegram', 'line', 'instagram', + 'voice', ].includes(key); }, }, diff --git a/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json b/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json index 7914dbb1c..2bbe7bf66 100644 --- a/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json +++ b/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json @@ -299,6 +299,46 @@ "ERROR_MESSAGE": "We were not able to save the WhatsApp channel" } }, + "VOICE": { + "TITLE": "Voice Channel", + "DESC": "Integrate Twilio Voice and start supporting your customers via phone calls.", + "PHONE_NUMBER": { + "LABEL": "Phone Number", + "PLACEHOLDER": "Enter your phone number (e.g. +1234567890)", + "ERROR": "Please provide a valid phone number in E.164 format (e.g. +1234567890)" + }, + "TWILIO": { + "ACCOUNT_SID": { + "LABEL": "Account SID", + "PLACEHOLDER": "Enter your Twilio Account SID", + "REQUIRED": "Account SID is required" + }, + "AUTH_TOKEN": { + "LABEL": "Auth Token", + "PLACEHOLDER": "Enter your Twilio Auth Token", + "REQUIRED": "Auth Token is required" + }, + "API_KEY_SID": { + "LABEL": "API Key SID", + "PLACEHOLDER": "Enter your Twilio API Key SID", + "REQUIRED": "API Key SID is required" + }, + "API_KEY_SECRET": { + "LABEL": "API Key Secret", + "PLACEHOLDER": "Enter your Twilio API Key Secret", + "REQUIRED": "API Key Secret is required" + }, + "TWIML_APP_SID": { + "LABEL": "TwiML App SID", + "PLACEHOLDER": "Enter your Twilio TwiML App SID (starts with AP)", + "REQUIRED": "TwiML App SID is required" + } + }, + "SUBMIT_BUTTON": "Create Voice Channel", + "API": { + "ERROR_MESSAGE": "We were not able to create the voice channel" + } + }, "API_CHANNEL": { "TITLE": "API Channel", "DESC": "Integrate with API channel and start supporting your customers.", diff --git a/app/javascript/dashboard/i18n/locale/en/integrations.json b/app/javascript/dashboard/i18n/locale/en/integrations.json index 071c95604..41f63d0a2 100644 --- a/app/javascript/dashboard/i18n/locale/en/integrations.json +++ b/app/javascript/dashboard/i18n/locale/en/integrations.json @@ -537,6 +537,8 @@ "CONVERSATION": "Conversation #{id}" }, "SELECTED": "{count} selected", + "SELECT_ALL": "Select all ({count})", + "UNSELECT_ALL": "Unselect all ({count})", "BULK_APPROVE_BUTTON": "Approve", "BULK_DELETE_BUTTON": "Delete", "BULK_APPROVE": { diff --git a/app/javascript/dashboard/routes/dashboard/captain/responses/Index.vue b/app/javascript/dashboard/routes/dashboard/captain/responses/Index.vue index 258e1c0c9..e771e72ed 100644 --- a/app/javascript/dashboard/routes/dashboard/captain/responses/Index.vue +++ b/app/javascript/dashboard/routes/dashboard/captain/responses/Index.vue @@ -157,6 +157,13 @@ const bulkCheckbox = computed({ }, }); +const buildSelectedCountLabel = computed(() => { + const count = responses.value?.length || 0; + return bulkSelectionState.value.allSelected + ? t('CAPTAIN.RESPONSES.UNSELECT_ALL', { count }) + : t('CAPTAIN.RESPONSES.SELECT_ALL', { count }); +}); + const handleCardHover = (isHovered, id) => { hoveredCard.value = isHovered ? id : null; }; @@ -270,7 +277,11 @@ onMounted(() => {