diff --git a/.env.example b/.env.example index 281cd64d1..5a6ab50ba 100644 --- a/.env.example +++ b/.env.example @@ -22,6 +22,11 @@ REDIS_URL=redis://redis:6379 # which will be the password for the redis service running inside the docker-compose # to make it secure REDIS_PASSWORD= +# Redis Sentinel can be used by passing list of sentinel host and ports e,g. sentinel_host1:port1,sentinel_host2:port2 +REDIS_SENTINELS= +# Redis sentinel master name is required when using sentinel, default value is "mymaster". +# You can find list of master using "SENTINEL masters" command +REDIS_SENTINEL_MASTER_NAME= # Postgres Database config variables POSTGRES_HOST=postgres diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 66e142ad6..d9409af96 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -26,6 +26,14 @@ A clear and concise description of what you expected to happen. If applicable, add screenshots to help explain your problem. +**Browser logs** + +Share the browser logs to debug the issue further + +**Server logs** + +Share the server logs to debug the issue further + **Environment** Describe whether you are using Chatwoot Cloud (app.chatwoot.com) or a self hosted installation of Chatwoot. If you are using a self hosted installation of Chatwoot describe the type of deployment (Docker/Linux VM installation/Heroku) diff --git a/.gitignore b/.gitignore index 3bf2fdbb1..146ae509f 100644 --- a/.gitignore +++ b/.gitignore @@ -23,7 +23,7 @@ *.log # Ignore application configuration node_modules - +master.key *.rdb # Ignore env files diff --git a/Gemfile.lock b/Gemfile.lock index 0c9cda2ec..a616cdf68 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -18,56 +18,56 @@ GEM specs: action-cable-testing (0.6.1) actioncable (>= 5.0) - actioncable (6.0.3.3) - actionpack (= 6.0.3.3) + actioncable (6.0.3.4) + actionpack (= 6.0.3.4) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (6.0.3.3) - actionpack (= 6.0.3.3) - activejob (= 6.0.3.3) - activerecord (= 6.0.3.3) - activestorage (= 6.0.3.3) - activesupport (= 6.0.3.3) + actionmailbox (6.0.3.4) + actionpack (= 6.0.3.4) + activejob (= 6.0.3.4) + activerecord (= 6.0.3.4) + activestorage (= 6.0.3.4) + activesupport (= 6.0.3.4) mail (>= 2.7.1) - actionmailer (6.0.3.3) - actionpack (= 6.0.3.3) - actionview (= 6.0.3.3) - activejob (= 6.0.3.3) + actionmailer (6.0.3.4) + actionpack (= 6.0.3.4) + actionview (= 6.0.3.4) + activejob (= 6.0.3.4) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 2.0) - actionpack (6.0.3.3) - actionview (= 6.0.3.3) - activesupport (= 6.0.3.3) + actionpack (6.0.3.4) + actionview (= 6.0.3.4) + activesupport (= 6.0.3.4) rack (~> 2.0, >= 2.0.8) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (6.0.3.3) - actionpack (= 6.0.3.3) - activerecord (= 6.0.3.3) - activestorage (= 6.0.3.3) - activesupport (= 6.0.3.3) + actiontext (6.0.3.4) + actionpack (= 6.0.3.4) + activerecord (= 6.0.3.4) + activestorage (= 6.0.3.4) + activesupport (= 6.0.3.4) nokogiri (>= 1.8.5) - actionview (6.0.3.3) - activesupport (= 6.0.3.3) + actionview (6.0.3.4) + activesupport (= 6.0.3.4) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.1, >= 1.2.0) - activejob (6.0.3.3) - activesupport (= 6.0.3.3) + activejob (6.0.3.4) + activesupport (= 6.0.3.4) globalid (>= 0.3.6) - activemodel (6.0.3.3) - activesupport (= 6.0.3.3) - activerecord (6.0.3.3) - activemodel (= 6.0.3.3) - activesupport (= 6.0.3.3) - activestorage (6.0.3.3) - actionpack (= 6.0.3.3) - activejob (= 6.0.3.3) - activerecord (= 6.0.3.3) + activemodel (6.0.3.4) + activesupport (= 6.0.3.4) + activerecord (6.0.3.4) + activemodel (= 6.0.3.4) + activesupport (= 6.0.3.4) + activestorage (6.0.3.4) + actionpack (= 6.0.3.4) + activejob (= 6.0.3.4) + activerecord (= 6.0.3.4) marcel (~> 0.3.1) - activesupport (6.0.3.3) + activesupport (6.0.3.4) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 0.7, < 2) minitest (~> 5.1) @@ -307,7 +307,7 @@ GEM multi_xml (0.6.0) multipart-post (2.1.1) netrc (0.11.0) - nio4r (2.5.3) + nio4r (2.5.4) nokogiri (1.10.10) mini_portile2 (~> 2.4.0) oauth (0.5.4) @@ -336,29 +336,29 @@ GEM rack rack-test (1.1.0) rack (>= 1.0, < 3) - rails (6.0.3.3) - actioncable (= 6.0.3.3) - actionmailbox (= 6.0.3.3) - actionmailer (= 6.0.3.3) - actionpack (= 6.0.3.3) - actiontext (= 6.0.3.3) - actionview (= 6.0.3.3) - activejob (= 6.0.3.3) - activemodel (= 6.0.3.3) - activerecord (= 6.0.3.3) - activestorage (= 6.0.3.3) - activesupport (= 6.0.3.3) + rails (6.0.3.4) + actioncable (= 6.0.3.4) + actionmailbox (= 6.0.3.4) + actionmailer (= 6.0.3.4) + actionpack (= 6.0.3.4) + actiontext (= 6.0.3.4) + actionview (= 6.0.3.4) + activejob (= 6.0.3.4) + activemodel (= 6.0.3.4) + activerecord (= 6.0.3.4) + activestorage (= 6.0.3.4) + activesupport (= 6.0.3.4) bundler (>= 1.3.0) - railties (= 6.0.3.3) + railties (= 6.0.3.4) sprockets-rails (>= 2.0.0) rails-dom-testing (2.0.3) activesupport (>= 4.2.0) nokogiri (>= 1.6) rails-html-sanitizer (1.3.0) loofah (~> 2.3) - railties (6.0.3.3) - actionpack (= 6.0.3.3) - activesupport (= 6.0.3.3) + railties (6.0.3.4) + actionpack (= 6.0.3.4) + activesupport (= 6.0.3.4) method_source rake (>= 0.8.7) thor (>= 0.20.3, < 2.0) @@ -481,7 +481,7 @@ GEM sprockets (3.7.2) concurrent-ruby (~> 1.0) rack (> 1, < 3) - sprockets-rails (3.2.1) + sprockets-rails (3.2.2) actionpack (>= 4.0) activesupport (>= 4.0) sprockets (>= 3.0.0) diff --git a/app/builders/messages/facebook/message_builder.rb b/app/builders/messages/facebook/message_builder.rb index 40b0aebc2..447fe3cec 100644 --- a/app/builders/messages/facebook/message_builder.rb +++ b/app/builders/messages/facebook/message_builder.rb @@ -50,8 +50,8 @@ class Messages::Facebook::MessageBuilder def attach_file(attachment, file_url) file_resource = LocalResource.new(file_url) - attachment.file.attach(io: file_resource.file, filename: file_resource.tmp_filename, content_type: file_resource.encoding) - rescue Errno::ETIMEDOUT, Errno::ECONNREFUSED, SocketError => e + attachment.file.attach(io: file_resource.file, filename: file_resource.filename, content_type: file_resource.encoding) + rescue *ExceptionList::URI_EXCEPTIONS => e Rails.logger.info "invalid url #{file_url} : #{e.message}" end diff --git a/app/controllers/api/v1/accounts/base_controller.rb b/app/controllers/api/v1/accounts/base_controller.rb index 01a0746b7..d0ae4c31d 100644 --- a/app/controllers/api/v1/accounts/base_controller.rb +++ b/app/controllers/api/v1/accounts/base_controller.rb @@ -15,7 +15,6 @@ class Api::V1::Accounts::BaseController < Api::BaseController elsif @resource&.is_a?(AgentBot) account_accessible_for_bot?(account) end - switch_locale account account end diff --git a/app/controllers/api/v1/accounts/callbacks_controller.rb b/app/controllers/api/v1/accounts/callbacks_controller.rb index 37116d8d0..9a4cd85e8 100644 --- a/app/controllers/api/v1/accounts/callbacks_controller.rb +++ b/app/controllers/api/v1/accounts/callbacks_controller.rb @@ -30,7 +30,7 @@ class Api::V1::Accounts::CallbacksController < Api::V1::Accounts::BaseController if (page_detail = (page_details || []).detect { |page| fb_page_id == page['id'] }) update_fb_page(fb_page_id, page_detail['access_token']) - return head :ok + render and return end end @@ -44,9 +44,9 @@ class Api::V1::Accounts::CallbacksController < Api::V1::Accounts::BaseController end def update_fb_page(fb_page_id, access_token) - get_fb_page(fb_page_id)&.update!( - user_access_token: @user_access_token, page_access_token: access_token - ) + fb_page = get_fb_page(fb_page_id) + fb_page&.update!(user_access_token: @user_access_token, page_access_token: access_token) + fb_page&.reauthorized! end def get_fb_page(fb_page_id) @@ -81,7 +81,7 @@ class Api::V1::Accounts::CallbacksController < Api::V1::Accounts::BaseController avatar_resource = LocalResource.new(uri) facebook_inbox.avatar.attach(io: avatar_resource.file, filename: avatar_resource.tmp_filename, content_type: avatar_resource.encoding) - rescue Errno::ETIMEDOUT, Errno::ECONNREFUSED, SocketError => e + rescue *ExceptionList::URI_EXCEPTIONS => e Rails.logger.info "invalid url #{file_url} : #{e.message}" end diff --git a/app/controllers/api/v1/accounts/contacts/conversations_controller.rb b/app/controllers/api/v1/accounts/contacts/conversations_controller.rb index 5c7bf77d7..8a9199b6b 100644 --- a/app/controllers/api/v1/accounts/contacts/conversations_controller.rb +++ b/app/controllers/api/v1/accounts/contacts/conversations_controller.rb @@ -1,7 +1,7 @@ class Api::V1::Accounts::Contacts::ConversationsController < Api::V1::Accounts::BaseController def index @conversations = Current.account.conversations.includes( - :assignee, :contact, :inbox + :assignee, :contact, :inbox, :taggings ).where(inbox_id: inbox_ids, contact_id: permitted_params[:contact_id]) end diff --git a/app/controllers/api/v1/accounts/contacts_controller.rb b/app/controllers/api/v1/accounts/contacts_controller.rb index 87b3898f1..a82a30414 100644 --- a/app/controllers/api/v1/accounts/contacts_controller.rb +++ b/app/controllers/api/v1/accounts/contacts_controller.rb @@ -8,6 +8,12 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController @contacts = Current.account.contacts end + # returns online contacts + def active + @contacts = Current.account.contacts.where(id: ::OnlineStatusTracker + .get_available_contact_ids(Current.account.id)) + end + def show; end def create @@ -63,6 +69,6 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController end def fetch_contact - @contact = Current.account.contacts.find(params[:id]) + @contact = Current.account.contacts.includes(contact_inboxes: [:inbox]).find(params[:id]) end end diff --git a/app/controllers/api/v1/accounts/conversations_controller.rb b/app/controllers/api/v1/accounts/conversations_controller.rb index 2f1bdcbfb..7642e94e3 100644 --- a/app/controllers/api/v1/accounts/conversations_controller.rb +++ b/app/controllers/api/v1/accounts/conversations_controller.rb @@ -15,6 +15,12 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro @conversations_count = result[:count] end + def search + result = conversation_finder.perform + @conversations = result[:conversations] + @conversations_count = result[:count] + end + def create @conversation = ::Conversation.create!(conversation_params) end @@ -26,6 +32,11 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro head :ok end + def unmute + @conversation.unmute! + head :ok + end + def transcript ConversationReplyMailer.conversation_transcript(@conversation, params[:email])&.deliver_later if params[:email].present? head :ok diff --git a/app/controllers/api/v1/accounts/inboxes_controller.rb b/app/controllers/api/v1/accounts/inboxes_controller.rb index 6c4a3cf7d..98163bae7 100644 --- a/app/controllers/api/v1/accounts/inboxes_controller.rb +++ b/app/controllers/api/v1/accounts/inboxes_controller.rb @@ -79,7 +79,7 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController def permitted_params params.permit(:id, :avatar, :name, :greeting_message, :greeting_enabled, channel: - [:type, :website_url, :widget_color, :welcome_title, :welcome_tagline, :webhook_url, :email]) + [:type, :website_url, :widget_color, :welcome_title, :welcome_tagline, :webhook_url, :email, :reply_time]) end def inbox_update_params @@ -91,6 +91,7 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController :welcome_tagline, :webhook_url, :email, + :reply_time, { selected_feature_flags: [] } ]) end diff --git a/app/controllers/api/v1/accounts/kbase/base_controller.rb b/app/controllers/api/v1/accounts/kbase/base_controller.rb new file mode 100644 index 000000000..f50140883 --- /dev/null +++ b/app/controllers/api/v1/accounts/kbase/base_controller.rb @@ -0,0 +1,9 @@ +class Api::V1::Accounts::Kbase::BaseController < Api::V1::Accounts::BaseController + before_action :portal + + private + + def portal + @portal ||= Current.account.kbase_portals.find_by(id: params[:portal_id]) + end +end diff --git a/app/controllers/api/v1/accounts/kbase/categories_controller.rb b/app/controllers/api/v1/accounts/kbase/categories_controller.rb new file mode 100644 index 000000000..e114ee5e4 --- /dev/null +++ b/app/controllers/api/v1/accounts/kbase/categories_controller.rb @@ -0,0 +1,32 @@ +class Api::V1::Accounts::Kbase::CategoriesController < Api::V1::Accounts::Kbase::BaseController + before_action :fetch_category, except: [:index, :create] + + def index + @categories = @portal.categories + end + + def create + @category = @portal.categories.create!(category_params) + end + + def update + @category.update!(category_params) + end + + def destroy + @category.destroy + head :ok + end + + private + + def fetch_category + @category = @portal.categories.find(params[:id]) + end + + def category_params + params.require(:category).permit( + :name, :description, :position + ) + end +end diff --git a/app/controllers/api/v1/accounts/kbase/portals_controller.rb b/app/controllers/api/v1/accounts/kbase/portals_controller.rb new file mode 100644 index 000000000..e0788b587 --- /dev/null +++ b/app/controllers/api/v1/accounts/kbase/portals_controller.rb @@ -0,0 +1,32 @@ +class Api::V1::Accounts::Kbase::PortalsController < Api::V1::Accounts::Kbase::BaseController + before_action :fetch_portal, except: [:index, :create] + + def index + @portals = Current.account.kbase_portals + end + + def create + @portal = Current.account.kbase_portals.create!(portal_params) + end + + def update + @portal.update!(portal_params) + end + + def destroy + @portal.destroy + head :ok + end + + private + + def fetch_portal + @portal = current_account.kbase_portals.find(params[:id]) + end + + def portal_params + params.require(:portal).permit( + :account_id, :color, :custom_domain, :header_text, :homepage_link, :name, :page_title, :slug + ) + end +end diff --git a/app/controllers/api/v1/widget/base_controller.rb b/app/controllers/api/v1/widget/base_controller.rb index 4aa2ec138..f8b2e2c96 100644 --- a/app/controllers/api/v1/widget/base_controller.rb +++ b/app/controllers/api/v1/widget/base_controller.rb @@ -4,10 +4,12 @@ class Api::V1::Widget::BaseController < ApplicationController private + def conversations + @conversations = @contact_inbox.conversations.where(inbox_id: auth_token_params[:inbox_id]) + end + def conversation - @conversation ||= @contact_inbox.conversations.where( - inbox_id: auth_token_params[:inbox_id] - ).last + @conversation ||= conversations.last end def auth_token_params @@ -20,8 +22,7 @@ class Api::V1::Widget::BaseController < ApplicationController def set_web_widget @web_widget = ::Channel::WebWidget.find_by!(website_token: permitted_params[:website_token]) - @account = @web_widget.account - switch_locale @account + @current_account = @web_widget.account end def set_contact diff --git a/app/controllers/api/v1/widget/labels_controller.rb b/app/controllers/api/v1/widget/labels_controller.rb index e8e409244..52d1fa3d3 100644 --- a/app/controllers/api/v1/widget/labels_controller.rb +++ b/app/controllers/api/v1/widget/labels_controller.rb @@ -1,20 +1,29 @@ class Api::V1::Widget::LabelsController < Api::V1::Widget::BaseController def create - conversation.label_list.add(permitted_params[:label]) - conversation.save! + if conversation.present? && label_defined_in_account? + conversation.label_list.add(permitted_params[:label]) + conversation.save! + end head :no_content end def destroy - conversation.label_list.remove(permitted_params[:id]) - conversation.save! + if conversation.present? + conversation.label_list.remove(permitted_params[:id]) + conversation.save! + end head :no_content end private + def label_defined_in_account? + label = @current_account.labels&.find_by(title: permitted_params[:label]) + label.present? + end + def permitted_params params.permit(:id, :label, :website_token) end diff --git a/app/controllers/api/v1/widget/messages_controller.rb b/app/controllers/api/v1/widget/messages_controller.rb index a3fed9fb4..a4f33b563 100644 --- a/app/controllers/api/v1/widget/messages_controller.rb +++ b/app/controllers/api/v1/widget/messages_controller.rb @@ -89,10 +89,10 @@ class Api::V1::Widget::MessagesController < Api::V1::Widget::BaseController end def update_contact(email) - contact_with_email = @account.contacts.find_by(email: email) + contact_with_email = @current_account.contacts.find_by(email: email) if contact_with_email @contact = ::ContactMergeAction.new( - account: @account, + account: @current_account, base_contact: contact_with_email, mergee_contact: @contact ).perform diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 561a797c3..58246bb73 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -5,6 +5,7 @@ class ApplicationController < ActionController::Base protect_from_forgery with: :null_session before_action :set_current_user, unless: :devise_controller? + around_action :switch_locale around_action :handle_with_exception, unless: :devise_controller? # after_action :verify_authorized @@ -64,15 +65,21 @@ class ApplicationController < ActionController::Base end def locale_from_account(account) + return unless account + I18n.available_locales.map(&:to_s).include?(account.locale) ? account.locale : nil end - def switch_locale(account) + def switch_locale(&action) # priority is for locale set in query string (mostly for widget/from js sdk) locale ||= locale_from_params # if local is not set in param, lets try account - locale ||= locale_from_account(account) - I18n.locale = locale || I18n.default_locale + locale ||= locale_from_account(@current_account) + # if nothing works we rely on default locale + locale ||= I18n.default_locale + # ensure locale won't bleed into other requests + # https://guides.rubyonrails.org/i18n.html#managing-the-locale-across-requests + I18n.with_locale(locale, &action) end def pundit_user diff --git a/app/controllers/devise_overrides/confirmations_controller.rb b/app/controllers/devise_overrides/confirmations_controller.rb index 057e77b12..0cb71f0d9 100644 --- a/app/controllers/devise_overrides/confirmations_controller.rb +++ b/app/controllers/devise_overrides/confirmations_controller.rb @@ -4,22 +4,34 @@ class DeviseOverrides::ConfirmationsController < Devise::ConfirmationsController def create @confirmable = User.find_by(confirmation_token: params[:confirmation_token]) - if @confirmable - if @confirmable.confirm || (@confirmable.confirmed_at && @confirmable.reset_password_token) - # confirmed now or already confirmed but quit before setting a password - render json: { "message": 'Success', "redirect_url": create_reset_token_link(@confirmable) }, status: :ok - elsif @confirmable.confirmed_at - render json: { "message": 'Already confirmed', "redirect_url": '/' }, status: 422 - else - render json: { "message": 'Failure', "redirect_url": '/' }, status: 422 - end + + if confirm + render_confirmation_success else - render json: { "message": 'Invalid token', "redirect_url": '/' }, status: 422 + render_confirmation_error end end protected + def confirm + @confirmable&.confirm || (@confirmable&.confirmed_at && @confirmable&.reset_password_token) + end + + def render_confirmation_success + render json: { "message": 'Success', "redirect_url": create_reset_token_link(@confirmable) }, status: :ok + end + + def render_confirmation_error + if @confirmable.blank? + render json: { "message": 'Invalid token', "redirect_url": '/' }, status: 422 + elsif @confirmable.confirmed_at + render json: { "message": 'Already confirmed', "redirect_url": '/' }, status: 422 + else + render json: { "message": 'Failure', "redirect_url": '/' }, status: 422 + end + end + def create_reset_token_link(user) raw, enc = Devise.token_generator.generate(user.class, :reset_password_token) user.reset_password_token = enc diff --git a/app/finders/conversation_finder.rb b/app/finders/conversation_finder.rb index 5ca1e12f6..f4bbcc149 100644 --- a/app/finders/conversation_finder.rb +++ b/app/finders/conversation_finder.rb @@ -27,6 +27,7 @@ class ConversationFinder find_all_conversations filter_by_status filter_by_labels if params[:labels] + filter_by_query if params[:q] mine_count, unassigned_count, all_count = set_count_for_all_conversations @@ -62,7 +63,7 @@ class ConversationFinder def find_all_conversations @conversations = current_account.conversations.includes( - :assignee, :inbox, contact: [:avatar_attachment] + :assignee, :inbox, :taggings, contact: [:avatar_attachment] ).where(inbox_id: @inbox_ids) end @@ -76,6 +77,12 @@ class ConversationFinder @conversations end + def filter_by_query + @conversations = @conversations.joins(:messages).where('messages.content LIKE :search', + search: "%#{params[:q]}%").includes(:messages).where('messages.content LIKE :search', + search: "%#{params[:q]}%") + end + def filter_by_status @conversations = @conversations.where(status: params[:status] || DEFAULT_STATUS) end diff --git a/app/finders/message_finder.rb b/app/finders/message_finder.rb index 00ef8ab3b..5295dce74 100644 --- a/app/finders/message_finder.rb +++ b/app/finders/message_finder.rb @@ -11,7 +11,7 @@ class MessageFinder private def conversation_messages - @conversation.messages.includes(:attachments, user: { avatar_attachment: :blob }) + @conversation.messages.includes(:attachments, :sender) end def messages diff --git a/app/javascript/dashboard/api/auth.js b/app/javascript/dashboard/api/auth.js index ae5031cd4..1a16108ca 100644 --- a/app/javascript/dashboard/api/auth.js +++ b/app/javascript/dashboard/api/auth.js @@ -138,4 +138,10 @@ export default { } return axios.put(endPoints('profileUpdate').url, formData); }, + + updateAvailability({ availability }) { + return axios.put(endPoints('profileUpdate').url, { + profile: { availability }, + }); + }, }; diff --git a/app/javascript/dashboard/api/channel/fbChannel.js b/app/javascript/dashboard/api/channel/fbChannel.js index 2e86cb927..f9c937122 100644 --- a/app/javascript/dashboard/api/channel/fbChannel.js +++ b/app/javascript/dashboard/api/channel/fbChannel.js @@ -12,6 +12,13 @@ class FBChannel extends ApiClient { params ); } + + reauthorizeFacebookPage({ omniauthToken, inboxId }) { + return axios.post(`${this.baseUrl()}/callbacks/reauthorize_page`, { + omniauth_token: omniauthToken, + inbox_id: inboxId, + }); + } } export default new FBChannel(); diff --git a/app/javascript/dashboard/api/inbox/conversation.js b/app/javascript/dashboard/api/inbox/conversation.js index fa23fda66..6e5c72ca0 100644 --- a/app/javascript/dashboard/api/inbox/conversation.js +++ b/app/javascript/dashboard/api/inbox/conversation.js @@ -43,6 +43,10 @@ class ConversationApi extends ApiClient { return axios.post(`${this.url}/${conversationId}/mute`); } + unmute(conversationId) { + return axios.post(`${this.url}/${conversationId}/unmute`); + } + meta({ inboxId, status, assigneeType, labels }) { return axios.get(`${this.url}/meta`, { params: { diff --git a/app/javascript/dashboard/api/specs/inbox/conversation.spec.js b/app/javascript/dashboard/api/specs/inbox/conversation.spec.js index ddea6b874..31be5e87e 100644 --- a/app/javascript/dashboard/api/specs/inbox/conversation.spec.js +++ b/app/javascript/dashboard/api/specs/inbox/conversation.spec.js @@ -14,7 +14,32 @@ describe('#ConversationAPI', () => { expect(conversationAPI).toHaveProperty('markMessageRead'); expect(conversationAPI).toHaveProperty('toggleTyping'); expect(conversationAPI).toHaveProperty('mute'); + expect(conversationAPI).toHaveProperty('unmute'); expect(conversationAPI).toHaveProperty('meta'); expect(conversationAPI).toHaveProperty('sendEmailTranscript'); }); + + describe('API calls', () => { + let originalAxios = null; + let axiosMock = null; + + beforeEach(() => { + originalAxios = window.axios; + axiosMock = { post: jest.fn(() => Promise.resolve()) }; + + window.axios = axiosMock; + }); + + afterEach(() => { + window.axios = originalAxios; + }); + + it('#unmute', () => { + conversationAPI.unmute(45); + + expect(axiosMock.post).toHaveBeenCalledWith( + '/api/v1/conversations/45/unmute' + ); + }); + }); }); diff --git a/app/javascript/dashboard/assets/scss/_foundation-custom.scss b/app/javascript/dashboard/assets/scss/_foundation-custom.scss index 209874312..175a65604 100644 --- a/app/javascript/dashboard/assets/scss/_foundation-custom.scss +++ b/app/javascript/dashboard/assets/scss/_foundation-custom.scss @@ -1,3 +1,4 @@ + .button { font-family: $body-font-family; font-weight: $font-weight-medium; diff --git a/app/javascript/dashboard/assets/scss/app.scss b/app/javascript/dashboard/assets/scss/app.scss index 3d01856ef..92edc8e42 100644 --- a/app/javascript/dashboard/assets/scss/app.scss +++ b/app/javascript/dashboard/assets/scss/app.scss @@ -14,5 +14,4 @@ @import '~bourbon/core/bourbon'; @include foundation-everything($flex: true); - @import 'woot'; diff --git a/app/javascript/dashboard/assets/scss/views/settings/integrations.scss b/app/javascript/dashboard/assets/scss/views/settings/integrations.scss index b43bbb4bc..7720c8d3e 100644 --- a/app/javascript/dashboard/assets/scss/views/settings/integrations.scss +++ b/app/javascript/dashboard/assets/scss/views/settings/integrations.scss @@ -8,6 +8,7 @@ .integration--image { display: flex; + height: 10rem; margin-right: $space-normal; width: 10rem; diff --git a/app/javascript/dashboard/assets/scss/widgets/_forms.scss b/app/javascript/dashboard/assets/scss/widgets/_forms.scss index 4606ba673..d824f9d48 100644 --- a/app/javascript/dashboard/assets/scss/widgets/_forms.scss +++ b/app/javascript/dashboard/assets/scss/widgets/_forms.scss @@ -28,3 +28,7 @@ input { font-size: $font-size-small; font-weight: $font-weight-medium; } + +.help-text { + font-weight: $font-weight-normal; +} diff --git a/app/javascript/dashboard/components/SettingsSection.vue b/app/javascript/dashboard/components/SettingsSection.vue index adf6f9912..fd930914b 100644 --- a/app/javascript/dashboard/components/SettingsSection.vue +++ b/app/javascript/dashboard/components/SettingsSection.vue @@ -1,6 +1,6 @@ @@ -50,6 +74,13 @@ export default { margin-bottom: $space-smaller; color: $color-body; + .copy-icon { + margin-left: $space-one; + &:hover { + color: $color-woot; + } + } + &.a { &:hover { text-decoration: underline; diff --git a/app/javascript/dashboard/routes/dashboard/settings/agents/AddAgent.vue b/app/javascript/dashboard/routes/dashboard/settings/agents/AddAgent.vue index 4c5b571e7..8cc1a83e1 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/agents/AddAgent.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/agents/AddAgent.vue @@ -21,16 +21,11 @@