Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
01577acb2e | ||
|
|
52c850f8b6 | ||
|
|
809509404b | ||
|
|
acb06e7df6 | ||
|
|
e849759e15 | ||
|
|
1e3f255ece | ||
|
|
a2764e5c1d | ||
|
|
c2db8a1fd7 | ||
|
|
bb18516403 | ||
|
|
d1a62fe6ab | ||
|
|
f68a4b55bb | ||
|
|
987c8f5c7c | ||
|
|
e011b4566b | ||
|
|
37a36349f2 | ||
|
|
cf5f6d5a74 | ||
|
|
d78cb67a2a | ||
|
|
b119d9e729 | ||
|
|
564fa5f392 | ||
|
|
58aae7a4a9 | ||
|
|
c6326993df | ||
|
|
76370267f3 | ||
|
|
e12edb51a2 | ||
|
|
1dfa173b3a | ||
|
|
a4c87f2052 | ||
|
|
b3e313a200 | ||
|
|
c0f5a59cf8 | ||
|
|
358171062e | ||
|
|
6cfd7d3836 | ||
|
|
0c9b682329 | ||
|
|
a34bb565af | ||
|
|
d00b8df952 | ||
|
|
4e01278ea8 | ||
|
|
26f36548c2 | ||
|
|
81708f92a8 | ||
|
|
97ee1bfa97 | ||
|
|
03b1a3d045 | ||
|
|
0651b11fda | ||
|
|
372bd75028 | ||
|
|
68fa694268 | ||
|
|
000c7603d6 | ||
|
|
a3b6e5655d | ||
|
|
ab77e03c92 | ||
|
|
69f55a25b6 | ||
|
|
46867e89cb | ||
|
|
2f2d2b4f73 | ||
|
|
fcbe68141d | ||
|
|
ac46d0c855 | ||
|
|
74db319a7c | ||
|
|
98e9fedfa6 | ||
|
|
f2c6799444 | ||
|
|
5119e7a79e | ||
|
|
8e6ce3a813 | ||
|
|
e5e73a08fe | ||
|
|
84df9c807d | ||
|
|
06289b03ea | ||
|
|
19855a90e2 | ||
|
|
09643b63b0 | ||
|
|
658a511b88 | ||
|
|
b0041ccf3f | ||
|
|
050df43789 | ||
|
|
3b3ab39ca3 | ||
|
|
7015609577 | ||
|
|
58ad0e6fd6 | ||
|
|
b8ea8e5b5e | ||
|
|
eada8c9af2 | ||
|
|
6de964a0bd | ||
|
|
9f9dd09985 | ||
|
|
a9849e402e | ||
|
|
a2ffcc4502 | ||
|
|
c831bee0e3 | ||
|
|
3b53ca157c | ||
|
|
de0702b679 | ||
|
|
cd5c2c51d9 | ||
|
|
cebd34053b | ||
|
|
ed25435ac2 | ||
|
|
0c65db925d | ||
|
|
590001cca2 | ||
|
|
e669f6766f | ||
|
|
99abbb8158 | ||
|
|
5c30bc3e2b | ||
|
|
5799b9fa26 | ||
|
|
b33701a666 | ||
|
|
6c3e2a0bd3 | ||
|
|
a494afd0e7 | ||
|
|
6998e9aa2d | ||
|
|
6bfa551c85 | ||
|
|
7c21cf2255 | ||
|
|
5749d25ff8 | ||
|
|
c1d68cc8ae | ||
|
|
5237e51f2c | ||
|
|
2c8bf00d38 | ||
|
|
3d3f0ff6ee | ||
|
|
2c3e37b157 | ||
|
|
a35269d275 | ||
|
|
68e697c379 | ||
|
|
0e0632be22 | ||
|
|
1c4afb10df | ||
|
|
ec2dc1b61b | ||
|
|
b9e85a628b | ||
|
|
5d3cce12d5 | ||
|
|
8c192559fe | ||
|
|
700721ea6d | ||
|
|
c54aae21ff | ||
|
|
1c6a539c0a | ||
|
|
3e99088fe3 | ||
|
|
ec9903ae82 | ||
|
|
219a8c4bc5 | ||
|
|
271ca53c58 | ||
|
|
f874925f0e | ||
|
|
bd7aeba484 | ||
|
|
40d0b2faf3 | ||
|
|
30244f79a6 | ||
|
|
1d94e65ca8 | ||
|
|
be9a055a3f | ||
|
|
e6bcf24864 | ||
|
|
1083a24a26 | ||
|
|
a1563917ba | ||
|
|
57abdc4d5f | ||
|
|
49ac4a4400 | ||
|
|
cc244833d0 | ||
|
|
d55a48037a | ||
|
|
6f6bb91823 | ||
|
|
a8f6eebd66 | ||
|
|
bba2750975 | ||
|
|
6129edce08 | ||
|
|
ed22e7cad4 | ||
|
|
edd0e2329f | ||
|
|
1761bec615 | ||
|
|
15aaa8883c | ||
|
|
8b7f6c691a | ||
|
|
1cb2226dbc | ||
|
|
1f4685ae4b | ||
|
|
54bdb2957f | ||
|
|
4f51a46c2b | ||
|
|
0c24df96a8 | ||
|
|
dddab0bbce | ||
|
|
c504067e2b | ||
|
|
b59e73b10b | ||
|
|
aaadd61e09 | ||
|
|
dcbbb09ebd | ||
|
|
fa2db5a729 | ||
|
|
b1b0268705 | ||
|
|
794a56d4cc | ||
|
|
6ad5a7452c | ||
|
|
0c3e8b6dbd | ||
|
|
a0ffefad71 | ||
|
|
799e0ed4f5 | ||
|
|
928766f07b | ||
|
|
b474e39feb | ||
|
|
3abcadb5cb | ||
|
|
a14f4ede87 | ||
|
|
2396b59f11 | ||
|
|
946a09928e | ||
|
|
2c381d726d | ||
|
|
11cbc4d952 | ||
|
|
d76455e840 | ||
|
|
aa5d01b572 | ||
|
|
22d1c8baf2 | ||
|
|
ccd0dc39ad | ||
|
|
84b96526db | ||
|
|
f2e2a0b7ed | ||
|
|
328edd24de | ||
|
|
26a3150fd8 | ||
|
|
571fefd7cd | ||
|
|
b74261205d | ||
|
|
f79c52b8c3 | ||
|
|
0a38632f14 | ||
|
|
671c5c931f | ||
|
|
9c4ce9af35 | ||
|
|
2821777e93 | ||
|
|
41314157c2 | ||
|
|
c80289e661 | ||
|
|
39c4fa111a | ||
|
|
4759730022 | ||
|
|
8de4ce0037 | ||
|
|
7245e66b31 | ||
|
|
772d4ae726 | ||
|
|
118c1458ab | ||
|
|
dace97b7b0 | ||
|
|
7b77bab18e | ||
|
|
7fc575a474 | ||
|
|
9acac38635 | ||
|
|
2ebd38c3b7 | ||
|
|
b866c54ad5 | ||
|
|
ff1625708e | ||
|
|
a03ed4eea0 | ||
|
|
ad83d1bb71 | ||
|
|
a60a33679f | ||
|
|
af1d8c0ee5 | ||
|
|
2ddd508aee | ||
|
|
8b841596a5 | ||
|
|
afdf1c70c1 | ||
|
|
6fdd4a2996 | ||
|
|
06d8916341 | ||
|
|
6563cccf34 | ||
|
|
5319af3dfc | ||
|
|
1ff9939a80 | ||
|
|
fdcc322660 | ||
|
|
75329e5de1 | ||
|
|
588f49cc34 | ||
|
|
65f3e83afd | ||
|
|
f94abaef5f | ||
|
|
9a8cbee470 | ||
|
|
78646b44b8 | ||
|
|
0a08e1ffc0 | ||
|
|
5853531d9a | ||
|
|
d13c4ff8c5 | ||
|
|
69b0434bb7 | ||
|
|
1d5150e3a7 | ||
|
|
7dd42f3fd7 | ||
|
|
4a23a393f7 | ||
|
|
8d6b3efa2e | ||
|
|
a27ccc5866 | ||
|
|
a9ca76d9df | ||
|
|
21e8a41206 | ||
|
|
1257f86f22 | ||
|
|
09e3413d10 | ||
|
|
6515b69560 | ||
|
|
5debe9e8ee | ||
|
|
7cd3cef0a2 | ||
|
|
eb80324eaa |
@@ -0,0 +1,3 @@
|
||||
---
|
||||
ignore:
|
||||
- CVE-2021-41098 # https://github.com/chatwoot/chatwoot/issues/3097 (update once azure blob storage is updated)
|
||||
@@ -7,7 +7,7 @@ defaults: &defaults
|
||||
working_directory: ~/build
|
||||
docker:
|
||||
# specify the version you desire here
|
||||
- image: circleci/ruby:3.0.2-node-browsers
|
||||
- image: cimg/ruby:3.0.2-node
|
||||
|
||||
# Specify service dependencies here if necessary
|
||||
# CircleCI maintains a library of pre-built images
|
||||
@@ -40,20 +40,20 @@ jobs:
|
||||
|
||||
- restore_cache:
|
||||
keys:
|
||||
- chatwoot-bundle-{{ checksum "Gemfile.lock" }}
|
||||
- chatwoot-bundle-{{ .Environment.CACHE_VERSION }}-{{ checksum "Gemfile.lock" }}
|
||||
- chatwoot-bundle
|
||||
|
||||
- run: bundle install --frozen --path ~/.bundle
|
||||
- save_cache:
|
||||
paths:
|
||||
- ~/.bundle
|
||||
key: chatwoot-bundle-{{ checksum "Gemfile.lock" }}
|
||||
key: chatwoot-bundle-{{ .Environment.CACHE_VERSION }}-{{ checksum "Gemfile.lock" }}
|
||||
|
||||
|
||||
# Only necessary if app uses webpacker or yarn in some other way
|
||||
- restore_cache:
|
||||
keys:
|
||||
- chatwoot-yarn-{{ checksum "yarn.lock" }}
|
||||
- chatwoot-yarn-{{ .Environment.CACHE_VERSION }}-{{ checksum "yarn.lock" }}
|
||||
- chatwoot-yarn-
|
||||
|
||||
- run:
|
||||
@@ -62,7 +62,7 @@ jobs:
|
||||
|
||||
# Store yarn / webpacker cache
|
||||
- save_cache:
|
||||
key: chatwoot-yarn-{{ checksum "yarn.lock" }}
|
||||
key: chatwoot-yarn-{{ .Environment.CACHE_VERSION }}-{{ checksum "yarn.lock" }}
|
||||
paths:
|
||||
- ~/.cache/yarn
|
||||
|
||||
|
||||
@@ -14,6 +14,14 @@ plugins:
|
||||
checks:
|
||||
similar-code:
|
||||
enabled: false
|
||||
method-count:
|
||||
enabled: true
|
||||
config:
|
||||
threshold: 30
|
||||
file-lines:
|
||||
enabled: true
|
||||
config:
|
||||
threshold: 300
|
||||
exclude_patterns:
|
||||
- "spec/"
|
||||
- "**/specs/"
|
||||
|
||||
+25
-5
@@ -41,7 +41,7 @@ RAILS_MAX_THREADS=5
|
||||
|
||||
# The email from which all outgoing emails are sent
|
||||
# could user either `email@yourdomain.com` or `BrandName <email@yourdomain.com>`
|
||||
MAILER_SENDER_EMAIL=Chatwoot <accounts@chatwoot.com>
|
||||
MAILER_SENDER_EMAIL="Chatwoot <accounts@chatwoot.com>"
|
||||
|
||||
|
||||
#SMTP domain key is set up for HELO checking
|
||||
@@ -85,8 +85,6 @@ AWS_ACCESS_KEY_ID=
|
||||
AWS_SECRET_ACCESS_KEY=
|
||||
AWS_REGION=
|
||||
|
||||
# Sentry
|
||||
SENTRY_DSN=
|
||||
|
||||
# Log settings
|
||||
# Disable if you want to write logs to a file
|
||||
@@ -102,6 +100,9 @@ FB_VERIFY_TOKEN=
|
||||
FB_APP_SECRET=
|
||||
FB_APP_ID=
|
||||
|
||||
# https://developers.facebook.com/docs/messenger-platform/instagram/get-started#app-dashboard
|
||||
IG_VERIFY_TOKEN=
|
||||
|
||||
# Twitter
|
||||
# documentation: https://www.chatwoot.com/docs/twitter-app-setup
|
||||
TWITTER_APP_ID=
|
||||
@@ -115,7 +116,7 @@ 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
|
||||
IOS_APP_ID=L7YLMN4634.com.chatwoot.app
|
||||
ANDROID_BUNDLE_ID=com.chatwoot.app
|
||||
|
||||
# https://developers.google.com/android/guides/client-auth (use keytool to print the fingerprint in the first section)
|
||||
@@ -139,6 +140,25 @@ ANDROID_SHA256_CERT_FINGERPRINT=AC:73:8E:DE:EB:56:EA:CC:10:87:02:A7:65:37:7B:38:
|
||||
USE_INBOX_AVATAR_FOR_BOT=true
|
||||
|
||||
|
||||
### APM and Error Monitoring configurations
|
||||
## Sentry
|
||||
# SENTRY_DSN=
|
||||
|
||||
## Scout
|
||||
## https://scoutapm.com/docs/ruby/configuration
|
||||
# SCOUT_KEY=YOURKEY
|
||||
# SCOUT_NAME=YOURAPPNAME (Production)
|
||||
# SCOUT_MONITOR=true
|
||||
|
||||
## NewRelic
|
||||
# https://docs.newrelic.com/docs/agents/ruby-agent/configuration/ruby-agent-configuration/
|
||||
# NEW_RELIC_LICENSE_KEY=
|
||||
|
||||
## Datadog
|
||||
## https://github.com/DataDog/dd-trace-rb/blob/master/docs/GettingStarted.md#environment-variables
|
||||
# DD_TRACE_AGENT_URL=
|
||||
|
||||
|
||||
|
||||
## IP look up configuration
|
||||
## ref https://github.com/alexreisner/geocoder/blob/master/README_API_GUIDE.md
|
||||
@@ -149,7 +169,7 @@ USE_INBOX_AVATAR_FOR_BOT=true
|
||||
|
||||
## Rack Attack configuration
|
||||
## To prevent and throttle abusive requests
|
||||
# ENABLE_RACK_ATTACK=false
|
||||
# ENABLE_RACK_ATTACK=true
|
||||
|
||||
|
||||
## Running chatwoot as an API only server
|
||||
|
||||
@@ -14,6 +14,7 @@ Metrics/ClassLength:
|
||||
- 'app/mailers/conversation_reply_mailer.rb'
|
||||
- 'app/models/message.rb'
|
||||
- 'app/builders/messages/facebook/message_builder.rb'
|
||||
- 'app/controllers/api/v1/accounts/contacts_controller.rb'
|
||||
RSpec/ExampleLength:
|
||||
Max: 25
|
||||
Style/Documentation:
|
||||
@@ -100,6 +101,7 @@ Metrics/AbcSize:
|
||||
- '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'
|
||||
- 'app/controllers/api/v1/accounts/inboxes_controller.rb'
|
||||
Metrics/CyclomaticComplexity:
|
||||
Max: 7
|
||||
Exclude:
|
||||
|
||||
@@ -56,14 +56,13 @@ gem 'activerecord-import'
|
||||
gem 'dotenv-rails'
|
||||
gem 'foreman'
|
||||
gem 'puma'
|
||||
gem 'rack-timeout'
|
||||
gem 'webpacker', '~> 5.x'
|
||||
# metrics on heroku
|
||||
gem 'barnes'
|
||||
|
||||
##--- gems for authentication & authorization ---##
|
||||
gem 'devise'
|
||||
gem 'devise-secure_password', '~> 2.0'
|
||||
gem 'devise-secure_password', '~> 2.0', git: 'https://github.com/chatwoot/devise-secure_password'
|
||||
gem 'devise_token_auth'
|
||||
# authorization
|
||||
gem 'jwt'
|
||||
@@ -78,7 +77,7 @@ gem 'wisper', '2.0.0'
|
||||
##--- gems for channels ---##
|
||||
# TODO: bump up gem to 2.0
|
||||
gem 'facebook-messenger'
|
||||
gem 'telegram-bot-ruby'
|
||||
gem 'line-bot-api'
|
||||
gem 'twilio-ruby', '~> 5.32.0'
|
||||
# twitty will handle subscription of twitter account events
|
||||
# gem 'twitty', git: 'https://github.com/chatwoot/twitty'
|
||||
@@ -93,7 +92,10 @@ gem 'google-cloud-dialogflow'
|
||||
##--- gems for debugging and error reporting ---##
|
||||
# static analysis
|
||||
gem 'brakeman'
|
||||
|
||||
##-- apm and error monitoring ---#
|
||||
gem 'ddtrace'
|
||||
gem 'newrelic_rpm'
|
||||
gem 'scout_apm'
|
||||
gem 'sentry-rails'
|
||||
gem 'sentry-ruby'
|
||||
@@ -119,6 +121,11 @@ gem 'hairtrigger'
|
||||
|
||||
gem 'procore-sift'
|
||||
|
||||
group :production, :staging do
|
||||
# we dont want request timing out in development while using byebug
|
||||
gem 'rack-timeout'
|
||||
end
|
||||
|
||||
group :development do
|
||||
gem 'annotate'
|
||||
gem 'bullet'
|
||||
@@ -137,12 +144,20 @@ group :test do
|
||||
gem 'cypress-on-rails', '~> 1.0'
|
||||
# fast cleaning of database
|
||||
gem 'database_cleaner'
|
||||
# mock http calls
|
||||
gem 'webmock'
|
||||
end
|
||||
|
||||
group :development, :test do
|
||||
# TODO: is this needed ?
|
||||
# errors thrown by devise password gem
|
||||
gem 'flay'
|
||||
gem 'rspec'
|
||||
# for error thrown by devise password gem
|
||||
gem 'active_record_query_trace'
|
||||
gem 'bundle-audit', require: false
|
||||
gem 'byebug', platform: :mri
|
||||
gem 'climate_control'
|
||||
gem 'factory_bot_rails'
|
||||
gem 'faker'
|
||||
gem 'listen'
|
||||
@@ -158,5 +173,4 @@ group :development, :test do
|
||||
gem 'simplecov', '0.17.1', require: false
|
||||
gem 'spring'
|
||||
gem 'spring-watcher-listen'
|
||||
gem 'webmock'
|
||||
end
|
||||
|
||||
+169
-155
@@ -1,63 +1,71 @@
|
||||
GIT
|
||||
remote: https://github.com/chatwoot/devise-secure_password
|
||||
revision: de11e8765654b8242d42101ee9c8ffc8126f7975
|
||||
specs:
|
||||
devise-secure_password (2.0.1)
|
||||
devise (>= 4.0.0, < 5.0.0)
|
||||
railties (>= 5.0.0, < 7.0.0)
|
||||
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
actioncable (6.1.4)
|
||||
actionpack (= 6.1.4)
|
||||
activesupport (= 6.1.4)
|
||||
actioncable (6.1.4.1)
|
||||
actionpack (= 6.1.4.1)
|
||||
activesupport (= 6.1.4.1)
|
||||
nio4r (~> 2.0)
|
||||
websocket-driver (>= 0.6.1)
|
||||
actionmailbox (6.1.4)
|
||||
actionpack (= 6.1.4)
|
||||
activejob (= 6.1.4)
|
||||
activerecord (= 6.1.4)
|
||||
activestorage (= 6.1.4)
|
||||
activesupport (= 6.1.4)
|
||||
actionmailbox (6.1.4.1)
|
||||
actionpack (= 6.1.4.1)
|
||||
activejob (= 6.1.4.1)
|
||||
activerecord (= 6.1.4.1)
|
||||
activestorage (= 6.1.4.1)
|
||||
activesupport (= 6.1.4.1)
|
||||
mail (>= 2.7.1)
|
||||
actionmailer (6.1.4)
|
||||
actionpack (= 6.1.4)
|
||||
actionview (= 6.1.4)
|
||||
activejob (= 6.1.4)
|
||||
activesupport (= 6.1.4)
|
||||
actionmailer (6.1.4.1)
|
||||
actionpack (= 6.1.4.1)
|
||||
actionview (= 6.1.4.1)
|
||||
activejob (= 6.1.4.1)
|
||||
activesupport (= 6.1.4.1)
|
||||
mail (~> 2.5, >= 2.5.4)
|
||||
rails-dom-testing (~> 2.0)
|
||||
actionpack (6.1.4)
|
||||
actionview (= 6.1.4)
|
||||
activesupport (= 6.1.4)
|
||||
actionpack (6.1.4.1)
|
||||
actionview (= 6.1.4.1)
|
||||
activesupport (= 6.1.4.1)
|
||||
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.1.4)
|
||||
actionpack (= 6.1.4)
|
||||
activerecord (= 6.1.4)
|
||||
activestorage (= 6.1.4)
|
||||
activesupport (= 6.1.4)
|
||||
actiontext (6.1.4.1)
|
||||
actionpack (= 6.1.4.1)
|
||||
activerecord (= 6.1.4.1)
|
||||
activestorage (= 6.1.4.1)
|
||||
activesupport (= 6.1.4.1)
|
||||
nokogiri (>= 1.8.5)
|
||||
actionview (6.1.4)
|
||||
activesupport (= 6.1.4)
|
||||
actionview (6.1.4.1)
|
||||
activesupport (= 6.1.4.1)
|
||||
builder (~> 3.1)
|
||||
erubi (~> 1.4)
|
||||
rails-dom-testing (~> 2.0)
|
||||
rails-html-sanitizer (~> 1.1, >= 1.2.0)
|
||||
active_record_query_trace (1.8)
|
||||
activejob (6.1.4)
|
||||
activesupport (= 6.1.4)
|
||||
activejob (6.1.4.1)
|
||||
activesupport (= 6.1.4.1)
|
||||
globalid (>= 0.3.6)
|
||||
activemodel (6.1.4)
|
||||
activesupport (= 6.1.4)
|
||||
activerecord (6.1.4)
|
||||
activemodel (= 6.1.4)
|
||||
activesupport (= 6.1.4)
|
||||
activemodel (6.1.4.1)
|
||||
activesupport (= 6.1.4.1)
|
||||
activerecord (6.1.4.1)
|
||||
activemodel (= 6.1.4.1)
|
||||
activesupport (= 6.1.4.1)
|
||||
activerecord-import (1.2.0)
|
||||
activerecord (>= 3.2)
|
||||
activestorage (6.1.4)
|
||||
actionpack (= 6.1.4)
|
||||
activejob (= 6.1.4)
|
||||
activerecord (= 6.1.4)
|
||||
activesupport (= 6.1.4)
|
||||
activestorage (6.1.4.1)
|
||||
actionpack (= 6.1.4.1)
|
||||
activejob (= 6.1.4.1)
|
||||
activerecord (= 6.1.4.1)
|
||||
activesupport (= 6.1.4.1)
|
||||
marcel (~> 1.0.0)
|
||||
mini_mime (>= 1.1.0)
|
||||
activesupport (6.1.4)
|
||||
activesupport (6.1.4.1)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
i18n (>= 1.6, < 2)
|
||||
minitest (>= 5.1)
|
||||
@@ -82,26 +90,22 @@ GEM
|
||||
rake (>= 10.4, < 14.0)
|
||||
ast (2.4.2)
|
||||
attr_extras (6.2.4)
|
||||
aws-eventstream (1.1.1)
|
||||
aws-partitions (1.482.0)
|
||||
aws-sdk-core (3.119.0)
|
||||
aws-eventstream (1.2.0)
|
||||
aws-partitions (1.513.0)
|
||||
aws-sdk-core (3.121.1)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
aws-partitions (~> 1, >= 1.239.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
jmespath (~> 1.0)
|
||||
aws-sdk-kms (1.46.0)
|
||||
aws-sdk-core (~> 3, >= 3.119.0)
|
||||
aws-sdk-kms (1.49.0)
|
||||
aws-sdk-core (~> 3, >= 3.120.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-sdk-s3 (1.98.0)
|
||||
aws-sdk-core (~> 3, >= 3.119.0)
|
||||
aws-sdk-s3 (1.103.0)
|
||||
aws-sdk-core (~> 3, >= 3.120.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-sigv4 (1.2.4)
|
||||
aws-sigv4 (~> 1.4)
|
||||
aws-sigv4 (1.4.0)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
axiom-types (0.1.1)
|
||||
descendants_tracker (~> 0.0.4)
|
||||
ice_nine (~> 0.11.0)
|
||||
thread_safe (~> 0.3, >= 0.3.1)
|
||||
azure-storage-blob (2.0.1)
|
||||
azure-storage-common (~> 2.0)
|
||||
nokogiri (~> 1.11.0.rc2)
|
||||
@@ -115,30 +119,29 @@ GEM
|
||||
statsd-ruby (~> 1.1)
|
||||
bcrypt (3.1.16)
|
||||
bindex (0.8.1)
|
||||
bootsnap (1.7.6)
|
||||
bootsnap (1.9.1)
|
||||
msgpack (~> 1.0)
|
||||
brakeman (5.1.1)
|
||||
browser (5.3.1)
|
||||
builder (3.2.4)
|
||||
bullet (6.1.4)
|
||||
bullet (6.1.5)
|
||||
activesupport (>= 3.0.0)
|
||||
uniform_notifier (~> 1.11)
|
||||
bundle-audit (0.1.0)
|
||||
bundler-audit
|
||||
bundler-audit (0.8.0)
|
||||
bundler-audit (0.9.0.1)
|
||||
bundler (>= 1.2.0, < 3)
|
||||
thor (~> 1.0)
|
||||
byebug (11.1.3)
|
||||
climate_control (1.0.1)
|
||||
coderay (1.1.3)
|
||||
coercible (1.0.0)
|
||||
descendants_tracker (~> 0.0.1)
|
||||
commonmarker (0.22.0)
|
||||
commonmarker (0.23.2)
|
||||
concurrent-ruby (1.1.9)
|
||||
connection_pool (2.2.5)
|
||||
crack (0.4.5)
|
||||
rexml
|
||||
crass (1.0.6)
|
||||
cypress-on-rails (1.10.1)
|
||||
cypress-on-rails (1.11.0)
|
||||
rack
|
||||
database_cleaner (2.0.1)
|
||||
database_cleaner-active_record (~> 2.0.0)
|
||||
@@ -148,21 +151,16 @@ GEM
|
||||
database_cleaner-core (2.0.1)
|
||||
datetime_picker_rails (0.0.7)
|
||||
momentjs-rails (>= 2.8.1)
|
||||
ddtrace (0.51.1)
|
||||
ddtrace (0.53.0)
|
||||
ffi (~> 1.0)
|
||||
msgpack
|
||||
declarative (0.0.20)
|
||||
descendants_tracker (0.0.4)
|
||||
thread_safe (~> 0.3, >= 0.3.1)
|
||||
devise (4.8.0)
|
||||
bcrypt (~> 3.0)
|
||||
orm_adapter (~> 0.1)
|
||||
railties (>= 4.1.0)
|
||||
responders
|
||||
warden (~> 1.2.3)
|
||||
devise-secure_password (2.0.1)
|
||||
devise (>= 4.0.0, < 5.0.0)
|
||||
railties (>= 5.0.0, < 7.0.0)
|
||||
devise_token_auth (1.2.0)
|
||||
bcrypt (~> 3.0)
|
||||
devise (> 3.5.2, < 5)
|
||||
@@ -177,13 +175,13 @@ GEM
|
||||
dotenv-rails (2.7.6)
|
||||
dotenv (= 2.7.6)
|
||||
railties (>= 3.2)
|
||||
down (5.2.3)
|
||||
down (5.2.4)
|
||||
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)
|
||||
erubis (2.7.0)
|
||||
et-orbi (1.2.5)
|
||||
tzinfo
|
||||
execjs (2.8.1)
|
||||
facebook-messenger (2.0.1)
|
||||
@@ -194,7 +192,7 @@ GEM
|
||||
factory_bot_rails (6.2.0)
|
||||
factory_bot (~> 6.2.0)
|
||||
railties (>= 5.0.0)
|
||||
faker (2.18.0)
|
||||
faker (2.19.0)
|
||||
i18n (>= 1.6, < 2)
|
||||
faraday (1.0.1)
|
||||
multipart-post (>= 1.2, < 3)
|
||||
@@ -202,10 +200,15 @@ GEM
|
||||
faraday (~> 1.0)
|
||||
fcm (1.0.3)
|
||||
faraday (~> 1)
|
||||
ffi (1.15.3)
|
||||
ffi (1.15.4)
|
||||
flag_shih_tzu (0.3.23)
|
||||
flay (2.12.1)
|
||||
erubis (~> 2.7.0)
|
||||
path_expander (~> 1.0)
|
||||
ruby_parser (~> 3.0)
|
||||
sexp_processor (~> 4.0)
|
||||
foreman (0.87.2)
|
||||
fugit (1.5.0)
|
||||
fugit (1.5.2)
|
||||
et-orbi (~> 1.1, >= 1.1.8)
|
||||
raabro (~> 1.4)
|
||||
gapic-common (0.3.4)
|
||||
@@ -214,9 +217,9 @@ GEM
|
||||
googleapis-common-protos-types (>= 1.0.4, < 2.0)
|
||||
googleauth (~> 0.9)
|
||||
grpc (~> 1.25)
|
||||
geocoder (1.6.7)
|
||||
geocoder (1.7.0)
|
||||
gli (2.20.1)
|
||||
globalid (0.5.1)
|
||||
globalid (0.5.2)
|
||||
activesupport (>= 5.0)
|
||||
google-apis-core (0.4.1)
|
||||
addressable (~> 2.5, >= 2.5.1)
|
||||
@@ -227,9 +230,9 @@ GEM
|
||||
retriable (>= 2.0, < 4.a)
|
||||
rexml
|
||||
webrick
|
||||
google-apis-iamcredentials_v1 (0.6.0)
|
||||
google-apis-iamcredentials_v1 (0.7.0)
|
||||
google-apis-core (>= 0.4, < 2.a)
|
||||
google-apis-storage_v1 (0.6.0)
|
||||
google-apis-storage_v1 (0.8.0)
|
||||
google-apis-core (>= 0.4, < 2.a)
|
||||
google-cloud-core (1.6.0)
|
||||
google-cloud-env (~> 1.0)
|
||||
@@ -242,7 +245,7 @@ GEM
|
||||
google-cloud-errors (~> 1.0)
|
||||
google-cloud-env (1.5.0)
|
||||
faraday (>= 0.17.3, < 2.0)
|
||||
google-cloud-errors (1.1.0)
|
||||
google-cloud-errors (1.2.0)
|
||||
google-cloud-storage (1.34.1)
|
||||
addressable (~> 2.5)
|
||||
digest-crc (~> 0.4)
|
||||
@@ -251,28 +254,32 @@ GEM
|
||||
google-cloud-core (~> 1.6)
|
||||
googleauth (>= 0.16.2, < 2.a)
|
||||
mini_mime (~> 1.0)
|
||||
google-protobuf (3.17.3-universal-darwin)
|
||||
google-protobuf (3.17.3-x86_64-linux)
|
||||
googleapis-common-protos (1.3.11)
|
||||
google-protobuf (3.18.1)
|
||||
google-protobuf (3.18.1-universal-darwin)
|
||||
google-protobuf (3.18.1-x86_64-linux)
|
||||
googleapis-common-protos (1.3.12)
|
||||
google-protobuf (~> 3.14)
|
||||
googleapis-common-protos-types (>= 1.0.6, < 2.0)
|
||||
googleapis-common-protos-types (~> 1.2)
|
||||
grpc (~> 1.27)
|
||||
googleapis-common-protos-types (1.1.0)
|
||||
googleapis-common-protos-types (1.2.0)
|
||||
google-protobuf (~> 3.14)
|
||||
googleauth (0.17.0)
|
||||
googleauth (0.17.1)
|
||||
faraday (>= 0.17.3, < 2.0)
|
||||
jwt (>= 1.4, < 3.0)
|
||||
memoist (~> 0.16)
|
||||
multi_json (~> 1.11)
|
||||
os (>= 0.9, < 2.0)
|
||||
signet (~> 0.14)
|
||||
signet (~> 0.15)
|
||||
groupdate (5.2.2)
|
||||
activesupport (>= 5)
|
||||
grpc (1.38.0-universal-darwin)
|
||||
google-protobuf (~> 3.15)
|
||||
grpc (1.41.0)
|
||||
google-protobuf (~> 3.17)
|
||||
googleapis-common-protos-types (~> 1.0)
|
||||
grpc (1.38.0-x86_64-linux)
|
||||
google-protobuf (~> 3.15)
|
||||
grpc (1.41.0-universal-darwin)
|
||||
google-protobuf (~> 3.17)
|
||||
googleapis-common-protos-types (~> 1.0)
|
||||
grpc (1.41.0-x86_64-linux)
|
||||
google-protobuf (~> 3.17)
|
||||
googleapis-common-protos-types (~> 1.0)
|
||||
haikunator (1.1.1)
|
||||
hairtrigger (0.2.24)
|
||||
@@ -286,13 +293,12 @@ GEM
|
||||
http-accept (1.7.0)
|
||||
http-cookie (1.0.4)
|
||||
domain_name (~> 0.5)
|
||||
httparty (0.18.1)
|
||||
httparty (0.20.0)
|
||||
mime-types (~> 3.0)
|
||||
multi_xml (>= 0.5.2)
|
||||
httpclient (2.8.3)
|
||||
i18n (1.8.10)
|
||||
concurrent-ruby (~> 1.0)
|
||||
ice_nine (0.11.2)
|
||||
image_processing (1.12.1)
|
||||
mini_magick (>= 4.9.5, < 5)
|
||||
ruby-vips (>= 2.0.17, < 3)
|
||||
@@ -311,7 +317,7 @@ GEM
|
||||
hana (~> 1.3)
|
||||
regexp_parser (~> 2.0)
|
||||
uri_template (~> 0.7)
|
||||
jwt (2.2.3)
|
||||
jwt (2.3.0)
|
||||
kaminari (1.2.1)
|
||||
activesupport (>= 4.1.0)
|
||||
kaminari-actionview (= 1.2.1)
|
||||
@@ -332,26 +338,28 @@ GEM
|
||||
addressable (~> 2.7)
|
||||
letter_opener (1.7.0)
|
||||
launchy (~> 2.2)
|
||||
liquid (5.0.1)
|
||||
listen (3.6.0)
|
||||
line-bot-api (1.22.0)
|
||||
liquid (5.1.0)
|
||||
listen (3.7.0)
|
||||
rb-fsevent (~> 0.10, >= 0.10.3)
|
||||
rb-inotify (~> 0.9, >= 0.9.10)
|
||||
loofah (2.11.0)
|
||||
loofah (2.12.0)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.5.9)
|
||||
mail (2.7.1)
|
||||
mini_mime (>= 0.1.1)
|
||||
marcel (1.0.1)
|
||||
marcel (1.0.2)
|
||||
maxminddb (0.1.22)
|
||||
memoist (0.16.2)
|
||||
method_source (1.0.0)
|
||||
mime-types (3.3.1)
|
||||
mime-types-data (~> 3.2015)
|
||||
mime-types-data (3.2021.0704)
|
||||
mime-types-data (3.2021.0901)
|
||||
mini_magick (4.11.0)
|
||||
mini_mime (1.1.0)
|
||||
mini_mime (1.1.2)
|
||||
mini_portile2 (2.5.3)
|
||||
minitest (5.14.4)
|
||||
mock_redis (0.28.0)
|
||||
mock_redis (0.29.0)
|
||||
ruby2_keywords
|
||||
momentjs-rails (2.20.1)
|
||||
railties (>= 3.1)
|
||||
@@ -362,7 +370,11 @@ GEM
|
||||
net-http-persistent (4.0.1)
|
||||
connection_pool (~> 2.2)
|
||||
netrc (0.11.0)
|
||||
nio4r (2.5.7)
|
||||
newrelic_rpm (8.0.0)
|
||||
nio4r (2.5.8)
|
||||
nokogiri (1.11.7)
|
||||
mini_portile2 (~> 2.5.0)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.11.7-arm64-darwin)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.11.7-x86_64-darwin)
|
||||
@@ -372,9 +384,10 @@ GEM
|
||||
oauth (0.5.6)
|
||||
orm_adapter (0.5.0)
|
||||
os (1.1.1)
|
||||
parallel (1.20.1)
|
||||
parallel (1.21.0)
|
||||
parser (3.0.2.0)
|
||||
ast (~> 2.4.1)
|
||||
path_expander (1.1.0)
|
||||
pg (1.2.3)
|
||||
procore-sift (0.16.0)
|
||||
rails (> 4.2.0)
|
||||
@@ -384,9 +397,9 @@ GEM
|
||||
pry-rails (0.3.9)
|
||||
pry (>= 0.10.4)
|
||||
public_suffix (4.0.6)
|
||||
puma (5.4.0)
|
||||
puma (5.5.2)
|
||||
nio4r (~> 2.0)
|
||||
pundit (2.1.0)
|
||||
pundit (2.1.1)
|
||||
activesupport (>= 3.0.0)
|
||||
raabro (1.4.0)
|
||||
racc (1.5.2)
|
||||
@@ -400,29 +413,29 @@ GEM
|
||||
rack-test (1.1.0)
|
||||
rack (>= 1.0, < 3)
|
||||
rack-timeout (0.6.0)
|
||||
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)
|
||||
rails (6.1.4.1)
|
||||
actioncable (= 6.1.4.1)
|
||||
actionmailbox (= 6.1.4.1)
|
||||
actionmailer (= 6.1.4.1)
|
||||
actionpack (= 6.1.4.1)
|
||||
actiontext (= 6.1.4.1)
|
||||
actionview (= 6.1.4.1)
|
||||
activejob (= 6.1.4.1)
|
||||
activemodel (= 6.1.4.1)
|
||||
activerecord (= 6.1.4.1)
|
||||
activestorage (= 6.1.4.1)
|
||||
activesupport (= 6.1.4.1)
|
||||
bundler (>= 1.15.0)
|
||||
railties (= 6.1.4)
|
||||
railties (= 6.1.4.1)
|
||||
sprockets-rails (>= 2.0.0)
|
||||
rails-dom-testing (2.0.3)
|
||||
activesupport (>= 4.2.0)
|
||||
nokogiri (>= 1.6)
|
||||
rails-html-sanitizer (1.3.0)
|
||||
rails-html-sanitizer (1.4.2)
|
||||
loofah (~> 2.3)
|
||||
railties (6.1.4)
|
||||
actionpack (= 6.1.4)
|
||||
activesupport (= 6.1.4)
|
||||
railties (6.1.4.1)
|
||||
actionpack (= 6.1.4.1)
|
||||
activesupport (= 6.1.4.1)
|
||||
method_source
|
||||
rake (>= 0.13)
|
||||
thor (~> 1.0)
|
||||
@@ -449,6 +462,10 @@ GEM
|
||||
netrc (~> 0.8)
|
||||
retriable (3.1.2)
|
||||
rexml (3.2.5)
|
||||
rspec (3.10.0)
|
||||
rspec-core (~> 3.10.0)
|
||||
rspec-expectations (~> 3.10.0)
|
||||
rspec-mocks (~> 3.10.0)
|
||||
rspec-core (3.10.1)
|
||||
rspec-support (~> 3.10.0)
|
||||
rspec-expectations (3.10.1)
|
||||
@@ -457,7 +474,7 @@ GEM
|
||||
rspec-mocks (3.10.2)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.10.0)
|
||||
rspec-rails (5.0.1)
|
||||
rspec-rails (5.0.2)
|
||||
actionpack (>= 5.2)
|
||||
activesupport (>= 5.2)
|
||||
railties (>= 5.2)
|
||||
@@ -466,35 +483,34 @@ GEM
|
||||
rspec-mocks (~> 3.10)
|
||||
rspec-support (~> 3.10)
|
||||
rspec-support (3.10.2)
|
||||
rubocop (1.18.4)
|
||||
rubocop (1.22.1)
|
||||
parallel (~> 1.10)
|
||||
parser (>= 3.0.0.0)
|
||||
rainbow (>= 2.2.2, < 4.0)
|
||||
regexp_parser (>= 1.8, < 3.0)
|
||||
rexml
|
||||
rubocop-ast (>= 1.8.0, < 2.0)
|
||||
rubocop-ast (>= 1.12.0, < 2.0)
|
||||
ruby-progressbar (~> 1.7)
|
||||
unicode-display_width (>= 1.4.0, < 3.0)
|
||||
rubocop-ast (1.8.0)
|
||||
rubocop-ast (1.12.0)
|
||||
parser (>= 3.0.1.1)
|
||||
rubocop-performance (1.11.4)
|
||||
rubocop-performance (1.11.5)
|
||||
rubocop (>= 1.7.0, < 2.0)
|
||||
rubocop-ast (>= 0.4.0)
|
||||
rubocop-rails (2.11.3)
|
||||
rubocop-rails (2.12.3)
|
||||
activesupport (>= 4.2.0)
|
||||
rack (>= 1.1)
|
||||
rubocop (>= 1.7.0, < 2.0)
|
||||
rubocop-rspec (2.4.0)
|
||||
rubocop (~> 1.0)
|
||||
rubocop-ast (>= 1.1.0)
|
||||
rubocop-rspec (2.5.0)
|
||||
rubocop (~> 1.19)
|
||||
ruby-progressbar (1.11.0)
|
||||
ruby-vips (2.1.2)
|
||||
ruby-vips (2.1.3)
|
||||
ffi (~> 1.12)
|
||||
ruby2_keywords (0.0.5)
|
||||
ruby2ruby (2.4.4)
|
||||
ruby_parser (~> 3.1)
|
||||
sexp_processor (~> 4.6)
|
||||
ruby_parser (3.16.0)
|
||||
ruby_parser (3.17.0)
|
||||
sexp_processor (~> 4.15, >= 4.15.1)
|
||||
sassc (2.4.0)
|
||||
ffi (~> 1.9)
|
||||
@@ -504,38 +520,38 @@ GEM
|
||||
sprockets (> 3.0)
|
||||
sprockets-rails
|
||||
tilt
|
||||
scout_apm (4.1.1)
|
||||
scout_apm (4.1.2)
|
||||
parser
|
||||
seed_dump (3.3.1)
|
||||
activerecord (>= 4)
|
||||
activesupport (>= 4)
|
||||
selectize-rails (0.12.6)
|
||||
semantic_range (3.0.0)
|
||||
sentry-rails (4.6.4)
|
||||
sentry-rails (4.7.3)
|
||||
railties (>= 5.0)
|
||||
sentry-ruby-core (~> 4.6.0)
|
||||
sentry-ruby (4.6.4)
|
||||
sentry-ruby-core (~> 4.7.0)
|
||||
sentry-ruby (4.7.3)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
faraday (>= 1.0)
|
||||
sentry-ruby-core (= 4.6.4)
|
||||
sentry-ruby-core (4.6.4)
|
||||
sentry-ruby-core (= 4.7.3)
|
||||
sentry-ruby-core (4.7.3)
|
||||
concurrent-ruby
|
||||
faraday
|
||||
sentry-sidekiq (4.6.4)
|
||||
sentry-ruby-core (~> 4.6.0)
|
||||
sentry-sidekiq (4.7.3)
|
||||
sentry-ruby-core (~> 4.7.0)
|
||||
sidekiq (>= 3.0)
|
||||
sexp_processor (4.15.3)
|
||||
shoulda-matchers (5.0.0)
|
||||
activesupport (>= 5.2.0)
|
||||
sidekiq (6.2.1)
|
||||
sidekiq (6.2.2)
|
||||
connection_pool (>= 2.2.2)
|
||||
rack (~> 2.0)
|
||||
redis (>= 4.2.0)
|
||||
sidekiq-cron (1.2.0)
|
||||
fugit (~> 1.1)
|
||||
sidekiq (>= 4.2.1)
|
||||
signet (0.15.0)
|
||||
addressable (~> 2.3)
|
||||
signet (0.16.0)
|
||||
addressable (~> 2.8)
|
||||
faraday (>= 0.17.3, < 2.0)
|
||||
jwt (>= 1.5, < 3.0)
|
||||
multi_json (~> 1.10)
|
||||
@@ -563,13 +579,8 @@ GEM
|
||||
sprockets (>= 3.0.0)
|
||||
squasher (0.6.2)
|
||||
statsd-ruby (1.5.0)
|
||||
telegram-bot-ruby (0.16.0)
|
||||
dry-inflector
|
||||
faraday
|
||||
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
|
||||
@@ -583,24 +594,20 @@ GEM
|
||||
oauth
|
||||
tzinfo (2.0.4)
|
||||
concurrent-ruby (~> 1.0)
|
||||
tzinfo-data (1.2021.1)
|
||||
tzinfo-data (1.2021.3)
|
||||
tzinfo (>= 1.0.0)
|
||||
uber (0.1.0)
|
||||
uglifier (4.2.0)
|
||||
execjs (>= 0.3.0, < 3)
|
||||
unf (0.1.4)
|
||||
unf_ext
|
||||
unf_ext (0.0.7.7)
|
||||
unicode-display_width (2.0.0)
|
||||
unf_ext (0.0.8)
|
||||
unicode-display_width (2.1.0)
|
||||
uniform_notifier (1.14.2)
|
||||
uri_template (0.7.0)
|
||||
valid_email2 (4.0.0)
|
||||
activemodel (>= 3.2)
|
||||
mail (~> 2.5)
|
||||
virtus (2.0.0)
|
||||
axiom-types (~> 0.1)
|
||||
coercible (~> 1.0)
|
||||
descendants_tracker (~> 0.0, >= 0.0.3)
|
||||
warden (1.2.9)
|
||||
rack (>= 2.0.9)
|
||||
web-console (4.1.0)
|
||||
@@ -608,11 +615,11 @@ GEM
|
||||
activemodel (>= 6.0.0)
|
||||
bindex (>= 0.4.0)
|
||||
railties (>= 6.0.0)
|
||||
webmock (3.13.0)
|
||||
addressable (>= 2.3.6)
|
||||
webmock (3.14.0)
|
||||
addressable (>= 2.8.0)
|
||||
crack (>= 0.3.2)
|
||||
hashdiff (>= 0.4.0, < 2.0.0)
|
||||
webpacker (5.4.0)
|
||||
webpacker (5.4.3)
|
||||
activesupport (>= 5.2)
|
||||
rack-proxy (>= 0.6.1)
|
||||
railties (>= 5.2)
|
||||
@@ -629,6 +636,9 @@ GEM
|
||||
|
||||
PLATFORMS
|
||||
arm64-darwin-20
|
||||
ruby
|
||||
x86_64-darwin-18
|
||||
x86_64-darwin-20
|
||||
x86_64-darwin-21
|
||||
x86_64-linux
|
||||
|
||||
@@ -648,12 +658,13 @@ DEPENDENCIES
|
||||
bullet
|
||||
bundle-audit
|
||||
byebug
|
||||
climate_control
|
||||
commonmarker
|
||||
cypress-on-rails (~> 1.0)
|
||||
database_cleaner
|
||||
ddtrace
|
||||
devise
|
||||
devise-secure_password (~> 2.0)
|
||||
devise-secure_password (~> 2.0)!
|
||||
devise_token_auth
|
||||
dotenv-rails
|
||||
down (~> 5.0)
|
||||
@@ -662,6 +673,7 @@ DEPENDENCIES
|
||||
faker
|
||||
fcm
|
||||
flag_shih_tzu
|
||||
flay
|
||||
foreman
|
||||
geocoder
|
||||
google-cloud-dialogflow
|
||||
@@ -678,10 +690,12 @@ DEPENDENCIES
|
||||
kaminari
|
||||
koala
|
||||
letter_opener
|
||||
line-bot-api
|
||||
liquid
|
||||
listen
|
||||
maxminddb
|
||||
mock_redis
|
||||
newrelic_rpm
|
||||
pg
|
||||
procore-sift
|
||||
pry-rails
|
||||
@@ -695,6 +709,7 @@ DEPENDENCIES
|
||||
redis-namespace
|
||||
responders
|
||||
rest-client
|
||||
rspec
|
||||
rspec-rails (~> 5.0.0)
|
||||
rubocop
|
||||
rubocop-performance
|
||||
@@ -713,7 +728,6 @@ DEPENDENCIES
|
||||
spring
|
||||
spring-watcher-listen
|
||||
squasher
|
||||
telegram-bot-ruby
|
||||
telephone_number
|
||||
time_diff
|
||||
twilio-ruby (~> 5.32.0)
|
||||
|
||||
@@ -22,6 +22,8 @@ ___
|
||||
<a title="Crowdin" target="_self" href="https://chatwoot.crowdin.com/chatwoot"><img src="https://badges.crowdin.net/e/37ced7eba411064bd792feb3b7a28b16/localized.svg"></a>
|
||||
<a href="https://discord.gg/cJXdrwS"><img src="https://img.shields.io/discord/647412545203994635" alt="Discord"></a>
|
||||
<a href="https://huntr.dev/bounties/disclose"><img src="https://cdn.huntr.dev/huntr_security_badge_mono.svg" alt="Huntr"></a>
|
||||
<a href="https://status.chatwoot.com"><img src="https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fchatwoot%2Fstatus%2Fmaster%2Fapi%2Fchatwoot%2Fuptime.json" alt="uptime"></a>
|
||||
<a href="https://status.chatwoot.com"><img src="https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fchatwoot%2Fstatus%2Fmaster%2Fapi%2Fchatwoot%2Fresponse-time.json" alt="response time"></a>
|
||||
</p>
|
||||
|
||||
<img src="https://s3.us-west-2.amazonaws.com/gh-assets.chatwoot.com/chatwoot-dashboard-assets.png" width="100%" alt="Chat dashboard"/>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
class ContactMergeAction
|
||||
include Events::Types
|
||||
pattr_initialize [:account!, :base_contact!, :mergee_contact!]
|
||||
|
||||
def perform
|
||||
@@ -11,7 +12,7 @@ class ContactMergeAction
|
||||
merge_conversations
|
||||
merge_messages
|
||||
merge_contact_inboxes
|
||||
remove_mergee_contact
|
||||
merge_and_remove_mergee_contact
|
||||
end
|
||||
@base_contact
|
||||
end
|
||||
@@ -40,7 +41,18 @@ class ContactMergeAction
|
||||
ContactInbox.where(contact_id: @mergee_contact.id).update(contact_id: @base_contact.id)
|
||||
end
|
||||
|
||||
def remove_mergee_contact
|
||||
def merge_and_remove_mergee_contact
|
||||
mergable_attribute_keys = %w[identifier name email phone_number additional_attributes custom_attributes]
|
||||
base_contact_attributes = base_contact.attributes.slice(*mergable_attribute_keys).compact_blank
|
||||
mergee_contact_attributes = mergee_contact.attributes.slice(*mergable_attribute_keys).compact_blank
|
||||
|
||||
# attributes in base contact are given preference
|
||||
merged_attributes = mergee_contact_attributes.deep_merge(base_contact_attributes)
|
||||
# retaining old pubsub token to notify the contacts that are listening
|
||||
mergee_pubsub_token = mergee_contact.pubsub_token
|
||||
|
||||
@mergee_contact.destroy!
|
||||
Rails.configuration.dispatcher.dispatch(CONTACT_MERGED, Time.zone.now, contact: @base_contact, tokens: [mergee_pubsub_token])
|
||||
@base_contact.update!(merged_attributes)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -43,7 +43,7 @@ $woot-logo-padding: $space-large $space-two;
|
||||
// Colors
|
||||
$color-woot: #1f93ff;
|
||||
$color-gray: #6e6f73;
|
||||
$color-light-gray: #999a9b;
|
||||
$color-light-gray: #747677;
|
||||
$color-border: #e0e6ed;
|
||||
$color-border-light: #f0f4f5;
|
||||
$color-background: #f4f6fb;
|
||||
|
||||
@@ -4,10 +4,11 @@
|
||||
# based on this we are showing "not sent from chatwoot" message in frontend
|
||||
# Hence there is no need to set user_id in message for outgoing echo messages.
|
||||
|
||||
class Messages::Facebook::MessageBuilder
|
||||
class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
|
||||
attr_reader :response
|
||||
|
||||
def initialize(response, inbox, outgoing_echo: false)
|
||||
super()
|
||||
@response = response
|
||||
@inbox = inbox
|
||||
@outgoing_echo = outgoing_echo
|
||||
@@ -47,30 +48,12 @@ class Messages::Facebook::MessageBuilder
|
||||
|
||||
def build_message
|
||||
@message = conversation.messages.create!(message_params)
|
||||
|
||||
@attachments.each do |attachment|
|
||||
process_attachment(attachment)
|
||||
end
|
||||
end
|
||||
|
||||
def process_attachment(attachment)
|
||||
return if attachment['type'].to_sym == :template
|
||||
|
||||
attachment_obj = @message.attachments.new(attachment_params(attachment).except(:remote_file_url))
|
||||
attachment_obj.save!
|
||||
attach_file(attachment_obj, attachment_params(attachment)[:remote_file_url]) if attachment_params(attachment)[:remote_file_url]
|
||||
end
|
||||
|
||||
def attach_file(attachment, file_url)
|
||||
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?
|
||||
@@ -89,28 +72,6 @@ class Messages::Facebook::MessageBuilder
|
||||
))
|
||||
end
|
||||
|
||||
def attachment_params(attachment)
|
||||
file_type = attachment['type'].to_sym
|
||||
params = { file_type: file_type, account_id: @message.account_id }
|
||||
|
||||
if [:image, :file, :audio, :video].include? file_type
|
||||
params.merge!(file_type_params(attachment))
|
||||
elsif file_type == :location
|
||||
params.merge!(location_params(attachment))
|
||||
elsif file_type == :fallback
|
||||
params.merge!(fallback_params(attachment))
|
||||
end
|
||||
|
||||
params
|
||||
end
|
||||
|
||||
def file_type_params(attachment)
|
||||
{
|
||||
external_url: attachment['payload']['url'],
|
||||
remote_file_url: attachment['payload']['url']
|
||||
}
|
||||
end
|
||||
|
||||
def location_params(attachment)
|
||||
lat = attachment['payload']['coordinates']['lat']
|
||||
long = attachment['payload']['coordinates']['long']
|
||||
@@ -148,6 +109,14 @@ class Messages::Facebook::MessageBuilder
|
||||
}
|
||||
end
|
||||
|
||||
def process_contact_params_result(result)
|
||||
{
|
||||
name: "#{result['first_name'] || 'John'} #{result['last_name'] || 'Doe'}",
|
||||
account_id: @inbox.account_id,
|
||||
remote_avatar_url: result['profile_pic'] || ''
|
||||
}
|
||||
end
|
||||
|
||||
def contact_params
|
||||
begin
|
||||
k = Koala::Facebook::API.new(@inbox.channel.page_access_token) if @inbox.facebook?
|
||||
@@ -155,14 +124,15 @@ class Messages::Facebook::MessageBuilder
|
||||
rescue Koala::Facebook::AuthenticationError
|
||||
@inbox.channel.authorization_error!
|
||||
raise
|
||||
rescue Koala::Facebook::ClientError => e
|
||||
result = {}
|
||||
# OAuthException, code: 100, error_subcode: 2018218, message: (#100) No profile available for this user
|
||||
# We don't need to capture this error as we don't care about contact params in case of echo messages
|
||||
Sentry.capture_exception(e) unless @outgoing_echo
|
||||
rescue StandardError => e
|
||||
result = {}
|
||||
Sentry.capture_exception(e)
|
||||
end
|
||||
{
|
||||
name: "#{result['first_name'] || 'John'} #{result['last_name'] || 'Doe'}",
|
||||
account_id: @inbox.account_id,
|
||||
remote_avatar_url: result['profile_pic'] || ''
|
||||
}
|
||||
process_contact_params_result(result)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
# This class creates both outgoing messages from chatwoot and echo outgoing messages based on the flag `outgoing_echo`
|
||||
# Assumptions
|
||||
# 1. Incase of an outgoing message which is echo, source_id will NOT be nil,
|
||||
# based on this we are showing "not sent from chatwoot" message in frontend
|
||||
# Hence there is no need to set user_id in message for outgoing echo messages.
|
||||
|
||||
class Messages::Instagram::MessageBuilder < Messages::Messenger::MessageBuilder
|
||||
attr_reader :messaging
|
||||
|
||||
def initialize(messaging, inbox, outgoing_echo: false)
|
||||
super()
|
||||
@messaging = messaging
|
||||
@inbox = inbox
|
||||
@outgoing_echo = outgoing_echo
|
||||
end
|
||||
|
||||
def perform
|
||||
return if @inbox.channel.reauthorization_required?
|
||||
|
||||
ActiveRecord::Base.transaction do
|
||||
build_message
|
||||
end
|
||||
rescue Koala::Facebook::AuthenticationError
|
||||
@inbox.channel.authorization_error!
|
||||
raise
|
||||
rescue StandardError => e
|
||||
Sentry.capture_exception(e)
|
||||
true
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def attachments
|
||||
@messaging[:message][:attachments] || {}
|
||||
end
|
||||
|
||||
def message_type
|
||||
@outgoing_echo ? :outgoing : :incoming
|
||||
end
|
||||
|
||||
def message_identifier
|
||||
message[:mid]
|
||||
end
|
||||
|
||||
def message_source_id
|
||||
@outgoing_echo ? recipient_id : sender_id
|
||||
end
|
||||
|
||||
def sender_id
|
||||
@messaging[:sender][:id]
|
||||
end
|
||||
|
||||
def recipient_id
|
||||
@messaging[:recipient][:id]
|
||||
end
|
||||
|
||||
def message
|
||||
@messaging[:message]
|
||||
end
|
||||
|
||||
def contact
|
||||
@contact ||= @inbox.contact_inboxes.find_by(source_id: message_source_id)&.contact
|
||||
end
|
||||
|
||||
def conversation
|
||||
@conversation ||= Conversation.find_by(conversation_params) || build_conversation
|
||||
end
|
||||
|
||||
def message_content
|
||||
@messaging[:message][:text]
|
||||
end
|
||||
|
||||
def build_message
|
||||
return if @outgoing_echo && already_sent_from_chatwoot?
|
||||
|
||||
@message = conversation.messages.create!(message_params)
|
||||
|
||||
attachments.each do |attachment|
|
||||
process_attachment(attachment)
|
||||
end
|
||||
end
|
||||
|
||||
def build_conversation
|
||||
@contact_inbox ||= contact.contact_inboxes.find_by!(source_id: message_source_id)
|
||||
Conversation.create!(conversation_params.merge(
|
||||
contact_inbox_id: @contact_inbox.id
|
||||
))
|
||||
end
|
||||
|
||||
def conversation_params
|
||||
{
|
||||
account_id: @inbox.account_id,
|
||||
inbox_id: @inbox.id,
|
||||
contact_id: contact.id,
|
||||
additional_attributes: {
|
||||
type: 'instagram_direct_message'
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def message_params
|
||||
{
|
||||
account_id: conversation.account_id,
|
||||
inbox_id: conversation.inbox_id,
|
||||
message_type: message_type,
|
||||
source_id: message_identifier,
|
||||
content: message_content,
|
||||
sender: @outgoing_echo ? nil : contact
|
||||
}
|
||||
end
|
||||
|
||||
def already_sent_from_chatwoot?
|
||||
cw_message = conversation.messages.where(
|
||||
source_id: @messaging[:message][:mid]
|
||||
).first
|
||||
|
||||
cw_message.present?
|
||||
end
|
||||
|
||||
### Sample response
|
||||
# {
|
||||
# "object": "instagram",
|
||||
# "entry": [
|
||||
# {
|
||||
# "id": "<IGID>",// ig id of the business
|
||||
# "time": 1569262486134,
|
||||
# "messaging": [
|
||||
# {
|
||||
# "sender": {
|
||||
# "id": "<IGSID>"
|
||||
# },
|
||||
# "recipient": {
|
||||
# "id": "<IGID>"
|
||||
# },
|
||||
# "timestamp": 1569262485349,
|
||||
# "message": {
|
||||
# "mid": "<MESSAGE_ID>",
|
||||
# "text": "<MESSAGE_CONTENT>"
|
||||
# }
|
||||
# }
|
||||
# ]
|
||||
# }
|
||||
# ],
|
||||
# }
|
||||
end
|
||||
@@ -15,21 +15,36 @@ class Messages::MessageBuilder
|
||||
|
||||
def perform
|
||||
@message = @conversation.messages.build(message_params)
|
||||
if @attachments.present?
|
||||
@attachments.each do |uploaded_attachment|
|
||||
attachment = @message.attachments.new(
|
||||
account_id: @message.account_id,
|
||||
file_type: file_type(uploaded_attachment&.content_type)
|
||||
)
|
||||
attachment.file.attach(uploaded_attachment)
|
||||
end
|
||||
end
|
||||
@message.save
|
||||
process_attachments
|
||||
process_emails
|
||||
@message.save!
|
||||
@message
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def process_attachments
|
||||
return if @attachments.blank?
|
||||
|
||||
@attachments.each do |uploaded_attachment|
|
||||
@message.attachments.build(
|
||||
account_id: @message.account_id,
|
||||
file_type: file_type(uploaded_attachment&.content_type),
|
||||
file: uploaded_attachment
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def process_emails
|
||||
return unless @conversation.inbox&.inbox_type == 'Email'
|
||||
|
||||
cc_emails = @params[:cc_emails].split(',') if @params[:cc_emails]
|
||||
bcc_emails = @params[:bcc_emails].split(',') if @params[:bcc_emails]
|
||||
|
||||
@message.content_attributes[:cc_emails] = cc_emails
|
||||
@message.content_attributes[:bcc_emails] = bcc_emails
|
||||
end
|
||||
|
||||
def message_type
|
||||
if @conversation.inbox.channel_type != 'Channel::Api' && @message_type == 'incoming'
|
||||
raise StandardError, 'Incoming messages are only allowed in Api inboxes'
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
class Messages::Messenger::MessageBuilder
|
||||
def process_attachment(attachment)
|
||||
return if attachment['type'].to_sym == :template
|
||||
|
||||
attachment_obj = @message.attachments.new(attachment_params(attachment).except(:remote_file_url))
|
||||
attachment_obj.save!
|
||||
attach_file(attachment_obj, attachment_params(attachment)[:remote_file_url]) if attachment_params(attachment)[:remote_file_url]
|
||||
end
|
||||
|
||||
def attach_file(attachment, file_url)
|
||||
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 attachment_params(attachment)
|
||||
file_type = attachment['type'].to_sym
|
||||
params = { file_type: file_type, account_id: @message.account_id }
|
||||
|
||||
if [:image, :file, :audio, :video].include? file_type
|
||||
params.merge!(file_type_params(attachment))
|
||||
elsif file_type == :location
|
||||
params.merge!(location_params(attachment))
|
||||
elsif file_type == :fallback
|
||||
params.merge!(fallback_params(attachment))
|
||||
end
|
||||
|
||||
params
|
||||
end
|
||||
|
||||
def file_type_params(attachment)
|
||||
{
|
||||
external_url: attachment['payload']['url'],
|
||||
remote_file_url: attachment['payload']['url']
|
||||
}
|
||||
end
|
||||
end
|
||||
@@ -4,7 +4,7 @@ class NotificationSubscriptionBuilder
|
||||
def perform
|
||||
# if multiple accounts were used to login in same browser
|
||||
move_subscription_to_user if identifier_subscription && identifier_subscription.user_id != user.id
|
||||
build_identifier_subscription if identifier_subscription.blank?
|
||||
identifier_subscription.blank? ? build_identifier_subscription : update_identifier_subscription
|
||||
identifier_subscription
|
||||
end
|
||||
|
||||
@@ -25,6 +25,10 @@ class NotificationSubscriptionBuilder
|
||||
end
|
||||
|
||||
def build_identifier_subscription
|
||||
user.notification_subscriptions.create(params.merge(identifier: identifier))
|
||||
@identifier_subscription = user.notification_subscriptions.create(params.merge(identifier: identifier))
|
||||
end
|
||||
|
||||
def update_identifier_subscription
|
||||
identifier_subscription.update(params.merge(identifier: identifier))
|
||||
end
|
||||
end
|
||||
|
||||
@@ -32,17 +32,34 @@ class V2::ReportBuilder
|
||||
private
|
||||
|
||||
def scope
|
||||
return account if params[:type].match?('account')
|
||||
return inbox if params[:type].match?('inbox')
|
||||
return user if params[:type].match?('agent')
|
||||
case params[:type]
|
||||
when :account
|
||||
account
|
||||
when :inbox
|
||||
inbox
|
||||
when :agent
|
||||
user
|
||||
when :label
|
||||
label
|
||||
when :team
|
||||
team
|
||||
end
|
||||
end
|
||||
|
||||
def inbox
|
||||
@inbox ||= account.inboxes.where(id: params[:id]).first
|
||||
@inbox ||= account.inboxes.find(params[:id])
|
||||
end
|
||||
|
||||
def user
|
||||
@user ||= account.users.where(id: params[:id]).first
|
||||
@user ||= account.users.find(params[:id])
|
||||
end
|
||||
|
||||
def label
|
||||
@label ||= account.labels.find(params[:id])
|
||||
end
|
||||
|
||||
def team
|
||||
@team ||= account.teams.find(params[:id])
|
||||
end
|
||||
|
||||
def conversations_count
|
||||
@@ -51,15 +68,14 @@ class V2::ReportBuilder
|
||||
.count
|
||||
end
|
||||
|
||||
# unscoped removes all scopes added to a model previously
|
||||
def incoming_messages_count
|
||||
scope.messages.unscoped.where(account_id: account.id).incoming
|
||||
scope.messages.incoming.unscope(:order)
|
||||
.group_by_day(:created_at, range: range, default_value: 0)
|
||||
.count
|
||||
end
|
||||
|
||||
def outgoing_messages_count
|
||||
scope.messages.unscoped.where(account_id: account.id).outgoing
|
||||
scope.messages.outgoing.unscope(:order)
|
||||
.group_by_day(:created_at, range: range, default_value: 0)
|
||||
.count
|
||||
end
|
||||
|
||||
@@ -9,21 +9,18 @@ class Api::V1::Accounts::AgentsController < Api::V1::Accounts::BaseController
|
||||
@agents = agents
|
||||
end
|
||||
|
||||
def create; end
|
||||
|
||||
def update
|
||||
@agent.update!(agent_params.slice(:name).compact)
|
||||
@agent.current_account_user.update!(agent_params.slice(:role, :availability, :auto_offline).compact)
|
||||
end
|
||||
|
||||
def destroy
|
||||
@agent.current_account_user.destroy
|
||||
head :ok
|
||||
end
|
||||
|
||||
def update
|
||||
@agent.update!(agent_params.except(:role))
|
||||
@agent.current_account_user.update!(role: agent_params[:role]) if agent_params[:role]
|
||||
render partial: 'api/v1/models/agent.json.jbuilder', locals: { resource: @agent }
|
||||
end
|
||||
|
||||
def create
|
||||
render partial: 'api/v1/models/agent.json.jbuilder', locals: { resource: @user }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def check_authorization
|
||||
@@ -47,22 +44,25 @@ class Api::V1::Accounts::AgentsController < Api::V1::Accounts::BaseController
|
||||
end
|
||||
|
||||
def save_account_user
|
||||
AccountUser.create!(
|
||||
AccountUser.create!({
|
||||
account_id: Current.account.id,
|
||||
user_id: @user.id,
|
||||
role: new_agent_params[:role],
|
||||
inviter_id: current_user.id
|
||||
)
|
||||
}.merge({
|
||||
role: new_agent_params[:role],
|
||||
availability: new_agent_params[:availability],
|
||||
auto_offline: new_agent_params[:auto_offline]
|
||||
}.compact))
|
||||
end
|
||||
|
||||
def agent_params
|
||||
params.require(:agent).permit(:email, :name, :role)
|
||||
params.require(:agent).permit(:name, :email, :name, :role, :availability, :auto_offline)
|
||||
end
|
||||
|
||||
def new_agent_params
|
||||
# intial string ensures the password requirements are met
|
||||
temp_password = "1!aA#{SecureRandom.alphanumeric(12)}"
|
||||
params.require(:agent).permit(:email, :name, :role)
|
||||
params.require(:agent).permit(:email, :name, :role, :availability, :auto_offline)
|
||||
.merge!(password: temp_password, password_confirmation: temp_password, inviter: current_user)
|
||||
end
|
||||
|
||||
|
||||
@@ -12,9 +12,10 @@ class Api::V1::Accounts::CallbacksController < Api::V1::Accounts::BaseController
|
||||
page_access_token: page_access_token
|
||||
)
|
||||
@facebook_inbox = Current.account.inboxes.create!(name: inbox_name, channel: facebook_channel)
|
||||
set_instagram_id(page_access_token, facebook_channel)
|
||||
set_avatar(@facebook_inbox, page_id)
|
||||
rescue StandardError => e
|
||||
Rails.logger.info e
|
||||
Sentry.capture_exception(e)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -22,6 +23,15 @@ class Api::V1::Accounts::CallbacksController < Api::V1::Accounts::BaseController
|
||||
@page_details = mark_already_existing_facebook_pages(fb_object.get_connections('me', 'accounts'))
|
||||
end
|
||||
|
||||
def set_instagram_id(page_access_token, facebook_channel)
|
||||
fb_object = Koala::Facebook::API.new(page_access_token)
|
||||
response = fb_object.get_connections('me', '', { fields: 'instagram_business_account' })
|
||||
return if response['instagram_business_account'].blank?
|
||||
|
||||
instagram_id = response['instagram_business_account']['id']
|
||||
facebook_channel.update(instagram_id: instagram_id)
|
||||
end
|
||||
|
||||
# get params[:inbox_id], current_account. params[:omniauth_token]
|
||||
def reauthorize_page
|
||||
if @inbox&.facebook?
|
||||
@@ -45,8 +55,13 @@ class Api::V1::Accounts::CallbacksController < Api::V1::Accounts::BaseController
|
||||
|
||||
def update_fb_page(fb_page_id, access_token)
|
||||
fb_page = get_fb_page(fb_page_id)
|
||||
fb_page&.update!(user_access_token: @user_access_token, page_access_token: access_token)
|
||||
fb_page&.reauthorized!
|
||||
ActiveRecord::Base.transaction do
|
||||
fb_page&.update!(user_access_token: @user_access_token, page_access_token: access_token)
|
||||
set_instagram_id(access_token, fb_page)
|
||||
fb_page&.reauthorized!
|
||||
rescue StandardError => e
|
||||
Sentry.capture_exception(e)
|
||||
end
|
||||
end
|
||||
|
||||
def get_fb_page(fb_page_id)
|
||||
|
||||
@@ -28,7 +28,7 @@ class Api::V1::Accounts::CampaignsController < Api::V1::Accounts::BaseController
|
||||
end
|
||||
|
||||
def campaign_params
|
||||
params.require(:campaign).permit(:title, :description, :message, :enabled, :inbox_id, :sender_id,
|
||||
params.require(:campaign).permit(:title, :description, :message, :enabled, :trigger_only_during_business_hours, :inbox_id, :sender_id,
|
||||
:scheduled_at, audience: [:type, :id], trigger_rules: {})
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
class Api::V1::Accounts::Contacts::BaseController < Api::V1::Accounts::BaseController
|
||||
before_action :ensure_contact
|
||||
|
||||
private
|
||||
|
||||
def ensure_contact
|
||||
@contact = Current.account.contacts.find(params[:contact_id])
|
||||
end
|
||||
end
|
||||
@@ -1,5 +1,4 @@
|
||||
class Api::V1::Accounts::Contacts::ContactInboxesController < Api::V1::Accounts::BaseController
|
||||
before_action :ensure_contact
|
||||
class Api::V1::Accounts::Contacts::ContactInboxesController < Api::V1::Accounts::Contacts::BaseController
|
||||
before_action :ensure_inbox, only: [:create]
|
||||
|
||||
def create
|
||||
@@ -13,8 +12,4 @@ class Api::V1::Accounts::Contacts::ContactInboxesController < Api::V1::Accounts:
|
||||
@inbox = Current.account.inboxes.find(params[:inbox_id])
|
||||
authorize @inbox, :show?
|
||||
end
|
||||
|
||||
def ensure_contact
|
||||
@contact = Current.account.contacts.find(params[:contact_id])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
class Api::V1::Accounts::Contacts::ConversationsController < Api::V1::Accounts::BaseController
|
||||
class Api::V1::Accounts::Contacts::ConversationsController < Api::V1::Accounts::Contacts::BaseController
|
||||
def index
|
||||
@conversations = Current.account.conversations.includes(
|
||||
:assignee, :contact, :inbox, :taggings
|
||||
).where(inbox_id: inbox_ids, contact_id: permitted_params[:contact_id])
|
||||
).where(inbox_id: inbox_ids, contact_id: @contact.id)
|
||||
end
|
||||
|
||||
private
|
||||
@@ -14,8 +14,4 @@ class Api::V1::Accounts::Contacts::ConversationsController < Api::V1::Accounts::
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
def permitted_params
|
||||
params.permit(:contact_id)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
class Api::V1::Accounts::Contacts::LabelsController < Api::V1::Accounts::BaseController
|
||||
class Api::V1::Accounts::Contacts::LabelsController < Api::V1::Accounts::Contacts::BaseController
|
||||
include LabelConcern
|
||||
|
||||
private
|
||||
|
||||
def model
|
||||
@model ||= Current.account.contacts.find(permitted_params[:contact_id])
|
||||
@model ||= @contact
|
||||
end
|
||||
|
||||
def permitted_params
|
||||
params.permit(:contact_id, labels: [])
|
||||
params.permit(labels: [])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
class Api::V1::Accounts::Contacts::NotesController < Api::V1::Accounts::Contacts::BaseController
|
||||
before_action :note, except: [:index, :create]
|
||||
|
||||
def index
|
||||
@notes = @contact.notes.latest.includes(:user)
|
||||
end
|
||||
|
||||
def create
|
||||
@note = @contact.notes.create!(note_params)
|
||||
end
|
||||
|
||||
def destroy
|
||||
@note.destroy
|
||||
head :ok
|
||||
end
|
||||
|
||||
def show; end
|
||||
|
||||
def update
|
||||
@note.update(note_params)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def note
|
||||
@note ||= @contact.notes.find(params[:id])
|
||||
end
|
||||
|
||||
def note_params
|
||||
params.require(:note).permit(:content).merge({ contact_id: @contact.id, user_id: Current.user.id })
|
||||
end
|
||||
end
|
||||
@@ -1,16 +1,18 @@
|
||||
class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
||||
include Sift
|
||||
|
||||
sort_on :email, type: :string
|
||||
sort_on :name, type: :string
|
||||
sort_on :name, internal_name: :order_on_name, type: :scope, scope_params: [:direction]
|
||||
sort_on :phone_number, type: :string
|
||||
sort_on :last_activity_at, type: :datetime
|
||||
sort_on :last_activity_at, internal_name: :order_on_last_activity_at, type: :scope, scope_params: [:direction]
|
||||
sort_on :company, internal_name: :order_on_company_name, type: :scope, scope_params: [:direction]
|
||||
sort_on :city, internal_name: :order_on_city, type: :scope, scope_params: [:direction]
|
||||
sort_on :country, internal_name: :order_on_country_name, type: :scope, scope_params: [:direction]
|
||||
|
||||
RESULTS_PER_PAGE = 15
|
||||
|
||||
before_action :check_authorization
|
||||
before_action :set_current_page, only: [:index, :active, :search]
|
||||
before_action :fetch_contact, only: [:show, :update, :contactable_inboxes]
|
||||
before_action :fetch_contact, only: [:show, :update, :destroy, :contactable_inboxes, :destroy_custom_attributes]
|
||||
before_action :set_include_contact_inboxes, only: [:index, :search]
|
||||
|
||||
def index
|
||||
@@ -30,10 +32,13 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
||||
end
|
||||
|
||||
def import
|
||||
render json: { error: I18n.t('errors.contacts.import.failed') }, status: :unprocessable_entity and return if params[:import_file].blank?
|
||||
|
||||
ActiveRecord::Base.transaction do
|
||||
import = Current.account.data_imports.create!(data_type: 'contacts')
|
||||
import.import_file.attach(params[:import_file])
|
||||
end
|
||||
|
||||
head :ok
|
||||
end
|
||||
|
||||
@@ -47,11 +52,24 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
||||
|
||||
def show; end
|
||||
|
||||
def filter
|
||||
result = ::Contacts::FilterService.new(params.permit!, current_user).perform
|
||||
contacts = result[:contacts]
|
||||
@contacts_count = result[:count]
|
||||
@contacts = fetch_contacts_with_conversation_count(contacts)
|
||||
end
|
||||
|
||||
def contactable_inboxes
|
||||
@all_contactable_inboxes = Contacts::ContactableInboxesService.new(contact: @contact).get
|
||||
@contactable_inboxes = @all_contactable_inboxes.select { |contactable_inbox| policy(contactable_inbox[:inbox]).show? }
|
||||
end
|
||||
|
||||
# TODO : refactor this method into dedicated contacts/custom_attributes controller class and routes
|
||||
def destroy_custom_attributes
|
||||
@contact.custom_attributes = @contact.custom_attributes.excluding(params[:custom_attributes])
|
||||
@contact.save!
|
||||
end
|
||||
|
||||
def create
|
||||
ActiveRecord::Base.transaction do
|
||||
@contact = Current.account.contacts.new(contact_params)
|
||||
@@ -70,15 +88,26 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
||||
}, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
def destroy
|
||||
if ::OnlineStatusTracker.get_presence(
|
||||
@contact.account.id, 'Contact', @contact.id
|
||||
)
|
||||
return render_error({ message: I18n.t('contacts.online.delete', contact_name: @contact.name.capitalize) },
|
||||
:unprocessable_entity)
|
||||
end
|
||||
|
||||
@contact.destroy!
|
||||
head :ok
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# TODO: Move this to a finder class
|
||||
def resolved_contacts
|
||||
return @resolved_contacts if @resolved_contacts
|
||||
|
||||
@resolved_contacts = Current.account.contacts
|
||||
.where.not(email: [nil, ''])
|
||||
.or(Current.account.contacts.where.not(phone_number: [nil, '']))
|
||||
@resolved_contacts = Current.account.contacts.resolved_contacts
|
||||
|
||||
@resolved_contacts = @resolved_contacts.tagged_with(params[:labels], any: true) if params[:labels].present?
|
||||
@resolved_contacts
|
||||
end
|
||||
@@ -133,4 +162,8 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
||||
def fetch_contact
|
||||
@contact = Current.account.contacts.includes(contact_inboxes: [:inbox]).find(params[:id])
|
||||
end
|
||||
|
||||
def render_error(error, error_status)
|
||||
render json: error, status: error_status
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,20 +1,34 @@
|
||||
class Api::V1::Accounts::Conversations::AssignmentsController < Api::V1::Accounts::Conversations::BaseController
|
||||
# assigns agent/team to a conversation
|
||||
def create
|
||||
set_assignee
|
||||
render json: @assignee
|
||||
if params.key?(:assignee_id)
|
||||
set_agent
|
||||
elsif params.key?(:team_id)
|
||||
set_team
|
||||
else
|
||||
render json: nil
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_assignee
|
||||
# if params[:assignee_id] is not a valid id, it will set to nil, hence unassigning the conversation
|
||||
if params.key?(:assignee_id)
|
||||
@assignee = Current.account.users.find_by(id: params[:assignee_id])
|
||||
@conversation.update_assignee(@assignee)
|
||||
elsif params.key?(:team_id)
|
||||
@assignee = Current.account.teams.find_by(id: params[:team_id])
|
||||
@conversation.update!(team: @assignee)
|
||||
def set_agent
|
||||
@agent = Current.account.users.find_by(id: params[:assignee_id])
|
||||
@conversation.update_assignee(@agent)
|
||||
render_agent
|
||||
end
|
||||
|
||||
def render_agent
|
||||
if @agent.nil?
|
||||
render json: nil
|
||||
else
|
||||
render partial: 'api/v1/models/agent', formats: [:json], locals: { resource: @agent }
|
||||
end
|
||||
end
|
||||
|
||||
def set_team
|
||||
@team = Current.account.teams.find_by(id: params[:team_id])
|
||||
@conversation.update!(team: @team)
|
||||
render json: @team
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2,7 +2,7 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
|
||||
include Events::Types
|
||||
include DateRangeHelper
|
||||
|
||||
before_action :conversation, except: [:index, :meta, :search, :create]
|
||||
before_action :conversation, except: [:index, :meta, :search, :create, :filter]
|
||||
before_action :contact_inbox, only: [:create]
|
||||
|
||||
def index
|
||||
@@ -31,6 +31,12 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
|
||||
|
||||
def show; end
|
||||
|
||||
def filter
|
||||
result = ::Conversations::FilterService.new(params.permit!, current_user).perform
|
||||
@conversations = result[:conversations]
|
||||
@conversations_count = result[:count]
|
||||
end
|
||||
|
||||
def mute
|
||||
@conversation.mute!
|
||||
head :ok
|
||||
@@ -60,15 +66,21 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
|
||||
def toggle_typing_status
|
||||
case params[:typing_status]
|
||||
when 'on'
|
||||
trigger_typing_event(CONVERSATION_TYPING_ON)
|
||||
trigger_typing_event(CONVERSATION_TYPING_ON, params[:is_private])
|
||||
when 'off'
|
||||
trigger_typing_event(CONVERSATION_TYPING_OFF)
|
||||
trigger_typing_event(CONVERSATION_TYPING_OFF, params[:is_private])
|
||||
end
|
||||
head :ok
|
||||
end
|
||||
|
||||
def update_last_seen
|
||||
@conversation.agent_last_seen_at = DateTime.now.utc
|
||||
@conversation.assignee_last_seen_at = DateTime.now.utc if assignee?
|
||||
@conversation.save!
|
||||
end
|
||||
|
||||
def custom_attributes
|
||||
@conversation.custom_attributes = params.permit(custom_attributes: {})[:custom_attributes]
|
||||
@conversation.save!
|
||||
end
|
||||
|
||||
@@ -80,9 +92,9 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
|
||||
@conversation.snoozed_until = parse_date_time(params[:snoozed_until].to_s) if params[:snoozed_until]
|
||||
end
|
||||
|
||||
def trigger_typing_event(event)
|
||||
def trigger_typing_event(event, is_private)
|
||||
user = current_user.presence || @resource
|
||||
Rails.configuration.dispatcher.dispatch(event, Time.zone.now, conversation: @conversation, user: user)
|
||||
Rails.configuration.dispatcher.dispatch(event, Time.zone.now, conversation: @conversation, user: user, is_private: is_private)
|
||||
end
|
||||
|
||||
def conversation
|
||||
@@ -112,6 +124,7 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
|
||||
|
||||
def conversation_params
|
||||
additional_attributes = params[:additional_attributes]&.permit! || {}
|
||||
custom_attributes = params[:custom_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
|
||||
@@ -122,11 +135,18 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
|
||||
contact_id: @contact_inbox.contact_id,
|
||||
contact_inbox_id: @contact_inbox.id,
|
||||
additional_attributes: additional_attributes,
|
||||
snoozed_until: params[:snoozed_until]
|
||||
custom_attributes: custom_attributes,
|
||||
snoozed_until: params[:snoozed_until],
|
||||
assignee_id: params[:assignee_id],
|
||||
team_id: params[:team_id]
|
||||
}.merge(status)
|
||||
end
|
||||
|
||||
def conversation_finder
|
||||
@conversation_finder ||= ConversationFinder.new(current_user, params)
|
||||
end
|
||||
|
||||
def assignee?
|
||||
@conversation.assignee_id? && current_user == @conversation.assignee
|
||||
end
|
||||
end
|
||||
|
||||
@@ -25,18 +25,17 @@ class Api::V1::Accounts::CustomAttributeDefinitionsController < Api::V1::Account
|
||||
private
|
||||
|
||||
def fetch_custom_attributes_definitions
|
||||
@custom_attribute_definitions = Current.account.custom_attribute_definitions.where(
|
||||
attribute_model: permitted_params[:attribute_model] || DEFAULT_ATTRIBUTE_MODEL
|
||||
)
|
||||
@custom_attribute_definitions = Current.account.custom_attribute_definitions.with_attribute_model(permitted_params[:attribute_model])
|
||||
end
|
||||
|
||||
def fetch_custom_attribute_definition
|
||||
@custom_attribute_definition = @custom_attribute_definitions.find(permitted_params[:id])
|
||||
@custom_attribute_definition = Current.account.custom_attribute_definitions.find(permitted_params[:id])
|
||||
end
|
||||
|
||||
def permitted_payload
|
||||
params.require(:custom_attribute_definition).permit(
|
||||
:attribute_display_name,
|
||||
:attribute_description,
|
||||
:attribute_display_type,
|
||||
:attribute_key,
|
||||
:attribute_model,
|
||||
@@ -45,6 +44,6 @@ class Api::V1::Accounts::CustomAttributeDefinitionsController < Api::V1::Account
|
||||
end
|
||||
|
||||
def permitted_params
|
||||
params.permit(:id, :filter_type)
|
||||
params.permit(:id, :filter_type, :attribute_model)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,26 +1,40 @@
|
||||
class Api::V1::Accounts::InboxMembersController < Api::V1::Accounts::BaseController
|
||||
before_action :fetch_inbox, only: [:create, :show]
|
||||
before_action :current_agents_ids, only: [:create]
|
||||
before_action :fetch_inbox
|
||||
before_action :current_agents_ids, only: [:update]
|
||||
|
||||
def create
|
||||
authorize @inbox, :create?
|
||||
begin
|
||||
# update also done via same action
|
||||
update_agents_list
|
||||
head :ok
|
||||
rescue StandardError => e
|
||||
Rails.logger.debug { "Rescued: #{e.inspect}" }
|
||||
render_could_not_create_error('Could not add agents to inbox')
|
||||
ActiveRecord::Base.transaction do
|
||||
params[:user_ids].map { |user_id| @inbox.add_member(user_id) }
|
||||
end
|
||||
fetch_updated_agents
|
||||
end
|
||||
|
||||
def show
|
||||
authorize @inbox, :show?
|
||||
@agents = Current.account.users.where(id: @inbox.members.select(:user_id))
|
||||
fetch_updated_agents
|
||||
end
|
||||
|
||||
def update
|
||||
authorize @inbox, :update?
|
||||
update_agents_list
|
||||
fetch_updated_agents
|
||||
end
|
||||
|
||||
def destroy
|
||||
authorize @inbox, :destroy?
|
||||
ActiveRecord::Base.transaction do
|
||||
params[:user_ids].map { |user_id| @inbox.remove_member(user_id) }
|
||||
end
|
||||
head :ok
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def fetch_updated_agents
|
||||
@agents = Current.account.users.where(id: @inbox.members.select(:user_id))
|
||||
end
|
||||
|
||||
def update_agents_list
|
||||
# get all the user_ids which the inbox currently has as members.
|
||||
# get the list of user_ids from params
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
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
|
||||
# we are already handling the authorization in fetch inbox
|
||||
before_action :check_authorization, except: [:show]
|
||||
|
||||
def index
|
||||
@inboxes = policy_scope(Current.account.inboxes.order_by_name.includes(:channel, { avatar_attachment: [:blob] }))
|
||||
end
|
||||
|
||||
def show; end
|
||||
|
||||
def assignable_agents
|
||||
@assignable_agents = (Current.account.users.where(id: @inbox.members.select(:user_id)) + Current.account.administrators).uniq
|
||||
end
|
||||
@@ -15,26 +18,36 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
||||
@campaigns = @inbox.campaigns
|
||||
end
|
||||
|
||||
def avatar
|
||||
@inbox.avatar.attachment.destroy! if @inbox.avatar.attached?
|
||||
head :ok
|
||||
end
|
||||
|
||||
def create
|
||||
ActiveRecord::Base.transaction do
|
||||
channel = create_channel
|
||||
@inbox = Current.account.inboxes.build(
|
||||
name: permitted_params[:name],
|
||||
greeting_message: permitted_params[:greeting_message],
|
||||
greeting_enabled: permitted_params[:greeting_enabled],
|
||||
channel: channel
|
||||
{
|
||||
name: inbox_name(channel),
|
||||
channel: channel
|
||||
}.merge(
|
||||
permitted_params.except(:channel)
|
||||
)
|
||||
)
|
||||
@inbox.avatar.attach(permitted_params[:avatar])
|
||||
@inbox.save!
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
@inbox.update(inbox_update_params.except(:channel))
|
||||
@inbox.update(permitted_params.except(:channel))
|
||||
@inbox.update_working_hours(params.permit(working_hours: Inbox::OFFISABLE_ATTRS)[:working_hours]) if params[:working_hours]
|
||||
return unless @inbox.channel.is_a?(Channel::WebWidget) && inbox_update_params[:channel].present?
|
||||
|
||||
@inbox.channel.update!(inbox_update_params[:channel])
|
||||
channel_attributes = get_channel_attributes(@inbox.channel_type)
|
||||
|
||||
# Inbox update doesn't necessarily need channel attributes
|
||||
return if permitted_params(channel_attributes)[:channel].blank?
|
||||
|
||||
@inbox.channel.update!(permitted_params(channel_attributes)[:channel])
|
||||
update_channel_feature_flags
|
||||
end
|
||||
|
||||
@@ -69,43 +82,59 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
||||
@agent_bot = AgentBot.find(params[:agent_bot]) if params[:agent_bot]
|
||||
end
|
||||
|
||||
def inbox_name(channel)
|
||||
return channel.try(:bot_name) if channel.is_a?(Channel::Telegram)
|
||||
|
||||
permitted_params[:name]
|
||||
end
|
||||
|
||||
def create_channel
|
||||
case permitted_params[:channel][:type]
|
||||
when 'web_widget'
|
||||
Current.account.web_widgets.create!(permitted_params[:channel].except(:type))
|
||||
Current.account.web_widgets.create!(permitted_params(Channel::WebWidget::EDITABLE_ATTRS)[:channel].except(:type))
|
||||
when 'api'
|
||||
Current.account.api_channels.create!(permitted_params[:channel].except(:type))
|
||||
Current.account.api_channels.create!(permitted_params(Channel::Api::EDITABLE_ATTRS)[:channel].except(:type))
|
||||
when 'email'
|
||||
Current.account.email_channels.create!(permitted_params[:channel].except(:type))
|
||||
Current.account.email_channels.create!(permitted_params(Channel::Email::EDITABLE_ATTRS)[:channel].except(:type))
|
||||
when 'line'
|
||||
Current.account.line_channels.create!(permitted_params(Channel::Line::EDITABLE_ATTRS)[:channel].except(:type))
|
||||
when 'telegram'
|
||||
Current.account.telegram_channels.create!(permitted_params(Channel::Telegram::EDITABLE_ATTRS)[:channel].except(:type))
|
||||
when 'whatsapp'
|
||||
Current.account.whatsapp_channels.create!(permitted_params(Channel::Whatsapp::EDITABLE_ATTRS)[:channel].except(:type))
|
||||
end
|
||||
end
|
||||
|
||||
def update_channel_feature_flags
|
||||
return unless inbox_update_params[:channel].key? :selected_feature_flags
|
||||
return unless @inbox.web_widget?
|
||||
return unless permitted_params(Channel::WebWidget::EDITABLE_ATTRS)[:channel].key? :selected_feature_flags
|
||||
|
||||
@inbox.channel.selected_feature_flags = inbox_update_params[:channel][:selected_feature_flags]
|
||||
@inbox.channel.selected_feature_flags = permitted_params(Channel::WebWidget::EDITABLE_ATTRS)[:channel][:selected_feature_flags]
|
||||
@inbox.channel.save!
|
||||
end
|
||||
|
||||
def permitted_params
|
||||
params.permit(:id, :avatar, :name, :greeting_message, :greeting_enabled, :enable_email_collect, :csat_survey_enabled, channel:
|
||||
[:type, :website_url, :widget_color, :welcome_title, :welcome_tagline, :webhook_url, :email, :reply_time])
|
||||
def permitted_params(channel_attributes = [])
|
||||
params.permit(
|
||||
:name, :avatar, :greeting_enabled, :greeting_message, :enable_email_collect, :csat_survey_enabled,
|
||||
:enable_auto_assignment, :working_hours_enabled, :out_of_office_message, :timezone,
|
||||
channel: [:type, *channel_attributes]
|
||||
)
|
||||
end
|
||||
|
||||
def inbox_update_params
|
||||
params.permit(:enable_auto_assignment, :enable_email_collect, :name, :avatar, :greeting_message, :greeting_enabled, :csat_survey_enabled,
|
||||
:working_hours_enabled, :out_of_office_message, :timezone,
|
||||
channel: [
|
||||
:website_url,
|
||||
:widget_color,
|
||||
:welcome_title,
|
||||
:welcome_tagline,
|
||||
:webhook_url,
|
||||
:email,
|
||||
:reply_time,
|
||||
:pre_chat_form_enabled,
|
||||
{ pre_chat_form_options: [:pre_chat_message, :require_email] },
|
||||
{ selected_feature_flags: [] }
|
||||
])
|
||||
def get_channel_attributes(channel_type)
|
||||
case channel_type
|
||||
when 'Channel::WebWidget'
|
||||
Channel::WebWidget::EDITABLE_ATTRS
|
||||
when 'Channel::Api'
|
||||
Channel::Api::EDITABLE_ATTRS
|
||||
when 'Channel::Email'
|
||||
Channel::Email::EDITABLE_ATTRS
|
||||
when 'Channel::Telegram'
|
||||
Channel::Telegram::EDITABLE_ATTRS
|
||||
when 'Channel::Line'
|
||||
Channel::Line::EDITABLE_ATTRS
|
||||
else
|
||||
[]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -55,7 +55,7 @@ class Api::V1::AccountsController < Api::BaseController
|
||||
end
|
||||
|
||||
def check_signup_enabled
|
||||
raise ActionController::RoutingError, 'Not Found' if ENV.fetch('ENABLE_ACCOUNT_SIGNUP', true) == 'false'
|
||||
raise ActionController::RoutingError, 'Not Found' if GlobalConfig.get_value('ENABLE_ACCOUNT_SIGNUP') == 'false'
|
||||
end
|
||||
|
||||
def pundit_user
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
class Api::V1::ProfilesController < Api::BaseController
|
||||
before_action :set_user
|
||||
|
||||
def show
|
||||
render partial: 'api/v1/models/user.json.jbuilder', locals: { resource: @user }
|
||||
end
|
||||
def show; end
|
||||
|
||||
def update
|
||||
if password_params[:password].present?
|
||||
@@ -15,19 +13,26 @@ class Api::V1::ProfilesController < Api::BaseController
|
||||
@user.update!(profile_params)
|
||||
end
|
||||
|
||||
def availability
|
||||
@user.account_users.find_by!(account_id: availability_params[:account_id]).update!(availability: availability_params[:availability])
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_user
|
||||
@user = current_user
|
||||
end
|
||||
|
||||
def availability_params
|
||||
params.require(:profile).permit(:account_id, :availability)
|
||||
end
|
||||
|
||||
def profile_params
|
||||
params.require(:profile).permit(
|
||||
:email,
|
||||
:name,
|
||||
:display_name,
|
||||
:avatar,
|
||||
:availability,
|
||||
ui_settings: {}
|
||||
)
|
||||
end
|
||||
|
||||
@@ -11,11 +11,19 @@ class Api::V1::Widget::ContactsController < Api::V1::Widget::BaseController
|
||||
render json: contact_identify_action.perform
|
||||
end
|
||||
|
||||
# TODO : clean up this with proper routes delete contacts/custom_attributes
|
||||
def destroy_custom_attributes
|
||||
@contact.custom_attributes = @contact.custom_attributes.excluding(params[:custom_attributes])
|
||||
@contact.save!
|
||||
render json: @contact
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def process_hmac
|
||||
return if params[:identifier_hash].blank?
|
||||
raise StandardError, 'HMAC failed: Invalid Identifier Hash Provided' unless valid_hmac?
|
||||
return if params[:identifier_hash].blank? && !@web_widget.hmac_mandatory
|
||||
|
||||
render json: { error: 'HMAC failed: Invalid Identifier Hash Provided' }, status: :unauthorized unless valid_hmac?
|
||||
|
||||
@contact_inbox.update(hmac_verified: true)
|
||||
end
|
||||
|
||||
@@ -8,8 +8,8 @@ class Api::V1::Widget::MessagesController < Api::V1::Widget::BaseController
|
||||
|
||||
def create
|
||||
@message = conversation.messages.new(message_params)
|
||||
@message.save
|
||||
build_attachment
|
||||
@message.save!
|
||||
end
|
||||
|
||||
def update
|
||||
@@ -29,13 +29,12 @@ class Api::V1::Widget::MessagesController < Api::V1::Widget::BaseController
|
||||
return if params[:message][:attachments].blank?
|
||||
|
||||
params[:message][:attachments].each do |uploaded_attachment|
|
||||
attachment = @message.attachments.new(
|
||||
@message.attachments.new(
|
||||
account_id: @message.account_id,
|
||||
file_type: helpers.file_type(uploaded_attachment&.content_type)
|
||||
file_type: helpers.file_type(uploaded_attachment&.content_type),
|
||||
file: uploaded_attachment
|
||||
)
|
||||
attachment.file.attach(uploaded_attachment)
|
||||
end
|
||||
@message.save!
|
||||
end
|
||||
|
||||
def set_conversation
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
|
||||
before_action :check_authorization
|
||||
|
||||
def account
|
||||
builder = V2::ReportBuilder.new(Current.account, account_report_params)
|
||||
def index
|
||||
builder = V2::ReportBuilder.new(Current.account, report_params)
|
||||
data = builder.build
|
||||
render json: data
|
||||
end
|
||||
|
||||
def account_summary
|
||||
render json: account_summary_metrics
|
||||
def summary
|
||||
render json: summary_metrics
|
||||
end
|
||||
|
||||
def agents
|
||||
@@ -23,31 +23,45 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
|
||||
render layout: false, template: 'api/v2/accounts/reports/inboxes.csv.erb', format: 'csv'
|
||||
end
|
||||
|
||||
def labels
|
||||
response.headers['Content-Type'] = 'text/csv'
|
||||
response.headers['Content-Disposition'] = 'attachment; filename=labels_report.csv'
|
||||
render layout: false, template: 'api/v2/accounts/reports/labels.csv.erb', format: 'csv'
|
||||
end
|
||||
|
||||
def teams
|
||||
response.headers['Content-Type'] = 'text/csv'
|
||||
response.headers['Content-Disposition'] = 'attachment; filename=teams_report.csv'
|
||||
render layout: false, template: 'api/v2/accounts/reports/teams.csv.erb', format: 'csv'
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def check_authorization
|
||||
raise Pundit::NotAuthorizedError unless Current.account_user.administrator?
|
||||
end
|
||||
|
||||
def account_summary_params
|
||||
def summary_params
|
||||
{
|
||||
type: :account,
|
||||
type: params[:type].to_sym,
|
||||
since: params[:since],
|
||||
until: params[:until]
|
||||
until: params[:until],
|
||||
id: params[:id]
|
||||
}
|
||||
end
|
||||
|
||||
def account_report_params
|
||||
def report_params
|
||||
{
|
||||
metric: params[:metric],
|
||||
type: :account,
|
||||
type: params[:type].to_sym,
|
||||
since: params[:since],
|
||||
until: params[:until]
|
||||
until: params[:until],
|
||||
id: params[:id]
|
||||
}
|
||||
end
|
||||
|
||||
def account_summary_metrics
|
||||
builder = V2::ReportBuilder.new(Current.account, account_summary_params)
|
||||
def summary_metrics
|
||||
builder = V2::ReportBuilder.new(Current.account, summary_params)
|
||||
builder.summary
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
class ApplicationController < ActionController::Base
|
||||
include DeviseTokenAuth::Concerns::SetUserByToken
|
||||
include RequestExceptionHandler
|
||||
include Pundit
|
||||
include SwitchLocale
|
||||
|
||||
@@ -9,22 +10,8 @@ class ApplicationController < ActionController::Base
|
||||
around_action :switch_locale
|
||||
around_action :handle_with_exception, unless: :devise_controller?
|
||||
|
||||
rescue_from ActiveRecord::RecordInvalid, with: :render_record_invalid
|
||||
|
||||
private
|
||||
|
||||
def handle_with_exception
|
||||
yield
|
||||
rescue ActiveRecord::RecordNotFound => 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')
|
||||
ensure
|
||||
# to address the thread variable leak issues in Puma/Thin webserver
|
||||
Current.reset
|
||||
end
|
||||
|
||||
def set_current_user
|
||||
@user ||= current_user
|
||||
Current.user = @user
|
||||
@@ -34,32 +21,6 @@ class ApplicationController < ActionController::Base
|
||||
@subscription ||= Current.account.subscription
|
||||
end
|
||||
|
||||
def render_unauthorized(message)
|
||||
render json: { error: message }, status: :unauthorized
|
||||
end
|
||||
|
||||
def render_not_found_error(message)
|
||||
render json: { error: message }, status: :not_found
|
||||
end
|
||||
|
||||
def render_could_not_create_error(message)
|
||||
render json: { error: message }, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
def render_internal_server_error(message)
|
||||
render json: { error: message }, status: :internal_server_error
|
||||
end
|
||||
|
||||
def render_record_invalid(exception)
|
||||
render json: {
|
||||
message: exception.record.errors.full_messages.join(', ')
|
||||
}, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
def render_error_response(exception)
|
||||
render json: exception.to_hash, status: exception.http_status
|
||||
end
|
||||
|
||||
def pundit_user
|
||||
{
|
||||
user: Current.user,
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
module RequestExceptionHandler
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
rescue_from ActiveRecord::RecordInvalid, with: :render_record_invalid
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def handle_with_exception
|
||||
yield
|
||||
rescue ActiveRecord::RecordNotFound => 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')
|
||||
ensure
|
||||
# to address the thread variable leak issues in Puma/Thin webserver
|
||||
Current.reset
|
||||
end
|
||||
|
||||
def render_unauthorized(message)
|
||||
render json: { error: message }, status: :unauthorized
|
||||
end
|
||||
|
||||
def render_not_found_error(message)
|
||||
render json: { error: message }, status: :not_found
|
||||
end
|
||||
|
||||
def render_could_not_create_error(message)
|
||||
render json: { error: message }, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
def render_internal_server_error(message)
|
||||
render json: { error: message }, status: :internal_server_error
|
||||
end
|
||||
|
||||
def render_record_invalid(exception)
|
||||
render json: {
|
||||
message: exception.record.errors.full_messages.join(', ')
|
||||
}, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
def render_error_response(exception)
|
||||
render json: exception.to_hash, status: exception.http_status
|
||||
end
|
||||
end
|
||||
@@ -23,9 +23,13 @@ class DashboardController < ActionController::Base
|
||||
'CREATE_NEW_ACCOUNT_FROM_DASHBOARD',
|
||||
'CHATWOOT_INBOX_TOKEN',
|
||||
'API_CHANNEL_NAME',
|
||||
'API_CHANNEL_THUMBNAIL'
|
||||
'API_CHANNEL_THUMBNAIL',
|
||||
'ANALYTICS_TOKEN',
|
||||
'ANALYTICS_HOST'
|
||||
).merge(
|
||||
APP_VERSION: Chatwoot.config[:version]
|
||||
APP_VERSION: Chatwoot.config[:version],
|
||||
VAPID_PUBLIC_KEY: VapidService.public_key,
|
||||
ENABLE_ACCOUNT_SIGNUP: GlobalConfigService.load('ENABLE_ACCOUNT_SIGNUP', 'false')
|
||||
)
|
||||
end
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ class Platform::Api::V1::AccountsController < PlatformController
|
||||
end
|
||||
|
||||
def destroy
|
||||
# TODO: obfusicate account
|
||||
DeleteObjectJob.perform_later(@resource)
|
||||
head :ok
|
||||
end
|
||||
|
||||
|
||||
@@ -7,8 +7,8 @@ class Platform::Api::V1::UsersController < PlatformController
|
||||
|
||||
def create
|
||||
@resource = (User.find_by(email: user_params[:email]) || User.new(user_params))
|
||||
@resource.confirm
|
||||
@resource.save!
|
||||
@resource.confirm
|
||||
@platform_app.platform_app_permissibles.find_or_create_by(permissible: @resource)
|
||||
end
|
||||
|
||||
@@ -19,21 +19,33 @@ class Platform::Api::V1::UsersController < PlatformController
|
||||
def show; end
|
||||
|
||||
def update
|
||||
@resource.update!(user_params)
|
||||
@resource.assign_attributes(user_update_params)
|
||||
@resource.save!
|
||||
end
|
||||
|
||||
def destroy
|
||||
# TODO: obfusicate user
|
||||
DeleteObjectJob.perform_later(@resource)
|
||||
head :ok
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def user_custom_attributes
|
||||
return @resource.custom_attributes.merge(user_params[:custom_attributes]) if user_params[:custom_attributes]
|
||||
|
||||
@resource.custom_attributes
|
||||
end
|
||||
|
||||
def user_update_params
|
||||
# we want the merged custom attributes not the original one
|
||||
user_params.except(:custom_attributes).merge({ custom_attributes: user_custom_attributes })
|
||||
end
|
||||
|
||||
def set_resource
|
||||
@resource = User.find(params[:id])
|
||||
end
|
||||
|
||||
def user_params
|
||||
params.permit(:name, :email, :password)
|
||||
params.permit(:name, :email, :password, custom_attributes: {})
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
class PlatformController < ActionController::API
|
||||
include RequestExceptionHandler
|
||||
|
||||
before_action :ensure_access_token
|
||||
before_action :set_platform_app
|
||||
before_action :set_resource, only: [:update, :show, :destroy]
|
||||
|
||||
@@ -7,8 +7,8 @@ class Public::Api::V1::Inboxes::MessagesController < Public::Api::V1::InboxesCon
|
||||
|
||||
def create
|
||||
@message = @conversation.messages.new(message_params)
|
||||
@message.save
|
||||
build_attachment
|
||||
@message.save!
|
||||
end
|
||||
|
||||
def update
|
||||
@@ -23,13 +23,12 @@ class Public::Api::V1::Inboxes::MessagesController < Public::Api::V1::InboxesCon
|
||||
return if params[:attachments].blank?
|
||||
|
||||
params[:attachments].each do |uploaded_attachment|
|
||||
attachment = @message.attachments.new(
|
||||
@message.attachments.new(
|
||||
account_id: @message.account_id,
|
||||
file_type: helpers.file_type(uploaded_attachment&.content_type)
|
||||
file_type: helpers.file_type(uploaded_attachment&.content_type),
|
||||
file: uploaded_attachment
|
||||
)
|
||||
attachment.file.attach(uploaded_attachment)
|
||||
end
|
||||
@message.save!
|
||||
end
|
||||
|
||||
def message_finder_params
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# TODO: we should switch to ActionController::API for the base classes
|
||||
# One of the specs is failing when I tried doing that, lets revisit in future
|
||||
class PublicController < ActionController::Base
|
||||
include RequestExceptionHandler
|
||||
skip_before_action :verify_authenticity_token
|
||||
end
|
||||
|
||||
@@ -3,10 +3,9 @@ class SuperAdmin::DashboardController < SuperAdmin::ApplicationController
|
||||
|
||||
def index
|
||||
@data = Conversation.unscoped.group_by_day(:created_at, range: 30.days.ago..2.seconds.ago).count.to_a
|
||||
@accounts_count = number_with_delimiter(Account.all.length)
|
||||
@users_count = number_with_delimiter(User.all.length)
|
||||
@inboxes_count = number_with_delimiter(Inbox.all.length)
|
||||
@conversations_count = number_with_delimiter(Conversation.all.length)
|
||||
@messages_count = number_with_delimiter(Message.all.length)
|
||||
@accounts_count = number_with_delimiter(Account.count)
|
||||
@users_count = number_with_delimiter(User.count)
|
||||
@inboxes_count = number_with_delimiter(Inbox.count)
|
||||
@conversations_count = number_with_delimiter(Conversation.count)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
class Webhooks::InstagramController < ApplicationController
|
||||
skip_before_action :authenticate_user!, raise: false
|
||||
skip_before_action :set_current_user
|
||||
|
||||
def verify
|
||||
if valid_instagram_token?(params['hub.verify_token'])
|
||||
Rails.logger.info('Instagram webhook verified')
|
||||
render json: params['hub.challenge']
|
||||
else
|
||||
render json: { error: 'Error; wrong verify token', status: 403 }
|
||||
end
|
||||
end
|
||||
|
||||
def events
|
||||
Rails.logger.info('Instagram webhook received events')
|
||||
if params['object'].casecmp('instagram').zero?
|
||||
::Webhooks::InstagramEventsJob.perform_later(params.to_unsafe_hash[:entry])
|
||||
render json: :ok
|
||||
else
|
||||
Rails.logger.info("Message is not received from the instagram webhook event: #{params['object']}")
|
||||
head :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def valid_instagram_token?(token)
|
||||
token == ENV['IG_VERIFY_TOKEN']
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,6 @@
|
||||
class Webhooks::LineController < ActionController::API
|
||||
def process_payload
|
||||
Webhooks::LineEventsJob.perform_later(params: params.to_unsafe_hash, signature: request.headers['x-line-signature'], post_body: request.raw_post)
|
||||
head :ok
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,6 @@
|
||||
class Webhooks::TelegramController < ActionController::API
|
||||
def process_payload
|
||||
Webhooks::TelegramEventsJob.perform_later(params.to_unsafe_hash)
|
||||
head :ok
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,6 @@
|
||||
class Webhooks::WhatsappController < ActionController::API
|
||||
def process_payload
|
||||
Webhooks::WhatsappEventsJob.perform_later(params.to_unsafe_hash)
|
||||
head :ok
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,12 @@
|
||||
class MessageDrop < BaseDrop
|
||||
include MessageFormatHelper
|
||||
|
||||
def sender_display_name
|
||||
@obj.sender.try(:available_name)
|
||||
end
|
||||
|
||||
def text_content
|
||||
content = @obj.try(:content)
|
||||
transform_user_mention_content content
|
||||
end
|
||||
end
|
||||
@@ -94,6 +94,8 @@ class ConversationFinder
|
||||
end
|
||||
|
||||
def filter_by_status
|
||||
return if params[:status] == 'all'
|
||||
|
||||
@conversations = @conversations.where(status: params[:status] || DEFAULT_STATUS)
|
||||
end
|
||||
|
||||
|
||||
@@ -3,12 +3,12 @@ module FileTypeHelper
|
||||
return :image if [
|
||||
'image/jpeg',
|
||||
'image/png',
|
||||
'image/svg+xml',
|
||||
'image/gif',
|
||||
'image/tiff',
|
||||
'image/bmp'
|
||||
].include?(content_type)
|
||||
|
||||
return :video if content_type.include?('video/')
|
||||
return :audio if content_type.include?('audio/')
|
||||
|
||||
:file
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
:has-accounts="hasAccounts"
|
||||
/>
|
||||
<woot-snackbar-box />
|
||||
<network-notification />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -15,6 +16,7 @@
|
||||
import { mapGetters } from 'vuex';
|
||||
import AddAccountModal from '../dashboard/components/layout/sidebarComponents/AddAccountModal';
|
||||
import WootSnackbarBox from './components/SnackbarContainer';
|
||||
import NetworkNotification from './components/NetworkNotification';
|
||||
import { accountIdFromPathname } from './helper/URLHelper';
|
||||
|
||||
export default {
|
||||
@@ -23,6 +25,7 @@ export default {
|
||||
components: {
|
||||
WootSnackbarBox,
|
||||
AddAccountModal,
|
||||
NetworkNotification,
|
||||
},
|
||||
|
||||
data() {
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
/* global axios */
|
||||
|
||||
import ApiClient from './ApiClient';
|
||||
|
||||
class AccountActions extends ApiClient {
|
||||
constructor() {
|
||||
super('actions', { accountScoped: true });
|
||||
}
|
||||
|
||||
merge(parentId, childId) {
|
||||
return axios.post(`${this.url}/contact_merge`, {
|
||||
base_contact_id: parentId,
|
||||
mergee_contact_id: childId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new AccountActions();
|
||||
@@ -0,0 +1,14 @@
|
||||
/* global axios */
|
||||
import ApiClient from './ApiClient';
|
||||
|
||||
class AttributeAPI extends ApiClient {
|
||||
constructor() {
|
||||
super('custom_attribute_definitions', { accountScoped: true });
|
||||
}
|
||||
|
||||
getAttributesByModel() {
|
||||
return axios.get(this.url);
|
||||
}
|
||||
}
|
||||
|
||||
export default new AttributeAPI();
|
||||
@@ -161,9 +161,9 @@ export default {
|
||||
});
|
||||
},
|
||||
|
||||
updateAvailability({ availability }) {
|
||||
return axios.put(endPoints('profileUpdate').url, {
|
||||
profile: { availability },
|
||||
updateAvailability(availabilityData) {
|
||||
return axios.post(endPoints('availabilityUpdate').url, {
|
||||
profile: { ...availabilityData },
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
@@ -2,7 +2,27 @@ import ApiClient from './ApiClient';
|
||||
|
||||
class ContactNotes extends ApiClient {
|
||||
constructor() {
|
||||
super('contact_notes', { accountScoped: true });
|
||||
super('notes', { accountScoped: true });
|
||||
this.contactId = null;
|
||||
}
|
||||
|
||||
get url() {
|
||||
return `${this.baseUrl()}/contacts/${this.contactId}/notes`;
|
||||
}
|
||||
|
||||
get(contactId) {
|
||||
this.contactId = contactId;
|
||||
return super.get();
|
||||
}
|
||||
|
||||
create(contactId, content) {
|
||||
this.contactId = contactId;
|
||||
return super.create({ content });
|
||||
}
|
||||
|
||||
delete(contactId, id) {
|
||||
this.contactId = contactId;
|
||||
return super.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -52,6 +52,20 @@ class ContactAPI extends ApiClient {
|
||||
)}`;
|
||||
return axios.get(requestURL);
|
||||
}
|
||||
|
||||
importContacts(file) {
|
||||
const formData = new FormData();
|
||||
formData.append('import_file', file);
|
||||
return axios.post(`${this.url}/import`, formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
});
|
||||
}
|
||||
|
||||
destroyCustomAttributes(contactId, customAttributes) {
|
||||
return axios.post(`${this.url}/${contactId}/destroy_custom_attributes`, {
|
||||
custom_attributes: customAttributes,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new ContactAPI();
|
||||
|
||||
@@ -13,6 +13,9 @@ const endPoints = {
|
||||
profileUpdate: {
|
||||
url: '/api/v1/profile',
|
||||
},
|
||||
availabilityUpdate: {
|
||||
url: '/api/v1/profile/availability',
|
||||
},
|
||||
logout: {
|
||||
url: 'auth/sign_out',
|
||||
},
|
||||
|
||||
@@ -51,9 +51,10 @@ class ConversationApi extends ApiClient {
|
||||
return axios.post(`${this.url}/${id}/update_last_seen`);
|
||||
}
|
||||
|
||||
toggleTyping({ conversationId, status }) {
|
||||
toggleTyping({ conversationId, status, isPrivate }) {
|
||||
return axios.post(`${this.url}/${conversationId}/toggle_typing_status`, {
|
||||
typing_status: status,
|
||||
is_private: isPrivate
|
||||
});
|
||||
}
|
||||
|
||||
@@ -80,6 +81,12 @@ class ConversationApi extends ApiClient {
|
||||
sendEmailTranscript({ conversationId, email }) {
|
||||
return axios.post(`${this.url}/${conversationId}/transcript`, { email });
|
||||
}
|
||||
|
||||
updateCustomAttributes({ conversationId, customAttributes }) {
|
||||
return axios.post(`${this.url}/${conversationId}/custom_attributes`, {
|
||||
custom_attributes: customAttributes,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new ConversationApi();
|
||||
|
||||
@@ -8,6 +8,8 @@ export const buildCreatePayload = ({
|
||||
contentAttributes,
|
||||
echoId,
|
||||
file,
|
||||
ccEmails = '',
|
||||
bccEmails = '',
|
||||
}) => {
|
||||
let payload;
|
||||
if (file) {
|
||||
@@ -18,12 +20,16 @@ export const buildCreatePayload = ({
|
||||
}
|
||||
payload.append('private', isPrivate);
|
||||
payload.append('echo_id', echoId);
|
||||
payload.append('cc_emails', ccEmails);
|
||||
payload.append('bcc_emails', bccEmails);
|
||||
} else {
|
||||
payload = {
|
||||
content: message,
|
||||
private: isPrivate,
|
||||
echo_id: echoId,
|
||||
content_attributes: contentAttributes,
|
||||
cc_emails: ccEmails,
|
||||
bcc_emails: bccEmails,
|
||||
};
|
||||
}
|
||||
return payload;
|
||||
@@ -41,6 +47,8 @@ class MessageApi extends ApiClient {
|
||||
contentAttributes,
|
||||
echo_id: echoId,
|
||||
file,
|
||||
ccEmails = '',
|
||||
bccEmails = '',
|
||||
}) {
|
||||
return axios({
|
||||
method: 'post',
|
||||
@@ -51,6 +59,8 @@ class MessageApi extends ApiClient {
|
||||
contentAttributes,
|
||||
echoId,
|
||||
file,
|
||||
ccEmails,
|
||||
bccEmails,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@ class InboxMembers extends ApiClient {
|
||||
super('inbox_members', { accountScoped: true });
|
||||
}
|
||||
|
||||
create({ inboxId, agentList }) {
|
||||
return axios.post(this.url, {
|
||||
update({ inboxId, agentList }) {
|
||||
return axios.patch(this.url, {
|
||||
inbox_id: inboxId,
|
||||
user_ids: agentList,
|
||||
});
|
||||
|
||||
@@ -13,6 +13,10 @@ class Inboxes extends ApiClient {
|
||||
getCampaigns(inboxId) {
|
||||
return axios.get(`${this.url}/${inboxId}/campaigns`);
|
||||
}
|
||||
|
||||
deleteInboxAvatar(inboxId) {
|
||||
return axios.delete(`${this.url}/${inboxId}/avatar`);
|
||||
}
|
||||
}
|
||||
|
||||
export default new Inboxes();
|
||||
|
||||
@@ -6,15 +6,15 @@ class ReportsAPI extends ApiClient {
|
||||
super('reports', { accountScoped: true, apiVersion: 'v2' });
|
||||
}
|
||||
|
||||
getAccountReports(metric, since, until) {
|
||||
return axios.get(`${this.url}/account`, {
|
||||
params: { metric, since, until },
|
||||
getReports(metric, since, until, type = 'account', id) {
|
||||
return axios.get(`${this.url}`, {
|
||||
params: { metric, since, until, type, id },
|
||||
});
|
||||
}
|
||||
|
||||
getAccountSummary(since, until) {
|
||||
return axios.get(`${this.url}/account_summary`, {
|
||||
params: { since, until },
|
||||
getSummary(since, until, type = 'account', id) {
|
||||
return axios.get(`${this.url}/summary`, {
|
||||
params: { since, until, type, id },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -23,6 +23,24 @@ class ReportsAPI extends ApiClient {
|
||||
params: { since, until },
|
||||
});
|
||||
}
|
||||
|
||||
getLabelReports(since, until) {
|
||||
return axios.get(`${this.url}/labels`, {
|
||||
params: { since, until },
|
||||
});
|
||||
}
|
||||
|
||||
getInboxReports(since, until) {
|
||||
return axios.get(`${this.url}/inboxes`, {
|
||||
params: { since, until },
|
||||
});
|
||||
}
|
||||
|
||||
getTeamReports(since, until) {
|
||||
return axios.get(`${this.url}/teams`, {
|
||||
params: { since, until },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new ReportsAPI();
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import accountActionsAPI from '../accountActions';
|
||||
import ApiClient from '../ApiClient';
|
||||
import describeWithAPIMock from './apiSpecHelper';
|
||||
|
||||
describe('#ContactsAPI', () => {
|
||||
it('creates correct instance', () => {
|
||||
expect(accountActionsAPI).toBeInstanceOf(ApiClient);
|
||||
expect(accountActionsAPI).toHaveProperty('merge');
|
||||
});
|
||||
|
||||
describeWithAPIMock('API calls', context => {
|
||||
it('#merge', () => {
|
||||
accountActionsAPI.merge(1, 2);
|
||||
expect(context.axiosMock.post).toHaveBeenCalledWith(
|
||||
'/api/v1/actions/contact_merge',
|
||||
{
|
||||
base_contact_id: 1,
|
||||
mergee_contact_id: 2,
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -59,6 +59,28 @@ describe('#ContactsAPI', () => {
|
||||
'/api/v1/contacts/search?include_contact_inboxes=false&page=1&sort=date&q=leads&labels[]=customer-support'
|
||||
);
|
||||
});
|
||||
|
||||
it('#destroyCustomAttributes', () => {
|
||||
contactAPI.destroyCustomAttributes(1, ['cloudCustomer']);
|
||||
expect(context.axiosMock.post).toHaveBeenCalledWith(
|
||||
'/api/v1/contacts/1/destroy_custom_attributes',
|
||||
{
|
||||
custom_attributes: ['cloudCustomer'],
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('#importContacts', () => {
|
||||
const file = 'file';
|
||||
contactAPI.importContacts(file);
|
||||
expect(context.axiosMock.post).toHaveBeenCalledWith(
|
||||
'/api/v1/contacts/import',
|
||||
expect.any(FormData),
|
||||
{
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -160,5 +160,18 @@ describe('#ConversationAPI', () => {
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('#updateCustomAttributes', () => {
|
||||
conversationAPI.updateCustomAttributes({
|
||||
conversationId: 45,
|
||||
customAttributes: { order_d: '1001' },
|
||||
});
|
||||
expect(context.axiosMock.post).toHaveBeenCalledWith(
|
||||
'/api/v1/conversations/45/custom_attributes',
|
||||
{
|
||||
custom_attributes: { order_d: '1001' },
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -35,12 +35,14 @@ describe('#ConversationAPI', () => {
|
||||
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');
|
||||
expect(formPayload.get('cc_emails')).toEqual('');
|
||||
});
|
||||
|
||||
it('builds object payload if file is not available', () => {
|
||||
@@ -56,6 +58,8 @@ describe('#ConversationAPI', () => {
|
||||
private: false,
|
||||
echo_id: 12,
|
||||
content_attributes: { in_reply_to: 12 },
|
||||
bcc_emails: '',
|
||||
cc_emails: '',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -27,5 +27,12 @@ describe('#InboxesAPI', () => {
|
||||
'/api/v1/inboxes/2/campaigns'
|
||||
);
|
||||
});
|
||||
|
||||
it('#deleteInboxAvatar', () => {
|
||||
inboxesAPI.deleteInboxAvatar(2);
|
||||
expect(context.axiosMock.delete).toHaveBeenCalledWith(
|
||||
'/api/v1/inboxes/2/avatar'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,37 +11,35 @@ describe('#Reports API', () => {
|
||||
expect(reportsAPI).toHaveProperty('create');
|
||||
expect(reportsAPI).toHaveProperty('update');
|
||||
expect(reportsAPI).toHaveProperty('delete');
|
||||
expect(reportsAPI).toHaveProperty('getAccountReports');
|
||||
expect(reportsAPI).toHaveProperty('getAccountSummary');
|
||||
expect(reportsAPI).toHaveProperty('getReports');
|
||||
expect(reportsAPI).toHaveProperty('getSummary');
|
||||
expect(reportsAPI).toHaveProperty('getAgentReports');
|
||||
expect(reportsAPI).toHaveProperty('getLabelReports');
|
||||
expect(reportsAPI).toHaveProperty('getInboxReports');
|
||||
expect(reportsAPI).toHaveProperty('getTeamReports');
|
||||
});
|
||||
describeWithAPIMock('API calls', context => {
|
||||
it('#getAccountReports', () => {
|
||||
reportsAPI.getAccountReports(
|
||||
'conversations_count',
|
||||
1621103400,
|
||||
1621621800
|
||||
);
|
||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v2/reports/account',
|
||||
{
|
||||
params: {
|
||||
metric: 'conversations_count',
|
||||
since: 1621103400,
|
||||
until: 1621621800,
|
||||
},
|
||||
}
|
||||
);
|
||||
reportsAPI.getReports('conversations_count', 1621103400, 1621621800);
|
||||
expect(context.axiosMock.get).toHaveBeenCalledWith('/api/v2/reports', {
|
||||
params: {
|
||||
metric: 'conversations_count',
|
||||
since: 1621103400,
|
||||
until: 1621621800,
|
||||
type: 'account',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('#getAccountSummary', () => {
|
||||
reportsAPI.getAccountSummary(1621103400, 1621621800);
|
||||
reportsAPI.getSummary(1621103400, 1621621800);
|
||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v2/reports/account_summary',
|
||||
'/api/v2/reports/summary',
|
||||
{
|
||||
params: {
|
||||
since: 1621103400,
|
||||
until: 1621621800,
|
||||
type: 'account',
|
||||
},
|
||||
}
|
||||
);
|
||||
@@ -59,5 +57,44 @@ describe('#Reports API', () => {
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('#getLabelReports', () => {
|
||||
reportsAPI.getLabelReports(1621103400, 1621621800);
|
||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v2/reports/labels',
|
||||
{
|
||||
params: {
|
||||
since: 1621103400,
|
||||
until: 1621621800,
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('#getInboxReports', () => {
|
||||
reportsAPI.getInboxReports(1621103400, 1621621800);
|
||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v2/reports/inboxes',
|
||||
{
|
||||
params: {
|
||||
since: 1621103400,
|
||||
until: 1621621800,
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('#getTeamReports', () => {
|
||||
reportsAPI.getTeamReports(1621103400, 1621621800);
|
||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v2/reports/teams',
|
||||
{
|
||||
params: {
|
||||
since: 1621103400,
|
||||
until: 1621621800,
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 13 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 22 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 36 KiB |
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 12 KiB |
@@ -93,3 +93,17 @@
|
||||
/* .slide-fade-leave-active for <2.1.8 */ {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.network-notification-fade-enter-active {
|
||||
transition: all .1s $ease-in-sine;
|
||||
}
|
||||
|
||||
.network-notification-fade-leave-active {
|
||||
transition: all .1s $ease-out-sine;
|
||||
}
|
||||
|
||||
.network-notification-fade-enter,
|
||||
.network-notification-fade-leave-to {
|
||||
transform: translateY(-$space-small);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
.card {
|
||||
margin-bottom: var(--space-small);
|
||||
padding: var(--space-small);
|
||||
padding: var(--space-normal);
|
||||
}
|
||||
|
||||
.button-wrapper .button.link.grey-btn {
|
||||
@@ -21,7 +21,7 @@
|
||||
font-size: $font-size-mini;
|
||||
max-width: 15rem;
|
||||
padding: $space-smaller $space-small;
|
||||
z-index: 9999;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
code {
|
||||
|
||||
@@ -55,6 +55,10 @@
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.w-100 {
|
||||
.w-full {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.h-full {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
.margin-right-small {
|
||||
margin-right: var(--space-small);
|
||||
}
|
||||
|
||||
.fs-small {
|
||||
font-size: var(--font-size-small);
|
||||
}
|
||||
|
||||
.fs-default {
|
||||
font-size: var(--font-size-default);
|
||||
}
|
||||
|
||||
.fw-medium {
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
|
||||
.p-normal {
|
||||
padding: var(--space-normal);
|
||||
}
|
||||
|
||||
.overflow-scroll {
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
.overflow-auto {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.overflow-hidden {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
||||
.border-right {
|
||||
border-right: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.border-left {
|
||||
border-left: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.bg-white {
|
||||
background-color: var(--white);
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
@import 'shared/assets/stylesheets/font-weights';
|
||||
@import 'shared/assets/stylesheets/shadows';
|
||||
@import 'shared/assets/stylesheets/border-radius';
|
||||
@import 'shared/assets/stylesheets/z-index';
|
||||
|
||||
@import 'variables';
|
||||
|
||||
@@ -48,3 +49,4 @@
|
||||
@import 'plugins/multiselect';
|
||||
@import 'plugins/dropdown';
|
||||
@import '~shared/assets/stylesheets/ionicons';
|
||||
@import 'utility-helpers';
|
||||
|
||||
@@ -15,6 +15,10 @@
|
||||
.multiselect {
|
||||
margin-bottom: var(--space-normal);
|
||||
|
||||
&.multiselect--disabled {
|
||||
opacity: .8;
|
||||
}
|
||||
|
||||
.multiselect--active {
|
||||
>.multiselect__tags {
|
||||
border-color: $color-woot;
|
||||
@@ -209,3 +213,53 @@
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.multiselect-wrap--medium {
|
||||
$multiselect-height: 4.8rem;
|
||||
|
||||
.multiselect__tags,
|
||||
.multiselect__input {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.multiselect__tags,
|
||||
.multiselect__input,
|
||||
.multiselect {
|
||||
background: var(--white);
|
||||
font-size: var(--font-size-small);
|
||||
height: $multiselect-height;
|
||||
min-height: $multiselect-height;
|
||||
}
|
||||
|
||||
.multiselect__input {
|
||||
height: $multiselect-height - $space-micro;
|
||||
min-height: $multiselect-height - $space-micro;
|
||||
}
|
||||
|
||||
.multiselect__single {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
font-size: var(--font-size-small);
|
||||
margin: 0;
|
||||
padding: var(--space-smaller) var(--space-micro);
|
||||
}
|
||||
|
||||
.multiselect__placeholder {
|
||||
margin: 0;
|
||||
padding: var(--space-smaller) var(--space-micro);
|
||||
}
|
||||
|
||||
.multiselect__select {
|
||||
min-height: $multiselect-height;
|
||||
}
|
||||
|
||||
.multiselect--disabled .multiselect__current,
|
||||
.multiselect--disabled .multiselect__select {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.multiselect__tags-wrap {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,7 +71,6 @@
|
||||
}
|
||||
|
||||
&.over {
|
||||
|
||||
&::after {
|
||||
background: $color-woot;
|
||||
}
|
||||
@@ -80,7 +79,7 @@
|
||||
background: $color-woot;
|
||||
}
|
||||
|
||||
&+.item {
|
||||
& + .item {
|
||||
&::before {
|
||||
background: $color-woot;
|
||||
}
|
||||
@@ -132,10 +131,13 @@
|
||||
@include padding($space-medium);
|
||||
@include border-light;
|
||||
@include full-height();
|
||||
|
||||
&.height-auto {
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.inoboxes-list {
|
||||
|
||||
.inbox-item {
|
||||
@include margin($space-normal);
|
||||
@include flex;
|
||||
@@ -189,9 +191,9 @@
|
||||
align-self: center;
|
||||
color: $medium-gray;
|
||||
font-size: $font-size-small;
|
||||
opacity: .7;
|
||||
opacity: 0.7;
|
||||
transform: translateX(0);
|
||||
transition: opacity 0.100s ease-in 0s, transform 0.200s ease-in 0.030s;
|
||||
transition: opacity 0.1s ease-in 0s, transform 0.2s ease-in 0.03s;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -226,7 +228,7 @@
|
||||
@include padding($space-medium);
|
||||
}
|
||||
|
||||
>a>img {
|
||||
> a > img {
|
||||
width: $space-larger * 5;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,14 +42,6 @@ $resolve-button-width: 13.2rem;
|
||||
margin-right: var(--space-normal);
|
||||
min-width: 0;
|
||||
|
||||
.user--name {
|
||||
@include margin(0);
|
||||
display: inline-block;
|
||||
font-size: $font-size-medium;
|
||||
line-height: 1.3;
|
||||
text-transform: capitalize;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.user--profile__meta {
|
||||
align-items: flex-start;
|
||||
@@ -59,12 +51,6 @@ $resolve-button-width: 13.2rem;
|
||||
margin-left: $space-slab;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.user--profile__button {
|
||||
font-size: $font-size-mini;
|
||||
margin-top: $space-micro;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -144,7 +144,10 @@
|
||||
padding-left: 0;
|
||||
|
||||
.conversation--details {
|
||||
border-radius: var(--border-radius-small);
|
||||
margin-left: 0;
|
||||
padding-left: var(--space-two);
|
||||
padding-right: var(--space-small);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
.image {
|
||||
.image,
|
||||
.video {
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
|
||||
@@ -26,7 +27,13 @@
|
||||
}
|
||||
|
||||
.modal-image {
|
||||
max-width: 85%;
|
||||
max-height: 90%;
|
||||
max-width: 90%;
|
||||
}
|
||||
|
||||
.modal-video {
|
||||
max-height: 75vh;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
&::before {
|
||||
@@ -35,11 +42,21 @@
|
||||
content: '';
|
||||
height: 20%;
|
||||
left: 0;
|
||||
opacity: .8;
|
||||
opacity: 0.8;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.video {
|
||||
.modal-container {
|
||||
width: auto;
|
||||
|
||||
.modal--close {
|
||||
z-index: var(--z-index-low);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.conversations-list-wrap {
|
||||
@@ -93,7 +110,7 @@
|
||||
|
||||
.conversation-panel {
|
||||
@include flex;
|
||||
@include flex-weight(1);
|
||||
@include flex-weight(1 1 1px);
|
||||
@include margin($zero);
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
@@ -113,10 +130,18 @@
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: $space-small;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
&.unread--toast {
|
||||
+.right {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
+.left {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
span {
|
||||
@include elegant-card;
|
||||
@include round-corner;
|
||||
@@ -140,6 +165,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
&.left {
|
||||
|
||||
.bubble {
|
||||
@@ -183,6 +209,19 @@
|
||||
}
|
||||
}
|
||||
|
||||
+.unread--toast {
|
||||
+.right {
|
||||
margin-top: $space-one;
|
||||
|
||||
.bubble {
|
||||
border-top-right-radius: $space-one;
|
||||
}
|
||||
}
|
||||
|
||||
+.left {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.right {
|
||||
@@ -226,6 +265,21 @@
|
||||
border-top-left-radius: $space-one;
|
||||
}
|
||||
}
|
||||
|
||||
+.unread--toast {
|
||||
+.left {
|
||||
margin-top: $space-one;
|
||||
|
||||
.bubble {
|
||||
border-top-left-radius: $space-one;
|
||||
}
|
||||
}
|
||||
|
||||
+.right {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
&.center {
|
||||
@@ -259,11 +313,11 @@
|
||||
display: flex;
|
||||
font-size: var(--font-size-small);
|
||||
justify-content: center;
|
||||
margin: var(--space-small) var(--space-normal);
|
||||
padding: var(--space-small) var(--space-normal);
|
||||
margin: var(--space-smaller) 0;
|
||||
padding: var(--space-smaller) var(--space-micro) var(--space-smaller) var(--space-one);
|
||||
|
||||
.is-text {
|
||||
display: inline-block;
|
||||
display: inline-flex;
|
||||
text-align: start;
|
||||
|
||||
@include breakpoint(xxxlarge up) {
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
.error {
|
||||
|
||||
#{$all-text-inputs},
|
||||
select,
|
||||
.multiselect>.multiselect__tags {
|
||||
.multiselect > .multiselect__tags {
|
||||
@include thin-border(var(--r-400));
|
||||
}
|
||||
|
||||
@@ -40,4 +39,8 @@ input {
|
||||
font-size: var(--font-size-small);
|
||||
height: var(--space-large);
|
||||
}
|
||||
|
||||
.error {
|
||||
border-color: var(--r-400);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,7 +71,8 @@
|
||||
@include padding($space-large);
|
||||
}
|
||||
|
||||
form {
|
||||
form,
|
||||
.modal-content {
|
||||
@include padding($space-large);
|
||||
align-self: center;
|
||||
|
||||
|
||||
@@ -32,7 +32,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.report-bar {
|
||||
@include margin(-1px $zero);
|
||||
@include background-white;
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
.date-picker {
|
||||
margin-left: var(--space-smaller);
|
||||
}
|
||||
|
||||
.margin-left-small {
|
||||
margin-left: var(--space-smaller);
|
||||
}
|
||||
|
||||
.reports-option__rounded--item {
|
||||
border-radius: 100%;
|
||||
height: var(--space-two);
|
||||
width: var(--space-two);
|
||||
}
|
||||
|
||||
.reports-option__item {
|
||||
flex-shrink: 0;
|
||||
margin-right: var(--space-small);
|
||||
}
|
||||
|
||||
.reports-option__label--swatch {
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.display-flex {
|
||||
display: flex;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import AccordionItemComponent from './AccordionItem';
|
||||
|
||||
export default {
|
||||
title: 'Components/Generic/Accordion',
|
||||
component: AccordionItemComponent,
|
||||
argTypes: {
|
||||
title: {
|
||||
control: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const Template = (args, { argTypes }) => ({
|
||||
props: Object.keys(argTypes),
|
||||
components: { AccordionItem: AccordionItemComponent },
|
||||
template: `
|
||||
<accordion-item v-bind="$props" @click="onClick">
|
||||
This is a sample content you can pass as a slot
|
||||
</accordion-item>
|
||||
`,
|
||||
});
|
||||
|
||||
export const AccordionItem = Template.bind({});
|
||||
AccordionItem.args = {
|
||||
onClick: action('Added'),
|
||||
title: 'Title of the accordion item',
|
||||
};
|
||||
@@ -0,0 +1,122 @@
|
||||
<template>
|
||||
<div class="cw-accordion">
|
||||
<button class="cw-accordion--title" @click="$emit('click')">
|
||||
<div class="cw-accordion--title-wrap">
|
||||
<emoji-or-icon class="icon-or-emoji" :icon="icon" :emoji="emoji" />
|
||||
<h5>
|
||||
{{ title }}
|
||||
</h5>
|
||||
</div>
|
||||
<div class="button-icon--wrap">
|
||||
<slot name="button" />
|
||||
<div class="chevron-icon__wrap">
|
||||
<i v-if="isOpen" class="ion-minus chevron-icon"></i>
|
||||
<i v-else class="ion-plus chevron-icon"></i>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
<div
|
||||
v-if="isOpen"
|
||||
class="cw-accordion--content"
|
||||
:class="{ compact: compact }"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import EmojiOrIcon from 'shared/components/EmojiOrIcon';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
EmojiOrIcon,
|
||||
},
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
compact: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
emoji: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
isOpen: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.cw-accordion {
|
||||
// This is done to fix contact sidebar border issues
|
||||
// If you are using it else, find a fix to remove this hack
|
||||
margin-top: -1px;
|
||||
font-size: var(--font-size-small);
|
||||
}
|
||||
.cw-accordion--title {
|
||||
align-items: center;
|
||||
background: var(--b-50);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
border-top: 1px solid var(--color-border);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin: 0;
|
||||
padding: var(--space-small) var(--space-normal);
|
||||
user-select: none;
|
||||
width: 100%;
|
||||
|
||||
h5 {
|
||||
font-size: var(--font-size-normal);
|
||||
margin-bottom: 0;
|
||||
padding: 0 var(--space-small) 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
.cw-accordion--title-wrap {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: var(--space-micro);
|
||||
}
|
||||
|
||||
.title-icon__wrap {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.icon-or-emoji {
|
||||
display: inline-block;
|
||||
width: var(--space-two);
|
||||
}
|
||||
|
||||
.button-icon--wrap {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.chevron-icon__wrap {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
width: var(--space-slab);
|
||||
color: var(--w-500);
|
||||
}
|
||||
|
||||
.cw-accordion--content {
|
||||
padding: var(--space-normal);
|
||||
|
||||
&.compact {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -26,6 +26,7 @@
|
||||
:active-label="label"
|
||||
:team-id="teamId"
|
||||
:chat="chat"
|
||||
:show-assignee="showAssigneeInConversationCard"
|
||||
/>
|
||||
|
||||
<div v-if="chatListLoading" class="text-center">
|
||||
@@ -119,6 +120,9 @@ export default {
|
||||
};
|
||||
});
|
||||
},
|
||||
showAssigneeInConversationCard() {
|
||||
return this.activeAssigneeTab === wootConstants.ASSIGNEE_TYPE.ALL;
|
||||
},
|
||||
inbox() {
|
||||
return this.$store.getters['inboxes/getInbox'](this.activeInbox);
|
||||
},
|
||||
@@ -194,7 +198,7 @@ export default {
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
handleKeyEvents(e) {
|
||||
getKeyboardListenerParams() {
|
||||
const allConversations = this.$refs.activeConversation.querySelectorAll(
|
||||
'div.conversations-list div.conversation'
|
||||
);
|
||||
@@ -205,7 +209,19 @@ export default {
|
||||
activeConversation
|
||||
);
|
||||
const lastConversationIndex = allConversations.length - 1;
|
||||
return {
|
||||
allConversations,
|
||||
activeConversation,
|
||||
activeConversationIndex,
|
||||
lastConversationIndex,
|
||||
};
|
||||
},
|
||||
handleKeyEvents(e) {
|
||||
if (hasPressedAltAndJKey(e)) {
|
||||
const {
|
||||
allConversations,
|
||||
activeConversationIndex,
|
||||
} = this.getKeyboardListenerParams();
|
||||
if (activeConversationIndex === -1) {
|
||||
allConversations[0].click();
|
||||
}
|
||||
@@ -214,6 +230,11 @@ export default {
|
||||
}
|
||||
}
|
||||
if (hasPressedAltAndKKey(e)) {
|
||||
const {
|
||||
allConversations,
|
||||
activeConversationIndex,
|
||||
lastConversationIndex,
|
||||
} = this.getKeyboardListenerParams();
|
||||
if (activeConversationIndex === -1) {
|
||||
allConversations[lastConversationIndex].click();
|
||||
} else if (activeConversationIndex < lastConversationIndex) {
|
||||
|
||||
@@ -0,0 +1,238 @@
|
||||
<template>
|
||||
<div class="custom-attribute">
|
||||
<div class="title-wrap">
|
||||
<h4 class="text-block-title title error">
|
||||
<span class="attribute-name" :class="{ error: $v.editedValue.$error }">
|
||||
{{ label }}
|
||||
</span>
|
||||
</h4>
|
||||
</div>
|
||||
<div v-show="isEditing">
|
||||
<div class="input-group small">
|
||||
<input
|
||||
ref="inputfield"
|
||||
v-model="editedValue"
|
||||
:type="inputType"
|
||||
class="input-group-field"
|
||||
autofocus="true"
|
||||
:class="{ error: $v.editedValue.$error }"
|
||||
@blur="$v.editedValue.$touch"
|
||||
@keyup.enter="onUpdate"
|
||||
/>
|
||||
<div class="input-group-button">
|
||||
<woot-button size="small" icon="ion-checkmark" @click="onUpdate" />
|
||||
</div>
|
||||
</div>
|
||||
<span v-if="shouldShowErrorMessage" class="error-message">
|
||||
{{ errorMessage }}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-show="!isEditing"
|
||||
class="value--view"
|
||||
:class="{ 'is-editable': showActions }"
|
||||
>
|
||||
<a
|
||||
v-if="isAttributeTypeLink"
|
||||
:href="value"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="value"
|
||||
>
|
||||
{{ value || '---' }}
|
||||
</a>
|
||||
<p v-else class="value">
|
||||
{{ formattedValue || '---' }}
|
||||
</p>
|
||||
<woot-button
|
||||
v-if="showActions"
|
||||
v-tooltip="$t('CUSTOM_ATTRIBUTES.ACTIONS.COPY')"
|
||||
variant="link"
|
||||
size="small"
|
||||
color-scheme="secondary"
|
||||
icon="ion-clipboard"
|
||||
class-names="edit-button"
|
||||
@click="onCopy"
|
||||
/>
|
||||
<woot-button
|
||||
v-if="showActions"
|
||||
v-tooltip="$t('CUSTOM_ATTRIBUTES.ACTIONS.EDIT')"
|
||||
variant="link"
|
||||
size="small"
|
||||
color-scheme="secondary"
|
||||
icon="ion-compose"
|
||||
class-names="edit-button"
|
||||
@click="onEdit"
|
||||
/>
|
||||
<woot-button
|
||||
v-if="showActions"
|
||||
v-tooltip="$t('CUSTOM_ATTRIBUTES.ACTIONS.DELETE')"
|
||||
variant="link"
|
||||
size="small"
|
||||
color-scheme="secondary"
|
||||
icon="ion-trash-a"
|
||||
class-names="edit-button"
|
||||
@click="onDelete"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import format from 'date-fns/format';
|
||||
import { required, url } from 'vuelidate/lib/validators';
|
||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||
|
||||
const DATE_FORMAT = 'yyyy-MM-dd';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
label: { type: String, required: true },
|
||||
value: { type: [String, Number], default: '' },
|
||||
showActions: { type: Boolean, default: false },
|
||||
attributeType: { type: String, default: 'text' },
|
||||
attributeKey: { type: String, required: true },
|
||||
contactId: { type: Number, default: null },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isEditing: false,
|
||||
editedValue:
|
||||
this.attributeType === 'date'
|
||||
? format(new Date(this.value || new Date()), DATE_FORMAT)
|
||||
: this.value,
|
||||
};
|
||||
},
|
||||
validations() {
|
||||
if (this.isAttributeTypeLink) {
|
||||
return {
|
||||
editedValue: { required, url },
|
||||
};
|
||||
}
|
||||
return {
|
||||
editedValue: { required },
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
isAttributeTypeLink() {
|
||||
return this.attributeType === 'link';
|
||||
},
|
||||
inputType() {
|
||||
return this.isAttributeTypeLink ? 'url' : this.attributeType;
|
||||
},
|
||||
shouldShowErrorMessage() {
|
||||
return this.$v.editedValue.$error;
|
||||
},
|
||||
errorMessage() {
|
||||
if (this.$v.editedValue.url) {
|
||||
return this.$t('CUSTOM_ATTRIBUTES.VALIDATIONS.INVALID_URL');
|
||||
}
|
||||
return this.$t('CUSTOM_ATTRIBUTES.VALIDATIONS.REQUIRED');
|
||||
},
|
||||
formattedValue() {
|
||||
if (this.attributeType === 'date') {
|
||||
return format(new Date(this.editedValue), 'dd-MM-yyyy');
|
||||
}
|
||||
return this.editedValue;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
bus.$on(BUS_EVENTS.FOCUS_CUSTOM_ATTRIBUTE, focusAttributeKey => {
|
||||
if (this.attributeKey === focusAttributeKey) {
|
||||
this.onEdit();
|
||||
}
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
focusInput() {
|
||||
if (this.$refs.inputfield) {
|
||||
this.$refs.inputfield.focus();
|
||||
}
|
||||
},
|
||||
onEdit() {
|
||||
this.isEditing = true;
|
||||
this.$nextTick(() => {
|
||||
this.focusInput();
|
||||
});
|
||||
},
|
||||
onUpdate() {
|
||||
const updatedValue =
|
||||
this.attributeType === 'date'
|
||||
? format(new Date(this.editedValue), DATE_FORMAT)
|
||||
: this.editedValue;
|
||||
|
||||
this.$v.$touch();
|
||||
if (this.$v.$invalid) {
|
||||
return;
|
||||
}
|
||||
this.isEditing = false;
|
||||
this.$emit('update', this.attributeKey, updatedValue);
|
||||
},
|
||||
onDelete() {
|
||||
this.isEditing = false;
|
||||
this.$emit('delete', this.attributeKey);
|
||||
},
|
||||
onCopy() {
|
||||
this.$emit('copy', this.value);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.custom-attribute {
|
||||
padding: var(--space-slab) var(--space-normal);
|
||||
}
|
||||
|
||||
.title-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: var(--space-mini);
|
||||
}
|
||||
.title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 0;
|
||||
}
|
||||
.attribute-name {
|
||||
&.error {
|
||||
color: var(--r-400);
|
||||
}
|
||||
}
|
||||
.title--icon {
|
||||
width: var(--space-two);
|
||||
}
|
||||
.edit-button {
|
||||
display: none;
|
||||
}
|
||||
.value--view {
|
||||
display: flex;
|
||||
|
||||
&.is-editable:hover {
|
||||
.value {
|
||||
background: var(--color-background);
|
||||
}
|
||||
.edit-button {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
.value {
|
||||
display: inline-block;
|
||||
min-width: var(--space-mega);
|
||||
border-radius: var(--border-radius-small);
|
||||
word-break: break-all;
|
||||
padding: var(--space-micro) var(--space-smaller);
|
||||
}
|
||||
.error-message {
|
||||
color: var(--r-400);
|
||||
display: block;
|
||||
font-size: 1.4rem;
|
||||
font-size: var(--font-size-small);
|
||||
font-weight: 400;
|
||||
margin-bottom: 1rem;
|
||||
margin-top: -1.6rem;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -54,7 +54,7 @@ export default {
|
||||
},
|
||||
mounted() {
|
||||
document.addEventListener('keydown', e => {
|
||||
if (this.show && e.keyCode === 27) {
|
||||
if (this.show && e.code === 'Escape') {
|
||||
this.onClose();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
<template>
|
||||
<transition name="network-notification-fade" tag="div">
|
||||
<div v-show="showNotification" class="ui-notification-container">
|
||||
<div class="ui-notification">
|
||||
<svg
|
||||
class="ui-notification-icon"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M18.364 5.636a9 9 0 010 12.728m0 0l-2.829-2.829m2.829 2.829L21 21M15.536 8.464a5 5 0 010 7.072m0 0l-2.829-2.829m-4.243 2.829a4.978 4.978 0 01-1.414-2.83m-1.414 5.658a9 9 0 01-2.167-9.238m7.824 2.167a1 1 0 111.414 1.414m-1.414-1.414L3 3m8.293 8.293l1.414 1.414"
|
||||
/>
|
||||
</svg>
|
||||
<p class="ui-notification-text">
|
||||
{{ $t('NETWORK.NOTIFICATION.TEXT') }}
|
||||
</p>
|
||||
<button class="ui-refresh-button" @click="refreshPage">
|
||||
{{ $t('NETWORK.BUTTON.REFRESH') }}
|
||||
</button>
|
||||
<button class="ui-close-button" @click="closeNotification">
|
||||
<i class="ui-close-icon icon ion-ios-close-outline" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
showNotification: !navigator.onLine,
|
||||
};
|
||||
},
|
||||
|
||||
mounted() {
|
||||
window.addEventListener('offline', this.updateOnlineStatus);
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('offline', this.updateOnlineStatus);
|
||||
},
|
||||
|
||||
methods: {
|
||||
refreshPage() {
|
||||
window.location.reload();
|
||||
},
|
||||
|
||||
closeNotification() {
|
||||
this.showNotification = false;
|
||||
},
|
||||
|
||||
updateOnlineStatus(event) {
|
||||
if (event.type === 'offline') {
|
||||
this.showNotification = true;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '~dashboard/assets/scss/mixins';
|
||||
|
||||
.ui-notification-container {
|
||||
max-width: 40rem;
|
||||
position: absolute;
|
||||
right: var(--space-normal);
|
||||
top: var(--space-normal);
|
||||
width: 100%;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.ui-notification {
|
||||
@include shadow;
|
||||
align-items: center;
|
||||
background-color: var(--white);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--space-one);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
max-width: 40rem;
|
||||
min-height: 3rem;
|
||||
min-width: 24rem;
|
||||
padding: var(--space-normal) var(--space-two);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.ui-notification-text {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.ui-refresh-button {
|
||||
color: var(--color-woot);
|
||||
font-size: var(--font-size-small);
|
||||
font-weight: bold;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.ui-notification-icon {
|
||||
color: var(--b-600);
|
||||
width: var(--font-size-mega);
|
||||
}
|
||||
|
||||
.ui-close-icon {
|
||||
color: var(--b-600);
|
||||
font-size: var(--font-size-large);
|
||||
}
|
||||
|
||||
.ui-close-button {
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -8,7 +8,7 @@
|
||||
icon="ion-checkmark"
|
||||
emoji="✅"
|
||||
:is-loading="isLoading"
|
||||
@click="() => toggleStatus(STATUS_TYPE.RESOLVED)"
|
||||
@click="onCmdResolveConversation"
|
||||
>
|
||||
{{ this.$t('CONVERSATION.HEADER.RESOLVE_ACTION') }}
|
||||
</woot-button>
|
||||
@@ -19,7 +19,7 @@
|
||||
icon="ion-refresh"
|
||||
emoji="👀"
|
||||
:is-loading="isLoading"
|
||||
@click="() => toggleStatus(STATUS_TYPE.OPEN)"
|
||||
@click="onCmdOpenConversation"
|
||||
>
|
||||
{{ this.$t('CONVERSATION.HEADER.REOPEN_ACTION') }}
|
||||
</woot-button>
|
||||
@@ -29,7 +29,7 @@
|
||||
color-scheme="primary"
|
||||
icon="ion-person"
|
||||
:is-loading="isLoading"
|
||||
@click="() => toggleStatus(STATUS_TYPE.OPEN)"
|
||||
@click="onCmdOpenConversation"
|
||||
>
|
||||
{{ this.$t('CONVERSATION.HEADER.OPEN_ACTION') }}
|
||||
</woot-button>
|
||||
@@ -118,6 +118,11 @@ import {
|
||||
startOfTomorrow,
|
||||
startOfWeek,
|
||||
} from 'date-fns';
|
||||
import {
|
||||
CMD_REOPEN_CONVERSATION,
|
||||
CMD_RESOLVE_CONVERSATION,
|
||||
CMD_SNOOZE_CONVERSATION,
|
||||
} from '../../routes/dashboard/commands/commandBarBusEvents';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -135,9 +140,7 @@ export default {
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
currentChat: 'getSelectedChat',
|
||||
}),
|
||||
...mapGetters({ currentChat: 'getSelectedChat' }),
|
||||
isOpen() {
|
||||
return this.currentChat.status === wootConstants.STATUS_TYPE.OPEN;
|
||||
},
|
||||
@@ -170,13 +173,25 @@ export default {
|
||||
};
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
bus.$on(CMD_SNOOZE_CONVERSATION, this.onCmdSnoozeConversation);
|
||||
bus.$on(CMD_REOPEN_CONVERSATION, this.onCmdOpenConversation);
|
||||
bus.$on(CMD_RESOLVE_CONVERSATION, this.onCmdResolveConversation);
|
||||
},
|
||||
destroyed() {
|
||||
bus.$off(CMD_SNOOZE_CONVERSATION, this.onCmdSnoozeConversation);
|
||||
bus.$off(CMD_REOPEN_CONVERSATION, this.onCmdOpenConversation);
|
||||
bus.$off(CMD_RESOLVE_CONVERSATION, this.onCmdResolveConversation);
|
||||
},
|
||||
methods: {
|
||||
async handleKeyEvents(e) {
|
||||
const allConversations = document.querySelectorAll(
|
||||
'.conversations-list .conversation'
|
||||
);
|
||||
if (hasPressedAltAndMKey(e)) {
|
||||
this.$refs.arrowDownButton.$el.click();
|
||||
if (this.$refs.arrowDownButton) {
|
||||
this.$refs.arrowDownButton.$el.click();
|
||||
}
|
||||
}
|
||||
if (hasPressedAltAndEKey(e)) {
|
||||
const activeConversation = document.querySelector(
|
||||
@@ -198,9 +213,22 @@ export default {
|
||||
allConversations[0].click();
|
||||
document.querySelector('.conversations-list').scrollTop = 0;
|
||||
}
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
},
|
||||
onCmdSnoozeConversation(snoozeType) {
|
||||
this.toggleStatus(
|
||||
this.STATUS_TYPE.SNOOZED,
|
||||
this.snoozeTimes[snoozeType] || null
|
||||
);
|
||||
},
|
||||
onCmdOpenConversation() {
|
||||
this.toggleStatus(this.STATUS_TYPE.OPEN);
|
||||
},
|
||||
onCmdResolveConversation() {
|
||||
this.toggleStatus(this.STATUS_TYPE.RESOLVED);
|
||||
},
|
||||
showOpenButton() {
|
||||
return this.isResolved || this.isSnoozed;
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="status">
|
||||
<div class="status-view">
|
||||
<availability-status-badge :status="currentUserAvailabilityStatus" />
|
||||
<availability-status-badge :status="currentUserAvailability" />
|
||||
<div class="status-view--title">
|
||||
{{ availabilityDisplayLabel }}
|
||||
</div>
|
||||
@@ -26,7 +26,9 @@
|
||||
color-scheme="secondary"
|
||||
class-names="status-change--dropdown-button"
|
||||
:is-disabled="status.disabled"
|
||||
@click="changeAvailabilityStatus(status.value)"
|
||||
@click="
|
||||
changeAvailabilityStatus(status.value, currentAccountId)
|
||||
"
|
||||
>
|
||||
<availability-status-badge :status="status.value" />
|
||||
{{ status.label }}
|
||||
@@ -75,18 +77,22 @@ export default {
|
||||
|
||||
computed: {
|
||||
...mapGetters({
|
||||
currentUser: 'getCurrentUser',
|
||||
getCurrentUserAvailability: 'getCurrentUserAvailability',
|
||||
getCurrentAccountId: 'getCurrentAccountId',
|
||||
}),
|
||||
availabilityDisplayLabel() {
|
||||
const availabilityIndex = AVAILABILITY_STATUS_KEYS.findIndex(
|
||||
key => key === this.currentUserAvailabilityStatus
|
||||
key => key === this.currentUserAvailability
|
||||
);
|
||||
return this.$t('PROFILE_SETTINGS.FORM.AVAILABILITY.STATUSES_LIST')[
|
||||
availabilityIndex
|
||||
];
|
||||
},
|
||||
currentUserAvailabilityStatus() {
|
||||
return this.currentUser.availability_status;
|
||||
currentAccountId() {
|
||||
return this.getCurrentAccountId;
|
||||
},
|
||||
currentUserAvailability() {
|
||||
return this.getCurrentUserAvailability;
|
||||
},
|
||||
availabilityStatuses() {
|
||||
return this.$t('PROFILE_SETTINGS.FORM.AVAILABILITY.STATUSES_LIST').map(
|
||||
@@ -94,7 +100,7 @@ export default {
|
||||
label: statusLabel,
|
||||
value: AVAILABILITY_STATUS_KEYS[index],
|
||||
disabled:
|
||||
this.currentUserAvailabilityStatus ===
|
||||
this.currentUserAvailability ===
|
||||
AVAILABILITY_STATUS_KEYS[index],
|
||||
})
|
||||
);
|
||||
@@ -108,16 +114,16 @@ export default {
|
||||
closeStatusMenu() {
|
||||
this.isStatusMenuOpened = false;
|
||||
},
|
||||
changeAvailabilityStatus(availability) {
|
||||
changeAvailabilityStatus(availability, accountId) {
|
||||
if (this.isUpdating) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isUpdating = true;
|
||||
|
||||
this.$store
|
||||
.dispatch('updateAvailability', {
|
||||
availability,
|
||||
availability: availability,
|
||||
account_id: accountId,
|
||||
})
|
||||
.finally(() => {
|
||||
this.isUpdating = false;
|
||||
|
||||
@@ -94,8 +94,16 @@ 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 {
|
||||
hasPressedAltAndCKey,
|
||||
hasPressedAltAndRKey,
|
||||
hasPressedAltAndSKey,
|
||||
hasPressedAltAndVKey,
|
||||
hasPressedCommandAndForwardSlash,
|
||||
isEscape,
|
||||
} from 'shared/helpers/KeyboardHelpers';
|
||||
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
||||
import router from '../../routes';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -252,19 +260,12 @@ export default {
|
||||
return frontendURL(`accounts/${this.accountId}/dashboard`);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
currentUser(newUserInfo, oldUserInfo) {
|
||||
if (!oldUserInfo.email && newUserInfo.email) {
|
||||
this.setChatwootUser();
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.$store.dispatch('labels/get');
|
||||
this.$store.dispatch('inboxes/get');
|
||||
this.$store.dispatch('notifications/unReadCount');
|
||||
this.$store.dispatch('teams/get');
|
||||
this.setChatwootUser();
|
||||
this.$store.dispatch('attributes/get');
|
||||
},
|
||||
|
||||
methods: {
|
||||
@@ -278,21 +279,34 @@ export default {
|
||||
if (hasPressedCommandAndForwardSlash(e)) {
|
||||
this.toggleKeyShortcutModal();
|
||||
}
|
||||
if (isEscape(e)) {
|
||||
this.closeKeyShortcutModal();
|
||||
}
|
||||
|
||||
if (hasPressedAltAndCKey(e)) {
|
||||
if (!this.isCurrentRouteSameAsNavigation('home')) {
|
||||
router.push({ name: 'home' });
|
||||
}
|
||||
} else if (hasPressedAltAndVKey(e)) {
|
||||
if (!this.isCurrentRouteSameAsNavigation('contacts_dashboard')) {
|
||||
router.push({ name: 'contacts_dashboard' });
|
||||
}
|
||||
} else if (hasPressedAltAndRKey(e)) {
|
||||
if (!this.isCurrentRouteSameAsNavigation('settings_account_reports')) {
|
||||
router.push({ name: 'settings_account_reports' });
|
||||
}
|
||||
} else if (hasPressedAltAndSKey(e)) {
|
||||
if (!this.isCurrentRouteSameAsNavigation('agent_list')) {
|
||||
router.push({ name: 'agent_list' });
|
||||
}
|
||||
}
|
||||
},
|
||||
isCurrentRouteSameAsNavigation(routeName) {
|
||||
return router.currentRoute && router.currentRoute.name === routeName;
|
||||
},
|
||||
toggleSupportChatWindow() {
|
||||
window.$chatwoot.toggle();
|
||||
},
|
||||
setChatwootUser() {
|
||||
if (!this.currentUser.email || !this.globalConfig.chatwootInboxToken) {
|
||||
return;
|
||||
}
|
||||
window.$chatwoot.setUser(this.currentUser.email, {
|
||||
name: this.currentUser.name,
|
||||
email: this.currentUser.email,
|
||||
avatar_url: this.currentUser.avatar_url,
|
||||
identifier_hash: this.currentUser.hmac_identifier,
|
||||
});
|
||||
},
|
||||
filterMenuItemsByRole(menuItems) {
|
||||
if (!this.currentRole) {
|
||||
return [];
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
<a href="#" :class="computedChildClass(child)">
|
||||
<div class="wrap">
|
||||
<i
|
||||
v-if="computedInboxClass(child)"
|
||||
v-if="menuItem.key === 'inbox'"
|
||||
class="inbox-icon"
|
||||
:class="computedInboxClass(child)"
|
||||
/>
|
||||
@@ -59,17 +59,10 @@
|
||||
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, eventListenerMixins],
|
||||
mixins: [adminMixin],
|
||||
props: {
|
||||
menuItem: {
|
||||
type: Object,
|
||||
@@ -124,20 +117,6 @@ 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;
|
||||
},
|
||||
|
||||
@@ -94,10 +94,6 @@ export default {
|
||||
methods: {
|
||||
logout() {
|
||||
Auth.logout();
|
||||
|
||||
if (this.globalConfig.chatwootInboxToken) {
|
||||
window.$chatwoot.reset();
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -17,7 +17,8 @@ const i18nConfig = new VueI18n({
|
||||
});
|
||||
|
||||
describe('AvailabilityStatus', () => {
|
||||
const currentUser = { availability_status: 'online' };
|
||||
const currentAvailability = 'online';
|
||||
const currentAccountId = '1';
|
||||
let store = null;
|
||||
let actions = null;
|
||||
let modules = null;
|
||||
@@ -33,7 +34,8 @@ describe('AvailabilityStatus', () => {
|
||||
modules = {
|
||||
auth: {
|
||||
getters: {
|
||||
getCurrentUser: () => currentUser,
|
||||
getCurrentUserAvailability: () => currentAvailability,
|
||||
getCurrentAccountId: () => currentAccountId,
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -77,7 +79,7 @@ describe('AvailabilityStatus', () => {
|
||||
|
||||
expect(actions.updateAvailability).toBeCalledWith(
|
||||
expect.any(Object),
|
||||
{ availability: 'offline' },
|
||||
{ availability: 'offline', account_id: currentAccountId },
|
||||
undefined
|
||||
);
|
||||
});
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user