Merge branch 'release/1.6.0'

This commit is contained in:
Sojan 2020-07-08 01:00:45 +05:30
commit d03fad3e1d
827 changed files with 26325 additions and 3033 deletions

View file

@ -16,7 +16,7 @@ defaults: &defaults
- image: circleci/redis:alpine
environment:
- CC_TEST_REPORTER_ID: b1b5c4447bf93f6f0b06a64756e35afd0810ea83649f03971cbf303b4449456f
- RAILS_LOG_TO_STDOUT: false
jobs:
build:
<<: *defaults
@ -69,11 +69,11 @@ jobs:
- run:
name: Download cc-test-reporter
command: |
mkdir -p tmp/
curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./tmp/cc-test-reporter
chmod +x ./tmp/cc-test-reporter
mkdir -p ~/tmp
curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ~/tmp/cc-test-reporter
chmod +x ~/tmp/cc-test-reporter
- persist_to_workspace:
root: tmp
root: ~/tmp
paths:
- cc-test-reporter
@ -98,10 +98,10 @@ jobs:
- run:
name: Run backend tests
command: |
bundle exec rspec $(circleci tests glob "spec/**/*_spec.rb" | circleci tests split --split-by=timings)
./tmp/cc-test-reporter format-coverage -t simplecov -o tmp/codeclimate.backend.json coverage/backend/.resultset.json
bundle exec rspec $(circleci tests glob "spec/**/*_spec.rb" | circleci tests split --split-by=timings) --profile=10
~/tmp/cc-test-reporter format-coverage -t simplecov -o ~/tmp/codeclimate.backend.json coverage/backend/.resultset.json
- persist_to_workspace:
root: tmp
root: ~/tmp
paths:
- codeclimate.backend.json
@ -109,21 +109,23 @@ jobs:
name: Run frontend tests
command: |
yarn test:coverage
./tmp/cc-test-reporter format-coverage -t lcov -o tmp/codeclimate.frontend.json buildreports/lcov.info
~/tmp/cc-test-reporter format-coverage -t lcov -o ~/tmp/codeclimate.frontend.json buildreports/lcov.info
- persist_to_workspace:
root: tmp
root: ~/tmp
paths:
- codeclimate.frontend.json
# collect reports
- store_test_results:
path: /tmp/test-results
path: ~/tmp/test-results
- store_artifacts:
path: /tmp/test-results
path: ~/tmp/test-results
destination: test-results
- store_artifacts:
path: log
- run:
name: Upload coverage results to Code Climate
command: |
./tmp/cc-test-reporter sum-coverage tmp/codeclimate.*.json -p 2 -o tmp/codeclimate.total.json
./tmp/cc-test-reporter upload-coverage -i tmp/codeclimate.total.json
~/tmp/cc-test-reporter sum-coverage ~/tmp/codeclimate.*.json -p 2 -o ~/tmp/codeclimate.total.json
~/tmp/cc-test-reporter upload-coverage -i ~/tmp/codeclimate.total.json

View file

@ -59,6 +59,7 @@ MANDRILL_INGRESS_API_KEY=
ACTIVE_STORAGE_SERVICE=local
# Amazon S3
# documentation: https://www.chatwoot.com/docs/configuring-s3-bucket-as-cloud-storage
S3_BUCKET_NAME=
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
@ -74,34 +75,35 @@ LOG_LEVEL=info
LOG_SIZE=500
### This environment variables are only required if you are setting up social media channels
#facebook
# Facebook
# documentation: https://www.chatwoot.com/docs/facebook-setup
FB_VERIFY_TOKEN=
FB_APP_SECRET=
FB_APP_ID=
# Twitter
# documentation: https://www.chatwoot.com/docs/twitter-app-setup
TWITTER_APP_ID=
TWITTER_CONSUMER_KEY=
TWITTER_CONSUMER_SECRET=
TWITTER_ENVIRONMENT=
#slack integration
SLACK_CLIENT_ID=
SLACK_CLIENT_SECRET=
### Change this env variable only if you are using a custom build mobile app
## Mobile app env variables
IOS_APP_ID=6C953F3RX2.com.chatwoot.app
#### This environment variables are only required in hosted version which has billing
ENABLE_BILLING=
## chargebee settings
CHARGEBEE_API_KEY=
CHARGEBEE_SITE=
CHARGEBEE_WEBHOOK_USERNAME=
CHARGEBEE_WEBHOOK_PASSWORD=
## Push Notification
## generate a new key value here : https://d3v.one/vapid-key-generator/
# VAPID_PUBLIC_KEY=
# VAPID_PRIVATE_KEY=
#
# for mobile apps
# FCM_SERVER_KEY=
## Bot Customizations
USE_INBOX_AVATAR_FOR_BOT=true

1
.github/FUNDING.yml vendored
View file

@ -1 +1,2 @@
open_collective: chatwoot
github: chatwoot

View file

@ -8,8 +8,17 @@ Lint/RaiseException:
Enabled: true
Lint/StructNewOverride:
Enabled: true
Lint/DeprecatedOpenSSLConstant:
Enabled: true
Lint/MixedRegexpCaptureTypes:
Enabled: true
Layout/LineLength:
Max: 150
Layout/EmptyLinesAroundAttributeAccessor:
Enabled: true
Layout/SpaceAroundMethodCallOperator:
Enabled: true
Metrics/ClassLength:
Max: 125
Exclude:
@ -18,6 +27,8 @@ RSpec/ExampleLength:
Max: 25
Style/Documentation:
Enabled: false
Style/ExponentialNotation:
Enabled: false
Style/FrozenStringLiteralComment:
Enabled: false
Style/SymbolArray:
@ -28,6 +39,14 @@ Style/HashTransformKeys:
Enabled: true
Style/HashTransformValues:
Enabled: true
Style/RedundantFetchBlock:
Enabled: true
Style/RedundantRegexpCharacterClass:
Enabled: true
Style/RedundantRegexpEscape:
Enabled: true
Style/SlicingWithRange:
Enabled: true
Style/GlobalVars:
Exclude:
- 'config/initializers/redis.rb'
@ -65,7 +84,6 @@ Style/GuardClause:
- 'app/builders/account_builder.rb'
- 'app/models/attachment.rb'
- 'app/models/message.rb'
- 'lib/webhooks/chargebee.rb'
- 'db/migrate/20190819005836_add_missing_indexes_on_taggings.acts_as_taggable_on_engine.rb'
Metrics/AbcSize:
Exclude:
@ -103,8 +121,8 @@ AllCops:
Exclude:
- 'bin/**/*'
- 'db/schema.rb'
- 'config/**/*'
- 'public/**/*'
- 'config/initializers/bot.rb'
- 'vendor/**/*'
- 'node_modules/**/*'
- 'lib/tasks/auto_annotate_models.rake'

View file

@ -88,7 +88,6 @@ Naming/MemoizedInstanceVariableName:
- 'app/controllers/application_controller.rb'
- 'app/models/message.rb'
- 'lib/integrations/widget/outgoing_message_builder.rb'
- 'lib/webhooks/chargebee.rb'
# Offense count: 4
# Cop supports --auto-correct.
@ -187,7 +186,6 @@ Rails/EnumHash:
- 'app/models/attachment.rb'
- 'app/models/conversation.rb'
- 'app/models/message.rb'
- 'app/models/subscription.rb'
- 'app/models/user.rb'
# Offense count: 1
@ -226,7 +224,6 @@ Rails/Output:
Rails/TimeZone:
Exclude:
- 'app/builders/report_builder.rb'
- 'app/models/subscription.rb'
- 'lib/reports/update_account_identity.rb'
- 'lib/reports/update_agent_identity.rb'
- 'lib/reports/update_identity.rb'
@ -269,24 +266,6 @@ Style/CommentedKeyword:
- 'app/controllers/api/v1/conversations/labels_controller.rb'
- 'app/controllers/api/v1/labels_controller.rb'
# Offense count: 1
# Configuration parameters: EnforcedStyle.
# SupportedStyles: annotated, template, unannotated
Style/FormatStringToken:
Exclude:
- 'lib/constants/redis_keys.rb'
# Offense count: 4
# Configuration parameters: AllowedVariables.
Style/GlobalVars:
Exclude:
- 'lib/redis/alfred.rb'
# Offense count: 4
Style/IdenticalConditionalBranches:
Exclude:
- 'app/controllers/api/v1/reports_controller.rb'
# Offense count: 1
# Configuration parameters: AllowIfModifier.
Style/IfInsideElse:

View file

@ -51,7 +51,7 @@ linters:
ElsePlacement:
enabled: true
style: same_line # or 'new_line'
style: new_line
EmptyLineBetweenBlocks:
enabled: true

View file

@ -56,9 +56,6 @@ gem 'administrate'
# https://karolgalanciak.com/blog/2019/11/30/from-activerecord-callbacks-to-publish-slash-subscribe-pattern-and-event-driven-design/
gem 'wisper', '2.0.0'
##--- gems for billing ---##
gem 'chargebee'
##--- gems for channels ---##
gem 'facebook-messenger'
gem 'telegram-bot-ruby'
@ -68,6 +65,8 @@ gem 'twilio-ruby', '~> 5.32.0'
gem 'twitty'
# facebook client
gem 'koala'
# slack client
gem 'slack-ruby-client'
# Random name generator
gem 'haikunator'
@ -84,6 +83,7 @@ gem 'sidekiq'
gem 'flag_shih_tzu'
##-- Push notification service --##
gem 'fcm'
gem 'webpush'
group :development do
@ -117,4 +117,5 @@ group :development, :test do
gem 'simplecov', '0.17.1', require: false
gem 'spring'
gem 'spring-watcher-listen'
gem 'webmock'
end

View file

