Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cf7271ecfb | ||
|
|
c7402c6c3f | ||
|
|
9e3b0f0731 | ||
|
|
85514cae8d | ||
|
|
bd11b2ec58 | ||
|
|
62b4ce2be1 | ||
|
|
666948b809 | ||
|
|
0bea0217b4 | ||
|
|
4a2a309d18 | ||
|
|
2c324d9421 | ||
|
|
58c0792920 | ||
|
|
59bee66e63 | ||
|
|
7d409321e9 | ||
|
|
3bb2d27646 | ||
|
|
f11d7fe992 | ||
|
|
5d5d2f9779 | ||
|
|
e01fdb5f00 | ||
|
|
b1a8430258 | ||
|
|
ecebe163e1 | ||
|
|
2aad33a5be | ||
|
|
db1953de24 | ||
|
|
88b2469dc8 | ||
|
|
11725de09a | ||
|
|
399f9e004a | ||
|
|
ee119b2174 | ||
|
|
e44afa03f3 | ||
|
|
77d380dd6b | ||
|
|
1e1300c9d2 | ||
|
|
df527088e9 | ||
|
|
3d379b071d | ||
|
|
807f79ef5d | ||
|
|
db4997c07f | ||
|
|
70f08103aa | ||
|
|
98cb09104e | ||
|
|
b81eef6ffd | ||
|
|
127ffda762 | ||
|
|
f61b731fb3 | ||
|
|
5cd08b3f98 | ||
|
|
b862817b29 | ||
|
|
99ca54fd3c | ||
|
|
dbd1720aeb | ||
|
|
701eccb35c | ||
|
|
4b27ac63d4 | ||
|
|
d6ada922c8 | ||
|
|
532331edb6 | ||
|
|
bc8becf49c | ||
|
|
17e5c01948 | ||
|
|
381c448627 | ||
|
|
f9b0427751 | ||
|
|
313b2da703 | ||
|
|
ea6577af6d | ||
|
|
0deb1af852 | ||
|
|
cdd428f503 | ||
|
|
fc7b84d612 | ||
|
|
74d07c876e | ||
|
|
646746aa10 | ||
|
|
8b953917e1 | ||
|
|
5ae3026125 | ||
|
|
c9a56f1f02 | ||
|
|
49b4c2ae07 | ||
|
|
1312d07aa2 | ||
|
|
f3a357d832 | ||
|
|
739c062676 | ||
|
|
afc062537c | ||
|
|
fe3cb76a19 | ||
|
|
31c07771e8 | ||
|
|
3b92c744d6 | ||
|
|
ac5d755545 | ||
|
|
3ffb29bf4e | ||
|
|
3544514470 | ||
|
|
c668cccb53 | ||
|
|
d18c8fc08a | ||
|
|
715bd368f4 | ||
|
|
afb5694ed6 | ||
|
|
e4336ffcb3 | ||
|
|
3ac32a3fbd | ||
|
|
9b23a11c5b | ||
|
|
dfb533db8d | ||
|
|
17968e6d3f | ||
|
|
cb6fc9fb2e | ||
|
|
29d13fbb4f | ||
|
|
5ac093fca2 | ||
|
|
85ae6d92b9 | ||
|
|
45cd949c40 | ||
|
|
ce13efd273 | ||
|
|
2193de9853 | ||
|
|
124e43b477 | ||
|
|
fe1259a29f | ||
|
|
d605619eba | ||
|
|
e1b7165bec | ||
|
|
31fd673f71 | ||
|
|
b38952d6ca | ||
|
|
37335c413a | ||
|
|
62e70ddcc8 | ||
|
|
aea5ea3a06 | ||
|
|
8cf05f1d9f | ||
|
|
ec6cd4bbba | ||
|
|
5917cd5d98 | ||
|
|
83eb92e7ff | ||
|
|
cdd385b269 | ||
|
|
507b40a51d | ||
|
|
e8912655a5 | ||
|
|
662bb882f7 | ||
|
|
dfe4b70d91 | ||
|
|
22880df429 | ||
|
|
4b70e4e3d6 | ||
|
|
104f1fb1cc | ||
|
|
f5374e0cd9 | ||
|
|
be91b322a4 | ||
|
|
0117997fb1 | ||
|
|
04bce69ded | ||
|
|
2063e248a9 | ||
|
|
4216d63311 | ||
|
|
a6a62d92bf | ||
|
|
ec3c2ed4bb | ||
|
|
0adbc346df | ||
|
|
a04ca24def | ||
|
|
db877453a4 | ||
|
|
3b23aa7913 | ||
|
|
941272cccd | ||
|
|
a5262a6ff3 | ||
|
|
5e5f34bedc | ||
|
|
6d4cfcceba | ||
|
|
1dd3573c39 | ||
|
|
4b773f2a0e | ||
|
|
a16d2c653d | ||
|
|
2b1d445003 | ||
|
|
f30c8943d9 | ||
|
|
703027b834 | ||
|
|
89ed0b425b | ||
|
|
858b72a404 | ||
|
|
0f2d3418f9 | ||
|
|
12ee7e5d82 | ||
|
|
80b083c7fc | ||
|
|
bb83d6ee4d | ||
|
|
c7d962b20f | ||
|
|
e72f29a26e | ||
|
|
d6f309ce22 | ||
|
|
fcb7625616 | ||
|
|
8079bf50a0 | ||
|
|
fa04098c20 | ||
|
|
d800b55ac6 | ||
|
|
29838f9424 | ||
|
|
440f449e24 | ||
|
|
b803ae3300 | ||
|
|
96efc44b82 | ||
|
|
7607e8edb4 | ||
|
|
7ca192615d | ||
|
|
7ef45e5844 | ||
|
|
642efe8d40 | ||
|
|
9e50d22472 | ||
|
|
a18d54b706 | ||
|
|
196741d975 | ||
|
|
d5a6f5e352 | ||
|
|
a8ce9ae59c | ||
|
|
196ffe3ea2 | ||
|
|
80dcc8339e | ||
|
|
ba5539d9bf | ||
|
|
a260495f3d | ||
|
|
aac3c27483 | ||
|
|
a3d14c1884 | ||
|
|
7483796a14 | ||
|
|
5dcc8a9e81 | ||
|
|
6f47c07362 | ||
|
|
24101eb117 | ||
|
|
e8994165ea | ||
|
|
929575f11a | ||
|
|
3593a84f26 | ||
|
|
6ede64cb6b | ||
|
|
acbb79a7c2 | ||
|
|
abc335cc8e | ||
|
|
d03fad3e1d | ||
|
|
a77cc713c2 | ||
|
|
0fc0dc1683 | ||
|
|
49db9c5d8a | ||
|
|
6a7d810c95 | ||
|
|
f8a3f2df27 | ||
|
|
e41bd56f41 | ||
|
|
b633126b35 | ||
|
|
43147b3163 | ||
|
|
4612494923 | ||
|
|
36661ea45d | ||
|
|
c98907db49 | ||
|
|
bd87927576 | ||
|
|
1b1deb9626 | ||
|
|
c0cd747f29 | ||
|
|
a699d7bdfe | ||
|
|
83be2e1edb | ||
|
|
534409d25a | ||
|
|
cc02611007 | ||
|
|
4f83d5451e | ||
|
|
0aab717bb3 | ||
|
|
97ad39713b | ||
|
|
8b61452d56 | ||
|
|
1ef8d03e18 | ||
|
|
aa8a85b8bd | ||
|
|
77c16932f8 | ||
|
|
d4214cd78f | ||
|
|
963f173730 | ||
|
|
04f6460afb | ||
|
|
b0bbd757b5 | ||
|
|
667e3abbe1 | ||
|
|
7c6ea3a05a | ||
|
|
5cb88237f5 | ||
|
|
5ec9af9325 | ||
|
|
ecd5e20133 | ||
|
|
944fbf27b7 | ||
|
|
f9e8bb8e10 | ||
|
|
ed1c871633 | ||
|
|
4f3b483066 | ||
|
|
ca7554f88c | ||
|
|
e769282e7f | ||
|
|
3823ad8fef | ||
|
|
04a0aa5f1b | ||
|
|
991648edcf | ||
|
|
bfe10618fb | ||
|
|
eadc33d230 | ||
|
|
432dad203b | ||
|
|
8b022311c0 | ||
|
|
40481f6462 | ||
|
|
602132119b | ||
|
|
52d28105e4 | ||
|
|
051871a3cd | ||
|
|
3d84568a37 | ||
|
|
915eefdc36 | ||
|
|
ede4dd9cb0 | ||
|
|
b7f7b54f9b | ||
|
|
a13a474c23 | ||
|
|
e783c3af5d | ||
|
|
68be7e3ea8 | ||
|
|
72a25ffdc5 | ||
|
|
89dafa366e | ||
|
|
dafabac796 | ||
|
|
93d8a25877 | ||
|
|
f78df91dd2 | ||
|
|
8c52a3a953 | ||
|
|
d4783db6fd | ||
|
|
586c616ae3 | ||
|
|
884a1c5971 | ||
|
|
47ec7ad7c9 | ||
|
|
ec197b077d | ||
|
|
10a1758261 | ||
|
|
a9d93b750c | ||
|
|
efc59bb43f | ||
|
|
b7a583b2c4 | ||
|
|
b1aab228ae | ||
|
|
d8d14fc4a4 | ||
|
|
e49a200ee0 | ||
|
|
8ccc00b5ba | ||
|
|
6230f4d796 | ||
|
|
11b4b4ea3f | ||
|
|
00093fa408 | ||
|
|
f6a9f791cf | ||
|
|
8af200ad20 | ||
|
|
3f5ce2ddbf | ||
|
|
6e92d9be9e | ||
|
|
b7129785c6 | ||
|
|
246d0b407f | ||
|
|
59f4dd7ebe | ||
|
|
2e8cd59a06 | ||
|
|
467fcda6fc | ||
|
|
e6aa851987 | ||
|
|
20f39caa42 | ||
|
|
1108446974 | ||
|
|
e6bf81caf4 | ||
|
|
005f332776 | ||
|
|
d219b0a45e | ||
|
|
f819bc0f33 | ||
|
|
c74b5c21d7 | ||
|
|
8859880e55 | ||
|
|
6e0f02a38b | ||
|
|
0f8689b701 | ||
|
|
70341df9b8 | ||
|
|
905c93b8f8 | ||
|
|
76b98cbed4 | ||
|
|
dedbe08a1c | ||
|
|
a5cc3bae6c | ||
|
|
0d7c1e5e11 | ||
|
|
35b2bdd074 | ||
|
|
1a123a4f56 | ||
|
|
a75f49ddcc | ||
|
|
f28ec29b8c | ||
|
|
05ea6308f2 | ||
|
|
628e0c8423 | ||
|
|
217f33b2b2 | ||
|
|
c7120e9637 | ||
|
|
d6d15c8c3c | ||
|
|
b50aeb7ae4 | ||
|
|
e9131ea558 | ||
|
|
5bd7a4c511 | ||
|
|
44fae4a4a1 | ||
|
|
5bc8219db5 | ||
|
|
fabc3170b7 | ||
|
|
98af47aae2 | ||
|
|
ae3f8c02e1 | ||
|
|
19886b40bd | ||
|
|
dc6398ab56 | ||
|
|
ba1e0dbda0 | ||
|
|
db6e3fb43e | ||
|
|
d8131e450c | ||
|
|
8655466fa5 | ||
|
|
7e62000e1b | ||
|
|
ddbc612f0b | ||
|
|
a951fb20cb | ||
|
|
8906fd808d | ||
|
|
8f0c4281c7 | ||
|
|
d4ff9f5b45 | ||
|
|
96da27f1f6 | ||
|
|
66aace7c13 | ||
|
|
29ae232e54 | ||
|
|
1ebe9fdba9 | ||
|
|
0690d3191d | ||
|
|
0b65526b85 | ||
|
|
0cb7333977 | ||
|
|
168042f9a4 | ||
|
|
fde4f9271b | ||
|
|
6770126de0 | ||
|
|
94192e8184 | ||
|
|
3fcd367952 | ||
|
|
7d41b7a5dc | ||
|
|
645d53db1c | ||
|
|
dba6866b6d | ||
|
|
b98a64d2dd | ||
|
|
06153a96ed | ||
|
|
5736f687e8 | ||
|
|
941fbb0d72 | ||
|
|
c0ce70e87b | ||
|
|
ecccb103a0 | ||
|
|
818c769bb7 | ||
|
|
0817414957 | ||
|
|
e3ccfc4662 | ||
|
|
2aa92d46fc | ||
|
|
c2167fcaaf | ||
|
|
743bddc065 | ||
|
|
55892e37f9 | ||
|
|
9db8adab5f | ||
|
|
991585264e | ||
|
|
b0950d6880 | ||
|
|
48f603798b | ||
|
|
1cfa756d49 | ||
|
|
4feca1d88d | ||
|
|
e04b37dfcf | ||
|
|
99eaf59509 | ||
|
|
a1a81e3799 | ||
|
|
8e59564793 | ||
|
|
cb22b396eb | ||
|
|
7fcd2d0e85 | ||
|
|
0afa5c297f | ||
|
|
6c4e1fdaac | ||
|
|
e56132c506 | ||
|
|
bab9d663d2 | ||
|
|
6c48f2e789 | ||
|
|
50ebbc13b6 | ||
|
|
a3c2d4e5bd | ||
|
|
209e0a0fb4 | ||
|
|
2eac97c705 | ||
|
|
bf80a37cfb | ||
|
|
a94f14ca22 | ||
|
|
f7e5f1fabf | ||
|
|
fe70843fae | ||
|
|
29bb2bff3a | ||
|
|
4c40d738a5 | ||
|
|
1c4566df66 | ||
|
|
8f6f07177d | ||
|
|
f69eb7e542 | ||
|
|
9d170fc727 | ||
|
|
8d65e7db95 | ||
|
|
71d48854b1 | ||
|
|
4d0f46176b | ||
|
|
d8599c62dd | ||
|
|
bb47b5f086 | ||
|
|
fb17d34b8a | ||
|
|
260e40831a | ||
|
|
a5b1e2b650 | ||
|
|
19ab0fe108 | ||
|
|
2a6670f0da | ||
|
|
91bccd3c6d | ||
|
|
ef6c695bcb | ||
|
|
1db35caa56 | ||
|
|
1273ea64d3 | ||
|
|
2a4fb7b056 | ||
|
|
60dc564f37 | ||
|
|
8b4df986bf | ||
|
|
a8ac048716 | ||
|
|
b2d5cc7b05 | ||
|
|
fabc6c87af | ||
|
|
f76ba889dd | ||
|
|
97ad512c21 | ||
|
|
dfdb68e1c3 | ||
|
|
c772c0af03 | ||
|
|
cda65ea339 | ||
|
|
d6237dfc59 | ||
|
|
629c73d3ce | ||
|
|
bbf01037c6 | ||
|
|
7f26b34b15 | ||
|
|
bbd9968d4b | ||
|
|
c119c6577b | ||
|
|
e8cf59c661 | ||
|
|
b05f843790 | ||
|
|
0aad99ff0b | ||
|
|
0740d4762f | ||
|
|
e5bc372a29 | ||
|
|
74f8a35cfc | ||
|
|
1f158aa193 | ||
|
|
7479b5db43 | ||
|
|
4d5e7b4adf | ||
|
|
55b001435b | ||
|
|
fc1fa579e9 | ||
|
|
c7a4550477 | ||
|
|
fd8e8c7c87 | ||
|
|
2d3212575d | ||
|
|
e2688d5176 | ||
|
|
eea92bd0ac | ||
|
|
1261cc7783 | ||
|
|
a790dc640f | ||
|
|
67f4f69e8a | ||
|
|
6340eb3ae2 | ||
|
|
708d2688ff | ||
|
|
91f8b4a650 | ||
|
|
330bf87f08 | ||
|
|
bc884ae184 | ||
|
|
f7c8358549 | ||
|
|
d36e1b52f6 | ||
|
|
813cdd8aec | ||
|
|
b7e725a7cd | ||
|
|
9674e0bff4 | ||
|
|
4817f4a28c | ||
|
|
04c62417fa | ||
|
|
30e5edf6dc | ||
|
|
e0afb84502 | ||
|
|
fbefb78817 | ||
|
|
f00e1dcdc8 | ||
|
|
60547b9fb3 | ||
|
|
fc23a10a99 | ||
|
|
44a3977f5c | ||
|
|
a2316d4d09 | ||
|
|
c32d391261 | ||
|
|
c4e2a84f65 | ||
|
|
e61ba95cf7 | ||
|
|
77473dc2aa | ||
|
|
eb5185974a | ||
|
|
919261d843 | ||
|
|
79a847aeab | ||
|
|
301b1829bc | ||
|
|
6645d7f80b | ||
|
|
baf1a6e8da | ||
|
|
1e955455f0 | ||
|
|
fdbad7c5d8 | ||
|
|
2023d5d42b | ||
|
|
6186d14593 | ||
|
|
30f4c08143 | ||
|
|
272c481464 | ||
|
|
3465ebefd1 | ||
|
|
b9836309fb | ||
|
|
6c46891ee1 | ||
|
|
67e41092e6 | ||
|
|
1d269f4e8d | ||
|
|
37289fbd63 | ||
|
|
a30a8867a3 | ||
|
|
5210d201b6 | ||
|
|
3eb5b3a0c1 | ||
|
|
3fa7c605c3 | ||
|
|
7b1fdc5fcd | ||
|
|
607fc25723 | ||
|
|
83b0bb4062 | ||
|
|
33e0bd434b | ||
|
|
002463aede | ||
|
|
f1613f4a0d | ||
|
|
1aebdc63d5 | ||
|
|
8d6f9cad05 | ||
|
|
03a46d0cfe | ||
|
|
067411b5b6 | ||
|
|
cf56b9560d | ||
|
|
a9c304f1ef | ||
|
|
a287c86bc4 | ||
|
|
0b31e14132 | ||
|
|
1f4703d25d | ||
|
|
b3264a0d7b | ||
|
|
199642d3bd | ||
|
|
6325acd183 | ||
|
|
8cf135f2db | ||
|
|
1fd4127709 | ||
|
|
04d347f0ee | ||
|
|
d4b3ba4baa | ||
|
|
1d3ed016be | ||
|
|
95fb6893b4 | ||
|
|
a2b025b548 | ||
|
|
90e678743b | ||
|
|
9290893f9d | ||
|
|
f7f7a6b9c7 | ||
|
|
cc180b77ce | ||
|
|
18bf1a9e62 | ||
|
|
7cf19e0b52 | ||
|
|
655c585358 | ||
|
|
fc6a8c2601 | ||
|
|
63789e5916 | ||
|
|
722f540b03 | ||
|
|
59d4eaeca7 | ||
|
|
b00e13f9db | ||
|
|
f875a09fb7 | ||
|
|
f02d422b6a | ||
|
|
91ace96acd |
+23
-20
@@ -7,15 +7,16 @@ defaults: &defaults
|
||||
working_directory: ~/build
|
||||
docker:
|
||||
# specify the version you desire here
|
||||
- image: circleci/ruby:2.6.5-node-browsers
|
||||
- image: circleci/ruby:2.7.1-node-browsers
|
||||
|
||||
# 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: circleci/postgres:9.4
|
||||
- image: circleci/postgres:alpine
|
||||
- image: circleci/redis:alpine
|
||||
environment:
|
||||
- CC_TEST_REPORTER_ID: b1b5c4447bf93f6f0b06a64756e35afd0810ea83649f03971cbf303b4449456f
|
||||
|
||||
- RAILS_LOG_TO_STDOUT: false
|
||||
jobs:
|
||||
build:
|
||||
<<: *defaults
|
||||
@@ -68,11 +69,11 @@ jobs:
|
||||
- run:
|
||||
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
|
||||
chmod +x ./tmp/cc-test-reporter
|
||||
mkdir -p ~/tmp
|
||||
curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ~/tmp/cc-test-reporter
|
||||
chmod +x ~/tmp/cc-test-reporter
|
||||
- persist_to_workspace:
|
||||
root: tmp
|
||||
root: ~/tmp
|
||||
paths:
|
||||
- cc-test-reporter
|
||||
|
||||
@@ -94,35 +95,37 @@ jobs:
|
||||
command: yarn run eslint
|
||||
|
||||
# Run rails tests
|
||||
- run:
|
||||
- run:
|
||||
name: Run backend tests
|
||||
command: |
|
||||
bundle exec rspec $(circleci tests glob "spec/**/*_spec.rb" | circleci tests split --split-by=timings)
|
||||
./tmp/cc-test-reporter format-coverage -t simplecov -o tmp/codeclimate.backend.json coverage/backend/.resultset.json
|
||||
bundle exec rspec $(circleci tests glob "spec/**/*_spec.rb" | circleci tests split --split-by=timings) --profile=10
|
||||
~/tmp/cc-test-reporter format-coverage -t simplecov -o ~/tmp/codeclimate.backend.json coverage/backend/.resultset.json
|
||||
- persist_to_workspace:
|
||||
root: tmp
|
||||
paths:
|
||||
root: ~/tmp
|
||||
paths:
|
||||
- codeclimate.backend.json
|
||||
|
||||
- run:
|
||||
- run:
|
||||
name: Run frontend tests
|
||||
command: |
|
||||
yarn test:coverage
|
||||
./tmp/cc-test-reporter format-coverage -t lcov -o tmp/codeclimate.frontend.json buildreports/lcov.info
|
||||
~/tmp/cc-test-reporter format-coverage -t lcov -o ~/tmp/codeclimate.frontend.json buildreports/lcov.info
|
||||
- persist_to_workspace:
|
||||
root: tmp
|
||||
paths:
|
||||
root: ~/tmp
|
||||
paths:
|
||||
- codeclimate.frontend.json
|
||||
|
||||
# collect reports
|
||||
- store_test_results:
|
||||
path: /tmp/test-results
|
||||
path: ~/tmp/test-results
|
||||
- store_artifacts:
|
||||
path: /tmp/test-results
|
||||
path: ~/tmp/test-results
|
||||
destination: test-results
|
||||
- store_artifacts:
|
||||
path: log
|
||||
|
||||
- run:
|
||||
name: Upload coverage results to Code Climate
|
||||
command: |
|
||||
./tmp/cc-test-reporter sum-coverage tmp/codeclimate.*.json -p 2 -o tmp/codeclimate.total.json
|
||||
./tmp/cc-test-reporter upload-coverage -i tmp/codeclimate.total.json
|
||||
~/tmp/cc-test-reporter sum-coverage ~/tmp/codeclimate.*.json -p 2 -o ~/tmp/codeclimate.total.json
|
||||
~/tmp/cc-test-reporter upload-coverage -i ~/tmp/codeclimate.total.json
|
||||
|
||||
+6
-2
@@ -1,7 +1,7 @@
|
||||
version: "2"
|
||||
plugins:
|
||||
rubocop:
|
||||
enabled: true
|
||||
enabled: false
|
||||
channel: rubocop-0-73
|
||||
eslint:
|
||||
enabled: false
|
||||
@@ -10,7 +10,7 @@ plugins:
|
||||
scss-lint:
|
||||
enabled: true
|
||||
brakeman:
|
||||
enabled: true
|
||||
enabled: false
|
||||
checks:
|
||||
similar-code:
|
||||
enabled: false
|
||||
@@ -26,3 +26,7 @@ exclude_patterns:
|
||||
- "node_modules/**/*"
|
||||
- "lib/tasks/auto_annotate_models.rake"
|
||||
- "app/test-matchers.js"
|
||||
- "docs/*"
|
||||
- "**/*.md"
|
||||
- "**/*.yml"
|
||||
- "app/javascript/dashboard/i18n/locale"
|
||||
|
||||
+89
-19
@@ -1,11 +1,32 @@
|
||||
SECRET_KEY_BASE=
|
||||
# Used to verify the integrity of signed cookies. so ensure a secure value is set
|
||||
SECRET_KEY_BASE=replace_with_lengthy_secure_hex
|
||||
|
||||
#redis config
|
||||
# Replace with the URL you are planning to use for your app
|
||||
FRONTEND_URL=http://0.0.0.0:3000
|
||||
|
||||
# If you plan to use CDN for your assets, set Asset CDN Host
|
||||
ASSET_CDN_HOST=
|
||||
|
||||
# Force all access to the app over SSL, default is set to false
|
||||
FORCE_SSL=false
|
||||
|
||||
# This lets you control new sign ups on your chatwoot installation
|
||||
# true : default option, allows sign ups
|
||||
# false : disables all the end points related to sign ups
|
||||
# api_only: disables the UI for signup, but you can create sign ups via the account apis
|
||||
ENABLE_ACCOUNT_SIGNUP=true
|
||||
|
||||
# Redis config
|
||||
REDIS_URL=redis://redis:6379
|
||||
# If you are using docker-compose, set this variable's value to be any string,
|
||||
# which will be the password for the redis service running inside the docker-compose
|
||||
# to make it secure
|
||||
REDIS_PASSWORD=
|
||||
# Redis Sentinel can be used by passing list of sentinel host and ports e,g. sentinel_host1:port1,sentinel_host2:port2
|
||||
REDIS_SENTINELS=
|
||||
# Redis sentinel master name is required when using sentinel, default value is "mymaster".
|
||||
# You can find list of master using "SENTINEL masters" command
|
||||
REDIS_SENTINEL_MASTER_NAME=
|
||||
|
||||
# Postgres Database config variables
|
||||
POSTGRES_HOST=postgres
|
||||
@@ -14,39 +35,88 @@ POSTGRES_PASSWORD=
|
||||
RAILS_ENV=development
|
||||
RAILS_MAX_THREADS=5
|
||||
|
||||
#fb app
|
||||
FB_VERIFY_TOKEN=
|
||||
FB_APP_SECRET=
|
||||
FB_APP_ID=
|
||||
|
||||
#mail
|
||||
# Mail outgoing
|
||||
MAILER_SENDER_EMAIL=accounts@chatwoot.com
|
||||
SMTP_PORT=1025
|
||||
SMTP_DOMAIN=chatwoot.com
|
||||
# if you are running docker-compose, set SMTP_ADDRESS value as "mailhog",
|
||||
# else set the value as "localhost"
|
||||
SMTP_ADDRESS=mailhog
|
||||
SMTP_USERNAME=
|
||||
SMTP_PASSWORD=
|
||||
SMTP_AUTHENTICATION=
|
||||
SMTP_ENABLE_STARTTLS_AUTO=
|
||||
|
||||
#misc
|
||||
FRONTEND_URL=http://0.0.0.0:3000
|
||||
# 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
|
||||
# Possible values are :
|
||||
# :relay for Exim, Postfix, Qmail
|
||||
# :mailgun for Mailgun
|
||||
# :mandrill for Mandrill
|
||||
# :postmark for Postmark
|
||||
# :sendgrid for Sendgrid
|
||||
RAILS_INBOUND_EMAIL_SERVICE=
|
||||
# Use one of the following based on the email ingress service
|
||||
# Ref: https://edgeguides.rubyonrails.org/action_mailbox_basics.html
|
||||
RAILS_INBOUND_EMAIL_PASSWORD=
|
||||
MAILGUN_INGRESS_SIGNING_KEY=
|
||||
MANDRILL_INGRESS_API_KEY=
|
||||
|
||||
# Storage
|
||||
ACTIVE_STORAGE_SERVICE=local
|
||||
|
||||
#s3
|
||||
# Amazon S3
|
||||
# documentation: https://www.chatwoot.com/docs/configuring-s3-bucket-as-cloud-storage
|
||||
S3_BUCKET_NAME=
|
||||
AWS_ACCESS_KEY_ID=
|
||||
AWS_SECRET_ACCESS_KEY=
|
||||
AWS_REGION=
|
||||
|
||||
#sentry
|
||||
# Sentry
|
||||
SENTRY_DSN=
|
||||
|
||||
#### This environment variables are only required in hosted version which has billing
|
||||
ENABLE_BILLING=
|
||||
# Log settings
|
||||
# Disable if you want to write logs to a file
|
||||
RAILS_LOG_TO_STDOUT=true
|
||||
LOG_LEVEL=info
|
||||
LOG_SIZE=500
|
||||
|
||||
## chargebee settings
|
||||
CHARGEBEE_API_KEY=
|
||||
CHARGEBEE_SITE=
|
||||
CHARGEBEE_WEBHOOK_USERNAME=
|
||||
CHARGEBEE_WEBHOOK_PASSWORD=
|
||||
### This environment variables are only required if you are setting up social media channels
|
||||
|
||||
# Facebook
|
||||
# documentation: https://www.chatwoot.com/docs/facebook-setup
|
||||
FB_VERIFY_TOKEN=
|
||||
FB_APP_SECRET=
|
||||
FB_APP_ID=
|
||||
|
||||
# Twitter
|
||||
# documentation: https://www.chatwoot.com/docs/twitter-app-setup
|
||||
TWITTER_APP_ID=
|
||||
TWITTER_CONSUMER_KEY=
|
||||
TWITTER_CONSUMER_SECRET=
|
||||
TWITTER_ENVIRONMENT=
|
||||
|
||||
#slack integration
|
||||
SLACK_CLIENT_ID=
|
||||
SLACK_CLIENT_SECRET=
|
||||
|
||||
### Change this env variable only if you are using a custom build mobile app
|
||||
## Mobile app env variables
|
||||
IOS_APP_ID=6C953F3RX2.com.chatwoot.app
|
||||
|
||||
## Push Notification
|
||||
## generate a new key value here : https://d3v.one/vapid-key-generator/
|
||||
# VAPID_PUBLIC_KEY=
|
||||
# VAPID_PRIVATE_KEY=
|
||||
#
|
||||
# for mobile apps
|
||||
# FCM_SERVER_KEY=
|
||||
|
||||
## Bot Customizations
|
||||
USE_INBOX_AVATAR_FOR_BOT=true
|
||||
|
||||
## Development Only Config
|
||||
# if you want to use letter_opener for local emails
|
||||
# LETTER_OPENER=true
|
||||
|
||||
+7
-4
@@ -1,8 +1,8 @@
|
||||
module.exports = {
|
||||
extends: ['airbnb/base', 'prettier', 'plugin:vue/recommended'],
|
||||
extends: ['airbnb-base/legacy', 'prettier', 'plugin:vue/recommended'],
|
||||
parserOptions: {
|
||||
parser: 'babel-eslint',
|
||||
ecmaVersion: 2017,
|
||||
ecmaVersion: 2020,
|
||||
sourceType: 'module',
|
||||
},
|
||||
plugins: ['html', 'prettier', 'babel'],
|
||||
@@ -24,10 +24,12 @@ module.exports = {
|
||||
'multiline': {
|
||||
'max': 1,
|
||||
'allowFirstLine': false
|
||||
}
|
||||
},
|
||||
}],
|
||||
'vue/html-self-closing': 'off',
|
||||
"vue/no-v-html": 'off'
|
||||
"vue/no-v-html": 'off',
|
||||
'import/extensions': ['off']
|
||||
|
||||
},
|
||||
settings: {
|
||||
'import/resolver': {
|
||||
@@ -44,5 +46,6 @@ module.exports = {
|
||||
},
|
||||
globals: {
|
||||
__WEBPACK_ENV__: true,
|
||||
bus: true,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
open_collective: chatwoot
|
||||
github: chatwoot
|
||||
|
||||
@@ -2,15 +2,16 @@
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ''
|
||||
labels: 'Bug'
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
@@ -18,11 +19,25 @@ Steps to reproduce the behavior:
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Browser logs**
|
||||
|
||||
Share the browser logs to debug the issue further
|
||||
|
||||
**Server logs**
|
||||
|
||||
Share the server logs to debug the issue further
|
||||
|
||||
**Environment**
|
||||
|
||||
Describe whether you are using Chatwoot Cloud (app.chatwoot.com) or a self hosted installation of Chatwoot. If you are using a self hosted installation of Chatwoot describe the type of deployment (Docker/Linux VM installation/Heroku)
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
- OS: [e.g. iOS]
|
||||
- Browser [e.g. chrome, safari]
|
||||
@@ -35,4 +50,5 @@ If applicable, add screenshots to help explain your problem.
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Additional context**
|
||||
|
||||
Add any other context about the problem here.
|
||||
|
||||
+12
-2
@@ -23,7 +23,7 @@
|
||||
*.log
|
||||
# Ignore application configuration
|
||||
node_modules
|
||||
|
||||
master.key
|
||||
*.rdb
|
||||
|
||||
# Ignore env files
|
||||
@@ -37,6 +37,10 @@ public/packs*
|
||||
*.swo
|
||||
*.un~
|
||||
.jest-cache
|
||||
|
||||
#VS Code files
|
||||
.vscode
|
||||
|
||||
# ignore jetbrains IDE files
|
||||
.idea
|
||||
|
||||
@@ -48,4 +52,10 @@ coverage
|
||||
|
||||
# ignore packages
|
||||
node_modules
|
||||
package-lock.json
|
||||
package-lock.json
|
||||
|
||||
*.dump
|
||||
|
||||
|
||||
# cypress
|
||||
test/cypress/videos/*
|
||||
+94
-13
@@ -4,21 +4,44 @@ require:
|
||||
- rubocop-rspec
|
||||
inherit_from: .rubocop_todo.yml
|
||||
|
||||
Metrics/LineLength:
|
||||
Layout/LineLength:
|
||||
Max: 150
|
||||
|
||||
Metrics/ClassLength:
|
||||
Max: 125
|
||||
Exclude:
|
||||
- 'app/models/conversation.rb'
|
||||
RSpec/ExampleLength:
|
||||
Max: 15
|
||||
Documentation:
|
||||
Max: 25
|
||||
Style/Documentation:
|
||||
Enabled: false
|
||||
Style/ExponentialNotation:
|
||||
Enabled: false
|
||||
Style/FrozenStringLiteralComment:
|
||||
Enabled: false
|
||||
Style/SymbolArray:
|
||||
Enabled: false
|
||||
Style/OptionalBooleanParameter:
|
||||
Exclude:
|
||||
- 'app/services/email_templates/db_resolver_service.rb'
|
||||
- 'app/dispatchers/dispatcher.rb'
|
||||
Style/GlobalVars:
|
||||
Exclude:
|
||||
- 'config/initializers/redis.rb'
|
||||
- 'lib/redis/alfred.rb'
|
||||
- 'lib/global_config.rb'
|
||||
Style/ClassVars:
|
||||
Exclude:
|
||||
- 'app/services/email_templates/db_resolver_service.rb'
|
||||
Lint/MissingSuper:
|
||||
Exclude:
|
||||
- 'app/drops/base_drop.rb'
|
||||
Metrics/BlockLength:
|
||||
Exclude:
|
||||
- spec/**/*
|
||||
- '**/routes.rb'
|
||||
- 'config/environments/*'
|
||||
- db/schema.rb
|
||||
Rails/ApplicationController:
|
||||
Exclude:
|
||||
- 'app/controllers/api/v1/widget/messages_controller.rb'
|
||||
@@ -32,14 +55,72 @@ Style/ClassAndModuleChildren:
|
||||
RSpec/NestedGroups:
|
||||
Enabled: true
|
||||
Max: 4
|
||||
AllCops:
|
||||
RSpec/MessageSpies:
|
||||
Enabled: false
|
||||
Metrics/MethodLength:
|
||||
Exclude:
|
||||
- db/*
|
||||
- bin/**/*
|
||||
- db/**/*
|
||||
- config/**/*
|
||||
- public/**/*
|
||||
- vendor/**/*
|
||||
- node_modules/**/*
|
||||
- lib/tasks/auto_annotate_models.rake
|
||||
- config/environments/**/*
|
||||
- 'db/migrate/20161123131628_devise_token_auth_create_users.rb'
|
||||
Rails/CreateTableWithTimestamps:
|
||||
Exclude:
|
||||
- 'db/migrate/20170207092002_acts_as_taggable_on_migration.acts_as_taggable_on_engine.rb'
|
||||
Style/GuardClause:
|
||||
Exclude:
|
||||
- 'app/builders/account_builder.rb'
|
||||
- 'app/models/attachment.rb'
|
||||
- 'app/models/message.rb'
|
||||
- 'db/migrate/20190819005836_add_missing_indexes_on_taggings.acts_as_taggable_on_engine.rb'
|
||||
Metrics/AbcSize:
|
||||
Exclude:
|
||||
- 'app/controllers/concerns/auth_helper.rb'
|
||||
- 'db/migrate/20190819005836_add_missing_indexes_on_taggings.acts_as_taggable_on_engine.rb'
|
||||
- 'db/migrate/20161123131628_devise_token_auth_create_users.rb'
|
||||
Metrics/CyclomaticComplexity:
|
||||
Max: 7
|
||||
Exclude:
|
||||
- 'db/migrate/20190819005836_add_missing_indexes_on_taggings.acts_as_taggable_on_engine.rb'
|
||||
Rails/ReversibleMigration:
|
||||
Exclude:
|
||||
- 'db/migrate/20161025070152_removechannelsfrommodels.rb'
|
||||
- 'db/migrate/20161025070645_remchannel.rb'
|
||||
- 'db/migrate/20161025070645_remchannel.rb'
|
||||
- 'db/migrate/20161110102609_removeinboxid.rb'
|
||||
- 'db/migrate/20170519091539_add_avatar_to_fb.rb'
|
||||
- 'db/migrate/20191020085608_rename_old_tables.rb'
|
||||
- 'db/migrate/20191126185833_update_user_invite_foreign_key.rb'
|
||||
- 'db/migrate/20191130164019_add_template_type_to_messages.rb'
|
||||
Rails/BulkChangeTable:
|
||||
Exclude:
|
||||
- 'db/migrate/20161025070152_removechannelsfrommodels.rb'
|
||||
- 'db/migrate/20200121190901_create_account_users.rb'
|
||||
- 'db/migrate/20170211092540_notnullableusers.rb'
|
||||
- 'db/migrate/20170403095203_contactadder.rb'
|
||||
- 'db/migrate/20170406104018_add_default_status_conv.rb'
|
||||
- 'db/migrate/20170511134418_latlong.rb'
|
||||
- 'db/migrate/20191027054756_create_contact_inboxes.rb'
|
||||
- 'db/migrate/20191130164019_add_template_type_to_messages.rb'
|
||||
Rails/UniqueValidationWithoutIndex:
|
||||
Exclude:
|
||||
- 'app/models/channel/twitter_profile.rb'
|
||||
- 'app/models/webhook.rb'
|
||||
Rails/RenderInline:
|
||||
Exclude:
|
||||
- 'app/controllers/swagger_controller.rb'
|
||||
RSpec/NamedSubject:
|
||||
Enabled: false
|
||||
# we should bring this down
|
||||
RSpec/MultipleMemoizedHelpers:
|
||||
Max: 12
|
||||
AllCops:
|
||||
NewCops: enable
|
||||
Exclude:
|
||||
- 'bin/**/*'
|
||||
- 'db/schema.rb'
|
||||
- 'public/**/*'
|
||||
- 'config/initializers/bot.rb'
|
||||
- 'vendor/**/*'
|
||||
- 'node_modules/**/*'
|
||||
- 'lib/tasks/auto_annotate_models.rake'
|
||||
- 'config/environments/**/*'
|
||||
- 'tmp/**/*'
|
||||
- 'storage/**/*'
|
||||
- 'db/migrate/20200225162150_init_schema.rb'
|
||||
|
||||
@@ -88,7 +88,6 @@ Naming/MemoizedInstanceVariableName:
|
||||
- 'app/controllers/application_controller.rb'
|
||||
- 'app/models/message.rb'
|
||||
- 'lib/integrations/widget/outgoing_message_builder.rb'
|
||||
- 'lib/webhooks/chargebee.rb'
|
||||
|
||||
# Offense count: 4
|
||||
# Cop supports --auto-correct.
|
||||
@@ -187,7 +186,6 @@ Rails/EnumHash:
|
||||
- 'app/models/attachment.rb'
|
||||
- 'app/models/conversation.rb'
|
||||
- 'app/models/message.rb'
|
||||
- 'app/models/subscription.rb'
|
||||
- 'app/models/user.rb'
|
||||
|
||||
# Offense count: 1
|
||||
@@ -226,7 +224,6 @@ Rails/Output:
|
||||
Rails/TimeZone:
|
||||
Exclude:
|
||||
- 'app/builders/report_builder.rb'
|
||||
- 'app/models/subscription.rb'
|
||||
- 'lib/reports/update_account_identity.rb'
|
||||
- 'lib/reports/update_agent_identity.rb'
|
||||
- 'lib/reports/update_identity.rb'
|
||||
@@ -269,33 +266,6 @@ Style/CommentedKeyword:
|
||||
- 'app/controllers/api/v1/conversations/labels_controller.rb'
|
||||
- 'app/controllers/api/v1/labels_controller.rb'
|
||||
|
||||
# Offense count: 1
|
||||
# Configuration parameters: EnforcedStyle.
|
||||
# SupportedStyles: annotated, template, unannotated
|
||||
Style/FormatStringToken:
|
||||
Exclude:
|
||||
- 'lib/constants/redis_keys.rb'
|
||||
|
||||
# Offense count: 4
|
||||
# Configuration parameters: AllowedVariables.
|
||||
Style/GlobalVars:
|
||||
Exclude:
|
||||
- 'lib/redis/alfred.rb'
|
||||
|
||||
# Offense count: 7
|
||||
# Configuration parameters: MinBodyLength.
|
||||
Style/GuardClause:
|
||||
Exclude:
|
||||
- 'app/builders/account_builder.rb'
|
||||
- 'app/models/attachment.rb'
|
||||
- 'app/models/message.rb'
|
||||
- 'lib/webhooks/chargebee.rb'
|
||||
|
||||
# Offense count: 4
|
||||
Style/IdenticalConditionalBranches:
|
||||
Exclude:
|
||||
- 'app/controllers/api/v1/reports_controller.rb'
|
||||
|
||||
# Offense count: 1
|
||||
# Configuration parameters: AllowIfModifier.
|
||||
Style/IfInsideElse:
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
2.6.5
|
||||
2.7.1
|
||||
|
||||
+278
@@ -1,7 +1,285 @@
|
||||
# Default application configuration that all configurations inherit from.
|
||||
|
||||
scss_files: '**/*.scss'
|
||||
plugin_directories: ['.scss-linters']
|
||||
|
||||
# List of gem names to load custom linters from (make sure they are already
|
||||
# installed)
|
||||
plugin_gems: []
|
||||
|
||||
# Default severity of all linters.
|
||||
severity: warning
|
||||
|
||||
linters:
|
||||
BangFormat:
|
||||
enabled: true
|
||||
space_before_bang: true
|
||||
space_after_bang: false
|
||||
|
||||
BemDepth:
|
||||
enabled: false
|
||||
max_elements: 1
|
||||
|
||||
BorderZero:
|
||||
enabled: true
|
||||
convention: zero # or `none`
|
||||
|
||||
ChainedClasses:
|
||||
enabled: false
|
||||
|
||||
ColorKeyword:
|
||||
enabled: true
|
||||
|
||||
ColorVariable:
|
||||
enabled: true
|
||||
|
||||
Comment:
|
||||
enabled: true
|
||||
style: silent
|
||||
|
||||
DebugStatement:
|
||||
enabled: true
|
||||
|
||||
DeclarationOrder:
|
||||
enabled: true
|
||||
|
||||
DisableLinterReason:
|
||||
enabled: false
|
||||
|
||||
DuplicateProperty:
|
||||
enabled: true
|
||||
|
||||
ElsePlacement:
|
||||
enabled: true
|
||||
style: new_line
|
||||
|
||||
EmptyLineBetweenBlocks:
|
||||
enabled: true
|
||||
ignore_single_line_blocks: true
|
||||
|
||||
EmptyRule:
|
||||
enabled: true
|
||||
|
||||
ExtendDirective:
|
||||
enabled: false
|
||||
|
||||
FinalNewline:
|
||||
enabled: true
|
||||
present: true
|
||||
|
||||
HexLength:
|
||||
enabled: true
|
||||
style: short # or 'long'
|
||||
|
||||
HexNotation:
|
||||
enabled: true
|
||||
style: lowercase # or 'uppercase'
|
||||
|
||||
HexValidation:
|
||||
enabled: true
|
||||
|
||||
IdSelector:
|
||||
enabled: true
|
||||
|
||||
ImportantRule:
|
||||
enabled: false
|
||||
|
||||
ImportPath:
|
||||
enabled: true
|
||||
leading_underscore: false
|
||||
filename_extension: false
|
||||
|
||||
Indentation:
|
||||
enabled: true
|
||||
allow_non_nested_indentation: false
|
||||
character: space # or 'tab'
|
||||
width: 2
|
||||
|
||||
LeadingZero:
|
||||
enabled: false
|
||||
|
||||
MergeableSelector:
|
||||
enabled: true
|
||||
force_nesting: true
|
||||
|
||||
NameFormat:
|
||||
enabled: true
|
||||
allow_leading_underscore: true
|
||||
convention: hyphenated_lowercase # or 'camel_case', or 'snake_case', or a regex pattern
|
||||
|
||||
NestingDepth:
|
||||
enabled: true
|
||||
max_depth: 6
|
||||
ignore_parent_selectors: false
|
||||
|
||||
PlaceholderInExtend:
|
||||
enabled: true
|
||||
|
||||
PrivateNamingConvention:
|
||||
enabled: false
|
||||
prefix: _
|
||||
|
||||
PropertyCount:
|
||||
enabled: false
|
||||
include_nested: false
|
||||
max_properties: 10
|
||||
|
||||
PropertySortOrder:
|
||||
enabled: true
|
||||
ignore_unspecified: false
|
||||
min_properties: 2
|
||||
separate_groups: false
|
||||
|
||||
PropertySpelling:
|
||||
enabled: true
|
||||
extra_properties: []
|
||||
disabled_properties: []
|
||||
|
||||
PropertyUnits:
|
||||
enabled: true
|
||||
global: [
|
||||
'ch',
|
||||
'em',
|
||||
'ex',
|
||||
'rem', # Font-relative lengths
|
||||
'cm',
|
||||
'in',
|
||||
'mm',
|
||||
'pc',
|
||||
'pt',
|
||||
'px',
|
||||
'q', # Absolute lengths
|
||||
'vh',
|
||||
'vw',
|
||||
'vmin',
|
||||
'vmax', # Viewport-percentage lengths
|
||||
'fr', # Grid fractional lengths
|
||||
'deg',
|
||||
'grad',
|
||||
'rad',
|
||||
'turn', # Angle
|
||||
'ms',
|
||||
's', # Duration
|
||||
'Hz',
|
||||
'kHz', # Frequency
|
||||
'dpi',
|
||||
'dpcm',
|
||||
'dppx', # Resolution
|
||||
'%',
|
||||
] # Other
|
||||
properties: {}
|
||||
|
||||
PseudoElement:
|
||||
enabled: true
|
||||
|
||||
QualifyingElement:
|
||||
enabled: true
|
||||
allow_element_with_attribute: false
|
||||
allow_element_with_class: false
|
||||
allow_element_with_id: false
|
||||
exclude:
|
||||
- 'app/assets/stylesheets/administrate/components/_buttons.scss'
|
||||
|
||||
SelectorDepth:
|
||||
enabled: true
|
||||
max_depth: 5
|
||||
|
||||
SelectorFormat:
|
||||
enabled: false
|
||||
|
||||
Shorthand:
|
||||
enabled: true
|
||||
allowed_shorthands: [1, 2, 3, 4]
|
||||
|
||||
SingleLinePerProperty:
|
||||
enabled: true
|
||||
allow_single_line_rule_sets: true
|
||||
|
||||
SingleLinePerSelector:
|
||||
enabled: true
|
||||
|
||||
SpaceAfterComma:
|
||||
enabled: true
|
||||
style: one_space # or 'no_space', or 'at_least_one_space'
|
||||
|
||||
SpaceAfterComment:
|
||||
enabled: false
|
||||
style: one_space # or 'no_space', or 'at_least_one_space'
|
||||
allow_empty_comments: true
|
||||
|
||||
SpaceAfterPropertyColon:
|
||||
enabled: true
|
||||
style: one_space # or 'no_space', or 'at_least_one_space', or 'aligned'
|
||||
|
||||
SpaceAfterPropertyName:
|
||||
enabled: true
|
||||
|
||||
SpaceAfterVariableColon:
|
||||
enabled: false
|
||||
style: one_space # or 'no_space', 'at_least_one_space' or 'one_space_or_newline'
|
||||
|
||||
SpaceAfterVariableName:
|
||||
enabled: true
|
||||
|
||||
SpaceAroundOperator:
|
||||
enabled: true
|
||||
style: one_space # or 'at_least_one_space', or 'no_space'
|
||||
|
||||
SpaceBeforeBrace:
|
||||
enabled: true
|
||||
style: space # or 'new_line'
|
||||
allow_single_line_padding: false
|
||||
|
||||
SpaceBetweenParens:
|
||||
enabled: true
|
||||
spaces: 0
|
||||
|
||||
StringQuotes:
|
||||
enabled: true
|
||||
style: single_quotes # or double_quotes
|
||||
|
||||
TrailingSemicolon:
|
||||
enabled: true
|
||||
|
||||
TrailingWhitespace:
|
||||
enabled: true
|
||||
|
||||
TrailingZero:
|
||||
enabled: false
|
||||
|
||||
TransitionAll:
|
||||
enabled: false
|
||||
|
||||
UnnecessaryMantissa:
|
||||
enabled: false
|
||||
|
||||
UnnecessaryParentReference:
|
||||
enabled: false
|
||||
|
||||
UrlFormat:
|
||||
enabled: true
|
||||
|
||||
UrlQuotes:
|
||||
enabled: true
|
||||
|
||||
VariableForProperty:
|
||||
enabled: false
|
||||
properties: []
|
||||
|
||||
VendorPrefix:
|
||||
enabled: true
|
||||
identifier_list: base
|
||||
additional_identifiers: []
|
||||
excluded_identifiers: []
|
||||
|
||||
ZeroUnit:
|
||||
enabled: true
|
||||
|
||||
Compass::*:
|
||||
enabled: false
|
||||
|
||||
exclude:
|
||||
- 'app/javascript/widget/assets/scss/_reset.scss'
|
||||
- 'app/javascript/widget/assets/scss/sdk.css'
|
||||
- 'app/assets/stylesheets/administrate/reset/_normalize.scss'
|
||||
- 'app/javascript/shared/assets/stylesheets/*.scss'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
source 'https://rubygems.org'
|
||||
|
||||
ruby '2.6.5'
|
||||
ruby '2.7.1'
|
||||
|
||||
##-- base gems for rails --##
|
||||
gem 'rack-cors', require: 'rack/cors'
|
||||
@@ -8,7 +8,7 @@ gem 'rails'
|
||||
# Reduces boot times through caching; required in config/boot.rb
|
||||
gem 'bootsnap', require: false
|
||||
|
||||
##-- rails helper gems --##
|
||||
##-- rails application helper gems --##
|
||||
gem 'acts-as-taggable-on'
|
||||
gem 'attr_extras'
|
||||
gem 'browser'
|
||||
@@ -16,16 +16,28 @@ gem 'hashie'
|
||||
gem 'jbuilder'
|
||||
gem 'kaminari'
|
||||
gem 'responders'
|
||||
gem 'rest-client'
|
||||
gem 'telephone_number'
|
||||
gem 'time_diff'
|
||||
gem 'tzinfo-data'
|
||||
gem 'valid_email2'
|
||||
# compress javascript config.assets.js_compressor
|
||||
gem 'uglifier'
|
||||
##-- used for single column multiple binary flags in notification settings/feature flagging --##
|
||||
gem 'flag_shih_tzu'
|
||||
# Random name generator for user names
|
||||
gem 'haikunator'
|
||||
# Template parsing safetly
|
||||
gem 'liquid'
|
||||
|
||||
##-- for active storage --##
|
||||
gem 'aws-sdk-s3', require: false
|
||||
gem 'azure-storage-blob', require: false
|
||||
gem 'google-cloud-storage', require: false
|
||||
gem 'mini_magick'
|
||||
|
||||
##-- gems for database --#
|
||||
gem 'groupdate'
|
||||
gem 'pg'
|
||||
gem 'redis'
|
||||
gem 'redis-namespace'
|
||||
@@ -35,7 +47,9 @@ gem 'redis-rack-cache'
|
||||
gem 'dotenv-rails'
|
||||
gem 'foreman'
|
||||
gem 'puma'
|
||||
gem 'webpacker'
|
||||
gem 'webpacker', '~> 5.x'
|
||||
# metrics on heroku
|
||||
gem 'barnes'
|
||||
|
||||
##--- gems for authentication & authorization ---##
|
||||
gem 'devise'
|
||||
@@ -43,60 +57,79 @@ gem 'devise_token_auth'
|
||||
# authorization
|
||||
gem 'jwt'
|
||||
gem 'pundit'
|
||||
# super admin
|
||||
gem 'administrate'
|
||||
|
||||
##--- gems for pubsub service ---##
|
||||
# https://karolgalanciak.com/blog/2019/11/30/from-activerecord-callbacks-to-publish-slash-subscribe-pattern-and-event-driven-design/
|
||||
gem 'wisper', '2.0.0'
|
||||
|
||||
##--- gems for reporting ---##
|
||||
gem 'nightfury'
|
||||
|
||||
##--- gems for billing ---##
|
||||
gem 'chargebee'
|
||||
|
||||
##--- gems for channels ---##
|
||||
gem 'facebook-messenger'
|
||||
# TODO: bump up gem to 2.0
|
||||
gem 'facebook-messenger', '1.5.0'
|
||||
gem 'telegram-bot-ruby'
|
||||
gem 'twitter'
|
||||
gem 'twilio-ruby', '~> 5.32.0'
|
||||
# twitty will handle subscription of twitter account events
|
||||
# gem 'twitty', git: 'https://github.com/chatwoot/twitty'
|
||||
gem 'twitty'
|
||||
# facebook client
|
||||
gem 'koala'
|
||||
# Random name generator
|
||||
gem 'haikunator'
|
||||
# slack client
|
||||
gem 'slack-ruby-client'
|
||||
|
||||
##--- gems for debugging and error reporting ---##
|
||||
# static analysis
|
||||
gem 'brakeman'
|
||||
gem 'scout_apm'
|
||||
gem 'sentry-raven'
|
||||
|
||||
##-- TODO: move these gems to appropriate groups --##
|
||||
# remove this gem in favor of active storage - github #158
|
||||
gem 'carrierwave-aws'
|
||||
##-- background job processing --##
|
||||
gem 'sidekiq'
|
||||
|
||||
##-- Push notification service --##
|
||||
gem 'fcm'
|
||||
gem 'webpush'
|
||||
|
||||
group :development do
|
||||
gem 'annotate'
|
||||
gem 'bullet'
|
||||
gem 'letter_opener'
|
||||
gem 'web-console'
|
||||
|
||||
# used in swagger build
|
||||
gem 'json_refs', git: 'https://github.com/tzmfreedom/json_refs', ref: 'e32deb0'
|
||||
|
||||
# When we want to squash migrations
|
||||
gem 'squasher'
|
||||
end
|
||||
|
||||
group :test do
|
||||
# Cypress in rails.
|
||||
gem 'cypress-on-rails', '~> 1.0'
|
||||
# fast cleaning of database
|
||||
gem 'database_cleaner'
|
||||
end
|
||||
|
||||
group :development, :test do
|
||||
# locking until https://github.com/codeclimate/test-reporter/issues/418 is resolved
|
||||
gem 'action-cable-testing'
|
||||
gem 'bundle-audit', require: false
|
||||
gem 'byebug', platform: :mri
|
||||
gem 'factory_bot_rails'
|
||||
gem 'faker'
|
||||
gem 'listen'
|
||||
gem 'mock_redis'
|
||||
gem 'mock_redis', git: 'https://github.com/sds/mock_redis', ref: '16d00789f0341a3aac35126c0ffe97a596753ff9'
|
||||
gem 'pry-rails'
|
||||
gem 'rspec-rails', '~> 4.0.0.beta2'
|
||||
gem 'rubocop', require: false
|
||||
gem 'rubocop-performance', require: false
|
||||
gem 'rubocop-rails', require: false
|
||||
gem 'rubocop-rspec', require: false
|
||||
gem 'scss_lint', require: false
|
||||
gem 'seed_dump'
|
||||
gem 'shoulda-matchers'
|
||||
gem 'simplecov', require: false
|
||||
gem 'simplecov', '0.17.1', require: false
|
||||
gem 'spring'
|
||||
gem 'spring-watcher-listen'
|
||||
gem 'webmock'
|
||||
end
|
||||
|
||||
+374
-241
@@ -1,313 +1,386 @@
|
||||
GIT
|
||||
remote: https://github.com/sds/mock_redis
|
||||
revision: 16d00789f0341a3aac35126c0ffe97a596753ff9
|
||||
ref: 16d00789f0341a3aac35126c0ffe97a596753ff9
|
||||
specs:
|
||||
mock_redis (0.22.0)
|
||||
|
||||
GIT
|
||||
remote: https://github.com/tzmfreedom/json_refs
|
||||
revision: e32deb073ce9aef39bdd63556bffd7fe7c2a803d
|
||||
ref: e32deb0
|
||||
specs:
|
||||
json_refs (0.1.2)
|
||||
hana
|
||||
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
action-cable-testing (0.6.0)
|
||||
action-cable-testing (0.6.1)
|
||||
actioncable (>= 5.0)
|
||||
actioncable (6.0.2.1)
|
||||
actionpack (= 6.0.2.1)
|
||||
actioncable (6.0.3.4)
|
||||
actionpack (= 6.0.3.4)
|
||||
nio4r (~> 2.0)
|
||||
websocket-driver (>= 0.6.1)
|
||||
actionmailbox (6.0.2.1)
|
||||
actionpack (= 6.0.2.1)
|
||||
activejob (= 6.0.2.1)
|
||||
activerecord (= 6.0.2.1)
|
||||
activestorage (= 6.0.2.1)
|
||||
activesupport (= 6.0.2.1)
|
||||
actionmailbox (6.0.3.4)
|
||||
actionpack (= 6.0.3.4)
|
||||
activejob (= 6.0.3.4)
|
||||
activerecord (= 6.0.3.4)
|
||||
activestorage (= 6.0.3.4)
|
||||
activesupport (= 6.0.3.4)
|
||||
mail (>= 2.7.1)
|
||||
actionmailer (6.0.2.1)
|
||||
actionpack (= 6.0.2.1)
|
||||
actionview (= 6.0.2.1)
|
||||
activejob (= 6.0.2.1)
|
||||
actionmailer (6.0.3.4)
|
||||
actionpack (= 6.0.3.4)
|
||||
actionview (= 6.0.3.4)
|
||||
activejob (= 6.0.3.4)
|
||||
mail (~> 2.5, >= 2.5.4)
|
||||
rails-dom-testing (~> 2.0)
|
||||
actionpack (6.0.2.1)
|
||||
actionview (= 6.0.2.1)
|
||||
activesupport (= 6.0.2.1)
|
||||
actionpack (6.0.3.4)
|
||||
actionview (= 6.0.3.4)
|
||||
activesupport (= 6.0.3.4)
|
||||
rack (~> 2.0, >= 2.0.8)
|
||||
rack-test (>= 0.6.3)
|
||||
rails-dom-testing (~> 2.0)
|
||||
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
||||
actiontext (6.0.2.1)
|
||||
actionpack (= 6.0.2.1)
|
||||
activerecord (= 6.0.2.1)
|
||||
activestorage (= 6.0.2.1)
|
||||
activesupport (= 6.0.2.1)
|
||||
actiontext (6.0.3.4)
|
||||
actionpack (= 6.0.3.4)
|
||||
activerecord (= 6.0.3.4)
|
||||
activestorage (= 6.0.3.4)
|
||||
activesupport (= 6.0.3.4)
|
||||
nokogiri (>= 1.8.5)
|
||||
actionview (6.0.2.1)
|
||||
activesupport (= 6.0.2.1)
|
||||
actionview (6.0.3.4)
|
||||
activesupport (= 6.0.3.4)
|
||||
builder (~> 3.1)
|
||||
erubi (~> 1.4)
|
||||
rails-dom-testing (~> 2.0)
|
||||
rails-html-sanitizer (~> 1.1, >= 1.2.0)
|
||||
activejob (6.0.2.1)
|
||||
activesupport (= 6.0.2.1)
|
||||
activejob (6.0.3.4)
|
||||
activesupport (= 6.0.3.4)
|
||||
globalid (>= 0.3.6)
|
||||
activemodel (6.0.2.1)
|
||||
activesupport (= 6.0.2.1)
|
||||
activerecord (6.0.2.1)
|
||||
activemodel (= 6.0.2.1)
|
||||
activesupport (= 6.0.2.1)
|
||||
activestorage (6.0.2.1)
|
||||
actionpack (= 6.0.2.1)
|
||||
activejob (= 6.0.2.1)
|
||||
activerecord (= 6.0.2.1)
|
||||
activemodel (6.0.3.4)
|
||||
activesupport (= 6.0.3.4)
|
||||
activerecord (6.0.3.4)
|
||||
activemodel (= 6.0.3.4)
|
||||
activesupport (= 6.0.3.4)
|
||||
activestorage (6.0.3.4)
|
||||
actionpack (= 6.0.3.4)
|
||||
activejob (= 6.0.3.4)
|
||||
activerecord (= 6.0.3.4)
|
||||
marcel (~> 0.3.1)
|
||||
activesupport (6.0.2.1)
|
||||
activesupport (6.0.3.4)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
i18n (>= 0.7, < 2)
|
||||
minitest (~> 5.1)
|
||||
tzinfo (~> 1.1)
|
||||
zeitwerk (~> 2.2)
|
||||
zeitwerk (~> 2.2, >= 2.2.2)
|
||||
acts-as-taggable-on (6.5.0)
|
||||
activerecord (>= 5.0, < 6.1)
|
||||
addressable (2.7.0)
|
||||
public_suffix (>= 2.0.2, < 5.0)
|
||||
annotate (3.0.3)
|
||||
administrate (0.14.0)
|
||||
actionpack (>= 4.2)
|
||||
actionview (>= 4.2)
|
||||
activerecord (>= 4.2)
|
||||
autoprefixer-rails (>= 6.0)
|
||||
datetime_picker_rails (~> 0.0.7)
|
||||
jquery-rails (>= 4.0)
|
||||
kaminari (>= 1.0)
|
||||
momentjs-rails (~> 2.8)
|
||||
sassc-rails (~> 2.1)
|
||||
selectize-rails (~> 0.6)
|
||||
annotate (3.1.1)
|
||||
activerecord (>= 3.2, < 7.0)
|
||||
rake (>= 10.4, < 14.0)
|
||||
ast (2.4.0)
|
||||
attr_extras (6.2.1)
|
||||
aws-eventstream (1.0.3)
|
||||
aws-partitions (1.259.0)
|
||||
aws-sdk-core (3.86.0)
|
||||
aws-eventstream (~> 1.0, >= 1.0.2)
|
||||
ast (2.4.1)
|
||||
attr_extras (6.2.4)
|
||||
autoprefixer-rails (9.8.6.3)
|
||||
execjs
|
||||
aws-eventstream (1.1.0)
|
||||
aws-partitions (1.360.0)
|
||||
aws-sdk-core (3.105.0)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
aws-partitions (~> 1, >= 1.239.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
jmespath (~> 1.0)
|
||||
aws-sdk-kms (1.27.0)
|
||||
aws-sdk-core (~> 3, >= 3.71.0)
|
||||
aws-sdk-kms (1.37.0)
|
||||
aws-sdk-core (~> 3, >= 3.99.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-sdk-s3 (1.60.1)
|
||||
aws-sdk-core (~> 3, >= 3.83.0)
|
||||
aws-sdk-s3 (1.79.1)
|
||||
aws-sdk-core (~> 3, >= 3.104.3)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-sigv4 (1.1.0)
|
||||
aws-eventstream (~> 1.0, >= 1.0.2)
|
||||
aws-sigv4 (1.2.2)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
axiom-types (0.1.1)
|
||||
descendants_tracker (~> 0.0.4)
|
||||
ice_nine (~> 0.11.0)
|
||||
thread_safe (~> 0.3, >= 0.3.1)
|
||||
bcrypt (3.1.13)
|
||||
azure-storage-blob (2.0.0)
|
||||
azure-storage-common (~> 2.0)
|
||||
nokogiri (~> 1.10.4)
|
||||
azure-storage-common (2.0.1)
|
||||
faraday (~> 1.0)
|
||||
faraday_middleware (~> 1.0.0.rc1)
|
||||
nokogiri (~> 1.10.4)
|
||||
barnes (0.0.8)
|
||||
multi_json (~> 1)
|
||||
statsd-ruby (~> 1.1)
|
||||
bcrypt (3.1.15)
|
||||
bindex (0.8.1)
|
||||
bootsnap (1.4.5)
|
||||
bootsnap (1.4.8)
|
||||
msgpack (~> 1.0)
|
||||
brakeman (4.7.2)
|
||||
browser (2.7.1)
|
||||
buftok (0.2.0)
|
||||
brakeman (4.9.0)
|
||||
browser (4.2.0)
|
||||
builder (3.2.4)
|
||||
bullet (6.0.2)
|
||||
bullet (6.1.0)
|
||||
activesupport (>= 3.0.0)
|
||||
uniform_notifier (~> 1.11)
|
||||
bundle-audit (0.1.0)
|
||||
bundler-audit
|
||||
bundler-audit (0.6.1)
|
||||
bundler-audit (0.7.0.1)
|
||||
bundler (>= 1.2.0, < 3)
|
||||
thor (~> 0.18)
|
||||
byebug (11.0.1)
|
||||
carrierwave (2.0.2)
|
||||
activemodel (>= 5.0.0)
|
||||
activesupport (>= 5.0.0)
|
||||
addressable (~> 2.6)
|
||||
image_processing (~> 1.1)
|
||||
mimemagic (>= 0.3.0)
|
||||
mini_mime (>= 0.1.3)
|
||||
carrierwave-aws (1.4.0)
|
||||
aws-sdk-s3 (~> 1.0)
|
||||
carrierwave (>= 0.7, < 2.1)
|
||||
chargebee (2.7.1)
|
||||
json_pure (~> 2.1)
|
||||
rest-client (>= 1.8, < 3.0)
|
||||
coderay (1.1.2)
|
||||
thor (>= 0.18, < 2)
|
||||
byebug (11.1.3)
|
||||
coderay (1.1.3)
|
||||
coercible (1.0.0)
|
||||
descendants_tracker (~> 0.0.1)
|
||||
concurrent-ruby (1.1.5)
|
||||
connection_pool (2.2.2)
|
||||
crass (1.0.5)
|
||||
concurrent-ruby (1.1.7)
|
||||
connection_pool (2.2.3)
|
||||
crack (0.4.3)
|
||||
safe_yaml (~> 1.0.0)
|
||||
crass (1.0.6)
|
||||
cypress-on-rails (1.8.0)
|
||||
rack
|
||||
database_cleaner (1.8.5)
|
||||
datetime_picker_rails (0.0.7)
|
||||
momentjs-rails (>= 2.8.1)
|
||||
declarative (0.0.20)
|
||||
declarative-option (0.1.0)
|
||||
descendants_tracker (0.0.4)
|
||||
thread_safe (~> 0.3, >= 0.3.1)
|
||||
devise (4.7.1)
|
||||
devise (4.7.2)
|
||||
bcrypt (~> 3.0)
|
||||
orm_adapter (~> 0.1)
|
||||
railties (>= 4.1.0)
|
||||
responders
|
||||
warden (~> 1.2.3)
|
||||
devise_token_auth (1.1.3)
|
||||
devise_token_auth (1.1.4)
|
||||
bcrypt (~> 3.0)
|
||||
devise (> 3.5.2, < 5)
|
||||
rails (>= 4.2.0, < 6.1)
|
||||
diff-lcs (1.3)
|
||||
sprockets (= 3.7.2)
|
||||
diff-lcs (1.4.4)
|
||||
digest-crc (0.6.1)
|
||||
rake (~> 13.0)
|
||||
docile (1.3.2)
|
||||
domain_name (0.5.20190701)
|
||||
unf (>= 0.0.5, < 1.0.0)
|
||||
dotenv (2.7.5)
|
||||
dotenv-rails (2.7.5)
|
||||
dotenv (= 2.7.5)
|
||||
railties (>= 3.2, < 6.1)
|
||||
dotenv (2.7.6)
|
||||
dotenv-rails (2.7.6)
|
||||
dotenv (= 2.7.6)
|
||||
railties (>= 3.2)
|
||||
equalizer (0.0.11)
|
||||
erubi (1.9.0)
|
||||
execjs (2.7.0)
|
||||
facebook-messenger (1.4.1)
|
||||
facebook-messenger (1.5.0)
|
||||
httparty (~> 0.13, >= 0.13.7)
|
||||
rack (>= 1.4.5)
|
||||
factory_bot (5.1.1)
|
||||
activesupport (>= 4.2.0)
|
||||
factory_bot_rails (5.1.1)
|
||||
factory_bot (~> 5.1.0)
|
||||
railties (>= 4.2.0)
|
||||
faker (2.9.0)
|
||||
i18n (>= 1.6, < 1.8)
|
||||
faraday (0.17.1)
|
||||
factory_bot (6.1.0)
|
||||
activesupport (>= 5.0.0)
|
||||
factory_bot_rails (6.1.0)
|
||||
factory_bot (~> 6.1.0)
|
||||
railties (>= 5.0.0)
|
||||
faker (2.13.0)
|
||||
i18n (>= 1.6, < 2)
|
||||
faraday (1.0.1)
|
||||
multipart-post (>= 1.2, < 3)
|
||||
ffi (1.11.3)
|
||||
foreman (0.86.0)
|
||||
faraday_middleware (1.0.0)
|
||||
faraday (~> 1.0)
|
||||
fcm (1.0.2)
|
||||
faraday (~> 1.0.0)
|
||||
ffi (1.13.1)
|
||||
flag_shih_tzu (0.3.23)
|
||||
foreman (0.87.2)
|
||||
gli (2.19.2)
|
||||
globalid (0.4.2)
|
||||
activesupport (>= 4.2.0)
|
||||
google-api-client (0.43.0)
|
||||
addressable (~> 2.5, >= 2.5.1)
|
||||
googleauth (~> 0.9)
|
||||
httpclient (>= 2.8.1, < 3.0)
|
||||
mini_mime (~> 1.0)
|
||||
representable (~> 3.0)
|
||||
retriable (>= 2.0, < 4.0)
|
||||
signet (~> 0.12)
|
||||
google-cloud-core (1.5.0)
|
||||
google-cloud-env (~> 1.0)
|
||||
google-cloud-errors (~> 1.0)
|
||||
google-cloud-env (1.3.3)
|
||||
faraday (>= 0.17.3, < 2.0)
|
||||
google-cloud-errors (1.0.1)
|
||||
google-cloud-storage (1.28.0)
|
||||
addressable (~> 2.5)
|
||||
digest-crc (~> 0.4)
|
||||
google-api-client (~> 0.33)
|
||||
google-cloud-core (~> 1.2)
|
||||
googleauth (~> 0.9)
|
||||
mini_mime (~> 1.0)
|
||||
googleauth (0.13.1)
|
||||
faraday (>= 0.17.3, < 2.0)
|
||||
jwt (>= 1.4, < 3.0)
|
||||
memoist (~> 0.16)
|
||||
multi_json (~> 1.11)
|
||||
os (>= 0.9, < 2.0)
|
||||
signet (~> 0.14)
|
||||
groupdate (5.1.0)
|
||||
activesupport (>= 5)
|
||||
haikunator (1.1.0)
|
||||
hashie (4.0.0)
|
||||
http (3.3.0)
|
||||
addressable (~> 2.3)
|
||||
http-cookie (~> 1.0)
|
||||
http-form_data (~> 2.0)
|
||||
http_parser.rb (~> 0.6.0)
|
||||
hana (1.3.6)
|
||||
hashdiff (1.0.1)
|
||||
hashie (4.1.0)
|
||||
hkdf (0.3.0)
|
||||
http-accept (1.7.0)
|
||||
http-cookie (1.0.3)
|
||||
domain_name (~> 0.5)
|
||||
http-form_data (2.1.1)
|
||||
http_parser.rb (0.6.0)
|
||||
httparty (0.17.3)
|
||||
httparty (0.18.1)
|
||||
mime-types (~> 3.0)
|
||||
multi_xml (>= 0.5.2)
|
||||
i18n (1.7.0)
|
||||
httpclient (2.8.3)
|
||||
i18n (1.8.5)
|
||||
concurrent-ruby (~> 1.0)
|
||||
ice_nine (0.11.2)
|
||||
image_processing (1.10.0)
|
||||
mini_magick (>= 4.9.5, < 5)
|
||||
ruby-vips (>= 2.0.13, < 3)
|
||||
inflecto (0.0.2)
|
||||
jaro_winkler (1.5.4)
|
||||
jbuilder (2.9.1)
|
||||
activesupport (>= 4.2.0)
|
||||
jbuilder (2.10.0)
|
||||
activesupport (>= 5.0.0)
|
||||
jmespath (1.4.0)
|
||||
json (2.3.0)
|
||||
json_pure (2.2.0)
|
||||
jwt (2.2.1)
|
||||
kaminari (1.1.1)
|
||||
jquery-rails (4.4.0)
|
||||
rails-dom-testing (>= 1, < 3)
|
||||
railties (>= 4.2.0)
|
||||
thor (>= 0.14, < 2.0)
|
||||
json (2.3.1)
|
||||
jwt (2.2.2)
|
||||
kaminari (1.2.1)
|
||||
activesupport (>= 4.1.0)
|
||||
kaminari-actionview (= 1.1.1)
|
||||
kaminari-activerecord (= 1.1.1)
|
||||
kaminari-core (= 1.1.1)
|
||||
kaminari-actionview (1.1.1)
|
||||
kaminari-actionview (= 1.2.1)
|
||||
kaminari-activerecord (= 1.2.1)
|
||||
kaminari-core (= 1.2.1)
|
||||
kaminari-actionview (1.2.1)
|
||||
actionview
|
||||
kaminari-core (= 1.1.1)
|
||||
kaminari-activerecord (1.1.1)
|
||||
kaminari-core (= 1.2.1)
|
||||
kaminari-activerecord (1.2.1)
|
||||
activerecord
|
||||
kaminari-core (= 1.1.1)
|
||||
kaminari-core (1.1.1)
|
||||
kaminari-core (= 1.2.1)
|
||||
kaminari-core (1.2.1)
|
||||
koala (3.0.0)
|
||||
addressable
|
||||
faraday
|
||||
json (>= 1.8)
|
||||
launchy (2.4.3)
|
||||
addressable (~> 2.3)
|
||||
launchy (2.5.0)
|
||||
addressable (~> 2.7)
|
||||
letter_opener (1.7.0)
|
||||
launchy (~> 2.2)
|
||||
liquid (4.0.3)
|
||||
listen (3.2.1)
|
||||
rb-fsevent (~> 0.10, >= 0.10.3)
|
||||
rb-inotify (~> 0.9, >= 0.9.10)
|
||||
loofah (2.4.0)
|
||||
loofah (2.7.0)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.5.9)
|
||||
mail (2.7.1)
|
||||
mini_mime (>= 0.1.1)
|
||||
marcel (0.3.3)
|
||||
mimemagic (~> 0.3.2)
|
||||
memoizable (0.4.2)
|
||||
thread_safe (~> 0.3, >= 0.3.1)
|
||||
method_source (0.9.2)
|
||||
mime-types (3.3)
|
||||
memoist (0.16.2)
|
||||
method_source (1.0.0)
|
||||
mime-types (3.3.1)
|
||||
mime-types-data (~> 3.2015)
|
||||
mime-types-data (3.2019.1009)
|
||||
mimemagic (0.3.3)
|
||||
mini_magick (4.9.5)
|
||||
mime-types-data (3.2020.0512)
|
||||
mimemagic (0.3.5)
|
||||
mini_magick (4.10.1)
|
||||
mini_mime (1.0.2)
|
||||
mini_portile2 (2.4.0)
|
||||
minitest (5.13.0)
|
||||
mock_redis (0.22.0)
|
||||
msgpack (1.3.1)
|
||||
minitest (5.14.2)
|
||||
momentjs-rails (2.20.1)
|
||||
railties (>= 3.1)
|
||||
msgpack (1.3.3)
|
||||
multi_json (1.15.0)
|
||||
multi_xml (0.6.0)
|
||||
multipart-post (2.1.1)
|
||||
naught (1.1.0)
|
||||
netrc (0.11.0)
|
||||
nightfury (1.0.1)
|
||||
nio4r (2.5.2)
|
||||
nokogiri (1.10.7)
|
||||
nio4r (2.5.4)
|
||||
nokogiri (1.10.10)
|
||||
mini_portile2 (~> 2.4.0)
|
||||
oauth (0.5.4)
|
||||
orm_adapter (0.5.0)
|
||||
parallel (1.19.1)
|
||||
parser (2.6.5.0)
|
||||
ast (~> 2.4.0)
|
||||
pg (1.1.4)
|
||||
pry (0.12.2)
|
||||
coderay (~> 1.1.0)
|
||||
method_source (~> 0.9.0)
|
||||
os (1.1.1)
|
||||
parallel (1.19.2)
|
||||
parser (2.7.1.4)
|
||||
ast (~> 2.4.1)
|
||||
pg (1.2.3)
|
||||
pry (0.13.1)
|
||||
coderay (~> 1.1)
|
||||
method_source (~> 1.0)
|
||||
pry-rails (0.3.9)
|
||||
pry (>= 0.10.4)
|
||||
public_suffix (4.0.1)
|
||||
puma (4.3.1)
|
||||
public_suffix (4.0.5)
|
||||
puma (4.3.5)
|
||||
nio4r (~> 2.0)
|
||||
pundit (2.1.0)
|
||||
activesupport (>= 3.0.0)
|
||||
rack (2.0.8)
|
||||
rack-cache (1.10.0)
|
||||
rack (2.2.3)
|
||||
rack-cache (1.12.0)
|
||||
rack (>= 0.4)
|
||||
rack-cors (1.1.0)
|
||||
rack-cors (1.1.1)
|
||||
rack (>= 2.0.0)
|
||||
rack-protection (2.0.7)
|
||||
rack
|
||||
rack-proxy (0.6.5)
|
||||
rack
|
||||
rack-test (1.1.0)
|
||||
rack (>= 1.0, < 3)
|
||||
rails (6.0.2.1)
|
||||
actioncable (= 6.0.2.1)
|
||||
actionmailbox (= 6.0.2.1)
|
||||
actionmailer (= 6.0.2.1)
|
||||
actionpack (= 6.0.2.1)
|
||||
actiontext (= 6.0.2.1)
|
||||
actionview (= 6.0.2.1)
|
||||
activejob (= 6.0.2.1)
|
||||
activemodel (= 6.0.2.1)
|
||||
activerecord (= 6.0.2.1)
|
||||
activestorage (= 6.0.2.1)
|
||||
activesupport (= 6.0.2.1)
|
||||
rails (6.0.3.4)
|
||||
actioncable (= 6.0.3.4)
|
||||
actionmailbox (= 6.0.3.4)
|
||||
actionmailer (= 6.0.3.4)
|
||||
actionpack (= 6.0.3.4)
|
||||
actiontext (= 6.0.3.4)
|
||||
actionview (= 6.0.3.4)
|
||||
activejob (= 6.0.3.4)
|
||||
activemodel (= 6.0.3.4)
|
||||
activerecord (= 6.0.3.4)
|
||||
activestorage (= 6.0.3.4)
|
||||
activesupport (= 6.0.3.4)
|
||||
bundler (>= 1.3.0)
|
||||
railties (= 6.0.2.1)
|
||||
railties (= 6.0.3.4)
|
||||
sprockets-rails (>= 2.0.0)
|
||||
rails-dom-testing (2.0.3)
|
||||
activesupport (>= 4.2.0)
|
||||
nokogiri (>= 1.6)
|
||||
rails-html-sanitizer (1.3.0)
|
||||
loofah (~> 2.3)
|
||||
railties (6.0.2.1)
|
||||
actionpack (= 6.0.2.1)
|
||||
activesupport (= 6.0.2.1)
|
||||
railties (6.0.3.4)
|
||||
actionpack (= 6.0.3.4)
|
||||
activesupport (= 6.0.3.4)
|
||||
method_source
|
||||
rake (>= 0.8.7)
|
||||
thor (>= 0.20.3, < 2.0)
|
||||
rainbow (3.0.0)
|
||||
rake (13.0.1)
|
||||
rb-fsevent (0.10.3)
|
||||
rb-inotify (0.10.0)
|
||||
rb-fsevent (0.10.4)
|
||||
rb-inotify (0.10.1)
|
||||
ffi (~> 1.0)
|
||||
redis (4.1.3)
|
||||
redis-namespace (1.7.0)
|
||||
redis (4.2.1)
|
||||
redis-namespace (1.8.0)
|
||||
redis (>= 3.0.4)
|
||||
redis-rack-cache (2.2.1)
|
||||
rack-cache (>= 1.10, < 2)
|
||||
redis-store (>= 1.6, < 2)
|
||||
redis-store (1.8.1)
|
||||
redis-store (1.9.0)
|
||||
redis (>= 4, < 5)
|
||||
responders (3.0.0)
|
||||
regexp_parser (1.7.1)
|
||||
representable (3.0.4)
|
||||
declarative (< 0.1.0)
|
||||
declarative-option (< 0.2.0)
|
||||
uber (< 0.2.0)
|
||||
responders (3.0.1)
|
||||
actionpack (>= 5.0)
|
||||
railties (>= 5.0)
|
||||
rest-client (2.1.0)
|
||||
@@ -315,101 +388,135 @@ GEM
|
||||
http-cookie (>= 1.0.2, < 2.0)
|
||||
mime-types (>= 1.16, < 4.0)
|
||||
netrc (~> 0.8)
|
||||
rspec-core (3.9.0)
|
||||
rspec-support (~> 3.9.0)
|
||||
rspec-expectations (3.9.0)
|
||||
retriable (3.1.2)
|
||||
rexml (3.2.4)
|
||||
rspec-core (3.9.2)
|
||||
rspec-support (~> 3.9.3)
|
||||
rspec-expectations (3.9.2)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.9.0)
|
||||
rspec-mocks (3.9.0)
|
||||
rspec-mocks (3.9.1)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.9.0)
|
||||
rspec-rails (4.0.0.beta3)
|
||||
rspec-rails (4.0.1)
|
||||
actionpack (>= 4.2)
|
||||
activesupport (>= 4.2)
|
||||
railties (>= 4.2)
|
||||
rspec-core (~> 3.8)
|
||||
rspec-expectations (~> 3.8)
|
||||
rspec-mocks (~> 3.8)
|
||||
rspec-support (~> 3.8)
|
||||
rspec-support (3.9.0)
|
||||
rubocop (0.78.0)
|
||||
jaro_winkler (~> 1.5.1)
|
||||
rspec-core (~> 3.9)
|
||||
rspec-expectations (~> 3.9)
|
||||
rspec-mocks (~> 3.9)
|
||||
rspec-support (~> 3.9)
|
||||
rspec-support (3.9.3)
|
||||
rubocop (0.89.1)
|
||||
parallel (~> 1.10)
|
||||
parser (>= 2.6)
|
||||
parser (>= 2.7.1.1)
|
||||
rainbow (>= 2.2.2, < 4.0)
|
||||
regexp_parser (>= 1.7)
|
||||
rexml
|
||||
rubocop-ast (>= 0.3.0, < 1.0)
|
||||
ruby-progressbar (~> 1.7)
|
||||
unicode-display_width (>= 1.4.0, < 1.7)
|
||||
rubocop-performance (1.5.1)
|
||||
rubocop (>= 0.71.0)
|
||||
rubocop-rails (2.4.0)
|
||||
unicode-display_width (>= 1.4.0, < 2.0)
|
||||
rubocop-ast (0.3.0)
|
||||
parser (>= 2.7.1.4)
|
||||
rubocop-performance (1.7.1)
|
||||
rubocop (>= 0.82.0)
|
||||
rubocop-rails (2.7.1)
|
||||
activesupport (>= 4.2.0)
|
||||
rack (>= 1.1)
|
||||
rubocop (>= 0.72.0)
|
||||
rubocop-rspec (1.37.1)
|
||||
rubocop (>= 0.68.1)
|
||||
rubocop (>= 0.87.0)
|
||||
rubocop-rspec (1.43.2)
|
||||
rubocop (~> 0.87)
|
||||
ruby-progressbar (1.10.1)
|
||||
ruby-vips (2.0.16)
|
||||
safe_yaml (1.0.5)
|
||||
sass (3.7.4)
|
||||
sass-listen (~> 4.0.0)
|
||||
sass-listen (4.0.0)
|
||||
rb-fsevent (~> 0.9, >= 0.9.4)
|
||||
rb-inotify (~> 0.9, >= 0.9.7)
|
||||
sassc (2.4.0)
|
||||
ffi (~> 1.9)
|
||||
sassc-rails (2.1.2)
|
||||
railties (>= 4.0.0)
|
||||
sassc (>= 2.0)
|
||||
sprockets (> 3.0)
|
||||
sprockets-rails
|
||||
tilt
|
||||
scout_apm (2.6.9)
|
||||
parser
|
||||
scss_lint (0.59.0)
|
||||
sass (~> 3.5, >= 3.5.5)
|
||||
seed_dump (3.3.1)
|
||||
activerecord (>= 4)
|
||||
activesupport (>= 4)
|
||||
sentry-raven (2.13.0)
|
||||
faraday (>= 0.7.6, < 1.0)
|
||||
shoulda-matchers (4.1.2)
|
||||
selectize-rails (0.12.6)
|
||||
semantic_range (2.3.0)
|
||||
sentry-raven (3.0.3)
|
||||
faraday (>= 1.0)
|
||||
shoulda-matchers (4.4.1)
|
||||
activesupport (>= 4.2.0)
|
||||
sidekiq (6.0.4)
|
||||
sidekiq (6.1.1)
|
||||
connection_pool (>= 2.2.2)
|
||||
rack (>= 2.0.0)
|
||||
rack-protection (>= 2.0.0)
|
||||
redis (>= 4.1.0)
|
||||
simple_oauth (0.3.1)
|
||||
rack (~> 2.0)
|
||||
redis (>= 4.2.0)
|
||||
signet (0.14.0)
|
||||
addressable (~> 2.3)
|
||||
faraday (>= 0.17.3, < 2.0)
|
||||
jwt (>= 1.5, < 3.0)
|
||||
multi_json (~> 1.10)
|
||||
simplecov (0.17.1)
|
||||
docile (~> 1.1)
|
||||
json (>= 1.8, < 3)
|
||||
simplecov-html (~> 0.10.0)
|
||||
simplecov-html (0.10.2)
|
||||
spring (2.1.0)
|
||||
slack-ruby-client (0.15.0)
|
||||
faraday (>= 1.0)
|
||||
faraday_middleware
|
||||
gli
|
||||
hashie
|
||||
websocket-driver
|
||||
spring (2.1.1)
|
||||
spring-watcher-listen (2.0.1)
|
||||
listen (>= 2.7, < 4.0)
|
||||
spring (>= 1.2, < 3.0)
|
||||
sprockets (4.0.0)
|
||||
sprockets (3.7.2)
|
||||
concurrent-ruby (~> 1.0)
|
||||
rack (> 1, < 3)
|
||||
sprockets-rails (3.2.1)
|
||||
sprockets-rails (3.2.2)
|
||||
actionpack (>= 4.0)
|
||||
activesupport (>= 4.0)
|
||||
sprockets (>= 3.0.0)
|
||||
telegram-bot-ruby (0.11.0)
|
||||
squasher (0.6.2)
|
||||
statsd-ruby (1.4.0)
|
||||
telegram-bot-ruby (0.12.0)
|
||||
faraday
|
||||
inflecto
|
||||
virtus
|
||||
thor (0.20.3)
|
||||
telephone_number (1.4.9)
|
||||
thor (1.0.1)
|
||||
thread_safe (0.3.6)
|
||||
tilt (2.0.10)
|
||||
time_diff (0.3.0)
|
||||
activesupport
|
||||
i18n
|
||||
twitter (6.2.0)
|
||||
addressable (~> 2.3)
|
||||
buftok (~> 0.2.0)
|
||||
equalizer (~> 0.0.11)
|
||||
http (~> 3.0)
|
||||
http-form_data (~> 2.0)
|
||||
http_parser.rb (~> 0.6.0)
|
||||
memoizable (~> 0.4.0)
|
||||
multipart-post (~> 2.0)
|
||||
naught (~> 1.0)
|
||||
simple_oauth (~> 0.3.0)
|
||||
tzinfo (1.2.5)
|
||||
twilio-ruby (5.32.0)
|
||||
faraday (~> 1.0.0)
|
||||
jwt (>= 1.5, <= 2.5)
|
||||
nokogiri (>= 1.6, < 2.0)
|
||||
twitty (0.1.1)
|
||||
oauth
|
||||
tzinfo (1.2.7)
|
||||
thread_safe (~> 0.1)
|
||||
tzinfo-data (1.2019.3)
|
||||
tzinfo-data (1.2020.1)
|
||||
tzinfo (>= 1.0.0)
|
||||
uber (0.1.0)
|
||||
uglifier (4.2.0)
|
||||
execjs (>= 0.3.0, < 3)
|
||||
unf (0.1.4)
|
||||
unf_ext
|
||||
unf_ext (0.0.7.6)
|
||||
unicode-display_width (1.6.0)
|
||||
unf_ext (0.0.7.7)
|
||||
unicode-display_width (1.7.0)
|
||||
uniform_notifier (1.13.0)
|
||||
valid_email2 (3.1.3)
|
||||
valid_email2 (3.3.1)
|
||||
activemodel (>= 3.2)
|
||||
mail (~> 2.5)
|
||||
virtus (1.0.5)
|
||||
@@ -419,20 +526,28 @@ GEM
|
||||
equalizer (~> 0.0, >= 0.0.9)
|
||||
warden (1.2.8)
|
||||
rack (>= 2.0.6)
|
||||
web-console (4.0.1)
|
||||
web-console (4.0.4)
|
||||
actionview (>= 6.0.0)
|
||||
activemodel (>= 6.0.0)
|
||||
bindex (>= 0.4.0)
|
||||
railties (>= 6.0.0)
|
||||
webpacker (4.2.2)
|
||||
activesupport (>= 4.2)
|
||||
webmock (3.8.3)
|
||||
addressable (>= 2.3.6)
|
||||
crack (>= 0.3.2)
|
||||
hashdiff (>= 0.4.0, < 2.0.0)
|
||||
webpacker (5.2.1)
|
||||
activesupport (>= 5.2)
|
||||
rack-proxy (>= 0.6.1)
|
||||
railties (>= 4.2)
|
||||
websocket-driver (0.7.1)
|
||||
railties (>= 5.2)
|
||||
semantic_range (>= 2.3.0)
|
||||
webpush (1.0.0)
|
||||
hkdf (~> 0.2)
|
||||
jwt (~> 2.0)
|
||||
websocket-driver (0.7.3)
|
||||
websocket-extensions (>= 0.1.0)
|
||||
websocket-extensions (0.1.4)
|
||||
websocket-extensions (0.1.5)
|
||||
wisper (2.0.0)
|
||||
zeitwerk (2.2.2)
|
||||
zeitwerk (2.4.0)
|
||||
|
||||
PLATFORMS
|
||||
ruby
|
||||
@@ -440,34 +555,43 @@ PLATFORMS
|
||||
DEPENDENCIES
|
||||
action-cable-testing
|
||||
acts-as-taggable-on
|
||||
administrate
|
||||
annotate
|
||||
attr_extras
|
||||
aws-sdk-s3
|
||||
azure-storage-blob
|
||||
barnes
|
||||
bootsnap
|
||||
brakeman
|
||||
browser
|
||||
bullet
|
||||
bundle-audit
|
||||
byebug
|
||||
carrierwave-aws
|
||||
chargebee
|
||||
cypress-on-rails (~> 1.0)
|
||||
database_cleaner
|
||||
devise
|
||||
devise_token_auth
|
||||
dotenv-rails
|
||||
facebook-messenger
|
||||
facebook-messenger (= 1.5.0)
|
||||
factory_bot_rails
|
||||
faker
|
||||
fcm
|
||||
flag_shih_tzu
|
||||
foreman
|
||||
google-cloud-storage
|
||||
groupdate
|
||||
haikunator
|
||||
hashie
|
||||
jbuilder
|
||||
json_refs!
|
||||
jwt
|
||||
kaminari
|
||||
koala
|
||||
letter_opener
|
||||
liquid
|
||||
listen
|
||||
mini_magick
|
||||
mock_redis
|
||||
nightfury
|
||||
mock_redis!
|
||||
pg
|
||||
pry-rails
|
||||
puma
|
||||
@@ -478,30 +602,39 @@ DEPENDENCIES
|
||||
redis-namespace
|
||||
redis-rack-cache
|
||||
responders
|
||||
rest-client
|
||||
rspec-rails (~> 4.0.0.beta2)
|
||||
rubocop
|
||||
rubocop-performance
|
||||
rubocop-rails
|
||||
rubocop-rspec
|
||||
scout_apm
|
||||
scss_lint
|
||||
seed_dump
|
||||
sentry-raven
|
||||
shoulda-matchers
|
||||
sidekiq
|
||||
simplecov
|
||||
simplecov (= 0.17.1)
|
||||
slack-ruby-client
|
||||
spring
|
||||
spring-watcher-listen
|
||||
squasher
|
||||
telegram-bot-ruby
|
||||
telephone_number
|
||||
time_diff
|
||||
twitter
|
||||
twilio-ruby (~> 5.32.0)
|
||||
twitty
|
||||
tzinfo-data
|
||||
uglifier
|
||||
valid_email2
|
||||
web-console
|
||||
webpacker
|
||||
webmock
|
||||
webpacker (~> 5.x)
|
||||
webpush
|
||||
wisper (= 2.0.0)
|
||||
|
||||
RUBY VERSION
|
||||
ruby 2.6.5p114
|
||||
ruby 2.7.1p83
|
||||
|
||||
BUNDLED WITH
|
||||
2.0.2
|
||||
2.1.4
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
release: bundle exec rails db:prepare
|
||||
web: bin/rails server -p $PORT -e $RAILS_ENV
|
||||
worker: bundle exec sidekiq -C config/sidekiq.yml
|
||||
worker: bundle exec sidekiq -C config/sidekiq.yml
|
||||
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
backend: bin/rails s -p 3000
|
||||
frontend: bin/webpack-dev-server
|
||||
worker: bundle exec sidekiq
|
||||
worker: bundle exec sidekiq -C config/sidekiq.yml
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
backend: RAILS_ENV=test bin/rails s -p 5050
|
||||
frontend: bin/webpack-dev-server
|
||||
worker: RAILS_ENV=test bundle exec sidekiq -C config/sidekiq.yml
|
||||
@@ -1,5 +1,5 @@
|
||||
<p align="center">
|
||||
<img src="https://storage.googleapis.com/chatwoot-assets/woot-logo.svg" alt="Woot-logo" width="240">
|
||||
<img src="https://s3.us-west-2.amazonaws.com/gh-assets.chatwoot.com/brand.svg" alt="Woot-logo" width="240">
|
||||
|
||||
<div align="center">A simple and elegant live chat software</div>
|
||||
<div align="center">An opensource alternative to Intercom, Zendesk, Drift, Crisp etc.</div>
|
||||
@@ -19,45 +19,75 @@ ___
|
||||
<a href="https://hub.docker.com/r/chatwoot/chatwoot/"><img src="https://img.shields.io/docker/cloud/build/chatwoot/chatwoot" alt="Docker Build Badge"></a>
|
||||
<img src="https://img.shields.io/github/license/chatwoot/chatwoot" alt="License">
|
||||
<img src="https://img.shields.io/github/commit-activity/m/chatwoot/chatwoot" alt="Commits-per-month">
|
||||
<img src="https://img.shields.io/discord/647412545203994635" alt="Discord">
|
||||
<a href="https://discord.gg/cJXdrwS"><img src="https://img.shields.io/badge/chat-Discord-violet?logo=discord" alt="Chat on Discord"></a>
|
||||
<a title="Crowdin" target="_self" href="https://chatwoot.crowdin.com/chatwoot"><img src="https://badges.crowdin.net/e/37ced7eba411064bd792feb3b7a28b16/localized.svg"></a>
|
||||
<a href="https://discord.gg/cJXdrwS"><img src="https://img.shields.io/discord/647412545203994635" alt="Discord"></a>
|
||||
</p>
|
||||
|
||||

|
||||
<img src="https://s3.us-west-2.amazonaws.com/gh-assets.chatwoot.com/chatwoot-dashboard-assets.png" width="100%" alt="Chat dashboard"/>
|
||||
|
||||
## Background
|
||||
Chatwoot is an open-source omnichannel customer support software. The development of Chatwoot started in 2016. It failed to succeed as a business and eventually shut up shop in 2017. During 2019 #Hacktoberfest, the maintainers decided to make it open-source, instead of letting the code rust in a private repo. With a pleasant surprise, Chatwoot became a trending project on Hacker News and best of all, got lots of love from the community.
|
||||
Now, a failed project is back on track and the prospects are looking great. The team is back to working on the project and this time, we are building it in the open. Thanks to the ideas and contributions from the community.
|
||||
|
||||
Chatwoot is a customer support tool for instant messaging channels which can help businesses to provide exceptional customer support. The development of Chatwoot started in 2016 and it failed to succeed as a business and eventually shut the shop in 2017. During 2019 #Hacktoberfest, the maintainers decided to make it opensource instead of letting the code rust in a private repo. With a pleasant surprise, Chatwoot became a trending project on Hacker News and best of all, got lots of love from the community.
|
||||
---
|
||||
|
||||
Now, a failed project is back on track and the prospects are looking great. The team is back to working on the project and we are building it in the open. Thanks to the ideas and contributions from the community.
|
||||
### Features
|
||||
|
||||
## Documentation
|
||||
Chatwoot gives an integrated view of conversations happening in different communication channels.
|
||||
|
||||
Detailed documentation is available at [www.chatwoot.com/docs](https://www.chatwoot.com/docs).
|
||||
It supports the following conversation channels:
|
||||
|
||||
You can find the quick setup docs [here](https://www.chatwoot.com/docs/quick-setup).
|
||||
- **Website**: Talk to your customers using our live chat widget and make use of our SDK to identify a user and provide contextual support.
|
||||
- **Facebook**: Connect your Facebook pages and start replying to the direct messages to your page.
|
||||
- **Twitter**: Connect your Twitter profiles and reply to direct messages or the tweets where you are mentioned.
|
||||
- **Whatsapp**: Connect your Whatsapp business account and manage the conversation in Chatwoot
|
||||
- **SMS**: Connect your Twilio SMS account and reply to the SMS queries in Chatwoot
|
||||
- **API Channel**: Build custom communication channels using our API channel.
|
||||
- **Email (beta)**: Forward all your email queries to Chatwoot and view it in our integrated dashboard.
|
||||
|
||||
## Heroku one-click deploy
|
||||
Other features include:
|
||||
|
||||
Deploying chatwoot to heroku, it's a breeze. It's as simple as clicking this button.
|
||||
- **Multi-brand inboxes**: Manage multiple brands or pages using a single dashboard.
|
||||
- **Private notes**: Inter team communication is possible using private notes in a conversation.
|
||||
- **Canned responses (Saved replies)**: Improve the response rate by adding saved replies for frequently asked questions.
|
||||
- **Conversation Labels**: Use conversation labelling to create custom workflows.
|
||||
- **Auto assignment**: Chatwoot intelligently assigns a ticket to the agents who have access to the inbox depending on their availability and load.
|
||||
- **Conversation continuity**: If the user has provided an email address through the chat widget, Chatwoot would send an email to the customer under the agent name so that the user can continue the conversation over the email.
|
||||
- **Multi-lingual support**: Chatwoot supports 10+ languages.
|
||||
- **Powerful API & Webhooks**: Extend the capability of the software using Chatwoot’s webhooks and APIs.
|
||||
- **Integrations**: Chatwoot natively integrates with Slack right now. Manage your conversations in Slack without logging into the dashboard.
|
||||
|
||||
---
|
||||
|
||||
### Documentation
|
||||
|
||||
Detailed documentation is available at [www.chatwoot.com/help-center](https://www.chatwoot.com/help-center).
|
||||
|
||||
---
|
||||
|
||||
### Branching model
|
||||
|
||||
We use the [git-flow](https://nvie.com/posts/a-successful-git-branching-model/) branching model. The base branch is `develop`.
|
||||
If you are looking for a stable version, please use the `master` or tags labelled as `v1.x.x`.
|
||||
|
||||
---
|
||||
|
||||
### Deployment
|
||||
|
||||
#### Heroku one-click deploy
|
||||
|
||||
Deploying Chatwoot to Heroku is a breeze. It's as simple as clicking this button:
|
||||
|
||||
[](https://heroku.com/deploy?template=https://github.com/chatwoot/chatwoot/tree/master)
|
||||
|
||||
Follow this [link](https://www.chatwoot.com/docs/environment-variables) to understand setting the correct environment variables for the app to work with all the features. There might be breakages if you do not set the relevant environment variables. This applies to deploying the docker image as well.
|
||||
Follow this [link](https://www.chatwoot.com/docs/environment-variables) to understand setting the correct environment variables for the app to work with all the features. There might be breakages if you do not set the relevant environment variables.
|
||||
|
||||
## Docker
|
||||
#### Other deployment options
|
||||
|
||||
You can use our official Docker image from [https://hub.docker.com/r/chatwoot/chatwoot](https://hub.docker.com/r/chatwoot/chatwoot)
|
||||
Please follow [deployment architecture guide](https://www.chatwoot.com/docs/deployment/architecture) to deploy with Docker or Caprover.
|
||||
|
||||
```bash
|
||||
docker pull chatwoot/chatwoot
|
||||
```
|
||||
---
|
||||
|
||||
Follow our [environment variables](https://www.chatwoot.com/docs/environment-variables/) guide to setup environment for Docker.
|
||||
|
||||
Follow our [docker development guide](https://www.chatwoot.com/docs/installation-guide-docker) to develop and debug the application using docker composer.
|
||||
|
||||
## Contributors ✨
|
||||
### Contributors ✨
|
||||
|
||||
Thanks goes to all these [wonderful people](https://www.chatwoot.com/docs/contributors):
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
module.exports = '';
|
||||
@@ -0,0 +1,48 @@
|
||||
class ContactIdentifyAction
|
||||
pattr_initialize [:contact!, :params!]
|
||||
|
||||
def perform
|
||||
ActiveRecord::Base.transaction do
|
||||
@contact = merge_contact(existing_identified_contact, @contact) if merge_contacts?(existing_identified_contact, @contact)
|
||||
@contact = merge_contact(existing_email_contact, @contact) if merge_contacts?(existing_email_contact, @contact)
|
||||
update_contact
|
||||
end
|
||||
@contact
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def account
|
||||
@account ||= @contact.account
|
||||
end
|
||||
|
||||
def existing_identified_contact
|
||||
return if params[:identifier].blank?
|
||||
|
||||
@existing_identified_contact ||= Contact.where(account_id: account.id).find_by(identifier: params[:identifier])
|
||||
end
|
||||
|
||||
def existing_email_contact
|
||||
return if params[:email].blank?
|
||||
|
||||
@existing_email_contact ||= Contact.where(account_id: account.id).find_by(email: params[:email])
|
||||
end
|
||||
|
||||
def merge_contacts?(existing_contact, _contact)
|
||||
existing_contact && existing_contact.id != @contact.id
|
||||
end
|
||||
|
||||
def update_contact
|
||||
custom_attributes = params[:custom_attributes] ? @contact.custom_attributes.merge(params[:custom_attributes]) : @contact.custom_attributes
|
||||
@contact.update!(params.slice(:name, :email, :identifier).merge({ custom_attributes: custom_attributes }))
|
||||
ContactAvatarJob.perform_later(@contact, params[:avatar_url]) if params[:avatar_url].present?
|
||||
end
|
||||
|
||||
def merge_contact(base_contact, merge_contact)
|
||||
ContactMergeAction.new(
|
||||
account: account,
|
||||
base_contact: base_contact,
|
||||
mergee_contact: merge_contact
|
||||
).perform
|
||||
end
|
||||
end
|
||||
@@ -5,9 +5,11 @@ class ContactMergeAction
|
||||
ActiveRecord::Base.transaction do
|
||||
validate_contacts
|
||||
merge_conversations
|
||||
merge_messages
|
||||
merge_contact_inboxes
|
||||
remove_mergee_contact
|
||||
end
|
||||
@base_contact
|
||||
end
|
||||
|
||||
private
|
||||
@@ -15,7 +17,7 @@ class ContactMergeAction
|
||||
def validate_contacts
|
||||
return if belongs_to_account?(@base_contact) && belongs_to_account?(@mergee_contact)
|
||||
|
||||
raise Exception, 'contact does not belong to the account'
|
||||
raise StandardError, 'contact does not belong to the account'
|
||||
end
|
||||
|
||||
def belongs_to_account?(contact)
|
||||
@@ -26,6 +28,10 @@ class ContactMergeAction
|
||||
Conversation.where(contact_id: @mergee_contact.id).update(contact_id: @base_contact.id)
|
||||
end
|
||||
|
||||
def merge_messages
|
||||
Message.where(sender: @mergee_contact).update(sender: @base_contact)
|
||||
end
|
||||
|
||||
def merge_contact_inboxes
|
||||
ContactInbox.where(contact_id: @mergee_contact.id).update(contact_id: @base_contact.id)
|
||||
end
|
||||
|
||||
@@ -1 +1,4 @@
|
||||
//= link_tree ../images
|
||||
//= link administrate/application.css
|
||||
//= link administrate/application.js
|
||||
//= link dashboardChart.js
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
// eslint-disable-next-line
|
||||
function prepareData(data) {
|
||||
var labels = [];
|
||||
var dataSet = [];
|
||||
data.forEach(item => {
|
||||
labels.push(item[0]);
|
||||
dataSet.push(item[1]);
|
||||
});
|
||||
return { labels, dataSet };
|
||||
}
|
||||
|
||||
function getChartOptions() {
|
||||
var fontFamily =
|
||||
'Inter,-apple-system,system-ui,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif';
|
||||
return {
|
||||
responsive: true,
|
||||
legend: { labels: { fontFamily } },
|
||||
scales: {
|
||||
xAxes: [
|
||||
{
|
||||
barPercentage: 1.26,
|
||||
ticks: { fontFamily },
|
||||
gridLines: { display: false },
|
||||
},
|
||||
],
|
||||
yAxes: [
|
||||
{
|
||||
ticks: { fontFamily },
|
||||
gridLines: { display: false },
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// eslint-disable-next-line
|
||||
function drawSuperAdminDashboard(data) {
|
||||
var ctx = document.getElementById('dashboard-chart').getContext('2d');
|
||||
var chartData = prepareData(data);
|
||||
// eslint-disable-next-line
|
||||
new Chart(ctx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: chartData.labels,
|
||||
datasets: [
|
||||
{
|
||||
label: 'Conversations',
|
||||
data: chartData.dataSet,
|
||||
backgroundColor: '#1f93ff',
|
||||
},
|
||||
],
|
||||
},
|
||||
options: getChartOptions(),
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
@charset 'utf-8';
|
||||
|
||||
@import 'reset/normalize';
|
||||
|
||||
@import 'utilities/variables';
|
||||
@import 'utilities/text-color';
|
||||
|
||||
@import 'selectize';
|
||||
@import 'datetime_picker';
|
||||
|
||||
@import 'library/clearfix';
|
||||
@import 'library/data-label';
|
||||
@import 'library/variables';
|
||||
|
||||
@import 'base/forms';
|
||||
@import 'base/layout';
|
||||
@import 'base/lists';
|
||||
@import 'base/tables';
|
||||
@import 'base/typography';
|
||||
|
||||
@import 'components/app-container';
|
||||
@import 'components/attributes';
|
||||
@import 'components/buttons';
|
||||
@import 'components/cells';
|
||||
@import 'components/field-unit';
|
||||
@import 'components/flashes';
|
||||
@import 'components/form-actions';
|
||||
@import 'components/main-content';
|
||||
@import 'components/navigation';
|
||||
@import 'components/pagination';
|
||||
@import 'components/search';
|
||||
@import 'components/reports';
|
||||
@@ -0,0 +1,103 @@
|
||||
fieldset {
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
legend {
|
||||
font-weight: $font-weight-medium;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
font-weight: $font-weight-medium;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
input,
|
||||
select {
|
||||
display: block;
|
||||
font-family: $base-font-family;
|
||||
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"],
|
||||
[type="email"],
|
||||
[type="month"],
|
||||
[type="number"],
|
||||
[type="password"],
|
||||
[type="search"],
|
||||
[type="tel"],
|
||||
[type="text"],
|
||||
[type="time"],
|
||||
[type="url"],
|
||||
[type="week"],
|
||||
input:not([type]),
|
||||
textarea {
|
||||
appearance: none;
|
||||
background-color: $white;
|
||||
border: $base-border;
|
||||
border-radius: $base-border-radius;
|
||||
padding: 0.5em;
|
||||
transition: border-color $base-duration $base-timing;
|
||||
width: 100%;
|
||||
|
||||
&:hover {
|
||||
border-color: mix($black, $base-border-color, 20%);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-color: $action-color;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background-color: mix($black, $white, 5%);
|
||||
cursor: not-allowed;
|
||||
|
||||
&:hover {
|
||||
border: $base-border;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
textarea {
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
[type="checkbox"],
|
||||
[type="radio"] {
|
||||
display: inline;
|
||||
margin-right: $small-spacing / 2;
|
||||
}
|
||||
|
||||
[type="file"] {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
[type="checkbox"],
|
||||
[type="radio"],
|
||||
[type="file"],
|
||||
select {
|
||||
&:focus {
|
||||
outline: $focus-outline;
|
||||
outline-offset: $focus-outline-offset;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
html {
|
||||
background-color: $color-white;
|
||||
box-sizing: border-box;
|
||||
font-size: 10px;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: inherit;
|
||||
}
|
||||
|
||||
figure {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
img,
|
||||
picture {
|
||||
margin: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
ul,
|
||||
ol {
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
dl {
|
||||
margin-bottom: $small-spacing;
|
||||
|
||||
dt {
|
||||
font-weight: $font-weight-medium;
|
||||
margin-top: $small-spacing;
|
||||
}
|
||||
|
||||
dd {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
font-size: $font-size-default;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
tr {
|
||||
border-bottom: $base-border;
|
||||
|
||||
th {
|
||||
font-weight: $font-weight-medium;
|
||||
|
||||
&.cell-label--avatar-field {
|
||||
a {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tbody tr {
|
||||
&:hover {
|
||||
background-color: $base-background-color;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: $focus-outline;
|
||||
outline-offset: -($focus-outline-width);
|
||||
}
|
||||
|
||||
td {
|
||||
&.cell-data--avatar-field {
|
||||
line-height: 1;
|
||||
text-align: center;
|
||||
|
||||
img {
|
||||
border-radius: 50%;
|
||||
height: $space-large;
|
||||
max-height: $space-large;
|
||||
width: $space-large;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
td,
|
||||
th {
|
||||
padding: $space-slab;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
td:first-child,
|
||||
th:first-child {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
td:last-child,
|
||||
th:last-child {
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
td img {
|
||||
max-height: 2rem;
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
body {
|
||||
color: $base-font-color;
|
||||
font-family: $base-font-family;
|
||||
font-size: $base-font-size;
|
||||
line-height: $base-line-height;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-family: $heading-font-family;
|
||||
font-size: $base-font-size;
|
||||
line-height: $heading-line-height;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0 0 $small-spacing;
|
||||
}
|
||||
|
||||
a {
|
||||
color: $action-color;
|
||||
transition: color $base-duration $base-timing;
|
||||
|
||||
&:hover {
|
||||
color: mix($black, $action-color, 25%);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: $focus-outline;
|
||||
outline-offset: $focus-outline-offset;
|
||||
}
|
||||
}
|
||||
|
||||
hr {
|
||||
border-bottom: $base-border;
|
||||
border-left: 0;
|
||||
border-right: 0;
|
||||
border-top: 0;
|
||||
margin: $base-spacing 0;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
.app-container {
|
||||
align-items: stretch;
|
||||
display: flex;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
max-width: 100rem;
|
||||
min-height: 100vh;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
.attribute-label {
|
||||
@include data-label;
|
||||
clear: left;
|
||||
float: left;
|
||||
margin-bottom: $base-spacing;
|
||||
margin-top: 0.25em;
|
||||
text-align: right;
|
||||
width: calc(15% - 1rem);
|
||||
}
|
||||
|
||||
.preserve-whitespace {
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.attribute-data {
|
||||
float: left;
|
||||
margin-bottom: $base-spacing;
|
||||
margin-left: 2rem;
|
||||
width: calc(85% - 1rem);
|
||||
}
|
||||
|
||||
.attribute--nested {
|
||||
border: $base-border;
|
||||
padding: $small-spacing;
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
button,
|
||||
input[type="button"],
|
||||
input[type="reset"],
|
||||
input[type="submit"],
|
||||
.button {
|
||||
appearance: none;
|
||||
background-color: $color-woot;
|
||||
border: 0;
|
||||
border-radius: $base-border-radius;
|
||||
color: $white;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
font-size: $font-size-default;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
font-weight: $font-weight-medium;
|
||||
line-height: 1;
|
||||
padding: $space-one $space-two;
|
||||
text-decoration: none;
|
||||
transition: background-color $base-duration $base-timing;
|
||||
user-select: none;
|
||||
vertical-align: middle;
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover {
|
||||
background-color: mix($black, $color-woot, 20%);
|
||||
color: $white;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: $focus-outline;
|
||||
outline-offset: $focus-outline-offset;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
|
||||
&:hover {
|
||||
background-color: $color-woot;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.button--alt {
|
||||
background-color: transparent;
|
||||
border: $base-border;
|
||||
border-color: $blue;
|
||||
color: $blue;
|
||||
margin-bottom: $base-spacing;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
.cell-label {
|
||||
&:hover {
|
||||
a {
|
||||
color: $action-color;
|
||||
}
|
||||
|
||||
svg {
|
||||
fill: $action-color;
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
display: inline-block;
|
||||
transition: color $base-duration $base-timing;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.cell-label--asc,
|
||||
.cell-label--desc {
|
||||
font-weight: $font-weight-medium;
|
||||
}
|
||||
|
||||
.cell-label__sort-indicator {
|
||||
float: right;
|
||||
margin-left: 5px;
|
||||
|
||||
svg {
|
||||
fill: $hint-grey;
|
||||
height: 13px;
|
||||
transition: transform $base-duration $base-timing;
|
||||
width: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
.cell-label__sort-indicator--desc {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.cell-data--number,
|
||||
.cell-label--number {
|
||||
text-align: right;
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
.field-unit {
|
||||
@include administrate-clearfix;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
margin-bottom: $base-spacing;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.field-unit__label {
|
||||
float: left;
|
||||
margin-left: 1rem;
|
||||
text-align: right;
|
||||
width: calc(15% - 1rem);
|
||||
}
|
||||
|
||||
.field-unit__field {
|
||||
float: left;
|
||||
margin-left: 2rem;
|
||||
max-width: 50rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.field-unit--nested {
|
||||
border: $base-border;
|
||||
margin-left: 7.5%;
|
||||
max-width: 60rem;
|
||||
padding: $small-spacing;
|
||||
width: 100%;
|
||||
|
||||
.field-unit__field {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.field-unit__label {
|
||||
width: 10rem;
|
||||
}
|
||||
}
|
||||
|
||||
.field-unit--required {
|
||||
label::after {
|
||||
color: $red;
|
||||
content: ' *';
|
||||
}
|
||||
}
|
||||
|
||||
.attribute-data--avatar-field {
|
||||
height: $space-larger;
|
||||
width: $space-larger;
|
||||
|
||||
img {
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
$base-spacing: 1.5em !default;
|
||||
$flashes: (
|
||||
"alert": #fff6bf,
|
||||
"error": #fbe3e4,
|
||||
"notice": #e5edf8,
|
||||
"success": #e6efc2,
|
||||
) !default;
|
||||
|
||||
@each $flash-type, $color in $flashes {
|
||||
.flash-#{$flash-type} {
|
||||
background-color: $color;
|
||||
color: mix($black, $color, 60%);
|
||||
display: block;
|
||||
margin-bottom: $base-spacing / 2;
|
||||
padding: $base-spacing / 2;
|
||||
text-align: center;
|
||||
|
||||
a {
|
||||
color: mix($black, $color, 70%);
|
||||
text-decoration: underline;
|
||||
|
||||
&:focus,
|
||||
&:hover {
|
||||
color: mix($black, $color, 90%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
.form-actions {
|
||||
margin-left: calc(15% + 2rem);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
.main-content {
|
||||
font-size: $font-size-default;
|
||||
left: 23rem;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.main-content__body {
|
||||
padding: $space-two;
|
||||
}
|
||||
|
||||
.main-content__header {
|
||||
align-items: center;
|
||||
background-color: $color-white;
|
||||
border-bottom: 1px solid $color-border;
|
||||
display: flex;
|
||||
min-height: 5.6rem;
|
||||
padding: $space-small $space-normal;
|
||||
}
|
||||
|
||||
.main-content__page-title {
|
||||
font-size: $font-size-large;
|
||||
font-weight: $font-weight-medium;
|
||||
margin-right: auto;
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
.logo-brand {
|
||||
margin-bottom: $space-normal;
|
||||
padding: $space-normal $space-smaller $space-small;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.navigation {
|
||||
background: $white;
|
||||
border-right: 1px solid $color-border;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: $font-size-default;
|
||||
font-weight: $font-weight-medium;
|
||||
height: 100%;
|
||||
justify-content: flex-start;
|
||||
left: 0;
|
||||
margin: 0;
|
||||
overflow: auto;
|
||||
padding: $space-normal;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
width: 23rem;
|
||||
z-index: 1023;
|
||||
|
||||
li {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
|
||||
a {
|
||||
color: $color-gray;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
i {
|
||||
min-width: $space-medium;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.navigation__link {
|
||||
background-color: transparent;
|
||||
color: $color-gray;
|
||||
display: block;
|
||||
line-height: 1;
|
||||
margin-bottom: $space-smaller;
|
||||
padding: $space-one;
|
||||
|
||||
&:hover {
|
||||
color: $blue;
|
||||
|
||||
a {
|
||||
color: $blue;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
&.navigation__link--active {
|
||||
background-color: $color-background;
|
||||
border-radius: $base-border-radius;
|
||||
color: $blue;
|
||||
|
||||
a {
|
||||
color: $blue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.logout {
|
||||
bottom: $space-normal;
|
||||
left: $space-normal;
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
.app-version {
|
||||
color: $color-gray;
|
||||
font-size: $font-size-small;
|
||||
padding-top: $space-smaller;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
.pagination {
|
||||
font-size: $font-size-default;
|
||||
margin-top: $base-spacing;
|
||||
padding-left: $base-spacing;
|
||||
padding-right: $base-spacing;
|
||||
text-align: center;
|
||||
|
||||
.first,
|
||||
.prev,
|
||||
.page,
|
||||
.next,
|
||||
.last {
|
||||
margin: $small-spacing;
|
||||
}
|
||||
|
||||
.current {
|
||||
font-weight: $font-weight-medium;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
.report--list {
|
||||
display: flex;
|
||||
padding: 0 $space-two $space-larger;
|
||||
}
|
||||
|
||||
.report-card {
|
||||
flex: 1;
|
||||
font-size: $font-size-small;
|
||||
text-align: center;
|
||||
|
||||
.metric {
|
||||
font-size: $font-size-bigger;
|
||||
font-weight: 200;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
.search {
|
||||
margin-left: auto;
|
||||
margin-right: 2rem;
|
||||
max-width: 44rem;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.search__input {
|
||||
background: $grey-1;
|
||||
padding-left: $space-normal * 2.5;
|
||||
padding-right: $space-normal * 2.5;
|
||||
}
|
||||
|
||||
.search__eyeglass-icon {
|
||||
fill: $grey-7;
|
||||
height: $space-normal;
|
||||
left: $space-normal;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: $space-normal;
|
||||
}
|
||||
|
||||
.search__clear-link {
|
||||
height: $space-normal;
|
||||
position: absolute;
|
||||
right: $space-normal * 0.75;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: $space-normal;
|
||||
}
|
||||
|
||||
.search__clear-icon {
|
||||
fill: $grey-5;
|
||||
height: $space-normal;
|
||||
position: absolute;
|
||||
transition: fill $base-duration $base-timing;
|
||||
width: $space-normal;
|
||||
|
||||
&:hover {
|
||||
fill: $action-color;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
@mixin administrate-clearfix {
|
||||
&::after {
|
||||
clear: both;
|
||||
content: '';
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
@mixin data-label {
|
||||
color: $hint-grey;
|
||||
font-size: 0.8em;
|
||||
font-weight: 400;
|
||||
letter-spacing: 0.0357em;
|
||||
position: relative;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
// Typography
|
||||
$base-font-family: 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-line-height: 1.5 !default;
|
||||
$heading-line-height: 1.2 !default;
|
||||
|
||||
// Other Sizes
|
||||
$base-border-radius: 4px !default;
|
||||
$base-spacing: $base-line-height * 1em !default;
|
||||
$small-spacing: $base-spacing / 2 !default;
|
||||
|
||||
// Colors
|
||||
$white: #fff !default;
|
||||
$black: #000 !default;
|
||||
|
||||
$blue: #1f93ff !default;
|
||||
$red: #ff382d !default;
|
||||
$light-yellow: #ffc532 !default;
|
||||
$light-green: #44ce4b !default;
|
||||
|
||||
$grey-0: #f6f7f7 !default;
|
||||
$grey-1: #f0f4f5 !default;
|
||||
$grey-2: #cfd8dc !default;
|
||||
$grey-5: #adb5bd !default;
|
||||
$grey-7: #293f54 !default;
|
||||
|
||||
$hint-grey: #7b808c !default;
|
||||
|
||||
// Font Colors
|
||||
$base-font-color: $grey-7 !default;
|
||||
$action-color: $blue !default;
|
||||
|
||||
// Background Colors
|
||||
$base-background-color: $grey-0 !default;
|
||||
|
||||
// Focus
|
||||
$focus-outline-color: transparentize($action-color, 0.4);
|
||||
$focus-outline-width: 3px;
|
||||
$focus-outline: $focus-outline-width solid $focus-outline-color;
|
||||
$focus-outline-offset: 1px;
|
||||
|
||||
// Flash Colors
|
||||
$flash-colors: (
|
||||
alert: $light-yellow,
|
||||
error: $red,
|
||||
notice: mix($white, $blue, 50%),
|
||||
success: $light-green
|
||||
);
|
||||
|
||||
// Border
|
||||
$base-border-color: $grey-1 !default;
|
||||
$base-border: 1px solid $base-border-color !default;
|
||||
|
||||
// Transitions
|
||||
$base-duration: 250ms !default;
|
||||
$base-timing: ease-in-out !default;
|
||||
@@ -0,0 +1,448 @@
|
||||
/*! normalize.css v7.0.0 | MIT License | github.com/necolas/normalize.css */
|
||||
|
||||
/* Document
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* 1. Correct the line height in all browsers.
|
||||
* 2. Prevent adjustments of font size after orientation changes in
|
||||
* IE on Windows Phone and in iOS.
|
||||
*/
|
||||
|
||||
html {
|
||||
line-height: 1.15; /* 1 */
|
||||
-ms-text-size-adjust: 100%; /* 2 */
|
||||
-webkit-text-size-adjust: 100%; /* 2 */
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
/* Sections
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Remove the margin in all browsers (opinionated).
|
||||
*/
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the correct display in IE 9-.
|
||||
*/
|
||||
|
||||
article,
|
||||
aside,
|
||||
footer,
|
||||
header,
|
||||
nav,
|
||||
section {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct the font size and margin on `h1` elements within `section` and
|
||||
* `article` contexts in Chrome, Firefox, and Safari.
|
||||
*/
|
||||
|
||||
h1 {
|
||||
font-size: 2em;
|
||||
margin: 0.67em 0;
|
||||
}
|
||||
|
||||
/* Grouping content
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Add the correct display in IE 9-.
|
||||
* 1. Add the correct display in IE.
|
||||
*/
|
||||
|
||||
figcaption,
|
||||
figure,
|
||||
main { /* 1 */
|
||||
display: block;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the correct margin in IE 8.
|
||||
*/
|
||||
|
||||
figure {
|
||||
margin: 1em 40px;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Add the correct box sizing in Firefox.
|
||||
* 2. Show the overflow in Edge and IE.
|
||||
*/
|
||||
|
||||
hr {
|
||||
box-sizing: content-box; /* 1 */
|
||||
height: 0; /* 1 */
|
||||
overflow: visible; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct the inheritance and scaling of font size in all browsers.
|
||||
* 2. Correct the odd `em` font sizing in all browsers.
|
||||
*/
|
||||
|
||||
pre {
|
||||
font-family: monospace, monospace; /* 1 */
|
||||
font-size: 1em; /* 2 */
|
||||
}
|
||||
|
||||
/* Text-level semantics
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* 1. Remove the gray background on active links in IE 10.
|
||||
* 2. Remove gaps in links underline in iOS 8+ and Safari 8+.
|
||||
*/
|
||||
|
||||
a {
|
||||
background-color: transparent; /* 1 */
|
||||
-webkit-text-decoration-skip: objects; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Remove the bottom border in Chrome 57- and Firefox 39-.
|
||||
* 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
|
||||
*/
|
||||
|
||||
abbr[title] {
|
||||
border-bottom: none; /* 1 */
|
||||
text-decoration: underline; /* 2 */
|
||||
text-decoration: underline dotted; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent the duplicate application of `bolder` by the next rule in Safari 6.
|
||||
*/
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: inherit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the correct font weight in Chrome, Edge, and Safari.
|
||||
*/
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct the inheritance and scaling of font size in all browsers.
|
||||
* 2. Correct the odd `em` font sizing in all browsers.
|
||||
*/
|
||||
|
||||
code,
|
||||
kbd,
|
||||
samp {
|
||||
font-family: monospace, monospace; /* 1 */
|
||||
font-size: 1em; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the correct font style in Android 4.3-.
|
||||
*/
|
||||
|
||||
dfn {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the correct background and color in IE 9-.
|
||||
*/
|
||||
|
||||
mark {
|
||||
background-color: #ff0;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the correct font size in all browsers.
|
||||
*/
|
||||
|
||||
small {
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent `sub` and `sup` elements from affecting the line height in
|
||||
* all browsers.
|
||||
*/
|
||||
|
||||
sub,
|
||||
sup {
|
||||
font-size: 75%;
|
||||
line-height: 0;
|
||||
position: relative;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
|
||||
/* Embedded content
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Add the correct display in IE 9-.
|
||||
*/
|
||||
|
||||
audio,
|
||||
video {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the correct display in iOS 4-7.
|
||||
*/
|
||||
|
||||
audio:not([controls]) {
|
||||
display: none;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the border on images inside links in IE 10-.
|
||||
*/
|
||||
|
||||
img {
|
||||
border-style: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide the overflow in IE.
|
||||
*/
|
||||
|
||||
svg:not(:root) {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Forms
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* 1. Change the font styles in all browsers (opinionated).
|
||||
* 2. Remove the margin in Firefox and Safari.
|
||||
*/
|
||||
|
||||
button,
|
||||
input,
|
||||
optgroup,
|
||||
select,
|
||||
textarea {
|
||||
font-family: sans-serif; /* 1 */
|
||||
font-size: 100%; /* 1 */
|
||||
line-height: 1.15; /* 1 */
|
||||
margin: 0; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the overflow in IE.
|
||||
* 1. Show the overflow in Edge.
|
||||
*/
|
||||
|
||||
button,
|
||||
input { /* 1 */
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the inheritance of text transform in Edge, Firefox, and IE.
|
||||
* 1. Remove the inheritance of text transform in Firefox.
|
||||
*/
|
||||
|
||||
button,
|
||||
select { /* 1 */
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Prevent a WebKit bug where (2) destroys native `audio` and `video`
|
||||
* controls in Android 4.
|
||||
* 2. Correct the inability to style clickable types in iOS and Safari.
|
||||
*/
|
||||
|
||||
button,
|
||||
html [type="button"], /* 1 */
|
||||
[type="reset"],
|
||||
[type="submit"] {
|
||||
-webkit-appearance: button; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the inner border and padding in Firefox.
|
||||
*/
|
||||
|
||||
button::-moz-focus-inner,
|
||||
[type="button"]::-moz-focus-inner,
|
||||
[type="reset"]::-moz-focus-inner,
|
||||
[type="submit"]::-moz-focus-inner {
|
||||
border-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore the focus styles unset by the previous rule.
|
||||
*/
|
||||
|
||||
button:-moz-focusring,
|
||||
[type="button"]:-moz-focusring,
|
||||
[type="reset"]:-moz-focusring,
|
||||
[type="submit"]:-moz-focusring {
|
||||
outline: 1px dotted ButtonText;
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct the padding in Firefox.
|
||||
*/
|
||||
|
||||
fieldset {
|
||||
padding: 0.35em 0.75em 0.625em;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct the text wrapping in Edge and IE.
|
||||
* 2. Correct the color inheritance from `fieldset` elements in IE.
|
||||
* 3. Remove the padding so developers are not caught out when they zero out
|
||||
* `fieldset` elements in all browsers.
|
||||
*/
|
||||
|
||||
legend {
|
||||
box-sizing: border-box; /* 1 */
|
||||
color: inherit; /* 2 */
|
||||
display: table; /* 1 */
|
||||
max-width: 100%; /* 1 */
|
||||
padding: 0; /* 3 */
|
||||
white-space: normal; /* 1 */
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Add the correct display in IE 9-.
|
||||
* 2. Add the correct vertical alignment in Chrome, Firefox, and Opera.
|
||||
*/
|
||||
|
||||
progress {
|
||||
display: inline-block; /* 1 */
|
||||
vertical-align: baseline; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the default vertical scrollbar in IE.
|
||||
*/
|
||||
|
||||
textarea {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Add the correct box sizing in IE 10-.
|
||||
* 2. Remove the padding in IE 10-.
|
||||
*/
|
||||
|
||||
[type="checkbox"],
|
||||
[type="radio"] {
|
||||
box-sizing: border-box; /* 1 */
|
||||
padding: 0; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct the cursor style of increment and decrement buttons in Chrome.
|
||||
*/
|
||||
|
||||
[type="number"]::-webkit-inner-spin-button,
|
||||
[type="number"]::-webkit-outer-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct the odd appearance in Chrome and Safari.
|
||||
* 2. Correct the outline style in Safari.
|
||||
*/
|
||||
|
||||
[type="search"] {
|
||||
-webkit-appearance: textfield; /* 1 */
|
||||
outline-offset: -2px; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the inner padding and cancel buttons in Chrome and Safari on macOS.
|
||||
*/
|
||||
|
||||
[type="search"]::-webkit-search-cancel-button,
|
||||
[type="search"]::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct the inability to style clickable types in iOS and Safari.
|
||||
* 2. Change font properties to `inherit` in Safari.
|
||||
*/
|
||||
|
||||
::-webkit-file-upload-button {
|
||||
-webkit-appearance: button; /* 1 */
|
||||
font: inherit; /* 2 */
|
||||
}
|
||||
|
||||
/* Interactive
|
||||
========================================================================== */
|
||||
|
||||
/*
|
||||
* Add the correct display in IE 9-.
|
||||
* 1. Add the correct display in Edge, IE, and Firefox.
|
||||
*/
|
||||
|
||||
details, /* 1 */
|
||||
menu {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/*
|
||||
* Add the correct display in all browsers.
|
||||
*/
|
||||
|
||||
summary {
|
||||
display: list-item;
|
||||
}
|
||||
|
||||
/* Scripting
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Add the correct display in IE 9-.
|
||||
*/
|
||||
|
||||
canvas {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the correct display in IE.
|
||||
*/
|
||||
|
||||
template {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Hidden
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Add the correct display in IE 10-.
|
||||
*/
|
||||
|
||||
[hidden] {
|
||||
display: none;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
.text-color-red {
|
||||
color: $alert-color;
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
// Font sizes
|
||||
$font-size-nano: 0.8rem;
|
||||
$font-size-micro: 1.0rem;
|
||||
$font-size-mini: 1.2rem;
|
||||
$font-size-small: 1.4rem;
|
||||
$font-size-default: 1.6rem;
|
||||
$font-size-medium: 1.8rem;
|
||||
$font-size-large: 2.2rem;
|
||||
$font-size-big: 2.4rem;
|
||||
$font-size-bigger: 3.0rem;
|
||||
$font-size-mega: 3.4rem;
|
||||
$font-size-giga: 4.0rem;
|
||||
|
||||
// spaces
|
||||
$zero: 0;
|
||||
$space-micro: 0.2rem;
|
||||
$space-smaller: 0.4rem;
|
||||
$space-small: 0.8rem;
|
||||
$space-one: 1rem;
|
||||
$space-slab: 1.2rem;
|
||||
$space-normal: 1.6rem;
|
||||
$space-two: 2.0rem;
|
||||
$space-medium: 2.4rem;
|
||||
$space-large: 3.2rem;
|
||||
$space-larger: 4.8rem;
|
||||
$space-jumbo: 6.4rem;
|
||||
$space-mega: 10.0rem;
|
||||
|
||||
// font-weight
|
||||
$font-weight-feather: 100;
|
||||
$font-weight-light: 300;
|
||||
$font-weight-normal: 400;
|
||||
$font-weight-medium: 500;
|
||||
$font-weight-bold: 600;
|
||||
$font-weight-black: 700;
|
||||
|
||||
//Navbar
|
||||
$nav-bar-width: 23rem;
|
||||
$header-height: 5.6rem;
|
||||
|
||||
$woot-logo-padding: $space-large $space-two;
|
||||
|
||||
// Colors
|
||||
$color-woot: #1f93ff;
|
||||
$color-gray: #6e6f73;
|
||||
$color-light-gray: #999a9b;
|
||||
$color-border: #e0e6ed;
|
||||
$color-border-light: #f0f4f5;
|
||||
$color-background: #f4f6fb;
|
||||
$color-border-dark: #cad0d4;
|
||||
$color-background-light: #f9fafc;
|
||||
$color-white: #fff;
|
||||
$color-body: #3c4858;
|
||||
$color-heading: #1f2d3d;
|
||||
$color-extra-light-blue: #f5f7f9;
|
||||
|
||||
$primary-color: $color-woot;
|
||||
$secondary-color: #35c5ff;
|
||||
$success-color: #44ce4b;
|
||||
$warning-color: #ffc532;
|
||||
$alert-color: #ff382d;
|
||||
|
||||
$masked-bg: rgba(0, 0, 0, .4);
|
||||
|
||||
// Color-palettes
|
||||
|
||||
$color-primary-light: #c7e3ff;
|
||||
$color-primary-dark: darken($color-woot, 20%);
|
||||
|
||||
// Thumbnail
|
||||
$thumbnail-radius: 4rem;
|
||||
|
||||
// chat-header
|
||||
$conv-header-height: 4rem;
|
||||
|
||||
// Inbox List
|
||||
|
||||
$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;
|
||||
|
||||
$swift-ease-out-duration: .4s !default;
|
||||
$swift-ease-out-timing-function: cubic-bezier(.25, .8, .25, 1) !default;
|
||||
$swift-ease-out: all $swift-ease-out-duration $swift-ease-out-timing-function !default;
|
||||
|
||||
// Ionicons
|
||||
$ionicons-font-path: '~ionicons/fonts';
|
||||
|
||||
// Transitions
|
||||
$transition-ease-in: all 0.250s ease-in;
|
||||
@@ -1,19 +0,0 @@
|
||||
# app/bot/facebook_bot.rb
|
||||
require 'facebook/messenger'
|
||||
include Facebook::Messenger
|
||||
|
||||
Bot.on :message do |message|
|
||||
response = ::Integrations::Facebook::MessageParser.new(message)
|
||||
::Integrations::Facebook::MessageCreator.new(response).perform
|
||||
end
|
||||
|
||||
Bot.on :delivery do |delivery|
|
||||
# delivery.ids # => 'mid.1457764197618:41d102a3e1ae206a38'
|
||||
# delivery.sender # => { 'id' => '1008372609250235' }
|
||||
# delivery.recipient # => { 'id' => '2015573629214912' }
|
||||
# delivery.at # => 2016-04-22 21:30:36 +0200
|
||||
# delivery.seq # => 37
|
||||
updater = Integrations::Facebook::DeliveryStatus.new(delivery)
|
||||
updater.perform
|
||||
puts "Human was online at #{delivery.at}"
|
||||
end
|
||||
@@ -0,0 +1,28 @@
|
||||
require 'facebook/messenger'
|
||||
|
||||
class FacebookBot
|
||||
include Facebook::Messenger
|
||||
|
||||
Bot.on :message do |message|
|
||||
Rails.logger.info "MESSAGE_RECIEVED #{message}"
|
||||
response = ::Integrations::Facebook::MessageParser.new(message)
|
||||
::Integrations::Facebook::MessageCreator.new(response).perform
|
||||
end
|
||||
|
||||
Bot.on :delivery do |delivery|
|
||||
# delivery.ids # => 'mid.1457764197618:41d102a3e1ae206a38'
|
||||
# delivery.sender # => { 'id' => '1008372609250235' }
|
||||
# delivery.recipient # => { 'id' => '2015573629214912' }
|
||||
# delivery.at # => 2016-04-22 21:30:36 +0200
|
||||
# delivery.seq # => 37
|
||||
updater = Integrations::Facebook::DeliveryStatus.new(delivery)
|
||||
updater.perform
|
||||
Rails.logger.info "Human was online at #{delivery.at}"
|
||||
end
|
||||
|
||||
Bot.on :message_echo do |message|
|
||||
Rails.logger.info "MESSAGE_ECHO #{message}"
|
||||
response = ::Integrations::Facebook::MessageParser.new(message)
|
||||
::Integrations::Facebook::MessageCreator.new(response).perform
|
||||
end
|
||||
end
|
||||
@@ -2,15 +2,18 @@
|
||||
|
||||
class AccountBuilder
|
||||
include CustomExceptions::Account
|
||||
pattr_initialize [:account_name!, :email!]
|
||||
pattr_initialize [:account_name!, :email!, :confirmed!, :user]
|
||||
|
||||
def perform
|
||||
validate_email
|
||||
validate_user
|
||||
if @user.nil?
|
||||
validate_email
|
||||
validate_user
|
||||
end
|
||||
ActiveRecord::Base.transaction do
|
||||
@account = create_account
|
||||
@user = create_and_link_user
|
||||
end
|
||||
[@user, @account]
|
||||
rescue StandardError => e
|
||||
@account&.destroy
|
||||
puts e.inspect
|
||||
@@ -38,24 +41,38 @@ class AccountBuilder
|
||||
|
||||
def create_account
|
||||
@account = Account.create!(name: @account_name)
|
||||
Current.account = @account
|
||||
end
|
||||
|
||||
def create_and_link_user
|
||||
password = Time.now.to_i
|
||||
@user = @account.users.new(email: @email,
|
||||
password: password,
|
||||
password_confirmation: password,
|
||||
role: User.roles['administrator'],
|
||||
name: email_to_name(@email))
|
||||
if @user.save!
|
||||
if @user.present? || create_user
|
||||
link_user_to_account(@user, @account)
|
||||
@user
|
||||
else
|
||||
raise UserErrors.new(errors: @user.errors)
|
||||
end
|
||||
end
|
||||
|
||||
def link_user_to_account(user, account)
|
||||
AccountUser.create!(
|
||||
account_id: account.id,
|
||||
user_id: user.id,
|
||||
role: AccountUser.roles['administrator']
|
||||
)
|
||||
end
|
||||
|
||||
def email_to_name(email)
|
||||
name = email[/[^@]+/]
|
||||
name.split('.').map(&:capitalize).join(' ')
|
||||
end
|
||||
|
||||
def create_user
|
||||
password = Time.now.to_i
|
||||
@user = User.new(email: @email,
|
||||
password: password,
|
||||
password_confirmation: password,
|
||||
name: email_to_name(@email))
|
||||
@user.confirm if @confirmed
|
||||
@user.save!
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
class ContactBuilder
|
||||
pattr_initialize [:source_id!, :inbox!, :contact_attributes!]
|
||||
|
||||
def perform
|
||||
contact_inbox = inbox.contact_inboxes.find_by(source_id: source_id)
|
||||
return contact_inbox if contact_inbox
|
||||
|
||||
build_contact
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def account
|
||||
@account ||= inbox.account
|
||||
end
|
||||
|
||||
def create_contact_inbox(contact)
|
||||
::ContactInbox.create!(
|
||||
contact_id: contact.id,
|
||||
inbox_id: inbox.id,
|
||||
source_id: source_id
|
||||
)
|
||||
end
|
||||
|
||||
def update_contact_avatar(contact)
|
||||
::ContactAvatarJob.perform_later(contact, contact_attributes[:avatar_url]) if contact_attributes[:avatar_url]
|
||||
end
|
||||
|
||||
def build_contact
|
||||
ActiveRecord::Base.transaction do
|
||||
contact = account.contacts.create!(
|
||||
name: contact_attributes[:name],
|
||||
phone_number: contact_attributes[:phone_number],
|
||||
email: contact_attributes[:email],
|
||||
identifier: contact_attributes[:identifier],
|
||||
additional_attributes: contact_attributes[:additional_attributes]
|
||||
)
|
||||
|
||||
contact_inbox = create_contact_inbox(contact)
|
||||
update_contact_avatar(contact)
|
||||
contact_inbox
|
||||
rescue StandardError => e
|
||||
Rails.logger.info e
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,142 @@
|
||||
# This class creates both outgoing messages from chatwoot and echo outgoing messages based on the flag `outgoing_echo`
|
||||
# Assumptions
|
||||
# 1. Incase of an outgoing message which is echo, source_id will NOT be nil,
|
||||
# based on this we are showing "not sent from chatwoot" message in frontend
|
||||
# Hence there is no need to set user_id in message for outgoing echo messages.
|
||||
|
||||
class Messages::Facebook::MessageBuilder
|
||||
attr_reader :response
|
||||
|
||||
def initialize(response, inbox, outgoing_echo: false)
|
||||
@response = response
|
||||
@inbox = inbox
|
||||
@outgoing_echo = outgoing_echo
|
||||
@sender_id = (@outgoing_echo ? @response.recipient_id : @response.sender_id)
|
||||
@message_type = (@outgoing_echo ? :outgoing : :incoming)
|
||||
end
|
||||
|
||||
def perform
|
||||
ActiveRecord::Base.transaction do
|
||||
build_contact
|
||||
build_message
|
||||
end
|
||||
rescue StandardError => e
|
||||
Raven.capture_exception(e)
|
||||
true
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def contact
|
||||
@contact ||= @inbox.contact_inboxes.find_by(source_id: @sender_id)&.contact
|
||||
end
|
||||
|
||||
def build_contact
|
||||
return if contact.present?
|
||||
|
||||
@contact = Contact.create!(contact_params.except(:remote_avatar_url))
|
||||
ContactAvatarJob.perform_later(@contact, contact_params[:remote_avatar_url]) if contact_params[:remote_avatar_url]
|
||||
@contact_inbox = ContactInbox.create(contact: contact, inbox: @inbox, source_id: @sender_id)
|
||||
end
|
||||
|
||||
def build_message
|
||||
@message = conversation.messages.create!(message_params)
|
||||
(response.attachments || []).each do |attachment|
|
||||
attachment_obj = @message.attachments.new(attachment_params(attachment).except(:remote_file_url))
|
||||
attachment_obj.save!
|
||||
attach_file(attachment_obj, attachment_params(attachment)[:remote_file_url]) if attachment_params(attachment)[:remote_file_url]
|
||||
end
|
||||
end
|
||||
|
||||
def attach_file(attachment, file_url)
|
||||
file_resource = LocalResource.new(file_url)
|
||||
attachment.file.attach(io: file_resource.file, filename: file_resource.filename, content_type: file_resource.encoding)
|
||||
rescue *ExceptionList::URI_EXCEPTIONS => e
|
||||
Rails.logger.info "invalid url #{file_url} : #{e.message}"
|
||||
end
|
||||
|
||||
def conversation
|
||||
@conversation ||= Conversation.find_by(conversation_params) || build_conversation
|
||||
end
|
||||
|
||||
def build_conversation
|
||||
@contact_inbox ||= contact.contact_inboxes.find_by!(source_id: @sender_id)
|
||||
Conversation.create!(conversation_params.merge(
|
||||
contact_inbox_id: @contact_inbox.id
|
||||
))
|
||||
end
|
||||
|
||||
def attachment_params(attachment)
|
||||
file_type = attachment['type'].to_sym
|
||||
params = { file_type: file_type, account_id: @message.account_id }
|
||||
|
||||
if [:image, :file, :audio, :video].include? file_type
|
||||
params.merge!(file_type_params(attachment))
|
||||
elsif file_type == :location
|
||||
params.merge!(location_params(attachment))
|
||||
elsif file_type == :fallback
|
||||
params.merge!(fallback_params(attachment))
|
||||
end
|
||||
|
||||
params
|
||||
end
|
||||
|
||||
def file_type_params(attachment)
|
||||
{
|
||||
external_url: attachment['payload']['url'],
|
||||
remote_file_url: attachment['payload']['url']
|
||||
}
|
||||
end
|
||||
|
||||
def location_params(attachment)
|
||||
lat = attachment['payload']['coordinates']['lat']
|
||||
long = attachment['payload']['coordinates']['long']
|
||||
{
|
||||
external_url: attachment['url'],
|
||||
coordinates_lat: lat,
|
||||
coordinates_long: long,
|
||||
fallback_title: attachment['title']
|
||||
}
|
||||
end
|
||||
|
||||
def fallback_params(attachment)
|
||||
{
|
||||
fallback_title: attachment['title'],
|
||||
external_url: attachment['url']
|
||||
}
|
||||
end
|
||||
|
||||
def conversation_params
|
||||
{
|
||||
account_id: @inbox.account_id,
|
||||
inbox_id: @inbox.id,
|
||||
contact_id: contact.id
|
||||
}
|
||||
end
|
||||
|
||||
def message_params
|
||||
{
|
||||
account_id: conversation.account_id,
|
||||
inbox_id: conversation.inbox_id,
|
||||
message_type: @message_type,
|
||||
content: response.content,
|
||||
source_id: response.identifier,
|
||||
sender: @outgoing_echo ? nil : contact
|
||||
}
|
||||
end
|
||||
|
||||
def contact_params
|
||||
begin
|
||||
k = Koala::Facebook::API.new(@inbox.channel.page_access_token) if @inbox.facebook?
|
||||
result = k.get_object(@sender_id) || {}
|
||||
rescue StandardError => e
|
||||
result = {}
|
||||
Raven.capture_exception(e)
|
||||
end
|
||||
{
|
||||
name: "#{result['first_name'] || 'John'} #{result['last_name'] || 'Doe'}",
|
||||
account_id: @inbox.account_id,
|
||||
remote_avatar_url: result['profile_pic'] || ''
|
||||
}
|
||||
end
|
||||
end
|
||||
@@ -1,2 +0,0 @@
|
||||
class Messages::IncomingMessageBuilder < Messages::MessageBuilder
|
||||
end
|
||||
@@ -1,130 +1,59 @@
|
||||
require 'open-uri'
|
||||
class Messages::MessageBuilder
|
||||
include ::FileTypeHelper
|
||||
attr_reader :message
|
||||
|
||||
# This class creates both outgoing messages from chatwoot and echo outgoing messages based on the flag `outgoing_echo`
|
||||
# Assumptions
|
||||
# 1. Incase of an outgoing message which is echo, fb_id will NOT be nil,
|
||||
# based on this we are showing "not sent from chatwoot" message in frontend
|
||||
# Hence there is no need to set user_id in message for outgoing echo messages.
|
||||
def initialize(user, conversation, params)
|
||||
@params = params
|
||||
@private = params[:private] || false
|
||||
@conversation = conversation
|
||||
@user = user
|
||||
@message_type = params[:message_type] || 'outgoing'
|
||||
@items = params.to_unsafe_h&.dig(:content_attributes, :items)
|
||||
@attachments = params[:attachments]
|
||||
@in_reply_to = params.to_unsafe_h&.dig(:content_attributes, :in_reply_to)
|
||||
end
|
||||
|
||||
module Messages
|
||||
class MessageBuilder
|
||||
attr_reader :response
|
||||
|
||||
def initialize(response, inbox, outgoing_echo = false)
|
||||
@response = response
|
||||
@inbox = inbox
|
||||
@sender_id = (outgoing_echo ? @response.recipient_id : @response.sender_id)
|
||||
@message_type = (outgoing_echo ? :outgoing : :incoming)
|
||||
end
|
||||
|
||||
def perform
|
||||
ActiveRecord::Base.transaction do
|
||||
build_contact
|
||||
build_message
|
||||
def perform
|
||||
@message = @conversation.messages.build(message_params)
|
||||
if @attachments.present?
|
||||
@attachments.each do |uploaded_attachment|
|
||||
attachment = @message.attachments.new(
|
||||
account_id: @message.account_id,
|
||||
file_type: file_type(uploaded_attachment&.content_type)
|
||||
)
|
||||
attachment.file.attach(uploaded_attachment)
|
||||
end
|
||||
rescue StandardError => e
|
||||
Raven.capture_exception(e)
|
||||
true
|
||||
end
|
||||
@message.save
|
||||
@message
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def message_type
|
||||
if @conversation.inbox.channel_type != 'Channel::Api' && @message_type == 'incoming'
|
||||
raise StandardError, 'Incoming messages are only allowed in Api inboxes'
|
||||
end
|
||||
|
||||
private
|
||||
@message_type
|
||||
end
|
||||
|
||||
def contact
|
||||
@contact ||= @inbox.contact_inboxes.find_by(source_id: @sender_id)&.contact
|
||||
end
|
||||
def sender
|
||||
message_type == 'outgoing' ? @user : @conversation.contact
|
||||
end
|
||||
|
||||
def build_contact
|
||||
return if contact.present?
|
||||
|
||||
@contact = Contact.create!(contact_params)
|
||||
ContactInbox.create(contact: contact, inbox: @inbox, source_id: @sender_id)
|
||||
end
|
||||
|
||||
def build_message
|
||||
@message = conversation.messages.new(message_params)
|
||||
(response.attachments || []).each do |attachment|
|
||||
@message.build_attachment(attachment_params(attachment))
|
||||
end
|
||||
@message.save!
|
||||
end
|
||||
|
||||
def build_attachment; end
|
||||
|
||||
def conversation
|
||||
@conversation ||= Conversation.find_by(conversation_params) || Conversation.create!(conversation_params)
|
||||
end
|
||||
|
||||
def attachment_params(attachment)
|
||||
file_type = attachment['type'].to_sym
|
||||
params = { file_type: file_type, account_id: @message.account_id }
|
||||
|
||||
if [:image, :file, :audio, :video].include? file_type
|
||||
params.merge!(file_type_params(attachment))
|
||||
elsif file_type == :location
|
||||
params.merge!(location_params(attachment))
|
||||
elsif file_type == :fallback
|
||||
params.merge!(fallback_params(attachment))
|
||||
end
|
||||
|
||||
params
|
||||
end
|
||||
|
||||
def file_type_params(attachment)
|
||||
{
|
||||
external_url: attachment['payload']['url'],
|
||||
remote_file_url: attachment['payload']['url']
|
||||
}
|
||||
end
|
||||
|
||||
def location_params(attachment)
|
||||
lat = attachment['payload']['coordinates']['lat']
|
||||
long = attachment['payload']['coordinates']['long']
|
||||
{
|
||||
external_url: attachment['url'],
|
||||
coordinates_lat: lat,
|
||||
coordinates_long: long,
|
||||
fallback_title: attachment['title']
|
||||
}
|
||||
end
|
||||
|
||||
def fallback_params(attachment)
|
||||
{
|
||||
fallback_title: attachment['title'],
|
||||
external_url: attachment['url']
|
||||
}
|
||||
end
|
||||
|
||||
def conversation_params
|
||||
{
|
||||
account_id: @inbox.account_id,
|
||||
inbox_id: @inbox.id,
|
||||
contact_id: contact.id
|
||||
}
|
||||
end
|
||||
|
||||
def message_params
|
||||
{
|
||||
account_id: conversation.account_id,
|
||||
inbox_id: conversation.inbox_id,
|
||||
message_type: @message_type,
|
||||
content: response.content,
|
||||
fb_id: response.identifier
|
||||
}
|
||||
end
|
||||
|
||||
def contact_params
|
||||
begin
|
||||
k = Koala::Facebook::API.new(@inbox.channel.page_access_token) if @inbox.facebook?
|
||||
result = k.get_object(@sender_id) || {}
|
||||
rescue Exception => e
|
||||
result = {}
|
||||
Raven.capture_exception(e)
|
||||
end
|
||||
{
|
||||
name: "#{result['first_name'] || 'John'} #{result['last_name'] || 'Doe'}",
|
||||
account_id: @inbox.account_id,
|
||||
remote_avatar_url: result['profile_pic'] || nil
|
||||
}
|
||||
end
|
||||
def message_params
|
||||
{
|
||||
account_id: @conversation.account_id,
|
||||
inbox_id: @conversation.inbox_id,
|
||||
message_type: message_type,
|
||||
content: @params[:content],
|
||||
private: @private,
|
||||
sender: sender,
|
||||
content_type: @params[:content_type],
|
||||
items: @items,
|
||||
in_reply_to: @in_reply_to,
|
||||
echo_id: @params[:echo_id]
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
class Messages::Outgoing::EchoBuilder < ::Messages::MessageBuilder
|
||||
end
|
||||
@@ -1,29 +0,0 @@
|
||||
class Messages::Outgoing::NormalBuilder
|
||||
attr_reader :message
|
||||
|
||||
def initialize(user, conversation, params)
|
||||
@content = params[:message]
|
||||
@private = ['1', 'true', 1, true].include? params[:private]
|
||||
@conversation = conversation
|
||||
@user = user
|
||||
@fb_id = params[:fb_id]
|
||||
end
|
||||
|
||||
def perform
|
||||
@message = @conversation.messages.create!(message_params)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def message_params
|
||||
{
|
||||
account_id: @conversation.account_id,
|
||||
inbox_id: @conversation.inbox_id,
|
||||
message_type: :outgoing,
|
||||
content: @content,
|
||||
private: @private,
|
||||
user_id: @user.id,
|
||||
fb_id: @fb_id
|
||||
}
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,32 @@
|
||||
class NotificationBuilder
|
||||
pattr_initialize [:notification_type!, :user!, :account!, :primary_actor!]
|
||||
|
||||
def perform
|
||||
return unless user_subscribed_to_notification?
|
||||
|
||||
build_notification
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def secondary_actor
|
||||
Current.user
|
||||
end
|
||||
|
||||
def user_subscribed_to_notification?
|
||||
notification_setting = user.notification_settings.find_by(account_id: account.id)
|
||||
return true if notification_setting.public_send("email_#{notification_type}?")
|
||||
return true if notification_setting.public_send("push_#{notification_type}?")
|
||||
|
||||
false
|
||||
end
|
||||
|
||||
def build_notification
|
||||
user.notifications.create!(
|
||||
notification_type: notification_type,
|
||||
account: account,
|
||||
primary_actor: primary_actor,
|
||||
secondary_actor: secondary_actor
|
||||
)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,30 @@
|
||||
class NotificationSubscriptionBuilder
|
||||
pattr_initialize [:params, :user!]
|
||||
|
||||
def perform
|
||||
# if multiple accounts were used to login in same browser
|
||||
move_subscription_to_user if identifier_subscription && identifier_subscription.user_id != user.id
|
||||
build_identifier_subscription if identifier_subscription.blank?
|
||||
identifier_subscription
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def identifier
|
||||
@identifier ||= params[:subscription_attributes][:endpoint] if params[:subscription_type] == 'browser_push'
|
||||
@identifier ||= params[:subscription_attributes][:device_id] if params[:subscription_type] == 'fcm'
|
||||
@identifier
|
||||
end
|
||||
|
||||
def identifier_subscription
|
||||
@identifier_subscription ||= NotificationSubscription.find_by(identifier: identifier)
|
||||
end
|
||||
|
||||
def move_subscription_to_user
|
||||
@identifier_subscription.update(user_id: user.id)
|
||||
end
|
||||
|
||||
def build_identifier_subscription
|
||||
user.notification_subscriptions.create(params.merge(identifier: identifier))
|
||||
end
|
||||
end
|
||||
@@ -1,77 +0,0 @@
|
||||
class ReportBuilder
|
||||
include CustomExceptions::Report
|
||||
|
||||
# Usage
|
||||
# rb = ReportBuilder.new a, { metric: 'conversations_count', type: :account, id: 1}
|
||||
# rb = ReportBuilder.new a, { metric: 'avg_first_response_time', type: :agent, id: 1}
|
||||
|
||||
IDENTITY_MAPPING = {
|
||||
account: AccountIdentity,
|
||||
agent: AgentIdentity
|
||||
}.freeze
|
||||
|
||||
def initialize(account, params)
|
||||
@account = account
|
||||
@params = params
|
||||
@identity = get_identity
|
||||
@start_time, @end_time = validate_times
|
||||
end
|
||||
|
||||
def build
|
||||
metric = @identity.send(@params[:metric])
|
||||
if metric.get.nil?
|
||||
metric.delete
|
||||
result = {}
|
||||
else
|
||||
result = metric.get_padded_range(@start_time, @end_time) || {}
|
||||
end
|
||||
formatted_hash(result)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def get_identity
|
||||
identity_class = IDENTITY_MAPPING[@params[:type]]
|
||||
raise InvalidIdentity if identity_class.nil?
|
||||
|
||||
@params[:id] = @account.id if identity_class == AccountIdentity
|
||||
identity_id = @params[:id]
|
||||
raise IdentityNotFound if identity_id.nil?
|
||||
|
||||
tags = identity_class == AccountIdentity ? nil : { account_id: @account.id }
|
||||
identity = identity_class.new(identity_id, tags: tags)
|
||||
raise MetricNotFound if @params[:metric].blank?
|
||||
raise MetricNotFound unless identity.respond_to?(@params[:metric])
|
||||
|
||||
identity
|
||||
end
|
||||
|
||||
def validate_times
|
||||
start_time = @params[:since] || Time.now.end_of_day - 30.days
|
||||
end_time = @params[:until] || Time.now.end_of_day
|
||||
start_time = begin
|
||||
parse_date_time(start_time)
|
||||
rescue StandardError
|
||||
raise(InvalidStartTime)
|
||||
end
|
||||
end_time = begin
|
||||
parse_date_time(end_time)
|
||||
rescue StandardError
|
||||
raise(InvalidEndTime)
|
||||
end
|
||||
[start_time, end_time]
|
||||
end
|
||||
|
||||
def parse_date_time(datetime)
|
||||
return datetime if datetime.is_a?(DateTime)
|
||||
return datetime.to_datetime if datetime.is_a?(Time) || datetime.is_a?(Date)
|
||||
|
||||
DateTime.strptime(datetime, '%s')
|
||||
end
|
||||
|
||||
def formatted_hash(hash)
|
||||
hash.each_with_object([]) do |p, arr|
|
||||
arr << { value: p[1], timestamp: p[0] }
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,111 @@
|
||||
class V2::ReportBuilder
|
||||
attr_reader :account, :params
|
||||
|
||||
def initialize(account, params)
|
||||
@account = account
|
||||
@params = params
|
||||
end
|
||||
|
||||
def timeseries
|
||||
send(params[:metric])
|
||||
end
|
||||
|
||||
# For backward compatible with old report
|
||||
def build
|
||||
timeseries.each_with_object([]) do |p, arr|
|
||||
arr << { value: p[1], timestamp: p[0].to_time.to_i }
|
||||
end
|
||||
end
|
||||
|
||||
def summary
|
||||
{
|
||||
conversations_count: conversations_count.values.sum,
|
||||
incoming_messages_count: incoming_messages_count.values.sum,
|
||||
outgoing_messages_count: outgoing_messages_count.values.sum,
|
||||
avg_first_response_time: avg_first_response_time_summary,
|
||||
avg_resolution_time: avg_resolution_time_summary,
|
||||
resolutions_count: resolutions_count.values.sum
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def scope
|
||||
return account if params[:type].match?('account')
|
||||
return inbox if params[:type].match?('inbox')
|
||||
return user if params[:type].match?('agent')
|
||||
end
|
||||
|
||||
def inbox
|
||||
@inbox ||= account.inboxes.where(id: params[:id]).first
|
||||
end
|
||||
|
||||
def user
|
||||
@user ||= account.users.where(id: params[:id]).first
|
||||
end
|
||||
|
||||
def conversations_count
|
||||
scope.conversations
|
||||
.group_by_day(:created_at, range: range, default_value: 0)
|
||||
.count
|
||||
end
|
||||
|
||||
# unscoped removes all scopes added to a model previously
|
||||
def incoming_messages_count
|
||||
scope.messages.unscoped.where(account_id: account.id).incoming
|
||||
.group_by_day(:created_at, range: range, default_value: 0)
|
||||
.count
|
||||
end
|
||||
|
||||
def outgoing_messages_count
|
||||
scope.messages.unscoped.where(account_id: account.id).outgoing
|
||||
.group_by_day(:created_at, range: range, default_value: 0)
|
||||
.count
|
||||
end
|
||||
|
||||
def resolutions_count
|
||||
scope.conversations
|
||||
.resolved
|
||||
.group_by_day(:created_at, range: range, default_value: 0)
|
||||
.count
|
||||
end
|
||||
|
||||
def avg_first_response_time
|
||||
scope.events
|
||||
.where(name: 'first_response')
|
||||
.group_by_day(:created_at, range: range, default_value: 0)
|
||||
.average(:value)
|
||||
end
|
||||
|
||||
def avg_resolution_time
|
||||
scope.events.where(name: 'conversation_resolved')
|
||||
.group_by_day(:created_at, range: range, default_value: 0)
|
||||
.average(:value)
|
||||
end
|
||||
|
||||
def range
|
||||
parse_date_time(params[:since])..parse_date_time(params[:until])
|
||||
end
|
||||
|
||||
# Taking average of average is not too accurate
|
||||
# https://en.wikipedia.org/wiki/Simpson's_paradox
|
||||
# TODO: Will optimize this later
|
||||
def avg_resolution_time_summary
|
||||
return 0 if avg_resolution_time.values.empty?
|
||||
|
||||
(avg_resolution_time.values.sum / avg_resolution_time.values.length)
|
||||
end
|
||||
|
||||
def avg_first_response_time_summary
|
||||
return 0 if avg_first_response_time.values.empty?
|
||||
|
||||
(avg_first_response_time.values.sum / avg_first_response_time.values.length)
|
||||
end
|
||||
|
||||
def parse_date_time(datetime)
|
||||
return datetime if datetime.is_a?(DateTime)
|
||||
return datetime.to_datetime if datetime.is_a?(Time) || datetime.is_a?(Date)
|
||||
|
||||
DateTime.strptime(datetime, '%s')
|
||||
end
|
||||
end
|
||||
@@ -1,5 +1,47 @@
|
||||
class RoomChannel < ApplicationCable::Channel
|
||||
def subscribed
|
||||
stream_from params[:pubsub_token]
|
||||
ensure_stream
|
||||
current_user
|
||||
current_account
|
||||
update_subscription
|
||||
broadcast_presence
|
||||
end
|
||||
|
||||
def update_presence
|
||||
update_subscription
|
||||
broadcast_presence
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def broadcast_presence
|
||||
data = { account_id: @current_account.id, users: ::OnlineStatusTracker.get_available_users(@current_account.id) }
|
||||
data[:contacts] = ::OnlineStatusTracker.get_available_contacts(@current_account.id) if @current_user.is_a? User
|
||||
ActionCable.server.broadcast(@pubsub_token, { event: 'presence.update', data: data })
|
||||
end
|
||||
|
||||
def ensure_stream
|
||||
@pubsub_token = params[:pubsub_token]
|
||||
stream_from @pubsub_token
|
||||
end
|
||||
|
||||
def update_subscription
|
||||
::OnlineStatusTracker.update_presence(@current_account.id, @current_user.class.name, @current_user.id)
|
||||
end
|
||||
|
||||
def current_user
|
||||
@current_user ||= if params[:user_id].blank?
|
||||
Contact.find_by!(pubsub_token: @pubsub_token)
|
||||
else
|
||||
User.find_by!(pubsub_token: @pubsub_token, id: params[:user_id])
|
||||
end
|
||||
end
|
||||
|
||||
def current_account
|
||||
@current_account ||= if @current_user.is_a? Contact
|
||||
@current_user.account
|
||||
else
|
||||
@current_user.accounts.find(params[:account_id])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
class Api::BaseController < ApplicationController
|
||||
include AccessTokenAuthHelper
|
||||
respond_to :json
|
||||
before_action :authenticate_user!
|
||||
unless Rails.env.development?
|
||||
rescue_from StandardError do |exception|
|
||||
Raven.capture_exception(exception)
|
||||
render json: { error: '500 error', message: exception.message }.to_json, status: 500
|
||||
end
|
||||
end
|
||||
before_action :authenticate_access_token!, if: :authenticate_by_access_token?
|
||||
before_action :validate_bot_access_token!, if: :authenticate_by_access_token?
|
||||
before_action :authenticate_user!, unless: :authenticate_by_access_token?
|
||||
|
||||
private
|
||||
|
||||
def set_conversation
|
||||
@conversation ||= current_account.conversations.find_by(display_id: params[:conversation_id])
|
||||
def authenticate_by_access_token?
|
||||
request.headers[:api_access_token].present? || request.headers[:HTTP_API_ACCESS_TOKEN].present?
|
||||
end
|
||||
end
|
||||
|
||||
+3
-3
@@ -1,10 +1,10 @@
|
||||
class Api::V1::Actions::ContactMergesController < Api::BaseController
|
||||
class Api::V1::Accounts::Actions::ContactMergesController < Api::V1::Accounts::BaseController
|
||||
before_action :set_base_contact, only: [:create]
|
||||
before_action :set_mergee_contact, only: [:create]
|
||||
|
||||
def create
|
||||
contact_merge_action = ContactMergeAction.new(
|
||||
account: current_account,
|
||||
account: Current.account,
|
||||
base_contact: @base_contact,
|
||||
mergee_contact: @mergee_contact
|
||||
)
|
||||
@@ -23,6 +23,6 @@ class Api::V1::Actions::ContactMergesController < Api::BaseController
|
||||
end
|
||||
|
||||
def contacts
|
||||
@contacts ||= current_account.contacts
|
||||
@contacts ||= Current.account.contacts
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,69 @@
|
||||
class Api::V1::Accounts::AgentsController < Api::V1::Accounts::BaseController
|
||||
before_action :fetch_agent, except: [:create, :index]
|
||||
before_action :check_authorization
|
||||
before_action :find_user, only: [:create]
|
||||
before_action :create_user, only: [:create]
|
||||
before_action :save_account_user, only: [:create]
|
||||
|
||||
def index
|
||||
@agents = agents
|
||||
end
|
||||
|
||||
def destroy
|
||||
@agent.current_account_user.destroy
|
||||
head :ok
|
||||
end
|
||||
|
||||
def update
|
||||
@agent.update!(agent_params.except(:role))
|
||||
@agent.current_account_user.update!(role: agent_params[:role]) if agent_params[:role]
|
||||
render partial: 'api/v1/models/agent.json.jbuilder', locals: { resource: @agent }
|
||||
end
|
||||
|
||||
def create
|
||||
render partial: 'api/v1/models/agent.json.jbuilder', locals: { resource: @user }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def check_authorization
|
||||
authorize(User)
|
||||
end
|
||||
|
||||
def fetch_agent
|
||||
@agent = agents.find(params[:id])
|
||||
end
|
||||
|
||||
def find_user
|
||||
@user = User.find_by(email: new_agent_params[:email])
|
||||
end
|
||||
|
||||
def create_user
|
||||
return if @user
|
||||
|
||||
@user = User.create!(new_agent_params.slice(:email, :name, :password, :password_confirmation))
|
||||
end
|
||||
|
||||
def save_account_user
|
||||
AccountUser.create!(
|
||||
account_id: Current.account.id,
|
||||
user_id: @user.id,
|
||||
role: new_agent_params[:role],
|
||||
inviter_id: current_user.id
|
||||
)
|
||||
end
|
||||
|
||||
def agent_params
|
||||
params.require(:agent).permit(:email, :name, :role)
|
||||
end
|
||||
|
||||
def new_agent_params
|
||||
time = Time.now.to_i
|
||||
params.require(:agent).permit(:email, :name, :role)
|
||||
.merge!(password: time, password_confirmation: time, inviter: current_user)
|
||||
end
|
||||
|
||||
def agents
|
||||
@agents ||= Current.account.users.order_by_full_name
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,30 @@
|
||||
class Api::V1::Accounts::BaseController < Api::BaseController
|
||||
before_action :current_account
|
||||
|
||||
private
|
||||
|
||||
def current_account
|
||||
@current_account ||= ensure_current_account
|
||||
Current.account = @current_account
|
||||
end
|
||||
|
||||
def ensure_current_account
|
||||
account = Account.find(params[:account_id])
|
||||
if current_user
|
||||
account_accessible_for_user?(account)
|
||||
elsif @resource&.is_a?(AgentBot)
|
||||
account_accessible_for_bot?(account)
|
||||
end
|
||||
account
|
||||
end
|
||||
|
||||
def account_accessible_for_user?(account)
|
||||
@current_account_user = account.account_users.find_by(user_id: current_user.id)
|
||||
Current.account_user = @current_account_user
|
||||
render_unauthorized('You are not authorized to access this account') unless @current_account_user
|
||||
end
|
||||
|
||||
def account_accessible_for_bot?(account)
|
||||
render_unauthorized('You are not authorized to access this account') unless @resource.agent_bot_inboxes.find_by(account_id: account.id)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,107 @@
|
||||
class Api::V1::Accounts::CallbacksController < Api::V1::Accounts::BaseController
|
||||
before_action :inbox, only: [:reauthorize_page]
|
||||
|
||||
def register_facebook_page
|
||||
user_access_token = params[:user_access_token]
|
||||
page_access_token = params[:page_access_token]
|
||||
page_id = params[:page_id]
|
||||
inbox_name = params[:inbox_name]
|
||||
ActiveRecord::Base.transaction do
|
||||
facebook_channel = Current.account.facebook_pages.create!(
|
||||
page_id: page_id, user_access_token: user_access_token,
|
||||
page_access_token: page_access_token
|
||||
)
|
||||
@facebook_inbox = Current.account.inboxes.create!(name: inbox_name, channel: facebook_channel)
|
||||
set_avatar(@facebook_inbox, page_id)
|
||||
rescue StandardError => e
|
||||
Rails.logger.info e
|
||||
end
|
||||
end
|
||||
|
||||
def facebook_pages
|
||||
@page_details = mark_already_existing_facebook_pages(fb_object.get_connections('me', 'accounts'))
|
||||
end
|
||||
|
||||
# get params[:inbox_id], current_account. params[:omniauth_token]
|
||||
def reauthorize_page
|
||||
if @inbox&.facebook?
|
||||
fb_page_id = @inbox.channel.page_id
|
||||
page_details = fb_object.get_connections('me', 'accounts')
|
||||
|
||||
if (page_detail = (page_details || []).detect { |page| fb_page_id == page['id'] })
|
||||
update_fb_page(fb_page_id, page_detail['access_token'])
|
||||
render and return
|
||||
end
|
||||
end
|
||||
|
||||
head :unprocessable_entity
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def inbox
|
||||
@inbox = Current.account.inboxes.find_by(id: params[:inbox_id])
|
||||
end
|
||||
|
||||
def update_fb_page(fb_page_id, access_token)
|
||||
fb_page = get_fb_page(fb_page_id)
|
||||
fb_page&.update!(user_access_token: @user_access_token, page_access_token: access_token)
|
||||
fb_page&.reauthorized!
|
||||
end
|
||||
|
||||
def get_fb_page(fb_page_id)
|
||||
Current.account.facebook_pages.find_by(page_id: fb_page_id)
|
||||
end
|
||||
|
||||
def fb_object
|
||||
@user_access_token = long_lived_token(params[:omniauth_token])
|
||||
Koala::Facebook::API.new(@user_access_token)
|
||||
end
|
||||
|
||||
def long_lived_token(omniauth_token)
|
||||
koala = Koala::Facebook::OAuth.new(ENV['FB_APP_ID'], ENV['FB_APP_SECRET'])
|
||||
koala.exchange_access_token_info(omniauth_token)['access_token']
|
||||
rescue StandardError => e
|
||||
Rails.logger.info e
|
||||
end
|
||||
|
||||
def mark_already_existing_facebook_pages(data)
|
||||
return [] if data.empty?
|
||||
|
||||
data.inject([]) do |result, page_detail|
|
||||
page_detail[:exists] = Current.account.facebook_pages.exists?(page_id: page_detail['id']) ? true : false
|
||||
result << page_detail
|
||||
end
|
||||
end
|
||||
|
||||
def set_avatar(facebook_inbox, page_id)
|
||||
uri = get_avatar_url(page_id)
|
||||
|
||||
return unless uri
|
||||
|
||||
avatar_resource = LocalResource.new(uri)
|
||||
facebook_inbox.avatar.attach(io: avatar_resource.file, filename: avatar_resource.tmp_filename, content_type: avatar_resource.encoding)
|
||||
rescue *ExceptionList::URI_EXCEPTIONS => e
|
||||
Rails.logger.info "invalid url #{file_url} : #{e.message}"
|
||||
end
|
||||
|
||||
def get_avatar_url(page_id)
|
||||
begin
|
||||
url = 'http://graph.facebook.com/' << page_id << '/picture?type=large'
|
||||
uri = URI.parse(url)
|
||||
tries = 3
|
||||
begin
|
||||
response = uri.open(redirect: false)
|
||||
rescue OpenURI::HTTPRedirect => e
|
||||
uri = e.uri # assigned from the "Location" response header
|
||||
retry if (tries -= 1).positive?
|
||||
raise
|
||||
end
|
||||
pic_url = response.base_uri.to_s
|
||||
rescue StandardError => e
|
||||
Rails.logger.debug "Rescued: #{e.inspect}"
|
||||
pic_url = nil
|
||||
end
|
||||
pic_url
|
||||
end
|
||||
end
|
||||
+5
-5
@@ -1,4 +1,4 @@
|
||||
class Api::V1::CannedResponsesController < Api::BaseController
|
||||
class Api::V1::Accounts::CannedResponsesController < Api::V1::Accounts::BaseController
|
||||
before_action :fetch_canned_response, only: [:update, :destroy]
|
||||
|
||||
def index
|
||||
@@ -6,7 +6,7 @@ class Api::V1::CannedResponsesController < Api::BaseController
|
||||
end
|
||||
|
||||
def create
|
||||
@canned_response = current_account.canned_responses.new(canned_response_params)
|
||||
@canned_response = Current.account.canned_responses.new(canned_response_params)
|
||||
@canned_response.save!
|
||||
render json: @canned_response
|
||||
end
|
||||
@@ -24,7 +24,7 @@ class Api::V1::CannedResponsesController < Api::BaseController
|
||||
private
|
||||
|
||||
def fetch_canned_response
|
||||
@canned_response = current_account.canned_responses.find(params[:id])
|
||||
@canned_response = Current.account.canned_responses.find(params[:id])
|
||||
end
|
||||
|
||||
def canned_response_params
|
||||
@@ -33,9 +33,9 @@ class Api::V1::CannedResponsesController < Api::BaseController
|
||||
|
||||
def canned_responses
|
||||
if params[:search]
|
||||
current_account.canned_responses.where('short_code ILIKE ?', "#{params[:search]}%")
|
||||
Current.account.canned_responses.where('short_code ILIKE ?', "#{params[:search]}%")
|
||||
else
|
||||
current_account.canned_responses
|
||||
Current.account.canned_responses
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,57 @@
|
||||
class Api::V1::Accounts::Channels::TwilioChannelsController < Api::V1::Accounts::BaseController
|
||||
before_action :authorize_request
|
||||
|
||||
def create
|
||||
ActiveRecord::Base.transaction do
|
||||
authenticate_twilio
|
||||
build_inbox
|
||||
setup_webhooks if @twilio_channel.sms?
|
||||
rescue Twilio::REST::TwilioError => e
|
||||
render_could_not_create_error(e.message)
|
||||
rescue StandardError => e
|
||||
render_could_not_create_error(e.message)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def authorize_request
|
||||
authorize ::Inbox
|
||||
end
|
||||
|
||||
def authenticate_twilio
|
||||
client = Twilio::REST::Client.new(permitted_params[:account_sid], permitted_params[:auth_token])
|
||||
client.messages.list(limit: 1)
|
||||
end
|
||||
|
||||
def setup_webhooks
|
||||
::Twilio::WebhookSetupService.new(inbox: @inbox).perform
|
||||
end
|
||||
|
||||
def phone_number
|
||||
medium == 'sms' ? permitted_params[:phone_number] : "whatsapp:#{permitted_params[:phone_number]}"
|
||||
end
|
||||
|
||||
def medium
|
||||
permitted_params[:medium]
|
||||
end
|
||||
|
||||
def build_inbox
|
||||
@twilio_channel = Current.account.twilio_sms.create!(
|
||||
account_sid: permitted_params[:account_sid],
|
||||
auth_token: permitted_params[:auth_token],
|
||||
phone_number: phone_number,
|
||||
medium: medium
|
||||
)
|
||||
@inbox = Current.account.inboxes.create(
|
||||
name: permitted_params[:name],
|
||||
channel: @twilio_channel
|
||||
)
|
||||
end
|
||||
|
||||
def permitted_params
|
||||
params.require(:twilio_channel).permit(
|
||||
:account_id, :phone_number, :account_sid, :auth_token, :name, :medium
|
||||
)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,26 @@
|
||||
class Api::V1::Accounts::Contacts::ContactInboxesController < Api::V1::Accounts::BaseController
|
||||
before_action :ensure_contact
|
||||
before_action :ensure_inbox, only: [:create]
|
||||
before_action :validate_channel_type
|
||||
|
||||
def create
|
||||
source_id = params[:source_id] || SecureRandom.uuid
|
||||
@contact_inbox = ContactInbox.create(contact: @contact, inbox: @inbox, source_id: source_id)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def validate_channel_type
|
||||
return if @inbox.channel_type == 'Channel::Api'
|
||||
|
||||
render json: { error: 'Contact Inbox creation is only allowed in API inboxes' }, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
def ensure_inbox
|
||||
@inbox = Current.account.inboxes.find(params[:inbox_id])
|
||||
end
|
||||
|
||||
def ensure_contact
|
||||
@contact = Current.account.contacts.find(params[:contact_id])
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,23 @@
|
||||
class Api::V1::Accounts::Contacts::ConversationsController < Api::V1::Accounts::BaseController
|
||||
def index
|
||||
@conversations = Current.account.conversations.includes(
|
||||
:assignee, :contact, :inbox, :taggings
|
||||
).where(inbox_id: inbox_ids, contact_id: permitted_params[:contact_id])
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def inbox_ids
|
||||
if current_user.administrator?
|
||||
Current.account.inboxes.pluck(:id)
|
||||
elsif current_user.agent?
|
||||
current_user.assigned_inboxes.pluck(:id)
|
||||
else
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
def permitted_params
|
||||
params.permit(:contact_id)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,74 @@
|
||||
class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
||||
protect_from_forgery with: :null_session
|
||||
|
||||
before_action :check_authorization
|
||||
before_action :fetch_contact, only: [:show, :update]
|
||||
|
||||
def index
|
||||
@contacts = Current.account.contacts
|
||||
end
|
||||
|
||||
# returns online contacts
|
||||
def active
|
||||
@contacts = Current.account.contacts.where(id: ::OnlineStatusTracker
|
||||
.get_available_contact_ids(Current.account.id))
|
||||
end
|
||||
|
||||
def show; end
|
||||
|
||||
def create
|
||||
ActiveRecord::Base.transaction do
|
||||
@contact = Current.account.contacts.new(contact_params)
|
||||
@contact.save!
|
||||
@contact_inbox = build_contact_inbox
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
@contact.update!(contact_update_params)
|
||||
rescue ActiveRecord::RecordInvalid => e
|
||||
render json: {
|
||||
message: e.record.errors.full_messages.join(', '),
|
||||
contact: Contact.find_by(email: contact_params[:email])
|
||||
}, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
def search
|
||||
render json: { error: 'Specify search string with parameter q' }, status: :unprocessable_entity if params[:q].blank? && return
|
||||
|
||||
@contacts = Current.account.contacts.where('name LIKE :search OR email LIKE :search', search: "%#{params[:q]}%")
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def check_authorization
|
||||
authorize(Contact)
|
||||
end
|
||||
|
||||
def build_contact_inbox
|
||||
return if params[:inbox_id].blank?
|
||||
|
||||
inbox = Current.account.inboxes.find(params[:inbox_id])
|
||||
source_id = params[:source_id] || SecureRandom.uuid
|
||||
ContactInbox.create(contact: @contact, inbox: inbox, source_id: source_id)
|
||||
end
|
||||
|
||||
def contact_params
|
||||
params.require(:contact).permit(:name, :email, :phone_number, additional_attributes: {}, custom_attributes: {})
|
||||
end
|
||||
|
||||
def contact_custom_attributes
|
||||
return @contact.custom_attributes.merge(contact_params[:custom_attributes]) if contact_params[:custom_attributes]
|
||||
|
||||
@contact.custom_attributes
|
||||
end
|
||||
|
||||
def contact_update_params
|
||||
# we want the merged custom attributes not the original one
|
||||
contact_params.except(:custom_attributes).merge({ custom_attributes: contact_custom_attributes })
|
||||
end
|
||||
|
||||
def fetch_contact
|
||||
@contact = Current.account.contacts.includes(contact_inboxes: [:inbox]).find(params[:id])
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,9 @@
|
||||
class Api::V1::Accounts::Conversations::AssignmentsController < Api::V1::Accounts::Conversations::BaseController
|
||||
# assign agent to a conversation
|
||||
def create
|
||||
# if params[:assignee_id] is not a valid id, it will set to nil, hence unassigning the conversation
|
||||
assignee = Current.account.users.find_by(id: params[:assignee_id])
|
||||
@conversation.update_assignee(assignee)
|
||||
render json: assignee
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,9 @@
|
||||
class Api::V1::Accounts::Conversations::BaseController < Api::V1::Accounts::BaseController
|
||||
before_action :conversation
|
||||
|
||||
private
|
||||
|
||||
def conversation
|
||||
@conversation ||= Current.account.conversations.find_by(display_id: params[:conversation_id])
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,11 @@
|
||||
class Api::V1::Accounts::Conversations::LabelsController < Api::V1::Accounts::Conversations::BaseController
|
||||
def create
|
||||
@conversation.update_labels(params[:labels])
|
||||
@labels = @conversation.label_list
|
||||
end
|
||||
|
||||
# all labels of the current conversation
|
||||
def index
|
||||
@labels = @conversation.label_list
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,19 @@
|
||||
class Api::V1::Accounts::Conversations::MessagesController < Api::V1::Accounts::Conversations::BaseController
|
||||
def index
|
||||
@messages = message_finder.perform
|
||||
end
|
||||
|
||||
def create
|
||||
user = current_user || @resource
|
||||
mb = Messages::MessageBuilder.new(user, @conversation, params)
|
||||
@message = mb.perform
|
||||
rescue StandardError => e
|
||||
render_could_not_create_error(e.message)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def message_finder
|
||||
@message_finder ||= MessageFinder.new(@conversation, params)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,96 @@
|
||||
class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseController
|
||||
include Events::Types
|
||||
|
||||
before_action :conversation, except: [:index]
|
||||
before_action :contact_inbox, only: [:create]
|
||||
|
||||
def index
|
||||
result = conversation_finder.perform
|
||||
@conversations = result[:conversations]
|
||||
@conversations_count = result[:count]
|
||||
end
|
||||
|
||||
def meta
|
||||
result = conversation_finder.perform
|
||||
@conversations_count = result[:count]
|
||||
end
|
||||
|
||||
def search
|
||||
result = conversation_finder.perform
|
||||
@conversations = result[:conversations]
|
||||
@conversations_count = result[:count]
|
||||
end
|
||||
|
||||
def create
|
||||
@conversation = ::Conversation.create!(conversation_params)
|
||||
end
|
||||
|
||||
def show; end
|
||||
|
||||
def mute
|
||||
@conversation.mute!
|
||||
head :ok
|
||||
end
|
||||
|
||||
def unmute
|
||||
@conversation.unmute!
|
||||
head :ok
|
||||
end
|
||||
|
||||
def transcript
|
||||
ConversationReplyMailer.conversation_transcript(@conversation, params[:email])&.deliver_later if params[:email].present?
|
||||
head :ok
|
||||
end
|
||||
|
||||
def toggle_status
|
||||
if params[:status]
|
||||
@conversation.status = params[:status]
|
||||
@status = @conversation.save
|
||||
else
|
||||
@status = @conversation.toggle_status
|
||||
end
|
||||
end
|
||||
|
||||
def toggle_typing_status
|
||||
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.agent_last_seen_at = DateTime.now.utc
|
||||
@conversation.save!
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def trigger_typing_event(event)
|
||||
user = current_user.presence || @resource
|
||||
Rails.configuration.dispatcher.dispatch(event, Time.zone.now, conversation: @conversation, user: user)
|
||||
end
|
||||
|
||||
def conversation
|
||||
@conversation ||= Current.account.conversations.find_by(display_id: params[:id])
|
||||
end
|
||||
|
||||
def contact_inbox
|
||||
@contact_inbox ||= ::ContactInbox.find_by!(source_id: params[:source_id])
|
||||
end
|
||||
|
||||
def conversation_params
|
||||
{
|
||||
account_id: Current.account.id,
|
||||
inbox_id: @contact_inbox.inbox_id,
|
||||
contact_id: @contact_inbox.contact_id,
|
||||
contact_inbox_id: @contact_inbox.id
|
||||
}
|
||||
end
|
||||
|
||||
def conversation_finder
|
||||
@conversation_finder ||= ConversationFinder.new(current_user, params)
|
||||
end
|
||||
end
|
||||
+3
-2
@@ -1,4 +1,4 @@
|
||||
class Api::V1::FacebookIndicatorsController < Api::BaseController
|
||||
class Api::V1::Accounts::FacebookIndicatorsController < Api::V1::Accounts::BaseController
|
||||
before_action :set_access_token
|
||||
around_action :handle_with_exception
|
||||
|
||||
@@ -26,6 +26,7 @@ class Api::V1::FacebookIndicatorsController < Api::BaseController
|
||||
def handle_with_exception
|
||||
yield
|
||||
rescue Facebook::Messenger::Error => e
|
||||
Rails.logger.debug "Rescued: #{e.inspect}"
|
||||
true
|
||||
end
|
||||
|
||||
@@ -37,7 +38,7 @@ class Api::V1::FacebookIndicatorsController < Api::BaseController
|
||||
end
|
||||
|
||||
def inbox
|
||||
@inbox ||= current_account.inboxes.find(permitted_params[:inbox_id])
|
||||
@inbox ||= Current.account.inboxes.find(permitted_params[:inbox_id])
|
||||
end
|
||||
|
||||
def set_access_token
|
||||
@@ -0,0 +1,45 @@
|
||||
class Api::V1::Accounts::InboxMembersController < Api::V1::Accounts::BaseController
|
||||
before_action :fetch_inbox, only: [:create, :show]
|
||||
before_action :current_agents_ids, only: [:create]
|
||||
|
||||
def create
|
||||
# update also done via same action
|
||||
update_agents_list
|
||||
head :ok
|
||||
rescue StandardError => e
|
||||
Rails.logger.debug "Rescued: #{e.inspect}"
|
||||
render_could_not_create_error('Could not add agents to inbox')
|
||||
end
|
||||
|
||||
def show
|
||||
@agents = Current.account.users.where(id: @inbox.members.select(:user_id))
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def update_agents_list
|
||||
# get all the user_ids which the inbox currently has as members.
|
||||
# get the list of user_ids from params
|
||||
# the missing ones are the agents which are to be deleted from the inbox
|
||||
# the new ones are the agents which are to be added to the inbox
|
||||
|
||||
agents_to_be_added_ids.each { |user_id| @inbox.add_member(user_id) }
|
||||
agents_to_be_removed_ids.each { |user_id| @inbox.remove_member(user_id) }
|
||||
end
|
||||
|
||||
def agents_to_be_added_ids
|
||||
params[:user_ids] - @current_agents_ids
|
||||
end
|
||||
|
||||
def agents_to_be_removed_ids
|
||||
@current_agents_ids - params[:user_ids]
|
||||
end
|
||||
|
||||
def current_agents_ids
|
||||
@current_agents_ids = @inbox.members.pluck(:id)
|
||||
end
|
||||
|
||||
def fetch_inbox
|
||||
@inbox = Current.account.inboxes.find(params[:inbox_id])
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,98 @@
|
||||
class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
||||
before_action :fetch_inbox, except: [:index, :create]
|
||||
before_action :fetch_agent_bot, only: [:set_agent_bot]
|
||||
before_action :check_authorization
|
||||
|
||||
def index
|
||||
@inboxes = policy_scope(Current.account.inboxes.order_by_name.includes(:channel, :avatar_attachment))
|
||||
end
|
||||
|
||||
def create
|
||||
ActiveRecord::Base.transaction do
|
||||
channel = create_channel
|
||||
@inbox = Current.account.inboxes.build(
|
||||
name: permitted_params[:name],
|
||||
greeting_message: permitted_params[:greeting_message],
|
||||
greeting_enabled: permitted_params[:greeting_enabled],
|
||||
channel: channel
|
||||
)
|
||||
@inbox.avatar.attach(permitted_params[:avatar])
|
||||
@inbox.save!
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
@inbox.update(inbox_update_params.except(:channel))
|
||||
return unless @inbox.channel.is_a?(Channel::WebWidget) && inbox_update_params[:channel].present?
|
||||
|
||||
@inbox.channel.update!(inbox_update_params[:channel])
|
||||
update_channel_feature_flags
|
||||
end
|
||||
|
||||
def set_agent_bot
|
||||
if @agent_bot
|
||||
agent_bot_inbox = @inbox.agent_bot_inbox || AgentBotInbox.new(inbox: @inbox)
|
||||
agent_bot_inbox.agent_bot = @agent_bot
|
||||
agent_bot_inbox.save!
|
||||
elsif @inbox.agent_bot_inbox.present?
|
||||
@inbox.agent_bot_inbox.destroy!
|
||||
end
|
||||
head :ok
|
||||
end
|
||||
|
||||
def destroy
|
||||
@inbox.destroy
|
||||
head :ok
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def fetch_inbox
|
||||
@inbox = Current.account.inboxes.find(params[:id])
|
||||
end
|
||||
|
||||
def fetch_agent_bot
|
||||
@agent_bot = AgentBot.find(params[:agent_bot]) if params[:agent_bot]
|
||||
end
|
||||
|
||||
def check_authorization
|
||||
authorize(Inbox)
|
||||
end
|
||||
|
||||
def create_channel
|
||||
case permitted_params[:channel][:type]
|
||||
when 'web_widget'
|
||||
Current.account.web_widgets.create!(permitted_params[:channel].except(:type))
|
||||
when 'api'
|
||||
Current.account.api_channels.create!(permitted_params[:channel].except(:type))
|
||||
when 'email'
|
||||
Current.account.email_channels.create!(permitted_params[:channel].except(:type))
|
||||
end
|
||||
end
|
||||
|
||||
def update_channel_feature_flags
|
||||
return unless inbox_update_params[:channel].key? :selected_feature_flags
|
||||
|
||||
@inbox.channel.selected_feature_flags = inbox_update_params[:channel][:selected_feature_flags]
|
||||
@inbox.channel.save!
|
||||
end
|
||||
|
||||
def permitted_params
|
||||
params.permit(:id, :avatar, :name, :greeting_message, :greeting_enabled, channel:
|
||||
[:type, :website_url, :widget_color, :welcome_title, :welcome_tagline, :webhook_url, :email, :reply_time])
|
||||
end
|
||||
|
||||
def inbox_update_params
|
||||
params.permit(:enable_auto_assignment, :name, :avatar, :greeting_message, :greeting_enabled,
|
||||
channel: [
|
||||
:website_url,
|
||||
:widget_color,
|
||||
:welcome_title,
|
||||
:welcome_tagline,
|
||||
:webhook_url,
|
||||
:email,
|
||||
:reply_time,
|
||||
{ selected_feature_flags: [] }
|
||||
])
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,18 @@
|
||||
class Api::V1::Accounts::Integrations::AppsController < Api::V1::Accounts::BaseController
|
||||
before_action :fetch_apps, only: [:index]
|
||||
before_action :fetch_app, only: [:show]
|
||||
|
||||
def index; end
|
||||
|
||||
def show; end
|
||||
|
||||
private
|
||||
|
||||
def fetch_apps
|
||||
@apps = Integrations::App.all.select(&:active?)
|
||||
end
|
||||
|
||||
def fetch_app
|
||||
@app = Integrations::App.find(id: params[:id])
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,38 @@
|
||||
class Api::V1::Accounts::Integrations::SlackController < Api::V1::Accounts::BaseController
|
||||
before_action :fetch_hook, only: [:update, :destroy]
|
||||
|
||||
def create
|
||||
builder = Integrations::Slack::HookBuilder.new(
|
||||
account: Current.account,
|
||||
code: params[:code],
|
||||
inbox_id: params[:inbox_id]
|
||||
)
|
||||
@hook = builder.perform
|
||||
create_chatwoot_slack_channel
|
||||
end
|
||||
|
||||
def update
|
||||
create_chatwoot_slack_channel
|
||||
render json: @hook
|
||||
end
|
||||
|
||||
def destroy
|
||||
@hook.destroy
|
||||
|
||||
head :ok
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def fetch_hook
|
||||
@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
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,9 @@
|
||||
class Api::V1::Accounts::Kbase::BaseController < Api::V1::Accounts::BaseController
|
||||
before_action :portal
|
||||
|
||||
private
|
||||
|
||||
def portal
|
||||
@portal ||= Current.account.kbase_portals.find_by(id: params[:portal_id])
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,32 @@
|
||||
class Api::V1::Accounts::Kbase::CategoriesController < Api::V1::Accounts::Kbase::BaseController
|
||||
before_action :fetch_category, except: [:index, :create]
|
||||
|
||||
def index
|
||||
@categories = @portal.categories
|
||||
end
|
||||
|
||||
def create
|
||||
@category = @portal.categories.create!(category_params)
|
||||
end
|
||||
|
||||
def update
|
||||
@category.update!(category_params)
|
||||
end
|
||||
|
||||
def destroy
|
||||
@category.destroy
|
||||
head :ok
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def fetch_category
|
||||
@category = @portal.categories.find(params[:id])
|
||||
end
|
||||
|
||||
def category_params
|
||||
params.require(:category).permit(
|
||||
:name, :description, :position
|
||||
)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,32 @@
|
||||
class Api::V1::Accounts::Kbase::PortalsController < Api::V1::Accounts::Kbase::BaseController
|
||||
before_action :fetch_portal, except: [:index, :create]
|
||||
|
||||
def index
|
||||
@portals = Current.account.kbase_portals
|
||||
end
|
||||
|
||||
def create
|
||||
@portal = Current.account.kbase_portals.create!(portal_params)
|
||||
end
|
||||
|
||||
def update
|
||||
@portal.update!(portal_params)
|
||||
end
|
||||
|
||||
def destroy
|
||||
@portal.destroy
|
||||
head :ok
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def fetch_portal
|
||||
@portal = current_account.kbase_portals.find(params[:id])
|
||||
end
|
||||
|
||||
def portal_params
|
||||
params.require(:portal).permit(
|
||||
:account_id, :color, :custom_domain, :header_text, :homepage_link, :name, :page_title, :slug
|
||||
)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,38 @@
|
||||
class Api::V1::Accounts::LabelsController < Api::V1::Accounts::BaseController
|
||||
before_action :current_account
|
||||
before_action :fetch_label, except: [:index, :create]
|
||||
before_action :check_authorization
|
||||
|
||||
def index
|
||||
@labels = policy_scope(Current.account.labels)
|
||||
end
|
||||
|
||||
def show; end
|
||||
|
||||
def create
|
||||
@label = Current.account.labels.create!(permitted_params)
|
||||
end
|
||||
|
||||
def update
|
||||
@label.update!(permitted_params)
|
||||
end
|
||||
|
||||
def destroy
|
||||
@label.destroy
|
||||
head :ok
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def fetch_label
|
||||
@label = Current.account.labels.find(params[:id])
|
||||
end
|
||||
|
||||
def check_authorization
|
||||
authorize(Label)
|
||||
end
|
||||
|
||||
def permitted_params
|
||||
params.require(:label).permit(:title, :description, :color, :show_on_sidebar)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,30 @@
|
||||
class Api::V1::Accounts::NotificationSettingsController < Api::V1::Accounts::BaseController
|
||||
before_action :set_user, :load_notification_setting
|
||||
|
||||
def show; end
|
||||
|
||||
def update
|
||||
update_flags
|
||||
@notification_setting.save!
|
||||
render action: 'show'
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_user
|
||||
@user = current_user
|
||||
end
|
||||
|
||||
def load_notification_setting
|
||||
@notification_setting = @user.notification_settings.find_by(account_id: Current.account.id)
|
||||
end
|
||||
|
||||
def notification_setting_params
|
||||
params.require(:notification_settings).permit(selected_email_flags: [], selected_push_flags: [])
|
||||
end
|
||||
|
||||
def update_flags
|
||||
@notification_setting.selected_email_flags = notification_setting_params[:selected_email_flags]
|
||||
@notification_setting.selected_push_flags = notification_setting_params[:selected_push_flags]
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,40 @@
|
||||
class Api::V1::Accounts::NotificationsController < Api::V1::Accounts::BaseController
|
||||
protect_from_forgery with: :null_session
|
||||
|
||||
before_action :fetch_notification, only: [:update]
|
||||
before_action :set_primary_actor, only: [:read_all]
|
||||
|
||||
def index
|
||||
@unread_count = current_user.notifications.where(account_id: current_account.id, read_at: nil).count
|
||||
@notifications = current_user.notifications.where(account_id: current_account.id).page params[:page]
|
||||
end
|
||||
|
||||
def read_all
|
||||
if @primary_actor
|
||||
current_user.notifications.where(account_id: current_account.id, primary_actor: @primary_actor, read_at: nil)
|
||||
.update(read_at: DateTime.now.utc)
|
||||
else
|
||||
current_user.notifications.where(account_id: current_account.id, read_at: nil).update(read_at: DateTime.now.utc)
|
||||
end
|
||||
|
||||
head :ok
|
||||
end
|
||||
|
||||
def update
|
||||
@notification.update(read_at: DateTime.now.utc)
|
||||
render json: @notification
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_primary_actor
|
||||
return unless params[:primary_actor_type]
|
||||
return unless Notification::PRIMARY_ACTORS.include?(params[:primary_actor_type])
|
||||
|
||||
@primary_actor = params[:primary_actor_type].safe_constantize.find_by(id: params[:primary_actor_id])
|
||||
end
|
||||
|
||||
def fetch_notification
|
||||
@notification = current_user.notifications.find(params[:id])
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,36 @@
|
||||
class Api::V1::Accounts::WebhooksController < Api::V1::Accounts::BaseController
|
||||
before_action :check_authorization
|
||||
before_action :fetch_webhook, only: [:update, :destroy]
|
||||
|
||||
def index
|
||||
@webhooks = Current.account.webhooks
|
||||
end
|
||||
|
||||
def create
|
||||
@webhook = Current.account.webhooks.new(webhook_params)
|
||||
@webhook.save!
|
||||
end
|
||||
|
||||
def update
|
||||
@webhook.update!(webhook_params)
|
||||
end
|
||||
|
||||
def destroy
|
||||
@webhook.destroy
|
||||
head :ok
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def webhook_params
|
||||
params.require(:webhook).permit(:inbox_id, :url)
|
||||
end
|
||||
|
||||
def fetch_webhook
|
||||
@webhook = Current.account.webhooks.find(params[:id])
|
||||
end
|
||||
|
||||
def check_authorization
|
||||
authorize(Webhook)
|
||||
end
|
||||
end
|
||||
@@ -2,8 +2,11 @@ class Api::V1::AccountsController < Api::BaseController
|
||||
include AuthHelper
|
||||
|
||||
skip_before_action :verify_authenticity_token, only: [:create]
|
||||
skip_before_action :authenticate_user!, :set_current_user, :check_subscription, :handle_with_exception,
|
||||
skip_before_action :authenticate_user!, :set_current_user, :handle_with_exception,
|
||||
only: [:create], raise: false
|
||||
before_action :check_signup_enabled, only: [:create]
|
||||
before_action :fetch_account, except: [:create]
|
||||
before_action :check_authorization, except: [:create]
|
||||
|
||||
rescue_from CustomExceptions::Account::InvalidEmail,
|
||||
CustomExceptions::Account::UserExists,
|
||||
@@ -11,23 +14,62 @@ class Api::V1::AccountsController < Api::BaseController
|
||||
with: :render_error_response
|
||||
|
||||
def create
|
||||
@user = AccountBuilder.new(
|
||||
@user, @account = AccountBuilder.new(
|
||||
account_name: account_params[:account_name],
|
||||
email: account_params[:email]
|
||||
email: account_params[:email],
|
||||
confirmed: confirmed?,
|
||||
user: current_user
|
||||
).perform
|
||||
if @user
|
||||
send_auth_headers(@user)
|
||||
render json: {
|
||||
data: @user.token_validation_response
|
||||
}
|
||||
render 'api/v1/accounts/create.json', locals: { resource: @user }
|
||||
else
|
||||
render_error_response(CustomExceptions::Account::SignupFailed.new({}))
|
||||
end
|
||||
end
|
||||
|
||||
def show
|
||||
render 'api/v1/accounts/show.json'
|
||||
end
|
||||
|
||||
def update
|
||||
@account.update!(account_params.slice(:name, :locale, :domain, :support_email))
|
||||
end
|
||||
|
||||
def update_active_at
|
||||
@current_account_user.active_at = Time.now.utc
|
||||
@current_account_user.save!
|
||||
head :ok
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def check_authorization
|
||||
authorize(Account)
|
||||
end
|
||||
|
||||
def confirmed?
|
||||
super_admin? && params[:confirmed]
|
||||
end
|
||||
|
||||
def fetch_account
|
||||
@account = current_user.accounts.find(params[:id])
|
||||
@current_account_user = @account.account_users.find_by(user_id: current_user.id)
|
||||
end
|
||||
|
||||
def account_params
|
||||
params.permit(:account_name, :email)
|
||||
params.permit(:account_name, :email, :name, :locale, :domain, :support_email)
|
||||
end
|
||||
|
||||
def check_signup_enabled
|
||||
raise ActionController::RoutingError, 'Not Found' if ENV.fetch('ENABLE_ACCOUNT_SIGNUP', true) == 'false'
|
||||
end
|
||||
|
||||
def pundit_user
|
||||
{
|
||||
user: current_user,
|
||||
account: @account,
|
||||
account_user: @current_account_user
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
class Api::V1::AgentBotsController < Api::BaseController
|
||||
skip_before_action :authenticate_user!
|
||||
|
||||
def index
|
||||
render json: AgentBot.all
|
||||
end
|
||||
end
|
||||
@@ -1,52 +0,0 @@
|
||||
class Api::V1::AgentsController < Api::BaseController
|
||||
before_action :fetch_agent, except: [:create, :index]
|
||||
before_action :check_authorization
|
||||
before_action :build_agent, only: [:create]
|
||||
|
||||
def index
|
||||
render json: agents
|
||||
end
|
||||
|
||||
def destroy
|
||||
@agent.destroy
|
||||
head :ok
|
||||
end
|
||||
|
||||
def update
|
||||
@agent.update!(agent_params)
|
||||
render json: @agent
|
||||
end
|
||||
|
||||
def create
|
||||
@agent.save!
|
||||
render json: @agent
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def check_authorization
|
||||
authorize(User)
|
||||
end
|
||||
|
||||
def fetch_agent
|
||||
@agent = agents.find(params[:id])
|
||||
end
|
||||
|
||||
def build_agent
|
||||
@agent = agents.new(new_agent_params)
|
||||
end
|
||||
|
||||
def agent_params
|
||||
params.require(:agent).permit(:email, :name, :role)
|
||||
end
|
||||
|
||||
def new_agent_params
|
||||
time = Time.now.to_i
|
||||
params.require(:agent).permit(:email, :name, :role)
|
||||
.merge!(password: time, password_confirmation: time, inviter: current_user)
|
||||
end
|
||||
|
||||
def agents
|
||||
@agents ||= current_account.users
|
||||
end
|
||||
end
|
||||
@@ -1,101 +0,0 @@
|
||||
require 'rest-client'
|
||||
require 'telegram/bot'
|
||||
class Api::V1::CallbacksController < ApplicationController
|
||||
skip_before_action :verify_authenticity_token, only: [:register_facebook_page]
|
||||
skip_before_action :authenticate_user!, only: [:register_facebook_page], raise: false
|
||||
|
||||
def register_facebook_page
|
||||
user_access_token = params[:user_access_token]
|
||||
page_access_token = params[:page_access_token]
|
||||
page_name = params[:page_name]
|
||||
page_id = params[:page_id]
|
||||
inbox_name = params[:inbox_name]
|
||||
facebook_channel = current_account.facebook_pages.create!(
|
||||
name: page_name, page_id: page_id, user_access_token: user_access_token,
|
||||
page_access_token: page_access_token, remote_avatar_url: set_avatar(page_id)
|
||||
)
|
||||
inbox = current_account.inboxes.create!(name: inbox_name, channel: facebook_channel)
|
||||
render json: inbox
|
||||
end
|
||||
|
||||
def get_facebook_pages
|
||||
@page_details = mark_already_existing_facebook_pages(fb_object.get_connections('me', 'accounts'))
|
||||
end
|
||||
|
||||
# get params[:inbox_id], current_account, params[:omniauth_token]
|
||||
def reauthorize_page
|
||||
if @inbox&.first&.facebook?
|
||||
fb_page_id = @inbox.channel.page_id
|
||||
page_details = fb_object.get_connections('me', 'accounts')
|
||||
|
||||
(page_details || []).each do |page_detail|
|
||||
if fb_page_id == page_detail['id'] # found the page which has to be reauthorised
|
||||
update_fb_page(fb_page_id, page_detail['access_token'])
|
||||
head :ok
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
head :unprocessable_entity
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def inbox
|
||||
@inbox = current_account.inboxes.find_by(id: params[:inbox_id])
|
||||
end
|
||||
|
||||
def update_fb_page
|
||||
if fb_page(fb_page_id)
|
||||
fb_page.update_attributes!(
|
||||
user_access_token: @user_access_token, page_access_token: access_token
|
||||
)
|
||||
head :ok
|
||||
else
|
||||
head :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def fb_page(fb_page_id)
|
||||
current_account.facebook_pages.find_by(page_id: fb_page_id)
|
||||
end
|
||||
|
||||
def fb_object
|
||||
@user_access_token = long_lived_token(params[:omniauth_token])
|
||||
Koala::Facebook::API.new(@user_access_token)
|
||||
end
|
||||
|
||||
def long_lived_token(omniauth_token)
|
||||
koala = Koala::Facebook::OAuth.new(ENV['FB_APP_ID'], ENV['FB_APP_SECRET'])
|
||||
long_lived_token = koala.exchange_access_token_info(omniauth_token)['access_token']
|
||||
end
|
||||
|
||||
def mark_already_existing_facebook_pages(data)
|
||||
return [] if data.empty?
|
||||
|
||||
data.inject([]) do |result, page_detail|
|
||||
current_account.facebook_pages.exists?(page_id: page_detail['id']) ? page_detail.merge!(exists: true) : page_detail.merge!(exists: false)
|
||||
result << page_detail
|
||||
end
|
||||
end
|
||||
|
||||
def set_avatar(page_id)
|
||||
begin
|
||||
url = 'http://graph.facebook.com/' << page_id << '/picture?type=large'
|
||||
uri = URI.parse(url)
|
||||
tries = 3
|
||||
begin
|
||||
response = uri.open(redirect: false)
|
||||
rescue OpenURI::HTTPRedirect => e
|
||||
uri = e.uri # assigned from the "Location" response header
|
||||
retry if (tries -= 1) > 0
|
||||
raise
|
||||
end
|
||||
pic_url = response.base_uri.to_s
|
||||
Rails.logger.info(pic_url)
|
||||
rescue StandardError => e
|
||||
pic_url = nil
|
||||
end
|
||||
pic_url
|
||||
end
|
||||
end
|
||||
@@ -1,45 +0,0 @@
|
||||
class Api::V1::ContactsController < Api::BaseController
|
||||
protect_from_forgery with: :null_session
|
||||
|
||||
before_action :check_authorization
|
||||
before_action :fetch_contact, only: [:show, :update]
|
||||
|
||||
skip_before_action :authenticate_user!, only: [:create]
|
||||
skip_before_action :set_current_user, only: [:create]
|
||||
skip_before_action :check_subscription, only: [:create]
|
||||
skip_around_action :handle_with_exception, only: [:create]
|
||||
|
||||
def index
|
||||
@contacts = current_account.contacts
|
||||
end
|
||||
|
||||
def show; end
|
||||
|
||||
def create
|
||||
@contact = Contact.new(contact_create_params)
|
||||
@contact.save!
|
||||
render json: @contact
|
||||
end
|
||||
|
||||
def update
|
||||
@contact.update!(contact_params)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def check_authorization
|
||||
authorize(Contact)
|
||||
end
|
||||
|
||||
def contact_params
|
||||
params.require(:contact).permit(:name, :email, :phone_number)
|
||||
end
|
||||
|
||||
def fetch_contact
|
||||
@contact = current_account.contacts.find(params[:id])
|
||||
end
|
||||
|
||||
def contact_create_params
|
||||
params.require(:contact).permit(:account_id, :inbox_id).merge!(name: SecureRandom.hex)
|
||||
end
|
||||
end
|
||||
@@ -1,10 +0,0 @@
|
||||
class Api::V1::Conversations::AssignmentsController < Api::BaseController
|
||||
before_action :set_conversation, only: [:create]
|
||||
|
||||
def create # assign agent to a conversation
|
||||
# if params[:assignee_id] is not a valid id, it will set to nil, hence unassigning the conversation
|
||||
assignee = current_account.users.find_by(id: params[:assignee_id])
|
||||
@conversation.update_assignee(assignee)
|
||||
render json: assignee
|
||||
end
|
||||
end
|
||||
@@ -1,12 +0,0 @@
|
||||
class Api::V1::Conversations::LabelsController < Api::BaseController
|
||||
before_action :set_conversation, only: [:create, :index]
|
||||
|
||||
def create
|
||||
@conversation.update_labels(params[:labels])
|
||||
@labels = @conversation.label_list
|
||||
end
|
||||
|
||||
def index # all labels of the current conversation
|
||||
@labels = @conversation.label_list
|
||||
end
|
||||
end
|
||||
@@ -1,8 +0,0 @@
|
||||
class Api::V1::Conversations::MessagesController < Api::BaseController
|
||||
before_action :set_conversation, only: [:create]
|
||||
|
||||
def create
|
||||
mb = Messages::Outgoing::NormalBuilder.new(current_user, @conversation, params)
|
||||
@message = mb.perform
|
||||
end
|
||||
end
|
||||
@@ -1,52 +0,0 @@
|
||||
class Api::V1::ConversationsController < Api::BaseController
|
||||
before_action :set_conversation, except: [:index, :get_messages]
|
||||
|
||||
# TODO: move this to public controller
|
||||
skip_before_action :authenticate_user!, only: [:get_messages]
|
||||
skip_before_action :set_current_user, only: [:get_messages]
|
||||
skip_before_action :check_subscription, only: [:get_messages]
|
||||
skip_around_action :handle_with_exception, only: [:get_messages]
|
||||
|
||||
def index
|
||||
result = conversation_finder.perform
|
||||
@conversations = result[:conversations]
|
||||
@conversations_count = result[:count]
|
||||
end
|
||||
|
||||
def show
|
||||
@messages = messages_finder.perform
|
||||
end
|
||||
|
||||
def toggle_status
|
||||
@status = @conversation.toggle_status
|
||||
end
|
||||
|
||||
def update_last_seen
|
||||
@conversation.agent_last_seen_at = parsed_last_seen_at
|
||||
@conversation.save!
|
||||
head :ok
|
||||
end
|
||||
|
||||
def get_messages
|
||||
@conversation = Conversation.find(params[:id])
|
||||
@messages = messages_finder.perform
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def parsed_last_seen_at
|
||||
DateTime.strptime(params[:agent_last_seen_at].to_s, '%s')
|
||||
end
|
||||
|
||||
def set_conversation
|
||||
@conversation ||= current_account.conversations.find_by(display_id: params[:id])
|
||||
end
|
||||
|
||||
def conversation_finder
|
||||
@conversation_finder ||= ConversationFinder.new(current_user, params)
|
||||
end
|
||||
|
||||
def messages_finder
|
||||
@message_finder ||= MessageFinder.new(@conversation, params)
|
||||
end
|
||||
end
|
||||
@@ -1,55 +0,0 @@
|
||||
module Api
|
||||
module V1
|
||||
class InboxMembersController < Api::BaseController
|
||||
before_action :fetch_inbox, only: [:create, :show]
|
||||
before_action :current_agents_ids, only: [:create]
|
||||
|
||||
def create
|
||||
# update also done via same action
|
||||
if @inbox
|
||||
begin
|
||||
update_agents_list
|
||||
head :ok
|
||||
rescue StandardError => e
|
||||
Rails.logger.debug "Rescued: #{e.inspect}"
|
||||
render_could_not_create_error('Could not add agents to inbox')
|
||||
end
|
||||
else
|
||||
render_not_found_error('Agents or inbox not found')
|
||||
end
|
||||
end
|
||||
|
||||
def show
|
||||
@agents = current_account.users.where(id: @inbox.members.pluck(:user_id))
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def update_agents_list
|
||||
# get all the user_ids which the inbox currently has as members.
|
||||
# get the list of user_ids from params
|
||||
# the missing ones are the agents which are to be deleted from the inbox
|
||||
# the new ones are the agents which are to be added to the inbox
|
||||
|
||||
agents_to_be_added_ids.each { |user_id| @inbox.add_member(user_id) }
|
||||
agents_to_be_removed_ids.each { |user_id| @inbox.remove_member(user_id) }
|
||||
end
|
||||
|
||||
def agents_to_be_added_ids
|
||||
params[:user_ids] - @current_agents_ids
|
||||
end
|
||||
|
||||
def agents_to_be_removed_ids
|
||||
@current_agents_ids - params[:user_ids]
|
||||
end
|
||||
|
||||
def current_agents_ids
|
||||
@current_agents_ids = @inbox.members.pluck(:id)
|
||||
end
|
||||
|
||||
def fetch_inbox
|
||||
@inbox = current_account.inboxes.find(params[:inbox_id])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user