Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2605f24539 | ||
|
|
dd76dd4204 | ||
|
|
86b4183bde | ||
|
|
4a21633a2b | ||
|
|
f8c46341f6 | ||
|
|
771cb5b9fc | ||
|
|
f43018e017 | ||
|
|
467f3b9191 | ||
|
|
715400f7ab | ||
|
|
7b4cac746a | ||
|
|
6169d172d8 | ||
|
|
e06a6a7c55 | ||
|
|
c608ca37e0 | ||
|
|
6097f4c122 | ||
|
|
8372ae418e | ||
|
|
71391d982b | ||
|
|
b77fad52fb | ||
|
|
40f31cb0a3 | ||
|
|
7f8a199b29 | ||
|
|
23fe8847d8 | ||
|
|
430547e026 | ||
|
|
d0702363f4 | ||
|
|
d6d0c79618 | ||
|
|
9f8e442333 | ||
|
|
4a3ddad02f | ||
|
|
207a03155e | ||
|
|
796a7805db | ||
|
|
a50ea9c88a | ||
|
|
711251560b | ||
|
|
c4837cd7ac | ||
|
|
4b748e2c8c | ||
|
|
8c8c5a77c8 | ||
|
|
02dd5ecfab | ||
|
|
f405572e5c | ||
|
|
25876993ff | ||
|
|
b3ba8b9513 | ||
|
|
4a2452173e | ||
|
|
c62d74a01d | ||
|
|
5edf0f2bbe | ||
|
|
dd1fe4f93a | ||
|
|
e730804b48 | ||
|
|
578414d788 | ||
|
|
619441ce1c | ||
|
|
3c3b6f90c7 | ||
|
|
647efa12e7 | ||
|
|
b3545f42f1 | ||
|
|
fb03b6f434 | ||
|
|
1aa7d6f7d8 | ||
|
|
96b719017b | ||
|
|
b94e67f5d7 | ||
|
|
c76b588850 | ||
|
|
a28b911132 | ||
|
|
6e4ae1a1bc | ||
|
|
9583a2dbad | ||
|
|
11adfd2384 | ||
|
|
28d102f526 | ||
|
|
3d7ca61481 | ||
|
|
7b9e4982cf | ||
|
|
4d458c2184 | ||
|
|
0582d15eb5 | ||
|
|
915fe3897c | ||
|
|
f08d1b35d0 | ||
|
|
a3cb26a317 | ||
|
|
eee89bf0d8 | ||
|
|
1de18391b4 | ||
|
|
bb16780278 | ||
|
|
4260441f8c | ||
|
|
12c0be002e | ||
|
|
aff14b697f | ||
|
|
87a6266ddc | ||
|
|
cebb1bf871 | ||
|
|
ed2772dd59 | ||
|
|
4f19fa763c | ||
|
|
46c1734ba1 | ||
|
|
9b615f11f1 | ||
|
|
4ca66c1195 | ||
|
|
cc74bebc0f | ||
|
|
2c8a3ef3b2 | ||
|
|
6c94768bdb | ||
|
|
dcecbf4b80 | ||
|
|
d590b0ebc4 | ||
|
|
85f19fa25a | ||
|
|
1ca1b4d36b | ||
|
|
9059f5906a | ||
|
|
eb0da2026f | ||
|
|
e348db1e37 | ||
|
|
7ba24b90c4 | ||
|
|
3b015d4dd8 | ||
|
|
ea44a32758 | ||
|
|
80d83b401c | ||
|
|
ea3e011d3f | ||
|
|
2586c8e7e5 | ||
|
|
b9025a98d3 | ||
|
|
b6c3d61163 | ||
|
|
991f291c69 | ||
|
|
5ad6db07b4 | ||
|
|
e345a4486d | ||
|
|
5ad7e93059 | ||
|
|
505c93c467 | ||
|
|
464e12ceb7 | ||
|
|
e6f8895c1b | ||
|
|
a703ef2de6 | ||
|
|
94a473c9f8 | ||
|
|
2591a04c0b | ||
|
|
351a3dc372 | ||
|
|
d5c9193d1a | ||
|
|
de4e4c6f65 | ||
|
|
f34a2e6115 | ||
|
|
9c7044c81b | ||
|
|
04148b95a6 | ||
|
|
3d73d8935a | ||
|
|
edd93ca4a3 | ||
|
|
2c15d5b4f3 | ||
|
|
74d9203868 | ||
|
|
25001689ec | ||
|
|
2da618bd5e | ||
|
|
ba7f387dde | ||
|
|
1115bd18ed | ||
|
|
e9a162e71a | ||
|
|
4201b57315 | ||
|
|
0ec3a56041 | ||
|
|
4e416b4d51 | ||
|
|
4ae9ed8f94 | ||
|
|
7b2ff2f112 | ||
|
|
e2ff1b1a15 | ||
|
|
d891c1e7b5 | ||
|
|
2a07eae69f | ||
|
|
375d943a48 | ||
|
|
4c0f6c1333 | ||
|
|
97675c61ea | ||
|
|
f4c0ce5dfa | ||
|
|
9f37a6e2ba | ||
|
|
047070ad87 | ||
|
|
1a3d39a7f3 | ||
|
|
a737f89c47 | ||
|
|
a7987d4d1c | ||
|
|
32673ea8b4 | ||
|
|
9454c6b14f | ||
|
|
cf10f3d03b | ||
|
|
fba7f40bee | ||
|
|
5c6958482f | ||
|
|
8dcb4a5ed4 | ||
|
|
dbb50e5923 | ||
|
|
903072ef14 | ||
|
|
fc1f257793 | ||
|
|
8821106da9 | ||
|
|
e99ea0b582 | ||
|
|
193a531e49 | ||
|
|
91b9168fae | ||
|
|
1f3c5002b3 | ||
|
|
c0276d252a | ||
|
|
52d1821cd3 | ||
|
|
e0d24e0a73 | ||
|
|
b1e1d7e423 | ||
|
|
a95d249ec1 | ||
|
|
a1034a70c4 | ||
|
|
357a374478 | ||
|
|
1467a8fa33 | ||
|
|
13eaff156a | ||
|
|
e7e46590a4 | ||
|
|
17018d392d | ||
|
|
69eaf3ff7f | ||
|
|
cd6c9a8fe9 | ||
|
|
5710b7559e | ||
|
|
8e6d8e7654 | ||
|
|
59deffc7e3 | ||
|
|
34e8ad9dc5 | ||
|
|
23965fbaa3 | ||
|
|
8cff690640 | ||
|
|
b304f5a826 | ||
|
|
e90d064648 | ||
|
|
504fc24fb3 | ||
|
|
693f2531ab | ||
|
|
e997aaceb7 | ||
|
|
a7c947aeae | ||
|
|
ab864a86fd | ||
|
|
9617137688 | ||
|
|
1b36143d27 | ||
|
|
241e7d0c29 | ||
|
|
185f916b2a | ||
|
|
4a68d13310 | ||
|
|
ceb2b9673c | ||
|
|
1749b4eb61 | ||
|
|
346d89ecc0 | ||
|
|
c64cffb294 | ||
|
|
6deb3764dc | ||
|
|
4398734bdf | ||
|
|
290196d43b | ||
|
|
fcd2b892bf | ||
|
|
cab5a3e53b | ||
|
|
359caf754c | ||
|
|
7fcac532df | ||
|
|
d57be3ffae | ||
|
|
5046dcaf61 | ||
|
|
7df68c6388 | ||
|
|
905fd62974 | ||
|
|
1c99294c8c | ||
|
|
94209d29cb | ||
|
|
9c31d7c672 | ||
|
|
991a42c417 | ||
|
|
4a902fce6d | ||
|
|
5475746a87 | ||
|
|
acba07cf6e | ||
|
|
f44be0b1e6 | ||
|
|
bec1266242 | ||
|
|
ebb04487a8 | ||
|
|
de37975be4 | ||
|
|
8a26d78623 | ||
|
|
7ee7062843 | ||
|
|
a0884310f4 | ||
|
|
9a9462f5cb | ||
|
|
9b6f1df871 | ||
|
|
ab4d63a006 | ||
|
|
0745ceb430 | ||
|
|
f7ed7756d9 | ||
|
|
e1fe4f09bc | ||
|
|
a61dae0bb2 | ||
|
|
fade94658a | ||
|
|
e775d22b98 | ||
|
|
e304ad27cb | ||
|
|
3e8ae3865f | ||
|
|
223fb2b7e4 | ||
|
|
666028a443 |
@@ -22,6 +22,9 @@ checks:
|
||||
enabled: true
|
||||
config:
|
||||
threshold: 300
|
||||
method-lines:
|
||||
config:
|
||||
threshold: 50
|
||||
exclude_patterns:
|
||||
- 'spec/'
|
||||
- '**/specs/'
|
||||
@@ -44,3 +47,9 @@ exclude_patterns:
|
||||
- 'app/javascript/shared/constants/countries.js'
|
||||
- 'app/javascript/dashboard/components/widgets/conversation/advancedFilterItems/languages.js'
|
||||
- 'app/javascript/dashboard/routes/dashboard/contacts/contactFilterItems/index.js'
|
||||
- 'app/javascript/dashboard/routes/dashboard/settings/automation/constants.js'
|
||||
- 'app/javascript/dashboard/components/widgets/FilterInput/FilterOperatorTypes.js'
|
||||
- 'app/javascript/dashboard/routes/dashboard/settings/reports/constants.js'
|
||||
- 'app/javascript/dashboard/i18n/index.js'
|
||||
- 'app/javascript/widget/i18n/index.js'
|
||||
- 'app/javascript/survey/i18n/index.js'
|
||||
|
||||
+2
-2
@@ -29,8 +29,8 @@ module.exports = {
|
||||
'vue/html-self-closing': 'off',
|
||||
"vue/no-v-html": 'off',
|
||||
'vue/singleline-html-element-content-newline': 'off',
|
||||
'import/extensions': ['off']
|
||||
|
||||
'import/extensions': ['off'],
|
||||
'no-console': 'error'
|
||||
},
|
||||
settings: {
|
||||
'import/resolver': {
|
||||
|
||||
@@ -11,10 +11,12 @@ Metrics/ClassLength:
|
||||
Max: 125
|
||||
Exclude:
|
||||
- 'app/models/conversation.rb'
|
||||
- 'app/models/contact.rb'
|
||||
- 'app/mailers/conversation_reply_mailer.rb'
|
||||
- 'app/models/message.rb'
|
||||
- 'app/builders/messages/facebook/message_builder.rb'
|
||||
- 'app/controllers/api/v1/accounts/contacts_controller.rb'
|
||||
- 'app/listeners/action_cable_listener.rb'
|
||||
RSpec/ExampleLength:
|
||||
Max: 25
|
||||
Style/Documentation:
|
||||
@@ -25,6 +27,8 @@ Style/FrozenStringLiteralComment:
|
||||
Enabled: false
|
||||
Style/SymbolArray:
|
||||
Enabled: false
|
||||
Style/OpenStructUse:
|
||||
Enabled: false
|
||||
Style/OptionalBooleanParameter:
|
||||
Exclude:
|
||||
- 'app/services/email_templates/db_resolver_service.rb'
|
||||
@@ -67,14 +71,20 @@ Rails/ApplicationController:
|
||||
- 'app/controllers/platform_controller.rb'
|
||||
- 'app/controllers/public_controller.rb'
|
||||
- 'app/controllers/survey/responses_controller.rb'
|
||||
Rails/CompactBlank:
|
||||
Enabled: false
|
||||
Rails/EnvironmentVariableAccess:
|
||||
Enabled: false
|
||||
Rails/TimeZoneAssignment:
|
||||
Enabled: false
|
||||
Rails/RedundantPresenceValidationOnBelongsTo:
|
||||
Enabled: false
|
||||
Style/ClassAndModuleChildren:
|
||||
EnforcedStyle: compact
|
||||
Exclude:
|
||||
- 'config/application.rb'
|
||||
Style/MapToHash:
|
||||
Enabled: false
|
||||
RSpec/NestedGroups:
|
||||
Enabled: true
|
||||
Max: 4
|
||||
@@ -82,6 +92,8 @@ RSpec/MessageSpies:
|
||||
Enabled: false
|
||||
RSpec/StubbedMock:
|
||||
Enabled: false
|
||||
RSpec/FactoryBot/SyntaxMethods:
|
||||
Enabled: false
|
||||
Naming/VariableNumber:
|
||||
Enabled: false
|
||||
Metrics/MethodLength:
|
||||
@@ -118,6 +130,7 @@ Rails/ReversibleMigration:
|
||||
- 'db/migrate/20191020085608_rename_old_tables.rb'
|
||||
- 'db/migrate/20191126185833_update_user_invite_foreign_key.rb'
|
||||
- 'db/migrate/20191130164019_add_template_type_to_messages.rb'
|
||||
- 'db/migrate/20210513083044_remove_not_null_from_webhook_url_channel_api.rb'
|
||||
Rails/BulkChangeTable:
|
||||
Exclude:
|
||||
- 'db/migrate/20161025070152_removechannelsfrommodels.rb'
|
||||
|
||||
+1
-1
@@ -50,7 +50,7 @@ decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
This Code of Conduct applies within all community spaces and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official e-mail address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
|
||||
+1
-1
@@ -2,4 +2,4 @@
|
||||
|
||||
Thanks for taking the time to contribute! :tada::+1:
|
||||
|
||||
Please refer to our [Contributing Guide](https://www.chatwoot.com/docs/contributing-guide) for detailed instructions.
|
||||
Please refer to our [Contributing Guide](https://www.chatwoot.com/docs/contributing-guide) for detailed instructions on how to contribute.
|
||||
|
||||
@@ -42,7 +42,7 @@ gem 'down', '~> 5.0'
|
||||
gem 'aws-sdk-s3', require: false
|
||||
gem 'azure-storage-blob', require: false
|
||||
gem 'google-cloud-storage', require: false
|
||||
gem 'image_processing'
|
||||
gem 'image_processing', '~> 1.12.2'
|
||||
|
||||
##-- gems for database --#
|
||||
gem 'groupdate'
|
||||
@@ -102,7 +102,7 @@ gem 'sentry-ruby'
|
||||
gem 'sentry-sidekiq'
|
||||
|
||||
##-- background job processing --##
|
||||
gem 'sidekiq'
|
||||
gem 'sidekiq', '~> 6.4.0'
|
||||
# We want cron jobs
|
||||
gem 'sidekiq-cron'
|
||||
|
||||
|
||||
+206
-205
@@ -9,73 +9,73 @@ GIT
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
actioncable (6.1.4.3)
|
||||
actionpack (= 6.1.4.3)
|
||||
activesupport (= 6.1.4.3)
|
||||
actioncable (6.1.4.7)
|
||||
actionpack (= 6.1.4.7)
|
||||
activesupport (= 6.1.4.7)
|
||||
nio4r (~> 2.0)
|
||||
websocket-driver (>= 0.6.1)
|
||||
actionmailbox (6.1.4.3)
|
||||
actionpack (= 6.1.4.3)
|
||||
activejob (= 6.1.4.3)
|
||||
activerecord (= 6.1.4.3)
|
||||
activestorage (= 6.1.4.3)
|
||||
activesupport (= 6.1.4.3)
|
||||
actionmailbox (6.1.4.7)
|
||||
actionpack (= 6.1.4.7)
|
||||
activejob (= 6.1.4.7)
|
||||
activerecord (= 6.1.4.7)
|
||||
activestorage (= 6.1.4.7)
|
||||
activesupport (= 6.1.4.7)
|
||||
mail (>= 2.7.1)
|
||||
actionmailer (6.1.4.3)
|
||||
actionpack (= 6.1.4.3)
|
||||
actionview (= 6.1.4.3)
|
||||
activejob (= 6.1.4.3)
|
||||
activesupport (= 6.1.4.3)
|
||||
actionmailer (6.1.4.7)
|
||||
actionpack (= 6.1.4.7)
|
||||
actionview (= 6.1.4.7)
|
||||
activejob (= 6.1.4.7)
|
||||
activesupport (= 6.1.4.7)
|
||||
mail (~> 2.5, >= 2.5.4)
|
||||
rails-dom-testing (~> 2.0)
|
||||
actionpack (6.1.4.3)
|
||||
actionview (= 6.1.4.3)
|
||||
activesupport (= 6.1.4.3)
|
||||
actionpack (6.1.4.7)
|
||||
actionview (= 6.1.4.7)
|
||||
activesupport (= 6.1.4.7)
|
||||
rack (~> 2.0, >= 2.0.9)
|
||||
rack-test (>= 0.6.3)
|
||||
rails-dom-testing (~> 2.0)
|
||||
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
||||
actiontext (6.1.4.3)
|
||||
actionpack (= 6.1.4.3)
|
||||
activerecord (= 6.1.4.3)
|
||||
activestorage (= 6.1.4.3)
|
||||
activesupport (= 6.1.4.3)
|
||||
actiontext (6.1.4.7)
|
||||
actionpack (= 6.1.4.7)
|
||||
activerecord (= 6.1.4.7)
|
||||
activestorage (= 6.1.4.7)
|
||||
activesupport (= 6.1.4.7)
|
||||
nokogiri (>= 1.8.5)
|
||||
actionview (6.1.4.3)
|
||||
activesupport (= 6.1.4.3)
|
||||
actionview (6.1.4.7)
|
||||
activesupport (= 6.1.4.7)
|
||||
builder (~> 3.1)
|
||||
erubi (~> 1.4)
|
||||
rails-dom-testing (~> 2.0)
|
||||
rails-html-sanitizer (~> 1.1, >= 1.2.0)
|
||||
active_record_query_trace (1.8)
|
||||
activejob (6.1.4.3)
|
||||
activesupport (= 6.1.4.3)
|
||||
activejob (6.1.4.7)
|
||||
activesupport (= 6.1.4.7)
|
||||
globalid (>= 0.3.6)
|
||||
activemodel (6.1.4.3)
|
||||
activesupport (= 6.1.4.3)
|
||||
activerecord (6.1.4.3)
|
||||
activemodel (= 6.1.4.3)
|
||||
activesupport (= 6.1.4.3)
|
||||
activerecord-import (1.2.0)
|
||||
activerecord (>= 3.2)
|
||||
activestorage (6.1.4.3)
|
||||
actionpack (= 6.1.4.3)
|
||||
activejob (= 6.1.4.3)
|
||||
activerecord (= 6.1.4.3)
|
||||
activesupport (= 6.1.4.3)
|
||||
activemodel (6.1.4.7)
|
||||
activesupport (= 6.1.4.7)
|
||||
activerecord (6.1.4.7)
|
||||
activemodel (= 6.1.4.7)
|
||||
activesupport (= 6.1.4.7)
|
||||
activerecord-import (1.3.0)
|
||||
activerecord (>= 4.2)
|
||||
activestorage (6.1.4.7)
|
||||
actionpack (= 6.1.4.7)
|
||||
activejob (= 6.1.4.7)
|
||||
activerecord (= 6.1.4.7)
|
||||
activesupport (= 6.1.4.7)
|
||||
marcel (~> 1.0.0)
|
||||
mini_mime (>= 1.1.0)
|
||||
activesupport (6.1.4.3)
|
||||
activesupport (6.1.4.7)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
i18n (>= 1.6, < 2)
|
||||
minitest (>= 5.1)
|
||||
tzinfo (~> 2.0)
|
||||
zeitwerk (~> 2.3)
|
||||
acts-as-taggable-on (8.1.0)
|
||||
activerecord (>= 5.0, < 6.2)
|
||||
acts-as-taggable-on (9.0.1)
|
||||
activerecord (>= 6.0, < 7.1)
|
||||
addressable (2.8.0)
|
||||
public_suffix (>= 2.0.2, < 5.0)
|
||||
administrate (0.16.0)
|
||||
administrate (0.17.0)
|
||||
actionpack (>= 5.0)
|
||||
actionview (>= 5.0)
|
||||
activerecord (>= 5.0)
|
||||
@@ -85,46 +85,46 @@ GEM
|
||||
momentjs-rails (~> 2.8)
|
||||
sassc-rails (~> 2.1)
|
||||
selectize-rails (~> 0.6)
|
||||
annotate (3.1.1)
|
||||
activerecord (>= 3.2, < 7.0)
|
||||
annotate (3.2.0)
|
||||
activerecord (>= 3.2, < 8.0)
|
||||
rake (>= 10.4, < 14.0)
|
||||
ast (2.4.2)
|
||||
attr_extras (6.2.4)
|
||||
attr_extras (6.2.5)
|
||||
aws-eventstream (1.2.0)
|
||||
aws-partitions (1.513.0)
|
||||
aws-sdk-core (3.121.1)
|
||||
aws-partitions (1.556.0)
|
||||
aws-sdk-core (3.126.2)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
aws-partitions (~> 1, >= 1.239.0)
|
||||
aws-partitions (~> 1, >= 1.525.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
jmespath (~> 1.0)
|
||||
aws-sdk-kms (1.49.0)
|
||||
aws-sdk-core (~> 3, >= 3.120.0)
|
||||
aws-sdk-kms (1.54.0)
|
||||
aws-sdk-core (~> 3, >= 3.126.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-sdk-s3 (1.103.0)
|
||||
aws-sdk-core (~> 3, >= 3.120.0)
|
||||
aws-sdk-s3 (1.112.0)
|
||||
aws-sdk-core (~> 3, >= 3.126.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.4)
|
||||
aws-sigv4 (1.4.0)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
azure-storage-blob (2.0.1)
|
||||
azure-storage-blob (2.0.3)
|
||||
azure-storage-common (~> 2.0)
|
||||
nokogiri (~> 1.11.0.rc2)
|
||||
azure-storage-common (2.0.2)
|
||||
nokogiri (~> 1, >= 1.10.8)
|
||||
azure-storage-common (2.0.4)
|
||||
faraday (~> 1.0)
|
||||
faraday_middleware (~> 1.0.0.rc1)
|
||||
faraday_middleware (~> 1.0, >= 1.0.0.rc1)
|
||||
net-http-persistent (~> 4.0)
|
||||
nokogiri (~> 1.11.0.rc2)
|
||||
nokogiri (~> 1, >= 1.10.8)
|
||||
barnes (0.0.9)
|
||||
multi_json (~> 1)
|
||||
statsd-ruby (~> 1.1)
|
||||
bcrypt (3.1.16)
|
||||
bindex (0.8.1)
|
||||
bootsnap (1.9.1)
|
||||
msgpack (~> 1.0)
|
||||
brakeman (5.1.1)
|
||||
bootsnap (1.10.3)
|
||||
msgpack (~> 1.2)
|
||||
brakeman (5.2.1)
|
||||
browser (5.3.1)
|
||||
builder (3.2.4)
|
||||
bullet (6.1.5)
|
||||
bullet (7.0.1)
|
||||
activesupport (>= 3.0.0)
|
||||
uniform_notifier (~> 1.11)
|
||||
bundle-audit (0.1.0)
|
||||
@@ -135,13 +135,13 @@ GEM
|
||||
byebug (11.1.3)
|
||||
climate_control (1.0.1)
|
||||
coderay (1.1.3)
|
||||
commonmarker (0.23.2)
|
||||
commonmarker (0.23.4)
|
||||
concurrent-ruby (1.1.9)
|
||||
connection_pool (2.2.5)
|
||||
crack (0.4.5)
|
||||
rexml
|
||||
crass (1.0.6)
|
||||
cypress-on-rails (1.11.0)
|
||||
cypress-on-rails (1.12.1)
|
||||
rack
|
||||
database_cleaner (2.0.1)
|
||||
database_cleaner-active_record (~> 2.0.0)
|
||||
@@ -151,11 +151,12 @@ GEM
|
||||
database_cleaner-core (2.0.1)
|
||||
datetime_picker_rails (0.0.7)
|
||||
momentjs-rails (>= 2.8.1)
|
||||
ddtrace (0.53.0)
|
||||
ffi (~> 1.0)
|
||||
ddtrace (0.54.2)
|
||||
debase-ruby_core_source (<= 0.10.14)
|
||||
msgpack
|
||||
debase-ruby_core_source (0.10.14)
|
||||
declarative (0.0.20)
|
||||
devise (4.8.0)
|
||||
devise (4.8.1)
|
||||
bcrypt (~> 3.0)
|
||||
orm_adapter (~> 0.1)
|
||||
railties (>= 4.1.0)
|
||||
@@ -165,7 +166,7 @@ GEM
|
||||
bcrypt (~> 3.0)
|
||||
devise (> 3.5.2, < 5)
|
||||
rails (>= 4.2.0, < 6.2)
|
||||
diff-lcs (1.4.4)
|
||||
diff-lcs (1.5.0)
|
||||
digest-crc (0.6.4)
|
||||
rake (>= 12.0.0, < 14.0.0)
|
||||
docile (1.4.0)
|
||||
@@ -175,14 +176,14 @@ GEM
|
||||
dotenv-rails (2.7.6)
|
||||
dotenv (= 2.7.6)
|
||||
railties (>= 3.2)
|
||||
down (5.2.4)
|
||||
down (5.3.0)
|
||||
addressable (~> 2.8)
|
||||
ecma-re-validator (0.3.0)
|
||||
regexp_parser (~> 2.0)
|
||||
ecma-re-validator (0.4.0)
|
||||
regexp_parser (~> 2.2)
|
||||
email_reply_trimmer (0.1.13)
|
||||
erubi (1.10.0)
|
||||
erubis (2.7.0)
|
||||
et-orbi (1.2.5)
|
||||
et-orbi (1.2.6)
|
||||
tzinfo
|
||||
execjs (2.8.1)
|
||||
facebook-messenger (2.0.1)
|
||||
@@ -197,11 +198,11 @@ GEM
|
||||
i18n (>= 1.6, < 2)
|
||||
faraday (1.0.1)
|
||||
multipart-post (>= 1.2, < 3)
|
||||
faraday_middleware (1.0.0)
|
||||
faraday_middleware (1.2.0)
|
||||
faraday (~> 1.0)
|
||||
fcm (1.0.3)
|
||||
fcm (1.0.5)
|
||||
faraday (~> 1)
|
||||
ffi (1.15.4)
|
||||
ffi (1.15.5)
|
||||
flag_shih_tzu (0.3.23)
|
||||
flay (2.12.1)
|
||||
erubis (~> 2.7.0)
|
||||
@@ -218,11 +219,11 @@ GEM
|
||||
googleapis-common-protos-types (>= 1.0.4, < 2.0)
|
||||
googleauth (~> 0.9)
|
||||
grpc (~> 1.25)
|
||||
geocoder (1.7.0)
|
||||
gli (2.20.1)
|
||||
geocoder (1.7.3)
|
||||
gli (2.21.0)
|
||||
globalid (1.0.0)
|
||||
activesupport (>= 5.0)
|
||||
google-apis-core (0.4.1)
|
||||
google-apis-core (0.4.2)
|
||||
addressable (~> 2.5, >= 2.5.1)
|
||||
googleauth (>= 0.16.2, < 2.a)
|
||||
httpclient (>= 2.8.1, < 3.a)
|
||||
@@ -231,9 +232,9 @@ GEM
|
||||
retriable (>= 2.0, < 4.a)
|
||||
rexml
|
||||
webrick
|
||||
google-apis-iamcredentials_v1 (0.7.0)
|
||||
google-apis-iamcredentials_v1 (0.10.0)
|
||||
google-apis-core (>= 0.4, < 2.a)
|
||||
google-apis-storage_v1 (0.8.0)
|
||||
google-apis-storage_v1 (0.11.0)
|
||||
google-apis-core (>= 0.4, < 2.a)
|
||||
google-cloud-core (1.6.0)
|
||||
google-cloud-env (~> 1.0)
|
||||
@@ -247,22 +248,22 @@ GEM
|
||||
google-cloud-env (1.5.0)
|
||||
faraday (>= 0.17.3, < 2.0)
|
||||
google-cloud-errors (1.2.0)
|
||||
google-cloud-storage (1.34.1)
|
||||
addressable (~> 2.5)
|
||||
google-cloud-storage (1.36.1)
|
||||
addressable (~> 2.8)
|
||||
digest-crc (~> 0.4)
|
||||
google-apis-iamcredentials_v1 (~> 0.1)
|
||||
google-apis-storage_v1 (~> 0.1)
|
||||
google-cloud-core (~> 1.6)
|
||||
googleauth (>= 0.16.2, < 2.a)
|
||||
mini_mime (~> 1.0)
|
||||
google-protobuf (3.18.1)
|
||||
google-protobuf (3.18.1-universal-darwin)
|
||||
google-protobuf (3.18.1-x86_64-linux)
|
||||
google-protobuf (3.19.4)
|
||||
google-protobuf (3.19.4-x86_64-darwin)
|
||||
google-protobuf (3.19.4-x86_64-linux)
|
||||
googleapis-common-protos (1.3.12)
|
||||
google-protobuf (~> 3.14)
|
||||
googleapis-common-protos-types (~> 1.2)
|
||||
grpc (~> 1.27)
|
||||
googleapis-common-protos-types (1.2.0)
|
||||
googleapis-common-protos-types (1.3.0)
|
||||
google-protobuf (~> 3.14)
|
||||
googleauth (0.17.1)
|
||||
faraday (>= 0.17.3, < 2.0)
|
||||
@@ -271,25 +272,25 @@ GEM
|
||||
multi_json (~> 1.11)
|
||||
os (>= 0.9, < 2.0)
|
||||
signet (~> 0.15)
|
||||
groupdate (5.2.2)
|
||||
activesupport (>= 5)
|
||||
grpc (1.41.0)
|
||||
google-protobuf (~> 3.17)
|
||||
groupdate (6.0.1)
|
||||
activesupport (>= 5.2)
|
||||
grpc (1.43.1)
|
||||
google-protobuf (~> 3.18)
|
||||
googleapis-common-protos-types (~> 1.0)
|
||||
grpc (1.41.0-universal-darwin)
|
||||
google-protobuf (~> 3.17)
|
||||
grpc (1.43.1-universal-darwin)
|
||||
google-protobuf (~> 3.18)
|
||||
googleapis-common-protos-types (~> 1.0)
|
||||
grpc (1.41.0-x86_64-linux)
|
||||
google-protobuf (~> 3.17)
|
||||
grpc (1.43.1-x86_64-linux)
|
||||
google-protobuf (~> 3.18)
|
||||
googleapis-common-protos-types (~> 1.0)
|
||||
haikunator (1.1.1)
|
||||
hairtrigger (0.2.24)
|
||||
activerecord (>= 5.0, < 7)
|
||||
hairtrigger (0.2.25)
|
||||
activerecord (>= 5.0, < 8)
|
||||
ruby2ruby (~> 2.4)
|
||||
ruby_parser (~> 3.10)
|
||||
hana (1.3.7)
|
||||
hashdiff (1.0.1)
|
||||
hashie (4.1.0)
|
||||
hashie (5.0.0)
|
||||
hkdf (0.3.0)
|
||||
html2text (0.2.1)
|
||||
nokogiri (~> 1.6)
|
||||
@@ -300,53 +301,55 @@ GEM
|
||||
mime-types (~> 3.0)
|
||||
multi_xml (>= 0.5.2)
|
||||
httpclient (2.8.3)
|
||||
i18n (1.8.11)
|
||||
i18n (1.10.0)
|
||||
concurrent-ruby (~> 1.0)
|
||||
image_processing (1.12.1)
|
||||
image_processing (1.12.2)
|
||||
mini_magick (>= 4.9.5, < 5)
|
||||
ruby-vips (>= 2.0.17, < 3)
|
||||
jbuilder (2.11.2)
|
||||
jbuilder (2.11.5)
|
||||
actionview (>= 5.0.0)
|
||||
activesupport (>= 5.0.0)
|
||||
jmespath (1.4.0)
|
||||
jmespath (1.6.0)
|
||||
jquery-rails (4.4.0)
|
||||
rails-dom-testing (>= 1, < 3)
|
||||
railties (>= 4.2.0)
|
||||
thor (>= 0.14, < 2.0)
|
||||
json (2.5.1)
|
||||
json_refs (0.1.6)
|
||||
json (2.6.1)
|
||||
json_refs (0.1.7)
|
||||
hana
|
||||
json_schemer (0.2.18)
|
||||
json_schemer (0.2.19)
|
||||
ecma-re-validator (~> 0.3)
|
||||
hana (~> 1.3)
|
||||
regexp_parser (~> 2.0)
|
||||
uri_template (~> 0.7)
|
||||
jwt (2.3.0)
|
||||
kaminari (1.2.1)
|
||||
kaminari (1.2.2)
|
||||
activesupport (>= 4.1.0)
|
||||
kaminari-actionview (= 1.2.1)
|
||||
kaminari-activerecord (= 1.2.1)
|
||||
kaminari-core (= 1.2.1)
|
||||
kaminari-actionview (1.2.1)
|
||||
kaminari-actionview (= 1.2.2)
|
||||
kaminari-activerecord (= 1.2.2)
|
||||
kaminari-core (= 1.2.2)
|
||||
kaminari-actionview (1.2.2)
|
||||
actionview
|
||||
kaminari-core (= 1.2.1)
|
||||
kaminari-activerecord (1.2.1)
|
||||
kaminari-core (= 1.2.2)
|
||||
kaminari-activerecord (1.2.2)
|
||||
activerecord
|
||||
kaminari-core (= 1.2.1)
|
||||
kaminari-core (1.2.1)
|
||||
koala (3.0.0)
|
||||
kaminari-core (= 1.2.2)
|
||||
kaminari-core (1.2.2)
|
||||
koala (3.1.0)
|
||||
addressable
|
||||
faraday
|
||||
faraday (< 2)
|
||||
json (>= 1.8)
|
||||
rexml
|
||||
launchy (2.5.0)
|
||||
addressable (~> 2.7)
|
||||
letter_opener (1.7.0)
|
||||
launchy (~> 2.2)
|
||||
line-bot-api (1.22.0)
|
||||
line-bot-api (1.23.0)
|
||||
liquid (5.1.0)
|
||||
listen (3.7.0)
|
||||
listen (3.7.1)
|
||||
rb-fsevent (~> 0.10, >= 0.10.3)
|
||||
rb-inotify (~> 0.9, >= 0.9.10)
|
||||
loofah (2.13.0)
|
||||
loofah (2.14.0)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.5.9)
|
||||
mail (2.7.1)
|
||||
@@ -355,43 +358,43 @@ GEM
|
||||
maxminddb (0.1.22)
|
||||
memoist (0.16.2)
|
||||
method_source (1.0.0)
|
||||
mime-types (3.3.1)
|
||||
mime-types (3.4.1)
|
||||
mime-types-data (~> 3.2015)
|
||||
mime-types-data (3.2021.0901)
|
||||
mime-types-data (3.2022.0105)
|
||||
mini_magick (4.11.0)
|
||||
mini_mime (1.1.2)
|
||||
mini_portile2 (2.5.3)
|
||||
mini_portile2 (2.8.0)
|
||||
minitest (5.15.0)
|
||||
mock_redis (0.29.0)
|
||||
mock_redis (0.30.0)
|
||||
ruby2_keywords
|
||||
momentjs-rails (2.20.1)
|
||||
momentjs-rails (2.29.1.1)
|
||||
railties (>= 3.1)
|
||||
msgpack (1.4.2)
|
||||
msgpack (1.4.5)
|
||||
multi_json (1.15.0)
|
||||
multi_xml (0.6.0)
|
||||
multipart-post (2.1.1)
|
||||
net-http-persistent (4.0.1)
|
||||
connection_pool (~> 2.2)
|
||||
netrc (0.11.0)
|
||||
newrelic_rpm (8.0.0)
|
||||
newrelic_rpm (8.4.0)
|
||||
nio4r (2.5.8)
|
||||
nokogiri (1.11.7)
|
||||
mini_portile2 (~> 2.5.0)
|
||||
nokogiri (1.13.3)
|
||||
mini_portile2 (~> 2.8.0)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.11.7-arm64-darwin)
|
||||
nokogiri (1.13.3-arm64-darwin)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.11.7-x86_64-darwin)
|
||||
nokogiri (1.13.3-x86_64-darwin)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.11.7-x86_64-linux)
|
||||
nokogiri (1.13.3-x86_64-linux)
|
||||
racc (~> 1.4)
|
||||
oauth (0.5.6)
|
||||
oauth (0.5.8)
|
||||
orm_adapter (0.5.0)
|
||||
os (1.1.1)
|
||||
os (1.1.4)
|
||||
parallel (1.21.0)
|
||||
parser (3.0.2.0)
|
||||
parser (3.1.1.0)
|
||||
ast (~> 2.4.1)
|
||||
path_expander (1.1.0)
|
||||
pg (1.2.3)
|
||||
pg (1.3.2)
|
||||
procore-sift (0.16.0)
|
||||
rails (> 4.2.0)
|
||||
pry (0.14.1)
|
||||
@@ -400,57 +403,57 @@ GEM
|
||||
pry-rails (0.3.9)
|
||||
pry (>= 0.10.4)
|
||||
public_suffix (4.0.6)
|
||||
puma (5.5.2)
|
||||
puma (5.6.2)
|
||||
nio4r (~> 2.0)
|
||||
pundit (2.1.1)
|
||||
pundit (2.2.0)
|
||||
activesupport (>= 3.0.0)
|
||||
raabro (1.4.0)
|
||||
racc (1.6.0)
|
||||
rack (2.2.3)
|
||||
rack-attack (6.5.0)
|
||||
rack-attack (6.6.0)
|
||||
rack (>= 1.0, < 3)
|
||||
rack-cors (1.1.1)
|
||||
rack (>= 2.0.0)
|
||||
rack-proxy (0.7.0)
|
||||
rack-proxy (0.7.2)
|
||||
rack
|
||||
rack-test (1.1.0)
|
||||
rack (>= 1.0, < 3)
|
||||
rack-timeout (0.6.0)
|
||||
rails (6.1.4.3)
|
||||
actioncable (= 6.1.4.3)
|
||||
actionmailbox (= 6.1.4.3)
|
||||
actionmailer (= 6.1.4.3)
|
||||
actionpack (= 6.1.4.3)
|
||||
actiontext (= 6.1.4.3)
|
||||
actionview (= 6.1.4.3)
|
||||
activejob (= 6.1.4.3)
|
||||
activemodel (= 6.1.4.3)
|
||||
activerecord (= 6.1.4.3)
|
||||
activestorage (= 6.1.4.3)
|
||||
activesupport (= 6.1.4.3)
|
||||
rails (6.1.4.7)
|
||||
actioncable (= 6.1.4.7)
|
||||
actionmailbox (= 6.1.4.7)
|
||||
actionmailer (= 6.1.4.7)
|
||||
actionpack (= 6.1.4.7)
|
||||
actiontext (= 6.1.4.7)
|
||||
actionview (= 6.1.4.7)
|
||||
activejob (= 6.1.4.7)
|
||||
activemodel (= 6.1.4.7)
|
||||
activerecord (= 6.1.4.7)
|
||||
activestorage (= 6.1.4.7)
|
||||
activesupport (= 6.1.4.7)
|
||||
bundler (>= 1.15.0)
|
||||
railties (= 6.1.4.3)
|
||||
railties (= 6.1.4.7)
|
||||
sprockets-rails (>= 2.0.0)
|
||||
rails-dom-testing (2.0.3)
|
||||
activesupport (>= 4.2.0)
|
||||
nokogiri (>= 1.6)
|
||||
rails-html-sanitizer (1.4.2)
|
||||
loofah (~> 2.3)
|
||||
railties (6.1.4.3)
|
||||
actionpack (= 6.1.4.3)
|
||||
activesupport (= 6.1.4.3)
|
||||
railties (6.1.4.7)
|
||||
actionpack (= 6.1.4.7)
|
||||
activesupport (= 6.1.4.7)
|
||||
method_source
|
||||
rake (>= 0.13)
|
||||
thor (~> 1.0)
|
||||
rainbow (3.0.0)
|
||||
rainbow (3.1.1)
|
||||
rake (13.0.6)
|
||||
rb-fsevent (0.11.0)
|
||||
rb-fsevent (0.11.1)
|
||||
rb-inotify (0.10.1)
|
||||
ffi (~> 1.0)
|
||||
redis (4.4.0)
|
||||
redis (4.6.0)
|
||||
redis-namespace (1.8.1)
|
||||
redis (>= 3.0.4)
|
||||
regexp_parser (2.1.1)
|
||||
regexp_parser (2.2.1)
|
||||
representable (3.1.1)
|
||||
declarative (< 0.1.0)
|
||||
trailblazer-option (>= 0.1.1, < 0.2.0)
|
||||
@@ -465,19 +468,19 @@ GEM
|
||||
netrc (~> 0.8)
|
||||
retriable (3.1.2)
|
||||
rexml (3.2.5)
|
||||
rspec (3.10.0)
|
||||
rspec-core (~> 3.10.0)
|
||||
rspec-expectations (~> 3.10.0)
|
||||
rspec-mocks (~> 3.10.0)
|
||||
rspec-core (3.10.1)
|
||||
rspec-support (~> 3.10.0)
|
||||
rspec-expectations (3.10.1)
|
||||
rspec (3.11.0)
|
||||
rspec-core (~> 3.11.0)
|
||||
rspec-expectations (~> 3.11.0)
|
||||
rspec-mocks (~> 3.11.0)
|
||||
rspec-core (3.11.0)
|
||||
rspec-support (~> 3.11.0)
|
||||
rspec-expectations (3.11.0)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.10.0)
|
||||
rspec-mocks (3.10.2)
|
||||
rspec-support (~> 3.11.0)
|
||||
rspec-mocks (3.11.0)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.10.0)
|
||||
rspec-rails (5.0.2)
|
||||
rspec-support (~> 3.11.0)
|
||||
rspec-rails (5.0.3)
|
||||
actionpack (>= 5.2)
|
||||
activesupport (>= 5.2)
|
||||
railties (>= 5.2)
|
||||
@@ -485,36 +488,36 @@ GEM
|
||||
rspec-expectations (~> 3.10)
|
||||
rspec-mocks (~> 3.10)
|
||||
rspec-support (~> 3.10)
|
||||
rspec-support (3.10.2)
|
||||
rubocop (1.22.1)
|
||||
rspec-support (3.11.0)
|
||||
rubocop (1.25.1)
|
||||
parallel (~> 1.10)
|
||||
parser (>= 3.0.0.0)
|
||||
parser (>= 3.1.0.0)
|
||||
rainbow (>= 2.2.2, < 4.0)
|
||||
regexp_parser (>= 1.8, < 3.0)
|
||||
rexml
|
||||
rubocop-ast (>= 1.12.0, < 2.0)
|
||||
rubocop-ast (>= 1.15.1, < 2.0)
|
||||
ruby-progressbar (~> 1.7)
|
||||
unicode-display_width (>= 1.4.0, < 3.0)
|
||||
rubocop-ast (1.12.0)
|
||||
parser (>= 3.0.1.1)
|
||||
rubocop-performance (1.11.5)
|
||||
rubocop-ast (1.16.0)
|
||||
parser (>= 3.1.1.0)
|
||||
rubocop-performance (1.13.2)
|
||||
rubocop (>= 1.7.0, < 2.0)
|
||||
rubocop-ast (>= 0.4.0)
|
||||
rubocop-rails (2.12.3)
|
||||
rubocop-rails (2.13.2)
|
||||
activesupport (>= 4.2.0)
|
||||
rack (>= 1.1)
|
||||
rubocop (>= 1.7.0, < 2.0)
|
||||
rubocop-rspec (2.5.0)
|
||||
rubocop-rspec (2.8.0)
|
||||
rubocop (~> 1.19)
|
||||
ruby-progressbar (1.11.0)
|
||||
ruby-vips (2.1.3)
|
||||
ruby-vips (2.1.4)
|
||||
ffi (~> 1.12)
|
||||
ruby2_keywords (0.0.5)
|
||||
ruby2ruby (2.4.4)
|
||||
ruby_parser (~> 3.1)
|
||||
sexp_processor (~> 4.6)
|
||||
ruby_parser (3.17.0)
|
||||
sexp_processor (~> 4.15, >= 4.15.1)
|
||||
ruby_parser (3.18.1)
|
||||
sexp_processor (~> 4.16)
|
||||
sassc (2.4.0)
|
||||
ffi (~> 1.9)
|
||||
sassc-rails (2.1.2)
|
||||
@@ -523,30 +526,28 @@ GEM
|
||||
sprockets (> 3.0)
|
||||
sprockets-rails
|
||||
tilt
|
||||
scout_apm (4.1.2)
|
||||
scout_apm (5.1.1)
|
||||
parser
|
||||
seed_dump (3.3.1)
|
||||
activerecord (>= 4)
|
||||
activesupport (>= 4)
|
||||
selectize-rails (0.12.6)
|
||||
semantic_range (3.0.0)
|
||||
sentry-rails (4.7.3)
|
||||
sentry-rails (5.1.0)
|
||||
railties (>= 5.0)
|
||||
sentry-ruby-core (~> 4.7.0)
|
||||
sentry-ruby (4.7.3)
|
||||
sentry-ruby-core (~> 5.1.0)
|
||||
sentry-ruby (5.1.0)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
faraday (>= 1.0)
|
||||
sentry-ruby-core (= 4.7.3)
|
||||
sentry-ruby-core (4.7.3)
|
||||
sentry-ruby-core (= 5.1.0)
|
||||
sentry-ruby-core (5.1.0)
|
||||
concurrent-ruby
|
||||
faraday
|
||||
sentry-sidekiq (4.7.3)
|
||||
sentry-ruby-core (~> 4.7.0)
|
||||
sentry-sidekiq (5.1.0)
|
||||
sentry-ruby-core (~> 5.1.0)
|
||||
sidekiq (>= 3.0)
|
||||
sexp_processor (4.15.3)
|
||||
shoulda-matchers (5.0.0)
|
||||
sexp_processor (4.16.0)
|
||||
shoulda-matchers (5.1.0)
|
||||
activesupport (>= 5.2.0)
|
||||
sidekiq (6.2.2)
|
||||
sidekiq (6.4.1)
|
||||
connection_pool (>= 2.2.2)
|
||||
rack (~> 2.0)
|
||||
redis (>= 4.2.0)
|
||||
@@ -563,7 +564,7 @@ GEM
|
||||
json (>= 1.8, < 3)
|
||||
simplecov-html (~> 0.10.0)
|
||||
simplecov-html (0.10.2)
|
||||
slack-ruby-client (0.17.0)
|
||||
slack-ruby-client (1.0.0)
|
||||
faraday (>= 1.0)
|
||||
faraday_middleware
|
||||
gli
|
||||
@@ -573,7 +574,7 @@ GEM
|
||||
spring-watcher-listen (2.0.1)
|
||||
listen (>= 2.7, < 4.0)
|
||||
spring (>= 1.2, < 3.0)
|
||||
sprockets (4.0.2)
|
||||
sprockets (4.0.3)
|
||||
concurrent-ruby (~> 1.0)
|
||||
rack (> 1, < 3)
|
||||
sprockets-rails (3.4.2)
|
||||
@@ -582,13 +583,13 @@ GEM
|
||||
sprockets (>= 3.0.0)
|
||||
squasher (0.6.2)
|
||||
statsd-ruby (1.5.0)
|
||||
telephone_number (1.4.12)
|
||||
thor (1.1.0)
|
||||
telephone_number (1.4.13)
|
||||
thor (1.2.1)
|
||||
tilt (2.0.10)
|
||||
time_diff (0.3.0)
|
||||
activesupport
|
||||
i18n
|
||||
trailblazer-option (0.1.1)
|
||||
trailblazer-option (0.1.2)
|
||||
twilio-ruby (5.32.0)
|
||||
faraday (~> 1.0.0)
|
||||
jwt (>= 1.5, <= 2.5)
|
||||
@@ -597,7 +598,7 @@ GEM
|
||||
oauth
|
||||
tzinfo (2.0.4)
|
||||
concurrent-ruby (~> 1.0)
|
||||
tzinfo-data (1.2021.3)
|
||||
tzinfo-data (1.2021.5)
|
||||
tzinfo (>= 1.0.0)
|
||||
uber (0.1.0)
|
||||
uglifier (4.2.0)
|
||||
@@ -608,12 +609,12 @@ GEM
|
||||
unicode-display_width (2.1.0)
|
||||
uniform_notifier (1.14.2)
|
||||
uri_template (0.7.0)
|
||||
valid_email2 (4.0.0)
|
||||
valid_email2 (4.0.3)
|
||||
activemodel (>= 3.2)
|
||||
mail (~> 2.5)
|
||||
warden (1.2.9)
|
||||
rack (>= 2.0.9)
|
||||
web-console (4.1.0)
|
||||
web-console (4.2.0)
|
||||
actionview (>= 6.0.0)
|
||||
activemodel (>= 6.0.0)
|
||||
bindex (>= 0.4.0)
|
||||
@@ -635,7 +636,7 @@ GEM
|
||||
websocket-extensions (>= 0.1.0)
|
||||
websocket-extensions (0.1.5)
|
||||
wisper (2.0.0)
|
||||
zeitwerk (2.5.1)
|
||||
zeitwerk (2.5.4)
|
||||
|
||||
PLATFORMS
|
||||
arm64-darwin-20
|
||||
@@ -687,7 +688,7 @@ DEPENDENCIES
|
||||
hairtrigger
|
||||
hashie
|
||||
html2text
|
||||
image_processing
|
||||
image_processing (~> 1.12.2)
|
||||
jbuilder
|
||||
json_refs
|
||||
json_schemer
|
||||
@@ -726,7 +727,7 @@ DEPENDENCIES
|
||||
sentry-ruby
|
||||
sentry-sidekiq
|
||||
shoulda-matchers
|
||||
sidekiq
|
||||
sidekiq (~> 6.4.0)
|
||||
sidekiq-cron
|
||||
simplecov (= 0.17.1)
|
||||
slack-ruby-client
|
||||
@@ -750,4 +751,4 @@ RUBY VERSION
|
||||
ruby 3.0.2p107
|
||||
|
||||
BUNDLED WITH
|
||||
2.2.25
|
||||
2.3.8
|
||||
|
||||
@@ -118,4 +118,4 @@ Thanks goes to all these [wonderful people](https://www.chatwoot.com/docs/contri
|
||||
<a href="https://github.com/chatwoot/chatwoot/graphs/contributors"><img src="https://opencollective.com/chatwoot/contributors.svg?width=890&button=false" /></a>
|
||||
|
||||
|
||||
*Chatwoot* © 2017-2021, Chatwoot Inc - Released under the MIT License.
|
||||
*Chatwoot* © 2017-2022, Chatwoot Inc - Released under the MIT License.
|
||||
|
||||
@@ -52,12 +52,11 @@ class ContactIdentifyAction
|
||||
end
|
||||
|
||||
def update_contact
|
||||
custom_attributes = params[:custom_attributes] ? @contact.custom_attributes.merge(params[:custom_attributes]) : @contact.custom_attributes
|
||||
# blank identifier or email will throw unique index error
|
||||
# TODO: replace reject { |_k, v| v.blank? } with compact_blank when rails is upgraded
|
||||
@contact.update!(params.slice(:name, :email, :identifier, :phone_number).reject do |_k, v|
|
||||
v.blank?
|
||||
end.merge({ custom_attributes: custom_attributes }))
|
||||
end.merge({ custom_attributes: custom_attributes, additional_attributes: additional_attributes }))
|
||||
ContactAvatarJob.perform_later(@contact, params[:avatar_url]) if params[:avatar_url].present?
|
||||
end
|
||||
|
||||
@@ -68,4 +67,16 @@ class ContactIdentifyAction
|
||||
mergee_contact: merge_contact
|
||||
).perform
|
||||
end
|
||||
|
||||
def custom_attributes
|
||||
params[:custom_attributes] ? @contact.custom_attributes.deep_merge(params[:custom_attributes].stringify_keys) : @contact.custom_attributes
|
||||
end
|
||||
|
||||
def additional_attributes
|
||||
if params[:additional_attributes]
|
||||
@contact.additional_attributes.deep_merge(params[:additional_attributes].stringify_keys)
|
||||
else
|
||||
@contact.additional_attributes
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
class AccountBuilder
|
||||
include CustomExceptions::Account
|
||||
pattr_initialize [:account_name!, :email!, :confirmed, :user, :user_full_name, :user_password]
|
||||
pattr_initialize [:account_name!, :email!, :confirmed, :user, :user_full_name, :user_password, :super_admin]
|
||||
|
||||
def perform
|
||||
if @user.nil?
|
||||
@@ -65,6 +65,7 @@ class AccountBuilder
|
||||
password: user_password,
|
||||
password_confirmation: user_password,
|
||||
name: @user_full_name)
|
||||
@user.type = 'SuperAdmin' if @super_admin
|
||||
@user.confirm if @confirmed
|
||||
@user.save!
|
||||
end
|
||||
|
||||
@@ -4,7 +4,7 @@ class ContactInboxBuilder
|
||||
def perform
|
||||
@contact = Contact.find(contact_id)
|
||||
@inbox = @contact.account.inboxes.find(inbox_id)
|
||||
return unless ['Channel::TwilioSms', 'Channel::Email', 'Channel::Api', 'Channel::Whatsapp'].include? @inbox.channel_type
|
||||
return unless ['Channel::TwilioSms', 'Channel::Sms', 'Channel::Email', 'Channel::Api', 'Channel::Whatsapp'].include? @inbox.channel_type
|
||||
|
||||
source_id = @source_id || generate_source_id
|
||||
create_contact_inbox(source_id) if source_id.present?
|
||||
@@ -13,12 +13,18 @@ class ContactInboxBuilder
|
||||
private
|
||||
|
||||
def generate_source_id
|
||||
return twilio_source_id if @inbox.channel_type == 'Channel::TwilioSms'
|
||||
return wa_source_id if @inbox.channel_type == 'Channel::Whatsapp'
|
||||
return @contact.email if @inbox.channel_type == 'Channel::Email'
|
||||
return SecureRandom.uuid if @inbox.channel_type == 'Channel::Api'
|
||||
|
||||
nil
|
||||
case @inbox.channel_type
|
||||
when 'Channel::TwilioSms'
|
||||
twilio_source_id
|
||||
when 'Channel::Whatsapp'
|
||||
wa_source_id
|
||||
when 'Channel::Email'
|
||||
@contact.email
|
||||
when 'Channel::Sms'
|
||||
@contact.phone_number
|
||||
when 'Channel::Api'
|
||||
SecureRandom.uuid
|
||||
end
|
||||
end
|
||||
|
||||
def wa_source_id
|
||||
|
||||
@@ -8,9 +8,11 @@ class Messages::MessageBuilder
|
||||
@conversation = conversation
|
||||
@user = user
|
||||
@message_type = params[:message_type] || 'outgoing'
|
||||
@items = params.to_unsafe_h&.dig(:content_attributes, :items)
|
||||
@attachments = params[:attachments]
|
||||
return unless params.instance_of?(ActionController::Parameters)
|
||||
|
||||
@in_reply_to = params.to_unsafe_h&.dig(:content_attributes, :in_reply_to)
|
||||
@items = params.to_unsafe_h&.dig(:content_attributes, :items)
|
||||
end
|
||||
|
||||
def perform
|
||||
@@ -27,11 +29,12 @@ class Messages::MessageBuilder
|
||||
return if @attachments.blank?
|
||||
|
||||
@attachments.each do |uploaded_attachment|
|
||||
@message.attachments.build(
|
||||
attachment = @message.attachments.build(
|
||||
account_id: @message.account_id,
|
||||
file_type: file_type(uploaded_attachment&.content_type),
|
||||
file: uploaded_attachment
|
||||
)
|
||||
|
||||
attachment.file_type = file_type(uploaded_attachment&.content_type) if uploaded_attachment.is_a?(ActionDispatch::Http::UploadedFile)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
class Messages::Messenger::MessageBuilder
|
||||
include ::FileTypeHelper
|
||||
|
||||
def process_attachment(attachment)
|
||||
return if attachment['type'].to_sym == :template
|
||||
|
||||
attachment_obj = @message.attachments.new(attachment_params(attachment).except(:remote_file_url))
|
||||
attachment_obj.save!
|
||||
attach_file(attachment_obj, attachment_params(attachment)[:remote_file_url]) if attachment_params(attachment)[:remote_file_url]
|
||||
fetch_story_link(attachment_obj) if attachment_obj.file_type == 'story_mention'
|
||||
update_attachment_file_type(attachment_obj)
|
||||
end
|
||||
|
||||
def attach_file(attachment, file_url)
|
||||
@@ -22,7 +26,7 @@ class Messages::Messenger::MessageBuilder
|
||||
file_type = attachment['type'].to_sym
|
||||
params = { file_type: file_type, account_id: @message.account_id }
|
||||
|
||||
if [:image, :file, :audio, :video].include? file_type
|
||||
if [:image, :file, :audio, :video, :share, :story_mention].include? file_type
|
||||
params.merge!(file_type_params(attachment))
|
||||
elsif file_type == :location
|
||||
params.merge!(location_params(attachment))
|
||||
@@ -39,4 +43,31 @@ class Messages::Messenger::MessageBuilder
|
||||
remote_file_url: attachment['payload']['url']
|
||||
}
|
||||
end
|
||||
|
||||
def update_attachment_file_type(attachment)
|
||||
return unless attachment.file_type == 'share' || attachment.file_type == 'story_mention'
|
||||
|
||||
attachment.file_type = file_type(attachment.file&.content_type)
|
||||
attachment.save!
|
||||
end
|
||||
|
||||
def fetch_story_link(attachment)
|
||||
message = attachment.message
|
||||
begin
|
||||
k = Koala::Facebook::API.new(@inbox.channel.page_access_token) if @inbox.facebook?
|
||||
result = k.get_object(message.source_id, fields: %w[story from]) || {}
|
||||
rescue Koala::Facebook::AuthenticationError
|
||||
@inbox.channel.authorization_error!
|
||||
raise
|
||||
rescue StandardError => e
|
||||
result = {}
|
||||
Sentry.capture_exception(e)
|
||||
end
|
||||
story_id = result['story']['mention']['id']
|
||||
story_sender = result['from']['username']
|
||||
message.content_attributes[:story_sender] = story_sender
|
||||
message.content_attributes[:story_id] = story_id
|
||||
message.content = I18n.t('conversations.messages.instagram_story_content', story_sender: story_sender)
|
||||
message.save!
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2,9 +2,14 @@ class V2::ReportBuilder
|
||||
include DateRangeHelper
|
||||
attr_reader :account, :params
|
||||
|
||||
DEFAULT_GROUP_BY = 'day'.freeze
|
||||
|
||||
def initialize(account, params)
|
||||
@account = account
|
||||
@params = params
|
||||
|
||||
timezone_offset = (params[:timezone_offset] || 0).to_f
|
||||
@timezone = ActiveSupport::TimeZone[timezone_offset]&.name
|
||||
end
|
||||
|
||||
def timeseries
|
||||
@@ -62,56 +67,58 @@ class V2::ReportBuilder
|
||||
@team ||= account.teams.find(params[:id])
|
||||
end
|
||||
|
||||
def get_grouped_values(object_scope)
|
||||
object_scope.group_by_period(
|
||||
params[:group_by] || DEFAULT_GROUP_BY,
|
||||
:created_at,
|
||||
default_value: 0,
|
||||
range: range,
|
||||
permit: %w[day week month year],
|
||||
time_zone: @timezone
|
||||
)
|
||||
end
|
||||
|
||||
def conversations_count
|
||||
scope.conversations
|
||||
.group_by_day(:created_at, range: range, default_value: 0)
|
||||
.count
|
||||
(get_grouped_values scope.conversations).count
|
||||
end
|
||||
|
||||
def incoming_messages_count
|
||||
scope.messages.incoming.unscope(:order)
|
||||
.group_by_day(:created_at, range: range, default_value: 0)
|
||||
.count
|
||||
(get_grouped_values scope.messages.incoming.unscope(:order)).count
|
||||
end
|
||||
|
||||
def outgoing_messages_count
|
||||
scope.messages.outgoing.unscope(:order)
|
||||
.group_by_day(:created_at, range: range, default_value: 0)
|
||||
.count
|
||||
(get_grouped_values scope.messages.outgoing.unscope(:order)).count
|
||||
end
|
||||
|
||||
def resolutions_count
|
||||
scope.conversations
|
||||
.resolved
|
||||
.group_by_day(:created_at, range: range, default_value: 0)
|
||||
.count
|
||||
(get_grouped_values scope.conversations.resolved).count
|
||||
end
|
||||
|
||||
def avg_first_response_time
|
||||
scope.events
|
||||
.where(name: 'first_response')
|
||||
.group_by_day(:created_at, range: range, default_value: 0)
|
||||
.average(:value)
|
||||
(get_grouped_values scope.reporting_events.where(name: 'first_response')).average(:value)
|
||||
end
|
||||
|
||||
def avg_resolution_time
|
||||
scope.events.where(name: 'conversation_resolved')
|
||||
.group_by_day(:created_at, range: range, default_value: 0)
|
||||
.average(:value)
|
||||
(get_grouped_values scope.reporting_events.where(name: 'conversation_resolved')).average(:value)
|
||||
end
|
||||
|
||||
# Taking average of average is not too accurate
|
||||
# https://en.wikipedia.org/wiki/Simpson's_paradox
|
||||
# TODO: Will optimize this later
|
||||
def avg_resolution_time_summary
|
||||
return 0 if avg_resolution_time.values.empty?
|
||||
avg_rt = scope.reporting_events
|
||||
.where(name: 'conversation_resolved', created_at: range)
|
||||
.average(:value)
|
||||
|
||||
(avg_resolution_time.values.sum / avg_resolution_time.values.length)
|
||||
return 0 if avg_rt.blank?
|
||||
|
||||
avg_rt
|
||||
end
|
||||
|
||||
def avg_first_response_time_summary
|
||||
return 0 if avg_first_response_time.values.empty?
|
||||
avg_frt = scope.reporting_events
|
||||
.where(name: 'first_response', created_at: range)
|
||||
.average(:value)
|
||||
|
||||
(avg_first_response_time.values.sum / avg_first_response_time.values.length)
|
||||
return 0 if avg_frt.blank?
|
||||
|
||||
avg_frt
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
class RoomChannel < ApplicationCable::Channel
|
||||
def subscribed
|
||||
# TODO: should we only do ensure stream if current account is present?
|
||||
# for now going ahead with guard clauses in update_subscription and broadcast_presence
|
||||
|
||||
ensure_stream
|
||||
current_user
|
||||
current_account
|
||||
@@ -15,6 +18,8 @@ class RoomChannel < ApplicationCable::Channel
|
||||
private
|
||||
|
||||
def broadcast_presence
|
||||
return if @current_account.blank?
|
||||
|
||||
data = { account_id: @current_account.id, users: ::OnlineStatusTracker.get_available_users(@current_account.id) }
|
||||
data[:contacts] = ::OnlineStatusTracker.get_available_contacts(@current_account.id) if @current_user.is_a? User
|
||||
ActionCable.server.broadcast(@pubsub_token, { event: 'presence.update', data: data })
|
||||
@@ -26,6 +31,8 @@ class RoomChannel < ApplicationCable::Channel
|
||||
end
|
||||
|
||||
def update_subscription
|
||||
return if @current_account.blank?
|
||||
|
||||
::OnlineStatusTracker.update_presence(@current_account.id, @current_user.class.name, @current_user.id)
|
||||
end
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ class Api::V1::Accounts::AgentsController < Api::V1::Accounts::BaseController
|
||||
end
|
||||
|
||||
def agents
|
||||
@agents ||= Current.account.users.order_by_full_name.includes({ avatar_attachment: [:blob] })
|
||||
@agents ||= Current.account.users.order_by_full_name.includes(:account_users, { avatar_attachment: [:blob] })
|
||||
end
|
||||
|
||||
def validate_limit
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
class Api::V1::Accounts::AutomationRulesController < Api::V1::Accounts::BaseController
|
||||
before_action :check_authorization
|
||||
before_action :fetch_automation_rule, only: [:show, :update, :destroy, :clone]
|
||||
|
||||
def index
|
||||
@automation_rules = Current.account.automation_rules
|
||||
end
|
||||
|
||||
def create
|
||||
@automation_rule = Current.account.automation_rules.create(automation_rules_permit)
|
||||
end
|
||||
|
||||
def show; end
|
||||
|
||||
def update
|
||||
@automation_rule.update(automation_rules_permit)
|
||||
end
|
||||
|
||||
def destroy
|
||||
@automation_rule.destroy!
|
||||
head :ok
|
||||
end
|
||||
|
||||
def clone
|
||||
automation_rule = Current.account.automation_rules.find_by(id: params[:automation_rule_id])
|
||||
new_rule = automation_rule.dup
|
||||
new_rule.save
|
||||
@automation_rule = new_rule
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def automation_rules_permit
|
||||
params.permit(
|
||||
:name, :description, :event_name, :account_id, :active,
|
||||
conditions: [:attribute_key, :filter_operator, :query_operator, { values: [] }],
|
||||
actions: [:action_name, { action_params: [{}] }]
|
||||
)
|
||||
end
|
||||
|
||||
def fetch_automation_rule
|
||||
@automation_rule = Current.account.automation_rules.find_by(id: params[:id])
|
||||
end
|
||||
end
|
||||
@@ -1,32 +1,6 @@
|
||||
class Api::V1::Accounts::BaseController < Api::BaseController
|
||||
include SwitchLocale
|
||||
include EnsureCurrentAccountHelper
|
||||
before_action :current_account
|
||||
around_action :switch_locale_using_account_locale
|
||||
|
||||
private
|
||||
|
||||
def current_account
|
||||
@current_account ||= ensure_current_account
|
||||
Current.account = @current_account
|
||||
end
|
||||
|
||||
def ensure_current_account
|
||||
account = Account.find(params[:account_id])
|
||||
if current_user
|
||||
account_accessible_for_user?(account)
|
||||
elsif @resource.is_a?(AgentBot)
|
||||
account_accessible_for_bot?(account)
|
||||
end
|
||||
account
|
||||
end
|
||||
|
||||
def account_accessible_for_user?(account)
|
||||
@current_account_user = account.account_users.find_by(user_id: current_user.id)
|
||||
Current.account_user = @current_account_user
|
||||
render_unauthorized('You are not authorized to access this account') unless @current_account_user
|
||||
end
|
||||
|
||||
def account_accessible_for_bot?(account)
|
||||
render_unauthorized('You are not authorized to access this account') unless @resource.agent_bot_inboxes.find_by(account_id: account.id)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
class Api::V1::Accounts::BulkActionsController < Api::V1::Accounts::BaseController
|
||||
before_action :type_matches?
|
||||
|
||||
def create
|
||||
if type_matches?
|
||||
::BulkActionsJob.perform_later(
|
||||
account: @current_account,
|
||||
user: current_user,
|
||||
params: permitted_params
|
||||
)
|
||||
head :ok
|
||||
else
|
||||
render json: { success: false }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def type_matches?
|
||||
['Conversation'].include?(params[:type])
|
||||
end
|
||||
|
||||
def permitted_params
|
||||
params.permit(:type, ids: [], fields: [:status, :assignee_id, :team_id], labels: [add: [], remove: []])
|
||||
end
|
||||
end
|
||||
@@ -18,7 +18,7 @@ class Api::V1::Accounts::CampaignsController < Api::V1::Accounts::BaseController
|
||||
def show; end
|
||||
|
||||
def update
|
||||
@campaign.update(campaign_params)
|
||||
@campaign.update!(campaign_params)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
class Api::V1::Accounts::Conversations::BaseController < Api::V1::Accounts::BaseController
|
||||
include EnsureCurrentAccountHelper
|
||||
before_action :conversation
|
||||
|
||||
private
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
class Api::V1::Accounts::Conversations::DirectUploadsController < ActiveStorage::DirectUploadsController
|
||||
include EnsureCurrentAccountHelper
|
||||
before_action :current_account
|
||||
before_action :conversation
|
||||
|
||||
def create
|
||||
return if @conversation.nil? || @current_account.nil?
|
||||
|
||||
super
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def conversation
|
||||
@conversation ||= Current.account.conversations.find_by(display_id: params[:conversation_id])
|
||||
end
|
||||
end
|
||||
@@ -13,7 +13,7 @@ class Api::V1::Accounts::Conversations::MessagesController < Api::V1::Accounts::
|
||||
|
||||
def destroy
|
||||
ActiveRecord::Base.transaction do
|
||||
message.update!(content: I18n.t('conversations.messages.deleted'), deleted: true)
|
||||
message.update!(content: I18n.t('conversations.messages.deleted'), content_attributes: { deleted: true })
|
||||
message.attachments.destroy_all
|
||||
end
|
||||
end
|
||||
|
||||
@@ -61,6 +61,7 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
|
||||
else
|
||||
@status = @conversation.toggle_status
|
||||
end
|
||||
assign_conversation if @conversation.status == 'open' && Current.user.is_a?(User) && Current.user&.agent?
|
||||
end
|
||||
|
||||
def toggle_typing_status
|
||||
@@ -93,6 +94,11 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
|
||||
@conversation.snoozed_until = parse_date_time(params[:snoozed_until].to_s) if params[:snoozed_until]
|
||||
end
|
||||
|
||||
def assign_conversation
|
||||
@agent = Current.account.users.find(current_user.id)
|
||||
@conversation.update_assignee(@agent)
|
||||
end
|
||||
|
||||
def trigger_typing_event(event, is_private)
|
||||
user = current_user.presence || @resource
|
||||
Rails.configuration.dispatcher.dispatch(event, Time.zone.now, conversation: @conversation, user: user, is_private: is_private)
|
||||
|
||||
@@ -30,8 +30,7 @@ class Api::V1::Accounts::CsatSurveyResponsesController < Api::V1::Accounts::Base
|
||||
def set_csat_survey_responses
|
||||
@csat_survey_responses = filtrate(
|
||||
Current.account.csat_survey_responses.includes([:conversation, :assigned_agent, :contact])
|
||||
)
|
||||
@csat_survey_responses = @csat_survey_responses.where(created_at: range) if range.present?
|
||||
).filter_by_created_at(range).filter_by_assigned_agent_id(params[:user_ids])
|
||||
end
|
||||
|
||||
def set_current_page_surveys
|
||||
|
||||
@@ -2,6 +2,7 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
||||
include Api::V1::InboxesHelper
|
||||
before_action :fetch_inbox, except: [:index, :create]
|
||||
before_action :fetch_agent_bot, only: [:set_agent_bot]
|
||||
before_action :validate_limit, only: [:create]
|
||||
# we are already handling the authorization in fetch inbox
|
||||
before_action :check_authorization, except: [:show]
|
||||
|
||||
@@ -47,7 +48,10 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
||||
# Inbox update doesn't necessarily need channel attributes
|
||||
return if permitted_params(channel_attributes)[:channel].blank?
|
||||
|
||||
validate_email_channel(channel_attributes) if @inbox.inbox_type == 'Email'
|
||||
if @inbox.inbox_type == 'Email'
|
||||
validate_email_channel(channel_attributes)
|
||||
@inbox.channel.reauthorized!
|
||||
end
|
||||
|
||||
@inbox.channel.update!(permitted_params(channel_attributes)[:channel])
|
||||
update_channel_feature_flags
|
||||
@@ -91,20 +95,9 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
||||
end
|
||||
|
||||
def create_channel
|
||||
case permitted_params[:channel][:type]
|
||||
when 'web_widget'
|
||||
Current.account.web_widgets.create!(permitted_params(Channel::WebWidget::EDITABLE_ATTRS)[:channel].except(:type))
|
||||
when 'api'
|
||||
Current.account.api_channels.create!(permitted_params(Channel::Api::EDITABLE_ATTRS)[:channel].except(:type))
|
||||
when 'email'
|
||||
Current.account.email_channels.create!(permitted_params(Channel::Email::EDITABLE_ATTRS)[:channel].except(:type))
|
||||
when 'line'
|
||||
Current.account.line_channels.create!(permitted_params(Channel::Line::EDITABLE_ATTRS)[:channel].except(:type))
|
||||
when 'telegram'
|
||||
Current.account.telegram_channels.create!(permitted_params(Channel::Telegram::EDITABLE_ATTRS)[:channel].except(:type))
|
||||
when 'whatsapp'
|
||||
Current.account.whatsapp_channels.create!(permitted_params(Channel::Whatsapp::EDITABLE_ATTRS)[:channel].except(:type))
|
||||
end
|
||||
return unless %w[web_widget api email line telegram whatsapp sms].include?(permitted_params[:channel][:type])
|
||||
|
||||
account_channels_method.create!(permitted_params(channel_type_from_params::EDITABLE_ATTRS)[:channel].except(:type))
|
||||
end
|
||||
|
||||
def update_channel_feature_flags
|
||||
@@ -118,16 +111,46 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
||||
def permitted_params(channel_attributes = [])
|
||||
params.permit(
|
||||
:name, :avatar, :greeting_enabled, :greeting_message, :enable_email_collect, :csat_survey_enabled,
|
||||
:enable_auto_assignment, :working_hours_enabled, :out_of_office_message, :timezone,
|
||||
:enable_auto_assignment, :working_hours_enabled, :out_of_office_message, :timezone, :allow_messages_after_resolved,
|
||||
channel: [:type, *channel_attributes]
|
||||
)
|
||||
end
|
||||
|
||||
def channel_type_from_params
|
||||
{
|
||||
'web_widget' => Channel::WebWidget,
|
||||
'api' => Channel::Api,
|
||||
'email' => Channel::Email,
|
||||
'line' => Channel::Line,
|
||||
'telegram' => Channel::Telegram,
|
||||
'whatsapp' => Channel::Whatsapp,
|
||||
'sms' => Channel::Sms
|
||||
}[permitted_params[:channel][:type]]
|
||||
end
|
||||
|
||||
def account_channels_method
|
||||
{
|
||||
'web_widget' => Current.account.web_widgets,
|
||||
'api' => Current.account.api_channels,
|
||||
'email' => Current.account.email_channels,
|
||||
'line' => Current.account.line_channels,
|
||||
'telegram' => Current.account.telegram_channels,
|
||||
'whatsapp' => Current.account.whatsapp_channels,
|
||||
'sms' => Current.account.sms_channels
|
||||
}[permitted_params[:channel][:type]]
|
||||
end
|
||||
|
||||
def get_channel_attributes(channel_type)
|
||||
if channel_type.constantize.const_defined?('EDITABLE_ATTRS')
|
||||
if channel_type.constantize.const_defined?(:EDITABLE_ATTRS)
|
||||
channel_type.constantize::EDITABLE_ATTRS.presence
|
||||
else
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
def validate_limit
|
||||
return unless Current.account.inboxes.count >= Current.account.usage_limits[:inboxes]
|
||||
|
||||
render_payment_required('Account limit exceeded. Upgrade to a higher plan')
|
||||
end
|
||||
end
|
||||
|
||||
@@ -4,6 +4,7 @@ class Api::V1::AccountsController < Api::BaseController
|
||||
skip_before_action :authenticate_user!, :set_current_user, :handle_with_exception,
|
||||
only: [:create], raise: false
|
||||
before_action :check_signup_enabled, only: [:create]
|
||||
before_action :validate_captcha, only: [:create]
|
||||
before_action :fetch_account, except: [:create]
|
||||
before_action :check_authorization, except: [:create]
|
||||
|
||||
@@ -55,7 +56,11 @@ class Api::V1::AccountsController < Api::BaseController
|
||||
end
|
||||
|
||||
def check_signup_enabled
|
||||
raise ActionController::RoutingError, 'Not Found' if GlobalConfig.get_value('ENABLE_ACCOUNT_SIGNUP') == 'false'
|
||||
raise ActionController::RoutingError, 'Not Found' if GlobalConfigService.load('ENABLE_ACCOUNT_SIGNUP', 'false') == 'false'
|
||||
end
|
||||
|
||||
def validate_captcha
|
||||
raise ActionController::InvalidAuthenticityToken, 'Invalid Captcha' unless ChatwootCaptcha.new(params[:h_captcha_client_response]).valid?
|
||||
end
|
||||
|
||||
def pundit_user
|
||||
|
||||
@@ -7,6 +7,12 @@ class Api::V1::NotificationSubscriptionsController < Api::BaseController
|
||||
render json: notification_subscription
|
||||
end
|
||||
|
||||
def destroy
|
||||
notification_subscription = NotificationSubscription.where(["subscription_attributes->>'push_token' = ?", params[:push_token]]).first
|
||||
notification_subscription.destroy
|
||||
head :ok
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_user
|
||||
|
||||
@@ -38,6 +38,7 @@ class Api::V1::ProfilesController < Api::BaseController
|
||||
:name,
|
||||
:display_name,
|
||||
:avatar,
|
||||
:message_signature,
|
||||
ui_settings: {}
|
||||
)
|
||||
end
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
class Api::V1::Widget::BaseController < ApplicationController
|
||||
include SwitchLocale
|
||||
include WebsiteTokenHelper
|
||||
|
||||
before_action :set_web_widget
|
||||
before_action :set_contact
|
||||
@@ -19,23 +20,6 @@ class Api::V1::Widget::BaseController < ApplicationController
|
||||
@conversation ||= conversations.last
|
||||
end
|
||||
|
||||
def auth_token_params
|
||||
@auth_token_params ||= ::Widget::TokenService.new(token: request.headers['X-Auth-Token']).decode_token
|
||||
end
|
||||
|
||||
def set_web_widget
|
||||
@web_widget = ::Channel::WebWidget.find_by!(website_token: permitted_params[:website_token])
|
||||
@current_account = @web_widget.account
|
||||
end
|
||||
|
||||
def set_contact
|
||||
@contact_inbox = @web_widget.inbox.contact_inboxes.find_by(
|
||||
source_id: auth_token_params[:source_id]
|
||||
)
|
||||
@contact = @contact_inbox&.contact
|
||||
raise ActiveRecord::RecordNotFound unless @contact
|
||||
end
|
||||
|
||||
def create_conversation
|
||||
::Conversation.create!(conversation_params)
|
||||
end
|
||||
@@ -94,10 +78,6 @@ class Api::V1::Widget::BaseController < ApplicationController
|
||||
{ timestamp: permitted_params[:message][:timestamp] }
|
||||
end
|
||||
|
||||
def permitted_params
|
||||
params.permit(:website_token)
|
||||
end
|
||||
|
||||
def message_params
|
||||
{
|
||||
account_id: conversation.account_id,
|
||||
|
||||
@@ -46,6 +46,7 @@ class Api::V1::Widget::ContactsController < Api::V1::Widget::BaseController
|
||||
end
|
||||
|
||||
def permitted_params
|
||||
params.permit(:website_token, :identifier, :identifier_hash, :email, :name, :avatar_url, :phone_number, custom_attributes: {})
|
||||
params.permit(:website_token, :identifier, :identifier_hash, :email, :name, :avatar_url, :phone_number, custom_attributes: {},
|
||||
additional_attributes: {})
|
||||
end
|
||||
end
|
||||
|
||||
@@ -44,6 +44,15 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
|
||||
head :ok
|
||||
end
|
||||
|
||||
def toggle_status
|
||||
head :not_found && return if conversation.nil?
|
||||
unless conversation.resolved?
|
||||
conversation.status = :resolved
|
||||
conversation.save
|
||||
end
|
||||
head :ok
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def trigger_typing_event(event)
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
class Api::V1::Widget::DirectUploadsController < ActiveStorage::DirectUploadsController
|
||||
include WebsiteTokenHelper
|
||||
before_action :set_web_widget
|
||||
before_action :set_contact
|
||||
|
||||
def create
|
||||
return if @contact.nil? || @current_account.nil?
|
||||
|
||||
super
|
||||
end
|
||||
end
|
||||
@@ -29,11 +29,12 @@ class Api::V1::Widget::MessagesController < Api::V1::Widget::BaseController
|
||||
return if params[:message][:attachments].blank?
|
||||
|
||||
params[:message][:attachments].each do |uploaded_attachment|
|
||||
@message.attachments.new(
|
||||
attachment = @message.attachments.new(
|
||||
account_id: @message.account_id,
|
||||
file_type: helpers.file_type(uploaded_attachment&.content_type),
|
||||
file: uploaded_attachment
|
||||
)
|
||||
|
||||
attachment.file_type = helpers.file_type(uploaded_attachment&.content_type) if uploaded_attachment.is_a?(ActionDispatch::Http::UploadedFile)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -41,12 +41,23 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
|
||||
raise Pundit::NotAuthorizedError unless Current.account_user.administrator?
|
||||
end
|
||||
|
||||
def summary_params
|
||||
def current_summary_params
|
||||
{
|
||||
type: params[:type].to_sym,
|
||||
since: params[:since],
|
||||
until: params[:until],
|
||||
id: params[:id]
|
||||
id: params[:id],
|
||||
since: range[:current][:since],
|
||||
until: range[:current][:until],
|
||||
group_by: params[:group_by]
|
||||
}
|
||||
end
|
||||
|
||||
def previous_summary_params
|
||||
{
|
||||
type: params[:type].to_sym,
|
||||
id: params[:id],
|
||||
since: range[:previous][:since],
|
||||
until: range[:previous][:until],
|
||||
group_by: params[:group_by]
|
||||
}
|
||||
end
|
||||
|
||||
@@ -56,12 +67,28 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
|
||||
type: params[:type].to_sym,
|
||||
since: params[:since],
|
||||
until: params[:until],
|
||||
id: params[:id]
|
||||
id: params[:id],
|
||||
group_by: params[:group_by],
|
||||
timezone_offset: params[:timezone_offset]
|
||||
}
|
||||
end
|
||||
|
||||
def range
|
||||
{
|
||||
current: {
|
||||
since: params[:since],
|
||||
until: params[:until]
|
||||
},
|
||||
previous: {
|
||||
since: (params[:since].to_i - (params[:until].to_i - params[:since].to_i)).to_s,
|
||||
until: params[:since]
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def summary_metrics
|
||||
builder = V2::ReportBuilder.new(Current.account, summary_params)
|
||||
builder.summary
|
||||
summary = V2::ReportBuilder.new(Current.account, current_summary_params).summary
|
||||
summary[:previous] = V2::ReportBuilder.new(Current.account, previous_summary_params).summary
|
||||
summary
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
module EnsureCurrentAccountHelper
|
||||
private
|
||||
|
||||
def current_account
|
||||
@current_account ||= ensure_current_account
|
||||
Current.account = @current_account
|
||||
end
|
||||
|
||||
def ensure_current_account
|
||||
account = Account.find(params[:account_id])
|
||||
if current_user
|
||||
account_accessible_for_user?(account)
|
||||
elsif @resource.is_a?(AgentBot)
|
||||
account_accessible_for_bot?(account)
|
||||
end
|
||||
account
|
||||
end
|
||||
|
||||
def account_accessible_for_user?(account)
|
||||
@current_account_user = account.account_users.find_by(user_id: current_user.id)
|
||||
Current.account_user = @current_account_user
|
||||
render_unauthorized('You are not authorized to access this account') unless @current_account_user
|
||||
end
|
||||
|
||||
def account_accessible_for_bot?(account)
|
||||
render_unauthorized('You are not authorized to access this account') unless @resource.agent_bot_inboxes.find_by(account_id: account.id)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,24 @@
|
||||
module WebsiteTokenHelper
|
||||
def auth_token_params
|
||||
@auth_token_params ||= ::Widget::TokenService.new(token: request.headers['X-Auth-Token']).decode_token
|
||||
end
|
||||
|
||||
def set_web_widget
|
||||
@web_widget = ::Channel::WebWidget.find_by!(website_token: permitted_params[:website_token])
|
||||
@current_account = @web_widget.account
|
||||
end
|
||||
|
||||
def set_contact
|
||||
@contact_inbox = @web_widget.inbox.contact_inboxes.find_by(
|
||||
source_id: auth_token_params[:source_id]
|
||||
)
|
||||
@contact = @contact_inbox&.contact
|
||||
raise ActiveRecord::RecordNotFound unless @contact
|
||||
|
||||
Current.contact = @contact
|
||||
end
|
||||
|
||||
def permitted_params
|
||||
params.permit(:website_token)
|
||||
end
|
||||
end
|
||||
@@ -25,7 +25,11 @@ class DashboardController < ActionController::Base
|
||||
'API_CHANNEL_NAME',
|
||||
'API_CHANNEL_THUMBNAIL',
|
||||
'ANALYTICS_TOKEN',
|
||||
'ANALYTICS_HOST'
|
||||
'ANALYTICS_HOST',
|
||||
'DIRECT_UPLOADS_ENABLED',
|
||||
'HCAPTCHA_SITE_KEY',
|
||||
'LOGOUT_REDIRECT_LINK',
|
||||
'DISABLE_USER_PROFILE_UPDATE'
|
||||
).merge(app_config)
|
||||
end
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ class Installation::OnboardingController < ApplicationController
|
||||
user_full_name: onboarding_params.dig(:user, :name),
|
||||
email: onboarding_params.dig(:user, :email),
|
||||
user_password: params.dig(:user, :password),
|
||||
super_admin: true,
|
||||
confirmed: true
|
||||
).perform
|
||||
rescue StandardError => e
|
||||
|
||||
@@ -9,11 +9,12 @@ class Platform::Api::V1::UsersController < PlatformController
|
||||
@resource = (User.find_by(email: user_params[:email]) || User.new(user_params))
|
||||
@resource.save!
|
||||
@resource.confirm
|
||||
@platform_app.platform_app_permissibles.find_or_create_by(permissible: @resource)
|
||||
@platform_app.platform_app_permissibles.find_or_create_by!(permissible: @resource)
|
||||
end
|
||||
|
||||
def login
|
||||
render json: { url: "#{ENV['FRONTEND_URL']}/app/login?email=#{@resource.email}&sso_auth_token=#{@resource.generate_sso_auth_token}" }
|
||||
encoded_email = ERB::Util.url_encode(@resource.email)
|
||||
render json: { url: "#{ENV['FRONTEND_URL']}/app/login?email=#{encoded_email}&sso_auth_token=#{@resource.generate_sso_auth_token}" }
|
||||
end
|
||||
|
||||
def show; end
|
||||
|
||||
@@ -33,11 +33,11 @@ class SuperAdmin::AccountsController < SuperAdmin::ApplicationController
|
||||
# empty values into nil values. It uses other APIs such as `resource_class`
|
||||
# and `dashboard`:
|
||||
#
|
||||
# def resource_params
|
||||
# params.require(resource_class.model_name.param_key).
|
||||
# permit(dashboard.permitted_attributes).
|
||||
# transform_values { |value| value == "" ? nil : value }
|
||||
# end
|
||||
def resource_params
|
||||
permitted_params = super
|
||||
permitted_params[:limits] = permitted_params[:limits].to_h.compact
|
||||
permitted_params
|
||||
end
|
||||
|
||||
# See https://administrate-prototype.herokuapp.com/customizing_controller_actions
|
||||
# for more information
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
class SuperAdmin::AppConfigsController < SuperAdmin::ApplicationController
|
||||
def show
|
||||
@allowed_configs = %w[FB_APP_ID FB_VERIFY_TOKEN FB_APP_SECRET]
|
||||
# ref: https://github.com/rubocop/rubocop/issues/7767
|
||||
# rubocop:disable Style/HashTransformValues
|
||||
@fb_config = InstallationConfig.where(name: @allowed_configs)
|
||||
.pluck(:name, :serialized_value)
|
||||
.map { |name, serialized_value| [name, serialized_value['value']] }
|
||||
.to_h
|
||||
# rubocop:enable Style/HashTransformValues
|
||||
end
|
||||
|
||||
def create
|
||||
params['app_config'].each do |key, value|
|
||||
i = InstallationConfig.where(name: key).first_or_create(value: value, locked: false)
|
||||
i.value = value
|
||||
i.save!
|
||||
end
|
||||
redirect_to super_admin_app_config_url
|
||||
end
|
||||
end
|
||||
@@ -8,7 +8,7 @@ class SuperAdmin::Devise::SessionsController < Devise::SessionsController
|
||||
def create
|
||||
redirect_to(super_admin_session_path, flash: { error: @error_message }) && return unless valid_credentials?
|
||||
|
||||
sign_in(@super_admin, scope: :super_admin)
|
||||
sign_in(:super_admin, @super_admin)
|
||||
flash.discard
|
||||
redirect_to super_admin_users_path
|
||||
end
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
class SuperAdmin::SuperAdminsController < SuperAdmin::ApplicationController
|
||||
# Overwrite any of the RESTful controller actions to implement custom behavior
|
||||
# For example, you may want to send an email after a foo is updated.
|
||||
#
|
||||
# def update
|
||||
# super
|
||||
# send_foo_updated_email(requested_resource)
|
||||
# end
|
||||
|
||||
# Override this method to specify custom lookup behavior.
|
||||
# This will be used to set the resource for the `show`, `edit`, and `update`
|
||||
# actions.
|
||||
#
|
||||
# def find_resource(param)
|
||||
# Foo.find_by!(slug: param)
|
||||
# end
|
||||
|
||||
# The result of this lookup will be available as `requested_resource`
|
||||
|
||||
# Override this if you have certain roles that require a subset
|
||||
# this will be used to set the records shown on the `index` action.
|
||||
#
|
||||
# def scoped_resource
|
||||
# if current_user.super_admin?
|
||||
# resource_class
|
||||
# else
|
||||
# resource_class.with_less_stuff
|
||||
# end
|
||||
# end
|
||||
|
||||
# Override `resource_params` if you want to transform the submitted
|
||||
# data before it's persisted. For example, the following would turn all
|
||||
# empty values into nil values. It uses other APIs such as `resource_class`
|
||||
# and `dashboard`:
|
||||
#
|
||||
# def resource_params
|
||||
# params.require(resource_class.model_name.param_key).
|
||||
# permit(dashboard.permitted_attributes).
|
||||
# transform_values { |value| value == "" ? nil : value }
|
||||
# end
|
||||
|
||||
# See https://administrate-prototype.herokuapp.com/customizing_controller_actions
|
||||
# for more information
|
||||
end
|
||||
@@ -33,12 +33,15 @@ class SuperAdmin::UsersController < SuperAdmin::ApplicationController
|
||||
# empty values into nil values. It uses other APIs such as `resource_class`
|
||||
# and `dashboard`:
|
||||
#
|
||||
# def resource_params
|
||||
# params.require(resource_class.model_name.param_key).
|
||||
# permit(dashboard.permitted_attributes).
|
||||
# transform_values { |value| value == "" ? nil : value }
|
||||
# end
|
||||
def resource_params
|
||||
permitted_params = super
|
||||
permitted_params.delete(:password) if permitted_params[:password].blank?
|
||||
permitted_params
|
||||
end
|
||||
|
||||
# See https://administrate-prototype.herokuapp.com/customizing_controller_actions
|
||||
# for more information
|
||||
def find_resource(param)
|
||||
super.becomes(User)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
class Webhooks::SmsController < ActionController::API
|
||||
def process_payload
|
||||
Webhooks::SmsEventsJob.perform_later(params['_json']&.first&.to_unsafe_hash)
|
||||
head :ok
|
||||
end
|
||||
end
|
||||
@@ -1,5 +1,8 @@
|
||||
class WidgetTestsController < ActionController::Base
|
||||
before_action :set_web_widget
|
||||
before_action :ensure_web_widget
|
||||
before_action :ensure_widget_position
|
||||
before_action :ensure_widget_type
|
||||
before_action :ensure_widget_style
|
||||
|
||||
def index
|
||||
render
|
||||
@@ -7,7 +10,24 @@ class WidgetTestsController < ActionController::Base
|
||||
|
||||
private
|
||||
|
||||
def set_web_widget
|
||||
@web_widget = Channel::WebWidget.first
|
||||
def ensure_widget_style
|
||||
@widget_style = params[:widget_style] || 'standard'
|
||||
end
|
||||
|
||||
def ensure_widget_position
|
||||
@widget_position = params[:position] || 'left'
|
||||
end
|
||||
|
||||
def ensure_widget_type
|
||||
@widget_type = params[:type] || 'expanded_bubble'
|
||||
end
|
||||
|
||||
def inbox_id
|
||||
@inbox_id ||= params[:inbox_id] || Channel::WebWidget.first.inbox.id
|
||||
end
|
||||
|
||||
def ensure_web_widget
|
||||
@inbox = Inbox.find(inbox_id)
|
||||
@web_widget = @inbox.channel
|
||||
end
|
||||
end
|
||||
|
||||
@@ -10,7 +10,7 @@ class WidgetsController < ActionController::Base
|
||||
private
|
||||
|
||||
def set_global_config
|
||||
@global_config = GlobalConfig.get('LOGO_THUMBNAIL', 'BRAND_NAME', 'WIDGET_BRAND_URL')
|
||||
@global_config = GlobalConfig.get('LOGO_THUMBNAIL', 'BRAND_NAME', 'WIDGET_BRAND_URL', 'DIRECT_UPLOADS_ENABLED')
|
||||
end
|
||||
|
||||
def set_web_widget
|
||||
|
||||
@@ -7,6 +7,8 @@ class AccountDashboard < Administrate::BaseDashboard
|
||||
# Each different type represents an Administrate::Field object,
|
||||
# which determines how the attribute is displayed
|
||||
# on pages throughout the dashboard.
|
||||
|
||||
enterprise_attribute_types = ChatwootApp.enterprise? ? { limits: Enterprise::AccountLimitsField } : {}
|
||||
ATTRIBUTE_TYPES = {
|
||||
id: Field::Number,
|
||||
name: Field::String,
|
||||
@@ -16,7 +18,7 @@ class AccountDashboard < Administrate::BaseDashboard
|
||||
conversations: CountField,
|
||||
locale: Field::Select.with_options(collection: LANGUAGES_CONFIG.map { |_x, y| y[:iso_639_1_code] }),
|
||||
account_users: Field::HasMany
|
||||
}.freeze
|
||||
}.merge(enterprise_attribute_types).freeze
|
||||
|
||||
# COLLECTION_ATTRIBUTES
|
||||
# an array of attributes that will be displayed on the model's index page.
|
||||
@@ -33,7 +35,8 @@ class AccountDashboard < Administrate::BaseDashboard
|
||||
|
||||
# SHOW_PAGE_ATTRIBUTES
|
||||
# an array of attributes that will be displayed on the model's show page.
|
||||
SHOW_PAGE_ATTRIBUTES = %i[
|
||||
enterprise_show_page_attributes = ChatwootApp.enterprise? ? %i[limits] : []
|
||||
SHOW_PAGE_ATTRIBUTES = (%i[
|
||||
id
|
||||
name
|
||||
created_at
|
||||
@@ -41,15 +44,16 @@ class AccountDashboard < Administrate::BaseDashboard
|
||||
locale
|
||||
conversations
|
||||
account_users
|
||||
].freeze
|
||||
] + enterprise_show_page_attributes).freeze
|
||||
|
||||
# FORM_ATTRIBUTES
|
||||
# an array of attributes that will be displayed
|
||||
# on the model's form (`new` and `edit`) pages.
|
||||
FORM_ATTRIBUTES = %i[
|
||||
enterprise_form_attributes = ChatwootApp.enterprise? ? %i[limits] : []
|
||||
FORM_ATTRIBUTES = (%i[
|
||||
name
|
||||
locale
|
||||
].freeze
|
||||
] + enterprise_form_attributes).freeze
|
||||
|
||||
# COLLECTION_FILTERS
|
||||
# a hash that defines filters that can be used while searching via the search
|
||||
@@ -69,4 +73,8 @@ class AccountDashboard < Administrate::BaseDashboard
|
||||
def display_resource(account)
|
||||
"##{account.id} #{account.name}"
|
||||
end
|
||||
|
||||
def permitted_attributes
|
||||
super + [limits: {}]
|
||||
end
|
||||
end
|
||||
|
||||
@@ -12,6 +12,7 @@ class AgentBotDashboard < Administrate::BaseDashboard
|
||||
avatar_url: AvatarField,
|
||||
id: Field::Number,
|
||||
name: Field::String,
|
||||
account: Field::BelongsTo.with_options(searchable: true, searchable_field: 'name', order: 'id DESC'),
|
||||
description: Field::String,
|
||||
outgoing_url: Field::String,
|
||||
created_at: Field::DateTime,
|
||||
@@ -26,6 +27,7 @@ class AgentBotDashboard < Administrate::BaseDashboard
|
||||
COLLECTION_ATTRIBUTES = %i[
|
||||
id
|
||||
avatar_url
|
||||
account
|
||||
name
|
||||
outgoing_url
|
||||
].freeze
|
||||
@@ -34,7 +36,7 @@ class AgentBotDashboard < Administrate::BaseDashboard
|
||||
# an array of attributes that will be displayed on the model's show page.
|
||||
SHOW_PAGE_ATTRIBUTES = %i[
|
||||
id
|
||||
avatar_url
|
||||
account
|
||||
name
|
||||
description
|
||||
outgoing_url
|
||||
@@ -45,6 +47,7 @@ class AgentBotDashboard < Administrate::BaseDashboard
|
||||
# on the model's form (`new` and `edit`) pages.
|
||||
FORM_ATTRIBUTES = %i[
|
||||
name
|
||||
account
|
||||
description
|
||||
outgoing_url
|
||||
].freeze
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
require 'administrate/base_dashboard'
|
||||
|
||||
class SuperAdminDashboard < Administrate::BaseDashboard
|
||||
# ATTRIBUTE_TYPES
|
||||
# a hash that describes the type of each of the model's fields.
|
||||
#
|
||||
# Each different type represents an Administrate::Field object,
|
||||
# which determines how the attribute is displayed
|
||||
# on pages throughout the dashboard.
|
||||
ATTRIBUTE_TYPES = {
|
||||
id: Field::Number,
|
||||
email: Field::String,
|
||||
password: Field::Password,
|
||||
remember_created_at: Field::DateTime,
|
||||
sign_in_count: Field::Number,
|
||||
current_sign_in_at: Field::DateTime,
|
||||
last_sign_in_at: Field::DateTime,
|
||||
current_sign_in_ip: Field::String.with_options(searchable: false),
|
||||
last_sign_in_ip: Field::String.with_options(searchable: false),
|
||||
created_at: Field::DateTime,
|
||||
updated_at: Field::DateTime
|
||||
}.freeze
|
||||
|
||||
# COLLECTION_ATTRIBUTES
|
||||
# an array of attributes that will be displayed on the model's index page.
|
||||
#
|
||||
# By default, it's limited to four items to reduce clutter on index pages.
|
||||
# Feel free to add, remove, or rearrange items.
|
||||
COLLECTION_ATTRIBUTES = %i[
|
||||
id
|
||||
email
|
||||
].freeze
|
||||
|
||||
# SHOW_PAGE_ATTRIBUTES
|
||||
# an array of attributes that will be displayed on the model's show page.
|
||||
SHOW_PAGE_ATTRIBUTES = %i[
|
||||
id
|
||||
email
|
||||
remember_created_at
|
||||
sign_in_count
|
||||
current_sign_in_at
|
||||
last_sign_in_at
|
||||
current_sign_in_ip
|
||||
last_sign_in_ip
|
||||
created_at
|
||||
updated_at
|
||||
].freeze
|
||||
|
||||
# FORM_ATTRIBUTES
|
||||
# an array of attributes that will be displayed
|
||||
# on the model's form (`new` and `edit`) pages.
|
||||
FORM_ATTRIBUTES = %i[
|
||||
email
|
||||
password
|
||||
].freeze
|
||||
|
||||
# COLLECTION_FILTERS
|
||||
# a hash that defines filters that can be used while searching via the search
|
||||
# field of the dashboard.
|
||||
#
|
||||
# For example to add an option to search for open resources by typing "open:"
|
||||
# in the search field:
|
||||
#
|
||||
# COLLECTION_FILTERS = {
|
||||
# open: ->(resources) { resources.where(open: true) }
|
||||
# }.freeze
|
||||
COLLECTION_FILTERS = {}.freeze
|
||||
|
||||
# Overwrite this method to customize how super admins are displayed
|
||||
# across all pages of the admin dashboard.
|
||||
#
|
||||
# def display_resource(super_admin)
|
||||
# "SuperAdmin ##{super_admin.id}"
|
||||
# end
|
||||
end
|
||||
@@ -30,6 +30,7 @@ class UserDashboard < Administrate::BaseDashboard
|
||||
created_at: Field::DateTime,
|
||||
updated_at: Field::DateTime,
|
||||
pubsub_token: Field::String,
|
||||
type: Field::Select.with_options(collection: [nil, 'SuperAdmin']),
|
||||
accounts: CountField
|
||||
}.freeze
|
||||
|
||||
@@ -44,6 +45,7 @@ class UserDashboard < Administrate::BaseDashboard
|
||||
name
|
||||
email
|
||||
accounts
|
||||
type
|
||||
].freeze
|
||||
|
||||
# SHOW_PAGE_ATTRIBUTES
|
||||
@@ -53,10 +55,12 @@ class UserDashboard < Administrate::BaseDashboard
|
||||
avatar_url
|
||||
unconfirmed_email
|
||||
name
|
||||
type
|
||||
display_name
|
||||
email
|
||||
created_at
|
||||
updated_at
|
||||
confirmed_at
|
||||
account_users
|
||||
].freeze
|
||||
|
||||
@@ -68,6 +72,8 @@ class UserDashboard < Administrate::BaseDashboard
|
||||
display_name
|
||||
email
|
||||
password
|
||||
confirmed_at
|
||||
type
|
||||
].freeze
|
||||
|
||||
# COLLECTION_FILTERS
|
||||
|
||||
@@ -12,11 +12,12 @@ class AsyncDispatcher < BaseDispatcher
|
||||
[
|
||||
CampaignListener.instance,
|
||||
CsatSurveyListener.instance,
|
||||
EventListener.instance,
|
||||
HookListener.instance,
|
||||
InstallationWebhookListener.instance,
|
||||
NotificationListener.instance,
|
||||
WebhookListener.instance
|
||||
ReportingEventListener.instance,
|
||||
WebhookListener.instance,
|
||||
AutomationRuleListener.instance
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2,6 +2,6 @@ require 'administrate/field/base'
|
||||
|
||||
class AvatarField < Administrate::Field::Base
|
||||
def avatar_url
|
||||
data.presence || '/admin/avatar.png'
|
||||
data.presence&.gsub('?d=404', '?d=mp')
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
require 'administrate/field/base'
|
||||
|
||||
class Enterprise::AccountLimitsField < Administrate::Field::Base
|
||||
def to_s
|
||||
data.present? ? data.to_json : { agents: nil, inboxes: nil }.to_json
|
||||
end
|
||||
end
|
||||
@@ -55,7 +55,7 @@ class ConversationFinder
|
||||
|
||||
def set_inboxes
|
||||
@inbox_ids = if params[:inbox_id]
|
||||
current_account.inboxes.where(id: params[:inbox_id])
|
||||
@current_user.assigned_inboxes.where(id: params[:inbox_id])
|
||||
else
|
||||
@current_user.assigned_inboxes.pluck(:id)
|
||||
end
|
||||
|
||||
@@ -26,8 +26,48 @@ module Api::V1::InboxesHelper
|
||||
def validate_smtp(channel_data)
|
||||
return unless channel_data.key?('smtp_enabled') && channel_data[:smtp_enabled]
|
||||
|
||||
smtp = Net::SMTP.start(channel_data[:smtp_address], channel_data[:smtp_port], channel_data[:smtp_domain], channel_data[:smtp_email],
|
||||
channel_data[:smtp_password], :login)
|
||||
smtp = Net::SMTP.new(channel_data[:smtp_address], channel_data[:smtp_port])
|
||||
|
||||
set_smtp_encryption(channel_data, smtp)
|
||||
|
||||
smtp.start(channel_data[:smtp_domain], channel_data[:smtp_email], channel_data[:smtp_password], :login)
|
||||
smtp.finish unless smtp&.nil?
|
||||
end
|
||||
|
||||
def set_smtp_encryption(channel_data, smtp)
|
||||
if channel_data[:smtp_enable_ssl_tls]
|
||||
set_enable_tls(channel_data, smtp)
|
||||
elsif channel_data[:smtp_enable_starttls_auto]
|
||||
set_enable_starttls_auto(channel_data, smtp)
|
||||
end
|
||||
end
|
||||
|
||||
def set_enable_starttls_auto(channel_data, smtp)
|
||||
return unless smtp.respond_to?(:enable_starttls_auto)
|
||||
|
||||
if channel_data[:smtp_openssl_verify_mode]
|
||||
context = enable_openssl_mode(channel_data[:smtp_openssl_verify_mode])
|
||||
smtp.enable_starttls_auto(context)
|
||||
else
|
||||
smtp.enable_starttls_auto
|
||||
end
|
||||
end
|
||||
|
||||
def set_enable_tls(channel_data, smtp)
|
||||
return unless smtp.respond_to?(:enable_tls)
|
||||
|
||||
if channel_data[:smtp_openssl_verify_mode]
|
||||
context = enable_openssl_mode(channel_data[:smtp_openssl_verify_mode])
|
||||
smtp.enable_tls(context)
|
||||
else
|
||||
smtp.enable_tls
|
||||
end
|
||||
end
|
||||
|
||||
def enable_openssl_mode(smtp_openssl_verify_mode)
|
||||
openssl_verify_mode = "OpenSSL::SSL::VERIFY_#{smtp_openssl_verify_mode.upcase}".constantize if smtp_openssl_verify_mode.is_a?(String)
|
||||
context = Net::SMTP.default_ssl_context
|
||||
context.verify_mode = openssl_verify_mode
|
||||
context
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
module Api::V1::ReportsHelper
|
||||
end
|
||||
@@ -3,7 +3,7 @@ module FileTypeHelper
|
||||
def file_type(content_type)
|
||||
return :image if image_file?(content_type)
|
||||
return :video if video_file?(content_type)
|
||||
return :audio if content_type.include?('audio/')
|
||||
return :audio if content_type&.include?('audio/')
|
||||
|
||||
:file
|
||||
end
|
||||
@@ -14,7 +14,8 @@ module FileTypeHelper
|
||||
'image/png',
|
||||
'image/gif',
|
||||
'image/bmp',
|
||||
'image/webp'
|
||||
'image/webp',
|
||||
'image'
|
||||
].include?(content_type)
|
||||
end
|
||||
|
||||
@@ -23,7 +24,8 @@ module FileTypeHelper
|
||||
'video/ogg',
|
||||
'video/mp4',
|
||||
'video/webm',
|
||||
'video/quicktime'
|
||||
'video/quicktime',
|
||||
'video'
|
||||
].include?(content_type)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<div id="app" class="app-wrapper app-root">
|
||||
<update-banner :latest-chatwoot-version="latestChatwootVersion" />
|
||||
<transition name="fade" mode="out-in">
|
||||
<router-view></router-view>
|
||||
</transition>
|
||||
@@ -13,24 +14,27 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { accountIdFromPathname } from './helper/URLHelper';
|
||||
import { mapGetters } from 'vuex';
|
||||
import AddAccountModal from '../dashboard/components/layout/sidebarComponents/AddAccountModal';
|
||||
import WootSnackbarBox from './components/SnackbarContainer';
|
||||
import NetworkNotification from './components/NetworkNotification';
|
||||
import { accountIdFromPathname } from './helper/URLHelper';
|
||||
import UpdateBanner from './components/app/UpdateBanner.vue';
|
||||
import WootSnackbarBox from './components/SnackbarContainer';
|
||||
|
||||
export default {
|
||||
name: 'App',
|
||||
|
||||
components: {
|
||||
WootSnackbarBox,
|
||||
AddAccountModal,
|
||||
NetworkNotification,
|
||||
UpdateBanner,
|
||||
WootSnackbarBox,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
showAddAccountModal: false,
|
||||
latestChatwootVersion: null,
|
||||
};
|
||||
},
|
||||
|
||||
@@ -38,6 +42,7 @@ export default {
|
||||
...mapGetters({
|
||||
getAccount: 'accounts/getAccount',
|
||||
currentUser: 'getCurrentUser',
|
||||
globalConfig: 'globalConfig/get',
|
||||
}),
|
||||
hasAccounts() {
|
||||
return (
|
||||
@@ -72,8 +77,12 @@ export default {
|
||||
|
||||
if (accountId) {
|
||||
await this.$store.dispatch('accounts/get');
|
||||
const { locale } = this.getAccount(accountId);
|
||||
const {
|
||||
locale,
|
||||
latest_chatwoot_version: latestChatwootVersion,
|
||||
} = this.getAccount(accountId);
|
||||
this.setLocale(locale);
|
||||
this.latestChatwootVersion = latestChatwootVersion;
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -82,6 +91,11 @@ export default {
|
||||
|
||||
<style lang="scss">
|
||||
@import './assets/scss/app';
|
||||
.update-banner {
|
||||
height: var(--space-larger);
|
||||
align-items: center;
|
||||
font-size: var(--font-size-small) !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style src="vue-multiselect/dist/vue-multiselect.min.css"></style>
|
||||
|
||||
@@ -13,7 +13,7 @@ export default {
|
||||
.post('auth/sign_in', creds)
|
||||
.then(response => {
|
||||
setAuthCredentials(response);
|
||||
resolve();
|
||||
resolve(response.data);
|
||||
})
|
||||
.catch(error => {
|
||||
reject(error.response);
|
||||
@@ -30,6 +30,7 @@ export default {
|
||||
user_full_name: creds.fullName.trim(),
|
||||
email: creds.email,
|
||||
password: creds.password,
|
||||
h_captcha_client_response: creds.hCaptchaClientResponse,
|
||||
})
|
||||
.then(response => {
|
||||
setAuthCredentials(response);
|
||||
@@ -138,13 +139,14 @@ export default {
|
||||
password,
|
||||
password_confirmation,
|
||||
displayName,
|
||||
avatar,
|
||||
...profileAttributes
|
||||
}) {
|
||||
const formData = new FormData();
|
||||
Object.keys(profileAttributes).forEach(key => {
|
||||
const value = profileAttributes[key];
|
||||
if (value) {
|
||||
formData.append(`profile[${key}]`, value);
|
||||
const hasValue = profileAttributes[key] === undefined;
|
||||
if (!hasValue) {
|
||||
formData.append(`profile[${key}]`, profileAttributes[key]);
|
||||
}
|
||||
});
|
||||
formData.append('profile[display_name]', displayName || '');
|
||||
@@ -152,6 +154,9 @@ export default {
|
||||
formData.append('profile[password]', password);
|
||||
formData.append('profile[password_confirmation]', password_confirmation);
|
||||
}
|
||||
if (avatar) {
|
||||
formData.append('profile[avatar]', avatar);
|
||||
}
|
||||
return axios.put(endPoints('profileUpdate').url, formData);
|
||||
},
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
/* global axios */
|
||||
import ApiClient from './ApiClient';
|
||||
|
||||
class AutomationsAPI extends ApiClient {
|
||||
constructor() {
|
||||
super('automation_rules', { accountScoped: true });
|
||||
}
|
||||
|
||||
clone(automationId) {
|
||||
return axios.post(`${this.url}/${automationId}/clone`);
|
||||
}
|
||||
}
|
||||
|
||||
export default new AutomationsAPI();
|
||||
@@ -6,15 +6,21 @@ class CSATReportsAPI extends ApiClient {
|
||||
super('csat_survey_responses', { accountScoped: true });
|
||||
}
|
||||
|
||||
get({ page, from, to } = {}) {
|
||||
get({ page, from, to, user_ids } = {}) {
|
||||
return axios.get(this.url, {
|
||||
params: { page, since: from, until: to, sort: '-created_at' },
|
||||
params: {
|
||||
page,
|
||||
since: from,
|
||||
until: to,
|
||||
sort: '-created_at',
|
||||
user_ids,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
getMetrics({ from, to } = {}) {
|
||||
getMetrics({ from, to, user_ids } = {}) {
|
||||
return axios.get(`${this.url}/metrics`, {
|
||||
params: { since: from, until: to },
|
||||
params: { since: from, until: to, user_ids },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
/* global axios */
|
||||
import ApiClient from './ApiClient';
|
||||
|
||||
class CustomViewsAPI extends ApiClient {
|
||||
constructor() {
|
||||
super('custom_filters', { accountScoped: true });
|
||||
}
|
||||
|
||||
getCustomViewsByFilterType(type) {
|
||||
return axios.get(`${this.url}?filter_type=${type}`);
|
||||
}
|
||||
|
||||
deleteCustomViews(id, type) {
|
||||
return axios.delete(`${this.url}/${id}?filter_type=${type}`);
|
||||
}
|
||||
}
|
||||
|
||||
export default new CustomViewsAPI();
|
||||
@@ -7,17 +7,19 @@ export const buildCreatePayload = ({
|
||||
isPrivate,
|
||||
contentAttributes,
|
||||
echoId,
|
||||
file,
|
||||
files,
|
||||
ccEmails = '',
|
||||
bccEmails = '',
|
||||
}) => {
|
||||
let payload;
|
||||
if (file) {
|
||||
if (files && files.length !== 0) {
|
||||
payload = new FormData();
|
||||
payload.append('attachments[]', file, file.name);
|
||||
if (message) {
|
||||
payload.append('content', message);
|
||||
}
|
||||
files.forEach(file => {
|
||||
payload.append('attachments[]', file);
|
||||
});
|
||||
payload.append('private', isPrivate);
|
||||
payload.append('echo_id', echoId);
|
||||
payload.append('cc_emails', ccEmails);
|
||||
@@ -46,7 +48,7 @@ class MessageApi extends ApiClient {
|
||||
private: isPrivate,
|
||||
contentAttributes,
|
||||
echo_id: echoId,
|
||||
file,
|
||||
files,
|
||||
ccEmails = '',
|
||||
bccEmails = '',
|
||||
}) {
|
||||
@@ -58,7 +60,7 @@ class MessageApi extends ApiClient {
|
||||
isPrivate,
|
||||
contentAttributes,
|
||||
echoId,
|
||||
file,
|
||||
files,
|
||||
ccEmails,
|
||||
bccEmails,
|
||||
}),
|
||||
|
||||
@@ -1,20 +1,36 @@
|
||||
/* global axios */
|
||||
import ApiClient from './ApiClient';
|
||||
|
||||
const getTimeOffset = () => -new Date().getTimezoneOffset() / 60;
|
||||
|
||||
class ReportsAPI extends ApiClient {
|
||||
constructor() {
|
||||
super('reports', { accountScoped: true, apiVersion: 'v2' });
|
||||
}
|
||||
|
||||
getReports(metric, since, until, type = 'account', id) {
|
||||
getReports(metric, since, until, type = 'account', id, group_by) {
|
||||
return axios.get(`${this.url}`, {
|
||||
params: { metric, since, until, type, id },
|
||||
params: {
|
||||
metric,
|
||||
since,
|
||||
until,
|
||||
type,
|
||||
id,
|
||||
group_by,
|
||||
timezone_offset: getTimeOffset(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
getSummary(since, until, type = 'account', id) {
|
||||
getSummary(since, until, type = 'account', id, group_by) {
|
||||
return axios.get(`${this.url}/summary`, {
|
||||
params: { since, until, type, id },
|
||||
params: {
|
||||
since,
|
||||
until,
|
||||
type,
|
||||
id,
|
||||
group_by,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import automations from '../automation';
|
||||
import ApiClient from '../ApiClient';
|
||||
|
||||
describe('#AutomationsAPI', () => {
|
||||
it('creates correct instance', () => {
|
||||
expect(automations).toBeInstanceOf(ApiClient);
|
||||
expect(automations).toHaveProperty('get');
|
||||
expect(automations).toHaveProperty('show');
|
||||
expect(automations).toHaveProperty('create');
|
||||
expect(automations).toHaveProperty('update');
|
||||
expect(automations).toHaveProperty('delete');
|
||||
expect(automations).toHaveProperty('clone');
|
||||
expect(automations.url).toBe('/api/v1/automation_rules');
|
||||
});
|
||||
});
|
||||
@@ -36,7 +36,7 @@ describe('#ConversationAPI', () => {
|
||||
echoId: 12,
|
||||
isPrivate: true,
|
||||
|
||||
file: new Blob(['test-content'], { type: 'application/pdf' }),
|
||||
files: [new Blob(['test-content'], { type: 'application/pdf' })],
|
||||
});
|
||||
expect(formPayload).toBeInstanceOf(FormData);
|
||||
expect(formPayload.get('content')).toEqual('test content');
|
||||
|
||||
@@ -27,6 +27,7 @@ describe('#Reports API', () => {
|
||||
since: 1621103400,
|
||||
until: 1621621800,
|
||||
type: 'account',
|
||||
timezone_offset: -0,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -26,7 +26,8 @@
|
||||
|
||||
code {
|
||||
border: 0;
|
||||
font-family: 'Monaco', Verdana;
|
||||
font-family: 'ui-monospace', 'SFMono-Regular', 'Menlo', 'Monaco', 'Consolas',
|
||||
'"Liberation Mono"', '"Courier New"', 'monospace';
|
||||
font-size: $font-size-mini;
|
||||
|
||||
&.hljs {
|
||||
@@ -55,7 +56,6 @@ code {
|
||||
padding-right: var(--space-normal);
|
||||
}
|
||||
|
||||
|
||||
.badge {
|
||||
border-radius: var(--border-radius-normal);
|
||||
}
|
||||
|
||||
@@ -10,10 +10,24 @@ body {
|
||||
|
||||
.app-wrapper {
|
||||
@include full-height;
|
||||
flex-grow: 0;
|
||||
min-height: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.app-root {
|
||||
.banner + .app-wrapper {
|
||||
.button--fixed-right-top {
|
||||
top: 5.6 * $space-one;
|
||||
}
|
||||
|
||||
.off-canvas-content {
|
||||
.button--fixed-right-top {
|
||||
top: $space-small;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is-closed .app-root {
|
||||
@include flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
@@ -21,6 +35,7 @@ body {
|
||||
.app-content {
|
||||
@include flex;
|
||||
@include full-height;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
||||
@@ -54,3 +54,9 @@
|
||||
.text-y-800 {
|
||||
color: var(--y-800);
|
||||
}
|
||||
|
||||
.text-ellipsis {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@@ -228,8 +228,14 @@
|
||||
@include flex-align(right, null);
|
||||
|
||||
.wrap {
|
||||
align-items: flex-end;
|
||||
display: flex;
|
||||
margin-right: $space-normal;
|
||||
text-align: right;
|
||||
|
||||
.sender--info {
|
||||
padding: var(--space-small) 0 var(--space-smaller) var(--space-small);
|
||||
}
|
||||
}
|
||||
|
||||
.bubble {
|
||||
|
||||
@@ -20,9 +20,28 @@
|
||||
color: $color-heading;
|
||||
}
|
||||
|
||||
.metric-wrap {
|
||||
align-items: baseline;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.metric {
|
||||
font-size: $font-size-bigger;
|
||||
font-size: $font-size-big;
|
||||
font-weight: $font-weight-feather;
|
||||
margin-top: $space-smaller;
|
||||
}
|
||||
|
||||
.metric-trend {
|
||||
font-size: $font-size-small;
|
||||
margin-left: $space-small;
|
||||
}
|
||||
|
||||
.metric-up {
|
||||
color: $success-color;
|
||||
}
|
||||
|
||||
.metric-down {
|
||||
color: $alert-color;
|
||||
}
|
||||
|
||||
.desc {
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
@include shadow;
|
||||
background-color: $woot-snackbar-bg;
|
||||
border-radius: $space-smaller;
|
||||
display: inline-block;
|
||||
display: inline-flex;
|
||||
margin-bottom: $space-small;
|
||||
max-width: 40rem;
|
||||
min-height: 3rem;
|
||||
|
||||
@@ -1,26 +1,51 @@
|
||||
l<template>
|
||||
<template>
|
||||
<div class="conversations-list-wrap">
|
||||
<slot></slot>
|
||||
<div class="chat-list__top" :class="{ filter__applied: hasAppliedFilters }">
|
||||
<div
|
||||
class="chat-list__top"
|
||||
:class="{ filter__applied: hasAppliedFiltersOrActiveFolders }"
|
||||
>
|
||||
<h1 class="page-title text-truncate" :title="pageTitle">
|
||||
{{ pageTitle }}
|
||||
</h1>
|
||||
|
||||
<div class="filter--actions">
|
||||
<chat-filter
|
||||
v-if="!hasAppliedFilters"
|
||||
v-if="!hasAppliedFiltersOrActiveFolders"
|
||||
@statusFilterChange="updateStatusType"
|
||||
/>
|
||||
<div v-if="hasAppliedFilters && !hasActiveFolders">
|
||||
<woot-button
|
||||
v-tooltip.top-end="$t('FILTER.CUSTOM_VIEWS.ADD.SAVE_BUTTON')"
|
||||
size="tiny"
|
||||
variant="smooth"
|
||||
color-scheme="secondary"
|
||||
icon="save"
|
||||
@click="onClickOpenAddFoldersModal"
|
||||
/>
|
||||
<woot-button
|
||||
v-tooltip.top-end="$t('FILTER.CLEAR_BUTTON_LABEL')"
|
||||
size="tiny"
|
||||
variant="smooth"
|
||||
color-scheme="alert"
|
||||
icon="dismiss-circle"
|
||||
@click="resetAndFetchData"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="hasActiveFolders">
|
||||
<woot-button
|
||||
v-tooltip.top-end="$t('FILTER.CUSTOM_VIEWS.DELETE.DELETE_BUTTON')"
|
||||
size="tiny"
|
||||
variant="smooth"
|
||||
color-scheme="alert"
|
||||
icon="delete"
|
||||
class="delete-custom-view__button"
|
||||
@click="onClickOpenDeleteFoldersModal"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<woot-button
|
||||
v-else
|
||||
size="small"
|
||||
variant="clear"
|
||||
color-scheme="alert"
|
||||
@click="resetAndFetchData"
|
||||
>
|
||||
{{ $t('FILTER.CLEAR_BUTTON_LABEL') }}
|
||||
</woot-button>
|
||||
<woot-button
|
||||
v-tooltip.top-end="$t('FILTER.TOOLTIP_LABEL')"
|
||||
variant="clear"
|
||||
color-scheme="secondary"
|
||||
@@ -33,8 +58,24 @@ l<template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<add-custom-views
|
||||
v-if="showAddFoldersModal"
|
||||
:custom-views-query="foldersQuery"
|
||||
:open-last-saved-item="openLastSavedItemInFolder"
|
||||
@close="onCloseAddFoldersModal"
|
||||
/>
|
||||
|
||||
<delete-custom-views
|
||||
v-if="showDeleteFoldersModal"
|
||||
:show-delete-popup.sync="showDeleteFoldersModal"
|
||||
:active-custom-view="activeFolder"
|
||||
:custom-views-id="foldersId"
|
||||
:open-last-item-after-delete="openLastItemAfterDeleteInFolder"
|
||||
@close="onCloseDeleteFoldersModal"
|
||||
/>
|
||||
|
||||
<chat-type-tabs
|
||||
v-if="!hasAppliedFilters"
|
||||
v-if="!hasAppliedFiltersOrActiveFolders"
|
||||
:items="assigneeTabItems"
|
||||
:active-tab="activeAssigneeTab"
|
||||
class="tab--chat-type"
|
||||
@@ -51,6 +92,7 @@ l<template>
|
||||
:key="chat.id"
|
||||
:active-label="label"
|
||||
:team-id="teamId"
|
||||
:folders-id="foldersId"
|
||||
:chat="chat"
|
||||
:conversation-type="conversationType"
|
||||
:show-assignee="showAssigneeInConversationCard"
|
||||
@@ -87,7 +129,7 @@ l<template>
|
||||
>
|
||||
<conversation-advanced-filter
|
||||
v-if="showAdvancedFilters"
|
||||
:filter-types="advancedFilterTypes"
|
||||
:initial-filter-types="advancedFilterTypes"
|
||||
:on-close="onToggleAdvanceFiltersModal"
|
||||
@applyFilter="onApplyFilter"
|
||||
/>
|
||||
@@ -108,6 +150,8 @@ import conversationMixin from '../mixins/conversations';
|
||||
import wootConstants from '../constants';
|
||||
import advancedFilterTypes from './widgets/conversation/advancedFilterItems';
|
||||
import filterQueryGenerator from '../helper/filterQueryGenerator.js';
|
||||
import AddCustomViews from 'dashboard/routes/dashboard/customviews/AddCustomViews';
|
||||
import DeleteCustomViews from 'dashboard/routes/dashboard/customviews/DeleteCustomViews.vue';
|
||||
|
||||
import {
|
||||
hasPressedAltAndJKey,
|
||||
@@ -116,10 +160,12 @@ import {
|
||||
|
||||
export default {
|
||||
components: {
|
||||
AddCustomViews,
|
||||
ChatTypeTabs,
|
||||
ConversationCard,
|
||||
ChatFilter,
|
||||
ConversationAdvancedFilter,
|
||||
DeleteCustomViews,
|
||||
},
|
||||
mixins: [timeMixin, conversationMixin, eventListenerMixins],
|
||||
props: {
|
||||
@@ -139,6 +185,10 @@ export default {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
foldersId: {
|
||||
type: [String, Number],
|
||||
default: 0,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -149,6 +199,9 @@ export default {
|
||||
...filter,
|
||||
attributeName: this.$t(`FILTER.ATTRIBUTES.${filter.attributeI18nKey}`),
|
||||
})),
|
||||
foldersQuery: {},
|
||||
showAddFoldersModal: false,
|
||||
showDeleteFoldersModal: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -163,9 +216,23 @@ export default {
|
||||
activeInbox: 'getSelectedInbox',
|
||||
conversationStats: 'conversationStats/getStats',
|
||||
appliedFilters: 'getAppliedConversationFilters',
|
||||
folders: 'customViews/getCustomViews',
|
||||
}),
|
||||
hasAppliedFilters() {
|
||||
return this.appliedFilters.length;
|
||||
return this.appliedFilters.length !== 0;
|
||||
},
|
||||
hasActiveFolders() {
|
||||
return this.activeFolder && this.foldersId !== 0;
|
||||
},
|
||||
hasAppliedFiltersOrActiveFolders() {
|
||||
return this.hasAppliedFilters || this.hasActiveFolders;
|
||||
},
|
||||
savedFoldersValue() {
|
||||
if (this.hasActiveFolders) {
|
||||
const payload = this.activeFolder.query;
|
||||
this.fetchSavedFilteredConversations(payload);
|
||||
}
|
||||
return {};
|
||||
},
|
||||
assigneeTabItems() {
|
||||
return this.$t('CHAT_LIST.ASSIGNEE_TYPE_TABS').map(item => {
|
||||
@@ -178,7 +245,10 @@ export default {
|
||||
});
|
||||
},
|
||||
showAssigneeInConversationCard() {
|
||||
return this.activeAssigneeTab === wootConstants.ASSIGNEE_TYPE.ALL;
|
||||
return (
|
||||
this.hasAppliedFiltersOrActiveFolders ||
|
||||
this.activeAssigneeTab === wootConstants.ASSIGNEE_TYPE.ALL
|
||||
);
|
||||
},
|
||||
inbox() {
|
||||
return this.$store.getters['inboxes/getInbox'](this.activeInbox);
|
||||
@@ -189,7 +259,9 @@ export default {
|
||||
);
|
||||
},
|
||||
currentPageFilterKey() {
|
||||
return this.hasAppliedFilters ? 'appliedFilters' : this.activeAssigneeTab;
|
||||
return this.hasAppliedFiltersOrActiveFolders
|
||||
? 'appliedFilters'
|
||||
: this.activeAssigneeTab;
|
||||
},
|
||||
currentFiltersPage() {
|
||||
return this.$store.getters['conversationPage/getCurrentPageFilter'](
|
||||
@@ -212,6 +284,7 @@ export default {
|
||||
conversationType: this.conversationType
|
||||
? this.conversationType
|
||||
: undefined,
|
||||
folders: this.hasActiveFolders ? this.savedFoldersValue : undefined,
|
||||
};
|
||||
},
|
||||
pageTitle() {
|
||||
@@ -227,11 +300,14 @@ export default {
|
||||
if (this.conversationType === 'mention') {
|
||||
return this.$t('CHAT_LIST.MENTION_HEADING');
|
||||
}
|
||||
if (this.hasActiveFolders) {
|
||||
return this.activeFolder.name;
|
||||
}
|
||||
return this.$t('CHAT_LIST.TAB_HEADING');
|
||||
},
|
||||
conversationList() {
|
||||
let conversationList = [];
|
||||
if (!this.hasAppliedFilters) {
|
||||
if (!this.hasAppliedFiltersOrActiveFolders) {
|
||||
const filters = this.conversationFilters;
|
||||
if (this.activeAssigneeTab === 'me') {
|
||||
conversationList = [...this.mineChatsList(filters)];
|
||||
@@ -246,6 +322,16 @@ export default {
|
||||
|
||||
return conversationList;
|
||||
},
|
||||
activeFolder() {
|
||||
if (this.foldersId) {
|
||||
const activeView = this.folders.filter(
|
||||
view => view.id === Number(this.foldersId)
|
||||
);
|
||||
const [firstValue] = activeView;
|
||||
return firstValue;
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
activeTeam() {
|
||||
if (this.teamId) {
|
||||
return this.$store.getters['teams/getTeam'](this.teamId);
|
||||
@@ -266,6 +352,11 @@ export default {
|
||||
conversationType() {
|
||||
this.resetAndFetchData();
|
||||
},
|
||||
activeFolder() {
|
||||
if (!this.hasAppliedFilters) {
|
||||
this.resetAndFetchData();
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.$store.dispatch('setChatFilter', this.activeStatus);
|
||||
@@ -280,10 +371,23 @@ export default {
|
||||
if (this.$route.name !== 'home') {
|
||||
this.$router.push({ name: 'home' });
|
||||
}
|
||||
this.foldersQuery = filterQueryGenerator(payload);
|
||||
this.$store.dispatch('conversationPage/reset');
|
||||
this.$store.dispatch('emptyAllConversations');
|
||||
this.fetchFilteredConversations(payload);
|
||||
},
|
||||
onClickOpenAddFoldersModal() {
|
||||
this.showAddFoldersModal = true;
|
||||
},
|
||||
onCloseAddFoldersModal() {
|
||||
this.showAddFoldersModal = false;
|
||||
},
|
||||
onClickOpenDeleteFoldersModal() {
|
||||
this.showDeleteFoldersModal = true;
|
||||
},
|
||||
onCloseDeleteFoldersModal() {
|
||||
this.showDeleteFoldersModal = false;
|
||||
},
|
||||
onToggleAdvanceFiltersModal() {
|
||||
this.showAdvancedFilters = !this.showAdvancedFilters;
|
||||
},
|
||||
@@ -335,6 +439,13 @@ export default {
|
||||
this.$store.dispatch('conversationPage/reset');
|
||||
this.$store.dispatch('emptyAllConversations');
|
||||
this.$store.dispatch('clearConversationFilters');
|
||||
if (this.hasActiveFolders) {
|
||||
const payload = this.activeFolder.query;
|
||||
this.fetchSavedFilteredConversations(payload);
|
||||
}
|
||||
if (this.foldersId) {
|
||||
return;
|
||||
}
|
||||
this.fetchConversations();
|
||||
},
|
||||
fetchConversations() {
|
||||
@@ -343,9 +454,14 @@ export default {
|
||||
.then(() => this.$emit('conversation-load'));
|
||||
},
|
||||
loadMoreConversations() {
|
||||
if (!this.hasAppliedFilters) {
|
||||
if (!this.hasAppliedFiltersOrActiveFolders) {
|
||||
this.fetchConversations();
|
||||
} else {
|
||||
}
|
||||
if (this.hasActiveFolders) {
|
||||
const payload = this.activeFolder.query;
|
||||
this.fetchSavedFilteredConversations(payload);
|
||||
}
|
||||
if (this.hasAppliedFilters) {
|
||||
this.fetchFilteredConversations(this.appliedFilters);
|
||||
}
|
||||
},
|
||||
@@ -359,6 +475,15 @@ export default {
|
||||
.then(() => this.$emit('conversation-load'));
|
||||
this.showAdvancedFilters = false;
|
||||
},
|
||||
fetchSavedFilteredConversations(payload) {
|
||||
let page = this.currentFiltersPage + 1;
|
||||
this.$store
|
||||
.dispatch('fetchFilteredConversations', {
|
||||
queryData: payload,
|
||||
page,
|
||||
})
|
||||
.then(() => this.$emit('conversation-load'));
|
||||
},
|
||||
updateAssigneeTab(selectedTab) {
|
||||
if (this.activeAssigneeTab !== selectedTab) {
|
||||
bus.$emit('clearSearchInput');
|
||||
@@ -374,6 +499,22 @@ export default {
|
||||
this.resetAndFetchData();
|
||||
}
|
||||
},
|
||||
openLastSavedItemInFolder() {
|
||||
const lastItemOfFolder = this.folders[this.folders.length - 1];
|
||||
const lastItemId = lastItemOfFolder.id;
|
||||
this.$router.push({
|
||||
name: 'folder_conversations',
|
||||
params: { id: lastItemId },
|
||||
});
|
||||
},
|
||||
openLastItemAfterDeleteInFolder() {
|
||||
if (this.folders.length > 0) {
|
||||
this.openLastSavedItemInFolder();
|
||||
} else {
|
||||
this.$router.push({ name: 'home' });
|
||||
this.fetchConversations();
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -413,7 +554,11 @@ export default {
|
||||
}
|
||||
|
||||
.filter__applied {
|
||||
padding: var(--space-slab) 0 !important;
|
||||
padding: 0 0 var(--space-slab) 0 !important;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.delete-custom-view__button {
|
||||
margin-right: var(--space-normal);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
rel="noopener noreferrer"
|
||||
class="value"
|
||||
>
|
||||
{{ value || '---' }}
|
||||
{{ urlValue }}
|
||||
</a>
|
||||
<p v-else class="value">
|
||||
{{ displayValue || '---' }}
|
||||
@@ -119,7 +119,7 @@ import format from 'date-fns/format';
|
||||
import { required, url } from 'vuelidate/lib/validators';
|
||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||
import MultiselectDropdown from 'shared/components/ui/MultiselectDropdown.vue';
|
||||
|
||||
import { isValidURL } from '../helper/URLHelper';
|
||||
const DATE_FORMAT = 'yyyy-MM-dd';
|
||||
|
||||
export default {
|
||||
@@ -141,16 +141,6 @@ export default {
|
||||
editedValue: null,
|
||||
};
|
||||
},
|
||||
validations() {
|
||||
if (this.isAttributeTypeLink) {
|
||||
return {
|
||||
editedValue: { required, url },
|
||||
};
|
||||
}
|
||||
return {
|
||||
editedValue: { required },
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
formattedValue() {
|
||||
@@ -184,6 +174,9 @@ export default {
|
||||
isAttributeTypeDate() {
|
||||
return this.attributeType === 'date';
|
||||
},
|
||||
urlValue() {
|
||||
return isValidURL(this.value) ? this.value : '---';
|
||||
},
|
||||
notAttributeTypeCheckboxAndList() {
|
||||
return !this.isAttributeTypeCheckbox && !this.isAttributeTypeList;
|
||||
},
|
||||
@@ -206,6 +199,23 @@ export default {
|
||||
return this.editedValue;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
value() {
|
||||
this.isEditing = false;
|
||||
this.editedValue = this.value;
|
||||
},
|
||||
},
|
||||
|
||||
validations() {
|
||||
if (this.isAttributeTypeLink) {
|
||||
return {
|
||||
editedValue: { required, url },
|
||||
};
|
||||
}
|
||||
return {
|
||||
editedValue: { required },
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.editedValue = this.formattedValue;
|
||||
bus.$on(BUS_EVENTS.FOCUS_CUSTOM_ATTRIBUTE, focusAttributeKey => {
|
||||
|
||||
@@ -4,7 +4,12 @@
|
||||
<div class="ui-notification">
|
||||
<fluent-icon icon="wifi-off" />
|
||||
<p class="ui-notification-text">
|
||||
{{ $t('NETWORK.NOTIFICATION.TEXT') }}
|
||||
{{
|
||||
useInstallationName(
|
||||
$t('NETWORK.NOTIFICATION.TEXT'),
|
||||
globalConfig.installationName
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
<woot-button variant="clear" size="small" @click="refreshPage">
|
||||
{{ $t('NETWORK.BUTTON.REFRESH') }}
|
||||
@@ -23,15 +28,28 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||
|
||||
export default {
|
||||
mixins: [globalConfigMixin],
|
||||
|
||||
data() {
|
||||
return {
|
||||
showNotification: !navigator.onLine,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters({ globalConfig: 'globalConfig/get' }),
|
||||
},
|
||||
|
||||
mounted() {
|
||||
window.addEventListener('offline', this.updateOnlineStatus);
|
||||
window.bus.$on(BUS_EVENTS.WEBSOCKET_DISCONNECT, () => {
|
||||
this.updateOnlineStatus({ type: 'offline' });
|
||||
});
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
|
||||
@@ -4,6 +4,11 @@
|
||||
<div class="ui-snackbar-text">
|
||||
{{ message }}
|
||||
</div>
|
||||
<div v-if="action" class="ui-snackbar-action">
|
||||
<router-link v-if="action.type == 'link'" :to="action.to">
|
||||
{{ action.message }}
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -12,6 +17,10 @@
|
||||
export default {
|
||||
props: {
|
||||
message: { type: String, default: '' },
|
||||
action: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
showButton: Boolean,
|
||||
duration: {
|
||||
type: [String, Number],
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
v-for="snackMessage in snackMessages"
|
||||
:key="snackMessage.key"
|
||||
:message="snackMessage.message"
|
||||
:action="snackMessage.action"
|
||||
/>
|
||||
</transition-group>
|
||||
</template>
|
||||
@@ -35,8 +36,12 @@ export default {
|
||||
bus.$off('newToastMessage', this.onNewToastMessage);
|
||||
},
|
||||
methods: {
|
||||
onNewToastMessage(message) {
|
||||
this.snackMessages.push({ key: new Date().getTime(), message });
|
||||
onNewToastMessage(message, action) {
|
||||
this.snackMessages.push({
|
||||
key: new Date().getTime(),
|
||||
message,
|
||||
action,
|
||||
});
|
||||
window.setTimeout(() => {
|
||||
this.snackMessages.splice(0, 1);
|
||||
}, this.duration);
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
<template>
|
||||
<banner
|
||||
v-if="shouldShowBanner"
|
||||
class="update-banner"
|
||||
color-scheme="primary"
|
||||
:banner-message="bannerMessage"
|
||||
href-link="https://github.com/chatwoot/chatwoot/releases"
|
||||
:href-link-text="$t('GENERAL_SETTINGS.LEARN_MORE')"
|
||||
has-close-button
|
||||
@close="dismissUpdateBanner"
|
||||
/>
|
||||
</template>
|
||||
<script>
|
||||
import Banner from 'dashboard/components/ui/Banner.vue';
|
||||
import LocalStorage from '../../helper/localStorage';
|
||||
import { mapGetters } from 'vuex';
|
||||
import adminMixin from 'dashboard/mixins/isAdmin';
|
||||
|
||||
const semver = require('semver');
|
||||
const dismissedUpdates = new LocalStorage('dismissedUpdates');
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Banner,
|
||||
},
|
||||
mixins: [adminMixin],
|
||||
props: {
|
||||
latestChatwootVersion: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({ globalConfig: 'globalConfig/get' }),
|
||||
hasAnUpdateAvailable() {
|
||||
if (!semver.valid(this.latestChatwootVersion)) {
|
||||
return false;
|
||||
}
|
||||
return semver.lt(
|
||||
this.globalConfig.appVersion,
|
||||
this.latestChatwootVersion
|
||||
);
|
||||
},
|
||||
bannerMessage() {
|
||||
return this.$t('GENERAL_SETTINGS.UPDATE_CHATWOOT', {
|
||||
latestChatwootVersion: this.latestChatwootVersion,
|
||||
});
|
||||
},
|
||||
shouldShowBanner() {
|
||||
return (
|
||||
this.globalConfig.displayManifest &&
|
||||
this.hasAnUpdateAvailable &&
|
||||
!this.isVersionNotificationDismissed(this.latestChatwootVersion) &&
|
||||
this.isAdmin
|
||||
);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
isVersionNotificationDismissed(version) {
|
||||
return dismissedUpdates.get().includes(version);
|
||||
},
|
||||
dismissUpdateBanner() {
|
||||
let updatedDismissedItems = dismissedUpdates.get();
|
||||
if (updatedDismissedItems instanceof Array) {
|
||||
updatedDismissedItems.push(this.latestChatwootVersion);
|
||||
} else {
|
||||
updatedDismissedItems = [this.latestChatwootVersion];
|
||||
}
|
||||
dismissedUpdates.store(updatedDismissedItems);
|
||||
this.latestChatwootVersion = this.globalConfig.appVersion;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -52,12 +52,16 @@
|
||||
<woot-dropdown-item v-if="!isPending">
|
||||
<woot-button
|
||||
variant="clear"
|
||||
color-scheme="secondary"
|
||||
size="small"
|
||||
icon="book-clock"
|
||||
@click="() => toggleStatus(STATUS_TYPE.PENDING)"
|
||||
>
|
||||
{{ this.$t('CONVERSATION.RESOLVE_DROPDOWN.MARK_PENDING') }}
|
||||
</woot-button>
|
||||
</woot-dropdown-item>
|
||||
|
||||
<woot-dropdown-divider v-if="isOpen" />
|
||||
<woot-dropdown-sub-menu
|
||||
v-if="isOpen"
|
||||
:title="this.$t('CONVERSATION.RESOLVE_DROPDOWN.SNOOZE.TITLE')"
|
||||
@@ -65,6 +69,9 @@
|
||||
<woot-dropdown-item>
|
||||
<woot-button
|
||||
variant="clear"
|
||||
color-scheme="secondary"
|
||||
size="small"
|
||||
icon="send-clock"
|
||||
@click="() => toggleStatus(STATUS_TYPE.SNOOZED, null)"
|
||||
>
|
||||
{{ this.$t('CONVERSATION.RESOLVE_DROPDOWN.SNOOZE.NEXT_REPLY') }}
|
||||
@@ -73,6 +80,9 @@
|
||||
<woot-dropdown-item>
|
||||
<woot-button
|
||||
variant="clear"
|
||||
color-scheme="secondary"
|
||||
size="small"
|
||||
icon="dual-screen-clock"
|
||||
@click="
|
||||
() => toggleStatus(STATUS_TYPE.SNOOZED, snoozeTimes.tomorrow)
|
||||
"
|
||||
@@ -83,6 +93,9 @@
|
||||
<woot-dropdown-item>
|
||||
<woot-button
|
||||
variant="clear"
|
||||
color-scheme="secondary"
|
||||
size="small"
|
||||
icon="calendar-clock"
|
||||
@click="
|
||||
() => toggleStatus(STATUS_TYPE.SNOOZED, snoozeTimes.nextWeek)
|
||||
"
|
||||
@@ -110,6 +123,8 @@ import {
|
||||
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue';
|
||||
import WootDropdownSubMenu from 'shared/components/ui/dropdown/DropdownSubMenu.vue';
|
||||
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue';
|
||||
import WootDropdownDivider from 'shared/components/ui/dropdown/DropdownDivider';
|
||||
|
||||
import wootConstants from '../../constants';
|
||||
import {
|
||||
getUnixTime,
|
||||
@@ -129,6 +144,7 @@ export default {
|
||||
WootDropdownItem,
|
||||
WootDropdownMenu,
|
||||
WootDropdownSubMenu,
|
||||
WootDropdownDivider,
|
||||
},
|
||||
mixins: [clickaway, alertMixin, eventListenerMixins],
|
||||
props: { conversationId: { type: [String, Number], required: true } },
|
||||
@@ -269,5 +285,6 @@ export default {
|
||||
margin-top: var(--space-micro);
|
||||
right: 0;
|
||||
max-width: 20rem;
|
||||
min-width: 15.6rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -21,6 +21,7 @@ import SubmitButton from './buttons/FormSubmitButton';
|
||||
import Tabs from './ui/Tabs/Tabs';
|
||||
import TabsItem from './ui/Tabs/TabsItem';
|
||||
import Thumbnail from './widgets/Thumbnail.vue';
|
||||
import ConfirmModal from './widgets/modal/ConfirmationModal.vue';
|
||||
|
||||
const WootUIKit = {
|
||||
AvatarUploader,
|
||||
@@ -45,6 +46,7 @@ const WootUIKit = {
|
||||
Tabs,
|
||||
TabsItem,
|
||||
Thumbnail,
|
||||
ConfirmModal,
|
||||
install(Vue) {
|
||||
const keys = Object.keys(this);
|
||||
keys.pop(); // remove 'install' from keys
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<aside class="woot-sidebar">
|
||||
<primary-sidebar
|
||||
:logo-source="globalConfig.logo"
|
||||
:logo-source="globalConfig.logoThumbnail"
|
||||
:installation-name="globalConfig.installationName"
|
||||
:account-id="accountId"
|
||||
:menu-items="primaryMenuItems"
|
||||
@@ -14,27 +14,11 @@
|
||||
:inboxes="inboxes"
|
||||
:labels="labels"
|
||||
:teams="teams"
|
||||
:custom-views="customViews"
|
||||
:menu-config="activeSecondaryMenu"
|
||||
:current-role="currentRole"
|
||||
@add-label="showAddLabelPopup"
|
||||
/>
|
||||
<woot-key-shortcut-modal
|
||||
v-if="showShortcutModal"
|
||||
@close="closeKeyShortcutModal"
|
||||
@clickaway="closeKeyShortcutModal"
|
||||
/>
|
||||
<account-selector
|
||||
:show-account-modal="showAccountModal"
|
||||
@close-account-modal="toggleAccountModal"
|
||||
@show-create-account-modal="openCreateAccountModal"
|
||||
/>
|
||||
<add-account-modal
|
||||
:show="showCreateAccountModal"
|
||||
@close-account-create-modal="closeCreateAccountModal"
|
||||
/>
|
||||
<woot-modal :show.sync="showAddLabelModal" :on-close="hideAddLabelPopup">
|
||||
<add-label-modal @close="hideAddLabelPopup" />
|
||||
</woot-modal>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
@@ -45,12 +29,8 @@ import adminMixin from '../../mixins/isAdmin';
|
||||
import { getSidebarItems } from './config/default-sidebar';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
|
||||
import AccountSelector from './sidebarComponents/AccountSelector.vue';
|
||||
import AddAccountModal from './sidebarComponents/AddAccountModal.vue';
|
||||
import AddLabelModal from '../../routes/dashboard/settings/labels/AddLabel';
|
||||
import PrimarySidebar from './sidebarComponents/Primary';
|
||||
import SecondarySidebar from './sidebarComponents/Secondary';
|
||||
import WootKeyShortcutModal from 'components/widgets/modal/WootKeyShortcutModal';
|
||||
import {
|
||||
hasPressedAltAndCKey,
|
||||
hasPressedAltAndRKey,
|
||||
@@ -64,21 +44,13 @@ import router from '../../routes';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
AccountSelector,
|
||||
AddAccountModal,
|
||||
AddLabelModal,
|
||||
PrimarySidebar,
|
||||
SecondarySidebar,
|
||||
WootKeyShortcutModal,
|
||||
},
|
||||
mixins: [adminMixin, alertMixin, eventListenerMixins],
|
||||
data() {
|
||||
return {
|
||||
showOptionsMenu: false,
|
||||
showAccountModal: false,
|
||||
showCreateAccountModal: false,
|
||||
showAddLabelModal: false,
|
||||
showShortcutModal: false,
|
||||
};
|
||||
},
|
||||
|
||||
@@ -92,6 +64,26 @@ export default {
|
||||
labels: 'labels/getLabelsOnSidebar',
|
||||
teams: 'teams/getMyTeams',
|
||||
}),
|
||||
activeCustomView() {
|
||||
if (this.activePrimaryMenu.key === 'contacts') {
|
||||
return 'contact';
|
||||
}
|
||||
if (this.activePrimaryMenu.key === 'conversations') {
|
||||
return 'conversation';
|
||||
}
|
||||
return '';
|
||||
},
|
||||
customViews() {
|
||||
return this.$store.getters['customViews/getCustomViewsByFilterType'](
|
||||
this.activeCustomView
|
||||
);
|
||||
},
|
||||
isConversationOrContactActive() {
|
||||
return (
|
||||
this.activePrimaryMenu.key === 'contacts' ||
|
||||
this.activePrimaryMenu.key === 'conversations'
|
||||
);
|
||||
},
|
||||
sideMenuConfig() {
|
||||
return getSidebarItems(this.accountId);
|
||||
},
|
||||
@@ -119,20 +111,32 @@ export default {
|
||||
return activePrimaryMenu;
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
activeCustomView() {
|
||||
this.fetchCustomViews();
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.$store.dispatch('labels/get');
|
||||
this.$store.dispatch('inboxes/get');
|
||||
this.$store.dispatch('notifications/unReadCount');
|
||||
this.$store.dispatch('teams/get');
|
||||
this.$store.dispatch('attributes/get');
|
||||
this.fetchCustomViews();
|
||||
},
|
||||
|
||||
methods: {
|
||||
fetchCustomViews() {
|
||||
if (this.isConversationOrContactActive) {
|
||||
this.$store.dispatch('customViews/get', this.activeCustomView);
|
||||
}
|
||||
},
|
||||
toggleKeyShortcutModal() {
|
||||
this.showShortcutModal = true;
|
||||
this.$emit('open-key-shortcut-modal');
|
||||
},
|
||||
closeKeyShortcutModal() {
|
||||
this.showShortcutModal = false;
|
||||
this.$emit('close-key-shortcut-modal');
|
||||
},
|
||||
handleKeyEvents(e) {
|
||||
if (hasPressedCommandAndForwardSlash(e)) {
|
||||
@@ -167,20 +171,10 @@ export default {
|
||||
window.$chatwoot.toggle();
|
||||
},
|
||||
toggleAccountModal() {
|
||||
this.showAccountModal = !this.showAccountModal;
|
||||
},
|
||||
openCreateAccountModal() {
|
||||
this.showAccountModal = false;
|
||||
this.showCreateAccountModal = true;
|
||||
},
|
||||
closeCreateAccountModal() {
|
||||
this.showCreateAccountModal = false;
|
||||
this.$emit('toggle-account-modal');
|
||||
},
|
||||
showAddLabelPopup() {
|
||||
this.showAddLabelModal = true;
|
||||
},
|
||||
hideAddLabelPopup() {
|
||||
this.showAddLabelModal = false;
|
||||
this.$emit('show-add-label-popup');
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -190,16 +184,8 @@ export default {
|
||||
.woot-sidebar {
|
||||
background: var(--white);
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.secondary-menu {
|
||||
background: var(--white);
|
||||
border-right: 1px solid var(--s-50);
|
||||
height: 100vh;
|
||||
width: 19rem;
|
||||
flex-shrink: 0;
|
||||
overflow: auto;
|
||||
padding: var(--space-small);
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ const contacts = accountId => ({
|
||||
routes: [
|
||||
'contacts_dashboard',
|
||||
'contact_profile_dashboard',
|
||||
'contacts_segments_dashboard',
|
||||
'contacts_labels_dashboard',
|
||||
],
|
||||
menuItems: [
|
||||
|
||||
@@ -14,6 +14,8 @@ const conversations = accountId => ({
|
||||
'conversations_through_team',
|
||||
'conversation_mentions',
|
||||
'conversation_through_mentions',
|
||||
'folder_conversations',
|
||||
'conversations_through_folders',
|
||||
],
|
||||
menuItems: [
|
||||
{
|
||||
|
||||
@@ -70,7 +70,7 @@ const settings = accountId => ({
|
||||
toStateName: 'attributes_list',
|
||||
},
|
||||
{
|
||||
icon: 'autocorrect',
|
||||
icon: 'automation',
|
||||
label: 'AUTOMATION',
|
||||
hasSubMenu: false,
|
||||
toState: frontendURL(`accounts/${accountId}/settings/automation/list`),
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
<template>
|
||||
<div v-if="showShowCurrentAccountContext" class="account-context--group">
|
||||
{{ $t('SIDEBAR.CURRENTLY_VIEWING_ACCOUNT') }}
|
||||
<p class="account-context--name text-ellipsis">
|
||||
{{ account.name }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
export default {
|
||||
computed: {
|
||||
...mapGetters({
|
||||
account: 'getCurrentAccount',
|
||||
userAccounts: 'getUserAccounts',
|
||||
}),
|
||||
showShowCurrentAccountContext() {
|
||||
return this.userAccounts.length > 1 && this.account.name;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.account-context--group {
|
||||
border-radius: var(--border-radius-normal);
|
||||
border: 1px solid var(--color-border);
|
||||
font-size: var(--font-size-mini);
|
||||
padding: var(--space-small);
|
||||
margin-bottom: var(--space-small);
|
||||
|
||||
.account-context--name {
|
||||
font-weight: var(--font-weight-medium);
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -54,7 +54,7 @@
|
||||
:class="{ 'is-active': isActive }"
|
||||
@click="e => handleProfileSettingClick(e, navigate)"
|
||||
>
|
||||
<fluent-icon icon="person" class="icon icon--font" />
|
||||
<fluent-icon icon="person" size="14" class="icon icon--font" />
|
||||
<span class="button__content">
|
||||
{{ $t('SIDEBAR_ITEMS.PROFILE_SETTINGS') }}
|
||||
</span>
|
||||
|
||||
@@ -93,7 +93,7 @@ export default {
|
||||
width: var(--space-jumbo);
|
||||
border-right: 1px solid var(--s-50);
|
||||
box-sizing: content-box;
|
||||
height: 100vh;
|
||||
height: 100%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<div v-if="hasSecondaryMenu" class="main-nav secondary-menu">
|
||||
<account-context />
|
||||
<transition-group name="menu-list" tag="ul" class="menu vertical">
|
||||
<secondary-nav-item
|
||||
v-for="menuItem in accessibleMenuItems"
|
||||
@@ -18,9 +19,11 @@
|
||||
<script>
|
||||
import { frontendURL } from '../../../helper/URLHelper';
|
||||
import SecondaryNavItem from './SecondaryNavItem.vue';
|
||||
import AccountContext from './AccountContext.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
AccountContext,
|
||||
SecondaryNavItem,
|
||||
},
|
||||
props: {
|
||||
@@ -40,6 +43,10 @@ export default {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
customViews: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
menuConfig: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
@@ -53,6 +60,9 @@ export default {
|
||||
hasSecondaryMenu() {
|
||||
return this.menuConfig.menuItems && this.menuConfig.menuItems.length;
|
||||
},
|
||||
contactCustomViews() {
|
||||
return this.customViews.filter(view => view.filter_type === 'contact');
|
||||
},
|
||||
accessibleMenuItems() {
|
||||
if (!this.currentRole) {
|
||||
return [];
|
||||
@@ -150,14 +160,57 @@ export default {
|
||||
})),
|
||||
};
|
||||
},
|
||||
foldersSection() {
|
||||
return {
|
||||
icon: 'folder',
|
||||
label: 'CUSTOM_VIEWS_FOLDER',
|
||||
hasSubMenu: true,
|
||||
key: 'custom_view',
|
||||
children: this.customViews
|
||||
.filter(view => view.filter_type === 'conversation')
|
||||
.map(view => ({
|
||||
id: view.id,
|
||||
label: view.name,
|
||||
truncateLabel: true,
|
||||
toState: frontendURL(
|
||||
`accounts/${this.accountId}/custom_view/${view.id}`
|
||||
),
|
||||
})),
|
||||
};
|
||||
},
|
||||
contactSegmentsSection() {
|
||||
return {
|
||||
icon: 'folder',
|
||||
label: 'CUSTOM_VIEWS_SEGMENTS',
|
||||
hasSubMenu: true,
|
||||
key: 'custom_view',
|
||||
children: this.customViews
|
||||
.filter(view => view.filter_type === 'contact')
|
||||
.map(view => ({
|
||||
id: view.id,
|
||||
label: view.name,
|
||||
truncateLabel: true,
|
||||
toState: frontendURL(
|
||||
`accounts/${this.accountId}/contacts/custom_view/${view.id}`
|
||||
),
|
||||
})),
|
||||
};
|
||||
},
|
||||
additionalSecondaryMenuItems() {
|
||||
let conversationMenuItems = [this.inboxSection, this.labelSection];
|
||||
let contactMenuItems = [this.contactLabelSection];
|
||||
if (this.teams.length) {
|
||||
conversationMenuItems = [this.teamSection, ...conversationMenuItems];
|
||||
}
|
||||
if (this.customViews.length) {
|
||||
conversationMenuItems = [this.foldersSection, ...conversationMenuItems];
|
||||
}
|
||||
if (this.contactCustomViews.length) {
|
||||
contactMenuItems = [this.contactSegmentsSection, ...contactMenuItems];
|
||||
}
|
||||
return {
|
||||
conversations: conversationMenuItems,
|
||||
contacts: [this.contactLabelSection],
|
||||
contacts: contactMenuItems,
|
||||
};
|
||||
},
|
||||
},
|
||||
@@ -168,3 +221,18 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.secondary-menu {
|
||||
background: var(--white);
|
||||
border-right: 1px solid var(--s-50);
|
||||
height: 100%;
|
||||
width: 19rem;
|
||||
flex-shrink: 0;
|
||||
overflow: hidden;
|
||||
padding: var(--space-small);
|
||||
|
||||
&:hover {
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
v-else
|
||||
class="secondary-menu--title secondary-menu--link fs-small"
|
||||
:class="computedClass"
|
||||
:to="menuItem.toState"
|
||||
:to="menuItem && menuItem.toState"
|
||||
>
|
||||
<fluent-icon
|
||||
:icon="menuItem.icon"
|
||||
@@ -15,6 +15,14 @@
|
||||
size="14"
|
||||
/>
|
||||
{{ $t(`SIDEBAR.${menuItem.label}`) }}
|
||||
<span
|
||||
v-if="menuItem.label === 'AUTOMATION'"
|
||||
data-view-component="true"
|
||||
label="Beta"
|
||||
class="beta"
|
||||
>
|
||||
{{ $t('SIDEBAR.BETA') }}
|
||||
</span>
|
||||
</router-link>
|
||||
|
||||
<ul v-if="hasSubMenu" class="nested vertical menu">
|
||||
@@ -73,14 +81,48 @@ export default {
|
||||
hasSubMenu() {
|
||||
return !!this.menuItem.children;
|
||||
},
|
||||
isInboxConversation() {
|
||||
return (
|
||||
this.$store.state.route.name === 'inbox_conversation' &&
|
||||
this.menuItem.toStateName === 'home'
|
||||
);
|
||||
},
|
||||
isTeamsSettings() {
|
||||
return (
|
||||
this.$store.state.route.name === 'settings_teams_edit' &&
|
||||
this.menuItem.toStateName === 'settings_teams_list'
|
||||
);
|
||||
},
|
||||
isInboxsSettings() {
|
||||
return (
|
||||
this.$store.state.route.name === 'settings_inbox_show' &&
|
||||
this.menuItem.toStateName === 'settings_inbox_list'
|
||||
);
|
||||
},
|
||||
isIntegrationsSettings() {
|
||||
return (
|
||||
this.$store.state.route.name === 'settings_integrations_webhook' &&
|
||||
this.menuItem.toStateName === 'settings_integrations'
|
||||
);
|
||||
},
|
||||
isApplicationsSettings() {
|
||||
return (
|
||||
this.$store.state.route.name === 'settings_applications_integration' &&
|
||||
this.menuItem.toStateName === 'settings_applications'
|
||||
);
|
||||
},
|
||||
|
||||
computedClass() {
|
||||
// If active Inbox is present
|
||||
// donot highlight conversations
|
||||
if (this.activeInbox) return ' ';
|
||||
|
||||
if (
|
||||
this.$store.state.route.name === 'inbox_conversation' &&
|
||||
this.menuItem.toStateName === 'home'
|
||||
this.isInboxConversation ||
|
||||
this.isTeamsSettings ||
|
||||
this.isInboxsSettings ||
|
||||
this.isIntegrationsSettings ||
|
||||
this.isApplicationsSettings
|
||||
) {
|
||||
return 'is-active';
|
||||
}
|
||||
@@ -187,4 +229,17 @@ export default {
|
||||
color: var(--w-500);
|
||||
}
|
||||
}
|
||||
.beta {
|
||||
padding-right: var(--space-smaller) !important;
|
||||
padding-left: var(--space-smaller) !important;
|
||||
margin-left: var(--space-half) !important;
|
||||
display: inline-block;
|
||||
font-size: var(--font-size-micro);
|
||||
font-weight: var(--font-weight-medium);
|
||||
line-height: 18px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 2em;
|
||||
color: var(--g-800);
|
||||
border-color: var(--g-700);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
<template>
|
||||
<div class="banner" :class="bannerClasses">
|
||||
<span class="banner-message">
|
||||
{{ bannerMessage }}
|
||||
<a
|
||||
v-if="hrefLink"
|
||||
:href="hrefLink"
|
||||
rel="noopener noreferrer nofollow"
|
||||
target="_blank"
|
||||
>
|
||||
{{ hrefLinkText }}
|
||||
</a>
|
||||
</span>
|
||||
<woot-button
|
||||
v-if="hasActionButton"
|
||||
size="small"
|
||||
variant="link"
|
||||
icon="arrow-right"
|
||||
color-scheme="primary"
|
||||
class-names="banner-action__button"
|
||||
@click="onClick"
|
||||
>
|
||||
{{ actionButtonLabel }}
|
||||
</woot-button>
|
||||
<woot-button
|
||||
v-if="hasCloseButton"
|
||||
size="small"
|
||||
variant="link"
|
||||
color-scheme="secondary"
|
||||
icon="dismiss-circle"
|
||||
class-names="banner-action__button"
|
||||
@click="onClickClose"
|
||||
>
|
||||
</woot-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
bannerMessage: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
hrefLink: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
hrefLinkText: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
hasActionButton: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
actionButtonLabel: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
colorScheme: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
hasCloseButton: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
bannerClasses() {
|
||||
return [this.colorScheme];
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onClick(e) {
|
||||
this.$emit('click', e);
|
||||
},
|
||||
onClickClose(e) {
|
||||
this.$emit('close', e);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.banner {
|
||||
display: flex;
|
||||
color: var(--white);
|
||||
font-size: var(--font-size-mini);
|
||||
padding: var(--space-slab) var(--space-normal);
|
||||
justify-content: center;
|
||||
position: sticky;
|
||||
|
||||
&.primary {
|
||||
background: var(--w-500);
|
||||
.banner-action__button {
|
||||
color: var(--white);
|
||||
}
|
||||
}
|
||||
|
||||
&.secondary {
|
||||
background: var(--s-200);
|
||||
color: var(--s-800);
|
||||
a {
|
||||
color: var(--s-800);
|
||||
}
|
||||
}
|
||||
|
||||
&.alert {
|
||||
background: var(--r-400);
|
||||
}
|
||||
|
||||
&.warning {
|
||||
background: var(--y-800);
|
||||
color: var(--s-600);
|
||||
a {
|
||||
color: var(--s-600);
|
||||
}
|
||||
}
|
||||
|
||||
&.gray {
|
||||
background: var(--b-500);
|
||||
.banner-action__button {
|
||||
color: var(--white);
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
margin-left: var(--space-smaller);
|
||||
text-decoration: underline;
|
||||
color: var(--white);
|
||||
font-size: var(--font-size-mini);
|
||||
}
|
||||
|
||||
.banner-action__button {
|
||||
margin: 0 var(--space-smaller);
|
||||
|
||||
::v-deep .button__content {
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.banner-message {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -30,7 +30,7 @@ export default {
|
||||
},
|
||||
value: {
|
||||
type: Date,
|
||||
default: () => [],
|
||||
default: [],
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="preview-item__wrap">
|
||||
<div
|
||||
v-for="(attachment, index) in attachments"
|
||||
:key="attachment.id"
|
||||
@@ -7,7 +7,7 @@
|
||||
>
|
||||
<div class="thumb-wrap">
|
||||
<img
|
||||
v-if="isTypeImage(attachment.resource.type)"
|
||||
v-if="isTypeImage(attachment.resource)"
|
||||
class="image-thumb"
|
||||
:src="attachment.thumb"
|
||||
/>
|
||||
@@ -15,16 +15,17 @@
|
||||
</div>
|
||||
<div class="file-name-wrap">
|
||||
<span class="item">
|
||||
{{ attachment.resource.name }}
|
||||
{{ fileName(attachment.resource) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="file-size-wrap">
|
||||
<span class="item">
|
||||
{{ formatFileSize(attachment.resource.size) }}
|
||||
<span class="item text-truncate">
|
||||
{{ formatFileSize(attachment.resource) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="remove-file-wrap">
|
||||
<woot-button
|
||||
v-if="!isTypeAudio(attachment.resource)"
|
||||
class="remove--attachment clear secondary"
|
||||
icon="dismiss"
|
||||
@click="() => onRemoveAttachment(index)"
|
||||
@@ -50,25 +51,42 @@ export default {
|
||||
onRemoveAttachment(index) {
|
||||
this.removeAttachment(index);
|
||||
},
|
||||
formatFileSize(size) {
|
||||
formatFileSize(file) {
|
||||
const size = file.byte_size || file.size;
|
||||
return formatBytes(size, 0);
|
||||
},
|
||||
isTypeImage(type) {
|
||||
isTypeImage(file) {
|
||||
const type = file.content_type || file.type;
|
||||
return type.includes('image');
|
||||
},
|
||||
isTypeAudio(file) {
|
||||
const type = file.content_type || file.type;
|
||||
return type.includes('audio');
|
||||
},
|
||||
fileName(file) {
|
||||
return file.filename || file.name;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.preview-item__wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: auto;
|
||||
margin-top: var(--space-normal);
|
||||
max-height: 20rem;
|
||||
}
|
||||
|
||||
.preview-item {
|
||||
display: flex;
|
||||
padding: var(--space-slab) 0 0;
|
||||
background: var(--color-background-light);
|
||||
background: var(--b-50);
|
||||
border-radius: var(--border-radius-normal);
|
||||
width: fit-content;
|
||||
width: 24rem;
|
||||
padding: var(--space-smaller);
|
||||
margin-top: var(--space-normal);
|
||||
margin-bottom: var(--space-one);
|
||||
}
|
||||
|
||||
.thumb-wrap {
|
||||
@@ -104,6 +122,7 @@ export default {
|
||||
|
||||
> .item {
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
font-size: var(--font-size-mini);
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
@@ -114,7 +133,8 @@ export default {
|
||||
}
|
||||
|
||||
.file-name-wrap {
|
||||
max-width: 50%;
|
||||
max-width: 60%;
|
||||
min-width: 50%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin-left: var(--space-small);
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
<template>
|
||||
<div
|
||||
class="filter"
|
||||
:class="{ error: v.action_params.$dirty && v.action_params.$error }"
|
||||
>
|
||||
<div class="filter-inputs">
|
||||
<select
|
||||
v-model="action_name"
|
||||
class="action__question"
|
||||
@change="resetFilter()"
|
||||
>
|
||||
<option
|
||||
v-for="attribute in actionTypes"
|
||||
:key="attribute.key"
|
||||
:value="attribute.key"
|
||||
>
|
||||
{{ attribute.label }}
|
||||
</option>
|
||||
</select>
|
||||
<div class="filter__answer--wrap">
|
||||
<div class="multiselect-wrap--small">
|
||||
<multiselect
|
||||
v-model="action_params"
|
||||
track-by="id"
|
||||
label="name"
|
||||
:placeholder="'Select'"
|
||||
:multiple="true"
|
||||
selected-label
|
||||
:select-label="$t('FORMS.MULTISELECT.ENTER_TO_SELECT')"
|
||||
deselect-label=""
|
||||
:max-height="160"
|
||||
:options="dropdownValues"
|
||||
:allow-empty="false"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<woot-button
|
||||
icon="dismiss"
|
||||
variant="clear"
|
||||
color-scheme="secondary"
|
||||
@click="removeAction"
|
||||
/>
|
||||
</div>
|
||||
<p
|
||||
v-if="v.action_params.$dirty && v.action_params.$error"
|
||||
class="filter-error"
|
||||
>
|
||||
{{ $t('FILTER.EMPTY_VALUE_ERROR') }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
value: {
|
||||
type: Object,
|
||||
default: () => null,
|
||||
},
|
||||
actionTypes: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
dropdownValues: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
v: {
|
||||
type: Object,
|
||||
default: () => null,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
action_name: {
|
||||
get() {
|
||||
if (!this.value) return null;
|
||||
return this.value.action_name;
|
||||
},
|
||||
set(value) {
|
||||
const payload = this.value || {};
|
||||
this.$emit('input', { ...payload, action_name: value });
|
||||
},
|
||||
},
|
||||
action_params: {
|
||||
get() {
|
||||
if (!this.value) return null;
|
||||
return this.value.action_params;
|
||||
},
|
||||
set(value) {
|
||||
const payload = this.value || {};
|
||||
this.$emit('input', { ...payload, action_params: value });
|
||||
},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
removeAction() {
|
||||
this.$emit('removeAction');
|
||||
},
|
||||
resetFilter() {
|
||||
this.$emit('resetFilter');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.filter {
|
||||
background: var(--color-background);
|
||||
padding: var(--space-small);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius-medium);
|
||||
margin-bottom: var(--space-small);
|
||||
}
|
||||
|
||||
.filter.error {
|
||||
background: var(--r-50);
|
||||
}
|
||||
|
||||
.filter-inputs {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.filter-error {
|
||||
color: var(--r-500);
|
||||
display: block;
|
||||
margin: var(--space-smaller) 0;
|
||||
}
|
||||
|
||||
.action__question,
|
||||
.filter__operator {
|
||||
margin-bottom: var(--space-zero);
|
||||
margin-right: var(--space-smaller);
|
||||
}
|
||||
|
||||
.action__question {
|
||||
max-width: 50%;
|
||||
}
|
||||
|
||||
.filter__answer--wrap {
|
||||
margin-right: var(--space-smaller);
|
||||
flex-grow: 1;
|
||||
|
||||
input {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
.filter__answer {
|
||||
&.answer--text-input {
|
||||
margin-bottom: var(--space-zero);
|
||||
}
|
||||
}
|
||||
|
||||
.filter__join-operator-wrap {
|
||||
position: relative;
|
||||
z-index: var(--z-index-twenty);
|
||||
margin: var(--space-zero);
|
||||
}
|
||||
|
||||
.filter__join-operator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
margin: var(--space-one) var(--space-zero);
|
||||
|
||||
.operator__line {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.operator__select {
|
||||
position: relative;
|
||||
width: auto;
|
||||
margin-bottom: var(--space-zero) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.multiselect {
|
||||
margin-bottom: var(--space-zero);
|
||||
}
|
||||
</style>
|
||||
@@ -18,11 +18,11 @@ export default {
|
||||
},
|
||||
backgroundColor: {
|
||||
type: String,
|
||||
default: 'white',
|
||||
default: '#c2e1ff',
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: '',
|
||||
default: '#1976cc',
|
||||
},
|
||||
customStyle: {
|
||||
type: Object,
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
export const OPERATOR_TYPES_1 = [
|
||||
{
|
||||
value: 'equal_to',
|
||||
label: 'Equal to',
|
||||
},
|
||||
{
|
||||
value: 'not_equal_to',
|
||||
label: 'Not equal to',
|
||||
},
|
||||
];
|
||||
|
||||
export const OPERATOR_TYPES_2 = [
|
||||
{
|
||||
value: 'equal_to',
|
||||
label: 'Equal to',
|
||||
},
|
||||
{
|
||||
value: 'not_equal_to',
|
||||
label: 'Not equal to',
|
||||
},
|
||||
{
|
||||
value: 'is_present',
|
||||
label: 'Is present',
|
||||
},
|
||||
{
|
||||
value: 'is_not_present',
|
||||
label: 'Is not present',
|
||||
},
|
||||
];
|
||||
|
||||
export const OPERATOR_TYPES_3 = [
|
||||
{
|
||||
value: 'equal_to',
|
||||
label: 'Equal to',
|
||||
},
|
||||
{
|
||||
value: 'not_equal_to',
|
||||
label: 'Not equal to',
|
||||
},
|
||||
{
|
||||
value: 'contains',
|
||||
label: 'Contains',
|
||||
},
|
||||
{
|
||||
value: 'does_not_contain',
|
||||
label: 'Does not contain',
|
||||
},
|
||||
];
|
||||
|
||||
export const OPERATOR_TYPES_4 = [
|
||||
{
|
||||
value: 'equal_to',
|
||||
label: 'Equal to',
|
||||
},
|
||||
{
|
||||
value: 'not_equal_to',
|
||||
label: 'Not equal to',
|
||||
},
|
||||
{
|
||||
value: 'is_present',
|
||||
label: 'Is present',
|
||||
},
|
||||
{
|
||||
value: 'is_not_present',
|
||||
label: 'Is not present',
|
||||
},
|
||||
{
|
||||
value: 'is_greater_than',
|
||||
label: 'Is greater than',
|
||||
},
|
||||
{
|
||||
value: 'is_less_than',
|
||||
label: 'Is less than',
|
||||
},
|
||||
];
|
||||
|
||||
export const OPERATOR_TYPES_5 = [
|
||||
{
|
||||
value: 'is_greater_than',
|
||||
label: 'Is greater than',
|
||||
},
|
||||
{
|
||||
value: 'is_less_than',
|
||||
label: 'Is less than',
|
||||
},
|
||||
{
|
||||
value: 'days_before',
|
||||
label: 'Is x days before',
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,15 @@
|
||||
import {
|
||||
OPERATOR_TYPES_1,
|
||||
OPERATOR_TYPES_2,
|
||||
OPERATOR_TYPES_3,
|
||||
OPERATOR_TYPES_4,
|
||||
} from './FilterOperatorTypes';
|
||||
|
||||
describe('#filterOperators', () => {
|
||||
it('Matches the correct Operators', () => {
|
||||
expect(OPERATOR_TYPES_1).toMatchObject(OPERATOR_TYPES_1);
|
||||
expect(OPERATOR_TYPES_2).toMatchObject(OPERATOR_TYPES_2);
|
||||
expect(OPERATOR_TYPES_3).toMatchObject(OPERATOR_TYPES_3);
|
||||
expect(OPERATOR_TYPES_4).toMatchObject(OPERATOR_TYPES_4);
|
||||
});
|
||||
});
|
||||
+46
-1
@@ -1,8 +1,29 @@
|
||||
<template>
|
||||
<div class="filters">
|
||||
<div class="filter">
|
||||
<div class="filter" :class="{ error: v.values.$dirty && v.values.$error }">
|
||||
<div class="filter-inputs">
|
||||
<select
|
||||
v-if="groupedFilters"
|
||||
v-model="attributeKey"
|
||||
class="filter__question"
|
||||
@change="resetFilter()"
|
||||
>
|
||||
<optgroup
|
||||
v-for="(group, i) in filterGroups"
|
||||
:key="i"
|
||||
:label="group.name"
|
||||
>
|
||||
<option
|
||||
v-for="attribute in group.attributes"
|
||||
:key="attribute.key"
|
||||
:value="attribute.key"
|
||||
>
|
||||
{{ attribute.name }}
|
||||
</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
<select
|
||||
v-else
|
||||
v-model="attributeKey"
|
||||
class="filter__question"
|
||||
@change="resetFilter()"
|
||||
@@ -63,6 +84,14 @@
|
||||
:option-height="104"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="inputType === 'date'" class="multiselect-wrap--small">
|
||||
<input
|
||||
v-model="values"
|
||||
type="date"
|
||||
:editable="false"
|
||||
class="answer--text-input datepicker"
|
||||
/>
|
||||
</div>
|
||||
<input
|
||||
v-else
|
||||
v-model="values"
|
||||
@@ -112,6 +141,10 @@ export default {
|
||||
type: String,
|
||||
default: 'plain_text',
|
||||
},
|
||||
dataType: {
|
||||
type: String,
|
||||
default: 'plain_text',
|
||||
},
|
||||
operators: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
@@ -132,6 +165,14 @@ export default {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
groupedFilters: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
filterGroups: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
attributeKey: {
|
||||
@@ -193,6 +234,10 @@ export default {
|
||||
border-radius: var(--border-radius-medium);
|
||||
}
|
||||
|
||||
.filter.error {
|
||||
background: var(--r-50);
|
||||
}
|
||||
|
||||
.filter-inputs {
|
||||
display: flex;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user