@ -18,56 +18,56 @@ GEM
specs:
action-cable-testing (0.6.1)
actioncable (>= 5.0)
actioncable (6.0.3.1)
actionpack (= 6.0.3.1)
actioncable (6.0.3.2)
actionpack (= 6.0.3.2)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
actionmailbox (6.0.3.1)
actionpack (= 6.0.3.1)
activejob (= 6.0.3.1)
activerecord (= 6.0.3.1)
activestorage (= 6.0.3.1)
activesupport (= 6.0.3.1)
actionmailbox (6.0.3.2)
actionpack (= 6.0.3.2)
activejob (= 6.0.3.2)
activerecord (= 6.0.3.2)
activestorage (= 6.0.3.2)
activesupport (= 6.0.3.2)
mail (>= 2.7.1)
actionmailer (6.0.3.1)
actionpack (= 6.0.3.1)
actionview (= 6.0.3.1)
activejob (= 6.0.3.1)
actionmailer (6.0.3.2)
actionpack (= 6.0.3.2)
actionview (= 6.0.3.2)
activejob (= 6.0.3.2)
mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0)
actionpack (6.0.3.1)
actionview (= 6.0.3.1)
activesupport (= 6.0.3.1)
actionpack (6.0.3.2)
actionview (= 6.0.3.2)
activesupport (= 6.0.3.2)
rack (~> 2.0, >= 2.0.8)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.2.0)
actiontext (6.0.3.1)
actionpack (= 6.0.3.1)
activerecord (= 6.0.3.1)
activestorage (= 6.0.3.1)
activesupport (= 6.0.3.1)
actiontext (6.0.3.2)
actionpack (= 6.0.3.2)
activerecord (= 6.0.3.2)
activestorage (= 6.0.3.2)
activesupport (= 6.0.3.2)
nokogiri (>= 1.8.5)
actionview (6.0.3.1)
activesupport (= 6.0.3.1)
actionview (6.0.3.2)
activesupport (= 6.0.3.2)
builder (~> 3.1)
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.1, >= 1.2.0)
activejob (6.0.3.1)
activesupport (= 6.0.3.1)
activejob (6.0.3.2)
activesupport (= 6.0.3.2)
globalid (>= 0.3.6)
activemodel (6.0.3.1)
activesupport (= 6.0.3.1)
activerecord (6.0.3.1)
activemodel (= 6.0.3.1)
activesupport (= 6.0.3.1)
activestorage (6.0.3.1)
actionpack (= 6.0.3.1)
activejob (= 6.0.3.1)
activerecord (= 6.0.3.1)
activemodel (6.0.3.2)
activesupport (= 6.0.3.2)
activerecord (6.0.3.2)
activemodel (= 6.0.3.2)
activesupport (= 6.0.3.2)
activestorage (6.0.3.2)
actionpack (= 6.0.3.2)
activejob (= 6.0.3.2)
activerecord (= 6.0.3.2)
marcel (~> 0.3.1)
activesupport (6.0.3.1)
activesupport (6.0.3.2)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 0.7, < 2)
minitest (~> 5.1)
@ -91,26 +91,26 @@ GEM
annotate (3.1.1)
activerecord (>= 3.2, < 7.0)
rake (>= 10.4, < 14.0)
ast (2.4.0)
attr_extras (6.2.3)
autoprefixer-rails (9.7.6)
ast (2.4.1)
attr_extras (6.2.4)
autoprefixer-rails (9.8.2)
execjs
aws-eventstream (1.1.0)
aws-partitions (1.317.0)
aws-sdk-core (3.96.1)
aws-partitions (1.332.0)
aws-sdk-core (3.100.0)
aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.239.0)
aws-sigv4 (~> 1.1)
jmespath (~> 1.0)
aws-sdk-kms (1.31.0)
aws-sdk-core (~> 3, >= 3.71.0)
aws-sdk-kms (1.34.1)
aws-sdk-core (~> 3, >= 3.99.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.65.0)
aws-sdk-core (~> 3, >= 3.96.1)
aws-sdk-s3 (1.69.1)
aws-sdk-core (~> 3, >= 3.99.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.1)
aws-sigv4 (1.1.3)
aws-eventstream (~> 1.0, >= 1.0.2)
aws-sigv4 (1.2.0)
aws-eventstream (~> 1, >= 1.0.2)
axiom-types (0.1.1)
descendants_tracker (~> 0.0.4)
ice_nine (~> 0.11.0)
@ -127,25 +127,24 @@ GEM
bootsnap (1.4.6)
msgpack (~> 1.0)
brakeman (4.8.2)
browser (4.1.0)
browser (4.2.0)
builder (3.2.4)
bullet (6.1.0)
activesupport (>= 3.0.0)
uniform_notifier (~> 1.11)
bundle-audit (0.1.0)
bundler-audit
bundler-audit (0.6.1)
bundler-audit (0.7.0.1)
bundler (>= 1.2.0, < 3)
thor (~> 0.18)
thor (>= 0.18, < 2)
byebug (11.1.3)
chargebee (2.7.5)
json_pure (~> 2.1)
rest-client (>= 1.8, < 3.0)
coderay (1.1.2)
coderay (1.1.3)
coercible (1.0.0)
descendants_tracker (~> 0.0.1)
concurrent-ruby (1.1.6)
connection_pool (2.2.2)
connection_pool (2.2.3)
crack (0.4.3)
safe_yaml (~> 1.0.0)
crass (1.0.6)
datetime_picker_rails (0.0.7)
momentjs-rails (>= 2.8.1)
@ -153,17 +152,18 @@ GEM
declarative-option (0.1.0)
descendants_tracker (0.0.4)
thread_safe (~> 0.3, >= 0.3.1)
devise (4.7.1)
devise (4.7.2)
bcrypt (~> 3.0)
orm_adapter (~> 0.1)
railties (>= 4.1.0)
responders
warden (~> 1.2.3)
devise_token_auth (1.1.3)
devise_token_auth (1.1.4)
bcrypt (~> 3.0)
devise (> 3.5.2, < 5)
rails (>= 4.2.0, < 6.1)
diff-lcs (1.3)
sprockets (= 3.7.2)
diff-lcs (1.4)
digest-crc (0.5.1)
docile (1.3.2)
domain_name (0.5.20190701)
@ -178,23 +178,26 @@ GEM
facebook-messenger (1.5.0)
httparty (~> 0.13, >= 0.13.7)
rack (>= 1.4.5)
factory_bot (5.2.0)
activesupport (>= 4.2.0)
factory_bot_rails (5.2.0)
factory_bot (~> 5.2.0)
railties (>= 4.2.0)
faker (2.11.0)
factory_bot (6.0.2)
activesupport (>= 5.0.0)
factory_bot_rails (6.0.0)
factory_bot (~> 6.0.0)
railties (>= 5.0.0)
faker (2.12.0)
i18n (>= 1.6, < 2)
faraday (1.0.1)
multipart-post (>= 1.2, < 3)
faraday_middleware (1.0.0)
faraday (~> 1.0)
ffi (1.12.2)
fcm (1.0.1)
faraday (~> 1.0.0)
ffi (1.13.1)
flag_shih_tzu (0.3.23)
foreman (0.87.1)
gli (2.19.1)
globalid (0.4.2)
activesupport (>= 4.2.0)
google-api-client (0.39.4)
google-api-client (0.41.0)
addressable (~> 2.5, >= 2.5.1)
googleauth (~> 0.9)
httpclient (>= 2.8.1, < 3.0)
@ -205,17 +208,17 @@ GEM
google-cloud-core (1.5.0)
google-cloud-env (~> 1.0)
google-cloud-errors (~> 1.0)
google-cloud-env (1.3.1)
google-cloud-env (1.3.2)
faraday (>= 0.17.3, < 2.0)
google-cloud-errors (1.0.0)
google-cloud-storage (1.26.1)
google-cloud-errors (1.0.1)
google-cloud-storage (1.26.2)
addressable (~> 2.5)
digest-crc (~> 0.4)
google-api-client (~> 0.33)
google-cloud-core (~> 1.2)
googleauth (~> 0.9)
mini_mime (~> 1.0)
googleauth (0.12.0)
googleauth (0.13.0)
faraday (>= 0.17.3, < 2.0)
jwt (>= 1.4, < 3.0)
memoist (~> 0.16)
@ -226,16 +229,17 @@ GEM
activesupport (>= 5)
haikunator (1.1.0)
hana (1.3.6)
hashdiff (1.0.1)
hashie (4.1.0)
hkdf (0.3.0)
http-accept (1.7.0)
http-cookie (1.0.3)
domain_name (~> 0.5)
httparty (0.18.0)
httparty (0.18.1)
mime-types (~> 3.0)
multi_xml (>= 0.5.2)
httpclient (2.8.3)
i18n (1.8.2)
i18n (1.8.3)
concurrent-ruby (~> 1.0)
ice_nine (0.11.2)
inflecto (0.0.2)
@ -247,7 +251,6 @@ GEM
railties (>= 4.2.0)
thor (>= 0.14, < 2.0)
json (2.3.0)
json_pure (2.3.0)
jwt (2.2.1)
kaminari (1.2.1)
activesupport (>= 4.1.0)
@ -272,7 +275,7 @@ GEM
listen (3.2.1)
rb-fsevent (~> 0.10, >= 0.10.3)
rb-inotify (~> 0.9, >= 0.9.10)
loofah (2.5.0)
loofah (2.6.0)
crass (~> 1.0.2)
nokogiri (>= 1.5.9)
mail (2.7.1)
@ -302,9 +305,9 @@ GEM
oauth (0.5.4)
orm_adapter (0.5.0)
os (1.1.0)
parallel (1.19.1)
parser (2.7.1.2)
ast (~> 2.4.0)
parallel (1.19.2)
parser (2.7.1.4)
ast (~> 2.4.1)
pg (1.2.3)
pry (0.13.1)
coderay (~> 1.1)
@ -316,8 +319,8 @@ GEM
nio4r (~> 2.0)
pundit (2.1.0)
activesupport (>= 3.0.0)
rack (2.2.2)
rack-cache (1.11.1)
rack (2.2.3)
rack-cache (1.12.0)
rack (>= 0.4)
rack-cors (1.1.1)
rack (>= 2.0.0)
@ -327,29 +330,29 @@ GEM
rack
rack-test (1.1.0)
rack (>= 1.0, < 3)
rails (6.0.3.1)
actioncable (= 6.0.3.1)
actionmailbox (= 6.0.3.1)
actionmailer (= 6.0.3.1)
actionpack (= 6.0.3.1)
actiontext (= 6.0.3.1)
actionview (= 6.0.3.1)
activejob (= 6.0.3.1)
activemodel (= 6.0.3.1)
activerecord (= 6.0.3.1)
activestorage (= 6.0.3.1)
activesupport (= 6.0.3.1)
rails (6.0.3.2)
actioncable (= 6.0.3.2)
actionmailbox (= 6.0.3.2)
actionmailer (= 6.0.3.2)
actionpack (= 6.0.3.2)
actiontext (= 6.0.3.2)
actionview (= 6.0.3.2)
activejob (= 6.0.3.2)
activemodel (= 6.0.3.2)
activerecord (= 6.0.3.2)
activestorage (= 6.0.3.2)
activesupport (= 6.0.3.2)
bundler (>= 1.3.0)
railties (= 6.0.3.1)
railties (= 6.0.3.2)
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.1)
actionpack (= 6.0.3.1)
activesupport (= 6.0.3.1)
railties (6.0.3.2)
actionpack (= 6.0.3.2)
activesupport (= 6.0.3.2)
method_source
rake (>= 0.8.7)
thor (>= 0.20.3, < 2.0)
@ -358,19 +361,20 @@ GEM
rb-fsevent (0.10.4)
rb-inotify (0.10.1)
ffi (~> 1.0)
redis (4.1.4)
redis (4.2.1)
redis-namespace (1.7.0)
redis (>= 3.0.4)
redis-rack-cache (2.2.1)
rack-cache (>= 1.10, < 2)
redis-store (>= 1.6, < 2)
redis-store (1.8.2)
redis-store (1.9.0)
redis (>= 4, < 5)
regexp_parser (1.7.1)
representable (3.0.4)
declarative (< 0.1.0)
declarative-option (< 0.2.0)
uber (< 0.2.0)
responders (3.0.0)
responders (3.0.1)
actionpack (>= 5.0)
railties (>= 5.0)
rest-client (2.1.0)
@ -397,28 +401,33 @@ GEM
rspec-mocks (~> 3.9)
rspec-support (~> 3.9)
rspec-support (3.9.3)
rubocop (0.83.0)
rubocop (0.86.0)
parallel (~> 1.10)
parser (>= 2.7.0.1)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.7)
rexml
rubocop-ast (>= 0.0.3, < 1.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 1.4.0, < 2.0)
rubocop-performance (1.5.2)
rubocop-ast (0.0.3)
parser (>= 2.7.0.1)
rubocop-performance (1.6.1)
rubocop (>= 0.71.0)
rubocop-rails (2.5.2)
activesupport
rubocop-rails (2.6.0)
activesupport (>= 4.2.0)
rack (>= 1.1)
rubocop (>= 0.72.0)
rubocop-rspec (1.39.0)
rubocop (>= 0.82.0)
rubocop-rspec (1.40.0)
rubocop (>= 0.68.1)
ruby-progressbar (1.10.1)
safe_yaml (1.0.5)
sass (3.7.4)
sass-listen (~> 4.0.0)
sass-listen (4.0.0)
rb-fsevent (~> 0.9, >= 0.9.4)
rb-inotify (~> 0.9, >= 0.9.7)
sassc (2.3.0)
sassc (2.4.0)
ffi (~> 1.9)
sassc-rails (2.1.2)
railties (>= 4.0.0)
@ -454,11 +463,18 @@ GEM
json (>= 1.8, < 3)
simplecov-html (~> 0.10.0)
simplecov-html (0.10.2)
slack-ruby-client (0.14.6)
activesupport
faraday (>= 0.9)
faraday_middleware
gli
hashie
websocket-driver
spring (2.1.0)
spring-watcher-listen (2.0.1)
listen (>= 2.7, < 4.0)
spring (>= 1.2, < 3.0)
sprockets (4.0.0)
sprockets (3.7.2)
concurrent-ruby (~> 1.0)
rack (> 1, < 3)
sprockets-rails (3.2.1)
@ -470,7 +486,7 @@ GEM
inflecto
virtus
telephone_number (1.4.7)
thor (0.20.3)
thor (1.0.1)
thread_safe (0.3.6)
tilt (2.0.10)
time_diff (0.3.0)
@ -504,11 +520,15 @@ GEM
equalizer (~> 0.0, >= 0.0.9)
warden (1.2.8)
rack (>= 2.0.6)
web-console (4.0.2)
web-console (4.0.3)
actionview (>= 6.0.0)
activemodel (>= 6.0.0)
bindex (>= 0.4.0)
railties (>= 6.0.0)
webmock (3.8.3)
addressable (>= 2.3.6)
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)
webpacker (5.1.1)
activesupport (>= 5.2)
rack-proxy (>= 0.6.1)
@ -517,9 +537,9 @@ GEM
webpush (1.0.0)
hkdf (~> 0.2)
jwt (~> 2.0)
websocket-driver (0.7.1)
websocket-driver (0.7.2)
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.4)
websocket-extensions (0.1.5)
wisper (2.0.0)
zeitwerk (2.3.0)
@ -540,13 +560,13 @@ DEPENDENCIES
bullet
bundle-audit
byebug
chargebee
devise
devise_token_auth
dotenv-rails
facebook-messenger
factory_bot_rails
faker
fcm
flag_shih_tzu
foreman
google-cloud-storage
@ -585,6 +605,7 @@ DEPENDENCIES
shoulda-matchers
sidekiq
simplecov (= 0.17.1)
slack-ruby-client
spring
spring-watcher-listen
telegram-bot-ruby
@ -596,6 +617,7 @@ DEPENDENCIES
uglifier
valid_email2
web-console
webmock
webpacker
webpush
wisper (= 2.0.0)

