diff --git a/.env.example b/.env.example index 5adb4a848..110bd4292 100644 --- a/.env.example +++ b/.env.example @@ -40,7 +40,8 @@ SMTP_AUTHENTICATION= SMTP_ENABLE_STARTTLS_AUTO= # Mail Incoming - +# This is the domain set for the reply emails when conversation continuity is enabled +MAILER_INBOUND_EMAIL_DOMAIN= # Set this to appropriate ingress channel with regards to incoming emails # Possible values are : # :relay for Exim, Postfix, Qmail diff --git a/.eslintrc.js b/.eslintrc.js index 9b99b9b61..c5cbba917 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -46,5 +46,6 @@ module.exports = { }, globals: { __WEBPACK_ENV__: true, + bus: true, }, }; diff --git a/.gitignore b/.gitignore index d058d20d1..3bf2fdbb1 100644 --- a/.gitignore +++ b/.gitignore @@ -55,3 +55,7 @@ node_modules package-lock.json *.dump + + +# cypress +test/cypress/videos/* \ No newline at end of file diff --git a/.scss-lint.yml b/.scss-lint.yml index 19fae0cf1..1cc029441 100644 --- a/.scss-lint.yml +++ b/.scss-lint.yml @@ -282,3 +282,4 @@ exclude: - 'app/javascript/widget/assets/scss/_reset.scss' - 'app/javascript/widget/assets/scss/sdk.css' - 'app/assets/stylesheets/administrate/reset/_normalize.scss' + - 'app/javascript/shared/assets/stylesheets/*.scss' diff --git a/Gemfile b/Gemfile index a769187de..677d56c1a 100644 --- a/Gemfile +++ b/Gemfile @@ -8,7 +8,7 @@ gem 'rails' # Reduces boot times through caching; required in config/boot.rb gem 'bootsnap', require: false -##-- rails helper gems --## +##-- rails application helper gems --## gem 'acts-as-taggable-on' gem 'attr_extras' gem 'browser' @@ -23,6 +23,12 @@ gem 'tzinfo-data' gem 'valid_email2' # compress javascript config.assets.js_compressor gem 'uglifier' +##-- used for single column multiple binary flags in notification settings/feature flagging --## +gem 'flag_shih_tzu' +# Random name generator for user names +gem 'haikunator' +# Template parsing safetly +gem 'liquid' ##-- for active storage --## gem 'aws-sdk-s3', require: false @@ -67,8 +73,6 @@ gem 'twitty' gem 'koala' # slack client gem 'slack-ruby-client' -# Random name generator -gem 'haikunator' ##--- gems for debugging and error reporting ---## # static analysis @@ -79,9 +83,6 @@ gem 'sentry-raven' ##-- background job processing --## gem 'sidekiq' -##-- used for single column multiple binary flags in notification settings/feature flagging --## -gem 'flag_shih_tzu' - ##-- Push notification service --## gem 'fcm' gem 'webpush' @@ -96,6 +97,13 @@ group :development do gem 'json_refs', git: 'https://github.com/tzmfreedom/json_refs', ref: 'e32deb0' end +group :test do + # Cypress in rails. + gem 'cypress-on-rails', '~> 1.0' + # fast cleaning of database + gem 'database_cleaner' +end + group :development, :test do # locking until https://github.com/codeclimate/test-reporter/issues/418 is resolved gem 'action-cable-testing' diff --git a/Gemfile.lock b/Gemfile.lock index 40e05f084..00e93c5e1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -146,6 +146,9 @@ GEM crack (0.4.3) safe_yaml (~> 1.0.0) crass (1.0.6) + cypress-on-rails (1.7.0) + rack + database_cleaner (1.8.5) datetime_picker_rails (0.0.7) momentjs-rails (>= 2.8.1) declarative (0.0.10) @@ -272,6 +275,7 @@ GEM addressable (~> 2.7) letter_opener (1.7.0) launchy (~> 2.2) + liquid (4.0.3) listen (3.2.1) rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) @@ -560,6 +564,8 @@ DEPENDENCIES bullet bundle-audit byebug + cypress-on-rails (~> 1.0) + database_cleaner devise devise_token_auth dotenv-rails @@ -579,6 +585,7 @@ DEPENDENCIES kaminari koala letter_opener + liquid listen mini_magick mock_redis! diff --git a/Procfile.test b/Procfile.test new file mode 100644 index 000000000..760852e80 --- /dev/null +++ b/Procfile.test @@ -0,0 +1,3 @@ +backend: RAILS_ENV=test bin/rails s -p 5050 +frontend: bin/webpack-dev-server +worker: RAILS_ENV=test bundle exec sidekiq -C config/sidekiq.yml diff --git a/README.md b/README.md index 75a254058..8d9fef405 100644 --- a/README.md +++ b/README.md @@ -27,9 +27,9 @@ ___ ## Background -Chatwoot is a customer support tool for instant messaging channels which can help businesses to provide exceptional customer support. The development of Chatwoot started in 2016 and it failed to succeed as a business and eventually shut the shop in 2017. During 2019 #Hacktoberfest, the maintainers decided to make it opensource instead of letting the code rust in a private repo. With a pleasant surprise, Chatwoot became a trending project on Hacker News and best of all, got lots of love from the community. +Chatwoot is a customer support tool for instant messaging channels which can help businesses provide exceptional customer support. The development of Chatwoot started in 2016. It failed to succeed as a business and eventually shut up shop in 2017. During 2019 #Hacktoberfest, the maintainers decided to make it opensource, instead of letting the code rust in a private repo. With a pleasant surprise, Chatwoot became a trending project on Hacker News and best of all, got lots of love from the community. -Now, a failed project is back on track and the prospects are looking great. The team is back to working on the project and we are building it in the open. Thanks to the ideas and contributions from the community. +Now, a failed project is back on track and the prospects are looking great. The team is back to working on the project and this time, we are building it in the open. Thanks to the ideas and contributions from the community. ## Documentation @@ -39,12 +39,12 @@ You can find the quick setup docs [here](https://www.chatwoot.com/docs/quick-set ## Branching model -We use [git-flow](https://nvie.com/posts/a-successful-git-branching-model/) branching model. The base branch is `develop`. +We use the [git-flow](https://nvie.com/posts/a-successful-git-branching-model/) branching model. The base branch is `develop`. If you are looking for a stable version, please use the `master` or tags labelled as `v1.x.x`. ## Heroku one-click deploy -Deploying chatwoot to heroku, it's a breeze. It's as simple as clicking this button. +Deploying chatwoot to heroku is a breeze. It's as simple as clicking this button: [![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/chatwoot/chatwoot/tree/master) @@ -54,7 +54,7 @@ Follow this [link](https://www.chatwoot.com/docs/environment-variables) to under Follow our [docker development guide](https://www.chatwoot.com/docs/installation-guide-docker) to develop and debug the application using `docker-compose`. -Follow our [environment variables](https://www.chatwoot.com/docs/environment-variables/) guide to setup environment for Docker. +Follow our [environment variables](https://www.chatwoot.com/docs/environment-variables/) guide to setup the environment for Docker. ## Contributors ✨ diff --git a/app/assets/stylesheets/administrate/components/_navigation.scss b/app/assets/stylesheets/administrate/components/_navigation.scss index f6b1a641d..1e7e35d25 100644 --- a/app/assets/stylesheets/administrate/components/_navigation.scss +++ b/app/assets/stylesheets/administrate/components/_navigation.scss @@ -1,6 +1,6 @@ .logo-brand { margin-bottom: $space-normal; - padding: $space-normal $space-smaller; + padding: $space-normal $space-smaller $space-small; text-align: center; } @@ -70,3 +70,9 @@ left: $space-normal; position: fixed; } + +.app-version { + color: $color-gray; + font-size: $font-size-small; + padding-top: $space-smaller; +} diff --git a/app/assets/stylesheets/administrate/reset/_normalize.scss b/app/assets/stylesheets/administrate/reset/_normalize.scss index fa4e73dd4..01f77c433 100644 --- a/app/assets/stylesheets/administrate/reset/_normalize.scss +++ b/app/assets/stylesheets/administrate/reset/_normalize.scss @@ -13,6 +13,7 @@ html { line-height: 1.15; /* 1 */ -ms-text-size-adjust: 100%; /* 2 */ -webkit-text-size-adjust: 100%; /* 2 */ + -moz-osx-font-smoothing: grayscale; } /* Sections diff --git a/app/builders/account_builder.rb b/app/builders/account_builder.rb index 1c7c1f63d..ae6db3b4e 100644 --- a/app/builders/account_builder.rb +++ b/app/builders/account_builder.rb @@ -2,15 +2,18 @@ class AccountBuilder include CustomExceptions::Account - pattr_initialize [:account_name!, :email!, :confirmed!] + pattr_initialize [:account_name!, :email!, :confirmed!, :user] def perform - validate_email - validate_user + if @user.nil? + validate_email + validate_user + end ActiveRecord::Base.transaction do @account = create_account @user = create_and_link_user end + [@user, @account] rescue StandardError => e @account&.destroy puts e.inspect @@ -42,13 +45,7 @@ class AccountBuilder end def create_and_link_user - password = Time.now.to_i - @user = User.new(email: @email, - password: password, - password_confirmation: password, - name: email_to_name(@email)) - @user.confirm if @confirmed - if @user.save! + if @user.present? || create_user link_user_to_account(@user, @account) @user else @@ -68,4 +65,14 @@ class AccountBuilder name = email[/[^@]+/] name.split('.').map(&:capitalize).join(' ') end + + def create_user + password = Time.now.to_i + @user = User.new(email: @email, + password: password, + password_confirmation: password, + name: email_to_name(@email)) + @user.confirm if @confirmed + @user.save! + end end diff --git a/app/builders/messages/facebook/message_builder.rb b/app/builders/messages/facebook/message_builder.rb new file mode 100644 index 000000000..0095b9e1b --- /dev/null +++ b/app/builders/messages/facebook/message_builder.rb @@ -0,0 +1,139 @@ +# This class creates both outgoing messages from chatwoot and echo outgoing messages based on the flag `outgoing_echo` +# Assumptions +# 1. Incase of an outgoing message which is echo, source_id will NOT be nil, +# based on this we are showing "not sent from chatwoot" message in frontend +# Hence there is no need to set user_id in message for outgoing echo messages. + +class Messages::Facebook::MessageBuilder + attr_reader :response + + def initialize(response, inbox, outgoing_echo = false) + @response = response + @inbox = inbox + @sender_id = (outgoing_echo ? @response.recipient_id : @response.sender_id) + @message_type = (outgoing_echo ? :outgoing : :incoming) + end + + def perform + ActiveRecord::Base.transaction do + build_contact + build_message + end + rescue StandardError => e + Raven.capture_exception(e) + true + end + + 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)) + ContactAvatarJob.perform_later(@contact, contact_params[:remote_avatar_url]) if contact_params[:remote_avatar_url] + @contact_inbox = ContactInbox.create(contact: contact, inbox: @inbox, source_id: @sender_id) + end + + def build_message + @message = conversation.messages.create!(message_params) + (response.attachments || []).each do |attachment| + attachment_obj = @message.attachments.new(attachment_params(attachment).except(:remote_file_url)) + attachment_obj.save! + attach_file(attachment_obj, attachment_params(attachment)[:remote_file_url]) if attachment_params(attachment)[:remote_file_url] + end + end + + def attach_file(attachment, file_url) + file_resource = LocalResource.new(file_url) + attachment.file.attach(io: file_resource.file, filename: file_resource.tmp_filename, content_type: file_resource.encoding) + 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 + )) + end + + def attachment_params(attachment) + file_type = attachment['type'].to_sym + params = { file_type: file_type, account_id: @message.account_id } + + if [:image, :file, :audio, :video].include? file_type + params.merge!(file_type_params(attachment)) + elsif file_type == :location + params.merge!(location_params(attachment)) + elsif file_type == :fallback + params.merge!(fallback_params(attachment)) + end + + params + end + + def file_type_params(attachment) + { + external_url: attachment['payload']['url'], + remote_file_url: attachment['payload']['url'] + } + end + + def location_params(attachment) + lat = attachment['payload']['coordinates']['lat'] + long = attachment['payload']['coordinates']['long'] + { + external_url: attachment['url'], + coordinates_lat: lat, + coordinates_long: long, + fallback_title: attachment['title'] + } + end + + def fallback_params(attachment) + { + fallback_title: attachment['title'], + external_url: attachment['url'] + } + end + + def conversation_params + { + account_id: @inbox.account_id, + inbox_id: @inbox.id, + contact_id: contact.id + } + end + + def message_params + { + account_id: conversation.account_id, + inbox_id: conversation.inbox_id, + message_type: @message_type, + content: response.content, + source_id: response.identifier, + sender: contact + } + end + + def contact_params + begin + k = Koala::Facebook::API.new(@inbox.channel.page_access_token) if @inbox.facebook? + result = k.get_object(@sender_id) || {} + rescue StandardError => e + result = {} + Raven.capture_exception(e) + end + { + name: "#{result['first_name'] || 'John'} #{result['last_name'] || 'Doe'}", + account_id: @inbox.account_id, + remote_avatar_url: result['profile_pic'] || '' + } + end +end diff --git a/app/builders/messages/incoming_message_builder.rb b/app/builders/messages/incoming_message_builder.rb deleted file mode 100644 index 3a01c703a..000000000 --- a/app/builders/messages/incoming_message_builder.rb +++ /dev/null @@ -1,2 +0,0 @@ -class Messages::IncomingMessageBuilder < Messages::MessageBuilder -end diff --git a/app/builders/messages/message_builder.rb b/app/builders/messages/message_builder.rb index a8251edb4..24f2f3aa6 100644 --- a/app/builders/messages/message_builder.rb +++ b/app/builders/messages/message_builder.rb @@ -1,139 +1,59 @@ -# This class creates both outgoing messages from chatwoot and echo outgoing messages based on the flag `outgoing_echo` -# Assumptions -# 1. Incase of an outgoing message which is echo, source_id will NOT be nil, -# based on this we are showing "not sent from chatwoot" message in frontend -# Hence there is no need to set user_id in message for outgoing echo messages. - class Messages::MessageBuilder - attr_reader :response + include ::FileTypeHelper + attr_reader :message - def initialize(response, inbox, outgoing_echo = false) - @response = response - @inbox = inbox - @sender_id = (outgoing_echo ? @response.recipient_id : @response.sender_id) - @message_type = (outgoing_echo ? :outgoing : :incoming) + def initialize(user, conversation, params) + @content = params[:content] + @private = params[:private] || false + @conversation = conversation + @user = user + @message_type = params[:message_type] || 'outgoing' + @content_type = params[:content_type] + @items = params.to_unsafe_h&.dig(:content_attributes, :items) + @attachments = params[:attachments] + @in_reply_to = params.to_unsafe_h&.dig(:content_attributes, :in_reply_to) end def perform - ActiveRecord::Base.transaction do - build_contact - build_message + @message = @conversation.messages.build(message_params) + if @attachments.present? + @attachments.each do |uploaded_attachment| + attachment = @message.attachments.new( + account_id: @message.account_id, + file_type: file_type(uploaded_attachment&.content_type) + ) + attachment.file.attach(uploaded_attachment) + end end - rescue StandardError => e - Raven.capture_exception(e) - true + @message.save + @message end 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)) - ContactAvatarJob.perform_later(@contact, contact_params[:remote_avatar_url]) if contact_params[:remote_avatar_url] - @contact_inbox = ContactInbox.create(contact: contact, inbox: @inbox, source_id: @sender_id) - end - - def build_message - @message = conversation.messages.create!(message_params) - (response.attachments || []).each do |attachment| - attachment_obj = @message.attachments.new(attachment_params(attachment).except(:remote_file_url)) - attachment_obj.save! - attach_file(attachment_obj, attachment_params(attachment)[:remote_file_url]) if attachment_params(attachment)[:remote_file_url] - end - end - - def attach_file(attachment, file_url) - file_resource = LocalResource.new(file_url) - attachment.file.attach(io: file_resource.file, filename: file_resource.tmp_filename, content_type: file_resource.encoding) - 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 - )) - end - - def attachment_params(attachment) - file_type = attachment['type'].to_sym - params = { file_type: file_type, account_id: @message.account_id } - - if [:image, :file, :audio, :video].include? file_type - params.merge!(file_type_params(attachment)) - elsif file_type == :location - params.merge!(location_params(attachment)) - elsif file_type == :fallback - params.merge!(fallback_params(attachment)) + def message_type + if @conversation.inbox.channel_type != 'Channel::Api' && @message_type == 'incoming' + raise StandardError, 'Incoming messages are only allowed in Api inboxes' end - params + @message_type end - def file_type_params(attachment) - { - external_url: attachment['payload']['url'], - remote_file_url: attachment['payload']['url'] - } - end - - def location_params(attachment) - lat = attachment['payload']['coordinates']['lat'] - long = attachment['payload']['coordinates']['long'] - { - external_url: attachment['url'], - coordinates_lat: lat, - coordinates_long: long, - fallback_title: attachment['title'] - } - end - - def fallback_params(attachment) - { - fallback_title: attachment['title'], - external_url: attachment['url'] - } - end - - def conversation_params - { - account_id: @inbox.account_id, - inbox_id: @inbox.id, - contact_id: contact.id - } + def sender + message_type == 'outgoing' ? @user : @conversation.contact end def message_params { - account_id: conversation.account_id, - inbox_id: conversation.inbox_id, - message_type: @message_type, - content: response.content, - source_id: response.identifier, - sender: contact - } - end - - def contact_params - begin - k = Koala::Facebook::API.new(@inbox.channel.page_access_token) if @inbox.facebook? - result = k.get_object(@sender_id) || {} - rescue Exception => e - result = {} - Raven.capture_exception(e) - end - { - name: "#{result['first_name'] || 'John'} #{result['last_name'] || 'Doe'}", - account_id: @inbox.account_id, - remote_avatar_url: result['profile_pic'] || '' + account_id: @conversation.account_id, + inbox_id: @conversation.inbox_id, + message_type: message_type, + content: @content, + private: @private, + sender: sender, + content_type: @content_type, + items: @items, + in_reply_to: @in_reply_to } end end diff --git a/app/builders/messages/outgoing/echo_builder.rb b/app/builders/messages/outgoing/echo_builder.rb deleted file mode 100644 index 7d5c3fa79..000000000 --- a/app/builders/messages/outgoing/echo_builder.rb +++ /dev/null @@ -1,2 +0,0 @@ -class Messages::Outgoing::EchoBuilder < ::Messages::MessageBuilder -end diff --git a/app/builders/messages/outgoing/normal_builder.rb b/app/builders/messages/outgoing/normal_builder.rb deleted file mode 100644 index 78572b0ee..000000000 --- a/app/builders/messages/outgoing/normal_builder.rb +++ /dev/null @@ -1,46 +0,0 @@ -class Messages::Outgoing::NormalBuilder - include ::FileTypeHelper - attr_reader :message - - def initialize(user, conversation, params) - @content = params[:content] - @private = params[:private] || false - @conversation = conversation - @user = user - @fb_id = params[:fb_id] - @content_type = params[:content_type] - @items = params.to_unsafe_h&.dig(:content_attributes, :items) - @attachments = params[:attachments] - end - - def perform - @message = @conversation.messages.build(message_params) - if @attachments.present? - @attachments.each do |uploaded_attachment| - attachment = @message.attachments.new( - account_id: @message.account_id, - file_type: file_type(uploaded_attachment&.content_type) - ) - attachment.file.attach(uploaded_attachment) - end - end - @message.save - @message - end - - private - - def message_params - { - account_id: @conversation.account_id, - inbox_id: @conversation.inbox_id, - message_type: :outgoing, - content: @content, - private: @private, - sender: @user, - source_id: @fb_id, - content_type: @content_type, - items: @items - } - end -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 new file mode 100644 index 000000000..7875b0f10 --- /dev/null +++ b/app/controllers/api/v1/accounts/contacts/contact_inboxes_controller.rb @@ -0,0 +1,26 @@ +class Api::V1::Accounts::Contacts::ContactInboxesController < Api::V1::Accounts::BaseController + before_action :ensure_contact + before_action :ensure_inbox, only: [:create] + before_action :validate_channel_type + + def create + source_id = params[:source_id] || SecureRandom.uuid + @contact_inbox = ContactInbox.create(contact: @contact, inbox: @inbox, source_id: source_id) + end + + private + + def validate_channel_type + return if @inbox.channel_type == 'Channel::Api' + + render json: { error: 'Contact Inbox creation is only allowed in API inboxes' }, status: :unprocessable_entity + end + + def ensure_inbox + @inbox = Current.account.inboxes.find(params[:inbox_id]) + end + + def ensure_contact + @contact = Current.account.contacts.find(params[:contact_id]) + end +end diff --git a/app/controllers/api/v1/accounts/contacts_controller.rb b/app/controllers/api/v1/accounts/contacts_controller.rb index 2208a2cf6..eef50ef11 100644 --- a/app/controllers/api/v1/accounts/contacts_controller.rb +++ b/app/controllers/api/v1/accounts/contacts_controller.rb @@ -11,21 +11,37 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController def show; end def create - @contact = Current.account.contacts.new(contact_create_params) - @contact.save! - render json: @contact + ActiveRecord::Base.transaction do + @contact = Current.account.contacts.new(contact_create_params) + @contact.save! + @contact_inbox = build_contact_inbox + end end def update @contact.update!(contact_params) end + def search + render json: { error: 'Specify search string with parameter q' }, status: :unprocessable_entity if params[:q].blank? && return + + @contacts = Current.account.contacts.where('name LIKE :search OR email LIKE :search', search: "%#{params[:q]}%") + end + private def check_authorization authorize(Contact) end + def build_contact_inbox + 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) + end + def contact_params params.require(:contact).permit(:name, :email, :phone_number) end diff --git a/app/controllers/api/v1/accounts/conversations/messages_controller.rb b/app/controllers/api/v1/accounts/conversations/messages_controller.rb index c4d595771..54cf7c595 100644 --- a/app/controllers/api/v1/accounts/conversations/messages_controller.rb +++ b/app/controllers/api/v1/accounts/conversations/messages_controller.rb @@ -5,8 +5,10 @@ class Api::V1::Accounts::Conversations::MessagesController < Api::V1::Accounts:: def create user = current_user || @resource - mb = Messages::Outgoing::NormalBuilder.new(user, @conversation, params) + mb = Messages::MessageBuilder.new(user, @conversation, params) @message = mb.perform + rescue StandardError => e + render_could_not_create_error(e.message) end private diff --git a/app/controllers/api/v1/accounts/conversations_controller.rb b/app/controllers/api/v1/accounts/conversations_controller.rb index 8d8099fdc..74f206e00 100644 --- a/app/controllers/api/v1/accounts/conversations_controller.rb +++ b/app/controllers/api/v1/accounts/conversations_controller.rb @@ -44,9 +44,8 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro end def update_last_seen - @conversation.agent_last_seen_at = parsed_last_seen_at + @conversation.agent_last_seen_at = DateTime.now.utc @conversation.save! - head :ok end private @@ -56,10 +55,6 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro Rails.configuration.dispatcher.dispatch(event, Time.zone.now, conversation: @conversation, user: user) end - def parsed_last_seen_at - DateTime.strptime(params[:agent_last_seen_at].to_s, '%s') - end - def conversation @conversation ||= Current.account.conversations.find_by(display_id: params[:id]) end diff --git a/app/controllers/api/v1/accounts/inboxes_controller.rb b/app/controllers/api/v1/accounts/inboxes_controller.rb index 45a190bfb..9360b700f 100644 --- a/app/controllers/api/v1/accounts/inboxes_controller.rb +++ b/app/controllers/api/v1/accounts/inboxes_controller.rb @@ -4,12 +4,12 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController before_action :check_authorization def index - @inboxes = policy_scope(Current.account.inboxes) + @inboxes = policy_scope(Current.account.inboxes.order_by_id.includes(:channel, :avatar_attachment)) end def create ActiveRecord::Base.transaction do - channel = web_widgets.create!(permitted_params[:channel].except(:type)) if permitted_params[:channel][:type] == 'web_widget' + channel = create_channel @inbox = Current.account.inboxes.build( name: permitted_params[:name], greeting_message: permitted_params[:greeting_message], @@ -23,7 +23,10 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController def update @inbox.update(inbox_update_params.except(:channel)) - @inbox.channel.update!(inbox_update_params[:channel]) if @inbox.channel.is_a?(Channel::WebWidget) && inbox_update_params[:channel].present? + return unless @inbox.channel.is_a?(Channel::WebWidget) && inbox_update_params[:channel].present? + + @inbox.channel.update!(inbox_update_params[:channel]) + update_channel_feature_flags end def set_agent_bot @@ -52,21 +55,43 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController @agent_bot = AgentBot.find(params[:agent_bot]) if params[:agent_bot] end - def web_widgets - Current.account.web_widgets - end - def check_authorization authorize(Inbox) end + def create_channel + case permitted_params[:channel][:type] + when 'web_widget' + Current.account.web_widgets.create!(permitted_params[:channel].except(:type)) + when 'api' + Current.account.api_channels.create!(permitted_params[:channel].except(:type)) + when 'email' + Current.account.email_channels.create!(permitted_params[:channel].except(:type)) + end + end + + def update_channel_feature_flags + return unless inbox_update_params[:channel].key? :selected_feature_flags + + @inbox.channel.selected_feature_flags = inbox_update_params[:channel][:selected_feature_flags] + @inbox.channel.save! + end + def permitted_params params.permit(:id, :avatar, :name, :greeting_message, :greeting_enabled, channel: - [:type, :website_url, :widget_color, :welcome_title, :welcome_tagline]) + [:type, :website_url, :widget_color, :welcome_title, :welcome_tagline, :webhook_url, :email]) end def inbox_update_params params.permit(:enable_auto_assignment, :name, :avatar, :greeting_message, :greeting_enabled, - channel: [:website_url, :widget_color, :welcome_title, :welcome_tagline]) + channel: [ + :website_url, + :widget_color, + :welcome_title, + :welcome_tagline, + :webhook_url, + :email, + selected_feature_flags: [] + ]) end end diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb index 3fb0509e3..933bd5a3a 100644 --- a/app/controllers/api/v1/accounts_controller.rb +++ b/app/controllers/api/v1/accounts_controller.rb @@ -14,14 +14,15 @@ class Api::V1::AccountsController < Api::BaseController with: :render_error_response def create - @user = AccountBuilder.new( + @user, @account = AccountBuilder.new( account_name: account_params[:account_name], email: account_params[:email], - confirmed: confirmed? + confirmed: confirmed?, + user: current_user ).perform if @user send_auth_headers(@user) - render partial: 'devise/auth.json', locals: { resource: @user } + render 'api/v1/accounts/create.json', locals: { resource: @user } else render_error_response(CustomExceptions::Account::SignupFailed.new({})) end @@ -32,7 +33,7 @@ class Api::V1::AccountsController < Api::BaseController end def update - @account.update!(account_params.slice(:name, :locale, :domain, :support_email, :domain_emails_enabled)) + @account.update!(account_params.slice(:name, :locale, :domain, :support_email)) end def update_active_at @@ -57,7 +58,7 @@ class Api::V1::AccountsController < Api::BaseController end def account_params - params.permit(:account_name, :email, :name, :locale, :domain, :support_email, :domain_emails_enabled) + params.permit(:account_name, :email, :name, :locale, :domain, :support_email) end def check_signup_enabled diff --git a/app/controllers/api/v1/profiles_controller.rb b/app/controllers/api/v1/profiles_controller.rb index 38f1a9b3b..bda3e8c90 100644 --- a/app/controllers/api/v1/profiles_controller.rb +++ b/app/controllers/api/v1/profiles_controller.rb @@ -16,6 +16,14 @@ class Api::V1::ProfilesController < Api::BaseController end def profile_params - params.require(:profile).permit(:email, :name, :password, :password_confirmation, :avatar, :availability) + params.require(:profile).permit( + :email, + :name, + :display_name, + :password, + :password_confirmation, + :avatar, + :availability + ) end end diff --git a/app/controllers/api/v1/widget/base_controller.rb b/app/controllers/api/v1/widget/base_controller.rb index 45cf3fc4f..4aa2ec138 100644 --- a/app/controllers/api/v1/widget/base_controller.rb +++ b/app/controllers/api/v1/widget/base_controller.rb @@ -30,4 +30,14 @@ class Api::V1::Widget::BaseController < ApplicationController ) @contact = @contact_inbox.contact end + + def browser_params + { + browser_name: browser.name, + browser_version: browser.full_version, + device_name: browser.device.name, + platform_name: browser.platform.name, + platform_version: browser.platform.version + } + end end diff --git a/app/controllers/api/v1/widget/events_controller.rb b/app/controllers/api/v1/widget/events_controller.rb index ea5326d6f..374541a10 100644 --- a/app/controllers/api/v1/widget/events_controller.rb +++ b/app/controllers/api/v1/widget/events_controller.rb @@ -11,7 +11,8 @@ class Api::V1::Widget::EventsController < Api::V1::Widget::BaseController def event_info { widget_language: params[:locale], - browser_language: browser.accept_language.first&.code + browser_language: browser.accept_language.first&.code, + browser: browser_params } end diff --git a/app/controllers/api/v1/widget/messages_controller.rb b/app/controllers/api/v1/widget/messages_controller.rb index 42cd313e2..afc0d71b2 100644 --- a/app/controllers/api/v1/widget/messages_controller.rb +++ b/app/controllers/api/v1/widget/messages_controller.rb @@ -66,16 +66,6 @@ class Api::V1::Widget::MessagesController < Api::V1::Widget::BaseController } end - def browser_params - { - browser_name: browser.name, - browser_version: browser.full_version, - device_name: browser.device.name, - platform_name: browser.platform.name, - platform_version: browser.platform.version - } - end - def timestamp_params { timestamp: permitted_params[:message][:timestamp] diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb index 272c2f546..27fa7fb86 100644 --- a/app/controllers/dashboard_controller.rb +++ b/app/controllers/dashboard_controller.rb @@ -15,7 +15,10 @@ class DashboardController < ActionController::Base 'WIDGET_BRAND_URL', 'TERMS_URL', 'PRIVACY_URL', - 'DISPLAY_MANIFEST' + 'DISPLAY_MANIFEST', + 'CREATE_NEW_ACCOUNT_FROM_DASHBOARD' + ).merge( + APP_VERSION: Chatwoot.config[:version] ) end end diff --git a/app/controllers/widgets_controller.rb b/app/controllers/widgets_controller.rb index c396de4ce..957203f16 100644 --- a/app/controllers/widgets_controller.rb +++ b/app/controllers/widgets_controller.rb @@ -11,7 +11,7 @@ class WidgetsController < ActionController::Base private def set_global_config - @global_config = GlobalConfig.get('LOGO_THUMBNAIL', 'INSTALLATION_NAME', 'WIDGET_BRAND_URL') + @global_config = GlobalConfig.get('LOGO_THUMBNAIL', 'BRAND_NAME', 'WIDGET_BRAND_URL') end def set_web_widget diff --git a/app/dashboards/user_dashboard.rb b/app/dashboards/user_dashboard.rb index 8ed132c61..f34f48257 100644 --- a/app/dashboards/user_dashboard.rb +++ b/app/dashboards/user_dashboard.rb @@ -24,7 +24,7 @@ class UserDashboard < Administrate::BaseDashboard confirmation_sent_at: Field::DateTime, unconfirmed_email: Field::String, name: Field::String, - nickname: Field::String, + display_name: Field::String, email: Field::String, tokens: Field::String.with_options(searchable: false), created_at: Field::DateTime, @@ -53,7 +53,7 @@ class UserDashboard < Administrate::BaseDashboard avatar_url unconfirmed_email name - nickname + display_name email created_at updated_at @@ -65,7 +65,7 @@ class UserDashboard < Administrate::BaseDashboard # on the model's form (`new` and `edit`) pages. FORM_ATTRIBUTES = %i[ name - nickname + display_name email password ].freeze diff --git a/app/drops/account_drop.rb b/app/drops/account_drop.rb new file mode 100644 index 000000000..2e3a2530f --- /dev/null +++ b/app/drops/account_drop.rb @@ -0,0 +1,2 @@ +class AccountDrop < BaseDrop +end diff --git a/app/drops/base_drop.rb b/app/drops/base_drop.rb new file mode 100644 index 000000000..e4b00e5d4 --- /dev/null +++ b/app/drops/base_drop.rb @@ -0,0 +1,13 @@ +class BaseDrop < Liquid::Drop + def initialize(obj) + @obj = obj + end + + def id + @obj.try(:id) + end + + def name + @obj.try(:name) + end +end diff --git a/app/drops/conversation_drop.rb b/app/drops/conversation_drop.rb new file mode 100644 index 000000000..03bab2f58 --- /dev/null +++ b/app/drops/conversation_drop.rb @@ -0,0 +1,5 @@ +class ConversationDrop < BaseDrop + def display_id + @obj.try(:display_id) + end +end diff --git a/app/drops/inbox_drop.rb b/app/drops/inbox_drop.rb new file mode 100644 index 000000000..7972e7325 --- /dev/null +++ b/app/drops/inbox_drop.rb @@ -0,0 +1,2 @@ +class InboxDrop < BaseDrop +end diff --git a/app/drops/user_drop.rb b/app/drops/user_drop.rb new file mode 100644 index 000000000..e2876a58a --- /dev/null +++ b/app/drops/user_drop.rb @@ -0,0 +1,2 @@ +class UserDrop < BaseDrop +end diff --git a/app/javascript/dashboard/api/account.js b/app/javascript/dashboard/api/account.js index 207420da6..82b0c434c 100644 --- a/app/javascript/dashboard/api/account.js +++ b/app/javascript/dashboard/api/account.js @@ -1,9 +1,14 @@ +/* global axios */ import ApiClient from './ApiClient'; class AccountAPI extends ApiClient { constructor() { super('', { accountScoped: true }); } + + createAccount(data) { + return axios.post(`${this.apiVersion}/accounts`, data); + } } export default new AccountAPI(); diff --git a/app/javascript/dashboard/api/auth.js b/app/javascript/dashboard/api/auth.js index 42d1fbc41..ae5031cd4 100644 --- a/app/javascript/dashboard/api/auth.js +++ b/app/javascript/dashboard/api/auth.js @@ -118,7 +118,12 @@ export default { return axios.post(urlData.url, { email }); }, - profileUpdate({ password, password_confirmation, ...profileAttributes }) { + profileUpdate({ + password, + password_confirmation, + displayName, + ...profileAttributes + }) { const formData = new FormData(); Object.keys(profileAttributes).forEach(key => { const value = profileAttributes[key]; @@ -126,6 +131,7 @@ export default { formData.append(`profile[${key}]`, value); } }); + formData.append('profile[display_name]', displayName || ''); if (password && password_confirmation) { formData.append('profile[password]', password); formData.append('profile[password_confirmation]', password_confirmation); diff --git a/app/javascript/dashboard/api/inbox/conversation.js b/app/javascript/dashboard/api/inbox/conversation.js index 3f1b2e199..5a9173a7a 100644 --- a/app/javascript/dashboard/api/inbox/conversation.js +++ b/app/javascript/dashboard/api/inbox/conversation.js @@ -29,10 +29,8 @@ class ConversationApi extends ApiClient { ); } - markMessageRead({ id, lastSeen }) { - return axios.post(`${this.url}/${id}/update_last_seen`, { - agent_last_seen_at: lastSeen, - }); + markMessageRead({ id }) { + return axios.post(`${this.url}/${id}/update_last_seen`); } toggleTyping({ conversationId, status }) { diff --git a/app/javascript/dashboard/api/inbox/message.js b/app/javascript/dashboard/api/inbox/message.js index c9681f685..07754fd10 100644 --- a/app/javascript/dashboard/api/inbox/message.js +++ b/app/javascript/dashboard/api/inbox/message.js @@ -7,10 +7,11 @@ class MessageApi extends ApiClient { super('conversations', { accountScoped: true }); } - create({ conversationId, message, private: isPrivate }) { + create({ conversationId, message, private: isPrivate, contentAttributes }) { return axios.post(`${this.url}/${conversationId}/messages`, { content: message, private: isPrivate, + content_attributes: contentAttributes, }); } @@ -20,9 +21,10 @@ class MessageApi extends ApiClient { }); } - sendAttachment([conversationId, { file }]) { + sendAttachment([conversationId, { file, isPrivate = false }]) { const formData = new FormData(); formData.append('attachments[]', file, file.name); + formData.append('private', isPrivate); return axios({ method: 'post', url: `${this.url}/${conversationId}/messages`, diff --git a/app/javascript/dashboard/assets/images/channels/api.png b/app/javascript/dashboard/assets/images/channels/api.png new file mode 100644 index 000000000..d9919fc21 Binary files /dev/null and b/app/javascript/dashboard/assets/images/channels/api.png differ diff --git a/app/javascript/dashboard/assets/images/channels/email.png b/app/javascript/dashboard/assets/images/channels/email.png new file mode 100644 index 000000000..304400e09 Binary files /dev/null and b/app/javascript/dashboard/assets/images/channels/email.png differ diff --git a/app/javascript/dashboard/assets/scss/_layout.scss b/app/javascript/dashboard/assets/scss/_layout.scss index cdb81c76a..7be546b01 100644 --- a/app/javascript/dashboard/assets/scss/_layout.scss +++ b/app/javascript/dashboard/assets/scss/_layout.scss @@ -1,11 +1,11 @@ html, body { - height: 100%; - width: 100%; - padding: 0; - margin: 0; - -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; + -webkit-font-smoothing: antialiased; + height: 100%; + margin: 0; + padding: 0; + width: 100%; } .app-wrapper { @@ -26,36 +26,40 @@ body { .view-box { @include full-height; - height: 100vh; @include margin(0); @include space-between-column; + + height: 100vh; } .view-panel { - flex-direction: column; @include margin($zero); @include padding($space-normal); + + flex-direction: column; overflow-y: auto; } .content-box { - overflow: auto; @include padding($space-normal); + + overflow: auto; } .back-button { @include flex; + align-items: center; color: $color-woot; + cursor: pointer; font-size: $font-size-default; font-weight: $font-weight-normal; margin-right: $space-normal; - cursor: pointer; - &:before { - vertical-align: text-bottom; - margin-right: $space-smaller; + &::before { font-size: $font-size-large; + margin-right: $space-small; + vertical-align: text-bottom; } } @@ -66,12 +70,14 @@ body { .no-items-error-message { @include flex; @include full-height; - justify-content: center; + align-items: center; flex-direction: column; + justify-content: center; img { - max-width: $space-mega; @include padding($space-one); + + max-width: $space-mega; } } diff --git a/app/javascript/dashboard/assets/scss/_variables.scss b/app/javascript/dashboard/assets/scss/_variables.scss index 1798d918b..c3cbe0c18 100644 --- a/app/javascript/dashboard/assets/scss/_variables.scss +++ b/app/javascript/dashboard/assets/scss/_variables.scss @@ -46,8 +46,8 @@ $color-gray: #6e6f73; $color-light-gray: #999a9b; $color-border: #e0e6ed; $color-border-light: #f0f4f5; -$color-background: #f4f6fb; $color-border-dark: #cad0d4; +$color-background: #f4f6fb; $color-background-light: #f9fafc; $color-white: #fff; $color-body: #3c4858; diff --git a/app/javascript/dashboard/assets/scss/app.scss b/app/javascript/dashboard/assets/scss/app.scss index 3a6f62972..3d01856ef 100644 --- a/app/javascript/dashboard/assets/scss/app.scss +++ b/app/javascript/dashboard/assets/scss/app.scss @@ -1,5 +1,7 @@ @import 'shared/assets/fonts/inter'; - +@import 'shared/assets/stylesheets/colors'; +@import 'shared/assets/stylesheets/spacing'; +@import 'shared/assets/stylesheets/font-size'; @import 'variables'; @import '~spinkit/scss/spinners/7-three-bounce'; diff --git a/app/javascript/dashboard/assets/scss/views/settings/inbox.scss b/app/javascript/dashboard/assets/scss/views/settings/inbox.scss index 05cfb9b9e..27ff6fb1f 100644 --- a/app/javascript/dashboard/assets/scss/views/settings/inbox.scss +++ b/app/javascript/dashboard/assets/scss/views/settings/inbox.scss @@ -202,7 +202,7 @@ } .settings--content { - @include margin($space-small $space-larger); + @include margin($space-small $space-large); .title { font-weight: $font-weight-medium; diff --git a/app/javascript/dashboard/assets/scss/widgets/_conversation-view.scss b/app/javascript/dashboard/assets/scss/widgets/_conversation-view.scss index 8c615a016..257ad16d7 100644 --- a/app/javascript/dashboard/assets/scss/widgets/_conversation-view.scss +++ b/app/javascript/dashboard/assets/scss/widgets/_conversation-view.scss @@ -8,21 +8,9 @@ font-weight: $font-weight-normal; position: relative; - .icon { - bottom: $space-smaller; - position: absolute; - right: $space-small; - } - .message-text__wrap { position: relative; - .time { - color: $color-primary-light; - display: block; - font-size: $font-size-micro; - line-height: 1.8; - } .link { color: $color-white; @@ -37,24 +25,10 @@ } } - .audio { - .time { - margin-top: -$space-two; - } - } - .image { cursor: pointer; position: relative; - .time { - bottom: $space-smaller; - color: $color-white; - position: absolute; - right: $space-small; - white-space: nowrap; - } - .modal-container { text-align: center; } @@ -74,30 +48,6 @@ width: 100%; } } - - .map { - @include flex; - flex-direction: column; - text-align: right; - - img { - @include padding($space-small); - max-height: 30rem; - max-width: 20rem; - } - - .time { - @include padding($space-small); - margin-left: -$space-smaller; - margin-top: -$space-two; - white-space: nowrap; - } - - .locname { - font-weight: $font-weight-medium; - padding: $space-smaller; - } - } } .conversations-sidebar { @@ -257,14 +207,6 @@ color: $color-body; margin-right: auto; - .time { - color: $color-light-gray; - } - - .image .time { - color: $color-white; - } - .link { color: $color-primary-dark; } @@ -321,10 +263,6 @@ right: $space-one; top: $space-smaller + $space-micro; } - - .time { - color: $color-light-gray; - } } } @@ -389,11 +327,6 @@ } } - .time { - color: $medium-gray; - font-size: $font-size-micro; - margin-left: $space-slab; - } } } diff --git a/app/javascript/dashboard/components/ModalHeader.vue b/app/javascript/dashboard/components/ModalHeader.vue index 9f930f983..9b80220a5 100644 --- a/app/javascript/dashboard/components/ModalHeader.vue +++ b/app/javascript/dashboard/components/ModalHeader.vue @@ -7,15 +7,25 @@

