Merge branch 'release/1.19.0'
This commit is contained in:
@@ -7,7 +7,7 @@ defaults: &defaults
|
||||
working_directory: ~/build
|
||||
docker:
|
||||
# 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
|
||||
# CircleCI maintains a library of pre-built images
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# pre-build stage
|
||||
ARG VARIANT=2.7
|
||||
ARG VARIANT=3
|
||||
FROM mcr.microsoft.com/vscode/devcontainers/ruby:${VARIANT}
|
||||
|
||||
# Update args in docker-compose.yaml to set the UID/GID of the "vscode" user.
|
||||
|
||||
@@ -10,8 +10,8 @@ services:
|
||||
context: ..
|
||||
dockerfile: .devcontainer/Dockerfile
|
||||
args:
|
||||
# Update 'VARIANT' to pick a Ruby version: 2, 2.7, 2.6, 2.5
|
||||
VARIANT: 2.7
|
||||
# Update 'VARIANT' to pick a Ruby version: https://github.com/microsoft/vscode-dev-containers/tree/main/containers/ruby
|
||||
VARIANT: 3
|
||||
# [Choice] Install Node.js
|
||||
INSTALL_NODE: "true"
|
||||
NODE_VERSION: "lts/*"
|
||||
|
||||
@@ -147,6 +147,10 @@ USE_INBOX_AVATAR_FOR_BOT=true
|
||||
# maxmindb api key to use geoip2 service
|
||||
# IP_LOOKUP_API_KEY=
|
||||
|
||||
## Rack Attack configuration
|
||||
## To prevent and throttle abusive requests
|
||||
# ENABLE_RACK_ATTACK=false
|
||||
|
||||
|
||||
## Running chatwoot as an API only server
|
||||
## setting this value to true will disable the frontend dashboard endpoints
|
||||
@@ -157,3 +161,7 @@ USE_INBOX_AVATAR_FOR_BOT=true
|
||||
# LETTER_OPENER=true
|
||||
# meant to be used in github codespaces
|
||||
# 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
|
||||
|
||||
41
.rubocop.yml
41
.rubocop.yml
@@ -13,6 +13,7 @@ Metrics/ClassLength:
|
||||
- 'app/models/conversation.rb'
|
||||
- 'app/mailers/conversation_reply_mailer.rb'
|
||||
- 'app/models/message.rb'
|
||||
- 'app/builders/messages/facebook/message_builder.rb'
|
||||
RSpec/ExampleLength:
|
||||
Max: 25
|
||||
Style/Documentation:
|
||||
@@ -24,12 +25,13 @@ Style/FrozenStringLiteralComment:
|
||||
Style/SymbolArray:
|
||||
Enabled: false
|
||||
Style/OptionalBooleanParameter:
|
||||
Exclude:
|
||||
Exclude:
|
||||
- 'app/services/email_templates/db_resolver_service.rb'
|
||||
- 'app/dispatchers/dispatcher.rb'
|
||||
Style/GlobalVars:
|
||||
Exclude:
|
||||
- 'config/initializers/redis.rb'
|
||||
- 'config/initializers/01_redis.rb'
|
||||
- 'config/initializers/rack_attack.rb'
|
||||
- 'lib/redis/alfred.rb'
|
||||
- 'lib/global_config.rb'
|
||||
Style/ClassVars:
|
||||
@@ -38,6 +40,14 @@ Style/ClassVars:
|
||||
Lint/MissingSuper:
|
||||
Exclude:
|
||||
- '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:
|
||||
Exclude:
|
||||
- spec/**/*
|
||||
@@ -55,6 +65,11 @@ Rails/ApplicationController:
|
||||
- 'app/controllers/widgets_controller.rb'
|
||||
- 'app/controllers/platform_controller.rb'
|
||||
- 'app/controllers/public_controller.rb'
|
||||
- 'app/controllers/survey/responses_controller.rb'
|
||||
Rails/EnvironmentVariableAccess:
|
||||
Enabled: false
|
||||
Rails/TimeZoneAssignment:
|
||||
Enabled: false
|
||||
Style/ClassAndModuleChildren:
|
||||
EnforcedStyle: compact
|
||||
Exclude:
|
||||
@@ -64,6 +79,10 @@ RSpec/NestedGroups:
|
||||
Max: 4
|
||||
RSpec/MessageSpies:
|
||||
Enabled: false
|
||||
RSpec/StubbedMock:
|
||||
Enabled: false
|
||||
Naming/VariableNumber:
|
||||
Enabled: false
|
||||
Metrics/MethodLength:
|
||||
Exclude:
|
||||
- 'db/migrate/20161123131628_devise_token_auth_create_users.rb'
|
||||
@@ -77,7 +96,7 @@ Style/GuardClause:
|
||||
- 'app/models/message.rb'
|
||||
- 'db/migrate/20190819005836_add_missing_indexes_on_taggings.acts_as_taggable_on_engine.rb'
|
||||
Metrics/AbcSize:
|
||||
Exclude:
|
||||
Exclude:
|
||||
- 'app/controllers/concerns/auth_helper.rb'
|
||||
- 'db/migrate/20190819005836_add_missing_indexes_on_taggings.acts_as_taggable_on_engine.rb'
|
||||
- 'db/migrate/20161123131628_devise_token_auth_create_users.rb'
|
||||
@@ -106,19 +125,24 @@ Rails/BulkChangeTable:
|
||||
- 'db/migrate/20191027054756_create_contact_inboxes.rb'
|
||||
- 'db/migrate/20191130164019_add_template_type_to_messages.rb'
|
||||
- 'db/migrate/20210425093724_convert_integration_hook_settings_field.rb'
|
||||
Rails/UniqueValidationWithoutIndex:
|
||||
Rails/UniqueValidationWithoutIndex:
|
||||
Exclude:
|
||||
- 'app/models/channel/twitter_profile.rb'
|
||||
- 'app/models/webhook.rb'
|
||||
- 'app/models/contact.rb'
|
||||
- 'app/models/integrations/hook.rb'
|
||||
Rails/RenderInline:
|
||||
Exclude:
|
||||
- 'app/controllers/swagger_controller.rb'
|
||||
Performance/CollectionLiteralInLoop:
|
||||
Exclude:
|
||||
- 'db/migrate/20210315101919_enable_email_channel.rb'
|
||||
RSpec/NamedSubject:
|
||||
Enabled: false
|
||||
# we should bring this down
|
||||
RSpec/MultipleMemoizedHelpers:
|
||||
Max: 12
|
||||
|
||||
AllCops:
|
||||
NewCops: enable
|
||||
Exclude:
|
||||
@@ -133,4 +157,11 @@ AllCops:
|
||||
- 'tmp/**/*'
|
||||
- 'storage/**/*'
|
||||
- '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'
|
||||
|
||||
ruby '2.7.3'
|
||||
ruby '3.0.2'
|
||||
|
||||
##-- base gems for rails --##
|
||||
gem 'rack-cors', require: 'rack/cors'
|
||||
@@ -33,19 +33,22 @@ gem 'liquid'
|
||||
gem 'commonmarker'
|
||||
# Validate Data against JSON Schema
|
||||
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 --##
|
||||
gem 'aws-sdk-s3', require: false
|
||||
gem 'azure-storage-blob', require: false
|
||||
gem 'google-cloud-storage', require: false
|
||||
gem 'mini_magick'
|
||||
gem 'image_processing'
|
||||
|
||||
##-- gems for database --#
|
||||
gem 'groupdate'
|
||||
gem 'pg'
|
||||
gem 'redis'
|
||||
gem 'redis-namespace'
|
||||
gem 'redis-rack-cache'
|
||||
# super fast record imports in bulk
|
||||
gem 'activerecord-import'
|
||||
|
||||
@@ -90,8 +93,11 @@ gem 'google-cloud-dialogflow'
|
||||
##--- gems for debugging and error reporting ---##
|
||||
# static analysis
|
||||
gem 'brakeman'
|
||||
gem 'ddtrace'
|
||||
gem 'scout_apm'
|
||||
gem 'sentry-raven'
|
||||
gem 'sentry-rails'
|
||||
gem 'sentry-ruby'
|
||||
gem 'sentry-sidekiq'
|
||||
|
||||
##-- background job processing --##
|
||||
gem 'sidekiq'
|
||||
@@ -120,7 +126,7 @@ group :development do
|
||||
gem 'web-console'
|
||||
|
||||
# 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
|
||||
gem 'squasher'
|
||||
@@ -134,19 +140,19 @@ group :test do
|
||||
end
|
||||
|
||||
group :development, :test do
|
||||
gem 'active_record_query_trace'
|
||||
gem 'bundle-audit', require: false
|
||||
gem 'byebug', platform: :mri
|
||||
gem 'factory_bot_rails'
|
||||
gem 'faker'
|
||||
gem 'listen'
|
||||
gem 'mock_redis', git: 'https://github.com/sds/mock_redis', ref: '16d00789f0341a3aac35126c0ffe97a596753ff9'
|
||||
gem 'mock_redis'
|
||||
gem 'pry-rails'
|
||||
gem 'rspec-rails', '~> 4.0.0.beta2'
|
||||
gem 'rspec-rails', '~> 5.0.0'
|
||||
gem 'rubocop', require: false
|
||||
gem 'rubocop-performance', require: false
|
||||
gem 'rubocop-rails', require: false
|
||||
gem 'rubocop-rspec', require: false
|
||||
gem 'scss_lint', require: false
|
||||
gem 'seed_dump'
|
||||
gem 'shoulda-matchers'
|
||||
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
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
actioncable (6.0.3.7)
|
||||
actionpack (= 6.0.3.7)
|
||||
actioncable (6.1.4)
|
||||
actionpack (= 6.1.4)
|
||||
activesupport (= 6.1.4)
|
||||
nio4r (~> 2.0)
|
||||
websocket-driver (>= 0.6.1)
|
||||
actionmailbox (6.0.3.7)
|
||||
actionpack (= 6.0.3.7)
|
||||
activejob (= 6.0.3.7)
|
||||
activerecord (= 6.0.3.7)
|
||||
activestorage (= 6.0.3.7)
|
||||
activesupport (= 6.0.3.7)
|
||||
actionmailbox (6.1.4)
|
||||
actionpack (= 6.1.4)
|
||||
activejob (= 6.1.4)
|
||||
activerecord (= 6.1.4)
|
||||
activestorage (= 6.1.4)
|
||||
activesupport (= 6.1.4)
|
||||
mail (>= 2.7.1)
|
||||
actionmailer (6.0.3.7)
|
||||
actionpack (= 6.0.3.7)
|
||||
actionview (= 6.0.3.7)
|
||||
activejob (= 6.0.3.7)
|
||||
actionmailer (6.1.4)
|
||||
actionpack (= 6.1.4)
|
||||
actionview (= 6.1.4)
|
||||
activejob (= 6.1.4)
|
||||
activesupport (= 6.1.4)
|
||||
mail (~> 2.5, >= 2.5.4)
|
||||
rails-dom-testing (~> 2.0)
|
||||
actionpack (6.0.3.7)
|
||||
actionview (= 6.0.3.7)
|
||||
activesupport (= 6.0.3.7)
|
||||
rack (~> 2.0, >= 2.0.8)
|
||||
actionpack (6.1.4)
|
||||
actionview (= 6.1.4)
|
||||
activesupport (= 6.1.4)
|
||||
rack (~> 2.0, >= 2.0.9)
|
||||
rack-test (>= 0.6.3)
|
||||
rails-dom-testing (~> 2.0)
|
||||
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
||||
actiontext (6.0.3.7)
|
||||
actionpack (= 6.0.3.7)
|
||||
activerecord (= 6.0.3.7)
|
||||
activestorage (= 6.0.3.7)
|
||||
activesupport (= 6.0.3.7)
|
||||
actiontext (6.1.4)
|
||||
actionpack (= 6.1.4)
|
||||
activerecord (= 6.1.4)
|
||||
activestorage (= 6.1.4)
|
||||
activesupport (= 6.1.4)
|
||||
nokogiri (>= 1.8.5)
|
||||
actionview (6.0.3.7)
|
||||
activesupport (= 6.0.3.7)
|
||||
actionview (6.1.4)
|
||||
activesupport (= 6.1.4)
|
||||
builder (~> 3.1)
|
||||
erubi (~> 1.4)
|
||||
rails-dom-testing (~> 2.0)
|
||||
rails-html-sanitizer (~> 1.1, >= 1.2.0)
|
||||
activejob (6.0.3.7)
|
||||
activesupport (= 6.0.3.7)
|
||||
active_record_query_trace (1.8)
|
||||
activejob (6.1.4)
|
||||
activesupport (= 6.1.4)
|
||||
globalid (>= 0.3.6)
|
||||
activemodel (6.0.3.7)
|
||||
activesupport (= 6.0.3.7)
|
||||
activerecord (6.0.3.7)
|
||||
activemodel (= 6.0.3.7)
|
||||
activesupport (= 6.0.3.7)
|
||||
activerecord-import (1.0.7)
|
||||
activemodel (6.1.4)
|
||||
activesupport (= 6.1.4)
|
||||
activerecord (6.1.4)
|
||||
activemodel (= 6.1.4)
|
||||
activesupport (= 6.1.4)
|
||||
activerecord-import (1.2.0)
|
||||
activerecord (>= 3.2)
|
||||
activestorage (6.0.3.7)
|
||||
actionpack (= 6.0.3.7)
|
||||
activejob (= 6.0.3.7)
|
||||
activerecord (= 6.0.3.7)
|
||||
activestorage (6.1.4)
|
||||
actionpack (= 6.1.4)
|
||||
activejob (= 6.1.4)
|
||||
activerecord (= 6.1.4)
|
||||
activesupport (= 6.1.4)
|
||||
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)
|
||||
i18n (>= 0.7, < 2)
|
||||
minitest (~> 5.1)
|
||||
tzinfo (~> 1.1)
|
||||
zeitwerk (~> 2.2, >= 2.2.2)
|
||||
acts-as-taggable-on (6.5.0)
|
||||
activerecord (>= 5.0, < 6.1)
|
||||
i18n (>= 1.6, < 2)
|
||||
minitest (>= 5.1)
|
||||
tzinfo (~> 2.0)
|
||||
zeitwerk (~> 2.3)
|
||||
acts-as-taggable-on (8.1.0)
|
||||
activerecord (>= 5.0, < 6.2)
|
||||
addressable (2.8.0)
|
||||
public_suffix (>= 2.0.2, < 5.0)
|
||||
administrate (0.16.0)
|
||||
@@ -90,23 +80,23 @@ GEM
|
||||
annotate (3.1.1)
|
||||
activerecord (>= 3.2, < 7.0)
|
||||
rake (>= 10.4, < 14.0)
|
||||
ast (2.4.1)
|
||||
ast (2.4.2)
|
||||
attr_extras (6.2.4)
|
||||
aws-eventstream (1.1.0)
|
||||
aws-partitions (1.360.0)
|
||||
aws-sdk-core (3.105.0)
|
||||
aws-eventstream (1.1.1)
|
||||
aws-partitions (1.482.0)
|
||||
aws-sdk-core (3.119.0)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
aws-partitions (~> 1, >= 1.239.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
jmespath (~> 1.0)
|
||||
aws-sdk-kms (1.37.0)
|
||||
aws-sdk-core (~> 3, >= 3.99.0)
|
||||
aws-sdk-kms (1.46.0)
|
||||
aws-sdk-core (~> 3, >= 3.119.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-sdk-s3 (1.79.1)
|
||||
aws-sdk-core (~> 3, >= 3.104.3)
|
||||
aws-sdk-s3 (1.98.0)
|
||||
aws-sdk-core (~> 3, >= 3.119.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-sigv4 (1.2.2)
|
||||
aws-sigv4 (1.2.4)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
axiom-types (0.1.1)
|
||||
descendants_tracker (~> 0.0.4)
|
||||
@@ -120,42 +110,48 @@ GEM
|
||||
faraday_middleware (~> 1.0.0.rc1)
|
||||
net-http-persistent (~> 4.0)
|
||||
nokogiri (~> 1.11.0.rc2)
|
||||
barnes (0.0.8)
|
||||
barnes (0.0.9)
|
||||
multi_json (~> 1)
|
||||
statsd-ruby (~> 1.1)
|
||||
bcrypt (3.1.16)
|
||||
bindex (0.8.1)
|
||||
bootsnap (1.4.8)
|
||||
bootsnap (1.7.6)
|
||||
msgpack (~> 1.0)
|
||||
brakeman (4.9.0)
|
||||
brakeman (5.1.1)
|
||||
browser (5.3.1)
|
||||
builder (3.2.4)
|
||||
bullet (6.1.0)
|
||||
bullet (6.1.4)
|
||||
activesupport (>= 3.0.0)
|
||||
uniform_notifier (~> 1.11)
|
||||
bundle-audit (0.1.0)
|
||||
bundler-audit
|
||||
bundler-audit (0.7.0.1)
|
||||
bundler-audit (0.8.0)
|
||||
bundler (>= 1.2.0, < 3)
|
||||
thor (>= 0.18, < 2)
|
||||
thor (~> 1.0)
|
||||
byebug (11.1.3)
|
||||
coderay (1.1.3)
|
||||
coercible (1.0.0)
|
||||
descendants_tracker (~> 0.0.1)
|
||||
commonmarker (0.21.1)
|
||||
ruby-enum (~> 0.5)
|
||||
concurrent-ruby (1.1.8)
|
||||
connection_pool (2.2.3)
|
||||
crack (0.4.3)
|
||||
safe_yaml (~> 1.0.0)
|
||||
commonmarker (0.22.0)
|
||||
concurrent-ruby (1.1.9)
|
||||
connection_pool (2.2.5)
|
||||
crack (0.4.5)
|
||||
rexml
|
||||
crass (1.0.6)
|
||||
cypress-on-rails (1.8.0)
|
||||
cypress-on-rails (1.10.1)
|
||||
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)
|
||||
momentjs-rails (>= 2.8.1)
|
||||
ddtrace (0.51.1)
|
||||
ffi (~> 1.0)
|
||||
msgpack
|
||||
declarative (0.0.20)
|
||||
declarative-option (0.1.0)
|
||||
descendants_tracker (0.0.4)
|
||||
thread_safe (~> 0.3, >= 0.3.1)
|
||||
devise (4.8.0)
|
||||
@@ -167,48 +163,49 @@ GEM
|
||||
devise-secure_password (2.0.1)
|
||||
devise (>= 4.0.0, < 5.0.0)
|
||||
railties (>= 5.0.0, < 7.0.0)
|
||||
devise_token_auth (1.1.4)
|
||||
devise_token_auth (1.2.0)
|
||||
bcrypt (~> 3.0)
|
||||
devise (> 3.5.2, < 5)
|
||||
rails (>= 4.2.0, < 6.1)
|
||||
sprockets (= 3.7.2)
|
||||
rails (>= 4.2.0, < 6.2)
|
||||
diff-lcs (1.4.4)
|
||||
digest-crc (0.6.1)
|
||||
rake (~> 13.0)
|
||||
docile (1.3.2)
|
||||
digest-crc (0.6.4)
|
||||
rake (>= 12.0.0, < 14.0.0)
|
||||
docile (1.4.0)
|
||||
domain_name (0.5.20190701)
|
||||
unf (>= 0.0.5, < 1.0.0)
|
||||
dotenv (2.7.6)
|
||||
dotenv-rails (2.7.6)
|
||||
dotenv (= 2.7.6)
|
||||
railties (>= 3.2)
|
||||
ecma-re-validator (0.2.1)
|
||||
regexp_parser (~> 1.2)
|
||||
equalizer (0.0.11)
|
||||
down (5.2.3)
|
||||
addressable (~> 2.8)
|
||||
dry-inflector (0.2.1)
|
||||
ecma-re-validator (0.3.0)
|
||||
regexp_parser (~> 2.0)
|
||||
erubi (1.10.0)
|
||||
et-orbi (1.2.4)
|
||||
tzinfo
|
||||
execjs (2.7.0)
|
||||
execjs (2.8.1)
|
||||
facebook-messenger (2.0.1)
|
||||
httparty (~> 0.13, >= 0.13.7)
|
||||
rack (>= 1.4.5)
|
||||
factory_bot (6.1.0)
|
||||
factory_bot (6.2.0)
|
||||
activesupport (>= 5.0.0)
|
||||
factory_bot_rails (6.1.0)
|
||||
factory_bot (~> 6.1.0)
|
||||
factory_bot_rails (6.2.0)
|
||||
factory_bot (~> 6.2.0)
|
||||
railties (>= 5.0.0)
|
||||
faker (2.13.0)
|
||||
faker (2.18.0)
|
||||
i18n (>= 1.6, < 2)
|
||||
faraday (1.0.1)
|
||||
multipart-post (>= 1.2, < 3)
|
||||
faraday_middleware (1.0.0)
|
||||
faraday (~> 1.0)
|
||||
fcm (1.0.2)
|
||||
faraday (~> 1.0.0)
|
||||
ffi (1.15.0)
|
||||
fcm (1.0.3)
|
||||
faraday (~> 1)
|
||||
ffi (1.15.3)
|
||||
flag_shih_tzu (0.3.23)
|
||||
foreman (0.87.2)
|
||||
fugit (1.4.1)
|
||||
fugit (1.5.0)
|
||||
et-orbi (~> 1.1, >= 1.1.8)
|
||||
raabro (~> 1.4)
|
||||
gapic-common (0.3.4)
|
||||
@@ -217,18 +214,23 @@ GEM
|
||||
googleapis-common-protos-types (>= 1.0.4, < 2.0)
|
||||
googleauth (~> 0.9)
|
||||
grpc (~> 1.25)
|
||||
geocoder (1.6.3)
|
||||
gli (2.19.2)
|
||||
globalid (0.4.2)
|
||||
activesupport (>= 4.2.0)
|
||||
google-api-client (0.43.0)
|
||||
geocoder (1.6.7)
|
||||
gli (2.20.1)
|
||||
globalid (0.5.1)
|
||||
activesupport (>= 5.0)
|
||||
google-apis-core (0.4.1)
|
||||
addressable (~> 2.5, >= 2.5.1)
|
||||
googleauth (~> 0.9)
|
||||
httpclient (>= 2.8.1, < 3.0)
|
||||
googleauth (>= 0.16.2, < 2.a)
|
||||
httpclient (>= 2.8.1, < 3.a)
|
||||
mini_mime (~> 1.0)
|
||||
representable (~> 3.0)
|
||||
retriable (>= 2.0, < 4.0)
|
||||
signet (~> 0.12)
|
||||
retriable (>= 2.0, < 4.a)
|
||||
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-env (~> 1.0)
|
||||
google-cloud-errors (~> 1.0)
|
||||
@@ -241,21 +243,23 @@ GEM
|
||||
google-cloud-env (1.5.0)
|
||||
faraday (>= 0.17.3, < 2.0)
|
||||
google-cloud-errors (1.1.0)
|
||||
google-cloud-storage (1.28.0)
|
||||
google-cloud-storage (1.34.1)
|
||||
addressable (~> 2.5)
|
||||
digest-crc (~> 0.4)
|
||||
google-api-client (~> 0.33)
|
||||
google-cloud-core (~> 1.2)
|
||||
googleauth (~> 0.9)
|
||||
google-apis-iamcredentials_v1 (~> 0.1)
|
||||
google-apis-storage_v1 (~> 0.1)
|
||||
google-cloud-core (~> 1.6)
|
||||
googleauth (>= 0.16.2, < 2.a)
|
||||
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)
|
||||
google-protobuf (~> 3.14)
|
||||
googleapis-common-protos-types (>= 1.0.6, < 2.0)
|
||||
grpc (~> 1.27)
|
||||
googleapis-common-protos-types (1.0.6)
|
||||
googleapis-common-protos-types (1.1.0)
|
||||
google-protobuf (~> 3.14)
|
||||
googleauth (0.16.2)
|
||||
googleauth (0.17.0)
|
||||
faraday (>= 0.17.3, < 2.0)
|
||||
jwt (>= 1.4, < 3.0)
|
||||
memoist (~> 0.16)
|
||||
@@ -264,11 +268,14 @@ GEM
|
||||
signet (~> 0.14)
|
||||
groupdate (5.2.2)
|
||||
activesupport (>= 5)
|
||||
grpc (1.37.1)
|
||||
grpc (1.38.0-universal-darwin)
|
||||
google-protobuf (~> 3.15)
|
||||
googleapis-common-protos-types (~> 1.0)
|
||||
haikunator (1.1.0)
|
||||
hairtrigger (0.2.23)
|
||||
grpc (1.38.0-x86_64-linux)
|
||||
google-protobuf (~> 3.15)
|
||||
googleapis-common-protos-types (~> 1.0)
|
||||
haikunator (1.1.1)
|
||||
hairtrigger (0.2.24)
|
||||
activerecord (>= 5.0, < 7)
|
||||
ruby2ruby (~> 2.4)
|
||||
ruby_parser (~> 3.10)
|
||||
@@ -277,7 +284,7 @@ GEM
|
||||
hashie (4.1.0)
|
||||
hkdf (0.3.0)
|
||||
http-accept (1.7.0)
|
||||
http-cookie (1.0.3)
|
||||
http-cookie (1.0.4)
|
||||
domain_name (~> 0.5)
|
||||
httparty (0.18.1)
|
||||
mime-types (~> 3.0)
|
||||
@@ -286,19 +293,23 @@ GEM
|
||||
i18n (1.8.10)
|
||||
concurrent-ruby (~> 1.0)
|
||||
ice_nine (0.11.2)
|
||||
inflecto (0.0.2)
|
||||
jbuilder (2.10.0)
|
||||
image_processing (1.12.1)
|
||||
mini_magick (>= 4.9.5, < 5)
|
||||
ruby-vips (>= 2.0.17, < 3)
|
||||
jbuilder (2.11.2)
|
||||
activesupport (>= 5.0.0)
|
||||
jmespath (1.4.0)
|
||||
jquery-rails (4.4.0)
|
||||
rails-dom-testing (>= 1, < 3)
|
||||
railties (>= 4.2.0)
|
||||
thor (>= 0.14, < 2.0)
|
||||
json (2.3.1)
|
||||
json_schemer (0.2.16)
|
||||
ecma-re-validator (~> 0.2)
|
||||
json (2.5.1)
|
||||
json_refs (0.1.6)
|
||||
hana
|
||||
json_schemer (0.2.18)
|
||||
ecma-re-validator (~> 0.3)
|
||||
hana (~> 1.3)
|
||||
regexp_parser (~> 1.5)
|
||||
regexp_parser (~> 2.0)
|
||||
uri_template (~> 0.7)
|
||||
jwt (2.2.3)
|
||||
kaminari (1.2.1)
|
||||
@@ -321,11 +332,11 @@ GEM
|
||||
addressable (~> 2.7)
|
||||
letter_opener (1.7.0)
|
||||
launchy (~> 2.2)
|
||||
liquid (4.0.3)
|
||||
listen (3.3.3)
|
||||
liquid (5.0.1)
|
||||
listen (3.6.0)
|
||||
rb-fsevent (~> 0.10, >= 0.10.3)
|
||||
rb-inotify (~> 0.9, >= 0.9.10)
|
||||
loofah (2.9.1)
|
||||
loofah (2.11.0)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.5.9)
|
||||
mail (2.7.1)
|
||||
@@ -336,98 +347,97 @@ GEM
|
||||
method_source (1.0.0)
|
||||
mime-types (3.3.1)
|
||||
mime-types-data (~> 3.2015)
|
||||
mime-types-data (3.2021.0225)
|
||||
mini_magick (4.10.1)
|
||||
mime-types-data (3.2021.0704)
|
||||
mini_magick (4.11.0)
|
||||
mini_mime (1.1.0)
|
||||
mini_portile2 (2.5.1)
|
||||
minitest (5.14.4)
|
||||
mock_redis (0.28.0)
|
||||
ruby2_keywords
|
||||
momentjs-rails (2.20.1)
|
||||
railties (>= 3.1)
|
||||
msgpack (1.3.3)
|
||||
msgpack (1.4.2)
|
||||
multi_json (1.15.0)
|
||||
multi_xml (0.6.0)
|
||||
multipart-post (2.1.1)
|
||||
net-http-persistent (4.0.0)
|
||||
net-http-persistent (4.0.1)
|
||||
connection_pool (~> 2.2)
|
||||
netrc (0.11.0)
|
||||
nio4r (2.5.7)
|
||||
nokogiri (1.11.6)
|
||||
mini_portile2 (~> 2.5.0)
|
||||
nokogiri (1.11.7-arm64-darwin)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.11.7-x86_64-darwin)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.11.7-x86_64-linux)
|
||||
racc (~> 1.4)
|
||||
oauth (0.5.6)
|
||||
orm_adapter (0.5.0)
|
||||
os (1.1.1)
|
||||
parallel (1.19.2)
|
||||
parser (2.7.1.4)
|
||||
parallel (1.20.1)
|
||||
parser (3.0.2.0)
|
||||
ast (~> 2.4.1)
|
||||
pg (1.2.3)
|
||||
procore-sift (0.15.0)
|
||||
procore-sift (0.16.0)
|
||||
rails (> 4.2.0)
|
||||
pry (0.13.1)
|
||||
pry (0.14.1)
|
||||
coderay (~> 1.1)
|
||||
method_source (~> 1.0)
|
||||
pry-rails (0.3.9)
|
||||
pry (>= 0.10.4)
|
||||
public_suffix (4.0.6)
|
||||
puma (4.3.8)
|
||||
puma (5.4.0)
|
||||
nio4r (~> 2.0)
|
||||
pundit (2.1.0)
|
||||
activesupport (>= 3.0.0)
|
||||
raabro (1.4.0)
|
||||
racc (1.5.2)
|
||||
rack (2.2.3)
|
||||
rack-cache (1.12.0)
|
||||
rack (>= 0.4)
|
||||
rack-attack (6.5.0)
|
||||
rack (>= 1.0, < 3)
|
||||
rack-cors (1.1.1)
|
||||
rack (>= 2.0.0)
|
||||
rack-proxy (0.6.5)
|
||||
rack-proxy (0.7.0)
|
||||
rack
|
||||
rack-test (1.1.0)
|
||||
rack (>= 1.0, < 3)
|
||||
rack-timeout (0.6.0)
|
||||
rails (6.0.3.7)
|
||||
actioncable (= 6.0.3.7)
|
||||
actionmailbox (= 6.0.3.7)
|
||||
actionmailer (= 6.0.3.7)
|
||||
actionpack (= 6.0.3.7)
|
||||
actiontext (= 6.0.3.7)
|
||||
actionview (= 6.0.3.7)
|
||||
activejob (= 6.0.3.7)
|
||||
activemodel (= 6.0.3.7)
|
||||
activerecord (= 6.0.3.7)
|
||||
activestorage (= 6.0.3.7)
|
||||
activesupport (= 6.0.3.7)
|
||||
bundler (>= 1.3.0)
|
||||
railties (= 6.0.3.7)
|
||||
rails (6.1.4)
|
||||
actioncable (= 6.1.4)
|
||||
actionmailbox (= 6.1.4)
|
||||
actionmailer (= 6.1.4)
|
||||
actionpack (= 6.1.4)
|
||||
actiontext (= 6.1.4)
|
||||
actionview (= 6.1.4)
|
||||
activejob (= 6.1.4)
|
||||
activemodel (= 6.1.4)
|
||||
activerecord (= 6.1.4)
|
||||
activestorage (= 6.1.4)
|
||||
activesupport (= 6.1.4)
|
||||
bundler (>= 1.15.0)
|
||||
railties (= 6.1.4)
|
||||
sprockets-rails (>= 2.0.0)
|
||||
rails-dom-testing (2.0.3)
|
||||
activesupport (>= 4.2.0)
|
||||
nokogiri (>= 1.6)
|
||||
rails-html-sanitizer (1.3.0)
|
||||
loofah (~> 2.3)
|
||||
railties (6.0.3.7)
|
||||
actionpack (= 6.0.3.7)
|
||||
activesupport (= 6.0.3.7)
|
||||
railties (6.1.4)
|
||||
actionpack (= 6.1.4)
|
||||
activesupport (= 6.1.4)
|
||||
method_source
|
||||
rake (>= 0.8.7)
|
||||
thor (>= 0.20.3, < 2.0)
|
||||
rake (>= 0.13)
|
||||
thor (~> 1.0)
|
||||
rainbow (3.0.0)
|
||||
rake (13.0.3)
|
||||
rb-fsevent (0.10.4)
|
||||
rake (13.0.6)
|
||||
rb-fsevent (0.11.0)
|
||||
rb-inotify (0.10.1)
|
||||
ffi (~> 1.0)
|
||||
redis (4.2.1)
|
||||
redis-namespace (1.8.0)
|
||||
redis (4.4.0)
|
||||
redis-namespace (1.8.1)
|
||||
redis (>= 3.0.4)
|
||||
redis-rack-cache (2.2.1)
|
||||
rack-cache (>= 1.10, < 2)
|
||||
redis-store (>= 1.6, < 2)
|
||||
redis-store (1.9.0)
|
||||
redis (>= 4, < 5)
|
||||
regexp_parser (1.7.1)
|
||||
representable (3.0.4)
|
||||
regexp_parser (2.1.1)
|
||||
representable (3.1.1)
|
||||
declarative (< 0.1.0)
|
||||
declarative-option (< 0.2.0)
|
||||
trailblazer-option (>= 0.1.1, < 0.2.0)
|
||||
uber (< 0.2.0)
|
||||
responders (3.0.1)
|
||||
actionpack (>= 5.0)
|
||||
@@ -439,57 +449,53 @@ GEM
|
||||
netrc (~> 0.8)
|
||||
retriable (3.1.2)
|
||||
rexml (3.2.5)
|
||||
rspec-core (3.9.2)
|
||||
rspec-support (~> 3.9.3)
|
||||
rspec-expectations (3.9.2)
|
||||
rspec-core (3.10.1)
|
||||
rspec-support (~> 3.10.0)
|
||||
rspec-expectations (3.10.1)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.9.0)
|
||||
rspec-mocks (3.9.1)
|
||||
rspec-support (~> 3.10.0)
|
||||
rspec-mocks (3.10.2)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.9.0)
|
||||
rspec-rails (4.0.1)
|
||||
actionpack (>= 4.2)
|
||||
activesupport (>= 4.2)
|
||||
railties (>= 4.2)
|
||||
rspec-core (~> 3.9)
|
||||
rspec-expectations (~> 3.9)
|
||||
rspec-mocks (~> 3.9)
|
||||
rspec-support (~> 3.9)
|
||||
rspec-support (3.9.3)
|
||||
rubocop (0.89.1)
|
||||
rspec-support (~> 3.10.0)
|
||||
rspec-rails (5.0.1)
|
||||
actionpack (>= 5.2)
|
||||
activesupport (>= 5.2)
|
||||
railties (>= 5.2)
|
||||
rspec-core (~> 3.10)
|
||||
rspec-expectations (~> 3.10)
|
||||
rspec-mocks (~> 3.10)
|
||||
rspec-support (~> 3.10)
|
||||
rspec-support (3.10.2)
|
||||
rubocop (1.18.4)
|
||||
parallel (~> 1.10)
|
||||
parser (>= 2.7.1.1)
|
||||
parser (>= 3.0.0.0)
|
||||
rainbow (>= 2.2.2, < 4.0)
|
||||
regexp_parser (>= 1.7)
|
||||
regexp_parser (>= 1.8, < 3.0)
|
||||
rexml
|
||||
rubocop-ast (>= 0.3.0, < 1.0)
|
||||
rubocop-ast (>= 1.8.0, < 2.0)
|
||||
ruby-progressbar (~> 1.7)
|
||||
unicode-display_width (>= 1.4.0, < 2.0)
|
||||
rubocop-ast (0.3.0)
|
||||
parser (>= 2.7.1.4)
|
||||
rubocop-performance (1.7.1)
|
||||
rubocop (>= 0.82.0)
|
||||
rubocop-rails (2.8.1)
|
||||
unicode-display_width (>= 1.4.0, < 3.0)
|
||||
rubocop-ast (1.8.0)
|
||||
parser (>= 3.0.1.1)
|
||||
rubocop-performance (1.11.4)
|
||||
rubocop (>= 1.7.0, < 2.0)
|
||||
rubocop-ast (>= 0.4.0)
|
||||
rubocop-rails (2.11.3)
|
||||
activesupport (>= 4.2.0)
|
||||
rack (>= 1.1)
|
||||
rubocop (>= 0.87.0)
|
||||
rubocop-rspec (1.43.2)
|
||||
rubocop (~> 0.87)
|
||||
ruby-enum (0.9.0)
|
||||
i18n
|
||||
ruby-progressbar (1.10.1)
|
||||
rubocop (>= 1.7.0, < 2.0)
|
||||
rubocop-rspec (2.4.0)
|
||||
rubocop (~> 1.0)
|
||||
rubocop-ast (>= 1.1.0)
|
||||
ruby-progressbar (1.11.0)
|
||||
ruby-vips (2.1.2)
|
||||
ffi (~> 1.12)
|
||||
ruby2_keywords (0.0.5)
|
||||
ruby2ruby (2.4.4)
|
||||
ruby_parser (~> 3.1)
|
||||
sexp_processor (~> 4.6)
|
||||
ruby_parser (3.15.0)
|
||||
rubocop (>= 0.87.0)
|
||||
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)
|
||||
ruby_parser (3.16.0)
|
||||
sexp_processor (~> 4.15, >= 4.15.1)
|
||||
sassc (2.4.0)
|
||||
ffi (~> 1.9)
|
||||
sassc-rails (2.1.2)
|
||||
@@ -498,21 +504,30 @@ GEM
|
||||
sprockets (> 3.0)
|
||||
sprockets-rails
|
||||
tilt
|
||||
scout_apm (2.6.9)
|
||||
scout_apm (4.1.1)
|
||||
parser
|
||||
scss_lint (0.59.0)
|
||||
sass (~> 3.5, >= 3.5.5)
|
||||
seed_dump (3.3.1)
|
||||
activerecord (>= 4)
|
||||
activesupport (>= 4)
|
||||
selectize-rails (0.12.6)
|
||||
semantic_range (2.3.0)
|
||||
sentry-raven (3.0.3)
|
||||
semantic_range (3.0.0)
|
||||
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)
|
||||
sexp_processor (4.15.1)
|
||||
shoulda-matchers (4.4.1)
|
||||
activesupport (>= 4.2.0)
|
||||
sidekiq (6.1.1)
|
||||
sentry-ruby-core (= 4.6.4)
|
||||
sentry-ruby-core (4.6.4)
|
||||
concurrent-ruby
|
||||
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)
|
||||
rack (~> 2.0)
|
||||
redis (>= 4.2.0)
|
||||
@@ -529,7 +544,7 @@ GEM
|
||||
json (>= 1.8, < 3)
|
||||
simplecov-html (~> 0.10.0)
|
||||
simplecov-html (0.10.2)
|
||||
slack-ruby-client (0.15.0)
|
||||
slack-ruby-client (0.17.0)
|
||||
faraday (>= 1.0)
|
||||
faraday_middleware
|
||||
gli
|
||||
@@ -539,7 +554,7 @@ GEM
|
||||
spring-watcher-listen (2.0.1)
|
||||
listen (>= 2.7, < 4.0)
|
||||
spring (>= 1.2, < 3.0)
|
||||
sprockets (3.7.2)
|
||||
sprockets (4.0.2)
|
||||
concurrent-ruby (~> 1.0)
|
||||
rack (> 1, < 3)
|
||||
sprockets-rails (3.2.2)
|
||||
@@ -547,27 +562,28 @@ GEM
|
||||
activesupport (>= 4.0)
|
||||
sprockets (>= 3.0.0)
|
||||
squasher (0.6.2)
|
||||
statsd-ruby (1.4.0)
|
||||
telegram-bot-ruby (0.12.0)
|
||||
statsd-ruby (1.5.0)
|
||||
telegram-bot-ruby (0.16.0)
|
||||
dry-inflector
|
||||
faraday
|
||||
inflecto
|
||||
virtus
|
||||
telephone_number (1.4.9)
|
||||
virtus (~> 2.0)
|
||||
telephone_number (1.4.12)
|
||||
thor (1.1.0)
|
||||
thread_safe (0.3.6)
|
||||
tilt (2.0.10)
|
||||
time_diff (0.3.0)
|
||||
activesupport
|
||||
i18n
|
||||
trailblazer-option (0.1.1)
|
||||
twilio-ruby (5.32.0)
|
||||
faraday (~> 1.0.0)
|
||||
jwt (>= 1.5, <= 2.5)
|
||||
nokogiri (>= 1.6, < 2.0)
|
||||
twitty (0.1.1)
|
||||
twitty (0.1.4)
|
||||
oauth
|
||||
tzinfo (1.2.9)
|
||||
thread_safe (~> 0.1)
|
||||
tzinfo-data (1.2020.1)
|
||||
tzinfo (2.0.4)
|
||||
concurrent-ruby (~> 1.0)
|
||||
tzinfo-data (1.2021.1)
|
||||
tzinfo (>= 1.0.0)
|
||||
uber (0.1.0)
|
||||
uglifier (4.2.0)
|
||||
@@ -575,46 +591,49 @@ GEM
|
||||
unf (0.1.4)
|
||||
unf_ext
|
||||
unf_ext (0.0.7.7)
|
||||
unicode-display_width (1.7.0)
|
||||
uniform_notifier (1.13.0)
|
||||
unicode-display_width (2.0.0)
|
||||
uniform_notifier (1.14.2)
|
||||
uri_template (0.7.0)
|
||||
valid_email2 (3.3.1)
|
||||
valid_email2 (4.0.0)
|
||||
activemodel (>= 3.2)
|
||||
mail (~> 2.5)
|
||||
virtus (1.0.5)
|
||||
virtus (2.0.0)
|
||||
axiom-types (~> 0.1)
|
||||
coercible (~> 1.0)
|
||||
descendants_tracker (~> 0.0, >= 0.0.3)
|
||||
equalizer (~> 0.0, >= 0.0.9)
|
||||
warden (1.2.9)
|
||||
rack (>= 2.0.9)
|
||||
web-console (4.0.4)
|
||||
web-console (4.1.0)
|
||||
actionview (>= 6.0.0)
|
||||
activemodel (>= 6.0.0)
|
||||
bindex (>= 0.4.0)
|
||||
railties (>= 6.0.0)
|
||||
webmock (3.8.3)
|
||||
webmock (3.13.0)
|
||||
addressable (>= 2.3.6)
|
||||
crack (>= 0.3.2)
|
||||
hashdiff (>= 0.4.0, < 2.0.0)
|
||||
webpacker (5.2.1)
|
||||
webpacker (5.4.0)
|
||||
activesupport (>= 5.2)
|
||||
rack-proxy (>= 0.6.1)
|
||||
railties (>= 5.2)
|
||||
semantic_range (>= 2.3.0)
|
||||
webpush (1.0.0)
|
||||
webpush (1.1.0)
|
||||
hkdf (~> 0.2)
|
||||
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.5)
|
||||
wisper (2.0.0)
|
||||
zeitwerk (2.4.2)
|
||||
|
||||
PLATFORMS
|
||||
ruby
|
||||
arm64-darwin-20
|
||||
x86_64-darwin-21
|
||||
x86_64-linux
|
||||
|
||||
DEPENDENCIES
|
||||
active_record_query_trace
|
||||
activerecord-import
|
||||
acts-as-taggable-on
|
||||
administrate
|
||||
@@ -632,10 +651,12 @@ DEPENDENCIES
|
||||
commonmarker
|
||||
cypress-on-rails (~> 1.0)
|
||||
database_cleaner
|
||||
ddtrace
|
||||
devise
|
||||
devise-secure_password (~> 2.0)
|
||||
devise_token_auth
|
||||
dotenv-rails
|
||||
down (~> 5.0)
|
||||
facebook-messenger
|
||||
factory_bot_rails
|
||||
faker
|
||||
@@ -649,8 +670,9 @@ DEPENDENCIES
|
||||
haikunator
|
||||
hairtrigger
|
||||
hashie
|
||||
image_processing
|
||||
jbuilder
|
||||
json_refs!
|
||||
json_refs
|
||||
json_schemer
|
||||
jwt
|
||||
kaminari
|
||||
@@ -659,30 +681,30 @@ DEPENDENCIES
|
||||
liquid
|
||||
listen
|
||||
maxminddb
|
||||
mini_magick
|
||||
mock_redis!
|
||||
mock_redis
|
||||
pg
|
||||
procore-sift
|
||||
pry-rails
|
||||
puma
|
||||
pundit
|
||||
rack-attack
|
||||
rack-cors
|
||||
rack-timeout
|
||||
rails
|
||||
redis
|
||||
redis-namespace
|
||||
redis-rack-cache
|
||||
responders
|
||||
rest-client
|
||||
rspec-rails (~> 4.0.0.beta2)
|
||||
rspec-rails (~> 5.0.0)
|
||||
rubocop
|
||||
rubocop-performance
|
||||
rubocop-rails
|
||||
rubocop-rspec
|
||||
scout_apm
|
||||
scss_lint
|
||||
seed_dump
|
||||
sentry-raven
|
||||
sentry-rails
|
||||
sentry-ruby
|
||||
sentry-sidekiq
|
||||
shoulda-matchers
|
||||
sidekiq
|
||||
sidekiq-cron
|
||||
@@ -706,7 +728,7 @@ DEPENDENCIES
|
||||
wisper (= 2.0.0)
|
||||
|
||||
RUBY VERSION
|
||||
ruby 2.7.3p183
|
||||
ruby 3.0.2p107
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
@@ -57,28 +56,22 @@ Other features include:
|
||||
- **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.
|
||||
|
||||
---
|
||||
## 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.
|
||||
|
||||
---
|
||||
|
||||
### 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`.
|
||||
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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
#### Other deployment options
|
||||
### Other deployment options
|
||||
|
||||
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):
|
||||
|
||||
|
||||
25
SECURITY.md
25
SECURITY.md
@@ -1,8 +1,31 @@
|
||||
# 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
|
||||
|
||||
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.
|
||||
|
||||
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": {
|
||||
"description": "Public root URL of the Chatwoot installation. This will be used in the emails.",
|
||||
"value": "https://CHANGE.herokuapp.com"
|
||||
},
|
||||
"INSTALLATION_ENV": {
|
||||
"description": "Installation method used for Chatwoot.",
|
||||
"value": "heroku"
|
||||
}
|
||||
},
|
||||
"formation": {
|
||||
|
||||
@@ -3,8 +3,9 @@ class ContactIdentifyAction
|
||||
|
||||
def perform
|
||||
ActiveRecord::Base.transaction do
|
||||
@contact = merge_contact(existing_identified_contact, @contact) if merge_contacts?(existing_identified_contact, @contact)
|
||||
@contact = merge_contact(existing_email_contact, @contact) if merge_contacts?(existing_email_contact, @contact)
|
||||
merge_if_existing_identified_contact
|
||||
merge_if_existing_email_contact
|
||||
merge_if_existing_phone_number_contact
|
||||
update_contact
|
||||
end
|
||||
@contact
|
||||
@@ -16,6 +17,18 @@ class ContactIdentifyAction
|
||||
@account ||= @contact.account
|
||||
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
|
||||
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])
|
||||
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)
|
||||
existing_contact && existing_contact.id != @contact.id
|
||||
end
|
||||
@@ -36,7 +55,9 @@ class ContactIdentifyAction
|
||||
custom_attributes = params[:custom_attributes] ? @contact.custom_attributes.merge(params[:custom_attributes]) : @contact.custom_attributes
|
||||
# blank identifier or email will throw unique index error
|
||||
# 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?
|
||||
end
|
||||
|
||||
|
||||
@@ -38,17 +38,30 @@ class ContactBuilder
|
||||
end
|
||||
|
||||
def find_contact
|
||||
contact = nil
|
||||
|
||||
contact = account.contacts.find_by(identifier: contact_attributes[:identifier]) if contact_attributes[:identifier].present?
|
||||
|
||||
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 = find_contact_by_identifier(contact_attributes[:identifier])
|
||||
contact ||= find_contact_by_email(contact_attributes[:email])
|
||||
contact ||= find_contact_by_phone_number(contact_attributes[:phone_number])
|
||||
contact
|
||||
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
|
||||
ActiveRecord::Base.transaction do
|
||||
contact = find_contact || create_contact
|
||||
@@ -57,6 +70,7 @@ class ContactBuilder
|
||||
contact_inbox
|
||||
rescue StandardError => e
|
||||
Rails.logger.info e
|
||||
raise e
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -17,12 +17,18 @@ class Messages::Facebook::MessageBuilder
|
||||
end
|
||||
|
||||
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
|
||||
build_contact
|
||||
build_message
|
||||
end
|
||||
ensure_contact_avatar
|
||||
rescue Koala::Facebook::AuthenticationError
|
||||
Rails.logger.info "Facebook Authorization expired for Inbox #{@inbox.id}"
|
||||
rescue StandardError => e
|
||||
Raven.capture_exception(e)
|
||||
Sentry.capture_exception(e)
|
||||
true
|
||||
end
|
||||
|
||||
@@ -36,7 +42,6 @@ class Messages::Facebook::MessageBuilder
|
||||
return if contact.present?
|
||||
|
||||
@contact = Contact.create!(contact_params.except(:remote_avatar_url))
|
||||
ContactAvatarJob.perform_later(@contact, contact_params[:remote_avatar_url]) if contact_params[:remote_avatar_url]
|
||||
@contact_inbox = ContactInbox.create(contact: contact, inbox: @inbox, source_id: @sender_id)
|
||||
end
|
||||
|
||||
@@ -56,10 +61,21 @@ class Messages::Facebook::MessageBuilder
|
||||
end
|
||||
|
||||
def attach_file(attachment, file_url)
|
||||
file_resource = LocalResource.new(file_url)
|
||||
attachment.file.attach(io: file_resource.file, filename: file_resource.filename, content_type: file_resource.encoding)
|
||||
rescue *ExceptionList::URI_EXCEPTIONS => e
|
||||
Rails.logger.info "invalid url #{file_url} : #{e.message}"
|
||||
attachment_file = Down.download(
|
||||
file_url
|
||||
)
|
||||
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
|
||||
|
||||
def conversation
|
||||
@@ -136,9 +152,12 @@ class Messages::Facebook::MessageBuilder
|
||||
begin
|
||||
k = Koala::Facebook::API.new(@inbox.channel.page_access_token) if @inbox.facebook?
|
||||
result = k.get_object(@sender_id) || {}
|
||||
rescue Koala::Facebook::AuthenticationError
|
||||
@inbox.channel.authorization_error!
|
||||
raise
|
||||
rescue StandardError => e
|
||||
result = {}
|
||||
Raven.capture_exception(e)
|
||||
Sentry.capture_exception(e)
|
||||
end
|
||||
{
|
||||
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])
|
||||
if current_user
|
||||
account_accessible_for_user?(account)
|
||||
elsif @resource&.is_a?(AgentBot)
|
||||
elsif @resource.is_a?(AgentBot)
|
||||
account_accessible_for_bot?(account)
|
||||
end
|
||||
account
|
||||
|
||||
@@ -69,39 +69,15 @@ class Api::V1::Accounts::CallbacksController < Api::V1::Accounts::BaseController
|
||||
return [] if data.empty?
|
||||
|
||||
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
|
||||
end
|
||||
end
|
||||
|
||||
def set_avatar(facebook_inbox, page_id)
|
||||
uri = get_avatar_url(page_id)
|
||||
|
||||
return unless uri
|
||||
|
||||
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
|
||||
avatar_file = Down.download(
|
||||
"http://graph.facebook.com/#{page_id}/picture?type=large"
|
||||
)
|
||||
facebook_inbox.avatar.attach(io: avatar_file, filename: avatar_file.original_filename, content_type: avatar_file.content_type)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -6,9 +6,8 @@ class Api::V1::Accounts::Channels::TwilioChannelsController < Api::V1::Accounts:
|
||||
authenticate_twilio
|
||||
build_inbox
|
||||
setup_webhooks if @twilio_channel.sms?
|
||||
rescue Twilio::REST::TwilioError => e
|
||||
render_could_not_create_error(e.message)
|
||||
rescue StandardError => e
|
||||
Sentry.capture_exception(e)
|
||||
render_could_not_create_error(e.message)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -11,6 +11,7 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
||||
before_action :check_authorization
|
||||
before_action :set_current_page, only: [:index, :active, :search]
|
||||
before_action :fetch_contact, only: [:show, :update, :contactable_inboxes]
|
||||
before_action :set_include_contact_inboxes, only: [:index, :search]
|
||||
|
||||
def index
|
||||
@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
|
||||
|
||||
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]}%"
|
||||
)
|
||||
@contacts_count = contacts.count
|
||||
@@ -87,11 +88,15 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
||||
end
|
||||
|
||||
def fetch_contacts_with_conversation_count(contacts)
|
||||
filtrate(contacts).left_outer_joins(:conversations)
|
||||
.select('contacts.*, COUNT(conversations.id) as conversations_count')
|
||||
.group('contacts.id')
|
||||
.includes([{ avatar_attachment: [:blob] }, { contact_inboxes: [:inbox] }])
|
||||
.page(@current_page).per(RESULTS_PER_PAGE)
|
||||
contacts_with_conversation_count = filtrate(contacts).left_outer_joins(:conversations)
|
||||
.select('contacts.*, COUNT(conversations.id) as conversations_count')
|
||||
.group('contacts.id')
|
||||
.includes([{ avatar_attachment: [:blob] }])
|
||||
.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
|
||||
|
||||
def build_contact_inbox
|
||||
@@ -103,7 +108,7 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
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 })
|
||||
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
|
||||
@contact = Current.account.contacts.includes(contact_inboxes: [:inbox]).find(params[:id])
|
||||
end
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseController
|
||||
include Events::Types
|
||||
include DateRangeHelper
|
||||
|
||||
before_action :conversation, except: [:index, :meta, :search, :create]
|
||||
before_action :contact_inbox, only: [:create]
|
||||
@@ -49,8 +50,8 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
|
||||
|
||||
def toggle_status
|
||||
if params[:status]
|
||||
@conversation.status = params[:status]
|
||||
@status = @conversation.save
|
||||
set_conversation_status
|
||||
@status = @conversation.save!
|
||||
else
|
||||
@status = @conversation.toggle_status
|
||||
end
|
||||
@@ -73,6 +74,12 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
|
||||
|
||||
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)
|
||||
user = current_user.presence || @resource
|
||||
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
|
||||
additional_attributes = params[:additional_attributes]&.permit! || {}
|
||||
status = params[:status].present? ? { status: params[:status] } : {}
|
||||
|
||||
# TODO: temporary fallback for the old bot status in conversation, we will remove after couple of releases
|
||||
status = { status: 'pending' } if status[:status] == 'bot'
|
||||
{
|
||||
account_id: Current.account.id,
|
||||
inbox_id: @contact_inbox.inbox_id,
|
||||
contact_id: @contact_inbox.contact_id,
|
||||
contact_inbox_id: @contact_inbox.id,
|
||||
additional_attributes: additional_attributes
|
||||
additional_attributes: additional_attributes,
|
||||
snoozed_until: params[:snoozed_until]
|
||||
}.merge(status)
|
||||
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
|
||||
head :ok
|
||||
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')
|
||||
end
|
||||
end
|
||||
|
||||
@@ -10,7 +10,7 @@ class Api::V1::WebhooksController < ApplicationController
|
||||
twitter_consumer.consume
|
||||
head :ok
|
||||
rescue StandardError => e
|
||||
Raven.capture_exception(e)
|
||||
Sentry.capture_exception(e)
|
||||
head :ok
|
||||
end
|
||||
|
||||
|
||||
@@ -94,6 +94,10 @@ class Api::V1::Widget::BaseController < ApplicationController
|
||||
{ timestamp: permitted_params[:message][:timestamp] }
|
||||
end
|
||||
|
||||
def permitted_params
|
||||
params.permit(:website_token)
|
||||
end
|
||||
|
||||
def message_params
|
||||
{
|
||||
account_id: conversation.account_id,
|
||||
|
||||
@@ -4,10 +4,4 @@ class Api::V1::Widget::CampaignsController < Api::V1::Widget::BaseController
|
||||
def index
|
||||
@campaigns = @web_widget.inbox.campaigns.where(enabled: true)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def permitted_params
|
||||
params.permit(:website_token)
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
@@ -4,10 +4,4 @@ class Api::V1::Widget::InboxMembersController < Api::V1::Widget::BaseController
|
||||
def index
|
||||
@inbox_members = @web_widget.inbox.inbox_members.includes(:user)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def permitted_params
|
||||
params.permit(:website_token)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -16,7 +16,7 @@ class ApplicationController < ActionController::Base
|
||||
def handle_with_exception
|
||||
yield
|
||||
rescue ActiveRecord::RecordNotFound => e
|
||||
Raven.capture_exception(e)
|
||||
Sentry.capture_exception(e)
|
||||
render_not_found_error('Resource could not be found')
|
||||
rescue Pundit::NotAuthorizedError
|
||||
render_unauthorized('You are not authorized to do this action')
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
module AuthHelper
|
||||
def send_auth_headers(user)
|
||||
data = user.create_new_auth_token
|
||||
response.headers[DeviseTokenAuth.headers_names[:"access-token"]] = data['access-token']
|
||||
response.headers[DeviseTokenAuth.headers_names[:"token-type"]] = 'Bearer'
|
||||
response.headers[DeviseTokenAuth.headers_names[:'access-token']] = data['access-token']
|
||||
response.headers[DeviseTokenAuth.headers_names[:'token-type']] = 'Bearer'
|
||||
response.headers[DeviseTokenAuth.headers_names[:client]] = data['client']
|
||||
response.headers[DeviseTokenAuth.headers_names[:expiry]] = data['expiry']
|
||||
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
|
||||
@message.update!(message_update_params)
|
||||
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
|
||||
|
||||
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?
|
||||
render inline: File.read(Rails.root.join('swagger', derived_path))
|
||||
else
|
||||
head 404
|
||||
head :not_found
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# TODO : Delete this and associated spec once 'api/widget/config' end point is merged
|
||||
class WidgetsController < ActionController::Base
|
||||
before_action :set_global_config
|
||||
before_action :set_web_widget
|
||||
|
||||
@@ -114,13 +114,13 @@ class ConversationFinder
|
||||
end
|
||||
|
||||
def current_page
|
||||
params[:page]
|
||||
params[:page] || 1
|
||||
end
|
||||
|
||||
def conversations
|
||||
@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
|
||||
|
||||
@@ -3,12 +3,17 @@
|
||||
<transition name="fade" mode="out-in">
|
||||
<router-view></router-view>
|
||||
</transition>
|
||||
<add-account-modal
|
||||
:show="showAddAccountModal"
|
||||
:has-accounts="hasAccounts"
|
||||
/>
|
||||
<woot-snackbar-box />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import AddAccountModal from '../dashboard/components/layout/sidebarComponents/AddAccountModal';
|
||||
import WootSnackbarBox from './components/SnackbarContainer';
|
||||
import { accountIdFromPathname } from './helper/URLHelper';
|
||||
|
||||
@@ -17,14 +22,36 @@ export default {
|
||||
|
||||
components: {
|
||||
WootSnackbarBox,
|
||||
AddAccountModal,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
showAddAccountModal: false,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters({
|
||||
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() {
|
||||
this.$store.dispatch('setUser');
|
||||
this.setLocale(window.chatwootConfig.selectedLocale);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import ApiClient from './ApiClient';
|
||||
|
||||
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) {
|
||||
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`, {
|
||||
status,
|
||||
snoozed_until: snoozedUntil,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,33 @@
|
||||
/* global axios */
|
||||
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 {
|
||||
constructor() {
|
||||
super('conversations', { accountScoped: true });
|
||||
@@ -15,18 +42,16 @@ class MessageApi extends ApiClient {
|
||||
echo_id: echoId,
|
||||
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({
|
||||
method: 'post',
|
||||
url: `${this.url}/${conversationId}/messages`,
|
||||
data: formData,
|
||||
data: buildCreatePayload({
|
||||
message,
|
||||
isPrivate,
|
||||
contentAttributes,
|
||||
echoId,
|
||||
file,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ describe('#ContactsAPI', () => {
|
||||
it('#get', () => {
|
||||
contactAPI.get(1, 'name', 'customer-support');
|
||||
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', () => {
|
||||
contactAPI.search('leads', 1, 'date', 'customer-support');
|
||||
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', () => {
|
||||
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(
|
||||
'page=1&sort=name&labels[]=customer-support'
|
||||
'include_contact_inboxes=false&page=1&sort=name&labels[]=customer-support'
|
||||
);
|
||||
expect(
|
||||
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`,
|
||||
{
|
||||
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 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 {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.multiselect__tags-wrap {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,22 @@
|
||||
font-weight: var(--font-weight-light);
|
||||
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 {
|
||||
@@ -20,17 +36,4 @@
|
||||
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;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.message-from-agent {
|
||||
color: $color-gray;
|
||||
font-size: $font-size-mini;
|
||||
}
|
||||
}
|
||||
|
||||
.conversation--meta {
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
{{ $t('CHAT_LIST.LIST.404') }}
|
||||
</p>
|
||||
|
||||
<div class="conversations-list">
|
||||
<div ref="activeConversation" class="conversations-list">
|
||||
<conversation-card
|
||||
v-for="chat in conversationList"
|
||||
:key="chat.id"
|
||||
@@ -62,8 +62,13 @@ import ChatFilter from './widgets/conversation/ChatFilter';
|
||||
import ChatTypeTabs from './widgets/ChatTypeTabs';
|
||||
import ConversationCard from './widgets/conversation/ConversationCard';
|
||||
import timeMixin from '../mixins/time';
|
||||
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
||||
import conversationMixin from '../mixins/conversations';
|
||||
import wootConstants from '../constants';
|
||||
import {
|
||||
hasPressedAltAndJKey,
|
||||
hasPressedAltAndKKey,
|
||||
} from 'shared/helpers/KeyboardHelpers';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -71,7 +76,7 @@ export default {
|
||||
ConversationCard,
|
||||
ChatFilter,
|
||||
},
|
||||
mixins: [timeMixin, conversationMixin],
|
||||
mixins: [timeMixin, conversationMixin, eventListenerMixins],
|
||||
props: {
|
||||
conversationInbox: {
|
||||
type: [String, Number],
|
||||
@@ -94,6 +99,7 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
currentChat: 'getSelectedChat',
|
||||
chatLists: 'getAllConversations',
|
||||
mineChatsList: 'getMineChats',
|
||||
allChatList: 'getAllStatusChats',
|
||||
@@ -188,6 +194,33 @@ export default {
|
||||
});
|
||||
},
|
||||
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() {
|
||||
this.$store.dispatch('conversationPage/reset');
|
||||
this.$store.dispatch('emptyAllConversations');
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
{{ this.$t('CONVERSATION.HEADER.REOPEN_ACTION') }}
|
||||
</woot-button>
|
||||
<woot-button
|
||||
v-else-if="isBot"
|
||||
v-else-if="showOpenButton"
|
||||
class-names="resolve"
|
||||
color-scheme="primary"
|
||||
icon="ion-person"
|
||||
@@ -34,7 +34,8 @@
|
||||
{{ this.$t('CONVERSATION.HEADER.OPEN_ACTION') }}
|
||||
</woot-button>
|
||||
<woot-button
|
||||
v-if="showDropDown"
|
||||
v-if="showAdditionalActions"
|
||||
ref="arrowDownButton"
|
||||
:color-scheme="buttonClass"
|
||||
:disabled="isLoading"
|
||||
icon="ion-arrow-down-b"
|
||||
@@ -43,19 +44,53 @@
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="showDropdown"
|
||||
v-if="showActionsDropdown"
|
||||
v-on-clickaway="closeDropdown"
|
||||
class="dropdown-pane dropdown-pane--open"
|
||||
>
|
||||
<woot-dropdown-menu>
|
||||
<woot-dropdown-item v-if="!isBot">
|
||||
<woot-dropdown-item v-if="!isPending">
|
||||
<woot-button
|
||||
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-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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -65,22 +100,37 @@
|
||||
import { mapGetters } from 'vuex';
|
||||
import { mixin as clickaway } from 'vue-clickaway';
|
||||
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 WootDropdownSubMenu from 'shared/components/ui/dropdown/DropdownSubMenu.vue';
|
||||
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue';
|
||||
import wootConstants from '../../constants';
|
||||
import {
|
||||
getUnixTime,
|
||||
addHours,
|
||||
addWeeks,
|
||||
startOfTomorrow,
|
||||
startOfWeek,
|
||||
} from 'date-fns';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
WootDropdownItem,
|
||||
WootDropdownMenu,
|
||||
WootDropdownSubMenu,
|
||||
},
|
||||
mixins: [clickaway, alertMixin],
|
||||
mixins: [clickaway, alertMixin, eventListenerMixins],
|
||||
props: { conversationId: { type: [String, Number], required: true } },
|
||||
data() {
|
||||
return {
|
||||
isLoading: false,
|
||||
showDropdown: false,
|
||||
showActionsDropdown: false,
|
||||
STATUS_TYPE: wootConstants.STATUS_TYPE,
|
||||
};
|
||||
},
|
||||
@@ -91,36 +141,83 @@ export default {
|
||||
isOpen() {
|
||||
return this.currentChat.status === wootConstants.STATUS_TYPE.OPEN;
|
||||
},
|
||||
isBot() {
|
||||
return this.currentChat.status === wootConstants.STATUS_TYPE.BOT;
|
||||
isPending() {
|
||||
return this.currentChat.status === wootConstants.STATUS_TYPE.PENDING;
|
||||
},
|
||||
isResolved() {
|
||||
return this.currentChat.status === wootConstants.STATUS_TYPE.RESOLVED;
|
||||
},
|
||||
isSnoozed() {
|
||||
return this.currentChat.status === wootConstants.STATUS_TYPE.SNOOZED;
|
||||
},
|
||||
buttonClass() {
|
||||
if (this.isBot) return 'primary';
|
||||
if (this.isPending) return 'primary';
|
||||
if (this.isOpen) return 'success';
|
||||
if (this.isResolved) return 'warning';
|
||||
return '';
|
||||
},
|
||||
showDropDown() {
|
||||
return !this.isBot;
|
||||
showAdditionalActions() {
|
||||
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: {
|
||||
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() {
|
||||
this.showDropdown = false;
|
||||
this.showActionsDropdown = false;
|
||||
},
|
||||
openDropdown() {
|
||||
this.showDropdown = true;
|
||||
this.showActionsDropdown = true;
|
||||
},
|
||||
toggleStatus(status) {
|
||||
toggleStatus(status, snoozedUntil) {
|
||||
this.closeDropdown();
|
||||
this.isLoading = true;
|
||||
this.$store
|
||||
.dispatch('toggleStatus', {
|
||||
conversationId: this.currentChat.id,
|
||||
status,
|
||||
snoozedUntil,
|
||||
})
|
||||
.then(() => {
|
||||
this.showAlert(this.$t('CONVERSATION.CHANGE_STATUS'));
|
||||
|
||||
@@ -50,10 +50,17 @@
|
||||
:show="showOptionsMenu"
|
||||
@toggle-accounts="toggleAccountModal"
|
||||
@show-support-chat-window="toggleSupportChatWindow"
|
||||
@key-shortcut-modal="toggleKeyShortcutModal"
|
||||
@close="toggleOptions"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<woot-key-shortcut-modal
|
||||
v-if="showShortcutModal"
|
||||
@close="closeKeyShortcutModal"
|
||||
@clickaway="closeKeyShortcutModal"
|
||||
/>
|
||||
|
||||
<account-selector
|
||||
:show-account-modal="showAccountModal"
|
||||
@close-account-modal="toggleAccountModal"
|
||||
@@ -86,6 +93,9 @@ import OptionsMenu from './sidebarComponents/OptionsMenu.vue';
|
||||
import AccountSelector from './sidebarComponents/AccountSelector.vue';
|
||||
import AddAccountModal from './sidebarComponents/AddAccountModal.vue';
|
||||
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 {
|
||||
components: {
|
||||
@@ -97,14 +107,16 @@ export default {
|
||||
AccountSelector,
|
||||
AddAccountModal,
|
||||
AddLabelModal,
|
||||
WootKeyShortcutModal,
|
||||
},
|
||||
mixins: [adminMixin, alertMixin],
|
||||
mixins: [adminMixin, alertMixin, eventListenerMixins],
|
||||
data() {
|
||||
return {
|
||||
showOptionsMenu: false,
|
||||
showAccountModal: false,
|
||||
showCreateAccountModal: false,
|
||||
showAddLabelModal: false,
|
||||
showShortcutModal: false,
|
||||
};
|
||||
},
|
||||
|
||||
@@ -254,7 +266,19 @@ export default {
|
||||
this.$store.dispatch('teams/get');
|
||||
this.setChatwootUser();
|
||||
},
|
||||
|
||||
methods: {
|
||||
toggleKeyShortcutModal() {
|
||||
this.showShortcutModal = true;
|
||||
},
|
||||
closeKeyShortcutModal() {
|
||||
this.showShortcutModal = false;
|
||||
},
|
||||
handleKeyEvents(e) {
|
||||
if (hasPressedCommandAndForwardSlash(e)) {
|
||||
this.toggleKeyShortcutModal();
|
||||
}
|
||||
},
|
||||
toggleSupportChatWindow() {
|
||||
window.$chatwoot.toggle();
|
||||
},
|
||||
|
||||
@@ -59,10 +59,17 @@
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
import router from '../../routes';
|
||||
import {
|
||||
hasPressedAltAndCKey,
|
||||
hasPressedAltAndVKey,
|
||||
hasPressedAltAndRKey,
|
||||
hasPressedAltAndSKey,
|
||||
} from 'shared/helpers/KeyboardHelpers';
|
||||
import adminMixin from '../../mixins/isAdmin';
|
||||
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
||||
import { getInboxClassByType } from 'dashboard/helper/inbox';
|
||||
export default {
|
||||
mixins: [adminMixin],
|
||||
mixins: [adminMixin, eventListenerMixins],
|
||||
props: {
|
||||
menuItem: {
|
||||
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) {
|
||||
return this.isAdmin && item.newLink !== undefined;
|
||||
},
|
||||
|
||||
@@ -9,6 +9,14 @@
|
||||
:header-title="$t('CREATE_ACCOUNT.NEW_ACCOUNT')"
|
||||
: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">
|
||||
<div class="medium-12 columns">
|
||||
@@ -53,6 +61,10 @@ export default {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
hasAccounts: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -90,3 +102,19 @@ export default {
|
||||
},
|
||||
};
|
||||
</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
|
||||
</woot-button>
|
||||
</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>
|
||||
<router-link
|
||||
:to="`/app/accounts/${accountId}/profile/settings`"
|
||||
|
||||
@@ -10,8 +10,11 @@
|
||||
</template>
|
||||
<script>
|
||||
import wootConstants from '../../constants';
|
||||
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
||||
import { hasPressedAltAndNKey } from 'shared/helpers/KeyboardHelpers';
|
||||
|
||||
export default {
|
||||
mixins: [eventListenerMixins],
|
||||
props: {
|
||||
items: {
|
||||
type: Array,
|
||||
@@ -28,6 +31,15 @@ export default {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handleKeyEvents(e) {
|
||||
if (hasPressedAltAndNKey(e)) {
|
||||
if (this.activeTab === wootConstants.ASSIGNEE_TYPE.ALL) {
|
||||
this.onTabChange(0);
|
||||
} else {
|
||||
this.onTabChange(this.activeTabIndex + 1);
|
||||
}
|
||||
}
|
||||
},
|
||||
onTabChange(selectedTabIndex) {
|
||||
if (this.items[selectedTabIndex].key !== this.activeTab) {
|
||||
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: '' },
|
||||
placeholder: { type: String, default: '' },
|
||||
isPrivate: { type: Boolean, default: false },
|
||||
isFormatMode: { type: Boolean, default: false },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -139,8 +140,17 @@ export default {
|
||||
value(newValue = '') {
|
||||
if (newValue !== this.lastValue) {
|
||||
const { tr } = this.state;
|
||||
tr.insertText(newValue, 0, tr.doc.content.size);
|
||||
this.state = this.view.state.apply(tr);
|
||||
if (this.isFormatMode) {
|
||||
this.state = createState(
|
||||
newValue,
|
||||
this.placeholder,
|
||||
this.plugins,
|
||||
this.isFormatMode
|
||||
);
|
||||
} else {
|
||||
tr.insertText(newValue, 0, tr.doc.content.size);
|
||||
this.state = this.view.state.apply(tr);
|
||||
}
|
||||
this.view.updateState(this.state);
|
||||
}
|
||||
},
|
||||
@@ -271,4 +281,25 @@ export default {
|
||||
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>
|
||||
|
||||
@@ -78,11 +78,17 @@
|
||||
|
||||
<script>
|
||||
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';
|
||||
export default {
|
||||
name: 'ReplyTopPanel',
|
||||
components: { FileUpload },
|
||||
mixins: [eventListenerMixins],
|
||||
props: {
|
||||
mode: {
|
||||
type: String,
|
||||
@@ -156,6 +162,14 @@ export default {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handleKeyEvents(e) {
|
||||
if (hasPressedAltAndWKey(e)) {
|
||||
this.toggleFormatMode();
|
||||
}
|
||||
if (hasPressedAltAndAKey(e)) {
|
||||
this.$refs.upload.$children[1].$el.click();
|
||||
}
|
||||
},
|
||||
toggleFormatMode() {
|
||||
this.setFormatMode(!this.isFormatMode);
|
||||
},
|
||||
|
||||
@@ -32,11 +32,17 @@
|
||||
<script>
|
||||
import { REPLY_EDITOR_MODES, CHAR_LENGTH_WARNING } from './constants';
|
||||
import EmojiOrIcon from 'shared/components/EmojiOrIcon';
|
||||
import {
|
||||
hasPressedAltAndPKey,
|
||||
hasPressedAltAndLKey,
|
||||
} from 'shared/helpers/KeyboardHelpers';
|
||||
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
||||
export default {
|
||||
name: 'ReplyTopPanel',
|
||||
components: {
|
||||
EmojiOrIcon,
|
||||
},
|
||||
mixins: [eventListenerMixins],
|
||||
props: {
|
||||
mode: {
|
||||
type: String,
|
||||
@@ -76,6 +82,14 @@ export default {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handleKeyEvents(e) {
|
||||
if (hasPressedAltAndPKey(e)) {
|
||||
this.handleNoteClick();
|
||||
}
|
||||
if (hasPressedAltAndLKey(e)) {
|
||||
this.handleReplyClick();
|
||||
}
|
||||
},
|
||||
handleReplyClick() {
|
||||
this.setReplyMode(REPLY_EDITOR_MODES.REPLY);
|
||||
},
|
||||
|
||||
@@ -12,12 +12,29 @@
|
||||
|
||||
<script>
|
||||
import wootConstants from '../../../constants';
|
||||
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
||||
import { hasPressedAltAndBKey } from 'shared/helpers/KeyboardHelpers';
|
||||
|
||||
export default {
|
||||
mixins: [eventListenerMixins],
|
||||
data: () => ({
|
||||
activeStatus: wootConstants.STATUS_TYPE.OPEN,
|
||||
}),
|
||||
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() {
|
||||
this.$store.dispatch('setChatFilter', this.activeStatus);
|
||||
this.$emit('statusFilterChange', this.activeStatus);
|
||||
|
||||
@@ -49,13 +49,11 @@ export default {
|
||||
},
|
||||
isContactPanelOpen: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
currentChat: 'getSelectedChat',
|
||||
}),
|
||||
...mapGetters({ currentChat: 'getSelectedChat' }),
|
||||
showContactPanel() {
|
||||
return this.isContactPanelOpen && this.currentChat.id;
|
||||
},
|
||||
|
||||
@@ -26,7 +26,12 @@
|
||||
{{ currentContact.name }}
|
||||
</h4>
|
||||
<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">
|
||||
{{ parsedLastMessage }}
|
||||
</span>
|
||||
@@ -144,6 +149,18 @@ export default {
|
||||
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() {
|
||||
const { content_attributes: contentAttributes } = this.lastMessageInChat;
|
||||
const { email: { subject } = {} } = contentAttributes || {};
|
||||
@@ -230,4 +247,9 @@ export default {
|
||||
font-size: var(--font-size-mini);
|
||||
}
|
||||
}
|
||||
|
||||
.last-message-icon {
|
||||
color: var(--s-600);
|
||||
font-size: var(--font-size-mini);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -32,33 +32,6 @@
|
||||
class="header-actions-wrap"
|
||||
: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" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -68,15 +41,15 @@ import { mapGetters } from 'vuex';
|
||||
import MoreActions from './MoreActions';
|
||||
import Thumbnail from '../Thumbnail';
|
||||
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 {
|
||||
components: {
|
||||
MoreActions,
|
||||
Thumbnail,
|
||||
AvailabilityStatusBadge,
|
||||
},
|
||||
mixins: [agentMixin],
|
||||
mixins: [agentMixin, eventListenerMixins],
|
||||
props: {
|
||||
chat: {
|
||||
type: Object,
|
||||
@@ -117,17 +90,11 @@ export default {
|
||||
},
|
||||
|
||||
methods: {
|
||||
assignAgent(agent) {
|
||||
this.$store
|
||||
.dispatch('assignAgent', {
|
||||
conversationId: this.currentChat.id,
|
||||
agentId: agent.id,
|
||||
})
|
||||
.then(() => {
|
||||
bus.$emit('newToastMessage', this.$t('CONVERSATION.CHANGE_AGENT'));
|
||||
});
|
||||
handleKeyEvents(e) {
|
||||
if (hasPressedAltAndOKey(e)) {
|
||||
this.$emit('contact-panel-toggle');
|
||||
}
|
||||
},
|
||||
removeAgent() {},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
<li v-if="hasAttachments || data.content" :class="alignBubble">
|
||||
<div :class="wrapClass">
|
||||
<div v-tooltip.top-start="sentByMessage" :class="bubbleClass">
|
||||
<bubble-mail-head
|
||||
v-if="isEmailContentType"
|
||||
:email-attributes="contentAttributes.email"
|
||||
:is-incoming="isIncoming"
|
||||
/>
|
||||
<bubble-text
|
||||
v-if="data.content"
|
||||
:message="message"
|
||||
@@ -41,6 +46,7 @@
|
||||
:message-type="data.message_type"
|
||||
:readable-time="readableTime"
|
||||
:source-id="data.source_id"
|
||||
:inbox-id="data.inbox_id"
|
||||
/>
|
||||
</div>
|
||||
<spinner v-if="isPending" size="tiny" />
|
||||
@@ -79,16 +85,19 @@ import copy from 'copy-text-to-clipboard';
|
||||
|
||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||
import timeMixin from '../../../mixins/time';
|
||||
|
||||
import BubbleMailHead from './bubble/MailHead';
|
||||
import BubbleText from './bubble/Text';
|
||||
import BubbleImage from './bubble/Image';
|
||||
import BubbleFile from './bubble/File';
|
||||
import BubbleActions from './bubble/Actions';
|
||||
|
||||
import Spinner from 'shared/components/Spinner';
|
||||
import ContextMenu from 'dashboard/modules/conversations/components/MessageContextMenu';
|
||||
|
||||
import { isEmptyObject } from 'dashboard/helper/commons';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import contentTypeMixin from 'shared/mixins/contentTypeMixin';
|
||||
import BubbleActions from './bubble/Actions';
|
||||
import { MESSAGE_TYPE, MESSAGE_STATUS } from 'shared/constants/messages';
|
||||
import { generateBotMessageContent } from './helpers/botMessageContentHelper';
|
||||
|
||||
@@ -98,6 +107,7 @@ export default {
|
||||
BubbleText,
|
||||
BubbleImage,
|
||||
BubbleFile,
|
||||
BubbleMailHead,
|
||||
ContextMenu,
|
||||
Spinner,
|
||||
},
|
||||
@@ -133,11 +143,11 @@ export default {
|
||||
|
||||
const {
|
||||
email: {
|
||||
content_type: contentType = '',
|
||||
html_content: { full: fullHTMLContent, reply: replyHTMLContent } = {},
|
||||
text_content: { full: fullTextContent, reply: replyTextContent } = {},
|
||||
} = {},
|
||||
} = this.contentAttributes;
|
||||
|
||||
let contentToBeParsed =
|
||||
replyHTMLContent ||
|
||||
replyTextContent ||
|
||||
@@ -147,7 +157,12 @@ export default {
|
||||
if (contentToBeParsed && this.isIncoming) {
|
||||
const parsedContent = this.stripStyleCharacters(contentToBeParsed);
|
||||
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 (
|
||||
|
||||
@@ -33,11 +33,11 @@
|
||||
|
||||
<div v-if="isATweet" class="banner">
|
||||
<span v-if="!selectedTweetId">
|
||||
{{ $t('CONVERSATION.LAST_INCOMING_TWEET') }}
|
||||
{{ $t('CONVERSATION.SELECT_A_TWEET_TO_REPLY') }}
|
||||
</span>
|
||||
<span v-else>
|
||||
{{ $t('CONVERSATION.REPLYING_TO') }}
|
||||
{{ selectedTweet }}
|
||||
{{ selectedTweet.content || '' }}
|
||||
</span>
|
||||
<button
|
||||
v-if="selectedTweetId"
|
||||
@@ -89,9 +89,10 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<ReplyBox
|
||||
<reply-box
|
||||
:conversation-id="currentChat.id"
|
||||
:in-reply-to="selectedTweetId"
|
||||
:is-a-tweet="isATweet"
|
||||
:selected-tweet="selectedTweet"
|
||||
@scrollToMessage="scrollToBottom"
|
||||
/>
|
||||
</div>
|
||||
@@ -207,10 +208,10 @@ export default {
|
||||
selectedTweet() {
|
||||
if (this.selectedTweetId) {
|
||||
const { messages = [] } = this.getMessages;
|
||||
const [selectedMessage = {}] = messages.filter(
|
||||
const [selectedMessage] = messages.filter(
|
||||
message => message.id === this.selectedTweetId
|
||||
);
|
||||
return selectedMessage.content || '';
|
||||
return selectedMessage || {};
|
||||
}
|
||||
return '';
|
||||
},
|
||||
@@ -233,7 +234,7 @@ export default {
|
||||
|
||||
created() {
|
||||
bus.$on('scrollToMessage', () => {
|
||||
setTimeout(() => this.scrollToBottom(), 0);
|
||||
this.$nextTick(() => this.scrollToBottom());
|
||||
this.makeMessagesRead();
|
||||
});
|
||||
|
||||
@@ -255,7 +256,7 @@ export default {
|
||||
this.conversationPanel = this.$el.querySelector('.conversation-panel');
|
||||
this.setScrollParams();
|
||||
this.conversationPanel.addEventListener('scroll', this.handleScroll);
|
||||
this.scrollToBottom();
|
||||
this.$nextTick(() => this.scrollToBottom());
|
||||
this.isLoadingPrevious = false;
|
||||
},
|
||||
removeScrollListener() {
|
||||
|
||||
@@ -74,6 +74,7 @@
|
||||
import { mapGetters } from 'vuex';
|
||||
import { mixin as clickaway } from 'vue-clickaway';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
||||
|
||||
import EmojiInput from 'shared/components/emoji/EmojiInput';
|
||||
import CannedResponse from './CannedResponse';
|
||||
@@ -105,11 +106,21 @@ export default {
|
||||
ReplyBottomPanel,
|
||||
WootMessageEditor,
|
||||
},
|
||||
mixins: [clickaway, inboxMixin, uiSettingsMixin, alertMixin],
|
||||
mixins: [
|
||||
clickaway,
|
||||
inboxMixin,
|
||||
uiSettingsMixin,
|
||||
alertMixin,
|
||||
eventListenerMixins,
|
||||
],
|
||||
props: {
|
||||
inReplyTo: {
|
||||
type: [String, Number],
|
||||
default: '',
|
||||
selectedTweet: {
|
||||
type: [Object, String],
|
||||
default: () => ({}),
|
||||
},
|
||||
isATweet: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
@@ -169,11 +180,14 @@ export default {
|
||||
return this.maxLength - this.message.length;
|
||||
},
|
||||
isReplyButtonDisabled() {
|
||||
const isMessageEmpty = !this.message.trim().replace(/\n/g, '').length;
|
||||
if (this.isATweet && !this.inReplyTo) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.hasAttachments) return false;
|
||||
|
||||
return (
|
||||
isMessageEmpty ||
|
||||
this.isMessageEmpty ||
|
||||
this.message.length === 0 ||
|
||||
this.message.length > this.maxLength
|
||||
);
|
||||
@@ -198,7 +212,7 @@ export default {
|
||||
}
|
||||
if (this.isATwitterInbox) {
|
||||
if (this.conversationType === 'tweet') {
|
||||
return MESSAGE_MAX_LENGTH.TWEET;
|
||||
return MESSAGE_MAX_LENGTH.TWEET - this.replyToUserLength - 2;
|
||||
}
|
||||
}
|
||||
return MESSAGE_MAX_LENGTH.GENERAL;
|
||||
@@ -235,6 +249,25 @@ export default {
|
||||
isOnPrivateNote() {
|
||||
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: {
|
||||
currentChat(conversation) {
|
||||
@@ -263,12 +296,6 @@ export default {
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
document.addEventListener('keydown', this.handleKeyEvents);
|
||||
},
|
||||
destroyed() {
|
||||
document.removeEventListener('keydown', this.handleKeyEvents);
|
||||
},
|
||||
methods: {
|
||||
toggleUserMention(currentMentionState) {
|
||||
this.hasUserMention = currentMentionState;
|
||||
@@ -327,6 +354,9 @@ export default {
|
||||
if (this.showRichContentEditor) {
|
||||
return;
|
||||
}
|
||||
if (this.$refs.messageInput === undefined) {
|
||||
return;
|
||||
}
|
||||
this.$nextTick(() => this.$refs.messageInput.focus());
|
||||
},
|
||||
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"
|
||||
/>
|
||||
<i
|
||||
v-if="isATweet && isIncoming"
|
||||
v-if="isATweet && (isIncoming || isOutgoing) && sourceId"
|
||||
v-tooltip.top-start="$t('CHAT_LIST.REPLY_TO_TWEET')"
|
||||
class="icon ion-reply cursor-pointer"
|
||||
@click="onTweetReply"
|
||||
/>
|
||||
<a
|
||||
v-if="isATweet && isIncoming"
|
||||
v-if="isATweet && (isOutgoing || isIncoming) && linkToTweet"
|
||||
:href="linkToTweet"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer nofollow"
|
||||
@@ -71,19 +71,33 @@ export default {
|
||||
type: [String, Number],
|
||||
default: '',
|
||||
},
|
||||
inboxId: {
|
||||
type: [String, Number],
|
||||
default: 0,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
inbox() {
|
||||
return this.$store.getters['inboxes/getInbox'](this.inboxId);
|
||||
},
|
||||
isIncoming() {
|
||||
return MESSAGE_TYPE.INCOMING === this.messageType;
|
||||
},
|
||||
isOutgoing() {
|
||||
return MESSAGE_TYPE.OUTGOING === this.messageType;
|
||||
},
|
||||
screenName() {
|
||||
const { additional_attributes: additionalAttributes = {} } =
|
||||
this.sender || {};
|
||||
return additionalAttributes?.screen_name || '';
|
||||
},
|
||||
linkToTweet() {
|
||||
if (!this.sourceId || !this.inbox.name) {
|
||||
return '';
|
||||
}
|
||||
const { screenName, sourceId } = this;
|
||||
return `https://twitter.com/${screenName}/status/${sourceId}`;
|
||||
return `https://twitter.com/${screenName ||
|
||||
this.inbox.name}/status/${sourceId}`;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
@@ -113,6 +127,13 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
.right {
|
||||
.ion-reply,
|
||||
.ion-android-open {
|
||||
color: var(--white);
|
||||
}
|
||||
}
|
||||
|
||||
.message-text--metadata {
|
||||
align-items: flex-end;
|
||||
display: flex;
|
||||
|
||||
@@ -36,7 +36,7 @@ export default {
|
||||
methods: {
|
||||
openLink() {
|
||||
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: {
|
||||
OPEN: 'open',
|
||||
RESOLVED: 'resolved',
|
||||
BOT: 'bot',
|
||||
PENDING: 'pending',
|
||||
SNOOZED: 'snoozed',
|
||||
},
|
||||
};
|
||||
export const DEFAULT_REDIRECT_URL = '/app/';
|
||||
|
||||
@@ -47,6 +47,13 @@ export const getSidebarItems = accountId => ({
|
||||
toState: frontendURL(`accounts/${accountId}/reports`),
|
||||
toStateName: 'settings_account_reports',
|
||||
},
|
||||
campaigns: {
|
||||
icon: 'ion-speakerphone',
|
||||
label: 'CAMPAIGNS',
|
||||
hasSubMenu: false,
|
||||
toState: frontendURL(`accounts/${accountId}/campaigns`),
|
||||
toStateName: 'settings_account_campaigns',
|
||||
},
|
||||
settings: {
|
||||
icon: 'ion-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: {
|
||||
routes: [
|
||||
'agent_list',
|
||||
|
||||
@@ -273,7 +273,7 @@
|
||||
"INBOX_UPDATE_SUB_TEXT": "تحديث إعدادات قناة التواصل",
|
||||
"AUTO_ASSIGNMENT_SUB_TEXT": "تمكين أو تعطيل الإسناد التلقائي للمحادثات الجديدة إلى الموظفين المضافين إلى قناة التواصل هذه.",
|
||||
"HMAC_VERIFICATION": "التحقق من هوية المستخدم",
|
||||
"HMAC_DESCRIPTION": "للتحقق من هوية المستخدمين، يسمح لك SDK بتمرير 'identity_hash' لكل مستخدم. يمكنك إنشاء HMAC باستخدام 'sha256' مع المفتاح المعروض هنا."
|
||||
"HMAC_DESCRIPTION": "للتحقق من هوية المستخدمين، يسمح لك SDK بتمرير 'identifier_hash' لكل مستخدم. يمكنك إنشاء HMAC باستخدام 'sha256' مع المفتاح المعروض هنا."
|
||||
},
|
||||
"FACEBOOK_REAUTHORIZE": {
|
||||
"TITLE": "إعادة التصريح",
|
||||
|
||||
@@ -273,7 +273,7 @@
|
||||
"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",
|
||||
"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": {
|
||||
"TITLE": "Reautoritza",
|
||||
|
||||
@@ -273,7 +273,7 @@
|
||||
"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.",
|
||||
"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": {
|
||||
"TITLE": "Znovu autorizovat",
|
||||
|
||||
@@ -273,7 +273,7 @@
|
||||
"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.",
|
||||
"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": {
|
||||
"TITLE": "Genautorisér",
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"SEARCH_MESSAGES": "Nachrichten durchsuchen",
|
||||
"SEARCH": {
|
||||
"TITLE": "Nachrichten durchsuchen",
|
||||
"RESULT_TITLE": "Suchergebnisse",
|
||||
"LOADING_MESSAGE": "Daten werden geladen...",
|
||||
"PLACEHOLDER": "Geben Sie einen Text ein, um danach zu suchen",
|
||||
"NO_MATCHING_RESULTS": "Keine Ergebnisse gefunden."
|
||||
|
||||
@@ -273,7 +273,7 @@
|
||||
"INBOX_UPDATE_SUB_TEXT": "Posteingangseinstellungen aktualisieren",
|
||||
"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_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": {
|
||||
"TITLE": "Neu autorisieren",
|
||||
|
||||
@@ -273,7 +273,7 @@
|
||||
"INBOX_UPDATE_SUB_TEXT": "Ενημερώστε τις ρυθμίσεις του κιβωτίου σας",
|
||||
"AUTO_ASSIGNMENT_SUB_TEXT": "Ενεργοποιήστε ή απενεργοποιήστε την αυτόματη αντιστοίχιση των νέων συζητήσεων στους πράκτορες αυτού του κιβωτίου.",
|
||||
"HMAC_VERIFICATION": "Επικύρωση Ταυτότητας Χρήστη",
|
||||
"HMAC_DESCRIPTION": "Για λόγους επικύρωσης της ταυτότητας των χρηστών, το SDK σας επιτρέπει να περάσετε ένα `identity_hash` για κάθε χρήστη. Μπορείτε να δημιουργήσετε HMAC χρησιμοποιώντας το 'sha256' με το κλειδί που εμφανίζεται εδώ."
|
||||
"HMAC_DESCRIPTION": "Για λόγους επικύρωσης της ταυτότητας των χρηστών, το SDK σας επιτρέπει να περάσετε ένα `identifier_hash` για κάθε χρήστη. Μπορείτε να δημιουργήσετε HMAC χρησιμοποιώντας το 'sha256' με το κλειδί που εμφανίζεται εδώ."
|
||||
},
|
||||
"FACEBOOK_REAUTHORIZE": {
|
||||
"TITLE": "Εκ νέου εξουσιοδότηση",
|
||||
|
||||
@@ -91,6 +91,23 @@
|
||||
},
|
||||
"SEARCH": {
|
||||
"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": {
|
||||
"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.",
|
||||
"HEADER_BTN_TXT": "Create a campaign",
|
||||
"HEADER_BTN_TXT": {
|
||||
"ONE_OFF": "Create a one off campaign",
|
||||
"ONGOING": "Create a ongoing campaign"
|
||||
},
|
||||
"ADD": {
|
||||
"TITLE": "Create a campaign",
|
||||
"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",
|
||||
"ERROR": "Audience is required"
|
||||
},
|
||||
"INBOX": {
|
||||
"LABEL": "Select Inbox",
|
||||
"PLACEHOLDER": "Select Inbox",
|
||||
"ERROR": "Inbox is required"
|
||||
},
|
||||
"MESSAGE": {
|
||||
"LABEL": "Message",
|
||||
"PLACEHOLDER": "Please enter the message of campaign",
|
||||
@@ -80,6 +88,7 @@
|
||||
"TABLE_HEADER": {
|
||||
"TITLE": "Title",
|
||||
"MESSAGE": "Message",
|
||||
"INBOX": "Inbox",
|
||||
"STATUS": "Status",
|
||||
"SENDER": "Sender",
|
||||
"URL": "URL",
|
||||
@@ -101,6 +110,16 @@
|
||||
"SENDER": {
|
||||
"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"
|
||||
},
|
||||
{
|
||||
"TEXT": "Bot",
|
||||
"VALUE": "bot"
|
||||
"TEXT": "Pending",
|
||||
"VALUE": "pending"
|
||||
},
|
||||
{
|
||||
"TEXT": "Snoozed",
|
||||
"VALUE": "snoozed"
|
||||
}
|
||||
],
|
||||
"ATTACHMENTS": {
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"SEARCH_MESSAGES": "Search for messages in conversations",
|
||||
"SEARCH": {
|
||||
"TITLE": "Search messages",
|
||||
"RESULT_TITLE": "Search Results",
|
||||
"LOADING_MESSAGE": "Crunching data...",
|
||||
"PLACEHOLDER": "Type any text to search messages",
|
||||
"NO_MATCHING_RESULTS": "No results found."
|
||||
@@ -22,7 +23,7 @@
|
||||
"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_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:",
|
||||
"REMOVE_SELECTION": "Remove Selection",
|
||||
"DOWNLOAD": "Download",
|
||||
@@ -41,7 +42,13 @@
|
||||
"DETAILS": "details"
|
||||
},
|
||||
"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": {
|
||||
"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_ATTACH_ICON": "Attach files",
|
||||
"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",
|
||||
"CHANGE_STATUS": "Conversation status changed",
|
||||
@@ -123,5 +143,11 @@
|
||||
"SELECT": {
|
||||
"PLACEHOLDER": "None"
|
||||
}
|
||||
},
|
||||
"EMAIL_HEADER": {
|
||||
"TO": "To",
|
||||
"BCC": "Bcc",
|
||||
"CC": "Cc",
|
||||
"SUBJECT": "Subject"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -273,7 +273,7 @@
|
||||
"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.",
|
||||
"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": {
|
||||
"TITLE": "Reauthorize",
|
||||
|
||||
@@ -98,6 +98,7 @@
|
||||
"CHANGE_ACCOUNTS": "Switch Account",
|
||||
"SELECTOR_SUBTITLE": "Select an account from the following list",
|
||||
"PROFILE_SETTINGS": "Profile Settings",
|
||||
"KEYBOARD_SHORTCUTS": "Keyboard Shortcuts",
|
||||
"LOGOUT": "Logout"
|
||||
},
|
||||
"APP_GLOBAL": {
|
||||
@@ -126,8 +127,8 @@
|
||||
"SIDEBAR": {
|
||||
"CONVERSATIONS": "Conversations",
|
||||
"REPORTS": "Reports",
|
||||
"CONTACTS": "Contacts",
|
||||
"SETTINGS": "Settings",
|
||||
"CONTACTS": "Contacts",
|
||||
"HOME": "Home",
|
||||
"AGENTS": "Agents",
|
||||
"INBOXES": "Inboxes",
|
||||
@@ -141,9 +142,13 @@
|
||||
"ALL_CONTACTS": "All Contacts",
|
||||
"TAGGED_WITH": "Tagged with",
|
||||
"REPORTS_OVERVIEW": "Overview",
|
||||
"CSAT": "CSAT"
|
||||
"CSAT": "CSAT",
|
||||
"CAMPAIGNS": "Campaigns",
|
||||
"ONGOING": "Ongoing",
|
||||
"ONE_OFF": "One off"
|
||||
},
|
||||
"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",
|
||||
"SELECTOR_SUBTITLE": "Create a new account",
|
||||
"API": {
|
||||
@@ -158,5 +163,30 @@
|
||||
},
|
||||
"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",
|
||||
"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_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": {
|
||||
"TITLE": "Reautorizar",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"HEADER": "Edustajat",
|
||||
"HEADER_BTN_TXT": "Lisää edustaja",
|
||||
"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": {
|
||||
"ADMINISTRATOR": "Ylläpitäjä",
|
||||
"AGENT": "Edustajat"
|
||||
@@ -29,9 +29,9 @@
|
||||
"PLACEHOLDER": "Ole hyvä ja kirjoita edustajan nimi"
|
||||
},
|
||||
"AGENT_TYPE": {
|
||||
"LABEL": "Agent Type",
|
||||
"PLACEHOLDER": "Please select a type",
|
||||
"ERROR": "Agent type is required"
|
||||
"LABEL": "Edustajan tyyppi",
|
||||
"PLACEHOLDER": "Valitse tyyppi",
|
||||
"ERROR": "Edustajan tyyppi on pakollinen"
|
||||
},
|
||||
"EMAIL": {
|
||||
"LABEL": "Sähköpostiosoite",
|
||||
@@ -66,9 +66,9 @@
|
||||
"PLACEHOLDER": "Ole hyvä ja kirjoita edustajan nimi"
|
||||
},
|
||||
"AGENT_TYPE": {
|
||||
"LABEL": "Agent Type",
|
||||
"PLACEHOLDER": "Please select a type",
|
||||
"ERROR": "Agent type is required"
|
||||
"LABEL": "Edustajan tyyppi",
|
||||
"PLACEHOLDER": "Valitse edustajan tyyppi",
|
||||
"ERROR": "Edustajan tyyppi on pakollinen"
|
||||
},
|
||||
"EMAIL": {
|
||||
"LABEL": "Sähköpostiosoite",
|
||||
@@ -90,7 +90,7 @@
|
||||
}
|
||||
},
|
||||
"SEARCH": {
|
||||
"NO_RESULTS": "No results found."
|
||||
"NO_RESULTS": "Ei hakutuloksia."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,56 +1,56 @@
|
||||
{
|
||||
"CAMPAIGN": {
|
||||
"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.",
|
||||
"HEADER_BTN_TXT": "Create a campaign",
|
||||
"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 kampanja. You can also edit or delete an existing kampanja by clicking on the Edit or Delete button.",
|
||||
"HEADER_BTN_TXT": "Luo kampanja",
|
||||
"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.",
|
||||
"CANCEL_BUTTON_TEXT": "Peruuta",
|
||||
"CREATE_BUTTON_TEXT": "Luo",
|
||||
"FORM": {
|
||||
"TITLE": {
|
||||
"LABEL": "Title",
|
||||
"PLACEHOLDER": "Please enter the title of campaign",
|
||||
"ERROR": "Title is required"
|
||||
"LABEL": "Otsikko",
|
||||
"PLACEHOLDER": "Syötä kampanjan otsikko",
|
||||
"ERROR": "Otsikko on pakollinen"
|
||||
},
|
||||
"SCHEDULED_AT": {
|
||||
"LABEL": "Scheduled time",
|
||||
"PLACEHOLDER": "Please select the time",
|
||||
"CONFIRM": "Confirm",
|
||||
"ERROR": "Scheduled time is required"
|
||||
"LABEL": "Kampanjan alkamisaika",
|
||||
"PLACEHOLDER": "Syötä kampanja alkamisaika",
|
||||
"CONFIRM": "Varmista",
|
||||
"ERROR": "Alkamisaika on pakollinen"
|
||||
},
|
||||
"AUDIENCE": {
|
||||
"LABEL": "Audience",
|
||||
"PLACEHOLDER": "Select the customer labels",
|
||||
"ERROR": "Audience is required"
|
||||
"LABEL": "Kohdeyleisö",
|
||||
"PLACEHOLDER": "Valitse tunnisteet",
|
||||
"ERROR": "Kohdeyleisö on pakollinen"
|
||||
},
|
||||
"MESSAGE": {
|
||||
"LABEL": "Message",
|
||||
"PLACEHOLDER": "Please enter the message of campaign",
|
||||
"ERROR": "Message is required"
|
||||
"LABEL": "Viesti",
|
||||
"PLACEHOLDER": "Syötä kampanjan viesti",
|
||||
"ERROR": "Viesti on pakollinen"
|
||||
},
|
||||
"SENT_BY": {
|
||||
"LABEL": "Lähettäjä",
|
||||
"PLACEHOLDER": "Please select the the content of campaign",
|
||||
"ERROR": "Sender is required"
|
||||
"PLACEHOLDER": "Syötä kampanjan lähettäjä",
|
||||
"ERROR": "Lähettäjä on pakollinen"
|
||||
},
|
||||
"END_POINT": {
|
||||
"LABEL": "URL",
|
||||
"PLACEHOLDER": "Please enter the URL",
|
||||
"PLACEHOLDER": "Syötä kampanjan URL",
|
||||
"ERROR": "Anna kelvollinen URL-osoite"
|
||||
},
|
||||
"TIME_ON_PAGE": {
|
||||
"LABEL": "Time on page(Seconds)",
|
||||
"PLACEHOLDER": "Please enter the time",
|
||||
"ERROR": "Time on page is required"
|
||||
"LABEL": "Aika sivulla (sekunttia)",
|
||||
"PLACEHOLDER": "Syötä aika",
|
||||
"ERROR": "Aika sivulla on pakollinen"
|
||||
},
|
||||
"ENABLED": "Enable campaign",
|
||||
"SUBMIT": "Add Campaign"
|
||||
"ENABLED": "Aktivoi kampanja",
|
||||
"SUBMIT": "Lisää kampanja"
|
||||
},
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Campaign created successfully",
|
||||
"ERROR_MESSAGE": "There was an error. Please try again."
|
||||
"SUCCESS_MESSAGE": "Kampanja luotu onnistuneesti",
|
||||
"ERROR_MESSAGE": "Hö! Kampanjaa luodessa tapahtui virhe"
|
||||
}
|
||||
},
|
||||
"DELETE": {
|
||||
@@ -62,41 +62,41 @@
|
||||
"NO": "Ei, säilytä "
|
||||
},
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Campaign deleted successfully",
|
||||
"ERROR_MESSAGE": "Could not delete the campaign. Please try again later."
|
||||
"SUCCESS_MESSAGE": "Kampanja poistettu onnistuneesti",
|
||||
"ERROR_MESSAGE": "Hö! Kampanjaa poistaessa tapahtui virhe. Kokeile myöhemmin uudelleen"
|
||||
}
|
||||
},
|
||||
"EDIT": {
|
||||
"TITLE": "Edit campaign",
|
||||
"TITLE": "Muokkaa kampanjaa",
|
||||
"UPDATE_BUTTON_TEXT": "Päivitä",
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Campaign updated successfully",
|
||||
"ERROR_MESSAGE": "Tapahtui virhe, yritä uudelleen"
|
||||
"SUCCESS_MESSAGE": "Kampanjaa muokattu onnistuneesti",
|
||||
"ERROR_MESSAGE": "Hö! Kampanjaa muokatessa tapahtui virhe"
|
||||
}
|
||||
},
|
||||
"LIST": {
|
||||
"LOADING_MESSAGE": "Loading campaigns...",
|
||||
"404": "There are no campaigns created for this inbox.",
|
||||
"LOADING_MESSAGE": "Ladataan kampanjoita...",
|
||||
"404": "Tälle kansiolle ei ole luotu kampanjoita.",
|
||||
"TABLE_HEADER": {
|
||||
"TITLE": "Title",
|
||||
"MESSAGE": "Message",
|
||||
"TITLE": "Otsikko",
|
||||
"MESSAGE": "Viesti",
|
||||
"STATUS": "Tila",
|
||||
"SENDER": "Sender",
|
||||
"SENDER": "Lähettäjä",
|
||||
"URL": "URL",
|
||||
"SCHEDULED_AT": "Scheduled time",
|
||||
"TIME_ON_PAGE": "Time(Seconds)",
|
||||
"CREATED_AT": "Created at"
|
||||
"SCHEDULED_AT": "Aloitusaika",
|
||||
"TIME_ON_PAGE": "Aika sivulla (sekunttia)",
|
||||
"CREATED_AT": "Luotu"
|
||||
},
|
||||
"BUTTONS": {
|
||||
"ADD": "Add",
|
||||
"ADD": "Luo uusi",
|
||||
"EDIT": "Muokkaa",
|
||||
"DELETE": "Poista"
|
||||
},
|
||||
"STATUS": {
|
||||
"ENABLED": "Käytössä",
|
||||
"DISABLED": "Pois käytöstä",
|
||||
"COMPLETED": "Completed",
|
||||
"ACTIVE": "Active"
|
||||
"COMPLETED": "Suoritettu",
|
||||
"ACTIVE": "Käynnissä"
|
||||
},
|
||||
"SENDER": {
|
||||
"BOT": "Botti"
|
||||
|
||||
@@ -81,6 +81,6 @@
|
||||
"VIEW_TWEET_IN_TWITTER": "Näytä twiitti Twitterissä",
|
||||
"REPLY_TO_TWEET": "Vastaa tähän twiittiin",
|
||||
"NO_MESSAGES": "Ei Viestejä",
|
||||
"NO_CONTENT": "No content available"
|
||||
"NO_CONTENT": "Sisältöä ei saatavilla"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,17 +19,17 @@
|
||||
},
|
||||
"LABELS": {
|
||||
"CONTACT": {
|
||||
"TITLE": "Contact Labels",
|
||||
"ERROR": "Couldn't update labels"
|
||||
"TITLE": "Yhteystietotunnisteet",
|
||||
"ERROR": "Tunnisteita ei voitu päivittää"
|
||||
},
|
||||
"CONVERSATION": {
|
||||
"TITLE": "Keskustelutunnisteet",
|
||||
"ADD_BUTTON": "Add Labels"
|
||||
"ADD_BUTTON": "Lisää tunnisteita"
|
||||
},
|
||||
"LABEL_SELECT": {
|
||||
"TITLE": "Add Labels",
|
||||
"PLACEHOLDER": "Search labels",
|
||||
"NO_RESULT": "No labels found"
|
||||
"TITLE": "Lisää tunnisteita",
|
||||
"PLACEHOLDER": "Hae tunnisteita",
|
||||
"NO_RESULT": "Tunnisteita ei löytynyt"
|
||||
}
|
||||
},
|
||||
"MUTE_CONTACT": "Mykistä Keskustelu",
|
||||
@@ -71,8 +71,8 @@
|
||||
"PHONE_NUMBER": {
|
||||
"PLACEHOLDER": "Anna yhteystiedon puhelinnumero",
|
||||
"LABEL": "Puhelinnumero",
|
||||
"HELP": "Phone number should be of E.164 format eg: +1415555555 [+][country code][area code][local phone number]",
|
||||
"ERROR": "Phone number should be either empty or of E.164 format"
|
||||
"HELP": "Puhelinnumeron tulee olla E.164 formaattia, esim: +1415555555 [+][maakoodi][aluekoodi][puhelinnumero]",
|
||||
"ERROR": "Puhelinnumeron tulee olla tyhjä tai E.164 formaattia"
|
||||
},
|
||||
"LOCATION": {
|
||||
"PLACEHOLDER": "Anna yhteystiedon sijainti",
|
||||
@@ -109,129 +109,129 @@
|
||||
"BUTTON_LABEL": "Aloita keskustelu",
|
||||
"TITLE": "Uusi keskustelu",
|
||||
"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": {
|
||||
"TO": {
|
||||
"LABEL": "To"
|
||||
"LABEL": "Kohde"
|
||||
},
|
||||
"INBOX": {
|
||||
"LABEL": "Inbox",
|
||||
"ERROR": "Select an inbox"
|
||||
"LABEL": "Kansio",
|
||||
"ERROR": "Valitse kansio"
|
||||
},
|
||||
"MESSAGE": {
|
||||
"LABEL": "Message",
|
||||
"PLACEHOLDER": "Write your message here",
|
||||
"ERROR": "Message can't be empty"
|
||||
"LABEL": "Viesti",
|
||||
"PLACEHOLDER": "Kirjoita viestisi tähän",
|
||||
"ERROR": "Viesti ei voi olla tyhjä"
|
||||
},
|
||||
"SUBMIT": "Send message",
|
||||
"SUBMIT": "Lähetä viesti",
|
||||
"CANCEL": "Peruuta",
|
||||
"SUCCESS_MESSAGE": "Message sent!",
|
||||
"ERROR_MESSAGE": "Couldn't send! try again"
|
||||
"SUCCESS_MESSAGE": "Viesti lähetetty!",
|
||||
"ERROR_MESSAGE": "Hö! Virhe, yritä uudelleen"
|
||||
}
|
||||
},
|
||||
"CONTACTS_PAGE": {
|
||||
"HEADER": "Yhteystiedot",
|
||||
"FIELDS": "Contact fields",
|
||||
"FIELDS": "Yhteystietokentät",
|
||||
"SEARCH_BUTTON": "Etsi",
|
||||
"SEARCH_INPUT_PLACEHOLDER": "Etsi yhteystietoja",
|
||||
"LIST": {
|
||||
"LOADING_MESSAGE": "Ladataan yhteystietoja...",
|
||||
"404": "Ei hakua vastaavia yhteystietoja 🔍",
|
||||
"NO_CONTACTS": "There are no available contacts",
|
||||
"NO_CONTACTS": "Yhtään kontaktia ei löytynyt.",
|
||||
"TABLE_HEADER": {
|
||||
"NAME": "Nimi",
|
||||
"PHONE_NUMBER": "Puhelinnumero",
|
||||
"CONVERSATIONS": "Keskustelut",
|
||||
"LAST_ACTIVITY": "Last Activity",
|
||||
"COUNTRY": "Country",
|
||||
"CITY": "City",
|
||||
"SOCIAL_PROFILES": "Social Profiles",
|
||||
"LAST_ACTIVITY": "Viimeksi aktiivinen",
|
||||
"COUNTRY": "Maa",
|
||||
"CITY": "Kaupunki",
|
||||
"SOCIAL_PROFILES": "Sosiaalisen median tilit",
|
||||
"COMPANY": "Yritys",
|
||||
"EMAIL_ADDRESS": "Sähköpostiosoite"
|
||||
},
|
||||
"VIEW_DETAILS": "View details"
|
||||
"VIEW_DETAILS": "Katso lisätietoja"
|
||||
}
|
||||
},
|
||||
"REMINDER": {
|
||||
"ADD_BUTTON": {
|
||||
"BUTTON": "Add",
|
||||
"TITLE": "Shift + Enter to create a task"
|
||||
"BUTTON": "Lisää",
|
||||
"TITLE": "Shift + Enter luodaksesi tehtävän"
|
||||
},
|
||||
"FOOTER": {
|
||||
"DUE_DATE": "Due date",
|
||||
"LABEL_TITLE": "Set type"
|
||||
"DUE_DATE": "Eräpäivä",
|
||||
"LABEL_TITLE": "Aseta tyyppi"
|
||||
}
|
||||
},
|
||||
"NOTES": {
|
||||
"HEADER": {
|
||||
"TITLE": "Notes"
|
||||
"TITLE": "Muistiinpanot"
|
||||
},
|
||||
"ADD": {
|
||||
"BUTTON": "Add",
|
||||
"PLACEHOLDER": "Add a note",
|
||||
"TITLE": "Shift + Enter to create a note"
|
||||
"BUTTON": "Lisää",
|
||||
"PLACEHOLDER": "Lisää muistiinpano",
|
||||
"TITLE": "Shift + Enter luodaksesi muistiinpano"
|
||||
},
|
||||
"FOOTER": {
|
||||
"BUTTON": "View all notes"
|
||||
"BUTTON": "Näe kaikki muistiinpanot"
|
||||
}
|
||||
},
|
||||
"EVENTS": {
|
||||
"HEADER": {
|
||||
"TITLE": "Activities"
|
||||
"TITLE": "Aktiviteetit"
|
||||
},
|
||||
"BUTTON": {
|
||||
"PILL_BUTTON_NOTES": "notes",
|
||||
"PILL_BUTTON_EVENTS": "events",
|
||||
"PILL_BUTTON_NOTES": "muistiinpanot",
|
||||
"PILL_BUTTON_EVENTS": "tapahtumat",
|
||||
"PILL_BUTTON_CONVO": "keskustelut"
|
||||
}
|
||||
},
|
||||
"CUSTOM_ATTRIBUTES": {
|
||||
"TITLE": "Mukautetut attribuutit",
|
||||
"BUTTON": "Add custom attribute",
|
||||
"BUTTON": "Lisää kustomoitu attribuutti",
|
||||
"ADD": {
|
||||
"TITLE": "Create custom attribute",
|
||||
"DESC": "Add custom information to this contact."
|
||||
"TITLE": "Lisää kustomoitu attribuutti",
|
||||
"DESC": "Lisää lisätietoja tähän kontaktiin."
|
||||
},
|
||||
"FORM": {
|
||||
"CREATE": "Add attribute",
|
||||
"CREATE": "Lisää atribuutti",
|
||||
"CANCEL": "Peruuta",
|
||||
"NAME": {
|
||||
"LABEL": "Custom attribute name",
|
||||
"PLACEHOLDER": "Eg: shopify id",
|
||||
"ERROR": "Invalid custom attribute name"
|
||||
"LABEL": "Attribuutin nimi",
|
||||
"PLACEHOLDER": "Esim: shopify id",
|
||||
"ERROR": "Virheellinen nimi"
|
||||
},
|
||||
"VALUE": {
|
||||
"LABEL": "Attribute value",
|
||||
"PLACEHOLDER": "Eg: 11901 "
|
||||
"LABEL": "Attribuutin arvo",
|
||||
"PLACEHOLDER": "Esim: 11901 "
|
||||
}
|
||||
}
|
||||
},
|
||||
"MERGE_CONTACTS": {
|
||||
"TITLE": "Merge contacts",
|
||||
"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.",
|
||||
"TITLE": "Yhdistä kontakteja",
|
||||
"DESCRIPTION": "Yhdistää kaksi kontakia. Ensimmäinen kontaktin tiedot pysyvät koskemattomina, mutta toisen attribuutit kopioidaan ensimmäiseen kustomoituina attribuutteina.",
|
||||
"PRIMARY": {
|
||||
"TITLE": "Primary contact"
|
||||
"TITLE": "Ensimmäinen kontakti"
|
||||
},
|
||||
"CHILD": {
|
||||
"TITLE": "Contact to merge",
|
||||
"PLACEHOLDER": "Choose a contact"
|
||||
"TITLE": "Yhdistettävä kontakti",
|
||||
"PLACEHOLDER": "Valitse kontakti"
|
||||
},
|
||||
"SUMMARY": {
|
||||
"TITLE": "Summary",
|
||||
"DELETE_WARNING": "Contact of <strong>%{childContactName}</strong>will be deleted.",
|
||||
"ATTRIBUTE_WARNING": "Contact details of <strong>%{childContactName}</strong> will be copied to <strong>%{primaryContactName}</strong>."
|
||||
"TITLE": "Yhteenveto",
|
||||
"DELETE_WARNING": "Kontakti <strong>%{childContactName}</strong> poistetaan.",
|
||||
"ATTRIBUTE_WARNING": "Kontaktin <strong>%{childContactName}</strong> tiedot kopioidaan kontaktiin <strong>%{primaryContactName}</strong>."
|
||||
},
|
||||
"SEARCH": {
|
||||
"ERROR": "ERROR_MESSAGE"
|
||||
},
|
||||
"FORM": {
|
||||
"SUBMIT": " Merge contacts",
|
||||
"SUBMIT": " Yhdistä",
|
||||
"CANCEL": "Peruuta",
|
||||
"CHILD_CONTACT": {
|
||||
"ERROR": "Select a child contact to merge"
|
||||
"ERROR": "Valitse yhdistettävä tili"
|
||||
},
|
||||
"SUCCESS_MESSAGE": "Contact merged successfully",
|
||||
"ERROR_MESSAGE": "Could not merge contcts, try again!"
|
||||
"SUCCESS_MESSAGE": "Kontaktien yhdistäminen onnistui",
|
||||
"ERROR_MESSAGE": "Virhe, yritä uudelleen!"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,11 +27,11 @@
|
||||
"REMOVE_SELECTION": "Poista valinnat",
|
||||
"DOWNLOAD": "Lataa",
|
||||
"UPLOADING_ATTACHMENTS": "Ladataan liitteitä...",
|
||||
"SUCCESS_DELETE_MESSAGE": "Message deleted successfully",
|
||||
"FAIL_DELETE_MESSSAGE": "Couldn't delete message! Try again",
|
||||
"NO_RESPONSE": "No response",
|
||||
"RATING_TITLE": "Rating",
|
||||
"FEEDBACK_TITLE": "Feedback",
|
||||
"SUCCESS_DELETE_MESSAGE": "Viesti poistettu onnistuneesti",
|
||||
"FAIL_DELETE_MESSSAGE": "Viestin poistaminen epäonnistui.",
|
||||
"NO_RESPONSE": "Ei vastausta",
|
||||
"RATING_TITLE": "Arvostelut",
|
||||
"FEEDBACK_TITLE": "Palaute",
|
||||
"HEADER": {
|
||||
"RESOLVE_ACTION": "Ratkaise",
|
||||
"REOPEN_ACTION": "Uudelleenavaa",
|
||||
@@ -41,7 +41,7 @@
|
||||
"DETAILS": "tiedot"
|
||||
},
|
||||
"RESOLVE_DROPDOWN": {
|
||||
"OPEN_BOT": "Open with bot"
|
||||
"OPEN_BOT": "Avaa botilla"
|
||||
},
|
||||
"FOOTER": {
|
||||
"MSG_INPUT": "Vaihto + enter siirtyäksesi uudelle riville. Aloita '/' valitaksesi tallennettu vastaus.",
|
||||
@@ -53,17 +53,17 @@
|
||||
"SEND": "Lähetä",
|
||||
"CREATE": "Lisää muistiinpano",
|
||||
"TWEET": "Twiittaa",
|
||||
"TIP_FORMAT_ICON": "Show rich text editor",
|
||||
"TIP_EMOJI_ICON": "Show emoji selector",
|
||||
"TIP_ATTACH_ICON": "Attach files",
|
||||
"ENTER_TO_SEND": "Enter to send",
|
||||
"DRAG_DROP": "Drag and drop here to attach"
|
||||
"TIP_FORMAT_ICON": "Näytä rikas tekstieditori",
|
||||
"TIP_EMOJI_ICON": "Näytä emojivalitsin",
|
||||
"TIP_ATTACH_ICON": "Liitä tiedosto",
|
||||
"ENTER_TO_SEND": "Paina Enter lähettääksesi",
|
||||
"DRAG_DROP": "Pudota tiedosto tähän liittääksesi"
|
||||
},
|
||||
"VISIBLE_TO_AGENTS": "Yksityinen huomautus: Näkyy vain sinulle ja tiimillesi",
|
||||
"CHANGE_STATUS": "Keskustelun tila muutettu",
|
||||
"CHANGE_AGENT": "Keskustelun vastaanottaja vaihdettu",
|
||||
"CHANGE_TEAM": "Conversation team changed",
|
||||
"FILE_SIZE_LIMIT": "File exceeds the {MAXIMUM_FILE_UPLOAD_SIZE} attachment limit",
|
||||
"CHANGE_TEAM": "Keskustelun tiimi vaihdettu",
|
||||
"FILE_SIZE_LIMIT": "Tiedoston koko ylittää maksimin: {MAXIMUM_FILE_UPLOAD_SIZE} ",
|
||||
"SENT_BY": "Lähettäjä:",
|
||||
"ASSIGNMENT": {
|
||||
"SELECT_AGENT": "Valitse edustaja",
|
||||
@@ -93,35 +93,35 @@
|
||||
}
|
||||
},
|
||||
"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.",
|
||||
"READ_LATEST_UPDATES": "Read our latest updates",
|
||||
"ALL_CONVERSATION": {
|
||||
"TITLE": "All your conversations in one place",
|
||||
"DESCRIPTION": "View all the conversations from your customers in one single dashboard. You can filter the conversations by the incoming channel, label and status."
|
||||
"TITLE": "Kaikki keskustelut yhdessä paikassa",
|
||||
"DESCRIPTION": "Kaikki asiakkaiden viestit yhdessä ohjauspaneelissa. Voit filtteröidä viestejä tunnisteen, kansion ja tiimin perusteella."
|
||||
},
|
||||
"TEAM_MEMBERS": {
|
||||
"TITLE": "Invite your team members",
|
||||
"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.",
|
||||
"NEW_LINK": "Click here to invite a team member"
|
||||
"TITLE": "Kutsu tiimijäseniä",
|
||||
"DESCRIPTION": "Koska olet valmis palvelemaan asiakkaita, kutsu muutama tiimijäsentä auttamaan sinua!",
|
||||
"NEW_LINK": "Paina tästä kutsuaksesi tiimijäsen"
|
||||
},
|
||||
"INBOXES": {
|
||||
"TITLE": "Connect Inboxes",
|
||||
"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.",
|
||||
"NEW_LINK": "Click here to create an inbox"
|
||||
"TITLE": "Yhdistä kansioita",
|
||||
"DESCRIPTION": "Yhdistä monta eri viestikanavaa jolla asiakkaat voivat ottaa sinun yhteyden. Esimerkiksi widgetti omille verkkosivuille, Twitterissä tai Whatsapissa.",
|
||||
"NEW_LINK": "Paina tästä luodaksesi kansio"
|
||||
},
|
||||
"LABELS": {
|
||||
"TITLE": "Organize conversations with labels",
|
||||
"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.",
|
||||
"NEW_LINK": "Click here to create tags"
|
||||
"TITLE": "Järjestä keskusteluja tunnisteilla",
|
||||
"DESCRIPTION": "Tunnisteet auttavat sinua järjestelemään eri keskusteluja.",
|
||||
"NEW_LINK": "Paina tästä luodaksesi uusi tunniste"
|
||||
}
|
||||
},
|
||||
"CONVERSATION_SIDEBAR": {
|
||||
"ASSIGNEE_LABEL": "Assigned Agent",
|
||||
"SELF_ASSIGN": "Assign to me",
|
||||
"TEAM_LABEL": "Assigned Team",
|
||||
"ASSIGNEE_LABEL": "Määrätty edustaja",
|
||||
"SELF_ASSIGN": "Määrätty minulle",
|
||||
"TEAM_LABEL": "Määrätty tiimi",
|
||||
"SELECT": {
|
||||
"PLACEHOLDER": "None"
|
||||
"PLACEHOLDER": "Ei mikään"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"CSAT": {
|
||||
"TITLE": "Rate your conversation",
|
||||
"PLACEHOLDER": "Tell us more..."
|
||||
"TITLE": "Arvostele keskustelu",
|
||||
"PLACEHOLDER": "Kerro meille lisää"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
"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": {
|
||||
"MULTISELECT": {
|
||||
@@ -54,10 +54,10 @@
|
||||
},
|
||||
"NOTIFICATIONS_PAGE": {
|
||||
"HEADER": "Ilmoitukset",
|
||||
"MARK_ALL_DONE": "Mark All Done",
|
||||
"MARK_ALL_DONE": "Merkkaa kaikki luetuksi",
|
||||
"LIST": {
|
||||
"LOADING_MESSAGE": "Loading notifications...",
|
||||
"404": "No Notifications",
|
||||
"LOADING_MESSAGE": "Ladataan ilmoituksia...",
|
||||
"404": "Ei ilmoituksia",
|
||||
"TABLE_HEADER": [
|
||||
"Nimi",
|
||||
"Puhelinnumero",
|
||||
@@ -66,10 +66,10 @@
|
||||
]
|
||||
},
|
||||
"TYPE_LABEL": {
|
||||
"conversation_creation": "New conversation",
|
||||
"conversation_assignment": "Conversation Assigned",
|
||||
"assigned_conversation_new_message": "New Message",
|
||||
"conversation_mention": "Mention"
|
||||
"conversation_creation": "Uusi keskustelu",
|
||||
"conversation_assignment": "Keskustelu määrätty",
|
||||
"assigned_conversation_new_message": "Uusi viesti",
|
||||
"conversation_mention": "Mainitse"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"INBOX_MGMT": {
|
||||
"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": {
|
||||
"404": "Tähän tiliin ei ole liitetty saapuneet-kansiota."
|
||||
},
|
||||
@@ -30,7 +30,7 @@
|
||||
"ADD": {
|
||||
"CHANNEL_NAME": {
|
||||
"LABEL": "Kansion nimi",
|
||||
"PLACEHOLDER": "Enter your inbox name (eg: Acme Inc)"
|
||||
"PLACEHOLDER": "Valitse kansion nimi (esim: Acme Oy)"
|
||||
},
|
||||
"WEBSITE_NAME": {
|
||||
"LABEL": "Sivuston nimi",
|
||||
@@ -47,7 +47,7 @@
|
||||
},
|
||||
"TWITTER": {
|
||||
"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": {
|
||||
"TITLE": "Sivuston chat",
|
||||
@@ -83,7 +83,7 @@
|
||||
"IN_A_FEW_MINUTES": "Muutamassa minuutissa",
|
||||
"IN_A_FEW_HOURS": "Muutamassa tunnissa",
|
||||
"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": {
|
||||
"LABEL": "Widgetin väri",
|
||||
@@ -128,12 +128,12 @@
|
||||
}
|
||||
},
|
||||
"SMS": {
|
||||
"TITLE": "SMS Channel via Twilio",
|
||||
"DESC": "Start supporting your customers via SMS with Twilio integration."
|
||||
"TITLE": "SMS Twilio:n kautta",
|
||||
"DESC": "Ala palvelemaan asiakkaitasi tekstiviestien avulla käyttäen Twilio-alustaa"
|
||||
},
|
||||
"WHATSAPP": {
|
||||
"TITLE": "Whatsapp Channel via Twilio",
|
||||
"DESC": "Start supporting your customers via Whatsapp with Twilio integration."
|
||||
"TITLE": "Whatsapp Twilio:n kautta",
|
||||
"DESC": "Ala palvelemaan asiakkaitasi Whatsapp-viestien avulla käyttäen Twilio-alustaa"
|
||||
},
|
||||
"API_CHANNEL": {
|
||||
"TITLE": "API-rajapinta",
|
||||
@@ -173,8 +173,8 @@
|
||||
"FINISH_MESSAGE": "Aloita välittämällä sähköpostit seuraavaan osoitteeseen."
|
||||
},
|
||||
"AUTH": {
|
||||
"TITLE": "Choose a channel",
|
||||
"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."
|
||||
"TITLE": "Valitse kanava",
|
||||
"DESC": "Chatwoot tukee live-chat widgettiä, Facebook-sivua, Twitter-profiilia, Whatsappia, Sähköpostia jne., as channels. Voit luoda oman kanavan API-rajapinnalla."
|
||||
},
|
||||
"AGENTS": {
|
||||
"TITLE": "Edustajat",
|
||||
@@ -264,16 +264,16 @@
|
||||
"INBOX_AGENTS": "Edustajat",
|
||||
"INBOX_AGENTS_SUB_TEXT": "Lisää tai poista edustajia tästä saapuneet-kansiosta",
|
||||
"UPDATE": "Päivitä",
|
||||
"ENABLE_EMAIL_COLLECT_BOX": "Enable email collect box",
|
||||
"ENABLE_EMAIL_COLLECT_BOX_SUB_TEXT": "Enable or disable email collect box on new conversation",
|
||||
"ENABLE_EMAIL_COLLECT_BOX": "Aktivoi sähköpostilaatikko",
|
||||
"ENABLE_EMAIL_COLLECT_BOX_SUB_TEXT": "Aktivoi tai disabloi sähköpostilaatikko uusissa keskusteluissa",
|
||||
"AUTO_ASSIGNMENT": "Ota automaattinen delegointi käyttöön",
|
||||
"ENABLE_CSAT": "Enable CSAT",
|
||||
"ENABLE_CSAT_SUB_TEXT": "Enable/Disable CSAT(Customer satisfaction) survey after resolving a conversation",
|
||||
"ENABLE_CSAT": "Aktivoi ASTK",
|
||||
"ENABLE_CSAT_SUB_TEXT": "Aktivoi/Disabloi ASTK(ASiakasTyytyväisyysKysely) keskustelun ratkaisun jälkeen",
|
||||
"INBOX_UPDATE_TITLE": "Postilaatikon tiedot",
|
||||
"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.",
|
||||
"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_VERIFICATION": "Käyttäjän identiteettivarmistus",
|
||||
"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": {
|
||||
"TITLE": "Uudelleenvaltuuta",
|
||||
@@ -282,38 +282,38 @@
|
||||
"MESSAGE_ERROR": "Tapahtui virhe, yritä uudelleen"
|
||||
},
|
||||
"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": {
|
||||
"LABEL": "Enable pre chat form",
|
||||
"LABEL": "Aktivoi pikakysely",
|
||||
"OPTIONS": {
|
||||
"ENABLED": "Yes",
|
||||
"DISABLED": "No"
|
||||
"ENABLED": "Kyllä",
|
||||
"DISABLED": "Ei"
|
||||
}
|
||||
},
|
||||
"PRE_CHAT_MESSAGE": {
|
||||
"LABEL": "Pre Chat Message",
|
||||
"PLACEHOLDER": "This message would be visible to the users along with the form"
|
||||
"LABEL": "Pikakyselyviesti",
|
||||
"PLACEHOLDER": "Tämä viesti näytetään käyttäjille joille pikakysely näytetään"
|
||||
},
|
||||
"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": {
|
||||
"TITLE": "Set your availability",
|
||||
"SUBTITLE": "Set your availability on your livechat widget",
|
||||
"WEEKLY_TITLE": "Set your weekly hours",
|
||||
"TIMEZONE_LABEL": "Select timezone",
|
||||
"UPDATE": "Update business hours settings",
|
||||
"TOGGLE_AVAILABILITY": "Enable business availability for this inbox",
|
||||
"UNAVAILABLE_MESSAGE_LABEL": "Unavailable message for vistors",
|
||||
"UNAVAILABLE_MESSAGE_DEFAULT": "We are unavailable at the moment. Leave a message we will respond once we are back.",
|
||||
"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.",
|
||||
"TITLE": "Aseta tilasi",
|
||||
"SUBTITLE": "Aseta tilasi livechat widgetissä",
|
||||
"WEEKLY_TITLE": "Aseta viikkotyöaikasi",
|
||||
"TIMEZONE_LABEL": "Aseta aikavyöhyke",
|
||||
"UPDATE": "Aseta työaika",
|
||||
"TOGGLE_AVAILABILITY": "Aktivoi työaikasaatavuus tälle kansiolle",
|
||||
"UNAVAILABLE_MESSAGE_LABEL": "Ei saatavilla-viesti käyttäjille",
|
||||
"UNAVAILABLE_MESSAGE_DEFAULT": "Emme ole saatavilla juuri nyt. Jätä viesti ja vastaamme heti kun pystymme.",
|
||||
"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": {
|
||||
"ENABLE": "Enable availability for this day",
|
||||
"UNAVAILABLE": "Unavailable",
|
||||
"HOURS": "hours",
|
||||
"VALIDATION_ERROR": "Starting time should be before closing time.",
|
||||
"CHOOSE": "Choose"
|
||||
"ENABLE": "Aktivoi saatavuus tälle päivälle",
|
||||
"UNAVAILABLE": "Ei saatavilla",
|
||||
"HOURS": "tuntia",
|
||||
"VALIDATION_ERROR": "Avaamisajan tulisi olla ennen sulkemisaikaa.",
|
||||
"CHOOSE": "Valitse"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,36 +1,36 @@
|
||||
{
|
||||
"INTEGRATION_APPS": {
|
||||
"FETCHING": "Fetching Integrations",
|
||||
"NO_HOOK_CONFIGURED": "There are no %{integrationId} integrations configured in this account.",
|
||||
"HEADER": "Applications",
|
||||
"FETCHING": "Haetaan integraatioita",
|
||||
"NO_HOOK_CONFIGURED": "Integraatiota IDllä %{integrationId} ei löytynyt tai ei ole konfiguroitu.",
|
||||
"HEADER": "Applikaatiot",
|
||||
"STATUS": {
|
||||
"ENABLED": "Käytössä",
|
||||
"DISABLED": "Pois käytöstä"
|
||||
},
|
||||
"CONFIGURE": "Määrittele",
|
||||
"ADD_BUTTON": "Add a new hook",
|
||||
"ADD_BUTTON": "Lisää uusi webhook",
|
||||
"DELETE": {
|
||||
"TITLE": {
|
||||
"INBOX": "Confirm deletion",
|
||||
"ACCOUNT": "Disconnect"
|
||||
"INBOX": "Varmista poisto",
|
||||
"ACCOUNT": "Katkaise"
|
||||
},
|
||||
"MESSAGE": {
|
||||
"INBOX": "Oletko varma että haluat poistaa?",
|
||||
"ACCOUNT": "Are you sure to disconnect?"
|
||||
"ACCOUNT": "Oletko varma että haluat katkaista?"
|
||||
},
|
||||
"CONFIRM_BUTTON_TEXT": {
|
||||
"INBOX": "Kyllä, poista",
|
||||
"ACCOUNT": "Yes, Disconnect"
|
||||
"ACCOUNT": "Kyllä, katkaise"
|
||||
},
|
||||
"CANCEL_BUTTON_TEXT": "Peruuta",
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Hook deleted successfully",
|
||||
"SUCCESS_MESSAGE": "Webhook poistettu onnistuneesti",
|
||||
"ERROR_MESSAGE": "Yhteyden muodostaminen Woot-palvelimelle ei onnistunut, yritä myöhemmin uudelleen"
|
||||
}
|
||||
},
|
||||
"LIST": {
|
||||
"FETCHING": "Fetching integration hooks",
|
||||
"INBOX": "Inbox",
|
||||
"FETCHING": "Haetaan integraatiowebhookkeja",
|
||||
"INBOX": "Kansio",
|
||||
"DELETE": {
|
||||
"BUTTON_TEXT": "Poista"
|
||||
}
|
||||
@@ -38,14 +38,14 @@
|
||||
"ADD": {
|
||||
"FORM": {
|
||||
"INBOX": {
|
||||
"LABEL": "Select Inbox",
|
||||
"PLACEHOLDER": "Select Inbox"
|
||||
"LABEL": "Valitse kansio",
|
||||
"PLACEHOLDER": "Valitse kansio"
|
||||
},
|
||||
"SUBMIT": "Luo",
|
||||
"CANCEL": "Peruuta"
|
||||
},
|
||||
"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"
|
||||
}
|
||||
},
|
||||
@@ -53,7 +53,7 @@
|
||||
"BUTTON_TEXT": "Yhdistä"
|
||||
},
|
||||
"DISCONNECT": {
|
||||
"BUTTON_TEXT": "Disconnect"
|
||||
"BUTTON_TEXT": "Katkaise"
|
||||
},
|
||||
"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."
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"PLACEHOLDER": "Salasana"
|
||||
},
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Kirjautuminen Onnistui",
|
||||
"SUCCESS_MESSAGE": "Kirjautuminen onnistui",
|
||||
"ERROR_MESSAGE": "Yhteyden muodostaminen Woot-palvelimelle ei onnistunut, yritä myöhemmin 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
Reference in New Issue
Block a user