View file

@ -29,7 +29,7 @@ class ContactMergeAction
end
def merge_messages
Message.where(contact_id: @mergee_contact.id).update(contact_id: @base_contact.id)
Message.where(sender: @mergee_contact).update(sender: @base_contact)
end
def merge_contact_inboxes

View file

@ -117,7 +117,8 @@ class Messages::MessageBuilder
inbox_id: conversation.inbox_id,
message_type: @message_type,
content: response.content,
source_id: response.identifier
source_id: response.identifier,
sender: contact
}
end

View file

@ -37,7 +37,7 @@ class Messages::Outgoing::NormalBuilder
message_type: :outgoing,
content: @content,
private: @private,
user_id: @user&.id,
sender: @user,
source_id: @fb_id,
content_type: @content_type,
items: @items

View file

@ -12,6 +12,8 @@ class NotificationSubscriptionBuilder
def identifier
@identifier ||= params[:subscription_attributes][:endpoint] if params[:subscription_type] == 'browser_push'
@identifier ||= params[:subscription_attributes][:device_id] if params[:subscription_type] == 'fcm'
@identifier
end
def identifier_subscription

View file

@ -1,10 +1,47 @@
class RoomChannel < ApplicationCable::Channel
def subscribed
stream_from params[:pubsub_token]
::OnlineStatusTracker.add_subscription(params[:pubsub_token])
ensure_stream
current_user
current_account
update_subscription
broadcast_presence
end
def unsubscribed
::OnlineStatusTracker.remove_subscription(params[:pubsub_token])
def update_presence
update_subscription
broadcast_presence
end
private
def broadcast_presence
data = { account_id: @current_account.id, users: ::OnlineStatusTracker.get_available_users(@current_account.id) }
data[:contacts] = ::OnlineStatusTracker.get_available_contacts(@current_account.id) if @current_user.is_a? User
ActionCable.server.broadcast(@pubsub_token, { event: 'presence.update', data: data })
end
def ensure_stream
@pubsub_token = params[:pubsub_token]
stream_from @pubsub_token
end
def update_subscription
::OnlineStatusTracker.update_presence(@current_account.id, @current_user.class.name, @current_user.id)
end
def current_user
@current_user ||= if params[:user_id].blank?
Contact.find_by!(pubsub_token: @pubsub_token)
else
User.find_by!(pubsub_token: @pubsub_token, id: params[:user_id])
end
end
def current_account
@current_account ||= if @current_user.is_a? Contact
@current_user.account
else
@current_user.accounts.find(params[:account_id])
end
end
end

View file

@ -10,12 +10,4 @@ class Api::BaseController < ApplicationController
def authenticate_by_access_token?
request.headers[:api_access_token].present? || request.headers[:HTTP_API_ACCESS_TOKEN].present?
end
def set_conversation
@conversation ||= current_account.conversations.find_by(display_id: params[:conversation_id])
end
def check_billing_enabled
raise ActionController::RoutingError, 'Not Found' unless ENV['BILLING_ENABLED']
end
end

View file

@ -1,10 +1,10 @@
class Api::V1::Accounts::Actions::ContactMergesController < Api::BaseController
class Api::V1::Accounts::Actions::ContactMergesController < Api::V1::Accounts::BaseController
before_action :set_base_contact, only: [:create]
before_action :set_mergee_contact, only: [:create]
def create
contact_merge_action = ContactMergeAction.new(
account: current_account,
account: Current.account,
base_contact: @base_contact,
mergee_contact: @mergee_contact
)
@ -23,6 +23,6 @@ class Api::V1::Accounts::Actions::ContactMergesController < Api::BaseController
end
def contacts
@contacts ||= current_account.contacts
@contacts ||= Current.account.contacts
end
end

View file