{{ headerContent }}

+ diff --git a/app/javascript/dashboard/components/buttons/FormSubmitButton.vue b/app/javascript/dashboard/components/buttons/FormSubmitButton.vue index d8934f675..9223ebcef 100644 --- a/app/javascript/dashboard/components/buttons/FormSubmitButton.vue +++ b/app/javascript/dashboard/components/buttons/FormSubmitButton.vue @@ -1,6 +1,7 @@ @@ -108,13 +160,16 @@ import SidebarItem from './SidebarItem'; import { frontendURL } from '../../helper/URLHelper'; import Thumbnail from '../widgets/Thumbnail'; import { getSidebarItems } from '../../i18n/default-sidebar'; +import { required, minLength } from 'vuelidate/lib/validators'; +import alertMixin from 'shared/mixins/alertMixin'; +// import accountMixin from '../../../../../mixins/account'; export default { components: { SidebarItem, Thumbnail, }, - mixins: [clickaway, adminMixin], + mixins: [clickaway, adminMixin, alertMixin], props: { route: { type: String, @@ -125,8 +180,18 @@ export default { return { showOptionsMenu: false, showAccountModal: false, + showCreateAccountModal: false, + accountName: '', + vertical: 'bottom', + horizontal: 'center', }; }, + validations: { + accountName: { + required, + minLength: minLength(1), + }, + }, computed: { ...mapGetters({ currentUser: 'getCurrentUser', @@ -134,8 +199,19 @@ export default { inboxes: 'inboxes/getInboxes', accountId: 'getCurrentAccountId', currentRole: 'getCurrentRole', + uiFlags: 'agents/getUIFlags', accountLabels: 'labels/getLabelsOnSidebar', }), + currentUserAvailableName() { + const { available_name: availableName } = this.currentUser; + return availableName; + }, + showChangeAccountOption() { + if (this.globalConfig.createNewAccountFromDashboard) { + return true; + } + return this.currentUser.accounts.length > 1; + }, sidemenuItems() { return getSidebarItems(this.accountId); }, @@ -230,6 +306,29 @@ export default { onClose() { this.showAccountModal = false; }, + createAccount() { + this.showAccountModal = false; + this.showCreateAccountModal = true; + }, + onCloseCreate() { + this.showCreateAccountModal = false; + }, + async addAccount() { + try { + const account_id = await this.$store.dispatch('accounts/create', { + account_name: this.accountName, + }); + this.onClose(); + this.showAlert(this.$t('CREATE_ACCOUNT.API.SUCCESS_MESSAGE')); + window.location = `/app/accounts/${account_id}/dashboard`; + } catch (error) { + if (error.response.status === 422) { + this.showAlert(this.$t('CREATE_ACCOUNT.API.EXIST_MESSAGE')); + } else { + this.showAlert(this.$t('CREATE_ACCOUNT.API.ERROR_MESSAGE')); + } + } + }, }, }; diff --git a/app/javascript/dashboard/components/layout/SidebarItem.vue b/app/javascript/dashboard/components/layout/SidebarItem.vue index 7ef2dfa88..e75b40619 100644 --- a/app/javascript/dashboard/components/layout/SidebarItem.vue +++ b/app/javascript/dashboard/components/layout/SidebarItem.vue @@ -57,13 +57,8 @@ import { mapGetters } from 'vuex'; import router from '../../routes'; import adminMixin from '../../mixins/isAdmin'; +import { INBOX_TYPES } from 'shared/mixins/inboxMixin'; -const INBOX_TYPES = { - WEB: 'Channel::WebWidget', - FB: 'Channel::FacebookPage', - TWITTER: 'Channel::TwitterProfile', - TWILIO: 'Channel::TwilioSms', -}; const getInboxClassByType = type => { switch (type) { case INBOX_TYPES.WEB: @@ -78,6 +73,12 @@ const getInboxClassByType = type => { case INBOX_TYPES.TWILIO: return 'ion-android-textsms'; + case INBOX_TYPES.API: + return 'ion-cloud'; + + case INBOX_TYPES.EMAIL: + return 'ion-email'; + default: return ''; } diff --git a/app/javascript/dashboard/components/ui/Tabs/Tabs.js b/app/javascript/dashboard/components/ui/Tabs/Tabs.js index 2164dbbb0..87abd41f7 100644 --- a/app/javascript/dashboard/components/ui/Tabs/Tabs.js +++ b/app/javascript/dashboard/components/ui/Tabs/Tabs.js @@ -1,5 +1,3 @@ -/* eslint no-unused-vars: ["error", { "args": "none" }] */ - export default { name: 'WootTabs', props: { @@ -8,7 +6,7 @@ export default { default: 0, }, }, - render(h) { + render() { const Tabs = this.$slots.default .filter( node => diff --git a/app/javascript/dashboard/components/ui/Tabs/TabsItem.js b/app/javascript/dashboard/components/ui/Tabs/TabsItem.vue similarity index 53% rename from app/javascript/dashboard/components/ui/Tabs/TabsItem.js rename to app/javascript/dashboard/components/ui/Tabs/TabsItem.vue index 3c57abdc3..99c53d24d 100644 --- a/app/javascript/dashboard/components/ui/Tabs/TabsItem.js +++ b/app/javascript/dashboard/components/ui/Tabs/TabsItem.vue @@ -1,7 +1,19 @@ -/* eslint no-unused-vars: ["error", { "args": "none" }] */ -/* eslint prefer-template: 0 */ -/* eslint no-console: 0 */ -/* eslint func-names: 0 */ + + diff --git a/app/javascript/dashboard/components/widgets/ChannelItem.vue b/app/javascript/dashboard/components/widgets/ChannelItem.vue index bd4636fd8..39fa2e028 100644 --- a/app/javascript/dashboard/components/widgets/ChannelItem.vue +++ b/app/javascript/dashboard/components/widgets/ChannelItem.vue @@ -1,35 +1,43 @@ @@ -37,7 +45,7 @@ export default { props: { channel: { - type: String, + type: Object, required: true, }, enabledFeatures: { @@ -45,22 +53,28 @@ export default { required: true, }, }, - methods: { - isActive(channel) { + computed: { + isActive() { + const { key } = this.channel; if (Object.keys(this.enabledFeatures) === 0) { return false; } - if (channel === 'facebook') { + if (key === 'facebook') { return this.enabledFeatures.channel_facebook; } - if (channel === 'twitter') { - return this.enabledFeatures.channel_facebook; + if (key === 'twitter') { + return this.enabledFeatures.channel_twitter; } - return ['website', 'twilio'].includes(channel); + if (key === 'email') { + return this.enabledFeatures.channel_email; + } + return ['website', 'twilio', 'api'].includes(key); }, + }, + methods: { onItemClick() { - if (this.isActive(this.channel)) { - this.$emit('channel-item-click', this.channel); + if (this.isActive) { + this.$emit('channel-item-click', this.channel.key); } }, }, diff --git a/app/javascript/dashboard/components/widgets/conversation/ConversationHeader.vue b/app/javascript/dashboard/components/widgets/conversation/ConversationHeader.vue index 7783e8c94..c5ce3d0be 100644 --- a/app/javascript/dashboard/components/widgets/conversation/ConversationHeader.vue +++ b/app/javascript/dashboard/components/widgets/conversation/ConversationHeader.vue @@ -25,7 +25,7 @@ -
+
No Chat {{ $t('CONVERSATION.404') }}
diff --git a/app/javascript/dashboard/components/widgets/conversation/Message.vue b/app/javascript/dashboard/components/widgets/conversation/Message.vue index 4ef1ddb4c..58d283acd 100644 --- a/app/javascript/dashboard/components/widgets/conversation/Message.vue +++ b/app/javascript/dashboard/components/widgets/conversation/Message.vue @@ -5,6 +5,7 @@ @@ -21,19 +22,29 @@ /> -

