Compare commits
439
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dcd18072b4 | ||
|
|
4244945e7b | ||
|
|
17725e4dd0 | ||
|
|
d55bb63aa0 | ||
|
|
bda2d1d9a4 | ||
|
|
6e22695b85 | ||
|
|
146e46d79f | ||
|
|
0af27a2387 | ||
|
|
7380f0e7ce | ||
|
|
2c5a0abcce | ||
|
|
f23e33c1a9 | ||
|
|
5588d6e344 | ||
|
|
59ace66c6f | ||
|
|
decef1ad52 | ||
|
|
0e19a4196b | ||
|
|
04dd65687b | ||
|
|
5f503b1a57 | ||
|
|
5f6e974531 | ||
|
|
a023eabf59 | ||
|
|
cb1e25a487 | ||
|
|
bcf7c6c45a | ||
|
|
b411b6ca21 | ||
|
|
cd217a3f86 | ||
|
|
6c79642450 | ||
|
|
201d476720 | ||
|
|
c1a2e67c9e | ||
|
|
1948bef9ac | ||
|
|
5486f39f7e | ||
|
|
f018b0013c | ||
|
|
950f085e80 | ||
|
|
268e26625b | ||
|
|
7041d86e4d | ||
|
|
78398ee800 | ||
|
|
7b09b02737 | ||
|
|
af7631d9f1 | ||
|
|
e0f33e62af | ||
|
|
5d224f2e04 | ||
|
|
4cb981d81b | ||
|
|
f891cfbf47 | ||
|
|
e54c8a857d | ||
|
|
66229b0d6b | ||
|
|
3eb1d49577 | ||
|
|
677888bcde | ||
|
|
abbb4180ea | ||
|
|
d1cbc56d67 | ||
|
|
b579ff832e | ||
|
|
39d0748a5b | ||
|
|
b4d20689b7 | ||
|
|
3b84b0fc47 | ||
|
|
23ea829510 | ||
|
|
ebe9daea00 | ||
|
|
36b6c0cb9c | ||
|
|
16c36a78f0 | ||
|
|
4999f7ed23 | ||
|
|
e8f4ac632f | ||
|
|
3c7854ccdf | ||
|
|
1e70223ed7 | ||
|
|
f34cd3ea61 | ||
|
|
68b8d721a7 | ||
|
|
d876419b27 | ||
|
|
8455186e9f | ||
|
|
b6584ec68b | ||
|
|
29f18c7f18 | ||
|
|
11b27f9805 | ||
|
|
653e0335c0 | ||
|
|
013dab7da6 | ||
|
|
2ba81830f3 | ||
|
|
dfba4770bd | ||
|
|
63e702ca12 | ||
|
|
394fece846 | ||
|
|
24fbab94c3 | ||
|
|
61e03fa33a | ||
|
|
6c4b92f7f6 | ||
|
|
f023325b0e | ||
|
|
dd05482cb0 | ||
|
|
d94108bf3f | ||
|
|
ab872beb1d | ||
|
|
d02cfff4cc | ||
|
|
736f269c49 | ||
|
|
b067acfd4a | ||
|
|
232fa6fd18 | ||
|
|
483d55df91 | ||
|
|
3e54d3654b | ||
|
|
b6831d464e | ||
|
|
8c67ea56e7 | ||
|
|
54bc4c23df | ||
|
|
58f47eb02c | ||
|
|
4fc62ed9ae | ||
|
|
4a89bab23a | ||
|
|
7438f3b157 | ||
|
|
9551dbc25f | ||
|
|
2e54f4a67c | ||
|
|
6d9b2a6b33 | ||
|
|
35a9acf099 | ||
|
|
b9694a0818 | ||
|
|
7416bbb25e | ||
|
|
b94c89ebf1 | ||
|
|
78ce8a4652 | ||
|
|
04c874fe35 | ||
|
|
eabe548520 | ||
|
|
83231b9678 | ||
|
|
f96b54e57f | ||
|
|
d3bf828524 | ||
|
|
c20415f3ba | ||
|
|
6b2ed4fe79 | ||
|
|
eca9ca90f1 | ||
|
|
9018ec9678 | ||
|
|
e55fe33f1b | ||
|
|
3b0788c3be | ||
|
|
626e67b859 | ||
|
|
28db18ebbd | ||
|
|
b28721e10b | ||
|
|
f77db4d814 | ||
|
|
62d8ec7edb | ||
|
|
7b09fa4a03 | ||
|
|
980013abae | ||
|
|
5c729ccea5 | ||
|
|
ae52ca87aa | ||
|
|
415bb23c37 | ||
|
|
9e173fca3c | ||
|
|
bd918ee506 | ||
|
|
7c9884b853 | ||
|
|
7ffa669c5c | ||
|
|
0bc20873f6 | ||
|
|
88de3359a5 | ||
|
|
a3d008da06 | ||
|
|
cbae95422d | ||
|
|
081c845c56 | ||
|
|
1e54d6deb3 | ||
|
|
bb5c6a416a | ||
|
|
6a07251007 | ||
|
|
759a66dd21 | ||
|
|
8189dd932c | ||
|
|
b6ba0f343e | ||
|
|
86ca90aa15 | ||
|
|
26e8877cd9 | ||
|
|
1b63adfb2e | ||
|
|
e27274a5a8 | ||
|
|
e5c198f839 | ||
|
|
72ba429159 | ||
|
|
3227cf7ff3 | ||
|
|
f0740120a8 | ||
|
|
3ea54065b1 | ||
|
|
6f19546c3c | ||
|
|
5c9ab21617 | ||
|
|
b71a580573 | ||
|
|
6a73953003 | ||
|
|
336af1ac9a | ||
|
|
e4de366b8d | ||
|
|
373c3dcd6c | ||
|
|
4b12dd89b0 | ||
|
|
c19cfeaa81 | ||
|
|
fa118ad18c | ||
|
|
249bfe44bb | ||
|
|
826d9ec5a7 | ||
|
|
d8b53f5d2f | ||
|
|
08ced452e1 | ||
|
|
24fe3805d8 | ||
|
|
845b0c0a68 | ||
|
|
8ce7539c5e | ||
|
|
5ee980465f | ||
|
|
fd633e1613 | ||
|
|
12a64f1b10 | ||
|
|
ffc2d98fb8 | ||
|
|
f7f04f788e | ||
|
|
a88d155dd7 | ||
|
|
e8b7e791a5 | ||
|
|
b18cac77fb | ||
|
|
647161121e | ||
|
|
cbbe939662 | ||
|
|
30cb141e5e | ||
|
|
b0d5c438ac | ||
|
|
3eefa4daee | ||
|
|
ddce999385 | ||
|
|
ff915dd2ea | ||
|
|
dd0930d75e | ||
|
|
3f0d96c24d | ||
|
|
c16b801562 | ||
|
|
018c468303 | ||
|
|
48bf8d08e5 | ||
|
|
fabb3c8da4 | ||
|
|
bdeb2f9812 | ||
|
|
53dc38e650 | ||
|
|
27fc24375d | ||
|
|
f999777a2d | ||
|
|
71d1e98765 | ||
|
|
2c93d563c1 | ||
|
|
f1b556d4a3 | ||
|
|
73c119cd3d | ||
|
|
0ad2f94151 | ||
|
|
f8cb806548 | ||
|
|
9ba5adfd60 | ||
|
|
0dd5104acb | ||
|
|
0968aead6c | ||
|
|
53d530b815 | ||
|
|
2429daa45c | ||
|
|
86894b27b5 | ||
|
|
05df4d4212 | ||
|
|
47fd0ea2b2 | ||
|
|
30b8a365dc | ||
|
|
29110ffd6b | ||
|
|
e9950afb1a | ||
|
|
f21298aeb9 | ||
|
|
7b6a56714f | ||
|
|
94a20af9db | ||
|
|
616371adbb | ||
|
|
2d4ef0c328 | ||
|
|
5e6e234afe | ||
|
|
18e9e771b5 | ||
|
|
0245e01763 | ||
|
|
fcd40e9636 | ||
|
|
4f5c5e9f85 | ||
|
|
cb07ac16d1 | ||
|
|
4fa299ff15 | ||
|
|
71e9566854 | ||
|
|
272f920811 | ||
|
|
e39d19b1e8 | ||
|
|
6c39aed882 | ||
|
|
8d43101892 | ||
|
|
f05535c5ad | ||
|
|
4503ba018d | ||
|
|
8bc2bc6d40 | ||
|
|
bfd192ebb2 | ||
|
|
6e13d22ff7 | ||
|
|
3e8b6e1aee | ||
|
|
3038aaef67 | ||
|
|
57a68666cb | ||
|
|
4acb824857 | ||
|
|
5c74674c2b | ||
|
|
52e49d5f9d | ||
|
|
e529e1206e | ||
|
|
a8009c7b39 | ||
|
|
99bb074656 | ||
|
|
9322112481 | ||
|
|
ce4cfee8bd | ||
|
|
f31fc2b375 | ||
|
|
e5f7807833 | ||
|
|
2483d99d3c | ||
|
|
336584c95a | ||
|
|
9ebabb9832 | ||
|
|
2acf09b3eb | ||
|
|
d18d3e9e2e | ||
|
|
deec1d213b | ||
|
|
25c3fb3c36 | ||
|
|
e2a6dc3e04 | ||
|
|
57feedbf25 | ||
|
|
71310c21ad | ||
|
|
623cd72897 | ||
|
|
04aa13e8aa | ||
|
|
6ab964b161 | ||
|
|
5598b4b27e | ||
|
|
64ae9f625a | ||
|
|
3dd3b7b3aa | ||
|
|
f451e59aec | ||
|
|
c04f67e8a5 | ||
|
|
6ddc99d066 | ||
|
|
4cbb3208e8 | ||
|
|
5f2a720920 | ||
|
|
acb7debd3f | ||
|
|
44f6a9ec3c | ||
|
|
24468d71fb | ||
|
|
26ef21a243 | ||
|
|
18235d3fb5 | ||
|
|
55972cc17a | ||
|
|
2daf1ae827 | ||
|
|
22421b5ac8 | ||
|
|
0d41853b0b | ||
|
|
355926897a | ||
|
|
4101a7b566 | ||
|
|
2d31535620 | ||
|
|
c54b469dcb | ||
|
|
29e8e41443 | ||
|
|
178bc80b25 | ||
|
|
ab039e14fd | ||
|
|
961c911c26 | ||
|
|
12ae49089b | ||
|
|
996f842882 | ||
|
|
616f702871 | ||
|
|
8015490aba | ||
|
|
fe8a67b76a | ||
|
|
b89c917198 | ||
|
|
7b8a3fcae0 | ||
|
|
2df83276e0 | ||
|
|
7b68a7639f | ||
|
|
e3b8c1fbb5 | ||
|
|
310c127693 | ||
|
|
37c0f1306c | ||
|
|
396ef02b0c | ||
|
|
ebefb2e201 | ||
|
|
6f09f20991 | ||
|
|
cfe86d9c06 | ||
|
|
84240e197a | ||
|
|
bafff90f18 | ||
|
|
961d810645 | ||
|
|
1a06bfd69c | ||
|
|
1cac2e86c3 | ||
|
|
04287a0924 | ||
|
|
c1c3a62412 | ||
|
|
b5dec067a1 | ||
|
|
f96f166196 | ||
|
|
9ed701065e | ||
|
|
180befbd42 | ||
|
|
4baa4363f4 | ||
|
|
1c8f41f387 | ||
|
|
7f0ca8b15d | ||
|
|
4c75bb68f4 | ||
|
|
ed82eb2932 | ||
|
|
b62166a18a | ||
|
|
bf2b75b281 | ||
|
|
c084ad5a68 | ||
|
|
dd3e658f19 | ||
|
|
453d7ad9fc | ||
|
|
47b6967dad | ||
|
|
d06cf27800 | ||
|
|
7f6a3b781f | ||
|
|
fde2b180fe | ||
|
|
2a1662c781 | ||
|
|
10d6e9551d | ||
|
|
69d46f278a | ||
|
|
0921a7c817 | ||
|
|
2a73f124ca | ||
|
|
ef33c60652 | ||
|
|
3a547de909 | ||
|
|
62e9fc1bc5 | ||
|
|
e052a061f4 | ||
|
|
9efadf8804 | ||
|
|
c8a40932cc | ||
|
|
9ddd428935 | ||
|
|
4d8ba0148c | ||
|
|
65c4ad70e1 | ||
|
|
9636478c2a | ||
|
|
65190422c4 | ||
|
|
a7bc855486 | ||
|
|
ff97536095 | ||
|
|
fcf0ba12d0 | ||
|
|
2cc82af344 | ||
|
|
c00855fce4 | ||
|
|
2d07577731 | ||
|
|
4c98006837 | ||
|
|
89e09857af | ||
|
|
12c338364e | ||
|
|
846f520ad2 | ||
|
|
6cbe1ed911 | ||
|
|
6c1ee4d965 | ||
|
|
1d8341504a | ||
|
|
f8ae6cd95c | ||
|
|
63dd2c5a93 | ||
|
|
1c47478104 | ||
|
|
39f14bebbd | ||
|
|
4b27fdf4db | ||
|
|
a6a0e78bbe | ||
|
|
703e19304d | ||
|
|
fa7bbdb0b3 | ||
|
|
d837065545 | ||
|
|
2a6f3356c9 | ||
|
|
c83105ce4f | ||
|
|
56d0b220f4 | ||
|
|
40ec0d109a | ||
|
|
480f34803b | ||
|
|
30f3928904 | ||
|
|
d188600559 | ||
|
|
4828071fc3 | ||
|
|
a3d21024a6 | ||
|
|
2b4f4f0b5c | ||
|
|
d66eb8b21f | ||
|
|
27419eef66 | ||
|
|
d7566c453d | ||
|
|
25ed66edf5 | ||
|
|
f72be94323 | ||
|
|
ea825d49da | ||
|
|
1d718b92b7 | ||
|
|
f3f665cabb | ||
|
|
8babf7dec3 | ||
|
|
5b480f563d | ||
|
|
3a77e672f8 | ||
|
|
3e75ac2de5 | ||
|
|
91c1061214 | ||
|
|
ec65b43993 | ||
|
|
1a81245987 | ||
|
|
2c3ad73542 | ||
|
|
59b1d59574 | ||
|
|
9de2edd300 | ||
|
|
66751b71dd | ||
|
|
7e415fbee3 | ||
|
|
50a927bac2 | ||
|
|
19ff738211 | ||
|
|
7c080fa9fa | ||
|
|
91e2da5e74 | ||
|
|
21691f2407 | ||
|
|
d495411ef5 | ||
|
|
918e1f25c2 | ||
|
|
10dd0ba647 | ||
|
|
550bea099c | ||
|
|
c7bf93ef8a | ||
|
|
ec7434d000 | ||
|
|
941e7e06cf | ||
|
|
09f46aa912 | ||
|
|
332ab5888c | ||
|
|
a940f4c1d2 | ||
|
|
5b4f92ccd2 | ||
|
|
ad03be4529 | ||
|
|
571e6bd0ec | ||
|
|
528da1d2cf | ||
|
|
4e8d17f017 | ||
|
|
3054a4cb59 | ||
|
|
71837bedf9 | ||
|
|
3c4514c9f7 | ||
|
|
4c7c1cd78a | ||
|
|
985b3f3bfc | ||
|
|
986abde531 | ||
|
|
b57063a8b8 | ||
|
|
022f4f899f | ||
|
|
191b8a64fe | ||
|
|
ea78020f2f | ||
|
|
dc1a41b136 | ||
|
|
28e7a5d228 | ||
|
|
40830046e8 | ||
|
|
d05c953eef | ||
|
|
4a770fdea7 | ||
|
|
239b1e2bf1 | ||
|
|
f963e00731 | ||
|
|
155a5b9947 | ||
|
|
26a75b250d | ||
|
|
20b84ee6a1 | ||
|
|
f1f14312fe | ||
|
|
212d7caab1 | ||
|
|
9cda16d0e8 | ||
|
|
a8acbb7427 | ||
|
|
1176e5eb8a | ||
|
|
996325f35b | ||
|
|
4f8ce7b597 | ||
|
|
f8e631a00c | ||
|
|
54bf33083e | ||
|
|
533f9f7fe4 | ||
|
|
82e5fc413e | ||
|
|
93daaea19b | ||
|
|
e6a49b5800 | ||
|
|
e204fe1c1f | ||
|
|
08fa370261 |
+26
-27
@@ -12,12 +12,12 @@ defaults: &defaults
|
||||
# Specify service dependencies here if necessary
|
||||
# CircleCI maintains a library of pre-built images
|
||||
# documented at https://circleci.com/docs/2.0/circleci-images/
|
||||
- image: cimg/postgres:14.1
|
||||
- image: cimg/postgres:15.3
|
||||
- image: cimg/redis:6.2.6
|
||||
environment:
|
||||
- RAILS_LOG_TO_STDOUT: false
|
||||
- COVERAGE: true
|
||||
- LOG_LEVEL: warn
|
||||
- RAILS_LOG_TO_STDOUT: false
|
||||
- COVERAGE: true
|
||||
- LOG_LEVEL: warn
|
||||
parallelism: 4
|
||||
resource_class: large
|
||||
|
||||
@@ -46,7 +46,7 @@ jobs:
|
||||
export NVM_DIR="$HOME/.nvm"
|
||||
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
|
||||
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"
|
||||
nvm install v16
|
||||
nvm install v20
|
||||
echo 'export NVM_DIR="$HOME/.nvm"' >> $BASH_ENV
|
||||
echo '[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"' >> $BASH_ENV
|
||||
|
||||
@@ -64,7 +64,6 @@ jobs:
|
||||
- ~/.bundle
|
||||
key: chatwoot-bundle-{{ .Environment.CACHE_VERSION }}-v20220524-{{ checksum "Gemfile.lock" }}
|
||||
|
||||
|
||||
# Only necessary if app uses webpacker or yarn in some other way
|
||||
- restore_cache:
|
||||
keys:
|
||||
@@ -73,7 +72,7 @@ jobs:
|
||||
|
||||
- run:
|
||||
name: yarn
|
||||
command: yarn install --cache-folder ~/.cache/yarn
|
||||
command: yarn install --frozen-lockfile --cache-folder ~/.cache/yarn
|
||||
|
||||
# Store yarn / webpacker cache
|
||||
- save_cache:
|
||||
@@ -82,7 +81,7 @@ jobs:
|
||||
- ~/.cache/yarn
|
||||
|
||||
- run:
|
||||
name: Download cc-test-reporter
|
||||
name: Download cc-test-reporter
|
||||
command: |
|
||||
mkdir -p ~/tmp
|
||||
curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ~/tmp/cc-test-reporter
|
||||
@@ -104,9 +103,8 @@ jobs:
|
||||
fi
|
||||
curl -L https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/6.3.0/openapi-generator-cli-6.3.0.jar > ~/tmp/openapi-generator-cli-6.3.0.jar
|
||||
java -jar ~/tmp/openapi-generator-cli-6.3.0.jar validate -i swagger/swagger.json
|
||||
|
||||
|
||||
# Database setup
|
||||
- run: yarn install --check-files
|
||||
- run: bundle exec rake db:create
|
||||
- run: bundle exec rake db:schema:load
|
||||
|
||||
@@ -117,7 +115,7 @@ jobs:
|
||||
- run:
|
||||
name: Rubocop
|
||||
command: bundle exec rubocop
|
||||
|
||||
|
||||
# - run:
|
||||
# name: Brakeman
|
||||
# command: bundle exec brakeman
|
||||
@@ -126,6 +124,21 @@ jobs:
|
||||
name: eslint
|
||||
command: yarn run eslint
|
||||
|
||||
# Run frontend tests
|
||||
- run:
|
||||
name: Run frontend tests
|
||||
command: |
|
||||
mkdir -p ~/tmp/test-results/frontend_specs
|
||||
~/tmp/cc-test-reporter before-build
|
||||
TESTFILES=$(circleci tests glob **/specs/*.spec.js | circleci tests split --split-by=timings)
|
||||
yarn test:coverage --profile 10 \
|
||||
--out ~/tmp/test-results/yarn.xml \
|
||||
-- ${TESTFILES}
|
||||
- run:
|
||||
name: Code Climate Test Coverage
|
||||
command: |
|
||||
~/tmp/cc-test-reporter format-coverage -t lcov -o "coverage/codeclimate.frontend_$CIRCLE_NODE_INDEX.json"
|
||||
|
||||
# Run rails tests
|
||||
- run:
|
||||
name: Run backend tests
|
||||
@@ -145,20 +158,6 @@ jobs:
|
||||
command: |
|
||||
~/tmp/cc-test-reporter format-coverage -t simplecov -o "coverage/codeclimate.$CIRCLE_NODE_INDEX.json"
|
||||
|
||||
- run:
|
||||
name: Run frontend tests
|
||||
command: |
|
||||
mkdir -p ~/tmp/test-results/frontend_specs
|
||||
~/tmp/cc-test-reporter before-build
|
||||
TESTFILES=$(circleci tests glob **/specs/*.spec.js | circleci tests split --split-by=timings)
|
||||
yarn test:coverage --profile 10 \
|
||||
--out ~/tmp/test-results/yarn.xml \
|
||||
-- ${TESTFILES}
|
||||
- run:
|
||||
name: Code Climate Test Coverage
|
||||
command: |
|
||||
~/tmp/cc-test-reporter format-coverage -t lcov -o "coverage/codeclimate.frontend_$CIRCLE_NODE_INDEX.json"
|
||||
|
||||
- persist_to_workspace:
|
||||
root: coverage
|
||||
paths:
|
||||
@@ -182,7 +181,7 @@ jobs:
|
||||
- attach_workspace:
|
||||
at: ~/build
|
||||
- run:
|
||||
name: Download cc-test-reporter
|
||||
name: Download cc-test-reporter
|
||||
command: |
|
||||
mkdir -p ~/tmp
|
||||
curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ~/tmp/cc-test-reporter
|
||||
@@ -204,4 +203,4 @@ workflows:
|
||||
- build
|
||||
- upload-coverage:
|
||||
requires:
|
||||
- build
|
||||
- build
|
||||
|
||||
+30
-1
@@ -81,7 +81,7 @@ SMTP_OPENSSL_VERIFY_MODE=peer
|
||||
# Mail Incoming
|
||||
# This is the domain set for the reply emails when conversation continuity is enabled
|
||||
MAILER_INBOUND_EMAIL_DOMAIN=
|
||||
# Set this to appropriate ingress channel with regards to incoming emails
|
||||
# Set this to the appropriate ingress channel with regards to incoming emails
|
||||
# Possible values are :
|
||||
# relay for Exim, Postfix, Qmail
|
||||
# mailgun for Mailgun
|
||||
@@ -91,10 +91,20 @@ MAILER_INBOUND_EMAIL_DOMAIN=
|
||||
RAILS_INBOUND_EMAIL_SERVICE=
|
||||
# Use one of the following based on the email ingress service
|
||||
# Ref: https://edgeguides.rubyonrails.org/action_mailbox_basics.html
|
||||
# Set this to a password of your choice and use it in the Inbound webhook
|
||||
RAILS_INBOUND_EMAIL_PASSWORD=
|
||||
|
||||
MAILGUN_INGRESS_SIGNING_KEY=
|
||||
MANDRILL_INGRESS_API_KEY=
|
||||
|
||||
# Creating Your Inbound Webhook Instructions for Postmark and Sendgrid:
|
||||
# Inbound webhook URL format:
|
||||
# https://actionmailbox:[YOUR_RAILS_INBOUND_EMAIL_PASSWORD]@[YOUR_CHATWOOT_DOMAIN.COM]/rails/action_mailbox/[RAILS_INBOUND_EMAIL_SERVICE]/inbound_emails
|
||||
# Note: Replace the values inside the brackets; do not include the brackets themselves.
|
||||
# Example: https://actionmailbox:mYRandomPassword3@chatwoot.example.com/rails/action_mailbox/postmark/inbound_emails
|
||||
# For Postmark
|
||||
# Ensure the 'Include raw email content in JSON payload' checkbox is selected in the inbound webhook section.
|
||||
|
||||
# Storage
|
||||
ACTIVE_STORAGE_SERVICE=local
|
||||
|
||||
@@ -176,6 +186,9 @@ ANDROID_SHA256_CERT_FINGERPRINT=AC:73:8E:DE:EB:56:EA:CC:10:87:02:A7:65:37:7B:38:
|
||||
# MICROSOFT CLARITY
|
||||
# MS_CLARITY_TOKEN=xxxxxxxxx
|
||||
|
||||
# GOOGLE_TAG_MANAGER
|
||||
# GOOGLE_TAG = GTM-XXXXXXX
|
||||
|
||||
## Scout
|
||||
## https://scoutapm.com/docs/ruby/configuration
|
||||
# SCOUT_KEY=YOURKEY
|
||||
@@ -199,6 +212,8 @@ ANDROID_SHA256_CERT_FINGERPRINT=AC:73:8E:DE:EB:56:EA:CC:10:87:02:A7:65:37:7B:38:
|
||||
## Rack Attack configuration
|
||||
## To prevent and throttle abusive requests
|
||||
# ENABLE_RACK_ATTACK=true
|
||||
# RACK_ATTACK_LIMIT=300
|
||||
# ENABLE_RACK_ATTACK_WIDGET_API=true
|
||||
|
||||
## Running chatwoot as an API only server
|
||||
## setting this value to true will disable the frontend dashboard endpoints
|
||||
@@ -230,3 +245,17 @@ AZURE_APP_SECRET=
|
||||
## Change these values to fine tune performance
|
||||
# control the concurrency setting of sidekiq
|
||||
# SIDEKIQ_CONCURRENCY=10
|
||||
|
||||
|
||||
# AI powered features
|
||||
## OpenAI key
|
||||
# OPENAI_API_KEY=
|
||||
|
||||
# Sentiment analysis model file path
|
||||
SENTIMENT_FILE_PATH=
|
||||
|
||||
|
||||
# Housekeeping/Performance related configurations
|
||||
# Set to true if you want to remove stale contact inboxes
|
||||
# contact_inboxes with no conversation older than 90 days will be removed
|
||||
# REMOVE_STALE_CONTACT_INBOX_JOB_STATUS=false
|
||||
|
||||
+8
-3
@@ -4,9 +4,10 @@ module.exports = {
|
||||
'prettier',
|
||||
'plugin:vue/recommended',
|
||||
'plugin:storybook/recommended',
|
||||
'plugin:cypress/recommended',
|
||||
],
|
||||
parserOptions: {
|
||||
parser: 'babel-eslint',
|
||||
parser: '@babel/eslint-parser',
|
||||
ecmaVersion: 2020,
|
||||
sourceType: 'module',
|
||||
},
|
||||
@@ -24,13 +25,16 @@ module.exports = {
|
||||
'jsx-a11y/label-has-for': 'off',
|
||||
'jsx-a11y/anchor-is-valid': 'off',
|
||||
'import/no-unresolved': 'off',
|
||||
'vue/html-indent': 'off',
|
||||
'vue/multi-word-component-names': 'off',
|
||||
'vue/max-attributes-per-line': [
|
||||
'error',
|
||||
{
|
||||
singleline: 20,
|
||||
singleline: {
|
||||
max: 20,
|
||||
},
|
||||
multiline: {
|
||||
max: 1,
|
||||
allowFirstLine: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -47,6 +51,7 @@ module.exports = {
|
||||
},
|
||||
],
|
||||
'vue/no-v-html': 'off',
|
||||
'vue/component-definition-name-casing': 'off',
|
||||
'vue/singleline-html-element-content-newline': 'off',
|
||||
'import/extensions': ['off'],
|
||||
'no-console': 'error',
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
name: Log Lines Percentage Check
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- develop
|
||||
|
||||
jobs:
|
||||
log_lines_check:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Check for log lines and calculate percentage
|
||||
run: |
|
||||
# Define the log line pattern
|
||||
LOG_LINE_PATTERN="Rails\.logger"
|
||||
|
||||
# Get the list of changed files in the pull request
|
||||
CHANGED_FILES=$(git diff --name-only)
|
||||
|
||||
# Initialize a flag to track if any files have insufficient log lines
|
||||
INSUFFICIENT_LOGS=0
|
||||
|
||||
for file in $CHANGED_FILES; do
|
||||
if [[ $file =~ \.rb$ && ! $file =~ _spec\.rb$ ]]; then
|
||||
# Count the total number of lines in the file
|
||||
total_lines=$(wc -l < "$file")
|
||||
|
||||
# Count the number of log lines in the file
|
||||
log_lines=$(grep -c "$LOG_LINE_PATTERN" "$file")
|
||||
|
||||
# Calculate the percentage of log lines
|
||||
if [ "$total_lines" -gt 0 ]; then
|
||||
percentage=$(awk "BEGIN { pc=100*${log_lines}/${total_lines}; i=int(pc); print (pc-i<0.5)?i:i+1 }")
|
||||
else
|
||||
percentage=0
|
||||
fi
|
||||
|
||||
# Check if the percentage is less than 5%
|
||||
if [ "$percentage" -lt 5 ]; then
|
||||
echo "Error: Log lines percentage is less than 5% ($percentage%) in $file. Please add more log lines using Rails.logger statements."
|
||||
INSUFFICIENT_LOGS=1
|
||||
else
|
||||
echo "Log lines percentage is $percentage% in $file. Code looks good!"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
# If any files have insufficient log lines, fail the action
|
||||
if [ "$INSUFFICIENT_LOGS" -eq 1 ]; then
|
||||
exit 1
|
||||
fi
|
||||
@@ -18,11 +18,12 @@ jobs:
|
||||
runs-on: ubuntu-20.04
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:10.8
|
||||
image: postgres:15.3
|
||||
env:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: ""
|
||||
POSTGRES_DB: postgres
|
||||
POSTGRES_HOST_AUTH_METHOD: trust
|
||||
ports:
|
||||
- 5432:5432
|
||||
# needed because the postgres container does not provide a healthcheck
|
||||
@@ -51,7 +52,8 @@ jobs:
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
node-version: 20
|
||||
cache: yarn
|
||||
|
||||
- name: yarn
|
||||
run: yarn install
|
||||
@@ -74,6 +76,8 @@ jobs:
|
||||
- name: Run backend tests
|
||||
run: |
|
||||
bundle exec rspec --profile=10 --format documentation
|
||||
env:
|
||||
NODE_OPTIONS: --openssl-legacy-provider
|
||||
|
||||
- name: Upload rails log folder
|
||||
uses: actions/upload-artifact@v3
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
# #
|
||||
# # This workflow will run specs related to response bot
|
||||
# # This can only be activated in installations Where vector extension is available.
|
||||
# #
|
||||
|
||||
name: Run Response Bot spec
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
- master
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-20.04
|
||||
services:
|
||||
postgres:
|
||||
image: ankane/pgvector
|
||||
env:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: ""
|
||||
POSTGRES_DB: postgres
|
||||
POSTGRES_HOST_AUTH_METHOD: trust
|
||||
ports:
|
||||
- 5432:5432
|
||||
# needed because the postgres container does not provide a healthcheck
|
||||
# tmpfs makes DB faster by using RAM
|
||||
options: >-
|
||||
--mount type=tmpfs,destination=/var/lib/postgresql/data
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
redis:
|
||||
image: redis
|
||||
ports:
|
||||
- 6379:6379
|
||||
options: --entrypoint redis-server
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.ref }}
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
|
||||
- uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 20
|
||||
cache: yarn
|
||||
|
||||
- name: yarn
|
||||
run: yarn install
|
||||
|
||||
- name: Create database
|
||||
run: bundle exec rake db:create
|
||||
|
||||
- name: Seed database
|
||||
run: bundle exec rake db:schema:load
|
||||
|
||||
- name: Enable ResponseBotService in installation
|
||||
run: RAILS_ENV=test bundle exec rails runner "Features::ResponseBotService.new.enable_in_installation"
|
||||
|
||||
# Run Response Bot specs
|
||||
- name: Run backend tests
|
||||
run: |
|
||||
bundle exec rspec \
|
||||
spec/enterprise/controllers/api/v1/accounts/response_sources_controller_spec.rb \
|
||||
spec/enterprise/services/enterprise/message_templates/response_bot_service_spec.rb \
|
||||
spec/enterprise/controllers/enterprise/api/v1/accounts/inboxes_controller_spec.rb:47 \
|
||||
--profile=10 \
|
||||
--format documentation
|
||||
|
||||
- name: Upload rails log folder
|
||||
uses: actions/upload-artifact@v3
|
||||
if: always()
|
||||
with:
|
||||
name: rails-log-folder
|
||||
path: log
|
||||
@@ -21,7 +21,8 @@ jobs:
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
node-version: 20
|
||||
cache: 'yarn'
|
||||
|
||||
- name: yarn
|
||||
run: yarn install
|
||||
@@ -30,9 +31,11 @@ jobs:
|
||||
run: |
|
||||
rm -rf enterprise
|
||||
rm -rf spec/enterprise
|
||||
|
||||
|
||||
- name: Run asset compile
|
||||
run: bundle exec rake assets:precompile
|
||||
env:
|
||||
NODE_OPTIONS: --openssl-legacy-provider
|
||||
|
||||
- name: Size Check
|
||||
run: yarn run size
|
||||
|
||||
@@ -74,3 +74,5 @@ yalc.lock
|
||||
/yarn-error.log
|
||||
yarn-debug.log*
|
||||
.yarn-integrity
|
||||
|
||||
/storybook-static
|
||||
+6
-9
@@ -8,15 +8,11 @@ Layout/LineLength:
|
||||
Max: 150
|
||||
|
||||
Metrics/ClassLength:
|
||||
Max: 125
|
||||
Max: 175
|
||||
Exclude:
|
||||
- 'app/models/conversation.rb'
|
||||
- 'app/models/contact.rb'
|
||||
- 'app/mailers/conversation_reply_mailer.rb'
|
||||
- 'app/models/message.rb'
|
||||
- 'app/builders/messages/facebook/message_builder.rb'
|
||||
- 'app/controllers/api/v1/accounts/contacts_controller.rb'
|
||||
- 'app/listeners/action_cable_listener.rb'
|
||||
- 'app/models/conversation.rb'
|
||||
|
||||
RSpec/ExampleLength:
|
||||
Max: 25
|
||||
Style/Documentation:
|
||||
@@ -62,6 +58,7 @@ Metrics/BlockLength:
|
||||
Metrics/ModuleLength:
|
||||
Exclude:
|
||||
- lib/seeders/message_seeder.rb
|
||||
- spec/support/slack_stubs.rb
|
||||
Rails/ApplicationController:
|
||||
Exclude:
|
||||
- 'app/controllers/api/v1/widget/messages_controller.rb'
|
||||
@@ -73,7 +70,7 @@ Rails/ApplicationController:
|
||||
- 'app/controllers/survey/responses_controller.rb'
|
||||
Rails/FindEach:
|
||||
Enabled: true
|
||||
Include:
|
||||
Include:
|
||||
- 'app/**/*.rb'
|
||||
Rails/CompactBlank:
|
||||
Enabled: false
|
||||
@@ -87,6 +84,7 @@ Style/ClassAndModuleChildren:
|
||||
EnforcedStyle: compact
|
||||
Exclude:
|
||||
- 'config/application.rb'
|
||||
- 'config/initializers/monkey_patches/*'
|
||||
Style/MapToHash:
|
||||
Enabled: false
|
||||
Style/HashSyntax:
|
||||
@@ -188,7 +186,6 @@ RSpec/IndexedLet:
|
||||
RSpec/NamedSubject:
|
||||
Enabled: false
|
||||
|
||||
|
||||
# we should bring this down
|
||||
RSpec/MultipleMemoizedHelpers:
|
||||
Max: 14
|
||||
|
||||
+13
-1
@@ -23,6 +23,18 @@ module.exports = {
|
||||
},
|
||||
'@storybook/addon-links',
|
||||
'@storybook/addon-essentials',
|
||||
{
|
||||
/**
|
||||
* Fix Storybook issue with PostCSS@8
|
||||
* @see https://github.com/storybookjs/storybook/issues/12668#issuecomment-773958085
|
||||
*/
|
||||
name: '@storybook/addon-postcss',
|
||||
options: {
|
||||
postcssLoaderOptions: {
|
||||
implementation: require('postcss'),
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
webpackFinal: config => {
|
||||
const newConfig = {
|
||||
@@ -35,7 +47,7 @@ module.exports = {
|
||||
|
||||
newConfig.module.rules.push({
|
||||
test: /\.scss$/,
|
||||
use: ['style-loader', 'css-loader', 'sass-loader'],
|
||||
use: ['style-loader', 'css-loader', 'postcss-loader', 'sass-loader'],
|
||||
include: path.resolve(__dirname, '../app/javascript'),
|
||||
});
|
||||
|
||||
|
||||
@@ -4,10 +4,12 @@ import Vuex from 'vuex';
|
||||
import VueI18n from 'vue-i18n';
|
||||
import Vuelidate from 'vuelidate';
|
||||
import Multiselect from 'vue-multiselect';
|
||||
import VueDOMPurifyHTML from 'vue-dompurify-html';
|
||||
import FluentIcon from 'shared/components/FluentIcon/DashboardIcon';
|
||||
|
||||
import WootUiKit from '../app/javascript/dashboard/components';
|
||||
import i18n from '../app/javascript/dashboard/i18n';
|
||||
import { domPurifyConfig } from 'shared/helpers/HTMLSanitizer';
|
||||
|
||||
import '../app/javascript/dashboard/assets/scss/storybook.scss';
|
||||
|
||||
@@ -15,6 +17,8 @@ Vue.use(VueI18n);
|
||||
Vue.use(Vuelidate);
|
||||
Vue.use(WootUiKit);
|
||||
Vue.use(Vuex);
|
||||
Vue.use(VueDOMPurifyHTML, domPurifyConfig);
|
||||
|
||||
Vue.component('multiselect', Multiselect);
|
||||
Vue.component('fluent-icon', FluentIcon);
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ ruby '3.2.2'
|
||||
|
||||
##-- base gems for rails --##
|
||||
gem 'rack-cors', require: 'rack/cors'
|
||||
gem 'rails', '~> 7'
|
||||
gem 'rails', '~> 7.0.8.0'
|
||||
# Reduces boot times through caching; required in config/boot.rb
|
||||
gem 'bootsnap', require: false
|
||||
|
||||
@@ -15,7 +15,7 @@ gem 'browser'
|
||||
gem 'hashie'
|
||||
gem 'jbuilder'
|
||||
gem 'kaminari'
|
||||
gem 'responders'
|
||||
gem 'responders', '>= 3.1.1'
|
||||
gem 'rest-client'
|
||||
gem 'telephone_number'
|
||||
gem 'time_diff'
|
||||
@@ -34,7 +34,7 @@ gem 'commonmarker'
|
||||
# Validate Data against JSON Schema
|
||||
gem 'json_schemer'
|
||||
# Rack middleware for blocking & throttling abusive requests
|
||||
gem 'rack-attack'
|
||||
gem 'rack-attack', '>= 6.7.0'
|
||||
# a utility tool for streaming, flexible and safe downloading of remote files
|
||||
gem 'down'
|
||||
# authentication type to fetch and send mail over oauth2.0
|
||||
@@ -67,15 +67,16 @@ gem 'webpacker'
|
||||
gem 'barnes'
|
||||
|
||||
##--- gems for authentication & authorization ---##
|
||||
gem 'devise'
|
||||
gem 'devise', '>= 4.9.3'
|
||||
gem 'devise-secure_password', git: 'https://github.com/chatwoot/devise-secure_password', branch: 'chatwoot'
|
||||
gem 'devise_token_auth'
|
||||
# authorization
|
||||
gem 'jwt'
|
||||
gem 'pundit'
|
||||
# super admin
|
||||
gem 'administrate'
|
||||
gem 'administrate', '>= 0.19.0'
|
||||
gem 'administrate-field-active_storage'
|
||||
gem 'administrate-field-belongs_to_search'
|
||||
|
||||
##--- gems for pubsub service ---##
|
||||
# https://karolgalanciak.com/blog/2019/11/30/from-activerecord-callbacks-to-publish-slash-subscribe-pattern-and-event-driven-design/
|
||||
@@ -91,7 +92,7 @@ gem 'twitty', '~> 0.1.5'
|
||||
# facebook client
|
||||
gem 'koala'
|
||||
# slack client
|
||||
gem 'slack-ruby-client', '~> 2.0.0'
|
||||
gem 'slack-ruby-client', '~> 2.2.0'
|
||||
# for dialogflow integrations
|
||||
gem 'google-cloud-dialogflow-v2'
|
||||
gem 'grpc'
|
||||
@@ -106,16 +107,16 @@ gem 'google-cloud-translate-v3'
|
||||
gem 'ddtrace', require: false
|
||||
gem 'elastic-apm', require: false
|
||||
gem 'newrelic_rpm', require: false
|
||||
gem 'newrelic-sidekiq-metrics', require: false
|
||||
gem 'newrelic-sidekiq-metrics', '>= 1.6.2', require: false
|
||||
gem 'scout_apm', require: false
|
||||
gem 'sentry-rails', require: false
|
||||
gem 'sentry-rails', '>= 5.13.0', require: false
|
||||
gem 'sentry-ruby', require: false
|
||||
gem 'sentry-sidekiq', require: false
|
||||
gem 'sentry-sidekiq', '>= 5.13.0', require: false
|
||||
|
||||
##-- background job processing --##
|
||||
gem 'sidekiq'
|
||||
gem 'sidekiq', '>= 7.1.3'
|
||||
# We want cron jobs
|
||||
gem 'sidekiq-cron'
|
||||
gem 'sidekiq-cron', '>= 1.11.0'
|
||||
|
||||
##-- Push notification service --##
|
||||
gem 'fcm'
|
||||
@@ -153,18 +154,28 @@ gem 'stripe'
|
||||
gem 'faker'
|
||||
|
||||
# Include logrange conditionally in intializer using env variable
|
||||
gem 'lograge', '~> 0.12.0', require: false
|
||||
gem 'lograge', '~> 0.14.0', require: false
|
||||
|
||||
# worked with microsoft refresh token
|
||||
gem 'omniauth-oauth2'
|
||||
|
||||
gem 'audited', '~> 5.3'
|
||||
gem 'audited', '~> 5.4', '>= 5.4.1'
|
||||
|
||||
# need for google auth
|
||||
gem 'omniauth'
|
||||
gem 'omniauth-google-oauth2'
|
||||
gem 'omniauth-rails_csrf_protection', '~> 1.0'
|
||||
|
||||
## Gems for reponse bot
|
||||
# adds cosine similarity to postgres using vector extension
|
||||
gem 'neighbor'
|
||||
gem 'pgvector'
|
||||
# Convert Website HTML to Markdown
|
||||
gem 'reverse_markdown'
|
||||
|
||||
# Sentiment analysis
|
||||
gem 'informers'
|
||||
|
||||
### Gems required only in specific deployment environments ###
|
||||
##############################################################
|
||||
|
||||
@@ -178,7 +189,7 @@ group :development do
|
||||
gem 'bullet'
|
||||
gem 'letter_opener'
|
||||
gem 'scss_lint', require: false
|
||||
gem 'web-console'
|
||||
gem 'web-console', '>= 4.2.1'
|
||||
|
||||
# used in swagger build
|
||||
gem 'json_refs'
|
||||
@@ -187,8 +198,10 @@ group :development do
|
||||
gem 'squasher'
|
||||
|
||||
# profiling
|
||||
gem 'rack-mini-profiler', require: false
|
||||
gem 'rack-mini-profiler', '>= 3.1.1', require: false
|
||||
gem 'stackprof'
|
||||
# Should install the associated chrome extension to view query logs
|
||||
gem 'meta_request'
|
||||
end
|
||||
|
||||
group :test do
|
||||
@@ -210,6 +223,7 @@ group :development, :test do
|
||||
gem 'bundle-audit', require: false
|
||||
gem 'byebug', platform: :mri
|
||||
gem 'climate_control'
|
||||
gem 'debug', '~> 1.8'
|
||||
gem 'factory_bot_rails'
|
||||
gem 'listen'
|
||||
gem 'mock_redis'
|
||||
|
||||
+166
-120
@@ -33,70 +33,70 @@ GIT
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
actioncable (7.0.5)
|
||||
actionpack (= 7.0.5)
|
||||
activesupport (= 7.0.5)
|
||||
actioncable (7.0.8)
|
||||
actionpack (= 7.0.8)
|
||||
activesupport (= 7.0.8)
|
||||
nio4r (~> 2.0)
|
||||
websocket-driver (>= 0.6.1)
|
||||
actionmailbox (7.0.5)
|
||||
actionpack (= 7.0.5)
|
||||
activejob (= 7.0.5)
|
||||
activerecord (= 7.0.5)
|
||||
activestorage (= 7.0.5)
|
||||
activesupport (= 7.0.5)
|
||||
actionmailbox (7.0.8)
|
||||
actionpack (= 7.0.8)
|
||||
activejob (= 7.0.8)
|
||||
activerecord (= 7.0.8)
|
||||
activestorage (= 7.0.8)
|
||||
activesupport (= 7.0.8)
|
||||
mail (>= 2.7.1)
|
||||
net-imap
|
||||
net-pop
|
||||
net-smtp
|
||||
actionmailer (7.0.5)
|
||||
actionpack (= 7.0.5)
|
||||
actionview (= 7.0.5)
|
||||
activejob (= 7.0.5)
|
||||
activesupport (= 7.0.5)
|
||||
actionmailer (7.0.8)
|
||||
actionpack (= 7.0.8)
|
||||
actionview (= 7.0.8)
|
||||
activejob (= 7.0.8)
|
||||
activesupport (= 7.0.8)
|
||||
mail (~> 2.5, >= 2.5.4)
|
||||
net-imap
|
||||
net-pop
|
||||
net-smtp
|
||||
rails-dom-testing (~> 2.0)
|
||||
actionpack (7.0.5)
|
||||
actionview (= 7.0.5)
|
||||
activesupport (= 7.0.5)
|
||||
actionpack (7.0.8)
|
||||
actionview (= 7.0.8)
|
||||
activesupport (= 7.0.8)
|
||||
rack (~> 2.0, >= 2.2.4)
|
||||
rack-test (>= 0.6.3)
|
||||
rails-dom-testing (~> 2.0)
|
||||
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
||||
actiontext (7.0.5)
|
||||
actionpack (= 7.0.5)
|
||||
activerecord (= 7.0.5)
|
||||
activestorage (= 7.0.5)
|
||||
activesupport (= 7.0.5)
|
||||
actiontext (7.0.8)
|
||||
actionpack (= 7.0.8)
|
||||
activerecord (= 7.0.8)
|
||||
activestorage (= 7.0.8)
|
||||
activesupport (= 7.0.8)
|
||||
globalid (>= 0.6.0)
|
||||
nokogiri (>= 1.8.5)
|
||||
actionview (7.0.5)
|
||||
activesupport (= 7.0.5)
|
||||
actionview (7.0.8)
|
||||
activesupport (= 7.0.8)
|
||||
builder (~> 3.1)
|
||||
erubi (~> 1.4)
|
||||
rails-dom-testing (~> 2.0)
|
||||
rails-html-sanitizer (~> 1.1, >= 1.2.0)
|
||||
active_record_query_trace (1.8)
|
||||
activejob (7.0.5)
|
||||
activesupport (= 7.0.5)
|
||||
activejob (7.0.8)
|
||||
activesupport (= 7.0.8)
|
||||
globalid (>= 0.3.6)
|
||||
activemodel (7.0.5)
|
||||
activesupport (= 7.0.5)
|
||||
activerecord (7.0.5)
|
||||
activemodel (= 7.0.5)
|
||||
activesupport (= 7.0.5)
|
||||
activemodel (7.0.8)
|
||||
activesupport (= 7.0.8)
|
||||
activerecord (7.0.8)
|
||||
activemodel (= 7.0.8)
|
||||
activesupport (= 7.0.8)
|
||||
activerecord-import (1.4.1)
|
||||
activerecord (>= 4.2)
|
||||
activestorage (7.0.5)
|
||||
actionpack (= 7.0.5)
|
||||
activejob (= 7.0.5)
|
||||
activerecord (= 7.0.5)
|
||||
activesupport (= 7.0.5)
|
||||
activestorage (7.0.8)
|
||||
actionpack (= 7.0.8)
|
||||
activejob (= 7.0.8)
|
||||
activerecord (= 7.0.8)
|
||||
activesupport (= 7.0.8)
|
||||
marcel (~> 1.0)
|
||||
mini_mime (>= 1.1.0)
|
||||
activesupport (7.0.5)
|
||||
activesupport (7.0.8)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
i18n (>= 1.6, < 2)
|
||||
minitest (>= 5.1)
|
||||
@@ -105,7 +105,7 @@ GEM
|
||||
activerecord (>= 6.0, < 7.1)
|
||||
addressable (2.8.4)
|
||||
public_suffix (>= 2.0.2, < 6.0)
|
||||
administrate (0.18.0)
|
||||
administrate (0.19.0)
|
||||
actionpack (>= 5.0)
|
||||
actionview (>= 5.0)
|
||||
activerecord (>= 5.0)
|
||||
@@ -116,14 +116,19 @@ GEM
|
||||
administrate-field-active_storage (0.4.2)
|
||||
administrate (>= 0.2.2)
|
||||
rails (>= 7.0)
|
||||
administrate-field-belongs_to_search (0.8.0)
|
||||
administrate (>= 0.3, < 1.0)
|
||||
jbuilder (~> 2)
|
||||
rails (>= 4.2, < 7.1)
|
||||
selectize-rails (~> 0.6)
|
||||
annotate (3.2.0)
|
||||
activerecord (>= 3.2, < 8.0)
|
||||
rake (>= 10.4, < 14.0)
|
||||
ast (2.4.2)
|
||||
attr_extras (7.1.0)
|
||||
audited (5.3.3)
|
||||
activerecord (>= 5.0, < 7.1)
|
||||
request_store (~> 1.2)
|
||||
audited (5.4.1)
|
||||
activerecord (>= 5.0, < 7.7)
|
||||
activesupport (>= 5.0, < 7.7)
|
||||
aws-eventstream (1.2.0)
|
||||
aws-partitions (1.760.0)
|
||||
aws-sdk-core (3.171.1)
|
||||
@@ -143,8 +148,10 @@ GEM
|
||||
barnes (0.0.9)
|
||||
multi_json (~> 1)
|
||||
statsd-ruby (~> 1.1)
|
||||
bcrypt (3.1.18)
|
||||
base64 (0.1.1)
|
||||
bcrypt (3.1.19)
|
||||
bindex (0.8.1)
|
||||
blingfire (0.1.8)
|
||||
bootsnap (1.16.0)
|
||||
msgpack (~> 1.2)
|
||||
brakeman (5.4.1)
|
||||
@@ -161,14 +168,14 @@ GEM
|
||||
byebug (11.1.3)
|
||||
climate_control (1.2.0)
|
||||
coderay (1.1.3)
|
||||
commonmarker (0.23.9)
|
||||
commonmarker (0.23.10)
|
||||
concurrent-ruby (1.2.2)
|
||||
connection_pool (2.4.0)
|
||||
connection_pool (2.4.1)
|
||||
crack (0.4.5)
|
||||
rexml
|
||||
crass (1.0.6)
|
||||
csv-safe (3.2.1)
|
||||
cypress-on-rails (1.13.1)
|
||||
cypress-on-rails (1.16.0)
|
||||
rack
|
||||
database_cleaner (2.0.2)
|
||||
database_cleaner-active_record (>= 2, < 3)
|
||||
@@ -183,8 +190,11 @@ GEM
|
||||
libddwaf (~> 1.8.2.0.0)
|
||||
msgpack
|
||||
debase-ruby_core_source (3.2.0)
|
||||
debug (1.8.0)
|
||||
irb (>= 1.5.0)
|
||||
reline (>= 0.3.1)
|
||||
declarative (0.0.20)
|
||||
devise (4.9.2)
|
||||
devise (4.9.3)
|
||||
bcrypt (~> 3.0)
|
||||
orm_adapter (~> 0.1)
|
||||
railties (>= 4.1.0)
|
||||
@@ -252,7 +262,7 @@ GEM
|
||||
rake
|
||||
flag_shih_tzu (0.3.23)
|
||||
foreman (0.87.2)
|
||||
fugit (1.8.1)
|
||||
fugit (1.9.0)
|
||||
et-orbi (~> 1, >= 1.2.7)
|
||||
raabro (~> 1.4)
|
||||
gapic-common (0.18.0)
|
||||
@@ -264,9 +274,9 @@ GEM
|
||||
googleauth (~> 1.0)
|
||||
grpc (~> 1.36)
|
||||
geocoder (1.8.1)
|
||||
gli (2.21.0)
|
||||
globalid (1.1.0)
|
||||
activesupport (>= 5.0)
|
||||
gli (2.21.1)
|
||||
globalid (1.2.1)
|
||||
activesupport (>= 6.1)
|
||||
gmail_xoauth (0.4.2)
|
||||
oauth (>= 0.3.6)
|
||||
google-apis-core (0.11.0)
|
||||
@@ -361,11 +371,18 @@ GEM
|
||||
image_processing (1.12.2)
|
||||
mini_magick (>= 4.9.5, < 5)
|
||||
ruby-vips (>= 2.0.17, < 3)
|
||||
informers (0.2.0)
|
||||
blingfire (>= 0.1.7)
|
||||
numo-narray
|
||||
onnxruntime (>= 0.5.1)
|
||||
io-console (0.6.0)
|
||||
irb (1.7.2)
|
||||
reline (>= 0.3.6)
|
||||
jbuilder (2.11.5)
|
||||
actionview (>= 5.0.0)
|
||||
activesupport (>= 5.0.0)
|
||||
jmespath (1.6.2)
|
||||
jquery-rails (4.5.1)
|
||||
jquery-rails (4.6.0)
|
||||
rails-dom-testing (>= 1, < 3)
|
||||
railties (>= 4.2.0)
|
||||
thor (>= 0.14, < 2.0)
|
||||
@@ -418,12 +435,12 @@ GEM
|
||||
llhttp-ffi (0.4.0)
|
||||
ffi-compiler (~> 1.0)
|
||||
rake (~> 13.0)
|
||||
lograge (0.12.0)
|
||||
lograge (0.14.0)
|
||||
actionpack (>= 4)
|
||||
activesupport (>= 4)
|
||||
railties (>= 4)
|
||||
request_store (~> 1.0)
|
||||
loofah (2.21.3)
|
||||
loofah (2.21.4)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.12.0)
|
||||
mail (2.8.1)
|
||||
@@ -434,23 +451,28 @@ GEM
|
||||
marcel (1.0.2)
|
||||
maxminddb (0.1.22)
|
||||
memoist (0.16.2)
|
||||
meta_request (0.7.4)
|
||||
rack-contrib (>= 1.1, < 3)
|
||||
railties (>= 3.0.0, < 7.1)
|
||||
method_source (1.0.0)
|
||||
mime-types (3.4.1)
|
||||
mime-types-data (~> 3.2015)
|
||||
mime-types-data (3.2023.0218.1)
|
||||
mini_magick (4.12.0)
|
||||
mini_mime (1.1.2)
|
||||
mini_portile2 (2.8.2)
|
||||
minitest (5.18.0)
|
||||
mini_mime (1.1.5)
|
||||
mini_portile2 (2.8.5)
|
||||
minitest (5.20.0)
|
||||
mock_redis (0.36.0)
|
||||
ruby2_keywords
|
||||
msgpack (1.7.0)
|
||||
multi_json (1.15.0)
|
||||
multi_xml (0.6.0)
|
||||
multipart-post (2.3.0)
|
||||
neighbor (0.2.3)
|
||||
activerecord (>= 5.2)
|
||||
net-http-persistent (4.0.2)
|
||||
connection_pool (~> 2.2)
|
||||
net-imap (0.3.4)
|
||||
net-imap (0.3.7)
|
||||
date
|
||||
net-protocol
|
||||
net-pop (0.1.2)
|
||||
@@ -460,20 +482,22 @@ GEM
|
||||
net-smtp (0.3.3)
|
||||
net-protocol
|
||||
netrc (0.11.0)
|
||||
newrelic-sidekiq-metrics (1.6.1)
|
||||
newrelic_rpm (~> 8)
|
||||
newrelic-sidekiq-metrics (1.6.2)
|
||||
newrelic_rpm (>= 8.0.0)
|
||||
sidekiq
|
||||
newrelic_rpm (8.16.0)
|
||||
newrelic_rpm (9.6.0)
|
||||
base64
|
||||
nio4r (2.5.9)
|
||||
nokogiri (1.15.2)
|
||||
nokogiri (1.15.4)
|
||||
mini_portile2 (~> 2.8.2)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.15.2-arm64-darwin)
|
||||
nokogiri (1.15.4-arm64-darwin)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.15.2-x86_64-darwin)
|
||||
nokogiri (1.15.4-x86_64-darwin)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.15.2-x86_64-linux)
|
||||
nokogiri (1.15.4-x86_64-linux)
|
||||
racc (~> 1.4)
|
||||
numo-narray (0.9.2.1)
|
||||
oauth (1.1.0)
|
||||
oauth-tty (~> 1.0, >= 1.0.1)
|
||||
snaky_hash (~> 2.0)
|
||||
@@ -502,6 +526,14 @@ GEM
|
||||
omniauth-rails_csrf_protection (1.0.1)
|
||||
actionpack (>= 4.2)
|
||||
omniauth (~> 2.0)
|
||||
onnxruntime (0.7.6)
|
||||
ffi
|
||||
onnxruntime (0.7.6-arm64-darwin)
|
||||
ffi
|
||||
onnxruntime (0.7.6-x86_64-darwin)
|
||||
ffi
|
||||
onnxruntime (0.7.6-x86_64-linux)
|
||||
ffi
|
||||
openssl (3.1.0)
|
||||
orm_adapter (0.5.0)
|
||||
os (1.1.4)
|
||||
@@ -512,6 +544,7 @@ GEM
|
||||
pg_search (2.3.6)
|
||||
activerecord (>= 5.2)
|
||||
activesupport (>= 5.2)
|
||||
pgvector (0.1.1)
|
||||
procore-sift (1.0.0)
|
||||
activerecord (>= 6.1)
|
||||
pry (0.14.2)
|
||||
@@ -520,18 +553,20 @@ GEM
|
||||
pry-rails (0.3.9)
|
||||
pry (>= 0.10.4)
|
||||
public_suffix (5.0.1)
|
||||
puma (6.2.2)
|
||||
puma (6.3.1)
|
||||
nio4r (~> 2.0)
|
||||
pundit (2.3.0)
|
||||
activesupport (>= 3.0.0)
|
||||
raabro (1.4.0)
|
||||
racc (1.7.0)
|
||||
rack (2.2.7)
|
||||
rack-attack (6.6.1)
|
||||
rack (>= 1.0, < 3)
|
||||
racc (1.7.3)
|
||||
rack (2.2.8)
|
||||
rack-attack (6.7.0)
|
||||
rack (>= 1.0, < 4)
|
||||
rack-contrib (2.4.0)
|
||||
rack (< 4)
|
||||
rack-cors (2.0.1)
|
||||
rack (>= 2.0.0)
|
||||
rack-mini-profiler (3.1.0)
|
||||
rack-mini-profiler (3.1.1)
|
||||
rack (>= 1.2.0)
|
||||
rack-protection (3.0.6)
|
||||
rack
|
||||
@@ -540,52 +575,55 @@ GEM
|
||||
rack-test (2.1.0)
|
||||
rack (>= 1.3)
|
||||
rack-timeout (0.6.3)
|
||||
rails (7.0.5)
|
||||
actioncable (= 7.0.5)
|
||||
actionmailbox (= 7.0.5)
|
||||
actionmailer (= 7.0.5)
|
||||
actionpack (= 7.0.5)
|
||||
actiontext (= 7.0.5)
|
||||
actionview (= 7.0.5)
|
||||
activejob (= 7.0.5)
|
||||
activemodel (= 7.0.5)
|
||||
activerecord (= 7.0.5)
|
||||
activestorage (= 7.0.5)
|
||||
activesupport (= 7.0.5)
|
||||
rails (7.0.8)
|
||||
actioncable (= 7.0.8)
|
||||
actionmailbox (= 7.0.8)
|
||||
actionmailer (= 7.0.8)
|
||||
actionpack (= 7.0.8)
|
||||
actiontext (= 7.0.8)
|
||||
actionview (= 7.0.8)
|
||||
activejob (= 7.0.8)
|
||||
activemodel (= 7.0.8)
|
||||
activerecord (= 7.0.8)
|
||||
activestorage (= 7.0.8)
|
||||
activesupport (= 7.0.8)
|
||||
bundler (>= 1.15.0)
|
||||
railties (= 7.0.5)
|
||||
rails-dom-testing (2.0.3)
|
||||
activesupport (>= 4.2.0)
|
||||
railties (= 7.0.8)
|
||||
rails-dom-testing (2.2.0)
|
||||
activesupport (>= 5.0.0)
|
||||
minitest
|
||||
nokogiri (>= 1.6)
|
||||
rails-html-sanitizer (1.6.0)
|
||||
loofah (~> 2.21)
|
||||
nokogiri (~> 1.14)
|
||||
railties (7.0.5)
|
||||
actionpack (= 7.0.5)
|
||||
activesupport (= 7.0.5)
|
||||
railties (7.0.8)
|
||||
actionpack (= 7.0.8)
|
||||
activesupport (= 7.0.8)
|
||||
method_source
|
||||
rake (>= 12.2)
|
||||
thor (~> 1.0)
|
||||
zeitwerk (~> 2.5)
|
||||
rainbow (3.1.1)
|
||||
rake (13.0.6)
|
||||
rake (13.1.0)
|
||||
rb-fsevent (0.11.2)
|
||||
rb-inotify (0.10.1)
|
||||
ffi (~> 1.0)
|
||||
redis (5.0.6)
|
||||
redis-client (>= 0.9.0)
|
||||
redis-client (0.14.1)
|
||||
redis-client (0.18.0)
|
||||
connection_pool
|
||||
redis-namespace (1.10.0)
|
||||
redis (>= 4)
|
||||
regexp_parser (2.8.0)
|
||||
reline (0.3.6)
|
||||
io-console (~> 0.5)
|
||||
representable (3.2.0)
|
||||
declarative (< 0.1.0)
|
||||
trailblazer-option (>= 0.1.1, < 0.2.0)
|
||||
uber (< 0.2.0)
|
||||
request_store (1.5.1)
|
||||
rack (>= 1.4)
|
||||
responders (3.1.0)
|
||||
responders (3.1.1)
|
||||
actionpack (>= 5.2)
|
||||
railties (>= 5.2)
|
||||
rest-client (2.1.0)
|
||||
@@ -594,6 +632,8 @@ GEM
|
||||
mime-types (>= 1.16, < 4.0)
|
||||
netrc (~> 0.8)
|
||||
retriable (3.1.2)
|
||||
reverse_markdown (2.1.1)
|
||||
nokogiri
|
||||
rexml (3.2.5)
|
||||
rspec-core (3.12.2)
|
||||
rspec-support (~> 3.12.0)
|
||||
@@ -669,23 +709,23 @@ GEM
|
||||
activesupport (>= 4)
|
||||
selectize-rails (0.12.6)
|
||||
semantic_range (3.0.0)
|
||||
sentry-rails (5.9.0)
|
||||
sentry-rails (5.13.0)
|
||||
railties (>= 5.0)
|
||||
sentry-ruby (~> 5.9.0)
|
||||
sentry-ruby (5.9.0)
|
||||
sentry-ruby (~> 5.13.0)
|
||||
sentry-ruby (5.13.0)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
sentry-sidekiq (5.9.0)
|
||||
sentry-ruby (~> 5.9.0)
|
||||
sentry-sidekiq (5.13.0)
|
||||
sentry-ruby (~> 5.13.0)
|
||||
sidekiq (>= 3.0)
|
||||
sexp_processor (4.17.0)
|
||||
shoulda-matchers (5.3.0)
|
||||
activesupport (>= 5.2.0)
|
||||
sidekiq (7.1.0)
|
||||
sidekiq (7.2.0)
|
||||
concurrent-ruby (< 2)
|
||||
connection_pool (>= 2.3.0)
|
||||
rack (>= 2.2.4)
|
||||
redis-client (>= 0.14.0)
|
||||
sidekiq-cron (1.10.0)
|
||||
sidekiq-cron (1.11.0)
|
||||
fugit (~> 1.8)
|
||||
globalid (>= 1.0.1)
|
||||
sidekiq (>= 6)
|
||||
@@ -699,13 +739,12 @@ GEM
|
||||
json (>= 1.8, < 3)
|
||||
simplecov-html (~> 0.10.0)
|
||||
simplecov-html (0.10.2)
|
||||
slack-ruby-client (2.0.0)
|
||||
slack-ruby-client (2.2.0)
|
||||
faraday (>= 2.0)
|
||||
faraday-mashify
|
||||
faraday-multipart
|
||||
gli
|
||||
hashie
|
||||
websocket-driver
|
||||
snaky_hash (2.0.1)
|
||||
hashie
|
||||
version_gem (~> 1.1, >= 1.1.1)
|
||||
@@ -726,12 +765,12 @@ GEM
|
||||
stripe (8.5.0)
|
||||
telephone_number (1.4.20)
|
||||
test-prof (1.2.1)
|
||||
thor (1.2.2)
|
||||
thor (1.3.0)
|
||||
tilt (2.2.0)
|
||||
time_diff (0.3.0)
|
||||
activesupport
|
||||
i18n
|
||||
timeout (0.3.2)
|
||||
timeout (0.4.0)
|
||||
trailblazer-option (0.1.2)
|
||||
twilio-ruby (5.77.0)
|
||||
faraday (>= 0.9, < 3.0)
|
||||
@@ -758,7 +797,7 @@ GEM
|
||||
version_gem (1.1.2)
|
||||
warden (1.2.9)
|
||||
rack (>= 2.0.9)
|
||||
web-console (4.2.0)
|
||||
web-console (4.2.1)
|
||||
actionview (>= 6.0.0)
|
||||
activemodel (>= 6.0.0)
|
||||
bindex (>= 0.4.0)
|
||||
@@ -777,14 +816,14 @@ GEM
|
||||
railties (>= 5.2)
|
||||
semantic_range (>= 2.3.0)
|
||||
webrick (1.8.1)
|
||||
websocket-driver (0.7.5)
|
||||
websocket-driver (0.7.6)
|
||||
websocket-extensions (>= 0.1.0)
|
||||
websocket-extensions (0.1.5)
|
||||
wisper (2.0.0)
|
||||
working_hours (1.4.1)
|
||||
activesupport (>= 3.2)
|
||||
tzinfo
|
||||
zeitwerk (2.6.8)
|
||||
zeitwerk (2.6.12)
|
||||
|
||||
PLATFORMS
|
||||
arm64-darwin-20
|
||||
@@ -801,11 +840,12 @@ DEPENDENCIES
|
||||
active_record_query_trace
|
||||
activerecord-import
|
||||
acts-as-taggable-on
|
||||
administrate
|
||||
administrate (>= 0.19.0)
|
||||
administrate-field-active_storage
|
||||
administrate-field-belongs_to_search
|
||||
annotate
|
||||
attr_extras
|
||||
audited (~> 5.3)
|
||||
audited (~> 5.4, >= 5.4.1)
|
||||
aws-sdk-s3
|
||||
azure-storage-blob!
|
||||
barnes
|
||||
@@ -821,7 +861,8 @@ DEPENDENCIES
|
||||
cypress-on-rails
|
||||
database_cleaner
|
||||
ddtrace
|
||||
devise
|
||||
debug (~> 1.8)
|
||||
devise (>= 4.9.3)
|
||||
devise-secure_password!
|
||||
devise_token_auth
|
||||
dotenv-rails
|
||||
@@ -846,6 +887,7 @@ DEPENDENCIES
|
||||
hashie
|
||||
html2text!
|
||||
image_processing
|
||||
informers
|
||||
jbuilder
|
||||
json_refs
|
||||
json_schemer
|
||||
@@ -856,10 +898,12 @@ DEPENDENCIES
|
||||
line-bot-api
|
||||
liquid
|
||||
listen
|
||||
lograge (~> 0.12.0)
|
||||
lograge (~> 0.14.0)
|
||||
maxminddb
|
||||
meta_request
|
||||
mock_redis
|
||||
newrelic-sidekiq-metrics
|
||||
neighbor
|
||||
newrelic-sidekiq-metrics (>= 1.6.2)
|
||||
newrelic_rpm
|
||||
omniauth
|
||||
omniauth-google-oauth2
|
||||
@@ -867,19 +911,21 @@ DEPENDENCIES
|
||||
omniauth-rails_csrf_protection (~> 1.0)
|
||||
pg
|
||||
pg_search
|
||||
pgvector
|
||||
procore-sift
|
||||
pry-rails
|
||||
puma
|
||||
pundit
|
||||
rack-attack
|
||||
rack-attack (>= 6.7.0)
|
||||
rack-cors
|
||||
rack-mini-profiler
|
||||
rack-mini-profiler (>= 3.1.1)
|
||||
rack-timeout
|
||||
rails (~> 7)
|
||||
rails (~> 7.0.8.0)
|
||||
redis
|
||||
redis-namespace
|
||||
responders
|
||||
responders (>= 3.1.1)
|
||||
rest-client
|
||||
reverse_markdown
|
||||
rspec-rails
|
||||
rspec_junit_formatter
|
||||
rubocop
|
||||
@@ -889,14 +935,14 @@ DEPENDENCIES
|
||||
scout_apm
|
||||
scss_lint
|
||||
seed_dump
|
||||
sentry-rails
|
||||
sentry-rails (>= 5.13.0)
|
||||
sentry-ruby
|
||||
sentry-sidekiq
|
||||
sentry-sidekiq (>= 5.13.0)
|
||||
shoulda-matchers
|
||||
sidekiq
|
||||
sidekiq-cron
|
||||
sidekiq (>= 7.1.3)
|
||||
sidekiq-cron (>= 1.11.0)
|
||||
simplecov (= 0.17.1)
|
||||
slack-ruby-client (~> 2.0.0)
|
||||
slack-ruby-client (~> 2.2.0)
|
||||
spring
|
||||
spring-watcher-listen
|
||||
squasher
|
||||
@@ -910,7 +956,7 @@ DEPENDENCIES
|
||||
tzinfo-data
|
||||
uglifier
|
||||
valid_email2
|
||||
web-console
|
||||
web-console (>= 4.2.1)
|
||||
web-push
|
||||
webmock
|
||||
webpacker
|
||||
|
||||
@@ -30,9 +30,23 @@ burn:
|
||||
bundle && yarn
|
||||
|
||||
run:
|
||||
@if [ -f ./.overmind.sock ]; then \
|
||||
echo "Overmind is already running. Use 'make force_run' to start a new instance."; \
|
||||
else \
|
||||
overmind start -f Procfile.dev; \
|
||||
fi
|
||||
|
||||
force_run:
|
||||
rm -f ./.overmind.sock
|
||||
overmind start -f Procfile.dev
|
||||
|
||||
debug:
|
||||
overmind connect backend
|
||||
|
||||
debug_worker:
|
||||
overmind connect worker
|
||||
|
||||
docker:
|
||||
docker build -t $(APP_NAME) -f ./docker/Dockerfile .
|
||||
|
||||
.PHONY: setup db_create db_migrate db_seed db console server burn docker run
|
||||
.PHONY: setup db_create db_migrate db_seed db console server burn docker run force_run debug debug_worker
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
backend: bin/rails s -p 3000
|
||||
frontend: bin/webpack-dev-server
|
||||
frontend: export NODE_OPTIONS=--openssl-legacy-provider && bin/webpack-dev-server
|
||||
# https://github.com/mperham/sidekiq/issues/3090#issuecomment-389748695
|
||||
worker: dotenv bundle exec sidekiq -C config/sidekiq.yml
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
backend: RAILS_ENV=test bin/rails s -p 5050
|
||||
frontend: bin/webpack-dev-server
|
||||
worker: dotenv RAILS_ENV=test bundle exec sidekiq -C config/sidekiq.yml
|
||||
frontend: export NODE_OPTIONS=--openssl-legacy-provider && bin/webpack-dev-server
|
||||
worker: RAILS_ENV=test dotenv bundle exec sidekiq -C config/sidekiq.yml
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
<p align="center">
|
||||
<img src="https://s3.us-west-2.amazonaws.com/gh-assets.chatwoot.com/brand.svg" alt="Woot-logo" width="240" />
|
||||
<img src="https://user-images.githubusercontent.com/2246121/282256557-1570674b-d142-4198-9740-69404cc6a339.png#gh-light-mode-only" width="100%" alt="Chat dashboard dark mode"/>
|
||||
<img src="https://user-images.githubusercontent.com/2246121/282256632-87f6a01b-6467-4e0e-8a93-7bbf66d03a17.png#gh-dark-mode-only" width="100%" alt="Chat dashboard"/>
|
||||
|
||||
<p align="center">Customer engagement suite, an open-source alternative to Intercom, Zendesk, Salesforce Service Cloud etc.</p>
|
||||
</p>
|
||||
___
|
||||
|
||||
<p align="center">
|
||||
# Chatwoot
|
||||
|
||||
Customer engagement suite, an open-source alternative to Intercom, Zendesk, Salesforce Service Cloud etc.
|
||||
<p>
|
||||
<a href="https://heroku.com/deploy?template=https://github.com/chatwoot/chatwoot/tree/master" alt="Deploy to Heroku">
|
||||
<img width="150" alt="Deploy" src="https://www.herokucdn.com/deploy/button.svg"/>
|
||||
</a>
|
||||
@@ -13,9 +15,7 @@
|
||||
</a>
|
||||
</p>
|
||||
|
||||
___
|
||||
|
||||
<p align="center">
|
||||
<p>
|
||||
<a href="https://codeclimate.com/github/chatwoot/chatwoot/maintainability"><img src="https://api.codeclimate.com/v1/badges/e6e3f66332c91e5a4c0c/maintainability" alt="Maintainability"></a>
|
||||
<img src="https://img.shields.io/circleci/build/github/chatwoot/chatwoot" alt="CircleCI Badge">
|
||||
<a href="https://hub.docker.com/r/chatwoot/chatwoot/"><img src="https://img.shields.io/docker/pulls/chatwoot/chatwoot" alt="Docker Pull Badge"></a>
|
||||
@@ -29,9 +29,8 @@ ___
|
||||
<a href="https://artifacthub.io/packages/helm/chatwoot/chatwoot"><img src="https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/artifact-hub" alt="Artifact HUB"></a>
|
||||
</p>
|
||||
|
||||
<img src="https://chatwoot-public-assets.s3.amazonaws.com/github/screenshot.png" width="100%" alt="Chat dashboard"/>
|
||||
|
||||
|
||||
<img src="https://user-images.githubusercontent.com/2246121/282255783-ee8a50c9-f42d-4752-8201-2d59965a663d.png#gh-light-mode-only" width="100%" alt="Chat dashboard dark mode"/>
|
||||
<img src="https://user-images.githubusercontent.com/2246121/282255784-3d1994ec-d895-4ff5-ac68-d819987e1869.png#gh-dark-mode-only" width="100%" alt="Chat dashboard"/>
|
||||
|
||||
Chatwoot is an open-source, self-hosted customer engagement suite. Chatwoot lets you view and manage your customer data, communicate with them irrespective of which medium they use, and re-engage them based on their profile.
|
||||
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
2.17.0
|
||||
3.1.0
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
2.3.0
|
||||
2.6.0
|
||||
|
||||
@@ -60,6 +60,9 @@
|
||||
],
|
||||
"stack": "heroku-20",
|
||||
"buildpacks": [
|
||||
{
|
||||
"url": "heroku/nodejs"
|
||||
},
|
||||
{
|
||||
"url": "heroku/ruby"
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ function prepareData(data) {
|
||||
|
||||
function getChartOptions() {
|
||||
var fontFamily =
|
||||
'Inter,-apple-system,system-ui,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif';
|
||||
'PlusJakarta,-apple-system,system-ui,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif';
|
||||
return {
|
||||
responsive: true,
|
||||
legend: { labels: { fontFamily } },
|
||||
|
||||
@@ -29,3 +29,5 @@
|
||||
@import 'components/pagination';
|
||||
@import 'components/search';
|
||||
@import 'components/reports';
|
||||
|
||||
@import 'custom_styles';
|
||||
|
||||
@@ -24,14 +24,6 @@ select {
|
||||
font-size: $base-font-size;
|
||||
}
|
||||
|
||||
input,
|
||||
select,
|
||||
textarea {
|
||||
display: block;
|
||||
font-family: $base-font-family;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
[type="color"],
|
||||
[type="date"],
|
||||
[type="datetime-local"],
|
||||
@@ -51,6 +43,7 @@ textarea {
|
||||
background-color: $white;
|
||||
border: $base-border;
|
||||
border-radius: $base-border-radius;
|
||||
font-family: $base-font-family;
|
||||
padding: 0.5em;
|
||||
transition: border-color $base-duration $base-timing;
|
||||
width: 100%;
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
float: left;
|
||||
margin-bottom: $base-spacing;
|
||||
margin-top: 0.25em;
|
||||
text-align: right;
|
||||
width: calc(15% - 1rem);
|
||||
text-align: left;
|
||||
width: calc(16% - 1rem);
|
||||
}
|
||||
|
||||
.preserve-whitespace {
|
||||
@@ -17,7 +17,7 @@
|
||||
float: left;
|
||||
margin-bottom: $base-spacing;
|
||||
margin-left: 2rem;
|
||||
width: calc(85% - 1rem);
|
||||
width: calc(84% - 1rem);
|
||||
}
|
||||
|
||||
.attribute--nested {
|
||||
|
||||
@@ -10,7 +10,7 @@ input[type="submit"],
|
||||
color: $white;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
font-size: $font-size-default;
|
||||
font-size: $font-size-small;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
font-weight: $font-weight-medium;
|
||||
line-height: 1;
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
.main-content {
|
||||
font-size: $font-size-default;
|
||||
left: 23rem;
|
||||
left: 21rem;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.main-content__body {
|
||||
font-size: $font-size-small;
|
||||
padding: $space-two;
|
||||
|
||||
table {
|
||||
font-size: $font-size-small;
|
||||
}
|
||||
}
|
||||
|
||||
.main-content__header {
|
||||
@@ -20,7 +25,7 @@
|
||||
}
|
||||
|
||||
.main-content__page-title {
|
||||
font-size: $font-size-large;
|
||||
font-size: $font-size-medium;
|
||||
font-weight: $font-weight-medium;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
.logo-brand {
|
||||
margin-bottom: $space-normal;
|
||||
padding: $space-normal $space-smaller $space-small;
|
||||
text-align: center;
|
||||
text-align: left;
|
||||
|
||||
img {
|
||||
margin-bottom: $space-smaller;
|
||||
max-height: 3rem;
|
||||
}
|
||||
}
|
||||
|
||||
.navigation {
|
||||
@@ -19,12 +24,13 @@
|
||||
padding: $space-normal;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
width: 23rem;
|
||||
width: 21rem;
|
||||
z-index: 1023;
|
||||
|
||||
li {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
font-size: $font-size-small;
|
||||
|
||||
a {
|
||||
color: $color-gray;
|
||||
@@ -35,6 +41,10 @@
|
||||
min-width: $space-medium;
|
||||
}
|
||||
}
|
||||
|
||||
hr {
|
||||
margin: $space-slab;
|
||||
}
|
||||
}
|
||||
|
||||
.navigation__link {
|
||||
@@ -43,7 +53,7 @@
|
||||
display: block;
|
||||
line-height: 1;
|
||||
margin-bottom: $space-smaller;
|
||||
padding: $space-one;
|
||||
padding: $space-small;
|
||||
|
||||
&:hover {
|
||||
color: $blue;
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
// custom styles for the dashboard
|
||||
|
||||
.feature-cell {
|
||||
background: $color-extra-light-blue;
|
||||
border-radius: 10px;
|
||||
float: left;
|
||||
margin-left: 8px;
|
||||
margin-top: 6px;
|
||||
padding: 4px 12px;
|
||||
|
||||
.icon-container {
|
||||
margin-right: 4px;
|
||||
|
||||
}
|
||||
|
||||
.value-container {
|
||||
margin-left: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.feature-container {
|
||||
max-width: 100rem;
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
// Typography
|
||||
$base-font-family: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
|
||||
$base-font-family: PlusJakarta, Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
|
||||
"Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
|
||||
sans-serif !default;
|
||||
$heading-font-family: $base-font-family !default;
|
||||
|
||||
$base-font-size: 10px !default;
|
||||
$base-font-size: 14px !default;
|
||||
|
||||
$base-line-height: 1.5 !default;
|
||||
$heading-line-height: 1.2 !default;
|
||||
|
||||
@@ -78,11 +78,6 @@ $conv-header-height: 4rem;
|
||||
$inbox-thumb-size: 4.8rem;
|
||||
|
||||
|
||||
// Spinner
|
||||
$spinkit-spinner-color: $color-white !default;
|
||||
$spinkit-spinner-margin: 0 0 0 1.6rem !default;
|
||||
$spinkit-size: 1.6rem !default;
|
||||
|
||||
// Snackbar default
|
||||
$woot-snackbar-bg: #323232;
|
||||
$woot-snackbar-button: #ffeb3b;
|
||||
|
||||
@@ -93,6 +93,9 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
|
||||
message_type: @message_type,
|
||||
content: response.content,
|
||||
source_id: response.identifier,
|
||||
content_attributes: {
|
||||
in_reply_to_external_id: response.in_reply_to_external_id
|
||||
},
|
||||
sender: @outgoing_echo ? nil : @contact_inbox.contact
|
||||
}
|
||||
end
|
||||
|
||||
@@ -76,6 +76,10 @@ class Messages::Instagram::MessageBuilder < Messages::Messenger::MessageBuilder
|
||||
message[:reply_to][:story] if message[:reply_to].present? && message[:reply_to][:story].present?
|
||||
end
|
||||
|
||||
def message_reply_attributes
|
||||
message[:reply_to][:mid] if message[:reply_to].present? && message[:reply_to][:mid].present?
|
||||
end
|
||||
|
||||
def build_message
|
||||
return if @outgoing_echo && already_sent_from_chatwoot?
|
||||
return if message_content.blank? && all_unsupported_files?
|
||||
@@ -118,7 +122,10 @@ class Messages::Instagram::MessageBuilder < Messages::Messenger::MessageBuilder
|
||||
message_type: message_type,
|
||||
source_id: message_identifier,
|
||||
content: message_content,
|
||||
sender: @outgoing_echo ? nil : contact
|
||||
sender: @outgoing_echo ? nil : contact,
|
||||
content_attributes: {
|
||||
in_reply_to_external_id: message_reply_attributes
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
|
||||
@@ -9,11 +9,11 @@ class Messages::MessageBuilder
|
||||
@user = user
|
||||
@message_type = params[:message_type] || 'outgoing'
|
||||
@attachments = params[:attachments]
|
||||
@automation_rule = @params&.dig(:content_attributes, :automation_rule_id)
|
||||
@automation_rule = content_attributes&.dig(:automation_rule_id)
|
||||
return unless params.instance_of?(ActionController::Parameters)
|
||||
|
||||
@in_reply_to = params.to_unsafe_h&.dig(:content_attributes, :in_reply_to)
|
||||
@items = params.to_unsafe_h&.dig(:content_attributes, :items)
|
||||
@in_reply_to = content_attributes&.dig(:in_reply_to)
|
||||
@items = content_attributes&.dig(:items)
|
||||
end
|
||||
|
||||
def perform
|
||||
@@ -26,6 +26,38 @@ class Messages::MessageBuilder
|
||||
|
||||
private
|
||||
|
||||
# Extracts content attributes from the given params.
|
||||
# - Converts ActionController::Parameters to a regular hash if needed.
|
||||
# - Attempts to parse a JSON string if content is a string.
|
||||
# - Returns an empty hash if content is not present, if there's a parsing error, or if it's an unexpected type.
|
||||
def content_attributes
|
||||
params = convert_to_hash(@params)
|
||||
content_attributes = params.fetch(:content_attributes, {})
|
||||
|
||||
return parse_json(content_attributes) if content_attributes.is_a?(String)
|
||||
return content_attributes if content_attributes.is_a?(Hash)
|
||||
|
||||
{}
|
||||
end
|
||||
|
||||
# Converts the given object to a hash.
|
||||
# If it's an instance of ActionController::Parameters, converts it to an unsafe hash.
|
||||
# Otherwise, returns the object as-is.
|
||||
def convert_to_hash(obj)
|
||||
return obj.to_unsafe_h if obj.instance_of?(ActionController::Parameters)
|
||||
|
||||
obj
|
||||
end
|
||||
|
||||
# Attempts to parse a string as JSON.
|
||||
# If successful, returns the parsed hash with symbolized names.
|
||||
# If unsuccessful, returns nil.
|
||||
def parse_json(content)
|
||||
JSON.parse(content, symbolize_names: true)
|
||||
rescue JSON::ParserError
|
||||
{}
|
||||
end
|
||||
|
||||
def process_attachments
|
||||
return if @attachments.blank?
|
||||
|
||||
@@ -48,17 +80,22 @@ class Messages::MessageBuilder
|
||||
def process_emails
|
||||
return unless @conversation.inbox&.inbox_type == 'Email'
|
||||
|
||||
cc_emails = []
|
||||
cc_emails = @params[:cc_emails].gsub(/\s+/, '').split(',') if @params[:cc_emails].present?
|
||||
cc_emails = process_email_string(@params[:cc_emails])
|
||||
bcc_emails = process_email_string(@params[:bcc_emails])
|
||||
to_emails = process_email_string(@params[:to_emails])
|
||||
|
||||
bcc_emails = []
|
||||
bcc_emails = @params[:bcc_emails].gsub(/\s+/, '').split(',') if @params[:bcc_emails].present?
|
||||
|
||||
all_email_addresses = cc_emails + bcc_emails
|
||||
all_email_addresses = cc_emails + bcc_emails + to_emails
|
||||
validate_email_addresses(all_email_addresses)
|
||||
|
||||
@message.content_attributes[:cc_emails] = cc_emails
|
||||
@message.content_attributes[:bcc_emails] = bcc_emails
|
||||
@message.content_attributes[:to_emails] = to_emails
|
||||
end
|
||||
|
||||
def process_email_string(email_string)
|
||||
return [] if email_string.blank?
|
||||
|
||||
email_string.gsub(/\s+/, '').split(',')
|
||||
end
|
||||
|
||||
def validate_email_addresses(all_emails)
|
||||
|
||||
@@ -15,12 +15,15 @@ class V2::ReportBuilder
|
||||
end
|
||||
|
||||
def timeseries
|
||||
send(params[:metric])
|
||||
return send(params[:metric]) if metric_valid?
|
||||
|
||||
Rails.logger.error "ReportBuilder: Invalid metric - #{params[:metric]}"
|
||||
{}
|
||||
end
|
||||
|
||||
# For backward compatible with old report
|
||||
def build
|
||||
if %w[avg_first_response_time avg_resolution_time].include?(params[:metric])
|
||||
if %w[avg_first_response_time avg_resolution_time reply_time].include?(params[:metric])
|
||||
timeseries.each_with_object([]) do |p, arr|
|
||||
arr << { value: p[1], timestamp: p[0].in_time_zone(@timezone).to_i, count: @grouped_values.count[p[0]] }
|
||||
end
|
||||
@@ -33,18 +36,19 @@ class V2::ReportBuilder
|
||||
|
||||
def summary
|
||||
{
|
||||
conversations_count: conversations_count.values.sum,
|
||||
incoming_messages_count: incoming_messages_count.values.sum,
|
||||
outgoing_messages_count: outgoing_messages_count.values.sum,
|
||||
conversations_count: conversations.count,
|
||||
incoming_messages_count: incoming_messages.count,
|
||||
outgoing_messages_count: outgoing_messages.count,
|
||||
avg_first_response_time: avg_first_response_time_summary,
|
||||
avg_resolution_time: avg_resolution_time_summary,
|
||||
resolutions_count: resolutions_count.values.sum
|
||||
resolutions_count: resolutions.count,
|
||||
reply_time: reply_time_summary
|
||||
}
|
||||
end
|
||||
|
||||
def conversation_metrics
|
||||
if params[:type].equal?(:account)
|
||||
conversations
|
||||
live_conversations
|
||||
else
|
||||
agent_metrics.sort_by { |hash| hash[:metric][:open] }.reverse
|
||||
end
|
||||
@@ -52,6 +56,16 @@ class V2::ReportBuilder
|
||||
|
||||
private
|
||||
|
||||
def metric_valid?
|
||||
%w[conversations_count
|
||||
incoming_messages_count
|
||||
outgoing_messages_count
|
||||
avg_first_response_time
|
||||
avg_resolution_time reply_time
|
||||
resolutions_count
|
||||
reply_time].include?(params[:metric])
|
||||
end
|
||||
|
||||
def inbox
|
||||
@inbox ||= account.inboxes.find(params[:id])
|
||||
end
|
||||
@@ -89,12 +103,12 @@ class V2::ReportBuilder
|
||||
email: @user.email,
|
||||
thumbnail: @user.avatar_url,
|
||||
availability: account_user.availability_status,
|
||||
metric: conversations
|
||||
metric: live_conversations
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
def conversations
|
||||
def live_conversations
|
||||
@open_conversations = scope.conversations.where(account_id: @account.id).open
|
||||
metric = {
|
||||
open: @open_conversations.count,
|
||||
|
||||
@@ -9,7 +9,6 @@ class Api::V1::Accounts::Actions::ContactMergesController < Api::V1::Accounts::B
|
||||
mergee_contact: @mergee_contact
|
||||
)
|
||||
contact_merge_action.perform
|
||||
render json: @base_contact
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
class Api::V1::Accounts::ArticlesController < Api::V1::Accounts::BaseController
|
||||
before_action :portal
|
||||
before_action :check_authorization
|
||||
before_action :fetch_article, except: [:index, :create, :attach_file, :reorder]
|
||||
before_action :fetch_article, except: [:index, :create, :reorder]
|
||||
before_action :set_current_page, only: [:index]
|
||||
|
||||
def index
|
||||
@@ -10,7 +10,7 @@ class Api::V1::Accounts::ArticlesController < Api::V1::Accounts::BaseController
|
||||
@articles_count = @all_articles.count
|
||||
|
||||
@articles = if list_params[:category_slug].present?
|
||||
@all_articles.order_by_position.page(@current_page).per(50)
|
||||
@all_articles.order_by_position.page(@current_page)
|
||||
else
|
||||
@all_articles.order_by_updated_at.page(@current_page)
|
||||
end
|
||||
@@ -36,17 +36,6 @@ class Api::V1::Accounts::ArticlesController < Api::V1::Accounts::BaseController
|
||||
head :ok
|
||||
end
|
||||
|
||||
def attach_file
|
||||
file_blob = ActiveStorage::Blob.create_and_upload!(
|
||||
key: nil,
|
||||
io: params[:background_image].tempfile,
|
||||
filename: params[:background_image].original_filename,
|
||||
content_type: params[:background_image].content_type
|
||||
)
|
||||
file_blob.save!
|
||||
render json: { file_url: url_for(file_blob) }
|
||||
end
|
||||
|
||||
def reorder
|
||||
Article.update_positions(params[:positions_hash])
|
||||
head :ok
|
||||
|
||||
@@ -20,16 +20,6 @@ class Api::V1::Accounts::AutomationRulesController < Api::V1::Accounts::BaseCont
|
||||
@automation_rule
|
||||
end
|
||||
|
||||
def attach_file
|
||||
file_blob = ActiveStorage::Blob.create_and_upload!(
|
||||
key: nil,
|
||||
io: params[:attachment].tempfile,
|
||||
filename: params[:attachment].original_filename,
|
||||
content_type: params[:attachment].content_type
|
||||
)
|
||||
render json: { blob_key: file_blob.key, blob_id: file_blob.id }
|
||||
end
|
||||
|
||||
def update
|
||||
ActiveRecord::Base.transaction do
|
||||
automation_rule_update
|
||||
|
||||
@@ -14,8 +14,18 @@ class Api::V1::Accounts::CallbacksController < Api::V1::Accounts::BaseController
|
||||
@facebook_inbox = Current.account.inboxes.create!(name: inbox_name, channel: facebook_channel)
|
||||
set_instagram_id(page_access_token, facebook_channel)
|
||||
set_avatar(@facebook_inbox, page_id)
|
||||
rescue StandardError => e
|
||||
ChatwootExceptionTracker.new(e).capture_exception
|
||||
end
|
||||
rescue StandardError => e
|
||||
ChatwootExceptionTracker.new(e).capture_exception
|
||||
Rails.logger.error "Error in register_facebook_page: #{e.message}"
|
||||
# Additional log statements
|
||||
log_additional_info
|
||||
end
|
||||
|
||||
def log_additional_info
|
||||
Rails.logger.debug do
|
||||
"user_access_token: #{params[:user_access_token]} , page_access_token: #{params[:page_access_token]} ,
|
||||
page_id: #{params[:page_id]}, inbox_name: #{params[:inbox_name]}"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -30,6 +40,8 @@ class Api::V1::Accounts::CallbacksController < Api::V1::Accounts::BaseController
|
||||
|
||||
instagram_id = response['instagram_business_account']['id']
|
||||
facebook_channel.update(instagram_id: instagram_id)
|
||||
rescue StandardError => e
|
||||
Rails.logger.error "Error in set_instagram_id: #{e.message}"
|
||||
end
|
||||
|
||||
# get params[:inbox_id], current_account. params[:omniauth_token]
|
||||
@@ -61,6 +73,7 @@ class Api::V1::Accounts::CallbacksController < Api::V1::Accounts::BaseController
|
||||
fb_page&.reauthorized!
|
||||
rescue StandardError => e
|
||||
ChatwootExceptionTracker.new(e).capture_exception
|
||||
Rails.logger.error "Error in update_fb_page: #{e.message}"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -77,7 +90,7 @@ class Api::V1::Accounts::CallbacksController < Api::V1::Accounts::BaseController
|
||||
koala = Koala::Facebook::OAuth.new(GlobalConfigService.load('FB_APP_ID', ''), GlobalConfigService.load('FB_APP_SECRET', ''))
|
||||
koala.exchange_access_token_info(omniauth_token)['access_token']
|
||||
rescue StandardError => e
|
||||
Rails.logger.error e
|
||||
Rails.logger.error "Error in long_lived_token: #{e.message}"
|
||||
end
|
||||
|
||||
def mark_already_existing_facebook_pages(data)
|
||||
|
||||
@@ -48,7 +48,7 @@ class Api::V1::Accounts::CategoriesController < Api::V1::Accounts::BaseControlle
|
||||
|
||||
def category_params
|
||||
params.require(:category).permit(
|
||||
:name, :description, :position, :slug, :locale, :parent_category_id, :associated_category_id
|
||||
:name, :description, :position, :slug, :locale, :icon, :parent_category_id, :associated_category_id
|
||||
)
|
||||
end
|
||||
|
||||
|
||||
@@ -18,7 +18,11 @@ class Api::V1::Accounts::Channels::TwilioChannelsController < Api::V1::Accounts:
|
||||
end
|
||||
|
||||
def authenticate_twilio
|
||||
client = Twilio::REST::Client.new(permitted_params[:account_sid], permitted_params[:auth_token])
|
||||
client = if permitted_params[:api_key_sid].present?
|
||||
Twilio::REST::Client.new(permitted_params[:api_key_sid], permitted_params[:auth_token], permitted_params[:account_sid])
|
||||
else
|
||||
Twilio::REST::Client.new(permitted_params[:account_sid], permitted_params[:auth_token])
|
||||
end
|
||||
client.messages.list(limit: 1)
|
||||
end
|
||||
|
||||
@@ -40,6 +44,7 @@ class Api::V1::Accounts::Channels::TwilioChannelsController < Api::V1::Accounts:
|
||||
@twilio_channel = Current.account.twilio_sms.create!(
|
||||
account_sid: permitted_params[:account_sid],
|
||||
auth_token: permitted_params[:auth_token],
|
||||
api_key_sid: permitted_params[:api_key_sid],
|
||||
messaging_service_sid: permitted_params[:messaging_service_sid].presence,
|
||||
phone_number: phone_number,
|
||||
medium: medium
|
||||
@@ -52,7 +57,7 @@ class Api::V1::Accounts::Channels::TwilioChannelsController < Api::V1::Accounts:
|
||||
|
||||
def permitted_params
|
||||
params.require(:twilio_channel).permit(
|
||||
:account_id, :messaging_service_sid, :phone_number, :account_sid, :auth_token, :name, :medium
|
||||
:account_id, :messaging_service_sid, :phone_number, :account_sid, :auth_token, :name, :medium, :api_key_sid
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
class Api::V1::Accounts::ContactInboxesController < Api::V1::Accounts::BaseController
|
||||
before_action :ensure_inbox
|
||||
|
||||
def filter
|
||||
contact_inbox = @inbox.contact_inboxes.where(inbox_id: permitted_params[:inbox_id], source_id: permitted_params[:source_id])
|
||||
return head :not_found if contact_inbox.empty?
|
||||
|
||||
@contact = contact_inbox.first.contact
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def ensure_inbox
|
||||
@inbox = Current.account.inboxes.find(permitted_params[:inbox_id])
|
||||
authorize @inbox, :show?
|
||||
end
|
||||
|
||||
def permitted_params
|
||||
params.permit(:inbox_id, :source_id)
|
||||
end
|
||||
end
|
||||
@@ -4,6 +4,7 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
||||
sort_on :name, internal_name: :order_on_name, type: :scope, scope_params: [:direction]
|
||||
sort_on :phone_number, type: :string
|
||||
sort_on :last_activity_at, internal_name: :order_on_last_activity_at, type: :scope, scope_params: [:direction]
|
||||
sort_on :created_at, internal_name: :order_on_created_at, type: :scope, scope_params: [:direction]
|
||||
sort_on :company, internal_name: :order_on_company_name, type: :scope, scope_params: [:direction]
|
||||
sort_on :city, internal_name: :order_on_city, type: :scope, scope_params: [:direction]
|
||||
sort_on :country, internal_name: :order_on_country_name, type: :scope, scope_params: [:direction]
|
||||
@@ -17,18 +18,19 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
||||
|
||||
def index
|
||||
@contacts_count = resolved_contacts.count
|
||||
@contacts = fetch_contacts_with_conversation_count(resolved_contacts)
|
||||
@contacts = fetch_contacts(resolved_contacts)
|
||||
end
|
||||
|
||||
def search
|
||||
render json: { error: 'Specify search string with parameter q' }, status: :unprocessable_entity if params[:q].blank? && return
|
||||
|
||||
contacts = resolved_contacts.where(
|
||||
'name ILIKE :search OR email ILIKE :search OR phone_number ILIKE :search OR contacts.identifier LIKE :search',
|
||||
'name ILIKE :search OR email ILIKE :search OR phone_number ILIKE :search OR contacts.identifier LIKE :search
|
||||
OR contacts.additional_attributes->>\'company_name\' ILIKE :search',
|
||||
search: "%#{params[:q].strip}%"
|
||||
)
|
||||
@contacts_count = contacts.count
|
||||
@contacts = fetch_contacts_with_conversation_count(contacts)
|
||||
@contacts = fetch_contacts(contacts)
|
||||
end
|
||||
|
||||
def import
|
||||
@@ -62,7 +64,7 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
||||
result = ::Contacts::FilterService.new(params.permit!, current_user).perform
|
||||
contacts = result[:contacts]
|
||||
@contacts_count = result[:count]
|
||||
@contacts = fetch_contacts_with_conversation_count(contacts)
|
||||
@contacts = fetch_contacts(contacts)
|
||||
end
|
||||
|
||||
def contactable_inboxes
|
||||
@@ -124,17 +126,14 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
||||
@current_page = params[:page] || 1
|
||||
end
|
||||
|
||||
def fetch_contacts_with_conversation_count(contacts)
|
||||
conversation_count_sub_query = 'SELECT COUNT(*) FROM "conversations" WHERE "conversations"."contact_id" = "contacts"."id"'
|
||||
contacts_with_conversation_count = filtrate(contacts)
|
||||
.select("contacts.*, (#{conversation_count_sub_query}) as conversations_count")
|
||||
.group('contacts.id')
|
||||
.includes([{ avatar_attachment: [:blob] }])
|
||||
.page(@current_page).per(RESULTS_PER_PAGE)
|
||||
def fetch_contacts(contacts)
|
||||
contacts_with_avatar = filtrate(contacts)
|
||||
.includes([{ avatar_attachment: [:blob] }])
|
||||
.page(@current_page).per(RESULTS_PER_PAGE)
|
||||
|
||||
return contacts_with_conversation_count.includes([{ contact_inboxes: [:inbox] }]) if @include_contact_inboxes
|
||||
return contacts_with_avatar.includes([{ contact_inboxes: [:inbox] }]) if @include_contact_inboxes
|
||||
|
||||
contacts_with_conversation_count
|
||||
contacts_with_avatar
|
||||
end
|
||||
|
||||
def build_contact_inbox
|
||||
|
||||
@@ -60,7 +60,7 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
|
||||
end
|
||||
|
||||
def toggle_status
|
||||
if params[:status]
|
||||
if params[:status].present?
|
||||
set_conversation_status
|
||||
@status = @conversation.save!
|
||||
else
|
||||
|
||||
@@ -63,7 +63,7 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
||||
end
|
||||
|
||||
def destroy
|
||||
::DeleteObjectJob.perform_later(@inbox) if @inbox.present?
|
||||
::DeleteObjectJob.perform_later(@inbox, Current.user, request.ip) if @inbox.present?
|
||||
render status: :ok, json: { message: I18n.t('messages.inbox_deletetion_response') }
|
||||
end
|
||||
|
||||
@@ -124,7 +124,7 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
||||
def inbox_attributes
|
||||
[:name, :avatar, :greeting_enabled, :greeting_message, :enable_email_collect, :csat_survey_enabled,
|
||||
:enable_auto_assignment, :working_hours_enabled, :out_of_office_message, :timezone, :allow_messages_after_resolved,
|
||||
:lock_to_single_conversation, :portal_id]
|
||||
:lock_to_single_conversation, :portal_id, :sender_name_type, :business_name]
|
||||
end
|
||||
|
||||
def permitted_params(channel_attributes = [])
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
class Api::V1::Accounts::Integrations::SlackController < Api::V1::Accounts::BaseController
|
||||
before_action :check_admin_authorization?
|
||||
before_action :fetch_hook, only: [:update, :destroy]
|
||||
before_action :fetch_hook, only: [:update, :destroy, :list_all_channels]
|
||||
|
||||
def list_all_channels
|
||||
@channels = channel_builder.fetch_channels
|
||||
end
|
||||
|
||||
def create
|
||||
ActiveRecord::Base.transaction do
|
||||
builder = Integrations::Slack::HookBuilder.new(
|
||||
account: Current.account,
|
||||
code: params[:code],
|
||||
inbox_id: params[:inbox_id]
|
||||
)
|
||||
@hook = builder.perform
|
||||
create_chatwoot_slack_channel
|
||||
end
|
||||
hook_builder = Integrations::Slack::HookBuilder.new(
|
||||
account: Current.account,
|
||||
code: params[:code],
|
||||
inbox_id: params[:inbox_id]
|
||||
)
|
||||
@hook = hook_builder.perform
|
||||
end
|
||||
|
||||
def update
|
||||
create_chatwoot_slack_channel
|
||||
render json: @hook
|
||||
@hook = channel_builder.update(permitted_params[:reference_id])
|
||||
render json: { error: I18n.t('errors.slack.invalid_channel_id') }, status: :unprocessable_entity if @hook.blank?
|
||||
end
|
||||
|
||||
def destroy
|
||||
@hook.destroy!
|
||||
|
||||
head :ok
|
||||
end
|
||||
|
||||
@@ -31,11 +31,11 @@ class Api::V1::Accounts::Integrations::SlackController < Api::V1::Accounts::Base
|
||||
@hook = Integrations::Hook.where(account: Current.account).find_by(app_id: 'slack')
|
||||
end
|
||||
|
||||
def create_chatwoot_slack_channel
|
||||
channel = params[:channel] || 'customer-conversations'
|
||||
builder = Integrations::Slack::ChannelBuilder.new(
|
||||
hook: @hook, channel: channel
|
||||
)
|
||||
builder.perform
|
||||
def channel_builder
|
||||
Integrations::Slack::ChannelBuilder.new(hook: @hook)
|
||||
end
|
||||
|
||||
def permitted_params
|
||||
params.permit(:reference_id)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -39,16 +39,6 @@ class Api::V1::Accounts::MacrosController < Api::V1::Accounts::BaseController
|
||||
head :ok
|
||||
end
|
||||
|
||||
def attach_file
|
||||
file_blob = ActiveStorage::Blob.create_and_upload!(
|
||||
key: nil,
|
||||
io: params[:attachment].tempfile,
|
||||
filename: params[:attachment].original_filename,
|
||||
content_type: params[:attachment].content_type
|
||||
)
|
||||
render json: { blob_key: file_blob.key, blob_id: file_blob.id }
|
||||
end
|
||||
|
||||
def execute
|
||||
::MacrosExecutionJob.perform_later(@macro, conversation_ids: params[:conversation_ids], user: Current.user)
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController
|
||||
include ::FileTypeHelper
|
||||
|
||||
before_action :fetch_portal, except: [:index, :create, :attach_file]
|
||||
before_action :fetch_portal, except: [:index, :create]
|
||||
before_action :check_authorization
|
||||
before_action :set_current_page, only: [:index]
|
||||
|
||||
@@ -47,22 +47,17 @@ class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController
|
||||
head :ok
|
||||
end
|
||||
|
||||
def logo
|
||||
@portal.logo.purge if @portal.logo.attached?
|
||||
head :ok
|
||||
end
|
||||
|
||||
def process_attached_logo
|
||||
blob_id = params[:blob_id]
|
||||
blob = ActiveStorage::Blob.find_by(id: blob_id)
|
||||
@portal.logo.attach(blob)
|
||||
end
|
||||
|
||||
def attach_file
|
||||
file_blob = ActiveStorage::Blob.create_and_upload!(
|
||||
key: nil,
|
||||
io: params[:logo].tempfile,
|
||||
filename: params[:logo].original_filename,
|
||||
content_type: params[:logo].content_type
|
||||
)
|
||||
render json: { blob_key: file_blob.key, blob_id: file_blob.id }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def fetch_portal
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
class Api::V1::Accounts::UploadController < Api::V1::Accounts::BaseController
|
||||
def create
|
||||
file_blob = ActiveStorage::Blob.create_and_upload!(
|
||||
key: nil,
|
||||
io: params[:attachment].tempfile,
|
||||
filename: params[:attachment].original_filename,
|
||||
content_type: params[:attachment].content_type
|
||||
)
|
||||
file_blob.save!
|
||||
|
||||
render json: { file_url: url_for(file_blob), blob_key: file_blob.key, blob_id: file_blob.id }
|
||||
end
|
||||
end
|
||||
@@ -1,7 +1,15 @@
|
||||
class Api::V1::Integrations::WebhooksController < ApplicationController
|
||||
def create
|
||||
builder = Integrations::Slack::IncomingMessageBuilder.new(params)
|
||||
builder = Integrations::Slack::IncomingMessageBuilder.new(permitted_params)
|
||||
response = builder.perform
|
||||
render json: response
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# TODO: This is a temporary solution to permit all params for slack unfurling job.
|
||||
# We should only permit the params that we use. Handle all the params based on events and send it to the respective services.
|
||||
def permitted_params
|
||||
params.permit!
|
||||
end
|
||||
end
|
||||
|
||||
@@ -79,6 +79,9 @@ class Api::V1::Widget::BaseController < ApplicationController
|
||||
sender: @contact,
|
||||
content: permitted_params[:message][:content],
|
||||
inbox_id: conversation.inbox_id,
|
||||
content_attributes: {
|
||||
in_reply_to: permitted_params[:message][:reply_to]
|
||||
},
|
||||
echo_id: permitted_params[:message][:echo_id],
|
||||
message_type: :incoming
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
|
||||
include Events::Types
|
||||
before_action :render_not_found_if_empty, only: [:toggle_typing, :toggle_status, :set_custom_attributes, :destroy_custom_attributes]
|
||||
|
||||
def index
|
||||
@conversation = conversation
|
||||
@@ -27,6 +28,7 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
|
||||
|
||||
conversation.contact_last_seen_at = DateTime.now.utc
|
||||
conversation.save!
|
||||
::Conversations::UpdateMessageStatusJob.perform_later(conversation.id, conversation.contact_last_seen_at)
|
||||
head :ok
|
||||
end
|
||||
|
||||
@@ -41,8 +43,6 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
|
||||
end
|
||||
|
||||
def toggle_typing
|
||||
head :ok && return if conversation.nil?
|
||||
|
||||
case permitted_params[:typing_status]
|
||||
when 'on'
|
||||
trigger_typing_event(CONVERSATION_TYPING_ON)
|
||||
@@ -54,8 +54,6 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
|
||||
end
|
||||
|
||||
def toggle_status
|
||||
return head :not_found if conversation.nil?
|
||||
|
||||
return head :forbidden unless @web_widget.end_conversation?
|
||||
|
||||
unless conversation.resolved?
|
||||
@@ -81,6 +79,10 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
|
||||
Rails.configuration.dispatcher.dispatch(event, Time.zone.now, conversation: conversation, user: @contact)
|
||||
end
|
||||
|
||||
def render_not_found_if_empty
|
||||
return head :not_found if conversation.nil?
|
||||
end
|
||||
|
||||
def permitted_params
|
||||
params.permit(:id, :typing_status, :website_token, :email, contact: [:name, :email, :phone_number],
|
||||
message: [:content, :referer_url, :timestamp, :echo_id],
|
||||
|
||||
@@ -17,7 +17,8 @@ class Api::V1::Widget::MessagesController < Api::V1::Widget::BaseController
|
||||
@message.update!(submitted_email: contact_email)
|
||||
ContactIdentifyAction.new(
|
||||
contact: @contact,
|
||||
params: { email: contact_email, name: contact_name }
|
||||
params: { email: contact_email, name: contact_name },
|
||||
retain_original_contact_name: true
|
||||
).perform
|
||||
else
|
||||
@message.update!(message_update_params[:message])
|
||||
@@ -63,7 +64,7 @@ class Api::V1::Widget::MessagesController < Api::V1::Widget::BaseController
|
||||
|
||||
def permitted_params
|
||||
# timestamp parameter is used in create conversation method
|
||||
params.permit(:id, :before, :after, :website_token, contact: [:name, :email], message: [:content, :referer_url, :timestamp, :echo_id])
|
||||
params.permit(:id, :before, :after, :website_token, contact: [:name, :email], message: [:content, :referer_url, :timestamp, :echo_id, :reply_to])
|
||||
end
|
||||
|
||||
def set_message
|
||||
|
||||
@@ -8,7 +8,7 @@ module EnsureCurrentAccountHelper
|
||||
|
||||
def ensure_current_account
|
||||
account = Account.find(params[:account_id])
|
||||
ensure_account_is_active?(account)
|
||||
render_unauthorized('Account is suspended') and return unless account.active?
|
||||
|
||||
if current_user
|
||||
account_accessible_for_user?(account)
|
||||
@@ -27,8 +27,4 @@ module EnsureCurrentAccountHelper
|
||||
def account_accessible_for_bot?(account)
|
||||
render_unauthorized('You are not authorized to access this account') unless @resource.agent_bot_inboxes.find_by(account_id: account.id)
|
||||
end
|
||||
|
||||
def ensure_account_is_active?(account)
|
||||
render_unauthorized('Account is suspended') unless account.active?
|
||||
end
|
||||
end
|
||||
|
||||
@@ -9,11 +9,14 @@ module RequestExceptionHandler
|
||||
|
||||
def handle_with_exception
|
||||
yield
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
rescue ActiveRecord::RecordNotFound => e
|
||||
log_handled_error(e)
|
||||
render_not_found_error('Resource could not be found')
|
||||
rescue Pundit::NotAuthorizedError
|
||||
rescue Pundit::NotAuthorizedError => e
|
||||
log_handled_error(e)
|
||||
render_unauthorized('You are not authorized to do this action')
|
||||
rescue ActionController::ParameterMissing => e
|
||||
log_handled_error(e)
|
||||
render_could_not_create_error(e.message)
|
||||
ensure
|
||||
# to address the thread variable leak issues in Puma/Thin webserver
|
||||
@@ -41,6 +44,7 @@ module RequestExceptionHandler
|
||||
end
|
||||
|
||||
def render_record_invalid(exception)
|
||||
log_handled_error(exception)
|
||||
render json: {
|
||||
message: exception.record.errors.full_messages.join(', '),
|
||||
attributes: exception.record.errors.attribute_names
|
||||
@@ -48,6 +52,11 @@ module RequestExceptionHandler
|
||||
end
|
||||
|
||||
def render_error_response(exception)
|
||||
log_handled_error(exception)
|
||||
render json: exception.to_hash, status: exception.http_status
|
||||
end
|
||||
|
||||
def log_handled_error(exception)
|
||||
logger.info("Handled error: #{exception.inspect}")
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
class DashboardController < ActionController::Base
|
||||
include SwitchLocale
|
||||
|
||||
before_action :set_application_pack
|
||||
before_action :set_global_config
|
||||
around_action :switch_locale
|
||||
before_action :ensure_installation_onboarding, only: [:index]
|
||||
@@ -14,7 +15,7 @@ class DashboardController < ActionController::Base
|
||||
|
||||
def set_global_config
|
||||
@global_config = GlobalConfig.get(
|
||||
'LOGO', 'LOGO_THUMBNAIL',
|
||||
'LOGO', 'LOGO_DARK', 'LOGO_THUMBNAIL',
|
||||
'INSTALLATION_NAME',
|
||||
'WIDGET_BRAND_URL', 'TERMS_URL',
|
||||
'PRIVACY_URL',
|
||||
@@ -60,4 +61,12 @@ class DashboardController < ActionController::Base
|
||||
GIT_SHA: GIT_HASH
|
||||
}
|
||||
end
|
||||
|
||||
def set_application_pack
|
||||
@application_pack = if request.path.include?('/auth') || request.path.include?('/login')
|
||||
'v3app'
|
||||
else
|
||||
'application'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
class Public::Api::V1::Inboxes::ConversationsController < Public::Api::V1::InboxesController
|
||||
include Events::Types
|
||||
before_action :set_conversation, only: [:toggle_typing, :update_last_seen]
|
||||
|
||||
def index
|
||||
@conversations = @contact_inbox.hmac_verified? ? @contact.conversations : @contact_inbox.conversations
|
||||
end
|
||||
@@ -7,18 +10,38 @@ class Public::Api::V1::Inboxes::ConversationsController < Public::Api::V1::Inbox
|
||||
@conversation = create_conversation
|
||||
end
|
||||
|
||||
def toggle_typing
|
||||
case params[:typing_status]
|
||||
when 'on'
|
||||
trigger_typing_event(CONVERSATION_TYPING_ON)
|
||||
when 'off'
|
||||
trigger_typing_event(CONVERSATION_TYPING_OFF)
|
||||
end
|
||||
head :ok
|
||||
end
|
||||
|
||||
def update_last_seen
|
||||
@conversation.contact_last_seen_at = DateTime.now.utc
|
||||
@conversation.save!
|
||||
::Conversations::UpdateMessageStatusJob.perform_later(@conversation.id, @conversation.contact_last_seen_at)
|
||||
head :ok
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_conversation
|
||||
@conversation = @contact_inbox.contact.conversations.find_by!(display_id: params[:id])
|
||||
end
|
||||
|
||||
def create_conversation
|
||||
::Conversation.create!(conversation_params)
|
||||
ConversationBuilder.new(params: conversation_params, contact_inbox: @contact_inbox).perform
|
||||
end
|
||||
|
||||
def trigger_typing_event(event)
|
||||
Rails.configuration.dispatcher.dispatch(event, Time.zone.now, conversation: @conversation, user: @conversation.contact)
|
||||
end
|
||||
|
||||
def conversation_params
|
||||
{
|
||||
account_id: @contact_inbox.contact.account_id,
|
||||
inbox_id: @contact_inbox.inbox_id,
|
||||
contact_id: @contact_inbox.contact_id,
|
||||
contact_inbox_id: @contact_inbox.id
|
||||
}
|
||||
params.permit(custom_attributes: {})
|
||||
end
|
||||
end
|
||||
|
||||
@@ -8,13 +8,22 @@ class Public::Api::V1::Portals::ArticlesController < Public::Api::V1::Portals::B
|
||||
def index
|
||||
@articles = @portal.articles
|
||||
@articles = @articles.search(list_params) if list_params.present?
|
||||
@articles.order(position: :asc)
|
||||
order_by_sort_param
|
||||
@articles.page(list_params[:page]) if list_params[:page].present?
|
||||
end
|
||||
|
||||
def show; end
|
||||
|
||||
private
|
||||
|
||||
def order_by_sort_param
|
||||
@articles = if list_params[:sort].present? && list_params[:sort] == 'views'
|
||||
@articles.order_by_views
|
||||
else
|
||||
@articles.order_by_position
|
||||
end
|
||||
end
|
||||
|
||||
def set_article
|
||||
@article = @portal.articles.find_by(slug: permitted_params[:article_slug])
|
||||
@article.increment_view_count
|
||||
@@ -30,12 +39,8 @@ class Public::Api::V1::Portals::ArticlesController < Public::Api::V1::Portals::B
|
||||
)
|
||||
end
|
||||
|
||||
def portal
|
||||
@portal ||= Portal.find_by!(slug: permitted_params[:slug], archived: false)
|
||||
end
|
||||
|
||||
def list_params
|
||||
params.permit(:query, :locale)
|
||||
params.permit(:query, :locale, :sort, :status)
|
||||
end
|
||||
|
||||
def permitted_params
|
||||
|
||||
@@ -1,8 +1,27 @@
|
||||
class Public::Api::V1::Portals::BaseController < PublicController
|
||||
before_action :show_plain_layout
|
||||
before_action :set_color_scheme
|
||||
around_action :set_locale
|
||||
after_action :allow_iframe_requests
|
||||
|
||||
private
|
||||
|
||||
def show_plain_layout
|
||||
@is_plain_layout_enabled = params[:show_plain_layout] == 'true'
|
||||
end
|
||||
|
||||
def set_color_scheme
|
||||
@theme = if %w[dark light].include?(params[:theme])
|
||||
params[:theme]
|
||||
else
|
||||
'system'
|
||||
end
|
||||
end
|
||||
|
||||
def portal
|
||||
@portal ||= Portal.find_by!(slug: params[:slug], archived: false)
|
||||
end
|
||||
|
||||
def set_locale(&)
|
||||
switch_locale_with_portal(&) if params[:locale].present?
|
||||
switch_locale_with_article(&) if params[:article_slug].present?
|
||||
@@ -25,6 +44,8 @@ class Public::Api::V1::Portals::BaseController < PublicController
|
||||
|
||||
def switch_locale_with_article(&)
|
||||
article = Article.find_by(slug: params[:article_slug])
|
||||
Rails.logger.info "Article: not found for slug: #{params[:article_slug]}"
|
||||
render_404 && return if article.blank?
|
||||
|
||||
@locale = if article.category.present?
|
||||
article.category.locale
|
||||
@@ -34,4 +55,13 @@ class Public::Api::V1::Portals::BaseController < PublicController
|
||||
|
||||
I18n.with_locale(@locale, &)
|
||||
end
|
||||
|
||||
def allow_iframe_requests
|
||||
response.headers.delete('X-Frame-Options') if @is_plain_layout_enabled
|
||||
end
|
||||
|
||||
def render_404
|
||||
portal
|
||||
render 'public/api/v1/portals/error/404', status: :not_found
|
||||
end
|
||||
end
|
||||
|
||||
@@ -13,10 +13,9 @@ class Public::Api::V1::Portals::CategoriesController < Public::Api::V1::Portals:
|
||||
private
|
||||
|
||||
def set_category
|
||||
@category = @portal.categories.find_by!(locale: params[:locale], slug: params[:category_slug])
|
||||
end
|
||||
@category = @portal.categories.find_by(locale: params[:locale], slug: params[:category_slug])
|
||||
|
||||
def portal
|
||||
@portal ||= Portal.find_by!(slug: params[:slug], archived: false)
|
||||
Rails.logger.info "Category: not found for slug: #{params[:category_slug]}"
|
||||
render_404 && return if @category.blank?
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
class SlackUploadsController < ApplicationController
|
||||
include Rails.application.routes.url_helpers
|
||||
before_action :set_blob, only: [:show]
|
||||
|
||||
def show
|
||||
if @blob
|
||||
redirect_to blob_url
|
||||
else
|
||||
redirect_to avatar_url
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_blob
|
||||
@blob = ActiveStorage::Blob.find_by(key: params[:blob_key])
|
||||
end
|
||||
|
||||
def blob_url
|
||||
url_for(@blob.representation(resize_to_fill: [250, nil]))
|
||||
end
|
||||
|
||||
def avatar_url
|
||||
base_url = ENV.fetch('FRONTEND_URL', nil)
|
||||
"#{base_url}/integrations/slack/#{params[:sender_type]}.png"
|
||||
end
|
||||
end
|
||||
@@ -6,6 +6,7 @@ class SuperAdmin::InstanceStatusesController < SuperAdmin::ApplicationController
|
||||
postgres_status
|
||||
redis_metrics
|
||||
chatwoot_edition
|
||||
instance_meta
|
||||
end
|
||||
|
||||
def chatwoot_edition
|
||||
@@ -18,6 +19,10 @@ class SuperAdmin::InstanceStatusesController < SuperAdmin::ApplicationController
|
||||
end
|
||||
end
|
||||
|
||||
def instance_meta
|
||||
@metrics['Database Migrations'] = ActiveRecord::Base.connection.migration_context.needs_migration? ? 'pending' : 'completed'
|
||||
end
|
||||
|
||||
def chatwoot_version
|
||||
@metrics['Chatwoot version'] = Chatwoot.config[:version]
|
||||
end
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
class Twilio::DeliveryStatusController < ApplicationController
|
||||
def create
|
||||
::Twilio::DeliveryStatusService.new(params: permitted_params).perform
|
||||
|
||||
head :no_content
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def permitted_params
|
||||
params.permit(
|
||||
:AccountSid,
|
||||
:From,
|
||||
:MessageSid,
|
||||
:MessagingServiceSid,
|
||||
:MessageStatus,
|
||||
:ErrorCode,
|
||||
:ErrorMessage
|
||||
)
|
||||
end
|
||||
end
|
||||
@@ -18,8 +18,8 @@ class AccountDashboard < Administrate::BaseDashboard
|
||||
end
|
||||
|
||||
ATTRIBUTE_TYPES = {
|
||||
id: Field::Number,
|
||||
name: Field::String,
|
||||
id: Field::Number.with_options(searchable: true),
|
||||
name: Field::String.with_options(searchable: true),
|
||||
created_at: Field::DateTime,
|
||||
updated_at: Field::DateTime,
|
||||
users: CountField,
|
||||
@@ -86,7 +86,10 @@ class AccountDashboard < Administrate::BaseDashboard
|
||||
"##{account.id} #{account.name}"
|
||||
end
|
||||
|
||||
def permitted_attributes
|
||||
# We do not use the action parameter but we still need to define it
|
||||
# to prevent an error from being raised (wrong number of arguments)
|
||||
# Reference: https://github.com/thoughtbot/administrate/pull/2356/files#diff-4e220b661b88f9a19ac527c50d6f1577ef6ab7b0bed2bfdf048e22e6bfa74a05R204
|
||||
def permitted_attributes(action)
|
||||
super + [limits: {}]
|
||||
end
|
||||
end
|
||||
|
||||
@@ -8,9 +8,9 @@ class AccountUserDashboard < Administrate::BaseDashboard
|
||||
# which determines how the attribute is displayed
|
||||
# on pages throughout the dashboard.
|
||||
ATTRIBUTE_TYPES = {
|
||||
account: Field::BelongsTo.with_options(searchable: true, searchable_field: 'name', order: 'id DESC'),
|
||||
user: Field::BelongsTo.with_options(searchable: true, searchable_field: 'name', order: 'id DESC'),
|
||||
inviter: Field::BelongsTo.with_options(class_name: 'User', searchable: true, searchable_field: 'name'),
|
||||
account: Field::BelongsToSearch.with_options(class_name: 'Account', searchable: true, searchable_field: [:name, :id], order: 'id DESC'),
|
||||
user: Field::BelongsToSearch.with_options(class_name: 'User', searchable: true, searchable_field: [:name, :email, :id], order: 'id DESC'),
|
||||
inviter: Field::BelongsToSearch.with_options(class_name: 'User', searchable: true, searchable_field: [:name, :email, :id], order: 'id DESC'),
|
||||
id: Field::Number,
|
||||
role: Field::Select.with_options(collection: AccountUser.roles.keys),
|
||||
created_at: Field::DateTime,
|
||||
|
||||
@@ -9,7 +9,7 @@ class UserDashboard < Administrate::BaseDashboard
|
||||
# on pages throughout the dashboard.
|
||||
ATTRIBUTE_TYPES = {
|
||||
account_users: Field::HasMany,
|
||||
id: Field::Number,
|
||||
id: Field::Number.with_options(searchable: true),
|
||||
avatar_url: AvatarField,
|
||||
avatar: Field::ActiveStorage.with_options(
|
||||
destroy_url: proc do |_namespace, _resource, attachment|
|
||||
@@ -28,9 +28,9 @@ class UserDashboard < Administrate::BaseDashboard
|
||||
confirmed_at: Field::DateTime,
|
||||
confirmation_sent_at: Field::DateTime,
|
||||
unconfirmed_email: Field::String,
|
||||
name: Field::String,
|
||||
name: Field::String.with_options(searchable: true),
|
||||
display_name: Field::String,
|
||||
email: Field::String,
|
||||
email: Field::String.with_options(searchable: true),
|
||||
tokens: Field::String.with_options(searchable: false),
|
||||
created_at: Field::DateTime,
|
||||
updated_at: Field::DateTime,
|
||||
|
||||
@@ -6,7 +6,8 @@ class ConversationFinder
|
||||
latest: 'latest',
|
||||
sort_on_created_at: 'sort_on_created_at',
|
||||
last_user_message_at: 'last_user_message_at',
|
||||
sort_on_priority: 'sort_on_priority'
|
||||
sort_on_priority: 'sort_on_priority',
|
||||
sort_on_waiting_since: 'sort_on_waiting_since'
|
||||
}.with_indifferent_access
|
||||
|
||||
# assumptions
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
module PortalHelper
|
||||
def generate_portal_bg_color(portal_color, theme)
|
||||
base_color = theme == 'dark' ? 'black' : 'white'
|
||||
"color-mix(in srgb, #{portal_color} 20%, #{base_color})"
|
||||
end
|
||||
|
||||
def generate_portal_bg(portal_color, theme)
|
||||
bg_image = theme == 'dark' ? 'hexagon-dark.svg' : 'hexagon-light.svg'
|
||||
"background: url(/assets/images/hc/#{bg_image}) #{generate_portal_bg_color(portal_color, theme)}"
|
||||
end
|
||||
|
||||
def language_name(locale)
|
||||
language_map = YAML.load_file(Rails.root.join('config/languages/language_map.yml'))
|
||||
language_map[locale] || locale
|
||||
end
|
||||
|
||||
def get_theme_names(theme)
|
||||
if theme == 'light'
|
||||
I18n.t('public_portal.header.appearance.light')
|
||||
elsif theme == 'dark'
|
||||
I18n.t('public_portal.header.appearance.dark')
|
||||
else
|
||||
I18n.t('public_portal.header.appearance.system')
|
||||
end
|
||||
end
|
||||
|
||||
def get_theme_icon(theme)
|
||||
if theme == 'light'
|
||||
'icons/sun'
|
||||
elsif theme == 'dark'
|
||||
'icons/moon'
|
||||
else
|
||||
'icons/monitor'
|
||||
end
|
||||
end
|
||||
|
||||
def generate_gradient_to_bottom(theme)
|
||||
"background-image: linear-gradient(to bottom, transparent, #{theme == 'dark' ? '#151718' : 'white'})"
|
||||
end
|
||||
|
||||
def generate_article_link(portal_slug, article_slug, theme)
|
||||
"/hc/#{portal_slug}/articles/#{article_slug}#{theme.present? && theme != 'system' ? "?theme=#{theme}" : ''}"
|
||||
end
|
||||
|
||||
def render_category_content(content)
|
||||
ChatwootMarkdownRenderer.new(content).render_markdown_to_plain_text
|
||||
end
|
||||
end
|
||||
@@ -17,21 +17,36 @@ module ReportHelper
|
||||
end
|
||||
|
||||
def conversations_count
|
||||
(get_grouped_values scope.conversations.where(account_id: account.id)).count
|
||||
(get_grouped_values conversations).count
|
||||
end
|
||||
|
||||
def incoming_messages_count
|
||||
(get_grouped_values scope.messages.where(account_id: account.id).incoming.unscope(:order)).count
|
||||
(get_grouped_values incoming_messages).count
|
||||
end
|
||||
|
||||
def outgoing_messages_count
|
||||
(get_grouped_values scope.messages.where(account_id: account.id).outgoing.unscope(:order)).count
|
||||
(get_grouped_values outgoing_messages).count
|
||||
end
|
||||
|
||||
def resolutions_count
|
||||
object_scope = scope.reporting_events.joins(:conversation).select(:conversation_id).where(account_id: account.id, name: :conversation_resolved,
|
||||
conversations: { status: :resolved }).distinct
|
||||
(get_grouped_values object_scope).count
|
||||
(get_grouped_values resolutions).count
|
||||
end
|
||||
|
||||
def conversations
|
||||
scope.conversations.where(account_id: account.id, created_at: range)
|
||||
end
|
||||
|
||||
def incoming_messages
|
||||
scope.messages.where(account_id: account.id, created_at: range).incoming.unscope(:order)
|
||||
end
|
||||
|
||||
def outgoing_messages
|
||||
scope.messages.where(account_id: account.id, created_at: range).outgoing.unscope(:order)
|
||||
end
|
||||
|
||||
def resolutions
|
||||
scope.reporting_events.joins(:conversation).select(:conversation_id).where(account_id: account.id, name: :conversation_resolved,
|
||||
conversations: { status: :resolved }, created_at: range).distinct
|
||||
end
|
||||
|
||||
def avg_first_response_time
|
||||
@@ -41,6 +56,13 @@ module ReportHelper
|
||||
grouped_reporting_events.average(:value)
|
||||
end
|
||||
|
||||
def reply_time
|
||||
grouped_reporting_events = (get_grouped_values scope.reporting_events.where(name: 'reply_time', account_id: account.id))
|
||||
return grouped_reporting_events.average(:value_in_business_hours) if params[:business_hours]
|
||||
|
||||
grouped_reporting_events.average(:value)
|
||||
end
|
||||
|
||||
def avg_resolution_time
|
||||
grouped_reporting_events = (get_grouped_values scope.reporting_events.where(name: 'conversation_resolved', account_id: account.id))
|
||||
return grouped_reporting_events.average(:value_in_business_hours) if params[:business_hours]
|
||||
@@ -51,17 +73,35 @@ module ReportHelper
|
||||
def avg_resolution_time_summary
|
||||
reporting_events = scope.reporting_events
|
||||
.where(name: 'conversation_resolved', account_id: account.id, created_at: range)
|
||||
avg_rt = params[:business_hours] ? reporting_events.average(:value_in_business_hours) : reporting_events.average(:value)
|
||||
avg_rt = if params[:business_hours].present?
|
||||
reporting_events.average(:value_in_business_hours)
|
||||
else
|
||||
reporting_events.average(:value)
|
||||
end
|
||||
|
||||
return 0 if avg_rt.blank?
|
||||
|
||||
avg_rt
|
||||
end
|
||||
|
||||
def reply_time_summary
|
||||
reporting_events = scope.reporting_events
|
||||
.where(name: 'reply_time', account_id: account.id, created_at: range)
|
||||
reply_time = params[:business_hours] ? reporting_events.average(:value_in_business_hours) : reporting_events.average(:value)
|
||||
|
||||
return 0 if reply_time.blank?
|
||||
|
||||
reply_time
|
||||
end
|
||||
|
||||
def avg_first_response_time_summary
|
||||
reporting_events = scope.reporting_events
|
||||
.where(name: 'first_response', account_id: account.id, created_at: range)
|
||||
avg_frt = params[:business_hours] ? reporting_events.average(:value_in_business_hours) : reporting_events.average(:value)
|
||||
avg_frt = if params[:business_hours].present?
|
||||
reporting_events.average(:value_in_business_hours)
|
||||
else
|
||||
reporting_events.average(:value)
|
||||
end
|
||||
|
||||
return 0 if avg_frt.blank?
|
||||
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="!authUIFlags.isFetching"
|
||||
v-if="!authUIFlags.isFetching && !accountUIFlags.isFetchingItem"
|
||||
id="app"
|
||||
class="app-wrapper app-root"
|
||||
class="app-wrapper h-full flex-grow-0 min-h-0 w-full"
|
||||
:class="{ 'app-rtl--wrapper': isRTLView }"
|
||||
:dir="isRTLView ? 'rtl' : 'ltr'"
|
||||
>
|
||||
<update-banner :latest-chatwoot-version="latestChatwootVersion" />
|
||||
<template v-if="!accountUIFlags.isFetchingItem && currentAccountId">
|
||||
<template v-if="currentAccountId">
|
||||
<payment-pending-banner />
|
||||
<upgrade-banner />
|
||||
</template>
|
||||
@@ -25,15 +26,16 @@
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import AddAccountModal from '../dashboard/components/layout/sidebarComponents/AddAccountModal';
|
||||
import AddAccountModal from '../dashboard/components/layout/sidebarComponents/AddAccountModal.vue';
|
||||
import LoadingState from './components/widgets/LoadingState.vue';
|
||||
import NetworkNotification from './components/NetworkNotification';
|
||||
import NetworkNotification from './components/NetworkNotification.vue';
|
||||
import UpdateBanner from './components/app/UpdateBanner.vue';
|
||||
import UpgradeBanner from './components/app/UpgradeBanner.vue';
|
||||
import PaymentPendingBanner from './components/app/PaymentPendingBanner.vue';
|
||||
import vueActionCable from './helper/actionCable';
|
||||
import WootSnackbarBox from './components/SnackbarContainer';
|
||||
import WootSnackbarBox from './components/SnackbarContainer.vue';
|
||||
import rtlMixin from 'shared/mixins/rtlMixin';
|
||||
import { setColorTheme } from './helper/themeHelper';
|
||||
import {
|
||||
registerSubscription,
|
||||
verifyServiceWorkerExistence,
|
||||
@@ -89,9 +91,18 @@ export default {
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.initializeColorTheme();
|
||||
this.listenToThemeChanges();
|
||||
this.setLocale(window.chatwootConfig.selectedLocale);
|
||||
},
|
||||
methods: {
|
||||
initializeColorTheme() {
|
||||
setColorTheme(window.matchMedia('(prefers-color-scheme: dark)').matches);
|
||||
},
|
||||
listenToThemeChanges() {
|
||||
const mql = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
mql.onchange = e => setColorTheme(e.matches);
|
||||
},
|
||||
setLocale(locale) {
|
||||
this.$root.$i18n.locale = locale;
|
||||
},
|
||||
@@ -100,10 +111,8 @@ export default {
|
||||
this.$store.dispatch('setActiveAccount', {
|
||||
accountId: this.currentAccountId,
|
||||
});
|
||||
const {
|
||||
locale,
|
||||
latest_chatwoot_version: latestChatwootVersion,
|
||||
} = this.getAccount(this.currentAccountId);
|
||||
const { locale, latest_chatwoot_version: latestChatwootVersion } =
|
||||
this.getAccount(this.currentAccountId);
|
||||
const { pubsub_token: pubsubToken } = this.currentUser || {};
|
||||
this.setLocale(locale);
|
||||
this.updateRTLDirectionView(locale);
|
||||
|
||||
@@ -15,9 +15,8 @@ class ApiClient {
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
get accountIdFromRoute() {
|
||||
const isInsideAccountScopedURLs = window.location.pathname.includes(
|
||||
'/app/accounts'
|
||||
);
|
||||
const isInsideAccountScopedURLs =
|
||||
window.location.pathname.includes('/app/accounts');
|
||||
|
||||
if (isInsideAccountScopedURLs) {
|
||||
return window.location.pathname.split('/')[3];
|
||||
|
||||
@@ -18,6 +18,10 @@ class CacheEnabledApiClient extends ApiClient {
|
||||
return this.getFromCache();
|
||||
}
|
||||
|
||||
return this.getFromNetwork();
|
||||
}
|
||||
|
||||
getFromNetwork() {
|
||||
return axios.get(this.url);
|
||||
}
|
||||
|
||||
@@ -32,7 +36,12 @@ class CacheEnabledApiClient extends ApiClient {
|
||||
}
|
||||
|
||||
async getFromCache() {
|
||||
await this.dataManager.initDb();
|
||||
try {
|
||||
// IDB is not supported in Firefox private mode: https://bugzilla.mozilla.org/show_bug.cgi?id=781982
|
||||
await this.dataManager.initDb();
|
||||
} catch {
|
||||
return this.getFromNetwork();
|
||||
}
|
||||
|
||||
const { data } = await axios.get(
|
||||
`/api/v1/accounts/${this.accountIdFromRoute}/cache_keys`
|
||||
@@ -55,16 +64,22 @@ class CacheEnabledApiClient extends ApiClient {
|
||||
}
|
||||
|
||||
async refetchAndCommit(newKey = null) {
|
||||
await this.dataManager.initDb();
|
||||
const response = await axios.get(this.url);
|
||||
this.dataManager.replace({
|
||||
modelName: this.cacheModelName,
|
||||
data: this.extractDataFromResponse(response),
|
||||
});
|
||||
const response = await this.getFromNetwork();
|
||||
|
||||
await this.dataManager.setCacheKeys({
|
||||
[this.cacheModelName]: newKey,
|
||||
});
|
||||
try {
|
||||
await this.dataManager.initDb();
|
||||
|
||||
this.dataManager.replace({
|
||||
modelName: this.cacheModelName,
|
||||
data: this.extractDataFromResponse(response),
|
||||
});
|
||||
|
||||
await this.dataManager.setCacheKeys({
|
||||
[this.cacheModelName]: newKey,
|
||||
});
|
||||
} catch {
|
||||
// Ignore error
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
@@ -3,47 +3,11 @@
|
||||
import Cookies from 'js-cookie';
|
||||
import endPoints from './endPoints';
|
||||
import {
|
||||
setAuthCredentials,
|
||||
clearCookiesOnLogout,
|
||||
deleteIndexedDBOnLogout,
|
||||
} from '../store/utils/api';
|
||||
|
||||
export default {
|
||||
login(creds) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios
|
||||
.post('auth/sign_in', creds)
|
||||
.then(response => {
|
||||
setAuthCredentials(response);
|
||||
resolve(response.data);
|
||||
})
|
||||
.catch(error => {
|
||||
reject(error.response);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
register(creds) {
|
||||
const urlData = endPoints('register');
|
||||
const fetchPromise = new Promise((resolve, reject) => {
|
||||
axios
|
||||
.post(urlData.url, {
|
||||
account_name: creds.accountName.trim(),
|
||||
user_full_name: creds.fullName.trim(),
|
||||
email: creds.email,
|
||||
password: creds.password,
|
||||
h_captcha_client_response: creds.hCaptchaClientResponse,
|
||||
})
|
||||
.then(response => {
|
||||
setAuthCredentials(response);
|
||||
resolve(response);
|
||||
})
|
||||
.catch(error => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
return fetchPromise;
|
||||
},
|
||||
validityCheck() {
|
||||
const urlData = endPoints('validityCheck');
|
||||
return axios.get(urlData.url);
|
||||
@@ -73,45 +37,6 @@ export default {
|
||||
}
|
||||
return false;
|
||||
},
|
||||
verifyPasswordToken({ confirmationToken }) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios
|
||||
.post('auth/confirmation', {
|
||||
confirmation_token: confirmationToken,
|
||||
})
|
||||
.then(response => {
|
||||
setAuthCredentials(response);
|
||||
resolve(response);
|
||||
})
|
||||
.catch(error => {
|
||||
reject(error.response);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
setNewPassword({ resetPasswordToken, password, confirmPassword }) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios
|
||||
.put('auth/password', {
|
||||
reset_password_token: resetPasswordToken,
|
||||
password_confirmation: confirmPassword,
|
||||
password,
|
||||
})
|
||||
.then(response => {
|
||||
setAuthCredentials(response);
|
||||
resolve(response);
|
||||
})
|
||||
.catch(error => {
|
||||
reject(error.response);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
resetPassword({ email }) {
|
||||
const urlData = endPoints('resetPassword');
|
||||
return axios.post(urlData.url, { email });
|
||||
},
|
||||
|
||||
profileUpdate({
|
||||
password,
|
||||
password_confirmation,
|
||||
@@ -159,6 +84,11 @@ export default {
|
||||
return axios.delete(endPoints('deleteAvatar').url);
|
||||
},
|
||||
|
||||
resetPassword({ email }) {
|
||||
const urlData = endPoints('resetPassword');
|
||||
return axios.post(urlData.url, { email });
|
||||
},
|
||||
|
||||
setActiveAccount({ accountId }) {
|
||||
const urlData = endPoints('setActiveAccount');
|
||||
return axios.put(urlData.url, {
|
||||
|
||||
@@ -9,14 +9,6 @@ class AutomationsAPI extends ApiClient {
|
||||
clone(automationId) {
|
||||
return axios.post(`${this.url}/${automationId}/clone`);
|
||||
}
|
||||
|
||||
attachment(file) {
|
||||
return axios.post(`${this.url}/attach_file`, file, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new AutomationsAPI();
|
||||
|
||||
@@ -53,6 +53,7 @@ class ContactAPI extends ApiClient {
|
||||
return axios.get(requestURL);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line default-param-last
|
||||
filter(page = 1, sortAttr = 'name', queryPayload) {
|
||||
let requestURL = `${this.url}/filter?${buildContactParams(page, sortAttr)}`;
|
||||
return axios.post(requestURL, queryPayload);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import accountAPI from '../account';
|
||||
import ApiClient from '../../ApiClient';
|
||||
import describeWithAPIMock from '../../specs/apiSpecHelper';
|
||||
|
||||
describe('#enterpriseAccountAPI', () => {
|
||||
it('creates correct instance', () => {
|
||||
@@ -13,17 +12,33 @@ describe('#enterpriseAccountAPI', () => {
|
||||
expect(accountAPI).toHaveProperty('checkout');
|
||||
});
|
||||
|
||||
describeWithAPIMock('API calls', context => {
|
||||
describe('API calls', () => {
|
||||
const originalAxios = window.axios;
|
||||
const axiosMock = {
|
||||
post: jest.fn(() => Promise.resolve()),
|
||||
get: jest.fn(() => Promise.resolve()),
|
||||
patch: jest.fn(() => Promise.resolve()),
|
||||
delete: jest.fn(() => Promise.resolve()),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
window.axios = axiosMock;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
window.axios = originalAxios;
|
||||
});
|
||||
|
||||
it('#checkout', () => {
|
||||
accountAPI.checkout();
|
||||
expect(context.axiosMock.post).toHaveBeenCalledWith(
|
||||
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||
'/enterprise/api/v1/checkout'
|
||||
);
|
||||
});
|
||||
|
||||
it('#subscription', () => {
|
||||
accountAPI.subscription();
|
||||
expect(context.axiosMock.post).toHaveBeenCalledWith(
|
||||
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||
'/enterprise/api/v1/subscription'
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/* global axios */
|
||||
|
||||
import PortalsAPI from './portals';
|
||||
import { getArticleSearchURL } from 'dashboard/helper/URLHelper.js';
|
||||
|
||||
class ArticlesAPI extends PortalsAPI {
|
||||
constructor() {
|
||||
@@ -12,14 +13,31 @@ class ArticlesAPI extends PortalsAPI {
|
||||
portalSlug,
|
||||
locale,
|
||||
status,
|
||||
author_id,
|
||||
category_slug,
|
||||
authorId,
|
||||
categorySlug,
|
||||
sort,
|
||||
}) {
|
||||
let baseUrl = `${this.url}/${portalSlug}/articles?page=${pageNumber}&locale=${locale}`;
|
||||
if (status !== undefined) baseUrl += `&status=${status}`;
|
||||
if (author_id) baseUrl += `&author_id=${author_id}`;
|
||||
if (category_slug) baseUrl += `&category_slug=${category_slug}`;
|
||||
return axios.get(baseUrl);
|
||||
const url = getArticleSearchURL({
|
||||
pageNumber,
|
||||
portalSlug,
|
||||
locale,
|
||||
status,
|
||||
authorId,
|
||||
categorySlug,
|
||||
sort,
|
||||
host: this.url,
|
||||
});
|
||||
|
||||
return axios.get(url);
|
||||
}
|
||||
|
||||
searchArticles({ portalSlug, query }) {
|
||||
const url = getArticleSearchURL({
|
||||
portalSlug,
|
||||
query,
|
||||
host: this.url,
|
||||
});
|
||||
return axios.get(url);
|
||||
}
|
||||
|
||||
getArticle({ id, portalSlug }) {
|
||||
@@ -47,20 +65,6 @@ class ArticlesAPI extends PortalsAPI {
|
||||
return axios.delete(`${this.url}/${portalSlug}/articles/${articleId}`);
|
||||
}
|
||||
|
||||
uploadImage({ portalSlug, file }) {
|
||||
let formData = new FormData();
|
||||
formData.append('background_image', file);
|
||||
return axios.post(
|
||||
`${this.url}/${portalSlug}/articles/attach_file`,
|
||||
formData,
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
reorderArticles({ portalSlug, reorderedGroup, categorySlug }) {
|
||||
return axios.post(`${this.url}/${portalSlug}/articles/reorder`, {
|
||||
positions_hash: reorderedGroup,
|
||||
|
||||
@@ -17,6 +17,10 @@ class PortalsAPI extends ApiClient {
|
||||
deletePortal(portalSlug) {
|
||||
return axios.delete(`${this.url}/${portalSlug}`);
|
||||
}
|
||||
|
||||
deleteLogo(portalSlug) {
|
||||
return axios.delete(`${this.url}/${portalSlug}/logo`);
|
||||
}
|
||||
}
|
||||
|
||||
export default PortalsAPI;
|
||||
|
||||
@@ -10,6 +10,7 @@ export const buildCreatePayload = ({
|
||||
files,
|
||||
ccEmails = '',
|
||||
bccEmails = '',
|
||||
toEmails = '',
|
||||
templateParams,
|
||||
}) => {
|
||||
let payload;
|
||||
@@ -25,6 +26,13 @@ export const buildCreatePayload = ({
|
||||
payload.append('echo_id', echoId);
|
||||
payload.append('cc_emails', ccEmails);
|
||||
payload.append('bcc_emails', bccEmails);
|
||||
|
||||
if (toEmails) {
|
||||
payload.append('to_emails', toEmails);
|
||||
}
|
||||
if (contentAttributes) {
|
||||
payload.append('content_attributes', JSON.stringify(contentAttributes));
|
||||
}
|
||||
} else {
|
||||
payload = {
|
||||
content: message,
|
||||
@@ -33,6 +41,7 @@ export const buildCreatePayload = ({
|
||||
content_attributes: contentAttributes,
|
||||
cc_emails: ccEmails,
|
||||
bcc_emails: bccEmails,
|
||||
to_emails: toEmails,
|
||||
template_params: templateParams,
|
||||
};
|
||||
}
|
||||
@@ -53,6 +62,7 @@ class MessageApi extends ApiClient {
|
||||
files,
|
||||
ccEmails = '',
|
||||
bccEmails = '',
|
||||
toEmails = '',
|
||||
templateParams,
|
||||
}) {
|
||||
return axios({
|
||||
@@ -66,6 +76,7 @@ class MessageApi extends ApiClient {
|
||||
files,
|
||||
ccEmails,
|
||||
bccEmails,
|
||||
toEmails,
|
||||
templateParams,
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -8,11 +8,19 @@ class IntegrationsAPI extends ApiClient {
|
||||
}
|
||||
|
||||
connectSlack(code) {
|
||||
return axios.post(`${this.baseUrl()}/integrations/slack`, {
|
||||
code: code,
|
||||
return axios.post(`${this.baseUrl()}/integrations/slack`, { code });
|
||||
}
|
||||
|
||||
updateSlack({ referenceId }) {
|
||||
return axios.patch(`${this.baseUrl()}/integrations/slack`, {
|
||||
reference_id: referenceId,
|
||||
});
|
||||
}
|
||||
|
||||
listAllSlackChannels() {
|
||||
return axios.get(`${this.baseUrl()}/integrations/slack/list_all_channels`);
|
||||
}
|
||||
|
||||
delete(integrationId) {
|
||||
return axios.delete(`${this.baseUrl()}/integrations/${integrationId}`);
|
||||
}
|
||||
|
||||
@@ -2,18 +2,62 @@
|
||||
|
||||
import ApiClient from '../ApiClient';
|
||||
|
||||
/**
|
||||
* Represents the data object for a OpenAI hook.
|
||||
* @typedef {Object} ConversationMessageData
|
||||
* @property {string} [tone] - The tone of the message.
|
||||
* @property {string} [content] - The content of the message.
|
||||
* @property {string} [conversation_display_id] - The display ID of the conversation (optional).
|
||||
*/
|
||||
|
||||
/**
|
||||
* A client for the OpenAI API.
|
||||
* @extends ApiClient
|
||||
*/
|
||||
class OpenAIAPI extends ApiClient {
|
||||
/**
|
||||
* Creates a new OpenAIAPI instance.
|
||||
*/
|
||||
constructor() {
|
||||
super('integrations', { accountScoped: true });
|
||||
|
||||
/**
|
||||
* The conversation events supported by the API.
|
||||
* @type {string[]}
|
||||
*/
|
||||
this.conversation_events = [
|
||||
'summarize',
|
||||
'reply_suggestion',
|
||||
'label_suggestion',
|
||||
];
|
||||
|
||||
/**
|
||||
* The message events supported by the API.
|
||||
* @type {string[]}
|
||||
*/
|
||||
this.message_events = ['rephrase'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes an event using the OpenAI API.
|
||||
* @param {Object} options - The options for the event.
|
||||
* @param {string} [options.type='rephrase'] - The type of event to process.
|
||||
* @param {string} [options.content] - The content of the event.
|
||||
* @param {string} [options.tone] - The tone of the event.
|
||||
* @param {string} [options.conversationId] - The ID of the conversation to process the event for.
|
||||
* @param {string} options.hookId - The ID of the hook to use for processing the event.
|
||||
* @returns {Promise} A promise that resolves with the result of the event processing.
|
||||
*/
|
||||
processEvent({ type = 'rephrase', content, tone, conversationId, hookId }) {
|
||||
/**
|
||||
* @type {ConversationMessageData}
|
||||
*/
|
||||
let data = {
|
||||
tone,
|
||||
content,
|
||||
};
|
||||
|
||||
if (type === 'reply_suggestion' || type === 'summarize') {
|
||||
if (this.conversation_events.includes(type)) {
|
||||
data = {
|
||||
conversation_display_id: conversationId,
|
||||
};
|
||||
|
||||
@@ -31,6 +31,7 @@ class ReportsAPI extends ApiClient {
|
||||
});
|
||||
}
|
||||
|
||||
// eslint-disable-next-line default-param-last
|
||||
getSummary(since, until, type = 'account', id, groupBy, businessHours) {
|
||||
return axios.get(`${this.url}/summary`, {
|
||||
params: {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import accountAPI from '../account';
|
||||
import ApiClient from '../ApiClient';
|
||||
import describeWithAPIMock from './apiSpecHelper';
|
||||
|
||||
describe('#accountAPI', () => {
|
||||
it('creates correct instance', () => {
|
||||
@@ -13,12 +12,28 @@ describe('#accountAPI', () => {
|
||||
expect(accountAPI).toHaveProperty('createAccount');
|
||||
});
|
||||
|
||||
describeWithAPIMock('API calls', context => {
|
||||
describe('API calls', () => {
|
||||
const originalAxios = window.axios;
|
||||
const axiosMock = {
|
||||
post: jest.fn(() => Promise.resolve()),
|
||||
get: jest.fn(() => Promise.resolve()),
|
||||
patch: jest.fn(() => Promise.resolve()),
|
||||
delete: jest.fn(() => Promise.resolve()),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
window.axios = axiosMock;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
window.axios = originalAxios;
|
||||
});
|
||||
|
||||
it('#createAccount', () => {
|
||||
accountAPI.createAccount({
|
||||
name: 'Chatwoot',
|
||||
});
|
||||
expect(context.axiosMock.post).toHaveBeenCalledWith('/api/v1/accounts', {
|
||||
expect(axiosMock.post).toHaveBeenCalledWith('/api/v1/accounts', {
|
||||
name: 'Chatwoot',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import accountActionsAPI from '../accountActions';
|
||||
import ApiClient from '../ApiClient';
|
||||
import describeWithAPIMock from './apiSpecHelper';
|
||||
|
||||
describe('#ContactsAPI', () => {
|
||||
it('creates correct instance', () => {
|
||||
@@ -8,10 +7,26 @@ describe('#ContactsAPI', () => {
|
||||
expect(accountActionsAPI).toHaveProperty('merge');
|
||||
});
|
||||
|
||||
describeWithAPIMock('API calls', context => {
|
||||
describe('API calls', () => {
|
||||
const originalAxios = window.axios;
|
||||
const axiosMock = {
|
||||
post: jest.fn(() => Promise.resolve()),
|
||||
get: jest.fn(() => Promise.resolve()),
|
||||
patch: jest.fn(() => Promise.resolve()),
|
||||
delete: jest.fn(() => Promise.resolve()),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
window.axios = axiosMock;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
window.axios = originalAxios;
|
||||
});
|
||||
|
||||
it('#merge', () => {
|
||||
accountActionsAPI.merge(1, 2);
|
||||
expect(context.axiosMock.post).toHaveBeenCalledWith(
|
||||
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||
'/api/v1/actions/contact_merge',
|
||||
{
|
||||
base_contact_id: 1,
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
function apiSpecHelper() {
|
||||
beforeEach(() => {
|
||||
this.originalAxios = window.axios;
|
||||
this.axiosMock = {
|
||||
post: jest.fn(() => Promise.resolve()),
|
||||
get: jest.fn(() => Promise.resolve()),
|
||||
patch: jest.fn(() => Promise.resolve()),
|
||||
delete: jest.fn(() => Promise.resolve()),
|
||||
};
|
||||
window.axios = this.axiosMock;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
window.axios = this.originalAxios;
|
||||
});
|
||||
}
|
||||
// https://stackoverflow.com/a/59344023/3901856
|
||||
const sharedWrapper = describe('sharedWrapper', () => {});
|
||||
export default function describeWithAPIMock(skillName, testFn) {
|
||||
return describe(skillName, function configureContext() {
|
||||
function Context() {}
|
||||
Context.prototype = sharedWrapper.ctx;
|
||||
this.ctx = new Context();
|
||||
apiSpecHelper.call(this);
|
||||
testFn.call(this, this);
|
||||
});
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import articlesAPI from '../helpCenter/articles';
|
||||
import ApiClient from 'dashboard/api/helpCenter/portals';
|
||||
import describeWithAPIMock from './apiSpecHelper';
|
||||
|
||||
describe('#PortalAPI', () => {
|
||||
it('creates correct instance', () => {
|
||||
@@ -12,39 +11,114 @@ describe('#PortalAPI', () => {
|
||||
expect(articlesAPI).toHaveProperty('delete');
|
||||
expect(articlesAPI).toHaveProperty('getArticles');
|
||||
});
|
||||
describeWithAPIMock('API calls', context => {
|
||||
describe('API calls', () => {
|
||||
const originalAxios = window.axios;
|
||||
const axiosMock = {
|
||||
post: jest.fn(() => Promise.resolve()),
|
||||
get: jest.fn(() => Promise.resolve()),
|
||||
patch: jest.fn(() => Promise.resolve()),
|
||||
delete: jest.fn(() => Promise.resolve()),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
window.axios = axiosMock;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
window.axios = originalAxios;
|
||||
});
|
||||
|
||||
it('#getArticles', () => {
|
||||
articlesAPI.getArticles({
|
||||
pageNumber: 1,
|
||||
portalSlug: 'room-rental',
|
||||
locale: 'en-US',
|
||||
status: 'published',
|
||||
author_id: '1',
|
||||
authorId: '1',
|
||||
});
|
||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||
expect(axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v1/portals/room-rental/articles?page=1&locale=en-US&status=published&author_id=1'
|
||||
);
|
||||
});
|
||||
});
|
||||
describeWithAPIMock('API calls', context => {
|
||||
describe('API calls', () => {
|
||||
const originalAxios = window.axios;
|
||||
const axiosMock = {
|
||||
post: jest.fn(() => Promise.resolve()),
|
||||
get: jest.fn(() => Promise.resolve()),
|
||||
patch: jest.fn(() => Promise.resolve()),
|
||||
delete: jest.fn(() => Promise.resolve()),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
window.axios = axiosMock;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
window.axios = originalAxios;
|
||||
});
|
||||
|
||||
it('#getArticle', () => {
|
||||
articlesAPI.getArticle({
|
||||
id: 1,
|
||||
portalSlug: 'room-rental',
|
||||
});
|
||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||
expect(axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v1/portals/room-rental/articles/1'
|
||||
);
|
||||
});
|
||||
});
|
||||
describeWithAPIMock('API calls', context => {
|
||||
describe('API calls', () => {
|
||||
const originalAxios = window.axios;
|
||||
const axiosMock = {
|
||||
post: jest.fn(() => Promise.resolve()),
|
||||
get: jest.fn(() => Promise.resolve()),
|
||||
patch: jest.fn(() => Promise.resolve()),
|
||||
delete: jest.fn(() => Promise.resolve()),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
window.axios = axiosMock;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
window.axios = originalAxios;
|
||||
});
|
||||
|
||||
it('#searchArticles', () => {
|
||||
articlesAPI.searchArticles({
|
||||
query: 'test',
|
||||
portalSlug: 'room-rental',
|
||||
});
|
||||
expect(axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v1/portals/room-rental/articles?query=test'
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('API calls', () => {
|
||||
const originalAxios = window.axios;
|
||||
const axiosMock = {
|
||||
post: jest.fn(() => Promise.resolve()),
|
||||
get: jest.fn(() => Promise.resolve()),
|
||||
patch: jest.fn(() => Promise.resolve()),
|
||||
delete: jest.fn(() => Promise.resolve()),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
window.axios = axiosMock;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
window.axios = originalAxios;
|
||||
});
|
||||
|
||||
it('#updateArticle', () => {
|
||||
articlesAPI.updateArticle({
|
||||
articleId: 1,
|
||||
portalSlug: 'room-rental',
|
||||
articleObj: { title: 'Update shipping address' },
|
||||
});
|
||||
expect(context.axiosMock.patch).toHaveBeenCalledWith(
|
||||
expect(axiosMock.patch).toHaveBeenCalledWith(
|
||||
'/api/v1/portals/room-rental/articles/1',
|
||||
{
|
||||
title: 'Update shipping address',
|
||||
@@ -52,13 +126,29 @@ describe('#PortalAPI', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
describeWithAPIMock('API calls', context => {
|
||||
describe('API calls', () => {
|
||||
const originalAxios = window.axios;
|
||||
const axiosMock = {
|
||||
post: jest.fn(() => Promise.resolve()),
|
||||
get: jest.fn(() => Promise.resolve()),
|
||||
patch: jest.fn(() => Promise.resolve()),
|
||||
delete: jest.fn(() => Promise.resolve()),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
window.axios = axiosMock;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
window.axios = originalAxios;
|
||||
});
|
||||
|
||||
it('#deleteArticle', () => {
|
||||
articlesAPI.deleteArticle({
|
||||
articleId: 1,
|
||||
portalSlug: 'room-rental',
|
||||
});
|
||||
expect(context.axiosMock.delete).toHaveBeenCalledWith(
|
||||
expect(axiosMock.delete).toHaveBeenCalledWith(
|
||||
'/api/v1/portals/room-rental/articles/1'
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,18 +1,30 @@
|
||||
import assignableAgentsAPI from '../assignableAgents';
|
||||
import describeWithAPIMock from './apiSpecHelper';
|
||||
|
||||
describe('#AssignableAgentsAPI', () => {
|
||||
describeWithAPIMock('API calls', context => {
|
||||
describe('API calls', () => {
|
||||
const originalAxios = window.axios;
|
||||
const axiosMock = {
|
||||
post: jest.fn(() => Promise.resolve()),
|
||||
get: jest.fn(() => Promise.resolve()),
|
||||
patch: jest.fn(() => Promise.resolve()),
|
||||
delete: jest.fn(() => Promise.resolve()),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
window.axios = axiosMock;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
window.axios = originalAxios;
|
||||
});
|
||||
|
||||
it('#getAssignableAgents', () => {
|
||||
assignableAgentsAPI.get([1]);
|
||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v1/assignable_agents',
|
||||
{
|
||||
params: {
|
||||
inbox_ids: [1],
|
||||
},
|
||||
}
|
||||
);
|
||||
expect(axiosMock.get).toHaveBeenCalledWith('/api/v1/assignable_agents', {
|
||||
params: {
|
||||
inbox_ids: [1],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import fbChannel from '../../channel/fbChannel';
|
||||
import ApiClient from '../../ApiClient';
|
||||
import describeWithAPIMock from '../apiSpecHelper';
|
||||
|
||||
describe('#FBChannel', () => {
|
||||
it('creates correct instance', () => {
|
||||
@@ -11,10 +10,26 @@ describe('#FBChannel', () => {
|
||||
expect(fbChannel).toHaveProperty('update');
|
||||
expect(fbChannel).toHaveProperty('delete');
|
||||
});
|
||||
describeWithAPIMock('API calls', context => {
|
||||
describe('API calls', () => {
|
||||
const originalAxios = window.axios;
|
||||
const axiosMock = {
|
||||
post: jest.fn(() => Promise.resolve()),
|
||||
get: jest.fn(() => Promise.resolve()),
|
||||
patch: jest.fn(() => Promise.resolve()),
|
||||
delete: jest.fn(() => Promise.resolve()),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
window.axios = axiosMock;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
window.axios = originalAxios;
|
||||
});
|
||||
|
||||
it('#create', () => {
|
||||
fbChannel.create({ omniauthToken: 'ASFM131CSF@#@$', appId: 'chatwoot' });
|
||||
expect(context.axiosMock.post).toHaveBeenCalledWith(
|
||||
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||
'/api/v1/callbacks/register_facebook_page',
|
||||
{
|
||||
omniauthToken: 'ASFM131CSF@#@$',
|
||||
@@ -27,7 +42,7 @@ describe('#FBChannel', () => {
|
||||
omniauthToken: 'ASFM131CSF@#@$',
|
||||
inboxId: 1,
|
||||
});
|
||||
expect(context.axiosMock.post).toHaveBeenCalledWith(
|
||||
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||
'/api/v1/callbacks/reauthorize_page',
|
||||
{
|
||||
omniauth_token: 'ASFM131CSF@#@$',
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import contactAPI, { buildContactParams } from '../contacts';
|
||||
import ApiClient from '../ApiClient';
|
||||
import describeWithAPIMock from './apiSpecHelper';
|
||||
|
||||
describe('#ContactsAPI', () => {
|
||||
it('creates correct instance', () => {
|
||||
@@ -15,56 +14,67 @@ describe('#ContactsAPI', () => {
|
||||
expect(contactAPI).toHaveProperty('destroyAvatar');
|
||||
});
|
||||
|
||||
describeWithAPIMock('API calls', context => {
|
||||
describe('API calls', () => {
|
||||
const originalAxios = window.axios;
|
||||
const axiosMock = {
|
||||
post: jest.fn(() => Promise.resolve()),
|
||||
get: jest.fn(() => Promise.resolve()),
|
||||
patch: jest.fn(() => Promise.resolve()),
|
||||
delete: jest.fn(() => Promise.resolve()),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
window.axios = axiosMock;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
window.axios = originalAxios;
|
||||
});
|
||||
|
||||
it('#get', () => {
|
||||
contactAPI.get(1, 'name', 'customer-support');
|
||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||
expect(axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v1/contacts?include_contact_inboxes=false&page=1&sort=name&labels[]=customer-support'
|
||||
);
|
||||
});
|
||||
|
||||
it('#getConversations', () => {
|
||||
contactAPI.getConversations(1);
|
||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||
expect(axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v1/contacts/1/conversations'
|
||||
);
|
||||
});
|
||||
|
||||
it('#getContactableInboxes', () => {
|
||||
contactAPI.getContactableInboxes(1);
|
||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||
expect(axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v1/contacts/1/contactable_inboxes'
|
||||
);
|
||||
});
|
||||
|
||||
it('#getContactLabels', () => {
|
||||
contactAPI.getContactLabels(1);
|
||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v1/contacts/1/labels'
|
||||
);
|
||||
expect(axiosMock.get).toHaveBeenCalledWith('/api/v1/contacts/1/labels');
|
||||
});
|
||||
|
||||
it('#updateContactLabels', () => {
|
||||
const labels = ['support-query'];
|
||||
contactAPI.updateContactLabels(1, labels);
|
||||
expect(context.axiosMock.post).toHaveBeenCalledWith(
|
||||
'/api/v1/contacts/1/labels',
|
||||
{
|
||||
labels,
|
||||
}
|
||||
);
|
||||
expect(axiosMock.post).toHaveBeenCalledWith('/api/v1/contacts/1/labels', {
|
||||
labels,
|
||||
});
|
||||
});
|
||||
|
||||
it('#search', () => {
|
||||
contactAPI.search('leads', 1, 'date', 'customer-support');
|
||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||
expect(axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v1/contacts/search?include_contact_inboxes=false&page=1&sort=date&q=leads&labels[]=customer-support'
|
||||
);
|
||||
});
|
||||
|
||||
it('#destroyCustomAttributes', () => {
|
||||
contactAPI.destroyCustomAttributes(1, ['cloudCustomer']);
|
||||
expect(context.axiosMock.post).toHaveBeenCalledWith(
|
||||
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||
'/api/v1/contacts/1/destroy_custom_attributes',
|
||||
{
|
||||
custom_attributes: ['cloudCustomer'],
|
||||
@@ -75,7 +85,7 @@ describe('#ContactsAPI', () => {
|
||||
it('#importContacts', () => {
|
||||
const file = 'file';
|
||||
contactAPI.importContacts(file);
|
||||
expect(context.axiosMock.post).toHaveBeenCalledWith(
|
||||
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||
'/api/v1/contacts/import',
|
||||
expect.any(FormData),
|
||||
{
|
||||
@@ -96,7 +106,7 @@ describe('#ContactsAPI', () => {
|
||||
],
|
||||
};
|
||||
contactAPI.filter(1, 'name', queryPayload);
|
||||
expect(context.axiosMock.post).toHaveBeenCalledWith(
|
||||
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||
'/api/v1/contacts/filter?include_contact_inboxes=false&page=1&sort=name',
|
||||
queryPayload
|
||||
);
|
||||
@@ -104,7 +114,7 @@ describe('#ContactsAPI', () => {
|
||||
|
||||
it('#destroyAvatar', () => {
|
||||
contactAPI.destroyAvatar(1);
|
||||
expect(context.axiosMock.delete).toHaveBeenCalledWith(
|
||||
expect(axiosMock.delete).toHaveBeenCalledWith(
|
||||
'/api/v1/contacts/1/avatar'
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import conversationsAPI from '../conversations';
|
||||
import ApiClient from '../ApiClient';
|
||||
import describeWithAPIMock from './apiSpecHelper';
|
||||
|
||||
describe('#ConversationApi', () => {
|
||||
it('creates correct instance', () => {
|
||||
@@ -14,10 +13,26 @@ describe('#ConversationApi', () => {
|
||||
expect(conversationsAPI).toHaveProperty('updateLabels');
|
||||
});
|
||||
|
||||
describeWithAPIMock('API calls', context => {
|
||||
describe('API calls', () => {
|
||||
const originalAxios = window.axios;
|
||||
const axiosMock = {
|
||||
post: jest.fn(() => Promise.resolve()),
|
||||
get: jest.fn(() => Promise.resolve()),
|
||||
patch: jest.fn(() => Promise.resolve()),
|
||||
delete: jest.fn(() => Promise.resolve()),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
window.axios = axiosMock;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
window.axios = originalAxios;
|
||||
});
|
||||
|
||||
it('#getLabels', () => {
|
||||
conversationsAPI.getLabels(1);
|
||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||
expect(axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v1/conversations/1/labels'
|
||||
);
|
||||
});
|
||||
@@ -25,7 +40,7 @@ describe('#ConversationApi', () => {
|
||||
it('#updateLabels', () => {
|
||||
const labels = ['support-query'];
|
||||
conversationsAPI.updateLabels(1, labels);
|
||||
expect(context.axiosMock.post).toHaveBeenCalledWith(
|
||||
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||
'/api/v1/conversations/1/labels',
|
||||
{
|
||||
labels,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import csatReportsAPI from '../csatReports';
|
||||
import ApiClient from '../ApiClient';
|
||||
import describeWithAPIMock from './apiSpecHelper';
|
||||
|
||||
describe('#Reports API', () => {
|
||||
it('creates correct instance', () => {
|
||||
@@ -9,10 +8,26 @@ describe('#Reports API', () => {
|
||||
expect(csatReportsAPI).toHaveProperty('get');
|
||||
expect(csatReportsAPI).toHaveProperty('getMetrics');
|
||||
});
|
||||
describeWithAPIMock('API calls', context => {
|
||||
describe('API calls', () => {
|
||||
const originalAxios = window.axios;
|
||||
const axiosMock = {
|
||||
post: jest.fn(() => Promise.resolve()),
|
||||
get: jest.fn(() => Promise.resolve()),
|
||||
patch: jest.fn(() => Promise.resolve()),
|
||||
delete: jest.fn(() => Promise.resolve()),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
window.axios = axiosMock;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
window.axios = originalAxios;
|
||||
});
|
||||
|
||||
it('#get', () => {
|
||||
csatReportsAPI.get({ page: 1, from: 1622485800, to: 1623695400 });
|
||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||
expect(axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v1/csat_survey_responses',
|
||||
{
|
||||
params: {
|
||||
@@ -26,7 +41,7 @@ describe('#Reports API', () => {
|
||||
});
|
||||
it('#getMetrics', () => {
|
||||
csatReportsAPI.getMetrics({ from: 1622485800, to: 1623695400 });
|
||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||
expect(axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v1/csat_survey_responses/metrics',
|
||||
{
|
||||
params: { since: 1622485800, until: 1623695400 },
|
||||
@@ -39,7 +54,7 @@ describe('#Reports API', () => {
|
||||
to: 1623695400,
|
||||
user_ids: 1,
|
||||
});
|
||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||
expect(axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v1/csat_survey_responses/download',
|
||||
{
|
||||
params: {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import conversationAPI from '../../inbox/conversation';
|
||||
import ApiClient from '../../ApiClient';
|
||||
import describeWithAPIMock from '../apiSpecHelper';
|
||||
|
||||
describe('#ConversationAPI', () => {
|
||||
it('creates correct instance', () => {
|
||||
@@ -22,7 +21,23 @@ describe('#ConversationAPI', () => {
|
||||
expect(conversationAPI).toHaveProperty('filter');
|
||||
});
|
||||
|
||||
describeWithAPIMock('API calls', context => {
|
||||
describe('API calls', () => {
|
||||
const originalAxios = window.axios;
|
||||
const axiosMock = {
|
||||
post: jest.fn(() => Promise.resolve()),
|
||||
get: jest.fn(() => Promise.resolve()),
|
||||
patch: jest.fn(() => Promise.resolve()),
|
||||
delete: jest.fn(() => Promise.resolve()),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
window.axios = axiosMock;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
window.axios = originalAxios;
|
||||
});
|
||||
|
||||
it('#get conversations', () => {
|
||||
conversationAPI.get({
|
||||
inboxId: 1,
|
||||
@@ -32,19 +47,16 @@ describe('#ConversationAPI', () => {
|
||||
labels: [],
|
||||
teamId: 1,
|
||||
});
|
||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v1/conversations',
|
||||
{
|
||||
params: {
|
||||
inbox_id: 1,
|
||||
team_id: 1,
|
||||
status: 'open',
|
||||
assignee_type: 'me',
|
||||
page: 1,
|
||||
labels: [],
|
||||
},
|
||||
}
|
||||
);
|
||||
expect(axiosMock.get).toHaveBeenCalledWith('/api/v1/conversations', {
|
||||
params: {
|
||||
inbox_id: 1,
|
||||
team_id: 1,
|
||||
status: 'open',
|
||||
assignee_type: 'me',
|
||||
page: 1,
|
||||
labels: [],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('#search', () => {
|
||||
@@ -53,7 +65,7 @@ describe('#ConversationAPI', () => {
|
||||
page: 1,
|
||||
});
|
||||
|
||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||
expect(axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v1/conversations/search',
|
||||
{
|
||||
params: {
|
||||
@@ -66,7 +78,7 @@ describe('#ConversationAPI', () => {
|
||||
|
||||
it('#toggleStatus', () => {
|
||||
conversationAPI.toggleStatus({ conversationId: 12, status: 'online' });
|
||||
expect(context.axiosMock.post).toHaveBeenCalledWith(
|
||||
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||
`/api/v1/conversations/12/toggle_status`,
|
||||
{
|
||||
status: 'online',
|
||||
@@ -77,7 +89,7 @@ describe('#ConversationAPI', () => {
|
||||
|
||||
it('#assignAgent', () => {
|
||||
conversationAPI.assignAgent({ conversationId: 12, agentId: 34 });
|
||||
expect(context.axiosMock.post).toHaveBeenCalledWith(
|
||||
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||
`/api/v1/conversations/12/assignments?assignee_id=34`,
|
||||
{}
|
||||
);
|
||||
@@ -85,7 +97,7 @@ describe('#ConversationAPI', () => {
|
||||
|
||||
it('#assignTeam', () => {
|
||||
conversationAPI.assignTeam({ conversationId: 12, teamId: 1 });
|
||||
expect(context.axiosMock.post).toHaveBeenCalledWith(
|
||||
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||
`/api/v1/conversations/12/assignments`,
|
||||
{
|
||||
team_id: 1,
|
||||
@@ -95,7 +107,7 @@ describe('#ConversationAPI', () => {
|
||||
|
||||
it('#markMessageRead', () => {
|
||||
conversationAPI.markMessageRead({ id: 12 });
|
||||
expect(context.axiosMock.post).toHaveBeenCalledWith(
|
||||
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||
`/api/v1/conversations/12/update_last_seen`
|
||||
);
|
||||
});
|
||||
@@ -105,7 +117,7 @@ describe('#ConversationAPI', () => {
|
||||
conversationId: 12,
|
||||
status: 'typing_on',
|
||||
});
|
||||
expect(context.axiosMock.post).toHaveBeenCalledWith(
|
||||
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||
`/api/v1/conversations/12/toggle_typing_status`,
|
||||
{
|
||||
typing_status: 'typing_on',
|
||||
@@ -115,14 +127,14 @@ describe('#ConversationAPI', () => {
|
||||
|
||||
it('#mute', () => {
|
||||
conversationAPI.mute(45);
|
||||
expect(context.axiosMock.post).toHaveBeenCalledWith(
|
||||
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||
'/api/v1/conversations/45/mute'
|
||||
);
|
||||
});
|
||||
|
||||
it('#unmute', () => {
|
||||
conversationAPI.unmute(45);
|
||||
expect(context.axiosMock.post).toHaveBeenCalledWith(
|
||||
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||
'/api/v1/conversations/45/unmute'
|
||||
);
|
||||
});
|
||||
@@ -135,18 +147,15 @@ describe('#ConversationAPI', () => {
|
||||
labels: [],
|
||||
teamId: 1,
|
||||
});
|
||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v1/conversations/meta',
|
||||
{
|
||||
params: {
|
||||
inbox_id: 1,
|
||||
team_id: 1,
|
||||
status: 'open',
|
||||
assignee_type: 'me',
|
||||
labels: [],
|
||||
},
|
||||
}
|
||||
);
|
||||
expect(axiosMock.get).toHaveBeenCalledWith('/api/v1/conversations/meta', {
|
||||
params: {
|
||||
inbox_id: 1,
|
||||
team_id: 1,
|
||||
status: 'open',
|
||||
assignee_type: 'me',
|
||||
labels: [],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('#sendEmailTranscript', () => {
|
||||
@@ -154,7 +163,7 @@ describe('#ConversationAPI', () => {
|
||||
conversationId: 45,
|
||||
email: 'john@acme.inc',
|
||||
});
|
||||
expect(context.axiosMock.post).toHaveBeenCalledWith(
|
||||
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||
'/api/v1/conversations/45/transcript',
|
||||
{
|
||||
email: 'john@acme.inc',
|
||||
@@ -167,7 +176,7 @@ describe('#ConversationAPI', () => {
|
||||
conversationId: 45,
|
||||
customAttributes: { order_d: '1001' },
|
||||
});
|
||||
expect(context.axiosMock.post).toHaveBeenCalledWith(
|
||||
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||
'/api/v1/conversations/45/custom_attributes',
|
||||
{
|
||||
custom_attributes: { order_d: '1001' },
|
||||
@@ -202,9 +211,7 @@ describe('#ConversationAPI', () => {
|
||||
},
|
||||
};
|
||||
conversationAPI.filter(payload);
|
||||
expect(
|
||||
context.axiosMock.post
|
||||
).toHaveBeenCalledWith(
|
||||
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||
'/api/v1/conversations/filter',
|
||||
payload.queryData,
|
||||
{ params: { page: payload.page } }
|
||||
@@ -213,7 +220,7 @@ describe('#ConversationAPI', () => {
|
||||
|
||||
it('#getAllAttachments', () => {
|
||||
conversationAPI.getAllAttachments(1);
|
||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||
expect(axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v1/conversations/1/attachments'
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import messageAPI, { buildCreatePayload } from '../../inbox/message';
|
||||
import ApiClient from '../../ApiClient';
|
||||
import describeWithAPIMock from '../apiSpecHelper';
|
||||
|
||||
describe('#ConversationAPI', () => {
|
||||
it('creates correct instance', () => {
|
||||
@@ -13,13 +12,29 @@ describe('#ConversationAPI', () => {
|
||||
expect(messageAPI).toHaveProperty('getPreviousMessages');
|
||||
});
|
||||
|
||||
describeWithAPIMock('API calls', context => {
|
||||
describe('API calls', () => {
|
||||
const originalAxios = window.axios;
|
||||
const axiosMock = {
|
||||
post: jest.fn(() => Promise.resolve()),
|
||||
get: jest.fn(() => Promise.resolve()),
|
||||
patch: jest.fn(() => Promise.resolve()),
|
||||
delete: jest.fn(() => Promise.resolve()),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
window.axios = axiosMock;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
window.axios = originalAxios;
|
||||
});
|
||||
|
||||
it('#getPreviousMessages', () => {
|
||||
messageAPI.getPreviousMessages({
|
||||
conversationId: 12,
|
||||
before: 4573,
|
||||
});
|
||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||
expect(axiosMock.get).toHaveBeenCalledWith(
|
||||
`/api/v1/conversations/12/messages`,
|
||||
{
|
||||
params: {
|
||||
@@ -35,7 +50,7 @@ describe('#ConversationAPI', () => {
|
||||
message: 'test content',
|
||||
echoId: 12,
|
||||
isPrivate: true,
|
||||
|
||||
contentAttributes: { in_reply_to: 12 },
|
||||
files: [new Blob(['test-content'], { type: 'application/pdf' })],
|
||||
});
|
||||
expect(formPayload).toBeInstanceOf(FormData);
|
||||
@@ -43,6 +58,10 @@ describe('#ConversationAPI', () => {
|
||||
expect(formPayload.get('echo_id')).toEqual('12');
|
||||
expect(formPayload.get('private')).toEqual('true');
|
||||
expect(formPayload.get('cc_emails')).toEqual('');
|
||||
expect(formPayload.get('bcc_emails')).toEqual('');
|
||||
expect(formPayload.get('content_attributes')).toEqual(
|
||||
'{"in_reply_to":12}'
|
||||
);
|
||||
});
|
||||
|
||||
it('builds object payload if file is not available', () => {
|
||||
@@ -58,8 +77,10 @@ describe('#ConversationAPI', () => {
|
||||
private: false,
|
||||
echo_id: 12,
|
||||
content_attributes: { in_reply_to: 12 },
|
||||
bcc_emails: '',
|
||||
cc_emails: '',
|
||||
bcc_emails: '',
|
||||
to_emails: '',
|
||||
template_params: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import inboxesAPI from '../inboxes';
|
||||
import ApiClient from '../ApiClient';
|
||||
import describeWithAPIMock from './apiSpecHelper';
|
||||
|
||||
describe('#InboxesAPI', () => {
|
||||
it('creates correct instance', () => {
|
||||
@@ -14,19 +13,32 @@ describe('#InboxesAPI', () => {
|
||||
expect(inboxesAPI).toHaveProperty('getAgentBot');
|
||||
expect(inboxesAPI).toHaveProperty('setAgentBot');
|
||||
});
|
||||
describeWithAPIMock('API calls', context => {
|
||||
|
||||
describe('API calls', () => {
|
||||
const originalAxios = window.axios;
|
||||
const axiosMock = {
|
||||
post: jest.fn(() => Promise.resolve()),
|
||||
get: jest.fn(() => Promise.resolve()),
|
||||
patch: jest.fn(() => Promise.resolve()),
|
||||
delete: jest.fn(() => Promise.resolve()),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
window.axios = axiosMock;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
window.axios = originalAxios;
|
||||
});
|
||||
|
||||
it('#getCampaigns', () => {
|
||||
inboxesAPI.getCampaigns(2);
|
||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v1/inboxes/2/campaigns'
|
||||
);
|
||||
expect(axiosMock.get).toHaveBeenCalledWith('/api/v1/inboxes/2/campaigns');
|
||||
});
|
||||
|
||||
it('#deleteInboxAvatar', () => {
|
||||
inboxesAPI.deleteInboxAvatar(2);
|
||||
expect(context.axiosMock.delete).toHaveBeenCalledWith(
|
||||
'/api/v1/inboxes/2/avatar'
|
||||
);
|
||||
expect(axiosMock.delete).toHaveBeenCalledWith('/api/v1/inboxes/2/avatar');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import integrationAPI from '../integrations';
|
||||
import ApiClient from '../ApiClient';
|
||||
import describeWithAPIMock from './apiSpecHelper';
|
||||
|
||||
describe('#integrationAPI', () => {
|
||||
it('creates correct instance', () => {
|
||||
@@ -11,14 +10,32 @@ describe('#integrationAPI', () => {
|
||||
expect(integrationAPI).toHaveProperty('update');
|
||||
expect(integrationAPI).toHaveProperty('delete');
|
||||
expect(integrationAPI).toHaveProperty('connectSlack');
|
||||
expect(integrationAPI).toHaveProperty('createHook');
|
||||
expect(integrationAPI).toHaveProperty('updateSlack');
|
||||
expect(integrationAPI).toHaveProperty('updateSlack');
|
||||
expect(integrationAPI).toHaveProperty('listAllSlackChannels');
|
||||
expect(integrationAPI).toHaveProperty('deleteHook');
|
||||
});
|
||||
describeWithAPIMock('API calls', context => {
|
||||
describe('API calls', () => {
|
||||
const originalAxios = window.axios;
|
||||
const axiosMock = {
|
||||
post: jest.fn(() => Promise.resolve()),
|
||||
get: jest.fn(() => Promise.resolve()),
|
||||
patch: jest.fn(() => Promise.resolve()),
|
||||
delete: jest.fn(() => Promise.resolve()),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
window.axios = axiosMock;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
window.axios = originalAxios;
|
||||
});
|
||||
|
||||
it('#connectSlack', () => {
|
||||
const code = 'SDNFJNSDFNDSJN';
|
||||
integrationAPI.connectSlack(code);
|
||||
expect(context.axiosMock.post).toHaveBeenCalledWith(
|
||||
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||
'/api/v1/integrations/slack',
|
||||
{
|
||||
code,
|
||||
@@ -26,11 +43,27 @@ describe('#integrationAPI', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('#updateSlack', () => {
|
||||
const updateObj = { referenceId: 'SDFSDGSVE' };
|
||||
integrationAPI.updateSlack(updateObj);
|
||||
expect(axiosMock.patch).toHaveBeenCalledWith(
|
||||
'/api/v1/integrations/slack',
|
||||
{
|
||||
reference_id: updateObj.referenceId,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('#listAllSlackChannels', () => {
|
||||
integrationAPI.listAllSlackChannels();
|
||||
expect(axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v1/integrations/slack/list_all_channels'
|
||||
);
|
||||
});
|
||||
|
||||
it('#delete', () => {
|
||||
integrationAPI.delete(2);
|
||||
expect(context.axiosMock.delete).toHaveBeenCalledWith(
|
||||
'/api/v1/integrations/2'
|
||||
);
|
||||
expect(axiosMock.delete).toHaveBeenCalledWith('/api/v1/integrations/2');
|
||||
});
|
||||
|
||||
it('#createHook', () => {
|
||||
@@ -39,7 +72,7 @@ describe('#integrationAPI', () => {
|
||||
settings: { api_key: 'SDFSDGSVE' },
|
||||
};
|
||||
integrationAPI.createHook(hookData);
|
||||
expect(context.axiosMock.post).toHaveBeenCalledWith(
|
||||
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||
'/api/v1/integrations/hooks',
|
||||
hookData
|
||||
);
|
||||
@@ -47,7 +80,7 @@ describe('#integrationAPI', () => {
|
||||
|
||||
it('#deleteHook', () => {
|
||||
integrationAPI.deleteHook(2);
|
||||
expect(context.axiosMock.delete).toHaveBeenCalledWith(
|
||||
expect(axiosMock.delete).toHaveBeenCalledWith(
|
||||
'/api/v1/integrations/hooks/2'
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import DyteAPIClient from '../../integrations/dyte';
|
||||
import ApiClient from '../../ApiClient';
|
||||
import describeWithAPIMock from '../apiSpecHelper';
|
||||
|
||||
describe('#accountAPI', () => {
|
||||
it('creates correct instance', () => {
|
||||
@@ -9,10 +8,26 @@ describe('#accountAPI', () => {
|
||||
expect(DyteAPIClient).toHaveProperty('addParticipantToMeeting');
|
||||
});
|
||||
|
||||
describeWithAPIMock('createAMeeting', context => {
|
||||
describe('createAMeeting', () => {
|
||||
const originalAxios = window.axios;
|
||||
const axiosMock = {
|
||||
post: jest.fn(() => Promise.resolve()),
|
||||
get: jest.fn(() => Promise.resolve()),
|
||||
patch: jest.fn(() => Promise.resolve()),
|
||||
delete: jest.fn(() => Promise.resolve()),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
window.axios = axiosMock;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
window.axios = originalAxios;
|
||||
});
|
||||
|
||||
it('creates a valid request', () => {
|
||||
DyteAPIClient.createAMeeting(1);
|
||||
expect(context.axiosMock.post).toHaveBeenCalledWith(
|
||||
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||
'/api/v1/integrations/dyte/create_a_meeting',
|
||||
{
|
||||
conversation_id: 1,
|
||||
@@ -21,10 +36,26 @@ describe('#accountAPI', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describeWithAPIMock('addParticipantToMeeting', context => {
|
||||
describe('addParticipantToMeeting', () => {
|
||||
const originalAxios = window.axios;
|
||||
const axiosMock = {
|
||||
post: jest.fn(() => Promise.resolve()),
|
||||
get: jest.fn(() => Promise.resolve()),
|
||||
patch: jest.fn(() => Promise.resolve()),
|
||||
delete: jest.fn(() => Promise.resolve()),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
window.axios = axiosMock;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
window.axios = originalAxios;
|
||||
});
|
||||
|
||||
it('creates a valid request', () => {
|
||||
DyteAPIClient.addParticipantToMeeting(1);
|
||||
expect(context.axiosMock.post).toHaveBeenCalledWith(
|
||||
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||
'/api/v1/integrations/dyte/add_participant_to_meeting',
|
||||
{
|
||||
message_id: 1,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import notificationsAPI from '../notifications';
|
||||
import ApiClient from '../ApiClient';
|
||||
import describeWithAPIMock from './apiSpecHelper';
|
||||
|
||||
describe('#NotificationAPI', () => {
|
||||
it('creates correct instance', () => {
|
||||
@@ -11,31 +10,47 @@ describe('#NotificationAPI', () => {
|
||||
expect(notificationsAPI).toHaveProperty('read');
|
||||
expect(notificationsAPI).toHaveProperty('readAll');
|
||||
});
|
||||
describeWithAPIMock('API calls', context => {
|
||||
describe('API calls', () => {
|
||||
const originalAxios = window.axios;
|
||||
const axiosMock = {
|
||||
post: jest.fn(() => Promise.resolve()),
|
||||
get: jest.fn(() => Promise.resolve()),
|
||||
patch: jest.fn(() => Promise.resolve()),
|
||||
delete: jest.fn(() => Promise.resolve()),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
window.axios = axiosMock;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
window.axios = originalAxios;
|
||||
});
|
||||
|
||||
it('#get', () => {
|
||||
notificationsAPI.get(1);
|
||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||
expect(axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v1/notifications?page=1'
|
||||
);
|
||||
});
|
||||
|
||||
it('#getNotifications', () => {
|
||||
notificationsAPI.getNotifications(1);
|
||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||
expect(axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v1/notifications/1/notifications'
|
||||
);
|
||||
});
|
||||
|
||||
it('#getUnreadCount', () => {
|
||||
notificationsAPI.getUnreadCount();
|
||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||
expect(axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v1/notifications/unread_count'
|
||||
);
|
||||
});
|
||||
|
||||
it('#read', () => {
|
||||
notificationsAPI.read(48670, 'Conversation');
|
||||
expect(context.axiosMock.post).toHaveBeenCalledWith(
|
||||
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||
'/api/v1/notifications/read_all',
|
||||
{
|
||||
primary_actor_id: 'Conversation',
|
||||
@@ -46,7 +61,7 @@ describe('#NotificationAPI', () => {
|
||||
|
||||
it('#readAll', () => {
|
||||
notificationsAPI.readAll();
|
||||
expect(context.axiosMock.post).toHaveBeenCalledWith(
|
||||
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||
'/api/v1/notifications/read_all'
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import reportsAPI from '../reports';
|
||||
import ApiClient from '../ApiClient';
|
||||
import describeWithAPIMock from './apiSpecHelper';
|
||||
|
||||
describe('#Reports API', () => {
|
||||
it('creates correct instance', () => {
|
||||
@@ -18,14 +17,30 @@ describe('#Reports API', () => {
|
||||
expect(reportsAPI).toHaveProperty('getInboxReports');
|
||||
expect(reportsAPI).toHaveProperty('getTeamReports');
|
||||
});
|
||||
describeWithAPIMock('API calls', context => {
|
||||
describe('API calls', () => {
|
||||
const originalAxios = window.axios;
|
||||
const axiosMock = {
|
||||
post: jest.fn(() => Promise.resolve()),
|
||||
get: jest.fn(() => Promise.resolve()),
|
||||
patch: jest.fn(() => Promise.resolve()),
|
||||
delete: jest.fn(() => Promise.resolve()),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
window.axios = axiosMock;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
window.axios = originalAxios;
|
||||
});
|
||||
|
||||
it('#getAccountReports', () => {
|
||||
reportsAPI.getReports({
|
||||
metric: 'conversations_count',
|
||||
from: 1621103400,
|
||||
to: 1621621800,
|
||||
});
|
||||
expect(context.axiosMock.get).toHaveBeenCalledWith('/api/v2/reports', {
|
||||
expect(axiosMock.get).toHaveBeenCalledWith('/api/v2/reports', {
|
||||
params: {
|
||||
metric: 'conversations_count',
|
||||
since: 1621103400,
|
||||
@@ -38,20 +53,17 @@ describe('#Reports API', () => {
|
||||
|
||||
it('#getAccountSummary', () => {
|
||||
reportsAPI.getSummary(1621103400, 1621621800);
|
||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v2/reports/summary',
|
||||
{
|
||||
params: {
|
||||
business_hours: undefined,
|
||||
group_by: undefined,
|
||||
id: undefined,
|
||||
since: 1621103400,
|
||||
timezone_offset: -0,
|
||||
type: 'account',
|
||||
until: 1621621800,
|
||||
},
|
||||
}
|
||||
);
|
||||
expect(axiosMock.get).toHaveBeenCalledWith('/api/v2/reports/summary', {
|
||||
params: {
|
||||
business_hours: undefined,
|
||||
group_by: undefined,
|
||||
id: undefined,
|
||||
since: 1621103400,
|
||||
timezone_offset: -0,
|
||||
type: 'account',
|
||||
until: 1621621800,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('#getAgentReports', () => {
|
||||
@@ -60,60 +72,48 @@ describe('#Reports API', () => {
|
||||
to: 1621621800,
|
||||
businessHours: true,
|
||||
});
|
||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v2/reports/agents',
|
||||
{
|
||||
params: {
|
||||
since: 1621103400,
|
||||
until: 1621621800,
|
||||
business_hours: true,
|
||||
},
|
||||
}
|
||||
);
|
||||
expect(axiosMock.get).toHaveBeenCalledWith('/api/v2/reports/agents', {
|
||||
params: {
|
||||
since: 1621103400,
|
||||
until: 1621621800,
|
||||
business_hours: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('#getLabelReports', () => {
|
||||
reportsAPI.getLabelReports({ from: 1621103400, to: 1621621800 });
|
||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v2/reports/labels',
|
||||
{
|
||||
params: {
|
||||
since: 1621103400,
|
||||
until: 1621621800,
|
||||
},
|
||||
}
|
||||
);
|
||||
expect(axiosMock.get).toHaveBeenCalledWith('/api/v2/reports/labels', {
|
||||
params: {
|
||||
since: 1621103400,
|
||||
until: 1621621800,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('#getInboxReports', () => {
|
||||
reportsAPI.getInboxReports({ from: 1621103400, to: 1621621800 });
|
||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v2/reports/inboxes',
|
||||
{
|
||||
params: {
|
||||
since: 1621103400,
|
||||
until: 1621621800,
|
||||
},
|
||||
}
|
||||
);
|
||||
expect(axiosMock.get).toHaveBeenCalledWith('/api/v2/reports/inboxes', {
|
||||
params: {
|
||||
since: 1621103400,
|
||||
until: 1621621800,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('#getTeamReports', () => {
|
||||
reportsAPI.getTeamReports({ from: 1621103400, to: 1621621800 });
|
||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v2/reports/teams',
|
||||
{
|
||||
params: {
|
||||
since: 1621103400,
|
||||
until: 1621621800,
|
||||
},
|
||||
}
|
||||
);
|
||||
expect(axiosMock.get).toHaveBeenCalledWith('/api/v2/reports/teams', {
|
||||
params: {
|
||||
since: 1621103400,
|
||||
until: 1621621800,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('#getConversationMetric', () => {
|
||||
reportsAPI.getConversationMetric('account');
|
||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||
expect(axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v2/reports/conversations',
|
||||
{
|
||||
params: {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import teamsAPI from '../teams';
|
||||
import ApiClient from '../ApiClient';
|
||||
import describeWithAPIMock from './apiSpecHelper';
|
||||
|
||||
describe('#TeamsAPI', () => {
|
||||
it('creates correct instance', () => {
|
||||
@@ -14,17 +13,33 @@ describe('#TeamsAPI', () => {
|
||||
expect(teamsAPI).toHaveProperty('addAgents');
|
||||
expect(teamsAPI).toHaveProperty('updateAgents');
|
||||
});
|
||||
describeWithAPIMock('API calls', context => {
|
||||
describe('API calls', () => {
|
||||
const originalAxios = window.axios;
|
||||
const axiosMock = {
|
||||
post: jest.fn(() => Promise.resolve()),
|
||||
get: jest.fn(() => Promise.resolve()),
|
||||
patch: jest.fn(() => Promise.resolve()),
|
||||
delete: jest.fn(() => Promise.resolve()),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
window.axios = axiosMock;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
window.axios = originalAxios;
|
||||
});
|
||||
|
||||
it('#getAgents', () => {
|
||||
teamsAPI.getAgents({ teamId: 1 });
|
||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||
expect(axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v1/teams/1/team_members'
|
||||
);
|
||||
});
|
||||
|
||||
it('#addAgents', () => {
|
||||
teamsAPI.addAgents({ teamId: 1, agentsList: { user_ids: [1, 10, 21] } });
|
||||
expect(context.axiosMock.post).toHaveBeenCalledWith(
|
||||
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||
'/api/v1/teams/1/team_members',
|
||||
{
|
||||
user_ids: { user_ids: [1, 10, 21] },
|
||||
@@ -38,7 +53,7 @@ describe('#TeamsAPI', () => {
|
||||
teamId: 1,
|
||||
agentsList,
|
||||
});
|
||||
expect(context.axiosMock.patch).toHaveBeenCalledWith(
|
||||
expect(axiosMock.patch).toHaveBeenCalledWith(
|
||||
'/api/v1/teams/1/team_members',
|
||||
{
|
||||
user_ids: agentsList,
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
<svg width="495" height="384" viewBox="0 0 495 384" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="No Chat History">
|
||||
<path id="Fill 1" fill-rule="evenodd" clip-rule="evenodd" d="M266.903 0C221.687 0 180.128 15.7196 147.313 42.0198H94.2771C87.3866 42.0198 81.749 47.6925 81.749 54.6257C81.749 61.559 87.3866 67.2317 94.2771 67.2317H121.268H129.178C136.068 67.2317 141.706 72.9043 141.706 79.8376C141.706 83.3 140.294 86.4599 138.023 88.7374C135.759 91.0233 132.619 92.4435 129.178 92.4435H112.303H102.986H57.5281C50.6376 92.4435 45 98.1162 45 105.049C45 111.983 50.6376 117.655 57.5281 117.655H90.071C80.2473 140.886 74.8059 166.446 74.8059 193.291C74.8059 203.031 75.5242 212.603 76.9023 221.957C77.4786 225.865 78.1718 229.739 78.982 233.571C102.618 229.932 127.691 227.008 153.866 224.873L155.837 226.68L157.692 224.571C189.137 222.108 222.144 220.797 256.187 220.797C328.516 220.797 396.201 226.714 454.072 237C456.302 227.268 457.806 217.259 458.516 207.023C458.841 202.485 459 197.905 459 193.291C459 86.5356 372.999 0 266.903 0Z" fill="#43484C"/>
|
||||
<path id="Fill 3" fill-rule="evenodd" clip-rule="evenodd" d="M20.2465 91.4282H12.6678C5.7005 91.4282 0 97.1882 0 104.228C0 111.268 5.7005 117.028 12.6678 117.028H20.2465C27.2137 117.028 32.9142 111.268 32.9142 104.228C32.9142 97.1882 27.2137 91.4282 20.2465 91.4282Z" fill="#43484C"/>
|
||||
<g id="Group 7">
|
||||
<mask id="mask0_201_141" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="78" y="219" width="417" height="165">
|
||||
<path id="Clip 6" fill-rule="evenodd" clip-rule="evenodd" d="M495 219V384H286.5H78V219H495Z" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_201_141)">
|
||||
<path id="Fill 5" fill-rule="evenodd" clip-rule="evenodd" d="M495 279.775C495 286.675 489.336 292.321 482.413 292.321H402.016C398.559 292.321 395.405 293.734 393.131 296.009C390.849 298.276 389.43 301.421 389.43 304.867C389.43 311.768 395.094 317.413 402.016 317.413H416.954C420.41 317.413 423.566 318.827 425.84 321.102C428.122 323.369 429.54 326.514 429.54 329.96C429.54 336.86 423.876 342.506 416.954 342.506H382.608C350.344 366.662 310.251 380.981 266.802 380.981C174.004 380.981 96.5026 315.699 78 228.695C101.747 225.073 126.937 222.162 153.235 220.038L155.215 221.836L157.079 219.736C188.671 217.286 221.833 215.981 256.036 215.981C328.703 215.981 396.705 221.869 454.847 232.107C452.02 244.302 448.026 256.045 442.991 267.228H482.413C485.879 267.228 489.017 268.642 491.3 270.917C493.582 273.184 495 276.328 495 279.775Z" fill="#34393D"/>
|
||||
</g>
|
||||
</g>
|
||||
<path id="Fill 8" fill-rule="evenodd" clip-rule="evenodd" d="M459 206.12C458.285 216.665 456.771 226.975 454.525 237C396.235 226.404 328.06 220.31 255.208 220.31C220.919 220.31 187.673 221.66 156 224.196L177.965 198.615L212.134 221.028L258.388 208.19L337.959 220.31L372.503 208.19L381.252 220.31L402.126 204.736L421.632 222.707L452.396 198L459 206.12Z" fill="#55575E"/>
|
||||
<path id="Fill 10" fill-rule="evenodd" clip-rule="evenodd" d="M344.905 216.502C353.565 203.915 358.526 188.987 358.397 173.012C358.043 128.578 318.46 92.8734 269.986 93.2602C221.512 93.6478 182.505 129.983 182.86 174.416C183.214 218.849 222.798 254.555 271.271 254.168C288.59 254.03 304.7 249.3 318.238 241.253L354.454 249.78L344.905 216.502Z" fill="#686B73"/>
|
||||
<path id="Fill 12" fill-rule="evenodd" clip-rule="evenodd" d="M245.029 171.813C245.069 176.862 241.008 180.988 235.959 181.028C230.909 181.069 226.783 177.008 226.743 171.959C226.703 166.909 230.763 162.784 235.813 162.743C240.861 162.703 244.987 166.763 245.029 171.813Z" fill="#BEC0D6"/>
|
||||
<path id="Fill 14" fill-rule="evenodd" clip-rule="evenodd" d="M277.942 171.812C277.982 176.862 273.922 180.988 268.872 181.028C263.822 181.068 259.696 177.008 259.657 171.959C259.616 166.909 263.677 162.783 268.726 162.743C273.775 162.703 277.901 166.763 277.942 171.812Z" fill="#BEC0D6"/>
|
||||
<path id="Fill 16" fill-rule="evenodd" clip-rule="evenodd" d="M310.856 171.812C310.897 176.861 306.836 180.987 301.786 181.028C296.737 181.068 292.611 177.008 292.571 171.958C292.53 166.908 296.591 162.783 301.641 162.743C306.689 162.702 310.815 166.763 310.856 171.812Z" fill="#BEC0D6"/>
|
||||
<path id="Fill 20" fill-rule="evenodd" clip-rule="evenodd" d="M173.714 235.887C173.714 241.693 175.571 247.099 178.763 251.649L175.376 263.789L188.564 260.581C193.523 263.465 199.402 265.143 205.714 265.143C223.388 265.143 237.714 252.044 237.714 235.887C237.714 219.728 223.388 206.629 205.714 206.629C188.041 206.629 173.714 219.728 173.714 235.887Z" fill="url(#paint0_linear_201_141)"/>
|
||||
<g id="Frame 148">
|
||||
<path id="Fill 26" fill-rule="evenodd" clip-rule="evenodd" d="M189 234.658C189 236.677 190.637 238.314 192.657 238.314C194.676 238.314 196.314 236.677 196.314 234.658C196.314 232.638 194.676 231 192.657 231C190.637 231 189 232.638 189 234.658Z" fill="#0D0D0D"/>
|
||||
<path id="Fill 24" fill-rule="evenodd" clip-rule="evenodd" d="M202.314 234.658C202.314 236.677 203.951 238.314 205.971 238.314C207.99 238.314 209.628 236.677 209.628 234.658C209.628 232.638 207.99 231 205.971 231C203.951 231 202.314 232.638 202.314 234.658Z" fill="#0D0D0D"/>
|
||||
<path id="Fill 22" fill-rule="evenodd" clip-rule="evenodd" d="M215.629 234.658C215.629 236.677 217.265 238.314 219.285 238.314C221.305 238.314 222.943 236.677 222.943 234.658C222.943 232.638 221.305 231 219.285 231C217.265 231 215.629 232.638 215.629 234.658Z" fill="#0D0D0D"/>
|
||||
</g>
|
||||
<path id="Fill 28" fill-rule="evenodd" clip-rule="evenodd" d="M153.6 223.557C127.481 225.685 102.461 228.601 78.8755 232.228C78.0671 228.408 77.3754 224.546 76.8003 220.65L117.229 190.171L153.6 223.557Z" fill="#55575E"/>
|
||||
<path id="Fill 30" fill-rule="evenodd" clip-rule="evenodd" d="M336.457 286.171C336.457 289.706 304.529 292.571 265.143 292.571C225.757 292.571 193.829 289.706 193.829 286.171C193.829 282.638 225.757 279.771 265.143 279.771C304.529 279.771 336.457 282.638 336.457 286.171Z" fill="#686B73"/>
|
||||
<path id="Fill 32" fill-rule="evenodd" clip-rule="evenodd" d="M453.486 345.599C446.446 345.599 440.686 339.839 440.686 332.799C440.686 325.76 446.446 320 453.486 320C460.526 320 466.286 325.76 466.286 332.799C466.286 339.839 460.526 345.599 453.486 345.599Z" fill="#34393D"/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_201_141" x1="205.714" y1="177.372" x2="147.434" y2="241.115" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#9DC9FB"/>
|
||||
<stop offset="1" stop-color="#9DC9FB"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.2 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user