@ -1,4 +1,4 @@
class Api::V1::Accounts::AgentsController < Api::BaseController
class Api::V1::Accounts::AgentsController < Api::V1::Accounts::BaseController
before_action :fetch_agent, except: [:create, :index]
before_action :check_authorization
before_action :find_user, only: [:create]
@ -46,7 +46,7 @@ class Api::V1::Accounts::AgentsController < Api::BaseController
def save_account_user
AccountUser.create!(
account_id: current_account.id,
account_id: Current.account.id,
user_id: @user.id,
role: new_agent_params[:role],
inviter_id: current_user.id
@ -64,6 +64,6 @@ class Api::V1::Accounts::AgentsController < Api::BaseController
end
def agents
@agents ||= current_account.users
@agents ||= Current.account.users
end
end

View file

@ -0,0 +1,31 @@
class Api::V1::Accounts::BaseController < Api::BaseController
before_action :current_account
private
def current_account
@current_account ||= ensure_current_account
Current.account = @current_account
end
def ensure_current_account
account = Account.find(params[:account_id])
if current_user
account_accessible_for_user?(account)
elsif @resource&.is_a?(AgentBot)
account_accessible_for_bot?(account)
end
switch_locale account
account
end
def account_accessible_for_user?(account)
@current_account_user = account.account_users.find_by(user_id: current_user.id)
Current.account_user = @current_account_user
render_unauthorized('You are not authorized to access this account') unless @current_account_user
end
def account_accessible_for_bot?(account)
render_unauthorized('You are not authorized to access this account') unless @resource.agent_bot_inboxes.find_by(account_id: account.id)
end
end

View file

@ -1,4 +1,4 @@
class Api::V1::Accounts::CallbacksController < Api::BaseController
class Api::V1::Accounts::CallbacksController < Api::V1::Accounts::BaseController
before_action :inbox, only: [:reauthorize_page]
def register_facebook_page
@ -7,11 +7,11 @@ class Api::V1::Accounts::CallbacksController < Api::BaseController
page_id = params[:page_id]
inbox_name = params[:inbox_name]
ActiveRecord::Base.transaction do
facebook_channel = current_account.facebook_pages.create!(
facebook_channel = Current.account.facebook_pages.create!(
page_id: page_id, user_access_token: user_access_token,
page_access_token: page_access_token
)
@facebook_inbox = current_account.inboxes.create!(name: inbox_name, channel: facebook_channel)
@facebook_inbox = Current.account.inboxes.create!(name: inbox_name, channel: facebook_channel)
set_avatar(@facebook_inbox, page_id)
rescue StandardError => e
Rails.logger.info e
@ -22,7 +22,7 @@ class Api::V1::Accounts::CallbacksController < Api::BaseController
@page_details = mark_already_existing_facebook_pages(fb_object.get_connections('me', 'accounts'))
end
# get params[:inbox_id], current_account, params[:omniauth_token]
# get params[:inbox_id], current_account. params[:omniauth_token]
def reauthorize_page
if @inbox&.facebook?
fb_page_id = @inbox.channel.page_id
@ -40,7 +40,7 @@ class Api::V1::Accounts::CallbacksController < Api::BaseController
private
def inbox
@inbox = current_account.inboxes.find_by(id: params[:inbox_id])
@inbox = Current.account.inboxes.find_by(id: params[:inbox_id])
end
def update_fb_page(fb_page_id, access_token)
@ -50,7 +50,7 @@ class Api::V1::Accounts::CallbacksController < Api::BaseController
end
def get_fb_page(fb_page_id)
current_account.facebook_pages.find_by(page_id: fb_page_id)
Current.account.facebook_pages.find_by(page_id: fb_page_id)
end
def fb_object
@ -69,7 +69,7 @@ class Api::V1::Accounts::CallbacksController < Api::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']) ? true : false
result << page_detail
end
end

View file

@ -1,4 +1,4 @@
class Api::V1::Accounts::CannedResponsesController < Api::BaseController
class Api::V1::Accounts::CannedResponsesController < Api::V1::Accounts::BaseController
before_action :fetch_canned_response, only: [:update, :destroy]
def index
@ -6,7 +6,7 @@ class Api::V1::Accounts::CannedResponsesController < Api::BaseController
end
def create
@canned_response = current_account.canned_responses.new(canned_response_params)
@canned_response = Current.account.canned_responses.new(canned_response_params)
@canned_response.save!
render json: @canned_response
end
@ -24,7 +24,7 @@ class Api::V1::Accounts::CannedResponsesController < Api::BaseController
private
def fetch_canned_response
@canned_response = current_account.canned_responses.find(params[:id])
@canned_response = Current.account.canned_responses.find(params[:id])
end
def canned_response_params
@ -33,9 +33,9 @@ class Api::V1::Accounts::CannedResponsesController < Api::BaseController
def canned_responses
if params[:search]
current_account.canned_responses.where('short_code ILIKE ?', "#{params[:search]}%")
Current.account.canned_responses.where('short_code ILIKE ?', "#{params[:search]}%")
else
current_account.canned_responses
Current.account.canned_responses
end
end
end

View file

@ -1,5 +1,4 @@
class Api::V1::Accounts::Channels::TwilioChannelsController < Api::BaseController
before_action :current_account
class Api::V1::Accounts::Channels::TwilioChannelsController < Api::V1::Accounts::BaseController
before_action :authorize_request
def create
@ -38,13 +37,13 @@ class Api::V1::Accounts::Channels::TwilioChannelsController < Api::BaseControlle
end
def build_inbox
@twilio_channel = current_account.twilio_sms.create!(
@twilio_channel = Current.account.twilio_sms.create!(
account_sid: permitted_params[:account_sid],
auth_token: permitted_params[:auth_token],
phone_number: phone_number,
medium: medium
)
@inbox = current_account.inboxes.create(
@inbox = Current.account.inboxes.create(
name: permitted_params[:name],
channel: @twilio_channel
)

View file

@ -1,6 +1,6 @@
class Api::V1::Accounts::Contacts::ConversationsController < Api::BaseController
class Api::V1::Accounts::Contacts::ConversationsController < Api::V1::Accounts::BaseController
def index
@conversations = current_account.conversations.includes(
@conversations = Current.account.conversations.includes(
:assignee, :contact, :inbox
).where(inbox_id: inbox_ids, contact_id: permitted_params[:contact_id])
end
@ -9,7 +9,7 @@ class Api::V1::Accounts::Contacts::ConversationsController < Api::BaseController
def inbox_ids
if current_user.administrator?
current_account.inboxes.pluck(:id)
Current.account.inboxes.pluck(:id)
elsif current_user.agent?
current_user.assigned_inboxes.pluck(:id)
else

View file

@ -1,17 +1,17 @@
class Api::V1::Accounts::ContactsController < Api::BaseController
class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
protect_from_forgery with: :null_session
before_action :check_authorization
before_action :fetch_contact, only: [:show, :update]
def index
@contacts = current_account.contacts
@contacts = Current.account.contacts
end
def show; end
def create
@contact = Contact.new(contact_create_params)
@contact = Current.account.contacts.new(contact_create_params)
@contact.save!
render json: @contact
end
@ -31,10 +31,10 @@ class Api::V1::Accounts::ContactsController < Api::BaseController
end
def fetch_contact
@contact = current_account.contacts.find(params[:id])
@contact = Current.account.contacts.find(params[:id])
end
def contact_create_params
params.require(:contact).permit(:account_id, :inbox_id).merge!(name: SecureRandom.hex)
params.require(:contact).permit(:name, :email, :phone_number)
end
end

View file

@ -1,10 +1,8 @@
class Api::V1::Accounts::Conversations::AssignmentsController < Api::BaseController
before_action :set_conversation, only: [:create]
class Api::V1::Accounts::Conversations::AssignmentsController < Api::V1::Accounts::Conversations::BaseController
# assign agent to a conversation
def create
# if params[:assignee_id] is not a valid id, it will set to nil, hence unassigning the conversation
assignee = current_account.users.find_by(id: params[:assignee_id])
assignee = Current.account.users.find_by(id: params[:assignee_id])
@conversation.update_assignee(assignee)
render json: assignee
end

View file

@ -0,0 +1,9 @@
class Api::V1::Accounts::Conversations::BaseController < Api::V1::Accounts::BaseController
before_action :conversation
private
def conversation
@conversation ||= Current.account.conversations.find_by(display_id: params[:conversation_id])
end
end

View file

@ -1,6 +1,4 @@
class Api::V1::Accounts::Conversations::LabelsController < Api::BaseController
before_action :set_conversation, only: [:create, :index]
class Api::V1::Accounts::Conversations::LabelsController < Api::V1::Accounts::Conversations::BaseController
def create
@conversation.update_labels(params[:labels])
@labels = @conversation.label_list

View file

@ -1,12 +1,11 @@
class Api::V1::Accounts::Conversations::MessagesController < Api::BaseController
before_action :set_conversation, only: [:index, :create]
class Api::V1::Accounts::Conversations::MessagesController < Api::V1::Accounts::Conversations::BaseController
def index
@messages = message_finder.perform
end
def create
mb = Messages::Outgoing::NormalBuilder.new(current_user, @conversation, params)
user = current_user || @resource
mb = Messages::Outgoing::NormalBuilder.new(user, @conversation, params)
@message = mb.perform
end

View file

@ -1,6 +1,5 @@
class Api::V1::Accounts::ConversationsController < Api::BaseController
class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseController
include Events::Types
before_action :current_account
before_action :conversation, except: [:index]
before_action :contact_inbox, only: [:create]
@ -62,7 +61,7 @@ class Api::V1::Accounts::ConversationsController < Api::BaseController
end
def conversation
@conversation ||= current_account.conversations.find_by(display_id: params[:id])
@conversation ||= Current.account.conversations.find_by(display_id: params[:id])
end
def contact_inbox
@ -71,7 +70,7 @@ class Api::V1::Accounts::ConversationsController < Api::BaseController
def conversation_params
{
account_id: current_account.id,
account_id: Current.account.id,
inbox_id: @contact_inbox.inbox_id,
contact_id: @contact_inbox.contact_id,
contact_inbox_id: @contact_inbox.id

View file

@ -1,4 +1,4 @@
class Api::V1::Accounts::FacebookIndicatorsController < Api::BaseController
class Api::V1::Accounts::FacebookIndicatorsController < Api::V1::Accounts::BaseController
before_action :set_access_token
around_action :handle_with_exception
@ -38,7 +38,7 @@ class Api::V1::Accounts::FacebookIndicatorsController < Api::BaseController
end
def inbox
@inbox ||= current_account.inboxes.find(permitted_params[:inbox_id])
@inbox ||= Current.account.inboxes.find(permitted_params[:inbox_id])
end
def set_access_token

View file

@ -1,4 +1,4 @@
class Api::V1::Accounts::InboxMembersController < Api::BaseController
class Api::V1::Accounts::InboxMembersController < Api::V1::Accounts::BaseController
before_action :fetch_inbox, only: [:create, :show]
before_action :current_agents_ids, only: [:create]
@ -12,7 +12,7 @@ class Api::V1::Accounts::InboxMembersController < Api::BaseController
end
def show
@agents = current_account.users.where(id: @inbox.members.pluck(:user_id))
@agents = Current.account.users.where(id: @inbox.members.pluck(:user_id))
end
private
@ -40,6 +40,6 @@ class Api::V1::Accounts::InboxMembersController < Api::BaseController
end
def fetch_inbox
@inbox = current_account.inboxes.find(params[:inbox_id])
@inbox = Current.account.inboxes.find(params[:inbox_id])
end
end

View file

@ -1,17 +1,21 @@
class Api::V1::Accounts::InboxesController < Api::BaseController
before_action :current_account
class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
before_action :fetch_inbox, except: [:index, :create]
before_action :fetch_agent_bot, only: [:set_agent_bot]
before_action :check_authorization
def index
@inboxes = policy_scope(current_account.inboxes)
@inboxes = policy_scope(Current.account.inboxes)
end
def create
ActiveRecord::Base.transaction do
channel = web_widgets.create!(permitted_params[:channel].except(:type)) if permitted_params[:channel][:type] == 'web_widget'
@inbox = current_account.inboxes.build(name: permitted_params[:name], channel: channel)
@inbox = Current.account.inboxes.build(
name: permitted_params[:name],
greeting_message: permitted_params[:greeting_message],
greeting_enabled: permitted_params[:greeting_enabled],
channel: channel
)
@inbox.avatar.attach(permitted_params[:avatar])
@inbox.save!
end
@ -41,7 +45,7 @@ class Api::V1::Accounts::InboxesController < Api::BaseController
private
def fetch_inbox
@inbox = current_account.inboxes.find(params[:id])
@inbox = Current.account.inboxes.find(params[:id])
end
def fetch_agent_bot
@ -49,7 +53,7 @@ class Api::V1::Accounts::InboxesController < Api::BaseController
end
def web_widgets
current_account.web_widgets
Current.account.web_widgets
end
def check_authorization
@ -57,11 +61,12 @@ class Api::V1::Accounts::InboxesController < Api::BaseController
end
def permitted_params
params.permit(:id, :avatar, :name, channel: [:type, :website_url, :widget_color, :welcome_title, :welcome_tagline, :agent_away_message])
params.permit(:id, :avatar, :name, :greeting_message, :greeting_enabled, channel:
[:type, :website_url, :widget_color, :welcome_title, :welcome_tagline])
end
def inbox_update_params
params.permit(:enable_auto_assignment, :name, :avatar, channel: [:website_url, :widget_color, :welcome_title,
:welcome_tagline, :agent_away_message])
params.permit(:enable_auto_assignment, :name, :avatar, :greeting_message, :greeting_enabled,
channel: [:website_url, :widget_color, :welcome_title, :welcome_tagline])
end
end

View file

@ -0,0 +1,18 @@
class Api::V1::Accounts::Integrations::AppsController < Api::V1::Accounts::BaseController
before_action :fetch_apps, only: [:index]
before_action :fetch_app, only: [:show]
def index; end
def show; end
private
def fetch_apps
@apps = Integrations::App.all.select(&:active?)
end
def fetch_app
@app = Integrations::App.find(id: params[:id])
end
end

View file

@ -0,0 +1,38 @@
class Api::V1::Accounts::Integrations::SlackController < Api::V1::Accounts::BaseController
before_action :fetch_hook, only: [:update, :destroy]
def create
builder = Integrations::Slack::HookBuilder.new(
account: current_account,
code: params[:code],
inbox_id: params[:inbox_id]
)
@hook = builder.perform
create_chatwoot_slack_channel
end
def update
create_chatwoot_slack_channel
render json: @hook
end
def destroy
@hook.destroy
head :ok
end
private
def fetch_hook
@hook = Integrations::Hook.find_by(app_id: 'slack')
end
def create_chatwoot_slack_channel
channel = params[:channel] || 'customer-conversations'
builder = Integrations::Slack::ChannelBuilder.new(
hook: @hook, channel: channel
)
builder.perform
end
end

View file

@ -1,10 +1,38 @@
class Api::V1::Accounts::LabelsController < Api::BaseController
# list all labels in account
class Api::V1::Accounts::LabelsController < Api::V1::Accounts::BaseController
before_action :current_account
before_action :fetch_label, except: [:index, :create]
before_action :check_authorization
def index
@labels = current_account.all_conversation_tags
@labels = policy_scope(Current.account.labels)
end
def most_used
@labels = ActsAsTaggableOn::Tag.most_used(params[:count] || 10)
def show; end
def create
@label = Current.account.labels.create!(permitted_params)
end
def update
@label.update!(permitted_params)
end
def destroy
@label.destroy
head :ok
end
private
def fetch_label
@label = Current.account.labels.find(params[:id])
end
def check_authorization
authorize(Label)
end
def permitted_params
params.require(:label).permit(:title, :description, :color, :show_on_sidebar)
end
end

View file

@ -1,4 +1,4 @@
class Api::V1::Accounts::NotificationSettingsController < Api::BaseController
class Api::V1::Accounts::NotificationSettingsController < Api::V1::Accounts::BaseController
before_action :set_user, :load_notification_setting
def show; end
@ -16,7 +16,7 @@ class Api::V1::Accounts::NotificationSettingsController < Api::BaseController
end
def load_notification_setting
@notification_setting = @user.notification_settings.find_by(account_id: current_account.id)
@notification_setting = @user.notification_settings.find_by(account_id: Current.account.id)
end
def notification_setting_params

View file

@ -1,11 +1,23 @@
class Api::V1::Accounts::NotificationsController < Api::BaseController
class Api::V1::Accounts::NotificationsController < Api::V1::Accounts::BaseController
protect_from_forgery with: :null_session
before_action :fetch_notification, only: [:update]
before_action :set_primary_actor, only: [:read_all]
def index
@notifications = current_user.notifications.where(account_id: current_account.id)
render json: @notifications
@unread_count = current_user.notifications.where(account_id: current_account.id, read_at: nil).count
@notifications = current_user.notifications.where(account_id: current_account.id).page params[:page]
end
def read_all
if @primary_actor
current_user.notifications.where(account_id: current_account.id, primary_actor: @primary_actor, read_at: nil)
.update(read_at: DateTime.now.utc)
else
current_user.notifications.where(account_id: current_account.id, read_at: nil).update(read_at: DateTime.now.utc)
end
head :ok
end
def update
@ -15,6 +27,13 @@ class Api::V1::Accounts::NotificationsController < Api::BaseController
private
def set_primary_actor
return unless params[:primary_actor_type]
return unless Notification::PRIMARY_ACTORS.include?(params[:primary_actor_type])
@primary_actor = params[:primary_actor_type].safe_constantize.find_by(id: params[:primary_actor_id])
end
def fetch_notification
@notification = current_user.notifications.find(params[:id])
end

View file

@ -1,13 +0,0 @@
class Api::V1::Accounts::SubscriptionsController < Api::BaseController
skip_before_action :check_subscription
before_action :check_billing_enabled
def index
render json: current_account.subscription_data
end
def status
render json: current_account.subscription.summary
end
end

View file

@ -1,14 +1,13 @@
class Api::V1::Accounts::WebhooksController < Api::BaseController
before_action :current_account
class Api::V1::Accounts::WebhooksController < Api::V1::Accounts::BaseController
before_action :check_authorization
before_action :fetch_webhook, only: [:update, :destroy]
def index
@webhooks = current_account.webhooks
@webhooks = Current.account.webhooks
end
def create
@webhook = current_account.webhooks.new(webhook_params)
@webhook = Current.account.webhooks.new(webhook_params)
@webhook.save!
end
@ -28,7 +27,7 @@ class Api::V1::Accounts::WebhooksController < Api::BaseController
end
def fetch_webhook
@webhook = current_account.webhooks.find(params[:id])
@webhook = Current.account.webhooks.find(params[:id])
end
def check_authorization

View file

@ -1,8 +1,8 @@
class Api::V1::Accounts::AccountsController < Api::BaseController
class Api::V1::AccountsController < Api::BaseController
include AuthHelper
skip_before_action :verify_authenticity_token, only: [:create]
skip_before_action :authenticate_user!, :set_current_user, :check_subscription, :handle_with_exception,
skip_before_action :authenticate_user!, :set_current_user, :handle_with_exception,
only: [:create], raise: false
before_action :check_signup_enabled, only: [:create]
before_action :fetch_account, except: [:create]

View file

@ -1,6 +1,5 @@
class Api::V1::AgentBotsController < Api::BaseController
skip_before_action :authenticate_user!
skip_before_action :check_subscription
def index
render json: AgentBot.all

View file

@ -0,0 +1,7 @@
class Api::V1::Integrations::WebhooksController < ApplicationController
def create
builder = Integrations::Slack::IncomingMessageBuilder.new(params)
response = builder.perform
render json: response
end
end

View file

@ -16,6 +16,6 @@ class Api::V1::ProfilesController < Api::BaseController
end
def profile_params
params.require(:profile).permit(:email, :name, :password, :password_confirmation, :avatar)
params.require(:profile).permit(:email, :name, :password, :password_confirmation, :avatar, :availability)
end
end

View file

@ -1,18 +1,6 @@
class Api::V1::WebhooksController < ApplicationController
skip_before_action :authenticate_user!, raise: false
skip_before_action :set_current_user
skip_before_action :check_subscription
before_action :login_from_basic_auth, only: [:chargebee]
before_action :check_billing_enabled, only: [:chargebee]
def chargebee
chargebee_consumer.consume
head :ok
rescue StandardError => e
Raven.capture_exception(e)
head :ok
end
def twitter_crc
render json: { response_token: "sha256=#{twitter_client.generate_crc(params[:crc_token])}" }
@ -34,16 +22,6 @@ class Api::V1::WebhooksController < ApplicationController
end
end
def login_from_basic_auth
authenticate_or_request_with_http_basic do |username, password|
username == ENV['CHARGEBEE_WEBHOOK_USERNAME'] && password == ENV['CHARGEBEE_WEBHOOK_PASSWORD']
end
end
def chargebee_consumer
@chargebee_consumer ||= ::Webhooks::Chargebee.new(params)
end
def twitter_consumer
@twitter_consumer ||= ::Webhooks::Twitter.new(params)
end

View file

@ -1,4 +1,7 @@
class Api::V1::Widget::BaseController < ApplicationController
before_action :set_web_widget
before_action :set_contact
private
def conversation

View file

@ -1,7 +1,4 @@
class Api::V1::Widget::ContactsController < Api::V1::Widget::BaseController
before_action :set_web_widget
before_action :set_contact
def update
contact_identify_action = ContactIdentifyAction.new(
contact: @contact,

View file

@ -1,12 +1,18 @@
class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
include Events::Types
before_action :set_web_widget
before_action :set_contact
def index
@conversation = conversation
end
def update_last_seen
head :ok && return if conversation.nil?
conversation.user_last_seen_at = DateTime.now.utc
conversation.save!
head :ok
end
def toggle_typing
head :ok && return if conversation.nil?

View file

@ -1,15 +1,20 @@
class Api::V1::Widget::EventsController < Api::V1::Widget::BaseController
include Events::Types
before_action :set_web_widget
before_action :set_contact
def create
Rails.configuration.dispatcher.dispatch(permitted_params[:name], Time.zone.now, contact_inbox: @contact_inbox)
Rails.configuration.dispatcher.dispatch(permitted_params[:name], Time.zone.now, contact_inbox: @contact_inbox, event_info: event_info)
head :no_content
end
private
def event_info
{
widget_language: params[:locale],
browser_language: browser.accept_language.first&.code
}
end
def permitted_params
params.permit(:name, :website_token)
end

View file

@ -1,5 +1,5 @@
class Api::V1::Widget::InboxMembersController < Api::V1::Widget::BaseController
before_action :set_web_widget
skip_before_action :set_contact
def index
@inbox_members = @web_widget.inbox.inbox_members.includes(:user)

View file

@ -1,7 +1,4 @@
class Api::V1::Widget::LabelsController < Api::V1::Widget::BaseController
before_action :set_web_widget
before_action :set_contact
def create
conversation.label_list.add(permitted_params[:label])
conversation.save!

View file

@ -1,6 +1,4 @@
class Api::V1::Widget::MessagesController < Api::V1::Widget::BaseController
before_action :set_web_widget
before_action :set_contact
before_action :set_conversation, only: [:create]
before_action :set_message, only: [:update]
@ -47,7 +45,7 @@ class Api::V1::Widget::MessagesController < Api::V1::Widget::BaseController
def message_params
{
account_id: conversation.account_id,
contact_id: @contact.id,
sender: @contact,
content: permitted_params[:message][:content],
inbox_id: conversation.inbox_id,
message_type: :incoming

View file

@ -1,6 +1,6 @@
class Api::V2::Accounts::ReportsController < Api::BaseController
class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
def account
builder = V2::ReportBuilder.new(current_account, account_report_params)
builder = V2::ReportBuilder.new(Current.account, account_report_params)
data = builder.build
render json: data
end
@ -29,7 +29,7 @@ class Api::V2::Accounts::ReportsController < Api::BaseController
end
def account_summary_metrics
builder = V2::ReportBuilder.new(current_account, account_summary_params)
builder = V2::ReportBuilder.new(Current.account, account_summary_params)
builder.summary
end
end

View file

@ -1,6 +1,5 @@
class ApiController < ApplicationController
skip_before_action :set_current_user, only: [:index]
skip_before_action :check_subscription, only: [:index]
def index
render json: { version: Chatwoot.config[:version], timestamp: Time.now.utc.to_formatted_s(:db) }

View file

@ -5,7 +5,6 @@ class ApplicationController < ActionController::Base
protect_from_forgery with: :null_session
before_action :set_current_user, unless: :devise_controller?
before_action :check_subscription, unless: :devise_controller?
around_action :handle_with_exception, unless: :devise_controller?
# after_action :verify_authorized
@ -13,40 +12,6 @@ class ApplicationController < ActionController::Base
private
def current_account
@current_account ||= find_current_account
Current.account = @current_account
end
def find_current_account
account = Account.find(params[:account_id])
if current_user
account_accessible_for_user?(account)
elsif @resource&.is_a?(AgentBot)
account_accessible_for_bot?(account)
end
switch_locale account
account
end
def switch_locale(account)
# priority is for locale set in query string (mostly for widget/from js sdk)
locale ||= (I18n.available_locales.map(&:to_s).include?(params[:locale]) ? params[:locale] : nil)
# if local is not set in param, lets try account
locale ||= (I18n.available_locales.map(&:to_s).include?(account.locale) ? account.locale : nil)
I18n.locale = locale || I18n.default_locale
end
def account_accessible_for_user?(account)
@current_account_user = account.account_users.find_by(user_id: current_user.id)
Current.account_user = @current_account_user
render_unauthorized('You are not authorized to access this account') unless @current_account_user
end
def account_accessible_for_bot?(account)
render_unauthorized('You are not authorized to access this account') unless @resource.agent_bot_inboxes.find_by(account_id: account.id)
end
def handle_with_exception
yield
rescue ActiveRecord::RecordNotFound => e
@ -65,7 +30,7 @@ class ApplicationController < ActionController::Base
end
def current_subscription
@subscription ||= current_account.subscription
@subscription ||= Current.account.subscription
end
def render_unauthorized(message)
@ -94,16 +59,20 @@ class ApplicationController < ActionController::Base
render json: exception.to_hash, status: exception.http_status
end
def check_subscription
# This block is left over from the initial version of chatwoot
# We might reuse this later in the hosted version of chatwoot.
return if !ENV['BILLING_ENABLED'] || !current_user
def locale_from_params
I18n.available_locales.map(&:to_s).include?(params[:locale]) ? params[:locale] : nil
end
if current_subscription.trial? && current_subscription.expiry < Date.current
render json: { error: 'Trial Expired' }, status: :trial_expired
elsif current_subscription.cancelled?
render json: { error: 'Account Suspended' }, status: :account_suspended
end
def locale_from_account(account)
I18n.available_locales.map(&:to_s).include?(account.locale) ? account.locale : nil
end
def switch_locale(account)
# priority is for locale set in query string (mostly for widget/from js sdk)
locale ||= locale_from_params
# if local is not set in param, lets try account
locale ||= locale_from_account(account)
I18n.locale = locale || I18n.default_locale
end
def pundit_user

View file

@ -4,6 +4,7 @@ class WidgetsController < ActionController::Base
before_action :set_token
before_action :set_contact
before_action :build_contact
after_action :allow_iframe_requests
def index; end
@ -50,4 +51,8 @@ class WidgetsController < ActionController::Base
def permitted_params
params.permit(:website_token, :cw_conversation)
end
def allow_iframe_requests
response.headers.delete('X-Frame-Options')
end
end

View file

@ -9,8 +9,7 @@ class AsyncDispatcher < BaseDispatcher
end
def listeners
listeners = [EventListener.instance, WebhookListener.instance]
listeners << SubscriptionListener.instance if ENV['BILLING_ENABLED']
listeners = [EventListener.instance, WebhookListener.instance, HookListener.instance]
listeners
end
end

View file

@ -62,7 +62,7 @@ class ConversationFinder
def find_all_conversations
@conversations = current_account.conversations.includes(
:assignee, :contact, :inbox
:assignee, :inbox, contact: [:avatar_attachment]
).where(inbox_id: @inbox_ids)
end

View file

@ -1,2 +0,0 @@
module Api::V1::SubscriptionsHelper
end

View file

@ -10,6 +10,10 @@ class ApiClient {
}
get url() {
return `${this.baseUrl()}/${this.resource}`;
}
baseUrl() {
let url = this.apiVersion;
if (this.options.accountScoped) {
const isInsideAccountScopedURLs = window.location.pathname.includes(
@ -21,7 +25,8 @@ class ApiClient {
url = `${url}/accounts/${accountId}`;
}
}
return `${url}/${this.resource}`;
return url;
}
get() {

View file

@ -118,21 +118,18 @@ export default {
return axios.post(urlData.url, { email });
},
profileUpdate({ name, email, password, password_confirmation, avatar }) {
profileUpdate({ password, password_confirmation, ...profileAttributes }) {
const formData = new FormData();
if (name) {
formData.append('profile[name]', name);
}
if (email) {
formData.append('profile[email]', email);
}
Object.keys(profileAttributes).forEach(key => {
const value = profileAttributes[key];
if (value) {
formData.append(`profile[${key}]`, value);
}
});
if (password && password_confirmation) {
formData.append('profile[password]', password);
formData.append('profile[password_confirmation]', password_confirmation);
}
if (avatar) {
formData.append('profile[avatar]', avatar);
}
return axios.put(endPoints('profileUpdate').url, formData);
},
};

View file

@ -1,20 +0,0 @@
/* global axios */
import endPoints from './endPoints';
export default {
getSubscription() {
const urlData = endPoints('subscriptions').get();
const fetchPromise = new Promise((resolve, reject) => {
axios
.get(urlData.url)
.then(response => {
resolve(response);
})
.catch(error => {
reject(error);
});
});
return fetchPromise;
},
};

View file

@ -6,20 +6,6 @@ class FBChannel extends ApiClient {
super('facebook_indicators', { accountScoped: true });
}
markSeen({ inboxId, contactId }) {
return axios.post(`${this.url}/mark_seen`, {
inbox_id: inboxId,
contact_id: contactId,
});
}
toggleTyping({ status, inboxId, contactId }) {
return axios.post(`${this.url}/typing_${status}`, {
inbox_id: inboxId,
contact_id: contactId,
});
}
create(params) {
return axios.post(
`${this.url.replace(this.resource, '')}callbacks/register_facebook_page`,

View file

@ -33,14 +33,6 @@ const endPoints = {
},
params: { omniauth_token: '' },
},
subscriptions: {
get() {
return {
url: '/api/v1/subscriptions',
};
},
},
};
export default page => {

View file

@ -6,13 +6,14 @@ class ConversationApi extends ApiClient {
super('conversations', { accountScoped: true });
}
get({ inboxId, status, assigneeType, page }) {
get({ inboxId, status, assigneeType, page, labels }) {
return axios.get(this.url, {
params: {
inbox_id: inboxId,
status,
assignee_type: assigneeType,
page,
labels,
},
});
}
@ -43,6 +44,17 @@ class ConversationApi extends ApiClient {
mute(conversationId) {
return axios.post(`${this.url}/${conversationId}/mute`);
}
meta({ inboxId, status, assigneeType, labels }) {
return axios.get(`${this.url}/meta`, {
params: {
inbox_id: inboxId,
status,
assignee_type: assigneeType,
labels,
},
});
}
}
export default new ConversationApi();

View file

@ -0,0 +1,21 @@
/* global axios */
import ApiClient from './ApiClient';
class IntegrationsAPI extends ApiClient {
constructor() {
super('integrations/apps', { accountScoped: true });
}
connectSlack(code) {
return axios.post(`${this.baseUrl()}/integrations/slack`, {
code: code,
});
}
delete(integrationId) {
return axios.delete(`${this.baseUrl()}/integrations/${integrationId}`);
}
}
export default new IntegrationsAPI();

View file

@ -0,0 +1,9 @@
import ApiClient from './ApiClient';
class LabelsAPI extends ApiClient {
constructor() {
super('labels', { accountScoped: true });
}
}
export default new LabelsAPI();

View file

@ -9,7 +9,5 @@ describe('#FBChannel', () => {
expect(fbChannel).toHaveProperty('create');
expect(fbChannel).toHaveProperty('update');
expect(fbChannel).toHaveProperty('delete');
expect(fbChannel).toHaveProperty('markSeen');
expect(fbChannel).toHaveProperty('toggleTyping');
});
});

View file

@ -1,14 +1,14 @@
import agents from '../contacts';
import contacts from '../contacts';
import ApiClient from '../ApiClient';
describe('#ContactsAPI', () => {
it('creates correct instance', () => {
expect(agents).toBeInstanceOf(ApiClient);
expect(agents).toHaveProperty('get');
expect(agents).toHaveProperty('show');
expect(agents).toHaveProperty('create');
expect(agents).toHaveProperty('update');
expect(agents).toHaveProperty('delete');
expect(agents).toHaveProperty('getConversations');
expect(contacts).toBeInstanceOf(ApiClient);
expect(contacts).toHaveProperty('get');
expect(contacts).toHaveProperty('show');
expect(contacts).toHaveProperty('create');
expect(contacts).toHaveProperty('update');
expect(contacts).toHaveProperty('delete');
expect(contacts).toHaveProperty('getConversations');
});
});

View file

@ -0,0 +1,19 @@
import conversationAPI from '../../inbox/conversation';
import ApiClient from '../../ApiClient';
describe('#ConversationAPI', () => {
it('creates correct instance', () => {
expect(conversationAPI).toBeInstanceOf(ApiClient);
expect(conversationAPI).toHaveProperty('get');
expect(conversationAPI).toHaveProperty('show');
expect(conversationAPI).toHaveProperty('create');
expect(conversationAPI).toHaveProperty('update');
expect(conversationAPI).toHaveProperty('delete');
expect(conversationAPI).toHaveProperty('toggleStatus');
expect(conversationAPI).toHaveProperty('assignAgent');
expect(conversationAPI).toHaveProperty('markMessageRead');
expect(conversationAPI).toHaveProperty('toggleTyping');
expect(conversationAPI).toHaveProperty('mute');
expect(conversationAPI).toHaveProperty('meta');
});
});

View file

@ -1,7 +1,7 @@
import inboxes from '../inboxes';
import ApiClient from '../ApiClient';
describe('#AgentAPI', () => {
describe('#InboxesAPI', () => {
it('creates correct instance', () => {
expect(inboxes).toBeInstanceOf(ApiClient);
expect(inboxes).toHaveProperty('get');

View file

@ -0,0 +1,14 @@
import labels from '../labels';
import ApiClient from '../ApiClient';
describe('#LabelsAPI', () => {
it('creates correct instance', () => {
expect(labels).toBeInstanceOf(ApiClient);
expect(labels).toHaveProperty('get');
expect(labels).toHaveProperty('show');
expect(labels).toHaveProperty('create');
expect(labels).toHaveProperty('update');
expect(labels).toHaveProperty('delete');
expect(labels.url).toBe('/api/v1/labels');
});
});

View file

@ -1,6 +1,6 @@
.button {
font-weight: $font-weight-medium;
font-family: $body-font-family;
font-weight: $font-weight-medium;
&.round {
border-radius: 1000px;
@ -20,10 +20,11 @@
}
.tooltip {
max-width: 15rem;
padding: $space-smaller $space-small;
border-radius: $space-smaller;
font-size: $font-size-mini;
max-width: 15rem;
padding: $space-smaller $space-small;
z-index: 9999;
}
code {

View file

@ -67,6 +67,7 @@ BlinkMacSystemFont,
"Segoe UI",
Roboto,
"Helvetica Neue",
Tahoma,
Arial,
sans-serif;
$body-antialiased: true;
@ -382,7 +383,7 @@ $label-color: $primary-color;
$label-color-alt: $black;
$label-palette: $foundation-palette;
$label-font-size: $font-size-micro;
$label-padding: $space-micro $space-smaller;
$label-padding: $space-smaller $space-small;
$label-radius: $space-micro;
// 21. Media Object

View file

@ -25,7 +25,7 @@
width: $space-medium;
&.message {
@include elegent-shadow;
@include normal-shadow;
background: $color-white;
border-radius: $space-large;
left: 0;

View file

@ -1,6 +1,5 @@
@import '~widget/assets/scss/mixins';
$elegant-shadow-color: rgba(49, 49, 93, 0.15);
$spinner-before-border-color: rgba(255, 255, 255, 0.7);
//borders
@ -141,12 +140,8 @@ $spinner-before-border-color: rgba(255, 255, 255, 0.7);
overflow-y: auto;
}
@mixin elegent-shadow() {
box-shadow: 0 10px 25px 0 $elegant-shadow-color;
}
@mixin elegant-card() {
@include elegent-shadow;
@include normal-shadow;
border-radius: $space-small;
}
@ -194,28 +189,42 @@ $spinner-before-border-color: rgba(255, 255, 255, 0.7);
border-bottom: $size solid $color;
border-left: $size solid transparent;
border-right: $size solid transparent;
} @else if $direction == 'right' {
}
@else if $direction == 'right' {
border-bottom: $size solid transparent;
border-left: $size solid $color;
border-top: $size solid transparent;
} @else if $direction == 'bottom' {
}
@else if $direction == 'bottom' {
border-left: $size solid transparent;
border-right: $size solid transparent;
border-top: $size solid $color;
} @else if $direction == 'left' {
}
@else if $direction == 'left' {
border-bottom: $size solid transparent;
border-right: $size solid $color;
border-top: $size solid transparent;
} @else if $direction == 'top-left' {
}
@else if $direction == 'top-left' {
border-right: $size solid transparent;
border-top: $size solid $color;
} @else if $direction == 'top-right' {
}
@else if $direction == 'top-right' {
border-left: $size solid transparent;
border-top: $size solid $color;
} @else if $direction == 'bottom-left' {
}
@else if $direction == 'bottom-left' {
border-bottom: $size solid $color;
border-right: $size solid transparent;
} @else if $direction == 'bottom-right' {
}
@else if $direction == 'bottom-right' {
border-bottom: $size solid $color;
border-left: $size solid transparent;
}

View file

@ -3,7 +3,6 @@
@import 'animations';
@import 'foundation-custom';
@import 'widgets/billing';
@import 'widgets/buttons';
@import 'widgets/conv-header';
@import 'widgets/conversation-card';

View file

@ -3,16 +3,17 @@
background: $color-white;
border: 1px solid $color-border;
border-radius: $space-smaller;
margin-bottom: $space-normal;
padding: $space-normal;
.integration--image {
display: flex;
margin-right: $space-normal;
width: 8rem;
width: 10rem;
img {
max-width: 8rem;
padding: $space-small;
max-width: 100%;
padding: $space-medium;
}
}

View file

@ -1,75 +0,0 @@
.billing {
@include full-height;
.row {
@include full-height;
}
.billing__stats {
@include flex;
}
.billing__form {
@include thin-border($color-border-light);
@include margin($zero - $space-micro);
@include full-height;
background: $color-white;
iframe {
@include full-height;
border: 0;
width: 100%;
}
}
.account-row {
@include padding($space-normal);
@include flex;
flex-direction: column;
// @include thin-border($color-border-light);
// @include margin(-$space-micro $zero);
background: $color-white;
font-size: $font-size-small;
.title {
color: $color-heading;
font-weight: $font-weight-medium;
}
.value {
font-size: $font-size-mega;
font-weight: $font-weight-light;
text-transform: capitalize;
}
}
}
.account-locked {
@include background-gray;
@include margin(0);
}
.lock-message {
@include flex;
@include full-height;
flex-direction: column;
@include flex-align(center, middle);
div {
@include flex;
@include full-height;
flex-direction: column;
@include flex-align(center, middle);
img {
@include margin($space-normal);
width: 10rem;
}
span {
font-size: $font-size-small;
font-weight: $font-weight-medium;
text-align: center;
}
}
}

View file

@ -66,6 +66,8 @@
}
.button.resolve--button {
@include flex-align($x: center, $y: middle);
width: 13.2rem;
>.icon {

View file

@ -78,6 +78,11 @@
font-size: $font-size-mini;
vertical-align: top;
}
.message-from-agent {
color: $color-gray;
font-size: $font-size-mini;
}
}
.conversation--meta {
@ -120,11 +125,11 @@
}
.conversation--message {
font-weight: $font-weight-medium;
font-weight: $font-weight-bold;
}
.conversation--user {
font-weight: $font-weight-medium;
font-weight: $font-weight-bold;
}
}

View file

@ -26,6 +26,7 @@
.link {
color: $color-white;
text-decoration: underline;
}
}
@ -308,7 +309,7 @@
&.is-private {
background: lighten($warning-color, 32%);
border: 1px solid $color-border;
border: 1px solid lighten($warning-color, 15%);
color: $color-heading;
padding-right: $space-large;
position: relative;

View file

@ -67,6 +67,10 @@
font-size: $font-size-small;
}
.content {
@include padding($space-large);
}
form {
@include padding($space-large);
align-self: center;

View file

@ -1,10 +1,15 @@
.reply-box {
@include elegant-card;
@include light-shadow;
border-bottom: 0;
border-radius: $space-small;
margin: $space-normal;
margin-top: 0;
max-height: $space-jumbo * 2;
transition: height 2s $ease-in-out-cubic;
max-height: $space-mega * 3;
transition: box-shadow .35s $ease-in-out-cubic, height 2s $ease-in-out-cubic;
&.is-focused {
@include normal-shadow;
}
.reply-box__top {
@include flex;
@ -42,7 +47,7 @@
&.is-private {
background: lighten($warning-color, 38%);
>input {
> input {
background: lighten($warning-color, 38%);
}
}
@ -58,7 +63,7 @@
}
}
.file-uploads>label {
.file-uploads > label {
cursor: pointer;
}
@ -68,13 +73,14 @@
padding: 0 $space-small;
}
>textarea {
> textarea {
@include ghost-input();
@include margin(0);
background: transparent;
// Override min-height : 50px in foundation
//
min-height: 1rem;
max-height: $space-mega * 2.4;
min-height: 4rem;
resize: none;
}
}

View file

@ -3,7 +3,7 @@
<div class="chat-list__top">
<h1 class="page-title">
<woot-sidemenu-icon />
{{ inbox.name || $t('CHAT_LIST.TAB_HEADING') }}
{{ pageTitle }}
</h1>
<chat-filter @statusFilterChange="updateStatusType" />
</div>
@ -15,14 +15,15 @@
@chatTabChange="updateAssigneeTab"
/>
<p v-if="!chatListLoading && !getChatsForTab().length" class="content-box">
<p v-if="!chatListLoading && !conversationList.length" class="content-box">
{{ $t('CHAT_LIST.LIST.404') }}
</p>
<div class="conversations-list">
<conversation-card
v-for="chat in getChatsForTab()"
v-for="chat in conversationList"
:key="chat.id"
:active-label="label"
:chat="chat"
/>
@ -40,7 +41,7 @@
<p
v-if="
getChatsForTab().length &&
conversationList.length &&
hasCurrentPageEndReached &&
!chatListLoading
"
@ -55,6 +56,7 @@
<script>
/* eslint-env browser */
/* eslint no-console: 0 */
/* global bus */
import { mapGetters } from 'vuex';
import ChatFilter from './widgets/conversation/ChatFilter';
@ -71,7 +73,16 @@ export default {
ChatFilter,
},
mixins: [timeMixin, conversationMixin],
props: ['conversationInbox'],
props: {
conversationInbox: {
type: [String, Number],
default: 0,
},
label: {
type: String,
default: '',
},
},
data() {
return {
activeAssigneeTab: wootConstants.ASSIGNEE_TYPE.ME,
@ -87,14 +98,17 @@ export default {
chatListLoading: 'getChatListLoadingStatus',
currentUserID: 'getCurrentUserID',
activeInbox: 'getSelectedInbox',
convStats: 'getConvTabStats',
conversationStats: 'conversationStats/getStats',
}),
assigneeTabItems() {
return this.$t('CHAT_LIST.ASSIGNEE_TYPE_TABS').map(item => ({
key: item.KEY,
name: item.NAME,
count: this.convStats[item.COUNT_KEY] || 0,
}));
return this.$t('CHAT_LIST.ASSIGNEE_TYPE_TABS').map(item => {
const count = this.conversationStats[item.COUNT_KEY] || 0;
return {
key: item.KEY,
name: item.NAME,
count,
};
});
},
inbox() {
return this.$store.getters['inboxes/getInbox'](this.activeInbox);
@ -109,16 +123,61 @@ export default {
this.activeAssigneeTab
);
},
conversationFilters() {
return {
inboxId: this.conversationInbox ? this.conversationInbox : undefined,
assigneeType: this.activeAssigneeTab,
status: this.activeStatus,
page: this.currentPage + 1,
labels: this.label ? [this.label] : undefined,
};
},
pageTitle() {
if (this.inbox.name) {
return this.inbox.name;
}
if (this.label) {
return `#${this.label}`;
}
return this.$t('CHAT_LIST.TAB_HEADING');
},
conversationList() {
let conversationList = [];
if (this.activeAssigneeTab === 'me') {
conversationList = this.mineChatsList.slice();
} else if (this.activeAssigneeTab === 'unassigned') {
conversationList = this.unAssignedChatsList.slice();
} else {
conversationList = this.allChatList.slice();
}
if (!this.label) {
return conversationList;
}
return conversationList.filter(conversation => {
const labels = this.$store.getters[
'conversationLabels/getConversationLabels'
](conversation.id);
return labels.includes(this.label);
});
},
},
watch: {
conversationInbox() {
this.resetAndFetchData();
},
label() {
this.resetAndFetchData();
},
},
mounted() {
this.$store.dispatch('setChatFilter', this.activeStatus);
this.resetAndFetchData();
this.$store.dispatch('agents/get');
bus.$on('fetch_conversation_stats', () => {
this.$store.dispatch('conversationStats/get', this.conversationFilters);
});
},
methods: {
resetAndFetchData() {
@ -127,12 +186,7 @@ export default {
this.fetchConversations();
},
fetchConversations() {
this.$store.dispatch('fetchAllConversations', {
inboxId: this.conversationInbox ? this.conversationInbox : undefined,
assigneeType: this.activeAssigneeTab,
status: this.activeStatus,
page: this.currentPage + 1,
});
this.$store.dispatch('fetchAllConversations', this.conversationFilters);
},
updateAssigneeTab(selectedTab) {
if (this.activeAssigneeTab !== selectedTab) {
@ -148,17 +202,6 @@ export default {
this.resetAndFetchData();
}
},
getChatsForTab() {
let copyList = [];
if (this.activeAssigneeTab === 'me') {
copyList = this.mineChatsList.slice();
} else if (this.activeAssigneeTab === 'unassigned') {
copyList = this.unAssignedChatsList.slice();
} else {
copyList = this.allChatList.slice();
}
return copyList;
},
},
};
</script>

View file

@ -6,6 +6,7 @@ import Code from './Code';
import ColorPicker from './widgets/ColorPicker';
import DeleteModal from './widgets/modal/DeleteModal.vue';
import Input from './widgets/forms/Input.vue';
import Label from './widgets/Label.vue';
import LoadingState from './widgets/LoadingState';
import Modal from './Modal';
import ModalHeader from './ModalHeader';
@ -25,6 +26,7 @@ const WootUIKit = {
DeleteModal,
Input,
LoadingState,
Label,
Modal,
ModalHeader,
ReportStatsCard,

View file

@ -18,21 +18,14 @@
:key="inboxSection.toState"
:menu-item="inboxSection"
/>
<sidebar-item
v-if="shouldShowInboxes"
:key="labelSection.toState"
:menu-item="labelSection"
/>
</transition-group>
</div>
<!-- this block is only required in the hosted version with billing enabled -->
<transition name="fade" mode="out-in">
<woot-status-bar
v-if="shouldShowStatusBox"
:message="trialMessage"
:button-text="$t('APP_GLOBAL.TRAIL_BUTTON')"
:button-route="{ name: 'billing' }"
:type="statusBarClass"
:show-button="isAdmin"
/>
</transition>
<div class="bottom-nav">
<transition name="menu-slide">
<div
@ -63,7 +56,11 @@
</div>
</transition>
<div class="current-user" @click.prevent="showOptions()">
<thumbnail :src="currentUser.avatar_url" :username="currentUser.name" />
<thumbnail
:src="currentUser.avatar_url"
:username="currentUser.name"
:status="currentUser.availability_status"
/>
<div class="current-user--data">
<h3 class="current-user--name">
{{ currentUser.name }}
@ -108,7 +105,6 @@ import { mixin as clickaway } from 'vue-clickaway';
import adminMixin from '../../mixins/isAdmin';
import Auth from '../../api/auth';
import SidebarItem from './SidebarItem';
import WootStatusBar from '../widgets/StatusBar';
import { frontendURL } from '../../helper/URLHelper';
import Thumbnail from '../widgets/Thumbnail';
import { getSidebarItems } from '../../i18n/default-sidebar';
@ -116,7 +112,6 @@ import { getSidebarItems } from '../../i18n/default-sidebar';
export default {
components: {
SidebarItem,
WootStatusBar,
Thumbnail,
},
mixins: [clickaway, adminMixin],
@ -135,12 +130,11 @@ export default {
computed: {
...mapGetters({
currentUser: 'getCurrentUser',
daysLeft: 'getTrialLeft',
globalConfig: 'globalConfig/get',
inboxes: 'inboxes/getInboxes',
subscriptionData: 'getSubscription',
accountId: 'getCurrentAccountId',
currentRole: 'getCurrentRole',
accountLabels: 'labels/getLabelsOnSidebar',
}),
sidemenuItems() {
return getSidebarItems(this.accountId);
@ -160,10 +154,6 @@ export default {
}
}
if (!window.chatwootConfig.billingEnabled) {
menuItems = this.filterBillingRoutes(menuItems);
}
return this.filterMenuItemsByRole(menuItems);
},
currentRoute() {
@ -190,38 +180,33 @@ export default {
})),
};
},
labelSection() {
return {
icon: 'ion-pound',
label: 'LABELS',
hasSubMenu: true,
key: 'label',
cssClass: 'menu-title align-justify',
toState: frontendURL(`accounts/${this.accountId}/settings/labels`),
toStateName: 'labels_list',
children: this.accountLabels.map(label => ({
id: label.id,
label: label.title,
color: label.color,
toState: frontendURL(
`accounts/${this.accountId}/label/${label.title}`
),
})),
};
},
dashboardPath() {
return frontendURL(`accounts/${this.accountId}/dashboard`);
},
shouldShowStatusBox() {
return (
window.chatwootConfig.billingEnabled &&
(this.subscriptionData.state === 'trial' ||
this.subscriptionData.state === 'cancelled')
);
},
statusBarClass() {
if (this.subscriptionData.state === 'trial') {
return 'warning';
}
if (this.subscriptionData.state === 'cancelled') {
return 'danger';
}
return '';
},
trialMessage() {
return `${this.daysLeft} ${this.$t('APP_GLOBAL.TRIAL_MESSAGE')}`;
},
},
mounted() {
this.$store.dispatch('inboxes/get');
},
methods: {
filterBillingRoutes(menuItems) {
return menuItems.filter(
menuItem => !menuItem.toState.includes('billing')
);
},
filterMenuItemsByRole(menuItems) {
if (!this.currentRole) {
return [];

View file

@ -36,7 +36,13 @@
v-if="computedInboxClass(child)"
class="inbox-icon"
:class="computedInboxClass(child)"
></i>
/>
<span
v-if="child.color"
class="label-color--display"
:style="{ backgroundColor: child.color }"
/>
{{ child.label }}
</div>
</a>
@ -126,8 +132,22 @@ export default {
};
</script>
<style lang="scss" scoped>
@import '~dashboard/assets/scss/variables';
.sub-menu-title {
display: flex;
justify-content: space-between;
}
.wrap {
display: flex;
align-items: center;
}
.label-color--display {
border-radius: $space-smaller;
height: $space-normal;
margin-right: $space-small;
width: $space-normal;
}
</style>

View file

@ -1,14 +1,26 @@
<template>
<span class="back-button ion-ios-arrow-left" @click.capture="goBack">Back</span>
<span class="back-button ion-ios-arrow-left" @click.capture="goBack">
{{ $t('GENERAL_SETTINGS.BACK') }}
</span>
</template>
<script>
import router from '../../routes/index';
export default {
props: {
backUrl: {
type: [String, Object],
default: '',
},
},
methods: {
goBack() {
router.go(-1);
if (this.backUrl !== '') {
router.push(this.backUrl);
} else {
router.go(-1);
}
},
},
};
</script>
</script>

View file

@ -40,10 +40,23 @@ export default {
type: String,
required: true,
},
enabledFeatures: {
type: Object,
required: true,
},
},
methods: {
isActive(channel) {
return ['facebook', 'website', 'twitter', 'twilio'].includes(channel);
if (Object.keys(this.enabledFeatures) === 0) {
return false;
}
if (channel === 'facebook') {
return this.enabledFeatures.channel_facebook;
}
if (channel === 'twitter') {
return this.enabledFeatures.channel_facebook;
}
return ['website', 'twilio'].includes(channel);
},
onItemClick() {
if (this.isActive(this.channel)) {

View file

@ -22,11 +22,6 @@ export default {
default: wootConstants.ASSIGNEE_TYPE.ME,
},
},
data() {
return {
tabsIndex: wootConstants.ASSIGNEE_TYPE.ME,
};
},
computed: {
activeTabIndex() {
return this.items.findIndex(item => item.key === this.activeTab);

View file

@ -0,0 +1,91 @@
<template>
<div
:class="labelClass"
:style="{ background: bgColor, color: textColor }"
:title="description"
>
<span v-if="!href">{{ title }}</span>
<a v-else :href="href" :style="{ color: textColor }">{{ title }}</a>
<i v-if="showIcon" class="label--icon" :class="icon" @click="onClick" />
</div>
</template>
<script>
export default {
props: {
title: {
type: String,
required: true,
},
description: {
type: String,
default: '',
},
href: {
type: String,
default: '',
},
bgColor: {
type: String,
default: '#1f93ff',
},
small: {
type: Boolean,
default: false,
},
showIcon: {
type: Boolean,
default: false,
},
icon: {
type: String,
default: 'ion-close',
},
},
computed: {
textColor() {
const color = this.bgColor.replace('#', '');
const r = parseInt(color.slice(0, 2), 16);
const g = parseInt(color.slice(2, 4), 16);
const b = parseInt(color.slice(4, 6), 16);
// http://stackoverflow.com/a/3943023/112731
return r * 0.299 + g * 0.587 + b * 0.114 > 186 ? '#000000' : '#FFFFFF';
},
labelClass() {
return `label ${this.small ? 'small' : ''}`;
},
},
methods: {
onClick() {
this.$emit('click', this.title);
},
},
};
</script>
<style scoped lang="scss">
@import '~dashboard/assets/scss/variables';
.label {
display: inline-block;
font-size: $font-size-small;
line-height: 1;
margin: $space-micro;
&.small {
font-size: $font-size-mini;
}
a {
&:hover {
text-decoration: underline;
}
}
}
.label--icon {
cursor: pointer;
font-size: $font-size-micro;
line-height: 1.5;
margin-left: $space-smaller;
}
</style>

View file

@ -1,24 +0,0 @@
<template>
<div class="status-bar" :class="type">
<p class="message">{{message}}</p>
<router-link
:to="buttonRoute"
class="button small warning nice"
v-if="showButton"
>
{{buttonText}}
</router-link>
</div>
</template>
<script>
export default {
props: {
message: String,
buttonRoute: Object,
buttonText: String,
showButton: Boolean,
type: String, // Danger, Info, Success, Warning
},
};
</script>

View file

@ -21,11 +21,6 @@
:style="badgeStyle"
src="~dashboard/assets/images/fb-badge.png"
/>
<div
v-else-if="status === 'online'"
class="source-badge user--online"
:style="statusStyle"
></div>
<img
v-if="badge === 'Channel::TwitterProfile'"
id="badge"
@ -33,7 +28,6 @@
:style="badgeStyle"
src="~dashboard/assets/images/twitter-badge.png"
/>
<img
v-if="badge === 'Channel::TwilioSms'"
id="badge"
@ -41,6 +35,11 @@
:style="badgeStyle"
src="~dashboard/assets/images/channels/whatsapp.png"
/>
<div
v-if="showStatusIndicator"
:class="`source-badge user-online-status user-online-status--${status}`"
:style="statusStyle"
/>
</div>
</template>
<script>
@ -89,6 +88,9 @@ export default {
};
},
computed: {
showStatusIndicator() {
return this.status === 'online' || this.status === 'busy';
},
avatarSize() {
return Number(this.size.replace(/\D+/g, ''));
},
@ -150,8 +152,7 @@ export default {
width: $space-slab;
}
.user--online {
background: $success-color;
.user-online-status {
border-radius: 50%;
bottom: $space-micro;
@ -159,5 +160,13 @@ export default {
content: ' ';
}
}
.user-online-status--online {
background: $success-color;
}
.user-online-status--busy {
background: $warning-color;
}
}
</style>

View file

@ -7,9 +7,10 @@
<Thumbnail
v-if="!hideThumbnail"
:src="currentContact.thumbnail"
:badge="currentContact.channel"
:badge="chatMetadata.channel"
class="columns"
:username="currentContact.name"
:status="currentContact.availability_status"
size="40px"
/>
<div class="conversation--details columns">
@ -23,15 +24,20 @@
{{ inboxName(chat.inbox_id) }}
</span>
</h4>
<p
class="conversation--message"
v-html="extractMessageText(lastMessageInChat)"
/>
<p v-if="lastMessageInChat" class="conversation--message">
<i v-if="messageByAgent" class="ion-ios-undo message-from-agent"></i>
<span v-if="lastMessageInChat.content">
{{ lastMessageInChat.content }}
</span>
<span v-else-if="!lastMessageInChat.attachments">{{ ` ` }}</span>
<span v-else>
<i :class="`small-icon ${this.$t(`${attachmentIconKey}.ICON`)}`"></i>
{{ this.$t(`${attachmentIconKey}.CONTENT`) }}
</span>
</p>
<div class="conversation--meta">
<span class="timestamp">
{{
lastMessageInChat ? dynamicTime(lastMessageInChat.created_at) : ''
}}
{{ dynamicTime(chat.timestamp) }}
</span>
<span class="unread">{{ getUnreadCount }}</span>
</div>
@ -39,11 +45,10 @@
</div>
</template>
<script>
/* eslint no-console: 0 */
/* eslint no-extra-boolean-cast: 0 */
import { mapGetters } from 'vuex';
import { MESSAGE_TYPE } from 'widget/helpers/constants';
import Thumbnail from '../Thumbnail';
import getEmojiSVG from '../emoji/utils';
import conversationMixin from '../../../mixins/conversations';
import timeMixin from '../../../mixins/time';
import router from '../../../routes';
@ -56,6 +61,10 @@ export default {
mixins: [timeMixin, conversationMixin],
props: {
activeLabel: {
type: String,
default: '',
},
chat: {
type: Object,
default: () => {},
@ -79,12 +88,22 @@ export default {
accountId: 'getCurrentAccountId',
}),
chatMetadata() {
return this.chat.meta;
},
currentContact() {
return this.$store.getters['contacts/getContact'](
this.chat.meta.sender.id
this.chatMetadata.sender.id
);
},
attachmentIconKey() {
const lastMessage = this.lastMessageInChat;
const [{ file_type: fileType } = {}] = lastMessage.attachments;
return `CHAT_LIST.ATTACHMENTS.${fileType}`;
},
isActiveChat() {
return this.currentChat.id === this.chat.id;
},
@ -104,38 +123,25 @@ export default {
lastMessageInChat() {
return this.lastMessage(this.chat);
},
messageByAgent() {
const lastMessage = this.lastMessageInChat;
const { message_type: messageType } = lastMessage;
return messageType === MESSAGE_TYPE.OUTGOING;
},
},
methods: {
cardClick(chat) {
const { activeInbox } = this;
const path = conversationUrl(this.accountId, activeInbox, chat.id);
const path = conversationUrl({
accountId: this.accountId,
activeInbox,
id: chat.id,
label: this.activeLabel,
});
router.push({ path: frontendURL(path) });
},
extractMessageText(chatItem) {
if (!chatItem) {
return '';
}
const { content, attachments } = chatItem;
if (content) {
return content;
}
if (!attachments) {
return ' ';
}
const [attachment] = attachments;
const { file_type: fileType } = attachment;
const key = `CHAT_LIST.ATTACHMENTS.${fileType}`;
return `
<i class="small-icon ${this.$t(`${key}.ICON`)}"></i>
${this.$t(`${key}.CONTENT`)}
`;
},
getEmojiSVG,
inboxName(inboxId) {
const stateInbox = this.$store.getters['inboxes/getInbox'](inboxId);
return stateInbox.name || '';

View file

@ -4,8 +4,9 @@
<Thumbnail
:src="currentContact.thumbnail"
size="40px"
:badge="currentContact.channel"
:badge="chatMetadata.channel"
:username="currentContact.name"
:status="currentContact.availability_status"
/>
<div class="user--profile__meta">
<h3 v-if="!isContactPanelOpen" class="user--name text-truncate">
@ -28,7 +29,7 @@
:allow-empty="true"
deselect-label="Remove"
placeholder="Select Agent"
selected-label=""
selected-label
select-label="Assign"
track-by="id"
@select="assignAgent"
@ -80,6 +81,10 @@ export default {
currentChat: 'getSelectedChat',
}),
chatMetadata() {
return this.chat.meta;
},
currentContact() {
return this.$store.getters['contacts/getContact'](
this.chat.meta.sender.id

View file

@ -121,23 +121,6 @@ export default {
);
return chat;
},
// Get current FB Page ID
getPageId() {
let stateInbox;
if (this.inboxId) {
const inboxId = Number(this.inboxId);
[stateInbox] = this.inboxesList.filter(
inbox => inbox.channel_id === inboxId
);
} else {
[stateInbox] = this.inboxesList;
}
return !stateInbox ? 0 : stateInbox.page_id;
},
// Get current FB Page ID link
linkToMessage() {
return `https://m.me/${this.getPageId}`;
},
getReadMessages() {
const chat = this.getMessages;
return chat === undefined ? null : this.readMessages(chat);

View file

@ -1,5 +1,5 @@
<template>
<div class="reply-box">
<div class="reply-box" :class="replyBoxClass">
<div class="reply-box__top" :class="{ 'is-private': isPrivate }">
<canned-response
v-if="showCannedResponsesList"
@ -13,13 +13,12 @@
v-on-clickaway="hideEmojiPicker"
:on-click="emojiOnClick"
/>
<textarea
<resizable-text-area
ref="messageInput"
v-model="message"
rows="1"
class="input"
type="text"
:placeholder="$t(messagePlaceHolder())"
:min-height="4"
@focus="onFocus"
@blur="onBlur"
/>
@ -93,18 +92,21 @@ import FileUpload from 'vue-upload-component';
import EmojiInput from '../emoji/EmojiInput';
import CannedResponse from './CannedResponse';
import ResizableTextArea from 'shared/components/ResizableTextArea';
export default {
components: {
EmojiInput,
CannedResponse,
FileUpload,
ResizableTextArea,
},
mixins: [clickaway],
data() {
return {
message: '',
isPrivate: false,
isFocused: false,
showEmojiPicker: false,
showCannedResponsesList: false,
isUploading: {
@ -119,12 +121,7 @@ export default {
currentChat: 'getSelectedChat',
}),
channelType() {
const {
meta: {
sender: { channel },
},
} = this.currentChat;
return channel;
return this.currentChat.meta.channel;
},
conversationType() {
const { additional_attributes: additionalAttributes } = this.currentChat;
@ -143,10 +140,7 @@ export default {
return 10000;
},
showFileUpload() {
return (
this.channelType === 'Channel::WebWidget' ||
this.channelType === 'Channel::TwilioSms'
);
return this.channelType === 'Channel::WebWidget';
},
replyButtonLabel() {
if (this.isPrivate) {
@ -157,6 +151,11 @@ export default {
}
return this.$t('CONVERSATION.REPLYBOX.SEND');
},
replyBoxClass() {
return {
'is-focused': this.isFocused,
};
},
},
watch: {
message(val) {
@ -211,18 +210,19 @@ export default {
if (this.message.length > this.maxLength) {
return;
}
const newMessage = this.message;
if (!this.showCannedResponsesList) {
this.clearMessage();
try {
await this.$store.dispatch('sendMessage', {
conversationId: this.currentChat.id,
message: this.message,
message: newMessage,
private: this.isPrivate,
});
this.$emit('scrollToMessage');
} catch (error) {
// Error
}
this.clearMessage();
this.hideEmojiPicker();
}
},
@ -260,16 +260,18 @@ export default {
},
onBlur() {
this.isFocused = false;
this.toggleTyping('off');
},
onFocus() {
this.isFocused = true;
this.toggleTyping('on');
},
toggleTyping(status) {
if (this.channelType === 'Channel::WebWidget' && !this.isPrivate) {
const conversationId = this.currentChat.id;
this.$store.dispatch('toggleTyping', {
this.$store.dispatch('conversationTypingStatus/toggleTyping', {
status,
conversationId,
});

View file

@ -1,15 +1,8 @@
/* eslint no-console: 0 */
import constants from '../constants';
import Auth from '../api/auth';
import router from '../routes';
const parseErrorCode = error => {
const errorStatus = error.response ? error.response.status : undefined;
// 901, 902 are used to identify billing related issues
if ([901, 902].includes(errorStatus)) {
const name = Auth.isAdmin() ? 'billing' : 'billing_deactivated';
router.push({ name });
}
return Promise.reject(error);
};

View file

@ -5,11 +5,14 @@ export const frontendURL = (path, params) => {
return `/app/${path}${stringifiedParams}`;
};
export const conversationUrl = (accountId, activeInbox, id) => {
const path = activeInbox
? `accounts/${accountId}/inbox/${activeInbox}/conversations/${id}`
: `accounts/${accountId}/conversations/${id}`;
return path;
export const conversationUrl = ({ accountId, activeInbox, id, label }) => {
if (activeInbox) {
return `accounts/${accountId}/inbox/${activeInbox}/conversations/${id}`;
}
if (label) {
return `accounts/${accountId}/label/${label}/conversations/${id}`;
}
return `accounts/${accountId}/conversations/${id}`;
};
export const accountIdFromPathname = pathname => {

View file

@ -1,5 +1,6 @@
import AuthAPI from '../api/auth';
import BaseActionCableConnector from '../../shared/helpers/BaseActionCableConnector';
/* global bus */
class ActionCableConnector extends BaseActionCableConnector {
constructor(app, pubsubToken) {
@ -17,6 +18,7 @@ class ActionCableConnector extends BaseActionCableConnector {
'conversation.typing_on': this.onTypingOn,
'conversation.typing_off': this.onTypingOff,
'conversation.contact_changed': this.onConversationContactChange,
'presence.update': this.onPresenceUpdate,
};
}
@ -28,6 +30,12 @@ class ActionCableConnector extends BaseActionCableConnector {
this.app.$store.dispatch('updateMessage', data);
};
onPresenceUpdate = data => {
this.app.$store.dispatch('contacts/updatePresence', data.contacts);
this.app.$store.dispatch('agents/updatePresence', data.users);
this.app.$store.dispatch('setCurrentUserAvailabilityStatus', data.users);
};
onConversationContactChange = payload => {
const { meta = {}, id: conversationId } = payload;
const { sender } = meta || {};
@ -45,10 +53,12 @@ class ActionCableConnector extends BaseActionCableConnector {
if (id) {
this.app.$store.dispatch('updateAssignee', { id, assignee });
}
this.fetchConversationStats();
};
onConversationCreated = data => {
this.app.$store.dispatch('addConversation', data);
this.fetchConversationStats();
};
onLogout = () => AuthAPI.logout();
@ -61,6 +71,7 @@ class ActionCableConnector extends BaseActionCableConnector {
onStatusChange = data => {
this.app.$store.dispatch('updateConversation', data);
this.fetchConversationStats();
};
onTypingOn = ({ conversation, user }) => {
@ -100,6 +111,10 @@ class ActionCableConnector extends BaseActionCableConnector {
this.onTypingOff({ conversation, user });
}, 30000);
};
fetchConversationStats = () => {
bus.$emit('fetch_conversation_stats');
};
}
export default {

View file

@ -7,15 +7,20 @@ import {
describe('#URL Helpers', () => {
describe('conversationUrl', () => {
it('should return direct conversation URL if activeInbox is nil', () => {
expect(conversationUrl(1, undefined, 1)).toBe(
expect(conversationUrl({ accountId: 1, id: 1 })).toBe(
'accounts/1/conversations/1'
);
});
it('should return ibox conversation URL if activeInbox is not nil', () => {
expect(conversationUrl(1, 2, 1)).toBe(
it('should return inbox conversation URL if activeInbox is not nil', () => {
expect(conversationUrl({ accountId: 1, id: 1, activeInbox: 2 })).toBe(
'accounts/1/inbox/2/conversations/1'
);
});
it('should return correct conversation URL if label is active', () => {
expect(
conversationUrl({ accountId: 1, label: 'customer-support', id: 1 })
).toBe('accounts/1/label/customer-support/conversations/1');
});
});
describe('frontendURL', () => {
@ -27,16 +32,6 @@ describe('#URL Helpers', () => {
});
});
/*
export const accountIdFromPathname = pathname => {
const isInsideAccountScopedURLs = pathname.includes('/app/accounts');
const accountId = isInsideAccountScopedURLs ? pathname.split('/')[3] : '';
return Number(accountId);
};
*/
describe('accountIdFromPathname', () => {
it('should return account id if accont scoped url is passed', () => {
expect(accountIdFromPathname('/app/accounts/1/settings/general')).toBe(1);

View file

@ -8,9 +8,10 @@ export const getSidebarItems = accountId => ({
'inbox_conversation',
'conversation_through_inbox',
'settings_account_reports',
'billing_deactivated',
'profile_settings',
'profile_settings_index',
'label_conversations',
'conversations_through_label',
],
menuItems: {
assignedToMe: {
@ -41,9 +42,8 @@ export const getSidebarItems = accountId => ({
settings: {
routes: [
'agent_list',
'agent_new',
'canned_list',
'canned_new',
'labels_list',
'settings_inbox',
'settings_inbox_new',
'settings_inbox_list',
@ -51,9 +51,9 @@ export const getSidebarItems = accountId => ({
'settings_inboxes_page_channel',
'settings_inboxes_add_agents',
'settings_inbox_finish',
'billing',
'settings_integrations',
'settings_integrations_webhook',
'settings_integrations_integration',
'general_settings',
'general_settings_index',
],
@ -79,6 +79,13 @@ export const getSidebarItems = accountId => ({
toState: frontendURL(`accounts/${accountId}/settings/inboxes/list`),
toStateName: 'settings_inbox_list',
},
labels: {
icon: 'ion-pricetags',
label: 'LABELS',
hasSubMenu: false,
toState: frontendURL(`accounts/${accountId}/settings/labels/list`),
toStateName: 'labels_list',
},
cannedResponses: {
icon: 'ion-chatbox-working',
label: 'CANNED_RESPONSES',
@ -88,13 +95,6 @@ export const getSidebarItems = accountId => ({
),
toStateName: 'canned_list',
},
billing: {
icon: 'ion-card',
label: 'BILLING',
hasSubMenu: false,
toState: frontendURL(`accounts/${accountId}/settings/billing`),
toStateName: 'billing',
},
settings_integrations: {
icon: 'ion-flash',
label: 'INTEGRATIONS',

Some files were not shown because too many files have changed in this diff Show more