diff --git a/Gemfile.lock b/Gemfile.lock index bbf2f3a75..057c560e2 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -443,7 +443,7 @@ GEM faraday-multipart json (>= 1.8) rexml - language_server-protocol (3.17.0.4) + language_server-protocol (3.17.0.5) launchy (3.1.1) addressable (~> 2.8) childprocess (~> 5.0) @@ -584,7 +584,7 @@ GEM psych (5.2.6) date stringio - public_suffix (6.0.1) + public_suffix (6.0.2) puma (6.6.0) nio4r (~> 2.0) pundit (2.5.0) diff --git a/app/controllers/api/v1/accounts/google/authorizations_controller.rb b/app/controllers/api/v1/accounts/google/authorizations_controller.rb index 1140a214b..87a7cfa3f 100644 --- a/app/controllers/api/v1/accounts/google/authorizations_controller.rb +++ b/app/controllers/api/v1/accounts/google/authorizations_controller.rb @@ -1,32 +1,23 @@ -class Api::V1::Accounts::Google::AuthorizationsController < Api::V1::Accounts::BaseController +class Api::V1::Accounts::Google::AuthorizationsController < Api::V1::Accounts::OauthAuthorizationController include GoogleConcern - before_action :check_authorization def create - email = params[:authorization][:email] redirect_url = google_client.auth_code.authorize_url( { redirect_uri: "#{base_url}/google/callback", - scope: 'email profile https://mail.google.com/', + scope: scope, response_type: 'code', prompt: 'consent', # the oauth flow does not return a refresh token, this is supposed to fix it access_type: 'offline', # the default is 'online' + state: state, client_id: GlobalConfigService.load('GOOGLE_OAUTH_CLIENT_ID', nil) } ) if redirect_url - cache_key = "google::#{email.downcase}" - ::Redis::Alfred.setex(cache_key, Current.account.id, 5.minutes) render json: { success: true, url: redirect_url } else render json: { success: false }, status: :unprocessable_entity end end - - private - - def check_authorization - raise Pundit::NotAuthorizedError unless Current.account_user.administrator? - end end diff --git a/app/controllers/api/v1/accounts/inboxes_controller.rb b/app/controllers/api/v1/accounts/inboxes_controller.rb index 61d16b2ca..e7b3b197b 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].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/api/v1/accounts/instagram/authorizations_controller.rb b/app/controllers/api/v1/accounts/instagram/authorizations_controller.rb index eace4411a..053c29731 100644 --- a/app/controllers/api/v1/accounts/instagram/authorizations_controller.rb +++ b/app/controllers/api/v1/accounts/instagram/authorizations_controller.rb @@ -1,7 +1,6 @@ -class Api::V1::Accounts::Instagram::AuthorizationsController < Api::V1::Accounts::BaseController +class Api::V1::Accounts::Instagram::AuthorizationsController < Api::V1::Accounts::OauthAuthorizationController include InstagramConcern include Instagram::IntegrationHelper - before_action :check_authorization def create # https://developers.facebook.com/docs/instagram-platform/instagram-api-with-instagram-login/business-login#step-1--get-authorization @@ -21,10 +20,4 @@ class Api::V1::Accounts::Instagram::AuthorizationsController < Api::V1::Accounts render json: { success: false }, status: :unprocessable_entity end end - - private - - def check_authorization - raise Pundit::NotAuthorizedError unless Current.account_user.administrator? - end end diff --git a/app/controllers/api/v1/accounts/integrations/notion_controller.rb b/app/controllers/api/v1/accounts/integrations/notion_controller.rb new file mode 100644 index 000000000..dff6ccece --- /dev/null +++ b/app/controllers/api/v1/accounts/integrations/notion_controller.rb @@ -0,0 +1,14 @@ +class Api::V1::Accounts::Integrations::NotionController < Api::V1::Accounts::BaseController + before_action :fetch_hook, only: [:destroy] + + def destroy + @hook.destroy! + head :ok + end + + private + + def fetch_hook + @hook = Integrations::Hook.where(account: Current.account).find_by(app_id: 'notion') + end +end \ No newline at end of file diff --git a/app/controllers/api/v1/accounts/microsoft/authorizations_controller.rb b/app/controllers/api/v1/accounts/microsoft/authorizations_controller.rb index df563094a..a300b5f59 100644 --- a/app/controllers/api/v1/accounts/microsoft/authorizations_controller.rb +++ b/app/controllers/api/v1/accounts/microsoft/authorizations_controller.rb @@ -1,28 +1,19 @@ -class Api::V1::Accounts::Microsoft::AuthorizationsController < Api::V1::Accounts::BaseController +class Api::V1::Accounts::Microsoft::AuthorizationsController < Api::V1::Accounts::OauthAuthorizationController include MicrosoftConcern - before_action :check_authorization def create - email = params[:authorization][:email] redirect_url = microsoft_client.auth_code.authorize_url( { redirect_uri: "#{base_url}/microsoft/callback", - scope: 'offline_access https://outlook.office.com/IMAP.AccessAsUser.All https://outlook.office.com/SMTP.Send openid profile', + scope: scope, + state: state, prompt: 'consent' } ) if redirect_url - cache_key = "microsoft::#{email.downcase}" - ::Redis::Alfred.setex(cache_key, Current.account.id, 5.minutes) render json: { success: true, url: redirect_url } else render json: { success: false }, status: :unprocessable_entity end end - - private - - def check_authorization - raise Pundit::NotAuthorizedError unless Current.account_user.administrator? - end end diff --git a/app/controllers/api/v1/accounts/notion/authorizations_controller.rb b/app/controllers/api/v1/accounts/notion/authorizations_controller.rb new file mode 100644 index 000000000..bb9b2f858 --- /dev/null +++ b/app/controllers/api/v1/accounts/notion/authorizations_controller.rb @@ -0,0 +1,21 @@ +class Api::V1::Accounts::Notion::AuthorizationsController < Api::V1::Accounts::OauthAuthorizationController + include NotionConcern + + def create + redirect_url = notion_client.auth_code.authorize_url( + { + redirect_uri: "#{base_url}/notion/callback", + response_type: 'code', + owner: 'user', + state: state, + client_id: GlobalConfigService.load('NOTION_CLIENT_ID', nil) + } + ) + + if redirect_url + render json: { success: true, url: redirect_url } + else + render json: { success: false }, status: :unprocessable_entity + end + end +end \ No newline at end of file diff --git a/app/controllers/api/v1/accounts/oauth_authorization_controller.rb b/app/controllers/api/v1/accounts/oauth_authorization_controller.rb new file mode 100644 index 000000000..feb218b59 --- /dev/null +++ b/app/controllers/api/v1/accounts/oauth_authorization_controller.rb @@ -0,0 +1,23 @@ +class Api::V1::Accounts::OauthAuthorizationController < Api::V1::Accounts::BaseController + before_action :check_authorization + + protected + + def scope + '' + end + + def state + Current.account.to_sgid(expires_in: 15.minutes).to_s + end + + def base_url + ENV.fetch('FRONTEND_URL', 'http://localhost:3000') + end + + private + + def check_authorization + raise Pundit::NotAuthorizedError unless Current.account_user.administrator? + end +end diff --git a/app/controllers/concerns/google_concern.rb b/app/controllers/concerns/google_concern.rb index 474b14aec..13de7ced3 100644 --- a/app/controllers/concerns/google_concern.rb +++ b/app/controllers/concerns/google_concern.rb @@ -14,7 +14,7 @@ module GoogleConcern private - def base_url - ENV.fetch('FRONTEND_URL', 'http://localhost:3000') + def scope + 'email profile https://mail.google.com/' end end diff --git a/app/controllers/concerns/microsoft_concern.rb b/app/controllers/concerns/microsoft_concern.rb index 507b9f8a3..c3e0994bd 100644 --- a/app/controllers/concerns/microsoft_concern.rb +++ b/app/controllers/concerns/microsoft_concern.rb @@ -15,7 +15,7 @@ module MicrosoftConcern private - def base_url - ENV.fetch('FRONTEND_URL', 'http://localhost:3000') + def scope + 'offline_access https://outlook.office.com/IMAP.AccessAsUser.All https://outlook.office.com/SMTP.Send openid profile email' end end diff --git a/app/controllers/concerns/notion_concern.rb b/app/controllers/concerns/notion_concern.rb new file mode 100644 index 000000000..2b94fe63b --- /dev/null +++ b/app/controllers/concerns/notion_concern.rb @@ -0,0 +1,21 @@ +module NotionConcern + extend ActiveSupport::Concern + + def notion_client + app_id = GlobalConfigService.load('NOTION_CLIENT_ID', nil) + app_secret = GlobalConfigService.load('NOTION_CLIENT_SECRET', nil) + + ::OAuth2::Client.new(app_id, app_secret, { + site: 'https://api.notion.com', + authorize_url: 'https://api.notion.com/v1/oauth/authorize', + token_url: 'https://api.notion.com/v1/oauth/token', + auth_scheme: :basic_auth + }) + end + + private + + def scope + '' + end +end diff --git a/app/controllers/notion/callbacks_controller.rb b/app/controllers/notion/callbacks_controller.rb new file mode 100644 index 000000000..94030fc8e --- /dev/null +++ b/app/controllers/notion/callbacks_controller.rb @@ -0,0 +1,36 @@ +class Notion::CallbacksController < OauthCallbackController + include NotionConcern + + private + + def provider_name + 'notion' + end + + def oauth_client + notion_client + end + + def handle_response + hook = account.hooks.new( + access_token: parsed_body['access_token'], + status: 'enabled', + app_id: 'notion', + settings: { + token_type: parsed_body['token_type'], + workspace_name: parsed_body['workspace_name'], + workspace_id: parsed_body['workspace_id'], + workspace_icon: parsed_body['workspace_icon'], + bot_id: parsed_body['bot_id'], + owner: parsed_body['owner'] + } + ) + + hook.save! + redirect_to notion_redirect_uri + end + + def notion_redirect_uri + "#{ENV.fetch('FRONTEND_URL', nil)}/app/accounts/#{account.id}/settings/integrations/notion" + end +end \ No newline at end of file diff --git a/app/controllers/oauth_callback_controller.rb b/app/controllers/oauth_callback_controller.rb index 9aa73956a..be0fa5008 100644 --- a/app/controllers/oauth_callback_controller.rb +++ b/app/controllers/oauth_callback_controller.rb @@ -6,7 +6,6 @@ class OauthCallbackController < ApplicationController ) handle_response - ::Redis::Alfred.delete(cache_key) rescue StandardError => e ChatwootExceptionTracker.new(e).capture_exception redirect_to '/' @@ -64,10 +63,6 @@ class OauthCallbackController < ApplicationController raise NotImplementedError end - def cache_key - "#{provider_name}::#{users_data['email'].downcase}" - end - def create_channel_with_inbox ActiveRecord::Base.transaction do channel_email = Channel::Email.create!(email: users_data['email'], account: account) @@ -86,12 +81,17 @@ class OauthCallbackController < ApplicationController decoded_token[0] end - def account_id - ::Redis::Alfred.get(cache_key) + def account_from_signed_id + raise ActionController::BadRequest, 'Missing state variable' if params[:state].blank? + + account = GlobalID::Locator.locate_signed(params[:state]) + raise 'Invalid or expired state' if account.nil? + + account end def account - @account ||= Account.find(account_id) + @account ||= account_from_signed_id end # Fallback name, for when name field is missing from users_data 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/controllers/super_admin/app_configs_controller.rb b/app/controllers/super_admin/app_configs_controller.rb index 204bfc95b..771f9f28c 100644 --- a/app/controllers/super_admin/app_configs_controller.rb +++ b/app/controllers/super_admin/app_configs_controller.rb @@ -39,6 +39,7 @@ class SuperAdmin::AppConfigsController < SuperAdmin::ApplicationController 'email' => ['MAILER_INBOUND_EMAIL_DOMAIN'], 'linear' => %w[LINEAR_CLIENT_ID LINEAR_CLIENT_SECRET], 'slack' => %w[SLACK_CLIENT_ID SLACK_CLIENT_SECRET], + 'notion' => %w[NOTION_CLIENT_ID NOTION_CLIENT_SECRET], 'instagram' => %w[INSTAGRAM_APP_ID INSTAGRAM_APP_SECRET INSTAGRAM_VERIFY_TOKEN INSTAGRAM_API_VERSION ENABLE_INSTAGRAM_CHANNEL_HUMAN_AGENT] } diff --git a/app/javascript/dashboard/api/notion_auth.js b/app/javascript/dashboard/api/notion_auth.js new file mode 100644 index 000000000..8a0027f9b --- /dev/null +++ b/app/javascript/dashboard/api/notion_auth.js @@ -0,0 +1,14 @@ +/* global axios */ +import ApiClient from './ApiClient'; + +class NotionOAuthClient extends ApiClient { + constructor() { + super('notion', { accountScoped: true }); + } + + generateAuthorization() { + return axios.post(`${this.url}/authorization`); + } +} + +export default new NotionOAuthClient(); diff --git a/app/javascript/dashboard/assets/scss/plugins/_multiselect.scss b/app/javascript/dashboard/assets/scss/plugins/_multiselect.scss index ffcfca545..1280eb069 100644 --- a/app/javascript/dashboard/assets/scss/plugins/_multiselect.scss +++ b/app/javascript/dashboard/assets/scss/plugins/_multiselect.scss @@ -47,7 +47,7 @@ @apply max-w-full; .multiselect__option { - @apply text-sm font-normal; + @apply text-sm font-normal flex justify-between items-center; span { @apply inline-block overflow-hidden text-ellipsis whitespace-nowrap w-fit; @@ -58,7 +58,7 @@ } &::after { - @apply bottom-0 flex items-center justify-center text-center; + @apply bottom-0 flex items-center justify-center text-center relative px-1 leading-tight; } &.multiselect__option--highlight { @@ -74,7 +74,7 @@ } &.multiselect__option--highlight::after { - @apply bg-transparent; + @apply bg-transparent text-n-slate-12; } &.multiselect__option--selected { 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 4f9751c4e..933e117a9 100644 --- a/app/javascript/dashboard/components/widgets/ChannelItem.vue +++ b/app/javascript/dashboard/components/widgets/ChannelItem.vue @@ -41,6 +41,10 @@ export default { ); } + if (key === 'voice') { + return this.enabledFeatures.channel_voice; + } + return [ 'website', 'twilio', @@ -50,6 +54,7 @@ export default { 'telegram', 'line', 'instagram', + 'voice', ].includes(key); }, }, diff --git a/app/javascript/dashboard/composables/useInbox.js b/app/javascript/dashboard/composables/useInbox.js index 67ce11ae2..3ad308a39 100644 --- a/app/javascript/dashboard/composables/useInbox.js +++ b/app/javascript/dashboard/composables/useInbox.js @@ -125,6 +125,10 @@ export const useInbox = () => { return channelType.value === INBOX_TYPES.INSTAGRAM; }); + const isAVoiceChannel = computed(() => { + return channelType.value === INBOX_TYPES.VOICE; + }); + return { inbox, isAFacebookInbox, @@ -142,5 +146,6 @@ export const useInbox = () => { is360DialogWhatsAppChannel, isAnEmailChannel, isAnInstagramChannel, + isAVoiceChannel, }; }; diff --git a/app/javascript/dashboard/helper/inbox.js b/app/javascript/dashboard/helper/inbox.js index ff6d73c84..ca880a79b 100644 --- a/app/javascript/dashboard/helper/inbox.js +++ b/app/javascript/dashboard/helper/inbox.js @@ -10,6 +10,7 @@ export const INBOX_TYPES = { LINE: 'Channel::Line', SMS: 'Channel::Sms', INSTAGRAM: 'Channel::Instagram', + VOICE: 'Channel::Voice', }; const INBOX_ICON_MAP_FILL = { @@ -22,6 +23,7 @@ const INBOX_ICON_MAP_FILL = { [INBOX_TYPES.TELEGRAM]: 'i-ri-telegram-fill', [INBOX_TYPES.LINE]: 'i-ri-line-fill', [INBOX_TYPES.INSTAGRAM]: 'i-ri-instagram-fill', + [INBOX_TYPES.VOICE]: 'i-ri-phone-fill', }; const DEFAULT_ICON_FILL = 'i-ri-chat-1-fill'; @@ -36,6 +38,7 @@ const INBOX_ICON_MAP_LINE = { [INBOX_TYPES.TELEGRAM]: 'i-ri-telegram-line', [INBOX_TYPES.LINE]: 'i-ri-line-line', [INBOX_TYPES.INSTAGRAM]: 'i-ri-instagram-line', + [INBOX_TYPES.VOICE]: 'i-ri-phone-line', }; const DEFAULT_ICON_LINE = 'i-ri-chat-1-line'; @@ -47,6 +50,7 @@ export const getInboxSource = (type, phoneNumber, inbox) => { case INBOX_TYPES.TWILIO: case INBOX_TYPES.WHATSAPP: + case INBOX_TYPES.VOICE: return phoneNumber || ''; case INBOX_TYPES.EMAIL: @@ -85,6 +89,9 @@ export const getReadableInboxByType = (type, phoneNumber) => { case INBOX_TYPES.LINE: return 'line'; + case INBOX_TYPES.VOICE: + return 'voice'; + default: return 'chat'; } @@ -124,6 +131,9 @@ export const getInboxClassByType = (type, phoneNumber) => { case INBOX_TYPES.INSTAGRAM: return 'brand-instagram'; + case INBOX_TYPES.VOICE: + return 'phone'; + default: return 'chat'; } diff --git a/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json b/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json index cc0fe4b33..be7a6f2f9 100644 --- a/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json +++ b/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json @@ -268,6 +268,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.", @@ -789,7 +829,8 @@ "TELEGRAM": "Telegram", "LINE": "Line", "API": "API Channel", - "INSTAGRAM": "Instagram" + "INSTAGRAM": "Instagram", + "VOICE": "Voice" } } } diff --git a/app/javascript/dashboard/i18n/locale/en/integrations.json b/app/javascript/dashboard/i18n/locale/en/integrations.json index 071c95604..b3e091722 100644 --- a/app/javascript/dashboard/i18n/locale/en/integrations.json +++ b/app/javascript/dashboard/i18n/locale/en/integrations.json @@ -328,6 +328,14 @@ "DESCRIPTION": "Linear workspace is not connected. Click the button below to connect your workspace to use this integration.", "BUTTON_TEXT": "Connect Linear workspace" } + }, + "NOTION": { + "DELETE": { + "TITLE": "Are you sure you want to delete the Notion integration?", + "MESSAGE": "Deleting this integration will remove access to your Notion workspace and stop all related functionality.", + "CONFIRM": "Yes, delete", + "CANCEL": "Cancel" + } } }, "CAPTAIN": { @@ -537,6 +545,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/i18n/locale/pt_BR/automation.json b/app/javascript/dashboard/i18n/locale/pt_BR/automation.json index 5b3af42a7..2c0d51696 100644 --- a/app/javascript/dashboard/i18n/locale/pt_BR/automation.json +++ b/app/javascript/dashboard/i18n/locale/pt_BR/automation.json @@ -152,7 +152,7 @@ "ATTRIBUTES": { "MESSAGE_TYPE": "Tipo da Mensagem", "MESSAGE_CONTAINS": "A mensagem contém", - "EMAIL": "e-mail", + "EMAIL": "E-mail", "INBOX": "Caixa de Entrada", "CONVERSATION_LANGUAGE": "Idioma da conversa", "PHONE_NUMBER": "Número de Telefone", diff --git a/app/javascript/dashboard/modules/contact/components/MergeContact.vue b/app/javascript/dashboard/modules/contact/components/MergeContact.vue index ae0d47f7f..4055a22e4 100644 --- a/app/javascript/dashboard/modules/contact/components/MergeContact.vue +++ b/app/javascript/dashboard/modules/contact/components/MergeContact.vue @@ -130,15 +130,15 @@ export default {
-
+