diff --git a/.codeclimate.yml b/.codeclimate.yml index f26021240..c9910f9ed 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -22,6 +22,9 @@ checks: enabled: true config: threshold: 300 + method-lines: + config: + threshold: 50 exclude_patterns: - 'spec/' - '**/specs/' @@ -44,3 +47,6 @@ exclude_patterns: - 'app/javascript/shared/constants/countries.js' - 'app/javascript/dashboard/components/widgets/conversation/advancedFilterItems/languages.js' - 'app/javascript/dashboard/routes/dashboard/contacts/contactFilterItems/index.js' + - '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' diff --git a/Gemfile b/Gemfile index f3fbce89d..0664e5aef 100644 --- a/Gemfile +++ b/Gemfile @@ -102,7 +102,7 @@ gem 'sentry-ruby' gem 'sentry-sidekiq' ##-- background job processing --## -gem 'sidekiq' +gem 'sidekiq', '~> 6.4.0' # We want cron jobs gem 'sidekiq-cron' diff --git a/Gemfile.lock b/Gemfile.lock index 079dd0ee6..4df118291 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -9,63 +9,63 @@ GIT GEM remote: https://rubygems.org/ specs: - actioncable (6.1.4.3) - actionpack (= 6.1.4.3) - activesupport (= 6.1.4.3) + actioncable (6.1.4.6) + actionpack (= 6.1.4.6) + activesupport (= 6.1.4.6) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (6.1.4.3) - actionpack (= 6.1.4.3) - activejob (= 6.1.4.3) - activerecord (= 6.1.4.3) - activestorage (= 6.1.4.3) - activesupport (= 6.1.4.3) + 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) mail (>= 2.7.1) - actionmailer (6.1.4.3) - actionpack (= 6.1.4.3) - actionview (= 6.1.4.3) - activejob (= 6.1.4.3) - activesupport (= 6.1.4.3) + 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) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 2.0) - actionpack (6.1.4.3) - actionview (= 6.1.4.3) - activesupport (= 6.1.4.3) + actionpack (6.1.4.6) + actionview (= 6.1.4.6) + activesupport (= 6.1.4.6) 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.3) - actionpack (= 6.1.4.3) - activerecord (= 6.1.4.3) - activestorage (= 6.1.4.3) - activesupport (= 6.1.4.3) + 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) nokogiri (>= 1.8.5) - actionview (6.1.4.3) - activesupport (= 6.1.4.3) + actionview (6.1.4.6) + activesupport (= 6.1.4.6) 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.3) - activesupport (= 6.1.4.3) + activejob (6.1.4.6) + activesupport (= 6.1.4.6) globalid (>= 0.3.6) - activemodel (6.1.4.3) - activesupport (= 6.1.4.3) - activerecord (6.1.4.3) - activemodel (= 6.1.4.3) - activesupport (= 6.1.4.3) + 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) activerecord-import (1.2.0) activerecord (>= 3.2) - activestorage (6.1.4.3) - actionpack (= 6.1.4.3) - activejob (= 6.1.4.3) - activerecord (= 6.1.4.3) - activesupport (= 6.1.4.3) + 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) marcel (~> 1.0.0) mini_mime (>= 1.1.0) - activesupport (6.1.4.3) + activesupport (6.1.4.6) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) @@ -300,7 +300,7 @@ GEM mime-types (~> 3.0) multi_xml (>= 0.5.2) httpclient (2.8.3) - i18n (1.8.11) + i18n (1.9.1) concurrent-ruby (~> 1.0) image_processing (1.12.1) mini_magick (>= 4.9.5, < 5) @@ -346,7 +346,7 @@ GEM listen (3.7.0) rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) - loofah (2.13.0) + loofah (2.14.0) crass (~> 1.0.2) nokogiri (>= 1.5.9) mail (2.7.1) @@ -400,7 +400,7 @@ GEM pry-rails (0.3.9) pry (>= 0.10.4) public_suffix (4.0.6) - puma (5.5.2) + puma (5.6.2) nio4r (~> 2.0) pundit (2.1.1) activesupport (>= 3.0.0) @@ -416,29 +416,29 @@ GEM rack-test (1.1.0) rack (>= 1.0, < 3) rack-timeout (0.6.0) - rails (6.1.4.3) - actioncable (= 6.1.4.3) - actionmailbox (= 6.1.4.3) - actionmailer (= 6.1.4.3) - actionpack (= 6.1.4.3) - actiontext (= 6.1.4.3) - actionview (= 6.1.4.3) - activejob (= 6.1.4.3) - activemodel (= 6.1.4.3) - activerecord (= 6.1.4.3) - activestorage (= 6.1.4.3) - activesupport (= 6.1.4.3) + 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) bundler (>= 1.15.0) - railties (= 6.1.4.3) + railties (= 6.1.4.6) 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.3) - actionpack (= 6.1.4.3) - activesupport (= 6.1.4.3) + railties (6.1.4.6) + actionpack (= 6.1.4.6) + activesupport (= 6.1.4.6) method_source rake (>= 0.13) thor (~> 1.0) @@ -447,7 +447,7 @@ GEM rb-fsevent (0.11.0) rb-inotify (0.10.1) ffi (~> 1.0) - redis (4.4.0) + redis (4.5.1) redis-namespace (1.8.1) redis (>= 3.0.4) regexp_parser (2.1.1) @@ -546,7 +546,7 @@ GEM sexp_processor (4.15.3) shoulda-matchers (5.0.0) activesupport (>= 5.2.0) - sidekiq (6.2.2) + sidekiq (6.4.0) connection_pool (>= 2.2.2) rack (~> 2.0) redis (>= 4.2.0) @@ -583,7 +583,7 @@ GEM squasher (0.6.2) statsd-ruby (1.5.0) telephone_number (1.4.12) - thor (1.1.0) + thor (1.2.1) tilt (2.0.10) time_diff (0.3.0) activesupport @@ -635,7 +635,7 @@ GEM websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) wisper (2.0.0) - zeitwerk (2.5.1) + zeitwerk (2.5.4) PLATFORMS arm64-darwin-20 @@ -726,7 +726,7 @@ DEPENDENCIES sentry-ruby sentry-sidekiq shoulda-matchers - sidekiq + sidekiq (~> 6.4.0) sidekiq-cron simplecov (= 0.17.1) slack-ruby-client diff --git a/app/builders/account_builder.rb b/app/builders/account_builder.rb index 0464c8013..575f14aaa 100644 --- a/app/builders/account_builder.rb +++ b/app/builders/account_builder.rb @@ -2,7 +2,7 @@ class AccountBuilder include CustomExceptions::Account - pattr_initialize [:account_name!, :email!, :confirmed, :user, :user_full_name, :user_password] + pattr_initialize [:account_name!, :email!, :confirmed, :user, :user_full_name, :user_password, :super_admin] def perform if @user.nil? @@ -65,6 +65,7 @@ class AccountBuilder password: user_password, password_confirmation: user_password, name: @user_full_name) + @user.type = 'SuperAdmin' if @super_admin @user.confirm if @confirmed @user.save! end diff --git a/app/builders/contact_inbox_builder.rb b/app/builders/contact_inbox_builder.rb index 8ee1b3fec..e7ae8b0aa 100644 --- a/app/builders/contact_inbox_builder.rb +++ b/app/builders/contact_inbox_builder.rb @@ -4,7 +4,7 @@ class ContactInboxBuilder def perform @contact = Contact.find(contact_id) @inbox = @contact.account.inboxes.find(inbox_id) - return unless ['Channel::TwilioSms', 'Channel::Email', 'Channel::Api', 'Channel::Whatsapp'].include? @inbox.channel_type + return unless ['Channel::TwilioSms', 'Channel::Sms', 'Channel::Email', 'Channel::Api', 'Channel::Whatsapp'].include? @inbox.channel_type source_id = @source_id || generate_source_id create_contact_inbox(source_id) if source_id.present? @@ -13,12 +13,18 @@ class ContactInboxBuilder private def generate_source_id - return twilio_source_id if @inbox.channel_type == 'Channel::TwilioSms' - return wa_source_id if @inbox.channel_type == 'Channel::Whatsapp' - return @contact.email if @inbox.channel_type == 'Channel::Email' - return SecureRandom.uuid if @inbox.channel_type == 'Channel::Api' - - nil + case @inbox.channel_type + when 'Channel::TwilioSms' + twilio_source_id + when 'Channel::Whatsapp' + wa_source_id + when 'Channel::Email' + @contact.email + when 'Channel::Sms' + @contact.phone_number + when 'Channel::Api' + SecureRandom.uuid + end end def wa_source_id diff --git a/app/builders/messages/message_builder.rb b/app/builders/messages/message_builder.rb index f84e38ca7..9c26ccca1 100644 --- a/app/builders/messages/message_builder.rb +++ b/app/builders/messages/message_builder.rb @@ -29,11 +29,12 @@ class Messages::MessageBuilder return if @attachments.blank? @attachments.each do |uploaded_attachment| - @message.attachments.build( + attachment = @message.attachments.build( account_id: @message.account_id, - file_type: file_type(uploaded_attachment&.content_type), file: uploaded_attachment ) + + attachment.file_type = file_type(uploaded_attachment&.content_type) if uploaded_attachment.is_a?(ActionDispatch::Http::UploadedFile) end end diff --git a/app/builders/v2/report_builder.rb b/app/builders/v2/report_builder.rb index 82e6ce94c..c2900c7e6 100644 --- a/app/builders/v2/report_builder.rb +++ b/app/builders/v2/report_builder.rb @@ -2,6 +2,8 @@ class V2::ReportBuilder include DateRangeHelper attr_reader :account, :params + DEFAULT_GROUP_BY = 'day'.freeze + def initialize(account, params) @account = account @params = params @@ -64,26 +66,30 @@ class V2::ReportBuilder def conversations_count scope.conversations - .group_by_day(:created_at, range: range, default_value: 0) + .group_by_period(params[:group_by] || DEFAULT_GROUP_BY, + :created_at, range: range, default_value: 0, permit: %w[day week month year]) .count end def incoming_messages_count scope.messages.incoming.unscope(:order) - .group_by_day(:created_at, range: range, default_value: 0) + .group_by_period(params[:group_by] || DEFAULT_GROUP_BY, + :created_at, range: range, default_value: 0, permit: %w[day week month year]) .count end def outgoing_messages_count scope.messages.outgoing.unscope(:order) - .group_by_day(:created_at, range: range, default_value: 0) + .group_by_period(params[:group_by] || DEFAULT_GROUP_BY, + :created_at, range: range, default_value: 0, permit: %w[day week month year]) .count end def resolutions_count scope.conversations .resolved - .group_by_day(:created_at, range: range, default_value: 0) + .group_by_period(params[:group_by] || DEFAULT_GROUP_BY, + :created_at, range: range, default_value: 0, permit: %w[day week month year]) .count end diff --git a/app/controllers/api/v1/accounts/automation_rules_controller.rb b/app/controllers/api/v1/accounts/automation_rules_controller.rb index 0ec35c765..12b7b5957 100644 --- a/app/controllers/api/v1/accounts/automation_rules_controller.rb +++ b/app/controllers/api/v1/accounts/automation_rules_controller.rb @@ -1,21 +1,44 @@ class Api::V1::Accounts::AutomationRulesController < Api::V1::Accounts::BaseController before_action :check_authorization + before_action :fetch_automation_rule, only: [:show, :update, :destroy, :clone] def index - @automation_rules = Current.account.automation_rules + @automation_rules = Current.account.automation_rules.active end def create @automation_rule = Current.account.automation_rules.create(automation_rules_permit) end + def show; end + + def update + @automation_rule.update(automation_rules_permit) + end + + def destroy + @automation_rule.destroy! + head :ok + end + + def clone + automation_rule = Current.account.automation_rules.find_by(id: params[:automation_rule_id]) + new_rule = automation_rule.dup + new_rule.save + @automation_rule = new_rule + end + private def automation_rules_permit params.permit( :name, :description, :event_name, :account_id, conditions: [:attribute_key, :filter_operator, :query_operator, { values: [] }], - actions: [:action_name, { action_params: [:intiated_at] }] + actions: [:action_name, { action_params: [] }] ) end + + def fetch_automation_rule + @automation_rule = Current.account.automation_rules.find_by(id: params[:id]) + end end diff --git a/app/controllers/api/v1/accounts/campaigns_controller.rb b/app/controllers/api/v1/accounts/campaigns_controller.rb index 11b563d81..18d0998c8 100644 --- a/app/controllers/api/v1/accounts/campaigns_controller.rb +++ b/app/controllers/api/v1/accounts/campaigns_controller.rb @@ -18,7 +18,7 @@ class Api::V1::Accounts::CampaignsController < Api::V1::Accounts::BaseController def show; end def update - @campaign.update(campaign_params) + @campaign.update!(campaign_params) end private diff --git a/app/controllers/api/v1/accounts/conversations_controller.rb b/app/controllers/api/v1/accounts/conversations_controller.rb index 22eee629e..0515eabca 100644 --- a/app/controllers/api/v1/accounts/conversations_controller.rb +++ b/app/controllers/api/v1/accounts/conversations_controller.rb @@ -61,6 +61,7 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro else @status = @conversation.toggle_status end + assign_conversation if @conversation.status == 'open' && Current.user.is_a?(User) && Current.user&.agent? end def toggle_typing_status @@ -93,6 +94,11 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro @conversation.snoozed_until = parse_date_time(params[:snoozed_until].to_s) if params[:snoozed_until] end + def assign_conversation + @agent = Current.account.users.find(current_user.id) + @conversation.update_assignee(@agent) + end + def trigger_typing_event(event, is_private) user = current_user.presence || @resource Rails.configuration.dispatcher.dispatch(event, Time.zone.now, conversation: @conversation, user: user, is_private: is_private) diff --git a/app/controllers/api/v1/accounts/inboxes_controller.rb b/app/controllers/api/v1/accounts/inboxes_controller.rb index 9f2cfba8c..e0c963d25 100644 --- a/app/controllers/api/v1/accounts/inboxes_controller.rb +++ b/app/controllers/api/v1/accounts/inboxes_controller.rb @@ -2,6 +2,7 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController include Api::V1::InboxesHelper before_action :fetch_inbox, except: [:index, :create] before_action :fetch_agent_bot, only: [:set_agent_bot] + before_action :validate_limit, only: [:create] # we are already handling the authorization in fetch inbox before_action :check_authorization, except: [:show] @@ -91,20 +92,9 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController end def create_channel - case permitted_params[:channel][:type] - when 'web_widget' - Current.account.web_widgets.create!(permitted_params(Channel::WebWidget::EDITABLE_ATTRS)[:channel].except(:type)) - when 'api' - Current.account.api_channels.create!(permitted_params(Channel::Api::EDITABLE_ATTRS)[:channel].except(:type)) - when 'email' - Current.account.email_channels.create!(permitted_params(Channel::Email::EDITABLE_ATTRS)[:channel].except(:type)) - when 'line' - Current.account.line_channels.create!(permitted_params(Channel::Line::EDITABLE_ATTRS)[:channel].except(:type)) - when 'telegram' - Current.account.telegram_channels.create!(permitted_params(Channel::Telegram::EDITABLE_ATTRS)[:channel].except(:type)) - when 'whatsapp' - Current.account.whatsapp_channels.create!(permitted_params(Channel::Whatsapp::EDITABLE_ATTRS)[:channel].except(:type)) - end + return unless %w[web_widget api email line telegram whatsapp sms].include?(permitted_params[:channel][:type]) + + account_channels_method.create!(permitted_params(channel_type_from_params::EDITABLE_ATTRS)[:channel].except(:type)) end def update_channel_feature_flags @@ -123,6 +113,30 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController ) end + def channel_type_from_params + { + 'web_widget' => Channel::WebWidget, + 'api' => Channel::Api, + 'email' => Channel::Email, + 'line' => Channel::Line, + 'telegram' => Channel::Telegram, + 'whatsapp' => Channel::Whatsapp, + 'sms' => Channel::Sms + }[permitted_params[:channel][:type]] + end + + def account_channels_method + { + 'web_widget' => Current.account.web_widgets, + 'api' => Current.account.api_channels, + 'email' => Current.account.email_channels, + 'line' => Current.account.line_channels, + 'telegram' => Current.account.telegram_channels, + 'whatsapp' => Current.account.whatsapp_channels, + 'sms' => Current.account.sms_channels + }[permitted_params[:channel][:type]] + end + def get_channel_attributes(channel_type) if channel_type.constantize.const_defined?('EDITABLE_ATTRS') channel_type.constantize::EDITABLE_ATTRS.presence @@ -130,4 +144,10 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController [] end end + + def validate_limit + return unless Current.account.inboxes.count >= Current.account.usage_limits[:inboxes] + + render_payment_required('Account limit exceeded. Upgrade to a higher plan') + end end diff --git a/app/controllers/api/v1/profiles_controller.rb b/app/controllers/api/v1/profiles_controller.rb index b915f0775..fb9c3b62c 100644 --- a/app/controllers/api/v1/profiles_controller.rb +++ b/app/controllers/api/v1/profiles_controller.rb @@ -38,6 +38,7 @@ class Api::V1::ProfilesController < Api::BaseController :name, :display_name, :avatar, + :message_signature, ui_settings: {} ) end diff --git a/app/controllers/api/v1/widget/messages_controller.rb b/app/controllers/api/v1/widget/messages_controller.rb index 0d9ab6323..c2fa6304b 100644 --- a/app/controllers/api/v1/widget/messages_controller.rb +++ b/app/controllers/api/v1/widget/messages_controller.rb @@ -29,11 +29,12 @@ class Api::V1::Widget::MessagesController < Api::V1::Widget::BaseController return if params[:message][:attachments].blank? params[:message][:attachments].each do |uploaded_attachment| - @message.attachments.new( + attachment = @message.attachments.new( account_id: @message.account_id, - file_type: helpers.file_type(uploaded_attachment&.content_type), file: uploaded_attachment ) + + attachment.file_type = helpers.file_type(uploaded_attachment&.content_type) if uploaded_attachment.is_a?(ActionDispatch::Http::UploadedFile) end end diff --git a/app/controllers/api/v2/accounts/reports_controller.rb b/app/controllers/api/v2/accounts/reports_controller.rb index 4fabd587e..9801d760d 100644 --- a/app/controllers/api/v2/accounts/reports_controller.rb +++ b/app/controllers/api/v2/accounts/reports_controller.rb @@ -46,7 +46,8 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController type: params[:type].to_sym, since: params[:since], until: params[:until], - id: params[:id] + id: params[:id], + group_by: params[:group_by] } end @@ -56,7 +57,8 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController type: params[:type].to_sym, since: params[:since], until: params[:until], - id: params[:id] + id: params[:id], + group_by: params[:group_by] } end diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb index 783b138af..52c6c87af 100644 --- a/app/controllers/dashboard_controller.rb +++ b/app/controllers/dashboard_controller.rb @@ -25,7 +25,8 @@ class DashboardController < ActionController::Base 'API_CHANNEL_NAME', 'API_CHANNEL_THUMBNAIL', 'ANALYTICS_TOKEN', - 'ANALYTICS_HOST' + 'ANALYTICS_HOST', + 'DIRECT_UPLOADS_ENABLED' ).merge(app_config) end diff --git a/app/controllers/installation/onboarding_controller.rb b/app/controllers/installation/onboarding_controller.rb index 130938fd8..272b4e5eb 100644 --- a/app/controllers/installation/onboarding_controller.rb +++ b/app/controllers/installation/onboarding_controller.rb @@ -10,6 +10,7 @@ class Installation::OnboardingController < ApplicationController user_full_name: onboarding_params.dig(:user, :name), email: onboarding_params.dig(:user, :email), user_password: params.dig(:user, :password), + super_admin: true, confirmed: true ).perform rescue StandardError => e diff --git a/app/controllers/platform/api/v1/users_controller.rb b/app/controllers/platform/api/v1/users_controller.rb index 4ee14d0b6..960dee0e3 100644 --- a/app/controllers/platform/api/v1/users_controller.rb +++ b/app/controllers/platform/api/v1/users_controller.rb @@ -13,7 +13,8 @@ class Platform::Api::V1::UsersController < PlatformController end def login - render json: { url: "#{ENV['FRONTEND_URL']}/app/login?email=#{@resource.email}&sso_auth_token=#{@resource.generate_sso_auth_token}" } + encoded_email = ERB::Util.url_encode(@resource.email) + render json: { url: "#{ENV['FRONTEND_URL']}/app/login?email=#{encoded_email}&sso_auth_token=#{@resource.generate_sso_auth_token}" } end def show; end diff --git a/app/controllers/super_admin/accounts_controller.rb b/app/controllers/super_admin/accounts_controller.rb index 4d35fa1d8..28cae96b0 100644 --- a/app/controllers/super_admin/accounts_controller.rb +++ b/app/controllers/super_admin/accounts_controller.rb @@ -33,11 +33,11 @@ class SuperAdmin::AccountsController < SuperAdmin::ApplicationController # empty values into nil values. It uses other APIs such as `resource_class` # and `dashboard`: # - # def resource_params - # params.require(resource_class.model_name.param_key). - # permit(dashboard.permitted_attributes). - # transform_values { |value| value == "" ? nil : value } - # end + def resource_params + permitted_params = super + permitted_params[:limits] = permitted_params[:limits].to_h.compact + permitted_params + end # See https://administrate-prototype.herokuapp.com/customizing_controller_actions # for more information diff --git a/app/controllers/super_admin/devise/sessions_controller.rb b/app/controllers/super_admin/devise/sessions_controller.rb index de7002206..b5e50d177 100644 --- a/app/controllers/super_admin/devise/sessions_controller.rb +++ b/app/controllers/super_admin/devise/sessions_controller.rb @@ -8,7 +8,7 @@ class SuperAdmin::Devise::SessionsController < Devise::SessionsController def create redirect_to(super_admin_session_path, flash: { error: @error_message }) && return unless valid_credentials? - sign_in(@super_admin, scope: :super_admin) + sign_in(:super_admin, @super_admin) flash.discard redirect_to super_admin_users_path end diff --git a/app/controllers/super_admin/super_admins_controller.rb b/app/controllers/super_admin/super_admins_controller.rb deleted file mode 100644 index 16d91a151..000000000 --- a/app/controllers/super_admin/super_admins_controller.rb +++ /dev/null @@ -1,44 +0,0 @@ -class SuperAdmin::SuperAdminsController < SuperAdmin::ApplicationController - # Overwrite any of the RESTful controller actions to implement custom behavior - # For example, you may want to send an email after a foo is updated. - # - # def update - # super - # send_foo_updated_email(requested_resource) - # end - - # Override this method to specify custom lookup behavior. - # This will be used to set the resource for the `show`, `edit`, and `update` - # actions. - # - # def find_resource(param) - # Foo.find_by!(slug: param) - # end - - # The result of this lookup will be available as `requested_resource` - - # Override this if you have certain roles that require a subset - # this will be used to set the records shown on the `index` action. - # - # def scoped_resource - # if current_user.super_admin? - # resource_class - # else - # resource_class.with_less_stuff - # end - # end - - # Override `resource_params` if you want to transform the submitted - # data before it's persisted. For example, the following would turn all - # empty values into nil values. It uses other APIs such as `resource_class` - # and `dashboard`: - # - # def resource_params - # params.require(resource_class.model_name.param_key). - # permit(dashboard.permitted_attributes). - # transform_values { |value| value == "" ? nil : value } - # end - - # See https://administrate-prototype.herokuapp.com/customizing_controller_actions - # for more information -end diff --git a/app/controllers/super_admin/users_controller.rb b/app/controllers/super_admin/users_controller.rb index 613670849..23c212c29 100644 --- a/app/controllers/super_admin/users_controller.rb +++ b/app/controllers/super_admin/users_controller.rb @@ -33,12 +33,15 @@ class SuperAdmin::UsersController < SuperAdmin::ApplicationController # empty values into nil values. It uses other APIs such as `resource_class` # and `dashboard`: # - # def resource_params - # params.require(resource_class.model_name.param_key). - # permit(dashboard.permitted_attributes). - # transform_values { |value| value == "" ? nil : value } - # end + def resource_params + permitted_params = super + permitted_params.delete(:password) if permitted_params[:password].blank? + permitted_params + end # See https://administrate-prototype.herokuapp.com/customizing_controller_actions # for more information + def find_resource(param) + super.becomes(User) + end end diff --git a/app/controllers/webhooks/sms_controller.rb b/app/controllers/webhooks/sms_controller.rb new file mode 100644 index 000000000..914357dc9 --- /dev/null +++ b/app/controllers/webhooks/sms_controller.rb @@ -0,0 +1,6 @@ +class Webhooks::SmsController < ActionController::API + def process_payload + Webhooks::SmsEventsJob.perform_later(params['_json']&.first&.to_unsafe_hash) + head :ok + end +end diff --git a/app/controllers/widgets_controller.rb b/app/controllers/widgets_controller.rb index 29c1f97f9..ad7d2e3c0 100644 --- a/app/controllers/widgets_controller.rb +++ b/app/controllers/widgets_controller.rb @@ -10,7 +10,7 @@ class WidgetsController < ActionController::Base private def set_global_config - @global_config = GlobalConfig.get('LOGO_THUMBNAIL', 'BRAND_NAME', 'WIDGET_BRAND_URL') + @global_config = GlobalConfig.get('LOGO_THUMBNAIL', 'BRAND_NAME', 'WIDGET_BRAND_URL', 'DIRECT_UPLOADS_ENABLED') end def set_web_widget diff --git a/app/dashboards/account_dashboard.rb b/app/dashboards/account_dashboard.rb index 3cc35d0d3..408589597 100644 --- a/app/dashboards/account_dashboard.rb +++ b/app/dashboards/account_dashboard.rb @@ -7,6 +7,8 @@ class AccountDashboard < Administrate::BaseDashboard # Each different type represents an Administrate::Field object, # which determines how the attribute is displayed # on pages throughout the dashboard. + + enterprise_attribute_types = ChatwootApp.enterprise? ? { limits: Enterprise::AccountLimitsField } : {} ATTRIBUTE_TYPES = { id: Field::Number, name: Field::String, @@ -16,7 +18,7 @@ class AccountDashboard < Administrate::BaseDashboard conversations: CountField, locale: Field::Select.with_options(collection: LANGUAGES_CONFIG.map { |_x, y| y[:iso_639_1_code] }), account_users: Field::HasMany - }.freeze + }.merge(enterprise_attribute_types).freeze # COLLECTION_ATTRIBUTES # an array of attributes that will be displayed on the model's index page. @@ -33,7 +35,8 @@ class AccountDashboard < Administrate::BaseDashboard # SHOW_PAGE_ATTRIBUTES # an array of attributes that will be displayed on the model's show page. - SHOW_PAGE_ATTRIBUTES = %i[ + enterprise_show_page_attributes = ChatwootApp.enterprise? ? %i[limits] : [] + SHOW_PAGE_ATTRIBUTES = (%i[ id name created_at @@ -41,15 +44,16 @@ class AccountDashboard < Administrate::BaseDashboard locale conversations account_users - ].freeze + ] + enterprise_show_page_attributes).freeze # FORM_ATTRIBUTES # an array of attributes that will be displayed # on the model's form (`new` and `edit`) pages. - FORM_ATTRIBUTES = %i[ + enterprise_form_attributes = ChatwootApp.enterprise? ? %i[limits] : [] + FORM_ATTRIBUTES = (%i[ name locale - ].freeze + ] + enterprise_form_attributes).freeze # COLLECTION_FILTERS # a hash that defines filters that can be used while searching via the search @@ -69,4 +73,8 @@ class AccountDashboard < Administrate::BaseDashboard def display_resource(account) "##{account.id} #{account.name}" end + + def permitted_attributes + super + [limits: {}] + end end diff --git a/app/dashboards/super_admin_dashboard.rb b/app/dashboards/super_admin_dashboard.rb deleted file mode 100644 index 1d9afd1ba..000000000 --- a/app/dashboards/super_admin_dashboard.rb +++ /dev/null @@ -1,75 +0,0 @@ -require 'administrate/base_dashboard' - -class SuperAdminDashboard < Administrate::BaseDashboard - # ATTRIBUTE_TYPES - # a hash that describes the type of each of the model's fields. - # - # Each different type represents an Administrate::Field object, - # which determines how the attribute is displayed - # on pages throughout the dashboard. - ATTRIBUTE_TYPES = { - id: Field::Number, - email: Field::String, - password: Field::Password, - remember_created_at: Field::DateTime, - sign_in_count: Field::Number, - current_sign_in_at: Field::DateTime, - last_sign_in_at: Field::DateTime, - current_sign_in_ip: Field::String.with_options(searchable: false), - last_sign_in_ip: Field::String.with_options(searchable: false), - created_at: Field::DateTime, - updated_at: Field::DateTime - }.freeze - - # COLLECTION_ATTRIBUTES - # an array of attributes that will be displayed on the model's index page. - # - # By default, it's limited to four items to reduce clutter on index pages. - # Feel free to add, remove, or rearrange items. - COLLECTION_ATTRIBUTES = %i[ - id - email - ].freeze - - # SHOW_PAGE_ATTRIBUTES - # an array of attributes that will be displayed on the model's show page. - SHOW_PAGE_ATTRIBUTES = %i[ - id - email - remember_created_at - sign_in_count - current_sign_in_at - last_sign_in_at - current_sign_in_ip - last_sign_in_ip - created_at - updated_at - ].freeze - - # FORM_ATTRIBUTES - # an array of attributes that will be displayed - # on the model's form (`new` and `edit`) pages. - FORM_ATTRIBUTES = %i[ - email - password - ].freeze - - # COLLECTION_FILTERS - # a hash that defines filters that can be used while searching via the search - # field of the dashboard. - # - # For example to add an option to search for open resources by typing "open:" - # in the search field: - # - # COLLECTION_FILTERS = { - # open: ->(resources) { resources.where(open: true) } - # }.freeze - COLLECTION_FILTERS = {}.freeze - - # Overwrite this method to customize how super admins are displayed - # across all pages of the admin dashboard. - # - # def display_resource(super_admin) - # "SuperAdmin ##{super_admin.id}" - # end -end diff --git a/app/dashboards/user_dashboard.rb b/app/dashboards/user_dashboard.rb index f34f48257..1d59525f2 100644 --- a/app/dashboards/user_dashboard.rb +++ b/app/dashboards/user_dashboard.rb @@ -30,6 +30,7 @@ class UserDashboard < Administrate::BaseDashboard created_at: Field::DateTime, updated_at: Field::DateTime, pubsub_token: Field::String, + type: Field::Select.with_options(collection: [nil, 'SuperAdmin']), accounts: CountField }.freeze @@ -44,6 +45,7 @@ class UserDashboard < Administrate::BaseDashboard name email accounts + type ].freeze # SHOW_PAGE_ATTRIBUTES @@ -53,10 +55,12 @@ class UserDashboard < Administrate::BaseDashboard avatar_url unconfirmed_email name + type display_name email created_at updated_at + confirmed_at account_users ].freeze @@ -68,6 +72,8 @@ class UserDashboard < Administrate::BaseDashboard display_name email password + confirmed_at + type ].freeze # COLLECTION_FILTERS diff --git a/app/fields/enterprise/account_limits_field.rb b/app/fields/enterprise/account_limits_field.rb new file mode 100644 index 000000000..5833952ad --- /dev/null +++ b/app/fields/enterprise/account_limits_field.rb @@ -0,0 +1,7 @@ +require 'administrate/field/base' + +class Enterprise::AccountLimitsField < Administrate::Field::Base + def to_s + data.present? ? data.to_json : { agents: nil, inboxes: nil }.to_json + end +end diff --git a/app/finders/conversation_finder.rb b/app/finders/conversation_finder.rb index 3409a2450..58013f128 100644 --- a/app/finders/conversation_finder.rb +++ b/app/finders/conversation_finder.rb @@ -55,7 +55,7 @@ class ConversationFinder def set_inboxes @inbox_ids = if params[:inbox_id] - current_account.inboxes.where(id: params[:inbox_id]) + @current_user.assigned_inboxes.where(id: params[:inbox_id]) else @current_user.assigned_inboxes.pluck(:id) end diff --git a/app/helpers/api/v1/inboxes_helper.rb b/app/helpers/api/v1/inboxes_helper.rb index 08f000cc6..8cdf8d987 100644 --- a/app/helpers/api/v1/inboxes_helper.rb +++ b/app/helpers/api/v1/inboxes_helper.rb @@ -26,8 +26,48 @@ module Api::V1::InboxesHelper def validate_smtp(channel_data) return unless channel_data.key?('smtp_enabled') && channel_data[:smtp_enabled] - smtp = Net::SMTP.start(channel_data[:smtp_address], channel_data[:smtp_port], channel_data[:smtp_domain], channel_data[:smtp_email], - channel_data[:smtp_password], :login) + smtp = Net::SMTP.new(channel_data[:smtp_address], channel_data[:smtp_port]) + + set_smtp_encryption(channel_data, smtp) + + smtp.start(channel_data[:smtp_domain], channel_data[:smtp_email], channel_data[:smtp_password], :login) smtp.finish unless smtp&.nil? end + + def set_smtp_encryption(channel_data, smtp) + if channel_data[:smtp_enable_ssl_tls] + set_enable_tls(channel_data, smtp) + elsif channel_data[:smtp_enable_starttls_auto] + set_enable_starttls_auto(channel_data, smtp) + end + end + + def set_enable_starttls_auto(channel_data, smtp) + return unless smtp.respond_to?(:enable_starttls_auto) + + if channel_data[:smtp_openssl_verify_mode] + context = enable_openssl_mode(channel_data[:smtp_openssl_verify_mode]) + smtp.enable_starttls_auto(context) + else + smtp.enable_starttls_auto + end + end + + def set_enable_tls(channel_data, smtp) + return unless smtp.respond_to?(:enable_tls) + + if channel_data[:smtp_openssl_verify_mode] + context = enable_openssl_mode(channel_data[:smtp_openssl_verify_mode]) + smtp.enable_tls(context) + else + smtp.enable_tls + end + end + + def enable_openssl_mode(smtp_openssl_verify_mode) + openssl_verify_mode = "OpenSSL::SSL::VERIFY_#{smtp_openssl_verify_mode.upcase}".constantize if smtp_openssl_verify_mode.is_a?(String) + context = Net::SMTP.default_ssl_context + context.verify_mode = openssl_verify_mode + context + end end diff --git a/app/javascript/dashboard/api/auth.js b/app/javascript/dashboard/api/auth.js index e5ed77a34..7079614c1 100644 --- a/app/javascript/dashboard/api/auth.js +++ b/app/javascript/dashboard/api/auth.js @@ -138,13 +138,14 @@ export default { password, password_confirmation, displayName, + avatar, ...profileAttributes }) { const formData = new FormData(); Object.keys(profileAttributes).forEach(key => { - const value = profileAttributes[key]; - if (value) { - formData.append(`profile[${key}]`, value); + const hasValue = profileAttributes[key] === undefined; + if (!hasValue) { + formData.append(`profile[${key}]`, profileAttributes[key]); } }); formData.append('profile[display_name]', displayName || ''); @@ -152,6 +153,9 @@ export default { formData.append('profile[password]', password); formData.append('profile[password_confirmation]', password_confirmation); } + if (avatar) { + formData.append('profile[avatar]', avatar); + } return axios.put(endPoints('profileUpdate').url, formData); }, diff --git a/app/javascript/dashboard/api/automation.js b/app/javascript/dashboard/api/automation.js new file mode 100644 index 000000000..e83ece3d1 --- /dev/null +++ b/app/javascript/dashboard/api/automation.js @@ -0,0 +1,14 @@ +/* global axios */ +import ApiClient from './ApiClient'; + +class AutomationsAPI extends ApiClient { + constructor() { + super('automation_rules', { accountScoped: true }); + } + + clone(automationId) { + return axios.post(`${this.url}/${automationId}/clone`); + } +} + +export default new AutomationsAPI(); diff --git a/app/javascript/dashboard/api/customViews.js b/app/javascript/dashboard/api/customViews.js new file mode 100644 index 000000000..3fd4e12a7 --- /dev/null +++ b/app/javascript/dashboard/api/customViews.js @@ -0,0 +1,18 @@ +/* global axios */ +import ApiClient from './ApiClient'; + +class CustomViewsAPI extends ApiClient { + constructor() { + super('custom_filters', { accountScoped: true }); + } + + getCustomViewsByFilterType(type) { + return axios.get(`${this.url}?filter_type=${type}`); + } + + deleteCustomViews(id, type) { + return axios.delete(`${this.url}/${id}?filter_type=${type}`); + } +} + +export default new CustomViewsAPI(); diff --git a/app/javascript/dashboard/api/inbox/message.js b/app/javascript/dashboard/api/inbox/message.js index bfea9a2b9..f3c5e83e9 100644 --- a/app/javascript/dashboard/api/inbox/message.js +++ b/app/javascript/dashboard/api/inbox/message.js @@ -7,17 +7,19 @@ export const buildCreatePayload = ({ isPrivate, contentAttributes, echoId, - file, + files, ccEmails = '', bccEmails = '', }) => { let payload; - if (file) { + if (files && files.length !== 0) { payload = new FormData(); - payload.append('attachments[]', file, file.name); if (message) { payload.append('content', message); } + files.forEach(file => { + payload.append('attachments[]', file); + }); payload.append('private', isPrivate); payload.append('echo_id', echoId); payload.append('cc_emails', ccEmails); @@ -46,7 +48,7 @@ class MessageApi extends ApiClient { private: isPrivate, contentAttributes, echo_id: echoId, - file, + files, ccEmails = '', bccEmails = '', }) { @@ -58,7 +60,7 @@ class MessageApi extends ApiClient { isPrivate, contentAttributes, echoId, - file, + files, ccEmails, bccEmails, }), diff --git a/app/javascript/dashboard/api/reports.js b/app/javascript/dashboard/api/reports.js index faeee779f..dbb2bf08d 100644 --- a/app/javascript/dashboard/api/reports.js +++ b/app/javascript/dashboard/api/reports.js @@ -6,15 +6,15 @@ class ReportsAPI extends ApiClient { super('reports', { accountScoped: true, apiVersion: 'v2' }); } - getReports(metric, since, until, type = 'account', id) { + getReports(metric, since, until, type = 'account', id, group_by) { return axios.get(`${this.url}`, { - params: { metric, since, until, type, id }, + params: { metric, since, until, type, id, group_by }, }); } - getSummary(since, until, type = 'account', id) { + getSummary(since, until, type = 'account', id, group_by) { return axios.get(`${this.url}/summary`, { - params: { since, until, type, id }, + params: { since, until, type, id, group_by }, }); } diff --git a/app/javascript/dashboard/api/specs/automation.spec.js b/app/javascript/dashboard/api/specs/automation.spec.js new file mode 100644 index 000000000..2ab4cae82 --- /dev/null +++ b/app/javascript/dashboard/api/specs/automation.spec.js @@ -0,0 +1,15 @@ +import automations from '../automation'; +import ApiClient from '../ApiClient'; + +describe('#AutomationsAPI', () => { + it('creates correct instance', () => { + expect(automations).toBeInstanceOf(ApiClient); + expect(automations).toHaveProperty('get'); + expect(automations).toHaveProperty('show'); + expect(automations).toHaveProperty('create'); + expect(automations).toHaveProperty('update'); + expect(automations).toHaveProperty('delete'); + expect(automations).toHaveProperty('clone'); + expect(automations.url).toBe('/api/v1/automation_rules'); + }); +}); diff --git a/app/javascript/dashboard/api/specs/inbox/message.spec.js b/app/javascript/dashboard/api/specs/inbox/message.spec.js index dd9cba850..6953621cb 100644 --- a/app/javascript/dashboard/api/specs/inbox/message.spec.js +++ b/app/javascript/dashboard/api/specs/inbox/message.spec.js @@ -36,7 +36,7 @@ describe('#ConversationAPI', () => { echoId: 12, isPrivate: true, - file: new Blob(['test-content'], { type: 'application/pdf' }), + files: [new Blob(['test-content'], { type: 'application/pdf' })], }); expect(formPayload).toBeInstanceOf(FormData); expect(formPayload.get('content')).toEqual('test content'); diff --git a/app/javascript/dashboard/assets/scss/widgets/_conversation-view.scss b/app/javascript/dashboard/assets/scss/widgets/_conversation-view.scss index 1be9e31e0..3066d0e43 100644 --- a/app/javascript/dashboard/assets/scss/widgets/_conversation-view.scss +++ b/app/javascript/dashboard/assets/scss/widgets/_conversation-view.scss @@ -228,8 +228,14 @@ @include flex-align(right, null); .wrap { + align-items: flex-end; + display: flex; margin-right: $space-normal; text-align: right; + + .sender--info { + padding: var(--space-small) 0 var(--space-smaller) var(--space-small); + } } .bubble { diff --git a/app/javascript/dashboard/assets/scss/widgets/_snackbar.scss b/app/javascript/dashboard/assets/scss/widgets/_snackbar.scss index b714b051d..5f60d827c 100644 --- a/app/javascript/dashboard/assets/scss/widgets/_snackbar.scss +++ b/app/javascript/dashboard/assets/scss/widgets/_snackbar.scss @@ -15,7 +15,7 @@ @include shadow; background-color: $woot-snackbar-bg; border-radius: $space-smaller; - display: inline-block; + display: inline-flex; margin-bottom: $space-small; max-width: 40rem; min-height: 3rem; diff --git a/app/javascript/dashboard/components/ChatList.vue b/app/javascript/dashboard/components/ChatList.vue index bb7858e6c..4173ed438 100644 --- a/app/javascript/dashboard/components/ChatList.vue +++ b/app/javascript/dashboard/components/ChatList.vue @@ -1,26 +1,51 @@ -l diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/components/InputRadioGroup.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/components/InputRadioGroup.vue new file mode 100644 index 000000000..3326261fd --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/components/InputRadioGroup.vue @@ -0,0 +1,45 @@ + + + + + diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/components/SingleSelectDropdown.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/components/SingleSelectDropdown.vue new file mode 100644 index 000000000..850b5a863 --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/components/SingleSelectDropdown.vue @@ -0,0 +1,44 @@ + + diff --git a/app/javascript/dashboard/routes/dashboard/settings/profile/Index.vue b/app/javascript/dashboard/routes/dashboard/settings/profile/Index.vue index 053ac5e50..f9bfc6a91 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/profile/Index.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/profile/Index.vue @@ -66,6 +66,7 @@ +
@@ -95,13 +96,15 @@ import { mapGetters } from 'vuex'; import { clearCookiesOnLogout } from '../../../../store/utils/api'; import NotificationSettings from './NotificationSettings'; import alertMixin from 'shared/mixins/alertMixin'; -import ChangePassword from './ChangePassword.vue'; +import ChangePassword from './ChangePassword'; +import MessageSignature from './MessageSignature'; import globalConfigMixin from 'shared/mixins/globalConfigMixin'; export default { components: { NotificationSettings, ChangePassword, + MessageSignature, }, mixins: [alertMixin, globalConfigMixin], data() { diff --git a/app/javascript/dashboard/routes/dashboard/settings/profile/MessageSignature.vue b/app/javascript/dashboard/routes/dashboard/settings/profile/MessageSignature.vue new file mode 100644 index 000000000..e08e4fdee --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/profile/MessageSignature.vue @@ -0,0 +1,123 @@ + + + + + diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/CsatResponses.vue b/app/javascript/dashboard/routes/dashboard/settings/reports/CsatResponses.vue index 184d85a99..b893b763e 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/reports/CsatResponses.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/reports/CsatResponses.vue @@ -1,6 +1,6 @@ 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 46071f450..28321cdba 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/reports/components/ReportFilters.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/reports/components/ReportFilters.vue @@ -127,6 +127,24 @@ :placeholder="$t('REPORT.CUSTOM_DATE_RANGE.PLACEHOLDER')" @change="onChange" /> +
+ + +
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 6b26f94b9..449b0383f 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/reports/components/WootReports.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/reports/components/WootReports.vue @@ -12,8 +12,11 @@ v-if="filterItemsList" :type="type" :filter-items-list="filterItemsList" + :group-by-filter-items-list="groupByfilterItemsList" + :selected-group-by-filter="selectedGroupByFilter" @date-range-change="onDateRangeChange" @filter-change="onFilterChange" + @group-by-filter-change="onGroupByFilterChange" />
@@ -51,6 +54,7 @@ import ReportFilters from './ReportFilters'; import fromUnixTime from 'date-fns/fromUnixTime'; import format from 'date-fns/format'; +import { GROUP_BY_FILTER } from '../constants'; const REPORTS_KEYS = { CONVERSATIONS: 'conversations_count', @@ -88,6 +92,9 @@ export default { to: 0, currentSelection: 0, selectedFilter: null, + groupBy: GROUP_BY_FILTER[1], + groupByfilterItemsList: this.$t('REPORT.GROUP_BY_DAY_OPTIONS'), + selectedGroupByFilter: null, }; }, computed: { @@ -105,9 +112,28 @@ export default { return {}; } if (!this.accountReport.data.length) return {}; - const labels = this.accountReport.data.map(element => - format(fromUnixTime(element.timestamp), 'dd/MMM') - ); + const labels = this.accountReport.data.map(element => { + if (this.groupBy.period === GROUP_BY_FILTER[2].period) { + let week_date = new Date(fromUnixTime(element.timestamp)); + const first_day = week_date.getDate() - week_date.getDay(); + const last_day = first_day + 6; + + const week_first_date = new Date(week_date.setDate(first_day)); + const week_last_date = new Date(week_date.setDate(last_day)); + + return `${format(week_first_date, 'dd/MM/yy')} - ${format( + week_last_date, + 'dd/MM/yy' + )}`; + } + if (this.groupBy.period === GROUP_BY_FILTER[3].period) { + return format(fromUnixTime(element.timestamp), 'MMM-yyyy'); + } + if (this.groupBy.period === GROUP_BY_FILTER[4].period) { + return format(fromUnixTime(element.timestamp), 'yyyy'); + } + return format(fromUnixTime(element.timestamp), 'dd-MMM-yyyy'); + }); const data = this.accountReport.data.map(element => element.value); return { labels, @@ -148,24 +174,26 @@ export default { methods: { fetchAllData() { if (this.selectedFilter) { - const { from, to } = this; + const { from, to, groupBy } = this; this.$store.dispatch('fetchAccountSummary', { from, to, type: this.type, id: this.selectedFilter.id, + groupBy: groupBy.period, }); this.fetchChartData(); } }, fetchChartData() { - const { from, to } = this; + const { from, to, groupBy } = this; this.$store.dispatch('fetchAccountReport', { metric: this.metrics[this.currentSelection].KEY, from, to, type: this.type, id: this.selectedFilter.id, + groupBy: groupBy.period, }); }, downloadReports() { @@ -195,9 +223,19 @@ export default { this.currentSelection = index; this.fetchChartData(); }, - onDateRangeChange({ from, to }) { + onDateRangeChange({ from, to, groupBy }) { this.from = from; this.to = to; + this.groupByfilterItemsList = this.fetchFilterItems(groupBy); + const filterItems = this.groupByfilterItemsList.filter( + item => item.id === this.groupBy.id + ); + if (filterItems.length > 0) { + this.selectedGroupByFilter = filterItems[0]; + } else { + this.selectedGroupByFilter = this.groupByfilterItemsList[0]; + this.groupBy = GROUP_BY_FILTER[this.selectedGroupByFilter.id]; + } this.fetchAllData(); }, onFilterChange(payload) { @@ -206,6 +244,22 @@ export default { this.fetchAllData(); } }, + onGroupByFilterChange(payload) { + this.groupBy = GROUP_BY_FILTER[payload.id]; + this.fetchAllData(); + }, + fetchFilterItems(group_by) { + switch (group_by) { + case GROUP_BY_FILTER[2].period: + return this.$t('REPORT.GROUP_BY_WEEK_OPTIONS'); + case GROUP_BY_FILTER[3].period: + return this.$t('REPORT.GROUP_BY_MONTH_OPTIONS'); + case GROUP_BY_FILTER[4].period: + return this.$t('REPORT.GROUP_BY_YEAR_OPTIONS'); + default: + return this.$t('REPORT.GROUP_BY_DAY_OPTIONS'); + } + }, }, }; diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/constants.js b/app/javascript/dashboard/routes/dashboard/settings/reports/constants.js new file mode 100644 index 000000000..f285061cb --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/reports/constants.js @@ -0,0 +1,6 @@ +export const GROUP_BY_FILTER = { + 1: { id: 1, period: 'day' }, + 2: { id: 2, period: 'week' }, + 3: { id: 3, period: 'month' }, + 4: { id: 4, period: 'year' }, +}; diff --git a/app/javascript/dashboard/routes/dashboard/settings/settings.routes.js b/app/javascript/dashboard/routes/dashboard/settings/settings.routes.js index 87910efc8..110f26fba 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/settings.routes.js +++ b/app/javascript/dashboard/routes/dashboard/settings/settings.routes.js @@ -11,6 +11,7 @@ import reports from './reports/reports.routes'; import campaigns from './campaigns/campaigns.routes'; import teams from './teams/teams.routes'; import attributes from './attributes/attributes.routes'; +import automation from './automation/automation.routes'; import store from '../../../store'; export default { @@ -38,5 +39,6 @@ export default { ...campaigns.routes, ...integrationapps.routes, ...attributes.routes, + ...automation.routes, ], }; diff --git a/app/javascript/dashboard/routes/login/Login.vue b/app/javascript/dashboard/routes/login/Login.vue index c31bd5479..8c6869d7b 100644 --- a/app/javascript/dashboard/routes/login/Login.vue +++ b/app/javascript/dashboard/routes/login/Login.vue @@ -133,7 +133,9 @@ export default { login() { this.loginApi.showLoading = true; const credentials = { - email: this.email ? this.email : this.credentials.email, + email: this.email + ? decodeURIComponent(this.email) + : this.credentials.email, password: this.credentials.password, sso_auth_token: this.ssoAuthToken, }; diff --git a/app/javascript/dashboard/store/index.js b/app/javascript/dashboard/store/index.js index b14b9d0b0..88c36cdf8 100755 --- a/app/javascript/dashboard/store/index.js +++ b/app/javascript/dashboard/store/index.js @@ -31,6 +31,8 @@ import teams from './modules/teams'; import userNotificationSettings from './modules/userNotificationSettings'; import webhooks from './modules/webhooks'; import attributes from './modules/attributes'; +import automations from './modules/automations'; +import customViews from './modules/customViews'; Vue.use(Vuex); export default new Vuex.Store({ @@ -65,5 +67,7 @@ export default new Vuex.Store({ userNotificationSettings, webhooks, attributes, + automations, + customViews, }, }); diff --git a/app/javascript/dashboard/store/modules/auth.js b/app/javascript/dashboard/store/modules/auth.js index ffd3aee26..4d5c75323 100644 --- a/app/javascript/dashboard/store/modules/auth.js +++ b/app/javascript/dashboard/store/modules/auth.js @@ -65,6 +65,12 @@ export const getters = { getCurrentUser(_state) { return _state.currentUser; }, + + getMessageSignature(_state) { + const { message_signature: messageSignature } = _state.currentUser; + + return messageSignature || ''; + }, }; // actions diff --git a/app/javascript/dashboard/store/modules/automations.js b/app/javascript/dashboard/store/modules/automations.js new file mode 100644 index 000000000..a588d9ae7 --- /dev/null +++ b/app/javascript/dashboard/store/modules/automations.js @@ -0,0 +1,100 @@ +import * as MutationHelpers from 'shared/helpers/vuex/mutationHelpers'; +import types from '../mutation-types'; +import AutomationAPI from '../../api/automation'; + +export const state = { + records: [], + uiFlags: { + isFetching: false, + isCreating: false, + isDeleting: false, + isUpdating: false, + }, +}; + +export const getters = { + getAutomations(_state) { + return _state.records; + }, + getUIFlags(_state) { + return _state.uiFlags; + }, +}; + +export const actions = { + get: async function getAutomations({ commit }) { + commit(types.SET_AUTOMATION_UI_FLAG, { isFetching: true }); + try { + const response = await AutomationAPI.get(); + commit(types.SET_AUTOMATIONS, response.data.payload); + } catch (error) { + // Ignore error + } finally { + commit(types.SET_AUTOMATION_UI_FLAG, { isFetching: false }); + } + }, + create: async function createAutomation({ commit }, automationObj) { + commit(types.SET_AUTOMATION_UI_FLAG, { isCreating: true }); + try { + const response = await AutomationAPI.create(automationObj); + commit(types.ADD_AUTOMATION, response.data); + } catch (error) { + throw new Error(error); + } finally { + commit(types.SET_AUTOMATION_UI_FLAG, { isCreating: false }); + } + }, + update: async ({ commit }, { id, ...updateObj }) => { + commit(types.SET_AUTOMATION_UI_FLAG, { isUpdating: true }); + try { + const response = await AutomationAPI.update(id, updateObj); + commit(types.EDIT_AUTOMATION, response.data.payload); + } catch (error) { + throw new Error(error); + } finally { + commit(types.SET_AUTOMATION_UI_FLAG, { isUpdating: false }); + } + }, + delete: async ({ commit }, id) => { + commit(types.SET_AUTOMATION_UI_FLAG, { isDeleting: true }); + try { + await AutomationAPI.delete(id); + commit(types.DELETE_AUTOMATION, id); + } catch (error) { + throw new Error(error); + } finally { + commit(types.SET_AUTOMATION_UI_FLAG, { isDeleting: false }); + } + }, + clone: async ({ commit }, id) => { + commit(types.SET_AUTOMATION_UI_FLAG, { isCloning: true }); + try { + await AutomationAPI.clone(id); + } catch (error) { + throw new Error(error); + } finally { + commit(types.SET_AUTOMATION_UI_FLAG, { isCloning: false }); + } + }, +}; + +export const mutations = { + [types.SET_AUTOMATION_UI_FLAG](_state, data) { + _state.uiFlags = { + ..._state.uiFlags, + ...data, + }; + }, + [types.ADD_AUTOMATION]: MutationHelpers.create, + [types.SET_AUTOMATIONS]: MutationHelpers.set, + [types.EDIT_AUTOMATION]: MutationHelpers.update, + [types.DELETE_AUTOMATION]: MutationHelpers.destroy, +}; + +export default { + namespaced: true, + actions, + state, + getters, + mutations, +}; diff --git a/app/javascript/dashboard/store/modules/contactConversations.js b/app/javascript/dashboard/store/modules/contactConversations.js index 3c3280721..50bbb4f99 100644 --- a/app/javascript/dashboard/store/modules/contactConversations.js +++ b/app/javascript/dashboard/store/modules/contactConversations.js @@ -24,7 +24,14 @@ export const actions = { commit(types.default.SET_CONTACT_CONVERSATIONS_UI_FLAG, { isCreating: true, }); - const { inboxId, message, contactId, sourceId, mailSubject } = params; + const { + inboxId, + message, + contactId, + sourceId, + mailSubject, + assigneeId, + } = params; try { const { data } = await ConversationApi.create({ inbox_id: inboxId, @@ -34,11 +41,13 @@ export const actions = { mail_subject: mailSubject, }, message, + assignee_id: assigneeId, }); commit(types.default.ADD_CONTACT_CONVERSATION, { id: contactId, data, }); + return data; } catch (error) { throw new Error(error); } finally { diff --git a/app/javascript/dashboard/store/modules/customViews.js b/app/javascript/dashboard/store/modules/customViews.js new file mode 100644 index 000000000..40b126663 --- /dev/null +++ b/app/javascript/dashboard/store/modules/customViews.js @@ -0,0 +1,84 @@ +import * as MutationHelpers from 'shared/helpers/vuex/mutationHelpers'; +import types from '../mutation-types'; +import CustomViewsAPI from '../../api/customViews'; + +export const state = { + records: [], + uiFlags: { + isFetching: false, + isCreating: false, + isDeleting: false, + }, +}; + +export const getters = { + getUIFlags(_state) { + return _state.uiFlags; + }, + getCustomViews(_state) { + return _state.records; + }, + getCustomViewsByFilterType: _state => filterType => { + return _state.records.filter(record => record.filter_type === filterType); + }, +}; + +export const actions = { + get: async function getCustomViews({ commit }, filterType) { + commit(types.SET_CUSTOM_VIEW_UI_FLAG, { isFetching: true }); + try { + const response = await CustomViewsAPI.getCustomViewsByFilterType( + filterType + ); + commit(types.SET_CUSTOM_VIEW, response.data); + } catch (error) { + // Ignore error + } finally { + commit(types.SET_CUSTOM_VIEW_UI_FLAG, { isFetching: false }); + } + }, + create: async function createCustomViews({ commit }, obj) { + commit(types.SET_CUSTOM_VIEW_UI_FLAG, { isCreating: true }); + try { + const response = await CustomViewsAPI.create(obj); + commit(types.ADD_CUSTOM_VIEW, response.data); + } catch (error) { + const errorMessage = error?.response?.data?.message; + throw new Error(errorMessage); + } finally { + commit(types.SET_CUSTOM_VIEW_UI_FLAG, { isCreating: false }); + } + }, + delete: async ({ commit }, { id, filterType }) => { + commit(types.SET_CUSTOM_VIEW_UI_FLAG, { isDeleting: true }); + try { + await CustomViewsAPI.deleteCustomViews(id, filterType); + commit(types.DELETE_CUSTOM_VIEW, id); + } catch (error) { + throw new Error(error); + } finally { + commit(types.SET_CUSTOM_VIEW_UI_FLAG, { isDeleting: false }); + } + }, +}; + +export const mutations = { + [types.SET_CUSTOM_VIEW_UI_FLAG](_state, data) { + _state.uiFlags = { + ..._state.uiFlags, + ...data, + }; + }, + + [types.ADD_CUSTOM_VIEW]: MutationHelpers.create, + [types.SET_CUSTOM_VIEW]: MutationHelpers.set, + [types.DELETE_CUSTOM_VIEW]: MutationHelpers.destroy, +}; + +export default { + namespaced: true, + actions, + state, + getters, + mutations, +}; diff --git a/app/javascript/dashboard/store/modules/inboxes.js b/app/javascript/dashboard/store/modules/inboxes.js index 83102ba97..a180a5b80 100644 --- a/app/javascript/dashboard/store/modules/inboxes.js +++ b/app/javascript/dashboard/store/modules/inboxes.js @@ -12,13 +12,16 @@ const buildInboxData = inboxParams => { Object.keys(inboxProperties).forEach(key => { formData.append(key, inboxProperties[key]); }); - const { selectedFeatureFlags = [], ...channelParams } = channel; - if (selectedFeatureFlags.length) { - selectedFeatureFlags.forEach(featureFlag => { - formData.append(`channel[selected_feature_flags][]`, featureFlag); - }); - } else { - formData.append('channel[selected_feature_flags][]', ''); + const { selectedFeatureFlags, ...channelParams } = channel; + // selectedFeatureFlags needs to be empty when creating a website channel + if (selectedFeatureFlags) { + if (selectedFeatureFlags.length) { + selectedFeatureFlags.forEach(featureFlag => { + formData.append(`channel[selected_feature_flags][]`, featureFlag); + }); + } else { + formData.append('channel[selected_feature_flags][]', ''); + } } Object.keys(channelParams).forEach(key => { formData.append(`channel[${key}]`, channel[key]); @@ -75,9 +78,11 @@ export const getters = { item => item.channel_type === INBOX_TYPES.TWILIO ); }, - getTwilioSMSInboxes($state) { + getSMSInboxes($state) { return $state.records.filter( - item => item.channel_type === INBOX_TYPES.TWILIO && item.medium === 'sms' + item => + item.channel_type === INBOX_TYPES.SMS || + (item.channel_type === INBOX_TYPES.TWILIO && item.medium === 'sms') ); }, dialogFlowEnabledInboxes($state) { diff --git a/app/javascript/dashboard/store/modules/reports.js b/app/javascript/dashboard/store/modules/reports.js index 87050bcef..027b2388a 100644 --- a/app/javascript/dashboard/store/modules/reports.js +++ b/app/javascript/dashboard/store/modules/reports.js @@ -43,7 +43,8 @@ export const actions = { reportObj.from, reportObj.to, reportObj.type, - reportObj.id + reportObj.id, + reportObj.groupBy ).then(accountReport => { let { data } = accountReport; data = data.filter( @@ -68,7 +69,8 @@ export const actions = { reportObj.from, reportObj.to, reportObj.type, - reportObj.id + reportObj.id, + reportObj.groupBy ) .then(accountSummary => { commit(types.default.SET_ACCOUNT_SUMMARY, accountSummary.data); diff --git a/app/javascript/dashboard/store/modules/specs/auth/getters.spec.js b/app/javascript/dashboard/store/modules/specs/auth/getters.spec.js index 742b259dc..08e5d350f 100644 --- a/app/javascript/dashboard/store/modules/specs/auth/getters.spec.js +++ b/app/javascript/dashboard/store/modules/specs/auth/getters.spec.js @@ -37,4 +37,20 @@ describe('#getters', () => { }) ).toEqual({ is_contact_sidebar_open: true }); }); + describe('#getMessageSignature', () => { + it('Return signature when signature is present', () => { + expect( + getters.getMessageSignature({ + currentUser: { message_signature: 'Thanks' }, + }) + ).toEqual('Thanks'); + }); + it('Return empty string when signature is not present', () => { + expect( + getters.getMessageSignature({ + currentUser: {}, + }) + ).toEqual(''); + }); + }); }); diff --git a/app/javascript/dashboard/store/modules/specs/automations/actions.spec.js b/app/javascript/dashboard/store/modules/specs/automations/actions.spec.js new file mode 100644 index 000000000..035681ce7 --- /dev/null +++ b/app/javascript/dashboard/store/modules/specs/automations/actions.spec.js @@ -0,0 +1,107 @@ +import axios from 'axios'; +import { actions } from '../../automations'; +import * as types from '../../../mutation-types'; +import automationsList from './fixtures'; + +const commit = jest.fn(); +global.axios = axios; +jest.mock('axios'); + +describe('#actions', () => { + describe('#get', () => { + it('sends correct actions if API is success', async () => { + axios.get.mockResolvedValue({ data: { payload: automationsList } }); + await actions.get({ commit }); + expect(commit.mock.calls).toEqual([ + [types.default.SET_AUTOMATION_UI_FLAG, { isFetching: true }], + [types.default.SET_AUTOMATIONS, automationsList], + [types.default.SET_AUTOMATION_UI_FLAG, { isFetching: false }], + ]); + }); + it('sends correct actions if API is error', async () => { + axios.get.mockRejectedValue({ message: 'Incorrect header' }); + await actions.get({ commit }); + expect(commit.mock.calls).toEqual([ + [types.default.SET_AUTOMATION_UI_FLAG, { isFetching: true }], + [types.default.SET_AUTOMATION_UI_FLAG, { isFetching: false }], + ]); + }); + }); + + describe('#create', () => { + it('sends correct actions if API is success', async () => { + axios.post.mockResolvedValue({ data: automationsList[0] }); + await actions.create({ commit }, automationsList[0]); + expect(commit.mock.calls).toEqual([ + [types.default.SET_AUTOMATION_UI_FLAG, { isCreating: true }], + [types.default.ADD_AUTOMATION, automationsList[0]], + [types.default.SET_AUTOMATION_UI_FLAG, { isCreating: false }], + ]); + }); + it('sends correct actions if API is error', async () => { + axios.post.mockRejectedValue({ message: 'Incorrect header' }); + await expect(actions.create({ commit })).rejects.toThrow(Error); + expect(commit.mock.calls).toEqual([ + [types.default.SET_AUTOMATION_UI_FLAG, { isCreating: true }], + [types.default.SET_AUTOMATION_UI_FLAG, { isCreating: false }], + ]); + }); + }); + + describe('#update', () => { + it('sends correct actions if API is success', async () => { + axios.patch.mockResolvedValue({ + data: { payload: automationsList[0] }, + }); + await actions.update({ commit }, automationsList[0]); + expect(commit.mock.calls).toEqual([ + [types.default.SET_AUTOMATION_UI_FLAG, { isUpdating: true }], + [types.default.EDIT_AUTOMATION, automationsList[0]], + [types.default.SET_AUTOMATION_UI_FLAG, { isUpdating: false }], + ]); + }); + it('sends correct actions if API is error', async () => { + axios.patch.mockRejectedValue({ message: 'Incorrect header' }); + await expect( + actions.update({ commit }, automationsList[0]) + ).rejects.toThrow(Error); + expect(commit.mock.calls).toEqual([ + [types.default.SET_AUTOMATION_UI_FLAG, { isUpdating: true }], + [types.default.SET_AUTOMATION_UI_FLAG, { isUpdating: false }], + ]); + }); + }); + + describe('#delete', () => { + it('sends correct actions if API is success', async () => { + axios.delete.mockResolvedValue({ data: automationsList[0] }); + await actions.delete({ commit }, automationsList[0].id); + expect(commit.mock.calls).toEqual([ + [types.default.SET_AUTOMATION_UI_FLAG, { isDeleting: true }], + [types.default.DELETE_AUTOMATION, automationsList[0].id], + [types.default.SET_AUTOMATION_UI_FLAG, { isDeleting: false }], + ]); + }); + it('sends correct actions if API is error', async () => { + axios.delete.mockRejectedValue({ message: 'Incorrect header' }); + await expect( + actions.delete({ commit }, automationsList[0].id) + ).rejects.toThrow(Error); + expect(commit.mock.calls).toEqual([ + [types.default.SET_AUTOMATION_UI_FLAG, { isDeleting: true }], + [types.default.SET_AUTOMATION_UI_FLAG, { isDeleting: false }], + ]); + }); + }); + + describe('#clone', () => { + it('clones the automation', async () => { + axios.post.mockResolvedValue({ data: automationsList[0] }); + await actions.clone({ commit }, automationsList[0]); + expect(commit.mock.calls).toEqual([ + [types.default.SET_AUTOMATION_UI_FLAG, { isCloning: true }], + [types.default.SET_AUTOMATION_UI_FLAG, { isCloning: false }], + ]); + }); + }); +}); diff --git a/app/javascript/dashboard/store/modules/specs/automations/fixtures.js b/app/javascript/dashboard/store/modules/specs/automations/fixtures.js new file mode 100644 index 000000000..5e5b7943e --- /dev/null +++ b/app/javascript/dashboard/store/modules/specs/automations/fixtures.js @@ -0,0 +1,36 @@ +export default [ + { + name: 'Test 5', + description: 'Hello', + id: 46, + account_id: 1, + event_name: 'conversation_created', + conditions: [ + { + values: ['open'], + attribute_key: 'status', + filter_operator: 'equal_to', + }, + ], + actions: [{ action_name: 'add_label', action_params: ['testlabel'] }], + created_on: '2022-02-08T10:46:32.387Z', + active: true, + }, + { + id: 47, + account_id: 1, + name: 'Snooze', + description: 'Test Description', + event_name: 'conversation_created', + conditions: [ + { + values: ['pending'], + attribute_key: 'status', + filter_operator: 'equal_to', + }, + ], + actions: [{ action_name: 'assign_team', action_params: [1] }], + created_on: '2022-02-08T11:19:44.714Z', + active: true, + }, +]; diff --git a/app/javascript/dashboard/store/modules/specs/automations/getters.spec.js b/app/javascript/dashboard/store/modules/specs/automations/getters.spec.js new file mode 100644 index 000000000..46814f02a --- /dev/null +++ b/app/javascript/dashboard/store/modules/specs/automations/getters.spec.js @@ -0,0 +1,25 @@ +import { getters } from '../../automations'; +import automations from './fixtures'; +describe('#getters', () => { + it('getAutomations', () => { + const state = { records: automations }; + expect(getters.getAutomations(state)).toEqual(automations); + }); + + it('getUIFlags', () => { + const state = { + uiFlags: { + isFetching: true, + isCreating: false, + isUpdating: false, + isDeleting: false, + }, + }; + expect(getters.getUIFlags(state)).toEqual({ + isFetching: true, + isCreating: false, + isUpdating: false, + isDeleting: false, + }); + }); +}); diff --git a/app/javascript/dashboard/store/modules/specs/automations/mutations.spec.js b/app/javascript/dashboard/store/modules/specs/automations/mutations.spec.js new file mode 100644 index 000000000..1eaea6380 --- /dev/null +++ b/app/javascript/dashboard/store/modules/specs/automations/mutations.spec.js @@ -0,0 +1,36 @@ +import types from '../../../mutation-types'; +import { mutations } from '../../automations'; +import automations from './fixtures'; +describe('#mutations', () => { + describe('#SET_automations', () => { + it('set autonmation records', () => { + const state = { records: [] }; + mutations[types.SET_AUTOMATIONS](state, automations); + expect(state.records).toEqual(automations); + }); + }); + + describe('#ADD_AUTOMATION', () => { + it('push newly created automatuion to the store', () => { + const state = { records: [automations[0]] }; + mutations[types.ADD_AUTOMATION](state, automations[1]); + expect(state.records).toEqual([automations[0], automations[1]]); + }); + }); + + describe('#EDIT_AUTOMATION', () => { + it('update automation record', () => { + const state = { records: [automations[0]] }; + mutations[types.EDIT_AUTOMATION](state, automations[0]); + expect(state.records[0].name).toEqual('Test 5'); + }); + }); + + describe('#DELETE_AUTOMATION', () => { + it('delete automation record', () => { + const state = { records: [automations[0]] }; + mutations[types.DELETE_AUTOMATION](state, 46); + expect(state.records).toEqual([]); + }); + }); +}); diff --git a/app/javascript/dashboard/store/modules/specs/customViews/actions.spec.js b/app/javascript/dashboard/store/modules/specs/customViews/actions.spec.js new file mode 100644 index 000000000..5de9732c6 --- /dev/null +++ b/app/javascript/dashboard/store/modules/specs/customViews/actions.spec.js @@ -0,0 +1,70 @@ +import axios from 'axios'; +import { actions } from '../../customViews'; +import * as types from '../../../mutation-types'; +import customViewList from './fixtures'; + +const commit = jest.fn(); +global.axios = axios; +jest.mock('axios'); + +describe('#actions', () => { + describe('#get', () => { + it('sends correct actions if API is success', async () => { + axios.get.mockResolvedValue({ data: customViewList }); + await actions.get({ commit }); + expect(commit.mock.calls).toEqual([ + [types.default.SET_CUSTOM_VIEW_UI_FLAG, { isFetching: true }], + [types.default.SET_CUSTOM_VIEW, customViewList], + [types.default.SET_CUSTOM_VIEW_UI_FLAG, { isFetching: false }], + ]); + }); + it('sends correct actions if API is error', async () => { + axios.get.mockRejectedValue({ message: 'Incorrect header' }); + await actions.get({ commit }); + expect(commit.mock.calls).toEqual([ + [types.default.SET_CUSTOM_VIEW_UI_FLAG, { isFetching: true }], + [types.default.SET_CUSTOM_VIEW_UI_FLAG, { isFetching: false }], + ]); + }); + }); + + describe('#create', () => { + it('sends correct actions if API is success', async () => { + axios.post.mockResolvedValue({ data: customViewList[0] }); + await actions.create({ commit }, customViewList[0]); + expect(commit.mock.calls).toEqual([ + [types.default.SET_CUSTOM_VIEW_UI_FLAG, { isCreating: true }], + [types.default.ADD_CUSTOM_VIEW, customViewList[0]], + [types.default.SET_CUSTOM_VIEW_UI_FLAG, { isCreating: false }], + ]); + }); + it('sends correct actions if API is error', async () => { + axios.post.mockRejectedValue({ message: 'Incorrect header' }); + await expect(actions.create({ commit })).rejects.toThrow(Error); + expect(commit.mock.calls).toEqual([ + [types.default.SET_CUSTOM_VIEW_UI_FLAG, { isCreating: true }], + [types.default.SET_CUSTOM_VIEW_UI_FLAG, { isCreating: false }], + ]); + }); + }); + + describe('#delete', () => { + it('sends correct actions if API is success', async () => { + axios.delete.mockResolvedValue({ data: customViewList[0] }); + await actions.delete({ commit }, { id: 1, filterType: 'contact' }); + expect(commit.mock.calls).toEqual([ + [types.default.SET_CUSTOM_VIEW_UI_FLAG, { isDeleting: true }], + [types.default.DELETE_CUSTOM_VIEW, 1], + [types.default.SET_CUSTOM_VIEW_UI_FLAG, { isDeleting: false }], + ]); + }); + it('sends correct actions if API is error', async () => { + axios.delete.mockRejectedValue({ message: 'Incorrect header' }); + await expect(actions.delete({ commit }, 1)).rejects.toThrow(Error); + expect(commit.mock.calls).toEqual([ + [types.default.SET_CUSTOM_VIEW_UI_FLAG, { isDeleting: true }], + [types.default.SET_CUSTOM_VIEW_UI_FLAG, { isDeleting: false }], + ]); + }); + }); +}); diff --git a/app/javascript/dashboard/store/modules/specs/customViews/fixtures.js b/app/javascript/dashboard/store/modules/specs/customViews/fixtures.js new file mode 100644 index 000000000..1a620add9 --- /dev/null +++ b/app/javascript/dashboard/store/modules/specs/customViews/fixtures.js @@ -0,0 +1,36 @@ +export default [ + { + name: 'Custom view', + filter_type: 0, + query: { + payload: [ + { + attribute_key: 'assignee_id', + filter_operator: 'equal_to', + values: [45], + query_operator: 'and', + }, + { + attribute_key: 'inbox_id', + filter_operator: 'equal_to', + values: [144], + query_operator: 'and', + }, + ], + }, + }, + { + name: 'Custom view 1', + filter_type: 1, + query: { + payload: [ + { + attribute_key: 'name', + filter_operator: 'equal_to', + values: ['john doe'], + query_operator: null, + }, + ], + }, + }, +]; diff --git a/app/javascript/dashboard/store/modules/specs/customViews/getters.spec.js b/app/javascript/dashboard/store/modules/specs/customViews/getters.spec.js new file mode 100644 index 000000000..ec50a7d5d --- /dev/null +++ b/app/javascript/dashboard/store/modules/specs/customViews/getters.spec.js @@ -0,0 +1,79 @@ +import { getters } from '../../customViews'; +import customViewList from './fixtures'; + +describe('#getters', () => { + it('getCustomViews', () => { + const state = { records: customViewList }; + expect(getters.getCustomViews(state)).toEqual([ + { + name: 'Custom view', + filter_type: 0, + query: { + payload: [ + { + attribute_key: 'assignee_id', + filter_operator: 'equal_to', + values: [45], + query_operator: 'and', + }, + { + attribute_key: 'inbox_id', + filter_operator: 'equal_to', + values: [144], + query_operator: 'and', + }, + ], + }, + }, + { + name: 'Custom view 1', + filter_type: 1, + query: { + payload: [ + { + attribute_key: 'name', + filter_operator: 'equal_to', + values: ['john doe'], + query_operator: null, + }, + ], + }, + }, + ]); + }); + + it('getCustomViewsByFilterType', () => { + const state = { records: customViewList }; + expect(getters.getCustomViewsByFilterType(state)(1)).toEqual([ + { + name: 'Custom view 1', + filter_type: 1, + query: { + payload: [ + { + attribute_key: 'name', + filter_operator: 'equal_to', + values: ['john doe'], + query_operator: null, + }, + ], + }, + }, + ]); + }); + + it('getUIFlags', () => { + const state = { + uiFlags: { + isFetching: true, + isCreating: false, + isDeleting: false, + }, + }; + expect(getters.getUIFlags(state)).toEqual({ + isFetching: true, + isCreating: false, + isDeleting: false, + }); + }); +}); diff --git a/app/javascript/dashboard/store/modules/specs/customViews/mutations.spec.js b/app/javascript/dashboard/store/modules/specs/customViews/mutations.spec.js new file mode 100644 index 000000000..05da48b6d --- /dev/null +++ b/app/javascript/dashboard/store/modules/specs/customViews/mutations.spec.js @@ -0,0 +1,29 @@ +import types from '../../../mutation-types'; +import { mutations } from '../../customViews'; +import customViewList from './fixtures'; + +describe('#mutations', () => { + describe('#SET_CUSTOM_VIEW', () => { + it('set custom view records', () => { + const state = { records: [] }; + mutations[types.SET_CUSTOM_VIEW](state, customViewList); + expect(state.records).toEqual(customViewList); + }); + }); + + describe('#ADD_CUSTOM_VIEW', () => { + it('push newly created custom views to the store', () => { + const state = { records: [customViewList] }; + mutations[types.ADD_CUSTOM_VIEW](state, customViewList[0]); + expect(state.records).toEqual([customViewList, customViewList[0]]); + }); + }); + + describe('#DELETE_CUSTOM_VIEW', () => { + it('delete custom view record', () => { + const state = { records: [customViewList[0]] }; + mutations[types.DELETE_CUSTOM_VIEW](state, customViewList[0]); + expect(state.records).toEqual([customViewList[0]]); + }); + }); +}); diff --git a/app/javascript/dashboard/store/modules/specs/inboxes/fixtures.js b/app/javascript/dashboard/store/modules/specs/inboxes/fixtures.js index 9db92b00a..f7b06d232 100644 --- a/app/javascript/dashboard/store/modules/specs/inboxes/fixtures.js +++ b/app/javascript/dashboard/store/modules/specs/inboxes/fixtures.js @@ -55,4 +55,11 @@ export default [ website_token: 'randomid125', enable_auto_assignment: true, }, + { + id: 6, + channel_id: 6, + name: 'Test Widget 6', + channel_type: 'Channel::Sms', + provider: 'default', + }, ]; diff --git a/app/javascript/dashboard/store/modules/specs/inboxes/getters.spec.js b/app/javascript/dashboard/store/modules/specs/inboxes/getters.spec.js index 73d6624e0..e8af7dd58 100644 --- a/app/javascript/dashboard/store/modules/specs/inboxes/getters.spec.js +++ b/app/javascript/dashboard/store/modules/specs/inboxes/getters.spec.js @@ -19,14 +19,14 @@ describe('#getters', () => { expect(getters.getTwilioInboxes(state).length).toEqual(1); }); - it('getTwilioSMSInboxes', () => { + it('getSMSInboxes', () => { const state = { records: inboxList }; - expect(getters.getTwilioSMSInboxes(state).length).toEqual(1); + expect(getters.getSMSInboxes(state).length).toEqual(2); }); it('dialogFlowEnabledInboxes', () => { const state = { records: inboxList }; - expect(getters.dialogFlowEnabledInboxes(state).length).toEqual(5); + expect(getters.dialogFlowEnabledInboxes(state).length).toEqual(6); }); it('getInbox', () => { diff --git a/app/javascript/dashboard/store/mutation-types.js b/app/javascript/dashboard/store/mutation-types.js index b723776c5..734e0b5e5 100755 --- a/app/javascript/dashboard/store/mutation-types.js +++ b/app/javascript/dashboard/store/mutation-types.js @@ -188,4 +188,17 @@ export default { ADD_CUSTOM_ATTRIBUTE: 'ADD_CUSTOM_ATTRIBUTE', EDIT_CUSTOM_ATTRIBUTE: 'EDIT_CUSTOM_ATTRIBUTE', DELETE_CUSTOM_ATTRIBUTE: 'DELETE_CUSTOM_ATTRIBUTE', + + // Automations + SET_AUTOMATION_UI_FLAG: 'SET_AUTOMATION_UI_FLAG', + SET_AUTOMATIONS: 'SET_AUTOMATIONS', + ADD_AUTOMATION: 'ADD_AUTOMATION', + EDIT_AUTOMATION: 'EDIT_AUTOMATION', + DELETE_AUTOMATION: 'DELETE_AUTOMATION', + + // Custom Views + SET_CUSTOM_VIEW_UI_FLAG: 'SET_CUSTOM_VIEW_UI_FLAG', + SET_CUSTOM_VIEW: 'SET_CUSTOM_VIEW', + ADD_CUSTOM_VIEW: 'ADD_CUSTOM_VIEW', + DELETE_CUSTOM_VIEW: 'DELETE_CUSTOM_VIEW', }; diff --git a/app/javascript/packs/sdk.js b/app/javascript/packs/sdk.js index d666b864b..bd2d718aa 100755 --- a/app/javascript/packs/sdk.js +++ b/app/javascript/packs/sdk.js @@ -27,6 +27,10 @@ export const hasUserKeys = user => REQUIRED_USER_KEYS.reduce((acc, key) => acc || !!user[key], false); const runSDK = ({ baseUrl, websiteToken }) => { + if (window.$chatwoot) { + return; + } + const chatwootSettings = window.chatwootSettings || {}; window.$chatwoot = { baseUrl, diff --git a/app/javascript/packs/widget.js b/app/javascript/packs/widget.js index 7aeaf4545..f9f57e2ef 100644 --- a/app/javascript/packs/widget.js +++ b/app/javascript/packs/widget.js @@ -7,6 +7,7 @@ import ActionCableConnector from '../widget/helpers/actionCable'; import { getAlertAudio } from 'shared/helpers/AudioNotificationHelper'; import i18n from '../widget/i18n'; +import router from '../widget/router'; Vue.use(VueI18n); Vue.use(Vuelidate); @@ -22,6 +23,7 @@ Vue.config.productionTip = false; window.onload = () => { window.WOOT_WIDGET = new Vue({ + router, store, i18n: i18nConfig, render: h => h(App), diff --git a/app/javascript/sdk/IFrameHelper.js b/app/javascript/sdk/IFrameHelper.js index ff9bcca91..8dba6b13d 100644 --- a/app/javascript/sdk/IFrameHelper.js +++ b/app/javascript/sdk/IFrameHelper.js @@ -150,11 +150,14 @@ export const IFrameHelper = { onBubbleClick(bubbleState); }, + closeWindow: () => { + onBubbleClick({ toggleValue: false }); + removeUnreadClass(); + }, + onBubbleToggle: isOpen => { IFrameHelper.sendMessage('toggle-open', { isOpen }); - if (!isOpen) { - IFrameHelper.events.resetUnreadMode(); - } else { + if (isOpen) { IFrameHelper.pushEvent('webwidget.triggered'); } }, @@ -164,40 +167,18 @@ export const IFrameHelper = { referrerHost, }); }, - - setUnreadMode: message => { - const { unreadMessageCount } = message; - const { isOpen } = window.$chatwoot; - const toggleValue = true; - - if (!isOpen && unreadMessageCount > 0) { - IFrameHelper.sendMessage('set-unread-view'); - onBubbleClick({ toggleValue }); - addUnreadClass(); - } - }, - - setCampaignMode: () => { - const { isOpen } = window.$chatwoot; - const toggleValue = true; - if (!isOpen) { - onBubbleClick({ toggleValue }); - addUnreadClass(); - } - }, - updateIframeHeight: message => { const { extraHeight = 0, isFixedHeight } = message; - if (!extraHeight) return; IFrameHelper.setFrameHeightToFitContent(extraHeight, isFixedHeight); }, - resetUnreadMode: () => { - IFrameHelper.sendMessage('unset-unread-view'); - removeUnreadClass(); + setUnreadMode: () => { + addUnreadClass(); + onBubbleClick({ toggleValue: true }); }, + resetUnreadMode: () => removeUnreadClass(), handleNotificationDot: event => { if (window.$chatwoot.hideMessageBubble) { return; @@ -253,14 +234,10 @@ export const IFrameHelper = { } }, toggleCloseButton: () => { + let isMobile = false; if (window.matchMedia('(max-width: 668px)').matches) { - IFrameHelper.sendMessage('toggle-close-button', { - showClose: true, - }); - } else { - IFrameHelper.sendMessage('toggle-close-button', { - showClose: false, - }); + isMobile = true; } + IFrameHelper.sendMessage('toggle-close-button', { isMobile }); }, }; diff --git a/app/javascript/sdk/sdk.js b/app/javascript/sdk/sdk.js index 50f147c70..2e2aca7aa 100644 --- a/app/javascript/sdk/sdk.js +++ b/app/javascript/sdk/sdk.js @@ -18,6 +18,8 @@ export const SDK_CSS = `.woot-widget-holder { .woot-widget-holder.has-unread-view { border-radius: 0 !important; + min-height: 80px; + height: auto; bottom: 94px; box-shadow: none !important; } diff --git a/app/javascript/shared/components/FluentIcon/dashboard-icons.json b/app/javascript/shared/components/FluentIcon/dashboard-icons.json index 5f8a72633..3b896a86b 100644 --- a/app/javascript/shared/components/FluentIcon/dashboard-icons.json +++ b/app/javascript/shared/components/FluentIcon/dashboard-icons.json @@ -14,16 +14,28 @@ "arrow-up-outline": "M4.21 10.733a.75.75 0 0 0 1.086 1.034l5.954-6.251V20.25a.75.75 0 0 0 1.5 0V5.516l5.955 6.251a.75.75 0 0 0 1.086-1.034l-7.067-7.42a.995.995 0 0 0-.58-.3.754.754 0 0 0-.29.001.995.995 0 0 0-.578.3L4.21 10.733Z", "attach-outline": "M11.772 3.743a6 6 0 0 1 8.66 8.302l-.19.197-8.8 8.798-.036.03a3.723 3.723 0 0 1-5.489-4.973.764.764 0 0 1 .085-.13l.054-.06.086-.088.142-.148.002.003 7.436-7.454a.75.75 0 0 1 .977-.074l.084.073a.75.75 0 0 1 .074.976l-.073.084-7.594 7.613a2.23 2.23 0 0 0 3.174 3.106l8.832-8.83A4.502 4.502 0 0 0 13 4.644l-.168.16-.013.014-9.536 9.536a.75.75 0 0 1-1.133-.977l.072-.084 9.549-9.55h.002Z", "autocorrect-outline": "M13.461 4.934c.293.184.548.42.752.698l.117.171 2.945 4.696H21.5a.75.75 0 0 1 .743.649l.007.102a.75.75 0 0 1-.75.75l-3.284-.001.006.009-.009-.01a4.75 4.75 0 1 1-3.463-1.5h.756L13.059 6.6a1.25 1.25 0 0 0-2.04-.112l-.078.112-7.556 12.048a.75.75 0 0 1-1.322-.699l.052-.098L9.67 5.803a2.75 2.75 0 0 1 3.791-.869ZM14.751 12a3.25 3.25 0 1 0 0 6.5 3.25 3.25 0 0 0 0-6.5Z", + "automation-outline": [ + "M21.78 3.28a.75.75 0 0 0-1.06-1.06l-2.012 2.012a4.251 4.251 0 0 0-5.463.462l-1.068 1.069a1.75 1.75 0 0 0 0 2.474l3.585 3.586a1.75 1.75 0 0 0 2.475 0l1.068-1.068a4.251 4.251 0 0 0 .463-5.463L21.78 3.28zm-3.585 2.475l.022.023l.003.002l.002.003l.023.022a2.75 2.75 0 0 1 0 3.89l-1.068 1.067a.25.25 0 0 1-.354 0l-3.586-3.585a.25.25 0 0 1 0-.354l1.068-1.068a2.75 2.75 0 0 1 3.89 0z", + "M10.78 11.28a.75.75 0 1 0-1.06-1.06L8 11.94l-.47-.47a.75.75 0 0 0-1.06 0l-1.775 1.775a4.251 4.251 0 0 0-.463 5.463L2.22 20.72a.75.75 0 1 0 1.06 1.06l2.012-2.012a4.251 4.251 0 0 0 5.463-.463l1.775-1.775a.75.75 0 0 0 0-1.06l-.47-.47l1.72-1.72a.75.75 0 1 0-1.06-1.06L11 14.94L9.06 13l1.72-1.72zm-3.314 2.247l.004.003l.003.004l2.993 2.993l.004.003l.003.004l.466.466l-1.244 1.245a2.75 2.75 0 0 1-3.89 0l-.05-.05a2.75 2.75 0 0 1 0-3.89L7 13.062l.466.466z" + ], "book-contacts-outline": "M15.5 12.25a.75.75 0 0 0-.75-.75h-5a.75.75 0 0 0-.75.75v.5c0 1 1.383 1.75 3.25 1.75s3.25-.75 3.25-1.75v-.5ZM14 8.745C14 7.78 13.217 7 12.25 7s-1.75.779-1.75 1.745a1.75 1.75 0 1 0 3.5 0ZM4 4.5A2.5 2.5 0 0 1 6.5 2H18a2.5 2.5 0 0 1 2.5 2.5v14.25a.75.75 0 0 1-.75.75H5.5a1 1 0 0 0 1 1h13.25a.75.75 0 0 1 0 1.5H6.5A2.5 2.5 0 0 1 4 19.5v-15Zm1.5 0V18H19V4.5a1 1 0 0 0-1-1H6.5a1 1 0 0 0-1 1Z", - "book-clock-outline": ["M13 9.125v1.625h.75a.625.625 0 1 1 0 1.25H12.5a.615.615 0 0 1-.063-.003.625.625 0 0 1-.688-.622v-2.25a.625.625 0 1 1 1.251 0Z", "M12.375 6.005a4.75 4.75 0 1 0 0 9.5 4.75 4.75 0 0 0 0-9.5Zm-3.5 4.75a3.5 3.5 0 1 1 7 0 3.5 3.5 0 0 1-7 0Z", "M6.5 2A2.5 2.5 0 0 0 4 4.5v15A2.5 2.5 0 0 0 6.5 22h13.25a.75.75 0 0 0 0-1.5H6.5a1 1 0 0 1-1-1h14.25a.75.75 0 0 0 .75-.75V4.5A2.5 2.5 0 0 0 18 2H6.5ZM19 18H5.5V4.5a1 1 0 0 1 1-1H18a1 1 0 0 1 1 1V18Z"], + "book-clock-outline": [ + "M13 9.125v1.625h.75a.625.625 0 1 1 0 1.25H12.5a.615.615 0 0 1-.063-.003.625.625 0 0 1-.688-.622v-2.25a.625.625 0 1 1 1.251 0Z", + "M12.375 6.005a4.75 4.75 0 1 0 0 9.5 4.75 4.75 0 0 0 0-9.5Zm-3.5 4.75a3.5 3.5 0 1 1 7 0 3.5 3.5 0 0 1-7 0Z", + "M6.5 2A2.5 2.5 0 0 0 4 4.5v15A2.5 2.5 0 0 0 6.5 22h13.25a.75.75 0 0 0 0-1.5H6.5a1 1 0 0 1-1-1h14.25a.75.75 0 0 0 .75-.75V4.5A2.5 2.5 0 0 0 18 2H6.5ZM19 18H5.5V4.5a1 1 0 0 1 1-1H18a1 1 0 0 1 1 1V18Z" + ], "building-bank-outline": "M13.032 2.325a1.75 1.75 0 0 0-2.064 0L3.547 7.74c-.978.713-.473 2.26.736 2.26H4.5v5.8A2.75 2.75 0 0 0 3 18.25v1.5c0 .413.336.75.75.75h16.5a.75.75 0 0 0 .75-.75v-1.5a2.75 2.75 0 0 0-1.5-2.45V10h.217c1.21 0 1.713-1.547.736-2.26l-7.421-5.416Zm-1.18 1.211a.25.25 0 0 1 .295 0L18.95 8.5H5.05l6.803-4.964ZM18 10v5.5h-2V10h2Zm-3.5 0v5.5h-1.75V10h1.75Zm-3.25 0v5.5H9.5V10h1.75Zm-5.5 7h12.5c.69 0 1.25.56 1.25 1.25V19h-15v-.75c0-.69.56-1.25 1.25-1.25ZM6 15.5V10h2v5.5H6Z", - "calendar-clock-outline": ["M21 6.25A3.25 3.25 0 0 0 17.75 3H6.25A3.25 3.25 0 0 0 3 6.25v11.5A3.25 3.25 0 0 0 6.25 21h5.772a6.471 6.471 0 0 1-.709-1.5H6.25a1.75 1.75 0 0 1-1.75-1.75V8.5h15v2.813a6.471 6.471 0 0 1 1.5.709V6.25ZM6.25 4.5h11.5c.966 0 1.75.784 1.75 1.75V7h-15v-.75c0-.966.784-1.75 1.75-1.75Z", "M23 17.5a5.5 5.5 0 1 0-11 0 5.5 5.5 0 0 0 11 0Zm-5.5 0h2a.5.5 0 0 1 0 1H17a.5.5 0 0 1-.5-.491v-3.01a.5.5 0 0 1 1 0V17.5Z"], + "calendar-clock-outline": [ + "M21 6.25A3.25 3.25 0 0 0 17.75 3H6.25A3.25 3.25 0 0 0 3 6.25v11.5A3.25 3.25 0 0 0 6.25 21h5.772a6.471 6.471 0 0 1-.709-1.5H6.25a1.75 1.75 0 0 1-1.75-1.75V8.5h15v2.813a6.471 6.471 0 0 1 1.5.709V6.25ZM6.25 4.5h11.5c.966 0 1.75.784 1.75 1.75V7h-15v-.75c0-.966.784-1.75 1.75-1.75Z", + "M23 17.5a5.5 5.5 0 1 0-11 0 5.5 5.5 0 0 0 11 0Zm-5.5 0h2a.5.5 0 0 1 0 1H17a.5.5 0 0 1-.5-.491v-3.01a.5.5 0 0 1 1 0V17.5Z" + ], "call-outline": "m7.056 2.418 1.167-.351a2.75 2.75 0 0 1 3.302 1.505l.902 2.006a2.75 2.75 0 0 1-.633 3.139L10.3 10.11a.25.25 0 0 0-.078.155c-.044.397.225 1.17.845 2.245.451.781.86 1.33 1.207 1.637.242.215.375.261.432.245l2.01-.615a2.75 2.75 0 0 1 3.034 1.02l1.281 1.776a2.75 2.75 0 0 1-.339 3.605l-.886.84a3.75 3.75 0 0 1-3.587.889c-2.754-.769-5.223-3.093-7.435-6.924-2.215-3.836-2.992-7.14-2.276-9.913a3.75 3.75 0 0 1 2.548-2.652Zm.433 1.437a2.25 2.25 0 0 0-1.529 1.59c-.602 2.332.087 5.261 2.123 8.788 2.033 3.522 4.222 5.582 6.54 6.23a2.25 2.25 0 0 0 2.151-.534l.887-.84a1.25 1.25 0 0 0 .154-1.639l-1.28-1.775a1.25 1.25 0 0 0-1.38-.464l-2.015.617c-1.17.348-2.232-.593-3.372-2.568C9 11.93 8.642 10.9 8.731 10.099c.047-.416.24-.8.546-1.086l1.494-1.393a1.25 1.25 0 0 0 .288-1.427l-.902-2.006a1.25 1.25 0 0 0-1.5-.684l-1.168.352Z", "chat-help-outline": "M12 2c5.523 0 10 4.477 10 10s-4.477 10-10 10a9.96 9.96 0 0 1-4.587-1.112l-3.826 1.067a1.25 1.25 0 0 1-1.54-1.54l1.068-3.823A9.96 9.96 0 0 1 2 12C2 6.477 6.477 2 12 2Zm0 1.5A8.5 8.5 0 0 0 3.5 12c0 1.47.373 2.883 1.073 4.137l.15.27-1.112 3.984 3.987-1.112.27.15A8.5 8.5 0 1 0 12 3.5Zm0 12a1 1 0 1 1 0 2 1 1 0 0 1 0-2Zm0-8.75a2.75 2.75 0 0 1 2.75 2.75c0 1.01-.297 1.574-1.051 2.359l-.169.171c-.622.622-.78.886-.78 1.47a.75.75 0 0 1-1.5 0c0-1.01.297-1.574 1.051-2.359l.169-.171c.622-.622.78-.886.78-1.47a1.25 1.25 0 0 0-2.493-.128l-.007.128a.75.75 0 0 1-1.5 0A2.75 2.75 0 0 1 12 6.75Z", "chat-multiple-outline": "M9.562 3a7.5 7.5 0 0 0-6.798 10.673l-.724 2.842a1.25 1.25 0 0 0 1.504 1.524c.75-.18 1.903-.457 2.93-.702A7.5 7.5 0 1 0 9.561 3Zm-6 7.5a6 6 0 1 1 3.33 5.375l-.244-.121-.264.063c-.923.22-1.99.475-2.788.667l.69-2.708.07-.276-.13-.253a5.971 5.971 0 0 1-.664-2.747Zm11 10.5c-1.97 0-3.762-.759-5.1-2h.1c.718 0 1.415-.089 2.08-.257.865.482 1.86.757 2.92.757.96 0 1.866-.225 2.67-.625l.243-.121.264.063c.922.22 1.966.445 2.74.61-.175-.751-.414-1.756-.642-2.651l-.07-.276.13-.253a5.971 5.971 0 0 0 .665-2.747 5.995 5.995 0 0 0-2.747-5.042 8.44 8.44 0 0 0-.8-2.047 7.503 7.503 0 0 1 4.344 10.263c.253 1.008.509 2.1.671 2.803a1.244 1.244 0 0 1-1.467 1.5 132.62 132.62 0 0 1-2.913-.64 7.476 7.476 0 0 1-3.088.663Z", "chat-outline": "M12 2c5.523 0 10 4.477 10 10s-4.477 10-10 10a9.96 9.96 0 0 1-4.587-1.112l-3.826 1.067a1.25 1.25 0 0 1-1.54-1.54l1.068-3.823A9.96 9.96 0 0 1 2 12C2 6.477 6.477 2 12 2Zm0 1.5A8.5 8.5 0 0 0 3.5 12c0 1.47.373 2.883 1.073 4.137l.15.27-1.112 3.984 3.987-1.112.27.15A8.5 8.5 0 1 0 12 3.5ZM8.75 13h4.498a.75.75 0 0 1 .102 1.493l-.102.007H8.75a.75.75 0 0 1-.102-1.493L8.75 13h4.498H8.75Zm0-3.5h6.505a.75.75 0 0 1 .101 1.493l-.101.007H8.75a.75.75 0 0 1-.102-1.493L8.75 9.5h6.505H8.75Z", "checkmark-circle-outline": "M12 2c5.523 0 10 4.477 10 10s-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2Zm0 1.5a8.5 8.5 0 1 0 0 17 8.5 8.5 0 0 0 0-17Zm-1.25 9.94 4.47-4.47a.75.75 0 0 1 1.133.976l-.073.084-5 5a.75.75 0 0 1-.976.073l-.084-.073-2.5-2.5a.75.75 0 0 1 .976-1.133l.084.073 1.97 1.97 4.47-4.47-4.47 4.47Z", "checkmark-circle-solid": "M12 2c5.523 0 10 4.477 10 10s-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2Zm3.22 6.97-4.47 4.47-1.97-1.97a.75.75 0 0 0-1.06 1.06l2.5 2.5a.75.75 0 0 0 1.06 0l5-5a.75.75 0 1 0-1.06-1.06Z", + "checkmark-square-solid": "M18 3a3 3 0 0 1 3 3v12a3 3 0 0 1-3 3H6a3 3 0 0 1-3-3V6a3 3 0 0 1 3-3h12zm-1.53 4.97L10 14.44l-2.47-2.47a.75.75 0 0 0-1.06 1.06l3 3a.75.75 0 0 0 1.06 0l7-7a.75.75 0 0 0-1.06-1.06z", "checkmark-outline": "M4.53 12.97a.75.75 0 0 0-1.06 1.06l4.5 4.5a.75.75 0 0 0 1.06 0l11-11a.75.75 0 0 0-1.06-1.06L8.5 16.94l-3.97-3.97Z", "chevron-down-outline": "M4.22 8.47a.75.75 0 0 1 1.06 0L12 15.19l6.72-6.72a.75.75 0 1 1 1.06 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L4.22 9.53a.75.75 0 0 1 0-1.06Z", "chevron-left-outline": "M15.53 4.22a.75.75 0 0 1 0 1.06L8.81 12l6.72 6.72a.75.75 0 1 1-1.06 1.06l-7.25-7.25a.75.75 0 0 1 0-1.06l7.25-7.25a.75.75 0 0 1 1.06 0Z", @@ -34,6 +46,17 @@ "cloud-outline": "M6.087 9.75a5.752 5.752 0 0 1 11.326 0h.087a4 4 0 0 1 0 8H6a4 4 0 0 1 0-8h.087ZM11.75 6.5a4.25 4.25 0 0 0-4.245 4.037.75.75 0 0 1-.749.713H6a2.5 2.5 0 0 0 0 5h11.5a2.5 2.5 0 0 0 0-5h-.756a.75.75 0 0 1-.75-.713A4.25 4.25 0 0 0 11.75 6.5Z", "code-outline": "m8.066 18.943 6.5-14.5a.75.75 0 0 1 1.404.518l-.036.096-6.5 14.5a.75.75 0 0 1-1.404-.518l.036-.096 6.5-14.5-6.5 14.5ZM2.22 11.47l4.25-4.25a.75.75 0 0 1 1.133.976l-.073.085L3.81 12l3.72 3.719a.75.75 0 0 1-.976 1.133l-.084-.073-4.25-4.25a.75.75 0 0 1-.073-.976l.073-.084 4.25-4.25-4.25 4.25Zm14.25-4.25a.75.75 0 0 1 .976-.073l.084.073 4.25 4.25a.75.75 0 0 1 .073.976l-.073.085-4.25 4.25a.75.75 0 0 1-1.133-.977l.073-.084L20.19 12l-3.72-3.72a.75.75 0 0 1 0-1.06Z", "contact-card-group-outline": "M18.75 4A3.25 3.25 0 0 1 22 7.25v9.505a3.25 3.25 0 0 1-3.25 3.25H5.25A3.25 3.25 0 0 1 2 16.755V7.25a3.25 3.25 0 0 1 3.066-3.245L5.25 4h13.5Zm0 1.5H5.25l-.144.006A1.75 1.75 0 0 0 3.5 7.25v9.505c0 .966.784 1.75 1.75 1.75h13.5a1.75 1.75 0 0 0 1.75-1.75V7.25a1.75 1.75 0 0 0-1.75-1.75Zm-9.497 7a.75.75 0 0 1 .75.75v.582c0 1.272-.969 1.918-2.502 1.918S5 15.104 5 13.831v-.581a.75.75 0 0 1 .75-.75h3.503Zm1.58-.001 1.417.001a.75.75 0 0 1 .75.75v.333c0 .963-.765 1.417-1.875 1.417-.116 0-.229-.005-.337-.015a2.85 2.85 0 0 0 .206-.9l.009-.253v-.582c0-.269-.061-.524-.17-.751Zm4.417.001h3a.75.75 0 0 1 .102 1.493L18.25 14h-3a.75.75 0 0 1-.102-1.493l.102-.007h3-3Zm-7.75-4a1.5 1.5 0 1 1 0 3.001 1.5 1.5 0 0 1 0-3.001Zm3.87.502a1.248 1.248 0 1 1 0 2.496 1.248 1.248 0 0 1 0-2.496Zm3.88.498h3a.75.75 0 0 1 .102 1.493L18.25 11h-3a.75.75 0 0 1-.102-1.493l.102-.007h3-3Z", + "copy-outline": [ + "M8 3a1 1 0 0 0-1 1v.5a.5.5 0 0 1-1 0V4a2 2 0 0 1 2-2h.5a.5.5 0 0 1 0 1H8z", + "M7 12a1 1 0 0 0 1 1h.5a.5.5 0 0 1 0 1H8a2 2 0 0 1-2-2v-.5a.5.5 0 0 1 1 0v.5z", + "M7 6.5a.5.5 0 0 0-1 0v3a.5.5 0 0 0 1 0v-3z", + "M16 3a1 1 0 0 1 1 1v.5a.5.5 0 0 0 1 0V4a2 2 0 0 0-2-2h-.5a.5.5 0 0 0 0 1h.5z", + "M16 13a1 1 0 0 0 1-1v-.5a.5.5 0 0 1 1 0v.5a2 2 0 0 1-2 2h-.5a.5.5 0 0 1 0-1h.5z", + "M17.5 6a.5.5 0 0 0-.5.5v3a.5.5 0 0 0 1 0v-3a.5.5 0 0 0-.5-.5z", + "M10.5 2a.5.5 0 0 0 0 1h3a.5.5 0 0 0 0-1h-3z", + "M10 13.5a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 0 1h-3a.5.5 0 0 1-.5-.5z", + "M4 6h1v6.5A2.5 2.5 0 0 0 7.5 15H14v1a2 2 0 0 1-2 2H5.5A3.5 3.5 0 0 1 2 14.5V8a2 2 0 0 1 2-2z" + ], "delete-outline": "M12 1.75a3.25 3.25 0 0 1 3.245 3.066L15.25 5h5.25a.75.75 0 0 1 .102 1.493L20.5 6.5h-.796l-1.28 13.02a2.75 2.75 0 0 1-2.561 2.474l-.176.006H8.313a2.75 2.75 0 0 1-2.714-2.307l-.023-.174L4.295 6.5H3.5a.75.75 0 0 1-.743-.648L2.75 5.75a.75.75 0 0 1 .648-.743L3.5 5h5.25A3.25 3.25 0 0 1 12 1.75Zm6.197 4.75H5.802l1.267 12.872a1.25 1.25 0 0 0 1.117 1.122l.127.006h7.374c.6 0 1.109-.425 1.225-1.002l.02-.126L18.196 6.5ZM13.75 9.25a.75.75 0 0 1 .743.648L14.5 10v7a.75.75 0 0 1-1.493.102L13 17v-7a.75.75 0 0 1 .75-.75Zm-3.5 0a.75.75 0 0 1 .743.648L11 10v7a.75.75 0 0 1-1.493.102L9.5 17v-7a.75.75 0 0 1 .75-.75Zm1.75-6a1.75 1.75 0 0 0-1.744 1.606L10.25 5h3.5A1.75 1.75 0 0 0 12 3.25Z", "dismiss-circle-outline": "M12 2c5.523 0 10 4.477 10 10s-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2Zm0 1.5a8.5 8.5 0 1 0 0 17 8.5 8.5 0 0 0 0-17Zm3.446 4.897.084.073a.75.75 0 0 1 .073.976l-.073.084L13.061 12l2.47 2.47a.75.75 0 0 1 .072.976l-.073.084a.75.75 0 0 1-.976.073l-.084-.073L12 13.061l-2.47 2.47a.75.75 0 0 1-.976.072l-.084-.073a.75.75 0 0 1-.073-.976l.073-.084L10.939 12l-2.47-2.47a.75.75 0 0 1-.072-.976l.073-.084a.75.75 0 0 1 .976-.073l.084.073L12 10.939l2.47-2.47a.75.75 0 0 1 .976-.072Z", "dismiss-outline": "m4.397 4.554.073-.084a.75.75 0 0 1 .976-.073l.084.073L12 10.939l6.47-6.47a.75.75 0 1 1 1.06 1.061L13.061 12l6.47 6.47a.75.75 0 0 1 .072.976l-.073.084a.75.75 0 0 1-.976.073l-.084-.073L12 13.061l-6.47 6.47a.75.75 0 0 1-1.06-1.061L10.939 12l-6.47-6.47a.75.75 0 0 1-.072-.976l.073-.084-.073.084Z", @@ -71,14 +94,17 @@ "power-outline": "M8.204 4.82a.75.75 0 0 1 .634 1.36A7.51 7.51 0 0 0 4.5 12.991c0 4.148 3.358 7.51 7.499 7.51s7.499-3.362 7.499-7.51a7.51 7.51 0 0 0-4.323-6.804.75.75 0 1 1 .637-1.358 9.01 9.01 0 0 1 5.186 8.162c0 4.976-4.029 9.01-9 9.01C7.029 22 3 17.966 3 12.99a9.01 9.01 0 0 1 5.204-8.17ZM12 2.496a.75.75 0 0 1 .743.648l.007.102v7.5a.75.75 0 0 1-1.493.102l-.007-.102v-7.5a.75.75 0 0 1 .75-.75Z", "quote-outline": "M7.5 6a2.5 2.5 0 0 1 2.495 2.336l.005.206c-.01 3.555-1.24 6.614-3.705 9.223a.75.75 0 1 1-1.09-1.03c1.64-1.737 2.66-3.674 3.077-5.859A2.5 2.5 0 1 1 7.5 6Zm9 0a2.5 2.5 0 0 1 2.495 2.336l.005.206c-.01 3.56-1.238 6.614-3.705 9.223a.75.75 0 1 1-1.09-1.03c1.643-1.738 2.662-3.672 3.078-5.859A2.5 2.5 0 1 1 16.5 6Zm-9 1.5a1 1 0 1 0 .993 1.117l.007-.124a1 1 0 0 0-1-.993Zm9 0a1 1 0 1 0 .993 1.117l.007-.124a1 1 0 0 0-1-.993Z", "resize-large-outline": "M6.25 4.5A1.75 1.75 0 0 0 4.5 6.25v1.5a.75.75 0 0 1-1.5 0v-1.5A3.25 3.25 0 0 1 6.25 3h1.5a.75.75 0 0 1 0 1.5h-1.5ZM19.5 6.25a1.75 1.75 0 0 0-1.75-1.75h-1.5a.75.75 0 0 1 0-1.5h1.5A3.25 3.25 0 0 1 21 6.25v1.5a.75.75 0 0 1-1.5 0v-1.5ZM19.5 17.75a1.75 1.75 0 0 1-1.75 1.75h-1.5a.75.75 0 0 0 0 1.5h1.5A3.25 3.25 0 0 0 21 17.75v-1.5a.75.75 0 0 0-1.5 0v1.5ZM4.5 17.75c0 .966.784 1.75 1.75 1.75h1.5a.75.75 0 0 1 0 1.5h-1.5A3.25 3.25 0 0 1 3 17.75v-1.5a.75.75 0 0 1 1.5 0v1.5ZM8.25 6A2.25 2.25 0 0 0 6 8.25v7.5A2.25 2.25 0 0 0 8.25 18h7.5A2.25 2.25 0 0 0 18 15.75v-7.5A2.25 2.25 0 0 0 15.75 6h-7.5ZM7.5 8.25a.75.75 0 0 1 .75-.75h7.5a.75.75 0 0 1 .75.75v7.5a.75.75 0 0 1-.75.75h-7.5a.75.75 0 0 1-.75-.75v-7.5Z", + "save-outline": "M3 5.75A2.75 2.75 0 0 1 5.75 3h9.964a3.25 3.25 0 0 1 2.299.952l2.035 2.035c.61.61.952 1.437.952 2.299v9.964A2.75 2.75 0 0 1 18.25 21H5.75A2.75 2.75 0 0 1 3 18.25V5.75ZM5.75 4.5c-.69 0-1.25.56-1.25 1.25v12.5c0 .69.56 1.25 1.25 1.25H6v-5.25A2.25 2.25 0 0 1 8.25 12h7.5A2.25 2.25 0 0 1 18 14.25v5.25h.25c.69 0 1.25-.56 1.25-1.25V8.286c0-.465-.184-.91-.513-1.238l-2.035-2.035a1.75 1.75 0 0 0-.952-.49V7.25a2.25 2.25 0 0 1-2.25 2.25h-4.5A2.25 2.25 0 0 1 7 7.25V4.5H5.75Zm10.75 15v-5.25a.75.75 0 0 0-.75-.75h-7.5a.75.75 0 0 0-.75.75v5.25h9Zm-8-15v2.75c0 .414.336.75.75.75h4.5a.75.75 0 0 0 .75-.75V4.5h-6Z", "search-outline": "M10 2.75a7.25 7.25 0 0 1 5.63 11.819l4.9 4.9a.75.75 0 0 1-.976 1.134l-.084-.073-4.901-4.9A7.25 7.25 0 1 1 10 2.75Zm0 1.5a5.75 5.75 0 1 0 0 11.5 5.75 5.75 0 0 0 0-11.5Z", "send-outline": "M5.694 12 2.299 3.272c-.236-.607.356-1.188.942-.982l.093.04 18 9a.75.75 0 0 1 .097 1.283l-.097.058-18 9c-.583.291-1.217-.244-1.065-.847l.03-.096L5.694 12 2.299 3.272 5.694 12ZM4.402 4.54l2.61 6.71h6.627a.75.75 0 0 1 .743.648l.007.102a.75.75 0 0 1-.649.743l-.101.007H7.01l-2.609 6.71L19.322 12 4.401 4.54Z", "send-clock-outline": "M5.694 12 2.299 3.272c-.236-.608.356-1.189.942-.982l.093.04 18 9a.752.752 0 0 1 .264 1.124 6.473 6.473 0 0 0-4.272-1.452L4.402 4.54l2.61 6.71h6.627a.75.75 0 0 1 .724.556c-.472.26-.909.578-1.3.944H7.011l-2.609 6.71 6.753-3.377a6.522 6.522 0 0 0-.147 1.75l-7.674 3.838c-.583.291-1.217-.245-1.065-.847l.03-.096L5.694 12ZM23 17.5a5.5 5.5 0 1 0-11 0 5.5 5.5 0 0 0 11 0Zm-5.5 0h2a.5.5 0 1 1 0 1H17a.5.5 0 0 1-.5-.5v-3a.5.5 0 0 1 1 0v2.5Z", "settings-outline": "M12.012 2.25c.734.008 1.465.093 2.182.253a.75.75 0 0 1 .582.649l.17 1.527a1.384 1.384 0 0 0 1.927 1.116l1.401-.615a.75.75 0 0 1 .85.174 9.792 9.792 0 0 1 2.204 3.792.75.75 0 0 1-.271.825l-1.242.916a1.381 1.381 0 0 0 0 2.226l1.243.915a.75.75 0 0 1 .272.826 9.797 9.797 0 0 1-2.204 3.792.75.75 0 0 1-.848.175l-1.407-.617a1.38 1.38 0 0 0-1.926 1.114l-.169 1.526a.75.75 0 0 1-.572.647 9.518 9.518 0 0 1-4.406 0 .75.75 0 0 1-.572-.647l-.168-1.524a1.382 1.382 0 0 0-1.926-1.11l-1.406.616a.75.75 0 0 1-.849-.175 9.798 9.798 0 0 1-2.204-3.796.75.75 0 0 1 .272-.826l1.243-.916a1.38 1.38 0 0 0 0-2.226l-1.243-.914a.75.75 0 0 1-.271-.826 9.793 9.793 0 0 1 2.204-3.792.75.75 0 0 1 .85-.174l1.4.615a1.387 1.387 0 0 0 1.93-1.118l.17-1.526a.75.75 0 0 1 .583-.65c.717-.159 1.45-.243 2.201-.252Zm0 1.5a9.135 9.135 0 0 0-1.354.117l-.109.977A2.886 2.886 0 0 1 6.525 7.17l-.898-.394a8.293 8.293 0 0 0-1.348 2.317l.798.587a2.881 2.881 0 0 1 0 4.643l-.799.588c.32.842.776 1.626 1.348 2.322l.905-.397a2.882 2.882 0 0 1 4.017 2.318l.11.984c.889.15 1.798.15 2.687 0l.11-.984a2.881 2.881 0 0 1 4.018-2.322l.905.396a8.296 8.296 0 0 0 1.347-2.318l-.798-.588a2.881 2.881 0 0 1 0-4.643l.796-.587a8.293 8.293 0 0 0-1.348-2.317l-.896.393a2.884 2.884 0 0 1-4.023-2.324l-.11-.976a8.988 8.988 0 0 0-1.333-.117ZM12 8.25a3.75 3.75 0 1 1 0 7.5 3.75 3.75 0 0 1 0-7.5Zm0 1.5a2.25 2.25 0 1 0 0 4.5 2.25 2.25 0 0 0 0-4.5Z", "share-outline": "M6.747 4h3.464a.75.75 0 0 1 .102 1.493l-.102.007H6.747a2.25 2.25 0 0 0-2.245 2.096l-.005.154v9.5a2.25 2.25 0 0 0 2.096 2.245l.154.005h9.5a2.25 2.25 0 0 0 2.245-2.096l.005-.154v-.498a.75.75 0 0 1 1.494-.101l.006.101v.498a3.75 3.75 0 0 1-3.55 3.745l-.2.005h-9.5a3.75 3.75 0 0 1-3.745-3.55l-.005-.2v-9.5a3.75 3.75 0 0 1 3.55-3.745l.2-.005h3.464-3.464ZM14.5 6.52V3.75a.75.75 0 0 1 1.187-.61l.082.069 5.994 5.75c.28.268.306.7.077.997l-.077.085-5.994 5.752a.75.75 0 0 1-1.262-.434l-.007-.107v-2.725l-.344.03c-2.4.25-4.7 1.33-6.914 3.26-.52.453-1.323.025-1.237-.658.664-5.32 3.446-8.252 8.195-8.62l.3-.02V3.75v2.77ZM16 5.509V7.25a.75.75 0 0 1-.75.75c-3.874 0-6.274 1.676-7.312 5.157l-.079.279.352-.237C10.45 11.737 12.798 11 15.251 11a.75.75 0 0 1 .743.648l.007.102v1.743L20.16 9.5l-4.16-3.991Z", + "signature-outline": "M14.75 16.5c1.308 0 1.818.582 2.205 1.874l.068.237c.183.658.292.854.513.946.259.106.431.091.703-.048l.147-.083c.053-.031.11-.068.176-.111l.663-.452c.616-.405 1.17-.672 1.843-.84a.75.75 0 0 1 .364 1.454 4.03 4.03 0 0 0-1.146.49l-.298.19-.48.329a5.45 5.45 0 0 1-.583.357c-.643.33-1.27.385-1.96.1-.746-.306-1.046-.78-1.327-1.721l-.156-.542c-.181-.59-.305-.68-.732-.68-.31 0-.63.155-1.069.523l-.184.16-.921.876c-1.408 1.324-2.609 1.966-4.328 1.966-1.686 0-3.144-.254-4.368-.768l2.947-.805c.447.049.921.073 1.421.073 1.183 0 2.032-.415 3.087-1.362l.258-.239.532-.511c.236-.227.414-.39.592-.54.684-.573 1.305-.873 2.033-.873Zm4.28-13.53a3.579 3.579 0 0 1 0 5.06l-.288.289c1.151 1.401 1.11 2.886.039 3.96l-2.001 2.002a.75.75 0 0 1-1.06-1.062l1.999-1.999c.485-.486.54-1.09-.04-1.838l-8.617 8.617a2.25 2.25 0 0 1-1 .58l-5.115 1.394a.75.75 0 0 1-.92-.92l1.394-5.116a2.25 2.25 0 0 1 .58-1L13.97 2.97a3.578 3.578 0 0 1 5.061 0Zm-4 1.06L5.062 14a.75.75 0 0 0-.193.332l-1.05 3.85 3.85-1.05A.75.75 0 0 0 8 16.938l9.969-9.969a2.078 2.078 0 1 0-2.94-2.939Z", "sound-source-outline": "M3.5 12a8.5 8.5 0 1 1 14.762 5.748l.992 1.135A9.966 9.966 0 0 0 22 12c0-5.523-4.477-10-10-10S2 6.477 2 12a9.966 9.966 0 0 0 2.746 6.883l.993-1.134A8.47 8.47 0 0 1 3.5 12Z M19.25 12.125a7.098 7.098 0 0 1-1.783 4.715l-.998-1.14a5.625 5.625 0 1 0-8.806-.15l-1.004 1.146a7.125 7.125 0 1 1 12.59-4.571Z M16.25 12a4.23 4.23 0 0 1-.821 2.511l-1.026-1.172a2.75 2.75 0 1 0-4.806 0L8.571 14.51A4.25 4.25 0 1 1 16.25 12Z M12.564 12.756a.75.75 0 0 0-1.128 0l-7 8A.75.75 0 0 0 5 22h14a.75.75 0 0 0 .564-1.244l-7-8Zm4.783 7.744H6.653L12 14.389l5.347 6.111Z", "speaker-1-outline": "M14.704 3.442c.191.226.296.512.296.808v15.502a1.25 1.25 0 0 1-2.058.954L7.975 16.5H4.25A2.25 2.25 0 0 1 2 14.25v-4.5A2.25 2.25 0 0 1 4.25 7.5h3.725l4.968-4.204a1.25 1.25 0 0 1 1.761.147ZM13.5 4.79 8.525 9H4.25a.75.75 0 0 0-.75.75v4.5c0 .415.336.75.75.75h4.275l4.975 4.213V4.79Zm3.604 3.851a.75.75 0 0 1 1.03.25c.574.94.862 1.992.862 3.14 0 1.149-.288 2.201-.862 3.141a.75.75 0 1 1-1.28-.781c.428-.702.642-1.483.642-2.36 0-.876-.214-1.657-.642-2.359a.75.75 0 0 1 .25-1.03Z", "speaker-mute-outline": "M12.92 3.316c.806-.717 2.08-.145 2.08.934v15.496c0 1.078-1.274 1.65-2.08.934l-4.492-3.994a.75.75 0 0 0-.498-.19H4.25A2.25 2.25 0 0 1 2 14.247V9.75a2.25 2.25 0 0 1 2.25-2.25h3.68a.75.75 0 0 0 .498-.19l4.491-3.993Zm.58 1.49L9.425 8.43A2.25 2.25 0 0 1 7.93 9H4.25a.75.75 0 0 0-.75.75v4.497c0 .415.336.75.75.75h3.68a2.25 2.25 0 0 1 1.495.57l4.075 3.623V4.807ZM16.22 9.22a.75.75 0 0 1 1.06 0L19 10.94l1.72-1.72a.75.75 0 1 1 1.06 1.06L20.06 12l1.72 1.72a.75.75 0 1 1-1.06 1.06L19 13.06l-1.72 1.72a.75.75 0 1 1-1.06-1.06L17.94 12l-1.72-1.72a.75.75 0 0 1 0-1.06Z", + "square-outline": "M3 6a3 3 0 0 1 3-3h8a3 3 0 0 1 3 3v8a3 3 0 0 1-3 3H6a3 3 0 0 1-3-3V6zm3-2a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2H6z", "star-emphasis-outline": "M13.209 3.103c-.495-1.004-1.926-1.004-2.421 0L8.43 7.88l-5.273.766c-1.107.161-1.55 1.522-.748 2.303l3.815 3.72-.9 5.25c-.19 1.103.968 1.944 1.959 1.424l4.715-2.48 4.716 2.48c.99.52 2.148-.32 1.96-1.424l-.902-5.25 3.816-3.72c.8-.78.359-2.142-.748-2.303l-5.273-.766-2.358-4.777ZM9.74 8.615l2.258-4.576 2.259 4.576a1.35 1.35 0 0 0 1.016.738l5.05.734-3.654 3.562a1.35 1.35 0 0 0-.388 1.195l.862 5.03-4.516-2.375a1.35 1.35 0 0 0-1.257 0l-4.516 2.374.862-5.029a1.35 1.35 0 0 0-.388-1.195l-3.654-3.562 5.05-.734c.44-.063.82-.34 1.016-.738ZM1.164 3.782a.75.75 0 0 0 .118 1.054l2.5 2a.75.75 0 1 0 .937-1.172l-2.5-2a.75.75 0 0 0-1.055.118Z M22.836 18.218a.75.75 0 0 0-.117-1.054l-2.5-2a.75.75 0 0 0-.938 1.172l2.5 2a.75.75 0 0 0 1.055-.117ZM1.282 17.164a.75.75 0 1 0 .937 1.172l2.5-2a.75.75 0 0 0-.937-1.172l-2.5 2ZM22.836 3.782a.75.75 0 0 1-.117 1.054l-2.5 2a.75.75 0 0 1-.938-1.172l2.5-2a.75.75 0 0 1 1.055.118Z", "subtract-outline": "M3.997 13H20a1 1 0 1 0 0-2H3.997a1 1 0 1 0 0 2Z", "tag-outline": "M19.75 2A2.25 2.25 0 0 1 22 4.25v5.462a3.25 3.25 0 0 1-.952 2.298l-8.5 8.503a3.255 3.255 0 0 1-4.597.001L3.489 16.06a3.25 3.25 0 0 1-.003-4.596l8.5-8.51A3.25 3.25 0 0 1 14.284 2h5.465Zm0 1.5h-5.465c-.465 0-.91.185-1.239.513l-8.512 8.523a1.75 1.75 0 0 0 .015 2.462l4.461 4.454a1.755 1.755 0 0 0 2.477 0l8.5-8.503a1.75 1.75 0 0 0 .513-1.237V4.25a.75.75 0 0 0-.75-.75ZM17 5.502a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3Z", @@ -86,7 +112,6 @@ "video-outline": "M13.75 4.5A3.25 3.25 0 0 1 17 7.75v.173l3.864-2.318A.75.75 0 0 1 22 6.248V17.75a.75.75 0 0 1-1.136.643L17 16.075v.175a3.25 3.25 0 0 1-3.25 3.25h-8.5A3.25 3.25 0 0 1 2 16.25v-8.5A3.25 3.25 0 0 1 5.25 4.5h8.5Zm0 1.5h-8.5A1.75 1.75 0 0 0 3.5 7.75v8.5c0 .966.784 1.75 1.75 1.75h8.5a1.75 1.75 0 0 0 1.75-1.75v-8.5A1.75 1.75 0 0 0 13.75 6Zm6.75 1.573L17 9.674v4.651l3.5 2.1V7.573Z", "warning-outline": "M10.91 2.782a2.25 2.25 0 0 1 2.975.74l.083.138 7.759 14.009a2.25 2.25 0 0 1-1.814 3.334l-.154.006H4.243a2.25 2.25 0 0 1-2.041-3.197l.072-.143L10.031 3.66a2.25 2.25 0 0 1 .878-.878Zm9.505 15.613-7.76-14.008a.75.75 0 0 0-1.254-.088l-.057.088-7.757 14.008a.75.75 0 0 0 .561 1.108l.095.006h15.516a.75.75 0 0 0 .696-1.028l-.04-.086-7.76-14.008 7.76 14.008ZM12 16.002a.999.999 0 1 1 0 1.997.999.999 0 0 1 0-1.997ZM11.995 8.5a.75.75 0 0 1 .744.647l.007.102.004 4.502a.75.75 0 0 1-1.494.103l-.006-.102-.004-4.502a.75.75 0 0 1 .75-.75Z", "wifi-off-outline": "m12.858 14.273 7.434 7.434a1 1 0 0 0 1.414-1.414l-17.999-18a1 1 0 1 0-1.414 1.414L5.39 6.804c-.643.429-1.254.927-1.821 1.495a12.382 12.382 0 0 0-1.39 1.683 1 1 0 0 0 1.644 1.14c.363-.524.761-1.01 1.16-1.41a9.94 9.94 0 0 1 1.855-1.46L7.99 9.405a8.14 8.14 0 0 0-3.203 3.377 1 1 0 0 0 1.784.903 6.08 6.08 0 0 1 1.133-1.563 6.116 6.116 0 0 1 1.77-1.234l1.407 1.407A5.208 5.208 0 0 0 8.336 13.7a5.25 5.25 0 0 0-1.09 1.612 1 1 0 0 0 1.832.802c.167-.381.394-.722.672-1a3.23 3.23 0 0 1 3.108-.841Zm-1.332-5.93 2.228 2.229a6.1 6.1 0 0 1 2.616 1.55c.444.444.837.995 1.137 1.582a1 1 0 1 0 1.78-.911 8.353 8.353 0 0 0-1.503-2.085 8.108 8.108 0 0 0-6.258-2.365ZM8.51 5.327l1.651 1.651a9.904 9.904 0 0 1 10.016 4.148 1 1 0 1 0 1.646-1.136A11.912 11.912 0 0 0 8.51 5.327Zm4.552 11.114a1.501 1.501 0 1 1-2.123 2.123 1.501 1.501 0 0 1 2.123-2.123Z", - "brand-facebook-outline": "M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z", "brand-line-outline": "M19.365 9.863c.349 0 .63.285.63.631 0 .345-.281.63-.63.63H17.61v1.125h1.755c.349 0 .63.283.63.63 0 .344-.281.629-.63.629h-2.386c-.345 0-.627-.285-.627-.629V8.108c0-.345.282-.63.63-.63h2.386c.346 0 .627.285.627.63 0 .349-.281.63-.63.63H17.61v1.125h1.755zm-3.855 3.016c0 .27-.174.51-.432.596-.064.021-.133.031-.199.031-.211 0-.391-.09-.51-.25l-2.443-3.317v2.94c0 .344-.279.629-.631.629-.346 0-.626-.285-.626-.629V8.108c0-.27.173-.51.43-.595.06-.023.136-.033.194-.033.195 0 .375.104.495.254l2.462 3.33V8.108c0-.345.282-.63.63-.63.345 0 .63.285.63.63v4.771zm-5.741 0c0 .344-.282.629-.631.629-.345 0-.627-.285-.627-.629V8.108c0-.345.282-.63.63-.63.346 0 .628.285.628.63v4.771zm-2.466.629H4.917c-.345 0-.63-.285-.63-.629V8.108c0-.345.285-.63.63-.63.348 0 .63.285.63.63v4.141h1.756c.348 0 .629.283.629.63 0 .344-.282.629-.629.629M24 10.314C24 4.943 18.615.572 12 .572S0 4.943 0 10.314c0 4.811 4.27 8.842 10.035 9.608.391.082.923.258 1.058.59.12.301.079.766.038 1.08l-.164 1.02c-.045.301-.24 1.186 1.049.645 1.291-.539 6.916-4.078 9.436-6.975C23.176 14.393 24 12.458 24 10.314", "brand-linkedin-outline": "M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z", @@ -94,7 +119,6 @@ "brand-telegram-outline": "M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z", "brand-twitter-outline": "M23.953 4.57a10 10 0 01-2.825.775 4.958 4.958 0 002.163-2.723c-.951.555-2.005.959-3.127 1.184a4.92 4.92 0 00-8.384 4.482C7.69 8.095 4.067 6.13 1.64 3.162a4.822 4.822 0 00-.666 2.475c0 1.71.87 3.213 2.188 4.096a4.904 4.904 0 01-2.228-.616v.06a4.923 4.923 0 003.946 4.827 4.996 4.996 0 01-2.212.085 4.936 4.936 0 004.604 3.417 9.867 9.867 0 01-6.102 2.105c-.39 0-.779-.023-1.17-.067a13.995 13.995 0 007.557 2.209c9.053 0 13.998-7.496 13.998-13.985 0-.21 0-.42-.015-.63A9.935 9.935 0 0024 4.59z", "brand-whatsapp-outline": "M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.48-8.413Z", - "add-solid": "M11.883 3.007 12 3a1 1 0 0 1 .993.883L13 4v7h7a1 1 0 0 1 .993.883L21 12a1 1 0 0 1-.883.993L20 13h-7v7a1 1 0 0 1-.883.993L12 21a1 1 0 0 1-.993-.883L11 20v-7H4a1 1 0 0 1-.993-.883L3 12a1 1 0 0 1 .883-.993L4 11h7V4a1 1 0 0 1 .883-.993L12 3l-.117.007Z", "subtract-solid": "M3.997 13H20a1 1 0 1 0 0-2H3.997a1 1 0 1 0 0 2Z" } diff --git a/app/javascript/shared/components/FluentIcon/icons.json b/app/javascript/shared/components/FluentIcon/icons.json index f3dc089b7..62798657d 100644 --- a/app/javascript/shared/components/FluentIcon/icons.json +++ b/app/javascript/shared/components/FluentIcon/icons.json @@ -2,6 +2,7 @@ "arrow-clockwise-outline": "M12 4.75a7.25 7.25 0 1 0 7.201 6.406c-.068-.588.358-1.156.95-1.156.515 0 .968.358 1.03.87a9.25 9.25 0 1 1-3.432-6.116V4.25a1 1 0 1 1 2.001 0v2.698l.034.052h-.034v.25a1 1 0 0 1-1 1h-3a1 1 0 1 1 0-2h.666A7.219 7.219 0 0 0 12 4.75Z", "arrow-right-outline": "M13.267 4.209a.75.75 0 0 0-1.034 1.086l6.251 5.955H3.75a.75.75 0 0 0 0 1.5h14.734l-6.251 5.954a.75.75 0 0 0 1.034 1.087l7.42-7.067a.996.996 0 0 0 .3-.58.758.758 0 0 0-.001-.29.995.995 0 0 0-.3-.578l-7.419-7.067Z", "attach-outline": "M11.772 3.743a6 6 0 0 1 8.66 8.302l-.19.197-8.8 8.798-.036.03a3.723 3.723 0 0 1-5.489-4.973.764.764 0 0 1 .085-.13l.054-.06.086-.088.142-.148.002.003 7.436-7.454a.75.75 0 0 1 .977-.074l.084.073a.75.75 0 0 1 .074.976l-.073.084-7.594 7.613a2.23 2.23 0 0 0 3.174 3.106l8.832-8.83A4.502 4.502 0 0 0 13 4.644l-.168.16-.013.014-9.536 9.536a.75.75 0 0 1-1.133-.977l.072-.084 9.549-9.55h.002Z", + "chevron-left-outline": "M15.53 4.22a.75.75 0 0 1 0 1.06L8.81 12l6.72 6.72a.75.75 0 1 1-1.06 1.06l-7.25-7.25a.75.75 0 0 1 0-1.06l7.25-7.25a.75.75 0 0 1 1.06 0Z", "chevron-right-outline": "M8.293 4.293a1 1 0 0 0 0 1.414L14.586 12l-6.293 6.293a1 1 0 1 0 1.414 1.414l7-7a1 1 0 0 0 0-1.414l-7-7a1 1 0 0 0-1.414 0Z", "dismiss-outline": "m4.397 4.554.073-.084a.75.75 0 0 1 .976-.073l.084.073L12 10.939l6.47-6.47a.75.75 0 1 1 1.06 1.061L13.061 12l6.47 6.47a.75.75 0 0 1 .072.976l-.073.084a.75.75 0 0 1-.976.073l-.084-.073L12 13.061l-6.47 6.47a.75.75 0 0 1-1.06-1.061L10.939 12l-6.47-6.47a.75.75 0 0 1-.072-.976l.073-.084-.073.084Z", "document-outline": "M18.5 20a.5.5 0 0 1-.5.5H6a.5.5 0 0 1-.5-.5V4a.5.5 0 0 1 .5-.5h6V8a2 2 0 0 0 2 2h4.5v10Zm-5-15.379L17.378 8.5H14a.5.5 0 0 1-.5-.5V4.621Zm5.914 3.793-5.829-5.828c-.026-.026-.058-.046-.085-.07a2.072 2.072 0 0 0-.219-.18c-.04-.027-.086-.045-.128-.068-.071-.04-.141-.084-.216-.116a1.977 1.977 0 0 0-.624-.138C12.266 2.011 12.22 2 12.172 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9.828a2 2 0 0 0-.586-1.414Z", diff --git a/app/javascript/shared/helpers/KeyboardHelpers.js b/app/javascript/shared/helpers/KeyboardHelpers.js index c9a6c6233..079b5ab62 100644 --- a/app/javascript/shared/helpers/KeyboardHelpers.js +++ b/app/javascript/shared/helpers/KeyboardHelpers.js @@ -89,3 +89,7 @@ export const hasPressedArrowUpKey = e => { export const hasPressedArrowDownKey = e => { return e.keyCode === 40; }; + +export const hasPressedCommandPlusKKey = e => { + return e.metaKey && e.keyCode === 75; +}; diff --git a/app/javascript/shared/mixins/alertMixin.js b/app/javascript/shared/mixins/alertMixin.js index ba0064f44..50ace13f1 100644 --- a/app/javascript/shared/mixins/alertMixin.js +++ b/app/javascript/shared/mixins/alertMixin.js @@ -1,7 +1,7 @@ export default { methods: { - showAlert(message) { - bus.$emit('newToastMessage', message); + showAlert(message, action) { + bus.$emit('newToastMessage', message, action); }, }, }; diff --git a/app/javascript/shared/mixins/filterMixin.js b/app/javascript/shared/mixins/filterMixin.js new file mode 100644 index 000000000..801fb2f08 --- /dev/null +++ b/app/javascript/shared/mixins/filterMixin.js @@ -0,0 +1,44 @@ +// import groupBy from 'lodash.groupby'; + +export default { + methods: { + setFilterAttributes() { + const allCustomAttributes = this.$store.getters[ + 'attributes/getAttributesByModel' + ](this.attributeModel); + const customAttributesFormatted = { + name: this.$t(`${this.filtersFori18n}.GROUPS.CUSTOM_ATTRIBUTES`), + attributes: allCustomAttributes.map(attr => { + return { + key: attr.attribute_key, + name: attr.attribute_display_name, + }; + }), + }; + const allFilterGroups = this.filterAttributeGroups.map(group => { + return { + name: this.$t(`${this.filtersFori18n}.GROUPS.${group.i18nGroup}`), + attributes: group.attributes.map(attribute => { + return { + key: attribute.key, + name: this.$t( + `${this.filtersFori18n}.ATTRIBUTES.${attribute.i18nKey}` + ), + }; + }), + }; + }); + const customAttributeTypes = allCustomAttributes.map(attr => { + return { + attributeKey: attr.attribute_key, + attributeI18nKey: `CUSTOM_ATTRIBUTE_${attr.attribute_display_type.toUpperCase()}`, + inputType: this.customAttributeInputType(attr.attribute_display_type), + filterOperators: this.getOperatorTypes(attr.attribute_display_type), + attributeModel: 'custom_attributes', + }; + }); + this.filterTypes = [...this.filterTypes, ...customAttributeTypes]; + this.filterGroups = [...allFilterGroups, customAttributesFormatted]; + }, + }, +}; diff --git a/app/javascript/shared/mixins/inboxMixin.js b/app/javascript/shared/mixins/inboxMixin.js index 022b9327e..aebbeebc1 100644 --- a/app/javascript/shared/mixins/inboxMixin.js +++ b/app/javascript/shared/mixins/inboxMixin.js @@ -8,6 +8,7 @@ export const INBOX_TYPES = { EMAIL: 'Channel::Email', TELEGRAM: 'Channel::Telegram', LINE: 'Channel::Line', + SMS: 'Channel::Sms', }; export default { diff --git a/app/javascript/shared/mixins/specs/MockComponent.vue b/app/javascript/shared/mixins/specs/MockComponent.vue new file mode 100644 index 000000000..1d1012c3a --- /dev/null +++ b/app/javascript/shared/mixins/specs/MockComponent.vue @@ -0,0 +1,9 @@ + + + diff --git a/app/javascript/shared/mixins/specs/filterFixtures.js b/app/javascript/shared/mixins/specs/filterFixtures.js new file mode 100644 index 000000000..6bacc4456 --- /dev/null +++ b/app/javascript/shared/mixins/specs/filterFixtures.js @@ -0,0 +1,29 @@ +export const filterGroups = [ + { + name: 'Standard Filters', + attributes: [ + { key: 'status', name: 'Status' }, + { key: 'assignee_id', name: 'Assignee Name' }, + { key: 'inbox_id', name: 'Inbox Name' }, + { key: 'team_id', name: 'Team Name' }, + { key: 'display_id', name: 'Conversation Identifier' }, + { key: 'campaign_id', name: 'Campaign Name' }, + { key: 'labels', name: 'Labels' }, + ], + }, + { + name: 'Additional Filters', + attributes: [ + { key: 'browser_language', name: 'Browser Language' }, + { key: 'country_code', name: 'Country Name' }, + { key: 'referer', name: 'Referer link' }, + ], + }, + { + name: 'Custom Attributes', + attributes: [ + { key: 'signed_up_at', name: 'Signed Up At' }, + { key: 'test', name: 'Test' }, + ], + }, +]; diff --git a/app/javascript/shared/mixins/specs/filterMixin.spec.js b/app/javascript/shared/mixins/specs/filterMixin.spec.js new file mode 100644 index 000000000..94fe95ea7 --- /dev/null +++ b/app/javascript/shared/mixins/specs/filterMixin.spec.js @@ -0,0 +1,13 @@ +import filterMixin from '../filterMixin'; +import { shallowMount } from '@vue/test-utils'; +import MockComponent from './MockComponent.vue'; + +describe('Test mixin function', () => { + const wrapper = shallowMount(MockComponent, { + mixins: [filterMixin], + }); + + it('should return proper value from bool', () => { + expect(wrapper.vm.setFilterAttributes).toBeTruthy(); + }); +}); diff --git a/app/javascript/shared/store/globalConfig.js b/app/javascript/shared/store/globalConfig.js index 69117179a..9082db4ac 100644 --- a/app/javascript/shared/store/globalConfig.js +++ b/app/javascript/shared/store/globalConfig.js @@ -5,6 +5,7 @@ const { BRAND_NAME: brandName, CHATWOOT_INBOX_TOKEN: chatwootInboxToken, CREATE_NEW_ACCOUNT_FROM_DASHBOARD: createNewAccountFromDashboard, + DIRECT_UPLOADS_ENABLED: directUploadsEnabled, DISPLAY_MANIFEST: displayManifest, INSTALLATION_NAME: installationName, LOGO_THUMBNAIL: logoThumbnail, @@ -21,6 +22,7 @@ const state = { brandName, chatwootInboxToken, createNewAccountFromDashboard, + directUploadsEnabled: directUploadsEnabled === 'true', displayManifest, installationName, logo, diff --git a/app/javascript/widget/App.vue b/app/javascript/widget/App.vue index 818ef6db4..7b047f7cc 100755 --- a/app/javascript/widget/App.vue +++ b/app/javascript/widget/App.vue @@ -1,61 +1,67 @@ diff --git a/app/javascript/widget/components/ChatFooter.vue b/app/javascript/widget/components/ChatFooter.vue index 7561f1930..f399ff252 100755 --- a/app/javascript/widget/components/ChatFooter.vue +++ b/app/javascript/widget/components/ChatFooter.vue @@ -1,30 +1,31 @@ @@ -115,19 +116,8 @@ export default { }, }; - - diff --git a/app/javascript/widget/components/ChatHeaderExpanded.vue b/app/javascript/widget/components/ChatHeaderExpanded.vue index f80b1b8ab..fc4aab125 100755 --- a/app/javascript/widget/components/ChatHeaderExpanded.vue +++ b/app/javascript/widget/components/ChatHeaderExpanded.vue @@ -1,14 +1,14 @@ @@ -47,6 +49,10 @@ export default { type: Array, default: () => {}, }, + hasConversation: { + type: Boolean, + default: false, + }, }, computed: { ...mapGetters({ widgetColor: 'appConfig/getWidgetColor' }), diff --git a/app/javascript/widget/components/UnreadMessage.vue b/app/javascript/widget/components/UnreadMessage.vue index 48a6b4686..13e8fc250 100644 --- a/app/javascript/widget/components/UnreadMessage.vue +++ b/app/javascript/widget/components/UnreadMessage.vue @@ -21,6 +21,10 @@ import messageFormatterMixin from 'shared/mixins/messageFormatterMixin'; import Thumbnail from 'dashboard/components/widgets/Thumbnail'; import configMixin from '../mixins/configMixin'; import { isEmptyObject } from 'widget/helpers/utils'; +import { + ON_CAMPAIGN_MESSAGE_CLICK, + ON_UNREAD_MESSAGE_CLICK, +} from '../constants/widgetBusEvents'; export default { name: 'UnreadMessage', components: { Thumbnail }, @@ -82,9 +86,9 @@ export default { }, onClickMessage() { if (this.campaignId) { - bus.$emit('on-campaign-view-clicked', this.campaignId); + bus.$emit(ON_CAMPAIGN_MESSAGE_CLICK, this.campaignId); } else { - bus.$emit('on-unread-view-clicked'); + bus.$emit(ON_UNREAD_MESSAGE_CLICK); } }, }, diff --git a/app/javascript/widget/views/Unread.vue b/app/javascript/widget/components/UnreadMessageList.vue similarity index 67% rename from app/javascript/widget/views/Unread.vue rename to app/javascript/widget/components/UnreadMessageList.vue index 7eafbdbe2..6d8005ef1 100644 --- a/app/javascript/widget/views/Unread.vue +++ b/app/javascript/widget/components/UnreadMessageList.vue @@ -1,20 +1,16 @@