Merge branch 'release/3.2.0'
This commit is contained in:
commit
83231b9678
@ -199,7 +199,7 @@ ANDROID_SHA256_CERT_FINGERPRINT=AC:73:8E:DE:EB:56:EA:CC:10:87:02:A7:65:37:7B:38:
|
|||||||
## Rack Attack configuration
|
## Rack Attack configuration
|
||||||
## To prevent and throttle abusive requests
|
## To prevent and throttle abusive requests
|
||||||
# ENABLE_RACK_ATTACK=true
|
# ENABLE_RACK_ATTACK=true
|
||||||
# RACK_ATTACK_IP_LIMIT=3000
|
# RACK_ATTACK_LIMIT=300
|
||||||
# ENABLE_RACK_ATTACK_WIDGET_API=true
|
# ENABLE_RACK_ATTACK_WIDGET_API=true
|
||||||
|
|
||||||
## Running chatwoot as an API only server
|
## Running chatwoot as an API only server
|
||||||
|
|||||||
10
.eslintrc.js
10
.eslintrc.js
@ -6,7 +6,7 @@ module.exports = {
|
|||||||
'plugin:storybook/recommended',
|
'plugin:storybook/recommended',
|
||||||
],
|
],
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
parser: 'babel-eslint',
|
parser: '@babel/eslint-parser',
|
||||||
ecmaVersion: 2020,
|
ecmaVersion: 2020,
|
||||||
sourceType: 'module',
|
sourceType: 'module',
|
||||||
},
|
},
|
||||||
@ -24,13 +24,16 @@ module.exports = {
|
|||||||
'jsx-a11y/label-has-for': 'off',
|
'jsx-a11y/label-has-for': 'off',
|
||||||
'jsx-a11y/anchor-is-valid': 'off',
|
'jsx-a11y/anchor-is-valid': 'off',
|
||||||
'import/no-unresolved': 'off',
|
'import/no-unresolved': 'off',
|
||||||
|
'vue/html-indent': 'off',
|
||||||
|
'vue/multi-word-component-names': 'off',
|
||||||
'vue/max-attributes-per-line': [
|
'vue/max-attributes-per-line': [
|
||||||
'error',
|
'error',
|
||||||
{
|
{
|
||||||
singleline: 20,
|
singleline: {
|
||||||
|
max: 20,
|
||||||
|
},
|
||||||
multiline: {
|
multiline: {
|
||||||
max: 1,
|
max: 1,
|
||||||
allowFirstLine: false,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@ -47,6 +50,7 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
'vue/no-v-html': 'off',
|
'vue/no-v-html': 'off',
|
||||||
|
'vue/component-definition-name-casing': 'off',
|
||||||
'vue/singleline-html-element-content-newline': 'off',
|
'vue/singleline-html-element-content-newline': 'off',
|
||||||
'import/extensions': ['off'],
|
'import/extensions': ['off'],
|
||||||
'no-console': 'error',
|
'no-console': 'error',
|
||||||
|
|||||||
7
.github/workflows/run_response_bot_spec.yml
vendored
7
.github/workflows/run_response_bot_spec.yml
vendored
@ -69,7 +69,12 @@ jobs:
|
|||||||
# Run Response Bot specs
|
# Run Response Bot specs
|
||||||
- name: Run backend tests
|
- name: Run backend tests
|
||||||
run: |
|
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
|
- name: Upload rails log folder
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
|
|||||||
@ -58,6 +58,7 @@ Metrics/BlockLength:
|
|||||||
Metrics/ModuleLength:
|
Metrics/ModuleLength:
|
||||||
Exclude:
|
Exclude:
|
||||||
- lib/seeders/message_seeder.rb
|
- lib/seeders/message_seeder.rb
|
||||||
|
- spec/support/slack_stubs.rb
|
||||||
Rails/ApplicationController:
|
Rails/ApplicationController:
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'app/controllers/api/v1/widget/messages_controller.rb'
|
- 'app/controllers/api/v1/widget/messages_controller.rb'
|
||||||
|
|||||||
14
Gemfile
14
Gemfile
@ -15,7 +15,7 @@ gem 'browser'
|
|||||||
gem 'hashie'
|
gem 'hashie'
|
||||||
gem 'jbuilder'
|
gem 'jbuilder'
|
||||||
gem 'kaminari'
|
gem 'kaminari'
|
||||||
gem 'responders'
|
gem 'responders', '>= 3.1.1'
|
||||||
gem 'rest-client'
|
gem 'rest-client'
|
||||||
gem 'telephone_number'
|
gem 'telephone_number'
|
||||||
gem 'time_diff'
|
gem 'time_diff'
|
||||||
@ -67,7 +67,7 @@ gem 'webpacker'
|
|||||||
gem 'barnes'
|
gem 'barnes'
|
||||||
|
|
||||||
##--- gems for authentication & authorization ---##
|
##--- 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-secure_password', git: 'https://github.com/chatwoot/devise-secure_password', branch: 'chatwoot'
|
||||||
gem 'devise_token_auth'
|
gem 'devise_token_auth'
|
||||||
# authorization
|
# authorization
|
||||||
@ -92,7 +92,7 @@ gem 'twitty', '~> 0.1.5'
|
|||||||
# facebook client
|
# facebook client
|
||||||
gem 'koala'
|
gem 'koala'
|
||||||
# slack client
|
# slack client
|
||||||
gem 'slack-ruby-client', '~> 2.0.0'
|
gem 'slack-ruby-client', '~> 2.2.0'
|
||||||
# for dialogflow integrations
|
# for dialogflow integrations
|
||||||
gem 'google-cloud-dialogflow-v2'
|
gem 'google-cloud-dialogflow-v2'
|
||||||
gem 'grpc'
|
gem 'grpc'
|
||||||
@ -109,9 +109,9 @@ gem 'elastic-apm', require: false
|
|||||||
gem 'newrelic_rpm', require: false
|
gem 'newrelic_rpm', require: false
|
||||||
gem 'newrelic-sidekiq-metrics', require: false
|
gem 'newrelic-sidekiq-metrics', require: false
|
||||||
gem 'scout_apm', require: false
|
gem 'scout_apm', require: false
|
||||||
gem 'sentry-rails', '>= 5.11.0', require: false
|
gem 'sentry-rails', '>= 5.12.0', require: false
|
||||||
gem 'sentry-ruby', require: false
|
gem 'sentry-ruby', require: false
|
||||||
gem 'sentry-sidekiq', '>= 5.11.0', require: false
|
gem 'sentry-sidekiq', '>= 5.12.0', require: false
|
||||||
|
|
||||||
##-- background job processing --##
|
##-- background job processing --##
|
||||||
gem 'sidekiq', '>= 7.1.3'
|
gem 'sidekiq', '>= 7.1.3'
|
||||||
@ -154,12 +154,12 @@ gem 'stripe'
|
|||||||
gem 'faker'
|
gem 'faker'
|
||||||
|
|
||||||
# Include logrange conditionally in intializer using env variable
|
# 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
|
# worked with microsoft refresh token
|
||||||
gem 'omniauth-oauth2'
|
gem 'omniauth-oauth2'
|
||||||
|
|
||||||
gem 'audited', '~> 5.3'
|
gem 'audited', '~> 5.4', '>= 5.4.0'
|
||||||
|
|
||||||
# need for google auth
|
# need for google auth
|
||||||
gem 'omniauth'
|
gem 'omniauth'
|
||||||
|
|||||||
47
Gemfile.lock
47
Gemfile.lock
@ -126,8 +126,8 @@ GEM
|
|||||||
rake (>= 10.4, < 14.0)
|
rake (>= 10.4, < 14.0)
|
||||||
ast (2.4.2)
|
ast (2.4.2)
|
||||||
attr_extras (7.1.0)
|
attr_extras (7.1.0)
|
||||||
audited (5.3.3)
|
audited (5.4.0)
|
||||||
activerecord (>= 5.0, < 7.1)
|
activerecord (>= 5.0, < 7.2)
|
||||||
request_store (~> 1.2)
|
request_store (~> 1.2)
|
||||||
aws-eventstream (1.2.0)
|
aws-eventstream (1.2.0)
|
||||||
aws-partitions (1.760.0)
|
aws-partitions (1.760.0)
|
||||||
@ -148,7 +148,7 @@ GEM
|
|||||||
barnes (0.0.9)
|
barnes (0.0.9)
|
||||||
multi_json (~> 1)
|
multi_json (~> 1)
|
||||||
statsd-ruby (~> 1.1)
|
statsd-ruby (~> 1.1)
|
||||||
bcrypt (3.1.18)
|
bcrypt (3.1.19)
|
||||||
bindex (0.8.1)
|
bindex (0.8.1)
|
||||||
blingfire (0.1.8)
|
blingfire (0.1.8)
|
||||||
bootsnap (1.16.0)
|
bootsnap (1.16.0)
|
||||||
@ -193,7 +193,7 @@ GEM
|
|||||||
irb (>= 1.5.0)
|
irb (>= 1.5.0)
|
||||||
reline (>= 0.3.1)
|
reline (>= 0.3.1)
|
||||||
declarative (0.0.20)
|
declarative (0.0.20)
|
||||||
devise (4.9.2)
|
devise (4.9.3)
|
||||||
bcrypt (~> 3.0)
|
bcrypt (~> 3.0)
|
||||||
orm_adapter (~> 0.1)
|
orm_adapter (~> 0.1)
|
||||||
railties (>= 4.1.0)
|
railties (>= 4.1.0)
|
||||||
@ -273,7 +273,7 @@ GEM
|
|||||||
googleauth (~> 1.0)
|
googleauth (~> 1.0)
|
||||||
grpc (~> 1.36)
|
grpc (~> 1.36)
|
||||||
geocoder (1.8.1)
|
geocoder (1.8.1)
|
||||||
gli (2.21.0)
|
gli (2.21.1)
|
||||||
globalid (1.2.1)
|
globalid (1.2.1)
|
||||||
activesupport (>= 6.1)
|
activesupport (>= 6.1)
|
||||||
gmail_xoauth (0.4.2)
|
gmail_xoauth (0.4.2)
|
||||||
@ -434,12 +434,12 @@ GEM
|
|||||||
llhttp-ffi (0.4.0)
|
llhttp-ffi (0.4.0)
|
||||||
ffi-compiler (~> 1.0)
|
ffi-compiler (~> 1.0)
|
||||||
rake (~> 13.0)
|
rake (~> 13.0)
|
||||||
lograge (0.13.0)
|
lograge (0.14.0)
|
||||||
actionpack (>= 4)
|
actionpack (>= 4)
|
||||||
activesupport (>= 4)
|
activesupport (>= 4)
|
||||||
railties (>= 4)
|
railties (>= 4)
|
||||||
request_store (~> 1.0)
|
request_store (~> 1.0)
|
||||||
loofah (2.21.3)
|
loofah (2.21.4)
|
||||||
crass (~> 1.0.2)
|
crass (~> 1.0.2)
|
||||||
nokogiri (>= 1.12.0)
|
nokogiri (>= 1.12.0)
|
||||||
mail (2.8.1)
|
mail (2.8.1)
|
||||||
@ -616,7 +616,7 @@ GEM
|
|||||||
uber (< 0.2.0)
|
uber (< 0.2.0)
|
||||||
request_store (1.5.1)
|
request_store (1.5.1)
|
||||||
rack (>= 1.4)
|
rack (>= 1.4)
|
||||||
responders (3.1.0)
|
responders (3.1.1)
|
||||||
actionpack (>= 5.2)
|
actionpack (>= 5.2)
|
||||||
railties (>= 5.2)
|
railties (>= 5.2)
|
||||||
rest-client (2.1.0)
|
rest-client (2.1.0)
|
||||||
@ -702,18 +702,18 @@ GEM
|
|||||||
activesupport (>= 4)
|
activesupport (>= 4)
|
||||||
selectize-rails (0.12.6)
|
selectize-rails (0.12.6)
|
||||||
semantic_range (3.0.0)
|
semantic_range (3.0.0)
|
||||||
sentry-rails (5.11.0)
|
sentry-rails (5.12.0)
|
||||||
railties (>= 5.0)
|
railties (>= 5.0)
|
||||||
sentry-ruby (~> 5.11.0)
|
sentry-ruby (~> 5.12.0)
|
||||||
sentry-ruby (5.11.0)
|
sentry-ruby (5.12.0)
|
||||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||||
sentry-sidekiq (5.11.0)
|
sentry-sidekiq (5.12.0)
|
||||||
sentry-ruby (~> 5.11.0)
|
sentry-ruby (~> 5.12.0)
|
||||||
sidekiq (>= 3.0)
|
sidekiq (>= 3.0)
|
||||||
sexp_processor (4.17.0)
|
sexp_processor (4.17.0)
|
||||||
shoulda-matchers (5.3.0)
|
shoulda-matchers (5.3.0)
|
||||||
activesupport (>= 5.2.0)
|
activesupport (>= 5.2.0)
|
||||||
sidekiq (7.1.3)
|
sidekiq (7.1.6)
|
||||||
concurrent-ruby (< 2)
|
concurrent-ruby (< 2)
|
||||||
connection_pool (>= 2.3.0)
|
connection_pool (>= 2.3.0)
|
||||||
rack (>= 2.2.4)
|
rack (>= 2.2.4)
|
||||||
@ -732,13 +732,12 @@ GEM
|
|||||||
json (>= 1.8, < 3)
|
json (>= 1.8, < 3)
|
||||||
simplecov-html (~> 0.10.0)
|
simplecov-html (~> 0.10.0)
|
||||||
simplecov-html (0.10.2)
|
simplecov-html (0.10.2)
|
||||||
slack-ruby-client (2.0.0)
|
slack-ruby-client (2.2.0)
|
||||||
faraday (>= 2.0)
|
faraday (>= 2.0)
|
||||||
faraday-mashify
|
faraday-mashify
|
||||||
faraday-multipart
|
faraday-multipart
|
||||||
gli
|
gli
|
||||||
hashie
|
hashie
|
||||||
websocket-driver
|
|
||||||
snaky_hash (2.0.1)
|
snaky_hash (2.0.1)
|
||||||
hashie
|
hashie
|
||||||
version_gem (~> 1.1, >= 1.1.1)
|
version_gem (~> 1.1, >= 1.1.1)
|
||||||
@ -817,7 +816,7 @@ GEM
|
|||||||
working_hours (1.4.1)
|
working_hours (1.4.1)
|
||||||
activesupport (>= 3.2)
|
activesupport (>= 3.2)
|
||||||
tzinfo
|
tzinfo
|
||||||
zeitwerk (2.6.11)
|
zeitwerk (2.6.12)
|
||||||
|
|
||||||
PLATFORMS
|
PLATFORMS
|
||||||
arm64-darwin-20
|
arm64-darwin-20
|
||||||
@ -839,7 +838,7 @@ DEPENDENCIES
|
|||||||
administrate-field-belongs_to_search
|
administrate-field-belongs_to_search
|
||||||
annotate
|
annotate
|
||||||
attr_extras
|
attr_extras
|
||||||
audited (~> 5.3)
|
audited (~> 5.4, >= 5.4.0)
|
||||||
aws-sdk-s3
|
aws-sdk-s3
|
||||||
azure-storage-blob!
|
azure-storage-blob!
|
||||||
barnes
|
barnes
|
||||||
@ -856,7 +855,7 @@ DEPENDENCIES
|
|||||||
database_cleaner
|
database_cleaner
|
||||||
ddtrace
|
ddtrace
|
||||||
debug (~> 1.8)
|
debug (~> 1.8)
|
||||||
devise
|
devise (>= 4.9.3)
|
||||||
devise-secure_password!
|
devise-secure_password!
|
||||||
devise_token_auth
|
devise_token_auth
|
||||||
dotenv-rails
|
dotenv-rails
|
||||||
@ -892,7 +891,7 @@ DEPENDENCIES
|
|||||||
line-bot-api
|
line-bot-api
|
||||||
liquid
|
liquid
|
||||||
listen
|
listen
|
||||||
lograge (~> 0.13.0)
|
lograge (~> 0.14.0)
|
||||||
maxminddb
|
maxminddb
|
||||||
mock_redis
|
mock_redis
|
||||||
neighbor
|
neighbor
|
||||||
@ -916,7 +915,7 @@ DEPENDENCIES
|
|||||||
rails (~> 7.0.8.0)
|
rails (~> 7.0.8.0)
|
||||||
redis
|
redis
|
||||||
redis-namespace
|
redis-namespace
|
||||||
responders
|
responders (>= 3.1.1)
|
||||||
rest-client
|
rest-client
|
||||||
reverse_markdown
|
reverse_markdown
|
||||||
rspec-rails
|
rspec-rails
|
||||||
@ -928,14 +927,14 @@ DEPENDENCIES
|
|||||||
scout_apm
|
scout_apm
|
||||||
scss_lint
|
scss_lint
|
||||||
seed_dump
|
seed_dump
|
||||||
sentry-rails (>= 5.11.0)
|
sentry-rails (>= 5.12.0)
|
||||||
sentry-ruby
|
sentry-ruby
|
||||||
sentry-sidekiq (>= 5.11.0)
|
sentry-sidekiq (>= 5.12.0)
|
||||||
shoulda-matchers
|
shoulda-matchers
|
||||||
sidekiq (>= 7.1.3)
|
sidekiq (>= 7.1.3)
|
||||||
sidekiq-cron (>= 1.10.1)
|
sidekiq-cron (>= 1.10.1)
|
||||||
simplecov (= 0.17.1)
|
simplecov (= 0.17.1)
|
||||||
slack-ruby-client (~> 2.0.0)
|
slack-ruby-client (~> 2.2.0)
|
||||||
spring
|
spring
|
||||||
spring-watcher-listen
|
spring-watcher-listen
|
||||||
squasher
|
squasher
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
2.4.0
|
2.6.0
|
||||||
|
|||||||
@ -24,14 +24,6 @@ select {
|
|||||||
font-size: $base-font-size;
|
font-size: $base-font-size;
|
||||||
}
|
}
|
||||||
|
|
||||||
input,
|
|
||||||
select,
|
|
||||||
textarea {
|
|
||||||
display: block;
|
|
||||||
font-family: $base-font-family;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
[type="color"],
|
[type="color"],
|
||||||
[type="date"],
|
[type="date"],
|
||||||
[type="datetime-local"],
|
[type="datetime-local"],
|
||||||
@ -51,6 +43,7 @@ textarea {
|
|||||||
background-color: $white;
|
background-color: $white;
|
||||||
border: $base-border;
|
border: $base-border;
|
||||||
border-radius: $base-border-radius;
|
border-radius: $base-border-radius;
|
||||||
|
font-family: $base-font-family;
|
||||||
padding: 0.5em;
|
padding: 0.5em;
|
||||||
transition: border-color $base-duration $base-timing;
|
transition: border-color $base-duration $base-timing;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
@ -4,8 +4,8 @@
|
|||||||
float: left;
|
float: left;
|
||||||
margin-bottom: $base-spacing;
|
margin-bottom: $base-spacing;
|
||||||
margin-top: 0.25em;
|
margin-top: 0.25em;
|
||||||
text-align: right;
|
text-align: left;
|
||||||
width: calc(15% - 1rem);
|
width: calc(16% - 1rem);
|
||||||
}
|
}
|
||||||
|
|
||||||
.preserve-whitespace {
|
.preserve-whitespace {
|
||||||
@ -17,7 +17,7 @@
|
|||||||
float: left;
|
float: left;
|
||||||
margin-bottom: $base-spacing;
|
margin-bottom: $base-spacing;
|
||||||
margin-left: 2rem;
|
margin-left: 2rem;
|
||||||
width: calc(85% - 1rem);
|
width: calc(84% - 1rem);
|
||||||
}
|
}
|
||||||
|
|
||||||
.attribute--nested {
|
.attribute--nested {
|
||||||
|
|||||||
@ -10,7 +10,7 @@ input[type="submit"],
|
|||||||
color: $white;
|
color: $white;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
font-size: $font-size-default;
|
font-size: $font-size-small;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
font-weight: $font-weight-medium;
|
font-weight: $font-weight-medium;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
|
|||||||
@ -1,13 +1,18 @@
|
|||||||
.main-content {
|
.main-content {
|
||||||
font-size: $font-size-default;
|
font-size: $font-size-default;
|
||||||
left: 23rem;
|
left: 21rem;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 0;
|
right: 0;
|
||||||
top: 0;
|
top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-content__body {
|
.main-content__body {
|
||||||
|
font-size: $font-size-small;
|
||||||
padding: $space-two;
|
padding: $space-two;
|
||||||
|
|
||||||
|
table {
|
||||||
|
font-size: $font-size-small;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-content__header {
|
.main-content__header {
|
||||||
@ -20,7 +25,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.main-content__page-title {
|
.main-content__page-title {
|
||||||
font-size: $font-size-large;
|
font-size: $font-size-medium;
|
||||||
font-weight: $font-weight-medium;
|
font-weight: $font-weight-medium;
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,12 @@
|
|||||||
.logo-brand {
|
.logo-brand {
|
||||||
margin-bottom: $space-normal;
|
margin-bottom: $space-normal;
|
||||||
padding: $space-normal $space-smaller $space-small;
|
padding: $space-normal $space-smaller $space-small;
|
||||||
text-align: center;
|
text-align: left;
|
||||||
|
|
||||||
|
img {
|
||||||
|
margin-bottom: $space-smaller;
|
||||||
|
max-height: 3rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.navigation {
|
.navigation {
|
||||||
@ -19,12 +24,13 @@
|
|||||||
padding: $space-normal;
|
padding: $space-normal;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
width: 23rem;
|
width: 21rem;
|
||||||
z-index: 1023;
|
z-index: 1023;
|
||||||
|
|
||||||
li {
|
li {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
font-size: $font-size-small;
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: $color-gray;
|
color: $color-gray;
|
||||||
@ -35,6 +41,10 @@
|
|||||||
min-width: $space-medium;
|
min-width: $space-medium;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
margin: $space-slab;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.navigation__link {
|
.navigation__link {
|
||||||
@ -43,7 +53,7 @@
|
|||||||
display: block;
|
display: block;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
margin-bottom: $space-smaller;
|
margin-bottom: $space-smaller;
|
||||||
padding: $space-one;
|
padding: $space-small;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: $blue;
|
color: $blue;
|
||||||
|
|||||||
@ -4,7 +4,7 @@ $base-font-family: PlusJakarta, Inter, -apple-system, BlinkMacSystemFont, "Segoe
|
|||||||
sans-serif !default;
|
sans-serif !default;
|
||||||
$heading-font-family: $base-font-family !default;
|
$heading-font-family: $base-font-family !default;
|
||||||
|
|
||||||
$base-font-size: 16px !default;
|
$base-font-size: 14px !default;
|
||||||
|
|
||||||
$base-line-height: 1.5 !default;
|
$base-line-height: 1.5 !default;
|
||||||
$heading-line-height: 1.2 !default;
|
$heading-line-height: 1.2 !default;
|
||||||
|
|||||||
@ -93,6 +93,9 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
|
|||||||
message_type: @message_type,
|
message_type: @message_type,
|
||||||
content: response.content,
|
content: response.content,
|
||||||
source_id: response.identifier,
|
source_id: response.identifier,
|
||||||
|
content_attributes: {
|
||||||
|
in_reply_to_external_id: response.in_reply_to_external_id
|
||||||
|
},
|
||||||
sender: @outgoing_echo ? nil : @contact_inbox.contact
|
sender: @outgoing_echo ? nil : @contact_inbox.contact
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|||||||
@ -15,7 +15,10 @@ class V2::ReportBuilder
|
|||||||
end
|
end
|
||||||
|
|
||||||
def timeseries
|
def timeseries
|
||||||
send(params[:metric])
|
return send(params[:metric]) if metric_valid?
|
||||||
|
|
||||||
|
Rails.logger.error "ReportBuilder: Invalid metric - #{params[:metric]}"
|
||||||
|
{}
|
||||||
end
|
end
|
||||||
|
|
||||||
# For backward compatible with old report
|
# For backward compatible with old report
|
||||||
@ -53,6 +56,16 @@ class V2::ReportBuilder
|
|||||||
|
|
||||||
private
|
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
|
def inbox
|
||||||
@inbox ||= account.inboxes.find(params[:id])
|
@inbox ||= account.inboxes.find(params[:id])
|
||||||
end
|
end
|
||||||
|
|||||||
@ -9,7 +9,6 @@ class Api::V1::Accounts::Actions::ContactMergesController < Api::V1::Accounts::B
|
|||||||
mergee_contact: @mergee_contact
|
mergee_contact: @mergee_contact
|
||||||
)
|
)
|
||||||
contact_merge_action.perform
|
contact_merge_action.perform
|
||||||
render json: @base_contact
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|||||||
@ -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
|
end
|
||||||
|
|
||||||
def toggle_status
|
def toggle_status
|
||||||
if params[:status]
|
if params[:status].present?
|
||||||
set_conversation_status
|
set_conversation_status
|
||||||
@status = @conversation.save!
|
@status = @conversation.save!
|
||||||
else
|
else
|
||||||
|
|||||||
@ -1,7 +1,15 @@
|
|||||||
class Api::V1::Integrations::WebhooksController < ApplicationController
|
class Api::V1::Integrations::WebhooksController < ApplicationController
|
||||||
def create
|
def create
|
||||||
builder = Integrations::Slack::IncomingMessageBuilder.new(params)
|
builder = Integrations::Slack::IncomingMessageBuilder.new(permitted_params)
|
||||||
response = builder.perform
|
response = builder.perform
|
||||||
render json: response
|
render json: response
|
||||||
end
|
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
|
end
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
|
class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
|
||||||
include Events::Types
|
include Events::Types
|
||||||
|
before_action :render_not_found_if_empty, only: [:toggle_typing, :toggle_status, :set_custom_attributes, :destroy_custom_attributes]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@conversation = conversation
|
@conversation = conversation
|
||||||
@ -27,6 +28,7 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
|
|||||||
|
|
||||||
conversation.contact_last_seen_at = DateTime.now.utc
|
conversation.contact_last_seen_at = DateTime.now.utc
|
||||||
conversation.save!
|
conversation.save!
|
||||||
|
::Conversations::MarkMessagesAsReadJob.perform_later(conversation)
|
||||||
head :ok
|
head :ok
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -41,8 +43,6 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def toggle_typing
|
def toggle_typing
|
||||||
head :ok && return if conversation.nil?
|
|
||||||
|
|
||||||
case permitted_params[:typing_status]
|
case permitted_params[:typing_status]
|
||||||
when 'on'
|
when 'on'
|
||||||
trigger_typing_event(CONVERSATION_TYPING_ON)
|
trigger_typing_event(CONVERSATION_TYPING_ON)
|
||||||
@ -54,8 +54,6 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def toggle_status
|
def toggle_status
|
||||||
return head :not_found if conversation.nil?
|
|
||||||
|
|
||||||
return head :forbidden unless @web_widget.end_conversation?
|
return head :forbidden unless @web_widget.end_conversation?
|
||||||
|
|
||||||
unless conversation.resolved?
|
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)
|
Rails.configuration.dispatcher.dispatch(event, Time.zone.now, conversation: conversation, user: @contact)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def render_not_found_if_empty
|
||||||
|
return head :not_found if conversation.nil?
|
||||||
|
end
|
||||||
|
|
||||||
def permitted_params
|
def permitted_params
|
||||||
params.permit(:id, :typing_status, :website_token, :email, contact: [:name, :email, :phone_number],
|
params.permit(:id, :typing_status, :website_token, :email, contact: [:name, :email, :phone_number],
|
||||||
message: [:content, :referer_url, :timestamp, :echo_id],
|
message: [:content, :referer_url, :timestamp, :echo_id],
|
||||||
|
|||||||
@ -9,11 +9,14 @@ module RequestExceptionHandler
|
|||||||
|
|
||||||
def handle_with_exception
|
def handle_with_exception
|
||||||
yield
|
yield
|
||||||
rescue ActiveRecord::RecordNotFound
|
rescue ActiveRecord::RecordNotFound => e
|
||||||
|
log_handled_error(e)
|
||||||
render_not_found_error('Resource could not be found')
|
render_not_found_error('Resource could not be found')
|
||||||
rescue Pundit::NotAuthorizedError
|
rescue Pundit::NotAuthorizedError => e
|
||||||
|
log_handled_error(e)
|
||||||
render_unauthorized('You are not authorized to do this action')
|
render_unauthorized('You are not authorized to do this action')
|
||||||
rescue ActionController::ParameterMissing => e
|
rescue ActionController::ParameterMissing => e
|
||||||
|
log_handled_error(e)
|
||||||
render_could_not_create_error(e.message)
|
render_could_not_create_error(e.message)
|
||||||
ensure
|
ensure
|
||||||
# to address the thread variable leak issues in Puma/Thin webserver
|
# to address the thread variable leak issues in Puma/Thin webserver
|
||||||
@ -41,6 +44,7 @@ module RequestExceptionHandler
|
|||||||
end
|
end
|
||||||
|
|
||||||
def render_record_invalid(exception)
|
def render_record_invalid(exception)
|
||||||
|
log_handled_error(exception)
|
||||||
render json: {
|
render json: {
|
||||||
message: exception.record.errors.full_messages.join(', '),
|
message: exception.record.errors.full_messages.join(', '),
|
||||||
attributes: exception.record.errors.attribute_names
|
attributes: exception.record.errors.attribute_names
|
||||||
@ -48,6 +52,11 @@ module RequestExceptionHandler
|
|||||||
end
|
end
|
||||||
|
|
||||||
def render_error_response(exception)
|
def render_error_response(exception)
|
||||||
|
log_handled_error(exception)
|
||||||
render json: exception.to_hash, status: exception.http_status
|
render json: exception.to_hash, status: exception.http_status
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def log_handled_error(exception)
|
||||||
|
logger.info("Handled error: #{exception.inspect}")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@ -33,7 +33,7 @@ class Public::Api::V1::Inboxes::ConversationsController < Public::Api::V1::Inbox
|
|||||||
end
|
end
|
||||||
|
|
||||||
def create_conversation
|
def create_conversation
|
||||||
::Conversation.create!(conversation_params)
|
ConversationBuilder.new(params: conversation_params, contact_inbox: @contact_inbox).perform
|
||||||
end
|
end
|
||||||
|
|
||||||
def trigger_typing_event(event)
|
def trigger_typing_event(event)
|
||||||
@ -41,11 +41,6 @@ class Public::Api::V1::Inboxes::ConversationsController < Public::Api::V1::Inbox
|
|||||||
end
|
end
|
||||||
|
|
||||||
def conversation_params
|
def conversation_params
|
||||||
{
|
params.permit(custom_attributes: {})
|
||||||
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
|
|
||||||
}
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
class Public::Api::V1::Portals::BaseController < PublicController
|
class Public::Api::V1::Portals::BaseController < PublicController
|
||||||
before_action :show_plain_layout
|
before_action :show_plain_layout
|
||||||
|
before_action :set_color_scheme
|
||||||
around_action :set_locale
|
around_action :set_locale
|
||||||
after_action :allow_iframe_requests
|
after_action :allow_iframe_requests
|
||||||
|
|
||||||
@ -9,6 +10,14 @@ class Public::Api::V1::Portals::BaseController < PublicController
|
|||||||
@is_plain_layout_enabled = params[:show_plain_layout] == 'true'
|
@is_plain_layout_enabled = params[:show_plain_layout] == 'true'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def set_color_scheme
|
||||||
|
@theme = if %w[dark light].include?(params[:theme])
|
||||||
|
params[:theme]
|
||||||
|
else
|
||||||
|
''
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def set_locale(&)
|
def set_locale(&)
|
||||||
switch_locale_with_portal(&) if params[:locale].present?
|
switch_locale_with_portal(&) if params[:locale].present?
|
||||||
switch_locale_with_article(&) if params[:article_slug].present?
|
switch_locale_with_article(&) if params[:article_slug].present?
|
||||||
|
|||||||
27
app/controllers/slack_uploads_controller.rb
Normal file
27
app/controllers/slack_uploads_controller.rb
Normal file
@ -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
|
||||||
21
app/controllers/twilio/delivery_status_controller.rb
Normal file
21
app/controllers/twilio/delivery_status_controller.rb
Normal file
@ -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
|
||||||
11
app/helpers/portal_helper.rb
Normal file
11
app/helpers/portal_helper.rb
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
module PortalHelper
|
||||||
|
def generate_portal_bg_color(portal_color, theme)
|
||||||
|
base_color = theme == 'dark' ? 'black' : 'white'
|
||||||
|
"color-mix(in srgb, #{portal_color} 10%, #{base_color})"
|
||||||
|
end
|
||||||
|
|
||||||
|
def generate_portal_bg(portal_color, theme)
|
||||||
|
bg_image = theme == 'dark' ? 'grid_dark.svg' : 'grid.svg'
|
||||||
|
"background: url(/assets/images/hc/#{bg_image}) #{generate_portal_bg_color(portal_color, theme)}"
|
||||||
|
end
|
||||||
|
end
|
||||||
@ -1,13 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
v-if="!authUIFlags.isFetching"
|
v-if="!authUIFlags.isFetching && !accountUIFlags.isFetchingItem"
|
||||||
id="app"
|
id="app"
|
||||||
class="app-wrapper h-full flex-grow-0 min-h-0 w-full"
|
class="app-wrapper h-full flex-grow-0 min-h-0 w-full"
|
||||||
:class="{ 'app-rtl--wrapper': isRTLView }"
|
:class="{ 'app-rtl--wrapper': isRTLView }"
|
||||||
:dir="isRTLView ? 'rtl' : 'ltr'"
|
:dir="isRTLView ? 'rtl' : 'ltr'"
|
||||||
>
|
>
|
||||||
<update-banner :latest-chatwoot-version="latestChatwootVersion" />
|
<update-banner :latest-chatwoot-version="latestChatwootVersion" />
|
||||||
<template v-if="!accountUIFlags.isFetchingItem && currentAccountId">
|
<template v-if="currentAccountId">
|
||||||
<payment-pending-banner />
|
<payment-pending-banner />
|
||||||
<upgrade-banner />
|
<upgrade-banner />
|
||||||
</template>
|
</template>
|
||||||
@ -111,10 +111,8 @@ export default {
|
|||||||
this.$store.dispatch('setActiveAccount', {
|
this.$store.dispatch('setActiveAccount', {
|
||||||
accountId: this.currentAccountId,
|
accountId: this.currentAccountId,
|
||||||
});
|
});
|
||||||
const {
|
const { locale, latest_chatwoot_version: latestChatwootVersion } =
|
||||||
locale,
|
this.getAccount(this.currentAccountId);
|
||||||
latest_chatwoot_version: latestChatwootVersion,
|
|
||||||
} = this.getAccount(this.currentAccountId);
|
|
||||||
const { pubsub_token: pubsubToken } = this.currentUser || {};
|
const { pubsub_token: pubsubToken } = this.currentUser || {};
|
||||||
this.setLocale(locale);
|
this.setLocale(locale);
|
||||||
this.updateRTLDirectionView(locale);
|
this.updateRTLDirectionView(locale);
|
||||||
|
|||||||
@ -15,9 +15,8 @@ class ApiClient {
|
|||||||
|
|
||||||
// eslint-disable-next-line class-methods-use-this
|
// eslint-disable-next-line class-methods-use-this
|
||||||
get accountIdFromRoute() {
|
get accountIdFromRoute() {
|
||||||
const isInsideAccountScopedURLs = window.location.pathname.includes(
|
const isInsideAccountScopedURLs =
|
||||||
'/app/accounts'
|
window.location.pathname.includes('/app/accounts');
|
||||||
);
|
|
||||||
|
|
||||||
if (isInsideAccountScopedURLs) {
|
if (isInsideAccountScopedURLs) {
|
||||||
return window.location.pathname.split('/')[3];
|
return window.location.pathname.split('/')[3];
|
||||||
|
|||||||
@ -84,6 +84,11 @@ export default {
|
|||||||
return axios.delete(endPoints('deleteAvatar').url);
|
return axios.delete(endPoints('deleteAvatar').url);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
resetPassword({ email }) {
|
||||||
|
const urlData = endPoints('resetPassword');
|
||||||
|
return axios.post(urlData.url, { email });
|
||||||
|
},
|
||||||
|
|
||||||
setActiveAccount({ accountId }) {
|
setActiveAccount({ accountId }) {
|
||||||
const urlData = endPoints('setActiveAccount');
|
const urlData = endPoints('setActiveAccount');
|
||||||
return axios.put(urlData.url, {
|
return axios.put(urlData.url, {
|
||||||
|
|||||||
@ -53,6 +53,7 @@ class ContactAPI extends ApiClient {
|
|||||||
return axios.get(requestURL);
|
return axios.get(requestURL);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line default-param-last
|
||||||
filter(page = 1, sortAttr = 'name', queryPayload) {
|
filter(page = 1, sortAttr = 'name', queryPayload) {
|
||||||
let requestURL = `${this.url}/filter?${buildContactParams(page, sortAttr)}`;
|
let requestURL = `${this.url}/filter?${buildContactParams(page, sortAttr)}`;
|
||||||
return axios.post(requestURL, queryPayload);
|
return axios.post(requestURL, queryPayload);
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import accountAPI from '../account';
|
import accountAPI from '../account';
|
||||||
import ApiClient from '../../ApiClient';
|
import ApiClient from '../../ApiClient';
|
||||||
import describeWithAPIMock from '../../specs/apiSpecHelper';
|
|
||||||
|
|
||||||
describe('#enterpriseAccountAPI', () => {
|
describe('#enterpriseAccountAPI', () => {
|
||||||
it('creates correct instance', () => {
|
it('creates correct instance', () => {
|
||||||
@ -13,17 +12,33 @@ describe('#enterpriseAccountAPI', () => {
|
|||||||
expect(accountAPI).toHaveProperty('checkout');
|
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', () => {
|
it('#checkout', () => {
|
||||||
accountAPI.checkout();
|
accountAPI.checkout();
|
||||||
expect(context.axiosMock.post).toHaveBeenCalledWith(
|
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||||
'/enterprise/api/v1/checkout'
|
'/enterprise/api/v1/checkout'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('#subscription', () => {
|
it('#subscription', () => {
|
||||||
accountAPI.subscription();
|
accountAPI.subscription();
|
||||||
expect(context.axiosMock.post).toHaveBeenCalledWith(
|
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||||
'/enterprise/api/v1/subscription'
|
'/enterprise/api/v1/subscription'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -31,6 +31,7 @@ class ReportsAPI extends ApiClient {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line default-param-last
|
||||||
getSummary(since, until, type = 'account', id, groupBy, businessHours) {
|
getSummary(since, until, type = 'account', id, groupBy, businessHours) {
|
||||||
return axios.get(`${this.url}/summary`, {
|
return axios.get(`${this.url}/summary`, {
|
||||||
params: {
|
params: {
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import accountAPI from '../account';
|
import accountAPI from '../account';
|
||||||
import ApiClient from '../ApiClient';
|
import ApiClient from '../ApiClient';
|
||||||
import describeWithAPIMock from './apiSpecHelper';
|
|
||||||
|
|
||||||
describe('#accountAPI', () => {
|
describe('#accountAPI', () => {
|
||||||
it('creates correct instance', () => {
|
it('creates correct instance', () => {
|
||||||
@ -13,12 +12,28 @@ describe('#accountAPI', () => {
|
|||||||
expect(accountAPI).toHaveProperty('createAccount');
|
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', () => {
|
it('#createAccount', () => {
|
||||||
accountAPI.createAccount({
|
accountAPI.createAccount({
|
||||||
name: 'Chatwoot',
|
name: 'Chatwoot',
|
||||||
});
|
});
|
||||||
expect(context.axiosMock.post).toHaveBeenCalledWith('/api/v1/accounts', {
|
expect(axiosMock.post).toHaveBeenCalledWith('/api/v1/accounts', {
|
||||||
name: 'Chatwoot',
|
name: 'Chatwoot',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import accountActionsAPI from '../accountActions';
|
import accountActionsAPI from '../accountActions';
|
||||||
import ApiClient from '../ApiClient';
|
import ApiClient from '../ApiClient';
|
||||||
import describeWithAPIMock from './apiSpecHelper';
|
|
||||||
|
|
||||||
describe('#ContactsAPI', () => {
|
describe('#ContactsAPI', () => {
|
||||||
it('creates correct instance', () => {
|
it('creates correct instance', () => {
|
||||||
@ -8,10 +7,26 @@ describe('#ContactsAPI', () => {
|
|||||||
expect(accountActionsAPI).toHaveProperty('merge');
|
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', () => {
|
it('#merge', () => {
|
||||||
accountActionsAPI.merge(1, 2);
|
accountActionsAPI.merge(1, 2);
|
||||||
expect(context.axiosMock.post).toHaveBeenCalledWith(
|
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||||
'/api/v1/actions/contact_merge',
|
'/api/v1/actions/contact_merge',
|
||||||
{
|
{
|
||||||
base_contact_id: 1,
|
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 articlesAPI from '../helpCenter/articles';
|
||||||
import ApiClient from 'dashboard/api/helpCenter/portals';
|
import ApiClient from 'dashboard/api/helpCenter/portals';
|
||||||
import describeWithAPIMock from './apiSpecHelper';
|
|
||||||
|
|
||||||
describe('#PortalAPI', () => {
|
describe('#PortalAPI', () => {
|
||||||
it('creates correct instance', () => {
|
it('creates correct instance', () => {
|
||||||
@ -12,7 +11,23 @@ describe('#PortalAPI', () => {
|
|||||||
expect(articlesAPI).toHaveProperty('delete');
|
expect(articlesAPI).toHaveProperty('delete');
|
||||||
expect(articlesAPI).toHaveProperty('getArticles');
|
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', () => {
|
it('#getArticles', () => {
|
||||||
articlesAPI.getArticles({
|
articlesAPI.getArticles({
|
||||||
pageNumber: 1,
|
pageNumber: 1,
|
||||||
@ -21,30 +36,62 @@ describe('#PortalAPI', () => {
|
|||||||
status: 'published',
|
status: 'published',
|
||||||
author_id: '1',
|
author_id: '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'
|
'/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', () => {
|
it('#getArticle', () => {
|
||||||
articlesAPI.getArticle({
|
articlesAPI.getArticle({
|
||||||
id: 1,
|
id: 1,
|
||||||
portalSlug: 'room-rental',
|
portalSlug: 'room-rental',
|
||||||
});
|
});
|
||||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
expect(axiosMock.get).toHaveBeenCalledWith(
|
||||||
'/api/v1/portals/room-rental/articles/1'
|
'/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('#updateArticle', () => {
|
it('#updateArticle', () => {
|
||||||
articlesAPI.updateArticle({
|
articlesAPI.updateArticle({
|
||||||
articleId: 1,
|
articleId: 1,
|
||||||
portalSlug: 'room-rental',
|
portalSlug: 'room-rental',
|
||||||
articleObj: { title: 'Update shipping address' },
|
articleObj: { title: 'Update shipping address' },
|
||||||
});
|
});
|
||||||
expect(context.axiosMock.patch).toHaveBeenCalledWith(
|
expect(axiosMock.patch).toHaveBeenCalledWith(
|
||||||
'/api/v1/portals/room-rental/articles/1',
|
'/api/v1/portals/room-rental/articles/1',
|
||||||
{
|
{
|
||||||
title: 'Update shipping address',
|
title: 'Update shipping address',
|
||||||
@ -52,13 +99,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', () => {
|
it('#deleteArticle', () => {
|
||||||
articlesAPI.deleteArticle({
|
articlesAPI.deleteArticle({
|
||||||
articleId: 1,
|
articleId: 1,
|
||||||
portalSlug: 'room-rental',
|
portalSlug: 'room-rental',
|
||||||
});
|
});
|
||||||
expect(context.axiosMock.delete).toHaveBeenCalledWith(
|
expect(axiosMock.delete).toHaveBeenCalledWith(
|
||||||
'/api/v1/portals/room-rental/articles/1'
|
'/api/v1/portals/room-rental/articles/1'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,18 +1,30 @@
|
|||||||
import assignableAgentsAPI from '../assignableAgents';
|
import assignableAgentsAPI from '../assignableAgents';
|
||||||
import describeWithAPIMock from './apiSpecHelper';
|
|
||||||
|
|
||||||
describe('#AssignableAgentsAPI', () => {
|
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', () => {
|
it('#getAssignableAgents', () => {
|
||||||
assignableAgentsAPI.get([1]);
|
assignableAgentsAPI.get([1]);
|
||||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
expect(axiosMock.get).toHaveBeenCalledWith('/api/v1/assignable_agents', {
|
||||||
'/api/v1/assignable_agents',
|
params: {
|
||||||
{
|
inbox_ids: [1],
|
||||||
params: {
|
},
|
||||||
inbox_ids: [1],
|
});
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import fbChannel from '../../channel/fbChannel';
|
import fbChannel from '../../channel/fbChannel';
|
||||||
import ApiClient from '../../ApiClient';
|
import ApiClient from '../../ApiClient';
|
||||||
import describeWithAPIMock from '../apiSpecHelper';
|
|
||||||
|
|
||||||
describe('#FBChannel', () => {
|
describe('#FBChannel', () => {
|
||||||
it('creates correct instance', () => {
|
it('creates correct instance', () => {
|
||||||
@ -11,10 +10,26 @@ describe('#FBChannel', () => {
|
|||||||
expect(fbChannel).toHaveProperty('update');
|
expect(fbChannel).toHaveProperty('update');
|
||||||
expect(fbChannel).toHaveProperty('delete');
|
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', () => {
|
it('#create', () => {
|
||||||
fbChannel.create({ omniauthToken: 'ASFM131CSF@#@$', appId: 'chatwoot' });
|
fbChannel.create({ omniauthToken: 'ASFM131CSF@#@$', appId: 'chatwoot' });
|
||||||
expect(context.axiosMock.post).toHaveBeenCalledWith(
|
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||||
'/api/v1/callbacks/register_facebook_page',
|
'/api/v1/callbacks/register_facebook_page',
|
||||||
{
|
{
|
||||||
omniauthToken: 'ASFM131CSF@#@$',
|
omniauthToken: 'ASFM131CSF@#@$',
|
||||||
@ -27,7 +42,7 @@ describe('#FBChannel', () => {
|
|||||||
omniauthToken: 'ASFM131CSF@#@$',
|
omniauthToken: 'ASFM131CSF@#@$',
|
||||||
inboxId: 1,
|
inboxId: 1,
|
||||||
});
|
});
|
||||||
expect(context.axiosMock.post).toHaveBeenCalledWith(
|
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||||
'/api/v1/callbacks/reauthorize_page',
|
'/api/v1/callbacks/reauthorize_page',
|
||||||
{
|
{
|
||||||
omniauth_token: 'ASFM131CSF@#@$',
|
omniauth_token: 'ASFM131CSF@#@$',
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import contactAPI, { buildContactParams } from '../contacts';
|
import contactAPI, { buildContactParams } from '../contacts';
|
||||||
import ApiClient from '../ApiClient';
|
import ApiClient from '../ApiClient';
|
||||||
import describeWithAPIMock from './apiSpecHelper';
|
|
||||||
|
|
||||||
describe('#ContactsAPI', () => {
|
describe('#ContactsAPI', () => {
|
||||||
it('creates correct instance', () => {
|
it('creates correct instance', () => {
|
||||||
@ -15,56 +14,67 @@ describe('#ContactsAPI', () => {
|
|||||||
expect(contactAPI).toHaveProperty('destroyAvatar');
|
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', () => {
|
it('#get', () => {
|
||||||
contactAPI.get(1, 'name', 'customer-support');
|
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'
|
'/api/v1/contacts?include_contact_inboxes=false&page=1&sort=name&labels[]=customer-support'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('#getConversations', () => {
|
it('#getConversations', () => {
|
||||||
contactAPI.getConversations(1);
|
contactAPI.getConversations(1);
|
||||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
expect(axiosMock.get).toHaveBeenCalledWith(
|
||||||
'/api/v1/contacts/1/conversations'
|
'/api/v1/contacts/1/conversations'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('#getContactableInboxes', () => {
|
it('#getContactableInboxes', () => {
|
||||||
contactAPI.getContactableInboxes(1);
|
contactAPI.getContactableInboxes(1);
|
||||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
expect(axiosMock.get).toHaveBeenCalledWith(
|
||||||
'/api/v1/contacts/1/contactable_inboxes'
|
'/api/v1/contacts/1/contactable_inboxes'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('#getContactLabels', () => {
|
it('#getContactLabels', () => {
|
||||||
contactAPI.getContactLabels(1);
|
contactAPI.getContactLabels(1);
|
||||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
expect(axiosMock.get).toHaveBeenCalledWith('/api/v1/contacts/1/labels');
|
||||||
'/api/v1/contacts/1/labels'
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('#updateContactLabels', () => {
|
it('#updateContactLabels', () => {
|
||||||
const labels = ['support-query'];
|
const labels = ['support-query'];
|
||||||
contactAPI.updateContactLabels(1, labels);
|
contactAPI.updateContactLabels(1, labels);
|
||||||
expect(context.axiosMock.post).toHaveBeenCalledWith(
|
expect(axiosMock.post).toHaveBeenCalledWith('/api/v1/contacts/1/labels', {
|
||||||
'/api/v1/contacts/1/labels',
|
labels,
|
||||||
{
|
});
|
||||||
labels,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('#search', () => {
|
it('#search', () => {
|
||||||
contactAPI.search('leads', 1, 'date', 'customer-support');
|
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'
|
'/api/v1/contacts/search?include_contact_inboxes=false&page=1&sort=date&q=leads&labels[]=customer-support'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('#destroyCustomAttributes', () => {
|
it('#destroyCustomAttributes', () => {
|
||||||
contactAPI.destroyCustomAttributes(1, ['cloudCustomer']);
|
contactAPI.destroyCustomAttributes(1, ['cloudCustomer']);
|
||||||
expect(context.axiosMock.post).toHaveBeenCalledWith(
|
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||||
'/api/v1/contacts/1/destroy_custom_attributes',
|
'/api/v1/contacts/1/destroy_custom_attributes',
|
||||||
{
|
{
|
||||||
custom_attributes: ['cloudCustomer'],
|
custom_attributes: ['cloudCustomer'],
|
||||||
@ -75,7 +85,7 @@ describe('#ContactsAPI', () => {
|
|||||||
it('#importContacts', () => {
|
it('#importContacts', () => {
|
||||||
const file = 'file';
|
const file = 'file';
|
||||||
contactAPI.importContacts(file);
|
contactAPI.importContacts(file);
|
||||||
expect(context.axiosMock.post).toHaveBeenCalledWith(
|
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||||
'/api/v1/contacts/import',
|
'/api/v1/contacts/import',
|
||||||
expect.any(FormData),
|
expect.any(FormData),
|
||||||
{
|
{
|
||||||
@ -96,7 +106,7 @@ describe('#ContactsAPI', () => {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
contactAPI.filter(1, 'name', queryPayload);
|
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',
|
'/api/v1/contacts/filter?include_contact_inboxes=false&page=1&sort=name',
|
||||||
queryPayload
|
queryPayload
|
||||||
);
|
);
|
||||||
@ -104,7 +114,7 @@ describe('#ContactsAPI', () => {
|
|||||||
|
|
||||||
it('#destroyAvatar', () => {
|
it('#destroyAvatar', () => {
|
||||||
contactAPI.destroyAvatar(1);
|
contactAPI.destroyAvatar(1);
|
||||||
expect(context.axiosMock.delete).toHaveBeenCalledWith(
|
expect(axiosMock.delete).toHaveBeenCalledWith(
|
||||||
'/api/v1/contacts/1/avatar'
|
'/api/v1/contacts/1/avatar'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import conversationsAPI from '../conversations';
|
import conversationsAPI from '../conversations';
|
||||||
import ApiClient from '../ApiClient';
|
import ApiClient from '../ApiClient';
|
||||||
import describeWithAPIMock from './apiSpecHelper';
|
|
||||||
|
|
||||||
describe('#ConversationApi', () => {
|
describe('#ConversationApi', () => {
|
||||||
it('creates correct instance', () => {
|
it('creates correct instance', () => {
|
||||||
@ -14,10 +13,26 @@ describe('#ConversationApi', () => {
|
|||||||
expect(conversationsAPI).toHaveProperty('updateLabels');
|
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', () => {
|
it('#getLabels', () => {
|
||||||
conversationsAPI.getLabels(1);
|
conversationsAPI.getLabels(1);
|
||||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
expect(axiosMock.get).toHaveBeenCalledWith(
|
||||||
'/api/v1/conversations/1/labels'
|
'/api/v1/conversations/1/labels'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -25,7 +40,7 @@ describe('#ConversationApi', () => {
|
|||||||
it('#updateLabels', () => {
|
it('#updateLabels', () => {
|
||||||
const labels = ['support-query'];
|
const labels = ['support-query'];
|
||||||
conversationsAPI.updateLabels(1, labels);
|
conversationsAPI.updateLabels(1, labels);
|
||||||
expect(context.axiosMock.post).toHaveBeenCalledWith(
|
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||||
'/api/v1/conversations/1/labels',
|
'/api/v1/conversations/1/labels',
|
||||||
{
|
{
|
||||||
labels,
|
labels,
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import csatReportsAPI from '../csatReports';
|
import csatReportsAPI from '../csatReports';
|
||||||
import ApiClient from '../ApiClient';
|
import ApiClient from '../ApiClient';
|
||||||
import describeWithAPIMock from './apiSpecHelper';
|
|
||||||
|
|
||||||
describe('#Reports API', () => {
|
describe('#Reports API', () => {
|
||||||
it('creates correct instance', () => {
|
it('creates correct instance', () => {
|
||||||
@ -9,10 +8,26 @@ describe('#Reports API', () => {
|
|||||||
expect(csatReportsAPI).toHaveProperty('get');
|
expect(csatReportsAPI).toHaveProperty('get');
|
||||||
expect(csatReportsAPI).toHaveProperty('getMetrics');
|
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', () => {
|
it('#get', () => {
|
||||||
csatReportsAPI.get({ page: 1, from: 1622485800, to: 1623695400 });
|
csatReportsAPI.get({ page: 1, from: 1622485800, to: 1623695400 });
|
||||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
expect(axiosMock.get).toHaveBeenCalledWith(
|
||||||
'/api/v1/csat_survey_responses',
|
'/api/v1/csat_survey_responses',
|
||||||
{
|
{
|
||||||
params: {
|
params: {
|
||||||
@ -26,7 +41,7 @@ describe('#Reports API', () => {
|
|||||||
});
|
});
|
||||||
it('#getMetrics', () => {
|
it('#getMetrics', () => {
|
||||||
csatReportsAPI.getMetrics({ from: 1622485800, to: 1623695400 });
|
csatReportsAPI.getMetrics({ from: 1622485800, to: 1623695400 });
|
||||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
expect(axiosMock.get).toHaveBeenCalledWith(
|
||||||
'/api/v1/csat_survey_responses/metrics',
|
'/api/v1/csat_survey_responses/metrics',
|
||||||
{
|
{
|
||||||
params: { since: 1622485800, until: 1623695400 },
|
params: { since: 1622485800, until: 1623695400 },
|
||||||
@ -39,7 +54,7 @@ describe('#Reports API', () => {
|
|||||||
to: 1623695400,
|
to: 1623695400,
|
||||||
user_ids: 1,
|
user_ids: 1,
|
||||||
});
|
});
|
||||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
expect(axiosMock.get).toHaveBeenCalledWith(
|
||||||
'/api/v1/csat_survey_responses/download',
|
'/api/v1/csat_survey_responses/download',
|
||||||
{
|
{
|
||||||
params: {
|
params: {
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import conversationAPI from '../../inbox/conversation';
|
import conversationAPI from '../../inbox/conversation';
|
||||||
import ApiClient from '../../ApiClient';
|
import ApiClient from '../../ApiClient';
|
||||||
import describeWithAPIMock from '../apiSpecHelper';
|
|
||||||
|
|
||||||
describe('#ConversationAPI', () => {
|
describe('#ConversationAPI', () => {
|
||||||
it('creates correct instance', () => {
|
it('creates correct instance', () => {
|
||||||
@ -22,7 +21,23 @@ describe('#ConversationAPI', () => {
|
|||||||
expect(conversationAPI).toHaveProperty('filter');
|
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', () => {
|
it('#get conversations', () => {
|
||||||
conversationAPI.get({
|
conversationAPI.get({
|
||||||
inboxId: 1,
|
inboxId: 1,
|
||||||
@ -32,19 +47,16 @@ describe('#ConversationAPI', () => {
|
|||||||
labels: [],
|
labels: [],
|
||||||
teamId: 1,
|
teamId: 1,
|
||||||
});
|
});
|
||||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
expect(axiosMock.get).toHaveBeenCalledWith('/api/v1/conversations', {
|
||||||
'/api/v1/conversations',
|
params: {
|
||||||
{
|
inbox_id: 1,
|
||||||
params: {
|
team_id: 1,
|
||||||
inbox_id: 1,
|
status: 'open',
|
||||||
team_id: 1,
|
assignee_type: 'me',
|
||||||
status: 'open',
|
page: 1,
|
||||||
assignee_type: 'me',
|
labels: [],
|
||||||
page: 1,
|
},
|
||||||
labels: [],
|
});
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('#search', () => {
|
it('#search', () => {
|
||||||
@ -53,7 +65,7 @@ describe('#ConversationAPI', () => {
|
|||||||
page: 1,
|
page: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
expect(axiosMock.get).toHaveBeenCalledWith(
|
||||||
'/api/v1/conversations/search',
|
'/api/v1/conversations/search',
|
||||||
{
|
{
|
||||||
params: {
|
params: {
|
||||||
@ -66,7 +78,7 @@ describe('#ConversationAPI', () => {
|
|||||||
|
|
||||||
it('#toggleStatus', () => {
|
it('#toggleStatus', () => {
|
||||||
conversationAPI.toggleStatus({ conversationId: 12, status: 'online' });
|
conversationAPI.toggleStatus({ conversationId: 12, status: 'online' });
|
||||||
expect(context.axiosMock.post).toHaveBeenCalledWith(
|
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||||
`/api/v1/conversations/12/toggle_status`,
|
`/api/v1/conversations/12/toggle_status`,
|
||||||
{
|
{
|
||||||
status: 'online',
|
status: 'online',
|
||||||
@ -77,7 +89,7 @@ describe('#ConversationAPI', () => {
|
|||||||
|
|
||||||
it('#assignAgent', () => {
|
it('#assignAgent', () => {
|
||||||
conversationAPI.assignAgent({ conversationId: 12, agentId: 34 });
|
conversationAPI.assignAgent({ conversationId: 12, agentId: 34 });
|
||||||
expect(context.axiosMock.post).toHaveBeenCalledWith(
|
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||||
`/api/v1/conversations/12/assignments?assignee_id=34`,
|
`/api/v1/conversations/12/assignments?assignee_id=34`,
|
||||||
{}
|
{}
|
||||||
);
|
);
|
||||||
@ -85,7 +97,7 @@ describe('#ConversationAPI', () => {
|
|||||||
|
|
||||||
it('#assignTeam', () => {
|
it('#assignTeam', () => {
|
||||||
conversationAPI.assignTeam({ conversationId: 12, teamId: 1 });
|
conversationAPI.assignTeam({ conversationId: 12, teamId: 1 });
|
||||||
expect(context.axiosMock.post).toHaveBeenCalledWith(
|
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||||
`/api/v1/conversations/12/assignments`,
|
`/api/v1/conversations/12/assignments`,
|
||||||
{
|
{
|
||||||
team_id: 1,
|
team_id: 1,
|
||||||
@ -95,7 +107,7 @@ describe('#ConversationAPI', () => {
|
|||||||
|
|
||||||
it('#markMessageRead', () => {
|
it('#markMessageRead', () => {
|
||||||
conversationAPI.markMessageRead({ id: 12 });
|
conversationAPI.markMessageRead({ id: 12 });
|
||||||
expect(context.axiosMock.post).toHaveBeenCalledWith(
|
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||||
`/api/v1/conversations/12/update_last_seen`
|
`/api/v1/conversations/12/update_last_seen`
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -105,7 +117,7 @@ describe('#ConversationAPI', () => {
|
|||||||
conversationId: 12,
|
conversationId: 12,
|
||||||
status: 'typing_on',
|
status: 'typing_on',
|
||||||
});
|
});
|
||||||
expect(context.axiosMock.post).toHaveBeenCalledWith(
|
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||||
`/api/v1/conversations/12/toggle_typing_status`,
|
`/api/v1/conversations/12/toggle_typing_status`,
|
||||||
{
|
{
|
||||||
typing_status: 'typing_on',
|
typing_status: 'typing_on',
|
||||||
@ -115,14 +127,14 @@ describe('#ConversationAPI', () => {
|
|||||||
|
|
||||||
it('#mute', () => {
|
it('#mute', () => {
|
||||||
conversationAPI.mute(45);
|
conversationAPI.mute(45);
|
||||||
expect(context.axiosMock.post).toHaveBeenCalledWith(
|
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||||
'/api/v1/conversations/45/mute'
|
'/api/v1/conversations/45/mute'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('#unmute', () => {
|
it('#unmute', () => {
|
||||||
conversationAPI.unmute(45);
|
conversationAPI.unmute(45);
|
||||||
expect(context.axiosMock.post).toHaveBeenCalledWith(
|
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||||
'/api/v1/conversations/45/unmute'
|
'/api/v1/conversations/45/unmute'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -135,18 +147,15 @@ describe('#ConversationAPI', () => {
|
|||||||
labels: [],
|
labels: [],
|
||||||
teamId: 1,
|
teamId: 1,
|
||||||
});
|
});
|
||||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
expect(axiosMock.get).toHaveBeenCalledWith('/api/v1/conversations/meta', {
|
||||||
'/api/v1/conversations/meta',
|
params: {
|
||||||
{
|
inbox_id: 1,
|
||||||
params: {
|
team_id: 1,
|
||||||
inbox_id: 1,
|
status: 'open',
|
||||||
team_id: 1,
|
assignee_type: 'me',
|
||||||
status: 'open',
|
labels: [],
|
||||||
assignee_type: 'me',
|
},
|
||||||
labels: [],
|
});
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('#sendEmailTranscript', () => {
|
it('#sendEmailTranscript', () => {
|
||||||
@ -154,7 +163,7 @@ describe('#ConversationAPI', () => {
|
|||||||
conversationId: 45,
|
conversationId: 45,
|
||||||
email: 'john@acme.inc',
|
email: 'john@acme.inc',
|
||||||
});
|
});
|
||||||
expect(context.axiosMock.post).toHaveBeenCalledWith(
|
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||||
'/api/v1/conversations/45/transcript',
|
'/api/v1/conversations/45/transcript',
|
||||||
{
|
{
|
||||||
email: 'john@acme.inc',
|
email: 'john@acme.inc',
|
||||||
@ -167,7 +176,7 @@ describe('#ConversationAPI', () => {
|
|||||||
conversationId: 45,
|
conversationId: 45,
|
||||||
customAttributes: { order_d: '1001' },
|
customAttributes: { order_d: '1001' },
|
||||||
});
|
});
|
||||||
expect(context.axiosMock.post).toHaveBeenCalledWith(
|
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||||
'/api/v1/conversations/45/custom_attributes',
|
'/api/v1/conversations/45/custom_attributes',
|
||||||
{
|
{
|
||||||
custom_attributes: { order_d: '1001' },
|
custom_attributes: { order_d: '1001' },
|
||||||
@ -202,9 +211,7 @@ describe('#ConversationAPI', () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
conversationAPI.filter(payload);
|
conversationAPI.filter(payload);
|
||||||
expect(
|
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||||
context.axiosMock.post
|
|
||||||
).toHaveBeenCalledWith(
|
|
||||||
'/api/v1/conversations/filter',
|
'/api/v1/conversations/filter',
|
||||||
payload.queryData,
|
payload.queryData,
|
||||||
{ params: { page: payload.page } }
|
{ params: { page: payload.page } }
|
||||||
@ -213,7 +220,7 @@ describe('#ConversationAPI', () => {
|
|||||||
|
|
||||||
it('#getAllAttachments', () => {
|
it('#getAllAttachments', () => {
|
||||||
conversationAPI.getAllAttachments(1);
|
conversationAPI.getAllAttachments(1);
|
||||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
expect(axiosMock.get).toHaveBeenCalledWith(
|
||||||
'/api/v1/conversations/1/attachments'
|
'/api/v1/conversations/1/attachments'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import messageAPI, { buildCreatePayload } from '../../inbox/message';
|
import messageAPI, { buildCreatePayload } from '../../inbox/message';
|
||||||
import ApiClient from '../../ApiClient';
|
import ApiClient from '../../ApiClient';
|
||||||
import describeWithAPIMock from '../apiSpecHelper';
|
|
||||||
|
|
||||||
describe('#ConversationAPI', () => {
|
describe('#ConversationAPI', () => {
|
||||||
it('creates correct instance', () => {
|
it('creates correct instance', () => {
|
||||||
@ -13,13 +12,29 @@ describe('#ConversationAPI', () => {
|
|||||||
expect(messageAPI).toHaveProperty('getPreviousMessages');
|
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', () => {
|
it('#getPreviousMessages', () => {
|
||||||
messageAPI.getPreviousMessages({
|
messageAPI.getPreviousMessages({
|
||||||
conversationId: 12,
|
conversationId: 12,
|
||||||
before: 4573,
|
before: 4573,
|
||||||
});
|
});
|
||||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
expect(axiosMock.get).toHaveBeenCalledWith(
|
||||||
`/api/v1/conversations/12/messages`,
|
`/api/v1/conversations/12/messages`,
|
||||||
{
|
{
|
||||||
params: {
|
params: {
|
||||||
@ -35,7 +50,6 @@ describe('#ConversationAPI', () => {
|
|||||||
message: 'test content',
|
message: 'test content',
|
||||||
echoId: 12,
|
echoId: 12,
|
||||||
isPrivate: true,
|
isPrivate: true,
|
||||||
|
|
||||||
files: [new Blob(['test-content'], { type: 'application/pdf' })],
|
files: [new Blob(['test-content'], { type: 'application/pdf' })],
|
||||||
});
|
});
|
||||||
expect(formPayload).toBeInstanceOf(FormData);
|
expect(formPayload).toBeInstanceOf(FormData);
|
||||||
@ -58,8 +72,10 @@ describe('#ConversationAPI', () => {
|
|||||||
private: false,
|
private: false,
|
||||||
echo_id: 12,
|
echo_id: 12,
|
||||||
content_attributes: { in_reply_to: 12 },
|
content_attributes: { in_reply_to: 12 },
|
||||||
bcc_emails: '',
|
|
||||||
cc_emails: '',
|
cc_emails: '',
|
||||||
|
bcc_emails: '',
|
||||||
|
to_emails: '',
|
||||||
|
template_params: undefined,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import inboxesAPI from '../inboxes';
|
import inboxesAPI from '../inboxes';
|
||||||
import ApiClient from '../ApiClient';
|
import ApiClient from '../ApiClient';
|
||||||
import describeWithAPIMock from './apiSpecHelper';
|
|
||||||
|
|
||||||
describe('#InboxesAPI', () => {
|
describe('#InboxesAPI', () => {
|
||||||
it('creates correct instance', () => {
|
it('creates correct instance', () => {
|
||||||
@ -14,19 +13,32 @@ describe('#InboxesAPI', () => {
|
|||||||
expect(inboxesAPI).toHaveProperty('getAgentBot');
|
expect(inboxesAPI).toHaveProperty('getAgentBot');
|
||||||
expect(inboxesAPI).toHaveProperty('setAgentBot');
|
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', () => {
|
it('#getCampaigns', () => {
|
||||||
inboxesAPI.getCampaigns(2);
|
inboxesAPI.getCampaigns(2);
|
||||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
expect(axiosMock.get).toHaveBeenCalledWith('/api/v1/inboxes/2/campaigns');
|
||||||
'/api/v1/inboxes/2/campaigns'
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('#deleteInboxAvatar', () => {
|
it('#deleteInboxAvatar', () => {
|
||||||
inboxesAPI.deleteInboxAvatar(2);
|
inboxesAPI.deleteInboxAvatar(2);
|
||||||
expect(context.axiosMock.delete).toHaveBeenCalledWith(
|
expect(axiosMock.delete).toHaveBeenCalledWith('/api/v1/inboxes/2/avatar');
|
||||||
'/api/v1/inboxes/2/avatar'
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import integrationAPI from '../integrations';
|
import integrationAPI from '../integrations';
|
||||||
import ApiClient from '../ApiClient';
|
import ApiClient from '../ApiClient';
|
||||||
import describeWithAPIMock from './apiSpecHelper';
|
|
||||||
|
|
||||||
describe('#integrationAPI', () => {
|
describe('#integrationAPI', () => {
|
||||||
it('creates correct instance', () => {
|
it('creates correct instance', () => {
|
||||||
@ -16,11 +15,27 @@ describe('#integrationAPI', () => {
|
|||||||
expect(integrationAPI).toHaveProperty('listAllSlackChannels');
|
expect(integrationAPI).toHaveProperty('listAllSlackChannels');
|
||||||
expect(integrationAPI).toHaveProperty('deleteHook');
|
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', () => {
|
it('#connectSlack', () => {
|
||||||
const code = 'SDNFJNSDFNDSJN';
|
const code = 'SDNFJNSDFNDSJN';
|
||||||
integrationAPI.connectSlack(code);
|
integrationAPI.connectSlack(code);
|
||||||
expect(context.axiosMock.post).toHaveBeenCalledWith(
|
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||||
'/api/v1/integrations/slack',
|
'/api/v1/integrations/slack',
|
||||||
{
|
{
|
||||||
code,
|
code,
|
||||||
@ -31,7 +46,7 @@ describe('#integrationAPI', () => {
|
|||||||
it('#updateSlack', () => {
|
it('#updateSlack', () => {
|
||||||
const updateObj = { referenceId: 'SDFSDGSVE' };
|
const updateObj = { referenceId: 'SDFSDGSVE' };
|
||||||
integrationAPI.updateSlack(updateObj);
|
integrationAPI.updateSlack(updateObj);
|
||||||
expect(context.axiosMock.patch).toHaveBeenCalledWith(
|
expect(axiosMock.patch).toHaveBeenCalledWith(
|
||||||
'/api/v1/integrations/slack',
|
'/api/v1/integrations/slack',
|
||||||
{
|
{
|
||||||
reference_id: updateObj.referenceId,
|
reference_id: updateObj.referenceId,
|
||||||
@ -41,16 +56,14 @@ describe('#integrationAPI', () => {
|
|||||||
|
|
||||||
it('#listAllSlackChannels', () => {
|
it('#listAllSlackChannels', () => {
|
||||||
integrationAPI.listAllSlackChannels();
|
integrationAPI.listAllSlackChannels();
|
||||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
expect(axiosMock.get).toHaveBeenCalledWith(
|
||||||
'/api/v1/integrations/slack/list_all_channels'
|
'/api/v1/integrations/slack/list_all_channels'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('#delete', () => {
|
it('#delete', () => {
|
||||||
integrationAPI.delete(2);
|
integrationAPI.delete(2);
|
||||||
expect(context.axiosMock.delete).toHaveBeenCalledWith(
|
expect(axiosMock.delete).toHaveBeenCalledWith('/api/v1/integrations/2');
|
||||||
'/api/v1/integrations/2'
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('#createHook', () => {
|
it('#createHook', () => {
|
||||||
@ -59,7 +72,7 @@ describe('#integrationAPI', () => {
|
|||||||
settings: { api_key: 'SDFSDGSVE' },
|
settings: { api_key: 'SDFSDGSVE' },
|
||||||
};
|
};
|
||||||
integrationAPI.createHook(hookData);
|
integrationAPI.createHook(hookData);
|
||||||
expect(context.axiosMock.post).toHaveBeenCalledWith(
|
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||||
'/api/v1/integrations/hooks',
|
'/api/v1/integrations/hooks',
|
||||||
hookData
|
hookData
|
||||||
);
|
);
|
||||||
@ -67,7 +80,7 @@ describe('#integrationAPI', () => {
|
|||||||
|
|
||||||
it('#deleteHook', () => {
|
it('#deleteHook', () => {
|
||||||
integrationAPI.deleteHook(2);
|
integrationAPI.deleteHook(2);
|
||||||
expect(context.axiosMock.delete).toHaveBeenCalledWith(
|
expect(axiosMock.delete).toHaveBeenCalledWith(
|
||||||
'/api/v1/integrations/hooks/2'
|
'/api/v1/integrations/hooks/2'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import DyteAPIClient from '../../integrations/dyte';
|
import DyteAPIClient from '../../integrations/dyte';
|
||||||
import ApiClient from '../../ApiClient';
|
import ApiClient from '../../ApiClient';
|
||||||
import describeWithAPIMock from '../apiSpecHelper';
|
|
||||||
|
|
||||||
describe('#accountAPI', () => {
|
describe('#accountAPI', () => {
|
||||||
it('creates correct instance', () => {
|
it('creates correct instance', () => {
|
||||||
@ -9,10 +8,26 @@ describe('#accountAPI', () => {
|
|||||||
expect(DyteAPIClient).toHaveProperty('addParticipantToMeeting');
|
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', () => {
|
it('creates a valid request', () => {
|
||||||
DyteAPIClient.createAMeeting(1);
|
DyteAPIClient.createAMeeting(1);
|
||||||
expect(context.axiosMock.post).toHaveBeenCalledWith(
|
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||||
'/api/v1/integrations/dyte/create_a_meeting',
|
'/api/v1/integrations/dyte/create_a_meeting',
|
||||||
{
|
{
|
||||||
conversation_id: 1,
|
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', () => {
|
it('creates a valid request', () => {
|
||||||
DyteAPIClient.addParticipantToMeeting(1);
|
DyteAPIClient.addParticipantToMeeting(1);
|
||||||
expect(context.axiosMock.post).toHaveBeenCalledWith(
|
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||||
'/api/v1/integrations/dyte/add_participant_to_meeting',
|
'/api/v1/integrations/dyte/add_participant_to_meeting',
|
||||||
{
|
{
|
||||||
message_id: 1,
|
message_id: 1,
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import notificationsAPI from '../notifications';
|
import notificationsAPI from '../notifications';
|
||||||
import ApiClient from '../ApiClient';
|
import ApiClient from '../ApiClient';
|
||||||
import describeWithAPIMock from './apiSpecHelper';
|
|
||||||
|
|
||||||
describe('#NotificationAPI', () => {
|
describe('#NotificationAPI', () => {
|
||||||
it('creates correct instance', () => {
|
it('creates correct instance', () => {
|
||||||
@ -11,31 +10,47 @@ describe('#NotificationAPI', () => {
|
|||||||
expect(notificationsAPI).toHaveProperty('read');
|
expect(notificationsAPI).toHaveProperty('read');
|
||||||
expect(notificationsAPI).toHaveProperty('readAll');
|
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', () => {
|
it('#get', () => {
|
||||||
notificationsAPI.get(1);
|
notificationsAPI.get(1);
|
||||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
expect(axiosMock.get).toHaveBeenCalledWith(
|
||||||
'/api/v1/notifications?page=1'
|
'/api/v1/notifications?page=1'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('#getNotifications', () => {
|
it('#getNotifications', () => {
|
||||||
notificationsAPI.getNotifications(1);
|
notificationsAPI.getNotifications(1);
|
||||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
expect(axiosMock.get).toHaveBeenCalledWith(
|
||||||
'/api/v1/notifications/1/notifications'
|
'/api/v1/notifications/1/notifications'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('#getUnreadCount', () => {
|
it('#getUnreadCount', () => {
|
||||||
notificationsAPI.getUnreadCount();
|
notificationsAPI.getUnreadCount();
|
||||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
expect(axiosMock.get).toHaveBeenCalledWith(
|
||||||
'/api/v1/notifications/unread_count'
|
'/api/v1/notifications/unread_count'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('#read', () => {
|
it('#read', () => {
|
||||||
notificationsAPI.read(48670, 'Conversation');
|
notificationsAPI.read(48670, 'Conversation');
|
||||||
expect(context.axiosMock.post).toHaveBeenCalledWith(
|
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||||
'/api/v1/notifications/read_all',
|
'/api/v1/notifications/read_all',
|
||||||
{
|
{
|
||||||
primary_actor_id: 'Conversation',
|
primary_actor_id: 'Conversation',
|
||||||
@ -46,7 +61,7 @@ describe('#NotificationAPI', () => {
|
|||||||
|
|
||||||
it('#readAll', () => {
|
it('#readAll', () => {
|
||||||
notificationsAPI.readAll();
|
notificationsAPI.readAll();
|
||||||
expect(context.axiosMock.post).toHaveBeenCalledWith(
|
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||||
'/api/v1/notifications/read_all'
|
'/api/v1/notifications/read_all'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import reportsAPI from '../reports';
|
import reportsAPI from '../reports';
|
||||||
import ApiClient from '../ApiClient';
|
import ApiClient from '../ApiClient';
|
||||||
import describeWithAPIMock from './apiSpecHelper';
|
|
||||||
|
|
||||||
describe('#Reports API', () => {
|
describe('#Reports API', () => {
|
||||||
it('creates correct instance', () => {
|
it('creates correct instance', () => {
|
||||||
@ -18,14 +17,30 @@ describe('#Reports API', () => {
|
|||||||
expect(reportsAPI).toHaveProperty('getInboxReports');
|
expect(reportsAPI).toHaveProperty('getInboxReports');
|
||||||
expect(reportsAPI).toHaveProperty('getTeamReports');
|
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', () => {
|
it('#getAccountReports', () => {
|
||||||
reportsAPI.getReports({
|
reportsAPI.getReports({
|
||||||
metric: 'conversations_count',
|
metric: 'conversations_count',
|
||||||
from: 1621103400,
|
from: 1621103400,
|
||||||
to: 1621621800,
|
to: 1621621800,
|
||||||
});
|
});
|
||||||
expect(context.axiosMock.get).toHaveBeenCalledWith('/api/v2/reports', {
|
expect(axiosMock.get).toHaveBeenCalledWith('/api/v2/reports', {
|
||||||
params: {
|
params: {
|
||||||
metric: 'conversations_count',
|
metric: 'conversations_count',
|
||||||
since: 1621103400,
|
since: 1621103400,
|
||||||
@ -38,20 +53,17 @@ describe('#Reports API', () => {
|
|||||||
|
|
||||||
it('#getAccountSummary', () => {
|
it('#getAccountSummary', () => {
|
||||||
reportsAPI.getSummary(1621103400, 1621621800);
|
reportsAPI.getSummary(1621103400, 1621621800);
|
||||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
expect(axiosMock.get).toHaveBeenCalledWith('/api/v2/reports/summary', {
|
||||||
'/api/v2/reports/summary',
|
params: {
|
||||||
{
|
business_hours: undefined,
|
||||||
params: {
|
group_by: undefined,
|
||||||
business_hours: undefined,
|
id: undefined,
|
||||||
group_by: undefined,
|
since: 1621103400,
|
||||||
id: undefined,
|
timezone_offset: -0,
|
||||||
since: 1621103400,
|
type: 'account',
|
||||||
timezone_offset: -0,
|
until: 1621621800,
|
||||||
type: 'account',
|
},
|
||||||
until: 1621621800,
|
});
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('#getAgentReports', () => {
|
it('#getAgentReports', () => {
|
||||||
@ -60,60 +72,48 @@ describe('#Reports API', () => {
|
|||||||
to: 1621621800,
|
to: 1621621800,
|
||||||
businessHours: true,
|
businessHours: true,
|
||||||
});
|
});
|
||||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
expect(axiosMock.get).toHaveBeenCalledWith('/api/v2/reports/agents', {
|
||||||
'/api/v2/reports/agents',
|
params: {
|
||||||
{
|
since: 1621103400,
|
||||||
params: {
|
until: 1621621800,
|
||||||
since: 1621103400,
|
business_hours: true,
|
||||||
until: 1621621800,
|
},
|
||||||
business_hours: true,
|
});
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('#getLabelReports', () => {
|
it('#getLabelReports', () => {
|
||||||
reportsAPI.getLabelReports({ from: 1621103400, to: 1621621800 });
|
reportsAPI.getLabelReports({ from: 1621103400, to: 1621621800 });
|
||||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
expect(axiosMock.get).toHaveBeenCalledWith('/api/v2/reports/labels', {
|
||||||
'/api/v2/reports/labels',
|
params: {
|
||||||
{
|
since: 1621103400,
|
||||||
params: {
|
until: 1621621800,
|
||||||
since: 1621103400,
|
},
|
||||||
until: 1621621800,
|
});
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('#getInboxReports', () => {
|
it('#getInboxReports', () => {
|
||||||
reportsAPI.getInboxReports({ from: 1621103400, to: 1621621800 });
|
reportsAPI.getInboxReports({ from: 1621103400, to: 1621621800 });
|
||||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
expect(axiosMock.get).toHaveBeenCalledWith('/api/v2/reports/inboxes', {
|
||||||
'/api/v2/reports/inboxes',
|
params: {
|
||||||
{
|
since: 1621103400,
|
||||||
params: {
|
until: 1621621800,
|
||||||
since: 1621103400,
|
},
|
||||||
until: 1621621800,
|
});
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('#getTeamReports', () => {
|
it('#getTeamReports', () => {
|
||||||
reportsAPI.getTeamReports({ from: 1621103400, to: 1621621800 });
|
reportsAPI.getTeamReports({ from: 1621103400, to: 1621621800 });
|
||||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
expect(axiosMock.get).toHaveBeenCalledWith('/api/v2/reports/teams', {
|
||||||
'/api/v2/reports/teams',
|
params: {
|
||||||
{
|
since: 1621103400,
|
||||||
params: {
|
until: 1621621800,
|
||||||
since: 1621103400,
|
},
|
||||||
until: 1621621800,
|
});
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('#getConversationMetric', () => {
|
it('#getConversationMetric', () => {
|
||||||
reportsAPI.getConversationMetric('account');
|
reportsAPI.getConversationMetric('account');
|
||||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
expect(axiosMock.get).toHaveBeenCalledWith(
|
||||||
'/api/v2/reports/conversations',
|
'/api/v2/reports/conversations',
|
||||||
{
|
{
|
||||||
params: {
|
params: {
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import teamsAPI from '../teams';
|
import teamsAPI from '../teams';
|
||||||
import ApiClient from '../ApiClient';
|
import ApiClient from '../ApiClient';
|
||||||
import describeWithAPIMock from './apiSpecHelper';
|
|
||||||
|
|
||||||
describe('#TeamsAPI', () => {
|
describe('#TeamsAPI', () => {
|
||||||
it('creates correct instance', () => {
|
it('creates correct instance', () => {
|
||||||
@ -14,17 +13,33 @@ describe('#TeamsAPI', () => {
|
|||||||
expect(teamsAPI).toHaveProperty('addAgents');
|
expect(teamsAPI).toHaveProperty('addAgents');
|
||||||
expect(teamsAPI).toHaveProperty('updateAgents');
|
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', () => {
|
it('#getAgents', () => {
|
||||||
teamsAPI.getAgents({ teamId: 1 });
|
teamsAPI.getAgents({ teamId: 1 });
|
||||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
expect(axiosMock.get).toHaveBeenCalledWith(
|
||||||
'/api/v1/teams/1/team_members'
|
'/api/v1/teams/1/team_members'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('#addAgents', () => {
|
it('#addAgents', () => {
|
||||||
teamsAPI.addAgents({ teamId: 1, agentsList: { user_ids: [1, 10, 21] } });
|
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',
|
'/api/v1/teams/1/team_members',
|
||||||
{
|
{
|
||||||
user_ids: { user_ids: [1, 10, 21] },
|
user_ids: { user_ids: [1, 10, 21] },
|
||||||
@ -38,7 +53,7 @@ describe('#TeamsAPI', () => {
|
|||||||
teamId: 1,
|
teamId: 1,
|
||||||
agentsList,
|
agentsList,
|
||||||
});
|
});
|
||||||
expect(context.axiosMock.patch).toHaveBeenCalledWith(
|
expect(axiosMock.patch).toHaveBeenCalledWith(
|
||||||
'/api/v1/teams/1/team_members',
|
'/api/v1/teams/1/team_members',
|
||||||
{
|
{
|
||||||
user_ids: agentsList,
|
user_ids: agentsList,
|
||||||
|
|||||||
@ -149,15 +149,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.multiselect-wrap--small {
|
.multiselect-wrap--small {
|
||||||
.multiselect__tags,
|
|
||||||
.multiselect__input {
|
|
||||||
@apply items-center flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.multiselect__tags,
|
.multiselect__tags,
|
||||||
.multiselect__input,
|
.multiselect__input,
|
||||||
.multiselect {
|
.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 {
|
.multiselect__input {
|
||||||
@ -165,11 +161,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.multiselect__single {
|
.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 {
|
.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 {
|
.multiselect__select {
|
||||||
@ -181,9 +181,6 @@
|
|||||||
@apply bg-transparent;
|
@apply bg-transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.multiselect__tags-wrap {
|
|
||||||
@apply flex-shrink-0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.multiselect-wrap--medium {
|
.multiselect-wrap--medium {
|
||||||
|
|||||||
@ -34,26 +34,26 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.hollow {
|
&.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 {
|
&.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;
|
@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 {
|
&.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 {
|
&.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 {
|
&.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 {
|
&: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 {
|
&.secondary {
|
||||||
@apply border-slate-100 dark:border-slate-700 text-slate-800 dark:text-slate-100;
|
@apply border-slate-100 dark:border-slate-700 text-slate-800 dark:text-slate-100;
|
||||||
@ -68,7 +68,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.warning {
|
&.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 {
|
&.is-image {
|
||||||
@apply rounded-lg;
|
@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;
|
@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 {
|
.page-top-bar {
|
||||||
@apply px-8 pt-9 pb-0;
|
@apply px-8 pt-9 pb-0;
|
||||||
|
|
||||||
|
|||||||
@ -10,7 +10,8 @@
|
|||||||
<div
|
<div
|
||||||
class="flex items-center justify-between py-0 px-4"
|
class="flex items-center justify-between py-0 px-4"
|
||||||
:class="{
|
: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">
|
<div class="flex max-w-[85%] justify-center items-center">
|
||||||
@ -24,9 +25,7 @@
|
|||||||
v-if="!hasAppliedFiltersOrActiveFolders"
|
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"
|
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"
|
||||||
>
|
>
|
||||||
{{
|
{{ $t(`CHAT_LIST.CHAT_STATUS_FILTER_ITEMS.${activeStatus}.TEXT`) }}
|
||||||
this.$t(`CHAT_LIST.CHAT_STATUS_FILTER_ITEMS.${activeStatus}.TEXT`)
|
|
||||||
}}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
@ -642,10 +641,8 @@ export default {
|
|||||||
},
|
},
|
||||||
handleKeyEvents(e) {
|
handleKeyEvents(e) {
|
||||||
if (hasPressedAltAndJKey(e)) {
|
if (hasPressedAltAndJKey(e)) {
|
||||||
const {
|
const { allConversations, activeConversationIndex } =
|
||||||
allConversations,
|
this.getKeyboardListenerParams();
|
||||||
activeConversationIndex,
|
|
||||||
} = this.getKeyboardListenerParams();
|
|
||||||
if (activeConversationIndex === -1) {
|
if (activeConversationIndex === -1) {
|
||||||
allConversations[0].click();
|
allConversations[0].click();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,10 +13,10 @@
|
|||||||
/>
|
/>
|
||||||
<div class="flex flex-row justify-end gap-2 py-2 px-0 w-full">
|
<div class="flex flex-row justify-end gap-2 py-2 px-0 w-full">
|
||||||
<woot-button variant="clear" @click.prevent="onClose">
|
<woot-button variant="clear" @click.prevent="onClose">
|
||||||
{{ this.$t('CONVERSATION.CUSTOM_SNOOZE.CANCEL') }}
|
{{ $t('CONVERSATION.CUSTOM_SNOOZE.CANCEL') }}
|
||||||
</woot-button>
|
</woot-button>
|
||||||
<woot-button>
|
<woot-button>
|
||||||
{{ this.$t('CONVERSATION.CUSTOM_SNOOZE.APPLY') }}
|
{{ $t('CONVERSATION.CUSTOM_SNOOZE.APPLY') }}
|
||||||
</woot-button>
|
</woot-button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="text--container">
|
<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') }}
|
{{ $t('COMPONENTS.CODE.BUTTON_TEXT') }}
|
||||||
</woot-button>
|
</woot-button>
|
||||||
<woot-button
|
<woot-button
|
||||||
|
|||||||
@ -4,15 +4,19 @@
|
|||||||
v-if="show"
|
v-if="show"
|
||||||
:class="modalClassName"
|
:class="modalClassName"
|
||||||
transition="modal"
|
transition="modal"
|
||||||
@click="onBackDropClick"
|
@mousedown="handleMouseDown"
|
||||||
>
|
>
|
||||||
<div :class="modalContainerClassName" @click.stop>
|
<div
|
||||||
|
:class="modalContainerClassName"
|
||||||
|
@mouse.stop
|
||||||
|
@mousedown="event => event.stopPropagation()"
|
||||||
|
>
|
||||||
<woot-button
|
<woot-button
|
||||||
v-if="showCloseButton"
|
v-if="showCloseButton"
|
||||||
color-scheme="secondary"
|
color-scheme="secondary"
|
||||||
icon="dismiss"
|
icon="dismiss"
|
||||||
variant="clear"
|
variant="clear"
|
||||||
class="modal--close"
|
class="absolute ltr:right-2 rtl:left-2 top-2 z-10"
|
||||||
@click="close"
|
@click="close"
|
||||||
/>
|
/>
|
||||||
<slot />
|
<slot />
|
||||||
@ -50,6 +54,11 @@ export default {
|
|||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
mousedDownOnBackdrop: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
modalContainerClassName() {
|
modalContainerClassName() {
|
||||||
let className =
|
let className =
|
||||||
@ -66,9 +75,9 @@ export default {
|
|||||||
'right-aligned': 'right-aligned',
|
'right-aligned': 'right-aligned',
|
||||||
};
|
};
|
||||||
|
|
||||||
return `modal-mask skip-context-menu ${modalClassNameMap[
|
return `modal-mask skip-context-menu ${
|
||||||
this.modalType
|
modalClassNameMap[this.modalType] || ''
|
||||||
] || ''}`;
|
}`;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
@ -77,13 +86,22 @@ export default {
|
|||||||
this.onClose();
|
this.onClose();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
document.body.addEventListener('mouseup', this.onMouseUp);
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
document.body.removeEventListener('mouseup', this.onMouseUp);
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
handleMouseDown() {
|
||||||
|
this.mousedDownOnBackdrop = true;
|
||||||
|
},
|
||||||
close() {
|
close() {
|
||||||
this.onClose();
|
this.onClose();
|
||||||
},
|
},
|
||||||
onBackDropClick() {
|
onMouseUp() {
|
||||||
if (this.closeOnBackdropClick) {
|
if (this.mousedDownOnBackdrop) {
|
||||||
|
this.mousedDownOnBackdrop = false;
|
||||||
this.onClose();
|
this.onClose();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@ -2,7 +2,8 @@
|
|||||||
<div
|
<div
|
||||||
class="ml-0 mr-0 flex pt-0 pr-4 pb-4 pl-0"
|
class="ml-0 mr-0 flex pt-0 pr-4 pb-4 pl-0"
|
||||||
:class="{
|
: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">
|
<div class="w-[30%] min-w-0 max-w-[30%] pr-12">
|
||||||
|
|||||||
@ -72,10 +72,8 @@ export default {
|
|||||||
const { custom_attributes: subscription } = account;
|
const { custom_attributes: subscription } = account;
|
||||||
if (!subscription) return EMPTY_SUBSCRIPTION_INFO;
|
if (!subscription) return EMPTY_SUBSCRIPTION_INFO;
|
||||||
|
|
||||||
const {
|
const { subscription_status: status, subscription_ends_on: endsOn } =
|
||||||
subscription_status: status,
|
subscription;
|
||||||
subscription_ends_on: endsOn,
|
|
||||||
} = subscription;
|
|
||||||
|
|
||||||
return { status, endsOn: new Date(endsOn) };
|
return { status, endsOn: new Date(endsOn) };
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
const semver = require('semver');
|
import semver from 'semver';
|
||||||
|
|
||||||
export const hasAnUpdateAvailable = (latestVersion, currentVersion) => {
|
export const hasAnUpdateAvailable = (latestVersion, currentVersion) => {
|
||||||
if (!semver.valid(latestVersion)) {
|
if (!semver.valid(latestVersion)) {
|
||||||
|
|||||||
@ -10,7 +10,7 @@
|
|||||||
:is-loading="isLoading"
|
:is-loading="isLoading"
|
||||||
@click="onCmdResolveConversation"
|
@click="onCmdResolveConversation"
|
||||||
>
|
>
|
||||||
{{ this.$t('CONVERSATION.HEADER.RESOLVE_ACTION') }}
|
{{ $t('CONVERSATION.HEADER.RESOLVE_ACTION') }}
|
||||||
</woot-button>
|
</woot-button>
|
||||||
<woot-button
|
<woot-button
|
||||||
v-else-if="isResolved"
|
v-else-if="isResolved"
|
||||||
@ -21,7 +21,7 @@
|
|||||||
:is-loading="isLoading"
|
:is-loading="isLoading"
|
||||||
@click="onCmdOpenConversation"
|
@click="onCmdOpenConversation"
|
||||||
>
|
>
|
||||||
{{ this.$t('CONVERSATION.HEADER.REOPEN_ACTION') }}
|
{{ $t('CONVERSATION.HEADER.REOPEN_ACTION') }}
|
||||||
</woot-button>
|
</woot-button>
|
||||||
<woot-button
|
<woot-button
|
||||||
v-else-if="showOpenButton"
|
v-else-if="showOpenButton"
|
||||||
@ -31,7 +31,7 @@
|
|||||||
:is-loading="isLoading"
|
:is-loading="isLoading"
|
||||||
@click="onCmdOpenConversation"
|
@click="onCmdOpenConversation"
|
||||||
>
|
>
|
||||||
{{ this.$t('CONVERSATION.HEADER.OPEN_ACTION') }}
|
{{ $t('CONVERSATION.HEADER.OPEN_ACTION') }}
|
||||||
</woot-button>
|
</woot-button>
|
||||||
<woot-button
|
<woot-button
|
||||||
v-if="showAdditionalActions"
|
v-if="showAdditionalActions"
|
||||||
@ -57,7 +57,7 @@
|
|||||||
icon="snooze"
|
icon="snooze"
|
||||||
@click="() => openSnoozeModal()"
|
@click="() => openSnoozeModal()"
|
||||||
>
|
>
|
||||||
{{ this.$t('CONVERSATION.RESOLVE_DROPDOWN.SNOOZE_UNTIL') }}
|
{{ $t('CONVERSATION.RESOLVE_DROPDOWN.SNOOZE_UNTIL') }}
|
||||||
</woot-button>
|
</woot-button>
|
||||||
</woot-dropdown-item>
|
</woot-dropdown-item>
|
||||||
<woot-dropdown-item v-if="!isPending">
|
<woot-dropdown-item v-if="!isPending">
|
||||||
@ -68,7 +68,7 @@
|
|||||||
icon="book-clock"
|
icon="book-clock"
|
||||||
@click="() => toggleStatus(STATUS_TYPE.PENDING)"
|
@click="() => toggleStatus(STATUS_TYPE.PENDING)"
|
||||||
>
|
>
|
||||||
{{ this.$t('CONVERSATION.RESOLVE_DROPDOWN.MARK_PENDING') }}
|
{{ $t('CONVERSATION.RESOLVE_DROPDOWN.MARK_PENDING') }}
|
||||||
</woot-button>
|
</woot-button>
|
||||||
</woot-dropdown-item>
|
</woot-dropdown-item>
|
||||||
</woot-dropdown-menu>
|
</woot-dropdown-menu>
|
||||||
|
|||||||
@ -11,7 +11,6 @@
|
|||||||
@key-shortcut-modal="toggleKeyShortcutModal"
|
@key-shortcut-modal="toggleKeyShortcutModal"
|
||||||
@open-notification-panel="openNotificationPanel"
|
@open-notification-panel="openNotificationPanel"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<secondary-sidebar
|
<secondary-sidebar
|
||||||
v-if="showSecondarySidebar"
|
v-if="showSecondarySidebar"
|
||||||
:class="sidebarClassName"
|
:class="sidebarClassName"
|
||||||
@ -114,7 +113,12 @@ export default {
|
|||||||
if (!isAvailableForTheUser) {
|
if (!isAvailableForTheUser) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
menuItem.alwaysVisibleOnChatwootInstances &&
|
||||||
|
!this.isACustomBrandedInstance
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (menuItem.featureFlag) {
|
if (menuItem.featureFlag) {
|
||||||
return this.isFeatureEnabledonAccount(
|
return this.isFeatureEnabledonAccount(
|
||||||
this.accountId,
|
this.accountId,
|
||||||
|
|||||||
@ -42,6 +42,7 @@ const primaryMenuItems = accountId => [
|
|||||||
key: 'helpcenter',
|
key: 'helpcenter',
|
||||||
label: 'HELP_CENTER.TITLE',
|
label: 'HELP_CENTER.TITLE',
|
||||||
featureFlag: FEATURE_FLAGS.HELP_CENTER,
|
featureFlag: FEATURE_FLAGS.HELP_CENTER,
|
||||||
|
alwaysVisibleOnChatwootInstances: true,
|
||||||
toState: frontendURL(`accounts/${accountId}/portals`),
|
toState: frontendURL(`accounts/${accountId}/portals`),
|
||||||
toStateName: 'default_portal_articles',
|
toStateName: 'default_portal_articles',
|
||||||
roles: ['administrator'],
|
roles: ['administrator'],
|
||||||
|
|||||||
@ -34,8 +34,8 @@
|
|||||||
<woot-submit-button
|
<woot-submit-button
|
||||||
:disabled="
|
:disabled="
|
||||||
$v.accountName.$invalid ||
|
$v.accountName.$invalid ||
|
||||||
$v.accountName.$invalid ||
|
$v.accountName.$invalid ||
|
||||||
uiFlags.isCreating
|
uiFlags.isCreating
|
||||||
"
|
"
|
||||||
:button-text="$t('CREATE_ACCOUNT.FORM.SUBMIT')"
|
:button-text="$t('CREATE_ACCOUNT.FORM.SUBMIT')"
|
||||||
:loading="uiFlags.isCreating"
|
:loading="uiFlags.isCreating"
|
||||||
|
|||||||
@ -3,7 +3,8 @@
|
|||||||
<button
|
<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="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="{
|
: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"
|
@click="openNotificationPanel"
|
||||||
>
|
>
|
||||||
|
|||||||
@ -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="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="{
|
:class="{
|
||||||
'bg-woot-25 dark:bg-slate-800': isActive,
|
'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"
|
@click="navigate"
|
||||||
>
|
>
|
||||||
@ -44,7 +45,8 @@
|
|||||||
class="text-sm text-slate-700 dark:text-slate-100"
|
class="text-sm text-slate-700 dark:text-slate-100"
|
||||||
:class="{
|
:class="{
|
||||||
'text-woot-500 dark:text-woot-500': isActive,
|
'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 }}
|
{{ label }}
|
||||||
|
|||||||
@ -35,7 +35,8 @@
|
|||||||
:class="{
|
:class="{
|
||||||
'text-slate-300 dark:text-slate-600': isCountZero && !isActiveView,
|
'text-slate-300 dark:text-slate-600': isCountZero && !isActiveView,
|
||||||
'text-slate-600 dark:text-slate-50': !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,
|
'bg-slate-50 dark:bg-slate-700': !isActiveView,
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
|
|||||||
@ -53,16 +53,24 @@ export default {
|
|||||||
const createdTimeDiff = Date.now() - this.createdAtTimestamp * 1000;
|
const createdTimeDiff = Date.now() - this.createdAtTimestamp * 1000;
|
||||||
const isBeforeAMonth = createdTimeDiff > DAY_IN_MILLI_SECONDS * 30;
|
const isBeforeAMonth = createdTimeDiff > DAY_IN_MILLI_SECONDS * 30;
|
||||||
return !isBeforeAMonth
|
return !isBeforeAMonth
|
||||||
? `Created ${this.createdAtTimeAgo}`
|
? `${this.$t('CHAT_LIST.CHAT_TIME_STAMP.CREATED.LATEST')} ${
|
||||||
: `Created at: ${this.dateFormat(this.createdAtTimestamp)}`;
|
this.createdAtTimeAgo
|
||||||
|
}`
|
||||||
|
: `${this.$t(
|
||||||
|
'CHAT_LIST.CHAT_TIME_STAMP.CREATED.OLDEST'
|
||||||
|
)} ${this.dateFormat(this.createdAtTimestamp)}`;
|
||||||
},
|
},
|
||||||
lastActivity() {
|
lastActivity() {
|
||||||
const lastActivityTimeDiff =
|
const lastActivityTimeDiff =
|
||||||
Date.now() - this.lastActivityTimestamp * 1000;
|
Date.now() - this.lastActivityTimestamp * 1000;
|
||||||
const isNotActive = lastActivityTimeDiff > DAY_IN_MILLI_SECONDS * 30;
|
const isNotActive = lastActivityTimeDiff > DAY_IN_MILLI_SECONDS * 30;
|
||||||
return !isNotActive
|
return !isNotActive
|
||||||
? `Last activity ${this.lastActivityAtTimeAgo}`
|
? `${this.$t('CHAT_LIST.CHAT_TIME_STAMP.LAST_ACTIVITY.ACTIVE')} ${
|
||||||
: `Last activity: ${this.dateFormat(this.lastActivityTimestamp)}`;
|
this.lastActivityAtTimeAgo
|
||||||
|
}`
|
||||||
|
: `${this.$t(
|
||||||
|
'CHAT_LIST.CHAT_TIME_STAMP.LAST_ACTIVITY.NOT_ACTIVE'
|
||||||
|
)} ${this.dateFormat(this.lastActivityTimestamp)}`;
|
||||||
},
|
},
|
||||||
tooltipText() {
|
tooltipText() {
|
||||||
return `${this.createdAt}
|
return `${this.createdAt}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="animation-container margin-top-1">
|
<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" />
|
<fluent-icon icon="wand" size="14" class="ai-typing--icon" />
|
||||||
<label>
|
<label>
|
||||||
{{ $t('INTEGRATION_SETTINGS.OPEN_AI.ASSISTANCE_MODAL.AI_WRITING') }}
|
{{ $t('INTEGRATION_SETTINGS.OPEN_AI.ASSISTANCE_MODAL.AI_WRITING') }}
|
||||||
|
|||||||
@ -1,11 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div class="preview-item__wrap flex overflow-auto max-h-[12.5rem]">
|
||||||
class="preview-item__wrap flex flex-col overflow-auto mt-4 max-h-[12.5rem]"
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
v-for="(attachment, index) in attachments"
|
v-for="(attachment, index) in attachments"
|
||||||
:key="attachment.id"
|
: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">
|
<div class="max-w-[4rem] flex-shrink-0 w-6 flex items-center">
|
||||||
<img
|
<img
|
||||||
@ -17,7 +15,7 @@
|
|||||||
📄
|
📄
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</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
|
<span
|
||||||
class="h-4 overflow-hidden text-sm font-medium text-ellipsis whitespace-nowrap"
|
class="h-4 overflow-hidden text-sm font-medium text-ellipsis whitespace-nowrap"
|
||||||
>
|
>
|
||||||
|
|||||||
@ -12,7 +12,7 @@
|
|||||||
/>
|
/>
|
||||||
<iframe
|
<iframe
|
||||||
v-if="configItem.type === 'frame' && configItem.url"
|
v-if="configItem.type === 'frame' && configItem.url"
|
||||||
:id="`dashboard-app--frame-${index}`"
|
:id="getFrameId(index)"
|
||||||
:src="configItem.url"
|
:src="configItem.url"
|
||||||
@load="() => onIframeLoad(index)"
|
@load="() => onIframeLoad(index)"
|
||||||
/>
|
/>
|
||||||
@ -39,6 +39,10 @@ export default {
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
position: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@ -82,10 +86,14 @@ export default {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
getFrameId(index) {
|
||||||
|
return `dashboard-app--frame-${this.position}-${index}`;
|
||||||
|
},
|
||||||
onIframeLoad(index) {
|
onIframeLoad(index) {
|
||||||
const frameElement = document.getElementById(
|
// A possible alternative is to use ref instead of document.getElementById
|
||||||
`dashboard-app--frame-${index}`
|
// 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 };
|
const eventData = { event: 'appContext', data: this.dashboardAppContext };
|
||||||
frameElement.contentWindow.postMessage(JSON.stringify(eventData), '*');
|
frameElement.contentWindow.postMessage(JSON.stringify(eventData), '*');
|
||||||
this.iframeLoading = false;
|
this.iframeLoading = false;
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
<!-- eslint-disable vue/no-mutating-props -->
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
@ -255,7 +256,9 @@ export default {
|
|||||||
value === 'conversation_attribute' ||
|
value === 'conversation_attribute' ||
|
||||||
value === 'contact_attribute'
|
value === 'contact_attribute'
|
||||||
) {
|
) {
|
||||||
|
// eslint-disable-next-line vue/no-mutating-props
|
||||||
this.value.custom_attribute_type = this.customAttributeType;
|
this.value.custom_attribute_type = this.customAttributeType;
|
||||||
|
// eslint-disable-next-line vue/no-mutating-props
|
||||||
} else this.value.custom_attribute_type = '';
|
} else this.value.custom_attribute_type = '';
|
||||||
},
|
},
|
||||||
immediate: true,
|
immediate: true,
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="editor-root">
|
<div ref="editorRoot" class="relative editor-root">
|
||||||
<tag-agents
|
<tag-agents
|
||||||
v-if="showUserMentions && isPrivate"
|
v-if="showUserMentions && isPrivate"
|
||||||
:search-key="mentionSearchKey"
|
:search-key="mentionSearchKey"
|
||||||
@ -23,6 +23,24 @@
|
|||||||
@change="onFileChange"
|
@change="onFileChange"
|
||||||
/>
|
/>
|
||||||
<div ref="editor" />
|
<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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -40,6 +58,7 @@ import {
|
|||||||
suggestionsPlugin,
|
suggestionsPlugin,
|
||||||
triggerCharacters,
|
triggerCharacters,
|
||||||
} from '@chatwoot/prosemirror-schema/src/mentions/plugin';
|
} from '@chatwoot/prosemirror-schema/src/mentions/plugin';
|
||||||
|
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||||
|
|
||||||
import TagAgents from '../conversation/TagAgents.vue';
|
import TagAgents from '../conversation/TagAgents.vue';
|
||||||
import CannedResponse from '../conversation/CannedResponse.vue';
|
import CannedResponse from '../conversation/CannedResponse.vue';
|
||||||
@ -47,6 +66,10 @@ import VariableList from '../conversation/VariableList.vue';
|
|||||||
import {
|
import {
|
||||||
appendSignature,
|
appendSignature,
|
||||||
removeSignature,
|
removeSignature,
|
||||||
|
insertAtCursor,
|
||||||
|
scrollCursorIntoView,
|
||||||
|
findNodeToInsertImage,
|
||||||
|
setURLWithQueryAndSize,
|
||||||
} from 'dashboard/helper/editorHelper';
|
} from 'dashboard/helper/editorHelper';
|
||||||
|
|
||||||
const TYPING_INDICATOR_IDLE_TIME = 4000;
|
const TYPING_INDICATOR_IDLE_TIME = 4000;
|
||||||
@ -66,13 +89,17 @@ import { CONVERSATION_EVENTS } from '../../../helper/AnalyticsHelper/events';
|
|||||||
import { checkFileSizeLimit } from 'shared/helpers/FileHelper';
|
import { checkFileSizeLimit } from 'shared/helpers/FileHelper';
|
||||||
import { uploadFile } from 'dashboard/helper/uploadHelper';
|
import { uploadFile } from 'dashboard/helper/uploadHelper';
|
||||||
import alertMixin from 'shared/mixins/alertMixin';
|
import alertMixin from 'shared/mixins/alertMixin';
|
||||||
import { findNodeToInsertImage } from 'dashboard/helper/messageEditorHelper';
|
import {
|
||||||
import { MESSAGE_EDITOR_MENU_OPTIONS } from 'dashboard/constants/editor';
|
MESSAGE_EDITOR_MENU_OPTIONS,
|
||||||
|
MESSAGE_EDITOR_IMAGE_RESIZES,
|
||||||
|
} from 'dashboard/constants/editor';
|
||||||
|
|
||||||
const createState = (
|
const createState = (
|
||||||
content,
|
content,
|
||||||
placeholder,
|
placeholder,
|
||||||
|
// eslint-disable-next-line default-param-last
|
||||||
plugins = [],
|
plugins = [],
|
||||||
|
// eslint-disable-next-line default-param-last
|
||||||
methods = {},
|
methods = {},
|
||||||
enabledMenuOptions
|
enabledMenuOptions
|
||||||
) => {
|
) => {
|
||||||
@ -108,6 +135,7 @@ export default {
|
|||||||
// allowSignature is a kill switch, ensuring no signature methods
|
// allowSignature is a kill switch, ensuring no signature methods
|
||||||
// are triggered except when this flag is true
|
// are triggered except when this flag is true
|
||||||
allowSignature: { type: Boolean, default: false },
|
allowSignature: { type: Boolean, default: false },
|
||||||
|
showImageResizeToolbar: { type: Boolean, default: false }, // A kill switch to show or hide the image toolbar
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@ -120,9 +148,16 @@ export default {
|
|||||||
editorView: null,
|
editorView: null,
|
||||||
range: null,
|
range: null,
|
||||||
state: undefined,
|
state: undefined,
|
||||||
|
isImageNodeSelected: false,
|
||||||
|
toolbarPosition: { top: 0, left: 0 },
|
||||||
|
sizes: MESSAGE_EDITOR_IMAGE_RESIZES,
|
||||||
|
selectedImageNode: null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
editorRoot() {
|
||||||
|
return this.$refs.editorRoot;
|
||||||
|
},
|
||||||
contentFromEditor() {
|
contentFromEditor() {
|
||||||
return MessageMarkdownSerializer.serialize(this.editorView.state.doc);
|
return MessageMarkdownSerializer.serialize(this.editorView.state.doc);
|
||||||
},
|
},
|
||||||
@ -270,6 +305,7 @@ export default {
|
|||||||
const tr = this.editorView.state.tr.replaceSelectionWith(node);
|
const tr = this.editorView.state.tr.replaceSelectionWith(node);
|
||||||
this.editorView.focus();
|
this.editorView.focus();
|
||||||
this.state = this.editorView.state.apply(tr);
|
this.state = this.editorView.state.apply(tr);
|
||||||
|
this.editorView.updateState(this.state);
|
||||||
this.emitOnChange();
|
this.emitOnChange();
|
||||||
this.$emit('clear-selection');
|
this.$emit('clear-selection');
|
||||||
}
|
}
|
||||||
@ -295,7 +331,17 @@ export default {
|
|||||||
mounted() {
|
mounted() {
|
||||||
this.createEditorView();
|
this.createEditorView();
|
||||||
this.editorView.updateState(this.state);
|
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: {
|
methods: {
|
||||||
reloadState(content = this.value) {
|
reloadState(content = this.value) {
|
||||||
@ -382,6 +428,7 @@ export default {
|
|||||||
state: this.state,
|
state: this.state,
|
||||||
dispatchTransaction: tx => {
|
dispatchTransaction: tx => {
|
||||||
this.state = this.state.apply(tx);
|
this.state = this.state.apply(tx);
|
||||||
|
this.editorView.updateState(this.state);
|
||||||
this.emitOnChange();
|
this.emitOnChange();
|
||||||
},
|
},
|
||||||
handleDOMEvents: {
|
handleDOMEvents: {
|
||||||
@ -394,6 +441,9 @@ export default {
|
|||||||
focus: () => {
|
focus: () => {
|
||||||
this.onFocus();
|
this.onFocus();
|
||||||
},
|
},
|
||||||
|
click: () => {
|
||||||
|
// this.isEditorMouseFocusedOnAnImage(); Enable it when the backend supports for message resize is done.
|
||||||
|
},
|
||||||
blur: () => {
|
blur: () => {
|
||||||
this.onBlur();
|
this.onBlur();
|
||||||
},
|
},
|
||||||
@ -406,6 +456,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() {
|
isEnterToSendEnabled() {
|
||||||
return isEditorHotKeyEnabled(this.uiSettings, 'enter');
|
return isEditorHotKeyEnabled(this.uiSettings, 'enter');
|
||||||
},
|
},
|
||||||
@ -438,11 +532,7 @@ export default {
|
|||||||
userFullName: mentionItem.name,
|
userFullName: mentionItem.name,
|
||||||
});
|
});
|
||||||
|
|
||||||
const tr = this.editorView.state.tr
|
this.insertNodeIntoEditor(node, this.range.from, this.range.to);
|
||||||
.replaceWith(this.range.from, this.range.to, node)
|
|
||||||
.insertText(` `);
|
|
||||||
this.state = this.editorView.state.apply(tr);
|
|
||||||
this.emitOnChange();
|
|
||||||
this.$track(CONVERSATION_EVENTS.USED_MENTIONS);
|
this.$track(CONVERSATION_EVENTS.USED_MENTIONS);
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
@ -452,30 +542,22 @@ export default {
|
|||||||
message: cannedItem,
|
message: cannedItem,
|
||||||
variables: this.variables,
|
variables: this.variables,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!this.editorView) {
|
if (!this.editorView) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
let from = this.range.from - 1;
|
|
||||||
let node = new MessageMarkdownTransformer(messageSchema).parse(
|
let node = new MessageMarkdownTransformer(messageSchema).parse(
|
||||||
updatedMessage
|
updatedMessage
|
||||||
);
|
);
|
||||||
|
|
||||||
if (node.textContent === updatedMessage) {
|
const from =
|
||||||
node = this.editorView.state.schema.text(updatedMessage);
|
node.textContent === updatedMessage
|
||||||
from = this.range.from;
|
? this.range.from
|
||||||
}
|
: this.range.from - 1;
|
||||||
|
|
||||||
const tr = this.editorView.state.tr.replaceWith(
|
this.insertNodeIntoEditor(node, from, this.range.to);
|
||||||
from,
|
|
||||||
this.range.to,
|
|
||||||
node
|
|
||||||
);
|
|
||||||
|
|
||||||
this.state = this.editorView.state.apply(tr);
|
|
||||||
this.emitOnChange();
|
|
||||||
|
|
||||||
tr.scrollIntoView();
|
|
||||||
this.$track(CONVERSATION_EVENTS.INSERTED_A_CANNED_RESPONSE);
|
this.$track(CONVERSATION_EVENTS.INSERTED_A_CANNED_RESPONSE);
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
@ -483,23 +565,14 @@ export default {
|
|||||||
if (!this.editorView) {
|
if (!this.editorView) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
let node = this.editorView.state.schema.text(`{{${variable}}}`);
|
|
||||||
const from = this.range.from;
|
|
||||||
|
|
||||||
const tr = this.editorView.state.tr.replaceWith(
|
const content = `{{${variable}}}`;
|
||||||
from,
|
let node = this.editorView.state.schema.text(content);
|
||||||
this.range.to,
|
const { from, to } = this.range;
|
||||||
node
|
|
||||||
);
|
|
||||||
|
|
||||||
this.state = this.editorView.state.apply(tr);
|
this.insertNodeIntoEditor(node, from, to);
|
||||||
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.showVariables = false;
|
this.showVariables = false;
|
||||||
this.$track(CONVERSATION_EVENTS.INSERTED_A_VARIABLE);
|
this.$track(CONVERSATION_EVENTS.INSERTED_A_VARIABLE);
|
||||||
tr.scrollIntoView();
|
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
openFileBrowser() {
|
openFileBrowser() {
|
||||||
@ -555,8 +628,6 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
emitOnChange() {
|
emitOnChange() {
|
||||||
this.editorView.updateState(this.state);
|
|
||||||
|
|
||||||
this.$emit('input', this.contentFromEditor);
|
this.$emit('input', this.contentFromEditor);
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -599,6 +670,7 @@ export default {
|
|||||||
() => this.resetTyping(),
|
() => this.resetTyping(),
|
||||||
TYPING_INDICATOR_IDLE_TIME
|
TYPING_INDICATOR_IDLE_TIME
|
||||||
);
|
);
|
||||||
|
// this.updateImgToolbarOnDelete(); Enable it when the backend supports for message resize is done.
|
||||||
},
|
},
|
||||||
onKeydown(event) {
|
onKeydown(event) {
|
||||||
if (this.isEnterToSendEnabled()) {
|
if (this.isEnterToSendEnabled()) {
|
||||||
@ -616,6 +688,19 @@ export default {
|
|||||||
onFocus() {
|
onFocus() {
|
||||||
this.$emit('focus');
|
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>
|
</script>
|
||||||
@ -709,6 +794,6 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.editor-warning__message {
|
.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>
|
</style>
|
||||||
|
|||||||
@ -32,7 +32,9 @@ const MAXIMUM_FILE_UPLOAD_SIZE = 4; // in MB
|
|||||||
const createState = (
|
const createState = (
|
||||||
content,
|
content,
|
||||||
placeholder,
|
placeholder,
|
||||||
|
// eslint-disable-next-line default-param-last
|
||||||
plugins = [],
|
plugins = [],
|
||||||
|
// eslint-disable-next-line default-param-last
|
||||||
methods = {},
|
methods = {},
|
||||||
enabledMenuOptions
|
enabledMenuOptions
|
||||||
) => {
|
) => {
|
||||||
|
|||||||
@ -14,10 +14,11 @@
|
|||||||
<file-upload
|
<file-upload
|
||||||
ref="upload"
|
ref="upload"
|
||||||
v-tooltip.top-end="$t('CONVERSATION.REPLYBOX.TIP_ATTACH_ICON')"
|
v-tooltip.top-end="$t('CONVERSATION.REPLYBOX.TIP_ATTACH_ICON')"
|
||||||
|
input-id="conversationAttachment"
|
||||||
:size="4096 * 4096"
|
:size="4096 * 4096"
|
||||||
:accept="allowedFileTypes"
|
:accept="allowedFileTypes"
|
||||||
:multiple="enableMultipleFileUpload"
|
:multiple="enableMultipleFileUpload"
|
||||||
:drop="true"
|
:drop="enableDragAndDrop"
|
||||||
:drop-directory="false"
|
:drop-directory="false"
|
||||||
:data="{
|
:data="{
|
||||||
direct_upload_url: '/rails/active_storage/direct_uploads',
|
direct_upload_url: '/rails/active_storage/direct_uploads',
|
||||||
@ -100,9 +101,9 @@
|
|||||||
<transition name="modal-fade">
|
<transition name="modal-fade">
|
||||||
<div
|
<div
|
||||||
v-show="$refs.upload && $refs.upload.dropActive"
|
v-show="$refs.upload && $refs.upload.dropActive"
|
||||||
class="modal-mask"
|
class="flex items-center justify-center gap-2 fixed left-0 right-0 top-0 bottom-0 w-full h-full z-20 text-slate-600 dark:text-slate-200 bg-white_transparent dark:bg-black_transparent flex-col"
|
||||||
>
|
>
|
||||||
<fluent-icon icon="cloud-backup" />
|
<fluent-icon icon="cloud-backup" size="40" />
|
||||||
<h4 class="page-sub-title text-slate-600 dark:text-slate-200">
|
<h4 class="page-sub-title text-slate-600 dark:text-slate-200">
|
||||||
{{ $t('CONVERSATION.REPLYBOX.DRAG_DROP') }}
|
{{ $t('CONVERSATION.REPLYBOX.DRAG_DROP') }}
|
||||||
</h4>
|
</h4>
|
||||||
@ -228,6 +229,10 @@ export default {
|
|||||||
type: String,
|
type: String,
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
|
newConversationModalActive: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters({
|
...mapGetters({
|
||||||
@ -274,6 +279,9 @@ export default {
|
|||||||
}
|
}
|
||||||
return ALLOWED_FILE_TYPES;
|
return ALLOWED_FILE_TYPES;
|
||||||
},
|
},
|
||||||
|
enableDragAndDrop() {
|
||||||
|
return !this.newConversationModalActive;
|
||||||
|
},
|
||||||
audioRecorderPlayStopIcon() {
|
audioRecorderPlayStopIcon() {
|
||||||
switch (this.recordingAudioState) {
|
switch (this.recordingAudioState) {
|
||||||
// playing paused recording stopped inactive destroyed
|
// playing paused recording stopped inactive destroyed
|
||||||
@ -346,12 +354,4 @@ export default {
|
|||||||
@apply dark:bg-slate-800 bg-slate-100;
|
@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>
|
</style>
|
||||||
|
|||||||
@ -1,8 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
:class="
|
: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`"
|
||||||
`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>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
@ -16,7 +16,7 @@
|
|||||||
>
|
>
|
||||||
<div class="items-center flex justify-between last:mt-4">
|
<div class="items-center flex justify-between last:mt-4">
|
||||||
<span class="text-slate-800 dark:text-slate-100 text-xs font-medium">{{
|
<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>
|
}}</span>
|
||||||
<filter-item
|
<filter-item
|
||||||
type="status"
|
type="status"
|
||||||
@ -28,7 +28,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="items-center flex justify-between last:mt-4">
|
<div class="items-center flex justify-between last:mt-4">
|
||||||
<span class="text-slate-800 dark:text-slate-100 text-xs font-medium">{{
|
<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>
|
}}</span>
|
||||||
<filter-item
|
<filter-item
|
||||||
type="sort"
|
type="sort"
|
||||||
|
|||||||
@ -52,6 +52,7 @@
|
|||||||
:key="currentChat.id + '-' + dashboardApp.id"
|
:key="currentChat.id + '-' + dashboardApp.id"
|
||||||
:is-visible="activeIndex - 1 === index"
|
:is-visible="activeIndex - 1 === index"
|
||||||
:config="dashboardApps[index].content"
|
:config="dashboardApps[index].content"
|
||||||
|
:position="index"
|
||||||
:current-chat="currentChat"
|
:current-chat="currentChat"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<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="{
|
:class="{
|
||||||
'active bg-slate-25 dark:bg-slate-800 border-woot-500': isActiveChat,
|
'active bg-slate-25 dark:bg-slate-800 border-woot-500': isActiveChat,
|
||||||
'unread-chat': hasUnread,
|
'unread-chat': hasUnread,
|
||||||
@ -31,7 +31,7 @@
|
|||||||
size="40px"
|
size="40px"
|
||||||
/>
|
/>
|
||||||
<div
|
<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">
|
<div class="flex justify-between">
|
||||||
<inbox-name v-if="showInboxName" :inbox="inbox" />
|
<inbox-name v-if="showInboxName" :inbox="inbox" />
|
||||||
@ -55,44 +55,11 @@
|
|||||||
>
|
>
|
||||||
{{ currentContact.name }}
|
{{ currentContact.name }}
|
||||||
</h4>
|
</h4>
|
||||||
<p
|
<message-preview
|
||||||
v-if="lastMessageInChat"
|
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"
|
: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"
|
||||||
<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>
|
|
||||||
<p
|
<p
|
||||||
v-else
|
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"
|
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"
|
icon="info"
|
||||||
/>
|
/>
|
||||||
<span>
|
<span>
|
||||||
{{ this.$t(`CHAT_LIST.NO_MESSAGES`) }}
|
{{ $t(`CHAT_LIST.NO_MESSAGES`) }}
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
<div class="conversation--meta flex flex-col absolute right-4 top-4">
|
<div class="absolute flex flex-col conversation--meta right-4 top-4">
|
||||||
<span class="text-black-600 text-xxs font-normal leading-4 ml-auto">
|
<span class="ml-auto font-normal leading-4 text-black-600 text-xxs">
|
||||||
<time-ago
|
<time-ago
|
||||||
:last-activity-timestamp="chat.timestamp"
|
:last-activity-timestamp="chat.timestamp"
|
||||||
:created-at-timestamp="chat.created_at"
|
:created-at-timestamp="chat.created_at"
|
||||||
@ -145,9 +112,8 @@
|
|||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import { MESSAGE_TYPE } from 'widget/helpers/constants';
|
|
||||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
|
||||||
import Thumbnail from '../Thumbnail.vue';
|
import Thumbnail from '../Thumbnail.vue';
|
||||||
|
import MessagePreview from './MessagePreview.vue';
|
||||||
import conversationMixin from '../../../mixins/conversations';
|
import conversationMixin from '../../../mixins/conversations';
|
||||||
import timeMixin from '../../../mixins/time';
|
import timeMixin from '../../../mixins/time';
|
||||||
import router from '../../../routes';
|
import router from '../../../routes';
|
||||||
@ -159,14 +125,6 @@ import alertMixin from 'shared/mixins/alertMixin';
|
|||||||
import TimeAgo from 'dashboard/components/ui/TimeAgo.vue';
|
import TimeAgo from 'dashboard/components/ui/TimeAgo.vue';
|
||||||
import CardLabels from './conversationCardComponents/CardLabels.vue';
|
import CardLabels from './conversationCardComponents/CardLabels.vue';
|
||||||
import PriorityMark from './PriorityMark.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 {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@ -175,16 +133,11 @@ export default {
|
|||||||
Thumbnail,
|
Thumbnail,
|
||||||
ConversationContextMenu,
|
ConversationContextMenu,
|
||||||
TimeAgo,
|
TimeAgo,
|
||||||
|
MessagePreview,
|
||||||
PriorityMark,
|
PriorityMark,
|
||||||
},
|
},
|
||||||
|
|
||||||
mixins: [
|
mixins: [inboxMixin, timeMixin, conversationMixin, alertMixin],
|
||||||
inboxMixin,
|
|
||||||
timeMixin,
|
|
||||||
conversationMixin,
|
|
||||||
messageFormatterMixin,
|
|
||||||
alertMixin,
|
|
||||||
],
|
|
||||||
props: {
|
props: {
|
||||||
activeLabel: {
|
activeLabel: {
|
||||||
type: String,
|
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() {
|
isActiveChat() {
|
||||||
return this.currentChat.id === this.chat.id;
|
return this.currentChat.id === this.chat.id;
|
||||||
},
|
},
|
||||||
@ -292,30 +231,6 @@ export default {
|
|||||||
return this.lastMessage(this.chat);
|
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() {
|
inbox() {
|
||||||
const { inbox_id: inboxId } = this.chat;
|
const { inbox_id: inboxId } = this.chat;
|
||||||
const stateInbox = this.$store.getters['inboxes/getInbox'](inboxId);
|
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"
|
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
|
<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
|
<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
|
<back-button
|
||||||
v-if="showBackButton"
|
v-if="showBackButton"
|
||||||
@ -19,26 +19,31 @@
|
|||||||
:username="currentContact.name"
|
:username="currentContact.name"
|
||||||
:status="currentContact.availability_status"
|
:status="currentContact.availability_status"
|
||||||
/>
|
/>
|
||||||
<div class="items-start flex flex-col ml-2 rtl:ml-0 rtl:mr-2 min-w-0">
|
<div
|
||||||
<woot-button
|
class="items-start flex flex-col ml-2 rtl:ml-0 rtl:mr-2 min-w-0 w-[inherit] overflow-hidden"
|
||||||
variant="link"
|
>
|
||||||
color-scheme="secondary"
|
<div class="flex items-center flex-row gap-1 m-0 p-0 w-[inherit]">
|
||||||
class="overflow-hidden whitespace-nowrap text-ellipsis"
|
<woot-button
|
||||||
@click.prevent="$emit('contact-panel-toggle')"
|
variant="link"
|
||||||
>
|
color-scheme="secondary"
|
||||||
<h3
|
class="[&>span]:overflow-hidden [&>span]:whitespace-nowrap [&>span]:text-ellipsis min-w-0"
|
||||||
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"
|
@click.prevent="$emit('contact-panel-toggle')"
|
||||||
>
|
>
|
||||||
<span>{{ currentContact.name }}</span>
|
<span
|
||||||
<fluent-icon
|
class="text-base leading-tight text-slate-900 dark:text-slate-100"
|
||||||
v-if="!isHMACVerified"
|
>
|
||||||
v-tooltip="$t('CONVERSATION.UNVERIFIED_SESSION')"
|
{{ currentContact.name }}
|
||||||
size="14"
|
</span>
|
||||||
class="text-yellow-600 dark:text-yellow-500 my-0 mx-0.5"
|
</woot-button>
|
||||||
icon="warning"
|
<fluent-icon
|
||||||
/>
|
v-if="!isHMACVerified"
|
||||||
</h3>
|
v-tooltip="$t('CONVERSATION.UNVERIFIED_SESSION')"
|
||||||
</woot-button>
|
size="14"
|
||||||
|
class="text-yellow-600 dark:text-yellow-500 my-0 mx-0 min-w-[14px]"
|
||||||
|
icon="warning"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="conversation--header--actions items-center flex text-xs gap-2 text-ellipsis overflow-hidden whitespace-nowrap"
|
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>
|
<template>
|
||||||
<woot-modal :show.sync="show" :on-close="onCancel">
|
<woot-modal :show.sync="show" :on-close="onCancel">
|
||||||
<div class="h-auto overflow-auto flex flex-col">
|
<div class="h-auto overflow-auto flex flex-col">
|
||||||
|
|||||||
@ -34,6 +34,11 @@
|
|||||||
:url="storyUrl"
|
:url="storyUrl"
|
||||||
/>
|
/>
|
||||||
</blockquote>
|
</blockquote>
|
||||||
|
<bubble-reply-to
|
||||||
|
v-if="inReplyToMessageId && inboxSupportsReplyTo"
|
||||||
|
:message="inReplyTo"
|
||||||
|
:message-type="data.message_type"
|
||||||
|
/>
|
||||||
<bubble-text
|
<bubble-text
|
||||||
v-if="data.content"
|
v-if="data.content"
|
||||||
:message="message"
|
:message="message"
|
||||||
@ -124,6 +129,7 @@
|
|||||||
:message="data"
|
:message="data"
|
||||||
@open="openContextMenu"
|
@open="openContextMenu"
|
||||||
@close="closeContextMenu"
|
@close="closeContextMenu"
|
||||||
|
@replyTo="handleReplyTo"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
@ -140,6 +146,7 @@ import BubbleLocation from './bubble/Location.vue';
|
|||||||
import BubbleMailHead from './bubble/MailHead.vue';
|
import BubbleMailHead from './bubble/MailHead.vue';
|
||||||
import BubbleText from './bubble/Text.vue';
|
import BubbleText from './bubble/Text.vue';
|
||||||
import BubbleContact from './bubble/Contact.vue';
|
import BubbleContact from './bubble/Contact.vue';
|
||||||
|
import BubbleReplyTo from './bubble/ReplyTo.vue';
|
||||||
import Spinner from 'shared/components/Spinner.vue';
|
import Spinner from 'shared/components/Spinner.vue';
|
||||||
import ContextMenu from 'dashboard/modules/conversations/components/MessageContextMenu.vue';
|
import ContextMenu from 'dashboard/modules/conversations/components/MessageContextMenu.vue';
|
||||||
import instagramImageErrorPlaceholder from './instagramImageErrorPlaceholder.vue';
|
import instagramImageErrorPlaceholder from './instagramImageErrorPlaceholder.vue';
|
||||||
@ -149,6 +156,8 @@ import { MESSAGE_TYPE, MESSAGE_STATUS } from 'shared/constants/messages';
|
|||||||
import { generateBotMessageContent } from './helpers/botMessageContentHelper';
|
import { generateBotMessageContent } from './helpers/botMessageContentHelper';
|
||||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||||
import { ACCOUNT_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
|
import { ACCOUNT_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
|
||||||
|
import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage';
|
||||||
|
import { LocalStorage } from 'shared/helpers/localStorage';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@ -162,6 +171,7 @@ export default {
|
|||||||
BubbleMailHead,
|
BubbleMailHead,
|
||||||
BubbleText,
|
BubbleText,
|
||||||
BubbleContact,
|
BubbleContact,
|
||||||
|
BubbleReplyTo,
|
||||||
ContextMenu,
|
ContextMenu,
|
||||||
Spinner,
|
Spinner,
|
||||||
instagramImageErrorPlaceholder,
|
instagramImageErrorPlaceholder,
|
||||||
@ -188,6 +198,14 @@ export default {
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
inboxSupportsReplyTo: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
inReplyTo: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@ -264,11 +282,19 @@ export default {
|
|||||||
) + botMessageContent
|
) + 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;
|
||||||
|
},
|
||||||
contextMenuEnabledOptions() {
|
contextMenuEnabledOptions() {
|
||||||
return {
|
return {
|
||||||
copy: this.hasText,
|
copy: this.hasText,
|
||||||
delete: this.hasText || this.hasAttachments,
|
delete: this.hasText || this.hasAttachments,
|
||||||
cannedResponse: this.isOutgoing && this.hasText,
|
cannedResponse: this.isOutgoing && this.hasText,
|
||||||
|
replyTo: !this.data.private && this.inboxSupportsReplyTo,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
contentAttributes() {
|
contentAttributes() {
|
||||||
@ -318,6 +344,8 @@ export default {
|
|||||||
left: isLeftAligned,
|
left: isLeftAligned,
|
||||||
right: isRightAligned,
|
right: isRightAligned,
|
||||||
'has-context-menu': this.showContextMenu,
|
'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-tweet-menu': this.isATweet,
|
||||||
'has-bg': this.showBackgroundHighlight,
|
'has-bg': this.showBackgroundHighlight,
|
||||||
};
|
};
|
||||||
@ -494,6 +522,13 @@ export default {
|
|||||||
this.showContextMenu = false;
|
this.showContextMenu = false;
|
||||||
this.contextMenuPosition = { x: null, y: null };
|
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() {
|
setupHighlightTimer() {
|
||||||
if (Number(this.$route.query.messageId) !== Number(this.data.id)) {
|
if (Number(this.$route.query.messageId) !== Number(this.data.id)) {
|
||||||
return;
|
return;
|
||||||
@ -613,6 +648,8 @@ li.right {
|
|||||||
}
|
}
|
||||||
|
|
||||||
li.left.has-tweet-menu .context-menu {
|
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;
|
@apply mb-6;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,93 @@
|
|||||||
|
<template>
|
||||||
|
<div class="overflow-hidden text-ellipsis whitespace-nowrap">
|
||||||
|
<template v-if="showMessageType">
|
||||||
|
<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"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<span v-if="message.content">
|
||||||
|
{{ parsedLastMessage }}
|
||||||
|
</span>
|
||||||
|
<span v-else-if="message.attachments">
|
||||||
|
<fluent-icon
|
||||||
|
v-if="attachmentIcon && showMessageType"
|
||||||
|
size="16"
|
||||||
|
class="-mt-0.5 align-middle inline-block text-slate-600 dark:text-slate-300"
|
||||||
|
:icon="attachmentIcon"
|
||||||
|
/>
|
||||||
|
{{ $t(`${attachmentMessageContent}`) }}
|
||||||
|
</span>
|
||||||
|
<span v-else>
|
||||||
|
{{ defaultEmptyMessage || $t('CHAT_LIST.NO_CONTENT') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { MESSAGE_TYPE } from 'widget/helpers/constants';
|
||||||
|
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||||
|
import { ATTACHMENT_ICONS } from 'shared/constants/messages';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'MessagePreview',
|
||||||
|
mixins: [messageFormatterMixin],
|
||||||
|
props: {
|
||||||
|
message: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
showMessageType: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
defaultEmptyMessage: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
messageByAgent() {
|
||||||
|
const { message_type: messageType } = this.message;
|
||||||
|
return messageType === MESSAGE_TYPE.OUTGOING;
|
||||||
|
},
|
||||||
|
isMessageAnActivity() {
|
||||||
|
const { message_type: messageType } = this.message;
|
||||||
|
return messageType === MESSAGE_TYPE.ACTIVITY;
|
||||||
|
},
|
||||||
|
isMessagePrivate() {
|
||||||
|
const { private: isPrivate } = this.message;
|
||||||
|
return isPrivate;
|
||||||
|
},
|
||||||
|
parsedLastMessage() {
|
||||||
|
const { content_attributes: contentAttributes } = this.message;
|
||||||
|
const { email: { subject } = {} } = contentAttributes || {};
|
||||||
|
return this.getPlainText(subject || this.message.content);
|
||||||
|
},
|
||||||
|
lastMessageFileType() {
|
||||||
|
const [{ file_type: fileType } = {}] = this.message.attachments;
|
||||||
|
return fileType;
|
||||||
|
},
|
||||||
|
attachmentIcon() {
|
||||||
|
return ATTACHMENT_ICONS[this.lastMessageFileType];
|
||||||
|
},
|
||||||
|
attachmentMessageContent() {
|
||||||
|
return `CHAT_LIST.ATTACHMENTS.${this.lastMessageFileType}.CONTENT`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="my-0 mx-4 px-1 flex max-h-[8vh] items-baseline justify-between hover:bg-slate-25 dark:hover:bg-slate-800 border border-dashed border-slate-100 dark:border-slate-700 rounded-sm overflow-auto"
|
||||||
|
>
|
||||||
|
<p class="w-fit !m-0">
|
||||||
|
{{ $t('CONVERSATION.FOOTER.MESSAGE_SIGNATURE_NOT_CONFIGURED') }}
|
||||||
|
<woot-button
|
||||||
|
color-scheme="primary"
|
||||||
|
variant="link"
|
||||||
|
@click="openProfileSettings"
|
||||||
|
>
|
||||||
|
{{ $t('CONVERSATION.FOOTER.CLICK_HERE') }}
|
||||||
|
</woot-button>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
methods: {
|
||||||
|
openProfileSettings() {
|
||||||
|
return this.$router.push({ name: 'profile_settings_index' });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style></style>
|
||||||
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="m-0 flex flex-col justify-between h-full flex-grow min-w-0">
|
<div class="flex flex-col justify-between flex-grow h-full min-w-0 m-0">
|
||||||
<banner
|
<banner
|
||||||
v-if="!currentChat.can_reply"
|
v-if="!currentChat.can_reply"
|
||||||
color-scheme="alert"
|
color-scheme="alert"
|
||||||
@ -7,21 +7,12 @@
|
|||||||
:href-link="replyWindowLink"
|
:href-link="replyWindowLink"
|
||||||
:href-link-text="replyWindowLinkText"
|
:href-link-text="replyWindowLinkText"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<banner
|
|
||||||
v-if="isATweet"
|
|
||||||
color-scheme="gray"
|
|
||||||
:banner-message="tweetBannerText"
|
|
||||||
:has-close-button="hasSelectedTweetId"
|
|
||||||
@close="removeTweetSelection"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div class="flex justify-end">
|
<div class="flex justify-end">
|
||||||
<woot-button
|
<woot-button
|
||||||
variant="smooth"
|
variant="smooth"
|
||||||
size="tiny"
|
size="tiny"
|
||||||
color-scheme="secondary"
|
color-scheme="secondary"
|
||||||
class="rounded-bl-calc rtl:rotate-180 rounded-tl-calc fixed top-[6.25rem] z-10 bg-white dark:bg-slate-700 border-slate-50 dark:border-slate-600 border-solid border border-r-0 box-border"
|
class="rounded-bl-calc rtl:rotate-180 rounded-tl-calc fixed top-[9.5rem] md:top-[6.25rem] z-10 bg-white dark:bg-slate-700 border-slate-50 dark:border-slate-600 border-solid border border-r-0 box-border"
|
||||||
:icon="isRightOrLeftIcon"
|
:icon="isRightOrLeftIcon"
|
||||||
@click="onToggleContactPanel"
|
@click="onToggleContactPanel"
|
||||||
/>
|
/>
|
||||||
@ -42,6 +33,8 @@
|
|||||||
:is-a-whatsapp-channel="isAWhatsAppChannel"
|
:is-a-whatsapp-channel="isAWhatsAppChannel"
|
||||||
:has-instagram-story="hasInstagramStory"
|
:has-instagram-story="hasInstagramStory"
|
||||||
:is-web-widget-inbox="isAWebWidgetInbox"
|
:is-web-widget-inbox="isAWebWidgetInbox"
|
||||||
|
:inbox-supports-reply-to="inboxSupportsReplyTo"
|
||||||
|
:in-reply-to="getInReplyToMessage(message)"
|
||||||
/>
|
/>
|
||||||
<li v-show="unreadMessageCount != 0" class="unread--toast">
|
<li v-show="unreadMessageCount != 0" class="unread--toast">
|
||||||
<span>
|
<span>
|
||||||
@ -63,6 +56,8 @@
|
|||||||
:is-a-whatsapp-channel="isAWhatsAppChannel"
|
:is-a-whatsapp-channel="isAWhatsAppChannel"
|
||||||
:has-instagram-story="hasInstagramStory"
|
:has-instagram-story="hasInstagramStory"
|
||||||
:is-web-widget-inbox="isAWebWidgetInbox"
|
:is-web-widget-inbox="isAWebWidgetInbox"
|
||||||
|
:inbox-supports-reply-to="inboxSupportsReplyTo"
|
||||||
|
:in-reply-to="getInReplyToMessage(message)"
|
||||||
/>
|
/>
|
||||||
<conversation-label-suggestion
|
<conversation-label-suggestion
|
||||||
v-if="shouldShowLabelSuggestions"
|
v-if="shouldShowLabelSuggestions"
|
||||||
@ -87,8 +82,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<reply-box
|
<reply-box
|
||||||
:conversation-id="currentChat.id"
|
:conversation-id="currentChat.id"
|
||||||
:is-a-tweet="isATweet"
|
|
||||||
:selected-tweet="selectedTweet"
|
|
||||||
:popout-reply-box.sync="isPopoutReplyBox"
|
:popout-reply-box.sync="isPopoutReplyBox"
|
||||||
@click="showPopoutReplyBox"
|
@click="showPopoutReplyBox"
|
||||||
/>
|
/>
|
||||||
@ -110,7 +103,7 @@ import { mapGetters } from 'vuex';
|
|||||||
import conversationMixin, {
|
import conversationMixin, {
|
||||||
filterDuplicateSourceMessages,
|
filterDuplicateSourceMessages,
|
||||||
} from '../../../mixins/conversations';
|
} from '../../../mixins/conversations';
|
||||||
import inboxMixin from 'shared/mixins/inboxMixin';
|
import inboxMixin, { INBOX_FEATURES } from 'shared/mixins/inboxMixin';
|
||||||
import configMixin from 'shared/mixins/configMixin';
|
import configMixin from 'shared/mixins/configMixin';
|
||||||
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
||||||
import aiMixin from 'dashboard/mixins/aiMixin';
|
import aiMixin from 'dashboard/mixins/aiMixin';
|
||||||
@ -123,6 +116,7 @@ import { LocalStorage } from 'shared/helpers/localStorage';
|
|||||||
|
|
||||||
// constants
|
// constants
|
||||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||||
|
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
||||||
import { REPLY_POLICY } from 'shared/constants/links';
|
import { REPLY_POLICY } from 'shared/constants/links';
|
||||||
import wootConstants from 'dashboard/constants/globals';
|
import wootConstants from 'dashboard/constants/globals';
|
||||||
import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage';
|
import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage';
|
||||||
@ -153,7 +147,6 @@ export default {
|
|||||||
isLoadingPrevious: true,
|
isLoadingPrevious: true,
|
||||||
heightBeforeLoad: null,
|
heightBeforeLoad: null,
|
||||||
conversationPanel: null,
|
conversationPanel: null,
|
||||||
selectedTweetId: null,
|
|
||||||
hasUserScrolled: false,
|
hasUserScrolled: false,
|
||||||
isProgrammaticScroll: false,
|
isProgrammaticScroll: false,
|
||||||
isPopoutReplyBox: false,
|
isPopoutReplyBox: false,
|
||||||
@ -164,12 +157,14 @@ export default {
|
|||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters({
|
...mapGetters({
|
||||||
|
accountId: 'getCurrentAccountId',
|
||||||
currentChat: 'getSelectedChat',
|
currentChat: 'getSelectedChat',
|
||||||
allConversations: 'getAllConversations',
|
allConversations: 'getAllConversations',
|
||||||
inboxesList: 'inboxes/getInboxes',
|
inboxesList: 'inboxes/getInboxes',
|
||||||
listLoadingStatus: 'getAllMessagesLoaded',
|
listLoadingStatus: 'getAllMessagesLoaded',
|
||||||
loadingChatList: 'getChatListLoadingStatus',
|
loadingChatList: 'getChatListLoadingStatus',
|
||||||
appIntegrations: 'integrations/getAppIntegrations',
|
appIntegrations: 'integrations/getAppIntegrations',
|
||||||
|
isFeatureEnabledonAccount: 'accounts/isFeatureEnabledonAccount',
|
||||||
currentAccountId: 'getCurrentAccountId',
|
currentAccountId: 'getCurrentAccountId',
|
||||||
}),
|
}),
|
||||||
isOpen() {
|
isOpen() {
|
||||||
@ -189,16 +184,6 @@ export default {
|
|||||||
inbox() {
|
inbox() {
|
||||||
return this.$store.getters['inboxes/getInbox'](this.inboxId);
|
return this.$store.getters['inboxes/getInbox'](this.inboxId);
|
||||||
},
|
},
|
||||||
hasSelectedTweetId() {
|
|
||||||
return !!this.selectedTweetId;
|
|
||||||
},
|
|
||||||
tweetBannerText() {
|
|
||||||
return !this.selectedTweetId
|
|
||||||
? this.$t('CONVERSATION.SELECT_A_TWEET_TO_REPLY')
|
|
||||||
: `
|
|
||||||
${this.$t('CONVERSATION.REPLYING_TO')}
|
|
||||||
${this.selectedTweet.content}` || '';
|
|
||||||
},
|
|
||||||
typingUsersList() {
|
typingUsersList() {
|
||||||
const userList = this.$store.getters[
|
const userList = this.$store.getters[
|
||||||
'conversationTypingStatus/getUserList'
|
'conversationTypingStatus/getUserList'
|
||||||
@ -257,17 +242,6 @@ export default {
|
|||||||
hasInstagramStory() {
|
hasInstagramStory() {
|
||||||
return this.conversationType === 'instagram_direct_message';
|
return this.conversationType === 'instagram_direct_message';
|
||||||
},
|
},
|
||||||
|
|
||||||
selectedTweet() {
|
|
||||||
if (this.selectedTweetId) {
|
|
||||||
const { messages = [] } = this.currentChat;
|
|
||||||
const [selectedMessage] = messages.filter(
|
|
||||||
message => message.id === this.selectedTweetId
|
|
||||||
);
|
|
||||||
return selectedMessage || {};
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
},
|
|
||||||
isRightOrLeftIcon() {
|
isRightOrLeftIcon() {
|
||||||
if (this.isContactPanelOpen) {
|
if (this.isContactPanelOpen) {
|
||||||
return 'arrow-chevron-right';
|
return 'arrow-chevron-right';
|
||||||
@ -316,6 +290,15 @@ export default {
|
|||||||
unreadMessageCount() {
|
unreadMessageCount() {
|
||||||
return this.currentChat.unread_count || 0;
|
return this.currentChat.unread_count || 0;
|
||||||
},
|
},
|
||||||
|
inboxSupportsReplyTo() {
|
||||||
|
return (
|
||||||
|
this.inboxHasFeature(INBOX_FEATURES.REPLY_TO) &&
|
||||||
|
this.isFeatureEnabledonAccount(
|
||||||
|
this.accountId,
|
||||||
|
FEATURE_FLAGS.MESSAGE_REPLY_TO
|
||||||
|
)
|
||||||
|
);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
@ -326,7 +309,6 @@ export default {
|
|||||||
this.fetchAllAttachmentsFromCurrentChat();
|
this.fetchAllAttachmentsFromCurrentChat();
|
||||||
this.fetchSuggestions();
|
this.fetchSuggestions();
|
||||||
this.messageSentSinceOpened = false;
|
this.messageSentSinceOpened = false;
|
||||||
this.selectedTweetId = null;
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -334,7 +316,6 @@ export default {
|
|||||||
bus.$on(BUS_EVENTS.SCROLL_TO_MESSAGE, this.onScrollToMessage);
|
bus.$on(BUS_EVENTS.SCROLL_TO_MESSAGE, this.onScrollToMessage);
|
||||||
// when a new message comes in, we refetch the label suggestions
|
// when a new message comes in, we refetch the label suggestions
|
||||||
bus.$on(BUS_EVENTS.FETCH_LABEL_SUGGESTIONS, this.fetchSuggestions);
|
bus.$on(BUS_EVENTS.FETCH_LABEL_SUGGESTIONS, this.fetchSuggestions);
|
||||||
bus.$on(BUS_EVENTS.SET_TWEET_REPLY, this.setSelectedTweet);
|
|
||||||
// when a message is sent we set the flag to true this hides the label suggestions,
|
// when a message is sent we set the flag to true this hides the label suggestions,
|
||||||
// until the chat is changed and the flag is reset in the watch for currentChat
|
// until the chat is changed and the flag is reset in the watch for currentChat
|
||||||
bus.$on(BUS_EVENTS.MESSAGE_SENT, () => {
|
bus.$on(BUS_EVENTS.MESSAGE_SENT, () => {
|
||||||
@ -405,10 +386,6 @@ export default {
|
|||||||
},
|
},
|
||||||
removeBusListeners() {
|
removeBusListeners() {
|
||||||
bus.$off(BUS_EVENTS.SCROLL_TO_MESSAGE, this.onScrollToMessage);
|
bus.$off(BUS_EVENTS.SCROLL_TO_MESSAGE, this.onScrollToMessage);
|
||||||
bus.$off(BUS_EVENTS.SET_TWEET_REPLY, this.setSelectedTweet);
|
|
||||||
},
|
|
||||||
setSelectedTweet(tweetId) {
|
|
||||||
this.selectedTweetId = tweetId;
|
|
||||||
},
|
},
|
||||||
onScrollToMessage({ messageId = '' } = {}) {
|
onScrollToMessage({ messageId = '' } = {}) {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
@ -450,16 +427,14 @@ export default {
|
|||||||
|
|
||||||
// label suggestions are not part of the messages list
|
// label suggestions are not part of the messages list
|
||||||
// so we need to handle them separately
|
// so we need to handle them separately
|
||||||
let labelSuggestions = this.conversationPanel.querySelector(
|
let labelSuggestions =
|
||||||
'.label-suggestion'
|
this.conversationPanel.querySelector('.label-suggestion');
|
||||||
);
|
|
||||||
|
|
||||||
// if there are unread messages, scroll to the first unread message
|
// if there are unread messages, scroll to the first unread message
|
||||||
if (this.unreadMessageCount > 0) {
|
if (this.unreadMessageCount > 0) {
|
||||||
// capturing only the unread messages
|
// capturing only the unread messages
|
||||||
relevantMessages = this.conversationPanel.querySelectorAll(
|
relevantMessages =
|
||||||
'.message--unread'
|
this.conversationPanel.querySelectorAll('.message--unread');
|
||||||
);
|
|
||||||
} else if (labelSuggestions) {
|
} else if (labelSuggestions) {
|
||||||
// when scrolling to the bottom, the label suggestions is below the last message
|
// when scrolling to the bottom, the label suggestions is below the last message
|
||||||
// so we scroll there if there are no unread messages
|
// so we scroll there if there are no unread messages
|
||||||
@ -533,8 +508,18 @@ export default {
|
|||||||
makeMessagesRead() {
|
makeMessagesRead() {
|
||||||
this.$store.dispatch('markMessagesRead', { id: this.currentChat.id });
|
this.$store.dispatch('markMessagesRead', { id: this.currentChat.id });
|
||||||
},
|
},
|
||||||
removeTweetSelection() {
|
|
||||||
this.selectedTweetId = null;
|
getInReplyToMessage(parentMessage) {
|
||||||
|
if (!parentMessage) return {};
|
||||||
|
const inReplyToMessageId = parentMessage.content_attributes?.in_reply_to;
|
||||||
|
if (!inReplyToMessageId) return {};
|
||||||
|
|
||||||
|
return this.currentChat?.messages.find(message => {
|
||||||
|
if (message.id === inReplyToMessageId) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -8,8 +8,10 @@
|
|||||||
}"
|
}"
|
||||||
class="shrink-0 rounded-sm inline-flex w-3.5 h-3.5"
|
class="shrink-0 rounded-sm inline-flex w-3.5 h-3.5"
|
||||||
:class="{
|
:class="{
|
||||||
'bg-red-50 dark:bg-red-700 dark:bg-opacity-30 text-red-500 dark:text-red-600': isUrgent,
|
'bg-red-50 dark:bg-red-700 dark:bg-opacity-30 text-red-500 dark:text-red-600':
|
||||||
'bg-slate-50 dark:bg-slate-700 text-slate-600 dark:text-slate-200': !isUrgent,
|
isUrgent,
|
||||||
|
'bg-slate-50 dark:bg-slate-700 text-slate-600 dark:text-slate-200':
|
||||||
|
!isUrgent,
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<fluent-icon
|
<fluent-icon
|
||||||
|
|||||||
@ -19,6 +19,11 @@
|
|||||||
@click="$emit('click')"
|
@click="$emit('click')"
|
||||||
/>
|
/>
|
||||||
<div class="reply-box__top">
|
<div class="reply-box__top">
|
||||||
|
<reply-to-message
|
||||||
|
v-if="shouldShowReplyToMessage"
|
||||||
|
:message="inReplyTo"
|
||||||
|
@dismiss="resetReplyToMessage"
|
||||||
|
/>
|
||||||
<canned-response
|
<canned-response
|
||||||
v-if="showMentions && hasSlashCommand"
|
v-if="showMentions && hasSlashCommand"
|
||||||
v-on-clickaway="hideMentions"
|
v-on-clickaway="hideMentions"
|
||||||
@ -86,21 +91,14 @@
|
|||||||
</div>
|
</div>
|
||||||
<div v-if="hasAttachments" class="attachment-preview-box" @paste="onPaste">
|
<div v-if="hasAttachments" class="attachment-preview-box" @paste="onPaste">
|
||||||
<attachment-preview
|
<attachment-preview
|
||||||
|
class="flex-col mt-4"
|
||||||
:attachments="attachedFiles"
|
:attachments="attachedFiles"
|
||||||
:remove-attachment="removeAttachment"
|
:remove-attachment="removeAttachment"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<message-signature-missing-alert
|
||||||
v-if="isSignatureEnabledForInbox && !isSignatureAvailable"
|
v-if="isSignatureEnabledForInbox && !isSignatureAvailable"
|
||||||
class="message-signature-wrap"
|
/>
|
||||||
>
|
|
||||||
<p class="message-signature">
|
|
||||||
{{ $t('CONVERSATION.FOOTER.MESSAGE_SIGNATURE_NOT_CONFIGURED') }}
|
|
||||||
<router-link :to="profilePath">
|
|
||||||
{{ $t('CONVERSATION.FOOTER.CLICK_HERE') }}
|
|
||||||
</router-link>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<reply-bottom-panel
|
<reply-bottom-panel
|
||||||
:conversation-id="conversationId"
|
:conversation-id="conversationId"
|
||||||
:enable-multiple-file-upload="enableMultipleFileUpload"
|
:enable-multiple-file-upload="enableMultipleFileUpload"
|
||||||
@ -123,6 +121,7 @@
|
|||||||
:toggle-audio-recorder="toggleAudioRecorder"
|
:toggle-audio-recorder="toggleAudioRecorder"
|
||||||
:toggle-emoji-picker="toggleEmojiPicker"
|
:toggle-emoji-picker="toggleEmojiPicker"
|
||||||
:message="message"
|
:message="message"
|
||||||
|
:new-conversation-modal-active="newConversationModalActive"
|
||||||
@selectWhatsappTemplate="openWhatsappTemplateModal"
|
@selectWhatsappTemplate="openWhatsappTemplateModal"
|
||||||
@toggle-editor="toggleRichContentEditor"
|
@toggle-editor="toggleRichContentEditor"
|
||||||
@replace-text="replaceText"
|
@replace-text="replaceText"
|
||||||
@ -149,22 +148,19 @@ import { mixin as clickaway } from 'vue-clickaway';
|
|||||||
import alertMixin from 'shared/mixins/alertMixin';
|
import alertMixin from 'shared/mixins/alertMixin';
|
||||||
|
|
||||||
import CannedResponse from './CannedResponse.vue';
|
import CannedResponse from './CannedResponse.vue';
|
||||||
|
import ReplyToMessage from './ReplyToMessage.vue';
|
||||||
import ResizableTextArea from 'shared/components/ResizableTextArea.vue';
|
import ResizableTextArea from 'shared/components/ResizableTextArea.vue';
|
||||||
import AttachmentPreview from 'dashboard/components/widgets/AttachmentsPreview.vue';
|
import AttachmentPreview from 'dashboard/components/widgets/AttachmentsPreview.vue';
|
||||||
import ReplyTopPanel from 'dashboard/components/widgets/WootWriter/ReplyTopPanel.vue';
|
import ReplyTopPanel from 'dashboard/components/widgets/WootWriter/ReplyTopPanel.vue';
|
||||||
import ReplyEmailHead from './ReplyEmailHead.vue';
|
import ReplyEmailHead from './ReplyEmailHead.vue';
|
||||||
import ReplyBottomPanel from 'dashboard/components/widgets/WootWriter/ReplyBottomPanel.vue';
|
import ReplyBottomPanel from 'dashboard/components/widgets/WootWriter/ReplyBottomPanel.vue';
|
||||||
|
import MessageSignatureMissingAlert from './MessageSignatureMissingAlert';
|
||||||
import Banner from 'dashboard/components/ui/Banner.vue';
|
import Banner from 'dashboard/components/ui/Banner.vue';
|
||||||
import { REPLY_EDITOR_MODES } from 'dashboard/components/widgets/WootWriter/constants';
|
import { REPLY_EDITOR_MODES } from 'dashboard/components/widgets/WootWriter/constants';
|
||||||
import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor.vue';
|
import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor.vue';
|
||||||
import WootAudioRecorder from 'dashboard/components/widgets/WootWriter/AudioRecorder.vue';
|
import WootAudioRecorder from 'dashboard/components/widgets/WootWriter/AudioRecorder.vue';
|
||||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||||
import { checkFileSizeLimit } from 'shared/helpers/FileHelper';
|
import { AUDIO_FORMATS } from 'shared/constants/messages';
|
||||||
import {
|
|
||||||
MAXIMUM_FILE_UPLOAD_SIZE,
|
|
||||||
MAXIMUM_FILE_UPLOAD_SIZE_TWILIO_SMS_CHANNEL,
|
|
||||||
AUDIO_FORMATS,
|
|
||||||
} from 'shared/constants/messages';
|
|
||||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||||
import {
|
import {
|
||||||
getMessageVariables,
|
getMessageVariables,
|
||||||
@ -174,21 +170,24 @@ import {
|
|||||||
import WhatsappTemplates from './WhatsappTemplates/Modal.vue';
|
import WhatsappTemplates from './WhatsappTemplates/Modal.vue';
|
||||||
import { buildHotKeys } from 'shared/helpers/KeyboardHelpers';
|
import { buildHotKeys } from 'shared/helpers/KeyboardHelpers';
|
||||||
import { MESSAGE_MAX_LENGTH } from 'shared/helpers/MessageTypeHelper';
|
import { MESSAGE_MAX_LENGTH } from 'shared/helpers/MessageTypeHelper';
|
||||||
import inboxMixin from 'shared/mixins/inboxMixin';
|
import inboxMixin, { INBOX_FEATURES } from 'shared/mixins/inboxMixin';
|
||||||
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
||||||
import { DirectUpload } from 'activestorage';
|
|
||||||
import { frontendURL } from '../../../helper/URLHelper';
|
|
||||||
import { trimContent, debounce } from '@chatwoot/utils';
|
import { trimContent, debounce } from '@chatwoot/utils';
|
||||||
import wootConstants from 'dashboard/constants/globals';
|
import wootConstants from 'dashboard/constants/globals';
|
||||||
import { isEditorHotKeyEnabled } from 'dashboard/mixins/uiSettings';
|
import { isEditorHotKeyEnabled } from 'dashboard/mixins/uiSettings';
|
||||||
import { CONVERSATION_EVENTS } from '../../../helper/AnalyticsHelper/events';
|
import { CONVERSATION_EVENTS } from '../../../helper/AnalyticsHelper/events';
|
||||||
import rtlMixin from 'shared/mixins/rtlMixin';
|
import rtlMixin from 'shared/mixins/rtlMixin';
|
||||||
|
import fileUploadMixin from 'dashboard/mixins/fileUploadMixin';
|
||||||
import {
|
import {
|
||||||
appendSignature,
|
appendSignature,
|
||||||
removeSignature,
|
removeSignature,
|
||||||
replaceSignature,
|
replaceSignature,
|
||||||
extractTextFromMarkdown,
|
extractTextFromMarkdown,
|
||||||
} from 'dashboard/helper/editorHelper';
|
} from 'dashboard/helper/editorHelper';
|
||||||
|
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
||||||
|
|
||||||
|
import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage';
|
||||||
|
import { LocalStorage } from 'shared/helpers/localStorage';
|
||||||
|
|
||||||
const EmojiInput = () => import('shared/components/emoji/EmojiInput');
|
const EmojiInput = () => import('shared/components/emoji/EmojiInput');
|
||||||
|
|
||||||
@ -196,6 +195,7 @@ export default {
|
|||||||
components: {
|
components: {
|
||||||
EmojiInput,
|
EmojiInput,
|
||||||
CannedResponse,
|
CannedResponse,
|
||||||
|
ReplyToMessage,
|
||||||
ResizableTextArea,
|
ResizableTextArea,
|
||||||
AttachmentPreview,
|
AttachmentPreview,
|
||||||
ReplyTopPanel,
|
ReplyTopPanel,
|
||||||
@ -205,6 +205,7 @@ export default {
|
|||||||
WootAudioRecorder,
|
WootAudioRecorder,
|
||||||
Banner,
|
Banner,
|
||||||
WhatsappTemplates,
|
WhatsappTemplates,
|
||||||
|
MessageSignatureMissingAlert,
|
||||||
},
|
},
|
||||||
mixins: [
|
mixins: [
|
||||||
clickaway,
|
clickaway,
|
||||||
@ -213,16 +214,9 @@ export default {
|
|||||||
alertMixin,
|
alertMixin,
|
||||||
messageFormatterMixin,
|
messageFormatterMixin,
|
||||||
rtlMixin,
|
rtlMixin,
|
||||||
|
fileUploadMixin,
|
||||||
],
|
],
|
||||||
props: {
|
props: {
|
||||||
selectedTweet: {
|
|
||||||
type: [Object, String],
|
|
||||||
default: () => ({}),
|
|
||||||
},
|
|
||||||
isATweet: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
popoutReplyBox: {
|
popoutReplyBox: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
@ -231,6 +225,7 @@ export default {
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
message: '',
|
message: '',
|
||||||
|
inReplyTo: {},
|
||||||
isFocused: false,
|
isFocused: false,
|
||||||
showEmojiPicker: false,
|
showEmojiPicker: false,
|
||||||
attachedFiles: [],
|
attachedFiles: [],
|
||||||
@ -252,6 +247,7 @@ export default {
|
|||||||
showUserMentions: false,
|
showUserMentions: false,
|
||||||
showCannedMenu: false,
|
showCannedMenu: false,
|
||||||
showVariablesMenu: false,
|
showVariablesMenu: false,
|
||||||
|
newConversationModalActive: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@ -262,7 +258,19 @@ export default {
|
|||||||
lastEmail: 'getLastEmailInSelectedChat',
|
lastEmail: 'getLastEmailInSelectedChat',
|
||||||
globalConfig: 'globalConfig/get',
|
globalConfig: 'globalConfig/get',
|
||||||
accountId: 'getCurrentAccountId',
|
accountId: 'getCurrentAccountId',
|
||||||
|
isFeatureEnabledonAccount: 'accounts/isFeatureEnabledonAccount',
|
||||||
}),
|
}),
|
||||||
|
shouldShowReplyToMessage() {
|
||||||
|
return (
|
||||||
|
this.inReplyTo?.id &&
|
||||||
|
!this.isPrivate &&
|
||||||
|
this.inboxHasFeature(INBOX_FEATURES.REPLY_TO) &&
|
||||||
|
this.isFeatureEnabledonAccount(
|
||||||
|
this.accountId,
|
||||||
|
FEATURE_FLAGS.MESSAGE_REPLY_TO
|
||||||
|
)
|
||||||
|
);
|
||||||
|
},
|
||||||
showRichContentEditor() {
|
showRichContentEditor() {
|
||||||
if (this.isOnPrivateNote || this.isRichEditorEnabled) {
|
if (this.isOnPrivateNote || this.isRichEditorEnabled) {
|
||||||
return true;
|
return true;
|
||||||
@ -334,10 +342,7 @@ export default {
|
|||||||
return this.maxLength - this.message.length;
|
return this.maxLength - this.message.length;
|
||||||
},
|
},
|
||||||
isReplyButtonDisabled() {
|
isReplyButtonDisabled() {
|
||||||
if (this.isATweet && !this.inReplyTo && !this.isOnPrivateNote) {
|
if (this.isATwitterInbox) return true;
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.hasAttachments || this.hasRecordedAudio) return false;
|
if (this.hasAttachments || this.hasRecordedAudio) return false;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -370,11 +375,6 @@ export default {
|
|||||||
if (this.isASmsInbox) {
|
if (this.isASmsInbox) {
|
||||||
return MESSAGE_MAX_LENGTH.TWILIO_SMS;
|
return MESSAGE_MAX_LENGTH.TWILIO_SMS;
|
||||||
}
|
}
|
||||||
if (this.isATwitterInbox) {
|
|
||||||
if (this.conversationType === 'tweet') {
|
|
||||||
return MESSAGE_MAX_LENGTH.TWEET - this.replyToUserLength - 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return MESSAGE_MAX_LENGTH.GENERAL;
|
return MESSAGE_MAX_LENGTH.GENERAL;
|
||||||
},
|
},
|
||||||
showFileUpload() {
|
showFileUpload() {
|
||||||
@ -392,8 +392,6 @@ export default {
|
|||||||
let sendMessageText = this.$t('CONVERSATION.REPLYBOX.SEND');
|
let sendMessageText = this.$t('CONVERSATION.REPLYBOX.SEND');
|
||||||
if (this.isPrivate) {
|
if (this.isPrivate) {
|
||||||
sendMessageText = this.$t('CONVERSATION.REPLYBOX.CREATE');
|
sendMessageText = this.$t('CONVERSATION.REPLYBOX.CREATE');
|
||||||
} else if (this.conversationType === 'tweet') {
|
|
||||||
sendMessageText = this.$t('CONVERSATION.REPLYBOX.TWEET');
|
|
||||||
}
|
}
|
||||||
const keyLabel = isEditorHotKeyEnabled(this.uiSettings, 'cmd_enter')
|
const keyLabel = isEditorHotKeyEnabled(this.uiSettings, 'cmd_enter')
|
||||||
? '(⌘ + ↵)'
|
? '(⌘ + ↵)'
|
||||||
@ -427,17 +425,12 @@ export default {
|
|||||||
isOnPrivateNote() {
|
isOnPrivateNote() {
|
||||||
return this.replyType === REPLY_EDITOR_MODES.NOTE;
|
return this.replyType === REPLY_EDITOR_MODES.NOTE;
|
||||||
},
|
},
|
||||||
inReplyTo() {
|
|
||||||
const selectedTweet = this.selectedTweet || {};
|
|
||||||
return selectedTweet.id;
|
|
||||||
},
|
|
||||||
isOnExpandedLayout() {
|
isOnExpandedLayout() {
|
||||||
const {
|
const {
|
||||||
LAYOUT_TYPES: { CONDENSED },
|
LAYOUT_TYPES: { CONDENSED },
|
||||||
} = wootConstants;
|
} = wootConstants;
|
||||||
const {
|
const { conversation_display_type: conversationDisplayType = CONDENSED } =
|
||||||
conversation_display_type: conversationDisplayType = CONDENSED,
|
this.uiSettings;
|
||||||
} = this.uiSettings;
|
|
||||||
return conversationDisplayType !== CONDENSED;
|
return conversationDisplayType !== CONDENSED;
|
||||||
},
|
},
|
||||||
emojiDialogClassOnExpandedLayoutAndRTLView() {
|
emojiDialogClassOnExpandedLayoutAndRTLView() {
|
||||||
@ -449,15 +442,6 @@ export default {
|
|||||||
}
|
}
|
||||||
return '';
|
return '';
|
||||||
},
|
},
|
||||||
replyToUserLength() {
|
|
||||||
const selectedTweet = this.selectedTweet || {};
|
|
||||||
const {
|
|
||||||
sender: {
|
|
||||||
additional_attributes: { screen_name: screenName = '' } = {},
|
|
||||||
} = {},
|
|
||||||
} = selectedTweet;
|
|
||||||
return screenName ? screenName.length : 0;
|
|
||||||
},
|
|
||||||
isMessageEmpty() {
|
isMessageEmpty() {
|
||||||
if (!this.message) {
|
if (!this.message) {
|
||||||
return true;
|
return true;
|
||||||
@ -485,9 +469,6 @@ export default {
|
|||||||
const { send_with_signature: isEnabled } = this.uiSettings;
|
const { send_with_signature: isEnabled } = this.uiSettings;
|
||||||
return isEnabled;
|
return isEnabled;
|
||||||
},
|
},
|
||||||
profilePath() {
|
|
||||||
return frontendURL(`accounts/${this.accountId}/profile/settings`);
|
|
||||||
},
|
|
||||||
editorMessageKey() {
|
editorMessageKey() {
|
||||||
const { editor_message_key: isEnabled } = this.uiSettings;
|
const { editor_message_key: isEnabled } = this.uiSettings;
|
||||||
return isEnabled;
|
return isEnabled;
|
||||||
@ -541,6 +522,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.setCCAndToEmailsFromLastChat();
|
this.setCCAndToEmailsFromLastChat();
|
||||||
|
this.fetchAndSetReplyTo();
|
||||||
},
|
},
|
||||||
conversationIdByRoute(conversationId, oldConversationId) {
|
conversationIdByRoute(conversationId, oldConversationId) {
|
||||||
if (conversationId !== oldConversationId) {
|
if (conversationId !== oldConversationId) {
|
||||||
@ -570,7 +552,7 @@ export default {
|
|||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
this.getFromDraft();
|
this.getFromDraft();
|
||||||
// Donot use the keyboard listener mixin here as the events here are supposed to be
|
// Don't use the keyboard listener mixin here as the events here are supposed to be
|
||||||
// working even if input/textarea is focussed.
|
// working even if input/textarea is focussed.
|
||||||
document.addEventListener('paste', this.onPaste);
|
document.addEventListener('paste', this.onPaste);
|
||||||
document.addEventListener('keydown', this.handleKeyEvents);
|
document.addEventListener('keydown', this.handleKeyEvents);
|
||||||
@ -582,10 +564,28 @@ export default {
|
|||||||
500,
|
500,
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.fetchAndSetReplyTo();
|
||||||
|
bus.$on(BUS_EVENTS.TOGGLE_REPLY_TO_MESSAGE, this.fetchAndSetReplyTo);
|
||||||
|
|
||||||
|
// A hacky fix to solve the drag and drop
|
||||||
|
// Is showing on top of new conversation modal drag and drop
|
||||||
|
// TODO need to find a better solution
|
||||||
|
bus.$on(
|
||||||
|
BUS_EVENTS.NEW_CONVERSATION_MODAL,
|
||||||
|
this.onNewConversationModalActive
|
||||||
|
);
|
||||||
},
|
},
|
||||||
destroyed() {
|
destroyed() {
|
||||||
document.removeEventListener('paste', this.onPaste);
|
document.removeEventListener('paste', this.onPaste);
|
||||||
document.removeEventListener('keydown', this.handleKeyEvents);
|
document.removeEventListener('keydown', this.handleKeyEvents);
|
||||||
|
bus.$off(BUS_EVENTS.TOGGLE_REPLY_TO_MESSAGE, this.fetchAndSetReplyTo);
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
bus.$off(
|
||||||
|
BUS_EVENTS.NEW_CONVERSATION_MODAL,
|
||||||
|
this.onNewConversationModalActive
|
||||||
|
);
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
toggleRichContentEditor() {
|
toggleRichContentEditor() {
|
||||||
@ -768,6 +768,7 @@ export default {
|
|||||||
: this.$track(CONVERSATION_EVENTS.SENT_MESSAGE, {
|
: this.$track(CONVERSATION_EVENTS.SENT_MESSAGE, {
|
||||||
channelType: this.channelType,
|
channelType: this.channelType,
|
||||||
signatureEnabled: this.sendWithSignature,
|
signatureEnabled: this.sendWithSignature,
|
||||||
|
hasReplyTo: !!this.inReplyTo?.id,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
async onSendReply() {
|
async onSendReply() {
|
||||||
@ -870,6 +871,7 @@ export default {
|
|||||||
}
|
}
|
||||||
this.attachedFiles = [];
|
this.attachedFiles = [];
|
||||||
this.isRecordingAudio = false;
|
this.isRecordingAudio = false;
|
||||||
|
this.resetReplyToMessage();
|
||||||
},
|
},
|
||||||
clearEmailField() {
|
clearEmailField() {
|
||||||
this.ccEmails = '';
|
this.ccEmails = '';
|
||||||
@ -944,67 +946,6 @@ export default {
|
|||||||
isPrivate,
|
isPrivate,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onFileUpload(file) {
|
|
||||||
if (this.globalConfig.directUploadsEnabled) {
|
|
||||||
this.onDirectFileUpload(file);
|
|
||||||
} else {
|
|
||||||
this.onIndirectFileUpload(file);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onDirectFileUpload(file) {
|
|
||||||
const MAXIMUM_SUPPORTED_FILE_UPLOAD_SIZE = this.isATwilioSMSChannel
|
|
||||||
? MAXIMUM_FILE_UPLOAD_SIZE_TWILIO_SMS_CHANNEL
|
|
||||||
: MAXIMUM_FILE_UPLOAD_SIZE;
|
|
||||||
|
|
||||||
if (!file) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (checkFileSizeLimit(file, MAXIMUM_SUPPORTED_FILE_UPLOAD_SIZE)) {
|
|
||||||
const upload = new DirectUpload(
|
|
||||||
file.file,
|
|
||||||
`/api/v1/accounts/${this.accountId}/conversations/${this.currentChat.id}/direct_uploads`,
|
|
||||||
{
|
|
||||||
directUploadWillCreateBlobWithXHR: xhr => {
|
|
||||||
xhr.setRequestHeader(
|
|
||||||
'api_access_token',
|
|
||||||
this.currentUser.access_token
|
|
||||||
);
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
upload.create((error, blob) => {
|
|
||||||
if (error) {
|
|
||||||
this.showAlert(error);
|
|
||||||
} else {
|
|
||||||
this.attachFile({ file, blob });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.showAlert(
|
|
||||||
this.$t('CONVERSATION.FILE_SIZE_LIMIT', {
|
|
||||||
MAXIMUM_SUPPORTED_FILE_UPLOAD_SIZE,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onIndirectFileUpload(file) {
|
|
||||||
const MAXIMUM_SUPPORTED_FILE_UPLOAD_SIZE = this.isATwilioSMSChannel
|
|
||||||
? MAXIMUM_FILE_UPLOAD_SIZE_TWILIO_SMS_CHANNEL
|
|
||||||
: MAXIMUM_FILE_UPLOAD_SIZE;
|
|
||||||
if (!file) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (checkFileSizeLimit(file, MAXIMUM_SUPPORTED_FILE_UPLOAD_SIZE)) {
|
|
||||||
this.attachFile({ file });
|
|
||||||
} else {
|
|
||||||
this.showAlert(
|
|
||||||
this.$t('CONVERSATION.FILE_SIZE_LIMIT', {
|
|
||||||
MAXIMUM_SUPPORTED_FILE_UPLOAD_SIZE,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
attachFile({ blob, file }) {
|
attachFile({ blob, file }) {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.readAsDataURL(file.file);
|
reader.readAsDataURL(file.file);
|
||||||
@ -1062,8 +1003,10 @@ export default {
|
|||||||
sender: this.sender,
|
sender: this.sender,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.inReplyTo) {
|
if (this.inReplyTo?.id) {
|
||||||
messagePayload.contentAttributes = { in_reply_to: this.inReplyTo };
|
messagePayload.contentAttributes = {
|
||||||
|
in_reply_to: this.inReplyTo.id,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.attachedFiles && this.attachedFiles.length) {
|
if (this.attachedFiles && this.attachedFiles.length) {
|
||||||
@ -1136,6 +1079,33 @@ export default {
|
|||||||
this.bccEmails = bcc.join(', ');
|
this.bccEmails = bcc.join(', ');
|
||||||
this.toEmails = to.join(', ');
|
this.toEmails = to.join(', ');
|
||||||
},
|
},
|
||||||
|
fetchAndSetReplyTo() {
|
||||||
|
const replyStorageKey = LOCAL_STORAGE_KEYS.MESSAGE_REPLY_TO;
|
||||||
|
const replyToMessageId = LocalStorage.getFromJsonStore(
|
||||||
|
replyStorageKey,
|
||||||
|
this.conversationId
|
||||||
|
);
|
||||||
|
|
||||||
|
this.inReplyTo = this.currentChat?.messages?.find(message => {
|
||||||
|
if (message.id === replyToMessageId) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
resetReplyToMessage() {
|
||||||
|
const replyStorageKey = LOCAL_STORAGE_KEYS.MESSAGE_REPLY_TO;
|
||||||
|
LocalStorage.deleteFromJsonStore(replyStorageKey, this.conversationId);
|
||||||
|
bus.$emit(BUS_EVENTS.TOGGLE_REPLY_TO_MESSAGE);
|
||||||
|
},
|
||||||
|
onNewConversationModalActive(isActive) {
|
||||||
|
// Issue is if the new conversation modal is open and we drag and drop the file
|
||||||
|
// then the file is not getting attached to the new conversation modal
|
||||||
|
// and it is getting attached to the current conversation reply box
|
||||||
|
// so to fix this we are removing the drag and drop event listener from the current conversation reply box
|
||||||
|
// When new conversation modal is open
|
||||||
|
this.newConversationModalActive = isActive;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
@ -1149,14 +1119,6 @@ export default {
|
|||||||
@apply py-2;
|
@apply py-2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-signature-wrap {
|
|
||||||
@apply my-0 mx-4 px-1 flex max-h-[8vh] items-baseline justify-between hover:bg-slate-25 dark:hover:bg-slate-800 border border-dashed border-slate-100 dark:border-slate-700 rounded-sm overflow-auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-signature {
|
|
||||||
@apply w-fit m-0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.attachment-preview-box {
|
.attachment-preview-box {
|
||||||
@apply bg-transparent py-0 px-4;
|
@apply bg-transparent py-0 px-4;
|
||||||
}
|
}
|
||||||
@ -1202,13 +1164,6 @@ export default {
|
|||||||
@apply left-1 -bottom-2;
|
@apply left-1 -bottom-2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.message-signature {
|
|
||||||
@apply mb-0;
|
|
||||||
|
|
||||||
::v-deep p:last-child {
|
|
||||||
@apply mb-0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.normal-editor__canned-box {
|
.normal-editor__canned-box {
|
||||||
width: calc(100% - 2 * var(--space-normal));
|
width: calc(100% - 2 * var(--space-normal));
|
||||||
|
|||||||
@ -0,0 +1,49 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="reply-editor bg-slate-50 dark:bg-slate-800 rounded-md py-1 pl-2 pr-1 text-xs tracking-wide mt-2 flex items-center gap-1.5 -mx-2"
|
||||||
|
>
|
||||||
|
<fluent-icon class="flex-shrink-0 icon" icon="arrow-reply" size="14" />
|
||||||
|
<div class="flex-grow gap-1 mt-px text-xs truncate">
|
||||||
|
{{ $t('CONVERSATION.REPLYBOX.REPLYING_TO') }}
|
||||||
|
<message-preview
|
||||||
|
:message="message"
|
||||||
|
:show-message-type="false"
|
||||||
|
:default-empty-message="$t('CONVERSATION.REPLY_MESSAGE_NOT_FOUND')"
|
||||||
|
class="inline"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<woot-button
|
||||||
|
v-tooltip="$t('CONVERSATION.REPLYBOX.DISMISS_REPLY')"
|
||||||
|
color-scheme="secondary"
|
||||||
|
icon="dismiss"
|
||||||
|
variant="clear"
|
||||||
|
size="tiny"
|
||||||
|
class="flex-shrink-0"
|
||||||
|
@click.stop="$emit('dismiss')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import MessagePreview from 'dashboard/components/widgets/conversation/MessagePreview.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: { MessagePreview },
|
||||||
|
props: {
|
||||||
|
message: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
// TODO: Remove this
|
||||||
|
// override for dashboard/assets/scss/widgets/_reply-box.scss
|
||||||
|
.reply-editor {
|
||||||
|
.icon {
|
||||||
|
margin-right: 0px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -1,3 +1,4 @@
|
|||||||
|
<!-- eslint-disable vue/no-mutating-props -->
|
||||||
<template>
|
<template>
|
||||||
<woot-modal :show.sync="show" :on-close="onClose" size="modal-big">
|
<woot-modal :show.sync="show" :on-close="onClose" size="modal-big">
|
||||||
<woot-modal-header
|
<woot-modal-header
|
||||||
|
|||||||
@ -33,7 +33,7 @@
|
|||||||
<woot-button variant="smooth" @click="$emit('resetTemplate')">
|
<woot-button variant="smooth" @click="$emit('resetTemplate')">
|
||||||
{{ $t('WHATSAPP_TEMPLATES.PARSER.GO_BACK_LABEL') }}
|
{{ $t('WHATSAPP_TEMPLATES.PARSER.GO_BACK_LABEL') }}
|
||||||
</woot-button>
|
</woot-button>
|
||||||
<woot-button @click="sendMessage">
|
<woot-button type="button" @click="sendMessage">
|
||||||
{{ $t('WHATSAPP_TEMPLATES.PARSER.SEND_MESSAGE_LABEL') }}
|
{{ $t('WHATSAPP_TEMPLATES.PARSER.SEND_MESSAGE_LABEL') }}
|
||||||
</woot-button>
|
</woot-button>
|
||||||
</footer>
|
</footer>
|
||||||
|
|||||||
@ -57,17 +57,6 @@
|
|||||||
@mouseenter="isHovered = true"
|
@mouseenter="isHovered = true"
|
||||||
@mouseleave="isHovered = false"
|
@mouseleave="isHovered = false"
|
||||||
/>
|
/>
|
||||||
<button
|
|
||||||
v-if="isATweet && (isIncoming || isOutgoing) && sourceId"
|
|
||||||
@click="onTweetReply"
|
|
||||||
>
|
|
||||||
<fluent-icon
|
|
||||||
v-tooltip.top-start="$t('CHAT_LIST.REPLY_TO_TWEET')"
|
|
||||||
icon="arrow-reply"
|
|
||||||
class="action--icon cursor-pointer"
|
|
||||||
size="16"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
<a
|
<a
|
||||||
v-if="isATweet && (isOutgoing || isIncoming) && linkToTweet"
|
v-if="isATweet && (isOutgoing || isIncoming) && linkToTweet"
|
||||||
:href="linkToTweet"
|
:href="linkToTweet"
|
||||||
@ -77,7 +66,7 @@
|
|||||||
<fluent-icon
|
<fluent-icon
|
||||||
v-tooltip.top-start="$t('CHAT_LIST.VIEW_TWEET_IN_TWITTER')"
|
v-tooltip.top-start="$t('CHAT_LIST.VIEW_TWEET_IN_TWITTER')"
|
||||||
icon="open"
|
icon="open"
|
||||||
class="action--icon cursor-pointer"
|
class="cursor-pointer action--icon"
|
||||||
size="16"
|
size="16"
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
@ -86,7 +75,6 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { MESSAGE_TYPE, MESSAGE_STATUS } from 'shared/constants/messages';
|
import { MESSAGE_TYPE, MESSAGE_STATUS } from 'shared/constants/messages';
|
||||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
|
||||||
import inboxMixin from 'shared/mixins/inboxMixin';
|
import inboxMixin from 'shared/mixins/inboxMixin';
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import timeMixin from '../../../../mixins/time';
|
import timeMixin from '../../../../mixins/time';
|
||||||
@ -187,8 +175,9 @@ export default {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
const { screenName, sourceId } = this;
|
const { screenName, sourceId } = this;
|
||||||
return `https://twitter.com/${screenName ||
|
return `https://twitter.com/${
|
||||||
this.inbox.name}/status/${sourceId}`;
|
screenName || this.inbox.name
|
||||||
|
}/status/${sourceId}`;
|
||||||
},
|
},
|
||||||
linkToStory() {
|
linkToStory() {
|
||||||
if (!this.storyId || !this.storySender) {
|
if (!this.storyId || !this.storySender) {
|
||||||
@ -212,7 +201,7 @@ export default {
|
|||||||
return !!this.sourceId;
|
return !!this.sourceId;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.isAWhatsAppChannel) {
|
if (this.isAWhatsAppChannel || this.isATwilioChannel) {
|
||||||
return this.sourceId && this.isSent;
|
return this.sourceId && this.isSent;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@ -222,9 +211,13 @@ export default {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.isAWhatsAppChannel) {
|
if (this.isAWhatsAppChannel || this.isATwilioChannel) {
|
||||||
return this.sourceId && this.isDelivered;
|
return this.sourceId && this.isDelivered;
|
||||||
}
|
}
|
||||||
|
// We will consider messages as delivered for web widget inbox if they are sent
|
||||||
|
if (this.isAWebWidgetInbox) {
|
||||||
|
return this.isSent;
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
@ -238,18 +231,13 @@ export default {
|
|||||||
return contactLastSeenAt >= this.createdAt;
|
return contactLastSeenAt >= this.createdAt;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.isAWhatsAppChannel) {
|
if (this.isAWhatsAppChannel || this.isATwilioChannel) {
|
||||||
return this.sourceId && this.isRead;
|
return this.sourceId && this.isRead;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
|
||||||
onTweetReply() {
|
|
||||||
bus.$emit(BUS_EVENTS.SET_TWEET_REPLY, this.id);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,41 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="px-2 py-1.5 -mx-2 rounded-sm min-w-[15rem] mb-2"
|
||||||
|
:class="{
|
||||||
|
'bg-slate-100 dark:bg-slate-600 dark:text-slate-50':
|
||||||
|
messageType === MESSAGE_TYPE.INCOMING,
|
||||||
|
'bg-woot-600 text-woot-50': messageType === MESSAGE_TYPE.OUTGOING,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<message-preview
|
||||||
|
:message="message"
|
||||||
|
:show-message-type="false"
|
||||||
|
:default-empty-message="$t('CONVERSATION.REPLY_MESSAGE_NOT_FOUND')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import MessagePreview from 'dashboard/components/widgets/conversation/MessagePreview.vue';
|
||||||
|
import { MESSAGE_TYPE } from 'shared/constants/messages';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ReplyTo',
|
||||||
|
components: {
|
||||||
|
MessagePreview,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
message: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
messageType: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return { MESSAGE_TYPE };
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@ -57,9 +57,8 @@ export default {
|
|||||||
async joinTheCall() {
|
async joinTheCall() {
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
try {
|
try {
|
||||||
const {
|
const { data: { authResponse: { authToken } = {} } = {} } =
|
||||||
data: { authResponse: { authToken } = {} } = {},
|
await DyteAPI.addParticipantToMeeting(this.messageId);
|
||||||
} = await DyteAPI.addParticipantToMeeting(this.messageId);
|
|
||||||
this.dyteAuthToken = authToken;
|
this.dyteAuthToken = authToken;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.showAlert(this.$t('INTEGRATION_SETTINGS.DYTE.JOIN_ERROR'));
|
this.showAlert(this.$t('INTEGRATION_SETTINGS.DYTE.JOIN_ERROR'));
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
<!-- eslint-disable vue/no-mutating-props -->
|
||||||
<template>
|
<template>
|
||||||
<woot-modal
|
<woot-modal
|
||||||
full-width
|
full-width
|
||||||
@ -212,8 +213,13 @@ export default {
|
|||||||
return this.activeFileType === ALLOWED_FILE_TYPES.AUDIO;
|
return this.activeFileType === ALLOWED_FILE_TYPES.AUDIO;
|
||||||
},
|
},
|
||||||
senderDetails() {
|
senderDetails() {
|
||||||
const { name, available_name: availableName, avatar_url, thumbnail, id } =
|
const {
|
||||||
this.activeAttachment?.sender || this.attachment?.sender;
|
name,
|
||||||
|
available_name: availableName,
|
||||||
|
avatar_url,
|
||||||
|
thumbnail,
|
||||||
|
id,
|
||||||
|
} = this.activeAttachment?.sender || this.attachment?.sender || {};
|
||||||
const currentUserID = this.currentUser?.id;
|
const currentUserID = this.currentUser?.id;
|
||||||
return {
|
return {
|
||||||
name: currentUserID === id ? 'You' : name || availableName || '',
|
name: currentUserID === id ? 'You' : name || availableName || '',
|
||||||
|
|||||||
@ -34,15 +34,13 @@
|
|||||||
:option="labelMenuConfig"
|
:option="labelMenuConfig"
|
||||||
:sub-menu-available="!!labels.length"
|
:sub-menu-available="!!labels.length"
|
||||||
>
|
>
|
||||||
<template>
|
<menu-item
|
||||||
<menu-item
|
v-for="label in labels"
|
||||||
v-for="label in labels"
|
:key="label.id"
|
||||||
:key="label.id"
|
:option="generateMenuLabelConfig(label, 'label')"
|
||||||
:option="generateMenuLabelConfig(label, 'label')"
|
variant="label"
|
||||||
variant="label"
|
@click="$emit('assign-label', label)"
|
||||||
@click="$emit('assign-label', label)"
|
/>
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</menu-item-with-submenu>
|
</menu-item-with-submenu>
|
||||||
<menu-item-with-submenu
|
<menu-item-with-submenu
|
||||||
:option="agentMenuConfig"
|
:option="agentMenuConfig"
|
||||||
|
|||||||
@ -17,7 +17,7 @@
|
|||||||
type="button"
|
type="button"
|
||||||
@click="onAvatarDelete"
|
@click="onAvatarDelete"
|
||||||
>
|
>
|
||||||
{{ this.$t('INBOX_MGMT.DELETE.AVATAR_DELETE_BUTTON_TEXT') }}
|
{{ $t('INBOX_MGMT.DELETE.AVATAR_DELETE_BUTTON_TEXT') }}
|
||||||
</woot-button>
|
</woot-button>
|
||||||
</div>
|
</div>
|
||||||
<label>
|
<label>
|
||||||
|
|||||||
@ -63,8 +63,9 @@
|
|||||||
<div v-if="filteredCountriesBySearch.length === 0">
|
<div v-if="filteredCountriesBySearch.length === 0">
|
||||||
<span
|
<span
|
||||||
class="flex items-center justify-center text-sm text-slate-500 dark:text-slate-300 mt-4"
|
class="flex items-center justify-center text-sm text-slate-500 dark:text-slate-300 mt-4"
|
||||||
>No results found</span
|
|
||||||
>
|
>
|
||||||
|
No results found
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
<!-- eslint-disable vue/no-mutating-props -->
|
||||||
<template>
|
<template>
|
||||||
<modal :show.sync="show" :on-close="closeModal">
|
<modal :show.sync="show" :on-close="closeModal">
|
||||||
<woot-modal-header :header-title="title" :header-content="message" />
|
<woot-modal-header :header-title="title" :header-content="message" />
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
<!-- eslint-disable vue/no-mutating-props -->
|
||||||
<template>
|
<template>
|
||||||
<modal :show.sync="show" :on-close="onClose">
|
<modal :show.sync="show" :on-close="onClose">
|
||||||
<woot-modal-header
|
<woot-modal-header
|
||||||
|
|||||||
@ -30,9 +30,7 @@
|
|||||||
<hotkey custom-class="min-h-[28px] min-w-[60px] normal-case key">
|
<hotkey custom-class="min-h-[28px] min-w-[60px] normal-case key">
|
||||||
{{ $t('KEYBOARD_SHORTCUTS.KEYS.ALT_OR_OPTION_KEY') }}
|
{{ $t('KEYBOARD_SHORTCUTS.KEYS.ALT_OR_OPTION_KEY') }}
|
||||||
</hotkey>
|
</hotkey>
|
||||||
<hotkey custom-class="min-h-[28px] w-9 key">
|
<hotkey custom-class="min-h-[28px] w-9 key"> J </hotkey>
|
||||||
J
|
|
||||||
</hotkey>
|
|
||||||
<span
|
<span
|
||||||
class="flex items-center font-semibold text-sm text-slate-800 dark:text-slate-100"
|
class="flex items-center font-semibold text-sm text-slate-800 dark:text-slate-100"
|
||||||
>
|
>
|
||||||
@ -42,9 +40,7 @@
|
|||||||
<hotkey custom-class="min-h-[28px] min-w-[60px] normal-case key">
|
<hotkey custom-class="min-h-[28px] min-w-[60px] normal-case key">
|
||||||
{{ $t('KEYBOARD_SHORTCUTS.KEYS.ALT_OR_OPTION_KEY') }}
|
{{ $t('KEYBOARD_SHORTCUTS.KEYS.ALT_OR_OPTION_KEY') }}
|
||||||
</hotkey>
|
</hotkey>
|
||||||
<hotkey custom-class="w-9 key">
|
<hotkey custom-class="w-9 key"> K </hotkey>
|
||||||
K
|
|
||||||
</hotkey>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -59,9 +55,7 @@
|
|||||||
<hotkey custom-class="min-h-[28px] min-w-[60px] normal-case key">
|
<hotkey custom-class="min-h-[28px] min-w-[60px] normal-case key">
|
||||||
{{ $t('KEYBOARD_SHORTCUTS.KEYS.ALT_OR_OPTION_KEY') }}
|
{{ $t('KEYBOARD_SHORTCUTS.KEYS.ALT_OR_OPTION_KEY') }}
|
||||||
</hotkey>
|
</hotkey>
|
||||||
<hotkey custom-class="w-9 key">
|
<hotkey custom-class="w-9 key"> E </hotkey>
|
||||||
E
|
|
||||||
</hotkey>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user