diff --git a/.circleci/config.yml b/.circleci/config.yml index a9e0bd450..fbb4fea90 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -19,6 +19,7 @@ defaults: &defaults - COVERAGE: true - LOG_LEVEL: warn parallelism: 4 + resource_class: large jobs: build: @@ -122,9 +123,11 @@ jobs: mkdir -p coverage ~/tmp/cc-test-reporter before-build TESTFILES=$(circleci tests glob "spec/**/*_spec.rb" | circleci tests split --split-by=timings) - bundle exec rspec --profile 10 \ - --out test-results/rspec/rspec.xml \ + bundle exec rspec --format progress \ + --format RspecJunitFormatter \ + --out ~/tmp/test-results/rspec.xml \ -- ${TESTFILES} + no_output_timeout: 30m - run: name: Code Climate Test Coverage command: | @@ -137,7 +140,7 @@ jobs: ~/tmp/cc-test-reporter before-build TESTFILES=$(circleci tests glob **/specs/*.spec.js | circleci tests split --split-by=timings) yarn test:coverage --profile 10 \ - --out test-results/frontend_specs/rspec.xml \ + --out ~/tmp/test-results/yarn.xml \ -- ${TESTFILES} - run: name: Code Climate Test Coverage diff --git a/.editorconfig b/.editorconfig index 7203adb09..2a5fe28cf 100644 --- a/.editorconfig +++ b/.editorconfig @@ -7,8 +7,8 @@ end_of_line = lf charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true -indent_style = spaces +indent_style = space tab_width = 2 -[{*.{rb,erb,js,coffee,json,yml,css,scss,sh,markdown,md,html}] +[*.{rb,erb,js,coffee,json,yml,css,scss,sh,markdown,md,html}] indent_size = 2 diff --git a/.github/workflows/publish_foss_docker.yml b/.github/workflows/publish_foss_docker.yml index 37f0f3e6e..2ddaba7e5 100644 --- a/.github/workflows/publish_foss_docker.yml +++ b/.github/workflows/publish_foss_docker.yml @@ -58,5 +58,6 @@ jobs: with: context: . file: docker/Dockerfile + platforms: linux/amd64 push: true tags: ${{ env.DOCKER_TAG }} diff --git a/.gitignore b/.gitignore index e4b14d2f5..11a8c50e3 100644 --- a/.gitignore +++ b/.gitignore @@ -39,9 +39,6 @@ public/packs* *.un~ .jest-cache -#VS Code files -.vscode - # ignore jetbrains IDE files .idea @@ -62,4 +59,4 @@ package-lock.json test/cypress/videos/* /config/master.key -/config/*.enc \ No newline at end of file +/config/*.enc diff --git a/.rubocop.yml b/.rubocop.yml index dafd9a620..3665ad2e3 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -16,6 +16,7 @@ Metrics/ClassLength: - 'app/models/message.rb' - 'app/builders/messages/facebook/message_builder.rb' - 'app/controllers/api/v1/accounts/contacts_controller.rb' + - 'app/controllers/api/v1/accounts/conversations_controller.rb' - 'app/listeners/action_cable_listener.rb' - 'app/models/conversation.rb' RSpec/ExampleLength: diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 000000000..254e696a4 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,32 @@ +{ + "recommendations": [ + // Spell check + "streetsidesoftware.code-spell-checker", + // Better Comments + "aaron-bond.better-comments", + // Rails Test Runner + "davidpallinder.rails-test-runner", + // Eslint + "dbaeumer.vscode-eslint", + // Auto Close Tag + "formulahendry.auto-close-tag", + // Auto Rename Tag + "formulahendry.auto-rename-tag", + // Hight light colors + "naumovs.color-highlight", + // GitLens + "eamodio.gitlens", + // Ruby + "rebornix.ruby", + // Vue + "octref.vetur", + // Prettier + "esbenp.prettier-vscode", + // Dot Env + "mikestead.dotenv", + // HTML CSS Support + "ecmel.vscode-html-css", + // Tailwind CSS Intellisense + "bradlc.vscode-tailwindcss", + ] +} diff --git a/Gemfile b/Gemfile index ca6c7856d..d47b5e449 100644 --- a/Gemfile +++ b/Gemfile @@ -174,6 +174,7 @@ group :development, :test do gem 'listen' gem 'mock_redis' gem 'pry-rails' + gem 'rspec_junit_formatter' gem 'rspec-rails', '~> 5.0.0' gem 'rubocop', require: false gem 'rubocop-performance', require: false diff --git a/Gemfile.lock b/Gemfile.lock index 5cf4f020d..5b14d5b5e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -286,9 +286,9 @@ GEM google-cloud-core (~> 1.6) googleauth (>= 0.16.2, < 2.a) mini_mime (~> 1.0) - google-protobuf (3.21.2) - google-protobuf (3.21.2-x86_64-darwin) - google-protobuf (3.21.2-x86_64-linux) + google-protobuf (3.21.7) + google-protobuf (3.21.7-x86_64-darwin) + google-protobuf (3.21.7-x86_64-linux) googleapis-common-protos (1.3.12) google-protobuf (~> 3.14) googleapis-common-protos-types (~> 1.2) @@ -536,6 +536,8 @@ GEM rspec-mocks (~> 3.10) rspec-support (~> 3.10) rspec-support (3.11.0) + rspec_junit_formatter (0.6.0) + rspec-core (>= 2, < 4, != 2.12.0) rubocop (1.31.2) json (~> 2.3) parallel (~> 1.10) @@ -769,6 +771,7 @@ DEPENDENCIES responders rest-client rspec-rails (~> 5.0.0) + rspec_junit_formatter rubocop rubocop-performance rubocop-rails @@ -805,4 +808,4 @@ RUBY VERSION ruby 3.0.4p208 BUNDLED WITH - 2.3.17 + 2.3.18 diff --git a/app/builders/contact_inbox_builder.rb b/app/builders/contact_inbox_builder.rb index e7ae8b0aa..8fcd2b158 100644 --- a/app/builders/contact_inbox_builder.rb +++ b/app/builders/contact_inbox_builder.rb @@ -1,13 +1,12 @@ +# This Builder will create a contact inbox with specified attributes. If the contact inbox already exists, it will be returned. +# For Specific Channels like whatsapp, email etc . it smartly generated appropriate the source id when none is provided. + class ContactInboxBuilder - pattr_initialize [:contact_id!, :inbox_id!, :source_id] + pattr_initialize [:contact, :inbox, :source_id, { hmac_verified: false }] def perform - @contact = Contact.find(contact_id) - @inbox = @contact.account.inboxes.find(inbox_id) - 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? + @source_id ||= generate_source_id + create_contact_inbox if source_id.present? end private @@ -19,23 +18,37 @@ class ContactInboxBuilder when 'Channel::Whatsapp' wa_source_id when 'Channel::Email' - @contact.email + email_source_id when 'Channel::Sms' - @contact.phone_number - when 'Channel::Api' + phone_source_id + when 'Channel::Api', 'Channel::WebWidget' SecureRandom.uuid + else + raise "Unsupported operation for this channel: #{@inbox.channel_type}" end end + def email_source_id + raise ActionController::ParameterMissing, 'contact email' unless @contact.email + + @contact.email + end + + def phone_source_id + raise ActionController::ParameterMissing, 'contact phone number' unless @contact.phone_number + + @contact.phone_number + end + def wa_source_id - return unless @contact.phone_number + raise ActionController::ParameterMissing, 'contact phone number' unless @contact.phone_number # whatsapp doesn't want the + in e164 format - "#{@contact.phone_number}.delete('+')" + @contact.phone_number.delete('+').to_s end def twilio_source_id - return unless @contact.phone_number + raise ActionController::ParameterMissing, 'contact phone number' unless @contact.phone_number case @inbox.channel.medium when 'sms' @@ -45,11 +58,11 @@ class ContactInboxBuilder end end - def create_contact_inbox(source_id) - ::ContactInbox.find_or_create_by!( + def create_contact_inbox + ::ContactInbox.create_with(hmac_verified: hmac_verified || false).find_or_create_by!( contact_id: @contact.id, inbox_id: @inbox.id, - source_id: source_id + source_id: @source_id ) end end diff --git a/app/builders/contact_builder.rb b/app/builders/contact_inbox_with_contact_builder.rb similarity index 51% rename from app/builders/contact_builder.rb rename to app/builders/contact_inbox_with_contact_builder.rb index 938072643..d97f64cfe 100644 --- a/app/builders/contact_builder.rb +++ b/app/builders/contact_inbox_with_contact_builder.rb @@ -1,25 +1,47 @@ -class ContactBuilder - pattr_initialize [:source_id!, :inbox!, :contact_attributes!, :hmac_verified] +# This Builder will create a contact and contact inbox with specified attributes. +# If an existing identified contact exisits, it will be returned. +# for contact inbox logic it uses the contact inbox builder + +class ContactInboxWithContactBuilder + pattr_initialize [:inbox!, :contact_attributes!, :source_id, :hmac_verified] def perform - contact_inbox = inbox.contact_inboxes.find_by(source_id: source_id) - return contact_inbox if contact_inbox + find_or_create_contact_and_contact_inbox + # in case of race conditions where contact is created by another thread + # we will try to find the contact and create a contact inbox + rescue ActiveRecord::RecordNotUnique + find_or_create_contact_and_contact_inbox + end - build_contact_inbox + def find_or_create_contact_and_contact_inbox + @contact_inbox = inbox.contact_inboxes.find_by(source_id: source_id) if source_id.present? + return @contact_inbox if @contact_inbox + + ActiveRecord::Base.transaction(requires_new: true) do + build_contact_with_contact_inbox + update_contact_avatar(@contact) unless @contact.avatar.attached? + @contact_inbox + end end private + def build_contact_with_contact_inbox + @contact = find_contact || create_contact + @contact_inbox = create_contact_inbox + end + def account @account ||= inbox.account end - def create_contact_inbox(contact) - ::ContactInbox.create_with(hmac_verified: hmac_verified || false).find_or_create_by!( - contact_id: contact.id, - inbox_id: inbox.id, - source_id: source_id - ) + def create_contact_inbox + ContactInboxBuilder.new( + contact: @contact, + inbox: @inbox, + source_id: @source_id, + hmac_verified: hmac_verified + ).perform end def update_contact_avatar(contact) @@ -61,16 +83,4 @@ class ContactBuilder account.contacts.find_by(phone_number: phone_number) end - - def build_contact_inbox - ActiveRecord::Base.transaction do - contact = find_contact || create_contact - contact_inbox = create_contact_inbox(contact) - update_contact_avatar(contact) - contact_inbox - rescue StandardError => e - Rails.logger.error e - raise e - end - end end diff --git a/app/builders/messages/facebook/message_builder.rb b/app/builders/messages/facebook/message_builder.rb index 9f670602a..42d567e54 100644 --- a/app/builders/messages/facebook/message_builder.rb +++ b/app/builders/messages/facebook/message_builder.rb @@ -22,10 +22,9 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder return if @inbox.channel.reauthorization_required? ActiveRecord::Base.transaction do - build_contact + build_contact_inbox build_message end - ensure_contact_avatar rescue Koala::Facebook::AuthenticationError @inbox.channel.authorization_error! rescue StandardError => e @@ -35,15 +34,12 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder private - def contact - @contact ||= @inbox.contact_inboxes.find_by(source_id: @sender_id)&.contact - end - - def build_contact - return if contact.present? - - @contact = Contact.create!(contact_params.except(:remote_avatar_url)) - @contact_inbox = ContactInbox.find_or_create_by!(contact: contact, inbox: @inbox, source_id: @sender_id) + def build_contact_inbox + @contact_inbox = ::ContactInboxWithContactBuilder.new( + source_id: @sender_id, + inbox: @inbox, + contact_attributes: contact_params + ).perform end def build_message @@ -54,19 +50,11 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder end end - def ensure_contact_avatar - return if contact_params[:remote_avatar_url].blank? - return if @contact.avatar.attached? - - Avatar::AvatarFromUrlJob.perform_later(@contact, contact_params[:remote_avatar_url]) - end - def conversation @conversation ||= Conversation.find_by(conversation_params) || build_conversation end def build_conversation - @contact_inbox ||= contact.contact_inboxes.find_by!(source_id: @sender_id) Conversation.create!(conversation_params.merge( contact_inbox_id: @contact_inbox.id )) @@ -94,7 +82,7 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder { account_id: @inbox.account_id, inbox_id: @inbox.id, - contact_id: contact.id + contact_id: @contact_inbox.contact_id } end @@ -105,7 +93,7 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder message_type: @message_type, content: response.content, source_id: response.identifier, - sender: @outgoing_echo ? nil : contact + sender: @outgoing_echo ? nil : @contact_inbox.contact } end @@ -113,7 +101,7 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder { name: "#{result['first_name'] || 'John'} #{result['last_name'] || 'Doe'}", account_id: @inbox.account_id, - remote_avatar_url: result['profile_pic'] || '' + avatar_url: result['profile_pic'] } end diff --git a/app/controllers/api/v1/accounts/articles_controller.rb b/app/controllers/api/v1/accounts/articles_controller.rb index a2aab527c..d650b19d9 100644 --- a/app/controllers/api/v1/accounts/articles_controller.rb +++ b/app/controllers/api/v1/accounts/articles_controller.rb @@ -42,8 +42,8 @@ class Api::V1::Accounts::ArticlesController < Api::V1::Accounts::BaseController def article_params params.require(:article).permit( - :title, :content, :description, :position, :category_id, :author_id, :associated_article_id, :status, meta: [:title, :description, - { tags: [] }] + :title, :slug, :content, :description, :position, :category_id, :author_id, :associated_article_id, :status, meta: [:title, :description, + { tags: [] }] ) end diff --git a/app/controllers/api/v1/accounts/contacts/contact_inboxes_controller.rb b/app/controllers/api/v1/accounts/contacts/contact_inboxes_controller.rb index fdcdcaf9e..b4287ae08 100644 --- a/app/controllers/api/v1/accounts/contacts/contact_inboxes_controller.rb +++ b/app/controllers/api/v1/accounts/contacts/contact_inboxes_controller.rb @@ -2,8 +2,11 @@ class Api::V1::Accounts::Contacts::ContactInboxesController < Api::V1::Accounts: before_action :ensure_inbox, only: [:create] def create - source_id = params[:source_id] || SecureRandom.uuid - @contact_inbox = ContactInbox.create!(contact: @contact, inbox: @inbox, source_id: source_id) + @contact_inbox = ContactInboxBuilder.new( + contact: @contact, + inbox: @inbox, + source_id: params[:source_id] + ).perform end private diff --git a/app/controllers/api/v1/accounts/contacts_controller.rb b/app/controllers/api/v1/accounts/contacts_controller.rb index 1c56e9c04..b86b973df 100644 --- a/app/controllers/api/v1/accounts/contacts_controller.rb +++ b/app/controllers/api/v1/accounts/contacts_controller.rb @@ -134,8 +134,11 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController return if params[:inbox_id].blank? inbox = Current.account.inboxes.find(params[:inbox_id]) - source_id = params[:source_id] || SecureRandom.uuid - ContactInbox.create!(contact: @contact, inbox: inbox, source_id: source_id) + ContactInboxBuilder.new( + contact: @contact, + inbox: inbox, + source_id: params[:source_id] + ).perform end def permitted_params diff --git a/app/controllers/api/v1/accounts/conversations_controller.rb b/app/controllers/api/v1/accounts/conversations_controller.rb index 0515eabca..8734a3dd4 100644 --- a/app/controllers/api/v1/accounts/conversations_controller.rb +++ b/app/controllers/api/v1/accounts/conversations_controller.rb @@ -3,7 +3,7 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro include DateRangeHelper before_action :conversation, except: [:index, :meta, :search, :create, :filter] - before_action :contact_inbox, only: [:create] + before_action :inbox, :contact, :contact_inbox, only: [:create] def index result = conversation_finder.perform @@ -109,22 +109,35 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro authorize @conversation.inbox, :show? end + def inbox + return if params[:inbox_id].blank? + + @inbox = Current.account.inboxes.find(params[:inbox_id]) + authorize @inbox, :show? + end + + def contact + return if params[:contact_id].blank? + + @contact = Current.account.contacts.find(params[:contact_id]) + end + def contact_inbox @contact_inbox = build_contact_inbox + # fallback for the old case where we do look up only using source id + # In future we need to change this and make sure we do look up on combination of inbox_id and source_id + # and deprecate the support of passing only source_id as the param @contact_inbox ||= ::ContactInbox.find_by!(source_id: params[:source_id]) authorize @contact_inbox.inbox, :show? end def build_contact_inbox - return if params[:contact_id].blank? || params[:inbox_id].blank? - - inbox = Current.account.inboxes.find(params[:inbox_id]) - authorize inbox, :show? + return if @inbox.blank? || @contact.blank? ContactInboxBuilder.new( - contact_id: params[:contact_id], - inbox_id: inbox.id, + contact: @contact, + inbox: @inbox, source_id: params[:source_id] ).perform end diff --git a/app/controllers/api/v1/accounts/csat_survey_responses_controller.rb b/app/controllers/api/v1/accounts/csat_survey_responses_controller.rb index 7b5c51d6e..9495bbfa8 100644 --- a/app/controllers/api/v1/accounts/csat_survey_responses_controller.rb +++ b/app/controllers/api/v1/accounts/csat_survey_responses_controller.rb @@ -22,7 +22,7 @@ class Api::V1::Accounts::CsatSurveyResponsesController < Api::V1::Accounts::Base def download response.headers['Content-Type'] = 'text/csv' response.headers['Content-Disposition'] = 'attachment; filename=csat_report.csv' - render layout: false, template: 'api/v1/accounts/csat_survey_responses/download.csv.erb', format: 'csv' + render layout: false, template: 'api/v1/accounts/csat_survey_responses/download', formats: [:csv] end private diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb index 2f18fbac2..507e00e64 100644 --- a/app/controllers/api/v1/accounts_controller.rb +++ b/app/controllers/api/v1/accounts_controller.rb @@ -24,7 +24,7 @@ class Api::V1::AccountsController < Api::BaseController ).perform if @user send_auth_headers(@user) - render 'api/v1/accounts/create.json', locals: { resource: @user } + render 'api/v1/accounts/create', format: :json, locals: { resource: @user } else render_error_response(CustomExceptions::Account::SignupFailed.new({})) end @@ -32,7 +32,7 @@ class Api::V1::AccountsController < Api::BaseController def show @latest_chatwoot_version = ::Redis::Alfred.get(::Redis::Alfred::LATEST_CHATWOOT_VERSION) - render 'api/v1/accounts/show.json' + render 'api/v1/accounts/show', format: :json end def update diff --git a/app/controllers/api/v1/notification_subscriptions_controller.rb b/app/controllers/api/v1/notification_subscriptions_controller.rb index a01c2ca03..1a797a74d 100644 --- a/app/controllers/api/v1/notification_subscriptions_controller.rb +++ b/app/controllers/api/v1/notification_subscriptions_controller.rb @@ -9,7 +9,7 @@ class Api::V1::NotificationSubscriptionsController < Api::BaseController def destroy notification_subscription = NotificationSubscription.where(["subscription_attributes->>'push_token' = ?", params[:push_token]]).first - notification_subscription.destroy! + notification_subscription.destroy! if notification_subscription.present? head :ok end diff --git a/app/controllers/api/v1/profiles_controller.rb b/app/controllers/api/v1/profiles_controller.rb index fb9c3b62c..20b8e7ae8 100644 --- a/app/controllers/api/v1/profiles_controller.rb +++ b/app/controllers/api/v1/profiles_controller.rb @@ -22,6 +22,11 @@ class Api::V1::ProfilesController < Api::BaseController @user.account_users.find_by!(account_id: availability_params[:account_id]).update!(availability: availability_params[:availability]) end + def set_active_account + @user.account_users.find_by(account_id: profile_params[:account_id]).update(active_at: Time.now.utc) + head :ok + end + private def set_user @@ -39,6 +44,7 @@ class Api::V1::ProfilesController < Api::BaseController :display_name, :avatar, :message_signature, + :account_id, ui_settings: {} ) end diff --git a/app/controllers/api/v2/accounts/reports_controller.rb b/app/controllers/api/v2/accounts/reports_controller.rb index bbc4dde7b..dedeb17bf 100644 --- a/app/controllers/api/v2/accounts/reports_controller.rb +++ b/app/controllers/api/v2/accounts/reports_controller.rb @@ -14,22 +14,22 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController def agents @report_data = generate_agents_report - generate_csv('agents_report', 'api/v2/accounts/reports/agents.csv.erb') + generate_csv('agents_report', 'api/v2/accounts/reports/agents') end def inboxes @report_data = generate_inboxes_report - generate_csv('inboxes_report', 'api/v2/accounts/reports/inboxes.csv.erb') + generate_csv('inboxes_report', 'api/v2/accounts/reports/inboxes') end def labels @report_data = generate_labels_report - generate_csv('labels_report', 'api/v2/accounts/reports/labels.csv.erb') + generate_csv('labels_report', 'api/v2/accounts/reports/labels') end def teams @report_data = generate_teams_report - generate_csv('teams_report', 'api/v2/accounts/reports/teams.csv.erb') + generate_csv('teams_report', 'api/v2/accounts/reports/teams') end def conversations @@ -43,7 +43,7 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController def generate_csv(filename, template) response.headers['Content-Type'] = 'text/csv' response.headers['Content-Disposition'] = "attachment; filename=#{filename}.csv" - render layout: false, template: template, format: 'csv' + render layout: false, template: template, formats: [:csv] end def check_authorization diff --git a/app/controllers/concerns/request_exception_handler.rb b/app/controllers/concerns/request_exception_handler.rb index 6e9ed04cc..2f53fdc2b 100644 --- a/app/controllers/concerns/request_exception_handler.rb +++ b/app/controllers/concerns/request_exception_handler.rb @@ -13,6 +13,8 @@ module RequestExceptionHandler render_not_found_error('Resource could not be found') rescue Pundit::NotAuthorizedError render_unauthorized('You are not authorized to do this action') + rescue ActionController::ParameterMissing => e + render_could_not_create_error(e.message) ensure # to address the thread variable leak issues in Puma/Thin webserver Current.reset diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb index 49993e6ee..84677c770 100644 --- a/app/controllers/dashboard_controller.rb +++ b/app/controllers/dashboard_controller.rb @@ -45,6 +45,7 @@ class DashboardController < ActionController::Base @portal = Portal.find_by(custom_domain: domain) return unless @portal + @locale = @portal.default_locale render 'public/api/v1/portals/show', layout: 'portal', portal: @portal and return end diff --git a/app/controllers/devise_overrides/confirmations_controller.rb b/app/controllers/devise_overrides/confirmations_controller.rb index 1a6dc4209..aaabbcf8c 100644 --- a/app/controllers/devise_overrides/confirmations_controller.rb +++ b/app/controllers/devise_overrides/confirmations_controller.rb @@ -14,7 +14,7 @@ class DeviseOverrides::ConfirmationsController < Devise::ConfirmationsController def render_confirmation_success send_auth_headers(@confirmable) - render partial: 'devise/auth.json', locals: { resource: @confirmable } + render partial: 'devise/auth', formats: [:json], locals: { resource: @confirmable } end def render_confirmation_error diff --git a/app/controllers/devise_overrides/passwords_controller.rb b/app/controllers/devise_overrides/passwords_controller.rb index 501b9f90c..06092c5ab 100644 --- a/app/controllers/devise_overrides/passwords_controller.rb +++ b/app/controllers/devise_overrides/passwords_controller.rb @@ -11,7 +11,7 @@ class DeviseOverrides::PasswordsController < Devise::PasswordsController @recoverable = User.find_by(reset_password_token: reset_password_token) if @recoverable && reset_password_and_confirmation(@recoverable) send_auth_headers(@recoverable) - render partial: 'devise/auth.json', locals: { resource: @recoverable } + render partial: 'devise/auth', formats: [:json], locals: { resource: @recoverable } else render json: { message: 'Invalid token', redirect_url: '/' }, status: :unprocessable_entity end diff --git a/app/controllers/devise_overrides/sessions_controller.rb b/app/controllers/devise_overrides/sessions_controller.rb index 3bbc6ee3b..831c41ddc 100644 --- a/app/controllers/devise_overrides/sessions_controller.rb +++ b/app/controllers/devise_overrides/sessions_controller.rb @@ -16,7 +16,7 @@ class DeviseOverrides::SessionsController < ::DeviseTokenAuth::SessionsControlle end def render_create_success - render partial: 'devise/auth.json', locals: { resource: @resource } + render partial: 'devise/auth', formats: [:json], locals: { resource: @resource } end private diff --git a/app/controllers/devise_overrides/token_validations_controller.rb b/app/controllers/devise_overrides/token_validations_controller.rb index b9830d79d..64b7949ac 100644 --- a/app/controllers/devise_overrides/token_validations_controller.rb +++ b/app/controllers/devise_overrides/token_validations_controller.rb @@ -2,7 +2,7 @@ class DeviseOverrides::TokenValidationsController < ::DeviseTokenAuth::TokenVali def validate_token # @resource will have been set by set_user_by_token concern if @resource - render 'devise/token.json' + render 'devise/token', formats: [:json] else render_validate_token_error end diff --git a/app/controllers/platform/api/v1/users_controller.rb b/app/controllers/platform/api/v1/users_controller.rb index 12c87deb5..2c8995f81 100644 --- a/app/controllers/platform/api/v1/users_controller.rb +++ b/app/controllers/platform/api/v1/users_controller.rb @@ -51,6 +51,6 @@ class Platform::Api::V1::UsersController < PlatformController end def user_params - params.permit(:name, :email, :password, custom_attributes: {}) + params.permit(:name, :display_name, :email, :password, custom_attributes: {}) end end diff --git a/app/controllers/public/api/v1/inboxes/contacts_controller.rb b/app/controllers/public/api/v1/inboxes/contacts_controller.rb index eb794f2a0..1fde3051e 100644 --- a/app/controllers/public/api/v1/inboxes/contacts_controller.rb +++ b/app/controllers/public/api/v1/inboxes/contacts_controller.rb @@ -4,7 +4,7 @@ class Public::Api::V1::Inboxes::ContactsController < Public::Api::V1::InboxesCon def create source_id = params[:source_id] || SecureRandom.uuid - @contact_inbox = ::ContactBuilder.new( + @contact_inbox = ::ContactInboxWithContactBuilder.new( source_id: source_id, inbox: @inbox_channel.inbox, contact_attributes: permitted_params.except(:identifier, :identifier_hash) diff --git a/app/javascript/dashboard/App.vue b/app/javascript/dashboard/App.vue index 7243e566e..0ff94efa5 100644 --- a/app/javascript/dashboard/App.vue +++ b/app/javascript/dashboard/App.vue @@ -87,6 +87,9 @@ export default { }, async initializeAccount() { await this.$store.dispatch('accounts/get'); + this.$store.dispatch('setActiveAccount', { + accountId: this.currentAccountId, + }); const { locale, latest_chatwoot_version: latestChatwootVersion, diff --git a/app/javascript/dashboard/api/agentBots.js b/app/javascript/dashboard/api/agentBots.js new file mode 100644 index 000000000..4de6fcee0 --- /dev/null +++ b/app/javascript/dashboard/api/agentBots.js @@ -0,0 +1,9 @@ +import ApiClient from './ApiClient'; + +class AgentBotsAPI extends ApiClient { + constructor() { + super('agent_bots', { accountScoped: true }); + } +} + +export default new AgentBotsAPI(); diff --git a/app/javascript/dashboard/api/auth.js b/app/javascript/dashboard/api/auth.js index 76042103f..ef1762f46 100644 --- a/app/javascript/dashboard/api/auth.js +++ b/app/javascript/dashboard/api/auth.js @@ -147,4 +147,13 @@ export default { deleteAvatar() { return axios.delete(endPoints('deleteAvatar').url); }, + + setActiveAccount({ accountId }) { + const urlData = endPoints('setActiveAccount'); + return axios.put(urlData.url, { + profile: { + account_id: accountId, + }, + }); + }, }; diff --git a/app/javascript/dashboard/api/endPoints.js b/app/javascript/dashboard/api/endPoints.js index c9d0955ef..8deb8d56a 100644 --- a/app/javascript/dashboard/api/endPoints.js +++ b/app/javascript/dashboard/api/endPoints.js @@ -40,6 +40,10 @@ const endPoints = { deleteAvatar: { url: '/api/v1/profile/avatar', }, + + setActiveAccount: { + url: '/api/v1/profile/set_active_account', + }, }; export default page => { diff --git a/app/javascript/dashboard/api/macros.js b/app/javascript/dashboard/api/macros.js new file mode 100644 index 000000000..7b123c9e8 --- /dev/null +++ b/app/javascript/dashboard/api/macros.js @@ -0,0 +1,16 @@ +/* global axios */ +import ApiClient from './ApiClient'; + +class MacrosAPI extends ApiClient { + constructor() { + super('macros', { accountScoped: true }); + } + + executeMacro({ macroId, conversationIds }) { + return axios.post(`${this.url}/${macroId}/execute`, { + conversation_ids: conversationIds, + }); + } +} + +export default new MacrosAPI(); diff --git a/app/javascript/dashboard/api/specs/agentBots.spec.js b/app/javascript/dashboard/api/specs/agentBots.spec.js new file mode 100644 index 000000000..c89dbfdf5 --- /dev/null +++ b/app/javascript/dashboard/api/specs/agentBots.spec.js @@ -0,0 +1,13 @@ +import AgentBotsAPI from '../agentBots'; +import ApiClient from '../ApiClient'; + +describe('#AgentBotsAPI', () => { + it('creates correct instance', () => { + expect(AgentBotsAPI).toBeInstanceOf(ApiClient); + expect(AgentBotsAPI).toHaveProperty('get'); + expect(AgentBotsAPI).toHaveProperty('show'); + expect(AgentBotsAPI).toHaveProperty('create'); + expect(AgentBotsAPI).toHaveProperty('update'); + expect(AgentBotsAPI).toHaveProperty('delete'); + }); +}); diff --git a/app/javascript/dashboard/api/specs/macros.spec.js b/app/javascript/dashboard/api/specs/macros.spec.js new file mode 100644 index 000000000..94e936521 --- /dev/null +++ b/app/javascript/dashboard/api/specs/macros.spec.js @@ -0,0 +1,14 @@ +import macros from '../macros'; +import ApiClient from '../ApiClient'; + +describe('#macrosAPI', () => { + it('creates correct instance', () => { + expect(macros).toBeInstanceOf(ApiClient); + expect(macros).toHaveProperty('get'); + expect(macros).toHaveProperty('create'); + expect(macros).toHaveProperty('update'); + expect(macros).toHaveProperty('delete'); + expect(macros).toHaveProperty('show'); + expect(macros.url).toBe('/api/v1/macros'); + }); +}); diff --git a/app/javascript/dashboard/assets/scss/widgets/_buttons.scss b/app/javascript/dashboard/assets/scss/widgets/_buttons.scss index 9d3f84b25..478045000 100644 --- a/app/javascript/dashboard/assets/scss/widgets/_buttons.scss +++ b/app/javascript/dashboard/assets/scss/widgets/_buttons.scss @@ -113,9 +113,22 @@ $default-button-height: 4.0rem; } &.clear { + color: var(--w-700); + + &.secondary { + color: var(--s-700) + } + + &.success { + color: var(--g-700) + } + + &.alert { + color: var(--r-700) + } &.warning { - color: var(--y-600); + color: var(--y-700) } &:hover { @@ -146,6 +159,8 @@ $default-button-height: 4.0rem; &.small { height: var(--space-large); + padding-bottom: var(--space-smaller); + padding-top: var(--space-smaller); } &.large { diff --git a/app/javascript/dashboard/assets/scss/widgets/_modal.scss b/app/javascript/dashboard/assets/scss/widgets/_modal.scss index fc497f069..543a60797 100644 --- a/app/javascript/dashboard/assets/scss/widgets/_modal.scss +++ b/app/javascript/dashboard/assets/scss/widgets/_modal.scss @@ -14,15 +14,9 @@ } .modal--close { - border-radius: 50%; - color: $color-heading; - cursor: pointer; - font-size: $font-size-big; - line-height: $space-normal; - padding: $space-normal; position: absolute; - right: $space-micro; - top: $space-micro; + right: $space-small; + top: $space-small; &:hover { background: $color-background; diff --git a/app/javascript/dashboard/components/Modal.vue b/app/javascript/dashboard/components/Modal.vue index f4cad844a..d5dd55ffa 100644 --- a/app/javascript/dashboard/components/Modal.vue +++ b/app/javascript/dashboard/components/Modal.vue @@ -7,9 +7,13 @@ @click="onBackDropClick" >
- +
diff --git a/app/javascript/dashboard/components/layout/Sidebar.vue b/app/javascript/dashboard/components/layout/Sidebar.vue index 314b594d2..b919be2cd 100644 --- a/app/javascript/dashboard/components/layout/Sidebar.vue +++ b/app/javascript/dashboard/components/layout/Sidebar.vue @@ -73,14 +73,14 @@ export default { computed: { ...mapGetters({ - currentUser: 'getCurrentUser', - globalConfig: 'globalConfig/get', - isACustomBrandedInstance: 'globalConfig/isACustomBrandedInstance', - isOnChatwootCloud: 'globalConfig/isOnChatwootCloud', - inboxes: 'inboxes/getInboxes', accountId: 'getCurrentAccountId', currentRole: 'getCurrentRole', + currentUser: 'getCurrentUser', + globalConfig: 'globalConfig/get', + inboxes: 'inboxes/getInboxes', + isACustomBrandedInstance: 'globalConfig/isACustomBrandedInstance', isFeatureEnabledonAccount: 'accounts/isFeatureEnabledonAccount', + isOnChatwootCloud: 'globalConfig/isOnChatwootCloud', labels: 'labels/getLabelsOnSidebar', teams: 'teams/getMyTeams', }), diff --git a/app/javascript/dashboard/components/layout/config/sidebarItems/primaryMenu.js b/app/javascript/dashboard/components/layout/config/sidebarItems/primaryMenu.js index cc4287503..fa4633ec1 100644 --- a/app/javascript/dashboard/components/layout/config/sidebarItems/primaryMenu.js +++ b/app/javascript/dashboard/components/layout/config/sidebarItems/primaryMenu.js @@ -39,7 +39,7 @@ const primaryMenuItems = accountId => [ label: 'HELP_CENTER.TITLE', featureFlag: 'help_center', toState: frontendURL(`accounts/${accountId}/portals`), - toStateName: 'list_all_portals', + toStateName: 'default_portal_articles', roles: ['administrator'], }, { diff --git a/app/javascript/dashboard/components/layout/config/sidebarItems/settings.js b/app/javascript/dashboard/components/layout/config/sidebarItems/settings.js index 990b35d4a..768e42ea5 100644 --- a/app/javascript/dashboard/components/layout/config/sidebarItems/settings.js +++ b/app/javascript/dashboard/components/layout/config/sidebarItems/settings.js @@ -1,45 +1,58 @@ +import { FEATURE_FLAGS } from '../../../../featureFlags'; import { frontendURL } from '../../../../helper/URLHelper'; const settings = accountId => ({ parentNav: 'settings', routes: [ + 'agent_bots', 'agent_list', - 'canned_list', - 'labels_list', - 'settings_inbox', 'attributes_list', - 'settings_inbox_new', - 'settings_inbox_list', - 'settings_inbox_show', - 'settings_inboxes_page_channel', - 'settings_inboxes_add_agents', - 'settings_inbox_finish', - 'settings_integrations', - 'settings_integrations_webhook', - 'settings_integrations_integration', - 'settings_applications', - 'settings_integrations_dashboard_apps', - 'settings_applications_webhook', - 'settings_applications_integration', - 'general_settings', + 'automation_list', + 'billing_settings_index', + 'canned_list', 'general_settings_index', + 'general_settings', + 'labels_list', + 'macros_edit', + 'macros_new', + 'macros_wrapper', + 'settings_applications_integration', + 'settings_applications_webhook', + 'settings_applications', + 'settings_inbox_finish', + 'settings_inbox_list', + 'settings_inbox_new', + 'settings_inbox_show', + 'settings_inbox', + 'settings_inboxes_add_agents', + 'settings_inboxes_page_channel', + 'settings_integrations_dashboard_apps', + 'settings_integrations_integration', + 'settings_integrations_webhook', + 'settings_integrations', + 'settings_teams_add_agents', + 'settings_teams_edit_finish', + 'settings_teams_edit_members', + 'settings_teams_edit', + 'settings_teams_finish', 'settings_teams_list', 'settings_teams_new', - 'settings_teams_add_agents', - 'settings_teams_finish', - 'settings_teams_edit', - 'settings_teams_edit_members', - 'settings_teams_edit_finish', - 'billing_settings_index', - 'automation_list', ], menuItems: [ + { + icon: 'briefcase', + label: 'ACCOUNT_SETTINGS', + hasSubMenu: false, + toState: frontendURL(`accounts/${accountId}/settings/general`), + toStateName: 'general_settings_index', + }, { icon: 'people', label: 'AGENTS', hasSubMenu: false, toState: frontendURL(`accounts/${accountId}/settings/agents/list`), toStateName: 'agent_list', + featureFlag: FEATURE_FLAGS.AGENT_MANAGEMENT, }, { icon: 'people-team', @@ -47,6 +60,7 @@ const settings = accountId => ({ hasSubMenu: false, toState: frontendURL(`accounts/${accountId}/settings/teams/list`), toStateName: 'settings_teams_list', + featureFlag: FEATURE_FLAGS.TEAM_MANAGEMENT, }, { icon: 'mail-inbox-all', @@ -54,6 +68,7 @@ const settings = accountId => ({ hasSubMenu: false, toState: frontendURL(`accounts/${accountId}/settings/inboxes/list`), toStateName: 'settings_inbox_list', + featureFlag: FEATURE_FLAGS.INBOX_MANAGEMENT, }, { icon: 'tag', @@ -61,6 +76,7 @@ const settings = accountId => ({ hasSubMenu: false, toState: frontendURL(`accounts/${accountId}/settings/labels/list`), toStateName: 'labels_list', + featureFlag: FEATURE_FLAGS.LABELS, }, { icon: 'code', @@ -70,13 +86,34 @@ const settings = accountId => ({ `accounts/${accountId}/settings/custom-attributes/list` ), toStateName: 'attributes_list', + featureFlag: FEATURE_FLAGS.CUSTOM_ATTRIBUTES, }, { icon: 'automation', label: 'AUTOMATION', + beta: true, hasSubMenu: false, toState: frontendURL(`accounts/${accountId}/settings/automation/list`), toStateName: 'automation_list', + featureFlag: FEATURE_FLAGS.AUTOMATIONS, + }, + { + icon: 'bot', + label: 'AGENT_BOTS', + beta: true, + hasSubMenu: false, + toState: frontendURL(`accounts/${accountId}/settings/agent-bots`), + toStateName: 'agent_bots', + featureFlag: FEATURE_FLAGS.AGENT_BOTS, + }, + { + icon: 'flash-settings', + label: 'MACROS', + hasSubMenu: false, + toState: frontendURL(`accounts/${accountId}/settings/macros`), + toStateName: 'macros_wrapper', + beta: true, + featureFlag: FEATURE_FLAGS.MACROS, }, { icon: 'chat-multiple', @@ -86,6 +123,7 @@ const settings = accountId => ({ `accounts/${accountId}/settings/canned-response/list` ), toStateName: 'canned_list', + featureFlag: FEATURE_FLAGS.CANNED_RESPONSES, }, { icon: 'flash-on', @@ -93,6 +131,7 @@ const settings = accountId => ({ hasSubMenu: false, toState: frontendURL(`accounts/${accountId}/settings/integrations`), toStateName: 'settings_integrations', + featureFlag: FEATURE_FLAGS.INTEGRATIONS, }, { icon: 'star-emphasis', @@ -100,6 +139,7 @@ const settings = accountId => ({ hasSubMenu: false, toState: frontendURL(`accounts/${accountId}/settings/applications`), toStateName: 'settings_applications', + featureFlag: FEATURE_FLAGS.INTEGRATIONS, }, { icon: 'credit-card-person', @@ -109,13 +149,6 @@ const settings = accountId => ({ toStateName: 'billing_settings_index', showOnlyOnCloud: true, }, - { - icon: 'settings', - label: 'ACCOUNT_SETTINGS', - hasSubMenu: false, - toState: frontendURL(`accounts/${accountId}/settings/general`), - toStateName: 'general_settings_index', - }, ], }); diff --git a/app/javascript/dashboard/components/layout/sidebarComponents/AccountSelector.vue b/app/javascript/dashboard/components/layout/sidebarComponents/AccountSelector.vue index e6e7df48e..ad432f42a 100644 --- a/app/javascript/dashboard/components/layout/sidebarComponents/AccountSelector.vue +++ b/app/javascript/dashboard/components/layout/sidebarComponents/AccountSelector.vue @@ -8,25 +8,33 @@ :header-title="$t('SIDEBAR_ITEMS.CHANGE_ACCOUNTS')" :header-content="$t('SIDEBAR_ITEMS.SELECTOR_SUBTITLE')" /> -
- - +
+
+
-
- - -
@@ -126,7 +127,6 @@ import alertMixin from 'shared/mixins/alertMixin'; import contentTypeMixin from 'shared/mixins/contentTypeMixin'; import { MESSAGE_TYPE, MESSAGE_STATUS } from 'shared/constants/messages'; import { generateBotMessageContent } from './helpers/botMessageContentHelper'; -import { copyTextToClipboard } from 'shared/helpers/clipboard'; export default { components: { @@ -408,11 +408,6 @@ export default { this.showAlert(this.$t('CONVERSATION.FAIL_DELETE_MESSSAGE')); } }, - async handleCopy() { - await copyTextToClipboard(this.data.content); - this.showAlert(this.$t('CONTACT_PANEL.COPY_SUCCESSFUL')); - this.showContextMenu = false; - }, async retrySendMessage() { await this.$store.dispatch('sendMessageWithData', this.data); }, @@ -425,6 +420,8 @@ export default { diff --git a/app/javascript/dashboard/components/widgets/conversation/contextMenu/Index.vue b/app/javascript/dashboard/components/widgets/conversation/contextMenu/Index.vue index dc5a8887f..4a1713165 100644 --- a/app/javascript/dashboard/components/widgets/conversation/contextMenu/Index.vue +++ b/app/javascript/dashboard/components/widgets/conversation/contextMenu/Index.vue @@ -141,9 +141,19 @@ export default { assignableAgentsUiFlags: 'inboxAssignableAgents/getUIFlags', }), assignableAgents() { - return this.$store.getters['inboxAssignableAgents/getAssignableAgents']( - this.inboxId - ); + return [ + { + confirmed: true, + name: 'None', + id: null, + role: 'agent', + account_id: 0, + email: 'None', + }, + ...this.$store.getters['inboxAssignableAgents/getAssignableAgents']( + this.inboxId + ), + ]; }, }, mounted() { diff --git a/app/javascript/dashboard/components/widgets/conversation/conversationBulkActions/AgentSelector.vue b/app/javascript/dashboard/components/widgets/conversation/conversationBulkActions/AgentSelector.vue index caee88392..518aac2f6 100644 --- a/app/javascript/dashboard/components/widgets/conversation/conversationBulkActions/AgentSelector.vue +++ b/app/javascript/dashboard/components/widgets/conversation/conversationBulkActions/AgentSelector.vue @@ -57,7 +57,7 @@
-

+

{{ $t('BULK_ACTION.ASSIGN_CONFIRMATION_LABEL', { conversationCount, @@ -67,6 +67,15 @@ {{ selectedAgent.name }} + ? +

+

+ {{ + $t('BULK_ACTION.UNASSIGN_CONFIRMATION_LABEL', { + conversationCount, + conversationLabel, + }) + }}

- {{ $t('BULK_ACTION.ASSIGN_LABEL') }} + {{ $t('BULK_ACTION.YES') }}
@@ -131,7 +140,17 @@ export default { agent.name.toLowerCase().includes(this.query.toLowerCase()) ); } - return this.assignableAgents; + return [ + { + confirmed: true, + name: 'None', + id: null, + role: 'agent', + account_id: 0, + email: 'None', + }, + ...this.assignableAgents, + ]; }, assignableAgents() { return this.$store.getters['inboxAssignableAgents/getAssignableAgents']( diff --git a/app/javascript/dashboard/components/widgets/conversation/conversationBulkActions/Index.vue b/app/javascript/dashboard/components/widgets/conversation/conversationBulkActions/Index.vue index 70b0464cc..600ff4393 100644 --- a/app/javascript/dashboard/components/widgets/conversation/conversationBulkActions/Index.vue +++ b/app/javascript/dashboard/components/widgets/conversation/conversationBulkActions/Index.vue @@ -181,7 +181,7 @@ export default { color: var(--y-700); font-size: var(--font-size-mini); margin-top: var(--space-small); - padding: var(--space-half) var(--space-one); + padding: var(--space-smaller) var(--space-small); } .popover-animation-enter-active, diff --git a/app/javascript/dashboard/components/widgets/mentions/MentionBox.vue b/app/javascript/dashboard/components/widgets/mentions/MentionBox.vue index e5963d0ee..4613d8357 100644 --- a/app/javascript/dashboard/components/widgets/mentions/MentionBox.vue +++ b/app/javascript/dashboard/components/widgets/mentions/MentionBox.vue @@ -20,7 +20,9 @@ diff --git a/app/javascript/dashboard/modules/widget-preview/components/Widget.vue b/app/javascript/dashboard/modules/widget-preview/components/Widget.vue index d2e5622a2..8ec864042 100644 --- a/app/javascript/dashboard/modules/widget-preview/components/Widget.vue +++ b/app/javascript/dashboard/modules/widget-preview/components/Widget.vue @@ -249,8 +249,8 @@ export default { display: flex; align-items: center; border-radius: calc(var(--border-radius-small) * 10); - height: calc(var(--space-three) * 2); - width: calc(var(--space-three) * 2); + height: calc(var(--space-large) * 2); + width: calc(var(--space-large) * 2); position: relative; overflow-wrap: anywhere; cursor: pointer; @@ -274,7 +274,7 @@ export default { display: inline; height: var(--space-medium); width: var(--space-micro); - left: var(--space-three); + left: var(--space-large); position: absolute; } diff --git a/app/javascript/dashboard/routes/dashboard/commands/goToCommandHotKeys.js b/app/javascript/dashboard/routes/dashboard/commands/goToCommandHotKeys.js index 9a5b09f2e..b1d17dcbb 100644 --- a/app/javascript/dashboard/routes/dashboard/commands/goToCommandHotKeys.js +++ b/app/javascript/dashboard/routes/dashboard/commands/goToCommandHotKeys.js @@ -16,6 +16,8 @@ import { ICON_CONVERSATION_REPORTS, } from './CommandBarIcons'; import { frontendURL } from '../../../helper/URLHelper'; +import { mapGetters } from 'vuex'; +import { FEATURE_FLAGS } from '../../../featureFlags'; const GO_TO_COMMANDS = [ { @@ -86,6 +88,7 @@ const GO_TO_COMMANDS = [ id: 'open_agent_settings', section: 'COMMAND_BAR.SECTIONS.SETTINGS', title: 'COMMAND_BAR.COMMANDS.GO_TO_SETTINGS_AGENTS', + featureFlag: FEATURE_FLAGS.AGENT_MANAGEMENT, icon: ICON_AGENT_REPORTS, path: accountId => `accounts/${accountId}/settings/agents/list`, role: ['administrator'], @@ -93,6 +96,7 @@ const GO_TO_COMMANDS = [ { id: 'open_team_settings', title: 'COMMAND_BAR.COMMANDS.GO_TO_SETTINGS_TEAMS', + featureFlag: FEATURE_FLAGS.TEAM_MANAGEMENT, section: 'COMMAND_BAR.SECTIONS.SETTINGS', icon: ICON_TEAM_REPORTS, path: accountId => `accounts/${accountId}/settings/teams/list`, @@ -101,6 +105,7 @@ const GO_TO_COMMANDS = [ { id: 'open_inbox_settings', title: 'COMMAND_BAR.COMMANDS.GO_TO_SETTINGS_INBOXES', + featureFlag: FEATURE_FLAGS.INBOX_MANAGEMENT, section: 'COMMAND_BAR.SECTIONS.SETTINGS', icon: ICON_INBOXES, path: accountId => `accounts/${accountId}/settings/inboxes/list`, @@ -109,6 +114,7 @@ const GO_TO_COMMANDS = [ { id: 'open_label_settings', title: 'COMMAND_BAR.COMMANDS.GO_TO_SETTINGS_LABELS', + featureFlag: FEATURE_FLAGS.LABELS, section: 'COMMAND_BAR.SECTIONS.SETTINGS', icon: ICON_LABELS, path: accountId => `accounts/${accountId}/settings/labels/list`, @@ -117,6 +123,7 @@ const GO_TO_COMMANDS = [ { id: 'open_canned_response_settings', title: 'COMMAND_BAR.COMMANDS.GO_TO_SETTINGS_CANNED_RESPONSES', + featureFlag: FEATURE_FLAGS.CANNED_RESPONSES, section: 'COMMAND_BAR.SECTIONS.SETTINGS', icon: ICON_CANNED_RESPONSE, path: accountId => `accounts/${accountId}/settings/canned-response/list`, @@ -125,6 +132,7 @@ const GO_TO_COMMANDS = [ { id: 'open_applications_settings', title: 'COMMAND_BAR.COMMANDS.GO_TO_SETTINGS_APPLICATIONS', + featureFlag: FEATURE_FLAGS.INTEGRATIONS, section: 'COMMAND_BAR.SECTIONS.SETTINGS', icon: ICON_APPS, path: accountId => `accounts/${accountId}/settings/applications`, @@ -158,8 +166,20 @@ const GO_TO_COMMANDS = [ export default { computed: { + ...mapGetters({ + accountId: 'getCurrentAccountId', + isFeatureEnabledonAccount: 'accounts/isFeatureEnabledonAccount', + }), goToCommandHotKeys() { - let commands = GO_TO_COMMANDS; + let commands = GO_TO_COMMANDS.filter(cmd => { + if (cmd.featureFlag) { + return this.isFeatureEnabledonAccount( + this.accountId, + cmd.featureFlag + ); + } + return true; + }); if (!this.isAdmin) { commands = commands.filter(command => command.role.includes('agent')); diff --git a/app/javascript/dashboard/routes/dashboard/conversation/contact/ContactInfoRow.vue b/app/javascript/dashboard/routes/dashboard/conversation/contact/ContactInfoRow.vue index 33559e123..4e33bc7fa 100644 --- a/app/javascript/dashboard/routes/dashboard/conversation/contact/ContactInfoRow.vue +++ b/app/javascript/dashboard/routes/dashboard/conversation/contact/ContactInfoRow.vue @@ -10,10 +10,11 @@ diff --git a/app/javascript/dashboard/routes/dashboard/conversation/contact/ConversationForm.vue b/app/javascript/dashboard/routes/dashboard/conversation/contact/ConversationForm.vue index 9dda5c02c..c9294aa2a 100644 --- a/app/javascript/dashboard/routes/dashboard/conversation/contact/ConversationForm.vue +++ b/app/javascript/dashboard/routes/dashboard/conversation/contact/ConversationForm.vue @@ -239,7 +239,7 @@ export default { const hasNextWord = value.includes(' '); const isShortCodeActive = this.hasSlashCommand && !hasNextWord; if (isShortCodeActive) { - this.cannedResponseSearchKey = value.substr(1, value.length); + this.cannedResponseSearchKey = value.substring(1); this.showCannedResponseMenu = true; } else { this.cannedResponseSearchKey = ''; diff --git a/app/javascript/dashboard/routes/dashboard/helpcenter/components/ArticleEditor.vue b/app/javascript/dashboard/routes/dashboard/helpcenter/components/ArticleEditor.vue index fef43a7fa..228118243 100644 --- a/app/javascript/dashboard/routes/dashboard/helpcenter/components/ArticleEditor.vue +++ b/app/javascript/dashboard/routes/dashboard/helpcenter/components/ArticleEditor.vue @@ -46,6 +46,7 @@ export default { return { articleTitle: '', articleContent: '', + saveArticle: () => {}, }; }, mounted() { diff --git a/app/javascript/dashboard/routes/dashboard/helpcenter/components/ArticleItem.vue b/app/javascript/dashboard/routes/dashboard/helpcenter/components/ArticleItem.vue index cecf1f39e..254302074 100644 --- a/app/javascript/dashboard/routes/dashboard/helpcenter/components/ArticleItem.vue +++ b/app/javascript/dashboard/routes/dashboard/helpcenter/components/ArticleItem.vue @@ -4,7 +4,7 @@
-
+
{{ title }}
@@ -16,7 +16,17 @@
- {{ category.name }} + + + {{ category.name }} + + @@ -43,6 +53,8 @@ @@ -140,4 +160,9 @@ td { } } } + +.category-link-content { + max-width: 16rem; + line-height: 1.5; +} diff --git a/app/javascript/dashboard/routes/dashboard/helpcenter/components/HelpCenterLayout.vue b/app/javascript/dashboard/routes/dashboard/helpcenter/components/HelpCenterLayout.vue index 528b55575..562d6bd07 100644 --- a/app/javascript/dashboard/routes/dashboard/helpcenter/components/HelpCenterLayout.vue +++ b/app/javascript/dashboard/routes/dashboard/helpcenter/components/HelpCenterLayout.vue @@ -59,6 +59,7 @@ import HelpCenterSidebar from '../components/Sidebar/Sidebar.vue'; import CommandBar from 'dashboard/routes/dashboard/commands/commandbar.vue'; import WootKeyShortcutModal from 'dashboard/components/widgets/modal/WootKeyShortcutModal'; import NotificationPanel from 'dashboard/routes/dashboard/notifications/components/NotificationPanel'; +import uiSettingsMixin from 'dashboard/mixins/uiSettings'; import portalMixin from '../mixins/portalMixin'; import AddCategory from '../pages/categories/AddCategory'; @@ -72,7 +73,7 @@ export default { PortalPopover, AddCategory, }, - mixins: [portalMixin], + mixins: [portalMixin, uiSettingsMixin], data() { return { isSidebarOpen: false, @@ -231,7 +232,13 @@ export default { }, updated() { const slug = this.$route.params.portalSlug; - if (slug) this.lastActivePortalSlug = slug; + if (slug) { + this.lastActivePortalSlug = slug; + this.updateUISettings({ + last_active_portal_slug: slug, + last_active_locale_code: this.selectedLocaleInPortal, + }); + } }, methods: { handleResize() { diff --git a/app/javascript/dashboard/routes/dashboard/helpcenter/components/PortalListItem.vue b/app/javascript/dashboard/routes/dashboard/helpcenter/components/PortalListItem.vue index e0e002002..0354ad22b 100644 --- a/app/javascript/dashboard/routes/dashboard/helpcenter/components/PortalListItem.vue +++ b/app/javascript/dashboard/routes/dashboard/helpcenter/components/PortalListItem.vue @@ -188,13 +188,15 @@ diff --git a/app/javascript/dashboard/routes/dashboard/helpcenter/pages/categories/CategoryListItem.vue b/app/javascript/dashboard/routes/dashboard/helpcenter/pages/categories/CategoryListItem.vue index 3a8b7a850..45b23e5ed 100644 --- a/app/javascript/dashboard/routes/dashboard/helpcenter/pages/categories/CategoryListItem.vue +++ b/app/javascript/dashboard/routes/dashboard/helpcenter/pages/categories/CategoryListItem.vue @@ -118,6 +118,6 @@ table { justify-content: center; color: var(--s-500); font-size: var(--font-size-default); - margin-top: var(--space-three); + margin-top: var(--space-large); } diff --git a/app/javascript/dashboard/routes/dashboard/settings/account/account.routes.js b/app/javascript/dashboard/routes/dashboard/settings/account/account.routes.js index c2ef17129..5e3df43b4 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/account/account.routes.js +++ b/app/javascript/dashboard/routes/dashboard/settings/account/account.routes.js @@ -10,7 +10,7 @@ export default { component: SettingsContent, props: { headerTitle: 'GENERAL_SETTINGS.TITLE', - icon: 'settings', + icon: 'briefcase', showNewButton: false, }, children: [ diff --git a/app/javascript/dashboard/routes/dashboard/settings/agentBots/Index.vue b/app/javascript/dashboard/routes/dashboard/settings/agentBots/Index.vue new file mode 100644 index 000000000..2020f044e --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/agentBots/Index.vue @@ -0,0 +1,18 @@ + + + diff --git a/app/javascript/dashboard/routes/dashboard/settings/agentBots/agentBot.routes.js b/app/javascript/dashboard/routes/dashboard/settings/agentBots/agentBot.routes.js new file mode 100644 index 000000000..e06f18a0b --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/agentBots/agentBot.routes.js @@ -0,0 +1,40 @@ +import SettingsContent from '../Wrapper'; +const Bot = () => import('./Index.vue'); +const CsmlEditBot = () => import('./csml/Edit.vue'); +const CsmlNewBot = () => import('./csml/New.vue'); +import { frontendURL } from '../../../../helper/URLHelper'; + +export default { + routes: [ + { + path: frontendURL('accounts/:accountId/settings/agent-bots'), + roles: ['administrator'], + component: SettingsContent, + props: { + headerTitle: 'AGENT_BOTS.HEADER', + icon: 'bot', + showNewButton: false, + }, + children: [ + { + path: '', + name: 'agent_bots', + component: Bot, + roles: ['administrator'], + }, + { + path: 'csml/new', + name: 'agent_bots_csml_new', + component: CsmlNewBot, + roles: ['administrator'], + }, + { + path: 'csml/:botId', + name: 'agent_bots_csml_edit', + component: CsmlEditBot, + roles: ['administrator'], + }, + ], + }, + ], +}; diff --git a/app/javascript/dashboard/routes/dashboard/settings/agentBots/csml/Edit.vue b/app/javascript/dashboard/routes/dashboard/settings/agentBots/csml/Edit.vue new file mode 100644 index 000000000..112961193 --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/agentBots/csml/Edit.vue @@ -0,0 +1,6 @@ + + diff --git a/app/javascript/dashboard/routes/dashboard/settings/agentBots/csml/New.vue b/app/javascript/dashboard/routes/dashboard/settings/agentBots/csml/New.vue new file mode 100644 index 000000000..84039a483 --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/agentBots/csml/New.vue @@ -0,0 +1,6 @@ + + diff --git a/app/javascript/dashboard/routes/dashboard/settings/canned/AddCanned.vue b/app/javascript/dashboard/routes/dashboard/settings/canned/AddCanned.vue index 897c722cd..ea0ca4b01 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/canned/AddCanned.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/canned/AddCanned.vue @@ -65,6 +65,10 @@ export default { }, mixins: [alertMixin], props: { + responseContent: { + type: String, + default: '', + }, onClose: { type: Function, default: () => {}, @@ -73,10 +77,7 @@ export default { data() { return { shortCode: '', - content: '', - - vertical: 'bottom', - horizontal: 'center', + content: this.responseContent || '', addCanned: { showLoading: false, message: '', diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/FinishSetup.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/FinishSetup.vue index 6d0a0a597..7931016e4 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/inbox/FinishSetup.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/FinishSetup.vue @@ -19,11 +19,21 @@ :script="currentInbox.callback_webhook_url" />
-
+
+

+ {{ $t('INBOX_MGMT.ADD.WHATSAPP.API_CALLBACK.WEBHOOK_URL') }} +

+ +

+ {{ + $t( + 'INBOX_MGMT.ADD.WHATSAPP.API_CALLBACK.WEBHOOK_VERIFICATION_TOKEN' + ) + }} +

@@ -99,7 +109,7 @@ export default { isASmsInbox() { return this.currentInbox.channel_type === 'Channel::Sms'; }, - isAWhatsappWhatsappCloudInbox() { + isWhatsAppCloudInbox() { return ( this.currentInbox.channel_type === 'Channel::Whatsapp' && this.currentInbox.provider === 'whatsapp_cloud' @@ -124,7 +134,7 @@ export default { )}`; } - if (this.isAWhatsappWhatsappCloudInbox) { + if (this.isWhatsAppCloudInbox) { return `${this.$t('INBOX_MGMT.FINISH.MESSAGE')}. ${this.$t( 'INBOX_MGMT.ADD.WHATSAPP.API_CALLBACK.SUBTITLE' )}`; @@ -159,4 +169,10 @@ export default { .settings-button { margin-right: var(--space-small); } + +.config--label { + color: var(--b-600); + font-weight: var(--font-weight-medium); + margin-top: var(--space-large); +} diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/WidgetBuilder.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/WidgetBuilder.vue index 8da5183d7..bdc20e14e 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/inbox/WidgetBuilder.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/WidgetBuilder.vue @@ -243,7 +243,7 @@ export default { this.$t('INBOX_MGMT.WIDGET_BUILDER.SCRIPT_SETTINGS', { options: JSON.stringify(options), }) + - script.substring(13, script.length) + script.substring(13) ); }, getWidgetViewOptions() { diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/CloudWhatsapp.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/CloudWhatsapp.vue index 073dc9c9a..a182bb8b2 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/CloudWhatsapp.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/CloudWhatsapp.vue @@ -85,25 +85,6 @@
-
- -
-
+
+ Macros +
+ + + + + diff --git a/app/javascript/dashboard/routes/dashboard/settings/macros/MacroEditor.vue b/app/javascript/dashboard/routes/dashboard/settings/macros/MacroEditor.vue new file mode 100644 index 000000000..2de1f787c --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/macros/MacroEditor.vue @@ -0,0 +1,9 @@ + + + + + diff --git a/app/javascript/dashboard/routes/dashboard/settings/macros/macros.routes.js b/app/javascript/dashboard/routes/dashboard/settings/macros/macros.routes.js new file mode 100644 index 000000000..9752b79fb --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/macros/macros.routes.js @@ -0,0 +1,38 @@ +import SettingsContent from '../Wrapper'; +import Macros from './Index'; +const MacroEditor = () => import('./MacroEditor'); +import { frontendURL } from 'dashboard/helper/URLHelper'; + +export default { + routes: [ + { + path: frontendURL('accounts/:accountId/settings/macros'), + component: SettingsContent, + props: { + headerTitle: 'MACROS.HEADER', + icon: 'flash-settings', + showNewButton: false, + }, + children: [ + { + path: '', + name: 'macros_wrapper', + component: Macros, + roles: ['administrator', 'agent'], + }, + { + path: 'new', + name: 'macros_new', + component: MacroEditor, + roles: ['administrator', 'agent'], + }, + { + path: ':macroId/edit', + name: 'macros_edit', + component: MacroEditor, + roles: ['administrator', 'agent'], + }, + ], + }, + ], +}; diff --git a/app/javascript/dashboard/routes/dashboard/settings/profile/Index.vue b/app/javascript/dashboard/routes/dashboard/settings/profile/Index.vue index 53152f09d..23b5425e2 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/profile/Index.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/profile/Index.vue @@ -70,6 +70,31 @@
+
+
+

+ {{ $t('PROFILE_SETTINGS.FORM.SEND_MESSAGE.TITLE') }} +

+

+ {{ $t('PROFILE_SETTINGS.FORM.SEND_MESSAGE.NOTE') }} +

+
+
+ +
+
@@ -102,14 +127,19 @@ import alertMixin from 'shared/mixins/alertMixin'; import ChangePassword from './ChangePassword'; import MessageSignature from './MessageSignature'; import globalConfigMixin from 'shared/mixins/globalConfigMixin'; +import uiSettingsMixin, { + isEditorHotKeyEnabled, +} from 'dashboard/mixins/uiSettings'; +import PreviewCard from 'dashboard/components/ui/PreviewCard.vue'; export default { components: { NotificationSettings, ChangePassword, MessageSignature, + PreviewCard, }, - mixins: [alertMixin, globalConfigMixin], + mixins: [alertMixin, globalConfigMixin, uiSettingsMixin], data() { return { avatarFile: '', @@ -119,6 +149,28 @@ export default { email: '', isProfileUpdating: false, errorMessage: '', + keyOptions: [ + { + key: 'enter', + src: '/assets/images/dashboard/editor/enter-editor.png', + heading: this.$t( + 'PROFILE_SETTINGS.FORM.SEND_MESSAGE.CARD.ENTER_KEY.HEADING' + ), + content: this.$t( + 'PROFILE_SETTINGS.FORM.SEND_MESSAGE.CARD.ENTER_KEY.CONTENT' + ), + }, + { + key: 'cmd_enter', + src: '/assets/images/dashboard/editor/cmd-editor.png', + heading: this.$t( + 'PROFILE_SETTINGS.FORM.SEND_MESSAGE.CARD.CMD_ENTER_KEY.HEADING' + ), + content: this.$t( + 'PROFILE_SETTINGS.FORM.SEND_MESSAGE.CARD.CMD_ENTER_KEY.CONTENT' + ), + }, + ], }; }, validations: { @@ -158,6 +210,7 @@ export default { this.avatarUrl = this.currentUser.avatar_url; this.displayName = this.currentUser.display_name; }, + isEditorHotKeyEnabled, async updateUser() { this.$v.$touch(); if (this.$v.$invalid) { @@ -207,6 +260,12 @@ export default { showDeleteButton() { return this.avatarUrl && !this.avatarUrl.includes('www.gravatar.com'); }, + toggleEditorMessageKey(key) { + this.updateUISettings({ editor_message_key: key }); + this.showAlert( + this.$t('PROFILE_SETTINGS.FORM.SEND_MESSAGE.UPDATE_SUCCESS') + ); + }, }, }; @@ -216,18 +275,32 @@ export default { @import '~dashboard/assets/scss/mixins.scss'; .profile--settings { - padding: 24px; overflow: auto; + padding: 24px; } .profile--settings--row { @include border-normal-bottom; + align-items: center; + display: flex; padding: $space-normal; + .small-3 { padding: $space-normal $space-medium $space-normal 0; } + .small-9 { padding: $space-normal; } + + .card-preview { + display: flex; + flex-direction: row; + + .preview-button { + cursor: pointer; + margin-right: var(--space-normal); + } + } } diff --git a/app/javascript/dashboard/routes/dashboard/settings/profile/MessageSignature.vue b/app/javascript/dashboard/routes/dashboard/settings/profile/MessageSignature.vue index e08e4fdee..557f239dc 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/profile/MessageSignature.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/profile/MessageSignature.vue @@ -77,7 +77,7 @@ export default { methods: { initValues() { const { message_signature: messageSignature } = this.currentUser; - this.messageSignature = messageSignature; + this.messageSignature = messageSignature || ''; }, async updateSignature() { this.$v.$touch(); diff --git a/app/javascript/dashboard/routes/dashboard/settings/profile/NotificationSettings.vue b/app/javascript/dashboard/routes/dashboard/settings/profile/NotificationSettings.vue index bbf92b43d..60584d726 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/profile/NotificationSettings.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/profile/NotificationSettings.vue @@ -154,7 +154,7 @@ ) }}

-
+
{ + it('getMacros', () => { + const state = { records: macros }; + expect(getters.getMacros(state)).toEqual(macros); + }); + + it('getMacro', () => { + const state = { records: macros }; + expect(getters.getMacro(state)(22)).toEqual(macros[0]); + }); + + it('getUIFlags', () => { + const state = { + uiFlags: { + isFetching: true, + isCreating: false, + isUpdating: false, + isDeleting: false, + isExecuting: false, + }, + }; + expect(getters.getUIFlags(state)).toEqual({ + isFetching: true, + isCreating: false, + isUpdating: false, + isDeleting: false, + isExecuting: false, + }); + }); +}); diff --git a/app/javascript/dashboard/store/modules/specs/macros/mutations.spec.js b/app/javascript/dashboard/store/modules/specs/macros/mutations.spec.js new file mode 100644 index 000000000..436738638 --- /dev/null +++ b/app/javascript/dashboard/store/modules/specs/macros/mutations.spec.js @@ -0,0 +1,38 @@ +import types from '../../../mutation-types'; +import { mutations } from '../../macros'; +import macros from './fixtures'; +describe('#mutations', () => { + describe('#SET_MACROS', () => { + it('set macrtos records', () => { + const state = { records: [] }; + mutations[types.SET_MACROS](state, macros); + expect(state.records).toEqual(macros); + }); + }); + + describe('#ADD_MACRO', () => { + it('push newly created macro to the store', () => { + const state = { records: [macros[0]] }; + mutations[types.ADD_MACRO](state, macros[1]); + expect(state.records).toEqual([macros[0], macros[1]]); + }); + }); + + describe('#EDIT_MACRO', () => { + it('update macro record', () => { + const state = { records: [macros[0]] }; + mutations[types.EDIT_MACRO](state, macros[0]); + expect(state.records[0].name).toEqual( + 'Assign billing label and sales team and message user' + ); + }); + }); + + describe('#DELETE_MACRO', () => { + it('delete macro record', () => { + const state = { records: [macros[0]] }; + mutations[types.DELETE_MACRO](state, 22); + expect(state.records).toEqual([]); + }); + }); +}); diff --git a/app/javascript/dashboard/store/mutation-types.js b/app/javascript/dashboard/store/mutation-types.js index 17a2c78ee..bcb6ad9a5 100755 --- a/app/javascript/dashboard/store/mutation-types.js +++ b/app/javascript/dashboard/store/mutation-types.js @@ -245,4 +245,18 @@ export default { UPDATE_CATEGORY: 'UPDATE_CATEGORY', REMOVE_CATEGORY: 'REMOVE_CATEGORY', REMOVE_CATEGORY_ID: 'REMOVE_CATEGORY_ID', + + // Agent Bots + SET_AGENT_BOT_UI_FLAG: 'SET_AGENT_BOT_UI_FLAG', + SET_AGENT_BOTS: 'SET_AGENT_BOTS', + ADD_AGENT_BOT: 'ADD_AGENT_BOT', + EDIT_AGENT_BOT: 'EDIT_AGENT_BOT', + DELETE_AGENT_BOT: 'DELETE_AGENT_BOT', + + // MACROS + SET_MACROS_UI_FLAG: 'SET_MACROS_UI_FLAG', + SET_MACROS: 'SET_MACROS', + ADD_MACRO: 'ADD_MACRO', + EDIT_MACRO: 'EDIT_MACRO', + DELETE_MACRO: 'DELETE_MACRO', }; diff --git a/app/javascript/dashboard/store/utils/api.js b/app/javascript/dashboard/store/utils/api.js index 292ebca7e..93791b760 100644 --- a/app/javascript/dashboard/store/utils/api.js +++ b/app/javascript/dashboard/store/utils/api.js @@ -7,6 +7,7 @@ import { CHATWOOT_RESET, CHATWOOT_SET_USER, } from '../../helper/scriptHelpers'; +import { LocalStorage, LOCAL_STORAGE_KEYS } from '../../helper/localStorage'; Cookies.defaults = { sameSite: 'Lax' }; @@ -37,10 +38,15 @@ export const clearBrowserSessionCookies = () => { Cookies.remove('user'); }; +export const clearLocalStorageOnLogout = () => { + LocalStorage.remove(LOCAL_STORAGE_KEYS.DRAFT_MESSAGES); +}; + export const clearCookiesOnLogout = () => { window.bus.$emit(CHATWOOT_RESET); window.bus.$emit(ANALYTICS_RESET); clearBrowserSessionCookies(); + clearLocalStorageOnLogout(); const globalConfig = window.globalConfig || {}; const logoutRedirectLink = globalConfig.LOGOUT_REDIRECT_LINK || '/'; window.location = logoutRedirectLink; diff --git a/app/javascript/packs/sdk.js b/app/javascript/packs/sdk.js index 064033841..2fdd6c57b 100755 --- a/app/javascript/packs/sdk.js +++ b/app/javascript/packs/sdk.js @@ -10,7 +10,7 @@ import { getUserCookieName, hasUserKeys, } from '../sdk/cookieHelpers'; -import { addClass, removeClass } from '../sdk/DOMHelpers'; +import { addClasses, removeClasses } from '../sdk/DOMHelpers'; import { SDK_SET_BUBBLE_VISIBILITY } from 'shared/constants/sharedFrameEvents'; const runSDK = ({ baseUrl, websiteToken }) => { if (window.$chatwoot) { @@ -41,12 +41,12 @@ const runSDK = ({ baseUrl, websiteToken }) => { let widgetElm = document.querySelector('.woot--bubble-holder'); let widgetHolder = document.querySelector('.woot-widget-holder'); if (visibility === 'hide') { - addClass(widgetHolder, 'woot-widget--without-bubble'); - addClass(widgetElm, 'woot-hidden'); + addClasses(widgetHolder, 'woot-widget--without-bubble'); + addClasses(widgetElm, 'woot-hidden'); window.$chatwoot.hideMessageBubble = true; } else if (visibility === 'show') { - removeClass(widgetElm, 'woot-hidden'); - removeClass(widgetHolder, 'woot-widget--without-bubble'); + removeClasses(widgetElm, 'woot-hidden'); + removeClasses(widgetHolder, 'woot-widget--without-bubble'); window.$chatwoot.hideMessageBubble = false; } IFrameHelper.sendMessage(SDK_SET_BUBBLE_VISIBILITY, { diff --git a/app/javascript/sdk/DOMHelpers.js b/app/javascript/sdk/DOMHelpers.js index 2ac6188e6..47a45cc78 100644 --- a/app/javascript/sdk/DOMHelpers.js +++ b/app/javascript/sdk/DOMHelpers.js @@ -3,68 +3,20 @@ import { IFrameHelper } from './IFrameHelper'; export const loadCSS = () => { const css = document.createElement('style'); - css.type = 'text/css'; css.innerHTML = `${SDK_CSS}`; document.body.appendChild(css); }; -export const wootOn = (elm, event, fn) => { - if (document.addEventListener) { - elm.addEventListener(event, fn, false); - } else if (document.attachEvent) { - // <= IE 8 loses scope so need to apply, we add this to object so we - // can detach later (can't detach anonymous functions) - // eslint-disable-next-line - elm[event + fn] = function() { - // eslint-disable-next-line - return fn.apply(elm, arguments); - }; - elm.attachEvent(`on${event}`, elm[event + fn]); - } -}; - -export const classHelper = (classes, action, elm) => { - let search; - let replace; - let i; - let has = false; - if (classes) { - // Trim any whitespace - const classarray = classes.split(/\s+/); - for (i = 0; i < classarray.length; i += 1) { - search = new RegExp(`\\b${classarray[i]}\\b`, 'g'); - replace = new RegExp(` *${classarray[i]}\\b`, 'g'); - if (action === 'remove') { - // eslint-disable-next-line - elm.className = elm.className.replace(replace, ''); - } else if (action === 'toggle') { - // eslint-disable-next-line - elm.className = elm.className.match(search) - ? elm.className.replace(replace, '') - : `${elm.className} ${classarray[i]}`; - } else if (action === 'has') { - if (elm.className.match(search)) { - has = true; - break; - } - } - } - } - return has; -}; - -export const addClass = (elm, classes) => { - if (classes) { - elm.className += ` ${classes}`; - } +export const addClasses = (elm, classes) => { + elm.classList.add(...classes.split(' ')); }; export const toggleClass = (elm, classes) => { - classHelper(classes, 'toggle', elm); + elm.classList.toggle(classes); }; -export const removeClass = (elm, classes) => { - classHelper(classes, 'remove', elm); +export const removeClasses = (elm, classes) => { + elm.classList.remove(...classes.split(' ')); }; export const onLocationChange = ({ referrerURL, referrerHost }) => { diff --git a/app/javascript/sdk/IFrameHelper.js b/app/javascript/sdk/IFrameHelper.js index 47596bce3..bbc2cfafd 100644 --- a/app/javascript/sdk/IFrameHelper.js +++ b/app/javascript/sdk/IFrameHelper.js @@ -1,9 +1,8 @@ import Cookies from 'js-cookie'; import { - wootOn, - addClass, + addClasses, loadCSS, - removeClass, + removeClasses, onLocationChangeListener, } from './DOMHelpers'; import { @@ -68,7 +67,7 @@ export const IFrameHelper = { holderClassName += ` woot-widget-holder--flat`; } - addClass(widgetHolder, holderClassName); + addClasses(widgetHolder, holderClassName); widgetHolder.appendChild(iframe); body.appendChild(widgetHolder); IFrameHelper.initPostMessageCommunication(); @@ -99,7 +98,7 @@ export const IFrameHelper = { }; }, initWindowSizeListener: () => { - wootOn(window, 'resize', () => IFrameHelper.toggleCloseButton()); + window.addEventListener('resize', () => IFrameHelper.toggleCloseButton()); }, preventDefaultScroll: () => { widgetHolder.addEventListener('wheel', event => { @@ -241,9 +240,9 @@ export const IFrameHelper = { event.unreadMessageCount > 0 && !bubbleElement.classList.contains('unread-notification') ) { - addClass(bubbleElement, 'unread-notification'); + addClasses(bubbleElement, 'unread-notification'); } else if (event.unreadMessageCount === 0) { - removeClass(bubbleElement, 'unread-notification'); + removeClasses(bubbleElement, 'unread-notification'); } }, @@ -284,7 +283,7 @@ export const IFrameHelper = { target: chatBubble, }); - addClass(closeBubble, closeBtnClassName); + addClasses(closeBubble, closeBtnClassName); chatIcon.style.background = widgetColor; closeBubble.style.background = widgetColor; diff --git a/app/javascript/sdk/bubbleHelpers.js b/app/javascript/sdk/bubbleHelpers.js index b1ef9110f..5eab20f5d 100644 --- a/app/javascript/sdk/bubbleHelpers.js +++ b/app/javascript/sdk/bubbleHelpers.js @@ -1,4 +1,4 @@ -import { addClass, removeClass, toggleClass, wootOn } from './DOMHelpers'; +import { addClasses, removeClasses, toggleClass } from './DOMHelpers'; import { IFrameHelper } from './IFrameHelper'; import { isExpandedView } from './settingsHelper'; @@ -41,14 +41,14 @@ export const createBubbleIcon = ({ className, src, target }) => { export const createBubbleHolder = hideMessageBubble => { if (hideMessageBubble) { - addClass(bubbleHolder, 'woot-hidden'); + addClasses(bubbleHolder, 'woot-hidden'); } - addClass(bubbleHolder, 'woot--bubble-holder'); + addClasses(bubbleHolder, 'woot--bubble-holder'); body.appendChild(bubbleHolder); }; export const createNotificationBubble = () => { - addClass(notificationBubble, 'woot--notification'); + addClasses(notificationBubble, 'woot--notification'); return notificationBubble; }; @@ -71,15 +71,15 @@ export const onBubbleClick = (props = {}) => { }; export const onClickChatBubble = () => { - wootOn(bubbleHolder, 'click', onBubbleClick); + bubbleHolder.addEventListener('click', onBubbleClick); }; export const addUnreadClass = () => { const holderEl = document.querySelector('.woot-widget-holder'); - addClass(holderEl, 'has-unread-view'); + addClasses(holderEl, 'has-unread-view'); }; export const removeUnreadClass = () => { const holderEl = document.querySelector('.woot-widget-holder'); - removeClass(holderEl, 'has-unread-view'); + removeClasses(holderEl, 'has-unread-view'); }; diff --git a/app/javascript/shared/assets/stylesheets/colors.scss b/app/javascript/shared/assets/stylesheets/colors.scss index 7c13fc39c..64b7f3f94 100644 --- a/app/javascript/shared/assets/stylesheets/colors.scss +++ b/app/javascript/shared/assets/stylesheets/colors.scss @@ -26,16 +26,16 @@ --g-800: #009000; --g-900: #007000; - --y-50: #fefde8; - --y-100: #fdfcc4; - --y-200: #fcf68c; - --y-300: #f9e736; - --y-400: #f6d819; - --y-500: #e6c00c; - --y-600: #c69608; - --y-700: #9e6b0a; + --y-50: #FEFDE8; + --y-100: #FDFCC4; + --y-200: #FCF68C; + --y-300: #F9E736; + --y-400: #F6D819; + --y-500: #E6C00C; + --y-600: #C69608; + --y-700: #9E6b0A; --y-800: #835510; - --y-900: #6f4514; + --y-900: #6F4514; --s-25: #F8FAFC; --s-50: #F1F5F8; @@ -50,11 +50,11 @@ --s-800: #293F51; --s-900: #1B2836; - --b-50: #f7f7f7; - --b-100: #ececed; - --b-200: #dddde0; - --b-300: #c6c7ca; - --b-400: #abacaf; + --b-50: #F7F7F7; + --b-100: #ECECED; + --b-200: #DDDDE0; + --b-300: #C6C7CA; + --b-400: #ABACAF; --b-500: #96979C; --b-600: #6E6F73; --b-700: #5A5B5F; diff --git a/app/javascript/shared/components/FluentIcon/dashboard-icons.json b/app/javascript/shared/components/FluentIcon/dashboard-icons.json index 3a823a450..bf6f4127d 100644 --- a/app/javascript/shared/components/FluentIcon/dashboard-icons.json +++ b/app/javascript/shared/components/FluentIcon/dashboard-icons.json @@ -29,7 +29,9 @@ "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-open-globe-outline": "M3.5 5.75a.25.25 0 0 1 .25-.25H10c.69 0 1.25.56 1.25 1.25v8.959a6.49 6.49 0 0 1 1.5-2.646V6.75c0-.69.56-1.25 1.25-1.25h6.25a.25.25 0 0 1 .25.25v5.982A6.518 6.518 0 0 1 22 12.81V5.75A1.75 1.75 0 0 0 20.25 4H14c-.788 0-1.499.331-2 .863A2.742 2.742 0 0 0 10 4H3.75A1.75 1.75 0 0 0 2 5.75v12.5c0 .966.784 1.75 1.75 1.75H10c.495 0 .96-.13 1.36-.36a6.473 6.473 0 0 1-.343-1.663A1.248 1.248 0 0 1 10 18.5H3.75a.25.25 0 0 1-.25-.25V5.75ZM16.007 17c.04-1.415.248-2.669.553-3.585.171-.513.364-.893.554-1.134.195-.247.329-.281.386-.281.057 0 .192.034.386.281.19.241.383.62.554 1.134.305.916.513 2.17.553 3.585h-2.986Zm-.396-3.9c.108-.323.23-.622.368-.887A5.504 5.504 0 0 0 12.023 17h2.984c.04-1.5.26-2.866.604-3.9Zm3.778 0a6.133 6.133 0 0 0-.368-.887A5.504 5.504 0 0 1 22.978 17h-2.985c-.04-1.5-.26-2.866-.604-3.9Zm.604 4.9h2.985a5.504 5.504 0 0 1-3.957 4.787c.138-.265.26-.564.368-.886.345-1.035.564-2.4.604-3.901Zm-2.107 4.719c-.194.247-.329.281-.386.281-.057 0-.191-.034-.386-.281-.19-.241-.383-.62-.554-1.135-.305-.915-.513-2.17-.553-3.584h2.986c-.04 1.415-.248 2.669-.553 3.584-.171.514-.364.894-.554 1.135ZM12.023 18a5.504 5.504 0 0 0 3.956 4.787 6.133 6.133 0 0 1-.367-.886c-.346-1.035-.565-2.4-.605-3.901h-2.984Z", + "bot-outline": "M17.753 14a2.25 2.25 0 0 1 2.25 2.25v.905a3.75 3.75 0 0 1-1.307 2.846C17.13 21.345 14.89 22 12 22c-2.89 0-5.128-.656-6.691-2a3.75 3.75 0 0 1-1.306-2.843v-.908A2.25 2.25 0 0 1 6.253 14h11.5Zm0 1.5h-11.5a.75.75 0 0 0-.75.75v.908c0 .655.286 1.278.784 1.706C7.545 19.945 9.44 20.502 12 20.502c2.56 0 4.458-.557 5.719-1.64a2.25 2.25 0 0 0 .784-1.706v-.906a.75.75 0 0 0-.75-.75ZM11.898 2.008 12 2a.75.75 0 0 1 .743.648l.007.102V3.5h3.5a2.25 2.25 0 0 1 2.25 2.25v4.505a2.25 2.25 0 0 1-2.25 2.25h-8.5a2.25 2.25 0 0 1-2.25-2.25V5.75A2.25 2.25 0 0 1 7.75 3.5h3.5v-.749a.75.75 0 0 1 .648-.743L12 2l-.102.007ZM16.25 5h-8.5a.75.75 0 0 0-.75.75v4.505c0 .414.336.75.75.75h8.5a.75.75 0 0 0 .75-.75V5.75a.75.75 0 0 0-.75-.75Zm-6.5 1.5a1.25 1.25 0 1 1 0 2.5 1.25 1.25 0 0 1 0-2.5Zm4.492 0a1.25 1.25 0 1 1 0 2.499 1.25 1.25 0 0 1 0-2.499Z", "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", + "briefcase-outline": "M8.75 3h6.5a.75.75 0 0 1 .743.648L16 3.75V7h1.75A3.25 3.25 0 0 1 21 10.25v6.5A3.25 3.25 0 0 1 17.75 20H6.25A3.25 3.25 0 0 1 3 16.75v-6.5A3.25 3.25 0 0 1 6.25 7H8V3.75a.75.75 0 0 1 .648-.743L8.75 3h6.5-6.5Zm9 5.5H6.25a1.75 1.75 0 0 0-1.75 1.75v6.5c0 .966.784 1.75 1.75 1.75h11.5a1.75 1.75 0 0 0 1.75-1.75v-6.5a1.75 1.75 0 0 0-1.75-1.75Zm-3.25-4h-5V7h5V4.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" @@ -79,6 +81,7 @@ "filter-outline": "M13.5 16a.75.75 0 0 1 0 1.5h-3a.75.75 0 0 1 0-1.5h3Zm3-5a.75.75 0 0 1 0 1.5h-9a.75.75 0 0 1 0-1.5h9Zm3-5a.75.75 0 0 1 0 1.5h-15a.75.75 0 0 1 0-1.5h15Z", "file-upload-outline": "M6 2a2 2 0 0 0-2 2v5.207a5.48 5.48 0 0 1 1-.185V4a1 1 0 0 1 1-1h4v3.5A1.5 1.5 0 0 0 11.5 8H15v8a1 1 0 0 1-1 1h-3.6a5.507 5.507 0 0 1-.657 1H14a2 2 0 0 0 2-2V7.414a1.5 1.5 0 0 0-.44-1.06l-3.914-3.915A1.5 1.5 0 0 0 10.586 2H6Zm8.793 5H11.5a.5.5 0 0 1-.5-.5V3.207L14.793 7ZM5.5 19a4.5 4.5 0 1 0 0-9a4.5 4.5 0 0 0 0 9Zm2.354-4.854a.5.5 0 1 1-.708.708L6 13.707V16.5a.5.5 0 0 1-1 0v-2.793l-1.146 1.147a.5.5 0 1 1-.708-.707l2-2A.5.5 0 0 1 5.497 12h.006a.498.498 0 0 1 .348.144l.003.003l2 2Z", "flash-on-outline": "m8.294 14-1.767 7.068c-.187.746.736 1.256 1.269.701L19.79 9.27A.75.75 0 0 0 19.25 8h-4.46l1.672-5.013A.75.75 0 0 0 15.75 2h-7a.75.75 0 0 0-.721.544l-3 10.5A.75.75 0 0 0 5.75 14h2.544Zm4.745-5.487a.75.75 0 0 0 .711.987h3.74l-8.824 9.196 1.316-5.264a.75.75 0 0 0-.727-.932h-2.51l2.57-9h5.394l-1.67 5.013Z", + "flash-settings-outline": "M6.19 2.77c.13-.455.547-.77 1.02-.77h5.25c.724 0 1.236.71 1.007 1.398l-.002.008L12.204 7h2.564c.946 0 1.407 1.144.766 1.811l-.003.004l-.237.242a5.545 5.545 0 0 0-1.374-.027l.894-.912a.056.056 0 0 0 .017-.032a.084.084 0 0 0-.007-.044a.079.079 0 0 0-.025-.034c-.005-.004-.013-.008-.031-.008h-3.27a.5.5 0 0 1-.471-.666L12.52 3.08a.062.062 0 0 0-.06-.08H7.211a.062.062 0 0 0-.06.045l-2.25 7.874c-.01.04.019.08.06.08H6.87a.5.5 0 0 1 .485.62l-1.325 5.3a.086.086 0 0 0-.003.03a.02.02 0 0 0 .003.011a.08.08 0 0 0 .072.04a.03.03 0 0 0 .01-.004a.087.087 0 0 0 .024-.018l.003-.004l2.882-2.94a5.573 5.573 0 0 0 .054 1.372l-2.22 2.267c-.754.782-2.059.06-1.795-.996l1.17-4.679H4.96a1.062 1.062 0 0 1-1.021-1.354l2.25-7.873Zm5.877 8.673a2 2 0 0 1-1.431 2.478l-.461.118a4.702 4.702 0 0 0 .01 1.016l.35.083a2 2 0 0 1 1.456 2.519l-.127.422c.257.204.537.378.835.518l.325-.344a2 2 0 0 1 2.91.002l.337.358c.292-.135.568-.302.822-.498l-.157-.556a2 2 0 0 1 1.431-2.479l.46-.117a4.702 4.702 0 0 0-.01-1.017l-.348-.082a2 2 0 0 1-1.456-2.52l.126-.421a4.32 4.32 0 0 0-.835-.519l-.325.344a2 2 0 0 1-2.91-.001l-.337-.358a4.316 4.316 0 0 0-.821.497l.156.557ZM14.5 15.5a1 1 0 1 1 0-2a1 1 0 0 1 0 2Z", "folder-outline": "M8.207 4c.46 0 .908.141 1.284.402l.156.12L12.022 6.5h7.728a2.25 2.25 0 0 1 2.229 1.938l.016.158.005.154v9a2.25 2.25 0 0 1-2.096 2.245L19.75 20H4.25a2.25 2.25 0 0 1-2.245-2.096L2 17.75V6.25a2.25 2.25 0 0 1 2.096-2.245L4.25 4h3.957Zm1.44 5.979a2.25 2.25 0 0 1-1.244.512l-.196.009-4.707-.001v7.251c0 .38.282.694.648.743l.102.007h15.5a.75.75 0 0 0 .743-.648l.007-.102v-9a.75.75 0 0 0-.648-.743L19.75 8h-7.729L9.647 9.979ZM8.207 5.5H4.25a.75.75 0 0 0-.743.648L3.5 6.25v2.749L8.207 9a.75.75 0 0 0 .395-.113l.085-.06 1.891-1.578-1.89-1.575a.75.75 0 0 0-.377-.167L8.207 5.5Z", "globe-outline": "M12 1.999c5.524 0 10.002 4.478 10.002 10.002 0 5.523-4.478 10.001-10.002 10.001-5.524 0-10.002-4.478-10.002-10.001C1.998 6.477 6.476 1.999 12 1.999ZM14.939 16.5H9.06c.652 2.414 1.786 4.002 2.939 4.002s2.287-1.588 2.939-4.002Zm-7.43 0H4.785a8.532 8.532 0 0 0 4.094 3.411c-.522-.82-.953-1.846-1.27-3.015l-.102-.395Zm11.705 0h-2.722c-.324 1.335-.792 2.5-1.373 3.411a8.528 8.528 0 0 0 3.91-3.127l.185-.283ZM7.094 10H3.735l-.005.017a8.525 8.525 0 0 0-.233 1.984c0 1.056.193 2.067.545 3h3.173a20.847 20.847 0 0 1-.123-5Zm8.303 0H8.603a18.966 18.966 0 0 0 .135 5h6.524a18.974 18.974 0 0 0 .135-5Zm4.868 0h-3.358c.062.647.095 1.317.095 2a20.3 20.3 0 0 1-.218 3h3.173a8.482 8.482 0 0 0 .544-3c0-.689-.082-1.36-.236-2ZM8.88 4.09l-.023.008A8.531 8.531 0 0 0 4.25 8.5h3.048c.314-1.752.86-3.278 1.583-4.41ZM12 3.499l-.116.005C10.62 3.62 9.396 5.622 8.83 8.5h6.342c-.566-2.87-1.783-4.869-3.045-4.995L12 3.5Zm3.12.59.107.175c.669 1.112 1.177 2.572 1.475 4.237h3.048a8.533 8.533 0 0 0-4.339-4.29l-.291-.121Z", "globe-desktop-outline": "M22.002 12C22.002 6.477 17.524 2 12 2 6.476 1.999 2 6.477 2 12.001c0 5.186 3.947 9.45 9.001 9.952V20.11c-.778-.612-1.478-1.905-1.939-3.61h1.94V15H8.737a18.969 18.969 0 0 1-.135-5h6.794c.068.64.105 1.31.105 2h1.5c0-.684-.033-1.353-.095-2h3.358c.154.64.237 1.31.237 2h1.5ZM4.786 16.5h2.722l.102.396c.317 1.17.748 2.195 1.27 3.015a8.532 8.532 0 0 1-4.094-3.41ZM3.736 10h3.358a20.847 20.847 0 0 0-.095 2c0 1.043.075 2.051.217 3H4.043a8.483 8.483 0 0 1-.544-3c0-.682.08-1.347.232-1.983L3.736 10Zm5.122-5.902.023-.008C8.16 5.222 7.611 6.748 7.298 8.5H4.25c.905-2 2.56-3.587 4.608-4.402Zm3.026-.594L12 3.5l.126.006c1.262.126 2.48 2.125 3.045 4.995H8.83c.568-2.878 1.79-4.88 3.055-4.996Zm3.343.76-.107-.174.291.121a8.533 8.533 0 0 1 4.339 4.29h-3.048c-.298-1.665-.806-3.125-1.475-4.237Z M12 19a1 1 0 0 0 1 1h3v2h-.5a.5.5 0 1 0 0 1h4a.5.5 0 0 0 0-1H19v-2h3a1 1 0 0 0 1-1v-5a1 1 0 0 0-1-1h-9a1 1 0 0 0-1 1v5Z", @@ -157,5 +160,6 @@ ], "pane-close-outline": "M9.193 9.249a.75.75 0 0 1 1.059-.056l2.5 2.25a.75.75 0 0 1 0 1.114l-2.5 2.25a.75.75 0 0 1-1.004-1.115l1.048-.942H6.75a.75.75 0 1 1 0-1.5h3.546l-1.048-.942a.75.75 0 0 1-.055-1.06ZM22 17.25A2.75 2.75 0 0 1 19.25 20H4.75A2.75 2.75 0 0 1 2 17.25V6.75A2.75 2.75 0 0 1 4.75 4h14.5A2.75 2.75 0 0 1 22 6.75v10.5Zm-2.75 1.25c.69 0 1.25-.56 1.25-1.25V6.749c0-.69-.56-1.25-1.25-1.25h-3.254V18.5h3.254Zm-4.754 0V5.5H4.75c-.69 0-1.25.56-1.25 1.25v10.5c0 .69.56 1.25 1.25 1.25h9.746Z", "chevron-left-solid": "M15.707 4.293a1 1 0 0 1 0 1.414L9.414 12l6.293 6.293a1 1 0 0 1-1.414 1.414l-7-7a1 1 0 0 1 0-1.414l7-7a1 1 0 0 1 1.414 0Z", - "chevron-right-solid": "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" + "chevron-right-solid": "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", + "comment-add-outline": "M12.022 3a6.473 6.473 0 0 0-.709 1.5H5.25A1.75 1.75 0 0 0 3.5 6.25v8.5c0 .966.784 1.75 1.75 1.75h2.249v3.75l5.015-3.75h6.236a1.75 1.75 0 0 0 1.75-1.75l.001-2.483a6.517 6.517 0 0 0 1.5-1.077L22 14.75A3.25 3.25 0 0 1 18.75 18h-5.738L8 21.75a1.25 1.25 0 0 1-1.999-1V18h-.75A3.25 3.25 0 0 1 2 14.75v-8.5A3.25 3.25 0 0 1 5.25 3h6.772ZM17.5 1a5.5 5.5 0 1 1 0 11 5.5 5.5 0 0 1 0-11Zm0 1.5-.09.008a.5.5 0 0 0-.402.402L17 3l-.001 3H14l-.09.008a.5.5 0 0 0-.402.402l-.008.09.008.09a.5.5 0 0 0 .402.402L14 7h2.999L17 10l.008.09a.5.5 0 0 0 .402.402l.09.008.09-.008a.5.5 0 0 0 .402-.402L18 10l-.001-3H21l.09-.008a.5.5 0 0 0 .402-.402l.008-.09-.008-.09a.5.5 0 0 0-.402-.402L21 6h-3.001L18 3l-.008-.09a.5.5 0 0 0-.402-.402L17.5 2.5Z" } diff --git a/app/javascript/shared/components/emoji/EmojiInput.vue b/app/javascript/shared/components/emoji/EmojiInput.vue index 387605913..84891b0e2 100644 --- a/app/javascript/shared/components/emoji/EmojiInput.vue +++ b/app/javascript/shared/components/emoji/EmojiInput.vue @@ -5,21 +5,21 @@
  • +
    + {{ selectedKey }} +
    -
    - {{ selectedKey }} -

    { - if ( - (isUserEmailAvailable && field.name === 'emailAddress') || - (isUserPhoneNumberAvailable && field.name === 'phoneNumber') - ) { + if (isUserEmailAvailable && field.name === 'emailAddress') { + return false; + } + if (isUserPhoneNumberAvailable && field.name === 'phoneNumber') { + return false; + } + if (isUserNameAvailable && field.name === 'fullName') { return false; } return true; @@ -236,6 +245,7 @@ export default { text: null, select: null, number: null, + checkbox: false, }; const validationKeys = Object.keys(validations); const validation = 'bail|required'; diff --git a/app/javascript/widget/components/TeamAvailability.vue b/app/javascript/widget/components/TeamAvailability.vue index 1f4c5d046..a3a6b0c02 100644 --- a/app/javascript/widget/components/TeamAvailability.vue +++ b/app/javascript/widget/components/TeamAvailability.vue @@ -13,7 +13,7 @@ }}

    - {{ replyWaitMeessage }} + {{ replyWaitMessage }}
    @@ -75,7 +75,7 @@ export default { } return anyAgentOnline; }, - replyWaitMeessage() { + replyWaitMessage() { const { workingHoursEnabled } = this.channelConfig; if (this.isOnline) { diff --git a/app/javascript/widget/components/UnreadMessageList.vue b/app/javascript/widget/components/UnreadMessageList.vue index 19afe49d1..6683c585e 100644 --- a/app/javascript/widget/components/UnreadMessageList.vue +++ b/app/javascript/widget/components/UnreadMessageList.vue @@ -107,13 +107,12 @@ export default { .clear-button { background: transparent; color: $color-woot; - padding: 0; border: 0; font-weight: $font-weight-bold; font-size: $font-size-medium; transition: all 0.3s var(--ease-in-cubic); margin-left: $space-smaller; - padding-right: $space-one; + padding: 0 $space-one 0 0; &:hover { transform: translateX($space-smaller); diff --git a/app/javascript/widget/helpers/IframeEventHelper.js b/app/javascript/widget/helpers/IframeEventHelper.js index 953802df8..40c0e1f1c 100644 --- a/app/javascript/widget/helpers/IframeEventHelper.js +++ b/app/javascript/widget/helpers/IframeEventHelper.js @@ -10,7 +10,7 @@ export const loadedEventConfig = () => { export const getExtraSpaceToScroll = () => { // This function calculates the extra space needed for the view to - // accomodate the height of close button + height of + // accommodate the height of close button + height of // read messages button. So that scrollbar won't appear const unreadMessageWrap = document.querySelector('.unread-messages'); const unreadCloseWrap = document.querySelector('.close-unread-wrap'); diff --git a/app/javascript/widget/helpers/utils.js b/app/javascript/widget/helpers/utils.js index 1a4b2c1d6..a4d5dafa3 100755 --- a/app/javascript/widget/helpers/utils.js +++ b/app/javascript/widget/helpers/utils.js @@ -3,13 +3,6 @@ import { WOOT_PREFIX } from './constants'; export const isEmptyObject = obj => Object.keys(obj).length === 0 && obj.constructor === Object; -export const arrayToHashById = array => - array.reduce((map, obj) => { - const newMap = map; - newMap[obj.id] = obj; - return newMap; - }, {}); - export const sendMessage = msg => { window.parent.postMessage( `chatwoot-widget:${JSON.stringify({ ...msg })}`, @@ -22,9 +15,7 @@ export const IFrameHelper = { sendMessage, isAValidEvent: e => { const isDataAString = typeof e.data === 'string'; - const isAValidWootEvent = - isDataAString && e.data.indexOf(WOOT_PREFIX) === 0; - return isAValidWootEvent; + return isDataAString && e.data.indexOf(WOOT_PREFIX) === 0; }, getMessage: e => JSON.parse(e.data.replace(WOOT_PREFIX, '')), }; diff --git a/app/javascript/widget/i18n/locale/da.json b/app/javascript/widget/i18n/locale/da.json index 03e6c1cb3..0fe256800 100644 --- a/app/javascript/widget/i18n/locale/da.json +++ b/app/javascript/widget/i18n/locale/da.json @@ -8,8 +8,8 @@ "SUBMIT": "Send" }, "MESSAGE_BUBBLE": { - "RETRY": "Send message again", - "ERROR_MESSAGE": "Couldn't send, try again" + "RETRY": "Send besked igen", + "ERROR_MESSAGE": "Kunne ikke sende, prøv igen" } }, "TEAM_AVAILABILITY": { @@ -22,8 +22,8 @@ "IN_A_DAY": "Svarer typisk på en dag" }, "START_CONVERSATION": "Start Samtale", - "END_CONVERSATION": "End Conversation", - "CONTINUE_CONVERSATION": "Continue conversation", + "END_CONVERSATION": "Afslut Samtale", + "CONTINUE_CONVERSATION": "Fortsæt samtale", "START_NEW_CONVERSATION": "Start en ny samtale", "UNREAD_VIEW": { "VIEW_MESSAGES_BUTTON": "Se nye beskeder", @@ -49,14 +49,14 @@ "EMAIL_ADDRESS": { "LABEL": "E-Mail Adresse", "PLACEHOLDER": "Indtast venligst din e-mail adresse", - "REQUIRED_ERROR": "Email Address is required", + "REQUIRED_ERROR": "E-mail adresse er påkrævet", "VALID_ERROR": "Indtast venligst en gyldig e-mailadresse" }, "PHONE_NUMBER": { "LABEL": "Telefonnummer", - "PLACEHOLDER": "Please enter your phone number", - "REQUIRED_ERROR": "Phone Number is required", - "VALID_ERROR": "Phone number should be of E.164 format eg: +1415555555" + "PLACEHOLDER": "Indtast venligst dit telefonnummer", + "REQUIRED_ERROR": "Telefonnummer er påkrævet", + "VALID_ERROR": "Telefonnummer skal være i E.164 format fx: +1415555555" }, "MESSAGE": { "LABEL": "Besked", @@ -64,8 +64,8 @@ "ERROR": "Beskeden er for kort" } }, - "CAMPAIGN_HEADER": "Please provide your name and email before starting the conversation", - "IS_REQUIRED": "is required" + "CAMPAIGN_HEADER": "Angiv venligst dit navn og e-mail, før du starter samtalen", + "IS_REQUIRED": "er påkrævet" }, "FILE_SIZE_LIMIT": "Filen overskrider grænsen på {MAXIMUM_FILE_UPLOAD_SIZE} for vedhæftede filer", "CHAT_FORM": { diff --git a/app/javascript/widget/i18n/locale/fa.json b/app/javascript/widget/i18n/locale/fa.json index ec6237a4e..7e5752a94 100644 --- a/app/javascript/widget/i18n/locale/fa.json +++ b/app/javascript/widget/i18n/locale/fa.json @@ -55,12 +55,12 @@ "PHONE_NUMBER": { "LABEL": "شماره تلفن", "PLACEHOLDER": "لطفا شماره تلفن خود را وارد کنید", - "REQUIRED_ERROR": "شماره تلفن الزامی است", + "REQUIRED_ERROR": "شماره تلفن مورد نیاز است", "VALID_ERROR": "شماره تلفن باید با فرمت E.164 باشد، به عنوان مثال: +1415555555" }, "MESSAGE": { "LABEL": "پیام", - "PLACEHOLDER": "لطفا ایمیل خود را وارد کنید", + "PLACEHOLDER": "لطفا پیام خود را وارد کنید", "ERROR": "پیام بسیار کوتاه است" } }, @@ -74,12 +74,12 @@ } }, "CSAT": { - "TITLE": "به مکالمه خود امتیاز دهید", - "SUBMITTED_TITLE": "با تشکر از شما برای ثبت رتبه", - "PLACEHOLDER": "توضیحات بیشتر بدهید..." + "TITLE": "به گفتگوی خود امتیاز دهید", + "SUBMITTED_TITLE": "با تشکر از شما برای ثبت امتیاز", + "PLACEHOLDER": "بیشتر به ما بگویید..." }, "EMAIL_TRANSCRIPT": { - "BUTTON_TEXT": "درخواست رونوشت مکالمه", + "BUTTON_TEXT": "درخواست رونوشت گفتگو", "SEND_EMAIL_SUCCESS": "رونوشت گفتگو با موفقیت ارسال شد", "SEND_EMAIL_ERROR": "خطایی پیش آمد. لطفا دوباره امتحان کنید" } diff --git a/app/javascript/widget/i18n/locale/fi.json b/app/javascript/widget/i18n/locale/fi.json index 75b325881..54363198e 100644 --- a/app/javascript/widget/i18n/locale/fi.json +++ b/app/javascript/widget/i18n/locale/fi.json @@ -14,7 +14,7 @@ }, "TEAM_AVAILABILITY": { "ONLINE": "Olemme online-tilassa", - "OFFLINE": "We are away at the moment" + "OFFLINE": "Olemme tällä hetkellä poissa" }, "REPLY_TIME": { "IN_A_FEW_MINUTES": "Vastaa tyypillisesti muutamassa minuutissa", diff --git a/app/javascript/widget/i18n/locale/lt.json b/app/javascript/widget/i18n/locale/lt.json new file mode 100644 index 000000000..ef4149191 --- /dev/null +++ b/app/javascript/widget/i18n/locale/lt.json @@ -0,0 +1,86 @@ +{ + "COMPONENTS": { + "FILE_BUBBLE": { + "DOWNLOAD": "Download", + "UPLOADING": "Uploading..." + }, + "FORM_BUBBLE": { + "SUBMIT": "Submit" + }, + "MESSAGE_BUBBLE": { + "RETRY": "Send message again", + "ERROR_MESSAGE": "Couldn't send, try again" + } + }, + "TEAM_AVAILABILITY": { + "ONLINE": "We are online", + "OFFLINE": "We are away at the moment" + }, + "REPLY_TIME": { + "IN_A_FEW_MINUTES": "Typically replies in a few minutes", + "IN_A_FEW_HOURS": "Typically replies in a few hours", + "IN_A_DAY": "Typically replies in a day" + }, + "START_CONVERSATION": "Start Conversation", + "END_CONVERSATION": "End Conversation", + "CONTINUE_CONVERSATION": "Continue conversation", + "START_NEW_CONVERSATION": "Start a new conversation", + "UNREAD_VIEW": { + "VIEW_MESSAGES_BUTTON": "See new messages", + "CLOSE_MESSAGES_BUTTON": "Close", + "COMPANY_FROM": "from", + "BOT": "Bot" + }, + "BUBBLE": { + "LABEL": "Chat with us" + }, + "POWERED_BY": "Powered by Chatwoot", + "EMAIL_PLACEHOLDER": "Please enter your email", + "CHAT_PLACEHOLDER": "Type your message", + "TODAY": "Today", + "YESTERDAY": "Yesterday", + "PRE_CHAT_FORM": { + "FIELDS": { + "FULL_NAME": { + "LABEL": "Full Name", + "PLACEHOLDER": "Please enter your full name", + "REQUIRED_ERROR": "Full Name is required" + }, + "EMAIL_ADDRESS": { + "LABEL": "Email Address", + "PLACEHOLDER": "Please enter your email address", + "REQUIRED_ERROR": "Email Address is required", + "VALID_ERROR": "Please enter a valid email address" + }, + "PHONE_NUMBER": { + "LABEL": "Phone Number", + "PLACEHOLDER": "Please enter your phone number", + "REQUIRED_ERROR": "Phone Number is required", + "VALID_ERROR": "Phone number should be of E.164 format eg: +1415555555" + }, + "MESSAGE": { + "LABEL": "Message", + "PLACEHOLDER": "Please enter your message", + "ERROR": "Message too short" + } + }, + "CAMPAIGN_HEADER": "Please provide your name and email before starting the conversation", + "IS_REQUIRED": "is required" + }, + "FILE_SIZE_LIMIT": "File exceeds the {MAXIMUM_FILE_UPLOAD_SIZE} attachment limit", + "CHAT_FORM": { + "INVALID": { + "FIELD": "Invalid field" + } + }, + "CSAT": { + "TITLE": "Rate your conversation", + "SUBMITTED_TITLE": "Thank you for submitting the rating", + "PLACEHOLDER": "Tell us more..." + }, + "EMAIL_TRANSCRIPT": { + "BUTTON_TEXT": "Request a conversation transcript", + "SEND_EMAIL_SUCCESS": "The chat transcript was sent successfully", + "SEND_EMAIL_ERROR": "There was an error, please try again" + } +} diff --git a/app/javascript/widget/i18n/locale/lv.json b/app/javascript/widget/i18n/locale/lv.json index ef4149191..42ad3db8e 100644 --- a/app/javascript/widget/i18n/locale/lv.json +++ b/app/javascript/widget/i18n/locale/lv.json @@ -1,86 +1,86 @@ { "COMPONENTS": { "FILE_BUBBLE": { - "DOWNLOAD": "Download", - "UPLOADING": "Uploading..." + "DOWNLOAD": "Lejupielādēt", + "UPLOADING": "Notiek Augšupielāde..." }, "FORM_BUBBLE": { - "SUBMIT": "Submit" + "SUBMIT": "Iesniegt" }, "MESSAGE_BUBBLE": { - "RETRY": "Send message again", - "ERROR_MESSAGE": "Couldn't send, try again" + "RETRY": "Sūtīt ziņojumu vēlreiz", + "ERROR_MESSAGE": "Nevarēja nosūtīt. Lūdzu, mēģiniet vēlreiz" } }, "TEAM_AVAILABILITY": { - "ONLINE": "We are online", - "OFFLINE": "We are away at the moment" + "ONLINE": "Mēs esam tiešsaistē", + "OFFLINE": "Šobrīd mēs neesam uz vietas" }, "REPLY_TIME": { - "IN_A_FEW_MINUTES": "Typically replies in a few minutes", - "IN_A_FEW_HOURS": "Typically replies in a few hours", - "IN_A_DAY": "Typically replies in a day" + "IN_A_FEW_MINUTES": "Parasti atbild pēc dažām minūtēm", + "IN_A_FEW_HOURS": "Parasti atbild pēc dažām stundām", + "IN_A_DAY": "Parasti atbild vienas dienas laikā" }, - "START_CONVERSATION": "Start Conversation", - "END_CONVERSATION": "End Conversation", - "CONTINUE_CONVERSATION": "Continue conversation", - "START_NEW_CONVERSATION": "Start a new conversation", + "START_CONVERSATION": "Sākt Sarunu", + "END_CONVERSATION": "Beigt Sarunu", + "CONTINUE_CONVERSATION": "Turpināt Sarunu", + "START_NEW_CONVERSATION": "Sākt jaunu sarunu", "UNREAD_VIEW": { - "VIEW_MESSAGES_BUTTON": "See new messages", - "CLOSE_MESSAGES_BUTTON": "Close", - "COMPANY_FROM": "from", + "VIEW_MESSAGES_BUTTON": "Skatīt jaunus ziņojumus", + "CLOSE_MESSAGES_BUTTON": "Aizvērt", + "COMPANY_FROM": "no", "BOT": "Bot" }, "BUBBLE": { - "LABEL": "Chat with us" + "LABEL": "Tērzēt ar mums" }, - "POWERED_BY": "Powered by Chatwoot", - "EMAIL_PLACEHOLDER": "Please enter your email", - "CHAT_PLACEHOLDER": "Type your message", - "TODAY": "Today", - "YESTERDAY": "Yesterday", + "POWERED_BY": "Darbināts ar Chatwoot", + "EMAIL_PLACEHOLDER": "Lūdzu, ievadiet savu e-pastu", + "CHAT_PLACEHOLDER": "Rakstiet savu ziņojumu", + "TODAY": "Šodien", + "YESTERDAY": "Vakar", "PRE_CHAT_FORM": { "FIELDS": { "FULL_NAME": { - "LABEL": "Full Name", - "PLACEHOLDER": "Please enter your full name", - "REQUIRED_ERROR": "Full Name is required" + "LABEL": "Pilns Vārds", + "PLACEHOLDER": "Lūdzu, ievadiet savu pilno vārdu", + "REQUIRED_ERROR": "Nepieciešams pilns vārds" }, "EMAIL_ADDRESS": { - "LABEL": "Email Address", - "PLACEHOLDER": "Please enter your email address", - "REQUIRED_ERROR": "Email Address is required", - "VALID_ERROR": "Please enter a valid email address" + "LABEL": "e-pasta Adrese", + "PLACEHOLDER": "Lūdzu ievadiet savu e-pasta adresi", + "REQUIRED_ERROR": "Nepieciešama e -pasta adrese", + "VALID_ERROR": "Lūdzu, ievadiet derīgu e-pasta adresi" }, "PHONE_NUMBER": { - "LABEL": "Phone Number", - "PLACEHOLDER": "Please enter your phone number", - "REQUIRED_ERROR": "Phone Number is required", - "VALID_ERROR": "Phone number should be of E.164 format eg: +1415555555" + "LABEL": "Telefona numurs", + "PLACEHOLDER": "Lūdzu, ievadiet savu tālruņa numuru", + "REQUIRED_ERROR": "Nepieciešams tālruņa numurs", + "VALID_ERROR": "Tālruņa numuram ir jābūt E.164 formātā, piemēram: +37155555555" }, "MESSAGE": { - "LABEL": "Message", - "PLACEHOLDER": "Please enter your message", - "ERROR": "Message too short" + "LABEL": "Ziņojums", + "PLACEHOLDER": "Lūdzu, ievadiet savu ziņojumu", + "ERROR": "Ziņojums ir pārāk īss" } }, - "CAMPAIGN_HEADER": "Please provide your name and email before starting the conversation", - "IS_REQUIRED": "is required" + "CAMPAIGN_HEADER": "Lūdzu, pirms sarunas sākšanas, norādiet savu vārdu un e -pastu", + "IS_REQUIRED": "ir nepieciešams" }, - "FILE_SIZE_LIMIT": "File exceeds the {MAXIMUM_FILE_UPLOAD_SIZE} attachment limit", + "FILE_SIZE_LIMIT": "Fails pārsniedz {MAXIMUM_FILE_UPLOAD_SIZE} pielikuma lieluma ierobežojumu", "CHAT_FORM": { "INVALID": { - "FIELD": "Invalid field" + "FIELD": "Nederīgs lauks" } }, "CSAT": { - "TITLE": "Rate your conversation", - "SUBMITTED_TITLE": "Thank you for submitting the rating", - "PLACEHOLDER": "Tell us more..." + "TITLE": "Novērtējiet Jūsu sarunu", + "SUBMITTED_TITLE": "Paldies, ka iesniedzāt novērtējumu", + "PLACEHOLDER": "Pastāsti mums vairāk..." }, "EMAIL_TRANSCRIPT": { - "BUTTON_TEXT": "Request a conversation transcript", - "SEND_EMAIL_SUCCESS": "The chat transcript was sent successfully", - "SEND_EMAIL_ERROR": "There was an error, please try again" + "BUTTON_TEXT": "Pieprasīt sarunas transkriptu", + "SEND_EMAIL_SUCCESS": "Sarunas transkripts tika veiksmīgi nosūtīts", + "SEND_EMAIL_ERROR": "Radās kļūda. Lūdzu, mēģiniet vēlreiz" } } diff --git a/app/javascript/widget/i18n/locale/ms.json b/app/javascript/widget/i18n/locale/ms.json index ef4149191..ba64b34e4 100644 --- a/app/javascript/widget/i18n/locale/ms.json +++ b/app/javascript/widget/i18n/locale/ms.json @@ -47,7 +47,7 @@ "REQUIRED_ERROR": "Full Name is required" }, "EMAIL_ADDRESS": { - "LABEL": "Email Address", + "LABEL": "Emel", "PLACEHOLDER": "Please enter your email address", "REQUIRED_ERROR": "Email Address is required", "VALID_ERROR": "Please enter a valid email address" diff --git a/app/javascript/widget/i18n/locale/pt_BR.json b/app/javascript/widget/i18n/locale/pt_BR.json index 6e9318fd2..055ee191d 100644 --- a/app/javascript/widget/i18n/locale/pt_BR.json +++ b/app/javascript/widget/i18n/locale/pt_BR.json @@ -22,7 +22,7 @@ "IN_A_DAY": "Responde normalmente em um dia" }, "START_CONVERSATION": "Iniciar Conversa", - "END_CONVERSATION": "Fim da Conversa", + "END_CONVERSATION": "Finalizar Conversa", "CONTINUE_CONVERSATION": "Continuar conversa", "START_NEW_CONVERSATION": "Iniciar uma nova conversa", "UNREAD_VIEW": { diff --git a/app/javascript/widget/i18n/locale/tr.json b/app/javascript/widget/i18n/locale/tr.json index f1b737504..64dc3ee12 100644 --- a/app/javascript/widget/i18n/locale/tr.json +++ b/app/javascript/widget/i18n/locale/tr.json @@ -24,7 +24,7 @@ "START_CONVERSATION": "Görüşmeyi Başlatın", "END_CONVERSATION": "Görüşmeyi Sonlandır", "CONTINUE_CONVERSATION": "Görüşmeye devam et", - "START_NEW_CONVERSATION": "Yeni Görüşme Başlatın", + "START_NEW_CONVERSATION": "Yeni çatı başla", "UNREAD_VIEW": { "VIEW_MESSAGES_BUTTON": "Yeni mesajları gör", "CLOSE_MESSAGES_BUTTON": "Kapat", diff --git a/app/javascript/widget/i18n/locale/vi.json b/app/javascript/widget/i18n/locale/vi.json index d2bfbd319..818702ab9 100644 --- a/app/javascript/widget/i18n/locale/vi.json +++ b/app/javascript/widget/i18n/locale/vi.json @@ -14,7 +14,7 @@ }, "TEAM_AVAILABILITY": { "ONLINE": "Chúng tôi đang trực tuyến", - "OFFLINE": "Hiện tại chúng tôi đang ngoại tuyến" + "OFFLINE": "Hiện tại chúng tôi đang bận chút" }, "REPLY_TIME": { "IN_A_FEW_MINUTES": "Thường trả lời sau vài phút", @@ -22,9 +22,9 @@ "IN_A_DAY": "Thường trả lời trong một ngày" }, "START_CONVERSATION": "Bắt đầu một cuộc trò chuyện", - "END_CONVERSATION": "End Conversation", - "CONTINUE_CONVERSATION": "Tiếp tục cuộc trò chuyện", - "START_NEW_CONVERSATION": "Bắt đầu cuộc trò chuyện mới", + "END_CONVERSATION": "Kết thúc hội thoại", + "CONTINUE_CONVERSATION": "Tiếp tục hội thoại", + "START_NEW_CONVERSATION": "Bắt đầu hội thoại mới", "UNREAD_VIEW": { "VIEW_MESSAGES_BUTTON": "Xem tin nhắn mới", "CLOSE_MESSAGES_BUTTON": "Đóng", @@ -34,7 +34,7 @@ "BUBBLE": { "LABEL": "Trò chuyện với chúng tôi" }, - "POWERED_BY": "Cung cấp bởi Chatwoot", + "POWERED_BY": "CC bởi Chatwoot", "EMAIL_PLACEHOLDER": "Vui lòng nhập email", "CHAT_PLACEHOLDER": "Gõ tin nhắn của bạn", "TODAY": "Hôm nay", @@ -49,37 +49,37 @@ "EMAIL_ADDRESS": { "LABEL": "Email", "PLACEHOLDER": "Vui lòng nhập email", - "REQUIRED_ERROR": "Email Address is required", + "REQUIRED_ERROR": "Phải có \u001dđịa chỉ email", "VALID_ERROR": "Vui lòng nhập một địa chỉ email hợp lệ" }, "PHONE_NUMBER": { "LABEL": "Số điện thoại", - "PLACEHOLDER": "Please enter your phone number", - "REQUIRED_ERROR": "Phone Number is required", - "VALID_ERROR": "Phone number should be of E.164 format eg: +1415555555" + "PLACEHOLDER": "\u001dVui lòng nhập số điện thoại của bạn", + "REQUIRED_ERROR": "Cần phải nhập số điện thoại", + "VALID_ERROR": "Số điện thoại nên theo định dạng E.164, ví dụ: +1415555555" }, "MESSAGE": { "LABEL": "Tin nhắn", "PLACEHOLDER": "Vui lòng nhập tin nhắn", - "ERROR": "Tin nhắnn quá ngắn" + "ERROR": "Tin nhắn quá ngắn" } }, - "CAMPAIGN_HEADER": "Vui lòng cung cấp tên và địa chỉ email của bạn trước khi bắt đầu cuộc trò chuyện", - "IS_REQUIRED": "is required" + "CAMPAIGN_HEADER": "Vui lòng cung cấp tên và địa chỉ email của bạn trước khi bắt đầu hội thoại", + "IS_REQUIRED": "được yêu cầu" }, - "FILE_SIZE_LIMIT": "File vượt quá kích thước giới hạn {MAXIMUM_FILE_UPLOAD_SIZE}", + "FILE_SIZE_LIMIT": "Tệp vượt quá kích thước giới hạn {MAXIMUM_FILE_UPLOAD_SIZE}", "CHAT_FORM": { "INVALID": { "FIELD": "Trường không hợp lệ" } }, "CSAT": { - "TITLE": "Đánh giá cuộc trò chuyện", + "TITLE": "Đánh giá hội thoại", "SUBMITTED_TITLE": "Cảm ơn vì đã đánh giá", "PLACEHOLDER": "Cho chúng tôi biết thêm..." }, "EMAIL_TRANSCRIPT": { - "BUTTON_TEXT": "Yêu cầu bản ghi cuộc trò chuyện", + "BUTTON_TEXT": "Yêu cầu bản ghi hội thoại", "SEND_EMAIL_SUCCESS": "Bản ghi cuộc trò chuyện đã được gửi thành công", "SEND_EMAIL_ERROR": "Đã có lỗi, vui lòng thử lại" } diff --git a/app/javascript/widget/store/modules/conversation/getters.js b/app/javascript/widget/store/modules/conversation/getters.js index 9d1c067f4..74e582348 100644 --- a/app/javascript/widget/store/modules/conversation/getters.js +++ b/app/javascript/widget/store/modules/conversation/getters.js @@ -32,7 +32,7 @@ export const getters = { }, getUnreadMessageCount: _state => { const { userLastSeenAt } = _state.meta; - const count = Object.values(_state.conversations).filter(chat => { + return Object.values(_state.conversations).filter(chat => { const { created_at: createdAt, message_type: messageType } = chat; const isOutGoing = messageType === MESSAGE_TYPE.OUTGOING; const hasNotSeen = userLastSeenAt @@ -40,7 +40,6 @@ export const getters = { : true; return hasNotSeen && isOutGoing; }).length; - return count; }, getUnreadTextMessages: (_state, _getters) => { const unreadCount = _getters.getUnreadMessageCount; @@ -50,7 +49,6 @@ export const getters = { return messageType === MESSAGE_TYPE.OUTGOING; }); const maxUnreadCount = Math.min(unreadCount, 3); - const allUnreadMessages = unreadAgentMessages.splice(-maxUnreadCount); - return allUnreadMessages; + return unreadAgentMessages.splice(-maxUnreadCount); }, }; diff --git a/app/javascript/widget/store/modules/conversation/helpers.js b/app/javascript/widget/store/modules/conversation/helpers.js index 44e2a6729..2ebac5242 100644 --- a/app/javascript/widget/store/modules/conversation/helpers.js +++ b/app/javascript/widget/store/modules/conversation/helpers.js @@ -29,7 +29,7 @@ const shouldShowAvatar = (message, nextMessage) => { export const groupConversationBySender = conversationsForADate => conversationsForADate.map((message, index) => { - let showAvatar = false; + let showAvatar; const isLastMessage = index === conversationsForADate.length - 1; if (isASubmittedFormMessage(message)) { showAvatar = false; diff --git a/app/javascript/widget/store/modules/conversation/mutations.js b/app/javascript/widget/store/modules/conversation/mutations.js index f47971f07..ca6dafada 100644 --- a/app/javascript/widget/store/modules/conversation/mutations.js +++ b/app/javascript/widget/store/modules/conversation/mutations.js @@ -88,8 +88,7 @@ export const mutations = { }, toggleAgentTypingStatus($state, { status }) { - const isTyping = status === 'on'; - $state.uiFlags.isAgentTyping = isTyping; + $state.uiFlags.isAgentTyping = status === 'on'; }, setMetaUserLastSeenAt($state, lastSeen) { diff --git a/app/javascript/widget/store/modules/conversationLabels.js b/app/javascript/widget/store/modules/conversationLabels.js index 3fbcd230d..3ae600082 100644 --- a/app/javascript/widget/store/modules/conversationLabels.js +++ b/app/javascript/widget/store/modules/conversationLabels.js @@ -9,14 +9,14 @@ export const actions = { try { await conversationLabels.create(label); } catch (error) { - // Ingore error + // Ignore error } }, destroy: async (_, label) => { try { await conversationLabels.destroy(label); } catch (error) { - // Ingore error + // Ignore error } }, }; diff --git a/app/jobs/bulk_actions_job.rb b/app/jobs/bulk_actions_job.rb index 75b834308..bc68b2b36 100644 --- a/app/jobs/bulk_actions_job.rb +++ b/app/jobs/bulk_actions_job.rb @@ -36,7 +36,7 @@ class BulkActionsJob < ApplicationJob def available_params(params) return unless params[:fields] - params[:fields].delete_if { |_k, v| v.nil? } + params[:fields].delete_if { |key, value| value.nil? && key == 'status' } end def bulk_add_labels(conversation) diff --git a/app/listeners/webhook_listener.rb b/app/listeners/webhook_listener.rb index 11a86e639..81d2c6b0a 100644 --- a/app/listeners/webhook_listener.rb +++ b/app/listeners/webhook_listener.rb @@ -54,7 +54,7 @@ class WebhookListener < BaseListener private def deliver_account_webhooks(payload, inbox) - inbox.account.webhooks.account.each do |webhook| + inbox.account.webhooks.account_type.each do |webhook| next unless webhook.subscriptions.include?(payload[:event]) WebhookJob.perform_later(webhook.url, payload) diff --git a/app/mailboxes/mailbox_helper.rb b/app/mailboxes/mailbox_helper.rb index 1519343ca..216d5c2c3 100644 --- a/app/mailboxes/mailbox_helper.rb +++ b/app/mailboxes/mailbox_helper.rb @@ -34,7 +34,7 @@ module MailboxHelper end def create_contact - @contact_inbox = ::ContactBuilder.new( + @contact_inbox = ::ContactInboxWithContactBuilder.new( source_id: processed_mail.original_sender, inbox: @inbox, contact_attributes: { diff --git a/app/mailboxes/support_mailbox.rb b/app/mailboxes/support_mailbox.rb index b0a1a68df..8accda902 100644 --- a/app/mailboxes/support_mailbox.rb +++ b/app/mailboxes/support_mailbox.rb @@ -72,7 +72,7 @@ class SupportMailbox < ApplicationMailbox end def find_or_create_contact - @contact = @inbox.contacts.find_by(email: @processed_mail.original_sender) + @contact = @inbox.contacts.find_by(email: @processed_mail.original_sender&.downcase) if @contact.present? @contact_inbox = ContactInbox.find_by(inbox: @inbox, contact: @contact) else diff --git a/app/models/article.rb b/app/models/article.rb index 1044e1107..ac6506d01 100644 --- a/app/models/article.rb +++ b/app/models/article.rb @@ -6,6 +6,7 @@ # content :text # description :text # meta :jsonb +# slug :string not null # status :integer # title :string # views :integer @@ -22,6 +23,7 @@ # # index_articles_on_associated_article_id (associated_article_id) # index_articles_on_author_id (author_id) +# index_articles_on_slug (slug) UNIQUE # class Article < ApplicationRecord include PgSearch::Model @@ -43,6 +45,8 @@ class Article < ApplicationRecord belongs_to :author, class_name: 'User' before_validation :ensure_account_id + before_validation :ensure_article_slug + validates :account_id, presence: true validates :category_id, presence: true validates :author_id, presence: true @@ -110,4 +114,8 @@ class Article < ApplicationRecord def ensure_account_id self.account_id = portal&.account_id end + + def ensure_article_slug + self.slug ||= "#{Time.now.utc.to_i}-#{title.underscore.parameterize(separator: '-')}" if title.present? + end end diff --git a/app/models/attachment.rb b/app/models/attachment.rb index 4fa1b7e57..cb877ce8a 100644 --- a/app/models/attachment.rb +++ b/app/models/attachment.rb @@ -14,6 +14,11 @@ # account_id :integer not null # message_id :integer not null # +# Indexes +# +# index_attachments_on_account_id (account_id) +# index_attachments_on_message_id (message_id) +# class Attachment < ApplicationRecord include Rails.application.routes.url_helpers diff --git a/app/models/channel/facebook_page.rb b/app/models/channel/facebook_page.rb index 2153708e2..bba45f326 100644 --- a/app/models/channel/facebook_page.rb +++ b/app/models/channel/facebook_page.rb @@ -37,16 +37,11 @@ class Channel::FacebookPage < ApplicationRecord end def create_contact_inbox(instagram_id, name) - ActiveRecord::Base.transaction do - contact = inbox.account.contacts.create!(name: name) - ::ContactInbox.create!( - contact_id: contact.id, - inbox_id: inbox.id, - source_id: instagram_id - ) - rescue StandardError => e - Rails.logger.error e - end + @contact_inbox = ::ContactInboxWithContactBuilder.new({ + source_id: instagram_id, + inbox: inbox, + contact_attributes: { name: name } + }).perform end def subscribe diff --git a/app/models/channel/twilio_sms.rb b/app/models/channel/twilio_sms.rb index f575b75a0..d6c9177fb 100644 --- a/app/models/channel/twilio_sms.rb +++ b/app/models/channel/twilio_sms.rb @@ -28,8 +28,8 @@ class Channel::TwilioSms < ApplicationRecord validates :auth_token, presence: true # Must have _one_ of messaging_service_sid _or_ phone_number, and messaging_service_sid is preferred - validates :messaging_service_sid, uniqueness: true, presence: true, unless: :phone_number.presence - validates :phone_number, absence: true, if: :messaging_service_sid.presence + validates :messaging_service_sid, uniqueness: true, presence: true, unless: :phone_number? + validates :phone_number, absence: true, if: :messaging_service_sid? validates :phone_number, uniqueness: true, allow_nil: true enum medium: { sms: 0, whatsapp: 1 } diff --git a/app/models/channel/twitter_profile.rb b/app/models/channel/twitter_profile.rb index 4f6fa7ba1..d0f765e9f 100644 --- a/app/models/channel/twitter_profile.rb +++ b/app/models/channel/twitter_profile.rb @@ -32,16 +32,11 @@ class Channel::TwitterProfile < ApplicationRecord end def create_contact_inbox(profile_id, name, additional_attributes) - ActiveRecord::Base.transaction do - contact = inbox.account.contacts.create!(additional_attributes: additional_attributes, name: name) - ::ContactInbox.create!( - contact_id: contact.id, - inbox_id: inbox.id, - source_id: profile_id - ) - rescue StandardError => e - Rails.logger.error e - end + ::ContactInboxWithContactBuilder.new({ + source_id: profile_id, + inbox: inbox, + contact_attributes: { name: name, additional_attributes: additional_attributes } + }).perform end def twitter_client diff --git a/app/models/channel/web_widget.rb b/app/models/channel/web_widget.rb index b85443633..59d392892 100644 --- a/app/models/channel/web_widget.rb +++ b/app/models/channel/web_widget.rb @@ -98,19 +98,9 @@ class Channel::WebWidget < ApplicationRecord end def create_contact_inbox(additional_attributes = {}) - ActiveRecord::Base.transaction do - contact = inbox.account.contacts.create!( - name: ::Haikunator.haikunate(1000), - additional_attributes: additional_attributes - ) - contact_inbox = ::ContactInbox.create!( - contact_id: contact.id, - inbox_id: inbox.id, - source_id: SecureRandom.uuid - ) - contact_inbox - rescue StandardError => e - Rails.logger.error e - end + ::ContactInboxWithContactBuilder.new({ + inbox: inbox, + contact_attributes: { additional_attributes: additional_attributes } + }).perform end end diff --git a/app/models/channel/whatsapp.rb b/app/models/channel/whatsapp.rb index 7be40ab33..8765cdf42 100644 --- a/app/models/channel/whatsapp.rb +++ b/app/models/channel/whatsapp.rb @@ -25,11 +25,12 @@ class Channel::Whatsapp < ApplicationRecord # default at the moment is 360dialog lets change later. PROVIDERS = %w[default whatsapp_cloud].freeze + before_validation :ensure_webhook_verify_token validates :provider, inclusion: { in: PROVIDERS } - validates :phone_number, presence: true, uniqueness: true validate :validate_provider_config + after_create :sync_templates def name @@ -56,6 +57,10 @@ class Channel::Whatsapp < ApplicationRecord private + def ensure_webhook_verify_token + provider_config['webhook_verify_token'] ||= SecureRandom.hex(16) if provider == 'whatsapp_cloud' + end + def validate_provider_config errors.add(:provider_config, 'Invalid Credentials') unless provider_service.validate_provider_config? end diff --git a/app/models/contact_inbox.rb b/app/models/contact_inbox.rb index dcbd2f56b..24f3b727a 100644 --- a/app/models/contact_inbox.rb +++ b/app/models/contact_inbox.rb @@ -22,6 +22,7 @@ class ContactInbox < ApplicationRecord include Pubsubable + include RegexHelper validates :inbox_id, presence: true validates :contact_id, presence: true validates :source_id, presence: true @@ -51,10 +52,10 @@ class ContactInbox < ApplicationRecord def validate_twilio_source_id # https://www.twilio.com/docs/glossary/what-e164#regex-matching-for-e164 - if inbox.channel.medium == 'sms' && !/\+[1-9]\d{1,14}\z/.match?(source_id) - errors.add(:source_id, 'invalid source id for twilio sms inbox. valid Regex /\+[1-9]\d{1,14}\z/') - elsif inbox.channel.medium == 'whatsapp' && !/whatsapp:\+[1-9]\d{1,14}\z/.match?(source_id) - errors.add(:source_id, 'invalid source id for twilio whatsapp inbox. valid Regex /whatsapp:\+[1-9]\d{1,14}\z/') + if inbox.channel.medium == 'sms' && !TWILIO_CHANNEL_SMS_REGEX.match?(source_id) + errors.add(:source_id, "invalid source id for twilio sms inbox. valid Regex #{TWILIO_CHANNEL_SMS_REGEX}") + elsif inbox.channel.medium == 'whatsapp' && !TWILIO_CHANNEL_WHATSAPP_REGEX.match?(source_id) + errors.add(:source_id, "invalid source id for twilio whatsapp inbox. valid Regex #{TWILIO_CHANNEL_WHATSAPP_REGEX}") end end @@ -62,8 +63,15 @@ class ContactInbox < ApplicationRecord errors.add(:source_id, "invalid source id for Email inbox. valid Regex #{Devise.email_regexp}") unless Devise.email_regexp.match?(source_id) end + def validate_whatsapp_source_id + return if WHATSAPP_CHANNEL_REGEX.match?(source_id) + + errors.add(:source_id, "invalid source id for whatsapp inbox. valid Regex #{WHATSAPP_CHANNEL_REGEX}") + end + def valid_source_id_format? validate_twilio_source_id if inbox.channel_type == 'Channel::TwilioSms' validate_email_source_id if inbox.channel_type == 'Channel::Email' + validate_whatsapp_source_id if inbox.channel_type == 'Channel::Whatsapp' end end diff --git a/app/models/conversation.rb b/app/models/conversation.rb index 3a833ef09..29eab8433 100644 --- a/app/models/conversation.rb +++ b/app/models/conversation.rb @@ -31,8 +31,10 @@ # index_conversations_on_account_id_and_display_id (account_id,display_id) UNIQUE # index_conversations_on_assignee_id_and_account_id (assignee_id,account_id) # index_conversations_on_campaign_id (campaign_id) +# index_conversations_on_contact_id (contact_id) # index_conversations_on_contact_inbox_id (contact_inbox_id) # index_conversations_on_first_reply_created_at (first_reply_created_at) +# index_conversations_on_inbox_id (inbox_id) # index_conversations_on_last_activity_at (last_activity_at) # index_conversations_on_status_and_account_id (status,account_id) # index_conversations_on_team_id (team_id) diff --git a/app/models/message.rb b/app/models/message.rb index a2382ae3a..a3f63dbe9 100644 --- a/app/models/message.rb +++ b/app/models/message.rb @@ -116,7 +116,7 @@ class Message < ApplicationRecord end def webhook_data - { + data = { account: account.webhook_data, additional_attributes: additional_attributes, content_attributes: content_attributes, @@ -131,6 +131,8 @@ class Message < ApplicationRecord sender: sender.try(:webhook_data), source_id: source_id } + data.merge!(attachments: attachments.map(&:push_event_data)) if attachments.present? + data end def content @@ -241,7 +243,7 @@ class Message < ApplicationRecord end def validate_attachments_limit(_attachment) - errors.add(attachments: 'exceeded maximum allowed') if attachments.size >= NUMBER_OF_PERMITTED_ATTACHMENTS + errors.add(:attachments, message: 'exceeded maximum allowed') if attachments.size >= NUMBER_OF_PERMITTED_ATTACHMENTS end def set_conversation_activity diff --git a/app/models/webhook.rb b/app/models/webhook.rb index fe97fe583..5b0095093 100644 --- a/app/models/webhook.rb +++ b/app/models/webhook.rb @@ -5,7 +5,7 @@ # id :bigint not null, primary key # subscriptions :jsonb # url :string -# webhook_type :integer default("account") +# webhook_type :integer default("account_type") # created_at :datetime not null # updated_at :datetime not null # account_id :integer @@ -23,7 +23,7 @@ class Webhook < ApplicationRecord validates :account_id, presence: true validates :url, uniqueness: { scope: [:account_id] }, format: URI::DEFAULT_PARSER.make_regexp(%w[http https]) validate :validate_webhook_subscriptions - enum webhook_type: { account: 0, inbox: 1 } + enum webhook_type: { account_type: 0, inbox_type: 1 } ALLOWED_WEBHOOK_EVENTS = %w[conversation_status_changed conversation_updated conversation_created message_created message_updated webwidget_triggered].freeze diff --git a/app/models/working_hour.rb b/app/models/working_hour.rb index 885165da2..01b972bd4 100644 --- a/app/models/working_hour.rb +++ b/app/models/working_hour.rb @@ -40,7 +40,10 @@ class WorkingHour < ApplicationRecord validate :open_all_day_and_closed_all_day def self.today - find_by(day_of_week: Date.current.wday) + # While getting the day of the week, consider the timezone as well. `first` would + # return the first working hour from the list of working hours available per week. + inbox = first.inbox + find_by(day_of_week: Time.zone.now.in_time_zone(inbox.timezone).to_date.wday) end def open_at?(time) diff --git a/app/services/instagram/message_text.rb b/app/services/instagram/message_text.rb index 89cb12884..abcbe2d60 100644 --- a/app/services/instagram/message_text.rb +++ b/app/services/instagram/message_text.rb @@ -42,7 +42,7 @@ class Instagram::MessageText < Instagram::WebhooksBaseService ChatwootExceptionTracker.new(e, account: @inbox.account).capture_exception end - find_or_create_contact(result) + find_or_create_contact(result) if result.present? end def agent_message_via_echo? @@ -71,6 +71,8 @@ class Instagram::MessageText < Instagram::WebhooksBaseService end def create_message + return unless @contact_inbox + Messages::Instagram::MessageBuilder.new(@messaging, @inbox, outgoing_echo: agent_message_via_echo?).perform end diff --git a/app/services/line/incoming_message_service.rb b/app/services/line/incoming_message_service.rb index 535c03dc7..48a52eb8f 100644 --- a/app/services/line/incoming_message_service.rb +++ b/app/services/line/incoming_message_service.rb @@ -81,7 +81,7 @@ class Line::IncomingMessageService end def set_contact - contact_inbox = ::ContactBuilder.new( + contact_inbox = ::ContactInboxWithContactBuilder.new( source_id: line_contact_info['userId'], inbox: inbox, contact_attributes: contact_attributes diff --git a/app/services/sms/incoming_message_service.rb b/app/services/sms/incoming_message_service.rb index 7ee6e3e63..7aa22b19e 100644 --- a/app/services/sms/incoming_message_service.rb +++ b/app/services/sms/incoming_message_service.rb @@ -37,7 +37,7 @@ class Sms::IncomingMessageService end def set_contact - contact_inbox = ::ContactBuilder.new( + contact_inbox = ::ContactInboxWithContactBuilder.new( source_id: params[:from], inbox: @inbox, contact_attributes: contact_attributes diff --git a/app/services/telegram/incoming_message_service.rb b/app/services/telegram/incoming_message_service.rb index d56e20e8d..e8ab8a72c 100644 --- a/app/services/telegram/incoming_message_service.rb +++ b/app/services/telegram/incoming_message_service.rb @@ -12,7 +12,7 @@ class Telegram::IncomingMessageService set_contact update_contact_avatar set_conversation - @message = @conversation.messages.create!( + @message = @conversation.messages.build( content: params[:message][:text].presence || params[:message][:caption], account_id: @inbox.account_id, inbox_id: @inbox.id, @@ -31,7 +31,7 @@ class Telegram::IncomingMessageService end def set_contact - contact_inbox = ::ContactBuilder.new( + contact_inbox = ::ContactInboxWithContactBuilder.new( source_id: params[:message][:from][:id], inbox: inbox, contact_attributes: contact_attributes diff --git a/app/services/twilio/incoming_message_service.rb b/app/services/twilio/incoming_message_service.rb index 50c77111c..4473131df 100644 --- a/app/services/twilio/incoming_message_service.rb +++ b/app/services/twilio/incoming_message_service.rb @@ -47,7 +47,7 @@ class Twilio::IncomingMessageService end def set_contact - contact_inbox = ::ContactBuilder.new( + contact_inbox = ::ContactInboxWithContactBuilder.new( source_id: params[:From], inbox: inbox, contact_attributes: contact_attributes diff --git a/app/services/whatsapp/incoming_message_base_service.rb b/app/services/whatsapp/incoming_message_base_service.rb index e55d5fd26..da08e02d6 100644 --- a/app/services/whatsapp/incoming_message_base_service.rb +++ b/app/services/whatsapp/incoming_message_base_service.rb @@ -12,7 +12,7 @@ class Whatsapp::IncomingMessageBaseService set_conversation - return if @processed_params[:messages].blank? + return if @processed_params[:messages].blank? || unprocessable_message_type? @message = @conversation.messages.build( content: message_content(@processed_params[:messages].first), @@ -48,7 +48,7 @@ class Whatsapp::IncomingMessageBaseService contact_params = @processed_params[:contacts]&.first return if contact_params.blank? - contact_inbox = ::ContactBuilder.new( + contact_inbox = ::ContactInboxWithContactBuilder.new( source_id: contact_params[:wa_id], inbox: inbox, contact_attributes: { name: contact_params.dig(:profile, :name), phone_number: "+#{@processed_params[:messages].first[:from]}" } @@ -86,6 +86,10 @@ class Whatsapp::IncomingMessageBaseService @processed_params[:messages].first[:type] end + def unprocessable_message_type? + %w[reaction contacts].include?(message_type) + end + def attach_files return if %w[text button interactive].include?(message_type) diff --git a/app/views/api/v1/accounts/agent_bots/create.json.jbuilder b/app/views/api/v1/accounts/agent_bots/create.json.jbuilder index f84bfc80d..f647ac383 100644 --- a/app/views/api/v1/accounts/agent_bots/create.json.jbuilder +++ b/app/views/api/v1/accounts/agent_bots/create.json.jbuilder @@ -1 +1 @@ -json.partial! 'api/v1/models/agent_bot.json.jbuilder', resource: AgentBotPresenter.new(@agent_bot) +json.partial! 'api/v1/models/agent_bot', formats: [:json], resource: AgentBotPresenter.new(@agent_bot) diff --git a/app/views/api/v1/accounts/agent_bots/index.json.jbuilder b/app/views/api/v1/accounts/agent_bots/index.json.jbuilder index e77d418fb..39c14a218 100644 --- a/app/views/api/v1/accounts/agent_bots/index.json.jbuilder +++ b/app/views/api/v1/accounts/agent_bots/index.json.jbuilder @@ -1,3 +1,3 @@ json.array! @agent_bots do |agent_bot| - json.partial! 'api/v1/models/agent_bot.json.jbuilder', resource: AgentBotPresenter.new(agent_bot) + json.partial! 'api/v1/models/agent_bot', formats: [:json], resource: AgentBotPresenter.new(agent_bot) end diff --git a/app/views/api/v1/accounts/agent_bots/show.json.jbuilder b/app/views/api/v1/accounts/agent_bots/show.json.jbuilder index f84bfc80d..f647ac383 100644 --- a/app/views/api/v1/accounts/agent_bots/show.json.jbuilder +++ b/app/views/api/v1/accounts/agent_bots/show.json.jbuilder @@ -1 +1 @@ -json.partial! 'api/v1/models/agent_bot.json.jbuilder', resource: AgentBotPresenter.new(@agent_bot) +json.partial! 'api/v1/models/agent_bot', formats: [:json], resource: AgentBotPresenter.new(@agent_bot) diff --git a/app/views/api/v1/accounts/agent_bots/update.json.jbuilder b/app/views/api/v1/accounts/agent_bots/update.json.jbuilder index f84bfc80d..f647ac383 100644 --- a/app/views/api/v1/accounts/agent_bots/update.json.jbuilder +++ b/app/views/api/v1/accounts/agent_bots/update.json.jbuilder @@ -1 +1 @@ -json.partial! 'api/v1/models/agent_bot.json.jbuilder', resource: AgentBotPresenter.new(@agent_bot) +json.partial! 'api/v1/models/agent_bot', formats: [:json], resource: AgentBotPresenter.new(@agent_bot) diff --git a/app/views/api/v1/accounts/agents/create.json.jbuilder b/app/views/api/v1/accounts/agents/create.json.jbuilder index 7f22d270d..adad85132 100644 --- a/app/views/api/v1/accounts/agents/create.json.jbuilder +++ b/app/views/api/v1/accounts/agents/create.json.jbuilder @@ -1 +1 @@ -json.partial! 'api/v1/models/agent.json.jbuilder', resource: @user +json.partial! 'api/v1/models/agent', formats: [:json], resource: @user diff --git a/app/views/api/v1/accounts/agents/index.json.jbuilder b/app/views/api/v1/accounts/agents/index.json.jbuilder index 8374ce793..935e789fb 100644 --- a/app/views/api/v1/accounts/agents/index.json.jbuilder +++ b/app/views/api/v1/accounts/agents/index.json.jbuilder @@ -1,3 +1,3 @@ json.array! @agents do |agent| - json.partial! 'api/v1/models/agent.json.jbuilder', resource: agent + json.partial! 'api/v1/models/agent', formats: [:json], resource: agent end diff --git a/app/views/api/v1/accounts/agents/update.json.jbuilder b/app/views/api/v1/accounts/agents/update.json.jbuilder index 38328ca08..18760d9b6 100644 --- a/app/views/api/v1/accounts/agents/update.json.jbuilder +++ b/app/views/api/v1/accounts/agents/update.json.jbuilder @@ -1 +1 @@ -json.partial! 'api/v1/models/agent.json.jbuilder', resource: @agent +json.partial! 'api/v1/models/agent', formats: [:json], resource: @agent diff --git a/app/views/api/v1/accounts/articles/_article.json.jbuilder b/app/views/api/v1/accounts/articles/_article.json.jbuilder index 2428924c5..4a0d8a505 100644 --- a/app/views/api/v1/accounts/articles/_article.json.jbuilder +++ b/app/views/api/v1/accounts/articles/_article.json.jbuilder @@ -15,7 +15,7 @@ end if article.portal.present? json.portal do - json.partial! 'api/v1/accounts/portals/portal.json.jbuilder', portal: article.portal + json.partial! 'api/v1/accounts/portals/portal', formats: [:json], portal: article.portal end end @@ -23,14 +23,14 @@ json.views article.views if article.author.present? json.author do - json.partial! 'api/v1/models/agent.json.jbuilder', resource: article.author + json.partial! 'api/v1/models/agent', formats: [:json], resource: article.author end end json.associated_articles do if article.associated_articles.any? json.array! article.associated_articles.each do |associated_article| - json.partial! 'api/v1/accounts/articles/associated_article.json.jbuilder', article: associated_article + json.partial! 'api/v1/accounts/articles/associated_article', formats: [:json], article: associated_article end end end diff --git a/app/views/api/v1/accounts/articles/_associated_article.json.jbuilder b/app/views/api/v1/accounts/articles/_associated_article.json.jbuilder index 4c5439d1a..74101ed33 100644 --- a/app/views/api/v1/accounts/articles/_associated_article.json.jbuilder +++ b/app/views/api/v1/accounts/articles/_associated_article.json.jbuilder @@ -8,7 +8,7 @@ json.account_id article.account_id if article.portal.present? json.portal do - json.partial! 'api/v1/accounts/portals/portal.json.jbuilder', portal: article.portal + json.partial! 'api/v1/accounts/portals/portal', formats: [:json], portal: article.portal end end @@ -16,6 +16,6 @@ json.views article.views if article.author.present? json.author do - json.partial! 'api/v1/models/agent.json.jbuilder', resource: article.author + json.partial! 'api/v1/models/agent', formats: [:json], resource: article.author end end diff --git a/app/views/api/v1/accounts/articles/index.json.jbuilder b/app/views/api/v1/accounts/articles/index.json.jbuilder index fef8dc521..906cabae1 100644 --- a/app/views/api/v1/accounts/articles/index.json.jbuilder +++ b/app/views/api/v1/accounts/articles/index.json.jbuilder @@ -5,4 +5,9 @@ end json.meta do json.current_page @current_page json.articles_count @articles_count + json.all_articles_count @articles_count + json.archived_articles_count @articles.archived.size + json.published_count @articles.published.size + json.draft_articles_count @articles.draft.size + json.mine_articles_count @articles.search_by_author(current_user.id).size if current_user.present? end diff --git a/app/views/api/v1/accounts/assignable_agents/index.json.jbuilder b/app/views/api/v1/accounts/assignable_agents/index.json.jbuilder index af71dea1e..295c5cef9 100644 --- a/app/views/api/v1/accounts/assignable_agents/index.json.jbuilder +++ b/app/views/api/v1/accounts/assignable_agents/index.json.jbuilder @@ -1,5 +1,5 @@ json.payload do json.array! @assignable_agents do |agent| - json.partial! 'api/v1/models/agent.json.jbuilder', resource: agent + json.partial! 'api/v1/models/agent', formats: [:json], resource: agent end end diff --git a/app/views/api/v1/accounts/automation_rules/clone.json.jbuilder b/app/views/api/v1/accounts/automation_rules/clone.json.jbuilder index 1a6c02187..1a78f4ac7 100644 --- a/app/views/api/v1/accounts/automation_rules/clone.json.jbuilder +++ b/app/views/api/v1/accounts/automation_rules/clone.json.jbuilder @@ -1,3 +1,3 @@ json.payload do - json.partial! 'api/v1/accounts/automation_rules/partials/automation_rule.json.jbuilder', automation_rule: @automation_rule + json.partial! 'api/v1/accounts/automation_rules/partials/automation_rule', formats: [:json], automation_rule: @automation_rule end diff --git a/app/views/api/v1/accounts/automation_rules/create.json.jbuilder b/app/views/api/v1/accounts/automation_rules/create.json.jbuilder index f2f893279..a43a8a1df 100644 --- a/app/views/api/v1/accounts/automation_rules/create.json.jbuilder +++ b/app/views/api/v1/accounts/automation_rules/create.json.jbuilder @@ -1 +1 @@ -json.partial! 'api/v1/accounts/automation_rules/partials/automation_rule.json.jbuilder', automation_rule: @automation_rule +json.partial! 'api/v1/accounts/automation_rules/partials/automation_rule', formats: [:json], automation_rule: @automation_rule diff --git a/app/views/api/v1/accounts/automation_rules/index.json.jbuilder b/app/views/api/v1/accounts/automation_rules/index.json.jbuilder index e4a363086..a4b4f81be 100644 --- a/app/views/api/v1/accounts/automation_rules/index.json.jbuilder +++ b/app/views/api/v1/accounts/automation_rules/index.json.jbuilder @@ -1,5 +1,5 @@ json.payload do json.array! @automation_rules do |automation_rule| - json.partial! 'api/v1/accounts/automation_rules/partials/automation_rule.json.jbuilder', automation_rule: automation_rule + json.partial! 'api/v1/accounts/automation_rules/partials/automation_rule', formats: [:json], automation_rule: automation_rule end end diff --git a/app/views/api/v1/accounts/automation_rules/show.json.jbuilder b/app/views/api/v1/accounts/automation_rules/show.json.jbuilder index 1a6c02187..1a78f4ac7 100644 --- a/app/views/api/v1/accounts/automation_rules/show.json.jbuilder +++ b/app/views/api/v1/accounts/automation_rules/show.json.jbuilder @@ -1,3 +1,3 @@ json.payload do - json.partial! 'api/v1/accounts/automation_rules/partials/automation_rule.json.jbuilder', automation_rule: @automation_rule + json.partial! 'api/v1/accounts/automation_rules/partials/automation_rule', formats: [:json], automation_rule: @automation_rule end diff --git a/app/views/api/v1/accounts/automation_rules/update.json.jbuilder b/app/views/api/v1/accounts/automation_rules/update.json.jbuilder index 1a6c02187..1a78f4ac7 100644 --- a/app/views/api/v1/accounts/automation_rules/update.json.jbuilder +++ b/app/views/api/v1/accounts/automation_rules/update.json.jbuilder @@ -1,3 +1,3 @@ json.payload do - json.partial! 'api/v1/accounts/automation_rules/partials/automation_rule.json.jbuilder', automation_rule: @automation_rule + json.partial! 'api/v1/accounts/automation_rules/partials/automation_rule', formats: [:json], automation_rule: @automation_rule end diff --git a/app/views/api/v1/accounts/callbacks/reauthorize_page.json.jbuilder b/app/views/api/v1/accounts/callbacks/reauthorize_page.json.jbuilder index 3bc362380..e12b90d18 100644 --- a/app/views/api/v1/accounts/callbacks/reauthorize_page.json.jbuilder +++ b/app/views/api/v1/accounts/callbacks/reauthorize_page.json.jbuilder @@ -1,3 +1,3 @@ json.data do - json.partial! 'api/v1/models/inbox.json.jbuilder', resource: @inbox + json.partial! 'api/v1/models/inbox', formats: [:json], resource: @inbox end diff --git a/app/views/api/v1/accounts/campaigns/create.json.jbuilder b/app/views/api/v1/accounts/campaigns/create.json.jbuilder index bd136a8a2..87482e1f4 100644 --- a/app/views/api/v1/accounts/campaigns/create.json.jbuilder +++ b/app/views/api/v1/accounts/campaigns/create.json.jbuilder @@ -1 +1 @@ -json.partial! 'api/v1/models/campaign.json.jbuilder', resource: @campaign +json.partial! 'api/v1/models/campaign', formats: [:json], resource: @campaign diff --git a/app/views/api/v1/accounts/campaigns/index.json.jbuilder b/app/views/api/v1/accounts/campaigns/index.json.jbuilder index c0e90acd7..c51ec1da5 100644 --- a/app/views/api/v1/accounts/campaigns/index.json.jbuilder +++ b/app/views/api/v1/accounts/campaigns/index.json.jbuilder @@ -1,3 +1,3 @@ json.array! @campaigns do |campaign| - json.partial! 'api/v1/models/campaign.json.jbuilder', resource: campaign + json.partial! 'api/v1/models/campaign', formats: [:json], resource: campaign end diff --git a/app/views/api/v1/accounts/campaigns/show.json.jbuilder b/app/views/api/v1/accounts/campaigns/show.json.jbuilder index bd136a8a2..87482e1f4 100644 --- a/app/views/api/v1/accounts/campaigns/show.json.jbuilder +++ b/app/views/api/v1/accounts/campaigns/show.json.jbuilder @@ -1 +1 @@ -json.partial! 'api/v1/models/campaign.json.jbuilder', resource: @campaign +json.partial! 'api/v1/models/campaign', formats: [:json], resource: @campaign diff --git a/app/views/api/v1/accounts/campaigns/update.json.jbuilder b/app/views/api/v1/accounts/campaigns/update.json.jbuilder index bd136a8a2..87482e1f4 100644 --- a/app/views/api/v1/accounts/campaigns/update.json.jbuilder +++ b/app/views/api/v1/accounts/campaigns/update.json.jbuilder @@ -1 +1 @@ -json.partial! 'api/v1/models/campaign.json.jbuilder', resource: @campaign +json.partial! 'api/v1/models/campaign', formats: [:json], resource: @campaign diff --git a/app/views/api/v1/accounts/categories/_category.json.jbuilder b/app/views/api/v1/accounts/categories/_category.json.jbuilder index dccf70bd6..35105c813 100644 --- a/app/views/api/v1/accounts/categories/_category.json.jbuilder +++ b/app/views/api/v1/accounts/categories/_category.json.jbuilder @@ -9,20 +9,20 @@ json.account_id category.account_id json.related_categories do if category.related_categories.any? json.array! category.related_categories.each do |related_category| - json.partial! 'api/v1/accounts/categories/associated_category.json.jbuilder', category: related_category + json.partial! 'api/v1/accounts/categories/associated_category', formats: [:json], category: related_category end end end if category.parent_category.present? json.parent_category do - json.partial! 'api/v1/accounts/categories/associated_category.json.jbuilder', category: category.parent_category + json.partial! 'api/v1/accounts/categories/associated_category', formats: [:json], category: category.parent_category end end if category.root_category.present? json.root_category do - json.partial! 'api/v1/accounts/categories/associated_category.json.jbuilder', category: category.root_category + json.partial! 'api/v1/accounts/categories/associated_category', formats: [:json], category: category.root_category end end diff --git a/app/views/api/v1/accounts/channels/twilio_channels/create.json.jbuilder b/app/views/api/v1/accounts/channels/twilio_channels/create.json.jbuilder index 981c1dec0..2ad94ff82 100644 --- a/app/views/api/v1/accounts/channels/twilio_channels/create.json.jbuilder +++ b/app/views/api/v1/accounts/channels/twilio_channels/create.json.jbuilder @@ -1 +1 @@ -json.partial! 'api/v1/models/inbox.json.jbuilder', resource: @inbox +json.partial! 'api/v1/models/inbox', formats: [:json], resource: @inbox diff --git a/app/views/api/v1/accounts/contacts/active.json.jbuilder b/app/views/api/v1/accounts/contacts/active.json.jbuilder index 70e112b56..cde9e5445 100644 --- a/app/views/api/v1/accounts/contacts/active.json.jbuilder +++ b/app/views/api/v1/accounts/contacts/active.json.jbuilder @@ -5,6 +5,6 @@ end json.payload do json.array! @contacts do |contact| - json.partial! 'api/v1/models/contact.json.jbuilder', resource: contact, with_contact_inboxes: true + json.partial! 'api/v1/models/contact', formats: [:json], resource: contact, with_contact_inboxes: true end end diff --git a/app/views/api/v1/accounts/contacts/avatar.json.jbuilder b/app/views/api/v1/accounts/contacts/avatar.json.jbuilder index 4cdf48e9b..897dcf594 100644 --- a/app/views/api/v1/accounts/contacts/avatar.json.jbuilder +++ b/app/views/api/v1/accounts/contacts/avatar.json.jbuilder @@ -1,3 +1,3 @@ json.payload do - json.partial! 'api/v1/models/contact.json.jbuilder', resource: @contact, with_contact_inboxes: false + json.partial! 'api/v1/models/contact', formats: [:json], resource: @contact, with_contact_inboxes: false end diff --git a/app/views/api/v1/accounts/contacts/contact_inboxes/create.json.jbuilder b/app/views/api/v1/accounts/contacts/contact_inboxes/create.json.jbuilder index 7d325d8fc..e73bf6ddd 100644 --- a/app/views/api/v1/accounts/contacts/contact_inboxes/create.json.jbuilder +++ b/app/views/api/v1/accounts/contacts/contact_inboxes/create.json.jbuilder @@ -1 +1 @@ -json.partial! 'api/v1/models/contact_inbox.json.jbuilder', resource: @contact_inbox +json.partial! 'api/v1/models/contact_inbox', formats: [:json], resource: @contact_inbox diff --git a/app/views/api/v1/accounts/contacts/contactable_inboxes.json.jbuilder b/app/views/api/v1/accounts/contacts/contactable_inboxes.json.jbuilder index 0e8b27d08..7acee2e41 100644 --- a/app/views/api/v1/accounts/contacts/contactable_inboxes.json.jbuilder +++ b/app/views/api/v1/accounts/contacts/contactable_inboxes.json.jbuilder @@ -1,7 +1,7 @@ json.payload do json.array! @contactable_inboxes do |contactable_inbox| json.inbox do - json.partial! 'api/v1/models/inbox.json.jbuilder', resource: contactable_inbox[:inbox] + json.partial! 'api/v1/models/inbox', formats: [:json], resource: contactable_inbox[:inbox] end json.source_id contactable_inbox[:source_id] end diff --git a/app/views/api/v1/accounts/contacts/conversations/index.json.jbuilder b/app/views/api/v1/accounts/contacts/conversations/index.json.jbuilder index 4e8bcc8cd..c81e4bdd0 100644 --- a/app/views/api/v1/accounts/contacts/conversations/index.json.jbuilder +++ b/app/views/api/v1/accounts/contacts/conversations/index.json.jbuilder @@ -1,5 +1,5 @@ json.payload do json.array! @conversations do |conversation| - json.partial! 'api/v1/conversations/partials/conversation.json.jbuilder', conversation: conversation + json.partial! 'api/v1/conversations/partials/conversation', formats: [:json], conversation: conversation end end diff --git a/app/views/api/v1/accounts/contacts/create.json.jbuilder b/app/views/api/v1/accounts/contacts/create.json.jbuilder index a9ac26750..ecbe596e8 100644 --- a/app/views/api/v1/accounts/contacts/create.json.jbuilder +++ b/app/views/api/v1/accounts/contacts/create.json.jbuilder @@ -1,6 +1,6 @@ json.payload do json.contact do - json.partial! 'api/v1/models/contact.json.jbuilder', resource: @contact, with_contact_inboxes: true + json.partial! 'api/v1/models/contact', formats: [:json], resource: @contact, with_contact_inboxes: true end json.contact_inbox do json.inbox @contact_inbox&.inbox diff --git a/app/views/api/v1/accounts/contacts/destroy_custom_attributes.json.jbuilder b/app/views/api/v1/accounts/contacts/destroy_custom_attributes.json.jbuilder index 64f6c9c31..524a393bf 100644 --- a/app/views/api/v1/accounts/contacts/destroy_custom_attributes.json.jbuilder +++ b/app/views/api/v1/accounts/contacts/destroy_custom_attributes.json.jbuilder @@ -1,3 +1,3 @@ json.payload do - json.partial! 'api/v1/models/contact.json.jbuilder', resource: @contact, with_contact_inboxes: true + json.partial! 'api/v1/models/contact', formats: [:json], resource: @contact, with_contact_inboxes: true end diff --git a/app/views/api/v1/accounts/contacts/filter.json.jbuilder b/app/views/api/v1/accounts/contacts/filter.json.jbuilder index b3af9a8b7..577dff4be 100644 --- a/app/views/api/v1/accounts/contacts/filter.json.jbuilder +++ b/app/views/api/v1/accounts/contacts/filter.json.jbuilder @@ -5,6 +5,6 @@ end json.payload do json.array! @contacts do |contact| - json.partial! 'api/v1/models/contact.json.jbuilder', resource: contact, with_contact_inboxes: @include_contact_inboxes + json.partial! 'api/v1/models/contact', formats: [:json], resource: contact, with_contact_inboxes: @include_contact_inboxes end end diff --git a/app/views/api/v1/accounts/contacts/index.json.jbuilder b/app/views/api/v1/accounts/contacts/index.json.jbuilder index b3af9a8b7..577dff4be 100644 --- a/app/views/api/v1/accounts/contacts/index.json.jbuilder +++ b/app/views/api/v1/accounts/contacts/index.json.jbuilder @@ -5,6 +5,6 @@ end json.payload do json.array! @contacts do |contact| - json.partial! 'api/v1/models/contact.json.jbuilder', resource: contact, with_contact_inboxes: @include_contact_inboxes + json.partial! 'api/v1/models/contact', formats: [:json], resource: contact, with_contact_inboxes: @include_contact_inboxes end end diff --git a/app/views/api/v1/accounts/contacts/notes/create.json.jbuilder b/app/views/api/v1/accounts/contacts/notes/create.json.jbuilder index b9f9e3fd6..5bec1112b 100644 --- a/app/views/api/v1/accounts/contacts/notes/create.json.jbuilder +++ b/app/views/api/v1/accounts/contacts/notes/create.json.jbuilder @@ -1 +1 @@ -json.partial! 'api/v1/models/note.json.jbuilder', resource: @note +json.partial! 'api/v1/models/note', formats: [:json], resource: @note diff --git a/app/views/api/v1/accounts/contacts/notes/index.json.jbuilder b/app/views/api/v1/accounts/contacts/notes/index.json.jbuilder index 67327b94c..f224301a2 100644 --- a/app/views/api/v1/accounts/contacts/notes/index.json.jbuilder +++ b/app/views/api/v1/accounts/contacts/notes/index.json.jbuilder @@ -1,3 +1,3 @@ json.array! @notes do |note| - json.partial! 'api/v1/models/note.json.jbuilder', resource: note + json.partial! 'api/v1/models/note', formats: [:json], resource: note end diff --git a/app/views/api/v1/accounts/contacts/notes/show.json.jbuilder b/app/views/api/v1/accounts/contacts/notes/show.json.jbuilder index b9f9e3fd6..5bec1112b 100644 --- a/app/views/api/v1/accounts/contacts/notes/show.json.jbuilder +++ b/app/views/api/v1/accounts/contacts/notes/show.json.jbuilder @@ -1 +1 @@ -json.partial! 'api/v1/models/note.json.jbuilder', resource: @note +json.partial! 'api/v1/models/note', formats: [:json], resource: @note diff --git a/app/views/api/v1/accounts/contacts/notes/update.json.jbuilder b/app/views/api/v1/accounts/contacts/notes/update.json.jbuilder index b9f9e3fd6..5bec1112b 100644 --- a/app/views/api/v1/accounts/contacts/notes/update.json.jbuilder +++ b/app/views/api/v1/accounts/contacts/notes/update.json.jbuilder @@ -1 +1 @@ -json.partial! 'api/v1/models/note.json.jbuilder', resource: @note +json.partial! 'api/v1/models/note', formats: [:json], resource: @note diff --git a/app/views/api/v1/accounts/contacts/search.json.jbuilder b/app/views/api/v1/accounts/contacts/search.json.jbuilder index b3af9a8b7..577dff4be 100644 --- a/app/views/api/v1/accounts/contacts/search.json.jbuilder +++ b/app/views/api/v1/accounts/contacts/search.json.jbuilder @@ -5,6 +5,6 @@ end json.payload do json.array! @contacts do |contact| - json.partial! 'api/v1/models/contact.json.jbuilder', resource: contact, with_contact_inboxes: @include_contact_inboxes + json.partial! 'api/v1/models/contact', formats: [:json], resource: contact, with_contact_inboxes: @include_contact_inboxes end end diff --git a/app/views/api/v1/accounts/contacts/show.json.jbuilder b/app/views/api/v1/accounts/contacts/show.json.jbuilder index 64f6c9c31..524a393bf 100644 --- a/app/views/api/v1/accounts/contacts/show.json.jbuilder +++ b/app/views/api/v1/accounts/contacts/show.json.jbuilder @@ -1,3 +1,3 @@ json.payload do - json.partial! 'api/v1/models/contact.json.jbuilder', resource: @contact, with_contact_inboxes: true + json.partial! 'api/v1/models/contact', formats: [:json], resource: @contact, with_contact_inboxes: true end diff --git a/app/views/api/v1/accounts/contacts/update.json.jbuilder b/app/views/api/v1/accounts/contacts/update.json.jbuilder index 64f6c9c31..524a393bf 100644 --- a/app/views/api/v1/accounts/contacts/update.json.jbuilder +++ b/app/views/api/v1/accounts/contacts/update.json.jbuilder @@ -1,3 +1,3 @@ json.payload do - json.partial! 'api/v1/models/contact.json.jbuilder', resource: @contact, with_contact_inboxes: true + json.partial! 'api/v1/models/contact', formats: [:json], resource: @contact, with_contact_inboxes: true end diff --git a/app/views/api/v1/accounts/conversations/create.json.jbuilder b/app/views/api/v1/accounts/conversations/create.json.jbuilder index 2d39b121c..c273dd3c6 100644 --- a/app/views/api/v1/accounts/conversations/create.json.jbuilder +++ b/app/views/api/v1/accounts/conversations/create.json.jbuilder @@ -1 +1 @@ -json.partial! 'api/v1/conversations/partials/conversation.json.jbuilder', conversation: @conversation +json.partial! 'api/v1/conversations/partials/conversation', formats: [:json], conversation: @conversation diff --git a/app/views/api/v1/accounts/conversations/filter.json.jbuilder b/app/views/api/v1/accounts/conversations/filter.json.jbuilder index c1d98e2c2..d72c4e857 100644 --- a/app/views/api/v1/accounts/conversations/filter.json.jbuilder +++ b/app/views/api/v1/accounts/conversations/filter.json.jbuilder @@ -5,6 +5,6 @@ json.meta do end json.payload do json.array! @conversations do |conversation| - json.partial! 'api/v1/conversations/partials/conversation.json.jbuilder', conversation: conversation + json.partial! 'api/v1/conversations/partials/conversation', formats: [:json], conversation: conversation end end diff --git a/app/views/api/v1/accounts/conversations/index.json.jbuilder b/app/views/api/v1/accounts/conversations/index.json.jbuilder index c54baab78..670899f38 100644 --- a/app/views/api/v1/accounts/conversations/index.json.jbuilder +++ b/app/views/api/v1/accounts/conversations/index.json.jbuilder @@ -7,7 +7,7 @@ json.data do end json.payload do json.array! @conversations do |conversation| - json.partial! 'api/v1/conversations/partials/conversation.json.jbuilder', conversation: conversation + json.partial! 'api/v1/conversations/partials/conversation', formats: [:json], conversation: conversation end end end diff --git a/app/views/api/v1/accounts/conversations/search.json.jbuilder b/app/views/api/v1/accounts/conversations/search.json.jbuilder index ac70d3a55..fce9749c3 100644 --- a/app/views/api/v1/accounts/conversations/search.json.jbuilder +++ b/app/views/api/v1/accounts/conversations/search.json.jbuilder @@ -5,6 +5,6 @@ json.meta do end json.payload do json.array! @conversations do |conversation| - json.partial! 'api/v1/models/conversation.json.jbuilder', conversation: conversation + json.partial! 'api/v1/models/conversation', formats: [:json], conversation: conversation end end diff --git a/app/views/api/v1/accounts/conversations/show.json.jbuilder b/app/views/api/v1/accounts/conversations/show.json.jbuilder index 2d39b121c..c273dd3c6 100644 --- a/app/views/api/v1/accounts/conversations/show.json.jbuilder +++ b/app/views/api/v1/accounts/conversations/show.json.jbuilder @@ -1 +1 @@ -json.partial! 'api/v1/conversations/partials/conversation.json.jbuilder', conversation: @conversation +json.partial! 'api/v1/conversations/partials/conversation', formats: [:json], conversation: @conversation diff --git a/app/views/api/v1/accounts/conversations/update_last_seen.json.jbuilder b/app/views/api/v1/accounts/conversations/update_last_seen.json.jbuilder index 2d39b121c..c273dd3c6 100644 --- a/app/views/api/v1/accounts/conversations/update_last_seen.json.jbuilder +++ b/app/views/api/v1/accounts/conversations/update_last_seen.json.jbuilder @@ -1 +1 @@ -json.partial! 'api/v1/conversations/partials/conversation.json.jbuilder', conversation: @conversation +json.partial! 'api/v1/conversations/partials/conversation', formats: [:json], conversation: @conversation diff --git a/app/views/api/v1/accounts/csat_survey_responses/index.json.jbuilder b/app/views/api/v1/accounts/csat_survey_responses/index.json.jbuilder index e5e3dbc93..ab154261f 100644 --- a/app/views/api/v1/accounts/csat_survey_responses/index.json.jbuilder +++ b/app/views/api/v1/accounts/csat_survey_responses/index.json.jbuilder @@ -1,3 +1,3 @@ json.array! @csat_survey_responses do |csat_survey_response| - json.partial! 'api/v1/models/csat_survey_response.json.jbuilder', resource: csat_survey_response + json.partial! 'api/v1/models/csat_survey_response', formats: [:json], resource: csat_survey_response end diff --git a/app/views/api/v1/accounts/custom_attribute_definitions/create.json.jbuilder b/app/views/api/v1/accounts/custom_attribute_definitions/create.json.jbuilder index d5be04284..b92e2d10c 100644 --- a/app/views/api/v1/accounts/custom_attribute_definitions/create.json.jbuilder +++ b/app/views/api/v1/accounts/custom_attribute_definitions/create.json.jbuilder @@ -1 +1 @@ -json.partial! 'api/v1/models/custom_attribute_definition.json.jbuilder', resource: @custom_attribute_definition +json.partial! 'api/v1/models/custom_attribute_definition', formats: [:json], resource: @custom_attribute_definition diff --git a/app/views/api/v1/accounts/custom_attribute_definitions/index.json.jbuilder b/app/views/api/v1/accounts/custom_attribute_definitions/index.json.jbuilder index 7bf1dd8aa..d7ab49bcd 100644 --- a/app/views/api/v1/accounts/custom_attribute_definitions/index.json.jbuilder +++ b/app/views/api/v1/accounts/custom_attribute_definitions/index.json.jbuilder @@ -1,3 +1,3 @@ json.array! @custom_attribute_definitions do |custom_attribute_definition| - json.partial! 'api/v1/models/custom_attribute_definition.json.jbuilder', resource: custom_attribute_definition + json.partial! 'api/v1/models/custom_attribute_definition', formats: [:json], resource: custom_attribute_definition end diff --git a/app/views/api/v1/accounts/custom_attribute_definitions/show.json.jbuilder b/app/views/api/v1/accounts/custom_attribute_definitions/show.json.jbuilder index d5be04284..b92e2d10c 100644 --- a/app/views/api/v1/accounts/custom_attribute_definitions/show.json.jbuilder +++ b/app/views/api/v1/accounts/custom_attribute_definitions/show.json.jbuilder @@ -1 +1 @@ -json.partial! 'api/v1/models/custom_attribute_definition.json.jbuilder', resource: @custom_attribute_definition +json.partial! 'api/v1/models/custom_attribute_definition', formats: [:json], resource: @custom_attribute_definition diff --git a/app/views/api/v1/accounts/custom_attribute_definitions/update.json.jbuilder b/app/views/api/v1/accounts/custom_attribute_definitions/update.json.jbuilder index d5be04284..b92e2d10c 100644 --- a/app/views/api/v1/accounts/custom_attribute_definitions/update.json.jbuilder +++ b/app/views/api/v1/accounts/custom_attribute_definitions/update.json.jbuilder @@ -1 +1 @@ -json.partial! 'api/v1/models/custom_attribute_definition.json.jbuilder', resource: @custom_attribute_definition +json.partial! 'api/v1/models/custom_attribute_definition', formats: [:json], resource: @custom_attribute_definition diff --git a/app/views/api/v1/accounts/custom_filters/create.json.jbuilder b/app/views/api/v1/accounts/custom_filters/create.json.jbuilder index 085761e41..0a2b07c45 100644 --- a/app/views/api/v1/accounts/custom_filters/create.json.jbuilder +++ b/app/views/api/v1/accounts/custom_filters/create.json.jbuilder @@ -1 +1 @@ -json.partial! 'api/v1/models/custom_filter.json.jbuilder', resource: @custom_filter +json.partial! 'api/v1/models/custom_filter', formats: [:json], resource: @custom_filter diff --git a/app/views/api/v1/accounts/custom_filters/index.json.jbuilder b/app/views/api/v1/accounts/custom_filters/index.json.jbuilder index 0fc3e67b4..ff3c2b253 100644 --- a/app/views/api/v1/accounts/custom_filters/index.json.jbuilder +++ b/app/views/api/v1/accounts/custom_filters/index.json.jbuilder @@ -1,3 +1,3 @@ json.array! @custom_filters do |custom_filter| - json.partial! 'api/v1/models/custom_filter.json.jbuilder', resource: custom_filter + json.partial! 'api/v1/models/custom_filter', formats: [:json], resource: custom_filter end diff --git a/app/views/api/v1/accounts/custom_filters/show.json.jbuilder b/app/views/api/v1/accounts/custom_filters/show.json.jbuilder index 085761e41..0a2b07c45 100644 --- a/app/views/api/v1/accounts/custom_filters/show.json.jbuilder +++ b/app/views/api/v1/accounts/custom_filters/show.json.jbuilder @@ -1 +1 @@ -json.partial! 'api/v1/models/custom_filter.json.jbuilder', resource: @custom_filter +json.partial! 'api/v1/models/custom_filter', formats: [:json], resource: @custom_filter diff --git a/app/views/api/v1/accounts/custom_filters/update.json.jbuilder b/app/views/api/v1/accounts/custom_filters/update.json.jbuilder index 085761e41..0a2b07c45 100644 --- a/app/views/api/v1/accounts/custom_filters/update.json.jbuilder +++ b/app/views/api/v1/accounts/custom_filters/update.json.jbuilder @@ -1 +1 @@ -json.partial! 'api/v1/models/custom_filter.json.jbuilder', resource: @custom_filter +json.partial! 'api/v1/models/custom_filter', formats: [:json], resource: @custom_filter diff --git a/app/views/api/v1/accounts/dashboard_apps/create.json.jbuilder b/app/views/api/v1/accounts/dashboard_apps/create.json.jbuilder index fd21053d3..6ee3dc072 100644 --- a/app/views/api/v1/accounts/dashboard_apps/create.json.jbuilder +++ b/app/views/api/v1/accounts/dashboard_apps/create.json.jbuilder @@ -1 +1 @@ -json.partial! 'api/v1/models/dashboard_app.json.jbuilder', resource: @dashboard_app +json.partial! 'api/v1/models/dashboard_app', formats: [:json], resource: @dashboard_app diff --git a/app/views/api/v1/accounts/dashboard_apps/index.json.jbuilder b/app/views/api/v1/accounts/dashboard_apps/index.json.jbuilder index d7e1f5a06..9ab1d643f 100644 --- a/app/views/api/v1/accounts/dashboard_apps/index.json.jbuilder +++ b/app/views/api/v1/accounts/dashboard_apps/index.json.jbuilder @@ -1,3 +1,3 @@ json.array! @dashboard_apps do |dashboard_app| - json.partial! 'api/v1/models/dashboard_app.json.jbuilder', resource: dashboard_app + json.partial! 'api/v1/models/dashboard_app', formats: [:json], resource: dashboard_app end diff --git a/app/views/api/v1/accounts/dashboard_apps/show.json.jbuilder b/app/views/api/v1/accounts/dashboard_apps/show.json.jbuilder index fd21053d3..6ee3dc072 100644 --- a/app/views/api/v1/accounts/dashboard_apps/show.json.jbuilder +++ b/app/views/api/v1/accounts/dashboard_apps/show.json.jbuilder @@ -1 +1 @@ -json.partial! 'api/v1/models/dashboard_app.json.jbuilder', resource: @dashboard_app +json.partial! 'api/v1/models/dashboard_app', formats: [:json], resource: @dashboard_app diff --git a/app/views/api/v1/accounts/dashboard_apps/update.json.jbuilder b/app/views/api/v1/accounts/dashboard_apps/update.json.jbuilder index fd21053d3..6ee3dc072 100644 --- a/app/views/api/v1/accounts/dashboard_apps/update.json.jbuilder +++ b/app/views/api/v1/accounts/dashboard_apps/update.json.jbuilder @@ -1 +1 @@ -json.partial! 'api/v1/models/dashboard_app.json.jbuilder', resource: @dashboard_app +json.partial! 'api/v1/models/dashboard_app', formats: [:json], resource: @dashboard_app diff --git a/app/views/api/v1/accounts/inbox_members/create.json.jbuilder b/app/views/api/v1/accounts/inbox_members/create.json.jbuilder index 1660ea43d..c80827ccf 100644 --- a/app/views/api/v1/accounts/inbox_members/create.json.jbuilder +++ b/app/views/api/v1/accounts/inbox_members/create.json.jbuilder @@ -1,5 +1,5 @@ json.payload do json.array! @agents do |agent| - json.partial! 'api/v1/models/agent.json.jbuilder', resource: agent + json.partial! 'api/v1/models/agent', formats: [:json], resource: agent end end diff --git a/app/views/api/v1/accounts/inbox_members/show.json.jbuilder b/app/views/api/v1/accounts/inbox_members/show.json.jbuilder index 1660ea43d..c80827ccf 100644 --- a/app/views/api/v1/accounts/inbox_members/show.json.jbuilder +++ b/app/views/api/v1/accounts/inbox_members/show.json.jbuilder @@ -1,5 +1,5 @@ json.payload do json.array! @agents do |agent| - json.partial! 'api/v1/models/agent.json.jbuilder', resource: agent + json.partial! 'api/v1/models/agent', formats: [:json], resource: agent end end diff --git a/app/views/api/v1/accounts/inbox_members/update.json.jbuilder b/app/views/api/v1/accounts/inbox_members/update.json.jbuilder index 1660ea43d..c80827ccf 100644 --- a/app/views/api/v1/accounts/inbox_members/update.json.jbuilder +++ b/app/views/api/v1/accounts/inbox_members/update.json.jbuilder @@ -1,5 +1,5 @@ json.payload do json.array! @agents do |agent| - json.partial! 'api/v1/models/agent.json.jbuilder', resource: agent + json.partial! 'api/v1/models/agent', formats: [:json], resource: agent end end diff --git a/app/views/api/v1/accounts/inboxes/agent_bot.json.jbuilder b/app/views/api/v1/accounts/inboxes/agent_bot.json.jbuilder index 73b364f04..2ffc9c4d8 100644 --- a/app/views/api/v1/accounts/inboxes/agent_bot.json.jbuilder +++ b/app/views/api/v1/accounts/inboxes/agent_bot.json.jbuilder @@ -1,3 +1,3 @@ json.agent_bot do - json.partial! 'api/v1/models/agent_bot.json.jbuilder', resource: @agent_bot if @agent_bot.present? + json.partial! 'api/v1/models/agent_bot', formats: [:json], resource: @agent_bot if @agent_bot.present? end diff --git a/app/views/api/v1/accounts/inboxes/assignable_agents.json.jbuilder b/app/views/api/v1/accounts/inboxes/assignable_agents.json.jbuilder index af71dea1e..295c5cef9 100644 --- a/app/views/api/v1/accounts/inboxes/assignable_agents.json.jbuilder +++ b/app/views/api/v1/accounts/inboxes/assignable_agents.json.jbuilder @@ -1,5 +1,5 @@ json.payload do json.array! @assignable_agents do |agent| - json.partial! 'api/v1/models/agent.json.jbuilder', resource: agent + json.partial! 'api/v1/models/agent', formats: [:json], resource: agent end end diff --git a/app/views/api/v1/accounts/inboxes/campaigns.json.jbuilder b/app/views/api/v1/accounts/inboxes/campaigns.json.jbuilder index c0e90acd7..c51ec1da5 100644 --- a/app/views/api/v1/accounts/inboxes/campaigns.json.jbuilder +++ b/app/views/api/v1/accounts/inboxes/campaigns.json.jbuilder @@ -1,3 +1,3 @@ json.array! @campaigns do |campaign| - json.partial! 'api/v1/models/campaign.json.jbuilder', resource: campaign + json.partial! 'api/v1/models/campaign', formats: [:json], resource: campaign end diff --git a/app/views/api/v1/accounts/inboxes/create.json.jbuilder b/app/views/api/v1/accounts/inboxes/create.json.jbuilder index 981c1dec0..2ad94ff82 100644 --- a/app/views/api/v1/accounts/inboxes/create.json.jbuilder +++ b/app/views/api/v1/accounts/inboxes/create.json.jbuilder @@ -1 +1 @@ -json.partial! 'api/v1/models/inbox.json.jbuilder', resource: @inbox +json.partial! 'api/v1/models/inbox', formats: [:json], resource: @inbox diff --git a/app/views/api/v1/accounts/inboxes/index.json.jbuilder b/app/views/api/v1/accounts/inboxes/index.json.jbuilder index c01d66d06..501ff97fd 100644 --- a/app/views/api/v1/accounts/inboxes/index.json.jbuilder +++ b/app/views/api/v1/accounts/inboxes/index.json.jbuilder @@ -1,5 +1,5 @@ json.payload do json.array! @inboxes do |inbox| - json.partial! 'api/v1/models/inbox.json.jbuilder', resource: inbox + json.partial! 'api/v1/models/inbox', formats: [:json], resource: inbox end end diff --git a/app/views/api/v1/accounts/inboxes/show.json.jbuilder b/app/views/api/v1/accounts/inboxes/show.json.jbuilder index 981c1dec0..2ad94ff82 100644 --- a/app/views/api/v1/accounts/inboxes/show.json.jbuilder +++ b/app/views/api/v1/accounts/inboxes/show.json.jbuilder @@ -1 +1 @@ -json.partial! 'api/v1/models/inbox.json.jbuilder', resource: @inbox +json.partial! 'api/v1/models/inbox', formats: [:json], resource: @inbox diff --git a/app/views/api/v1/accounts/inboxes/update.json.jbuilder b/app/views/api/v1/accounts/inboxes/update.json.jbuilder index 981c1dec0..2ad94ff82 100644 --- a/app/views/api/v1/accounts/inboxes/update.json.jbuilder +++ b/app/views/api/v1/accounts/inboxes/update.json.jbuilder @@ -1 +1 @@ -json.partial! 'api/v1/models/inbox.json.jbuilder', resource: @inbox +json.partial! 'api/v1/models/inbox', formats: [:json], resource: @inbox diff --git a/app/views/api/v1/accounts/integrations/apps/index.json.jbuilder b/app/views/api/v1/accounts/integrations/apps/index.json.jbuilder index 09acda0cb..4b46a29d9 100644 --- a/app/views/api/v1/accounts/integrations/apps/index.json.jbuilder +++ b/app/views/api/v1/accounts/integrations/apps/index.json.jbuilder @@ -1,5 +1,5 @@ json.payload do json.array! @apps do |app| - json.partial! 'api/v1/models/app.json.jbuilder', resource: app + json.partial! 'api/v1/models/app', formats: [:json], resource: app end end diff --git a/app/views/api/v1/accounts/integrations/apps/show.json.jbuilder b/app/views/api/v1/accounts/integrations/apps/show.json.jbuilder index 442b50a49..427033743 100644 --- a/app/views/api/v1/accounts/integrations/apps/show.json.jbuilder +++ b/app/views/api/v1/accounts/integrations/apps/show.json.jbuilder @@ -1 +1 @@ -json.partial! 'api/v1/models/app.json.jbuilder', resource: @app +json.partial! 'api/v1/models/app', formats: [:json], resource: @app diff --git a/app/views/api/v1/accounts/integrations/hooks/create.json.jbuilder b/app/views/api/v1/accounts/integrations/hooks/create.json.jbuilder index 6362fabd4..6c6f81001 100644 --- a/app/views/api/v1/accounts/integrations/hooks/create.json.jbuilder +++ b/app/views/api/v1/accounts/integrations/hooks/create.json.jbuilder @@ -1 +1 @@ -json.partial! 'api/v1/models/hook.json.jbuilder', resource: @hook +json.partial! 'api/v1/models/hook', formats: [:json], resource: @hook diff --git a/app/views/api/v1/accounts/integrations/hooks/update.json.jbuilder b/app/views/api/v1/accounts/integrations/hooks/update.json.jbuilder index 6362fabd4..6c6f81001 100644 --- a/app/views/api/v1/accounts/integrations/hooks/update.json.jbuilder +++ b/app/views/api/v1/accounts/integrations/hooks/update.json.jbuilder @@ -1 +1 @@ -json.partial! 'api/v1/models/hook.json.jbuilder', resource: @hook +json.partial! 'api/v1/models/hook', formats: [:json], resource: @hook diff --git a/app/views/api/v1/accounts/macros/create.json.jbuilder b/app/views/api/v1/accounts/macros/create.json.jbuilder index 5c8ef098f..acc2a889c 100644 --- a/app/views/api/v1/accounts/macros/create.json.jbuilder +++ b/app/views/api/v1/accounts/macros/create.json.jbuilder @@ -1,3 +1,3 @@ json.payload do - json.partial! 'api/v1/models/macro.json.jbuilder', macro: @macro + json.partial! 'api/v1/models/macro', formats: [:json], macro: @macro end diff --git a/app/views/api/v1/accounts/macros/index.json.jbuilder b/app/views/api/v1/accounts/macros/index.json.jbuilder index 5f5c5a9c2..ab434043b 100644 --- a/app/views/api/v1/accounts/macros/index.json.jbuilder +++ b/app/views/api/v1/accounts/macros/index.json.jbuilder @@ -1,5 +1,5 @@ json.payload do json.array! @macros do |macro| - json.partial! 'api/v1/models/macro.json.jbuilder', macro: macro + json.partial! 'api/v1/models/macro', formats: [:json], macro: macro end end diff --git a/app/views/api/v1/accounts/macros/show.json.jbuilder b/app/views/api/v1/accounts/macros/show.json.jbuilder index 5c8ef098f..acc2a889c 100644 --- a/app/views/api/v1/accounts/macros/show.json.jbuilder +++ b/app/views/api/v1/accounts/macros/show.json.jbuilder @@ -1,3 +1,3 @@ json.payload do - json.partial! 'api/v1/models/macro.json.jbuilder', macro: @macro + json.partial! 'api/v1/models/macro', formats: [:json], macro: @macro end diff --git a/app/views/api/v1/accounts/macros/update.json.jbuilder b/app/views/api/v1/accounts/macros/update.json.jbuilder index 5c8ef098f..acc2a889c 100644 --- a/app/views/api/v1/accounts/macros/update.json.jbuilder +++ b/app/views/api/v1/accounts/macros/update.json.jbuilder @@ -1,3 +1,3 @@ json.payload do - json.partial! 'api/v1/models/macro.json.jbuilder', macro: @macro + json.partial! 'api/v1/models/macro', formats: [:json], macro: @macro end diff --git a/app/views/api/v1/accounts/portals/_portal.json.jbuilder b/app/views/api/v1/accounts/portals/_portal.json.jbuilder index a1a16a647..2d1ffb309 100644 --- a/app/views/api/v1/accounts/portals/_portal.json.jbuilder +++ b/app/views/api/v1/accounts/portals/_portal.json.jbuilder @@ -11,7 +11,7 @@ json.archived portal.archived json.config do json.allowed_locales do json.array! portal.config['allowed_locales'].each do |locale| - json.partial! 'api/v1/models/portal_config.json.jbuilder', locale: locale, portal: portal + json.partial! 'api/v1/models/portal_config', formats: [:json], locale: locale, portal: portal end end end @@ -21,7 +21,7 @@ json.logo portal.file_base_data if portal.logo.present? json.portal_members do if portal.members.any? json.array! portal.members.each do |member| - json.partial! 'api/v1/models/agent.json.jbuilder', resource: member + json.partial! 'api/v1/models/agent', formats: [:json], resource: member end end end diff --git a/app/views/api/v1/accounts/show.json.jbuilder b/app/views/api/v1/accounts/show.json.jbuilder index 2bbdda576..2f1bbcbc6 100644 --- a/app/views/api/v1/accounts/show.json.jbuilder +++ b/app/views/api/v1/accounts/show.json.jbuilder @@ -1,2 +1,2 @@ -json.partial! 'api/v1/models/account.json.jbuilder', resource: @account +json.partial! 'api/v1/models/account', formats: [:json], resource: @account json.latest_chatwoot_version @latest_chatwoot_version diff --git a/app/views/api/v1/accounts/team_members/create.json.jbuilder b/app/views/api/v1/accounts/team_members/create.json.jbuilder index d8c0d209f..a32c982b0 100644 --- a/app/views/api/v1/accounts/team_members/create.json.jbuilder +++ b/app/views/api/v1/accounts/team_members/create.json.jbuilder @@ -1,3 +1,3 @@ json.array! @team_members do |team_member| - json.partial! 'api/v1/models/agent.json.jbuilder', resource: team_member + json.partial! 'api/v1/models/agent', formats: [:json], resource: team_member end diff --git a/app/views/api/v1/accounts/team_members/index.json.jbuilder b/app/views/api/v1/accounts/team_members/index.json.jbuilder index d8c0d209f..a32c982b0 100644 --- a/app/views/api/v1/accounts/team_members/index.json.jbuilder +++ b/app/views/api/v1/accounts/team_members/index.json.jbuilder @@ -1,3 +1,3 @@ json.array! @team_members do |team_member| - json.partial! 'api/v1/models/agent.json.jbuilder', resource: team_member + json.partial! 'api/v1/models/agent', formats: [:json], resource: team_member end diff --git a/app/views/api/v1/accounts/teams/create.json.jbuilder b/app/views/api/v1/accounts/teams/create.json.jbuilder index 31635c33f..a470a1060 100644 --- a/app/views/api/v1/accounts/teams/create.json.jbuilder +++ b/app/views/api/v1/accounts/teams/create.json.jbuilder @@ -1 +1 @@ -json.partial! 'api/v1/models/team.json.jbuilder', resource: @team +json.partial! 'api/v1/models/team', formats: [:json], resource: @team diff --git a/app/views/api/v1/accounts/teams/index.json.jbuilder b/app/views/api/v1/accounts/teams/index.json.jbuilder index 9f7e69be7..c1a6d811d 100644 --- a/app/views/api/v1/accounts/teams/index.json.jbuilder +++ b/app/views/api/v1/accounts/teams/index.json.jbuilder @@ -1,3 +1,3 @@ json.array! @teams do |team| - json.partial! 'api/v1/models/team.json.jbuilder', resource: team + json.partial! 'api/v1/models/team', formats: [:json], resource: team end diff --git a/app/views/api/v1/accounts/teams/show.json.jbuilder b/app/views/api/v1/accounts/teams/show.json.jbuilder index 31635c33f..a470a1060 100644 --- a/app/views/api/v1/accounts/teams/show.json.jbuilder +++ b/app/views/api/v1/accounts/teams/show.json.jbuilder @@ -1 +1 @@ -json.partial! 'api/v1/models/team.json.jbuilder', resource: @team +json.partial! 'api/v1/models/team', formats: [:json], resource: @team diff --git a/app/views/api/v1/accounts/teams/update.json.jbuilder b/app/views/api/v1/accounts/teams/update.json.jbuilder index 31635c33f..a470a1060 100644 --- a/app/views/api/v1/accounts/teams/update.json.jbuilder +++ b/app/views/api/v1/accounts/teams/update.json.jbuilder @@ -1 +1 @@ -json.partial! 'api/v1/models/team.json.jbuilder', resource: @team +json.partial! 'api/v1/models/team', formats: [:json], resource: @team diff --git a/app/views/api/v1/accounts/update.json.jbuilder b/app/views/api/v1/accounts/update.json.jbuilder index c1905c972..bd43a8658 100644 --- a/app/views/api/v1/accounts/update.json.jbuilder +++ b/app/views/api/v1/accounts/update.json.jbuilder @@ -1 +1 @@ -json.partial! 'api/v1/models/account.json.jbuilder', resource: @account +json.partial! 'api/v1/models/account', formats: [:json], resource: @account diff --git a/app/views/api/v1/conversations/partials/_conversation.json.jbuilder b/app/views/api/v1/conversations/partials/_conversation.json.jbuilder index 3c645e093..24ba32a2b 100644 --- a/app/views/api/v1/conversations/partials/_conversation.json.jbuilder +++ b/app/views/api/v1/conversations/partials/_conversation.json.jbuilder @@ -1,16 +1,16 @@ json.meta do json.sender do - json.partial! 'api/v1/models/contact.json.jbuilder', resource: conversation.contact + json.partial! 'api/v1/models/contact', formats: [:json], resource: conversation.contact end json.channel conversation.inbox.try(:channel_type) if conversation.assignee&.account json.assignee do - json.partial! 'api/v1/models/agent.json.jbuilder', resource: conversation.assignee + json.partial! 'api/v1/models/agent', formats: [:json], resource: conversation.assignee end end if conversation.team.present? json.team do - json.partial! 'api/v1/models/team.json.jbuilder', resource: conversation.team + json.partial! 'api/v1/models/team', formats: [:json], resource: conversation.team end end json.hmac_verified conversation.contact_inbox&.hmac_verified diff --git a/app/views/api/v1/models/_app.json.jbuilder b/app/views/api/v1/models/_app.json.jbuilder index a38745e7a..0a05926c4 100644 --- a/app/views/api/v1/models/_app.json.jbuilder +++ b/app/views/api/v1/models/_app.json.jbuilder @@ -6,6 +6,6 @@ json.action resource.action json.button resource.action json.hooks do json.array! @current_account.hooks.where(app_id: resource.id) do |hook| - json.partial! 'api/v1/models/hook.json.jbuilder', resource: hook + json.partial! 'api/v1/models/hook', formats: [:json], resource: hook end end diff --git a/app/views/api/v1/models/_campaign.json.jbuilder b/app/views/api/v1/models/_campaign.json.jbuilder index edafd98f9..8706175df 100644 --- a/app/views/api/v1/models/_campaign.json.jbuilder +++ b/app/views/api/v1/models/_campaign.json.jbuilder @@ -3,10 +3,10 @@ json.title resource.title json.description resource.description json.account_id resource.account_id json.inbox do - json.partial! 'api/v1/models/inbox.json.jbuilder', resource: resource.inbox + json.partial! 'api/v1/models/inbox', formats: [:json], resource: resource.inbox end json.sender do - json.partial! 'api/v1/models/agent.json.jbuilder', resource: resource.sender if resource.sender.present? + json.partial! 'api/v1/models/agent', formats: [:json], resource: resource.sender if resource.sender.present? end json.message resource.message json.campaign_status resource.campaign_status diff --git a/app/views/api/v1/models/_contact.json.jbuilder b/app/views/api/v1/models/_contact.json.jbuilder index a3088f965..c5ce61bb4 100644 --- a/app/views/api/v1/models/_contact.json.jbuilder +++ b/app/views/api/v1/models/_contact.json.jbuilder @@ -14,7 +14,7 @@ json.last_activity_at resource.last_activity_at.to_i if resource[:last_activity_ if defined?(with_contact_inboxes) && with_contact_inboxes.present? json.contact_inboxes do json.array! resource.contact_inboxes do |contact_inbox| - json.partial! 'api/v1/models/contact_inbox.json.jbuilder', resource: contact_inbox + json.partial! 'api/v1/models/contact_inbox', formats: [:json], resource: contact_inbox end end end diff --git a/app/views/api/v1/models/_contact_inbox.json.jbuilder b/app/views/api/v1/models/_contact_inbox.json.jbuilder index b0214d298..6fd480082 100644 --- a/app/views/api/v1/models/_contact_inbox.json.jbuilder +++ b/app/views/api/v1/models/_contact_inbox.json.jbuilder @@ -1,4 +1,4 @@ json.source_id resource.source_id json.inbox do - json.partial! 'api/v1/models/inbox.json.jbuilder', resource: resource.inbox + json.partial! 'api/v1/models/inbox', formats: [:json], resource: resource.inbox end diff --git a/app/views/api/v1/models/_csat_survey_response.json.jbuilder b/app/views/api/v1/models/_csat_survey_response.json.jbuilder index 52515c613..3470c4646 100644 --- a/app/views/api/v1/models/_csat_survey_response.json.jbuilder +++ b/app/views/api/v1/models/_csat_survey_response.json.jbuilder @@ -5,13 +5,13 @@ json.account_id resource.account_id json.message_id resource.message_id if resource.contact json.contact do - json.partial! 'api/v1/models/contact.json.jbuilder', resource: resource.contact + json.partial! 'api/v1/models/contact', formats: [:json], resource: resource.contact end end json.conversation_id resource.conversation.display_id if resource.assigned_agent json.assigned_agent do - json.partial! 'api/v1/models/agent.json.jbuilder', resource: resource.assigned_agent + json.partial! 'api/v1/models/agent', formats: [:json], resource: resource.assigned_agent end end json.created_at resource.created_at.to_i diff --git a/app/views/api/v1/models/_macro.json.jbuilder b/app/views/api/v1/models/_macro.json.jbuilder index f88d5922b..5a13d1ba6 100644 --- a/app/views/api/v1/models/_macro.json.jbuilder +++ b/app/views/api/v1/models/_macro.json.jbuilder @@ -4,13 +4,13 @@ json.visibility macro.visibility if macro.created_by.present? json.created_by do - json.partial! 'api/v1/models/agent.json.jbuilder', resource: macro.created_by + json.partial! 'api/v1/models/agent', formats: [:json], resource: macro.created_by end end if macro.updated_by.present? json.updated_by do - json.partial! 'api/v1/models/agent.json.jbuilder', resource: macro.updated_by + json.partial! 'api/v1/models/agent', formats: [:json], resource: macro.updated_by end end diff --git a/app/views/api/v1/models/_note.json.jbuilder b/app/views/api/v1/models/_note.json.jbuilder index 3640bbc2c..3631022af 100644 --- a/app/views/api/v1/models/_note.json.jbuilder +++ b/app/views/api/v1/models/_note.json.jbuilder @@ -4,7 +4,7 @@ json.account_id json.account_id json.contact_id json.contact_id if resource.user.present? json.user do - json.partial! 'api/v1/models/agent.json.jbuilder', resource: resource.user + json.partial! 'api/v1/models/agent', formats: [:json], resource: resource.user end end json.created_at resource.created_at.to_i diff --git a/app/views/api/v1/profiles/availability.jbuilder b/app/views/api/v1/profiles/availability.jbuilder index 5a6dc2dad..0a4b4f9fa 100644 --- a/app/views/api/v1/profiles/availability.jbuilder +++ b/app/views/api/v1/profiles/availability.jbuilder @@ -1 +1 @@ -json.partial! 'api/v1/models/user.json.jbuilder', resource: @user +json.partial! 'api/v1/models/user', formats: [:json], resource: @user diff --git a/app/views/api/v1/profiles/show.json.jbuilder b/app/views/api/v1/profiles/show.json.jbuilder index 5a6dc2dad..0a4b4f9fa 100644 --- a/app/views/api/v1/profiles/show.json.jbuilder +++ b/app/views/api/v1/profiles/show.json.jbuilder @@ -1 +1 @@ -json.partial! 'api/v1/models/user.json.jbuilder', resource: @user +json.partial! 'api/v1/models/user', formats: [:json], resource: @user diff --git a/app/views/api/v1/profiles/update.json.jbuilder b/app/views/api/v1/profiles/update.json.jbuilder index 5a6dc2dad..0a4b4f9fa 100644 --- a/app/views/api/v1/profiles/update.json.jbuilder +++ b/app/views/api/v1/profiles/update.json.jbuilder @@ -1 +1 @@ -json.partial! 'api/v1/models/user.json.jbuilder', resource: @user +json.partial! 'api/v1/models/user', formats: [:json], resource: @user diff --git a/app/views/api/v1/widget/contacts/show.json.jbuilder b/app/views/api/v1/widget/contacts/show.json.jbuilder index d6228cbfe..2e7a38277 100644 --- a/app/views/api/v1/widget/contacts/show.json.jbuilder +++ b/app/views/api/v1/widget/contacts/show.json.jbuilder @@ -2,3 +2,4 @@ json.id @contact.id json.name @contact.name json.email @contact.email json.phone_number @contact.phone_number +json.identifier @contact.identifier diff --git a/app/views/devise/_auth.json.jbuilder b/app/views/devise/_auth.json.jbuilder index 3c8c11d52..31fb0abb7 100644 --- a/app/views/devise/_auth.json.jbuilder +++ b/app/views/devise/_auth.json.jbuilder @@ -1,3 +1,3 @@ json.data do - json.partial! 'api/v1/models/user.json.jbuilder', resource: resource + json.partial! 'api/v1/models/user', formats: [:json], resource: resource end diff --git a/app/views/devise/token.json.jbuilder b/app/views/devise/token.json.jbuilder index 7de1c51fd..414b295d2 100644 --- a/app/views/devise/token.json.jbuilder +++ b/app/views/devise/token.json.jbuilder @@ -1,6 +1,6 @@ json.payload do json.success true - json.partial! 'auth.json.jbuilder', resource: @resource + json.partial! 'auth', formats: [:json], resource: @resource json.data do json.created_at @resource.created_at end diff --git a/app/views/fields/account_features_field/_form.html.erb b/app/views/fields/account_features_field/_form.html.erb index 823a043a6..c067e81c1 100644 --- a/app/views/fields/account_features_field/_form.html.erb +++ b/app/views/fields/account_features_field/_form.html.erb @@ -4,5 +4,6 @@
    <% field.data.each do |key,val| %> <%= key %>: <%= check_box "enabled_features", "feature_#{key}", { checked: val }, true, false %> +
    <% end %>
    diff --git a/app/views/platform/api/v1/agent_bots/create.json.jbuilder b/app/views/platform/api/v1/agent_bots/create.json.jbuilder index 32123ab29..17143d3e2 100644 --- a/app/views/platform/api/v1/agent_bots/create.json.jbuilder +++ b/app/views/platform/api/v1/agent_bots/create.json.jbuilder @@ -1 +1 @@ -json.partial! 'platform/api/v1/models/agent_bot.json.jbuilder', resource: @resource +json.partial! 'platform/api/v1/models/agent_bot', formats: [:json], resource: @resource diff --git a/app/views/platform/api/v1/agent_bots/index.json.jbuilder b/app/views/platform/api/v1/agent_bots/index.json.jbuilder index 9dd6dcce6..daa54aa2b 100644 --- a/app/views/platform/api/v1/agent_bots/index.json.jbuilder +++ b/app/views/platform/api/v1/agent_bots/index.json.jbuilder @@ -1,3 +1,3 @@ json.array! @resources do |resource| - json.partial! 'platform/api/v1/models/agent_bot.json.jbuilder', resource: resource.permissible + json.partial! 'platform/api/v1/models/agent_bot', formats: [:json], resource: resource.permissible end diff --git a/app/views/platform/api/v1/agent_bots/show.json.jbuilder b/app/views/platform/api/v1/agent_bots/show.json.jbuilder index 32123ab29..17143d3e2 100644 --- a/app/views/platform/api/v1/agent_bots/show.json.jbuilder +++ b/app/views/platform/api/v1/agent_bots/show.json.jbuilder @@ -1 +1 @@ -json.partial! 'platform/api/v1/models/agent_bot.json.jbuilder', resource: @resource +json.partial! 'platform/api/v1/models/agent_bot', formats: [:json], resource: @resource diff --git a/app/views/platform/api/v1/agent_bots/update.json.jbuilder b/app/views/platform/api/v1/agent_bots/update.json.jbuilder index 32123ab29..17143d3e2 100644 --- a/app/views/platform/api/v1/agent_bots/update.json.jbuilder +++ b/app/views/platform/api/v1/agent_bots/update.json.jbuilder @@ -1 +1 @@ -json.partial! 'platform/api/v1/models/agent_bot.json.jbuilder', resource: @resource +json.partial! 'platform/api/v1/models/agent_bot', formats: [:json], resource: @resource diff --git a/app/views/platform/api/v1/users/create.json.jbuilder b/app/views/platform/api/v1/users/create.json.jbuilder index 46d8b6d34..382b0a0e3 100644 --- a/app/views/platform/api/v1/users/create.json.jbuilder +++ b/app/views/platform/api/v1/users/create.json.jbuilder @@ -1 +1 @@ -json.partial! 'platform/api/v1/models/user.json.jbuilder', resource: @resource +json.partial! 'platform/api/v1/models/user', formats: [:json], resource: @resource diff --git a/app/views/platform/api/v1/users/show.json.jbuilder b/app/views/platform/api/v1/users/show.json.jbuilder index 46d8b6d34..382b0a0e3 100644 --- a/app/views/platform/api/v1/users/show.json.jbuilder +++ b/app/views/platform/api/v1/users/show.json.jbuilder @@ -1 +1 @@ -json.partial! 'platform/api/v1/models/user.json.jbuilder', resource: @resource +json.partial! 'platform/api/v1/models/user', formats: [:json], resource: @resource diff --git a/app/views/platform/api/v1/users/update.json.jbuilder b/app/views/platform/api/v1/users/update.json.jbuilder index 46d8b6d34..382b0a0e3 100644 --- a/app/views/platform/api/v1/users/update.json.jbuilder +++ b/app/views/platform/api/v1/users/update.json.jbuilder @@ -1 +1 @@ -json.partial! 'platform/api/v1/models/user.json.jbuilder', resource: @resource +json.partial! 'platform/api/v1/models/user', formats: [:json], resource: @resource diff --git a/app/views/public/api/v1/csat_survey/show.json.jbuilder b/app/views/public/api/v1/csat_survey/show.json.jbuilder index 1ab9e564d..3e5e3e2b9 100644 --- a/app/views/public/api/v1/csat_survey/show.json.jbuilder +++ b/app/views/public/api/v1/csat_survey/show.json.jbuilder @@ -1 +1 @@ -json.partial! 'public/api/v1/models/csat_survey.json.jbuilder', resource: @message +json.partial! 'public/api/v1/models/csat_survey', formats: [:json], resource: @message diff --git a/app/views/public/api/v1/csat_survey/update.json.jbuilder b/app/views/public/api/v1/csat_survey/update.json.jbuilder index 1ab9e564d..3e5e3e2b9 100644 --- a/app/views/public/api/v1/csat_survey/update.json.jbuilder +++ b/app/views/public/api/v1/csat_survey/update.json.jbuilder @@ -1 +1 @@ -json.partial! 'public/api/v1/models/csat_survey.json.jbuilder', resource: @message +json.partial! 'public/api/v1/models/csat_survey', formats: [:json], resource: @message diff --git a/app/views/public/api/v1/inboxes/contacts/create.json.jbuilder b/app/views/public/api/v1/inboxes/contacts/create.json.jbuilder index 4a635bb5a..f3f9124fa 100644 --- a/app/views/public/api/v1/inboxes/contacts/create.json.jbuilder +++ b/app/views/public/api/v1/inboxes/contacts/create.json.jbuilder @@ -1,3 +1,3 @@ json.source_id @contact_inbox.source_id json.pubsub_token @contact_inbox.pubsub_token -json.partial! 'public/api/v1/models/contact.json.jbuilder', resource: @contact_inbox.contact +json.partial! 'public/api/v1/models/contact', resource: @contact_inbox.contact, formats: [:json] diff --git a/app/views/public/api/v1/inboxes/contacts/show.json.jbuilder b/app/views/public/api/v1/inboxes/contacts/show.json.jbuilder index 4a635bb5a..74cab2e16 100644 --- a/app/views/public/api/v1/inboxes/contacts/show.json.jbuilder +++ b/app/views/public/api/v1/inboxes/contacts/show.json.jbuilder @@ -1,3 +1,3 @@ json.source_id @contact_inbox.source_id json.pubsub_token @contact_inbox.pubsub_token -json.partial! 'public/api/v1/models/contact.json.jbuilder', resource: @contact_inbox.contact +json.partial! 'public/api/v1/models/contact', formats: [:json], resource: @contact_inbox.contact diff --git a/app/views/public/api/v1/inboxes/contacts/update.json.jbuilder b/app/views/public/api/v1/inboxes/contacts/update.json.jbuilder index 4a635bb5a..74cab2e16 100644 --- a/app/views/public/api/v1/inboxes/contacts/update.json.jbuilder +++ b/app/views/public/api/v1/inboxes/contacts/update.json.jbuilder @@ -1,3 +1,3 @@ json.source_id @contact_inbox.source_id json.pubsub_token @contact_inbox.pubsub_token -json.partial! 'public/api/v1/models/contact.json.jbuilder', resource: @contact_inbox.contact +json.partial! 'public/api/v1/models/contact', formats: [:json], resource: @contact_inbox.contact diff --git a/app/views/public/api/v1/inboxes/conversations/create.json.jbuilder b/app/views/public/api/v1/inboxes/conversations/create.json.jbuilder index 81d59cde5..1b9588465 100644 --- a/app/views/public/api/v1/inboxes/conversations/create.json.jbuilder +++ b/app/views/public/api/v1/inboxes/conversations/create.json.jbuilder @@ -1 +1 @@ -json.partial! 'public/api/v1/models/conversation.json.jbuilder', resource: @conversation +json.partial! 'public/api/v1/models/conversation', formats: [:json], resource: @conversation diff --git a/app/views/public/api/v1/inboxes/conversations/index.json.jbuilder b/app/views/public/api/v1/inboxes/conversations/index.json.jbuilder index 97942b57b..ac2e88d50 100644 --- a/app/views/public/api/v1/inboxes/conversations/index.json.jbuilder +++ b/app/views/public/api/v1/inboxes/conversations/index.json.jbuilder @@ -1,3 +1,3 @@ json.array! @conversations do |conversation| - json.partial! 'public/api/v1/models/conversation.json.jbuilder', resource: conversation + json.partial! 'public/api/v1/models/conversation', formats: [:json], resource: conversation end diff --git a/app/views/public/api/v1/inboxes/messages/create.json.jbuilder b/app/views/public/api/v1/inboxes/messages/create.json.jbuilder index 8b138e948..e2b85bfc8 100644 --- a/app/views/public/api/v1/inboxes/messages/create.json.jbuilder +++ b/app/views/public/api/v1/inboxes/messages/create.json.jbuilder @@ -1 +1 @@ -json.partial! 'public/api/v1/models/message.json.jbuilder', resource: @message +json.partial! 'public/api/v1/models/message', formats: [:json], resource: @message diff --git a/app/views/public/api/v1/inboxes/messages/index.json.jbuilder b/app/views/public/api/v1/inboxes/messages/index.json.jbuilder index 124c79e2d..af01db421 100644 --- a/app/views/public/api/v1/inboxes/messages/index.json.jbuilder +++ b/app/views/public/api/v1/inboxes/messages/index.json.jbuilder @@ -1,3 +1,3 @@ json.array! @messages do |message| - json.partial! 'public/api/v1/models/message.json.jbuilder', resource: message + json.partial! 'public/api/v1/models/message', formats: [:json], resource: message end diff --git a/app/views/public/api/v1/inboxes/messages/update.json.jbuilder b/app/views/public/api/v1/inboxes/messages/update.json.jbuilder index 8b138e948..e2b85bfc8 100644 --- a/app/views/public/api/v1/inboxes/messages/update.json.jbuilder +++ b/app/views/public/api/v1/inboxes/messages/update.json.jbuilder @@ -1 +1 @@ -json.partial! 'public/api/v1/models/message.json.jbuilder', resource: @message +json.partial! 'public/api/v1/models/message', formats: [:json], resource: @message diff --git a/app/views/public/api/v1/models/_article.json.jbuilder b/app/views/public/api/v1/models/_article.json.jbuilder index 80b254a8a..994c6c029 100644 --- a/app/views/public/api/v1/models/_article.json.jbuilder +++ b/app/views/public/api/v1/models/_article.json.jbuilder @@ -9,7 +9,7 @@ json.last_updated_at article.updated_at if article.portal.present? json.portal do - json.partial! 'api/v1/accounts/portals/portal.json.jbuilder', portal: article.portal + json.partial! 'api/v1/accounts/portals/portal', formats: [:json], portal: article.portal end end @@ -17,14 +17,14 @@ json.views article.views if article.author.present? json.author do - json.partial! 'api/v1/models/agent.json.jbuilder', resource: article.author + json.partial! 'api/v1/models/agent', formats: [:json], resource: article.author end end json.associated_articles do if article.associated_articles.any? json.array! article.associated_articles.each do |associated_article| - json.partial! 'api/v1/accounts/articles/associated_article.json.jbuilder', article: associated_article + json.partial! 'api/v1/accounts/articles/associated_article', formats: [:json], article: associated_article end end end diff --git a/app/views/public/api/v1/models/_associated_article.json.jbuilder b/app/views/public/api/v1/models/_associated_article.json.jbuilder index 02b4dd4db..178d67d24 100644 --- a/app/views/public/api/v1/models/_associated_article.json.jbuilder +++ b/app/views/public/api/v1/models/_associated_article.json.jbuilder @@ -10,6 +10,6 @@ json.views article.views if article.author.present? json.author do - json.partial! 'api/v1/models/agent.json.jbuilder', resource: article.author + json.partial! 'api/v1/models/agent', formats: [:json], resource: article.author end end diff --git a/app/views/public/api/v1/models/_conversation.json.jbuilder b/app/views/public/api/v1/models/_conversation.json.jbuilder index 25d888d3f..793deec48 100644 --- a/app/views/public/api/v1/models/_conversation.json.jbuilder +++ b/app/views/public/api/v1/models/_conversation.json.jbuilder @@ -5,7 +5,7 @@ json.status resource.status json.agent_last_seen_at resource.agent_last_seen_at.to_i json.messages do json.array! resource.messages do |message| - json.partial! 'public/api/v1/models/message.json.jbuilder', resource: message + json.partial! 'public/api/v1/models/message', formats: [:json], resource: message end end json.contact resource.contact diff --git a/app/views/public/api/v1/models/_portal.json.jbuilder b/app/views/public/api/v1/models/_portal.json.jbuilder index 3f0a148fb..a6bff31fd 100644 --- a/app/views/public/api/v1/models/_portal.json.jbuilder +++ b/app/views/public/api/v1/models/_portal.json.jbuilder @@ -8,7 +8,7 @@ json.slug portal.slug json.categories do if portal.categories.any? json.array! portal.categories.each do |category| - json.partial! 'public/api/v1/models/category.json.jbuilder', category: category + json.partial! 'public/api/v1/models/category', formats: [:json], category: category end end end diff --git a/app/views/public/api/v1/portals/articles/show.json.jbuilder b/app/views/public/api/v1/portals/articles/show.json.jbuilder index 16444b425..6967782e2 100644 --- a/app/views/public/api/v1/portals/articles/show.json.jbuilder +++ b/app/views/public/api/v1/portals/articles/show.json.jbuilder @@ -1 +1 @@ -json.partial! 'public/api/v1/models/article.json.jbuilder', article: @article +json.partial! 'public/api/v1/models/article', formats: [:json], article: @article diff --git a/app/views/public/api/v1/portals/categories/show.json.jbuilder b/app/views/public/api/v1/portals/categories/show.json.jbuilder index 6584b0a3d..611862028 100644 --- a/app/views/public/api/v1/portals/categories/show.json.jbuilder +++ b/app/views/public/api/v1/portals/categories/show.json.jbuilder @@ -1 +1 @@ -json.partial! 'public/api/v1/models/category.json.jbuilder', category: @category +json.partial! 'public/api/v1/models/category', formats: [:json], category: @category diff --git a/app/views/public/api/v1/portals/show.json.jbuilder b/app/views/public/api/v1/portals/show.json.jbuilder index 70b9c4f07..ab58f858f 100644 --- a/app/views/public/api/v1/portals/show.json.jbuilder +++ b/app/views/public/api/v1/portals/show.json.jbuilder @@ -1 +1 @@ -json.partial! 'public/api/v1/models/portal.json.jbuilder', portal: @portal +json.partial! 'public/api/v1/models/portal', formats: [:json], portal: @portal diff --git a/config/app.yml b/config/app.yml index d5b65c1fc..518c981ac 100644 --- a/config/app.yml +++ b/config/app.yml @@ -1,5 +1,5 @@ shared: &shared - version: '2.9.1' + version: '2.10.0' development: <<: *shared diff --git a/config/features.yml b/config/features.yml index 6fbb73a78..77d594fcf 100644 --- a/config/features.yml +++ b/config/features.yml @@ -15,3 +15,25 @@ enabled: false - name: help_center enabled: true +- name: agent_bots + enabled: false +- name: macros + enabled: false +- name: agent_management + enabled: true +- name: team_management + enabled: true +- name: inbox_management + enabled: true +- name: labels + enabled: true +- name: custom_attributes + enabled: true +- name: automations + enabled: true +- name: canned_responses + enabled: true +- name: integrations + enabled: true +- name: voice_recorder + enabled: true diff --git a/config/locales/cs.yml b/config/locales/cs.yml index c4d7c54ec..58c3b772e 100644 --- a/config/locales/cs.yml +++ b/config/locales/cs.yml @@ -115,9 +115,9 @@ cs: assigned_with_assignee: "Assigned to %{assignee_name} via %{team_name} by %{user_name}" removed: "Unassigned from %{team_name} by %{user_name}" labels: - added: "%{user_name} added %{labels}" - removed: "%{user_name} removed %{labels}" - muted: "%{user_name} has muted the conversation" + added: "%{user_name} odstranil/a %{labels}" + removed: "%{user_name} odebral/a %{labels}" + muted: "%{user_name} ztlumil/a konverzaci" unmuted: "%{user_name} has unmuted the conversation" templates: greeting_message_body: "%{account_name} typically replies in a few hours." @@ -129,7 +129,7 @@ cs: header: from_with_name: '%{assignee_name} from %{inbox_name} <%{from_email}>' reply_with_name: '%{assignee_name} from %{inbox_name} ' - email_subject: "New messages on this conversation" + email_subject: "Nové zprávy v této konverzaci" transcript_subject: "Přepis konverzace" survey: response: "Please rate this conversation, %{link}" diff --git a/config/locales/da.yml b/config/locales/da.yml index 3e52f7aa1..885bdc6b6 100644 --- a/config/locales/da.yml +++ b/config/locales/da.yml @@ -23,9 +23,9 @@ da: reset_password_failure: Åh nej! Vi kunne ikke finde nogen bruger med den angivne e-mail. errors: validations: - presence: must not be blank + presence: må ikke være tomt webhook: - invalid: Invalid events + invalid: Ugyldige begivenheder signup: disposable_email: Vi tillader ikke engangs e-mails invalid_email: Du har indtastet en ugyldig e-mail @@ -33,78 +33,78 @@ da: failed: Tilmelding mislykkedes data_import: data_type: - invalid: Invalid data type + invalid: Ugyldig datatype contacts: import: - failed: File is blank + failed: Filen er tom email: invalid: Invalid email phone_number: - invalid: should be in e164 format + invalid: skal være i e164 format categories: locale: - unique: should be unique in the category and portal + unique: bør være unik i kategorien og portalen inboxes: imap: - socket_error: Please check the network connection, IMAP address and try again. - no_response_error: Please check the IMAP credentials and try again. - host_unreachable_error: Host unreachable, Please check the IMAP address, IMAP port and try again. - connection_timed_out_error: Connection timed out for %{address}:%{port} - connection_closed_error: Connection closed. + socket_error: Tjek venligst netværksforbindelsen, IMAP-adressen og prøv igen. + no_response_error: Tjek venligst IMAP-legitimationsoplysningerne og prøv igen. + host_unreachable_error: Vært utilgængeligt, tjek venligst IMAP-adressen, IMAP-porten og prøv igen. + connection_timed_out_error: Forbindelsen fik timeout for %{address}:%{port} + connection_closed_error: Forbindelsen er lukket. validations: - name: should not start or end with symbols, and it should not have < > / \ @ characters. + name: bør ikke starte eller slutte med symboler, og det skal ikke have < > / \ @ tegn. reports: - period: Reporting period %{since} to %{until} + period: Rapporteringsperiode %{since} til %{until} agent_csv: - agent_name: Agent name - conversations_count: Conversations count - avg_first_response_time: Avg first response time (Minutes) - avg_resolution_time: Avg resolution time (Minutes) + agent_name: Agentens navn + conversations_count: Samtaler tæller + avg_first_response_time: Gns første svartid (minutter) + avg_resolution_time: Gennemsnitlig afviklingstid (protokol) inbox_csv: - inbox_name: Inbox name - inbox_type: Inbox type - conversations_count: No. of conversations - avg_first_response_time: Avg first response time (Minutes) - avg_resolution_time: Avg resolution time (Minutes) + inbox_name: Indbakkens navn + inbox_type: Indbakke type + conversations_count: Antal samtaler + avg_first_response_time: Gns første svartid (minutter) + avg_resolution_time: Gennemsnitlig afviklingstid (protokol) label_csv: - label_title: Label - conversations_count: No. of conversations - avg_first_response_time: Avg first response time (Minutes) - avg_resolution_time: Avg resolution time (Minutes) + label_title: Etiketter + conversations_count: Antal samtaler + avg_first_response_time: Gns første svartid (minutter) + avg_resolution_time: Gennemsnitlig afviklingstid (protokol) team_csv: - team_name: Team name - conversations_count: Conversations count - avg_first_response_time: Avg first response time (Minutes) - avg_resolution_time: Avg resolution time (Minutes) - default_group_by: day + team_name: Team navn + conversations_count: Samtaler tæller + avg_first_response_time: Gns første svartid (minutter) + avg_resolution_time: Gennemsnitlig afviklingstid (protokol) + default_group_by: dag csat: headers: - contact_name: Contact Name - contact_email_address: Contact Email Address - contact_phone_number: Contact Phone Number - link_to_the_conversation: Link to the conversation + contact_name: Kontakt Navn + contact_email_address: Kontakt E-Mail Adresse + contact_phone_number: Kontakt Telefonnummer + link_to_the_conversation: Link til samtalen agent_name: Agentens Navn - rating: Rating - feedback: Feedback Comment - recorded_at: Recorded date + rating: Bedømmelse + feedback: Feedback Kommentar + recorded_at: Optaget dato notifications: notification_title: - conversation_creation: "[New conversation] - #%{display_id} has been created in %{inbox_name}" - conversation_assignment: "[Assigned to you] - #%{display_id} has been assigned to you" - assigned_conversation_new_message: "[New message] - #%{display_id} %{content}" - conversation_mention: "You have been mentioned in conversation [ID - %{display_id}] by %{name}" + conversation_creation: "[Ny samtale] - #%{display_id} er blevet oprettet i %{inbox_name}" + conversation_assignment: "[Tildelt dig] - #%{display_id} er blevet tildelt dig" + assigned_conversation_new_message: "[Ny besked] - #%{display_id} %{content}" + conversation_mention: "Du er blevet nævnt i samtalen [ID - %{display_id}] af %{name}" conversations: messages: - instagram_story_content: "%{story_sender} mentioned you in the story: " - instagram_deleted_story_content: This story is no longer available. - deleted: This message was deleted + instagram_story_content: "%{story_sender} nævnte dig i historien: " + instagram_deleted_story_content: Denne historie er ikke længere tilgængelig. + deleted: Denne besked blev slettet activity: status: resolved: "Samtalen blev markeret som løst af %{user_name}" - contact_resolved: "Conversation was resolved by %{contact_name}" + contact_resolved: "Samtalen blev løst af %{contact_name}" open: "Samtalen blev genåbnet af %{user_name}" - pending: "Conversation was marked as pending by %{user_name}" - snoozed: "Conversation was snoozed by %{user_name}" + pending: "Samtalen blev markeret som afventende af %{user_name}" + snoozed: "Samtalen blev udskudt af %{user_name}" auto_resolved: "Samtalen blev markeret som løst af systemet på grund af %{duration} dages inaktivitet" assignee: self_assigned: "%{user_name} selv-tildelte denne samtale" @@ -112,8 +112,8 @@ da: removed: "Samtale fjernet tildeling af %{user_name}" team: assigned: "Tildelt %{team_name} af %{user_name}" - assigned_with_assignee: "Assigned to %{assignee_name} via %{team_name} by %{user_name}" - removed: "Unassigned from %{team_name} by %{user_name}" + assigned_with_assignee: "Tildelt %{assignee_name} via %{team_name} af %{user_name}" + removed: "Ikke tildelt fra %{team_name} af %{user_name}" labels: added: "%{user_name} tilføjede %{labels}" removed: "%{user_name} fjernede %{labels}" @@ -123,29 +123,29 @@ da: greeting_message_body: "%{account_name} svarer typisk på et par timer." ways_to_reach_you_message_body: "Giv teamet en måde at kontakte dig på." email_input_box_message_body: "Få besked via e-mail" - csat_input_message_body: "Please rate the conversation" + csat_input_message_body: "Bedøm venligst samtalen" reply: email: header: - from_with_name: '%{assignee_name} from %{inbox_name} <%{from_email}>' - reply_with_name: '%{assignee_name} from %{inbox_name} ' + from_with_name: '%{assignee_name} fra %{inbox_name} <%{from_email}>' + reply_with_name: '%{assignee_name} fra %{inbox_name} ' email_subject: "Nye beskeder i denne samtale" transcript_subject: "Samtaleudskrift" survey: - response: "Please rate this conversation, %{link}" + response: "Bedøm denne samtale, %{link}" contacts: online: - delete: "%{contact_name} is Online, please try again later" + delete: "%{contact_name} er online, prøv igen senere" integration_apps: slack: name: "Slack" - description: "Slack is a chat tool that brings all your communication together in one place. By integrating Slack, you can get notified of all the new conversations in your account right inside your Slack." + description: "Slack er et chatværktøj, der bringer al din kommunikation sammen på ét sted. Ved at integrere Slack, kan du få besked om alle de nye samtaler på din konto lige inde i din Slack." webhooks: name: "Webhooks" - description: "Webhook events provide you the realtime information about what's happening in your account. You can make use of the webhooks to communicate the events to your favourite apps like Slack or Github. Click on Configure to set up your webhooks." + description: "Webhook begivenheder giver dig realtime oplysninger om, hvad der sker på din konto. Du kan gøre brug af webhooks til at kommunikere begivenhederne til dine foretrukne apps som Slack eller Github. Klik på Konfigurer for at opsætte dine webhooks." dialogflow: name: "Dialogflow" - description: "Build chatbots using Dialogflow and connect them to your inbox quickly. Let the bots handle the queries before handing them off to a customer service agent." + description: "Byg chatbots ved hjælp af Dialogflow og tilslut dem hurtigt til din indbakke. Lad robotterne håndtere forespørgslerne, før de afleverer dem til en kundeservice agent." fullcontact: - name: "Fullcontact" - description: "FullContact integration helps to enrich visitor profiles. Identify the users as soon as they share their email address and offer them tailored customer service. Connect your FullContact to your account by sharing the FullContact API Key." + name: "Fuldkontakt" + description: "FullContact integration hjælper med at berige besøgende profiler. Identificere brugerne, så snart de deler deres e-mail-adresse og tilbyde dem skræddersyet kundeservice. Tilslut din FullContact til din konto ved at dele FullContact API-nøglen." diff --git a/config/locales/devise.lt.yml b/config/locales/devise.lt.yml new file mode 100644 index 000000000..15c8151fc --- /dev/null +++ b/config/locales/devise.lt.yml @@ -0,0 +1,63 @@ +#Additional translations at https://github.com/plataformatec/devise/wiki/I18n +lt: + devise: + confirmations: + confirmed: "Your email address has been successfully confirmed." + send_instructions: "You will receive an email with instructions for how to confirm your email address in a few minutes." + send_paranoid_instructions: "If your email address exists in our database, you will receive an email with instructions for how to confirm your email address in a few minutes." + failure: + already_authenticated: "You are already signed in." + inactive: "Your account is not activated yet." + invalid: "Invalid %{authentication_keys}/password or account is not verified yet." + locked: "Your account is locked." + last_attempt: "You have one more attempt before your account is locked." + not_found_in_database: "Invalid %{authentication_keys} or password." + timeout: "Your session expired. Please sign in again to continue." + unauthenticated: "You need to sign in or sign up before continuing." + unconfirmed: "You have to confirm your email address before continuing." + mailer: + confirmation_instructions: + subject: "Confirmation Instructions" + reset_password_instructions: + subject: "Reset password instructions" + unlock_instructions: + subject: "Unlock instructions" + password_change: + subject: "Password Changed" + omniauth_callbacks: + failure: "Could not authenticate you from %{kind} because \"%{reason}\"." + success: "Successfully authenticated from %{kind} account." + passwords: + no_token: "You can't access this page without coming from a password reset email. If you do come from a password reset email, please make sure you used the full URL provided." + send_instructions: "You will receive an email with instructions on how to reset your password in a few minutes." + send_paranoid_instructions: "If your email address exists in our database, you will receive a password recovery link at your email address in a few minutes." + updated: "Your password has been changed successfully. You are now signed in." + updated_not_active: "Your password has been changed successfully." + registrations: + destroyed: "Bye! Your account has been successfully cancelled. We hope to see you again soon." + signed_up: "Welcome! You have signed up successfully." + signed_up_but_inactive: "You have signed up successfully. However, we could not sign you in because your account is not yet activated." + signed_up_but_locked: "You have signed up successfully. However, we could not sign you in because your account is locked." + signed_up_but_unconfirmed: "A message with a confirmation link has been sent to your email address. Please follow the link to activate your account." + update_needs_confirmation: "You updated your account successfully, but we need to verify your new email address. Please check your email and follow the confirm link to confirm your new email address." + updated: "Your account has been updated successfully." + sessions: + signed_in: "Signed in successfully." + signed_out: "Signed out successfully." + already_signed_out: "Signed out successfully." + unlocks: + send_instructions: "You will receive an email with instructions for how to unlock your account in a few minutes." + send_paranoid_instructions: "If your account exists, you will receive an email with instructions for how to unlock it in a few minutes." + unlocked: "Your account has been unlocked successfully. Please sign in to continue." + errors: + messages: + already_confirmed: "was already confirmed, please try signing in" + confirmation_period_expired: "needs to be confirmed within %{period}, please request a new one" + expired: "has expired, please request a new one" + not_found: "not found" + not_locked: "was not locked" + not_saved: + one: "1 error prohibited this %{resource} from being saved:" + few: "%{count} errors prohibited this %{resource} from being saved:" + many: "%{count} errors prohibited this %{resource} from being saved:" + other: "%{count} errors prohibited this %{resource} from being saved:" diff --git a/config/locales/devise.lv.yml b/config/locales/devise.lv.yml index dc234073b..c336f798f 100644 --- a/config/locales/devise.lv.yml +++ b/config/locales/devise.lv.yml @@ -2,61 +2,61 @@ lv: devise: confirmations: - confirmed: "Your email address has been successfully confirmed." - send_instructions: "You will receive an email with instructions for how to confirm your email address in a few minutes." - send_paranoid_instructions: "If your email address exists in our database, you will receive an email with instructions for how to confirm your email address in a few minutes." + confirmed: "Jūsu e-pasta adrese ir veiksmīgi apstiprināta." + send_instructions: "Dažu minūšu laikā jūs saņemsit e-pasta ziņojumu ar norādījumiem, kā apstiprināt savu e-pasta adresi." + send_paranoid_instructions: "Ja jūsu e-pasta adrese pastāv mūsu datubāzē, dažu minūšu laikā jūs saņemsit e-pasta ziņojumu ar norādījumiem, kā apstiprināt savu e-pasta adresi." failure: - already_authenticated: "You are already signed in." - inactive: "Your account is not activated yet." - invalid: "Invalid %{authentication_keys}/password or account is not verified yet." - locked: "Your account is locked." - last_attempt: "You have one more attempt before your account is locked." - not_found_in_database: "Invalid %{authentication_keys} or password." - timeout: "Your session expired. Please sign in again to continue." - unauthenticated: "You need to sign in or sign up before continuing." - unconfirmed: "You have to confirm your email address before continuing." + already_authenticated: "Jūs jau esat pierakstījies." + inactive: "Jūsu konts vēl nav aktivizēts." + invalid: "Nederīga %{authentication_keys}/parole, vai konts vēl nav verificēts." + locked: "Jūsu konts ir bloķēts." + last_attempt: "Jums ir vēl viens mēģinājums, pirms jūsu konts tiek bloķēts." + not_found_in_database: "Nederīga %{authentication_keys} vai parole." + timeout: "Jūsu sesijai beidzās derīguma termiņš. Lūdzu, pierakstieties vēlreiz, lai turpinātu." + unauthenticated: "Pirms turpināt, jums ir jāpierakstās vai jāreģistrējas." + unconfirmed: "Pirms turpināt, jums ir jāapstiprina sava e-pasta adrese." mailer: confirmation_instructions: - subject: "Confirmation Instructions" + subject: "Apstiprināšanas Instrukcijas" reset_password_instructions: - subject: "Reset password instructions" + subject: "Paroles atiestatīšanas instrukcijas" unlock_instructions: - subject: "Unlock instructions" + subject: "Atbloķēšanas instrukcijas" password_change: - subject: "Password Changed" + subject: "Parole nomainīta" omniauth_callbacks: - failure: "Could not authenticate you from %{kind} because \"%{reason}\"." - success: "Successfully authenticated from %{kind} account." + failure: "Nevarēja jūs autentificēt no %{kind} jo \"%{reason}\"." + success: "Veiksmīgi autentificēts no %{kind} konta." passwords: - no_token: "You can't access this page without coming from a password reset email. If you do come from a password reset email, please make sure you used the full URL provided." - send_instructions: "You will receive an email with instructions on how to reset your password in a few minutes." - send_paranoid_instructions: "If your email address exists in our database, you will receive a password recovery link at your email address in a few minutes." - updated: "Your password has been changed successfully. You are now signed in." - updated_not_active: "Your password has been changed successfully." + no_token: "Šai lapai nevar piekļūt, ja neesat saņēmis paroles atiestatīšanas e-pasta ziņojumu. Ja esat saņēmis paroles atiestatīšanas e-pasta ziņojumu, lūdzu, pārliecinieties, vai esat izmantojis pilnu norādīto URL." + send_instructions: "Dažu minūšu laikā jūs saņemsit e-pasta ziņojumu ar norādījumiem, kā atiestatīt paroli." + send_paranoid_instructions: "Ja jūsu e-pasta adrese pastāv mūsu datubāzē, pēc dažām minūtēm uz jūsu e-pasta adresi saņemsit paroles atgūšanas saiti." + updated: "Jūsu parole ir veiksmīgi nomainīta. Tagad Jūs esat pierakstījies." + updated_not_active: "Jūsu parole ir veiksmīgi nomainīta." registrations: - destroyed: "Bye! Your account has been successfully cancelled. We hope to see you again soon." - signed_up: "Welcome! You have signed up successfully." - signed_up_but_inactive: "You have signed up successfully. However, we could not sign you in because your account is not yet activated." - signed_up_but_locked: "You have signed up successfully. However, we could not sign you in because your account is locked." - signed_up_but_unconfirmed: "A message with a confirmation link has been sent to your email address. Please follow the link to activate your account." - update_needs_confirmation: "You updated your account successfully, but we need to verify your new email address. Please check your email and follow the confirm link to confirm your new email address." - updated: "Your account has been updated successfully." + destroyed: "Uz redzēšanos! Jūsu konts ir veiksmīgi atcelts. Mēs ceram, ka drīz Jūs atkal redzēsim." + signed_up: "Laipni lūdzam! Jūs esat veiksmīgi piereģistrējies." + signed_up_but_inactive: "Jūs esat veiksmīgi piereģistrējies. Tomēr, mēs nevarējām Jūs pierakstīt, jo Jūsu konts vēl nav aktivizēts." + signed_up_but_locked: "Jūs esat veiksmīgi piereģistrējies. Tomēr, mēs nevarējām Jūs pierakstīt, jo Jūsu konts ir bloķēts." + signed_up_but_unconfirmed: "Uz jūsu e -pasta adresi ir nosūtīts ziņojums ar apstiprinājuma saiti. Lūdzu, atveriet saiti, lai aktivizētu savu kontu." + update_needs_confirmation: "Jūs veiksmīgi atjauninājāt savu kontu un mums ir jāpārbauda Jūsu jaunā e -pasta adrese. Lūdzu, pārbaudiet savu e -pastu un atveriet apstiprināšanas saiti, lai apstiprinātu jauno e-pasta adresi." + updated: "Jūsu konts ir veiksmīgi atjaunināts." sessions: - signed_in: "Signed in successfully." - signed_out: "Signed out successfully." - already_signed_out: "Signed out successfully." + signed_in: "Pierakstīšanās veiksmīga." + signed_out: "Izrakstīšanās veiksmīga." + already_signed_out: "Izrakstīšanās veiksmīga." unlocks: - send_instructions: "You will receive an email with instructions for how to unlock your account in a few minutes." - send_paranoid_instructions: "If your account exists, you will receive an email with instructions for how to unlock it in a few minutes." - unlocked: "Your account has been unlocked successfully. Please sign in to continue." + send_instructions: "Dažu minūšu laikā Jūs saņemsit e-pastu ar norādījumiem kā atbloķēt kontu." + send_paranoid_instructions: "Ja konts pastāv, Jūs dažu minūšu laikā saņemsit e-pastu ar norādījumiem kā to atbloķēt." + unlocked: "Jūsu konts ir veiksmīgi atbloķēts. Lūdzu, pierakstieties, lai turpinātu." errors: messages: - already_confirmed: "was already confirmed, please try signing in" - confirmation_period_expired: "needs to be confirmed within %{period}, please request a new one" - expired: "has expired, please request a new one" - not_found: "not found" - not_locked: "was not locked" + already_confirmed: "jau bija apstiprināts. Lūdzu, mēģiniet pierakstīties" + confirmation_period_expired: "ir jāapstiprina %{period} laikā. Lūdzu pieprasiet jaunu" + expired: "ir beidzies derīguma termiņš. Lūdzu, pieprasiet jaunu" + not_found: "nav atrasts" + not_locked: "nebija bloķēts" not_saved: - zero: "%{count} errors prohibited this %{resource} from being saved:" - one: "1 error prohibited this %{resource} from being saved:" - other: "%{count} errors prohibited this %{resource} from being saved:" + zero: "%{count} kļūdas neļāva saglabāt šo %{resource}:" + one: "1 kļūda neļāva saglabāt šo %{resource}:" + other: "%{count} kļūdas neļāva saglabāt šo %{resource}:" diff --git a/config/locales/el.yml b/config/locales/el.yml index df0cac548..d53161c16 100644 --- a/config/locales/el.yml +++ b/config/locales/el.yml @@ -23,7 +23,7 @@ el: reset_password_failure: Ωχ όχι! Δεν υπάρχει κάποιος χρήστης με το συγκεκριμένο email. errors: validations: - presence: must not be blank + presence: δεν πρέπει να είναι κενό webhook: invalid: Μη έγκυρα συμβάντα signup: @@ -33,17 +33,17 @@ el: failed: Η εγγραφή απέτυχε data_import: data_type: - invalid: Invalid data type + invalid: Μη έγκυρος τύπος δεδομένων contacts: import: failed: Το αρχείο είναι κενό email: - invalid: Invalid email + invalid: Ακατάλληλο email phone_number: - invalid: should be in e164 format + invalid: πρέπει να είναι σε μορφή e164 categories: locale: - unique: should be unique in the category and portal + unique: πρέπει να είναι μοναδικό στην κατηγορία και την πύλη inboxes: imap: socket_error: Παρακαλώ ελέγξτε τη σύνδεση δικτύου, τη διεύθυνση IMAP και προσπαθήστε ξανά. diff --git a/config/locales/es.yml b/config/locales/es.yml index 557b15e1f..8ab172407 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -23,7 +23,7 @@ es: reset_password_failure: '¡Uh ho! No hemos podido encontrar ningún usuario con el correo electrónico especificado.' errors: validations: - presence: must not be blank + presence: no debe estar en blanco webhook: invalid: Eventos inválidos signup: @@ -33,17 +33,17 @@ es: failed: Registro fallido data_import: data_type: - invalid: Invalid data type + invalid: Tipo de datos no válido contacts: import: failed: Archivo está en blanco email: - invalid: Invalid email + invalid: Email inválido phone_number: - invalid: should be in e164 format + invalid: debe estar en formato e164 categories: locale: - unique: should be unique in the category and portal + unique: debe ser único en la categoría y el portal inboxes: imap: socket_error: Verifique la conexión de red, la dirección IMAP y vuelva a intentarlo. @@ -52,7 +52,7 @@ es: connection_timed_out_error: Se agotó el tiempo de conexión para %{address}:%{port} connection_closed_error: Conexión cerrada. validations: - name: should not start or end with symbols, and it should not have < > / \ @ characters. + name: no debe comenzar ni terminar con símbolos, y no debe tener caracteres < > / \ @. reports: period: Reportando el periodo desde %{since} hasta %{until} agent_csv: @@ -96,7 +96,7 @@ es: conversations: messages: instagram_story_content: "%{story_sender} te mencionó en la historia: " - instagram_deleted_story_content: This story is no longer available. + instagram_deleted_story_content: Esta historia ya no está disponible. deleted: Este mensaje se ha eliminado activity: status: diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 125460a76..0187c3ed6 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -23,9 +23,9 @@ fr: reset_password_failure: Oh oh ! Nous n'avons trouvé aucun utilisateur avec le courriel spécifié. errors: validations: - presence: must not be blank + presence: Ne peut être vide webhook: - invalid: Invalid events + invalid: Événements non valides signup: disposable_email: Nous n'autorisons pas les courriels jetables invalid_email: Vous avez entré un courriel non valide @@ -33,21 +33,21 @@ fr: failed: L'inscription a échoué data_import: data_type: - invalid: Invalid data type + invalid: Type de données incorrect contacts: import: failed: Le fichier est vide email: - invalid: Invalid email + invalid: Email non valide phone_number: - invalid: should be in e164 format + invalid: Doit être au format e164 categories: locale: - unique: should be unique in the category and portal + unique: Doit être unique dans la catégorie et le portail inboxes: imap: - socket_error: Please check the network connection, IMAP address and try again. - no_response_error: Please check the IMAP credentials and try again. + socket_error: Veuillez vérifier la connexion, l'adresse IMAP et réessayez. + no_response_error: Veuillez vérifier les identifiants IMAP et réessayez. host_unreachable_error: Host unreachable, Please check the IMAP address, IMAP port and try again. connection_timed_out_error: Connection timed out for %{address}:%{port} connection_closed_error: Connection closed. diff --git a/config/locales/lt.yml b/config/locales/lt.yml new file mode 100644 index 000000000..525f40f92 --- /dev/null +++ b/config/locales/lt.yml @@ -0,0 +1,151 @@ +#Files in the config/locales directory are used for internationalization +#and are automatically loaded by Rails. If you want to use locales other +#than English, add the necessary files in this directory. +#To use the locales, use `I18n.t`: +#I18n.t 'hello' +#In views, this is aliased to just `t`: +#<%= t('hello') %> +#To use a different locale, set it with `I18n.locale`: +#I18n.locale = :es +#This would use the information in config/locales/es.yml. +#The following keys must be escaped otherwise they will not be retrieved by +#the default I18n backend: +#true, false, on, off, yes, no +#Instead, surround them with single quotes. +#en: +#'true': 'foo' +#To learn more, please read the Rails Internationalization guide +#available at https://guides.rubyonrails.org/i18n.html. +lt: + hello: "Hello world" + messages: + reset_password_success: Woot! Request for password reset is successful. Check your mail for instructions. + reset_password_failure: Uh ho! We could not find any user with the specified email. + errors: + validations: + presence: must not be blank + webhook: + invalid: Invalid events + signup: + disposable_email: We do not allow disposable emails + invalid_email: You have entered an invalid email + email_already_exists: "You have already signed up for an account with %{email}" + failed: Signup failed + data_import: + data_type: + invalid: Invalid data type + contacts: + import: + failed: File is blank + email: + invalid: Invalid email + phone_number: + invalid: should be in e164 format + categories: + locale: + unique: should be unique in the category and portal + inboxes: + imap: + socket_error: Please check the network connection, IMAP address and try again. + no_response_error: Please check the IMAP credentials and try again. + host_unreachable_error: Host unreachable, Please check the IMAP address, IMAP port and try again. + connection_timed_out_error: Connection timed out for %{address}:%{port} + connection_closed_error: Connection closed. + validations: + name: should not start or end with symbols, and it should not have < > / \ @ characters. + reports: + period: Reporting period %{since} to %{until} + agent_csv: + agent_name: Agent name + conversations_count: Conversations count + avg_first_response_time: Avg first response time (Minutes) + avg_resolution_time: Avg resolution time (Minutes) + inbox_csv: + inbox_name: Inbox name + inbox_type: Inbox type + conversations_count: No. of conversations + avg_first_response_time: Avg first response time (Minutes) + avg_resolution_time: Avg resolution time (Minutes) + label_csv: + label_title: Label + conversations_count: No. of conversations + avg_first_response_time: Avg first response time (Minutes) + avg_resolution_time: Avg resolution time (Minutes) + team_csv: + team_name: Team name + conversations_count: Conversations count + avg_first_response_time: Avg first response time (Minutes) + avg_resolution_time: Avg resolution time (Minutes) + default_group_by: day + csat: + headers: + contact_name: Contact Name + contact_email_address: Contact Email Address + contact_phone_number: Contact Phone Number + link_to_the_conversation: Link to the conversation + agent_name: Agent Name + rating: Rating + feedback: Feedback Comment + recorded_at: Recorded date + notifications: + notification_title: + conversation_creation: "[New conversation] - #%{display_id} has been created in %{inbox_name}" + conversation_assignment: "[Assigned to you] - #%{display_id} has been assigned to you" + assigned_conversation_new_message: "[New message] - #%{display_id} %{content}" + conversation_mention: "You have been mentioned in conversation [ID - %{display_id}] by %{name}" + conversations: + messages: + instagram_story_content: "%{story_sender} mentioned you in the story: " + instagram_deleted_story_content: This story is no longer available. + deleted: This message was deleted + activity: + status: + resolved: "Conversation was marked resolved by %{user_name}" + contact_resolved: "Conversation was resolved by %{contact_name}" + open: "Conversation was reopened by %{user_name}" + pending: "Conversation was marked as pending by %{user_name}" + snoozed: "Conversation was snoozed by %{user_name}" + auto_resolved: "Conversation was marked resolved by system due to %{duration} days of inactivity" + assignee: + self_assigned: "%{user_name} self-assigned this conversation" + assigned: "Assigned to %{assignee_name} by %{user_name}" + removed: "Conversation unassigned by %{user_name}" + team: + assigned: "Assigned to %{team_name} by %{user_name}" + assigned_with_assignee: "Assigned to %{assignee_name} via %{team_name} by %{user_name}" + removed: "Unassigned from %{team_name} by %{user_name}" + labels: + added: "%{user_name} added %{labels}" + removed: "%{user_name} removed %{labels}" + muted: "%{user_name} has muted the conversation" + unmuted: "%{user_name} has unmuted the conversation" + templates: + greeting_message_body: "%{account_name} typically replies in a few hours." + ways_to_reach_you_message_body: "Give the team a way to reach you." + email_input_box_message_body: "Get notified by email" + csat_input_message_body: "Please rate the conversation" + reply: + email: + header: + from_with_name: '%{assignee_name} from %{inbox_name} <%{from_email}>' + reply_with_name: '%{assignee_name} from %{inbox_name} ' + email_subject: "New messages on this conversation" + transcript_subject: "Conversation Transcript" + survey: + response: "Please rate this conversation, %{link}" + contacts: + online: + delete: "%{contact_name} is Online, please try again later" + integration_apps: + slack: + name: "Slack" + description: "Slack is a chat tool that brings all your communication together in one place. By integrating Slack, you can get notified of all the new conversations in your account right inside your Slack." + webhooks: + name: "Webhooks" + description: "Webhook events provide you the realtime information about what's happening in your account. You can make use of the webhooks to communicate the events to your favourite apps like Slack or Github. Click on Configure to set up your webhooks." + dialogflow: + name: "Dialogflow" + description: "Build chatbots using Dialogflow and connect them to your inbox quickly. Let the bots handle the queries before handing them off to a customer service agent." + fullcontact: + name: "Fullcontact" + description: "FullContact integration helps to enrich visitor profiles. Identify the users as soon as they share their email address and offer them tailored customer service. Connect your FullContact to your account by sharing the FullContact API Key." diff --git a/config/locales/lv.yml b/config/locales/lv.yml index afaf15682..41a792dc3 100644 --- a/config/locales/lv.yml +++ b/config/locales/lv.yml @@ -17,135 +17,135 @@ #To learn more, please read the Rails Internationalization guide #available at https://guides.rubyonrails.org/i18n.html. lv: - hello: "Hello world" + hello: "Sveika pasaule" messages: - reset_password_success: Woot! Request for password reset is successful. Check your mail for instructions. - reset_password_failure: Uh ho! We could not find any user with the specified email. + reset_password_success: Urā! Paroles atiestatīšanas pieprasījums ir veiksmīgs. Pārbaudiet savu e-pastu, lai iegūtu norādījumus. + reset_password_failure: Ak, vai! Mēs nevarējām atrast nevienu lietotāju ar norādīto e -pastu. errors: validations: - presence: must not be blank + presence: nedrīkst būt tukšs webhook: - invalid: Invalid events + invalid: Nederīgi notikumi signup: - disposable_email: We do not allow disposable emails - invalid_email: You have entered an invalid email - email_already_exists: "You have already signed up for an account with %{email}" - failed: Signup failed + disposable_email: Mēs nepieļaujam vienreizējās lietošanas e-pasta adreses + invalid_email: Jūs esat ievadījis nederīgu e-pasta adresi + email_already_exists: "Jūs jau esat reģistrējis kontu ar %{email}" + failed: Reģistrēšanās neizdevās data_import: data_type: - invalid: Invalid data type + invalid: Nederīgs datu tips contacts: import: - failed: File is blank + failed: Fails ir tukšs email: - invalid: Invalid email + invalid: Nederīga e-pasta adrese phone_number: - invalid: should be in e164 format + invalid: vajadzētu būt E.164 formātā categories: locale: - unique: should be unique in the category and portal + unique: vajadzētu būt unikālai, kategorijā un portālā inboxes: imap: - socket_error: Please check the network connection, IMAP address and try again. - no_response_error: Please check the IMAP credentials and try again. - host_unreachable_error: Host unreachable, Please check the IMAP address, IMAP port and try again. - connection_timed_out_error: Connection timed out for %{address}:%{port} - connection_closed_error: Connection closed. + socket_error: Lūdzu, pārbaudiet tīkla savienojumu, IMAP adresi un mēģiniet vēlreiz. + no_response_error: Lūdzu, pārbaudiet IMAP akreditācijas datus un mēģiniet vēlreiz. + host_unreachable_error: Resursdators nav pieejams. Lūdzu, pārbaudiet IMAP adresi, IMAP portu un mēģiniet vēlreiz. + connection_timed_out_error: Savienojumam %{address}:%{port} iestājās taimauts + connection_closed_error: Savienojums slēgts. validations: - name: should not start or end with symbols, and it should not have < > / \ @ characters. + name: nevajadzētu sākties vai beigties ar simboliem, un nevajadzētu saturēt <> / \ @ rakstzīmes. reports: - period: Reporting period %{since} to %{until} + period: Ziņošanas periods %{since} līdz %{until} agent_csv: - agent_name: Agent name - conversations_count: Conversations count - avg_first_response_time: Avg first response time (Minutes) - avg_resolution_time: Avg resolution time (Minutes) + agent_name: Aģenta vārds + conversations_count: Sarunu skaits + avg_first_response_time: Vidējais pirmās reakcijas laiks (Minūtes) + avg_resolution_time: Vidējais atrisināšanas laiks (Minūtes) inbox_csv: - inbox_name: Inbox name - inbox_type: Inbox type - conversations_count: No. of conversations - avg_first_response_time: Avg first response time (Minutes) - avg_resolution_time: Avg resolution time (Minutes) + inbox_name: Iesūtnes nosaukums + inbox_type: Iesūtnes tips + conversations_count: Sarunu skaits + avg_first_response_time: Vidējais pirmās reakcijas laiks (Minūtes) + avg_resolution_time: Vidējais atrisināšanas laiks (Minūtes) label_csv: - label_title: Label - conversations_count: No. of conversations - avg_first_response_time: Avg first response time (Minutes) - avg_resolution_time: Avg resolution time (Minutes) + label_title: Etiķete + conversations_count: Sarunu skaits + avg_first_response_time: Vidējais pirmās atbildes laiks (Minūtes) + avg_resolution_time: Vidējais atrisināšanas laiks (minūtes) team_csv: - team_name: Team name - conversations_count: Conversations count - avg_first_response_time: Avg first response time (Minutes) - avg_resolution_time: Avg resolution time (Minutes) - default_group_by: day + team_name: Komandas nosaukums + conversations_count: Sarunu skaits + avg_first_response_time: Vidējais pirmās atbildes laiks (Minūtes) + avg_resolution_time: Vidējais atrisinājuma laiks (Minūtes) + default_group_by: diena csat: headers: - contact_name: Contact Name - contact_email_address: Contact Email Address - contact_phone_number: Contact Phone Number - link_to_the_conversation: Link to the conversation - agent_name: Agent Name - rating: Rating - feedback: Feedback Comment - recorded_at: Recorded date + contact_name: Kontaktpersonas Vārds + contact_email_address: Kontaktpersonas E-pasta Adrese + contact_phone_number: Kontaktpersonas Tālruņa Numurs + link_to_the_conversation: Saite uz sarunu + agent_name: Aģenta Vārds + rating: Vērtējums + feedback: Atsauksmes Komentārs + recorded_at: Reģistrētais datums notifications: notification_title: - conversation_creation: "[New conversation] - #%{display_id} has been created in %{inbox_name}" - conversation_assignment: "[Assigned to you] - #%{display_id} has been assigned to you" - assigned_conversation_new_message: "[New message] - #%{display_id} %{content}" - conversation_mention: "You have been mentioned in conversation [ID - %{display_id}] by %{name}" + conversation_creation: "[Jauna saruna] - #%{display_id} ir izveidota iesūtnē %{inbox_name}" + conversation_assignment: "[Piešķirts Jums] - Jums ir piešķirts #%{display_id}" + assigned_conversation_new_message: "[Jauns ziņojums] - #%{display_id} %{content}" + conversation_mention: "Jūs pieminēja sarunā [ID - %{display_id}] kā %{name}" conversations: messages: - instagram_story_content: "%{story_sender} mentioned you in the story: " - instagram_deleted_story_content: This story is no longer available. - deleted: This message was deleted + instagram_story_content: "%{story_sender} pieminēja jūs stāstā: " + instagram_deleted_story_content: Šis stāsts vairs nav pieejams. + deleted: Šis ziņojums ir izdzēsts activity: status: - resolved: "Conversation was marked resolved by %{user_name}" - contact_resolved: "Conversation was resolved by %{contact_name}" - open: "Conversation was reopened by %{user_name}" - pending: "Conversation was marked as pending by %{user_name}" - snoozed: "Conversation was snoozed by %{user_name}" - auto_resolved: "Conversation was marked resolved by system due to %{duration} days of inactivity" + resolved: "%{user_name} sarunu atzīmēja kā atrisinātu" + contact_resolved: "%{contact_name} atrisināja sarunu" + open: "%{user_name} atkārtoti atvēra sarunu" + pending: "%{user_name} sarunu atzīmēja kā neapstiprinātu" + snoozed: "%{user_name} atlika sarunu" + auto_resolved: "Sistēma sarunu atzīmēja kā atrisinātu %{duration} dienu neaktivitātes dēļ" assignee: - self_assigned: "%{user_name} self-assigned this conversation" - assigned: "Assigned to %{assignee_name} by %{user_name}" - removed: "Conversation unassigned by %{user_name}" + self_assigned: "%{user_name} sev piešķīra šo sarunu" + assigned: "%{user_name} piešķīra sarunu %{assignee_name}" + removed: "%{user_name} noņēma piešķiršanu" team: - assigned: "Assigned to %{team_name} by %{user_name}" - assigned_with_assignee: "Assigned to %{assignee_name} via %{team_name} by %{user_name}" - removed: "Unassigned from %{team_name} by %{user_name}" + assigned: "%{user_name} piešķīra sarunu %{team_name}" + assigned_with_assignee: "%{user_name} caur %{team_name} piešķīra sarunu %{assignee_name}" + removed: "%{user_name} noņēma piešķiršanu %{team_name}" labels: - added: "%{user_name} added %{labels}" - removed: "%{user_name} removed %{labels}" - muted: "%{user_name} has muted the conversation" - unmuted: "%{user_name} has unmuted the conversation" + added: "%{user_name} pievienoja %{labels}" + removed: "%{user_name} noņēma %{labels}" + muted: "%{user_name} izslēdza sarunu" + unmuted: "%{user_name} ieslēdza sarunu" templates: - greeting_message_body: "%{account_name} typically replies in a few hours." - ways_to_reach_you_message_body: "Give the team a way to reach you." - email_input_box_message_body: "Get notified by email" - csat_input_message_body: "Please rate the conversation" + greeting_message_body: "%{account_name} parasti atbild dažu stundu laikā." + ways_to_reach_you_message_body: "Dodiet komandai iespēju ar jums sazināties." + email_input_box_message_body: "Saņemiet paziņojumus pa e-pastu" + csat_input_message_body: "Lūdzu, novērtējiet sarunu" reply: email: header: - from_with_name: '%{assignee_name} from %{inbox_name} <%{from_email}>' - reply_with_name: '%{assignee_name} from %{inbox_name} ' - email_subject: "New messages on this conversation" - transcript_subject: "Conversation Transcript" + from_with_name: '%{assignee_name} no %{inbox_name} <%{from_email}>' + reply_with_name: '%{assignee_name} no %{inbox_name} ' + email_subject: "Jauni ziņojumi šajā sarunā" + transcript_subject: "Sarunas Transkripts" survey: - response: "Please rate this conversation, %{link}" + response: "Lūdzu, novērtējiet šo sarunu, %{link}" contacts: online: - delete: "%{contact_name} is Online, please try again later" + delete: "%{contact_name} ir Tiešsaistē, lūdzu, vēlāk mēģiniet vēlreiz" integration_apps: slack: name: "Slack" - description: "Slack is a chat tool that brings all your communication together in one place. By integrating Slack, you can get notified of all the new conversations in your account right inside your Slack." + description: "Slack ir tērzēšanas rīks, kas apvieno visu Jūsu saziņu vienuviet. Integrējot Slack, Jūs varat saņemt paziņojumus par visām jaunajām sarunām savā kontā tieši savā Slack." webhooks: name: "Webhooks" - description: "Webhook events provide you the realtime information about what's happening in your account. You can make use of the webhooks to communicate the events to your favourite apps like Slack or Github. Click on Configure to set up your webhooks." + description: "WebHook notikumi sniedz Jums reāllaika informāciju par to, kas notiek Jūsu kontā. Jūs varat izmantot webhook, lai paziņotu notikumus savām iecienītākajām lietotnēm, piemēram, Slack vai Github. Noklikšķiniet uz Konfigurēt, lai iestatītu savus webhook." dialogflow: name: "Dialogflow" - description: "Build chatbots using Dialogflow and connect them to your inbox quickly. Let the bots handle the queries before handing them off to a customer service agent." + description: "Veidojiet tērzēšanas robotus, izmantojot Dialogflow, un ātri savienojiet tos ar iesūtni. Ļaujiet botiem apstrādāt vaicājumus, pirms tos nododat klientu apkalpošanas aģentam." fullcontact: name: "Fullcontact" - description: "FullContact integration helps to enrich visitor profiles. Identify the users as soon as they share their email address and offer them tailored customer service. Connect your FullContact to your account by sharing the FullContact API Key." + description: "FullContact integrācija palīdz bagātināt apmeklētāju profilus. Identificējiet lietotājus, tiklīdz viņi kopīgo savu e-pasta adresi, un piedāvājiet viņiem pielāgotu klientu apkalpošanu. Savienojiet FullContact ar savu kontu, kopīgojot FullContact API atslēgu." diff --git a/config/locales/ms.yml b/config/locales/ms.yml index 21c581bab..7a67f2197 100644 --- a/config/locales/ms.yml +++ b/config/locales/ms.yml @@ -83,7 +83,7 @@ ms: contact_email_address: Contact Email Address contact_phone_number: Contact Phone Number link_to_the_conversation: Link to the conversation - agent_name: Agent Name + agent_name: Nama Ejen rating: Rating feedback: Feedback Comment recorded_at: Recorded date diff --git a/config/locales/tr.yml b/config/locales/tr.yml index 52d06a722..dd7d368cf 100644 --- a/config/locales/tr.yml +++ b/config/locales/tr.yml @@ -23,9 +23,9 @@ tr: reset_password_failure: Belirtilen e-postaya sahip herhangi bir kullanıcı bulamadık. errors: validations: - presence: must not be blank + presence: boş bırakılmamalı webhook: - invalid: Invalid events + invalid: Hatalı işlem signup: disposable_email: Tek kullanımlık e-postalara izin vermiyoruz invalid_email: Geçersiz bir e-posta girdiniz @@ -33,21 +33,21 @@ tr: failed: Kayıt başarısız oldu data_import: data_type: - invalid: Invalid data type + invalid: Hatalı veri türü contacts: import: failed: Dosya boş email: - invalid: Invalid email + invalid: Hatalı e-posta phone_number: - invalid: should be in e164 format + invalid: e164 formatında olmalı categories: locale: - unique: should be unique in the category and portal + unique: kategori ve portalde tekil olmalı inboxes: imap: - socket_error: Please check the network connection, IMAP address and try again. - no_response_error: Please check the IMAP credentials and try again. + socket_error: Lütfen ağ bağlantınızı, IMAP adresini kontrol edin ve tekrar deneyin. + no_response_error: Lütfen IMAP erişim bilgilerinizi kontrol edip tekrar deneyin. host_unreachable_error: Host unreachable, Please check the IMAP address, IMAP port and try again. connection_timed_out_error: Connection timed out for %{address}:%{port} connection_closed_error: Connection closed. diff --git a/config/locales/vi.yml b/config/locales/vi.yml index 387de7dd2..55dee46ce 100644 --- a/config/locales/vi.yml +++ b/config/locales/vi.yml @@ -17,15 +17,15 @@ #To learn more, please read the Rails Internationalization guide #available at https://guides.rubyonrails.org/i18n.html. vi: - hello: "Xin chào bạn" + hello: "Chào thế giới" messages: reset_password_success: Chà! Yêu cầu đặt lại mật khẩu thành công. Kiểm tra thư của bạn để biết hướng dẫn. reset_password_failure: Uh ho! Chúng tôi không thể tìm thấy bất kỳ người dùng nào có email được chỉ định. errors: validations: - presence: must not be blank + presence: không được để trống webhook: - invalid: Invalid events + invalid: Sự kiện không hợp lệ signup: disposable_email: Chúng tôi không cho phép các email dùng một lần invalid_email: Bạn đã nhập một email không hợp lệ @@ -33,81 +33,81 @@ vi: failed: Đăng ký thât bại data_import: data_type: - invalid: Invalid data type + invalid: Kiểu dữ liệu không hợp lệ contacts: import: - failed: Chưa chọn file + failed: Chưa chọn tệp email: - invalid: Invalid email + invalid: Email không hợp lệ phone_number: - invalid: should be in e164 format + invalid: nên theo đinh dạng e164 categories: locale: - unique: should be unique in the category and portal + unique: phải là duy nhất trong danh mục và cổng thông tin inboxes: imap: - socket_error: Please check the network connection, IMAP address and try again. - no_response_error: Please check the IMAP credentials and try again. - host_unreachable_error: Host unreachable, Please check the IMAP address, IMAP port and try again. - connection_timed_out_error: Connection timed out for %{address}:%{port} - connection_closed_error: Connection closed. + socket_error: Vui lòng kiểm tra kết nối mạng, địa chỉ IMAP và thử lại. + no_response_error: Vui lòng kiểm tra thông tin đăng nhập IMAP và thử lại. + host_unreachable_error: Máy chủ không thể truy cập được, Vui lòng kiểm tra địa chỉ IMAP, cổng IMAP và thử lại. + connection_timed_out_error: Kết nối đã hết thời gian chờ %{address}:%{port} + connection_closed_error: Kêt nối bị đóng. validations: - name: should not start or end with symbols, and it should not have < > / \ @ characters. + name: không nên bắt đầu hoặc kết thúc bằng các ký hiệu và không nên có kí tự < > / \ @. reports: period: Thời gian báo cáo từ %{since} đến %{until} agent_csv: - agent_name: Tên nhà cung cấp - conversations_count: Số cuộc trò chuyện + agent_name: Tên tổng đài viên + conversations_count: Số hội thoại avg_first_response_time: Thời gian trung bình của phản hồi đầu tiên (phút) avg_resolution_time: Thời gian giải quyết trung bình (phút) inbox_csv: - inbox_name: Inbox name - inbox_type: Inbox type - conversations_count: No. of conversations + inbox_name: Tên kênh + inbox_type: Kiểu kênh + conversations_count: Số hội thoại avg_first_response_time: Thời gian trung bình của phản hồi đầu tiên (phút) avg_resolution_time: Thời gian giải quyết trung bình (phút) label_csv: label_title: Nhãn - conversations_count: No. of conversations + conversations_count: Số hội thoại avg_first_response_time: Thời gian trung bình của phản hồi đầu tiên (phút) avg_resolution_time: Thời gian giải quyết trung bình (phút) team_csv: team_name: Tên nhóm - conversations_count: Số cuộc trò chuyện + conversations_count: Số hội thoại avg_first_response_time: Thời gian trung bình của phản hồi đầu tiên (phút) avg_resolution_time: Thời gian giải quyết trung bình (phút) default_group_by: ngày csat: headers: - contact_name: Contact Name - contact_email_address: Contact Email Address - contact_phone_number: Contact Phone Number - link_to_the_conversation: Link to the conversation + contact_name: Tên liên hệ + contact_email_address: Địa chỉ email của liên hệ + contact_phone_number: Số điện thoại của liên hệ + link_to_the_conversation: Liên kế tới hội thoại agent_name: Tên nhà cung cấp rating: Đánh giá - feedback: Feedback Comment - recorded_at: Recorded date + feedback: Bình luận phản hồi + recorded_at: Ngày nghi notifications: notification_title: - conversation_creation: "[Cuộc trò chuyện mới] - #%{display_id} đã được tạo trong %{inbox_name}" + conversation_creation: "[Hội thoại mới] - #%{display_id} đã được tạo trong %{inbox_name}" conversation_assignment: "[Phân công cho bạn] - #%{display_id} đã được phân công cho bạn" assigned_conversation_new_message: "[Tin nhắn mới] - #%{display_id} %{content}" - conversation_mention: "Bạn đã được nhắn đến trong cuộc trò chuyện [ID - %{display_id}] bởi %{name}" + conversation_mention: "Bạn đã được nhắn đến trong hội thoại [ID - %{display_id}] bởi %{name}" conversations: messages: - instagram_story_content: "%{story_sender} mentioned you in the story: " - instagram_deleted_story_content: This story is no longer available. + instagram_story_content: "%{story_sender} đã đề cập đến bạn trong hội thoại: " + instagram_deleted_story_content: Hội thoại này không còn nữa. deleted: Tin nhắn đã bị xoá activity: status: resolved: "Cuộc trò chuyện được đánh dấu là đã giải quyết bởi %{user_name}" - contact_resolved: "Conversation was resolved by %{contact_name}" + contact_resolved: "Hội thoại đã được giải quyết bởi %{contact_name}" open: "Cuộc trò chuyện đã được mở lại bởi %{user_name}" - pending: "Cuộc trò chuyện được đánh dấu là chưa giải quyết bởi %{user_name}" - snoozed: "Cuộc trò chuyện đã được tạm dừng lại bởi %{user_name}" - auto_resolved: "Cuộc trò chuyện được đánh dấu là đã giải quyết bởi hệ thống vì %{duration} ngày không hoạt động" + pending: "Hội thoại được đánh dấu là chưa giải quyết bởi %{user_name}" + snoozed: "Hội thoại đã được tạm dừng lại bởi %{user_name}" + auto_resolved: "Hội thoại được đánh dấu là đã giải quyết bởi hệ thống vì %{duration} ngày không hoạt động" assignee: - self_assigned: "%{user_name} phân công chính mình vào cuộc trò chuyện này" + self_assigned: "%{user_name} phân công chính mình vào hội thoại này" assigned: "Chỉ định %{assignee_name} bởi %{user_name}" removed: "Cuộc hội thoại chưa được chỉ định bởi %{user_name}" team: @@ -117,13 +117,13 @@ vi: labels: added: "%{user_name} thêm %{labels}" removed: "%{user_name} xoá %{labels}" - muted: "%{user_name} đã tắt tiếng cuộc trò chuyện" + muted: "%{user_name} đã tắt tiếng hội thoại" unmuted: "%{user_name} đã bật tiếng cuộc trò chuyện" templates: greeting_message_body: "%{account_name} thường trả lời trong vài giờ." - ways_to_reach_you_message_body: "Cung cấp cho nhóm một cách để tiếp cận bạn." + ways_to_reach_you_message_body: "Trong lúc chờ đội ngũ hỗ trợ phản hồi, bạn hãy để lại email để nhận được thông báo nhanh nhất nhé." email_input_box_message_body: "Nhận thông báo qua email" - csat_input_message_body: "Bạn hãy vui lòng đánh giá cuộc trò chuyện" + csat_input_message_body: "Bạn hãy vui lòng đánh giá hội thoại" reply: email: header: @@ -132,7 +132,7 @@ vi: email_subject: "Tin nhắn mới về cuộc trò chuyện này" transcript_subject: "Bản ghi cuộc hội thoại" survey: - response: "Bạn hãy vui lòng đánh giá cuộc trò chuyện, %{link}" + response: "Bạn hãy vui lòng đánh giá hội thoại, %{link}" contacts: online: delete: "%{contact_name} đang trực tiếng, vui lòng thử lại sau" diff --git a/config/routes.rb b/config/routes.rb index 903c5efc8..8b9b43863 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -182,6 +182,7 @@ Rails.application.routes.draw do delete :avatar, on: :collection member do post :availability + put :set_active_account end end diff --git a/db/migrate/20220926164441_add_slug_to_article.rb b/db/migrate/20220926164441_add_slug_to_article.rb new file mode 100644 index 000000000..4eac09b86 --- /dev/null +++ b/db/migrate/20220926164441_add_slug_to_article.rb @@ -0,0 +1,21 @@ +class AddSlugToArticle < ActiveRecord::Migration[6.1] + def up + add_column :articles, :slug, :string + + update_past_articles_with_slug + + add_index :articles, :slug + change_column_null(:articles, :slug, false) + end + + def down + remove_column(:articles, :slug) + end + + def update_past_articles_with_slug + Article.all.each_with_index do |article, index| + slug = article.title.underscore.parameterize(separator: '-') + article.update!(slug: "#{slug}-#{index}") + end + end +end diff --git a/db/migrate/20220930025317_add_unique_index_to_slug.rb b/db/migrate/20220930025317_add_unique_index_to_slug.rb new file mode 100644 index 000000000..e35f6289c --- /dev/null +++ b/db/migrate/20220930025317_add_unique_index_to_slug.rb @@ -0,0 +1,6 @@ +class AddUniqueIndexToSlug < ActiveRecord::Migration[6.1] + def change + remove_index :articles, :slug + add_index :articles, :slug, unique: true + end +end diff --git a/db/migrate/20221010212946_add_index_to_message_attachments.rb b/db/migrate/20221010212946_add_index_to_message_attachments.rb new file mode 100644 index 000000000..1ba2be299 --- /dev/null +++ b/db/migrate/20221010212946_add_index_to_message_attachments.rb @@ -0,0 +1,8 @@ +class AddIndexToMessageAttachments < ActiveRecord::Migration[6.1] + def change + add_index :attachments, :account_id + add_index :attachments, :message_id + add_index :conversations, :contact_id + add_index :conversations, :inbox_id + end +end diff --git a/db/migrate/20221017201914_add_features_to_accounts.rb b/db/migrate/20221017201914_add_features_to_accounts.rb new file mode 100644 index 000000000..60f47f7d3 --- /dev/null +++ b/db/migrate/20221017201914_add_features_to_accounts.rb @@ -0,0 +1,20 @@ +class AddFeaturesToAccounts < ActiveRecord::Migration[6.1] + def change + Account.find_in_batches do |account_batch| + account_batch.each do |account| + account.enable_features( + 'agent_management', + 'automations', + 'canned_responses', + 'custom_attributes', + 'inbox_management', + 'integrations', + 'labels', + 'team_management', + 'voice_recorder' + ) + account.save! + end + end + end +end diff --git a/db/schema.rb b/db/schema.rb index a20a90f65..d989b754b 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2022_09_20_014549) do +ActiveRecord::Schema.define(version: 2022_10_17_201914) do # These are extensions that must be enabled in order to support this database enable_extension "pg_stat_statements" @@ -131,8 +131,10 @@ ActiveRecord::Schema.define(version: 2022_09_20_014549) do t.bigint "author_id" t.bigint "associated_article_id" t.jsonb "meta", default: {} + t.string "slug", null: false t.index ["associated_article_id"], name: "index_articles_on_associated_article_id" t.index ["author_id"], name: "index_articles_on_author_id" + t.index ["slug"], name: "index_articles_on_slug", unique: true end create_table "attachments", id: :serial, force: :cascade do |t| @@ -146,6 +148,8 @@ ActiveRecord::Schema.define(version: 2022_09_20_014549) do t.datetime "updated_at", null: false t.string "fallback_title" t.string "extension" + t.index ["account_id"], name: "index_attachments_on_account_id" + t.index ["message_id"], name: "index_attachments_on_message_id" end create_table "automation_rules", force: :cascade do |t| @@ -408,8 +412,10 @@ ActiveRecord::Schema.define(version: 2022_09_20_014549) do t.index ["account_id"], name: "index_conversations_on_account_id" t.index ["assignee_id", "account_id"], name: "index_conversations_on_assignee_id_and_account_id" t.index ["campaign_id"], name: "index_conversations_on_campaign_id" + t.index ["contact_id"], name: "index_conversations_on_contact_id" t.index ["contact_inbox_id"], name: "index_conversations_on_contact_inbox_id" t.index ["first_reply_created_at"], name: "index_conversations_on_first_reply_created_at" + t.index ["inbox_id"], name: "index_conversations_on_inbox_id" t.index ["last_activity_at"], name: "index_conversations_on_last_activity_at" t.index ["status", "account_id"], name: "index_conversations_on_status_and_account_id" t.index ["team_id"], name: "index_conversations_on_team_id" diff --git a/db/seeds.rb b/db/seeds.rb index c4c7eda71..ead862669 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -46,8 +46,13 @@ unless Rails.env.production? inbox = Inbox.create!(channel: web_widget, account: account, name: 'Acme Support') InboxMember.create!(user: user, inbox: inbox) - contact = Contact.create!(name: 'jane', email: 'jane@example.com', phone_number: '+2320000', account: account) - contact_inbox = ContactInbox.create!(inbox: inbox, contact: contact, source_id: user.id, hmac_verified: true) + contact = ::ContactInboxWithContactBuilder.new( + source_id: user.id, + inbox: inbox, + hmac_verified: true, + contact_attributes: { name: 'jane', email: 'jane@example.com', phone_number: '+2320000' } + ).perform&.contact + conversation = Conversation.create!( account: account, inbox: inbox, diff --git a/deployment/nginx_chatwoot.conf b/deployment/nginx_chatwoot.conf index 8359431c9..95a5b145a 100644 --- a/deployment/nginx_chatwoot.conf +++ b/deployment/nginx_chatwoot.conf @@ -1,3 +1,14 @@ +upstream backend { + zone upstreams 64K; + server 127.0.0.1:3000; + keepalive 32; +} + +map $http_upgrade $connection_upgrade { + default upgrade; + '' close; +} + server { listen 80; listen [::]:80; @@ -6,12 +17,12 @@ server { access_log /var/log/nginx/chatwoot_access_80.log; error_log /var/log/nginx/chatwoot_error_80.log; - return 301 https://chatwoot.domain.com/; + return 301 https://chatwoot.domain.com$request_uri; } server { - listen 443 ssl http2; - listen [::]:443 ssl http2; + listen 443 ssl http2 reuseport; + listen [::]:443 ssl http2 reuseport; server_name chatwoot.domain.com www.chatwoot.domain.com; underscores_in_headers on; @@ -20,28 +31,33 @@ server { error_log /var/log/nginx/chatwoot_error_443.log; location / { - proxy_pass_header Authorization; - proxy_pass http://127.0.0.1:3000; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - proxy_set_header Host $host; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-Forwarded-Ssl on; # Optional - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_http_version 1.1; - proxy_set_header Connection ""; - proxy_buffering off; - client_max_body_size 0; - proxy_read_timeout 36000s; - proxy_redirect off; + proxy_pass http://backend; + proxy_redirect off; + + proxy_pass_header Authorization; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Ssl on; # Optional + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + + client_max_body_size 0; + proxy_read_timeout 36000s; } ssl_certificate /etc/letsencrypt/live/chatwoot.domain.com/fullchain.pem; # managed by Certbot ssl_certificate_key /etc/letsencrypt/live/chatwoot.domain.com/privkey.pem; # managed by Certbot - add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384; ssl_prefer_server_ciphers off; ssl_dhparam /etc/ssl/dhparam; + ssl_early_data on; + ssl_buffer_size 4k; + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 1d; + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; } diff --git a/lib/regex_helper.rb b/lib/regex_helper.rb index 2bbd51809..a36a8cd1d 100644 --- a/lib/regex_helper.rb +++ b/lib/regex_helper.rb @@ -6,4 +6,8 @@ module RegexHelper # shouldn't start with a underscore or hyphen UNICODE_CHARACTER_NUMBER_HYPHEN_UNDERSCORE = Regexp.new('\A[\p{L}\p{N}]+[\p{L}\p{N}_-]+\Z') MENTION_REGEX = Regexp.new('\[(@[\w_. ]+)\]\(mention://(?:user|team)/\d+/(.*?)+\)') + + TWILIO_CHANNEL_SMS_REGEX = Regexp.new('^\+\d{1,15}\z') + TWILIO_CHANNEL_WHATSAPP_REGEX = Regexp.new('^whatsapp:\+\d{1,15}\z') + WHATSAPP_CHANNEL_REGEX = Regexp.new('^\d{1,14}\z') end diff --git a/package.json b/package.json index a62526801..d3476875b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@chatwoot/chatwoot", - "version": "2.9.1", + "version": "2.10.0", "license": "MIT", "scripts": { "eslint": "eslint app/**/*.{js,vue}", diff --git a/public/assets/images/dashboard/editor/cmd-editor.png b/public/assets/images/dashboard/editor/cmd-editor.png new file mode 100644 index 000000000..7d0b154c6 Binary files /dev/null and b/public/assets/images/dashboard/editor/cmd-editor.png differ diff --git a/public/assets/images/dashboard/editor/enter-editor.png b/public/assets/images/dashboard/editor/enter-editor.png new file mode 100644 index 000000000..3deaa5845 Binary files /dev/null and b/public/assets/images/dashboard/editor/enter-editor.png differ diff --git a/spec/builders/contact_inbox_builder_spec.rb b/spec/builders/contact_inbox_builder_spec.rb index 2de5b6b60..46b0d749c 100644 --- a/spec/builders/contact_inbox_builder_spec.rb +++ b/spec/builders/contact_inbox_builder_spec.rb @@ -12,43 +12,54 @@ describe ::ContactInboxBuilder do it 'does not create contact inbox when contact inbox already exists with the source id provided' do existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: twilio_inbox, source_id: contact.phone_number) contact_inbox = described_class.new( - contact_id: contact.id, - inbox_id: twilio_inbox.id, + contact: contact, + inbox: twilio_inbox, source_id: contact.phone_number ).perform - expect(contact_inbox.id).to be(existing_contact_inbox.id) + expect(contact_inbox.id).to eq(existing_contact_inbox.id) end it 'does not create contact inbox when contact inbox already exists with phone number and source id is not provided' do existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: twilio_inbox, source_id: contact.phone_number) contact_inbox = described_class.new( - contact_id: contact.id, - inbox_id: twilio_inbox.id + contact: contact, + inbox: twilio_inbox ).perform - expect(contact_inbox.id).to be(existing_contact_inbox.id) + expect(contact_inbox.id).to eq(existing_contact_inbox.id) end it 'creates a new contact inbox when different source id is provided' do existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: twilio_inbox, source_id: contact.phone_number) contact_inbox = described_class.new( - contact_id: contact.id, - inbox_id: twilio_inbox.id, + contact: contact, + inbox: twilio_inbox, source_id: '+224213223422' ).perform - expect(contact_inbox.id).not_to be(existing_contact_inbox.id) - expect(contact_inbox.source_id).not_to be('+224213223422') + expect(contact_inbox.id).not_to eq(existing_contact_inbox.id) + expect(contact_inbox.source_id).to eq('+224213223422') end it 'creates a contact inbox with contact phone number when source id not provided and no contact inbox exists' do contact_inbox = described_class.new( - contact_id: contact.id, - inbox_id: twilio_inbox.id + contact: contact, + inbox: twilio_inbox ).perform - expect(contact_inbox.source_id).not_to be(contact.phone_number) + expect(contact_inbox.source_id).to eq(contact.phone_number) + end + + it 'raises error when contact phone number is not present and no source id is provided' do + contact.update!(phone_number: nil) + + expect do + described_class.new( + contact: contact, + inbox: twilio_inbox + ).perform + end.to raise_error(ActionController::ParameterMissing, 'param is missing or the value is empty: contact phone number') end end @@ -59,43 +70,111 @@ describe ::ContactInboxBuilder do it 'does not create contact inbox when contact inbox already exists with the source id provided' do existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: twilio_inbox, source_id: "whatsapp:#{contact.phone_number}") contact_inbox = described_class.new( - contact_id: contact.id, - inbox_id: twilio_inbox.id, + contact: contact, + inbox: twilio_inbox, source_id: "whatsapp:#{contact.phone_number}" ).perform + expect(contact_inbox.id).to eq(existing_contact_inbox.id) + end + + it 'does not create contact inbox when contact inbox already exists with phone number and source id is not provided' do + existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: twilio_inbox, source_id: "whatsapp:#{contact.phone_number}") + contact_inbox = described_class.new( + contact: contact, + inbox: twilio_inbox + ).perform + + expect(contact_inbox.id).to eq(existing_contact_inbox.id) + end + + it 'creates a new contact inbox when different source id is provided' do + existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: twilio_inbox, source_id: "whatsapp:#{contact.phone_number}") + contact_inbox = described_class.new( + contact: contact, + inbox: twilio_inbox, + source_id: 'whatsapp:+555555' + ).perform + + expect(contact_inbox.id).not_to eq(existing_contact_inbox.id) + expect(contact_inbox.source_id).to eq('whatsapp:+555555') + end + + it 'creates a contact inbox with contact phone number when source id not provided and no contact inbox exists' do + contact_inbox = described_class.new( + contact: contact, + inbox: twilio_inbox + ).perform + + expect(contact_inbox.source_id).to eq("whatsapp:#{contact.phone_number}") + end + + it 'raises error when contact phone number is not present and no source id is provided' do + contact.update!(phone_number: nil) + + expect do + described_class.new( + contact: contact, + inbox: twilio_inbox + ).perform + end.to raise_error(ActionController::ParameterMissing, 'param is missing or the value is empty: contact phone number') + end + end + + describe 'whatsapp inbox' do + let(:whatsapp_inbox) { create(:channel_whatsapp, account: account, sync_templates: false, validate_provider_config: false).inbox } + + it 'does not create contact inbox when contact inbox already exists with the source id provided' do + existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: whatsapp_inbox, source_id: contact.phone_number&.delete('+')) + contact_inbox = described_class.new( + contact: contact, + inbox: whatsapp_inbox, + source_id: contact.phone_number&.delete('+') + ).perform + expect(contact_inbox.id).to be(existing_contact_inbox.id) end it 'does not create contact inbox when contact inbox already exists with phone number and source id is not provided' do - existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: twilio_inbox, source_id: "whatsapp:#{contact.phone_number}") + existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: whatsapp_inbox, source_id: contact.phone_number&.delete('+')) contact_inbox = described_class.new( - contact_id: contact.id, - inbox_id: twilio_inbox.id + contact: contact, + inbox: whatsapp_inbox ).perform expect(contact_inbox.id).to be(existing_contact_inbox.id) end it 'creates a new contact inbox when different source id is provided' do - existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: twilio_inbox, source_id: "whatsapp:#{contact.phone_number}") + existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: whatsapp_inbox, source_id: contact.phone_number&.delete('+')) contact_inbox = described_class.new( - contact_id: contact.id, - inbox_id: twilio_inbox.id, - source_id: 'whatsapp:+555555' + contact: contact, + inbox: whatsapp_inbox, + source_id: '555555' ).perform expect(contact_inbox.id).not_to be(existing_contact_inbox.id) - expect(contact_inbox.source_id).not_to be('whatsapp:+55555') + expect(contact_inbox.source_id).not_to be('555555') end it 'creates a contact inbox with contact phone number when source id not provided and no contact inbox exists' do contact_inbox = described_class.new( - contact_id: contact.id, - inbox_id: twilio_inbox.id + contact: contact, + inbox: whatsapp_inbox ).perform - expect(contact_inbox.source_id).not_to be("whatsapp:#{contact.phone_number}") + expect(contact_inbox.source_id).to eq(contact.phone_number&.delete('+')) + end + + it 'raises error when contact phone number is not present and no source id is provided' do + contact.update!(phone_number: nil) + + expect do + described_class.new( + contact: contact, + inbox: whatsapp_inbox + ).perform + end.to raise_error(ActionController::ParameterMissing, 'param is missing or the value is empty: contact phone number') end end @@ -106,43 +185,54 @@ describe ::ContactInboxBuilder do it 'does not create contact inbox when contact inbox already exists with the source id provided' do existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: sms_inbox, source_id: contact.phone_number) contact_inbox = described_class.new( - contact_id: contact.id, - inbox_id: sms_inbox.id, + contact: contact, + inbox: sms_inbox, source_id: contact.phone_number ).perform - expect(contact_inbox.id).to be(existing_contact_inbox.id) + expect(contact_inbox.id).to eq(existing_contact_inbox.id) end it 'does not create contact inbox when contact inbox already exists with phone number and source id is not provided' do existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: sms_inbox, source_id: contact.phone_number) contact_inbox = described_class.new( - contact_id: contact.id, - inbox_id: sms_inbox.id + contact: contact, + inbox: sms_inbox ).perform - expect(contact_inbox.id).to be(existing_contact_inbox.id) + expect(contact_inbox.id).to eq(existing_contact_inbox.id) end it 'creates a new contact inbox when different source id is provided' do existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: sms_inbox, source_id: contact.phone_number) contact_inbox = described_class.new( - contact_id: contact.id, - inbox_id: sms_inbox.id, + contact: contact, + inbox: sms_inbox, source_id: '+224213223422' ).perform - expect(contact_inbox.id).not_to be(existing_contact_inbox.id) - expect(contact_inbox.source_id).not_to be('+224213223422') + expect(contact_inbox.id).not_to eq(existing_contact_inbox.id) + expect(contact_inbox.source_id).to eq('+224213223422') end it 'creates a contact inbox with contact phone number when source id not provided and no contact inbox exists' do contact_inbox = described_class.new( - contact_id: contact.id, - inbox_id: sms_inbox.id + contact: contact, + inbox: sms_inbox ).perform - expect(contact_inbox.source_id).not_to be(contact.phone_number) + expect(contact_inbox.source_id).to eq(contact.phone_number) + end + + it 'raises error when contact phone number is not present and no source id is provided' do + contact.update!(phone_number: nil) + + expect do + described_class.new( + contact: contact, + inbox: sms_inbox + ).perform + end.to raise_error(ActionController::ParameterMissing, 'param is missing or the value is empty: contact phone number') end end @@ -153,43 +243,54 @@ describe ::ContactInboxBuilder do it 'does not create contact inbox when contact inbox already exists with the source id provided' do existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: email_inbox, source_id: contact.email) contact_inbox = described_class.new( - contact_id: contact.id, - inbox_id: email_inbox.id, + contact: contact, + inbox: email_inbox, source_id: contact.email ).perform - expect(contact_inbox.id).to be(existing_contact_inbox.id) + expect(contact_inbox.id).to eq(existing_contact_inbox.id) end it 'does not create contact inbox when contact inbox already exists with email and source id is not provided' do existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: email_inbox, source_id: contact.email) contact_inbox = described_class.new( - contact_id: contact.id, - inbox_id: email_inbox.id + contact: contact, + inbox: email_inbox ).perform - expect(contact_inbox.id).to be(existing_contact_inbox.id) + expect(contact_inbox.id).to eq(existing_contact_inbox.id) end it 'creates a new contact inbox when different source id is provided' do existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: email_inbox, source_id: contact.email) contact_inbox = described_class.new( - contact_id: contact.id, - inbox_id: email_inbox.id, + contact: contact, + inbox: email_inbox, source_id: 'xyc@xyc.com' ).perform - expect(contact_inbox.id).not_to be(existing_contact_inbox.id) - expect(contact_inbox.source_id).not_to be('xyc@xyc.com') + expect(contact_inbox.id).not_to eq(existing_contact_inbox.id) + expect(contact_inbox.source_id).to eq('xyc@xyc.com') end it 'creates a contact inbox with contact email when source id not provided and no contact inbox exists' do contact_inbox = described_class.new( - contact_id: contact.id, - inbox_id: email_inbox.id + contact: contact, + inbox: email_inbox ).perform - expect(contact_inbox.source_id).not_to be(contact.email) + expect(contact_inbox.source_id).to eq(contact.email) + end + + it 'raises error when contact email is not present and no source id is provided' do + contact.update!(email: nil) + + expect do + described_class.new( + contact: contact, + inbox: email_inbox + ).perform + end.to raise_error(ActionController::ParameterMissing, 'param is missing or the value is empty: contact email') end end @@ -200,83 +301,34 @@ describe ::ContactInboxBuilder do it 'does not create contact inbox when contact inbox already exists with the source id provided' do existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: api_inbox, source_id: 'test') contact_inbox = described_class.new( - contact_id: contact.id, - inbox_id: api_inbox.id, + contact: contact, + inbox: api_inbox, source_id: 'test' ).perform - expect(contact_inbox.id).to be(existing_contact_inbox.id) + expect(contact_inbox.id).to eq(existing_contact_inbox.id) end it 'creates a new contact inbox when different source id is provided' do existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: api_inbox, source_id: SecureRandom.uuid) contact_inbox = described_class.new( - contact_id: contact.id, - inbox_id: api_inbox.id, + contact: contact, + inbox: api_inbox, source_id: 'test' ).perform - expect(contact_inbox.id).not_to be(existing_contact_inbox.id) - expect(contact_inbox.source_id).not_to be('test') + expect(contact_inbox.id).not_to eq(existing_contact_inbox.id) + expect(contact_inbox.source_id).to eq('test') end it 'creates a contact inbox with SecureRandom.uuid when source id not provided and no contact inbox exists' do contact_inbox = described_class.new( - contact_id: contact.id, - inbox_id: api_inbox.id + contact: contact, + inbox: api_inbox ).perform expect(contact_inbox.source_id).not_to be_nil end end - - describe 'web widget' do - let!(:website_channel) { create(:channel_widget, account: account) } - let!(:website_inbox) { create(:inbox, channel: website_channel, account: account) } - - it 'does not create contact inbox' do - contact_inbox = described_class.new( - contact_id: contact.id, - inbox_id: website_inbox.id, - source_id: 'test' - ).perform - - expect(contact_inbox).to be_nil - end - end - - describe 'facebook inbox' do - before do - stub_request(:post, /graph.facebook.com/) - end - - let!(:facebook_channel) { create(:channel_facebook_page, account: account) } - let!(:facebook_inbox) { create(:inbox, channel: facebook_channel, account: account) } - - it 'does not create contact inbox' do - contact_inbox = described_class.new( - contact_id: contact.id, - inbox_id: facebook_inbox.id, - source_id: 'test' - ).perform - - expect(contact_inbox).to be_nil - end - end - - describe 'twitter inbox' do - let!(:twitter_channel) { create(:channel_twitter_profile, account: account) } - let!(:twitter_inbox) { create(:inbox, channel: twitter_channel, account: account) } - - it 'does not create contact inbox' do - contact_inbox = described_class.new( - contact_id: contact.id, - inbox_id: twitter_inbox.id, - source_id: 'test' - ).perform - - expect(contact_inbox).to be_nil - end - end end end diff --git a/spec/builders/contact_builder_spec.rb b/spec/builders/contact_inbox_with_contact_builder_spec.rb similarity index 98% rename from spec/builders/contact_builder_spec.rb rename to spec/builders/contact_inbox_with_contact_builder_spec.rb index 29df0da22..e76d199d4 100644 --- a/spec/builders/contact_builder_spec.rb +++ b/spec/builders/contact_inbox_with_contact_builder_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -describe ::ContactBuilder do +describe ::ContactInboxWithContactBuilder do let(:account) { create(:account) } let(:inbox) { create(:inbox, account: account) } let(:contact) { create(:contact, email: 'xyc@example.com', phone_number: '+23423424123', account: account, identifier: '123') } diff --git a/spec/controllers/api/v1/accounts/articles_controller_spec.rb b/spec/controllers/api/v1/accounts/articles_controller_spec.rb index 1e98af260..a34079c69 100644 --- a/spec/controllers/api/v1/accounts/articles_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/articles_controller_spec.rb @@ -24,6 +24,7 @@ RSpec.describe 'Api::V1::Accounts::Articles', type: :request do category_id: category.id, description: 'test description', title: 'MyTitle', + slug: 'my-title', content: 'This is my content.', status: :published, author_id: agent.id @@ -39,8 +40,9 @@ RSpec.describe 'Api::V1::Accounts::Articles', type: :request do end it 'associate to the root article' do - root_article = create(:article, category: category, portal: portal, account_id: account.id, author_id: agent.id, associated_article_id: nil) - parent_article = create(:article, category: category, portal: portal, account_id: account.id, author_id: agent.id, + root_article = create(:article, category: category, slug: 'root-article', portal: portal, account_id: account.id, author_id: agent.id, + associated_article_id: nil) + parent_article = create(:article, category: category, slug: 'parent-article', portal: portal, account_id: account.id, author_id: agent.id, associated_article_id: root_article.id) article_params = { @@ -48,6 +50,7 @@ RSpec.describe 'Api::V1::Accounts::Articles', type: :request do category_id: category.id, description: 'test description', title: 'MyTitle', + slug: 'MyTitle', content: 'This is my content.', status: :published, author_id: agent.id, @@ -73,6 +76,7 @@ RSpec.describe 'Api::V1::Accounts::Articles', type: :request do category_id: category.id, description: 'test description', title: 'MyTitle', + slug: 'MyTitle', content: 'This is my content.', status: :published, author_id: agent.id, @@ -191,6 +195,8 @@ RSpec.describe 'Api::V1::Accounts::Articles', type: :request do json_response = JSON.parse(response.body) expect(json_response['payload'].count).to be 1 expect(json_response['meta']['articles_count']).to be 2 + expect(json_response['meta']['all_articles_count']).to be 2 + expect(json_response['meta']['mine_articles_count']).to be 1 end end @@ -210,9 +216,9 @@ RSpec.describe 'Api::V1::Accounts::Articles', type: :request do it 'get associated articles' do root_article = create(:article, category: category, portal: portal, account_id: account.id, author_id: agent.id, associated_article_id: nil) - child_article_1 = create(:article, category: category, portal: portal, account_id: account.id, author_id: agent.id, + child_article_1 = create(:article, slug: 'child-1', category: category, portal: portal, account_id: account.id, author_id: agent.id, associated_article_id: root_article.id) - child_article_2 = create(:article, category: category, portal: portal, account_id: account.id, author_id: agent.id, + child_article_2 = create(:article, slug: 'child-2', category: category, portal: portal, account_id: account.id, author_id: agent.id, associated_article_id: root_article.id) get "/api/v1/accounts/#{account.id}/portals/#{portal.slug}/articles/#{root_article.id}", diff --git a/spec/controllers/api/v1/accounts/bulk_actions_controller_spec.rb b/spec/controllers/api/v1/accounts/bulk_actions_controller_spec.rb index 434ae86d7..c7bb494c2 100644 --- a/spec/controllers/api/v1/accounts/bulk_actions_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/bulk_actions_controller_spec.rb @@ -75,6 +75,46 @@ RSpec.describe 'Api::V1::Accounts::BulkActionsController', type: :request do expect(Conversation.first.status).to eq('open') end + it 'Bulk remove assignee id from conversations' do + Conversation.first.update(assignee_id: agent_1.id) + Conversation.second.update(assignee_id: agent_2.id) + params = { type: 'Conversation', fields: { assignee_id: nil }, ids: Conversation.first(3).pluck(:display_id) } + + expect(Conversation.first.status).to eq('open') + expect(Conversation.first.assignee_id).to eq(agent_1.id) + expect(Conversation.second.assignee_id).to eq(agent_2.id) + + perform_enqueued_jobs do + post "/api/v1/accounts/#{account.id}/bulk_actions", + headers: agent.create_new_auth_token, + params: params + + expect(response).to have_http_status(:success) + end + + expect(Conversation.first.assignee_id).to be_nil + expect(Conversation.second.assignee_id).to be_nil + expect(Conversation.first.status).to eq('open') + end + + it 'Do not bulk update status to nil' do + Conversation.first.update(assignee_id: agent_1.id) + Conversation.second.update(assignee_id: agent_2.id) + params = { type: 'Conversation', fields: { status: nil }, ids: Conversation.first(3).pluck(:display_id) } + + expect(Conversation.first.status).to eq('open') + + perform_enqueued_jobs do + post "/api/v1/accounts/#{account.id}/bulk_actions", + headers: agent.create_new_auth_token, + params: params + + expect(response).to have_http_status(:success) + end + + expect(Conversation.first.status).to eq('open') + end + it 'Bulk update conversation status and assignee id' do params = { type: 'Conversation', fields: { assignee_id: agent_1.id, status: 'snoozed' }, ids: Conversation.first(3).pluck(:display_id) } diff --git a/spec/controllers/api/v1/profiles_controller_spec.rb b/spec/controllers/api/v1/profiles_controller_spec.rb index 0e386752f..8c331d932 100644 --- a/spec/controllers/api/v1/profiles_controller_spec.rb +++ b/spec/controllers/api/v1/profiles_controller_spec.rb @@ -195,4 +195,27 @@ RSpec.describe 'Profile API', type: :request do end end end + + describe 'PUT /api/v1/profile/set_active_account' do + context 'when it is an unauthenticated user' do + it 'returns unauthorized' do + put '/api/v1/profile/set_active_account' + + expect(response).to have_http_status(:unauthorized) + end + end + + context 'when it is an authenticated user' do + let(:agent) { create(:user, password: 'Test123!', account: account, role: :agent) } + + it 'updates the last active account id' do + put '/api/v1/profile/set_active_account', + params: { profile: { account_id: account.id } }, + headers: agent.create_new_auth_token, + as: :json + + expect(response).to have_http_status(:success) + end + end + end end diff --git a/spec/controllers/platform/api/v1/users_controller_spec.rb b/spec/controllers/platform/api/v1/users_controller_spec.rb index 57fa81bd4..ca3cd3bf9 100644 --- a/spec/controllers/platform/api/v1/users_controller_spec.rb +++ b/spec/controllers/platform/api/v1/users_controller_spec.rb @@ -96,15 +96,24 @@ RSpec.describe 'Platform Users API', type: :request do it 'creates a new user and permissible for the user' do expect do - post '/platform/api/v1/users/', params: { name: 'test', email: 'test@test.com', password: 'Password1!', + post '/platform/api/v1/users/', params: { name: 'test', display_name: 'displaytest', + email: 'test@test.com', password: 'Password1!', custom_attributes: { test: 'test_create' } }, headers: { api_access_token: platform_app.access_token.token }, as: :json end.not_to enqueue_mail expect(response).to have_http_status(:success) data = JSON.parse(response.body) - expect(data['email']).to eq('test@test.com') - expect(data['custom_attributes']['test']).to eq('test_create') + expect(data).to match( + hash_including( + 'name' => 'test', + 'display_name' => 'displaytest', + 'email' => 'test@test.com', + 'custom_attributes' => { + 'test' => 'test_create' + } + ) + ) expect(platform_app.platform_app_permissibles.first.permissible_id).to eq data['id'] end diff --git a/spec/factories/articles.rb b/spec/factories/articles.rb index 9ba5d9ac4..ec9c4cd2d 100644 --- a/spec/factories/articles.rb +++ b/spec/factories/articles.rb @@ -3,7 +3,7 @@ FactoryBot.define do account_id { 1 } category_id { 1 } author_id { 1 } - title { 'MyString' } + title { "#{Faker::Movie.title} #{SecureRandom.hex}" } content { 'MyText' } description { 'MyDescrption' } status { 1 } diff --git a/spec/factories/channel/channel_whatsapp.rb b/spec/factories/channel/channel_whatsapp.rb index a1cd4bee4..58f569ba3 100644 --- a/spec/factories/channel/channel_whatsapp.rb +++ b/spec/factories/channel/channel_whatsapp.rb @@ -44,8 +44,8 @@ FactoryBot.define do channel_whatsapp.define_singleton_method(:sync_templates) { return } unless options.sync_templates channel_whatsapp.define_singleton_method(:validate_provider_config) { return } unless options.validate_provider_config if channel_whatsapp.provider == 'whatsapp_cloud' - channel_whatsapp.provider_config = { 'api_key' => 'test_key', 'phone_number_id' => '123456789', 'business_account_id' => '123456789', - 'webhook_verify_token': 'test_token' } + channel_whatsapp.provider_config = channel_whatsapp.provider_config.merge({ 'api_key' => 'test_key', 'phone_number_id' => '123456789', + 'business_account_id' => '123456789' }) end end diff --git a/spec/factories/contact_inbox.rb b/spec/factories/contact_inbox.rb index 23abde2ca..e114a1567 100644 --- a/spec/factories/contact_inbox.rb +++ b/spec/factories/contact_inbox.rb @@ -15,6 +15,8 @@ def generate_source_id(contact_inbox) contact_inbox.inbox.channel.medium == 'sms' ? Faker::PhoneNumber.cell_phone_in_e164 : "whatsapp:#{Faker::PhoneNumber.cell_phone_in_e164}" when 'Channel::Email' "#{SecureRandom.uuid}@acme.inc" + when 'Channel::Whatsapp' + Faker::PhoneNumber.cell_phone_in_e164.delete('+') else SecureRandom.uuid end diff --git a/spec/factories/instagram/instagram_message_create_event.rb b/spec/factories/instagram/instagram_message_create_event.rb index b821580b4..d024deccc 100644 --- a/spec/factories/instagram/instagram_message_create_event.rb +++ b/spec/factories/instagram/instagram_message_create_event.rb @@ -147,4 +147,39 @@ FactoryBot.define do end initialize_with { attributes } end + + factory :instagram_story_mention_event_with_echo, class: Hash do + entry do + [ + { + 'id': 'instagram-message-id-1234', + 'time': '2021-09-08T06:34:04+0000', + 'messaging': [ + { + 'sender': { + 'id': 'Sender-id-1' + }, + 'recipient': { + 'id': 'chatwoot-app-user-id-1' + }, + 'timestamp': '2021-09-08T06:34:04+0000', + 'message': { + 'mid': 'message-id-1', + 'attachments': [ + { + 'type': 'story_mention', + 'payload': { + 'url': 'https://www.example.com/test.jpeg' + } + } + ], + 'is_echo': true + } + } + ] + } + ] + end + initialize_with { attributes } + end end diff --git a/spec/fixtures/files/support_uppercase.eml b/spec/fixtures/files/support_uppercase.eml new file mode 100644 index 000000000..9354312ee --- /dev/null +++ b/spec/fixtures/files/support_uppercase.eml @@ -0,0 +1,632 @@ +From: Sony Mathew +Mime-Version: 1.0 (Apple Message framework v1244.3) +Content-Type: multipart/alternative; boundary="Apple-Mail=_33A037C7-4BB3-4772-AE52-FCF2D7535F74" +Subject: Discussion: Let's debate these attachments +Date: Tue, 20 Apr 2020 04:20:20 -0400 +In-Reply-To: <4e6e35f5a38b4_479f13bb90078178@small-app-01.mail> +Reply-To: Sony Mathew +To: "Replies" +References: <4e6e35f5a38b4_479f13bb90078178@small-app-01.mail> +Message-Id: <0CB459E0-0336-41DA-BC88-E6E28C697DDB@chatwoot.com> +X-Mailer: Apple Mail (2.1244.3) + +--Apple-Mail=_33A037C7-4BB3-4772-AE52-FCF2D7535F74 +Content-Transfer-Encoding: quoted-printable +Content-Type: text/plain; + charset=utf-8 + +Let's talk about these images: + + +--Apple-Mail=_33A037C7-4BB3-4772-AE52-FCF2D7535F74 +Content-Type: multipart/related; + type="text/html"; + boundary="Apple-Mail=_83444AF4-343C-4F75-AF8F-14E1E7434FC1" + + +--Apple-Mail=_83444AF4-343C-4F75-AF8F-14E1E7434FC1 +Content-Transfer-Encoding: base64 +Content-Disposition: inline; + filename=avatar1.jpeg +Content-Type: image/jpg; + name="avatar1.jpeg" +Content-Id: <7AAEB353-2341-4D46-A054-5CA5CB2363B7> + +/9j/4AAQSkZJRgABAQAAAQABAAD/4gxYSUNDX1BST0ZJTEUAAQEAAAxITGlubwIQAABtbnRyUkdC +IFhZWiAHzgACAAkABgAxAABhY3NwTVNGVAAAAABJRUMgc1JHQgAAAAAAAAAAAAAAAAAA9tYAAQAA +AADTLUhQICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABFj +cHJ0AAABUAAAADNkZXNjAAABhAAAAGx3dHB0AAAB8AAAABRia3B0AAACBAAAABRyWFlaAAACGAAA +ABRnWFlaAAACLAAAABRiWFlaAAACQAAAABRkbW5kAAACVAAAAHBkbWRkAAACxAAAAIh2dWVkAAAD +TAAAAIZ2aWV3AAAD1AAAACRsdW1pAAAD+AAAABRtZWFzAAAEDAAAACR0ZWNoAAAEMAAAAAxyVFJD +AAAEPAAACAxnVFJDAAAEPAAACAxiVFJDAAAEPAAACAx0ZXh0AAAAAENvcHlyaWdodCAoYykgMTk5 +OCBIZXdsZXR0LVBhY2thcmQgQ29tcGFueQAAZGVzYwAAAAAAAAASc1JHQiBJRUM2MTk2Ni0yLjEA +AAAAAAAAAAAAABJzUkdCIElFQzYxOTY2LTIuMQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAWFlaIAAAAAAAAPNRAAEAAAABFsxYWVogAAAAAAAAAAAAAAAA +AAAAAFhZWiAAAAAAAABvogAAOPUAAAOQWFlaIAAAAAAAAGKZAAC3hQAAGNpYWVogAAAAAAAAJKAA +AA+EAAC2z2Rlc2MAAAAAAAAAFklFQyBodHRwOi8vd3d3LmllYy5jaAAAAAAAAAAAAAAAFklFQyBo +dHRwOi8vd3d3LmllYy5jaAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAABkZXNjAAAAAAAAAC5JRUMgNjE5NjYtMi4xIERlZmF1bHQgUkdCIGNvbG91ciBzcGFjZSAt +IHNSR0IAAAAAAAAAAAAAAC5JRUMgNjE5NjYtMi4xIERlZmF1bHQgUkdCIGNvbG91ciBzcGFjZSAt +IHNSR0IAAAAAAAAAAAAAAAAAAAAAAAAAAAAAZGVzYwAAAAAAAAAsUmVmZXJlbmNlIFZpZXdpbmcg +Q29uZGl0aW9uIGluIElFQzYxOTY2LTIuMQAAAAAAAAAAAAAALFJlZmVyZW5jZSBWaWV3aW5nIENv +bmRpdGlvbiBpbiBJRUM2MTk2Ni0yLjEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHZpZXcAAAAA +ABOk/gAUXy4AEM8UAAPtzAAEEwsAA1yeAAAAAVhZWiAAAAAAAEwJVgBQAAAAVx/nbWVhcwAAAAAA +AAABAAAAAAAAAAAAAAAAAAAAAAAAAo8AAAACc2lnIAAAAABDUlQgY3VydgAAAAAAAAQAAAAABQAK +AA8AFAAZAB4AIwAoAC0AMgA3ADsAQABFAEoATwBUAFkAXgBjAGgAbQByAHcAfACBAIYAiwCQAJUA +mgCfAKQAqQCuALIAtwC8AMEAxgDLANAA1QDbAOAA5QDrAPAA9gD7AQEBBwENARMBGQEfASUBKwEy +ATgBPgFFAUwBUgFZAWABZwFuAXUBfAGDAYsBkgGaAaEBqQGxAbkBwQHJAdEB2QHhAekB8gH6AgMC +DAIUAh0CJgIvAjgCQQJLAlQCXQJnAnECegKEAo4CmAKiAqwCtgLBAssC1QLgAusC9QMAAwsDFgMh +Ay0DOANDA08DWgNmA3IDfgOKA5YDogOuA7oDxwPTA+AD7AP5BAYEEwQgBC0EOwRIBFUEYwRxBH4E +jASaBKgEtgTEBNME4QTwBP4FDQUcBSsFOgVJBVgFZwV3BYYFlgWmBbUFxQXVBeUF9gYGBhYGJwY3 +BkgGWQZqBnsGjAadBq8GwAbRBuMG9QcHBxkHKwc9B08HYQd0B4YHmQesB78H0gflB/gICwgfCDII +RghaCG4IggiWCKoIvgjSCOcI+wkQCSUJOglPCWQJeQmPCaQJugnPCeUJ+woRCicKPQpUCmoKgQqY +Cq4KxQrcCvMLCwsiCzkLUQtpC4ALmAuwC8gL4Qv5DBIMKgxDDFwMdQyODKcMwAzZDPMNDQ0mDUAN +Wg10DY4NqQ3DDd4N+A4TDi4OSQ5kDn8Omw62DtIO7g8JDyUPQQ9eD3oPlg+zD88P7BAJECYQQxBh +EH4QmxC5ENcQ9RETETERTxFtEYwRqhHJEegSBxImEkUSZBKEEqMSwxLjEwMTIxNDE2MTgxOkE8UT +5RQGFCcUSRRqFIsUrRTOFPAVEhU0FVYVeBWbFb0V4BYDFiYWSRZsFo8WshbWFvoXHRdBF2UXiReu +F9IX9xgbGEAYZRiKGK8Y1Rj6GSAZRRlrGZEZtxndGgQaKhpRGncanhrFGuwbFBs7G2MbihuyG9oc +AhwqHFIcexyjHMwc9R0eHUcdcB2ZHcMd7B4WHkAeah6UHr4e6R8THz4faR+UH78f6iAVIEEgbCCY +IMQg8CEcIUghdSGhIc4h+yInIlUigiKvIt0jCiM4I2YjlCPCI/AkHyRNJHwkqyTaJQklOCVoJZcl +xyX3JicmVyaHJrcm6CcYJ0kneierJ9woDSg/KHEooijUKQYpOClrKZ0p0CoCKjUqaCqbKs8rAis2 +K2krnSvRLAUsOSxuLKIs1y0MLUEtdi2rLeEuFi5MLoIuty7uLyQvWi+RL8cv/jA1MGwwpDDbMRIx +SjGCMbox8jIqMmMymzLUMw0zRjN/M7gz8TQrNGU0njTYNRM1TTWHNcI1/TY3NnI2rjbpNyQ3YDec +N9c4FDhQOIw4yDkFOUI5fzm8Ofk6Njp0OrI67zstO2s7qjvoPCc8ZTykPOM9Ij1hPaE94D4gPmA+ +oD7gPyE/YT+iP+JAI0BkQKZA50EpQWpBrEHuQjBCckK1QvdDOkN9Q8BEA0RHRIpEzkUSRVVFmkXe +RiJGZ0arRvBHNUd7R8BIBUhLSJFI10kdSWNJqUnwSjdKfUrESwxLU0uaS+JMKkxyTLpNAk1KTZNN +3E4lTm5Ot08AT0lPk0/dUCdQcVC7UQZRUFGbUeZSMVJ8UsdTE1NfU6pT9lRCVI9U21UoVXVVwlYP +VlxWqVb3V0RXklfgWC9YfVjLWRpZaVm4WgdaVlqmWvVbRVuVW+VcNVyGXNZdJ114XcleGl5sXr1f +D19hX7NgBWBXYKpg/GFPYaJh9WJJYpxi8GNDY5dj62RAZJRk6WU9ZZJl52Y9ZpJm6Gc9Z5Nn6Wg/ +aJZo7GlDaZpp8WpIap9q92tPa6dr/2xXbK9tCG1gbbluEm5rbsRvHm94b9FwK3CGcOBxOnGVcfBy +S3KmcwFzXXO4dBR0cHTMdSh1hXXhdj52m3b4d1Z3s3gReG54zHkqeYl553pGeqV7BHtje8J8IXyB +fOF9QX2hfgF+Yn7CfyN/hH/lgEeAqIEKgWuBzYIwgpKC9INXg7qEHYSAhOOFR4Wrhg6GcobXhzuH +n4gEiGmIzokziZmJ/opkisqLMIuWi/yMY4zKjTGNmI3/jmaOzo82j56QBpBukNaRP5GokhGSepLj +k02TtpQglIqU9JVflcmWNJaflwqXdZfgmEyYuJkkmZCZ/JpomtWbQpuvnByciZz3nWSd0p5Anq6f +HZ+Ln/qgaaDYoUehtqImopajBqN2o+akVqTHpTilqaYapoum/adup+CoUqjEqTepqaocqo+rAqt1 +q+msXKzQrUStuK4trqGvFq+LsACwdbDqsWCx1rJLssKzOLOutCW0nLUTtYq2AbZ5tvC3aLfguFm4 +0blKucK6O7q1uy67p7whvJu9Fb2Pvgq+hL7/v3q/9cBwwOzBZ8Hjwl/C28NYw9TEUcTOxUvFyMZG +xsPHQce/yD3IvMk6ybnKOMq3yzbLtsw1zLXNNc21zjbOts83z7jQOdC60TzRvtI/0sHTRNPG1EnU +y9VO1dHWVdbY11zX4Nhk2OjZbNnx2nba+9uA3AXcit0Q3ZbeHN6i3ynfr+A24L3hROHM4lPi2+Nj +4+vkc+T85YTmDeaW5x/nqegy6LzpRunQ6lvq5etw6/vshu0R7ZzuKO6070DvzPBY8OXxcvH/8ozz +GfOn9DT0wvVQ9d72bfb794r4Gfio+Tj5x/pX+uf7d/wH/Jj9Kf26/kv+3P9t////2wBDAAICAgIC +AQICAgICAgIDAwYEAwMDAwcFBQQGCAcICAgHCAgJCg0LCQkMCggICw8LDA0ODg4OCQsQEQ8OEQ0O +Dg7/2wBDAQICAgMDAwYEBAYOCQgJDg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4O +Dg4ODg4ODg4ODg4ODg7/wAARCADwAPADASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAEC +AwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0Kx +wRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1 +dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ +2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QA +tREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYk +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 +mk8VWW5WRGJGAOaAP//Z + +--Apple-Mail=_83444AF4-343C-4F75-AF8F-14E1E7434FC1 +Content-Transfer-Encoding: base64 +Content-Disposition: inline; + filename=avatar2.jpg +Content-Type: image/jpg; + x-unix-mode=0700; + name="avatar2.jpg" +Content-Id: <4594E827-6E69-4329-8691-6BC35E3E73A0> + +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 + +--Apple-Mail=_83444AF4-343C-4F75-AF8F-14E1E7434FC1-- + +--Apple-Mail=_33A037C7-4BB3-4772-AE52-FCF2D7535F74-- \ No newline at end of file diff --git a/spec/jobs/webhooks/instagram_events_job_spec.rb b/spec/jobs/webhooks/instagram_events_job_spec.rb index 91600375f..89951e33a 100644 --- a/spec/jobs/webhooks/instagram_events_job_spec.rb +++ b/spec/jobs/webhooks/instagram_events_job_spec.rb @@ -11,7 +11,7 @@ describe Webhooks::InstagramEventsJob do end let!(:account) { create(:account) } - let(:return_onject) do + let(:return_object) do { name: 'Jane', id: 'Sender-id-1', account_id: instagram_inbox.account_id, @@ -24,6 +24,7 @@ describe Webhooks::InstagramEventsJob do let!(:unsend_event) { build(:instagram_message_unsend_event).with_indifferent_access } let!(:attachment_params) { build(:instagram_message_attachment_event).with_indifferent_access } let!(:story_mention_params) { build(:instagram_story_mention_event).with_indifferent_access } + let!(:story_mention_echo_params) { build(:instagram_story_mention_event_with_echo).with_indifferent_access } let(:fb_object) { double } describe '#perform' do @@ -31,7 +32,7 @@ describe Webhooks::InstagramEventsJob do it 'creates incoming message in the instagram inbox' do allow(Koala::Facebook::API).to receive(:new).and_return(fb_object) allow(fb_object).to receive(:get_object).and_return( - return_onject.with_indifferent_access + return_object.with_indifferent_access ) instagram_webhook.perform_now(dm_params[:entry]) @@ -45,7 +46,7 @@ describe Webhooks::InstagramEventsJob do it 'creates test text message in the instagram inbox' do allow(Koala::Facebook::API).to receive(:new).and_return(fb_object) allow(fb_object).to receive(:get_object).and_return( - return_onject.with_indifferent_access + return_object.with_indifferent_access ) instagram_webhook.perform_now(test_params[:entry]) @@ -78,7 +79,7 @@ describe Webhooks::InstagramEventsJob do it 'creates incoming message with attachments in the instagram inbox' do allow(Koala::Facebook::API).to receive(:new).and_return(fb_object) allow(fb_object).to receive(:get_object).and_return( - return_onject.with_indifferent_access + return_object.with_indifferent_access ) instagram_webhook.perform_now(attachment_params[:entry]) @@ -92,7 +93,7 @@ describe Webhooks::InstagramEventsJob do it 'creates incoming message with attachments in the instagram inbox for story mention' do allow(Koala::Facebook::API).to receive(:new).and_return(fb_object) allow(fb_object).to receive(:get_object).and_return( - return_onject.with_indifferent_access, + return_object.with_indifferent_access, { story: { mention: { @@ -113,6 +114,19 @@ describe Webhooks::InstagramEventsJob do expect(instagram_inbox.messages.count).to be 1 expect(instagram_inbox.messages.last.attachments.count).to be 1 end + + it 'creates does not create contact or messages' do + allow(Koala::Facebook::API).to receive(:new).and_return(fb_object) + allow(fb_object).to receive(:get_object).and_raise(Koala::Facebook::ClientError) + + instagram_webhook.perform_now(story_mention_echo_params[:entry]) + + instagram_inbox.reload + + expect(instagram_inbox.contacts.count).to be 0 + expect(instagram_inbox.contact_inboxes.count).to be 0 + expect(instagram_inbox.messages.count).to be 0 + end end end end diff --git a/spec/jobs/webhooks/whatsapp_events_job_spec.rb b/spec/jobs/webhooks/whatsapp_events_job_spec.rb index 2a28fe41f..00fde3d41 100644 --- a/spec/jobs/webhooks/whatsapp_events_job_spec.rb +++ b/spec/jobs/webhooks/whatsapp_events_job_spec.rb @@ -62,6 +62,59 @@ RSpec.describe Webhooks::WhatsappEventsJob, type: :job do job.perform_now(wb_params) end + it 'Ignore reaction type message and stop raising error' do + other_channel = create(:channel_whatsapp, phone_number: '+1987654', provider: 'whatsapp_cloud', sync_templates: false, + validate_provider_config: false) + wb_params = { + phone_number: channel.phone_number, + object: 'whatsapp_business_account', + entry: [{ + changes: [{ + value: { + contacts: [{ profile: { name: 'Test Test' }, wa_id: '1111981136571' }], + messages: [{ + from: '1111981136571', reaction: { emoji: '👍' }, timestamp: '1664799904', type: 'reaction' + }], + metadata: { + phone_number_id: other_channel.provider_config['phone_number_id'], + display_phone_number: other_channel.phone_number.delete('+') + } + } + }] + }] + }.with_indifferent_access + expect do + Whatsapp::IncomingMessageWhatsappCloudService.new(inbox: other_channel.inbox, params: wb_params).perform + end.not_to change(Message, :count) + end + + it 'Ignore contacts type message and stop raising error' do + other_channel = create(:channel_whatsapp, phone_number: '+1987654', provider: 'whatsapp_cloud', sync_templates: false, + validate_provider_config: false) + wb_params = { + phone_number: channel.phone_number, + object: 'whatsapp_business_account', + entry: [{ + changes: [{ + value: { + contacts: [{ profile: { name: 'Test Test' }, wa_id: '1111981136571' }], + messages: [{ from: '1111981136571', + contacts: [{ phones: [{ phone: '+1987654' }], name: { first_name: 'contact name' } }], + timestamp: '1664799904', + type: 'contacts' }], + metadata: { + phone_number_id: other_channel.provider_config['phone_number_id'], + display_phone_number: other_channel.phone_number.delete('+') + } + } + }] + }] + }.with_indifferent_access + expect do + Whatsapp::IncomingMessageWhatsappCloudService.new(inbox: other_channel.inbox, params: wb_params).perform + end.not_to change(Message, :count) + end + it 'will not enque Whatsapp::IncomingMessageWhatsappCloudService when invalid phone number id' do other_channel = create(:channel_whatsapp, phone_number: '+1987654', provider: 'whatsapp_cloud', sync_templates: false, validate_provider_config: false) diff --git a/spec/mailboxes/support_mailbox_spec.rb b/spec/mailboxes/support_mailbox_spec.rb index 59217b212..946275b71 100644 --- a/spec/mailboxes/support_mailbox_spec.rb +++ b/spec/mailboxes/support_mailbox_spec.rb @@ -103,14 +103,35 @@ RSpec.describe SupportMailbox, type: :mailbox do end describe 'handle inbox contacts' do - let(:contact) { create(:contact, account: account, email: support_mail.mail.from.first) } - let(:contact_inbox) { create(:contact_inbox, inbox: channel_email.inbox, contact: contact) } + let!(:contact) { create(:contact, account: account, email: support_mail.mail.from.first) } + let!(:contact_inbox) { create(:contact_inbox, inbox: channel_email.inbox, contact: contact) } it 'does not create new contact if that contact exists in the inbox' do - # making sure we have a contact already present - expect(contact_inbox.contact.email).to eq(support_mail.mail.from.first) - described_subject + expect do + described_subject + end + .to(not_change { Contact.count } + .and(not_change { ContactInbox.count })) + expect(conversation.messages.last.sender.id).to eq(contact.id) + expect(conversation.contact_inbox).to eq(contact_inbox) + end + + context 'with uppercase reply-to' do + let(:support_mail) { create_inbound_email_from_fixture('support_uppercase.eml') } + let!(:contact) { create(:contact, account: account, email: support_mail.mail.from.first) } + let!(:contact_inbox) { create(:contact_inbox, inbox: channel_email.inbox, contact: contact) } + + it 'does not create new contact if that contact exists in the inbox' do + expect do + described_subject + end + .to(not_change { Contact.count } + .and(not_change { ContactInbox.count })) + + expect(conversation.messages.last.sender.id).to eq(contact.id) + expect(conversation.contact_inbox).to eq(contact_inbox) + end end end diff --git a/spec/models/article_spec.rb b/spec/models/article_spec.rb index 6a66838ef..2e0f957dc 100644 --- a/spec/models/article_spec.rb +++ b/spec/models/article_spec.rb @@ -25,13 +25,15 @@ RSpec.describe Article, type: :model do let!(:category_3) { create(:category, slug: 'category_3', locale: 'es', portal_id: portal_2.id) } before do - create(:article, category_id: category_1.id, content: 'This is the content', description: 'this is the description', title: 'this is title', + create(:article, category_id: category_1.id, content: 'This is the content', description: 'this is the description', + slug: 'this-is-title', title: 'this is title', portal_id: portal_1.id, author_id: user.id) - create(:article, category_id: category_1.id, title: 'title 1', content: 'This is the content', portal_id: portal_1.id, author_id: user.id) - create(:article, category_id: category_2.id, title: 'title 2', portal_id: portal_2.id, author_id: user.id) - create(:article, category_id: category_2.id, title: 'title 3', portal_id: portal_1.id, author_id: user.id) - create(:article, category_id: category_3.id, title: 'title 6', portal_id: portal_2.id, author_id: user.id, status: :published) - create(:article, category_id: category_2.id, title: 'title 7', portal_id: portal_1.id, author_id: user.id, status: :published) + create(:article, category_id: category_1.id, slug: 'title-1', title: 'title 1', content: 'This is the content', portal_id: portal_1.id, + author_id: user.id) + create(:article, category_id: category_2.id, slug: 'title-2', title: 'title 2', portal_id: portal_2.id, author_id: user.id) + create(:article, category_id: category_2.id, slug: 'title-3', title: 'title 3', portal_id: portal_1.id, author_id: user.id) + create(:article, category_id: category_3.id, slug: 'title-6', title: 'title 6', portal_id: portal_2.id, author_id: user.id, status: :published) + create(:article, category_id: category_2.id, slug: 'title-7', title: 'title 7', portal_id: portal_1.id, author_id: user.id, status: :published) end context 'when no parameters passed' do @@ -117,11 +119,23 @@ RSpec.describe Article, type: :model do records = portal_1.articles.search(params) expect(records.count).to eq(2) end + + it 'auto saves article slug' do + article = create(:article, category_id: category_1.id, title: 'the awesome article 1', content: 'This is the content', portal_id: portal_1.id, + author_id: user.id) + expect(article.slug).to include('the-awesome-article-1') + end end context 'with pagination' do it 'returns paginated articles' do - create_list(:article, 30, category_id: category_2.id, title: 'title 1', portal_id: portal_2.id, author_id: user.id) + build_list(:article, 30) do |record, i| + record.category_id = category_2.id + record.title = "title #{i}" + record.portal_id = portal_2.id + record.author_id = user.id + record.save! + end params = { category_slug: 'category_2' } records = portal_2.articles.search(params) expect(records.count).to eq(25) diff --git a/spec/models/channel/whatsapp_spec.rb b/spec/models/channel/whatsapp_spec.rb index d96ffdc85..9a6620a5e 100644 --- a/spec/models/channel/whatsapp_spec.rb +++ b/spec/models/channel/whatsapp_spec.rb @@ -20,4 +20,20 @@ RSpec.describe Channel::Whatsapp do expect(channel.save).to be(true) end end + + describe 'webhook_verify_token' do + it 'generates webhook_verify_token if not present' do + channel = create(:channel_whatsapp, provider_config: { webhook_verify_token: nil }, provider: 'whatsapp_cloud', account: create(:account), + validate_provider_config: false, sync_templates: false) + + expect(channel.provider_config['webhook_verify_token']).not_to be_nil + end + + it 'does not generate webhook_verify_token if present' do + channel = create(:channel_whatsapp, provider: 'whatsapp_cloud', provider_config: { webhook_verify_token: '123' }, account: create(:account), + validate_provider_config: false, sync_templates: false) + + expect(channel.provider_config['webhook_verify_token']).to eq '123' + end + end end diff --git a/spec/models/contact_inbox_spec.rb b/spec/models/contact_inbox_spec.rb index 1c575f105..e38ad81e8 100644 --- a/spec/models/contact_inbox_spec.rb +++ b/spec/models/contact_inbox_spec.rb @@ -37,4 +37,59 @@ RSpec.describe ContactInbox do expect(obj.pubsub_token).to eq(new_token) end end + + describe 'validations' do + context 'when source_id' do + it 'validates whatsapp channel source_id' do + whatsapp_inbox = create(:channel_whatsapp, sync_templates: false, validate_provider_config: false).inbox + contact = create(:contact) + valid_source_id = build(:contact_inbox, contact: contact, inbox: whatsapp_inbox, source_id: '1234567890') + ci_character_in_source_id = build(:contact_inbox, contact: contact, inbox: whatsapp_inbox, source_id: '1234567890aaa') + ci_plus_in_source_id = build(:contact_inbox, contact: contact, inbox: whatsapp_inbox, source_id: '+1234567890') + expect(valid_source_id.valid?).to be(true) + expect(ci_character_in_source_id.valid?).to be(false) + expect(ci_character_in_source_id.errors.full_messages).to eq( + ['Source invalid source id for whatsapp inbox. valid Regex (?-mix:^\\d{1,14}\\z)'] + ) + expect(ci_plus_in_source_id.valid?).to be(false) + expect(ci_plus_in_source_id.errors.full_messages).to eq( + ['Source invalid source id for whatsapp inbox. valid Regex (?-mix:^\\d{1,14}\\z)'] + ) + end + + it 'validates twilio sms channel source_id' do + twilio_sms_inbox = create(:channel_twilio_sms).inbox + contact = create(:contact) + valid_source_id = build(:contact_inbox, contact: contact, inbox: twilio_sms_inbox, source_id: '+1234567890') + ci_character_in_source_id = build(:contact_inbox, contact: contact, inbox: twilio_sms_inbox, source_id: '+1234567890aaa') + ci_without_plus_in_source_id = build(:contact_inbox, contact: contact, inbox: twilio_sms_inbox, source_id: '1234567890') + expect(valid_source_id.valid?).to be(true) + expect(ci_character_in_source_id.valid?).to be(false) + expect(ci_character_in_source_id.errors.full_messages).to eq( + ['Source invalid source id for twilio sms inbox. valid Regex (?-mix:^\\+\\d{1,15}\\z)'] + ) + expect(ci_without_plus_in_source_id.valid?).to be(false) + expect(ci_without_plus_in_source_id.errors.full_messages).to eq( + ['Source invalid source id for twilio sms inbox. valid Regex (?-mix:^\\+\\d{1,15}\\z)'] + ) + end + + it 'validates twilio whatsapp channel source_id' do + twilio_whatsapp_inbox = create(:channel_twilio_sms, medium: :whatsapp).inbox + contact = create(:contact) + valid_source_id = build(:contact_inbox, contact: contact, inbox: twilio_whatsapp_inbox, source_id: 'whatsapp:+1234567890') + ci_character_in_source_id = build(:contact_inbox, contact: contact, inbox: twilio_whatsapp_inbox, source_id: 'whatsapp:+1234567890aaa') + ci_without_plus_in_source_id = build(:contact_inbox, contact: contact, inbox: twilio_whatsapp_inbox, source_id: 'whatsapp:1234567890') + expect(valid_source_id.valid?).to be(true) + expect(ci_character_in_source_id.valid?).to be(false) + expect(ci_character_in_source_id.errors.full_messages).to eq( + ['Source invalid source id for twilio whatsapp inbox. valid Regex (?-mix:^whatsapp:\\+\\d{1,15}\\z)'] + ) + expect(ci_without_plus_in_source_id.valid?).to be(false) + expect(ci_without_plus_in_source_id.errors.full_messages).to eq( + ['Source invalid source id for twilio whatsapp inbox. valid Regex (?-mix:^whatsapp:\\+\\d{1,15}\\z)'] + ) + end + end + end end diff --git a/spec/models/message_spec.rb b/spec/models/message_spec.rb index e74a1129d..aeff941da 100644 --- a/spec/models/message_spec.rb +++ b/spec/models/message_spec.rb @@ -33,6 +33,21 @@ RSpec.describe Message, type: :model do end end + context 'with webhook_data' do + it 'contains the message attachment when attachment is present' do + message = create(:message) + attachment = message.attachments.new(account_id: message.account_id, file_type: :image) + attachment.file.attach(io: File.open(Rails.root.join('spec/assets/avatar.png')), filename: 'avatar.png', content_type: 'image/png') + attachment.save! + expect(message.webhook_data.key?(:attachments)).to be true + end + + it 'does not contain the message attachment when attachment is not present' do + message = create(:message) + expect(message.webhook_data.key?(:attachments)).to be false + end + end + context 'when message is created' do let(:message) { build(:message, account: create(:account)) } @@ -106,6 +121,19 @@ RSpec.describe Message, type: :model do end end + context 'when attachments size maximum' do + let(:message) { build(:message, content_type: nil, account: create(:account)) } + + it 'add errors to message for attachment size is more than allowed limit' do + 16.times.each do + attachment = message.attachments.new(account_id: message.account_id, file_type: :image) + attachment.file.attach(io: File.open(Rails.root.join('spec/assets/avatar.png')), filename: 'avatar.png', content_type: 'image/png') + end + + expect(message.errors.messages).to eq({ attachments: ['exceeded maximum allowed'] }) + end + end + context 'when email notifiable message' do let(:message) { build(:message, content_type: nil, account: create(:account)) } diff --git a/spec/models/working_hour_spec.rb b/spec/models/working_hour_spec.rb index c126f6f87..a5018a28e 100644 --- a/spec/models/working_hour_spec.rb +++ b/spec/models/working_hour_spec.rb @@ -88,4 +88,18 @@ RSpec.describe WorkingHour do 'Validation failed: open_all_day and closed_all_day cannot be true at the same time') end end + + context 'when on monday 9am in Sydney timezone' do + let(:inbox) { create(:inbox) } + + before do + Time.zone = 'Australia/Sydney' + inbox.update(timezone: 'Australia/Sydney') + travel_to '10.10.2022 9:00 AEDT' + end + + it 'is considered working hour' do + expect(described_class.today.open_now?).to be true + end + end end diff --git a/spec/services/contacts/filter_service_spec.rb b/spec/services/contacts/filter_service_spec.rb index 5f52a1a3c..7e6661aed 100644 --- a/spec/services/contacts/filter_service_spec.rb +++ b/spec/services/contacts/filter_service_spec.rb @@ -167,6 +167,7 @@ describe ::Contacts::FilterService do context 'with x_days_before filter' do before do + Time.zone = 'UTC' el_contact.update(last_activity_at: (Time.zone.today - 4.days)) cs_contact.update(last_activity_at: (Time.zone.today - 5.days)) en_contact.update(last_activity_at: (Time.zone.today - 2.days)) diff --git a/spec/services/conversations/filter_service_spec.rb b/spec/services/conversations/filter_service_spec.rb index ed489bd30..a17351ec3 100644 --- a/spec/services/conversations/filter_service_spec.rb +++ b/spec/services/conversations/filter_service_spec.rb @@ -309,6 +309,7 @@ describe ::Conversations::FilterService do context 'with x_days_before filter' do before do + Time.zone = 'UTC' en_conversation_1.update!(last_activity_at: (Time.zone.today - 4.days)) en_conversation_2.update!(last_activity_at: (Time.zone.today - 5.days)) user_2_assigned_conversation.update!(last_activity_at: (Time.zone.today - 2.days)) diff --git a/spec/support/negated_matchers.rb b/spec/support/negated_matchers.rb new file mode 100644 index 000000000..2adeec64a --- /dev/null +++ b/spec/support/negated_matchers.rb @@ -0,0 +1,3 @@ +# "not_change" is needed to support chaining "change" matchers +# see https://stackoverflow.com/a/34969429/58876 +RSpec::Matchers.define_negated_matcher :not_change, :change diff --git a/tailwind.config.js b/tailwind.config.js index e839d00ec..90408a779 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -14,16 +14,18 @@ module.exports = { colors: { white: colors.white, woot: { - 50: '#E3F2FF', - 100: '#BBDDFF', - 200: '#8FC9FF', - 300: '#61B3FF', - 400: '#3FA3FF', + 25: '#F5FAFF', + 50: '#EBF5FF', + 75: '#D6EBFF', + 100: '#C2E1FF', + 200: '#99CEFF', + 300: '#70BAFF', + 400: '#47A6FF', 500: '#1F93FF', - 600: '#2284F0', - 700: '#2272DC', - 800: '#2161CA', - 900: '#1F41AB', + 600: '#1976CC', + 700: '#135899', + 800: '#0C3B66', + 900: '#061D33', }, green: { 50: '#E6F8E6', @@ -38,38 +40,40 @@ module.exports = { 900: '#007000', }, yellow: { - 50: '#FFFEE8', - 100: '#FFFAC5', - 200: '#FFF69E', - 300: '#FEF176', - 400: '#FCEC56', - 500: '#F9E736', - 600: '#FFDD3A', - 700: '#FFC532', - 800: '#FDAD2A', - 900: '#F9841B', + 50: '#FEFDE8', + 100: '#FDFCC4', + 200: '#FCF68C', + 300: '#F9E736', + 400: '#F6D819', + 500: '#E6C00C', + 600: '#C69608', + 700: '#9E6b0A', + 800: '#835510', + 900: '#6F4514', }, slate: { - 50: '#F4F6FB', - 100: '#C8D6E6', - 200: '#ABBACE', - 300: '#8C9EB6', - 400: '#7489A4', - 500: '#5D7592', - 600: '#506781', - 700: '#40546B', - 800: '#314155', - 900: '#1F2D3D', + 25: '#F8FAFC', + 50: '#F1F5F8', + 75: '#EBF0F5', + 100: ' #E4EBF1', + 200: ' #C9D7E3', + 300: ' #AEC3D5', + 400: ' #93AFC8', + 500: ' #779BBB', + 600: ' #446888', + 700: ' #37546D', + 800: ' #293F51', + 900: ' #1B2836', }, black: { - 50: '#F8F9FE', - 100: '#F2F3F7', - 200: '#E9EAEF', - 300: '#DADBDF', - 400: '#B6B7BB', + 50: '#F7F7F7', + 100: '#ECECED', + 200: '#DDDDE0', + 300: '#C6C7CA', + 400: '#ABACAF', 500: '#96979C', 600: '#6E6F73', - 700: '#3C4858', + 700: '#5A5B5F', 800: '#3C3D40', 900: '#1B1C1F', },