Merge branch 'release/2.2.1'
This commit is contained in:
commit
ed2772dd59
87 changed files with 1306 additions and 348 deletions
11
.rubocop.yml
11
.rubocop.yml
|
@ -26,6 +26,8 @@ Style/FrozenStringLiteralComment:
|
|||
Enabled: false
|
||||
Style/SymbolArray:
|
||||
Enabled: false
|
||||
Style/OpenStructUse:
|
||||
Enabled: false
|
||||
Style/OptionalBooleanParameter:
|
||||
Exclude:
|
||||
- 'app/services/email_templates/db_resolver_service.rb'
|
||||
|
@ -68,14 +70,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
|
||||
|
@ -83,6 +91,8 @@ RSpec/MessageSpies:
|
|||
Enabled: false
|
||||
RSpec/StubbedMock:
|
||||
Enabled: false
|
||||
RSpec/FactoryBot/SyntaxMethods:
|
||||
Enabled: false
|
||||
Naming/VariableNumber:
|
||||
Enabled: false
|
||||
Metrics/MethodLength:
|
||||
|
@ -119,6 +129,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'
|
||||
|
|
285
Gemfile.lock
285
Gemfile.lock
|
@ -56,8 +56,8 @@ GEM
|
|||
activerecord (6.1.4.6)
|
||||
activemodel (= 6.1.4.6)
|
||||
activesupport (= 6.1.4.6)
|
||||
activerecord-import (1.2.0)
|
||||
activerecord (>= 3.2)
|
||||
activerecord-import (1.3.0)
|
||||
activerecord (>= 4.2)
|
||||
activestorage (6.1.4.6)
|
||||
actionpack (= 6.1.4.6)
|
||||
activejob (= 6.1.4.6)
|
||||
|
@ -71,11 +71,11 @@ GEM
|
|||
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)
|
||||
|
@ -141,7 +141,7 @@ GEM
|
|||
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.19.2)
|
||||
google-protobuf (3.19.2-x86_64-darwin)
|
||||
google-protobuf (3.19.2-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,50 +301,52 @@ GEM
|
|||
mime-types (~> 3.0)
|
||||
multi_xml (>= 0.5.2)
|
||||
httpclient (2.8.3)
|
||||
i18n (1.9.1)
|
||||
i18n (1.10.0)
|
||||
concurrent-ruby (~> 1.0)
|
||||
image_processing (1.12.1)
|
||||
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.14.0)
|
||||
|
@ -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)
|
||||
|
@ -402,16 +405,16 @@ GEM
|
|||
public_suffix (4.0.6)
|
||||
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)
|
||||
|
@ -442,15 +445,15 @@ GEM
|
|||
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.5.1)
|
||||
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.4.0)
|
||||
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
|
||||
|
@ -582,13 +583,13 @@ GEM
|
|||
sprockets (>= 3.0.0)
|
||||
squasher (0.6.2)
|
||||
statsd-ruby (1.5.0)
|
||||
telephone_number (1.4.12)
|
||||
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)
|
||||
|
|
|
@ -7,6 +7,9 @@ class V2::ReportBuilder
|
|||
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
|
||||
|
@ -64,60 +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_period(params[:group_by] || DEFAULT_GROUP_BY,
|
||||
:created_at, range: range, default_value: 0, permit: %w[day week month year])
|
||||
.count
|
||||
(get_grouped_values scope.conversations).count
|
||||
end
|
||||
|
||||
def incoming_messages_count
|
||||
scope.messages.incoming.unscope(:order)
|
||||
.group_by_period(params[:group_by] || DEFAULT_GROUP_BY,
|
||||
:created_at, range: range, default_value: 0, permit: %w[day week month year])
|
||||
.count
|
||||
(get_grouped_values scope.messages.incoming.unscope(:order)).count
|
||||
end
|
||||
|
||||
def outgoing_messages_count
|
||||
scope.messages.outgoing.unscope(:order)
|
||||
.group_by_period(params[:group_by] || DEFAULT_GROUP_BY,
|
||||
:created_at, range: range, default_value: 0, permit: %w[day week month year])
|
||||
.count
|
||||
(get_grouped_values scope.messages.outgoing.unscope(:order)).count
|
||||
end
|
||||
|
||||
def resolutions_count
|
||||
scope.conversations
|
||||
.resolved
|
||||
.group_by_period(params[:group_by] || DEFAULT_GROUP_BY,
|
||||
:created_at, range: range, default_value: 0, permit: %w[day week month year])
|
||||
.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.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.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.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.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
|
||||
|
|
26
app/controllers/api/v1/accounts/bulk_actions_controller.rb
Normal file
26
app/controllers/api/v1/accounts/bulk_actions_controller.rb
Normal file
|
@ -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
|
|
@ -138,7 +138,7 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
|||
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
|
||||
[]
|
||||
|
|
|
@ -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]
|
||||
|
||||
|
@ -58,6 +59,10 @@ class Api::V1::AccountsController < Api::BaseController
|
|||
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
|
||||
{
|
||||
user: current_user,
|
||||
|
|
|
@ -58,7 +58,8 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
|
|||
since: params[:since],
|
||||
until: params[:until],
|
||||
id: params[:id],
|
||||
group_by: params[:group_by]
|
||||
group_by: params[:group_by],
|
||||
timezone_offset: params[:timezone_offset]
|
||||
}
|
||||
end
|
||||
|
||||
|
|
|
@ -26,7 +26,10 @@ class DashboardController < ActionController::Base
|
|||
'API_CHANNEL_THUMBNAIL',
|
||||
'ANALYTICS_TOKEN',
|
||||
'ANALYTICS_HOST',
|
||||
'DIRECT_UPLOADS_ENABLED'
|
||||
'DIRECT_UPLOADS_ENABLED',
|
||||
'HCAPTCHA_SITE_KEY',
|
||||
'LOGOUT_REDIRECT_LINK',
|
||||
'DISABLE_USER_PROFILE_UPDATE'
|
||||
).merge(app_config)
|
||||
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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
/* global axios */
|
||||
import ApiClient from './ApiClient';
|
||||
|
||||
const getTimeOffset = () => -new Date().getTimezoneOffset() / 60;
|
||||
|
||||
class ReportsAPI extends ApiClient {
|
||||
constructor() {
|
||||
super('reports', { accountScoped: true, apiVersion: 'v2' });
|
||||
|
@ -8,13 +10,27 @@ class ReportsAPI extends ApiClient {
|
|||
|
||||
getReports(metric, since, until, type = 'account', id, group_by) {
|
||||
return axios.get(`${this.url}`, {
|
||||
params: { metric, since, until, type, id, group_by },
|
||||
params: {
|
||||
metric,
|
||||
since,
|
||||
until,
|
||||
type,
|
||||
id,
|
||||
group_by,
|
||||
timezone_offset: getTimeOffset(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
getSummary(since, until, type = 'account', id, group_by) {
|
||||
return axios.get(`${this.url}/summary`, {
|
||||
params: { since, until, type, id, group_by },
|
||||
params: {
|
||||
since,
|
||||
until,
|
||||
type,
|
||||
id,
|
||||
group_by,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -27,6 +27,7 @@ describe('#Reports API', () => {
|
|||
since: 1621103400,
|
||||
until: 1621621800,
|
||||
type: 'account',
|
||||
timezone_offset: -0,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
|
@ -54,3 +54,9 @@
|
|||
.text-y-800 {
|
||||
color: var(--y-800);
|
||||
}
|
||||
|
||||
.text-ellipsis {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
|
|
@ -219,7 +219,7 @@ export default {
|
|||
folders: 'customViews/getCustomViews',
|
||||
}),
|
||||
hasAppliedFilters() {
|
||||
return this.appliedFilters.length;
|
||||
return this.appliedFilters.length !== 0;
|
||||
},
|
||||
hasActiveFolders() {
|
||||
return this.activeFolder && this.foldersId !== 0;
|
||||
|
@ -460,7 +460,8 @@ export default {
|
|||
if (this.hasActiveFolders) {
|
||||
const payload = this.activeFolder.query;
|
||||
this.fetchSavedFilteredConversations(payload);
|
||||
} else {
|
||||
}
|
||||
if (this.hasAppliedFilters) {
|
||||
this.fetchFilteredConversations(this.appliedFilters);
|
||||
}
|
||||
},
|
||||
|
|
|
@ -30,6 +30,7 @@
|
|||
<script>
|
||||
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||
|
||||
export default {
|
||||
mixins: [globalConfigMixin],
|
||||
|
@ -46,6 +47,9 @@ export default {
|
|||
|
||||
mounted() {
|
||||
window.addEventListener('offline', this.updateOnlineStatus);
|
||||
window.bus.$on(BUS_EVENTS.WEBSOCKET_DISCONNECT, () => {
|
||||
this.updateOnlineStatus({ type: 'offline' });
|
||||
});
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
|
|
|
@ -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>
|
|
@ -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: {
|
||||
|
|
|
@ -110,7 +110,7 @@ import { ALLOWED_FILE_TYPES } from 'shared/constants/messages';
|
|||
|
||||
import { REPLY_EDITOR_MODES } from './constants';
|
||||
export default {
|
||||
name: 'ReplyTopPanel',
|
||||
name: 'ReplyBottomPanel',
|
||||
components: { FileUpload },
|
||||
mixins: [eventListenerMixins, uiSettingsMixin, inboxMixin],
|
||||
props: {
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
<template>
|
||||
<li v-if="hasAttachments || data.content" :class="alignBubble">
|
||||
<li
|
||||
v-if="hasAttachments || data.content || isEmailContentType"
|
||||
:class="alignBubble"
|
||||
>
|
||||
<div :class="wrapClass">
|
||||
<div v-tooltip.top-start="messageToolTip" :class="bubbleClass">
|
||||
<bubble-mail-head
|
||||
|
|
|
@ -65,7 +65,7 @@
|
|||
</div>
|
||||
<div class="features-item">
|
||||
<h2 class="block-title">
|
||||
<span class="emoji">🏷</span>{{ $t('ONBOARDING.LABELS.TITLE') }}
|
||||
<span class="emoji">🔖</span>{{ $t('ONBOARDING.LABELS.TITLE') }}
|
||||
</h2>
|
||||
<p class="intro-body">
|
||||
{{ $t('ONBOARDING.LABELS.DESCRIPTION') }}
|
||||
|
|
|
@ -67,11 +67,21 @@
|
|||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="showMessageSignature"
|
||||
v-if="isSignatureEnabledForInbox"
|
||||
v-tooltip="$t('CONVERSATION.FOOTER.MESSAGE_SIGN_TOOLTIP')"
|
||||
class="message-signature-wrap"
|
||||
>
|
||||
<p class="message-signature" v-html="formatMessage(messageSignature)" />
|
||||
<p
|
||||
v-if="isSignatureAvailable"
|
||||
class="message-signature"
|
||||
v-html="formatMessage(messageSignature)"
|
||||
/>
|
||||
<p v-else class="message-signature">
|
||||
{{ $t('CONVERSATION.FOOTER.MESSAGE_SIGNATURE_NOT_CONFIGURED') }}
|
||||
<router-link :to="profilePath">
|
||||
{{ $t('CONVERSATION.FOOTER.CLICK_HERE') }}
|
||||
</router-link>
|
||||
</p>
|
||||
</div>
|
||||
<reply-bottom-panel
|
||||
:mode="replyType"
|
||||
|
@ -124,6 +134,7 @@ import { MESSAGE_MAX_LENGTH } from 'shared/helpers/MessageTypeHelper';
|
|||
import inboxMixin from 'shared/mixins/inboxMixin';
|
||||
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
||||
import { DirectUpload } from 'activestorage';
|
||||
import { frontendURL } from '../../../helper/URLHelper';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
@ -180,6 +191,7 @@ export default {
|
|||
messageSignature: 'getMessageSignature',
|
||||
currentUser: 'getCurrentUser',
|
||||
globalConfig: 'globalConfig/get',
|
||||
accountId: 'getCurrentAccountId',
|
||||
}),
|
||||
|
||||
showRichContentEditor() {
|
||||
|
@ -350,13 +362,19 @@ export default {
|
|||
enableMultipleFileUpload() {
|
||||
return this.isAnEmailChannel || this.isAWebWidgetInbox || this.isAPIInbox;
|
||||
},
|
||||
showMessageSignature() {
|
||||
isSignatureEnabledForInbox() {
|
||||
return !this.isPrivate && this.isAnEmailChannel && this.sendWithSignature;
|
||||
},
|
||||
isSignatureAvailable() {
|
||||
return !!this.messageSignature;
|
||||
},
|
||||
sendWithSignature() {
|
||||
const { send_with_signature: isEnabled } = this.uiSettings;
|
||||
return isEnabled;
|
||||
},
|
||||
profilePath() {
|
||||
return frontendURL(`accounts/${this.accountId}/profile/settings`);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
currentChat(conversation) {
|
||||
|
@ -469,7 +487,7 @@ export default {
|
|||
}
|
||||
if (!this.showMentions) {
|
||||
let newMessage = this.message;
|
||||
if (this.sendWithSignature && this.messageSignature) {
|
||||
if (this.isSignatureEnabledForInbox && this.messageSignature) {
|
||||
newMessage += '\n\n' + this.messageSignature;
|
||||
}
|
||||
const messagePayload = this.getMessagePayload(newMessage);
|
||||
|
@ -479,7 +497,7 @@ export default {
|
|||
'createPendingMessageAndSend',
|
||||
messagePayload
|
||||
);
|
||||
this.$emit(BUS_EVENTS.SCROLL_TO_MESSAGE);
|
||||
bus.$emit(BUS_EVENTS.SCROLL_TO_MESSAGE);
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error?.response?.data?.error ||
|
||||
|
@ -713,4 +731,12 @@ export default {
|
|||
filter: drop-shadow(0px 4px 4px rgba(0, 0, 0, 0.08));
|
||||
}
|
||||
}
|
||||
|
||||
.message-signature {
|
||||
margin-bottom: 0;
|
||||
|
||||
::v-deep p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -19,14 +19,16 @@
|
|||
v-if="isPrivate"
|
||||
v-tooltip.top-start="$t('CONVERSATION.VISIBLE_TO_AGENTS')"
|
||||
icon="lock-closed"
|
||||
class="action--icon"
|
||||
class="action--icon lock--icon--private"
|
||||
size="16"
|
||||
@mouseenter="isHovered = true"
|
||||
@mouseleave="isHovered = false"
|
||||
/>
|
||||
<button @click="onTweetReply">
|
||||
<button
|
||||
v-if="isATweet && (isIncoming || isOutgoing) && sourceId"
|
||||
@click="onTweetReply"
|
||||
>
|
||||
<fluent-icon
|
||||
v-if="isATweet && (isIncoming || isOutgoing) && sourceId"
|
||||
v-tooltip.top-start="$t('CHAT_LIST.REPLY_TO_TWEET')"
|
||||
icon="arrow-reply"
|
||||
class="action--icon cursor-pointer"
|
||||
|
@ -141,6 +143,10 @@ export default {
|
|||
.action--icon {
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
.lock--icon--private {
|
||||
color: var(--s-400);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -205,7 +211,7 @@ export default {
|
|||
|
||||
.is-private {
|
||||
.message-text--metadata {
|
||||
align-items: flex-end;
|
||||
align-items: center;
|
||||
|
||||
.time {
|
||||
color: var(--s-400);
|
||||
|
|
|
@ -61,7 +61,9 @@
|
|||
"ENABLE_SIGN_TOOLTIP": "Enable signature",
|
||||
"DISABLE_SIGN_TOOLTIP": "Disable signature",
|
||||
"MSG_INPUT": "Shift + enter for new line. Start with '/' to select a Canned Response.",
|
||||
"PRIVATE_MSG_INPUT": "Shift + enter for new line. This will be visible only to Agents"
|
||||
"PRIVATE_MSG_INPUT": "Shift + enter for new line. This will be visible only to Agents",
|
||||
"MESSAGE_SIGNATURE_NOT_CONFIGURED": "Message signature is not configured, please configure it in profile settings.",
|
||||
"CLICK_HERE": "Click here to update"
|
||||
},
|
||||
"REPLYBOX": {
|
||||
"REPLY": "Reply",
|
||||
|
|
|
@ -463,7 +463,8 @@
|
|||
"HOURS": "hours",
|
||||
"VALIDATION_ERROR": "Starting time should be before closing time.",
|
||||
"CHOOSE": "Choose"
|
||||
}
|
||||
},
|
||||
"ALL_DAY":"All-Day"
|
||||
},
|
||||
"IMAP": {
|
||||
"TITLE": "IMAP",
|
||||
|
|
|
@ -142,6 +142,7 @@
|
|||
}
|
||||
},
|
||||
"SIDEBAR": {
|
||||
"CURRENTLY_VIEWING_ACCOUNT": "Currently viewing:",
|
||||
"CONVERSATIONS": "Conversations",
|
||||
"ALL_CONVERSATIONS": "All Conversations",
|
||||
"MENTIONED_CONVERSATIONS": "Mentions",
|
||||
|
|
|
@ -75,8 +75,14 @@
|
|||
"
|
||||
@blur="$v.credentials.confirmPassword.$touch"
|
||||
/>
|
||||
<div v-if="globalConfig.hCaptchaSiteKey" class="h-captcha--box">
|
||||
<vue-hcaptcha
|
||||
:sitekey="globalConfig.hCaptchaSiteKey"
|
||||
@verify="onRecaptchaVerified"
|
||||
/>
|
||||
</div>
|
||||
<woot-submit-button
|
||||
:disabled="isSignupInProgress"
|
||||
:disabled="isSignupInProgress || !hasAValidCaptcha"
|
||||
:button-text="$t('REGISTER.SUBMIT')"
|
||||
:loading="isSignupInProgress"
|
||||
button-class="large expanded"
|
||||
|
@ -107,8 +113,11 @@ import { mapGetters } from 'vuex';
|
|||
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import { DEFAULT_REDIRECT_URL } from '../../constants';
|
||||
|
||||
import VueHcaptcha from '@hcaptcha/vue-hcaptcha';
|
||||
export default {
|
||||
components: {
|
||||
VueHcaptcha,
|
||||
},
|
||||
mixins: [globalConfigMixin, alertMixin],
|
||||
data() {
|
||||
return {
|
||||
|
@ -118,6 +127,7 @@ export default {
|
|||
email: '',
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
hCaptchaClientResponse: '',
|
||||
},
|
||||
isSignupInProgress: false,
|
||||
error: '',
|
||||
|
@ -153,9 +163,7 @@ export default {
|
|||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
globalConfig: 'globalConfig/get',
|
||||
}),
|
||||
...mapGetters({ globalConfig: 'globalConfig/get' }),
|
||||
termsLink() {
|
||||
return this.$t('REGISTER.TERMS_ACCEPT')
|
||||
.replace('https://www.chatwoot.com/terms', this.globalConfig.termsURL)
|
||||
|
@ -164,6 +172,12 @@ export default {
|
|||
this.globalConfig.privacyURL
|
||||
);
|
||||
},
|
||||
hasAValidCaptcha() {
|
||||
if (this.globalConfig.hCaptchaSiteKey) {
|
||||
return !!this.credentials.hCaptchaClientResponse;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async submit() {
|
||||
|
@ -187,6 +201,9 @@ export default {
|
|||
this.isSignupInProgress = false;
|
||||
}
|
||||
},
|
||||
onRecaptchaVerified(token) {
|
||||
this.credentials.hCaptchaClientResponse = token;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -234,5 +251,9 @@ export default {
|
|||
text-align: center;
|
||||
margin: var(--space-normal) 0 0 0;
|
||||
}
|
||||
|
||||
.h-captcha--box {
|
||||
margin-bottom: var(--space-one);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -127,11 +127,16 @@ export default {
|
|||
this.showAlert(this.$t('AGENT_MGMT.ADD.API.SUCCESS_MESSAGE'));
|
||||
this.onClose();
|
||||
} catch (error) {
|
||||
const {
|
||||
response: { data: { error: errorResponse = '' } = {} } = {},
|
||||
} = error;
|
||||
let errorMessage = '';
|
||||
if (error.response.status === 422) {
|
||||
this.showAlert(this.$t('AGENT_MGMT.ADD.API.EXIST_MESSAGE'));
|
||||
errorMessage = this.$t('AGENT_MGMT.ADD.API.EXIST_MESSAGE');
|
||||
} else {
|
||||
this.showAlert(this.$t('AGENT_MGMT.ADD.API.ERROR_MESSAGE'));
|
||||
errorMessage = this.$t('AGENT_MGMT.ADD.API.ERROR_MESSAGE');
|
||||
}
|
||||
this.showAlert(errorResponse || errorMessage);
|
||||
}
|
||||
},
|
||||
},
|
||||
|
|
|
@ -352,8 +352,8 @@ export default {
|
|||
switch (type) {
|
||||
case 'assign_team':
|
||||
case 'send_email_to_team':
|
||||
return this.$store.getters['teams/getTeams'];
|
||||
case 'add_label':
|
||||
return this.$store.getters['teams/getTeams'];
|
||||
case 'add_label':
|
||||
return this.$store.getters['labels/getLabels'].map(i => {
|
||||
return {
|
||||
id: i.title,
|
||||
|
@ -441,9 +441,8 @@ export default {
|
|||
const formattedConditions = automation.conditions.map(condition => {
|
||||
const inputType = this.automationTypes[
|
||||
automation.event_name
|
||||
].conditions.find(
|
||||
item => item.key === condition.attribute_key
|
||||
).inputType;
|
||||
].conditions.find(item => item.key === condition.attribute_key)
|
||||
.inputType;
|
||||
if (inputType === 'plain_text') {
|
||||
return {
|
||||
...condition,
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<input
|
||||
v-model="isDayEnabled"
|
||||
name="enable-day"
|
||||
class="enable-day"
|
||||
class="enable-checkbox"
|
||||
type="checkbox"
|
||||
:title="$t('INBOX_MGMT.BUSINESS_HOURS.DAY.ENABLE')"
|
||||
/>
|
||||
|
@ -14,26 +14,38 @@
|
|||
</div>
|
||||
<div v-if="isDayEnabled" class="hours-select-wrap">
|
||||
<div class="hours-range">
|
||||
<div class="checkbox-wrap open-all-day">
|
||||
<input
|
||||
v-model="isOpenAllDay"
|
||||
name="enable-open-all-day"
|
||||
class="enable-checkbox"
|
||||
type="checkbox"
|
||||
:title="$t('INBOX_MGMT.BUSINESS_HOURS.ALL_DAY')"
|
||||
/>
|
||||
<span>{{ $t('INBOX_MGMT.BUSINESS_HOURS.ALL_DAY') }}</span>
|
||||
</div>
|
||||
<multiselect
|
||||
v-model="fromTime"
|
||||
:options="timeSlots"
|
||||
:options="fromTimeSlots"
|
||||
deselect-label=""
|
||||
select-label=""
|
||||
selected-label=""
|
||||
:placeholder="$t('INBOX_MGMT.BUSINESS_HOURS.DAY.CHOOSE')"
|
||||
:allow-empty="false"
|
||||
:disabled="isOpenAllDay"
|
||||
/>
|
||||
<div class="separator-icon">
|
||||
<fluent-icon icon="subtract" type="solid" size="16" />
|
||||
</div>
|
||||
<multiselect
|
||||
v-model="toTime"
|
||||
:options="timeSlots"
|
||||
:options="toTimeSlots"
|
||||
deselect-label=""
|
||||
select-label=""
|
||||
selected-label=""
|
||||
:placeholder="$t('INBOX_MGMT.BUSINESS_HOURS.DAY.CHOOSE')"
|
||||
:allow-empty="false"
|
||||
:disabled="isOpenAllDay"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="hasError" class="date-error">
|
||||
|
@ -79,9 +91,14 @@ export default {
|
|||
},
|
||||
},
|
||||
computed: {
|
||||
timeSlots() {
|
||||
fromTimeSlots() {
|
||||
return timeSlots;
|
||||
},
|
||||
toTimeSlots() {
|
||||
return timeSlots.filter(slot => {
|
||||
return slot !== '12:00 AM';
|
||||
});
|
||||
},
|
||||
isDayEnabled: {
|
||||
get() {
|
||||
return this.timeSlot.from && this.timeSlot.to;
|
||||
|
@ -93,12 +110,14 @@ export default {
|
|||
from: timeSlots[0],
|
||||
to: timeSlots[16],
|
||||
valid: true,
|
||||
openAllDay: false,
|
||||
}
|
||||
: {
|
||||
...this.timeSlot,
|
||||
from: '',
|
||||
to: '',
|
||||
valid: false,
|
||||
openAllDay: false,
|
||||
};
|
||||
this.$emit('update', newSlot);
|
||||
},
|
||||
|
@ -146,15 +165,39 @@ export default {
|
|||
return parse(this.toTime, 'hh:mm a', new Date());
|
||||
},
|
||||
totalHours() {
|
||||
const totalHours = differenceInMinutes(this.toDate, this.fromDate) / 60;
|
||||
if (this.toTime === '12:00 AM') {
|
||||
return 24 + totalHours;
|
||||
if (this.timeSlot.openAllDay) {
|
||||
return 24;
|
||||
}
|
||||
const totalHours = differenceInMinutes(this.toDate, this.fromDate) / 60;
|
||||
return totalHours;
|
||||
},
|
||||
hasError() {
|
||||
return !this.timeSlot.valid;
|
||||
},
|
||||
isOpenAllDay: {
|
||||
get() {
|
||||
return this.timeSlot.openAllDay;
|
||||
},
|
||||
set(value) {
|
||||
if (value) {
|
||||
this.$emit('update', {
|
||||
...this.timeSlot,
|
||||
from: '12:00 AM',
|
||||
to: '11:59 PM',
|
||||
valid: true,
|
||||
openAllDay: value,
|
||||
});
|
||||
} else {
|
||||
this.$emit('update', {
|
||||
...this.timeSlot,
|
||||
from: '09:00 AM',
|
||||
to: '05:00 PM',
|
||||
valid: true,
|
||||
openAllDay: value,
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -182,7 +225,7 @@ export default {
|
|||
box-sizing: content-box;
|
||||
border-bottom: 1px solid var(--color-border-light);
|
||||
}
|
||||
.enable-day {
|
||||
.enable-checkbox {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
|
@ -240,4 +283,17 @@ export default {
|
|||
font-size: var(--font-size-mini);
|
||||
color: var(--r-300);
|
||||
}
|
||||
|
||||
.open-all-day {
|
||||
margin-right: var(--space-medium);
|
||||
span {
|
||||
font-size: var(--font-size-small);
|
||||
font-weight: var(--font-weight-medium);
|
||||
margin-left: var(--space-smaller);
|
||||
}
|
||||
input {
|
||||
font-size: var(--font-size-small);
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -86,6 +86,7 @@ export const timeSlotParse = timeSlots => {
|
|||
close_hour: closeHour,
|
||||
close_minutes: closeMinutes,
|
||||
closed_all_day: closedAllDay,
|
||||
open_all_day: openAllDay,
|
||||
} = slot;
|
||||
const from = closedAllDay ? '' : getTime(openHour, openMinutes);
|
||||
const to = closedAllDay ? '' : getTime(closeHour, closeMinutes);
|
||||
|
@ -95,13 +96,15 @@ export const timeSlotParse = timeSlots => {
|
|||
to,
|
||||
from,
|
||||
valid: !closedAllDay,
|
||||
openAllDay,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
export const timeSlotTransform = timeSlots => {
|
||||
return timeSlots.map(slot => {
|
||||
const closed = !(slot.to && slot.from);
|
||||
const closed = slot.openAllDay ? false : !(slot.to && slot.from);
|
||||
const openAllDay = slot.openAllDay;
|
||||
let fromDate = '';
|
||||
let toDate = '';
|
||||
let openHour = '';
|
||||
|
@ -125,6 +128,7 @@ export const timeSlotTransform = timeSlots => {
|
|||
open_minutes: openMinutes,
|
||||
close_hour: closeHour,
|
||||
close_minutes: closeMinutes,
|
||||
open_all_day: openAllDay,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
|
|
@ -40,6 +40,7 @@ describe('#timeSlotParse', () => {
|
|||
close_hour: 4,
|
||||
close_minutes: 30,
|
||||
closed_all_day: false,
|
||||
open_all_day: false,
|
||||
};
|
||||
|
||||
expect(timeSlotParse([slot])).toStrictEqual([
|
||||
|
@ -48,6 +49,7 @@ describe('#timeSlotParse', () => {
|
|||
from: '01:30 AM',
|
||||
to: '04:30 AM',
|
||||
valid: true,
|
||||
openAllDay: false,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
@ -60,6 +62,7 @@ describe('#timeSlotTransform', () => {
|
|||
from: '01:30 AM',
|
||||
to: '04:30 AM',
|
||||
valid: true,
|
||||
openAllDay: false,
|
||||
};
|
||||
|
||||
expect(timeSlotTransform([slot])).toStrictEqual([
|
||||
|
@ -70,6 +73,7 @@ describe('#timeSlotTransform', () => {
|
|||
close_hour: 4,
|
||||
close_minutes: 30,
|
||||
closed_all_day: false,
|
||||
open_all_day: false,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
|
|
@ -48,7 +48,10 @@
|
|||
@input="$v.displayName.$touch"
|
||||
/>
|
||||
</label>
|
||||
<label :class="{ error: $v.email.$error }">
|
||||
<label
|
||||
v-if="!globalConfig.disableUserProfileUpdate"
|
||||
:class="{ error: $v.email.$error }"
|
||||
>
|
||||
{{ $t('PROFILE_SETTINGS.FORM.EMAIL.LABEL') }}
|
||||
<input
|
||||
v-model.trim="email"
|
||||
|
@ -67,7 +70,7 @@
|
|||
</div>
|
||||
</form>
|
||||
<message-signature />
|
||||
<change-password />
|
||||
<change-password v-if="!globalConfig.disableUserProfileUpdate" />
|
||||
<notification-settings />
|
||||
<div class="profile--settings--row row">
|
||||
<div class="columns small-3">
|
||||
|
|
|
@ -50,6 +50,7 @@ import subDays from 'date-fns/subDays';
|
|||
import startOfDay from 'date-fns/startOfDay';
|
||||
import getUnixTime from 'date-fns/getUnixTime';
|
||||
import { GROUP_BY_FILTER } from '../constants';
|
||||
import endOfDay from 'date-fns/endOfDay';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
@ -79,9 +80,9 @@ export default {
|
|||
},
|
||||
to() {
|
||||
if (this.isDateRangeSelected) {
|
||||
return this.fromCustomDate(this.customDateRange[1]);
|
||||
return this.toCustomDate(this.customDateRange[1]);
|
||||
}
|
||||
return this.fromCustomDate(new Date());
|
||||
return this.toCustomDate(new Date());
|
||||
},
|
||||
from() {
|
||||
if (this.isDateRangeSelected) {
|
||||
|
@ -134,6 +135,9 @@ export default {
|
|||
fromCustomDate(date) {
|
||||
return getUnixTime(startOfDay(date));
|
||||
},
|
||||
toCustomDate(date) {
|
||||
return getUnixTime(endOfDay(date));
|
||||
},
|
||||
changeDateSelection(selectedRange) {
|
||||
this.currentDateRangeSelection = selectedRange;
|
||||
this.onDateRangeChange();
|
||||
|
|
|
@ -148,13 +148,15 @@
|
|||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import WootDateRangePicker from 'dashboard/components/ui/DateRangePicker.vue';
|
||||
const CUSTOM_DATE_RANGE_ID = 5;
|
||||
import subDays from 'date-fns/subDays';
|
||||
import startOfDay from 'date-fns/startOfDay';
|
||||
import endOfDay from 'date-fns/endOfDay';
|
||||
import getUnixTime from 'date-fns/getUnixTime';
|
||||
import startOfDay from 'date-fns/startOfDay';
|
||||
import subDays from 'date-fns/subDays';
|
||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||
import WootDateRangePicker from 'dashboard/components/ui/DateRangePicker.vue';
|
||||
|
||||
import { GROUP_BY_FILTER } from '../constants';
|
||||
const CUSTOM_DATE_RANGE_ID = 5;
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
@ -194,9 +196,9 @@ export default {
|
|||
},
|
||||
to() {
|
||||
if (this.isDateRangeSelected) {
|
||||
return this.fromCustomDate(this.customDateRange[1]);
|
||||
return this.toCustomDate(this.customDateRange[1]);
|
||||
}
|
||||
return this.fromCustomDate(new Date());
|
||||
return this.toCustomDate(new Date());
|
||||
},
|
||||
from() {
|
||||
if (this.isDateRangeSelected) {
|
||||
|
@ -253,6 +255,7 @@ export default {
|
|||
},
|
||||
methods: {
|
||||
onDateRangeChange() {
|
||||
console.log(this.from, this.to);
|
||||
this.$emit('date-range-change', {
|
||||
from: this.from,
|
||||
to: this.to,
|
||||
|
@ -262,6 +265,9 @@ export default {
|
|||
fromCustomDate(date) {
|
||||
return getUnixTime(startOfDay(date));
|
||||
},
|
||||
toCustomDate(date) {
|
||||
return getUnixTime(endOfDay(date));
|
||||
},
|
||||
changeDateSelection(selectedRange) {
|
||||
this.currentDateRangeSelection = selectedRange;
|
||||
this.onDateRangeChange();
|
||||
|
|
|
@ -50,7 +50,7 @@
|
|||
</div>
|
||||
</form>
|
||||
<div class="column text-center sigin__footer">
|
||||
<p>
|
||||
<p v-if="!globalConfig.disableUserProfileUpdate">
|
||||
<router-link to="auth/reset/password">
|
||||
{{ $t('LOGIN.FORGOT_PASSWORD') }}
|
||||
</router-link>
|
||||
|
|
|
@ -71,6 +71,19 @@ export const getters = {
|
|||
|
||||
return messageSignature || '';
|
||||
},
|
||||
|
||||
getCurrentAccount(_state) {
|
||||
const { accounts = [] } = _state.currentUser;
|
||||
const [currentAccount = {}] = accounts.filter(
|
||||
account => account.id === _state.currentAccountId
|
||||
);
|
||||
return currentAccount || {};
|
||||
},
|
||||
|
||||
getUserAccounts(_state) {
|
||||
const { accounts = [] } = _state.currentUser;
|
||||
return accounts;
|
||||
},
|
||||
};
|
||||
|
||||
// actions
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
/* eslint no-console: 0 */
|
||||
/* eslint no-param-reassign: 0 */
|
||||
/* eslint no-shadow: 0 */
|
||||
import compareAsc from 'date-fns/compareAsc';
|
||||
import fromUnixTime from 'date-fns/fromUnixTime';
|
||||
|
||||
import * as types from '../mutation-types';
|
||||
import Report from '../../api/reports';
|
||||
|
||||
|
@ -48,7 +45,8 @@ export const actions = {
|
|||
).then(accountReport => {
|
||||
let { data } = accountReport;
|
||||
data = data.filter(
|
||||
el => compareAsc(new Date(), fromUnixTime(el.timestamp)) > -1
|
||||
el =>
|
||||
reportObj.to - el.timestamp > 0 && el.timestamp - reportObj.from >= 0
|
||||
);
|
||||
if (
|
||||
reportObj.metric === 'avg_first_response_time' ||
|
||||
|
|
|
@ -53,4 +53,60 @@ describe('#getters', () => {
|
|||
).toEqual('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getCurrentAccount', () => {
|
||||
it('returns correct values', () => {
|
||||
expect(
|
||||
getters.getCurrentAccount({
|
||||
currentUser: {},
|
||||
currentAccountId: 1,
|
||||
})
|
||||
).toEqual({});
|
||||
|
||||
expect(
|
||||
getters.getCurrentAccount({
|
||||
currentUser: {
|
||||
accounts: [
|
||||
{
|
||||
name: 'Chatwoot',
|
||||
id: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
currentAccountId: 1,
|
||||
})
|
||||
).toEqual({
|
||||
name: 'Chatwoot',
|
||||
id: 1,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getUserAccounts', () => {
|
||||
it('returns correct values', () => {
|
||||
expect(
|
||||
getters.getUserAccounts({
|
||||
currentUser: {},
|
||||
})
|
||||
).toEqual([]);
|
||||
|
||||
expect(
|
||||
getters.getUserAccounts({
|
||||
currentUser: {
|
||||
accounts: [
|
||||
{
|
||||
name: 'Chatwoot',
|
||||
id: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
).toEqual([
|
||||
{
|
||||
name: 'Chatwoot',
|
||||
id: 1,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -44,5 +44,9 @@ export const clearCookiesOnLogout = () => {
|
|||
|
||||
Cookies.remove('auth_data');
|
||||
Cookies.remove('user');
|
||||
window.location = frontendURL('login');
|
||||
|
||||
const globalConfig = window.globalConfig || {};
|
||||
const logoutRedirectLink =
|
||||
globalConfig.LOGOUT_REDIRECT_LINK || frontendURL('login');
|
||||
window.location = logoutRedirectLink;
|
||||
};
|
||||
|
|
|
@ -1,30 +1,11 @@
|
|||
import Cookies from 'js-cookie';
|
||||
import { IFrameHelper } from '../sdk/IFrameHelper';
|
||||
import { getBubbleView } from '../sdk/bubbleHelpers';
|
||||
import md5 from 'md5';
|
||||
|
||||
const REQUIRED_USER_KEYS = ['avatar_url', 'email', 'name'];
|
||||
|
||||
const ALLOWED_USER_ATTRIBUTES = [...REQUIRED_USER_KEYS, 'identifier_hash'];
|
||||
|
||||
export const getUserCookieName = () => {
|
||||
const SET_USER_COOKIE_PREFIX = 'cw_user_';
|
||||
const { websiteToken: websiteIdentifier } = window.$chatwoot;
|
||||
return `${SET_USER_COOKIE_PREFIX}${websiteIdentifier}`;
|
||||
};
|
||||
|
||||
export const getUserString = ({ identifier = '', user }) => {
|
||||
const userStringWithSortedKeys = ALLOWED_USER_ATTRIBUTES.reduce(
|
||||
(acc, key) => `${acc}${key}${user[key] || ''}`,
|
||||
''
|
||||
);
|
||||
return `${userStringWithSortedKeys}identifier${identifier}`;
|
||||
};
|
||||
|
||||
const computeHashForUserData = (...args) => md5(getUserString(...args));
|
||||
|
||||
export const hasUserKeys = user =>
|
||||
REQUIRED_USER_KEYS.reduce((acc, key) => acc || !!user[key], false);
|
||||
import { getBubbleView } from '../sdk/settingsHelper';
|
||||
import {
|
||||
computeHashForUserData,
|
||||
getUserCookieName,
|
||||
hasUserKeys,
|
||||
} from '../sdk/cookieHelpers';
|
||||
|
||||
const runSDK = ({ baseUrl, websiteToken }) => {
|
||||
if (window.$chatwoot) {
|
||||
|
@ -43,6 +24,7 @@ const runSDK = ({ baseUrl, websiteToken }) => {
|
|||
type: getBubbleView(chatwootSettings.type),
|
||||
launcherTitle: chatwootSettings.launcherTitle || '',
|
||||
showPopoutButton: chatwootSettings.showPopoutButton || false,
|
||||
widgetStyle: chatwootSettings.widgetStyle || 'standard',
|
||||
|
||||
toggle(state) {
|
||||
IFrameHelper.events.toggleBubble(state);
|
||||
|
|
|
@ -23,8 +23,10 @@ import {
|
|||
removeUnreadClass,
|
||||
} from './bubbleHelpers';
|
||||
import { dispatchWindowEvent } from 'shared/helpers/CustomEventHelper';
|
||||
|
||||
const EVENT_NAME = 'chatwoot:ready';
|
||||
import { CHATWOOT_ERROR, CHATWOOT_READY } from '../widget/constants/sdkEvents';
|
||||
import { SET_USER_ERROR } from '../widget/constants/errorTypes';
|
||||
import { getUserCookieName } from './cookieHelpers';
|
||||
import { isFlatWidgetStyle } from './settingsHelper';
|
||||
|
||||
export const IFrameHelper = {
|
||||
getUrl({ baseUrl, websiteToken }) {
|
||||
|
@ -51,6 +53,10 @@ export const IFrameHelper = {
|
|||
if (window.$chatwoot.hideMessageBubble) {
|
||||
holderClassName += ` woot-widget--without-bubble`;
|
||||
}
|
||||
if (isFlatWidgetStyle(window.$chatwoot.widgetStyle)) {
|
||||
holderClassName += ` woot-widget-holder--flat`;
|
||||
}
|
||||
|
||||
addClass(widgetHolder, holderClassName);
|
||||
widgetHolder.appendChild(iframe);
|
||||
body.appendChild(widgetHolder);
|
||||
|
@ -120,6 +126,7 @@ export const IFrameHelper = {
|
|||
position: window.$chatwoot.position,
|
||||
hideMessageBubble: window.$chatwoot.hideMessageBubble,
|
||||
showPopoutButton: window.$chatwoot.showPopoutButton,
|
||||
widgetStyle: window.$chatwoot.widgetStyle,
|
||||
});
|
||||
IFrameHelper.onLoad({
|
||||
widgetColor: message.config.channelConfig.widgetColor,
|
||||
|
@ -129,7 +136,14 @@ export const IFrameHelper = {
|
|||
if (window.$chatwoot.user) {
|
||||
IFrameHelper.sendMessage('set-user', window.$chatwoot.user);
|
||||
}
|
||||
dispatchWindowEvent(EVENT_NAME);
|
||||
dispatchWindowEvent({ eventName: CHATWOOT_READY });
|
||||
},
|
||||
error: ({ errorType, data }) => {
|
||||
dispatchWindowEvent({ eventName: CHATWOOT_ERROR, data: data });
|
||||
|
||||
if (errorType === SET_USER_ERROR) {
|
||||
Cookies.remove(getUserCookieName());
|
||||
}
|
||||
},
|
||||
|
||||
setBubbleLabel(message) {
|
||||
|
@ -214,21 +228,27 @@ export const IFrameHelper = {
|
|||
createBubbleHolder();
|
||||
onLocationChangeListener();
|
||||
if (!window.$chatwoot.hideMessageBubble) {
|
||||
let className = 'woot-widget-bubble';
|
||||
let closeBtnClassName = `woot-elements--${window.$chatwoot.position} woot-widget-bubble woot--close woot--hide`;
|
||||
|
||||
if (isFlatWidgetStyle(window.$chatwoot.widgetStyle)) {
|
||||
className += ' woot-widget-bubble--flat';
|
||||
closeBtnClassName += ' woot-widget-bubble--flat';
|
||||
}
|
||||
|
||||
const chatIcon = createBubbleIcon({
|
||||
className: 'woot-widget-bubble',
|
||||
className,
|
||||
src: bubbleImg,
|
||||
target: chatBubble,
|
||||
});
|
||||
|
||||
const closeIcon = closeBubble;
|
||||
const closeIconclassName = `woot-elements--${window.$chatwoot.position} woot-widget-bubble woot--close woot--hide`;
|
||||
addClass(closeIcon, closeIconclassName);
|
||||
addClass(closeBubble, closeBtnClassName);
|
||||
|
||||
chatIcon.style.background = widgetColor;
|
||||
closeIcon.style.background = widgetColor;
|
||||
closeBubble.style.background = widgetColor;
|
||||
|
||||
bubbleHolder.appendChild(chatIcon);
|
||||
bubbleHolder.appendChild(closeIcon);
|
||||
bubbleHolder.appendChild(closeBubble);
|
||||
bubbleHolder.appendChild(createNotificationBubble());
|
||||
onClickChatBubble();
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { addClass, removeClass, toggleClass, wootOn } from './DOMHelpers';
|
||||
import { IFrameHelper } from './IFrameHelper';
|
||||
import { BUBBLE_DESIGN } from './constants';
|
||||
import { isExpandedView } from './settingsHelper';
|
||||
|
||||
export const bubbleImg =
|
||||
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAMAAABg3Am1AAAAUVBMVEUAAAD///////////////////////////////////////////////////////////////////////////////////////////////////////8IN+deAAAAGnRSTlMAAwgJEBk0TVheY2R5eo+ut8jb5OXs8fX2+cjRDTIAAADsSURBVHgBldZbkoMgFIThRgQv8SKKgGf/C51UnJqaRI30/9zfe+NQUQ3TvG7bOk9DVeCmshmj/CuOTYnrdBfkUOg0zlOtl9OWVuEk4+QyZ3DIevmSt/ioTvK1VH/s5bY3YdM9SBZ/mUUyWgx+U06ycgp7D8msxSvtc4HXL9BLdj2elSEfhBJAI0QNgJEBI1BEBsQClVBVGDgwYOLAhJkDM1YOrNg4sLFAsLJgZsHEgoEFFQt0JAFGFjQsKAMJ0LFAexKgZYFyJIDxJIBNJEDNAtSJBLCeBDCOBFAPzwFA94ED+zmhwDO9358r8ANtIsMXi7qVAwAAAABJRU5ErkJggg==';
|
||||
|
@ -13,10 +13,6 @@ export const chatBubble = document.createElement('button');
|
|||
export const closeBubble = document.createElement('button');
|
||||
export const notificationBubble = document.createElement('span');
|
||||
|
||||
export const getBubbleView = type =>
|
||||
BUBBLE_DESIGN.includes(type) ? type : BUBBLE_DESIGN[0];
|
||||
export const isExpandedView = type => getBubbleView(type) === BUBBLE_DESIGN[1];
|
||||
|
||||
export const setBubbleText = bubbleText => {
|
||||
if (isExpandedView(window.$chatwoot.type)) {
|
||||
const textNode = document.getElementById('woot-widget--expanded__text');
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
export const BUBBLE_DESIGN = ['standard', 'expanded_bubble'];
|
||||
export const WIDGET_DESIGN = ['standard', 'flat'];
|
||||
|
|
23
app/javascript/sdk/cookieHelpers.js
Normal file
23
app/javascript/sdk/cookieHelpers.js
Normal file
|
@ -0,0 +1,23 @@
|
|||
import md5 from 'md5';
|
||||
|
||||
const REQUIRED_USER_KEYS = ['avatar_url', 'email', 'name'];
|
||||
const ALLOWED_USER_ATTRIBUTES = [...REQUIRED_USER_KEYS, 'identifier_hash'];
|
||||
|
||||
export const getUserCookieName = () => {
|
||||
const SET_USER_COOKIE_PREFIX = 'cw_user_';
|
||||
const { websiteToken: websiteIdentifier } = window.$chatwoot;
|
||||
return `${SET_USER_COOKIE_PREFIX}${websiteIdentifier}`;
|
||||
};
|
||||
|
||||
export const getUserString = ({ identifier = '', user }) => {
|
||||
const userStringWithSortedKeys = ALLOWED_USER_ATTRIBUTES.reduce(
|
||||
(acc, key) => `${acc}${key}${user[key] || ''}`,
|
||||
''
|
||||
);
|
||||
return `${userStringWithSortedKeys}identifier${identifier}`;
|
||||
};
|
||||
|
||||
export const computeHashForUserData = (...args) => md5(getUserString(...args));
|
||||
|
||||
export const hasUserKeys = user =>
|
||||
REQUIRED_USER_KEYS.reduce((acc, key) => acc || !!user[key], false);
|
|
@ -1,5 +1,10 @@
|
|||
export const SDK_CSS = `.woot-widget-holder {
|
||||
box-shadow: 0 5px 40px rgba(0, 0, 0, .16) !important;
|
||||
export const SDK_CSS = `
|
||||
:root {
|
||||
--b-100: #F2F3F7;
|
||||
}
|
||||
|
||||
.woot-widget-holder {
|
||||
box-shadow: 0 5px 40px rgba(0, 0, 0, .16);
|
||||
opacity: 1;
|
||||
will-change: transform, opacity;
|
||||
transform: translateY(0);
|
||||
|
@ -9,6 +14,12 @@ export const SDK_CSS = `.woot-widget-holder {
|
|||
z-index: 2147483000 !important;
|
||||
}
|
||||
|
||||
.woot-widget-holder.woot-widget-holder--flat {
|
||||
box-shadow: none;
|
||||
border-radius: 0;
|
||||
border: 1px solid var(--b-100);
|
||||
}
|
||||
|
||||
.woot-widget-holder iframe {
|
||||
border: 0;
|
||||
height: 100% !important;
|
||||
|
@ -22,21 +33,45 @@ export const SDK_CSS = `.woot-widget-holder {
|
|||
height: auto;
|
||||
bottom: 94px;
|
||||
box-shadow: none !important;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.woot-widget-bubble {
|
||||
background: #1f93ff;
|
||||
border-radius: 100px !important;
|
||||
border-radius: 100px;
|
||||
border-width: 0px;
|
||||
bottom: 20px;
|
||||
padding: 0px;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, .16) !important;
|
||||
cursor: pointer;
|
||||
height: 64px !important;
|
||||
height: 64px;
|
||||
padding: 0px;
|
||||
position: fixed;
|
||||
width: 64px !important;
|
||||
z-index: 2147483000 !important;
|
||||
user-select: none;
|
||||
width: 64px;
|
||||
z-index: 2147483000 !important;
|
||||
}
|
||||
|
||||
.woot-widget-bubble.woot-widget-bubble--flat {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.woot-widget-holder.woot-widget-holder--flat {
|
||||
bottom: 90px;
|
||||
}
|
||||
|
||||
.woot-widget-bubble.woot-widget-bubble--flat {
|
||||
height: 56px;
|
||||
width: 56px;
|
||||
}
|
||||
|
||||
.woot-widget-bubble.woot-widget-bubble--flat img {
|
||||
margin: 16px;
|
||||
}
|
||||
|
||||
.woot-widget-bubble.woot-widget-bubble--flat.woot--close::before,
|
||||
.woot-widget-bubble.woot-widget-bubble--flat.woot--close::after {
|
||||
left: 28px;
|
||||
top: 16px;
|
||||
}
|
||||
|
||||
.woot-widget-bubble.unread-notification::after {
|
||||
|
@ -184,7 +219,7 @@ export const SDK_CSS = `.woot-widget-holder {
|
|||
|
||||
@media only screen and (min-width: 667px) {
|
||||
.woot-widget-holder {
|
||||
border-radius: 16px !important;
|
||||
border-radius: 16px;
|
||||
bottom: 104px;
|
||||
height: calc(85% - 64px - 20px);
|
||||
max-height: 590px !important;
|
||||
|
|
11
app/javascript/sdk/settingsHelper.js
Normal file
11
app/javascript/sdk/settingsHelper.js
Normal file
|
@ -0,0 +1,11 @@
|
|||
import { BUBBLE_DESIGN, WIDGET_DESIGN } from './constants';
|
||||
|
||||
export const getBubbleView = type =>
|
||||
BUBBLE_DESIGN.includes(type) ? type : BUBBLE_DESIGN[0];
|
||||
|
||||
export const isExpandedView = type => getBubbleView(type) === BUBBLE_DESIGN[1];
|
||||
|
||||
export const getWidgetStyle = style =>
|
||||
WIDGET_DESIGN.includes(style) ? style : WIDGET_DESIGN[0];
|
||||
|
||||
export const isFlatWidgetStyle = style => style === 'flat';
|
|
@ -1,17 +0,0 @@
|
|||
import { getBubbleView, isExpandedView } from '../bubbleHelpers';
|
||||
|
||||
describe('#getBubbleView', () => {
|
||||
it('returns correct view', () => {
|
||||
expect(getBubbleView('')).toEqual('standard');
|
||||
expect(getBubbleView('standard')).toEqual('standard');
|
||||
expect(getBubbleView('expanded_bubble')).toEqual('expanded_bubble');
|
||||
});
|
||||
});
|
||||
|
||||
describe('#isExpandedView', () => {
|
||||
it('returns true if it is expanded view', () => {
|
||||
expect(isExpandedView('')).toEqual(false);
|
||||
expect(isExpandedView('standard')).toEqual(false);
|
||||
expect(isExpandedView('expanded_bubble')).toEqual(true);
|
||||
});
|
||||
});
|
|
@ -1,4 +1,8 @@
|
|||
import { getUserCookieName, getUserString, hasUserKeys } from '../../packs/sdk';
|
||||
import {
|
||||
getUserCookieName,
|
||||
getUserString,
|
||||
hasUserKeys,
|
||||
} from '../cookieHelpers';
|
||||
|
||||
describe('#getUserCookieName', () => {
|
||||
it('returns correct cookie name', () => {
|
38
app/javascript/sdk/specs/settingsHelpers.spec.js
Normal file
38
app/javascript/sdk/specs/settingsHelpers.spec.js
Normal file
|
@ -0,0 +1,38 @@
|
|||
import {
|
||||
getBubbleView,
|
||||
getWidgetStyle,
|
||||
isExpandedView,
|
||||
isFlatWidgetStyle,
|
||||
} from '../settingsHelper';
|
||||
|
||||
describe('#getBubbleView', () => {
|
||||
it('returns correct view', () => {
|
||||
expect(getBubbleView('')).toEqual('standard');
|
||||
expect(getBubbleView('standard')).toEqual('standard');
|
||||
expect(getBubbleView('expanded_bubble')).toEqual('expanded_bubble');
|
||||
});
|
||||
});
|
||||
|
||||
describe('#isExpandedView', () => {
|
||||
it('returns true if it is expanded view', () => {
|
||||
expect(isExpandedView('')).toEqual(false);
|
||||
expect(isExpandedView('standard')).toEqual(false);
|
||||
expect(isExpandedView('expanded_bubble')).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getWidgetStyle', () => {
|
||||
it('returns correct view', () => {
|
||||
expect(getWidgetStyle('')).toEqual('standard');
|
||||
expect(getWidgetStyle('standard')).toEqual('standard');
|
||||
expect(getWidgetStyle('flat')).toEqual('flat');
|
||||
});
|
||||
});
|
||||
|
||||
describe('#isFlatWidgetStyle', () => {
|
||||
it('returns true if it is expanded view', () => {
|
||||
expect(isFlatWidgetStyle('')).toEqual(false);
|
||||
expect(isFlatWidgetStyle('standard')).toEqual(false);
|
||||
expect(isFlatWidgetStyle('flat')).toEqual(true);
|
||||
});
|
||||
});
|
|
@ -5,4 +5,5 @@ export const BUS_EVENTS = {
|
|||
FOCUS_CUSTOM_ATTRIBUTE: 'FOCUS_CUSTOM_ATTRIBUTE',
|
||||
SCROLL_TO_MESSAGE: 'SCROLL_TO_MESSAGE',
|
||||
TOGGLE_SIDEMENU: 'TOGGLE_SIDEMENU',
|
||||
WEBSOCKET_DISCONNECT: 'WEBSOCKET_DISCONNECT',
|
||||
};
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { createConsumer } from '@rails/actioncable';
|
||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||
|
||||
const PRESENCE_INTERVAL = 20000;
|
||||
|
||||
|
@ -18,6 +19,7 @@ class BaseActionCableConnector {
|
|||
this.perform('update_presence');
|
||||
},
|
||||
received: this.onReceived,
|
||||
disconnected: this.onDisconnected,
|
||||
}
|
||||
);
|
||||
this.app = app;
|
||||
|
@ -33,6 +35,11 @@ class BaseActionCableConnector {
|
|||
this.consumer.disconnect();
|
||||
}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
onDisconnected() {
|
||||
window.bus.$emit(BUS_EVENTS.WEBSOCKET_DISCONNECT);
|
||||
}
|
||||
|
||||
onReceived = ({ event, data } = {}) => {
|
||||
if (this.isAValidEvent(data)) {
|
||||
if (this.events[event] && typeof this.events[event] === 'function') {
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
export const createEvent = eventName => {
|
||||
export const createEvent = ({ eventName, data = null }) => {
|
||||
let event;
|
||||
if (typeof window.CustomEvent === 'function') {
|
||||
event = new CustomEvent(eventName);
|
||||
event = new CustomEvent(eventName, { detail: data });
|
||||
} else {
|
||||
event = document.createEvent('CustomEvent');
|
||||
event.initCustomEvent(eventName, false, false, null);
|
||||
event.initCustomEvent(eventName, false, false, data);
|
||||
}
|
||||
return event;
|
||||
};
|
||||
|
||||
export const dispatchWindowEvent = eventName => {
|
||||
const event = createEvent(eventName);
|
||||
export const dispatchWindowEvent = ({ eventName, data }) => {
|
||||
const event = createEvent({ eventName, data });
|
||||
window.dispatchEvent(event);
|
||||
};
|
||||
|
|
|
@ -3,7 +3,7 @@ import { dispatchWindowEvent } from '../CustomEventHelper';
|
|||
describe('dispatchWindowEvent', () => {
|
||||
it('dispatches correct event', () => {
|
||||
window.dispatchEvent = jest.fn();
|
||||
dispatchWindowEvent('chatwoot:ready');
|
||||
dispatchWindowEvent({ eventName: 'chatwoot:ready' });
|
||||
expect(dispatchEvent).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,12 +7,14 @@ const {
|
|||
CREATE_NEW_ACCOUNT_FROM_DASHBOARD: createNewAccountFromDashboard,
|
||||
DIRECT_UPLOADS_ENABLED: directUploadsEnabled,
|
||||
DISPLAY_MANIFEST: displayManifest,
|
||||
HCAPTCHA_SITE_KEY: hCaptchaSiteKey,
|
||||
INSTALLATION_NAME: installationName,
|
||||
LOGO_THUMBNAIL: logoThumbnail,
|
||||
LOGO: logo,
|
||||
PRIVACY_URL: privacyURL,
|
||||
TERMS_URL: termsURL,
|
||||
WIDGET_BRAND_URL: widgetBrandURL,
|
||||
DISABLE_USER_PROFILE_UPDATE: disableUserProfileUpdate,
|
||||
} = window.globalConfig || {};
|
||||
|
||||
const state = {
|
||||
|
@ -23,7 +25,9 @@ const state = {
|
|||
chatwootInboxToken,
|
||||
createNewAccountFromDashboard,
|
||||
directUploadsEnabled: directUploadsEnabled === 'true',
|
||||
disableUserProfileUpdate: disableUserProfileUpdate === 'true',
|
||||
displayManifest,
|
||||
hCaptchaSiteKey,
|
||||
installationName,
|
||||
logo,
|
||||
logoThumbnail,
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
'is-mobile': isMobile,
|
||||
'is-widget-right': isRightAligned,
|
||||
'is-bubble-hidden': hideMessageBubble,
|
||||
'is-flat-design': isWidgetStyleFlat,
|
||||
}"
|
||||
>
|
||||
<router-view></router-view>
|
||||
|
@ -61,6 +62,7 @@ export default {
|
|||
isWidgetOpen: 'appConfig/getIsWidgetOpen',
|
||||
messageCount: 'conversation/getMessageCount',
|
||||
unreadMessageCount: 'conversation/getUnreadMessageCount',
|
||||
isWidgetStyleFlat: 'appConfig/isWidgetStyleFlat',
|
||||
}),
|
||||
isIFrame() {
|
||||
return IFrameHelper.isIFrame();
|
||||
|
|
|
@ -57,3 +57,30 @@ body {
|
|||
padding-left: $space-normal;
|
||||
}
|
||||
}
|
||||
|
||||
.is-flat-design {
|
||||
.chat-bubble {
|
||||
border-bottom-left-radius: 0 !important;
|
||||
border-bottom-right-radius: 0 !important;
|
||||
border-top-left-radius: 0 !important;
|
||||
border-top-right-radius: 0 !important;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
|
||||
input {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.chat-message--input {
|
||||
border-radius: 0 !important;
|
||||
box-shadow: none !important;
|
||||
|
||||
&.is-focused {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
<template>
|
||||
<footer
|
||||
v-if="!hideReplyBox"
|
||||
class="shadow-sm rounded-lg bg-white mb-1 z-50 relative"
|
||||
class="shadow-sm bg-white mb-1 z-50 relative"
|
||||
:class="{ 'rounded-lg': !isWidgetStyleFlat }"
|
||||
>
|
||||
<chat-input-wrap
|
||||
:on-send-message="handleSendMessage"
|
||||
|
@ -54,6 +55,7 @@ export default {
|
|||
widgetColor: 'appConfig/getWidgetColor',
|
||||
getConversationSize: 'conversation/getConversationSize',
|
||||
currentUser: 'contacts/getCurrentUser',
|
||||
isWidgetStyleFlat: 'appConfig/isWidgetStyleFlat',
|
||||
}),
|
||||
textColor() {
|
||||
return getContrastingTextColor(this.widgetColor);
|
||||
|
|
|
@ -97,7 +97,6 @@ export default {
|
|||
@import '~widget/assets/scss/mixins';
|
||||
|
||||
.header-wrap {
|
||||
border-radius: $space-normal $space-normal 0 0;
|
||||
flex-shrink: 0;
|
||||
transition: max-height 300ms;
|
||||
z-index: 99;
|
||||
|
|
1
app/javascript/widget/constants/errorTypes.js
Normal file
1
app/javascript/widget/constants/errorTypes.js
Normal file
|
@ -0,0 +1 @@
|
|||
export const SET_USER_ERROR = 'SET_USER_ERROR';
|
2
app/javascript/widget/constants/sdkEvents.js
Normal file
2
app/javascript/widget/constants/sdkEvents.js
Normal file
|
@ -0,0 +1,2 @@
|
|||
export const CHATWOOT_ERROR = 'chatwoot:error';
|
||||
export const CHATWOOT_READY = 'chatwoot:ready';
|
|
@ -31,9 +31,12 @@ export default {
|
|||
closeHour,
|
||||
closeMinute,
|
||||
closedAllDay,
|
||||
openAllDay,
|
||||
} = this.currentDayAvailability;
|
||||
const { utcOffset } = this.channelConfig;
|
||||
|
||||
if (openAllDay) return true;
|
||||
|
||||
if (closedAllDay) return false;
|
||||
|
||||
const startTime = buildDateFromTime(openHour, openMinute, utcOffset);
|
||||
|
@ -56,6 +59,7 @@ export default {
|
|||
openMinute: workingHourConfig.open_minutes,
|
||||
closeHour: workingHourConfig.close_hour,
|
||||
closeMinute: workingHourConfig.close_minutes,
|
||||
openAllDay: workingHourConfig.open_all_day,
|
||||
};
|
||||
},
|
||||
isInBusinessHours() {
|
||||
|
|
|
@ -6,14 +6,15 @@ import {
|
|||
} from '../types';
|
||||
|
||||
const state = {
|
||||
showPopoutButton: false,
|
||||
hideMessageBubble: false,
|
||||
position: 'right',
|
||||
isWebWidgetTriggered: false,
|
||||
isCampaignViewClicked: false,
|
||||
isWebWidgetTriggered: false,
|
||||
isWidgetOpen: false,
|
||||
widgetColor: '',
|
||||
position: 'right',
|
||||
referrerHost: '',
|
||||
showPopoutButton: false,
|
||||
widgetColor: '',
|
||||
widgetStyle: 'standard',
|
||||
};
|
||||
|
||||
export const getters = {
|
||||
|
@ -23,14 +24,19 @@ export const getters = {
|
|||
getIsWidgetOpen: $state => $state.isWidgetOpen,
|
||||
getWidgetColor: $state => $state.widgetColor,
|
||||
getReferrerHost: $state => $state.referrerHost,
|
||||
isWidgetStyleFlat: $state => $state.widgetStyle === 'flat',
|
||||
};
|
||||
|
||||
export const actions = {
|
||||
setAppConfig({ commit }, { showPopoutButton, position, hideMessageBubble }) {
|
||||
setAppConfig(
|
||||
{ commit },
|
||||
{ showPopoutButton, position, hideMessageBubble, widgetStyle = 'rounded' }
|
||||
) {
|
||||
commit(SET_WIDGET_APP_CONFIG, {
|
||||
showPopoutButton: !!showPopoutButton,
|
||||
position: position || 'right',
|
||||
hideMessageBubble: !!hideMessageBubble,
|
||||
position: position || 'right',
|
||||
showPopoutButton: !!showPopoutButton,
|
||||
widgetStyle,
|
||||
});
|
||||
},
|
||||
toggleWidgetOpen({ commit }, isWidgetOpen) {
|
||||
|
@ -49,6 +55,7 @@ export const mutations = {
|
|||
$state.showPopoutButton = data.showPopoutButton;
|
||||
$state.position = data.position;
|
||||
$state.hideMessageBubble = data.hideMessageBubble;
|
||||
$state.widgetStyle = data.widgetStyle;
|
||||
},
|
||||
[TOGGLE_WIDGET_OPEN]($state, isWidgetOpen) {
|
||||
$state.isWidgetOpen = isWidgetOpen;
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { IFrameHelper } from 'widget/helpers/utils';
|
||||
import ContactsAPI from '../../api/contacts';
|
||||
|
||||
import { SET_USER_ERROR } from '../../constants/errorTypes';
|
||||
const state = {
|
||||
currentUser: {},
|
||||
};
|
||||
|
@ -38,7 +39,14 @@ export const actions = {
|
|||
dispatch('conversation/fetchOldConversations', {}, { root: true });
|
||||
}
|
||||
} catch (error) {
|
||||
// Ignore error
|
||||
const {
|
||||
response: { data },
|
||||
} = error;
|
||||
IFrameHelper.sendMessage({
|
||||
event: 'error',
|
||||
errorType: SET_USER_ERROR,
|
||||
data,
|
||||
});
|
||||
}
|
||||
},
|
||||
setCustomAttributes: async (_, customAttributes = {}) => {
|
||||
|
|
|
@ -2,6 +2,7 @@ import { API } from 'widget/helpers/axios';
|
|||
import { actions } from '../../contacts';
|
||||
|
||||
const commit = jest.fn();
|
||||
const dispatch = jest.fn();
|
||||
jest.mock('widget/helpers/axios');
|
||||
|
||||
describe('#actions', () => {
|
||||
|
@ -11,11 +12,16 @@ describe('#actions', () => {
|
|||
email: 'thoma@sphadikam.com',
|
||||
name: 'Adu Thoma',
|
||||
avatar_url: '',
|
||||
identifier_hash: 'malana_hash',
|
||||
identifier_hash: 'random_hex_identifier_hash',
|
||||
};
|
||||
API.patch.mockResolvedValue({ data: { pubsub_token: 'token' } });
|
||||
await actions.update({ commit }, { identifier: 1, user });
|
||||
await actions.update({ commit, dispatch }, { identifier: 1, user });
|
||||
expect(commit.mock.calls).toEqual([]);
|
||||
expect(dispatch.mock.calls).toEqual([
|
||||
['get'],
|
||||
['conversation/clearConversations', {}, { root: true }],
|
||||
['conversation/fetchOldConversations', {}, { root: true }],
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
59
app/jobs/bulk_actions_job.rb
Normal file
59
app/jobs/bulk_actions_job.rb
Normal file
|
@ -0,0 +1,59 @@
|
|||
class BulkActionsJob < ApplicationJob
|
||||
queue_as :medium
|
||||
attr_accessor :records
|
||||
|
||||
MODEL_TYPE = ['Conversation'].freeze
|
||||
|
||||
def perform(account:, params:, user:)
|
||||
@account = account
|
||||
Current.user = user
|
||||
@params = params
|
||||
@records = records_to_updated(params[:ids])
|
||||
bulk_update
|
||||
ensure
|
||||
Current.reset
|
||||
end
|
||||
|
||||
def bulk_update
|
||||
bulk_remove_labels
|
||||
bulk_conversation_update
|
||||
end
|
||||
|
||||
def bulk_conversation_update
|
||||
params = available_params(@params)
|
||||
records.each do |conversation|
|
||||
bulk_add_labels(conversation)
|
||||
conversation.update(params) if params
|
||||
end
|
||||
end
|
||||
|
||||
def bulk_remove_labels
|
||||
records.each do |conversation|
|
||||
remove_labels(conversation)
|
||||
end
|
||||
end
|
||||
|
||||
def available_params(params)
|
||||
return unless params[:fields]
|
||||
|
||||
params[:fields].delete_if { |_k, v| v.nil? }
|
||||
end
|
||||
|
||||
def bulk_add_labels(conversation)
|
||||
conversation.add_labels(@params[:labels][:add]) if @params[:labels] && @params[:labels][:add]
|
||||
end
|
||||
|
||||
def remove_labels(conversation)
|
||||
return unless @params[:labels] && @params[:labels][:remove]
|
||||
|
||||
labels = conversation.label_list - @params[:labels][:remove]
|
||||
conversation.update(label_list: labels)
|
||||
end
|
||||
|
||||
def records_to_updated(ids)
|
||||
current_model = @params[:type].camelcase
|
||||
return unless MODEL_TYPE.include?(current_model)
|
||||
|
||||
current_model.constantize&.where(account_id: @account.id, display_id: ids)
|
||||
end
|
||||
end
|
|
@ -3,7 +3,7 @@
|
|||
module OutOfOffisable
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
OFFISABLE_ATTRS = %w[day_of_week closed_all_day open_hour open_minutes close_hour close_minutes].freeze
|
||||
OFFISABLE_ATTRS = %w[day_of_week closed_all_day open_hour open_minutes close_hour close_minutes open_all_day].freeze
|
||||
|
||||
included do
|
||||
has_many :working_hours, dependent: :destroy_async
|
||||
|
@ -29,7 +29,8 @@ module OutOfOffisable
|
|||
# "open_hour"=>9,
|
||||
# "open_minutes"=>0,
|
||||
# "close_hour"=>17,
|
||||
# "close_minutes"=>0},...]
|
||||
# "close_minutes"=>0,
|
||||
# "open_all_day=>false" },...]
|
||||
def update_working_hours(params)
|
||||
ActiveRecord::Base.transaction do
|
||||
params.each do |working_hour|
|
||||
|
@ -41,12 +42,12 @@ module OutOfOffisable
|
|||
private
|
||||
|
||||
def create_default_working_hours
|
||||
working_hours.create!(day_of_week: 0, closed_all_day: true)
|
||||
working_hours.create!(day_of_week: 1, open_hour: 9, open_minutes: 0, close_hour: 17, close_minutes: 0)
|
||||
working_hours.create!(day_of_week: 2, open_hour: 9, open_minutes: 0, close_hour: 17, close_minutes: 0)
|
||||
working_hours.create!(day_of_week: 3, open_hour: 9, open_minutes: 0, close_hour: 17, close_minutes: 0)
|
||||
working_hours.create!(day_of_week: 4, open_hour: 9, open_minutes: 0, close_hour: 17, close_minutes: 0)
|
||||
working_hours.create!(day_of_week: 5, open_hour: 9, open_minutes: 0, close_hour: 17, close_minutes: 0)
|
||||
working_hours.create!(day_of_week: 6, closed_all_day: true)
|
||||
working_hours.create!(day_of_week: 0, closed_all_day: true, open_all_day: false)
|
||||
working_hours.create!(day_of_week: 1, open_hour: 9, open_minutes: 0, close_hour: 17, close_minutes: 0, open_all_day: false)
|
||||
working_hours.create!(day_of_week: 2, open_hour: 9, open_minutes: 0, close_hour: 17, close_minutes: 0, open_all_day: false)
|
||||
working_hours.create!(day_of_week: 3, open_hour: 9, open_minutes: 0, close_hour: 17, close_minutes: 0, open_all_day: false)
|
||||
working_hours.create!(day_of_week: 4, open_hour: 9, open_minutes: 0, close_hour: 17, close_minutes: 0, open_all_day: false)
|
||||
working_hours.create!(day_of_week: 5, open_hour: 9, open_minutes: 0, close_hour: 17, close_minutes: 0, open_all_day: false)
|
||||
working_hours.create!(day_of_week: 6, closed_all_day: true, open_all_day: false)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -183,6 +183,9 @@ class Conversation < ApplicationRecord
|
|||
end
|
||||
|
||||
def notify_conversation_updation
|
||||
return unless previous_changes.keys.present? && (previous_changes.keys & %w[team_id assignee_id status snoozed_until
|
||||
custom_attributes]).present?
|
||||
|
||||
dispatcher_dispatch(CONVERSATION_UPDATED)
|
||||
end
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
# close_minutes :integer
|
||||
# closed_all_day :boolean default(FALSE)
|
||||
# day_of_week :integer not null
|
||||
# open_all_day :boolean default(FALSE)
|
||||
# open_hour :integer
|
||||
# open_minutes :integer
|
||||
# created_at :datetime not null
|
||||
|
@ -22,6 +23,7 @@
|
|||
class WorkingHour < ApplicationRecord
|
||||
belongs_to :inbox
|
||||
|
||||
before_validation :ensure_open_all_day_hours
|
||||
before_save :assign_account
|
||||
|
||||
validates :open_hour, presence: true, unless: :closed_all_day?
|
||||
|
@ -35,6 +37,7 @@ class WorkingHour < ApplicationRecord
|
|||
validates :close_minutes, inclusion: 0..59, unless: :closed_all_day?
|
||||
|
||||
validate :close_after_open, unless: :closed_all_day?
|
||||
validate :open_all_day_and_closed_all_day
|
||||
|
||||
def self.today
|
||||
find_by(day_of_week: Date.current.wday)
|
||||
|
@ -69,4 +72,19 @@ class WorkingHour < ApplicationRecord
|
|||
|
||||
errors.add(:close_hour, 'Closing time cannot be before opening time')
|
||||
end
|
||||
|
||||
def ensure_open_all_day_hours
|
||||
return unless open_all_day?
|
||||
|
||||
self.open_hour = 0
|
||||
self.open_minutes = 0
|
||||
self.close_hour = 23
|
||||
self.close_minutes = 59
|
||||
end
|
||||
|
||||
def open_all_day_and_closed_all_day
|
||||
return unless open_all_day? && closed_all_day?
|
||||
|
||||
errors.add(:base, 'open_all_day and closed_all_day cannot be true at the same time')
|
||||
end
|
||||
end
|
||||
|
|
|
@ -14,10 +14,11 @@
|
|||
|
||||
window.chatwootSettings = {
|
||||
hideMessageBubble: false,
|
||||
position: 'left',
|
||||
position: '<%= @widget_position %>',
|
||||
locale: 'en',
|
||||
type: 'expanded_bubble',
|
||||
type: '<%= @widget_type %>',
|
||||
showPopoutButton: true,
|
||||
widgetStyle: '<%= @widget_style %>',
|
||||
};
|
||||
|
||||
(function(d,t) {
|
||||
|
@ -36,14 +37,18 @@ window.chatwootSettings = {
|
|||
})(document,"script");
|
||||
|
||||
window.addEventListener('chatwoot:ready', function() {
|
||||
console.log(window.$chatwoot);
|
||||
console.log('chatwoot:ready', window.$chatwoot);
|
||||
if (window.location.search.includes('setUser')) {
|
||||
window.$chatwoot.setUser('<%= user_id %>', {
|
||||
identifier_hash: '<%= user_hash %>',
|
||||
identifier_hash: 'a<%= user_hash %>',
|
||||
email: 'jane@example.com',
|
||||
name: 'Jane Doe',
|
||||
phone_number: ''
|
||||
});
|
||||
}
|
||||
})
|
||||
|
||||
window.addEventListener('chatwoot:error', function(e) {
|
||||
console.log('chatwoot:error', e.details)
|
||||
})
|
||||
</script>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
shared: &shared
|
||||
version: '2.2.0'
|
||||
version: '2.2.1'
|
||||
|
||||
development:
|
||||
<<: *shared
|
||||
|
|
|
@ -47,3 +47,15 @@
|
|||
- name: DIRECT_UPLOADS_ENABLED
|
||||
value: false
|
||||
locked: false
|
||||
- name: HCAPTCHA_SITE_KEY
|
||||
value:
|
||||
locked: false
|
||||
- name: HCAPTCHA_SERVER_KEY
|
||||
value:
|
||||
locked: false
|
||||
- name: LOGOUT_REDIRECT_LINK
|
||||
value: /app/login
|
||||
locked: false
|
||||
- name: DISABLE_USER_PROFILE_UPDATE
|
||||
value: false
|
||||
locked: false
|
||||
|
|
|
@ -40,6 +40,7 @@ Rails.application.routes.draw do
|
|||
resource :contact_merge, only: [:create]
|
||||
end
|
||||
|
||||
resource :bulk_actions, only: [:create]
|
||||
resources :agents, only: [:index, :create, :update, :destroy]
|
||||
resources :agent_bots, only: [:index, :create, :show, :update, :destroy]
|
||||
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
class AddOpenAllDayToWorkingHour < ActiveRecord::Migration[6.1]
|
||||
def change
|
||||
add_column :working_hours, :open_all_day, :boolean, default: false
|
||||
end
|
||||
end
|
5
db/migrate/20220218120357_add_h_captcha_key.rb
Normal file
5
db/migrate/20220218120357_add_h_captcha_key.rb
Normal file
|
@ -0,0 +1,5 @@
|
|||
class AddHCaptchaKey < ActiveRecord::Migration[6.1]
|
||||
def change
|
||||
ConfigLoader.new.process
|
||||
end
|
||||
end
|
|
@ -10,7 +10,7 @@
|
|||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema.define(version: 2022_02_15_060751) do
|
||||
ActiveRecord::Schema.define(version: 2022_02_18_120357) do
|
||||
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "pg_stat_statements"
|
||||
|
@ -773,6 +773,7 @@ ActiveRecord::Schema.define(version: 2022_02_15_060751) do
|
|||
t.integer "close_minutes"
|
||||
t.datetime "created_at", precision: 6, null: false
|
||||
t.datetime "updated_at", precision: 6, null: false
|
||||
t.boolean "open_all_day", default: false
|
||||
t.index ["account_id"], name: "index_working_hours_on_account_id"
|
||||
t.index ["inbox_id"], name: "index_working_hours_on_inbox_id"
|
||||
end
|
||||
|
|
|
@ -35,7 +35,9 @@ COPY Gemfile Gemfile.lock ./
|
|||
|
||||
# natively compile grpc and protobuf to support alpine musl (dialogflow-docker workflow)
|
||||
# https://github.com/googleapis/google-cloud-ruby/issues/13306
|
||||
RUN apk add --no-cache musl ruby-full ruby-dev gcc make musl-dev openssl openssl-dev g++ linux-headers
|
||||
# adding xz as nokogiri was failing to build libxml
|
||||
# https://github.com/chatwoot/chatwoot/issues/4045
|
||||
RUN apk add --no-cache musl ruby-full ruby-dev gcc make musl-dev openssl openssl-dev g++ linux-headers xz
|
||||
RUN bundle config set --local force_ruby_platform true
|
||||
|
||||
# Do not install development or test gems in production
|
||||
|
|
25
lib/chatwoot_captcha.rb
Normal file
25
lib/chatwoot_captcha.rb
Normal file
|
@ -0,0 +1,25 @@
|
|||
class ChatwootCaptcha
|
||||
def initialize(client_response)
|
||||
@client_response = client_response
|
||||
@server_key = GlobalConfigService.load('HCAPTCHA_SERVER_KEY', '')
|
||||
end
|
||||
|
||||
def valid?
|
||||
return true if @server_key.blank?
|
||||
return false if @client_response.blank?
|
||||
|
||||
validate_client_response?
|
||||
end
|
||||
|
||||
def validate_client_response?
|
||||
response = HTTParty.post('https://hcaptcha.com/siteverify',
|
||||
body: {
|
||||
response: @client_response,
|
||||
secret: @server_key
|
||||
})
|
||||
|
||||
return unless response.success?
|
||||
|
||||
response.parsed_response['success']
|
||||
end
|
||||
end
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@chatwoot/chatwoot",
|
||||
"version": "2.2.0",
|
||||
"version": "2.2.1",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"eslint": "eslint app/**/*.{js,vue} --fix",
|
||||
|
@ -20,6 +20,7 @@
|
|||
"@braid/vue-formulate": "^2.5.2",
|
||||
"@chatwoot/prosemirror-schema": "https://github.com/chatwoot/prosemirror-schema.git#7e8acadd10d7b932c0dc0bd0a18f804434f83517",
|
||||
"@chatwoot/utils": "^0.0.3",
|
||||
"@hcaptcha/vue-hcaptcha": "^0.3.2",
|
||||
"@rails/actioncable": "6.1.3",
|
||||
"@rails/webpacker": "5.3.0",
|
||||
"@sentry/tracing": "^6.4.1",
|
||||
|
|
145
spec/controllers/api/v1/accounts/bulk_actions_controller_spec.rb
Normal file
145
spec/controllers/api/v1/accounts/bulk_actions_controller_spec.rb
Normal file
|
@ -0,0 +1,145 @@
|
|||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Api::V1::Accounts::BulkActionsController', type: :request do
|
||||
include ActiveJob::TestHelper
|
||||
let(:account) { create(:account) }
|
||||
let(:agent_1) { create(:user, account: account, role: :agent) }
|
||||
let(:agent_2) { create(:user, account: account, role: :agent) }
|
||||
|
||||
before do
|
||||
create(:conversation, account_id: account.id, status: :open)
|
||||
create(:conversation, account_id: account.id, status: :open)
|
||||
create(:conversation, account_id: account.id, status: :open)
|
||||
create(:conversation, account_id: account.id, status: :open)
|
||||
end
|
||||
|
||||
describe 'POST /api/v1/accounts/{account.id}/bulk_action' do
|
||||
context 'when it is an unauthenticated user' do
|
||||
let(:agent) { create(:user) }
|
||||
|
||||
it 'returns unauthorized' do
|
||||
post "/api/v1/accounts/#{account.id}/bulk_actions",
|
||||
headers: agent.create_new_auth_token,
|
||||
params: { type: 'Conversation', fields: { status: 'open' }, ids: [1, 2, 3] }
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an authenticated user' do
|
||||
let(:agent) { create(:user, account: account, role: :agent) }
|
||||
|
||||
it 'Ignores bulk_actions for wrong type' do
|
||||
post "/api/v1/accounts/#{account.id}/bulk_actions",
|
||||
headers: agent.create_new_auth_token,
|
||||
params: { type: 'Test', fields: { status: 'snoozed' }, ids: %w[1 2 3] }
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
end
|
||||
|
||||
it 'Bulk update conversation status' do
|
||||
expect(Conversation.first.status).to eq('open')
|
||||
expect(Conversation.last.status).to eq('open')
|
||||
expect(Conversation.first.assignee_id).to eq(nil)
|
||||
|
||||
perform_enqueued_jobs do
|
||||
post "/api/v1/accounts/#{account.id}/bulk_actions",
|
||||
headers: agent.create_new_auth_token,
|
||||
params: { type: 'Conversation', fields: { status: 'snoozed' }, ids: Conversation.first(3).pluck(:display_id) }
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
end
|
||||
|
||||
expect(Conversation.first.status).to eq('snoozed')
|
||||
expect(Conversation.last.status).to eq('open')
|
||||
expect(Conversation.first.assignee_id).to eq(nil)
|
||||
end
|
||||
|
||||
it 'Bulk update conversation assignee id' do
|
||||
params = { type: 'Conversation', fields: { assignee_id: agent_1.id }, ids: Conversation.first(3).pluck(:display_id) }
|
||||
|
||||
expect(Conversation.first.status).to eq('open')
|
||||
expect(Conversation.first.assignee_id).to eq(nil)
|
||||
expect(Conversation.second.assignee_id).to eq(nil)
|
||||
|
||||
perform_enqueued_jobs do
|
||||
post "/api/v1/accounts/#{account.id}/bulk_actions",
|
||||
headers: agent.create_new_auth_token,
|
||||
params: params
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
end
|
||||
|
||||
expect(Conversation.first.assignee_id).to eq(agent_1.id)
|
||||
expect(Conversation.second.assignee_id).to eq(agent_1.id)
|
||||
expect(Conversation.first.status).to eq('open')
|
||||
end
|
||||
|
||||
it 'Bulk update conversation status and assignee id' do
|
||||
params = { type: 'Conversation', fields: { assignee_id: agent_1.id, status: 'snoozed' }, ids: Conversation.first(3).pluck(:display_id) }
|
||||
|
||||
expect(Conversation.first.status).to eq('open')
|
||||
expect(Conversation.second.assignee_id).to eq(nil)
|
||||
|
||||
perform_enqueued_jobs do
|
||||
post "/api/v1/accounts/#{account.id}/bulk_actions",
|
||||
headers: agent.create_new_auth_token,
|
||||
params: params
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
end
|
||||
|
||||
expect(Conversation.first.assignee_id).to eq(agent_1.id)
|
||||
expect(Conversation.second.assignee_id).to eq(agent_1.id)
|
||||
expect(Conversation.first.status).to eq('snoozed')
|
||||
expect(Conversation.second.status).to eq('snoozed')
|
||||
end
|
||||
|
||||
it 'Bulk update conversation labels' do
|
||||
params = { type: 'Conversation', ids: Conversation.first(3).pluck(:display_id), labels: { add: %w[support priority_customer] } }
|
||||
|
||||
expect(Conversation.first.labels).to eq([])
|
||||
expect(Conversation.second.labels).to eq([])
|
||||
|
||||
perform_enqueued_jobs do
|
||||
post "/api/v1/accounts/#{account.id}/bulk_actions",
|
||||
headers: agent.create_new_auth_token,
|
||||
params: params
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
end
|
||||
|
||||
expect(Conversation.first.label_list).to eq(%w[support priority_customer])
|
||||
expect(Conversation.second.label_list).to eq(%w[support priority_customer])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST /api/v1/accounts/{account.id}/bulk_actions' do
|
||||
context 'when it is an authenticated user' do
|
||||
let(:agent) { create(:user, account: account, role: :agent) }
|
||||
|
||||
it 'Bulk delete conversation labels' do
|
||||
Conversation.first.add_labels(%w[support priority_customer])
|
||||
Conversation.second.add_labels(%w[support priority_customer])
|
||||
Conversation.third.add_labels(%w[support priority_customer])
|
||||
|
||||
params = { type: 'Conversation', ids: Conversation.first(3).pluck(:display_id), labels: { remove: %w[support] } }
|
||||
|
||||
expect(Conversation.first.label_list).to eq(%w[support priority_customer])
|
||||
expect(Conversation.second.label_list).to eq(%w[support priority_customer])
|
||||
|
||||
perform_enqueued_jobs do
|
||||
post "/api/v1/accounts/#{account.id}/bulk_actions",
|
||||
headers: agent.create_new_auth_token,
|
||||
params: params
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
end
|
||||
|
||||
expect(Conversation.first.label_list).to eq(['priority_customer'])
|
||||
expect(Conversation.second.label_list).to eq(['priority_customer'])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -30,6 +30,25 @@ RSpec.describe 'Accounts API', type: :request do
|
|||
end
|
||||
end
|
||||
|
||||
it 'calls ChatwootCaptcha' do
|
||||
with_modified_env ENABLE_ACCOUNT_SIGNUP: 'true' do
|
||||
captcha = double
|
||||
allow(account_builder).to receive(:perform).and_return([user, account])
|
||||
allow(ChatwootCaptcha).to receive(:new).and_return(captcha)
|
||||
allow(captcha).to receive(:valid?).and_return(true)
|
||||
|
||||
params = { account_name: 'test', email: email, user: nil, user_full_name: user_full_name, password: 'Password1!',
|
||||
h_captcha_client_response: '123' }
|
||||
|
||||
post api_v1_accounts_url,
|
||||
params: params,
|
||||
as: :json
|
||||
|
||||
expect(ChatwootCaptcha).to have_received(:new).with('123')
|
||||
expect(response.headers.keys).to include('access-token', 'token-type', 'client', 'expiry', 'uid')
|
||||
end
|
||||
end
|
||||
|
||||
it 'renders error response on invalid params' do
|
||||
with_modified_env ENABLE_ACCOUNT_SIGNUP: 'true' do
|
||||
allow(account_builder).to receive(:perform).and_return(nil)
|
||||
|
|
63
spec/jobs/bulk_actions_job_spec.rb
Normal file
63
spec/jobs/bulk_actions_job_spec.rb
Normal file
|
@ -0,0 +1,63 @@
|
|||
require 'rails_helper'
|
||||
|
||||
RSpec.describe BulkActionsJob, type: :job do
|
||||
params = {
|
||||
type: 'Conversation',
|
||||
fields: { status: 'snoozed' },
|
||||
ids: Conversation.first(3).pluck(:display_id)
|
||||
}
|
||||
|
||||
subject(:job) { described_class.perform_later(account: account, params: params, user: agent) }
|
||||
|
||||
let(:account) { create(:account) }
|
||||
let!(:agent) { create(:user, account: account, role: :agent) }
|
||||
let!(:conversation_1) { create(:conversation, account_id: account.id, status: :open) }
|
||||
let!(:conversation_2) { create(:conversation, account_id: account.id, status: :open) }
|
||||
let!(:conversation_3) { create(:conversation, account_id: account.id, status: :open) }
|
||||
|
||||
it 'enqueues the job' do
|
||||
expect { job }.to have_enqueued_job(described_class)
|
||||
.with(account: account, params: params, user: agent)
|
||||
.on_queue('medium')
|
||||
end
|
||||
|
||||
context 'when job is triggered' do
|
||||
let(:bulk_action_job) { double }
|
||||
|
||||
before do
|
||||
allow(bulk_action_job).to receive(:perform)
|
||||
end
|
||||
|
||||
it 'bulk updates the status' do
|
||||
params = {
|
||||
type: 'Conversation',
|
||||
fields: { status: 'snoozed', assignee_id: agent.id },
|
||||
ids: Conversation.first(3).pluck(:display_id)
|
||||
}
|
||||
|
||||
expect(Conversation.first.status).to eq('open')
|
||||
|
||||
described_class.perform_now(account: account, params: params, user: agent)
|
||||
|
||||
expect(conversation_1.reload.status).to eq('snoozed')
|
||||
expect(conversation_2.reload.status).to eq('snoozed')
|
||||
expect(conversation_3.reload.status).to eq('snoozed')
|
||||
end
|
||||
|
||||
it 'bulk updates the assignee_id' do
|
||||
params = {
|
||||
type: 'Conversation',
|
||||
fields: { status: 'snoozed', assignee_id: agent.id },
|
||||
ids: Conversation.first(3).pluck(:display_id)
|
||||
}
|
||||
|
||||
expect(Conversation.first.assignee_id).to eq(nil)
|
||||
|
||||
described_class.perform_now(account: account, params: params, user: agent)
|
||||
|
||||
expect(Conversation.first.assignee_id).to eq(agent.id)
|
||||
expect(Conversation.second.assignee_id).to eq(agent.id)
|
||||
expect(Conversation.third.assignee_id).to eq(agent.id)
|
||||
end
|
||||
end
|
||||
end
|
25
spec/lib/chatwoot_captcha_spec.rb
Normal file
25
spec/lib/chatwoot_captcha_spec.rb
Normal file
|
@ -0,0 +1,25 @@
|
|||
require 'rails_helper'
|
||||
|
||||
describe ChatwootCaptcha do
|
||||
it 'returns true if HCAPTCHA SERVER KEY is absent' do
|
||||
expect(described_class.new('random_key').valid?).to eq(true)
|
||||
end
|
||||
|
||||
context 'when HCAPTCHA SERVER KEY is present' do
|
||||
before do
|
||||
create(:installation_config, { name: 'HCAPTCHA_SERVER_KEY', value: 'hcaptch_server_key' })
|
||||
end
|
||||
|
||||
it 'returns false if client response is blank' do
|
||||
expect(described_class.new('').valid?).to eq false
|
||||
end
|
||||
|
||||
it 'returns true if client response is valid' do
|
||||
captcha_request = double
|
||||
allow(HTTParty).to receive(:post).and_return(captcha_request)
|
||||
allow(captcha_request).to receive(:success?).and_return(true)
|
||||
allow(captcha_request).to receive(:parsed_response).and_return({ 'success' => true })
|
||||
expect(described_class.new('valid_response').valid?).to eq true
|
||||
end
|
||||
end
|
||||
end
|
|
@ -71,15 +71,15 @@ describe Integrations::Slack::SendOnSlackService do
|
|||
attachment = message.attachments.new(account_id: message.account_id, file_type: :image)
|
||||
attachment.file.attach(io: File.open(Rails.root.join('spec/assets/avatar.png')), filename: 'avatar.png', content_type: 'image/png')
|
||||
|
||||
expect(slack_client).to receive(:files_upload).with(
|
||||
channels: hook.reference_id,
|
||||
initial_comment: 'Attached File!',
|
||||
content: anything,
|
||||
filename: attachment.file.filename,
|
||||
filetype: 'png',
|
||||
thread_ts: conversation.identifier,
|
||||
title: anything
|
||||
).and_return(file_attachment)
|
||||
expect(slack_client).to receive(:files_upload).with(hash_including(
|
||||
channels: hook.reference_id,
|
||||
thread_ts: conversation.identifier,
|
||||
initial_comment: 'Attached File!',
|
||||
filetype: 'png',
|
||||
content: anything,
|
||||
filename: attachment.file.filename,
|
||||
title: attachment.file.filename
|
||||
)).and_return(file_attachment)
|
||||
|
||||
message.save!
|
||||
|
||||
|
|
|
@ -90,6 +90,20 @@ RSpec.describe Conversation, type: :model do
|
|||
.with(described_class::CONVERSATION_READ, kind_of(Time), conversation: conversation, notifiable_assignee_change: true)
|
||||
expect(Rails.configuration.dispatcher).to have_received(:dispatch)
|
||||
.with(described_class::ASSIGNEE_CHANGED, kind_of(Time), conversation: conversation, notifiable_assignee_change: true)
|
||||
expect(Rails.configuration.dispatcher).to have_received(:dispatch)
|
||||
.with(described_class::CONVERSATION_UPDATED, kind_of(Time), conversation: conversation, notifiable_assignee_change: true)
|
||||
end
|
||||
|
||||
it 'will not run conversation_updated event for empty updates' do
|
||||
conversation.save!
|
||||
expect(Rails.configuration.dispatcher).not_to have_received(:dispatch)
|
||||
.with(described_class::CONVERSATION_UPDATED, kind_of(Time), conversation: conversation, notifiable_assignee_change: true)
|
||||
end
|
||||
|
||||
it 'will not run conversation_updated event for non whitelisted keys' do
|
||||
conversation.update(updated_at: DateTime.now.utc)
|
||||
expect(Rails.configuration.dispatcher).not_to have_received(:dispatch)
|
||||
.with(described_class::CONVERSATION_UPDATED, kind_of(Time), conversation: conversation, notifiable_assignee_change: true)
|
||||
end
|
||||
|
||||
it 'creates conversation activities' do
|
||||
|
|
|
@ -50,4 +50,42 @@ RSpec.describe WorkingHour do
|
|||
expect(described_class.today.closed_now?).to be true
|
||||
end
|
||||
end
|
||||
|
||||
context 'when open_all_day is true' do
|
||||
let(:inbox) { create(:inbox) }
|
||||
|
||||
before do
|
||||
Time.zone = 'UTC'
|
||||
inbox.working_hours.find_by(day_of_week: 5).update(open_all_day: true)
|
||||
travel_to '18.02.2022 11:00'.to_datetime
|
||||
end
|
||||
|
||||
it 'updates open hour and close hour' do
|
||||
expect(described_class.today.open_all_day?).to be true
|
||||
expect(described_class.today.open_hour).to be 0
|
||||
expect(described_class.today.open_minutes).to be 0
|
||||
expect(described_class.today.close_hour).to be 23
|
||||
expect(described_class.today.close_minutes).to be 59
|
||||
end
|
||||
end
|
||||
|
||||
context 'when open_all_day and closed_all_day true at the same time' do
|
||||
let(:inbox) { create(:inbox) }
|
||||
|
||||
before do
|
||||
Time.zone = 'UTC'
|
||||
inbox.working_hours.find_by(day_of_week: 5).update(open_all_day: true)
|
||||
travel_to '18.02.2022 11:00'.to_datetime
|
||||
end
|
||||
|
||||
it 'throws validation error' do
|
||||
working_hour = inbox.working_hours.find_by(day_of_week: 5)
|
||||
working_hour.closed_all_day = true
|
||||
expect(working_hour.invalid?).to be true
|
||||
expect do
|
||||
working_hour.save!
|
||||
end.to raise_error(ActiveRecord::RecordInvalid,
|
||||
'Validation failed: open_all_day and closed_all_day cannot be true at the same time')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -162,7 +162,7 @@ describe ::Contacts::FilterService do
|
|||
expected_count = Contact.where("created_at < ? AND custom_attributes->>'customer_type' = ?", Date.tomorrow, 'platinum').count
|
||||
|
||||
expect(result[:contacts].length).to be expected_count
|
||||
expect(result[:contacts].first.id).to eq(el_contact.id)
|
||||
expect(result[:contacts].pluck(:id)).to include(el_contact.id)
|
||||
end
|
||||
|
||||
context 'with x_days_before filter' do
|
||||
|
|
11
yarn.lock
11
yarn.lock
|
@ -1283,6 +1283,11 @@
|
|||
postcss "7.0.32"
|
||||
purgecss "^2.3.0"
|
||||
|
||||
"@hcaptcha/vue-hcaptcha@^0.3.2":
|
||||
version "0.3.2"
|
||||
resolved "https://registry.yarnpkg.com/@hcaptcha/vue-hcaptcha/-/vue-hcaptcha-0.3.2.tgz#0f77d6fc19bc47eadb6b2181eee5fc132441a942"
|
||||
integrity sha512-JiJsAJh+fSe+uf9N3ek7CKzX/r79+hx+rMPch+e2/h9+Ei3VyJtb2Dgk1DhG/dyUdrooPIzkNMr6gfo75Cn22g==
|
||||
|
||||
"@istanbuljs/load-nyc-config@^1.0.0":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced"
|
||||
|
@ -14838,9 +14843,9 @@ url-loader@^4.1.1:
|
|||
schema-utils "^3.0.0"
|
||||
|
||||
url-parse@^1.4.3, url-parse@^1.5.1:
|
||||
version "1.5.3"
|
||||
resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.3.tgz#71c1303d38fb6639ade183c2992c8cc0686df862"
|
||||
integrity sha512-IIORyIQD9rvj0A4CLWsHkBBJuNqWpFQe224b6j9t/ABmquIS0qDU2pY6kl6AuOrL5OkCXHMCFNe1jBcuAggjvQ==
|
||||
version "1.5.7"
|
||||
resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.7.tgz#00780f60dbdae90181f51ed85fb24109422c932a"
|
||||
integrity sha512-HxWkieX+STA38EDk7CE9MEryFeHCKzgagxlGvsdS7WBImq9Mk+PGwiT56w82WI3aicwJA8REp42Cxo98c8FZMA==
|
||||
dependencies:
|
||||
querystringify "^2.1.1"
|
||||
requires-port "^1.0.0"
|
||||
|
|
Loading…
Reference in a new issue