diff --git a/.codeclimate.yml b/.codeclimate.yml index c9910f9ed..916b98510 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -50,3 +50,6 @@ exclude_patterns: - 'app/javascript/dashboard/routes/dashboard/settings/automation/constants.js' - 'app/javascript/dashboard/components/widgets/FilterInput/FilterOperatorTypes.js' - 'app/javascript/dashboard/routes/dashboard/settings/reports/constants.js' + - 'app/javascript/dashboard/i18n/index.js' + - 'app/javascript/widget/i18n/index.js' + - 'app/javascript/survey/i18n/index.js' diff --git a/.env.example b/.env.example index 36ca66be1..cc7e1c2dd 100644 --- a/.env.example +++ b/.env.example @@ -32,6 +32,11 @@ REDIS_SENTINELS= # You can find list of master using "SENTINEL masters" command REDIS_SENTINEL_MASTER_NAME= +# Redis premium breakage in heroku fix +# enable the following configuration +# ref: https://github.com/chatwoot/chatwoot/issues/2420 +# REDIS_OPENSSL_VERIFY_MODE=none + # Postgres Database config variables POSTGRES_HOST=postgres POSTGRES_USERNAME=postgres diff --git a/.eslintrc.js b/.eslintrc.js index a52594492..f30e21e03 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -29,8 +29,8 @@ module.exports = { 'vue/html-self-closing': 'off', "vue/no-v-html": 'off', 'vue/singleline-html-element-content-newline': 'off', - 'import/extensions': ['off'] - + 'import/extensions': ['off'], + 'no-console': 'error' }, settings: { 'import/resolver': { diff --git a/.rubocop.yml b/.rubocop.yml index e0c98cdc4..c3096dd4a 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -16,6 +16,7 @@ Metrics/ClassLength: - 'app/models/message.rb' - 'app/builders/messages/facebook/message_builder.rb' - 'app/controllers/api/v1/accounts/contacts_controller.rb' + - 'app/listeners/action_cable_listener.rb' RSpec/ExampleLength: Max: 25 Style/Documentation: diff --git a/Gemfile b/Gemfile index 0664e5aef..297a88497 100644 --- a/Gemfile +++ b/Gemfile @@ -42,7 +42,7 @@ gem 'down', '~> 5.0' gem 'aws-sdk-s3', require: false gem 'azure-storage-blob', require: false gem 'google-cloud-storage', require: false -gem 'image_processing' +gem 'image_processing', '~> 1.12.2' ##-- gems for database --# gem 'groupdate' diff --git a/Gemfile.lock b/Gemfile.lock index 5e2abbac6..eb6022ac7 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -9,63 +9,63 @@ GIT GEM remote: https://rubygems.org/ specs: - actioncable (6.1.4.6) - actionpack (= 6.1.4.6) - activesupport (= 6.1.4.6) + actioncable (6.1.4.7) + actionpack (= 6.1.4.7) + activesupport (= 6.1.4.7) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (6.1.4.6) - actionpack (= 6.1.4.6) - activejob (= 6.1.4.6) - activerecord (= 6.1.4.6) - activestorage (= 6.1.4.6) - activesupport (= 6.1.4.6) + actionmailbox (6.1.4.7) + actionpack (= 6.1.4.7) + activejob (= 6.1.4.7) + activerecord (= 6.1.4.7) + activestorage (= 6.1.4.7) + activesupport (= 6.1.4.7) mail (>= 2.7.1) - actionmailer (6.1.4.6) - actionpack (= 6.1.4.6) - actionview (= 6.1.4.6) - activejob (= 6.1.4.6) - activesupport (= 6.1.4.6) + actionmailer (6.1.4.7) + actionpack (= 6.1.4.7) + actionview (= 6.1.4.7) + activejob (= 6.1.4.7) + activesupport (= 6.1.4.7) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 2.0) - actionpack (6.1.4.6) - actionview (= 6.1.4.6) - activesupport (= 6.1.4.6) + actionpack (6.1.4.7) + actionview (= 6.1.4.7) + activesupport (= 6.1.4.7) rack (~> 2.0, >= 2.0.9) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (6.1.4.6) - actionpack (= 6.1.4.6) - activerecord (= 6.1.4.6) - activestorage (= 6.1.4.6) - activesupport (= 6.1.4.6) + actiontext (6.1.4.7) + actionpack (= 6.1.4.7) + activerecord (= 6.1.4.7) + activestorage (= 6.1.4.7) + activesupport (= 6.1.4.7) nokogiri (>= 1.8.5) - actionview (6.1.4.6) - activesupport (= 6.1.4.6) + actionview (6.1.4.7) + activesupport (= 6.1.4.7) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.1, >= 1.2.0) active_record_query_trace (1.8) - activejob (6.1.4.6) - activesupport (= 6.1.4.6) + activejob (6.1.4.7) + activesupport (= 6.1.4.7) globalid (>= 0.3.6) - activemodel (6.1.4.6) - activesupport (= 6.1.4.6) - activerecord (6.1.4.6) - activemodel (= 6.1.4.6) - activesupport (= 6.1.4.6) + activemodel (6.1.4.7) + activesupport (= 6.1.4.7) + activerecord (6.1.4.7) + activemodel (= 6.1.4.7) + activesupport (= 6.1.4.7) activerecord-import (1.3.0) activerecord (>= 4.2) - activestorage (6.1.4.6) - actionpack (= 6.1.4.6) - activejob (= 6.1.4.6) - activerecord (= 6.1.4.6) - activesupport (= 6.1.4.6) + activestorage (6.1.4.7) + actionpack (= 6.1.4.7) + activejob (= 6.1.4.7) + activerecord (= 6.1.4.7) + activesupport (= 6.1.4.7) marcel (~> 1.0.0) mini_mime (>= 1.1.0) - activesupport (6.1.4.6) + activesupport (6.1.4.7) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) @@ -135,7 +135,7 @@ GEM byebug (11.1.3) climate_control (1.0.1) coderay (1.1.3) - commonmarker (0.23.2) + commonmarker (0.23.4) concurrent-ruby (1.1.9) connection_pool (2.2.5) crack (0.4.5) @@ -303,7 +303,7 @@ GEM httpclient (2.8.3) i18n (1.10.0) concurrent-ruby (~> 1.0) - image_processing (1.12.1) + image_processing (1.12.2) mini_magick (>= 4.9.5, < 5) ruby-vips (>= 2.0.17, < 3) jbuilder (2.11.5) @@ -419,29 +419,29 @@ GEM rack-test (1.1.0) rack (>= 1.0, < 3) rack-timeout (0.6.0) - rails (6.1.4.6) - actioncable (= 6.1.4.6) - actionmailbox (= 6.1.4.6) - actionmailer (= 6.1.4.6) - actionpack (= 6.1.4.6) - actiontext (= 6.1.4.6) - actionview (= 6.1.4.6) - activejob (= 6.1.4.6) - activemodel (= 6.1.4.6) - activerecord (= 6.1.4.6) - activestorage (= 6.1.4.6) - activesupport (= 6.1.4.6) + rails (6.1.4.7) + actioncable (= 6.1.4.7) + actionmailbox (= 6.1.4.7) + actionmailer (= 6.1.4.7) + actionpack (= 6.1.4.7) + actiontext (= 6.1.4.7) + actionview (= 6.1.4.7) + activejob (= 6.1.4.7) + activemodel (= 6.1.4.7) + activerecord (= 6.1.4.7) + activestorage (= 6.1.4.7) + activesupport (= 6.1.4.7) bundler (>= 1.15.0) - railties (= 6.1.4.6) + railties (= 6.1.4.7) sprockets-rails (>= 2.0.0) rails-dom-testing (2.0.3) activesupport (>= 4.2.0) nokogiri (>= 1.6) rails-html-sanitizer (1.4.2) loofah (~> 2.3) - railties (6.1.4.6) - actionpack (= 6.1.4.6) - activesupport (= 6.1.4.6) + railties (6.1.4.7) + actionpack (= 6.1.4.7) + activesupport (= 6.1.4.7) method_source rake (>= 0.13) thor (~> 1.0) @@ -574,7 +574,7 @@ GEM spring-watcher-listen (2.0.1) listen (>= 2.7, < 4.0) spring (>= 1.2, < 3.0) - sprockets (4.0.2) + sprockets (4.0.3) concurrent-ruby (~> 1.0) rack (> 1, < 3) sprockets-rails (3.4.2) @@ -688,7 +688,7 @@ DEPENDENCIES hairtrigger hashie html2text - image_processing + image_processing (~> 1.12.2) jbuilder json_refs json_schemer @@ -751,4 +751,4 @@ RUBY VERSION ruby 3.0.2p107 BUNDLED WITH - 2.2.25 + 2.3.8 diff --git a/app.json b/app.json index 0d908761c..64edc4a81 100644 --- a/app.json +++ b/app.json @@ -32,6 +32,10 @@ "INSTALLATION_ENV": { "description": "Installation method used for Chatwoot.", "value": "heroku" + }, + "REDIS_OPENSSL_VERIFY_MODE":{ + "description": "OpenSSL verification mode for Redis connections. ref https://help.heroku.com/HC0F8CUS/redis-connection-issues", + "value": "none" } }, "formation": { diff --git a/app/actions/contact_identify_action.rb b/app/actions/contact_identify_action.rb index 655c6bc1c..746d1bfec 100644 --- a/app/actions/contact_identify_action.rb +++ b/app/actions/contact_identify_action.rb @@ -52,12 +52,11 @@ class ContactIdentifyAction end def update_contact - custom_attributes = params[:custom_attributes] ? @contact.custom_attributes.merge(params[:custom_attributes]) : @contact.custom_attributes # blank identifier or email will throw unique index error # TODO: replace reject { |_k, v| v.blank? } with compact_blank when rails is upgraded @contact.update!(params.slice(:name, :email, :identifier, :phone_number).reject do |_k, v| v.blank? - end.merge({ custom_attributes: custom_attributes })) + end.merge({ custom_attributes: custom_attributes, additional_attributes: additional_attributes })) ContactAvatarJob.perform_later(@contact, params[:avatar_url]) if params[:avatar_url].present? end @@ -68,4 +67,16 @@ class ContactIdentifyAction mergee_contact: merge_contact ).perform end + + def custom_attributes + params[:custom_attributes] ? @contact.custom_attributes.deep_merge(params[:custom_attributes].stringify_keys) : @contact.custom_attributes + end + + def additional_attributes + if params[:additional_attributes] + @contact.additional_attributes.deep_merge(params[:additional_attributes].stringify_keys) + else + @contact.additional_attributes + end + end end diff --git a/app/builders/messages/messenger/message_builder.rb b/app/builders/messages/messenger/message_builder.rb index 08aa58be0..2e58825f4 100644 --- a/app/builders/messages/messenger/message_builder.rb +++ b/app/builders/messages/messenger/message_builder.rb @@ -1,10 +1,14 @@ class Messages::Messenger::MessageBuilder + include ::FileTypeHelper + def process_attachment(attachment) return if attachment['type'].to_sym == :template attachment_obj = @message.attachments.new(attachment_params(attachment).except(:remote_file_url)) attachment_obj.save! attach_file(attachment_obj, attachment_params(attachment)[:remote_file_url]) if attachment_params(attachment)[:remote_file_url] + fetch_story_link(attachment_obj) if attachment_obj.file_type == 'story_mention' + update_attachment_file_type(attachment_obj) end def attach_file(attachment, file_url) @@ -22,7 +26,7 @@ class Messages::Messenger::MessageBuilder file_type = attachment['type'].to_sym params = { file_type: file_type, account_id: @message.account_id } - if [:image, :file, :audio, :video].include? file_type + if [:image, :file, :audio, :video, :share, :story_mention].include? file_type params.merge!(file_type_params(attachment)) elsif file_type == :location params.merge!(location_params(attachment)) @@ -39,4 +43,31 @@ class Messages::Messenger::MessageBuilder remote_file_url: attachment['payload']['url'] } end + + def update_attachment_file_type(attachment) + return unless attachment.file_type == 'share' || attachment.file_type == 'story_mention' + + attachment.file_type = file_type(attachment.file&.content_type) + attachment.save! + end + + def fetch_story_link(attachment) + message = attachment.message + begin + k = Koala::Facebook::API.new(@inbox.channel.page_access_token) if @inbox.facebook? + result = k.get_object(message.source_id, fields: %w[story from]) || {} + rescue Koala::Facebook::AuthenticationError + @inbox.channel.authorization_error! + raise + rescue StandardError => e + result = {} + Sentry.capture_exception(e) + end + story_id = result['story']['mention']['id'] + story_sender = result['from']['username'] + message.content_attributes[:story_sender] = story_sender + message.content_attributes[:story_id] = story_id + message.content = I18n.t('conversations.messages.instagram_story_content', story_sender: story_sender) + message.save! + end end diff --git a/app/builders/v2/report_builder.rb b/app/builders/v2/report_builder.rb index 7f0dd470c..1f17dbb08 100644 --- a/app/builders/v2/report_builder.rb +++ b/app/builders/v2/report_builder.rb @@ -95,15 +95,15 @@ class V2::ReportBuilder end def avg_first_response_time - (get_grouped_values scope.events.where(name: 'first_response')).average(:value) + (get_grouped_values scope.reporting_events.where(name: 'first_response')).average(:value) end def avg_resolution_time - (get_grouped_values scope.events.where(name: 'conversation_resolved')).average(:value) + (get_grouped_values scope.reporting_events.where(name: 'conversation_resolved')).average(:value) end def avg_resolution_time_summary - avg_rt = scope.events + avg_rt = scope.reporting_events .where(name: 'conversation_resolved', created_at: range) .average(:value) @@ -113,7 +113,7 @@ class V2::ReportBuilder end def avg_first_response_time_summary - avg_frt = scope.events + avg_frt = scope.reporting_events .where(name: 'first_response', created_at: range) .average(:value) diff --git a/app/channels/room_channel.rb b/app/channels/room_channel.rb index b3c597770..05bff5d86 100644 --- a/app/channels/room_channel.rb +++ b/app/channels/room_channel.rb @@ -1,5 +1,8 @@ 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 @@ -15,6 +18,8 @@ class RoomChannel < ApplicationCable::Channel private def broadcast_presence + return if @current_account.blank? + 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 }) @@ -26,6 +31,8 @@ class RoomChannel < ApplicationCable::Channel end def update_subscription + return if @current_account.blank? + ::OnlineStatusTracker.update_presence(@current_account.id, @current_user.class.name, @current_user.id) end diff --git a/app/controllers/api/v1/accounts/agent_bots_controller.rb b/app/controllers/api/v1/accounts/agent_bots_controller.rb index 7348ef255..9f4ef2cd9 100644 --- a/app/controllers/api/v1/accounts/agent_bots_controller.rb +++ b/app/controllers/api/v1/accounts/agent_bots_controller.rb @@ -18,7 +18,7 @@ class Api::V1::Accounts::AgentBotsController < Api::V1::Accounts::BaseController end def destroy - @agent_bot.destroy + @agent_bot.destroy! head :ok end diff --git a/app/controllers/api/v1/accounts/agents_controller.rb b/app/controllers/api/v1/accounts/agents_controller.rb index a666d1a67..09b648a6f 100644 --- a/app/controllers/api/v1/accounts/agents_controller.rb +++ b/app/controllers/api/v1/accounts/agents_controller.rb @@ -18,7 +18,7 @@ class Api::V1::Accounts::AgentsController < Api::V1::Accounts::BaseController end def destroy - @agent.current_account_user.destroy + @agent.current_account_user.destroy! head :ok end @@ -68,7 +68,7 @@ class Api::V1::Accounts::AgentsController < Api::V1::Accounts::BaseController end def agents - @agents ||= Current.account.users.order_by_full_name.includes({ avatar_attachment: [:blob] }) + @agents ||= Current.account.users.order_by_full_name.includes(:account_users, { avatar_attachment: [:blob] }) end def validate_limit diff --git a/app/controllers/api/v1/accounts/automation_rules_controller.rb b/app/controllers/api/v1/accounts/automation_rules_controller.rb index 12b7b5957..3971dbcaf 100644 --- a/app/controllers/api/v1/accounts/automation_rules_controller.rb +++ b/app/controllers/api/v1/accounts/automation_rules_controller.rb @@ -3,7 +3,7 @@ class Api::V1::Accounts::AutomationRulesController < Api::V1::Accounts::BaseCont before_action :fetch_automation_rule, only: [:show, :update, :destroy, :clone] def index - @automation_rules = Current.account.automation_rules.active + @automation_rules = Current.account.automation_rules end def create @@ -32,9 +32,9 @@ class Api::V1::Accounts::AutomationRulesController < Api::V1::Accounts::BaseCont def automation_rules_permit params.permit( - :name, :description, :event_name, :account_id, + :name, :description, :event_name, :account_id, :active, conditions: [:attribute_key, :filter_operator, :query_operator, { values: [] }], - actions: [:action_name, { action_params: [] }] + actions: [:action_name, { action_params: [{}] }] ) end diff --git a/app/controllers/api/v1/accounts/base_controller.rb b/app/controllers/api/v1/accounts/base_controller.rb index ddb0f44f4..e30effc59 100644 --- a/app/controllers/api/v1/accounts/base_controller.rb +++ b/app/controllers/api/v1/accounts/base_controller.rb @@ -1,32 +1,6 @@ class Api::V1::Accounts::BaseController < Api::BaseController include SwitchLocale + include EnsureCurrentAccountHelper before_action :current_account around_action :switch_locale_using_account_locale - - private - - def current_account - @current_account ||= ensure_current_account - Current.account = @current_account - end - - def ensure_current_account - account = Account.find(params[:account_id]) - if current_user - account_accessible_for_user?(account) - elsif @resource.is_a?(AgentBot) - account_accessible_for_bot?(account) - end - account - end - - def account_accessible_for_user?(account) - @current_account_user = account.account_users.find_by(user_id: current_user.id) - Current.account_user = @current_account_user - render_unauthorized('You are not authorized to access this account') unless @current_account_user - end - - def account_accessible_for_bot?(account) - render_unauthorized('You are not authorized to access this account') unless @resource.agent_bot_inboxes.find_by(account_id: account.id) - end end diff --git a/app/controllers/api/v1/accounts/campaigns_controller.rb b/app/controllers/api/v1/accounts/campaigns_controller.rb index 18d0998c8..6d2fb7729 100644 --- a/app/controllers/api/v1/accounts/campaigns_controller.rb +++ b/app/controllers/api/v1/accounts/campaigns_controller.rb @@ -11,7 +11,7 @@ class Api::V1::Accounts::CampaignsController < Api::V1::Accounts::BaseController end def destroy - @campaign.destroy + @campaign.destroy! head :ok end diff --git a/app/controllers/api/v1/accounts/canned_responses_controller.rb b/app/controllers/api/v1/accounts/canned_responses_controller.rb index bbfa9c4b7..031ffc415 100644 --- a/app/controllers/api/v1/accounts/canned_responses_controller.rb +++ b/app/controllers/api/v1/accounts/canned_responses_controller.rb @@ -17,7 +17,7 @@ class Api::V1::Accounts::CannedResponsesController < Api::V1::Accounts::BaseCont end def destroy - @canned_response.destroy + @canned_response.destroy! head :ok end diff --git a/app/controllers/api/v1/accounts/contacts/notes_controller.rb b/app/controllers/api/v1/accounts/contacts/notes_controller.rb index fb9f3c5c3..7bc9dd121 100644 --- a/app/controllers/api/v1/accounts/contacts/notes_controller.rb +++ b/app/controllers/api/v1/accounts/contacts/notes_controller.rb @@ -10,7 +10,7 @@ class Api::V1::Accounts::Contacts::NotesController < Api::V1::Accounts::Contacts end def destroy - @note.destroy + @note.destroy! head :ok end diff --git a/app/controllers/api/v1/accounts/conversations/base_controller.rb b/app/controllers/api/v1/accounts/conversations/base_controller.rb index f521719ae..b55b72013 100644 --- a/app/controllers/api/v1/accounts/conversations/base_controller.rb +++ b/app/controllers/api/v1/accounts/conversations/base_controller.rb @@ -1,4 +1,5 @@ class Api::V1::Accounts::Conversations::BaseController < Api::V1::Accounts::BaseController + include EnsureCurrentAccountHelper before_action :conversation private diff --git a/app/controllers/api/v1/accounts/conversations/direct_uploads_controller.rb b/app/controllers/api/v1/accounts/conversations/direct_uploads_controller.rb new file mode 100644 index 000000000..f4ac05d6e --- /dev/null +++ b/app/controllers/api/v1/accounts/conversations/direct_uploads_controller.rb @@ -0,0 +1,17 @@ +class Api::V1::Accounts::Conversations::DirectUploadsController < ActiveStorage::DirectUploadsController + include EnsureCurrentAccountHelper + before_action :current_account + before_action :conversation + + def create + return if @conversation.nil? || @current_account.nil? + + super + end + + private + + def conversation + @conversation ||= Current.account.conversations.find_by(display_id: params[:conversation_id]) + end +end diff --git a/app/controllers/api/v1/accounts/conversations/messages_controller.rb b/app/controllers/api/v1/accounts/conversations/messages_controller.rb index ffd00461b..77a3a7081 100644 --- a/app/controllers/api/v1/accounts/conversations/messages_controller.rb +++ b/app/controllers/api/v1/accounts/conversations/messages_controller.rb @@ -13,7 +13,7 @@ class Api::V1::Accounts::Conversations::MessagesController < Api::V1::Accounts:: def destroy ActiveRecord::Base.transaction do - message.update!(content: I18n.t('conversations.messages.deleted'), deleted: true) + message.update!(content: I18n.t('conversations.messages.deleted'), content_attributes: { deleted: true }) message.attachments.destroy_all end end diff --git a/app/controllers/api/v1/accounts/csat_survey_responses_controller.rb b/app/controllers/api/v1/accounts/csat_survey_responses_controller.rb index 26de6d406..347f028f7 100644 --- a/app/controllers/api/v1/accounts/csat_survey_responses_controller.rb +++ b/app/controllers/api/v1/accounts/csat_survey_responses_controller.rb @@ -30,8 +30,7 @@ class Api::V1::Accounts::CsatSurveyResponsesController < Api::V1::Accounts::Base def set_csat_survey_responses @csat_survey_responses = filtrate( Current.account.csat_survey_responses.includes([:conversation, :assigned_agent, :contact]) - ) - @csat_survey_responses = @csat_survey_responses.where(created_at: range) if range.present? + ).filter_by_created_at(range).filter_by_assigned_agent_id(params[:user_ids]) end def set_current_page_surveys diff --git a/app/controllers/api/v1/accounts/custom_attribute_definitions_controller.rb b/app/controllers/api/v1/accounts/custom_attribute_definitions_controller.rb index 419540438..3840644ce 100644 --- a/app/controllers/api/v1/accounts/custom_attribute_definitions_controller.rb +++ b/app/controllers/api/v1/accounts/custom_attribute_definitions_controller.rb @@ -18,7 +18,7 @@ class Api::V1::Accounts::CustomAttributeDefinitionsController < Api::V1::Account end def destroy - @custom_attribute_definition.destroy + @custom_attribute_definition.destroy! head :no_content end diff --git a/app/controllers/api/v1/accounts/custom_filters_controller.rb b/app/controllers/api/v1/accounts/custom_filters_controller.rb index e6c7b6857..188f0e623 100644 --- a/app/controllers/api/v1/accounts/custom_filters_controller.rb +++ b/app/controllers/api/v1/accounts/custom_filters_controller.rb @@ -18,7 +18,7 @@ class Api::V1::Accounts::CustomFiltersController < Api::V1::Accounts::BaseContro end def destroy - @custom_filter.destroy + @custom_filter.destroy! head :no_content end diff --git a/app/controllers/api/v1/accounts/inboxes_controller.rb b/app/controllers/api/v1/accounts/inboxes_controller.rb index 2bda5c07a..66a71985d 100644 --- a/app/controllers/api/v1/accounts/inboxes_controller.rb +++ b/app/controllers/api/v1/accounts/inboxes_controller.rb @@ -48,7 +48,10 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController # Inbox update doesn't necessarily need channel attributes return if permitted_params(channel_attributes)[:channel].blank? - validate_email_channel(channel_attributes) if @inbox.inbox_type == 'Email' + if @inbox.inbox_type == 'Email' + validate_email_channel(channel_attributes) + @inbox.channel.reauthorized! + end @inbox.channel.update!(permitted_params(channel_attributes)[:channel]) update_channel_feature_flags @@ -70,7 +73,7 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController end def destroy - @inbox.destroy + @inbox.destroy! head :ok end diff --git a/app/controllers/api/v1/accounts/integrations/hooks_controller.rb b/app/controllers/api/v1/accounts/integrations/hooks_controller.rb index 18a16a30d..dd2af4ef2 100644 --- a/app/controllers/api/v1/accounts/integrations/hooks_controller.rb +++ b/app/controllers/api/v1/accounts/integrations/hooks_controller.rb @@ -11,7 +11,7 @@ class Api::V1::Accounts::Integrations::HooksController < Api::V1::Accounts::Base end def destroy - @hook.destroy + @hook.destroy! head :ok end diff --git a/app/controllers/api/v1/accounts/integrations/slack_controller.rb b/app/controllers/api/v1/accounts/integrations/slack_controller.rb index 537ddd688..b5571b245 100644 --- a/app/controllers/api/v1/accounts/integrations/slack_controller.rb +++ b/app/controllers/api/v1/accounts/integrations/slack_controller.rb @@ -20,7 +20,7 @@ class Api::V1::Accounts::Integrations::SlackController < Api::V1::Accounts::Base end def destroy - @hook.destroy + @hook.destroy! head :ok end diff --git a/app/controllers/api/v1/accounts/kbase/categories_controller.rb b/app/controllers/api/v1/accounts/kbase/categories_controller.rb index e114ee5e4..a40053dd2 100644 --- a/app/controllers/api/v1/accounts/kbase/categories_controller.rb +++ b/app/controllers/api/v1/accounts/kbase/categories_controller.rb @@ -14,7 +14,7 @@ class Api::V1::Accounts::Kbase::CategoriesController < Api::V1::Accounts::Kbase: end def destroy - @category.destroy + @category.destroy! head :ok end diff --git a/app/controllers/api/v1/accounts/kbase/portals_controller.rb b/app/controllers/api/v1/accounts/kbase/portals_controller.rb index e0788b587..804b2d421 100644 --- a/app/controllers/api/v1/accounts/kbase/portals_controller.rb +++ b/app/controllers/api/v1/accounts/kbase/portals_controller.rb @@ -14,7 +14,7 @@ class Api::V1::Accounts::Kbase::PortalsController < Api::V1::Accounts::Kbase::Ba end def destroy - @portal.destroy + @portal.destroy! head :ok end diff --git a/app/controllers/api/v1/accounts/labels_controller.rb b/app/controllers/api/v1/accounts/labels_controller.rb index 547b9e6d6..54455943b 100644 --- a/app/controllers/api/v1/accounts/labels_controller.rb +++ b/app/controllers/api/v1/accounts/labels_controller.rb @@ -18,7 +18,7 @@ class Api::V1::Accounts::LabelsController < Api::V1::Accounts::BaseController end def destroy - @label.destroy + @label.destroy! head :ok end diff --git a/app/controllers/api/v1/accounts/teams_controller.rb b/app/controllers/api/v1/accounts/teams_controller.rb index adfeed62e..e8688dcfb 100644 --- a/app/controllers/api/v1/accounts/teams_controller.rb +++ b/app/controllers/api/v1/accounts/teams_controller.rb @@ -18,7 +18,7 @@ class Api::V1::Accounts::TeamsController < Api::V1::Accounts::BaseController end def destroy - @team.destroy + @team.destroy! head :ok end diff --git a/app/controllers/api/v1/accounts/webhooks_controller.rb b/app/controllers/api/v1/accounts/webhooks_controller.rb index 58f9b21a0..0add18047 100644 --- a/app/controllers/api/v1/accounts/webhooks_controller.rb +++ b/app/controllers/api/v1/accounts/webhooks_controller.rb @@ -16,7 +16,7 @@ class Api::V1::Accounts::WebhooksController < Api::V1::Accounts::BaseController end def destroy - @webhook.destroy + @webhook.destroy! head :ok end diff --git a/app/controllers/api/v1/notification_subscriptions_controller.rb b/app/controllers/api/v1/notification_subscriptions_controller.rb index 5f1cf30e4..a01c2ca03 100644 --- a/app/controllers/api/v1/notification_subscriptions_controller.rb +++ b/app/controllers/api/v1/notification_subscriptions_controller.rb @@ -9,7 +9,7 @@ class Api::V1::NotificationSubscriptionsController < Api::BaseController def destroy notification_subscription = NotificationSubscription.where(["subscription_attributes->>'push_token' = ?", params[:push_token]]).first - notification_subscription.destroy + notification_subscription.destroy! head :ok end diff --git a/app/controllers/api/v1/widget/base_controller.rb b/app/controllers/api/v1/widget/base_controller.rb index 38c880526..8df4737db 100644 --- a/app/controllers/api/v1/widget/base_controller.rb +++ b/app/controllers/api/v1/widget/base_controller.rb @@ -1,5 +1,6 @@ class Api::V1::Widget::BaseController < ApplicationController include SwitchLocale + include WebsiteTokenHelper before_action :set_web_widget before_action :set_contact @@ -19,23 +20,6 @@ class Api::V1::Widget::BaseController < ApplicationController @conversation ||= conversations.last end - def auth_token_params - @auth_token_params ||= ::Widget::TokenService.new(token: request.headers['X-Auth-Token']).decode_token - end - - def set_web_widget - @web_widget = ::Channel::WebWidget.find_by!(website_token: permitted_params[:website_token]) - @current_account = @web_widget.account - end - - def set_contact - @contact_inbox = @web_widget.inbox.contact_inboxes.find_by( - source_id: auth_token_params[:source_id] - ) - @contact = @contact_inbox&.contact - raise ActiveRecord::RecordNotFound unless @contact - end - def create_conversation ::Conversation.create!(conversation_params) end @@ -94,10 +78,6 @@ class Api::V1::Widget::BaseController < ApplicationController { timestamp: permitted_params[:message][:timestamp] } end - def permitted_params - params.permit(:website_token) - end - def message_params { account_id: conversation.account_id, diff --git a/app/controllers/api/v1/widget/contacts_controller.rb b/app/controllers/api/v1/widget/contacts_controller.rb index d745c4153..fbc303a4f 100644 --- a/app/controllers/api/v1/widget/contacts_controller.rb +++ b/app/controllers/api/v1/widget/contacts_controller.rb @@ -46,6 +46,7 @@ class Api::V1::Widget::ContactsController < Api::V1::Widget::BaseController end def permitted_params - params.permit(:website_token, :identifier, :identifier_hash, :email, :name, :avatar_url, :phone_number, custom_attributes: {}) + params.permit(:website_token, :identifier, :identifier_hash, :email, :name, :avatar_url, :phone_number, custom_attributes: {}, + additional_attributes: {}) end end diff --git a/app/controllers/api/v1/widget/conversations_controller.rb b/app/controllers/api/v1/widget/conversations_controller.rb index 8d28345d4..cc1b16b75 100644 --- a/app/controllers/api/v1/widget/conversations_controller.rb +++ b/app/controllers/api/v1/widget/conversations_controller.rb @@ -44,6 +44,15 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController head :ok end + def toggle_status + head :not_found && return if conversation.nil? + unless conversation.resolved? + conversation.status = :resolved + conversation.save + end + head :ok + end + private def trigger_typing_event(event) diff --git a/app/controllers/api/v1/widget/direct_uploads_controller.rb b/app/controllers/api/v1/widget/direct_uploads_controller.rb new file mode 100644 index 000000000..a6abdb3e1 --- /dev/null +++ b/app/controllers/api/v1/widget/direct_uploads_controller.rb @@ -0,0 +1,11 @@ +class Api::V1::Widget::DirectUploadsController < ActiveStorage::DirectUploadsController + include WebsiteTokenHelper + before_action :set_web_widget + before_action :set_contact + + def create + return if @contact.nil? || @current_account.nil? + + super + end +end diff --git a/app/controllers/api/v2/accounts/reports_controller.rb b/app/controllers/api/v2/accounts/reports_controller.rb index a83611676..efa09a43c 100644 --- a/app/controllers/api/v2/accounts/reports_controller.rb +++ b/app/controllers/api/v2/accounts/reports_controller.rb @@ -41,12 +41,22 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController raise Pundit::NotAuthorizedError unless Current.account_user.administrator? end - def summary_params + def current_summary_params { type: params[:type].to_sym, - since: params[:since], - until: params[:until], id: params[:id], + since: range[:current][:since], + until: range[:current][:until], + group_by: params[:group_by] + } + end + + def previous_summary_params + { + type: params[:type].to_sym, + id: params[:id], + since: range[:previous][:since], + until: range[:previous][:until], group_by: params[:group_by] } end @@ -63,8 +73,22 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController } end + def range + { + current: { + since: params[:since], + until: params[:until] + }, + previous: { + since: (params[:since].to_i - (params[:until].to_i - params[:since].to_i)).to_s, + until: params[:since] + } + } + end + def summary_metrics - builder = V2::ReportBuilder.new(Current.account, summary_params) - builder.summary + summary = V2::ReportBuilder.new(Current.account, current_summary_params).summary + summary[:previous] = V2::ReportBuilder.new(Current.account, previous_summary_params).summary + summary end end diff --git a/app/controllers/concerns/ensure_current_account_helper.rb b/app/controllers/concerns/ensure_current_account_helper.rb new file mode 100644 index 000000000..dccc64350 --- /dev/null +++ b/app/controllers/concerns/ensure_current_account_helper.rb @@ -0,0 +1,28 @@ +module EnsureCurrentAccountHelper + private + + def current_account + @current_account ||= ensure_current_account + Current.account = @current_account + end + + def ensure_current_account + account = Account.find(params[:account_id]) + if current_user + account_accessible_for_user?(account) + elsif @resource.is_a?(AgentBot) + account_accessible_for_bot?(account) + end + account + end + + def account_accessible_for_user?(account) + @current_account_user = account.account_users.find_by(user_id: current_user.id) + Current.account_user = @current_account_user + render_unauthorized('You are not authorized to access this account') unless @current_account_user + end + + def account_accessible_for_bot?(account) + render_unauthorized('You are not authorized to access this account') unless @resource.agent_bot_inboxes.find_by(account_id: account.id) + end +end diff --git a/app/controllers/concerns/website_token_helper.rb b/app/controllers/concerns/website_token_helper.rb new file mode 100644 index 000000000..0158a4107 --- /dev/null +++ b/app/controllers/concerns/website_token_helper.rb @@ -0,0 +1,24 @@ +module WebsiteTokenHelper + def auth_token_params + @auth_token_params ||= ::Widget::TokenService.new(token: request.headers['X-Auth-Token']).decode_token + end + + def set_web_widget + @web_widget = ::Channel::WebWidget.find_by!(website_token: permitted_params[:website_token]) + @current_account = @web_widget.account + end + + def set_contact + @contact_inbox = @web_widget.inbox.contact_inboxes.find_by( + source_id: auth_token_params[:source_id] + ) + @contact = @contact_inbox&.contact + raise ActiveRecord::RecordNotFound unless @contact + + Current.contact = @contact + end + + def permitted_params + params.permit(:website_token) + end +end diff --git a/app/controllers/platform/api/v1/account_users_controller.rb b/app/controllers/platform/api/v1/account_users_controller.rb index 8f651cfd9..b8a8f701a 100644 --- a/app/controllers/platform/api/v1/account_users_controller.rb +++ b/app/controllers/platform/api/v1/account_users_controller.rb @@ -13,7 +13,7 @@ class Platform::Api::V1::AccountUsersController < PlatformController end def destroy - @resource.account_users.find_by(user_id: account_user_params[:user_id])&.destroy + @resource.account_users.find_by(user_id: account_user_params[:user_id])&.destroy! head :ok end diff --git a/app/controllers/platform/api/v1/users_controller.rb b/app/controllers/platform/api/v1/users_controller.rb index 960dee0e3..bf5b642f8 100644 --- a/app/controllers/platform/api/v1/users_controller.rb +++ b/app/controllers/platform/api/v1/users_controller.rb @@ -9,7 +9,7 @@ class Platform::Api::V1::UsersController < PlatformController @resource = (User.find_by(email: user_params[:email]) || User.new(user_params)) @resource.save! @resource.confirm - @platform_app.platform_app_permissibles.find_or_create_by(permissible: @resource) + @platform_app.platform_app_permissibles.find_or_create_by!(permissible: @resource) end def login diff --git a/app/dispatchers/async_dispatcher.rb b/app/dispatchers/async_dispatcher.rb index 3f95405f9..f2f238cd4 100644 --- a/app/dispatchers/async_dispatcher.rb +++ b/app/dispatchers/async_dispatcher.rb @@ -12,10 +12,10 @@ class AsyncDispatcher < BaseDispatcher [ CampaignListener.instance, CsatSurveyListener.instance, - EventListener.instance, HookListener.instance, InstallationWebhookListener.instance, NotificationListener.instance, + ReportingEventListener.instance, WebhookListener.instance, AutomationRuleListener.instance ] diff --git a/app/helpers/api/v1/reports_helper.rb b/app/helpers/api/v1/reports_helper.rb deleted file mode 100644 index 308bab3e6..000000000 --- a/app/helpers/api/v1/reports_helper.rb +++ /dev/null @@ -1,2 +0,0 @@ -module Api::V1::ReportsHelper -end diff --git a/app/helpers/file_type_helper.rb b/app/helpers/file_type_helper.rb index 503c7f25b..db3f6249d 100644 --- a/app/helpers/file_type_helper.rb +++ b/app/helpers/file_type_helper.rb @@ -3,7 +3,7 @@ module FileTypeHelper def file_type(content_type) return :image if image_file?(content_type) return :video if video_file?(content_type) - return :audio if content_type.include?('audio/') + return :audio if content_type&.include?('audio/') :file end @@ -14,7 +14,8 @@ module FileTypeHelper 'image/png', 'image/gif', 'image/bmp', - 'image/webp' + 'image/webp', + 'image' ].include?(content_type) end @@ -23,7 +24,8 @@ module FileTypeHelper 'video/ogg', 'video/mp4', 'video/webm', - 'video/quicktime' + 'video/quicktime', + 'video' ].include?(content_type) end end diff --git a/app/javascript/dashboard/App.vue b/app/javascript/dashboard/App.vue index 502d8bda1..2c91de16f 100644 --- a/app/javascript/dashboard/App.vue +++ b/app/javascript/dashboard/App.vue @@ -1,5 +1,6 @@ diff --git a/app/javascript/dashboard/components/index.js b/app/javascript/dashboard/components/index.js index f737bd313..d2614421c 100644 --- a/app/javascript/dashboard/components/index.js +++ b/app/javascript/dashboard/components/index.js @@ -21,6 +21,7 @@ import SubmitButton from './buttons/FormSubmitButton'; import Tabs from './ui/Tabs/Tabs'; import TabsItem from './ui/Tabs/TabsItem'; import Thumbnail from './widgets/Thumbnail.vue'; +import ConfirmModal from './widgets/modal/ConfirmationModal.vue'; const WootUIKit = { AvatarUploader, @@ -45,6 +46,7 @@ const WootUIKit = { Tabs, TabsItem, Thumbnail, + ConfirmModal, install(Vue) { const keys = Object.keys(this); keys.pop(); // remove 'install' from keys diff --git a/app/javascript/dashboard/components/layout/Sidebar.vue b/app/javascript/dashboard/components/layout/Sidebar.vue index ee64f4949..826d766df 100644 --- a/app/javascript/dashboard/components/layout/Sidebar.vue +++ b/app/javascript/dashboard/components/layout/Sidebar.vue @@ -19,23 +19,6 @@ :current-role="currentRole" @add-label="showAddLabelPopup" /> - - - - - - @@ -46,12 +29,8 @@ import adminMixin from '../../mixins/isAdmin'; import { getSidebarItems } from './config/default-sidebar'; import alertMixin from 'shared/mixins/alertMixin'; -import AccountSelector from './sidebarComponents/AccountSelector.vue'; -import AddAccountModal from './sidebarComponents/AddAccountModal.vue'; -import AddLabelModal from '../../routes/dashboard/settings/labels/AddLabel'; import PrimarySidebar from './sidebarComponents/Primary'; import SecondarySidebar from './sidebarComponents/Secondary'; -import WootKeyShortcutModal from 'components/widgets/modal/WootKeyShortcutModal'; import { hasPressedAltAndCKey, hasPressedAltAndRKey, @@ -65,21 +44,13 @@ import router from '../../routes'; export default { components: { - AccountSelector, - AddAccountModal, - AddLabelModal, PrimarySidebar, SecondarySidebar, - WootKeyShortcutModal, }, mixins: [adminMixin, alertMixin, eventListenerMixins], data() { return { showOptionsMenu: false, - showAccountModal: false, - showCreateAccountModal: false, - showAddLabelModal: false, - showShortcutModal: false, }; }, @@ -162,10 +133,10 @@ export default { } }, toggleKeyShortcutModal() { - this.showShortcutModal = true; + this.$emit('open-key-shortcut-modal'); }, closeKeyShortcutModal() { - this.showShortcutModal = false; + this.$emit('close-key-shortcut-modal'); }, handleKeyEvents(e) { if (hasPressedCommandAndForwardSlash(e)) { @@ -200,20 +171,10 @@ export default { window.$chatwoot.toggle(); }, toggleAccountModal() { - this.showAccountModal = !this.showAccountModal; - }, - openCreateAccountModal() { - this.showAccountModal = false; - this.showCreateAccountModal = true; - }, - closeCreateAccountModal() { - this.showCreateAccountModal = false; + this.$emit('toggle-account-modal'); }, showAddLabelPopup() { - this.showAddLabelModal = true; - }, - hideAddLabelPopup() { - this.showAddLabelModal = false; + this.$emit('show-add-label-popup'); }, }, }; @@ -223,6 +184,8 @@ export default { .woot-sidebar { background: var(--white); display: flex; + min-height: 0; + height: 100%; } diff --git a/app/javascript/dashboard/components/layout/sidebarComponents/Primary.vue b/app/javascript/dashboard/components/layout/sidebarComponents/Primary.vue index 0f568ab75..002f371bf 100644 --- a/app/javascript/dashboard/components/layout/sidebarComponents/Primary.vue +++ b/app/javascript/dashboard/components/layout/sidebarComponents/Primary.vue @@ -93,7 +93,7 @@ export default { width: var(--space-jumbo); border-right: 1px solid var(--s-50); box-sizing: content-box; - height: 100vh; + height: 100%; flex-shrink: 0; } diff --git a/app/javascript/dashboard/components/layout/sidebarComponents/Secondary.vue b/app/javascript/dashboard/components/layout/sidebarComponents/Secondary.vue index 6222490e4..1f98305a1 100644 --- a/app/javascript/dashboard/components/layout/sidebarComponents/Secondary.vue +++ b/app/javascript/dashboard/components/layout/sidebarComponents/Secondary.vue @@ -225,7 +225,7 @@ export default { .secondary-menu { background: var(--white); border-right: 1px solid var(--s-50); - height: 100vh; + height: 100%; width: 19rem; flex-shrink: 0; overflow: hidden; diff --git a/app/javascript/dashboard/components/layout/sidebarComponents/SecondaryNavItem.vue b/app/javascript/dashboard/components/layout/sidebarComponents/SecondaryNavItem.vue index 3a344d985..ffd8018d5 100644 --- a/app/javascript/dashboard/components/layout/sidebarComponents/SecondaryNavItem.vue +++ b/app/javascript/dashboard/components/layout/sidebarComponents/SecondaryNavItem.vue @@ -20,7 +20,8 @@ data-view-component="true" label="Beta" class="beta" - >Beta + > + {{ $t('SIDEBAR.BETA') }} @@ -233,7 +234,7 @@ export default { padding-left: var(--space-smaller) !important; margin-left: var(--space-half) !important; display: inline-block; - font-size: var(--font-size-mini); + font-size: var(--font-size-micro); font-weight: var(--font-weight-medium); line-height: 18px; border: 1px solid transparent; diff --git a/app/javascript/dashboard/components/ui/Banner.vue b/app/javascript/dashboard/components/ui/Banner.vue index 52d7c5740..c9d71bfaf 100644 --- a/app/javascript/dashboard/components/ui/Banner.vue +++ b/app/javascript/dashboard/components/ui/Banner.vue @@ -1,6 +1,6 @@ @@ -210,8 +208,7 @@ export default { if (!cityAndCountry) { return ''; } - const countryFlag = countryCode ? flag(countryCode) : '🌎'; - return `${cityAndCountry} ${countryFlag}`; + return this.findCountryFlag(countryCode, cityAndCountry); }, socialProfiles() { const { @@ -222,22 +219,11 @@ export default { return { twitter: twitterScreenName, ...(socialProfiles || {}) }; }, // Delete Modal - deleteConfirmText() { - return `${this.$t('DELETE_CONTACT.CONFIRM.YES')} ${this.contact.name}`; - }, - deleteRejectText() { - return `${this.$t('DELETE_CONTACT.CONFIRM.NO')} ${this.contact.name}`; - }, confirmDeleteMessage() { return `${this.$t('DELETE_CONTACT.CONFIRM.MESSAGE')} ${ this.contact.name } ?`; }, - confirmPlaceHolderText() { - return `${this.$t('DELETE_CONTACT.CONFIRM.PLACE_HOLDER', { - contactName: this.contact.name, - })}`; - }, }, methods: { toggleMergeModal() { @@ -261,6 +247,14 @@ export default { this.showConversationModal = false; this.showEditModal = false; }, + findCountryFlag(countryCode, cityAndCountry) { + try { + const countryFlag = countryCode ? flag(countryCode) : '🌎'; + return `${cityAndCountry} ${countryFlag}`; + } catch (error) { + return ''; + } + }, async deleteContact({ id }) { try { await this.$store.dispatch('contacts/delete', id); diff --git a/app/javascript/dashboard/routes/dashboard/settings/automation/Index.vue b/app/javascript/dashboard/routes/dashboard/settings/automation/Index.vue index be79dd4f5..811fc95c6 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/automation/Index.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/automation/Index.vue @@ -34,12 +34,19 @@ {{ automation.name }} {{ automation.description }} - - + {{ readableTime(automation.created_on) }} @@ -120,6 +127,11 @@ @saveAutomation="submitAutomation" /> + diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/Index.vue b/app/javascript/dashboard/routes/dashboard/settings/reports/Index.vue index 1f2839048..fe0240d74 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/reports/Index.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/reports/Index.vue @@ -10,6 +10,7 @@ @@ -48,6 +50,7 @@ import fromUnixTime from 'date-fns/fromUnixTime'; import format from 'date-fns/format'; import ReportFilterSelector from './components/FilterSelector'; import { GROUP_BY_FILTER } from './constants'; +import reportMixin from '../../../../mixins/reportMixin'; const REPORTS_KEYS = { CONVERSATIONS: 'conversations_count', @@ -62,6 +65,7 @@ export default { components: { ReportFilterSelector, }, + mixins: [reportMixin], data() { return { from: 0, diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/components/FilterSelector.vue b/app/javascript/dashboard/routes/dashboard/settings/reports/components/FilterSelector.vue index 8df8beec3..885b76283 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/reports/components/FilterSelector.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/reports/components/FilterSelector.vue @@ -24,7 +24,7 @@ @change="onChange" />
+
+ +
diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/components/ReportFilters.vue b/app/javascript/dashboard/routes/dashboard/settings/reports/components/ReportFilters.vue index 2fd32ee69..857a74363 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/reports/components/ReportFilters.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/reports/components/ReportFilters.vue @@ -255,7 +255,6 @@ export default { }, methods: { onDateRangeChange() { - console.log(this.from, this.to); this.$emit('date-range-change', { from: this.from, to: this.to, diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/components/WootReports.vue b/app/javascript/dashboard/routes/dashboard/settings/reports/components/WootReports.vue index 449b0383f..8ce264080 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/reports/components/WootReports.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/reports/components/WootReports.vue @@ -27,7 +27,8 @@ :heading="metric.NAME" :index="index" :on-click="changeSelection" - :point="accountSummary[metric.KEY]" + :point="displayMetric(metric.KEY)" + :trend="calculateTrend(metric.KEY)" :selected="index === currentSelection" /> @@ -55,6 +56,7 @@ import ReportFilters from './ReportFilters'; import fromUnixTime from 'date-fns/fromUnixTime'; import format from 'date-fns/format'; import { GROUP_BY_FILTER } from '../constants'; +import reportMixin from '../../../../../mixins/reportMixin'; const REPORTS_KEYS = { CONVERSATIONS: 'conversations_count', @@ -68,6 +70,7 @@ export default { components: { ReportFilters, }, + mixins: [reportMixin], props: { type: { type: String, diff --git a/app/javascript/dashboard/routes/index.js b/app/javascript/dashboard/routes/index.js index b2d19cf7f..14ef77bc3 100644 --- a/app/javascript/dashboard/routes/index.js +++ b/app/javascript/dashboard/routes/index.js @@ -5,6 +5,7 @@ import login from './login/login.routes'; import dashboard from './dashboard/dashboard.routes'; import authRoute from './auth/auth.routes'; import { frontendURL } from '../helper/URLHelper'; +import { clearBrowserSessionCookies } from '../store/utils/api'; const routes = [ ...login.routes, @@ -101,6 +102,13 @@ export const validateAuthenticateRoutePermission = (to, from, next) => { return nextRoute ? next(frontendURL(nextRoute)) : next(); }; +const validateSSOLoginParams = to => { + const isLoginRoute = to.name === 'login'; + const { email, sso_auth_token: ssoAuthToken } = to.query || {}; + const hasValidSSOParams = email && ssoAuthToken; + return isLoginRoute && hasValidSSOParams; +}; + const validateRouteAccess = (to, from, next) => { if ( window.chatwootConfig.signupEnabled !== 'true' && @@ -111,6 +119,11 @@ const validateRouteAccess = (to, from, next) => { next(frontendURL(`accounts/${user.account_id}/dashboard`)); } + if (validateSSOLoginParams(to)) { + clearBrowserSessionCookies(); + return next(); + } + if (authIgnoreRoutes.includes(to.name)) { return next(); } diff --git a/app/javascript/dashboard/routes/login/Login.vue b/app/javascript/dashboard/routes/login/Login.vue index 92fea2e36..428974af7 100644 --- a/app/javascript/dashboard/routes/login/Login.vue +++ b/app/javascript/dashboard/routes/login/Login.vue @@ -80,6 +80,7 @@ export default { mixins: [globalConfigMixin], props: { ssoAuthToken: { type: String, default: '' }, + ssoAccountId: { type: String, default: '' }, redirectUrl: { type: String, default: '' }, config: { type: String, default: '' }, email: { type: String, default: '' }, @@ -138,6 +139,7 @@ export default { : this.credentials.email, password: this.credentials.password, sso_auth_token: this.ssoAuthToken, + ssoAccountId: this.ssoAccountId, }; this.$store .dispatch('login', credentials) diff --git a/app/javascript/dashboard/routes/login/login.routes.js b/app/javascript/dashboard/routes/login/login.routes.js index c53cffa92..f43e632bf 100644 --- a/app/javascript/dashboard/routes/login/login.routes.js +++ b/app/javascript/dashboard/routes/login/login.routes.js @@ -12,6 +12,7 @@ export default { email: route.query.email, ssoAuthToken: route.query.sso_auth_token, redirectUrl: route.query.route_url, + ssoAccountId: route.query.sso_account_id, }), }, ], diff --git a/app/javascript/dashboard/store/modules/auth.js b/app/javascript/dashboard/store/modules/auth.js index d9ec54564..5b2feef11 100644 --- a/app/javascript/dashboard/store/modules/auth.js +++ b/app/javascript/dashboard/store/modules/auth.js @@ -6,7 +6,7 @@ import authAPI from '../../api/auth'; import createAxios from '../../helper/APIHelper'; import actionCable from '../../helper/actionCable'; import { setUser, getHeaderExpiry, clearCookiesOnLogout } from '../utils/api'; -import { DEFAULT_REDIRECT_URL } from '../../constants'; +import { getLoginRedirectURL } from '../../helper/URLHelper'; const state = { currentUser: { @@ -88,15 +88,16 @@ export const getters = { // actions export const actions = { - login({ commit }, credentials) { + login({ commit }, { ssoAccountId, ...credentials }) { return new Promise((resolve, reject) => { authAPI .login(credentials) - .then(() => { + .then(response => { commit(types.default.SET_CURRENT_USER); window.axios = createAxios(axios); actionCable.init(Vue); - window.location = DEFAULT_REDIRECT_URL; + + window.location = getLoginRedirectURL(ssoAccountId, response.data); resolve(); }) .catch(error => { diff --git a/app/javascript/dashboard/store/modules/contactConversations.js b/app/javascript/dashboard/store/modules/contactConversations.js index 50bbb4f99..9c9f03016 100644 --- a/app/javascript/dashboard/store/modules/contactConversations.js +++ b/app/javascript/dashboard/store/modules/contactConversations.js @@ -66,9 +66,6 @@ export const actions = { id: contactId, data: response.data.payload, }); - commit(types.default.SET_ALL_CONVERSATION, response.data.payload, { - root: true, - }); commit(types.default.SET_CONTACT_CONVERSATIONS_UI_FLAG, { isFetching: false, }); diff --git a/app/javascript/dashboard/store/modules/conversations/getters.js b/app/javascript/dashboard/store/modules/conversations/getters.js index e4b063899..60ca0d590 100644 --- a/app/javascript/dashboard/store/modules/conversations/getters.js +++ b/app/javascript/dashboard/store/modules/conversations/getters.js @@ -1,4 +1,5 @@ import authAPI from '../../../api/auth'; +import { MESSAGE_TYPE } from 'shared/constants/messages'; import { applyPageFilters } from './helpers'; export const getSelectedChatConversation = ({ @@ -19,6 +20,26 @@ const getters = { ); return selectedChat || {}; }, + getLastEmailInSelectedChat: (stage, _getters) => { + const selectedChat = _getters.getSelectedChat; + const { messages = [] } = selectedChat; + const lastEmail = [...messages].reverse().find(message => { + const { + content_attributes: contentAttributes = {}, + message_type: messageType, + } = message; + const { email = {} } = contentAttributes; + const isIncomingOrOutgoing = + messageType === MESSAGE_TYPE.OUTGOING || + messageType === MESSAGE_TYPE.INCOMING; + if (email.from && isIncomingOrOutgoing) { + return true; + } + return false; + }); + + return lastEmail; + }, getMineChats: _state => activeFilters => { const currentUserID = authAPI.getCurrentUser().id; diff --git a/app/javascript/dashboard/store/modules/conversations/index.js b/app/javascript/dashboard/store/modules/conversations/index.js index c8519c253..87d81d070 100644 --- a/app/javascript/dashboard/store/modules/conversations/index.js +++ b/app/javascript/dashboard/store/modules/conversations/index.js @@ -4,6 +4,7 @@ import getters, { getSelectedChatConversation } from './getters'; import actions from './actions'; import { findPendingMessageIndex } from './helpers'; import wootConstants from '../../../constants'; +import { BUS_EVENTS } from '../../../../shared/constants/busEvents'; const state = { allConversations: [], @@ -108,7 +109,7 @@ export const mutations = { chat.messages.push(message); chat.timestamp = message.created_at; if (selectedChatId === conversationId) { - window.bus.$emit('scrollToMessage'); + window.bus.$emit(BUS_EVENTS.SCROLL_TO_MESSAGE); } } }, @@ -130,7 +131,7 @@ export const mutations = { }; Vue.set(allConversations, currentConversationIndex, currentConversation); if (_state.selectedChatId === conversation.id) { - window.bus.$emit('scrollToMessage'); + window.bus.$emit(BUS_EVENTS.SCROLL_TO_MESSAGE); } } else { _state.allConversations.push(conversation); diff --git a/app/javascript/dashboard/store/modules/csat.js b/app/javascript/dashboard/store/modules/csat.js index d38b212be..7bc1dad6d 100644 --- a/app/javascript/dashboard/store/modules/csat.js +++ b/app/javascript/dashboard/store/modules/csat.js @@ -82,10 +82,13 @@ export const getters = { }; export const actions = { - get: async function getResponses({ commit }, { page = 1, from, to } = {}) { + get: async function getResponses( + { commit }, + { page = 1, from, to, user_ids } = {} + ) { commit(types.SET_CSAT_RESPONSE_UI_FLAG, { isFetching: true }); try { - const response = await CSATReports.get({ page, from, to }); + const response = await CSATReports.get({ page, from, to, user_ids }); commit(types.SET_CSAT_RESPONSE, response.data); } catch (error) { // Ignore error @@ -93,10 +96,10 @@ export const actions = { commit(types.SET_CSAT_RESPONSE_UI_FLAG, { isFetching: false }); } }, - getMetrics: async function getMetrics({ commit }, { from, to }) { + getMetrics: async function getMetrics({ commit }, { from, to, user_ids }) { commit(types.SET_CSAT_RESPONSE_UI_FLAG, { isFetchingMetrics: true }); try { - const response = await CSATReports.getMetrics({ from, to }); + const response = await CSATReports.getMetrics({ from, to, user_ids }); commit(types.SET_CSAT_RESPONSE_METRICS, response.data); } catch (error) { // Ignore error diff --git a/app/javascript/dashboard/store/modules/labels.js b/app/javascript/dashboard/store/modules/labels.js index 4183c28ed..f597212a3 100644 --- a/app/javascript/dashboard/store/modules/labels.js +++ b/app/javascript/dashboard/store/modules/labels.js @@ -45,7 +45,8 @@ export const actions = { const response = await LabelsAPI.create(cannedObj); commit(types.ADD_LABEL, response.data); } catch (error) { - throw new Error(error); + const errorMessage = error?.response?.data?.message; + throw new Error(errorMessage); } finally { commit(types.SET_LABEL_UI_FLAG, { isCreating: false }); } diff --git a/app/javascript/dashboard/store/modules/reports.js b/app/javascript/dashboard/store/modules/reports.js index 33f62a9c4..cb1efe3ff 100644 --- a/app/javascript/dashboard/store/modules/reports.js +++ b/app/javascript/dashboard/store/modules/reports.js @@ -20,6 +20,7 @@ const state = { incoming_messages_count: 0, outgoing_messages_count: 0, resolutions_count: 0, + previous: {}, }, }; @@ -124,24 +125,6 @@ const mutations = { }, [types.default.SET_ACCOUNT_SUMMARY](_state, summaryData) { _state.accountSummary = summaryData; - // Average First Response Time - let avgFirstResTimeInHr = 0; - if (summaryData.avg_first_response_time) { - avgFirstResTimeInHr = ( - summaryData.avg_first_response_time / 3600 - ).toFixed(2); - avgFirstResTimeInHr = `${avgFirstResTimeInHr} Hr`; - } - // Average Resolution Time - let avgResolutionTimeInHr = 0; - if (summaryData.avg_resolution_time) { - avgResolutionTimeInHr = (summaryData.avg_resolution_time / 3600).toFixed( - 2 - ); - avgResolutionTimeInHr = `${avgResolutionTimeInHr} Hr`; - } - _state.accountSummary.avg_first_response_time = avgFirstResTimeInHr; - _state.accountSummary.avg_resolution_time = avgResolutionTimeInHr; }, }; diff --git a/app/javascript/dashboard/store/modules/specs/contactConversations/actions.spec.js b/app/javascript/dashboard/store/modules/specs/contactConversations/actions.spec.js index 3846d4aa7..0de32311b 100644 --- a/app/javascript/dashboard/store/modules/specs/contactConversations/actions.spec.js +++ b/app/javascript/dashboard/store/modules/specs/contactConversations/actions.spec.js @@ -19,7 +19,6 @@ describe('#actions', () => { types.default.SET_CONTACT_CONVERSATIONS, { id: 1, data: conversationList }, ], - [types.default.SET_ALL_CONVERSATION, conversationList, { root: true }], [ types.default.SET_CONTACT_CONVERSATIONS_UI_FLAG, { isFetching: false }, diff --git a/app/javascript/dashboard/store/modules/specs/conversations/getters.spec.js b/app/javascript/dashboard/store/modules/specs/conversations/getters.spec.js index bb718bb24..7dc0dade5 100644 --- a/app/javascript/dashboard/store/modules/specs/conversations/getters.spec.js +++ b/app/javascript/dashboard/store/modules/specs/conversations/getters.spec.js @@ -131,4 +131,34 @@ describe('#getters', () => { expect(getters.getAppliedConversationFilters(state)).toEqual(filtersList); }); }); + + describe('#getLastEmailInSelectedChat', () => { + it('Returns cc in last email', () => { + const state = {}; + const getSelectedChat = { + messages: [ + { + message_type: 1, + content_attributes: { + email: { + from: 'why@how.my', + cc: ['nithin@me.co', 'we@who.why'], + }, + }, + }, + ], + }; + expect( + getters.getLastEmailInSelectedChat(state, { getSelectedChat }) + ).toEqual({ + message_type: 1, + content_attributes: { + email: { + from: 'why@how.my', + cc: ['nithin@me.co', 'we@who.why'], + }, + }, + }); + }); + }); }); diff --git a/app/javascript/dashboard/store/modules/specs/conversations/mutations.spec.js b/app/javascript/dashboard/store/modules/specs/conversations/mutations.spec.js index cc08939d0..b7828fff3 100644 --- a/app/javascript/dashboard/store/modules/specs/conversations/mutations.spec.js +++ b/app/javascript/dashboard/store/modules/specs/conversations/mutations.spec.js @@ -120,7 +120,7 @@ describe('#mutations', () => { timestamp: 1602256198, }, ]); - expect(global.bus.$emit).toHaveBeenCalledWith('scrollToMessage'); + expect(global.bus.$emit).toHaveBeenCalledWith('SCROLL_TO_MESSAGE'); }); it('update message if it exist in the store', () => { diff --git a/app/javascript/dashboard/store/utils/api.js b/app/javascript/dashboard/store/utils/api.js index 651f0cd61..37fd68c7b 100644 --- a/app/javascript/dashboard/store/utils/api.js +++ b/app/javascript/dashboard/store/utils/api.js @@ -38,13 +38,15 @@ export const setAuthCredentials = response => { setUser(response.data.data, expiryDate); }; +export const clearBrowserSessionCookies = () => { + Cookies.remove('auth_data'); + Cookies.remove('user'); +}; + export const clearCookiesOnLogout = () => { window.bus.$emit(CHATWOOT_RESET); window.bus.$emit(ANALYTICS_RESET); - - Cookies.remove('auth_data'); - Cookies.remove('user'); - + clearBrowserSessionCookies(); const globalConfig = window.globalConfig || {}; const logoutRedirectLink = globalConfig.LOGOUT_REDIRECT_LINK || frontendURL('login'); diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js index 9b3875856..79b9e927e 100644 --- a/app/javascript/packs/application.js +++ b/app/javascript/packs/application.js @@ -21,7 +21,10 @@ import App from '../dashboard/App'; import i18n from '../dashboard/i18n'; import createAxios from '../dashboard/helper/APIHelper'; import commonHelpers, { isJSONValid } from '../dashboard/helper/commons'; -import { getAlertAudio } from '../shared/helpers/AudioNotificationHelper'; +import { + getAlertAudio, + initOnEvents, +} from '../shared/helpers/AudioNotificationHelper'; import { initFaviconSwitcher } from '../shared/helpers/faviconHelper'; import router from '../dashboard/routes'; import store from '../dashboard/store'; @@ -102,6 +105,13 @@ window.onload = () => { vueActionCable.init(); }; +const setupAudioListeners = () => { + getAlertAudio().then(() => { + initOnEvents.forEach(event => { + document.removeEventListener(event, setupAudioListeners, false); + }); + }); +}; window.addEventListener('load', () => { verifyServiceWorkerExistence(registration => registration.pushManager.getSubscription().then(subscription => { @@ -110,6 +120,9 @@ window.addEventListener('load', () => { } }) ); - getAlertAudio(); + window.playAudioAlert = () => {}; + initOnEvents.forEach(e => { + document.addEventListener(e, setupAudioListeners, false); + }); initFaviconSwitcher(); }); diff --git a/app/javascript/packs/sdk.js b/app/javascript/packs/sdk.js index c4bfcdb08..66f6ba790 100755 --- a/app/javascript/packs/sdk.js +++ b/app/javascript/packs/sdk.js @@ -25,6 +25,7 @@ const runSDK = ({ baseUrl, websiteToken }) => { launcherTitle: chatwootSettings.launcherTitle || '', showPopoutButton: chatwootSettings.showPopoutButton || false, widgetStyle: chatwootSettings.widgetStyle || 'standard', + resetTriggered: false, toggle(state) { IFrameHelper.events.toggleBubble(state); @@ -100,6 +101,8 @@ const runSDK = ({ baseUrl, websiteToken }) => { baseUrl: window.$chatwoot.baseUrl, websiteToken: window.$chatwoot.websiteToken, }); + + window.$chatwoot.resetTriggered = true; }, }; diff --git a/app/javascript/packs/survey.js b/app/javascript/packs/survey.js index 3c65f8b82..30bbcf47a 100644 --- a/app/javascript/packs/survey.js +++ b/app/javascript/packs/survey.js @@ -3,6 +3,7 @@ import Vuelidate from 'vuelidate'; import VueI18n from 'vue-i18n'; import App from '../survey/App.vue'; import i18n from '../survey/i18n'; +import store from '../survey/store'; Vue.use(VueI18n); Vue.use(Vuelidate); @@ -20,6 +21,7 @@ Vue.config.productionTip = false; window.onload = () => { window.WOOT_SURVEY = new Vue({ i18n: i18nConfig, + store, render: h => h(App), }).$mount('#app'); }; diff --git a/app/javascript/packs/widget.js b/app/javascript/packs/widget.js index f9f57e2ef..421e2f8b9 100644 --- a/app/javascript/packs/widget.js +++ b/app/javascript/packs/widget.js @@ -4,7 +4,6 @@ import VueI18n from 'vue-i18n'; import store from '../widget/store'; import App from '../widget/App.vue'; import ActionCableConnector from '../widget/helpers/actionCable'; -import { getAlertAudio } from 'shared/helpers/AudioNotificationHelper'; import i18n from '../widget/i18n'; import router from '../widget/router'; @@ -33,5 +32,4 @@ window.onload = () => { window.WOOT_WIDGET, window.chatwootPubsubToken ); - getAlertAudio(); }; diff --git a/app/javascript/sdk/IFrameHelper.js b/app/javascript/sdk/IFrameHelper.js index 2d851ba49..032519912 100644 --- a/app/javascript/sdk/IFrameHelper.js +++ b/app/javascript/sdk/IFrameHelper.js @@ -26,6 +26,10 @@ import { dispatchWindowEvent } from 'shared/helpers/CustomEventHelper'; import { CHATWOOT_ERROR, CHATWOOT_READY } from '../widget/constants/sdkEvents'; import { SET_USER_ERROR } from '../widget/constants/errorTypes'; import { getUserCookieName } from './cookieHelpers'; +import { + getAlertAudio, + initOnEvents, +} from 'shared/helpers/AudioNotificationHelper'; import { isFlatWidgetStyle } from './settingsHelper'; export const IFrameHelper = { @@ -114,6 +118,19 @@ export const IFrameHelper = { iframe.setAttribute('style', `height: ${updatedIframeHeight} !important`); }, + setupAudioListeners: () => { + const { baseUrl = '' } = window.$chatwoot; + getAlertAudio(baseUrl).then(() => + initOnEvents.forEach(event => { + document.removeEventListener( + event, + IFrameHelper.setupAudioListeners, + false + ); + }) + ); + }, + events: { loaded: message => { Cookies.set('cw_conversation', message.config.authToken, { @@ -136,7 +153,18 @@ export const IFrameHelper = { if (window.$chatwoot.user) { IFrameHelper.sendMessage('set-user', window.$chatwoot.user); } + dispatchWindowEvent({ eventName: CHATWOOT_READY }); + + window.playAudioAlert = () => {}; + + initOnEvents.forEach(e => { + document.addEventListener(e, IFrameHelper.setupAudioListeners, false); + }); + + if (!window.$chatwoot.resetTriggered) { + dispatchWindowEvent({ eventName: CHATWOOT_READY }); + } }, error: ({ errorType, data }) => { dispatchWindowEvent({ eventName: CHATWOOT_ERROR, data: data }); @@ -212,6 +240,10 @@ export const IFrameHelper = { closeChat: () => { onBubbleClick({ toggleValue: false }); }, + + playAudio: () => { + window.playAudioAlert(); + }, }, pushEvent: eventName => { IFrameHelper.sendMessage('push-event', { eventName }); diff --git a/app/javascript/sdk/sdk.js b/app/javascript/sdk/sdk.js index bbd920382..a13e36d0f 100644 --- a/app/javascript/sdk/sdk.js +++ b/app/javascript/sdk/sdk.js @@ -29,7 +29,7 @@ export const SDK_CSS = ` .woot-widget-holder.has-unread-view { border-radius: 0 !important; - min-height: 80px; + min-height: 80px !important; height: auto; bottom: 94px; box-shadow: none !important; diff --git a/app/javascript/shared/components/Branding.vue b/app/javascript/shared/components/Branding.vue index faa212e9e..0b075d04d 100644 --- a/app/javascript/shared/components/Branding.vue +++ b/app/javascript/shared/components/Branding.vue @@ -21,7 +21,6 @@ diff --git a/app/javascript/widget/api/conversation.js b/app/javascript/widget/api/conversation.js index 3552765c7..fdb3842fd 100755 --- a/app/javascript/widget/api/conversation.js +++ b/app/javascript/widget/api/conversation.js @@ -48,6 +48,11 @@ const sendEmailTranscript = async ({ email }) => { { email } ); }; +const toggleStatus = async () => { + return API.get( + `/api/v1/widget/conversations/toggle_status${window.location.search}` + ); +}; export { createConversationAPI, @@ -58,4 +63,5 @@ export { toggleTyping, setUserLastSeenAt, sendEmailTranscript, + toggleStatus, }; diff --git a/app/javascript/widget/assets/scss/views/_conversation.scss b/app/javascript/widget/assets/scss/views/_conversation.scss index dbf7f716f..7de312eac 100644 --- a/app/javascript/widget/assets/scss/views/_conversation.scss +++ b/app/javascript/widget/assets/scss/views/_conversation.scss @@ -51,7 +51,10 @@ .has-attachment { overflow: hidden; - padding: 0; + + :not([audio]) { + padding: 0; + } &.has-text { margin-top: $space-smaller; @@ -213,11 +216,14 @@ display: inline-block; font-size: $font-size-default; line-height: 1.5; - max-width: 100%; padding: $space-slab $space-normal; text-align: left; word-break: break-word; + :not([audio]) { + max-width: 100%; + } + >a { color: $color-primary; word-break: break-all; diff --git a/app/javascript/widget/components/AgentMessage.vue b/app/javascript/widget/components/AgentMessage.vue index def769bc3..f3d499126 100755 --- a/app/javascript/widget/components/AgentMessage.vue +++ b/app/javascript/widget/components/AgentMessage.vue @@ -34,6 +34,9 @@ :readable-time="readableTime" @error="onImageLoadError" /> + diff --git a/app/javascript/widget/components/ChatAttachment.vue b/app/javascript/widget/components/ChatAttachment.vue index ff37abefd..87da3f5df 100755 --- a/app/javascript/widget/components/ChatAttachment.vue +++ b/app/javascript/widget/components/ChatAttachment.vue @@ -3,7 +3,7 @@ :size="4096 * 2048" :accept="allowedFileTypes" :data="{ - direct_upload_url: '/rails/active_storage/direct_uploads', + direct_upload_url: '/api/v1/widget/direct_uploads', direct_upload: true, }" @input-file="onFileUpload" @@ -66,11 +66,15 @@ export default { this.isUploading = true; try { if (checkFileSizeLimit(file, MAXIMUM_FILE_UPLOAD_SIZE)) { + const { websiteToken } = window.chatwootWebChannel; const upload = new DirectUpload( file.file, - '/rails/active_storage/direct_uploads', - null, - file.file.name + `/api/v1/widget/direct_uploads?website_token=${websiteToken}`, + { + directUploadWillCreateBlobWithXHR: xhr => { + xhr.setRequestHeader('X-Auth-Token', window.authToken); + }, + } ); upload.create((error, blob) => { diff --git a/app/javascript/widget/components/ChatFooter.vue b/app/javascript/widget/components/ChatFooter.vue index 423eb2642..7ed33acbd 100755 --- a/app/javascript/widget/components/ChatFooter.vue +++ b/app/javascript/widget/components/ChatFooter.vue @@ -37,12 +37,13 @@ import CustomButton from 'shared/components/Button'; import ChatInputWrap from 'widget/components/ChatInputWrap.vue'; import { BUS_EVENTS } from 'shared/constants/busEvents'; import { sendEmailTranscript } from 'widget/api/conversation'; - +import routerMixin from 'widget/mixins/routerMixin'; export default { components: { ChatInputWrap, CustomButton, }, + mixins: [routerMixin], props: { msg: { type: String, @@ -53,7 +54,7 @@ export default { ...mapGetters({ conversationAttributes: 'conversationAttributes/getConversationParams', widgetColor: 'appConfig/getWidgetColor', - getConversationSize: 'conversation/getConversationSize', + conversationSize: 'conversation/getConversationSize', currentUser: 'contacts/getCurrentUser', isWidgetStyleFlat: 'appConfig/isWidgetStyleFlat', }), @@ -80,12 +81,11 @@ export default { 'clearConversationAttributes', ]), async handleSendMessage(content) { - const conversationSize = this.getConversationSize; await this.sendMessage({ content, }); // Update conversation attributes on new conversation - if (conversationSize === 0) { + if (this.conversationSize === 0) { this.getAttributes(); } }, @@ -95,7 +95,12 @@ export default { startNewConversation() { this.clearConversations(); this.clearConversationAttributes(); - window.bus.$emit(BUS_EVENTS.START_NEW_CONVERSATION); + + // To create a new conversation, we are redirecting + // the user to pre-chat with contact fields disabled + // Pass disableContactFields params to the route + // This would disable the contact fields in the pre-chat form + this.replaceRoute('prechat-form', { disableContactFields: true }); }, async sendTranscript() { const { email } = this.currentUser; diff --git a/app/javascript/widget/components/HeaderActions.vue b/app/javascript/widget/components/HeaderActions.vue index b9fb76ff7..4d6e5faa9 100644 --- a/app/javascript/widget/components/HeaderActions.vue +++ b/app/javascript/widget/components/HeaderActions.vue @@ -1,5 +1,13 @@ diff --git a/app/javascript/widget/components/PreChat/Form.vue b/app/javascript/widget/components/PreChat/Form.vue index c053a58f6..7fdf33375 100644 --- a/app/javascript/widget/components/PreChat/Form.vue +++ b/app/javascript/widget/components/PreChat/Form.vue @@ -10,7 +10,7 @@ {{ headerMessage }} ({}), }, + disableContactFields: { + type: Boolean, + default: false, + }, }, validations() { const identityValidations = { @@ -99,7 +103,7 @@ export default { if (this.hasActiveCampaign) { return identityValidations; } - if (this.options.requireEmail) { + if (this.areContactFieldsVisible) { return { ...identityValidations, ...messageValidation, @@ -135,6 +139,9 @@ export default { } return this.options.preChatMessage; }, + areContactFieldsVisible() { + return this.options.requireEmail && !this.disableContactFields; + }, }, methods: { onSubmit() { diff --git a/app/javascript/widget/components/dropdown/DropdownMenu.vue b/app/javascript/widget/components/dropdown/DropdownMenu.vue new file mode 100644 index 000000000..1bccb7147 --- /dev/null +++ b/app/javascript/widget/components/dropdown/DropdownMenu.vue @@ -0,0 +1,83 @@ + + + + diff --git a/app/javascript/widget/components/dropdown/DropdownMenuItem.vue b/app/javascript/widget/components/dropdown/DropdownMenuItem.vue new file mode 100644 index 000000000..1ba355d98 --- /dev/null +++ b/app/javascript/widget/components/dropdown/DropdownMenuItem.vue @@ -0,0 +1,67 @@ + + + diff --git a/app/javascript/widget/helpers/actionCable.js b/app/javascript/widget/helpers/actionCable.js index c5c262c1b..84edb35ba 100644 --- a/app/javascript/widget/helpers/actionCable.js +++ b/app/javascript/widget/helpers/actionCable.js @@ -11,12 +11,16 @@ class ActionCableConnector extends BaseActionCableConnector { 'conversation.typing_on': this.onTypingOn, 'conversation.typing_off': this.onTypingOff, 'conversation.status_changed': this.onStatusChange, + 'conversation.created': this.onConversationCreated, 'presence.update': this.onPresenceUpdate, 'contact.merged': this.onContactMerge, }; } onStatusChange = data => { + if (data.status === 'resolved') { + this.app.$store.dispatch('campaign/resetCampaign'); + } this.app.$store.dispatch('conversationAttributes/update', data); }; @@ -33,6 +37,10 @@ class ActionCableConnector extends BaseActionCableConnector { this.app.$store.dispatch('conversation/addOrUpdateMessage', data); }; + onConversationCreated = () => { + this.app.$store.dispatch('conversationAttributes/getAttributes'); + }; + onPresenceUpdate = data => { this.app.$store.dispatch('agent/updatePresence', data.users); }; diff --git a/app/javascript/widget/i18n/locale/ar.json b/app/javascript/widget/i18n/locale/ar.json index 5671c001e..6a06f6c15 100644 --- a/app/javascript/widget/i18n/locale/ar.json +++ b/app/javascript/widget/i18n/locale/ar.json @@ -22,6 +22,7 @@ "IN_A_DAY": "عادة نقوم بالرد خلال يوم واحد" }, "START_CONVERSATION": "ابدأ المحادثة", + "END_CONVERSATION": "إنهاء المحادثة", "CONTINUE_CONVERSATION": "متابعة المحادثة", "START_NEW_CONVERSATION": "ابدأ محادثة جديدة", "UNREAD_VIEW": { @@ -66,7 +67,7 @@ }, "CSAT": { "TITLE": "قيم محادثتك", - "SUBMITTED_TITLE": "شكرا لك على تقديم التقييم", + "SUBMITTED_TITLE": "شكرا لك على تقييم المحادثة", "PLACEHOLDER": "أخبرنا المزيد..." }, "EMAIL_TRANSCRIPT": { diff --git a/app/javascript/widget/i18n/locale/bg.json b/app/javascript/widget/i18n/locale/bg.json index c101f2755..f11042192 100644 --- a/app/javascript/widget/i18n/locale/bg.json +++ b/app/javascript/widget/i18n/locale/bg.json @@ -22,6 +22,7 @@ "IN_A_DAY": "Обикновено отговаряме до един ден" }, "START_CONVERSATION": "Започнете разговор", + "END_CONVERSATION": "End Conversation", "CONTINUE_CONVERSATION": "Continue conversation", "START_NEW_CONVERSATION": "Започнете нов разговор", "UNREAD_VIEW": { diff --git a/app/javascript/widget/i18n/locale/ca.json b/app/javascript/widget/i18n/locale/ca.json index dc8db24dd..e0a2c1e64 100644 --- a/app/javascript/widget/i18n/locale/ca.json +++ b/app/javascript/widget/i18n/locale/ca.json @@ -22,6 +22,7 @@ "IN_A_DAY": "Normalment respon en un dia" }, "START_CONVERSATION": "Inicia la conversa", + "END_CONVERSATION": "End Conversation", "CONTINUE_CONVERSATION": "Continue conversation", "START_NEW_CONVERSATION": "Start a new conversation", "UNREAD_VIEW": { diff --git a/app/javascript/widget/i18n/locale/cs.json b/app/javascript/widget/i18n/locale/cs.json index 110b83e4a..52c1b060c 100644 --- a/app/javascript/widget/i18n/locale/cs.json +++ b/app/javascript/widget/i18n/locale/cs.json @@ -22,6 +22,7 @@ "IN_A_DAY": "Obvykle odpoví za den" }, "START_CONVERSATION": "Zahájit konverzaci", + "END_CONVERSATION": "End Conversation", "CONTINUE_CONVERSATION": "Continue conversation", "START_NEW_CONVERSATION": "Zahájit novou konverzaci", "UNREAD_VIEW": { diff --git a/app/javascript/widget/i18n/locale/da.json b/app/javascript/widget/i18n/locale/da.json index 6af84bafd..4051ffb69 100644 --- a/app/javascript/widget/i18n/locale/da.json +++ b/app/javascript/widget/i18n/locale/da.json @@ -22,6 +22,7 @@ "IN_A_DAY": "Svarer typisk på en dag" }, "START_CONVERSATION": "Start Samtale", + "END_CONVERSATION": "End Conversation", "CONTINUE_CONVERSATION": "Continue conversation", "START_NEW_CONVERSATION": "Start en ny samtale", "UNREAD_VIEW": { diff --git a/app/javascript/widget/i18n/locale/de.json b/app/javascript/widget/i18n/locale/de.json index 4466c66e6..5a3588ae1 100644 --- a/app/javascript/widget/i18n/locale/de.json +++ b/app/javascript/widget/i18n/locale/de.json @@ -22,6 +22,7 @@ "IN_A_DAY": "Wir antworten üblicherweise innerhalb eines Tages" }, "START_CONVERSATION": "Unterhaltung beginnen", + "END_CONVERSATION": "Konversation beenden", "CONTINUE_CONVERSATION": "Konversation fortsetzen", "START_NEW_CONVERSATION": "Neue Unterhaltung starten", "UNREAD_VIEW": { @@ -42,7 +43,7 @@ "FIELDS": { "FULL_NAME": { "LABEL": "Vollständiger Name", - "PLACEHOLDER": "Bitte geben Sie Ihren Namen ein, dies wird in Gesprächen angezeigt", + "PLACEHOLDER": "Bitte geben Sie Ihren vollständigen Namen ein", "ERROR": "Vollständiger Name ist erforderlich" }, "EMAIL_ADDRESS": { @@ -56,7 +57,7 @@ "ERROR": "Nachricht ist zu kurz" } }, - "CAMPAIGN_HEADER": "Bitte geben Sie Ihren Namen und Ihre E-Mail-Adresse an, bevor Sie das Gespräch beginnen" + "CAMPAIGN_HEADER": "Bitte geben Sie Ihren Namen und Ihre E-Mail-Adresse an, bevor Sie die Konversation beginnen" }, "FILE_SIZE_LIMIT": "Die Datei überschreitet das Anhangslimit von {MAXIMUM_FILE_UPLOAD_SIZE}", "CHAT_FORM": { @@ -65,9 +66,9 @@ } }, "CSAT": { - "TITLE": "Bewerte deine Unterhaltung", + "TITLE": "Bewerten Sie Ihre Konversation", "SUBMITTED_TITLE": "Danke, dass Sie die Bewertung eingereicht haben", - "PLACEHOLDER": "Erzähl uns mehr..." + "PLACEHOLDER": "Erzählen Sie uns mehr..." }, "EMAIL_TRANSCRIPT": { "BUTTON_TEXT": "Chat-Protokoll anfordern", diff --git a/app/javascript/widget/i18n/locale/el.json b/app/javascript/widget/i18n/locale/el.json index f8f0dab3f..0cca803f3 100644 --- a/app/javascript/widget/i18n/locale/el.json +++ b/app/javascript/widget/i18n/locale/el.json @@ -22,6 +22,7 @@ "IN_A_DAY": "Τυπικά έχετε απάντηση σε μία ημέρα" }, "START_CONVERSATION": "Έναρξη Συνομιλίας", + "END_CONVERSATION": "Τέλος Συνομιλίας", "CONTINUE_CONVERSATION": "Συνέχιση συνομιλίας", "START_NEW_CONVERSATION": "Έναρξη νέας συνομιλίας", "UNREAD_VIEW": { diff --git a/app/javascript/widget/i18n/locale/en.json b/app/javascript/widget/i18n/locale/en.json index 7718ea4d1..66bf83fe5 100644 --- a/app/javascript/widget/i18n/locale/en.json +++ b/app/javascript/widget/i18n/locale/en.json @@ -22,6 +22,7 @@ "IN_A_DAY": "Typically replies in a day" }, "START_CONVERSATION": "Start Conversation", + "END_CONVERSATION": "End Conversation", "CONTINUE_CONVERSATION": "Continue conversation", "START_NEW_CONVERSATION": "Start a new conversation", "UNREAD_VIEW": { diff --git a/app/javascript/widget/i18n/locale/es.json b/app/javascript/widget/i18n/locale/es.json index 0ca88c798..5889171f9 100644 --- a/app/javascript/widget/i18n/locale/es.json +++ b/app/javascript/widget/i18n/locale/es.json @@ -22,7 +22,8 @@ "IN_A_DAY": "Normalmente responde en un día" }, "START_CONVERSATION": "Iniciar conversación", - "CONTINUE_CONVERSATION": "Continue conversation", + "END_CONVERSATION": "End Conversation", + "CONTINUE_CONVERSATION": "Continuar conversación", "START_NEW_CONVERSATION": "Iniciar una nueva conversación", "UNREAD_VIEW": { "VIEW_MESSAGES_BUTTON": "Ver nuevos mensajes", diff --git a/app/javascript/widget/i18n/locale/fa.json b/app/javascript/widget/i18n/locale/fa.json index 1265cdb22..603c9e10f 100644 --- a/app/javascript/widget/i18n/locale/fa.json +++ b/app/javascript/widget/i18n/locale/fa.json @@ -22,6 +22,7 @@ "IN_A_DAY": "به طور معمول در یک روز پاسخ می دهند" }, "START_CONVERSATION": "شروع گفتگو", + "END_CONVERSATION": "پایان گفتگو", "CONTINUE_CONVERSATION": "ادامه گفتگو", "START_NEW_CONVERSATION": "یک مکالمه جدید را شروع کنید", "UNREAD_VIEW": { diff --git a/app/javascript/widget/i18n/locale/fi.json b/app/javascript/widget/i18n/locale/fi.json index e399a27ca..9bf988f04 100644 --- a/app/javascript/widget/i18n/locale/fi.json +++ b/app/javascript/widget/i18n/locale/fi.json @@ -22,6 +22,7 @@ "IN_A_DAY": "Vastaa tyypillisesti päivässä" }, "START_CONVERSATION": "Aloita keskustelu", + "END_CONVERSATION": "End Conversation", "CONTINUE_CONVERSATION": "Continue conversation", "START_NEW_CONVERSATION": "Start a new conversation", "UNREAD_VIEW": { diff --git a/app/javascript/widget/i18n/locale/fr.json b/app/javascript/widget/i18n/locale/fr.json index d151513ee..8df1a14d9 100644 --- a/app/javascript/widget/i18n/locale/fr.json +++ b/app/javascript/widget/i18n/locale/fr.json @@ -22,6 +22,7 @@ "IN_A_DAY": "Répond généralement dans la journée" }, "START_CONVERSATION": "Démarrer la conversation", + "END_CONVERSATION": "End Conversation", "CONTINUE_CONVERSATION": "Continue conversation", "START_NEW_CONVERSATION": "Démarrer une nouvelle conversation", "UNREAD_VIEW": { diff --git a/app/javascript/widget/i18n/locale/he.json b/app/javascript/widget/i18n/locale/he.json index 966583c97..72d44e5ea 100644 --- a/app/javascript/widget/i18n/locale/he.json +++ b/app/javascript/widget/i18n/locale/he.json @@ -22,6 +22,7 @@ "IN_A_DAY": "מענה ממוצע לאחר יום" }, "START_CONVERSATION": "התחל שיחה", + "END_CONVERSATION": "End Conversation", "CONTINUE_CONVERSATION": "Continue conversation", "START_NEW_CONVERSATION": "התחל שיחה חדשה", "UNREAD_VIEW": { diff --git a/app/javascript/widget/i18n/locale/hi.json b/app/javascript/widget/i18n/locale/hi.json index 7718ea4d1..66bf83fe5 100644 --- a/app/javascript/widget/i18n/locale/hi.json +++ b/app/javascript/widget/i18n/locale/hi.json @@ -22,6 +22,7 @@ "IN_A_DAY": "Typically replies in a day" }, "START_CONVERSATION": "Start Conversation", + "END_CONVERSATION": "End Conversation", "CONTINUE_CONVERSATION": "Continue conversation", "START_NEW_CONVERSATION": "Start a new conversation", "UNREAD_VIEW": { diff --git a/app/javascript/widget/i18n/locale/hu.json b/app/javascript/widget/i18n/locale/hu.json index 523de698c..6ae189e82 100644 --- a/app/javascript/widget/i18n/locale/hu.json +++ b/app/javascript/widget/i18n/locale/hu.json @@ -22,6 +22,7 @@ "IN_A_DAY": "Általánban egy napon belül válaszol" }, "START_CONVERSATION": "Beszélgetés megkezdése", + "END_CONVERSATION": "End Conversation", "CONTINUE_CONVERSATION": "Continue conversation", "START_NEW_CONVERSATION": "Új beszélgetés megkezdése", "UNREAD_VIEW": { diff --git a/app/javascript/widget/i18n/locale/id.json b/app/javascript/widget/i18n/locale/id.json index 50fd7ef72..770737786 100644 --- a/app/javascript/widget/i18n/locale/id.json +++ b/app/javascript/widget/i18n/locale/id.json @@ -22,6 +22,7 @@ "IN_A_DAY": "Biasanya membalas dalam sehari" }, "START_CONVERSATION": "Mulai Percakapan", + "END_CONVERSATION": "End Conversation", "CONTINUE_CONVERSATION": "Continue conversation", "START_NEW_CONVERSATION": "Start a new conversation", "UNREAD_VIEW": { diff --git a/app/javascript/widget/i18n/locale/it.json b/app/javascript/widget/i18n/locale/it.json index b11718a2f..61bdcee80 100644 --- a/app/javascript/widget/i18n/locale/it.json +++ b/app/javascript/widget/i18n/locale/it.json @@ -22,6 +22,7 @@ "IN_A_DAY": "In genere risponde in un giorno" }, "START_CONVERSATION": "Avvia conversazione", + "END_CONVERSATION": "Termina conversazione", "CONTINUE_CONVERSATION": "Continua conversazione", "START_NEW_CONVERSATION": "Avvia una nuova conversazione", "UNREAD_VIEW": { @@ -43,7 +44,7 @@ "FULL_NAME": { "LABEL": "Nome completo", "PLACEHOLDER": "Per favore inserisci il tuo nome completo", - "ERROR": "Il nome completo è richiesto" + "ERROR": "Il nome completo è obbligatorio" }, "EMAIL_ADDRESS": { "LABEL": "Indirizzo email", diff --git a/app/javascript/widget/i18n/locale/ja.json b/app/javascript/widget/i18n/locale/ja.json index 219d8c267..b2677661b 100644 --- a/app/javascript/widget/i18n/locale/ja.json +++ b/app/javascript/widget/i18n/locale/ja.json @@ -22,6 +22,7 @@ "IN_A_DAY": "通常数日以内にご返信します。" }, "START_CONVERSATION": "チャットを開始する", + "END_CONVERSATION": "End Conversation", "CONTINUE_CONVERSATION": "Continue conversation", "START_NEW_CONVERSATION": "Start a new conversation", "UNREAD_VIEW": { diff --git a/app/javascript/widget/i18n/locale/ko.json b/app/javascript/widget/i18n/locale/ko.json index 8df2e8a3f..06871ff0c 100644 --- a/app/javascript/widget/i18n/locale/ko.json +++ b/app/javascript/widget/i18n/locale/ko.json @@ -22,6 +22,7 @@ "IN_A_DAY": "보통 하루 안에 응답" }, "START_CONVERSATION": "대화 시작", + "END_CONVERSATION": "End Conversation", "CONTINUE_CONVERSATION": "Continue conversation", "START_NEW_CONVERSATION": "Start a new conversation", "UNREAD_VIEW": { diff --git a/app/javascript/widget/i18n/locale/lv.json b/app/javascript/widget/i18n/locale/lv.json index 7718ea4d1..66bf83fe5 100644 --- a/app/javascript/widget/i18n/locale/lv.json +++ b/app/javascript/widget/i18n/locale/lv.json @@ -22,6 +22,7 @@ "IN_A_DAY": "Typically replies in a day" }, "START_CONVERSATION": "Start Conversation", + "END_CONVERSATION": "End Conversation", "CONTINUE_CONVERSATION": "Continue conversation", "START_NEW_CONVERSATION": "Start a new conversation", "UNREAD_VIEW": { diff --git a/app/javascript/widget/i18n/locale/ml.json b/app/javascript/widget/i18n/locale/ml.json index e87791032..20142dbc9 100644 --- a/app/javascript/widget/i18n/locale/ml.json +++ b/app/javascript/widget/i18n/locale/ml.json @@ -22,6 +22,7 @@ "IN_A_DAY": "സാധാരണയായി ഒരു ദിവസത്തിൽ മറുപടി നൽകുന്നു" }, "START_CONVERSATION": "സംഭാഷണം ആരംഭിക്കുക", + "END_CONVERSATION": "End Conversation", "CONTINUE_CONVERSATION": "Continue conversation", "START_NEW_CONVERSATION": "ഒരു പുതിയ സംഭാഷണം ആരംഭിക്കുക", "UNREAD_VIEW": { diff --git a/app/javascript/widget/i18n/locale/ne.json b/app/javascript/widget/i18n/locale/ne.json index a6ec8f639..4c180cd1f 100644 --- a/app/javascript/widget/i18n/locale/ne.json +++ b/app/javascript/widget/i18n/locale/ne.json @@ -22,6 +22,7 @@ "IN_A_DAY": "धेरै जसो एक दिनमा जवाफ हुन्छ" }, "START_CONVERSATION": "कुराकानी सुरु गर्नुहोस्", + "END_CONVERSATION": "End Conversation", "CONTINUE_CONVERSATION": "Continue conversation", "START_NEW_CONVERSATION": "Start a new conversation", "UNREAD_VIEW": { diff --git a/app/javascript/widget/i18n/locale/nl.json b/app/javascript/widget/i18n/locale/nl.json index 878e0884b..8983ef628 100644 --- a/app/javascript/widget/i18n/locale/nl.json +++ b/app/javascript/widget/i18n/locale/nl.json @@ -22,6 +22,7 @@ "IN_A_DAY": "Reageert meestal binnen een dag" }, "START_CONVERSATION": "Start Chat", + "END_CONVERSATION": "End Conversation", "CONTINUE_CONVERSATION": "Continue conversation", "START_NEW_CONVERSATION": "Een nieuw gesprek starten", "UNREAD_VIEW": { diff --git a/app/javascript/widget/i18n/locale/no.json b/app/javascript/widget/i18n/locale/no.json index f5acf8e51..68e373318 100644 --- a/app/javascript/widget/i18n/locale/no.json +++ b/app/javascript/widget/i18n/locale/no.json @@ -22,6 +22,7 @@ "IN_A_DAY": "Svarer vanligvis innen en dag" }, "START_CONVERSATION": "Start samtale", + "END_CONVERSATION": "End Conversation", "CONTINUE_CONVERSATION": "Continue conversation", "START_NEW_CONVERSATION": "Start en ny samtale", "UNREAD_VIEW": { diff --git a/app/javascript/widget/i18n/locale/pl.json b/app/javascript/widget/i18n/locale/pl.json index 872bdd42b..17999f14a 100644 --- a/app/javascript/widget/i18n/locale/pl.json +++ b/app/javascript/widget/i18n/locale/pl.json @@ -22,6 +22,7 @@ "IN_A_DAY": "Zwykle odpowiadamy w przeciągu jednego dnia" }, "START_CONVERSATION": "Rozpocznij rozmowę", + "END_CONVERSATION": "End Conversation", "CONTINUE_CONVERSATION": "Kontynuuj rozmowę", "START_NEW_CONVERSATION": "Rozpocznij rozmowę", "UNREAD_VIEW": { diff --git a/app/javascript/widget/i18n/locale/pt.json b/app/javascript/widget/i18n/locale/pt.json index a12031986..3bf35af69 100644 --- a/app/javascript/widget/i18n/locale/pt.json +++ b/app/javascript/widget/i18n/locale/pt.json @@ -22,6 +22,7 @@ "IN_A_DAY": "Normalmente respondemos num dia" }, "START_CONVERSATION": "Iniciar Conversa", + "END_CONVERSATION": "Terminar Conversa", "CONTINUE_CONVERSATION": "Continuar conversa", "START_NEW_CONVERSATION": "Iniciar uma nova conversa", "UNREAD_VIEW": { diff --git a/app/javascript/widget/i18n/locale/pt_BR.json b/app/javascript/widget/i18n/locale/pt_BR.json index 165055552..2e8aaed8d 100644 --- a/app/javascript/widget/i18n/locale/pt_BR.json +++ b/app/javascript/widget/i18n/locale/pt_BR.json @@ -22,6 +22,7 @@ "IN_A_DAY": "Responde normalmente em um dia" }, "START_CONVERSATION": "Iniciar Conversa", + "END_CONVERSATION": "End Conversation", "CONTINUE_CONVERSATION": "Continuar conversa", "START_NEW_CONVERSATION": "Iniciar uma nova conversa", "UNREAD_VIEW": { diff --git a/app/javascript/widget/i18n/locale/ro.json b/app/javascript/widget/i18n/locale/ro.json index 1516e2640..8d9be551d 100644 --- a/app/javascript/widget/i18n/locale/ro.json +++ b/app/javascript/widget/i18n/locale/ro.json @@ -22,6 +22,7 @@ "IN_A_DAY": "Typically replies in a day" }, "START_CONVERSATION": "Start Conversation", + "END_CONVERSATION": "End Conversation", "CONTINUE_CONVERSATION": "Continue conversation", "START_NEW_CONVERSATION": "Start a new conversation", "UNREAD_VIEW": { diff --git a/app/javascript/widget/i18n/locale/ru.json b/app/javascript/widget/i18n/locale/ru.json index 149e81812..ca466f4be 100644 --- a/app/javascript/widget/i18n/locale/ru.json +++ b/app/javascript/widget/i18n/locale/ru.json @@ -22,6 +22,7 @@ "IN_A_DAY": "Обычно отвечаем в течение дня" }, "START_CONVERSATION": "Начать диалог", + "END_CONVERSATION": "Завершить диалог", "CONTINUE_CONVERSATION": "Продолжить беседу", "START_NEW_CONVERSATION": "Начать диалог", "UNREAD_VIEW": { diff --git a/app/javascript/widget/i18n/locale/sk.json b/app/javascript/widget/i18n/locale/sk.json index 7d9057efd..71138fcd2 100644 --- a/app/javascript/widget/i18n/locale/sk.json +++ b/app/javascript/widget/i18n/locale/sk.json @@ -22,6 +22,7 @@ "IN_A_DAY": "Zvyčajne odpovedá do dňa" }, "START_CONVERSATION": "Začať konverzáciu", + "END_CONVERSATION": "End Conversation", "CONTINUE_CONVERSATION": "Continue conversation", "START_NEW_CONVERSATION": "Začať novú konverzáciu", "UNREAD_VIEW": { diff --git a/app/javascript/widget/i18n/locale/sr.json b/app/javascript/widget/i18n/locale/sr.json new file mode 100644 index 000000000..d867e526c --- /dev/null +++ b/app/javascript/widget/i18n/locale/sr.json @@ -0,0 +1,78 @@ +{ + "COMPONENTS": { + "FILE_BUBBLE": { + "DOWNLOAD": "Preuzimanje", + "UPLOADING": "Dodavanje..." + }, + "FORM_BUBBLE": { + "SUBMIT": "Pošalji" + }, + "MESSAGE_BUBBLE": { + "RETRY": "Ponovo pošalji poruku", + "ERROR_MESSAGE": "Slanje neuspešno, pokušajte ponovo" + } + }, + "TEAM_AVAILABILITY": { + "ONLINE": "Dostupni smo", + "OFFLINE": "Trenutno nismo dostupni" + }, + "REPLY_TIME": { + "IN_A_FEW_MINUTES": "Obično odgovaramo u roku od nekoliko minuta", + "IN_A_FEW_HOURS": "Obično odgovaramo u roku od nekoliko sati", + "IN_A_DAY": "Obično odgovaramo u roku od jednog dana" + }, + "START_CONVERSATION": "Započnite razgovor", + "END_CONVERSATION": "End Conversation", + "CONTINUE_CONVERSATION": "Nastavi razgovor", + "START_NEW_CONVERSATION": "Započnite novi razgovor", + "UNREAD_VIEW": { + "VIEW_MESSAGES_BUTTON": "Pogledajte nove poruke", + "CLOSE_MESSAGES_BUTTON": "Zatvori", + "COMPANY_FROM": "od", + "BOT": "Bot" + }, + "BUBBLE": { + "LABEL": "Razgovarajte sa nama" + }, + "POWERED_BY": "Pokreće Chatwoot", + "EMAIL_PLACEHOLDER": "Molimo unesite vašu e-mail adresu", + "CHAT_PLACEHOLDER": "Napišite vašu poruku", + "TODAY": "Danas", + "YESTERDAY": "Juče", + "PRE_CHAT_FORM": { + "FIELDS": { + "FULL_NAME": { + "LABEL": "Puno ime", + "PLACEHOLDER": "Molimo unesite vaše puno ime", + "ERROR": "Puno ime je obavezno" + }, + "EMAIL_ADDRESS": { + "LABEL": "E-mail adresa", + "PLACEHOLDER": "Molimo unesite vašu e-mail adresu", + "ERROR": "Neispravna e-mail adresa" + }, + "MESSAGE": { + "LABEL": "Poruka", + "PLACEHOLDER": "Molimo unesite vašu poruku", + "ERROR": "Poruka prekratka" + } + }, + "CAMPAIGN_HEADER": "Please provide your name and email before starting the conversation" + }, + "FILE_SIZE_LIMIT": "File exceeds the {MAXIMUM_FILE_UPLOAD_SIZE} attachment limit", + "CHAT_FORM": { + "INVALID": { + "FIELD": "Invalid field" + } + }, + "CSAT": { + "TITLE": "Ocenite razgovor", + "SUBMITTED_TITLE": "Thank you for submitting the rating", + "PLACEHOLDER": "Pričajte nam više..." + }, + "EMAIL_TRANSCRIPT": { + "BUTTON_TEXT": "Zatražite kopiju razgovora", + "SEND_EMAIL_SUCCESS": "The chat transcript was sent successfully", + "SEND_EMAIL_ERROR": "There was an error, please try again" + } +} diff --git a/app/javascript/widget/i18n/locale/sv.json b/app/javascript/widget/i18n/locale/sv.json index 89d9b7404..9dc83348c 100644 --- a/app/javascript/widget/i18n/locale/sv.json +++ b/app/javascript/widget/i18n/locale/sv.json @@ -22,6 +22,7 @@ "IN_A_DAY": "Svar vanligtvis inom en dag" }, "START_CONVERSATION": "Starta konversation", + "END_CONVERSATION": "End Conversation", "CONTINUE_CONVERSATION": "Fortsätt konversation", "START_NEW_CONVERSATION": "Starta konversation", "UNREAD_VIEW": { diff --git a/app/javascript/widget/i18n/locale/ta.json b/app/javascript/widget/i18n/locale/ta.json index b9c3dc237..57ede7387 100644 --- a/app/javascript/widget/i18n/locale/ta.json +++ b/app/javascript/widget/i18n/locale/ta.json @@ -22,6 +22,7 @@ "IN_A_DAY": "Typically replies in a day" }, "START_CONVERSATION": "Start Conversation", + "END_CONVERSATION": "End Conversation", "CONTINUE_CONVERSATION": "Continue conversation", "START_NEW_CONVERSATION": "Start a new conversation", "UNREAD_VIEW": { diff --git a/app/javascript/widget/i18n/locale/th.json b/app/javascript/widget/i18n/locale/th.json index bd6bf1210..908920e05 100644 --- a/app/javascript/widget/i18n/locale/th.json +++ b/app/javascript/widget/i18n/locale/th.json @@ -22,6 +22,7 @@ "IN_A_DAY": "Typically replies in a day" }, "START_CONVERSATION": "Start Conversation", + "END_CONVERSATION": "End Conversation", "CONTINUE_CONVERSATION": "Continue conversation", "START_NEW_CONVERSATION": "Start a new conversation", "UNREAD_VIEW": { diff --git a/app/javascript/widget/i18n/locale/tr.json b/app/javascript/widget/i18n/locale/tr.json index 937b4a673..b1ebc81b0 100644 --- a/app/javascript/widget/i18n/locale/tr.json +++ b/app/javascript/widget/i18n/locale/tr.json @@ -8,13 +8,13 @@ "SUBMIT": "Yolla" }, "MESSAGE_BUBBLE": { - "RETRY": "Send message again", - "ERROR_MESSAGE": "Couldn't send, try again" + "RETRY": "Mesajı tekrar gönder", + "ERROR_MESSAGE": "Gönderilemedi, tekrar deneyin" } }, "TEAM_AVAILABILITY": { "ONLINE": "Çevrimiçi", - "OFFLINE": "Şuan operatörlerimiz müsait değil" + "OFFLINE": "Şu an operatörlerimiz müsait değil" }, "REPLY_TIME": { "IN_A_FEW_MINUTES": "Genellikle birkaç dakika içinde yanıt verir", @@ -22,8 +22,9 @@ "IN_A_DAY": "Genellikle bir gün içinde yanıtlar" }, "START_CONVERSATION": "Görüşmeyi Başlatın", - "CONTINUE_CONVERSATION": "Continue conversation", - "START_NEW_CONVERSATION": "Görüşmeyi Başlatın", + "END_CONVERSATION": "Görüşmeyi Sonlandır", + "CONTINUE_CONVERSATION": "Görüşmeye devam et", + "START_NEW_CONVERSATION": "Yeni Görüşme Başlatın", "UNREAD_VIEW": { "VIEW_MESSAGES_BUTTON": "Yeni mesajları gör", "CLOSE_MESSAGES_BUTTON": "Kapat", @@ -56,7 +57,7 @@ "ERROR": "Mesaj çok kısa" } }, - "CAMPAIGN_HEADER": "Please provide your name and email before starting the conversation" + "CAMPAIGN_HEADER": "Görüşmeye başlamadan önce lütfen isminizi ve eposta adresinizi belirtiniz" }, "FILE_SIZE_LIMIT": "Dosya {MAXIMUM_FILE_UPLOAD_SIZE} sınırını aşıyor", "CHAT_FORM": { diff --git a/app/javascript/widget/i18n/locale/uk.json b/app/javascript/widget/i18n/locale/uk.json index 0ccde5fee..58e0d7329 100644 --- a/app/javascript/widget/i18n/locale/uk.json +++ b/app/javascript/widget/i18n/locale/uk.json @@ -22,6 +22,7 @@ "IN_A_DAY": "Зазвичай, відповідаємо протягом доби" }, "START_CONVERSATION": "Розпочати розмову", + "END_CONVERSATION": "End Conversation", "CONTINUE_CONVERSATION": "Continue conversation", "START_NEW_CONVERSATION": "Розпочати нову розмову", "UNREAD_VIEW": { diff --git a/app/javascript/widget/i18n/locale/vi.json b/app/javascript/widget/i18n/locale/vi.json index 8481b1786..842eaf12f 100644 --- a/app/javascript/widget/i18n/locale/vi.json +++ b/app/javascript/widget/i18n/locale/vi.json @@ -22,6 +22,7 @@ "IN_A_DAY": "Thường trả lời trong một ngày" }, "START_CONVERSATION": "Bắt đầu một cuộc trò chuyện", + "END_CONVERSATION": "End Conversation", "CONTINUE_CONVERSATION": "Tiếp tục cuộc trò chuyện", "START_NEW_CONVERSATION": "Bắt đầu cuộc trò chuyện mới", "UNREAD_VIEW": { diff --git a/app/javascript/widget/i18n/locale/zh_CN.json b/app/javascript/widget/i18n/locale/zh_CN.json index cce5f2908..dc2319c5c 100644 --- a/app/javascript/widget/i18n/locale/zh_CN.json +++ b/app/javascript/widget/i18n/locale/zh_CN.json @@ -8,8 +8,8 @@ "SUBMIT": "提交" }, "MESSAGE_BUBBLE": { - "RETRY": "Send message again", - "ERROR_MESSAGE": "Couldn't send, try again" + "RETRY": "重新发送消息", + "ERROR_MESSAGE": "无法发送,请重试" } }, "TEAM_AVAILABILITY": { @@ -22,8 +22,9 @@ "IN_A_DAY": "通常在一天之内回复您" }, "START_CONVERSATION": "开始会话", - "CONTINUE_CONVERSATION": "Continue conversation", - "START_NEW_CONVERSATION": "Start a new conversation", + "END_CONVERSATION": "End Conversation", + "CONTINUE_CONVERSATION": "继续对话", + "START_NEW_CONVERSATION": "开始新的对话", "UNREAD_VIEW": { "VIEW_MESSAGES_BUTTON": "查看新消息", "CLOSE_MESSAGES_BUTTON": "关闭", @@ -56,7 +57,7 @@ "ERROR": "消息太短了" } }, - "CAMPAIGN_HEADER": "Please provide your name and email before starting the conversation" + "CAMPAIGN_HEADER": "请在开始对话之前提供您的姓名和电子邮件" }, "FILE_SIZE_LIMIT": "文件超过大小 {MAXIMUM_FILE_UPLOAD_SIZE} 附件限制", "CHAT_FORM": { @@ -66,12 +67,12 @@ }, "CSAT": { "TITLE": "评价您的对话", - "SUBMITTED_TITLE": "Thank you for submitting the rating", + "SUBMITTED_TITLE": "感谢您提交评分", "PLACEHOLDER": "请告诉我们更多..." }, "EMAIL_TRANSCRIPT": { - "BUTTON_TEXT": "Request a conversation transcript", - "SEND_EMAIL_SUCCESS": "The chat transcript was sent successfully", + "BUTTON_TEXT": "请求会话抄本", + "SEND_EMAIL_SUCCESS": "已成功发送聊天记录", "SEND_EMAIL_ERROR": "出现错误,请重试" } } diff --git a/app/javascript/widget/i18n/locale/zh_TW.json b/app/javascript/widget/i18n/locale/zh_TW.json index bd767c301..020c5a21d 100644 --- a/app/javascript/widget/i18n/locale/zh_TW.json +++ b/app/javascript/widget/i18n/locale/zh_TW.json @@ -8,8 +8,8 @@ "SUBMIT": "送出" }, "MESSAGE_BUBBLE": { - "RETRY": "Send message again", - "ERROR_MESSAGE": "Couldn't send, try again" + "RETRY": "重新發送訊息", + "ERROR_MESSAGE": "無法傳送!請重新嘗試。" } }, "TEAM_AVAILABILITY": { @@ -22,7 +22,8 @@ "IN_A_DAY": "通常在一天內回覆" }, "START_CONVERSATION": "開始對話", - "CONTINUE_CONVERSATION": "Continue conversation", + "END_CONVERSATION": "End Conversation", + "CONTINUE_CONVERSATION": "繼續對話", "START_NEW_CONVERSATION": "開始一個新對話", "UNREAD_VIEW": { "VIEW_MESSAGES_BUTTON": "查看新訊息", @@ -56,22 +57,22 @@ "ERROR": "訊息過短" } }, - "CAMPAIGN_HEADER": "Please provide your name and email before starting the conversation" + "CAMPAIGN_HEADER": "在開始對話之前請提供您的名字及電郵" }, - "FILE_SIZE_LIMIT": "File exceeds the {MAXIMUM_FILE_UPLOAD_SIZE} attachment limit", + "FILE_SIZE_LIMIT": "{MAXIMUM_FILE_UPLOAD_SIZE} 已超出檔案大小限制", "CHAT_FORM": { "INVALID": { "FIELD": "無效的欄位" } }, "CSAT": { - "TITLE": "Rate your conversation", - "SUBMITTED_TITLE": "Thank you for submitting the rating", + "TITLE": "為此對話評分", + "SUBMITTED_TITLE": "感謝您提交的評分", "PLACEHOLDER": "告訴我們更多..." }, "EMAIL_TRANSCRIPT": { - "BUTTON_TEXT": "Request a conversation transcript", - "SEND_EMAIL_SUCCESS": "The chat transcript was sent successfully", + "BUTTON_TEXT": "請求對話記錄", + "SEND_EMAIL_SUCCESS": "對話記錄已成功發送", "SEND_EMAIL_ERROR": "出現錯誤,請重試" } } diff --git a/app/javascript/widget/mixins/routerMixin.js b/app/javascript/widget/mixins/routerMixin.js index f75cc65e1..b3b37f6fd 100644 --- a/app/javascript/widget/mixins/routerMixin.js +++ b/app/javascript/widget/mixins/routerMixin.js @@ -1,8 +1,8 @@ export default { methods: { - async replaceRoute(name) { + async replaceRoute(name, params = {}) { if (this.$route.name !== name) { - return this.$router.replace({ name }); + return this.$router.replace({ name, params }); } return undefined; }, diff --git a/app/javascript/widget/store/modules/campaign.js b/app/javascript/widget/store/modules/campaign.js index 673f52567..381364543 100644 --- a/app/javascript/widget/store/modules/campaign.js +++ b/app/javascript/widget/store/modules/campaign.js @@ -100,6 +100,7 @@ export const actions = { { root: true } ); await triggerCampaign({ campaignId, websiteToken }); + commit('setCampaignExecuted', true); commit('setActiveCampaign', {}); } catch (error) { commit('setError', true); @@ -113,6 +114,7 @@ export const actions = { }, resetCampaign: async ({ commit }) => { try { + commit('setCampaignExecuted', false); commit('setActiveCampaign', {}); } catch (error) { commit('setError', true); @@ -130,6 +132,12 @@ export const mutations = { setError($state, value) { Vue.set($state.uiFlags, 'isError', value); }, + setHasFetched($state, value) { + Vue.set($state.uiFlags, 'hasFetched', value); + }, + setCampaignExecuted($state, data) { + Vue.set($state, 'campaignHasExecuted', data); + }, }; export default { diff --git a/app/javascript/widget/store/modules/contacts.js b/app/javascript/widget/store/modules/contacts.js index 79fb36de6..ef146ce72 100644 --- a/app/javascript/widget/store/modules/contacts.js +++ b/app/javascript/widget/store/modules/contacts.js @@ -24,17 +24,38 @@ export const actions = { }, update: async ({ dispatch }, { identifier, user: userObject }) => { try { + const { + email, + name, + avatar_url, + identifier_hash, + phone_number, + company_name, + city, + country_code, + description, + custom_attributes, + social_profiles, + } = userObject; const user = { - email: userObject.email, - name: userObject.name, - avatar_url: userObject.avatar_url, - identifier_hash: userObject.identifier_hash, - phone_number: userObject.phone_number, + email, + name, + avatar_url, + identifier_hash, + phone_number, + additional_attributes: { + company_name, + city, + description, + country_code, + social_profiles, + }, + custom_attributes, }; await ContactsAPI.update(identifier, user); dispatch('get'); - if (userObject.identifier_hash) { + if (identifier_hash) { dispatch('conversation/clearConversations', {}, { root: true }); dispatch('conversation/fetchOldConversations', {}, { root: true }); } diff --git a/app/javascript/widget/store/modules/conversation/actions.js b/app/javascript/widget/store/modules/conversation/actions.js index 2ec3560a7..c915ea6e7 100644 --- a/app/javascript/widget/store/modules/conversation/actions.js +++ b/app/javascript/widget/store/modules/conversation/actions.js @@ -5,6 +5,7 @@ import { sendAttachmentAPI, toggleTyping, setUserLastSeenAt, + toggleStatus, } from 'widget/api/conversation'; import { createTemporaryMessage, getNonDeletedMessages } from './helpers'; @@ -130,4 +131,8 @@ export const actions = { // IgnoreError } }, + + resolveConversation: async () => { + await toggleStatus(); + }, }; diff --git a/app/javascript/widget/store/modules/specs/campaign/actions.spec.js b/app/javascript/widget/store/modules/specs/campaign/actions.spec.js index cdd3bf12c..303149daa 100644 --- a/app/javascript/widget/store/modules/specs/campaign/actions.spec.js +++ b/app/javascript/widget/store/modules/specs/campaign/actions.spec.js @@ -132,6 +132,7 @@ describe('#actions', () => { root: true, }, ], + ['setCampaignExecuted', true], ['setActiveCampaign', {}], [ 'conversation/setConversationUIFlag', @@ -176,7 +177,10 @@ describe('#actions', () => { it('sends correct actions if execute campaign API is success', async () => { API.post.mockResolvedValue({}); await actions.resetCampaign({ commit }); - expect(commit.mock.calls).toEqual([['setActiveCampaign', {}]]); + expect(commit.mock.calls).toEqual([ + ['setCampaignExecuted', false], + ['setActiveCampaign', {}], + ]); }); }); }); diff --git a/app/javascript/widget/store/modules/specs/campaign/mutations.spec.js b/app/javascript/widget/store/modules/specs/campaign/mutations.spec.js index b35d0df15..98a723a8b 100644 --- a/app/javascript/widget/store/modules/specs/campaign/mutations.spec.js +++ b/app/javascript/widget/store/modules/specs/campaign/mutations.spec.js @@ -25,4 +25,12 @@ describe('#mutations', () => { expect(state.activeCampaign).toEqual(campaigns[0]); }); }); + + describe('#setCampaignExecuted', () => { + it('set campaign executed flag', () => { + const state = { records: [], uiFlags: {}, campaignHasExecuted: false }; + mutations.setCampaignExecuted(state, true); + expect(state.campaignHasExecuted).toEqual(true); + }); + }); }); diff --git a/app/javascript/widget/views/Home.vue b/app/javascript/widget/views/Home.vue index 32aae26f1..0fbfa094f 100755 --- a/app/javascript/widget/views/Home.vue +++ b/app/javascript/widget/views/Home.vue @@ -15,7 +15,6 @@ import configMixin from '../mixins/configMixin'; import TeamAvailability from 'widget/components/TeamAvailability'; import { mapGetters } from 'vuex'; -import { BUS_EVENTS } from 'shared/constants/busEvents'; import routerMixin from 'widget/mixins/routerMixin'; export default { name: 'Home', @@ -34,28 +33,23 @@ export default { }, }, data() { - return { - isOnCollapsedView: false, - isOnNewConversation: false, - }; + return {}; }, computed: { ...mapGetters({ availableAgents: 'agent/availableAgents', activeCampaign: 'campaign/getActiveCampaign', conversationSize: 'conversation/getConversationSize', + currentUser: 'contacts/getCurrentUser', }), }, - mounted() { - bus.$on(BUS_EVENTS.START_NEW_CONVERSATION, () => { - this.isOnCollapsedView = true; - this.isOnNewConversation = true; - }); - }, methods: { startConversation() { + const isUserEmailAvailable = !!this.currentUser.email; if (this.preChatFormEnabled && !this.conversationSize) { - return this.replaceRoute('prechat-form'); + return this.replaceRoute('prechat-form', { + disableContactFields: isUserEmailAvailable, + }); } return this.replaceRoute('messages'); }, diff --git a/app/javascript/widget/views/PreChatForm.vue b/app/javascript/widget/views/PreChatForm.vue index ac70e4e56..cb0b4c217 100644 --- a/app/javascript/widget/views/PreChatForm.vue +++ b/app/javascript/widget/views/PreChatForm.vue @@ -1,6 +1,10 @@