diff --git a/.env.example b/.env.example index 62b08a7b1..7f40616b5 100644 --- a/.env.example +++ b/.env.example @@ -60,9 +60,10 @@ MAILER_SENDER_EMAIL=Chatwoot #SMTP domain key is set up for HELO checking SMTP_DOMAIN=chatwoot.com -# the default value is set "mailhog" and is used by docker-compose for development environments, +# Set the value to "mailhog" if using docker-compose for development environments, # Set the value as "localhost" or your SMTP address in other environments -SMTP_ADDRESS=mailhog +# If SMTP_ADDRESS is empty, Chatwoot would try to use sendmail(postfix) +SMTP_ADDRESS= SMTP_PORT=1025 SMTP_USERNAME= SMTP_PASSWORD= diff --git a/.rubocop.yml b/.rubocop.yml index 3665ad2e3..dafd9a620 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -16,7 +16,6 @@ Metrics/ClassLength: - 'app/models/message.rb' - 'app/builders/messages/facebook/message_builder.rb' - 'app/controllers/api/v1/accounts/contacts_controller.rb' - - 'app/controllers/api/v1/accounts/conversations_controller.rb' - 'app/listeners/action_cable_listener.rb' - 'app/models/conversation.rb' RSpec/ExampleLength: diff --git a/Gemfile b/Gemfile index d47b5e449..77720f414 100644 --- a/Gemfile +++ b/Gemfile @@ -4,7 +4,7 @@ ruby '3.0.4' ##-- base gems for rails --## gem 'rack-cors', require: 'rack/cors' -gem 'rails', '~>6.1' +gem 'rails', '~> 6.1', '>= 6.1.6.1' # Reduces boot times through caching; required in config/boot.rb gem 'bootsnap', require: false @@ -56,7 +56,7 @@ gem 'activerecord-import' gem 'dotenv-rails' gem 'foreman' gem 'puma' -gem 'webpacker', '~> 5.x' +gem 'webpacker', '~> 5.4', '>= 5.4.3' # metrics on heroku gem 'barnes' @@ -94,7 +94,7 @@ gem 'ddtrace' gem 'elastic-apm' gem 'newrelic_rpm' gem 'scout_apm' -gem 'sentry-rails', '~> 5.3' +gem 'sentry-rails', '~> 5.3', '>= 5.3.1' gem 'sentry-ruby', '~> 5.3' gem 'sentry-sidekiq', '~> 5.3' @@ -175,7 +175,7 @@ group :development, :test do gem 'mock_redis' gem 'pry-rails' gem 'rspec_junit_formatter' - gem 'rspec-rails', '~> 5.0.0' + gem 'rspec-rails', '~> 5.0.3' gem 'rubocop', require: false gem 'rubocop-performance', require: false gem 'rubocop-rails', require: false diff --git a/Gemfile.lock b/Gemfile.lock index 686c3a37b..e96ae1b9e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -398,7 +398,7 @@ GEM llhttp-ffi (0.4.0) ffi-compiler (~> 1.0) rake (~> 13.0) - loofah (2.18.0) + loofah (2.19.1) crass (~> 1.0.2) nokogiri (>= 1.5.9) mail (2.7.1) @@ -427,14 +427,14 @@ GEM netrc (0.11.0) newrelic_rpm (8.9.0) nio4r (2.5.8) - nokogiri (1.13.9) + nokogiri (1.13.10) mini_portile2 (~> 2.8.0) racc (~> 1.4) - nokogiri (1.13.9-arm64-darwin) + nokogiri (1.13.10-arm64-darwin) racc (~> 1.4) - nokogiri (1.13.9-x86_64-darwin) + nokogiri (1.13.10-x86_64-darwin) racc (~> 1.4) - nokogiri (1.13.9-x86_64-linux) + nokogiri (1.13.10-x86_64-linux) racc (~> 1.4) oauth (0.5.10) orm_adapter (0.5.0) @@ -459,7 +459,7 @@ GEM pundit (2.2.0) activesupport (>= 3.0.0) raabro (1.4.0) - racc (1.6.0) + racc (1.6.1) rack (2.2.4) rack-attack (6.6.1) rack (>= 1.0, < 3) @@ -488,8 +488,8 @@ GEM rails-dom-testing (2.0.3) activesupport (>= 4.2.0) nokogiri (>= 1.6) - rails-html-sanitizer (1.4.3) - loofah (~> 2.3) + rails-html-sanitizer (1.4.4) + loofah (~> 2.19, >= 2.19.1) railties (6.1.6.1) actionpack (= 6.1.6.1) activesupport (= 6.1.6.1) @@ -765,12 +765,12 @@ DEPENDENCIES rack-attack rack-cors rack-timeout - rails (~> 6.1) + rails (~> 6.1, >= 6.1.6.1) redis redis-namespace responders rest-client - rspec-rails (~> 5.0.0) + rspec-rails (~> 5.0.3) rspec_junit_formatter rubocop rubocop-performance @@ -778,7 +778,7 @@ DEPENDENCIES rubocop-rspec scout_apm seed_dump - sentry-rails (~> 5.3) + sentry-rails (~> 5.3, >= 5.3.1) sentry-ruby (~> 5.3) sentry-sidekiq (~> 5.3) shoulda-matchers @@ -799,7 +799,7 @@ DEPENDENCIES valid_email2 web-console webmock - webpacker (~> 5.x) + webpacker (~> 5.4, >= 5.4.3) webpush wisper (= 2.0.0) working_hours diff --git a/app.json b/app.json index 055235032..6324672fb 100644 --- a/app.json +++ b/app.json @@ -41,16 +41,24 @@ "formation": { "web": { "quantity": 1, - "size": "FREE" + "size": "basic" }, "worker": { "quantity": 1, - "size": "FREE" + "size": "basic" } }, "stack": "heroku-20", "image": "heroku/ruby", - "addons": [ "heroku-redis", "heroku-postgresql"], + "addons": [ + { + "plan": "heroku-redis:mini" + }, + { + "plan": "heroku-postgresql:mini" + } + ], + "stack": "heroku-20", "buildpacks": [ { "url": "heroku/ruby" diff --git a/app/builders/conversation_builder.rb b/app/builders/conversation_builder.rb new file mode 100644 index 000000000..6a995b188 --- /dev/null +++ b/app/builders/conversation_builder.rb @@ -0,0 +1,40 @@ +class ConversationBuilder + pattr_initialize [:params!, :contact_inbox!] + + def perform + look_up_exising_conversation || create_new_conversation + end + + private + + def look_up_exising_conversation + return unless @contact_inbox.inbox.lock_to_single_conversation? + + @contact_inbox.conversations.last + end + + def create_new_conversation + ::Conversation.create!(conversation_params) + end + + def conversation_params + additional_attributes = params[:additional_attributes]&.permit! || {} + custom_attributes = params[:custom_attributes]&.permit! || {} + status = params[:status].present? ? { status: params[:status] } : {} + + # TODO: temporary fallback for the old bot status in conversation, we will remove after couple of releases + # commenting this out to see if there are any errors, if not we can remove this in subsequent releases + # status = { status: 'pending' } if status[:status] == 'bot' + { + account_id: @contact_inbox.inbox.account_id, + inbox_id: @contact_inbox.inbox_id, + contact_id: @contact_inbox.contact_id, + contact_inbox_id: @contact_inbox.id, + additional_attributes: additional_attributes, + custom_attributes: custom_attributes, + snoozed_until: params[:snoozed_until], + assignee_id: params[:assignee_id], + team_id: params[:team_id] + }.merge(status) + end +end diff --git a/app/builders/messages/messenger/message_builder.rb b/app/builders/messages/messenger/message_builder.rb index d3b5bf6b9..0739829aa 100644 --- a/app/builders/messages/messenger/message_builder.rb +++ b/app/builders/messages/messenger/message_builder.rb @@ -46,6 +46,7 @@ class Messages::Messenger::MessageBuilder end def update_attachment_file_type(attachment) + return if @message.reload.attachments.blank? return unless attachment.file_type == 'share' || attachment.file_type == 'story_mention' attachment.file_type = file_type(attachment.file&.content_type) @@ -62,6 +63,7 @@ class Messages::Messenger::MessageBuilder story_sender = result['from']['username'] message.content_attributes[:story_sender] = story_sender message.content_attributes[:story_id] = story_id + message.content_attributes[:image_type] = 'story_mention' message.content = I18n.t('conversations.messages.instagram_story_content', story_sender: story_sender) message.save! end @@ -74,6 +76,7 @@ class Messages::Messenger::MessageBuilder raise rescue Koala::Facebook::ClientError => e # The exception occurs when we are trying fetch the deleted story or blocked story. + @message.attachments.destroy_all @message.update(content: I18n.t('conversations.messages.instagram_deleted_story_content')) Rails.logger.error e {} diff --git a/app/controllers/api/v1/accounts/contacts/conversations_controller.rb b/app/controllers/api/v1/accounts/contacts/conversations_controller.rb index cadfe133f..5e0a0e55e 100644 --- a/app/controllers/api/v1/accounts/contacts/conversations_controller.rb +++ b/app/controllers/api/v1/accounts/contacts/conversations_controller.rb @@ -2,7 +2,7 @@ class Api::V1::Accounts::Contacts::ConversationsController < Api::V1::Accounts:: def index @conversations = Current.account.conversations.includes( :assignee, :contact, :inbox, :taggings - ).where(inbox_id: inbox_ids, contact_id: @contact.id) + ).where(inbox_id: inbox_ids, contact_id: @contact.id).order(id: :desc).limit(20) end private diff --git a/app/controllers/api/v1/accounts/conversations_controller.rb b/app/controllers/api/v1/accounts/conversations_controller.rb index 8734a3dd4..ec107dfff 100644 --- a/app/controllers/api/v1/accounts/conversations_controller.rb +++ b/app/controllers/api/v1/accounts/conversations_controller.rb @@ -24,7 +24,7 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro def create ActiveRecord::Base.transaction do - @conversation = ::Conversation.create!(conversation_params) + @conversation = ConversationBuilder.new(params: params, contact_inbox: @contact_inbox).perform Messages::MessageBuilder.new(Current.user, @conversation, params[:message]).perform if params[:message].present? end end @@ -75,10 +75,13 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro end def update_last_seen - # rubocop:disable Rails/SkipsModelValidations - @conversation.update_column(:agent_last_seen_at, DateTime.now.utc) - @conversation.update_column(:assignee_last_seen_at, DateTime.now.utc) if assignee? - # rubocop:enable Rails/SkipsModelValidations + update_last_seen_on_conversation(DateTime.now.utc, assignee?) + end + + def unread + last_incoming_message = @conversation.messages.incoming.last + last_seen_at = last_incoming_message.created_at - 1.second if last_incoming_message.present? + update_last_seen_on_conversation(last_seen_at, true) end def custom_attributes @@ -88,9 +91,18 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro private + def update_last_seen_on_conversation(last_seen_at, update_assignee) + # rubocop:disable Rails/SkipsModelValidations + @conversation.update_column(:agent_last_seen_at, last_seen_at) + @conversation.update_column(:assignee_last_seen_at, last_seen_at) if update_assignee.present? + # rubocop:enable Rails/SkipsModelValidations + end + def set_conversation_status - status = params[:status] == 'bot' ? 'pending' : params[:status] - @conversation.status = status + # TODO: temporary fallback for the old bot status in conversation, we will remove after couple of releases + # commenting this out to see if there are any errors, if not we can remove this in subsequent releases + # status = params[:status] == 'bot' ? 'pending' : params[:status] + @conversation.status = params[:status] @conversation.snoozed_until = parse_date_time(params[:snoozed_until].to_s) if params[:snoozed_until] end @@ -142,31 +154,11 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro ).perform end - def conversation_params - additional_attributes = params[:additional_attributes]&.permit! || {} - custom_attributes = params[:custom_attributes]&.permit! || {} - status = params[:status].present? ? { status: params[:status] } : {} - - # TODO: temporary fallback for the old bot status in conversation, we will remove after couple of releases - status = { status: 'pending' } if status[:status] == 'bot' - { - account_id: Current.account.id, - inbox_id: @contact_inbox.inbox_id, - contact_id: @contact_inbox.contact_id, - contact_inbox_id: @contact_inbox.id, - additional_attributes: additional_attributes, - custom_attributes: custom_attributes, - snoozed_until: params[:snoozed_until], - assignee_id: params[:assignee_id], - team_id: params[:team_id] - }.merge(status) - end - def conversation_finder - @conversation_finder ||= ConversationFinder.new(current_user, params) + @conversation_finder ||= ConversationFinder.new(Current.user, params) end def assignee? - @conversation.assignee_id? && current_user == @conversation.assignee + @conversation.assignee_id? && Current.user == @conversation.assignee end end diff --git a/app/controllers/api/v1/accounts/inboxes_controller.rb b/app/controllers/api/v1/accounts/inboxes_controller.rb index 95662e29b..24507977e 100644 --- a/app/controllers/api/v1/accounts/inboxes_controller.rb +++ b/app/controllers/api/v1/accounts/inboxes_controller.rb @@ -113,7 +113,8 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController def inbox_attributes [:name, :avatar, :greeting_enabled, :greeting_message, :enable_email_collect, :csat_survey_enabled, - :enable_auto_assignment, :working_hours_enabled, :out_of_office_message, :timezone, :allow_messages_after_resolved] + :enable_auto_assignment, :working_hours_enabled, :out_of_office_message, :timezone, :allow_messages_after_resolved, + :lock_to_single_conversation] end def permitted_params(channel_attributes = []) diff --git a/app/controllers/api/v1/accounts/portals_controller.rb b/app/controllers/api/v1/accounts/portals_controller.rb index 62a98a872..d28ae54b7 100644 --- a/app/controllers/api/v1/accounts/portals_controller.rb +++ b/app/controllers/api/v1/accounts/portals_controller.rb @@ -21,6 +21,7 @@ class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController def create @portal = Current.account.portals.build(portal_params) + @portal.custom_domain = parsed_custom_domain @portal.save! process_attached_logo end @@ -28,6 +29,7 @@ class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController def update ActiveRecord::Base.transaction do @portal.update!(portal_params) if params[:portal].present? + # @portal.custom_domain = parsed_custom_domain process_attached_logo rescue StandardError => e Rails.logger.error e @@ -73,4 +75,9 @@ class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController def set_current_page @current_page = params[:page] || 1 end + + def parsed_custom_domain + domain = URI.parse(@portal.custom_domain) + domain.is_a?(URI::HTTP) ? domain.host : @portal.custom_domain + end end diff --git a/app/controllers/api/v1/profiles_controller.rb b/app/controllers/api/v1/profiles_controller.rb index 20b8e7ae8..cbf801e82 100644 --- a/app/controllers/api/v1/profiles_controller.rb +++ b/app/controllers/api/v1/profiles_controller.rb @@ -18,6 +18,10 @@ class Api::V1::ProfilesController < Api::BaseController head :ok end + def auto_offline + @user.account_users.find_by!(account_id: auto_offline_params[:account_id]).update!(auto_offline: auto_offline_params[:auto_offline] || false) + end + def availability @user.account_users.find_by!(account_id: availability_params[:account_id]).update!(availability: availability_params[:availability]) end @@ -37,6 +41,10 @@ class Api::V1::ProfilesController < Api::BaseController params.require(:profile).permit(:account_id, :availability) end + def auto_offline_params + params.require(:profile).permit(:account_id, :auto_offline) + end + def profile_params params.require(:profile).permit( :email, diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb index 84677c770..30b78772a 100644 --- a/app/controllers/dashboard_controller.rb +++ b/app/controllers/dashboard_controller.rb @@ -16,8 +16,7 @@ class DashboardController < ActionController::Base @global_config = GlobalConfig.get( 'LOGO', 'LOGO_THUMBNAIL', 'INSTALLATION_NAME', - 'WIDGET_BRAND_URL', - 'TERMS_URL', + 'WIDGET_BRAND_URL', 'TERMS_URL', 'PRIVACY_URL', 'DISPLAY_MANIFEST', 'CREATE_NEW_ACCOUNT_FROM_DASHBOARD', @@ -25,12 +24,12 @@ class DashboardController < ActionController::Base 'API_CHANNEL_NAME', 'API_CHANNEL_THUMBNAIL', 'ANALYTICS_TOKEN', - 'ANALYTICS_HOST', 'DIRECT_UPLOADS_ENABLED', 'HCAPTCHA_SITE_KEY', 'LOGOUT_REDIRECT_LINK', 'DISABLE_USER_PROFILE_UPDATE', - 'DEPLOYMENT_ENV' + 'DEPLOYMENT_ENV', + 'CSML_EDITOR_HOST' ).merge(app_config) end diff --git a/app/controllers/platform/api/v1/accounts_controller.rb b/app/controllers/platform/api/v1/accounts_controller.rb index 2873cc22c..1ea7d4954 100644 --- a/app/controllers/platform/api/v1/accounts_controller.rb +++ b/app/controllers/platform/api/v1/accounts_controller.rb @@ -1,8 +1,7 @@ class Platform::Api::V1::AccountsController < PlatformController def create - @resource = Account.new(account_params) + @resource = Account.create!(account_params) update_resource_features - @resource.save! @platform_app.platform_app_permissibles.find_or_create_by(permissible: @resource) end diff --git a/app/finders/conversation_finder.rb b/app/finders/conversation_finder.rb index 58e911362..b337e8458 100644 --- a/app/finders/conversation_finder.rb +++ b/app/finders/conversation_finder.rb @@ -56,7 +56,6 @@ class ConversationFinder filter_by_team if @team filter_by_labels if params[:labels] filter_by_query if params[:q] - filter_by_reply_status end def set_inboxes @@ -76,12 +75,9 @@ class ConversationFinder end def find_all_conversations - if params[:conversation_type] == 'mention' - conversation_ids = current_account.mentions.where(user: current_user).pluck(:conversation_id) - @conversations = current_account.conversations.where(id: conversation_ids) - else - @conversations = current_account.conversations.where(inbox_id: @inbox_ids) - end + @conversations = current_account.conversations.where(inbox_id: @inbox_ids) + filter_by_conversation_type if params[:conversation_type] + @conversations end def filter_by_assignee_type @@ -96,8 +92,15 @@ class ConversationFinder @conversations end - def filter_by_reply_status - @conversations = @conversations.where(first_reply_created_at: nil) if params[:reply_status] == 'unattended' + def filter_by_conversation_type + case @params[:conversation_type] + when 'mention' + conversation_ids = current_account.mentions.where(user: current_user).pluck(:conversation_id) + @conversations = @conversations.where(id: conversation_ids) + when 'unattended' + @conversations = @conversations.where(first_reply_created_at: nil) + end + @conversations end def filter_by_query diff --git a/app/finders/message_finder.rb b/app/finders/message_finder.rb index e2581e0ad..7a00e5cbf 100644 --- a/app/finders/message_finder.rb +++ b/app/finders/message_finder.rb @@ -21,7 +21,9 @@ class MessageFinder end def current_messages - if @params[:before].present? + if @params[:after].present? + messages.reorder('created_at asc').where('id >= ?', @params[:before].to_i).limit(20) + elsif @params[:before].present? messages.reorder('created_at desc').where('id < ?', @params[:before].to_i).limit(20).reverse else messages.reorder('created_at desc').limit(20).reverse diff --git a/app/javascript/dashboard/api/auth.js b/app/javascript/dashboard/api/auth.js index ef1762f46..19ba40a42 100644 --- a/app/javascript/dashboard/api/auth.js +++ b/app/javascript/dashboard/api/auth.js @@ -144,6 +144,12 @@ export default { }); }, + updateAutoOffline(accountId, autoOffline = false) { + return axios.post(endPoints('autoOffline').url, { + profile: { account_id: accountId, auto_offline: autoOffline }, + }); + }, + deleteAvatar() { return axios.delete(endPoints('deleteAvatar').url); }, diff --git a/app/javascript/dashboard/api/endPoints.js b/app/javascript/dashboard/api/endPoints.js index 8deb8d56a..678386d50 100644 --- a/app/javascript/dashboard/api/endPoints.js +++ b/app/javascript/dashboard/api/endPoints.js @@ -16,6 +16,9 @@ const endPoints = { availabilityUpdate: { url: '/api/v1/profile/availability', }, + autoOffline: { + url: '/api/v1/profile/auto_offline', + }, logout: { url: 'auth/sign_out', }, diff --git a/app/javascript/dashboard/api/inbox/conversation.js b/app/javascript/dashboard/api/inbox/conversation.js index 22548499d..8d5f5b82c 100644 --- a/app/javascript/dashboard/api/inbox/conversation.js +++ b/app/javascript/dashboard/api/inbox/conversation.js @@ -68,6 +68,10 @@ class ConversationApi extends ApiClient { return axios.post(`${this.url}/${id}/update_last_seen`); } + markMessagesUnread({ id }) { + return axios.post(`${this.url}/${id}/unread`); + } + toggleTyping({ conversationId, status, isPrivate }) { return axios.post(`${this.url}/${conversationId}/toggle_typing_status`, { typing_status: status, diff --git a/app/javascript/dashboard/api/inboxes.js b/app/javascript/dashboard/api/inboxes.js index a76ef1414..657a6d0e6 100644 --- a/app/javascript/dashboard/api/inboxes.js +++ b/app/javascript/dashboard/api/inboxes.js @@ -13,6 +13,16 @@ class Inboxes extends ApiClient { deleteInboxAvatar(inboxId) { return axios.delete(`${this.url}/${inboxId}/avatar`); } + + getAgentBot(inboxId) { + return axios.get(`${this.url}/${inboxId}/agent_bot`); + } + + setAgentBot(inboxId, botId) { + return axios.post(`${this.url}/${inboxId}/set_agent_bot`, { + agent_bot: botId, + }); + } } export default new Inboxes(); diff --git a/app/javascript/dashboard/api/specs/inboxes.spec.js b/app/javascript/dashboard/api/specs/inboxes.spec.js index 0d755d44a..e471dac86 100644 --- a/app/javascript/dashboard/api/specs/inboxes.spec.js +++ b/app/javascript/dashboard/api/specs/inboxes.spec.js @@ -11,6 +11,8 @@ describe('#InboxesAPI', () => { expect(inboxesAPI).toHaveProperty('update'); expect(inboxesAPI).toHaveProperty('delete'); expect(inboxesAPI).toHaveProperty('getCampaigns'); + expect(inboxesAPI).toHaveProperty('getAgentBot'); + expect(inboxesAPI).toHaveProperty('setAgentBot'); }); describeWithAPIMock('API calls', context => { it('#getCampaigns', () => { diff --git a/app/javascript/dashboard/api/testimonials.js b/app/javascript/dashboard/api/testimonials.js new file mode 100644 index 000000000..705b7aabb --- /dev/null +++ b/app/javascript/dashboard/api/testimonials.js @@ -0,0 +1,6 @@ +/* global axios */ +import wootConstants from 'dashboard/constants'; + +export const getTestimonialContent = () => { + return axios.get(wootConstants.TESTIMONIAL_URL); +}; diff --git a/app/javascript/dashboard/assets/scss/_foundation-settings.scss b/app/javascript/dashboard/assets/scss/_foundation-settings.scss index 52fa64ba2..414cb2539 100644 --- a/app/javascript/dashboard/assets/scss/_foundation-settings.scss +++ b/app/javascript/dashboard/assets/scss/_foundation-settings.scss @@ -74,8 +74,8 @@ Tahoma, Arial, sans-serif; $body-antialiased: true; -$global-margin: $space-one; -$global-padding: $space-one; +$global-margin: $space-small; +$global-padding: $space-micro; $global-weight-normal: normal; $global-weight-bold: bold; $global-radius: 0; diff --git a/app/javascript/dashboard/assets/scss/_woot.scss b/app/javascript/dashboard/assets/scss/_woot.scss index 416aa808b..675771715 100644 --- a/app/javascript/dashboard/assets/scss/_woot.scss +++ b/app/javascript/dashboard/assets/scss/_woot.scss @@ -20,6 +20,24 @@ @include foundation-everything($flex: true); +@include foundation-prototype-text-utilities; +@include foundation-prototype-text-transformation; +@include foundation-prototype-text-decoration; +@include foundation-prototype-font-styling; +@include foundation-prototype-list-style-type; +@include foundation-prototype-rounded; +@include foundation-prototype-bordered; +@include foundation-prototype-shadow; +@include foundation-prototype-separator; +@include foundation-prototype-overflow; +@include foundation-prototype-display; +@include foundation-prototype-position; +@include foundation-prototype-border-box; +@include foundation-prototype-border-none; +@include foundation-prototype-sizing; +@include foundation-prototype-spacing; + + @import 'typography'; @import 'layout'; @import 'animations'; diff --git a/app/javascript/dashboard/assets/scss/widgets/_buttons.scss b/app/javascript/dashboard/assets/scss/widgets/_buttons.scss index 478045000..15f344ddf 100644 --- a/app/javascript/dashboard/assets/scss/widgets/_buttons.scss +++ b/app/javascript/dashboard/assets/scss/widgets/_buttons.scss @@ -155,12 +155,20 @@ $default-button-height: 4.0rem; // Sizes &.tiny { height: var(--space-medium); + + .icon+.button__content { + padding-left: var(--space-micro); + } } &.small { height: var(--space-large); padding-bottom: var(--space-smaller); padding-top: var(--space-smaller); + + .icon+.button__content { + padding-left: var(--space-smaller); + } } &.large { @@ -190,6 +198,10 @@ $default-button-height: 4.0rem; height: auto; margin: 0; padding: 0; + + &:hover { + text-decoration: underline; + } } } diff --git a/app/javascript/dashboard/components/ChatList.vue b/app/javascript/dashboard/components/ChatList.vue index 16865d0dc..a957ff78b 100644 --- a/app/javascript/dashboard/components/ChatList.vue +++ b/app/javascript/dashboard/components/ChatList.vue @@ -102,6 +102,7 @@ @assign-agent="onAssignAgent" @update-conversations="onUpdateConversations" @assign-labels="onAssignLabels" + @assign-team="onAssignTeamsForBulk" />
@@ -184,6 +186,11 @@ import { hasPressedAltAndJKey, hasPressedAltAndKKey, } from 'shared/helpers/KeyboardHelpers'; +import { conversationListPageURL } from '../helper/URLHelper'; +import { + isOnMentionsView, + isOnUnattendedView, +} from '../store/modules/conversations/helpers/actionHelpers'; export default { components: { @@ -332,14 +339,15 @@ export default { status: this.activeStatus, page: this.currentPage + 1, labels: this.label ? [this.label] : undefined, - teamId: this.teamId ? this.teamId : undefined, - conversationType: this.conversationType - ? this.conversationType - : undefined, + teamId: this.teamId || undefined, + conversationType: this.conversationType || undefined, folders: this.hasActiveFolders ? this.savedFoldersValue : undefined, }; }, pageTitle() { + if (this.hasAppliedFilters) { + return this.$t('CHAT_LIST.TAB_HEADING'); + } if (this.inbox.name) { return this.inbox.name; } @@ -352,6 +360,9 @@ export default { if (this.conversationType === 'mention') { return this.$t('CHAT_LIST.MENTION_HEADING'); } + if (this.conversationType === 'unattended') { + return this.$t('CHAT_LIST.UNATTENDED_HEADING'); + } if (this.hasActiveFolders) { return this.activeFolder.name; } @@ -431,9 +442,6 @@ export default { }, methods: { onApplyFilter(payload) { - if (this.$route.name !== 'home') { - this.$router.push({ name: 'home' }); - } this.resetBulkActions(); this.foldersQuery = filterQueryGenerator(payload); this.$store.dispatch('conversationPage/reset'); @@ -636,6 +644,35 @@ export default { this.showAlert(this.$t('BULK_ACTION.ASSIGN_FAILED')); } }, + async markAsUnread(conversationId) { + try { + await this.$store.dispatch('markMessagesUnread', { + id: conversationId, + }); + const { + params: { accountId, inbox_id: inboxId, label, teamId }, + name, + } = this.$route; + let conversationType = ''; + if (isOnMentionsView({ route: { name } })) { + conversationType = 'mention'; + } else if (isOnUnattendedView({ route: { name } })) { + conversationType = 'unattended'; + } + this.$router.push( + conversationListPageURL({ + accountId, + conversationType: conversationType, + customViewId: this.foldersId, + inboxId, + label, + teamId, + }) + ); + } catch (error) { + // Ignore error + } + }, async onAssignTeam(team, conversationId = null) { try { await this.$store.dispatch('assignTeam', { @@ -685,6 +722,21 @@ export default { this.showAlert(this.$t('BULK_ACTION.LABELS.ASSIGN_FAILED')); } }, + async onAssignTeamsForBulk(team) { + try { + await this.$store.dispatch('bulkActions/process', { + type: 'Conversation', + ids: this.selectedConversations, + fields: { + team_id: team.id, + }, + }); + this.selectedConversations = []; + this.showAlert(this.$t('BULK_ACTION.TEAMS.ASSIGN_SUCCESFUL')); + } catch (err) { + this.showAlert(this.$t('BULK_ACTION.TEAMS.ASSIGN_FAILED')); + } + }, async onUpdateConversations(status) { try { await this.$store.dispatch('bulkActions/process', { diff --git a/app/javascript/dashboard/components/layout/AvailabilityStatus.vue b/app/javascript/dashboard/components/layout/AvailabilityStatus.vue index 1c70e6755..0a2698a9a 100644 --- a/app/javascript/dashboard/components/layout/AvailabilityStatus.vue +++ b/app/javascript/dashboard/components/layout/AvailabilityStatus.vue @@ -18,12 +18,35 @@ + +
+ + + + {{ $t('SIDEBAR.SET_AUTO_OFFLINE.TEXT') }} + +
+ + +
+ diff --git a/app/javascript/dashboard/components/widgets/conversation/contextMenu/Index.vue b/app/javascript/dashboard/components/widgets/conversation/contextMenu/Index.vue index a8a5f3cb3..6a27d31eb 100644 --- a/app/javascript/dashboard/components/widgets/conversation/contextMenu/Index.vue +++ b/app/javascript/dashboard/components/widgets/conversation/contextMenu/Index.vue @@ -1,5 +1,11 @@