Compare commits
173
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd016c5ad4 | ||
|
|
dcd18072b4 | ||
|
|
4244945e7b | ||
|
|
17725e4dd0 | ||
|
|
d55bb63aa0 | ||
|
|
bda2d1d9a4 | ||
|
|
6e22695b85 | ||
|
|
146e46d79f | ||
|
|
0af27a2387 | ||
|
|
7380f0e7ce | ||
|
|
2c5a0abcce | ||
|
|
f23e33c1a9 | ||
|
|
5588d6e344 | ||
|
|
59ace66c6f | ||
|
|
decef1ad52 | ||
|
|
0e19a4196b | ||
|
|
04dd65687b | ||
|
|
5f503b1a57 | ||
|
|
5f6e974531 | ||
|
|
a023eabf59 | ||
|
|
cb1e25a487 | ||
|
|
bcf7c6c45a | ||
|
|
b411b6ca21 | ||
|
|
cd217a3f86 | ||
|
|
6c79642450 | ||
|
|
201d476720 | ||
|
|
c1a2e67c9e | ||
|
|
1948bef9ac | ||
|
|
5486f39f7e | ||
|
|
f018b0013c | ||
|
|
950f085e80 | ||
|
|
268e26625b | ||
|
|
7041d86e4d | ||
|
|
78398ee800 | ||
|
|
7b09b02737 | ||
|
|
af7631d9f1 | ||
|
|
e0f33e62af | ||
|
|
5d224f2e04 | ||
|
|
4cb981d81b | ||
|
|
f891cfbf47 | ||
|
|
e54c8a857d | ||
|
|
66229b0d6b | ||
|
|
3eb1d49577 | ||
|
|
677888bcde | ||
|
|
abbb4180ea | ||
|
|
d1cbc56d67 | ||
|
|
b579ff832e | ||
|
|
39d0748a5b | ||
|
|
b4d20689b7 | ||
|
|
3b84b0fc47 | ||
|
|
23ea829510 | ||
|
|
ebe9daea00 | ||
|
|
36b6c0cb9c | ||
|
|
16c36a78f0 | ||
|
|
4999f7ed23 | ||
|
|
e8f4ac632f | ||
|
|
3c7854ccdf | ||
|
|
1e70223ed7 | ||
|
|
f34cd3ea61 | ||
|
|
68b8d721a7 | ||
|
|
d876419b27 | ||
|
|
8455186e9f | ||
|
|
b6584ec68b | ||
|
|
29f18c7f18 | ||
|
|
11b27f9805 | ||
|
|
653e0335c0 | ||
|
|
013dab7da6 | ||
|
|
2ba81830f3 | ||
|
|
dfba4770bd | ||
|
|
63e702ca12 | ||
|
|
394fece846 | ||
|
|
24fbab94c3 | ||
|
|
61e03fa33a | ||
|
|
6c4b92f7f6 | ||
|
|
f023325b0e | ||
|
|
dd05482cb0 | ||
|
|
d94108bf3f | ||
|
|
ab872beb1d | ||
|
|
d02cfff4cc | ||
|
|
736f269c49 | ||
|
|
b067acfd4a | ||
|
|
232fa6fd18 | ||
|
|
483d55df91 | ||
|
|
3e54d3654b | ||
|
|
b6831d464e | ||
|
|
8c67ea56e7 | ||
|
|
54bc4c23df | ||
|
|
58f47eb02c | ||
|
|
4fc62ed9ae | ||
|
|
4a89bab23a | ||
|
|
7438f3b157 | ||
|
|
9551dbc25f | ||
|
|
2e54f4a67c | ||
|
|
6d9b2a6b33 | ||
|
|
35a9acf099 | ||
|
|
b9694a0818 | ||
|
|
7416bbb25e | ||
|
|
b94c89ebf1 | ||
|
|
78ce8a4652 | ||
|
|
04c874fe35 | ||
|
|
eabe548520 | ||
|
|
83231b9678 | ||
|
|
f96b54e57f | ||
|
|
d3bf828524 | ||
|
|
c20415f3ba | ||
|
|
6b2ed4fe79 | ||
|
|
eca9ca90f1 | ||
|
|
9018ec9678 | ||
|
|
e55fe33f1b | ||
|
|
3b0788c3be | ||
|
|
626e67b859 | ||
|
|
28db18ebbd | ||
|
|
b28721e10b | ||
|
|
f77db4d814 | ||
|
|
62d8ec7edb | ||
|
|
7b09fa4a03 | ||
|
|
980013abae | ||
|
|
5c729ccea5 | ||
|
|
ae52ca87aa | ||
|
|
415bb23c37 | ||
|
|
9e173fca3c | ||
|
|
bd918ee506 | ||
|
|
7c9884b853 | ||
|
|
7ffa669c5c | ||
|
|
0bc20873f6 | ||
|
|
88de3359a5 | ||
|
|
a3d008da06 | ||
|
|
cbae95422d | ||
|
|
081c845c56 | ||
|
|
1e54d6deb3 | ||
|
|
bb5c6a416a | ||
|
|
6a07251007 | ||
|
|
759a66dd21 | ||
|
|
8189dd932c | ||
|
|
b6ba0f343e | ||
|
|
86ca90aa15 | ||
|
|
26e8877cd9 | ||
|
|
1b63adfb2e | ||
|
|
e27274a5a8 | ||
|
|
e5c198f839 | ||
|
|
72ba429159 | ||
|
|
3227cf7ff3 | ||
|
|
f0740120a8 | ||
|
|
3ea54065b1 | ||
|
|
6f19546c3c | ||
|
|
5c9ab21617 | ||
|
|
b71a580573 | ||
|
|
6a73953003 | ||
|
|
336af1ac9a | ||
|
|
e4de366b8d | ||
|
|
373c3dcd6c | ||
|
|
4b12dd89b0 | ||
|
|
c19cfeaa81 | ||
|
|
fa118ad18c | ||
|
|
249bfe44bb | ||
|
|
826d9ec5a7 | ||
|
|
d8b53f5d2f | ||
|
|
08ced452e1 | ||
|
|
24fe3805d8 | ||
|
|
845b0c0a68 | ||
|
|
8ce7539c5e | ||
|
|
5ee980465f | ||
|
|
fd633e1613 | ||
|
|
12a64f1b10 | ||
|
|
ffc2d98fb8 | ||
|
|
f7f04f788e | ||
|
|
a88d155dd7 | ||
|
|
e8b7e791a5 | ||
|
|
b18cac77fb | ||
|
|
647161121e | ||
|
|
cbbe939662 | ||
|
|
30cb141e5e | ||
|
|
b0d5c438ac |
+21
-2
@@ -81,7 +81,7 @@ SMTP_OPENSSL_VERIFY_MODE=peer
|
||||
# Mail Incoming
|
||||
# This is the domain set for the reply emails when conversation continuity is enabled
|
||||
MAILER_INBOUND_EMAIL_DOMAIN=
|
||||
# Set this to appropriate ingress channel with regards to incoming emails
|
||||
# Set this to the appropriate ingress channel with regards to incoming emails
|
||||
# Possible values are :
|
||||
# relay for Exim, Postfix, Qmail
|
||||
# mailgun for Mailgun
|
||||
@@ -91,10 +91,20 @@ MAILER_INBOUND_EMAIL_DOMAIN=
|
||||
RAILS_INBOUND_EMAIL_SERVICE=
|
||||
# Use one of the following based on the email ingress service
|
||||
# Ref: https://edgeguides.rubyonrails.org/action_mailbox_basics.html
|
||||
# Set this to a password of your choice and use it in the Inbound webhook
|
||||
RAILS_INBOUND_EMAIL_PASSWORD=
|
||||
|
||||
MAILGUN_INGRESS_SIGNING_KEY=
|
||||
MANDRILL_INGRESS_API_KEY=
|
||||
|
||||
# Creating Your Inbound Webhook Instructions for Postmark and Sendgrid:
|
||||
# Inbound webhook URL format:
|
||||
# https://actionmailbox:[YOUR_RAILS_INBOUND_EMAIL_PASSWORD]@[YOUR_CHATWOOT_DOMAIN.COM]/rails/action_mailbox/[RAILS_INBOUND_EMAIL_SERVICE]/inbound_emails
|
||||
# Note: Replace the values inside the brackets; do not include the brackets themselves.
|
||||
# Example: https://actionmailbox:mYRandomPassword3@chatwoot.example.com/rails/action_mailbox/postmark/inbound_emails
|
||||
# For Postmark
|
||||
# Ensure the 'Include raw email content in JSON payload' checkbox is selected in the inbound webhook section.
|
||||
|
||||
# Storage
|
||||
ACTIVE_STORAGE_SERVICE=local
|
||||
|
||||
@@ -176,6 +186,9 @@ ANDROID_SHA256_CERT_FINGERPRINT=AC:73:8E:DE:EB:56:EA:CC:10:87:02:A7:65:37:7B:38:
|
||||
# MICROSOFT CLARITY
|
||||
# MS_CLARITY_TOKEN=xxxxxxxxx
|
||||
|
||||
# GOOGLE_TAG_MANAGER
|
||||
# GOOGLE_TAG = GTM-XXXXXXX
|
||||
|
||||
## Scout
|
||||
## https://scoutapm.com/docs/ruby/configuration
|
||||
# SCOUT_KEY=YOURKEY
|
||||
@@ -199,7 +212,7 @@ ANDROID_SHA256_CERT_FINGERPRINT=AC:73:8E:DE:EB:56:EA:CC:10:87:02:A7:65:37:7B:38:
|
||||
## Rack Attack configuration
|
||||
## To prevent and throttle abusive requests
|
||||
# ENABLE_RACK_ATTACK=true
|
||||
# RACK_ATTACK_IP_LIMIT=3000
|
||||
# RACK_ATTACK_LIMIT=300
|
||||
# ENABLE_RACK_ATTACK_WIDGET_API=true
|
||||
|
||||
## Running chatwoot as an API only server
|
||||
@@ -240,3 +253,9 @@ AZURE_APP_SECRET=
|
||||
|
||||
# Sentiment analysis model file path
|
||||
SENTIMENT_FILE_PATH=
|
||||
|
||||
|
||||
# Housekeeping/Performance related configurations
|
||||
# Set to true if you want to remove stale contact inboxes
|
||||
# contact_inboxes with no conversation older than 90 days will be removed
|
||||
# REMOVE_STALE_CONTACT_INBOX_JOB_STATUS=false
|
||||
|
||||
+8
-3
@@ -4,9 +4,10 @@ module.exports = {
|
||||
'prettier',
|
||||
'plugin:vue/recommended',
|
||||
'plugin:storybook/recommended',
|
||||
'plugin:cypress/recommended',
|
||||
],
|
||||
parserOptions: {
|
||||
parser: 'babel-eslint',
|
||||
parser: '@babel/eslint-parser',
|
||||
ecmaVersion: 2020,
|
||||
sourceType: 'module',
|
||||
},
|
||||
@@ -24,13 +25,16 @@ module.exports = {
|
||||
'jsx-a11y/label-has-for': 'off',
|
||||
'jsx-a11y/anchor-is-valid': 'off',
|
||||
'import/no-unresolved': 'off',
|
||||
'vue/html-indent': 'off',
|
||||
'vue/multi-word-component-names': 'off',
|
||||
'vue/max-attributes-per-line': [
|
||||
'error',
|
||||
{
|
||||
singleline: 20,
|
||||
singleline: {
|
||||
max: 20,
|
||||
},
|
||||
multiline: {
|
||||
max: 1,
|
||||
allowFirstLine: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -47,6 +51,7 @@ module.exports = {
|
||||
},
|
||||
],
|
||||
'vue/no-v-html': 'off',
|
||||
'vue/component-definition-name-casing': 'off',
|
||||
'vue/singleline-html-element-content-newline': 'off',
|
||||
'import/extensions': ['off'],
|
||||
'no-console': 'error',
|
||||
|
||||
@@ -69,7 +69,12 @@ jobs:
|
||||
# Run Response Bot specs
|
||||
- name: Run backend tests
|
||||
run: |
|
||||
bundle exec rspec spec/enterprise/controllers/api/v1/accounts/response_sources_controller_spec.rb spec/enterprise/controllers/enterprise/api/v1/accounts/inboxes_controller_spec.rb:47 --profile=10 --format documentation
|
||||
bundle exec rspec \
|
||||
spec/enterprise/controllers/api/v1/accounts/response_sources_controller_spec.rb \
|
||||
spec/enterprise/services/enterprise/message_templates/response_bot_service_spec.rb \
|
||||
spec/enterprise/controllers/enterprise/api/v1/accounts/inboxes_controller_spec.rb:47 \
|
||||
--profile=10 \
|
||||
--format documentation
|
||||
|
||||
- name: Upload rails log folder
|
||||
uses: actions/upload-artifact@v3
|
||||
|
||||
@@ -58,6 +58,7 @@ Metrics/BlockLength:
|
||||
Metrics/ModuleLength:
|
||||
Exclude:
|
||||
- lib/seeders/message_seeder.rb
|
||||
- spec/support/slack_stubs.rb
|
||||
Rails/ApplicationController:
|
||||
Exclude:
|
||||
- 'app/controllers/api/v1/widget/messages_controller.rb'
|
||||
|
||||
@@ -15,7 +15,7 @@ gem 'browser'
|
||||
gem 'hashie'
|
||||
gem 'jbuilder'
|
||||
gem 'kaminari'
|
||||
gem 'responders'
|
||||
gem 'responders', '>= 3.1.1'
|
||||
gem 'rest-client'
|
||||
gem 'telephone_number'
|
||||
gem 'time_diff'
|
||||
@@ -67,7 +67,7 @@ gem 'webpacker'
|
||||
gem 'barnes'
|
||||
|
||||
##--- gems for authentication & authorization ---##
|
||||
gem 'devise'
|
||||
gem 'devise', '>= 4.9.3'
|
||||
gem 'devise-secure_password', git: 'https://github.com/chatwoot/devise-secure_password', branch: 'chatwoot'
|
||||
gem 'devise_token_auth'
|
||||
# authorization
|
||||
@@ -92,7 +92,7 @@ gem 'twitty', '~> 0.1.5'
|
||||
# facebook client
|
||||
gem 'koala'
|
||||
# slack client
|
||||
gem 'slack-ruby-client', '~> 2.0.0'
|
||||
gem 'slack-ruby-client', '~> 2.2.0'
|
||||
# for dialogflow integrations
|
||||
gem 'google-cloud-dialogflow-v2'
|
||||
gem 'grpc'
|
||||
@@ -107,16 +107,16 @@ gem 'google-cloud-translate-v3'
|
||||
gem 'ddtrace', require: false
|
||||
gem 'elastic-apm', require: false
|
||||
gem 'newrelic_rpm', require: false
|
||||
gem 'newrelic-sidekiq-metrics', require: false
|
||||
gem 'newrelic-sidekiq-metrics', '>= 1.6.2', require: false
|
||||
gem 'scout_apm', require: false
|
||||
gem 'sentry-rails', '>= 5.11.0', require: false
|
||||
gem 'sentry-rails', '>= 5.13.0', require: false
|
||||
gem 'sentry-ruby', require: false
|
||||
gem 'sentry-sidekiq', '>= 5.11.0', require: false
|
||||
gem 'sentry-sidekiq', '>= 5.13.0', require: false
|
||||
|
||||
##-- background job processing --##
|
||||
gem 'sidekiq', '>= 7.1.3'
|
||||
# We want cron jobs
|
||||
gem 'sidekiq-cron', '>= 1.10.1'
|
||||
gem 'sidekiq-cron', '>= 1.11.0'
|
||||
|
||||
##-- Push notification service --##
|
||||
gem 'fcm'
|
||||
@@ -154,12 +154,12 @@ gem 'stripe'
|
||||
gem 'faker'
|
||||
|
||||
# Include logrange conditionally in intializer using env variable
|
||||
gem 'lograge', '~> 0.13.0', require: false
|
||||
gem 'lograge', '~> 0.14.0', require: false
|
||||
|
||||
# worked with microsoft refresh token
|
||||
gem 'omniauth-oauth2'
|
||||
|
||||
gem 'audited', '~> 5.3'
|
||||
gem 'audited', '~> 5.4', '>= 5.4.1'
|
||||
|
||||
# need for google auth
|
||||
gem 'omniauth'
|
||||
@@ -200,6 +200,8 @@ group :development do
|
||||
# profiling
|
||||
gem 'rack-mini-profiler', '>= 3.1.1', require: false
|
||||
gem 'stackprof'
|
||||
# Should install the associated chrome extension to view query logs
|
||||
gem 'meta_request'
|
||||
end
|
||||
|
||||
group :test do
|
||||
|
||||
+45
-38
@@ -126,9 +126,9 @@ GEM
|
||||
rake (>= 10.4, < 14.0)
|
||||
ast (2.4.2)
|
||||
attr_extras (7.1.0)
|
||||
audited (5.3.3)
|
||||
activerecord (>= 5.0, < 7.1)
|
||||
request_store (~> 1.2)
|
||||
audited (5.4.1)
|
||||
activerecord (>= 5.0, < 7.7)
|
||||
activesupport (>= 5.0, < 7.7)
|
||||
aws-eventstream (1.2.0)
|
||||
aws-partitions (1.760.0)
|
||||
aws-sdk-core (3.171.1)
|
||||
@@ -148,7 +148,8 @@ GEM
|
||||
barnes (0.0.9)
|
||||
multi_json (~> 1)
|
||||
statsd-ruby (~> 1.1)
|
||||
bcrypt (3.1.18)
|
||||
base64 (0.1.1)
|
||||
bcrypt (3.1.19)
|
||||
bindex (0.8.1)
|
||||
blingfire (0.1.8)
|
||||
bootsnap (1.16.0)
|
||||
@@ -174,7 +175,7 @@ GEM
|
||||
rexml
|
||||
crass (1.0.6)
|
||||
csv-safe (3.2.1)
|
||||
cypress-on-rails (1.13.1)
|
||||
cypress-on-rails (1.16.0)
|
||||
rack
|
||||
database_cleaner (2.0.2)
|
||||
database_cleaner-active_record (>= 2, < 3)
|
||||
@@ -193,7 +194,7 @@ GEM
|
||||
irb (>= 1.5.0)
|
||||
reline (>= 0.3.1)
|
||||
declarative (0.0.20)
|
||||
devise (4.9.2)
|
||||
devise (4.9.3)
|
||||
bcrypt (~> 3.0)
|
||||
orm_adapter (~> 0.1)
|
||||
railties (>= 4.1.0)
|
||||
@@ -261,7 +262,7 @@ GEM
|
||||
rake
|
||||
flag_shih_tzu (0.3.23)
|
||||
foreman (0.87.2)
|
||||
fugit (1.8.1)
|
||||
fugit (1.9.0)
|
||||
et-orbi (~> 1, >= 1.2.7)
|
||||
raabro (~> 1.4)
|
||||
gapic-common (0.18.0)
|
||||
@@ -273,7 +274,7 @@ GEM
|
||||
googleauth (~> 1.0)
|
||||
grpc (~> 1.36)
|
||||
geocoder (1.8.1)
|
||||
gli (2.21.0)
|
||||
gli (2.21.1)
|
||||
globalid (1.2.1)
|
||||
activesupport (>= 6.1)
|
||||
gmail_xoauth (0.4.2)
|
||||
@@ -434,12 +435,12 @@ GEM
|
||||
llhttp-ffi (0.4.0)
|
||||
ffi-compiler (~> 1.0)
|
||||
rake (~> 13.0)
|
||||
lograge (0.13.0)
|
||||
lograge (0.14.0)
|
||||
actionpack (>= 4)
|
||||
activesupport (>= 4)
|
||||
railties (>= 4)
|
||||
request_store (~> 1.0)
|
||||
loofah (2.21.3)
|
||||
loofah (2.21.4)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.12.0)
|
||||
mail (2.8.1)
|
||||
@@ -450,13 +451,16 @@ GEM
|
||||
marcel (1.0.2)
|
||||
maxminddb (0.1.22)
|
||||
memoist (0.16.2)
|
||||
meta_request (0.7.4)
|
||||
rack-contrib (>= 1.1, < 3)
|
||||
railties (>= 3.0.0, < 7.1)
|
||||
method_source (1.0.0)
|
||||
mime-types (3.4.1)
|
||||
mime-types-data (~> 3.2015)
|
||||
mime-types-data (3.2023.0218.1)
|
||||
mini_magick (4.12.0)
|
||||
mini_mime (1.1.5)
|
||||
mini_portile2 (2.8.4)
|
||||
mini_portile2 (2.8.5)
|
||||
minitest (5.20.0)
|
||||
mock_redis (0.36.0)
|
||||
ruby2_keywords
|
||||
@@ -478,10 +482,11 @@ GEM
|
||||
net-smtp (0.3.3)
|
||||
net-protocol
|
||||
netrc (0.11.0)
|
||||
newrelic-sidekiq-metrics (1.6.1)
|
||||
newrelic_rpm (~> 8)
|
||||
newrelic-sidekiq-metrics (1.6.2)
|
||||
newrelic_rpm (>= 8.0.0)
|
||||
sidekiq
|
||||
newrelic_rpm (8.16.0)
|
||||
newrelic_rpm (9.6.0)
|
||||
base64
|
||||
nio4r (2.5.9)
|
||||
nokogiri (1.15.4)
|
||||
mini_portile2 (~> 2.8.2)
|
||||
@@ -553,10 +558,12 @@ GEM
|
||||
pundit (2.3.0)
|
||||
activesupport (>= 3.0.0)
|
||||
raabro (1.4.0)
|
||||
racc (1.7.1)
|
||||
racc (1.7.3)
|
||||
rack (2.2.8)
|
||||
rack-attack (6.7.0)
|
||||
rack (>= 1.0, < 4)
|
||||
rack-contrib (2.4.0)
|
||||
rack (< 4)
|
||||
rack-cors (2.0.1)
|
||||
rack (>= 2.0.0)
|
||||
rack-mini-profiler (3.1.1)
|
||||
@@ -597,13 +604,13 @@ GEM
|
||||
thor (~> 1.0)
|
||||
zeitwerk (~> 2.5)
|
||||
rainbow (3.1.1)
|
||||
rake (13.0.6)
|
||||
rake (13.1.0)
|
||||
rb-fsevent (0.11.2)
|
||||
rb-inotify (0.10.1)
|
||||
ffi (~> 1.0)
|
||||
redis (5.0.6)
|
||||
redis-client (>= 0.9.0)
|
||||
redis-client (0.17.0)
|
||||
redis-client (0.18.0)
|
||||
connection_pool
|
||||
redis-namespace (1.10.0)
|
||||
redis (>= 4)
|
||||
@@ -616,7 +623,7 @@ GEM
|
||||
uber (< 0.2.0)
|
||||
request_store (1.5.1)
|
||||
rack (>= 1.4)
|
||||
responders (3.1.0)
|
||||
responders (3.1.1)
|
||||
actionpack (>= 5.2)
|
||||
railties (>= 5.2)
|
||||
rest-client (2.1.0)
|
||||
@@ -702,23 +709,23 @@ GEM
|
||||
activesupport (>= 4)
|
||||
selectize-rails (0.12.6)
|
||||
semantic_range (3.0.0)
|
||||
sentry-rails (5.11.0)
|
||||
sentry-rails (5.13.0)
|
||||
railties (>= 5.0)
|
||||
sentry-ruby (~> 5.11.0)
|
||||
sentry-ruby (5.11.0)
|
||||
sentry-ruby (~> 5.13.0)
|
||||
sentry-ruby (5.13.0)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
sentry-sidekiq (5.11.0)
|
||||
sentry-ruby (~> 5.11.0)
|
||||
sentry-sidekiq (5.13.0)
|
||||
sentry-ruby (~> 5.13.0)
|
||||
sidekiq (>= 3.0)
|
||||
sexp_processor (4.17.0)
|
||||
shoulda-matchers (5.3.0)
|
||||
activesupport (>= 5.2.0)
|
||||
sidekiq (7.1.3)
|
||||
sidekiq (7.2.0)
|
||||
concurrent-ruby (< 2)
|
||||
connection_pool (>= 2.3.0)
|
||||
rack (>= 2.2.4)
|
||||
redis-client (>= 0.14.0)
|
||||
sidekiq-cron (1.10.1)
|
||||
sidekiq-cron (1.11.0)
|
||||
fugit (~> 1.8)
|
||||
globalid (>= 1.0.1)
|
||||
sidekiq (>= 6)
|
||||
@@ -732,13 +739,12 @@ GEM
|
||||
json (>= 1.8, < 3)
|
||||
simplecov-html (~> 0.10.0)
|
||||
simplecov-html (0.10.2)
|
||||
slack-ruby-client (2.0.0)
|
||||
slack-ruby-client (2.2.0)
|
||||
faraday (>= 2.0)
|
||||
faraday-mashify
|
||||
faraday-multipart
|
||||
gli
|
||||
hashie
|
||||
websocket-driver
|
||||
snaky_hash (2.0.1)
|
||||
hashie
|
||||
version_gem (~> 1.1, >= 1.1.1)
|
||||
@@ -759,7 +765,7 @@ GEM
|
||||
stripe (8.5.0)
|
||||
telephone_number (1.4.20)
|
||||
test-prof (1.2.1)
|
||||
thor (1.2.2)
|
||||
thor (1.3.0)
|
||||
tilt (2.2.0)
|
||||
time_diff (0.3.0)
|
||||
activesupport
|
||||
@@ -817,7 +823,7 @@ GEM
|
||||
working_hours (1.4.1)
|
||||
activesupport (>= 3.2)
|
||||
tzinfo
|
||||
zeitwerk (2.6.11)
|
||||
zeitwerk (2.6.12)
|
||||
|
||||
PLATFORMS
|
||||
arm64-darwin-20
|
||||
@@ -839,7 +845,7 @@ DEPENDENCIES
|
||||
administrate-field-belongs_to_search
|
||||
annotate
|
||||
attr_extras
|
||||
audited (~> 5.3)
|
||||
audited (~> 5.4, >= 5.4.1)
|
||||
aws-sdk-s3
|
||||
azure-storage-blob!
|
||||
barnes
|
||||
@@ -856,7 +862,7 @@ DEPENDENCIES
|
||||
database_cleaner
|
||||
ddtrace
|
||||
debug (~> 1.8)
|
||||
devise
|
||||
devise (>= 4.9.3)
|
||||
devise-secure_password!
|
||||
devise_token_auth
|
||||
dotenv-rails
|
||||
@@ -892,11 +898,12 @@ DEPENDENCIES
|
||||
line-bot-api
|
||||
liquid
|
||||
listen
|
||||
lograge (~> 0.13.0)
|
||||
lograge (~> 0.14.0)
|
||||
maxminddb
|
||||
meta_request
|
||||
mock_redis
|
||||
neighbor
|
||||
newrelic-sidekiq-metrics
|
||||
newrelic-sidekiq-metrics (>= 1.6.2)
|
||||
newrelic_rpm
|
||||
omniauth
|
||||
omniauth-google-oauth2
|
||||
@@ -916,7 +923,7 @@ DEPENDENCIES
|
||||
rails (~> 7.0.8.0)
|
||||
redis
|
||||
redis-namespace
|
||||
responders
|
||||
responders (>= 3.1.1)
|
||||
rest-client
|
||||
reverse_markdown
|
||||
rspec-rails
|
||||
@@ -928,14 +935,14 @@ DEPENDENCIES
|
||||
scout_apm
|
||||
scss_lint
|
||||
seed_dump
|
||||
sentry-rails (>= 5.11.0)
|
||||
sentry-rails (>= 5.13.0)
|
||||
sentry-ruby
|
||||
sentry-sidekiq (>= 5.11.0)
|
||||
sentry-sidekiq (>= 5.13.0)
|
||||
shoulda-matchers
|
||||
sidekiq (>= 7.1.3)
|
||||
sidekiq-cron (>= 1.10.1)
|
||||
sidekiq-cron (>= 1.11.0)
|
||||
simplecov (= 0.17.1)
|
||||
slack-ruby-client (~> 2.0.0)
|
||||
slack-ruby-client (~> 2.2.0)
|
||||
spring
|
||||
spring-watcher-listen
|
||||
squasher
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
backend: RAILS_ENV=test bin/rails s -p 5050
|
||||
frontend: bin/webpack-dev-server
|
||||
worker: dotenv RAILS_ENV=test bundle exec sidekiq -C config/sidekiq.yml
|
||||
frontend: export NODE_OPTIONS=--openssl-legacy-provider && bin/webpack-dev-server
|
||||
worker: RAILS_ENV=test dotenv bundle exec sidekiq -C config/sidekiq.yml
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
<p align="center">
|
||||
<img src="https://s3.us-west-2.amazonaws.com/gh-assets.chatwoot.com/brand.svg" alt="Woot-logo" width="240" />
|
||||
<img src="https://user-images.githubusercontent.com/2246121/282256557-1570674b-d142-4198-9740-69404cc6a339.png#gh-light-mode-only" width="100%" alt="Chat dashboard dark mode"/>
|
||||
<img src="https://user-images.githubusercontent.com/2246121/282256632-87f6a01b-6467-4e0e-8a93-7bbf66d03a17.png#gh-dark-mode-only" width="100%" alt="Chat dashboard"/>
|
||||
|
||||
<p align="center">Customer engagement suite, an open-source alternative to Intercom, Zendesk, Salesforce Service Cloud etc.</p>
|
||||
</p>
|
||||
___
|
||||
|
||||
<p align="center">
|
||||
# Chatwoot
|
||||
|
||||
Customer engagement suite, an open-source alternative to Intercom, Zendesk, Salesforce Service Cloud etc.
|
||||
<p>
|
||||
<a href="https://heroku.com/deploy?template=https://github.com/chatwoot/chatwoot/tree/master" alt="Deploy to Heroku">
|
||||
<img width="150" alt="Deploy" src="https://www.herokucdn.com/deploy/button.svg"/>
|
||||
</a>
|
||||
@@ -13,9 +15,7 @@
|
||||
</a>
|
||||
</p>
|
||||
|
||||
___
|
||||
|
||||
<p align="center">
|
||||
<p>
|
||||
<a href="https://codeclimate.com/github/chatwoot/chatwoot/maintainability"><img src="https://api.codeclimate.com/v1/badges/e6e3f66332c91e5a4c0c/maintainability" alt="Maintainability"></a>
|
||||
<img src="https://img.shields.io/circleci/build/github/chatwoot/chatwoot" alt="CircleCI Badge">
|
||||
<a href="https://hub.docker.com/r/chatwoot/chatwoot/"><img src="https://img.shields.io/docker/pulls/chatwoot/chatwoot" alt="Docker Pull Badge"></a>
|
||||
@@ -29,9 +29,8 @@ ___
|
||||
<a href="https://artifacthub.io/packages/helm/chatwoot/chatwoot"><img src="https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/artifact-hub" alt="Artifact HUB"></a>
|
||||
</p>
|
||||
|
||||
<img src="https://chatwoot-public-assets.s3.amazonaws.com/github/screenshot.png" width="100%" alt="Chat dashboard"/>
|
||||
|
||||
|
||||
<img src="https://user-images.githubusercontent.com/2246121/282255783-ee8a50c9-f42d-4752-8201-2d59965a663d.png#gh-light-mode-only" width="100%" alt="Chat dashboard dark mode"/>
|
||||
<img src="https://user-images.githubusercontent.com/2246121/282255784-3d1994ec-d895-4ff5-ac68-d819987e1869.png#gh-dark-mode-only" width="100%" alt="Chat dashboard"/>
|
||||
|
||||
Chatwoot is an open-source, self-hosted customer engagement suite. Chatwoot lets you view and manage your customer data, communicate with them irrespective of which medium they use, and re-engage them based on their profile.
|
||||
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
2.4.0
|
||||
2.6.0
|
||||
|
||||
@@ -24,14 +24,6 @@ select {
|
||||
font-size: $base-font-size;
|
||||
}
|
||||
|
||||
input,
|
||||
select,
|
||||
textarea {
|
||||
display: block;
|
||||
font-family: $base-font-family;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
[type="color"],
|
||||
[type="date"],
|
||||
[type="datetime-local"],
|
||||
@@ -51,6 +43,7 @@ textarea {
|
||||
background-color: $white;
|
||||
border: $base-border;
|
||||
border-radius: $base-border-radius;
|
||||
font-family: $base-font-family;
|
||||
padding: 0.5em;
|
||||
transition: border-color $base-duration $base-timing;
|
||||
width: 100%;
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
float: left;
|
||||
margin-bottom: $base-spacing;
|
||||
margin-top: 0.25em;
|
||||
text-align: right;
|
||||
width: calc(15% - 1rem);
|
||||
text-align: left;
|
||||
width: calc(16% - 1rem);
|
||||
}
|
||||
|
||||
.preserve-whitespace {
|
||||
@@ -17,7 +17,7 @@
|
||||
float: left;
|
||||
margin-bottom: $base-spacing;
|
||||
margin-left: 2rem;
|
||||
width: calc(85% - 1rem);
|
||||
width: calc(84% - 1rem);
|
||||
}
|
||||
|
||||
.attribute--nested {
|
||||
|
||||
@@ -10,7 +10,7 @@ input[type="submit"],
|
||||
color: $white;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
font-size: $font-size-default;
|
||||
font-size: $font-size-small;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
font-weight: $font-weight-medium;
|
||||
line-height: 1;
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
.main-content {
|
||||
font-size: $font-size-default;
|
||||
left: 23rem;
|
||||
left: 21rem;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.main-content__body {
|
||||
font-size: $font-size-small;
|
||||
padding: $space-two;
|
||||
|
||||
table {
|
||||
font-size: $font-size-small;
|
||||
}
|
||||
}
|
||||
|
||||
.main-content__header {
|
||||
@@ -20,7 +25,7 @@
|
||||
}
|
||||
|
||||
.main-content__page-title {
|
||||
font-size: $font-size-large;
|
||||
font-size: $font-size-medium;
|
||||
font-weight: $font-weight-medium;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
.logo-brand {
|
||||
margin-bottom: $space-normal;
|
||||
padding: $space-normal $space-smaller $space-small;
|
||||
text-align: center;
|
||||
text-align: left;
|
||||
|
||||
img {
|
||||
margin-bottom: $space-smaller;
|
||||
max-height: 3rem;
|
||||
}
|
||||
}
|
||||
|
||||
.navigation {
|
||||
@@ -19,12 +24,13 @@
|
||||
padding: $space-normal;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
width: 23rem;
|
||||
width: 21rem;
|
||||
z-index: 1023;
|
||||
|
||||
li {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
font-size: $font-size-small;
|
||||
|
||||
a {
|
||||
color: $color-gray;
|
||||
@@ -35,6 +41,10 @@
|
||||
min-width: $space-medium;
|
||||
}
|
||||
}
|
||||
|
||||
hr {
|
||||
margin: $space-slab;
|
||||
}
|
||||
}
|
||||
|
||||
.navigation__link {
|
||||
@@ -43,7 +53,7 @@
|
||||
display: block;
|
||||
line-height: 1;
|
||||
margin-bottom: $space-smaller;
|
||||
padding: $space-one;
|
||||
padding: $space-small;
|
||||
|
||||
&:hover {
|
||||
color: $blue;
|
||||
|
||||
@@ -4,7 +4,7 @@ $base-font-family: PlusJakarta, Inter, -apple-system, BlinkMacSystemFont, "Segoe
|
||||
sans-serif !default;
|
||||
$heading-font-family: $base-font-family !default;
|
||||
|
||||
$base-font-size: 16px !default;
|
||||
$base-font-size: 14px !default;
|
||||
|
||||
$base-line-height: 1.5 !default;
|
||||
$heading-line-height: 1.2 !default;
|
||||
|
||||
@@ -93,6 +93,9 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
|
||||
message_type: @message_type,
|
||||
content: response.content,
|
||||
source_id: response.identifier,
|
||||
content_attributes: {
|
||||
in_reply_to_external_id: response.in_reply_to_external_id
|
||||
},
|
||||
sender: @outgoing_echo ? nil : @contact_inbox.contact
|
||||
}
|
||||
end
|
||||
|
||||
@@ -76,6 +76,10 @@ class Messages::Instagram::MessageBuilder < Messages::Messenger::MessageBuilder
|
||||
message[:reply_to][:story] if message[:reply_to].present? && message[:reply_to][:story].present?
|
||||
end
|
||||
|
||||
def message_reply_attributes
|
||||
message[:reply_to][:mid] if message[:reply_to].present? && message[:reply_to][:mid].present?
|
||||
end
|
||||
|
||||
def build_message
|
||||
return if @outgoing_echo && already_sent_from_chatwoot?
|
||||
return if message_content.blank? && all_unsupported_files?
|
||||
@@ -118,7 +122,10 @@ class Messages::Instagram::MessageBuilder < Messages::Messenger::MessageBuilder
|
||||
message_type: message_type,
|
||||
source_id: message_identifier,
|
||||
content: message_content,
|
||||
sender: @outgoing_echo ? nil : contact
|
||||
sender: @outgoing_echo ? nil : contact,
|
||||
content_attributes: {
|
||||
in_reply_to_external_id: message_reply_attributes
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
|
||||
@@ -9,11 +9,11 @@ class Messages::MessageBuilder
|
||||
@user = user
|
||||
@message_type = params[:message_type] || 'outgoing'
|
||||
@attachments = params[:attachments]
|
||||
@automation_rule = @params&.dig(:content_attributes, :automation_rule_id)
|
||||
@automation_rule = content_attributes&.dig(:automation_rule_id)
|
||||
return unless params.instance_of?(ActionController::Parameters)
|
||||
|
||||
@in_reply_to = params.to_unsafe_h&.dig(:content_attributes, :in_reply_to)
|
||||
@items = params.to_unsafe_h&.dig(:content_attributes, :items)
|
||||
@in_reply_to = content_attributes&.dig(:in_reply_to)
|
||||
@items = content_attributes&.dig(:items)
|
||||
end
|
||||
|
||||
def perform
|
||||
@@ -26,6 +26,38 @@ class Messages::MessageBuilder
|
||||
|
||||
private
|
||||
|
||||
# Extracts content attributes from the given params.
|
||||
# - Converts ActionController::Parameters to a regular hash if needed.
|
||||
# - Attempts to parse a JSON string if content is a string.
|
||||
# - Returns an empty hash if content is not present, if there's a parsing error, or if it's an unexpected type.
|
||||
def content_attributes
|
||||
params = convert_to_hash(@params)
|
||||
content_attributes = params.fetch(:content_attributes, {})
|
||||
|
||||
return parse_json(content_attributes) if content_attributes.is_a?(String)
|
||||
return content_attributes if content_attributes.is_a?(Hash)
|
||||
|
||||
{}
|
||||
end
|
||||
|
||||
# Converts the given object to a hash.
|
||||
# If it's an instance of ActionController::Parameters, converts it to an unsafe hash.
|
||||
# Otherwise, returns the object as-is.
|
||||
def convert_to_hash(obj)
|
||||
return obj.to_unsafe_h if obj.instance_of?(ActionController::Parameters)
|
||||
|
||||
obj
|
||||
end
|
||||
|
||||
# Attempts to parse a string as JSON.
|
||||
# If successful, returns the parsed hash with symbolized names.
|
||||
# If unsuccessful, returns nil.
|
||||
def parse_json(content)
|
||||
JSON.parse(content, symbolize_names: true)
|
||||
rescue JSON::ParserError
|
||||
{}
|
||||
end
|
||||
|
||||
def process_attachments
|
||||
return if @attachments.blank?
|
||||
|
||||
|
||||
@@ -15,7 +15,10 @@ class V2::ReportBuilder
|
||||
end
|
||||
|
||||
def timeseries
|
||||
send(params[:metric])
|
||||
return send(params[:metric]) if metric_valid?
|
||||
|
||||
Rails.logger.error "ReportBuilder: Invalid metric - #{params[:metric]}"
|
||||
{}
|
||||
end
|
||||
|
||||
# For backward compatible with old report
|
||||
@@ -53,6 +56,16 @@ class V2::ReportBuilder
|
||||
|
||||
private
|
||||
|
||||
def metric_valid?
|
||||
%w[conversations_count
|
||||
incoming_messages_count
|
||||
outgoing_messages_count
|
||||
avg_first_response_time
|
||||
avg_resolution_time reply_time
|
||||
resolutions_count
|
||||
reply_time].include?(params[:metric])
|
||||
end
|
||||
|
||||
def inbox
|
||||
@inbox ||= account.inboxes.find(params[:id])
|
||||
end
|
||||
|
||||
@@ -9,7 +9,6 @@ class Api::V1::Accounts::Actions::ContactMergesController < Api::V1::Accounts::B
|
||||
mergee_contact: @mergee_contact
|
||||
)
|
||||
contact_merge_action.perform
|
||||
render json: @base_contact
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -10,7 +10,7 @@ class Api::V1::Accounts::ArticlesController < Api::V1::Accounts::BaseController
|
||||
@articles_count = @all_articles.count
|
||||
|
||||
@articles = if list_params[:category_slug].present?
|
||||
@all_articles.order_by_position.page(@current_page).per(50)
|
||||
@all_articles.order_by_position.page(@current_page)
|
||||
else
|
||||
@all_articles.order_by_updated_at.page(@current_page)
|
||||
end
|
||||
|
||||
@@ -48,7 +48,7 @@ class Api::V1::Accounts::CategoriesController < Api::V1::Accounts::BaseControlle
|
||||
|
||||
def category_params
|
||||
params.require(:category).permit(
|
||||
:name, :description, :position, :slug, :locale, :parent_category_id, :associated_category_id
|
||||
:name, :description, :position, :slug, :locale, :icon, :parent_category_id, :associated_category_id
|
||||
)
|
||||
end
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
class Api::V1::Accounts::ContactInboxesController < Api::V1::Accounts::BaseController
|
||||
before_action :ensure_inbox
|
||||
|
||||
def filter
|
||||
contact_inbox = @inbox.contact_inboxes.where(inbox_id: permitted_params[:inbox_id], source_id: permitted_params[:source_id])
|
||||
return head :not_found if contact_inbox.empty?
|
||||
|
||||
@contact = contact_inbox.first.contact
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def ensure_inbox
|
||||
@inbox = Current.account.inboxes.find(permitted_params[:inbox_id])
|
||||
authorize @inbox, :show?
|
||||
end
|
||||
|
||||
def permitted_params
|
||||
params.permit(:inbox_id, :source_id)
|
||||
end
|
||||
end
|
||||
@@ -60,7 +60,7 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
|
||||
end
|
||||
|
||||
def toggle_status
|
||||
if params[:status]
|
||||
if params[:status].present?
|
||||
set_conversation_status
|
||||
@status = @conversation.save!
|
||||
else
|
||||
|
||||
@@ -47,6 +47,11 @@ class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController
|
||||
head :ok
|
||||
end
|
||||
|
||||
def logo
|
||||
@portal.logo.purge if @portal.logo.attached?
|
||||
head :ok
|
||||
end
|
||||
|
||||
def process_attached_logo
|
||||
blob_id = params[:blob_id]
|
||||
blob = ActiveStorage::Blob.find_by(id: blob_id)
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
class Api::V1::Integrations::WebhooksController < ApplicationController
|
||||
def create
|
||||
builder = Integrations::Slack::IncomingMessageBuilder.new(params)
|
||||
builder = Integrations::Slack::IncomingMessageBuilder.new(permitted_params)
|
||||
response = builder.perform
|
||||
render json: response
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# TODO: This is a temporary solution to permit all params for slack unfurling job.
|
||||
# We should only permit the params that we use. Handle all the params based on events and send it to the respective services.
|
||||
def permitted_params
|
||||
params.permit!
|
||||
end
|
||||
end
|
||||
|
||||
@@ -79,6 +79,9 @@ class Api::V1::Widget::BaseController < ApplicationController
|
||||
sender: @contact,
|
||||
content: permitted_params[:message][:content],
|
||||
inbox_id: conversation.inbox_id,
|
||||
content_attributes: {
|
||||
in_reply_to: permitted_params[:message][:reply_to]
|
||||
},
|
||||
echo_id: permitted_params[:message][:echo_id],
|
||||
message_type: :incoming
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
|
||||
include Events::Types
|
||||
before_action :render_not_found_if_empty, only: [:toggle_typing, :toggle_status, :set_custom_attributes, :destroy_custom_attributes]
|
||||
|
||||
def index
|
||||
@conversation = conversation
|
||||
@@ -27,6 +28,7 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
|
||||
|
||||
conversation.contact_last_seen_at = DateTime.now.utc
|
||||
conversation.save!
|
||||
::Conversations::UpdateMessageStatusJob.perform_later(conversation.id, conversation.contact_last_seen_at)
|
||||
head :ok
|
||||
end
|
||||
|
||||
@@ -41,8 +43,6 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
|
||||
end
|
||||
|
||||
def toggle_typing
|
||||
head :ok && return if conversation.nil?
|
||||
|
||||
case permitted_params[:typing_status]
|
||||
when 'on'
|
||||
trigger_typing_event(CONVERSATION_TYPING_ON)
|
||||
@@ -54,8 +54,6 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
|
||||
end
|
||||
|
||||
def toggle_status
|
||||
return head :not_found if conversation.nil?
|
||||
|
||||
return head :forbidden unless @web_widget.end_conversation?
|
||||
|
||||
unless conversation.resolved?
|
||||
@@ -81,6 +79,10 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
|
||||
Rails.configuration.dispatcher.dispatch(event, Time.zone.now, conversation: conversation, user: @contact)
|
||||
end
|
||||
|
||||
def render_not_found_if_empty
|
||||
return head :not_found if conversation.nil?
|
||||
end
|
||||
|
||||
def permitted_params
|
||||
params.permit(:id, :typing_status, :website_token, :email, contact: [:name, :email, :phone_number],
|
||||
message: [:content, :referer_url, :timestamp, :echo_id],
|
||||
|
||||
@@ -64,7 +64,7 @@ class Api::V1::Widget::MessagesController < Api::V1::Widget::BaseController
|
||||
|
||||
def permitted_params
|
||||
# timestamp parameter is used in create conversation method
|
||||
params.permit(:id, :before, :after, :website_token, contact: [:name, :email], message: [:content, :referer_url, :timestamp, :echo_id])
|
||||
params.permit(:id, :before, :after, :website_token, contact: [:name, :email], message: [:content, :referer_url, :timestamp, :echo_id, :reply_to])
|
||||
end
|
||||
|
||||
def set_message
|
||||
|
||||
@@ -9,11 +9,14 @@ module RequestExceptionHandler
|
||||
|
||||
def handle_with_exception
|
||||
yield
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
rescue ActiveRecord::RecordNotFound => e
|
||||
log_handled_error(e)
|
||||
render_not_found_error('Resource could not be found')
|
||||
rescue Pundit::NotAuthorizedError
|
||||
rescue Pundit::NotAuthorizedError => e
|
||||
log_handled_error(e)
|
||||
render_unauthorized('You are not authorized to do this action')
|
||||
rescue ActionController::ParameterMissing => e
|
||||
log_handled_error(e)
|
||||
render_could_not_create_error(e.message)
|
||||
ensure
|
||||
# to address the thread variable leak issues in Puma/Thin webserver
|
||||
@@ -41,6 +44,7 @@ module RequestExceptionHandler
|
||||
end
|
||||
|
||||
def render_record_invalid(exception)
|
||||
log_handled_error(exception)
|
||||
render json: {
|
||||
message: exception.record.errors.full_messages.join(', '),
|
||||
attributes: exception.record.errors.attribute_names
|
||||
@@ -48,6 +52,11 @@ module RequestExceptionHandler
|
||||
end
|
||||
|
||||
def render_error_response(exception)
|
||||
log_handled_error(exception)
|
||||
render json: exception.to_hash, status: exception.http_status
|
||||
end
|
||||
|
||||
def log_handled_error(exception)
|
||||
logger.info("Handled error: #{exception.inspect}")
|
||||
end
|
||||
end
|
||||
|
||||
@@ -23,6 +23,7 @@ class Public::Api::V1::Inboxes::ConversationsController < Public::Api::V1::Inbox
|
||||
def update_last_seen
|
||||
@conversation.contact_last_seen_at = DateTime.now.utc
|
||||
@conversation.save!
|
||||
::Conversations::UpdateMessageStatusJob.perform_later(@conversation.id, @conversation.contact_last_seen_at)
|
||||
head :ok
|
||||
end
|
||||
|
||||
@@ -33,7 +34,7 @@ class Public::Api::V1::Inboxes::ConversationsController < Public::Api::V1::Inbox
|
||||
end
|
||||
|
||||
def create_conversation
|
||||
::Conversation.create!(conversation_params)
|
||||
ConversationBuilder.new(params: conversation_params, contact_inbox: @contact_inbox).perform
|
||||
end
|
||||
|
||||
def trigger_typing_event(event)
|
||||
@@ -41,11 +42,6 @@ class Public::Api::V1::Inboxes::ConversationsController < Public::Api::V1::Inbox
|
||||
end
|
||||
|
||||
def conversation_params
|
||||
{
|
||||
account_id: @contact_inbox.contact.account_id,
|
||||
inbox_id: @contact_inbox.inbox_id,
|
||||
contact_id: @contact_inbox.contact_id,
|
||||
contact_inbox_id: @contact_inbox.id
|
||||
}
|
||||
params.permit(custom_attributes: {})
|
||||
end
|
||||
end
|
||||
|
||||
@@ -39,10 +39,6 @@ class Public::Api::V1::Portals::ArticlesController < Public::Api::V1::Portals::B
|
||||
)
|
||||
end
|
||||
|
||||
def portal
|
||||
@portal ||= Portal.find_by!(slug: permitted_params[:slug], archived: false)
|
||||
end
|
||||
|
||||
def list_params
|
||||
params.permit(:query, :locale, :sort, :status)
|
||||
end
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
class Public::Api::V1::Portals::BaseController < PublicController
|
||||
before_action :show_plain_layout
|
||||
before_action :set_color_scheme
|
||||
around_action :set_locale
|
||||
after_action :allow_iframe_requests
|
||||
|
||||
@@ -9,6 +10,18 @@ class Public::Api::V1::Portals::BaseController < PublicController
|
||||
@is_plain_layout_enabled = params[:show_plain_layout] == 'true'
|
||||
end
|
||||
|
||||
def set_color_scheme
|
||||
@theme = if %w[dark light].include?(params[:theme])
|
||||
params[:theme]
|
||||
else
|
||||
'system'
|
||||
end
|
||||
end
|
||||
|
||||
def portal
|
||||
@portal ||= Portal.find_by!(slug: params[:slug], archived: false)
|
||||
end
|
||||
|
||||
def set_locale(&)
|
||||
switch_locale_with_portal(&) if params[:locale].present?
|
||||
switch_locale_with_article(&) if params[:article_slug].present?
|
||||
@@ -31,6 +44,8 @@ class Public::Api::V1::Portals::BaseController < PublicController
|
||||
|
||||
def switch_locale_with_article(&)
|
||||
article = Article.find_by(slug: params[:article_slug])
|
||||
Rails.logger.info "Article: not found for slug: #{params[:article_slug]}"
|
||||
render_404 && return if article.blank?
|
||||
|
||||
@locale = if article.category.present?
|
||||
article.category.locale
|
||||
@@ -44,4 +59,9 @@ class Public::Api::V1::Portals::BaseController < PublicController
|
||||
def allow_iframe_requests
|
||||
response.headers.delete('X-Frame-Options') if @is_plain_layout_enabled
|
||||
end
|
||||
|
||||
def render_404
|
||||
portal
|
||||
render 'public/api/v1/portals/error/404', status: :not_found
|
||||
end
|
||||
end
|
||||
|
||||
@@ -13,10 +13,9 @@ class Public::Api::V1::Portals::CategoriesController < Public::Api::V1::Portals:
|
||||
private
|
||||
|
||||
def set_category
|
||||
@category = @portal.categories.find_by!(locale: params[:locale], slug: params[:category_slug])
|
||||
end
|
||||
@category = @portal.categories.find_by(locale: params[:locale], slug: params[:category_slug])
|
||||
|
||||
def portal
|
||||
@portal ||= Portal.find_by!(slug: params[:slug], archived: false)
|
||||
Rails.logger.info "Category: not found for slug: #{params[:category_slug]}"
|
||||
render_404 && return if @category.blank?
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
class SlackUploadsController < ApplicationController
|
||||
include Rails.application.routes.url_helpers
|
||||
before_action :set_blob, only: [:show]
|
||||
|
||||
def show
|
||||
if @blob
|
||||
redirect_to blob_url
|
||||
else
|
||||
redirect_to avatar_url
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_blob
|
||||
@blob = ActiveStorage::Blob.find_by(key: params[:blob_key])
|
||||
end
|
||||
|
||||
def blob_url
|
||||
url_for(@blob.representation(resize_to_fill: [250, nil]))
|
||||
end
|
||||
|
||||
def avatar_url
|
||||
base_url = ENV.fetch('FRONTEND_URL', nil)
|
||||
"#{base_url}/integrations/slack/#{params[:sender_type]}.png"
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,21 @@
|
||||
class Twilio::DeliveryStatusController < ApplicationController
|
||||
def create
|
||||
::Twilio::DeliveryStatusService.new(params: permitted_params).perform
|
||||
|
||||
head :no_content
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def permitted_params
|
||||
params.permit(
|
||||
:AccountSid,
|
||||
:From,
|
||||
:MessageSid,
|
||||
:MessagingServiceSid,
|
||||
:MessageStatus,
|
||||
:ErrorCode,
|
||||
:ErrorMessage
|
||||
)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,48 @@
|
||||
module PortalHelper
|
||||
def generate_portal_bg_color(portal_color, theme)
|
||||
base_color = theme == 'dark' ? 'black' : 'white'
|
||||
"color-mix(in srgb, #{portal_color} 20%, #{base_color})"
|
||||
end
|
||||
|
||||
def generate_portal_bg(portal_color, theme)
|
||||
bg_image = theme == 'dark' ? 'hexagon-dark.svg' : 'hexagon-light.svg'
|
||||
"background: url(/assets/images/hc/#{bg_image}) #{generate_portal_bg_color(portal_color, theme)}"
|
||||
end
|
||||
|
||||
def language_name(locale)
|
||||
language_map = YAML.load_file(Rails.root.join('config/languages/language_map.yml'))
|
||||
language_map[locale] || locale
|
||||
end
|
||||
|
||||
def get_theme_names(theme)
|
||||
if theme == 'light'
|
||||
I18n.t('public_portal.header.appearance.light')
|
||||
elsif theme == 'dark'
|
||||
I18n.t('public_portal.header.appearance.dark')
|
||||
else
|
||||
I18n.t('public_portal.header.appearance.system')
|
||||
end
|
||||
end
|
||||
|
||||
def get_theme_icon(theme)
|
||||
if theme == 'light'
|
||||
'icons/sun'
|
||||
elsif theme == 'dark'
|
||||
'icons/moon'
|
||||
else
|
||||
'icons/monitor'
|
||||
end
|
||||
end
|
||||
|
||||
def generate_gradient_to_bottom(theme)
|
||||
"background-image: linear-gradient(to bottom, transparent, #{theme == 'dark' ? '#151718' : 'white'})"
|
||||
end
|
||||
|
||||
def generate_article_link(portal_slug, article_slug, theme)
|
||||
"/hc/#{portal_slug}/articles/#{article_slug}#{theme.present? && theme != 'system' ? "?theme=#{theme}" : ''}"
|
||||
end
|
||||
|
||||
def render_category_content(content)
|
||||
ChatwootMarkdownRenderer.new(content).render_markdown_to_plain_text
|
||||
end
|
||||
end
|
||||
@@ -1,13 +1,13 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="!authUIFlags.isFetching"
|
||||
v-if="!authUIFlags.isFetching && !accountUIFlags.isFetchingItem"
|
||||
id="app"
|
||||
class="app-wrapper h-full flex-grow-0 min-h-0 w-full"
|
||||
:class="{ 'app-rtl--wrapper': isRTLView }"
|
||||
:dir="isRTLView ? 'rtl' : 'ltr'"
|
||||
>
|
||||
<update-banner :latest-chatwoot-version="latestChatwootVersion" />
|
||||
<template v-if="!accountUIFlags.isFetchingItem && currentAccountId">
|
||||
<template v-if="currentAccountId">
|
||||
<payment-pending-banner />
|
||||
<upgrade-banner />
|
||||
</template>
|
||||
@@ -111,10 +111,8 @@ export default {
|
||||
this.$store.dispatch('setActiveAccount', {
|
||||
accountId: this.currentAccountId,
|
||||
});
|
||||
const {
|
||||
locale,
|
||||
latest_chatwoot_version: latestChatwootVersion,
|
||||
} = this.getAccount(this.currentAccountId);
|
||||
const { locale, latest_chatwoot_version: latestChatwootVersion } =
|
||||
this.getAccount(this.currentAccountId);
|
||||
const { pubsub_token: pubsubToken } = this.currentUser || {};
|
||||
this.setLocale(locale);
|
||||
this.updateRTLDirectionView(locale);
|
||||
|
||||
@@ -15,9 +15,8 @@ class ApiClient {
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
get accountIdFromRoute() {
|
||||
const isInsideAccountScopedURLs = window.location.pathname.includes(
|
||||
'/app/accounts'
|
||||
);
|
||||
const isInsideAccountScopedURLs =
|
||||
window.location.pathname.includes('/app/accounts');
|
||||
|
||||
if (isInsideAccountScopedURLs) {
|
||||
return window.location.pathname.split('/')[3];
|
||||
|
||||
@@ -84,6 +84,11 @@ export default {
|
||||
return axios.delete(endPoints('deleteAvatar').url);
|
||||
},
|
||||
|
||||
resetPassword({ email }) {
|
||||
const urlData = endPoints('resetPassword');
|
||||
return axios.post(urlData.url, { email });
|
||||
},
|
||||
|
||||
setActiveAccount({ accountId }) {
|
||||
const urlData = endPoints('setActiveAccount');
|
||||
return axios.put(urlData.url, {
|
||||
|
||||
@@ -53,6 +53,7 @@ class ContactAPI extends ApiClient {
|
||||
return axios.get(requestURL);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line default-param-last
|
||||
filter(page = 1, sortAttr = 'name', queryPayload) {
|
||||
let requestURL = `${this.url}/filter?${buildContactParams(page, sortAttr)}`;
|
||||
return axios.post(requestURL, queryPayload);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import accountAPI from '../account';
|
||||
import ApiClient from '../../ApiClient';
|
||||
import describeWithAPIMock from '../../specs/apiSpecHelper';
|
||||
|
||||
describe('#enterpriseAccountAPI', () => {
|
||||
it('creates correct instance', () => {
|
||||
@@ -13,17 +12,33 @@ describe('#enterpriseAccountAPI', () => {
|
||||
expect(accountAPI).toHaveProperty('checkout');
|
||||
});
|
||||
|
||||
describeWithAPIMock('API calls', context => {
|
||||
describe('API calls', () => {
|
||||
const originalAxios = window.axios;
|
||||
const axiosMock = {
|
||||
post: jest.fn(() => Promise.resolve()),
|
||||
get: jest.fn(() => Promise.resolve()),
|
||||
patch: jest.fn(() => Promise.resolve()),
|
||||
delete: jest.fn(() => Promise.resolve()),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
window.axios = axiosMock;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
window.axios = originalAxios;
|
||||
});
|
||||
|
||||
it('#checkout', () => {
|
||||
accountAPI.checkout();
|
||||
expect(context.axiosMock.post).toHaveBeenCalledWith(
|
||||
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||
'/enterprise/api/v1/checkout'
|
||||
);
|
||||
});
|
||||
|
||||
it('#subscription', () => {
|
||||
accountAPI.subscription();
|
||||
expect(context.axiosMock.post).toHaveBeenCalledWith(
|
||||
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||
'/enterprise/api/v1/subscription'
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/* global axios */
|
||||
|
||||
import PortalsAPI from './portals';
|
||||
import { getArticleSearchURL } from 'dashboard/helper/URLHelper.js';
|
||||
|
||||
class ArticlesAPI extends PortalsAPI {
|
||||
constructor() {
|
||||
@@ -12,14 +13,31 @@ class ArticlesAPI extends PortalsAPI {
|
||||
portalSlug,
|
||||
locale,
|
||||
status,
|
||||
author_id,
|
||||
category_slug,
|
||||
authorId,
|
||||
categorySlug,
|
||||
sort,
|
||||
}) {
|
||||
let baseUrl = `${this.url}/${portalSlug}/articles?page=${pageNumber}&locale=${locale}`;
|
||||
if (status !== undefined) baseUrl += `&status=${status}`;
|
||||
if (author_id) baseUrl += `&author_id=${author_id}`;
|
||||
if (category_slug) baseUrl += `&category_slug=${category_slug}`;
|
||||
return axios.get(baseUrl);
|
||||
const url = getArticleSearchURL({
|
||||
pageNumber,
|
||||
portalSlug,
|
||||
locale,
|
||||
status,
|
||||
authorId,
|
||||
categorySlug,
|
||||
sort,
|
||||
host: this.url,
|
||||
});
|
||||
|
||||
return axios.get(url);
|
||||
}
|
||||
|
||||
searchArticles({ portalSlug, query }) {
|
||||
const url = getArticleSearchURL({
|
||||
portalSlug,
|
||||
query,
|
||||
host: this.url,
|
||||
});
|
||||
return axios.get(url);
|
||||
}
|
||||
|
||||
getArticle({ id, portalSlug }) {
|
||||
|
||||
@@ -17,6 +17,10 @@ class PortalsAPI extends ApiClient {
|
||||
deletePortal(portalSlug) {
|
||||
return axios.delete(`${this.url}/${portalSlug}`);
|
||||
}
|
||||
|
||||
deleteLogo(portalSlug) {
|
||||
return axios.delete(`${this.url}/${portalSlug}/logo`);
|
||||
}
|
||||
}
|
||||
|
||||
export default PortalsAPI;
|
||||
|
||||
@@ -26,9 +26,13 @@ export const buildCreatePayload = ({
|
||||
payload.append('echo_id', echoId);
|
||||
payload.append('cc_emails', ccEmails);
|
||||
payload.append('bcc_emails', bccEmails);
|
||||
|
||||
if (toEmails) {
|
||||
payload.append('to_emails', toEmails);
|
||||
}
|
||||
if (contentAttributes) {
|
||||
payload.append('content_attributes', JSON.stringify(contentAttributes));
|
||||
}
|
||||
} else {
|
||||
payload = {
|
||||
content: message,
|
||||
|
||||
@@ -31,6 +31,7 @@ class ReportsAPI extends ApiClient {
|
||||
});
|
||||
}
|
||||
|
||||
// eslint-disable-next-line default-param-last
|
||||
getSummary(since, until, type = 'account', id, groupBy, businessHours) {
|
||||
return axios.get(`${this.url}/summary`, {
|
||||
params: {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import accountAPI from '../account';
|
||||
import ApiClient from '../ApiClient';
|
||||
import describeWithAPIMock from './apiSpecHelper';
|
||||
|
||||
describe('#accountAPI', () => {
|
||||
it('creates correct instance', () => {
|
||||
@@ -13,12 +12,28 @@ describe('#accountAPI', () => {
|
||||
expect(accountAPI).toHaveProperty('createAccount');
|
||||
});
|
||||
|
||||
describeWithAPIMock('API calls', context => {
|
||||
describe('API calls', () => {
|
||||
const originalAxios = window.axios;
|
||||
const axiosMock = {
|
||||
post: jest.fn(() => Promise.resolve()),
|
||||
get: jest.fn(() => Promise.resolve()),
|
||||
patch: jest.fn(() => Promise.resolve()),
|
||||
delete: jest.fn(() => Promise.resolve()),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
window.axios = axiosMock;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
window.axios = originalAxios;
|
||||
});
|
||||
|
||||
it('#createAccount', () => {
|
||||
accountAPI.createAccount({
|
||||
name: 'Chatwoot',
|
||||
});
|
||||
expect(context.axiosMock.post).toHaveBeenCalledWith('/api/v1/accounts', {
|
||||
expect(axiosMock.post).toHaveBeenCalledWith('/api/v1/accounts', {
|
||||
name: 'Chatwoot',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import accountActionsAPI from '../accountActions';
|
||||
import ApiClient from '../ApiClient';
|
||||
import describeWithAPIMock from './apiSpecHelper';
|
||||
|
||||
describe('#ContactsAPI', () => {
|
||||
it('creates correct instance', () => {
|
||||
@@ -8,10 +7,26 @@ describe('#ContactsAPI', () => {
|
||||
expect(accountActionsAPI).toHaveProperty('merge');
|
||||
});
|
||||
|
||||
describeWithAPIMock('API calls', context => {
|
||||
describe('API calls', () => {
|
||||
const originalAxios = window.axios;
|
||||
const axiosMock = {
|
||||
post: jest.fn(() => Promise.resolve()),
|
||||
get: jest.fn(() => Promise.resolve()),
|
||||
patch: jest.fn(() => Promise.resolve()),
|
||||
delete: jest.fn(() => Promise.resolve()),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
window.axios = axiosMock;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
window.axios = originalAxios;
|
||||
});
|
||||
|
||||
it('#merge', () => {
|
||||
accountActionsAPI.merge(1, 2);
|
||||
expect(context.axiosMock.post).toHaveBeenCalledWith(
|
||||
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||
'/api/v1/actions/contact_merge',
|
||||
{
|
||||
base_contact_id: 1,
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
function apiSpecHelper() {
|
||||
beforeEach(() => {
|
||||
this.originalAxios = window.axios;
|
||||
this.axiosMock = {
|
||||
post: jest.fn(() => Promise.resolve()),
|
||||
get: jest.fn(() => Promise.resolve()),
|
||||
patch: jest.fn(() => Promise.resolve()),
|
||||
delete: jest.fn(() => Promise.resolve()),
|
||||
};
|
||||
window.axios = this.axiosMock;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
window.axios = this.originalAxios;
|
||||
});
|
||||
}
|
||||
// https://stackoverflow.com/a/59344023/3901856
|
||||
const sharedWrapper = describe('sharedWrapper', () => {});
|
||||
export default function describeWithAPIMock(skillName, testFn) {
|
||||
return describe(skillName, function configureContext() {
|
||||
function Context() {}
|
||||
Context.prototype = sharedWrapper.ctx;
|
||||
this.ctx = new Context();
|
||||
apiSpecHelper.call(this);
|
||||
testFn.call(this, this);
|
||||
});
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import articlesAPI from '../helpCenter/articles';
|
||||
import ApiClient from 'dashboard/api/helpCenter/portals';
|
||||
import describeWithAPIMock from './apiSpecHelper';
|
||||
|
||||
describe('#PortalAPI', () => {
|
||||
it('creates correct instance', () => {
|
||||
@@ -12,39 +11,114 @@ describe('#PortalAPI', () => {
|
||||
expect(articlesAPI).toHaveProperty('delete');
|
||||
expect(articlesAPI).toHaveProperty('getArticles');
|
||||
});
|
||||
describeWithAPIMock('API calls', context => {
|
||||
describe('API calls', () => {
|
||||
const originalAxios = window.axios;
|
||||
const axiosMock = {
|
||||
post: jest.fn(() => Promise.resolve()),
|
||||
get: jest.fn(() => Promise.resolve()),
|
||||
patch: jest.fn(() => Promise.resolve()),
|
||||
delete: jest.fn(() => Promise.resolve()),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
window.axios = axiosMock;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
window.axios = originalAxios;
|
||||
});
|
||||
|
||||
it('#getArticles', () => {
|
||||
articlesAPI.getArticles({
|
||||
pageNumber: 1,
|
||||
portalSlug: 'room-rental',
|
||||
locale: 'en-US',
|
||||
status: 'published',
|
||||
author_id: '1',
|
||||
authorId: '1',
|
||||
});
|
||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||
expect(axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v1/portals/room-rental/articles?page=1&locale=en-US&status=published&author_id=1'
|
||||
);
|
||||
});
|
||||
});
|
||||
describeWithAPIMock('API calls', context => {
|
||||
describe('API calls', () => {
|
||||
const originalAxios = window.axios;
|
||||
const axiosMock = {
|
||||
post: jest.fn(() => Promise.resolve()),
|
||||
get: jest.fn(() => Promise.resolve()),
|
||||
patch: jest.fn(() => Promise.resolve()),
|
||||
delete: jest.fn(() => Promise.resolve()),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
window.axios = axiosMock;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
window.axios = originalAxios;
|
||||
});
|
||||
|
||||
it('#getArticle', () => {
|
||||
articlesAPI.getArticle({
|
||||
id: 1,
|
||||
portalSlug: 'room-rental',
|
||||
});
|
||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||
expect(axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v1/portals/room-rental/articles/1'
|
||||
);
|
||||
});
|
||||
});
|
||||
describeWithAPIMock('API calls', context => {
|
||||
describe('API calls', () => {
|
||||
const originalAxios = window.axios;
|
||||
const axiosMock = {
|
||||
post: jest.fn(() => Promise.resolve()),
|
||||
get: jest.fn(() => Promise.resolve()),
|
||||
patch: jest.fn(() => Promise.resolve()),
|
||||
delete: jest.fn(() => Promise.resolve()),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
window.axios = axiosMock;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
window.axios = originalAxios;
|
||||
});
|
||||
|
||||
it('#searchArticles', () => {
|
||||
articlesAPI.searchArticles({
|
||||
query: 'test',
|
||||
portalSlug: 'room-rental',
|
||||
});
|
||||
expect(axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v1/portals/room-rental/articles?query=test'
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('API calls', () => {
|
||||
const originalAxios = window.axios;
|
||||
const axiosMock = {
|
||||
post: jest.fn(() => Promise.resolve()),
|
||||
get: jest.fn(() => Promise.resolve()),
|
||||
patch: jest.fn(() => Promise.resolve()),
|
||||
delete: jest.fn(() => Promise.resolve()),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
window.axios = axiosMock;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
window.axios = originalAxios;
|
||||
});
|
||||
|
||||
it('#updateArticle', () => {
|
||||
articlesAPI.updateArticle({
|
||||
articleId: 1,
|
||||
portalSlug: 'room-rental',
|
||||
articleObj: { title: 'Update shipping address' },
|
||||
});
|
||||
expect(context.axiosMock.patch).toHaveBeenCalledWith(
|
||||
expect(axiosMock.patch).toHaveBeenCalledWith(
|
||||
'/api/v1/portals/room-rental/articles/1',
|
||||
{
|
||||
title: 'Update shipping address',
|
||||
@@ -52,13 +126,29 @@ describe('#PortalAPI', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
describeWithAPIMock('API calls', context => {
|
||||
describe('API calls', () => {
|
||||
const originalAxios = window.axios;
|
||||
const axiosMock = {
|
||||
post: jest.fn(() => Promise.resolve()),
|
||||
get: jest.fn(() => Promise.resolve()),
|
||||
patch: jest.fn(() => Promise.resolve()),
|
||||
delete: jest.fn(() => Promise.resolve()),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
window.axios = axiosMock;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
window.axios = originalAxios;
|
||||
});
|
||||
|
||||
it('#deleteArticle', () => {
|
||||
articlesAPI.deleteArticle({
|
||||
articleId: 1,
|
||||
portalSlug: 'room-rental',
|
||||
});
|
||||
expect(context.axiosMock.delete).toHaveBeenCalledWith(
|
||||
expect(axiosMock.delete).toHaveBeenCalledWith(
|
||||
'/api/v1/portals/room-rental/articles/1'
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,18 +1,30 @@
|
||||
import assignableAgentsAPI from '../assignableAgents';
|
||||
import describeWithAPIMock from './apiSpecHelper';
|
||||
|
||||
describe('#AssignableAgentsAPI', () => {
|
||||
describeWithAPIMock('API calls', context => {
|
||||
describe('API calls', () => {
|
||||
const originalAxios = window.axios;
|
||||
const axiosMock = {
|
||||
post: jest.fn(() => Promise.resolve()),
|
||||
get: jest.fn(() => Promise.resolve()),
|
||||
patch: jest.fn(() => Promise.resolve()),
|
||||
delete: jest.fn(() => Promise.resolve()),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
window.axios = axiosMock;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
window.axios = originalAxios;
|
||||
});
|
||||
|
||||
it('#getAssignableAgents', () => {
|
||||
assignableAgentsAPI.get([1]);
|
||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v1/assignable_agents',
|
||||
{
|
||||
params: {
|
||||
inbox_ids: [1],
|
||||
},
|
||||
}
|
||||
);
|
||||
expect(axiosMock.get).toHaveBeenCalledWith('/api/v1/assignable_agents', {
|
||||
params: {
|
||||
inbox_ids: [1],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import fbChannel from '../../channel/fbChannel';
|
||||
import ApiClient from '../../ApiClient';
|
||||
import describeWithAPIMock from '../apiSpecHelper';
|
||||
|
||||
describe('#FBChannel', () => {
|
||||
it('creates correct instance', () => {
|
||||
@@ -11,10 +10,26 @@ describe('#FBChannel', () => {
|
||||
expect(fbChannel).toHaveProperty('update');
|
||||
expect(fbChannel).toHaveProperty('delete');
|
||||
});
|
||||
describeWithAPIMock('API calls', context => {
|
||||
describe('API calls', () => {
|
||||
const originalAxios = window.axios;
|
||||
const axiosMock = {
|
||||
post: jest.fn(() => Promise.resolve()),
|
||||
get: jest.fn(() => Promise.resolve()),
|
||||
patch: jest.fn(() => Promise.resolve()),
|
||||
delete: jest.fn(() => Promise.resolve()),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
window.axios = axiosMock;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
window.axios = originalAxios;
|
||||
});
|
||||
|
||||
it('#create', () => {
|
||||
fbChannel.create({ omniauthToken: 'ASFM131CSF@#@$', appId: 'chatwoot' });
|
||||
expect(context.axiosMock.post).toHaveBeenCalledWith(
|
||||
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||
'/api/v1/callbacks/register_facebook_page',
|
||||
{
|
||||
omniauthToken: 'ASFM131CSF@#@$',
|
||||
@@ -27,7 +42,7 @@ describe('#FBChannel', () => {
|
||||
omniauthToken: 'ASFM131CSF@#@$',
|
||||
inboxId: 1,
|
||||
});
|
||||
expect(context.axiosMock.post).toHaveBeenCalledWith(
|
||||
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||
'/api/v1/callbacks/reauthorize_page',
|
||||
{
|
||||
omniauth_token: 'ASFM131CSF@#@$',
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import contactAPI, { buildContactParams } from '../contacts';
|
||||
import ApiClient from '../ApiClient';
|
||||
import describeWithAPIMock from './apiSpecHelper';
|
||||
|
||||
describe('#ContactsAPI', () => {
|
||||
it('creates correct instance', () => {
|
||||
@@ -15,56 +14,67 @@ describe('#ContactsAPI', () => {
|
||||
expect(contactAPI).toHaveProperty('destroyAvatar');
|
||||
});
|
||||
|
||||
describeWithAPIMock('API calls', context => {
|
||||
describe('API calls', () => {
|
||||
const originalAxios = window.axios;
|
||||
const axiosMock = {
|
||||
post: jest.fn(() => Promise.resolve()),
|
||||
get: jest.fn(() => Promise.resolve()),
|
||||
patch: jest.fn(() => Promise.resolve()),
|
||||
delete: jest.fn(() => Promise.resolve()),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
window.axios = axiosMock;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
window.axios = originalAxios;
|
||||
});
|
||||
|
||||
it('#get', () => {
|
||||
contactAPI.get(1, 'name', 'customer-support');
|
||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||
expect(axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v1/contacts?include_contact_inboxes=false&page=1&sort=name&labels[]=customer-support'
|
||||
);
|
||||
});
|
||||
|
||||
it('#getConversations', () => {
|
||||
contactAPI.getConversations(1);
|
||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||
expect(axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v1/contacts/1/conversations'
|
||||
);
|
||||
});
|
||||
|
||||
it('#getContactableInboxes', () => {
|
||||
contactAPI.getContactableInboxes(1);
|
||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||
expect(axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v1/contacts/1/contactable_inboxes'
|
||||
);
|
||||
});
|
||||
|
||||
it('#getContactLabels', () => {
|
||||
contactAPI.getContactLabels(1);
|
||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v1/contacts/1/labels'
|
||||
);
|
||||
expect(axiosMock.get).toHaveBeenCalledWith('/api/v1/contacts/1/labels');
|
||||
});
|
||||
|
||||
it('#updateContactLabels', () => {
|
||||
const labels = ['support-query'];
|
||||
contactAPI.updateContactLabels(1, labels);
|
||||
expect(context.axiosMock.post).toHaveBeenCalledWith(
|
||||
'/api/v1/contacts/1/labels',
|
||||
{
|
||||
labels,
|
||||
}
|
||||
);
|
||||
expect(axiosMock.post).toHaveBeenCalledWith('/api/v1/contacts/1/labels', {
|
||||
labels,
|
||||
});
|
||||
});
|
||||
|
||||
it('#search', () => {
|
||||
contactAPI.search('leads', 1, 'date', 'customer-support');
|
||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||
expect(axiosMock.get).toHaveBeenCalledWith(
|
||||
'/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(
|
||||
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||
'/api/v1/contacts/1/destroy_custom_attributes',
|
||||
{
|
||||
custom_attributes: ['cloudCustomer'],
|
||||
@@ -75,7 +85,7 @@ describe('#ContactsAPI', () => {
|
||||
it('#importContacts', () => {
|
||||
const file = 'file';
|
||||
contactAPI.importContacts(file);
|
||||
expect(context.axiosMock.post).toHaveBeenCalledWith(
|
||||
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||
'/api/v1/contacts/import',
|
||||
expect.any(FormData),
|
||||
{
|
||||
@@ -96,7 +106,7 @@ describe('#ContactsAPI', () => {
|
||||
],
|
||||
};
|
||||
contactAPI.filter(1, 'name', queryPayload);
|
||||
expect(context.axiosMock.post).toHaveBeenCalledWith(
|
||||
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||
'/api/v1/contacts/filter?include_contact_inboxes=false&page=1&sort=name',
|
||||
queryPayload
|
||||
);
|
||||
@@ -104,7 +114,7 @@ describe('#ContactsAPI', () => {
|
||||
|
||||
it('#destroyAvatar', () => {
|
||||
contactAPI.destroyAvatar(1);
|
||||
expect(context.axiosMock.delete).toHaveBeenCalledWith(
|
||||
expect(axiosMock.delete).toHaveBeenCalledWith(
|
||||
'/api/v1/contacts/1/avatar'
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import conversationsAPI from '../conversations';
|
||||
import ApiClient from '../ApiClient';
|
||||
import describeWithAPIMock from './apiSpecHelper';
|
||||
|
||||
describe('#ConversationApi', () => {
|
||||
it('creates correct instance', () => {
|
||||
@@ -14,10 +13,26 @@ describe('#ConversationApi', () => {
|
||||
expect(conversationsAPI).toHaveProperty('updateLabels');
|
||||
});
|
||||
|
||||
describeWithAPIMock('API calls', context => {
|
||||
describe('API calls', () => {
|
||||
const originalAxios = window.axios;
|
||||
const axiosMock = {
|
||||
post: jest.fn(() => Promise.resolve()),
|
||||
get: jest.fn(() => Promise.resolve()),
|
||||
patch: jest.fn(() => Promise.resolve()),
|
||||
delete: jest.fn(() => Promise.resolve()),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
window.axios = axiosMock;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
window.axios = originalAxios;
|
||||
});
|
||||
|
||||
it('#getLabels', () => {
|
||||
conversationsAPI.getLabels(1);
|
||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||
expect(axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v1/conversations/1/labels'
|
||||
);
|
||||
});
|
||||
@@ -25,7 +40,7 @@ describe('#ConversationApi', () => {
|
||||
it('#updateLabels', () => {
|
||||
const labels = ['support-query'];
|
||||
conversationsAPI.updateLabels(1, labels);
|
||||
expect(context.axiosMock.post).toHaveBeenCalledWith(
|
||||
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||
'/api/v1/conversations/1/labels',
|
||||
{
|
||||
labels,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import csatReportsAPI from '../csatReports';
|
||||
import ApiClient from '../ApiClient';
|
||||
import describeWithAPIMock from './apiSpecHelper';
|
||||
|
||||
describe('#Reports API', () => {
|
||||
it('creates correct instance', () => {
|
||||
@@ -9,10 +8,26 @@ describe('#Reports API', () => {
|
||||
expect(csatReportsAPI).toHaveProperty('get');
|
||||
expect(csatReportsAPI).toHaveProperty('getMetrics');
|
||||
});
|
||||
describeWithAPIMock('API calls', context => {
|
||||
describe('API calls', () => {
|
||||
const originalAxios = window.axios;
|
||||
const axiosMock = {
|
||||
post: jest.fn(() => Promise.resolve()),
|
||||
get: jest.fn(() => Promise.resolve()),
|
||||
patch: jest.fn(() => Promise.resolve()),
|
||||
delete: jest.fn(() => Promise.resolve()),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
window.axios = axiosMock;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
window.axios = originalAxios;
|
||||
});
|
||||
|
||||
it('#get', () => {
|
||||
csatReportsAPI.get({ page: 1, from: 1622485800, to: 1623695400 });
|
||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||
expect(axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v1/csat_survey_responses',
|
||||
{
|
||||
params: {
|
||||
@@ -26,7 +41,7 @@ describe('#Reports API', () => {
|
||||
});
|
||||
it('#getMetrics', () => {
|
||||
csatReportsAPI.getMetrics({ from: 1622485800, to: 1623695400 });
|
||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||
expect(axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v1/csat_survey_responses/metrics',
|
||||
{
|
||||
params: { since: 1622485800, until: 1623695400 },
|
||||
@@ -39,7 +54,7 @@ describe('#Reports API', () => {
|
||||
to: 1623695400,
|
||||
user_ids: 1,
|
||||
});
|
||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||
expect(axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v1/csat_survey_responses/download',
|
||||
{
|
||||
params: {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import conversationAPI from '../../inbox/conversation';
|
||||
import ApiClient from '../../ApiClient';
|
||||
import describeWithAPIMock from '../apiSpecHelper';
|
||||
|
||||
describe('#ConversationAPI', () => {
|
||||
it('creates correct instance', () => {
|
||||
@@ -22,7 +21,23 @@ describe('#ConversationAPI', () => {
|
||||
expect(conversationAPI).toHaveProperty('filter');
|
||||
});
|
||||
|
||||
describeWithAPIMock('API calls', context => {
|
||||
describe('API calls', () => {
|
||||
const originalAxios = window.axios;
|
||||
const axiosMock = {
|
||||
post: jest.fn(() => Promise.resolve()),
|
||||
get: jest.fn(() => Promise.resolve()),
|
||||
patch: jest.fn(() => Promise.resolve()),
|
||||
delete: jest.fn(() => Promise.resolve()),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
window.axios = axiosMock;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
window.axios = originalAxios;
|
||||
});
|
||||
|
||||
it('#get conversations', () => {
|
||||
conversationAPI.get({
|
||||
inboxId: 1,
|
||||
@@ -32,19 +47,16 @@ describe('#ConversationAPI', () => {
|
||||
labels: [],
|
||||
teamId: 1,
|
||||
});
|
||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v1/conversations',
|
||||
{
|
||||
params: {
|
||||
inbox_id: 1,
|
||||
team_id: 1,
|
||||
status: 'open',
|
||||
assignee_type: 'me',
|
||||
page: 1,
|
||||
labels: [],
|
||||
},
|
||||
}
|
||||
);
|
||||
expect(axiosMock.get).toHaveBeenCalledWith('/api/v1/conversations', {
|
||||
params: {
|
||||
inbox_id: 1,
|
||||
team_id: 1,
|
||||
status: 'open',
|
||||
assignee_type: 'me',
|
||||
page: 1,
|
||||
labels: [],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('#search', () => {
|
||||
@@ -53,7 +65,7 @@ describe('#ConversationAPI', () => {
|
||||
page: 1,
|
||||
});
|
||||
|
||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||
expect(axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v1/conversations/search',
|
||||
{
|
||||
params: {
|
||||
@@ -66,7 +78,7 @@ describe('#ConversationAPI', () => {
|
||||
|
||||
it('#toggleStatus', () => {
|
||||
conversationAPI.toggleStatus({ conversationId: 12, status: 'online' });
|
||||
expect(context.axiosMock.post).toHaveBeenCalledWith(
|
||||
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||
`/api/v1/conversations/12/toggle_status`,
|
||||
{
|
||||
status: 'online',
|
||||
@@ -77,7 +89,7 @@ describe('#ConversationAPI', () => {
|
||||
|
||||
it('#assignAgent', () => {
|
||||
conversationAPI.assignAgent({ conversationId: 12, agentId: 34 });
|
||||
expect(context.axiosMock.post).toHaveBeenCalledWith(
|
||||
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||
`/api/v1/conversations/12/assignments?assignee_id=34`,
|
||||
{}
|
||||
);
|
||||
@@ -85,7 +97,7 @@ describe('#ConversationAPI', () => {
|
||||
|
||||
it('#assignTeam', () => {
|
||||
conversationAPI.assignTeam({ conversationId: 12, teamId: 1 });
|
||||
expect(context.axiosMock.post).toHaveBeenCalledWith(
|
||||
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||
`/api/v1/conversations/12/assignments`,
|
||||
{
|
||||
team_id: 1,
|
||||
@@ -95,7 +107,7 @@ describe('#ConversationAPI', () => {
|
||||
|
||||
it('#markMessageRead', () => {
|
||||
conversationAPI.markMessageRead({ id: 12 });
|
||||
expect(context.axiosMock.post).toHaveBeenCalledWith(
|
||||
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||
`/api/v1/conversations/12/update_last_seen`
|
||||
);
|
||||
});
|
||||
@@ -105,7 +117,7 @@ describe('#ConversationAPI', () => {
|
||||
conversationId: 12,
|
||||
status: 'typing_on',
|
||||
});
|
||||
expect(context.axiosMock.post).toHaveBeenCalledWith(
|
||||
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||
`/api/v1/conversations/12/toggle_typing_status`,
|
||||
{
|
||||
typing_status: 'typing_on',
|
||||
@@ -115,14 +127,14 @@ describe('#ConversationAPI', () => {
|
||||
|
||||
it('#mute', () => {
|
||||
conversationAPI.mute(45);
|
||||
expect(context.axiosMock.post).toHaveBeenCalledWith(
|
||||
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||
'/api/v1/conversations/45/mute'
|
||||
);
|
||||
});
|
||||
|
||||
it('#unmute', () => {
|
||||
conversationAPI.unmute(45);
|
||||
expect(context.axiosMock.post).toHaveBeenCalledWith(
|
||||
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||
'/api/v1/conversations/45/unmute'
|
||||
);
|
||||
});
|
||||
@@ -135,18 +147,15 @@ describe('#ConversationAPI', () => {
|
||||
labels: [],
|
||||
teamId: 1,
|
||||
});
|
||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v1/conversations/meta',
|
||||
{
|
||||
params: {
|
||||
inbox_id: 1,
|
||||
team_id: 1,
|
||||
status: 'open',
|
||||
assignee_type: 'me',
|
||||
labels: [],
|
||||
},
|
||||
}
|
||||
);
|
||||
expect(axiosMock.get).toHaveBeenCalledWith('/api/v1/conversations/meta', {
|
||||
params: {
|
||||
inbox_id: 1,
|
||||
team_id: 1,
|
||||
status: 'open',
|
||||
assignee_type: 'me',
|
||||
labels: [],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('#sendEmailTranscript', () => {
|
||||
@@ -154,7 +163,7 @@ describe('#ConversationAPI', () => {
|
||||
conversationId: 45,
|
||||
email: 'john@acme.inc',
|
||||
});
|
||||
expect(context.axiosMock.post).toHaveBeenCalledWith(
|
||||
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||
'/api/v1/conversations/45/transcript',
|
||||
{
|
||||
email: 'john@acme.inc',
|
||||
@@ -167,7 +176,7 @@ describe('#ConversationAPI', () => {
|
||||
conversationId: 45,
|
||||
customAttributes: { order_d: '1001' },
|
||||
});
|
||||
expect(context.axiosMock.post).toHaveBeenCalledWith(
|
||||
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||
'/api/v1/conversations/45/custom_attributes',
|
||||
{
|
||||
custom_attributes: { order_d: '1001' },
|
||||
@@ -202,9 +211,7 @@ describe('#ConversationAPI', () => {
|
||||
},
|
||||
};
|
||||
conversationAPI.filter(payload);
|
||||
expect(
|
||||
context.axiosMock.post
|
||||
).toHaveBeenCalledWith(
|
||||
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||
'/api/v1/conversations/filter',
|
||||
payload.queryData,
|
||||
{ params: { page: payload.page } }
|
||||
@@ -213,7 +220,7 @@ describe('#ConversationAPI', () => {
|
||||
|
||||
it('#getAllAttachments', () => {
|
||||
conversationAPI.getAllAttachments(1);
|
||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||
expect(axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v1/conversations/1/attachments'
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import messageAPI, { buildCreatePayload } from '../../inbox/message';
|
||||
import ApiClient from '../../ApiClient';
|
||||
import describeWithAPIMock from '../apiSpecHelper';
|
||||
|
||||
describe('#ConversationAPI', () => {
|
||||
it('creates correct instance', () => {
|
||||
@@ -13,13 +12,29 @@ describe('#ConversationAPI', () => {
|
||||
expect(messageAPI).toHaveProperty('getPreviousMessages');
|
||||
});
|
||||
|
||||
describeWithAPIMock('API calls', context => {
|
||||
describe('API calls', () => {
|
||||
const originalAxios = window.axios;
|
||||
const axiosMock = {
|
||||
post: jest.fn(() => Promise.resolve()),
|
||||
get: jest.fn(() => Promise.resolve()),
|
||||
patch: jest.fn(() => Promise.resolve()),
|
||||
delete: jest.fn(() => Promise.resolve()),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
window.axios = axiosMock;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
window.axios = originalAxios;
|
||||
});
|
||||
|
||||
it('#getPreviousMessages', () => {
|
||||
messageAPI.getPreviousMessages({
|
||||
conversationId: 12,
|
||||
before: 4573,
|
||||
});
|
||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||
expect(axiosMock.get).toHaveBeenCalledWith(
|
||||
`/api/v1/conversations/12/messages`,
|
||||
{
|
||||
params: {
|
||||
@@ -35,7 +50,7 @@ describe('#ConversationAPI', () => {
|
||||
message: 'test content',
|
||||
echoId: 12,
|
||||
isPrivate: true,
|
||||
|
||||
contentAttributes: { in_reply_to: 12 },
|
||||
files: [new Blob(['test-content'], { type: 'application/pdf' })],
|
||||
});
|
||||
expect(formPayload).toBeInstanceOf(FormData);
|
||||
@@ -43,6 +58,10 @@ describe('#ConversationAPI', () => {
|
||||
expect(formPayload.get('echo_id')).toEqual('12');
|
||||
expect(formPayload.get('private')).toEqual('true');
|
||||
expect(formPayload.get('cc_emails')).toEqual('');
|
||||
expect(formPayload.get('bcc_emails')).toEqual('');
|
||||
expect(formPayload.get('content_attributes')).toEqual(
|
||||
'{"in_reply_to":12}'
|
||||
);
|
||||
});
|
||||
|
||||
it('builds object payload if file is not available', () => {
|
||||
@@ -58,8 +77,10 @@ describe('#ConversationAPI', () => {
|
||||
private: false,
|
||||
echo_id: 12,
|
||||
content_attributes: { in_reply_to: 12 },
|
||||
bcc_emails: '',
|
||||
cc_emails: '',
|
||||
bcc_emails: '',
|
||||
to_emails: '',
|
||||
template_params: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import inboxesAPI from '../inboxes';
|
||||
import ApiClient from '../ApiClient';
|
||||
import describeWithAPIMock from './apiSpecHelper';
|
||||
|
||||
describe('#InboxesAPI', () => {
|
||||
it('creates correct instance', () => {
|
||||
@@ -14,19 +13,32 @@ describe('#InboxesAPI', () => {
|
||||
expect(inboxesAPI).toHaveProperty('getAgentBot');
|
||||
expect(inboxesAPI).toHaveProperty('setAgentBot');
|
||||
});
|
||||
describeWithAPIMock('API calls', context => {
|
||||
|
||||
describe('API calls', () => {
|
||||
const originalAxios = window.axios;
|
||||
const axiosMock = {
|
||||
post: jest.fn(() => Promise.resolve()),
|
||||
get: jest.fn(() => Promise.resolve()),
|
||||
patch: jest.fn(() => Promise.resolve()),
|
||||
delete: jest.fn(() => Promise.resolve()),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
window.axios = axiosMock;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
window.axios = originalAxios;
|
||||
});
|
||||
|
||||
it('#getCampaigns', () => {
|
||||
inboxesAPI.getCampaigns(2);
|
||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v1/inboxes/2/campaigns'
|
||||
);
|
||||
expect(axiosMock.get).toHaveBeenCalledWith('/api/v1/inboxes/2/campaigns');
|
||||
});
|
||||
|
||||
it('#deleteInboxAvatar', () => {
|
||||
inboxesAPI.deleteInboxAvatar(2);
|
||||
expect(context.axiosMock.delete).toHaveBeenCalledWith(
|
||||
'/api/v1/inboxes/2/avatar'
|
||||
);
|
||||
expect(axiosMock.delete).toHaveBeenCalledWith('/api/v1/inboxes/2/avatar');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import integrationAPI from '../integrations';
|
||||
import ApiClient from '../ApiClient';
|
||||
import describeWithAPIMock from './apiSpecHelper';
|
||||
|
||||
describe('#integrationAPI', () => {
|
||||
it('creates correct instance', () => {
|
||||
@@ -16,11 +15,27 @@ describe('#integrationAPI', () => {
|
||||
expect(integrationAPI).toHaveProperty('listAllSlackChannels');
|
||||
expect(integrationAPI).toHaveProperty('deleteHook');
|
||||
});
|
||||
describeWithAPIMock('API calls', context => {
|
||||
describe('API calls', () => {
|
||||
const originalAxios = window.axios;
|
||||
const axiosMock = {
|
||||
post: jest.fn(() => Promise.resolve()),
|
||||
get: jest.fn(() => Promise.resolve()),
|
||||
patch: jest.fn(() => Promise.resolve()),
|
||||
delete: jest.fn(() => Promise.resolve()),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
window.axios = axiosMock;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
window.axios = originalAxios;
|
||||
});
|
||||
|
||||
it('#connectSlack', () => {
|
||||
const code = 'SDNFJNSDFNDSJN';
|
||||
integrationAPI.connectSlack(code);
|
||||
expect(context.axiosMock.post).toHaveBeenCalledWith(
|
||||
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||
'/api/v1/integrations/slack',
|
||||
{
|
||||
code,
|
||||
@@ -31,7 +46,7 @@ describe('#integrationAPI', () => {
|
||||
it('#updateSlack', () => {
|
||||
const updateObj = { referenceId: 'SDFSDGSVE' };
|
||||
integrationAPI.updateSlack(updateObj);
|
||||
expect(context.axiosMock.patch).toHaveBeenCalledWith(
|
||||
expect(axiosMock.patch).toHaveBeenCalledWith(
|
||||
'/api/v1/integrations/slack',
|
||||
{
|
||||
reference_id: updateObj.referenceId,
|
||||
@@ -41,16 +56,14 @@ describe('#integrationAPI', () => {
|
||||
|
||||
it('#listAllSlackChannels', () => {
|
||||
integrationAPI.listAllSlackChannels();
|
||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||
expect(axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v1/integrations/slack/list_all_channels'
|
||||
);
|
||||
});
|
||||
|
||||
it('#delete', () => {
|
||||
integrationAPI.delete(2);
|
||||
expect(context.axiosMock.delete).toHaveBeenCalledWith(
|
||||
'/api/v1/integrations/2'
|
||||
);
|
||||
expect(axiosMock.delete).toHaveBeenCalledWith('/api/v1/integrations/2');
|
||||
});
|
||||
|
||||
it('#createHook', () => {
|
||||
@@ -59,7 +72,7 @@ describe('#integrationAPI', () => {
|
||||
settings: { api_key: 'SDFSDGSVE' },
|
||||
};
|
||||
integrationAPI.createHook(hookData);
|
||||
expect(context.axiosMock.post).toHaveBeenCalledWith(
|
||||
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||
'/api/v1/integrations/hooks',
|
||||
hookData
|
||||
);
|
||||
@@ -67,7 +80,7 @@ describe('#integrationAPI', () => {
|
||||
|
||||
it('#deleteHook', () => {
|
||||
integrationAPI.deleteHook(2);
|
||||
expect(context.axiosMock.delete).toHaveBeenCalledWith(
|
||||
expect(axiosMock.delete).toHaveBeenCalledWith(
|
||||
'/api/v1/integrations/hooks/2'
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import DyteAPIClient from '../../integrations/dyte';
|
||||
import ApiClient from '../../ApiClient';
|
||||
import describeWithAPIMock from '../apiSpecHelper';
|
||||
|
||||
describe('#accountAPI', () => {
|
||||
it('creates correct instance', () => {
|
||||
@@ -9,10 +8,26 @@ describe('#accountAPI', () => {
|
||||
expect(DyteAPIClient).toHaveProperty('addParticipantToMeeting');
|
||||
});
|
||||
|
||||
describeWithAPIMock('createAMeeting', context => {
|
||||
describe('createAMeeting', () => {
|
||||
const originalAxios = window.axios;
|
||||
const axiosMock = {
|
||||
post: jest.fn(() => Promise.resolve()),
|
||||
get: jest.fn(() => Promise.resolve()),
|
||||
patch: jest.fn(() => Promise.resolve()),
|
||||
delete: jest.fn(() => Promise.resolve()),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
window.axios = axiosMock;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
window.axios = originalAxios;
|
||||
});
|
||||
|
||||
it('creates a valid request', () => {
|
||||
DyteAPIClient.createAMeeting(1);
|
||||
expect(context.axiosMock.post).toHaveBeenCalledWith(
|
||||
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||
'/api/v1/integrations/dyte/create_a_meeting',
|
||||
{
|
||||
conversation_id: 1,
|
||||
@@ -21,10 +36,26 @@ describe('#accountAPI', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describeWithAPIMock('addParticipantToMeeting', context => {
|
||||
describe('addParticipantToMeeting', () => {
|
||||
const originalAxios = window.axios;
|
||||
const axiosMock = {
|
||||
post: jest.fn(() => Promise.resolve()),
|
||||
get: jest.fn(() => Promise.resolve()),
|
||||
patch: jest.fn(() => Promise.resolve()),
|
||||
delete: jest.fn(() => Promise.resolve()),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
window.axios = axiosMock;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
window.axios = originalAxios;
|
||||
});
|
||||
|
||||
it('creates a valid request', () => {
|
||||
DyteAPIClient.addParticipantToMeeting(1);
|
||||
expect(context.axiosMock.post).toHaveBeenCalledWith(
|
||||
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||
'/api/v1/integrations/dyte/add_participant_to_meeting',
|
||||
{
|
||||
message_id: 1,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import notificationsAPI from '../notifications';
|
||||
import ApiClient from '../ApiClient';
|
||||
import describeWithAPIMock from './apiSpecHelper';
|
||||
|
||||
describe('#NotificationAPI', () => {
|
||||
it('creates correct instance', () => {
|
||||
@@ -11,31 +10,47 @@ describe('#NotificationAPI', () => {
|
||||
expect(notificationsAPI).toHaveProperty('read');
|
||||
expect(notificationsAPI).toHaveProperty('readAll');
|
||||
});
|
||||
describeWithAPIMock('API calls', context => {
|
||||
describe('API calls', () => {
|
||||
const originalAxios = window.axios;
|
||||
const axiosMock = {
|
||||
post: jest.fn(() => Promise.resolve()),
|
||||
get: jest.fn(() => Promise.resolve()),
|
||||
patch: jest.fn(() => Promise.resolve()),
|
||||
delete: jest.fn(() => Promise.resolve()),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
window.axios = axiosMock;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
window.axios = originalAxios;
|
||||
});
|
||||
|
||||
it('#get', () => {
|
||||
notificationsAPI.get(1);
|
||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||
expect(axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v1/notifications?page=1'
|
||||
);
|
||||
});
|
||||
|
||||
it('#getNotifications', () => {
|
||||
notificationsAPI.getNotifications(1);
|
||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||
expect(axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v1/notifications/1/notifications'
|
||||
);
|
||||
});
|
||||
|
||||
it('#getUnreadCount', () => {
|
||||
notificationsAPI.getUnreadCount();
|
||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||
expect(axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v1/notifications/unread_count'
|
||||
);
|
||||
});
|
||||
|
||||
it('#read', () => {
|
||||
notificationsAPI.read(48670, 'Conversation');
|
||||
expect(context.axiosMock.post).toHaveBeenCalledWith(
|
||||
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||
'/api/v1/notifications/read_all',
|
||||
{
|
||||
primary_actor_id: 'Conversation',
|
||||
@@ -46,7 +61,7 @@ describe('#NotificationAPI', () => {
|
||||
|
||||
it('#readAll', () => {
|
||||
notificationsAPI.readAll();
|
||||
expect(context.axiosMock.post).toHaveBeenCalledWith(
|
||||
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||
'/api/v1/notifications/read_all'
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import reportsAPI from '../reports';
|
||||
import ApiClient from '../ApiClient';
|
||||
import describeWithAPIMock from './apiSpecHelper';
|
||||
|
||||
describe('#Reports API', () => {
|
||||
it('creates correct instance', () => {
|
||||
@@ -18,14 +17,30 @@ describe('#Reports API', () => {
|
||||
expect(reportsAPI).toHaveProperty('getInboxReports');
|
||||
expect(reportsAPI).toHaveProperty('getTeamReports');
|
||||
});
|
||||
describeWithAPIMock('API calls', context => {
|
||||
describe('API calls', () => {
|
||||
const originalAxios = window.axios;
|
||||
const axiosMock = {
|
||||
post: jest.fn(() => Promise.resolve()),
|
||||
get: jest.fn(() => Promise.resolve()),
|
||||
patch: jest.fn(() => Promise.resolve()),
|
||||
delete: jest.fn(() => Promise.resolve()),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
window.axios = axiosMock;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
window.axios = originalAxios;
|
||||
});
|
||||
|
||||
it('#getAccountReports', () => {
|
||||
reportsAPI.getReports({
|
||||
metric: 'conversations_count',
|
||||
from: 1621103400,
|
||||
to: 1621621800,
|
||||
});
|
||||
expect(context.axiosMock.get).toHaveBeenCalledWith('/api/v2/reports', {
|
||||
expect(axiosMock.get).toHaveBeenCalledWith('/api/v2/reports', {
|
||||
params: {
|
||||
metric: 'conversations_count',
|
||||
since: 1621103400,
|
||||
@@ -38,20 +53,17 @@ describe('#Reports API', () => {
|
||||
|
||||
it('#getAccountSummary', () => {
|
||||
reportsAPI.getSummary(1621103400, 1621621800);
|
||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v2/reports/summary',
|
||||
{
|
||||
params: {
|
||||
business_hours: undefined,
|
||||
group_by: undefined,
|
||||
id: undefined,
|
||||
since: 1621103400,
|
||||
timezone_offset: -0,
|
||||
type: 'account',
|
||||
until: 1621621800,
|
||||
},
|
||||
}
|
||||
);
|
||||
expect(axiosMock.get).toHaveBeenCalledWith('/api/v2/reports/summary', {
|
||||
params: {
|
||||
business_hours: undefined,
|
||||
group_by: undefined,
|
||||
id: undefined,
|
||||
since: 1621103400,
|
||||
timezone_offset: -0,
|
||||
type: 'account',
|
||||
until: 1621621800,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('#getAgentReports', () => {
|
||||
@@ -60,60 +72,48 @@ describe('#Reports API', () => {
|
||||
to: 1621621800,
|
||||
businessHours: true,
|
||||
});
|
||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v2/reports/agents',
|
||||
{
|
||||
params: {
|
||||
since: 1621103400,
|
||||
until: 1621621800,
|
||||
business_hours: true,
|
||||
},
|
||||
}
|
||||
);
|
||||
expect(axiosMock.get).toHaveBeenCalledWith('/api/v2/reports/agents', {
|
||||
params: {
|
||||
since: 1621103400,
|
||||
until: 1621621800,
|
||||
business_hours: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('#getLabelReports', () => {
|
||||
reportsAPI.getLabelReports({ from: 1621103400, to: 1621621800 });
|
||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v2/reports/labels',
|
||||
{
|
||||
params: {
|
||||
since: 1621103400,
|
||||
until: 1621621800,
|
||||
},
|
||||
}
|
||||
);
|
||||
expect(axiosMock.get).toHaveBeenCalledWith('/api/v2/reports/labels', {
|
||||
params: {
|
||||
since: 1621103400,
|
||||
until: 1621621800,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('#getInboxReports', () => {
|
||||
reportsAPI.getInboxReports({ from: 1621103400, to: 1621621800 });
|
||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v2/reports/inboxes',
|
||||
{
|
||||
params: {
|
||||
since: 1621103400,
|
||||
until: 1621621800,
|
||||
},
|
||||
}
|
||||
);
|
||||
expect(axiosMock.get).toHaveBeenCalledWith('/api/v2/reports/inboxes', {
|
||||
params: {
|
||||
since: 1621103400,
|
||||
until: 1621621800,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('#getTeamReports', () => {
|
||||
reportsAPI.getTeamReports({ from: 1621103400, to: 1621621800 });
|
||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v2/reports/teams',
|
||||
{
|
||||
params: {
|
||||
since: 1621103400,
|
||||
until: 1621621800,
|
||||
},
|
||||
}
|
||||
);
|
||||
expect(axiosMock.get).toHaveBeenCalledWith('/api/v2/reports/teams', {
|
||||
params: {
|
||||
since: 1621103400,
|
||||
until: 1621621800,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('#getConversationMetric', () => {
|
||||
reportsAPI.getConversationMetric('account');
|
||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||
expect(axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v2/reports/conversations',
|
||||
{
|
||||
params: {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import teamsAPI from '../teams';
|
||||
import ApiClient from '../ApiClient';
|
||||
import describeWithAPIMock from './apiSpecHelper';
|
||||
|
||||
describe('#TeamsAPI', () => {
|
||||
it('creates correct instance', () => {
|
||||
@@ -14,17 +13,33 @@ describe('#TeamsAPI', () => {
|
||||
expect(teamsAPI).toHaveProperty('addAgents');
|
||||
expect(teamsAPI).toHaveProperty('updateAgents');
|
||||
});
|
||||
describeWithAPIMock('API calls', context => {
|
||||
describe('API calls', () => {
|
||||
const originalAxios = window.axios;
|
||||
const axiosMock = {
|
||||
post: jest.fn(() => Promise.resolve()),
|
||||
get: jest.fn(() => Promise.resolve()),
|
||||
patch: jest.fn(() => Promise.resolve()),
|
||||
delete: jest.fn(() => Promise.resolve()),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
window.axios = axiosMock;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
window.axios = originalAxios;
|
||||
});
|
||||
|
||||
it('#getAgents', () => {
|
||||
teamsAPI.getAgents({ teamId: 1 });
|
||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||
expect(axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v1/teams/1/team_members'
|
||||
);
|
||||
});
|
||||
|
||||
it('#addAgents', () => {
|
||||
teamsAPI.addAgents({ teamId: 1, agentsList: { user_ids: [1, 10, 21] } });
|
||||
expect(context.axiosMock.post).toHaveBeenCalledWith(
|
||||
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||
'/api/v1/teams/1/team_members',
|
||||
{
|
||||
user_ids: { user_ids: [1, 10, 21] },
|
||||
@@ -38,7 +53,7 @@ describe('#TeamsAPI', () => {
|
||||
teamId: 1,
|
||||
agentsList,
|
||||
});
|
||||
expect(context.axiosMock.patch).toHaveBeenCalledWith(
|
||||
expect(axiosMock.patch).toHaveBeenCalledWith(
|
||||
'/api/v1/teams/1/team_members',
|
||||
{
|
||||
user_ids: agentsList,
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
@import '~vue2-datepicker/scss/index';
|
||||
|
||||
.mx-datepicker-popup {
|
||||
@apply z-[99999];
|
||||
}
|
||||
|
||||
.date-picker {
|
||||
&.no-margin {
|
||||
.mx-input {
|
||||
@@ -29,10 +25,52 @@
|
||||
.mx-input[readonly] {
|
||||
@apply bg-white dark:bg-slate-900 cursor-pointer;
|
||||
}
|
||||
|
||||
.mx-icon-calendar {
|
||||
@apply dark:text-slate-500;
|
||||
}
|
||||
}
|
||||
|
||||
.mx-calendar-content .cell:hover {
|
||||
@apply bg-slate-75 dark:bg-slate-700 text-slate-900 dark:text-slate-100;
|
||||
.mx-datepicker-main {
|
||||
@apply border-0 bg-white dark:bg-slate-800;
|
||||
|
||||
.cell {
|
||||
&.disabled {
|
||||
@apply bg-slate-25 dark:bg-slate-900 text-slate-200 dark:text-slate-300;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&.hover-in-range,
|
||||
&.in-range {
|
||||
@apply bg-slate-75 dark:bg-slate-700 text-slate-900 dark:text-slate-100;
|
||||
}
|
||||
}
|
||||
|
||||
.mx-time {
|
||||
@apply border-0 bg-white dark:bg-slate-800;
|
||||
|
||||
.mx-time-header {
|
||||
@apply border-0;
|
||||
}
|
||||
|
||||
.mx-time-item {
|
||||
&.disabled {
|
||||
@apply bg-slate-25 dark:bg-slate-900;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@apply bg-slate-75 dark:bg-slate-700;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.today {
|
||||
@apply font-semibold;
|
||||
}
|
||||
}
|
||||
|
||||
.mx-datepicker-popup {
|
||||
@apply z-[99999];
|
||||
}
|
||||
|
||||
.mx-datepicker-inline {
|
||||
@@ -41,24 +79,4 @@
|
||||
.mx-calendar {
|
||||
@apply w-full;
|
||||
}
|
||||
|
||||
.cell.disabled {
|
||||
@apply bg-slate-25 dark:bg-slate-900 text-slate-200 dark:text-slate-300;
|
||||
}
|
||||
|
||||
.mx-time-item.disabled {
|
||||
@apply bg-slate-25 dark:bg-slate-900;
|
||||
}
|
||||
|
||||
.today {
|
||||
@apply font-semibold;
|
||||
}
|
||||
|
||||
.mx-datepicker-main {
|
||||
@apply border-0 bg-white dark:bg-slate-800;
|
||||
}
|
||||
|
||||
.mx-time-header {
|
||||
@apply border-0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,3 +16,23 @@
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.integration-hooks {
|
||||
.formulate-input[data-type='checkbox'] {
|
||||
.formulate-input-wrapper {
|
||||
@apply flex;
|
||||
|
||||
.formulate-input-element {
|
||||
@apply pr-2;
|
||||
|
||||
input {
|
||||
@apply mb-0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.formulate-input-element-decorator {
|
||||
@apply hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,15 +149,11 @@
|
||||
}
|
||||
|
||||
.multiselect-wrap--small {
|
||||
.multiselect__tags,
|
||||
.multiselect__input {
|
||||
@apply items-center flex;
|
||||
}
|
||||
|
||||
.multiselect__tags,
|
||||
.multiselect__input,
|
||||
.multiselect {
|
||||
@apply bg-white dark:bg-slate-900 text-slate-800 dark:text-slate-100 rounded-[5px] text-sm h-10 min-h-[2.5rem];
|
||||
@apply bg-white dark:bg-slate-900 text-slate-800 dark:text-slate-100 rounded-[5px] text-sm min-h-[2.5rem];
|
||||
}
|
||||
|
||||
.multiselect__input {
|
||||
@@ -165,11 +161,15 @@
|
||||
}
|
||||
|
||||
.multiselect__single {
|
||||
@apply items-center flex m-0 text-sm max-h-[2.375rem] text-slate-800 dark:text-slate-100 bg-white dark:bg-slate-900 py-1 px-0.5;
|
||||
@apply items-center flex m-0 text-sm max-h-[2.375rem] text-slate-800 dark:text-slate-100 bg-white dark:bg-slate-900 py-3 px-0.5;
|
||||
}
|
||||
|
||||
.multiselect__placeholder {
|
||||
@apply m-0 py-1 px-0.5;
|
||||
@apply m-0 py-2 px-0.5;
|
||||
}
|
||||
|
||||
.multiselect__tag {
|
||||
@apply py-[6px] my-[1px];
|
||||
}
|
||||
|
||||
.multiselect__select {
|
||||
@@ -181,9 +181,6 @@
|
||||
@apply bg-transparent;
|
||||
}
|
||||
|
||||
.multiselect__tags-wrap {
|
||||
@apply flex-shrink-0;
|
||||
}
|
||||
}
|
||||
|
||||
.multiselect-wrap--medium {
|
||||
|
||||
@@ -34,26 +34,26 @@
|
||||
}
|
||||
|
||||
&.hollow {
|
||||
@apply border border-slate-200 dark:border-slate-600 text-woot-700 dark:text-woot-100 hover:bg-woot-50 dark:hover:bg-woot-900;
|
||||
@apply border border-woot-500 dark:border-woot-500 text-woot-500 dark:text-woot-500 hover:bg-woot-50 dark:hover:bg-woot-900;
|
||||
|
||||
&.secondary {
|
||||
@apply text-slate-700 border-slate-200 dark:border-slate-600 dark:text-slate-100 hover:bg-slate-50 dark:hover:bg-slate-700;
|
||||
}
|
||||
|
||||
&.success {
|
||||
@apply text-green-700 dark:text-green-100 hover:bg-green-50 dark:hover:bg-green-800;
|
||||
@apply text-green-700 dark:text-green-400 border-green-100 dark:border-green-600 hover:bg-green-50 dark:hover:bg-green-800;
|
||||
}
|
||||
|
||||
&.alert {
|
||||
@apply text-red-700 dark:text-red-100 hover:bg-red-50 dark:hover:bg-red-800;
|
||||
@apply text-red-700 dark:text-red-400 border-red-100 dark:border-red-600 hover:bg-red-50 dark:hover:bg-red-800;
|
||||
}
|
||||
|
||||
&.warning {
|
||||
@apply text-yellow-700 dark:text-yellow-100 hover:bg-yellow-50 dark:hover:bg-yellow-800;
|
||||
@apply text-yellow-600 dark:text-yellow-600 border-yellow-600 dark:border-yellow-700 hover:bg-yellow-50 dark:hover:bg-yellow-800;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@apply bg-slate-75 dark:bg-slate-900 border-slate-100 dark:border-slate-700;
|
||||
@apply bg-woot-75 dark:bg-woot-800 border-slate-100 dark:border-woot-600 dark:text-woot-400;
|
||||
|
||||
&.secondary {
|
||||
@apply border-slate-100 dark:border-slate-700 text-slate-800 dark:text-slate-100;
|
||||
@@ -68,7 +68,7 @@
|
||||
}
|
||||
|
||||
&.warning {
|
||||
@apply border-slate-100 dark:border-slate-700 text-yellow-700 dark:text-yellow-700;
|
||||
@apply border-slate-100 dark:border-slate-700 text-yellow-700 dark:text-yellow-500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,6 +152,10 @@
|
||||
|
||||
&.is-image {
|
||||
@apply rounded-lg;
|
||||
|
||||
.message__mail-head {
|
||||
@apply px-4 py-2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,10 +4,6 @@
|
||||
@apply flex items-center justify-center bg-modal dark:bg-modal z-[9990] h-full left-0 fixed top-0 w-full;
|
||||
}
|
||||
|
||||
.modal--close {
|
||||
@apply absolute right-2 rtl:right-[unset] rtl:left-2 top-2;
|
||||
}
|
||||
|
||||
.page-top-bar {
|
||||
@apply px-8 pt-9 pb-0;
|
||||
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
<div
|
||||
class="flex items-center justify-between py-0 px-4"
|
||||
:class="{
|
||||
'pb-3 border-b border-slate-75 dark:border-slate-700': hasAppliedFiltersOrActiveFolders,
|
||||
'pb-3 border-b border-slate-75 dark:border-slate-700':
|
||||
hasAppliedFiltersOrActiveFolders,
|
||||
}"
|
||||
>
|
||||
<div class="flex max-w-[85%] justify-center items-center">
|
||||
@@ -24,9 +25,7 @@
|
||||
v-if="!hasAppliedFiltersOrActiveFolders"
|
||||
class="p-1 my-0.5 mx-1 rounded-md capitalize bg-slate-50 dark:bg-slate-800 text-xxs text-slate-600 dark:text-slate-300"
|
||||
>
|
||||
{{
|
||||
this.$t(`CHAT_LIST.CHAT_STATUS_FILTER_ITEMS.${activeStatus}.TEXT`)
|
||||
}}
|
||||
{{ $t(`CHAT_LIST.CHAT_STATUS_FILTER_ITEMS.${activeStatus}.TEXT`) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
@@ -208,6 +207,7 @@ import DeleteCustomViews from 'dashboard/routes/dashboard/customviews/DeleteCust
|
||||
import ConversationBulkActions from './widgets/conversation/conversationBulkActions/Index.vue';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import filterMixin from 'shared/mixins/filterMixin';
|
||||
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
||||
import languages from 'dashboard/components/widgets/conversation/advancedFilterItems/languages';
|
||||
import countries from 'shared/constants/countries';
|
||||
import { generateValuesForEditCustomViews } from 'dashboard/helper/customViewsHelper';
|
||||
@@ -239,6 +239,7 @@ export default {
|
||||
eventListenerMixins,
|
||||
alertMixin,
|
||||
filterMixin,
|
||||
uiSettingsMixin,
|
||||
],
|
||||
props: {
|
||||
conversationInbox: {
|
||||
@@ -409,8 +410,12 @@ export default {
|
||||
},
|
||||
conversationListPagination() {
|
||||
const conversationsPerPage = 25;
|
||||
const hasChatsOnView =
|
||||
this.chatsOnView &&
|
||||
Array.isArray(this.chatsOnView) &&
|
||||
!this.chatsOnView.length;
|
||||
const isNoFiltersOrFoldersAndChatListNotEmpty =
|
||||
!this.hasAppliedFiltersOrActiveFolders && this.chatsOnView !== [];
|
||||
!this.hasAppliedFiltersOrActiveFolders && hasChatsOnView;
|
||||
const isUnderPerPage =
|
||||
this.chatsOnView.length < conversationsPerPage &&
|
||||
this.activeAssigneeTabCount < conversationsPerPage &&
|
||||
@@ -516,6 +521,7 @@ export default {
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.setFiltersFromUISettings();
|
||||
this.$store.dispatch('setChatStatusFilter', this.activeStatus);
|
||||
this.$store.dispatch('setChatSortFilter', this.activeSortBy);
|
||||
this.resetAndFetchData();
|
||||
@@ -545,6 +551,12 @@ export default {
|
||||
this.$store.dispatch('customViews/update', payloadData);
|
||||
this.closeAdvanceFiltersModal();
|
||||
},
|
||||
setFiltersFromUISettings() {
|
||||
const { conversations_filter_by: filterBy = {} } = this.uiSettings;
|
||||
const { status, order_by: orderBy } = filterBy;
|
||||
this.activeStatus = status || wootConstants.STATUS_TYPE.OPEN;
|
||||
this.activeSortBy = orderBy || wootConstants.SORT_BY_TYPE.LATEST;
|
||||
},
|
||||
onClickOpenAddFoldersModal() {
|
||||
this.showAddFoldersModal = true;
|
||||
},
|
||||
@@ -642,10 +654,8 @@ export default {
|
||||
},
|
||||
handleKeyEvents(e) {
|
||||
if (hasPressedAltAndJKey(e)) {
|
||||
const {
|
||||
allConversations,
|
||||
activeConversationIndex,
|
||||
} = this.getKeyboardListenerParams();
|
||||
const { allConversations, activeConversationIndex } =
|
||||
this.getKeyboardListenerParams();
|
||||
if (activeConversationIndex === -1) {
|
||||
allConversations[0].click();
|
||||
}
|
||||
|
||||
@@ -13,10 +13,10 @@
|
||||
/>
|
||||
<div class="flex flex-row justify-end gap-2 py-2 px-0 w-full">
|
||||
<woot-button variant="clear" @click.prevent="onClose">
|
||||
{{ this.$t('CONVERSATION.CUSTOM_SNOOZE.CANCEL') }}
|
||||
{{ $t('CONVERSATION.CUSTOM_SNOOZE.CANCEL') }}
|
||||
</woot-button>
|
||||
<woot-button>
|
||||
{{ this.$t('CONVERSATION.CUSTOM_SNOOZE.APPLY') }}
|
||||
{{ $t('CONVERSATION.CUSTOM_SNOOZE.APPLY') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="text--container">
|
||||
<woot-button size="small" class=" button--text" @click="onCopy">
|
||||
<woot-button size="small" class="button--text" @click="onCopy">
|
||||
{{ $t('COMPONENTS.CODE.BUTTON_TEXT') }}
|
||||
</woot-button>
|
||||
<woot-button
|
||||
|
||||
@@ -4,15 +4,19 @@
|
||||
v-if="show"
|
||||
:class="modalClassName"
|
||||
transition="modal"
|
||||
@click="onBackDropClick"
|
||||
@mousedown="handleMouseDown"
|
||||
>
|
||||
<div :class="modalContainerClassName" @click.stop>
|
||||
<div
|
||||
:class="modalContainerClassName"
|
||||
@mouse.stop
|
||||
@mousedown="event => event.stopPropagation()"
|
||||
>
|
||||
<woot-button
|
||||
v-if="showCloseButton"
|
||||
color-scheme="secondary"
|
||||
icon="dismiss"
|
||||
variant="clear"
|
||||
class="modal--close"
|
||||
class="absolute ltr:right-2 rtl:left-2 top-2 z-10"
|
||||
@click="close"
|
||||
/>
|
||||
<slot />
|
||||
@@ -50,6 +54,11 @@ export default {
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
mousedDownOnBackdrop: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
modalContainerClassName() {
|
||||
let className =
|
||||
@@ -66,9 +75,9 @@ export default {
|
||||
'right-aligned': 'right-aligned',
|
||||
};
|
||||
|
||||
return `modal-mask skip-context-menu ${modalClassNameMap[
|
||||
this.modalType
|
||||
] || ''}`;
|
||||
return `modal-mask skip-context-menu ${
|
||||
modalClassNameMap[this.modalType] || ''
|
||||
}`;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
@@ -77,13 +86,22 @@ export default {
|
||||
this.onClose();
|
||||
}
|
||||
});
|
||||
|
||||
document.body.addEventListener('mouseup', this.onMouseUp);
|
||||
},
|
||||
beforeDestroy() {
|
||||
document.body.removeEventListener('mouseup', this.onMouseUp);
|
||||
},
|
||||
methods: {
|
||||
handleMouseDown() {
|
||||
this.mousedDownOnBackdrop = true;
|
||||
},
|
||||
close() {
|
||||
this.onClose();
|
||||
},
|
||||
onBackDropClick() {
|
||||
if (this.closeOnBackdropClick) {
|
||||
onMouseUp() {
|
||||
if (this.mousedDownOnBackdrop) {
|
||||
this.mousedDownOnBackdrop = false;
|
||||
this.onClose();
|
||||
}
|
||||
},
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
<div
|
||||
class="ml-0 mr-0 flex pt-0 pr-4 pb-4 pl-0"
|
||||
:class="{
|
||||
'pt-4 border-b border-solid border-slate-50 dark:border-slate-700/30': showBorder,
|
||||
'pt-4 border-b border-solid border-slate-50 dark:border-slate-700/30':
|
||||
showBorder,
|
||||
}"
|
||||
>
|
||||
<div class="w-[30%] min-w-0 max-w-[30%] pr-12">
|
||||
|
||||
@@ -72,10 +72,8 @@ export default {
|
||||
const { custom_attributes: subscription } = account;
|
||||
if (!subscription) return EMPTY_SUBSCRIPTION_INFO;
|
||||
|
||||
const {
|
||||
subscription_status: status,
|
||||
subscription_ends_on: endsOn,
|
||||
} = subscription;
|
||||
const { subscription_status: status, subscription_ends_on: endsOn } =
|
||||
subscription;
|
||||
|
||||
return { status, endsOn: new Date(endsOn) };
|
||||
},
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const semver = require('semver');
|
||||
import semver from 'semver';
|
||||
|
||||
export const hasAnUpdateAvailable = (latestVersion, currentVersion) => {
|
||||
if (!semver.valid(latestVersion)) {
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
:is-loading="isLoading"
|
||||
@click="onCmdResolveConversation"
|
||||
>
|
||||
{{ this.$t('CONVERSATION.HEADER.RESOLVE_ACTION') }}
|
||||
{{ $t('CONVERSATION.HEADER.RESOLVE_ACTION') }}
|
||||
</woot-button>
|
||||
<woot-button
|
||||
v-else-if="isResolved"
|
||||
@@ -21,7 +21,7 @@
|
||||
:is-loading="isLoading"
|
||||
@click="onCmdOpenConversation"
|
||||
>
|
||||
{{ this.$t('CONVERSATION.HEADER.REOPEN_ACTION') }}
|
||||
{{ $t('CONVERSATION.HEADER.REOPEN_ACTION') }}
|
||||
</woot-button>
|
||||
<woot-button
|
||||
v-else-if="showOpenButton"
|
||||
@@ -31,7 +31,7 @@
|
||||
:is-loading="isLoading"
|
||||
@click="onCmdOpenConversation"
|
||||
>
|
||||
{{ this.$t('CONVERSATION.HEADER.OPEN_ACTION') }}
|
||||
{{ $t('CONVERSATION.HEADER.OPEN_ACTION') }}
|
||||
</woot-button>
|
||||
<woot-button
|
||||
v-if="showAdditionalActions"
|
||||
@@ -57,7 +57,7 @@
|
||||
icon="snooze"
|
||||
@click="() => openSnoozeModal()"
|
||||
>
|
||||
{{ this.$t('CONVERSATION.RESOLVE_DROPDOWN.SNOOZE_UNTIL') }}
|
||||
{{ $t('CONVERSATION.RESOLVE_DROPDOWN.SNOOZE_UNTIL') }}
|
||||
</woot-button>
|
||||
</woot-dropdown-item>
|
||||
<woot-dropdown-item v-if="!isPending">
|
||||
@@ -68,7 +68,7 @@
|
||||
icon="book-clock"
|
||||
@click="() => toggleStatus(STATUS_TYPE.PENDING)"
|
||||
>
|
||||
{{ this.$t('CONVERSATION.RESOLVE_DROPDOWN.MARK_PENDING') }}
|
||||
{{ $t('CONVERSATION.RESOLVE_DROPDOWN.MARK_PENDING') }}
|
||||
</woot-button>
|
||||
</woot-dropdown-item>
|
||||
</woot-dropdown-menu>
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
@key-shortcut-modal="toggleKeyShortcutModal"
|
||||
@open-notification-panel="openNotificationPanel"
|
||||
/>
|
||||
|
||||
<secondary-sidebar
|
||||
v-if="showSecondarySidebar"
|
||||
:class="sidebarClassName"
|
||||
@@ -114,7 +113,12 @@ export default {
|
||||
if (!isAvailableForTheUser) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
menuItem.alwaysVisibleOnChatwootInstances &&
|
||||
!this.isACustomBrandedInstance
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (menuItem.featureFlag) {
|
||||
return this.isFeatureEnabledonAccount(
|
||||
this.accountId,
|
||||
|
||||
@@ -42,6 +42,7 @@ const primaryMenuItems = accountId => [
|
||||
key: 'helpcenter',
|
||||
label: 'HELP_CENTER.TITLE',
|
||||
featureFlag: FEATURE_FLAGS.HELP_CENTER,
|
||||
alwaysVisibleOnChatwootInstances: true,
|
||||
toState: frontendURL(`accounts/${accountId}/portals`),
|
||||
toStateName: 'default_portal_articles',
|
||||
roles: ['administrator'],
|
||||
|
||||
@@ -34,8 +34,8 @@
|
||||
<woot-submit-button
|
||||
:disabled="
|
||||
$v.accountName.$invalid ||
|
||||
$v.accountName.$invalid ||
|
||||
uiFlags.isCreating
|
||||
$v.accountName.$invalid ||
|
||||
uiFlags.isCreating
|
||||
"
|
||||
:button-text="$t('CREATE_ACCOUNT.FORM.SUBMIT')"
|
||||
:loading="uiFlags.isCreating"
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
<button
|
||||
class="text-slate-600 dark:text-slate-100 w-10 h-10 my-2 flex items-center justify-center rounded-lg hover:bg-slate-25 dark:hover:bg-slate-700 dark:hover:text-slate-100 hover:text-slate-600 relative"
|
||||
:class="{
|
||||
'bg-woot-50 dark:bg-slate-800 text-woot-500 hover:bg-woot-50': isNotificationPanelActive,
|
||||
'bg-woot-50 dark:bg-slate-800 text-woot-500 hover:bg-woot-50':
|
||||
isNotificationPanelActive,
|
||||
}"
|
||||
@click="openNotificationPanel"
|
||||
>
|
||||
|
||||
@@ -136,6 +136,7 @@ export default {
|
||||
toStateName: 'labels_list',
|
||||
showModalForNewItem: true,
|
||||
modalName: 'AddLabel',
|
||||
dataTestid: 'sidebar-new-label-button',
|
||||
children: this.labels.map(label => ({
|
||||
id: label.id,
|
||||
label: label.title,
|
||||
|
||||
+4
-2
@@ -9,7 +9,8 @@
|
||||
class="font-medium h-7 my-1 hover:bg-slate-25 hover:text-bg-50 flex items-center px-2 rounded-md dark:hover:bg-slate-800"
|
||||
:class="{
|
||||
'bg-woot-25 dark:bg-slate-800': isActive,
|
||||
'text-ellipsis overflow-hidden whitespace-nowrap max-w-full': shouldTruncate,
|
||||
'text-ellipsis overflow-hidden whitespace-nowrap max-w-full':
|
||||
shouldTruncate,
|
||||
}"
|
||||
@click="navigate"
|
||||
>
|
||||
@@ -44,7 +45,8 @@
|
||||
class="text-sm text-slate-700 dark:text-slate-100"
|
||||
:class="{
|
||||
'text-woot-500 dark:text-woot-500': isActive,
|
||||
'text-ellipsis overflow-hidden whitespace-nowrap max-w-full': shouldTruncate,
|
||||
'text-ellipsis overflow-hidden whitespace-nowrap max-w-full':
|
||||
shouldTruncate,
|
||||
}"
|
||||
>
|
||||
{{ label }}
|
||||
|
||||
@@ -35,7 +35,8 @@
|
||||
:class="{
|
||||
'text-slate-300 dark:text-slate-600': isCountZero && !isActiveView,
|
||||
'text-slate-600 dark:text-slate-50': !isCountZero && !isActiveView,
|
||||
'bg-woot-75 dark:bg-woot-200 text-woot-600 dark:text-woot-600': isActiveView,
|
||||
'bg-woot-75 dark:bg-woot-200 text-woot-600 dark:text-woot-600':
|
||||
isActiveView,
|
||||
'bg-slate-50 dark:bg-slate-700': !isActiveView,
|
||||
}"
|
||||
>
|
||||
@@ -77,6 +78,7 @@
|
||||
variant="clear"
|
||||
color-scheme="secondary"
|
||||
icon="add"
|
||||
:data-testid="menuItem.dataTestid"
|
||||
@click="e => newLinkClick(e, navigate)"
|
||||
>
|
||||
{{ $t(`SIDEBAR.${menuItem.newLinkTag}`) }}
|
||||
|
||||
@@ -53,16 +53,24 @@ export default {
|
||||
const createdTimeDiff = Date.now() - this.createdAtTimestamp * 1000;
|
||||
const isBeforeAMonth = createdTimeDiff > DAY_IN_MILLI_SECONDS * 30;
|
||||
return !isBeforeAMonth
|
||||
? `Created ${this.createdAtTimeAgo}`
|
||||
: `Created at: ${this.dateFormat(this.createdAtTimestamp)}`;
|
||||
? `${this.$t('CHAT_LIST.CHAT_TIME_STAMP.CREATED.LATEST')} ${
|
||||
this.createdAtTimeAgo
|
||||
}`
|
||||
: `${this.$t(
|
||||
'CHAT_LIST.CHAT_TIME_STAMP.CREATED.OLDEST'
|
||||
)} ${this.dateFormat(this.createdAtTimestamp)}`;
|
||||
},
|
||||
lastActivity() {
|
||||
const lastActivityTimeDiff =
|
||||
Date.now() - this.lastActivityTimestamp * 1000;
|
||||
const isNotActive = lastActivityTimeDiff > DAY_IN_MILLI_SECONDS * 30;
|
||||
return !isNotActive
|
||||
? `Last activity ${this.lastActivityAtTimeAgo}`
|
||||
: `Last activity: ${this.dateFormat(this.lastActivityTimestamp)}`;
|
||||
? `${this.$t('CHAT_LIST.CHAT_TIME_STAMP.LAST_ACTIVITY.ACTIVE')} ${
|
||||
this.lastActivityAtTimeAgo
|
||||
}`
|
||||
: `${this.$t(
|
||||
'CHAT_LIST.CHAT_TIME_STAMP.LAST_ACTIVITY.NOT_ACTIVE'
|
||||
)} ${this.dateFormat(this.lastActivityTimestamp)}`;
|
||||
},
|
||||
tooltipText() {
|
||||
return `${this.createdAt}
|
||||
|
||||
@@ -61,6 +61,7 @@ export default {
|
||||
computed: {
|
||||
...mapGetters({
|
||||
currentChat: 'getSelectedChat',
|
||||
isAChatwootInstance: 'globalConfig/isAChatwootInstance',
|
||||
}),
|
||||
isAICTAModalDismissed() {
|
||||
return this.uiSettings.is_open_ai_cta_modal_dismissed;
|
||||
@@ -70,7 +71,8 @@ export default {
|
||||
return (
|
||||
this.isAdmin &&
|
||||
!this.isAIIntegrationEnabled &&
|
||||
!this.isAICTAModalDismissed
|
||||
!this.isAICTAModalDismissed &&
|
||||
this.isAChatwootInstance
|
||||
);
|
||||
},
|
||||
// Display a AI CTA button for agents and other admins who have not yet opened the AI assistance modal.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="animation-container margin-top-1">
|
||||
<div class="ai-typing--wrap ">
|
||||
<div class="ai-typing--wrap">
|
||||
<fluent-icon icon="wand" size="14" class="ai-typing--icon" />
|
||||
<label>
|
||||
{{ $t('INTEGRATION_SETTINGS.OPEN_AI.ASSISTANCE_MODAL.AI_WRITING') }}
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
<template>
|
||||
<div
|
||||
class="preview-item__wrap flex flex-col overflow-auto mt-4 max-h-[12.5rem]"
|
||||
>
|
||||
<div class="preview-item__wrap flex overflow-auto max-h-[12.5rem]">
|
||||
<div
|
||||
v-for="(attachment, index) in attachments"
|
||||
:key="attachment.id"
|
||||
class="preview-item flex p-1 bg-slate-50 dark:bg-slate-800 rounded-md w-[15rem] mb-1"
|
||||
class="preview-item flex items-center p-1 bg-slate-50 dark:bg-slate-800 gap-1 rounded-md w-[15rem] mb-1"
|
||||
>
|
||||
<div class="max-w-[4rem] flex-shrink-0 w-6 flex items-center">
|
||||
<img
|
||||
@@ -17,7 +15,7 @@
|
||||
📄
|
||||
</span>
|
||||
</div>
|
||||
<div class="max-w-[60%] min-w-[50%] overflow-hidden text-ellipsis ml-2">
|
||||
<div class="max-w-[60%] min-w-[50%] overflow-hidden text-ellipsis">
|
||||
<span
|
||||
class="h-4 overflow-hidden text-sm font-medium text-ellipsis whitespace-nowrap"
|
||||
>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
/>
|
||||
<iframe
|
||||
v-if="configItem.type === 'frame' && configItem.url"
|
||||
:id="`dashboard-app--frame-${index}`"
|
||||
:id="getFrameId(index)"
|
||||
:src="configItem.url"
|
||||
@load="() => onIframeLoad(index)"
|
||||
/>
|
||||
@@ -39,6 +39,10 @@ export default {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
position: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -82,10 +86,14 @@ export default {
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
getFrameId(index) {
|
||||
return `dashboard-app--frame-${this.position}-${index}`;
|
||||
},
|
||||
onIframeLoad(index) {
|
||||
const frameElement = document.getElementById(
|
||||
`dashboard-app--frame-${index}`
|
||||
);
|
||||
// A possible alternative is to use ref instead of document.getElementById
|
||||
// However, when ref is used together with v-for, the ref you get will be
|
||||
// an array containing the child components mirroring the data source.
|
||||
const frameElement = document.getElementById(this.getFrameId(index));
|
||||
const eventData = { event: 'appContext', data: this.dashboardAppContext };
|
||||
frameElement.contentWindow.postMessage(JSON.stringify(eventData), '*');
|
||||
this.iframeLoading = false;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
<!-- eslint-disable vue/no-mutating-props -->
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
@@ -255,7 +256,9 @@ export default {
|
||||
value === 'conversation_attribute' ||
|
||||
value === 'contact_attribute'
|
||||
) {
|
||||
// eslint-disable-next-line vue/no-mutating-props
|
||||
this.value.custom_attribute_type = this.customAttributeType;
|
||||
// eslint-disable-next-line vue/no-mutating-props
|
||||
} else this.value.custom_attribute_type = '';
|
||||
},
|
||||
immediate: true,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="editor-root">
|
||||
<div ref="editorRoot" class="relative editor-root">
|
||||
<tag-agents
|
||||
v-if="showUserMentions && isPrivate"
|
||||
:search-key="mentionSearchKey"
|
||||
@@ -23,6 +23,24 @@
|
||||
@change="onFileChange"
|
||||
/>
|
||||
<div ref="editor" />
|
||||
<div
|
||||
v-show="isImageNodeSelected && showImageResizeToolbar"
|
||||
class="absolute shadow-md rounded-[4px] flex gap-1 py-1 px-1 bg-slate-50 dark:bg-slate-700 text-slate-800 dark:text-slate-50"
|
||||
:style="{
|
||||
top: toolbarPosition.top,
|
||||
left: toolbarPosition.left,
|
||||
}"
|
||||
>
|
||||
<button
|
||||
v-for="size in sizes"
|
||||
:key="size.name"
|
||||
class="text-xs font-medium rounded-[4px] border border-solid border-slate-200 dark:border-slate-600 px-1.5 py-0.5 hover:bg-slate-100 dark:hover:bg-slate-800"
|
||||
@click="setURLWithQueryAndImageSize(size)"
|
||||
>
|
||||
{{ size.name }}
|
||||
</button>
|
||||
</div>
|
||||
<slot name="footer" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -40,6 +58,7 @@ import {
|
||||
suggestionsPlugin,
|
||||
triggerCharacters,
|
||||
} from '@chatwoot/prosemirror-schema/src/mentions/plugin';
|
||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||
|
||||
import TagAgents from '../conversation/TagAgents.vue';
|
||||
import CannedResponse from '../conversation/CannedResponse.vue';
|
||||
@@ -47,6 +66,10 @@ import VariableList from '../conversation/VariableList.vue';
|
||||
import {
|
||||
appendSignature,
|
||||
removeSignature,
|
||||
insertAtCursor,
|
||||
scrollCursorIntoView,
|
||||
findNodeToInsertImage,
|
||||
setURLWithQueryAndSize,
|
||||
} from 'dashboard/helper/editorHelper';
|
||||
|
||||
const TYPING_INDICATOR_IDLE_TIME = 4000;
|
||||
@@ -66,13 +89,17 @@ import { CONVERSATION_EVENTS } from '../../../helper/AnalyticsHelper/events';
|
||||
import { checkFileSizeLimit } from 'shared/helpers/FileHelper';
|
||||
import { uploadFile } from 'dashboard/helper/uploadHelper';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import { findNodeToInsertImage } from 'dashboard/helper/messageEditorHelper';
|
||||
import { MESSAGE_EDITOR_MENU_OPTIONS } from 'dashboard/constants/editor';
|
||||
import {
|
||||
MESSAGE_EDITOR_MENU_OPTIONS,
|
||||
MESSAGE_EDITOR_IMAGE_RESIZES,
|
||||
} from 'dashboard/constants/editor';
|
||||
|
||||
const createState = (
|
||||
content,
|
||||
placeholder,
|
||||
// eslint-disable-next-line default-param-last
|
||||
plugins = [],
|
||||
// eslint-disable-next-line default-param-last
|
||||
methods = {},
|
||||
enabledMenuOptions
|
||||
) => {
|
||||
@@ -108,6 +135,8 @@ export default {
|
||||
// allowSignature is a kill switch, ensuring no signature methods
|
||||
// are triggered except when this flag is true
|
||||
allowSignature: { type: Boolean, default: false },
|
||||
channelType: { type: String, default: '' },
|
||||
showImageResizeToolbar: { type: Boolean, default: false }, // A kill switch to show or hide the image toolbar
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -120,9 +149,16 @@ export default {
|
||||
editorView: null,
|
||||
range: null,
|
||||
state: undefined,
|
||||
isImageNodeSelected: false,
|
||||
toolbarPosition: { top: 0, left: 0 },
|
||||
sizes: MESSAGE_EDITOR_IMAGE_RESIZES,
|
||||
selectedImageNode: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
editorRoot() {
|
||||
return this.$refs.editorRoot;
|
||||
},
|
||||
contentFromEditor() {
|
||||
return MessageMarkdownSerializer.serialize(this.editorView.state.doc);
|
||||
},
|
||||
@@ -231,8 +267,11 @@ export default {
|
||||
sendWithSignature() {
|
||||
// this is considered the source of truth, we watch this property
|
||||
// on change, we toggle the signature in the editor
|
||||
const { send_with_signature: isEnabled } = this.uiSettings;
|
||||
return isEnabled && this.allowSignature && !this.isPrivate;
|
||||
if (this.allowSignature && !this.isPrivate && this.channelType) {
|
||||
return this.fetchSignatureFlagFromUiSettings(this.channelType);
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
@@ -270,6 +309,7 @@ export default {
|
||||
const tr = this.editorView.state.tr.replaceSelectionWith(node);
|
||||
this.editorView.focus();
|
||||
this.state = this.editorView.state.apply(tr);
|
||||
this.editorView.updateState(this.state);
|
||||
this.emitOnChange();
|
||||
this.$emit('clear-selection');
|
||||
}
|
||||
@@ -295,7 +335,17 @@ export default {
|
||||
mounted() {
|
||||
this.createEditorView();
|
||||
this.editorView.updateState(this.state);
|
||||
this.focusEditor(this.value);
|
||||
this.focusEditorInputField();
|
||||
|
||||
// BUS Event to insert text or markdown into the editor at the
|
||||
// current cursor position.
|
||||
// Components using this
|
||||
// 1. SearchPopover.vue
|
||||
|
||||
bus.$on(BUS_EVENTS.INSERT_INTO_RICH_EDITOR, this.insertContentIntoEditor);
|
||||
},
|
||||
beforeDestroy() {
|
||||
bus.$off(BUS_EVENTS.INSERT_INTO_RICH_EDITOR, this.insertContentIntoEditor);
|
||||
},
|
||||
methods: {
|
||||
reloadState(content = this.value) {
|
||||
@@ -382,6 +432,7 @@ export default {
|
||||
state: this.state,
|
||||
dispatchTransaction: tx => {
|
||||
this.state = this.state.apply(tx);
|
||||
this.editorView.updateState(this.state);
|
||||
this.emitOnChange();
|
||||
},
|
||||
handleDOMEvents: {
|
||||
@@ -394,6 +445,9 @@ export default {
|
||||
focus: () => {
|
||||
this.onFocus();
|
||||
},
|
||||
click: () => {
|
||||
this.isEditorMouseFocusedOnAnImage();
|
||||
},
|
||||
blur: () => {
|
||||
this.onBlur();
|
||||
},
|
||||
@@ -406,6 +460,50 @@ export default {
|
||||
},
|
||||
});
|
||||
},
|
||||
isEditorMouseFocusedOnAnImage() {
|
||||
if (!this.showImageResizeToolbar) {
|
||||
return;
|
||||
}
|
||||
this.selectedImageNode = document.querySelector(
|
||||
'img.ProseMirror-selectednode'
|
||||
);
|
||||
if (this.selectedImageNode) {
|
||||
this.isImageNodeSelected = !!this.selectedImageNode;
|
||||
// Get the position of the selected node
|
||||
this.setToolbarPosition();
|
||||
} else {
|
||||
this.isImageNodeSelected = false;
|
||||
}
|
||||
},
|
||||
setToolbarPosition() {
|
||||
const editorRect = this.editorRoot.getBoundingClientRect();
|
||||
const rect = this.selectedImageNode.getBoundingClientRect();
|
||||
this.toolbarPosition = {
|
||||
top: `${rect.top - editorRect.top - 30}px`,
|
||||
left: `${rect.left - editorRect.left - 4}px`,
|
||||
};
|
||||
},
|
||||
setURLWithQueryAndImageSize(size) {
|
||||
if (!this.showImageResizeToolbar) {
|
||||
return;
|
||||
}
|
||||
setURLWithQueryAndSize(this.selectedImageNode, size, this.editorView);
|
||||
this.isImageNodeSelected = false;
|
||||
},
|
||||
updateImgToolbarOnDelete() {
|
||||
// check if the selected node is present or not on keyup
|
||||
// this is needed because the user can select an image and then delete it
|
||||
// in that case, the selected node will be null and we need to hide the toolbar
|
||||
// otherwise, the toolbar will be visible even when the image is deleted and cause some errors
|
||||
if (this.selectedImageNode) {
|
||||
const hasImgSelectedNode = document.querySelector(
|
||||
'img.ProseMirror-selectednode'
|
||||
);
|
||||
if (!hasImgSelectedNode) {
|
||||
this.isImageNodeSelected = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
isEnterToSendEnabled() {
|
||||
return isEditorHotKeyEnabled(this.uiSettings, 'enter');
|
||||
},
|
||||
@@ -438,11 +536,7 @@ export default {
|
||||
userFullName: mentionItem.name,
|
||||
});
|
||||
|
||||
const tr = this.editorView.state.tr
|
||||
.replaceWith(this.range.from, this.range.to, node)
|
||||
.insertText(` `);
|
||||
this.state = this.editorView.state.apply(tr);
|
||||
this.emitOnChange();
|
||||
this.insertNodeIntoEditor(node, this.range.from, this.range.to);
|
||||
this.$track(CONVERSATION_EVENTS.USED_MENTIONS);
|
||||
|
||||
return false;
|
||||
@@ -452,30 +546,22 @@ export default {
|
||||
message: cannedItem,
|
||||
variables: this.variables,
|
||||
});
|
||||
|
||||
if (!this.editorView) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let from = this.range.from - 1;
|
||||
let node = new MessageMarkdownTransformer(messageSchema).parse(
|
||||
updatedMessage
|
||||
);
|
||||
|
||||
if (node.textContent === updatedMessage) {
|
||||
node = this.editorView.state.schema.text(updatedMessage);
|
||||
from = this.range.from;
|
||||
}
|
||||
const from =
|
||||
node.textContent === updatedMessage
|
||||
? this.range.from
|
||||
: this.range.from - 1;
|
||||
|
||||
const tr = this.editorView.state.tr.replaceWith(
|
||||
from,
|
||||
this.range.to,
|
||||
node
|
||||
);
|
||||
this.insertNodeIntoEditor(node, from, this.range.to);
|
||||
|
||||
this.state = this.editorView.state.apply(tr);
|
||||
this.emitOnChange();
|
||||
|
||||
tr.scrollIntoView();
|
||||
this.$track(CONVERSATION_EVENTS.INSERTED_A_CANNED_RESPONSE);
|
||||
return false;
|
||||
},
|
||||
@@ -483,23 +569,14 @@ export default {
|
||||
if (!this.editorView) {
|
||||
return null;
|
||||
}
|
||||
let node = this.editorView.state.schema.text(`{{${variable}}}`);
|
||||
const from = this.range.from;
|
||||
|
||||
const tr = this.editorView.state.tr.replaceWith(
|
||||
from,
|
||||
this.range.to,
|
||||
node
|
||||
);
|
||||
const content = `{{${variable}}}`;
|
||||
let node = this.editorView.state.schema.text(content);
|
||||
const { from, to } = this.range;
|
||||
|
||||
this.state = this.editorView.state.apply(tr);
|
||||
this.emitOnChange();
|
||||
|
||||
// The `{{ }}` are added to the message, but the cursor is placed
|
||||
// and onExit of suggestionsPlugin is not called. So we need to manually hide
|
||||
this.insertNodeIntoEditor(node, from, to);
|
||||
this.showVariables = false;
|
||||
this.$track(CONVERSATION_EVENTS.INSERTED_A_VARIABLE);
|
||||
tr.scrollIntoView();
|
||||
return false;
|
||||
},
|
||||
openFileBrowser() {
|
||||
@@ -555,8 +632,6 @@ export default {
|
||||
},
|
||||
|
||||
emitOnChange() {
|
||||
this.editorView.updateState(this.state);
|
||||
|
||||
this.$emit('input', this.contentFromEditor);
|
||||
},
|
||||
|
||||
@@ -599,6 +674,7 @@ export default {
|
||||
() => this.resetTyping(),
|
||||
TYPING_INDICATOR_IDLE_TIME
|
||||
);
|
||||
this.updateImgToolbarOnDelete();
|
||||
},
|
||||
onKeydown(event) {
|
||||
if (this.isEnterToSendEnabled()) {
|
||||
@@ -616,6 +692,19 @@ export default {
|
||||
onFocus() {
|
||||
this.$emit('focus');
|
||||
},
|
||||
insertContentIntoEditor(content, defaultFrom = 0) {
|
||||
const from = defaultFrom || this.editorView.state.selection.from || 0;
|
||||
let node = new MessageMarkdownTransformer(messageSchema).parse(content);
|
||||
|
||||
this.insertNodeIntoEditor(node, from, undefined);
|
||||
},
|
||||
insertNodeIntoEditor(node, from = 0, to = 0) {
|
||||
this.state = insertAtCursor(this.editorView, node, from, to);
|
||||
this.emitOnChange();
|
||||
this.$nextTick(() => {
|
||||
scrollCursorIntoView(this.editorView);
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -709,6 +798,6 @@ export default {
|
||||
}
|
||||
|
||||
.editor-warning__message {
|
||||
@apply text-red-400 dark:text-red-400 font-normal pt-1 pb-0 px-0;
|
||||
@apply text-red-400 dark:text-red-400 font-normal text-sm pt-1 pb-0 px-0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -32,7 +32,9 @@ const MAXIMUM_FILE_UPLOAD_SIZE = 4; // in MB
|
||||
const createState = (
|
||||
content,
|
||||
placeholder,
|
||||
// eslint-disable-next-line default-param-last
|
||||
plugins = [],
|
||||
// eslint-disable-next-line default-param-last
|
||||
methods = {},
|
||||
enabledMenuOptions
|
||||
) => {
|
||||
|
||||
@@ -14,10 +14,11 @@
|
||||
<file-upload
|
||||
ref="upload"
|
||||
v-tooltip.top-end="$t('CONVERSATION.REPLYBOX.TIP_ATTACH_ICON')"
|
||||
input-id="conversationAttachment"
|
||||
:size="4096 * 4096"
|
||||
:accept="allowedFileTypes"
|
||||
:multiple="enableMultipleFileUpload"
|
||||
:drop="true"
|
||||
:drop="enableDragAndDrop"
|
||||
:drop-directory="false"
|
||||
:data="{
|
||||
direct_upload_url: '/rails/active_storage/direct_uploads',
|
||||
@@ -100,14 +101,24 @@
|
||||
<transition name="modal-fade">
|
||||
<div
|
||||
v-show="$refs.upload && $refs.upload.dropActive"
|
||||
class="modal-mask"
|
||||
class="fixed top-0 bottom-0 left-0 right-0 z-20 flex flex-col items-center justify-center w-full h-full gap-2 text-slate-600 dark:text-slate-200 bg-white_transparent dark:bg-black_transparent"
|
||||
>
|
||||
<fluent-icon icon="cloud-backup" />
|
||||
<fluent-icon icon="cloud-backup" size="40" />
|
||||
<h4 class="page-sub-title text-slate-600 dark:text-slate-200">
|
||||
{{ $t('CONVERSATION.REPLYBOX.DRAG_DROP') }}
|
||||
</h4>
|
||||
</div>
|
||||
</transition>
|
||||
<woot-button
|
||||
v-if="enableInsertArticleInReply"
|
||||
v-tooltip.top-end="$t('HELP_CENTER.ARTICLE_SEARCH.OPEN_ARTICLE_SEARCH')"
|
||||
icon="document-text-link"
|
||||
color-scheme="secondary"
|
||||
variant="smooth"
|
||||
size="small"
|
||||
:title="$t('HELP_CENTER.ARTICLE_SEARCH.OPEN_ARTICLE_SEARCH')"
|
||||
@click="toggleInsertArticle"
|
||||
/>
|
||||
</div>
|
||||
<div class="right-wrap">
|
||||
<woot-button
|
||||
@@ -228,6 +239,14 @@ export default {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
newConversationModalActive: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
portalSlug: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
@@ -274,6 +293,9 @@ export default {
|
||||
}
|
||||
return ALLOWED_FILE_TYPES;
|
||||
},
|
||||
enableDragAndDrop() {
|
||||
return !this.newConversationModalActive;
|
||||
},
|
||||
audioRecorderPlayStopIcon() {
|
||||
switch (this.recordingAudioState) {
|
||||
// playing paused recording stopped inactive destroyed
|
||||
@@ -291,14 +313,21 @@ export default {
|
||||
return !this.isOnPrivateNote;
|
||||
},
|
||||
sendWithSignature() {
|
||||
const { send_with_signature: isEnabled } = this.uiSettings;
|
||||
return isEnabled;
|
||||
// channelType is sourced from inboxMixin
|
||||
return this.fetchSignatureFlagFromUiSettings(this.channelType);
|
||||
},
|
||||
signatureToggleTooltip() {
|
||||
return this.sendWithSignature
|
||||
? this.$t('CONVERSATION.FOOTER.DISABLE_SIGN_TOOLTIP')
|
||||
: this.$t('CONVERSATION.FOOTER.ENABLE_SIGN_TOOLTIP');
|
||||
},
|
||||
enableInsertArticleInReply() {
|
||||
const isFeatEnabled = this.isFeatureEnabledonAccount(
|
||||
this.accountId,
|
||||
FEATURE_FLAGS.INSERT_ARTICLE_IN_REPLY
|
||||
);
|
||||
return isFeatEnabled && this.portalSlug;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
ActiveStorage.start();
|
||||
@@ -310,13 +339,14 @@ export default {
|
||||
}
|
||||
},
|
||||
toggleMessageSignature() {
|
||||
this.updateUISettings({
|
||||
send_with_signature: !this.sendWithSignature,
|
||||
});
|
||||
this.setSignatureFlagForInbox(this.channelType, !this.sendWithSignature);
|
||||
},
|
||||
replaceText(text) {
|
||||
this.$emit('replace-text', text);
|
||||
},
|
||||
toggleInsertArticle() {
|
||||
this.$emit('toggle-insert-article');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -346,12 +376,4 @@ export default {
|
||||
@apply dark:bg-slate-800 bg-slate-100;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-mask {
|
||||
@apply text-slate-600 dark:text-slate-200 bg-white_transparent dark:bg-black_transparent flex-col;
|
||||
}
|
||||
|
||||
.icon {
|
||||
@apply text-[5rem];
|
||||
}
|
||||
</style>
|
||||
|
||||
+1
-3
@@ -1,8 +1,6 @@
|
||||
<template>
|
||||
<div
|
||||
:class="
|
||||
`status-badge status-badge__${status} rounded-full w-2.5 h-2.5 mr-0.5 rtl:mr-0 rtl:ml-0.5 inline-flex`
|
||||
"
|
||||
:class="`status-badge status-badge__${status} rounded-full w-2.5 h-2.5 mr-0.5 rtl:mr-0 rtl:ml-0.5 inline-flex`"
|
||||
/>
|
||||
</template>
|
||||
<script>
|
||||
|
||||
+16
-6
@@ -16,7 +16,7 @@
|
||||
>
|
||||
<div class="items-center flex justify-between last:mt-4">
|
||||
<span class="text-slate-800 dark:text-slate-100 text-xs font-medium">{{
|
||||
this.$t('CHAT_LIST.CHAT_SORT.STATUS')
|
||||
$t('CHAT_LIST.CHAT_SORT.STATUS')
|
||||
}}</span>
|
||||
<filter-item
|
||||
type="status"
|
||||
@@ -28,11 +28,11 @@
|
||||
</div>
|
||||
<div class="items-center flex justify-between last:mt-4">
|
||||
<span class="text-slate-800 dark:text-slate-100 text-xs font-medium">{{
|
||||
this.$t('CHAT_LIST.CHAT_SORT.ORDER_BY')
|
||||
$t('CHAT_LIST.CHAT_SORT.ORDER_BY')
|
||||
}}</span>
|
||||
<filter-item
|
||||
type="sort"
|
||||
:selected-value="chatSortFilter"
|
||||
:selected-value="sortFilter"
|
||||
:items="chatSortItems"
|
||||
path-prefix="CHAT_LIST.CHAT_SORT_FILTER_ITEMS"
|
||||
@onChangeFilter="onChangeFilter"
|
||||
@@ -47,12 +47,13 @@ import wootConstants from 'dashboard/constants/globals';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { mixin as clickaway } from 'vue-clickaway';
|
||||
import FilterItem from './FilterItem.vue';
|
||||
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
FilterItem,
|
||||
},
|
||||
mixins: [clickaway],
|
||||
mixins: [clickaway, uiSettingsMixin],
|
||||
data() {
|
||||
return {
|
||||
showActionsDropdown: false,
|
||||
@@ -83,8 +84,17 @@ export default {
|
||||
closeDropdown() {
|
||||
this.showActionsDropdown = false;
|
||||
},
|
||||
onChangeFilter(type, value) {
|
||||
this.$emit('changeFilter', type, value);
|
||||
onChangeFilter(value, type) {
|
||||
this.$emit('changeFilter', value, type);
|
||||
this.saveSelectedFilter(type, value);
|
||||
},
|
||||
saveSelectedFilter(type, value) {
|
||||
this.updateUISettings({
|
||||
conversations_filter_by: {
|
||||
status: type === 'status' ? value : this.chatStatus,
|
||||
order_by: type === 'sort' ? value : this.sortFilter,
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -52,6 +52,7 @@
|
||||
:key="currentChat.id + '-' + dashboardApp.id"
|
||||
:is-visible="activeIndex - 1 === index"
|
||||
:config="dashboardApps[index].content"
|
||||
:position="index"
|
||||
:current-chat="currentChat"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div
|
||||
class="conversation flex flex-shrink-0 flex-grow-0 w-auto max-w-full cursor-pointer relative py-0 px-4 border-transparent border-l-2 border-t-0 border-b-0 border-r-0 border-solid items-start hover:bg-slate-25 dark:hover:bg-slate-800 group"
|
||||
class="relative flex items-start flex-grow-0 flex-shrink-0 w-auto max-w-full px-4 py-0 border-t-0 border-b-0 border-l-2 border-r-0 border-transparent border-solid cursor-pointer conversation hover:bg-slate-25 dark:hover:bg-slate-800 group"
|
||||
:class="{
|
||||
'active bg-slate-25 dark:bg-slate-800 border-woot-500': isActiveChat,
|
||||
'unread-chat': hasUnread,
|
||||
@@ -31,7 +31,7 @@
|
||||
size="40px"
|
||||
/>
|
||||
<div
|
||||
class="py-3 px-0 border-b group-last:border-transparent group-hover:border-transparent border-slate-50 dark:border-slate-800/75 columns"
|
||||
class="px-0 py-3 border-b group-last:border-transparent group-hover:border-transparent border-slate-50 dark:border-slate-800/75 columns"
|
||||
>
|
||||
<div class="flex justify-between">
|
||||
<inbox-name v-if="showInboxName" :inbox="inbox" />
|
||||
@@ -55,44 +55,11 @@
|
||||
>
|
||||
{{ currentContact.name }}
|
||||
</h4>
|
||||
<p
|
||||
<message-preview
|
||||
v-if="lastMessageInChat"
|
||||
class="conversation--message text-slate-700 dark:text-slate-200 text-sm my-0 mx-2 leading-6 h-6 max-w-[96%] w-[16.875rem] overflow-hidden text-ellipsis whitespace-nowrap"
|
||||
>
|
||||
<fluent-icon
|
||||
v-if="isMessagePrivate"
|
||||
size="16"
|
||||
class="-mt-0.5 align-middle text-slate-600 dark:text-slate-300 inline-block"
|
||||
icon="lock-closed"
|
||||
/>
|
||||
<fluent-icon
|
||||
v-else-if="messageByAgent"
|
||||
size="16"
|
||||
class="-mt-0.5 align-middle text-slate-600 dark:text-slate-300 inline-block"
|
||||
icon="arrow-reply"
|
||||
/>
|
||||
<fluent-icon
|
||||
v-else-if="isMessageAnActivity"
|
||||
size="16"
|
||||
class="-mt-0.5 align-middle text-slate-600 dark:text-slate-300 inline-block"
|
||||
icon="info"
|
||||
/>
|
||||
<span v-if="lastMessageInChat.content">
|
||||
{{ parsedLastMessage }}
|
||||
</span>
|
||||
<span v-else-if="lastMessageInChat.attachments">
|
||||
<fluent-icon
|
||||
v-if="attachmentIcon"
|
||||
size="16"
|
||||
class="-mt-0.5 align-middle inline-block text-slate-600 dark:text-slate-300"
|
||||
:icon="attachmentIcon"
|
||||
/>
|
||||
{{ this.$t(`${attachmentMessageContent}`) }}
|
||||
</span>
|
||||
<span v-else>
|
||||
{{ $t('CHAT_LIST.NO_CONTENT') }}
|
||||
</span>
|
||||
</p>
|
||||
:message="lastMessageInChat"
|
||||
class="conversation--message my-0 mx-2 leading-6 h-6 max-w-[96%] w-[16.875rem] text-sm text-slate-700 dark:text-slate-200"
|
||||
/>
|
||||
<p
|
||||
v-else
|
||||
class="conversation--message text-slate-700 dark:text-slate-200 text-sm my-0 mx-2 leading-6 h-6 max-w-[96%] w-[16.875rem] overflow-hidden text-ellipsis whitespace-nowrap"
|
||||
@@ -103,11 +70,11 @@
|
||||
icon="info"
|
||||
/>
|
||||
<span>
|
||||
{{ this.$t(`CHAT_LIST.NO_MESSAGES`) }}
|
||||
{{ $t(`CHAT_LIST.NO_MESSAGES`) }}
|
||||
</span>
|
||||
</p>
|
||||
<div class="conversation--meta flex flex-col absolute right-4 top-4">
|
||||
<span class="text-black-600 text-xxs font-normal leading-4 ml-auto">
|
||||
<div class="absolute flex flex-col conversation--meta right-4 top-4">
|
||||
<span class="ml-auto font-normal leading-4 text-black-600 text-xxs">
|
||||
<time-ago
|
||||
:last-activity-timestamp="chat.timestamp"
|
||||
:created-at-timestamp="chat.created_at"
|
||||
@@ -145,9 +112,8 @@
|
||||
</template>
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { MESSAGE_TYPE } from 'widget/helpers/constants';
|
||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||
import Thumbnail from '../Thumbnail.vue';
|
||||
import MessagePreview from './MessagePreview.vue';
|
||||
import conversationMixin from '../../../mixins/conversations';
|
||||
import timeMixin from '../../../mixins/time';
|
||||
import router from '../../../routes';
|
||||
@@ -159,14 +125,6 @@ import alertMixin from 'shared/mixins/alertMixin';
|
||||
import TimeAgo from 'dashboard/components/ui/TimeAgo.vue';
|
||||
import CardLabels from './conversationCardComponents/CardLabels.vue';
|
||||
import PriorityMark from './PriorityMark.vue';
|
||||
const ATTACHMENT_ICONS = {
|
||||
image: 'image',
|
||||
audio: 'headphones-sound-wave',
|
||||
video: 'video',
|
||||
file: 'document',
|
||||
location: 'location',
|
||||
fallback: 'link',
|
||||
};
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -175,16 +133,11 @@ export default {
|
||||
Thumbnail,
|
||||
ConversationContextMenu,
|
||||
TimeAgo,
|
||||
MessagePreview,
|
||||
PriorityMark,
|
||||
},
|
||||
|
||||
mixins: [
|
||||
inboxMixin,
|
||||
timeMixin,
|
||||
conversationMixin,
|
||||
messageFormatterMixin,
|
||||
alertMixin,
|
||||
],
|
||||
mixins: [inboxMixin, timeMixin, conversationMixin, alertMixin],
|
||||
props: {
|
||||
activeLabel: {
|
||||
type: String,
|
||||
@@ -258,20 +211,6 @@ export default {
|
||||
);
|
||||
},
|
||||
|
||||
lastMessageFileType() {
|
||||
const lastMessage = this.lastMessageInChat;
|
||||
const [{ file_type: fileType } = {}] = lastMessage.attachments;
|
||||
return fileType;
|
||||
},
|
||||
|
||||
attachmentIcon() {
|
||||
return ATTACHMENT_ICONS[this.lastMessageFileType];
|
||||
},
|
||||
|
||||
attachmentMessageContent() {
|
||||
return `CHAT_LIST.ATTACHMENTS.${this.lastMessageFileType}.CONTENT`;
|
||||
},
|
||||
|
||||
isActiveChat() {
|
||||
return this.currentChat.id === this.chat.id;
|
||||
},
|
||||
@@ -292,30 +231,6 @@ export default {
|
||||
return this.lastMessage(this.chat);
|
||||
},
|
||||
|
||||
messageByAgent() {
|
||||
const lastMessage = this.lastMessageInChat;
|
||||
const { message_type: messageType } = lastMessage;
|
||||
return messageType === MESSAGE_TYPE.OUTGOING;
|
||||
},
|
||||
|
||||
isMessageAnActivity() {
|
||||
const lastMessage = this.lastMessageInChat;
|
||||
const { message_type: messageType } = lastMessage;
|
||||
return messageType === MESSAGE_TYPE.ACTIVITY;
|
||||
},
|
||||
|
||||
isMessagePrivate() {
|
||||
const lastMessage = this.lastMessageInChat;
|
||||
const { private: isPrivate } = lastMessage;
|
||||
return isPrivate;
|
||||
},
|
||||
|
||||
parsedLastMessage() {
|
||||
const { content_attributes: contentAttributes } = this.lastMessageInChat;
|
||||
const { email: { subject } = {} } = contentAttributes || {};
|
||||
return this.getPlainText(subject || this.lastMessageInChat.content);
|
||||
},
|
||||
|
||||
inbox() {
|
||||
const { inbox_id: inboxId } = this.chat;
|
||||
const stateInbox = this.$store.getters['inboxes/getInbox'](inboxId);
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
class="bg-white dark:bg-slate-900 flex justify-between items-center py-2 px-4 border-b border-slate-50 dark:border-slate-800/50 flex-col md:flex-row"
|
||||
>
|
||||
<div
|
||||
class="flex-1 w-100 flex flex-col md:flex-row items-center justify-center"
|
||||
class="flex-1 w-full min-w-0 flex flex-col md:flex-row items-center justify-center"
|
||||
>
|
||||
<div
|
||||
class="flex justify-center items-center mr-4 rtl:mr-0 rtl:ml-4 min-w-0"
|
||||
class="flex justify-start items-center mr-4 rtl:mr-0 rtl:ml-4 min-w-0 w-[inherit]"
|
||||
>
|
||||
<back-button
|
||||
v-if="showBackButton"
|
||||
@@ -19,26 +19,31 @@
|
||||
:username="currentContact.name"
|
||||
:status="currentContact.availability_status"
|
||||
/>
|
||||
<div class="items-start flex flex-col ml-2 rtl:ml-0 rtl:mr-2 min-w-0">
|
||||
<woot-button
|
||||
variant="link"
|
||||
color-scheme="secondary"
|
||||
class="overflow-hidden whitespace-nowrap text-ellipsis"
|
||||
@click.prevent="$emit('contact-panel-toggle')"
|
||||
>
|
||||
<h3
|
||||
class="text-base inline-block leading-tight capitalize m-0 p-0 text-ellipsis overflow-hidden whitespace-nowrap text-slate-900 dark:text-slate-100"
|
||||
<div
|
||||
class="items-start flex flex-col ml-2 rtl:ml-0 rtl:mr-2 min-w-0 w-[inherit] overflow-hidden"
|
||||
>
|
||||
<div class="flex items-center flex-row gap-1 m-0 p-0 w-[inherit]">
|
||||
<woot-button
|
||||
variant="link"
|
||||
color-scheme="secondary"
|
||||
class="[&>span]:overflow-hidden [&>span]:whitespace-nowrap [&>span]:text-ellipsis min-w-0"
|
||||
@click.prevent="$emit('contact-panel-toggle')"
|
||||
>
|
||||
<span>{{ currentContact.name }}</span>
|
||||
<fluent-icon
|
||||
v-if="!isHMACVerified"
|
||||
v-tooltip="$t('CONVERSATION.UNVERIFIED_SESSION')"
|
||||
size="14"
|
||||
class="text-yellow-600 dark:text-yellow-500 my-0 mx-0.5"
|
||||
icon="warning"
|
||||
/>
|
||||
</h3>
|
||||
</woot-button>
|
||||
<span
|
||||
class="text-base leading-tight text-slate-900 dark:text-slate-100"
|
||||
>
|
||||
{{ currentContact.name }}
|
||||
</span>
|
||||
</woot-button>
|
||||
<fluent-icon
|
||||
v-if="!isHMACVerified"
|
||||
v-tooltip="$t('CONVERSATION.UNVERIFIED_SESSION')"
|
||||
size="14"
|
||||
class="text-yellow-600 dark:text-yellow-500 my-0 mx-0 min-w-[14px]"
|
||||
icon="warning"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="conversation--header--actions items-center flex text-xs gap-2 text-ellipsis overflow-hidden whitespace-nowrap"
|
||||
>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
<!-- eslint-disable vue/no-mutating-props -->
|
||||
<template>
|
||||
<woot-modal :show.sync="show" :on-close="onCancel">
|
||||
<div class="h-auto overflow-auto flex flex-col">
|
||||
|
||||
@@ -22,18 +22,13 @@
|
||||
:bcc="emailHeadAttributes.bcc"
|
||||
:is-incoming="isIncoming"
|
||||
/>
|
||||
<blockquote v-if="storyReply" class="story-reply-quote">
|
||||
<span>{{ $t('CONVERSATION.REPLIED_TO_STORY') }}</span>
|
||||
<bubble-image
|
||||
v-if="!hasImgStoryError && storyUrl"
|
||||
:url="storyUrl"
|
||||
@error="onStoryLoadError"
|
||||
/>
|
||||
<bubble-video
|
||||
v-else-if="hasImgStoryError && storyUrl"
|
||||
:url="storyUrl"
|
||||
/>
|
||||
</blockquote>
|
||||
<instagram-story-reply v-if="storyUrl" :story-url="storyUrl" />
|
||||
<bubble-reply-to
|
||||
v-if="inReplyToMessageId && inboxSupportsReplyTo.incoming"
|
||||
:message="inReplyTo"
|
||||
:message-type="data.message_type"
|
||||
:parent-has-attachments="hasAttachments"
|
||||
/>
|
||||
<bubble-text
|
||||
v-if="data.content"
|
||||
:message="message"
|
||||
@@ -53,10 +48,15 @@
|
||||
</span>
|
||||
<div v-if="!isPending && hasAttachments">
|
||||
<div v-for="attachment in attachments" :key="attachment.id">
|
||||
<instagram-story
|
||||
v-if="isAnInstagramStory"
|
||||
:story-url="attachment.data_url"
|
||||
@error="onMediaLoadError"
|
||||
/>
|
||||
<bubble-image-audio-video
|
||||
v-if="isAttachmentImageVideoAudio(attachment.file_type)"
|
||||
v-else-if="isAttachmentImageVideoAudio(attachment.file_type)"
|
||||
:attachment="attachment"
|
||||
@error="onImageLoadError"
|
||||
@error="onMediaLoadError"
|
||||
/>
|
||||
<bubble-location
|
||||
v-else-if="attachment.file_type === 'location'"
|
||||
@@ -69,9 +69,6 @@
|
||||
:name="data.content"
|
||||
:phone-number="attachment.fallback_title"
|
||||
/>
|
||||
<instagram-image-error-placeholder
|
||||
v-else-if="hasImageError && hasInstagramStory"
|
||||
/>
|
||||
<bubble-file v-else :url="attachment.data_url" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -83,7 +80,6 @@
|
||||
:story-id="`${storyId}`"
|
||||
:is-a-tweet="isATweet"
|
||||
:is-a-whatsapp-channel="isAWhatsAppChannel"
|
||||
:has-instagram-story="hasInstagramStory"
|
||||
:is-email="isEmailContentType"
|
||||
:is-private="data.private"
|
||||
:message-type="data.message_type"
|
||||
@@ -124,6 +120,7 @@
|
||||
:message="data"
|
||||
@open="openContextMenu"
|
||||
@close="closeContextMenu"
|
||||
@replyTo="handleReplyTo"
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
@@ -131,40 +128,42 @@
|
||||
<script>
|
||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||
import BubbleActions from './bubble/Actions.vue';
|
||||
import BubbleContact from './bubble/Contact.vue';
|
||||
import BubbleFile from './bubble/File.vue';
|
||||
import BubbleImage from './bubble/Image.vue';
|
||||
import BubbleVideo from './bubble/Video.vue';
|
||||
import BubbleImageAudioVideo from './bubble/ImageAudioVideo.vue';
|
||||
import BubbleIntegration from './bubble/Integration.vue';
|
||||
import BubbleLocation from './bubble/Location.vue';
|
||||
import BubbleMailHead from './bubble/MailHead.vue';
|
||||
import BubbleReplyTo from './bubble/ReplyTo.vue';
|
||||
import BubbleText from './bubble/Text.vue';
|
||||
import BubbleContact from './bubble/Contact.vue';
|
||||
import Spinner from 'shared/components/Spinner.vue';
|
||||
import ContextMenu from 'dashboard/modules/conversations/components/MessageContextMenu.vue';
|
||||
import instagramImageErrorPlaceholder from './instagramImageErrorPlaceholder.vue';
|
||||
import InstagramStory from './bubble/InstagramStory.vue';
|
||||
import InstagramStoryReply from './bubble/InstagramStoryReply.vue';
|
||||
import Spinner from 'shared/components/Spinner.vue';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import contentTypeMixin from 'shared/mixins/contentTypeMixin';
|
||||
import { MESSAGE_TYPE, MESSAGE_STATUS } from 'shared/constants/messages';
|
||||
import { generateBotMessageContent } from './helpers/botMessageContentHelper';
|
||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||
import { ACCOUNT_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
|
||||
import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage';
|
||||
import { LocalStorage } from 'shared/helpers/localStorage';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
BubbleActions,
|
||||
BubbleContact,
|
||||
BubbleFile,
|
||||
BubbleImage,
|
||||
BubbleVideo,
|
||||
BubbleImageAudioVideo,
|
||||
BubbleIntegration,
|
||||
BubbleLocation,
|
||||
BubbleMailHead,
|
||||
BubbleReplyTo,
|
||||
BubbleText,
|
||||
BubbleContact,
|
||||
ContextMenu,
|
||||
InstagramStory,
|
||||
InstagramStoryReply,
|
||||
Spinner,
|
||||
instagramImageErrorPlaceholder,
|
||||
},
|
||||
mixins: [alertMixin, messageFormatterMixin, contentTypeMixin],
|
||||
props: {
|
||||
@@ -180,22 +179,25 @@ export default {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
hasInstagramStory: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isWebWidgetInbox: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
inboxSupportsReplyTo: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
inReplyTo: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showContextMenu: false,
|
||||
hasImageError: false,
|
||||
hasMediaLoadError: false,
|
||||
contextMenuPosition: {},
|
||||
showBackgroundHighlight: false,
|
||||
hasImgStoryError: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -264,18 +266,29 @@ export default {
|
||||
) + botMessageContent
|
||||
);
|
||||
},
|
||||
inReplyToMessageId() {
|
||||
// Why not use the inReplyTo object directly?
|
||||
// Glad you asked! The inReplyTo object may or may not be available
|
||||
// depending on the current scroll position of the message list
|
||||
// since old messages are only loaded when the user scrolls up
|
||||
return this.data.content_attributes?.in_reply_to;
|
||||
},
|
||||
isAnInstagramStory() {
|
||||
return this.contentAttributes.image_type === 'story_mention';
|
||||
},
|
||||
contextMenuEnabledOptions() {
|
||||
return {
|
||||
copy: this.hasText,
|
||||
delete: this.hasText || this.hasAttachments,
|
||||
cannedResponse: this.isOutgoing && this.hasText,
|
||||
replyTo: !this.data.private && this.inboxSupportsReplyTo.outgoing,
|
||||
};
|
||||
},
|
||||
contentAttributes() {
|
||||
return this.data.content_attributes || {};
|
||||
},
|
||||
externalError() {
|
||||
return this.contentAttributes.external_error || null;
|
||||
return this.contentAttributes.external_error || '';
|
||||
},
|
||||
sender() {
|
||||
return this.data.sender || {};
|
||||
@@ -292,9 +305,6 @@ export default {
|
||||
storyUrl() {
|
||||
return this.contentAttributes.story_url || null;
|
||||
},
|
||||
storyReply() {
|
||||
return this.storyUrl && this.hasInstagramStory;
|
||||
},
|
||||
contentType() {
|
||||
const {
|
||||
data: { content_type: contentType },
|
||||
@@ -318,6 +328,8 @@ export default {
|
||||
left: isLeftAligned,
|
||||
right: isRightAligned,
|
||||
'has-context-menu': this.showContextMenu,
|
||||
// this handles the offset required to align the context menu button
|
||||
// extra alignment is required since a tweet message has a the user name and avatar below it
|
||||
'has-tweet-menu': this.isATweet,
|
||||
'has-bg': this.showBackgroundHighlight,
|
||||
};
|
||||
@@ -373,7 +385,7 @@ export default {
|
||||
return false;
|
||||
}
|
||||
if (this.isFailed) {
|
||||
return this.externalError ? '' : this.$t(`CONVERSATION.SEND_FAILED`);
|
||||
return this.externalError || this.$t(`CONVERSATION.SEND_FAILED`);
|
||||
}
|
||||
return false;
|
||||
},
|
||||
@@ -431,13 +443,11 @@ export default {
|
||||
},
|
||||
watch: {
|
||||
data() {
|
||||
this.hasImageError = false;
|
||||
this.hasImgStoryError = false;
|
||||
this.hasMediaLoadError = false;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.hasImageError = false;
|
||||
this.hasImgStoryError = false;
|
||||
this.hasMediaLoadError = false;
|
||||
bus.$on(BUS_EVENTS.ON_MESSAGE_LIST_SCROLL, this.closeContextMenu);
|
||||
this.setupHighlightTimer();
|
||||
},
|
||||
@@ -447,13 +457,13 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
isAttachmentImageVideoAudio(fileType) {
|
||||
return ['image', 'audio', 'video'].includes(fileType);
|
||||
return ['image', 'audio', 'video', 'story_mention'].includes(fileType);
|
||||
},
|
||||
hasMediaAttachment(type) {
|
||||
if (this.hasAttachments && this.data.attachments.length > 0) {
|
||||
const { attachments = [{}] } = this.data;
|
||||
const { file_type: fileType } = attachments[0];
|
||||
return fileType === type && !this.hasImageError;
|
||||
return fileType === type && !this.hasMediaLoadError;
|
||||
}
|
||||
if (this.storyReply) {
|
||||
return true;
|
||||
@@ -466,11 +476,8 @@ export default {
|
||||
async retrySendMessage() {
|
||||
await this.$store.dispatch('sendMessageWithData', this.data);
|
||||
},
|
||||
onImageLoadError() {
|
||||
this.hasImageError = true;
|
||||
},
|
||||
onStoryLoadError() {
|
||||
this.hasImgStoryError = true;
|
||||
onMediaLoadError() {
|
||||
this.hasMediaLoadError = true;
|
||||
},
|
||||
openContextMenu(e) {
|
||||
const shouldSkipContextMenu =
|
||||
@@ -494,6 +501,13 @@ export default {
|
||||
this.showContextMenu = false;
|
||||
this.contextMenuPosition = { x: null, y: null };
|
||||
},
|
||||
handleReplyTo() {
|
||||
const replyStorageKey = LOCAL_STORAGE_KEYS.MESSAGE_REPLY_TO;
|
||||
const { conversation_id: conversationId, id: replyTo } = this.data;
|
||||
|
||||
LocalStorage.updateJsonStore(replyStorageKey, conversationId, replyTo);
|
||||
bus.$emit(BUS_EVENTS.TOGGLE_REPLY_TO_MESSAGE, this.data);
|
||||
},
|
||||
setupHighlightTimer() {
|
||||
if (Number(this.$route.query.messageId) !== Number(this.data.id)) {
|
||||
return;
|
||||
@@ -613,6 +627,8 @@ li.right {
|
||||
}
|
||||
|
||||
li.left.has-tweet-menu .context-menu {
|
||||
// this handles the offset required to align the context menu button
|
||||
// extra alignment is required since a tweet message has a the user name and avatar below it
|
||||
@apply mb-6;
|
||||
}
|
||||
|
||||
@@ -693,8 +709,4 @@ li.right {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.story-reply-quote {
|
||||
@apply mt-2 mx-4 mb-0 px-2 pb-0 pt-2 border-l-4 border-solid border-slate-75 dark:border-slate-600 text-slate-600 dark:text-slate-200;
|
||||
}
|
||||
</style>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user