Merge branch 'release/1.19.0'
This commit is contained in:
commit
20b96c7c60
@ -7,7 +7,7 @@ defaults: &defaults
|
|||||||
working_directory: ~/build
|
working_directory: ~/build
|
||||||
docker:
|
docker:
|
||||||
# specify the version you desire here
|
# specify the version you desire here
|
||||||
- image: circleci/ruby:2.7.3-node-browsers
|
- image: circleci/ruby:3.0.2-node-browsers
|
||||||
|
|
||||||
# Specify service dependencies here if necessary
|
# Specify service dependencies here if necessary
|
||||||
# CircleCI maintains a library of pre-built images
|
# CircleCI maintains a library of pre-built images
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
# pre-build stage
|
# pre-build stage
|
||||||
ARG VARIANT=2.7
|
ARG VARIANT=3
|
||||||
FROM mcr.microsoft.com/vscode/devcontainers/ruby:${VARIANT}
|
FROM mcr.microsoft.com/vscode/devcontainers/ruby:${VARIANT}
|
||||||
|
|
||||||
# Update args in docker-compose.yaml to set the UID/GID of the "vscode" user.
|
# Update args in docker-compose.yaml to set the UID/GID of the "vscode" user.
|
||||||
|
|||||||
@ -10,8 +10,8 @@ services:
|
|||||||
context: ..
|
context: ..
|
||||||
dockerfile: .devcontainer/Dockerfile
|
dockerfile: .devcontainer/Dockerfile
|
||||||
args:
|
args:
|
||||||
# Update 'VARIANT' to pick a Ruby version: 2, 2.7, 2.6, 2.5
|
# Update 'VARIANT' to pick a Ruby version: https://github.com/microsoft/vscode-dev-containers/tree/main/containers/ruby
|
||||||
VARIANT: 2.7
|
VARIANT: 3
|
||||||
# [Choice] Install Node.js
|
# [Choice] Install Node.js
|
||||||
INSTALL_NODE: "true"
|
INSTALL_NODE: "true"
|
||||||
NODE_VERSION: "lts/*"
|
NODE_VERSION: "lts/*"
|
||||||
|
|||||||
@ -147,6 +147,10 @@ USE_INBOX_AVATAR_FOR_BOT=true
|
|||||||
# maxmindb api key to use geoip2 service
|
# maxmindb api key to use geoip2 service
|
||||||
# IP_LOOKUP_API_KEY=
|
# IP_LOOKUP_API_KEY=
|
||||||
|
|
||||||
|
## Rack Attack configuration
|
||||||
|
## To prevent and throttle abusive requests
|
||||||
|
# ENABLE_RACK_ATTACK=false
|
||||||
|
|
||||||
|
|
||||||
## Running chatwoot as an API only server
|
## Running chatwoot as an API only server
|
||||||
## setting this value to true will disable the frontend dashboard endpoints
|
## setting this value to true will disable the frontend dashboard endpoints
|
||||||
@ -157,3 +161,7 @@ USE_INBOX_AVATAR_FOR_BOT=true
|
|||||||
# LETTER_OPENER=true
|
# LETTER_OPENER=true
|
||||||
# meant to be used in github codespaces
|
# meant to be used in github codespaces
|
||||||
# WEBPACKER_DEV_SERVER_PUBLIC=
|
# WEBPACKER_DEV_SERVER_PUBLIC=
|
||||||
|
|
||||||
|
# If you want to use official mobile app,
|
||||||
|
# the notifications would be relayed via a Chatwoot server
|
||||||
|
ENABLE_PUSH_RELAY_SERVER=true
|
||||||
|
|||||||
35
.rubocop.yml
35
.rubocop.yml
@ -13,6 +13,7 @@ Metrics/ClassLength:
|
|||||||
- 'app/models/conversation.rb'
|
- 'app/models/conversation.rb'
|
||||||
- 'app/mailers/conversation_reply_mailer.rb'
|
- 'app/mailers/conversation_reply_mailer.rb'
|
||||||
- 'app/models/message.rb'
|
- 'app/models/message.rb'
|
||||||
|
- 'app/builders/messages/facebook/message_builder.rb'
|
||||||
RSpec/ExampleLength:
|
RSpec/ExampleLength:
|
||||||
Max: 25
|
Max: 25
|
||||||
Style/Documentation:
|
Style/Documentation:
|
||||||
@ -29,7 +30,8 @@ Style/OptionalBooleanParameter:
|
|||||||
- 'app/dispatchers/dispatcher.rb'
|
- 'app/dispatchers/dispatcher.rb'
|
||||||
Style/GlobalVars:
|
Style/GlobalVars:
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'config/initializers/redis.rb'
|
- 'config/initializers/01_redis.rb'
|
||||||
|
- 'config/initializers/rack_attack.rb'
|
||||||
- 'lib/redis/alfred.rb'
|
- 'lib/redis/alfred.rb'
|
||||||
- 'lib/global_config.rb'
|
- 'lib/global_config.rb'
|
||||||
Style/ClassVars:
|
Style/ClassVars:
|
||||||
@ -38,6 +40,14 @@ Style/ClassVars:
|
|||||||
Lint/MissingSuper:
|
Lint/MissingSuper:
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'app/drops/base_drop.rb'
|
- 'app/drops/base_drop.rb'
|
||||||
|
Lint/SymbolConversion:
|
||||||
|
Enabled: false
|
||||||
|
Lint/EmptyBlock:
|
||||||
|
Exclude:
|
||||||
|
- 'app/views/api/v1/accounts/conversations/toggle_status.json.jbuilder'
|
||||||
|
Lint/OrAssignmentToConstant:
|
||||||
|
Exclude:
|
||||||
|
- 'lib/redis/config.rb'
|
||||||
Metrics/BlockLength:
|
Metrics/BlockLength:
|
||||||
Exclude:
|
Exclude:
|
||||||
- spec/**/*
|
- spec/**/*
|
||||||
@ -55,6 +65,11 @@ Rails/ApplicationController:
|
|||||||
- 'app/controllers/widgets_controller.rb'
|
- 'app/controllers/widgets_controller.rb'
|
||||||
- 'app/controllers/platform_controller.rb'
|
- 'app/controllers/platform_controller.rb'
|
||||||
- 'app/controllers/public_controller.rb'
|
- 'app/controllers/public_controller.rb'
|
||||||
|
- 'app/controllers/survey/responses_controller.rb'
|
||||||
|
Rails/EnvironmentVariableAccess:
|
||||||
|
Enabled: false
|
||||||
|
Rails/TimeZoneAssignment:
|
||||||
|
Enabled: false
|
||||||
Style/ClassAndModuleChildren:
|
Style/ClassAndModuleChildren:
|
||||||
EnforcedStyle: compact
|
EnforcedStyle: compact
|
||||||
Exclude:
|
Exclude:
|
||||||
@ -64,6 +79,10 @@ RSpec/NestedGroups:
|
|||||||
Max: 4
|
Max: 4
|
||||||
RSpec/MessageSpies:
|
RSpec/MessageSpies:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
RSpec/StubbedMock:
|
||||||
|
Enabled: false
|
||||||
|
Naming/VariableNumber:
|
||||||
|
Enabled: false
|
||||||
Metrics/MethodLength:
|
Metrics/MethodLength:
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'db/migrate/20161123131628_devise_token_auth_create_users.rb'
|
- 'db/migrate/20161123131628_devise_token_auth_create_users.rb'
|
||||||
@ -111,14 +130,19 @@ Rails/UniqueValidationWithoutIndex:
|
|||||||
- 'app/models/channel/twitter_profile.rb'
|
- 'app/models/channel/twitter_profile.rb'
|
||||||
- 'app/models/webhook.rb'
|
- 'app/models/webhook.rb'
|
||||||
- 'app/models/contact.rb'
|
- 'app/models/contact.rb'
|
||||||
|
- 'app/models/integrations/hook.rb'
|
||||||
Rails/RenderInline:
|
Rails/RenderInline:
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'app/controllers/swagger_controller.rb'
|
- 'app/controllers/swagger_controller.rb'
|
||||||
|
Performance/CollectionLiteralInLoop:
|
||||||
|
Exclude:
|
||||||
|
- 'db/migrate/20210315101919_enable_email_channel.rb'
|
||||||
RSpec/NamedSubject:
|
RSpec/NamedSubject:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
# we should bring this down
|
# we should bring this down
|
||||||
RSpec/MultipleMemoizedHelpers:
|
RSpec/MultipleMemoizedHelpers:
|
||||||
Max: 12
|
Max: 12
|
||||||
|
|
||||||
AllCops:
|
AllCops:
|
||||||
NewCops: enable
|
NewCops: enable
|
||||||
Exclude:
|
Exclude:
|
||||||
@ -133,4 +157,11 @@ AllCops:
|
|||||||
- 'tmp/**/*'
|
- 'tmp/**/*'
|
||||||
- 'storage/**/*'
|
- 'storage/**/*'
|
||||||
- 'db/migrate/20200225162150_init_schema.rb'
|
- 'db/migrate/20200225162150_init_schema.rb'
|
||||||
- 'config/initializers/azure_storage_service_patch.rb'
|
- 'db/migrate/20210611180222_create_active_storage_variant_records.active_storage.rb'
|
||||||
|
- 'db/migrate/20210611180221_add_service_name_to_active_storage_blobs.active_storage.rb'
|
||||||
|
- db/migrate/20200309213132_add_account_id_to_agent_bot_inboxes.rb
|
||||||
|
- db/migrate/20200331095710_add_identifier_to_contact.rb
|
||||||
|
- db/migrate/20200429082655_add_medium_to_twilio_sms.rb
|
||||||
|
- db/migrate/20200503151130_add_account_feature_flag.rb
|
||||||
|
- db/migrate/20200927135222_add_last_activity_at_to_conversation.rb
|
||||||
|
- db/migrate/20210306170117_add_last_activity_at_to_contacts.rb
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
2.7.3
|
3.0.2
|
||||||
|
|||||||
22
Gemfile
22
Gemfile
@ -1,6 +1,6 @@
|
|||||||
source 'https://rubygems.org'
|
source 'https://rubygems.org'
|
||||||
|
|
||||||
ruby '2.7.3'
|
ruby '3.0.2'
|
||||||
|
|
||||||
##-- base gems for rails --##
|
##-- base gems for rails --##
|
||||||
gem 'rack-cors', require: 'rack/cors'
|
gem 'rack-cors', require: 'rack/cors'
|
||||||
@ -33,19 +33,22 @@ gem 'liquid'
|
|||||||
gem 'commonmarker'
|
gem 'commonmarker'
|
||||||
# Validate Data against JSON Schema
|
# Validate Data against JSON Schema
|
||||||
gem 'json_schemer'
|
gem 'json_schemer'
|
||||||
|
# Rack middleware for blocking & throttling abusive requests
|
||||||
|
gem 'rack-attack'
|
||||||
|
# a utility tool for streaming, flexible and safe downloading of remote files
|
||||||
|
gem 'down', '~> 5.0'
|
||||||
|
|
||||||
##-- for active storage --##
|
##-- for active storage --##
|
||||||
gem 'aws-sdk-s3', require: false
|
gem 'aws-sdk-s3', require: false
|
||||||
gem 'azure-storage-blob', require: false
|
gem 'azure-storage-blob', require: false
|
||||||
gem 'google-cloud-storage', require: false
|
gem 'google-cloud-storage', require: false
|
||||||
gem 'mini_magick'
|
gem 'image_processing'
|
||||||
|
|
||||||
##-- gems for database --#
|
##-- gems for database --#
|
||||||
gem 'groupdate'
|
gem 'groupdate'
|
||||||
gem 'pg'
|
gem 'pg'
|
||||||
gem 'redis'
|
gem 'redis'
|
||||||
gem 'redis-namespace'
|
gem 'redis-namespace'
|
||||||
gem 'redis-rack-cache'
|
|
||||||
# super fast record imports in bulk
|
# super fast record imports in bulk
|
||||||
gem 'activerecord-import'
|
gem 'activerecord-import'
|
||||||
|
|
||||||
@ -90,8 +93,11 @@ gem 'google-cloud-dialogflow'
|
|||||||
##--- gems for debugging and error reporting ---##
|
##--- gems for debugging and error reporting ---##
|
||||||
# static analysis
|
# static analysis
|
||||||
gem 'brakeman'
|
gem 'brakeman'
|
||||||
|
gem 'ddtrace'
|
||||||
gem 'scout_apm'
|
gem 'scout_apm'
|
||||||
gem 'sentry-raven'
|
gem 'sentry-rails'
|
||||||
|
gem 'sentry-ruby'
|
||||||
|
gem 'sentry-sidekiq'
|
||||||
|
|
||||||
##-- background job processing --##
|
##-- background job processing --##
|
||||||
gem 'sidekiq'
|
gem 'sidekiq'
|
||||||
@ -120,7 +126,7 @@ group :development do
|
|||||||
gem 'web-console'
|
gem 'web-console'
|
||||||
|
|
||||||
# used in swagger build
|
# used in swagger build
|
||||||
gem 'json_refs', git: 'https://github.com/tzmfreedom/json_refs', ref: '131b11294fd6af9c428171f38516e6222a58c874'
|
gem 'json_refs'
|
||||||
|
|
||||||
# When we want to squash migrations
|
# When we want to squash migrations
|
||||||
gem 'squasher'
|
gem 'squasher'
|
||||||
@ -134,19 +140,19 @@ group :test do
|
|||||||
end
|
end
|
||||||
|
|
||||||
group :development, :test do
|
group :development, :test do
|
||||||
|
gem 'active_record_query_trace'
|
||||||
gem 'bundle-audit', require: false
|
gem 'bundle-audit', require: false
|
||||||
gem 'byebug', platform: :mri
|
gem 'byebug', platform: :mri
|
||||||
gem 'factory_bot_rails'
|
gem 'factory_bot_rails'
|
||||||
gem 'faker'
|
gem 'faker'
|
||||||
gem 'listen'
|
gem 'listen'
|
||||||
gem 'mock_redis', git: 'https://github.com/sds/mock_redis', ref: '16d00789f0341a3aac35126c0ffe97a596753ff9'
|
gem 'mock_redis'
|
||||||
gem 'pry-rails'
|
gem 'pry-rails'
|
||||||
gem 'rspec-rails', '~> 4.0.0.beta2'
|
gem 'rspec-rails', '~> 5.0.0'
|
||||||
gem 'rubocop', require: false
|
gem 'rubocop', require: false
|
||||||
gem 'rubocop-performance', require: false
|
gem 'rubocop-performance', require: false
|
||||||
gem 'rubocop-rails', require: false
|
gem 'rubocop-rails', require: false
|
||||||
gem 'rubocop-rspec', require: false
|
gem 'rubocop-rspec', require: false
|
||||||
gem 'scss_lint', require: false
|
|
||||||
gem 'seed_dump'
|
gem 'seed_dump'
|
||||||
gem 'shoulda-matchers'
|
gem 'shoulda-matchers'
|
||||||
gem 'simplecov', '0.17.1', require: false
|
gem 'simplecov', '0.17.1', require: false
|
||||||
|
|||||||
530
Gemfile.lock
530
Gemfile.lock
@ -1,80 +1,70 @@
|
|||||||
GIT
|
|
||||||
remote: https://github.com/sds/mock_redis
|
|
||||||
revision: 16d00789f0341a3aac35126c0ffe97a596753ff9
|
|
||||||
ref: 16d00789f0341a3aac35126c0ffe97a596753ff9
|
|
||||||
specs:
|
|
||||||
mock_redis (0.22.0)
|
|
||||||
|
|
||||||
GIT
|
|
||||||
remote: https://github.com/tzmfreedom/json_refs
|
|
||||||
revision: 131b11294fd6af9c428171f38516e6222a58c874
|
|
||||||
ref: 131b11294fd6af9c428171f38516e6222a58c874
|
|
||||||
specs:
|
|
||||||
json_refs (0.1.6)
|
|
||||||
hana
|
|
||||||
|
|
||||||
GEM
|
GEM
|
||||||
remote: https://rubygems.org/
|
remote: https://rubygems.org/
|
||||||
specs:
|
specs:
|
||||||
actioncable (6.0.3.7)
|
actioncable (6.1.4)
|
||||||
actionpack (= 6.0.3.7)
|
actionpack (= 6.1.4)
|
||||||
|
activesupport (= 6.1.4)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
websocket-driver (>= 0.6.1)
|
websocket-driver (>= 0.6.1)
|
||||||
actionmailbox (6.0.3.7)
|
actionmailbox (6.1.4)
|
||||||
actionpack (= 6.0.3.7)
|
actionpack (= 6.1.4)
|
||||||
activejob (= 6.0.3.7)
|
activejob (= 6.1.4)
|
||||||
activerecord (= 6.0.3.7)
|
activerecord (= 6.1.4)
|
||||||
activestorage (= 6.0.3.7)
|
activestorage (= 6.1.4)
|
||||||
activesupport (= 6.0.3.7)
|
activesupport (= 6.1.4)
|
||||||
mail (>= 2.7.1)
|
mail (>= 2.7.1)
|
||||||
actionmailer (6.0.3.7)
|
actionmailer (6.1.4)
|
||||||
actionpack (= 6.0.3.7)
|
actionpack (= 6.1.4)
|
||||||
actionview (= 6.0.3.7)
|
actionview (= 6.1.4)
|
||||||
activejob (= 6.0.3.7)
|
activejob (= 6.1.4)
|
||||||
|
activesupport (= 6.1.4)
|
||||||
mail (~> 2.5, >= 2.5.4)
|
mail (~> 2.5, >= 2.5.4)
|
||||||
rails-dom-testing (~> 2.0)
|
rails-dom-testing (~> 2.0)
|
||||||
actionpack (6.0.3.7)
|
actionpack (6.1.4)
|
||||||
actionview (= 6.0.3.7)
|
actionview (= 6.1.4)
|
||||||
activesupport (= 6.0.3.7)
|
activesupport (= 6.1.4)
|
||||||
rack (~> 2.0, >= 2.0.8)
|
rack (~> 2.0, >= 2.0.9)
|
||||||
rack-test (>= 0.6.3)
|
rack-test (>= 0.6.3)
|
||||||
rails-dom-testing (~> 2.0)
|
rails-dom-testing (~> 2.0)
|
||||||
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
||||||
actiontext (6.0.3.7)
|
actiontext (6.1.4)
|
||||||
actionpack (= 6.0.3.7)
|
actionpack (= 6.1.4)
|
||||||
activerecord (= 6.0.3.7)
|
activerecord (= 6.1.4)
|
||||||
activestorage (= 6.0.3.7)
|
activestorage (= 6.1.4)
|
||||||
activesupport (= 6.0.3.7)
|
activesupport (= 6.1.4)
|
||||||
nokogiri (>= 1.8.5)
|
nokogiri (>= 1.8.5)
|
||||||
actionview (6.0.3.7)
|
actionview (6.1.4)
|
||||||
activesupport (= 6.0.3.7)
|
activesupport (= 6.1.4)
|
||||||
builder (~> 3.1)
|
builder (~> 3.1)
|
||||||
erubi (~> 1.4)
|
erubi (~> 1.4)
|
||||||
rails-dom-testing (~> 2.0)
|
rails-dom-testing (~> 2.0)
|
||||||
rails-html-sanitizer (~> 1.1, >= 1.2.0)
|
rails-html-sanitizer (~> 1.1, >= 1.2.0)
|
||||||
activejob (6.0.3.7)
|
active_record_query_trace (1.8)
|
||||||
activesupport (= 6.0.3.7)
|
activejob (6.1.4)
|
||||||
|
activesupport (= 6.1.4)
|
||||||
globalid (>= 0.3.6)
|
globalid (>= 0.3.6)
|
||||||
activemodel (6.0.3.7)
|
activemodel (6.1.4)
|
||||||
activesupport (= 6.0.3.7)
|
activesupport (= 6.1.4)
|
||||||
activerecord (6.0.3.7)
|
activerecord (6.1.4)
|
||||||
activemodel (= 6.0.3.7)
|
activemodel (= 6.1.4)
|
||||||
activesupport (= 6.0.3.7)
|
activesupport (= 6.1.4)
|
||||||
activerecord-import (1.0.7)
|
activerecord-import (1.2.0)
|
||||||
activerecord (>= 3.2)
|
activerecord (>= 3.2)
|
||||||
activestorage (6.0.3.7)
|
activestorage (6.1.4)
|
||||||
actionpack (= 6.0.3.7)
|
actionpack (= 6.1.4)
|
||||||
activejob (= 6.0.3.7)
|
activejob (= 6.1.4)
|
||||||
activerecord (= 6.0.3.7)
|
activerecord (= 6.1.4)
|
||||||
|
activesupport (= 6.1.4)
|
||||||
marcel (~> 1.0.0)
|
marcel (~> 1.0.0)
|
||||||
activesupport (6.0.3.7)
|
mini_mime (>= 1.1.0)
|
||||||
|
activesupport (6.1.4)
|
||||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||||
i18n (>= 0.7, < 2)
|
i18n (>= 1.6, < 2)
|
||||||
minitest (~> 5.1)
|
minitest (>= 5.1)
|
||||||
tzinfo (~> 1.1)
|
tzinfo (~> 2.0)
|
||||||
zeitwerk (~> 2.2, >= 2.2.2)
|
zeitwerk (~> 2.3)
|
||||||
acts-as-taggable-on (6.5.0)
|
acts-as-taggable-on (8.1.0)
|
||||||
activerecord (>= 5.0, < 6.1)
|
activerecord (>= 5.0, < 6.2)
|
||||||
addressable (2.8.0)
|
addressable (2.8.0)
|
||||||
public_suffix (>= 2.0.2, < 5.0)
|
public_suffix (>= 2.0.2, < 5.0)
|
||||||
administrate (0.16.0)
|
administrate (0.16.0)
|
||||||
@ -90,23 +80,23 @@ GEM
|
|||||||
annotate (3.1.1)
|
annotate (3.1.1)
|
||||||
activerecord (>= 3.2, < 7.0)
|
activerecord (>= 3.2, < 7.0)
|
||||||
rake (>= 10.4, < 14.0)
|
rake (>= 10.4, < 14.0)
|
||||||
ast (2.4.1)
|
ast (2.4.2)
|
||||||
attr_extras (6.2.4)
|
attr_extras (6.2.4)
|
||||||
aws-eventstream (1.1.0)
|
aws-eventstream (1.1.1)
|
||||||
aws-partitions (1.360.0)
|
aws-partitions (1.482.0)
|
||||||
aws-sdk-core (3.105.0)
|
aws-sdk-core (3.119.0)
|
||||||
aws-eventstream (~> 1, >= 1.0.2)
|
aws-eventstream (~> 1, >= 1.0.2)
|
||||||
aws-partitions (~> 1, >= 1.239.0)
|
aws-partitions (~> 1, >= 1.239.0)
|
||||||
aws-sigv4 (~> 1.1)
|
aws-sigv4 (~> 1.1)
|
||||||
jmespath (~> 1.0)
|
jmespath (~> 1.0)
|
||||||
aws-sdk-kms (1.37.0)
|
aws-sdk-kms (1.46.0)
|
||||||
aws-sdk-core (~> 3, >= 3.99.0)
|
aws-sdk-core (~> 3, >= 3.119.0)
|
||||||
aws-sigv4 (~> 1.1)
|
aws-sigv4 (~> 1.1)
|
||||||
aws-sdk-s3 (1.79.1)
|
aws-sdk-s3 (1.98.0)
|
||||||
aws-sdk-core (~> 3, >= 3.104.3)
|
aws-sdk-core (~> 3, >= 3.119.0)
|
||||||
aws-sdk-kms (~> 1)
|
aws-sdk-kms (~> 1)
|
||||||
aws-sigv4 (~> 1.1)
|
aws-sigv4 (~> 1.1)
|
||||||
aws-sigv4 (1.2.2)
|
aws-sigv4 (1.2.4)
|
||||||
aws-eventstream (~> 1, >= 1.0.2)
|
aws-eventstream (~> 1, >= 1.0.2)
|
||||||
axiom-types (0.1.1)
|
axiom-types (0.1.1)
|
||||||
descendants_tracker (~> 0.0.4)
|
descendants_tracker (~> 0.0.4)
|
||||||
@ -120,42 +110,48 @@ GEM
|
|||||||
faraday_middleware (~> 1.0.0.rc1)
|
faraday_middleware (~> 1.0.0.rc1)
|
||||||
net-http-persistent (~> 4.0)
|
net-http-persistent (~> 4.0)
|
||||||
nokogiri (~> 1.11.0.rc2)
|
nokogiri (~> 1.11.0.rc2)
|
||||||
barnes (0.0.8)
|
barnes (0.0.9)
|
||||||
multi_json (~> 1)
|
multi_json (~> 1)
|
||||||
statsd-ruby (~> 1.1)
|
statsd-ruby (~> 1.1)
|
||||||
bcrypt (3.1.16)
|
bcrypt (3.1.16)
|
||||||
bindex (0.8.1)
|
bindex (0.8.1)
|
||||||
bootsnap (1.4.8)
|
bootsnap (1.7.6)
|
||||||
msgpack (~> 1.0)
|
msgpack (~> 1.0)
|
||||||
brakeman (4.9.0)
|
brakeman (5.1.1)
|
||||||
browser (5.3.1)
|
browser (5.3.1)
|
||||||
builder (3.2.4)
|
builder (3.2.4)
|
||||||
bullet (6.1.0)
|
bullet (6.1.4)
|
||||||
activesupport (>= 3.0.0)
|
activesupport (>= 3.0.0)
|
||||||
uniform_notifier (~> 1.11)
|
uniform_notifier (~> 1.11)
|
||||||
bundle-audit (0.1.0)
|
bundle-audit (0.1.0)
|
||||||
bundler-audit
|
bundler-audit
|
||||||
bundler-audit (0.7.0.1)
|
bundler-audit (0.8.0)
|
||||||
bundler (>= 1.2.0, < 3)
|
bundler (>= 1.2.0, < 3)
|
||||||
thor (>= 0.18, < 2)
|
thor (~> 1.0)
|
||||||
byebug (11.1.3)
|
byebug (11.1.3)
|
||||||
coderay (1.1.3)
|
coderay (1.1.3)
|
||||||
coercible (1.0.0)
|
coercible (1.0.0)
|
||||||
descendants_tracker (~> 0.0.1)
|
descendants_tracker (~> 0.0.1)
|
||||||
commonmarker (0.21.1)
|
commonmarker (0.22.0)
|
||||||
ruby-enum (~> 0.5)
|
concurrent-ruby (1.1.9)
|
||||||
concurrent-ruby (1.1.8)
|
connection_pool (2.2.5)
|
||||||
connection_pool (2.2.3)
|
crack (0.4.5)
|
||||||
crack (0.4.3)
|
rexml
|
||||||
safe_yaml (~> 1.0.0)
|
|
||||||
crass (1.0.6)
|
crass (1.0.6)
|
||||||
cypress-on-rails (1.8.0)
|
cypress-on-rails (1.10.1)
|
||||||
rack
|
rack
|
||||||
database_cleaner (1.8.5)
|
database_cleaner (2.0.1)
|
||||||
|
database_cleaner-active_record (~> 2.0.0)
|
||||||
|
database_cleaner-active_record (2.0.1)
|
||||||
|
activerecord (>= 5.a)
|
||||||
|
database_cleaner-core (~> 2.0.0)
|
||||||
|
database_cleaner-core (2.0.1)
|
||||||
datetime_picker_rails (0.0.7)
|
datetime_picker_rails (0.0.7)
|
||||||
momentjs-rails (>= 2.8.1)
|
momentjs-rails (>= 2.8.1)
|
||||||
|
ddtrace (0.51.1)
|
||||||
|
ffi (~> 1.0)
|
||||||
|
msgpack
|
||||||
declarative (0.0.20)
|
declarative (0.0.20)
|
||||||
declarative-option (0.1.0)
|
|
||||||
descendants_tracker (0.0.4)
|
descendants_tracker (0.0.4)
|
||||||
thread_safe (~> 0.3, >= 0.3.1)
|
thread_safe (~> 0.3, >= 0.3.1)
|
||||||
devise (4.8.0)
|
devise (4.8.0)
|
||||||
@ -167,48 +163,49 @@ GEM
|
|||||||
devise-secure_password (2.0.1)
|
devise-secure_password (2.0.1)
|
||||||
devise (>= 4.0.0, < 5.0.0)
|
devise (>= 4.0.0, < 5.0.0)
|
||||||
railties (>= 5.0.0, < 7.0.0)
|
railties (>= 5.0.0, < 7.0.0)
|
||||||
devise_token_auth (1.1.4)
|
devise_token_auth (1.2.0)
|
||||||
bcrypt (~> 3.0)
|
bcrypt (~> 3.0)
|
||||||
devise (> 3.5.2, < 5)
|
devise (> 3.5.2, < 5)
|
||||||
rails (>= 4.2.0, < 6.1)
|
rails (>= 4.2.0, < 6.2)
|
||||||
sprockets (= 3.7.2)
|
|
||||||
diff-lcs (1.4.4)
|
diff-lcs (1.4.4)
|
||||||
digest-crc (0.6.1)
|
digest-crc (0.6.4)
|
||||||
rake (~> 13.0)
|
rake (>= 12.0.0, < 14.0.0)
|
||||||
docile (1.3.2)
|
docile (1.4.0)
|
||||||
domain_name (0.5.20190701)
|
domain_name (0.5.20190701)
|
||||||
unf (>= 0.0.5, < 1.0.0)
|
unf (>= 0.0.5, < 1.0.0)
|
||||||
dotenv (2.7.6)
|
dotenv (2.7.6)
|
||||||
dotenv-rails (2.7.6)
|
dotenv-rails (2.7.6)
|
||||||
dotenv (= 2.7.6)
|
dotenv (= 2.7.6)
|
||||||
railties (>= 3.2)
|
railties (>= 3.2)
|
||||||
ecma-re-validator (0.2.1)
|
down (5.2.3)
|
||||||
regexp_parser (~> 1.2)
|
addressable (~> 2.8)
|
||||||
equalizer (0.0.11)
|
dry-inflector (0.2.1)
|
||||||
|
ecma-re-validator (0.3.0)
|
||||||
|
regexp_parser (~> 2.0)
|
||||||
erubi (1.10.0)
|
erubi (1.10.0)
|
||||||
et-orbi (1.2.4)
|
et-orbi (1.2.4)
|
||||||
tzinfo
|
tzinfo
|
||||||
execjs (2.7.0)
|
execjs (2.8.1)
|
||||||
facebook-messenger (2.0.1)
|
facebook-messenger (2.0.1)
|
||||||
httparty (~> 0.13, >= 0.13.7)
|
httparty (~> 0.13, >= 0.13.7)
|
||||||
rack (>= 1.4.5)
|
rack (>= 1.4.5)
|
||||||
factory_bot (6.1.0)
|
factory_bot (6.2.0)
|
||||||
activesupport (>= 5.0.0)
|
activesupport (>= 5.0.0)
|
||||||
factory_bot_rails (6.1.0)
|
factory_bot_rails (6.2.0)
|
||||||
factory_bot (~> 6.1.0)
|
factory_bot (~> 6.2.0)
|
||||||
railties (>= 5.0.0)
|
railties (>= 5.0.0)
|
||||||
faker (2.13.0)
|
faker (2.18.0)
|
||||||
i18n (>= 1.6, < 2)
|
i18n (>= 1.6, < 2)
|
||||||
faraday (1.0.1)
|
faraday (1.0.1)
|
||||||
multipart-post (>= 1.2, < 3)
|
multipart-post (>= 1.2, < 3)
|
||||||
faraday_middleware (1.0.0)
|
faraday_middleware (1.0.0)
|
||||||
faraday (~> 1.0)
|
faraday (~> 1.0)
|
||||||
fcm (1.0.2)
|
fcm (1.0.3)
|
||||||
faraday (~> 1.0.0)
|
faraday (~> 1)
|
||||||
ffi (1.15.0)
|
ffi (1.15.3)
|
||||||
flag_shih_tzu (0.3.23)
|
flag_shih_tzu (0.3.23)
|
||||||
foreman (0.87.2)
|
foreman (0.87.2)
|
||||||
fugit (1.4.1)
|
fugit (1.5.0)
|
||||||
et-orbi (~> 1.1, >= 1.1.8)
|
et-orbi (~> 1.1, >= 1.1.8)
|
||||||
raabro (~> 1.4)
|
raabro (~> 1.4)
|
||||||
gapic-common (0.3.4)
|
gapic-common (0.3.4)
|
||||||
@ -217,18 +214,23 @@ GEM
|
|||||||
googleapis-common-protos-types (>= 1.0.4, < 2.0)
|
googleapis-common-protos-types (>= 1.0.4, < 2.0)
|
||||||
googleauth (~> 0.9)
|
googleauth (~> 0.9)
|
||||||
grpc (~> 1.25)
|
grpc (~> 1.25)
|
||||||
geocoder (1.6.3)
|
geocoder (1.6.7)
|
||||||
gli (2.19.2)
|
gli (2.20.1)
|
||||||
globalid (0.4.2)
|
globalid (0.5.1)
|
||||||
activesupport (>= 4.2.0)
|
activesupport (>= 5.0)
|
||||||
google-api-client (0.43.0)
|
google-apis-core (0.4.1)
|
||||||
addressable (~> 2.5, >= 2.5.1)
|
addressable (~> 2.5, >= 2.5.1)
|
||||||
googleauth (~> 0.9)
|
googleauth (>= 0.16.2, < 2.a)
|
||||||
httpclient (>= 2.8.1, < 3.0)
|
httpclient (>= 2.8.1, < 3.a)
|
||||||
mini_mime (~> 1.0)
|
mini_mime (~> 1.0)
|
||||||
representable (~> 3.0)
|
representable (~> 3.0)
|
||||||
retriable (>= 2.0, < 4.0)
|
retriable (>= 2.0, < 4.a)
|
||||||
signet (~> 0.12)
|
rexml
|
||||||
|
webrick
|
||||||
|
google-apis-iamcredentials_v1 (0.6.0)
|
||||||
|
google-apis-core (>= 0.4, < 2.a)
|
||||||
|
google-apis-storage_v1 (0.6.0)
|
||||||
|
google-apis-core (>= 0.4, < 2.a)
|
||||||
google-cloud-core (1.6.0)
|
google-cloud-core (1.6.0)
|
||||||
google-cloud-env (~> 1.0)
|
google-cloud-env (~> 1.0)
|
||||||
google-cloud-errors (~> 1.0)
|
google-cloud-errors (~> 1.0)
|
||||||
@ -241,21 +243,23 @@ GEM
|
|||||||
google-cloud-env (1.5.0)
|
google-cloud-env (1.5.0)
|
||||||
faraday (>= 0.17.3, < 2.0)
|
faraday (>= 0.17.3, < 2.0)
|
||||||
google-cloud-errors (1.1.0)
|
google-cloud-errors (1.1.0)
|
||||||
google-cloud-storage (1.28.0)
|
google-cloud-storage (1.34.1)
|
||||||
addressable (~> 2.5)
|
addressable (~> 2.5)
|
||||||
digest-crc (~> 0.4)
|
digest-crc (~> 0.4)
|
||||||
google-api-client (~> 0.33)
|
google-apis-iamcredentials_v1 (~> 0.1)
|
||||||
google-cloud-core (~> 1.2)
|
google-apis-storage_v1 (~> 0.1)
|
||||||
googleauth (~> 0.9)
|
google-cloud-core (~> 1.6)
|
||||||
|
googleauth (>= 0.16.2, < 2.a)
|
||||||
mini_mime (~> 1.0)
|
mini_mime (~> 1.0)
|
||||||
google-protobuf (3.15.8)
|
google-protobuf (3.17.3-universal-darwin)
|
||||||
|
google-protobuf (3.17.3-x86_64-linux)
|
||||||
googleapis-common-protos (1.3.11)
|
googleapis-common-protos (1.3.11)
|
||||||
google-protobuf (~> 3.14)
|
google-protobuf (~> 3.14)
|
||||||
googleapis-common-protos-types (>= 1.0.6, < 2.0)
|
googleapis-common-protos-types (>= 1.0.6, < 2.0)
|
||||||
grpc (~> 1.27)
|
grpc (~> 1.27)
|
||||||
googleapis-common-protos-types (1.0.6)
|
googleapis-common-protos-types (1.1.0)
|
||||||
google-protobuf (~> 3.14)
|
google-protobuf (~> 3.14)
|
||||||
googleauth (0.16.2)
|
googleauth (0.17.0)
|
||||||
faraday (>= 0.17.3, < 2.0)
|
faraday (>= 0.17.3, < 2.0)
|
||||||
jwt (>= 1.4, < 3.0)
|
jwt (>= 1.4, < 3.0)
|
||||||
memoist (~> 0.16)
|
memoist (~> 0.16)
|
||||||
@ -264,11 +268,14 @@ GEM
|
|||||||
signet (~> 0.14)
|
signet (~> 0.14)
|
||||||
groupdate (5.2.2)
|
groupdate (5.2.2)
|
||||||
activesupport (>= 5)
|
activesupport (>= 5)
|
||||||
grpc (1.37.1)
|
grpc (1.38.0-universal-darwin)
|
||||||
google-protobuf (~> 3.15)
|
google-protobuf (~> 3.15)
|
||||||
googleapis-common-protos-types (~> 1.0)
|
googleapis-common-protos-types (~> 1.0)
|
||||||
haikunator (1.1.0)
|
grpc (1.38.0-x86_64-linux)
|
||||||
hairtrigger (0.2.23)
|
google-protobuf (~> 3.15)
|
||||||
|
googleapis-common-protos-types (~> 1.0)
|
||||||
|
haikunator (1.1.1)
|
||||||
|
hairtrigger (0.2.24)
|
||||||
activerecord (>= 5.0, < 7)
|
activerecord (>= 5.0, < 7)
|
||||||
ruby2ruby (~> 2.4)
|
ruby2ruby (~> 2.4)
|
||||||
ruby_parser (~> 3.10)
|
ruby_parser (~> 3.10)
|
||||||
@ -277,7 +284,7 @@ GEM
|
|||||||
hashie (4.1.0)
|
hashie (4.1.0)
|
||||||
hkdf (0.3.0)
|
hkdf (0.3.0)
|
||||||
http-accept (1.7.0)
|
http-accept (1.7.0)
|
||||||
http-cookie (1.0.3)
|
http-cookie (1.0.4)
|
||||||
domain_name (~> 0.5)
|
domain_name (~> 0.5)
|
||||||
httparty (0.18.1)
|
httparty (0.18.1)
|
||||||
mime-types (~> 3.0)
|
mime-types (~> 3.0)
|
||||||
@ -286,19 +293,23 @@ GEM
|
|||||||
i18n (1.8.10)
|
i18n (1.8.10)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
ice_nine (0.11.2)
|
ice_nine (0.11.2)
|
||||||
inflecto (0.0.2)
|
image_processing (1.12.1)
|
||||||
jbuilder (2.10.0)
|
mini_magick (>= 4.9.5, < 5)
|
||||||
|
ruby-vips (>= 2.0.17, < 3)
|
||||||
|
jbuilder (2.11.2)
|
||||||
activesupport (>= 5.0.0)
|
activesupport (>= 5.0.0)
|
||||||
jmespath (1.4.0)
|
jmespath (1.4.0)
|
||||||
jquery-rails (4.4.0)
|
jquery-rails (4.4.0)
|
||||||
rails-dom-testing (>= 1, < 3)
|
rails-dom-testing (>= 1, < 3)
|
||||||
railties (>= 4.2.0)
|
railties (>= 4.2.0)
|
||||||
thor (>= 0.14, < 2.0)
|
thor (>= 0.14, < 2.0)
|
||||||
json (2.3.1)
|
json (2.5.1)
|
||||||
json_schemer (0.2.16)
|
json_refs (0.1.6)
|
||||||
ecma-re-validator (~> 0.2)
|
hana
|
||||||
|
json_schemer (0.2.18)
|
||||||
|
ecma-re-validator (~> 0.3)
|
||||||
hana (~> 1.3)
|
hana (~> 1.3)
|
||||||
regexp_parser (~> 1.5)
|
regexp_parser (~> 2.0)
|
||||||
uri_template (~> 0.7)
|
uri_template (~> 0.7)
|
||||||
jwt (2.2.3)
|
jwt (2.2.3)
|
||||||
kaminari (1.2.1)
|
kaminari (1.2.1)
|
||||||
@ -321,11 +332,11 @@ GEM
|
|||||||
addressable (~> 2.7)
|
addressable (~> 2.7)
|
||||||
letter_opener (1.7.0)
|
letter_opener (1.7.0)
|
||||||
launchy (~> 2.2)
|
launchy (~> 2.2)
|
||||||
liquid (4.0.3)
|
liquid (5.0.1)
|
||||||
listen (3.3.3)
|
listen (3.6.0)
|
||||||
rb-fsevent (~> 0.10, >= 0.10.3)
|
rb-fsevent (~> 0.10, >= 0.10.3)
|
||||||
rb-inotify (~> 0.9, >= 0.9.10)
|
rb-inotify (~> 0.9, >= 0.9.10)
|
||||||
loofah (2.9.1)
|
loofah (2.11.0)
|
||||||
crass (~> 1.0.2)
|
crass (~> 1.0.2)
|
||||||
nokogiri (>= 1.5.9)
|
nokogiri (>= 1.5.9)
|
||||||
mail (2.7.1)
|
mail (2.7.1)
|
||||||
@ -336,98 +347,97 @@ GEM
|
|||||||
method_source (1.0.0)
|
method_source (1.0.0)
|
||||||
mime-types (3.3.1)
|
mime-types (3.3.1)
|
||||||
mime-types-data (~> 3.2015)
|
mime-types-data (~> 3.2015)
|
||||||
mime-types-data (3.2021.0225)
|
mime-types-data (3.2021.0704)
|
||||||
mini_magick (4.10.1)
|
mini_magick (4.11.0)
|
||||||
mini_mime (1.1.0)
|
mini_mime (1.1.0)
|
||||||
mini_portile2 (2.5.1)
|
|
||||||
minitest (5.14.4)
|
minitest (5.14.4)
|
||||||
|
mock_redis (0.28.0)
|
||||||
|
ruby2_keywords
|
||||||
momentjs-rails (2.20.1)
|
momentjs-rails (2.20.1)
|
||||||
railties (>= 3.1)
|
railties (>= 3.1)
|
||||||
msgpack (1.3.3)
|
msgpack (1.4.2)
|
||||||
multi_json (1.15.0)
|
multi_json (1.15.0)
|
||||||
multi_xml (0.6.0)
|
multi_xml (0.6.0)
|
||||||
multipart-post (2.1.1)
|
multipart-post (2.1.1)
|
||||||
net-http-persistent (4.0.0)
|
net-http-persistent (4.0.1)
|
||||||
connection_pool (~> 2.2)
|
connection_pool (~> 2.2)
|
||||||
netrc (0.11.0)
|
netrc (0.11.0)
|
||||||
nio4r (2.5.7)
|
nio4r (2.5.7)
|
||||||
nokogiri (1.11.6)
|
nokogiri (1.11.7-arm64-darwin)
|
||||||
mini_portile2 (~> 2.5.0)
|
racc (~> 1.4)
|
||||||
|
nokogiri (1.11.7-x86_64-darwin)
|
||||||
|
racc (~> 1.4)
|
||||||
|
nokogiri (1.11.7-x86_64-linux)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
oauth (0.5.6)
|
oauth (0.5.6)
|
||||||
orm_adapter (0.5.0)
|
orm_adapter (0.5.0)
|
||||||
os (1.1.1)
|
os (1.1.1)
|
||||||
parallel (1.19.2)
|
parallel (1.20.1)
|
||||||
parser (2.7.1.4)
|
parser (3.0.2.0)
|
||||||
ast (~> 2.4.1)
|
ast (~> 2.4.1)
|
||||||
pg (1.2.3)
|
pg (1.2.3)
|
||||||
procore-sift (0.15.0)
|
procore-sift (0.16.0)
|
||||||
rails (> 4.2.0)
|
rails (> 4.2.0)
|
||||||
pry (0.13.1)
|
pry (0.14.1)
|
||||||
coderay (~> 1.1)
|
coderay (~> 1.1)
|
||||||
method_source (~> 1.0)
|
method_source (~> 1.0)
|
||||||
pry-rails (0.3.9)
|
pry-rails (0.3.9)
|
||||||
pry (>= 0.10.4)
|
pry (>= 0.10.4)
|
||||||
public_suffix (4.0.6)
|
public_suffix (4.0.6)
|
||||||
puma (4.3.8)
|
puma (5.4.0)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
pundit (2.1.0)
|
pundit (2.1.0)
|
||||||
activesupport (>= 3.0.0)
|
activesupport (>= 3.0.0)
|
||||||
raabro (1.4.0)
|
raabro (1.4.0)
|
||||||
racc (1.5.2)
|
racc (1.5.2)
|
||||||
rack (2.2.3)
|
rack (2.2.3)
|
||||||
rack-cache (1.12.0)
|
rack-attack (6.5.0)
|
||||||
rack (>= 0.4)
|
rack (>= 1.0, < 3)
|
||||||
rack-cors (1.1.1)
|
rack-cors (1.1.1)
|
||||||
rack (>= 2.0.0)
|
rack (>= 2.0.0)
|
||||||
rack-proxy (0.6.5)
|
rack-proxy (0.7.0)
|
||||||
rack
|
rack
|
||||||
rack-test (1.1.0)
|
rack-test (1.1.0)
|
||||||
rack (>= 1.0, < 3)
|
rack (>= 1.0, < 3)
|
||||||
rack-timeout (0.6.0)
|
rack-timeout (0.6.0)
|
||||||
rails (6.0.3.7)
|
rails (6.1.4)
|
||||||
actioncable (= 6.0.3.7)
|
actioncable (= 6.1.4)
|
||||||
actionmailbox (= 6.0.3.7)
|
actionmailbox (= 6.1.4)
|
||||||
actionmailer (= 6.0.3.7)
|
actionmailer (= 6.1.4)
|
||||||
actionpack (= 6.0.3.7)
|
actionpack (= 6.1.4)
|
||||||
actiontext (= 6.0.3.7)
|
actiontext (= 6.1.4)
|
||||||
actionview (= 6.0.3.7)
|
actionview (= 6.1.4)
|
||||||
activejob (= 6.0.3.7)
|
activejob (= 6.1.4)
|
||||||
activemodel (= 6.0.3.7)
|
activemodel (= 6.1.4)
|
||||||
activerecord (= 6.0.3.7)
|
activerecord (= 6.1.4)
|
||||||
activestorage (= 6.0.3.7)
|
activestorage (= 6.1.4)
|
||||||
activesupport (= 6.0.3.7)
|
activesupport (= 6.1.4)
|
||||||
bundler (>= 1.3.0)
|
bundler (>= 1.15.0)
|
||||||
railties (= 6.0.3.7)
|
railties (= 6.1.4)
|
||||||
sprockets-rails (>= 2.0.0)
|
sprockets-rails (>= 2.0.0)
|
||||||
rails-dom-testing (2.0.3)
|
rails-dom-testing (2.0.3)
|
||||||
activesupport (>= 4.2.0)
|
activesupport (>= 4.2.0)
|
||||||
nokogiri (>= 1.6)
|
nokogiri (>= 1.6)
|
||||||
rails-html-sanitizer (1.3.0)
|
rails-html-sanitizer (1.3.0)
|
||||||
loofah (~> 2.3)
|
loofah (~> 2.3)
|
||||||
railties (6.0.3.7)
|
railties (6.1.4)
|
||||||
actionpack (= 6.0.3.7)
|
actionpack (= 6.1.4)
|
||||||
activesupport (= 6.0.3.7)
|
activesupport (= 6.1.4)
|
||||||
method_source
|
method_source
|
||||||
rake (>= 0.8.7)
|
rake (>= 0.13)
|
||||||
thor (>= 0.20.3, < 2.0)
|
thor (~> 1.0)
|
||||||
rainbow (3.0.0)
|
rainbow (3.0.0)
|
||||||
rake (13.0.3)
|
rake (13.0.6)
|
||||||
rb-fsevent (0.10.4)
|
rb-fsevent (0.11.0)
|
||||||
rb-inotify (0.10.1)
|
rb-inotify (0.10.1)
|
||||||
ffi (~> 1.0)
|
ffi (~> 1.0)
|
||||||
redis (4.2.1)
|
redis (4.4.0)
|
||||||
redis-namespace (1.8.0)
|
redis-namespace (1.8.1)
|
||||||
redis (>= 3.0.4)
|
redis (>= 3.0.4)
|
||||||
redis-rack-cache (2.2.1)
|
regexp_parser (2.1.1)
|
||||||
rack-cache (>= 1.10, < 2)
|
representable (3.1.1)
|
||||||
redis-store (>= 1.6, < 2)
|
|
||||||
redis-store (1.9.0)
|
|
||||||
redis (>= 4, < 5)
|
|
||||||
regexp_parser (1.7.1)
|
|
||||||
representable (3.0.4)
|
|
||||||
declarative (< 0.1.0)
|
declarative (< 0.1.0)
|
||||||
declarative-option (< 0.2.0)
|
trailblazer-option (>= 0.1.1, < 0.2.0)
|
||||||
uber (< 0.2.0)
|
uber (< 0.2.0)
|
||||||
responders (3.0.1)
|
responders (3.0.1)
|
||||||
actionpack (>= 5.0)
|
actionpack (>= 5.0)
|
||||||
@ -439,57 +449,53 @@ GEM
|
|||||||
netrc (~> 0.8)
|
netrc (~> 0.8)
|
||||||
retriable (3.1.2)
|
retriable (3.1.2)
|
||||||
rexml (3.2.5)
|
rexml (3.2.5)
|
||||||
rspec-core (3.9.2)
|
rspec-core (3.10.1)
|
||||||
rspec-support (~> 3.9.3)
|
rspec-support (~> 3.10.0)
|
||||||
rspec-expectations (3.9.2)
|
rspec-expectations (3.10.1)
|
||||||
diff-lcs (>= 1.2.0, < 2.0)
|
diff-lcs (>= 1.2.0, < 2.0)
|
||||||
rspec-support (~> 3.9.0)
|
rspec-support (~> 3.10.0)
|
||||||
rspec-mocks (3.9.1)
|
rspec-mocks (3.10.2)
|
||||||
diff-lcs (>= 1.2.0, < 2.0)
|
diff-lcs (>= 1.2.0, < 2.0)
|
||||||
rspec-support (~> 3.9.0)
|
rspec-support (~> 3.10.0)
|
||||||
rspec-rails (4.0.1)
|
rspec-rails (5.0.1)
|
||||||
actionpack (>= 4.2)
|
actionpack (>= 5.2)
|
||||||
activesupport (>= 4.2)
|
activesupport (>= 5.2)
|
||||||
railties (>= 4.2)
|
railties (>= 5.2)
|
||||||
rspec-core (~> 3.9)
|
rspec-core (~> 3.10)
|
||||||
rspec-expectations (~> 3.9)
|
rspec-expectations (~> 3.10)
|
||||||
rspec-mocks (~> 3.9)
|
rspec-mocks (~> 3.10)
|
||||||
rspec-support (~> 3.9)
|
rspec-support (~> 3.10)
|
||||||
rspec-support (3.9.3)
|
rspec-support (3.10.2)
|
||||||
rubocop (0.89.1)
|
rubocop (1.18.4)
|
||||||
parallel (~> 1.10)
|
parallel (~> 1.10)
|
||||||
parser (>= 2.7.1.1)
|
parser (>= 3.0.0.0)
|
||||||
rainbow (>= 2.2.2, < 4.0)
|
rainbow (>= 2.2.2, < 4.0)
|
||||||
regexp_parser (>= 1.7)
|
regexp_parser (>= 1.8, < 3.0)
|
||||||
rexml
|
rexml
|
||||||
rubocop-ast (>= 0.3.0, < 1.0)
|
rubocop-ast (>= 1.8.0, < 2.0)
|
||||||
ruby-progressbar (~> 1.7)
|
ruby-progressbar (~> 1.7)
|
||||||
unicode-display_width (>= 1.4.0, < 2.0)
|
unicode-display_width (>= 1.4.0, < 3.0)
|
||||||
rubocop-ast (0.3.0)
|
rubocop-ast (1.8.0)
|
||||||
parser (>= 2.7.1.4)
|
parser (>= 3.0.1.1)
|
||||||
rubocop-performance (1.7.1)
|
rubocop-performance (1.11.4)
|
||||||
rubocop (>= 0.82.0)
|
rubocop (>= 1.7.0, < 2.0)
|
||||||
rubocop-rails (2.8.1)
|
rubocop-ast (>= 0.4.0)
|
||||||
|
rubocop-rails (2.11.3)
|
||||||
activesupport (>= 4.2.0)
|
activesupport (>= 4.2.0)
|
||||||
rack (>= 1.1)
|
rack (>= 1.1)
|
||||||
rubocop (>= 0.87.0)
|
rubocop (>= 1.7.0, < 2.0)
|
||||||
rubocop-rspec (1.43.2)
|
rubocop-rspec (2.4.0)
|
||||||
rubocop (~> 0.87)
|
rubocop (~> 1.0)
|
||||||
ruby-enum (0.9.0)
|
rubocop-ast (>= 1.1.0)
|
||||||
i18n
|
ruby-progressbar (1.11.0)
|
||||||
ruby-progressbar (1.10.1)
|
ruby-vips (2.1.2)
|
||||||
|
ffi (~> 1.12)
|
||||||
|
ruby2_keywords (0.0.5)
|
||||||
ruby2ruby (2.4.4)
|
ruby2ruby (2.4.4)
|
||||||
ruby_parser (~> 3.1)
|
ruby_parser (~> 3.1)
|
||||||
sexp_processor (~> 4.6)
|
sexp_processor (~> 4.6)
|
||||||
ruby_parser (3.15.0)
|
ruby_parser (3.16.0)
|
||||||
rubocop (>= 0.87.0)
|
sexp_processor (~> 4.15, >= 4.15.1)
|
||||||
sexp_processor (~> 4.9)
|
|
||||||
safe_yaml (1.0.5)
|
|
||||||
sass (3.7.4)
|
|
||||||
sass-listen (~> 4.0.0)
|
|
||||||
sass-listen (4.0.0)
|
|
||||||
rb-fsevent (~> 0.9, >= 0.9.4)
|
|
||||||
rb-inotify (~> 0.9, >= 0.9.7)
|
|
||||||
sassc (2.4.0)
|
sassc (2.4.0)
|
||||||
ffi (~> 1.9)
|
ffi (~> 1.9)
|
||||||
sassc-rails (2.1.2)
|
sassc-rails (2.1.2)
|
||||||
@ -498,21 +504,30 @@ GEM
|
|||||||
sprockets (> 3.0)
|
sprockets (> 3.0)
|
||||||
sprockets-rails
|
sprockets-rails
|
||||||
tilt
|
tilt
|
||||||
scout_apm (2.6.9)
|
scout_apm (4.1.1)
|
||||||
parser
|
parser
|
||||||
scss_lint (0.59.0)
|
|
||||||
sass (~> 3.5, >= 3.5.5)
|
|
||||||
seed_dump (3.3.1)
|
seed_dump (3.3.1)
|
||||||
activerecord (>= 4)
|
activerecord (>= 4)
|
||||||
activesupport (>= 4)
|
activesupport (>= 4)
|
||||||
selectize-rails (0.12.6)
|
selectize-rails (0.12.6)
|
||||||
semantic_range (2.3.0)
|
semantic_range (3.0.0)
|
||||||
sentry-raven (3.0.3)
|
sentry-rails (4.6.4)
|
||||||
|
railties (>= 5.0)
|
||||||
|
sentry-ruby-core (~> 4.6.0)
|
||||||
|
sentry-ruby (4.6.4)
|
||||||
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||||
faraday (>= 1.0)
|
faraday (>= 1.0)
|
||||||
sexp_processor (4.15.1)
|
sentry-ruby-core (= 4.6.4)
|
||||||
shoulda-matchers (4.4.1)
|
sentry-ruby-core (4.6.4)
|
||||||
activesupport (>= 4.2.0)
|
concurrent-ruby
|
||||||
sidekiq (6.1.1)
|
faraday
|
||||||
|
sentry-sidekiq (4.6.4)
|
||||||
|
sentry-ruby-core (~> 4.6.0)
|
||||||
|
sidekiq (>= 3.0)
|
||||||
|
sexp_processor (4.15.3)
|
||||||
|
shoulda-matchers (5.0.0)
|
||||||
|
activesupport (>= 5.2.0)
|
||||||
|
sidekiq (6.2.1)
|
||||||
connection_pool (>= 2.2.2)
|
connection_pool (>= 2.2.2)
|
||||||
rack (~> 2.0)
|
rack (~> 2.0)
|
||||||
redis (>= 4.2.0)
|
redis (>= 4.2.0)
|
||||||
@ -529,7 +544,7 @@ GEM
|
|||||||
json (>= 1.8, < 3)
|
json (>= 1.8, < 3)
|
||||||
simplecov-html (~> 0.10.0)
|
simplecov-html (~> 0.10.0)
|
||||||
simplecov-html (0.10.2)
|
simplecov-html (0.10.2)
|
||||||
slack-ruby-client (0.15.0)
|
slack-ruby-client (0.17.0)
|
||||||
faraday (>= 1.0)
|
faraday (>= 1.0)
|
||||||
faraday_middleware
|
faraday_middleware
|
||||||
gli
|
gli
|
||||||
@ -539,7 +554,7 @@ GEM
|
|||||||
spring-watcher-listen (2.0.1)
|
spring-watcher-listen (2.0.1)
|
||||||
listen (>= 2.7, < 4.0)
|
listen (>= 2.7, < 4.0)
|
||||||
spring (>= 1.2, < 3.0)
|
spring (>= 1.2, < 3.0)
|
||||||
sprockets (3.7.2)
|
sprockets (4.0.2)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
rack (> 1, < 3)
|
rack (> 1, < 3)
|
||||||
sprockets-rails (3.2.2)
|
sprockets-rails (3.2.2)
|
||||||
@ -547,27 +562,28 @@ GEM
|
|||||||
activesupport (>= 4.0)
|
activesupport (>= 4.0)
|
||||||
sprockets (>= 3.0.0)
|
sprockets (>= 3.0.0)
|
||||||
squasher (0.6.2)
|
squasher (0.6.2)
|
||||||
statsd-ruby (1.4.0)
|
statsd-ruby (1.5.0)
|
||||||
telegram-bot-ruby (0.12.0)
|
telegram-bot-ruby (0.16.0)
|
||||||
|
dry-inflector
|
||||||
faraday
|
faraday
|
||||||
inflecto
|
virtus (~> 2.0)
|
||||||
virtus
|
telephone_number (1.4.12)
|
||||||
telephone_number (1.4.9)
|
|
||||||
thor (1.1.0)
|
thor (1.1.0)
|
||||||
thread_safe (0.3.6)
|
thread_safe (0.3.6)
|
||||||
tilt (2.0.10)
|
tilt (2.0.10)
|
||||||
time_diff (0.3.0)
|
time_diff (0.3.0)
|
||||||
activesupport
|
activesupport
|
||||||
i18n
|
i18n
|
||||||
|
trailblazer-option (0.1.1)
|
||||||
twilio-ruby (5.32.0)
|
twilio-ruby (5.32.0)
|
||||||
faraday (~> 1.0.0)
|
faraday (~> 1.0.0)
|
||||||
jwt (>= 1.5, <= 2.5)
|
jwt (>= 1.5, <= 2.5)
|
||||||
nokogiri (>= 1.6, < 2.0)
|
nokogiri (>= 1.6, < 2.0)
|
||||||
twitty (0.1.1)
|
twitty (0.1.4)
|
||||||
oauth
|
oauth
|
||||||
tzinfo (1.2.9)
|
tzinfo (2.0.4)
|
||||||
thread_safe (~> 0.1)
|
concurrent-ruby (~> 1.0)
|
||||||
tzinfo-data (1.2020.1)
|
tzinfo-data (1.2021.1)
|
||||||
tzinfo (>= 1.0.0)
|
tzinfo (>= 1.0.0)
|
||||||
uber (0.1.0)
|
uber (0.1.0)
|
||||||
uglifier (4.2.0)
|
uglifier (4.2.0)
|
||||||
@ -575,46 +591,49 @@ GEM
|
|||||||
unf (0.1.4)
|
unf (0.1.4)
|
||||||
unf_ext
|
unf_ext
|
||||||
unf_ext (0.0.7.7)
|
unf_ext (0.0.7.7)
|
||||||
unicode-display_width (1.7.0)
|
unicode-display_width (2.0.0)
|
||||||
uniform_notifier (1.13.0)
|
uniform_notifier (1.14.2)
|
||||||
uri_template (0.7.0)
|
uri_template (0.7.0)
|
||||||
valid_email2 (3.3.1)
|
valid_email2 (4.0.0)
|
||||||
activemodel (>= 3.2)
|
activemodel (>= 3.2)
|
||||||
mail (~> 2.5)
|
mail (~> 2.5)
|
||||||
virtus (1.0.5)
|
virtus (2.0.0)
|
||||||
axiom-types (~> 0.1)
|
axiom-types (~> 0.1)
|
||||||
coercible (~> 1.0)
|
coercible (~> 1.0)
|
||||||
descendants_tracker (~> 0.0, >= 0.0.3)
|
descendants_tracker (~> 0.0, >= 0.0.3)
|
||||||
equalizer (~> 0.0, >= 0.0.9)
|
|
||||||
warden (1.2.9)
|
warden (1.2.9)
|
||||||
rack (>= 2.0.9)
|
rack (>= 2.0.9)
|
||||||
web-console (4.0.4)
|
web-console (4.1.0)
|
||||||
actionview (>= 6.0.0)
|
actionview (>= 6.0.0)
|
||||||
activemodel (>= 6.0.0)
|
activemodel (>= 6.0.0)
|
||||||
bindex (>= 0.4.0)
|
bindex (>= 0.4.0)
|
||||||
railties (>= 6.0.0)
|
railties (>= 6.0.0)
|
||||||
webmock (3.8.3)
|
webmock (3.13.0)
|
||||||
addressable (>= 2.3.6)
|
addressable (>= 2.3.6)
|
||||||
crack (>= 0.3.2)
|
crack (>= 0.3.2)
|
||||||
hashdiff (>= 0.4.0, < 2.0.0)
|
hashdiff (>= 0.4.0, < 2.0.0)
|
||||||
webpacker (5.2.1)
|
webpacker (5.4.0)
|
||||||
activesupport (>= 5.2)
|
activesupport (>= 5.2)
|
||||||
rack-proxy (>= 0.6.1)
|
rack-proxy (>= 0.6.1)
|
||||||
railties (>= 5.2)
|
railties (>= 5.2)
|
||||||
semantic_range (>= 2.3.0)
|
semantic_range (>= 2.3.0)
|
||||||
webpush (1.0.0)
|
webpush (1.1.0)
|
||||||
hkdf (~> 0.2)
|
hkdf (~> 0.2)
|
||||||
jwt (~> 2.0)
|
jwt (~> 2.0)
|
||||||
websocket-driver (0.7.3)
|
webrick (1.7.0)
|
||||||
|
websocket-driver (0.7.5)
|
||||||
websocket-extensions (>= 0.1.0)
|
websocket-extensions (>= 0.1.0)
|
||||||
websocket-extensions (0.1.5)
|
websocket-extensions (0.1.5)
|
||||||
wisper (2.0.0)
|
wisper (2.0.0)
|
||||||
zeitwerk (2.4.2)
|
zeitwerk (2.4.2)
|
||||||
|
|
||||||
PLATFORMS
|
PLATFORMS
|
||||||
ruby
|
arm64-darwin-20
|
||||||
|
x86_64-darwin-21
|
||||||
|
x86_64-linux
|
||||||
|
|
||||||
DEPENDENCIES
|
DEPENDENCIES
|
||||||
|
active_record_query_trace
|
||||||
activerecord-import
|
activerecord-import
|
||||||
acts-as-taggable-on
|
acts-as-taggable-on
|
||||||
administrate
|
administrate
|
||||||
@ -632,10 +651,12 @@ DEPENDENCIES
|
|||||||
commonmarker
|
commonmarker
|
||||||
cypress-on-rails (~> 1.0)
|
cypress-on-rails (~> 1.0)
|
||||||
database_cleaner
|
database_cleaner
|
||||||
|
ddtrace
|
||||||
devise
|
devise
|
||||||
devise-secure_password (~> 2.0)
|
devise-secure_password (~> 2.0)
|
||||||
devise_token_auth
|
devise_token_auth
|
||||||
dotenv-rails
|
dotenv-rails
|
||||||
|
down (~> 5.0)
|
||||||
facebook-messenger
|
facebook-messenger
|
||||||
factory_bot_rails
|
factory_bot_rails
|
||||||
faker
|
faker
|
||||||
@ -649,8 +670,9 @@ DEPENDENCIES
|
|||||||
haikunator
|
haikunator
|
||||||
hairtrigger
|
hairtrigger
|
||||||
hashie
|
hashie
|
||||||
|
image_processing
|
||||||
jbuilder
|
jbuilder
|
||||||
json_refs!
|
json_refs
|
||||||
json_schemer
|
json_schemer
|
||||||
jwt
|
jwt
|
||||||
kaminari
|
kaminari
|
||||||
@ -659,30 +681,30 @@ DEPENDENCIES
|
|||||||
liquid
|
liquid
|
||||||
listen
|
listen
|
||||||
maxminddb
|
maxminddb
|
||||||
mini_magick
|
mock_redis
|
||||||
mock_redis!
|
|
||||||
pg
|
pg
|
||||||
procore-sift
|
procore-sift
|
||||||
pry-rails
|
pry-rails
|
||||||
puma
|
puma
|
||||||
pundit
|
pundit
|
||||||
|
rack-attack
|
||||||
rack-cors
|
rack-cors
|
||||||
rack-timeout
|
rack-timeout
|
||||||
rails
|
rails
|
||||||
redis
|
redis
|
||||||
redis-namespace
|
redis-namespace
|
||||||
redis-rack-cache
|
|
||||||
responders
|
responders
|
||||||
rest-client
|
rest-client
|
||||||
rspec-rails (~> 4.0.0.beta2)
|
rspec-rails (~> 5.0.0)
|
||||||
rubocop
|
rubocop
|
||||||
rubocop-performance
|
rubocop-performance
|
||||||
rubocop-rails
|
rubocop-rails
|
||||||
rubocop-rspec
|
rubocop-rspec
|
||||||
scout_apm
|
scout_apm
|
||||||
scss_lint
|
|
||||||
seed_dump
|
seed_dump
|
||||||
sentry-raven
|
sentry-rails
|
||||||
|
sentry-ruby
|
||||||
|
sentry-sidekiq
|
||||||
shoulda-matchers
|
shoulda-matchers
|
||||||
sidekiq
|
sidekiq
|
||||||
sidekiq-cron
|
sidekiq-cron
|
||||||
@ -706,7 +728,7 @@ DEPENDENCIES
|
|||||||
wisper (= 2.0.0)
|
wisper (= 2.0.0)
|
||||||
|
|
||||||
RUBY VERSION
|
RUBY VERSION
|
||||||
ruby 2.7.3p183
|
ruby 3.0.2p107
|
||||||
|
|
||||||
BUNDLED WITH
|
BUNDLED WITH
|
||||||
2.1.4
|
2.2.25
|
||||||
|
|||||||
35
README.md
35
README.md
@ -29,9 +29,8 @@ ___
|
|||||||
Chatwoot is an open-source omnichannel customer support software. 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 open-source, 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 an open-source omnichannel customer support software. 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 open-source, 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 this time, 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.
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Features
|
## Features
|
||||||
|
|
||||||
Chatwoot gives an integrated view of conversations happening in different communication channels.
|
Chatwoot gives an integrated view of conversations happening in different communication channels.
|
||||||
|
|
||||||
@ -57,28 +56,22 @@ Other features include:
|
|||||||
- **Powerful API & Webhooks**: Extend the capability of the software using Chatwoot’s webhooks and APIs.
|
- **Powerful API & Webhooks**: Extend the capability of the software using Chatwoot’s webhooks and APIs.
|
||||||
- **Integrations**: Chatwoot natively integrates with Slack right now. Manage your conversations in Slack without logging into the dashboard.
|
- **Integrations**: Chatwoot natively integrates with Slack right now. Manage your conversations in Slack without logging into the dashboard.
|
||||||
|
|
||||||
---
|
## Documentation
|
||||||
|
|
||||||
### Documentation
|
Detailed documentation is available at [chatwoot.com/help-center](https://www.chatwoot.com/help-center).
|
||||||
|
|
||||||
Detailed documentation is available at [www.chatwoot.com/help-center](https://www.chatwoot.com/help-center).
|
## Translation process
|
||||||
|
|
||||||
### Translation process
|
|
||||||
|
|
||||||
The translation process for Chatwoot web and mobile app is managed at [https://translate.chatwoot.com](https://translate.chatwoot.com) using Crowdin. Please read the [translation guide](https://www.chatwoot.com/docs/contributing/translating-chatwoot-to-your-language) for contributing to Chatwoot.
|
The translation process for Chatwoot web and mobile app is managed at [https://translate.chatwoot.com](https://translate.chatwoot.com) using Crowdin. Please read the [translation guide](https://www.chatwoot.com/docs/contributing/translating-chatwoot-to-your-language) for contributing to Chatwoot.
|
||||||
|
|
||||||
---
|
## Branching model
|
||||||
|
|
||||||
### Branching model
|
|
||||||
|
|
||||||
We use the [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`.
|
If you are looking for a stable version, please use the `master` or tags labelled as `v1.x.x`.
|
||||||
|
|
||||||
---
|
## Deployment
|
||||||
|
|
||||||
### Deployment
|
### Heroku one-click deploy
|
||||||
|
|
||||||
#### Heroku one-click deploy
|
|
||||||
|
|
||||||
Deploying Chatwoot to Heroku is 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:
|
||||||
|
|
||||||
@ -86,13 +79,21 @@ Deploying Chatwoot to Heroku is a breeze. It's as simple as clicking this button
|
|||||||
|
|
||||||
Follow this [link](https://www.chatwoot.com/docs/environment-variables) to understand setting the correct environment variables for the app to work with all the features. There might be breakages if you do not set the relevant environment variables.
|
Follow this [link](https://www.chatwoot.com/docs/environment-variables) to understand setting the correct environment variables for the app to work with all the features. There might be breakages if you do not set the relevant environment variables.
|
||||||
|
|
||||||
#### Other deployment options
|
### Other deployment options
|
||||||
|
|
||||||
Please follow [deployment architecture guide](https://www.chatwoot.com/docs/deployment/architecture) to deploy with Docker or Caprover.
|
Please follow [deployment architecture guide](https://www.chatwoot.com/docs/deployment/architecture) to deploy with Docker or Caprover.
|
||||||
|
|
||||||
---
|
## Security
|
||||||
|
|
||||||
### Contributors ✨
|
Looking to report a vulnerability? Please refer our [SECURITY.md](./SECURITY.md) file.
|
||||||
|
|
||||||
|
|
||||||
|
## Community? Questions? Support ?
|
||||||
|
|
||||||
|
If you need help or just want to hang out, come, say hi on our [Discord](https://discord.gg/cJXdrwS) server.
|
||||||
|
|
||||||
|
|
||||||
|
## Contributors ✨
|
||||||
|
|
||||||
Thanks goes to all these [wonderful people](https://www.chatwoot.com/docs/contributors):
|
Thanks goes to all these [wonderful people](https://www.chatwoot.com/docs/contributors):
|
||||||
|
|
||||||
|
|||||||
25
SECURITY.md
25
SECURITY.md
@ -1,8 +1,31 @@
|
|||||||
# Security Policy
|
# Security Policy
|
||||||
|
Chatwoot is looking forward to working with security researchers across the world to keep Chatwoot and our users safe. If you have found an issue in our systems/applications, please reach out to us.
|
||||||
|
|
||||||
## Reporting a Vulnerability
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
We use [huntr.dev](https://huntr.dev/) for security issues that affect our project. If you believe you have found a vulnerability, please disclose it via this [form](https://huntr.dev/bounties/disclose).
|
We use [huntr.dev](https://huntr.dev/) for security issues that affect our project. If you believe you have found a vulnerability, please disclose it via this [form](https://huntr.dev/bounties/disclose).
|
||||||
|
|
||||||
This will enable us to review the vulnerability, fix it promptly, and reward you for your efforts.
|
This will enable us to review the vulnerability, fix it promptly, and reward you for your efforts.
|
||||||
|
|
||||||
If you have any questions about the process, feel free to reach out to hello@chatwoot.com.
|
If you have any questions about the process, feel free to reach out to security@chatwoot.com.
|
||||||
|
|
||||||
|
|
||||||
|
## Out of scope
|
||||||
|
|
||||||
|
Please do not perform testing against Chatwoot production services. Use a self hosted instance to perform tests.
|
||||||
|
|
||||||
|
We consider the following to be out of scope, though there may be exceptions.
|
||||||
|
|
||||||
|
- Missing HTTP security headers
|
||||||
|
- Self XSS
|
||||||
|
- HTTP Host Header XSS without working proof-of-concept
|
||||||
|
- Incomplete/Missing SPF/DKIM
|
||||||
|
- Denial of Service attacks
|
||||||
|
- DNSSEC
|
||||||
|
- Social Engineering attacks
|
||||||
|
|
||||||
|
If you are not sure about the scope, please create a report.
|
||||||
|
|
||||||
|
## Thanks
|
||||||
|
|
||||||
|
Thank you for keeping Chatwoot and our users safe. 🙇
|
||||||
|
|||||||
4
app.json
4
app.json
@ -28,6 +28,10 @@
|
|||||||
"FRONTEND_URL": {
|
"FRONTEND_URL": {
|
||||||
"description": "Public root URL of the Chatwoot installation. This will be used in the emails.",
|
"description": "Public root URL of the Chatwoot installation. This will be used in the emails.",
|
||||||
"value": "https://CHANGE.herokuapp.com"
|
"value": "https://CHANGE.herokuapp.com"
|
||||||
|
},
|
||||||
|
"INSTALLATION_ENV": {
|
||||||
|
"description": "Installation method used for Chatwoot.",
|
||||||
|
"value": "heroku"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"formation": {
|
"formation": {
|
||||||
|
|||||||
@ -3,8 +3,9 @@ class ContactIdentifyAction
|
|||||||
|
|
||||||
def perform
|
def perform
|
||||||
ActiveRecord::Base.transaction do
|
ActiveRecord::Base.transaction do
|
||||||
@contact = merge_contact(existing_identified_contact, @contact) if merge_contacts?(existing_identified_contact, @contact)
|
merge_if_existing_identified_contact
|
||||||
@contact = merge_contact(existing_email_contact, @contact) if merge_contacts?(existing_email_contact, @contact)
|
merge_if_existing_email_contact
|
||||||
|
merge_if_existing_phone_number_contact
|
||||||
update_contact
|
update_contact
|
||||||
end
|
end
|
||||||
@contact
|
@contact
|
||||||
@ -16,6 +17,18 @@ class ContactIdentifyAction
|
|||||||
@account ||= @contact.account
|
@account ||= @contact.account
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def merge_if_existing_identified_contact
|
||||||
|
@contact = merge_contact(existing_identified_contact, @contact) if merge_contacts?(existing_identified_contact, @contact)
|
||||||
|
end
|
||||||
|
|
||||||
|
def merge_if_existing_email_contact
|
||||||
|
@contact = merge_contact(existing_email_contact, @contact) if merge_contacts?(existing_email_contact, @contact)
|
||||||
|
end
|
||||||
|
|
||||||
|
def merge_if_existing_phone_number_contact
|
||||||
|
@contact = merge_contact(existing_phone_number_contact, @contact) if merge_contacts?(existing_phone_number_contact, @contact)
|
||||||
|
end
|
||||||
|
|
||||||
def existing_identified_contact
|
def existing_identified_contact
|
||||||
return if params[:identifier].blank?
|
return if params[:identifier].blank?
|
||||||
|
|
||||||
@ -28,6 +41,12 @@ class ContactIdentifyAction
|
|||||||
@existing_email_contact ||= Contact.where(account_id: account.id).find_by(email: params[:email])
|
@existing_email_contact ||= Contact.where(account_id: account.id).find_by(email: params[:email])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def existing_phone_number_contact
|
||||||
|
return if params[:phone_number].blank?
|
||||||
|
|
||||||
|
@existing_phone_number_contact ||= Contact.where(account_id: account.id).find_by(phone_number: params[:phone_number])
|
||||||
|
end
|
||||||
|
|
||||||
def merge_contacts?(existing_contact, _contact)
|
def merge_contacts?(existing_contact, _contact)
|
||||||
existing_contact && existing_contact.id != @contact.id
|
existing_contact && existing_contact.id != @contact.id
|
||||||
end
|
end
|
||||||
@ -36,7 +55,9 @@ class ContactIdentifyAction
|
|||||||
custom_attributes = params[:custom_attributes] ? @contact.custom_attributes.merge(params[:custom_attributes]) : @contact.custom_attributes
|
custom_attributes = params[:custom_attributes] ? @contact.custom_attributes.merge(params[:custom_attributes]) : @contact.custom_attributes
|
||||||
# blank identifier or email will throw unique index error
|
# blank identifier or email will throw unique index error
|
||||||
# TODO: replace reject { |_k, v| v.blank? } with compact_blank when rails is upgraded
|
# TODO: replace reject { |_k, v| v.blank? } with compact_blank when rails is upgraded
|
||||||
@contact.update!(params.slice(:name, :email, :identifier).reject { |_k, v| v.blank? }.merge({ custom_attributes: custom_attributes }))
|
@contact.update!(params.slice(:name, :email, :identifier, :phone_number).reject do |_k, v|
|
||||||
|
v.blank?
|
||||||
|
end.merge({ custom_attributes: custom_attributes }))
|
||||||
ContactAvatarJob.perform_later(@contact, params[:avatar_url]) if params[:avatar_url].present?
|
ContactAvatarJob.perform_later(@contact, params[:avatar_url]) if params[:avatar_url].present?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@ -38,17 +38,30 @@ class ContactBuilder
|
|||||||
end
|
end
|
||||||
|
|
||||||
def find_contact
|
def find_contact
|
||||||
contact = nil
|
contact = find_contact_by_identifier(contact_attributes[:identifier])
|
||||||
|
contact ||= find_contact_by_email(contact_attributes[:email])
|
||||||
contact = account.contacts.find_by(identifier: contact_attributes[:identifier]) if contact_attributes[:identifier].present?
|
contact ||= find_contact_by_phone_number(contact_attributes[:phone_number])
|
||||||
|
|
||||||
contact ||= account.contacts.find_by(email: contact_attributes[:email]) if contact_attributes[:email].present?
|
|
||||||
|
|
||||||
contact ||= account.contacts.find_by(phone_number: contact_attributes[:phone_number]) if contact_attributes[:phone_number].present?
|
|
||||||
|
|
||||||
contact
|
contact
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def find_contact_by_identifier(identifier)
|
||||||
|
return if identifier.blank?
|
||||||
|
|
||||||
|
account.contacts.find_by(identifier: identifier)
|
||||||
|
end
|
||||||
|
|
||||||
|
def find_contact_by_email(email)
|
||||||
|
return if email.blank?
|
||||||
|
|
||||||
|
account.contacts.find_by(email: email.downcase)
|
||||||
|
end
|
||||||
|
|
||||||
|
def find_contact_by_phone_number(phone_number)
|
||||||
|
return if phone_number.blank?
|
||||||
|
|
||||||
|
account.contacts.find_by(phone_number: phone_number)
|
||||||
|
end
|
||||||
|
|
||||||
def build_contact_inbox
|
def build_contact_inbox
|
||||||
ActiveRecord::Base.transaction do
|
ActiveRecord::Base.transaction do
|
||||||
contact = find_contact || create_contact
|
contact = find_contact || create_contact
|
||||||
@ -57,6 +70,7 @@ class ContactBuilder
|
|||||||
contact_inbox
|
contact_inbox
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
Rails.logger.info e
|
Rails.logger.info e
|
||||||
|
raise e
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@ -17,12 +17,18 @@ class Messages::Facebook::MessageBuilder
|
|||||||
end
|
end
|
||||||
|
|
||||||
def perform
|
def perform
|
||||||
|
# This channel might require reauthorization, may be owner might have changed the fb password
|
||||||
|
return if @inbox.channel.reauthorization_required?
|
||||||
|
|
||||||
ActiveRecord::Base.transaction do
|
ActiveRecord::Base.transaction do
|
||||||
build_contact
|
build_contact
|
||||||
build_message
|
build_message
|
||||||
end
|
end
|
||||||
|
ensure_contact_avatar
|
||||||
|
rescue Koala::Facebook::AuthenticationError
|
||||||
|
Rails.logger.info "Facebook Authorization expired for Inbox #{@inbox.id}"
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
Raven.capture_exception(e)
|
Sentry.capture_exception(e)
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -36,7 +42,6 @@ class Messages::Facebook::MessageBuilder
|
|||||||
return if contact.present?
|
return if contact.present?
|
||||||
|
|
||||||
@contact = Contact.create!(contact_params.except(:remote_avatar_url))
|
@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)
|
@contact_inbox = ContactInbox.create(contact: contact, inbox: @inbox, source_id: @sender_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -56,10 +61,21 @@ class Messages::Facebook::MessageBuilder
|
|||||||
end
|
end
|
||||||
|
|
||||||
def attach_file(attachment, file_url)
|
def attach_file(attachment, file_url)
|
||||||
file_resource = LocalResource.new(file_url)
|
attachment_file = Down.download(
|
||||||
attachment.file.attach(io: file_resource.file, filename: file_resource.filename, content_type: file_resource.encoding)
|
file_url
|
||||||
rescue *ExceptionList::URI_EXCEPTIONS => e
|
)
|
||||||
Rails.logger.info "invalid url #{file_url} : #{e.message}"
|
attachment.file.attach(
|
||||||
|
io: attachment_file,
|
||||||
|
filename: attachment_file.original_filename,
|
||||||
|
content_type: attachment_file.content_type
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def ensure_contact_avatar
|
||||||
|
return if contact_params[:remote_avatar_url].blank?
|
||||||
|
return if @contact.avatar.attached?
|
||||||
|
|
||||||
|
ContactAvatarJob.perform_later(@contact, contact_params[:remote_avatar_url])
|
||||||
end
|
end
|
||||||
|
|
||||||
def conversation
|
def conversation
|
||||||
@ -136,9 +152,12 @@ class Messages::Facebook::MessageBuilder
|
|||||||
begin
|
begin
|
||||||
k = Koala::Facebook::API.new(@inbox.channel.page_access_token) if @inbox.facebook?
|
k = Koala::Facebook::API.new(@inbox.channel.page_access_token) if @inbox.facebook?
|
||||||
result = k.get_object(@sender_id) || {}
|
result = k.get_object(@sender_id) || {}
|
||||||
|
rescue Koala::Facebook::AuthenticationError
|
||||||
|
@inbox.channel.authorization_error!
|
||||||
|
raise
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
result = {}
|
result = {}
|
||||||
Raven.capture_exception(e)
|
Sentry.capture_exception(e)
|
||||||
end
|
end
|
||||||
{
|
{
|
||||||
name: "#{result['first_name'] || 'John'} #{result['last_name'] || 'Doe'}",
|
name: "#{result['first_name'] || 'John'} #{result['last_name'] || 'Doe'}",
|
||||||
|
|||||||
@ -14,7 +14,7 @@ class Api::V1::Accounts::BaseController < Api::BaseController
|
|||||||
account = Account.find(params[:account_id])
|
account = Account.find(params[:account_id])
|
||||||
if current_user
|
if current_user
|
||||||
account_accessible_for_user?(account)
|
account_accessible_for_user?(account)
|
||||||
elsif @resource&.is_a?(AgentBot)
|
elsif @resource.is_a?(AgentBot)
|
||||||
account_accessible_for_bot?(account)
|
account_accessible_for_bot?(account)
|
||||||
end
|
end
|
||||||
account
|
account
|
||||||
|
|||||||
@ -69,39 +69,15 @@ class Api::V1::Accounts::CallbacksController < Api::V1::Accounts::BaseController
|
|||||||
return [] if data.empty?
|
return [] if data.empty?
|
||||||
|
|
||||||
data.inject([]) do |result, page_detail|
|
data.inject([]) do |result, page_detail|
|
||||||
page_detail[:exists] = Current.account.facebook_pages.exists?(page_id: page_detail['id']) ? true : false
|
page_detail[:exists] = Current.account.facebook_pages.exists?(page_id: page_detail['id'])
|
||||||
result << page_detail
|
result << page_detail
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_avatar(facebook_inbox, page_id)
|
def set_avatar(facebook_inbox, page_id)
|
||||||
uri = get_avatar_url(page_id)
|
avatar_file = Down.download(
|
||||||
|
"http://graph.facebook.com/#{page_id}/picture?type=large"
|
||||||
return unless uri
|
)
|
||||||
|
facebook_inbox.avatar.attach(io: avatar_file, filename: avatar_file.original_filename, content_type: avatar_file.content_type)
|
||||||
avatar_resource = LocalResource.new(uri)
|
|
||||||
facebook_inbox.avatar.attach(io: avatar_resource.file, filename: avatar_resource.tmp_filename, content_type: avatar_resource.encoding)
|
|
||||||
rescue *ExceptionList::URI_EXCEPTIONS => e
|
|
||||||
Rails.logger.info "invalid url #{file_url} : #{e.message}"
|
|
||||||
end
|
|
||||||
|
|
||||||
def get_avatar_url(page_id)
|
|
||||||
begin
|
|
||||||
url = 'http://graph.facebook.com/' << page_id << '/picture?type=large'
|
|
||||||
uri = URI.parse(url)
|
|
||||||
tries = 3
|
|
||||||
begin
|
|
||||||
response = uri.open(redirect: false)
|
|
||||||
rescue OpenURI::HTTPRedirect => e
|
|
||||||
uri = e.uri # assigned from the "Location" response header
|
|
||||||
retry if (tries -= 1).positive?
|
|
||||||
raise
|
|
||||||
end
|
|
||||||
pic_url = response.base_uri.to_s
|
|
||||||
rescue StandardError => e
|
|
||||||
Rails.logger.debug "Rescued: #{e.inspect}"
|
|
||||||
pic_url = nil
|
|
||||||
end
|
|
||||||
pic_url
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@ -6,9 +6,8 @@ class Api::V1::Accounts::Channels::TwilioChannelsController < Api::V1::Accounts:
|
|||||||
authenticate_twilio
|
authenticate_twilio
|
||||||
build_inbox
|
build_inbox
|
||||||
setup_webhooks if @twilio_channel.sms?
|
setup_webhooks if @twilio_channel.sms?
|
||||||
rescue Twilio::REST::TwilioError => e
|
|
||||||
render_could_not_create_error(e.message)
|
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
|
Sentry.capture_exception(e)
|
||||||
render_could_not_create_error(e.message)
|
render_could_not_create_error(e.message)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@ -11,6 +11,7 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
|||||||
before_action :check_authorization
|
before_action :check_authorization
|
||||||
before_action :set_current_page, only: [:index, :active, :search]
|
before_action :set_current_page, only: [:index, :active, :search]
|
||||||
before_action :fetch_contact, only: [:show, :update, :contactable_inboxes]
|
before_action :fetch_contact, only: [:show, :update, :contactable_inboxes]
|
||||||
|
before_action :set_include_contact_inboxes, only: [:index, :search]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@contacts_count = resolved_contacts.count
|
@contacts_count = resolved_contacts.count
|
||||||
@ -21,7 +22,7 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
|||||||
render json: { error: 'Specify search string with parameter q' }, status: :unprocessable_entity if params[:q].blank? && return
|
render json: { error: 'Specify search string with parameter q' }, status: :unprocessable_entity if params[:q].blank? && return
|
||||||
|
|
||||||
contacts = resolved_contacts.where(
|
contacts = resolved_contacts.where(
|
||||||
'name ILIKE :search OR email ILIKE :search OR phone_number ILIKE :search',
|
'name ILIKE :search OR email ILIKE :search OR phone_number ILIKE :search OR contacts.identifier LIKE :search',
|
||||||
search: "%#{params[:q]}%"
|
search: "%#{params[:q]}%"
|
||||||
)
|
)
|
||||||
@contacts_count = contacts.count
|
@contacts_count = contacts.count
|
||||||
@ -87,11 +88,15 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def fetch_contacts_with_conversation_count(contacts)
|
def fetch_contacts_with_conversation_count(contacts)
|
||||||
filtrate(contacts).left_outer_joins(:conversations)
|
contacts_with_conversation_count = filtrate(contacts).left_outer_joins(:conversations)
|
||||||
.select('contacts.*, COUNT(conversations.id) as conversations_count')
|
.select('contacts.*, COUNT(conversations.id) as conversations_count')
|
||||||
.group('contacts.id')
|
.group('contacts.id')
|
||||||
.includes([{ avatar_attachment: [:blob] }, { contact_inboxes: [:inbox] }])
|
.includes([{ avatar_attachment: [:blob] }])
|
||||||
.page(@current_page).per(RESULTS_PER_PAGE)
|
.page(@current_page).per(RESULTS_PER_PAGE)
|
||||||
|
|
||||||
|
return contacts_with_conversation_count.includes([{ contact_inboxes: [:inbox] }]) if @include_contact_inboxes
|
||||||
|
|
||||||
|
contacts_with_conversation_count
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_contact_inbox
|
def build_contact_inbox
|
||||||
@ -103,7 +108,7 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def contact_params
|
def contact_params
|
||||||
params.require(:contact).permit(:name, :email, :phone_number, additional_attributes: {}, custom_attributes: {})
|
params.require(:contact).permit(:name, :identifier, :email, :phone_number, additional_attributes: {}, custom_attributes: {})
|
||||||
end
|
end
|
||||||
|
|
||||||
def contact_custom_attributes
|
def contact_custom_attributes
|
||||||
@ -117,6 +122,14 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
|||||||
contact_params.except(:custom_attributes).merge({ custom_attributes: contact_custom_attributes })
|
contact_params.except(:custom_attributes).merge({ custom_attributes: contact_custom_attributes })
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def set_include_contact_inboxes
|
||||||
|
@include_contact_inboxes = if params[:include_contact_inboxes].present?
|
||||||
|
params[:include_contact_inboxes] == 'true'
|
||||||
|
else
|
||||||
|
true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def fetch_contact
|
def fetch_contact
|
||||||
@contact = Current.account.contacts.includes(contact_inboxes: [:inbox]).find(params[:id])
|
@contact = Current.account.contacts.includes(contact_inboxes: [:inbox]).find(params[:id])
|
||||||
end
|
end
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseController
|
class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseController
|
||||||
include Events::Types
|
include Events::Types
|
||||||
|
include DateRangeHelper
|
||||||
|
|
||||||
before_action :conversation, except: [:index, :meta, :search, :create]
|
before_action :conversation, except: [:index, :meta, :search, :create]
|
||||||
before_action :contact_inbox, only: [:create]
|
before_action :contact_inbox, only: [:create]
|
||||||
@ -49,8 +50,8 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
|
|||||||
|
|
||||||
def toggle_status
|
def toggle_status
|
||||||
if params[:status]
|
if params[:status]
|
||||||
@conversation.status = params[:status]
|
set_conversation_status
|
||||||
@status = @conversation.save
|
@status = @conversation.save!
|
||||||
else
|
else
|
||||||
@status = @conversation.toggle_status
|
@status = @conversation.toggle_status
|
||||||
end
|
end
|
||||||
@ -73,6 +74,12 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def set_conversation_status
|
||||||
|
status = params[:status] == 'bot' ? 'pending' : params[:status]
|
||||||
|
@conversation.status = status
|
||||||
|
@conversation.snoozed_until = parse_date_time(params[:snoozed_until].to_s) if params[:snoozed_until]
|
||||||
|
end
|
||||||
|
|
||||||
def trigger_typing_event(event)
|
def trigger_typing_event(event)
|
||||||
user = current_user.presence || @resource
|
user = current_user.presence || @resource
|
||||||
Rails.configuration.dispatcher.dispatch(event, Time.zone.now, conversation: @conversation, user: user)
|
Rails.configuration.dispatcher.dispatch(event, Time.zone.now, conversation: @conversation, user: user)
|
||||||
@ -106,12 +113,16 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
|
|||||||
def conversation_params
|
def conversation_params
|
||||||
additional_attributes = params[:additional_attributes]&.permit! || {}
|
additional_attributes = params[:additional_attributes]&.permit! || {}
|
||||||
status = params[:status].present? ? { status: params[:status] } : {}
|
status = params[:status].present? ? { status: params[:status] } : {}
|
||||||
|
|
||||||
|
# TODO: temporary fallback for the old bot status in conversation, we will remove after couple of releases
|
||||||
|
status = { status: 'pending' } if status[:status] == 'bot'
|
||||||
{
|
{
|
||||||
account_id: Current.account.id,
|
account_id: Current.account.id,
|
||||||
inbox_id: @contact_inbox.inbox_id,
|
inbox_id: @contact_inbox.inbox_id,
|
||||||
contact_id: @contact_inbox.contact_id,
|
contact_id: @contact_inbox.contact_id,
|
||||||
contact_inbox_id: @contact_inbox.id,
|
contact_inbox_id: @contact_inbox.id,
|
||||||
additional_attributes: additional_attributes
|
additional_attributes: additional_attributes,
|
||||||
|
snoozed_until: params[:snoozed_until]
|
||||||
}.merge(status)
|
}.merge(status)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,50 @@
|
|||||||
|
class Api::V1::Accounts::CustomAttributeDefinitionsController < Api::V1::Accounts::BaseController
|
||||||
|
before_action :fetch_custom_attributes_definitions, except: [:create]
|
||||||
|
before_action :fetch_custom_attribute_definition, only: [:show, :update, :destroy]
|
||||||
|
DEFAULT_ATTRIBUTE_MODEL = 'conversation_attribute'.freeze
|
||||||
|
|
||||||
|
def index; end
|
||||||
|
|
||||||
|
def show; end
|
||||||
|
|
||||||
|
def create
|
||||||
|
@custom_attribute_definition = Current.account.custom_attribute_definitions.create!(
|
||||||
|
permitted_payload
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
@custom_attribute_definition.update!(permitted_payload)
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
@custom_attribute_definition.destroy
|
||||||
|
head :no_content
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def fetch_custom_attributes_definitions
|
||||||
|
@custom_attribute_definitions = Current.account.custom_attribute_definitions.where(
|
||||||
|
attribute_model: permitted_params[:attribute_model] || DEFAULT_ATTRIBUTE_MODEL
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def fetch_custom_attribute_definition
|
||||||
|
@custom_attribute_definition = @custom_attribute_definitions.find(permitted_params[:id])
|
||||||
|
end
|
||||||
|
|
||||||
|
def permitted_payload
|
||||||
|
params.require(:custom_attribute_definition).permit(
|
||||||
|
:attribute_display_name,
|
||||||
|
:attribute_display_type,
|
||||||
|
:attribute_key,
|
||||||
|
:attribute_model,
|
||||||
|
:default_value
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def permitted_params
|
||||||
|
params.permit(:id, :filter_type)
|
||||||
|
end
|
||||||
|
end
|
||||||
@ -9,7 +9,7 @@ class Api::V1::Accounts::InboxMembersController < Api::V1::Accounts::BaseControl
|
|||||||
update_agents_list
|
update_agents_list
|
||||||
head :ok
|
head :ok
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
Rails.logger.debug "Rescued: #{e.inspect}"
|
Rails.logger.debug { "Rescued: #{e.inspect}" }
|
||||||
render_could_not_create_error('Could not add agents to inbox')
|
render_could_not_create_error('Could not add agents to inbox')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@ -10,7 +10,7 @@ class Api::V1::WebhooksController < ApplicationController
|
|||||||
twitter_consumer.consume
|
twitter_consumer.consume
|
||||||
head :ok
|
head :ok
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
Raven.capture_exception(e)
|
Sentry.capture_exception(e)
|
||||||
head :ok
|
head :ok
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@ -94,6 +94,10 @@ class Api::V1::Widget::BaseController < ApplicationController
|
|||||||
{ timestamp: permitted_params[:message][:timestamp] }
|
{ timestamp: permitted_params[:message][:timestamp] }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def permitted_params
|
||||||
|
params.permit(:website_token)
|
||||||
|
end
|
||||||
|
|
||||||
def message_params
|
def message_params
|
||||||
{
|
{
|
||||||
account_id: conversation.account_id,
|
account_id: conversation.account_id,
|
||||||
|
|||||||
@ -4,10 +4,4 @@ class Api::V1::Widget::CampaignsController < Api::V1::Widget::BaseController
|
|||||||
def index
|
def index
|
||||||
@campaigns = @web_widget.inbox.campaigns.where(enabled: true)
|
@campaigns = @web_widget.inbox.campaigns.where(enabled: true)
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def permitted_params
|
|
||||||
params.permit(:website_token)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|||||||
41
app/controllers/api/v1/widget/configs_controller.rb
Normal file
41
app/controllers/api/v1/widget/configs_controller.rb
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
class Api::V1::Widget::ConfigsController < Api::V1::Widget::BaseController
|
||||||
|
before_action :set_global_config
|
||||||
|
|
||||||
|
def create
|
||||||
|
build_contact
|
||||||
|
set_token
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_global_config
|
||||||
|
@global_config = GlobalConfig.get('LOGO_THUMBNAIL', 'BRAND_NAME', 'WIDGET_BRAND_URL')
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_contact
|
||||||
|
@contact_inbox = @web_widget.inbox.contact_inboxes.find_by(
|
||||||
|
source_id: auth_token_params[:source_id]
|
||||||
|
)
|
||||||
|
@contact = @contact_inbox&.contact
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_contact
|
||||||
|
return if @contact.present?
|
||||||
|
|
||||||
|
@contact_inbox = @web_widget.create_contact_inbox(additional_attributes)
|
||||||
|
@contact = @contact_inbox.contact
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_token
|
||||||
|
payload = { source_id: @contact_inbox.source_id, inbox_id: @web_widget.inbox.id }
|
||||||
|
@token = ::Widget::TokenService.new(payload: payload).generate_token
|
||||||
|
end
|
||||||
|
|
||||||
|
def additional_attributes
|
||||||
|
if @web_widget.inbox.account.feature_enabled?('ip_lookup')
|
||||||
|
{ created_at_ip: request.remote_ip }
|
||||||
|
else
|
||||||
|
{}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@ -29,6 +29,6 @@ class Api::V1::Widget::ContactsController < Api::V1::Widget::BaseController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def permitted_params
|
def permitted_params
|
||||||
params.permit(:website_token, :identifier, :identifier_hash, :email, :name, :avatar_url, custom_attributes: {})
|
params.permit(:website_token, :identifier, :identifier_hash, :email, :name, :avatar_url, :phone_number, custom_attributes: {})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@ -4,10 +4,4 @@ class Api::V1::Widget::InboxMembersController < Api::V1::Widget::BaseController
|
|||||||
def index
|
def index
|
||||||
@inbox_members = @web_widget.inbox.inbox_members.includes(:user)
|
@inbox_members = @web_widget.inbox.inbox_members.includes(:user)
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def permitted_params
|
|
||||||
params.permit(:website_token)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|||||||
@ -16,7 +16,7 @@ class ApplicationController < ActionController::Base
|
|||||||
def handle_with_exception
|
def handle_with_exception
|
||||||
yield
|
yield
|
||||||
rescue ActiveRecord::RecordNotFound => e
|
rescue ActiveRecord::RecordNotFound => e
|
||||||
Raven.capture_exception(e)
|
Sentry.capture_exception(e)
|
||||||
render_not_found_error('Resource could not be found')
|
render_not_found_error('Resource could not be found')
|
||||||
rescue Pundit::NotAuthorizedError
|
rescue Pundit::NotAuthorizedError
|
||||||
render_unauthorized('You are not authorized to do this action')
|
render_unauthorized('You are not authorized to do this action')
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
module AuthHelper
|
module AuthHelper
|
||||||
def send_auth_headers(user)
|
def send_auth_headers(user)
|
||||||
data = user.create_new_auth_token
|
data = user.create_new_auth_token
|
||||||
response.headers[DeviseTokenAuth.headers_names[:"access-token"]] = data['access-token']
|
response.headers[DeviseTokenAuth.headers_names[:'access-token']] = data['access-token']
|
||||||
response.headers[DeviseTokenAuth.headers_names[:"token-type"]] = 'Bearer'
|
response.headers[DeviseTokenAuth.headers_names[:'token-type']] = 'Bearer'
|
||||||
response.headers[DeviseTokenAuth.headers_names[:client]] = data['client']
|
response.headers[DeviseTokenAuth.headers_names[:client]] = data['client']
|
||||||
response.headers[DeviseTokenAuth.headers_names[:expiry]] = data['expiry']
|
response.headers[DeviseTokenAuth.headers_names[:expiry]] = data['expiry']
|
||||||
response.headers[DeviseTokenAuth.headers_names[:uid]] = data['uid']
|
response.headers[DeviseTokenAuth.headers_names[:uid]] = data['uid']
|
||||||
|
|||||||
32
app/controllers/public/api/v1/csat_survey_controller.rb
Normal file
32
app/controllers/public/api/v1/csat_survey_controller.rb
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
class Public::Api::V1::CsatSurveyController < PublicController
|
||||||
|
before_action :set_conversation
|
||||||
|
before_action :set_message
|
||||||
|
|
||||||
|
def show; end
|
||||||
|
|
||||||
|
def update
|
||||||
|
render json: { error: 'You cannot update the CSAT survey after 14 days' }, status: :unprocessable_entity and return if check_csat_locked
|
||||||
|
|
||||||
|
@message.update!(message_update_params[:message])
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_conversation
|
||||||
|
return if params[:id].blank?
|
||||||
|
|
||||||
|
@conversation = Conversation.find_by!(uuid: params[:id])
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_message
|
||||||
|
@message = @conversation.messages.find_by!(content_type: 'input_csat')
|
||||||
|
end
|
||||||
|
|
||||||
|
def message_update_params
|
||||||
|
params.permit(message: [{ submitted_values: [:name, :title, :value, { csat_survey_response: [:feedback_message, :rating] }] }])
|
||||||
|
end
|
||||||
|
|
||||||
|
def check_csat_locked
|
||||||
|
(Time.zone.now.to_date - @message.created_at.to_date).to_i > 14
|
||||||
|
end
|
||||||
|
end
|
||||||
@ -14,7 +14,7 @@ class Public::Api::V1::Inboxes::MessagesController < Public::Api::V1::InboxesCon
|
|||||||
def update
|
def update
|
||||||
@message.update!(message_update_params)
|
@message.update!(message_update_params)
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
render json: { error: @contact.errors, message: e.message }.to_json, status: 500
|
render json: { error: @contact.errors, message: e.message }.to_json, status: :internal_server_error
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|||||||
10
app/controllers/survey/responses_controller.rb
Normal file
10
app/controllers/survey/responses_controller.rb
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
class Survey::ResponsesController < ActionController::Base
|
||||||
|
before_action :set_global_config
|
||||||
|
def show; end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_global_config
|
||||||
|
@global_config = GlobalConfig.get('LOGO_THUMBNAIL', 'BRAND_NAME', 'WIDGET_BRAND_URL')
|
||||||
|
end
|
||||||
|
end
|
||||||
@ -3,7 +3,7 @@ class SwaggerController < ApplicationController
|
|||||||
if Rails.env.development? || Rails.env.test?
|
if Rails.env.development? || Rails.env.test?
|
||||||
render inline: File.read(Rails.root.join('swagger', derived_path))
|
render inline: File.read(Rails.root.join('swagger', derived_path))
|
||||||
else
|
else
|
||||||
head 404
|
head :not_found
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
# TODO : Delete this and associated spec once 'api/widget/config' end point is merged
|
||||||
class WidgetsController < ActionController::Base
|
class WidgetsController < ActionController::Base
|
||||||
before_action :set_global_config
|
before_action :set_global_config
|
||||||
before_action :set_web_widget
|
before_action :set_web_widget
|
||||||
|
|||||||
@ -114,13 +114,13 @@ class ConversationFinder
|
|||||||
end
|
end
|
||||||
|
|
||||||
def current_page
|
def current_page
|
||||||
params[:page]
|
params[:page] || 1
|
||||||
end
|
end
|
||||||
|
|
||||||
def conversations
|
def conversations
|
||||||
@conversations = @conversations.includes(
|
@conversations = @conversations.includes(
|
||||||
:taggings, :inbox, { assignee: { avatar_attachment: [:blob] } }, { contact: { avatar_attachment: [:blob] } }
|
:taggings, :inbox, { assignee: { avatar_attachment: [:blob] } }, { contact: { avatar_attachment: [:blob] } }, :team
|
||||||
)
|
)
|
||||||
current_page ? @conversations.latest.page(current_page) : @conversations.latest
|
@conversations.latest.page(current_page)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@ -3,12 +3,17 @@
|
|||||||
<transition name="fade" mode="out-in">
|
<transition name="fade" mode="out-in">
|
||||||
<router-view></router-view>
|
<router-view></router-view>
|
||||||
</transition>
|
</transition>
|
||||||
|
<add-account-modal
|
||||||
|
:show="showAddAccountModal"
|
||||||
|
:has-accounts="hasAccounts"
|
||||||
|
/>
|
||||||
<woot-snackbar-box />
|
<woot-snackbar-box />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
|
import AddAccountModal from '../dashboard/components/layout/sidebarComponents/AddAccountModal';
|
||||||
import WootSnackbarBox from './components/SnackbarContainer';
|
import WootSnackbarBox from './components/SnackbarContainer';
|
||||||
import { accountIdFromPathname } from './helper/URLHelper';
|
import { accountIdFromPathname } from './helper/URLHelper';
|
||||||
|
|
||||||
@ -17,14 +22,36 @@ export default {
|
|||||||
|
|
||||||
components: {
|
components: {
|
||||||
WootSnackbarBox,
|
WootSnackbarBox,
|
||||||
|
AddAccountModal,
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
showAddAccountModal: false,
|
||||||
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters({
|
...mapGetters({
|
||||||
getAccount: 'accounts/getAccount',
|
getAccount: 'accounts/getAccount',
|
||||||
|
currentUser: 'getCurrentUser',
|
||||||
}),
|
}),
|
||||||
|
hasAccounts() {
|
||||||
|
return (
|
||||||
|
this.currentUser &&
|
||||||
|
this.currentUser.accounts &&
|
||||||
|
this.currentUser.accounts.length !== 0
|
||||||
|
);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
currentUser() {
|
||||||
|
if (!this.hasAccounts) {
|
||||||
|
this.showAddAccountModal = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.$store.dispatch('setUser');
|
this.$store.dispatch('setUser');
|
||||||
this.setLocale(window.chatwootConfig.selectedLocale);
|
this.setLocale(window.chatwootConfig.selectedLocale);
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
import ApiClient from './ApiClient';
|
import ApiClient from './ApiClient';
|
||||||
|
|
||||||
export const buildContactParams = (page, sortAttr, label, search) => {
|
export const buildContactParams = (page, sortAttr, label, search) => {
|
||||||
let params = `page=${page}&sort=${sortAttr}`;
|
let params = `include_contact_inboxes=false&page=${page}&sort=${sortAttr}`;
|
||||||
if (search) {
|
if (search) {
|
||||||
params = `${params}&q=${search}`;
|
params = `${params}&q=${search}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -28,9 +28,10 @@ class ConversationApi extends ApiClient {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleStatus({ conversationId, status }) {
|
toggleStatus({ conversationId, status, snoozedUntil = null }) {
|
||||||
return axios.post(`${this.url}/${conversationId}/toggle_status`, {
|
return axios.post(`${this.url}/${conversationId}/toggle_status`, {
|
||||||
status,
|
status,
|
||||||
|
snoozed_until: snoozedUntil,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2,6 +2,33 @@
|
|||||||
/* global axios */
|
/* global axios */
|
||||||
import ApiClient from '../ApiClient';
|
import ApiClient from '../ApiClient';
|
||||||
|
|
||||||
|
export const buildCreatePayload = ({
|
||||||
|
message,
|
||||||
|
isPrivate,
|
||||||
|
contentAttributes,
|
||||||
|
echoId,
|
||||||
|
file,
|
||||||
|
}) => {
|
||||||
|
let payload;
|
||||||
|
if (file) {
|
||||||
|
payload = new FormData();
|
||||||
|
payload.append('attachments[]', file, file.name);
|
||||||
|
if (message) {
|
||||||
|
payload.append('content', message);
|
||||||
|
}
|
||||||
|
payload.append('private', isPrivate);
|
||||||
|
payload.append('echo_id', echoId);
|
||||||
|
} else {
|
||||||
|
payload = {
|
||||||
|
content: message,
|
||||||
|
private: isPrivate,
|
||||||
|
echo_id: echoId,
|
||||||
|
content_attributes: contentAttributes,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return payload;
|
||||||
|
};
|
||||||
|
|
||||||
class MessageApi extends ApiClient {
|
class MessageApi extends ApiClient {
|
||||||
constructor() {
|
constructor() {
|
||||||
super('conversations', { accountScoped: true });
|
super('conversations', { accountScoped: true });
|
||||||
@ -15,18 +42,16 @@ class MessageApi extends ApiClient {
|
|||||||
echo_id: echoId,
|
echo_id: echoId,
|
||||||
file,
|
file,
|
||||||
}) {
|
}) {
|
||||||
const formData = new FormData();
|
|
||||||
if (file) formData.append('attachments[]', file, file.name);
|
|
||||||
if (message) formData.append('content', message);
|
|
||||||
if (contentAttributes)
|
|
||||||
formData.append('content_attributes', JSON.stringify(contentAttributes));
|
|
||||||
|
|
||||||
formData.append('private', isPrivate);
|
|
||||||
formData.append('echo_id', echoId);
|
|
||||||
return axios({
|
return axios({
|
||||||
method: 'post',
|
method: 'post',
|
||||||
url: `${this.url}/${conversationId}/messages`,
|
url: `${this.url}/${conversationId}/messages`,
|
||||||
data: formData,
|
data: buildCreatePayload({
|
||||||
|
message,
|
||||||
|
isPrivate,
|
||||||
|
contentAttributes,
|
||||||
|
echoId,
|
||||||
|
file,
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -17,7 +17,7 @@ describe('#ContactsAPI', () => {
|
|||||||
it('#get', () => {
|
it('#get', () => {
|
||||||
contactAPI.get(1, 'name', 'customer-support');
|
contactAPI.get(1, 'name', 'customer-support');
|
||||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||||
'/api/v1/contacts?page=1&sort=name&labels[]=customer-support'
|
'/api/v1/contacts?include_contact_inboxes=false&page=1&sort=name&labels[]=customer-support'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -56,7 +56,7 @@ describe('#ContactsAPI', () => {
|
|||||||
it('#search', () => {
|
it('#search', () => {
|
||||||
contactAPI.search('leads', 1, 'date', 'customer-support');
|
contactAPI.search('leads', 1, 'date', 'customer-support');
|
||||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||||
'/api/v1/contacts/search?page=1&sort=date&q=leads&labels[]=customer-support'
|
'/api/v1/contacts/search?include_contact_inboxes=false&page=1&sort=date&q=leads&labels[]=customer-support'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -64,12 +64,16 @@ describe('#ContactsAPI', () => {
|
|||||||
|
|
||||||
describe('#buildContactParams', () => {
|
describe('#buildContactParams', () => {
|
||||||
it('returns correct string', () => {
|
it('returns correct string', () => {
|
||||||
expect(buildContactParams(1, 'name', '', '')).toBe('page=1&sort=name');
|
expect(buildContactParams(1, 'name', '', '')).toBe(
|
||||||
|
'include_contact_inboxes=false&page=1&sort=name'
|
||||||
|
);
|
||||||
expect(buildContactParams(1, 'name', 'customer-support', '')).toBe(
|
expect(buildContactParams(1, 'name', 'customer-support', '')).toBe(
|
||||||
'page=1&sort=name&labels[]=customer-support'
|
'include_contact_inboxes=false&page=1&sort=name&labels[]=customer-support'
|
||||||
);
|
);
|
||||||
expect(
|
expect(
|
||||||
buildContactParams(1, 'name', 'customer-support', 'message-content')
|
buildContactParams(1, 'name', 'customer-support', 'message-content')
|
||||||
).toBe('page=1&sort=name&q=message-content&labels[]=customer-support');
|
).toBe(
|
||||||
|
'include_contact_inboxes=false&page=1&sort=name&q=message-content&labels[]=customer-support'
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -69,6 +69,7 @@ describe('#ConversationAPI', () => {
|
|||||||
`/api/v1/conversations/12/toggle_status`,
|
`/api/v1/conversations/12/toggle_status`,
|
||||||
{
|
{
|
||||||
status: 'online',
|
status: 'online',
|
||||||
|
snoozed_until: null,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import messageAPI from '../../inbox/message';
|
import messageAPI, { buildCreatePayload } from '../../inbox/message';
|
||||||
import ApiClient from '../../ApiClient';
|
import ApiClient from '../../ApiClient';
|
||||||
import describeWithAPIMock from '../apiSpecHelper';
|
import describeWithAPIMock from '../apiSpecHelper';
|
||||||
|
|
||||||
@ -29,4 +29,34 @@ describe('#ConversationAPI', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
describe('#buildCreatePayload', () => {
|
||||||
|
it('builds form payload if file is available', () => {
|
||||||
|
const formPayload = buildCreatePayload({
|
||||||
|
message: 'test content',
|
||||||
|
echoId: 12,
|
||||||
|
isPrivate: true,
|
||||||
|
file: new Blob(['test-content'], { type: 'application/pdf' }),
|
||||||
|
});
|
||||||
|
expect(formPayload).toBeInstanceOf(FormData);
|
||||||
|
expect(formPayload.get('content')).toEqual('test content');
|
||||||
|
expect(formPayload.get('echo_id')).toEqual('12');
|
||||||
|
expect(formPayload.get('private')).toEqual('true');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('builds object payload if file is not available', () => {
|
||||||
|
expect(
|
||||||
|
buildCreatePayload({
|
||||||
|
message: 'test content',
|
||||||
|
isPrivate: false,
|
||||||
|
echoId: 12,
|
||||||
|
contentAttributes: { in_reply_to: 12 },
|
||||||
|
})
|
||||||
|
).toEqual({
|
||||||
|
content: 'test content',
|
||||||
|
private: false,
|
||||||
|
echo_id: 12,
|
||||||
|
content_attributes: { in_reply_to: 12 },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -204,4 +204,8 @@
|
|||||||
.multiselect--disabled .multiselect__select {
|
.multiselect--disabled .multiselect__select {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.multiselect__tags-wrap {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,6 +8,22 @@
|
|||||||
font-weight: var(--font-weight-light);
|
font-weight: var(--font-weight-light);
|
||||||
margin-top: var(--space-large);
|
margin-top: var(--space-large);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.update-subscription--checkbox {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
input {
|
||||||
|
line-height: 1.5;
|
||||||
|
margin-right: var(--space-one);
|
||||||
|
margin-top: var(--space-smaller);
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
font-size: var(--font-size-small);
|
||||||
|
line-height: 1.5;
|
||||||
|
margin-bottom: var(--space-normal);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.alert-box {
|
.alert-box {
|
||||||
@ -20,17 +36,4 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.update-subscription--checkbox {
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
input {
|
|
||||||
line-height: 1.5;
|
|
||||||
margin-right: var(--space-one);
|
|
||||||
}
|
|
||||||
|
|
||||||
div {
|
|
||||||
font-size: var(--font-size-small);
|
|
||||||
line-height: 1.5;
|
|
||||||
margin-bottom: var(--space-normal);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -90,11 +90,6 @@
|
|||||||
font-size: $font-size-mini;
|
font-size: $font-size-mini;
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-from-agent {
|
|
||||||
color: $color-gray;
|
|
||||||
font-size: $font-size-mini;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.conversation--meta {
|
.conversation--meta {
|
||||||
|
|||||||
@ -19,7 +19,7 @@
|
|||||||
{{ $t('CHAT_LIST.LIST.404') }}
|
{{ $t('CHAT_LIST.LIST.404') }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="conversations-list">
|
<div ref="activeConversation" class="conversations-list">
|
||||||
<conversation-card
|
<conversation-card
|
||||||
v-for="chat in conversationList"
|
v-for="chat in conversationList"
|
||||||
:key="chat.id"
|
:key="chat.id"
|
||||||
@ -62,8 +62,13 @@ import ChatFilter from './widgets/conversation/ChatFilter';
|
|||||||
import ChatTypeTabs from './widgets/ChatTypeTabs';
|
import ChatTypeTabs from './widgets/ChatTypeTabs';
|
||||||
import ConversationCard from './widgets/conversation/ConversationCard';
|
import ConversationCard from './widgets/conversation/ConversationCard';
|
||||||
import timeMixin from '../mixins/time';
|
import timeMixin from '../mixins/time';
|
||||||
|
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
||||||
import conversationMixin from '../mixins/conversations';
|
import conversationMixin from '../mixins/conversations';
|
||||||
import wootConstants from '../constants';
|
import wootConstants from '../constants';
|
||||||
|
import {
|
||||||
|
hasPressedAltAndJKey,
|
||||||
|
hasPressedAltAndKKey,
|
||||||
|
} from 'shared/helpers/KeyboardHelpers';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@ -71,7 +76,7 @@ export default {
|
|||||||
ConversationCard,
|
ConversationCard,
|
||||||
ChatFilter,
|
ChatFilter,
|
||||||
},
|
},
|
||||||
mixins: [timeMixin, conversationMixin],
|
mixins: [timeMixin, conversationMixin, eventListenerMixins],
|
||||||
props: {
|
props: {
|
||||||
conversationInbox: {
|
conversationInbox: {
|
||||||
type: [String, Number],
|
type: [String, Number],
|
||||||
@ -94,6 +99,7 @@ export default {
|
|||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters({
|
...mapGetters({
|
||||||
|
currentChat: 'getSelectedChat',
|
||||||
chatLists: 'getAllConversations',
|
chatLists: 'getAllConversations',
|
||||||
mineChatsList: 'getMineChats',
|
mineChatsList: 'getMineChats',
|
||||||
allChatList: 'getAllStatusChats',
|
allChatList: 'getAllStatusChats',
|
||||||
@ -188,6 +194,33 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
handleKeyEvents(e) {
|
||||||
|
const allConversations = this.$refs.activeConversation.querySelectorAll(
|
||||||
|
'div.conversations-list div.conversation'
|
||||||
|
);
|
||||||
|
const activeConversation = this.$refs.activeConversation.querySelector(
|
||||||
|
'div.conversations-list div.conversation.active'
|
||||||
|
);
|
||||||
|
const activeConversationIndex = [...allConversations].indexOf(
|
||||||
|
activeConversation
|
||||||
|
);
|
||||||
|
const lastConversationIndex = allConversations.length - 1;
|
||||||
|
if (hasPressedAltAndJKey(e)) {
|
||||||
|
if (activeConversationIndex === -1) {
|
||||||
|
allConversations[0].click();
|
||||||
|
}
|
||||||
|
if (activeConversationIndex >= 1) {
|
||||||
|
allConversations[activeConversationIndex - 1].click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (hasPressedAltAndKKey(e)) {
|
||||||
|
if (activeConversationIndex === -1) {
|
||||||
|
allConversations[lastConversationIndex].click();
|
||||||
|
} else if (activeConversationIndex < lastConversationIndex) {
|
||||||
|
allConversations[activeConversationIndex + 1].click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
resetAndFetchData() {
|
resetAndFetchData() {
|
||||||
this.$store.dispatch('conversationPage/reset');
|
this.$store.dispatch('conversationPage/reset');
|
||||||
this.$store.dispatch('emptyAllConversations');
|
this.$store.dispatch('emptyAllConversations');
|
||||||
|
|||||||
@ -24,7 +24,7 @@
|
|||||||
{{ this.$t('CONVERSATION.HEADER.REOPEN_ACTION') }}
|
{{ this.$t('CONVERSATION.HEADER.REOPEN_ACTION') }}
|
||||||
</woot-button>
|
</woot-button>
|
||||||
<woot-button
|
<woot-button
|
||||||
v-else-if="isBot"
|
v-else-if="showOpenButton"
|
||||||
class-names="resolve"
|
class-names="resolve"
|
||||||
color-scheme="primary"
|
color-scheme="primary"
|
||||||
icon="ion-person"
|
icon="ion-person"
|
||||||
@ -34,7 +34,8 @@
|
|||||||
{{ this.$t('CONVERSATION.HEADER.OPEN_ACTION') }}
|
{{ this.$t('CONVERSATION.HEADER.OPEN_ACTION') }}
|
||||||
</woot-button>
|
</woot-button>
|
||||||
<woot-button
|
<woot-button
|
||||||
v-if="showDropDown"
|
v-if="showAdditionalActions"
|
||||||
|
ref="arrowDownButton"
|
||||||
:color-scheme="buttonClass"
|
:color-scheme="buttonClass"
|
||||||
:disabled="isLoading"
|
:disabled="isLoading"
|
||||||
icon="ion-arrow-down-b"
|
icon="ion-arrow-down-b"
|
||||||
@ -43,19 +44,53 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="showDropdown"
|
v-if="showActionsDropdown"
|
||||||
v-on-clickaway="closeDropdown"
|
v-on-clickaway="closeDropdown"
|
||||||
class="dropdown-pane dropdown-pane--open"
|
class="dropdown-pane dropdown-pane--open"
|
||||||
>
|
>
|
||||||
<woot-dropdown-menu>
|
<woot-dropdown-menu>
|
||||||
<woot-dropdown-item v-if="!isBot">
|
<woot-dropdown-item v-if="!isPending">
|
||||||
<woot-button
|
<woot-button
|
||||||
variant="clear"
|
variant="clear"
|
||||||
@click="() => toggleStatus(STATUS_TYPE.BOT)"
|
@click="() => toggleStatus(STATUS_TYPE.PENDING)"
|
||||||
>
|
>
|
||||||
{{ this.$t('CONVERSATION.RESOLVE_DROPDOWN.OPEN_BOT') }}
|
{{ this.$t('CONVERSATION.RESOLVE_DROPDOWN.MARK_PENDING') }}
|
||||||
</woot-button>
|
</woot-button>
|
||||||
</woot-dropdown-item>
|
</woot-dropdown-item>
|
||||||
|
|
||||||
|
<woot-dropdown-sub-menu
|
||||||
|
v-if="isOpen"
|
||||||
|
:title="this.$t('CONVERSATION.RESOLVE_DROPDOWN.SNOOZE.TITLE')"
|
||||||
|
>
|
||||||
|
<woot-dropdown-item>
|
||||||
|
<woot-button
|
||||||
|
variant="clear"
|
||||||
|
@click="() => toggleStatus(STATUS_TYPE.SNOOZED, null)"
|
||||||
|
>
|
||||||
|
{{ this.$t('CONVERSATION.RESOLVE_DROPDOWN.SNOOZE.NEXT_REPLY') }}
|
||||||
|
</woot-button>
|
||||||
|
</woot-dropdown-item>
|
||||||
|
<woot-dropdown-item>
|
||||||
|
<woot-button
|
||||||
|
variant="clear"
|
||||||
|
@click="
|
||||||
|
() => toggleStatus(STATUS_TYPE.SNOOZED, snoozeTimes.tomorrow)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{ this.$t('CONVERSATION.RESOLVE_DROPDOWN.SNOOZE.TOMORROW') }}
|
||||||
|
</woot-button>
|
||||||
|
</woot-dropdown-item>
|
||||||
|
<woot-dropdown-item>
|
||||||
|
<woot-button
|
||||||
|
variant="clear"
|
||||||
|
@click="
|
||||||
|
() => toggleStatus(STATUS_TYPE.SNOOZED, snoozeTimes.nextWeek)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{ this.$t('CONVERSATION.RESOLVE_DROPDOWN.SNOOZE.NEXT_WEEK') }}
|
||||||
|
</woot-button>
|
||||||
|
</woot-dropdown-item>
|
||||||
|
</woot-dropdown-sub-menu>
|
||||||
</woot-dropdown-menu>
|
</woot-dropdown-menu>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -65,22 +100,37 @@
|
|||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import { mixin as clickaway } from 'vue-clickaway';
|
import { mixin as clickaway } from 'vue-clickaway';
|
||||||
import alertMixin from 'shared/mixins/alertMixin';
|
import alertMixin from 'shared/mixins/alertMixin';
|
||||||
|
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
||||||
|
import {
|
||||||
|
hasPressedAltAndEKey,
|
||||||
|
hasPressedCommandPlusAltAndEKey,
|
||||||
|
hasPressedAltAndMKey,
|
||||||
|
} from 'shared/helpers/KeyboardHelpers';
|
||||||
|
|
||||||
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue';
|
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue';
|
||||||
|
import WootDropdownSubMenu from 'shared/components/ui/dropdown/DropdownSubMenu.vue';
|
||||||
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue';
|
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue';
|
||||||
import wootConstants from '../../constants';
|
import wootConstants from '../../constants';
|
||||||
|
import {
|
||||||
|
getUnixTime,
|
||||||
|
addHours,
|
||||||
|
addWeeks,
|
||||||
|
startOfTomorrow,
|
||||||
|
startOfWeek,
|
||||||
|
} from 'date-fns';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
WootDropdownItem,
|
WootDropdownItem,
|
||||||
WootDropdownMenu,
|
WootDropdownMenu,
|
||||||
|
WootDropdownSubMenu,
|
||||||
},
|
},
|
||||||
mixins: [clickaway, alertMixin],
|
mixins: [clickaway, alertMixin, eventListenerMixins],
|
||||||
props: { conversationId: { type: [String, Number], required: true } },
|
props: { conversationId: { type: [String, Number], required: true } },
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
showDropdown: false,
|
showActionsDropdown: false,
|
||||||
STATUS_TYPE: wootConstants.STATUS_TYPE,
|
STATUS_TYPE: wootConstants.STATUS_TYPE,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@ -91,36 +141,83 @@ export default {
|
|||||||
isOpen() {
|
isOpen() {
|
||||||
return this.currentChat.status === wootConstants.STATUS_TYPE.OPEN;
|
return this.currentChat.status === wootConstants.STATUS_TYPE.OPEN;
|
||||||
},
|
},
|
||||||
isBot() {
|
isPending() {
|
||||||
return this.currentChat.status === wootConstants.STATUS_TYPE.BOT;
|
return this.currentChat.status === wootConstants.STATUS_TYPE.PENDING;
|
||||||
},
|
},
|
||||||
isResolved() {
|
isResolved() {
|
||||||
return this.currentChat.status === wootConstants.STATUS_TYPE.RESOLVED;
|
return this.currentChat.status === wootConstants.STATUS_TYPE.RESOLVED;
|
||||||
},
|
},
|
||||||
|
isSnoozed() {
|
||||||
|
return this.currentChat.status === wootConstants.STATUS_TYPE.SNOOZED;
|
||||||
|
},
|
||||||
buttonClass() {
|
buttonClass() {
|
||||||
if (this.isBot) return 'primary';
|
if (this.isPending) return 'primary';
|
||||||
if (this.isOpen) return 'success';
|
if (this.isOpen) return 'success';
|
||||||
if (this.isResolved) return 'warning';
|
if (this.isResolved) return 'warning';
|
||||||
return '';
|
return '';
|
||||||
},
|
},
|
||||||
showDropDown() {
|
showAdditionalActions() {
|
||||||
return !this.isBot;
|
return !this.isPending && !this.isSnoozed;
|
||||||
|
},
|
||||||
|
snoozeTimes() {
|
||||||
|
return {
|
||||||
|
// tomorrow = 9AM next day
|
||||||
|
tomorrow: getUnixTime(addHours(startOfTomorrow(), 9)),
|
||||||
|
// next week = 9AM Monday, next week
|
||||||
|
nextWeek: getUnixTime(
|
||||||
|
addHours(startOfWeek(addWeeks(new Date(), 1), { weekStartsOn: 1 }), 9)
|
||||||
|
),
|
||||||
|
};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
async handleKeyEvents(e) {
|
||||||
|
const allConversations = document.querySelectorAll(
|
||||||
|
'.conversations-list .conversation'
|
||||||
|
);
|
||||||
|
if (hasPressedAltAndMKey(e)) {
|
||||||
|
this.$refs.arrowDownButton.$el.click();
|
||||||
|
}
|
||||||
|
if (hasPressedAltAndEKey(e)) {
|
||||||
|
const activeConversation = document.querySelector(
|
||||||
|
'div.conversations-list div.conversation.active'
|
||||||
|
);
|
||||||
|
const activeConversationIndex = [...allConversations].indexOf(
|
||||||
|
activeConversation
|
||||||
|
);
|
||||||
|
const lastConversationIndex = allConversations.length - 1;
|
||||||
|
try {
|
||||||
|
await this.toggleStatus(wootConstants.STATUS_TYPE.RESOLVED);
|
||||||
|
} catch (error) {
|
||||||
|
// error
|
||||||
|
}
|
||||||
|
if (hasPressedCommandPlusAltAndEKey(e)) {
|
||||||
|
if (activeConversationIndex < lastConversationIndex) {
|
||||||
|
allConversations[activeConversationIndex + 1].click();
|
||||||
|
} else if (allConversations.length > 1) {
|
||||||
|
allConversations[0].click();
|
||||||
|
document.querySelector('.conversations-list').scrollTop = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
showOpenButton() {
|
||||||
|
return this.isResolved || this.isSnoozed;
|
||||||
|
},
|
||||||
closeDropdown() {
|
closeDropdown() {
|
||||||
this.showDropdown = false;
|
this.showActionsDropdown = false;
|
||||||
},
|
},
|
||||||
openDropdown() {
|
openDropdown() {
|
||||||
this.showDropdown = true;
|
this.showActionsDropdown = true;
|
||||||
},
|
},
|
||||||
toggleStatus(status) {
|
toggleStatus(status, snoozedUntil) {
|
||||||
this.closeDropdown();
|
this.closeDropdown();
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
this.$store
|
this.$store
|
||||||
.dispatch('toggleStatus', {
|
.dispatch('toggleStatus', {
|
||||||
conversationId: this.currentChat.id,
|
conversationId: this.currentChat.id,
|
||||||
status,
|
status,
|
||||||
|
snoozedUntil,
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.showAlert(this.$t('CONVERSATION.CHANGE_STATUS'));
|
this.showAlert(this.$t('CONVERSATION.CHANGE_STATUS'));
|
||||||
|
|||||||
@ -50,10 +50,17 @@
|
|||||||
:show="showOptionsMenu"
|
:show="showOptionsMenu"
|
||||||
@toggle-accounts="toggleAccountModal"
|
@toggle-accounts="toggleAccountModal"
|
||||||
@show-support-chat-window="toggleSupportChatWindow"
|
@show-support-chat-window="toggleSupportChatWindow"
|
||||||
|
@key-shortcut-modal="toggleKeyShortcutModal"
|
||||||
@close="toggleOptions"
|
@close="toggleOptions"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<woot-key-shortcut-modal
|
||||||
|
v-if="showShortcutModal"
|
||||||
|
@close="closeKeyShortcutModal"
|
||||||
|
@clickaway="closeKeyShortcutModal"
|
||||||
|
/>
|
||||||
|
|
||||||
<account-selector
|
<account-selector
|
||||||
:show-account-modal="showAccountModal"
|
:show-account-modal="showAccountModal"
|
||||||
@close-account-modal="toggleAccountModal"
|
@close-account-modal="toggleAccountModal"
|
||||||
@ -86,6 +93,9 @@ import OptionsMenu from './sidebarComponents/OptionsMenu.vue';
|
|||||||
import AccountSelector from './sidebarComponents/AccountSelector.vue';
|
import AccountSelector from './sidebarComponents/AccountSelector.vue';
|
||||||
import AddAccountModal from './sidebarComponents/AddAccountModal.vue';
|
import AddAccountModal from './sidebarComponents/AddAccountModal.vue';
|
||||||
import AddLabelModal from '../../routes/dashboard/settings/labels/AddLabel';
|
import AddLabelModal from '../../routes/dashboard/settings/labels/AddLabel';
|
||||||
|
import WootKeyShortcutModal from 'components/widgets/modal/WootKeyShortcutModal';
|
||||||
|
import { hasPressedCommandAndForwardSlash } from 'shared/helpers/KeyboardHelpers';
|
||||||
|
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@ -97,14 +107,16 @@ export default {
|
|||||||
AccountSelector,
|
AccountSelector,
|
||||||
AddAccountModal,
|
AddAccountModal,
|
||||||
AddLabelModal,
|
AddLabelModal,
|
||||||
|
WootKeyShortcutModal,
|
||||||
},
|
},
|
||||||
mixins: [adminMixin, alertMixin],
|
mixins: [adminMixin, alertMixin, eventListenerMixins],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
showOptionsMenu: false,
|
showOptionsMenu: false,
|
||||||
showAccountModal: false,
|
showAccountModal: false,
|
||||||
showCreateAccountModal: false,
|
showCreateAccountModal: false,
|
||||||
showAddLabelModal: false,
|
showAddLabelModal: false,
|
||||||
|
showShortcutModal: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -254,7 +266,19 @@ export default {
|
|||||||
this.$store.dispatch('teams/get');
|
this.$store.dispatch('teams/get');
|
||||||
this.setChatwootUser();
|
this.setChatwootUser();
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
toggleKeyShortcutModal() {
|
||||||
|
this.showShortcutModal = true;
|
||||||
|
},
|
||||||
|
closeKeyShortcutModal() {
|
||||||
|
this.showShortcutModal = false;
|
||||||
|
},
|
||||||
|
handleKeyEvents(e) {
|
||||||
|
if (hasPressedCommandAndForwardSlash(e)) {
|
||||||
|
this.toggleKeyShortcutModal();
|
||||||
|
}
|
||||||
|
},
|
||||||
toggleSupportChatWindow() {
|
toggleSupportChatWindow() {
|
||||||
window.$chatwoot.toggle();
|
window.$chatwoot.toggle();
|
||||||
},
|
},
|
||||||
|
|||||||
@ -59,10 +59,17 @@
|
|||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
|
|
||||||
import router from '../../routes';
|
import router from '../../routes';
|
||||||
|
import {
|
||||||
|
hasPressedAltAndCKey,
|
||||||
|
hasPressedAltAndVKey,
|
||||||
|
hasPressedAltAndRKey,
|
||||||
|
hasPressedAltAndSKey,
|
||||||
|
} from 'shared/helpers/KeyboardHelpers';
|
||||||
import adminMixin from '../../mixins/isAdmin';
|
import adminMixin from '../../mixins/isAdmin';
|
||||||
|
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
||||||
import { getInboxClassByType } from 'dashboard/helper/inbox';
|
import { getInboxClassByType } from 'dashboard/helper/inbox';
|
||||||
export default {
|
export default {
|
||||||
mixins: [adminMixin],
|
mixins: [adminMixin, eventListenerMixins],
|
||||||
props: {
|
props: {
|
||||||
menuItem: {
|
menuItem: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@ -117,6 +124,20 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
handleKeyEvents(e) {
|
||||||
|
if (hasPressedAltAndCKey(e)) {
|
||||||
|
router.push({ name: 'home' });
|
||||||
|
}
|
||||||
|
if (hasPressedAltAndVKey(e)) {
|
||||||
|
router.push({ name: 'contacts_dashboard' });
|
||||||
|
}
|
||||||
|
if (hasPressedAltAndRKey(e)) {
|
||||||
|
router.push({ name: 'settings_account_reports' });
|
||||||
|
}
|
||||||
|
if (hasPressedAltAndSKey(e)) {
|
||||||
|
router.push({ name: 'settings_home' });
|
||||||
|
}
|
||||||
|
},
|
||||||
showItem(item) {
|
showItem(item) {
|
||||||
return this.isAdmin && item.newLink !== undefined;
|
return this.isAdmin && item.newLink !== undefined;
|
||||||
},
|
},
|
||||||
|
|||||||
@ -9,6 +9,14 @@
|
|||||||
:header-title="$t('CREATE_ACCOUNT.NEW_ACCOUNT')"
|
:header-title="$t('CREATE_ACCOUNT.NEW_ACCOUNT')"
|
||||||
:header-content="$t('CREATE_ACCOUNT.SELECTOR_SUBTITLE')"
|
:header-content="$t('CREATE_ACCOUNT.SELECTOR_SUBTITLE')"
|
||||||
/>
|
/>
|
||||||
|
<div v-if="!hasAccounts" class="alert-wrap">
|
||||||
|
<div class="callout alert">
|
||||||
|
<div class="icon-wrap">
|
||||||
|
<i class="ion-alert-circled"></i>
|
||||||
|
</div>
|
||||||
|
{{ $t('CREATE_ACCOUNT.NO_ACCOUNT_WARNING') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<form class="row" @submit.prevent="addAccount">
|
<form class="row" @submit.prevent="addAccount">
|
||||||
<div class="medium-12 columns">
|
<div class="medium-12 columns">
|
||||||
@ -53,6 +61,10 @@ export default {
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
hasAccounts: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@ -90,3 +102,19 @@ export default {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
<style lang="scss">
|
||||||
|
.alert-wrap {
|
||||||
|
margin: var(--space-zero) var(--space-large);
|
||||||
|
margin-top: var(--space-medium);
|
||||||
|
.callout {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-wrap {
|
||||||
|
font-size: var(--font-size-big);
|
||||||
|
margin-left: var(--space-smaller);
|
||||||
|
margin-right: var(--space-slab);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@ -26,6 +26,16 @@
|
|||||||
Contact Support
|
Contact Support
|
||||||
</woot-button>
|
</woot-button>
|
||||||
</woot-dropdown-item>
|
</woot-dropdown-item>
|
||||||
|
<woot-dropdown-item>
|
||||||
|
<woot-button
|
||||||
|
variant="clear"
|
||||||
|
size="small"
|
||||||
|
class=" change-accounts--button"
|
||||||
|
@click="$emit('key-shortcut-modal')"
|
||||||
|
>
|
||||||
|
{{ $t('SIDEBAR_ITEMS.KEYBOARD_SHORTCUTS') }}
|
||||||
|
</woot-button>
|
||||||
|
</woot-dropdown-item>
|
||||||
<woot-dropdown-item>
|
<woot-dropdown-item>
|
||||||
<router-link
|
<router-link
|
||||||
:to="`/app/accounts/${accountId}/profile/settings`"
|
:to="`/app/accounts/${accountId}/profile/settings`"
|
||||||
|
|||||||
@ -10,8 +10,11 @@
|
|||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
import wootConstants from '../../constants';
|
import wootConstants from '../../constants';
|
||||||
|
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
||||||
|
import { hasPressedAltAndNKey } from 'shared/helpers/KeyboardHelpers';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
mixins: [eventListenerMixins],
|
||||||
props: {
|
props: {
|
||||||
items: {
|
items: {
|
||||||
type: Array,
|
type: Array,
|
||||||
@ -28,6 +31,15 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
handleKeyEvents(e) {
|
||||||
|
if (hasPressedAltAndNKey(e)) {
|
||||||
|
if (this.activeTab === wootConstants.ASSIGNEE_TYPE.ALL) {
|
||||||
|
this.onTabChange(0);
|
||||||
|
} else {
|
||||||
|
this.onTabChange(this.activeTabIndex + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
onTabChange(selectedTabIndex) {
|
onTabChange(selectedTabIndex) {
|
||||||
if (this.items[selectedTabIndex].key !== this.activeTab) {
|
if (this.items[selectedTabIndex].key !== this.activeTab) {
|
||||||
this.$emit('chatTabChange', this.items[selectedTabIndex].key);
|
this.$emit('chatTabChange', this.items[selectedTabIndex].key);
|
||||||
|
|||||||
@ -0,0 +1,38 @@
|
|||||||
|
<template>
|
||||||
|
<div class="inbox">
|
||||||
|
<i :class="icon" />
|
||||||
|
<span>{{ inbox.name }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import { INBOX_TYPES } from 'shared/mixins/inboxMixin';
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
inbox: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
icon() {
|
||||||
|
if (this.inbox.channel_type === INBOX_TYPES.WEB) {
|
||||||
|
return 'icon ion-earth';
|
||||||
|
}
|
||||||
|
return 'icon ion-android-textsms';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.inbox {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
.icon {
|
||||||
|
margin-right: var(--space-micro);
|
||||||
|
min-width: var(--space-normal);
|
||||||
|
position: relative;
|
||||||
|
top: -1px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -57,6 +57,7 @@ export default {
|
|||||||
value: { type: String, default: '' },
|
value: { type: String, default: '' },
|
||||||
placeholder: { type: String, default: '' },
|
placeholder: { type: String, default: '' },
|
||||||
isPrivate: { type: Boolean, default: false },
|
isPrivate: { type: Boolean, default: false },
|
||||||
|
isFormatMode: { type: Boolean, default: false },
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@ -139,8 +140,17 @@ export default {
|
|||||||
value(newValue = '') {
|
value(newValue = '') {
|
||||||
if (newValue !== this.lastValue) {
|
if (newValue !== this.lastValue) {
|
||||||
const { tr } = this.state;
|
const { tr } = this.state;
|
||||||
|
if (this.isFormatMode) {
|
||||||
|
this.state = createState(
|
||||||
|
newValue,
|
||||||
|
this.placeholder,
|
||||||
|
this.plugins,
|
||||||
|
this.isFormatMode
|
||||||
|
);
|
||||||
|
} else {
|
||||||
tr.insertText(newValue, 0, tr.doc.content.size);
|
tr.insertText(newValue, 0, tr.doc.content.size);
|
||||||
this.state = this.view.state.apply(tr);
|
this.state = this.view.state.apply(tr);
|
||||||
|
}
|
||||||
this.view.updateState(this.state);
|
this.view.updateState(this.state);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -271,4 +281,25 @@ export default {
|
|||||||
padding: 0 var(--space-smaller);
|
padding: 0 var(--space-smaller);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.editor-wrap {
|
||||||
|
margin-bottom: var(--space-normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-editor {
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--border-radius-normal);
|
||||||
|
padding: 0 var(--space-slab);
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor_warning {
|
||||||
|
border: 1px solid var(--r-400);
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-warning__message {
|
||||||
|
color: var(--r-400);
|
||||||
|
font-weight: var(--font-weight-normal);
|
||||||
|
padding: var(--space-smaller) 0 0 0;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -78,11 +78,17 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import FileUpload from 'vue-upload-component';
|
import FileUpload from 'vue-upload-component';
|
||||||
|
import {
|
||||||
|
hasPressedAltAndWKey,
|
||||||
|
hasPressedAltAndAKey,
|
||||||
|
} from 'shared/helpers/KeyboardHelpers';
|
||||||
|
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
||||||
|
|
||||||
import { REPLY_EDITOR_MODES } from './constants';
|
import { REPLY_EDITOR_MODES } from './constants';
|
||||||
export default {
|
export default {
|
||||||
name: 'ReplyTopPanel',
|
name: 'ReplyTopPanel',
|
||||||
components: { FileUpload },
|
components: { FileUpload },
|
||||||
|
mixins: [eventListenerMixins],
|
||||||
props: {
|
props: {
|
||||||
mode: {
|
mode: {
|
||||||
type: String,
|
type: String,
|
||||||
@ -156,6 +162,14 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
handleKeyEvents(e) {
|
||||||
|
if (hasPressedAltAndWKey(e)) {
|
||||||
|
this.toggleFormatMode();
|
||||||
|
}
|
||||||
|
if (hasPressedAltAndAKey(e)) {
|
||||||
|
this.$refs.upload.$children[1].$el.click();
|
||||||
|
}
|
||||||
|
},
|
||||||
toggleFormatMode() {
|
toggleFormatMode() {
|
||||||
this.setFormatMode(!this.isFormatMode);
|
this.setFormatMode(!this.isFormatMode);
|
||||||
},
|
},
|
||||||
|
|||||||
@ -32,11 +32,17 @@
|
|||||||
<script>
|
<script>
|
||||||
import { REPLY_EDITOR_MODES, CHAR_LENGTH_WARNING } from './constants';
|
import { REPLY_EDITOR_MODES, CHAR_LENGTH_WARNING } from './constants';
|
||||||
import EmojiOrIcon from 'shared/components/EmojiOrIcon';
|
import EmojiOrIcon from 'shared/components/EmojiOrIcon';
|
||||||
|
import {
|
||||||
|
hasPressedAltAndPKey,
|
||||||
|
hasPressedAltAndLKey,
|
||||||
|
} from 'shared/helpers/KeyboardHelpers';
|
||||||
|
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
||||||
export default {
|
export default {
|
||||||
name: 'ReplyTopPanel',
|
name: 'ReplyTopPanel',
|
||||||
components: {
|
components: {
|
||||||
EmojiOrIcon,
|
EmojiOrIcon,
|
||||||
},
|
},
|
||||||
|
mixins: [eventListenerMixins],
|
||||||
props: {
|
props: {
|
||||||
mode: {
|
mode: {
|
||||||
type: String,
|
type: String,
|
||||||
@ -76,6 +82,14 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
handleKeyEvents(e) {
|
||||||
|
if (hasPressedAltAndPKey(e)) {
|
||||||
|
this.handleNoteClick();
|
||||||
|
}
|
||||||
|
if (hasPressedAltAndLKey(e)) {
|
||||||
|
this.handleReplyClick();
|
||||||
|
}
|
||||||
|
},
|
||||||
handleReplyClick() {
|
handleReplyClick() {
|
||||||
this.setReplyMode(REPLY_EDITOR_MODES.REPLY);
|
this.setReplyMode(REPLY_EDITOR_MODES.REPLY);
|
||||||
},
|
},
|
||||||
|
|||||||
@ -12,12 +12,29 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import wootConstants from '../../../constants';
|
import wootConstants from '../../../constants';
|
||||||
|
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
||||||
|
import { hasPressedAltAndBKey } from 'shared/helpers/KeyboardHelpers';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
mixins: [eventListenerMixins],
|
||||||
data: () => ({
|
data: () => ({
|
||||||
activeStatus: wootConstants.STATUS_TYPE.OPEN,
|
activeStatus: wootConstants.STATUS_TYPE.OPEN,
|
||||||
}),
|
}),
|
||||||
methods: {
|
methods: {
|
||||||
|
handleKeyEvents(e) {
|
||||||
|
if (hasPressedAltAndBKey(e)) {
|
||||||
|
if (this.activeStatus === wootConstants.STATUS_TYPE.OPEN) {
|
||||||
|
this.activeStatus = wootConstants.STATUS_TYPE.RESOLVED;
|
||||||
|
} else if (this.activeStatus === wootConstants.STATUS_TYPE.RESOLVED) {
|
||||||
|
this.activeStatus = wootConstants.STATUS_TYPE.PENDING;
|
||||||
|
} else if (this.activeStatus === wootConstants.STATUS_TYPE.PENDING) {
|
||||||
|
this.activeStatus = wootConstants.STATUS_TYPE.SNOOZED;
|
||||||
|
} else if (this.activeStatus === wootConstants.STATUS_TYPE.SNOOZED) {
|
||||||
|
this.activeStatus = wootConstants.STATUS_TYPE.OPEN;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.onTabChange();
|
||||||
|
},
|
||||||
onTabChange() {
|
onTabChange() {
|
||||||
this.$store.dispatch('setChatFilter', this.activeStatus);
|
this.$store.dispatch('setChatFilter', this.activeStatus);
|
||||||
this.$emit('statusFilterChange', this.activeStatus);
|
this.$emit('statusFilterChange', this.activeStatus);
|
||||||
|
|||||||
@ -49,13 +49,11 @@ export default {
|
|||||||
},
|
},
|
||||||
isContactPanelOpen: {
|
isContactPanelOpen: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters({
|
...mapGetters({ currentChat: 'getSelectedChat' }),
|
||||||
currentChat: 'getSelectedChat',
|
|
||||||
}),
|
|
||||||
showContactPanel() {
|
showContactPanel() {
|
||||||
return this.isContactPanelOpen && this.currentChat.id;
|
return this.isContactPanelOpen && this.currentChat.id;
|
||||||
},
|
},
|
||||||
|
|||||||
@ -26,7 +26,12 @@
|
|||||||
{{ currentContact.name }}
|
{{ currentContact.name }}
|
||||||
</h4>
|
</h4>
|
||||||
<p v-if="lastMessageInChat" class="conversation--message">
|
<p v-if="lastMessageInChat" class="conversation--message">
|
||||||
<i v-if="messageByAgent" class="ion-ios-undo message-from-agent"></i>
|
<i v-if="isMessagePrivate" class="ion-locked last-message-icon" />
|
||||||
|
<i v-else-if="messageByAgent" class="ion-ios-undo last-message-icon" />
|
||||||
|
<i
|
||||||
|
v-else-if="isMessageAnActivity"
|
||||||
|
class="ion-information-circled last-message-icon"
|
||||||
|
/>
|
||||||
<span v-if="lastMessageInChat.content">
|
<span v-if="lastMessageInChat.content">
|
||||||
{{ parsedLastMessage }}
|
{{ parsedLastMessage }}
|
||||||
</span>
|
</span>
|
||||||
@ -144,6 +149,18 @@ export default {
|
|||||||
return messageType === MESSAGE_TYPE.OUTGOING;
|
return messageType === MESSAGE_TYPE.OUTGOING;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
isMessageAnActivity() {
|
||||||
|
const lastMessage = this.lastMessageInChat;
|
||||||
|
const { message_type: messageType } = lastMessage;
|
||||||
|
return messageType === MESSAGE_TYPE.ACTIVITY;
|
||||||
|
},
|
||||||
|
|
||||||
|
isMessagePrivate() {
|
||||||
|
const lastMessage = this.lastMessageInChat;
|
||||||
|
const { private: isPrivate } = lastMessage;
|
||||||
|
return isPrivate;
|
||||||
|
},
|
||||||
|
|
||||||
parsedLastMessage() {
|
parsedLastMessage() {
|
||||||
const { content_attributes: contentAttributes } = this.lastMessageInChat;
|
const { content_attributes: contentAttributes } = this.lastMessageInChat;
|
||||||
const { email: { subject } = {} } = contentAttributes || {};
|
const { email: { subject } = {} } = contentAttributes || {};
|
||||||
@ -230,4 +247,9 @@ export default {
|
|||||||
font-size: var(--font-size-mini);
|
font-size: var(--font-size-mini);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.last-message-icon {
|
||||||
|
color: var(--s-600);
|
||||||
|
font-size: var(--font-size-mini);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -32,33 +32,6 @@
|
|||||||
class="header-actions-wrap"
|
class="header-actions-wrap"
|
||||||
:class="{ 'has-open-sidebar': isContactPanelOpen }"
|
:class="{ 'has-open-sidebar': isContactPanelOpen }"
|
||||||
>
|
>
|
||||||
<div class="multiselect-box multiselect-wrap--small">
|
|
||||||
<i class="icon ion-headphone" />
|
|
||||||
<multiselect
|
|
||||||
v-model="currentChat.meta.assignee"
|
|
||||||
:loading="uiFlags.isFetching"
|
|
||||||
:allow-empty="true"
|
|
||||||
deselect-label=""
|
|
||||||
:options="agentsList"
|
|
||||||
:placeholder="$t('CONVERSATION.ASSIGNMENT.SELECT_AGENT')"
|
|
||||||
select-label=""
|
|
||||||
label="name"
|
|
||||||
selected-label
|
|
||||||
track-by="id"
|
|
||||||
@select="assignAgent"
|
|
||||||
@remove="removeAgent"
|
|
||||||
>
|
|
||||||
<template slot="option" slot-scope="props">
|
|
||||||
<div class="option__desc">
|
|
||||||
<availability-status-badge
|
|
||||||
:status="props.option.availability_status"
|
|
||||||
/>
|
|
||||||
<span class="option__title">{{ props.option.name }}</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<span slot="noResult">{{ $t('AGENT_MGMT.SEARCH.NO_RESULTS') }}</span>
|
|
||||||
</multiselect>
|
|
||||||
</div>
|
|
||||||
<more-actions :conversation-id="currentChat.id" />
|
<more-actions :conversation-id="currentChat.id" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -68,15 +41,15 @@ import { mapGetters } from 'vuex';
|
|||||||
import MoreActions from './MoreActions';
|
import MoreActions from './MoreActions';
|
||||||
import Thumbnail from '../Thumbnail';
|
import Thumbnail from '../Thumbnail';
|
||||||
import agentMixin from '../../../mixins/agentMixin.js';
|
import agentMixin from '../../../mixins/agentMixin.js';
|
||||||
import AvailabilityStatusBadge from '../conversation/AvailabilityStatusBadge';
|
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
||||||
|
import { hasPressedAltAndOKey } from 'shared/helpers/KeyboardHelpers';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
MoreActions,
|
MoreActions,
|
||||||
Thumbnail,
|
Thumbnail,
|
||||||
AvailabilityStatusBadge,
|
|
||||||
},
|
},
|
||||||
mixins: [agentMixin],
|
mixins: [agentMixin, eventListenerMixins],
|
||||||
props: {
|
props: {
|
||||||
chat: {
|
chat: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@ -117,17 +90,11 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
assignAgent(agent) {
|
handleKeyEvents(e) {
|
||||||
this.$store
|
if (hasPressedAltAndOKey(e)) {
|
||||||
.dispatch('assignAgent', {
|
this.$emit('contact-panel-toggle');
|
||||||
conversationId: this.currentChat.id,
|
}
|
||||||
agentId: agent.id,
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
bus.$emit('newToastMessage', this.$t('CONVERSATION.CHANGE_AGENT'));
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
removeAgent() {},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -2,6 +2,11 @@
|
|||||||
<li v-if="hasAttachments || data.content" :class="alignBubble">
|
<li v-if="hasAttachments || data.content" :class="alignBubble">
|
||||||
<div :class="wrapClass">
|
<div :class="wrapClass">
|
||||||
<div v-tooltip.top-start="sentByMessage" :class="bubbleClass">
|
<div v-tooltip.top-start="sentByMessage" :class="bubbleClass">
|
||||||
|
<bubble-mail-head
|
||||||
|
v-if="isEmailContentType"
|
||||||
|
:email-attributes="contentAttributes.email"
|
||||||
|
:is-incoming="isIncoming"
|
||||||
|
/>
|
||||||
<bubble-text
|
<bubble-text
|
||||||
v-if="data.content"
|
v-if="data.content"
|
||||||
:message="message"
|
:message="message"
|
||||||
@ -41,6 +46,7 @@
|
|||||||
:message-type="data.message_type"
|
:message-type="data.message_type"
|
||||||
:readable-time="readableTime"
|
:readable-time="readableTime"
|
||||||
:source-id="data.source_id"
|
:source-id="data.source_id"
|
||||||
|
:inbox-id="data.inbox_id"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<spinner v-if="isPending" size="tiny" />
|
<spinner v-if="isPending" size="tiny" />
|
||||||
@ -79,16 +85,19 @@ import copy from 'copy-text-to-clipboard';
|
|||||||
|
|
||||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||||
import timeMixin from '../../../mixins/time';
|
import timeMixin from '../../../mixins/time';
|
||||||
|
|
||||||
|
import BubbleMailHead from './bubble/MailHead';
|
||||||
import BubbleText from './bubble/Text';
|
import BubbleText from './bubble/Text';
|
||||||
import BubbleImage from './bubble/Image';
|
import BubbleImage from './bubble/Image';
|
||||||
import BubbleFile from './bubble/File';
|
import BubbleFile from './bubble/File';
|
||||||
|
import BubbleActions from './bubble/Actions';
|
||||||
|
|
||||||
import Spinner from 'shared/components/Spinner';
|
import Spinner from 'shared/components/Spinner';
|
||||||
import ContextMenu from 'dashboard/modules/conversations/components/MessageContextMenu';
|
import ContextMenu from 'dashboard/modules/conversations/components/MessageContextMenu';
|
||||||
|
|
||||||
import { isEmptyObject } from 'dashboard/helper/commons';
|
import { isEmptyObject } from 'dashboard/helper/commons';
|
||||||
import alertMixin from 'shared/mixins/alertMixin';
|
import alertMixin from 'shared/mixins/alertMixin';
|
||||||
import contentTypeMixin from 'shared/mixins/contentTypeMixin';
|
import contentTypeMixin from 'shared/mixins/contentTypeMixin';
|
||||||
import BubbleActions from './bubble/Actions';
|
|
||||||
import { MESSAGE_TYPE, MESSAGE_STATUS } from 'shared/constants/messages';
|
import { MESSAGE_TYPE, MESSAGE_STATUS } from 'shared/constants/messages';
|
||||||
import { generateBotMessageContent } from './helpers/botMessageContentHelper';
|
import { generateBotMessageContent } from './helpers/botMessageContentHelper';
|
||||||
|
|
||||||
@ -98,6 +107,7 @@ export default {
|
|||||||
BubbleText,
|
BubbleText,
|
||||||
BubbleImage,
|
BubbleImage,
|
||||||
BubbleFile,
|
BubbleFile,
|
||||||
|
BubbleMailHead,
|
||||||
ContextMenu,
|
ContextMenu,
|
||||||
Spinner,
|
Spinner,
|
||||||
},
|
},
|
||||||
@ -133,11 +143,11 @@ export default {
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
email: {
|
email: {
|
||||||
|
content_type: contentType = '',
|
||||||
html_content: { full: fullHTMLContent, reply: replyHTMLContent } = {},
|
html_content: { full: fullHTMLContent, reply: replyHTMLContent } = {},
|
||||||
text_content: { full: fullTextContent, reply: replyTextContent } = {},
|
text_content: { full: fullTextContent, reply: replyTextContent } = {},
|
||||||
} = {},
|
} = {},
|
||||||
} = this.contentAttributes;
|
} = this.contentAttributes;
|
||||||
|
|
||||||
let contentToBeParsed =
|
let contentToBeParsed =
|
||||||
replyHTMLContent ||
|
replyHTMLContent ||
|
||||||
replyTextContent ||
|
replyTextContent ||
|
||||||
@ -147,7 +157,12 @@ export default {
|
|||||||
if (contentToBeParsed && this.isIncoming) {
|
if (contentToBeParsed && this.isIncoming) {
|
||||||
const parsedContent = this.stripStyleCharacters(contentToBeParsed);
|
const parsedContent = this.stripStyleCharacters(contentToBeParsed);
|
||||||
if (parsedContent) {
|
if (parsedContent) {
|
||||||
return parsedContent;
|
// This is a temporary fix for line-breaks in text/plain emails
|
||||||
|
// Now, It is not rendered properly in the email preview.
|
||||||
|
// FIXME: Remove this once we have a better solution for rendering text/plain emails
|
||||||
|
return contentType.includes('text/plain')
|
||||||
|
? parsedContent.replace(/\n/g, '<br />')
|
||||||
|
: parsedContent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -33,11 +33,11 @@
|
|||||||
|
|
||||||
<div v-if="isATweet" class="banner">
|
<div v-if="isATweet" class="banner">
|
||||||
<span v-if="!selectedTweetId">
|
<span v-if="!selectedTweetId">
|
||||||
{{ $t('CONVERSATION.LAST_INCOMING_TWEET') }}
|
{{ $t('CONVERSATION.SELECT_A_TWEET_TO_REPLY') }}
|
||||||
</span>
|
</span>
|
||||||
<span v-else>
|
<span v-else>
|
||||||
{{ $t('CONVERSATION.REPLYING_TO') }}
|
{{ $t('CONVERSATION.REPLYING_TO') }}
|
||||||
{{ selectedTweet }}
|
{{ selectedTweet.content || '' }}
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
v-if="selectedTweetId"
|
v-if="selectedTweetId"
|
||||||
@ -89,9 +89,10 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ReplyBox
|
<reply-box
|
||||||
:conversation-id="currentChat.id"
|
:conversation-id="currentChat.id"
|
||||||
:in-reply-to="selectedTweetId"
|
:is-a-tweet="isATweet"
|
||||||
|
:selected-tweet="selectedTweet"
|
||||||
@scrollToMessage="scrollToBottom"
|
@scrollToMessage="scrollToBottom"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -207,10 +208,10 @@ export default {
|
|||||||
selectedTweet() {
|
selectedTweet() {
|
||||||
if (this.selectedTweetId) {
|
if (this.selectedTweetId) {
|
||||||
const { messages = [] } = this.getMessages;
|
const { messages = [] } = this.getMessages;
|
||||||
const [selectedMessage = {}] = messages.filter(
|
const [selectedMessage] = messages.filter(
|
||||||
message => message.id === this.selectedTweetId
|
message => message.id === this.selectedTweetId
|
||||||
);
|
);
|
||||||
return selectedMessage.content || '';
|
return selectedMessage || {};
|
||||||
}
|
}
|
||||||
return '';
|
return '';
|
||||||
},
|
},
|
||||||
@ -233,7 +234,7 @@ export default {
|
|||||||
|
|
||||||
created() {
|
created() {
|
||||||
bus.$on('scrollToMessage', () => {
|
bus.$on('scrollToMessage', () => {
|
||||||
setTimeout(() => this.scrollToBottom(), 0);
|
this.$nextTick(() => this.scrollToBottom());
|
||||||
this.makeMessagesRead();
|
this.makeMessagesRead();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -255,7 +256,7 @@ export default {
|
|||||||
this.conversationPanel = this.$el.querySelector('.conversation-panel');
|
this.conversationPanel = this.$el.querySelector('.conversation-panel');
|
||||||
this.setScrollParams();
|
this.setScrollParams();
|
||||||
this.conversationPanel.addEventListener('scroll', this.handleScroll);
|
this.conversationPanel.addEventListener('scroll', this.handleScroll);
|
||||||
this.scrollToBottom();
|
this.$nextTick(() => this.scrollToBottom());
|
||||||
this.isLoadingPrevious = false;
|
this.isLoadingPrevious = false;
|
||||||
},
|
},
|
||||||
removeScrollListener() {
|
removeScrollListener() {
|
||||||
|
|||||||
@ -74,6 +74,7 @@
|
|||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import { mixin as clickaway } from 'vue-clickaway';
|
import { mixin as clickaway } from 'vue-clickaway';
|
||||||
import alertMixin from 'shared/mixins/alertMixin';
|
import alertMixin from 'shared/mixins/alertMixin';
|
||||||
|
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
||||||
|
|
||||||
import EmojiInput from 'shared/components/emoji/EmojiInput';
|
import EmojiInput from 'shared/components/emoji/EmojiInput';
|
||||||
import CannedResponse from './CannedResponse';
|
import CannedResponse from './CannedResponse';
|
||||||
@ -105,11 +106,21 @@ export default {
|
|||||||
ReplyBottomPanel,
|
ReplyBottomPanel,
|
||||||
WootMessageEditor,
|
WootMessageEditor,
|
||||||
},
|
},
|
||||||
mixins: [clickaway, inboxMixin, uiSettingsMixin, alertMixin],
|
mixins: [
|
||||||
|
clickaway,
|
||||||
|
inboxMixin,
|
||||||
|
uiSettingsMixin,
|
||||||
|
alertMixin,
|
||||||
|
eventListenerMixins,
|
||||||
|
],
|
||||||
props: {
|
props: {
|
||||||
inReplyTo: {
|
selectedTweet: {
|
||||||
type: [String, Number],
|
type: [Object, String],
|
||||||
default: '',
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
isATweet: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
@ -169,11 +180,14 @@ export default {
|
|||||||
return this.maxLength - this.message.length;
|
return this.maxLength - this.message.length;
|
||||||
},
|
},
|
||||||
isReplyButtonDisabled() {
|
isReplyButtonDisabled() {
|
||||||
const isMessageEmpty = !this.message.trim().replace(/\n/g, '').length;
|
if (this.isATweet && !this.inReplyTo) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.hasAttachments) return false;
|
if (this.hasAttachments) return false;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
isMessageEmpty ||
|
this.isMessageEmpty ||
|
||||||
this.message.length === 0 ||
|
this.message.length === 0 ||
|
||||||
this.message.length > this.maxLength
|
this.message.length > this.maxLength
|
||||||
);
|
);
|
||||||
@ -198,7 +212,7 @@ export default {
|
|||||||
}
|
}
|
||||||
if (this.isATwitterInbox) {
|
if (this.isATwitterInbox) {
|
||||||
if (this.conversationType === 'tweet') {
|
if (this.conversationType === 'tweet') {
|
||||||
return MESSAGE_MAX_LENGTH.TWEET;
|
return MESSAGE_MAX_LENGTH.TWEET - this.replyToUserLength - 2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return MESSAGE_MAX_LENGTH.GENERAL;
|
return MESSAGE_MAX_LENGTH.GENERAL;
|
||||||
@ -235,6 +249,25 @@ export default {
|
|||||||
isOnPrivateNote() {
|
isOnPrivateNote() {
|
||||||
return this.replyType === REPLY_EDITOR_MODES.NOTE;
|
return this.replyType === REPLY_EDITOR_MODES.NOTE;
|
||||||
},
|
},
|
||||||
|
inReplyTo() {
|
||||||
|
const selectedTweet = this.selectedTweet || {};
|
||||||
|
return selectedTweet.id;
|
||||||
|
},
|
||||||
|
replyToUserLength() {
|
||||||
|
const selectedTweet = this.selectedTweet || {};
|
||||||
|
const {
|
||||||
|
sender: {
|
||||||
|
additional_attributes: { screen_name: screenName = '' } = {},
|
||||||
|
} = {},
|
||||||
|
} = selectedTweet;
|
||||||
|
return screenName ? screenName.length : 0;
|
||||||
|
},
|
||||||
|
isMessageEmpty() {
|
||||||
|
if (!this.message) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return !this.message.trim().replace(/\n/g, '').length;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
currentChat(conversation) {
|
currentChat(conversation) {
|
||||||
@ -263,12 +296,6 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
|
||||||
document.addEventListener('keydown', this.handleKeyEvents);
|
|
||||||
},
|
|
||||||
destroyed() {
|
|
||||||
document.removeEventListener('keydown', this.handleKeyEvents);
|
|
||||||
},
|
|
||||||
methods: {
|
methods: {
|
||||||
toggleUserMention(currentMentionState) {
|
toggleUserMention(currentMentionState) {
|
||||||
this.hasUserMention = currentMentionState;
|
this.hasUserMention = currentMentionState;
|
||||||
@ -327,6 +354,9 @@ export default {
|
|||||||
if (this.showRichContentEditor) {
|
if (this.showRichContentEditor) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (this.$refs.messageInput === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.$nextTick(() => this.$refs.messageInput.focus());
|
this.$nextTick(() => this.$refs.messageInput.focus());
|
||||||
},
|
},
|
||||||
emojiOnClick(emoji) {
|
emojiOnClick(emoji) {
|
||||||
|
|||||||
@ -0,0 +1,119 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="input-group-wrap">
|
||||||
|
<div class="input-group small" :class="{ error: $v.ccEmails.$error }">
|
||||||
|
<label class="input-group-label">
|
||||||
|
{{ $t('CONVERSATION.REPLYBOX.EMAIL_HEAD.CC.LABEL') }}
|
||||||
|
</label>
|
||||||
|
<div class="input-group-field">
|
||||||
|
<woot-input
|
||||||
|
v-model.trim="ccEmails"
|
||||||
|
type="email"
|
||||||
|
:class="{ error: $v.ccEmails.$error }"
|
||||||
|
:placeholder="$t('CONVERSATION.REPLYBOX.EMAIL_HEAD.CC.PLACEHOLDER')"
|
||||||
|
@blur="$v.ccEmails.$touch"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<woot-button
|
||||||
|
v-if="!showBcc"
|
||||||
|
variant="clear"
|
||||||
|
size="small"
|
||||||
|
@click="handleAddBcc"
|
||||||
|
>
|
||||||
|
{{ $t('CONVERSATION.REPLYBOX.EMAIL_HEAD.ADD_BCC') }}
|
||||||
|
</woot-button>
|
||||||
|
</div>
|
||||||
|
<span v-if="$v.ccEmails.$error" class="message">
|
||||||
|
{{ $t('CONVERSATION.REPLYBOX.EMAIL_HEAD.CC.ERROR') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="showBcc" class="input-group-wrap">
|
||||||
|
<div class="input-group small" :class="{ error: $v.bccEmails.$error }">
|
||||||
|
<label class="input-group-label">
|
||||||
|
{{ $t('CONVERSATION.REPLYBOX.EMAIL_HEAD.BCC.LABEL') }}
|
||||||
|
</label>
|
||||||
|
<div class="input-group-field">
|
||||||
|
<woot-input
|
||||||
|
v-model.trim="bccEmails"
|
||||||
|
type="email"
|
||||||
|
:class="{ error: $v.bccEmails.$error }"
|
||||||
|
:placeholder="
|
||||||
|
$t('CONVERSATION.REPLYBOX.EMAIL_HEAD.BCC.PLACEHOLDER')
|
||||||
|
"
|
||||||
|
@blur="$v.bccEmails.$touch"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span v-if="$v.bccEmails.$error" class="message">
|
||||||
|
{{ $t('CONVERSATION.REPLYBOX.EMAIL_HEAD.BCC.ERROR') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { validEmailsByComma } from './helpers/emailHeadHelper';
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
ccEmails: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
bccEmails: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
showBcc: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
validations: {
|
||||||
|
ccEmails: {
|
||||||
|
hasValidEmails(value) {
|
||||||
|
return validEmailsByComma(value);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
bccEmails: {
|
||||||
|
hasValidEmails(value) {
|
||||||
|
return validEmailsByComma(value);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleAddBcc() {
|
||||||
|
this.showBcc = true;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.input-group-wrap .message {
|
||||||
|
font-size: var(--font-size-small);
|
||||||
|
color: var(--r-500);
|
||||||
|
}
|
||||||
|
.input-group {
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
margin-bottom: var(--space-smaller);
|
||||||
|
margin-top: var(--space-smaller);
|
||||||
|
|
||||||
|
.input-group-label {
|
||||||
|
border-color: transparent;
|
||||||
|
background: transparent;
|
||||||
|
font-size: var(--font-size-mini);
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
}
|
||||||
|
.input-group-field::v-deep input {
|
||||||
|
margin-bottom: 0;
|
||||||
|
border-color: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group.error {
|
||||||
|
border-bottom-color: var(--r-500);
|
||||||
|
.input-group-label {
|
||||||
|
color: var(--r-500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -14,13 +14,13 @@
|
|||||||
@mouseleave="isHovered = false"
|
@mouseleave="isHovered = false"
|
||||||
/>
|
/>
|
||||||
<i
|
<i
|
||||||
v-if="isATweet && isIncoming"
|
v-if="isATweet && (isIncoming || isOutgoing) && sourceId"
|
||||||
v-tooltip.top-start="$t('CHAT_LIST.REPLY_TO_TWEET')"
|
v-tooltip.top-start="$t('CHAT_LIST.REPLY_TO_TWEET')"
|
||||||
class="icon ion-reply cursor-pointer"
|
class="icon ion-reply cursor-pointer"
|
||||||
@click="onTweetReply"
|
@click="onTweetReply"
|
||||||
/>
|
/>
|
||||||
<a
|
<a
|
||||||
v-if="isATweet && isIncoming"
|
v-if="isATweet && (isOutgoing || isIncoming) && linkToTweet"
|
||||||
:href="linkToTweet"
|
:href="linkToTweet"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer nofollow"
|
rel="noopener noreferrer nofollow"
|
||||||
@ -71,19 +71,33 @@ export default {
|
|||||||
type: [String, Number],
|
type: [String, Number],
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
|
inboxId: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
inbox() {
|
||||||
|
return this.$store.getters['inboxes/getInbox'](this.inboxId);
|
||||||
|
},
|
||||||
isIncoming() {
|
isIncoming() {
|
||||||
return MESSAGE_TYPE.INCOMING === this.messageType;
|
return MESSAGE_TYPE.INCOMING === this.messageType;
|
||||||
},
|
},
|
||||||
|
isOutgoing() {
|
||||||
|
return MESSAGE_TYPE.OUTGOING === this.messageType;
|
||||||
|
},
|
||||||
screenName() {
|
screenName() {
|
||||||
const { additional_attributes: additionalAttributes = {} } =
|
const { additional_attributes: additionalAttributes = {} } =
|
||||||
this.sender || {};
|
this.sender || {};
|
||||||
return additionalAttributes?.screen_name || '';
|
return additionalAttributes?.screen_name || '';
|
||||||
},
|
},
|
||||||
linkToTweet() {
|
linkToTweet() {
|
||||||
|
if (!this.sourceId || !this.inbox.name) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
const { screenName, sourceId } = this;
|
const { screenName, sourceId } = this;
|
||||||
return `https://twitter.com/${screenName}/status/${sourceId}`;
|
return `https://twitter.com/${screenName ||
|
||||||
|
this.inbox.name}/status/${sourceId}`;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@ -113,6 +127,13 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.right {
|
||||||
|
.ion-reply,
|
||||||
|
.ion-android-open {
|
||||||
|
color: var(--white);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.message-text--metadata {
|
.message-text--metadata {
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@ -36,7 +36,7 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
openLink() {
|
openLink() {
|
||||||
const win = window.open(this.url, '_blank', 'noopener');
|
const win = window.open(this.url, '_blank', 'noopener');
|
||||||
win.focus();
|
if (win) win.focus();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -0,0 +1,82 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-if="showHead"
|
||||||
|
class="message__mail-head"
|
||||||
|
:class="{ 'is-incoming': isIncoming }"
|
||||||
|
>
|
||||||
|
<div v-if="toMails" class="meta-wrap">
|
||||||
|
<span class="message__content--type">{{ $t('EMAIL_HEADER.TO') }}:</span>
|
||||||
|
<span>{{ toMails }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="ccMails" class="meta-wrap">
|
||||||
|
<span class="message__content--type">{{ $t('EMAIL_HEADER.CC') }}:</span>
|
||||||
|
<span>{{ ccMails }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="bccMails" class="meta-wrap">
|
||||||
|
<span class="message__content--type">{{ $t('EMAIL_HEADER.BCC') }}:</span>
|
||||||
|
<span>{{ bccMails }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="subject" class="meta-wrap">
|
||||||
|
<span class="message__content--type">
|
||||||
|
{{ $t('EMAIL_HEADER.SUBJECT') }}:
|
||||||
|
</span>
|
||||||
|
<span>{{ subject }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
emailAttributes: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
isIncoming: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
toMails() {
|
||||||
|
const to = this.emailAttributes.to || [];
|
||||||
|
return to.join(', ');
|
||||||
|
},
|
||||||
|
ccMails() {
|
||||||
|
const cc = this.emailAttributes.cc || [];
|
||||||
|
return cc.join(', ');
|
||||||
|
},
|
||||||
|
bccMails() {
|
||||||
|
const bcc = this.emailAttributes.bcc || [];
|
||||||
|
return bcc.join(', ');
|
||||||
|
},
|
||||||
|
subject() {
|
||||||
|
return this.emailAttributes.subject || '';
|
||||||
|
},
|
||||||
|
showHead() {
|
||||||
|
return this.toMails || this.ccMails || this.bccMails;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.message__mail-head {
|
||||||
|
padding-bottom: var(--space-small);
|
||||||
|
margin-bottom: var(--space-small);
|
||||||
|
border-bottom: 1px solid var(--w-300);
|
||||||
|
|
||||||
|
&.is-incoming {
|
||||||
|
border-bottom: 1px solid var(--color-border-light);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-wrap {
|
||||||
|
.message__content--type {
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
font-size: var(--font-size-mini);
|
||||||
|
}
|
||||||
|
span {
|
||||||
|
font-size: var(--font-size-mini);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
import emailValidator from 'vuelidate/lib/validators/email';
|
||||||
|
|
||||||
|
export const validEmailsByComma = value => {
|
||||||
|
if (!value.length) return true;
|
||||||
|
const emails = value.split(',');
|
||||||
|
return emails.every(email => emailValidator(email));
|
||||||
|
};
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
import { validEmailsByComma } from '../emailHeadHelper';
|
||||||
|
|
||||||
|
describe('#validEmailsByComma', () => {
|
||||||
|
it('returns true when empty string is passed', () => {
|
||||||
|
expect(validEmailsByComma('')).toEqual(true);
|
||||||
|
});
|
||||||
|
it('returns true when valid emails separated by comma is passed', () => {
|
||||||
|
expect(validEmailsByComma('ni@njan.com,po@va.da')).toEqual(true);
|
||||||
|
});
|
||||||
|
it('returns false when one of the email passed is invalid', () => {
|
||||||
|
expect(validEmailsByComma('ni@njan.com,pova.da')).toEqual(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,27 @@
|
|||||||
|
import ReplyEmailHead from '../ReplyEmailHead';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'Components/ReplyBox/EmailHead',
|
||||||
|
component: ReplyEmailHead,
|
||||||
|
argTypes: {
|
||||||
|
ccEmails: {
|
||||||
|
control: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
bccEmails: {
|
||||||
|
control: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const Template = (args, { argTypes }) => ({
|
||||||
|
props: Object.keys(argTypes),
|
||||||
|
components: { ReplyEmailHead },
|
||||||
|
template:
|
||||||
|
'<reply-email-head v-bind="$props" @add="onAdd" @click="onClick"></reply-email-head>',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const Add = Template.bind({});
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
import WootKeyboardShortcutModal from './WootKeyShortcutModal.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'Components/Shortcuts/Keyboard Shortcut',
|
||||||
|
component: WootKeyboardShortcutModal,
|
||||||
|
argTypes: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
const Template = (args, { argTypes }) => ({
|
||||||
|
props: Object.keys(argTypes),
|
||||||
|
components: { WootKeyboardShortcutModal },
|
||||||
|
template:
|
||||||
|
'<woot-keyboard-shortcut-modal v-bind="$props"></woot-keyboard-shortcut-modal>',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const KeyboardShortcut = Template.bind({});
|
||||||
|
KeyboardShortcut.args = {};
|
||||||
@ -0,0 +1,182 @@
|
|||||||
|
<template>
|
||||||
|
<transition name="slide-up">
|
||||||
|
<div class="modal-mask">
|
||||||
|
<div v-on-clickaway="() => $emit('clickaway')" class="modal-container">
|
||||||
|
<div class="header-wrap">
|
||||||
|
<div class="title-shortcut-key__wrap">
|
||||||
|
<h2 class="page-title">
|
||||||
|
{{ $t('SIDEBAR_ITEMS.KEYBOARD_SHORTCUTS') }}
|
||||||
|
</h2>
|
||||||
|
<div class="shortcut-key__wrap">
|
||||||
|
<p class="shortcut-key">
|
||||||
|
{{ $t('KEYBOARD_SHORTCUTS.KEYS.COMMAND_KEY') }}
|
||||||
|
</p>
|
||||||
|
<p class="shortcut-key key">
|
||||||
|
{{ $t('KEYBOARD_SHORTCUTS.KEYS.FORWARD_SLASH_KEY') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<i class="ion-android-close modal--close" @click="$emit('close')"></i>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="shortcut__wrap">
|
||||||
|
<div class="title-key__wrap">
|
||||||
|
<span class="sub-block-title">
|
||||||
|
{{ $t('KEYBOARD_SHORTCUTS.TITLE.OPEN_CONVERSATION') }}
|
||||||
|
</span>
|
||||||
|
<div class="shortcut-key__wrap">
|
||||||
|
<div class="open-conversation__key">
|
||||||
|
<span class="shortcut-key">
|
||||||
|
{{ $t('KEYBOARD_SHORTCUTS.KEYS.ALT_OR_OPTION_KEY') }}
|
||||||
|
</span>
|
||||||
|
<span class="shortcut-key">
|
||||||
|
J
|
||||||
|
</span>
|
||||||
|
<span class="forward-slash sub-block-title">
|
||||||
|
{{ $t('KEYBOARD_SHORTCUTS.KEYS.FORWARD_SLASH_KEY') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span class="shortcut-key">
|
||||||
|
{{ $t('KEYBOARD_SHORTCUTS.KEYS.ALT_OR_OPTION_KEY') }}
|
||||||
|
</span>
|
||||||
|
<span class="shortcut-key key">
|
||||||
|
K
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="title-key__wrap">
|
||||||
|
<span class="sub-block-title">
|
||||||
|
{{ $t('KEYBOARD_SHORTCUTS.TITLE.RESOLVE_AND_NEXT') }}
|
||||||
|
</span>
|
||||||
|
<div class="shortcut-key__wrap">
|
||||||
|
<span class="shortcut-key">
|
||||||
|
{{ $t('KEYBOARD_SHORTCUTS.KEYS.COMMAND_KEY') }}
|
||||||
|
</span>
|
||||||
|
<span class="shortcut-key">
|
||||||
|
{{ $t('KEYBOARD_SHORTCUTS.KEYS.ALT_OR_OPTION_KEY') }}
|
||||||
|
</span>
|
||||||
|
<span class="shortcut-key key">
|
||||||
|
E
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-for="shortcutKey in shortcutKeys"
|
||||||
|
:key="shortcutKey.id"
|
||||||
|
class="title-key__wrap"
|
||||||
|
>
|
||||||
|
<span class="sub-block-title">
|
||||||
|
{{ title(shortcutKey) }}
|
||||||
|
</span>
|
||||||
|
<div class="shortcut-key__wrap">
|
||||||
|
<span class="shortcut-key">
|
||||||
|
{{ shortcutKey.firstkey }}
|
||||||
|
</span>
|
||||||
|
<span class="shortcut-key key">
|
||||||
|
{{ shortcutKey.secondKey }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mixin as clickaway } from 'vue-clickaway';
|
||||||
|
import { SHORTCUT_KEYS } from './constants';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
mixins: [clickaway],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
shortcutKeys: SHORTCUT_KEYS,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
title(item) {
|
||||||
|
return this.$t(`KEYBOARD_SHORTCUTS.TITLE.${item.label}`);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.modal-container {
|
||||||
|
padding: var(--space-medium) var(--space-large) var(--space-large)
|
||||||
|
var(--space-large);
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-wrap {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-shortcut-key__wrap {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: var(--space-small);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
font-size: var(--font-size-big);
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
}
|
||||||
|
|
||||||
|
.shortcut-key__wrap {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: var(--space-smaller);
|
||||||
|
margin-left: var(--space-small);
|
||||||
|
}
|
||||||
|
|
||||||
|
.shortcut__wrap {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 0.5fr);
|
||||||
|
gap: var(--space-smaller) var(--space-large);
|
||||||
|
margin-top: var(--space-small);
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-key__wrap {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
min-width: 40rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sub-block-title {
|
||||||
|
font-size: var(--font-size-small);
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
}
|
||||||
|
|
||||||
|
.forward-slash {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
}
|
||||||
|
|
||||||
|
.shortcut-key {
|
||||||
|
background: var(--color-background);
|
||||||
|
padding: var(--space-small) var(--space-one);
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
font-size: var(--font-size-mini);
|
||||||
|
align-items: center;
|
||||||
|
border-radius: var(--border-radius-normal);
|
||||||
|
margin-right: var(--space-small);
|
||||||
|
}
|
||||||
|
|
||||||
|
.key {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: var(--space-large);
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.open-conversation__key {
|
||||||
|
display: flex;
|
||||||
|
margin-right: var(--space-small);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,86 @@
|
|||||||
|
export const SHORTCUT_KEYS = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
label: 'NAVIGATE_DROPDOWN',
|
||||||
|
firstkey: 'Up',
|
||||||
|
secondKey: 'Down',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
label: 'RESOLVE_CONVERSATION',
|
||||||
|
firstkey: 'Alt / ⌥',
|
||||||
|
secondKey: 'E',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
label: 'GO_TO_CONVERSATION_DASHBOARD',
|
||||||
|
firstkey: 'Alt / ⌥',
|
||||||
|
secondKey: 'C',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
label: 'ADD_ATTACHMENT',
|
||||||
|
firstkey: 'Alt / ⌥',
|
||||||
|
secondKey: 'A',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
label: 'GO_TO_CONTACTS_DASHBOARD',
|
||||||
|
firstkey: 'Alt / ⌥',
|
||||||
|
secondKey: 'V',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 6,
|
||||||
|
label: 'TOGGLE_SIDEBAR',
|
||||||
|
firstkey: 'Alt / ⌥',
|
||||||
|
secondKey: 'O',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 7,
|
||||||
|
label: 'GO_TO_REPORTS_SIDEBAR',
|
||||||
|
firstkey: 'Alt / ⌥',
|
||||||
|
secondKey: 'R',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 8,
|
||||||
|
label: 'MOVE_TO_NEXT_TAB',
|
||||||
|
firstkey: 'Alt / ⌥',
|
||||||
|
secondKey: 'N',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 9,
|
||||||
|
label: 'GO_TO_SETTINGS',
|
||||||
|
firstkey: 'Alt / ⌥',
|
||||||
|
secondKey: 'S',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 10,
|
||||||
|
label: 'SWITCH_CONVERSATION_STATUS',
|
||||||
|
firstkey: 'Alt / ⌥',
|
||||||
|
secondKey: 'B',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 11,
|
||||||
|
label: 'SWITCH_TO_PRIVATE_NOTE',
|
||||||
|
firstkey: 'Alt / ⌥',
|
||||||
|
secondKey: 'P',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 12,
|
||||||
|
label: 'TOGGLE_RICH_CONTENT_EDITOR',
|
||||||
|
firstkey: 'Alt / ⌥',
|
||||||
|
secondKey: 'W',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 13,
|
||||||
|
label: 'SWITCH_TO_REPLY',
|
||||||
|
firstkey: 'Alt / ⌥',
|
||||||
|
secondKey: 'L',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 14,
|
||||||
|
label: 'TOGGLE_SNOOZE_DROPDOWN',
|
||||||
|
firstkey: 'Alt / ⌥',
|
||||||
|
secondKey: 'M',
|
||||||
|
},
|
||||||
|
];
|
||||||
@ -8,7 +8,8 @@ export default {
|
|||||||
STATUS_TYPE: {
|
STATUS_TYPE: {
|
||||||
OPEN: 'open',
|
OPEN: 'open',
|
||||||
RESOLVED: 'resolved',
|
RESOLVED: 'resolved',
|
||||||
BOT: 'bot',
|
PENDING: 'pending',
|
||||||
|
SNOOZED: 'snoozed',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
export const DEFAULT_REDIRECT_URL = '/app/';
|
export const DEFAULT_REDIRECT_URL = '/app/';
|
||||||
|
|||||||
@ -47,6 +47,13 @@ export const getSidebarItems = accountId => ({
|
|||||||
toState: frontendURL(`accounts/${accountId}/reports`),
|
toState: frontendURL(`accounts/${accountId}/reports`),
|
||||||
toStateName: 'settings_account_reports',
|
toStateName: 'settings_account_reports',
|
||||||
},
|
},
|
||||||
|
campaigns: {
|
||||||
|
icon: 'ion-speakerphone',
|
||||||
|
label: 'CAMPAIGNS',
|
||||||
|
hasSubMenu: false,
|
||||||
|
toState: frontendURL(`accounts/${accountId}/campaigns`),
|
||||||
|
toStateName: 'settings_account_campaigns',
|
||||||
|
},
|
||||||
settings: {
|
settings: {
|
||||||
icon: 'ion-settings',
|
icon: 'ion-settings',
|
||||||
label: 'SETTINGS',
|
label: 'SETTINGS',
|
||||||
@ -105,6 +112,32 @@ export const getSidebarItems = accountId => ({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
campaigns: {
|
||||||
|
routes: ['settings_account_campaigns', 'one_off'],
|
||||||
|
menuItems: {
|
||||||
|
back: {
|
||||||
|
icon: 'ion-ios-arrow-back',
|
||||||
|
label: 'HOME',
|
||||||
|
hasSubMenu: false,
|
||||||
|
toStateName: 'home',
|
||||||
|
toState: frontendURL(`accounts/${accountId}/dashboard`),
|
||||||
|
},
|
||||||
|
ongoingCampaigns: {
|
||||||
|
icon: 'ion-arrow-swap',
|
||||||
|
label: 'ONGOING',
|
||||||
|
hasSubMenu: false,
|
||||||
|
toState: frontendURL(`accounts/${accountId}/campaigns/ongoing`),
|
||||||
|
toStateName: 'settings_account_campaigns',
|
||||||
|
},
|
||||||
|
onOffCampaigns: {
|
||||||
|
icon: 'ion-radio-waves',
|
||||||
|
label: 'ONE_OFF',
|
||||||
|
hasSubMenu: false,
|
||||||
|
toState: frontendURL(`accounts/${accountId}/campaigns/one_off`),
|
||||||
|
toStateName: 'one_off',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
settings: {
|
settings: {
|
||||||
routes: [
|
routes: [
|
||||||
'agent_list',
|
'agent_list',
|
||||||
|
|||||||
@ -273,7 +273,7 @@
|
|||||||
"INBOX_UPDATE_SUB_TEXT": "تحديث إعدادات قناة التواصل",
|
"INBOX_UPDATE_SUB_TEXT": "تحديث إعدادات قناة التواصل",
|
||||||
"AUTO_ASSIGNMENT_SUB_TEXT": "تمكين أو تعطيل الإسناد التلقائي للمحادثات الجديدة إلى الموظفين المضافين إلى قناة التواصل هذه.",
|
"AUTO_ASSIGNMENT_SUB_TEXT": "تمكين أو تعطيل الإسناد التلقائي للمحادثات الجديدة إلى الموظفين المضافين إلى قناة التواصل هذه.",
|
||||||
"HMAC_VERIFICATION": "التحقق من هوية المستخدم",
|
"HMAC_VERIFICATION": "التحقق من هوية المستخدم",
|
||||||
"HMAC_DESCRIPTION": "للتحقق من هوية المستخدمين، يسمح لك SDK بتمرير 'identity_hash' لكل مستخدم. يمكنك إنشاء HMAC باستخدام 'sha256' مع المفتاح المعروض هنا."
|
"HMAC_DESCRIPTION": "للتحقق من هوية المستخدمين، يسمح لك SDK بتمرير 'identifier_hash' لكل مستخدم. يمكنك إنشاء HMAC باستخدام 'sha256' مع المفتاح المعروض هنا."
|
||||||
},
|
},
|
||||||
"FACEBOOK_REAUTHORIZE": {
|
"FACEBOOK_REAUTHORIZE": {
|
||||||
"TITLE": "إعادة التصريح",
|
"TITLE": "إعادة التصريح",
|
||||||
|
|||||||
@ -273,7 +273,7 @@
|
|||||||
"INBOX_UPDATE_SUB_TEXT": "Actualitza la configuració de la safata d'entrada",
|
"INBOX_UPDATE_SUB_TEXT": "Actualitza la configuració de la safata d'entrada",
|
||||||
"AUTO_ASSIGNMENT_SUB_TEXT": "Activa o desactiva l'assignació automàtica d'agents disponibles a les noves converses",
|
"AUTO_ASSIGNMENT_SUB_TEXT": "Activa o desactiva l'assignació automàtica d'agents disponibles a les noves converses",
|
||||||
"HMAC_VERIFICATION": "Validació de la Identitat del Usuari",
|
"HMAC_VERIFICATION": "Validació de la Identitat del Usuari",
|
||||||
"HMAC_DESCRIPTION": "Inorder validate the users identity, the SDK allows you to pass an `identity_hash` for each user. You can generate HMAC using 'sha256' with the key shown here."
|
"HMAC_DESCRIPTION": "Inorder validate the users identity, the SDK allows you to pass an `identifier_hash` for each user. You can generate HMAC using 'sha256' with the key shown here."
|
||||||
},
|
},
|
||||||
"FACEBOOK_REAUTHORIZE": {
|
"FACEBOOK_REAUTHORIZE": {
|
||||||
"TITLE": "Reautoritza",
|
"TITLE": "Reautoritza",
|
||||||
|
|||||||
@ -273,7 +273,7 @@
|
|||||||
"INBOX_UPDATE_SUB_TEXT": "Aktualizujte nastavení doručené pošty",
|
"INBOX_UPDATE_SUB_TEXT": "Aktualizujte nastavení doručené pošty",
|
||||||
"AUTO_ASSIGNMENT_SUB_TEXT": "Povolit nebo zakázat automatické přiřazování nových konverzací agentům přidaným do této schránky.",
|
"AUTO_ASSIGNMENT_SUB_TEXT": "Povolit nebo zakázat automatické přiřazování nových konverzací agentům přidaným do této schránky.",
|
||||||
"HMAC_VERIFICATION": "User Identity Validation",
|
"HMAC_VERIFICATION": "User Identity Validation",
|
||||||
"HMAC_DESCRIPTION": "Inorder validate the users identity, the SDK allows you to pass an `identity_hash` for each user. You can generate HMAC using 'sha256' with the key shown here."
|
"HMAC_DESCRIPTION": "Inorder validate the users identity, the SDK allows you to pass an `identifier_hash` for each user. You can generate HMAC using 'sha256' with the key shown here."
|
||||||
},
|
},
|
||||||
"FACEBOOK_REAUTHORIZE": {
|
"FACEBOOK_REAUTHORIZE": {
|
||||||
"TITLE": "Znovu autorizovat",
|
"TITLE": "Znovu autorizovat",
|
||||||
|
|||||||
@ -273,7 +273,7 @@
|
|||||||
"INBOX_UPDATE_SUB_TEXT": "Opdater dine indbakkeindstillinger",
|
"INBOX_UPDATE_SUB_TEXT": "Opdater dine indbakkeindstillinger",
|
||||||
"AUTO_ASSIGNMENT_SUB_TEXT": "Aktiver eller deaktiver automatisk tildeling af nye samtaler til agenter tilføjet til denne indbakke.",
|
"AUTO_ASSIGNMENT_SUB_TEXT": "Aktiver eller deaktiver automatisk tildeling af nye samtaler til agenter tilføjet til denne indbakke.",
|
||||||
"HMAC_VERIFICATION": "User Identity Validation",
|
"HMAC_VERIFICATION": "User Identity Validation",
|
||||||
"HMAC_DESCRIPTION": "Inorder validate the users identity, the SDK allows you to pass an `identity_hash` for each user. You can generate HMAC using 'sha256' with the key shown here."
|
"HMAC_DESCRIPTION": "Inorder validate the users identity, the SDK allows you to pass an `identifier_hash` for each user. You can generate HMAC using 'sha256' with the key shown here."
|
||||||
},
|
},
|
||||||
"FACEBOOK_REAUTHORIZE": {
|
"FACEBOOK_REAUTHORIZE": {
|
||||||
"TITLE": "Genautorisér",
|
"TITLE": "Genautorisér",
|
||||||
|
|||||||
@ -9,6 +9,7 @@
|
|||||||
"SEARCH_MESSAGES": "Nachrichten durchsuchen",
|
"SEARCH_MESSAGES": "Nachrichten durchsuchen",
|
||||||
"SEARCH": {
|
"SEARCH": {
|
||||||
"TITLE": "Nachrichten durchsuchen",
|
"TITLE": "Nachrichten durchsuchen",
|
||||||
|
"RESULT_TITLE": "Suchergebnisse",
|
||||||
"LOADING_MESSAGE": "Daten werden geladen...",
|
"LOADING_MESSAGE": "Daten werden geladen...",
|
||||||
"PLACEHOLDER": "Geben Sie einen Text ein, um danach zu suchen",
|
"PLACEHOLDER": "Geben Sie einen Text ein, um danach zu suchen",
|
||||||
"NO_MATCHING_RESULTS": "Keine Ergebnisse gefunden."
|
"NO_MATCHING_RESULTS": "Keine Ergebnisse gefunden."
|
||||||
|
|||||||
@ -273,7 +273,7 @@
|
|||||||
"INBOX_UPDATE_SUB_TEXT": "Posteingangseinstellungen aktualisieren",
|
"INBOX_UPDATE_SUB_TEXT": "Posteingangseinstellungen aktualisieren",
|
||||||
"AUTO_ASSIGNMENT_SUB_TEXT": "Aktivieren oder deaktivieren Sie die automatische Zuweisung verfügbarer Agenten für neue Konversationen",
|
"AUTO_ASSIGNMENT_SUB_TEXT": "Aktivieren oder deaktivieren Sie die automatische Zuweisung verfügbarer Agenten für neue Konversationen",
|
||||||
"HMAC_VERIFICATION": "Benutzeridentitätsüberprüfung",
|
"HMAC_VERIFICATION": "Benutzeridentitätsüberprüfung",
|
||||||
"HMAC_DESCRIPTION": "Um die Benutzer-Identität zu validieren, kannst du einen `identity_hash` für jeden Benutzer übergeben. Du kannst den Hash mithilfe des 'sha256' Verfahrens generieren, der notwendige Schlüssel wird hier angezeigt."
|
"HMAC_DESCRIPTION": "Um die Benutzer-Identität zu validieren, kannst du einen `identifier_hash` für jeden Benutzer übergeben. Du kannst den Hash mithilfe des 'sha256' Verfahrens generieren, der notwendige Schlüssel wird hier angezeigt."
|
||||||
},
|
},
|
||||||
"FACEBOOK_REAUTHORIZE": {
|
"FACEBOOK_REAUTHORIZE": {
|
||||||
"TITLE": "Neu autorisieren",
|
"TITLE": "Neu autorisieren",
|
||||||
|
|||||||
@ -273,7 +273,7 @@
|
|||||||
"INBOX_UPDATE_SUB_TEXT": "Ενημερώστε τις ρυθμίσεις του κιβωτίου σας",
|
"INBOX_UPDATE_SUB_TEXT": "Ενημερώστε τις ρυθμίσεις του κιβωτίου σας",
|
||||||
"AUTO_ASSIGNMENT_SUB_TEXT": "Ενεργοποιήστε ή απενεργοποιήστε την αυτόματη αντιστοίχιση των νέων συζητήσεων στους πράκτορες αυτού του κιβωτίου.",
|
"AUTO_ASSIGNMENT_SUB_TEXT": "Ενεργοποιήστε ή απενεργοποιήστε την αυτόματη αντιστοίχιση των νέων συζητήσεων στους πράκτορες αυτού του κιβωτίου.",
|
||||||
"HMAC_VERIFICATION": "Επικύρωση Ταυτότητας Χρήστη",
|
"HMAC_VERIFICATION": "Επικύρωση Ταυτότητας Χρήστη",
|
||||||
"HMAC_DESCRIPTION": "Για λόγους επικύρωσης της ταυτότητας των χρηστών, το SDK σας επιτρέπει να περάσετε ένα `identity_hash` για κάθε χρήστη. Μπορείτε να δημιουργήσετε HMAC χρησιμοποιώντας το 'sha256' με το κλειδί που εμφανίζεται εδώ."
|
"HMAC_DESCRIPTION": "Για λόγους επικύρωσης της ταυτότητας των χρηστών, το SDK σας επιτρέπει να περάσετε ένα `identifier_hash` για κάθε χρήστη. Μπορείτε να δημιουργήσετε HMAC χρησιμοποιώντας το 'sha256' με το κλειδί που εμφανίζεται εδώ."
|
||||||
},
|
},
|
||||||
"FACEBOOK_REAUTHORIZE": {
|
"FACEBOOK_REAUTHORIZE": {
|
||||||
"TITLE": "Εκ νέου εξουσιοδότηση",
|
"TITLE": "Εκ νέου εξουσιοδότηση",
|
||||||
|
|||||||
@ -91,6 +91,23 @@
|
|||||||
},
|
},
|
||||||
"SEARCH": {
|
"SEARCH": {
|
||||||
"NO_RESULTS": "No results found."
|
"NO_RESULTS": "No results found."
|
||||||
|
},
|
||||||
|
"MULTI_SELECTOR": {
|
||||||
|
"PLACEHOLDER": "None",
|
||||||
|
"TITLE": {
|
||||||
|
"AGENT": "Select agent",
|
||||||
|
"TEAM": "Select team"
|
||||||
|
},
|
||||||
|
"SEARCH": {
|
||||||
|
"NO_RESULTS": {
|
||||||
|
"AGENT": "No agents found",
|
||||||
|
"TEAM": "No teams found"
|
||||||
|
},
|
||||||
|
"PLACEHOLDER": {
|
||||||
|
"AGENT": "Search agents",
|
||||||
|
"TEAM": "Search teams"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,7 +2,10 @@
|
|||||||
"CAMPAIGN": {
|
"CAMPAIGN": {
|
||||||
"HEADER": "Campaigns",
|
"HEADER": "Campaigns",
|
||||||
"SIDEBAR_TXT": "Proactive messages allow the customer to send outbound messages to their contacts which would trigger more conversations. Click on <b>Add Campaign</b> to create a new campaign. You can also edit or delete an existing campaign by clicking on the Edit or Delete button.",
|
"SIDEBAR_TXT": "Proactive messages allow the customer to send outbound messages to their contacts which would trigger more conversations. Click on <b>Add Campaign</b> to create a new campaign. You can also edit or delete an existing campaign by clicking on the Edit or Delete button.",
|
||||||
"HEADER_BTN_TXT": "Create a campaign",
|
"HEADER_BTN_TXT": {
|
||||||
|
"ONE_OFF": "Create a one off campaign",
|
||||||
|
"ONGOING": "Create a ongoing campaign"
|
||||||
|
},
|
||||||
"ADD": {
|
"ADD": {
|
||||||
"TITLE": "Create a campaign",
|
"TITLE": "Create a campaign",
|
||||||
"DESC": "Proactive messages allow the customer to send outbound messages to their contacts which would trigger more conversations.",
|
"DESC": "Proactive messages allow the customer to send outbound messages to their contacts which would trigger more conversations.",
|
||||||
@ -25,6 +28,11 @@
|
|||||||
"PLACEHOLDER": "Select the customer labels",
|
"PLACEHOLDER": "Select the customer labels",
|
||||||
"ERROR": "Audience is required"
|
"ERROR": "Audience is required"
|
||||||
},
|
},
|
||||||
|
"INBOX": {
|
||||||
|
"LABEL": "Select Inbox",
|
||||||
|
"PLACEHOLDER": "Select Inbox",
|
||||||
|
"ERROR": "Inbox is required"
|
||||||
|
},
|
||||||
"MESSAGE": {
|
"MESSAGE": {
|
||||||
"LABEL": "Message",
|
"LABEL": "Message",
|
||||||
"PLACEHOLDER": "Please enter the message of campaign",
|
"PLACEHOLDER": "Please enter the message of campaign",
|
||||||
@ -80,6 +88,7 @@
|
|||||||
"TABLE_HEADER": {
|
"TABLE_HEADER": {
|
||||||
"TITLE": "Title",
|
"TITLE": "Title",
|
||||||
"MESSAGE": "Message",
|
"MESSAGE": "Message",
|
||||||
|
"INBOX": "Inbox",
|
||||||
"STATUS": "Status",
|
"STATUS": "Status",
|
||||||
"SENDER": "Sender",
|
"SENDER": "Sender",
|
||||||
"URL": "URL",
|
"URL": "URL",
|
||||||
@ -101,6 +110,16 @@
|
|||||||
"SENDER": {
|
"SENDER": {
|
||||||
"BOT": "Bot"
|
"BOT": "Bot"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"ONE_OFF": {
|
||||||
|
"HEADER": "One off campaigns",
|
||||||
|
"404": "There are no one off campaigns created",
|
||||||
|
"INBOXES_NOT_FOUND": "Please create an sms inbox and start adding campaigns"
|
||||||
|
},
|
||||||
|
"ONGOING": {
|
||||||
|
"HEADER": "Ongoing campaigns",
|
||||||
|
"404": "There are no ongoing campaigns created",
|
||||||
|
"INBOXES_NOT_FOUND": "Please create an website inbox and start adding campaigns"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -47,8 +47,12 @@
|
|||||||
"VALUE": "resolved"
|
"VALUE": "resolved"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"TEXT": "Bot",
|
"TEXT": "Pending",
|
||||||
"VALUE": "bot"
|
"VALUE": "pending"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"TEXT": "Snoozed",
|
||||||
|
"VALUE": "snoozed"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"ATTACHMENTS": {
|
"ATTACHMENTS": {
|
||||||
|
|||||||
@ -9,6 +9,7 @@
|
|||||||
"SEARCH_MESSAGES": "Search for messages in conversations",
|
"SEARCH_MESSAGES": "Search for messages in conversations",
|
||||||
"SEARCH": {
|
"SEARCH": {
|
||||||
"TITLE": "Search messages",
|
"TITLE": "Search messages",
|
||||||
|
"RESULT_TITLE": "Search Results",
|
||||||
"LOADING_MESSAGE": "Crunching data...",
|
"LOADING_MESSAGE": "Crunching data...",
|
||||||
"PLACEHOLDER": "Type any text to search messages",
|
"PLACEHOLDER": "Type any text to search messages",
|
||||||
"NO_MATCHING_RESULTS": "No results found."
|
"NO_MATCHING_RESULTS": "No results found."
|
||||||
@ -22,7 +23,7 @@
|
|||||||
"24_HOURS_WINDOW": "24 hour message window restriction",
|
"24_HOURS_WINDOW": "24 hour message window restriction",
|
||||||
"TWILIO_WHATSAPP_CAN_REPLY": "You can only reply to this conversation using a template message due to",
|
"TWILIO_WHATSAPP_CAN_REPLY": "You can only reply to this conversation using a template message due to",
|
||||||
"TWILIO_WHATSAPP_24_HOURS_WINDOW": "24 hour message window restriction",
|
"TWILIO_WHATSAPP_24_HOURS_WINDOW": "24 hour message window restriction",
|
||||||
"LAST_INCOMING_TWEET": "You are replying to the last incoming tweet",
|
"SELECT_A_TWEET_TO_REPLY": "Please select a tweet to reply to.",
|
||||||
"REPLYING_TO": "You are replying to:",
|
"REPLYING_TO": "You are replying to:",
|
||||||
"REMOVE_SELECTION": "Remove Selection",
|
"REMOVE_SELECTION": "Remove Selection",
|
||||||
"DOWNLOAD": "Download",
|
"DOWNLOAD": "Download",
|
||||||
@ -41,7 +42,13 @@
|
|||||||
"DETAILS": "details"
|
"DETAILS": "details"
|
||||||
},
|
},
|
||||||
"RESOLVE_DROPDOWN": {
|
"RESOLVE_DROPDOWN": {
|
||||||
"OPEN_BOT": "Open with bot"
|
"MARK_PENDING": "Mark as pending",
|
||||||
|
"SNOOZE": {
|
||||||
|
"TITLE": "Snooze until",
|
||||||
|
"NEXT_REPLY": "Next reply",
|
||||||
|
"TOMORROW": "Tomorrow",
|
||||||
|
"NEXT_WEEK": "Next week"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"FOOTER": {
|
"FOOTER": {
|
||||||
"MSG_INPUT": "Shift + enter for new line. Start with '/' to select a Canned Response.",
|
"MSG_INPUT": "Shift + enter for new line. Start with '/' to select a Canned Response.",
|
||||||
@ -57,7 +64,20 @@
|
|||||||
"TIP_EMOJI_ICON": "Show emoji selector",
|
"TIP_EMOJI_ICON": "Show emoji selector",
|
||||||
"TIP_ATTACH_ICON": "Attach files",
|
"TIP_ATTACH_ICON": "Attach files",
|
||||||
"ENTER_TO_SEND": "Enter to send",
|
"ENTER_TO_SEND": "Enter to send",
|
||||||
"DRAG_DROP": "Drag and drop here to attach"
|
"DRAG_DROP": "Drag and drop here to attach",
|
||||||
|
"EMAIL_HEAD": {
|
||||||
|
"ADD_BCC": "Add bcc",
|
||||||
|
"CC": {
|
||||||
|
"LABEL": "CC",
|
||||||
|
"PLACEHOLDER": "Emails separated by commas",
|
||||||
|
"ERROR": "Please enter valid email addresses"
|
||||||
|
},
|
||||||
|
"BCC": {
|
||||||
|
"LABEL": "BCC",
|
||||||
|
"PLACEHOLDER": "Emails separated by commas",
|
||||||
|
"ERROR": "Please enter valid email addresses"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"VISIBLE_TO_AGENTS": "Private Note: Only visible to you and your team",
|
"VISIBLE_TO_AGENTS": "Private Note: Only visible to you and your team",
|
||||||
"CHANGE_STATUS": "Conversation status changed",
|
"CHANGE_STATUS": "Conversation status changed",
|
||||||
@ -123,5 +143,11 @@
|
|||||||
"SELECT": {
|
"SELECT": {
|
||||||
"PLACEHOLDER": "None"
|
"PLACEHOLDER": "None"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"EMAIL_HEADER": {
|
||||||
|
"TO": "To",
|
||||||
|
"BCC": "Bcc",
|
||||||
|
"CC": "Cc",
|
||||||
|
"SUBJECT": "Subject"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -273,7 +273,7 @@
|
|||||||
"INBOX_UPDATE_SUB_TEXT": "Update your inbox settings",
|
"INBOX_UPDATE_SUB_TEXT": "Update your inbox settings",
|
||||||
"AUTO_ASSIGNMENT_SUB_TEXT": "Enable or disable the automatic assignment of new conversations to the agents added to this inbox.",
|
"AUTO_ASSIGNMENT_SUB_TEXT": "Enable or disable the automatic assignment of new conversations to the agents added to this inbox.",
|
||||||
"HMAC_VERIFICATION": "User Identity Validation",
|
"HMAC_VERIFICATION": "User Identity Validation",
|
||||||
"HMAC_DESCRIPTION": "Inorder validate the users identity, the SDK allows you to pass an `identity_hash` for each user. You can generate HMAC using 'sha256' with the key shown here."
|
"HMAC_DESCRIPTION": "Inorder to validate the user's identity, the SDK allows you to pass an `identifier_hash` for each user. You can generate HMAC using 'sha256' with the key shown here."
|
||||||
},
|
},
|
||||||
"FACEBOOK_REAUTHORIZE": {
|
"FACEBOOK_REAUTHORIZE": {
|
||||||
"TITLE": "Reauthorize",
|
"TITLE": "Reauthorize",
|
||||||
|
|||||||
@ -98,6 +98,7 @@
|
|||||||
"CHANGE_ACCOUNTS": "Switch Account",
|
"CHANGE_ACCOUNTS": "Switch Account",
|
||||||
"SELECTOR_SUBTITLE": "Select an account from the following list",
|
"SELECTOR_SUBTITLE": "Select an account from the following list",
|
||||||
"PROFILE_SETTINGS": "Profile Settings",
|
"PROFILE_SETTINGS": "Profile Settings",
|
||||||
|
"KEYBOARD_SHORTCUTS": "Keyboard Shortcuts",
|
||||||
"LOGOUT": "Logout"
|
"LOGOUT": "Logout"
|
||||||
},
|
},
|
||||||
"APP_GLOBAL": {
|
"APP_GLOBAL": {
|
||||||
@ -126,8 +127,8 @@
|
|||||||
"SIDEBAR": {
|
"SIDEBAR": {
|
||||||
"CONVERSATIONS": "Conversations",
|
"CONVERSATIONS": "Conversations",
|
||||||
"REPORTS": "Reports",
|
"REPORTS": "Reports",
|
||||||
"CONTACTS": "Contacts",
|
|
||||||
"SETTINGS": "Settings",
|
"SETTINGS": "Settings",
|
||||||
|
"CONTACTS": "Contacts",
|
||||||
"HOME": "Home",
|
"HOME": "Home",
|
||||||
"AGENTS": "Agents",
|
"AGENTS": "Agents",
|
||||||
"INBOXES": "Inboxes",
|
"INBOXES": "Inboxes",
|
||||||
@ -141,9 +142,13 @@
|
|||||||
"ALL_CONTACTS": "All Contacts",
|
"ALL_CONTACTS": "All Contacts",
|
||||||
"TAGGED_WITH": "Tagged with",
|
"TAGGED_WITH": "Tagged with",
|
||||||
"REPORTS_OVERVIEW": "Overview",
|
"REPORTS_OVERVIEW": "Overview",
|
||||||
"CSAT": "CSAT"
|
"CSAT": "CSAT",
|
||||||
|
"CAMPAIGNS": "Campaigns",
|
||||||
|
"ONGOING": "Ongoing",
|
||||||
|
"ONE_OFF": "One off"
|
||||||
},
|
},
|
||||||
"CREATE_ACCOUNT": {
|
"CREATE_ACCOUNT": {
|
||||||
|
"NO_ACCOUNT_WARNING": "Uh oh! We could not find any Chatwoot accounts. Please create a new account to continue.",
|
||||||
"NEW_ACCOUNT": "New Account",
|
"NEW_ACCOUNT": "New Account",
|
||||||
"SELECTOR_SUBTITLE": "Create a new account",
|
"SELECTOR_SUBTITLE": "Create a new account",
|
||||||
"API": {
|
"API": {
|
||||||
@ -158,5 +163,30 @@
|
|||||||
},
|
},
|
||||||
"SUBMIT": "Submit"
|
"SUBMIT": "Submit"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"KEYBOARD_SHORTCUTS": {
|
||||||
|
"TITLE": {
|
||||||
|
"OPEN_CONVERSATION": "Open conversation",
|
||||||
|
"RESOLVE_AND_NEXT": "Resolve and move to next",
|
||||||
|
"NAVIGATE_DROPDOWN": "Navigate dropdown items",
|
||||||
|
"RESOLVE_CONVERSATION": "Resolve Conversation",
|
||||||
|
"GO_TO_CONVERSATION_DASHBOARD": "Go to Conversation Dashboard",
|
||||||
|
"ADD_ATTACHMENT": "Add Attachment",
|
||||||
|
"GO_TO_CONTACTS_DASHBOARD": "Go to Contacts Dashboard",
|
||||||
|
"TOGGLE_SIDEBAR": "Toggle Sidebar",
|
||||||
|
"GO_TO_REPORTS_SIDEBAR": "Go to Reports sidebar",
|
||||||
|
"MOVE_TO_NEXT_TAB": "Move to next tab in conversation list",
|
||||||
|
"GO_TO_SETTINGS": "Go to Settings",
|
||||||
|
"SWITCH_CONVERSATION_STATUS": "Switch Conversation status",
|
||||||
|
"SWITCH_TO_PRIVATE_NOTE": "Switch to Private Note",
|
||||||
|
"TOGGLE_RICH_CONTENT_EDITOR": "Toggle Rich Content editor",
|
||||||
|
"SWITCH_TO_REPLY": "Switch to Reply",
|
||||||
|
"TOGGLE_SNOOZE_DROPDOWN": "Toggle snooze dropdown"
|
||||||
|
},
|
||||||
|
"KEYS": {
|
||||||
|
"COMMAND_KEY": "⌘",
|
||||||
|
"ALT_OR_OPTION_KEY": "Alt / ⌥",
|
||||||
|
"FORWARD_SLASH_KEY": "/"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -273,7 +273,7 @@
|
|||||||
"INBOX_UPDATE_SUB_TEXT": "Actualizar la configuración de tu bandeja de entrada",
|
"INBOX_UPDATE_SUB_TEXT": "Actualizar la configuración de tu bandeja de entrada",
|
||||||
"AUTO_ASSIGNMENT_SUB_TEXT": "Activar o desactivar la asignación automática de nuevas conversaciones a los agentes añadidos a esta bandeja de entrada.",
|
"AUTO_ASSIGNMENT_SUB_TEXT": "Activar o desactivar la asignación automática de nuevas conversaciones a los agentes añadidos a esta bandeja de entrada.",
|
||||||
"HMAC_VERIFICATION": "Validación de identidad de usuario",
|
"HMAC_VERIFICATION": "Validación de identidad de usuario",
|
||||||
"HMAC_DESCRIPTION": "Con el fin de validar la identidad de los usuarios, el SDK le permite pasar un `identity_hash` por cada usuario. Puede generar HMAC usando 'sha256' con la clave que se muestra aquí."
|
"HMAC_DESCRIPTION": "Con el fin de validar la identidad de los usuarios, el SDK le permite pasar un `identifier_hash` por cada usuario. Puede generar HMAC usando 'sha256' con la clave que se muestra aquí."
|
||||||
},
|
},
|
||||||
"FACEBOOK_REAUTHORIZE": {
|
"FACEBOOK_REAUTHORIZE": {
|
||||||
"TITLE": "Reautorizar",
|
"TITLE": "Reautorizar",
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
"HEADER": "Edustajat",
|
"HEADER": "Edustajat",
|
||||||
"HEADER_BTN_TXT": "Lisää edustaja",
|
"HEADER_BTN_TXT": "Lisää edustaja",
|
||||||
"LOADING": "Haetaan Edustajalistaa",
|
"LOADING": "Haetaan Edustajalistaa",
|
||||||
"SIDEBAR_TXT": "<p><b>Edustajat</b></p> <p> An <b>Edustaja</b> on jäsenenä asiakastukitiimissäsi. </p><p> Edustajat voivat katsella ja vastata viesteihin asiakkailtasi. Luettelo näyttää kaikki edustajat, jotka ovat tällä hetkellä tililläsi. </p><p> Klikkaa <b>Lisää edustaja</b> lisätäksesi uuden edustajan. Edustaja, jonka lisäät, saa sähköpostiviestin, jossa on vahvistuslinkki tilin aktivointiin, jonka jälkeen he voivat käyttää Chatwoot -sovellusta ja vastata viesteihin. </p><p> Pääsy Chatwoot'n ominaisuuksiin perustuu seuraaviin rooleihin. </p><p> <b>Edustaja</b> - Tällä roolilla toimivat edustajat voivat käyttää vain saapuneita, raportteja ja keskusteluja. He voivat määrittää keskusteluja muille edustajille tai itse ratkaista keskusteluja.</p><p> <b>Ylläpitäjä</b> - Ylläpitäjällä on pääsy kaikkiin Chatwoot ominaisuuksiin, jotka ovat käytössä tililläsi, mukaan lukien asetukset sekä kaikki normaalien asiamiesten oikeudet.</p>",
|
"SIDEBAR_TXT": "<p><b>Edustajat</b></p> <p> <b>Edustaja</b> on jäsenenä asiakastukitiimissäsi. </p><p> Edustajat voivat katsella ja vastata viesteihin asiakkailtasi. Luettelo näyttää kaikki edustajat, jotka ovat tällä hetkellä tililläsi. </p><p> Klikkaa <b>Lisää edustaja</b> lisätäksesi uuden edustajan. Edustaja, jonka lisäät, saa sähköpostiviestin, jossa on vahvistuslinkki tilin aktivointiin, jonka jälkeen he voivat käyttää Chatwoot -sovellusta ja vastata viesteihin. </p><p> Pääsy Chatwoot'n ominaisuuksiin perustuu seuraaviin rooleihin. </p><p> <b>Edustaja</b> - Tällä roolilla toimivat edustajat voivat käyttää vain saapuneita, raportteja ja keskusteluja. He voivat määrittää keskusteluja muille edustajille tai itse ratkaista keskusteluja.</p><p> <b>Ylläpitäjä</b> - Ylläpitäjällä on pääsy kaikkiin Chatwoot ominaisuuksiin, jotka ovat käytössä tililläsi, mukaan lukien asetukset sekä kaikki normaalien asiamiesten oikeudet.</p>",
|
||||||
"AGENT_TYPES": {
|
"AGENT_TYPES": {
|
||||||
"ADMINISTRATOR": "Ylläpitäjä",
|
"ADMINISTRATOR": "Ylläpitäjä",
|
||||||
"AGENT": "Edustajat"
|
"AGENT": "Edustajat"
|
||||||
@ -29,9 +29,9 @@
|
|||||||
"PLACEHOLDER": "Ole hyvä ja kirjoita edustajan nimi"
|
"PLACEHOLDER": "Ole hyvä ja kirjoita edustajan nimi"
|
||||||
},
|
},
|
||||||
"AGENT_TYPE": {
|
"AGENT_TYPE": {
|
||||||
"LABEL": "Agent Type",
|
"LABEL": "Edustajan tyyppi",
|
||||||
"PLACEHOLDER": "Please select a type",
|
"PLACEHOLDER": "Valitse tyyppi",
|
||||||
"ERROR": "Agent type is required"
|
"ERROR": "Edustajan tyyppi on pakollinen"
|
||||||
},
|
},
|
||||||
"EMAIL": {
|
"EMAIL": {
|
||||||
"LABEL": "Sähköpostiosoite",
|
"LABEL": "Sähköpostiosoite",
|
||||||
@ -66,9 +66,9 @@
|
|||||||
"PLACEHOLDER": "Ole hyvä ja kirjoita edustajan nimi"
|
"PLACEHOLDER": "Ole hyvä ja kirjoita edustajan nimi"
|
||||||
},
|
},
|
||||||
"AGENT_TYPE": {
|
"AGENT_TYPE": {
|
||||||
"LABEL": "Agent Type",
|
"LABEL": "Edustajan tyyppi",
|
||||||
"PLACEHOLDER": "Please select a type",
|
"PLACEHOLDER": "Valitse edustajan tyyppi",
|
||||||
"ERROR": "Agent type is required"
|
"ERROR": "Edustajan tyyppi on pakollinen"
|
||||||
},
|
},
|
||||||
"EMAIL": {
|
"EMAIL": {
|
||||||
"LABEL": "Sähköpostiosoite",
|
"LABEL": "Sähköpostiosoite",
|
||||||
@ -90,7 +90,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"SEARCH": {
|
"SEARCH": {
|
||||||
"NO_RESULTS": "No results found."
|
"NO_RESULTS": "Ei hakutuloksia."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,56 +1,56 @@
|
|||||||
{
|
{
|
||||||
"CAMPAIGN": {
|
"CAMPAIGN": {
|
||||||
"HEADER": "Campaigns",
|
"HEADER": "Kampanja",
|
||||||
"SIDEBAR_TXT": "Proactive messages allow the customer to send outbound messages to their contacts which would trigger more conversations. Click on <b>Add Campaign</b> to create a new campaign. You can also edit or delete an existing campaign by clicking on the Edit or Delete button.",
|
"SIDEBAR_TXT": "Proactive messages allow the customer to send outbound messages to their contacts which would trigger more conversations. Click on <b>Add Campaign</b> to create a new kampanja. You can also edit or delete an existing kampanja by clicking on the Edit or Delete button.",
|
||||||
"HEADER_BTN_TXT": "Create a campaign",
|
"HEADER_BTN_TXT": "Luo kampanja",
|
||||||
"ADD": {
|
"ADD": {
|
||||||
"TITLE": "Create a campaign",
|
"TITLE": "Luo kampanja",
|
||||||
"DESC": "Proactive messages allow the customer to send outbound messages to their contacts which would trigger more conversations.",
|
"DESC": "Proactive messages allow the customer to send outbound messages to their contacts which would trigger more conversations.",
|
||||||
"CANCEL_BUTTON_TEXT": "Peruuta",
|
"CANCEL_BUTTON_TEXT": "Peruuta",
|
||||||
"CREATE_BUTTON_TEXT": "Luo",
|
"CREATE_BUTTON_TEXT": "Luo",
|
||||||
"FORM": {
|
"FORM": {
|
||||||
"TITLE": {
|
"TITLE": {
|
||||||
"LABEL": "Title",
|
"LABEL": "Otsikko",
|
||||||
"PLACEHOLDER": "Please enter the title of campaign",
|
"PLACEHOLDER": "Syötä kampanjan otsikko",
|
||||||
"ERROR": "Title is required"
|
"ERROR": "Otsikko on pakollinen"
|
||||||
},
|
},
|
||||||
"SCHEDULED_AT": {
|
"SCHEDULED_AT": {
|
||||||
"LABEL": "Scheduled time",
|
"LABEL": "Kampanjan alkamisaika",
|
||||||
"PLACEHOLDER": "Please select the time",
|
"PLACEHOLDER": "Syötä kampanja alkamisaika",
|
||||||
"CONFIRM": "Confirm",
|
"CONFIRM": "Varmista",
|
||||||
"ERROR": "Scheduled time is required"
|
"ERROR": "Alkamisaika on pakollinen"
|
||||||
},
|
},
|
||||||
"AUDIENCE": {
|
"AUDIENCE": {
|
||||||
"LABEL": "Audience",
|
"LABEL": "Kohdeyleisö",
|
||||||
"PLACEHOLDER": "Select the customer labels",
|
"PLACEHOLDER": "Valitse tunnisteet",
|
||||||
"ERROR": "Audience is required"
|
"ERROR": "Kohdeyleisö on pakollinen"
|
||||||
},
|
},
|
||||||
"MESSAGE": {
|
"MESSAGE": {
|
||||||
"LABEL": "Message",
|
"LABEL": "Viesti",
|
||||||
"PLACEHOLDER": "Please enter the message of campaign",
|
"PLACEHOLDER": "Syötä kampanjan viesti",
|
||||||
"ERROR": "Message is required"
|
"ERROR": "Viesti on pakollinen"
|
||||||
},
|
},
|
||||||
"SENT_BY": {
|
"SENT_BY": {
|
||||||
"LABEL": "Lähettäjä",
|
"LABEL": "Lähettäjä",
|
||||||
"PLACEHOLDER": "Please select the the content of campaign",
|
"PLACEHOLDER": "Syötä kampanjan lähettäjä",
|
||||||
"ERROR": "Sender is required"
|
"ERROR": "Lähettäjä on pakollinen"
|
||||||
},
|
},
|
||||||
"END_POINT": {
|
"END_POINT": {
|
||||||
"LABEL": "URL",
|
"LABEL": "URL",
|
||||||
"PLACEHOLDER": "Please enter the URL",
|
"PLACEHOLDER": "Syötä kampanjan URL",
|
||||||
"ERROR": "Anna kelvollinen URL-osoite"
|
"ERROR": "Anna kelvollinen URL-osoite"
|
||||||
},
|
},
|
||||||
"TIME_ON_PAGE": {
|
"TIME_ON_PAGE": {
|
||||||
"LABEL": "Time on page(Seconds)",
|
"LABEL": "Aika sivulla (sekunttia)",
|
||||||
"PLACEHOLDER": "Please enter the time",
|
"PLACEHOLDER": "Syötä aika",
|
||||||
"ERROR": "Time on page is required"
|
"ERROR": "Aika sivulla on pakollinen"
|
||||||
},
|
},
|
||||||
"ENABLED": "Enable campaign",
|
"ENABLED": "Aktivoi kampanja",
|
||||||
"SUBMIT": "Add Campaign"
|
"SUBMIT": "Lisää kampanja"
|
||||||
},
|
},
|
||||||
"API": {
|
"API": {
|
||||||
"SUCCESS_MESSAGE": "Campaign created successfully",
|
"SUCCESS_MESSAGE": "Kampanja luotu onnistuneesti",
|
||||||
"ERROR_MESSAGE": "There was an error. Please try again."
|
"ERROR_MESSAGE": "Hö! Kampanjaa luodessa tapahtui virhe"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"DELETE": {
|
"DELETE": {
|
||||||
@ -62,41 +62,41 @@
|
|||||||
"NO": "Ei, säilytä "
|
"NO": "Ei, säilytä "
|
||||||
},
|
},
|
||||||
"API": {
|
"API": {
|
||||||
"SUCCESS_MESSAGE": "Campaign deleted successfully",
|
"SUCCESS_MESSAGE": "Kampanja poistettu onnistuneesti",
|
||||||
"ERROR_MESSAGE": "Could not delete the campaign. Please try again later."
|
"ERROR_MESSAGE": "Hö! Kampanjaa poistaessa tapahtui virhe. Kokeile myöhemmin uudelleen"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"EDIT": {
|
"EDIT": {
|
||||||
"TITLE": "Edit campaign",
|
"TITLE": "Muokkaa kampanjaa",
|
||||||
"UPDATE_BUTTON_TEXT": "Päivitä",
|
"UPDATE_BUTTON_TEXT": "Päivitä",
|
||||||
"API": {
|
"API": {
|
||||||
"SUCCESS_MESSAGE": "Campaign updated successfully",
|
"SUCCESS_MESSAGE": "Kampanjaa muokattu onnistuneesti",
|
||||||
"ERROR_MESSAGE": "Tapahtui virhe, yritä uudelleen"
|
"ERROR_MESSAGE": "Hö! Kampanjaa muokatessa tapahtui virhe"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"LIST": {
|
"LIST": {
|
||||||
"LOADING_MESSAGE": "Loading campaigns...",
|
"LOADING_MESSAGE": "Ladataan kampanjoita...",
|
||||||
"404": "There are no campaigns created for this inbox.",
|
"404": "Tälle kansiolle ei ole luotu kampanjoita.",
|
||||||
"TABLE_HEADER": {
|
"TABLE_HEADER": {
|
||||||
"TITLE": "Title",
|
"TITLE": "Otsikko",
|
||||||
"MESSAGE": "Message",
|
"MESSAGE": "Viesti",
|
||||||
"STATUS": "Tila",
|
"STATUS": "Tila",
|
||||||
"SENDER": "Sender",
|
"SENDER": "Lähettäjä",
|
||||||
"URL": "URL",
|
"URL": "URL",
|
||||||
"SCHEDULED_AT": "Scheduled time",
|
"SCHEDULED_AT": "Aloitusaika",
|
||||||
"TIME_ON_PAGE": "Time(Seconds)",
|
"TIME_ON_PAGE": "Aika sivulla (sekunttia)",
|
||||||
"CREATED_AT": "Created at"
|
"CREATED_AT": "Luotu"
|
||||||
},
|
},
|
||||||
"BUTTONS": {
|
"BUTTONS": {
|
||||||
"ADD": "Add",
|
"ADD": "Luo uusi",
|
||||||
"EDIT": "Muokkaa",
|
"EDIT": "Muokkaa",
|
||||||
"DELETE": "Poista"
|
"DELETE": "Poista"
|
||||||
},
|
},
|
||||||
"STATUS": {
|
"STATUS": {
|
||||||
"ENABLED": "Käytössä",
|
"ENABLED": "Käytössä",
|
||||||
"DISABLED": "Pois käytöstä",
|
"DISABLED": "Pois käytöstä",
|
||||||
"COMPLETED": "Completed",
|
"COMPLETED": "Suoritettu",
|
||||||
"ACTIVE": "Active"
|
"ACTIVE": "Käynnissä"
|
||||||
},
|
},
|
||||||
"SENDER": {
|
"SENDER": {
|
||||||
"BOT": "Botti"
|
"BOT": "Botti"
|
||||||
|
|||||||
@ -81,6 +81,6 @@
|
|||||||
"VIEW_TWEET_IN_TWITTER": "Näytä twiitti Twitterissä",
|
"VIEW_TWEET_IN_TWITTER": "Näytä twiitti Twitterissä",
|
||||||
"REPLY_TO_TWEET": "Vastaa tähän twiittiin",
|
"REPLY_TO_TWEET": "Vastaa tähän twiittiin",
|
||||||
"NO_MESSAGES": "Ei Viestejä",
|
"NO_MESSAGES": "Ei Viestejä",
|
||||||
"NO_CONTENT": "No content available"
|
"NO_CONTENT": "Sisältöä ei saatavilla"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,17 +19,17 @@
|
|||||||
},
|
},
|
||||||
"LABELS": {
|
"LABELS": {
|
||||||
"CONTACT": {
|
"CONTACT": {
|
||||||
"TITLE": "Contact Labels",
|
"TITLE": "Yhteystietotunnisteet",
|
||||||
"ERROR": "Couldn't update labels"
|
"ERROR": "Tunnisteita ei voitu päivittää"
|
||||||
},
|
},
|
||||||
"CONVERSATION": {
|
"CONVERSATION": {
|
||||||
"TITLE": "Keskustelutunnisteet",
|
"TITLE": "Keskustelutunnisteet",
|
||||||
"ADD_BUTTON": "Add Labels"
|
"ADD_BUTTON": "Lisää tunnisteita"
|
||||||
},
|
},
|
||||||
"LABEL_SELECT": {
|
"LABEL_SELECT": {
|
||||||
"TITLE": "Add Labels",
|
"TITLE": "Lisää tunnisteita",
|
||||||
"PLACEHOLDER": "Search labels",
|
"PLACEHOLDER": "Hae tunnisteita",
|
||||||
"NO_RESULT": "No labels found"
|
"NO_RESULT": "Tunnisteita ei löytynyt"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"MUTE_CONTACT": "Mykistä Keskustelu",
|
"MUTE_CONTACT": "Mykistä Keskustelu",
|
||||||
@ -71,8 +71,8 @@
|
|||||||
"PHONE_NUMBER": {
|
"PHONE_NUMBER": {
|
||||||
"PLACEHOLDER": "Anna yhteystiedon puhelinnumero",
|
"PLACEHOLDER": "Anna yhteystiedon puhelinnumero",
|
||||||
"LABEL": "Puhelinnumero",
|
"LABEL": "Puhelinnumero",
|
||||||
"HELP": "Phone number should be of E.164 format eg: +1415555555 [+][country code][area code][local phone number]",
|
"HELP": "Puhelinnumeron tulee olla E.164 formaattia, esim: +1415555555 [+][maakoodi][aluekoodi][puhelinnumero]",
|
||||||
"ERROR": "Phone number should be either empty or of E.164 format"
|
"ERROR": "Puhelinnumeron tulee olla tyhjä tai E.164 formaattia"
|
||||||
},
|
},
|
||||||
"LOCATION": {
|
"LOCATION": {
|
||||||
"PLACEHOLDER": "Anna yhteystiedon sijainti",
|
"PLACEHOLDER": "Anna yhteystiedon sijainti",
|
||||||
@ -109,129 +109,129 @@
|
|||||||
"BUTTON_LABEL": "Aloita keskustelu",
|
"BUTTON_LABEL": "Aloita keskustelu",
|
||||||
"TITLE": "Uusi keskustelu",
|
"TITLE": "Uusi keskustelu",
|
||||||
"DESC": "Aloita uusi keskustelu lähettämällä uusi viesti.",
|
"DESC": "Aloita uusi keskustelu lähettämällä uusi viesti.",
|
||||||
"NO_INBOX": "Couldn't find an inbox to initiate a new conversation with this contact.",
|
"NO_INBOX": "Kansiota jolla tälle kontaktille olisi voinut luoda keskustelun ei löytynyt.",
|
||||||
"FORM": {
|
"FORM": {
|
||||||
"TO": {
|
"TO": {
|
||||||
"LABEL": "To"
|
"LABEL": "Kohde"
|
||||||
},
|
},
|
||||||
"INBOX": {
|
"INBOX": {
|
||||||
"LABEL": "Inbox",
|
"LABEL": "Kansio",
|
||||||
"ERROR": "Select an inbox"
|
"ERROR": "Valitse kansio"
|
||||||
},
|
},
|
||||||
"MESSAGE": {
|
"MESSAGE": {
|
||||||
"LABEL": "Message",
|
"LABEL": "Viesti",
|
||||||
"PLACEHOLDER": "Write your message here",
|
"PLACEHOLDER": "Kirjoita viestisi tähän",
|
||||||
"ERROR": "Message can't be empty"
|
"ERROR": "Viesti ei voi olla tyhjä"
|
||||||
},
|
},
|
||||||
"SUBMIT": "Send message",
|
"SUBMIT": "Lähetä viesti",
|
||||||
"CANCEL": "Peruuta",
|
"CANCEL": "Peruuta",
|
||||||
"SUCCESS_MESSAGE": "Message sent!",
|
"SUCCESS_MESSAGE": "Viesti lähetetty!",
|
||||||
"ERROR_MESSAGE": "Couldn't send! try again"
|
"ERROR_MESSAGE": "Hö! Virhe, yritä uudelleen"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"CONTACTS_PAGE": {
|
"CONTACTS_PAGE": {
|
||||||
"HEADER": "Yhteystiedot",
|
"HEADER": "Yhteystiedot",
|
||||||
"FIELDS": "Contact fields",
|
"FIELDS": "Yhteystietokentät",
|
||||||
"SEARCH_BUTTON": "Etsi",
|
"SEARCH_BUTTON": "Etsi",
|
||||||
"SEARCH_INPUT_PLACEHOLDER": "Etsi yhteystietoja",
|
"SEARCH_INPUT_PLACEHOLDER": "Etsi yhteystietoja",
|
||||||
"LIST": {
|
"LIST": {
|
||||||
"LOADING_MESSAGE": "Ladataan yhteystietoja...",
|
"LOADING_MESSAGE": "Ladataan yhteystietoja...",
|
||||||
"404": "Ei hakua vastaavia yhteystietoja 🔍",
|
"404": "Ei hakua vastaavia yhteystietoja 🔍",
|
||||||
"NO_CONTACTS": "There are no available contacts",
|
"NO_CONTACTS": "Yhtään kontaktia ei löytynyt.",
|
||||||
"TABLE_HEADER": {
|
"TABLE_HEADER": {
|
||||||
"NAME": "Nimi",
|
"NAME": "Nimi",
|
||||||
"PHONE_NUMBER": "Puhelinnumero",
|
"PHONE_NUMBER": "Puhelinnumero",
|
||||||
"CONVERSATIONS": "Keskustelut",
|
"CONVERSATIONS": "Keskustelut",
|
||||||
"LAST_ACTIVITY": "Last Activity",
|
"LAST_ACTIVITY": "Viimeksi aktiivinen",
|
||||||
"COUNTRY": "Country",
|
"COUNTRY": "Maa",
|
||||||
"CITY": "City",
|
"CITY": "Kaupunki",
|
||||||
"SOCIAL_PROFILES": "Social Profiles",
|
"SOCIAL_PROFILES": "Sosiaalisen median tilit",
|
||||||
"COMPANY": "Yritys",
|
"COMPANY": "Yritys",
|
||||||
"EMAIL_ADDRESS": "Sähköpostiosoite"
|
"EMAIL_ADDRESS": "Sähköpostiosoite"
|
||||||
},
|
},
|
||||||
"VIEW_DETAILS": "View details"
|
"VIEW_DETAILS": "Katso lisätietoja"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"REMINDER": {
|
"REMINDER": {
|
||||||
"ADD_BUTTON": {
|
"ADD_BUTTON": {
|
||||||
"BUTTON": "Add",
|
"BUTTON": "Lisää",
|
||||||
"TITLE": "Shift + Enter to create a task"
|
"TITLE": "Shift + Enter luodaksesi tehtävän"
|
||||||
},
|
},
|
||||||
"FOOTER": {
|
"FOOTER": {
|
||||||
"DUE_DATE": "Due date",
|
"DUE_DATE": "Eräpäivä",
|
||||||
"LABEL_TITLE": "Set type"
|
"LABEL_TITLE": "Aseta tyyppi"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"NOTES": {
|
"NOTES": {
|
||||||
"HEADER": {
|
"HEADER": {
|
||||||
"TITLE": "Notes"
|
"TITLE": "Muistiinpanot"
|
||||||
},
|
},
|
||||||
"ADD": {
|
"ADD": {
|
||||||
"BUTTON": "Add",
|
"BUTTON": "Lisää",
|
||||||
"PLACEHOLDER": "Add a note",
|
"PLACEHOLDER": "Lisää muistiinpano",
|
||||||
"TITLE": "Shift + Enter to create a note"
|
"TITLE": "Shift + Enter luodaksesi muistiinpano"
|
||||||
},
|
},
|
||||||
"FOOTER": {
|
"FOOTER": {
|
||||||
"BUTTON": "View all notes"
|
"BUTTON": "Näe kaikki muistiinpanot"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"EVENTS": {
|
"EVENTS": {
|
||||||
"HEADER": {
|
"HEADER": {
|
||||||
"TITLE": "Activities"
|
"TITLE": "Aktiviteetit"
|
||||||
},
|
},
|
||||||
"BUTTON": {
|
"BUTTON": {
|
||||||
"PILL_BUTTON_NOTES": "notes",
|
"PILL_BUTTON_NOTES": "muistiinpanot",
|
||||||
"PILL_BUTTON_EVENTS": "events",
|
"PILL_BUTTON_EVENTS": "tapahtumat",
|
||||||
"PILL_BUTTON_CONVO": "keskustelut"
|
"PILL_BUTTON_CONVO": "keskustelut"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"CUSTOM_ATTRIBUTES": {
|
"CUSTOM_ATTRIBUTES": {
|
||||||
"TITLE": "Mukautetut attribuutit",
|
"TITLE": "Mukautetut attribuutit",
|
||||||
"BUTTON": "Add custom attribute",
|
"BUTTON": "Lisää kustomoitu attribuutti",
|
||||||
"ADD": {
|
"ADD": {
|
||||||
"TITLE": "Create custom attribute",
|
"TITLE": "Lisää kustomoitu attribuutti",
|
||||||
"DESC": "Add custom information to this contact."
|
"DESC": "Lisää lisätietoja tähän kontaktiin."
|
||||||
},
|
},
|
||||||
"FORM": {
|
"FORM": {
|
||||||
"CREATE": "Add attribute",
|
"CREATE": "Lisää atribuutti",
|
||||||
"CANCEL": "Peruuta",
|
"CANCEL": "Peruuta",
|
||||||
"NAME": {
|
"NAME": {
|
||||||
"LABEL": "Custom attribute name",
|
"LABEL": "Attribuutin nimi",
|
||||||
"PLACEHOLDER": "Eg: shopify id",
|
"PLACEHOLDER": "Esim: shopify id",
|
||||||
"ERROR": "Invalid custom attribute name"
|
"ERROR": "Virheellinen nimi"
|
||||||
},
|
},
|
||||||
"VALUE": {
|
"VALUE": {
|
||||||
"LABEL": "Attribute value",
|
"LABEL": "Attribuutin arvo",
|
||||||
"PLACEHOLDER": "Eg: 11901 "
|
"PLACEHOLDER": "Esim: 11901 "
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"MERGE_CONTACTS": {
|
"MERGE_CONTACTS": {
|
||||||
"TITLE": "Merge contacts",
|
"TITLE": "Yhdistä kontakteja",
|
||||||
"DESCRIPTION": "Merge contact is helpful when you have duplicated entries of the same contact. Merging action takes a primary contact and a child contact. After merging, all details in the primary contact will remain the same. If the primary contact doesn't have a field, then the value from the child contact will be used after merging. If a conflict happens, fields in primary contact will remain unaffected, but fields from secondary will be copied to the custom attributes in the primary contact.",
|
"DESCRIPTION": "Yhdistää kaksi kontakia. Ensimmäinen kontaktin tiedot pysyvät koskemattomina, mutta toisen attribuutit kopioidaan ensimmäiseen kustomoituina attribuutteina.",
|
||||||
"PRIMARY": {
|
"PRIMARY": {
|
||||||
"TITLE": "Primary contact"
|
"TITLE": "Ensimmäinen kontakti"
|
||||||
},
|
},
|
||||||
"CHILD": {
|
"CHILD": {
|
||||||
"TITLE": "Contact to merge",
|
"TITLE": "Yhdistettävä kontakti",
|
||||||
"PLACEHOLDER": "Choose a contact"
|
"PLACEHOLDER": "Valitse kontakti"
|
||||||
},
|
},
|
||||||
"SUMMARY": {
|
"SUMMARY": {
|
||||||
"TITLE": "Summary",
|
"TITLE": "Yhteenveto",
|
||||||
"DELETE_WARNING": "Contact of <strong>%{childContactName}</strong>will be deleted.",
|
"DELETE_WARNING": "Kontakti <strong>%{childContactName}</strong> poistetaan.",
|
||||||
"ATTRIBUTE_WARNING": "Contact details of <strong>%{childContactName}</strong> will be copied to <strong>%{primaryContactName}</strong>."
|
"ATTRIBUTE_WARNING": "Kontaktin <strong>%{childContactName}</strong> tiedot kopioidaan kontaktiin <strong>%{primaryContactName}</strong>."
|
||||||
},
|
},
|
||||||
"SEARCH": {
|
"SEARCH": {
|
||||||
"ERROR": "ERROR_MESSAGE"
|
"ERROR": "ERROR_MESSAGE"
|
||||||
},
|
},
|
||||||
"FORM": {
|
"FORM": {
|
||||||
"SUBMIT": " Merge contacts",
|
"SUBMIT": " Yhdistä",
|
||||||
"CANCEL": "Peruuta",
|
"CANCEL": "Peruuta",
|
||||||
"CHILD_CONTACT": {
|
"CHILD_CONTACT": {
|
||||||
"ERROR": "Select a child contact to merge"
|
"ERROR": "Valitse yhdistettävä tili"
|
||||||
},
|
},
|
||||||
"SUCCESS_MESSAGE": "Contact merged successfully",
|
"SUCCESS_MESSAGE": "Kontaktien yhdistäminen onnistui",
|
||||||
"ERROR_MESSAGE": "Could not merge contcts, try again!"
|
"ERROR_MESSAGE": "Virhe, yritä uudelleen!"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -27,11 +27,11 @@
|
|||||||
"REMOVE_SELECTION": "Poista valinnat",
|
"REMOVE_SELECTION": "Poista valinnat",
|
||||||
"DOWNLOAD": "Lataa",
|
"DOWNLOAD": "Lataa",
|
||||||
"UPLOADING_ATTACHMENTS": "Ladataan liitteitä...",
|
"UPLOADING_ATTACHMENTS": "Ladataan liitteitä...",
|
||||||
"SUCCESS_DELETE_MESSAGE": "Message deleted successfully",
|
"SUCCESS_DELETE_MESSAGE": "Viesti poistettu onnistuneesti",
|
||||||
"FAIL_DELETE_MESSSAGE": "Couldn't delete message! Try again",
|
"FAIL_DELETE_MESSSAGE": "Viestin poistaminen epäonnistui.",
|
||||||
"NO_RESPONSE": "No response",
|
"NO_RESPONSE": "Ei vastausta",
|
||||||
"RATING_TITLE": "Rating",
|
"RATING_TITLE": "Arvostelut",
|
||||||
"FEEDBACK_TITLE": "Feedback",
|
"FEEDBACK_TITLE": "Palaute",
|
||||||
"HEADER": {
|
"HEADER": {
|
||||||
"RESOLVE_ACTION": "Ratkaise",
|
"RESOLVE_ACTION": "Ratkaise",
|
||||||
"REOPEN_ACTION": "Uudelleenavaa",
|
"REOPEN_ACTION": "Uudelleenavaa",
|
||||||
@ -41,7 +41,7 @@
|
|||||||
"DETAILS": "tiedot"
|
"DETAILS": "tiedot"
|
||||||
},
|
},
|
||||||
"RESOLVE_DROPDOWN": {
|
"RESOLVE_DROPDOWN": {
|
||||||
"OPEN_BOT": "Open with bot"
|
"OPEN_BOT": "Avaa botilla"
|
||||||
},
|
},
|
||||||
"FOOTER": {
|
"FOOTER": {
|
||||||
"MSG_INPUT": "Vaihto + enter siirtyäksesi uudelle riville. Aloita '/' valitaksesi tallennettu vastaus.",
|
"MSG_INPUT": "Vaihto + enter siirtyäksesi uudelle riville. Aloita '/' valitaksesi tallennettu vastaus.",
|
||||||
@ -53,17 +53,17 @@
|
|||||||
"SEND": "Lähetä",
|
"SEND": "Lähetä",
|
||||||
"CREATE": "Lisää muistiinpano",
|
"CREATE": "Lisää muistiinpano",
|
||||||
"TWEET": "Twiittaa",
|
"TWEET": "Twiittaa",
|
||||||
"TIP_FORMAT_ICON": "Show rich text editor",
|
"TIP_FORMAT_ICON": "Näytä rikas tekstieditori",
|
||||||
"TIP_EMOJI_ICON": "Show emoji selector",
|
"TIP_EMOJI_ICON": "Näytä emojivalitsin",
|
||||||
"TIP_ATTACH_ICON": "Attach files",
|
"TIP_ATTACH_ICON": "Liitä tiedosto",
|
||||||
"ENTER_TO_SEND": "Enter to send",
|
"ENTER_TO_SEND": "Paina Enter lähettääksesi",
|
||||||
"DRAG_DROP": "Drag and drop here to attach"
|
"DRAG_DROP": "Pudota tiedosto tähän liittääksesi"
|
||||||
},
|
},
|
||||||
"VISIBLE_TO_AGENTS": "Yksityinen huomautus: Näkyy vain sinulle ja tiimillesi",
|
"VISIBLE_TO_AGENTS": "Yksityinen huomautus: Näkyy vain sinulle ja tiimillesi",
|
||||||
"CHANGE_STATUS": "Keskustelun tila muutettu",
|
"CHANGE_STATUS": "Keskustelun tila muutettu",
|
||||||
"CHANGE_AGENT": "Keskustelun vastaanottaja vaihdettu",
|
"CHANGE_AGENT": "Keskustelun vastaanottaja vaihdettu",
|
||||||
"CHANGE_TEAM": "Conversation team changed",
|
"CHANGE_TEAM": "Keskustelun tiimi vaihdettu",
|
||||||
"FILE_SIZE_LIMIT": "File exceeds the {MAXIMUM_FILE_UPLOAD_SIZE} attachment limit",
|
"FILE_SIZE_LIMIT": "Tiedoston koko ylittää maksimin: {MAXIMUM_FILE_UPLOAD_SIZE} ",
|
||||||
"SENT_BY": "Lähettäjä:",
|
"SENT_BY": "Lähettäjä:",
|
||||||
"ASSIGNMENT": {
|
"ASSIGNMENT": {
|
||||||
"SELECT_AGENT": "Valitse edustaja",
|
"SELECT_AGENT": "Valitse edustaja",
|
||||||
@ -93,35 +93,35 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ONBOARDING": {
|
"ONBOARDING": {
|
||||||
"TITLE": "Hey 👋, Welcome to %{installationName}!",
|
"TITLE": "Hei 👋, tervetuloa %{installationName}!",
|
||||||
"DESCRIPTION": "Thanks for signing up. We want you to get the most out of %{installationName}. Here are a few things you can do in %{installationName} to make the experience delightful.",
|
"DESCRIPTION": "Thanks for signing up. We want you to get the most out of %{installationName}. Here are a few things you can do in %{installationName} to make the experience delightful.",
|
||||||
"READ_LATEST_UPDATES": "Read our latest updates",
|
"READ_LATEST_UPDATES": "Read our latest updates",
|
||||||
"ALL_CONVERSATION": {
|
"ALL_CONVERSATION": {
|
||||||
"TITLE": "All your conversations in one place",
|
"TITLE": "Kaikki keskustelut yhdessä paikassa",
|
||||||
"DESCRIPTION": "View all the conversations from your customers in one single dashboard. You can filter the conversations by the incoming channel, label and status."
|
"DESCRIPTION": "Kaikki asiakkaiden viestit yhdessä ohjauspaneelissa. Voit filtteröidä viestejä tunnisteen, kansion ja tiimin perusteella."
|
||||||
},
|
},
|
||||||
"TEAM_MEMBERS": {
|
"TEAM_MEMBERS": {
|
||||||
"TITLE": "Invite your team members",
|
"TITLE": "Kutsu tiimijäseniä",
|
||||||
"DESCRIPTION": "Since you are getting ready to talk to your customer, bring in your teammates to assist you. You can invite your teammates by adding their email address to the agent list.",
|
"DESCRIPTION": "Koska olet valmis palvelemaan asiakkaita, kutsu muutama tiimijäsentä auttamaan sinua!",
|
||||||
"NEW_LINK": "Click here to invite a team member"
|
"NEW_LINK": "Paina tästä kutsuaksesi tiimijäsen"
|
||||||
},
|
},
|
||||||
"INBOXES": {
|
"INBOXES": {
|
||||||
"TITLE": "Connect Inboxes",
|
"TITLE": "Yhdistä kansioita",
|
||||||
"DESCRIPTION": "Connect various channels through which your customers would be talking to you. It can be a website live-chat, your Facebook or Twitter page or even your WhatsApp number.",
|
"DESCRIPTION": "Yhdistä monta eri viestikanavaa jolla asiakkaat voivat ottaa sinun yhteyden. Esimerkiksi widgetti omille verkkosivuille, Twitterissä tai Whatsapissa.",
|
||||||
"NEW_LINK": "Click here to create an inbox"
|
"NEW_LINK": "Paina tästä luodaksesi kansio"
|
||||||
},
|
},
|
||||||
"LABELS": {
|
"LABELS": {
|
||||||
"TITLE": "Organize conversations with labels",
|
"TITLE": "Järjestä keskusteluja tunnisteilla",
|
||||||
"DESCRIPTION": "Labels provide an easier way to categorize your conversation. Create some labels like #support-enquiry, #billing-question etc., so that you can use them in a conversation later.",
|
"DESCRIPTION": "Tunnisteet auttavat sinua järjestelemään eri keskusteluja.",
|
||||||
"NEW_LINK": "Click here to create tags"
|
"NEW_LINK": "Paina tästä luodaksesi uusi tunniste"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"CONVERSATION_SIDEBAR": {
|
"CONVERSATION_SIDEBAR": {
|
||||||
"ASSIGNEE_LABEL": "Assigned Agent",
|
"ASSIGNEE_LABEL": "Määrätty edustaja",
|
||||||
"SELF_ASSIGN": "Assign to me",
|
"SELF_ASSIGN": "Määrätty minulle",
|
||||||
"TEAM_LABEL": "Assigned Team",
|
"TEAM_LABEL": "Määrätty tiimi",
|
||||||
"SELECT": {
|
"SELECT": {
|
||||||
"PLACEHOLDER": "None"
|
"PLACEHOLDER": "Ei mikään"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"CSAT": {
|
"CSAT": {
|
||||||
"TITLE": "Rate your conversation",
|
"TITLE": "Arvostele keskustelu",
|
||||||
"PLACEHOLDER": "Tell us more..."
|
"PLACEHOLDER": "Kerro meille lisää"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -43,7 +43,7 @@
|
|||||||
"CUSTOM_EMAIL_DOMAIN_ENABLED": "Voit nyt vastaanottaa sähköposteja mukautetulla verkkotunnuksellasi."
|
"CUSTOM_EMAIL_DOMAIN_ENABLED": "Voit nyt vastaanottaa sähköposteja mukautetulla verkkotunnuksellasi."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"UPDATE_CHATWOOT": "An update %{latestChatwootVersion} for Chatwoot is available. Please update your instance."
|
"UPDATE_CHATWOOT": "Uusi ChatWoot-päivitys on saatavilla. Uusin versio on %{latestChatwootVersion}"
|
||||||
},
|
},
|
||||||
"FORMS": {
|
"FORMS": {
|
||||||
"MULTISELECT": {
|
"MULTISELECT": {
|
||||||
@ -54,10 +54,10 @@
|
|||||||
},
|
},
|
||||||
"NOTIFICATIONS_PAGE": {
|
"NOTIFICATIONS_PAGE": {
|
||||||
"HEADER": "Ilmoitukset",
|
"HEADER": "Ilmoitukset",
|
||||||
"MARK_ALL_DONE": "Mark All Done",
|
"MARK_ALL_DONE": "Merkkaa kaikki luetuksi",
|
||||||
"LIST": {
|
"LIST": {
|
||||||
"LOADING_MESSAGE": "Loading notifications...",
|
"LOADING_MESSAGE": "Ladataan ilmoituksia...",
|
||||||
"404": "No Notifications",
|
"404": "Ei ilmoituksia",
|
||||||
"TABLE_HEADER": [
|
"TABLE_HEADER": [
|
||||||
"Nimi",
|
"Nimi",
|
||||||
"Puhelinnumero",
|
"Puhelinnumero",
|
||||||
@ -66,10 +66,10 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"TYPE_LABEL": {
|
"TYPE_LABEL": {
|
||||||
"conversation_creation": "New conversation",
|
"conversation_creation": "Uusi keskustelu",
|
||||||
"conversation_assignment": "Conversation Assigned",
|
"conversation_assignment": "Keskustelu määrätty",
|
||||||
"assigned_conversation_new_message": "New Message",
|
"assigned_conversation_new_message": "Uusi viesti",
|
||||||
"conversation_mention": "Mention"
|
"conversation_mention": "Mainitse"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"INBOX_MGMT": {
|
"INBOX_MGMT": {
|
||||||
"HEADER": "Saapuneet-kansiot",
|
"HEADER": "Saapuneet-kansiot",
|
||||||
"SIDEBAR_TXT": "<p><b>Postilaatikko</b></p> <p> Kun yhdistät sivuston tai facebook-sivun Chatwotiin, sitä kutsutaan <b>postilaatikoksi</b>. Sinulla voi olla rajoittamaton määrä postilaatikoita Chatwoot tililläsi. </p><p> Klikkaa <b>Lisää postilaatikko</b> yhdistääksesi verkkosivuston tai Facebook-sivun. </p><p> Kojelaudalla näet kaikki keskustelut kaikista saapuneet-kansiostasi yhdessä paikassa ja vastaat niihin `Keskustelut`-välilehdessä. </p><p> Voit myös nähdä postilaatikkoon liittyviä keskusteluja klikkaamalla postilaatikon nimeä kojelaudan vasemmassa paneelissa. </p>",
|
"SIDEBAR_TXT": "<p><b>Postilaatikko</b></p> <p> Kun yhdistät sivuston tai Facebook-sivun Chatwotiin, sitä kutsutaan <b>postilaatikoksi</b>. Sinulla voi olla rajoittamaton määrä postilaatikoita Chatwoot tililläsi. </p><p> Klikkaa <b>Lisää postilaatikko</b> yhdistääksesi verkkosivuston tai Facebook-sivun. </p><p> Kojelaudalla näet kaikki keskustelut kaikista saapuneet-kansiostasi yhdessä paikassa ja vastaat niihin `Keskustelut`-välilehdessä. </p><p> Voit myös nähdä postilaatikkoon liittyviä keskusteluja klikkaamalla postilaatikon nimeä kojelaudan vasemmassa paneelissa. </p>",
|
||||||
"LIST": {
|
"LIST": {
|
||||||
"404": "Tähän tiliin ei ole liitetty saapuneet-kansiota."
|
"404": "Tähän tiliin ei ole liitetty saapuneet-kansiota."
|
||||||
},
|
},
|
||||||
@ -30,7 +30,7 @@
|
|||||||
"ADD": {
|
"ADD": {
|
||||||
"CHANNEL_NAME": {
|
"CHANNEL_NAME": {
|
||||||
"LABEL": "Kansion nimi",
|
"LABEL": "Kansion nimi",
|
||||||
"PLACEHOLDER": "Enter your inbox name (eg: Acme Inc)"
|
"PLACEHOLDER": "Valitse kansion nimi (esim: Acme Oy)"
|
||||||
},
|
},
|
||||||
"WEBSITE_NAME": {
|
"WEBSITE_NAME": {
|
||||||
"LABEL": "Sivuston nimi",
|
"LABEL": "Sivuston nimi",
|
||||||
@ -47,7 +47,7 @@
|
|||||||
},
|
},
|
||||||
"TWITTER": {
|
"TWITTER": {
|
||||||
"HELP": "Lisätäksesi twitter-profiilin kanavaksesi, sinun tulee autentikoida twitter-tilisi klikkaamalla \"Kirjaudu sisään Twitterillä\" ",
|
"HELP": "Lisätäksesi twitter-profiilin kanavaksesi, sinun tulee autentikoida twitter-tilisi klikkaamalla \"Kirjaudu sisään Twitterillä\" ",
|
||||||
"ERROR_MESSAGE": "There was an error connecting to Twitter, please try again"
|
"ERROR_MESSAGE": "Twitteriin yhdistäessä tapahtui virhe"
|
||||||
},
|
},
|
||||||
"WEBSITE_CHANNEL": {
|
"WEBSITE_CHANNEL": {
|
||||||
"TITLE": "Sivuston chat",
|
"TITLE": "Sivuston chat",
|
||||||
@ -83,7 +83,7 @@
|
|||||||
"IN_A_FEW_MINUTES": "Muutamassa minuutissa",
|
"IN_A_FEW_MINUTES": "Muutamassa minuutissa",
|
||||||
"IN_A_FEW_HOURS": "Muutamassa tunnissa",
|
"IN_A_FEW_HOURS": "Muutamassa tunnissa",
|
||||||
"IN_A_DAY": "Päivän kuluessa",
|
"IN_A_DAY": "Päivän kuluessa",
|
||||||
"HELP_TEXT": "Vastausaika näytetään chat -widgetissä"
|
"HELP_TEXT": "Vastausaika näytetään chat-widgetissä"
|
||||||
},
|
},
|
||||||
"WIDGET_COLOR": {
|
"WIDGET_COLOR": {
|
||||||
"LABEL": "Widgetin väri",
|
"LABEL": "Widgetin väri",
|
||||||
@ -128,12 +128,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"SMS": {
|
"SMS": {
|
||||||
"TITLE": "SMS Channel via Twilio",
|
"TITLE": "SMS Twilio:n kautta",
|
||||||
"DESC": "Start supporting your customers via SMS with Twilio integration."
|
"DESC": "Ala palvelemaan asiakkaitasi tekstiviestien avulla käyttäen Twilio-alustaa"
|
||||||
},
|
},
|
||||||
"WHATSAPP": {
|
"WHATSAPP": {
|
||||||
"TITLE": "Whatsapp Channel via Twilio",
|
"TITLE": "Whatsapp Twilio:n kautta",
|
||||||
"DESC": "Start supporting your customers via Whatsapp with Twilio integration."
|
"DESC": "Ala palvelemaan asiakkaitasi Whatsapp-viestien avulla käyttäen Twilio-alustaa"
|
||||||
},
|
},
|
||||||
"API_CHANNEL": {
|
"API_CHANNEL": {
|
||||||
"TITLE": "API-rajapinta",
|
"TITLE": "API-rajapinta",
|
||||||
@ -173,8 +173,8 @@
|
|||||||
"FINISH_MESSAGE": "Aloita välittämällä sähköpostit seuraavaan osoitteeseen."
|
"FINISH_MESSAGE": "Aloita välittämällä sähköpostit seuraavaan osoitteeseen."
|
||||||
},
|
},
|
||||||
"AUTH": {
|
"AUTH": {
|
||||||
"TITLE": "Choose a channel",
|
"TITLE": "Valitse kanava",
|
||||||
"DESC": "Chatwoot supports live-chat widget, Facebook page, Twitter profile, Whatsapp, Email etc., as channels. If you want to build a custom channel, you can create it using the API channel. Select one channel from the options below to proceed."
|
"DESC": "Chatwoot tukee live-chat widgettiä, Facebook-sivua, Twitter-profiilia, Whatsappia, Sähköpostia jne., as channels. Voit luoda oman kanavan API-rajapinnalla."
|
||||||
},
|
},
|
||||||
"AGENTS": {
|
"AGENTS": {
|
||||||
"TITLE": "Edustajat",
|
"TITLE": "Edustajat",
|
||||||
@ -264,16 +264,16 @@
|
|||||||
"INBOX_AGENTS": "Edustajat",
|
"INBOX_AGENTS": "Edustajat",
|
||||||
"INBOX_AGENTS_SUB_TEXT": "Lisää tai poista edustajia tästä saapuneet-kansiosta",
|
"INBOX_AGENTS_SUB_TEXT": "Lisää tai poista edustajia tästä saapuneet-kansiosta",
|
||||||
"UPDATE": "Päivitä",
|
"UPDATE": "Päivitä",
|
||||||
"ENABLE_EMAIL_COLLECT_BOX": "Enable email collect box",
|
"ENABLE_EMAIL_COLLECT_BOX": "Aktivoi sähköpostilaatikko",
|
||||||
"ENABLE_EMAIL_COLLECT_BOX_SUB_TEXT": "Enable or disable email collect box on new conversation",
|
"ENABLE_EMAIL_COLLECT_BOX_SUB_TEXT": "Aktivoi tai disabloi sähköpostilaatikko uusissa keskusteluissa",
|
||||||
"AUTO_ASSIGNMENT": "Ota automaattinen delegointi käyttöön",
|
"AUTO_ASSIGNMENT": "Ota automaattinen delegointi käyttöön",
|
||||||
"ENABLE_CSAT": "Enable CSAT",
|
"ENABLE_CSAT": "Aktivoi ASTK",
|
||||||
"ENABLE_CSAT_SUB_TEXT": "Enable/Disable CSAT(Customer satisfaction) survey after resolving a conversation",
|
"ENABLE_CSAT_SUB_TEXT": "Aktivoi/Disabloi ASTK(ASiakasTyytyväisyysKysely) keskustelun ratkaisun jälkeen",
|
||||||
"INBOX_UPDATE_TITLE": "Postilaatikon tiedot",
|
"INBOX_UPDATE_TITLE": "Postilaatikon tiedot",
|
||||||
"INBOX_UPDATE_SUB_TEXT": "Päivitä postilaatikon asetukset",
|
"INBOX_UPDATE_SUB_TEXT": "Päivitä postilaatikon asetukset",
|
||||||
"AUTO_ASSIGNMENT_SUB_TEXT": "Ota käyttöön tai poista käytöstä automaattinen keskusteluiden delegointi edustajille.",
|
"AUTO_ASSIGNMENT_SUB_TEXT": "Ota käyttöön tai poista käytöstä automaattinen keskusteluiden delegointi edustajille.",
|
||||||
"HMAC_VERIFICATION": "User Identity Validation",
|
"HMAC_VERIFICATION": "Käyttäjän identiteettivarmistus",
|
||||||
"HMAC_DESCRIPTION": "Inorder validate the users identity, the SDK allows you to pass an `identity_hash` for each user. You can generate HMAC using 'sha256' with the key shown here."
|
"HMAC_DESCRIPTION": "Varmistaaksemme käyttäjän identiteetin, SDK sallii sinun antaa `identifier_hash` jokaiselle käyttäjälle. Voit generoida HMAC käyttäen 'sha256' avaimella joka on alla."
|
||||||
},
|
},
|
||||||
"FACEBOOK_REAUTHORIZE": {
|
"FACEBOOK_REAUTHORIZE": {
|
||||||
"TITLE": "Uudelleenvaltuuta",
|
"TITLE": "Uudelleenvaltuuta",
|
||||||
@ -282,38 +282,38 @@
|
|||||||
"MESSAGE_ERROR": "Tapahtui virhe, yritä uudelleen"
|
"MESSAGE_ERROR": "Tapahtui virhe, yritä uudelleen"
|
||||||
},
|
},
|
||||||
"PRE_CHAT_FORM": {
|
"PRE_CHAT_FORM": {
|
||||||
"DESCRIPTION": "Pre chat forms enable you to capture user information before they start conversation with you.",
|
"DESCRIPTION": "Pikakysely kysyy käyttäjän sähköpostin ja nimen ennen keskustelua",
|
||||||
"ENABLE": {
|
"ENABLE": {
|
||||||
"LABEL": "Enable pre chat form",
|
"LABEL": "Aktivoi pikakysely",
|
||||||
"OPTIONS": {
|
"OPTIONS": {
|
||||||
"ENABLED": "Yes",
|
"ENABLED": "Kyllä",
|
||||||
"DISABLED": "No"
|
"DISABLED": "Ei"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"PRE_CHAT_MESSAGE": {
|
"PRE_CHAT_MESSAGE": {
|
||||||
"LABEL": "Pre Chat Message",
|
"LABEL": "Pikakyselyviesti",
|
||||||
"PLACEHOLDER": "This message would be visible to the users along with the form"
|
"PLACEHOLDER": "Tämä viesti näytetään käyttäjille joille pikakysely näytetään"
|
||||||
},
|
},
|
||||||
"REQUIRE_EMAIL": {
|
"REQUIRE_EMAIL": {
|
||||||
"LABEL": "Visitors should provide their name and email address before starting the chat"
|
"LABEL": "Käyttäjien tulee antaa sähköpostiosoite ja nimi ennen keskustelun aloitusta"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"BUSINESS_HOURS": {
|
"BUSINESS_HOURS": {
|
||||||
"TITLE": "Set your availability",
|
"TITLE": "Aseta tilasi",
|
||||||
"SUBTITLE": "Set your availability on your livechat widget",
|
"SUBTITLE": "Aseta tilasi livechat widgetissä",
|
||||||
"WEEKLY_TITLE": "Set your weekly hours",
|
"WEEKLY_TITLE": "Aseta viikkotyöaikasi",
|
||||||
"TIMEZONE_LABEL": "Select timezone",
|
"TIMEZONE_LABEL": "Aseta aikavyöhyke",
|
||||||
"UPDATE": "Update business hours settings",
|
"UPDATE": "Aseta työaika",
|
||||||
"TOGGLE_AVAILABILITY": "Enable business availability for this inbox",
|
"TOGGLE_AVAILABILITY": "Aktivoi työaikasaatavuus tälle kansiolle",
|
||||||
"UNAVAILABLE_MESSAGE_LABEL": "Unavailable message for vistors",
|
"UNAVAILABLE_MESSAGE_LABEL": "Ei saatavilla-viesti käyttäjille",
|
||||||
"UNAVAILABLE_MESSAGE_DEFAULT": "We are unavailable at the moment. Leave a message we will respond once we are back.",
|
"UNAVAILABLE_MESSAGE_DEFAULT": "Emme ole saatavilla juuri nyt. Jätä viesti ja vastaamme heti kun pystymme.",
|
||||||
"TOGGLE_HELP": "Enabling business availability will show the available hours on live chat widget even if all the agents are offline. Outside available hours vistors can be warned with a message and a pre-chat form.",
|
"TOGGLE_HELP": "Aktivoimalla työaikasaatavuus livechat widgetissä näytetään olevanne saatavilla, vaikka kukaan edustaja ei olisi paikalla. Näiden aikojen ulkopuolella käyttäjille näytetään varoitus ja pikakysely.",
|
||||||
"DAY": {
|
"DAY": {
|
||||||
"ENABLE": "Enable availability for this day",
|
"ENABLE": "Aktivoi saatavuus tälle päivälle",
|
||||||
"UNAVAILABLE": "Unavailable",
|
"UNAVAILABLE": "Ei saatavilla",
|
||||||
"HOURS": "hours",
|
"HOURS": "tuntia",
|
||||||
"VALIDATION_ERROR": "Starting time should be before closing time.",
|
"VALIDATION_ERROR": "Avaamisajan tulisi olla ennen sulkemisaikaa.",
|
||||||
"CHOOSE": "Choose"
|
"CHOOSE": "Valitse"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,36 +1,36 @@
|
|||||||
{
|
{
|
||||||
"INTEGRATION_APPS": {
|
"INTEGRATION_APPS": {
|
||||||
"FETCHING": "Fetching Integrations",
|
"FETCHING": "Haetaan integraatioita",
|
||||||
"NO_HOOK_CONFIGURED": "There are no %{integrationId} integrations configured in this account.",
|
"NO_HOOK_CONFIGURED": "Integraatiota IDllä %{integrationId} ei löytynyt tai ei ole konfiguroitu.",
|
||||||
"HEADER": "Applications",
|
"HEADER": "Applikaatiot",
|
||||||
"STATUS": {
|
"STATUS": {
|
||||||
"ENABLED": "Käytössä",
|
"ENABLED": "Käytössä",
|
||||||
"DISABLED": "Pois käytöstä"
|
"DISABLED": "Pois käytöstä"
|
||||||
},
|
},
|
||||||
"CONFIGURE": "Määrittele",
|
"CONFIGURE": "Määrittele",
|
||||||
"ADD_BUTTON": "Add a new hook",
|
"ADD_BUTTON": "Lisää uusi webhook",
|
||||||
"DELETE": {
|
"DELETE": {
|
||||||
"TITLE": {
|
"TITLE": {
|
||||||
"INBOX": "Confirm deletion",
|
"INBOX": "Varmista poisto",
|
||||||
"ACCOUNT": "Disconnect"
|
"ACCOUNT": "Katkaise"
|
||||||
},
|
},
|
||||||
"MESSAGE": {
|
"MESSAGE": {
|
||||||
"INBOX": "Oletko varma että haluat poistaa?",
|
"INBOX": "Oletko varma että haluat poistaa?",
|
||||||
"ACCOUNT": "Are you sure to disconnect?"
|
"ACCOUNT": "Oletko varma että haluat katkaista?"
|
||||||
},
|
},
|
||||||
"CONFIRM_BUTTON_TEXT": {
|
"CONFIRM_BUTTON_TEXT": {
|
||||||
"INBOX": "Kyllä, poista",
|
"INBOX": "Kyllä, poista",
|
||||||
"ACCOUNT": "Yes, Disconnect"
|
"ACCOUNT": "Kyllä, katkaise"
|
||||||
},
|
},
|
||||||
"CANCEL_BUTTON_TEXT": "Peruuta",
|
"CANCEL_BUTTON_TEXT": "Peruuta",
|
||||||
"API": {
|
"API": {
|
||||||
"SUCCESS_MESSAGE": "Hook deleted successfully",
|
"SUCCESS_MESSAGE": "Webhook poistettu onnistuneesti",
|
||||||
"ERROR_MESSAGE": "Yhteyden muodostaminen Woot-palvelimelle ei onnistunut, yritä myöhemmin uudelleen"
|
"ERROR_MESSAGE": "Yhteyden muodostaminen Woot-palvelimelle ei onnistunut, yritä myöhemmin uudelleen"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"LIST": {
|
"LIST": {
|
||||||
"FETCHING": "Fetching integration hooks",
|
"FETCHING": "Haetaan integraatiowebhookkeja",
|
||||||
"INBOX": "Inbox",
|
"INBOX": "Kansio",
|
||||||
"DELETE": {
|
"DELETE": {
|
||||||
"BUTTON_TEXT": "Poista"
|
"BUTTON_TEXT": "Poista"
|
||||||
}
|
}
|
||||||
@ -38,14 +38,14 @@
|
|||||||
"ADD": {
|
"ADD": {
|
||||||
"FORM": {
|
"FORM": {
|
||||||
"INBOX": {
|
"INBOX": {
|
||||||
"LABEL": "Select Inbox",
|
"LABEL": "Valitse kansio",
|
||||||
"PLACEHOLDER": "Select Inbox"
|
"PLACEHOLDER": "Valitse kansio"
|
||||||
},
|
},
|
||||||
"SUBMIT": "Luo",
|
"SUBMIT": "Luo",
|
||||||
"CANCEL": "Peruuta"
|
"CANCEL": "Peruuta"
|
||||||
},
|
},
|
||||||
"API": {
|
"API": {
|
||||||
"SUCCESS_MESSAGE": "Integration hook added successfully",
|
"SUCCESS_MESSAGE": "Integraatio webhook lisätty",
|
||||||
"ERROR_MESSAGE": "Yhteyden muodostaminen Woot-palvelimelle ei onnistunut, yritä myöhemmin uudelleen"
|
"ERROR_MESSAGE": "Yhteyden muodostaminen Woot-palvelimelle ei onnistunut, yritä myöhemmin uudelleen"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -53,7 +53,7 @@
|
|||||||
"BUTTON_TEXT": "Yhdistä"
|
"BUTTON_TEXT": "Yhdistä"
|
||||||
},
|
},
|
||||||
"DISCONNECT": {
|
"DISCONNECT": {
|
||||||
"BUTTON_TEXT": "Disconnect"
|
"BUTTON_TEXT": "Katkaise"
|
||||||
},
|
},
|
||||||
"SIDEBAR_DESCRIPTION": {
|
"SIDEBAR_DESCRIPTION": {
|
||||||
"DIALOGFLOW": "Dialogflow is a natural language understanding platform that makes it easy to design and integrate a conversational user interface into your mobile app, web application, device, bot, interactive voice response system, and so on. <br /> <br /> Dialogflow integration with %{installationName} allows you to configure a Dialogflow bot with your inboxes which lets the bot handle the queries initially and hand them over to an agent when needed. Dialogflow can be used to qualifying the leads, reduce the workload of agents by providing frequently asked questions etc. <br /> <br /> To add Dialogflow, you need to create a Service Account in your Google project console and share the credentials. Please refer to the Dialogflow docs for more information."
|
"DIALOGFLOW": "Dialogflow is a natural language understanding platform that makes it easy to design and integrate a conversational user interface into your mobile app, web application, device, bot, interactive voice response system, and so on. <br /> <br /> Dialogflow integration with %{installationName} allows you to configure a Dialogflow bot with your inboxes which lets the bot handle the queries initially and hand them over to an agent when needed. Dialogflow can be used to qualifying the leads, reduce the workload of agents by providing frequently asked questions etc. <br /> <br /> To add Dialogflow, you need to create a Service Account in your Google project console and share the credentials. Please refer to the Dialogflow docs for more information."
|
||||||
|
|||||||
@ -10,7 +10,7 @@
|
|||||||
"PLACEHOLDER": "Salasana"
|
"PLACEHOLDER": "Salasana"
|
||||||
},
|
},
|
||||||
"API": {
|
"API": {
|
||||||
"SUCCESS_MESSAGE": "Kirjautuminen Onnistui",
|
"SUCCESS_MESSAGE": "Kirjautuminen onnistui",
|
||||||
"ERROR_MESSAGE": "Yhteyden muodostaminen Woot-palvelimelle ei onnistunut, yritä myöhemmin uudelleen",
|
"ERROR_MESSAGE": "Yhteyden muodostaminen Woot-palvelimelle ei onnistunut, yritä myöhemmin uudelleen",
|
||||||
"UNAUTH": "Käyttäjätunnus / salasana virheellinen. Yritä uudelleen"
|
"UNAUTH": "Käyttäjätunnus / salasana virheellinen. Yritä uudelleen"
|
||||||
},
|
},
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user