+ +
+ +
+ {{ sender.available_name || sender.name }} +
+
- - diff --git a/app/javascript/dashboard/components/widgets/conversation/MessagesView.vue b/app/javascript/dashboard/components/widgets/conversation/MessagesView.vue index 7a67b4e46..2689fe550 100644 --- a/app/javascript/dashboard/components/widgets/conversation/MessagesView.vue +++ b/app/javascript/dashboard/components/widgets/conversation/MessagesView.vue @@ -5,9 +5,38 @@ :is-contact-panel-open="isContactPanelOpen" @contactPanelToggle="onToggleContactPanel" /> + + +
    -
  • +
  • @@ -15,6 +44,7 @@ v-for="message in getReadMessages" :key="message.id" :data="message" + :is-a-tweet="isATweet" />
  • @@ -25,6 +55,7 @@ v-for="message in getUnReadMessages" :key="message.id" :data="message" + :is-a-tweet="isATweet" />
+ + diff --git a/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue b/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue index 499394184..58fdf8e58 100644 --- a/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue +++ b/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue @@ -17,7 +17,7 @@ ref="messageInput" v-model="message" class="input" - :placeholder="$t(messagePlaceHolder())" + :placeholder="messagePlaceHolder" :min-height="4" @focus="onFocus" @blur="onBlur" @@ -25,46 +25,43 @@ - - + +
- {{ readableTime }} diff --git a/app/javascript/dashboard/components/widgets/conversation/bubble/Text.vue b/app/javascript/dashboard/components/widgets/conversation/bubble/Text.vue index 1d266516c..1a8c44a6f 100644 --- a/app/javascript/dashboard/components/widgets/conversation/bubble/Text.vue +++ b/app/javascript/dashboard/components/widgets/conversation/bubble/Text.vue @@ -1,12 +1,24 @@ diff --git a/app/javascript/dashboard/constants.js b/app/javascript/dashboard/constants.js index 35559648c..9a00775bf 100644 --- a/app/javascript/dashboard/constants.js +++ b/app/javascript/dashboard/constants.js @@ -12,5 +12,6 @@ export default { STATUS_TYPE: { OPEN: 'open', RESOLVED: 'resolved', + BOT: 'bot', }, }; diff --git a/app/javascript/dashboard/helper/actionCable.js b/app/javascript/dashboard/helper/actionCable.js index 4efde4d91..1fb9961dc 100644 --- a/app/javascript/dashboard/helper/actionCable.js +++ b/app/javascript/dashboard/helper/actionCable.js @@ -1,6 +1,5 @@ import AuthAPI from '../api/auth'; import BaseActionCableConnector from '../../shared/helpers/BaseActionCableConnector'; -/* global bus */ class ActionCableConnector extends BaseActionCableConnector { constructor(app, pubsubToken) { diff --git a/app/javascript/dashboard/i18n/index.js b/app/javascript/dashboard/i18n/index.js index 0ed3f5667..f1c32cee3 100644 --- a/app/javascript/dashboard/i18n/index.js +++ b/app/javascript/dashboard/i18n/index.js @@ -9,6 +9,7 @@ import ml from './locale/ml'; import pt from './locale/pt'; import pt_BR from './locale/pt_BR'; import ro from './locale/ro'; +import fa from './locale/fa'; import ta from './locale/ta'; import it from './locale/it'; @@ -24,6 +25,7 @@ export default { pt_BR, pt, ro, + fa, ta, it, }; diff --git a/app/javascript/dashboard/i18n/locale/el/settings.json b/app/javascript/dashboard/i18n/locale/el/settings.json index 6a1c1e9ae..b8b641c2b 100644 --- a/app/javascript/dashboard/i18n/locale/el/settings.json +++ b/app/javascript/dashboard/i18n/locale/el/settings.json @@ -52,15 +52,15 @@ "LABEL": "Διαθεσιμότητα", "STATUSES_LIST": [ { - "value": "στη γραμμή", + "value": "online", "label": "Στην Γραμμή" }, { - "value": "απασχολημένος", + "value": "busy", "label": "Απασχολημένος" }, { - "value": "εκτός", + "value": "offline", "label": "Εκτός" } ] diff --git a/app/javascript/dashboard/i18n/locale/en/chatlist.json b/app/javascript/dashboard/i18n/locale/en/chatlist.json index 7b63efade..a0b31427f 100644 --- a/app/javascript/dashboard/i18n/locale/en/chatlist.json +++ b/app/javascript/dashboard/i18n/locale/en/chatlist.json @@ -10,7 +10,8 @@ "SEARCH": { "INPUT": "Search for People, Chats, Saved Replies .." }, - "STATUS_TABS": [{ + "STATUS_TABS": [ + { "NAME": "Open", "KEY": "openCount" }, @@ -19,8 +20,8 @@ "KEY": "allConvCount" } ], - - "ASSIGNEE_TYPE_TABS": [{ + "ASSIGNEE_TYPE_TABS": [ + { "NAME": "Mine", "KEY": "me", "COUNT_KEY": "mineCount" @@ -36,17 +37,20 @@ "COUNT_KEY": "allCount" } ], - - "CHAT_STATUS_ITEMS": [{ + "CHAT_STATUS_ITEMS": [ + { "TEXT": "Open", "VALUE": "open" }, { "TEXT": "Resolved", "VALUE": "resolved" + }, + { + "TEXT": "Bot", + "VALUE": "bot" } ], - "ATTACHMENTS": { "image": { "ICON": "ion-image", @@ -72,6 +76,9 @@ "ICON": "ion-link", "CONTENT": "has shared a url" } - } + }, + "RECEIVED_VIA_EMAIL": "Received via email", + "VIEW_TWEET_IN_TWITTER": "View tweet in Twitter", + "REPLY_TO_TWEET": "Reply to this tweet" } } diff --git a/app/javascript/dashboard/i18n/locale/en/conversation.json b/app/javascript/dashboard/i18n/locale/en/conversation.json index b07e4e1f1..ab86c648a 100644 --- a/app/javascript/dashboard/i18n/locale/en/conversation.json +++ b/app/javascript/dashboard/i18n/locale/en/conversation.json @@ -9,6 +9,11 @@ "CLICK_HERE": "Click here", "LOADING_INBOXES": "Loading inboxes", "LOADING_CONVERSATIONS": "Loading Conversations", + "CANNOT_REPLY": "You cannot reply due to", + "24_HOURS_WINDOW": "24 hour message window restriction", + "LAST_INCOMING_TWEET": "You are replying to the last incoming tweet", + "REPLYING_TO": "You are replying to:", + "REMOVE_SELECTION": "Remove Selection", "DOWNLOAD": "Download", "HEADER": { "RESOLVE_ACTION": "Resolve", diff --git a/app/javascript/dashboard/i18n/locale/en/generalSettings.json b/app/javascript/dashboard/i18n/locale/en/generalSettings.json index 31b5f9fe2..0a3fc0b8e 100644 --- a/app/javascript/dashboard/i18n/locale/en/generalSettings.json +++ b/app/javascript/dashboard/i18n/locale/en/generalSettings.json @@ -24,8 +24,8 @@ "ERROR": "" }, "DOMAIN": { - "LABEL": "Domain", - "PLACEHOLDER": "Your website domain", + "LABEL": "Incoming Email Domain", + "PLACEHOLDER": "The domain where you will receive the emails", "ERROR": "" }, "SUPPORT_EMAIL": { @@ -33,14 +33,9 @@ "PLACEHOLDER": "Your company's support email", "ERROR": "" }, - "ENABLE_DOMAIN_EMAIL": { - "LABEL": "Enable domain email", - "PLACEHOLDER": "Enable the custom domain email", - "ERROR": "", - "OPTIONS": { - "ENABLED": "Enabled", - "DISABLED": "Disabled" - } + "FEATURES": { + "INBOUND_EMAIL_ENABLED": "Conversation continuity with emails is enabled for your account.", + "CUSTOM_EMAIL_DOMAIN_ENABLED": "You can receive emails in your custom domain now." } } } diff --git a/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json b/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json index c885c9ab5..fb4c71134 100644 --- a/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json +++ b/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json @@ -115,6 +115,43 @@ "ERROR_MESSAGE": "We were not able to authenticate Twilio credentials, please try again" } }, + "API_CHANNEL": { + "TITLE": "API Channel", + "DESC": "Integrate with API channel and start supporting your customers.", + "CHANNEL_NAME": { + "LABEL": "Channel Name", + "PLACEHOLDER": "Please enter a channel name", + "ERROR": "This field is required" + }, + "WEBHOOK_URL": { + "LABEL": "Webhook URL", + "SUBTITLE": "Configure the URL where you want to recieve callbacks on events.", + "PLACEHOLDER": "Webhook URL" + }, + "SUBMIT_BUTTON": "Create API Channel", + "API": { + "ERROR_MESSAGE": "We were not able to save the api channel" + } + }, + "EMAIL_CHANNEL": { + "TITLE": "Email Channel", + "DESC": "Integrate you email inbox.", + "CHANNEL_NAME": { + "LABEL": "Channel Name", + "PLACEHOLDER": "Please enter a channel name", + "ERROR": "This field is required" + }, + "EMAIL": { + "LABEL": "Email", + "SUBTITLE": "Email where your customers sends you support tickets", + "PLACEHOLDER": "Email" + }, + "SUBMIT_BUTTON": "Create Email Channel", + "API": { + "ERROR_MESSAGE": "We were not able to save the email channel" + }, + "FINISH_MESSAGE": "Start forwarding your emails to the following email address." + }, "AUTH": { "TITLE": "Channels", "DESC": "Currently we support Website live chat widgets, Facebook Pages and Twitter profiles as platforms. We have more platforms like Whatsapp, Email, Telegram and Line in the works, which will be out soon." @@ -175,7 +212,17 @@ "ERROR_MESSAGE": "Could not delete inbox. Please try again later." } }, + "TABS": { + "SETTINGS": "Settings", + "COLLABORATORS": "Collaborators", + "CONFIGURATION": "Configuration" + }, "SETTINGS": "Settings", + "FEATURES": { + "LABEL": "Features", + "DISPLAY_FILE_PICKER": "Display file picker on the widget", + "DISPLAY_EMOJI_PICKER": "Display emoji picker on the widget" + }, "SETTINGS_POPUP": { "MESSENGER_HEADING": "Messenger Script", "MESSENGER_SUB_HEAD": "Place this button inside your body tag", diff --git a/app/javascript/dashboard/i18n/locale/en/settings.json b/app/javascript/dashboard/i18n/locale/en/settings.json index 6ab397d63..f39997b6e 100644 --- a/app/javascript/dashboard/i18n/locale/en/settings.json +++ b/app/javascript/dashboard/i18n/locale/en/settings.json @@ -44,9 +44,14 @@ "LABEL": "Profile Image" }, "NAME": { - "LABEL": "Your name", - "ERROR": "Please enter a valid name", - "PLACEHOLDER": "Please enter your name, this would be displayed in conversations" + "LABEL": "Your full name", + "ERROR": "Please enter a valid full name", + "PLACEHOLDER": "Please enter your full name" + }, + "DISPLAY_NAME": { + "LABEL": "Display name", + "ERROR": "Please enter a valid display name", + "PLACEHOLDER": "Please enter a display name, this would be displayed in conversations" }, "AVAILABILITY": { "LABEL": "Availability", @@ -122,5 +127,22 @@ "INTEGRATIONS": "Integrations", "ACCOUNT_SETTINGS": "Account Settings", "LABELS": "Labels" + }, + "CREATE_ACCOUNT": { + "NEW_ACCOUNT": "New Account", + "SELECTOR_SUBTITLE": "Create a new account", + "API": { + "SUCCESS_MESSAGE": "Account created successfully", + "EXIST_MESSAGE": "Account already exists", + "ERROR_MESSAGE": "Could not connect to Woot Server, Please try again later" + }, + "FORM": { + "NAME": { + "LABEL": "Account Name", + "PLACEHOLDER": "Wayne Enterprises" + }, + "SUBMIT": "Submit" + } + } } diff --git a/app/javascript/dashboard/i18n/locale/fa/agentMgmt.json b/app/javascript/dashboard/i18n/locale/fa/agentMgmt.json new file mode 100644 index 000000000..7e63ce7a9 --- /dev/null +++ b/app/javascript/dashboard/i18n/locale/fa/agentMgmt.json @@ -0,0 +1,102 @@ +{ + "AGENT_MGMT": { + "HEADER": "اپراتورها", + "HEADER_BTN_TXT": "اضافه کردن اپراتور", + "LOADING": "دریافت لیست اپراتورها", + "SIDEBAR_TXT": "

اپراتورها

یک اپراتور یکی از اعضای تیم پشتیبانی است.

اپراتورها می‌توانند پیام‌های کاربران را ببینند و به آن‌ها پاسخ بدهند. این لیست حاوی تمام اپراتورهایی است که در حساب شما تعریف شده اند.

با زدن روی دکمه اضافه کردن اپراتور می‌توانید یک اپراتور جدید معرفی کنید. به ایمیل اپراتوری که معرفی می‌کنید یک دعوتنامه ارسال می‌شود که بعد از پذیرفتن آن اپراتور می‌تواند به پیام‌های کاربران پاسخ بدهد.

بسته به سطح دسترسی تعیین شده یک اپراتور می‌تواند به بخش‌های مشخصی از اکانت دسترسی پیدا کند

اپراتور - اپراتورهایی که این نقش را داشته باشند تنها می‌توانند به صندوق‌های ورودی، گزارشات و گفتگوها دسترسی داشته باشند. آن‌ها می‌توانند یک مکالمه را به اپراتور دیگر یا خودشان تخصیص دهند و یا یک مکالمه را حل شده اعلام کنند.

مدیر - مدیران می‌توانند علاوه بر تمام بخش‌هایی که یک اپراتور دسترسی دارد، به تمام بخش‌هایی که در حساب کاربری شما وجود دارد دسترسی داشته باشند.

", + "AGENT_TYPES": [ + { + "name": "administrator", + "label": "مدیر" + }, + { + "name": "agent", + "label": "اپراتور" + } + ], + "LIST": { + "404": "در حال حاضر هیچ اپراتوری برای این حساب معرفی نشده است.", + "TITLE": "مدیریت اپراتورها", + "DESC": "می‌توانید به تیم‌تان اپراتور اضافه کرده یا اپراتورهای فعلی را حذف کنید", + "NAME": "نام", + "EMAIL": "ایمیل", + "STATUS": "وضعیت", + "ACTIONS": "عملیات", + "VERIFIED": "تایید شده", + "VERIFICATION_PENDING": "در انتظار تایید" + }, + "ADD": { + "TITLE": "اضافه کردن اپراتور به تیم", + "DESC": "می‌توانید افرادی را معرفی کنید که مسئول پشتیبانی آنلاین صندوق‌های ورودی باشند", + "CANCEL_BUTTON_TEXT": "انصراف", + "FORM": { + "NAME": { + "LABEL": "اسم اپراتور", + "PLACEHOLDER": "لطفا اسم اپراتور را وارد نمایید" + }, + "AGENT_TYPE": { + "LABEL": "نوع اپراتور", + "PLACEHOLDER": "لطفا نوع دسترسی اپراتور را مشخص کنید", + "ERROR": "تعیین کردن نوع اپراتور الزامی است" + }, + "EMAIL": { + "LABEL": "ایمیل", + "PLACEHOLDER": "لطفا آدرس ایمیل اپراتور را وارد نمایید" + }, + "SUBMIT": "اضافه کردن اپراتور" + }, + "API": { + "SUCCESS_MESSAGE": "اپراتور معرفی شد", + "EXIST_MESSAGE": "این اپراتور قبلا معرفی شده، لطفا ایمیل دیگری را امتحان کنید", + "ERROR_MESSAGE": "متاسفانه ارتباط با سرور برقرار نشد، مجددا امتحان کنید" + } + }, + "DELETE": { + "BUTTON_TEXT": "حذف", + "API": { + "SUCCESS_MESSAGE": "اپراتور حذف شد", + "ERROR_MESSAGE": "متاسفانه ارتباط با سرور برقرار نشد، مجددا امتحان کنید" + }, + "CONFIRM": { + "TITLE": "تاییدیه حذف", + "MESSAGE": "مطمئن هستید که حذف شود ", + "YES": "بله، حذف شود ", + "NO": "نه، بماند " + } + }, + "EDIT": { + "TITLE": "تغییر مشخصات اپراتور", + "FORM": { + "NAME": { + "LABEL": "اسم اپراتور", + "PLACEHOLDER": "لطفا اسم اپراتور را وارد کنید" + }, + "AGENT_TYPE": { + "LABEL": "نوع اپراتور", + "PLACEHOLDER": "لطفا نوع اپراتور را انتخاب کنید", + "ERROR": "تعیین کردن نوع اپراتور الزامی است" + }, + "EMAIL": { + "LABEL": "ایمیل", + "PLACEHOLDER": "لطفا ایمیل اپراتور را وارد کنید" + }, + "SUBMIT": "تغییر اپراتور" + }, + "BUTTON_TEXT": "ویرایش", + "CANCEL_BUTTON_TEXT": "انصراف", + "API": { + "SUCCESS_MESSAGE": "اطلاعات اپراتور تغییر یافت", + "ERROR_MESSAGE": "متاسفانه ارتباط با سرور برقرار نشد، مجددا امتحان کنید" + }, + "PASSWORD_RESET": { + "ADMIN_RESET_BUTTON": "تغییر رمز عبور", + "ADMIN_SUCCESS_MESSAGE": "یک ایمیل حاوی روش تغییر دادن رمز عبور برای اپراتور ارسال شد", + "SUCCESS_MESSAGE": "تغییر رمز عبور اپراتور با موفقیت انجام شد", + "ERROR_MESSAGE": "متاسفانه ارتباط با سرور برقرار نشد، مجددا امتحان کنید" + } + }, + "SEARCH": { + "NO_RESULTS": "اپراتوری یافت نشد." + } + } +} diff --git a/app/javascript/dashboard/i18n/locale/fa/cannedMgmt.json b/app/javascript/dashboard/i18n/locale/fa/cannedMgmt.json new file mode 100644 index 000000000..679b0b9fc --- /dev/null +++ b/app/javascript/dashboard/i18n/locale/fa/cannedMgmt.json @@ -0,0 +1,76 @@ +{ + "CANNED_MGMT": { + "HEADER": "پاسخ‌های آماده", + "HEADER_BTN_TXT": "اضافه کردن پاسخ آماده", + "LOADING": "دریافت پاسخ‌های آماده", + "SEARCH_404": "هیچ آیتمی با این مشخصات یافت نشد", + "SIDEBAR_TXT": "

پاسخ‌های آماده

پاسخ‌های آماده قالب‌هایی متنی هستند که برای جواب دادن سریع به یک گفتگو به کار می‌آیند.

برای ساختن یک جواب آماده، روی دکمه اضافه کردن جواب آماده کلیک کنید. همچنین با زدن روی دکمه «تغییر» یا «حذف» می‌توانید یک پاسخ آماده را تغییر داده یا حذف کنید.

پاسخ‌های آماده با استفاده و با کمک کدهای کوتاه ساخته شده‌اند. اپراتورها با زدن کلید '/' از صفحه کلید می‌توانند به لیست پاسخ‌های آماده دسترسی پیدا کنند.

", + "LIST": { + "404": "هیچ پاسخ آماده‌ای برای این حساب تعریف نشده است", + "TITLE": "مدیریت پاسخ‌های آماده", + "DESC": "پاسخ‌های آماده قالب‌های متنی پیش آماده‌ای هستند که برای پاسخگویی سریع به یک گفتگو می‌توانند مفید واقع شوند", + "TABLE_HEADER": [ + "کدهای کوتاه", + "محتوا", + "عملیات" + ] + }, + "ADD": { + "TITLE": "اضافه کردن پاسخ آماده", + "DESC": "پاسخ‌های آماده قالب‌های متنی پیش آماده‌ای هستند که برای پاسخگویی سریع به یک گفتگو می‌توانند مفید واقع شوند", + "CANCEL_BUTTON_TEXT": "انصراف", + "FORM": { + "SHORT_CODE": { + "LABEL": "کد کوتاه", + "PLACEHOLDER": "لطفا یک کد کوتاه وارد کنید", + "ERROR": "وجود کد کوتاه ضروری است" + }, + "CONTENT": { + "LABEL": "محتوا", + "PLACEHOLDER": "لطفا محتوای این پاسخ را تایپ کنید", + "ERROR": "محتوا ضروری است" + }, + "SUBMIT": "ثبت" + }, + "API": { + "SUCCESS_MESSAGE": "پاسخ آماده با موفقیت ثبت شد", + "ERROR_MESSAGE": "متاسفانه ارتباط با سرور برقرار نشد، مجددا امتحان کنید" + } + }, + "EDIT": { + "TITLE": "ویرایش پاسخ آماده", + "CANCEL_BUTTON_TEXT": "انصراف", + "FORM": { + "SHORT_CODE": { + "LABEL": "کد کوتاه", + "PLACEHOLDER": "لطفا یک کد کوتاه وارد کنید", + "ERROR": "وجود کد کوتاه ضروری است" + }, + "CONTENT": { + "LABEL": "محتوا", + "PLACEHOLDER": "لطفا محتوای این پاسخ را تایپ کنید", + "ERROR": "محتوا ضروری است" + }, + "SUBMIT": "ثبت" + }, + "BUTTON_TEXT": "ویرایش", + "API": { + "SUCCESS_MESSAGE": "پاسخ آماده تغییر داده شد", + "ERROR_MESSAGE": "متاسفانه ارتباط با سرور برقرار نشد، مجددا امتحان کنید" + } + }, + "DELETE": { + "BUTTON_TEXT": "حذف", + "API": { + "SUCCESS_MESSAGE": "پاسخ آماده با موفقیت حذف شد", + "ERROR_MESSAGE": "متاسفانه ارتباط با سرور برقرار نشد، مجددا امتحان کنید" + }, + "CONFIRM": { + "TITLE": "تاییدیه حذف", + "MESSAGE": "مطمئن هستید حذف شود؟ ", + "YES": "بله، حذف شود ", + "NO": "نه، بماند " + } + } + } +} diff --git a/app/javascript/dashboard/i18n/locale/fa/chatlist.json b/app/javascript/dashboard/i18n/locale/fa/chatlist.json new file mode 100644 index 000000000..19f0d9050 --- /dev/null +++ b/app/javascript/dashboard/i18n/locale/fa/chatlist.json @@ -0,0 +1,77 @@ +{ + "CHAT_LIST": { + "LOADING": "در حال دریافت گفتگوها", + "LOAD_MORE_CONVERSATIONS": "دریافت گفتگوهای بیشتر", + "EOF": "همه گفتگوها دریافت شدند 🎉", + "LIST": { + "404": "هیچ گفتگوی فعالی در این گروه نیست" + }, + "TAB_HEADING": "گفتگوها", + "SEARCH": { + "INPUT": "پیدا کردن افراد، گفتگوها و پاسخ‌های از پیش نوشته شده..." + }, + "STATUS_TABS": [{ + "NAME": "باز", + "KEY": "openCount" + }, + { + "NAME": "حل شده", + "KEY": "allConvCount" + } + ], + + "ASSIGNEE_TYPE_TABS": [{ + "NAME": "من", + "KEY": "me", + "COUNT_KEY": "mineCount" + }, + { + "NAME": "تخصیص داده نشده", + "KEY": "unassigned", + "COUNT_KEY": "unAssignedCount" + }, + { + "NAME": "همه", + "KEY": "all", + "COUNT_KEY": "allCount" + } + ], + + "CHAT_STATUS_ITEMS": [{ + "TEXT": "باز", + "VALUE": "open" + }, + { + "TEXT": "حل شده", + "VALUE": "resolved" + } + ], + + "ATTACHMENTS": { + "image": { + "ICON": "ion-image", + "CONTENT": "پیام تصویری" + }, + "audio": { + "ICON": "ion-volume-high", + "CONTENT": "پیام صوتی" + }, + "video": { + "ICON": "ion-ios-videocam", + "CONTENT": "پیام ویدیویی" + }, + "file": { + "ICON": "ion-document", + "CONTENT": "فایل الصاقی" + }, + "location": { + "ICON": "ion-ios-location", + "CONTENT": "Location" + }, + "fallback": { + "ICON": "ion-link", + "CONTENT": "یک آدرس URL به اشتراک گذاشته شده" + } + } + } +} diff --git a/app/javascript/dashboard/i18n/locale/fa/contact.json b/app/javascript/dashboard/i18n/locale/fa/contact.json new file mode 100644 index 000000000..f5270ff7a --- /dev/null +++ b/app/javascript/dashboard/i18n/locale/fa/contact.json @@ -0,0 +1,21 @@ +{ + "CONTACT_PANEL": { + "CONVERSATION_TITLE": "جزئیات مکالمه", + "BROWSER": "مرورگر", + "OS": "سیستم عامل", + "INITIATED_FROM": "شروع شده از", + "INITIATED_AT": "شروع شده در", + "CONVERSATIONS": { + "NO_RECORDS_FOUND": "این اولین گفتگوی این کاربر است", + "TITLE": "گفتگوهای قبلی" + }, + "LABELS": { + "TITLE": "برچسب‌های گفتگو", + "UPDATE_BUTTON": "تغییر برچسب‌ها", + "UPDATE_ERROR": "برچسب‌ها تغییری نکردند، لطفا بعدا امتحان کنید", + "TAG_PLACEHOLDER": "برچسب جدید", + "PLACEHOLDER": "پیدا کردن یا اضافه کردن برچسب" + }, + "MUTE_CONTACT": "بی‌صدا کردن گفتگو" + } +} diff --git a/app/javascript/dashboard/i18n/locale/fa/conversation.json b/app/javascript/dashboard/i18n/locale/fa/conversation.json new file mode 100644 index 000000000..b07e4e1f1 --- /dev/null +++ b/app/javascript/dashboard/i18n/locale/fa/conversation.json @@ -0,0 +1,35 @@ +{ + "CONVERSATION": { + "404": "Please select a conversation from left pane", + "NO_MESSAGE_1": "Uh oh! Looks like there are no messages from customers in your inbox.", + "NO_MESSAGE_2": " to send a message to your page!", + "NO_INBOX_1": "Hola! Looks like you haven't added any inboxes yet.", + "NO_INBOX_2": " to get started", + "NO_INBOX_AGENT": "Uh Oh! Looks like you are not part of any inbox. Please contact your administrator", + "CLICK_HERE": "Click here", + "LOADING_INBOXES": "Loading inboxes", + "LOADING_CONVERSATIONS": "Loading Conversations", + "DOWNLOAD": "Download", + "HEADER": { + "RESOLVE_ACTION": "Resolve", + "REOPEN_ACTION": "Reopen", + "OPEN": "More", + "CLOSE": "Close", + "DETAILS": "details" + }, + "FOOTER": { + "MSG_INPUT": "Shift + enter for new line. Start with '/' to select a Canned Response.", + "PRIVATE_MSG_INPUT": "Shift + enter for new line. This will be visible only to Agents" + }, + "REPLYBOX": { + "REPLY": "Reply", + "PRIVATE_NOTE": "Private Note", + "SEND": "Send", + "CREATE": "Add Note", + "TWEET": "Tweet" + }, + "VISIBLE_TO_AGENTS": "Private Note: Only visible to you and your team", + "CHANGE_STATUS": "Conversation status changed", + "CHANGE_AGENT": "Conversation Assignee changed" + } +} diff --git a/app/javascript/dashboard/i18n/locale/fa/generalSettings.json b/app/javascript/dashboard/i18n/locale/fa/generalSettings.json new file mode 100644 index 000000000..8d6a60a42 --- /dev/null +++ b/app/javascript/dashboard/i18n/locale/fa/generalSettings.json @@ -0,0 +1,46 @@ +{ + "GENERAL_SETTINGS": { + "TITLE": "تنظیمات حساب", + "SUBMIT": "تغییر تنظیمات", + "UPDATE": { + "ERROR": "تنظیمات تغییری نکرد، دوباره امتحان کنید!", + "SUCCESS": "تنظیمات با موفقیت اعمال شد" + }, + "FORM": { + "ERROR": "لطفا ایرادات فرم را برطرف کنید", + "GENERAL_SECTION": { + "TITLE": "تنظیمات عمومی", + "NOTE": "" + }, + "NAME": { + "LABEL": "عنوان حساب", + "PLACEHOLDER": "عنوان حساب شما", + "ERROR": "لطفا عنوان حساب را به درستی وارد نمایید" + }, + "LANGUAGE": { + "LABEL": "زبان سایت (آزمایشی)", + "PLACEHOLDER": "زبان نمایش المان‌های متنی سایت", + "ERROR": "" + }, + "DOMAIN": { + "LABEL": "دامنه", + "PLACEHOLDER": "دامنه سایت شما", + "ERROR": "" + }, + "SUPPORT_EMAIL": { + "LABEL": "ایمیل پشتیبانی", + "PLACEHOLDER": "ایمیل پشتیبانی شرکت شما", + "ERROR": "" + }, + "ENABLE_DOMAIN_EMAIL": { + "LABEL": "فعال کردن ایمیل دامنه", + "PLACEHOLDER": "فعال کردن ایمیل دامنه‌های اختصاصی", + "ERROR": "", + "OPTIONS": { + "ENABLED": "فعال", + "DISABLED": "غیرفعال" + } + } + } + } +} diff --git a/app/javascript/dashboard/i18n/locale/fa/inboxMgmt.json b/app/javascript/dashboard/i18n/locale/fa/inboxMgmt.json new file mode 100644 index 000000000..85c7ccf08 --- /dev/null +++ b/app/javascript/dashboard/i18n/locale/fa/inboxMgmt.json @@ -0,0 +1,191 @@ +{ + "INBOX_MGMT": { + "HEADER": "صندوق‌های ورودی", + "SIDEBAR_TXT": "

صندوق ورودی

وقتی چت ووت به یک وب سایت یا یک صفحه فیس بوک متصل شود به آن صندوق ورودی می‌گوید. شما در حساب چت ووت خود می‌توانید بی‌نهایت صندوق ورودی داشته باشید.

روی دکمه اضافه کردن صندوق ورودی کلیک کنید تا به یک وب سایت یا یک صفحه فیس بوک وصل شوید.

در داشبورد، می‌توانید گفتگوهای همه صندوق‌های ورودی را یکجا ببینید و در تب «گفتگوها» به آن‌ها پاسخ بدهید.

همچنین می‌توانید با کلیک کردن روی اسم صندوق ورودی از قسمت سمت چپ، فقط گفتگوهای همان صندوق را ببینید.

", + "LIST": { + "404": "برای این حساب هیچ صندوق ورودی معرفی نشده است." + }, + "CREATE_FLOW": [ + { + "title": "کانال ورودی را انتخاب کنید", + "route": "settings_inbox_new", + "body": "جایی که قرار است امکان گفتگوی آنلاین در آنجا فراهم شود را انتخاب کنید" + }, + { + "title": "ساخت صندوق ورودی", + "route": "settings_inboxes_page_channel", + "body": "به حساب کاربری وارد شوید و صندوق ورودی بسازید." + }, + { + "title": "معرفی اپراتور", + "route": "settings_inboxes_add_agents", + "body": "اپراتورها را به صندوق ورودی ساخته شده تخصیص می‌دهد" + }, + { + "title": "ماشالله!", + "route": "settings_inbox_finish", + "body": "دیگه می‌تونی بترکونی" + } + ], + "ADD": { + "FB": { + "HELP": "پانویس: با وارد شدن ما فقط به پیام‌های صفحه دسترسی پیدا می‌کنیم. پیام‌های خصوصی شما را هرگز نخواهیم دید", + "CHOOSE_PAGE": "انتخاب صفحه", + "CHOOSE_PLACEHOLDER": "از لیست صفحه مورد نظر را انتخاب کنید", + "INBOX_NAME": "عنوان صندوق ورودی", + "ADD_NAME": "یک اسم به صندوق ورودی خود اضافه کنید", + "PICK_NAME": "یک اسم برای صندوق ورودی خود انتخاب کنید", + "PICK_A_VALUE": "یک مقدار انتخاب کنید" + }, + "TWITTER": { + "HELP": "برای اضافه کردن امکان گفتگو از صفحه پروفایل توییترتان، لازم است با زدن دکمه `ورود با توییتر` پروفایل توییتر خود را شناسایی کنید' " + }, + "WEBSITE_CHANNEL": { + "TITLE": "کانال وب سایت", + "DESC": "یک کانال به وب سایت خود بسازید تا مشتریان بتوانند از طریق ویجت سایت با شما گفتگو کنند.", + "LOADING_MESSAGE": "در حال ساخت کانال پشتیبانی آنلاین سایت", + "CHANNEL_AVATAR": { + "LABEL": "آواتار کانال" + }, + "CHANNEL_NAME": { + "LABEL": "عنوان سایت", + "PLACEHOLDER": "عنوان سایت خود را وارد کنید (به عنوان مثال: Acme Inc)" + }, + "CHANNEL_DOMAIN": { + "LABEL": "دامنه سایت", + "PLACEHOLDER": "دامنه سایت خود را وارد کنید (به عنوان مثال: acme.com)" + }, + "CHANNEL_WELCOME_TITLE": { + "LABEL": "تیتر خوش آمدگویی", + "PLACEHOLDER": "سلام!" + }, + "CHANNEL_WELCOME_TAGLINE": { + "LABEL": "زیرتیتر خوش آمدگویی", + "PLACEHOLDER": "دسترسی به ما ساده است. هر سوالی پیش آمد همینجا از ما بپرسید." + }, + "CHANNEL_GREETING_MESSAGE": { + "LABEL": "پیام پاسخگویی کانال", + "PLACEHOLDER": "شرکت ما در اسرع وقت به پیام‌ها پاسخ می‌دهد" + }, + "CHANNEL_GREETING_TOGGLE": { + "LABEL": "فعال کردن پیام پاسخگویی", + "HELP_TEXT": "به محض اینکه کاربر گفتگویی را شروع کرد، پیام مشخصی در جواب او ارسال می‌شود", + "ENABLED": "فعال", + "DISABLED": "غیرفعال" + }, + "WIDGET_COLOR": { + "LABEL": "رنگ ویجت", + "PLACEHOLDER": "رنگی که در ویجت استفاده می‌شود را تعیین کنید" + }, + "SUBMIT_BUTTON": "ساختن صندوق ورودی" + }, + "TWILIO": { + "TITLE": "کانال اس ام اس تولیو/واتساپ", + "DESC": "به Twilio متصل شوید و مشتریان خود را از طریق پیامک یا واتساپ پشتیبانی کنید", + "ACCOUNT_SID": { + "LABEL": "شناسه SID", + "PLACEHOLDER": "لطفا شناسه SID حساب Twilio خود را وارد کنید", + "ERROR": "پر کردن این فیلد ضروری است" + }, + "CHANNEL_TYPE": { + "LABEL": "نوع کانال", + "ERROR": "لطفا نوع کانال را انتخاب کنید" + }, + "AUTH_TOKEN": { + "LABEL": "Auth توکن", + "PLACEHOLDER": "لطفا توکن Auth حساب Twilio خود را وارد کنید", + "ERROR": "پر کردن این فیلد ضروری است" + }, + "CHANNEL_NAME": { + "LABEL": "عنوان کانال", + "PLACEHOLDER": "لطفا اسم یک کانال را وارد کنید", + "ERROR": "پر کردن این فیلد ضروری است" + }, + "PHONE_NUMBER": { + "LABEL": "شماره تلفن", + "PLACEHOLDER": "لطفا شماره‌ای که پیام‌ می‌بایست به آن ارسال شود را وارد کنید", + "ERROR": "لطفا شماره تلفن را به شکل صحیح وارد کنید. شماره می‌بایست با کاراکتر `+` شروع شود" + }, + "API_CALLBACK": { + "TITLE": "آدرس URL مربوط به API", + "SUBTITLE": "لازم است آدرس Callback URL موجود در Twilio را به آنچه که در اینجا ذکر شده تنظیم کنید" + }, + "SUBMIT_BUTTON": "ساختن کانال Twilio", + "API": { + "ERROR_MESSAGE": "متاسفانه Twilio اطلاعات ارائه شده را تایید نمی کند، لطفا اصلاح و مجددا تلاش کنید" + } + }, + "AUTH": { + "TITLE": "کانال‌ها", + "DESC": "در حال حاضر ما ویجت‌های پشتیبانی آنلاین سایت‌ها، صفحات فیس بوک و پروفایل‌های توییتر را پشتیبانی می‌کنیم. پلتفرم‌های دیگری مثل واتساپ، ایمیل، تلگرام و لاین در برنامه کاری ما قرار دارد و به زودی آماده خواهند شد." + }, + "AGENTS": { + "TITLE": "اپراتورها", + "DESC": "در اینجا می‌توانید اپراتورها را به صندوق‌ ورودی خود اختصاص دهید. توجه داشته باشید که فقط اپراتورهایی که در اینجا معرفی شده باشند می‌توانند به پیام‌های این صندوق پاسخ بدهند.دیگر اپراتورها نخواهند توانست پیام‌های این صندوق را ببینید یا به آن‌ها پاسخی بدهند.
پانویس:به عنوان مدیر اگر می‌خواهید به همه صندوق‌های ورودی دسترسی داشته باشید می‌بایست خود را به عنوان اپراتور به همه صندوق‌ها اضافه کنید." + }, + "DETAILS": { + "TITLE": "جزئیات صندوق‌ ورودی", + "DESC": "از کادر زیر صفحه فیس بوکی که می‌خواهید به چت ووت متصل شود انتخاب کنید. همچنین می‌توانید برای تشخیص بهتر یک اسم مشخص برای این صندوق تعیین کنید." + }, + "FINISH": { + "TITLE": "ماشالله!", + "DESC": "چت ووت با موفقیت به فیس بوک متصل شد. از به بعد هر مشتری که پیامی در این صفحه بنویسید، آن پیام در صندوق ورودی چت ووت ظاهر می‌شود و گفتگویی ایجاد می‌شود..
فراموش نکنید که ما یک ویجت برای سایت‌ها هم داریم که با قرار دادن آن در سایت، مشتری می‌تواند به صورت آنلاین با شما گفتگو کند و پیام‌ها در صندوق ورودی آن ظاهر می‌شود
باحال نیست؟ ما همیشه باحالیم! :)" + } + }, + "DETAILS": { + "LOADING_FB": "در حال احراز هویت با فیس بوک...", + "ERROR_FB_AUTH": "اشکالی پیش آمد.. لطفا دوباره سعی کنید...", + "CREATING_CHANNEL": "در حال ساخت صندوق ورودی...", + "TITLE": "تنظیمات صفحه ورودی", + "DESC": "" + }, + "AGENTS": { + "BUTTON_TEXT": "اضافه کردن اپراتور", + "ADD_AGENTS": "اضافه کردن اپراتور به صندوق ورودی..." + }, + "FINISH": { + "TITLE": "صندوق ورودی حاضره!", + "MESSAGE": "حالا از طریق این کانال جدید می‌توانید با مشتریان صحبت کنید. به امید موفقیت ", + "BUTTON_TEXT": "نشانم بده", + "WEBSITE_SUCCESS": "ساختن کانال وب سایت با موفقیت انجام شد. قطعه کد زیر را کپی کرده و در سایت خود قرار دهید. در صورتیکه مشتری از ویجت پشتیبانی آنلاین استفاده کند گفتگوی شما در این صندوق ورودی ظاهر می‌شود." + }, + "REAUTH": "احراز هویت مجدد", + "VIEW": "نمایش", + "EDIT": { + "API": { + "SUCCESS_MESSAGE": "تنظمیات صندوق ورودی اعمال شد", + "AUTO_ASSIGNMENT_SUCCESS_MESSAGE": "وضعیت واگذاری خودکار گفتگو به اپراتورها تنظیم شد", + "ERROR_MESSAGE": "در حال حاضر امکان تغییر رنگ ویجت امکان‌پذیر نیست. لطفا بعدا امتحان کنید." + }, + "AUTO_ASSIGNMENT": { + "ENABLED": "فعال", + "DISABLED": "غیرفعال" + } + }, + "DELETE": { + "BUTTON_TEXT": "حذف", + "CONFIRM": { + "TITLE": "تاییدیه حذف", + "MESSAGE": "مطمئن هستید که حذف شود ", + "YES": "بله، حذف شود ", + "NO": "نه، بماند " + }, + "API": { + "SUCCESS_MESSAGE": "صندوق ورودی حذف شد", + "ERROR_MESSAGE": "صندوق ورودی حذف نشد، لطفا بعدا امتحان کنید" + } + }, + "SETTINGS": "تنظیمات", + "SETTINGS_POPUP": { + "MESSENGER_HEADING": "اسکریپت ویجت", + "MESSENGER_SUB_HEAD": "این دکمه را در تگ body قرار دهید", + "INBOX_AGENTS": "اپراتورها", + "INBOX_AGENTS_SUB_TEXT": "اضافه کردن یا حذف کردن دسترسی اپراتور به صندوق ورودی", + "UPDATE": "اعمال شود", + "AUTO_ASSIGNMENT": "فعال کردن واگذاری خودکار گفتگو به اپراتورها", + "INBOX_UPDATE_TITLE": "تنظیمات صندوق ورودی", + "INBOX_UPDATE_SUB_TEXT": "تغییر پارامترهای صندوق ورودی", + "AUTO_ASSIGNMENT_SUB_TEXT": "فعال کردن یا غیرفعال کردن واگذاری خودکار گفتگوها به اپراتورهای عضو این صندوق ورودی." + } + } +} diff --git a/app/javascript/dashboard/i18n/locale/fa/index.js b/app/javascript/dashboard/i18n/locale/fa/index.js new file mode 100644 index 000000000..558cc9b3a --- /dev/null +++ b/app/javascript/dashboard/i18n/locale/fa/index.js @@ -0,0 +1,32 @@ +/* eslint-disable */ +import { default as _agentMgmt } from './agentMgmt.json'; +import { default as _cannedMgmt } from './cannedMgmt.json'; +import { default as _chatlist } from './chatlist.json'; +import { default as _contact } from './contact.json'; +import { default as _conversation } from './conversation.json'; +import { default as _inboxMgmt } from './inboxMgmt.json'; +import { default as _login } from './login.json'; +import { default as _report } from './report.json'; +import { default as _resetPassword } from './resetPassword.json'; +import { default as _setNewPassword } from './setNewPassword.json'; +import { default as _settings } from './settings.json'; +import { default as _signup } from './signup.json'; +import { default as _integrations } from './integrations.json'; +import { default as _generalSettings } from './generalSettings.json'; + +export default { + ..._agentMgmt, + ..._cannedMgmt, + ..._chatlist, + ..._contact, + ..._conversation, + ..._inboxMgmt, + ..._login, + ..._report, + ..._resetPassword, + ..._setNewPassword, + ..._settings, + ..._signup, + ..._integrations, + ..._generalSettings, +}; diff --git a/app/javascript/dashboard/i18n/locale/fa/integrations.json b/app/javascript/dashboard/i18n/locale/fa/integrations.json new file mode 100644 index 000000000..5005c1847 --- /dev/null +++ b/app/javascript/dashboard/i18n/locale/fa/integrations.json @@ -0,0 +1,54 @@ +{ + "INTEGRATION_SETTINGS": { + "HEADER": "برنامه‌های تلفیق شده", + "WEBHOOK": { + "TITLE": "وب هوک", + "CONFIGURE": "تنظیمات", + "HEADER": "تنظیمات وب هوک", + "HEADER_BTN_TXT": "اضافه کردن یک وب هوک جدید", + "INTEGRATION_TXT": "وب هوک، امکان دسترسی لحظه‌ای به اطلاعات حساب چت وت شما را فراهم می‌کند. وب هوک‌ها را می‌توان به دیگر ابزارها مثل اسلک یا گیت‌هاب متصل کرد. دکمه تنظیمات را بزنید تا وب هوک خود را تنظیم کنید ", + "LOADING": "درحال دریافت اطلاعات وب هوک", + "SEARCH_404": "هیچ گزینه‌ای با این شرایط پیدا نشد", + "SIDEBAR_TXT": "

وب هوک‌ها

وب هوک‌ها اجرا کننده‌ی درخواست‌های HTTP هستند که برای هر حسابی قابل تنظیم شدن هستند. به عنوان مثال می‌توان وقتی گفتگوی جدیدی ایجاد شد یک وب سرویس صدا زده شود. برای هر حساب می‌توان چند وب هوک ایجاد کرد.

برای ساختن یک وب هوک, روی دکمه اضافه کردن وب هوک جدید کلیک کنید. همچنین با زدن دکمه «حذف» می‌توانید وب هوک ساخته شده را حذف کنید.

", + "LIST": { + "404": "هیچ وب هوکی برای این حساب ساخته نشده است", + "TITLE": "مدیریت وب هوک‌ها", + "DESC": "وب هوک‌ها قالب‌های پاسخگویی آماده‌ای هستند که می‌توانند برای پاسخگویی سریع به تیکت‌ها به کار گرفته شوند.", + "TABLE_HEADER": [ + "آدرس مقصد وب هوک", + "رویدادها" + ] + }, + "ADD": { + "CANCEL": "انصراف", + "TITLE": "اضافه کردن وب هوک جدید", + "DESC": "رویدادهای وب هوک اطلاعات لحظه‌ای حساب چت ووت شما را منتقل می‌کنند. لطفا آدرس URL صحیحی وارد کنید.", + "FORM": { + "END_POINT": { + "LABEL": "آدرس URL وب هوک", + "PLACEHOLDER": "به عنوان مثال: https://example/api/webhook", + "ERROR": "لطفا آدرس URL صحیحی وارد کنید" + }, + "SUBMIT": "ساخت وب هوک" + }, + "API": { + "SUCCESS_MESSAGE": "وب هوک ساخته شد", + "ERROR_MESSAGE": "متاسفانه ارتباط با سرور برقرار نشد، مجددا امتحان کنید" + } + }, + "DELETE": { + "BUTTON_TEXT": "حذف", + "API": { + "SUCCESS_MESSAGE": "وب هوک حذف شد", + "ERROR_MESSAGE": "متاسفانه ارتباط با سرور برقرار نشد، مجددا امتحان کنید" + }, + "CONFIRM": { + "TITLE": "تاییدیه حذف", + "MESSAGE": "مطمئن هستید که می‌خواهید حذف شود ", + "YES": "بله، حذف شود", + "NO": "خیر، بماند" + } + } + } + } +} diff --git a/app/javascript/dashboard/i18n/locale/fa/login.json b/app/javascript/dashboard/i18n/locale/fa/login.json new file mode 100644 index 000000000..94f1e5ce9 --- /dev/null +++ b/app/javascript/dashboard/i18n/locale/fa/login.json @@ -0,0 +1,21 @@ +{ + "LOGIN": { + "TITLE": "ورود به چت ووت", + "EMAIL": { + "LABEL": "ایمیل", + "PLACEHOLDER": "ایمیل به عنوان مثال: someone@example.com" + }, + "PASSWORD": { + "LABEL": "رمز عبور", + "PLACEHOLDER": "رمز عبور" + }, + "API": { + "SUCCESS_MESSAGE": "ورود موفق", + "ERROR_MESSAGE": "متاسفانه ارتباط با سرور برقرار نشد، مجددا امتحان کنید", + "UNAUTH": "این نام کاربری و رمز عبور صحیح نیست" + }, + "FORGOT_PASSWORD": "رمز عبورتان را فراموش کردید؟", + "CREATE_NEW_ACCOUNT": "حساب جدید بسازید", + "SUBMIT": "ورود" + } +} diff --git a/app/javascript/dashboard/i18n/locale/fa/report.json b/app/javascript/dashboard/i18n/locale/fa/report.json new file mode 100644 index 000000000..06df95ebc --- /dev/null +++ b/app/javascript/dashboard/i18n/locale/fa/report.json @@ -0,0 +1,19 @@ +{ + "REPORT": { + "HEADER": "گزارشات", + "LOADING_CHART": "در حال دریافت اطلاعات...", + "NO_ENOUGH_DATA": "متاسفانه اطلاعات کافی دریافت نشد، لطفا بعدا دوباره امتحان کنید", + "METRICS": [ + { "NAME": "گفتگوها", "KEY": "conversations_count", "DESC": "( جمع کل )" }, + { "NAME": "پیام‌های ورودی", "KEY": "incoming_messages_count", "DESC": "( جمع کل )" }, + { "NAME": "پیام‌های خروجی", "KEY": "outgoing_messages_count", "DESC": "( جمع کل )" }, + { "NAME": "زمان تا اولین پاسخ", "KEY": "avg_first_response_time", "DESC": "( میانگین )" }, + { "NAME": "زمان تا حل شدن مساله", "KEY": "avg_resolution_time", "DESC": "( میانگین )" }, + { "NAME": "تعداد مسائل حل شده", "KEY": "resolutions_count", "DESC": "( جمع کل )" } + ], + "DATE_RANGE": [ + { "id": 0, "name": "در ۷ روز گذشته" }, + { "id": 1, "name": "در ۳۰ روز گذشته" } + ] + } +} diff --git a/app/javascript/dashboard/i18n/locale/fa/resetPassword.json b/app/javascript/dashboard/i18n/locale/fa/resetPassword.json new file mode 100644 index 000000000..c26df9e8f --- /dev/null +++ b/app/javascript/dashboard/i18n/locale/fa/resetPassword.json @@ -0,0 +1,15 @@ +{ + "RESET_PASSWORD": { + "TITLE": "ریست کردن رمز عبور", + "EMAIL": { + "LABEL": "ایمیل", + "PLACEHOLDER": "لطفا ایمیل خود را وارد کنید", + "ERROR": "ظاهرا این ایمیل صحیح نیست، لطفا اصلاح کنید" + }, + "API": { + "SUCCESS_MESSAGE": "لینک ریست کردن رمز عبور به ایمیلتان ارسال شد", + "ERROR_MESSAGE": "ارتباط با سرور برقرار نشد، لطفا مجددا امتحان کنید" + }, + "SUBMIT": "ثبت" + } +} diff --git a/app/javascript/dashboard/i18n/locale/fa/setNewPassword.json b/app/javascript/dashboard/i18n/locale/fa/setNewPassword.json new file mode 100644 index 000000000..1a20a61ab --- /dev/null +++ b/app/javascript/dashboard/i18n/locale/fa/setNewPassword.json @@ -0,0 +1,20 @@ +{ + "SET_NEW_PASSWORD": { + "TITLE": "گذاشتن رمز جدید", + "PASSWORD": { + "LABEL": "رمز عبور", + "PLACEHOLDER": "رمز عبور", + "ERROR": "رمز عبور خیلی کوتاه است" + }, + "CONFIRM_PASSWORD": { + "LABEL": "تکرار رمز عبور", + "PLACEHOLDER": "لطفا رمز عبور را مجددا وارد کنید", + "ERROR": "تکرار رمز عبور می‌بایست با رمز عبور یکسان باشد" + }, + "API": { + "SUCCESS_MESSAGE": "رمز عوض شد", + "ERROR_MESSAGE": "ارتباط با سرور برقرار نشد، لطفا مجددا امتحان کنید" + }, + "SUBMIT": "ثبت" + } +} diff --git a/app/javascript/dashboard/i18n/locale/fa/settings.json b/app/javascript/dashboard/i18n/locale/fa/settings.json new file mode 100644 index 000000000..4dd911dfa --- /dev/null +++ b/app/javascript/dashboard/i18n/locale/fa/settings.json @@ -0,0 +1,108 @@ +{ + "PROFILE_SETTINGS": { + "LINK": "تنظیمات پروفایل", + "TITLE": "تنظیمات پروفایل", + "BTN_TEXT": "تغییر دادن پروفایل", + "AFTER_EMAIL_CHANGED": "پروفایلتان با موفقیت تغییر یافت، اطلاعات ورود به سیستم تغییر کرده لذا لطفا مجددا به سیستم وارد شوید", + "FORM": { + "AVATAR": "عکس پروفایل", + "ERROR": "لطفا ایرادات ذکر شده را برطرف کنید", + "REMOVE_IMAGE": "حذف", + "UPLOAD_IMAGE": "آپلود عکس", + "UPDATE_IMAGE": "تغییر عکس", + "PROFILE_SECTION": { + "TITLE": "پروفایل", + "NOTE": "ایمیل عامل شناسایی شما برای ورود به سیستم است" + }, + "PASSWORD_SECTION": { + "TITLE": "رمز عبور", + "NOTE": "تغییر دادن رمز عبور باعث می‌شود مجبور شوید دوباره به سیستم وارد شوید" + }, + "ACCESS_TOKEN": { + "TITLE": "توکن دسترسی", + "NOTE": "از این توکن برای دسترسی از طریق API استفاده می‌شود" + }, + "EMAIL_NOTIFICATIONS_SECTION": { + "TITLE": "اعلامیه‌ به ایمیل", + "NOTE": "اینجا می‌توانید تنظیمات اعلامیه‌هایی که به ایمیل ارسال می‌شود تغییر دهید", + "CONVERSATION_ASSIGNMENT": "هر وقت گفتگویی به من اختصاص داده شد،‌ ایمیل بفرست", + "CONVERSATION_CREATION": "هر وقت گفتگوی جدیدی شروع شد برای من ایمیل بفرست" + }, + "API": { + "UPDATE_SUCCESS": "تغییرات تنظیمات اعلامیه‌ها با موفقیت ثبت شد", + "UPDATE_ERROR": "در ثبت تغییرات اعلامیه‌ها اشکالی پیش آمد، لطفا دوباره امتحان کنید" + }, + "PUSH_NOTIFICATIONS_SECTION": { + "TITLE": "پوش نوتیفیکیشن", + "NOTE": "اینجا می‌توانید تنظیمات پوش نوتیفیکیشن را تغییر دهید", + "CONVERSATION_ASSIGNMENT": "هر وقت گفتگویی به من اختصاص داده شد، برای من پوش نوتیفیکیشن بفرست", + "CONVERSATION_CREATION": "هر وقت گفتگوی جدیدی شروع شد برای من پوش نوتیفیکیشن بفرست", + "HAS_ENABLED_PUSH": "در این مرورگر پوش نوتیفیکیشن را فعال کرده‌اید", + "REQUEST_PUSH": "فعال کردن پوش نوتیفیکیشن" + }, + "PROFILE_IMAGE": { + "LABEL": "عکس پروفایل" + }, + "NAME": { + "LABEL": "اسم شما", + "ERROR": "لطفا اسم خود را به شکل صحیح وارد کنید", + "PLACEHOLDER": "لطفا اسم خود را وارد کنید، این اسم در گفتگوها دیده می‌شود" + }, + "EMAIL": { + "LABEL": "ایمیل شما", + "ERROR": "لطفا ایمیل خود را به شکل صحیح وارد کنید", + "PLACEHOLDER": "لطفا ایمیل خود را وارد کنید، این ایمیل در گفتگوها دیده می‌شود" + }, + "PASSWORD": { + "LABEL": "رمز عبور", + "ERROR": "رمز عبور می‌بایست ۶ کاراکتر یا بیشتر باشد", + "PLACEHOLDER": "لطفا رمز عبور جدیدی وارد کنید" + }, + "PASSWORD_CONFIRMATION": { + "LABEL": "تکرار رمز عبور", + "ERROR": "تکرار رمز عبور می‌بایست با رمز عبور یکسان باشد", + "PLACEHOLDER": "لطفا رمز عبور را مجددا وارد کنید" + } + } + }, + "SIDEBAR_ITEMS": { + "CHANGE_ACCOUNTS": "سوییچ به یک حساب دیگر", + "SELECTOR_SUBTITLE": "از لیست یکی از حساب‌ها را انتخاب کنید", + "PROFILE_SETTINGS": "تنظیمات پروفایل", + "LOGOUT": "خروج از حساب" + }, + "APP_GLOBAL": { + "TRIAL_MESSAGE": "روز تا اتمام دوره آزمایشی باقی است.", + "TRAIL_BUTTON": "الان بخرید" + }, + "COMPONENTS": { + "CODE": { + "BUTTON_TEXT": "کپی", + "COPY_SUCCESSFUL": "کد به حافظه کپی شد" + }, + "FILE_BUBBLE": { + "DOWNLOAD": "دانلود", + "UPLOADING": "در حال آپلود..." + }, + "FORM_BUBBLE": { + "SUBMIT": "ثبت" + } + }, + "CONFIRM_EMAIL": "در حال تایید...", + "SETTINGS": { + "INBOXES": { + "NEW_INBOX": "اضافه کردن صندوق ورودی" + } + }, + "SIDEBAR": { + "CONVERSATIONS": "گفتگوها", + "REPORTS": "گزارشات", + "SETTINGS": "تنظیمات", + "HOME": "صفحه اصلی", + "AGENTS": "اپراتورها", + "INBOXES": "صندوق‌های ورودی", + "CANNED_RESPONSES": "پاسخ‌های آماده", + "INTEGRATIONS": "برنامه‌های تلفیق شده", + "ACCOUNT_SETTINGS": "تنظیمات حساب" + } +} diff --git a/app/javascript/dashboard/i18n/locale/fa/signup.json b/app/javascript/dashboard/i18n/locale/fa/signup.json new file mode 100644 index 000000000..0fb013ec9 --- /dev/null +++ b/app/javascript/dashboard/i18n/locale/fa/signup.json @@ -0,0 +1,32 @@ +{ + "REGISTER": { + "TRY_WOOT": "ثبت حساب", + "TITLE": "ثبت نام", + "TERMS_ACCEPT": "با ثبت نام، اعلام می‌دارید که قوانین و شرایط استفاده از ما را تایید کرده و می‌پذیرید.", + "ACCOUNT_NAME": { + "LABEL": "عنوان حساب", + "PLACEHOLDER": "شرکت ایران ناسیونال", + "ERROR": "عنوان خیلی کوتاه است" + }, + "EMAIL": { + "LABEL": "ایمیل", + "PLACEHOLDER": "bruce@wayne.enterprises", + "ERROR": "ایمیل اشتباه است" + }, + "PASSWORD": { + "LABEL": "رمز عبور", + "PLACEHOLDER": "رمز عبور", + "ERROR": "رمز عبور خیلی کوتاه است" + }, + "CONFIRM_PASSWORD": { + "LABEL": "تکرار رمز عبور", + "PLACEHOLDER": "تکرار رمز عبور", + "ERROR": "رمز عبور و تکرار رمز عبور یکسان نیستند" + }, + "API": { + "SUCCESS_MESSAGE": "ثبت نام با موفقیت انجام شد", + "ERROR_MESSAGE": "ارتباط با سرور برقرار نشد، لطفا بعدا امتحان کنید" + }, + "SUBMIT": "ثبت" + } +} diff --git a/app/javascript/dashboard/i18n/locale/fa/webhooks.json b/app/javascript/dashboard/i18n/locale/fa/webhooks.json new file mode 100644 index 000000000..db99efb59 --- /dev/null +++ b/app/javascript/dashboard/i18n/locale/fa/webhooks.json @@ -0,0 +1,5 @@ +{ + "WEBHOOKS_SETTINGS": { + "HEADER": "تنظیمات وب هوک" + } +} diff --git a/app/javascript/dashboard/routes/auth/Signup.vue b/app/javascript/dashboard/routes/auth/Signup.vue index 8e7047e5a..7ca18e6df 100644 --- a/app/javascript/dashboard/routes/auth/Signup.vue +++ b/app/javascript/dashboard/routes/auth/Signup.vue @@ -13,7 +13,7 @@
+
+ {{ `v${globalConfig.appVersion}` }} +
+ diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/AddAgents.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/AddAgents.vue index 237b9f94e..7491732e4 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/inbox/AddAgents.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/AddAgents.vue @@ -15,7 +15,7 @@ v-model="selectedAgents" :options="agentList" track-by="id" - label="name" + label="available_name" :multiple="true" :close-on-select="false" :clear-on-select="false" diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/ChannelList.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/ChannelList.vue index a5db5a5c1..deaf91183 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/inbox/ChannelList.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/ChannelList.vue @@ -30,12 +30,14 @@ export default { data() { return { channelList: [ - 'website', - 'facebook', - 'twitter', - 'twilio', - 'telegram', - 'line', + { key: 'website', name: 'Website' }, + { key: 'facebook', name: 'Facebook' }, + { key: 'twitter', name: 'Twitter' }, + { key: 'twilio', name: 'Twilio' }, + { key: 'email', name: 'Email' }, + { key: 'api', name: 'API' }, + { key: 'telegram', name: 'Telegram' }, + { key: 'line', name: 'Line' }, ], enabledFeatures: {}, }; diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/FinishSetup.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/FinishSetup.vue index 77f4025e8..a70a5b0b1 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/inbox/FinishSetup.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/FinishSetup.vue @@ -21,6 +21,14 @@ > +
+ + +
Twilio SMS + + Email + + + Api + diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/Settings.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/Settings.vue index 8a30a4dce..3b322a680 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/inbox/Settings.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/Settings.vue @@ -3,9 +3,18 @@ + > + + + + -
+
- - + + +
+ + +
+
+ + +
+ -
+
- -
- - - -
- -
- - - -
-
-
+
+
- +
+
+
+ + + +
+
diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Email.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Email.vue new file mode 100644 index 000000000..b17273d44 --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Email.vue @@ -0,0 +1,113 @@ + + + diff --git a/app/javascript/dashboard/routes/dashboard/settings/integrations/Index.vue b/app/javascript/dashboard/routes/dashboard/settings/integrations/Index.vue index 6d8ca1895..5bfeceac2 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/integrations/Index.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/integrations/Index.vue @@ -1,7 +1,7 @@